From 6ad8d20694fc4bd995b88e21addd2c35ccbc9bb0 Mon Sep 17 00:00:00 2001 From: Steve Gravrock Date: Wed, 15 Feb 2023 21:26:28 -0800 Subject: [PATCH 1/2] Report the path/url of the file that the spec/suite was defined in Fixes #1884 --- lib/jasmine-core/jasmine.js | 114 +++++++++++++++++-------------- spec/core/SpecSpec.js | 2 + spec/core/integration/EnvSpec.js | 93 +++++++++++++++++++++++++ src/core/Env.js | 25 +++++-- src/core/Spec.js | 50 ++++++-------- src/core/Suite.js | 3 + src/core/SuiteBuilder.js | 34 ++++----- 7 files changed, 216 insertions(+), 105 deletions(-) diff --git a/lib/jasmine-core/jasmine.js b/lib/jasmine-core/jasmine.js index 2bb7824d..8d4de616 100644 --- a/lib/jasmine-core/jasmine.js +++ b/lib/jasmine-core/jasmine.js @@ -1,5 +1,5 @@ /* -Copyright (c) 2008-2022 Pivotal Labs +Copyright (c) 2008-2023 Pivotal Labs Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the @@ -741,6 +741,7 @@ getJasmineRequireObj().Spec = function(j$) { this.asyncExpectationFactory = attrs.asyncExpectationFactory; this.resultCallback = attrs.resultCallback || function() {}; this.id = attrs.id; + this.filename = attrs.filename; this.description = attrs.description || ''; this.queueableFn = attrs.queueableFn; this.beforeAndAfterFns = @@ -774,35 +775,7 @@ getJasmineRequireObj().Spec = function(j$) { this.exclude(); } - /** - * @typedef SpecResult - * @property {String} id - The unique id of this spec. - * @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 {Expectation[]} failedExpectations - The list of expectations that failed during execution of this spec. - * @property {Expectation[]} passedExpectations - The list of expectations that passed during execution of this spec. - * @property {Expectation[]} deprecationWarnings - The list of deprecation warnings that occurred during execution this spec. - * @property {String} pendingReason - If the spec is {@link pending}, this will be the reason. - * @property {String} status - Once the spec has completed, this string represents the pass/fail status of this spec. - * @property {number} duration - The time in ms used by the spec execution, including any before/afterEach. - * @property {Object} properties - User-supplied properties, if any, that were set using {@link Env#setSpecProperty} - * @property {DebugLogEntry[]|null} debugLogs - Messages, if any, that were logged using {@link jasmine.debugLog} during a failing spec. - * @since 2.0.0 - */ - this.result = { - id: this.id, - description: this.description, - fullName: this.getFullName(), - failedExpectations: [], - passedExpectations: [], - deprecationWarnings: [], - pendingReason: '', - duration: null, - properties: null, - debugLogs: null - }; - - this.reportedDone = false; + this.reset(); } Spec.prototype.addExpectationResult = function(passed, data, isError) { @@ -912,14 +885,31 @@ getJasmineRequireObj().Spec = function(j$) { }; Spec.prototype.reset = function() { + /** + * @typedef SpecResult + * @property {String} id - The unique id of this spec. + * @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} filename - The name of the file the spec was defined in. + * @property {Expectation[]} failedExpectations - The list of expectations that failed during execution of this spec. + * @property {Expectation[]} passedExpectations - The list of expectations that passed during execution of this spec. + * @property {Expectation[]} deprecationWarnings - The list of deprecation warnings that occurred during execution this spec. + * @property {String} pendingReason - If the spec is {@link pending}, this will be the reason. + * @property {String} status - Once the spec has completed, this string represents the pass/fail status of this spec. + * @property {number} duration - The time in ms used by the spec execution, including any before/afterEach. + * @property {Object} properties - User-supplied properties, if any, that were set using {@link Env#setSpecProperty} + * @property {DebugLogEntry[]|null} debugLogs - Messages, if any, that were logged using {@link jasmine.debugLog} during a failing spec. + * @since 2.0.0 + */ this.result = { id: this.id, description: this.description, fullName: this.getFullName(), + filename: this.filename, failedExpectations: [], passedExpectations: [], deprecationWarnings: [], - pendingReason: this.excludeMessage, + pendingReason: this.excludeMessage || '', duration: null, properties: null, debugLogs: null @@ -1816,17 +1806,23 @@ getJasmineRequireObj().Env = function(j$) { this.describe = function(description, definitionFn) { ensureIsNotNested('describe'); - return suiteBuilder.describe(description, definitionFn).metadata; + const filename = callerCallerFilename(); + return suiteBuilder.describe(description, definitionFn, filename) + .metadata; }; this.xdescribe = function(description, definitionFn) { ensureIsNotNested('xdescribe'); - return suiteBuilder.xdescribe(description, definitionFn).metadata; + const filename = callerCallerFilename(); + return suiteBuilder.xdescribe(description, definitionFn, filename) + .metadata; }; this.fdescribe = function(description, definitionFn) { ensureIsNotNested('fdescribe'); - return suiteBuilder.fdescribe(description, definitionFn).metadata; + const filename = callerCallerFilename(); + return suiteBuilder.fdescribe(description, definitionFn, filename) + .metadata; }; function specResultCallback(spec, result, next) { @@ -1853,17 +1849,20 @@ getJasmineRequireObj().Env = function(j$) { this.it = function(description, fn, timeout) { ensureIsNotNested('it'); - return suiteBuilder.it(description, fn, timeout).metadata; + const filename = callerCallerFilename(); + return suiteBuilder.it(description, fn, timeout, filename).metadata; }; this.xit = function(description, fn, timeout) { ensureIsNotNested('xit'); - return suiteBuilder.xit(description, fn, timeout).metadata; + const filename = callerCallerFilename(); + return suiteBuilder.xit(description, fn, timeout, filename).metadata; }; this.fit = function(description, fn, timeout) { ensureIsNotNested('fit'); - return suiteBuilder.fit(description, fn, timeout).metadata; + const filename = callerCallerFilename(); + return suiteBuilder.fit(description, fn, timeout, filename).metadata; }; /** @@ -2003,6 +2002,10 @@ getJasmineRequireObj().Env = function(j$) { }; } + function callerCallerFilename() { + return new j$.StackTrace(new Error()).frames[3].file; + } + return Env; }; @@ -9530,6 +9533,7 @@ getJasmineRequireObj().Suite = function(j$) { this.id = attrs.id; this.parentSuite = attrs.parentSuite; this.description = attrs.description; + this.filename = attrs.filename; this.expectationFactory = attrs.expectationFactory; this.asyncExpectationFactory = attrs.asyncExpectationFactory; this.throwOnExpectationFailure = !!attrs.throwOnExpectationFailure; @@ -9635,6 +9639,7 @@ getJasmineRequireObj().Suite = function(j$) { * @property {String} id - The unique id of this suite. * @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} filename - The name of the file the suite was defined in. * @property {Expectation[]} failedExpectations - The list of expectations that failed in an {@link afterAll} for this suite. * @property {Expectation[]} 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. @@ -9646,6 +9651,7 @@ getJasmineRequireObj().Suite = function(j$) { id: this.id, description: this.description, fullName: this.getFullName(), + filename: this.filename, failedExpectations: [], deprecationWarnings: [], duration: null, @@ -9873,9 +9879,9 @@ getJasmineRequireObj().SuiteBuilder = function(j$) { this.focusedRunables = []; } - describe(description, definitionFn) { + describe(description, definitionFn, filename) { ensureIsFunction(definitionFn, 'describe'); - const suite = this.suiteFactory_(description); + const suite = this.suiteFactory_(description, filename); if (definitionFn.length > 0) { throw new Error('describe does not expect any arguments'); } @@ -9886,9 +9892,9 @@ getJasmineRequireObj().SuiteBuilder = function(j$) { return suite; } - fdescribe(description, definitionFn) { + fdescribe(description, definitionFn, filename) { ensureIsFunction(definitionFn, 'fdescribe'); - const suite = this.suiteFactory_(description); + const suite = this.suiteFactory_(description, filename); suite.isFocused = true; this.focusedRunables.push(suite.id); @@ -9898,37 +9904,37 @@ getJasmineRequireObj().SuiteBuilder = function(j$) { return suite; } - xdescribe(description, definitionFn) { + xdescribe(description, definitionFn, filename) { ensureIsFunction(definitionFn, 'xdescribe'); - const suite = this.suiteFactory_(description); + const suite = this.suiteFactory_(description, filename); suite.exclude(); this.addSpecsToSuite_(suite, definitionFn); return suite; } - it(description, fn, timeout) { + it(description, fn, timeout, filename) { // it() sometimes doesn't have a fn argument, so only check the type if // it's given. if (arguments.length > 1 && typeof fn !== 'undefined') { ensureIsFunctionOrAsync(fn, 'it'); } - return this.it_(description, fn, timeout); + return this.it_(description, fn, timeout, filename); } - xit(description, fn, timeout) { + xit(description, fn, timeout, filename) { // xit(), like it(), doesn't always have a fn argument, so only check the // type when needed. if (arguments.length > 1 && typeof fn !== 'undefined') { ensureIsFunctionOrAsync(fn, 'xit'); } - const spec = this.it_(description, fn, timeout); + const spec = this.it_(description, fn, timeout, filename); spec.exclude('Temporarily disabled with xit'); return spec; } - fit(description, fn, timeout) { + fit(description, fn, timeout, filename) { // Unlike it and xit, the function is required because it doesn't make // sense to focus on nothing. ensureIsFunctionOrAsync(fn, 'fit'); @@ -9936,7 +9942,7 @@ getJasmineRequireObj().SuiteBuilder = function(j$) { if (timeout) { j$.util.validateTimeout(timeout); } - const spec = this.specFactory_(description, fn, timeout); + const spec = this.specFactory_(description, fn, timeout, filename); this.currentDeclarationSuite_.addChild(spec); this.focusedRunables.push(spec.id); this.unfocusAncestor_(); @@ -9996,12 +10002,12 @@ getJasmineRequireObj().SuiteBuilder = function(j$) { }); } - it_(description, fn, timeout) { + it_(description, fn, timeout, filename) { if (timeout) { j$.util.validateTimeout(timeout); } - const spec = this.specFactory_(description, fn, timeout); + const spec = this.specFactory_(description, fn, timeout, filename); if (this.currentDeclarationSuite_.markedExcluding) { spec.exclude(); } @@ -10010,11 +10016,12 @@ getJasmineRequireObj().SuiteBuilder = function(j$) { return spec; } - suiteFactory_(description) { + suiteFactory_(description, filename) { const config = this.env_.configuration(); return new j$.Suite({ id: 'suite' + this.nextSuiteId_++, description, + filename, parentSuite: this.currentDeclarationSuite_, timer: new j$.Timer(), expectationFactory: this.expectationFactory_, @@ -10047,12 +10054,13 @@ getJasmineRequireObj().SuiteBuilder = function(j$) { this.currentDeclarationSuite_ = parentSuite; } - specFactory_(description, fn, timeout) { + specFactory_(description, fn, timeout, filename) { this.totalSpecsDefined++; const config = this.env_.configuration(); const suite = this.currentDeclarationSuite_; const spec = new j$.Spec({ id: 'spec' + this.nextSpecId_++, + filename, beforeAndAfterFns: beforeAndAfterFns(suite), expectationFactory: this.expectationFactory_, asyncExpectationFactory: this.specAsyncExpectationFactory_, diff --git a/spec/core/SpecSpec.js b/spec/core/SpecSpec.js index 33bea8b3..40e84424 100644 --- a/spec/core/SpecSpec.js +++ b/spec/core/SpecSpec.js @@ -195,6 +195,7 @@ describe('Spec', function() { onStart: startCallback, resultCallback: resultCallback, description: 'with a spec', + filename: 'someSpecFile.js', getSpecName: function() { return 'a suite with a spec'; }, @@ -219,6 +220,7 @@ describe('Spec', function() { status: 'pending', description: 'with a spec', fullName: 'a suite with a spec', + filename: 'someSpecFile.js', failedExpectations: [], passedExpectations: [], deprecationWarnings: [], diff --git a/spec/core/integration/EnvSpec.js b/spec/core/integration/EnvSpec.js index 8f2990ad..4923a9bd 100644 --- a/spec/core/integration/EnvSpec.js +++ b/spec/core/integration/EnvSpec.js @@ -4004,6 +4004,99 @@ describe('Env integration', function() { ); }); + it('reports suite and spec filenames', async function() { + const methods = ['suiteStarted', 'suiteDone', 'specStarted', 'specDone']; + const reporter = jasmine.createSpyObj('reporter', methods); + env.addReporter(reporter); + + // Simulate calling through global it and describe, + // which add another stack frame vs calling env methods directly + function describeShim(name, fn) { + env.describe(name, fn); + } + function itShim(name, fn) { + env.it(name, fn); + } + + describeShim('a suite', function() { + itShim('a spec', function() {}); + }); + + await env.execute(); + + for (const method of methods) { + expect(reporter[method]) + .withContext(method) + .toHaveBeenCalledWith( + jasmine.objectContaining({ + filename: jasmine.stringMatching(/EnvSpec\.js$/) + }) + ); + } + }); + + it('reports skipped suite and spec filenames', async function() { + const methods = ['suiteStarted', 'suiteDone', 'specStarted', 'specDone']; + const reporter = jasmine.createSpyObj('reporter', methods); + env.addReporter(reporter); + + // Simulate calling through global it and describe, + // which add another stack frame vs calling env methods directly + function xdescribeShim(name, fn) { + env.xdescribe(name, fn); + } + function xitShim(name, fn) { + env.xit(name, fn); + } + + xdescribeShim('a suite', function() { + xitShim('a spec', function() {}); + }); + + await env.execute(); + + for (const method of methods) { + expect(reporter[method]) + .withContext(method) + .toHaveBeenCalledWith( + jasmine.objectContaining({ + filename: jasmine.stringMatching(/EnvSpec\.js$/) + }) + ); + } + }); + + it('reports focused suite and spec filenames', async function() { + const methods = ['suiteStarted', 'suiteDone', 'specStarted', 'specDone']; + const reporter = jasmine.createSpyObj('reporter', methods); + env.addReporter(reporter); + + // Simulate calling through global it and describe, + // which add another stack frame vs calling env methods directly + function fdescribeShim(name, fn) { + env.fdescribe(name, fn); + } + function fitShim(name, fn) { + env.fit(name, fn); + } + + fdescribeShim('a suite', function() { + fitShim('a spec', function() {}); + }); + + await env.execute(); + + for (const method of methods) { + expect(reporter[method]) + .withContext(method) + .toHaveBeenCalledWith( + jasmine.objectContaining({ + filename: jasmine.stringMatching(/EnvSpec\.js$/) + }) + ); + } + }); + function browserEventMethods() { return { addEventListener() {}, diff --git a/src/core/Env.js b/src/core/Env.js index b6fabb6d..115dcd5f 100644 --- a/src/core/Env.js +++ b/src/core/Env.js @@ -674,17 +674,23 @@ getJasmineRequireObj().Env = function(j$) { this.describe = function(description, definitionFn) { ensureIsNotNested('describe'); - return suiteBuilder.describe(description, definitionFn).metadata; + const filename = callerCallerFilename(); + return suiteBuilder.describe(description, definitionFn, filename) + .metadata; }; this.xdescribe = function(description, definitionFn) { ensureIsNotNested('xdescribe'); - return suiteBuilder.xdescribe(description, definitionFn).metadata; + const filename = callerCallerFilename(); + return suiteBuilder.xdescribe(description, definitionFn, filename) + .metadata; }; this.fdescribe = function(description, definitionFn) { ensureIsNotNested('fdescribe'); - return suiteBuilder.fdescribe(description, definitionFn).metadata; + const filename = callerCallerFilename(); + return suiteBuilder.fdescribe(description, definitionFn, filename) + .metadata; }; function specResultCallback(spec, result, next) { @@ -711,17 +717,20 @@ getJasmineRequireObj().Env = function(j$) { this.it = function(description, fn, timeout) { ensureIsNotNested('it'); - return suiteBuilder.it(description, fn, timeout).metadata; + const filename = callerCallerFilename(); + return suiteBuilder.it(description, fn, timeout, filename).metadata; }; this.xit = function(description, fn, timeout) { ensureIsNotNested('xit'); - return suiteBuilder.xit(description, fn, timeout).metadata; + const filename = callerCallerFilename(); + return suiteBuilder.xit(description, fn, timeout, filename).metadata; }; this.fit = function(description, fn, timeout) { ensureIsNotNested('fit'); - return suiteBuilder.fit(description, fn, timeout).metadata; + const filename = callerCallerFilename(); + return suiteBuilder.fit(description, fn, timeout, filename).metadata; }; /** @@ -861,5 +870,9 @@ getJasmineRequireObj().Env = function(j$) { }; } + function callerCallerFilename() { + return new j$.StackTrace(new Error()).frames[3].file; + } + return Env; }; diff --git a/src/core/Spec.js b/src/core/Spec.js index f4149622..81544c46 100644 --- a/src/core/Spec.js +++ b/src/core/Spec.js @@ -4,6 +4,7 @@ getJasmineRequireObj().Spec = function(j$) { this.asyncExpectationFactory = attrs.asyncExpectationFactory; this.resultCallback = attrs.resultCallback || function() {}; this.id = attrs.id; + this.filename = attrs.filename; this.description = attrs.description || ''; this.queueableFn = attrs.queueableFn; this.beforeAndAfterFns = @@ -37,35 +38,7 @@ getJasmineRequireObj().Spec = function(j$) { this.exclude(); } - /** - * @typedef SpecResult - * @property {String} id - The unique id of this spec. - * @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 {Expectation[]} failedExpectations - The list of expectations that failed during execution of this spec. - * @property {Expectation[]} passedExpectations - The list of expectations that passed during execution of this spec. - * @property {Expectation[]} deprecationWarnings - The list of deprecation warnings that occurred during execution this spec. - * @property {String} pendingReason - If the spec is {@link pending}, this will be the reason. - * @property {String} status - Once the spec has completed, this string represents the pass/fail status of this spec. - * @property {number} duration - The time in ms used by the spec execution, including any before/afterEach. - * @property {Object} properties - User-supplied properties, if any, that were set using {@link Env#setSpecProperty} - * @property {DebugLogEntry[]|null} debugLogs - Messages, if any, that were logged using {@link jasmine.debugLog} during a failing spec. - * @since 2.0.0 - */ - this.result = { - id: this.id, - description: this.description, - fullName: this.getFullName(), - failedExpectations: [], - passedExpectations: [], - deprecationWarnings: [], - pendingReason: '', - duration: null, - properties: null, - debugLogs: null - }; - - this.reportedDone = false; + this.reset(); } Spec.prototype.addExpectationResult = function(passed, data, isError) { @@ -175,14 +148,31 @@ getJasmineRequireObj().Spec = function(j$) { }; Spec.prototype.reset = function() { + /** + * @typedef SpecResult + * @property {String} id - The unique id of this spec. + * @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} filename - The name of the file the spec was defined in. + * @property {Expectation[]} failedExpectations - The list of expectations that failed during execution of this spec. + * @property {Expectation[]} passedExpectations - The list of expectations that passed during execution of this spec. + * @property {Expectation[]} deprecationWarnings - The list of deprecation warnings that occurred during execution this spec. + * @property {String} pendingReason - If the spec is {@link pending}, this will be the reason. + * @property {String} status - Once the spec has completed, this string represents the pass/fail status of this spec. + * @property {number} duration - The time in ms used by the spec execution, including any before/afterEach. + * @property {Object} properties - User-supplied properties, if any, that were set using {@link Env#setSpecProperty} + * @property {DebugLogEntry[]|null} debugLogs - Messages, if any, that were logged using {@link jasmine.debugLog} during a failing spec. + * @since 2.0.0 + */ this.result = { id: this.id, description: this.description, fullName: this.getFullName(), + filename: this.filename, failedExpectations: [], passedExpectations: [], deprecationWarnings: [], - pendingReason: this.excludeMessage, + pendingReason: this.excludeMessage || '', duration: null, properties: null, debugLogs: null diff --git a/src/core/Suite.js b/src/core/Suite.js index 449ff861..5a1a38a7 100644 --- a/src/core/Suite.js +++ b/src/core/Suite.js @@ -4,6 +4,7 @@ getJasmineRequireObj().Suite = function(j$) { this.id = attrs.id; this.parentSuite = attrs.parentSuite; this.description = attrs.description; + this.filename = attrs.filename; this.expectationFactory = attrs.expectationFactory; this.asyncExpectationFactory = attrs.asyncExpectationFactory; this.throwOnExpectationFailure = !!attrs.throwOnExpectationFailure; @@ -109,6 +110,7 @@ getJasmineRequireObj().Suite = function(j$) { * @property {String} id - The unique id of this suite. * @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} filename - The name of the file the suite was defined in. * @property {Expectation[]} failedExpectations - The list of expectations that failed in an {@link afterAll} for this suite. * @property {Expectation[]} 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. @@ -120,6 +122,7 @@ getJasmineRequireObj().Suite = function(j$) { id: this.id, description: this.description, fullName: this.getFullName(), + filename: this.filename, failedExpectations: [], deprecationWarnings: [], duration: null, diff --git a/src/core/SuiteBuilder.js b/src/core/SuiteBuilder.js index c29b2f40..50aabeb3 100644 --- a/src/core/SuiteBuilder.js +++ b/src/core/SuiteBuilder.js @@ -22,9 +22,9 @@ getJasmineRequireObj().SuiteBuilder = function(j$) { this.focusedRunables = []; } - describe(description, definitionFn) { + describe(description, definitionFn, filename) { ensureIsFunction(definitionFn, 'describe'); - const suite = this.suiteFactory_(description); + const suite = this.suiteFactory_(description, filename); if (definitionFn.length > 0) { throw new Error('describe does not expect any arguments'); } @@ -35,9 +35,9 @@ getJasmineRequireObj().SuiteBuilder = function(j$) { return suite; } - fdescribe(description, definitionFn) { + fdescribe(description, definitionFn, filename) { ensureIsFunction(definitionFn, 'fdescribe'); - const suite = this.suiteFactory_(description); + const suite = this.suiteFactory_(description, filename); suite.isFocused = true; this.focusedRunables.push(suite.id); @@ -47,37 +47,37 @@ getJasmineRequireObj().SuiteBuilder = function(j$) { return suite; } - xdescribe(description, definitionFn) { + xdescribe(description, definitionFn, filename) { ensureIsFunction(definitionFn, 'xdescribe'); - const suite = this.suiteFactory_(description); + const suite = this.suiteFactory_(description, filename); suite.exclude(); this.addSpecsToSuite_(suite, definitionFn); return suite; } - it(description, fn, timeout) { + it(description, fn, timeout, filename) { // it() sometimes doesn't have a fn argument, so only check the type if // it's given. if (arguments.length > 1 && typeof fn !== 'undefined') { ensureIsFunctionOrAsync(fn, 'it'); } - return this.it_(description, fn, timeout); + return this.it_(description, fn, timeout, filename); } - xit(description, fn, timeout) { + xit(description, fn, timeout, filename) { // xit(), like it(), doesn't always have a fn argument, so only check the // type when needed. if (arguments.length > 1 && typeof fn !== 'undefined') { ensureIsFunctionOrAsync(fn, 'xit'); } - const spec = this.it_(description, fn, timeout); + const spec = this.it_(description, fn, timeout, filename); spec.exclude('Temporarily disabled with xit'); return spec; } - fit(description, fn, timeout) { + fit(description, fn, timeout, filename) { // Unlike it and xit, the function is required because it doesn't make // sense to focus on nothing. ensureIsFunctionOrAsync(fn, 'fit'); @@ -85,7 +85,7 @@ getJasmineRequireObj().SuiteBuilder = function(j$) { if (timeout) { j$.util.validateTimeout(timeout); } - const spec = this.specFactory_(description, fn, timeout); + const spec = this.specFactory_(description, fn, timeout, filename); this.currentDeclarationSuite_.addChild(spec); this.focusedRunables.push(spec.id); this.unfocusAncestor_(); @@ -145,12 +145,12 @@ getJasmineRequireObj().SuiteBuilder = function(j$) { }); } - it_(description, fn, timeout) { + it_(description, fn, timeout, filename) { if (timeout) { j$.util.validateTimeout(timeout); } - const spec = this.specFactory_(description, fn, timeout); + const spec = this.specFactory_(description, fn, timeout, filename); if (this.currentDeclarationSuite_.markedExcluding) { spec.exclude(); } @@ -159,11 +159,12 @@ getJasmineRequireObj().SuiteBuilder = function(j$) { return spec; } - suiteFactory_(description) { + suiteFactory_(description, filename) { const config = this.env_.configuration(); return new j$.Suite({ id: 'suite' + this.nextSuiteId_++, description, + filename, parentSuite: this.currentDeclarationSuite_, timer: new j$.Timer(), expectationFactory: this.expectationFactory_, @@ -196,12 +197,13 @@ getJasmineRequireObj().SuiteBuilder = function(j$) { this.currentDeclarationSuite_ = parentSuite; } - specFactory_(description, fn, timeout) { + specFactory_(description, fn, timeout, filename) { this.totalSpecsDefined++; const config = this.env_.configuration(); const suite = this.currentDeclarationSuite_; const spec = new j$.Spec({ id: 'spec' + this.nextSpecId_++, + filename, beforeAndAfterFns: beforeAndAfterFns(suite), expectationFactory: this.expectationFactory_, asyncExpectationFactory: this.specAsyncExpectationFactory_, From 166e5f4d6c5018cba21fa68ff4bff329dab4b7e4 Mon Sep 17 00:00:00 2001 From: Steve Gravrock Date: Sat, 25 Feb 2023 10:24:14 -0800 Subject: [PATCH 2/2] Report the ID of each suite/spec's parent This is intended to support parallel execution, which is planned for a future release of Jasmine. Because the execution of unrelated suites will interleave when run in parallel, reporters will not be able to assume that the most recent `suiteStarted` event identifies the parent of the current suite/spec. By adding this feature now, we allow reporters to support both parallel execution and at least some 4.x versions without having to implement two different ways of finding the parent suite. --- lib/jasmine-core/jasmine-html.js | 2 +- lib/jasmine-core/jasmine.js | 14 ++- spec/core/SpecSpec.js | 2 + spec/core/integration/EnvSpec.js | 185 +++++++++++++++++++++++++++++-- src/core/Spec.js | 3 + src/core/Suite.js | 3 + src/core/SuiteBuilder.js | 8 +- 7 files changed, 202 insertions(+), 15 deletions(-) diff --git a/lib/jasmine-core/jasmine-html.js b/lib/jasmine-core/jasmine-html.js index 2ebc6d0e..47035410 100644 --- a/lib/jasmine-core/jasmine-html.js +++ b/lib/jasmine-core/jasmine-html.js @@ -1,5 +1,5 @@ /* -Copyright (c) 2008-2022 Pivotal Labs +Copyright (c) 2008-2023 Pivotal Labs Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/lib/jasmine-core/jasmine.js b/lib/jasmine-core/jasmine.js index 8d4de616..3e77af64 100644 --- a/lib/jasmine-core/jasmine.js +++ b/lib/jasmine-core/jasmine.js @@ -742,6 +742,7 @@ getJasmineRequireObj().Spec = function(j$) { this.resultCallback = attrs.resultCallback || function() {}; this.id = attrs.id; this.filename = attrs.filename; + this.parentSuiteId = attrs.parentSuiteId; this.description = attrs.description || ''; this.queueableFn = attrs.queueableFn; this.beforeAndAfterFns = @@ -890,6 +891,7 @@ getJasmineRequireObj().Spec = function(j$) { * @property {String} id - The unique id of this spec. * @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 - The name of the file the spec was defined in. * @property {Expectation[]} failedExpectations - The list of expectations that failed during execution of this spec. * @property {Expectation[]} passedExpectations - The list of expectations that passed during execution of this spec. @@ -905,6 +907,7 @@ getJasmineRequireObj().Spec = function(j$) { id: this.id, description: this.description, fullName: this.getFullName(), + parentSuiteId: this.parentSuiteId, filename: this.filename, failedExpectations: [], passedExpectations: [], @@ -9533,6 +9536,7 @@ getJasmineRequireObj().Suite = function(j$) { this.id = attrs.id; this.parentSuite = attrs.parentSuite; this.description = attrs.description; + this.reportedParentSuiteId = attrs.reportedParentSuiteId; this.filename = attrs.filename; this.expectationFactory = attrs.expectationFactory; this.asyncExpectationFactory = attrs.asyncExpectationFactory; @@ -9639,6 +9643,7 @@ getJasmineRequireObj().Suite = function(j$) { * @property {String} id - The unique id of this suite. * @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 - The name of the file the suite was defined in. * @property {Expectation[]} failedExpectations - The list of expectations that failed in an {@link afterAll} for this suite. * @property {Expectation[]} deprecationWarnings - The list of deprecation warnings that occurred on this suite. @@ -9651,6 +9656,7 @@ getJasmineRequireObj().Suite = function(j$) { id: this.id, description: this.description, fullName: this.getFullName(), + parentSuiteId: this.reportedParentSuiteId, filename: this.filename, failedExpectations: [], deprecationWarnings: [], @@ -10018,11 +10024,15 @@ getJasmineRequireObj().SuiteBuilder = function(j$) { suiteFactory_(description, filename) { const config = this.env_.configuration(); + const parentSuite = this.currentDeclarationSuite_; + const reportedParentSuiteId = + parentSuite === this.topSuite ? null : parentSuite.id; return new j$.Suite({ id: 'suite' + this.nextSuiteId_++, description, filename, - parentSuite: this.currentDeclarationSuite_, + parentSuite, + reportedParentSuiteId, timer: new j$.Timer(), expectationFactory: this.expectationFactory_, asyncExpectationFactory: this.suiteAsyncExpectationFactory_, @@ -10058,9 +10068,11 @@ getJasmineRequireObj().SuiteBuilder = function(j$) { this.totalSpecsDefined++; const config = this.env_.configuration(); const suite = this.currentDeclarationSuite_; + const parentSuiteId = suite === this.topSuite ? null : suite.id; const spec = new j$.Spec({ id: 'spec' + this.nextSpecId_++, filename, + parentSuiteId, beforeAndAfterFns: beforeAndAfterFns(suite), expectationFactory: this.expectationFactory_, asyncExpectationFactory: this.specAsyncExpectationFactory_, diff --git a/spec/core/SpecSpec.js b/spec/core/SpecSpec.js index 40e84424..599760ed 100644 --- a/spec/core/SpecSpec.js +++ b/spec/core/SpecSpec.js @@ -195,6 +195,7 @@ describe('Spec', function() { onStart: startCallback, resultCallback: resultCallback, description: 'with a spec', + parentSuiteId: 'suite1', filename: 'someSpecFile.js', getSpecName: function() { return 'a suite with a spec'; @@ -220,6 +221,7 @@ describe('Spec', function() { status: 'pending', description: 'with a spec', fullName: 'a suite with a spec', + parentSuiteId: 'suite1', filename: 'someSpecFile.js', failedExpectations: [], passedExpectations: [], diff --git a/spec/core/integration/EnvSpec.js b/spec/core/integration/EnvSpec.js index 4923a9bd..dbcaff2d 100644 --- a/spec/core/integration/EnvSpec.js +++ b/spec/core/integration/EnvSpec.js @@ -1933,11 +1933,17 @@ describe('Env integration', function() { 'specStarted', 'specDone' ]); + const suiteFullNameToId = {}; + reporter.suiteStarted.and.callFake(function(e) { + suiteFullNameToId[e.fullName] = e.id; + }); env.addReporter(reporter); + env.it('a top level spec', function() {}); + env.describe('A Suite', function() { - env.it('with a top level spec', function() { + env.it('with a spec', function() { env.expect(true).toBe(true); }); env.describe('with a nested suite', function() { @@ -1960,37 +1966,109 @@ describe('Env integration', function() { await env.execute(); expect(reporter.jasmineStarted).toHaveBeenCalledWith({ - totalSpecsDefined: 5, + totalSpecsDefined: 6, order: jasmine.any(jasmineUnderTest.Order) }); - expect(reporter.specDone.calls.count()).toBe(5); + expect(reporter.specStarted.calls.count()).toBe(6); + expect(reporter.specDone.calls.count()).toBe(6); - expect(reporter.specDone).toHaveBeenCalledWith( + expect(reporter.specStarted).toHaveBeenCalledWith( jasmine.objectContaining({ - description: 'with a top level spec', - status: 'passed' + description: 'a top level spec', + parentSuiteId: null }) ); - expect(reporter.specDone).toHaveBeenCalledWith( jasmine.objectContaining({ - description: "with an x'ed spec", - status: 'pending' + description: 'a top level spec', + status: 'passed', + parentSuiteId: null + }) + ); + expect(reporter.specStarted).toHaveBeenCalledWith( + jasmine.objectContaining({ + description: 'with a spec', + parentSuiteId: suiteFullNameToId['A Suite'] }) ); - expect(reporter.specDone).toHaveBeenCalledWith( jasmine.objectContaining({ description: 'with a spec', - status: 'failed' + status: 'passed', + parentSuiteId: suiteFullNameToId['A Suite'] }) ); + expect(reporter.specStarted).toHaveBeenCalledWith( + jasmine.objectContaining({ + description: "with an x'ed spec", + parentSuiteId: suiteFullNameToId['A Suite with a nested suite'] + }) + ); + expect(reporter.specDone).toHaveBeenCalledWith( + jasmine.objectContaining({ + description: "with an x'ed spec", + status: 'pending', + parentSuiteId: suiteFullNameToId['A Suite with a nested suite'] + }) + ); + + expect(reporter.specStarted).toHaveBeenCalledWith( + jasmine.objectContaining({ + description: 'with a spec', + parentSuiteId: suiteFullNameToId['A Suite with a nested suite'] + }) + ); + expect(reporter.specDone).toHaveBeenCalledWith( + jasmine.objectContaining({ + description: 'with a spec', + status: 'failed', + parentSuiteId: suiteFullNameToId['A Suite with a nested suite'] + }) + ); + + expect(reporter.specStarted).toHaveBeenCalledWith( + jasmine.objectContaining({ + description: 'is pending', + parentSuiteId: + suiteFullNameToId['A Suite with only non-executable specs'] + }) + ); expect(reporter.specDone).toHaveBeenCalledWith( jasmine.objectContaining({ description: 'is pending', - status: 'pending' + status: 'pending', + parentSuiteId: + suiteFullNameToId['A Suite with only non-executable specs'] + }) + ); + + expect(reporter.suiteStarted).toHaveBeenCalledWith( + jasmine.objectContaining({ + description: 'A Suite', + parentSuiteId: null + }) + ); + expect(reporter.suiteDone).toHaveBeenCalledWith( + jasmine.objectContaining({ + description: 'A Suite', + status: 'passed', + parentSuiteId: null + }) + ); + + expect(reporter.suiteStarted).toHaveBeenCalledWith( + jasmine.objectContaining({ + description: 'with a nested suite', + parentSuiteId: suiteFullNameToId['A Suite'] + }) + ); + expect(reporter.suiteDone).toHaveBeenCalledWith( + jasmine.objectContaining({ + description: 'with a nested suite', + status: 'passed', + parentSuiteId: suiteFullNameToId['A Suite'] }) ); @@ -2001,6 +2079,89 @@ describe('Env integration', function() { expect(suiteResult.description).toEqual('A Suite'); }); + it('reports focused specs and suites as expected', async function() { + const reporter = jasmine.createSpyObj('fakeReporter', [ + 'suiteStarted', + 'suiteDone', + 'specStarted', + 'specDone' + ]); + const suiteFullNameToId = {}; + reporter.suiteStarted.and.callFake(function(e) { + suiteFullNameToId[e.fullName] = e.id; + }); + + env.fit('a focused top level spec', function() {}); + + env.describe('a suite', function() { + env.fdescribe('a focused suite', function() { + env.fit('a focused spec', function() {}); + }); + }); + + env.addReporter(reporter); + await env.execute(); + + expect(reporter.specStarted).toHaveBeenCalledTimes(2); + expect(reporter.specDone).toHaveBeenCalledTimes(2); + + expect(reporter.specStarted).toHaveBeenCalledWith( + jasmine.objectContaining({ + description: 'a focused top level spec', + parentSuiteId: null + }) + ); + expect(reporter.specDone).toHaveBeenCalledWith( + jasmine.objectContaining({ + description: 'a focused top level spec', + status: 'passed', + parentSuiteId: null + }) + ); + + expect(reporter.specStarted).toHaveBeenCalledWith( + jasmine.objectContaining({ + description: 'a focused spec', + parentSuiteId: suiteFullNameToId['a suite a focused suite'] + }) + ); + expect(reporter.specDone).toHaveBeenCalledWith( + jasmine.objectContaining({ + description: 'a focused spec', + status: 'passed', + parentSuiteId: suiteFullNameToId['a suite a focused suite'] + }) + ); + + expect(reporter.suiteStarted).toHaveBeenCalledWith( + jasmine.objectContaining({ + description: 'a suite', + parentSuiteId: null + }) + ); + expect(reporter.suiteDone).toHaveBeenCalledWith( + jasmine.objectContaining({ + description: 'a suite', + status: 'passed', + parentSuiteId: null + }) + ); + + expect(reporter.suiteStarted).toHaveBeenCalledWith( + jasmine.objectContaining({ + description: 'a focused suite', + parentSuiteId: suiteFullNameToId['a suite'] + }) + ); + expect(reporter.suiteDone).toHaveBeenCalledWith( + jasmine.objectContaining({ + description: 'a focused suite', + status: 'passed', + parentSuiteId: suiteFullNameToId['a suite'] + }) + ); + }); + it('should report the random seed at the beginning and end of execution', async function() { const reporter = jasmine.createSpyObj('fakeReporter', [ 'jasmineStarted', diff --git a/src/core/Spec.js b/src/core/Spec.js index 81544c46..52a24a5d 100644 --- a/src/core/Spec.js +++ b/src/core/Spec.js @@ -5,6 +5,7 @@ getJasmineRequireObj().Spec = function(j$) { this.resultCallback = attrs.resultCallback || function() {}; this.id = attrs.id; this.filename = attrs.filename; + this.parentSuiteId = attrs.parentSuiteId; this.description = attrs.description || ''; this.queueableFn = attrs.queueableFn; this.beforeAndAfterFns = @@ -153,6 +154,7 @@ getJasmineRequireObj().Spec = function(j$) { * @property {String} id - The unique id of this spec. * @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 - The name of the file the spec was defined in. * @property {Expectation[]} failedExpectations - The list of expectations that failed during execution of this spec. * @property {Expectation[]} passedExpectations - The list of expectations that passed during execution of this spec. @@ -168,6 +170,7 @@ getJasmineRequireObj().Spec = function(j$) { id: this.id, description: this.description, fullName: this.getFullName(), + parentSuiteId: this.parentSuiteId, filename: this.filename, failedExpectations: [], passedExpectations: [], diff --git a/src/core/Suite.js b/src/core/Suite.js index 5a1a38a7..55a5227f 100644 --- a/src/core/Suite.js +++ b/src/core/Suite.js @@ -4,6 +4,7 @@ getJasmineRequireObj().Suite = function(j$) { this.id = attrs.id; this.parentSuite = attrs.parentSuite; this.description = attrs.description; + this.reportedParentSuiteId = attrs.reportedParentSuiteId; this.filename = attrs.filename; this.expectationFactory = attrs.expectationFactory; this.asyncExpectationFactory = attrs.asyncExpectationFactory; @@ -110,6 +111,7 @@ getJasmineRequireObj().Suite = function(j$) { * @property {String} id - The unique id of this suite. * @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 - The name of the file the suite was defined in. * @property {Expectation[]} failedExpectations - The list of expectations that failed in an {@link afterAll} for this suite. * @property {Expectation[]} deprecationWarnings - The list of deprecation warnings that occurred on this suite. @@ -122,6 +124,7 @@ getJasmineRequireObj().Suite = function(j$) { id: this.id, description: this.description, fullName: this.getFullName(), + parentSuiteId: this.reportedParentSuiteId, filename: this.filename, failedExpectations: [], deprecationWarnings: [], diff --git a/src/core/SuiteBuilder.js b/src/core/SuiteBuilder.js index 50aabeb3..e02955b4 100644 --- a/src/core/SuiteBuilder.js +++ b/src/core/SuiteBuilder.js @@ -161,11 +161,15 @@ getJasmineRequireObj().SuiteBuilder = function(j$) { suiteFactory_(description, filename) { const config = this.env_.configuration(); + const parentSuite = this.currentDeclarationSuite_; + const reportedParentSuiteId = + parentSuite === this.topSuite ? null : parentSuite.id; return new j$.Suite({ id: 'suite' + this.nextSuiteId_++, description, filename, - parentSuite: this.currentDeclarationSuite_, + parentSuite, + reportedParentSuiteId, timer: new j$.Timer(), expectationFactory: this.expectationFactory_, asyncExpectationFactory: this.suiteAsyncExpectationFactory_, @@ -201,9 +205,11 @@ getJasmineRequireObj().SuiteBuilder = function(j$) { this.totalSpecsDefined++; const config = this.env_.configuration(); const suite = this.currentDeclarationSuite_; + const parentSuiteId = suite === this.topSuite ? null : suite.id; const spec = new j$.Spec({ id: 'spec' + this.nextSpecId_++, filename, + parentSuiteId, beforeAndAfterFns: beforeAndAfterFns(suite), expectationFactory: this.expectationFactory_, asyncExpectationFactory: this.specAsyncExpectationFactory_,