Files
jasmine/src/core/Env.js
Steve Gravrock f12f4395f0 Redesigned moudule system
* Top level private APIs (e.g. jasmine.private.whatever) are no longer
  exposed
* jasmineRequire is no longer exposed
* core is self-booting
* Globals are automatically created in browsers. (They can subsequently
  be removed by user code if desired.)
* Globals are *not* automatically created in Node. An installGlobals
  function is exported instead. The jasmine package calls installGlobals
  unless configured not to do so.
* In Node, the same instance is returned each time jasmine-core is
  imported. A reset function is exported. It effectively resets all state
  by discarding the env and creating a new one. This allows mulitple
  sequential runs within the same process to be independent of each
  other, but does not allow multiple concurrent runs. (That probably never
  worked anyway.)

Fixes #2094
2026-02-15 20:16:45 -08:00

850 lines
26 KiB
JavaScript

getJasmineRequireObj().Env = function(j$, private$) {
'use strict';
const DEFAULT_IT_DESCRIBE_STACK_DEPTH = 3;
/**
* @class Env
* @since 2.0.0
* @classdesc The Jasmine environment.<br>
* _Note:_ Do not construct this directly. You can obtain the Env instance by
* calling {@link jasmine.getEnv}.
* @hideconstructor
*/
function Env(envOptions) {
envOptions = envOptions || {};
const self = this;
const GlobalErrors = envOptions.GlobalErrors || private$.GlobalErrors;
const global = envOptions.global || j$.getGlobal();
const realSetTimeout = global.setTimeout;
const realClearTimeout = global.clearTimeout;
const stackClearer = private$.getStackClearer(global);
this.clock = new private$.Clock(
global,
function() {
return new private$.DelayedFunctionScheduler();
},
new private$.MockDate(global)
);
const globalErrors = new GlobalErrors(
global,
// Configuration is late-bound because GlobalErrors needs to be constructed
// before it's set to detect load-time errors in browsers
() => this.configuration()
);
const { installGlobalErrors, uninstallGlobalErrors } = (function() {
let installed = false;
return {
installGlobalErrors() {
if (!installed) {
globalErrors.install();
installed = true;
}
},
uninstallGlobalErrors() {
if (installed) {
globalErrors.uninstall();
installed = false;
}
}
};
})();
const runableResources = new private$.RunableResources({
getCurrentRunableId: function() {
const r = runner.currentRunable();
return r ? r.id : null;
},
globalErrors
});
let reportDispatcher;
let topSuite;
let runner;
let parallelLoadingState = null; // 'specs', 'helpers', or null for non-parallel
const config = new private$.Configuration();
if (!envOptions.suppressLoadErrors) {
installGlobalErrors();
globalErrors.pushListener(function loadtimeErrorHandler(error) {
topSuite.addExpectationResult(false, {
passed: false,
globalErrorType: 'load',
message: error.message,
stack: error.stack,
filename: error.filename,
lineno: error.lineno
});
});
}
/**
* Configure your jasmine environment
* @name Env#configure
* @since 3.3.0
* @argument {Configuration} configuration
* @function
*/
this.configure = function(changes) {
if (parallelLoadingState) {
throw new Error(
'Jasmine cannot be configured via Env in parallel mode'
);
}
config.update(changes);
deprecator.verboseDeprecations(config.verboseDeprecations);
stackClearer.setSafariYieldStrategy(config.safariYieldStrategy);
};
/**
* Get the current configuration for your jasmine environment
* @name Env#configuration
* @since 3.3.0
* @function
* @returns {Configuration}
*/
this.configuration = function() {
return config.copy();
};
this.setDefaultSpyStrategy = function(defaultStrategyFn) {
runableResources.setDefaultSpyStrategy(defaultStrategyFn);
};
this.addSpyStrategy = function(name, fn) {
runableResources.customSpyStrategies()[name] = fn;
};
this.addCustomEqualityTester = function(tester) {
runableResources.customEqualityTesters().push(tester);
};
this.addMatchers = function(matchersToAdd) {
runableResources.addCustomMatchers(matchersToAdd);
};
this.addAsyncMatchers = function(matchersToAdd) {
runableResources.addCustomAsyncMatchers(matchersToAdd);
};
this.addCustomObjectFormatter = function(formatter) {
runableResources.customObjectFormatters().push(formatter);
};
private$.Expectation.addCoreMatchers(private$.matchers);
private$.Expectation.addAsyncCoreMatchers(private$.asyncMatchers);
const expectationFactory = function(actual, spec) {
return private$.Expectation.factory({
matchersUtil: runableResources.makeMatchersUtil(),
customMatchers: runableResources.customMatchers(),
actual: actual,
addExpectationResult: addExpectationResult
});
function addExpectationResult(passed, result) {
return spec.addExpectationResult(passed, result);
}
};
const handleThrowUnlessFailure = function(passed, result) {
if (!passed) {
/**
* @interface
* @name ThrowUnlessFailure
* @extends Error
* @description Represents a failure of an expectation evaluated with
* {@link throwUnless}. Properties of this error are a subset of the
* properties of {@link ExpectationResult} and have the same values.
*
* @property {String} matcherName - The name of the matcher that was executed for this expectation.
* @property {String} message - The failure message for the expectation.
*/
const error = new Error(result.message);
error.message = result.message;
error.matcherName = result.matcherName;
throw error;
}
};
const throwUnlessFactory = function(actual, spec) {
return private$.Expectation.factory({
matchersUtil: runableResources.makeMatchersUtil(),
customMatchers: runableResources.customMatchers(),
actual: actual,
addExpectationResult: handleThrowUnlessFailure
});
};
const throwUnlessAsyncFactory = function(actual, spec) {
return private$.Expectation.asyncFactory({
matchersUtil: runableResources.makeMatchersUtil(),
customAsyncMatchers: runableResources.customAsyncMatchers(),
actual: actual,
addExpectationResult: handleThrowUnlessFailure
});
};
// TODO: Unify recordLateError with recordLateExpectation? The extra
// diagnostic info added by the latter is probably useful in most cases.
function recordLateError(error) {
const isExpectationResult =
error.matcherName !== undefined && error.passed !== undefined;
const result = isExpectationResult
? error
: private$.buildExpectationResult({
error,
passed: false,
matcherName: '',
expected: '',
actual: ''
});
routeLateFailure(result);
}
function recordLateExpectation(runable, runableType, result) {
const delayedExpectationResult = {};
Object.keys(result).forEach(function(k) {
delayedExpectationResult[k] = result[k];
});
delayedExpectationResult.passed = false;
delayedExpectationResult.globalErrorType = 'lateExpectation';
delayedExpectationResult.message =
runableType +
' "' +
runable.getFullName() +
'" ran a "' +
result.matcherName +
'" expectation after it finished.\n';
if (result.message) {
delayedExpectationResult.message +=
'Message: "' + result.message + '"\n';
}
delayedExpectationResult.message +=
'1. Did you forget to return or await the result of expectAsync?\n' +
'2. Was done() invoked before an async operation completed?\n' +
'3. Did an expectation follow a call to done()?';
topSuite.addExpectationResult(false, delayedExpectationResult);
}
function routeLateFailure(expectationResult) {
// Report the result on the nearest ancestor suite that hasn't already
// been reported done.
for (let r = runner.currentRunable(); r; r = r.parentSuite) {
if (!r.reportedDone) {
if (r === topSuite) {
expectationResult.globalErrorType = 'lateError';
}
r.addExpectationResult(false, expectationResult);
return;
}
}
// If we get here, all results have been reported and there's nothing we
// can do except log the result and hope the user sees it.
// eslint-disable-next-line no-console
console.error('Jasmine received a result after the suite finished:');
// eslint-disable-next-line no-console
console.error(expectationResult);
}
const asyncExpectationFactory = function(actual, spec, runableType) {
return private$.Expectation.asyncFactory({
matchersUtil: runableResources.makeMatchersUtil(),
customAsyncMatchers: runableResources.customAsyncMatchers(),
actual: actual,
addExpectationResult: addExpectationResult
});
function addExpectationResult(passed, result) {
if (runner.currentRunable() !== spec) {
recordLateExpectation(spec, runableType, result);
}
return spec.addExpectationResult(passed, result);
}
};
/**
* Causes a deprecation warning to be logged to the console and reported to
* reporters.
*
* The optional second parameter is an object that can have either of the
* following properties:
*
* omitStackTrace: Whether to omit the stack trace. Optional. Defaults to
* false. This option is ignored if the deprecation is an Error. Set this
* when the stack trace will not contain anything that helps the user find
* the source of the deprecation.
*
* ignoreRunnable: Whether to log the deprecation on the root suite, ignoring
* the spec or suite that's running when it happens. Optional. Defaults to
* false.
*
* @name Env#deprecated
* @since 2.99
* @function
* @param {String|Error} deprecation The deprecation message
* @param {Object} [options] Optional extra options, as described above
*/
Object.defineProperty(this, 'deprecated', {
enumerable: true,
value: function(deprecation, options) {
const runable = runner.currentRunable() || topSuite;
deprecator.addDeprecationWarning(runable, deprecation, options);
}
});
function runQueue(options) {
options.clearStack = options.clearStack || stackClearer;
options.timeout = {
setTimeout: realSetTimeout,
clearTimeout: realClearTimeout
};
options.fail = self.fail;
options.globalErrors = globalErrors;
options.onException =
options.onException ||
function(e) {
(runner.currentRunable() || topSuite).handleException(e);
};
new private$.QueueRunner(options).execute();
}
const suiteBuilder = new private$.SuiteBuilder({
env: this,
expectationFactory,
asyncExpectationFactory,
onLateError: recordLateError,
runQueue
});
topSuite = suiteBuilder.topSuite;
const deprecator =
envOptions?.deprecator ?? new private$.Deprecator(topSuite);
/**
* Provides the root suite, through which all suites and specs can be
* accessed.
* @function
* @name Env#topSuite
* @return {Suite} the root suite
* @since 2.0.0
*/
this.topSuite = function() {
ensureNonParallel('topSuite');
return topSuite.metadata;
};
/**
* This represents the available reporter callback for an object passed to {@link Env#addReporter}.
* @interface Reporter
* @see custom_reporter
*/
reportDispatcher = new private$.ReportDispatcher(
private$.reporterEvents,
function(options) {
options.SkipPolicy = private$.NeverSkipPolicy;
return runQueue(options);
},
recordLateError
);
runner = new private$.Runner({
topSuite,
totalSpecsDefined: () => suiteBuilder.totalSpecsDefined,
focusedRunables: () => suiteBuilder.focusedRunables,
runableResources,
reportDispatcher,
runQueue,
TreeProcessor: private$.TreeProcessor,
globalErrors,
getConfig: () => config
});
this.setParallelLoadingState = function(state) {
parallelLoadingState = state;
};
this.parallelReset = function() {
suiteBuilder.parallelReset();
runner.parallelReset();
};
/**
* Executes the specs.
*
* If called with no parameter or with a falsy parameter,
* all specs will be executed except those that are excluded by a
* [spec filter]{@link Configuration#specFilter} or other mechanism. If the
* parameter is a list of spec/suite IDs, only those specs/suites will
* be run.
*
* execute should not be called more than once unless the env has been
* configured with `{autoCleanClosures: false}`.
*
* execute returns a promise. The promise will be resolved to the same
* {@link JasmineDoneInfo|overall result} that's passed to a reporter's
* `jasmineDone` method, even if the suite did not pass. To determine
* whether the suite passed, check the value that the promise resolves to
* or use a {@link Reporter}. The promise will be rejected in the case of
* certain serious errors that prevent execution from starting.
*
* @name Env#execute
* @since 2.0.0
* @function
* @async
* @param {(string[])=} runablesToRun IDs of suites and/or specs to run
* @return {Promise<JasmineDoneInfo>}
*/
this.execute = async function(runablesToRun) {
installGlobalErrors();
// Karma incorrectly loads jasmine-core as an ES module. It isn't one,
// and we don't test that configuration. Warn about it.
if (private$.loadedAsBrowserEsm) {
this.deprecated(
"jasmine-core isn't an ES module but it was loaded as one. This is not a supported configuration."
);
}
if (parallelLoadingState) {
validateConfigForParallel();
}
const result = await runner.execute(runablesToRun);
this.cleanup_();
return result;
};
/**
* Add a custom reporter to the Jasmine environment.
* @name Env#addReporter
* @since 2.0.0
* @function
* @param {Reporter} reporterToAdd The reporter to be added.
* @see custom_reporter
*/
this.addReporter = function(reporterToAdd) {
if (parallelLoadingState) {
throw new Error('Reporters cannot be added via Env in parallel mode');
}
reportDispatcher.addReporter(reporterToAdd);
};
/**
* Provide a fallback reporter if no other reporters have been specified.
* @name Env#provideFallbackReporter
* @since 2.5.0
* @function
* @param {Reporter} reporterToAdd The reporter
* @see custom_reporter
*/
this.provideFallbackReporter = function(reporterToAdd) {
reportDispatcher.provideFallbackReporter(reporterToAdd);
};
/**
* Clear all registered reporters
* @name Env#clearReporters
* @since 2.5.2
* @function
*/
this.clearReporters = function() {
if (parallelLoadingState) {
throw new Error('Reporters cannot be removed via Env in parallel mode');
}
reportDispatcher.clearReporters();
};
/**
* Configures whether Jasmine should allow the same function to be spied on
* more than once during the execution of a spec. By default, spying on
* a function that is already a spy will cause an error.
* @name Env#allowRespy
* @function
* @since 2.5.0
* @param {boolean} allow Whether to allow respying
*/
this.allowRespy = function(allow) {
runableResources.spyRegistry.allowRespy(allow);
};
this.spyOn = function() {
return runableResources.spyRegistry.spyOn.apply(
runableResources.spyRegistry,
arguments
);
};
this.spyOnProperty = function() {
return runableResources.spyRegistry.spyOnProperty.apply(
runableResources.spyRegistry,
arguments
);
};
this.spyOnAllFunctions = function() {
return runableResources.spyRegistry.spyOnAllFunctions.apply(
runableResources.spyRegistry,
arguments
);
};
this.createSpy = function(name, originalFn) {
return runableResources.spyFactory.createSpy(name, originalFn);
};
this.createSpyObj = function(baseName, methodNames, propertyNames) {
return runableResources.spyFactory.createSpyObj(
baseName,
methodNames,
propertyNames
);
};
this.spyOnGlobalErrorsAsync = async function(fn) {
const spy = this.createSpy('global error handler');
const associatedRunable = runner.currentRunable();
let cleanedUp = false;
globalErrors.setOverrideListener(spy, () => {
if (!cleanedUp) {
const message =
'Global error spy was not uninstalled. (Did you ' +
'forget to await the return value of spyOnGlobalErrorsAsync?)';
associatedRunable.addExpectationResult(false, {
matcherName: '',
passed: false,
expected: '',
actual: '',
message,
error: null
});
}
cleanedUp = true;
});
try {
const maybePromise = fn(spy);
if (!private$.isPromiseLike(maybePromise)) {
throw new Error(
'The callback to spyOnGlobalErrorsAsync must be an async or promise-returning function'
);
}
await maybePromise;
} finally {
if (!cleanedUp) {
cleanedUp = true;
globalErrors.removeOverrideListener();
}
}
};
function ensureIsNotNested(method) {
const runable = runner.currentRunable();
if (runable !== null && runable !== undefined) {
throw new Error(
"'" + method + "' should only be used in 'describe' function"
);
}
}
function ensureNonParallel(method) {
if (parallelLoadingState) {
throw new Error(`'${method}' is not available in parallel mode`);
}
}
function ensureNonParallelOrInDescribe(msg) {
if (parallelLoadingState && !suiteBuilder.inDescribe()) {
throw new Error(msg);
}
}
function ensureNonParallelOrInHelperOrInDescribe(method) {
if (parallelLoadingState === 'specs' && !suiteBuilder.inDescribe()) {
throw new Error(
'In parallel mode, ' +
method +
' must be in a describe block or in a helper file'
);
}
}
function validateConfigForParallel() {
if (!config.random) {
throw new Error('Randomization cannot be disabled in parallel mode');
}
if (config.seed !== null && config.seed !== undefined) {
throw new Error('Random seed cannot be set in parallel mode');
}
}
this.describe = function(description, definitionFn) {
ensureIsNotNested('describe');
const filename = indirectCallerFilename(describeStackDepth());
return suiteBuilder.describe(description, definitionFn, filename)
.metadata;
};
this.xdescribe = function(description, definitionFn) {
ensureIsNotNested('xdescribe');
const filename = indirectCallerFilename(describeStackDepth());
return suiteBuilder.xdescribe(description, definitionFn, filename)
.metadata;
};
this.fdescribe = function(description, definitionFn) {
ensureIsNotNested('fdescribe');
ensureNonParallel('fdescribe');
const filename = indirectCallerFilename(describeStackDepth());
return suiteBuilder.fdescribe(description, definitionFn, filename)
.metadata;
};
this.it = function(description, fn, timeout) {
ensureIsNotNested('it');
const filename = indirectCallerFilename(itStackDepth());
return suiteBuilder.it(description, fn, timeout, filename).metadata;
};
this.xit = function(description, fn, timeout) {
ensureIsNotNested('xit');
const filename = indirectCallerFilename(itStackDepth());
return suiteBuilder.xit(description, fn, timeout, filename).metadata;
};
this.fit = function(description, fn, timeout) {
ensureIsNotNested('fit');
ensureNonParallel('fit');
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
* @since 5.10.0
* @function
* @param {String} key The name of the property
* @returns {*} The value of the property
*/
this.getSpecProperty = function(key) {
if (
!runner.currentRunable() ||
runner.currentRunable() == runner.currentSuite()
) {
throw new Error(
"'getSpecProperty' was used when there was no current spec"
);
}
return runner.currentRunable().getSpecProperty(key);
};
/**
* Sets a user-defined property that will be provided to reporters as part of the properties field of {@link SpecResult}
* @name Env#setSpecProperty
* @since 3.6.0
* @function
* @param {String} key The name of the property
* @param {*} value The value of the property
*/
this.setSpecProperty = function(key, value) {
if (
!runner.currentRunable() ||
runner.currentRunable() == runner.currentSuite()
) {
throw new Error(
"'setSpecProperty' was used when there was no current spec"
);
}
runner.currentRunable().setSpecProperty(key, value);
};
/**
* Sets a user-defined property that will be provided to reporters as part of the properties field of {@link SuiteResult}
* @name Env#setSuiteProperty
* @since 3.6.0
* @function
* @param {String} key The name of the property
* @param {*} value The value of the property
*/
this.setSuiteProperty = function(key, value) {
if (!runner.currentSuite()) {
throw new Error(
"'setSuiteProperty' was used when there was no current suite"
);
}
runner.currentSuite().setSuiteProperty(key, value);
};
this.debugLog = function(msg) {
const maybeSpec = runner.currentRunable();
if (!maybeSpec || !maybeSpec.debugLog) {
throw new Error("'debugLog' was called when there was no current spec");
}
maybeSpec.debugLog(msg);
};
this.expect = function(actual) {
const runable = runner.currentRunable();
if (!runable) {
throw new Error(
"'expect' was used when there was no current spec, this could be because an asynchronous test timed out"
);
}
return runable.expectationFactory(actual, runable);
};
this.expectAsync = function(actual) {
const runable = runner.currentRunable();
if (!runable) {
throw new Error(
"'expectAsync' was used when there was no current spec, this could be because an asynchronous test timed out"
);
}
return runable.asyncExpectationFactory(actual, runable);
};
this.throwUnless = function(actual) {
const runable = runner.currentRunable();
return throwUnlessFactory(actual, runable);
};
this.throwUnlessAsync = function(actual) {
const runable = runner.currentRunable();
return throwUnlessAsyncFactory(actual, runable);
};
this.beforeEach = function(beforeEachFunction, timeout) {
ensureIsNotNested('beforeEach');
ensureNonParallelOrInHelperOrInDescribe('beforeEach');
suiteBuilder.beforeEach(beforeEachFunction, timeout);
};
this.beforeAll = function(beforeAllFunction, timeout) {
ensureIsNotNested('beforeAll');
// This message is -npm-specific, but currently parallel operation is
// only supported via -npm.
ensureNonParallelOrInDescribe(
"In parallel mode, 'beforeAll' " +
'must be in a describe block. Use the globalSetup config ' +
'property for exactly-once setup in parallel mode.'
);
suiteBuilder.beforeAll(beforeAllFunction, timeout);
};
this.afterEach = function(afterEachFunction, timeout) {
ensureIsNotNested('afterEach');
ensureNonParallelOrInHelperOrInDescribe('afterEach');
suiteBuilder.afterEach(afterEachFunction, timeout);
};
this.afterAll = function(afterAllFunction, timeout) {
ensureIsNotNested('afterAll');
// This message is -npm-specific, but currently parallel operation is
// only supported via -npm.
ensureNonParallelOrInDescribe(
"In parallel mode, 'afterAll' " +
'must be in a describe block. Use the globalTeardown config ' +
'property for exactly-once teardown in parallel mode.'
);
suiteBuilder.afterAll(afterAllFunction, timeout);
};
this.pending = function(message) {
let fullMessage = private$.Spec.pendingSpecExceptionMessage;
if (message) {
fullMessage += message;
}
throw fullMessage;
};
this.fail = function(error) {
if (!runner.currentRunable()) {
throw new Error(
"'fail' was used when there was no current spec, this could be because an asynchronous test timed out"
);
}
let message = 'Failed';
if (error) {
message += ': ';
if (error.message) {
message += error.message;
} else if (private$.isString(error)) {
message += error;
} else {
// pretty print all kind of objects. This includes arrays.
const pp = runableResources.makePrettyPrinter();
message += pp(error);
}
}
runner.currentRunable().addExpectationResult(false, {
matcherName: '',
passed: false,
expected: '',
actual: '',
message: message,
error: error && error.message ? error : null
});
if (config.stopSpecOnExpectationFailure) {
throw new Error(message);
}
};
this.pp = function(value) {
const pp = runner.currentRunable()
? runableResources.makePrettyPrinter()
: private$.basicPrettyPrinter;
return pp(value);
};
this.cleanup_ = function() {
uninstallGlobalErrors();
};
}
function indirectCallerFilename(depth) {
const frames = new private$.StackTrace(new Error()).frames;
// 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;
};