Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Alternative for deepEqual, formatting, diffs and snapshots #1341

Merged
merged 24 commits into from
Jun 25, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
1349b17
Extract serialized error formatting into its own module
novemberborn Apr 18, 2017
e3a3ef4
Don't indent actual/expected/difference values
novemberborn Apr 18, 2017
072311f
Ensure ExecutionContext properties are not enumerable
novemberborn Apr 13, 2017
375ca3d
Reorder test-worker imports
novemberborn Apr 19, 2017
a3e2bec
Fix 'label' property in verbose reporter tests
novemberborn Apr 20, 2017
03d85ef
Consistent use of newlines in mini and verbose reporters
novemberborn Apr 20, 2017
6e54d92
Reduce repetition between error message and value label
novemberborn Apr 20, 2017
b2daa32
Use Concordance
novemberborn Apr 4, 2017
9ffbb46
Implement AVA snapshots using Concordance
novemberborn Apr 12, 2017
9100d12
Document t.title accessor
novemberborn Apr 13, 2017
92c9e48
Add @concordance/react plugin
novemberborn May 18, 2017
56dd845
Utilize maxDepth option
novemberborn May 21, 2017
bf071d2
Utilize invert option in snapshot diffs
novemberborn Jun 18, 2017
e425e66
Print useful errors when snapshot files are incompatible or corrupted
novemberborn Jun 18, 2017
e2ec60d
Fail gracefully when legacy snapshot files are encountered
novemberborn Jun 18, 2017
aacffe1
Increase debounce delays in watcher
novemberborn Jun 19, 2017
d9a2072
Include dirty sources in watcher debug output
novemberborn Jun 19, 2017
1aac50f
Track snapshot files touched during test run, ignore in watcher
novemberborn Jun 19, 2017
b89cb6f
Treat loaded snapshot files as test dependencies
novemberborn Jun 19, 2017
ff37dc0
Determine snapshot directory by where the test is located
novemberborn Jun 19, 2017
68a4ab2
Add integration test for appending to an existing snapshot file
novemberborn Jun 19, 2017
d5ce698
Update readme
novemberborn Jun 19, 2017
cea9369
concordance@2
novemberborn Jun 19, 2017
1d9d915
Automatically watch for snapshot changes
novemberborn Jun 25, 2017
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/recipes/watch-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ AVA uses [`chokidar`] as the file watcher. Note that even if you see warnings ab

In AVA there's a distinction between *source files* and *test files*. As you can imagine the *test files* contain your tests. *Source files* are all other files that are needed for the tests to run, be it your source code or test fixtures.

By default AVA watches for changes to the test files, `package.json`, and any other `.js` files. It'll ignore files in [certain directories](https://github.com/novemberborn/ignore-by-default/blob/master/index.js) as provided by the [`ignore-by-default`] package.
By default AVA watches for changes to the test files, snapshot files, `package.json`, and any other `.js` files. It'll ignore files in [certain directories](https://github.com/novemberborn/ignore-by-default/blob/master/index.js) as provided by the [`ignore-by-default`] package.

You can configure patterns for the source files in the [`ava` section of your `package.json`] file, using the `source` key.

Expand Down
148 changes: 95 additions & 53 deletions lib/assert.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,32 @@
'use strict';
const concordance = require('concordance');
const coreAssert = require('core-assert');
const deepEqual = require('lodash.isequal');
const observableToPromise = require('observable-to-promise');
const isObservable = require('is-observable');
const isPromise = require('is-promise');
const jestDiff = require('jest-diff');
const concordanceOptions = require('./concordance-options').default;
const concordanceDiffOptions = require('./concordance-options').diff;
const enhanceAssert = require('./enhance-assert');
const formatAssertError = require('./format-assert-error');
const snapshotManager = require('./snapshot-manager');

function formatDescriptorDiff(actualDescriptor, expectedDescriptor, options) {
options = Object.assign({}, options, concordanceDiffOptions);
return {
label: 'Difference:',
formatted: concordance.diffDescriptors(actualDescriptor, expectedDescriptor, options)
};
}

function formatDescriptorWithLabel(label, descriptor) {
return {
label,
formatted: concordance.formatDescriptor(descriptor, concordanceOptions)
};
}

function formatWithLabel(label, value) {
return formatDescriptorWithLabel(label, concordance.describe(value, concordanceOptions));
}

class AssertionError extends Error {
constructor(opts) {
Expand Down Expand Up @@ -61,16 +81,12 @@ function wrapAssertions(callbacks) {
if (Object.is(actual, expected)) {
pass(this);
} else {
const diff = formatAssertError.formatDiff(actual, expected);
const values = diff ? [diff] : [
formatAssertError.formatWithLabel('Actual:', actual),
formatAssertError.formatWithLabel('Must be the same as:', expected)
];

const actualDescriptor = concordance.describe(actual, concordanceOptions);
const expectedDescriptor = concordance.describe(expected, concordanceOptions);
fail(this, new AssertionError({
assertion: 'is',
message,
values
values: [formatDescriptorDiff(actualDescriptor, expectedDescriptor)]
}));
}
},
Expand All @@ -80,37 +96,36 @@ function wrapAssertions(callbacks) {
fail(this, new AssertionError({
assertion: 'not',
message,
values: [formatAssertError.formatWithLabel('Value is the same as:', actual)]
values: [formatWithLabel('Value is the same as:', actual)]
}));
} else {
pass(this);
}
},

deepEqual(actual, expected, message) {
if (deepEqual(actual, expected)) {
const result = concordance.compare(actual, expected, concordanceOptions);
if (result.pass) {
pass(this);
} else {
const diff = formatAssertError.formatDiff(actual, expected);
const values = diff ? [diff] : [
formatAssertError.formatWithLabel('Actual:', actual),
formatAssertError.formatWithLabel('Must be deeply equal to:', expected)
];

const actualDescriptor = result.actual || concordance.describe(actual, concordanceOptions);
const expectedDescriptor = result.expected || concordance.describe(expected, concordanceOptions);
fail(this, new AssertionError({
assertion: 'deepEqual',
message,
values
values: [formatDescriptorDiff(actualDescriptor, expectedDescriptor)]
}));
}
},

notDeepEqual(actual, expected, message) {
if (deepEqual(actual, expected)) {
const result = concordance.compare(actual, expected, concordanceOptions);
if (result.pass) {
const actualDescriptor = result.actual || concordance.describe(actual, concordanceOptions);
fail(this, new AssertionError({
assertion: 'notDeepEqual',
message,
values: [formatAssertError.formatWithLabel('Value is deeply equal:', actual)]
values: [formatDescriptorWithLabel('Value is deeply equal:', actualDescriptor)]
}));
} else {
pass(this);
Expand All @@ -128,7 +143,7 @@ function wrapAssertions(callbacks) {
assertion: 'throws',
improperUsage: true,
message: '`t.throws()` must be called with a function, Promise, or Observable',
values: [formatAssertError.formatWithLabel('Called with:', fn)]
values: [formatWithLabel('Called with:', fn)]
}));
return;
}
Expand Down Expand Up @@ -157,15 +172,13 @@ function wrapAssertions(callbacks) {
}, coreAssertThrowsErrorArg);
return actual;
} catch (err) {
const values = threw ?
[formatAssertError.formatWithLabel('Threw unexpected exception:', actual)] :
null;

throw new AssertionError({
assertion: 'throws',
message,
stack,
values
values: threw ?
[formatWithLabel('Threw unexpected exception:', actual)] :
null
});
}
};
Expand All @@ -177,7 +190,7 @@ function wrapAssertions(callbacks) {
throw new AssertionError({
assertion: 'throws',
message: 'Expected promise to be rejected, but it was resolved instead',
values: [formatAssertError.formatWithLabel('Resolved with:', value)]
values: [formatWithLabel('Resolved with:', value)]
});
}, reason => test(makeRethrow(reason), stack));

Expand Down Expand Up @@ -206,7 +219,7 @@ function wrapAssertions(callbacks) {
assertion: 'notThrows',
improperUsage: true,
message: '`t.notThrows()` must be called with a function, Promise, or Observable',
values: [formatAssertError.formatWithLabel('Called with:', fn)]
values: [formatWithLabel('Called with:', fn)]
}));
return;
}
Expand All @@ -219,7 +232,7 @@ function wrapAssertions(callbacks) {
assertion: 'notThrows',
message,
stack,
values: [formatAssertError.formatWithLabel('Threw:', err.actual)]
values: [formatWithLabel('Threw:', err.actual)]
});
}
};
Expand All @@ -246,28 +259,57 @@ function wrapAssertions(callbacks) {
fail(this, new AssertionError({
assertion: 'ifError',
message,
values: [formatAssertError.formatWithLabel('Error:', actual)]
values: [formatWithLabel('Error:', actual)]
}));
} else {
pass(this);
}
},

snapshot(actual, message) {
const state = this._test.getSnapshotState();
const result = state.match(this.title, actual);
snapshot(expected, optionsOrMessage, message) {
const options = {};
if (typeof optionsOrMessage === 'string') {
message = optionsOrMessage;
} else if (optionsOrMessage) {
options.id = optionsOrMessage.id;
}
options.expected = expected;
options.message = message;

let result;
try {
result = this._test.compareWithSnapshot(options);
} catch (err) {
if (!(err instanceof snapshotManager.SnapshotError)) {
throw err;
}

const improperUsage = {name: err.name, snapPath: err.snapPath};
if (err instanceof snapshotManager.VersionMismatchError) {
improperUsage.snapVersion = err.snapVersion;
improperUsage.expectedVersion = err.expectedVersion;
}

fail(this, new AssertionError({
assertion: 'snapshot',
message: message || 'Could not compare snapshot',
improperUsage
}));
return;
}

if (result.pass) {
pass(this);
} else {
const diff = jestDiff(result.expected.trim(), result.actual.trim(), {expand: true})
// Remove annotation
.split('\n')
.slice(3)
.join('\n');
} else if (result.actual) {
fail(this, new AssertionError({
assertion: 'snapshot',
message: message || 'Did not match snapshot',
values: [{label: 'Difference:', formatted: diff}]
values: [formatDescriptorDiff(result.actual, result.expected, {invert: true})]
}));
} else {
fail(this, new AssertionError({
assertion: 'snapshot',
message: message || 'No snapshot available, run with --update-snapshots'
}));
}
}
Expand All @@ -280,7 +322,7 @@ function wrapAssertions(callbacks) {
assertion: 'truthy',
message,
operator: '!!',
values: [formatAssertError.formatWithLabel('Value is not truthy:', actual)]
values: [formatWithLabel('Value is not truthy:', actual)]
});
}
},
Expand All @@ -291,7 +333,7 @@ function wrapAssertions(callbacks) {
assertion: 'falsy',
message,
operator: '!',
values: [formatAssertError.formatWithLabel('Value is not falsy:', actual)]
values: [formatWithLabel('Value is not falsy:', actual)]
});
}
},
Expand All @@ -301,7 +343,7 @@ function wrapAssertions(callbacks) {
throw new AssertionError({
assertion: 'true',
message,
values: [formatAssertError.formatWithLabel('Value is not `true`:', actual)]
values: [formatWithLabel('Value is not `true`:', actual)]
});
}
},
Expand All @@ -311,7 +353,7 @@ function wrapAssertions(callbacks) {
throw new AssertionError({
assertion: 'false',
message,
values: [formatAssertError.formatWithLabel('Value is not `false`:', actual)]
values: [formatWithLabel('Value is not `false`:', actual)]
});
}
},
Expand All @@ -322,15 +364,15 @@ function wrapAssertions(callbacks) {
assertion: 'regex',
improperUsage: true,
message: '`t.regex()` must be called with a string',
values: [formatAssertError.formatWithLabel('Called with:', string)]
values: [formatWithLabel('Called with:', string)]
});
}
if (!(regex instanceof RegExp)) {
throw new AssertionError({
assertion: 'regex',
improperUsage: true,
message: '`t.regex()` must be called with a regular expression',
values: [formatAssertError.formatWithLabel('Called with:', regex)]
values: [formatWithLabel('Called with:', regex)]
});
}

Expand All @@ -339,8 +381,8 @@ function wrapAssertions(callbacks) {
assertion: 'regex',
message,
values: [
formatAssertError.formatWithLabel('Value must match expression:', string),
formatAssertError.formatWithLabel('Regular expression:', regex)
formatWithLabel('Value must match expression:', string),
formatWithLabel('Regular expression:', regex)
]
});
}
Expand All @@ -352,15 +394,15 @@ function wrapAssertions(callbacks) {
assertion: 'notRegex',
improperUsage: true,
message: '`t.notRegex()` must be called with a string',
values: [formatAssertError.formatWithLabel('Called with:', string)]
values: [formatWithLabel('Called with:', string)]
});
}
if (!(regex instanceof RegExp)) {
throw new AssertionError({
assertion: 'notRegex',
improperUsage: true,
message: '`t.notRegex()` must be called with a regular expression',
values: [formatAssertError.formatWithLabel('Called with:', regex)]
values: [formatWithLabel('Called with:', regex)]
});
}

Expand All @@ -369,8 +411,8 @@ function wrapAssertions(callbacks) {
assertion: 'notRegex',
message,
values: [
formatAssertError.formatWithLabel('Value must not match expression:', string),
formatAssertError.formatWithLabel('Regular expression:', regex)
formatWithLabel('Value must not match expression:', string),
formatWithLabel('Regular expression:', regex)
]
});
}
Expand Down
2 changes: 1 addition & 1 deletion lib/ava-files.js
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ class AvaFiles {
ignored = getDefaultIgnorePatterns().concat(ignored, overrideDefaultIgnorePatterns);

if (paths.length === 0) {
paths = ['package.json', '**/*.js'];
paths = ['package.json', '**/*.js', '**/*.snap'];
}

paths = paths.concat(this.files);
Expand Down
Loading