console-testing-library
Version:
Testing console the right way
273 lines (236 loc) • 6.96 kB
JavaScript
import { Console } from 'console';
import { Writable } from 'stream';
import util from 'util';
import { toMatchInlineSnapshot } from 'jest-snapshot';
import prettyFormat from 'pretty-format';
import ansiStripper from 'strip-ansi';
const INSPECT_SYMBOL = util.inspect.custom;
const instances = new WeakMap();
const LEVELS = {
log: [
'log',
'trace',
'dir',
'dirxml',
'group',
'groupCollapsed',
'debug',
'timeLog',
],
info: ['count', 'info', 'timeEnd'],
warn: ['warn', 'countReset'],
error: ['error', 'assert'],
};
export function createConsole({
isSilent: defaultIsSilent = true,
stripAnsi: defaultStripAnsi = false,
} = {}) {
let logs = [];
let records = {};
let levels = {
log: '',
info: '',
warn: '',
error: '',
};
let currentLevel = undefined;
let currentMethod = undefined;
let isSilent = defaultIsSilent;
let stripAnsi = defaultStripAnsi;
let targetConsole = undefined;
const stringifyLogs = logs => {
return logs.map(log => log[1]).join('\n');
};
const writable = new Writable({
write(chunk, encoding, callback) {
const message = chunk
.toString('utf8')
// Strip out the new line character in the end
.slice(0, -1);
const normalizedMessage = stripAnsi ? ansiStripper(message) : message;
logs.push([currentLevel, normalizedMessage]);
if (currentLevel && currentLevel in levels) {
levels[currentLevel] = [levels[currentLevel], normalizedMessage]
.filter(Boolean)
.join('\n');
}
records[currentMethod] = [records[currentMethod], normalizedMessage]
.filter(Boolean)
.join('\n');
callback();
},
});
const testingConsole = new Console(writable, writable);
testingConsole.clear = function clear() {
logs = [];
records = {};
levels = {
log: '',
info: '',
warn: '',
error: '',
};
currentLevel = undefined;
currentMethod = undefined;
};
Object.getOwnPropertyNames(testingConsole).forEach(property => {
if (
typeof testingConsole[property] === 'function' &&
// Internal properties like `_stdoutErrorHandler` and `_stderrErrorHandler`
!property.startsWith('_')
) {
if (property !== 'clear') {
const originalFunction = testingConsole[property];
testingConsole[property] = function(...args) {
currentLevel = undefined;
currentMethod = property;
Object.keys(LEVELS).forEach(level => {
if (LEVELS[level].includes(property)) {
currentLevel = level;
}
});
const prettiedArguments = args.map(argv => {
if (
(typeof argv === 'object' || typeof argv === 'function') &&
argv !== null
) {
// We return an object with inspect symbol here because string substitutions requires objects
return { [INSPECT_SYMBOL]: () => prettyFormat(argv) };
}
return argv;
});
const returnValue = originalFunction.apply(this, prettiedArguments);
if (!isSilent && targetConsole) {
targetConsole[property].apply(this, args);
}
return returnValue;
};
}
Object.defineProperty(testingConsole[property], 'name', {
value: property,
});
testingConsole[property].testingConsole = testingConsole;
if (typeof jest === 'object' && typeof jest.spyOn === 'function') {
jest.spyOn(testingConsole, property);
Object.defineProperty(testingConsole[property], 'name', {
value: property,
});
testingConsole[property].testingConsole = testingConsole;
}
}
});
instances.set(testingConsole, {
get log() {
return stringifyLogs(logs);
},
get logs() {
return logs;
},
levels: {
get log() {
return levels.log;
},
get info() {
return levels.info;
},
get warn() {
return levels.warn;
},
get error() {
return levels.error;
},
},
get stderr() {
return stringifyLogs(
logs.filter(([logLevel]) => ['warn', 'error'].includes(logLevel))
);
},
get stdout() {
return stringifyLogs(
logs.filter(([logLevel]) => ['log', 'info'].includes(logLevel))
);
},
getRecord(method) {
return records[method] || '';
},
get silence() {
return isSilent;
},
set silence(shouldSilent = true) {
isSilent = !!shouldSilent;
},
get _targetConsole() {
return targetConsole;
},
set _targetConsole(target) {
targetConsole = target;
},
});
return testingConsole;
}
export function mockConsole(
testingConsole,
targetConsoleParent = global,
targetConsoleKey = 'console'
) {
const targetConsole = targetConsoleParent[targetConsoleKey];
targetConsoleParent[targetConsoleKey] = testingConsole;
const instance = instances.get(testingConsole);
instance._targetConsole = targetConsole;
return () => {
targetConsoleParent[targetConsoleKey] = targetConsole;
instance._targetConsole = undefined;
};
}
export function getLog(testingConsole = global.console) {
return instances.get(testingConsole);
}
export function silenceConsole(
testingConsole = global.console,
shouldSilent = true
) {
if (typeof arguments[0] === 'boolean') {
testingConsole = global.console;
shouldSilent = arguments[0];
}
instances.get(testingConsole).silence = !!shouldSilent;
}
export function restore() {
global.console = global.originalConsole;
}
if (typeof expect === 'function' && typeof expect.extend === 'function') {
expect.extend({
toMatchInlineSnapshot(received, ...args) {
/* ------- Workaround for custom inline snapshot matchers ------- */
const error = new Error();
const stacks = error.stack.split('\n');
for (let i = 1; i < stacks.length; i += 1) {
if (stacks[i].includes(__filename)) {
stacks.splice(1, i);
break;
}
}
error.stack = stacks.join('\n');
const context = Object.assign(this, { error });
/* -------------------------------------------------------------- */
const testingConsoleInstance =
(received && received.testingConsole) || received;
if (!testingConsoleInstance || !instances.has(testingConsoleInstance)) {
return toMatchInlineSnapshot.call(context, received, ...args);
}
if (typeof received === 'function') {
return toMatchInlineSnapshot.call(
context,
getLog().getRecord(received.name),
...args
);
} else if (typeof received === 'object') {
return toMatchInlineSnapshot.call(
context,
getLog(received).log,
...args
);
}
},
});
}