export default function jasmineEnv()

in packages/jest-jasmine2/src/jasmine/Env.ts [53:711]


export default function jasmineEnv(j$: Jasmine) {
  return class Env {
    specFilter: (spec: Spec) => boolean;
    catchExceptions: (value: unknown) => boolean;
    throwOnExpectationFailure: (value: unknown) => void;
    catchingExceptions: () => boolean;
    topSuite: () => Suite;
    fail: (error: Error | AssertionErrorWithStack) => void;
    pending: (message: string) => void;
    afterAll: (afterAllFunction: QueueableFn['fn'], timeout?: number) => void;
    fit: (
      description: Circus.TestNameLike,
      fn: QueueableFn['fn'],
      timeout?: number,
    ) => Spec;
    throwingExpectationFailures: () => boolean;
    randomizeTests: (value: unknown) => void;
    randomTests: () => boolean;
    seed: (value: unknown) => unknown;
    execute: (
      runnablesToRun?: Array<string>,
      suiteTree?: Suite,
    ) => Promise<void>;
    fdescribe: (
      description: Circus.TestNameLike,
      specDefinitions: SpecDefinitionsFn,
    ) => Suite;
    spyOn: (
      obj: Record<string, Spy>,
      methodName: string,
      accessType?: keyof PropertyDescriptor,
    ) => Spy;
    beforeEach: (
      beforeEachFunction: QueueableFn['fn'],
      timeout?: number,
    ) => void;
    afterEach: (afterEachFunction: QueueableFn['fn'], timeout?: number) => void;
    clearReporters: () => void;
    addReporter: (reporterToAdd: Reporter) => void;
    it: (
      description: Circus.TestNameLike,
      fn: QueueableFn['fn'],
      timeout?: number,
    ) => Spec;
    xdescribe: (
      description: Circus.TestNameLike,
      specDefinitions: SpecDefinitionsFn,
    ) => Suite;
    xit: (
      description: Circus.TestNameLike,
      fn: QueueableFn['fn'],
      timeout?: number,
    ) => Spec;
    beforeAll: (beforeAllFunction: QueueableFn['fn'], timeout?: number) => void;
    todo: () => Spec;
    provideFallbackReporter: (reporterToAdd: Reporter) => void;
    allowRespy: (allow: boolean) => void;
    describe: (
      description: Circus.TestNameLike,
      specDefinitions: SpecDefinitionsFn,
    ) => Suite;

    constructor() {
      let totalSpecsDefined = 0;

      let catchExceptions = true;

      const realSetTimeout = globalThis.setTimeout;
      const realClearTimeout = globalThis.clearTimeout;

      const runnableResources: Record<string, {spies: Array<Spy>}> = {};
      const currentlyExecutingSuites: Array<Suite> = [];
      let currentSpec: Spec | null = null;
      let throwOnExpectationFailure = false;
      let random = false;
      let seed: unknown | null = null;
      let nextSpecId = 0;
      let nextSuiteId = 0;

      const getNextSpecId = function () {
        return `spec${nextSpecId++}`;
      };

      const getNextSuiteId = function () {
        return `suite${nextSuiteId++}`;
      };

      const topSuite = new j$.Suite({
        id: getNextSuiteId(),
        description: '',
        getTestPath() {
          return j$.testPath;
        },
      });
      let currentDeclarationSuite = topSuite;

      const currentSuite = function () {
        return currentlyExecutingSuites[currentlyExecutingSuites.length - 1];
      };

      const currentRunnable = function () {
        return currentSpec || currentSuite();
      };

      const reporter = new j$.ReportDispatcher([
        'jasmineStarted',
        'jasmineDone',
        'suiteStarted',
        'suiteDone',
        'specStarted',
        'specDone',
      ]);

      this.specFilter = function () {
        return true;
      };

      const defaultResourcesForRunnable = function (
        id: string,
        _parentRunnableId?: string,
      ) {
        const resources = {spies: []};

        runnableResources[id] = resources;
      };

      const clearResourcesForRunnable = function (id: string) {
        spyRegistry.clearSpies();
        delete runnableResources[id];
      };

      const beforeAndAfterFns = function (suite: Suite) {
        return function () {
          let afters: Array<QueueableFn> = [];
          let befores: Array<QueueableFn> = [];

          while (suite) {
            befores = befores.concat(suite.beforeFns);
            afters = afters.concat(suite.afterFns);

            suite = suite.parentSuite!;
          }

          return {
            befores: befores.reverse(),
            afters,
          };
        };
      };

      const getSpecName = function (spec: Spec, suite: Suite) {
        const fullName = [spec.description];
        const suiteFullName = suite.getFullName();

        if (suiteFullName !== '') {
          fullName.unshift(suiteFullName);
        }

        return fullName.join(' ');
      };

      this.catchExceptions = function (value) {
        catchExceptions = !!value;
        return catchExceptions;
      };

      this.catchingExceptions = function () {
        return catchExceptions;
      };

      this.throwOnExpectationFailure = function (value) {
        throwOnExpectationFailure = !!value;
      };

      this.throwingExpectationFailures = function () {
        return throwOnExpectationFailure;
      };

      this.randomizeTests = function (value) {
        random = !!value;
      };

      this.randomTests = function () {
        return random;
      };

      this.seed = function (value) {
        if (value) {
          seed = value;
        }
        return seed;
      };

      const queueRunnerFactory = (options: QueueRunnerOptions) => {
        options.clearTimeout = realClearTimeout;
        options.fail = this.fail;
        options.setTimeout = realSetTimeout;
        return queueRunner(options);
      };

      this.topSuite = function () {
        return topSuite;
      };

      const uncaught: NodeJS.UncaughtExceptionListener &
        NodeJS.UnhandledRejectionListener = (err: any) => {
        if (currentSpec) {
          currentSpec.onException(err);
          currentSpec.cancel();
        } else {
          console.error('Unhandled error');
          console.error(err.stack);
        }
      };

      let oldListenersException: Array<NodeJS.UncaughtExceptionListener>;
      let oldListenersRejection: Array<NodeJS.UnhandledRejectionListener>;
      const executionSetup = function () {
        // Need to ensure we are the only ones handling these exceptions.
        oldListenersException = process.listeners('uncaughtException').slice();
        oldListenersRejection = process.listeners('unhandledRejection').slice();

        j$.process.removeAllListeners('uncaughtException');
        j$.process.removeAllListeners('unhandledRejection');

        j$.process.on('uncaughtException', uncaught);
        j$.process.on('unhandledRejection', uncaught);
      };

      const executionTeardown = function () {
        j$.process.removeListener('uncaughtException', uncaught);
        j$.process.removeListener('unhandledRejection', uncaught);

        // restore previous exception handlers
        oldListenersException.forEach(listener => {
          j$.process.on('uncaughtException', listener);
        });

        oldListenersRejection.forEach(listener => {
          j$.process.on('unhandledRejection', listener);
        });
      };

      this.execute = async function (runnablesToRun, suiteTree = topSuite) {
        if (!runnablesToRun) {
          if (focusedRunnables.length) {
            runnablesToRun = focusedRunnables;
          } else {
            runnablesToRun = [suiteTree.id];
          }
        }

        if (currentlyExecutingSuites.length === 0) {
          executionSetup();
        }

        const lastDeclarationSuite = currentDeclarationSuite;

        await treeProcessor({
          nodeComplete(suite) {
            if (!suite.disabled) {
              clearResourcesForRunnable(suite.id);
            }
            currentlyExecutingSuites.pop();
            if (suite === topSuite) {
              reporter.jasmineDone({
                failedExpectations: topSuite.result.failedExpectations,
              });
            } else {
              reporter.suiteDone(suite.getResult());
            }
          },
          nodeStart(suite) {
            currentlyExecutingSuites.push(suite as Suite);
            defaultResourcesForRunnable(
              suite.id,
              suite.parentSuite && suite.parentSuite.id,
            );
            if (suite === topSuite) {
              reporter.jasmineStarted({totalSpecsDefined});
            } else {
              reporter.suiteStarted(suite.result);
            }
          },
          queueRunnerFactory,
          runnableIds: runnablesToRun,
          tree: suiteTree as TreeNode,
        });

        currentDeclarationSuite = lastDeclarationSuite;

        if (currentlyExecutingSuites.length === 0) {
          executionTeardown();
        }
      };

      this.addReporter = function (reporterToAdd) {
        reporter.addReporter(reporterToAdd);
      };

      this.provideFallbackReporter = function (reporterToAdd) {
        reporter.provideFallbackReporter(reporterToAdd);
      };

      this.clearReporters = function () {
        reporter.clearReporters();
      };

      const spyRegistry = new j$.SpyRegistry({
        currentSpies() {
          if (!currentRunnable()) {
            throw new Error(
              'Spies must be created in a before function or a spec',
            );
          }
          return runnableResources[currentRunnable().id].spies;
        },
      });

      this.allowRespy = function (allow) {
        spyRegistry.allowRespy(allow);
      };

      this.spyOn = function (...args) {
        return spyRegistry.spyOn.apply(spyRegistry, args);
      };

      const suiteFactory = function (description: Circus.TestNameLike) {
        const suite = new j$.Suite({
          id: getNextSuiteId(),
          description,
          parentSuite: currentDeclarationSuite,
          throwOnExpectationFailure,
          getTestPath() {
            return j$.testPath;
          },
        });

        return suite;
      };

      this.describe = function (
        description: Circus.TestNameLike,
        specDefinitions,
      ) {
        const suite = suiteFactory(description);
        if (specDefinitions === undefined) {
          throw new Error(
            'Missing second argument. It must be a callback function.',
          );
        }
        if (typeof specDefinitions !== 'function') {
          throw new Error(
            `Invalid second argument, ${specDefinitions}. It must be a callback function.`,
          );
        }
        if (specDefinitions.length > 0) {
          throw new Error('describe does not expect any arguments');
        }
        if (currentDeclarationSuite.markedPending) {
          suite.pend();
        }
        if (currentDeclarationSuite.markedTodo) {
          // @ts-expect-error TODO Possible error: Suite does not have todo method
          suite.todo();
        }
        addSpecsToSuite(suite, specDefinitions);
        return suite;
      };

      this.xdescribe = function (description, specDefinitions) {
        const suite = suiteFactory(description);
        suite.pend();
        addSpecsToSuite(suite, specDefinitions);
        return suite;
      };

      const focusedRunnables: Array<string> = [];

      this.fdescribe = function (description, specDefinitions) {
        const suite = suiteFactory(description);
        suite.isFocused = true;

        focusedRunnables.push(suite.id);
        unfocusAncestor();
        addSpecsToSuite(suite, specDefinitions);

        return suite;
      };

      const addSpecsToSuite = (
        suite: Suite,
        specDefinitions: SpecDefinitionsFn,
      ) => {
        const parentSuite = currentDeclarationSuite;
        parentSuite.addChild(suite);
        currentDeclarationSuite = suite;

        let declarationError: undefined | Error = undefined;
        let describeReturnValue: unknown | Error;
        try {
          describeReturnValue = specDefinitions.call(suite);
        } catch (e: any) {
          declarationError = e;
        }

        if (isPromise(describeReturnValue)) {
          declarationError = new Error(
            'Returning a Promise from "describe" is not supported. Tests must be defined synchronously.',
          );
        } else if (describeReturnValue !== undefined) {
          declarationError = new Error(
            'A "describe" callback must not return a value.',
          );
        }

        if (declarationError) {
          this.it('encountered a declaration exception', () => {
            throw declarationError;
          });
        }

        currentDeclarationSuite = parentSuite;
      };

      function findFocusedAncestor(suite: Suite) {
        while (suite) {
          if (suite.isFocused) {
            return suite.id;
          }
          suite = suite.parentSuite!;
        }

        return null;
      }

      function unfocusAncestor() {
        const focusedAncestor = findFocusedAncestor(currentDeclarationSuite);
        if (focusedAncestor) {
          for (let i = 0; i < focusedRunnables.length; i++) {
            if (focusedRunnables[i] === focusedAncestor) {
              focusedRunnables.splice(i, 1);
              break;
            }
          }
        }
      }

      const specFactory = (
        description: Circus.TestNameLike,
        fn: QueueableFn['fn'],
        suite: Suite,
        timeout?: number,
      ): Spec => {
        totalSpecsDefined++;
        const spec = new j$.Spec({
          id: getNextSpecId(),
          beforeAndAfterFns: beforeAndAfterFns(suite),
          resultCallback: specResultCallback,
          getSpecName(spec: Spec) {
            return getSpecName(spec, suite);
          },
          getTestPath() {
            return j$.testPath;
          },
          onStart: specStarted,
          description,
          queueRunnerFactory,
          userContext() {
            return suite.clonedSharedUserContext();
          },
          queueableFn: {
            fn,
            timeout() {
              return timeout || j$._DEFAULT_TIMEOUT_INTERVAL;
            },
          },
          throwOnExpectationFailure,
        });

        if (!this.specFilter(spec)) {
          spec.disable();
        }

        return spec;

        function specResultCallback(result: SpecResult) {
          clearResourcesForRunnable(spec.id);
          currentSpec = null;
          reporter.specDone(result);
        }

        function specStarted(spec: Spec) {
          currentSpec = spec;
          defaultResourcesForRunnable(spec.id, suite.id);
          reporter.specStarted(spec.result);
        }
      };

      this.it = function (description, fn, timeout) {
        description = convertDescriptorToString(description);
        if (fn === undefined) {
          throw new Error(
            'Missing second argument. It must be a callback function. Perhaps you want to use `test.todo` for a test placeholder.',
          );
        }
        if (typeof fn !== 'function') {
          throw new Error(
            `Invalid second argument, ${fn}. It must be a callback function.`,
          );
        }
        const spec = specFactory(
          description,
          fn,
          currentDeclarationSuite,
          timeout,
        );
        if (currentDeclarationSuite.markedPending) {
          spec.pend();
        }

        // When a test is defined inside another, jasmine will not run it.
        // This check throws an error to warn the user about the edge-case.
        if (currentSpec !== null) {
          throw new Error(
            `Tests cannot be nested. Test "${spec.description}" cannot run because it is nested within "${currentSpec.description}".`,
          );
        }
        currentDeclarationSuite.addChild(spec);
        return spec;
      };

      this.xit = function (...args) {
        const spec = this.it.apply(this, args);
        spec.pend('Temporarily disabled with xit');
        return spec;
      };

      this.todo = function () {
        const description = arguments[0];
        if (arguments.length !== 1 || typeof description !== 'string') {
          throw new ErrorWithStack(
            'Todo must be called with only a description.',
            this.todo,
          );
        }

        const spec = specFactory(
          description,
          () => {},
          currentDeclarationSuite,
        );
        if (currentDeclarationSuite.markedPending) {
          spec.pend();
        } else {
          spec.todo();
        }
        currentDeclarationSuite.addChild(spec);
        return spec;
      };

      this.fit = function (description, fn, timeout) {
        const spec = specFactory(
          description,
          fn,
          currentDeclarationSuite,
          timeout,
        );
        currentDeclarationSuite.addChild(spec);
        if (currentDeclarationSuite.markedPending) {
          spec.pend();
        } else {
          focusedRunnables.push(spec.id);
        }
        unfocusAncestor();
        return spec;
      };

      this.beforeEach = function (beforeEachFunction, timeout) {
        currentDeclarationSuite.beforeEach({
          fn: beforeEachFunction,
          timeout() {
            return timeout || j$._DEFAULT_TIMEOUT_INTERVAL;
          },
        });
      };

      this.beforeAll = function (beforeAllFunction, timeout) {
        currentDeclarationSuite.beforeAll({
          fn: beforeAllFunction,
          timeout() {
            return timeout || j$._DEFAULT_TIMEOUT_INTERVAL;
          },
        });
      };

      this.afterEach = function (afterEachFunction, timeout) {
        currentDeclarationSuite.afterEach({
          fn: afterEachFunction,
          timeout() {
            return timeout || j$._DEFAULT_TIMEOUT_INTERVAL;
          },
        });
      };

      this.afterAll = function (afterAllFunction, timeout) {
        currentDeclarationSuite.afterAll({
          fn: afterAllFunction,
          timeout() {
            return timeout || j$._DEFAULT_TIMEOUT_INTERVAL;
          },
        });
      };

      this.pending = function (message) {
        let fullMessage = j$.Spec.pendingSpecExceptionMessage;
        if (message) {
          fullMessage += message;
        }
        throw fullMessage;
      };

      this.fail = function (error) {
        let checkIsError;
        let message;

        if (
          error instanceof AssertionError ||
          (error && error.name === AssertionError.name)
        ) {
          checkIsError = false;
          // @ts-expect-error TODO Possible error: j$.Spec does not have expand property
          message = assertionErrorMessage(error, {expand: j$.Spec.expand});
        } else {
          const check = isError(error);

          checkIsError = check.isError;
          message = check.message;
        }
        const errorAsErrorObject = checkIsError ? error : new Error(message!);
        const runnable = currentRunnable();

        if (!runnable) {
          errorAsErrorObject.message = `Caught error after test environment was torn down\n\n${errorAsErrorObject.message}`;

          throw errorAsErrorObject;
        }

        runnable.addExpectationResult(false, {
          matcherName: '',
          passed: false,
          expected: '',
          actual: '',
          message,
          error: errorAsErrorObject,
        });
      };
    }
  };