diff --git a/lib/assert.js b/lib/assert.js index 0dae2073f..00375110d 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -5,8 +5,7 @@ const observableToPromise = require('observable-to-promise'); const indentString = require('indent-string'); const isObservable = require('is-observable'); const isPromise = require('is-promise'); -const jestSnapshot = require('jest-snapshot'); -const snapshotState = require('./snapshot-state'); +const Snapshot = require('./snapshot'); const x = module.exports; const noop = () => {}; @@ -152,23 +151,13 @@ x.ifError = (err, msg) => { test(!err, create(err, 'Error', '!==', msg, x.ifError)); }; -x._snapshot = function (tree, optionalMessage, match, snapshotStateGetter) { - // Set defaults - this allows tests to mock deps easily - const toMatchSnapshot = match || jestSnapshot.toMatchSnapshot; - const getState = snapshotStateGetter || snapshotState.get; - - const state = getState(); - - const context = { - dontThrow() {}, - currentTestName: this.title, - snapshotState: state - }; +x._snapshot = function (tree, optionalMessage, customSnapshot) { + const snapshot = customSnapshot || Snapshot.getSnapshot(); // Symbols can't be serialized and saved in a snapshot, that's why tree // is saved in the `__ava_react_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)); + const result = snapshot.match(this.title, serializedTree); let message = 'Please check your code or --update-snapshots'; @@ -176,14 +165,11 @@ x._snapshot = function (tree, optionalMessage, match, snapshotStateGetter) { message += '\n\n' + indentString(optionalMessage, 2); } - state.save(); - 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)); + if (!result.pass) { + expected = result.expected; + // 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 diff --git a/lib/main.js b/lib/main.js index a9205371a..93a263dcc 100644 --- a/lib/main.js +++ b/lib/main.js @@ -3,6 +3,7 @@ const process = require('./process-adapter'); const serializeError = require('./serialize-error'); const globals = require('./globals'); const Runner = require('./runner'); +const snapshot = require('./snapshot').getSnapshot(); const send = process.send; const opts = globals.options; @@ -48,6 +49,8 @@ function test(props) { } function exit() { + snapshot.save(); + const stats = runner._buildStats(); send('results', {stats}); diff --git a/lib/snapshot-state.js b/lib/snapshot-state.js deleted file mode 100644 index c7b942d07..000000000 --- a/lib/snapshot-state.js +++ /dev/null @@ -1,30 +0,0 @@ -'use strict'; -const path = require('path'); -const jestSnapshot = require('jest-snapshot'); -const globals = require('./globals'); - -const x = module.exports; - -x.get = (initializeState, globalsOptions) => { - if (!x.state) { - // Set defaults - this allows tests to mock deps easily - const options = globalsOptions || globals.options; - const initializeSnapshotState = initializeState || jestSnapshot.initializeSnapshotState; - - const filename = options.file; - const dirname = path.dirname(filename); - const snapshotFileName = path.basename(filename) + '.snap'; - const snapshotsFolder = path.join(dirname, '__snapshots__', snapshotFileName); - - x.state = initializeSnapshotState( - filename, - options.updateSnapshots, - snapshotsFolder, - true - ); - } - - return x.state; -}; - -x.state = null; diff --git a/lib/snapshot.js b/lib/snapshot.js new file mode 100644 index 000000000..368fecc15 --- /dev/null +++ b/lib/snapshot.js @@ -0,0 +1,58 @@ +'use strict'; +const path = require('path'); +const fs = require('fs'); +const isEqual = require('lodash.isequal'); +const mkdirp = require('mkdirp'); +const globals = require('./globals'); + +class Snapshot { + constructor(testPath, options) { + if (!testPath) { + throw new TypeError('Test file path is required'); + } + + this.dirPath = path.join(path.dirname(testPath), '__snapshots__'); + this.filePath = path.join(this.dirPath, path.basename(testPath) + '.snap'); + this.tests = {}; + this.options = options || {}; + + if (fs.existsSync(this.filePath)) { + this.tests = JSON.parse(fs.readFileSync(this.filePath)); + } + } + + save() { + mkdirp.sync(this.dirPath); + fs.writeFileSync(this.filePath, JSON.stringify(this.tests, null, ' ')); + } + + match(testTitle, actual) { + const expected = this.tests[testTitle]; + if (!expected || this.options.update) { + this.tests[testTitle] = actual; + + return {pass: true}; + } + + const isMatch = isEqual(actual, expected); + if (isMatch) { + return {pass: true}; + } + + return { + pass: false, + actual, + expected + }; + } +} + +const x = module.exports = Snapshot; + +Snapshot.getSnapshot = () => { + if (!x.snapshot) { + x.snapshot = new Snapshot(globals.options.file, {update: globals.options.updateSnapshots}); + } + + return x.snapshot; +}; diff --git a/package.json b/package.json index 3a370c4b0..9ff5d95fa 100644 --- a/package.json +++ b/package.json @@ -131,7 +131,6 @@ "is-obj": "^1.0.0", "is-observable": "^0.2.0", "is-promise": "^2.1.0", - "jest-snapshot": "^18.1.0", "last-line-stream": "^1.0.0", "lodash.debounce": "^4.0.3", "lodash.difference": "^4.3.0", @@ -142,6 +141,7 @@ "max-timeout": "^1.0.0", "md5-hex": "^2.0.0", "meow": "^3.7.0", + "mkdirp": "^0.5.1", "ms": "^0.7.1", "multimatch": "^2.1.0", "observable-to-promise": "^0.4.0", diff --git a/test/assert.js b/test/assert.js index e0abdf307..4cd1c4c24 100644 --- a/test/assert.js +++ b/test/assert.js @@ -1,6 +1,8 @@ 'use strict'; +const path = require('path'); +const uniqueTempDir = require('unique-temp-dir'); const test = require('tap').test; -const sinon = require('sinon'); +const Snapshot = require('../lib/snapshot'); const assert = require('../lib/assert'); test('.pass()', t => { @@ -487,43 +489,35 @@ test('.deepEqual() should not mask RangeError from underlying assert', t => { t.end(); }); -test('snapshot makes a snapshot using a library and global options', t => { - const saveSpy = sinon.spy(); - const state = {save: saveSpy}; - const stateGetter = sinon.stub().returns(state); - const matchStub = sinon.stub().returns({pass: true}); +test('.snapshot()', t => { + t.plan(4); - assert.title = 'Test name'; + const snapshot = new Snapshot(path.join(uniqueTempDir(), 'test.js')); + t.deepEqual(snapshot.tests, {}); - t.plan(4); + assert.title = 'test title'; t.doesNotThrow(() => { - assert._snapshot('tree', undefined, matchStub, stateGetter); + assert._snapshot('tree', undefined, snapshot); }); - t.ok(stateGetter.called); - - t.match(matchStub.firstCall.thisValue, { - currentTestName: 'Test name', - snapshotState: state + t.throws(() => { + assert._snapshot('grass', undefined, snapshot); }); - t.ok(saveSpy.calledOnce); + t.deepEqual(snapshot.tests, {'test title': 'tree'}); delete assert.title; - t.end(); }); -test('snapshot handles jsx tree', t => { - const saveSpy = sinon.spy(); - const state = {save: saveSpy}; - const stateGetter = sinon.stub().returns(state); - const matchStub = sinon.stub().returns({pass: true}); +test('.snapshot() handles JSX', t => { + t.plan(4); - assert.title = 'Test name'; + const snapshot = new Snapshot(path.join(uniqueTempDir(), 'test.js')); + t.deepEqual(snapshot.tests, {}); - t.plan(5); + assert.title = 'test title'; t.doesNotThrow(() => { const tree = { @@ -534,28 +528,31 @@ test('snapshot handles jsx tree', t => { Object.defineProperty(tree, '$$typeof', {value: Symbol.for('react.test.json')}); - assert._snapshot(tree, undefined, matchStub, stateGetter); + assert._snapshot(tree, undefined, snapshot); }); - t.ok(stateGetter.called); - - const savedTree = JSON.parse(matchStub.firstCall.args[0]); - t.deepEqual(savedTree, { - __ava_react_jsx: { // eslint-disable-line camelcase - type: 'h1', + t.throws(() => { + const tree = { + type: 'div', children: ['Hello'], props: {} - } - }); + }; + + Object.defineProperty(tree, '$$typeof', {value: Symbol.for('react.test.json')}); - t.match(matchStub.firstCall.thisValue, { - currentTestName: 'Test name', - snapshotState: state + assert._snapshot(tree, undefined, snapshot); }); - t.ok(saveSpy.calledOnce); + t.deepEqual(snapshot.tests, { + 'test title': { + __ava_react_jsx: { // eslint-disable-line camelcase + type: 'h1', + children: ['Hello'], + props: {} + } + } + }); delete assert.title; - t.end(); }); diff --git a/test/snapshot-state.js b/test/snapshot-state.js deleted file mode 100644 index 3e324cc30..000000000 --- a/test/snapshot-state.js +++ /dev/null @@ -1,46 +0,0 @@ -'use strict'; -const path = require('path'); -const test = require('tap').test; -const sinon = require('sinon'); -const snapshotState = require('../lib/snapshot-state'); - -test('snapshot state gets created and returned', t => { - const stateStub = sinon.stub().returns('state'); - - t.plan(3); - - t.doesNotThrow(() => { - const result = snapshotState.get(stateStub, { - file: path.join('hello', 'world.test.js'), - updateSnapshots: false - }); - - t.is(result, 'state'); - }); - - t.ok(stateStub.calledWith( - path.join('hello', 'world.test.js'), - false, - path.join('hello', '__snapshots__', 'world.test.js.snap'), - true - )); - - t.end(); -}); - -test('snapshot state is returned immediately if it already exists', t => { - const stateSpy = sinon.spy(); - - t.plan(3); - - snapshotState.state = 'already made state'; - - t.doesNotThrow(() => { - const result = snapshotState.get(stateSpy); - t.is(result, 'already made state'); - }); - - t.false(stateSpy.called); - - t.end(); -}); diff --git a/test/snapshot.js b/test/snapshot.js new file mode 100644 index 000000000..e9b0092bb --- /dev/null +++ b/test/snapshot.js @@ -0,0 +1,94 @@ +'use strict'; +const path = require('path'); +const fs = require('fs'); +const uniqueTempDir = require('unique-temp-dir'); +const mkdirp = require('mkdirp'); +const test = require('tap').test; +const Snapshot = require('../lib/snapshot'); +const globals = require('../lib/globals'); + +test('fail without test path', t => { + t.throws(() => new Snapshot(), 'Test file path is required'); + t.end(); +}); + +test('build paths', t => { + const dirPath = uniqueTempDir(); + + const snapshot = new Snapshot(path.join(dirPath, 'test.js')); + t.is(snapshot.dirPath, path.join(dirPath, '__snapshots__')); + t.is(snapshot.filePath, path.join(dirPath, '__snapshots__', 'test.js.snap')); + t.deepEqual(snapshot.tests, {}); + t.end(); +}); + +test('read existing snapshots', t => { + const dirPath = uniqueTempDir(); + mkdirp.sync(path.join(dirPath, '__snapshots__')); + fs.writeFileSync(path.join(dirPath, '__snapshots__', 'test.js.snap'), JSON.stringify({a: {b: 1}})); + + const snapshot = new Snapshot(path.join(dirPath, 'test.js')); + t.deepEqual(snapshot.tests, {a: {b: 1}}); + t.end(); +}); + +test('match without snapshot', t => { + const snapshot = new Snapshot(path.join(uniqueTempDir(), 'test.js')); + t.deepEqual(snapshot.match('a', {b: 1}), {pass: true}); + t.deepEqual(snapshot.tests, {a: {b: 1}}); + t.end(); +}); + +test('successful match with snapshot', t => { + const snapshot = new Snapshot(path.join(uniqueTempDir(), 'test.js')); + t.deepEqual(snapshot.match('a', {b: 1}), {pass: true}); + t.deepEqual(snapshot.match('a', {b: 1}), {pass: true}); + t.end(); +}); + +test('failed match with snapshot', t => { + const snapshot = new Snapshot(path.join(uniqueTempDir(), 'test.js')); + t.deepEqual(snapshot.match('a', {b: 1}), {pass: true}); + t.deepEqual(snapshot.match('a', {b: 2}), { + pass: false, + actual: {b: 2}, + expected: {b: 1} + }); + t.end(); +}); + +test('update snapshots', t => { + const snapshot = new Snapshot(path.join(uniqueTempDir(), 'test.js'), {update: true}); + t.deepEqual(snapshot.match('a', {b: 1}), {pass: true}); + t.deepEqual(snapshot.match('a', {b: 2}), {pass: true}); + t.end(); +}); + +test('save snapshots', t => { + const dirPath = path.join(uniqueTempDir(), 'sub', 'dir'); + const snapshot = new Snapshot(path.join(dirPath, 'test.js')); + snapshot.match('a', {b: 1}); + snapshot.match('b', {a: 1}); + snapshot.save(); + + const snapshots = JSON.parse(fs.readFileSync(path.join(dirPath, '__snapshots__', 'test.js.snap'))); + t.deepEqual(snapshots, { + a: {b: 1}, + b: {a: 1} + }); + t.end(); +}); + +test('return singleton', t => { + const oldValue = globals.options.file; + globals.options.file = path.join(uniqueTempDir(), 'test.js'); + + const firstSnapshot = Snapshot.getSnapshot(); + t.true(firstSnapshot instanceof Snapshot); + + const secondSnapshot = Snapshot.getSnapshot(); + t.is(secondSnapshot, firstSnapshot); + + globals.options.file = oldValue; + t.end(); +});