Skip to content

Commit

Permalink
Magic assert (#1154)
Browse files Browse the repository at this point in the history
  • Loading branch information
Vadim Demedes authored and sindresorhus committed Feb 2, 2017
1 parent 9616dde commit c9e6e6f
Show file tree
Hide file tree
Showing 20 changed files with 1,157 additions and 180 deletions.
38 changes: 26 additions & 12 deletions lib/assert.js
Expand Up @@ -17,15 +17,17 @@ function create(val, expected, operator, msg, fn) {
return {
actual: val,
expected,
message: msg,
message: msg || ' ',
operator,
stackStartFunction: fn
};
}

function test(ok, opts) {
if (!ok) {
throw new assert.AssertionError(opts);
const err = new assert.AssertionError(opts);
err.showOutput = ['fail', 'throws', 'notThrows'].indexOf(err.operator) === -1;
throw err;
}
}

Expand Down Expand Up @@ -109,7 +111,7 @@ x.throws = (fn, err, msg) => {

return result;
} catch (err) {
test(false, create(err.actual, err.expected, err.operator, err.message, x.throws));
test(false, create(err.actual, err.expected, 'throws', err.message, x.throws));
}
};

Expand All @@ -134,7 +136,7 @@ x.notThrows = (fn, msg) => {
try {
assert.doesNotThrow(fn, msg);
} catch (err) {
test(false, create(err.actual, err.expected, err.operator, err.message, x.notThrows));
test(false, create(err.actual, err.expected, 'notThrows', err.message, x.notThrows));
}
};

Expand Down Expand Up @@ -163,21 +165,33 @@ x._snapshot = function (tree, optionalMessage, match, snapshotStateGetter) {
snapshotState: state
};

const result = toMatchSnapshot.call(context, tree);
// symbols can't be serialized and saved in a snapshot,
// that's why tree is saved in `jsx` prop, so that JSX can be detected later
const serializedTree = tree.$$typeof === Symbol.for('react.test.json') ? {__ava_react_jsx: tree} : tree; // eslint-disable-line camelcase
const result = toMatchSnapshot.call(context, JSON.stringify(serializedTree));

let message = 'Please check your code or --update-snapshots\n\n';
let message = 'Please check your code or --update-snapshots';

if (optionalMessage) {
message += indentString(optionalMessage, 2);
}

if (typeof result.message === 'function') {
message += indentString(result.message(), 2);
message += '\n\n' + indentString(optionalMessage, 2);
}

state.save();

test(result.pass, create(result, false, 'snapshot', message, x.snap));
let expected;

if (result.expected) {
// JSON in a snapshot is surrounded with `"`, because jest-snapshot
// serializes snapshot values too, so it ends up double JSON encoded
expected = JSON.parse(result.expected.slice(1).slice(0, -1));
// Define a `$$typeof` symbol, so that pretty-format detects it as React tree
if (expected.__ava_react_jsx) { // eslint-disable-line camelcase
expected = expected.__ava_react_jsx; // eslint-disable-line camelcase
Object.defineProperty(expected, '$$typeof', {value: Symbol.for('react.test.json')});
}
}

test(result.pass, create(tree, expected, 'snapshot', message, x.snapshot));
};

x.snapshot = function (tree, optionalMessage) {
Expand Down
4 changes: 2 additions & 2 deletions lib/cli.js
Expand Up @@ -129,9 +129,9 @@ exports.run = () => {
if (cli.flags.tap && !cli.flags.watch) {
reporter = tapReporter();
} else if (cli.flags.verbose || isCi) {
reporter = verboseReporter();
reporter = verboseReporter({basePath: pkgDir});
} else {
reporter = miniReporter({watching: cli.flags.watch});
reporter = miniReporter({watching: cli.flags.watch, basePath: pkgDir});
}

reporter.api = api;
Expand Down
45 changes: 45 additions & 0 deletions lib/code-excerpt.js
@@ -0,0 +1,45 @@
'use strict';
const fs = require('fs');
const equalLength = require('equal-length');
const codeExcerpt = require('code-excerpt');
const truncate = require('cli-truncate');
const chalk = require('chalk');

const formatLineNumber = (lineNumber, maxLineNumber) => {
return ' '.repeat(String(maxLineNumber).length - String(lineNumber).length) + lineNumber;
};

module.exports = (file, line, options) => {
options = options || {};

const maxWidth = options.maxWidth || 80;
const source = fs.readFileSync(file, 'utf8');
const excerpt = codeExcerpt(source, line, {around: 1});
if (!excerpt) {
return null;
}

const lines = excerpt.map(item => ({
line: item.line,
value: truncate(item.value, maxWidth - String(line).length - 5)
}));

const joinedLines = lines.map(line => line.value).join('\n');
const extendedLines = equalLength(joinedLines).split('\n');

return lines
.map((item, index) => ({
line: item.line,
value: extendedLines[index]
}))
.map(item => {
const isErrorSource = item.line === line;

const lineNumber = formatLineNumber(item.line, line) + ':';
const coloredLineNumber = isErrorSource ? lineNumber : chalk.grey(lineNumber);
const result = ` ${coloredLineNumber} ${item.value}`;

return isErrorSource ? chalk.bgRed(result) : result;
})
.join('\n');
};
3 changes: 2 additions & 1 deletion lib/colors.js
Expand Up @@ -2,12 +2,13 @@
const chalk = require('chalk');

module.exports = {
title: chalk.white,
title: chalk.bold.white,
error: chalk.red,
skip: chalk.yellow,
todo: chalk.blue,
pass: chalk.green,
duration: chalk.gray.dim,
errorSource: chalk.gray,
errorStack: chalk.gray,
stack: chalk.red,
information: chalk.magenta
Expand Down
92 changes: 52 additions & 40 deletions lib/enhance-assert.js
@@ -1,67 +1,79 @@
'use strict';

module.exports = enhanceAssert;
module.exports.formatter = formatter;
const dotProp = require('dot-prop');

// When adding patterns, don't forget to add to
// https://github.com/avajs/babel-preset-transform-test-files/blob/master/espower-patterns.json
// Then release a new version of that preset and bump the SemVer range here.
module.exports.PATTERNS = [
const PATTERNS = [
't.truthy(value, [message])',
't.falsy(value, [message])',
't.true(value, [message])',
't.false(value, [message])',
't.is(value, expected, [message])',
't.not(value, expected, [message])',
't.deepEqual(value, expected, [message])',
't.notDeepEqual(value, expected, [message])',
't.regex(contents, regex, [message])',
't.notRegex(contents, regex, [message])'
];

module.exports.NON_ENHANCED_PATTERNS = [
const NON_ENHANCED_PATTERNS = [
't.pass([message])',
't.fail([message])',
't.throws(fn, [message])',
't.notThrows(fn, [message])',
't.ifError(error, [message])',
't.snapshot(contents, [message])'
't.snapshot(contents, [message])',
't.is(value, expected, [message])',
't.not(value, expected, [message])',
't.deepEqual(value, expected, [message])',
't.notDeepEqual(value, expected, [message])'
];

function enhanceAssert(opts) {
const enhanceAssert = opts => {
const empower = require('empower-core');

const enhanced = empower(
opts.assert,
{
destructive: false,
onError: opts.onError,
onSuccess: opts.onSuccess,
patterns: module.exports.PATTERNS,
wrapOnlyPatterns: module.exports.NON_ENHANCED_PATTERNS,
bindReceiver: false
}
);
const enhanced = empower(opts.assert, {
destructive: false,
onError: opts.onError,
onSuccess: opts.onSuccess,
patterns: PATTERNS,
wrapOnlyPatterns: NON_ENHANCED_PATTERNS,
bindReceiver: false
});

return enhanced;
}
};

function formatter() {
const createFormatter = require('power-assert-context-formatter');
const SuccinctRenderer = require('power-assert-renderer-succinct');
const AssertionRenderer = require('power-assert-renderer-assertion');
const isRangeMatch = (a, b) => {
return (a[0] === b[0] && a[1] === b[1]) ||
(a[0] > b[0] && a[0] < b[1]) ||
(a[1] > b[0] && a[1] < b[1]);
};

return createFormatter({
renderers: [
{
ctor: AssertionRenderer
},
{
ctor: SuccinctRenderer,
options: {
maxDepth: 3
}
}
]
});
}
const computeStatement = (tokens, range) => {
return tokens
.filter(token => isRangeMatch(token.range, range))
.map(token => token.value === undefined ? token.type.label : token.value)
.join('');
};

const getNode = (ast, path) => dotProp.get(ast, path.replace(/\//g, '.'));

const formatter = () => {
return context => {
const ast = JSON.parse(context.source.ast);
const tokens = JSON.parse(context.source.tokens);
const args = context.args[0].events;

return args
.map(arg => {
const range = getNode(ast, arg.espath).range;

return [computeStatement(tokens, range), arg.value];
})
.reverse();
};
};

module.exports = enhanceAssert;
module.exports.PATTERNS = PATTERNS;
module.exports.NON_ENHANCED_PATTERNS = NON_ENHANCED_PATTERNS;
module.exports.formatter = formatter;
10 changes: 10 additions & 0 deletions lib/extract-stack.js
@@ -0,0 +1,10 @@
'use strict';
const stackLineRegex = /^.+ \(.+:[0-9]+:[0-9]+\)$/;

module.exports = stack => {
return stack
.split('\n')
.filter(line => stackLineRegex.test(line))
.map(line => line.trim())
.join('\n');
};
72 changes: 72 additions & 0 deletions lib/format-assert-error.js
@@ -0,0 +1,72 @@
'use strict';
const indentString = require('indent-string');
const chalk = require('chalk');
const diff = require('diff');

const cleanUp = line => {
if (line[0] === '+') {
return `${chalk.green('+')} ${line.slice(1)}`;
}

if (line[0] === '-') {
return `${chalk.red('-')} ${line.slice(1)}`;
}

if (line.match(/@@/)) {
return null;
}

if (line.match(/\\ No newline/)) {
return null;
}

return ` ${line}`;
};

module.exports = err => {
if (err.statements) {
const statements = JSON.parse(err.statements);

return statements
.map(statement => `${statement[0]}\n${chalk.grey('=>')} ${statement[1]}`)
.join('\n\n') + '\n';
}

if ((err.actualType === 'object' || err.actualType === 'array') && err.actualType === err.expectedType) {
const patch = diff.createPatch('string', err.actual, err.expected);
const msg = patch
.split('\n')
.slice(4)
.map(cleanUp)
.filter(Boolean)
.join('\n');

return `Difference:\n\n${msg}`;
}

if (err.actualType === 'string' && err.expectedType === 'string') {
const patch = diff.diffChars(err.actual, err.expected);
const msg = patch
.map(part => {
if (part.added) {
return chalk.bgGreen.black(part.value);
}

if (part.removed) {
return chalk.bgRed.black(part.value);
}

return part.value;
})
.join('');

return `Difference:\n\n${msg}\n`;
}

return [
'Actual:\n',
`${indentString(err.actual, 2)}\n`,
'Expected:\n',
`${indentString(err.expected, 2)}\n`
].join('\n');
};

0 comments on commit c9e6e6f

Please sign in to comment.