diff --git a/.circleci/config.yml b/.circleci/config.yml index fe0b2b31..fce54861 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 @@ -93,7 +93,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 d61e6b74..64f297e6 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Microsoft Edge) as well as Node. | Environment | Supported versions | |-------------------|----------------------------------| | Node | 18.20.5+*, 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 b9d9d06b..2899dac7 100644 --- a/lib/jasmine-core/jasmine.js +++ b/lib/jasmine-core/jasmine.js @@ -59,6 +59,7 @@ var getJasmineRequireObj = (function(jasmineGlobal) { j$.util = jRequire.util(j$); j$.errors = jRequire.errors(); j$.formatErrorMsg = jRequire.formatErrorMsg(); + j$.AllOf = jRequire.AllOf(j$); j$.Any = jRequire.Any(j$); j$.Anything = jRequire.Anything(j$); j$.CallTracker = jRequire.CallTracker(j$); @@ -417,6 +418,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. @@ -873,12 +887,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. @@ -1052,7 +1065,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 }; } @@ -1127,6 +1153,8 @@ getJasmineRequireObj().Order = function() { }; getJasmineRequireObj().Env = function(j$) { + const DEFAULT_IT_DESCRIBE_STACK_DEPTH = 3; + /** * @class Env * @since 2.0.0 @@ -1722,14 +1750,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; }; @@ -1737,30 +1765,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 @@ -1946,11 +1982,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; @@ -2086,6 +2123,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$) { function Any(expectedObject) { if (typeof expectedObject === 'undefined') { @@ -3391,7 +3456,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); @@ -3447,6 +3535,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; + } } } @@ -10666,12 +10764,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. @@ -10878,6 +10975,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; } /** @@ -11559,7 +11669,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.result).then(next); }; const resultCallback = (result, next) => { @@ -11833,5 +11946,5 @@ getJasmineRequireObj().UserContext = function(j$) { }; getJasmineRequireObj().version = function() { - return '5.12.0'; + return '5.12.1'; }; diff --git a/package.json b/package.json index 649cd867..905d1526 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "jasmine-core", "license": "MIT", - "version": "5.12.0", + "version": "5.12.1", "repository": { "type": "git", "url": "https://github.com/jasmine/jasmine.git" 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/release_notes/6.0.0-alpha.1.md b/release_notes/6.0.0-alpha.1.md new file mode 100644 index 00000000..c1ceab40 --- /dev/null +++ b/release_notes/6.0.0-alpha.1.md @@ -0,0 +1,116 @@ +# Jasmine Core 6.0.0-alpha.1 Release Notes + +This is a pre-release, intended to offer a preview of breaking changes and to +solicit feedback. + +## A Note About Pre-Release Compatibility + +There may be additional breaking changes in future 6.0 pre-releases or in the +final 6.0 release. That's allowed by the semver specification, but users are +sometimes unpleasantly surprised by it. + +NPM's implementation of carat version ranges assumes that subsequent +pre-releases and final releases are fully compatible with earlier pre-releases. +If your package.json contains `"jasmine-core": "^6.0.0-alpha.1`, +NPM might install any later 6.x version even though there is no guarantee of +compatibility. If that isn't ok, you should specify an exact pre-release version: +`"jasmine-core": "6.0.0-alpha.1`. + + +## Breaking changes + +### Changes that affect reporters + +* Irrelevant properties such as `status` and `failedExpectations` are omitted + from [the event passed to suiteStarted](https://jasmine.github.io/api/6.0.0-alpha.1/global.html#SuiteStartedEvent). + + This change should be compatible with most existing reporters but could break + reporters that manage their internal state in unusual ways. Please + [open an issue](https://github.com/jasmine/jasmine/issues/new?template=bug_report.yml) + if you find a published reporter package that works with jasmine-core 5.x but + not with this release. + +### Changes that affect browser boot files + +* The `createElement` and `createTextNode` options of `HtmlReporter` are ignored. + `HtmlReporter` now unconditionally uses `document.createElement` and + `document.createTextNode`. + +### Changes that affect spec writing + +* HTML reporters cache configuration throughout each run. Configuration changes + made while specs are running will not affect reporter behavior. +* Global error spies always receive a single argument. Previously, the browser + error event was passed as the second argument. + +## New features + +* A new `HtmlReporterV2` with several improvements over the old `HtmlReporter`: + * Clicking a spec/suite link does exact filtering rather than a substring + match. + * The old dots are replaced with a progress bar. This improves usability with + large suites and fixes an accessibility problem. + * Details of failed specs are displayed as soon as each spec finishes. + * Initialization and wire-up in boot files are much simpler. + + If you're using jasmine-browser-runner or copying boot1.js from the standalone + distribution, you'll automatically get the new reporter. If you maintain your + own boot files, you'll get the old reporter unless you update your boot1.js + to match the one that's in this package. + + The new reporter produces `spec` query string parameters that are different + from those created by the old reporter. If you use non-Jasmine software that + interprets the `spec` parameter, such as karma-jasmine, you may not be able to + adopt `HtmlReporterV2` unlesss it's updated. +* Use `globalThis` to determine the global object during initialization + This makes jasmine-core more tolerant of buggy bundlers or loaders that + cause `this` to be undefined in the global context. + + +## Deprecations + +* Warn if jasmine-core is loaded as an ES module in a browser. + This is an untested and unsupported configuration that has been known to cause + problems in the past. +* Deprecated `HtmlReporter` and `HtmlSpecFilter` in favor of `HtmlReporterV2`. + + +## Documentation improvements + +* Improved API reference documentation for APIs that are used from browser boot + files. + + +## Internal improvements + +* Removed remaining code that supported suite re-entry. +* Encapsulated suite and spec result and status management. +* Adopted strict mode throughout the codebase. +* Decomposed `HtmlReporter` into components and converted to ES6 classes. +* Made global error handling more uniform between browsers and Node. + + +## Supported environments + +This version has been tested in the following environments. + +| Environment | Supported versions | +|-------------------|--------------------------------| +| Node | 20, 22, 24 | +| Safari | 16**, 17** | +| Chrome | 141* | +| Firefox | 102**, 115**, 128**, 140, 143* | +| 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 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..5075817a 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..6442ede0 --- /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 jasmineUnderTest.MatchersUtil(); + const allOf = new jasmineUnderTest.AllOf('foo'); + + expect(allOf.asymmetricMatch('foo', matchersUtil)).toBeTrue(); + }); + + it('matches a single matcher', function() { + const matchersUtil = new jasmineUnderTest.MatchersUtil(); + const allOf = new jasmineUnderTest.AllOf( + new jasmineUnderTest.StringContaining('oo') + ); + + expect(allOf.asymmetricMatch('foo', matchersUtil)).toBeTrue(); + }); + + it('matches multiple matchers', function() { + const matchersUtil = new jasmineUnderTest.MatchersUtil(); + const allOf = new jasmineUnderTest.AllOf( + new jasmineUnderTest.StringContaining('o'), + new jasmineUnderTest.StringContaining('f') + ); + + expect(allOf.asymmetricMatch('foo', matchersUtil)).toBeTrue(); + }); + + it('does not match when value does not match', function() { + const matchersUtil = new jasmineUnderTest.MatchersUtil(); + const allOf = new jasmineUnderTest.AllOf('bar'); + + expect(allOf.asymmetricMatch('foo', matchersUtil)).toBeFalse(); + }); + + it('does not match when any matchers fail', function() { + const matchersUtil = new jasmineUnderTest.MatchersUtil(); + const allOf = new jasmineUnderTest.AllOf( + new jasmineUnderTest.StringContaining('o'), + new jasmineUnderTest.StringContaining('x') + ); + + expect(allOf.asymmetricMatch('foo', matchersUtil)).toBeFalse(); + }); + + it('jasmineToStrings itself', function() { + const matcher = new jasmineUnderTest.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 jasmineUnderTest.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 f1b8504d..15bfd196 100644 --- a/spec/core/integration/EnvSpec.js +++ b/spec/core/integration/EnvSpec.js @@ -2309,6 +2309,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/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 3e4fb741..2f8f61b3 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 @@ -594,14 +596,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; }; @@ -609,30 +611,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 @@ -818,11 +828,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; } /** diff --git a/src/core/TreeRunner.js b/src/core/TreeRunner.js index 8b7c5703..581dc0c5 100644 --- a/src/core/TreeRunner.js +++ b/src/core/TreeRunner.js @@ -47,7 +47,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.result).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 95ea4bba..608e83d7 100644 --- a/src/core/base.js +++ b/src/core/base.js @@ -221,6 +221,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 9ce8aed3..7d8f1078 100644 --- a/src/core/requireCore.js +++ b/src/core/requireCore.js @@ -35,6 +35,7 @@ var getJasmineRequireObj = (function(jasmineGlobal) { j$.util = jRequire.util(j$); j$.errors = jRequire.errors(); j$.formatErrorMsg = jRequire.formatErrorMsg(); + j$.AllOf = jRequire.AllOf(j$); j$.Any = jRequire.Any(j$); j$.Anything = jRequire.Anything(j$); j$.CallTracker = jRequire.CallTracker(j$);