From a946746b92c3a709d98d15a0e787e227473d9c4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikita=20Volodin=20=5B=D0=9D=D0=B8=D0=BA=D0=B8=D1=82=D0=B0?= =?UTF-8?q?=20=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=5D?= Date: Sun, 24 Mar 2019 14:17:56 -0400 Subject: [PATCH 01/23] Refactor assertions to be a class This changes assertions to be properties on the class. Before, assertions were functions which needed to be bound to the test instance. This led to the fact that snapshot() assertion accessed property which only existed on Test - to compare snapshots. Now, the functions which assertions access are clearly defined as those that need to be implemented by child class. This change also updates how ExecutionContext is created, as well as minor changes to the test/assert.js. --- lib/assert.js | 331 +++++++++++++++++++++++++++--------------- lib/enhance-assert.js | 1 + lib/test.js | 97 ++++++------- test/assert.js | 94 ++++++------ 4 files changed, 306 insertions(+), 217 deletions(-) diff --git a/lib/assert.js b/lib/assert.js index 1a9dccf11..ecce5795d 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -1,5 +1,6 @@ 'use strict'; const concordance = require('concordance'); +const empower = require('empower-core'); const isError = require('is-error'); const isPromise = require('is-promise'); const concordanceOptions = require('./concordance-options').default; @@ -229,39 +230,119 @@ function assertExpectations({assertion, actual, expectations, message, prefix, s } } -function wrapAssertions(callbacks) { - const {pass, pending, fail} = callbacks; - const noop = () => {}; +const noop = () => {}; +class Assertions { + /** + * @protected + */ + _pass() { + throw new Error('not implemented'); + } + + /** + * @param {Promise} promise + * @protected + */ + _pending() { + throw new Error('not implemented'); + } - const assertions = { - pass() { - pass(this); - }, + /** + * @param {Error} err + * @protected + */ + _fail() { + throw new Error('not implemented'); + } - fail(message) { - fail(this, new AssertionError({ + /** + * @param {Array} args + * @protected + */ + _skip() { + throw new Error('not implemented'); + } + + /** + * @param { {expected, [id]: string, [message]: string} } options + * @returns { {pass: boolean, [actual], [expected]} } + * @protected + */ + _compareWithSnapshot() { + throw new Error('not implemented'); + } + + /** + * @private + */ + _skippable(assertionFn) { + assertionFn.skip = (...args) => this._skip(...args); + return assertionFn; + } + + /** + * @private + */ + _enhanced(pattern, assertionFn) { + const empowerOptions = { + onError: event => { + if (event.powerAssertContext) { + event.error.statements = enhanceAssert.formatter(event.powerAssertContext); + } + + this._fail(event.error); + }, + onSuccess: () => { + this._pass(); + }, + bindReceiver: false, + patterns: [pattern] + }; + return empower(assertionFn, empowerOptions); + } + + constructor() { + /** + * @public + */ + this.pass = this._skippable(() => { + this._pass(); + }); + + /** + * @param message + * @public + */ + this.fail = this._skippable(message => { + this._fail(new AssertionError({ assertion: 'fail', message: message || 'Test failed via `t.fail()`' })); - }, + }); - is(actual, expected, message) { + /** + * @param actual + * @param expected + * @param message + * @public + */ + this.is = this._skippable((actual, expected, message) => { if (Object.is(actual, expected)) { - pass(this); + this._pass(); } else { const result = concordance.compare(actual, expected, concordanceOptions); const actualDescriptor = result.actual || concordance.describe(actual, concordanceOptions); const expectedDescriptor = result.expected || concordance.describe(expected, concordanceOptions); if (result.pass) { - fail(this, new AssertionError({ + this._fail(new AssertionError({ assertion: 'is', message, raw: {actual, expected}, values: [formatDescriptorWithLabel('Values are deeply equal to each other, but they are not the same:', actualDescriptor)] })); } else { - fail(this, new AssertionError({ + this._fail(new AssertionError({ assertion: 'is', message, raw: {actual, expected}, @@ -269,55 +350,75 @@ function wrapAssertions(callbacks) { })); } } - }, + }); - not(actual, expected, message) { + /** + * @param actual + * @param expected + * @param message + * @public + */ + this.not = this._skippable((actual, expected, message) => { if (Object.is(actual, expected)) { - fail(this, new AssertionError({ + this._fail(new AssertionError({ assertion: 'not', message, raw: {actual, expected}, values: [formatWithLabel('Value is the same as:', actual)] })); } else { - pass(this); + this._pass(); } - }, + }); - deepEqual(actual, expected, message) { + /** + * @param actual + * @param expected + * @param message + * @public + */ + this.deepEqual = this._skippable((actual, expected, message) => { const result = concordance.compare(actual, expected, concordanceOptions); if (result.pass) { - pass(this); + this._pass(); } else { const actualDescriptor = result.actual || concordance.describe(actual, concordanceOptions); const expectedDescriptor = result.expected || concordance.describe(expected, concordanceOptions); - fail(this, new AssertionError({ + this._fail(new AssertionError({ assertion: 'deepEqual', message, raw: {actual, expected}, values: [formatDescriptorDiff(actualDescriptor, expectedDescriptor)] })); } - }, + }); - notDeepEqual(actual, expected, message) { + /** + * + * @param actual + * @param expected + * @param message + * + * @public + */ + this.notDeepEqual = this._skippable((actual, expected, message) => { const result = concordance.compare(actual, expected, concordanceOptions); if (result.pass) { const actualDescriptor = result.actual || concordance.describe(actual, concordanceOptions); - fail(this, new AssertionError({ + this._fail(new AssertionError({ assertion: 'notDeepEqual', message, raw: {actual, expected}, values: [formatDescriptorWithLabel('Value is deeply equal:', actualDescriptor)] })); } else { - pass(this); + this._pass(); } - }, + }); - throws(fn, expectations, message) { + this.throws = this._skippable((fn, expectations, message) => { if (typeof fn !== 'function') { - fail(this, new AssertionError({ + this._fail(new AssertionError({ assertion: 'throws', improperUsage: true, message: '`t.throws()` must be called with a function', @@ -327,23 +428,22 @@ function wrapAssertions(callbacks) { } try { - expectations = validateExpectations('throws', expectations, arguments.length); + expectations = validateExpectations('throws', expectations); } catch (error) { - fail(this, error); + this._fail(error); return; } let retval; - let actual; - let threw = false; + let actual = null; try { retval = fn(); if (isPromise(retval)) { try { - retval.catch(noop); + Promise.resolve(retval).catch(noop); } catch (_) {} - fail(this, new AssertionError({ + this._fail(new AssertionError({ assertion: 'throws', message, values: [formatWithLabel('Function returned a promise. Use `t.throwsAsync()` instead:', retval)] @@ -352,11 +452,10 @@ function wrapAssertions(callbacks) { } } catch (error) { actual = error; - threw = true; } - if (!threw) { - fail(this, new AssertionError({ + if (!actual) { + this._fail(new AssertionError({ assertion: 'throws', message, values: [formatWithLabel('Function returned:', retval)] @@ -372,16 +471,16 @@ function wrapAssertions(callbacks) { message, prefix: 'Function threw' }); - pass(this); + this._pass(); return actual; } catch (error) { - fail(this, error); + this._fail(error); } - }, + }); - throwsAsync(thrower, expectations, message) { + this.throwsAsync = this._skippable((thrower, expectations, message) => { if (typeof thrower !== 'function' && !isPromise(thrower)) { - fail(this, new AssertionError({ + this._fail(new AssertionError({ assertion: 'throwsAsync', improperUsage: true, message: '`t.throwsAsync()` must be called with a function or promise', @@ -391,16 +490,16 @@ function wrapAssertions(callbacks) { } try { - expectations = validateExpectations('throwsAsync', expectations, arguments.length); + expectations = validateExpectations('throwsAsync', expectations); } catch (error) { - fail(this, error); + this._fail(error); return Promise.resolve(); } const handlePromise = (promise, wasReturned) => { // Record stack before it gets lost in the promise chain. const stack = getStack(); - const intermediate = promise.then(value => { + const intermediate = Promise.resolve(promise).then(value => { throw new AssertionError({ assertion: 'throwsAsync', message, @@ -419,7 +518,7 @@ function wrapAssertions(callbacks) { return reason; }); - pending(this, intermediate); + this._pending(intermediate); // Don't reject the returned promise, even if the assertion fails. return intermediate.catch(noop); }; @@ -429,17 +528,15 @@ function wrapAssertions(callbacks) { } let retval; - let actual; - let threw = false; + let actual = null; try { retval = thrower(); } catch (error) { actual = error; - threw = true; } - if (threw) { - fail(this, new AssertionError({ + if (actual) { + this._fail(new AssertionError({ assertion: 'throwsAsync', message, actualStack: actual.stack, @@ -452,17 +549,17 @@ function wrapAssertions(callbacks) { return handlePromise(retval, true); } - fail(this, new AssertionError({ + this._fail(new AssertionError({ assertion: 'throwsAsync', message, values: [formatWithLabel('Function returned:', retval)] })); return Promise.resolve(); - }, + }); - notThrows(fn, message) { + this.notThrows = this._skippable((fn, message) => { if (typeof fn !== 'function') { - fail(this, new AssertionError({ + this._fail(new AssertionError({ assertion: 'notThrows', improperUsage: true, message: '`t.notThrows()` must be called with a function', @@ -474,7 +571,7 @@ function wrapAssertions(callbacks) { try { fn(); } catch (error) { - fail(this, new AssertionError({ + this._fail(new AssertionError({ assertion: 'notThrows', message, actualStack: error.stack, @@ -483,12 +580,12 @@ function wrapAssertions(callbacks) { return; } - pass(this); - }, + this._pass(); + }); - notThrowsAsync(nonThrower, message) { + this.notThrowsAsync = this._skippable((nonThrower, message) => { if (typeof nonThrower !== 'function' && !isPromise(nonThrower)) { - fail(this, new AssertionError({ + this._fail(new AssertionError({ assertion: 'notThrowsAsync', improperUsage: true, message: '`t.notThrowsAsync()` must be called with a function or promise', @@ -500,7 +597,7 @@ function wrapAssertions(callbacks) { const handlePromise = (promise, wasReturned) => { // Record stack before it gets lost in the promise chain. const stack = getStack(); - const intermediate = promise.then(noop, reason => { + const intermediate = Promise.resolve(promise).then(noop, reason => { throw new AssertionError({ assertion: 'notThrowsAsync', message, @@ -508,7 +605,7 @@ function wrapAssertions(callbacks) { values: [formatWithLabel(`${wasReturned ? 'Returned promise' : 'Promise'} rejected with:`, reason)] }); }); - pending(this, intermediate); + this._pending(intermediate); // Don't reject the returned promise, even if the assertion fails. return intermediate.catch(noop); }; @@ -521,7 +618,7 @@ function wrapAssertions(callbacks) { try { retval = nonThrower(); } catch (error) { - fail(this, new AssertionError({ + this._fail(new AssertionError({ assertion: 'notThrowsAsync', message, actualStack: error.stack, @@ -531,7 +628,7 @@ function wrapAssertions(callbacks) { } if (!isPromise(retval)) { - fail(this, new AssertionError({ + this._fail(new AssertionError({ assertion: 'notThrowsAsync', message, values: [formatWithLabel('Function did not return a promise. Use `t.notThrows()` instead:', retval)] @@ -540,9 +637,9 @@ function wrapAssertions(callbacks) { } return handlePromise(retval, true); - }, + }); - snapshot(expected, optionsOrMessage, message) { + this.snapshot = this._skippable((expected, optionsOrMessage, message) => { const options = {}; if (typeof optionsOrMessage === 'string') { message = optionsOrMessage; @@ -555,7 +652,7 @@ function wrapAssertions(callbacks) { let result; try { - result = this.compareWithSnapshot(options); + result = this._compareWithSnapshot(options); } catch (error) { if (!(error instanceof snapshotManager.SnapshotError)) { throw error; @@ -567,7 +664,7 @@ function wrapAssertions(callbacks) { improperUsage.expectedVersion = error.expectedVersion; } - fail(this, new AssertionError({ + this._fail(new AssertionError({ assertion: 'snapshot', message: message || 'Could not compare snapshot', improperUsage @@ -576,74 +673,74 @@ function wrapAssertions(callbacks) { } if (result.pass) { - pass(this); + this._pass(); } else if (result.actual) { - fail(this, new AssertionError({ + this._fail(new AssertionError({ assertion: 'snapshot', message: message || 'Did not match snapshot', values: [formatDescriptorDiff(result.actual, result.expected, {invert: true})] })); } else { - fail(this, new AssertionError({ + this._fail(new AssertionError({ assertion: 'snapshot', message: message || 'No snapshot available, run with --update-snapshots' })); } - }, + }); - truthy(actual, message) { + this.truthy = this._skippable((actual, message) => { if (actual) { - pass(this); + this._pass(); } else { - fail(this, new AssertionError({ + this._fail(new AssertionError({ assertion: 'truthy', message, operator: '!!', values: [formatWithLabel('Value is not truthy:', actual)] })); } - }, + }); - falsy(actual, message) { + this.falsy = this._skippable((actual, message) => { if (actual) { - fail(this, new AssertionError({ + this._fail(new AssertionError({ assertion: 'falsy', message, operator: '!', values: [formatWithLabel('Value is not falsy:', actual)] })); } else { - pass(this); + this._pass(); } - }, + }); - true(actual, message) { + this.true = this._skippable((actual, message) => { if (actual === true) { - pass(this); + this._pass(); } else { - fail(this, new AssertionError({ + this._fail(new AssertionError({ assertion: 'true', message, values: [formatWithLabel('Value is not `true`:', actual)] })); } - }, + }); - false(actual, message) { + this.false = this._skippable((actual, message) => { if (actual === false) { - pass(this); + this._pass(); } else { - fail(this, new AssertionError({ + this._fail(new AssertionError({ assertion: 'false', message, values: [formatWithLabel('Value is not `false`:', actual)] })); } - }, + }); - regex(string, regex, message) { + this.regex = this._skippable((string, regex, message) => { if (typeof string !== 'string') { - fail(this, new AssertionError({ + this._fail(new AssertionError({ assertion: 'regex', improperUsage: true, message: '`t.regex()` must be called with a string', @@ -653,7 +750,7 @@ function wrapAssertions(callbacks) { } if (!(regex instanceof RegExp)) { - fail(this, new AssertionError({ + this._fail(new AssertionError({ assertion: 'regex', improperUsage: true, message: '`t.regex()` must be called with a regular expression', @@ -663,7 +760,7 @@ function wrapAssertions(callbacks) { } if (!regex.test(string)) { - fail(this, new AssertionError({ + this._fail(new AssertionError({ assertion: 'regex', message, values: [ @@ -674,12 +771,12 @@ function wrapAssertions(callbacks) { return; } - pass(this); - }, + this._pass(); + }); - notRegex(string, regex, message) { + this.notRegex = this._skippable((string, regex, message) => { if (typeof string !== 'string') { - fail(this, new AssertionError({ + this._fail(new AssertionError({ assertion: 'notRegex', improperUsage: true, message: '`t.notRegex()` must be called with a string', @@ -689,7 +786,7 @@ function wrapAssertions(callbacks) { } if (!(regex instanceof RegExp)) { - fail(this, new AssertionError({ + this._fail(new AssertionError({ assertion: 'notRegex', improperUsage: true, message: '`t.notRegex()` must be called with a regular expression', @@ -699,7 +796,7 @@ function wrapAssertions(callbacks) { } if (regex.test(string)) { - fail(this, new AssertionError({ + this._fail(new AssertionError({ assertion: 'notRegex', message, values: [ @@ -710,24 +807,22 @@ function wrapAssertions(callbacks) { return; } - pass(this); - } - }; - - const enhancedAssertions = enhanceAssert(pass, fail, { - assert(actual, message) { - if (!actual) { - throw new AssertionError({ - assertion: 'assert', - message, - operator: '!!', - values: [formatWithLabel('Value is not truthy:', actual)] - }); - } - } - }); + this._pass(); + }); - return Object.assign(assertions, enhancedAssertions); + this.assert = this._skippable(this._enhanced( + 'assert(value, [message])', + (actual, message) => { + if (!actual) { + throw new AssertionError({ + assertion: 'assert', + message, + operator: '!!', + values: [formatWithLabel('Value is not truthy:', actual)] + }); + } + }) + ); + } } - -exports.wrapAssertions = wrapAssertions; +exports.Assertions = Assertions; diff --git a/lib/enhance-assert.js b/lib/enhance-assert.js index 88ace2290..0243f8eb0 100644 --- a/lib/enhance-assert.js +++ b/lib/enhance-assert.js @@ -48,3 +48,4 @@ const enhanceAssert = (pass, fail, assertions) => { }; module.exports = enhanceAssert; +module.exports.formatter = formatter; diff --git a/lib/test.js b/lib/test.js index ad86c1c4f..b58f9cecb 100644 --- a/lib/test.js +++ b/lib/test.js @@ -22,68 +22,39 @@ const captureStack = start => { return obj.stack; }; -const assertions = assert.wrapAssertions({ - pass(test) { - test.countPassedAssertion(); - }, - - pending(test, promise) { - test.addPendingAssertion(promise); - }, - - fail(test, error) { - test.addFailedAssertion(error); - } -}); -const assertionNames = Object.keys(assertions); - -function log(...inputArgs) { - const args = inputArgs.map(value => { - return typeof value === 'string' ? - value : - concordance.format(value, concordanceOptions); - }); - - if (args.length > 0) { - this.addLog(args.join(' ')); - } -} - -function plan(count) { - this.plan(count, captureStack(this.plan)); -} - -function timeout(ms) { - this.timeout(ms); -} - const testMap = new WeakMap(); -class ExecutionContext { +class ExecutionContext extends assert.Assertions { constructor(test) { + super(); testMap.set(this, test); - const skip = () => { - test.countPassedAssertion(); - }; - - const boundPlan = plan.bind(test); - boundPlan.skip = () => {}; - - Object.defineProperties(this, assertionNames.reduce((props, name) => { - props[name] = {value: assertions[name].bind(test)}; - props[name].value.skip = skip; - return props; - }, { - log: {value: log.bind(test)}, - plan: {value: boundPlan}, - timeout: {value: timeout.bind(test)} - })); - + this.plan.skip = () => {}; this.snapshot.skip = () => { test.skipSnapshot(); }; } + log(...inputArgs) { + const args = inputArgs.map(value => { + return typeof value === 'string' ? + value : + concordance.format(value, concordanceOptions); + }); + + if (args.length > 0) { + testMap.get(this).addLog(args.join(' ')); + } + } + + plan(count) { + const test = testMap.get(this); + test.plan(count, captureStack(test.plan)); // Or captureStack(this.plan) ? + } + + timeout(ms) { + testMap.get(this).timeout(ms); + } + get end() { const end = testMap.get(this).bindEndCallback(); const endFn = error => end(error, captureStack(endFn)); @@ -102,6 +73,26 @@ class ExecutionContext { testMap.get(this).contextRef.set(context); } + _pass() { + testMap.get(this).countPassedAssertion(); + } + + _pending(promise) { + testMap.get(this).addPendingAssertion(promise); + } + + _fail(err) { + testMap.get(this).addFailedAssertion(err); + } + + _skip() { + testMap.get(this).countPassedAssertion(); + } + + _compareWithSnapshot(options) { + return testMap.get(this).compareWithSnapshot(options); + } + _throwsArgStart(assertion, file, line) { testMap.get(this).trackThrows({assertion, file, line}); } diff --git a/test/assert.js b/test/assert.js index 83566ce63..8fb5bdcd7 100644 --- a/test/assert.js +++ b/test/assert.js @@ -9,40 +9,30 @@ const renderer = require('react-test-renderer'); const {test} = require('tap'); const assert = require('../lib/assert'); const snapshotManager = require('../lib/snapshot-manager'); -const Test = require('../lib/test'); const HelloMessage = require('./fixture/hello-message'); let lastFailure = null; let lastPassed = false; -const assertions = assert.wrapAssertions({ - pass(testObj) { - if (testObj !== assertions && !(testObj instanceof Test)) { - throw new Error('Expected testObj'); - } +const assertions = new class extends assert.Assertions { + _pass() { lastPassed = true; - }, - - pending(testObj, promise) { - if (testObj !== assertions && !(testObj instanceof Test)) { - throw new Error('Expected testObj'); - } + } + _pending(promise) { promise.then(() => { lastPassed = true; }, err => { lastFailure = err; }); - }, - - fail(testObj, error) { - if (testObj !== assertions && !(testObj instanceof Test)) { - throw new Error('Expected testObj'); - } + } + _fail(error) { lastFailure = error; } -}); + + _skip() {} +}(); function assertFailure(t, subset) { if (!lastFailure) { @@ -1245,28 +1235,40 @@ test('.snapshot()', t => { fixedLocation: null, updating }); - const setup = title => { - return new Test({ - title, - compareTestSnapshot: options => manager.compare(options) - }); + const setup = _title => { + return new class extends assertions.constructor { + constructor(title) { + super(); + this.title = title; + this.snapshotInvocationCount = 0; + } + + _compareWithSnapshot(assertionOptions) { + return manager.compare({ + belongsTo: assertionOptions.id || this.title, + expected: assertionOptions.expected, + index: assertionOptions.id ? 0 : this.snapshotInvocationCount++, + label: assertionOptions.id ? '' : assertionOptions.message || `Snapshot ${this.snapshotInvocationCount}` + }); + } + }(_title); }; passes(t, () => { - const testInstance = setup('passes'); - assertions.snapshot.call(testInstance, {foo: 'bar'}); - assertions.snapshot.call(testInstance, {foo: 'bar'}, {id: 'fixed id'}, 'message not included in snapshot report'); - assertions.snapshot.call(testInstance, React.createElement(HelloMessage, {name: 'Sindre'})); - assertions.snapshot.call(testInstance, renderer.create(React.createElement(HelloMessage, {name: 'Sindre'})).toJSON()); + const assertions = setup('passes'); + assertions.snapshot({foo: 'ar'}); + assertions.snapshot({foo: 'bar'}, {id: 'fixed id'}, 'message not included in snapshot report'); + assertions.snapshot(React.createElement(HelloMessage, {name: 'Sindre'})); + assertions.snapshot(renderer.create(React.createElement(HelloMessage, {name: 'Sindre'})).toJSON()); }); { - const testInstance = setup('fails'); + const assertions = setup('fails'); if (updating) { - assertions.snapshot.call(testInstance, {foo: 'bar'}); + assertions.snapshot({foo: 'bar'}); } else { failsWith(t, () => { - assertions.snapshot.call(testInstance, {foo: 'not bar'}); + assertions.snapshot({foo: 'not bar'}); }, { assertion: 'snapshot', message: 'Did not match snapshot', @@ -1276,8 +1278,8 @@ test('.snapshot()', t => { } failsWith(t, () => { - const testInstance = setup('fails (fixed id)'); - assertions.snapshot.call(testInstance, {foo: 'not bar'}, {id: 'fixed id'}, 'different message, also not included in snapshot report'); + const assertions = setup('fails (fixed id)'); + assertions.snapshot({foo: 'not bar'}, {id: 'fixed id'}, 'different message, also not included in snapshot report'); }, { assertion: 'snapshot', message: 'different message, also not included in snapshot report', @@ -1285,12 +1287,12 @@ test('.snapshot()', t => { }); { - const testInstance = setup('fails'); + const assertions = setup('fails'); if (updating) { - assertions.snapshot.call(testInstance, {foo: 'bar'}, 'my message'); + assertions.snapshot({foo: 'bar'}, 'my message'); } else { failsWith(t, () => { - assertions.snapshot.call(testInstance, {foo: 'not bar'}, 'my message'); + assertions.snapshot({foo: 'not bar'}, 'my message'); }, { assertion: 'snapshot', message: 'my message', @@ -1300,23 +1302,23 @@ test('.snapshot()', t => { } { - const testInstance = setup('rendered comparison'); + const assertions = setup('rendered comparison'); if (updating) { - assertions.snapshot.call(testInstance, renderer.create(React.createElement(HelloMessage, {name: 'Sindre'})).toJSON()); + assertions.snapshot(renderer.create(React.createElement(HelloMessage, {name: 'Sindre'})).toJSON()); } else { passes(t, () => { - assertions.snapshot.call(testInstance, React.createElement('div', null, 'Hello ', React.createElement('mark', null, 'Sindre'))); + assertions.snapshot(React.createElement('div', null, 'Hello ', React.createElement('mark', null, 'Sindre'))); }); } } { - const testInstance = setup('rendered comparison'); + const assertions = setup('rendered comparison'); if (updating) { - assertions.snapshot.call(testInstance, renderer.create(React.createElement(HelloMessage, {name: 'Sindre'})).toJSON()); + assertions.snapshot(renderer.create(React.createElement(HelloMessage, {name: 'Sindre'})).toJSON()); } else { failsWith(t, () => { - assertions.snapshot.call(testInstance, renderer.create(React.createElement(HelloMessage, {name: 'Vadim'})).toJSON()); + assertions.snapshot(renderer.create(React.createElement(HelloMessage, {name: 'Vadim'})).toJSON()); }, { assertion: 'snapshot', message: 'Did not match snapshot', @@ -1326,12 +1328,12 @@ test('.snapshot()', t => { } { - const testInstance = setup('element comparison'); + const assertions = setup('element comparison'); if (updating) { - assertions.snapshot.call(testInstance, React.createElement(HelloMessage, {name: 'Sindre'})); + assertions.snapshot(React.createElement(HelloMessage, {name: 'Sindre'})); } else { failsWith(t, () => { - assertions.snapshot.call(testInstance, React.createElement(HelloMessage, {name: 'Vadim'})); + assertions.snapshot(React.createElement(HelloMessage, {name: 'Vadim'})); }, { assertion: 'snapshot', message: 'Did not match snapshot', From b4953735343be00d2bdb32a8d8608d25b0084c25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikita=20Volodin=20=5B=D0=9D=D0=B8=D0=BA=D0=B8=D1=82=D0=B0?= =?UTF-8?q?=20=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=5D?= Date: Sun, 24 Mar 2019 14:25:49 -0400 Subject: [PATCH 02/23] Mark enhanceAssert() for deletion --- lib/enhance-assert.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/enhance-assert.js b/lib/enhance-assert.js index 0243f8eb0..812c24017 100644 --- a/lib/enhance-assert.js +++ b/lib/enhance-assert.js @@ -27,6 +27,7 @@ const formatter = context => { .reverse(); }; +// Todo Should be removed const enhanceAssert = (pass, fail, assertions) => { const empower = require('empower-core'); return empower(assertions, { From b27e0e9ae365ae1ff1afddd16f76f7e14394586e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikita=20Volodin=20=5B=D0=9D=D0=B8=D0=BA=D0=B8=D1=82=D0=B0?= =?UTF-8?q?=20=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=5D?= Date: Sun, 24 Mar 2019 14:53:59 -0400 Subject: [PATCH 03/23] Do not rely on passed arguments length in validateExpectations() --- lib/assert.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/assert.js b/lib/assert.js index ecce5795d..c307764a7 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -70,12 +70,12 @@ function getStack() { return obj.stack; } -function validateExpectations(assertion, expectations, numArgs) { // eslint-disable-line complexity +function validateExpectations(assertion, expectations) { // eslint-disable-line complexity if (typeof expectations === 'function') { expectations = {instanceOf: expectations}; } else if (typeof expectations === 'string' || expectations instanceof RegExp) { expectations = {message: expectations}; - } else if (numArgs === 1 || expectations === null) { + } else if (expectations === null || expectations === undefined) { expectations = {}; } else if (typeof expectations !== 'object' || Array.isArray(expectations) || Object.keys(expectations).length === 0) { throw new AssertionError({ From 977080b120c25d0bc9dbab99a38e29e11b4ef6ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikita=20Volodin=20=5B=D0=9D=D0=B8=D0=BA=D0=B8=D1=82=D0=B0?= =?UTF-8?q?=20=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=5D?= Date: Sun, 24 Mar 2019 15:03:34 -0400 Subject: [PATCH 04/23] Add comments describing changes --- lib/assert.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/assert.js b/lib/assert.js index c307764a7..be49cd0e9 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -440,6 +440,9 @@ class Assertions { retval = fn(); if (isPromise(retval)) { try { + // In reality isPromise returns if something is PromiseLike + // i.e. has a .then() function. Wrapping in Promise.resolve() + // casts it to actual promise Promise.resolve(retval).catch(noop); } catch (_) {} @@ -499,6 +502,7 @@ class Assertions { const handlePromise = (promise, wasReturned) => { // Record stack before it gets lost in the promise chain. const stack = getStack(); + // Wrap in Promise.resolve to cast PromiseLike into real Promise const intermediate = Promise.resolve(promise).then(value => { throw new AssertionError({ assertion: 'throwsAsync', From ce49d5344e63bea843dc130e44d94f3004e6bbc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikita=20Volodin=20=5B=D0=9D=D0=B8=D0=BA=D0=B8=D1=82=D0=B0?= =?UTF-8?q?=20=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=5D?= Date: Sun, 31 Mar 2019 16:49:44 -0400 Subject: [PATCH 05/23] Add regression tests for #1676 --- test/assert.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/test/assert.js b/test/assert.js index 8fb5bdcd7..e8d65b89d 100644 --- a/test/assert.js +++ b/test/assert.js @@ -856,6 +856,27 @@ test('.throws()', gather(t => { throw err; }, {code: 42}); }); + + // Regression test for https://github.com/avajs/ava/issues/1676 + fails(t, () => { + assertions.throws(() => { + throw new Error('foo'); + }, false); + }); + + // Regression test for https://github.com/avajs/ava/issues/1676 + passes(t, () => { + assertions.throws(() => { + throw new Error('foo'); + }, null); + }); + + // Regression test for https://github.com/avajs/ava/issues/1676 + fails(t, () => { + assertions.throws(() => { + throw new Error('foo'); + }, undefined); + }); })); test('.throws() returns the thrown error', t => { From 9061a0818e28f1d2c7d11503c217e43417b33d90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikita=20Volodin=20=5B=D0=9D=D0=B8=D0=BA=D0=B8=D1=82=D0=B0?= =?UTF-8?q?=20=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=5D?= Date: Sun, 31 Mar 2019 17:16:58 -0400 Subject: [PATCH 06/23] Retain functionality passing number of args as #1676 fix --- lib/assert.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/assert.js b/lib/assert.js index be49cd0e9..225949904 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -70,12 +70,12 @@ function getStack() { return obj.stack; } -function validateExpectations(assertion, expectations) { // eslint-disable-line complexity +function validateExpectations(assertion, expectations, numArgs) { // eslint-disable-line complexity if (typeof expectations === 'function') { expectations = {instanceOf: expectations}; } else if (typeof expectations === 'string' || expectations instanceof RegExp) { expectations = {message: expectations}; - } else if (expectations === null || expectations === undefined) { + } else if (numArgs === 1 || expectations === null) { expectations = {}; } else if (typeof expectations !== 'object' || Array.isArray(expectations) || Object.keys(expectations).length === 0) { throw new AssertionError({ @@ -416,7 +416,8 @@ class Assertions { } }); - this.throws = this._skippable((fn, expectations, message) => { + this.throws = this._skippable((...args) => { + let [fn, expectations, message] = args; if (typeof fn !== 'function') { this._fail(new AssertionError({ assertion: 'throws', @@ -428,7 +429,7 @@ class Assertions { } try { - expectations = validateExpectations('throws', expectations); + expectations = validateExpectations('throws', expectations, args.length); } catch (error) { this._fail(error); return; @@ -481,7 +482,8 @@ class Assertions { } }); - this.throwsAsync = this._skippable((thrower, expectations, message) => { + this.throwsAsync = this._skippable((...args) => { + let [thrower, expectations, message] = args; if (typeof thrower !== 'function' && !isPromise(thrower)) { this._fail(new AssertionError({ assertion: 'throwsAsync', @@ -493,7 +495,7 @@ class Assertions { } try { - expectations = validateExpectations('throwsAsync', expectations); + expectations = validateExpectations('throwsAsync', expectations, args.length); } catch (error) { this._fail(error); return Promise.resolve(); From 4efe9d516cef45fe7d8d3693a32a07e2d4224f17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikita=20Volodin=20=5B=D0=9D=D0=B8=D0=BA=D0=B8=D1=82=D0=B0?= =?UTF-8?q?=20=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=5D?= Date: Sun, 31 Mar 2019 17:19:44 -0400 Subject: [PATCH 07/23] Add comment explaining usage of ...args in .throws() --- lib/assert.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/assert.js b/lib/assert.js index 225949904..9b72c80e2 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -417,6 +417,9 @@ class Assertions { }); this.throws = this._skippable((...args) => { + // Since arrow functions do not support 'arguments', we are using rest + // operator, so we can determine the total number of arguments passed + // to the function. let [fn, expectations, message] = args; if (typeof fn !== 'function') { this._fail(new AssertionError({ From 63c8ffc84c9927c1e9a371f40e5ba61f10b654cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikita=20Volodin=20=5B=D0=9D=D0=B8=D0=BA=D0=B8=D1=82=D0=B0?= =?UTF-8?q?=20=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=5D?= Date: Sun, 31 Mar 2019 20:19:18 -0400 Subject: [PATCH 08/23] Hide private methods by not assigning them to instance --- lib/assert.js | 240 ++++++++++++++++++++++---------------------------- 1 file changed, 105 insertions(+), 135 deletions(-) diff --git a/lib/assert.js b/lib/assert.js index 9b72c80e2..dd843c952 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -232,89 +232,59 @@ function assertExpectations({assertion, actual, expectations, message, prefix, s const noop = () => {}; class Assertions { - /** - * @protected - */ - _pass() { - throw new Error('not implemented'); - } - - /** - * @param {Promise} promise - * @protected - */ - _pending() { - throw new Error('not implemented'); - } - - /** - * @param {Error} err - * @protected - */ - _fail() { - throw new Error('not implemented'); - } - - /** - * @param {Array} args - * @protected - */ - _skip() { - throw new Error('not implemented'); - } - - /** - * @param { {expected, [id]: string, [message]: string} } options - * @returns { {pass: boolean, [actual], [expected]} } - * @protected - */ - _compareWithSnapshot() { - throw new Error('not implemented'); - } - - /** - * @private - */ - _skippable(assertionFn) { - assertionFn.skip = (...args) => this._skip(...args); - return assertionFn; - } - - /** - * @private - */ - _enhanced(pattern, assertionFn) { - const empowerOptions = { - onError: event => { - if (event.powerAssertContext) { - event.error.statements = enhanceAssert.formatter(event.powerAssertContext); - } + constructor({ + pass = () => { + throw new Error('not implemented'); + }, + pending = () => { + throw new Error('not implemented'); + }, + fail = () => { + throw new Error('not implemented'); + }, + skip = () => { + throw new Error('not implemented'); + }, + compareWithSnapshot = () => { + throw new Error('not implemented'); + } + } = {}) { + const _skippable = assertionFn => { + assertionFn.skip = (...args) => skip(...args); + return assertionFn; + }; - this._fail(event.error); - }, - onSuccess: () => { - this._pass(); - }, - bindReceiver: false, - patterns: [pattern] + const _enhanced = (pattern, assertionFn) => { + const empowerOptions = { + onError: event => { + if (event.powerAssertContext) { + event.error.statements = enhanceAssert.formatter(event.powerAssertContext); + } + + fail(event.error); + }, + onSuccess: () => { + pass(); + }, + bindReceiver: false, + patterns: [pattern] + }; + return empower(assertionFn, empowerOptions); }; - return empower(assertionFn, empowerOptions); - } - constructor() { /** * @public */ - this.pass = this._skippable(() => { - this._pass(); + this.pass = _skippable(() => { + pass(); }); /** * @param message * @public */ - this.fail = this._skippable(message => { - this._fail(new AssertionError({ + this.fail = _skippable(message => { + fail(new AssertionError({ assertion: 'fail', message: message || 'Test failed via `t.fail()`' })); @@ -326,23 +296,23 @@ class Assertions { * @param message * @public */ - this.is = this._skippable((actual, expected, message) => { + this.is = _skippable((actual, expected, message) => { if (Object.is(actual, expected)) { - this._pass(); + pass(); } else { const result = concordance.compare(actual, expected, concordanceOptions); const actualDescriptor = result.actual || concordance.describe(actual, concordanceOptions); const expectedDescriptor = result.expected || concordance.describe(expected, concordanceOptions); if (result.pass) { - this._fail(new AssertionError({ + fail(new AssertionError({ assertion: 'is', message, raw: {actual, expected}, values: [formatDescriptorWithLabel('Values are deeply equal to each other, but they are not the same:', actualDescriptor)] })); } else { - this._fail(new AssertionError({ + fail(new AssertionError({ assertion: 'is', message, raw: {actual, expected}, @@ -358,16 +328,16 @@ class Assertions { * @param message * @public */ - this.not = this._skippable((actual, expected, message) => { + this.not = _skippable((actual, expected, message) => { if (Object.is(actual, expected)) { - this._fail(new AssertionError({ + fail(new AssertionError({ assertion: 'not', message, raw: {actual, expected}, values: [formatWithLabel('Value is the same as:', actual)] })); } else { - this._pass(); + pass(); } }); @@ -377,14 +347,14 @@ class Assertions { * @param message * @public */ - this.deepEqual = this._skippable((actual, expected, message) => { + this.deepEqual = _skippable((actual, expected, message) => { const result = concordance.compare(actual, expected, concordanceOptions); if (result.pass) { - this._pass(); + pass(); } else { const actualDescriptor = result.actual || concordance.describe(actual, concordanceOptions); const expectedDescriptor = result.expected || concordance.describe(expected, concordanceOptions); - this._fail(new AssertionError({ + fail(new AssertionError({ assertion: 'deepEqual', message, raw: {actual, expected}, @@ -401,28 +371,28 @@ class Assertions { * * @public */ - this.notDeepEqual = this._skippable((actual, expected, message) => { + this.notDeepEqual = _skippable((actual, expected, message) => { const result = concordance.compare(actual, expected, concordanceOptions); if (result.pass) { const actualDescriptor = result.actual || concordance.describe(actual, concordanceOptions); - this._fail(new AssertionError({ + fail(new AssertionError({ assertion: 'notDeepEqual', message, raw: {actual, expected}, values: [formatDescriptorWithLabel('Value is deeply equal:', actualDescriptor)] })); } else { - this._pass(); + pass(); } }); - this.throws = this._skippable((...args) => { + this.throws = _skippable((...args) => { // Since arrow functions do not support 'arguments', we are using rest // operator, so we can determine the total number of arguments passed // to the function. let [fn, expectations, message] = args; if (typeof fn !== 'function') { - this._fail(new AssertionError({ + fail(new AssertionError({ assertion: 'throws', improperUsage: true, message: '`t.throws()` must be called with a function', @@ -434,7 +404,7 @@ class Assertions { try { expectations = validateExpectations('throws', expectations, args.length); } catch (error) { - this._fail(error); + fail(error); return; } @@ -450,7 +420,7 @@ class Assertions { Promise.resolve(retval).catch(noop); } catch (_) {} - this._fail(new AssertionError({ + fail(new AssertionError({ assertion: 'throws', message, values: [formatWithLabel('Function returned a promise. Use `t.throwsAsync()` instead:', retval)] @@ -462,7 +432,7 @@ class Assertions { } if (!actual) { - this._fail(new AssertionError({ + fail(new AssertionError({ assertion: 'throws', message, values: [formatWithLabel('Function returned:', retval)] @@ -478,17 +448,17 @@ class Assertions { message, prefix: 'Function threw' }); - this._pass(); + pass(); return actual; } catch (error) { - this._fail(error); + fail(error); } }); - this.throwsAsync = this._skippable((...args) => { + this.throwsAsync = _skippable((...args) => { let [thrower, expectations, message] = args; if (typeof thrower !== 'function' && !isPromise(thrower)) { - this._fail(new AssertionError({ + fail(new AssertionError({ assertion: 'throwsAsync', improperUsage: true, message: '`t.throwsAsync()` must be called with a function or promise', @@ -500,7 +470,7 @@ class Assertions { try { expectations = validateExpectations('throwsAsync', expectations, args.length); } catch (error) { - this._fail(error); + fail(error); return Promise.resolve(); } @@ -527,7 +497,7 @@ class Assertions { return reason; }); - this._pending(intermediate); + pending(intermediate); // Don't reject the returned promise, even if the assertion fails. return intermediate.catch(noop); }; @@ -545,7 +515,7 @@ class Assertions { } if (actual) { - this._fail(new AssertionError({ + fail(new AssertionError({ assertion: 'throwsAsync', message, actualStack: actual.stack, @@ -558,7 +528,7 @@ class Assertions { return handlePromise(retval, true); } - this._fail(new AssertionError({ + fail(new AssertionError({ assertion: 'throwsAsync', message, values: [formatWithLabel('Function returned:', retval)] @@ -566,9 +536,9 @@ class Assertions { return Promise.resolve(); }); - this.notThrows = this._skippable((fn, message) => { + this.notThrows = _skippable((fn, message) => { if (typeof fn !== 'function') { - this._fail(new AssertionError({ + fail(new AssertionError({ assertion: 'notThrows', improperUsage: true, message: '`t.notThrows()` must be called with a function', @@ -580,7 +550,7 @@ class Assertions { try { fn(); } catch (error) { - this._fail(new AssertionError({ + fail(new AssertionError({ assertion: 'notThrows', message, actualStack: error.stack, @@ -589,12 +559,12 @@ class Assertions { return; } - this._pass(); + pass(); }); - this.notThrowsAsync = this._skippable((nonThrower, message) => { + this.notThrowsAsync = _skippable((nonThrower, message) => { if (typeof nonThrower !== 'function' && !isPromise(nonThrower)) { - this._fail(new AssertionError({ + fail(new AssertionError({ assertion: 'notThrowsAsync', improperUsage: true, message: '`t.notThrowsAsync()` must be called with a function or promise', @@ -614,7 +584,7 @@ class Assertions { values: [formatWithLabel(`${wasReturned ? 'Returned promise' : 'Promise'} rejected with:`, reason)] }); }); - this._pending(intermediate); + pending(intermediate); // Don't reject the returned promise, even if the assertion fails. return intermediate.catch(noop); }; @@ -627,7 +597,7 @@ class Assertions { try { retval = nonThrower(); } catch (error) { - this._fail(new AssertionError({ + fail(new AssertionError({ assertion: 'notThrowsAsync', message, actualStack: error.stack, @@ -637,7 +607,7 @@ class Assertions { } if (!isPromise(retval)) { - this._fail(new AssertionError({ + fail(new AssertionError({ assertion: 'notThrowsAsync', message, values: [formatWithLabel('Function did not return a promise. Use `t.notThrows()` instead:', retval)] @@ -648,7 +618,7 @@ class Assertions { return handlePromise(retval, true); }); - this.snapshot = this._skippable((expected, optionsOrMessage, message) => { + this.snapshot = _skippable((expected, optionsOrMessage, message) => { const options = {}; if (typeof optionsOrMessage === 'string') { message = optionsOrMessage; @@ -661,7 +631,7 @@ class Assertions { let result; try { - result = this._compareWithSnapshot(options); + result = compareWithSnapshot(options); } catch (error) { if (!(error instanceof snapshotManager.SnapshotError)) { throw error; @@ -673,7 +643,7 @@ class Assertions { improperUsage.expectedVersion = error.expectedVersion; } - this._fail(new AssertionError({ + fail(new AssertionError({ assertion: 'snapshot', message: message || 'Could not compare snapshot', improperUsage @@ -682,26 +652,26 @@ class Assertions { } if (result.pass) { - this._pass(); + pass(); } else if (result.actual) { - this._fail(new AssertionError({ + fail(new AssertionError({ assertion: 'snapshot', message: message || 'Did not match snapshot', values: [formatDescriptorDiff(result.actual, result.expected, {invert: true})] })); } else { - this._fail(new AssertionError({ + fail(new AssertionError({ assertion: 'snapshot', message: message || 'No snapshot available, run with --update-snapshots' })); } }); - this.truthy = this._skippable((actual, message) => { + this.truthy = _skippable((actual, message) => { if (actual) { - this._pass(); + pass(); } else { - this._fail(new AssertionError({ + fail(new AssertionError({ assertion: 'truthy', message, operator: '!!', @@ -710,24 +680,24 @@ class Assertions { } }); - this.falsy = this._skippable((actual, message) => { + this.falsy = _skippable((actual, message) => { if (actual) { - this._fail(new AssertionError({ + fail(new AssertionError({ assertion: 'falsy', message, operator: '!', values: [formatWithLabel('Value is not falsy:', actual)] })); } else { - this._pass(); + pass(); } }); - this.true = this._skippable((actual, message) => { + this.true = _skippable((actual, message) => { if (actual === true) { - this._pass(); + pass(); } else { - this._fail(new AssertionError({ + fail(new AssertionError({ assertion: 'true', message, values: [formatWithLabel('Value is not `true`:', actual)] @@ -735,11 +705,11 @@ class Assertions { } }); - this.false = this._skippable((actual, message) => { + this.false = _skippable((actual, message) => { if (actual === false) { - this._pass(); + pass(); } else { - this._fail(new AssertionError({ + fail(new AssertionError({ assertion: 'false', message, values: [formatWithLabel('Value is not `false`:', actual)] @@ -747,9 +717,9 @@ class Assertions { } }); - this.regex = this._skippable((string, regex, message) => { + this.regex = _skippable((string, regex, message) => { if (typeof string !== 'string') { - this._fail(new AssertionError({ + fail(new AssertionError({ assertion: 'regex', improperUsage: true, message: '`t.regex()` must be called with a string', @@ -759,7 +729,7 @@ class Assertions { } if (!(regex instanceof RegExp)) { - this._fail(new AssertionError({ + fail(new AssertionError({ assertion: 'regex', improperUsage: true, message: '`t.regex()` must be called with a regular expression', @@ -769,7 +739,7 @@ class Assertions { } if (!regex.test(string)) { - this._fail(new AssertionError({ + fail(new AssertionError({ assertion: 'regex', message, values: [ @@ -780,12 +750,12 @@ class Assertions { return; } - this._pass(); + pass(); }); - this.notRegex = this._skippable((string, regex, message) => { + this.notRegex = _skippable((string, regex, message) => { if (typeof string !== 'string') { - this._fail(new AssertionError({ + fail(new AssertionError({ assertion: 'notRegex', improperUsage: true, message: '`t.notRegex()` must be called with a string', @@ -795,7 +765,7 @@ class Assertions { } if (!(regex instanceof RegExp)) { - this._fail(new AssertionError({ + fail(new AssertionError({ assertion: 'notRegex', improperUsage: true, message: '`t.notRegex()` must be called with a regular expression', @@ -805,7 +775,7 @@ class Assertions { } if (regex.test(string)) { - this._fail(new AssertionError({ + fail(new AssertionError({ assertion: 'notRegex', message, values: [ @@ -816,10 +786,10 @@ class Assertions { return; } - this._pass(); + pass(); }); - this.assert = this._skippable(this._enhanced( + this.assert = _skippable(_enhanced( 'assert(value, [message])', (actual, message) => { if (!actual) { From 9e22fd27f7681760294ed79a041ee3015e745f33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikita=20Volodin=20=5B=D0=9D=D0=B8=D0=BA=D0=B8=D1=82=D0=B0?= =?UTF-8?q?=20=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=5D?= Date: Mon, 1 Apr 2019 00:49:23 -0400 Subject: [PATCH 09/23] Adjust how we extends Assertions class Now we pass functions to super constructor to hide private functions. --- lib/test.js | 38 ++++++++++++++++------------------- test/assert.js | 54 ++++++++++++++++++++++++++------------------------ 2 files changed, 45 insertions(+), 47 deletions(-) diff --git a/lib/test.js b/lib/test.js index b58f9cecb..955358e1f 100644 --- a/lib/test.js +++ b/lib/test.js @@ -25,7 +25,23 @@ const captureStack = start => { const testMap = new WeakMap(); class ExecutionContext extends assert.Assertions { constructor(test) { - super(); + super({ + pass: () => { + test.countPassedAssertion(); + }, + pending: (promise) => { + test.addPendingAssertion(promise); + }, + fail: (err) => { + test.addFailedAssertion(err); + }, + skip: () => { + test.countPassedAssertion(); + }, + compareWithSnapshot: (options) => { + return test.compareWithSnapshot(options); + } + }); testMap.set(this, test); this.plan.skip = () => {}; @@ -73,26 +89,6 @@ class ExecutionContext extends assert.Assertions { testMap.get(this).contextRef.set(context); } - _pass() { - testMap.get(this).countPassedAssertion(); - } - - _pending(promise) { - testMap.get(this).addPendingAssertion(promise); - } - - _fail(err) { - testMap.get(this).addFailedAssertion(err); - } - - _skip() { - testMap.get(this).countPassedAssertion(); - } - - _compareWithSnapshot(options) { - return testMap.get(this).compareWithSnapshot(options); - } - _throwsArgStart(assertion, file, line) { testMap.get(this).trackThrows({assertion, file, line}); } diff --git a/test/assert.js b/test/assert.js index e8d65b89d..925d9c6cd 100644 --- a/test/assert.js +++ b/test/assert.js @@ -15,23 +15,25 @@ let lastFailure = null; let lastPassed = false; const assertions = new class extends assert.Assertions { - _pass() { - lastPassed = true; - } - - _pending(promise) { - promise.then(() => { - lastPassed = true; - }, err => { - lastFailure = err; - }); - } - - _fail(error) { - lastFailure = error; + constructor(overwrites = {}) { + super({ + pass: () => { + lastPassed = true; + }, + pending: (promise) => { + promise.then(() => { + lastPassed = true; + }, err => { + lastFailure = err; + }); + }, + fail: (error) => { + lastFailure = error; + }, + skip: () => {}, + ...overwrites, + }) } - - _skip() {} }(); function assertFailure(t, subset) { @@ -1259,19 +1261,19 @@ test('.snapshot()', t => { const setup = _title => { return new class extends assertions.constructor { constructor(title) { - super(); + super({ + compareWithSnapshot: (assertionOptions) => { + return manager.compare({ + belongsTo: assertionOptions.id || this.title, + expected: assertionOptions.expected, + index: assertionOptions.id ? 0 : this.snapshotInvocationCount++, + label: assertionOptions.id ? '' : assertionOptions.message || `Snapshot ${this.snapshotInvocationCount}` + }); + } + }); this.title = title; this.snapshotInvocationCount = 0; } - - _compareWithSnapshot(assertionOptions) { - return manager.compare({ - belongsTo: assertionOptions.id || this.title, - expected: assertionOptions.expected, - index: assertionOptions.id ? 0 : this.snapshotInvocationCount++, - label: assertionOptions.id ? '' : assertionOptions.message || `Snapshot ${this.snapshotInvocationCount}` - }); - } }(_title); }; From 1ef541f23a3a7f66f168ed751f8815813daff075 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikita=20Volodin=20=5B=D0=9D=D0=B8=D0=BA=D0=B8=D1=82=D0=B0?= =?UTF-8?q?=20=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=5D?= Date: Mon, 1 Apr 2019 00:59:37 -0400 Subject: [PATCH 10/23] Remove unneeded try-catch in .throws() assertion --- lib/assert.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/lib/assert.js b/lib/assert.js index dd843c952..3962081c4 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -413,13 +413,10 @@ class Assertions { try { retval = fn(); if (isPromise(retval)) { - try { - // In reality isPromise returns if something is PromiseLike - // i.e. has a .then() function. Wrapping in Promise.resolve() - // casts it to actual promise - Promise.resolve(retval).catch(noop); - } catch (_) {} - + // In reality isPromise returns if something is PromiseLike + // i.e. has a .then() function. Wrapping in Promise.resolve() + // casts it to actual promise + Promise.resolve(retval).catch(noop); fail(new AssertionError({ assertion: 'throws', message, From 7adec902614138dd017980b7b9e0bea1646f3a90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikita=20Volodin=20=5B=D0=9D=D0=B8=D0=BA=D0=B8=D1=82=D0=B0?= =?UTF-8?q?=20=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=5D?= Date: Mon, 1 Apr 2019 01:12:16 -0400 Subject: [PATCH 11/23] Fix typo which should have failed the test --- test/assert.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/assert.js b/test/assert.js index 925d9c6cd..0128e12e5 100644 --- a/test/assert.js +++ b/test/assert.js @@ -1279,7 +1279,7 @@ test('.snapshot()', t => { passes(t, () => { const assertions = setup('passes'); - assertions.snapshot({foo: 'ar'}); + assertions.snapshot({foo: 'bar'}); assertions.snapshot({foo: 'bar'}, {id: 'fixed id'}, 'message not included in snapshot report'); assertions.snapshot(React.createElement(HelloMessage, {name: 'Sindre'})); assertions.snapshot(renderer.create(React.createElement(HelloMessage, {name: 'Sindre'})).toJSON()); From 086c33950a34272b044d20521fd2211867784d35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikita=20Volodin=20=5B=D0=9D=D0=B8=D0=BA=D0=B8=D1=82=D0=B0?= =?UTF-8?q?=20=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=5D?= Date: Mon, 1 Apr 2019 01:12:21 -0400 Subject: [PATCH 12/23] Style change by xo --- test/assert.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/assert.js b/test/assert.js index 0128e12e5..2fd1b039b 100644 --- a/test/assert.js +++ b/test/assert.js @@ -20,19 +20,19 @@ const assertions = new class extends assert.Assertions { pass: () => { lastPassed = true; }, - pending: (promise) => { + pending: promise => { promise.then(() => { lastPassed = true; }, err => { lastFailure = err; }); }, - fail: (error) => { + fail: error => { lastFailure = error; }, skip: () => {}, - ...overwrites, - }) + ...overwrites + }); } }(); @@ -1262,7 +1262,7 @@ test('.snapshot()', t => { return new class extends assertions.constructor { constructor(title) { super({ - compareWithSnapshot: (assertionOptions) => { + compareWithSnapshot: assertionOptions => { return manager.compare({ belongsTo: assertionOptions.id || this.title, expected: assertionOptions.expected, From 1761c2635bf47c5a34b7b87da2f9d546ff14bdf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikita=20Volodin=20=5B=D0=9D=D0=B8=D0=BA=D0=B8=D1=82=D0=B0?= =?UTF-8?q?=20=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=5D?= Date: Mon, 1 Apr 2019 01:15:31 -0400 Subject: [PATCH 13/23] Separate test lines to make sure each of them passes --- test/assert.js | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/test/assert.js b/test/assert.js index 2fd1b039b..eed93d2d8 100644 --- a/test/assert.js +++ b/test/assert.js @@ -1277,13 +1277,25 @@ test('.snapshot()', t => { }(_title); }; - passes(t, () => { + { const assertions = setup('passes'); - assertions.snapshot({foo: 'bar'}); - assertions.snapshot({foo: 'bar'}, {id: 'fixed id'}, 'message not included in snapshot report'); - assertions.snapshot(React.createElement(HelloMessage, {name: 'Sindre'})); - assertions.snapshot(renderer.create(React.createElement(HelloMessage, {name: 'Sindre'})).toJSON()); - }); + + passes(t, () => { + assertions.snapshot({foo: 'bar'}); + }); + + passes(t, () => { + assertions.snapshot({foo: 'bar'}, {id: 'fixed id'}, 'message not included in snapshot report'); + }); + + passes(t, () => { + assertions.snapshot(React.createElement(HelloMessage, {name: 'Sindre'})); + }); + + passes(t, () => { + assertions.snapshot(renderer.create(React.createElement(HelloMessage, {name: 'Sindre'})).toJSON()); + }); + } { const assertions = setup('fails'); From b74519d44264c9eb79263e1c12a5b11279226c69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikita=20Volodin=20=5B=D0=9D=D0=B8=D0=BA=D0=B8=D1=82=D0=B0?= =?UTF-8?q?=20=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=5D?= Date: Mon, 1 Apr 2019 01:19:07 -0400 Subject: [PATCH 14/23] Adjust style with xo --- lib/test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/test.js b/lib/test.js index 955358e1f..e1e34a25b 100644 --- a/lib/test.js +++ b/lib/test.js @@ -29,16 +29,16 @@ class ExecutionContext extends assert.Assertions { pass: () => { test.countPassedAssertion(); }, - pending: (promise) => { + pending: promise => { test.addPendingAssertion(promise); }, - fail: (err) => { + fail: err => { test.addFailedAssertion(err); }, skip: () => { test.countPassedAssertion(); }, - compareWithSnapshot: (options) => { + compareWithSnapshot: options => { return test.compareWithSnapshot(options); } }); From a752a75a38bfe1de1e905bd4811a9259ed6f56a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikita=20Volodin=20=5B=D0=9D=D0=B8=D0=BA=D0=B8=D1=82=D0=B0?= =?UTF-8?q?=20=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=5D?= Date: Mon, 1 Apr 2019 01:36:30 -0400 Subject: [PATCH 15/23] Remove unneeded enhanceAssert() and move comment about PATTERNS --- lib/assert.js | 3 +++ lib/enhance-assert.js | 29 ----------------------------- 2 files changed, 3 insertions(+), 29 deletions(-) diff --git a/lib/assert.js b/lib/assert.js index 3962081c4..15ab4944c 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -254,6 +254,9 @@ class Assertions { return assertionFn; }; + // When adding new enhanced functions with new 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. const _enhanced = (pattern, assertionFn) => { const empowerOptions = { onError: event => { diff --git a/lib/enhance-assert.js b/lib/enhance-assert.js index 812c24017..e15f3ec95 100644 --- a/lib/enhance-assert.js +++ b/lib/enhance-assert.js @@ -4,13 +4,6 @@ const dotProp = require('dot-prop'); const generate = require('@babel/generator').default; const concordanceOptions = require('./concordance-options').default; -// 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. -const PATTERNS = [ - 't.assert(value, [message])' -]; - const computeStatement = node => generate(node).code; const getNode = (ast, path) => dotProp.get(ast, path.replace(/\//g, '.')); @@ -27,26 +20,4 @@ const formatter = context => { .reverse(); }; -// Todo Should be removed -const enhanceAssert = (pass, fail, assertions) => { - const empower = require('empower-core'); - return empower(assertions, { - destructive: true, - onError(event) { - const {error} = event; - if (event.powerAssertContext) { // Context may be missing in internal tests. - error.statements = formatter(event.powerAssertContext); - } - - fail(this, error); - }, - onSuccess() { - pass(this); - }, - patterns: PATTERNS, - bindReceiver: false - }); -}; - -module.exports = enhanceAssert; module.exports.formatter = formatter; From d6d89ec7fb689dc7254b4ba6e74b811888d16928 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikita=20Volodin=20=5B=D0=9D=D0=B8=D0=BA=D0=B8=D1=82=D0=B0?= =?UTF-8?q?=20=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=5D?= Date: Sat, 6 Apr 2019 13:24:51 -0400 Subject: [PATCH 16/23] Remove unneeded jsdoc comments --- lib/assert.js | 33 --------------------------------- 1 file changed, 33 deletions(-) diff --git a/lib/assert.js b/lib/assert.js index 15ab4944c..9e4e5394e 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -275,17 +275,10 @@ class Assertions { return empower(assertionFn, empowerOptions); }; - /** - * @public - */ this.pass = _skippable(() => { pass(); }); - /** - * @param message - * @public - */ this.fail = _skippable(message => { fail(new AssertionError({ assertion: 'fail', @@ -293,12 +286,6 @@ class Assertions { })); }); - /** - * @param actual - * @param expected - * @param message - * @public - */ this.is = _skippable((actual, expected, message) => { if (Object.is(actual, expected)) { pass(); @@ -325,12 +312,6 @@ class Assertions { } }); - /** - * @param actual - * @param expected - * @param message - * @public - */ this.not = _skippable((actual, expected, message) => { if (Object.is(actual, expected)) { fail(new AssertionError({ @@ -344,12 +325,6 @@ class Assertions { } }); - /** - * @param actual - * @param expected - * @param message - * @public - */ this.deepEqual = _skippable((actual, expected, message) => { const result = concordance.compare(actual, expected, concordanceOptions); if (result.pass) { @@ -366,14 +341,6 @@ class Assertions { } }); - /** - * - * @param actual - * @param expected - * @param message - * - * @public - */ this.notDeepEqual = _skippable((actual, expected, message) => { const result = concordance.compare(actual, expected, concordanceOptions); if (result.pass) { From 261b59c9684c1ce7a21221cd6d5eac58819c767b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikita=20Volodin=20=5B=D0=9D=D0=B8=D0=BA=D0=B8=D1=82=D0=B0?= =?UTF-8?q?=20=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=5D?= Date: Sat, 6 Apr 2019 13:54:55 -0400 Subject: [PATCH 17/23] Check that all assertions are properly bound --- test/assert.js | 99 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/test/assert.js b/test/assert.js index eed93d2d8..18ad12615 100644 --- a/test/assert.js +++ b/test/assert.js @@ -170,6 +170,11 @@ test('.pass()', t => { assertions.pass(); }); + passes(t, () => { + const {pass} = assertions; + pass(); + }); + t.end(); }); @@ -188,6 +193,14 @@ test('.fail()', t => { message: 'my message' }); + failsWith(t, () => { + const {fail} = assertions; + fail(); + }, { + assertion: 'fail', + message: 'Test failed via `t.fail()`' + }); + t.end(); }); @@ -196,6 +209,11 @@ test('.is()', t => { assertions.is('foo', 'foo'); }); + passes(t, () => { + const {is} = assertions; + is('foo', 'foo'); + }); + passes(t, () => { assertions.is('', ''); }); @@ -373,6 +391,11 @@ test('.not()', t => { assertions.not('foo', 'bar'); }); + passes(t, () => { + const {not} = assertions; + not('foo', 'bar'); + }); + fails(t, () => { assertions.not(NaN, NaN); }); @@ -419,6 +442,11 @@ test('.deepEqual()', t => { }); }); + passes(t, () => { + const {deepEqual} = assertions; + deepEqual({a: 'a', b: 'b'}, {b: 'b', a: 'a'}); + }); + passes(t, () => { assertions.deepEqual({ a: 'a', @@ -662,6 +690,11 @@ test('.notDeepEqual()', t => { assertions.notDeepEqual({a: 'a'}, {a: 'b'}); }); + passes(t, () => { + const {notDeepEqual} = assertions; + notDeepEqual({a: 'a'}, {a: 'b'}); + }); + passes(t, () => { assertions.notDeepEqual(['a', 'b'], ['c', 'd']); }); @@ -700,6 +733,15 @@ test('.throws()', gather(t => { values: [{label: 'Function returned:', formatted: /undefined/}] }); + failsWith(t, () => { + const {throws} = assertions; + throws(() => {}); + }, { + assertion: 'throws', + message: '', + values: [{label: 'Function returned:', formatted: /undefined/}] + }); + // Fails because function doesn't throw. Asserts that 'my message' is used // as the assertion message (*not* compared against the error). failsWith(t, () => { @@ -900,6 +942,15 @@ test('.throwsAsync()', gather(t => { values: [{label: 'Promise resolved with:', formatted: /'foo'/}] }); + eventuallyFailsWith(t, () => { + const {throwsAsync} = assertions; + return throwsAsync(Promise.resolve('foo')); + }, { + assertion: 'throwsAsync', + message: '', + values: [{label: 'Promise resolved with:', formatted: /'foo'/}] + }); + // Fails because the promise is resolved with an Error eventuallyFailsWith(t, () => assertions.throwsAsync(Promise.resolve(new Error())), { assertion: 'throwsAsync', @@ -1140,6 +1191,11 @@ test('.notThrows()', gather(t => { assertions.notThrows(() => {}); }); + passes(t, () => { + const {notThrows} = assertions; + notThrows(() => {}); + }); + // Fails because the function throws. failsWith(t, () => { assertions.notThrows(() => { @@ -1168,6 +1224,11 @@ test('.notThrowsAsync()', gather(t => { // Passes because the promise is resolved eventuallyPasses(t, () => assertions.notThrowsAsync(Promise.resolve())); + eventuallyPasses(t, () => { + const {notThrowsAsync} = assertions; + return notThrowsAsync(Promise.resolve()); + }); + // Fails because the promise is rejected eventuallyFailsWith(t, () => assertions.notThrowsAsync(Promise.reject(new Error())), { assertion: 'notThrowsAsync', @@ -1405,6 +1466,12 @@ test('.truthy()', t => { assertions.truthy(true); }); + passes(t, () => { + const {truthy} = assertions; + truthy(1); + truthy(true); + }); + t.end(); }); @@ -1432,6 +1499,12 @@ test('.falsy()', t => { assertions.falsy(false); }); + passes(t, () => { + const {falsy} = assertions; + falsy(0); + falsy(false); + }); + t.end(); }); @@ -1472,6 +1545,11 @@ test('.true()', t => { assertions.true(true); }); + passes(t, () => { + const {true: trueFn} = assertions; + trueFn(true); + }); + t.end(); }); @@ -1512,6 +1590,11 @@ test('.false()', t => { assertions.false(false); }); + passes(t, () => { + const {false: falseFn} = assertions; + falseFn(false); + }); + t.end(); }); @@ -1520,6 +1603,11 @@ test('.regex()', t => { assertions.regex('abc', /^abc$/); }); + passes(t, () => { + const {regex} = assertions; + regex('abc', /^abc$/); + }); + failsWith(t, () => { assertions.regex('foo', /^abc$/); }, { @@ -1571,6 +1659,11 @@ test('.notRegex()', t => { assertions.notRegex('abc', /def/); }); + passes(t, () => { + const {notRegex} = assertions; + notRegex('abc', /def/); + }); + failsWith(t, () => { assertions.notRegex('abc', /abc/); }, { @@ -1640,5 +1733,11 @@ test('.assert()', t => { assertions.assert(true); }); + passes(t, () => { + const {assert} = assertions; + assert(1); + assert(true); + }); + t.end(); }); From 270e9b1e4089acbc5e399f963dc378b9f8ef6c7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikita=20Volodin=20=5B=D0=9D=D0=B8=D0=BA=D0=B8=D1=82=D0=B0?= =?UTF-8?q?=20=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=5D?= Date: Sat, 6 Apr 2019 13:55:20 -0400 Subject: [PATCH 18/23] Check that snapshot assertion is properly bound --- test/assert.js | 5 +++++ test/fixture/assert.js.md | 10 ++++++++-- test/fixture/assert.js.snap | Bin 409 -> 411 bytes test/test.js | 1 + 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/test/assert.js b/test/assert.js index 18ad12615..1ed0da746 100644 --- a/test/assert.js +++ b/test/assert.js @@ -1345,6 +1345,11 @@ test('.snapshot()', t => { assertions.snapshot({foo: 'bar'}); }); + passes(t, () => { + const {snapshot} = assertions; + snapshot({foo: 'bar'}); + }); + passes(t, () => { assertions.snapshot({foo: 'bar'}, {id: 'fixed id'}, 'message not included in snapshot report'); }); diff --git a/test/fixture/assert.js.md b/test/fixture/assert.js.md index effe453d7..a3d865488 100644 --- a/test/fixture/assert.js.md +++ b/test/fixture/assert.js.md @@ -1,4 +1,4 @@ -# Snapshot report for `test/assert.js` +# Snapshot report for `assert.js` The actual snapshot is saved in `assert.js.snap`. @@ -36,11 +36,17 @@ Generated by [AVA](https://ava.li). > Snapshot 2 + { + foo: 'bar', + } + +> Snapshot 3 + -> Snapshot 3 +> Snapshot 4
Hello diff --git a/test/fixture/assert.js.snap b/test/fixture/assert.js.snap index 371b3be27a96ffa2a5488e199a15227d32b7a752..8295b2b7b21336bd64d3f739dfcad2e83004951b 100644 GIT binary patch literal 411 zcmV;M0c8F`RzVgBUAfIu)5FN0!DMhNC&Vqj=;>tFq2|96+C4|QLE zvAAQ-2o{Y6VwLBK_5L?}8w@vmFA}L1Qey;*ZUEvl8#n2tnsYw0X21I~=u|T!BUp4I zGXuk3hM9`O2I?mnH*il|C&YP-5iBYKbOtj6JJ>CZ9E?nQK-vU|g&Eld8QJ`kvQm>v z7#ZPA0RaI9MqUP>v5fpcHB2Im%nXdIf{e^*`T1~mQeqKJjFbf#2-GN$SeB?;kW-qT znWtNnng}$Fi4g>tqJRn#z&5c82r}|`q~_%0`=%BbC#I+J^OI;jOI~7bDvZMxoSBzW zluF16OpGj%Kn3wYTn5CAaHlY*WR?jC;B)~$10xp$BNGE756A@!a9hD1P+(x>W@Kyw zTMTk31K1+w{Xl`EKzs*o2}^EbQ8t<_Oc=H>V%P%l8`$XKkrIYMGz@rB2LL~6;MTMR F0033-sIdS5 literal 409 zcmV;K0cQR|RzVg6*tfIu)5UxVW9j0_A-ZvCr&?Emia^r7zSFBW&q z8Ns4lObiSv&lBtYZ}>JCZunj#QZ1y$2o{Y6;xijJ>7|-;KC@=O`!eWMGb1Bd)DCFL zUWS>9!UpOm88>iGTPMVMj1eq)5Qv!>*uidKFz$jIiOl$DxX!pI0` z3J3@=F!C}0jb-Eqs$mjgWM*Jw6=Y;i%g=|ilM;)V8Dv2Q0yPRGmL=*ID_uuZH2f{Z*KsX00MzNy8l9!m93gfT^XXd38r4n)i z6C+C`P(eHpmjQ7j+$qc{nPmb3I9M z0JeyEKTzN(5Z{4Y!jhX_)vSgV>rSp)z8 D5ahB} diff --git a/test/test.js b/test/test.js index 847836b97..02290cc5d 100644 --- a/test/test.js +++ b/test/test.js @@ -737,6 +737,7 @@ test('snapshot assertion can be skipped', t => { title: 'passes', fn(t) { t.snapshot.skip({not: {a: 'match'}}); + t.snapshot.skip({not: {b: 'match'}}); t.snapshot(React.createElement(HelloMessage, {name: 'Sindre'})); } }).run().then(result => { From 8451f206c55ad530478ea35e835183543544903c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikita=20Volodin=20=5B=D0=9D=D0=B8=D0=BA=D0=B8=D1=82=D0=B0?= =?UTF-8?q?=20=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=5D?= Date: Sat, 6 Apr 2019 14:11:33 -0400 Subject: [PATCH 19/23] Remove unneeded comment --- lib/test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/test.js b/lib/test.js index e1e34a25b..be2ae4ad1 100644 --- a/lib/test.js +++ b/lib/test.js @@ -64,7 +64,7 @@ class ExecutionContext extends assert.Assertions { plan(count) { const test = testMap.get(this); - test.plan(count, captureStack(test.plan)); // Or captureStack(this.plan) ? + test.plan(count, captureStack(test.plan)); } timeout(ms) { From acb27a0fd31201df9723f3df01c45513141fdef2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikita=20Volodin=20=5B=D0=9D=D0=B8=D0=BA=D0=B8=D1=82=D0=B0?= =?UTF-8?q?=20=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=5D?= Date: Sat, 6 Apr 2019 14:52:00 -0400 Subject: [PATCH 20/23] Check that ExecutionContext functions are bound --- test/test.js | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/test/test.js b/test/test.js index 02290cc5d..e594278d4 100644 --- a/test/test.js +++ b/test/test.js @@ -798,3 +798,54 @@ test('timeout is refreshed on assert', t => { t.is(result.passed, true); }); }); + +test('.log() is bound', t => { + return ava(a => { + const {log} = a; + [1, 2, 3].forEach(val => { + log('value: ' + val); + }); + ['value foo', 'value bar'].forEach(val => log(val)); + }).run().then(result => { + t.deepEqual(result.logs, [ + 'value: 1', + 'value: 2', + 'value: 3', + 'value foo', + 'value bar' + ]); + }); +}); + +test('.plan() is bound', t => { + return ava(a => { + const {plan} = a; + plan(3); + + a.pass(); + a.is(2, 2); + a.truthy('string'); + }).run().then(result => { + t.true(result.passed); + }); +}); + +test('.timeout() is bound', t => { + return ava(a => { + const {timeout} = a; + timeout(10); + a.pass(); + }).run().then(result => { + t.true(result.passed); + }); +}); + +test('.end() is bound', t => { + return ava.cb(a => { + const {end} = a; + a.pass(); + end(); + }).run().then(result => { + t.true(result.passed); + }); +}); From 7dde150ea49837a3ba3eef11b6b23fd37e808c14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikita=20Volodin=20=5B=D0=9D=D0=B8=D0=BA=D0=B8=D1=82=D0=B0?= =?UTF-8?q?=20=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=5D?= Date: Sat, 6 Apr 2019 14:53:24 -0400 Subject: [PATCH 21/23] Ensure fns on ExecutionContext are bound to it --- lib/test.js | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/lib/test.js b/lib/test.js index be2ae4ad1..9886d5955 100644 --- a/lib/test.js +++ b/lib/test.js @@ -44,31 +44,30 @@ class ExecutionContext extends assert.Assertions { }); testMap.set(this, test); - this.plan.skip = () => {}; this.snapshot.skip = () => { test.skipSnapshot(); }; - } - log(...inputArgs) { - const args = inputArgs.map(value => { - return typeof value === 'string' ? - value : - concordance.format(value, concordanceOptions); - }); + this.log = (...inputArgs) => { + const args = inputArgs.map(value => { + return typeof value === 'string' ? + value : + concordance.format(value, concordanceOptions); + }); + if (args.length > 0) { + test.addLog(args.join(' ')); + } + }; - if (args.length > 0) { - testMap.get(this).addLog(args.join(' ')); - } - } + this.plan = count => { + test.plan(count, captureStack(test.plan)); + }; - plan(count) { - const test = testMap.get(this); - test.plan(count, captureStack(test.plan)); - } + this.plan.skip = () => {}; - timeout(ms) { - testMap.get(this).timeout(ms); + this.timeout = ms => { + test.timeout(ms); + }; } get end() { From 245c222c25384ba1f04e1bf9022eaf492a8e2c9c Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Fri, 19 Apr 2019 18:39:42 +0200 Subject: [PATCH 22/23] Final tweaks --- lib/assert.js | 103 ++++++++++++++++++++++---------------------------- 1 file changed, 46 insertions(+), 57 deletions(-) diff --git a/lib/assert.js b/lib/assert.js index 9e4e5394e..3df1fd955 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -28,6 +28,10 @@ function formatWithLabel(label, value) { } const hasOwnProperty = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop); +const noop = () => {}; +const notImplemented = () => { + throw new Error('not implemented'); +}; class AssertionError extends Error { constructor(opts) { @@ -230,63 +234,49 @@ function assertExpectations({assertion, actual, expectations, message, prefix, s } } -const noop = () => {}; class Assertions { constructor({ - pass = () => { - throw new Error('not implemented'); - }, - pending = () => { - throw new Error('not implemented'); - }, - fail = () => { - throw new Error('not implemented'); - }, - skip = () => { - throw new Error('not implemented'); - }, - compareWithSnapshot = () => { - throw new Error('not implemented'); - } + pass = notImplemented, + pending = notImplemented, + fail = notImplemented, + skip = notImplemented, + compareWithSnapshot = notImplemented } = {}) { - const _skippable = assertionFn => { - assertionFn.skip = (...args) => skip(...args); + const withSkip = assertionFn => { + assertionFn.skip = skip; return assertionFn; }; // When adding new enhanced functions with new 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. - const _enhanced = (pattern, assertionFn) => { - const empowerOptions = { - onError: event => { - if (event.powerAssertContext) { - event.error.statements = enhanceAssert.formatter(event.powerAssertContext); - } - - fail(event.error); - }, - onSuccess: () => { - pass(); - }, - bindReceiver: false, - patterns: [pattern] - }; - return empower(assertionFn, empowerOptions); - }; + const withPowerAssert = (pattern, assertionFn) => empower(assertionFn, { + onError: event => { + if (event.powerAssertContext) { + event.error.statements = enhanceAssert.formatter(event.powerAssertContext); + } + + fail(event.error); + }, + onSuccess: () => { + pass(); + }, + bindReceiver: false, + patterns: [pattern] + }); - this.pass = _skippable(() => { + this.pass = withSkip(() => { pass(); }); - this.fail = _skippable(message => { + this.fail = withSkip(message => { fail(new AssertionError({ assertion: 'fail', message: message || 'Test failed via `t.fail()`' })); }); - this.is = _skippable((actual, expected, message) => { + this.is = withSkip((actual, expected, message) => { if (Object.is(actual, expected)) { pass(); } else { @@ -312,7 +302,7 @@ class Assertions { } }); - this.not = _skippable((actual, expected, message) => { + this.not = withSkip((actual, expected, message) => { if (Object.is(actual, expected)) { fail(new AssertionError({ assertion: 'not', @@ -325,7 +315,7 @@ class Assertions { } }); - this.deepEqual = _skippable((actual, expected, message) => { + this.deepEqual = withSkip((actual, expected, message) => { const result = concordance.compare(actual, expected, concordanceOptions); if (result.pass) { pass(); @@ -341,7 +331,7 @@ class Assertions { } }); - this.notDeepEqual = _skippable((actual, expected, message) => { + this.notDeepEqual = withSkip((actual, expected, message) => { const result = concordance.compare(actual, expected, concordanceOptions); if (result.pass) { const actualDescriptor = result.actual || concordance.describe(actual, concordanceOptions); @@ -356,7 +346,7 @@ class Assertions { } }); - this.throws = _skippable((...args) => { + this.throws = withSkip((...args) => { // Since arrow functions do not support 'arguments', we are using rest // operator, so we can determine the total number of arguments passed // to the function. @@ -383,9 +373,7 @@ class Assertions { try { retval = fn(); if (isPromise(retval)) { - // In reality isPromise returns if something is PromiseLike - // i.e. has a .then() function. Wrapping in Promise.resolve() - // casts it to actual promise + // Here isPromise() checks if something is "promise like". Cast to an actual promise. Promise.resolve(retval).catch(noop); fail(new AssertionError({ assertion: 'throws', @@ -422,7 +410,7 @@ class Assertions { } }); - this.throwsAsync = _skippable((...args) => { + this.throwsAsync = withSkip((...args) => { let [thrower, expectations, message] = args; if (typeof thrower !== 'function' && !isPromise(thrower)) { fail(new AssertionError({ @@ -444,7 +432,7 @@ class Assertions { const handlePromise = (promise, wasReturned) => { // Record stack before it gets lost in the promise chain. const stack = getStack(); - // Wrap in Promise.resolve to cast PromiseLike into real Promise + // Handle "promise like" objects by casting to a real Promise. const intermediate = Promise.resolve(promise).then(value => { throw new AssertionError({ assertion: 'throwsAsync', @@ -503,7 +491,7 @@ class Assertions { return Promise.resolve(); }); - this.notThrows = _skippable((fn, message) => { + this.notThrows = withSkip((fn, message) => { if (typeof fn !== 'function') { fail(new AssertionError({ assertion: 'notThrows', @@ -529,7 +517,7 @@ class Assertions { pass(); }); - this.notThrowsAsync = _skippable((nonThrower, message) => { + this.notThrowsAsync = withSkip((nonThrower, message) => { if (typeof nonThrower !== 'function' && !isPromise(nonThrower)) { fail(new AssertionError({ assertion: 'notThrowsAsync', @@ -543,6 +531,7 @@ class Assertions { const handlePromise = (promise, wasReturned) => { // Record stack before it gets lost in the promise chain. const stack = getStack(); + // Handle "promise like" objects by casting to a real Promise. const intermediate = Promise.resolve(promise).then(noop, reason => { throw new AssertionError({ assertion: 'notThrowsAsync', @@ -585,7 +574,7 @@ class Assertions { return handlePromise(retval, true); }); - this.snapshot = _skippable((expected, optionsOrMessage, message) => { + this.snapshot = withSkip((expected, optionsOrMessage, message) => { const options = {}; if (typeof optionsOrMessage === 'string') { message = optionsOrMessage; @@ -634,7 +623,7 @@ class Assertions { } }); - this.truthy = _skippable((actual, message) => { + this.truthy = withSkip((actual, message) => { if (actual) { pass(); } else { @@ -647,7 +636,7 @@ class Assertions { } }); - this.falsy = _skippable((actual, message) => { + this.falsy = withSkip((actual, message) => { if (actual) { fail(new AssertionError({ assertion: 'falsy', @@ -660,7 +649,7 @@ class Assertions { } }); - this.true = _skippable((actual, message) => { + this.true = withSkip((actual, message) => { if (actual === true) { pass(); } else { @@ -672,7 +661,7 @@ class Assertions { } }); - this.false = _skippable((actual, message) => { + this.false = withSkip((actual, message) => { if (actual === false) { pass(); } else { @@ -684,7 +673,7 @@ class Assertions { } }); - this.regex = _skippable((string, regex, message) => { + this.regex = withSkip((string, regex, message) => { if (typeof string !== 'string') { fail(new AssertionError({ assertion: 'regex', @@ -720,7 +709,7 @@ class Assertions { pass(); }); - this.notRegex = _skippable((string, regex, message) => { + this.notRegex = withSkip((string, regex, message) => { if (typeof string !== 'string') { fail(new AssertionError({ assertion: 'notRegex', @@ -756,7 +745,7 @@ class Assertions { pass(); }); - this.assert = _skippable(_enhanced( + this.assert = withSkip(withPowerAssert( 'assert(value, [message])', (actual, message) => { if (!actual) { From dd8def8d9e086a0566179927899dc8c48f4f5442 Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Mon, 22 Apr 2019 11:57:32 +0200 Subject: [PATCH 23/23] Fix bad merge --- lib/assert.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/assert.js b/lib/assert.js index 992106130..33dd64693 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -532,7 +532,7 @@ class Assertions { // Record stack before it gets lost in the promise chain. const stack = getStack(); // Handle "promise like" objects by casting to a real Promise. - const intermediate = Promise.resolve(promise).catch(error => { + const intermediate = Promise.resolve(promise).then(noop, error => { // eslint-disable-line promise/prefer-await-to-then throw new AssertionError({ assertion: 'notThrowsAsync', message,