diff --git a/lib/jasmine-core/jasmine-html.js b/lib/jasmine-core/jasmine-html.js index 8399eda4..9c24ba17 100644 --- a/lib/jasmine-core/jasmine-html.js +++ b/lib/jasmine-core/jasmine-html.js @@ -423,9 +423,53 @@ jasmineRequire.HtmlReporter = function(j$) { ); } + if (result.trace) { + messages.appendChild(traceTable(result.trace)); + } + return failure; } + function traceTable(trace) { + var tbody = createDom('tbody'); + + trace.forEach(function(entry) { + tbody.appendChild( + createDom( + 'tr', + {}, + createDom('td', {}, entry.timestamp.toString()), + createDom('td', {}, entry.message) + ) + ); + }); + + return createDom( + 'div', + { className: 'jasmine-trace' }, + createDom( + 'div', + { className: 'jasmine-trace-header' }, + 'Trace information' + ), + createDom( + 'table', + {}, + createDom( + 'thead', + {}, + createDom( + 'tr', + {}, + createDom('th', {}, 'Time (ms)'), + createDom('th', {}, 'Message') + ) + ), + tbody + ) + ); + } + function summaryList(resultsTree, domParent) { var specListNode; for (var i = 0; i < resultsTree.children.length; i++) { diff --git a/lib/jasmine-core/jasmine.css b/lib/jasmine-core/jasmine.css index dba3ca98..87e5f002 100644 --- a/lib/jasmine-core/jasmine.css +++ b/lib/jasmine-core/jasmine.css @@ -268,4 +268,17 @@ body { border: 1px solid #ddd; background: white; white-space: pre; +} +.jasmine_html-reporter .jasmine-trace { + margin: 5px 0 0 0; + padding: 5px; + color: #666; + border: 1px solid #ddd; + background: white; +} +.jasmine_html-reporter .jasmine-trace table { + border-spacing: 0; +} +.jasmine_html-reporter .jasmine-trace table, .jasmine_html-reporter .jasmine-trace th, .jasmine_html-reporter .jasmine-trace td { + border: 1px solid #ddd; } \ No newline at end of file diff --git a/lib/jasmine-core/jasmine.js b/lib/jasmine-core/jasmine.js index ab9bb00e..f6778e23 100644 --- a/lib/jasmine-core/jasmine.js +++ b/lib/jasmine-core/jasmine.js @@ -589,6 +589,22 @@ getJasmineRequireObj().base = function(j$, jasmineGlobal) { putativeSpy.calls instanceof j$.CallTracker ); }; + + /** + * Logs a message for use in debugging. If the spec fails, trace messages + * will be included in the {@link SpecResult|result} passed to the + * reporter's specDone method. + * + * This method should be called only when a spec (including any associated + * beforeEach or afterEach functions) is running. + * @function + * @name jasmine.trace + * @since 3.10.0 + * @param {String} msg - The message to log + */ + j$.trace = function(msg) { + j$.getEnv().trace(msg); + }; }; getJasmineRequireObj().util = function(j$) { @@ -823,8 +839,9 @@ getJasmineRequireObj().Spec = function(j$) { * @property {String} status - Once the spec has completed, this string represents the pass/fail status of this spec. * @property {number} duration - The time in ms used by the spec execution, including any before/afterEach. * @property {Object} properties - User-supplied properties, if any, that were set using {@link Env#setSpecProperty} + * @property {TraceEntry[]|null} trace - Trace messages, if any, that were logged using {@link Env#trace} during a failing spec. * @since 2.0.0 -x */ + */ this.result = { id: this.id, description: this.description, @@ -834,7 +851,8 @@ x */ deprecationWarnings: [], pendingReason: '', duration: null, - properties: null + properties: null, + trace: null }; } @@ -879,6 +897,11 @@ x */ self.queueableFn.fn = null; self.result.status = self.status(excluded, failSpecWithNoExp); self.result.duration = self.timer.elapsed(); + + if (self.result.status !== 'failed') { + self.result.trace = null; + } + self.resultCallback(self.result, done); } }; @@ -990,6 +1013,20 @@ x */ ); }; + Spec.prototype.trace = function(msg) { + if (!this.result.trace) { + this.result.trace = []; + } + + /** + * @typedef TraceEntry + * @property {String} message - The message that was passed to {@link Env#trace}. + * @property {number} timestamp - The time when the entry was added, in + * milliseconds from the spec's start time + */ + this.result.trace.push({ message: msg, timestamp: this.timer.elapsed() }); + }; + var extractCustomPendingMessage = function(e) { var fullMessage = e.toString(), boilerplateStart = fullMessage.indexOf(Spec.pendingSpecExceptionMessage), @@ -2383,6 +2420,16 @@ getJasmineRequireObj().Env = function(j$) { currentSuite().setSuiteProperty(key, value); }; + this.trace = function(msg) { + var maybeSpec = currentRunnable(); + + if (!maybeSpec || !maybeSpec.trace) { + throw new Error("'trace' was called when there was no current spec"); + } + + maybeSpec.trace(msg); + }; + this.expect = function(actual) { if (!currentRunnable()) { throw new Error( diff --git a/spec/core/SpecSpec.js b/spec/core/SpecSpec.js index fa530049..9f250e9f 100644 --- a/spec/core/SpecSpec.js +++ b/spec/core/SpecSpec.js @@ -228,7 +228,8 @@ describe('Spec', function() { deprecationWarnings: [], pendingReason: '', duration: jasmine.any(Number), - properties: null + properties: null, + trace: null }, 'things' ); @@ -516,4 +517,110 @@ describe('Spec', function() { args.cleanupFns[0].fn(); expect(resultCallback.calls.first().args[0].failedExpectations).toEqual([]); }); + + describe('#trace', function() { + it('adds the messages to the result', function() { + var timer = jasmine.createSpyObj('timer', ['start', 'elapsed']), + spec = new jasmineUnderTest.Spec({ + queueableFn: { + fn: function() {} + }, + queueRunnerFactory: function() {}, + timer: timer + }), + t1 = 123, + t2 = 456; + + spec.execute(); + expect(spec.result.trace).toBeNull(); + timer.elapsed.and.returnValue(t1); + spec.trace('msg 1'); + expect(spec.result.trace).toEqual([{ message: 'msg 1', timestamp: t1 }]); + timer.elapsed.and.returnValue(t2); + spec.trace('msg 2'); + expect(spec.result.trace).toEqual([ + { message: 'msg 1', timestamp: t1 }, + { message: 'msg 2', timestamp: t2 } + ]); + }); + + describe('When the spec passes', function() { + it('omits the messages from the reported result', function() { + var resultCallback = jasmine.createSpy('resultCallback'), + spec = new jasmineUnderTest.Spec({ + queueableFn: { + fn: function() {} + }, + resultCallback: resultCallback, + queueRunnerFactory: function(config) { + spec.trace('msg'); + config.cleanupFns.forEach(function(fn) { + fn.fn(); + }); + config.onComplete(false); + } + }); + + spec.execute(function() {}); + expect(resultCallback).toHaveBeenCalledWith( + jasmine.objectContaining({ trace: null }), + undefined + ); + }); + + it('removes the messages to save memory', function() { + var resultCallback = jasmine.createSpy('resultCallback'), + spec = new jasmineUnderTest.Spec({ + queueableFn: { + fn: function() {} + }, + resultCallback: resultCallback, + queueRunnerFactory: function(config) { + spec.trace('msg'); + config.cleanupFns.forEach(function(fn) { + fn.fn(); + }); + config.onComplete(false); + } + }); + + spec.execute(function() {}); + expect(resultCallback).toHaveBeenCalled(); + expect(spec.result.trace).toBeNull(); + }); + }); + + describe('When the spec fails', function() { + it('includes the messages in the reported result', function() { + var resultCallback = jasmine.createSpy('resultCallback'), + timer = jasmine.createSpyObj('timer', ['start', 'elapsed']), + spec = new jasmineUnderTest.Spec({ + queueableFn: { + fn: function() {} + }, + resultCallback: resultCallback, + queueRunnerFactory: function(config) { + spec.trace('msg'); + spec.onException(new Error('nope')); + config.cleanupFns.forEach(function(fn) { + fn.fn(); + }); + config.onComplete(true); + }, + timer: timer + }), + timestamp = 12345; + + timer.elapsed.and.returnValue(timestamp); + + spec.execute(function() {}); + expect(resultCallback).toHaveBeenCalledWith( + jasmine.objectContaining({ + trace: [{ message: 'msg', timestamp: timestamp }] + }), + undefined + ); + }); + }); + }); }); diff --git a/spec/core/baseSpec.js b/spec/core/baseSpec.js index 0c6ef23a..a35edff7 100644 --- a/spec/core/baseSpec.js +++ b/spec/core/baseSpec.js @@ -186,4 +186,12 @@ describe('base helpers', function() { }); }); }); + + describe('trace', function() { + it("forwards to the current env's trace function", function() { + spyOn(jasmineUnderTest.getEnv(), 'trace'); + jasmineUnderTest.trace('a message'); + expect(jasmineUnderTest.getEnv().trace).toHaveBeenCalledWith('a message'); + }); + }); }); diff --git a/spec/core/integration/EnvSpec.js b/spec/core/integration/EnvSpec.js index b257d085..bf70f9dc 100644 --- a/spec/core/integration/EnvSpec.js +++ b/spec/core/integration/EnvSpec.js @@ -3234,4 +3234,87 @@ describe('Env integration', function() { }); }); }); + + it('sends traces to the reporter when the spec fails', function(done) { + var reporter = jasmine.createSpyObj('reporter', ['specDone']), + startTime, + endTime; + + env.addReporter(reporter); + env.configure({ random: false }); + + env.it('fails', function() { + startTime = new Date().getTime(); + env.trace('message 1'); + env.trace('message 2'); + env.expect(1).toBe(2); + endTime = new Date().getTime(); + }); + + env.it('passes', function() { + env.trace('message that should not be reported'); + }); + + env.execute(null, function() { + function numberInRange(min, max) { + return { + asymmetricMatch: function(compareTo) { + return compareTo >= min && compareTo <= max; + }, + jasmineToString: function(pp) { + return ''; + } + }; + } + + var duration; + + expect(reporter.specDone).toHaveBeenCalledTimes(2); + duration = reporter.specDone.calls.argsFor(0)[0].duration; + expect(reporter.specDone.calls.argsFor(0)[0]).toEqual( + jasmine.objectContaining({ + trace: [ + { + timestamp: numberInRange(0, duration), + message: 'message 1' + }, + { + timestamp: numberInRange(0, duration), + message: 'message 2' + } + ] + }) + ); + expect(reporter.specDone.calls.argsFor(1)[0].trace).toBeFalsy(); + done(); + }); + }); + + it('reports an error when trace is used when a spec is not running', function(done) { + var reporter = jasmine.createSpyObj('reporter', ['suiteDone']); + + env.describe('a suite', function() { + env.beforeAll(function() { + env.trace('a message'); + }); + + env.it('a spec', function() {}); + }); + + env.addReporter(reporter); + env.execute(null, function() { + expect(reporter.suiteDone).toHaveBeenCalledWith( + jasmine.objectContaining({ + failedExpectations: [ + jasmine.objectContaining({ + message: jasmine.stringContaining( + "'trace' was called when there was no current spec" + ) + }) + ] + }) + ); + done(); + }); + }); }); diff --git a/spec/html/HtmlReporterSpec.js b/spec/html/HtmlReporterSpec.js index 5fef9a21..13dce454 100644 --- a/spec/html/HtmlReporterSpec.js +++ b/spec/html/HtmlReporterSpec.js @@ -1339,6 +1339,23 @@ describe('HtmlReporter', function() { } ] }; + var failingSpecResultWithTrace = { + id: 567, + status: 'failed', + description: 'a failing spec', + fullName: 'a suite inner suite a failing spec', + passedExpectations: [], + failedExpectations: [ + { + message: 'a failure message', + stack: 'a stack trace' + } + ], + trace: [ + { timestamp: 123, message: 'msg 1' }, + { timestamp: 456, message: 'msg 1' } + ] + }; var passingSuiteResult = { id: 1, @@ -1356,18 +1373,20 @@ describe('HtmlReporter', function() { reporter.suiteDone(passingSuiteResult); reporter.suiteDone(failingSuiteResult); reporter.suiteDone(passingSuiteResult); + reporter.specStarted(failingSpecResultWithTrace); + reporter.specDone(failingSpecResultWithTrace); reporter.jasmineDone({}); }); it('reports the specs counts', function() { var alertBar = container.querySelector('.jasmine-alert .jasmine-bar'); - expect(alertBar.innerHTML).toMatch(/2 specs, 2 failure/); + expect(alertBar.innerHTML).toMatch(/3 specs, 3 failures/); }); it('reports failure messages and stack traces', function() { var specFailures = container.querySelector('.jasmine-failures'); - expect(specFailures.childNodes.length).toEqual(2); + expect(specFailures.childNodes.length).toEqual(3); var specFailure = specFailures.childNodes[0]; expect(specFailure.getAttribute('class')).toMatch(/jasmine-failed/); @@ -1408,6 +1427,18 @@ describe('HtmlReporter', function() { expect(suiteStackTrace.innerHTML).toEqual('a stack trace'); }); + it('reports traces when present', function() { + var specFailure = container.querySelectorAll( + '.jasmine-spec-detail.jasmine-failed' + )[2], + trace = specFailure.querySelector('.jasmine-trace table'), + rows; + + expect(trace).toBeTruthy(); + rows = trace.querySelectorAll('tbody tr'); + expect(rows.length).toEqual(2); + }); + it('provides links to focus on a failure and each containing suite', function() { var description = container.querySelector( '.jasmine-failures .jasmine-description' diff --git a/src/core/Env.js b/src/core/Env.js index 895579c1..3cd79c5f 100644 --- a/src/core/Env.js +++ b/src/core/Env.js @@ -1318,6 +1318,16 @@ getJasmineRequireObj().Env = function(j$) { currentSuite().setSuiteProperty(key, value); }; + this.trace = function(msg) { + var maybeSpec = currentRunnable(); + + if (!maybeSpec || !maybeSpec.trace) { + throw new Error("'trace' was called when there was no current spec"); + } + + maybeSpec.trace(msg); + }; + this.expect = function(actual) { if (!currentRunnable()) { throw new Error( diff --git a/src/core/Spec.js b/src/core/Spec.js index 2691ce04..cd23a238 100644 --- a/src/core/Spec.js +++ b/src/core/Spec.js @@ -68,8 +68,9 @@ getJasmineRequireObj().Spec = function(j$) { * @property {String} status - Once the spec has completed, this string represents the pass/fail status of this spec. * @property {number} duration - The time in ms used by the spec execution, including any before/afterEach. * @property {Object} properties - User-supplied properties, if any, that were set using {@link Env#setSpecProperty} + * @property {TraceEntry[]|null} trace - Trace messages, if any, that were logged using {@link Env#trace} during a failing spec. * @since 2.0.0 -x */ + */ this.result = { id: this.id, description: this.description, @@ -79,7 +80,8 @@ x */ deprecationWarnings: [], pendingReason: '', duration: null, - properties: null + properties: null, + trace: null }; } @@ -124,6 +126,11 @@ x */ self.queueableFn.fn = null; self.result.status = self.status(excluded, failSpecWithNoExp); self.result.duration = self.timer.elapsed(); + + if (self.result.status !== 'failed') { + self.result.trace = null; + } + self.resultCallback(self.result, done); } }; @@ -235,6 +242,20 @@ x */ ); }; + Spec.prototype.trace = function(msg) { + if (!this.result.trace) { + this.result.trace = []; + } + + /** + * @typedef TraceEntry + * @property {String} message - The message that was passed to {@link Env#trace}. + * @property {number} timestamp - The time when the entry was added, in + * milliseconds from the spec's start time + */ + this.result.trace.push({ message: msg, timestamp: this.timer.elapsed() }); + }; + var extractCustomPendingMessage = function(e) { var fullMessage = e.toString(), boilerplateStart = fullMessage.indexOf(Spec.pendingSpecExceptionMessage), diff --git a/src/core/base.js b/src/core/base.js index ee5c0cad..912e52f2 100644 --- a/src/core/base.js +++ b/src/core/base.js @@ -420,4 +420,20 @@ getJasmineRequireObj().base = function(j$, jasmineGlobal) { putativeSpy.calls instanceof j$.CallTracker ); }; + + /** + * Logs a message for use in debugging. If the spec fails, trace messages + * will be included in the {@link SpecResult|result} passed to the + * reporter's specDone method. + * + * This method should be called only when a spec (including any associated + * beforeEach or afterEach functions) is running. + * @function + * @name jasmine.trace + * @since 3.10.0 + * @param {String} msg - The message to log + */ + j$.trace = function(msg) { + j$.getEnv().trace(msg); + }; }; diff --git a/src/html/HtmlReporter.js b/src/html/HtmlReporter.js index dc91c4dc..80b9d03e 100644 --- a/src/html/HtmlReporter.js +++ b/src/html/HtmlReporter.js @@ -392,9 +392,53 @@ jasmineRequire.HtmlReporter = function(j$) { ); } + if (result.trace) { + messages.appendChild(traceTable(result.trace)); + } + return failure; } + function traceTable(trace) { + var tbody = createDom('tbody'); + + trace.forEach(function(entry) { + tbody.appendChild( + createDom( + 'tr', + {}, + createDom('td', {}, entry.timestamp.toString()), + createDom('td', {}, entry.message) + ) + ); + }); + + return createDom( + 'div', + { className: 'jasmine-trace' }, + createDom( + 'div', + { className: 'jasmine-trace-header' }, + 'Trace information' + ), + createDom( + 'table', + {}, + createDom( + 'thead', + {}, + createDom( + 'tr', + {}, + createDom('th', {}, 'Time (ms)'), + createDom('th', {}, 'Message') + ) + ), + tbody + ) + ); + } + function summaryList(resultsTree, domParent) { var specListNode; for (var i = 0; i < resultsTree.children.length; i++) { diff --git a/src/html/_HTMLReporter.scss b/src/html/_HTMLReporter.scss index 2ee9c9be..7ab2d515 100644 --- a/src/html/_HTMLReporter.scss +++ b/src/html/_HTMLReporter.scss @@ -388,4 +388,20 @@ body { background: white; white-space: pre; } + + .jasmine-trace { + margin: 5px 0 0 0; + padding: 5px; + color: $light-text-color; + border: 1px solid #ddd; + background: white; + + table { + border-spacing: 0; + } + + table, th, td { + border: 1px solid #ddd; + } + } }