diff --git a/lib/jasmine-core/boot.js b/lib/jasmine-core/boot.js index 72f45b5e..4655042c 100644 --- a/lib/jasmine-core/boot.js +++ b/lib/jasmine-core/boot.js @@ -1,5 +1,5 @@ /* -Copyright (c) 2008-2017 Pivotal Labs +Copyright (c) 2008-2018 Pivotal Labs Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/lib/jasmine-core/jasmine-html.js b/lib/jasmine-core/jasmine-html.js index 50de0be7..4e9558cd 100644 --- a/lib/jasmine-core/jasmine-html.js +++ b/lib/jasmine-core/jasmine-html.js @@ -1,5 +1,5 @@ /* -Copyright (c) 2008-2017 Pivotal Labs +Copyright (c) 2008-2018 Pivotal Labs Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the @@ -35,6 +35,46 @@ jasmineRequire.HtmlReporter = function(j$) { elapsed: function() { return 0; } }; + function ResultsStateBuilder() { + this.topResults = new j$.ResultsNode({}, '', null); + this.currentParent = this.topResults; + this.specsExecuted = 0; + this.failureCount = 0; + this.pendingSpecCount = 0; + } + + ResultsStateBuilder.prototype.suiteStarted = function(result) { + this.currentParent.addChild(result, 'suite'); + this.currentParent = this.currentParent.last(); + }; + + ResultsStateBuilder.prototype.suiteDone = function(result) { + if (this.currentParent !== this.topResults) { + this.currentParent = this.currentParent.parent; + } + }; + + ResultsStateBuilder.prototype.specStarted = function(result) { + }; + + ResultsStateBuilder.prototype.specDone = function(result) { + this.currentParent.addChild(result, 'spec'); + + if (result.status !== 'disabled') { + this.specsExecuted++; + } + + if (result.status === 'failed') { + this.failureCount++; + } + + if (result.status == 'pending') { + this.pendingSpecCount++; + } + }; + + + function HtmlReporter(options) { var env = options.env || {}, getContainer = options.getContainer, @@ -47,9 +87,6 @@ jasmineRequire.HtmlReporter = function(j$) { filterSpecs = options.filterSpecs, timer = options.timer || noopTimer, results = [], - specsExecuted = 0, - failureCount = 0, - pendingSpecCount = 0, htmlReporterMain, symbols, failedSuites = []; @@ -78,12 +115,10 @@ jasmineRequire.HtmlReporter = function(j$) { var summary = createDom('div', {className: 'jasmine-summary'}); - var topResults = new j$.ResultsNode({}, '', null), - currentParent = topResults; + var stateBuilder = new ResultsStateBuilder(); this.suiteStarted = function(result) { - currentParent.addChild(result, 'suite'); - currentParent = currentParent.last(); + stateBuilder.suiteStarted(result); }; this.suiteDone = function(result) { @@ -91,27 +126,21 @@ jasmineRequire.HtmlReporter = function(j$) { failedSuites.push(result); } - if (currentParent == topResults) { - return; - } - - currentParent = currentParent.parent; + stateBuilder.suiteDone(result); }; this.specStarted = function(result) { - currentParent.addChild(result, 'spec'); + stateBuilder.specStarted(result); }; var failures = []; this.specDone = function(result) { + stateBuilder.specDone(result); + if(noExpectations(result) && typeof console !== 'undefined' && typeof console.error !== 'undefined') { console.error('Spec \'' + result.fullName + '\' has no expectations.'); } - if (result.status != 'disabled') { - specsExecuted++; - } - if (!symbols){ symbols = find('.jasmine-symbol-summary'); } @@ -124,11 +153,9 @@ jasmineRequire.HtmlReporter = function(j$) { )); if (result.status == 'failed') { - failureCount++; - var failure = createDom('div', {className: 'jasmine-spec-detail jasmine-failed'}, - failureDescription(result, currentParent), + failureDescription(result, stateBuilder.currentParent), createDom('div', {className: 'jasmine-messages'}) ); var messages = failure.childNodes[1]; @@ -141,10 +168,6 @@ jasmineRequire.HtmlReporter = function(j$) { failures.push(failure); } - - if (result.status == 'pending') { - pendingSpecCount++; - } }; this.jasmineDone = function(doneResult) { @@ -207,8 +230,8 @@ jasmineRequire.HtmlReporter = function(j$) { } }; - if (specsExecuted < totalSpecsDefined) { - var skippedMessage = 'Ran ' + specsExecuted + ' of ' + totalSpecsDefined + ' specs - run all'; + if (stateBuilder.specsExecuted < totalSpecsDefined) { + var skippedMessage = 'Ran ' + stateBuilder.specsExecuted + ' of ' + totalSpecsDefined + ' specs - run all'; var skippedLink = addToExistingQueryString('spec', ''); alert.appendChild( createDom('span', {className: 'jasmine-bar jasmine-skipped'}, @@ -219,11 +242,11 @@ jasmineRequire.HtmlReporter = function(j$) { var statusBarMessage = ''; var statusBarClassName = 'jasmine-overall-result jasmine-bar '; var globalFailures = (doneResult && doneResult.failedExpectations) || []; - var failed = failureCount + globalFailures.length + failedSuites.length > 0; + var failed = stateBuilder.failureCount + globalFailures.length + failedSuites.length > 0; if (totalSpecsDefined > 0 || failed) { - statusBarMessage += pluralize('spec', specsExecuted) + ', ' + pluralize('failure', failureCount); - if (pendingSpecCount) { statusBarMessage += ', ' + pluralize('pending spec', pendingSpecCount); } + statusBarMessage += pluralize('spec', stateBuilder.specsExecuted) + ', ' + pluralize('failure', stateBuilder.failureCount); + if (stateBuilder.pendingSpecCount) { statusBarMessage += ', ' + pluralize('pending spec', stateBuilder.pendingSpecCount); } } if (doneResult.overallStatus === 'passed') { @@ -276,7 +299,7 @@ jasmineRequire.HtmlReporter = function(j$) { var results = find('.jasmine-results'); results.appendChild(summary); - summaryList(topResults, summary); + summaryList(stateBuilder.topResults, summary); function summaryList(resultsTree, domParent) { var specListNode; diff --git a/lib/jasmine-core/jasmine.js b/lib/jasmine-core/jasmine.js index fbd9dd17..2a270915 100644 --- a/lib/jasmine-core/jasmine.js +++ b/lib/jasmine-core/jasmine.js @@ -34,7 +34,7 @@ var getJasmineRequireObj = (function (jasmineGlobal) { if (typeof window !== 'undefined' && typeof window.toString === 'function' && window.toString() === '[object GjsGlobal]') { jasmineGlobal = window; } - jasmineRequire = jasmineGlobal.jasmineRequire = jasmineGlobal.jasmineRequire || {}; + jasmineRequire = jasmineGlobal.jasmineRequire = {}; } function getJasmineRequire() { @@ -54,7 +54,7 @@ var getJasmineRequireObj = (function (jasmineGlobal) { j$.MockDate = jRequire.MockDate(); j$.getClearStack = jRequire.clearStack(j$); j$.Clock = jRequire.Clock(); - j$.DelayedFunctionScheduler = jRequire.DelayedFunctionScheduler(); + j$.DelayedFunctionScheduler = jRequire.DelayedFunctionScheduler(j$); j$.Env = jRequire.Env(j$); j$.StackTrace = jRequire.StackTrace(j$); j$.ExceptionFormatter = jRequire.ExceptionFormatter(j$); @@ -1951,6 +1951,9 @@ getJasmineRequireObj().clearStack = function(j$) { }; getJasmineRequireObj().Clock = function() { + + var NODE_JS = typeof process !== 'undefined' && process.versions && typeof process.versions.node === 'string'; + /** * _Note:_ Do not construct this directly, Jasmine will make one during booting. You can get the current clock with {@link jasmine.clock}. * @class Clock @@ -1974,6 +1977,7 @@ getJasmineRequireObj().Clock = function() { delayedFunctionScheduler, timer; + self.FakeTimeout = FakeTimeout; /** * Install the mock clock over the built-in methods. @@ -2080,7 +2084,15 @@ getJasmineRequireObj().Clock = function() { } function setTimeout(fn, delay) { - return delayedFunctionScheduler.scheduleFunction(fn, delay, argSlice(arguments, 2)); + if (!NODE_JS) { + return delayedFunctionScheduler.scheduleFunction(fn, delay, argSlice(arguments, 2)); + } + + var timeout = new FakeTimeout(); + + delayedFunctionScheduler.scheduleFunction(fn, delay, argSlice(arguments, 2), false, timeout); + + return timeout; } function clearTimeout(id) { @@ -2088,7 +2100,15 @@ getJasmineRequireObj().Clock = function() { } function setInterval(fn, interval) { - return delayedFunctionScheduler.scheduleFunction(fn, interval, argSlice(arguments, 2), true); + if (!NODE_JS) { + return delayedFunctionScheduler.scheduleFunction(fn, interval, argSlice(arguments, 2), true); + } + + var timeout = new FakeTimeout(); + + delayedFunctionScheduler.scheduleFunction(fn, interval, argSlice(arguments, 2), true, timeout); + + return timeout; } function clearInterval(id) { @@ -2100,16 +2120,30 @@ getJasmineRequireObj().Clock = function() { } } + /** + * Mocks Node.js Timeout class + */ + function FakeTimeout() {} + + FakeTimeout.prototype.ref = function () { + return this; + }; + + FakeTimeout.prototype.unref = function () { + return this; + }; + return Clock; }; -getJasmineRequireObj().DelayedFunctionScheduler = function() { +getJasmineRequireObj().DelayedFunctionScheduler = function(j$) { function DelayedFunctionScheduler() { var self = this; var scheduledLookup = []; var scheduledFunctions = {}; var currentTime = 0; var delayedFnCount = 0; + var deletedKeys = []; self.tick = function(millis, tickDate) { millis = millis || 0; @@ -2156,6 +2190,8 @@ getJasmineRequireObj().DelayedFunctionScheduler = function() { }; self.removeFunctionWithId = function(timeoutKey) { + deletedKeys.push(timeoutKey); + for (var runAtMillis in scheduledFunctions) { var funcs = scheduledFunctions[runAtMillis]; var i = indexOfFirstToPass(funcs, function (func) { @@ -2232,6 +2268,7 @@ getJasmineRequireObj().DelayedFunctionScheduler = function() { currentTime = newCurrentTime; var funcsToRun = scheduledFunctions[currentTime]; + delete scheduledFunctions[currentTime]; forEachFunction(funcsToRun, function(funcToRun) { @@ -2241,8 +2278,13 @@ getJasmineRequireObj().DelayedFunctionScheduler = function() { }); forEachFunction(funcsToRun, function(funcToRun) { + if (j$.util.arrayContains(deletedKeys, funcToRun.timeoutKey)) { + // skip a timeoutKey deleted whilst we were running + return; + } funcToRun.funcToCall.apply(null, funcToRun.params || []); }); + deletedKeys = []; } while (scheduledLookup.length > 0 && // checking first if we're out of time prevents setTimeout(0) // scheduled in a funcToRun from forcing an extra iteration @@ -4127,6 +4169,8 @@ getJasmineRequireObj().pp = function(j$) { function PrettyPrinter() { this.ppNestLevel_ = 0; this.seen = []; + this.length = 0; + this.stringParts = []; } function hasCustomToString(value) { @@ -4212,32 +4256,15 @@ getJasmineRequireObj().pp = function(j$) { return objKeys.length > length; }; - PrettyPrinter.prototype.emitArray = j$.unimplementedMethod_; - PrettyPrinter.prototype.emitSet = j$.unimplementedMethod_; - PrettyPrinter.prototype.emitMap = j$.unimplementedMethod_; - PrettyPrinter.prototype.emitObject = j$.unimplementedMethod_; - PrettyPrinter.prototype.emitScalar = j$.unimplementedMethod_; - PrettyPrinter.prototype.emitString = j$.unimplementedMethod_; - PrettyPrinter.prototype.emitDomElement = j$.unimplementedMethod_; - - function StringPrettyPrinter() { - PrettyPrinter.call(this); - - this.length = 0; - this.stringParts = []; - } - - j$.util.inherit(StringPrettyPrinter, PrettyPrinter); - - StringPrettyPrinter.prototype.emitScalar = function(value) { + PrettyPrinter.prototype.emitScalar = function(value) { this.append(value); }; - StringPrettyPrinter.prototype.emitString = function(value) { + PrettyPrinter.prototype.emitString = function(value) { this.append('\'' + value + '\''); }; - StringPrettyPrinter.prototype.emitArray = function(array) { + PrettyPrinter.prototype.emitArray = function(array) { if (this.ppNestLevel_ > j$.MAX_PRETTY_PRINT_DEPTH) { this.append('Array'); return; @@ -4271,7 +4298,7 @@ getJasmineRequireObj().pp = function(j$) { this.append(' ]'); }; - StringPrettyPrinter.prototype.emitSet = function(set) { + PrettyPrinter.prototype.emitSet = function(set) { if (this.ppNestLevel_ > j$.MAX_PRETTY_PRINT_DEPTH) { this.append('Set'); return; @@ -4291,7 +4318,7 @@ getJasmineRequireObj().pp = function(j$) { this.append(' )'); }; - StringPrettyPrinter.prototype.emitMap = function(map) { + PrettyPrinter.prototype.emitMap = function(map) { if (this.ppNestLevel_ > j$.MAX_PRETTY_PRINT_DEPTH) { this.append('Map'); return; @@ -4311,7 +4338,7 @@ getJasmineRequireObj().pp = function(j$) { this.append(' )'); }; - StringPrettyPrinter.prototype.emitObject = function(obj) { + PrettyPrinter.prototype.emitObject = function(obj) { var ctor = obj.constructor, constructorName; @@ -4344,7 +4371,7 @@ getJasmineRequireObj().pp = function(j$) { this.append(' })'); }; - StringPrettyPrinter.prototype.emitTypedArray = function(arr) { + PrettyPrinter.prototype.emitTypedArray = function(arr) { var constructorName = j$.fnNameFor(arr.constructor), limitedArray = Array.prototype.slice.call(arr, 0, j$.MAX_PRETTY_PRINT_ARRAY_LENGTH), itemsString = Array.prototype.join.call(limitedArray, ', '); @@ -4356,7 +4383,7 @@ getJasmineRequireObj().pp = function(j$) { this.append(constructorName + ' [ ' + itemsString + ' ]'); }; - StringPrettyPrinter.prototype.emitDomElement = function(el) { + PrettyPrinter.prototype.emitDomElement = function(el) { var closingTag = ''; if (el.innerHTML === '') { @@ -4368,7 +4395,7 @@ getJasmineRequireObj().pp = function(j$) { } }; - StringPrettyPrinter.prototype.formatProperty = function(obj, property, isGetter) { + PrettyPrinter.prototype.formatProperty = function(obj, property, isGetter) { this.append(property); this.append(': '); if (isGetter) { @@ -4378,7 +4405,7 @@ getJasmineRequireObj().pp = function(j$) { } }; - StringPrettyPrinter.prototype.append = function(value) { + PrettyPrinter.prototype.append = function(value) { var result = truncate(value, j$.MAX_PRETTY_PRINT_CHARS - this.length); this.length += result.value.length; this.stringParts.push(result.value); @@ -4388,6 +4415,7 @@ getJasmineRequireObj().pp = function(j$) { } }; + function truncate(s, maxlen) { if (s.length <= maxlen) { return { value: s, truncated: false }; @@ -4434,9 +4462,9 @@ getJasmineRequireObj().pp = function(j$) { return extraKeys; } return function(value) { - var stringPrettyPrinter = new StringPrettyPrinter(); - stringPrettyPrinter.format(value); - return stringPrettyPrinter.stringParts.join(''); + var prettyPrinter = new PrettyPrinter(); + prettyPrinter.format(value); + return prettyPrinter.stringParts.join(''); }; }; @@ -5086,6 +5114,7 @@ getJasmineRequireObj().SpyRegistry = function(j$) { function SpyRegistry(options) { options = options || {}; + var global = options.global || j$.getGlobal(); var currentSpies = options.currentSpies || function() { return []; }; this.allowRespy = function(allow){ @@ -5124,7 +5153,7 @@ getJasmineRequireObj().SpyRegistry = function(j$) { spiedMethod = j$.createSpy(methodName, originalMethod), restoreStrategy; - if (Object.prototype.hasOwnProperty.call(obj, methodName)) { + if (Object.prototype.hasOwnProperty.call(obj, methodName) || (obj === global && methodName === 'onerror')) { restoreStrategy = function() { obj[methodName] = originalMethod; }; diff --git a/lib/jasmine-core/node_boot.js b/lib/jasmine-core/node_boot.js index ce90a3ba..2b2d7adf 100644 --- a/lib/jasmine-core/node_boot.js +++ b/lib/jasmine-core/node_boot.js @@ -1,5 +1,5 @@ /* -Copyright (c) 2008-2017 Pivotal Labs +Copyright (c) 2008-2018 Pivotal Labs Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/spec/core/ClockSpec.js b/spec/core/ClockSpec.js index e585149a..f094370c 100644 --- a/spec/core/ClockSpec.js +++ b/spec/core/ClockSpec.js @@ -1,5 +1,7 @@ describe("Clock", function() { + var NODE_JS = typeof process !== 'undefined' && process.versions && typeof process.versions.node === 'string'; + it("does not replace setTimeout until it is installed", function() { var fakeSetTimeout = jasmine.createSpy("global setTimeout"), fakeGlobal = { setTimeout: fakeSetTimeout }, @@ -294,13 +296,19 @@ describe("Clock", function() { fakeGlobal = { setTimeout: fakeSetTimeout }, delayedFn = jasmine.createSpy('delayedFn'), mockDate = { install: function() {}, tick: function() {}, uninstall: function() {} }, - clock = new jasmineUnderTest.Clock(fakeGlobal, function () { return delayedFunctionScheduler; }, mockDate); + clock = new jasmineUnderTest.Clock(fakeGlobal, function () { return delayedFunctionScheduler; }, mockDate), + timeout = new clock.FakeTimeout(); clock.install(); clock.setTimeout(delayedFn, 0, 'a', 'b'); expect(fakeSetTimeout).not.toHaveBeenCalled(); - expect(delayedFunctionScheduler.scheduleFunction).toHaveBeenCalledWith(delayedFn, 0, ['a', 'b']); + + if (!NODE_JS) { + expect(delayedFunctionScheduler.scheduleFunction).toHaveBeenCalledWith(delayedFn, 0, ['a', 'b']); + } else { + expect(delayedFunctionScheduler.scheduleFunction).toHaveBeenCalledWith(delayedFn, 0, ['a', 'b'], false, timeout); + } }); it("returns an id for the delayed function", function() { @@ -312,12 +320,16 @@ describe("Clock", function() { delayedFn = jasmine.createSpy('delayedFn'), mockDate = { install: function() {}, tick: function() {}, uninstall: function() {} }, clock = new jasmineUnderTest.Clock(fakeGlobal, function () { return delayedFunctionScheduler; }, mockDate), - timeoutId; + timeout; clock.install(); - timeoutId = clock.setTimeout(delayedFn, 0); + timeout = clock.setTimeout(delayedFn, 0); - expect(timeoutId).toEqual(123); + if (!NODE_JS) { + expect(timeout).toEqual(123); + } else { + expect(timeout.constructor.name).toEqual('FakeTimeout'); + } }); it("clears the scheduled function with the scheduler", function() { @@ -342,13 +354,19 @@ describe("Clock", function() { fakeGlobal = { setInterval: fakeSetInterval }, delayedFn = jasmine.createSpy('delayedFn'), mockDate = { install: function() {}, tick: function() {}, uninstall: function() {} }, - clock = new jasmineUnderTest.Clock(fakeGlobal, function () { return delayedFunctionScheduler; }, mockDate); + clock = new jasmineUnderTest.Clock(fakeGlobal, function () { return delayedFunctionScheduler; }, mockDate), + timeout = new clock.FakeTimeout; clock.install(); clock.setInterval(delayedFn, 0, 'a', 'b'); expect(fakeSetInterval).not.toHaveBeenCalled(); - expect(delayedFunctionScheduler.scheduleFunction).toHaveBeenCalledWith(delayedFn, 0, ['a', 'b'], true); + + if (!NODE_JS) { + expect(delayedFunctionScheduler.scheduleFunction).toHaveBeenCalledWith(delayedFn, 0, ['a', 'b'], true); + } else { + expect(delayedFunctionScheduler.scheduleFunction).toHaveBeenCalledWith(delayedFn, 0, ['a', 'b'], true, timeout); + } }); it("returns an id for the delayed function", function() { @@ -360,12 +378,16 @@ describe("Clock", function() { delayedFn = jasmine.createSpy('delayedFn'), mockDate = { install: function() {}, tick: function() {}, uninstall: function() {} }, clock = new jasmineUnderTest.Clock(fakeGlobal, function () { return delayedFunctionScheduler; }, mockDate), - intervalId; + interval; clock.install(); - intervalId = clock.setInterval(delayedFn, 0); + interval = clock.setInterval(delayedFn, 0); - expect(intervalId).toEqual(123); + if (!NODE_JS) { + expect(interval).toEqual(123); + } else { + expect(interval.constructor.name).toEqual('FakeTimeout'); + } }); it("clears the scheduled function with the scheduler", function() { @@ -648,4 +670,41 @@ describe("Clock (acceptance)", function() { expect(actualTimes).toEqual([baseTime.getTime(), baseTime.getTime() + 1, baseTime.getTime() + 3]); }) + + it('correctly clears a scheduled timeout while the Clock is advancing', function () { + var delayedFunctionScheduler = new jasmineUnderTest.DelayedFunctionScheduler(), + global = {Date: Date, setTimeout: undefined}, + mockDate = new jasmineUnderTest.MockDate(global), + clock = new jasmineUnderTest.Clock(global, function () { return delayedFunctionScheduler; }, mockDate); + + clock.install(); + + var timerId2; + + global.setTimeout(function () { + global.clearTimeout(timerId2); + }, 100); + + timerId2 = global.setTimeout(fail, 100); + + clock.tick(100); + }); + + it('correctly clears a scheduled interval while the Clock is advancing', function () { + var delayedFunctionScheduler = new jasmineUnderTest.DelayedFunctionScheduler(), + global = {Date: Date, setTimeout: undefined}, + mockDate = new jasmineUnderTest.MockDate(global), + clock = new jasmineUnderTest.Clock(global, function () { return delayedFunctionScheduler; }, mockDate); + + clock.install(); + + var timerId2; + var timerId1 = global.setInterval(function () { + global.clearInterval(timerId2); + }, 100); + + timerId2 = global.setInterval(fail, 100); + + clock.tick(400); + }); }); diff --git a/spec/core/DelayedFunctionSchedulerSpec.js b/spec/core/DelayedFunctionSchedulerSpec.js index c26252ef..fa846d6f 100644 --- a/spec/core/DelayedFunctionSchedulerSpec.js +++ b/spec/core/DelayedFunctionSchedulerSpec.js @@ -216,21 +216,23 @@ describe("DelayedFunctionScheduler", function() { it("removes functions during a tick that runs the function", function() { var scheduler = new jasmineUnderTest.DelayedFunctionScheduler(), - fn = jasmine.createSpy('fn'), + spy = jasmine.createSpy('fn'), + spyAndRemove = jasmine.createSpy('fn'), fnDelay = 10, timeoutKey; - timeoutKey = scheduler.scheduleFunction(fn, fnDelay, [], true); - scheduler.scheduleFunction(function () { + spyAndRemove.and.callFake(function() { scheduler.removeFunctionWithId(timeoutKey); - }, 2 * fnDelay); + }); - expect(fn).not.toHaveBeenCalled(); + scheduler.scheduleFunction(spyAndRemove, fnDelay); - scheduler.tick(3 * fnDelay); + timeoutKey = scheduler.scheduleFunction(spy, fnDelay, [], true); - expect(fn).toHaveBeenCalled(); - expect(fn.calls.count()).toBe(2); + scheduler.tick(2 * fnDelay); + + expect(spy).not.toHaveBeenCalled(); + expect(spyAndRemove).toHaveBeenCalled(); }); it("removes functions during the first tick that runs the function", function() { diff --git a/spec/core/SpyRegistrySpec.js b/spec/core/SpyRegistrySpec.js index b25dbaaa..3a11cba8 100644 --- a/spec/core/SpyRegistrySpec.js +++ b/spec/core/SpyRegistrySpec.js @@ -275,6 +275,24 @@ describe("SpyRegistry", function() { expect(jasmineUnderTest.isSpy(subject.spiedFunc)).toBe(false); }); + + it("restores window.onerror by overwriting, not deleting", function() { + function FakeWindow() { + } + FakeWindow.prototype.onerror = function() {}; + + var spies = [], + global = new FakeWindow(), + spyRegistry = new jasmineUnderTest.SpyRegistry({ + currentSpies: function() { return spies; }, + global: global + }); + + spyRegistry.spyOn(global, 'onerror'); + spyRegistry.clearSpies(); + expect(global.onerror).toBe(FakeWindow.prototype.onerror); + expect(global.hasOwnProperty('onerror')).toBe(true); + }); }); describe('spying on properties', function() { diff --git a/spec/html/HtmlReporterSpec.js b/spec/html/HtmlReporterSpec.js index 91497df0..795a5ed9 100644 --- a/spec/html/HtmlReporterSpec.js +++ b/spec/html/HtmlReporterSpec.js @@ -241,7 +241,7 @@ describe("HtmlReporter", function() { }); describe("when Jasmine is done", function() { - it("adds EMPTY to the link title of specs that have no expectations", function() { + it("adds a warning to the link title of specs that have no expectations", function() { if (!window.console) { window.console = { error: function(){} }; } @@ -260,7 +260,7 @@ describe("HtmlReporter", function() { reporter.initialize(); reporter.jasmineStarted({}); reporter.suiteStarted({id: 1}); - reporter.specStarted({id: 1, status: 'passed', passedExpectations: [], failedExpectations: []}); + reporter.specStarted({id: 1, passedExpectations: [], failedExpectations: []}); reporter.specDone({ id: 1, status: 'passed', diff --git a/spec/html/SpyRegistryHtmlSpec.js b/spec/html/SpyRegistryHtmlSpec.js new file mode 100644 index 00000000..fd9a81fb --- /dev/null +++ b/spec/html/SpyRegistryHtmlSpec.js @@ -0,0 +1,34 @@ +describe('Spy Registry browser-specific behavior', function() { + it('can spy on and unspy window.onerror', function() { + requireWriteableOnerror(); + + var spies = [], + spyRegistry = new jasmineUnderTest.SpyRegistry({ + currentSpies: function() { return spies; }, + global: window + }), + originalHandler = window.onerror; + + try { + spyRegistry.spyOn(window, 'onerror'); + spyRegistry.clearSpies(); + expect(window.onerror).toBe(originalHandler); + } finally { + window.onerror = originalHandler; + } + }); + + function requireWriteableOnerror() { + var descriptor; + + try { + descriptor = Object.getOwnPropertyDescriptor(window, 'onerror'); + } catch(e) { + // IE 8 doesn't support `definePropery` on non-DOM nodes + } + + if (descriptor && !(descriptor.writable || descriptor.set)) { + pending('Browser declares window.onerror to be readonly'); + } + } +}); diff --git a/spec/support/jasmine.yml b/spec/support/jasmine.yml index e7e989d8..36c29e91 100644 --- a/spec/support/jasmine.yml +++ b/spec/support/jasmine.yml @@ -3,6 +3,7 @@ src_dir: - 'src' src_files: + - 'core/requireCore.js' - 'core/base.js' - 'core/util.js' #end of known dependencies diff --git a/src/core/Clock.js b/src/core/Clock.js index a0602c6a..8681c390 100644 --- a/src/core/Clock.js +++ b/src/core/Clock.js @@ -1,4 +1,7 @@ getJasmineRequireObj().Clock = function() { + + var NODE_JS = typeof process !== 'undefined' && process.versions && typeof process.versions.node === 'string'; + /** * _Note:_ Do not construct this directly, Jasmine will make one during booting. You can get the current clock with {@link jasmine.clock}. * @class Clock @@ -22,6 +25,7 @@ getJasmineRequireObj().Clock = function() { delayedFunctionScheduler, timer; + self.FakeTimeout = FakeTimeout; /** * Install the mock clock over the built-in methods. @@ -128,7 +132,15 @@ getJasmineRequireObj().Clock = function() { } function setTimeout(fn, delay) { - return delayedFunctionScheduler.scheduleFunction(fn, delay, argSlice(arguments, 2)); + if (!NODE_JS) { + return delayedFunctionScheduler.scheduleFunction(fn, delay, argSlice(arguments, 2)); + } + + var timeout = new FakeTimeout(); + + delayedFunctionScheduler.scheduleFunction(fn, delay, argSlice(arguments, 2), false, timeout); + + return timeout; } function clearTimeout(id) { @@ -136,7 +148,15 @@ getJasmineRequireObj().Clock = function() { } function setInterval(fn, interval) { - return delayedFunctionScheduler.scheduleFunction(fn, interval, argSlice(arguments, 2), true); + if (!NODE_JS) { + return delayedFunctionScheduler.scheduleFunction(fn, interval, argSlice(arguments, 2), true); + } + + var timeout = new FakeTimeout(); + + delayedFunctionScheduler.scheduleFunction(fn, interval, argSlice(arguments, 2), true, timeout); + + return timeout; } function clearInterval(id) { @@ -148,5 +168,18 @@ getJasmineRequireObj().Clock = function() { } } + /** + * Mocks Node.js Timeout class + */ + function FakeTimeout() {} + + FakeTimeout.prototype.ref = function () { + return this; + }; + + FakeTimeout.prototype.unref = function () { + return this; + }; + return Clock; }; diff --git a/src/core/DelayedFunctionScheduler.js b/src/core/DelayedFunctionScheduler.js index 4756b323..62cf5dc9 100644 --- a/src/core/DelayedFunctionScheduler.js +++ b/src/core/DelayedFunctionScheduler.js @@ -1,10 +1,11 @@ -getJasmineRequireObj().DelayedFunctionScheduler = function() { +getJasmineRequireObj().DelayedFunctionScheduler = function(j$) { function DelayedFunctionScheduler() { var self = this; var scheduledLookup = []; var scheduledFunctions = {}; var currentTime = 0; var delayedFnCount = 0; + var deletedKeys = []; self.tick = function(millis, tickDate) { millis = millis || 0; @@ -51,6 +52,8 @@ getJasmineRequireObj().DelayedFunctionScheduler = function() { }; self.removeFunctionWithId = function(timeoutKey) { + deletedKeys.push(timeoutKey); + for (var runAtMillis in scheduledFunctions) { var funcs = scheduledFunctions[runAtMillis]; var i = indexOfFirstToPass(funcs, function (func) { @@ -127,6 +130,7 @@ getJasmineRequireObj().DelayedFunctionScheduler = function() { currentTime = newCurrentTime; var funcsToRun = scheduledFunctions[currentTime]; + delete scheduledFunctions[currentTime]; forEachFunction(funcsToRun, function(funcToRun) { @@ -136,8 +140,13 @@ getJasmineRequireObj().DelayedFunctionScheduler = function() { }); forEachFunction(funcsToRun, function(funcToRun) { + if (j$.util.arrayContains(deletedKeys, funcToRun.timeoutKey)) { + // skip a timeoutKey deleted whilst we were running + return; + } funcToRun.funcToCall.apply(null, funcToRun.params || []); }); + deletedKeys = []; } while (scheduledLookup.length > 0 && // checking first if we're out of time prevents setTimeout(0) // scheduled in a funcToRun from forcing an extra iteration diff --git a/src/core/PrettyPrinter.js b/src/core/PrettyPrinter.js index 71b6434a..618dd510 100644 --- a/src/core/PrettyPrinter.js +++ b/src/core/PrettyPrinter.js @@ -3,6 +3,8 @@ getJasmineRequireObj().pp = function(j$) { function PrettyPrinter() { this.ppNestLevel_ = 0; this.seen = []; + this.length = 0; + this.stringParts = []; } function hasCustomToString(value) { @@ -88,32 +90,15 @@ getJasmineRequireObj().pp = function(j$) { return objKeys.length > length; }; - PrettyPrinter.prototype.emitArray = j$.unimplementedMethod_; - PrettyPrinter.prototype.emitSet = j$.unimplementedMethod_; - PrettyPrinter.prototype.emitMap = j$.unimplementedMethod_; - PrettyPrinter.prototype.emitObject = j$.unimplementedMethod_; - PrettyPrinter.prototype.emitScalar = j$.unimplementedMethod_; - PrettyPrinter.prototype.emitString = j$.unimplementedMethod_; - PrettyPrinter.prototype.emitDomElement = j$.unimplementedMethod_; - - function StringPrettyPrinter() { - PrettyPrinter.call(this); - - this.length = 0; - this.stringParts = []; - } - - j$.util.inherit(StringPrettyPrinter, PrettyPrinter); - - StringPrettyPrinter.prototype.emitScalar = function(value) { + PrettyPrinter.prototype.emitScalar = function(value) { this.append(value); }; - StringPrettyPrinter.prototype.emitString = function(value) { + PrettyPrinter.prototype.emitString = function(value) { this.append('\'' + value + '\''); }; - StringPrettyPrinter.prototype.emitArray = function(array) { + PrettyPrinter.prototype.emitArray = function(array) { if (this.ppNestLevel_ > j$.MAX_PRETTY_PRINT_DEPTH) { this.append('Array'); return; @@ -147,7 +132,7 @@ getJasmineRequireObj().pp = function(j$) { this.append(' ]'); }; - StringPrettyPrinter.prototype.emitSet = function(set) { + PrettyPrinter.prototype.emitSet = function(set) { if (this.ppNestLevel_ > j$.MAX_PRETTY_PRINT_DEPTH) { this.append('Set'); return; @@ -167,7 +152,7 @@ getJasmineRequireObj().pp = function(j$) { this.append(' )'); }; - StringPrettyPrinter.prototype.emitMap = function(map) { + PrettyPrinter.prototype.emitMap = function(map) { if (this.ppNestLevel_ > j$.MAX_PRETTY_PRINT_DEPTH) { this.append('Map'); return; @@ -187,7 +172,7 @@ getJasmineRequireObj().pp = function(j$) { this.append(' )'); }; - StringPrettyPrinter.prototype.emitObject = function(obj) { + PrettyPrinter.prototype.emitObject = function(obj) { var ctor = obj.constructor, constructorName; @@ -220,7 +205,7 @@ getJasmineRequireObj().pp = function(j$) { this.append(' })'); }; - StringPrettyPrinter.prototype.emitTypedArray = function(arr) { + PrettyPrinter.prototype.emitTypedArray = function(arr) { var constructorName = j$.fnNameFor(arr.constructor), limitedArray = Array.prototype.slice.call(arr, 0, j$.MAX_PRETTY_PRINT_ARRAY_LENGTH), itemsString = Array.prototype.join.call(limitedArray, ', '); @@ -232,7 +217,7 @@ getJasmineRequireObj().pp = function(j$) { this.append(constructorName + ' [ ' + itemsString + ' ]'); }; - StringPrettyPrinter.prototype.emitDomElement = function(el) { + PrettyPrinter.prototype.emitDomElement = function(el) { var closingTag = ''; if (el.innerHTML === '') { @@ -244,7 +229,7 @@ getJasmineRequireObj().pp = function(j$) { } }; - StringPrettyPrinter.prototype.formatProperty = function(obj, property, isGetter) { + PrettyPrinter.prototype.formatProperty = function(obj, property, isGetter) { this.append(property); this.append(': '); if (isGetter) { @@ -254,7 +239,7 @@ getJasmineRequireObj().pp = function(j$) { } }; - StringPrettyPrinter.prototype.append = function(value) { + PrettyPrinter.prototype.append = function(value) { var result = truncate(value, j$.MAX_PRETTY_PRINT_CHARS - this.length); this.length += result.value.length; this.stringParts.push(result.value); @@ -264,6 +249,7 @@ getJasmineRequireObj().pp = function(j$) { } }; + function truncate(s, maxlen) { if (s.length <= maxlen) { return { value: s, truncated: false }; @@ -310,8 +296,8 @@ getJasmineRequireObj().pp = function(j$) { return extraKeys; } return function(value) { - var stringPrettyPrinter = new StringPrettyPrinter(); - stringPrettyPrinter.format(value); - return stringPrettyPrinter.stringParts.join(''); + var prettyPrinter = new PrettyPrinter(); + prettyPrinter.format(value); + return prettyPrinter.stringParts.join(''); }; }; diff --git a/src/core/SpyRegistry.js b/src/core/SpyRegistry.js index 139bfa3d..7ccb5d3c 100644 --- a/src/core/SpyRegistry.js +++ b/src/core/SpyRegistry.js @@ -4,6 +4,7 @@ getJasmineRequireObj().SpyRegistry = function(j$) { function SpyRegistry(options) { options = options || {}; + var global = options.global || j$.getGlobal(); var currentSpies = options.currentSpies || function() { return []; }; this.allowRespy = function(allow){ @@ -42,7 +43,7 @@ getJasmineRequireObj().SpyRegistry = function(j$) { spiedMethod = j$.createSpy(methodName, originalMethod), restoreStrategy; - if (Object.prototype.hasOwnProperty.call(obj, methodName)) { + if (Object.prototype.hasOwnProperty.call(obj, methodName) || (obj === global && methodName === 'onerror')) { restoreStrategy = function() { obj[methodName] = originalMethod; }; diff --git a/src/core/requireCore.js b/src/core/requireCore.js index 78fb343a..b1e19eed 100644 --- a/src/core/requireCore.js +++ b/src/core/requireCore.js @@ -12,7 +12,7 @@ var getJasmineRequireObj = (function (jasmineGlobal) { if (typeof window !== 'undefined' && typeof window.toString === 'function' && window.toString() === '[object GjsGlobal]') { jasmineGlobal = window; } - jasmineRequire = jasmineGlobal.jasmineRequire = jasmineGlobal.jasmineRequire || {}; + jasmineRequire = jasmineGlobal.jasmineRequire = {}; } function getJasmineRequire() { @@ -32,7 +32,7 @@ var getJasmineRequireObj = (function (jasmineGlobal) { j$.MockDate = jRequire.MockDate(); j$.getClearStack = jRequire.clearStack(j$); j$.Clock = jRequire.Clock(); - j$.DelayedFunctionScheduler = jRequire.DelayedFunctionScheduler(); + j$.DelayedFunctionScheduler = jRequire.DelayedFunctionScheduler(j$); j$.Env = jRequire.Env(j$); j$.StackTrace = jRequire.StackTrace(j$); j$.ExceptionFormatter = jRequire.ExceptionFormatter(j$); diff --git a/src/html/HtmlReporter.js b/src/html/HtmlReporter.js index 181ee3a9..72d4ee78 100644 --- a/src/html/HtmlReporter.js +++ b/src/html/HtmlReporter.js @@ -5,6 +5,46 @@ jasmineRequire.HtmlReporter = function(j$) { elapsed: function() { return 0; } }; + function ResultsStateBuilder() { + this.topResults = new j$.ResultsNode({}, '', null); + this.currentParent = this.topResults; + this.specsExecuted = 0; + this.failureCount = 0; + this.pendingSpecCount = 0; + } + + ResultsStateBuilder.prototype.suiteStarted = function(result) { + this.currentParent.addChild(result, 'suite'); + this.currentParent = this.currentParent.last(); + }; + + ResultsStateBuilder.prototype.suiteDone = function(result) { + if (this.currentParent !== this.topResults) { + this.currentParent = this.currentParent.parent; + } + }; + + ResultsStateBuilder.prototype.specStarted = function(result) { + }; + + ResultsStateBuilder.prototype.specDone = function(result) { + this.currentParent.addChild(result, 'spec'); + + if (result.status !== 'disabled') { + this.specsExecuted++; + } + + if (result.status === 'failed') { + this.failureCount++; + } + + if (result.status == 'pending') { + this.pendingSpecCount++; + } + }; + + + function HtmlReporter(options) { var env = options.env || {}, getContainer = options.getContainer, @@ -17,9 +57,6 @@ jasmineRequire.HtmlReporter = function(j$) { filterSpecs = options.filterSpecs, timer = options.timer || noopTimer, results = [], - specsExecuted = 0, - failureCount = 0, - pendingSpecCount = 0, htmlReporterMain, symbols, failedSuites = []; @@ -48,12 +85,10 @@ jasmineRequire.HtmlReporter = function(j$) { var summary = createDom('div', {className: 'jasmine-summary'}); - var topResults = new j$.ResultsNode({}, '', null), - currentParent = topResults; + var stateBuilder = new ResultsStateBuilder(); this.suiteStarted = function(result) { - currentParent.addChild(result, 'suite'); - currentParent = currentParent.last(); + stateBuilder.suiteStarted(result); }; this.suiteDone = function(result) { @@ -61,27 +96,21 @@ jasmineRequire.HtmlReporter = function(j$) { failedSuites.push(result); } - if (currentParent == topResults) { - return; - } - - currentParent = currentParent.parent; + stateBuilder.suiteDone(result); }; this.specStarted = function(result) { - currentParent.addChild(result, 'spec'); + stateBuilder.specStarted(result); }; var failures = []; this.specDone = function(result) { + stateBuilder.specDone(result); + if(noExpectations(result) && typeof console !== 'undefined' && typeof console.error !== 'undefined') { console.error('Spec \'' + result.fullName + '\' has no expectations.'); } - if (result.status != 'disabled') { - specsExecuted++; - } - if (!symbols){ symbols = find('.jasmine-symbol-summary'); } @@ -94,11 +123,9 @@ jasmineRequire.HtmlReporter = function(j$) { )); if (result.status == 'failed') { - failureCount++; - var failure = createDom('div', {className: 'jasmine-spec-detail jasmine-failed'}, - failureDescription(result, currentParent), + failureDescription(result, stateBuilder.currentParent), createDom('div', {className: 'jasmine-messages'}) ); var messages = failure.childNodes[1]; @@ -111,10 +138,6 @@ jasmineRequire.HtmlReporter = function(j$) { failures.push(failure); } - - if (result.status == 'pending') { - pendingSpecCount++; - } }; this.jasmineDone = function(doneResult) { @@ -177,8 +200,8 @@ jasmineRequire.HtmlReporter = function(j$) { } }; - if (specsExecuted < totalSpecsDefined) { - var skippedMessage = 'Ran ' + specsExecuted + ' of ' + totalSpecsDefined + ' specs - run all'; + if (stateBuilder.specsExecuted < totalSpecsDefined) { + var skippedMessage = 'Ran ' + stateBuilder.specsExecuted + ' of ' + totalSpecsDefined + ' specs - run all'; var skippedLink = addToExistingQueryString('spec', ''); alert.appendChild( createDom('span', {className: 'jasmine-bar jasmine-skipped'}, @@ -189,11 +212,11 @@ jasmineRequire.HtmlReporter = function(j$) { var statusBarMessage = ''; var statusBarClassName = 'jasmine-overall-result jasmine-bar '; var globalFailures = (doneResult && doneResult.failedExpectations) || []; - var failed = failureCount + globalFailures.length + failedSuites.length > 0; + var failed = stateBuilder.failureCount + globalFailures.length + failedSuites.length > 0; if (totalSpecsDefined > 0 || failed) { - statusBarMessage += pluralize('spec', specsExecuted) + ', ' + pluralize('failure', failureCount); - if (pendingSpecCount) { statusBarMessage += ', ' + pluralize('pending spec', pendingSpecCount); } + statusBarMessage += pluralize('spec', stateBuilder.specsExecuted) + ', ' + pluralize('failure', stateBuilder.failureCount); + if (stateBuilder.pendingSpecCount) { statusBarMessage += ', ' + pluralize('pending spec', stateBuilder.pendingSpecCount); } } if (doneResult.overallStatus === 'passed') { @@ -246,7 +269,7 @@ jasmineRequire.HtmlReporter = function(j$) { var results = find('.jasmine-results'); results.appendChild(summary); - summaryList(topResults, summary); + summaryList(stateBuilder.topResults, summary); function summaryList(resultsTree, domParent) { var specListNode;