Skip to content

Commit c9e6e6f

Browse files
Vadim Demedessindresorhus
Vadim Demedes
authored andcommitted
Magic assert (#1154)
1 parent 9616dde commit c9e6e6f

20 files changed

+1157
-180
lines changed

lib/assert.js

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,17 @@ function create(val, expected, operator, msg, fn) {
1717
return {
1818
actual: val,
1919
expected,
20-
message: msg,
20+
message: msg || ' ',
2121
operator,
2222
stackStartFunction: fn
2323
};
2424
}
2525

2626
function test(ok, opts) {
2727
if (!ok) {
28-
throw new assert.AssertionError(opts);
28+
const err = new assert.AssertionError(opts);
29+
err.showOutput = ['fail', 'throws', 'notThrows'].indexOf(err.operator) === -1;
30+
throw err;
2931
}
3032
}
3133

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

110112
return result;
111113
} catch (err) {
112-
test(false, create(err.actual, err.expected, err.operator, err.message, x.throws));
114+
test(false, create(err.actual, err.expected, 'throws', err.message, x.throws));
113115
}
114116
};
115117

@@ -134,7 +136,7 @@ x.notThrows = (fn, msg) => {
134136
try {
135137
assert.doesNotThrow(fn, msg);
136138
} catch (err) {
137-
test(false, create(err.actual, err.expected, err.operator, err.message, x.notThrows));
139+
test(false, create(err.actual, err.expected, 'notThrows', err.message, x.notThrows));
138140
}
139141
};
140142

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

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

168-
let message = 'Please check your code or --update-snapshots\n\n';
173+
let message = 'Please check your code or --update-snapshots';
169174

170175
if (optionalMessage) {
171-
message += indentString(optionalMessage, 2);
172-
}
173-
174-
if (typeof result.message === 'function') {
175-
message += indentString(result.message(), 2);
176+
message += '\n\n' + indentString(optionalMessage, 2);
176177
}
177178

178179
state.save();
179180

180-
test(result.pass, create(result, false, 'snapshot', message, x.snap));
181+
let expected;
182+
183+
if (result.expected) {
184+
// JSON in a snapshot is surrounded with `"`, because jest-snapshot
185+
// serializes snapshot values too, so it ends up double JSON encoded
186+
expected = JSON.parse(result.expected.slice(1).slice(0, -1));
187+
// Define a `$$typeof` symbol, so that pretty-format detects it as React tree
188+
if (expected.__ava_react_jsx) { // eslint-disable-line camelcase
189+
expected = expected.__ava_react_jsx; // eslint-disable-line camelcase
190+
Object.defineProperty(expected, '$$typeof', {value: Symbol.for('react.test.json')});
191+
}
192+
}
193+
194+
test(result.pass, create(tree, expected, 'snapshot', message, x.snapshot));
181195
};
182196

183197
x.snapshot = function (tree, optionalMessage) {

lib/cli.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,9 @@ exports.run = () => {
129129
if (cli.flags.tap && !cli.flags.watch) {
130130
reporter = tapReporter();
131131
} else if (cli.flags.verbose || isCi) {
132-
reporter = verboseReporter();
132+
reporter = verboseReporter({basePath: pkgDir});
133133
} else {
134-
reporter = miniReporter({watching: cli.flags.watch});
134+
reporter = miniReporter({watching: cli.flags.watch, basePath: pkgDir});
135135
}
136136

137137
reporter.api = api;

lib/code-excerpt.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
'use strict';
2+
const fs = require('fs');
3+
const equalLength = require('equal-length');
4+
const codeExcerpt = require('code-excerpt');
5+
const truncate = require('cli-truncate');
6+
const chalk = require('chalk');
7+
8+
const formatLineNumber = (lineNumber, maxLineNumber) => {
9+
return ' '.repeat(String(maxLineNumber).length - String(lineNumber).length) + lineNumber;
10+
};
11+
12+
module.exports = (file, line, options) => {
13+
options = options || {};
14+
15+
const maxWidth = options.maxWidth || 80;
16+
const source = fs.readFileSync(file, 'utf8');
17+
const excerpt = codeExcerpt(source, line, {around: 1});
18+
if (!excerpt) {
19+
return null;
20+
}
21+
22+
const lines = excerpt.map(item => ({
23+
line: item.line,
24+
value: truncate(item.value, maxWidth - String(line).length - 5)
25+
}));
26+
27+
const joinedLines = lines.map(line => line.value).join('\n');
28+
const extendedLines = equalLength(joinedLines).split('\n');
29+
30+
return lines
31+
.map((item, index) => ({
32+
line: item.line,
33+
value: extendedLines[index]
34+
}))
35+
.map(item => {
36+
const isErrorSource = item.line === line;
37+
38+
const lineNumber = formatLineNumber(item.line, line) + ':';
39+
const coloredLineNumber = isErrorSource ? lineNumber : chalk.grey(lineNumber);
40+
const result = ` ${coloredLineNumber} ${item.value}`;
41+
42+
return isErrorSource ? chalk.bgRed(result) : result;
43+
})
44+
.join('\n');
45+
};

lib/colors.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22
const chalk = require('chalk');
33

44
module.exports = {
5-
title: chalk.white,
5+
title: chalk.bold.white,
66
error: chalk.red,
77
skip: chalk.yellow,
88
todo: chalk.blue,
99
pass: chalk.green,
1010
duration: chalk.gray.dim,
11+
errorSource: chalk.gray,
1112
errorStack: chalk.gray,
1213
stack: chalk.red,
1314
information: chalk.magenta

lib/enhance-assert.js

Lines changed: 52 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,79 @@
11
'use strict';
2-
3-
module.exports = enhanceAssert;
4-
module.exports.formatter = formatter;
2+
const dotProp = require('dot-prop');
53

64
// When adding patterns, don't forget to add to
75
// https://github.com/avajs/babel-preset-transform-test-files/blob/master/espower-patterns.json
86
// Then release a new version of that preset and bump the SemVer range here.
9-
module.exports.PATTERNS = [
7+
const PATTERNS = [
108
't.truthy(value, [message])',
119
't.falsy(value, [message])',
1210
't.true(value, [message])',
1311
't.false(value, [message])',
1412
't.is(value, expected, [message])',
1513
't.not(value, expected, [message])',
16-
't.deepEqual(value, expected, [message])',
17-
't.notDeepEqual(value, expected, [message])',
1814
't.regex(contents, regex, [message])',
1915
't.notRegex(contents, regex, [message])'
2016
];
2117

22-
module.exports.NON_ENHANCED_PATTERNS = [
18+
const NON_ENHANCED_PATTERNS = [
2319
't.pass([message])',
2420
't.fail([message])',
2521
't.throws(fn, [message])',
2622
't.notThrows(fn, [message])',
2723
't.ifError(error, [message])',
28-
't.snapshot(contents, [message])'
24+
't.snapshot(contents, [message])',
25+
't.is(value, expected, [message])',
26+
't.not(value, expected, [message])',
27+
't.deepEqual(value, expected, [message])',
28+
't.notDeepEqual(value, expected, [message])'
2929
];
3030

31-
function enhanceAssert(opts) {
31+
const enhanceAssert = opts => {
3232
const empower = require('empower-core');
33-
34-
const enhanced = empower(
35-
opts.assert,
36-
{
37-
destructive: false,
38-
onError: opts.onError,
39-
onSuccess: opts.onSuccess,
40-
patterns: module.exports.PATTERNS,
41-
wrapOnlyPatterns: module.exports.NON_ENHANCED_PATTERNS,
42-
bindReceiver: false
43-
}
44-
);
33+
const enhanced = empower(opts.assert, {
34+
destructive: false,
35+
onError: opts.onError,
36+
onSuccess: opts.onSuccess,
37+
patterns: PATTERNS,
38+
wrapOnlyPatterns: NON_ENHANCED_PATTERNS,
39+
bindReceiver: false
40+
});
4541

4642
return enhanced;
47-
}
43+
};
4844

49-
function formatter() {
50-
const createFormatter = require('power-assert-context-formatter');
51-
const SuccinctRenderer = require('power-assert-renderer-succinct');
52-
const AssertionRenderer = require('power-assert-renderer-assertion');
45+
const isRangeMatch = (a, b) => {
46+
return (a[0] === b[0] && a[1] === b[1]) ||
47+
(a[0] > b[0] && a[0] < b[1]) ||
48+
(a[1] > b[0] && a[1] < b[1]);
49+
};
5350

54-
return createFormatter({
55-
renderers: [
56-
{
57-
ctor: AssertionRenderer
58-
},
59-
{
60-
ctor: SuccinctRenderer,
61-
options: {
62-
maxDepth: 3
63-
}
64-
}
65-
]
66-
});
67-
}
51+
const computeStatement = (tokens, range) => {
52+
return tokens
53+
.filter(token => isRangeMatch(token.range, range))
54+
.map(token => token.value === undefined ? token.type.label : token.value)
55+
.join('');
56+
};
57+
58+
const getNode = (ast, path) => dotProp.get(ast, path.replace(/\//g, '.'));
59+
60+
const formatter = () => {
61+
return context => {
62+
const ast = JSON.parse(context.source.ast);
63+
const tokens = JSON.parse(context.source.tokens);
64+
const args = context.args[0].events;
65+
66+
return args
67+
.map(arg => {
68+
const range = getNode(ast, arg.espath).range;
69+
70+
return [computeStatement(tokens, range), arg.value];
71+
})
72+
.reverse();
73+
};
74+
};
75+
76+
module.exports = enhanceAssert;
77+
module.exports.PATTERNS = PATTERNS;
78+
module.exports.NON_ENHANCED_PATTERNS = NON_ENHANCED_PATTERNS;
79+
module.exports.formatter = formatter;

lib/extract-stack.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
'use strict';
2+
const stackLineRegex = /^.+ \(.+:[0-9]+:[0-9]+\)$/;
3+
4+
module.exports = stack => {
5+
return stack
6+
.split('\n')
7+
.filter(line => stackLineRegex.test(line))
8+
.map(line => line.trim())
9+
.join('\n');
10+
};

lib/format-assert-error.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
'use strict';
2+
const indentString = require('indent-string');
3+
const chalk = require('chalk');
4+
const diff = require('diff');
5+
6+
const cleanUp = line => {
7+
if (line[0] === '+') {
8+
return `${chalk.green('+')} ${line.slice(1)}`;
9+
}
10+
11+
if (line[0] === '-') {
12+
return `${chalk.red('-')} ${line.slice(1)}`;
13+
}
14+
15+
if (line.match(/@@/)) {
16+
return null;
17+
}
18+
19+
if (line.match(/\\ No newline/)) {
20+
return null;
21+
}
22+
23+
return ` ${line}`;
24+
};
25+
26+
module.exports = err => {
27+
if (err.statements) {
28+
const statements = JSON.parse(err.statements);
29+
30+
return statements
31+
.map(statement => `${statement[0]}\n${chalk.grey('=>')} ${statement[1]}`)
32+
.join('\n\n') + '\n';
33+
}
34+
35+
if ((err.actualType === 'object' || err.actualType === 'array') && err.actualType === err.expectedType) {
36+
const patch = diff.createPatch('string', err.actual, err.expected);
37+
const msg = patch
38+
.split('\n')
39+
.slice(4)
40+
.map(cleanUp)
41+
.filter(Boolean)
42+
.join('\n');
43+
44+
return `Difference:\n\n${msg}`;
45+
}
46+
47+
if (err.actualType === 'string' && err.expectedType === 'string') {
48+
const patch = diff.diffChars(err.actual, err.expected);
49+
const msg = patch
50+
.map(part => {
51+
if (part.added) {
52+
return chalk.bgGreen.black(part.value);
53+
}
54+
55+
if (part.removed) {
56+
return chalk.bgRed.black(part.value);
57+
}
58+
59+
return part.value;
60+
})
61+
.join('');
62+
63+
return `Difference:\n\n${msg}\n`;
64+
}
65+
66+
return [
67+
'Actual:\n',
68+
`${indentString(err.actual, 2)}\n`,
69+
'Expected:\n',
70+
`${indentString(err.expected, 2)}\n`
71+
].join('\n');
72+
};

0 commit comments

Comments
 (0)