diff --git a/CHANGELOG.md b/CHANGELOG.md index f546890f9800..33376b8ff8a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ ## master +### Features + +* `[jest-util]` Add the following methods to the "console" implementations: + `assert`, `count`, `countReset`, `dir`, `dirxml`, `group`, `groupCollapsed`, + `groupEnd`, `time`, `timeEnd` + ([#5514](https://github.com/facebook/jest/pull/5514)) + ## jest 22.2.2 ### Fixes @@ -16,8 +23,9 @@ ([#5494](https://github.com/facebook/jest/pull/5494)) ### Chore & Maintenance -* `[filenames]` Standardize file names in root ([#5500](https://github.com/facebook/jest/pull/5500)) +* `[filenames]` Standardize file names in root + ([#5500](https://github.com/facebook/jest/pull/5500)) ## jest 22.2.1 diff --git a/packages/jest-util/src/Console.js b/packages/jest-util/src/Console.js index 864be1520b14..3f7f109a7284 100644 --- a/packages/jest-util/src/Console.js +++ b/packages/jest-util/src/Console.js @@ -8,10 +8,11 @@ */ /* global stream$Writable */ -import type {LogType, LogMessage} from 'types/Console'; +import type {LogType, LogMessage, LogCounters, LogTimers} from 'types/Console'; import {format} from 'util'; import {Console} from 'console'; +import chalk from 'chalk'; import clearLine from './clear_line'; type Formatter = (type: LogType, message: LogMessage) => string; @@ -19,6 +20,9 @@ type Formatter = (type: LogType, message: LogMessage) => string; export default class CustomConsole extends Console { _stdout: stream$Writable; _formatBuffer: Formatter; + _counters: LogCounters; + _timers: LogTimers; + _groupDepth: number; constructor( stdout: stream$Writable, @@ -27,31 +31,107 @@ export default class CustomConsole extends Console { ) { super(stdout, stderr); this._formatBuffer = formatBuffer || ((type, message) => message); + this._counters = {}; + this._timers = {}; + this._groupDepth = 0; + } + + _logToParentConsole(message: string) { + super.log(message); } _log(type: LogType, message: string) { clearLine(this._stdout); - super.log(this._formatBuffer(type, message)); + this._logToParentConsole( + this._formatBuffer(type, ' '.repeat(this._groupDepth) + message), + ); + } + + assert(...args: Array) { + if (args[0]) { + this._log('assert', format(...args.slice(1))); + } + } + + count(label: string = 'default') { + if (!this._counters[label]) { + this._counters[label] = 0; + } + + this._log('count', format(`${label}: ${++this._counters[label]}`)); } - debug(...args: Array) { - this._log('debug', format.apply(null, arguments)); + countReset(label: string = 'default') { + this._counters[label] = 0; } - log(...args: Array) { - this._log('log', format.apply(null, arguments)); + debug(...args: Array) { + this._log('debug', format(...args)); } - info(...args: Array) { - this._log('info', format.apply(null, arguments)); + dir(...args: Array) { + this._log('dir', format(...args)); + } + + dirxml(...args: Array) { + this._log('dirxml', format(...args)); + } + + error(...args: Array) { + this._log('error', format(...args)); + } + + group(...args: Array) { + this._groupDepth++; + + if (args.length > 0) { + this._log('group', chalk.bold(format(...args))); + } } - warn(...args: Array) { - this._log('warn', format.apply(null, arguments)); + groupCollapsed(...args: Array) { + this._groupDepth++; + + if (args.length > 0) { + this._log('groupCollapsed', chalk.bold(format(...args))); + } + } + + groupEnd() { + if (this._groupDepth > 0) { + this._groupDepth--; + } + } + + info(...args: Array) { + this._log('info', format(...args)); + } + + log(...args: Array) { + this._log('log', format(...args)); + } + + time(label: string = 'default') { + if (this._timers[label]) { + return; + } + + this._timers[label] = new Date(); + } + + timeEnd(label: string = 'default') { + const startTime = this._timers[label]; + + if (startTime) { + const endTime = new Date(); + const time = (endTime - startTime) / 1000; + this._log('time', format(`${label}: ${time}ms`)); + delete this._timers[label]; + } } - error(...args: Array) { - this._log('error', format.apply(null, arguments)); + warn(...args: Array) { + this._log('warn', format(...args)); } getBuffer() { diff --git a/packages/jest-util/src/__tests__/buffered_console.test.js b/packages/jest-util/src/__tests__/buffered_console.test.js new file mode 100644 index 000000000000..bbae95c8c578 --- /dev/null +++ b/packages/jest-util/src/__tests__/buffered_console.test.js @@ -0,0 +1,134 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import chalk from 'chalk'; +import BufferedConsole from '../buffered_console'; + +describe('CustomConsole', () => { + let _console; + const stdout = () => + _console + .getBuffer() + .map(log => log.message) + .join('\n'); + + beforeEach(() => { + _console = new BufferedConsole(); + }); + + describe('assert', () => { + test('log when the assertion is truthy', () => { + _console.assert(true, 'ok'); + + expect(stdout()).toMatch('ok'); + }); + + test('do not log when the assertion is falsy', () => { + _console.assert(false, 'ok'); + + expect(stdout()).toEqual(''); + }); + }); + + describe('count', () => { + test('count using the default counter', () => { + _console.count(); + _console.count(); + _console.count(); + + expect(stdout()).toEqual('default: 1\ndefault: 2\ndefault: 3'); + }); + + test('count using the a labeled counter', () => { + _console.count('custom'); + _console.count('custom'); + _console.count('custom'); + + expect(stdout()).toEqual('custom: 1\ncustom: 2\ncustom: 3'); + }); + + test('countReset restarts default counter', () => { + _console.count(); + _console.count(); + _console.countReset(); + _console.count(); + expect(stdout()).toEqual('default: 1\ndefault: 2\ndefault: 1'); + }); + + test('countReset restarts custom counter', () => { + _console.count('custom'); + _console.count('custom'); + _console.countReset('custom'); + _console.count('custom'); + + expect(stdout()).toEqual('custom: 1\ncustom: 2\ncustom: 1'); + }); + }); + + describe('group', () => { + test('group without label', () => { + _console.group(); + _console.log('hey'); + _console.group(); + _console.log('there'); + + expect(stdout()).toEqual(' hey\n there'); + }); + + test('group with label', () => { + _console.group('first'); + _console.log('hey'); + _console.group('second'); + _console.log('there'); + + expect(stdout()).toEqual(` ${chalk.bold('first')} + hey + ${chalk.bold('second')} + there`); + }); + + test('groupEnd remove the indentation of the current group', () => { + _console.group(); + _console.log('hey'); + _console.groupEnd(); + _console.log('there'); + + expect(stdout()).toEqual(' hey\nthere'); + }); + + test('groupEnd can not remove the indentation below the starting point', () => { + _console.groupEnd(); + _console.groupEnd(); + _console.group(); + _console.log('hey'); + _console.groupEnd(); + _console.log('there'); + + expect(stdout()).toEqual(' hey\nthere'); + }); + }); + + describe('time', () => { + test('should return the time between time() and timeEnd() on default timer', () => { + _console.time(); + _console.timeEnd(); + + expect(stdout()).toMatch('default: '); + expect(stdout()).toMatch('ms'); + }); + + test('should return the time between time() and timeEnd() on custom timer', () => { + _console.time('custom'); + _console.timeEnd('custom'); + + expect(stdout()).toMatch('custom: '); + expect(stdout()).toMatch('ms'); + }); + }); +}); diff --git a/packages/jest-util/src/__tests__/console.test.js b/packages/jest-util/src/__tests__/console.test.js new file mode 100644 index 000000000000..d42203e29271 --- /dev/null +++ b/packages/jest-util/src/__tests__/console.test.js @@ -0,0 +1,136 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import chalk from 'chalk'; +import CustomConsole from '../Console'; + +describe('CustomConsole', () => { + let _console; + let _stdout = ''; + + beforeEach(() => { + _console = new CustomConsole(process.stdout, process.stderr); + jest.spyOn(_console, '_logToParentConsole').mockImplementation(message => { + _stdout += message + '\n'; + }); + + _stdout = ''; + }); + + describe('assert', () => { + test('log when the assertion is truthy', () => { + _console.assert(true, 'ok'); + + expect(_stdout).toMatch('ok'); + }); + + test('do not log when the assertion is falsy', () => { + _console.assert(false, 'ok'); + + expect(_stdout).toEqual(''); + }); + }); + + describe('count', () => { + test('count using the default counter', () => { + _console.count(); + _console.count(); + _console.count(); + + expect(_stdout).toEqual('default: 1\ndefault: 2\ndefault: 3\n'); + }); + + test('count using the a labeled counter', () => { + _console.count('custom'); + _console.count('custom'); + _console.count('custom'); + + expect(_stdout).toEqual('custom: 1\ncustom: 2\ncustom: 3\n'); + }); + + test('countReset restarts default counter', () => { + _console.count(); + _console.count(); + _console.countReset(); + _console.count(); + expect(_stdout).toEqual('default: 1\ndefault: 2\ndefault: 1\n'); + }); + + test('countReset restarts custom counter', () => { + _console.count('custom'); + _console.count('custom'); + _console.countReset('custom'); + _console.count('custom'); + + expect(_stdout).toEqual('custom: 1\ncustom: 2\ncustom: 1\n'); + }); + }); + + describe('group', () => { + test('group without label', () => { + _console.group(); + _console.log('hey'); + _console.group(); + _console.log('there'); + + expect(_stdout).toEqual(' hey\n there\n'); + }); + + test('group with label', () => { + _console.group('first'); + _console.log('hey'); + _console.group('second'); + _console.log('there'); + + expect(_stdout).toEqual(` ${chalk.bold('first')} + hey + ${chalk.bold('second')} + there +`); + }); + + test('groupEnd remove the indentation of the current group', () => { + _console.group(); + _console.log('hey'); + _console.groupEnd(); + _console.log('there'); + + expect(_stdout).toEqual(' hey\nthere\n'); + }); + + test('groupEnd can not remove the indentation below the starting point', () => { + _console.groupEnd(); + _console.groupEnd(); + _console.group(); + _console.log('hey'); + _console.groupEnd(); + _console.log('there'); + + expect(_stdout).toEqual(' hey\nthere\n'); + }); + }); + + describe('time', () => { + test('should return the time between time() and timeEnd() on default timer', () => { + _console.time(); + _console.timeEnd(); + + expect(_stdout).toMatch('default: '); + expect(_stdout).toMatch('ms'); + }); + + test('should return the time between time() and timeEnd() on custom timer', () => { + _console.time('custom'); + _console.timeEnd('custom'); + + expect(_stdout).toMatch('custom: '); + expect(_stdout).toMatch('ms'); + }); + }); +}); diff --git a/packages/jest-util/src/buffered_console.js b/packages/jest-util/src/buffered_console.js index 5581595c12d3..1a563b500ca2 100644 --- a/packages/jest-util/src/buffered_console.js +++ b/packages/jest-util/src/buffered_console.js @@ -7,19 +7,32 @@ * @flow */ -import type {ConsoleBuffer, LogMessage, LogType} from 'types/Console'; +import type { + ConsoleBuffer, + LogMessage, + LogType, + LogCounters, + LogTimers, +} from 'types/Console'; import {Console} from 'console'; import {format} from 'util'; +import chalk from 'chalk'; import callsites from 'callsites'; export default class BufferedConsole extends Console { _buffer: ConsoleBuffer; + _counters: LogCounters; + _timers: LogTimers; + _groupDepth: number; constructor() { const buffer = []; super({write: message => BufferedConsole.write(buffer, 'log', message)}); this._buffer = buffer; + this._counters = {}; + this._timers = {}; + this._groupDepth = 0; } static write( @@ -30,28 +43,110 @@ export default class BufferedConsole extends Console { ) { const call = callsites()[level != null ? level : 2]; const origin = call.getFileName() + ':' + call.getLineNumber(); - buffer.push({message, origin, type}); + + buffer.push({ + message, + origin, + type, + }); + return buffer; } - debug() { - BufferedConsole.write(this._buffer, 'debug', format.apply(null, arguments)); + _log(type: LogType, message: LogMessage) { + BufferedConsole.write( + this._buffer, + type, + ' '.repeat(this._groupDepth) + message, + 3, + ); + } + + assert(...args: Array) { + if (args[0]) { + this._log('assert', format(...args.slice(1))); + } } - log() { - BufferedConsole.write(this._buffer, 'log', format.apply(null, arguments)); + count(label: string = 'default') { + if (!this._counters[label]) { + this._counters[label] = 0; + } + + this._log('count', format(`${label}: ${++this._counters[label]}`)); + } + + countReset(label: string = 'default') { + this._counters[label] = 0; } - info() { - BufferedConsole.write(this._buffer, 'info', format.apply(null, arguments)); + debug(...args: Array) { + this._log('debug', format(...args)); } - warn() { - BufferedConsole.write(this._buffer, 'warn', format.apply(null, arguments)); + dir(...args: Array) { + this._log('dir', format(...args)); + } + + dirxml(...args: Array) { + this._log('dirxml', format(...args)); + } + + error(...args: Array) { + this._log('error', format(...args)); + } + + group(...args: Array) { + this._groupDepth++; + + if (args.length > 0) { + this._log('group', chalk.bold(format(...args))); + } + } + + groupCollapsed(...args: Array) { + this._groupDepth++; + + if (args.length > 0) { + this._log('groupCollapsed', chalk.bold(format(...args))); + } + } + + groupEnd() { + if (this._groupDepth > 0) { + this._groupDepth--; + } + } + + info(...args: Array) { + this._log('info', format(...args)); + } + + log(...args: Array) { + this._log('log', format(...args)); + } + + time(label: string = 'default') { + if (this._timers[label]) { + return; + } + + this._timers[label] = new Date(); + } + + timeEnd(label: string = 'default') { + const startTime = this._timers[label]; + + if (startTime) { + const endTime = new Date(); + const time = (endTime - startTime) / 1000; + this._log('time', format(`${label}: ${time}ms`)); + delete this._timers[label]; + } } - error() { - BufferedConsole.write(this._buffer, 'error', format.apply(null, arguments)); + warn(...args: Array) { + this._log('warn', format(...args)); } getBuffer() { diff --git a/types/Console.js b/types/Console.js index eed7c8e15eaf..bc50107e2d4f 100644 --- a/types/Console.js +++ b/types/Console.js @@ -13,5 +13,19 @@ export type LogEntry = {| origin: string, type: LogType, |}; -export type LogType = 'debug' | 'log' | 'info' | 'warn' | 'error'; +export type LogCounters = {[label: string]: number}; +export type LogTimers = {[label: string]: Date}; +export type LogType = + | 'assert' + | 'count' + | 'debug' + | 'dir' + | 'dirxml' + | 'error' + | 'group' + | 'groupCollapsed' + | 'info' + | 'log' + | 'time' + | 'warn'; export type ConsoleBuffer = Array;