diff --git a/lib/jasmine-core/jasmine.js b/lib/jasmine-core/jasmine.js index 90adb387..f281779f 100644 --- a/lib/jasmine-core/jasmine.js +++ b/lib/jasmine-core/jasmine.js @@ -886,12 +886,11 @@ getJasmineRequireObj().Spec = function(j$) { * @property {String} description - The description passed to the {@link it} that created this spec. * @property {String} fullName - The full description including all ancestors of this spec. * @property {String|null} parentSuiteId - The ID of the suite containing this spec, or null if this spec is not in a describe(). - * @property {String} filename - Deprecated. The name of the file the spec was defined in. + * @property {String} filename - The name of the file the spec was defined in. * Note: The value may be incorrect if zone.js is installed or * `it`/`fit`/`xit` have been replaced with versions that don't maintain the - * same call stack height as the originals. This property may be removed in - * a future version unless there is enough user interest in keeping it. - * See {@link https://github.com/jasmine/jasmine/issues/2065}. + * same call stack height as the originals. You can fix that by setting + * {@link Configuration#extraItStackFrames}. * @property {ExpectationResult[]} failedExpectations - The list of expectations that failed during execution of this spec. * @property {ExpectationResult[]} passedExpectations - The list of expectations that passed during execution of this spec. * @property {ExpectationResult[]} deprecationWarnings - The list of deprecation warnings that occurred during execution this spec. @@ -1065,7 +1064,20 @@ getJasmineRequireObj().Spec = function(j$) { * @returns {Array.} * @since 5.7.0 */ - getPath: this.getPath.bind(this) + getPath: this.getPath.bind(this), + + /** + * The name of the file the spec was defined in. + * Note: The value may be incorrect if zone.js is installed or + * `it`/`fit`/`xit` have been replaced with versions that don't maintain the + * same call stack height as the originals. You can fix that by setting + * {@link Configuration#extraItStackFrames}. + * @name Spec#filename + * @readonly + * @type {string} + * @since 5.13.0 + */ + filename: this.filename }; } @@ -1140,6 +1152,8 @@ getJasmineRequireObj().Order = function() { }; getJasmineRequireObj().Env = function(j$) { + const DEFAULT_IT_DESCRIBE_STACK_DEPTH = 3; + /** * @class Env * @since 2.0.0 @@ -1734,14 +1748,14 @@ getJasmineRequireObj().Env = function(j$) { this.describe = function(description, definitionFn) { ensureIsNotNested('describe'); - const filename = callerCallerFilename(); + const filename = indirectCallerFilename(describeStackDepth()); return suiteBuilder.describe(description, definitionFn, filename) .metadata; }; this.xdescribe = function(description, definitionFn) { ensureIsNotNested('xdescribe'); - const filename = callerCallerFilename(); + const filename = indirectCallerFilename(describeStackDepth()); return suiteBuilder.xdescribe(description, definitionFn, filename) .metadata; }; @@ -1749,30 +1763,38 @@ getJasmineRequireObj().Env = function(j$) { this.fdescribe = function(description, definitionFn) { ensureIsNotNested('fdescribe'); ensureNonParallel('fdescribe'); - const filename = callerCallerFilename(); + const filename = indirectCallerFilename(describeStackDepth()); return suiteBuilder.fdescribe(description, definitionFn, filename) .metadata; }; this.it = function(description, fn, timeout) { ensureIsNotNested('it'); - const filename = callerCallerFilename(); + const filename = indirectCallerFilename(itStackDepth()); return suiteBuilder.it(description, fn, timeout, filename).metadata; }; this.xit = function(description, fn, timeout) { ensureIsNotNested('xit'); - const filename = callerCallerFilename(); + const filename = indirectCallerFilename(itStackDepth()); return suiteBuilder.xit(description, fn, timeout, filename).metadata; }; this.fit = function(description, fn, timeout) { ensureIsNotNested('fit'); ensureNonParallel('fit'); - const filename = callerCallerFilename(); + const filename = indirectCallerFilename(itStackDepth()); return suiteBuilder.fit(description, fn, timeout, filename).metadata; }; + function itStackDepth() { + return DEFAULT_IT_DESCRIBE_STACK_DEPTH + config.extraItStackFrames; + } + + function describeStackDepth() { + return DEFAULT_IT_DESCRIBE_STACK_DEPTH + config.extraDescribeStackFrames; + } + /** * Get a user-defined property as part of the properties field of {@link SpecResult} * @name Env#getSpecProperty @@ -1958,11 +1980,12 @@ getJasmineRequireObj().Env = function(j$) { }; } - function callerCallerFilename() { + function indirectCallerFilename(depth) { const frames = new j$.StackTrace(new Error()).frames; - // frames[3] should always exist except in Jasmine's own tests, which bypass - // the global it/describe layer, but don't crash if it doesn't. - return frames[3] && frames[3].file; + // The specified frame should always exist except in Jasmine's own tests, + // which bypass the global it/describe layer, but could be absent in case + // of misconfiguration. Don't crash if it's absent. + return frames[depth] && frames[depth].file; } return Env; @@ -3431,7 +3454,30 @@ getJasmineRequireObj().Configuration = function(j$) { * @type Boolean * @default false */ - detectLateRejectionHandling: false + detectLateRejectionHandling: false, + + /** + * The number of extra stack frames inserted by a wrapper around {@link it} + * or by some other local modification. Jasmine uses this to determine the + * filename for {@link SpecStartedEvent} and {@link SpecDoneEvent}. + * @name Configuration#extraItStackFrames + * @since 5.13.0 + * @type number + * @default 0 + */ + extraItStackFrames: 0, + + /** + * The number of extra stack frames inserted by a wrapper around + * {@link describe} or by some other local modification. Jasmine uses this + * to determine the filename for {@link SpecStartedEvent} and + * {@link SpecDoneEvent}. + * @name Configuration#extraDescribeStackFrames + * @since 5.13.0 + * @type number + * @default 0 + */ + extraDescribeStackFrames: 0 }; Object.freeze(defaultConfig); @@ -3487,6 +3533,16 @@ getJasmineRequireObj().Configuration = function(j$) { if (changes.hasOwnProperty('verboseDeprecations')) { this.#values.verboseDeprecations = changes.verboseDeprecations; } + + // 0 is a valid value for both of these, so a truthiness check wouldn't work + if (typeof changes.extraItStackFrames !== 'undefined') { + this.#values.extraItStackFrames = changes.extraItStackFrames; + } + + if (typeof changes.extraDescribeStackFrames !== 'undefined') { + this.#values.extraDescribeStackFrames = + changes.extraDescribeStackFrames; + } } } @@ -10663,12 +10719,11 @@ getJasmineRequireObj().Suite = function(j$) { * @property {String} description - The description text passed to the {@link describe} that made this suite. * @property {String} fullName - The full description including all ancestors of this suite. * @property {String|null} parentSuiteId - The ID of the suite containing this suite, or null if this is not in another describe(). - * @property {String} filename - Deprecated. The name of the file the suite was defined in. + * @property {String} filename - The name of the file the suite was defined in. * Note: The value may be incorrect if zone.js is installed or * `describe`/`fdescribe`/`xdescribe` have been replaced with versions that - * don't maintain the same call stack height as the originals. This property - * may be removed in a future version unless there is enough user interest - * in keeping it. See {@link https://github.com/jasmine/jasmine/issues/2065}. + * don't maintain the same call stack height as the originals. You can fix + * that by setting {@link Configuration#extraDescribeStackFrames}. * @property {ExpectationResult[]} failedExpectations - The list of expectations that failed in an {@link afterAll} for this suite. * @property {ExpectationResult[]} deprecationWarnings - The list of deprecation warnings that occurred on this suite. * @property {String} status - Once the suite has completed, this string represents the pass/fail status of this suite. @@ -10875,6 +10930,19 @@ getJasmineRequireObj().Suite = function(j$) { * @since 2.0.0 */ this.description = suite.description; + + /** + * The name of the file the suite was defined in. + * Note: The value may be incorrect if zone.js is installed or + * `describe`/`fdescribe`/`xdescribe` have been replaced with versions + * that don't maintain the same call stack height as the originals. You + * can fix that by setting {@link Configuration#extraItStackFrames}. + * @name Suite#filename + * @readonly + * @type {string} + * @since 5.13.0 + */ + this.filename = suite.filename; } /** diff --git a/spec/core/ConfigurationSpec.js b/spec/core/ConfigurationSpec.js index bc548380..3b3d8a91 100644 --- a/spec/core/ConfigurationSpec.js +++ b/spec/core/ConfigurationSpec.js @@ -13,7 +13,9 @@ describe('Configuration', function() { ...standardBooleanKeys, 'seed', 'specFilter', - 'verboseDeprecations' + 'verboseDeprecations', + 'extraItStackFrames', + 'extraDescribeStackFrames' ]; Object.freeze(standardBooleanKeys); Object.freeze(allKeys); @@ -32,6 +34,8 @@ describe('Configuration', function() { expect(subject.forbidDuplicateNames).toEqual(false); expect(subject.verboseDeprecations).toEqual(false); expect(subject.detectLateRejectionHandling).toEqual(false); + expect(subject.extraItStackFrames).toEqual(0); + expect(subject.extraDescribeStackFrames).toEqual(0); }); describe('copy()', function() { @@ -130,5 +134,25 @@ describe('Configuration', function() { subject.update({ seed: null }); expect(subject.seed).toBeNull(); }); + + it('sets extraItStackFrames when not undefined', function() { + const subject = new jasmineUnderTest.Configuration(); + + subject.update({ extraItStackFrames: undefined }); + expect(subject.extraItStackFrames).toEqual(0); + + subject.update({ extraItStackFrames: 100000 }); + expect(subject.extraItStackFrames).toEqual(100000); + }); + + it('sets extraDescribeStackFrames when not undefined', function() { + const subject = new jasmineUnderTest.Configuration(); + + subject.update({ extraDescribeStackFrames: undefined }); + expect(subject.extraDescribeStackFrames).toEqual(0); + + subject.update({ extraDescribeStackFrames: 100000 }); + expect(subject.extraDescribeStackFrames).toEqual(100000); + }); }); }); diff --git a/spec/core/EnvSpec.js b/spec/core/EnvSpec.js index db258a00..39392ce1 100644 --- a/spec/core/EnvSpec.js +++ b/spec/core/EnvSpec.js @@ -95,7 +95,7 @@ describe('Env', function() { }); }); - it('accepts its own current configureation', function() { + it('accepts its own current configuration', function() { env.configure(env.configuration()); }); @@ -198,6 +198,29 @@ describe('Env', function() { expect(innerSuite.parentSuite).toBe(suite); expect(spec.getFullName()).toEqual('outer suite inner suite a spec'); }); + + it('sets the caller filename correctly when extraDescribeStackFrames is not set', function() { + // IIFE is used to match the stack depth when global describe() is called + const suite = (function() { + return env[methodName]('a suite', function() { + env.it('a spec'); + }); + })(); + expect(suite.filename).toMatch(/EnvSpec\.js$/); + }); + + it('sets the caller filename correctly when extraDescribeStackFrames is set', function() { + env.configure({ extraDescribeStackFrames: 2 }); + // IIFE is used to match the stack depth when global describe() is called + const suite = (function() { + return specHelpers.callerFilenameShim(function() { + return env[methodName]('a suite', function() { + env.it('a spec'); + }); + }); + })(); + expect(suite.filename).toMatch(/EnvSpec\.js$/); + }); } describe('#describe', function() { @@ -300,6 +323,25 @@ describe('Env', function() { .not.toEqual(''); expect(spec.pend).toBeFalsy(); }); + + it('sets the caller filename correctly when extraItStackFrames is not set', function() { + // IIFE is used to match the stack depth when global it() is called + const spec = (function() { + return env[methodName]('a spec', function() {}); + })(); + expect(spec.filename).toMatch(/EnvSpec\.js$/); + }); + + it('sets the caller filename correctly when extraItStackFrames is set', function() { + env.configure({ extraItStackFrames: 2 }); + // IIFE is used to match the stack depth when global it() is called + const spec = (function() { + return specHelpers.callerFilenameShim(function() { + return env[methodName]('a spec', function() {}); + }); + })(); + expect(spec.filename).toMatch(/EnvSpec\.js$/); + }); } describe('#it', function() { diff --git a/spec/helpers/callerFilenameShim.js b/spec/helpers/callerFilenameShim.js new file mode 100644 index 00000000..18f4a132 --- /dev/null +++ b/spec/helpers/callerFilenameShim.js @@ -0,0 +1,5 @@ +(function() { + specHelpers.callerFilenameShim = function(fn) { + return fn(); + }; +})(); diff --git a/spec/support/jasmine-browser.js b/spec/support/jasmine-browser.js index dd24f3eb..09ffdf9b 100644 --- a/spec/support/jasmine-browser.js +++ b/spec/support/jasmine-browser.js @@ -23,6 +23,7 @@ module.exports = { 'helpers/BrowserFlags.js', 'helpers/domHelpers.js', 'helpers/integrationMatchers.js', + 'helpers/callerFilenameShim.js', 'helpers/defineJasmineUnderTest.js', 'helpers/resetEnv.js' ], diff --git a/spec/support/jasmine.json b/spec/support/jasmine.json index a2018b66..e9094d74 100644 --- a/spec/support/jasmine.json +++ b/spec/support/jasmine.json @@ -8,6 +8,7 @@ "helpers/init.js", "helpers/domHelpers.js", "helpers/integrationMatchers.js", + "helpers/callerFilenameShim.js", "helpers/overrideConsoleLogForCircleCi.js", "helpers/nodeDefineJasmineUnderTest.js", "helpers/resetEnv.js" diff --git a/src/core/Configuration.js b/src/core/Configuration.js index 4d32d5ce..1c13839d 100644 --- a/src/core/Configuration.js +++ b/src/core/Configuration.js @@ -123,7 +123,30 @@ getJasmineRequireObj().Configuration = function(j$) { * @type Boolean * @default false */ - detectLateRejectionHandling: false + detectLateRejectionHandling: false, + + /** + * The number of extra stack frames inserted by a wrapper around {@link it} + * or by some other local modification. Jasmine uses this to determine the + * filename for {@link SpecStartedEvent} and {@link SpecDoneEvent}. + * @name Configuration#extraItStackFrames + * @since 5.13.0 + * @type number + * @default 0 + */ + extraItStackFrames: 0, + + /** + * The number of extra stack frames inserted by a wrapper around + * {@link describe} or by some other local modification. Jasmine uses this + * to determine the filename for {@link SpecStartedEvent} and + * {@link SpecDoneEvent}. + * @name Configuration#extraDescribeStackFrames + * @since 5.13.0 + * @type number + * @default 0 + */ + extraDescribeStackFrames: 0 }; Object.freeze(defaultConfig); @@ -179,6 +202,16 @@ getJasmineRequireObj().Configuration = function(j$) { if (changes.hasOwnProperty('verboseDeprecations')) { this.#values.verboseDeprecations = changes.verboseDeprecations; } + + // 0 is a valid value for both of these, so a truthiness check wouldn't work + if (typeof changes.extraItStackFrames !== 'undefined') { + this.#values.extraItStackFrames = changes.extraItStackFrames; + } + + if (typeof changes.extraDescribeStackFrames !== 'undefined') { + this.#values.extraDescribeStackFrames = + changes.extraDescribeStackFrames; + } } } diff --git a/src/core/Env.js b/src/core/Env.js index 701b4586..cb89128b 100644 --- a/src/core/Env.js +++ b/src/core/Env.js @@ -1,4 +1,6 @@ getJasmineRequireObj().Env = function(j$) { + const DEFAULT_IT_DESCRIBE_STACK_DEPTH = 3; + /** * @class Env * @since 2.0.0 @@ -593,14 +595,14 @@ getJasmineRequireObj().Env = function(j$) { this.describe = function(description, definitionFn) { ensureIsNotNested('describe'); - const filename = callerCallerFilename(); + const filename = indirectCallerFilename(describeStackDepth()); return suiteBuilder.describe(description, definitionFn, filename) .metadata; }; this.xdescribe = function(description, definitionFn) { ensureIsNotNested('xdescribe'); - const filename = callerCallerFilename(); + const filename = indirectCallerFilename(describeStackDepth()); return suiteBuilder.xdescribe(description, definitionFn, filename) .metadata; }; @@ -608,30 +610,38 @@ getJasmineRequireObj().Env = function(j$) { this.fdescribe = function(description, definitionFn) { ensureIsNotNested('fdescribe'); ensureNonParallel('fdescribe'); - const filename = callerCallerFilename(); + const filename = indirectCallerFilename(describeStackDepth()); return suiteBuilder.fdescribe(description, definitionFn, filename) .metadata; }; this.it = function(description, fn, timeout) { ensureIsNotNested('it'); - const filename = callerCallerFilename(); + const filename = indirectCallerFilename(itStackDepth()); return suiteBuilder.it(description, fn, timeout, filename).metadata; }; this.xit = function(description, fn, timeout) { ensureIsNotNested('xit'); - const filename = callerCallerFilename(); + const filename = indirectCallerFilename(itStackDepth()); return suiteBuilder.xit(description, fn, timeout, filename).metadata; }; this.fit = function(description, fn, timeout) { ensureIsNotNested('fit'); ensureNonParallel('fit'); - const filename = callerCallerFilename(); + const filename = indirectCallerFilename(itStackDepth()); return suiteBuilder.fit(description, fn, timeout, filename).metadata; }; + function itStackDepth() { + return DEFAULT_IT_DESCRIBE_STACK_DEPTH + config.extraItStackFrames; + } + + function describeStackDepth() { + return DEFAULT_IT_DESCRIBE_STACK_DEPTH + config.extraDescribeStackFrames; + } + /** * Get a user-defined property as part of the properties field of {@link SpecResult} * @name Env#getSpecProperty @@ -817,11 +827,12 @@ getJasmineRequireObj().Env = function(j$) { }; } - function callerCallerFilename() { + function indirectCallerFilename(depth) { const frames = new j$.StackTrace(new Error()).frames; - // frames[3] should always exist except in Jasmine's own tests, which bypass - // the global it/describe layer, but don't crash if it doesn't. - return frames[3] && frames[3].file; + // The specified frame should always exist except in Jasmine's own tests, + // which bypass the global it/describe layer, but could be absent in case + // of misconfiguration. Don't crash if it's absent. + return frames[depth] && frames[depth].file; } return Env; diff --git a/src/core/Spec.js b/src/core/Spec.js index 9d13d757..e97c919d 100644 --- a/src/core/Spec.js +++ b/src/core/Spec.js @@ -99,12 +99,11 @@ getJasmineRequireObj().Spec = function(j$) { * @property {String} description - The description passed to the {@link it} that created this spec. * @property {String} fullName - The full description including all ancestors of this spec. * @property {String|null} parentSuiteId - The ID of the suite containing this spec, or null if this spec is not in a describe(). - * @property {String} filename - Deprecated. The name of the file the spec was defined in. + * @property {String} filename - The name of the file the spec was defined in. * Note: The value may be incorrect if zone.js is installed or * `it`/`fit`/`xit` have been replaced with versions that don't maintain the - * same call stack height as the originals. This property may be removed in - * a future version unless there is enough user interest in keeping it. - * See {@link https://github.com/jasmine/jasmine/issues/2065}. + * same call stack height as the originals. You can fix that by setting + * {@link Configuration#extraItStackFrames}. * @property {ExpectationResult[]} failedExpectations - The list of expectations that failed during execution of this spec. * @property {ExpectationResult[]} passedExpectations - The list of expectations that passed during execution of this spec. * @property {ExpectationResult[]} deprecationWarnings - The list of deprecation warnings that occurred during execution this spec. @@ -278,7 +277,20 @@ getJasmineRequireObj().Spec = function(j$) { * @returns {Array.} * @since 5.7.0 */ - getPath: this.getPath.bind(this) + getPath: this.getPath.bind(this), + + /** + * The name of the file the spec was defined in. + * Note: The value may be incorrect if zone.js is installed or + * `it`/`fit`/`xit` have been replaced with versions that don't maintain the + * same call stack height as the originals. You can fix that by setting + * {@link Configuration#extraItStackFrames}. + * @name Spec#filename + * @readonly + * @type {string} + * @since 5.13.0 + */ + filename: this.filename }; } diff --git a/src/core/Suite.js b/src/core/Suite.js index e8cc044c..55423ddb 100644 --- a/src/core/Suite.js +++ b/src/core/Suite.js @@ -101,12 +101,11 @@ getJasmineRequireObj().Suite = function(j$) { * @property {String} description - The description text passed to the {@link describe} that made this suite. * @property {String} fullName - The full description including all ancestors of this suite. * @property {String|null} parentSuiteId - The ID of the suite containing this suite, or null if this is not in another describe(). - * @property {String} filename - Deprecated. The name of the file the suite was defined in. + * @property {String} filename - The name of the file the suite was defined in. * Note: The value may be incorrect if zone.js is installed or * `describe`/`fdescribe`/`xdescribe` have been replaced with versions that - * don't maintain the same call stack height as the originals. This property - * may be removed in a future version unless there is enough user interest - * in keeping it. See {@link https://github.com/jasmine/jasmine/issues/2065}. + * don't maintain the same call stack height as the originals. You can fix + * that by setting {@link Configuration#extraDescribeStackFrames}. * @property {ExpectationResult[]} failedExpectations - The list of expectations that failed in an {@link afterAll} for this suite. * @property {ExpectationResult[]} deprecationWarnings - The list of deprecation warnings that occurred on this suite. * @property {String} status - Once the suite has completed, this string represents the pass/fail status of this suite. @@ -313,6 +312,19 @@ getJasmineRequireObj().Suite = function(j$) { * @since 2.0.0 */ this.description = suite.description; + + /** + * The name of the file the suite was defined in. + * Note: The value may be incorrect if zone.js is installed or + * `describe`/`fdescribe`/`xdescribe` have been replaced with versions + * that don't maintain the same call stack height as the originals. You + * can fix that by setting {@link Configuration#extraItStackFrames}. + * @name Suite#filename + * @readonly + * @type {string} + * @since 5.13.0 + */ + this.filename = suite.filename; } /**