diff --git a/lib/jasmine-core/jasmine.js b/lib/jasmine-core/jasmine.js index d02dd1be..e55f611f 100644 --- a/lib/jasmine-core/jasmine.js +++ b/lib/jasmine-core/jasmine.js @@ -617,7 +617,7 @@ getJasmineRequireObj().base = function(j$, jasmineGlobal) { /** * Logs a message for use in debugging. If the spec fails, trace messages - * will be included in the {@link SpecResult|result} passed to the + * will be included in the {@link SpecDoneEvent|result} passed to the * reporter's specDone method. * * This method should be called only when a spec (including any associated @@ -844,6 +844,7 @@ getJasmineRequireObj().Spec = function(j$) { return this.result.properties[key]; } + // TODO: throw if the key or value is not structred cloneable setSpecProperty(key, value) { this.result.properties = this.result.properties || {}; this.result.properties[key] = value; @@ -867,8 +868,45 @@ getJasmineRequireObj().Spec = function(j$) { } reset() { + this.result = { + id: this.id, + description: this.description, + fullName: this.getFullName(), + parentSuiteId: this.parentSuiteId, + filename: this.filename, + failedExpectations: [], + passedExpectations: [], + deprecationWarnings: [], + pendingReason: this.excludeMessage || '', + duration: null, + properties: null, + debugLogs: null + }; + this.markedPending = this.markedExcluding; + this.reportedDone = false; + } + + startedEvent() { /** - * @typedef SpecResult + * @typedef SpecStartedEvent + * @property {String} id - The unique id of this spec. + * @property {String} description - The description passed to the {@link it} that created this spec. + * @property {String} fullName - The full description including all ancestors of this spec. + * @property {String|null} parentSuiteId - The ID of the suite containing this spec, or null if this spec is not in a describe(). + * @property {String} filename - Deprecated. The name of the file the spec was defined in. + * Note: The value may be incorrect if zone.js is installed or + * `it`/`fit`/`xit` have been replaced with versions that don't maintain the + * same call stack height as the originals. This property may be removed in + * a future version unless there is enough user interest in keeping it. + * See {@link https://github.com/jasmine/jasmine/issues/2065}. + * @since 6.0.0 + */ + return this.#commonEventFields(); + } + + doneEvent() { + /** + * @typedef SpecDoneEvent * @property {String} id - The unique id of this spec. * @property {String} description - The description passed to the {@link it} that created this spec. * @property {String} fullName - The full description including all ancestors of this spec. @@ -887,24 +925,37 @@ getJasmineRequireObj().Spec = function(j$) { * @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 {DebugLogEntry[]|null} debugLogs - Messages, if any, that were logged using {@link jasmine.debugLog} during a failing spec. - * @since 2.0.0 + * @since 6.0.0 */ - this.result = { + const event = { + ...this.#commonEventFields() + }; + const toCopy = [ + 'failedExpectations', + 'passedExpectations', + 'deprecationWarnings', + 'pendingReason', + 'status', + 'duration', + 'properties', + 'debugLogs' + ]; + + for (const k of toCopy) { + event[k] = this.result[k]; + } + + return event; + } + + #commonEventFields() { + return { id: this.id, description: this.description, fullName: this.getFullName(), parentSuiteId: this.parentSuiteId, - filename: this.filename, - failedExpectations: [], - passedExpectations: [], - deprecationWarnings: [], - pendingReason: this.excludeMessage || '', - duration: null, - properties: null, - debugLogs: null + filename: this.filename }; - this.markedPending = this.markedExcluding; - this.reportedDone = false; } handleException(e) { @@ -1747,7 +1798,7 @@ getJasmineRequireObj().Env = function(j$) { }; /** - * Get a user-defined property as part of the properties field of {@link SpecResult} + * Get a user-defined property as part of the properties field of {@link SpecDoneEvent} * @name Env#getSpecProperty * @since 5.10.0 * @function @@ -2039,7 +2090,7 @@ getJasmineRequireObj().JsApiReporter = function(j$) { * @function * @param {Number} index - The position in the specs list to start from. * @param {Number} length - Maximum number of specs results to return. - * @return {SpecResult[]} + * @return {SpecDoneEvent[]} */ this.specResults = function(index, length) { return specs.slice(index, index + length); @@ -2050,7 +2101,7 @@ getJasmineRequireObj().JsApiReporter = function(j$) { * @name jsApiReporter#specs * @since 2.0.0 * @function - * @return {SpecResult[]} + * @return {SpecDoneEvent[]} */ this.specs = function() { return specs; @@ -8715,7 +8766,7 @@ getJasmineRequireObj().reporterEvents = function() { * `specStarted` is invoked when an `it` starts to run (including associated `beforeEach` functions) * @function * @name Reporter#specStarted - * @param {SpecResult} result Information about the individual {@link it} being run + * @param {SpecStartedEvent} result Information about the individual {@link it} being run * @param {Function} [done] Used to specify to Jasmine that this callback is asynchronous and Jasmine should wait until it has been called before moving on. * @returns {} Optionally return a Promise instead of using `done` to cause Jasmine to wait for completion. * @see async @@ -8727,7 +8778,7 @@ getJasmineRequireObj().reporterEvents = function() { * While jasmine doesn't require any specific functions, not defining a `specDone` will make it impossible for a reporter to know when a spec has failed. * @function * @name Reporter#specDone - * @param {SpecResult} result + * @param {SpecDoneEvent} result * @param {Function} [done] Used to specify to Jasmine that this callback is asynchronous and Jasmine should wait until it has been called before moving on. * @returns {} Optionally return a Promise instead of using `done` to cause Jasmine to wait for completion. * @see async @@ -11514,7 +11565,7 @@ getJasmineRequireObj().TreeRunner = function(j$) { const onStart = next => { this.#currentRunableTracker.setCurrentSpec(spec); this.#runableResources.initForRunable(spec.id, spec.parentSuiteId); - this.#reportDispatcher.specStarted(spec.result).then(next); + this.#reportDispatcher.specStarted(spec.startedEvent()).then(next); }; const resultCallback = (result, next) => { this.#specComplete(spec).then(next); @@ -11721,7 +11772,7 @@ getJasmineRequireObj().TreeRunner = function(j$) { async #reportSpecDone(spec) { spec.reportedDone = true; - await this.#reportDispatcher.specDone(spec.result); + await this.#reportDispatcher.specDone(spec.doneEvent()); } async #reportChildrenOfBeforeAllFailure(suite) { @@ -11737,7 +11788,7 @@ getJasmineRequireObj().TreeRunner = function(j$) { await this.#reportDispatcher.suiteDone(child.result); } else { /* a spec */ - await this.#reportDispatcher.specStarted(child.result); + await this.#reportDispatcher.specStarted(child.startedEvent()); child.addExpectationResult( false, diff --git a/spec/core/SpecSpec.js b/spec/core/SpecSpec.js index 88f55543..5d727729 100644 --- a/spec/core/SpecSpec.js +++ b/spec/core/SpecSpec.js @@ -412,4 +412,246 @@ describe('Spec', function() { }); }); }); + + describe('#startedEvent', function() { + it('includes only properties that are known before execution', function() { + const spec = new jasmineUnderTest.Spec({ + id: 'spec1', + parentSuiteId: 'suite1', + description: 'a spec', + filename: 'somefile.js', + getPath() { + return ['a suite', 'a spec']; + }, + queueableFn: { fn: () => {} } + }); + + expect(spec.startedEvent()).toEqual({ + id: 'spec1', + parentSuiteId: 'suite1', + description: 'a spec', + fullName: 'a suite a spec', + filename: 'somefile.js' + }); + }); + }); + + describe('#doneEvent', function() { + it('returns the event for a passed spec', function() { + const timer = { + start() {}, + elapsed() { + return 123; + } + }; + const spec = new jasmineUnderTest.Spec({ + id: 'spec1', + parentSuiteId: 'suite1', + description: 'a spec', + filename: 'somefile.js', + getPath() { + return ['a suite', 'a spec']; + }, + queueableFn: { fn: () => {} }, + timer: timer + }); + + spec.addExpectationResult(true, { + matcherName: 'a passing expectation', + passed: true + }); + spec.executionFinished(false, false); + + expect(spec.doneEvent()).toEqual({ + id: 'spec1', + parentSuiteId: 'suite1', + description: 'a spec', + fullName: 'a suite a spec', + filename: 'somefile.js', + status: 'passed', + passedExpectations: [ + { + matcherName: 'a passing expectation', + passed: true, + message: 'Passed.', + stack: '', + globalErrorType: undefined + } + ], + failedExpectations: [], + deprecationWarnings: [], + debugLogs: null, // TODO change to [] + properties: null, // TODO change to {} + pendingReason: '', + duration: 123 + }); + }); + + it('returns the event for a failed spec', function() { + const timer = { + start() {}, + elapsed() { + return 123; + } + }; + const spec = new jasmineUnderTest.Spec({ + id: 'spec1', + parentSuiteId: 'suite1', + description: 'a spec', + filename: 'somefile.js', + getPath() { + return ['a suite', 'a spec']; + }, + queueableFn: { fn: () => {} }, + timer: timer + }); + + spec.addExpectationResult(true, { + matcherName: 'a passing expectation', + passed: true + }); + spec.addExpectationResult(false, { + matcherName: 'a failing expectation', + passed: false, + error: new Error('failed') + }); + spec.executionFinished(false, false); + + expect(spec.doneEvent()).toEqual({ + id: 'spec1', + parentSuiteId: 'suite1', + description: 'a spec', + fullName: 'a suite a spec', + filename: 'somefile.js', + status: 'failed', + passedExpectations: [ + { + matcherName: 'a passing expectation', + passed: true, + message: 'Passed.', + stack: '', + globalErrorType: undefined + } + ], + failedExpectations: [ + { + matcherName: 'a failing expectation', + passed: false, + message: jasmine.stringMatching(/^Error: failed/), + stack: jasmine.stringContaining('SpecSpec.js'), + globalErrorType: undefined + } + ], + deprecationWarnings: [], + debugLogs: null, // TODO change to [] + properties: null, // TODO change to {} + pendingReason: '', + duration: 123 + }); + }); + + it("reports a status of 'pending' for a declaratively pended spec", function() { + const spec = new jasmineUnderTest.Spec({ + queueableFn: {} + }); + + spec.executionFinished(false, false); + + const result = spec.doneEvent(); + expect(result.status).toEqual('pending'); + expect(result.pendingReason).toEqual(''); + }); + + it("reports a status of 'pending' for a spec pended by #pend", function() { + const spec = new jasmineUnderTest.Spec({ + queueableFn: { fn: () => {} } + }); + + spec.pend('nope'); + spec.executionFinished(false, false); + + const result = spec.doneEvent(); + expect(result.status).toEqual('pending'); + expect(result.pendingReason).toEqual('nope'); + }); + + it("reports a status of 'excluded' for an excluded spec", function() { + const spec = new jasmineUnderTest.Spec({ + queueableFn: { fn: () => {} } + }); + + spec.executionFinished(true, false); + + expect(spec.doneEvent().status).toEqual('excluded'); + }); + + describe('When failSpecWithNoExpectations is true', function() { + it("reports a status of 'failed' for a spec with no expectations", function() { + const spec = new jasmineUnderTest.Spec({ + queueableFn: { fn: () => {} } + }); + + spec.executionFinished(false, true); + + expect(spec.doneEvent().status).toEqual('failed'); + }); + }); + + it('includes deprecation warnings', function() { + const spec = new jasmineUnderTest.Spec({ + queueableFn: { fn: () => {} } + }); + + spec.addDeprecationWarning('stop that'); + + expect(spec.doneEvent().deprecationWarnings).toEqual([ + { + // TODO: remove irrelevant properties + message: 'stop that', + stack: jasmine.stringContaining('SpecSpec.js'), + matcherName: undefined, + passed: undefined, + globalErrorType: undefined + } + ]); + }); + + it('includes debug logs', function() { + const timer = { + start() {}, + elapsed() { + return 123; + } + }; + const spec = new jasmineUnderTest.Spec({ + timer, + queueableFn: { fn: () => {} } + }); + + spec.debugLog('maybe this will help'); + + expect(spec.doneEvent().debugLogs).toEqual([ + { + message: 'maybe this will help', + timestamp: 123 + } + ]); + }); + + it('includes spec properties', function() { + const spec = new jasmineUnderTest.Spec({ + queueableFn: { fn: () => {} } + }); + + spec.setSpecProperty('foo', 'bar'); + spec.setSpecProperty('baz', { grault: ['wombat'] }); + + expect(spec.doneEvent().properties).toEqual({ + foo: 'bar', + baz: { grault: ['wombat'] } + }); + }); + + // it("excludes properties that aren't in the public API"); + }); }); diff --git a/spec/core/TreeRunnerSpec.js b/spec/core/TreeRunnerSpec.js index 93eab8c0..ee53c123 100644 --- a/spec/core/TreeRunnerSpec.js +++ b/spec/core/TreeRunnerSpec.js @@ -28,7 +28,9 @@ describe('TreeRunner', function() { spec.id, spec.parentSuiteId ); - expect(reportDispatcher.specStarted).toHaveBeenCalledWith(spec.result); + expect(reportDispatcher.specStarted).toHaveBeenCalledWith( + spec.startedEvent() + ); await Promise.resolve(); expect(reportDispatcher.specStarted).toHaveBeenCalledBefore(next); await expectAsync(executePromise).toBePending(); @@ -61,7 +63,7 @@ describe('TreeRunner', function() { expect(currentRunableTracker.currentSpec()).toBeFalsy(); expect(runableResources.clearForRunable).toHaveBeenCalledWith(spec.id); - expect(reportDispatcher.specDone).toHaveBeenCalledWith(spec.result); + expect(reportDispatcher.specDone).toHaveBeenCalledWith(spec.doneEvent()); expect(spec.result.duration).toEqual('the elapsed time'); expect(spec.reportedDone).toEqual(true); await Promise.resolve(); diff --git a/spec/core/integration/EnvSpec.js b/spec/core/integration/EnvSpec.js index a62731a3..688ab1c0 100644 --- a/spec/core/integration/EnvSpec.js +++ b/spec/core/integration/EnvSpec.js @@ -1636,15 +1636,18 @@ describe('Env integration', function() { expect(reporter.specDone.calls.count()).toBe(6); const baseSpecEvent = { + id: jasmine.any(String), + filename: jasmine.any(String) + }; + const baseSpecDoneEvent = { + ...baseSpecEvent, passedExpectations: [], failedExpectations: [], deprecationWarnings: [], pendingReason: '', duration: null, properties: null, - debugLogs: null, - id: jasmine.any(String), - filename: jasmine.any(String) + debugLogs: null }; expect(reporter.specStarted.calls.argsFor(0)[0]).toEqual({ @@ -1654,7 +1657,7 @@ describe('Env integration', function() { parentSuiteId: null }); expect(reporter.specDone.calls.argsFor(0)[0]).toEqual({ - ...baseSpecEvent, + ...baseSpecDoneEvent, description: 'a top level spec', fullName: 'a top level spec', status: 'passed', @@ -1668,7 +1671,7 @@ describe('Env integration', function() { parentSuiteId: suiteFullNameToId['A Suite'] }); expect(reporter.specDone.calls.argsFor(1)[0]).toEqual({ - ...baseSpecEvent, + ...baseSpecDoneEvent, description: 'with a spec', fullName: 'A Suite with a spec', status: 'passed', @@ -1689,11 +1692,10 @@ describe('Env integration', function() { ...baseSpecEvent, description: "with an x'ed spec", fullName: "A Suite with a nested suite with an x'ed spec", - parentSuiteId: suiteFullNameToId['A Suite with a nested suite'], - pendingReason: 'Temporarily disabled with xit' + parentSuiteId: suiteFullNameToId['A Suite with a nested suite'] }); expect(reporter.specDone.calls.argsFor(2)[0]).toEqual({ - ...baseSpecEvent, + ...baseSpecDoneEvent, description: "with an x'ed spec", fullName: "A Suite with a nested suite with an x'ed spec", status: 'pending', @@ -1709,7 +1711,7 @@ describe('Env integration', function() { parentSuiteId: suiteFullNameToId['A Suite with a nested suite'] }); expect(reporter.specDone.calls.argsFor(3)[0]).toEqual({ - ...baseSpecEvent, + ...baseSpecDoneEvent, description: 'with a spec', fullName: 'A Suite with a nested suite with a spec', status: 'failed', @@ -1730,7 +1732,7 @@ describe('Env integration', function() { parentSuiteId: suiteFullNameToId['A Suite with only non-executable specs'] }); expect(reporter.specDone.calls.argsFor(4)[0]).toEqual({ - ...baseSpecEvent, + ...baseSpecDoneEvent, description: 'is pending', status: 'pending', fullName: 'A Suite with only non-executable specs is pending', @@ -1743,12 +1745,10 @@ describe('Env integration', function() { ...baseSpecEvent, description: 'is xed', fullName: 'A Suite with only non-executable specs is xed', - parentSuiteId: - suiteFullNameToId['A Suite with only non-executable specs'], - pendingReason: 'Temporarily disabled with xit' + parentSuiteId: suiteFullNameToId['A Suite with only non-executable specs'] }); expect(reporter.specDone.calls.argsFor(5)[0]).toEqual({ - ...baseSpecEvent, + ...baseSpecDoneEvent, description: 'is xed', status: 'pending', fullName: 'A Suite with only non-executable specs is xed', diff --git a/src/core/Env.js b/src/core/Env.js index e0ed30f3..7cbc61fd 100644 --- a/src/core/Env.js +++ b/src/core/Env.js @@ -621,7 +621,7 @@ getJasmineRequireObj().Env = function(j$) { }; /** - * Get a user-defined property as part of the properties field of {@link SpecResult} + * Get a user-defined property as part of the properties field of {@link SpecDoneEvent} * @name Env#getSpecProperty * @since 5.10.0 * @function diff --git a/src/core/JsApiReporter.js b/src/core/JsApiReporter.js index 8ee3ecc5..6ab524af 100644 --- a/src/core/JsApiReporter.js +++ b/src/core/JsApiReporter.js @@ -96,7 +96,7 @@ getJasmineRequireObj().JsApiReporter = function(j$) { * @function * @param {Number} index - The position in the specs list to start from. * @param {Number} length - Maximum number of specs results to return. - * @return {SpecResult[]} + * @return {SpecDoneEvent[]} */ this.specResults = function(index, length) { return specs.slice(index, index + length); @@ -107,7 +107,7 @@ getJasmineRequireObj().JsApiReporter = function(j$) { * @name jsApiReporter#specs * @since 2.0.0 * @function - * @return {SpecResult[]} + * @return {SpecDoneEvent[]} */ this.specs = function() { return specs; diff --git a/src/core/Spec.js b/src/core/Spec.js index defa642c..05383d93 100644 --- a/src/core/Spec.js +++ b/src/core/Spec.js @@ -70,6 +70,7 @@ getJasmineRequireObj().Spec = function(j$) { return this.result.properties[key]; } + // TODO: throw if the key or value is not structred cloneable setSpecProperty(key, value) { this.result.properties = this.result.properties || {}; this.result.properties[key] = value; @@ -93,8 +94,45 @@ getJasmineRequireObj().Spec = function(j$) { } reset() { + this.result = { + id: this.id, + description: this.description, + fullName: this.getFullName(), + parentSuiteId: this.parentSuiteId, + filename: this.filename, + failedExpectations: [], + passedExpectations: [], + deprecationWarnings: [], + pendingReason: this.excludeMessage || '', + duration: null, + properties: null, + debugLogs: null + }; + this.markedPending = this.markedExcluding; + this.reportedDone = false; + } + + startedEvent() { /** - * @typedef SpecResult + * @typedef SpecStartedEvent + * @property {String} id - The unique id of this spec. + * @property {String} description - The description passed to the {@link it} that created this spec. + * @property {String} fullName - The full description including all ancestors of this spec. + * @property {String|null} parentSuiteId - The ID of the suite containing this spec, or null if this spec is not in a describe(). + * @property {String} filename - Deprecated. The name of the file the spec was defined in. + * Note: The value may be incorrect if zone.js is installed or + * `it`/`fit`/`xit` have been replaced with versions that don't maintain the + * same call stack height as the originals. This property may be removed in + * a future version unless there is enough user interest in keeping it. + * See {@link https://github.com/jasmine/jasmine/issues/2065}. + * @since 6.0.0 + */ + return this.#commonEventFields(); + } + + doneEvent() { + /** + * @typedef SpecDoneEvent * @property {String} id - The unique id of this spec. * @property {String} description - The description passed to the {@link it} that created this spec. * @property {String} fullName - The full description including all ancestors of this spec. @@ -113,24 +151,37 @@ getJasmineRequireObj().Spec = function(j$) { * @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 {DebugLogEntry[]|null} debugLogs - Messages, if any, that were logged using {@link jasmine.debugLog} during a failing spec. - * @since 2.0.0 + * @since 6.0.0 */ - this.result = { + const event = { + ...this.#commonEventFields() + }; + const toCopy = [ + 'failedExpectations', + 'passedExpectations', + 'deprecationWarnings', + 'pendingReason', + 'status', + 'duration', + 'properties', + 'debugLogs' + ]; + + for (const k of toCopy) { + event[k] = this.result[k]; + } + + return event; + } + + #commonEventFields() { + return { id: this.id, description: this.description, fullName: this.getFullName(), parentSuiteId: this.parentSuiteId, - filename: this.filename, - failedExpectations: [], - passedExpectations: [], - deprecationWarnings: [], - pendingReason: this.excludeMessage || '', - duration: null, - properties: null, - debugLogs: null + filename: this.filename }; - this.markedPending = this.markedExcluding; - this.reportedDone = false; } handleException(e) { diff --git a/src/core/TreeRunner.js b/src/core/TreeRunner.js index 8b7c5703..1412a1c6 100644 --- a/src/core/TreeRunner.js +++ b/src/core/TreeRunner.js @@ -48,7 +48,7 @@ getJasmineRequireObj().TreeRunner = function(j$) { const onStart = next => { this.#currentRunableTracker.setCurrentSpec(spec); this.#runableResources.initForRunable(spec.id, spec.parentSuiteId); - this.#reportDispatcher.specStarted(spec.result).then(next); + this.#reportDispatcher.specStarted(spec.startedEvent()).then(next); }; const resultCallback = (result, next) => { this.#specComplete(spec).then(next); @@ -255,7 +255,7 @@ getJasmineRequireObj().TreeRunner = function(j$) { async #reportSpecDone(spec) { spec.reportedDone = true; - await this.#reportDispatcher.specDone(spec.result); + await this.#reportDispatcher.specDone(spec.doneEvent()); } async #reportChildrenOfBeforeAllFailure(suite) { @@ -271,7 +271,7 @@ getJasmineRequireObj().TreeRunner = function(j$) { await this.#reportDispatcher.suiteDone(child.result); } else { /* a spec */ - await this.#reportDispatcher.specStarted(child.result); + await this.#reportDispatcher.specStarted(child.startedEvent()); child.addExpectationResult( false, diff --git a/src/core/base.js b/src/core/base.js index 95ea4bba..2b32ffb2 100644 --- a/src/core/base.js +++ b/src/core/base.js @@ -421,7 +421,7 @@ getJasmineRequireObj().base = function(j$, jasmineGlobal) { /** * Logs a message for use in debugging. If the spec fails, trace messages - * will be included in the {@link SpecResult|result} passed to the + * will be included in the {@link SpecDoneEvent|result} passed to the * reporter's specDone method. * * This method should be called only when a spec (including any associated diff --git a/src/core/reporterEvents.js b/src/core/reporterEvents.js index 88efb9b3..e073494a 100644 --- a/src/core/reporterEvents.js +++ b/src/core/reporterEvents.js @@ -72,7 +72,7 @@ getJasmineRequireObj().reporterEvents = function() { * `specStarted` is invoked when an `it` starts to run (including associated `beforeEach` functions) * @function * @name Reporter#specStarted - * @param {SpecResult} result Information about the individual {@link it} being run + * @param {SpecStartedEvent} result Information about the individual {@link it} being run * @param {Function} [done] Used to specify to Jasmine that this callback is asynchronous and Jasmine should wait until it has been called before moving on. * @returns {} Optionally return a Promise instead of using `done` to cause Jasmine to wait for completion. * @see async @@ -84,7 +84,7 @@ getJasmineRequireObj().reporterEvents = function() { * While jasmine doesn't require any specific functions, not defining a `specDone` will make it impossible for a reporter to know when a spec has failed. * @function * @name Reporter#specDone - * @param {SpecResult} result + * @param {SpecDoneEvent} result * @param {Function} [done] Used to specify to Jasmine that this callback is asynchronous and Jasmine should wait until it has been called before moving on. * @returns {} Optionally return a Promise instead of using `done` to cause Jasmine to wait for completion. * @see async