diff --git a/.circleci/config.yml b/.circleci/config.yml index e8dc4ac8..b827fca9 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,5 +1,5 @@ # Run tests against supported Node versions, and (except for pull requests) -# against supported browsers. +# against supported browsers that are available on Saucelabs. version: 2.1 @@ -89,7 +89,7 @@ jobs: export SAUCE_TUNNEL_NAME=$CIRCLE_WORKFLOW_JOB_ID scripts/start-sauce-connect set +o errexit - scripts/run-all-browsers + scripts/run-sauce-browsers exitcode=$? set -o errexit scripts/stop-sauce-connect diff --git a/.github/workflows/safari.yml b/.github/workflows/safari.yml new file mode 100644 index 00000000..d22ae3a6 --- /dev/null +++ b/.github/workflows/safari.yml @@ -0,0 +1,21 @@ +name: Test in latest available Safari + +on: + push: + pull_request: + +jobs: + build: + runs-on: macos-latest + + steps: + - name: Report Safari version + run: osascript -e 'get version of application "Safari"' + - uses: actions/checkout@v4 + - name: Use Node.js 22.x + uses: actions/setup-node@v4 + with: + node-version: 22.x + - run: npm install + - run: npm run build + - run: JASMINE_BROWSER=safari npm run ci diff --git a/README.md b/README.md index d809d61c..6d6d69a7 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Microsoft Edge) as well as Node. | Environment | Supported versions | |-------------------|----------------------------------| | Node | 20, 22, 24 | -| Safari | 16*, 17* | +| Safari | 16*, 17*, 26* | | Chrome | Evergreen | | Firefox | Evergreen, 102*, 115*, 128*, 140 | | Edge | Evergreen | diff --git a/RELEASE.md b/RELEASE.md index 632a75ee..28c81ee8 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -28,9 +28,18 @@ should also rev to that version. When ready to release - specs are all green and the stories are done: -1. Update the release notes in `release_notes` - use the Anchorman gem to generate the markdown file and edit accordingly. Include a list of supported environments. -1. Update the version in `package.json` -1. Run `npm run build`. +1. Update the release notes in `release_notes` - use the Anchorman gem to + generate the Markdown file and edit accordingly. Include a list of supported + environments. Get that information from these places: + * For Node, see .circleci/config.yml or the README. + * For Firefox ESR and Safari <=17, see scripts/run-sauce-browsers or the README. + * For evergreen browsers, trigger a Circle CI run and check the + [Saucelabs dashboard](https://app.saucelabs.com/dashboard/tests?ownerId=90a771d55857492da3bd5251a2d92457&ownerType=user&ownerName=jasmine-js&start=last7days) + once it's finished. + * For Safari >17, trigger the [Safari action](https://github.com/jasmine/jasmine/actions/workflows/safari.yml) + and get the version from the output. +2. Update the version in `package.json` +3. Run `npm run build`. ### Commit and push core changes diff --git a/lib/jasmine-core/jasmine.js b/lib/jasmine-core/jasmine.js index 6aa4b083..88a8b3e6 100644 --- a/lib/jasmine-core/jasmine.js +++ b/lib/jasmine-core/jasmine.js @@ -50,6 +50,7 @@ var getJasmineRequireObj = (function() { j$.private.util = jRequire.util(j$); j$.private.errors = jRequire.errors(); j$.private.formatErrorMsg = jRequire.formatErrorMsg(j$); + j$.private.AllOf = jRequire.AllOf(j$); j$.private.Any = jRequire.Any(j$); j$.private.Anything = jRequire.Anything(j$); j$.private.CallTracker = jRequire.CallTracker(j$); @@ -407,6 +408,19 @@ getJasmineRequireObj().base = function(j$, jasmineGlobal) { ); }; + /** + * Get an {@link AsymmetricEqualityTester} that will succeed if the actual + * value being compared matches every provided equality tester. + * @name asymmetricEqualityTesters.allOf + * @emittedName jasmine.allOf + * @since 5.13.0 + * @function + * @param {...*} arguments - The asymmetric equality checkers to compare. + */ + j$.allOf = function() { + return new j$.AllOf(...arguments); + }; + /** * Get an {@link AsymmetricEqualityTester} that will succeed if the actual * value being compared is an instance of the specified class/constructor. @@ -933,12 +947,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. @@ -1120,7 +1133,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 }; } @@ -1199,6 +1225,8 @@ getJasmineRequireObj().Order = function() { getJasmineRequireObj().Env = function(j$) { 'use strict'; + const DEFAULT_IT_DESCRIBE_STACK_DEPTH = 3; + /** * @class Env * @since 2.0.0 @@ -1789,14 +1817,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; }; @@ -1804,30 +1832,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 SpecDoneEvent} * @name Env#getSpecProperty @@ -2013,11 +2049,12 @@ getJasmineRequireObj().Env = function(j$) { }; } - function callerCallerFilename() { + function indirectCallerFilename(depth) { const frames = new j$.private.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; @@ -2155,6 +2192,34 @@ getJasmineRequireObj().JsApiReporter = function(j$) { return JsApiReporter; }; +getJasmineRequireObj().AllOf = function(j$) { + function AllOf() { + const expectedValues = Array.from(arguments); + if (expectedValues.length === 0) { + throw new TypeError( + 'jasmine.allOf() expects at least one argument to be passed.' + ); + } + this.expectedValues = expectedValues; + } + + AllOf.prototype.asymmetricMatch = function(other, matchersUtil) { + for (const expectedValue of this.expectedValues) { + if (!matchersUtil.equals(other, expectedValue)) { + return false; + } + } + + return true; + }; + + AllOf.prototype.jasmineToString = function(pp) { + return ''; + }; + + return AllOf; +}; + getJasmineRequireObj().Any = function(j$) { 'use strict'; @@ -3510,7 +3575,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); @@ -3561,6 +3649,16 @@ getJasmineRequireObj().Configuration = function(j$) { if (typeof changes.seed !== 'undefined') { this.#values.seed = changes.seed; } + + // 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; + } } } @@ -10968,12 +11066,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. @@ -11181,6 +11278,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; } /** @@ -11860,7 +11970,10 @@ getJasmineRequireObj().TreeRunner = function(j$) { _executeSpec(spec, specOverallDone) { const onStart = next => { this.#currentRunableTracker.setCurrentSpec(spec); - this.#runableResources.initForRunable(spec.id, spec.parentSuiteId); + this.#runableResources.initForRunable( + spec.id, + spec.parentSuiteId || this.#executionTree.topSuite.id + ); this.#reportDispatcher.specStarted(spec.startedEvent()).then(next); }; const resultCallback = (result, next) => { diff --git a/release_notes/5.12.1.md b/release_notes/5.12.1.md new file mode 100644 index 00000000..4dc84ced --- /dev/null +++ b/release_notes/5.12.1.md @@ -0,0 +1,29 @@ +Jasmine Core 5.12.1 Release Notes + +## Bug fixes + +* Fix custom matchers in top-level specs + * Merges [#2088](https://github.com/jasmine/jasmine/pull/2088) from @bonkevin + +## Supported environments + +This version has been tested in the following environments. + +| Environment | Supported versions | +|-------------|--------------------------------| +| Node | 18.20.5**, 20, 22, 24 | +| Safari | 16**, 17** | +| Chrome | 141* | +| Firefox | 102**, 115**, 128**, 140, 144* | +| Edge | 141* | + +\* Evergreen browser. Each version of Jasmine is tested against the latest +version available at release time.
+\** Supported on a best-effort basis. Support for these versions may be dropped +if it becomes impractical, and bugs affecting only these versions may not be +treated as release blockers. + + +------ + +_Release Notes generated with _[Anchorman](http://github.com/infews/anchorman)_ diff --git a/scripts/run-all-browsers b/scripts/run-sauce-browsers similarity index 80% rename from scripts/run-all-browsers rename to scripts/run-sauce-browsers index f28619a0..fc6abd7b 100755 --- a/scripts/run-all-browsers +++ b/scripts/run-sauce-browsers @@ -1,5 +1,10 @@ #!/bin/sh +# Run tests in supported browsers that are available on Saucelabs. +# Note: The latest Safari version is tested via GitHub Actions because Saucelabs +# only makes it available to paid enterprise accounts. See +# .github/workflows/safari.yml. + run_browser() { browser=$1 version=$2 diff --git a/spec/core/ConfigurationSpec.js b/spec/core/ConfigurationSpec.js index aec33af2..1fcee555 100644 --- a/spec/core/ConfigurationSpec.js +++ b/spec/core/ConfigurationSpec.js @@ -10,7 +10,13 @@ describe('Configuration', function() { 'detectLateRejectionHandling', 'verboseDeprecations' ]; - const allKeys = [...standardBooleanKeys, 'seed', 'specFilter']; + const allKeys = [ + ...standardBooleanKeys, + 'seed', + 'specFilter', + 'extraItStackFrames', + 'extraDescribeStackFrames' + ]; Object.freeze(standardBooleanKeys); Object.freeze(allKeys); @@ -28,6 +34,8 @@ describe('Configuration', function() { expect(subject.forbidDuplicateNames).toEqual(true); expect(subject.verboseDeprecations).toEqual(false); expect(subject.detectLateRejectionHandling).toEqual(false); + expect(subject.extraItStackFrames).toEqual(0); + expect(subject.extraDescribeStackFrames).toEqual(0); }); describe('copy()', function() { @@ -109,5 +117,25 @@ describe('Configuration', function() { subject.update({ seed: null }); expect(subject.seed).toBeNull(); }); + + it('sets extraItStackFrames when not undefined', function() { + const subject = new privateUnderTest.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 privateUnderTest.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 faffc2d7..8aa1954a 100644 --- a/spec/core/EnvSpec.js +++ b/spec/core/EnvSpec.js @@ -1,4 +1,3 @@ -// TODO: Fix these unit tests! describe('Env', function() { let env; beforeEach(function() { @@ -95,7 +94,7 @@ describe('Env', function() { }); }); - it('accepts its own current configureation', function() { + it('accepts its own current configuration', function() { env.configure(env.configuration()); }); @@ -198,6 +197,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 +322,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/core/asymmetric_equality/AllOfSpec.js b/spec/core/asymmetric_equality/AllOfSpec.js new file mode 100644 index 00000000..625d1fcd --- /dev/null +++ b/spec/core/asymmetric_equality/AllOfSpec.js @@ -0,0 +1,63 @@ +describe('AllOf', function() { + it('matches a single value', function() { + const matchersUtil = new privateUnderTest.MatchersUtil(); + const allOf = new privateUnderTest.AllOf('foo'); + + expect(allOf.asymmetricMatch('foo', matchersUtil)).toBeTrue(); + }); + + it('matches a single matcher', function() { + const matchersUtil = new privateUnderTest.MatchersUtil(); + const allOf = new privateUnderTest.AllOf( + new privateUnderTest.StringContaining('oo') + ); + + expect(allOf.asymmetricMatch('foo', matchersUtil)).toBeTrue(); + }); + + it('matches multiple matchers', function() { + const matchersUtil = new privateUnderTest.MatchersUtil(); + const allOf = new privateUnderTest.AllOf( + new privateUnderTest.StringContaining('o'), + new privateUnderTest.StringContaining('f') + ); + + expect(allOf.asymmetricMatch('foo', matchersUtil)).toBeTrue(); + }); + + it('does not match when value does not match', function() { + const matchersUtil = new privateUnderTest.MatchersUtil(); + const allOf = new privateUnderTest.AllOf('bar'); + + expect(allOf.asymmetricMatch('foo', matchersUtil)).toBeFalse(); + }); + + it('does not match when any matchers fail', function() { + const matchersUtil = new privateUnderTest.MatchersUtil(); + const allOf = new privateUnderTest.AllOf( + new privateUnderTest.StringContaining('o'), + new privateUnderTest.StringContaining('x') + ); + + expect(allOf.asymmetricMatch('foo', matchersUtil)).toBeFalse(); + }); + + it('jasmineToStrings itself', function() { + const matcher = new privateUnderTest.AllOf('o'); + const pp = jasmine.createSpy('pp').and.returnValue('sample'); + + expect(matcher.jasmineToString(pp)).toEqual(''); + expect(pp).toHaveBeenCalledWith(['o']); + }); + + describe('when called without an argument', function() { + it('tells the user to pass a constructor argument', function() { + expect(function() { + new privateUnderTest.AllOf(); + }).toThrowError( + TypeError, + 'jasmine.allOf() expects at least one argument to be passed.' + ); + }); + }); +}); diff --git a/spec/core/integration/EnvSpec.js b/spec/core/integration/EnvSpec.js index b57b8d00..2bb81ef0 100644 --- a/spec/core/integration/EnvSpec.js +++ b/spec/core/integration/EnvSpec.js @@ -2278,6 +2278,34 @@ describe('Env integration', function() { await env.execute(); }); + it('Custom matchers set in top-level beforeAll should be available to all specs and suites', async function() { + const matchers = { + toFoo: function() {} + }; + + env.beforeAll(function() { + env.addMatchers(matchers); + }); + + env.describe('suite - top-level', function() { + env.it('has access to the custom matcher', function() { + expect(env.expect().toFoo).toBeDefined(); + }); + + env.describe('suite - nested', function() { + env.it('has access to the custom matcher', function() { + expect(env.expect().toFoo).toBeDefined(); + }); + }); + }); + + env.it('spec - top-level - has access to the custom matcher', function() { + expect(env.expect().toFoo).toBeDefined(); + }); + + await env.execute(); + }); + it('throws an exception if you try to create a spy outside of a runnable', async function() { const obj = { fn: function() {} }; let exception; diff --git a/spec/core/jasmineNamespaceSpec.js b/spec/core/jasmineNamespaceSpec.js index 986b6a27..1e004bc1 100644 --- a/spec/core/jasmineNamespaceSpec.js +++ b/spec/core/jasmineNamespaceSpec.js @@ -30,6 +30,7 @@ describe('The jasmine namespace', function() { 'version', // Asymmetric equality testers + 'allOf', 'any', 'anything', 'arrayContaining', 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 42c62503..984eb138 100644 --- a/src/core/Configuration.js +++ b/src/core/Configuration.js @@ -128,7 +128,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 (typeof changes.seed !== 'undefined') { this.#values.seed = changes.seed; } + + // 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 5cfbeed4..c3a2e710 100644 --- a/src/core/Env.js +++ b/src/core/Env.js @@ -1,6 +1,8 @@ getJasmineRequireObj().Env = function(j$) { 'use strict'; + const DEFAULT_IT_DESCRIBE_STACK_DEPTH = 3; + /** * @class Env * @since 2.0.0 @@ -591,14 +593,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; }; @@ -606,30 +608,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 SpecDoneEvent} * @name Env#getSpecProperty @@ -815,11 +825,12 @@ getJasmineRequireObj().Env = function(j$) { }; } - function callerCallerFilename() { + function indirectCallerFilename(depth) { const frames = new j$.private.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 8e39f044..089995ce 100644 --- a/src/core/Spec.js +++ b/src/core/Spec.js @@ -156,12 +156,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. @@ -343,7 +342,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 0210e07e..63fe3fbc 100644 --- a/src/core/Suite.js +++ b/src/core/Suite.js @@ -149,12 +149,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. @@ -362,6 +361,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/src/core/TreeRunner.js b/src/core/TreeRunner.js index d30ab517..2bc878ba 100644 --- a/src/core/TreeRunner.js +++ b/src/core/TreeRunner.js @@ -49,7 +49,10 @@ getJasmineRequireObj().TreeRunner = function(j$) { _executeSpec(spec, specOverallDone) { const onStart = next => { this.#currentRunableTracker.setCurrentSpec(spec); - this.#runableResources.initForRunable(spec.id, spec.parentSuiteId); + this.#runableResources.initForRunable( + spec.id, + spec.parentSuiteId || this.#executionTree.topSuite.id + ); this.#reportDispatcher.specStarted(spec.startedEvent()).then(next); }; const resultCallback = (result, next) => { diff --git a/src/core/asymmetric_equality/AllOf.js b/src/core/asymmetric_equality/AllOf.js new file mode 100644 index 00000000..db67a947 --- /dev/null +++ b/src/core/asymmetric_equality/AllOf.js @@ -0,0 +1,27 @@ +getJasmineRequireObj().AllOf = function(j$) { + function AllOf() { + const expectedValues = Array.from(arguments); + if (expectedValues.length === 0) { + throw new TypeError( + 'jasmine.allOf() expects at least one argument to be passed.' + ); + } + this.expectedValues = expectedValues; + } + + AllOf.prototype.asymmetricMatch = function(other, matchersUtil) { + for (const expectedValue of this.expectedValues) { + if (!matchersUtil.equals(other, expectedValue)) { + return false; + } + } + + return true; + }; + + AllOf.prototype.jasmineToString = function(pp) { + return ''; + }; + + return AllOf; +}; diff --git a/src/core/base.js b/src/core/base.js index 0fbad676..b7cf8606 100644 --- a/src/core/base.js +++ b/src/core/base.js @@ -229,6 +229,19 @@ getJasmineRequireObj().base = function(j$, jasmineGlobal) { ); }; + /** + * Get an {@link AsymmetricEqualityTester} that will succeed if the actual + * value being compared matches every provided equality tester. + * @name asymmetricEqualityTesters.allOf + * @emittedName jasmine.allOf + * @since 5.13.0 + * @function + * @param {...*} arguments - The asymmetric equality checkers to compare. + */ + j$.allOf = function() { + return new j$.AllOf(...arguments); + }; + /** * Get an {@link AsymmetricEqualityTester} that will succeed if the actual * value being compared is an instance of the specified class/constructor. diff --git a/src/core/requireCore.js b/src/core/requireCore.js index 77d45121..0b26678d 100644 --- a/src/core/requireCore.js +++ b/src/core/requireCore.js @@ -26,6 +26,7 @@ var getJasmineRequireObj = (function() { j$.private.util = jRequire.util(j$); j$.private.errors = jRequire.errors(); j$.private.formatErrorMsg = jRequire.formatErrorMsg(j$); + j$.private.AllOf = jRequire.AllOf(j$); j$.private.Any = jRequire.Any(j$); j$.private.Anything = jRequire.Anything(j$); j$.private.CallTracker = jRequire.CallTracker(j$);