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 1e4c891c..75c578c5 100644 --- a/lib/jasmine-core/jasmine.js +++ b/lib/jasmine-core/jasmine.js @@ -64,7 +64,10 @@ 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$.deprecatingSuiteProxy = jRequire.deprecatingSuiteProxy(j$); + j$.deprecatingSpecProxy = jRequire.deprecatingSpecProxy(j$); j$.StackTrace = jRequire.StackTrace(j$); j$.ExceptionFormatter = jRequire.ExceptionFormatter(j$); j$.ExpectationFilterChain = jRequire.ExpectationFilterChain(); @@ -1001,7 +1004,6 @@ getJasmineRequireObj().Env = function(j$) { var currentlyExecutingSuites = []; var currentDeclarationSuite = null; var hasFailures = false; - var deprecationsToSuppress = []; /** * This represents the available options to configure Jasmine. @@ -1197,6 +1199,7 @@ getJasmineRequireObj().Env = function(j$) { if (configuration.hasOwnProperty('verboseDeprecations')) { config.verboseDeprecations = configuration.verboseDeprecations; + deprecator.verboseDeprecations(config.verboseDeprecations); } }; @@ -1451,48 +1454,31 @@ getJasmineRequireObj().Env = function(j$) { return buildExpectationResult(attrs); }; - 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) { @@ -1528,6 +1514,7 @@ getJasmineRequireObj().Env = function(j$) { asyncExpectationFactory: suiteAsyncExpectationFactory, expectationResultFactory: expectationResultFactory }); + var deprecator = new j$.Deprecator(topSuite); defaultResourcesForRunnable(topSuite.id); currentDeclarationSuite = topSuite; @@ -1539,7 +1526,7 @@ getJasmineRequireObj().Env = function(j$) { * @return {Suite} the root suite */ this.topSuite = function() { - return topSuite; + return j$.deprecatingSuiteProxy(topSuite, null, this); }; /** @@ -3367,6 +3354,267 @@ 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; +}; + +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() {} @@ -3779,7 +4027,7 @@ getJasmineRequireObj().buildExpectationResult = function(j$) { var result = { matcherName: options.matcherName, message: message(), - stack: stack(), + stack: options.omitStackTrace ? '' : stack(), passed: options.passed }; @@ -7466,8 +7714,12 @@ 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.onException( 'An asynchronous before/it/after ' + @@ -7483,6 +7735,8 @@ getJasmineRequireObj().QueueRunner = function(j$) { '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 23c0d373..56412c7a 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.' + ); }); }); @@ -289,60 +421,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/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 d476426f..707bab02 100644 --- a/spec/core/integration/EnvSpec.js +++ b/spec/core/integration/EnvSpec.js @@ -2627,70 +2627,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/helpers/checkForProxy.js b/spec/helpers/checkForProxy.js new file mode 100644 index 00000000..b4062d0b --- /dev/null +++ b/spec/helpers/checkForProxy.js @@ -0,0 +1,17 @@ +/* eslint-disable compat/compat */ +(function(env) { + function hasProxyConstructor() { + try { + new Proxy({}, {}); + return true; + } catch (e) { + return false; + } + } + + env.requireProxy = function() { + if (!hasProxyConstructor()) { + env.pending('Environment does not support Proxy'); + } + }; +})(jasmine.getEnv()); 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/spec/support/jasmine-browser.js b/spec/support/jasmine-browser.js index 9062bf8a..1fc16d01 100644 --- a/spec/support/jasmine-browser.js +++ b/spec/support/jasmine-browser.js @@ -21,6 +21,7 @@ module.exports = { 'helpers/generator.js', 'helpers/BrowserFlags.js', 'helpers/checkForMap.js', + 'helpers/checkForProxy.js', 'helpers/checkForSet.js', 'helpers/checkForSymbol.js', 'helpers/checkForUrl.js', diff --git a/spec/support/jasmine.json b/spec/support/jasmine.json index 1a0ca3b2..d6369bf2 100644 --- a/spec/support/jasmine.json +++ b/spec/support/jasmine.json @@ -8,6 +8,7 @@ "helpers/asyncAwait.js", "helpers/generator.js", "helpers/checkForMap.js", + "helpers/checkForProxy.js", "helpers/checkForSet.js", "helpers/checkForSymbol.js", "helpers/checkForUrl.js", 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 39c8cf81..3368de22 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); } }; @@ -482,48 +482,31 @@ getJasmineRequireObj().Env = function(j$) { return buildExpectationResult(attrs); }; - 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) { @@ -559,6 +542,7 @@ getJasmineRequireObj().Env = function(j$) { asyncExpectationFactory: suiteAsyncExpectationFactory, expectationResultFactory: expectationResultFactory }); + var deprecator = new j$.Deprecator(topSuite); defaultResourcesForRunnable(topSuite.id); currentDeclarationSuite = topSuite; @@ -570,7 +554,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/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 b3458bab..e811f0b8 100644 --- a/src/core/QueueRunner.js +++ b/src/core/QueueRunner.js @@ -215,8 +215,12 @@ 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.onException( 'An asynchronous before/it/after ' + @@ -232,6 +236,8 @@ getJasmineRequireObj().QueueRunner = function(j$) { 'function to not return a promise.' ); } + + this.deprecated(msg, { omitStackTrace: true }); } }; 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 aac94b87..2dd88c06 100644 --- a/src/core/requireCore.js +++ b/src/core/requireCore.js @@ -42,7 +42,10 @@ 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$.deprecatingSuiteProxy = jRequire.deprecatingSuiteProxy(j$); + j$.deprecatingSpecProxy = jRequire.deprecatingSpecProxy(j$); j$.StackTrace = jRequire.StackTrace(j$); j$.ExceptionFormatter = jRequire.ExceptionFormatter(j$); j$.ExpectationFilterChain = jRequire.ExpectationFilterChain(); 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; + } }