diff --git a/lib/jasmine-core/jasmine.js b/lib/jasmine-core/jasmine.js index 9a528ddd..738d5a44 100644 --- a/lib/jasmine-core/jasmine.js +++ b/lib/jasmine-core/jasmine.js @@ -774,7 +774,6 @@ getJasmineRequireObj().Spec = function(j$) { function Spec(attrs) { this.expectationFactory = attrs.expectationFactory; this.asyncExpectationFactory = attrs.asyncExpectationFactory; - this.setTimeout = attrs.setTimeout; this.id = attrs.id; this.filename = attrs.filename; this.parentSuiteId = attrs.parentSuiteId; @@ -858,87 +857,6 @@ getJasmineRequireObj().Spec = function(j$) { } }; - Spec.prototype.execute = function( - runQueue, - globalErrors, - onStart, - // TODO: may be able to merge resultCallback into onComplete - resultCallback, - onComplete, - excluded, - failSpecWithNoExp, - detectLateRejectionHandling - ) { - const start = { - fn: done => { - this.executionStarted(); - onStart(done); - } - }; - - const complete = { - fn: done => { - this.executionFinished(excluded, failSpecWithNoExp); - resultCallback(this.result, done); - }, - type: 'specCleanup' - }; - - const fns = this.beforeAndAfterFns(); - - const runnerConfig = { - isLeaf: true, - queueableFns: [...fns.befores, this.queueableFn, ...fns.afters], - onException: e => this.handleException(e), - onMultipleDone: () => { - // Issue a deprecation. Include the context ourselves and pass - // ignoreRunnable: true, since getting here always means that we've already - // moved on and the current runnable isn't the one that caused the problem. - this.onLateError( - new Error( - 'An asynchronous spec, beforeEach, or afterEach function called its ' + - "'done' callback more than once.\n(in spec: " + - this.getFullName() + - ')' - ) - ); - }, - onComplete: () => { - if (this.result.status === 'failed') { - onComplete(new j$.StopExecutionError('spec failed')); - } else { - onComplete(); - } - }, - userContext: this.userContext(), - runnableName: this.getFullName.bind(this) - }; - - if (this.markedPending || excluded === true) { - runnerConfig.queueableFns = []; - } - - runnerConfig.queueableFns.unshift(start); - - if (detectLateRejectionHandling) { - // Conditional because the setTimeout imposes a significant performance - // penalty in suites with lots of fast specs. - runnerConfig.queueableFns.push({ - fn: done => { - // setTimeout is necessary to trigger rejectionhandled events - // TODO: let clearStack know about this so it doesn't do redundant setTimeouts - this.setTimeout(function() { - globalErrors.reportUnhandledRejections(); - done(); - }); - } - }); - } - runnerConfig.queueableFns.push(complete); - - runQueue(runnerConfig); - }; - Spec.prototype.reset = function() { /** * @typedef SpecResult @@ -1027,6 +945,8 @@ getJasmineRequireObj().Spec = function(j$) { this.pend(message); }; + // TODO: ensure that all access to result goes through .getResult() + // so that the status is correct. Spec.prototype.getResult = function() { this.result.status = this.status(); return this.result; @@ -11151,7 +11071,6 @@ getJasmineRequireObj().SuiteBuilder = function(j$) { const config = this.env_.configuration(); const suite = this.currentDeclarationSuite_; const parentSuiteId = suite === this.topSuite ? null : suite.id; - const global = j$.getGlobal(); const spec = new j$.Spec({ id: 'spec' + this.nextSpecId_++, filename, @@ -11159,7 +11078,6 @@ getJasmineRequireObj().SuiteBuilder = function(j$) { beforeAndAfterFns: beforeAndAfterFns(suite), expectationFactory: this.expectationFactory_, asyncExpectationFactory: this.specAsyncExpectationFactory_, - setTimeout: global.setTimeout.bind(global), onLateError: this.onLateError_, getPath: spec => this.getSpecPath_(spec, suite), description: description, @@ -11518,6 +11436,7 @@ getJasmineRequireObj().TreeProcessor = function(j$) { getJasmineRequireObj().TreeRunner = function(j$) { class TreeRunner { #executionTree; + #setTimeout; #globalErrors; #runableResources; #reportDispatcher; @@ -11530,6 +11449,7 @@ getJasmineRequireObj().TreeRunner = function(j$) { constructor(attrs) { this.#executionTree = attrs.executionTree; this.#globalErrors = attrs.globalErrors; + this.#setTimeout = attrs.setTimeout || setTimeout.bind(globalThis); this.#runableResources = attrs.runableResources; this.#reportDispatcher = attrs.reportDispatcher; this.#runQueue = attrs.runQueue; @@ -11574,31 +11494,103 @@ getJasmineRequireObj().TreeRunner = function(j$) { if (node.suite) { this.#executeSuiteSegment(node.suite, node.segmentNumber, done); } else { - this.#executeSpec(node.spec, done); + this._executeSpec(node.spec, done); } } }; }); } - #executeSpec(spec, done) { - const config = this.#getConfig(); - spec.execute( - this.#runQueueWithSkipPolicy.bind(this), - this.#globalErrors, - next => { - this.#currentRunableTracker.setCurrentSpec(spec); - this.#runableResources.initForRunable(spec.id, spec.parentSuiteId); - this.#reportDispatcher.specStarted(spec.result).then(next); - }, - (result, next) => { - this.#specComplete(spec).then(next); - }, - done, - this.#executionTree.isExcluded(spec), - config.failSpecWithNoExpectations, - config.detectLateRejectionHandling + // Only exposed for testing. + _executeSpec(spec, specOverallDone) { + const onStart = next => { + this.#currentRunableTracker.setCurrentSpec(spec); + this.#runableResources.initForRunable(spec.id, spec.parentSuiteId); + this.#reportDispatcher.specStarted(spec.result).then(next); + }; + const resultCallback = (result, next) => { + this.#specComplete(spec).then(next); + }; + const queueableFns = this.#specQueueableFns( + spec, + onStart, + resultCallback ); + + this.#runQueueWithSkipPolicy({ + isLeaf: true, + queueableFns, + onException: e => spec.handleException(e), + onMultipleDone: () => { + // Issue an erorr. Include the context ourselves and pass + // ignoreRunnable: true, since getting here always means that we've already + // moved on and the current runnable isn't the one that caused the problem. + spec.onLateError( + new Error( + 'An asynchronous spec, beforeEach, or afterEach function called its ' + + "'done' callback more than once.\n(in spec: " + + spec.getFullName() + + ')' + ) + ); + }, + onComplete: () => { + if (spec.result.status === 'failed') { + specOverallDone(new j$.StopExecutionError('spec failed')); + } else { + specOverallDone(); + } + }, + userContext: spec.userContext(), + runnableName: spec.getFullName.bind(spec) + }); + } + + #specQueueableFns(spec, onStart, resultCallback) { + const config = this.#getConfig(); + const excluded = this.#executionTree.isExcluded(spec); + const ba = spec.beforeAndAfterFns(); + let fns = [...ba.befores, spec.queueableFn, ...ba.afters]; + + if (spec.markedPending || excluded === true) { + fns = []; + } + + const start = { + fn(done) { + spec.executionStarted(); + onStart(done); + } + }; + + const complete = { + fn(done) { + spec.executionFinished(excluded, config.failSpecWithNoExpectations); + resultCallback(spec.result, done); + }, + type: 'specCleanup' + }; + + fns.unshift(start); + + if (config.detectLateRejectionHandling) { + // Conditional because the setTimeout imposes a significant performance + // penalty in suites with lots of fast specs. + const globalErrors = this.#globalErrors; + fns.push({ + fn: done => { + // setTimeout is necessary to trigger rejectionhandled events + // TODO: let clearStack know about this so it doesn't do redundant setTimeouts + this.#setTimeout(function() { + globalErrors.reportUnhandledRejections(); + done(); + }); + } + }); + } + + fns.push(complete); + return fns; } #executeSuiteSegment(suite, segmentNumber, done) { diff --git a/spec/core/RunnerSpec.js b/spec/core/RunnerSpec.js index 33f9719e..1758ebaf 100644 --- a/spec/core/RunnerSpec.js +++ b/spec/core/RunnerSpec.js @@ -224,7 +224,7 @@ describe('Runner', function() { }); }); - describe('Integration with TreeProcessor', function() { + describe('Integration with TreeProcessor and TreeRunner', function() { let suiteNumber, specNumber, runQueue, @@ -250,6 +250,8 @@ describe('Runner', function() { // Reasonable defaults, may be overridden in some cases failSpecWithNoExpectations = false; detectLateRejectionHandling = false; + + spyOn(jasmineUnderTest.TreeRunner.prototype, '_executeSpec'); }); function StubSuite(attrs) { @@ -280,6 +282,10 @@ describe('Runner', function() { this.id = 'spec' + specNumber++; this.markedPending = attrs.markedPending || false; this.execute = jasmine.createSpy(this.id + '#execute'); + this.beforeAndAfterFns = () => ({ befores: [], afters: [] }); + this.userContext = () => ({}); + this.getFullName = () => ''; + this.queueableFn = () => {}; } function makeRunner(topSuite) { @@ -306,24 +312,41 @@ describe('Runner', function() { }); } + function arrayNotContaining(item) { + return { + asymmetricMatch(other, matchersUtil) { + if (!jasmine.isArray_(other)) { + return false; + } + + for (const x of other) { + if (matchersUtil.equals(x, item)) { + return false; + } + } + + return true; + } + }; + } + + // Precondition: jasmineUnderTest.TreeRunner.prototype._executeSpec is a spy function verifyAndFinishSpec(spec, queueableFn, shouldBeExcluded) { + const ex = jasmineUnderTest.TreeRunner.prototype._executeSpec; + ex.withArgs(spec, 'onComplete').and.callThrough(); + queueableFn.fn('onComplete'); - expect(spec.execute).toHaveBeenCalledWith( - jasmine.any(Function), - globalErrors, - jasmine.any(Function), - jasmine.any(Function), - 'onComplete', - shouldBeExcluded, - failSpecWithNoExpectations, - detectLateRejectionHandling + expect(ex).toHaveBeenCalledWith(spec, 'onComplete'); + + expect(runQueue).toHaveBeenCalledWith( + jasmine.objectContaining({ + isLeaf: true, + SkipPolicy: jasmineUnderTest.CompleteOnFirstErrorSkipPolicy, + queueableFns: shouldBeExcluded + ? arrayNotContaining(spec.queueableFn) + : jasmine.arrayContaining([spec.queueableFn]) + }) ); - spec.execute.calls.mostRecent().args[0]({ for: spec.id, isLeaf: true }); - expect(runQueue).toHaveBeenCalledWith({ - for: spec.id, - isLeaf: true, - SkipPolicy: jasmineUnderTest.CompleteOnFirstErrorSkipPolicy - }); } it('runs a single spec', async function() { @@ -467,16 +490,9 @@ describe('Runner', function() { expect(queueableFns.length).toBe(2); queueableFns[1].fn('foo'); - expect(spec.execute).toHaveBeenCalledWith( - jasmine.any(Function), - globalErrors, - jasmine.any(Function), - jasmine.any(Function), - 'foo', - false, - true, - detectLateRejectionHandling - ); + expect( + jasmineUnderTest.TreeRunner.prototype._executeSpec + ).toHaveBeenCalledWith(spec, 'foo'); await expectAsync(promise).toBePending(); }); @@ -568,14 +584,20 @@ describe('Runner', function() { await Promise.resolve(); expect(runQueue).toHaveBeenCalledTimes(1); const queueableFns = runQueue.calls.mostRecent().args[0].queueableFns; - queueableFns[0].fn(); + queueableFns[0].fn('done'); - expect(specs[0].execute).not.toHaveBeenCalled(); - expect(specs[1].execute).toHaveBeenCalled(); + expect( + jasmineUnderTest.TreeRunner.prototype._executeSpec + ).not.toHaveBeenCalledWith(specs[0], jasmine.anything()); + expect( + jasmineUnderTest.TreeRunner.prototype._executeSpec + ).toHaveBeenCalledWith(specs[1], 'done'); - queueableFns[1].fn(); + queueableFns[1].fn('done'); - expect(specs[0].execute).toHaveBeenCalled(); + expect( + jasmineUnderTest.TreeRunner.prototype._executeSpec + ).toHaveBeenCalledWith(specs[0], 'done'); await expectAsync(promise).toBePending(); }); @@ -590,32 +612,20 @@ describe('Runner', function() { await Promise.resolve(); expect(runQueue).toHaveBeenCalledTimes(1); const queueableFns = runQueue.calls.mostRecent().args[0].queueableFns; - queueableFns[0].fn(); + queueableFns[0].fn('done'); - expect(nonSpecified.execute).not.toHaveBeenCalled(); - expect(specified.execute).toHaveBeenCalledWith( - jasmine.any(Function), - globalErrors, - jasmine.any(Function), - jasmine.any(Function), - undefined, - false, - false, - detectLateRejectionHandling - ); + expect( + jasmineUnderTest.TreeRunner.prototype._executeSpec + ).not.toHaveBeenCalledWith(nonSpecified, jasmine.anything()); + expect( + jasmineUnderTest.TreeRunner.prototype._executeSpec + ).toHaveBeenCalledWith(specified, 'done'); - queueableFns[1].fn(); + queueableFns[1].fn('done'); - expect(nonSpecified.execute).toHaveBeenCalledWith( - jasmine.any(Function), - globalErrors, - jasmine.any(Function), - jasmine.any(Function), - undefined, - true, - false, - detectLateRejectionHandling - ); + expect( + jasmineUnderTest.TreeRunner.prototype._executeSpec + ).toHaveBeenCalledWith(nonSpecified, 'done'); await expectAsync(promise).toBePending(); }); @@ -637,11 +647,15 @@ describe('Runner', function() { expect(specifiedSpec.execute).not.toHaveBeenCalled(); const nodeQueueableFns = runQueue.calls.mostRecent().args[0].queueableFns; - nodeQueueableFns[1].fn(); - expect(nonSpecifiedSpec.execute).toHaveBeenCalled(); + nodeQueueableFns[1].fn('done'); + expect( + jasmineUnderTest.TreeRunner.prototype._executeSpec + ).toHaveBeenCalledWith(nonSpecifiedSpec, 'done'); - queueableFns[1].fn(); - expect(specifiedSpec.execute).toHaveBeenCalled(); + queueableFns[1].fn('done'); + expect( + jasmineUnderTest.TreeRunner.prototype._executeSpec + ).toHaveBeenCalledWith(specifiedSpec, 'done'); await expectAsync(promise).toBePending(); }); @@ -660,17 +674,23 @@ describe('Runner', function() { const queueableFns = runQueue.calls.mostRecent().args[0].queueableFns; expect(queueableFns.length).toBe(2); - queueableFns[0].fn(); - expect(spec1.execute).toHaveBeenCalled(); + queueableFns[0].fn('done'); + expect( + jasmineUnderTest.TreeRunner.prototype._executeSpec + ).toHaveBeenCalledWith(spec1, 'done'); queueableFns[1].fn(); const childFns = runQueue.calls.mostRecent().args[0].queueableFns; expect(childFns.length).toBe(3); - childFns[1].fn(); - expect(spec2.execute).toHaveBeenCalled(); + childFns[1].fn('done'); + expect( + jasmineUnderTest.TreeRunner.prototype._executeSpec + ).toHaveBeenCalledWith(spec2, 'done'); - childFns[2].fn(); - expect(spec3.execute).toHaveBeenCalled(); + childFns[2].fn('done'); + expect( + jasmineUnderTest.TreeRunner.prototype._executeSpec + ).toHaveBeenCalledWith(spec3, 'done'); await expectAsync(promise).toBePending(); }); @@ -699,26 +719,36 @@ describe('Runner', function() { queueableFns[0].fn(); expect(runQueue.calls.mostRecent().args[0].queueableFns.length).toBe(2); - runQueue.calls.mostRecent().args[0].queueableFns[1].fn(); - expect(spec1.execute).toHaveBeenCalled(); + runQueue.calls.mostRecent().args[0].queueableFns[1].fn('done'); + expect( + jasmineUnderTest.TreeRunner.prototype._executeSpec + ).toHaveBeenCalledWith(spec1, 'done'); - queueableFns[1].fn(); - expect(spec4.execute).toHaveBeenCalled(); + queueableFns[1].fn('done'); + expect( + jasmineUnderTest.TreeRunner.prototype._executeSpec + ).toHaveBeenCalledWith(spec4, 'done'); queueableFns[2].fn(); expect(runQueue.calls.count()).toBe(3); expect(runQueue.calls.mostRecent().args[0].queueableFns.length).toBe(2); - runQueue.calls.mostRecent().args[0].queueableFns[1].fn(); - expect(spec2.execute).toHaveBeenCalled(); + runQueue.calls.mostRecent().args[0].queueableFns[1].fn('done'); + expect( + jasmineUnderTest.TreeRunner.prototype._executeSpec + ).toHaveBeenCalledWith(spec2, 'done'); - queueableFns[3].fn(); - expect(spec5.execute).toHaveBeenCalled(); + queueableFns[3].fn('done'); + expect( + jasmineUnderTest.TreeRunner.prototype._executeSpec + ).toHaveBeenCalledWith(spec5, 'done'); queueableFns[4].fn(); expect(runQueue.calls.count()).toBe(4); expect(runQueue.calls.mostRecent().args[0].queueableFns.length).toBe(2); - runQueue.calls.mostRecent().args[0].queueableFns[1].fn(); - expect(spec3.execute).toHaveBeenCalled(); + runQueue.calls.mostRecent().args[0].queueableFns[1].fn('done'); + expect( + jasmineUnderTest.TreeRunner.prototype._executeSpec + ).toHaveBeenCalledWith(spec3, 'done'); await expectAsync(promise).toBePending(); }); @@ -754,11 +784,15 @@ describe('Runner', function() { runQueue.calls.mostRecent().args[0].queueableFns[1].fn(); expect(runQueue.calls.count()).toBe(3); - runQueue.calls.mostRecent().args[0].queueableFns[1].fn(); - expect(spec1.execute).toHaveBeenCalled(); + runQueue.calls.mostRecent().args[0].queueableFns[1].fn('done'); + expect( + jasmineUnderTest.TreeRunner.prototype._executeSpec + ).toHaveBeenCalledWith(spec1, 'done'); - queueableFns[1].fn(); - expect(spec4.execute).toHaveBeenCalled(); + queueableFns[1].fn('done'); + expect( + jasmineUnderTest.TreeRunner.prototype._executeSpec + ).toHaveBeenCalledWith(spec4, 'done'); queueableFns[2].fn(); expect(runQueue.calls.count()).toBe(4); @@ -767,11 +801,15 @@ describe('Runner', function() { runQueue.calls.mostRecent().args[0].queueableFns[1].fn(); expect(runQueue.calls.count()).toBe(5); - runQueue.calls.mostRecent().args[0].queueableFns[1].fn(); - expect(spec2.execute).toHaveBeenCalled(); + runQueue.calls.mostRecent().args[0].queueableFns[1].fn('done'); + expect( + jasmineUnderTest.TreeRunner.prototype._executeSpec + ).toHaveBeenCalledWith(spec2, 'done'); - queueableFns[3].fn(); - expect(spec5.execute).toHaveBeenCalled(); + queueableFns[3].fn('done'); + expect( + jasmineUnderTest.TreeRunner.prototype._executeSpec + ).toHaveBeenCalledWith(spec5, 'done'); queueableFns[4].fn(); expect(runQueue.calls.count()).toBe(6); @@ -780,8 +818,10 @@ describe('Runner', function() { runQueue.calls.mostRecent().args[0].queueableFns[1].fn(); expect(runQueue.calls.count()).toBe(7); - runQueue.calls.mostRecent().args[0].queueableFns[1].fn(); - expect(spec3.execute).toHaveBeenCalled(); + runQueue.calls.mostRecent().args[0].queueableFns[1].fn('done'); + expect( + jasmineUnderTest.TreeRunner.prototype._executeSpec + ).toHaveBeenCalledWith(spec3, 'done'); await expectAsync(promise).toBePending(); }); @@ -803,8 +843,10 @@ describe('Runner', function() { expect(queueableFns.length).toBe(11); for (let i = 0; i < 11; i++) { - queueableFns[i].fn(); - expect(specs[i].execute).toHaveBeenCalled(); + queueableFns[i].fn('done'); + expect( + jasmineUnderTest.TreeRunner.prototype._executeSpec + ).toHaveBeenCalledWith(specs[i], 'done'); } await expectAsync(promise).toBePending(); diff --git a/spec/core/SpecSpec.js b/spec/core/SpecSpec.js index c18b6993..d5a15487 100644 --- a/spec/core/SpecSpec.js +++ b/spec/core/SpecSpec.js @@ -33,170 +33,6 @@ describe('Spec', function() { expect(jasmineUnderTest.Spec.isPendingSpecException(void 0)).toBe(false); }); - it('delegates execution to a QueueRunner', function() { - const fakeQueueRunner = jasmine.createSpy('fakeQueueRunner'), - spec = new jasmineUnderTest.Spec({ - description: 'my test', - id: 'some-id', - queueableFn: { fn: function() {} } - }); - - spec.execute(fakeQueueRunner); - - expect(fakeQueueRunner).toHaveBeenCalled(); - }); - - it('should call the start callback on execution', function() { - const fakeQueueRunner = jasmine.createSpy('fakeQueueRunner'), - startCallback = jasmine.createSpy('startCallback'), - spec = new jasmineUnderTest.Spec({ - id: 123, - description: 'foo bar', - queueableFn: { fn: function() {} } - }); - - spec.execute(fakeQueueRunner, null, startCallback); - - fakeQueueRunner.calls.mostRecent().args[0].queueableFns[0].fn(); - expect(startCallback).toHaveBeenCalled(); - }); - - it('should call the start callback on execution but before any befores are called', function() { - const fakeQueueRunner = jasmine.createSpy('fakeQueueRunner'); - let beforesWereCalled = false; - const startCallback = jasmine - .createSpy('start-callback') - .and.callFake(function() { - expect(beforesWereCalled).toBe(false); - }); - const spec = new jasmineUnderTest.Spec({ - queueableFn: { fn: function() {} }, - beforeFns: function() { - return [ - function() { - beforesWereCalled = true; - } - ]; - } - }); - - spec.execute(fakeQueueRunner, null, startCallback); - - fakeQueueRunner.calls.mostRecent().args[0].queueableFns[0].fn(); - expect(startCallback).toHaveBeenCalled(); - }); - - it('provides all before fns and after fns to be run', function() { - const fakeQueueRunner = jasmine.createSpy('fakeQueueRunner'), - before = jasmine.createSpy('before'), - after = jasmine.createSpy('after'), - queueableFn = { - fn: jasmine.createSpy('test body').and.callFake(function() { - expect(before).toHaveBeenCalled(); - expect(after).not.toHaveBeenCalled(); - }) - }, - spec = new jasmineUnderTest.Spec({ - queueableFn: queueableFn, - beforeAndAfterFns: function() { - return { befores: [before], afters: [after] }; - } - }); - - spec.execute(fakeQueueRunner, null, null); - - const options = fakeQueueRunner.calls.mostRecent().args[0]; - expect(options.queueableFns).toEqual([ - { fn: jasmine.any(Function) }, - before, - queueableFn, - after, - { - fn: jasmine.any(Function), - type: 'specCleanup' - } - ]); - }); - - describe('Late promise rejection handling', function() { - it('is enabled when the detectLateRejectionHandling param is true', function() { - const fakeQueueRunner = jasmine.createSpy('fakeQueueRunner'); - const globalErrors = jasmine.createSpyObj('globalErrors', [ - 'reportUnhandledRejections' - ]); - const setTimeout = jasmine.createSpy('setTimeout'); - const before = jasmine.createSpy('before'); - const after = jasmine.createSpy('after'); - const queueableFn = { - fn: jasmine.createSpy('test body').and.callFake(function() { - expect(before).toHaveBeenCalled(); - expect(after).not.toHaveBeenCalled(); - }) - }; - const spec = new jasmineUnderTest.Spec({ - queueableFn, - setTimeout, - beforeAndAfterFns: function() { - return { befores: [before], afters: [after] }; - } - }); - - spec.execute( - fakeQueueRunner, - globalErrors, - null, - null, - null, - false, - false, - true - ); - - const options = fakeQueueRunner.calls.mostRecent().args[0]; - expect(options.queueableFns).toEqual([ - { fn: jasmine.any(Function) }, - before, - queueableFn, - after, - { fn: jasmine.any(Function) }, - { - fn: jasmine.any(Function), - type: 'specCleanup' - } - ]); - - const done = jasmine.createSpy('done'); - options.queueableFns[4].fn(done); - expect(globalErrors.reportUnhandledRejections).not.toHaveBeenCalled(); - expect(done).not.toHaveBeenCalled(); - - expect(setTimeout).toHaveBeenCalledOnceWith(jasmine.any(Function)); - setTimeout.calls.argsFor(0)[0](); - expect(globalErrors.reportUnhandledRejections).toHaveBeenCalled(); - expect(globalErrors.reportUnhandledRejections).toHaveBeenCalledBefore( - done - ); - }); - }); - - it("tells the queue runner that it's a leaf node", function() { - const fakeQueueRunner = jasmine.createSpy('fakeQueueRunner'), - spec = new jasmineUnderTest.Spec({ - queueableFn: { fn: function() {} }, - beforeAndAfterFns: function() { - return { befores: [], afters: [] }; - } - }); - - spec.execute(fakeQueueRunner); - - expect(fakeQueueRunner).toHaveBeenCalledWith( - jasmine.objectContaining({ - isLeaf: true - }) - ); - }); - it('is marked pending if created without a function body', function() { const startCallback = jasmine.createSpy('startCallback'), resultCallback = jasmine.createSpy('resultCallback'), @@ -209,234 +45,49 @@ describe('Spec', function() { expect(spec.status()).toBe('pending'); }); - it('can be excluded at execution time by a parent', function() { - const fakeQueueRunner = jasmine.createSpy('fakeQueueRunner'), - startCallback = jasmine.createSpy('startCallback'), - specBody = jasmine.createSpy('specBody'), - resultCallback = jasmine.createSpy('resultCallback'), - spec = new jasmineUnderTest.Spec({ - queueableFn: { fn: specBody } + describe('#executionFinished', function() { + it('removes the fn if autoCleanClosures is true', function() { + const spec = new jasmineUnderTest.Spec({ + queueableFn: { fn: () => {} }, + autoCleanClosures: true }); - spec.execute( - fakeQueueRunner, - null, - startCallback, - resultCallback, - 'onComplete', - true - ); - - expect(fakeQueueRunner).toHaveBeenCalledWith( - jasmine.objectContaining({ - onComplete: jasmine.any(Function), - queueableFns: [ - { fn: jasmine.any(Function) }, - { - fn: jasmine.any(Function), - type: 'specCleanup' - } - ] - }) - ); - expect(specBody).not.toHaveBeenCalled(); - - const args = fakeQueueRunner.calls.mostRecent().args[0]; - args.queueableFns[0].fn(); - expect(startCallback).toHaveBeenCalled(); - args.queueableFns[args.queueableFns.length - 1].fn(); - expect(resultCallback).toHaveBeenCalled(); - - expect(spec.result.status).toBe('excluded'); - }); - - it('can be marked pending, but still calls callbacks when executed', function() { - const fakeQueueRunner = jasmine.createSpy('fakeQueueRunner'), - startCallback = jasmine.createSpy('startCallback'), - resultCallback = jasmine.createSpy('resultCallback'), - spec = new jasmineUnderTest.Spec({ - description: 'with a spec', - parentSuiteId: 'suite1', - filename: 'someSpecFile.js', - getPath: function() { - return ['a suite', 'with a spec']; - }, - queueableFn: { fn: null } - }); - - spec.pend(); - - expect(spec.status()).toBe('pending'); - - spec.execute(fakeQueueRunner, null, startCallback, resultCallback); - - expect(fakeQueueRunner).toHaveBeenCalled(); - - const args = fakeQueueRunner.calls.mostRecent().args[0]; - args.queueableFns[0].fn(); - expect(startCallback).toHaveBeenCalled(); - args.queueableFns[1].fn('things'); - expect(resultCallback).toHaveBeenCalledWith( - { - id: spec.id, - status: 'pending', - description: 'with a spec', - fullName: 'a suite with a spec', - parentSuiteId: 'suite1', - filename: 'someSpecFile.js', - failedExpectations: [], - passedExpectations: [], - deprecationWarnings: [], - pendingReason: '', - duration: jasmine.any(Number), - properties: null, - debugLogs: null - }, - 'things' - ); - }); - - it('should call the done callback on execution complete', function() { - const done = jasmine.createSpy('done callback'), - spec = new jasmineUnderTest.Spec({ - queueableFn: { fn: function() {} }, - catchExceptions: function() { - return false; - } - }); - - spec.execute( - attrs => attrs.onComplete(), - null, - function() {}, - function() {}, - done - ); - - expect(done).toHaveBeenCalled(); - }); - - it('should call the done callback with an error if the spec is failed', function() { - const done = jasmine.createSpy('done callback'), - spec = new jasmineUnderTest.Spec({ - queueableFn: { fn: function() {} }, - catchExceptions: function() { - return false; - } - }); - - function runQueue(attrs) { - spec.result.status = 'failed'; - attrs.onComplete(); - } - spec.execute(runQueue, null, function() {}, function() {}, done); - - expect(done).toHaveBeenCalledWith( - jasmine.any(jasmineUnderTest.StopExecutionError) - ); - }); - - it('should report the duration of the test', function() { - const timer = jasmine.createSpyObj('timer', { - start: null, - elapsed: 77000 - }); - const resultCallback = jasmine.createSpy('resultCallback'); - const spec = new jasmineUnderTest.Spec({ - queueableFn: { fn: jasmine.createSpy('spec body') }, - catchExceptions: function() { - return false; - }, - timer: timer + spec.executionFinished(); + expect(spec.queueableFn.fn).toBeFalsy(); }); - function runQueue(config) { - config.queueableFns.forEach(function(qf) { - qf.fn(); + it('removes the fn after execution if autoCleanClosures is undefined', function() { + const spec = new jasmineUnderTest.Spec({ + queueableFn: { fn: () => {} }, + autoCleanClosures: undefined }); - config.onComplete(); - } - spec.execute(runQueue, null, function() {}, resultCallback, function() {}); - expect(resultCallback).toHaveBeenCalled(); - expect(resultCallback.calls.argsFor(0)[0].duration).toEqual(77000); - }); - - it('removes the fn after execution if autoCleanClosures is true', function() { - const done = jasmine.createSpy('done callback'); - const spec = new jasmineUnderTest.Spec({ - queueableFn: { fn() {} }, - autoCleanClosures: true + spec.executionFinished(); + expect(spec.queueableFn.fn).toBeFalsy(); }); - function runQueue(config) { - config.queueableFns.forEach(function(qf) { - qf.fn(); + it('does not remove the fn after execution if autoCleanClosures is false', function() { + function originalFn() {} + const spec = new jasmineUnderTest.Spec({ + queueableFn: { fn: originalFn }, + autoCleanClosures: false }); - config.onComplete(); - } - spec.execute(runQueue, null, function() {}, function() {}, done); - expect(done).toHaveBeenCalled(); - expect(spec.queueableFn.fn).toBeFalsy(); - }); - - it('removes the fn after execution if autoCleanClosures is undefined', function() { - const done = jasmine.createSpy('done callback'); - const spec = new jasmineUnderTest.Spec({ - queueableFn: { fn() {} }, - autoCleanClosures: undefined + spec.executionFinished(); + expect(spec.queueableFn.fn).toBe(originalFn); }); - - function runQueue(config) { - config.queueableFns.forEach(function(qf) { - qf.fn(); - }); - config.onComplete(); - } - - spec.execute(runQueue, null, function() {}, function() {}, done); - expect(done).toHaveBeenCalled(); - expect(spec.queueableFn.fn).toBeFalsy(); }); - it('does not remove the fn after execution if autoCleanClosures is false', function() { - const done = jasmine.createSpy('done callback'); - function originalFn() {} - const spec = new jasmineUnderTest.Spec({ - queueableFn: { fn: originalFn }, - autoCleanClosures: false + describe('#setSpecProperty', function() { + it('adds the property to the result', function() { + const spec = new jasmineUnderTest.Spec({ + queueableFn: { fn: () => {} } + }); + + spec.setSpecProperty('a', 4); + + expect(spec.result.properties).toEqual({ a: 4 }); }); - - function runQueue(config) { - config.queueableFns.forEach(function(qf) { - qf.fn(); - }); - config.onComplete(); - } - - spec.execute(runQueue, null, function() {}, function() {}, done); - expect(done).toHaveBeenCalled(); - expect(spec.queueableFn.fn).toBe(originalFn); - }); - - it('should report properties set during the test', function() { - const done = jasmine.createSpy('done callback'), - spec = new jasmineUnderTest.Spec({ - queueableFn: { fn: jasmine.createSpy('spec body') }, - catchExceptions: function() { - return false; - } - }); - spec.setSpecProperty('a', 4); - spec.execute( - attrs => attrs.onComplete(), - null, - function() {}, - function() {}, - done - ); - expect(spec.result.properties).toEqual({ a: 4 }); }); it('#status returns passing by default', function() { @@ -446,69 +97,84 @@ describe('Spec', function() { expect(spec.status()).toBe('passed'); }); - it('#status returns passed if all expectations in the spec have passed', function() { - const spec = new jasmineUnderTest.Spec({ - queueableFn: { fn: jasmine.createSpy('spec body') } - }); - spec.addExpectationResult(true, {}); - expect(spec.status()).toBe('passed'); - }); - - it('#status returns failed if any expectations in the spec have failed', function() { - const spec = new jasmineUnderTest.Spec({ - queueableFn: { fn: jasmine.createSpy('spec body') } - }); - spec.addExpectationResult(true, {}); - spec.addExpectationResult(false, {}); - expect(spec.status()).toBe('failed'); - }); - - it('keeps track of passed and failed expectations', function() { - const fakeQueueRunner = jasmine.createSpy('queueRunner'), - resultCallback = jasmine.createSpy('resultCallback'), - spec = new jasmineUnderTest.Spec({ - queueableFn: { fn: jasmine.createSpy('spec body') } + describe('#status', function() { + it('returns "passed"" by default', function() { + const spec = new jasmineUnderTest.Spec({ + queueableFn: { fn: () => {} } }); - spec.addExpectationResult(true, { message: 'expectation1' }); - spec.addExpectationResult(false, { message: 'expectation2' }); + expect(spec.status()).toBe('passed'); + }); - spec.execute(fakeQueueRunner, null, function() {}, resultCallback); - - const fns = fakeQueueRunner.calls.mostRecent().args[0].queueableFns; - fns[fns.length - 1].fn(); - - expect(resultCallback.calls.first().args[0].passedExpectations).toEqual([ - jasmine.objectContaining({ message: 'expectation1' }) - ]); - expect(resultCallback.calls.first().args[0].failedExpectations).toEqual([ - jasmine.objectContaining({ message: 'expectation2' }) - ]); - }); - - it("throws an ExpectationFailed error upon receiving a failed expectation when 'throwOnExpectationFailure' is set", function() { - const fakeQueueRunner = jasmine.createSpy('queueRunner'), - resultCallback = jasmine.createSpy('resultCallback'), - spec = new jasmineUnderTest.Spec({ - queueableFn: { fn: function() {} }, - resultCallback: resultCallback, - throwOnExpectationFailure: true + it('returns "passed"" if all expectations passed', function() { + const spec = new jasmineUnderTest.Spec({ + queueableFn: { fn: () => {} } }); - spec.addExpectationResult(true, { message: 'passed' }); - expect(function() { - spec.addExpectationResult(false, { message: 'failed' }); - }).toThrowError(jasmineUnderTest.errors.ExpectationFailed); + spec.addExpectationResult(true, {}); - spec.execute(fakeQueueRunner, null, function() {}, resultCallback); + expect(spec.status()).toBe('passed'); + }); - const fns = fakeQueueRunner.calls.mostRecent().args[0].queueableFns; - fns[fns.length - 1].fn(); - expect(resultCallback.calls.first().args[0].passedExpectations).toEqual([ - jasmine.objectContaining({ message: 'passed' }) - ]); - expect(resultCallback.calls.first().args[0].failedExpectations).toEqual([ - jasmine.objectContaining({ message: 'failed' }) - ]); + it('returns "failed" if any expectation failed', function() { + const spec = new jasmineUnderTest.Spec({ + queueableFn: { fn: () => {} } + }); + + spec.addExpectationResult(true, {}); + spec.addExpectationResult(false, {}); + + expect(spec.status()).toBe('failed'); + }); + }); + + describe('#addExpectationResult', function() { + it('keeps track of passed and failed expectations', function() { + const spec = new jasmineUnderTest.Spec({ + queueableFn: { fn: () => {} } + }); + + spec.addExpectationResult(true, { message: 'expectation1' }); + spec.addExpectationResult(false, { message: 'expectation2' }); + + expect(spec.result.passedExpectations).toEqual([ + jasmine.objectContaining({ message: 'expectation1' }) + ]); + expect(spec.result.failedExpectations).toEqual([ + jasmine.objectContaining({ message: 'expectation2' }) + ]); + }); + + describe("when 'throwOnExpectationFailure' is set", function() { + it('throws an ExpectationFailed error', function() { + const spec = new jasmineUnderTest.Spec({ + queueableFn: { fn: () => {} }, + throwOnExpectationFailure: true + }); + + spec.addExpectationResult(true, { message: 'passed' }); + expect(function() { + spec.addExpectationResult(false, { message: 'failed' }); + }).toThrowError(jasmineUnderTest.errors.ExpectationFailed); + + expect(spec.result.failedExpectations).toEqual([ + jasmine.objectContaining({ message: 'failed' }) + ]); + }); + }); + + describe("when 'throwOnExpectationFailure' is not set", function() { + it('does not throw', function() { + const spec = new jasmineUnderTest.Spec({ + queueableFn: { fn: () => {} } + }); + + spec.addExpectationResult(false, { message: 'failed' }); + + expect(spec.result.failedExpectations).toEqual([ + jasmine.objectContaining({ message: 'failed' }) + ]); + }); + }); }); it('forwards late expectation failures to onLateError', function() { @@ -639,123 +305,47 @@ describe('Spec', function() { expect(spec.metadata.getPath()).toEqual(['expected val']); }); - describe('when a spec is marked pending during execution', function() { - it('should mark the spec as pending', function() { - const fakeQueueRunner = function(opts) { - opts.onException( - new Error(jasmineUnderTest.Spec.pendingSpecExceptionMessage) - ); - }, - spec = new jasmineUnderTest.Spec({ - description: 'my test', - id: 'some-id', - queueableFn: { fn: function() {} } - }); - - spec.execute(fakeQueueRunner); - - expect(spec.status()).toEqual('pending'); - expect(spec.result.pendingReason).toEqual(''); - }); - - it('should set the pendingReason', function() { - const fakeQueueRunner = function(opts) { - opts.onException( - new Error( - jasmineUnderTest.Spec.pendingSpecExceptionMessage + - 'custom message' - ) - ); - }, - spec = new jasmineUnderTest.Spec({ - description: 'my test', - id: 'some-id', - queueableFn: { fn: function() {} } - }); - - spec.execute(fakeQueueRunner); - - expect(spec.status()).toEqual('pending'); - expect(spec.result.pendingReason).toEqual('custom message'); - }); - }); - - it('should log a failure when handling an exception', function() { - const fakeQueueRunner = jasmine.createSpy('queueRunner'), - resultCallback = jasmine.createSpy('resultCallback'), - spec = new jasmineUnderTest.Spec({ - queueableFn: { fn: function() {} } + describe('#handleException', function() { + it('records a failure', function() { + const spec = new jasmineUnderTest.Spec({ + queueableFn: {} }); - spec.handleException('foo'); - spec.execute(fakeQueueRunner, null, function() {}, resultCallback); + spec.handleException('foo'); - const args = fakeQueueRunner.calls.mostRecent().args[0]; - args.queueableFns[args.queueableFns.length - 1].fn(); - expect(resultCallback.calls.first().args[0].failedExpectations).toEqual([ - { - message: 'foo thrown', - matcherName: '', - passed: false, - expected: '', - actual: '', - stack: null - } - ]); - }); - - it('should not log an additional failure when handling an ExpectationFailed error', function() { - const fakeQueueRunner = jasmine.createSpy('queueRunner'), - resultCallback = jasmine.createSpy('resultCallback'), - spec = new jasmineUnderTest.Spec({ - queueableFn: { fn: function() {} } - }); - - spec.handleException(new jasmineUnderTest.errors.ExpectationFailed()); - spec.execute(fakeQueueRunner, null, function() {}, resultCallback); - - const args = fakeQueueRunner.calls.mostRecent().args[0]; - args.queueableFns[args.queueableFns.length - 1].fn(); - expect(resultCallback.calls.first().args[0].failedExpectations).toEqual([]); - }); - - it('treats multiple done calls as late errors', function() { - const runQueue = jasmine.createSpy('runQueue'), - onLateError = jasmine.createSpy('onLateError'), - spec = new jasmineUnderTest.Spec({ - onLateError: onLateError, - queueableFn: { fn: function() {} }, - getPath: function() { - return ['a spec']; + expect(spec.result.failedExpectations).toEqual([ + { + message: 'foo thrown', + matcherName: '', + passed: false, + expected: '', + actual: '', + stack: null } + ]); + }); + + it('does not record an additional failure when the error is ExpectationFailed', function() { + const spec = new jasmineUnderTest.Spec({ + queueableFn: {} }); - spec.execute(runQueue); + spec.handleException(new jasmineUnderTest.errors.ExpectationFailed()); - expect(runQueue).toHaveBeenCalled(); - runQueue.calls.argsFor(0)[0].onMultipleDone(); - - expect(onLateError).toHaveBeenCalledTimes(1); - expect(onLateError.calls.argsFor(0)[0]).toBeInstanceOf(Error); - expect(onLateError.calls.argsFor(0)[0].message).toEqual( - 'An asynchronous spec, beforeEach, or afterEach function called its ' + - "'done' callback more than once.\n(in spec: a spec)" - ); + expect(spec.result.failedExpectations).toEqual([]); + }); }); - describe('#trace', function() { + describe('#debugLog', function() { it('adds the messages to the result', function() { - const timer = jasmine.createSpyObj('timer', ['start', 'elapsed']), - spec = new jasmineUnderTest.Spec({ - queueableFn: { - fn: function() {} - }, - timer: timer - }), - t1 = 123, - t2 = 456; + const timer = jasmine.createSpyObj('timer', ['start', 'elapsed']); + const spec = new jasmineUnderTest.Spec({ + queueableFn: { fn: () => {} }, + timer: timer + }); + const t1 = 123; + const t2 = 456; - spec.execute(() => {}); expect(spec.result.debugLogs).toBeNull(); timer.elapsed.and.returnValue(t1); spec.debugLog('msg 1'); @@ -771,99 +361,36 @@ describe('Spec', function() { }); describe('When the spec passes', function() { - it('omits the messages from the reported result', function() { - const resultCallback = jasmine.createSpy('resultCallback'), - spec = new jasmineUnderTest.Spec({ - queueableFn: { - fn: function() {} - } - }); + it('removes the logs from the result', function() { + const spec = new jasmineUnderTest.Spec({ + queueableFn: { fn: () => {} } + }); - function runQueue(config) { - spec.debugLog('msg'); - for (const fn of config.queueableFns) { - fn.fn(); - } - config.onComplete(false); - } + spec.debugLog('msg'); + spec.executionFinished(); - spec.execute( - runQueue, - null, - function() {}, - resultCallback, - function() {} - ); - expect(resultCallback).toHaveBeenCalledWith( - jasmine.objectContaining({ debugLogs: null }), - undefined - ); - }); - - it('removes the messages to save memory', function() { - const resultCallback = jasmine.createSpy('resultCallback'), - spec = new jasmineUnderTest.Spec({ - queueableFn: { - fn: function() {} - } - }); - - function runQueue(config) { - spec.debugLog('msg'); - for (const fn of config.queueableFns) { - fn.fn(); - } - config.onComplete(false); - } - - spec.execute( - runQueue, - null, - function() {}, - resultCallback, - function() {} - ); - expect(resultCallback).toHaveBeenCalled(); expect(spec.result.debugLogs).toBeNull(); }); }); describe('When the spec fails', function() { - it('includes the messages in the reported result', function() { - const resultCallback = jasmine.createSpy('resultCallback'), - timer = jasmine.createSpyObj('timer', ['start', 'elapsed']), - spec = new jasmineUnderTest.Spec({ - queueableFn: { - fn: function() {} - }, - timer: timer - }), - timestamp = 12345; + it('includes the messages in the result', function() { + const timer = jasmine.createSpyObj('timer', ['start', 'elapsed']); + const spec = new jasmineUnderTest.Spec({ + queueableFn: { fn: () => {} }, + timer: timer + }); + const timestamp = 12345; timer.elapsed.and.returnValue(timestamp); - function runQueue(config) { - spec.debugLog('msg'); - spec.handleException(new Error('nope')); - for (const fn of config.queueableFns) { - fn.fn(); - } - config.onComplete(true); - } + spec.debugLog('msg'); + spec.handleException(new Error('nope')); + spec.executionFinished(); - spec.execute( - runQueue, - null, - function() {}, - resultCallback, - function() {} - ); - expect(resultCallback).toHaveBeenCalledWith( - jasmine.objectContaining({ - debugLogs: [{ message: 'msg', timestamp: timestamp }] - }), - undefined - ); + expect(spec.result.debugLogs).toEqual([ + { message: 'msg', timestamp: timestamp } + ]); }); }); }); diff --git a/spec/core/TreeRunnerSpec.js b/spec/core/TreeRunnerSpec.js index d9184845..c7b1f1b3 100644 --- a/spec/core/TreeRunnerSpec.js +++ b/spec/core/TreeRunnerSpec.js @@ -71,7 +71,131 @@ describe('TreeRunner', function() { await expectAsync(executePromise).toBePending(); }); - function runSingleSpecSuite(spec) { + it('runs before and after fns', function() { + const before = { fn: jasmine.createSpy('before') }; + const after = { fn: jasmine.createSpy('after') }; + const queueableFn = { + fn: jasmine.createSpy('test body').and.callFake(function() { + expect(before).toHaveBeenCalled(); + expect(after).not.toHaveBeenCalled(); + }) + }; + const spec = new jasmineUnderTest.Spec({ + queueableFn: queueableFn, + beforeAndAfterFns: function() { + return { befores: [before], afters: [after] }; + } + }); + + const { runQueue, suiteRunQueueArgs } = runSingleSpecSuite(spec); + suiteRunQueueArgs.queueableFns[0].fn(); + expect(runQueue).toHaveBeenCalledTimes(1); + const specRunQueueArgs = runQueue.calls.mostRecent().args[0]; + + expect(specRunQueueArgs.queueableFns[1]).toEqual(before); + expect(specRunQueueArgs.queueableFns[2]).toEqual(queueableFn); + expect(specRunQueueArgs.queueableFns[3]).toEqual(after); + }); + + it('marks specs pending at runtime', function() { + let spec; + const queueableFn = { + fn() { + spec.pend(); + } + }; + spec = new jasmineUnderTest.Spec({ queueableFn }); + + const { runQueue, suiteRunQueueArgs } = runSingleSpecSuite(spec); + suiteRunQueueArgs.queueableFns[0].fn(); + expect(runQueue).toHaveBeenCalledTimes(1); + const specRunQueueArgs = runQueue.calls.mostRecent().args[0]; + + expect(specRunQueueArgs.queueableFns[1]).toEqual(queueableFn); + queueableFn.fn(); + + expect(spec.status()).toEqual('pending'); + expect(spec.getResult().status).toEqual('pending'); + expect(spec.getResult().pendingReason).toEqual(''); + }); + + it('marks specs pending at runtime with a message', function() { + let spec; + const queueableFn = { + fn() { + spec.pend('some reason'); + } + }; + spec = new jasmineUnderTest.Spec({ queueableFn }); + + const { runQueue, suiteRunQueueArgs } = runSingleSpecSuite(spec); + suiteRunQueueArgs.queueableFns[0].fn(); + expect(runQueue).toHaveBeenCalledTimes(1); + const specRunQueueArgs = runQueue.calls.mostRecent().args[0]; + + expect(specRunQueueArgs.queueableFns[1]).toEqual(queueableFn); + queueableFn.fn(); + + expect(spec.status()).toEqual('pending'); + expect(spec.getResult().status).toEqual('pending'); + expect(spec.getResult().pendingReason).toEqual('some reason'); + }); + + describe('Late promise rejection handling', function() { + it('is enabled when the detectLateRejectionHandling param is true', function() { + const before = jasmine.createSpy('before'); + const after = jasmine.createSpy('after'); + const queueableFn = { + fn: jasmine.createSpy('test body').and.callFake(function() { + expect(before).toHaveBeenCalled(); + expect(after).not.toHaveBeenCalled(); + }) + }; + const spec = new jasmineUnderTest.Spec({ + queueableFn, + beforeAndAfterFns: function() { + return { befores: [before], afters: [after] }; + } + }); + + const { + runQueue, + setTimeout, + suiteRunQueueArgs, + globalErrors + } = runSingleSpecSuite(spec, { detectLateRejectionHandling: true }); + + suiteRunQueueArgs.queueableFns[0].fn(); + expect(runQueue).toHaveBeenCalledTimes(1); + const specRunQueueOpts = runQueue.calls.mostRecent().args[0]; + + expect(specRunQueueOpts.queueableFns).toEqual([ + { fn: jasmine.any(Function) }, + before, + queueableFn, + after, + { fn: jasmine.any(Function) }, + { + fn: jasmine.any(Function), + type: 'specCleanup' + } + ]); + + const done = jasmine.createSpy('done'); + specRunQueueOpts.queueableFns[4].fn(done); + expect(globalErrors.reportUnhandledRejections).not.toHaveBeenCalled(); + expect(done).not.toHaveBeenCalled(); + + expect(setTimeout).toHaveBeenCalledOnceWith(jasmine.any(Function)); + setTimeout.calls.argsFor(0)[0](); + expect(globalErrors.reportUnhandledRejections).toHaveBeenCalled(); + expect(globalErrors.reportUnhandledRejections).toHaveBeenCalledBefore( + done + ); + }); + }); + + function runSingleSpecSuite(spec, optionalConfig) { const topSuiteId = 'suite1'; spec.parentSuiteId = topSuiteId; const topSuite = new jasmineUnderTest.Suite({ id: topSuiteId }); @@ -88,15 +212,19 @@ describe('TreeRunner', function() { const runQueue = jasmine.createSpy('runQueue'); const reportDispatcher = mockReportDispatcher(); const runableResources = mockRunableResources(); + const globalErrors = mockGlobalErrors(); + const setTimeout = jasmine.createSpy('setTimeout'); const currentRunableTracker = new jasmineUnderTest.CurrentRunableTracker(); const subject = new jasmineUnderTest.TreeRunner({ executionTree, runQueue, + globalErrors, + setTimeout, runableResources, reportDispatcher, currentRunableTracker, getConfig() { - return {}; + return optionalConfig || {}; }, reportChildrenOfBeforeAllFailure() {} }); @@ -108,6 +236,8 @@ describe('TreeRunner', function() { return { runQueue, + globalErrors, + setTimeout, currentRunableTracker, runableResources, reportDispatcher, @@ -136,4 +266,8 @@ describe('TreeRunner', function() { 'clearForRunable' ]); } + + function mockGlobalErrors() { + return jasmine.createSpyObj('globalErrors', ['reportUnhandledRejections']); + } }); diff --git a/src/core/Spec.js b/src/core/Spec.js index 223b561f..3af546de 100644 --- a/src/core/Spec.js +++ b/src/core/Spec.js @@ -2,7 +2,6 @@ getJasmineRequireObj().Spec = function(j$) { function Spec(attrs) { this.expectationFactory = attrs.expectationFactory; this.asyncExpectationFactory = attrs.asyncExpectationFactory; - this.setTimeout = attrs.setTimeout; this.id = attrs.id; this.filename = attrs.filename; this.parentSuiteId = attrs.parentSuiteId; @@ -86,87 +85,6 @@ getJasmineRequireObj().Spec = function(j$) { } }; - Spec.prototype.execute = function( - runQueue, - globalErrors, - onStart, - // TODO: may be able to merge resultCallback into onComplete - resultCallback, - onComplete, - excluded, - failSpecWithNoExp, - detectLateRejectionHandling - ) { - const start = { - fn: done => { - this.executionStarted(); - onStart(done); - } - }; - - const complete = { - fn: done => { - this.executionFinished(excluded, failSpecWithNoExp); - resultCallback(this.result, done); - }, - type: 'specCleanup' - }; - - const fns = this.beforeAndAfterFns(); - - const runnerConfig = { - isLeaf: true, - queueableFns: [...fns.befores, this.queueableFn, ...fns.afters], - onException: e => this.handleException(e), - onMultipleDone: () => { - // Issue a deprecation. Include the context ourselves and pass - // ignoreRunnable: true, since getting here always means that we've already - // moved on and the current runnable isn't the one that caused the problem. - this.onLateError( - new Error( - 'An asynchronous spec, beforeEach, or afterEach function called its ' + - "'done' callback more than once.\n(in spec: " + - this.getFullName() + - ')' - ) - ); - }, - onComplete: () => { - if (this.result.status === 'failed') { - onComplete(new j$.StopExecutionError('spec failed')); - } else { - onComplete(); - } - }, - userContext: this.userContext(), - runnableName: this.getFullName.bind(this) - }; - - if (this.markedPending || excluded === true) { - runnerConfig.queueableFns = []; - } - - runnerConfig.queueableFns.unshift(start); - - if (detectLateRejectionHandling) { - // Conditional because the setTimeout imposes a significant performance - // penalty in suites with lots of fast specs. - runnerConfig.queueableFns.push({ - fn: done => { - // setTimeout is necessary to trigger rejectionhandled events - // TODO: let clearStack know about this so it doesn't do redundant setTimeouts - this.setTimeout(function() { - globalErrors.reportUnhandledRejections(); - done(); - }); - } - }); - } - runnerConfig.queueableFns.push(complete); - - runQueue(runnerConfig); - }; - Spec.prototype.reset = function() { /** * @typedef SpecResult @@ -255,6 +173,8 @@ getJasmineRequireObj().Spec = function(j$) { this.pend(message); }; + // TODO: ensure that all access to result goes through .getResult() + // so that the status is correct. Spec.prototype.getResult = function() { this.result.status = this.status(); return this.result; diff --git a/src/core/SuiteBuilder.js b/src/core/SuiteBuilder.js index affa9693..76b45b3d 100644 --- a/src/core/SuiteBuilder.js +++ b/src/core/SuiteBuilder.js @@ -237,7 +237,6 @@ getJasmineRequireObj().SuiteBuilder = function(j$) { const config = this.env_.configuration(); const suite = this.currentDeclarationSuite_; const parentSuiteId = suite === this.topSuite ? null : suite.id; - const global = j$.getGlobal(); const spec = new j$.Spec({ id: 'spec' + this.nextSpecId_++, filename, @@ -245,7 +244,6 @@ getJasmineRequireObj().SuiteBuilder = function(j$) { beforeAndAfterFns: beforeAndAfterFns(suite), expectationFactory: this.expectationFactory_, asyncExpectationFactory: this.specAsyncExpectationFactory_, - setTimeout: global.setTimeout.bind(global), onLateError: this.onLateError_, getPath: spec => this.getSpecPath_(spec, suite), description: description, diff --git a/src/core/TreeRunner.js b/src/core/TreeRunner.js index d65b4fec..2ec24182 100644 --- a/src/core/TreeRunner.js +++ b/src/core/TreeRunner.js @@ -1,6 +1,7 @@ getJasmineRequireObj().TreeRunner = function(j$) { class TreeRunner { #executionTree; + #setTimeout; #globalErrors; #runableResources; #reportDispatcher; @@ -13,6 +14,7 @@ getJasmineRequireObj().TreeRunner = function(j$) { constructor(attrs) { this.#executionTree = attrs.executionTree; this.#globalErrors = attrs.globalErrors; + this.#setTimeout = attrs.setTimeout || setTimeout.bind(globalThis); this.#runableResources = attrs.runableResources; this.#reportDispatcher = attrs.reportDispatcher; this.#runQueue = attrs.runQueue; @@ -57,31 +59,103 @@ getJasmineRequireObj().TreeRunner = function(j$) { if (node.suite) { this.#executeSuiteSegment(node.suite, node.segmentNumber, done); } else { - this.#executeSpec(node.spec, done); + this._executeSpec(node.spec, done); } } }; }); } - #executeSpec(spec, done) { - const config = this.#getConfig(); - spec.execute( - this.#runQueueWithSkipPolicy.bind(this), - this.#globalErrors, - next => { - this.#currentRunableTracker.setCurrentSpec(spec); - this.#runableResources.initForRunable(spec.id, spec.parentSuiteId); - this.#reportDispatcher.specStarted(spec.result).then(next); - }, - (result, next) => { - this.#specComplete(spec).then(next); - }, - done, - this.#executionTree.isExcluded(spec), - config.failSpecWithNoExpectations, - config.detectLateRejectionHandling + // Only exposed for testing. + _executeSpec(spec, specOverallDone) { + const onStart = next => { + this.#currentRunableTracker.setCurrentSpec(spec); + this.#runableResources.initForRunable(spec.id, spec.parentSuiteId); + this.#reportDispatcher.specStarted(spec.result).then(next); + }; + const resultCallback = (result, next) => { + this.#specComplete(spec).then(next); + }; + const queueableFns = this.#specQueueableFns( + spec, + onStart, + resultCallback ); + + this.#runQueueWithSkipPolicy({ + isLeaf: true, + queueableFns, + onException: e => spec.handleException(e), + onMultipleDone: () => { + // Issue an erorr. Include the context ourselves and pass + // ignoreRunnable: true, since getting here always means that we've already + // moved on and the current runnable isn't the one that caused the problem. + spec.onLateError( + new Error( + 'An asynchronous spec, beforeEach, or afterEach function called its ' + + "'done' callback more than once.\n(in spec: " + + spec.getFullName() + + ')' + ) + ); + }, + onComplete: () => { + if (spec.result.status === 'failed') { + specOverallDone(new j$.StopExecutionError('spec failed')); + } else { + specOverallDone(); + } + }, + userContext: spec.userContext(), + runnableName: spec.getFullName.bind(spec) + }); + } + + #specQueueableFns(spec, onStart, resultCallback) { + const config = this.#getConfig(); + const excluded = this.#executionTree.isExcluded(spec); + const ba = spec.beforeAndAfterFns(); + let fns = [...ba.befores, spec.queueableFn, ...ba.afters]; + + if (spec.markedPending || excluded === true) { + fns = []; + } + + const start = { + fn(done) { + spec.executionStarted(); + onStart(done); + } + }; + + const complete = { + fn(done) { + spec.executionFinished(excluded, config.failSpecWithNoExpectations); + resultCallback(spec.result, done); + }, + type: 'specCleanup' + }; + + fns.unshift(start); + + if (config.detectLateRejectionHandling) { + // Conditional because the setTimeout imposes a significant performance + // penalty in suites with lots of fast specs. + const globalErrors = this.#globalErrors; + fns.push({ + fn: done => { + // setTimeout is necessary to trigger rejectionhandled events + // TODO: let clearStack know about this so it doesn't do redundant setTimeouts + this.#setTimeout(function() { + globalErrors.reportUnhandledRejections(); + done(); + }); + } + }); + } + + fns.push(complete); + return fns; } #executeSuiteSegment(suite, segmentNumber, done) {