diff --git a/README.md b/README.md index d74d0e77..cdfdd539 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![Build Status](https://travis-ci.org/jasmine/jasmine.svg?branch=master)](https://travis-ci.org/jasmine/jasmine) [![Open Source Helpers](https://www.codetriage.com/jasmine/jasmine/badges/users.svg)](https://www.codetriage.com/jasmine/jasmine) +[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fjasmine%2Fjasmine.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fjasmine%2Fjasmine?ref=badge_shield) # A JavaScript Testing Framework @@ -75,3 +76,7 @@ Jasmine tests itself across many browsers (Safari, Chrome, Firefox, PhantomJS, M * Sheel Choksi Copyright (c) 2008-2018 Pivotal Labs. This software is licensed under the MIT License. + + +## License +[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fjasmine%2Fjasmine.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fjasmine%2Fjasmine?ref=badge_large) \ No newline at end of file diff --git a/lib/jasmine-core/boot.js b/lib/jasmine-core/boot.js index 86dfd74a..cd1a29a6 100644 --- a/lib/jasmine-core/boot.js +++ b/lib/jasmine-core/boot.js @@ -73,21 +73,21 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. var filterSpecs = !!queryString.getParam("spec"); - var stoppingOnSpecFailure = queryString.getParam("failFast"); - env.stopOnSpecFailure(stoppingOnSpecFailure); - - var throwingExpectationFailures = queryString.getParam("throwFailures"); - env.throwOnExpectationFailure(throwingExpectationFailures); + var config = { + failFast: queryString.getParam("failFast"), + oneFailurePerSpec: queryString.getParam("oneFailurePerSpec"), + hideDisabled: queryString.getParam("hideDisabled") + }; var random = queryString.getParam("random"); if (random !== undefined && random !== "") { - env.randomizeTests(random); + config.random = random; } var seed = queryString.getParam("seed"); if (seed) { - env.seed(seed); + config.seed = seed; } /** @@ -118,10 +118,12 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. filterString: function() { return queryString.getParam("spec"); } }); - env.specFilter = function(spec) { + config.specFilter = function(spec) { return specFilter.matches(spec.getFullName()); }; + env.configure(config); + /** * Setting up timing functions to be able to be overridden. Certain browsers (Safari, IE 8, phantomjs) require this hack. */ diff --git a/lib/jasmine-core/boot/boot.js b/lib/jasmine-core/boot/boot.js index 57f52e5e..2d684628 100644 --- a/lib/jasmine-core/boot/boot.js +++ b/lib/jasmine-core/boot/boot.js @@ -51,21 +51,21 @@ var filterSpecs = !!queryString.getParam("spec"); - var stoppingOnSpecFailure = queryString.getParam("failFast"); - env.stopOnSpecFailure(stoppingOnSpecFailure); - - var throwingExpectationFailures = queryString.getParam("throwFailures"); - env.throwOnExpectationFailure(throwingExpectationFailures); + var config = { + failFast: queryString.getParam("failFast"), + oneFailurePerSpec: queryString.getParam("oneFailurePerSpec"), + hideDisabled: queryString.getParam("hideDisabled") + }; var random = queryString.getParam("random"); if (random !== undefined && random !== "") { - env.randomizeTests(random); + config.random = random; } var seed = queryString.getParam("seed"); if (seed) { - env.seed(seed); + config.seed = seed; } /** @@ -96,10 +96,12 @@ filterString: function() { return queryString.getParam("spec"); } }); - env.specFilter = function(spec) { + config.specFilter = function(spec) { return specFilter.matches(spec.getFullName()); }; + env.configure(config); + /** * Setting up timing functions to be able to be overridden. Certain browsers (Safari, IE 8, phantomjs) require this hack. */ diff --git a/lib/jasmine-core/jasmine-html.js b/lib/jasmine-core/jasmine-html.js index 539d8c61..3f808bf6 100644 --- a/lib/jasmine-core/jasmine-html.js +++ b/lib/jasmine-core/jasmine-html.js @@ -80,7 +80,7 @@ jasmineRequire.HtmlReporter = function(j$) { function HtmlReporter(options) { - var env = options.env || {}, + var config = function() { return (options.env && options.env.configuration()) || {}; }, getContainer = options.getContainer, createElement = options.createElement, createTextNode = options.createTextNode, @@ -148,7 +148,7 @@ jasmineRequire.HtmlReporter = function(j$) { } symbols.appendChild(createDom('li', { - className: noExpectations(result) ? 'jasmine-empty' : 'jasmine-' + result.status, + className: this.displaySpecInCorrectFormat(result), id: 'spec_' + result.id, title: result.fullName } @@ -161,6 +161,17 @@ jasmineRequire.HtmlReporter = function(j$) { addDeprecationWarnings(result); }; + this.displaySpecInCorrectFormat = function(result) { + return noExpectations(result) ? 'jasmine-empty' : this.resultStatus(result.status); + }; + + this.resultStatus = function(status) { + if(status === 'excluded') { + return config().hideDisabled ? 'jasmine-excluded-no-display' : 'jasmine-excluded'; + } + return 'jasmine-' + status; + }; + this.jasmineDone = function(doneResult) { var banner = find('.jasmine-banner'); var alert = find('.jasmine-alert'); @@ -168,7 +179,7 @@ jasmineRequire.HtmlReporter = function(j$) { var i; alert.appendChild(createDom('span', {className: 'jasmine-duration'}, 'finished in ' + timer.elapsed() / 1000 + 's')); - banner.appendChild(optionsMenu(env)); + banner.appendChild(optionsMenu(config())); if (stateBuilder.specsExecuted < totalSpecsDefined) { var skippedMessage = 'Ran ' + stateBuilder.specsExecuted + ' of ' + totalSpecsDefined + ' specs - run all'; @@ -328,7 +339,7 @@ jasmineRequire.HtmlReporter = function(j$) { } } - function optionsMenu(env) { + function optionsMenu(config) { var optionsMenuDom = createDom('div', { className: 'jasmine-run-options' }, createDom('span', { className: 'jasmine-trigger' }, 'Options'), createDom('div', { className: 'jasmine-payload' }, @@ -352,26 +363,39 @@ jasmineRequire.HtmlReporter = function(j$) { id: 'jasmine-random-order', type: 'checkbox' }), - createDom('label', { className: 'jasmine-label', 'for': 'jasmine-random-order' }, 'run tests in random order')) + createDom('label', { className: 'jasmine-label', 'for': 'jasmine-random-order' }, 'run tests in random order')), + createDom('div', { className: 'jasmine-hide-disabled' }, + createDom('input', { + className: 'jasmine-disabled', + id: 'jasmine-hide-disabled', + type: 'checkbox' + }), + createDom('label', { className: 'jasmine-label', 'for': 'jasmine-hide-disabled' }, 'hide disabled tests')) ) ); var failFastCheckbox = optionsMenuDom.querySelector('#jasmine-fail-fast'); - failFastCheckbox.checked = env.stoppingOnSpecFailure(); + failFastCheckbox.checked = config.failFast; failFastCheckbox.onclick = function() { - navigateWithNewParam('failFast', !env.stoppingOnSpecFailure()); + navigateWithNewParam('failFast', !config.failFast); }; var throwCheckbox = optionsMenuDom.querySelector('#jasmine-throw-failures'); - throwCheckbox.checked = env.throwingExpectationFailures(); + throwCheckbox.checked = config.oneFailurePerSpec; throwCheckbox.onclick = function() { - navigateWithNewParam('throwFailures', !env.throwingExpectationFailures()); + navigateWithNewParam('throwFailures', !config.oneFailurePerSpec); }; var randomCheckbox = optionsMenuDom.querySelector('#jasmine-random-order'); - randomCheckbox.checked = env.randomTests(); + randomCheckbox.checked = config.random; randomCheckbox.onclick = function() { - navigateWithNewParam('random', !env.randomTests()); + navigateWithNewParam('random', !config.random); + }; + + var hideDisabled = optionsMenuDom.querySelector('#jasmine-hide-disabled'); + hideDisabled.checked = config.hideDisabled; + hideDisabled.onclick = function() { + navigateWithNewParam('hideDisabled', !config.hideDisabled); }; var optionsTrigger = optionsMenuDom.querySelector('.jasmine-trigger'), diff --git a/lib/jasmine-core/jasmine.css b/lib/jasmine-core/jasmine.css index 50c59d54..3750281e 100644 --- a/lib/jasmine-core/jasmine.css +++ b/lib/jasmine-core/jasmine.css @@ -1,3 +1,4 @@ +@charset "UTF-8"; body { overflow-y: scroll; } .jasmine_html-reporter { background-color: #eee; padding: 5px; margin: -8px; font-size: 11px; font-family: Monaco, "Lucida Console", monospace; line-height: 14px; color: #333; } @@ -15,15 +16,16 @@ body { overflow-y: scroll; } .jasmine_html-reporter .jasmine-symbol-summary { overflow: hidden; *zoom: 1; margin: 14px 0; } .jasmine_html-reporter .jasmine-symbol-summary li { display: inline-block; height: 10px; width: 14px; font-size: 16px; } .jasmine_html-reporter .jasmine-symbol-summary li.jasmine-passed { font-size: 14px; } -.jasmine_html-reporter .jasmine-symbol-summary li.jasmine-passed:before { color: #007069; content: "\02022"; } +.jasmine_html-reporter .jasmine-symbol-summary li.jasmine-passed:before { color: #007069; content: "•"; } .jasmine_html-reporter .jasmine-symbol-summary li.jasmine-failed { line-height: 9px; } -.jasmine_html-reporter .jasmine-symbol-summary li.jasmine-failed:before { color: #ca3a11; content: "\d7"; font-weight: bold; margin-left: -1px; } +.jasmine_html-reporter .jasmine-symbol-summary li.jasmine-failed:before { color: #ca3a11; content: "×"; font-weight: bold; margin-left: -1px; } .jasmine_html-reporter .jasmine-symbol-summary li.jasmine-excluded { font-size: 14px; } -.jasmine_html-reporter .jasmine-symbol-summary li.jasmine-excluded:before { color: #bababa; content: "\02022"; } +.jasmine_html-reporter .jasmine-symbol-summary li.jasmine-excluded:before { color: #bababa; content: "•"; } +.jasmine_html-reporter .jasmine-symbol-summary li.jasmine-excluded-no-display { font-size: 14px; display: none; } .jasmine_html-reporter .jasmine-symbol-summary li.jasmine-pending { line-height: 17px; } .jasmine_html-reporter .jasmine-symbol-summary li.jasmine-pending:before { color: #ba9d37; content: "*"; } .jasmine_html-reporter .jasmine-symbol-summary li.jasmine-empty { font-size: 14px; } -.jasmine_html-reporter .jasmine-symbol-summary li.jasmine-empty:before { color: #ba9d37; content: "\02022"; } +.jasmine_html-reporter .jasmine-symbol-summary li.jasmine-empty:before { color: #ba9d37; content: "•"; } .jasmine_html-reporter .jasmine-run-options { float: right; margin-right: 5px; border: 1px solid #8a4182; color: #8a4182; position: relative; line-height: 20px; } .jasmine_html-reporter .jasmine-run-options .jasmine-trigger { cursor: pointer; padding: 8px 16px; } .jasmine_html-reporter .jasmine-run-options .jasmine-payload { position: absolute; display: none; right: -1px; border: 1px solid #8a4182; background-color: #eee; white-space: nowrap; padding: 4px 8px; } @@ -48,6 +50,11 @@ body { overflow-y: scroll; } .jasmine_html-reporter .jasmine-summary li.jasmine-empty a { color: #ba9d37; } .jasmine_html-reporter .jasmine-summary li.jasmine-pending a { color: #ba9d37; } .jasmine_html-reporter .jasmine-summary li.jasmine-excluded a { color: #bababa; } +.jasmine_html-reporter .jasmine-specs li.jasmine-passed a:before { content: "• "; } +.jasmine_html-reporter .jasmine-specs li.jasmine-failed a:before { content: "× "; } +.jasmine_html-reporter .jasmine-specs li.jasmine-empty a:before { content: "* "; } +.jasmine_html-reporter .jasmine-specs li.jasmine-pending a:before { content: "• "; } +.jasmine_html-reporter .jasmine-specs li.jasmine-excluded a:before { content: "• "; } .jasmine_html-reporter .jasmine-description + .jasmine-suite { margin-top: 0; } .jasmine_html-reporter .jasmine-suite { margin-top: 14px; } .jasmine_html-reporter .jasmine-suite a { color: #333; } diff --git a/lib/jasmine-core/jasmine.js b/lib/jasmine-core/jasmine.js index 0fe31b92..a3320b56 100644 --- a/lib/jasmine-core/jasmine.js +++ b/lib/jasmine-core/jasmine.js @@ -59,7 +59,9 @@ var getJasmineRequireObj = (function (jasmineGlobal) { j$.Env = jRequire.Env(j$); j$.StackTrace = jRequire.StackTrace(j$); j$.ExceptionFormatter = jRequire.ExceptionFormatter(j$); - j$.Expectation = jRequire.Expectation(); + j$.ExpectationFilterChain = jRequire.ExpectationFilterChain(); + j$.Expectation = jRequire.Expectation(j$); + j$.AsyncExpectation = jRequire.AsyncExpectation(j$); j$.buildExpectationResult = jRequire.buildExpectationResult(); j$.JsApiReporter = jRequire.JsApiReporter(); j$.matchersUtil = jRequire.matchersUtil(j$); @@ -260,7 +262,11 @@ getJasmineRequireObj().base = function(j$, jasmineGlobal) { }; j$.isPromise = function(obj) { - return typeof jasmineGlobal.Promise !== 'undefined' && obj.constructor === jasmineGlobal.Promise; + return typeof jasmineGlobal.Promise !== 'undefined' && !!obj && obj.constructor === jasmineGlobal.Promise; + }; + + j$.isPromiseLike = function(obj) { + return !!obj && j$.isFunction_(obj.then); }; j$.fnNameFor = function(func) { @@ -536,6 +542,7 @@ getJasmineRequireObj().util = function(j$) { getJasmineRequireObj().Spec = function(j$) { function Spec(attrs) { this.expectationFactory = attrs.expectationFactory; + this.asyncExpectationFactory = attrs.asyncExpectationFactory; this.resultCallback = attrs.resultCallback || function() {}; this.id = attrs.id; this.description = attrs.description || ''; @@ -592,6 +599,10 @@ getJasmineRequireObj().Spec = function(j$) { return this.expectationFactory(actual, this); }; + Spec.prototype.expectAsync = function(actual) { + return this.asyncExpectationFactory(actual, this); + }; + Spec.prototype.execute = function(onComplete, excluded) { var self = this; @@ -788,13 +799,62 @@ getJasmineRequireObj().Env = function(j$) { var currentSpec = null; var currentlyExecutingSuites = []; var currentDeclarationSuite = null; - var throwOnExpectationFailure = false; - var stopOnSpecFailure = false; - var random = true; - var seed = null; - var handlingLoadErrors = true; var hasFailures = false; + /** + * This represents the available options to configure Jasmine. + * Options that are not provided will use their default values + * @interface Configuration + */ + var config = { + /** + * Whether to randomize spec execution order + * @name Configuration#random + * @type Boolean + * @default true + */ + random: true, + /** + * Seed to use as the basis of randomization. + * Null causes the seed to be determined randomly at the start of execution. + * @name Configuration#seed + * @type function + * @default null + */ + seed: null, + /** + * Whether to stop execution of the suite after the first spec failure + * @name Configuration#failFast + * @type Boolean + * @default false + */ + failFast: false, + /** + * Whether to cause specs to only have one expectation failure. + * @name Configuration#oneFailurePerSpec + * @type Boolean + * @default false + */ + oneFailurePerSpec: false, + /** + * Function to use to filter specs + * @name Configuration#specFilter + * @type function + * @default true + */ + specFilter: function() { + return true; + }, + /** + * Whether or not reporters should hide disabled specs from their output. + * Currently only supported by Jasmine's HTMLReporter + * @name Configuration#hideDisabled + * @type Boolean + * @default false + */ + hideDisabled: false + }; + var currentSuite = function() { return currentlyExecutingSuites[currentlyExecutingSuites.length - 1]; }; @@ -827,10 +887,63 @@ getJasmineRequireObj().Env = function(j$) { }); } - this.specFilter = function() { - return true; + /** + * Configure your jasmine environment + * @name Env#configure + * @argument {Configuration} configuration + * @function + */ + this.configure = function(configuration) { + if (configuration.specFilter) { + config.specFilter = configuration.specFilter; + } + + if (configuration.hasOwnProperty('random')) { + config.random = !!configuration.random; + } + + if (configuration.hasOwnProperty('seed')) { + config.seed = configuration.seed; + } + + if (configuration.hasOwnProperty('failFast')) { + config.failFast = configuration.failFast; + } + + if (configuration.hasOwnProperty('oneFailurePerSpec')) { + config.oneFailurePerSpec = configuration.oneFailurePerSpec; + } + + if (configuration.hasOwnProperty('hideDisabled')) { + config.hideDisabled = configuration.hideDisabled; + } }; + /** + * Get the current configuration for your jasmine environment + * @name Env#configuration + * @function + * @returns {Configuration} + */ + this.configuration = function() { + var result = {}; + for (var property in config) { + result[property] = config[property]; + } + return result; + }; + + Object.defineProperty(this, 'specFilter', { + get: function() { + self.deprecated('Getting specFilter directly from Env is deprecated, please check the specFilter option from `configuration`'); + return config.specFilter; + }, + set: function(val) { + self.deprecated('Setting specFilter directly on Env is deprecated, please use the specFilter option in `configure`'); + config.specFilter = val; + } + }); + this.addSpyStrategy = function(name, fn) { if(!currentRunnable()) { throw new Error('Custom spy strategies must be added in a before function or a spec'); @@ -881,6 +994,19 @@ getJasmineRequireObj().Env = function(j$) { } }; + var asyncExpectationFactory = function(actual, spec) { + return j$.AsyncExpectation.factory({ + util: j$.matchersUtil, + customEqualityTesters: runnableResources[spec.id].customEqualityTesters, + actual: actual, + addExpectationResult: addExpectationResult + }); + + function addExpectationResult(passed, result) { + return spec.addExpectationResult(passed, result); + } + }; + var defaultResourcesForRunnable = function(id, parentRunnableId) { var resources = {spies: [], customEqualityTesters: [], customMatchers: {}, customSpyStrategies: {}}; @@ -939,35 +1065,85 @@ getJasmineRequireObj().Env = function(j$) { var maximumSpecCallbackDepth = 20; var currentSpecCallbackDepth = 0; + /** + * Sets whether Jasmine should throw an Error when an expectation fails. + * This causes a spec to only have one expectation failure. + * @name Env#throwOnExpectationFailure + * @function + * @param {Boolean} value Whether to throw when a expectation fails + * @deprecated Use the `oneFailurePerSpec` option with {@link Env#configure} + */ this.throwOnExpectationFailure = function(value) { - throwOnExpectationFailure = !!value; + this.deprecated('Setting throwOnExpectationFailure directly on Env is deprecated, please use the oneFailurePerSpec option in `configure`'); + this.configure({oneFailurePerSpec: !!value}); }; this.throwingExpectationFailures = function() { - return throwOnExpectationFailure; + this.deprecated('Getting throwingExpectationFailures directly from Env is deprecated, please check the oneFailurePerSpec option from `configuration`'); + return config.oneFailurePerSpec; }; + /** + * Set whether to stop suite execution when a spec fails + * @name Env#stopOnSpecFailure + * @function + * @param {Boolean} value Whether to stop suite execution when a spec fails + * @deprecated Use the `failFast` option with {@link Env#configure} + */ this.stopOnSpecFailure = function(value) { - stopOnSpecFailure = !!value; + this.deprecated('Setting stopOnSpecFailure directly is deprecated, please use the failFast option in `configure`'); + this.configure({failFast: !!value}); }; this.stoppingOnSpecFailure = function() { - return stopOnSpecFailure; + this.deprecated('Getting stoppingOnSpecFailure directly from Env is deprecated, please check the failFast option from `configuration`'); + return config.failFast; }; + /** + * Set whether to randomize test execution order + * @name Env#randomizeTests + * @function + * @param {Boolean} value Whether to randomize execution order + * @deprecated Use the `random` option with {@link Env#configure} + */ this.randomizeTests = function(value) { - random = !!value; + this.deprecated('Setting randomizeTests directly is deprecated, please use the random option in `configure`'); + config.random = !!value; }; this.randomTests = function() { - return random; + this.deprecated('Getting randomTests directly from Env is deprecated, please check the random option from `configuration`'); + return config.random; }; + /** + * Set the random number seed for spec randomization + * @name Env#seed + * @function + * @param {Number} value The seed value + * @deprecated Use the `seed` option with {@link Env#configure} + */ this.seed = function(value) { + this.deprecated('Setting seed directly is deprecated, please use the seed option in `configure`'); if (value) { - seed = value; + config.seed = value; } - return seed; + return config.seed; + }; + + this.hidingDisabled = function(value) { + this.deprecated('Getting hidingDisabled directly from Env is deprecated, please check the hideDisabled option from `configuration`'); + return config.hideDisabled; + }; + + /** + * @name Env#hideDisabled + * @function + */ + this.hideDisabled = function(value) { + this.deprecated('Setting hideDisabled directly is deprecated, please use the hideDisabled option in `configure`'); + config.hideDisabled = !!value; }; this.deprecated = function(deprecation) { @@ -981,9 +1157,9 @@ getJasmineRequireObj().Env = function(j$) { var queueRunnerFactory = function(options, args) { var failFast = false; if (options.isLeaf) { - failFast = throwOnExpectationFailure; + failFast = config.oneFailurePerSpec; } else if (!options.isReporter) { - failFast = stopOnSpecFailure; + failFast = config.failFast; } options.clearStack = options.clearStack || clearStack; options.timeout = {setTimeout: realSetTimeout, clearTimeout: realClearTimeout}; @@ -1003,6 +1179,7 @@ getJasmineRequireObj().Env = function(j$) { id: getNextSuiteId(), description: 'Jasmine__TopLevel__Suite', expectationFactory: expectationFactory, + asyncExpectationFactory: asyncExpectationFactory, expectationResultFactory: expectationResultFactory }); defaultResourcesForRunnable(topSuite.id); @@ -1097,8 +1274,8 @@ getJasmineRequireObj().Env = function(j$) { } var order = new j$.Order({ - random: random, - seed: seed + random: config.random, + seed: config.seed }); var processor = new j$.TreeProcessor({ @@ -1128,7 +1305,7 @@ getJasmineRequireObj().Env = function(j$) { return order.sort(node.children); }, excludeNode: function(spec) { - return !self.specFilter(spec); + return !config.specFilter(spec); } }); @@ -1196,10 +1373,22 @@ getJasmineRequireObj().Env = function(j$) { reporter.addReporter(reporterToAdd); }; + /** + * Provide a fallback reporter if no other reporters have been specified. + * @name Env#provideFallbackReporter + * @function + * @param {Reporter} reporterToAdd The reporter + * @see custom_reporter + */ this.provideFallbackReporter = function(reporterToAdd) { reporter.provideFallbackReporter(reporterToAdd); }; + /** + * Clear all registered reporters + * @name Env#clearReporters + * @function + */ this.clearReporters = function() { reporter.clearReporters(); }; @@ -1238,6 +1427,10 @@ getJasmineRequireObj().Env = function(j$) { return spyRegistry.spyOnProperty.apply(spyRegistry, arguments); }; + this.spyOnAllFunctions = function() { + return spyRegistry.spyOnAllFunctions.apply(spyRegistry, arguments); + }; + this.createSpy = function(name, originalFn) { if (arguments.length === 1 && j$.isFunction_(name)) { originalFn = name; @@ -1277,8 +1470,9 @@ getJasmineRequireObj().Env = function(j$) { description: description, parentSuite: currentDeclarationSuite, expectationFactory: expectationFactory, + asyncExpectationFactory: asyncExpectationFactory, expectationResultFactory: expectationResultFactory, - throwOnExpectationFailure: throwOnExpectationFailure + throwOnExpectationFailure: config.oneFailurePerSpec }); return suite; @@ -1370,6 +1564,7 @@ getJasmineRequireObj().Env = function(j$) { id: getNextSpecId(), beforeAndAfterFns: beforeAndAfterFns(suite), expectationFactory: expectationFactory, + asyncExpectationFactory: asyncExpectationFactory, resultCallback: specResultCallback, getSpecName: function(spec) { return getSpecName(spec, suite); @@ -1381,9 +1576,9 @@ getJasmineRequireObj().Env = function(j$) { userContext: function() { return suite.clonedSharedUserContext(); }, queueableFn: { fn: fn, - timeout: function() { return timeout || j$.DEFAULT_TIMEOUT_INTERVAL; } + timeout: timeout || 0 }, - throwOnExpectationFailure: throwOnExpectationFailure + throwOnExpectationFailure: config.oneFailurePerSpec }); return spec; @@ -1451,12 +1646,20 @@ getJasmineRequireObj().Env = function(j$) { return currentRunnable().expect(actual); }; + this.expectAsync = function(actual) { + if (!currentRunnable()) { + throw new Error('\'expectAsync\' was used when there was no current spec, this could be because an asynchronous test timed out'); + } + + return currentRunnable().expectAsync(actual); + }; + this.beforeEach = function(beforeEachFunction, timeout) { ensureIsNotNested('beforeEach'); ensureIsFunctionOrAsync(beforeEachFunction, 'beforeEach'); currentDeclarationSuite.beforeEach({ fn: beforeEachFunction, - timeout: function() { return timeout || j$.DEFAULT_TIMEOUT_INTERVAL; } + timeout: timeout || 0 }); }; @@ -1465,7 +1668,7 @@ getJasmineRequireObj().Env = function(j$) { ensureIsFunctionOrAsync(beforeAllFunction, 'beforeAll'); currentDeclarationSuite.beforeAll({ fn: beforeAllFunction, - timeout: function() { return timeout || j$.DEFAULT_TIMEOUT_INTERVAL; } + timeout: timeout || 0 }); }; @@ -1475,7 +1678,7 @@ getJasmineRequireObj().Env = function(j$) { afterEachFunction.isCleanup = true; currentDeclarationSuite.afterEach({ fn: afterEachFunction, - timeout: function() { return timeout || j$.DEFAULT_TIMEOUT_INTERVAL; } + timeout: timeout || 0 }); }; @@ -1484,7 +1687,7 @@ getJasmineRequireObj().Env = function(j$) { ensureIsFunctionOrAsync(afterAllFunction, 'afterAll'); currentDeclarationSuite.afterAll({ fn: afterAllFunction, - timeout: function() { return timeout || j$.DEFAULT_TIMEOUT_INTERVAL; } + timeout: timeout || 0 }); }; @@ -1523,7 +1726,7 @@ getJasmineRequireObj().Env = function(j$) { error: error && error.message ? error : null }); - if (self.throwingExpectationFailures()) { + if (config.oneFailurePerSpec) { throw new Error(message); } }; @@ -1941,6 +2144,235 @@ getJasmineRequireObj().Truthy = function(j$) { return Truthy; }; +getJasmineRequireObj().AsyncExpectation = function(j$) { + var promiseForMessage = { + jasmineToString: function() { return 'a promise'; } + }; + + /** + * Asynchronous matchers. + * @namespace async-matchers + */ + function AsyncExpectation(options) { + var global = options.global || j$.getGlobal(); + this.util = options.util || { buildFailureMessage: function() {} }; + this.customEqualityTesters = options.customEqualityTesters || []; + this.addExpectationResult = options.addExpectationResult || function(){}; + this.actual = options.actual; + this.filters = new j$.ExpectationFilterChain(); + + if (!global.Promise) { + throw new Error('expectAsync is unavailable because the environment does not support promises.'); + } + + if (!j$.isPromiseLike(this.actual)) { + throw new Error('Expected expectAsync to be called with a promise.'); + } + + ['toBeResolved', 'toBeRejected', 'toBeResolvedTo', 'toBeRejectedWith'].forEach(wrapCompare.bind(this)); + } + + function wrapCompare(name) { + var matcher = this[name]; + this[name] = function() { + var self = this; + var args = Array.prototype.slice.call(arguments); + args.unshift(this.actual); + + // Capture the call stack here, before we go async, so that it will + // contain frames that are relevant to the user instead of just parts + // of Jasmine. + var errorForStack = j$.util.errorWithStack(); + + var matcherCompare = this.instantiateMatcher(matcher); + + return matcherCompare.apply(self, args).then(function(result) { + var message; + + args[0] = promiseForMessage; + message = j$.Expectation.prototype.buildMessage.call(self, result, name, args); + + self.addExpectationResult(result.pass, { + matcherName: name, + passed: result.pass, + message: message, + error: undefined, + errorForStack: errorForStack, + actual: self.actual + }); + }); + }; + } + + AsyncExpectation.prototype.instantiateMatcher = function(matcher) { + var comparisonFunc = this.filters.selectComparisonFunc(matcher); + return comparisonFunc || matcher; + }; + + /** + * Expect a promise to be resolved. + * @function + * @async + * @name async-matchers#toBeResolved + * @example + * await expectAsync(aPromise).toBeResolved(); + * @example + * return expectAsync(aPromise).toBeResolved(); + */ + AsyncExpectation.prototype.toBeResolved = function(actual) { + return actual.then( + function() { return {pass: true}; }, + function() { return {pass: false}; } + ); + }; + + /** + * Expect a promise to be rejected. + * @function + * @async + * @name async-matchers#toBeRejected + * @example + * await expectAsync(aPromise).toBeRejected(); + * @example + * return expectAsync(aPromise).toBeRejected(); + */ + AsyncExpectation.prototype.toBeRejected = function(actual) { + return actual.then( + function() { return {pass: false}; }, + function() { return {pass: true}; } + ); + }; + + /** + * Expect a promise to be resolved to a value equal to the expected, using deep equality comparison. + * @function + * @async + * @name async-matchers#toBeResolvedTo + * @param {Object} expected - Value that the promise is expected to resolve to + * @example + * await expectAsync(aPromise).toBeResolvedTo({prop: 'value'}); + * @example + * return expectAsync(aPromise).toBeResolvedTo({prop: 'value'}); + */ + AsyncExpectation.prototype.toBeResolvedTo = function(actualPromise, expectedValue) { + var self = this; + + function prefix(passed) { + return 'Expected a promise ' + + (passed ? 'not ' : '') + + 'to be resolved to ' + j$.pp(expectedValue); + } + + return actualPromise.then( + function(actualValue) { + if (self.util.equals(actualValue, expectedValue, self.customEqualityTesters)) { + return { + pass: true, + message: prefix(true) + '.' + }; + } else { + return { + pass: false, + message: prefix(false) + ' but it was resolved to ' + j$.pp(actualValue) + '.' + }; + } + }, + function() { + return { + pass: false, + message: prefix(false) + ' but it was rejected.' + }; + } + ); + }; + + /** + * Expect a promise to be rejected to a value equal to the expected, using deep equality comparison. + * @function + * @async + * @name async-matchers#toBeRejectedWith + * @param {Object} expected - Value that the promise is expected to reject to + * @example + * await expectAsync(aPromise).toBeRejectedWith({prop: 'value'}); + * @example + * return expectAsync(aPromise).toBeRejectedWith({prop: 'value'}); + */ + AsyncExpectation.prototype.toBeRejectedWith = function(actualPromise, expectedValue) { + var self = this; + + function prefix(passed) { + return 'Expected a promise ' + + (passed ? 'not ' : '') + + 'to be rejected with ' + j$.pp(expectedValue); + } + + return actualPromise.then( + function() { + return { + pass: false, + message: prefix(false) + ' but it was resolved.' + }; + }, + function(actualValue) { + if (self.util.equals(actualValue, expectedValue, self.customEqualityTesters)) { + return { + pass: true, + message: prefix(true) + '.' + }; + } else { + return { + pass: false, + message: prefix(false) + ' but it was rejected with ' + j$.pp(actualValue) + '.' + }; + } + } + ); + }; + + AsyncExpectation.prototype.addFilter = function(filter) { + var result = Object.create(this); + result.filters = this.filters.addFilter(filter); + return result; + }; + + AsyncExpectation.factory = function(options) { + var expect = new AsyncExpectation(options); + expect.not = expect.addFilter(negatingFilter); + + return expect; + }; + + var negatingFilter = { + selectComparisonFunc: function(matcher) { + function defaultNegativeCompare() { + return matcher.apply(this, arguments).then(function(result) { + result.pass = !result.pass; + return result; + }); + } + + return defaultNegativeCompare; + }, + buildFailureMessage: function(result, matcherName, args, util) { + if (result.message) { + if (j$.isFunction_(result.message)) { + return result.message(); + } else { + return result.message; + } + } + + args = args.slice(); + args.unshift(true); + args.unshift(matcherName); + return util.buildFailureMessage.apply(null, args); + } + }; + + + return AsyncExpectation; +}; + getJasmineRequireObj().CallTracker = function(j$) { /** @@ -2499,6 +2931,8 @@ getJasmineRequireObj().ExceptionFormatter = function(j$) { if (error.name && error.message) { message += error.name + ': ' + error.message; + } else if (error.message) { + message += error.message; } else { message += error.toString() + ' thrown'; } @@ -2576,7 +3010,7 @@ getJasmineRequireObj().ExceptionFormatter = function(j$) { return ExceptionFormatter; }; -getJasmineRequireObj().Expectation = function() { +getJasmineRequireObj().Expectation = function(j$) { /** * Matchers that come with Jasmine out of the box. @@ -2587,7 +3021,7 @@ getJasmineRequireObj().Expectation = function() { this.customEqualityTesters = options.customEqualityTesters || []; this.actual = options.actual; this.addExpectationResult = options.addExpectationResult || function(){}; - this.isNot = options.isNot; + this.filters = new j$.ExpectationFilterChain(); var customMatchers = options.customMatchers || {}; for (var matcherName in customMatchers) { @@ -2610,18 +3044,14 @@ getJasmineRequireObj().Expectation = function() { Expectation.prototype.instantiateMatcher = function(matcherFactory) { var matcher = matcherFactory(this.util, this.customEqualityTesters); - - if (this.filter && this.filter.selectComparisonFunc) { - return this.filter.selectComparisonFunc(matcher); - } - - return matcher.compare; + var comparisonFunc = this.filters.selectComparisonFunc(matcher); + return comparisonFunc || matcher.compare; }; Expectation.prototype.processResult = function(result, name, expected, args) { var message = this.buildMessage(result, name, args); - if (expected.length == 1) { + if (expected.length === 1) { expected = expected[0]; } @@ -2640,20 +3070,33 @@ getJasmineRequireObj().Expectation = function() { }; Expectation.prototype.buildMessage = function(result, name, args) { + var util = this.util; + if (result.pass) { return ''; - } else if (this.filter && this.filter.buildFailureMessage) { - return this.filter.buildFailureMessage(result, name, args, this.util); - } else if (!result.message) { - args = args.slice(); - args.unshift(false); - args.unshift(name); - return this.util.buildFailureMessage.apply(null, args); - } else if (Object.prototype.toString.apply(result.message) === '[object Function]') { - return result.message(); - } else { - return result.message; } + + var msg = this.filters.buildFailureMessage(result, name, args, util, defaultMessage); + return this.filters.modifyFailureMessage(msg || defaultMessage()); + + function defaultMessage() { + if (!result.message) { + args = args.slice(); + args.unshift(false); + args.unshift(name); + return util.buildFailureMessage.apply(null, args); + } else if (j$.isFunction_(result.message)) { + return result.message(); + } else { + return result.message; + } + } + }; + + Expectation.prototype.addFilter = function(filter) { + var result = Object.create(this); + result.filters = this.filters.addFilter(filter); + return result; }; Expectation.addCoreMatchers = function(matchers) { @@ -2665,19 +3108,19 @@ getJasmineRequireObj().Expectation = function() { }; Expectation.Factory = function(options) { - options = options || {}; + var expect = new Expectation(options || {}); + expect.not = expect.addFilter(negatingFilter); - var expect = new Expectation(options); - - // TODO: this would be nice as its own Object - NegativeExpectation - // TODO: copy instead of mutate options - options.isNot = true; - expect.not = new Expectation(options); - expect.not.filter = negatingFilter; + expect.withContext = function(message) { + var result = this.addFilter(new ContextAddingFilter(message)); + result.not = result.addFilter(negatingFilter); + return result; + }; return expect; }; + var negatingFilter = { selectComparisonFunc: function(matcher) { function defaultNegativeCompare() { @@ -2690,7 +3133,11 @@ getJasmineRequireObj().Expectation = function() { }, buildFailureMessage: function(result, matcherName, args, util) { if (result.message) { - return result.message; + if (j$.isFunction_(result.message)) { + return result.message(); + } else { + return result.message; + } } args = args.slice(); @@ -2700,9 +3147,65 @@ getJasmineRequireObj().Expectation = function() { } }; + + function ContextAddingFilter(message) { + this.message = message; + } + + ContextAddingFilter.prototype.modifyFailureMessage = function(msg) { + return this.message + ': ' + msg; + }; + return Expectation; }; +getJasmineRequireObj().ExpectationFilterChain = function() { + function ExpectationFilterChain(maybeFilter, prev) { + this.filter_ = maybeFilter; + this.prev_ = prev; + } + + ExpectationFilterChain.prototype.addFilter = function(filter) { + return new ExpectationFilterChain(filter, this); + }; + + ExpectationFilterChain.prototype.selectComparisonFunc = function(matcher) { + return this.callFirst_('selectComparisonFunc', arguments).result; + }; + + ExpectationFilterChain.prototype.buildFailureMessage = function(result, matcherName, args, util) { + return this.callFirst_('buildFailureMessage', arguments).result; + }; + + ExpectationFilterChain.prototype.modifyFailureMessage = function(msg) { + var result = this.callFirst_('modifyFailureMessage', arguments).result; + return result || msg; + }; + + ExpectationFilterChain.prototype.callFirst_ = function(fname, args) { + var prevResult; + + if (this.prev_) { + prevResult = this.prev_.callFirst_(fname, args); + + if (prevResult.found) { + return prevResult; + } + } + + if (this.filter_ && this.filter_[fname]) { + return { + found: true, + result: this.filter_[fname].apply(this.filter_, args) + }; + } + + return {found: false}; + }; + + return ExpectationFilterChain; +}; + //TODO: expectation result may make more sense as a presentation of an expectation. getJasmineRequireObj().buildExpectationResult = function() { function buildExpectationResult(options) { @@ -2750,7 +3253,9 @@ getJasmineRequireObj().buildExpectationResult = function() { var error = options.error; if (!error) { - if (options.stack) { + if (options.errorForStack) { + error = options.errorForStack; + } else if (options.stack) { error = options; } else { try { @@ -3429,7 +3934,7 @@ getJasmineRequireObj().ObjectPath = function(j$) { return ObjectPath; }; -getJasmineRequireObj().toBe = function() { +getJasmineRequireObj().toBe = function(j$) { /** * {@link expect} the actual value to be `===` to the expected value. * @function @@ -3438,12 +3943,20 @@ getJasmineRequireObj().toBe = function() { * @example * expect(thing).toBe(realThing); */ - function toBe() { + function toBe(util) { + var tip = ' Tip: To check for deep equality, use .toEqual() instead of .toBe().'; + return { compare: function(actual, expected) { - return { - pass: actual === expected + var result = { + pass: actual === expected, }; + + if (typeof expected === 'object') { + result.message = util.buildFailureMessage('toBe', result.pass, actual, expected) + tip; + } + + return result; } }; } @@ -4778,26 +5291,29 @@ getJasmineRequireObj().QueueRunner = function(j$) { function once(fn) { var called = false; - return function() { + return function(arg) { if (!called) { called = true; - fn.apply(null, arguments); + // Direct call using single parameter, because cleanup/next does not need more + fn(arg); } return null; }; } + function emptyFn() {} + function QueueRunner(attrs) { var queueableFns = attrs.queueableFns || []; this.queueableFns = queueableFns.concat(attrs.cleanupFns || []); this.firstCleanupIx = queueableFns.length; - this.onComplete = attrs.onComplete || function() {}; + this.onComplete = attrs.onComplete || emptyFn; this.clearStack = attrs.clearStack || function(fn) {fn();}; - this.onException = attrs.onException || function() {}; + this.onException = attrs.onException || emptyFn; this.userContext = attrs.userContext || new j$.UserContext(); this.timeout = attrs.timeout || {setTimeout: setTimeout, clearTimeout: clearTimeout}; - this.fail = attrs.fail || function() {}; - this.globalErrors = attrs.globalErrors || { pushListener: function() {}, popListener: function() {} }; + this.fail = attrs.fail || emptyFn; + this.globalErrors = attrs.globalErrors || { pushListener: emptyFn, popListener: emptyFn }; this.completeOnFirstError = !!attrs.completeOnFirstError; this.errored = false; @@ -4839,7 +5355,9 @@ getJasmineRequireObj().QueueRunner = function(j$) { next(error); }, cleanup = once(function cleanup() { - self.clearTimeout(timeoutId); + if (timeoutId !== void 0) { + self.clearTimeout(timeoutId); + } self.globalErrors.popListener(handleError); }), next = once(function next(err) { @@ -4878,12 +5396,16 @@ getJasmineRequireObj().QueueRunner = function(j$) { self.globalErrors.pushListener(handleError); - if (queueableFn.timeout) { + if (queueableFn.timeout !== undefined) { + var timeoutInterval = queueableFn.timeout || j$.DEFAULT_TIMEOUT_INTERVAL; timeoutId = self.setTimeout(function() { - var error = new Error('Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.'); + var error = new Error( + 'Timeout - Async callback was not invoked within ' + timeoutInterval + 'ms ' + + (queueableFn.timeout ? '(custom timeout)' : '(set by jasmine.DEFAULT_TIMEOUT_INTERVAL)') + ); onException(error); next(); - }, queueableFn.timeout()); + }, timeoutInterval); } try { @@ -4932,7 +5454,7 @@ getJasmineRequireObj().QueueRunner = function(j$) { return; } - self.errored = result.errored; + self.errored = self.errored || result.errored; if (this.completeOnFirstError && result.errored) { this.skipToCleanup(iterativeIndex); @@ -5196,6 +5718,25 @@ getJasmineRequireObj().interface = function(jasmine, env) { return env.expect(actual); }, + /** + * Create an asynchronous expectation for a spec. Note that the matchers + * that are provided by an asynchronous expectation all return promises + * which must be either returned from the spec or waited for using `await` + * in order for Jasmine to associate them with the correct spec. + * @name expectAsync + * @function + * @global + * @param {Object} actual - Actual computed value to test expectations against. + * @return {async-matchers} + * @example + * await expectAsync(somePromise).toBeResolved(); + * @example + * return expectAsync(somePromise).toBeResolved(); + */ + expectAsync: function(actual) { + return env.expectAsync(actual); + }, + /** * Mark a spec as pending, expectation results will be ignored. * @name pending @@ -5245,6 +5786,18 @@ getJasmineRequireObj().interface = function(jasmine, env) { return env.spyOnProperty(obj, methodName, accessType); }, + /** + * Installs spies on all writable and configurable properties of an object. + * @name spyOnAllFunctions + * @function + * @global + * @param {Object} obj - The object upon which to install the {@link Spy}s + * @returns {Object} the spied object + */ + spyOnAllFunctions: function(obj) { + return env.spyOnAllFunctions(obj); + }, + jsApiReporter: new jasmine.JsApiReporter({ timer: new jasmine.Timer() }), @@ -5663,6 +6216,23 @@ getJasmineRequireObj().SpyRegistry = function(j$) { return spy; }; + this.spyOnAllFunctions = function(obj) { + if (j$.util.isUndefined(obj)) { + throw new Error('spyOnAllFunctions could not find an object to spy upon'); + } + + for (var prop in obj) { + if (Object.prototype.hasOwnProperty.call(obj, prop) && obj[prop] instanceof Function) { + var descriptor = Object.getOwnPropertyDescriptor(obj, prop); + if ((descriptor.writable || descriptor.set) && descriptor.configurable) { + this.spyOn(obj, prop); + } + } + } + + return obj; + }; + this.clearSpies = function() { var spies = currentSpies(); for (var i = spies.length - 1; i >= 0; i--) { @@ -5923,6 +6493,7 @@ getJasmineRequireObj().Suite = function(j$) { this.parentSuite = attrs.parentSuite; this.description = attrs.description; this.expectationFactory = attrs.expectationFactory; + this.asyncExpectationFactory = attrs.asyncExpectationFactory; this.expectationResultFactory = attrs.expectationResultFactory; this.throwOnExpectationFailure = !!attrs.throwOnExpectationFailure; @@ -5955,6 +6526,10 @@ getJasmineRequireObj().Suite = function(j$) { return this.expectationFactory(actual, this); }; + Suite.prototype.expectAsync = function(actual) { + return this.asyncExpectationFactory(actual, this); + }; + Suite.prototype.getFullName = function() { var fullName = []; for (var parentSuite = this; parentSuite; parentSuite = parentSuite.parentSuite) { @@ -6284,8 +6859,11 @@ getJasmineRequireObj().TreeProcessor = function() { queueRunnerFactory({ onComplete: function () { + var args = Array.prototype.slice.call(arguments, [0]); node.cleanupBeforeAfter(); - nodeComplete(node, node.getResult(), done); + nodeComplete(node, node.getResult(), function() { + done.apply(undefined, args); + }); }, queueableFns: [onStart].concat(wrapChildren(node, segmentNumber)), userContext: node.sharedUserContext(), @@ -6341,5 +6919,5 @@ getJasmineRequireObj().UserContext = function(j$) { }; getJasmineRequireObj().version = function() { - return '3.1.0'; + return '3.2.1'; }; diff --git a/lib/jasmine-core/version.rb b/lib/jasmine-core/version.rb index 54d46883..87b6585b 100644 --- a/lib/jasmine-core/version.rb +++ b/lib/jasmine-core/version.rb @@ -4,6 +4,6 @@ # module Jasmine module Core - VERSION = "3.1.0" + VERSION = "3.2.1" end end diff --git a/package.json b/package.json index 9000a56e..f6854cd0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "jasmine-core", "license": "MIT", - "version": "3.1.0", + "version": "3.2.1", "repository": { "type": "git", "url": "https://github.com/jasmine/jasmine.git" @@ -16,7 +16,7 @@ "test": "grunt jshint execSpecsInNode" }, "description": "Official packaging of Jasmine's core files for use by Node.js projects.", - "homepage": "http://jasmine.github.io", + "homepage": "https://jasmine.github.io", "main": "./lib/jasmine-core.js", "devDependencies": { "glob": "~7.1.2", diff --git a/release_notes/3.2.0.md b/release_notes/3.2.0.md new file mode 100644 index 00000000..6b46ce5f --- /dev/null +++ b/release_notes/3.2.0.md @@ -0,0 +1,83 @@ +# Jasmine-Core 3.2 Release Notes + +## Summary + +This release contains a number of fixes and pull requests + +## Changes + +* Add spyOnAllFunctions function + - Merges [#1581](https://github.com/jasmine/jasmine/issues/1581) from @aeisenberg + - Fixes [#1421](https://github.com/jasmine/jasmine/issues/1421) + + +* Improve timeout error message + - Merges [#1567](https://github.com/jasmine/jasmine/issues/1567) from @ikonst + + +* Fix JSDoc naming for Env functions + - See [#1565](https://github.com/jasmine/jasmine/issues/1565) + + +* Add documentation for more public functions on Env + - Fixes [#1565](https://github.com/jasmine/jasmine/issues/1565) + + +* Added a basic set of async matchers + - Fixes [#1447](https://github.com/jasmine/jasmine/issues/1447) + - Fixes [#1547](https://github.com/jasmine/jasmine/issues/1547) + + +* Properly cascade StopExecutionError's up the tree + - Fixes [#1563](https://github.com/jasmine/jasmine/issues/1563) + + +* Implemented hiding of disabled specs + - Merges [#1561](https://github.com/jasmine/jasmine/issues/1561) from @SamFare + + +* Line-break long expectation failure messages + - See [#296](https://github.com/jasmine/jasmine/issues/296) + + +* Better detection of DOM Nodes for equality + - Fixes [#1172](https://github.com/jasmine/jasmine/issues/1172) + + +* Fix typo from `incimplete` to `incomplete` + - Merges [#1555](https://github.com/jasmine/jasmine/issues/1555) from @yinm + + +* Allow omitting the name argument: `createSpy(func)` + - Merges [#1551](https://github.com/jasmine/jasmine/issues/1551) from @riophae + + +* name new global status stuff correctly in API docs + + +* Check for accidental global variable creation + + +* Fixed global variable leak + - Fixes [#1534](https://github.com/jasmine/jasmine/issues/1534) + + +* Correctly format stack traces for errors with multiline messages + - Fixes [#1526](https://github.com/jasmine/jasmine/issues/1526) + + +* Change message for extra elements at end of actual array + - Merges [#1527](https://github.com/jasmine/jasmine/issues/1527) from @majidmade + - Fixes [#1485](https://github.com/jasmine/jasmine/issues/1485) + + +* Report unhandled rejections as globalErrors. + - Merges [#1521](https://github.com/jasmine/jasmine/issues/1521) from @johnjbarton + + +* add some links to more tutorials from the api docs + + +------ + +_Release Notes generated with _[Anchorman](http://github.com/infews/anchorman)_ diff --git a/release_notes/3.2.1.md b/release_notes/3.2.1.md new file mode 100644 index 00000000..ee25ef46 --- /dev/null +++ b/release_notes/3.2.1.md @@ -0,0 +1,11 @@ +# Jasmine-Core 3.2.1 Release Notes + +## Changes + +* Correctly expose `spyOnAllFunctions` + - See [#1581](https://github.com/jasmine/jasmine/issues/1581) + + +------ + +_Release Notes generated with _[Anchorman](http://github.com/infews/anchorman)_ diff --git a/spec/core/AsyncExpectationSpec.js b/spec/core/AsyncExpectationSpec.js new file mode 100644 index 00000000..7aefa191 --- /dev/null +++ b/spec/core/AsyncExpectationSpec.js @@ -0,0 +1,397 @@ +describe('AsyncExpectation', function() { + describe('Factory', function() { + it('throws an Error if promises are not available', function() { + var thenable = {then: function() {}}, + options = {global: {}, actual: thenable} + function f() { jasmineUnderTest.AsyncExpectation.factory(options); } + expect(f).toThrowError('expectAsync is unavailable because the environment does not support promises.'); + }); + + it('throws an Error if the argument is not a promise', function() { + jasmine.getEnv().requirePromises(); + function f() { + jasmineUnderTest.AsyncExpectation.factory({actual: 'not a promise'}); + } + expect(f).toThrowError('Expected expectAsync to be called with a promise.'); + }); + }); + + describe('#toBeResolved', function() { + it('passes if the actual is resolved', function() { + jasmine.getEnv().requirePromises(); + var addExpectationResult = jasmine.createSpy('addExpectationResult'), + actual = Promise.resolve(), + expectation = new jasmineUnderTest.AsyncExpectation({ + util: jasmineUnderTest.matchersUtil, + actual: actual, + addExpectationResult: addExpectationResult + }); + + return expectation.toBeResolved().then(function() { + expect(addExpectationResult).toHaveBeenCalledWith(true, { + matcherName: 'toBeResolved', + passed: true, + message: '', + error: undefined, + errorForStack: jasmine.any(Error), + actual: actual + }); + }); + }); + + it('fails if the actual is rejected', function() { + jasmine.getEnv().requirePromises(); + var addExpectationResult = jasmine.createSpy('addExpectationResult'), + actual = Promise.reject('AsyncExpectationSpec rejection'), + expectation = new jasmineUnderTest.AsyncExpectation({ + util: jasmineUnderTest.matchersUtil, + actual: actual, + addExpectationResult: addExpectationResult + }); + + return expectation.toBeResolved().then(function() { + expect(addExpectationResult).toHaveBeenCalledWith(false, { + matcherName: 'toBeResolved', + passed: false, + message: 'Expected a promise to be resolved.', + error: undefined, + errorForStack: jasmine.any(Error), + actual: actual + }); + }); + }); + }); + + describe('#toBeRejected', function() { + it('passes if the actual is rejected', function() { + jasmine.getEnv().requirePromises(); + var addExpectationResult = jasmine.createSpy('addExpectationResult'), + actual = Promise.reject('AsyncExpectationSpec rejection'), + expectation = new jasmineUnderTest.AsyncExpectation({ + util: jasmineUnderTest.matchersUtil, + actual: actual, + addExpectationResult: addExpectationResult + }); + + return expectation.toBeRejected().then(function() { + expect(addExpectationResult).toHaveBeenCalledWith(true, { + matcherName: 'toBeRejected', + passed: true, + message: '', + error: undefined, + errorForStack: jasmine.any(Error), + actual: actual + }); + }); + }); + + it('fails if the actual is resolved', function() { + jasmine.getEnv().requirePromises(); + var addExpectationResult = jasmine.createSpy('addExpectationResult'), + actual = Promise.resolve(), + expectation = new jasmineUnderTest.AsyncExpectation({ + util: jasmineUnderTest.matchersUtil, + actual: actual, + addExpectationResult: addExpectationResult + }); + + return expectation.toBeRejected().then(function() { + expect(addExpectationResult).toHaveBeenCalledWith(false, { + matcherName: 'toBeRejected', + passed: false, + message: 'Expected a promise to be rejected.', + error: undefined, + errorForStack: jasmine.any(Error), + actual: actual + }); + }); + }); + }); + + describe('#toBeRejectedWith', function () { + it('should return true if the promise is rejected with the expected value', function () { + jasmine.getEnv().requirePromises(); + + var actual = Promise.reject({error: 'PEBCAK'}); + var addExpectationResult = jasmine.createSpy('addExpectationResult'), + expectation = new jasmineUnderTest.AsyncExpectation({ + util: jasmineUnderTest.matchersUtil, + actual: actual, + addExpectationResult: addExpectationResult + }); + + return expectation.toBeRejectedWith({error: 'PEBCAK'}).then(function () { + expect(addExpectationResult).toHaveBeenCalledWith(true, { + matcherName: 'toBeRejectedWith', + passed: true, + message: '', + error: undefined, + errorForStack: jasmine.any(Error), + actual: actual + }); + }); + + }); + + it('should fail if the promise resolves', function () { + jasmine.getEnv().requirePromises(); + + var actual = Promise.resolve('AsyncExpectation error'); + var addExpectationResult = jasmine.createSpy('addExpectationResult'), + expectation = new jasmineUnderTest.AsyncExpectation({ + util: jasmineUnderTest.matchersUtil, + actual: actual, + addExpectationResult: addExpectationResult + }); + + return expectation.toBeRejectedWith('').then(function () { + expect(addExpectationResult).toHaveBeenCalledWith(false, { + matcherName: 'toBeRejectedWith', + passed: false, + message: "Expected a promise to be rejected with '' but it was resolved.", + error: undefined, + errorForStack: jasmine.any(Error), + actual: actual + }); + }); + }); + + it('should fail if the promise is rejected with a different value', function () { + jasmine.getEnv().requirePromises(); + + var actual = Promise.reject('A Bad Apple'); + var addExpectationResult = jasmine.createSpy('addExpectationResult'), + expectation = new jasmineUnderTest.AsyncExpectation({ + util: jasmineUnderTest.matchersUtil, + actual: actual, + addExpectationResult: addExpectationResult + }); + + return expectation.toBeRejectedWith('Some Cool Thing').then(function () { + expect(addExpectationResult).toHaveBeenCalledWith(false, { + matcherName: 'toBeRejectedWith', + passed: false, + message: "Expected a promise to be rejected with 'Some Cool Thing' but it was rejected with 'A Bad Apple'.", + error: undefined, + errorForStack: jasmine.any(Error), + actual: actual + }); + }); + }); + + it('should build its error correctly when negated', function () { + jasmine.getEnv().requirePromises(); + + var addExpectationResult = jasmine.createSpy('addExpectationResult'), + expectation = jasmineUnderTest.AsyncExpectation.factory({ + util: jasmineUnderTest.matchersUtil, + actual: Promise.reject(true), + addExpectationResult: addExpectationResult + }); + + return expectation.not.toBeRejectedWith(true).then(function () { + expect(addExpectationResult).toHaveBeenCalledWith(false, + jasmine.objectContaining({ + passed: false, + message: 'Expected a promise not to be rejected with true.' + }) + ); + }); + }); + + it('should support custom equality testers', function () { + jasmine.getEnv().requirePromises(); + + var addExpectationResult = jasmine.createSpy('addExpectationResult'), + expectation = new jasmineUnderTest.AsyncExpectation({ + util: jasmineUnderTest.matchersUtil, + customEqualityTesters: [function() { return true; }], + actual: Promise.reject('actual'), + addExpectationResult: addExpectationResult + }); + + return expectation.toBeRejectedWith('expected').then(function() { + expect(addExpectationResult).toHaveBeenCalledWith(true, + jasmine.objectContaining({passed: true})); + }); + }); + }); + + describe('#toBeResolvedTo', function() { + it('passes if the promise is resolved to the expected value', function() { + jasmine.getEnv().requirePromises(); + + var actual = Promise.resolve({foo: 42}); + var addExpectationResult = jasmine.createSpy('addExpectationResult'), + expectation = new jasmineUnderTest.AsyncExpectation({ + util: jasmineUnderTest.matchersUtil, + actual: actual, + addExpectationResult: addExpectationResult + }); + + return expectation.toBeResolvedTo({foo: 42}).then(function() { + expect(addExpectationResult).toHaveBeenCalledWith(true, { + matcherName: 'toBeResolvedTo', + passed: true, + message: '', + error: undefined, + errorForStack: jasmine.any(Error), + actual: actual + }); + }); + }); + + it('fails if the promise is rejected', function() { + jasmine.getEnv().requirePromises(); + + var actual = Promise.reject('AsyncExpectationSpec error'); + var addExpectationResult = jasmine.createSpy('addExpectationResult'), + expectation = new jasmineUnderTest.AsyncExpectation({ + util: jasmineUnderTest.matchersUtil, + actual: actual, + addExpectationResult: addExpectationResult + }); + + return expectation.toBeResolvedTo('').then(function() { + expect(addExpectationResult).toHaveBeenCalledWith(false, { + matcherName: 'toBeResolvedTo', + passed: false, + message: "Expected a promise to be resolved to '' but it was rejected.", + error: undefined, + errorForStack: jasmine.any(Error), + actual: actual + }); + }); + }); + + it('fails if the promise is resolved to a different value', function() { + jasmine.getEnv().requirePromises(); + + var actual = Promise.resolve({foo: 17}); + var addExpectationResult = jasmine.createSpy('addExpectationResult'), + expectation = new jasmineUnderTest.AsyncExpectation({ + util: jasmineUnderTest.matchersUtil, + actual: actual, + addExpectationResult: addExpectationResult + }); + + return expectation.toBeResolvedTo({foo: 42}).then(function() { + expect(addExpectationResult).toHaveBeenCalledWith(false, { + matcherName: 'toBeResolvedTo', + passed: false, + message: 'Expected a promise to be resolved to Object({ foo: 42 }) but it was resolved to Object({ foo: 17 }).', + error: undefined, + errorForStack: jasmine.any(Error), + actual: actual + }); + }); + }); + + it('builds its message correctly when negated', function() { + jasmine.getEnv().requirePromises(); + + var addExpectationResult = jasmine.createSpy('addExpectationResult'), + expectation = jasmineUnderTest.AsyncExpectation.factory({ + util: jasmineUnderTest.matchersUtil, + actual: Promise.resolve(true), + addExpectationResult: addExpectationResult + }); + + return expectation.not.toBeResolvedTo(true).then(function() { + expect(addExpectationResult).toHaveBeenCalledWith(false, + jasmine.objectContaining({ + passed: false, + message: 'Expected a promise not to be resolved to true.' + }) + ); + }); + }); + + it('supports custom equality testers', function() { + jasmine.getEnv().requirePromises(); + + var addExpectationResult = jasmine.createSpy('addExpectationResult'), + expectation = new jasmineUnderTest.AsyncExpectation({ + util: jasmineUnderTest.matchersUtil, + customEqualityTesters: [function() { return true; }], + actual: Promise.resolve('actual'), + addExpectationResult: addExpectationResult + }); + + return expectation.toBeResolvedTo('expected').then(function() { + expect(addExpectationResult).toHaveBeenCalledWith(true, + jasmine.objectContaining({passed: true})); + }); + }); + }); + + describe('#not', function() { + it('converts a pass to a fail', function() { + jasmine.getEnv().requirePromises(); + + var addExpectationResult = jasmine.createSpy('addExpectationResult'), + actual = Promise.resolve(), + expectation = jasmineUnderTest.AsyncExpectation.factory({ + util: jasmineUnderTest.matchersUtil, + actual: actual, + addExpectationResult: addExpectationResult + }); + + return expectation.not.toBeResolved().then(function() { + expect(addExpectationResult).toHaveBeenCalledWith(false, + jasmine.objectContaining({ + passed: false, + message: 'Expected a promise not to be resolved.' + }) + ); + }); + }); + + it('converts a fail to a pass', function() { + jasmine.getEnv().requirePromises(); + + var addExpectationResult = jasmine.createSpy('addExpectationResult'), + actual = Promise.reject(), + expectation = jasmineUnderTest.AsyncExpectation.factory({ + util: jasmineUnderTest.matchersUtil, + actual: actual, + addExpectationResult: addExpectationResult + }); + + return expectation.not.toBeResolved().then(function() { + expect(addExpectationResult).toHaveBeenCalledWith(true, + jasmine.objectContaining({ + passed: true, + message: '' + }) + ); + }); + }); + }); + + it('propagates rejections from the comparison function', function() { + jasmine.getEnv().requirePromises(); + var error = new Error('AsyncExpectationSpec failure'); + + spyOn(jasmineUnderTest.AsyncExpectation.prototype, 'toBeResolved') + .and.returnValue(Promise.reject(error)); + + var addExpectationResult = jasmine.createSpy('addExpectationResult'), + actual = dummyPromise(), + expectation = new jasmineUnderTest.AsyncExpectation({ + actual: actual, + addExpectationResult: addExpectationResult + }); + + return expectation.toBeResolved() + .then( + function() { fail('Expected a rejection'); }, + function(e) { expect(e).toBe(error); } + ); + }); + + function dummyPromise() { + return new Promise(function(resolve, reject) { + }); + } +}); diff --git a/spec/core/EnvSpec.js b/spec/core/EnvSpec.js index 5c9118c8..f731d36b 100644 --- a/spec/core/EnvSpec.js +++ b/spec/core/EnvSpec.js @@ -27,7 +27,7 @@ describe("Env", function() { }); it('can configure specs to throw errors on expectation failures', function() { - env.throwOnExpectationFailure(true); + env.configure({oneFailurePerSpec: true}); spyOn(jasmineUnderTest, 'Spec'); env.it('foo', function() {}); @@ -37,7 +37,7 @@ describe("Env", function() { }); it('can configure suites to throw errors on expectation failures', function() { - env.throwOnExpectationFailure(true); + env.configure({oneFailurePerSpec: true}); spyOn(jasmineUnderTest, 'Suite'); env.describe('foo', function() {}); @@ -46,6 +46,22 @@ describe("Env", function() { })); }); + it('defaults to multiple failures for specs', function() { + spyOn(jasmineUnderTest, 'Spec'); + env.it('bar', function() {}); + expect(jasmineUnderTest.Spec).toHaveBeenCalledWith(jasmine.objectContaining({ + throwOnExpectationFailure: false + })); + }); + + it('defaults to multiple failures for suites', function() { + spyOn(jasmineUnderTest, 'Suite'); + env.describe('foo', function() {}); + expect(jasmineUnderTest.Suite).toHaveBeenCalledWith(jasmine.objectContaining({ + throwOnExpectationFailure: false + })); + }); + describe('#describe', function () { it("throws an error when given arguments", function() { expect(function() { diff --git a/spec/core/ExceptionFormatterSpec.js b/spec/core/ExceptionFormatterSpec.js index 95a476b0..9cc3638b 100644 --- a/spec/core/ExceptionFormatterSpec.js +++ b/spec/core/ExceptionFormatterSpec.js @@ -37,6 +37,28 @@ describe("ExceptionFormatter", function() { expect(message).toEqual('A Classic Mistake: you got your foo in my bar'); }); + it('formats unnamed exceptions with message', function() { + var unnamedError = {message: 'This is an unnamed error message.'}; + + var exceptionFormatter = new jasmineUnderTest.ExceptionFormatter(), + message = exceptionFormatter.message(unnamedError); + + expect(message).toEqual('This is an unnamed error message.'); + }); + + it('formats empty exceptions with toString format', function() { + var EmptyError = function() {}; + EmptyError.prototype.toString = function() { + return '[EmptyError]'; + }; + var emptyError = new EmptyError(); + + var exceptionFormatter = new jasmineUnderTest.ExceptionFormatter(), + message = exceptionFormatter.message(emptyError); + + expect(message).toEqual('[EmptyError] thrown'); + }); + it("formats thrown exceptions that aren't errors", function() { var thrown = "crazy error", exceptionFormatter = new jasmineUnderTest.ExceptionFormatter(), diff --git a/spec/core/ExpectationResultSpec.js b/spec/core/ExpectationResultSpec.js index 41785287..53d2c7e3 100644 --- a/spec/core/ExpectationResultSpec.js +++ b/spec/core/ExpectationResultSpec.js @@ -44,6 +44,21 @@ describe("buildExpectationResult", function() { expect(result.stack).toEqual('foo'); }); + it("delegates stack formatting to the provided formatter if there was a provided errorForStack", function() { + var fakeError = {stack: 'foo'}, + stackFormatter = jasmine.createSpy("stack formatter").and.returnValue(fakeError.stack); + + var result = jasmineUnderTest.buildExpectationResult( + { + passed: false, + errorForStack: fakeError, + stackFormatter: stackFormatter + }); + + expect(stackFormatter).toHaveBeenCalledWith(fakeError); + expect(result.stack).toEqual('foo'); + }); + it("matcherName returns passed matcherName", function() { var result = jasmineUnderTest.buildExpectationResult({matcherName: 'some-value'}); expect(result.matcherName).toBe('some-value'); diff --git a/spec/core/ExpectationSpec.js b/spec/core/ExpectationSpec.js index 78088013..e0471c08 100644 --- a/spec/core/ExpectationSpec.js +++ b/spec/core/ExpectationSpec.js @@ -33,7 +33,9 @@ describe("Expectation", function() { matchers = { toFoo: matcherFactory }, - util = {}, + util = { + buildFailureMessage: jasmine.createSpy('buildFailureMessage') + }, customEqualityTesters = ['a'], addExpectationResult = jasmine.createSpy("addExpectationResult"), expectation; @@ -380,7 +382,7 @@ describe("Expectation", function() { error: undefined }); }); - + it("reports a custom error message to the spec", function() { var customError = new Error("I am a custom error"); var matchers = { @@ -508,9 +510,9 @@ describe("Expectation", function() { actual: "an actual", addExpectationResult: addExpectationResult }); - + expectation.withContext("Some context").toFoo("hello"); - + expect(addExpectationResult).toHaveBeenCalledWith(false, jasmine.objectContaining({ message: "Some context: failure message" @@ -532,9 +534,9 @@ describe("Expectation", function() { actual: "an actual", addExpectationResult: addExpectationResult }); - + expectation.withContext("Some context").toFoo("hello"); - + expect(addExpectationResult).toHaveBeenCalledWith(false, jasmine.objectContaining({ message: "Some context: msg" @@ -561,9 +563,9 @@ describe("Expectation", function() { actual: "an actual", addExpectationResult: addExpectationResult }); - + expectation.withContext("Some context").toFoo("hello"); - + expect(addExpectationResult).toHaveBeenCalledWith(false, jasmine.objectContaining({ message: "Some context: msg" @@ -586,9 +588,9 @@ describe("Expectation", function() { actual: "an actual", addExpectationResult: addExpectationResult }); - + expectation.withContext("Some context").not.toFoo(); - + expect(addExpectationResult).toHaveBeenCalledWith(false, jasmine.objectContaining({ message: "Some context: Expected 'an actual' not to foo." @@ -617,10 +619,10 @@ describe("Expectation", function() { customMatchers: matchers, addExpectationResult: addExpectationResult }); - + expectation.withContext("Some context").not.toFoo("hello"); - - expect(addExpectationResult).toHaveBeenCalledWith(false, + + expect(addExpectationResult).toHaveBeenCalledWith(false, jasmine.objectContaining({ message: "Some context: I am a custom message", }) diff --git a/spec/core/QueueRunnerSpec.js b/spec/core/QueueRunnerSpec.js index 0e57ae96..632565ba 100644 --- a/spec/core/QueueRunnerSpec.js +++ b/spec/core/QueueRunnerSpec.js @@ -183,7 +183,7 @@ describe("QueueRunner", function() { it("sets a timeout if requested for asynchronous functions so they don't go on forever", function() { var timeout = 3, - beforeFn = { fn: function(done) { }, type: 'before', timeout: function() { return timeout; } }, + beforeFn = { fn: function(done) { }, type: 'before', timeout: timeout }, queueableFn = { fn: jasmine.createSpy('fn'), type: 'queueable' }, onComplete = jasmine.createSpy('onComplete'), onException = jasmine.createSpy('onException'), @@ -304,7 +304,7 @@ describe("QueueRunner", function() { }); it("continues running functions when an exception is thrown in async code without timing out", function() { - var queueableFn = { fn: function(done) { throwAsync(); }, timeout: function() { return 1; } }, + var queueableFn = { fn: function(done) { throwAsync(); }, timeout: 1 }, nextQueueableFn = { fn: jasmine.createSpy("nextFunction") }, onException = jasmine.createSpy('onException'), globalErrors = { pushListener: jasmine.createSpy('pushListener'), popListener: jasmine.createSpy('popListener') }, @@ -481,15 +481,18 @@ describe("QueueRunner", function() { var queueableFn = { fn: function() { throw new Error("error"); } }, nextQueueableFn = { fn: jasmine.createSpy("nextFunction") }, cleanupFn = { fn: jasmine.createSpy("cleanup") }, + onComplete = jasmine.createSpy("onComplete"), queueRunner = new jasmineUnderTest.QueueRunner({ queueableFns: [queueableFn, nextQueueableFn], cleanupFns: [cleanupFn], + onComplete: onComplete, completeOnFirstError: true }); queueRunner.execute(); expect(nextQueueableFn.fn).not.toHaveBeenCalled(); expect(cleanupFn.fn).toHaveBeenCalled(); + expect(onComplete).toHaveBeenCalledWith(jasmine.any(jasmineUnderTest.StopExecutionError)); }); it("does not skip when a cleanup function throws", function() { diff --git a/spec/core/SpyRegistrySpec.js b/spec/core/SpyRegistrySpec.js index 92939f83..920502d1 100644 --- a/spec/core/SpyRegistrySpec.js +++ b/spec/core/SpyRegistrySpec.js @@ -214,6 +214,97 @@ describe("SpyRegistry", function() { }); }); + describe("#spyOnAllFunctions", function() { + it("checks for the existence of the object", function() { + var spyRegistry = new jasmineUnderTest.SpyRegistry(); + expect(function() { + spyRegistry.spyOnAllFunctions(void 0); + }).toThrowError(/spyOnAllFunctions could not find an object to spy upon/); + }); + + it("overrides all writable and configurable functions of the object", function() { + var spyRegistry = new jasmineUnderTest.SpyRegistry({createSpy: function() { + return 'I am a spy'; + }}); + var createNoop = function() { return function() { /**/}; }; + var noop1 = createNoop(); + var noop2 = createNoop(); + var noop3 = createNoop(); + var noop4 = createNoop(); + var noop5 = createNoop(); + + var parent = { + notSpied1: noop1 + }; + var subject = Object.create(parent); + Object.defineProperty(subject, 'spied1', { + value: noop1, + writable: true, + configurable: true, + enumerable: true + }); + Object.defineProperty(subject, 'spied2', { + value: noop2, + writable: true, + configurable: true, + enumerable: true + }); + var _spied3 = noop3; + Object.defineProperty(subject, 'spied3', { + configurable: true, + set: function (val) { + _spied3 = val; + }, + get: function() { + return _spied3; + }, + enumerable: true + }); + subject.spied4 = noop4; + Object.defineProperty(subject, 'notSpied2', { + value: noop2, + writable: false, + configurable: true, + enumerable: true + }); + Object.defineProperty(subject, 'notSpied3', { + value: noop3, + writable: true, + configurable: false, + enumerable: true + }); + Object.defineProperty(subject, 'notSpied4', { + configurable: false, + set: function(val) { /**/ }, + get: function() { + return noop4; + }, + enumerable: true + }); + Object.defineProperty(subject, 'notSpied5', { + value: noop5, + writable: true, + configurable: true, + enumerable: false + }); + subject.notSpied6 = 6; + + var spiedObject = spyRegistry.spyOnAllFunctions(subject); + + expect(subject.notSpied1).toBe(noop1); + expect(subject.notSpied2).toBe(noop2); + expect(subject.notSpied3).toBe(noop3); + expect(subject.notSpied4).toBe(noop4); + expect(subject.notSpied5).toBe(noop5); + expect(subject.notSpied6).toBe(6); + expect(subject.spied1).toBe('I am a spy'); + expect(subject.spied2).toBe('I am a spy'); + expect(subject.spied3).toBe('I am a spy'); + expect(subject.spied4).toBe('I am a spy'); + expect(spiedObject).toBe(subject); + }); + }); + describe("#clearSpies", function() { it("restores the original functions on the spied-upon objects", function() { var spies = [], diff --git a/spec/core/TreeProcessorSpec.js b/spec/core/TreeProcessorSpec.js index 6ab3a15e..2e8a9ca8 100644 --- a/spec/core/TreeProcessorSpec.js +++ b/spec/core/TreeProcessorSpec.js @@ -297,7 +297,7 @@ describe("TreeProcessor", function() { node.getResult.and.returnValue({ my: 'result' }); queueRunner.calls.mostRecent().args[0].onComplete(); - expect(nodeComplete).toHaveBeenCalledWith(node, { my: 'result' }, nodeDone); + expect(nodeComplete).toHaveBeenCalledWith(node, { my: 'result' }, jasmine.any(Function)); }); it("runs a node with children", function() { @@ -328,6 +328,37 @@ describe("TreeProcessor", function() { expect(leaf2.execute).toHaveBeenCalledWith('bar', false); }); + it("cascades errors up the tree", function() { + var leaf = new Leaf(), + node = new Node({ children: [leaf] }), + root = new Node({ children: [node] }), + queueRunner = jasmine.createSpy('queueRunner'), + nodeComplete = jasmine.createSpy('nodeComplete'), + processor = new jasmineUnderTest.TreeProcessor({ + tree: root, + runnableIds: [node.id], + nodeComplete: nodeComplete, + queueRunnerFactory: queueRunner + }), + treeComplete = jasmine.createSpy('treeComplete'), + nodeDone = jasmine.createSpy('nodeDone'); + + processor.execute(treeComplete); + var queueableFns = queueRunner.calls.mostRecent().args[0].queueableFns; + queueableFns[0].fn(nodeDone); + + queueableFns = queueRunner.calls.mostRecent().args[0].queueableFns; + expect(queueableFns.length).toBe(2); + + queueableFns[1].fn('foo'); + expect(leaf.execute).toHaveBeenCalledWith('foo', false); + + queueRunner.calls.mostRecent().args[0].onComplete('things'); + expect(nodeComplete).toHaveBeenCalled(); + nodeComplete.calls.mostRecent().args[2](); + expect(nodeDone).toHaveBeenCalledWith('things'); + }); + it("runs an excluded node with leaf", function() { var leaf1 = new Leaf(), node = new Node({ children: [leaf1] }), @@ -361,7 +392,7 @@ describe("TreeProcessor", function() { node.getResult.and.returnValue({ im: 'disabled' }); queueRunner.calls.mostRecent().args[0].onComplete(); - expect(nodeComplete).toHaveBeenCalledWith(node, { im: 'disabled' }, nodeDone); + expect(nodeComplete).toHaveBeenCalledWith(node, { im: 'disabled' }, jasmine.any(Function)); }); it("runs beforeAlls for a node with children", function() { diff --git a/spec/core/UtilSpec.js b/spec/core/UtilSpec.js index 8c42fddb..a097dc30 100644 --- a/spec/core/UtilSpec.js +++ b/spec/core/UtilSpec.js @@ -31,6 +31,105 @@ describe("jasmineUnderTest.util", function() { }); }); + describe("promise utils", function () { + + var mockNativePromise, + mockPromiseLikeObject; + + var mockPromiseLike = function () {this.then = function () {};}; + + beforeEach(function () { + jasmine.getEnv().requirePromises(); + mockNativePromise = new Promise(function (res, rej) {}); + mockPromiseLikeObject = new mockPromiseLike(); + }); + + describe("isPromise", function () { + + it("should return true when passed a native promise", function () { + expect(jasmineUnderTest.isPromise(mockNativePromise)).toBe(true); + }); + + it("should return false for promise like objects", function () { + expect(jasmineUnderTest.isPromise(mockPromiseLikeObject)).toBe(false); + }); + + it("should return false for strings", function () { + expect(jasmineUnderTest.isPromise("hello")).toBe(false); + }); + + it("should return false for numbers", function () { + expect(jasmineUnderTest.isPromise(3)).toBe(false); + }); + + it("should return false for null", function () { + expect(jasmineUnderTest.isPromise(null)).toBe(false); + }); + + it("should return false for undefined", function () { + expect(jasmineUnderTest.isPromise(undefined)).toBe(false); + }); + + it("should return false for arrays", function () { + expect(jasmineUnderTest.isPromise([])).toBe(false); + }); + + it("should return false for objects", function () { + expect(jasmineUnderTest.isPromise({})).toBe(false); + }); + + it("should return false for boolean values", function () { + expect(jasmineUnderTest.isPromise(true)).toBe(false); + }); + + }); + + describe("isPromiseLike", function () { + + it("should return true when passed a native promise", function () { + expect(jasmineUnderTest.isPromiseLike(mockNativePromise)).toBe(true); + }); + + it("should return true for promise like objects", function () { + expect(jasmineUnderTest.isPromiseLike(mockPromiseLikeObject)).toBe(true); + }); + + it("should return false if then is not a function", function () { + expect(jasmineUnderTest.isPromiseLike({then:{its:"Not a function :O"}})).toBe(false); + }); + + it("should return false for strings", function () { + expect(jasmineUnderTest.isPromiseLike("hello")).toBe(false); + }); + + it("should return false for numbers", function () { + expect(jasmineUnderTest.isPromiseLike(3)).toBe(false); + }); + + it("should return false for null", function () { + expect(jasmineUnderTest.isPromiseLike(null)).toBe(false); + }); + + it("should return false for undefined", function () { + expect(jasmineUnderTest.isPromiseLike(undefined)).toBe(false); + }); + + it("should return false for arrays", function () { + expect(jasmineUnderTest.isPromiseLike([])).toBe(false); + }); + + it("should return false for objects", function () { + expect(jasmineUnderTest.isPromiseLike({})).toBe(false); + }); + + it("should return false for boolean values", function () { + expect(jasmineUnderTest.isPromiseLike(true)).toBe(false); + }); + + }); + + }); + describe("isUndefined", function() { it("reports if a variable is defined", function() { var a; diff --git a/spec/core/integration/CustomMatchersSpec.js b/spec/core/integration/CustomMatchersSpec.js index 6c1fba2b..17e66354 100644 --- a/spec/core/integration/CustomMatchersSpec.js +++ b/spec/core/integration/CustomMatchersSpec.js @@ -4,7 +4,7 @@ describe("Custom Matchers (Integration)", function() { beforeEach(function() { env = new jasmineUnderTest.Env(); - env.randomizeTests(false); + env.configure({random: false}); }); it("allows adding more matchers local to a spec", function(done) { diff --git a/spec/core/integration/CustomSpyStrategiesSpec.js b/spec/core/integration/CustomSpyStrategiesSpec.js index ba82362b..796d3478 100644 --- a/spec/core/integration/CustomSpyStrategiesSpec.js +++ b/spec/core/integration/CustomSpyStrategiesSpec.js @@ -3,7 +3,7 @@ describe('Custom Spy Strategies (Integration)', function() { beforeEach(function() { env = new jasmineUnderTest.Env(); - env.randomizeTests(false); + env.configure({random: false}); }); it('allows adding more strategies local to a suite', function(done) { diff --git a/spec/core/integration/EnvSpec.js b/spec/core/integration/EnvSpec.js index 9098cefe..dc690984 100644 --- a/spec/core/integration/EnvSpec.js +++ b/spec/core/integration/EnvSpec.js @@ -17,7 +17,7 @@ describe("Env integration", function() { }; env.addReporter({ jasmineDone: assertions}); - env.randomizeTests(false); + env.configure({random: false}); env.describe("A Suite", function() { env.it("with a spec", function() { @@ -46,7 +46,7 @@ describe("Env integration", function() { }; env.addReporter({ jasmineDone: assertions }); - env.randomizeTests(false); + env.configure({random: false}); env.describe("Outer suite", function() { env.it("an outer spec", function() { @@ -81,7 +81,7 @@ describe("Env integration", function() { }; env.addReporter({ jasmineDone: assertions }); - env.randomizeTests(false); + env.configure({random: false}); env.describe("Outer suite", function() { @@ -200,7 +200,7 @@ describe("Env integration", function() { var env = new jasmineUnderTest.Env(); env.addReporter({jasmineDone: done}); - env.randomizeTests(false); + env.configure({random: false}); env.describe("tests", function() { var firstTimeThrough = true, firstSpecContext, secondSpecContext; @@ -783,9 +783,11 @@ describe("Env integration", function() { }); }); - env.specFilter = function(spec) { - return /^first suite/.test(spec.getFullName()); - }; + env.configure({ + specFilter: function(spec) { + return /^first suite/.test(spec.getFullName()); + } + }); env.execute(); }); @@ -953,7 +955,7 @@ describe("Env integration", function() { }; env.addReporter({ jasmineDone: assertions }); - env.randomizeTests(false); + env.configure({random: false}); env.describe("tests", function() { env.it("test with mock clock", function() { @@ -1106,17 +1108,15 @@ describe("Env integration", function() { it('should wait a custom interval before reporting async functions that fail to call done', function(done) { var env = createMockedEnv(), - reporter = jasmine.createSpyObj('fakeReport', ['jasmineDone', 'suiteDone', 'specDone']), - timeoutFailure = (/^Error: Timeout - Async callback was not invoked within timeout specified by jasmine\.DEFAULT_TIMEOUT_INTERVAL\./); - + reporter = jasmine.createSpyObj('fakeReport', ['jasmineDone', 'suiteDone', 'specDone']); reporter.jasmineDone.and.callFake(function(r) { expect(r.failedExpectations).toEqual([]); - expect(reporter.suiteDone).toHaveFailedExpectationsForRunnable('suite beforeAll', [ timeoutFailure ]); - expect(reporter.suiteDone).toHaveFailedExpectationsForRunnable('suite afterAll', [ timeoutFailure ]); - expect(reporter.specDone).toHaveFailedExpectationsForRunnable('suite beforeEach times out', [ timeoutFailure ]); - expect(reporter.specDone).toHaveFailedExpectationsForRunnable('suite afterEach times out', [ timeoutFailure ]); - expect(reporter.specDone).toHaveFailedExpectationsForRunnable('suite it times out', [ timeoutFailure ]); + expect(reporter.suiteDone).toHaveFailedExpectationsForRunnable('suite beforeAll', [ /^Error: Timeout - Async callback was not invoked within 5000ms \(custom timeout\)/ ]); + expect(reporter.suiteDone).toHaveFailedExpectationsForRunnable('suite afterAll', [ /^Error: Timeout - Async callback was not invoked within 2000ms \(custom timeout\)/ ]); + expect(reporter.specDone).toHaveFailedExpectationsForRunnable('suite beforeEach times out', [/^Error: Timeout - Async callback was not invoked within 1000ms \(custom timeout\)/]); + expect(reporter.specDone).toHaveFailedExpectationsForRunnable('suite afterEach times out', [ /^Error: Timeout - Async callback was not invoked within 4000ms \(custom timeout\)/ ]); + expect(reporter.specDone).toHaveFailedExpectationsForRunnable('suite it times out', [ /^Error: Timeout - Async callback was not invoked within 6000ms \(custom timeout\)/ ]); jasmine.clock().tick(1); realSetTimeout(done); @@ -1478,8 +1478,7 @@ describe("Env integration", function() { "specStarted", "specDone" ]); - env.randomizeTests(true); - env.seed('123456'); + env.configure({random: true, seed: '123456'}); reporter.jasmineDone.and.callFake(function(doneArg) { expect(reporter.jasmineStarted).toHaveBeenCalled(); @@ -1493,7 +1492,7 @@ describe("Env integration", function() { }); env.addReporter(reporter); - env.randomizeTests(true); + env.configure({random: true}); env.execute(); }); @@ -1664,7 +1663,7 @@ describe("Env integration", function() { }); env.addReporter(reporter); - env.randomizeTests(false); + env.configure({random: false}); env.describe("testing custom equality testers", function() { env.it("with a custom tester", function() { @@ -1700,7 +1699,7 @@ describe("Env integration", function() { }); env.addReporter(reporter); - env.randomizeTests(false); + env.configure({random: false}); env.describe("testing custom equality testers", function() { env.beforeAll(function() { env.addCustomEqualityTester(function(a, b) { return true; }); }); @@ -1741,7 +1740,7 @@ describe("Env integration", function() { }); env.addReporter(reporter); - env.randomizeTests(false); + env.configure({random: false}); env.describe("testing custom equality testers", function() { env.it("with a custom tester", function() { @@ -1794,7 +1793,7 @@ describe("Env integration", function() { }); env.addReporter(reporter); - env.randomizeTests(false); + env.configure({random: false}); env.describe("testing custom equality testers", function() { env.beforeAll(function() { env.addCustomEqualityTester(function(a, b) { return true; })}); @@ -2415,4 +2414,106 @@ describe("Env integration", function() { env.execute(); }); + + it('supports async matchers', function(done) { + jasmine.getEnv().requirePromises(); + + var env = new jasmineUnderTest.Env(), + specDone = jasmine.createSpy('specDone'), + suiteDone = jasmine.createSpy('suiteDone'); + + env.addReporter({ + specDone: specDone, + suiteDone: suiteDone, + jasmineDone: function(result) { + expect(result.failedExpectations).toEqual([jasmine.objectContaining({ + message: 'Expected a promise to be rejected.' + })]); + + expect(specDone).toHaveBeenCalledWith(jasmine.objectContaining({ + description: 'has an async failure', + failedExpectations: [jasmine.objectContaining({ + message: 'Expected a promise to be rejected.' + })] + })); + + expect(suiteDone).toHaveBeenCalledWith(jasmine.objectContaining({ + description: 'a suite', + failedExpectations: [jasmine.objectContaining({ + message: 'Expected a promise to be rejected.' + })] + })); + + done(); + } + }); + + function fail(innerDone) { + var resolve; + var p = new Promise(function(res, rej) { resolve = res }); + env.expectAsync(p).toBeRejected().then(innerDone); + resolve(); + } + + env.afterAll(fail); + env.describe('a suite', function() { + env.afterAll(fail); + env.it('has an async failure', fail); + }); + + env.execute(); + }); + + it('provides custom equality testers to async matchers', function(done) { + jasmine.getEnv().requirePromises(); + + var env = new jasmineUnderTest.Env(), + specDone = jasmine.createSpy('specDone'); + + env.addReporter({ + specDone: specDone, + jasmineDone: function() { + expect(specDone).toHaveBeenCalledWith(jasmine.objectContaining({ + description: 'has an async failure', + failedExpectations: [] + })); + done(); + } + }); + + env.it('has an async failure', function() { + env.addCustomEqualityTester(function() { return true; }); + var p = Promise.resolve('something'); + return env.expectAsync(p).toBeResolvedTo('something else'); + }); + + env.execute(); + }); + + it('includes useful stack frames in async matcher failures', function(done) { + jasmine.getEnv().requirePromises(); + + var env = new jasmineUnderTest.Env(), + specDone = jasmine.createSpy('specDone'); + + env.addReporter({ + specDone: specDone, + jasmineDone: function() { + expect(specDone).toHaveBeenCalledWith(jasmine.objectContaining({ + failedExpectations: [jasmine.objectContaining({ + stack: jasmine.stringMatching('EnvSpec.js') + })] + })); + done(); + } + }); + + env.it('has an async failure', function() { + env.addCustomEqualityTester(function() { return true; }); + var p = Promise.resolve(); + return env.expectAsync(p).toBeRejected(); + }); + + env.execute(); + }); }); diff --git a/spec/core/integration/SpecRunningSpec.js b/spec/core/integration/SpecRunningSpec.js index e0d8b2ed..1bd670b4 100644 --- a/spec/core/integration/SpecRunningSpec.js +++ b/spec/core/integration/SpecRunningSpec.js @@ -4,7 +4,7 @@ describe("spec running", function () { beforeEach(function() { jasmine.getEnv().registerIntegrationMatchers(); env = new jasmineUnderTest.Env(); - env.randomizeTests(false); + env.configure({random: false}); }); it('should assign spec ids sequentially', function() { @@ -740,8 +740,7 @@ describe("spec running", function () { it("should run the tests in a consistent order when a seed is supplied", function(done) { var actions = []; - env.seed('123456'); - env.randomizeTests(true); + env.configure({random: true, seed: '123456'}); env.beforeEach(function () { actions.push('topSuite beforeEach'); @@ -865,7 +864,7 @@ describe("spec running", function () { }); }); - env.throwOnExpectationFailure(true); + env.configure({oneFailurePerSpec: true}); var assertions = function() { expect(actions).toEqual([ @@ -900,7 +899,7 @@ describe("spec running", function () { }); }); - env.throwOnExpectationFailure(true); + env.configure({oneFailurePerSpec: true}); var assertions = function() { expect(actions).toEqual([ @@ -932,7 +931,7 @@ describe("spec running", function () { }); }); - env.throwOnExpectationFailure(true); + env.configure({oneFailurePerSpec: true}); var assertions = function() { expect(actions).toEqual([ @@ -946,23 +945,145 @@ describe("spec running", function () { env.execute(); }); + + it("skips to cleanup functions after an error with deprecations", function(done) { + var actions = []; + + spyOn(env, 'deprecated'); + + env.describe('Something', function() { + env.beforeEach(function() { + actions.push('outer beforeEach'); + throw new Error("error"); + }); + + env.afterEach(function() { + actions.push('outer afterEach'); + }); + + env.describe('Inner', function() { + env.beforeEach(function() { + actions.push('inner beforeEach'); + }); + + env.afterEach(function() { + actions.push('inner afterEach'); + }); + + env.it('does it' , function() { + actions.push('inner it'); + }); + }); + }); + + env.throwOnExpectationFailure(true); + + var assertions = function() { + expect(actions).toEqual([ + 'outer beforeEach', + 'inner afterEach', + 'outer afterEach' + ]); + expect(env.deprecated).toHaveBeenCalled(); + done(); + }; + + env.addReporter({jasmineDone: assertions}); + + env.execute(); + }); + + it("skips to cleanup functions after done.fail is called with deprecations", function(done) { + var actions = []; + + spyOn(env, 'deprecated'); + + env.describe('Something', function() { + env.beforeEach(function(done) { + actions.push('beforeEach'); + done.fail('error'); + actions.push('after done.fail'); + }); + + env.afterEach(function() { + actions.push('afterEach'); + }); + + env.it('does it' , function() { + actions.push('it'); + }); + }); + + env.throwOnExpectationFailure(true); + + var assertions = function() { + expect(actions).toEqual([ + 'beforeEach', + 'afterEach' + ]); + expect(env.deprecated).toHaveBeenCalled(); + done(); + }; + + env.addReporter({jasmineDone: assertions}); + + env.execute(); + }); + + it("skips to cleanup functions when an async function times out with deprecations", function(done) { + var actions = []; + + spyOn(env, 'deprecated'); + + env.describe('Something', function() { + env.beforeEach(function(innerDone) { + actions.push('beforeEach'); + }, 1); + + env.afterEach(function() { + actions.push('afterEach'); + }); + + env.it('does it' , function() { + actions.push('it'); + }); + }); + + env.throwOnExpectationFailure(true); + + var assertions = function() { + expect(actions).toEqual([ + 'beforeEach', + 'afterEach' + ]); + expect(env.deprecated).toHaveBeenCalled(); + done(); + }; + + env.addReporter({jasmineDone: assertions}); + + env.execute(); + }); }); describe("when stopOnSpecFailure is on", function() { it("does not run further specs when one fails", function(done) { var actions = []; - env.it('fails', function() { - actions.push('fails'); - env.expect(1).toBe(2); + env.describe('wrapper', function() { + env.it('fails', function() { + actions.push('fails'); + env.expect(1).toBe(2); + }); }); - env.it('does not run', function() { - actions.push('does not run'); + env.describe('holder', function() { + env.it('does not run', function() { + actions.push('does not run'); + }); }); - env.randomizeTests(false); - env.stopOnSpecFailure(true); + env.configure({random: false, failFast: true}); var assertions = function() { expect(actions).toEqual(['fails']); @@ -972,5 +1093,36 @@ describe("spec running", function () { env.addReporter({ jasmineDone: assertions }); env.execute(); }); + + it("does not run further specs when one fails when configured with deprecated option", function(done) { + var actions = []; + + spyOn(env, 'deprecated'); + + env.describe('wrapper', function() { + env.it('fails', function() { + actions.push('fails'); + env.expect(1).toBe(2); + }); + }); + + env.describe('holder', function() { + env.it('does not run', function() { + actions.push('does not run'); + }); + }); + + env.configure({random: false}); + env.stopOnSpecFailure(true); + + var assertions = function() { + expect(actions).toEqual(['fails']); + expect(env.deprecated).toHaveBeenCalled(); + done(); + }; + + env.addReporter({ jasmineDone: assertions }); + env.execute(); + }); }); }); diff --git a/spec/core/matchers/toBeSpec.js b/spec/core/matchers/toBeSpec.js index 541eda96..e065af57 100644 --- a/spec/core/matchers/toBeSpec.js +++ b/spec/core/matchers/toBeSpec.js @@ -1,17 +1,56 @@ describe("toBe", function() { - it("passes when actual === expected", function() { - var matcher = jasmineUnderTest.matchers.toBe(), + it("passes with no message when actual === expected", function() { + var matcher = jasmineUnderTest.matchers.toBe(jasmineUnderTest.matchersUtil), result; result = matcher.compare(1, 1); expect(result.pass).toBe(true); }); - it("fails when actual !== expected", function() { - var matcher = jasmineUnderTest.matchers.toBe(), + it("passes with a custom message when expected is an array", function() { + var matcher = jasmineUnderTest.matchers.toBe(jasmineUnderTest.matchersUtil), + result, + array = [1]; + + result = matcher.compare(array, array); + expect(result.pass).toBe(true); + expect(result.message).toBe("Expected [ 1 ] not to be [ 1 ]. Tip: To check for deep equality, use .toEqual() instead of .toBe().") + }); + + it("passes with a custom message when expected is an object", function() { + var matcher = jasmineUnderTest.matchers.toBe(jasmineUnderTest.matchersUtil), + result, + obj = {foo: "bar"}; + + result = matcher.compare(obj, obj); + expect(result.pass).toBe(true); + expect(result.message).toBe("Expected Object({ foo: 'bar' }) not to be Object({ foo: 'bar' }). Tip: To check for deep equality, use .toEqual() instead of .toBe().") + }); + + it("fails with no message when actual !== expected", function() { + var matcher = jasmineUnderTest.matchers.toBe(jasmineUnderTest.matchersUtil), result; result = matcher.compare(1, 2); expect(result.pass).toBe(false); + expect(result.message).toBeUndefined(); + }); + + it("fails with a custom message when expected is an array", function() { + var matcher = jasmineUnderTest.matchers.toBe(jasmineUnderTest.matchersUtil), + result; + + result = matcher.compare([1], [1]); + expect(result.pass).toBe(false); + expect(result.message).toBe("Expected [ 1 ] to be [ 1 ]. Tip: To check for deep equality, use .toEqual() instead of .toBe().") + }); + + it("fails with a custom message when expected is an object", function() { + var matcher = jasmineUnderTest.matchers.toBe(jasmineUnderTest.matchersUtil), + result; + + result = matcher.compare({foo: "bar"}, {foo: "bar"}); + expect(result.pass).toBe(false); + expect(result.message).toBe("Expected Object({ foo: 'bar' }) to be Object({ foo: 'bar' }). Tip: To check for deep equality, use .toEqual() instead of .toBe().") }); }); diff --git a/spec/helpers/promises.js b/spec/helpers/promises.js new file mode 100644 index 00000000..76c6c024 --- /dev/null +++ b/spec/helpers/promises.js @@ -0,0 +1,8 @@ +(function(env) { + env.requirePromises = function() { + if (typeof Promise !== 'function') { + env.pending("Environment does not support promises"); + } + }; +})(jasmine.getEnv()); + diff --git a/spec/html/HtmlReporterSpec.js b/spec/html/HtmlReporterSpec.js index 21108fc5..8b40139c 100644 --- a/spec/html/HtmlReporterSpec.js +++ b/spec/html/HtmlReporterSpec.js @@ -103,7 +103,6 @@ describe("HtmlReporter", function() { createTextNode: function() { return document.createTextNode.apply(document, arguments); } }); reporter.initialize(); - reporter.specDone({id: 789, status: "excluded", fullName: "symbols should have titles", passedExpectations: [], failedExpectations: []}); var specEl = container.querySelector('.jasmine-symbol-summary li'); @@ -519,7 +518,7 @@ describe("HtmlReporter", function() { } }); - env.stopOnSpecFailure(true); + env.configure({failFast: true}); reporter.initialize(); reporter.jasmineDone({}); @@ -575,7 +574,7 @@ describe("HtmlReporter", function() { } }); - env.stopOnSpecFailure(true); + env.configure({failFast: true}); reporter.initialize(); reporter.jasmineDone({}); @@ -629,7 +628,7 @@ describe("HtmlReporter", function() { } }); - env.throwOnExpectationFailure(true); + env.configure({oneFailurePerSpec: true}); reporter.initialize(); reporter.jasmineDone({}); @@ -685,7 +684,7 @@ describe("HtmlReporter", function() { } }); - env.throwOnExpectationFailure(true); + env.configure({oneFailurePerSpec: true}); reporter.initialize(); reporter.jasmineDone({}); @@ -696,7 +695,82 @@ describe("HtmlReporter", function() { expect(navigateHandler).toHaveBeenCalledWith('throwFailures', false); }); }); + describe("UI for hiding disabled specs", function() { + it("should be unchecked if not hiding disabled specs", function() { + var env = new jasmineUnderTest.Env(), + container = document.createElement("div"), + getContainer = function() { + return container; + }, + reporter = new jasmineUnderTest.HtmlReporter({ + env: env, + getContainer: getContainer, + createElement: function() { + return document.createElement.apply(document, arguments); + }, + createTextNode: function() { + return document.createTextNode.apply(document, arguments); + } + }); + env.configure({hideDisabled: false}); + reporter.initialize(); + reporter.jasmineDone({}); + + var disabledUI = container.querySelector(".jasmine-disabled"); + expect(disabledUI.checked).toBe(false); + }); + + it("should be checked if hiding disabled", function() { + var env = new jasmineUnderTest.Env(), + container = document.createElement("div"), + getContainer = function() { + return container; + }, + reporter = new jasmineUnderTest.HtmlReporter({ + env: env, + getContainer: getContainer, + createElement: function() { + return document.createElement.apply(document, arguments); + }, + createTextNode: function() { + return document.createTextNode.apply(document, arguments); + } + }); + + env.configure({hideDisabled: true}); + reporter.initialize(); + reporter.jasmineDone({}); + + var disabledUI = container.querySelector(".jasmine-disabled"); + expect(disabledUI.checked).toBe(true); + }); + + it("should not display specs that have been disabled", function() { + var env = new jasmineUnderTest.Env(), + container = document.createElement('div'), + getContainer = function() {return container;}, + reporter = new jasmineUnderTest.HtmlReporter({ + env: env, + getContainer: getContainer, + createElement: function() { return document.createElement.apply(document, arguments); }, + createTextNode: function() { return document.createTextNode.apply(document, arguments); } + }); + + env.configure({hideDisabled: true}); + reporter.initialize(); + reporter.specDone({ + id: 789, + status: "excluded", + fullName: "symbols should have titles", + passedExpectations: [], + failedExpectations: [] + }); + + var specEl = container.querySelector('.jasmine-symbol-summary li'); + expect(specEl.getAttribute("class")).toEqual("jasmine-excluded-no-display"); + }); + }); describe("UI for running tests in random order", function() { it("should be unchecked if not randomizing", function() { var env = new jasmineUnderTest.Env(), @@ -715,7 +789,7 @@ describe("HtmlReporter", function() { } }); - env.randomizeTests(false); + env.configure({random: false}); reporter.initialize(); reporter.jasmineDone({}); @@ -740,7 +814,7 @@ describe("HtmlReporter", function() { } }); - env.randomizeTests(true); + env.configure({random: true}); reporter.initialize(); reporter.jasmineDone({}); @@ -767,7 +841,7 @@ describe("HtmlReporter", function() { } }); - env.randomizeTests(false); + env.configure({random: false}); reporter.initialize(); reporter.jasmineDone({}); @@ -796,7 +870,7 @@ describe("HtmlReporter", function() { } }); - env.randomizeTests(true); + env.configure({random: true}); reporter.initialize(); reporter.jasmineDone({}); diff --git a/spec/support/jasmine.json b/spec/support/jasmine.json index b2c83485..1d94e2f5 100644 --- a/spec/support/jasmine.json +++ b/spec/support/jasmine.json @@ -11,6 +11,7 @@ "helpers/checkForSymbol.js", "helpers/checkForTypedArrays.js", "helpers/integrationMatchers.js", + "helpers/promises.js", "helpers/nodeDefineJasmineUnderTest.js" ], "random": true diff --git a/spec/support/jasmine.yml b/spec/support/jasmine.yml index 88560b1a..6460624e 100644 --- a/spec/support/jasmine.yml +++ b/spec/support/jasmine.yml @@ -24,6 +24,7 @@ helpers: - 'helpers/checkForSymbol.js' - 'helpers/checkForTypedArrays.js' - 'helpers/integrationMatchers.js' + - 'helpers/promises.js' - 'helpers/defineJasmineUnderTest.js' spec_files: - '**/*[Ss]pec.js' diff --git a/src/core/AsyncExpectation.js b/src/core/AsyncExpectation.js new file mode 100644 index 00000000..4b9e7ef0 --- /dev/null +++ b/src/core/AsyncExpectation.js @@ -0,0 +1,228 @@ +getJasmineRequireObj().AsyncExpectation = function(j$) { + var promiseForMessage = { + jasmineToString: function() { return 'a promise'; } + }; + + /** + * Asynchronous matchers. + * @namespace async-matchers + */ + function AsyncExpectation(options) { + var global = options.global || j$.getGlobal(); + this.util = options.util || { buildFailureMessage: function() {} }; + this.customEqualityTesters = options.customEqualityTesters || []; + this.addExpectationResult = options.addExpectationResult || function(){}; + this.actual = options.actual; + this.filters = new j$.ExpectationFilterChain(); + + if (!global.Promise) { + throw new Error('expectAsync is unavailable because the environment does not support promises.'); + } + + if (!j$.isPromiseLike(this.actual)) { + throw new Error('Expected expectAsync to be called with a promise.'); + } + + ['toBeResolved', 'toBeRejected', 'toBeResolvedTo', 'toBeRejectedWith'].forEach(wrapCompare.bind(this)); + } + + function wrapCompare(name) { + var matcher = this[name]; + this[name] = function() { + var self = this; + var args = Array.prototype.slice.call(arguments); + args.unshift(this.actual); + + // Capture the call stack here, before we go async, so that it will + // contain frames that are relevant to the user instead of just parts + // of Jasmine. + var errorForStack = j$.util.errorWithStack(); + + var matcherCompare = this.instantiateMatcher(matcher); + + return matcherCompare.apply(self, args).then(function(result) { + var message; + + args[0] = promiseForMessage; + message = j$.Expectation.prototype.buildMessage.call(self, result, name, args); + + self.addExpectationResult(result.pass, { + matcherName: name, + passed: result.pass, + message: message, + error: undefined, + errorForStack: errorForStack, + actual: self.actual + }); + }); + }; + } + + AsyncExpectation.prototype.instantiateMatcher = function(matcher) { + var comparisonFunc = this.filters.selectComparisonFunc(matcher); + return comparisonFunc || matcher; + }; + + /** + * Expect a promise to be resolved. + * @function + * @async + * @name async-matchers#toBeResolved + * @example + * await expectAsync(aPromise).toBeResolved(); + * @example + * return expectAsync(aPromise).toBeResolved(); + */ + AsyncExpectation.prototype.toBeResolved = function(actual) { + return actual.then( + function() { return {pass: true}; }, + function() { return {pass: false}; } + ); + }; + + /** + * Expect a promise to be rejected. + * @function + * @async + * @name async-matchers#toBeRejected + * @example + * await expectAsync(aPromise).toBeRejected(); + * @example + * return expectAsync(aPromise).toBeRejected(); + */ + AsyncExpectation.prototype.toBeRejected = function(actual) { + return actual.then( + function() { return {pass: false}; }, + function() { return {pass: true}; } + ); + }; + + /** + * Expect a promise to be resolved to a value equal to the expected, using deep equality comparison. + * @function + * @async + * @name async-matchers#toBeResolvedTo + * @param {Object} expected - Value that the promise is expected to resolve to + * @example + * await expectAsync(aPromise).toBeResolvedTo({prop: 'value'}); + * @example + * return expectAsync(aPromise).toBeResolvedTo({prop: 'value'}); + */ + AsyncExpectation.prototype.toBeResolvedTo = function(actualPromise, expectedValue) { + var self = this; + + function prefix(passed) { + return 'Expected a promise ' + + (passed ? 'not ' : '') + + 'to be resolved to ' + j$.pp(expectedValue); + } + + return actualPromise.then( + function(actualValue) { + if (self.util.equals(actualValue, expectedValue, self.customEqualityTesters)) { + return { + pass: true, + message: prefix(true) + '.' + }; + } else { + return { + pass: false, + message: prefix(false) + ' but it was resolved to ' + j$.pp(actualValue) + '.' + }; + } + }, + function() { + return { + pass: false, + message: prefix(false) + ' but it was rejected.' + }; + } + ); + }; + + /** + * Expect a promise to be rejected to a value equal to the expected, using deep equality comparison. + * @function + * @async + * @name async-matchers#toBeRejectedWith + * @param {Object} expected - Value that the promise is expected to reject to + * @example + * await expectAsync(aPromise).toBeRejectedWith({prop: 'value'}); + * @example + * return expectAsync(aPromise).toBeRejectedWith({prop: 'value'}); + */ + AsyncExpectation.prototype.toBeRejectedWith = function(actualPromise, expectedValue) { + var self = this; + + function prefix(passed) { + return 'Expected a promise ' + + (passed ? 'not ' : '') + + 'to be rejected with ' + j$.pp(expectedValue); + } + + return actualPromise.then( + function() { + return { + pass: false, + message: prefix(false) + ' but it was resolved.' + }; + }, + function(actualValue) { + if (self.util.equals(actualValue, expectedValue, self.customEqualityTesters)) { + return { + pass: true, + message: prefix(true) + '.' + }; + } else { + return { + pass: false, + message: prefix(false) + ' but it was rejected with ' + j$.pp(actualValue) + '.' + }; + } + } + ); + }; + + AsyncExpectation.prototype.addFilter = function(filter) { + var result = Object.create(this); + result.filters = this.filters.addFilter(filter); + return result; + }; + + AsyncExpectation.factory = function(options) { + var expect = new AsyncExpectation(options); + expect.not = expect.addFilter(negatingFilter); + + return expect; + }; + + var negatingFilter = { + selectComparisonFunc: function(matcher) { + function defaultNegativeCompare() { + return matcher.apply(this, arguments).then(function(result) { + result.pass = !result.pass; + return result; + }); + } + + return defaultNegativeCompare; + }, + buildFailureMessage: function(result, matcherName, args, util) { + if (result.message) { + if (j$.isFunction_(result.message)) { + return result.message(); + } else { + return result.message; + } + } + + args = args.slice(); + args.unshift(true); + args.unshift(matcherName); + return util.buildFailureMessage.apply(null, args); + } + }; + + + return AsyncExpectation; +}; diff --git a/src/core/Env.js b/src/core/Env.js index 9caaacd4..19105525 100644 --- a/src/core/Env.js +++ b/src/core/Env.js @@ -23,13 +23,62 @@ getJasmineRequireObj().Env = function(j$) { var currentSpec = null; var currentlyExecutingSuites = []; var currentDeclarationSuite = null; - var throwOnExpectationFailure = false; - var stopOnSpecFailure = false; - var random = true; - var seed = null; - var handlingLoadErrors = true; var hasFailures = false; + /** + * This represents the available options to configure Jasmine. + * Options that are not provided will use their default values + * @interface Configuration + */ + var config = { + /** + * Whether to randomize spec execution order + * @name Configuration#random + * @type Boolean + * @default true + */ + random: true, + /** + * Seed to use as the basis of randomization. + * Null causes the seed to be determined randomly at the start of execution. + * @name Configuration#seed + * @type function + * @default null + */ + seed: null, + /** + * Whether to stop execution of the suite after the first spec failure + * @name Configuration#failFast + * @type Boolean + * @default false + */ + failFast: false, + /** + * Whether to cause specs to only have one expectation failure. + * @name Configuration#oneFailurePerSpec + * @type Boolean + * @default false + */ + oneFailurePerSpec: false, + /** + * Function to use to filter specs + * @name Configuration#specFilter + * @type function + * @default true + */ + specFilter: function() { + return true; + }, + /** + * Whether or not reporters should hide disabled specs from their output. + * Currently only supported by Jasmine's HTMLReporter + * @name Configuration#hideDisabled + * @type Boolean + * @default false + */ + hideDisabled: false + }; + var currentSuite = function() { return currentlyExecutingSuites[currentlyExecutingSuites.length - 1]; }; @@ -62,10 +111,63 @@ getJasmineRequireObj().Env = function(j$) { }); } - this.specFilter = function() { - return true; + /** + * Configure your jasmine environment + * @name Env#configure + * @argument {Configuration} configuration + * @function + */ + this.configure = function(configuration) { + if (configuration.specFilter) { + config.specFilter = configuration.specFilter; + } + + if (configuration.hasOwnProperty('random')) { + config.random = !!configuration.random; + } + + if (configuration.hasOwnProperty('seed')) { + config.seed = configuration.seed; + } + + if (configuration.hasOwnProperty('failFast')) { + config.failFast = configuration.failFast; + } + + if (configuration.hasOwnProperty('oneFailurePerSpec')) { + config.oneFailurePerSpec = configuration.oneFailurePerSpec; + } + + if (configuration.hasOwnProperty('hideDisabled')) { + config.hideDisabled = configuration.hideDisabled; + } }; + /** + * Get the current configuration for your jasmine environment + * @name Env#configuration + * @function + * @returns {Configuration} + */ + this.configuration = function() { + var result = {}; + for (var property in config) { + result[property] = config[property]; + } + return result; + }; + + Object.defineProperty(this, 'specFilter', { + get: function() { + self.deprecated('Getting specFilter directly from Env is deprecated, please check the specFilter option from `configuration`'); + return config.specFilter; + }, + set: function(val) { + self.deprecated('Setting specFilter directly on Env is deprecated, please use the specFilter option in `configure`'); + config.specFilter = val; + } + }); + this.addSpyStrategy = function(name, fn) { if(!currentRunnable()) { throw new Error('Custom spy strategies must be added in a before function or a spec'); @@ -116,6 +218,19 @@ getJasmineRequireObj().Env = function(j$) { } }; + var asyncExpectationFactory = function(actual, spec) { + return j$.AsyncExpectation.factory({ + util: j$.matchersUtil, + customEqualityTesters: runnableResources[spec.id].customEqualityTesters, + actual: actual, + addExpectationResult: addExpectationResult + }); + + function addExpectationResult(passed, result) { + return spec.addExpectationResult(passed, result); + } + }; + var defaultResourcesForRunnable = function(id, parentRunnableId) { var resources = {spies: [], customEqualityTesters: [], customMatchers: {}, customSpyStrategies: {}}; @@ -174,35 +289,85 @@ getJasmineRequireObj().Env = function(j$) { var maximumSpecCallbackDepth = 20; var currentSpecCallbackDepth = 0; + /** + * Sets whether Jasmine should throw an Error when an expectation fails. + * This causes a spec to only have one expectation failure. + * @name Env#throwOnExpectationFailure + * @function + * @param {Boolean} value Whether to throw when a expectation fails + * @deprecated Use the `oneFailurePerSpec` option with {@link Env#configure} + */ this.throwOnExpectationFailure = function(value) { - throwOnExpectationFailure = !!value; + this.deprecated('Setting throwOnExpectationFailure directly on Env is deprecated, please use the oneFailurePerSpec option in `configure`'); + this.configure({oneFailurePerSpec: !!value}); }; this.throwingExpectationFailures = function() { - return throwOnExpectationFailure; + this.deprecated('Getting throwingExpectationFailures directly from Env is deprecated, please check the oneFailurePerSpec option from `configuration`'); + return config.oneFailurePerSpec; }; + /** + * Set whether to stop suite execution when a spec fails + * @name Env#stopOnSpecFailure + * @function + * @param {Boolean} value Whether to stop suite execution when a spec fails + * @deprecated Use the `failFast` option with {@link Env#configure} + */ this.stopOnSpecFailure = function(value) { - stopOnSpecFailure = !!value; + this.deprecated('Setting stopOnSpecFailure directly is deprecated, please use the failFast option in `configure`'); + this.configure({failFast: !!value}); }; this.stoppingOnSpecFailure = function() { - return stopOnSpecFailure; + this.deprecated('Getting stoppingOnSpecFailure directly from Env is deprecated, please check the failFast option from `configuration`'); + return config.failFast; }; + /** + * Set whether to randomize test execution order + * @name Env#randomizeTests + * @function + * @param {Boolean} value Whether to randomize execution order + * @deprecated Use the `random` option with {@link Env#configure} + */ this.randomizeTests = function(value) { - random = !!value; + this.deprecated('Setting randomizeTests directly is deprecated, please use the random option in `configure`'); + config.random = !!value; }; this.randomTests = function() { - return random; + this.deprecated('Getting randomTests directly from Env is deprecated, please check the random option from `configuration`'); + return config.random; }; + /** + * Set the random number seed for spec randomization + * @name Env#seed + * @function + * @param {Number} value The seed value + * @deprecated Use the `seed` option with {@link Env#configure} + */ this.seed = function(value) { + this.deprecated('Setting seed directly is deprecated, please use the seed option in `configure`'); if (value) { - seed = value; + config.seed = value; } - return seed; + return config.seed; + }; + + this.hidingDisabled = function(value) { + this.deprecated('Getting hidingDisabled directly from Env is deprecated, please check the hideDisabled option from `configuration`'); + return config.hideDisabled; + }; + + /** + * @name Env#hideDisabled + * @function + */ + this.hideDisabled = function(value) { + this.deprecated('Setting hideDisabled directly is deprecated, please use the hideDisabled option in `configure`'); + config.hideDisabled = !!value; }; this.deprecated = function(deprecation) { @@ -216,9 +381,9 @@ getJasmineRequireObj().Env = function(j$) { var queueRunnerFactory = function(options, args) { var failFast = false; if (options.isLeaf) { - failFast = throwOnExpectationFailure; + failFast = config.oneFailurePerSpec; } else if (!options.isReporter) { - failFast = stopOnSpecFailure; + failFast = config.failFast; } options.clearStack = options.clearStack || clearStack; options.timeout = {setTimeout: realSetTimeout, clearTimeout: realClearTimeout}; @@ -238,6 +403,7 @@ getJasmineRequireObj().Env = function(j$) { id: getNextSuiteId(), description: 'Jasmine__TopLevel__Suite', expectationFactory: expectationFactory, + asyncExpectationFactory: asyncExpectationFactory, expectationResultFactory: expectationResultFactory }); defaultResourcesForRunnable(topSuite.id); @@ -332,8 +498,8 @@ getJasmineRequireObj().Env = function(j$) { } var order = new j$.Order({ - random: random, - seed: seed + random: config.random, + seed: config.seed }); var processor = new j$.TreeProcessor({ @@ -363,7 +529,7 @@ getJasmineRequireObj().Env = function(j$) { return order.sort(node.children); }, excludeNode: function(spec) { - return !self.specFilter(spec); + return !config.specFilter(spec); } }); @@ -431,10 +597,22 @@ getJasmineRequireObj().Env = function(j$) { reporter.addReporter(reporterToAdd); }; + /** + * Provide a fallback reporter if no other reporters have been specified. + * @name Env#provideFallbackReporter + * @function + * @param {Reporter} reporterToAdd The reporter + * @see custom_reporter + */ this.provideFallbackReporter = function(reporterToAdd) { reporter.provideFallbackReporter(reporterToAdd); }; + /** + * Clear all registered reporters + * @name Env#clearReporters + * @function + */ this.clearReporters = function() { reporter.clearReporters(); }; @@ -473,6 +651,10 @@ getJasmineRequireObj().Env = function(j$) { return spyRegistry.spyOnProperty.apply(spyRegistry, arguments); }; + this.spyOnAllFunctions = function() { + return spyRegistry.spyOnAllFunctions.apply(spyRegistry, arguments); + }; + this.createSpy = function(name, originalFn) { if (arguments.length === 1 && j$.isFunction_(name)) { originalFn = name; @@ -512,8 +694,9 @@ getJasmineRequireObj().Env = function(j$) { description: description, parentSuite: currentDeclarationSuite, expectationFactory: expectationFactory, + asyncExpectationFactory: asyncExpectationFactory, expectationResultFactory: expectationResultFactory, - throwOnExpectationFailure: throwOnExpectationFailure + throwOnExpectationFailure: config.oneFailurePerSpec }); return suite; @@ -605,6 +788,7 @@ getJasmineRequireObj().Env = function(j$) { id: getNextSpecId(), beforeAndAfterFns: beforeAndAfterFns(suite), expectationFactory: expectationFactory, + asyncExpectationFactory: asyncExpectationFactory, resultCallback: specResultCallback, getSpecName: function(spec) { return getSpecName(spec, suite); @@ -616,9 +800,9 @@ getJasmineRequireObj().Env = function(j$) { userContext: function() { return suite.clonedSharedUserContext(); }, queueableFn: { fn: fn, - timeout: function() { return timeout || j$.DEFAULT_TIMEOUT_INTERVAL; } + timeout: timeout || 0 }, - throwOnExpectationFailure: throwOnExpectationFailure + throwOnExpectationFailure: config.oneFailurePerSpec }); return spec; @@ -686,12 +870,20 @@ getJasmineRequireObj().Env = function(j$) { return currentRunnable().expect(actual); }; + this.expectAsync = function(actual) { + if (!currentRunnable()) { + throw new Error('\'expectAsync\' was used when there was no current spec, this could be because an asynchronous test timed out'); + } + + return currentRunnable().expectAsync(actual); + }; + this.beforeEach = function(beforeEachFunction, timeout) { ensureIsNotNested('beforeEach'); ensureIsFunctionOrAsync(beforeEachFunction, 'beforeEach'); currentDeclarationSuite.beforeEach({ fn: beforeEachFunction, - timeout: function() { return timeout || j$.DEFAULT_TIMEOUT_INTERVAL; } + timeout: timeout || 0 }); }; @@ -700,7 +892,7 @@ getJasmineRequireObj().Env = function(j$) { ensureIsFunctionOrAsync(beforeAllFunction, 'beforeAll'); currentDeclarationSuite.beforeAll({ fn: beforeAllFunction, - timeout: function() { return timeout || j$.DEFAULT_TIMEOUT_INTERVAL; } + timeout: timeout || 0 }); }; @@ -710,7 +902,7 @@ getJasmineRequireObj().Env = function(j$) { afterEachFunction.isCleanup = true; currentDeclarationSuite.afterEach({ fn: afterEachFunction, - timeout: function() { return timeout || j$.DEFAULT_TIMEOUT_INTERVAL; } + timeout: timeout || 0 }); }; @@ -719,7 +911,7 @@ getJasmineRequireObj().Env = function(j$) { ensureIsFunctionOrAsync(afterAllFunction, 'afterAll'); currentDeclarationSuite.afterAll({ fn: afterAllFunction, - timeout: function() { return timeout || j$.DEFAULT_TIMEOUT_INTERVAL; } + timeout: timeout || 0 }); }; @@ -758,7 +950,7 @@ getJasmineRequireObj().Env = function(j$) { error: error && error.message ? error : null }); - if (self.throwingExpectationFailures()) { + if (config.oneFailurePerSpec) { throw new Error(message); } }; diff --git a/src/core/ExceptionFormatter.js b/src/core/ExceptionFormatter.js index 359a8303..329418ab 100644 --- a/src/core/ExceptionFormatter.js +++ b/src/core/ExceptionFormatter.js @@ -7,6 +7,8 @@ getJasmineRequireObj().ExceptionFormatter = function(j$) { if (error.name && error.message) { message += error.name + ': ' + error.message; + } else if (error.message) { + message += error.message; } else { message += error.toString() + ' thrown'; } diff --git a/src/core/Expectation.js b/src/core/Expectation.js index d5690d67..5367ab24 100644 --- a/src/core/Expectation.js +++ b/src/core/Expectation.js @@ -39,7 +39,7 @@ getJasmineRequireObj().Expectation = function(j$) { Expectation.prototype.processResult = function(result, name, expected, args) { var message = this.buildMessage(result, name, args); - if (expected.length == 1) { + if (expected.length === 1) { expected = expected[0]; } @@ -58,14 +58,13 @@ getJasmineRequireObj().Expectation = function(j$) { }; Expectation.prototype.buildMessage = function(result, name, args) { - var util = this.util, - msg; + var util = this.util; if (result.pass) { return ''; } - msg = this.filters.buildFailureMessage(result, name, args, util, defaultMessage); + var msg = this.filters.buildFailureMessage(result, name, args, util, defaultMessage); return this.filters.modifyFailureMessage(msg || defaultMessage()); function defaultMessage() { diff --git a/src/core/ExpectationResult.js b/src/core/ExpectationResult.js index b4a43e4f..869d5d24 100644 --- a/src/core/ExpectationResult.js +++ b/src/core/ExpectationResult.js @@ -45,7 +45,9 @@ getJasmineRequireObj().buildExpectationResult = function() { var error = options.error; if (!error) { - if (options.stack) { + if (options.errorForStack) { + error = options.errorForStack; + } else if (options.stack) { error = options; } else { try { diff --git a/src/core/QueueRunner.js b/src/core/QueueRunner.js index 932c50ba..a926cc91 100644 --- a/src/core/QueueRunner.js +++ b/src/core/QueueRunner.js @@ -5,26 +5,29 @@ getJasmineRequireObj().QueueRunner = function(j$) { function once(fn) { var called = false; - return function() { + return function(arg) { if (!called) { called = true; - fn.apply(null, arguments); + // Direct call using single parameter, because cleanup/next does not need more + fn(arg); } return null; }; } + function emptyFn() {} + function QueueRunner(attrs) { var queueableFns = attrs.queueableFns || []; this.queueableFns = queueableFns.concat(attrs.cleanupFns || []); this.firstCleanupIx = queueableFns.length; - this.onComplete = attrs.onComplete || function() {}; + this.onComplete = attrs.onComplete || emptyFn; this.clearStack = attrs.clearStack || function(fn) {fn();}; - this.onException = attrs.onException || function() {}; + this.onException = attrs.onException || emptyFn; this.userContext = attrs.userContext || new j$.UserContext(); this.timeout = attrs.timeout || {setTimeout: setTimeout, clearTimeout: clearTimeout}; - this.fail = attrs.fail || function() {}; - this.globalErrors = attrs.globalErrors || { pushListener: function() {}, popListener: function() {} }; + this.fail = attrs.fail || emptyFn; + this.globalErrors = attrs.globalErrors || { pushListener: emptyFn, popListener: emptyFn }; this.completeOnFirstError = !!attrs.completeOnFirstError; this.errored = false; @@ -66,7 +69,9 @@ getJasmineRequireObj().QueueRunner = function(j$) { next(error); }, cleanup = once(function cleanup() { - self.clearTimeout(timeoutId); + if (timeoutId !== void 0) { + self.clearTimeout(timeoutId); + } self.globalErrors.popListener(handleError); }), next = once(function next(err) { @@ -105,12 +110,16 @@ getJasmineRequireObj().QueueRunner = function(j$) { self.globalErrors.pushListener(handleError); - if (queueableFn.timeout) { + if (queueableFn.timeout !== undefined) { + var timeoutInterval = queueableFn.timeout || j$.DEFAULT_TIMEOUT_INTERVAL; timeoutId = self.setTimeout(function() { - var error = new Error('Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.'); + var error = new Error( + 'Timeout - Async callback was not invoked within ' + timeoutInterval + 'ms ' + + (queueableFn.timeout ? '(custom timeout)' : '(set by jasmine.DEFAULT_TIMEOUT_INTERVAL)') + ); onException(error); next(); - }, queueableFn.timeout()); + }, timeoutInterval); } try { @@ -159,7 +168,7 @@ getJasmineRequireObj().QueueRunner = function(j$) { return; } - self.errored = result.errored; + self.errored = self.errored || result.errored; if (this.completeOnFirstError && result.errored) { this.skipToCleanup(iterativeIndex); diff --git a/src/core/Spec.js b/src/core/Spec.js index 3049ea95..524dece6 100644 --- a/src/core/Spec.js +++ b/src/core/Spec.js @@ -1,6 +1,7 @@ getJasmineRequireObj().Spec = function(j$) { function Spec(attrs) { this.expectationFactory = attrs.expectationFactory; + this.asyncExpectationFactory = attrs.asyncExpectationFactory; this.resultCallback = attrs.resultCallback || function() {}; this.id = attrs.id; this.description = attrs.description || ''; @@ -57,6 +58,10 @@ getJasmineRequireObj().Spec = function(j$) { return this.expectationFactory(actual, this); }; + Spec.prototype.expectAsync = function(actual) { + return this.asyncExpectationFactory(actual, this); + }; + Spec.prototype.execute = function(onComplete, excluded) { var self = this; diff --git a/src/core/SpyRegistry.js b/src/core/SpyRegistry.js index eaec419a..17fd1971 100644 --- a/src/core/SpyRegistry.js +++ b/src/core/SpyRegistry.js @@ -120,6 +120,23 @@ getJasmineRequireObj().SpyRegistry = function(j$) { return spy; }; + this.spyOnAllFunctions = function(obj) { + if (j$.util.isUndefined(obj)) { + throw new Error('spyOnAllFunctions could not find an object to spy upon'); + } + + for (var prop in obj) { + if (Object.prototype.hasOwnProperty.call(obj, prop) && obj[prop] instanceof Function) { + var descriptor = Object.getOwnPropertyDescriptor(obj, prop); + if ((descriptor.writable || descriptor.set) && descriptor.configurable) { + this.spyOn(obj, prop); + } + } + } + + return obj; + }; + this.clearSpies = function() { var spies = currentSpies(); for (var i = spies.length - 1; i >= 0; i--) { diff --git a/src/core/Suite.js b/src/core/Suite.js index 7ea2c168..c85f10c7 100644 --- a/src/core/Suite.js +++ b/src/core/Suite.js @@ -5,6 +5,7 @@ getJasmineRequireObj().Suite = function(j$) { this.parentSuite = attrs.parentSuite; this.description = attrs.description; this.expectationFactory = attrs.expectationFactory; + this.asyncExpectationFactory = attrs.asyncExpectationFactory; this.expectationResultFactory = attrs.expectationResultFactory; this.throwOnExpectationFailure = !!attrs.throwOnExpectationFailure; @@ -37,6 +38,10 @@ getJasmineRequireObj().Suite = function(j$) { return this.expectationFactory(actual, this); }; + Suite.prototype.expectAsync = function(actual) { + return this.asyncExpectationFactory(actual, this); + }; + Suite.prototype.getFullName = function() { var fullName = []; for (var parentSuite = this; parentSuite; parentSuite = parentSuite.parentSuite) { diff --git a/src/core/TreeProcessor.js b/src/core/TreeProcessor.js index 43ca9c5b..d17b5d55 100644 --- a/src/core/TreeProcessor.js +++ b/src/core/TreeProcessor.js @@ -174,8 +174,11 @@ getJasmineRequireObj().TreeProcessor = function() { queueRunnerFactory({ onComplete: function () { + var args = Array.prototype.slice.call(arguments, [0]); node.cleanupBeforeAfter(); - nodeComplete(node, node.getResult(), done); + nodeComplete(node, node.getResult(), function() { + done.apply(undefined, args); + }); }, queueableFns: [onStart].concat(wrapChildren(node, segmentNumber)), userContext: node.sharedUserContext(), diff --git a/src/core/base.js b/src/core/base.js index 2c29d4da..c843eeaf 100644 --- a/src/core/base.js +++ b/src/core/base.js @@ -120,7 +120,11 @@ getJasmineRequireObj().base = function(j$, jasmineGlobal) { }; j$.isPromise = function(obj) { - return typeof jasmineGlobal.Promise !== 'undefined' && obj.constructor === jasmineGlobal.Promise; + return typeof jasmineGlobal.Promise !== 'undefined' && !!obj && obj.constructor === jasmineGlobal.Promise; + }; + + j$.isPromiseLike = function(obj) { + return !!obj && j$.isFunction_(obj.then); }; j$.fnNameFor = function(func) { diff --git a/src/core/matchers/toBe.js b/src/core/matchers/toBe.js index bcbbb1a6..96b236a7 100644 --- a/src/core/matchers/toBe.js +++ b/src/core/matchers/toBe.js @@ -1,4 +1,4 @@ -getJasmineRequireObj().toBe = function() { +getJasmineRequireObj().toBe = function(j$) { /** * {@link expect} the actual value to be `===` to the expected value. * @function @@ -7,12 +7,20 @@ getJasmineRequireObj().toBe = function() { * @example * expect(thing).toBe(realThing); */ - function toBe() { + function toBe(util) { + var tip = ' Tip: To check for deep equality, use .toEqual() instead of .toBe().'; + return { compare: function(actual, expected) { - return { - pass: actual === expected + var result = { + pass: actual === expected, }; + + if (typeof expected === 'object') { + result.message = util.buildFailureMessage('toBe', result.pass, actual, expected) + tip; + } + + return result; } }; } diff --git a/src/core/requireCore.js b/src/core/requireCore.js index 252ba2a9..6ccacf44 100644 --- a/src/core/requireCore.js +++ b/src/core/requireCore.js @@ -39,6 +39,7 @@ var getJasmineRequireObj = (function (jasmineGlobal) { j$.ExceptionFormatter = jRequire.ExceptionFormatter(j$); j$.ExpectationFilterChain = jRequire.ExpectationFilterChain(); j$.Expectation = jRequire.Expectation(j$); + j$.AsyncExpectation = jRequire.AsyncExpectation(j$); j$.buildExpectationResult = jRequire.buildExpectationResult(); j$.JsApiReporter = jRequire.JsApiReporter(); j$.matchersUtil = jRequire.matchersUtil(j$); diff --git a/src/core/requireInterface.js b/src/core/requireInterface.js index ee43d438..c2d6de8e 100644 --- a/src/core/requireInterface.js +++ b/src/core/requireInterface.js @@ -167,6 +167,25 @@ getJasmineRequireObj().interface = function(jasmine, env) { return env.expect(actual); }, + /** + * Create an asynchronous expectation for a spec. Note that the matchers + * that are provided by an asynchronous expectation all return promises + * which must be either returned from the spec or waited for using `await` + * in order for Jasmine to associate them with the correct spec. + * @name expectAsync + * @function + * @global + * @param {Object} actual - Actual computed value to test expectations against. + * @return {async-matchers} + * @example + * await expectAsync(somePromise).toBeResolved(); + * @example + * return expectAsync(somePromise).toBeResolved(); + */ + expectAsync: function(actual) { + return env.expectAsync(actual); + }, + /** * Mark a spec as pending, expectation results will be ignored. * @name pending @@ -216,6 +235,18 @@ getJasmineRequireObj().interface = function(jasmine, env) { return env.spyOnProperty(obj, methodName, accessType); }, + /** + * Installs spies on all writable and configurable properties of an object. + * @name spyOnAllFunctions + * @function + * @global + * @param {Object} obj - The object upon which to install the {@link Spy}s + * @returns {Object} the spied object + */ + spyOnAllFunctions: function(obj) { + return env.spyOnAllFunctions(obj); + }, + jsApiReporter: new jasmine.JsApiReporter({ timer: new jasmine.Timer() }), diff --git a/src/html/HtmlReporter.js b/src/html/HtmlReporter.js index 0a2f4e5c..a5ad4c63 100644 --- a/src/html/HtmlReporter.js +++ b/src/html/HtmlReporter.js @@ -51,7 +51,7 @@ jasmineRequire.HtmlReporter = function(j$) { function HtmlReporter(options) { - var env = options.env || {}, + var config = function() { return (options.env && options.env.configuration()) || {}; }, getContainer = options.getContainer, createElement = options.createElement, createTextNode = options.createTextNode, @@ -119,7 +119,7 @@ jasmineRequire.HtmlReporter = function(j$) { } symbols.appendChild(createDom('li', { - className: noExpectations(result) ? 'jasmine-empty' : 'jasmine-' + result.status, + className: this.displaySpecInCorrectFormat(result), id: 'spec_' + result.id, title: result.fullName } @@ -132,6 +132,17 @@ jasmineRequire.HtmlReporter = function(j$) { addDeprecationWarnings(result); }; + this.displaySpecInCorrectFormat = function(result) { + return noExpectations(result) ? 'jasmine-empty' : this.resultStatus(result.status); + }; + + this.resultStatus = function(status) { + if(status === 'excluded') { + return config().hideDisabled ? 'jasmine-excluded-no-display' : 'jasmine-excluded'; + } + return 'jasmine-' + status; + }; + this.jasmineDone = function(doneResult) { var banner = find('.jasmine-banner'); var alert = find('.jasmine-alert'); @@ -139,7 +150,7 @@ jasmineRequire.HtmlReporter = function(j$) { var i; alert.appendChild(createDom('span', {className: 'jasmine-duration'}, 'finished in ' + timer.elapsed() / 1000 + 's')); - banner.appendChild(optionsMenu(env)); + banner.appendChild(optionsMenu(config())); if (stateBuilder.specsExecuted < totalSpecsDefined) { var skippedMessage = 'Ran ' + stateBuilder.specsExecuted + ' of ' + totalSpecsDefined + ' specs - run all'; @@ -299,7 +310,7 @@ jasmineRequire.HtmlReporter = function(j$) { } } - function optionsMenu(env) { + function optionsMenu(config) { var optionsMenuDom = createDom('div', { className: 'jasmine-run-options' }, createDom('span', { className: 'jasmine-trigger' }, 'Options'), createDom('div', { className: 'jasmine-payload' }, @@ -323,26 +334,39 @@ jasmineRequire.HtmlReporter = function(j$) { id: 'jasmine-random-order', type: 'checkbox' }), - createDom('label', { className: 'jasmine-label', 'for': 'jasmine-random-order' }, 'run tests in random order')) + createDom('label', { className: 'jasmine-label', 'for': 'jasmine-random-order' }, 'run tests in random order')), + createDom('div', { className: 'jasmine-hide-disabled' }, + createDom('input', { + className: 'jasmine-disabled', + id: 'jasmine-hide-disabled', + type: 'checkbox' + }), + createDom('label', { className: 'jasmine-label', 'for': 'jasmine-hide-disabled' }, 'hide disabled tests')) ) ); var failFastCheckbox = optionsMenuDom.querySelector('#jasmine-fail-fast'); - failFastCheckbox.checked = env.stoppingOnSpecFailure(); + failFastCheckbox.checked = config.failFast; failFastCheckbox.onclick = function() { - navigateWithNewParam('failFast', !env.stoppingOnSpecFailure()); + navigateWithNewParam('failFast', !config.failFast); }; var throwCheckbox = optionsMenuDom.querySelector('#jasmine-throw-failures'); - throwCheckbox.checked = env.throwingExpectationFailures(); + throwCheckbox.checked = config.oneFailurePerSpec; throwCheckbox.onclick = function() { - navigateWithNewParam('throwFailures', !env.throwingExpectationFailures()); + navigateWithNewParam('throwFailures', !config.oneFailurePerSpec); }; var randomCheckbox = optionsMenuDom.querySelector('#jasmine-random-order'); - randomCheckbox.checked = env.randomTests(); + randomCheckbox.checked = config.random; randomCheckbox.onclick = function() { - navigateWithNewParam('random', !env.randomTests()); + navigateWithNewParam('random', !config.random); + }; + + var hideDisabled = optionsMenuDom.querySelector('#jasmine-hide-disabled'); + hideDisabled.checked = config.hideDisabled; + hideDisabled.onclick = function() { + navigateWithNewParam('hideDisabled', !config.hideDisabled); }; var optionsTrigger = optionsMenuDom.querySelector('.jasmine-trigger'), diff --git a/src/html/_HTMLReporter.scss b/src/html/_HTMLReporter.scss index 3a820db5..3a2ae12d 100644 --- a/src/html/_HTMLReporter.scss +++ b/src/html/_HTMLReporter.scss @@ -16,6 +16,11 @@ $empty-color: #eff543; $neutral-color: #bababa; $jasmine-color: #8a4182; +$passing-mark: "\02022"; +$failing-mark: "\d7"; +$pending-mark: "*"; +$space: "\0020"; + $font-size: 11px; $large-font-size: 14px; @@ -120,7 +125,7 @@ body { &:before { color: $passing-color; - content: "\02022"; + content: $passing-mark; } } @@ -129,26 +134,31 @@ body { &:before { color: $failing-color; - content: "\d7"; + content: $failing-mark; font-weight: bold; margin-left: -1px; } } - &.jasmine-excluded { + &.jasmine-excluded { font-size: 14px; &:before { color: $neutral-color; - content: "\02022"; + content: $passing-mark; } } + &.jasmine-excluded-no-display { + font-size: 14px; + display: none; + } + &.jasmine-pending { line-height: 17px; &:before { color: $pending-color; - content: "*"; + content: $pending-mark; } } @@ -157,7 +167,7 @@ body { &:before { color: $pending-color; - content: "\02022"; + content: $passing-mark; } } } @@ -294,7 +304,31 @@ body { &.jasmine-excluded a { color: $neutral-color; + } + } + } + + .jasmine-specs { + li { + &.jasmine-passed a:before { + content: $passing-mark + $space; } + + &.jasmine-failed a:before { + content: $failing-mark + $space; + } + + &.jasmine-empty a:before { + content: $pending-mark + $space; + } + + &.jasmine-pending a:before { + content: $passing-mark + $space; + } + + &.jasmine-excluded a:before { + content: $passing-mark + $space; + } } }