From 6a2a30d540fe57528f8f66da243c02dfe5180aab Mon Sep 17 00:00:00 2001 From: Steve Gravrock Date: Mon, 24 May 2021 20:38:26 -0700 Subject: [PATCH 1/2] Improved & unified deprecation handling * De-duplication now happens in core, not in reporters. This ensures that the console doesn't get flooded. * Stack traces are opt-out, not opt-in. * The current runnable is not reported or logged for certain deprecations where it's irrelevant. * HtmlReporter shows stack traces in expandable widgets. * Env#deprecated and Env#deprecatedOnceWithStack are merged. --- lib/jasmine-core/jasmine-html.js | 80 ++++- lib/jasmine-core/jasmine.css | 19 + lib/jasmine-core/jasmine.js | 262 ++++++++++---- spec/core/DeprecatorSpec.js | 329 ++++++++++++++++++ spec/core/EnvSpec.js | 54 --- spec/core/QueueRunnerSpec.js | 6 +- .../integration/CustomAsyncMatchersSpec.js | 3 +- spec/core/integration/CustomMatchersSpec.js | 3 +- spec/core/integration/DeprecationSpec.js | 322 +++++++++++++++++ spec/core/integration/EnvSpec.js | 64 ---- spec/html/HtmlReporterSpec.js | 122 +++++++ src/core/Deprecator.js | 90 +++++ src/core/Env.js | 128 ++++--- src/core/ExpectationResult.js | 2 +- src/core/QueueRunner.js | 30 +- src/core/matchers/matchersUtil.js | 6 +- src/core/requireCore.js | 5 +- src/html/HtmlReporter.js | 80 ++++- src/html/_HTMLReporter.scss | 25 ++ 19 files changed, 1328 insertions(+), 302 deletions(-) create mode 100644 spec/core/DeprecatorSpec.js create mode 100644 spec/core/integration/DeprecationSpec.js create mode 100644 src/core/Deprecator.js diff --git a/lib/jasmine-core/jasmine-html.js b/lib/jasmine-core/jasmine-html.js index 8539c76c..ca67c293 100644 --- a/lib/jasmine-core/jasmine-html.js +++ b/lib/jasmine-core/jasmine-html.js @@ -308,7 +308,8 @@ jasmineRequire.HtmlReporter = function(j$) { addDeprecationWarnings(doneResult); for (i = 0; i < deprecationWarnings.length; i++) { - var context; + var children = [], + context; switch (deprecationWarnings[i].runnableType) { case 'spec': @@ -321,13 +322,23 @@ jasmineRequire.HtmlReporter = function(j$) { context = ''; } + deprecationWarnings[i].message.split('\n').forEach(function(line) { + children.push(line); + children.push(createDom('br')); + }); + + children[0] = 'DEPRECATION: ' + children[0]; + children.push(context); + + if (deprecationWarnings[i].stack) { + children.push(createExpander(deprecationWarnings[i].stack)); + } + alert.appendChild( createDom( 'span', { className: 'jasmine-bar jasmine-warning' }, - 'DEPRECATION: ' + deprecationWarnings[i].message, - createDom('br'), - context + children ) ); } @@ -642,17 +653,44 @@ jasmineRequire.HtmlReporter = function(j$) { if (result && result.deprecationWarnings) { for (var i = 0; i < result.deprecationWarnings.length; i++) { var warning = result.deprecationWarnings[i].message; - if (!j$.util.arrayContains(warning)) { - deprecationWarnings.push({ - message: warning, - runnableName: result.fullName, - runnableType: runnableType - }); - } + deprecationWarnings.push({ + message: warning, + stack: result.deprecationWarnings[i].stack, + runnableName: result.fullName, + runnableType: runnableType + }); } } } + function createExpander(stackTrace) { + var expandLink = createDom('a', { href: '#' }, 'Show stack trace'); + var root = createDom( + 'div', + { className: 'jasmine-expander' }, + expandLink, + createDom( + 'div', + { className: 'jasmine-expander-contents jasmine-stack-trace' }, + stackTrace + ) + ); + + expandLink.addEventListener('click', function(e) { + e.preventDefault(); + + if (root.classList.contains('jasmine-expanded')) { + root.classList.remove('jasmine-expanded'); + expandLink.textContent = 'Show stack trace'; + } else { + root.classList.add('jasmine-expanded'); + expandLink.textContent = 'Hide stack trace'; + } + }); + + return root; + } + function find(selector) { return getContainer().querySelector('.jasmine_html-reporter ' + selector); } @@ -666,11 +704,23 @@ jasmineRequire.HtmlReporter = function(j$) { } } - function createDom(type, attrs, childrenVarArgs) { - var el = createElement(type); + function createDom(type, attrs, childrenArrayOrVarArgs) { + var el = createElement(type), + children, + i; - for (var i = 2; i < arguments.length; i++) { - var child = arguments[i]; + if (j$.isArray_(childrenArrayOrVarArgs)) { + children = childrenArrayOrVarArgs; + } else { + children = []; + + for (i = 2; i < arguments.length; i++) { + children.push(arguments[i]); + } + } + + for (i = 0; i < children.length; i++) { + var child = children[i]; if (typeof child === 'string') { el.appendChild(createTextNode(child)); diff --git a/lib/jasmine-core/jasmine.css b/lib/jasmine-core/jasmine.css index dba3ca98..18b6457c 100644 --- a/lib/jasmine-core/jasmine.css +++ b/lib/jasmine-core/jasmine.css @@ -165,6 +165,8 @@ body { background-color: #bababa; } .jasmine_html-reporter .jasmine-bar.jasmine-warning { + margin-top: 14px; + margin-bottom: 14px; background-color: #ba9d37; color: #333; } @@ -268,4 +270,21 @@ body { border: 1px solid #ddd; background: white; white-space: pre; +} +.jasmine_html-reporter .jasmine-expander a { + display: block; + margin-left: 14px; + color: blue; + text-decoration: underline; +} +.jasmine_html-reporter .jasmine-expander-contents { + display: none; +} +.jasmine_html-reporter .jasmine-expanded { + padding-bottom: 10px; +} +.jasmine_html-reporter .jasmine-expanded .jasmine-expander-contents { + display: block; + margin-left: 14px; + padding: 5px; } \ No newline at end of file diff --git a/lib/jasmine-core/jasmine.js b/lib/jasmine-core/jasmine.js index 15b3193c..fe04d065 100644 --- a/lib/jasmine-core/jasmine.js +++ b/lib/jasmine-core/jasmine.js @@ -64,6 +64,7 @@ var getJasmineRequireObj = (function(jasmineGlobal) { j$.getClearStack = jRequire.clearStack(j$); j$.Clock = jRequire.Clock(); j$.DelayedFunctionScheduler = jRequire.DelayedFunctionScheduler(j$); + j$.Deprecator = jRequire.Deprecator(j$); j$.Env = jRequire.Env(j$); j$.deprecatingThisProxy = jRequire.deprecatingThisProxy(j$); j$.StackTrace = jRequire.StackTrace(j$); @@ -80,7 +81,7 @@ var getJasmineRequireObj = (function(jasmineGlobal) { j$.basicPrettyPrinter_ = j$.makePrettyPrinter(); Object.defineProperty(j$, 'pp', { get: function() { - j$.getEnv().deprecatedOnceWithStack( + j$.getEnv().deprecated( 'jasmine.pp is deprecated and will be removed in a future release. ' + 'Use the pp method of the matchersUtil passed to the matcher factory ' + "or the asymmetric equality tester's `asymmetricMatch` method " + @@ -97,7 +98,7 @@ var getJasmineRequireObj = (function(jasmineGlobal) { }); Object.defineProperty(j$, 'matchersUtil', { get: function() { - j$.getEnv().deprecatedOnceWithStack( + j$.getEnv().deprecated( 'jasmine.matchersUtil is deprecated and will be removed ' + 'in a future release. Use the instance passed to the matcher factory or ' + "the asymmetric equality tester's `asymmetricMatch` method instead. " + @@ -1035,7 +1036,6 @@ getJasmineRequireObj().Env = function(j$) { var currentlyExecutingSuites = []; var currentDeclarationSuite = null; var hasFailures = false; - var deprecationsToSuppress = []; /** * This represents the available options to configure Jasmine. @@ -1231,6 +1231,7 @@ getJasmineRequireObj().Env = function(j$) { if (configuration.hasOwnProperty('verboseDeprecations')) { config.verboseDeprecations = configuration.verboseDeprecations; + deprecator.verboseDeprecations(config.verboseDeprecations); } }; @@ -1252,13 +1253,19 @@ getJasmineRequireObj().Env = function(j$) { Object.defineProperty(this, 'specFilter', { get: function() { self.deprecated( - 'Getting specFilter directly from Env is deprecated and will be removed in a future version of Jasmine, please check the specFilter option from `configuration`' + 'Getting specFilter directly from Env is deprecated and will be ' + + 'removed in a future version of Jasmine. Please check the ' + + 'specFilter option from `configuration` instead.', + { ignoreRunnable: true } ); return config.specFilter; }, set: function(val) { self.deprecated( - 'Setting specFilter directly on Env is deprecated and will be removed in a future version of Jasmine, please use the specFilter option in `configure`' + 'Setting specFilter directly on Env is deprecated and will be ' + + 'removed in a future version of Jasmine. Please use the ' + + 'specFilter option in `configure` instead.', + { ignoreRunnable: true } ); config.specFilter = val; } @@ -1306,7 +1313,7 @@ getJasmineRequireObj().Env = function(j$) { for (var matcherName in matchersToAdd) { if (matchersToAdd[matcherName].length > 1) { - self.deprecatedOnceWithStack( + self.deprecated( 'The matcher factory for "' + matcherName + '" ' + @@ -1331,7 +1338,7 @@ getJasmineRequireObj().Env = function(j$) { for (var matcherName in matchersToAdd) { if (matchersToAdd[matcherName].length > 1) { - self.deprecatedOnceWithStack( + self.deprecated( 'The matcher factory for "' + matcherName + '" ' + @@ -1538,14 +1545,21 @@ getJasmineRequireObj().Env = function(j$) { */ this.throwOnExpectationFailure = function(value) { this.deprecated( - 'Setting throwOnExpectationFailure directly on Env is deprecated and will be removed in a future version of Jasmine, please use the oneFailurePerSpec option in `configure`' + 'Setting throwOnExpectationFailure directly on Env is deprecated ' + + 'and will be removed in a future version of Jasmine. Please use the ' + + 'oneFailurePerSpec option in `configure` instead.', + { ignoreRunnable: true } ); this.configure({ oneFailurePerSpec: !!value }); }; this.throwingExpectationFailures = function() { this.deprecated( - 'Getting throwingExpectationFailures directly from Env is deprecated and will be removed in a future version of Jasmine, please check the oneFailurePerSpec option from `configuration`' + 'Getting throwingExpectationFailures directly from Env is ' + + 'deprecated and will be removed in a future version of Jasmine. ' + + 'Please check the oneFailurePerSpec option from `configuration` ' + + 'instead.', + { ignoreRunnable: true } ); return config.oneFailurePerSpec; }; @@ -1560,14 +1574,20 @@ getJasmineRequireObj().Env = function(j$) { */ this.stopOnSpecFailure = function(value) { this.deprecated( - 'Setting stopOnSpecFailure directly is deprecated and will be removed in a future version of Jasmine, please use the failFast option in `configure`' + 'Setting stopOnSpecFailure directly is deprecated and will be ' + + 'removed in a future version of Jasmine. Please use the failFast ' + + 'option in `configure` instead.', + { ignoreRunnable: true } ); this.configure({ failFast: !!value }); }; this.stoppingOnSpecFailure = function() { this.deprecated( - 'Getting stoppingOnSpecFailure directly from Env is deprecated and will be removed in a future version of Jasmine, please check the failFast option from `configuration`' + 'Getting stoppingOnSpecFailure directly from Env is deprecated ' + + 'and will be removed in a future version of Jasmine. Please check ' + + 'the failFast option from `configuration` instead.', + { ignoreRunnable: true } ); return config.failFast; }; @@ -1582,14 +1602,20 @@ getJasmineRequireObj().Env = function(j$) { */ this.randomizeTests = function(value) { this.deprecated( - 'Setting randomizeTests directly is deprecated and will be removed in a future version of Jasmine, please use the random option in `configure`' + 'Setting randomizeTests directly is deprecated and will be removed ' + + 'in a future version of Jasmine. Please use the random option in ' + + '`configure` instead.', + { ignoreRunnable: true } ); config.random = !!value; }; this.randomTests = function() { this.deprecated( - 'Getting randomTests directly from Env is deprecated and will be removed in a future version of Jasmine, please check the random option from `configuration`' + 'Getting randomTests directly from Env is deprecated and will be ' + + 'removed in a future version of Jasmine. Please check the random ' + + 'option from `configuration` instead.', + { ignoreRunnable: true } ); return config.random; }; @@ -1604,7 +1630,10 @@ getJasmineRequireObj().Env = function(j$) { */ this.seed = function(value) { this.deprecated( - 'Setting seed directly is deprecated and will be removed in a future version of Jasmine, please use the seed option in `configure`' + 'Setting seed directly is deprecated and will be removed in a ' + + 'future version of Jasmine. Please use the seed option in ' + + '`configure` instead.', + { ignoreRunnable: true } ); if (value) { config.seed = value; @@ -1614,7 +1643,10 @@ getJasmineRequireObj().Env = function(j$) { this.hidingDisabled = function(value) { this.deprecated( - 'Getting hidingDisabled directly from Env is deprecated and will be removed in a future version of Jasmine, please check the hideDisabled option from `configuration`' + 'Getting hidingDisabled directly from Env is deprecated and will ' + + 'be removed in a future version of Jasmine. Please check the ' + + 'hideDisabled option from `configuration` instead.', + { ignoreRunnable: true } ); return config.hideDisabled; }; @@ -1626,53 +1658,39 @@ getJasmineRequireObj().Env = function(j$) { */ this.hideDisabled = function(value) { this.deprecated( - 'Setting hideDisabled directly is deprecated and will be removed in a future version of Jasmine, please use the hideDisabled option in `configure`' + 'Setting hideDisabled directly is deprecated and will be removed ' + + 'in a future version of Jasmine. Please use the hideDisabled option ' + + 'in `configure` instead.', + { ignoreRunnable: true } ); config.hideDisabled = !!value; }; - this.deprecated = function(deprecation) { + /** + * Causes a deprecation warning to be logged to the console and reported to + * reporters. + * + * The optional second parameter is an object that can have either of the + * following properties: + * + * omitStackTrace: Whether to omit the stack trace. Optional. Defaults to + * false. This option is ignored if the deprecation is an Error. Set this + * when the stack trace will not contain anything that helps the user find + * the source of the deprecation. + * + * ignoreRunnable: Whether to log the deprecation on the root suite, ignoring + * the spec or suite that's running when it happens. Optional. Defaults to + * false. + * + * @name Env#deprecated + * @since 2.99 + * @function + * @param {String|Error} deprecation The deprecation message + * @param {Object} [options] Optional extra options, as described above + */ + this.deprecated = function(deprecation, options) { var runnable = currentRunnable() || topSuite; - var context; - - if (runnable === topSuite) { - context = ''; - } else if (runnable === currentSuite()) { - context = ' (in suite: ' + runnable.getFullName() + ')'; - } else { - context = ' (in spec: ' + runnable.getFullName() + ')'; - } - - runnable.addDeprecationWarning(deprecation); - if ( - typeof console !== 'undefined' && - typeof console.error === 'function' - ) { - console.error('DEPRECATION: ' + deprecation + context); - } - }; - - this.deprecatedOnceWithStack = function(deprecation) { - var formatter = new j$.ExceptionFormatter(), - stackTrace = formatter - .stack(j$.util.errorWithStack()) - .replace(/^Error\n/m, ''); - - if (config.verboseDeprecations) { - this.deprecated(deprecation + '\n' + stackTrace); - } else { - if (deprecationsToSuppress.indexOf(deprecation) === -1) { - this.deprecated( - deprecation + - '\n' + - 'Note: This message will be shown only once. ' + - 'Set config.verboseDeprecations to true to see every occurrence.\n' + - stackTrace - ); - } - - deprecationsToSuppress.push(deprecation); - } + deprecator.addDeprecationWarning(runnable, deprecation, options); }; var queueRunnerFactory = function(options, args) { @@ -1708,6 +1726,7 @@ getJasmineRequireObj().Env = function(j$) { asyncExpectationFactory: suiteAsyncExpectationFactory, expectationResultFactory: expectationResultFactory }); + var deprecator = new j$.Deprecator(topSuite); defaultResourcesForRunnable(topSuite.id); currentDeclarationSuite = topSuite; @@ -3707,6 +3726,97 @@ getJasmineRequireObj().deprecatingThisProxy = function(j$) { }; }; +getJasmineRequireObj().Deprecator = function(j$) { + function Deprecator(topSuite) { + this.topSuite_ = topSuite; + this.verbose_ = false; + this.toSuppress_ = []; + } + + var verboseNote = + 'Note: This message will be shown only once. ' + + 'Set config.verboseDeprecations to true to see every occurrence.'; + + Deprecator.prototype.verboseDeprecations = function(enabled) { + this.verbose_ = enabled; + }; + + // runnable is a spec or a suite. + // deprecation is a string or an Error. + // See Env#deprecated for a description of the options argument. + Deprecator.prototype.addDeprecationWarning = function( + runnable, + deprecation, + options + ) { + options = options || {}; + + if (!this.verbose_ && !j$.isError_(deprecation)) { + if (this.toSuppress_.indexOf(deprecation) !== -1) { + return; + } + this.toSuppress_.push(deprecation); + } + + this.log_(runnable, deprecation, options); + this.report_(runnable, deprecation, options); + }; + + Deprecator.prototype.log_ = function(runnable, deprecation, options) { + var context; + + if (j$.isError_(deprecation)) { + console.error(deprecation); + return; + } + + if (runnable === this.topSuite_ || options.ignoreRunnable) { + context = ''; + } else if (runnable.children) { + context = ' (in suite: ' + runnable.getFullName() + ')'; + } else { + context = ' (in spec: ' + runnable.getFullName() + ')'; + } + + if (!options.omitStackTrace) { + context += '\n' + this.stackTrace_(); + } + + if (!this.verbose_) { + context += '\n' + verboseNote; + } + + console.error('DEPRECATION: ' + deprecation + context); + }; + + Deprecator.prototype.stackTrace_ = function() { + var formatter = new j$.ExceptionFormatter(); + return formatter.stack(j$.util.errorWithStack()).replace(/^Error\n/m, ''); + }; + + Deprecator.prototype.report_ = function(runnable, deprecation, options) { + if (options.ignoreRunnable) { + runnable = this.topSuite_; + } + + if (j$.isError_(deprecation)) { + runnable.addDeprecationWarning(deprecation); + return; + } + + if (!this.verbose_) { + deprecation += '\n' + verboseNote; + } + + runnable.addDeprecationWarning({ + message: deprecation, + omitStackTrace: options.omitStackTrace || false + }); + }; + + return Deprecator; +}; + getJasmineRequireObj().errors = function() { function ExpectationFailed() {} @@ -4119,7 +4229,7 @@ getJasmineRequireObj().buildExpectationResult = function(j$) { var result = { matcherName: options.matcherName, message: message(), - stack: stack(), + stack: options.omitStackTrace ? '' : stack(), passed: options.passed }; @@ -4917,7 +5027,7 @@ getJasmineRequireObj().MatchersUtil = function(j$) { */ MatchersUtil.prototype.contains = function(haystack, needle, customTesters) { if (customTesters) { - j$.getEnv().deprecatedOnceWithStack( + j$.getEnv().deprecated( 'Passing custom equality testers ' + 'to MatchersUtil#contains is deprecated. ' + 'See for details.' @@ -5054,7 +5164,7 @@ getJasmineRequireObj().MatchersUtil = function(j$) { diffBuilder = customTestersOrDiffBuilder; } else { if (customTestersOrDiffBuilder) { - j$.getEnv().deprecatedOnceWithStack( + j$.getEnv().deprecated( 'Passing custom equality testers ' + 'to MatchersUtil#equals is deprecated. ' + 'See for details.' @@ -5062,7 +5172,7 @@ getJasmineRequireObj().MatchersUtil = function(j$) { } if (diffBuilderOrNothing) { - j$.getEnv().deprecatedOnceWithStack( + j$.getEnv().deprecated( 'Diff builder should be passed ' + 'as the third argument to MatchersUtil#equals, not the fourth. ' + 'See for details.' @@ -7894,25 +8004,29 @@ getJasmineRequireObj().QueueRunner = function(j$) { }; QueueRunner.prototype.diagnoseConflictingAsync_ = function(fn, retval) { + var msg; + if (retval && j$.isFunction_(retval.then)) { - // Issue a warning that matches the user's code + // Issue a warning that matches the user's code. + // Omit the stack trace because there's almost certainly no user code + // on the stack at this point. if (j$.isAsyncFunction_(fn)) { - this.deprecated( + msg = 'An asynchronous before/it/after ' + - 'function was defined with the async keyword but also took a ' + - 'done callback. This is not supported and will stop working in' + - ' the future. Either remove the done callback (recommended) or ' + - 'remove the async keyword.' - ); + 'function was defined with the async keyword but also took a ' + + 'done callback. This is not supported and will stop working in' + + ' the future. Either remove the done callback (recommended) or ' + + 'remove the async keyword.'; } else { - this.deprecated( + msg = 'An asynchronous before/it/after ' + - 'function took a done callback but also returned a promise. ' + - 'This is not supported and will stop working in the future. ' + - 'Either remove the done callback (recommended) or change the ' + - 'function to not return a promise.' - ); + 'function took a done callback but also returned a promise. ' + + 'This is not supported and will stop working in the future. ' + + 'Either remove the done callback (recommended) or change the ' + + 'function to not return a promise.'; } + + this.deprecated(msg, { omitStackTrace: true }); } }; diff --git a/spec/core/DeprecatorSpec.js b/spec/core/DeprecatorSpec.js new file mode 100644 index 00000000..8e4bbe75 --- /dev/null +++ b/spec/core/DeprecatorSpec.js @@ -0,0 +1,329 @@ +/* eslint no-console: 0 */ +describe('Deprecator', function() { + describe('#deprecate', function() { + beforeEach(function() { + spyOn(console, 'error'); + }); + + it('logs the mesage without context when the runnable is the top suite', function() { + var runnable = { addDeprecationWarning: function() {} }; + var deprecator = new jasmineUnderTest.Deprecator(runnable); + deprecator.verboseDeprecations(true); + + deprecator.addDeprecationWarning(runnable, 'the message', { + omitStackTrace: true + }); + + expect(console.error).toHaveBeenCalledWith('DEPRECATION: the message'); + }); + + it('logs the message in a descendant suite', function() { + var runnable = { + addDeprecationWarning: function() {}, + getFullName: function() { + return 'the suite'; + }, + children: [] + }; + var deprecator = new jasmineUnderTest.Deprecator({}); + deprecator.verboseDeprecations(true); + + deprecator.addDeprecationWarning(runnable, 'the message', { + omitStackTrace: true + }); + + expect(console.error).toHaveBeenCalledWith( + 'DEPRECATION: the message (in suite: the suite)' + ); + }); + + it('logs and reports the message in a spec', function() { + var runnable = { + addDeprecationWarning: function() {}, + getFullName: function() { + return 'the spec'; + } + }; + var deprecator = new jasmineUnderTest.Deprecator({}); + deprecator.verboseDeprecations(true); + + deprecator.addDeprecationWarning(runnable, 'the message', { + omitStackTrace: true + }); + + expect(console.error).toHaveBeenCalledWith( + 'DEPRECATION: the message (in spec: the spec)' + ); + }); + + it('logs and reports the message without runnable info when ignoreRunnable is true', function() { + var topSuite = jasmine.createSpyObj('topSuite', [ + 'addDeprecationWarning', + 'getFullName' + ]); + var deprecator = new jasmineUnderTest.Deprecator(topSuite); + var runnable = jasmine.createSpyObj('spec', [ + 'addDeprecationWarning', + 'getFullName' + ]); + runnable.getFullName.and.returnValue('a spec'); + + deprecator.addDeprecationWarning(runnable, 'the message', { + ignoreRunnable: true + }); + + expect(topSuite.addDeprecationWarning).toHaveBeenCalledWith( + jasmine.objectContaining({ + message: jasmine.stringMatching(/^the message/) + }) + ); + expect(runnable.addDeprecationWarning).not.toHaveBeenCalled(); + expect(console.error).toHaveBeenCalledWith( + jasmine.stringMatching(/the message/) + ); + expect(console.error).not.toHaveBeenCalledWith( + jasmine.stringMatching(/a spec/) + ); + }); + + describe('with no options', function() { + it('includes the stack trace', function() { + testStackTrace(undefined); + }); + }); + + it('omits the stack trace when omitStackTrace is true', function() { + testNoStackTrace({ omitStackTrace: true }); + }); + + it('includes the stack trace when omitStackTrace is false', function() { + testStackTrace({ omitStackTrace: false }); + }); + + it('includes the stack trace when omitStackTrace is undefined', function() { + testStackTrace({ includeStackTrace: undefined }); + }); + + it('emits the deprecation only once when verboseDeprecations is not set', function() { + var deprecator = new jasmineUnderTest.Deprecator({}); + var runnable1 = jasmine.createSpyObj('runnable1', [ + 'addDeprecationWarning', + 'getFullName' + ]); + var runnable2 = jasmine.createSpyObj('runnable2', [ + 'addDeprecationWarning', + 'getFullName' + ]); + + deprecator.addDeprecationWarning(runnable1, 'the message'); + deprecator.addDeprecationWarning(runnable1, 'the message'); + deprecator.addDeprecationWarning(runnable2, 'the message'); + + expect(runnable1.addDeprecationWarning).toHaveBeenCalledTimes(1); + expect(runnable2.addDeprecationWarning).not.toHaveBeenCalled(); + }); + + it('emits the deprecation only once when verboseDeprecations is false', function() { + var deprecator = new jasmineUnderTest.Deprecator({}); + var runnable1 = jasmine.createSpyObj('runnable1', [ + 'addDeprecationWarning', + 'getFullName' + ]); + var runnable2 = jasmine.createSpyObj('runnable2', [ + 'addDeprecationWarning', + 'getFullName' + ]); + + deprecator.verboseDeprecations(false); + deprecator.addDeprecationWarning(runnable1, 'the message'); + deprecator.addDeprecationWarning(runnable1, 'the message'); + deprecator.addDeprecationWarning(runnable2, 'the message'); + + expect(runnable1.addDeprecationWarning).toHaveBeenCalledTimes(1); + expect(runnable2.addDeprecationWarning).not.toHaveBeenCalled(); + }); + + it('emits the deprecation for each call when verboseDeprecations is true', function() { + var deprecator = new jasmineUnderTest.Deprecator({}); + var runnable1 = jasmine.createSpyObj('runnable1', [ + 'addDeprecationWarning', + 'getFullName' + ]); + var runnable2 = jasmine.createSpyObj('runnable2', [ + 'addDeprecationWarning', + 'getFullName' + ]); + + deprecator.verboseDeprecations(true); + deprecator.addDeprecationWarning(runnable1, 'the message'); + deprecator.addDeprecationWarning(runnable1, 'the message'); + deprecator.addDeprecationWarning(runnable2, 'the message'); + + expect(runnable1.addDeprecationWarning).toHaveBeenCalledTimes(2); + expect(runnable2.addDeprecationWarning).toHaveBeenCalled(); + }); + + it('includes a note about verboseDeprecations', function() { + var deprecator = new jasmineUnderTest.Deprecator({}); + var runnable = jasmine.createSpyObj('runnable', [ + 'addDeprecationWarning', + 'getFullName' + ]); + + deprecator.addDeprecationWarning(runnable, 'the message'); + + expect(runnable.addDeprecationWarning).toHaveBeenCalledTimes(1); + expect( + runnable.addDeprecationWarning.calls.argsFor(0)[0].message + ).toContain(verboseDeprecationsNote()); + expect(console.error).toHaveBeenCalledTimes(1); + expect(console.error.calls.argsFor(0)[0]).toContain( + verboseDeprecationsNote() + ); + }); + + it('omits the note about verboseDeprecations when verboseDeprecations is true', function() { + var deprecator = new jasmineUnderTest.Deprecator({}); + var runnable = jasmine.createSpyObj('runnable', [ + 'addDeprecationWarning', + 'getFullName' + ]); + + deprecator.verboseDeprecations(true); + deprecator.addDeprecationWarning(runnable, 'the message'); + + expect(runnable.addDeprecationWarning).toHaveBeenCalledTimes(1); + expect( + runnable.addDeprecationWarning.calls.argsFor(0)[0].message + ).not.toContain(verboseDeprecationsNote()); + expect(console.error).toHaveBeenCalledTimes(1); + expect(console.error.calls.argsFor(0)[0]).not.toContain( + verboseDeprecationsNote() + ); + }); + + describe('When the deprecation is an Error', function() { + // This form is used by external systems like atom-jasmine3-test-runner + // to report their own deprecations through Jasmine. See + // . + it('passes the error through unchanged', function() { + var deprecator = new jasmineUnderTest.Deprecator({}); + var runnable = jasmine.createSpyObj('runnable', [ + 'addDeprecationWarning', + 'getFullName' + ]); + var exceptionFormatter = new jasmineUnderTest.ExceptionFormatter(); + var deprecation, originalStack; + + try { + throw new Error('the deprecation'); + } catch (err) { + deprecation = err; + originalStack = err.stack; + } + + deprecator.addDeprecationWarning(runnable, deprecation); + + expect(runnable.addDeprecationWarning).toHaveBeenCalledTimes(1); + expect( + runnable.addDeprecationWarning.calls.argsFor(0)[0].message + ).toEqual('the deprecation'); + expect(runnable.addDeprecationWarning.calls.argsFor(0)[0].stack).toBe( + originalStack + ); + expect(console.error).toHaveBeenCalledTimes(1); + expect(console.error.calls.argsFor(0)[0].message).toEqual( + 'the deprecation' + ); + expect(console.error.calls.argsFor(0)[0].stack).toEqual(originalStack); + }); + + it('reports the deprecation every time, regardless of config.verboseDeprecations', function() { + var deprecator = new jasmineUnderTest.Deprecator({}); + var runnable = jasmine.createSpyObj('runnable', [ + 'addDeprecationWarning', + 'getFullName' + ]); + var deprecation; + + try { + throw new Error('the deprecation'); + } catch (err) { + deprecation = err; + } + + deprecator.addDeprecationWarning(runnable, deprecation); + deprecator.addDeprecationWarning(runnable, deprecation); + + expect(runnable.addDeprecationWarning).toHaveBeenCalledTimes(2); + expect(console.error).toHaveBeenCalledTimes(2); + }); + + it('omits the note about verboseDeprecations', function() { + var deprecator = new jasmineUnderTest.Deprecator({}); + var runnable = jasmine.createSpyObj('runnable', [ + 'addDeprecationWarning', + 'getFullName' + ]); + var deprecation; + + try { + throw new Error('the deprecation'); + } catch (err) { + deprecation = err; + } + + deprecator.addDeprecationWarning(runnable, deprecation); + + expect(runnable.addDeprecationWarning).toHaveBeenCalledTimes(1); + expect( + runnable.addDeprecationWarning.calls.argsFor(0)[0].message + ).not.toContain(verboseDeprecationsNote()); + expect(console.error).toHaveBeenCalledTimes(1); + expect(console.error.calls.argsFor(0)[0]).not.toContain( + verboseDeprecationsNote() + ); + }); + }); + + function verboseDeprecationsNote() { + return ( + 'Note: This message will be shown only once. Set ' + + 'config.verboseDeprecations to true to see every occurrence.' + ); + } + + function testStackTrace(options) { + var deprecator = new jasmineUnderTest.Deprecator({}); + var runnable = jasmine.createSpyObj('runnable', [ + 'addDeprecationWarning', + 'getFullName' + ]); + + deprecator.addDeprecationWarning(runnable, 'the message', options); + + expect(runnable.addDeprecationWarning).toHaveBeenCalledWith({ + message: jasmine.stringMatching(/^the message/), + omitStackTrace: false + }); + expect(console.error).toHaveBeenCalledTimes(1); + expect(console.error.calls.argsFor(0)[0]).toContain('the message'); + expect(console.error.calls.argsFor(0)[0]).toContain('DeprecatorSpec.js'); + } + + function testNoStackTrace(options) { + var deprecator = new jasmineUnderTest.Deprecator({}); + var runnable = jasmine.createSpyObj('runnable', [ + 'addDeprecationWarning', + 'getFullName' + ]); + + deprecator.addDeprecationWarning(runnable, 'the message', options); + + expect(runnable.addDeprecationWarning).toHaveBeenCalledWith({ + message: jasmine.stringMatching(/^the message/), + omitStackTrace: true + }); + } + }); +}); diff --git a/spec/core/EnvSpec.js b/spec/core/EnvSpec.js index 20c3c17e..70bb24f8 100644 --- a/spec/core/EnvSpec.js +++ b/spec/core/EnvSpec.js @@ -294,60 +294,6 @@ describe('Env', function() { }); }); - describe('#deprecatedOnceWithStack', function() { - it('includes a stack trace', function() { - spyOn(env, 'deprecated'); - - env.deprecatedOnceWithStack('msg'); - - expect(env.deprecated).toHaveBeenCalled(); - var msg = env.deprecated.calls.argsFor(0)[0]; - expect(msg).toContain('msg'); - expect(msg).toContain('EnvSpec.js'); - expect(msg).not.toContain('Error'); - }); - - describe('When verboseDeprecations is true', function() { - it('calls #deprecated every time', function() { - env.configure({ verboseDeprecations: true }); - spyOn(env, 'deprecated'); - - env.deprecatedOnceWithStack('msg'); - env.deprecatedOnceWithStack('msg'); - expect(env.deprecated).toHaveBeenCalledWith( - jasmine.stringMatching(/msg/) - ); - expect(env.deprecated).toHaveBeenCalledTimes(2); - expect(env.deprecated).not.toHaveBeenCalledWith( - jasmine.stringMatching(/only once/) - ); - }); - }); - - describe('When verboseDeprecations is false', function() { - it('calls #deprecated once per unique message', function() { - env.configure({ verboseDeprecations: false }); - spyOn(env, 'deprecated'); - - env.deprecatedOnceWithStack('foo'); - env.deprecatedOnceWithStack('bar'); - env.deprecatedOnceWithStack('foo'); - - expect(env.deprecated).toHaveBeenCalledWith( - jasmine.stringMatching( - /foo\nNote: This message will be shown only once. Set config.verboseDeprecations to true to see every occurrence/m - ) - ); - expect(env.deprecated).toHaveBeenCalledWith( - jasmine.stringMatching( - /bar\nNote: This message will be shown only once. Set config.verboseDeprecations to true to see every occurrence/m - ) - ); - expect(env.deprecated).toHaveBeenCalledTimes(2); - }); - }); - }); - describe('when not constructed with suppressLoadErrors: true', function() { it('installs a global error handler on construction', function() { var globalErrors = jasmine.createSpyObj('globalErrors', [ diff --git a/spec/core/QueueRunnerSpec.js b/spec/core/QueueRunnerSpec.js index 7333b02a..9615a2cf 100644 --- a/spec/core/QueueRunnerSpec.js +++ b/spec/core/QueueRunnerSpec.js @@ -533,7 +533,8 @@ describe('QueueRunner', function() { 'before/it/after function took a done callback but also returned a ' + 'promise. This is not supported and will stop working in the future. ' + 'Either remove the done callback (recommended) or change the function ' + - 'to not return a promise.' + 'to not return a promise.', + { omitStackTrace: true } ); }); @@ -553,7 +554,8 @@ describe('QueueRunner', function() { 'before/it/after function was defined with the async keyword but ' + 'also took a done callback. This is not supported and will stop ' + 'working in the future. Either remove the done callback ' + - '(recommended) or remove the async keyword.' + '(recommended) or remove the async keyword.', + { omitStackTrace: true } ); }); }); diff --git a/spec/core/integration/CustomAsyncMatchersSpec.js b/spec/core/integration/CustomAsyncMatchersSpec.js index cb30216d..5cc16c82 100644 --- a/spec/core/integration/CustomAsyncMatchersSpec.js +++ b/spec/core/integration/CustomAsyncMatchersSpec.js @@ -202,7 +202,7 @@ describe('Custom Async Matchers (Integration)', function() { env.execute(null, done); }); - it('logs a deprecation once per matcher if the matcher factory takes two arguments', function(done) { + it('logs a distinct deprecation for each matcher if the matcher factory takes two arguments', function(done) { var matcherFactory = function(matchersUtil, customEqualityTesters) { return { compare: function() {} }; }; @@ -232,7 +232,6 @@ describe('Custom Async Matchers (Integration)', function() { 'See for details.' ) ); - expect(env.deprecated).toHaveBeenCalledTimes(2); done(); } diff --git a/spec/core/integration/CustomMatchersSpec.js b/spec/core/integration/CustomMatchersSpec.js index 9b203072..3fcb0a07 100644 --- a/spec/core/integration/CustomMatchersSpec.js +++ b/spec/core/integration/CustomMatchersSpec.js @@ -350,7 +350,7 @@ describe('Custom Matchers (Integration)', function() { env.execute(null, done); }); - it('logs a deprecation once per matcher if the matcher factory takes two arguments', function(done) { + it('logs a distinct deprecation per matcher if the matcher factory takes two arguments', function(done) { var matcherFactory = function(matchersUtil, customEqualityTesters) { return { compare: function() {} }; }; @@ -380,7 +380,6 @@ describe('Custom Matchers (Integration)', function() { 'See for details.' ) ); - expect(env.deprecated).toHaveBeenCalledTimes(2); done(); } diff --git a/spec/core/integration/DeprecationSpec.js b/spec/core/integration/DeprecationSpec.js new file mode 100644 index 00000000..5f9fb454 --- /dev/null +++ b/spec/core/integration/DeprecationSpec.js @@ -0,0 +1,322 @@ +/* eslint no-console: 0 */ +describe('Deprecation (integration)', function() { + var env; + + beforeEach(function() { + env = new jasmineUnderTest.Env(); + }); + + afterEach(function() { + env.cleanup_(); + }); + + it('reports a deprecation on the top suite', function(done) { + var reporter = jasmine.createSpyObj('reporter', ['jasmineDone']); + env.addReporter(reporter); + spyOn(console, 'error'); + + env.beforeAll(function() { + env.deprecated('the message'); + }); + env.it('a spec', function() {}); + + env.execute(null, function() { + expect(reporter.jasmineDone).toHaveBeenCalledWith( + jasmine.objectContaining({ + deprecationWarnings: [ + jasmine.objectContaining({ + message: jasmine.stringMatching(/^the message/) + }) + ] + }) + ); + expect(console.error).toHaveBeenCalledWith( + jasmine.stringMatching(/^DEPRECATION: the message/) + ); + done(); + }); + }); + + it('reports a deprecation on a descendent suite', function(done) { + var reporter = jasmine.createSpyObj('reporter', ['suiteDone']); + env.addReporter(reporter); + spyOn(console, 'error'); + + env.describe('a suite', function() { + env.beforeAll(function() { + env.deprecated('the message'); + }); + env.it('a spec', function() {}); + }); + + env.execute(null, function() { + expect(reporter.suiteDone).toHaveBeenCalledWith( + jasmine.objectContaining({ + deprecationWarnings: [ + jasmine.objectContaining({ + message: jasmine.stringMatching(/^the message/) + }) + ] + }) + ); + expect(console.error).toHaveBeenCalledWith( + jasmine.stringMatching( + /^DEPRECATION: the message \(in suite: a suite\)/ + ) + ); + done(); + }); + }); + + it('reports a deprecation on a spec', function(done) { + var reporter = jasmine.createSpyObj('reporter', ['specDone']); + env.addReporter(reporter); + spyOn(console, 'error'); + + env.describe('a suite', function() { + env.it('a spec', function() { + env.deprecated('the message'); + }); + }); + + env.execute(null, function() { + expect(reporter.specDone).toHaveBeenCalledWith( + jasmine.objectContaining({ + deprecationWarnings: [ + jasmine.objectContaining({ + message: jasmine.stringMatching(/^the message/) + }) + ] + }) + ); + expect(console.error).toHaveBeenCalledWith( + jasmine.stringMatching( + /^DEPRECATION: the message \(in spec: a suite a spec\)/ + ) + ); + done(); + }); + }); + + it('omits the suite or spec context when ignoreRunnable is true', function(done) { + var reporter = jasmine.createSpyObj('reporter', ['jasmineDone']); + env.addReporter(reporter); + spyOn(console, 'error'); + + env.it('a spec', function() { + env.deprecated('the message', { ignoreRunnable: true }); + }); + + env.execute(null, function() { + expect(reporter.jasmineDone).toHaveBeenCalledWith( + jasmine.objectContaining({ + deprecationWarnings: [ + jasmine.objectContaining({ + message: jasmine.stringMatching(/^the message/) + }) + ] + }) + ); + expect(console.error).toHaveBeenCalledWith( + jasmine.stringMatching(/the message/) + ); + expect(console.error).not.toHaveBeenCalledWith( + jasmine.stringMatching(/a spec/) + ); + done(); + }); + }); + + it('includes the stack trace', function(done) { + var reporter = jasmine.createSpyObj('reporter', ['specDone']); + env.addReporter(reporter); + spyOn(console, 'error'); + + env.describe('a suite', function() { + env.it('a spec', function() { + env.deprecated('the message'); + }); + }); + + env.execute(null, function() { + expect(reporter.specDone).toHaveBeenCalledWith( + jasmine.objectContaining({ + deprecationWarnings: [ + jasmine.objectContaining({ + stack: jasmine.stringMatching(/DeprecationSpec.js/) + }) + ] + }) + ); + expect(console.error).toHaveBeenCalled(); + expect(console.error.calls.argsFor(0)[0].replace(/\n/g, 'NL')).toMatch( + /^DEPRECATION: the message \(in spec: a suite a spec\)NL.*DeprecationSpec.js/ + ); + done(); + }); + }); + + it('excludes the stack trace when omitStackTrace is true', function(done) { + var reporter = jasmine.createSpyObj('reporter', ['specDone']); + env.addReporter(reporter); + spyOn(console, 'error'); + + env.describe('a suite', function() { + env.it('a spec', function() { + env.deprecated('the message', { omitStackTrace: true }); + }); + }); + + env.execute(null, function() { + expect(reporter.specDone).toHaveBeenCalledWith( + jasmine.objectContaining({ + deprecationWarnings: [ + jasmine.objectContaining({ + stack: jasmine.falsy() + }) + ] + }) + ); + expect(console.error).toHaveBeenCalled(); + expect(console.error).not.toHaveBeenCalledWith( + jasmine.stringMatching(/DeprecationSpec.js/) + ); + done(); + }); + }); + + it('emits a given deprecation only once', function(done) { + var reporter = jasmine.createSpyObj('reporter', ['specDone', 'suiteDone']); + env.addReporter(reporter); + spyOn(console, 'error'); + + env.describe('a suite', function() { + env.beforeAll(function() { + env.deprecated('the message'); + env.deprecated('the message'); + }); + + env.it('a spec', function() { + env.deprecated('the message'); + env.deprecated('a different message'); + }); + }); + + env.execute(null, function() { + expect(reporter.suiteDone).toHaveBeenCalledWith( + jasmine.objectContaining({ + deprecationWarnings: [ + // only one + jasmine.objectContaining({ + message: jasmine.stringMatching(/^the message/) + }) + ] + }) + ); + expect(reporter.specDone).toHaveBeenCalledWith( + jasmine.objectContaining({ + deprecationWarnings: [ + // only the other one + jasmine.objectContaining({ + message: jasmine.stringMatching(/^a different message/) + }) + ] + }) + ); + expect(console.error).toHaveBeenCalledTimes(2); + expect(console.error).toHaveBeenCalledWith( + jasmine.stringMatching( + /^DEPRECATION: the message \(in suite: a suite\)/ + ) + ); + expect(console.error).toHaveBeenCalledWith( + jasmine.stringMatching( + /^DEPRECATION: a different message \(in spec: a suite a spec\)/ + ) + ); + done(); + }); + }); + + it('emits a given deprecation each time when config.verboseDeprecations is true', function(done) { + var reporter = jasmine.createSpyObj('reporter', ['specDone', 'suiteDone']); + env.addReporter(reporter); + spyOn(console, 'error'); + + env.configure({ verboseDeprecations: true }); + + env.describe('a suite', function() { + env.beforeAll(function() { + env.deprecated('the message'); + env.deprecated('the message'); + }); + + env.it('a spec', function() { + env.deprecated('the message'); + }); + }); + + env.execute(null, function() { + expect(reporter.suiteDone).toHaveBeenCalledWith( + jasmine.objectContaining({ + deprecationWarnings: [ + jasmine.objectContaining({ + message: jasmine.stringMatching(/^the message/) + }), + jasmine.objectContaining({ + message: jasmine.stringMatching(/^the message/) + }) + ] + }) + ); + expect(reporter.specDone).toHaveBeenCalledWith( + jasmine.objectContaining({ + deprecationWarnings: [ + jasmine.objectContaining({ + message: jasmine.stringMatching(/^the message/) + }) + ] + }) + ); + expect(console.error).toHaveBeenCalledTimes(3); + expect(console.error.calls.argsFor(0)[0]).toMatch( + /^DEPRECATION: the message \(in suite: a suite\)/ + ); + expect(console.error.calls.argsFor(1)[0]).toMatch( + /^DEPRECATION: the message \(in suite: a suite\)/ + ); + expect(console.error.calls.argsFor(2)[0]).toMatch( + /^DEPRECATION: the message \(in spec: a suite a spec\)/ + ); + expect(console.error.calls.argsFor(2)[0]).toMatch( + /^DEPRECATION: the message \(in spec: a suite a spec\)/ + ); + done(); + }); + }); + + it('handles deprecations that occur before execute() is called', function(done) { + var reporter = jasmine.createSpyObj('reporter', ['jasmineDone']); + env.addReporter(reporter); + spyOn(console, 'error'); + + env.deprecated('the message'); + env.it('a spec', function() {}); + + env.execute(null, function() { + expect(reporter.jasmineDone).toHaveBeenCalledWith( + jasmine.objectContaining({ + deprecationWarnings: [ + jasmine.objectContaining({ + message: jasmine.stringMatching(/^the message/) + }) + ] + }) + ); + expect(console.error).toHaveBeenCalledWith( + jasmine.stringMatching(/^DEPRECATION: the message/) + ); + done(); + }); + }); +}); diff --git a/spec/core/integration/EnvSpec.js b/spec/core/integration/EnvSpec.js index 7d57ae51..57eff7d9 100644 --- a/spec/core/integration/EnvSpec.js +++ b/spec/core/integration/EnvSpec.js @@ -2626,70 +2626,6 @@ describe('Env integration', function() { }); }); - it('should report deprecation warnings on the correct specs and suites', function(done) { - var reporter = jasmine.createSpyObj('reporter', [ - 'jasmineDone', - 'suiteDone', - 'specDone' - ]); - - // prevent deprecation from being displayed, as well as letting us observe calls - spyOn(console, 'error'); - - env.addReporter(reporter); - - env.deprecated('top level deprecation'); - - env.describe('suite', function() { - env.beforeAll(function() { - env.deprecated('suite level deprecation'); - }); - - env.it('spec', function() { - env.deprecated('spec level deprecation'); - }); - }); - - env.execute(null, function() { - var result = reporter.jasmineDone.calls.argsFor(0)[0]; - expect(result.deprecationWarnings).toEqual([ - jasmine.objectContaining({ message: 'top level deprecation' }) - ]); - /* eslint-disable-next-line no-console */ - expect(console.error).toHaveBeenCalledWith( - 'DEPRECATION: top level deprecation' - ); - - expect(reporter.suiteDone).toHaveBeenCalledWith( - jasmine.objectContaining({ - fullName: 'suite', - deprecationWarnings: [ - jasmine.objectContaining({ message: 'suite level deprecation' }) - ] - }) - ); - /* eslint-disable-next-line no-console */ - expect(console.error).toHaveBeenCalledWith( - 'DEPRECATION: suite level deprecation (in suite: suite)' - ); - - expect(reporter.specDone).toHaveBeenCalledWith( - jasmine.objectContaining({ - fullName: 'suite spec', - deprecationWarnings: [ - jasmine.objectContaining({ message: 'spec level deprecation' }) - ] - }) - ); - /* eslint-disable-next-line no-console */ - expect(console.error).toHaveBeenCalledWith( - 'DEPRECATION: spec level deprecation (in spec: suite spec)' - ); - - done(); - }); - }); - it('should report deprecation stack with an error object', function(done) { var exceptionFormatter = new jasmineUnderTest.ExceptionFormatter(), reporter = jasmine.createSpyObj('reporter', [ diff --git a/spec/html/HtmlReporterSpec.js b/spec/html/HtmlReporterSpec.js index 2db0e160..c21aa66e 100644 --- a/spec/html/HtmlReporterSpec.js +++ b/spec/html/HtmlReporterSpec.js @@ -303,6 +303,128 @@ describe('HtmlReporter', function() { expect(alertBars[3].innerHTML).toMatch(/global deprecation/); expect(alertBars[3].innerHTML).not.toMatch(/in /); }); + + it('displays expandable stack traces', function() { + var 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); + } + }), + expander, + expanderLink, + expanderContents; + + reporter.initialize(); + + reporter.jasmineStarted({}); + reporter.jasmineDone({ + deprecationWarnings: [ + { + message: 'a deprecation', + stack: 'a stack trace' + } + ], + failedExpectations: [] + }); + + expander = container.querySelector( + '.jasmine-alert .jasmine-bar .jasmine-expander' + ); + expanderContents = expander.querySelector('.jasmine-expander-contents'); + expect(expanderContents.textContent).toMatch(/a stack trace/); + + expanderLink = expander.querySelector('a'); + expect(expander).not.toHaveClass('jasmine-expanded'); + expect(expanderLink.textContent).toMatch(/Show stack trace/); + + expanderLink.click(); + expect(expander).toHaveClass('jasmine-expanded'); + expect(expanderLink.textContent).toMatch(/Hide stack trace/); + expanderLink.click(); + + expect(expander).not.toHaveClass('jasmine-expanded'); + expect(expanderLink.textContent).toMatch(/Show stack trace/); + }); + + it('omits the expander when there is no stack trace', function() { + var 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); + } + }), + warningBar; + + reporter.initialize(); + + reporter.jasmineStarted({}); + reporter.jasmineDone({ + deprecationWarnings: [ + { + message: 'a deprecation', + stack: '' + } + ], + failedExpectations: [] + }); + + warningBar = container.querySelector('.jasmine-warning'); + expect(warningBar.querySelector('.jasmine-expander')).toBeFalsy(); + }); + + it('nicely formats the verboseDeprecations note', function() { + var 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); + } + }), + alertBar; + + reporter.initialize(); + + reporter.jasmineStarted({}); + reporter.jasmineDone({ + deprecationWarnings: [ + { + message: + 'a deprecation\nNote: This message will be shown only once. Set config.verboseDeprecations to true to see every occurrence.' + } + ], + failedExpectations: [] + }); + + alertBar = container.querySelector('.jasmine-warning'); + + expect(alertBar.innerHTML).toMatch( + /a deprecation
Note: This message will be shown only once/ + ); + }); }); describe('when Jasmine is done', function() { diff --git a/src/core/Deprecator.js b/src/core/Deprecator.js new file mode 100644 index 00000000..3e63c9d3 --- /dev/null +++ b/src/core/Deprecator.js @@ -0,0 +1,90 @@ +getJasmineRequireObj().Deprecator = function(j$) { + function Deprecator(topSuite) { + this.topSuite_ = topSuite; + this.verbose_ = false; + this.toSuppress_ = []; + } + + var verboseNote = + 'Note: This message will be shown only once. ' + + 'Set config.verboseDeprecations to true to see every occurrence.'; + + Deprecator.prototype.verboseDeprecations = function(enabled) { + this.verbose_ = enabled; + }; + + // runnable is a spec or a suite. + // deprecation is a string or an Error. + // See Env#deprecated for a description of the options argument. + Deprecator.prototype.addDeprecationWarning = function( + runnable, + deprecation, + options + ) { + options = options || {}; + + if (!this.verbose_ && !j$.isError_(deprecation)) { + if (this.toSuppress_.indexOf(deprecation) !== -1) { + return; + } + this.toSuppress_.push(deprecation); + } + + this.log_(runnable, deprecation, options); + this.report_(runnable, deprecation, options); + }; + + Deprecator.prototype.log_ = function(runnable, deprecation, options) { + var context; + + if (j$.isError_(deprecation)) { + console.error(deprecation); + return; + } + + if (runnable === this.topSuite_ || options.ignoreRunnable) { + context = ''; + } else if (runnable.children) { + context = ' (in suite: ' + runnable.getFullName() + ')'; + } else { + context = ' (in spec: ' + runnable.getFullName() + ')'; + } + + if (!options.omitStackTrace) { + context += '\n' + this.stackTrace_(); + } + + if (!this.verbose_) { + context += '\n' + verboseNote; + } + + console.error('DEPRECATION: ' + deprecation + context); + }; + + Deprecator.prototype.stackTrace_ = function() { + var formatter = new j$.ExceptionFormatter(); + return formatter.stack(j$.util.errorWithStack()).replace(/^Error\n/m, ''); + }; + + Deprecator.prototype.report_ = function(runnable, deprecation, options) { + if (options.ignoreRunnable) { + runnable = this.topSuite_; + } + + if (j$.isError_(deprecation)) { + runnable.addDeprecationWarning(deprecation); + return; + } + + if (!this.verbose_) { + deprecation += '\n' + verboseNote; + } + + runnable.addDeprecationWarning({ + message: deprecation, + omitStackTrace: options.omitStackTrace || false + }); + }; + + return Deprecator; +}; diff --git a/src/core/Env.js b/src/core/Env.js index c3985a28..3e5fa8d2 100644 --- a/src/core/Env.js +++ b/src/core/Env.js @@ -32,7 +32,6 @@ getJasmineRequireObj().Env = function(j$) { var currentlyExecutingSuites = []; var currentDeclarationSuite = null; var hasFailures = false; - var deprecationsToSuppress = []; /** * This represents the available options to configure Jasmine. @@ -228,6 +227,7 @@ getJasmineRequireObj().Env = function(j$) { if (configuration.hasOwnProperty('verboseDeprecations')) { config.verboseDeprecations = configuration.verboseDeprecations; + deprecator.verboseDeprecations(config.verboseDeprecations); } }; @@ -249,13 +249,19 @@ getJasmineRequireObj().Env = function(j$) { Object.defineProperty(this, 'specFilter', { get: function() { self.deprecated( - 'Getting specFilter directly from Env is deprecated and will be removed in a future version of Jasmine, please check the specFilter option from `configuration`' + 'Getting specFilter directly from Env is deprecated and will be ' + + 'removed in a future version of Jasmine. Please check the ' + + 'specFilter option from `configuration` instead.', + { ignoreRunnable: true } ); return config.specFilter; }, set: function(val) { self.deprecated( - 'Setting specFilter directly on Env is deprecated and will be removed in a future version of Jasmine, please use the specFilter option in `configure`' + 'Setting specFilter directly on Env is deprecated and will be ' + + 'removed in a future version of Jasmine. Please use the ' + + 'specFilter option in `configure` instead.', + { ignoreRunnable: true } ); config.specFilter = val; } @@ -303,7 +309,7 @@ getJasmineRequireObj().Env = function(j$) { for (var matcherName in matchersToAdd) { if (matchersToAdd[matcherName].length > 1) { - self.deprecatedOnceWithStack( + self.deprecated( 'The matcher factory for "' + matcherName + '" ' + @@ -328,7 +334,7 @@ getJasmineRequireObj().Env = function(j$) { for (var matcherName in matchersToAdd) { if (matchersToAdd[matcherName].length > 1) { - self.deprecatedOnceWithStack( + self.deprecated( 'The matcher factory for "' + matcherName + '" ' + @@ -535,14 +541,21 @@ getJasmineRequireObj().Env = function(j$) { */ this.throwOnExpectationFailure = function(value) { this.deprecated( - 'Setting throwOnExpectationFailure directly on Env is deprecated and will be removed in a future version of Jasmine, please use the oneFailurePerSpec option in `configure`' + 'Setting throwOnExpectationFailure directly on Env is deprecated ' + + 'and will be removed in a future version of Jasmine. Please use the ' + + 'oneFailurePerSpec option in `configure` instead.', + { ignoreRunnable: true } ); this.configure({ oneFailurePerSpec: !!value }); }; this.throwingExpectationFailures = function() { this.deprecated( - 'Getting throwingExpectationFailures directly from Env is deprecated and will be removed in a future version of Jasmine, please check the oneFailurePerSpec option from `configuration`' + 'Getting throwingExpectationFailures directly from Env is ' + + 'deprecated and will be removed in a future version of Jasmine. ' + + 'Please check the oneFailurePerSpec option from `configuration` ' + + 'instead.', + { ignoreRunnable: true } ); return config.oneFailurePerSpec; }; @@ -557,14 +570,20 @@ getJasmineRequireObj().Env = function(j$) { */ this.stopOnSpecFailure = function(value) { this.deprecated( - 'Setting stopOnSpecFailure directly is deprecated and will be removed in a future version of Jasmine, please use the failFast option in `configure`' + 'Setting stopOnSpecFailure directly is deprecated and will be ' + + 'removed in a future version of Jasmine. Please use the failFast ' + + 'option in `configure` instead.', + { ignoreRunnable: true } ); this.configure({ failFast: !!value }); }; this.stoppingOnSpecFailure = function() { this.deprecated( - 'Getting stoppingOnSpecFailure directly from Env is deprecated and will be removed in a future version of Jasmine, please check the failFast option from `configuration`' + 'Getting stoppingOnSpecFailure directly from Env is deprecated ' + + 'and will be removed in a future version of Jasmine. Please check ' + + 'the failFast option from `configuration` instead.', + { ignoreRunnable: true } ); return config.failFast; }; @@ -579,14 +598,20 @@ getJasmineRequireObj().Env = function(j$) { */ this.randomizeTests = function(value) { this.deprecated( - 'Setting randomizeTests directly is deprecated and will be removed in a future version of Jasmine, please use the random option in `configure`' + 'Setting randomizeTests directly is deprecated and will be removed ' + + 'in a future version of Jasmine. Please use the random option in ' + + '`configure` instead.', + { ignoreRunnable: true } ); config.random = !!value; }; this.randomTests = function() { this.deprecated( - 'Getting randomTests directly from Env is deprecated and will be removed in a future version of Jasmine, please check the random option from `configuration`' + 'Getting randomTests directly from Env is deprecated and will be ' + + 'removed in a future version of Jasmine. Please check the random ' + + 'option from `configuration` instead.', + { ignoreRunnable: true } ); return config.random; }; @@ -601,7 +626,10 @@ getJasmineRequireObj().Env = function(j$) { */ this.seed = function(value) { this.deprecated( - 'Setting seed directly is deprecated and will be removed in a future version of Jasmine, please use the seed option in `configure`' + 'Setting seed directly is deprecated and will be removed in a ' + + 'future version of Jasmine. Please use the seed option in ' + + '`configure` instead.', + { ignoreRunnable: true } ); if (value) { config.seed = value; @@ -611,7 +639,10 @@ getJasmineRequireObj().Env = function(j$) { this.hidingDisabled = function(value) { this.deprecated( - 'Getting hidingDisabled directly from Env is deprecated and will be removed in a future version of Jasmine, please check the hideDisabled option from `configuration`' + 'Getting hidingDisabled directly from Env is deprecated and will ' + + 'be removed in a future version of Jasmine. Please check the ' + + 'hideDisabled option from `configuration` instead.', + { ignoreRunnable: true } ); return config.hideDisabled; }; @@ -623,53 +654,39 @@ getJasmineRequireObj().Env = function(j$) { */ this.hideDisabled = function(value) { this.deprecated( - 'Setting hideDisabled directly is deprecated and will be removed in a future version of Jasmine, please use the hideDisabled option in `configure`' + 'Setting hideDisabled directly is deprecated and will be removed ' + + 'in a future version of Jasmine. Please use the hideDisabled option ' + + 'in `configure` instead.', + { ignoreRunnable: true } ); config.hideDisabled = !!value; }; - this.deprecated = function(deprecation) { + /** + * Causes a deprecation warning to be logged to the console and reported to + * reporters. + * + * The optional second parameter is an object that can have either of the + * following properties: + * + * omitStackTrace: Whether to omit the stack trace. Optional. Defaults to + * false. This option is ignored if the deprecation is an Error. Set this + * when the stack trace will not contain anything that helps the user find + * the source of the deprecation. + * + * ignoreRunnable: Whether to log the deprecation on the root suite, ignoring + * the spec or suite that's running when it happens. Optional. Defaults to + * false. + * + * @name Env#deprecated + * @since 2.99 + * @function + * @param {String|Error} deprecation The deprecation message + * @param {Object} [options] Optional extra options, as described above + */ + this.deprecated = function(deprecation, options) { var runnable = currentRunnable() || topSuite; - var context; - - if (runnable === topSuite) { - context = ''; - } else if (runnable === currentSuite()) { - context = ' (in suite: ' + runnable.getFullName() + ')'; - } else { - context = ' (in spec: ' + runnable.getFullName() + ')'; - } - - runnable.addDeprecationWarning(deprecation); - if ( - typeof console !== 'undefined' && - typeof console.error === 'function' - ) { - console.error('DEPRECATION: ' + deprecation + context); - } - }; - - this.deprecatedOnceWithStack = function(deprecation) { - var formatter = new j$.ExceptionFormatter(), - stackTrace = formatter - .stack(j$.util.errorWithStack()) - .replace(/^Error\n/m, ''); - - if (config.verboseDeprecations) { - this.deprecated(deprecation + '\n' + stackTrace); - } else { - if (deprecationsToSuppress.indexOf(deprecation) === -1) { - this.deprecated( - deprecation + - '\n' + - 'Note: This message will be shown only once. ' + - 'Set config.verboseDeprecations to true to see every occurrence.\n' + - stackTrace - ); - } - - deprecationsToSuppress.push(deprecation); - } + deprecator.addDeprecationWarning(runnable, deprecation, options); }; var queueRunnerFactory = function(options, args) { @@ -705,6 +722,7 @@ getJasmineRequireObj().Env = function(j$) { asyncExpectationFactory: suiteAsyncExpectationFactory, expectationResultFactory: expectationResultFactory }); + var deprecator = new j$.Deprecator(topSuite); defaultResourcesForRunnable(topSuite.id); currentDeclarationSuite = topSuite; diff --git a/src/core/ExpectationResult.js b/src/core/ExpectationResult.js index 7f298bc7..d3ea5cb3 100644 --- a/src/core/ExpectationResult.js +++ b/src/core/ExpectationResult.js @@ -16,7 +16,7 @@ getJasmineRequireObj().buildExpectationResult = function(j$) { var result = { matcherName: options.matcherName, message: message(), - stack: stack(), + stack: options.omitStackTrace ? '' : stack(), passed: options.passed }; diff --git a/src/core/QueueRunner.js b/src/core/QueueRunner.js index be9e7f62..f19262ef 100644 --- a/src/core/QueueRunner.js +++ b/src/core/QueueRunner.js @@ -215,25 +215,29 @@ getJasmineRequireObj().QueueRunner = function(j$) { }; QueueRunner.prototype.diagnoseConflictingAsync_ = function(fn, retval) { + var msg; + if (retval && j$.isFunction_(retval.then)) { - // Issue a warning that matches the user's code + // Issue a warning that matches the user's code. + // Omit the stack trace because there's almost certainly no user code + // on the stack at this point. if (j$.isAsyncFunction_(fn)) { - this.deprecated( + msg = 'An asynchronous before/it/after ' + - 'function was defined with the async keyword but also took a ' + - 'done callback. This is not supported and will stop working in' + - ' the future. Either remove the done callback (recommended) or ' + - 'remove the async keyword.' - ); + 'function was defined with the async keyword but also took a ' + + 'done callback. This is not supported and will stop working in' + + ' the future. Either remove the done callback (recommended) or ' + + 'remove the async keyword.'; } else { - this.deprecated( + msg = 'An asynchronous before/it/after ' + - 'function took a done callback but also returned a promise. ' + - 'This is not supported and will stop working in the future. ' + - 'Either remove the done callback (recommended) or change the ' + - 'function to not return a promise.' - ); + 'function took a done callback but also returned a promise. ' + + 'This is not supported and will stop working in the future. ' + + 'Either remove the done callback (recommended) or change the ' + + 'function to not return a promise.'; } + + this.deprecated(msg, { omitStackTrace: true }); } }; diff --git a/src/core/matchers/matchersUtil.js b/src/core/matchers/matchersUtil.js index 9f488854..c40a707a 100644 --- a/src/core/matchers/matchersUtil.js +++ b/src/core/matchers/matchersUtil.js @@ -35,7 +35,7 @@ getJasmineRequireObj().MatchersUtil = function(j$) { */ MatchersUtil.prototype.contains = function(haystack, needle, customTesters) { if (customTesters) { - j$.getEnv().deprecatedOnceWithStack( + j$.getEnv().deprecated( 'Passing custom equality testers ' + 'to MatchersUtil#contains is deprecated. ' + 'See for details.' @@ -172,7 +172,7 @@ getJasmineRequireObj().MatchersUtil = function(j$) { diffBuilder = customTestersOrDiffBuilder; } else { if (customTestersOrDiffBuilder) { - j$.getEnv().deprecatedOnceWithStack( + j$.getEnv().deprecated( 'Passing custom equality testers ' + 'to MatchersUtil#equals is deprecated. ' + 'See for details.' @@ -180,7 +180,7 @@ getJasmineRequireObj().MatchersUtil = function(j$) { } if (diffBuilderOrNothing) { - j$.getEnv().deprecatedOnceWithStack( + j$.getEnv().deprecated( 'Diff builder should be passed ' + 'as the third argument to MatchersUtil#equals, not the fourth. ' + 'See for details.' diff --git a/src/core/requireCore.js b/src/core/requireCore.js index b2faf0fe..e20ad56d 100644 --- a/src/core/requireCore.js +++ b/src/core/requireCore.js @@ -42,6 +42,7 @@ var getJasmineRequireObj = (function(jasmineGlobal) { j$.getClearStack = jRequire.clearStack(j$); j$.Clock = jRequire.Clock(); j$.DelayedFunctionScheduler = jRequire.DelayedFunctionScheduler(j$); + j$.Deprecator = jRequire.Deprecator(j$); j$.Env = jRequire.Env(j$); j$.deprecatingThisProxy = jRequire.deprecatingThisProxy(j$); j$.StackTrace = jRequire.StackTrace(j$); @@ -58,7 +59,7 @@ var getJasmineRequireObj = (function(jasmineGlobal) { j$.basicPrettyPrinter_ = j$.makePrettyPrinter(); Object.defineProperty(j$, 'pp', { get: function() { - j$.getEnv().deprecatedOnceWithStack( + j$.getEnv().deprecated( 'jasmine.pp is deprecated and will be removed in a future release. ' + 'Use the pp method of the matchersUtil passed to the matcher factory ' + "or the asymmetric equality tester's `asymmetricMatch` method " + @@ -75,7 +76,7 @@ var getJasmineRequireObj = (function(jasmineGlobal) { }); Object.defineProperty(j$, 'matchersUtil', { get: function() { - j$.getEnv().deprecatedOnceWithStack( + j$.getEnv().deprecated( 'jasmine.matchersUtil is deprecated and will be removed ' + 'in a future release. Use the instance passed to the matcher factory or ' + "the asymmetric equality tester's `asymmetricMatch` method instead. " + diff --git a/src/html/HtmlReporter.js b/src/html/HtmlReporter.js index e591ccac..98f958d1 100644 --- a/src/html/HtmlReporter.js +++ b/src/html/HtmlReporter.js @@ -277,7 +277,8 @@ jasmineRequire.HtmlReporter = function(j$) { addDeprecationWarnings(doneResult); for (i = 0; i < deprecationWarnings.length; i++) { - var context; + var children = [], + context; switch (deprecationWarnings[i].runnableType) { case 'spec': @@ -290,13 +291,23 @@ jasmineRequire.HtmlReporter = function(j$) { context = ''; } + deprecationWarnings[i].message.split('\n').forEach(function(line) { + children.push(line); + children.push(createDom('br')); + }); + + children[0] = 'DEPRECATION: ' + children[0]; + children.push(context); + + if (deprecationWarnings[i].stack) { + children.push(createExpander(deprecationWarnings[i].stack)); + } + alert.appendChild( createDom( 'span', { className: 'jasmine-bar jasmine-warning' }, - 'DEPRECATION: ' + deprecationWarnings[i].message, - createDom('br'), - context + children ) ); } @@ -611,17 +622,44 @@ jasmineRequire.HtmlReporter = function(j$) { if (result && result.deprecationWarnings) { for (var i = 0; i < result.deprecationWarnings.length; i++) { var warning = result.deprecationWarnings[i].message; - if (!j$.util.arrayContains(warning)) { - deprecationWarnings.push({ - message: warning, - runnableName: result.fullName, - runnableType: runnableType - }); - } + deprecationWarnings.push({ + message: warning, + stack: result.deprecationWarnings[i].stack, + runnableName: result.fullName, + runnableType: runnableType + }); } } } + function createExpander(stackTrace) { + var expandLink = createDom('a', { href: '#' }, 'Show stack trace'); + var root = createDom( + 'div', + { className: 'jasmine-expander' }, + expandLink, + createDom( + 'div', + { className: 'jasmine-expander-contents jasmine-stack-trace' }, + stackTrace + ) + ); + + expandLink.addEventListener('click', function(e) { + e.preventDefault(); + + if (root.classList.contains('jasmine-expanded')) { + root.classList.remove('jasmine-expanded'); + expandLink.textContent = 'Show stack trace'; + } else { + root.classList.add('jasmine-expanded'); + expandLink.textContent = 'Hide stack trace'; + } + }); + + return root; + } + function find(selector) { return getContainer().querySelector('.jasmine_html-reporter ' + selector); } @@ -635,11 +673,23 @@ jasmineRequire.HtmlReporter = function(j$) { } } - function createDom(type, attrs, childrenVarArgs) { - var el = createElement(type); + function createDom(type, attrs, childrenArrayOrVarArgs) { + var el = createElement(type), + children, + i; - for (var i = 2; i < arguments.length; i++) { - var child = arguments[i]; + if (j$.isArray_(childrenArrayOrVarArgs)) { + children = childrenArrayOrVarArgs; + } else { + children = []; + + for (i = 2; i < arguments.length; i++) { + children.push(arguments[i]); + } + } + + for (i = 0; i < children.length; i++) { + var child = children[i]; if (typeof child === 'string') { el.appendChild(createTextNode(child)); diff --git a/src/html/_HTMLReporter.scss b/src/html/_HTMLReporter.scss index 2b128a04..d2d58b0d 100644 --- a/src/html/_HTMLReporter.scss +++ b/src/html/_HTMLReporter.scss @@ -231,6 +231,8 @@ body { } &.jasmine-warning { + margin-top: $margin-unit; + margin-bottom: $margin-unit; background-color: $pending-color; color: $text-color; } @@ -386,4 +388,27 @@ body { background: white; white-space: pre; } + + .jasmine-expander { + a { + display: block; + margin-left: $margin-unit; + color: blue; + text-decoration: underline; + } + } + + .jasmine-expander-contents { + display: none; + } + + .jasmine-expanded { + padding-bottom: 10px; + } + + .jasmine-expanded .jasmine-expander-contents { + display: block; + margin-left: $margin-unit; + padding: 5px; + } } From 00c1e3d608e36903b567237e2f40f63f05f1a2a5 Mon Sep 17 00:00:00 2001 From: Steve Gravrock Date: Sat, 22 May 2021 14:13:58 -0700 Subject: [PATCH 2/2] Deprecate access to non-public Suite and Spec members via Env#topSuite The deprecation warning relies on Proxy, and won't work in environments that don't have it. Among Jasmine's supported environments, that's Safari 9, Safari 8, and all versions of IE. --- lib/jasmine-core/jasmine.js | 174 +++++++++++++++++++++++++++++- spec/core/EnvSpec.js | 136 ++++++++++++++++++++++- src/core/Env.js | 2 +- src/core/deprecatingSpecProxy.js | 70 ++++++++++++ src/core/deprecatingSuiteProxy.js | 98 +++++++++++++++++ src/core/requireCore.js | 2 + 6 files changed, 478 insertions(+), 4 deletions(-) create mode 100644 src/core/deprecatingSpecProxy.js create mode 100644 src/core/deprecatingSuiteProxy.js diff --git a/lib/jasmine-core/jasmine.js b/lib/jasmine-core/jasmine.js index fe04d065..e382dbbe 100644 --- a/lib/jasmine-core/jasmine.js +++ b/lib/jasmine-core/jasmine.js @@ -67,6 +67,8 @@ var getJasmineRequireObj = (function(jasmineGlobal) { j$.Deprecator = jRequire.Deprecator(j$); j$.Env = jRequire.Env(j$); j$.deprecatingThisProxy = jRequire.deprecatingThisProxy(j$); + j$.deprecatingSuiteProxy = jRequire.deprecatingSuiteProxy(j$); + j$.deprecatingSpecProxy = jRequire.deprecatingSpecProxy(j$); j$.StackTrace = jRequire.StackTrace(j$); j$.ExceptionFormatter = jRequire.ExceptionFormatter(j$); j$.ExpectationFilterChain = jRequire.ExpectationFilterChain(); @@ -1738,7 +1740,7 @@ getJasmineRequireObj().Env = function(j$) { * @return {Suite} the root suite */ this.topSuite = function() { - return topSuite; + return j$.deprecatingSuiteProxy(topSuite, null, this); }; /** @@ -3693,6 +3695,176 @@ getJasmineRequireObj().DelayedFunctionScheduler = function(j$) { return DelayedFunctionScheduler; }; +/* eslint-disable compat/compat */ +// TODO: Remove this in the next major release. +getJasmineRequireObj().deprecatingSpecProxy = function(j$) { + function isMember(target, prop) { + return ( + Object.keys(target).indexOf(prop) !== -1 || + Object.keys(j$.Spec.prototype).indexOf(prop) !== -1 + ); + } + + function isAllowedMember(prop) { + return prop === 'description' || prop === 'getFullName'; + } + + function msg(member) { + var memberName = member.toString().replace(/^Symbol\((.+)\)$/, '$1'); + return ( + 'Access to private Spec members (in this case `' + + memberName + + '`) via Env#topSuite is not supported and will break in ' + + 'a future release. See ' + + 'for correct usage.' + ); + } + + try { + new Proxy({}, {}); + } catch (e) { + // Environment does not support Poxy. + return function(spec) { + return spec; + }; + } + + function DeprecatingSpecProxyHandler(env) { + this._env = env; + } + + DeprecatingSpecProxyHandler.prototype.get = function(target, prop, receiver) { + this._maybeDeprecate(target, prop); + + if (prop === 'getFullName') { + // getFullName calls a private method. Re-bind 'this' to avoid a bogus + // deprecation warning. + return target.getFullName.bind(target); + } else { + return target[prop]; + } + }; + + DeprecatingSpecProxyHandler.prototype.set = function(target, prop, value) { + this._maybeDeprecate(target, prop); + return (target[prop] = value); + }; + + DeprecatingSpecProxyHandler.prototype._maybeDeprecate = function( + target, + prop + ) { + if (isMember(target, prop) && !isAllowedMember(prop)) { + this._env.deprecated(msg(prop)); + } + }; + + function deprecatingSpecProxy(spec, env) { + return new Proxy(spec, new DeprecatingSpecProxyHandler(env)); + } + + return deprecatingSpecProxy; +}; + +/* eslint-disable compat/compat */ +// TODO: Remove this in the next major release. +getJasmineRequireObj().deprecatingSuiteProxy = function(j$) { + var allowedMembers = [ + 'children', + 'description', + 'parentSuite', + 'getFullName' + ]; + + function isMember(target, prop) { + return ( + Object.keys(target).indexOf(prop) !== -1 || + Object.keys(j$.Suite.prototype).indexOf(prop) !== -1 + ); + } + + function isAllowedMember(prop) { + return allowedMembers.indexOf(prop) !== -1; + } + + function msg(member) { + var memberName = member.toString().replace(/^Symbol\((.+)\)$/, '$1'); + return ( + 'Access to private Suite members (in this case `' + + memberName + + '`) via Env#topSuite is not supported and will break in ' + + 'a future release. See ' + + 'for correct usage.' + ); + } + try { + new Proxy({}, {}); + } catch (e) { + // Environment does not support Poxy. + return function(suite) { + return suite; + }; + } + + function DeprecatingSuiteProxyHandler(parentSuite, env) { + this._parentSuite = parentSuite; + this._env = env; + } + + DeprecatingSuiteProxyHandler.prototype.get = function( + target, + prop, + receiver + ) { + if (prop === 'children') { + if (!this._children) { + this._children = target.children.map( + this._proxyForChild.bind(this, receiver) + ); + } + + return this._children; + } else if (prop === 'parentSuite') { + return this._parentSuite; + } else { + this._maybeDeprecate(target, prop); + return target[prop]; + } + }; + + DeprecatingSuiteProxyHandler.prototype.set = function(target, prop, value) { + debugger; + this._maybeDeprecate(target, prop); + return (target[prop] = value); + }; + + DeprecatingSuiteProxyHandler.prototype._maybeDeprecate = function( + target, + prop + ) { + if (isMember(target, prop) && !isAllowedMember(prop)) { + this._env.deprecated(msg(prop)); + } + }; + + DeprecatingSuiteProxyHandler.prototype._proxyForChild = function( + ownProxy, + child + ) { + if (child.children) { + return deprecatingSuiteProxy(child, ownProxy, this._env); + } else { + return j$.deprecatingSpecProxy(child, this._env); + } + }; + + function deprecatingSuiteProxy(suite, parentSuite, env) { + return new Proxy(suite, new DeprecatingSuiteProxyHandler(parentSuite, env)); + } + + return deprecatingSuiteProxy; +}; + /* eslint-disable compat/compat */ // TODO: Remove this in the next major release. getJasmineRequireObj().deprecatingThisProxy = function(j$) { diff --git a/spec/core/EnvSpec.js b/spec/core/EnvSpec.js index 70bb24f8..09949c22 100644 --- a/spec/core/EnvSpec.js +++ b/spec/core/EnvSpec.js @@ -26,9 +26,141 @@ describe('Env', function() { }); describe('#topSuite', function() { - it('returns the Jasmine top suite for users to traverse the spec tree', function() { - var suite = env.topSuite(); + it('returns an object that describes the tree of suites and specs', function() { + var suite; + spyOn(env, 'deprecated'); + + env.it('a top level spec'); + env.describe('a suite', function() { + env.it('a spec'); + env.describe('a nested suite', function() { + env.it('a nested spec'); + }); + }); + + suite = env.topSuite(); expect(suite.description).toEqual('Jasmine__TopLevel__Suite'); + expect(suite.getFullName()).toEqual(''); + expect(suite.children.length).toEqual(2); + + expect(suite.children[0].description).toEqual('a top level spec'); + expect(suite.children[0].getFullName()).toEqual('a top level spec'); + expect(suite.children[0].children).toBeFalsy(); + + expect(suite.children[1].description).toEqual('a suite'); + expect(suite.children[1].getFullName()).toEqual('a suite'); + expect(suite.children[1].parentSuite).toBe(suite); + expect(suite.children[1].children.length).toEqual(2); + + expect(suite.children[1].children[0].description).toEqual('a spec'); + expect(suite.children[1].children[0].getFullName()).toEqual( + 'a suite a spec' + ); + expect(suite.children[1].children[0].children).toBeFalsy(); + + expect(suite.children[1].children[1].description).toEqual( + 'a nested suite' + ); + expect(suite.children[1].children[1].getFullName()).toEqual( + 'a suite a nested suite' + ); + expect(suite.children[1].children[1].parentSuite).toBe(suite.children[1]); + expect(suite.children[1].children[1].children.length).toEqual(1); + + expect(suite.children[1].children[1].children[0].description).toEqual( + 'a nested spec' + ); + expect(suite.children[1].children[1].children[0].getFullName()).toEqual( + 'a suite a nested suite a nested spec' + ); + expect(suite.children[1].children[1].children[0].children).toBeFalsy(); + }); + + it('does not deprecate access to public Suite and Spec members', function() { + jasmine.getEnv().requireProxy(); + var suite; + spyOn(env, 'deprecated'); + + env.it('a top level spec'); + env.describe('a suite', function() { + env.it('a spec'); + }); + + suite = env.topSuite(); + suite.description; + suite.getFullName(); + suite.children; + suite.parentSuite; + suite.children[0].description; + suite.children[0].getFullName(); + suite.children[0].children; + + suite.children[1].description; + suite.children[1].getFullName(); + suite.children[1].parentSuite; + suite.children[1].children; + + expect(env.deprecated).not.toHaveBeenCalled(); + }); + + it('deprecates access to internal Suite and Spec members', function() { + jasmine.getEnv().requireProxy(); + var topSuite, expectationFactory, spec; + + env.it('a top level spec'); + spyOn(env, 'deprecated'); + topSuite = env.topSuite(); + + topSuite.expectationFactory; + expect(env.deprecated).toHaveBeenCalledWith( + 'Access to private Suite ' + + 'members (in this case `expectationFactory`) via Env#topSuite is ' + + 'not supported and will break in a future release. See ' + + ' for correct usage.' + ); + env.deprecated.calls.reset(); + + topSuite.expectationFactory = expectationFactory; + expect(env.deprecated).toHaveBeenCalledWith( + 'Access to private Suite ' + + 'members (in this case `expectationFactory`) via Env#topSuite is ' + + 'not supported and will break in a future release. See ' + + ' for correct usage.' + ); + + topSuite.status(); + expect(env.deprecated).toHaveBeenCalledWith( + 'Access to private Suite ' + + 'members (in this case `status`) via Env#topSuite is ' + + 'not supported and will break in a future release. See ' + + ' for correct usage.' + ); + + spec = topSuite.children[0]; + spec.pend(); + expect(env.deprecated).toHaveBeenCalledWith( + 'Access to private Spec ' + + 'members (in this case `pend`) via Env#topSuite ' + + 'is not supported and will break in a future release. See ' + + ' for correct usage.' + ); + + expectationFactory = spec.expectationFactory; + expect(env.deprecated).toHaveBeenCalledWith( + 'Access to private Spec ' + + 'members (in this case `expectationFactory`) via Env#topSuite ' + + 'is not supported and will break in a future release. See ' + + ' for correct usage.' + ); + env.deprecated.calls.reset(); + + spec.expectationFactory = expectationFactory; + expect(env.deprecated).toHaveBeenCalledWith( + 'Access to private Spec ' + + 'members (in this case `expectationFactory`) via Env#topSuite ' + + 'is not supported and will break in a future release. See ' + + ' for correct usage.' + ); }); }); diff --git a/src/core/Env.js b/src/core/Env.js index 3e5fa8d2..1d143793 100644 --- a/src/core/Env.js +++ b/src/core/Env.js @@ -734,7 +734,7 @@ getJasmineRequireObj().Env = function(j$) { * @return {Suite} the root suite */ this.topSuite = function() { - return topSuite; + return j$.deprecatingSuiteProxy(topSuite, null, this); }; /** diff --git a/src/core/deprecatingSpecProxy.js b/src/core/deprecatingSpecProxy.js new file mode 100644 index 00000000..c490705f --- /dev/null +++ b/src/core/deprecatingSpecProxy.js @@ -0,0 +1,70 @@ +/* eslint-disable compat/compat */ +// TODO: Remove this in the next major release. +getJasmineRequireObj().deprecatingSpecProxy = function(j$) { + function isMember(target, prop) { + return ( + Object.keys(target).indexOf(prop) !== -1 || + Object.keys(j$.Spec.prototype).indexOf(prop) !== -1 + ); + } + + function isAllowedMember(prop) { + return prop === 'description' || prop === 'getFullName'; + } + + function msg(member) { + var memberName = member.toString().replace(/^Symbol\((.+)\)$/, '$1'); + return ( + 'Access to private Spec members (in this case `' + + memberName + + '`) via Env#topSuite is not supported and will break in ' + + 'a future release. See ' + + 'for correct usage.' + ); + } + + try { + new Proxy({}, {}); + } catch (e) { + // Environment does not support Poxy. + return function(spec) { + return spec; + }; + } + + function DeprecatingSpecProxyHandler(env) { + this._env = env; + } + + DeprecatingSpecProxyHandler.prototype.get = function(target, prop, receiver) { + this._maybeDeprecate(target, prop); + + if (prop === 'getFullName') { + // getFullName calls a private method. Re-bind 'this' to avoid a bogus + // deprecation warning. + return target.getFullName.bind(target); + } else { + return target[prop]; + } + }; + + DeprecatingSpecProxyHandler.prototype.set = function(target, prop, value) { + this._maybeDeprecate(target, prop); + return (target[prop] = value); + }; + + DeprecatingSpecProxyHandler.prototype._maybeDeprecate = function( + target, + prop + ) { + if (isMember(target, prop) && !isAllowedMember(prop)) { + this._env.deprecated(msg(prop)); + } + }; + + function deprecatingSpecProxy(spec, env) { + return new Proxy(spec, new DeprecatingSpecProxyHandler(env)); + } + + return deprecatingSpecProxy; +}; diff --git a/src/core/deprecatingSuiteProxy.js b/src/core/deprecatingSuiteProxy.js new file mode 100644 index 00000000..11759374 --- /dev/null +++ b/src/core/deprecatingSuiteProxy.js @@ -0,0 +1,98 @@ +/* eslint-disable compat/compat */ +// TODO: Remove this in the next major release. +getJasmineRequireObj().deprecatingSuiteProxy = function(j$) { + var allowedMembers = [ + 'children', + 'description', + 'parentSuite', + 'getFullName' + ]; + + function isMember(target, prop) { + return ( + Object.keys(target).indexOf(prop) !== -1 || + Object.keys(j$.Suite.prototype).indexOf(prop) !== -1 + ); + } + + function isAllowedMember(prop) { + return allowedMembers.indexOf(prop) !== -1; + } + + function msg(member) { + var memberName = member.toString().replace(/^Symbol\((.+)\)$/, '$1'); + return ( + 'Access to private Suite members (in this case `' + + memberName + + '`) via Env#topSuite is not supported and will break in ' + + 'a future release. See ' + + 'for correct usage.' + ); + } + try { + new Proxy({}, {}); + } catch (e) { + // Environment does not support Poxy. + return function(suite) { + return suite; + }; + } + + function DeprecatingSuiteProxyHandler(parentSuite, env) { + this._parentSuite = parentSuite; + this._env = env; + } + + DeprecatingSuiteProxyHandler.prototype.get = function( + target, + prop, + receiver + ) { + if (prop === 'children') { + if (!this._children) { + this._children = target.children.map( + this._proxyForChild.bind(this, receiver) + ); + } + + return this._children; + } else if (prop === 'parentSuite') { + return this._parentSuite; + } else { + this._maybeDeprecate(target, prop); + return target[prop]; + } + }; + + DeprecatingSuiteProxyHandler.prototype.set = function(target, prop, value) { + debugger; + this._maybeDeprecate(target, prop); + return (target[prop] = value); + }; + + DeprecatingSuiteProxyHandler.prototype._maybeDeprecate = function( + target, + prop + ) { + if (isMember(target, prop) && !isAllowedMember(prop)) { + this._env.deprecated(msg(prop)); + } + }; + + DeprecatingSuiteProxyHandler.prototype._proxyForChild = function( + ownProxy, + child + ) { + if (child.children) { + return deprecatingSuiteProxy(child, ownProxy, this._env); + } else { + return j$.deprecatingSpecProxy(child, this._env); + } + }; + + function deprecatingSuiteProxy(suite, parentSuite, env) { + return new Proxy(suite, new DeprecatingSuiteProxyHandler(parentSuite, env)); + } + + return deprecatingSuiteProxy; +}; diff --git a/src/core/requireCore.js b/src/core/requireCore.js index e20ad56d..80de1e28 100644 --- a/src/core/requireCore.js +++ b/src/core/requireCore.js @@ -45,6 +45,8 @@ var getJasmineRequireObj = (function(jasmineGlobal) { j$.Deprecator = jRequire.Deprecator(j$); j$.Env = jRequire.Env(j$); j$.deprecatingThisProxy = jRequire.deprecatingThisProxy(j$); + j$.deprecatingSuiteProxy = jRequire.deprecatingSuiteProxy(j$); + j$.deprecatingSpecProxy = jRequire.deprecatingSpecProxy(j$); j$.StackTrace = jRequire.StackTrace(j$); j$.ExceptionFormatter = jRequire.ExceptionFormatter(j$); j$.ExpectationFilterChain = jRequire.ExpectationFilterChain();