diff --git a/README.md b/README.md index 175a528..286dda6 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ # jest-tap-reporter -Jest retporter that outputs [TAP](https://testanything.org/tap-specification.html) results + +Jest reporter that outputs valid [TAP](https://testanything.org/tap-specification.html) output and highlights similar to Jest's default reporter. + +![jest-tap-reporter exaple](./docs/example.png) ## Installation @@ -16,6 +19,7 @@ npm install --dev jest-tap-reporter ## Usage #### Add to your jest configuration + ```javascript { "reporters": [ @@ -25,6 +29,7 @@ npm install --dev jest-tap-reporter ``` #### Log levels + By default jest-tap-reporter will log the suite path and a resume at the end of the report. If you reduce the report to the bare minimum you can set the reporter logLevel to error. ```javascript @@ -34,3 +39,5 @@ By default jest-tap-reporter will log the suite path and a resume at the end of ] } ``` + +Available log levels are: `ERROR`, `WARN`, `INFO`. \ No newline at end of file diff --git a/demo/.eslintrc.json b/demo/.eslintrc.json new file mode 100644 index 0000000..2379cfb --- /dev/null +++ b/demo/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "mailonline/jest" +} diff --git a/demo/__snapshots__/foo.test.js.snap b/demo/__snapshots__/foo.test.js.snap new file mode 100644 index 0000000..399da61 --- /dev/null +++ b/demo/__snapshots__/foo.test.js.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Foobar snapshot that always changes 1`] = ` +Object { + "time": 1510160275651, +} +`; + +exports[`Foobar static snapshot 1`] = `"static text"`; diff --git a/demo/foo.test.js b/demo/foo.test.js new file mode 100644 index 0000000..00dff5e --- /dev/null +++ b/demo/foo.test.js @@ -0,0 +1,35 @@ +test('foo succeeds', () => { + +}); + +test('bar fails', () => { + throw new Error('bar closed'); +}); + +// Successful no-namer test. +test('', () => {}); + +// Failed no-namer test. +// eslint-disable-next-line jest/no-identical-title +test('', () => { + throw new TypeError('Unknown error occured.'); +}); + +test('skipped test'); + +// eslint-disable-next-line jest/no-disabled-tests +xit('a sample todo test', () => {}); + +describe('Foobar', () => { + test('sample test that succeeds', () => {}); + + test('static snapshot', () => { + expect('static text').toMatchSnapshot(); + }); + + test('snapshot that always changes', () => { + expect({ + time: Date.now() + }).toMatchSnapshot(); + }); +}); diff --git a/demo/pass.test.js b/demo/pass.test.js new file mode 100644 index 0000000..4b26368 --- /dev/null +++ b/demo/pass.test.js @@ -0,0 +1 @@ +test('this is test suite that always passes', () => {}); diff --git a/docs/example.png b/docs/example.png new file mode 100644 index 0000000..2755533 Binary files /dev/null and b/docs/example.png differ diff --git a/package.json b/package.json index 31b50dd..ee01ac8 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "main": "index.js", "scripts": { "test": "jest", + "demo": "jest --testPathPattern 'demo/.+\\.test\\.js' --testRegex 'demo/.+\\.test\\.js'", "lint": "eslint --ignore-path .gitignore '**/*.js'", "precommit": "npm run lint", "prepush": "npm test", @@ -38,10 +39,11 @@ ], "reporters": [ ["./", {"logLevel": "INFO"}] - ] + ], + "testRegex": "test\\/.+\\.(test|spec)\\.jsx?$" }, "dependencies": { "chalk": "^2.3.0", - "ms": "^2.0.0" + "strip-ansi": "4.0.0" } } diff --git a/src/LineWriter.js b/src/LineWriter.js new file mode 100644 index 0000000..2c50b27 --- /dev/null +++ b/src/LineWriter.js @@ -0,0 +1,241 @@ +const path = require('path'); +const chalk = require('chalk'); + +const REG_TRACE_LINE = /\s*(.+)\((.+):([0-9]+):([0-9]+)\)$/; +const REG_INTERNALS = /^(node_modules|internal)\//; +const REG_AT = /^\s*at/; +const REG_ERROR = /^\s*Error:\s*/; + +const MDASH = '\u2014'; +const CIRCLE = '●'; + +const FAIL_TEXT = 'FAIL'; +const PASS_TEXT = 'PASS'; + +const FAIL = chalk.supportsColor ? + chalk`{reset.inverse.bold.red ${FAIL_TEXT} }` : + ` ${FAIL_TEXT} `; + +const PASS = chalk.supportsColor ? + chalk`{reset.inverse.bold.green ${PASS_TEXT} }` : + ` ${PASS_TEXT} `; + +const formatComment = (line) => chalk`{hidden #} ${line}`; +const formatFailureMessageTraceLine = (description, relativeFilePath, row, column) => + chalk`${description}({cyan ${relativeFilePath}}:{black.bold ${row}}:{black.bold ${column}})`; + +class LineWriter { + constructor (logger, root) { + this.counter = 0; + this.logger = logger; + this.root = root; + this.planWritten = false; + } + + getNextNumber () { + this.counter++; + + return this.counter; + } + + blank () { + this.logger.info(''); + } + + comment (line) { + this.logger.info(formatComment(line)); + } + + start (numSuites) { + this.blank(); + this.blank(); + this.comment(chalk`{green Starting...}`); + + if (numSuites) { + this.commentLight(`${numSuites} test suites found.`); + } + } + + commentLight (line) { + this.comment(chalk`{dim ${line}}`); + } + + keyValue (key, value) { + // eslint-disable-next-line no-use-extend-native/no-use-extend-native + const keyFormatted = (key + ':').padEnd(12, ' '); + + this.comment(chalk`{bold ${keyFormatted}} ${value}`); + } + + keyValueList (key, list) { + let value = ''; + + for (const [label, style, num] of list) { + value += (value ? ', ' : '') + chalk`{${style} ${num} ${label}}`; + } + + this.keyValue(key, value); + } + + stats (name, failed, skipped, passed, total) { + const list = []; + + if (total) { + if (failed) { + list.push(['failed', 'red.bold', failed]); + } + + if (skipped) { + list.push(['skipped', 'yellow.bold', skipped]); + } + + if (passed) { + list.push(['passed', 'green.bold', passed]); + } + } + + list.push(['total', 'reset', total]); + this.keyValueList(name, list); + } + + snapshots (failed, updated, added, passed, total) { + if (!total) { + return; + } + + const list = []; + + if (failed) { + list.push(['failed', 'red.bold', failed]); + } + + if (updated) { + list.push(['updated', 'yellow.bold', updated]); + } + + if (added) { + list.push(['added', 'green.bold', added]); + } + + if (passed) { + list.push(['passed', 'green.bold', passed]); + } + + list.push(['total', 'reset', total]); + + this.keyValueList('Snapshots', list); + } + + result (okNotOK, title) { + this.logger.log(okNotOK + chalk` {grey.dim ${this.getNextNumber()}} ${title}`); + } + + passed (title) { + this.result(chalk`{green ok}`, title ? `${MDASH} ${title}` : ''); + } + + failed (title) { + this.result(chalk`{red not ok}`, chalk`{red.bold ${CIRCLE} ${title}}`); + } + + pending (title) { + this.result(chalk`{yellow ok}`, chalk`{yellow #} {yellow.bold SKIP} ${title}`); + } + + getPathRelativeToRoot (filePath) { + return path.relative(this.root, filePath); + } + + formatFailureMessage (message) { + const [firstLine, ...lines] = message.split('\n'); + const outputLines = []; + const whitespace = ' '; + + const push = (line) => { + outputLines.push(line); + }; + const pushTraceLine = (line) => push(chalk` {grey ${line}}`); + const pushTraceLineDim = (line) => pushTraceLine(chalk`{dim ${line}}`); + + let firstLineFormatted = firstLine; + + // Remove leading `Error: ` + firstLineFormatted = firstLineFormatted.replace(REG_ERROR, ''); + + push(''); + push(firstLineFormatted); + push(''); + + let internalsStarted = false; + let isFirstTraceLine = true; + + for (const line of lines) { + if (line.match(REG_AT)) { + if (isFirstTraceLine) { + isFirstTraceLine = false; + + const isLastLineBlank = outputLines[outputLines.length - 1] === ''; + + if (!isLastLineBlank) { + push(''); + } + push(chalk`{bold.dim Stack trace:}`); + push(''); + } + + const matches = line.match(REG_TRACE_LINE); + + if (matches) { + const [, description, file, row, column] = matches; + const relativeFilePath = path.relative(this.root, file); + + if (relativeFilePath.match(REG_INTERNALS)) { + internalsStarted = true; + } + + // eslint-disable-next-line no-lonely-if + if (internalsStarted) { + pushTraceLineDim(formatFailureMessageTraceLine(description, relativeFilePath, row, column)); + } else { + pushTraceLine(formatFailureMessageTraceLine(description, relativeFilePath, row, column)); + } + } else { + pushTraceLine(line); + } + } else { + push(line); + } + } + + push(''); + + return outputLines.map((line) => formatComment(whitespace + line)).join('\n'); + } + + errors (messages) { + if (!messages.length) { + return; + } + + const formattedMessages = messages.map((message) => this.formatFailureMessage(message)).join('\n'); + + this.logger.error(formattedMessages); + } + + suite (isFail, dir, base) { + const label = isFail ? FAIL : PASS; + + this.comment(chalk`${label} {grey ${this.getPathRelativeToRoot(dir)}${path.sep}}{bold ${base}}`); + } + + plan (count = this.counter) { + if (this.planWritten) { + throw new Error('TAP test plan can be written only once.'); + } + + this.logger.log(chalk`{reset.inverse 1..${count}}`); + this.planWritten = true; + } +} + +module.exports = LineWriter; diff --git a/src/Logger.js b/src/Logger.js new file mode 100644 index 0000000..7c7c762 --- /dev/null +++ b/src/Logger.js @@ -0,0 +1,54 @@ +/* eslint-disable sort-keys */ +const LEVELS = { + ERROR: 1, + WARN: 2, + INFO: 3 +}; +/* eslint-enable sort-keys */ + +// eslint-disable-next-line no-console +const DEFAULT_LOG = console.log; +const sLevel = Symbol('level'); + +class Logger { + constructor ({log = DEFAULT_LOG, logLevel = 'INFO'} = {}) { + this.log = log; + this.setLevel(logLevel); + } + + setLevel (levelName) { + if (typeof levelName !== 'string') { + throw new TypeError('Level must be a string'); + } + + if (!LEVELS[levelName]) { + throw new Error('Unknown level'); + } + + this[sLevel] = LEVELS[levelName]; + } + + getLevel () { + return Object.keys(LEVELS).filter((key) => LEVELS[key] === this[sLevel])[0]; + } + + info (...args) { + if (this[sLevel] >= LEVELS.INFO) { + this.log(...args); + } + } + + warn (...args) { + if (this[sLevel] >= LEVELS.WARN) { + this.log(...args); + } + } + + error (...args) { + if (this[sLevel] >= LEVELS.ERROR) { + this.log(...args); + } + } +} + +module.exports = Logger; diff --git a/src/TapReporter.js b/src/TapReporter.js index 5084145..8dfeff8 100755 --- a/src/TapReporter.js +++ b/src/TapReporter.js @@ -1,24 +1,62 @@ /* eslint-disable id-match, class-methods-use-this, no-console */ const path = require('path'); const chalk = require('chalk'); -const ms = require('ms'); -const Logger = require('./helpers/Logger'); +const Logger = require('./Logger'); +const LineWriter = require('./LineWriter'); + +const STATUS_PASSED = 'passed'; +const STATUS_FAILED = 'failed'; +const STATUS_PENDING = 'pending'; + +const sShouldFail = Symbol('shouldFail'); class TapReporter { constructor (globalConfig = {}, options = {}) { const {logLevel = 'INFO'} = options; - this._globalConfig = globalConfig; - this._options = options; - this._shouldFail = false; - this._watch = this._globalConfig.watch; - this.logger = new Logger({ - logLevel - }); - this.counter = 0; - - this.logger.log('\n'); - this.logger.info('\n\n# Starting ...\n'); + this.globalConfig = globalConfig; + this.options = options; + this[sShouldFail] = false; + this.writer = new LineWriter(new Logger({logLevel}), globalConfig.rootDir); + this.onAssertionResult = this.onAssertionResult.bind(this); + + this.onRunStartResults = {}; + this.onRunStartOptions = {}; + } + + pathRelativeToRoot (filePath) { + return path.relative(this.globalConfig.rootDir, filePath); + } + + onAssertionResult (assertiontResult) { + const {ancestorTitles = [], failureMessages, title, status} = assertiontResult; + + let formattedTitle = status === STATUS_FAILED ? + chalk`{red ${title}}` : + chalk`{rgb(80,80,80) ${title}}`; + + formattedTitle = [...ancestorTitles, formattedTitle].join(' › '); + + switch (status) { + case STATUS_PASSED: + if (!this.globalConfig.watch) { + this.writer.passed(formattedTitle); + } + break; + case STATUS_FAILED: + this.writer.failed(formattedTitle); + this.writer.errors(failureMessages); + break; + case STATUS_PENDING: + this.writer.pending(formattedTitle); + this.writer.errors(failureMessages); + break; + default: + + // eslint-disable-next-line no-warning-comments + // TODO: add tests for this and reconsider in general what to do in the default case. + this.writer.commentLight(chalk`{italic Unknown status: ${status}}`); + } } onTestResult (contexts, suite) { @@ -26,73 +64,59 @@ class TapReporter { if (testFilePath) { const {dir, base} = path.parse(testFilePath); - const prefix = this._watch ? '' : '\n'; - const label = chalk[numFailingTests > 0 ? 'bgRed' : 'bgGreen'](` ${chalk.black('SUITE')} `); - this.logger.info(`${prefix}${chalk.grey('#')}${label} ${chalk.grey(`${dir}${path.sep}`)}${base}`); + if (!this.globalConfig.watch) { + this.writer.blank(); + } + this.writer.suite(numFailingTests > 0, dir, base); + this.writer.blank(); } - testResults.forEach((test) => { - this.counter += 1; - - if (test.status === 'passed') { - if (!this._watch) { - this.logger.log(`${chalk.green('ok')} ${this.counter} ${test.title}`); - } - } else if (test.status === 'failed') { - this.logger.log(`${chalk.red('not ok')} ${this.counter} ${test.title}`); - if (test.failureMessages.length > 0) { - const diagnostics = test.failureMessages - .reduce((lines, msg) => lines.concat(msg.split('\n')), []) - .map((line) => chalk.grey(`# ${line}`)) - .join('\n'); - - this.logger.error(diagnostics); - } - } else if (test.status === 'pending') { - this.logger.log(`${chalk.yellow('ok')} ${test.title} ${chalk.yellow('# SKIP')}`); - } - }); + testResults.forEach(this.onAssertionResult); } - onRunComplete (contexts, results) { - const { - numFailedTestSuites, - numFailedTests, - numPassedTestSuites, - numPassedTests, - numPendingTestSuites, - numPendingTests, - numTotalTestSuites, - numTotalTests, - startTime - } = results; - const skippedTestSuites = numPendingTestSuites > 0 ? `${chalk.yellow(`${numPendingTestSuites} skipped`)}, ` : ''; - const skippedTests = numPendingTests > 0 ? `${chalk.yellow(`${numPendingTests} skipped`)}, ` : ''; - - this._shouldFail = numFailedTestSuites > 0 || numFailedTests > 0; - - this.logger.info('\n'); - if (numFailedTestSuites > 0) { - this.logger.info(`# testSuites: ${skippedTestSuites}${chalk.red(`${numFailedTestSuites} failed`)}, ${numTotalTestSuites} total`); - } else { - this.logger.info(`# testSuites: ${skippedTestSuites}${chalk.green(`${numPassedTestSuites} passed`)}, ${numTotalTestSuites} total`); - } + onRunStart (results, options) { + this.onRunStartOptions = options; - if (numFailedTests > 0) { - this.logger.info(`# tests: ${skippedTests}${chalk.red(`${numFailedTests} failed`)}, ${numTotalTests} total`); - } else { - this.logger.info(`# tests: ${skippedTests}${chalk.green(`${numPassedTests} passed`)}, ${numTotalTests} total`); - } - - this.logger.info(`# time: ${ms(Date.now() - startTime)}`); - this.logger.info('\n'); + this.writer.start(results.numTotalTestSuites); + } - this.counter = 0; + onRunComplete (contexts, aggregatedResults) { + const {estimatedTime} = this.onRunStartOptions; + + const snapshotResults = aggregatedResults.snapshot; + const snapshotsAdded = snapshotResults.added; + const snapshotsFailed = snapshotResults.unmatched; + const snapshotsPassed = snapshotResults.matched; + const snapshotsTotal = snapshotResults.total; + const snapshotsUpdated = snapshotResults.updated; + const suitesFailed = aggregatedResults.numFailedTestSuites; + const suitesPassed = aggregatedResults.numPassedTestSuites; + const suitesPending = aggregatedResults.numPendingTestSuites; + const suitesTotal = aggregatedResults.numTotalTestSuites; + const testsFailed = aggregatedResults.numFailedTests; + const testsPassed = aggregatedResults.numPassedTests; + const testsPending = aggregatedResults.numPendingTests; + const testsTotal = aggregatedResults.numTotalTests; + const startTime = aggregatedResults.startTime; + + this[sShouldFail] = testsFailed > 0 || suitesFailed > 0; + + this.writer.blank(); + this.writer.plan(); + this.writer.blank(); + this.writer.stats('Test Suites', suitesFailed, suitesPending, suitesPassed, suitesTotal); + this.writer.stats('Tests', testsFailed, testsPending, testsPassed, testsTotal); + if (snapshotsTotal) { + this.writer.snapshots(snapshotsFailed, snapshotsUpdated, snapshotsAdded, snapshotsPassed, snapshotsTotal); + } + this.writer.keyValue('Time', `${((Date.now() - startTime) / 1e3).toFixed(3)}s` + (estimatedTime ? `, estimated ${estimatedTime}s` : '')); + this.writer.commentLight('Ran all test suites.'); + this.writer.blank(); } getLastError () { - if (this._shouldFail) { + if (this[sShouldFail]) { return new Error('TAP Reporter: failing tests found'); } diff --git a/src/helpers/Logger.js b/src/helpers/Logger.js deleted file mode 100644 index f380b1f..0000000 --- a/src/helpers/Logger.js +++ /dev/null @@ -1,50 +0,0 @@ -/* eslint-disable sort-keys */ -const LEVELS = { - ERROR: 1, - WARN: 2, - INFO: 3 -}; -/* eslint-enable sort-keys */ - -// eslint-disable-next-line no-console -const DEFAULT_LOG = (...args) => console.log(...args); -const level = Symbol('level'); - -class Logger { - constructor ({log = DEFAULT_LOG, logLevel} = {}) { - this.log = log; - this.setLevel(logLevel || 'INFO'); - } - - setLevel (newLevel) { - if (typeof LEVELS[newLevel] === 'undefined') { - throw new TypeError('Unknown level'); - } - - this[level] = LEVELS[newLevel]; - } - - getLevel () { - return Object.keys(LEVELS).filter((key) => LEVELS[key] === this[level])[0]; - } - - info (...args) { - if (this[level] >= LEVELS.INFO) { - this.log(...args); - } - } - - warn (...args) { - if (this[level] >= LEVELS.WARN) { - this.log(...args); - } - } - - error (...args) { - if (this[level] >= LEVELS.ERROR) { - this.log(...args); - } - } -} - -module.exports = Logger; diff --git a/test/LineWriter.spec.js b/test/LineWriter.spec.js new file mode 100644 index 0000000..a36d521 --- /dev/null +++ b/test/LineWriter.spec.js @@ -0,0 +1,491 @@ +/* eslint-disable max-nested-callbacks */ +const chalk = require('chalk'); +const Logger = require('../src/Logger'); +const LineWriter = require('../src/LineWriter'); + +jest.mock('chalk'); +jest.mock('../src/Logger'); + +const create = (root = '/jest-tap-reporter', logger = new Logger()) => { + const writer = new LineWriter(logger, root); + + return writer; +}; + +const MDASH = '\u2014'; +const CIRCLE = '●'; + +describe('LineWriter', () => { + beforeEach(() => { + chalk.__stripColors(); + }); + + test('is a function', () => { + expect(typeof LineWriter).toBe('function'); + }); + + test('can construct without error', () => { + create(); + }); + + describe('.getNextNumber()', () => { + test('returns a positive number', () => { + const writer = create(); + const num = writer.getNextNumber(); + + expect(typeof num).toBe('number'); + expect(num).toBeGreaterThan(0); + }); + + test('starts with 1', () => { + const writer = create(); + const num = writer.getNextNumber(); + + expect(num).toBe(1); + }); + + test('each next number increases by 1', () => { + const writer = create(); + let last = writer.getNextNumber(); + + for (let index = 0; index < 100; index++) { + const current = writer.getNextNumber(); + + expect(last + 1).toBe(current); + last = current; + } + }); + }); + + describe('.blank()', () => { + test('logs a blank line using .info()', () => { + const writer = create(); + + writer.blank(); + + expect(writer.logger.info).toHaveBeenCalledTimes(1); + expect(writer.logger.info.mock.calls[0]).toEqual(['']); + }); + }); + + describe('.comment()', () => { + test('logs using .info()', () => { + const writer = create(); + + writer.comment('something'); + + expect(writer.logger.info).toHaveBeenCalledTimes(1); + }); + + test('writes a TAP comment', () => { + chalk.__stripColors(); + + const writer = create(); + + writer.comment('foo'); + + expect(writer.logger.info).toHaveBeenCalledTimes(1); + expect(writer.logger.info.mock.calls[0]).toEqual(['# foo']); + }); + + test('hides comment hash symbol from console', () => { + chalk.__showTemplates(); + + const writer = create(); + + writer.comment('foo'); + + expect(writer.logger.info).toHaveBeenCalledTimes(1); + expect(writer.logger.info.mock.calls[0]).toEqual(['{hidden #} foo']); + }); + }); + + describe('.start()', () => { + test('prints start message', () => { + chalk.__stripColors(); + + const writer = create(); + + writer.start(); + + expect(writer.logger.info.mock.calls).toMatchSnapshot(); + }); + + test('prints start message with suite number if provided', () => { + chalk.__stripColors(); + + const writer = create(); + + writer.start(3); + + expect(writer.logger.info.mock.calls).toMatchSnapshot(); + }); + }); + + describe('.commentLight()', () => { + test('prints dimmed comment', () => { + const writer = create(); + + writer.comment = jest.fn(); + writer.commentLight('foo'); + + expect(writer.comment.mock.calls).toMatchSnapshot(); + }); + }); + + describe('.keyValue()', () => { + test('prints key-value pair', () => { + const writer = create(); + + writer.comment = jest.fn(); + writer.keyValue('foo', 'bar'); + + expect(writer.comment.mock.calls).toMatchSnapshot(); + }); + }); + + describe('.keyValueList()', () => { + test('formats 3-tuple list into a value and calls .keyValue()', () => { + chalk.__showTemplates(); + + const writer = create(); + + writer.keyValue = jest.fn(); + writer.keyValueList('foo', [ + ['name1', 'styles1', 1], + ['name2', 'styles2', 2], + ['name3', 'styles3', 3], + ['name4', 'styles4', 4] + ]); + + expect(writer.keyValue).toHaveBeenCalledTimes(1); + expect(writer.keyValue.mock.calls[0]).toMatchSnapshot(); + }); + }); + + describe('.stats()', () => { + describe('when zero tests', () => { + test('shows only total zero', () => { + const writer = create(); + + writer.keyValue = jest.fn(); + writer.stats('foo', 0, 0, 0, 0); + + expect(writer.keyValue).toHaveBeenCalledTimes(1); + expect(writer.keyValue.mock.calls[0]).toMatchSnapshot(); + }); + }); + + describe('when all tests pass', () => { + test('shows only passed and total tests', () => { + const writer = create(); + + writer.keyValue = jest.fn(); + writer.stats('foo', 0, 0, 1, 1); + + expect(writer.keyValue).toHaveBeenCalledTimes(1); + expect(writer.keyValue.mock.calls[0]).toMatchSnapshot(); + }); + }); + + describe('when some tests fail', () => { + test('shows only passed, failed and total tests', () => { + const writer = create(); + + writer.keyValue = jest.fn(); + writer.stats('foo', 1, 0, 1, 2); + + expect(writer.keyValue).toHaveBeenCalledTimes(1); + expect(writer.keyValue.mock.calls[0]).toMatchSnapshot(); + }); + }); + + describe('when some tests are skipped', () => { + test('shows all items', () => { + const writer = create(); + + writer.keyValue = jest.fn(); + writer.stats('foo', 1, 1, 1, 3); + + expect(writer.keyValue).toHaveBeenCalledTimes(1); + expect(writer.keyValue.mock.calls[0]).toMatchSnapshot(); + }); + + describe('and no tests fail', () => { + test('shows only passed, skipped and total tests', () => { + const writer = create(); + + writer.keyValue = jest.fn(); + writer.stats('foo', 0, 1, 1, 2); + + expect(writer.keyValue).toHaveBeenCalledTimes(1); + expect(writer.keyValue.mock.calls[0]).toMatchSnapshot(); + }); + }); + }); + }); + + describe('.snapshots()', () => { + describe('when no snapshots exit', () => { + test('should not print anything', () => { + const writer = create(); + + writer.keyValue = jest.fn(); + writer.snapshots(0, 0, 0, 0, 0); + + expect(writer.keyValue).toHaveBeenCalledTimes(0); + }); + }); + + describe('when all values are greater than zero', () => { + test('prints them all', () => { + const writer = create(); + + writer.keyValue = jest.fn(); + writer.snapshots(1, 1, 1, 1, 4); + + expect(writer.keyValue).toHaveBeenCalledTimes(1); + expect(writer.keyValue.mock.calls[0]).toMatchSnapshot(); + }); + }); + }); + + describe('.result()', () => { + test('logs passed test', () => { + const writer = create(); + + writer.logger.log = jest.fn(); + writer.result('ok', 'Test passed'); + + expect(writer.logger.log).toHaveBeenCalledTimes(1); + expect(writer.logger.log.mock.calls[0][0]).toBe('ok 1 Test passed'); + }); + + test('logs failed test', () => { + const writer = create(); + + writer.logger.log = jest.fn(); + writer.result('not ok', 'Test failed'); + + expect(writer.logger.log).toHaveBeenCalledTimes(1); + expect(writer.logger.log.mock.calls[0][0]).toBe('not ok 1 Test failed'); + }); + + test('increments test counter', () => { + const writer = create(); + + writer.logger.log = jest.fn(); + + writer.result('ok', 'Test passed'); + writer.result('not ok', 'Test failed'); + + expect(writer.logger.log).toHaveBeenCalledTimes(2); + + expect(writer.logger.log.mock.calls[0][0]).toBe('ok 1 Test passed'); + expect(writer.logger.log.mock.calls[1][0]).toBe('not ok 2 Test failed'); + }); + }); + + describe('.passed()', () => { + test('calls .result() with the right parameters', () => { + chalk.__stripColors(); + + const writer = create(); + + writer.result = jest.fn(); + writer.passed('Test passed'); + + expect(writer.result).toHaveBeenCalledTimes(1); + expect(writer.result.mock.calls[0]).toEqual(['ok', `${MDASH} Test passed`]); + }); + + test('colors "ok" green', () => { + chalk.__showTemplates(); + + const writer = create(); + + writer.result = jest.fn(); + writer.passed('Test passed'); + + expect(writer.result).toHaveBeenCalledTimes(1); + expect(writer.result.mock.calls[0][0]).toMatchSnapshot(); + }); + }); + + describe('.failed()', () => { + test('calls .result() with the right parameters', () => { + chalk.__stripColors(); + + const writer = create(); + + writer.result = jest.fn(); + writer.failed('Test failed'); + + expect(writer.result).toHaveBeenCalledTimes(1); + expect(writer.result.mock.calls[0]).toEqual(['not ok', `${CIRCLE} Test failed`]); + }); + + test('colors "not ok" red', () => { + chalk.__showTemplates(); + + const writer = create(); + + writer.result = jest.fn(); + writer.failed('Test failed'); + + expect(writer.result).toHaveBeenCalledTimes(1); + expect(writer.result.mock.calls[0][0]).toMatchSnapshot(); + }); + }); + + describe('.pending()', () => { + test('calls .result() with the right parameters', () => { + chalk.__stripColors(); + + const writer = create(); + + writer.result = jest.fn(); + writer.pending('Test pending'); + + expect(writer.result).toHaveBeenCalledTimes(1); + expect(writer.result.mock.calls[0]).toEqual(['ok', '# SKIP Test pending']); + }); + + test('colors "ok" yellow', () => { + chalk.__showTemplates(); + + const writer = create(); + + writer.result = jest.fn(); + writer.pending('Test pending'); + + expect(writer.result).toHaveBeenCalledTimes(1); + expect(writer.result.mock.calls[0][0]).toMatchSnapshot(); + }); + }); + + describe('.getPathRelativeToRoot()', () => { + test('returns path relative to root folder', () => { + const writer = create('/foo'); + const rel = writer.getPathRelativeToRoot('/foo/bar'); + + expect(rel).toBe('bar'); + }); + }); + + describe('.errors()', () => { + test('logs using STDERR', () => { + const writer = create(); + + writer.logger.error = jest.fn(); + writer.errors(['Error: foobar']); + + expect(writer.logger.error).toHaveBeenCalledTimes(1); + }); + + test('strips "Error:" leading text', () => { + chalk.__stripColors(); + + const writer = create(); + + writer.logger.error = jest.fn(); + writer.errors([ + 'Error: foobar' + + '\n' + + 'at Something (/foo/bar.js:10:10)' + ]); + + expect(writer.logger.error).toHaveBeenCalledTimes(1); + expect(writer.logger.error.mock.calls[0][0].includes('Error:')).toBe(false); + expect(writer.logger.error.mock.calls[0][0].includes('foobar')).toBe(true); + }); + + test('format stack trace', () => { + chalk.__stripColors(); + + const writer = create(); + + writer.logger.error = jest.fn(); + writer.errors([ + 'Error: foobar' + + '\n' + + 'at Something (/foo/bar.js:10:10)' + + '\n' + + 'at Foobar (/foo/bar2.js:20:20)' + ]); + + expect(writer.logger.error).toHaveBeenCalledTimes(1); + expect(writer.logger.error.mock.calls[0][0]).not.toMatch('.*Stack trace.*'); + expect(writer.logger.error.mock.calls[0][0]).toMatchSnapshot(); + }); + }); + + describe('.suite()', () => { + test('prints test suite result', () => { + chalk.__stripColors(); + + const writer = create('/foo'); + + writer.comment = jest.fn(); + writer.suite(true, '/foo/bar/', 'kappa.js'); + + expect(writer.comment).toHaveBeenCalledTimes(1); + expect(writer.comment.mock.calls[0][0]).toMatchSnapshot(); + }); + + test('makes directory path relative', () => { + chalk.__stripColors(); + + const writer = create('/foo'); + + writer.comment = jest.fn(); + writer.suite(true, '/foo/bar/', 'kappa.js'); + + expect(writer.comment).toHaveBeenCalledTimes(1); + + const line = writer.comment.mock.calls[0][0]; + + expect(line.includes('/foo/bar/kappa.js')).toBe(false); + expect(line.includes('bar/kappa.js')).toBe(true); + }); + }); + + describe('.plan()', () => { + test('prints test plan with supplied test count', () => { + chalk.__stripColors(); + + const writer = create(); + + writer.logger.log = jest.fn(); + writer.plan(100); + + expect(writer.logger.log).toHaveBeenCalledTimes(1); + expect(writer.logger.log.mock.calls[0][0]).toBe('1..100'); + }); + + test('can be written only once', () => { + const writer = create(); + + writer.logger.log = jest.fn(); + writer.plan(100); + expect(() => writer.plan(100)).toThrow(Error); + }); + + describe('when test count is not provided', () => { + test('prints test plan using the current counter as test count', () => { + chalk.__stripColors(); + + const writer = create(); + + writer.logger.log = jest.fn(); + writer.result('ok'); + writer.result('ok'); + writer.result('ok'); + writer.plan(); + + expect(writer.logger.log).toHaveBeenCalledTimes(4); + expect(writer.logger.log.mock.calls[3][0]).toBe('1..3'); + }); + }); + }); +}); diff --git a/test/Logger.spec.js b/test/Logger.spec.js new file mode 100644 index 0000000..660cd23 --- /dev/null +++ b/test/Logger.spec.js @@ -0,0 +1,138 @@ +const Logger = require('../src/Logger'); + +describe('Logger', () => { + test('must set the INFO as the default level', () => { + const logger = new Logger(); + + expect(logger.getLevel()).toBe('INFO'); + }); + + /* eslint-disable no-console */ + test('must use console.log as default log function', () => { + const logger = new Logger(); + + expect(logger.log).toBe(console.log); + }); + /* eslint-enable no-console */ + + test('must be possible to pass a log function', () => { + const log = jest.fn(); + const logger = new Logger({log}); + + logger.log('foo', 'bar'); + + expect(log).toHaveBeenCalledTimes(1); + expect(log).toHaveBeenCalledWith('foo', 'bar'); + }); + + test('must be possible to pass a default log level', () => { + const config = { + logLevel: 'ERROR' + }; + const logger = new Logger(config); + + expect(logger.getLevel()).toBe('ERROR'); + }); + + test('must be possible to change the log level', () => { + const logger = new Logger(); + + expect(logger.getLevel()).toBe('INFO'); + logger.setLevel('ERROR'); + expect(logger.getLevel()).toBe('ERROR'); + logger.setLevel('WARN'); + expect(logger.getLevel()).toBe('WARN'); + }); + + test('must throw if you try to change the level with an unknown one', () => { + const logger = new Logger(); + + expect(() => logger.setLevel('asfasdfsadf')).toThrow(Error); + expect(() => logger.setLevel(23423)).toThrow(TypeError); + expect(() => logger.setLevel()).toThrow(TypeError); + }); + + test('warn log must log no matter the log level', () => { + const log = jest.fn(); + const logger = new Logger({ + log, + logLevel: 'INFO' + }); + + logger.log('INFO'); + expect(log).toHaveBeenCalledWith('INFO'); + + logger.setLevel('WARN'); + logger.log('WARN'); + expect(log).toHaveBeenCalledWith('WARN'); + + logger.setLevel('ERROR'); + logger.log('ERROR'); + expect(log).toHaveBeenCalledWith('ERROR'); + + expect(log).toHaveBeenCalledTimes(3); + }); + + test('info must log if log level is INFO', () => { + const log = jest.fn(); + const logger = new Logger({ + log, + logLevel: 'ERROR' + }); + + logger.info('test'); + expect(log).not.toHaveBeenCalled(); + + logger.setLevel('WARN'); + logger.info('test'); + expect(log).not.toHaveBeenCalled(); + + logger.setLevel('INFO'); + logger.info('test'); + expect(log).toHaveBeenCalledWith('test'); + + expect(log).toHaveBeenCalledTimes(1); + }); + + test('WARN must log if log level is INFO or WARN', () => { + const log = jest.fn(); + const logger = new Logger({ + log, + logLevel: 'ERROR' + }); + + logger.warn('test'); + expect(log).not.toHaveBeenCalled(); + + logger.setLevel('WARN'); + logger.warn('test'); + expect(log).toHaveBeenCalledWith('test'); + + logger.setLevel('INFO'); + logger.warn('test2'); + expect(log).toHaveBeenCalledWith('test2'); + + expect(log).toHaveBeenCalledTimes(2); + }); + + test('ERROR must log if log level is INFO or WARN or ERROR', () => { + const log = jest.fn(); + const logger = new Logger({ + log, + logLevel: 'ERROR' + }); + + logger.error('test'); + expect(log).toHaveBeenCalledWith('test'); + + logger.setLevel('WARN'); + logger.error('test2'); + expect(log).toHaveBeenCalledWith('test2'); + + logger.setLevel('INFO'); + logger.error('test3'); + expect(log).toHaveBeenCalledWith('test3'); + + expect(log).toHaveBeenCalledTimes(3); + }); +}); diff --git a/test/TapReporter.spec.js b/test/TapReporter.spec.js index 553f6d6..6dc056a 100644 --- a/test/TapReporter.spec.js +++ b/test/TapReporter.spec.js @@ -1,4 +1,5 @@ -/* eslint-disable no-console */ +/* eslint-disable no-console, max-nested-callbacks */ +const chalk = require('chalk'); const TapReporter = require('../src/TapReporter'); const { failingTestSuite, @@ -7,343 +8,195 @@ const { skippedTestSuite } = require('./fixtures'); -jest.mock('chalk', () => ({ - bgBlue: (str) => str, - bgGreen: (str) => str, - bgRed: (str) => str, - black: (str) => str, - green: (str) => str, - grey: (str) => str, - red: (str) => str, - yellow: (str) => str -})); - -let origLog; - -const string = { - any: expect.stringMatching(/.*/), - empty: expect.stringMatching(/(^$)|(\s+$)/), - // eslint-disable-next-line no-useless-escape - startsWith: (query) => expect.stringMatching(new RegExp('^' + query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'))) -}; - -const processTestLine = (testLine) => { - const parts = testLine.split(' '); - const isSuccess = testLine.indexOf('not') !== 0; - const status = isSuccess ? parts[0] : `${parts[0]} ${parts[1]}`; - const hasDirective = testLine.indexOf('# SKIP') >= 0; - const descriptionStartIdx = isSuccess ? 2 : 3; - - let description; - let directive; - let diagnostics; - - if (hasDirective) { - const directiveStartIdx = parts.indexOf('#'); - - description = parts.slice(descriptionStartIdx, directiveStartIdx).join(' '); - directive = parts.slice(directiveStartIdx).join(' '); - } else { - description = parts.slice(descriptionStartIdx).join(' '); - directive = null; - } - - const descriptionsParts = description.split('\n'); - - if (descriptionsParts.length > 1) { - description = descriptionsParts[0]; - diagnostics = descriptionsParts.slice(1); - } else { - diagnostics = null; - } - - return { - description, - diagnostics, - directive, - status - }; -}; - -beforeEach(() => { - origLog = console.log; - console.log = jest.fn(); -}); - -afterEach(() => { - console.log = origLog; -}); - -test('TapReporter must publish the globalConfig and the options', () => { - const globalConfig = {}; - const options = {}; - const tapReporter = new TapReporter(globalConfig, options); - - expect(tapReporter._globalConfig).toBe(globalConfig); - expect(tapReporter._options).toBe(options); -}); +jest.mock('chalk'); +jest.mock('../src/LineWriter'); -test('TapReporter must set _shouldFail to false by default', () => { - const tapReporter = new TapReporter(); - - expect(tapReporter._shouldFail).toBe(false); -}); - -test('TapReporter must log the start of the tests', () => { - // eslint-disable-next-line no-unused-vars - const tapReporter = new TapReporter(); - - expect(console.log).toHaveBeenCalledTimes(2); - expect(console.log).toHaveBeenCalledWith('\n'); - expect(console.log).toHaveBeenCalledWith('\n\n# Starting ...\n'); -}); +describe('TapReporter', () => { + test('must publish the globalConfig and the options', () => { + const globalConfig = {}; + const options = {}; + const tapReporter = new TapReporter(globalConfig, options); -test('TapReporter onTestResults must output error tests', () => { - const tapReporter = new TapReporter(); - - console.log.mockClear(); - - tapReporter.onTestResult({}, failingTestSuite); - - const { - description, - directive, - status - } = processTestLine(console.log.mock.calls[0][0]); - - const { - diagnostics - } = processTestLine(console.log.mock.calls[1][0]); - - expect(status).toBe('not ok'); - expect(description).not.toBe(string.notEmpty); - expect(directive).toBeNull(); - expect(diagnostics.length > 0).toBe(true); - - diagnostics.forEach((diagnosticsLine) => { - expect(diagnosticsLine).toEqual(string.startsWith('# ')); + expect(tapReporter.globalConfig).toBe(globalConfig); + expect(tapReporter.options).toBe(options); }); -}); - -test('TapReporter onTestResults must output passing tests', () => { - const tapReporter = new TapReporter(); - - console.log.mockClear(); - - tapReporter.onTestResult({}, passingTestSuite); - const { - description, - diagnostics, - directive, - status - } = processTestLine(console.log.mock.calls[1][0]); + test('must log the start of the tests', () => { + const tapReporter = new TapReporter(); - expect(status).toBe('ok'); - expect(description).not.toBe(string.notEmpty); - expect(directive).toBeNull(); - expect(diagnostics).toBeNull(); -}); - -test('TapReporter must output a Suite log with the Suites filePath if possible', () => { - let tapReporter = new TapReporter(); + tapReporter.onRunStart({}, {}); - console.log.mockClear(); + expect(tapReporter.writer.start).toHaveBeenCalledTimes(1); + expect(tapReporter.writer.start).not.toHaveBeenCalledTimes(2); + }); - tapReporter.onTestResult({}, passingTestSuite); + test('getLastError must return an error the run should fail and undefined otherwise', () => { + let tapReporter = new TapReporter(); + const results = { + numFailedTests: 1, + numFailedTestSuites: 0, + numPassedTests: 0, + numPassedTestSuites: 0, + numPendingTests: 0, + numPendingTestSuites: 0, + numTotalTests: 0, + numTotalTestSuites: 0, + snapshot: {}, + startTime: Date.now() - 2000 + }; + + tapReporter.onRunComplete({}, results); + expect(tapReporter.getLastError()).toBeInstanceOf(Error); + + tapReporter = new TapReporter(); + expect(tapReporter.getLastError()).toBe(undefined); + }); - expect(console.log.mock.calls[0][0]).toBe('\n# SUITE /Users/carlospastor/dev/mailonline/jest-tap-reporter/TapReporter.spec.js'); - tapReporter = new TapReporter(); + describe('onTestResults', () => { + test('must output error tests', () => { + chalk.__stripColors(); + const tapReporter = new TapReporter(); - console.log.mockClear(); + tapReporter.onTestResult({}, failingTestSuite); - tapReporter.onTestResult({}, failingTestSuite); + expect(tapReporter.writer.failed).toHaveBeenCalledTimes(1); + expect(tapReporter.writer.failed.mock.calls).toMatchSnapshot(); + }); - expect(console.log.mock.calls[0][0]).not.toEqual(string.startsWith('\n# SUITE')); -}); + test('must output passing tests', () => { + chalk.__stripColors(); + const tapReporter = new TapReporter(); -test('TapReporter onTestResults must output skipped tests', () => { - const tapReporter = new TapReporter(); + tapReporter.onTestResult({}, passingTestSuite); - console.log.mockClear(); + expect(tapReporter.writer.passed).toHaveBeenCalledTimes(1); + expect(tapReporter.writer.passed.mock.calls).toMatchSnapshot(); + }); - tapReporter.onTestResult({}, skippedTestSuite); + test('must output skipped tests', () => { + chalk.__stripColors(); + const tapReporter = new TapReporter(); - expect(console.log).toHaveBeenCalledTimes(1); + tapReporter.onTestResult({}, skippedTestSuite); - const { - description, - diagnostics, - directive, - status - } = processTestLine(console.log.mock.calls[0][0]); + expect(tapReporter.writer.pending).toHaveBeenCalledTimes(1); + expect(tapReporter.writer.pending.mock.calls).toMatchSnapshot(); + }); - expect(status).toBe('ok'); - expect(description).not.toBe(string.notEmpty); - expect(directive).toBe('# SKIP'); - expect(diagnostics).toBeNull(); -}); + test('must output all the tests on a suite tests', () => { + chalk.__stripColors(); + const tapReporter = new TapReporter(); -test('TapReporter onTestResults must output all the tests on a suite tests', () => { - const tapReporter = new TapReporter(); + tapReporter.onTestResult({}, severalTestsSuite); - console.log.mockClear(); + expect(tapReporter.writer.passed.mock.calls).toMatchSnapshot(); + expect(tapReporter.writer.failed.mock.calls).toMatchSnapshot(); + expect(tapReporter.writer.pending.mock.calls).toMatchSnapshot(); + }); - tapReporter.onTestResult({}, severalTestsSuite); + describe('suite log', () => { + test('must output a suite log with the Suites filePath if possible', () => { + const tapReporter = new TapReporter(); - const testLines = console.log.mock.calls.map((call) => call[0]); + tapReporter.onTestResult({}, passingTestSuite); - testLines.forEach((testLine) => { - const { - description, - directive, - status - } = processTestLine(testLine); - - expect(status).toBe('ok'); - expect(description).not.toBe(string.notEmpty); - expect(directive).toBeNull(); + expect(tapReporter.writer.suite).toHaveBeenCalledTimes(1); + expect(tapReporter.writer.suite.mock.calls).toMatchSnapshot(); + }); + }); }); -}); -test('TapReporter onRunComplete must set _shouldFail to true if a suite failed', () => { - const tapReporter = new TapReporter(); - const results = { - numFailedTests: 0, - numFailedTestSuites: 1, - numPassedTests: 0, - numPassedTestSuites: 0, - numPendingTests: 0, - numPendingTestSuites: 0, - numTotalTests: 0, - numTotalTestSuites: 0, - startTime: Date.now() - 2000 - }; - - tapReporter.onRunComplete({}, results); - expect(tapReporter._shouldFail).toBe(true); -}); - -test('TapReporter onRunComplete must set _shouldFail to true if a a test failed', () => { - const tapReporter = new TapReporter(); - const results = { - numFailedTests: 1, - numFailedTestSuites: 0, - numPassedTests: 0, - numPassedTestSuites: 0, - numPendingTests: 0, - numPendingTestSuites: 0, - numTotalTests: 0, - numTotalTestSuites: 0, - startTime: Date.now() - 2000 - }; - - tapReporter.onRunComplete({}, results); - expect(tapReporter._shouldFail).toBe(true); -}); - -test('TapReporter onRunComplete all suites and tests pass', () => { - const tapReporter = new TapReporter(); - const results = { - numFailedTests: 0, - numFailedTestSuites: 0, - numPassedTests: 10, - numPassedTestSuites: 2, - numPendingTests: 0, - numPendingTestSuites: 0, - numTotalTests: 10, - numTotalTestSuites: 2, - startTime: Date.now() - 2000 - }; - - tapReporter.onRunComplete({}, results); - - expect(console.log).toHaveBeenCalledWith('# testSuites: 2 passed, 2 total'); - expect(console.log).toHaveBeenCalledWith('# tests: 10 passed, 10 total'); - expect(console.log).toHaveBeenCalledWith('# time: 2s'); -}); - -test('TapReporter onRunComplete some suites and tests fail', () => { - const tapReporter = new TapReporter(); - const results = { - numFailedTests: 1, - numFailedTestSuites: 1, - numPassedTests: 10, - numPassedTestSuites: 2, - numPendingTests: 0, - numPendingTestSuites: 0, - numTotalTests: 10, - numTotalTestSuites: 2, - startTime: Date.now() - 2000 - }; - - tapReporter.onRunComplete({}, results); - - expect(console.log).toHaveBeenCalledWith('# testSuites: 1 failed, 2 total'); - expect(console.log).toHaveBeenCalledWith('# tests: 1 failed, 10 total'); - expect(console.log).toHaveBeenCalledWith('# time: 2s'); -}); - -test('TapReporter onRunComplete 1 suite failed to execute', () => { - const tapReporter = new TapReporter(); - const results = { - numFailedTests: 0, - numFailedTestSuites: 1, - numPassedTests: 10, - numPassedTestSuites: 1, - numPendingTests: 0, - numPendingTestSuites: 0, - numTotalTests: 10, - numTotalTestSuites: 2, - startTime: Date.now() - 2000 - }; - - tapReporter.onRunComplete({}, results); - - expect(console.log).toHaveBeenCalledWith('# testSuites: 1 failed, 2 total'); - expect(console.log).toHaveBeenCalledWith('# tests: 10 passed, 10 total'); - expect(console.log).toHaveBeenCalledWith('# time: 2s'); -}); - -test('TapReporter onRunComplete some suites and tests skipped', () => { - const tapReporter = new TapReporter(); - const results = { - numFailedTests: 0, - numFailedTestSuites: 0, - numPassedTests: 5, - numPassedTestSuites: 1, - numPendingTests: 5, - numPendingTestSuites: 1, - numTotalTests: 10, - numTotalTestSuites: 2, - startTime: Date.now() - 2000 - }; - - tapReporter.onRunComplete({}, results); - - expect(console.log).toHaveBeenCalledWith('# testSuites: 1 skipped, 1 passed, 2 total'); - expect(console.log).toHaveBeenCalledWith('# tests: 5 skipped, 5 passed, 10 total'); - expect(console.log).toHaveBeenCalledWith('# time: 2s'); -}); - -test('TapReporter getLastError must return an error the run should fail and undefined otherwise', () => { - let tapReporter = new TapReporter(); - const results = { - numFailedTests: 1, - numFailedTestSuites: 0, - numPassedTests: 0, - numPassedTestSuites: 0, - numPendingTests: 0, - numPendingTestSuites: 0, - numTotalTests: 0, - numTotalTestSuites: 0, - startTime: Date.now() - 2000 - }; - - console.log.mockClear(); - tapReporter.onRunComplete({}, results); - expect(tapReporter.getLastError()).toBeInstanceOf(Error); - - tapReporter = new TapReporter(); - expect(tapReporter.getLastError()).toBe(undefined); + describe('onRunComplete', () => { + test('all suites and tests pass', () => { + const tapReporter = new TapReporter(); + const results = { + numFailedTests: 0, + numFailedTestSuites: 0, + numPassedTests: 10, + numPassedTestSuites: 2, + numPendingTests: 0, + numPendingTestSuites: 0, + numTotalTests: 10, + numTotalTestSuites: 2, + snapshot: {}, + startTime: Date.now() - 2000 + }; + + tapReporter.onRunComplete({}, results); + + expect(tapReporter.writer.stats.mock.calls).toEqual([ + ['Test Suites', 0, 0, 2, 2], + ['Tests', 0, 0, 10, 10] + ]); + }); + + test('some suites and tests fail', () => { + const tapReporter = new TapReporter(); + const results = { + numFailedTests: 1, + numFailedTestSuites: 1, + numPassedTests: 10, + numPassedTestSuites: 2, + numPendingTests: 0, + numPendingTestSuites: 0, + numTotalTests: 10, + numTotalTestSuites: 2, + snapshot: {}, + startTime: Date.now() - 2000 + }; + + tapReporter.onRunComplete({}, results); + + expect(tapReporter.writer.stats.mock.calls).toEqual([ + ['Test Suites', 1, 0, 2, 2], + ['Tests', 1, 0, 10, 10] + ]); + }); + + test('1 suite failed to execute', () => { + const tapReporter = new TapReporter(); + const results = { + numFailedTests: 0, + numFailedTestSuites: 1, + numPassedTests: 10, + numPassedTestSuites: 1, + numPendingTests: 0, + numPendingTestSuites: 0, + numTotalTests: 10, + numTotalTestSuites: 2, + snapshot: {}, + startTime: Date.now() - 2000 + }; + + tapReporter.onRunComplete({}, results); + + expect(tapReporter.writer.stats.mock.calls).toEqual([ + ['Test Suites', 1, 0, 1, 2], + ['Tests', 0, 0, 10, 10] + ]); + }); + + test('some suites and tests skipped', () => { + const tapReporter = new TapReporter(); + const results = { + numFailedTests: 0, + numFailedTestSuites: 0, + numPassedTests: 5, + numPassedTestSuites: 1, + numPendingTests: 5, + numPendingTestSuites: 1, + numTotalTests: 10, + numTotalTestSuites: 2, + snapshot: {}, + startTime: Date.now() - 2000 + }; + + tapReporter.onRunComplete({}, results); + + expect(tapReporter.writer.stats.mock.calls).toEqual([ + ['Test Suites', 0, 1, 1, 2], + ['Tests', 0, 5, 5, 10] + ]); + }); + }); }); diff --git a/test/__mocks__/chalk.js b/test/__mocks__/chalk.js new file mode 100644 index 0000000..960ced8 --- /dev/null +++ b/test/__mocks__/chalk.js @@ -0,0 +1,34 @@ +/* eslint-disable id-match */ +const stripAnsi = require('strip-ansi'); + +const chalkActual = require.requireActual('chalk'); + +const chalk = jest.fn(); + +chalk.__useActual = () => { + chalk.mockImplementation(chalkActual); +}; + +chalk.__showTemplates = () => { + chalk.mockImplementation((templates, ...rest) => { + let str = ''; + + for (let index = 0; index < rest.length; index++) { + str += templates[index] + rest[index]; + } + + return str + templates[templates.length - 1]; + }); +}; + +chalk.__stripColors = () => { + chalk.mockImplementation((...args) => { + const formatted = chalkActual(...args); + + return stripAnsi(formatted); + }); +}; + +chalk.__stripColors(); + +module.exports = chalk; diff --git a/test/__snapshots__/LineWriter.spec.js.snap b/test/__snapshots__/LineWriter.spec.js.snap new file mode 100644 index 0000000..f63d96d --- /dev/null +++ b/test/__snapshots__/LineWriter.spec.js.snap @@ -0,0 +1,116 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`LineWriter .commentLight() prints dimmed comment 1`] = ` +Array [ + Array [ + "foo", + ], +] +`; + +exports[`LineWriter .errors() format stack trace 1`] = ` +"# +# foobar +# +# Stack trace: +# +# at Something (../foo/bar.js:10:10) +# at Foobar (../foo/bar2.js:20:20) +# " +`; + +exports[`LineWriter .failed() colors "not ok" red 1`] = `"{red not ok}"`; + +exports[`LineWriter .keyValue() prints key-value pair 1`] = ` +Array [ + Array [ + "foo: bar", + ], +] +`; + +exports[`LineWriter .keyValueList() formats 3-tuple list into a value and calls .keyValue() 1`] = ` +Array [ + "foo", + "{styles1 1 name1}, {styles2 2 name2}, {styles3 3 name3}, {styles4 4 name4}", +] +`; + +exports[`LineWriter .passed() colors "ok" green 1`] = `"{green ok}"`; + +exports[`LineWriter .pending() colors "ok" yellow 1`] = `"{yellow ok}"`; + +exports[`LineWriter .snapshots() when all values are greater than zero prints them all 1`] = ` +Array [ + "Snapshots", + "1 failed, 1 updated, 1 added, 1 passed, 4 total", +] +`; + +exports[`LineWriter .start() prints start message 1`] = ` +Array [ + Array [ + "", + ], + Array [ + "", + ], + Array [ + "# Starting...", + ], +] +`; + +exports[`LineWriter .start() prints start message with suite number if provided 1`] = ` +Array [ + Array [ + "", + ], + Array [ + "", + ], + Array [ + "# Starting...", + ], + Array [ + "# 3 test suites found.", + ], +] +`; + +exports[`LineWriter .stats() when all tests pass shows only passed and total tests 1`] = ` +Array [ + "foo", + "1 passed, 1 total", +] +`; + +exports[`LineWriter .stats() when some tests are skipped and no tests fail shows only passed, skipped and total tests 1`] = ` +Array [ + "foo", + "1 skipped, 1 passed, 2 total", +] +`; + +exports[`LineWriter .stats() when some tests are skipped shows all items 1`] = ` +Array [ + "foo", + "1 failed, 1 skipped, 1 passed, 3 total", +] +`; + +exports[`LineWriter .stats() when some tests fail shows only passed, failed and total tests 1`] = ` +Array [ + "foo", + "1 failed, 1 passed, 2 total", +] +`; + +exports[`LineWriter .stats() when zero tests shows only total zero 1`] = ` +Array [ + "foo", + "0 total", +] +`; + +exports[`LineWriter .suite() prints test suite result 1`] = `" FAIL bar/kappa.js"`; diff --git a/test/__snapshots__/TapReporter.spec.js.snap b/test/__snapshots__/TapReporter.spec.js.snap new file mode 100644 index 0000000..ffe1afe --- /dev/null +++ b/test/__snapshots__/TapReporter.spec.js.snap @@ -0,0 +1,65 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TapReporter onTestResults must output all the tests on a suite tests 1`] = ` +Array [ + Array [ + "must log an error if there is a problem processing the results", + ], + Array [ + "must return the passed result obj without modifying it", + ], + Array [ + "must output the tap results to a file", + ], + Array [ + "must output the plan", + ], + Array [ + "must properly output success test lines", + ], + Array [ + "must properly output error test lines", + ], + Array [ + "must add the skiped directive to skiped test lines", + ], +] +`; + +exports[`TapReporter onTestResults must output all the tests on a suite tests 2`] = `Array []`; + +exports[`TapReporter onTestResults must output all the tests on a suite tests 3`] = `Array []`; + +exports[`TapReporter onTestResults must output error tests 1`] = ` +Array [ + Array [ + "must output the plan", + ], +] +`; + +exports[`TapReporter onTestResults must output passing tests 1`] = ` +Array [ + Array [ + "must output the plan", + ], +] +`; + +exports[`TapReporter onTestResults must output skipped tests 1`] = ` +Array [ + Array [ + "must log an error if there is a problem processing the results", + ], +] +`; + +exports[`TapReporter onTestResults suite log must output a suite log with the Suites filePath if possible 1`] = ` +Array [ + Array [ + false, + "/jest-tap-reporter/test", + "TapReporter.spec.js", + ], +] +`; diff --git a/test/fixtures/failingTestSuite.json b/test/fixtures/failingTestSuite.json index 1fbcf14..03013c8 100644 --- a/test/fixtures/failingTestSuite.json +++ b/test/fixtures/failingTestSuite.json @@ -1,6 +1,7 @@ { "testResults":[ { + "ancestorTitles": [], "failureMessages":[ "Error: expect(jest.fn()).toHaveBeenCalledWith(expected)\n\nExpected mock function to have been called with:\n [StringMatching /.*/, StringMatching /^1\\.\\.7/]\nBut it was called with:\n [\"/Users/carlospastor/dev/mailonline/mol-fe-jest-tap-results-processor/test-results.tap\", \"1..4\nok 1 mol-fe-jest-tap-results-processor must log an error if there is a problem processing the results\nok 2 mol-fe-jest-tap-results-processor must return the passed result obj without modifying it\nok 3 mol-fe-jest-tap-results-processor must output the tap results to a file\nok 4 mol-fe-jest-tap-results-processor must output the plan\n# tests 4\n# pass 4\n# fail 0\"]\n at Object.it (/Users/carlospastor/dev/mailonline/mol-fe-jest-tap-results-processor/test/index.spec.js:127:5)\n at Object.asyncFn (/Users/carlospastor/dev/mailonline/mol-fe-jest-tap-results-processor/node_modules/jest-jasmine2/build/jasmine-async.js:68:30)\n at resolve (/Users/carlospastor/dev/mailonline/mol-fe-jest-tap-results-processor/node_modules/jest-jasmine2/build/queueRunner.js:38:12)\n at Promise ()\n at mapper (/Users/carlospastor/dev/mailonline/mol-fe-jest-tap-results-processor/node_modules/jest-jasmine2/build/queueRunner.js:31:21)\n at Promise.resolve.then.el (/Users/carlospastor/dev/mailonline/mol-fe-jest-tap-results-processor/node_modules/p-map/index.js:42:16)\n at \n at process._tickCallback (internal/process/next_tick.js:169:7)" ], @@ -10,7 +11,7 @@ ], "endTime":1500478842090, "message":" ● mol-fe-jest-tap-results-processor › must output the plan\n\n expect(jest.fn()).toHaveBeenCalledWith(expected)\n \n Expected mock function to have been called with:\n [StringMatching /.*/, StringMatching /^1\\.\\.7/]\n But it was called with:\n [\"/Users/carlospastor/dev/mailonline/mol-fe-jest-tap-results-processor/test-results.tap\", \"1..4\n ok 1 mol-fe-jest-tap-results-processor must log an error if there is a problem processing the results\n ok 2 mol-fe-jest-tap-results-processor must return the passed result obj without modifying it\n ok 3 mol-fe-jest-tap-results-processor must output the tap results to a file\n ok 4 mol-fe-jest-tap-results-processor must output the plan\n # tests 4\n # pass 4\n # fail 0\"]\n \n at Object.it (test/index.spec.js:127:5)\n at Promise ()\n at \n at process._tickCallback (internal/process/next_tick.js:169:7)\n", - "name":"/Users/carlospastor/dev/mailonline/mol-fe-jest-tap-results-processor/test/index.spec.js", + "name":"/jest-tap-reporter/test/index.spec.js", "startTime":1500478841634, "status":"failed", "summary":"" diff --git a/test/fixtures/passingTestSuite.json b/test/fixtures/passingTestSuite.json index 0781aba..167eafd 100644 --- a/test/fixtures/passingTestSuite.json +++ b/test/fixtures/passingTestSuite.json @@ -14,5 +14,5 @@ "startTime":1500478745692, "status":"passed", "summary":"", - "testFilePath": "/Users/carlospastor/dev/mailonline/jest-tap-reporter/TapReporter.spec.js" + "testFilePath": "/jest-tap-reporter/test/TapReporter.spec.js" } \ No newline at end of file diff --git a/test/fixtures/severalTestsSuite.json b/test/fixtures/severalTestsSuite.json index 3cf06de..24f6baf 100644 --- a/test/fixtures/severalTestsSuite.json +++ b/test/fixtures/severalTestsSuite.json @@ -52,7 +52,7 @@ ], "endTime":1500478996335, "message":"", - "name":"/Users/carlospastor/dev/mailonline/mol-fe-jest-tap-results-processor/test/index.spec.js", + "name":"/jest-tap-reporter/test/index.spec.js", "startTime":1500478996144, "status":"passed", "summary":"" diff --git a/test/fixtures/skippedTestSuite.json b/test/fixtures/skippedTestSuite.json index aa12dc9..60ea16d 100644 --- a/test/fixtures/skippedTestSuite.json +++ b/test/fixtures/skippedTestSuite.json @@ -10,7 +10,7 @@ ], "endTime":1500479077870, "message":"", - "name":"/Users/carlospastor/dev/mailonline/mol-fe-jest-tap-results-processor/test/index.spec.js", + "name":"/jest-tap-reporter/test/index.spec.js", "startTime":1500479077405, "status":"passed", "summary":"" diff --git a/test/helpers/Logger.spec.js b/test/helpers/Logger.spec.js deleted file mode 100644 index a0ad236..0000000 --- a/test/helpers/Logger.spec.js +++ /dev/null @@ -1,144 +0,0 @@ -const Logger = require('../../src/helpers/Logger'); - -test('Logger must set the INFO as the default level', () => { - const logger = new Logger(); - - expect(logger.getLevel()).toBe('INFO'); -}); - -/* eslint-disable no-console */ -test('Logger must use console.log as default log function', () => { - const realLog = console.log; - - console.log = jest.fn(); - const logger = new Logger(); - - logger.log('foo', 'bar'); - - expect(console.log).toHaveBeenCalledTimes(1); - expect(console.log).toHaveBeenCalledWith('foo', 'bar'); - - console.log = realLog; -}); -/* eslint-enable no-console */ - -test('Logger must be possible to pass a log function', () => { - const log = jest.fn(); - const logger = new Logger({log}); - - logger.log('foo', 'bar'); - - expect(log).toHaveBeenCalledTimes(1); - expect(log).toHaveBeenCalledWith('foo', 'bar'); -}); - -test('Logger must be possible to pass a default log level', () => { - const config = { - logLevel: 'ERROR' - }; - const logger = new Logger(config); - - expect(logger.getLevel()).toBe('ERROR'); -}); - -test('Logger must be possible to change the log level', () => { - const logger = new Logger(); - - expect(logger.getLevel()).toBe('INFO'); - logger.setLevel('ERROR'); - expect(logger.getLevel()).toBe('ERROR'); - logger.setLevel('WARN'); - expect(logger.getLevel()).toBe('WARN'); -}); - -test('Logger must throw if you try to change the level with an unknown one', () => { - const logger = new Logger(); - - expect(() => logger.setLevel('asfasdfsadf')).toThrow(TypeError); - expect(() => logger.setLevel(23423)).toThrow(TypeError); - expect(() => logger.setLevel()).toThrow(TypeError); -}); - -test('logger warn log must log no matter the log level', () => { - const log = jest.fn(); - const logger = new Logger({ - log, - logLevel: 'INFO' - }); - - logger.log('INFO'); - expect(log).toHaveBeenCalledWith('INFO'); - - logger.setLevel('WARN'); - logger.log('WARN'); - expect(log).toHaveBeenCalledWith('WARN'); - - logger.setLevel('ERROR'); - logger.log('ERROR'); - expect(log).toHaveBeenCalledWith('ERROR'); - - expect(log).toHaveBeenCalledTimes(3); -}); - -test('logger info must log if log level is INFO', () => { - const log = jest.fn(); - const logger = new Logger({ - log, - logLevel: 'ERROR' - }); - - logger.info('test'); - expect(log).not.toHaveBeenCalled(); - - logger.setLevel('WARN'); - logger.info('test'); - expect(log).not.toHaveBeenCalled(); - - logger.setLevel('INFO'); - logger.info('test'); - expect(log).toHaveBeenCalledWith('test'); - - expect(log).toHaveBeenCalledTimes(1); -}); - -test('logger WARN must log if log level is INFO or WARN', () => { - const log = jest.fn(); - const logger = new Logger({ - log, - logLevel: 'ERROR' - }); - - logger.warn('test'); - expect(log).not.toHaveBeenCalled(); - - logger.setLevel('WARN'); - logger.warn('test'); - expect(log).toHaveBeenCalledWith('test'); - - logger.setLevel('INFO'); - logger.warn('test2'); - expect(log).toHaveBeenCalledWith('test2'); - - expect(log).toHaveBeenCalledTimes(2); -}); - -test('logger ERROR must log if log level is INFO or WARN or ERROR', () => { - const log = jest.fn(); - const logger = new Logger({ - log, - logLevel: 'ERROR' - }); - - logger.error('test'); - expect(log).toHaveBeenCalledWith('test'); - - logger.setLevel('WARN'); - logger.error('test2'); - expect(log).toHaveBeenCalledWith('test2'); - - logger.setLevel('INFO'); - logger.error('test3'); - expect(log).toHaveBeenCalledWith('test3'); - - expect(log).toHaveBeenCalledTimes(3); -});