@pmoo/testy
Advanced tools
Comparing version 5.0.2 to 5.1.0
@@ -11,2 +11,36 @@ # Changelog | ||
## [5.1.0] - 2021-01-13 | ||
This release includes a lot of contributions (4 new contributors!), and several refactorings to continue improving the quality of the tool. | ||
### Added | ||
* [[feature] added after() hook to run code after each test](https://github.com/ngarbezza/testy/issues/135), thank you @adico1! | ||
* [[feature] isIncludedIn() and isNotIncludedIn() assertions](https://github.com/ngarbezza/testy/issues/75), thank you @ask-imran for your first contribution! | ||
* [[feature] warning message when no tests found](https://github.com/ngarbezza/testy/issues/157), thank you @niyonx for your first contribution! | ||
* [[feature] show error when a requested file does not exist](https://github.com/ngarbezza/testy/issues/158), thank you @chelsieng for your first contribution! | ||
* [[feature] global error handler](https://github.com/ngarbezza/testy/issues/177) | ||
### Fixed | ||
* [[bug] suite and test names cannot be empty](https://github.com/ngarbezza/testy/issues/136), thank you @ask-imran! | ||
* [[bug] includes() and doesNotInclude() matchers only work on Arrays](https://github.com/ngarbezza/testy/issues/130), thank you @trochepablo for your first contribution! | ||
* [[bug] color for pending summary was not painted](https://github.com/ngarbezza/testy/issues/173) | ||
* [[bug] it was possible to mark tests as pending without specifying reason](https://github.com/ngarbezza/testy/issues/172) | ||
### Refactored | ||
* [rename "master" branch to "main"](https://github.com/ngarbezza/testy/issues/133); also, an ADR was added to track the decision that we want a better vocabulary | ||
* [parametrizable i18n messages](https://github.com/ngarbezza/testy/issues/71) | ||
* [write more tests for the i18n module](https://github.com/ngarbezza/testy/issues/179) | ||
* [throw error objects instead of strings](https://github.com/ngarbezza/testy/issues/176) | ||
* [speed up tests by not creating error messages on successful assertions](https://github.com/ngarbezza/testy/commit/531d1d6360c93a3aae2f11bd0c957c45e93cd35c) | ||
* [added some npm scripts for test coverage and dependencies graph](https://github.com/ngarbezza/testy/commit/d4ca1fa7804b2353458eb214d1f302fefc9fed9d) | ||
* [changes in modularization: extract assertion and test result reporter](https://github.com/ngarbezza/testy/commit/4913b5a187bc0700b3de4b5b1a9adc0e2a8dc57e) | ||
* add more tests and increased the current coverage ([example 1](https://github.com/ngarbezza/testy/commit/be41db9872ea4490b5dae238d6c553b214667326), [example 2](https://github.com/ngarbezza/testy/commit/28b2ee51078300382c7398cb40203d6e40ca26d1)) | ||
* formatter object decoupled from console ui (ADR 0004 added [here](https://github.com/ngarbezza/testy/commit/9ab5c55fd4738054effef1e1aab15824a62c6750)) | ||
* avoid test doubles at all (ADR 0005 added [here](https://github.com/ngarbezza/testy/commit/5a65fbc6e6e58b1f03f996c381240d4a1b8c3875)), removed test runner double | ||
... and more minor cleanups. | ||
## [5.0.2] - 2020-10-13 | ||
@@ -209,3 +243,4 @@ | ||
[Unreleased]: https://github.com/ngarbezza/testy/compare/v5.0.2...HEAD | ||
[Unreleased]: https://github.com/ngarbezza/testy/compare/v5.1.0...HEAD | ||
[5.1.0]: https://github.com/ngarbezza/testy/compare/v5.0.2...v5.1.0 | ||
[5.0.2]: https://github.com/ngarbezza/testy/compare/v5.0.1...v5.0.2 | ||
@@ -212,0 +247,0 @@ [5.0.1]: https://github.com/ngarbezza/testy/compare/v5.0.0...v5.0.1 |
'use strict'; | ||
const Utils = require('./utils'); | ||
const EqualityAssertionStrategy = require('./equality_assertion_strategy'); | ||
const TestResult = require('./test_result'); | ||
const TestResultReporter = require('./test_result_reporter'); | ||
const Assertion = require('./assertion'); | ||
const { I18nMessage } = require('./i18n'); | ||
class TestResultReporter { | ||
constructor(runner) { | ||
this._runner = runner; | ||
} | ||
const { isStringWithContent } = require('./utils'); | ||
report(result) { | ||
this._runner.setResultForCurrentTest(result); | ||
} | ||
translated(key) { | ||
return this._runner._i18n.translate(key); | ||
} | ||
} | ||
class FailureGenerator extends TestResultReporter { | ||
with(description) { | ||
this.report(TestResult.failure(description || this.translated('explicitly_failed'))); | ||
this.report(TestResult.failure(description || I18nMessage.of('explicitly_failed'))); | ||
} | ||
@@ -29,4 +18,12 @@ } | ||
dueTo(reason) { | ||
this.report(TestResult.explicitlyMarkedAsPending(reason)); | ||
if (isStringWithContent(reason)) { | ||
this.report(TestResult.explicitlyMarkedAsPending(reason)); | ||
} else { | ||
this.report(TestResult.error(this.invalidReasonErrorMessage())); | ||
} | ||
} | ||
invalidReasonErrorMessage() { | ||
return I18nMessage.of('invalid_pending_reason'); | ||
} | ||
} | ||
@@ -84,225 +81,2 @@ | ||
class Assertion extends TestResultReporter { | ||
constructor(runner, actual) { | ||
super(runner); | ||
this._actual = actual; | ||
} | ||
// Boolean assertions | ||
isTrue() { | ||
this._booleanAssertion(true, this.translated('be_true')); | ||
} | ||
isFalse() { | ||
this._booleanAssertion(false, this.translated('be_false')); | ||
} | ||
// Undefined value assertions | ||
isUndefined() { | ||
this._undefinedAssertion(this.translated('be_undefined')); | ||
} | ||
isNotUndefined() { | ||
this._notUndefinedAssertion(this.translated('be_defined')); | ||
} | ||
// Null value assertions | ||
isNull() { | ||
this._nullAssertion(this.translated('be_null')); | ||
} | ||
isNotNull() { | ||
this._notNullAssertion(this.translated('be_not_null')); | ||
} | ||
// Equality assertions | ||
isEqualTo(expected, criteria) { | ||
this._equalityAssertion(expected, criteria, true); | ||
} | ||
isNotEqualTo(expected, criteria) { | ||
this._equalityAssertion(expected, criteria, false); | ||
} | ||
// Collection assertions | ||
includes(expectedObject, equalityCriteria) { | ||
const resultIsSuccessful = this._actual.find(element => | ||
this._areConsideredEqual(element, expectedObject, equalityCriteria)); | ||
const failureMessage = `${this.translated('include')} ${Utils.prettyPrint(expectedObject)}`; | ||
this._reportAssertionResult(resultIsSuccessful, failureMessage); | ||
} | ||
doesNotInclude(expectedObject, equalityCriteria) { | ||
const resultIsSuccessful = !this._actual.find(element => | ||
this._areConsideredEqual(element, expectedObject, equalityCriteria)); | ||
const failureMessage = `${this.translated('not_include')} ${Utils.prettyPrint(expectedObject)}`; | ||
this._reportAssertionResult(resultIsSuccessful, failureMessage); | ||
} | ||
includesExactly(...objects) { | ||
const resultIsSuccessful = this._haveElementsConsideredEqual(this._actual, objects); | ||
const failureMessage = `${this.translated('include_exactly')} ${Utils.prettyPrint(objects)}`; | ||
this._reportAssertionResult(resultIsSuccessful, failureMessage); | ||
} | ||
isEmpty() { | ||
const resultIsSuccessful = Utils.numberOfElements(this._actual || {}) === 0 | ||
&& Utils.notNullOrUndefined(this._actual); | ||
const failureMessage = this.translated('be_empty'); | ||
this._reportAssertionResult(resultIsSuccessful, failureMessage); | ||
} | ||
isNotEmpty() { | ||
const setValueWhenUndefined = this._actual || {}; | ||
const resultIsSuccessful = Utils.numberOfElements(setValueWhenUndefined) > 0; | ||
const failureMessage = this.translated('be_not_empty'); | ||
this._reportAssertionResult(resultIsSuccessful, failureMessage); | ||
} | ||
// Exception assertions | ||
raises(errorExpectation) { | ||
this._exceptionAssertion(errorExpectation, true); | ||
} | ||
doesNotRaise(notExpectedError) { | ||
this._exceptionAssertion(notExpectedError, false); | ||
} | ||
doesNotRaiseAnyErrors() { | ||
let noErrorsOccurred = false; | ||
let failureMessage = ''; | ||
try { | ||
this._actual.call(); | ||
noErrorsOccurred = true; | ||
} catch (error) { | ||
noErrorsOccurred = false; | ||
failureMessage = `${this.translated('expected_no_errors')}, ${this.translated('but')} ${Utils.prettyPrint(error)} ${this.translated('was_raised')}`; | ||
} finally { | ||
this._reportAssertionResult(noErrorsOccurred, failureMessage, true); | ||
} | ||
} | ||
// Numeric assertions | ||
isNearTo(number, precisionDigits = 4) { | ||
const result = Number.parseFloat((this._actual).toFixed(precisionDigits)) === number; | ||
const failureMessage = `be near to ${number} (using ${precisionDigits} precision digits)`; | ||
this._reportAssertionResult(result, failureMessage, false); | ||
} | ||
// String assertions | ||
matches(regex) { | ||
const result = this._actual.match(regex); | ||
const failureMessage = `${this.translated('match')} ${regex}`; | ||
this._reportAssertionResult(result, failureMessage, false); | ||
} | ||
// Private | ||
_equalityAssertion(expected, criteria, shouldBeEqual) { | ||
const { comparisonResult, additionalFailureMessage, overrideFailureMessage } = | ||
EqualityAssertionStrategy.evaluate(this._actual, expected, criteria); | ||
const resultIsSuccessful = shouldBeEqual ? comparisonResult : !comparisonResult; | ||
if (overrideFailureMessage) { | ||
this._reportAssertionResult(resultIsSuccessful, overrideFailureMessage, true); | ||
} else { | ||
const expectationMessage = shouldBeEqual ? this.translated('be_equal_to') : this.translated('be_not_equal_to'); | ||
const failureMessage = `${expectationMessage} ${Utils.prettyPrint(expected)}${additionalFailureMessage}`; | ||
this._reportAssertionResult(resultIsSuccessful, failureMessage, false); | ||
} | ||
} | ||
_areConsideredEqual(objectOne, objectTwo, equalityCriteria) { | ||
return EqualityAssertionStrategy.evaluate(objectOne, objectTwo, equalityCriteria).comparisonResult; | ||
} | ||
_booleanAssertion(expectedBoolean, failureMessage) { | ||
const resultIsSuccessful = this._actual === expectedBoolean; | ||
this._reportAssertionResult(resultIsSuccessful, failureMessage); | ||
} | ||
_undefinedAssertion(failureMessage) { | ||
const resultIsSuccessful = this._actual === undefined; | ||
this._reportAssertionResult(resultIsSuccessful, failureMessage); | ||
} | ||
_notUndefinedAssertion(failureMessage) { | ||
const resultIsSuccessful = this._actual !== undefined; | ||
this._reportAssertionResult(resultIsSuccessful, failureMessage); | ||
} | ||
_nullAssertion(failureMessage) { | ||
const resultIsSuccessful = this._actual === null; | ||
this._reportAssertionResult(resultIsSuccessful, failureMessage); | ||
} | ||
_notNullAssertion(failureMessage) { | ||
const resultIsSuccessful = this._actual !== null; | ||
this._reportAssertionResult(resultIsSuccessful, failureMessage); | ||
} | ||
_exceptionAssertion(errorExpectation, shouldFail) { | ||
let hasFailed = false; | ||
let actualError = undefined; | ||
try { | ||
this._actual(); | ||
hasFailed = !shouldFail; | ||
} catch (error) { | ||
actualError = error; | ||
const errorCheck = this._checkIfErrorMatchesExpectation(errorExpectation, actualError); | ||
hasFailed = shouldFail ? errorCheck : !errorCheck; | ||
} finally { | ||
const toHappenOrNot = shouldFail ? this.translated('to_happen') : this.translated('not_to_happen'); | ||
const expectedErrorIntroduction = `${this.translated('expected')} ${this.translated('expecting_error')} ${Utils.prettyPrint(errorExpectation)} ${toHappenOrNot}`; | ||
const failureMessage = typeof actualError !== 'undefined' ? | ||
`${expectedErrorIntroduction}, ${this.translated('but_got')} ${Utils.prettyPrint(actualError)} ${this.translated('instead')}` : expectedErrorIntroduction; | ||
this._reportAssertionResult(hasFailed, failureMessage, true); | ||
} | ||
} | ||
_checkIfErrorMatchesExpectation(errorExpectation, actualError) { | ||
if (Utils.isRegex(errorExpectation)) { | ||
return errorExpectation.test(actualError); | ||
} else { | ||
return this._areConsideredEqual(actualError, errorExpectation); | ||
} | ||
} | ||
_reportAssertionResult(wasSuccess, matcherFailureMessage, overrideFailureMessage) { | ||
if (wasSuccess) { | ||
this.report(TestResult.success()); | ||
} else { | ||
const defaultFailureMessage = `${this.translated('expected')} ${this._actualResultAsString()} ${this.translated('to')}${matcherFailureMessage}`; | ||
this.report(TestResult.failure(overrideFailureMessage ? matcherFailureMessage : defaultFailureMessage)); | ||
} | ||
} | ||
_actualResultAsString() { | ||
return Utils.prettyPrint(this._actual); | ||
} | ||
_haveElementsConsideredEqual(collectionOne, collectionTwo) { | ||
const collectionOneArray = Array.from(collectionOne); | ||
const collectionTwoArray = Array.from(collectionTwo); | ||
if (collectionOneArray.length !== collectionTwoArray.length) return false; | ||
for (let i = 0; i < collectionOne.length; i++) { | ||
const includedInOne = collectionOne.find(element => | ||
this._areConsideredEqual(element, collectionTwoArray[i])); | ||
const includedInTwo = collectionTwo.find(element => | ||
this._areConsideredEqual(element, collectionOneArray[i])); | ||
if (!includedInOne || !includedInTwo) return false; | ||
} | ||
return true; | ||
} | ||
} | ||
module.exports = { Asserter, FailureGenerator, PendingMarker }; |
@@ -15,3 +15,3 @@ 'use strict'; | ||
userConfiguration = require(resolvePathFor(CONFIGURATION_FILE_NAME)); | ||
} catch(error) { | ||
} catch (error) { | ||
userConfiguration = {}; | ||
@@ -36,4 +36,8 @@ } | ||
filter() { | ||
return new RegExp(this._configurationOptions.filter); | ||
return new RegExp(this.filterRaw()); | ||
} | ||
filterRaw() { | ||
return this._configurationOptions.filter; | ||
} | ||
@@ -40,0 +44,0 @@ language() { |
'use strict'; | ||
const I18n = require('./i18n'); | ||
const { I18n } = require('./i18n'); | ||
const Formatter = require('./formatter'); | ||
// Colors and emphasis | ||
const off = '\x1b[0m'; | ||
const bold = '\x1b[1m'; | ||
const grey = '\x1b[30m'; | ||
const red = '\x1b[31m'; | ||
const green = '\x1b[32m'; | ||
const yellow = '\x1b[33m'; | ||
class ConsoleUI { | ||
constructor() { | ||
this.useLanguage(I18n.defaultLanguage()); | ||
static successfulExitCode() { | ||
return 0; | ||
} | ||
static failedExitCode() { | ||
return 1; | ||
} | ||
// Callbacks for runner/suite/test | ||
@@ -22,21 +19,12 @@ | ||
return { | ||
whenPending: test => { | ||
this._displayResult(this.translated('wip'), test, yellow); | ||
if (test.isExplicitlyMarkedPending()) | ||
this._displayResultDetail(test.result().reason()); | ||
}, | ||
whenSkipped: test => { | ||
this._displayResult(this.translated('skip'), test, grey); | ||
}, | ||
whenSuccess: test => { | ||
this._displayResult(this.translated('ok'), test, green); | ||
}, | ||
whenFailed: test => { | ||
this._displayResult(this.translated('fail'), test, red); | ||
this._displayResultDetail(test.result().failureMessage()); | ||
}, | ||
whenErrored: test => { | ||
this._displayResult(this.translated('error'), test, red); | ||
this._displayResultDetail(test.result().failureMessage()); | ||
} | ||
whenPending: test => | ||
this._formatter.displayPendingResult(test), | ||
whenSkipped: test => | ||
this._formatter.displaySkippedResult(test), | ||
whenSuccess: test => | ||
this._formatter.displaySuccessResult(test), | ||
whenFailed: test => | ||
this._formatter.displayFailureResult(test), | ||
whenErrored: test => | ||
this._formatter.displayErrorResult(test), | ||
}; | ||
@@ -47,12 +35,6 @@ } | ||
return { | ||
onStart: suite => { | ||
console.log(`\n${suite.name()}:`); | ||
this._displaySeparator('-'); | ||
}, | ||
onFinish: suite => { | ||
this._displaySeparator('-'); | ||
console.log(`${this.translated('summary_of')} ${suite.name()}:`); | ||
this._displayCountFor(suite); | ||
this._displaySeparator(); | ||
} | ||
onStart: suite => | ||
this._formatter.displaySuiteStart(suite), | ||
onFinish: suite => | ||
this._formatter.displaySuiteEnd(suite), | ||
}; | ||
@@ -63,101 +45,33 @@ } | ||
return { | ||
onFinish: runner => { | ||
this.displayErrorsAndFailuresSummary(runner); | ||
this._displaySummary(runner); | ||
} | ||
onFinish: runner => | ||
this._formatter.displayRunnerEnd(runner), | ||
onSuccess: _runner => | ||
this._exitWithCode(ConsoleUI.successfulExitCode()), | ||
onFailure: _runner => | ||
this._exitWithCode(ConsoleUI.failedExitCode()), | ||
}; | ||
} | ||
displayInitialSummary(configuration, paths) { | ||
this._displaySeparator(); | ||
console.log(this._inBold(this.translated('starting_testy'))); | ||
this._displayRunConfiguration(paths, configuration); | ||
this._displaySeparator(); | ||
start(configuration, paths, runnerBlock) { | ||
this._formatter.start(); | ||
this._formatter.displayInitialInformation(configuration, paths); | ||
runnerBlock.call(); | ||
this._formatter.end(); | ||
} | ||
measuringTotalTime(code) { | ||
const name = this.translated('total_time'); | ||
console.time(name); | ||
code(); | ||
console.timeEnd(name); | ||
} | ||
useLanguage(language) { | ||
this._i18n = new I18n(language); | ||
const i18n = new I18n(language); | ||
this._formatter = new Formatter(console, i18n); | ||
} | ||
translated(key) { | ||
return this._i18n.translate(key); | ||
exitWithError(errorMessage) { | ||
this._formatter.displayError(errorMessage); | ||
this._exitWithCode(ConsoleUI.failedExitCode()); | ||
} | ||
_displayRunConfiguration(paths, configuration) { | ||
const testPaths = this.translated('running_tests_in'); | ||
const failFast = this.translated('fail_fast'); | ||
const randomOrder = this.translated('random_order'); | ||
const padding = Math.max(testPaths.length, failFast.length, randomOrder.length); | ||
console.log(`${testPaths.padEnd(padding)} : ${paths}`); | ||
console.log(`${failFast.padEnd(padding)} : ${this._humanBoolean(configuration.failFastMode().enabled())}`); | ||
console.log(`${randomOrder.padEnd(padding)} : ${this._humanBoolean(configuration.randomOrder())}`); | ||
_exitWithCode(exitCode) { | ||
process.exit(exitCode); | ||
} | ||
_displayIfNonZero(quantity, word, color = off) { | ||
return quantity > 0 ? `, ${this._withColor(`${quantity} ${word}`, color)}` : ''; | ||
} | ||
_displayResult(result, test, color) { | ||
console.log(`[${color}${this._inBold(result)}] ${this._withColor(test.name(), color)}`); | ||
} | ||
_displayResultDetail(detail) { | ||
console.log(` => ${detail}`); | ||
} | ||
_displaySeparator(character = '=') { | ||
console.log(character.repeat(80)); | ||
} | ||
_displayCountFor(runner) { | ||
const passedCount = this._displayIfNonZero(runner.successCount(), this.translated('passed'), green); | ||
const failureCount = this._displayIfNonZero(runner.failuresCount(), this.translated('failed'), red); | ||
const errorCount = this._displayIfNonZero(runner.errorsCount(), this.translated('errors'), red); | ||
const pendingCount = this._displayIfNonZero(runner.pendingCount(), this.translated('pending')); | ||
const skippedCount = this._displayIfNonZero(runner.skippedCount(), this.translated('skipped'), yellow); | ||
console.log(`${runner.totalCount()} test(s)${passedCount}${failureCount}${errorCount}${pendingCount}${skippedCount}`); | ||
} | ||
displayErrorsAndFailuresSummary(runner) { | ||
if (runner.hasErrorsOrFailures()) { | ||
console.log(`\n${this.translated('failures_summary')}`); | ||
runner.allFailuresAndErrors().forEach(test => { | ||
if (test.isFailure()) { | ||
this._displayResult(this.translated('fail'), test, red); | ||
} else { | ||
this._displayResult(this.translated('error'), test, red); | ||
} | ||
this._displayResultDetail(test.result().failureMessage()); | ||
}); | ||
this._displaySeparator(); | ||
} | ||
} | ||
_displaySummary(runner) { | ||
console.log(`\n${this.translated('total')}`); | ||
this._displayCountFor(runner); | ||
this._displaySeparator(); | ||
} | ||
_inBold(text) { | ||
return `${bold}${text}${off}`; | ||
} | ||
_withColor(text, color) { | ||
return `${color}${text}${off}`; | ||
} | ||
_humanBoolean(boolean) { | ||
return boolean === true ? this.translated('yes') : this.translated('no'); | ||
} | ||
} | ||
module.exports = ConsoleUI; |
'use strict'; | ||
const Utils = require('./utils'); | ||
const { I18nMessage } = require('./i18n'); | ||
const { isString, isFunction, isUndefined, respondsTo, isCyclic, deepStrictEqual } = require('./utils'); | ||
@@ -28,3 +29,3 @@ const EqualityAssertionStrategy = { | ||
appliesTo(actual, expected) { | ||
return actual === undefined && expected === undefined; | ||
return isUndefined(actual) && isUndefined(expected); | ||
}, | ||
@@ -35,3 +36,3 @@ | ||
comparisonResult: false, | ||
overrideFailureMessage: 'Equality cannot be determined. Both parts are undefined', | ||
overrideFailureMessage: () => I18nMessage.of('equality_assertion_failed_due_to_undetermination'), | ||
}; | ||
@@ -45,3 +46,3 @@ }, | ||
appliesTo(actual, expected, criteria) { | ||
return Utils.isFunction(criteria); | ||
return isFunction(criteria); | ||
}, | ||
@@ -52,3 +53,3 @@ | ||
comparisonResult: criteria(actual, expected), | ||
additionalFailureMessage: '' | ||
additionalFailureMessage: () => I18nMessage.empty(), | ||
}; | ||
@@ -62,3 +63,3 @@ }, | ||
appliesTo(actual, expected, criteria) { | ||
return Utils.isString(criteria); | ||
return isString(criteria); | ||
}, | ||
@@ -75,3 +76,3 @@ | ||
_comparisonCanBeMade(actual, expected, criteria) { | ||
return Utils.respondsTo(actual, criteria) && Utils.respondsTo(expected, criteria); | ||
return respondsTo(actual, criteria) && respondsTo(expected, criteria); | ||
}, | ||
@@ -82,3 +83,3 @@ | ||
comparisonResult: actual[criteria](expected), | ||
additionalFailureMessage: '' | ||
additionalFailureMessage: () => I18nMessage.empty(), | ||
}; | ||
@@ -90,3 +91,3 @@ }, | ||
comparisonResult: false, | ||
additionalFailureMessage: ` Equality check failed. Objects do not have '${criteria}' property` | ||
additionalFailureMessage: () => I18nMessage.of('equality_assertion_failed_due_to_missing_property', criteria), | ||
}; | ||
@@ -100,3 +101,3 @@ }, | ||
appliesTo(actual, _expected) { | ||
return Utils.respondsTo(actual, 'equals'); | ||
return respondsTo(actual, 'equals'); | ||
}, | ||
@@ -107,3 +108,3 @@ | ||
comparisonResult: actual.equals(expected), | ||
additionalFailureMessage: '' | ||
additionalFailureMessage: () => I18nMessage.empty(), | ||
}; | ||
@@ -117,3 +118,3 @@ }, | ||
appliesTo(actual, expected) { | ||
return Utils.isCyclic(actual) || Utils.isCyclic(expected); | ||
return isCyclic(actual) || isCyclic(expected); | ||
}, | ||
@@ -124,3 +125,3 @@ | ||
comparisonResult: false, | ||
additionalFailureMessage: ' (circular references found, equality check cannot be done. Please compare objects\' properties individually)' | ||
additionalFailureMessage: () => I18nMessage.of('equality_assertion_failed_due_to_circular_references'), | ||
}; | ||
@@ -139,4 +140,4 @@ }, | ||
return { | ||
comparisonResult: Utils.deepStrictEqual(actual, expected), | ||
additionalFailureMessage: '' | ||
comparisonResult: deepStrictEqual(actual, expected), | ||
additionalFailureMessage: () => I18nMessage.empty(), | ||
}; | ||
@@ -143,0 +144,0 @@ }, |
145
lib/i18n.js
'use strict'; | ||
const TRANSLATIONS = require('./translations'); | ||
const { subclassResponsibility, isUndefined } = require('./utils'); | ||
class I18nMessage { | ||
static of(key, ...params) { | ||
return new InternationalizedMessage(key, ...params); | ||
} | ||
static empty() { | ||
return new EmptyMessage(); | ||
} | ||
static joined(messages, joinedBy) { | ||
const messagesWithContent = messages.filter(message => message.hasContent()); | ||
if (messagesWithContent.length === 0) { | ||
throw new Error('No messages with content have been found to be composed'); | ||
} else if (messagesWithContent.length === 1) { | ||
return messagesWithContent[0]; | ||
} else { | ||
return new ComposedInternationalizedMessage(messagesWithContent, joinedBy); | ||
} | ||
} | ||
expressedIn(_locale) { | ||
subclassResponsibility(); | ||
} | ||
hasContent() { | ||
subclassResponsibility(); | ||
} | ||
} | ||
class EmptyMessage extends I18nMessage { | ||
expressedIn(_locale) { | ||
return ''; | ||
} | ||
hasContent() { | ||
return false; | ||
} | ||
} | ||
class ComposedInternationalizedMessage extends I18nMessage { | ||
constructor(messages, joinString) { | ||
super(); | ||
this._messages = messages; | ||
this._joinString = joinString; | ||
Object.freeze(this); | ||
} | ||
expressedIn(locale) { | ||
return this._messages | ||
.map(message => message.expressedIn(locale)) | ||
.join(this._joinString); | ||
} | ||
hasContent() { | ||
return true; | ||
} | ||
} | ||
class InternationalizedMessage extends I18nMessage { | ||
constructor(key, ...params) { | ||
super(); | ||
this._key = key; | ||
this._params = params; | ||
Object.freeze(this); | ||
} | ||
expressedIn(locale) { | ||
return locale.translate(this._key, ...this._params); | ||
} | ||
hasContent() { | ||
return true; | ||
} | ||
} | ||
class I18n { | ||
static default(translations = TRANSLATIONS) { | ||
return new this(this.defaultLanguage(), translations); | ||
} | ||
static defaultLanguage() { | ||
@@ -10,3 +91,4 @@ return 'en'; | ||
constructor(languageCode = I18n.defaultLanguage(), translations = TRANSLATIONS) { | ||
constructor(languageCode, translations = TRANSLATIONS) { | ||
this._assertLanguageIsSupported(languageCode, translations); | ||
this._languageCode = languageCode; | ||
@@ -16,23 +98,62 @@ this._translations = translations; | ||
translate(key) { | ||
const languageTranslations = this.translationsForCurrentLanguage(); | ||
const translatedText = languageTranslations[key] || this.defaultTranslationFor(key); | ||
if (translatedText === undefined) | ||
throw this.keyNotFoundMessage(key); | ||
return translatedText; | ||
translate(key, ...params) { | ||
const languageTranslations = this._translationsForCurrentLanguage(); | ||
const translatedText = languageTranslations[key] || this._defaultTranslationFor(key); | ||
this._validateResultingTextExists(translatedText, key); | ||
return this._evaluateWithPotentialArguments(translatedText, params); | ||
} | ||
translationsForCurrentLanguage() { | ||
return this._translations[this._languageCode] || {}; | ||
// assertions | ||
_assertLanguageIsSupported(languageCode, translations) { | ||
const supportedLanguages = Object.keys(translations); | ||
if (!supportedLanguages.includes(languageCode)) { | ||
throw new Error(`Language '${languageCode}' is not supported. Allowed values: ${supportedLanguages.join(', ')}`); | ||
} | ||
} | ||
defaultTranslationFor(key) { | ||
_validateResultingTextExists(text, key) { | ||
if (isUndefined(text)) { | ||
throw new Error(this._keyNotFoundMessage(key)); | ||
} | ||
} | ||
_validateNumberOfArgumentsMatch(expectedParams, givenParams) { | ||
if (expectedParams.length !== givenParams.length) { | ||
throw new Error(this._wrongNumberOfArgumentsErrorMessage(expectedParams, givenParams)); | ||
} | ||
} | ||
// private | ||
_evaluateWithPotentialArguments(translatedText, params) { | ||
const translatedTextArguments = translatedText.match(/%s/g) || []; | ||
if (translatedTextArguments.length > 0) { | ||
this._validateNumberOfArgumentsMatch(translatedTextArguments, params); | ||
// Node 15 will include the replaceAll() method | ||
return translatedText.replace(/%s/g, () => params.shift()); | ||
} else { | ||
return translatedText; | ||
} | ||
} | ||
_translationsForCurrentLanguage() { | ||
return this._translations[this._languageCode]; | ||
} | ||
_defaultTranslationFor(key) { | ||
return this._translations[I18n.defaultLanguage()][key]; | ||
} | ||
keyNotFoundMessage(key) { | ||
// error messages | ||
_keyNotFoundMessage(key) { | ||
return `${key} not found in translations (language: ${this._languageCode})!`; | ||
} | ||
_wrongNumberOfArgumentsErrorMessage(expectedParams, givenParams) { | ||
return `Expected ${expectedParams.length} argument(s) on the translation string, got ${givenParams.length} instead`; | ||
} | ||
} | ||
module.exports = I18n; | ||
module.exports = { I18n, I18nMessage }; |
@@ -18,2 +18,14 @@ 'use strict'; | ||
}, | ||
_packageData() { | ||
return require('../package.json'); | ||
}, | ||
_exitSuccessfully() { | ||
process.exit(0); | ||
}, | ||
_exitWithFailure() { | ||
process.exit(1); | ||
}, | ||
}; | ||
@@ -29,5 +41,5 @@ | ||
execute(_params) { | ||
const { version, description, homepage } = require('../package.json'); | ||
const { version, description, homepage } = this._packageData(); | ||
console.log(`Testy version: ${version}\n${description}\n${homepage}`); | ||
process.exit(0); | ||
this._exitSuccessfully(); | ||
}, | ||
@@ -44,3 +56,3 @@ }; | ||
execute(_params) { | ||
const { description } = require('../package.json'); | ||
const { description } = this._packageData(); | ||
console.log(`Testy: ${description}\n`); | ||
@@ -52,3 +64,3 @@ console.log('Usage: \n'); | ||
console.log(' -v, --version Displays the current version'); | ||
process.exit(0); | ||
this._exitSuccessfully(); | ||
}, | ||
@@ -65,4 +77,13 @@ }; | ||
execute(params) { | ||
const testyInstance = Testy.configuredWith(Configuration.current()); | ||
testyInstance.run(params); | ||
try { | ||
const testyInstance = Testy.configuredWith(Configuration.current()); | ||
testyInstance.run(params); | ||
this._exitSuccessfully(); | ||
} catch (error) { | ||
const { bugs } = this._packageData(); | ||
console.log('Sorry, an unexpected error happened while loading Testy, you did nothing wrong. Details below:'); | ||
console.log(error.stack); | ||
console.log(`Please report this issue including the full error message and stacktrace at ${bugs.url}.`); | ||
this._exitWithFailure(); | ||
} | ||
}, | ||
@@ -69,0 +90,0 @@ }; |
@@ -24,6 +24,6 @@ 'use strict'; | ||
static evaluate(test, failFastMode) { | ||
static evaluate(test, context) { | ||
return [SkippedTest, TestWithoutDefinition, TestWithDefinition] | ||
.find(result => result.canHandle(test, failFastMode)) | ||
.handle(test, failFastMode); | ||
.find(result => result.canHandle(test, context)) | ||
.handle(test, context); | ||
} | ||
@@ -59,4 +59,4 @@ | ||
class SkippedTest extends TestResult { | ||
static canHandle(test, failFastMode) { | ||
return failFastMode.hasFailed(); | ||
static canHandle(test, context) { | ||
return context.failFastMode.hasFailed(); | ||
} | ||
@@ -92,3 +92,3 @@ | ||
static handle(test, failFastMode) { | ||
static handle(test, context) { | ||
test.evaluate(); | ||
@@ -98,3 +98,3 @@ const possibleResults = [TestWithoutAssertion, TestErrored, TestExplicitlyMarkedPending, TestSucceeded, TestFailed]; | ||
.find(result => result.canHandle(test)) | ||
.handle(test, failFastMode); | ||
.handle(test, context); | ||
} | ||
@@ -135,4 +135,4 @@ } | ||
static handle(test, failFastMode) { | ||
failFastMode.registerFailure(); | ||
static handle(test, context) { | ||
context.failFastMode.registerFailure(); | ||
test.finishWithError(); | ||
@@ -160,5 +160,5 @@ } | ||
static handle(test, failFastMode) { | ||
static handle(test, context) { | ||
test.setResult(new this()); | ||
super.handle(test, failFastMode); | ||
super.handle(test, context); | ||
} | ||
@@ -190,4 +190,4 @@ | ||
static handle(test, failFastMode) { | ||
failFastMode.registerFailure(); | ||
static handle(test, context) { | ||
context.failFastMode.registerFailure(); | ||
test.finishWithFailure(); | ||
@@ -194,0 +194,0 @@ } |
@@ -5,4 +5,4 @@ 'use strict'; | ||
const TestSuite = require('./test_suite'); | ||
const FailFast = require("./fail_fast"); | ||
const I18n = require('./i18n'); | ||
const FailFast = require('./fail_fast'); | ||
const { I18n } = require('./i18n'); | ||
const { shuffle } = require('./utils'); | ||
@@ -20,3 +20,3 @@ | ||
registerSuite(name, suiteBody = () => {}, callbacks = {}) { | ||
registerSuite(name, suiteBody, callbacks) { | ||
const suiteToAdd = new TestSuite(name, suiteBody, callbacks); | ||
@@ -28,11 +28,15 @@ return this.addSuite(suiteToAdd); | ||
const testToAdd = new Test(name, testBody, callbacks); | ||
this.currentSuite().addTest(testToAdd); | ||
this._currentSuite.addTest(testToAdd); | ||
} | ||
registerBefore(beforeBlock) { | ||
this.currentSuite().before(beforeBlock); | ||
this._currentSuite.before(beforeBlock); | ||
} | ||
registerAfter(afterBlock) { | ||
this._currentSuite.after(afterBlock); | ||
} | ||
addSuite(suiteToAdd) { | ||
this.suites().push(suiteToAdd); | ||
this._suites.push(suiteToAdd); | ||
this._setCurrentSuite(suiteToAdd); | ||
@@ -58,5 +62,5 @@ return suiteToAdd; | ||
this._randomizeSuites(); | ||
this.suites().forEach(suite => { | ||
this._suites.forEach(suite => { | ||
this._setCurrentSuite(suite); | ||
suite.run(this._failFastMode, this._randomOrder); | ||
suite.run(this._executionContext()); | ||
}); | ||
@@ -66,10 +70,19 @@ this._callbacks.onFinish(this); | ||
_executionContext() { | ||
return { | ||
failFastMode: this._failFastMode, | ||
randomOrderMode: this._randomOrder, | ||
}; | ||
} | ||
setResultForCurrentTest(result) { | ||
this.currentSuite().currentTest().setResult(result); | ||
this._currentSuite.currentTest().setResult(result); | ||
} | ||
finish(callbacks) { | ||
return this._considerResultAsSucceeded() | ||
? callbacks.success() | ||
: callbacks.failure(); | ||
finish() { | ||
if (this.hasErrorsOrFailures()) { | ||
return this._callbacks.onFailure(this); | ||
} else { | ||
return this._callbacks.onSuccess(this); | ||
} | ||
} | ||
@@ -103,15 +116,5 @@ | ||
// Accessing | ||
currentSuite() { | ||
return this._currentSuite; | ||
} | ||
suites() { | ||
return this._suites; | ||
} | ||
allFailuresAndErrors() { | ||
return this.suites().reduce( | ||
(failures, suite) => failures.concat(suite.allFailuresAndErrors()), [] | ||
return this._suites.reduce((failures, suite) => | ||
failures.concat(suite.allFailuresAndErrors()), [], | ||
); | ||
@@ -130,8 +133,6 @@ } | ||
_considerResultAsSucceeded() { | ||
return this.errorsCount() + this.failuresCount() === 0; | ||
} | ||
_countEach(property) { | ||
return this.suites().reduce((count, suite) => count + suite[property](), 0); | ||
return this._suites.reduce((count, suite) => | ||
count + suite[property](), 0, | ||
); | ||
} | ||
@@ -141,3 +142,3 @@ | ||
if (this._randomOrder) { | ||
shuffle(this.suites()); | ||
shuffle(this._suites); | ||
} | ||
@@ -144,0 +145,0 @@ } |
'use strict'; | ||
const FailFast = require('./fail_fast'); | ||
const { shuffle, isString, isFunction } = require('./utils'); | ||
const { shuffle, isUndefined, isStringWithContent, isFunction } = require('./utils'); | ||
@@ -13,2 +12,3 @@ class TestSuite { | ||
this._before = undefined; | ||
this._after = undefined; | ||
} | ||
@@ -22,15 +22,24 @@ | ||
before(initialization) { | ||
if (this._before === undefined) | ||
this._before = initialization; | ||
else | ||
throw 'There is already a before() block. Please leave just one before() block and run again the tests.'; | ||
before(initializationBlock) { | ||
if (isUndefined(this._before)) { | ||
this._before = initializationBlock; | ||
} else { | ||
throw new Error('There is already a before() block. Please leave just one before() block and run again the tests.'); | ||
} | ||
} | ||
after(releasingBlock) { | ||
if (isUndefined(this._after)) { | ||
this._after = releasingBlock; | ||
} else { | ||
throw new Error('There is already an after() block. Please leave just one after() block and run again the tests.'); | ||
} | ||
} | ||
// Executing | ||
run(failFastMode = FailFast.default(), randomOrderMode = false) { | ||
run(context) { | ||
this._callbacks.onStart(this); | ||
this._evaluateSuiteDefinition(); | ||
this._runTests(failFastMode, randomOrderMode); | ||
this._runTests(context); | ||
this._callbacks.onFinish(this); | ||
@@ -96,4 +105,4 @@ } | ||
_ensureNameIsValid(name) { | ||
if (!isString(name)) { | ||
throw 'Suite does not have a valid name'; | ||
if (!isStringWithContent(name)) { | ||
throw new Error('Suite does not have a valid name'); | ||
} | ||
@@ -104,3 +113,3 @@ } | ||
if (!isFunction(body)) { | ||
throw 'Suite does not have a valid body'; | ||
throw new Error('Suite does not have a valid body'); | ||
} | ||
@@ -123,5 +132,8 @@ } | ||
// TODO: reify configuration instead of many boolean flags | ||
_runTests(failFastMode, randomOrderMode) { | ||
if (randomOrderMode) { | ||
_evaluateAfterBlock() { | ||
this._after && this._after.call(); | ||
} | ||
_runTests(context) { | ||
if (context.randomOrderMode) { | ||
this._randomizeTests(); | ||
@@ -132,3 +144,4 @@ } | ||
this._evaluateBeforeBlock(); | ||
test.run(failFastMode); | ||
test.run(context); | ||
this._evaluateAfterBlock(); | ||
}); | ||
@@ -135,0 +148,0 @@ } |
'use strict'; | ||
const TestResult = require('./test_result'); | ||
const FailFast = require('./fail_fast'); | ||
const { isString, isFunction, isUndefined } = require('./utils'); | ||
const { isStringWithContent, isFunction, isUndefined } = require('./utils'); | ||
@@ -17,4 +16,4 @@ class Test { | ||
run(failFastMode = FailFast.default()) { | ||
TestResult.evaluate(this, failFastMode); | ||
run(context) { | ||
TestResult.evaluate(this, context); | ||
} | ||
@@ -41,4 +40,5 @@ | ||
setResult(result) { | ||
if(this.hasNoResult() || this.isSuccess()) | ||
if (this.hasNoResult() || this.isSuccess()) { | ||
this._result = result; | ||
} | ||
} | ||
@@ -65,7 +65,7 @@ | ||
hasDefinition() { | ||
return this.body() !== undefined; | ||
return !isUndefined(this.body()); | ||
} | ||
hasNoResult() { | ||
return this.result() === undefined; | ||
return isUndefined(this.result()); | ||
} | ||
@@ -124,4 +124,4 @@ | ||
_ensureNameIsValid(name) { | ||
if (!isString(name)) { | ||
throw 'Test does not have a valid name'; | ||
if (!isStringWithContent(name)) { | ||
throw new Error('Test does not have a valid name'); | ||
} | ||
@@ -132,3 +132,3 @@ } | ||
if (!isUndefined(body) && !isFunction(body)) { | ||
throw 'Test does not have a valid body'; | ||
throw new Error('Test does not have a valid body'); | ||
} | ||
@@ -135,0 +135,0 @@ } |
@@ -19,26 +19,26 @@ { | ||
"failures_summary": "Failures summary:", | ||
"expected": "Expected", | ||
"to": "to ", | ||
"be_equal_to": "be equal to", | ||
"be_not_equal_to": "be not equal to", | ||
"to_happen": "to happen", | ||
"not_to_happen": "not to happen", | ||
"but_got": "but got", | ||
"instead": "instead", | ||
"expecting_error": "error", | ||
"be_true": "be true", | ||
"be_false": "be false", | ||
"be_undefined": "be undefined", | ||
"be_defined": "be defined", | ||
"be_null": "be null", | ||
"be_not_null": "be not null", | ||
"include": "include", | ||
"not_include": "not include", | ||
"include_exactly": "include exactly", | ||
"be_empty": "be empty", | ||
"be_not_empty": "be not empty", | ||
"match": "match", | ||
"expected_no_errors": "Expected no errors to happen", | ||
"but": "but", | ||
"was_raised": "was raised", | ||
"expectation_be_true": "Expected %s to be true", | ||
"expectation_be_false": "Expected %s to be false", | ||
"expectation_be_undefined": "Expected %s to be undefined", | ||
"expectation_be_defined": "Expected %s to be defined", | ||
"expectation_be_null": "Expected %s to be null", | ||
"expectation_be_not_null": "Expected %s to be not null", | ||
"expectation_be_near_to": "Expected %s to be near to %s (using %s precision digits)", | ||
"expectation_match_regex": "Expected %s to match %s", | ||
"expectation_include": "Expected %s to include %s", | ||
"expectation_not_include": "Expected %s to not include %s", | ||
"expectation_be_included_in": "Expected %s to be included in %s", | ||
"expectation_be_not_included_in": "Expected %s to be not included in %s", | ||
"expectation_include_exactly": "Expected %s to include exactly %s", | ||
"expectation_be_empty": "Expected %s to be empty", | ||
"expectation_be_not_empty": "Expected %s to be not empty", | ||
"expectation_error": "Expected error %s to happen", | ||
"expectation_no_error": "Expected error %s not to happen", | ||
"expectation_no_errors": "Expected no errors to happen, but %s was raised", | ||
"expectation_different_error": "Expected error %s to happen, but got %s instead", | ||
"equality_assertion_be_equal_to": "Expected %s to be equal to %s", | ||
"equality_assertion_be_not_equal_to": "Expected %s to be not equal to %s", | ||
"equality_assertion_failed_due_to_undetermination": "Equality cannot be determined. Both parts are undefined", | ||
"equality_assertion_failed_due_to_missing_property": "Equality check failed. Objects do not have %s property", | ||
"equality_assertion_failed_due_to_circular_references": "(circular references found, equality check cannot be done. Please compare objects' properties individually)", | ||
"explicitly_failed": "Explicitly failed", | ||
@@ -48,3 +48,4 @@ "running_tests_in": "Running tests in", | ||
"random_order": "Random order", | ||
"starting_testy": "Starting Testy!" | ||
"starting_testy": "Starting Testy!", | ||
"invalid_pending_reason": "In order to mark a test as pending, you need to specify a reason." | ||
}, | ||
@@ -68,26 +69,26 @@ "es": { | ||
"failures_summary": "Resumen de errores/fallidos:", | ||
"expected": "Se esperaba que", | ||
"to": "", | ||
"be_equal_to": "sea igual a", | ||
"be_not_equal_to": "no sea igual a", | ||
"to_happen": "ocurra", | ||
"not_to_happen": "no ocurra", | ||
"but_got": "pero se obtuvo", | ||
"instead": "en su lugar", | ||
"expecting_error": "el error", | ||
"be_true": "sea true", | ||
"be_false": "sea false", | ||
"be_undefined": "sea undefined", | ||
"be_defined": "esté definido", | ||
"be_null": "sea null", | ||
"be_not_null": "no sea null", | ||
"include": "incluya a", | ||
"not_include": "no incluya a", | ||
"include_exactly": "incluya exactamente a", | ||
"be_empty": "sea vacío", | ||
"be_not_empty": "no sea vacío", | ||
"match": "sea cubierto por la expresión regular", | ||
"expected_no_errors": "No se esperaban errores", | ||
"but": "pero", | ||
"was_raised": "fue lanzado", | ||
"expectation_be_true": "Se esperaba que %s sea true", | ||
"expectation_be_false": "Se esperaba que %s sea false", | ||
"expectation_be_undefined": "Se esperaba que %s sea undefined", | ||
"expectation_be_defined": "Se esperaba que %s esté definido", | ||
"expectation_be_null": "Se esperaba que %s sea null", | ||
"expectation_be_not_null": "Se esperaba que %s no sea null", | ||
"expectation_be_near_to": "Se esperaba que %s esté cercano a %s (usando %s dígitos de precisión)", | ||
"expectation_match_regex": "Se esperaba que %s sea cubierto por la expresión regular %s", | ||
"expectation_include": "Se esperaba que %s incluya a %s", | ||
"expectation_not_include": "Se esperaba que %s incluya a %s", | ||
"expectation_be_included_in": "Se esperaba que %s esté incluido en %s", | ||
"expectation_be_not_included_in": "Se esperaba que %s no esté incluido en %s", | ||
"expectation_include_exactly": "Se esperaba que %s incluya exactamente a %s", | ||
"expectation_be_empty": "Se esperaba que %s sea vacío", | ||
"expectation_be_not_empty": "Se esperaba que %s no sea vacío", | ||
"expectation_error": "Se esperaba que ocurra el error %s", | ||
"expectation_no_error": "No se esperaba que ocurra el error %s", | ||
"expectation_no_errors": "No se esperaban errores, pero %s fue lanzado", | ||
"expectation_different_error": "Se esperaba que ocurra el error %s, pero en su lugar ocurrió el error %s", | ||
"equality_assertion_be_equal_to": "Se esperaba que %s sea igual a %s", | ||
"equality_assertion_be_not_equal_to": "Se esperaba que %s no sea igual a %s", | ||
"equality_undetermined": "La igualdad no puede ser determinada. Ambas partes son undefined", | ||
"equality_assertion_failed_due_to_missing_property": "Igualdad entre objetos fallida. Los objetos no tienen la propiedad %s", | ||
"equality_assertion_failed_due_to_circular_references": "(se encontraron referencias circulares, no es posible verificar la igualdad. Por favor, comparar propiedades de los objetos de manera individual)", | ||
"explicitly_failed": "Explícitamente marcado como fallido", | ||
@@ -97,4 +98,5 @@ "running_tests_in": "Ejecutando tests en", | ||
"random_order": "Orden aleatorio", | ||
"starting_testy": "Comenzando a ejecutar Testy!" | ||
"starting_testy": "Comenzando a ejecutar Testy!", | ||
"invalid_pending_reason": "Para marcar un test como pendiente, es necesario especificar un motivo." | ||
} | ||
} |
@@ -9,3 +9,5 @@ 'use strict'; | ||
const isCyclic = object => { | ||
if (object === null) { return false; } | ||
if (object === null) { | ||
return false; | ||
} | ||
const seenObjects = []; | ||
@@ -15,3 +17,5 @@ | ||
if (typeof obj === 'object') { | ||
if (seenObjects.includes(obj)) return true; | ||
if (seenObjects.includes(obj)) { | ||
return true; | ||
} | ||
seenObjects.push(obj); | ||
@@ -42,5 +46,4 @@ return !!Object.keys(obj).find(key => detect(obj[key])); | ||
} | ||
fs.readdirSync(dir).forEach(entry => | ||
results.push(...allFilesMatching(path.join(dir, entry), regex, results)) | ||
results.push(...allFilesMatching(path.join(dir, entry), regex, results)), | ||
); | ||
@@ -78,2 +81,5 @@ return results; | ||
const isStringWithContent = string => | ||
isString(string) && string.replace(/\s/g, '').length > 0; | ||
const respondsTo = (object, methodName) => | ||
@@ -94,2 +100,22 @@ notNullOrUndefined(object) && isFunction(object[methodName]); | ||
const stringToArray = object => Array.from(object); | ||
const arrayToArray = object => object; | ||
const setToArray = object => Array.from(object); | ||
const mapToArray = object => Array.from(object.values()); | ||
const convertToArray = object => { | ||
const conversionFunctions = { | ||
String: stringToArray, | ||
Array: arrayToArray, | ||
Set: setToArray, | ||
Map: mapToArray, | ||
}; | ||
const conversionFunction = conversionFunctions[object.constructor.name]; | ||
return conversionFunction(object); | ||
}; | ||
const subclassResponsibility = () => { | ||
throw new Error('subclass responsibility'); | ||
}; | ||
module.exports = { | ||
@@ -103,2 +129,3 @@ // comparison on objects | ||
isString, | ||
isStringWithContent, | ||
isFunction, | ||
@@ -112,5 +139,8 @@ isUndefined, | ||
numberOfElements, | ||
convertToArray, | ||
// files | ||
resolvePathFor, | ||
allFilesMatching, | ||
// object orientation | ||
subclassResponsibility, | ||
}; |
{ | ||
"name": "@pmoo/testy", | ||
"version": "5.0.2", | ||
"version": "5.1.0", | ||
"description": "A minimal testing framework, for educational purposes.", | ||
@@ -10,2 +10,5 @@ "homepage": "https://ngarbezza.github.io/testy/", | ||
}, | ||
"bugs": { | ||
"url": "https://github.com/ngarbezza/testy/issues" | ||
}, | ||
"engines": { | ||
@@ -17,3 +20,6 @@ "node": ">= 8.*" | ||
"coverage": "npx nyc@latest --reporter=lcov --reporter=text-summary npm test", | ||
"open-coverage-report": "xdg-open coverage/lcov-report/index.html", | ||
"generate-dependencies-graph": "npx madge -i testy-dependencies.png lib/ bin/", | ||
"lint": "npx eslint@6.8.0 .", | ||
"lint-fix": "npx eslint@6.8.0 . --fix", | ||
"test": "bin/testy_cli.js" | ||
@@ -20,0 +26,0 @@ }, |
@@ -18,10 +18,9 @@ # Testy | ||
![package-size](https://img.shields.io/bundlephobia/min/@pmoo/testy.svg?logo=npm) | ||
![activity](https://img.shields.io/github/commit-activity/w/ngarbezza/testy.svg?logo=npm) | ||
![activity](https://img.shields.io/github/commit-activity/m/ngarbezza/testy?logo=npm) | ||
![release-date](https://img.shields.io/github/release-date/ngarbezza/testy.svg?logo=npm) | ||
\ | ||
[![all-contributors](https://img.shields.io/badge/all_contributors-2-orange.svg?logo=open-source-initiative)](#contributors) | ||
[![all-contributors](https://img.shields.io/github/all-contributors/ngarbezza/testy?logo=open-source-initiative)](#Contributors) | ||
A very simple JS testing framework, for educational purposes. Live at npm at [@pmoo/testy](https://www.npmjs.com/package/@pmoo/testy). | ||
:arrow_right: [v4 (legacy version) documentation here](README_v4.md) \ | ||
:arrow_right: [Documentación en español aquí](README_es.md) | ||
@@ -168,2 +167,19 @@ | ||
``` | ||
* **Running code after every test**: just like many testing frameworks have, there is a way to execute some code after (cleanp) every test in a suite using the `after()` function. Example: | ||
```javascript | ||
const { suite, test, before, after, assert } = require('@pmoo/testy'); | ||
suite('using the after() helper', () => { | ||
let answer; | ||
before(() => { | ||
answer = 42; | ||
}); | ||
after(() => { | ||
answer = undefined; | ||
}); | ||
}); | ||
``` | ||
* **Support for pending tests**: if a test has no body, it will be reported as `[WIP]` and it won't be considered a failure. | ||
@@ -221,4 +237,10 @@ * **Fail-Fast mode**: if enabled, it stops execution in the first test that fails (or has an error). Remaining tests will be marked as skipped. | ||
<td align="center"><a href="https://github.com/abraaoduarte"><img src="https://avatars2.githubusercontent.com/u/6676804?v=4" width="100px;" alt=""/><br /><sub><b>Abraão Duarte</b></sub></a><br /><a href="https://github.com/ngarbezza/testy/commits?author=abraaoduarte" title="Code">💻</a></td> | ||
<td align="center"><a href="http://adico.tech"><img src="https://avatars0.githubusercontent.com/u/5412270?v=4" width="100px;" alt=""/><br /><sub><b>adico</b></sub></a><br /><a href="https://github.com/ngarbezza/testy/commits?author=adico1" title="Code">💻</a></td> | ||
<td align="center"><a href="http://adico.tech"><img src="https://avatars0.githubusercontent.com/u/5412270?v=4" width="100px;" alt=""/><br /><sub><b>adico</b></sub></a><br /><a href="https://github.com/ngarbezza/testy/commits?author=adico1" title="Code">💻</a> <a href="https://github.com/ngarbezza/testy/commits?author=adico1" title="Tests">⚠️</a></td> | ||
<td align="center"><a href="https://github.com/ask-imran"><img src="https://avatars0.githubusercontent.com/u/20487103?v=4" width="100px;" alt=""/><br /><sub><b>Askar Imran</b></sub></a><br /><a href="https://github.com/ngarbezza/testy/commits?author=ask-imran" title="Code">💻</a> <a href="https://github.com/ngarbezza/testy/commits?author=ask-imran" title="Tests">⚠️</a></td> | ||
<td align="center"><a href="http://www.nigelyong.com/"><img src="https://avatars2.githubusercontent.com/u/23243585?v=4" width="100px;" alt=""/><br /><sub><b>Nigel Yong</b></sub></a><br /><a href="https://github.com/ngarbezza/testy/commits?author=niyonx" title="Code">💻</a></td> | ||
<td align="center"><a href="https://github.com/chelsieng"><img src="https://avatars1.githubusercontent.com/u/60008262?v=4" width="100px;" alt=""/><br /><sub><b>Chelsie Ng</b></sub></a><br /><a href="https://github.com/ngarbezza/testy/commits?author=chelsieng" title="Code">💻</a></td> | ||
</tr> | ||
<tr> | ||
<td align="center"><a href="https://github.com/trochepablo"><img src="https://avatars2.githubusercontent.com/u/18213369?v=4" width="100px;" alt=""/><br /><sub><b>Pablo T</b></sub></a><br /><a href="https://github.com/ngarbezza/testy/commits?author=trochepablo" title="Tests">⚠️</a> <a href="https://github.com/ngarbezza/testy/commits?author=trochepablo" title="Code">💻</a></td> | ||
</tr> | ||
</table> | ||
@@ -225,0 +247,0 @@ |
38
testy.js
@@ -7,3 +7,3 @@ 'use strict'; | ||
const ConsoleUI = require(`${libDir}/console_ui`); | ||
const Utils = require(`${libDir}/utils`); | ||
const { allFilesMatching, resolvePathFor } = require(`${libDir}/utils`); | ||
@@ -21,3 +21,3 @@ const ui = new ConsoleUI(); | ||
function suite(name, suiteBody) { | ||
return testRunner.registerSuite(name, suiteBody, ui.suiteCallbacks()); | ||
testRunner.registerSuite(name, suiteBody, ui.suiteCallbacks()); | ||
} | ||
@@ -29,2 +29,6 @@ | ||
function after(initialization) { | ||
testRunner.registerAfter(initialization); | ||
} | ||
class Testy { | ||
@@ -46,10 +50,6 @@ // instance creation | ||
this._loadAllRequestedFiles(); | ||
ui.displayInitialSummary(this._configuration, this._testFilesPathsToRun()); | ||
ui.measuringTotalTime(() => | ||
testRunner.run() | ||
ui.start(this._configuration, this._testFilesPathsToRun(), () => | ||
testRunner.run(), | ||
); | ||
testRunner.finish({ | ||
success: () => process.exit(0), | ||
failure: () => process.exit(1), | ||
}); | ||
testRunner.finish(); | ||
} | ||
@@ -72,7 +72,11 @@ | ||
_loadAllRequestedFiles() { | ||
this._resolvedTestFilesPathsToRun().forEach(path => | ||
Utils.allFilesMatching(path, this._testFilesFilter()).forEach(file => | ||
require(file) | ||
) | ||
); | ||
try { | ||
this._resolvedTestFilesPathsToRun().forEach(path => | ||
allFilesMatching(path, this._testFilesFilter()).forEach(file => | ||
require(file), | ||
), | ||
); | ||
} catch (err) { | ||
ui.exitWithError(`Error: ${err.path} does not exist.`); | ||
} | ||
} | ||
@@ -85,4 +89,4 @@ | ||
_resolvedTestFilesPathsToRun() { | ||
return this._testFilesPathsToRun().map(path => Utils.resolvePathFor(path)); | ||
_resolvedTestFilesPathsToRun(){ | ||
return this._testFilesPathsToRun().map(path => resolvePathFor(path)); | ||
} | ||
@@ -99,2 +103,2 @@ | ||
module.exports = { Testy, suite, test, before, assert, fail, pending }; | ||
module.exports = { Testy, suite, test, before, after, assert, fail, pending }; |
Sorry, the diff of this file is not supported yet
No bug tracker
MaintenancePackage does not have a linked bug tracker in package.json.
Found 1 instance in 1 package
84176
23
1642
0
250