Repository: wix/Detox
Pull request #4214 (2023-10-11 12:43:22)

refactor: reporter for Jest
## Description

Resolves @d4vidi's comments towards #4199 and #4115.


Patch:
diff --git a/detox/jest.config.js b/detox/jest.config.js
index 323f83261..0901108b3 100644
--- a/detox/jest.config.js
+++ b/detox/jest.config.js
@@ -1,3 +1,5 @@
+const DEBUG = process.argv.includes('--reporters');
+
 /** @type{import('jest-allure2-reporter').ReporterOptions} */
 const jestAllure2ReporterOptions = {
   testCase: {
@@ -14,7 +16,7 @@ module.exports = {
   setupFiles: [
     '<rootDir>/__tests__/setupJest.js'
   ],
-  testEnvironment: 'jest-allure2-reporter/environment-node',
+  testEnvironment: DEBUG ? 'node' : 'jest-allure2-reporter/environment-node',
   testRunner: 'jest-circus/runner',
   roots: [
     'node_modules',
diff --git a/detox/runners/jest/reporter.js b/detox/runners/jest/reporter.js
index a6f87d991..1d2f07b09 100644
--- a/detox/runners/jest/reporter.js
+++ b/detox/runners/jest/reporter.js
@@ -1,1 +1,21 @@
-module.exports = require('./reporters/DetoxReporter');
+/** @typedef {import('@jest/reporters').Reporter} Reporter */
+
+const {
+  DetoxIPCReporter,
+  DetoxReporterDispatcher,
+  DetoxSummaryReporter,
+  DetoxVerboseReporter,
+} = require('./reporters');
+
+/** @implements {Reporter} */
+class DetoxReporter extends DetoxReporterDispatcher {
+  constructor(globalConfig) {
+    super(globalConfig, {
+      DetoxSummaryReporter,
+      DetoxVerboseReporter,
+      DetoxIPCReporter,
+    });
+  }
+}
+
+module.exports = DetoxReporter;
diff --git a/detox/runners/jest/reporters/DetoxIPCReporter.js b/detox/runners/jest/reporters/DetoxIPCReporter.js
new file mode 100644
index 000000000..9e4c6eaba
--- /dev/null
+++ b/detox/runners/jest/reporters/DetoxIPCReporter.js
@@ -0,0 +1,34 @@
+const { config, reportTestResults } = require('../../../internals');
+
+/** @typedef {import('@jest/reporters').Reporter} Reporter */
+
+/** @implements {Partial<Reporter>} */
+class DetoxIPCReporter {
+  /**
+   * @param {Set<import('@jest/reporters').TestContext>} testContexts
+   * @param {import('@jest/reporters').AggregatedResult} aggregatedResult
+   */
+  async onRunComplete(testContexts, aggregatedResult) {
+    const lostTests = aggregatedResult.numTotalTestSuites - aggregatedResult.testResults.length;
+
+    await reportTestResults(aggregatedResult.testResults.map(r => ({
+      success: !r.failureMessage,
+      testFilePath: r.testFilePath,
+      testExecError: r.testExecError,
+      isPermanentFailure: lostTests > 0 || this._isPermanentFailure(r),
+    })));
+  }
+
+  /**
+   * @param {import('@jest/test-result').TestResult} testResult
+   */
+  _isPermanentFailure(testResult) {
+    if (config.testRunner.jest.retryAfterCircusRetries) {
+      return false;
+    }
+
+    return testResult.testResults.some(r => r.status === 'failed' && r.invocations > 1);
+  }
+}
+
+module.exports = DetoxIPCReporter;
diff --git a/detox/runners/jest/reporters/DetoxReporter.js b/detox/runners/jest/reporters/DetoxReporter.js
deleted file mode 100644
index b0d687cb4..000000000
--- a/detox/runners/jest/reporters/DetoxReporter.js
+++ /dev/null
@@ -1,154 +0,0 @@
-const resolveFrom = require('resolve-from');
-/** @type {new (globalConfig: any) => import('@jest/reporters').VerboseReporter} */
-const JestVerboseReporter = require(resolveFrom(process.cwd(), '@jest/reporters')).VerboseReporter;
-/** @type {new (globalConfig: any) => import('@jest/reporters').SummaryReporter} */
-const SummaryReporter = require(resolveFrom(process.cwd(), '@jest/reporters')).SummaryReporter;
-
-const { config, reportTestResults, unsafe_conductEarlyTeardown, cleanup } = require('../../../internals');
-const Deferred = require('../../../src/utils/Deferred');
-
-class DetoxReporter extends JestVerboseReporter {
-  constructor(globalConfig) {
-    super(globalConfig);
-
-    /** @type {Deferred | null} */
-    this._lastRunComplete = null;
-    /** @type {Set<string>} */
-    this._pendingTestFiles = new Set();
-    /** @type {Promise<any> | null} */
-    this._runCompletePromise = null;
-    /** @type {import('@jest/reporters').SummaryReporter | null} */
-    this._summaryReporter = this._initSummaryReporter();
-  }
-
-  onRunStart(aggregatedResults, options) {
-    super.onRunStart(aggregatedResults, options);
-
-    if (this._summaryReporter) {
-      this._summaryReporter.onRunStart(aggregatedResults, options);
-    }
-  }
-
-  onTestFileStart(test) {
-    this._pendingTestFiles.add(test.path);
-
-    // @ts-ignore Precaution in case Jest migrates to the new signature
-    if (typeof super.onTestFileStart === 'function') {
-      // @ts-ignore
-      super.onTestFileStart(test);
-    } else {
-      super.onTestStart(test);
-    }
-  }
-
-  onTestFileResult(test, testResult, aggregatedResult) {
-    this._pendingTestFiles.delete(test.path);
-
-    // @ts-ignore Precaution in case Jest migrates to the new signature
-    if (typeof super.onTestFileResult === 'function') {
-      // @ts-ignore
-      super.onTestFileResult(test, testResult, aggregatedResult);
-    } else {
-      super.onTestResult(test, testResult, aggregatedResult);
-    }
-
-    if (this._lastRunComplete && this._pendingTestFiles.size === 0) {
-      this._lastRunComplete.resolve(aggregatedResult);
-    }
-  }
-
-  /**
-   * @param {Set<import('@jest/reporters').TestContext>} testContexts
-   * @param {import('@jest/reporters').AggregatedResult} aggregatedResult
-   * @returns {Promise<any> | null}
-   */
-  // @ts-ignore We need to use the complete signature, not the one from the base class
-  onRunComplete(testContexts, aggregatedResult) {
-    if (!this._runCompletePromise) {
-      // Both `_lastRunComplete` and `_runCompletePromise` are used to prevent
-      // a bug in Jest, where `onRunComplete` is called multiple times when
-      // Jest runs with `--bail` and multiple workers, and `onRunComplete`
-      // is implemented as an asynchronous long-running operation.
-      this._lastRunComplete = this._pendingTestFiles.size === 0
-        ? Deferred.resolved(aggregatedResult)
-        : new Deferred();
-
-      this._runCompletePromise = this._onRunComplete(testContexts, aggregatedResult).catch(err => {
-        console.error(err);
-        throw err;
-      });
-    }
-
-    return this._runCompletePromise;
-  }
-
-  /**
-   * @param {Set<import('@jest/reporters').TestContext>} testContexts
-   * @param {import('@jest/reporters').AggregatedResult} aggregatedResult
-   * @returns {Promise<any> | null}
-   */
-  async _onRunComplete(testContexts, { numFailedTests }) {
-    const bail = this._globalConfig.bail;
-    const earlyTeardown = bail > 0 && numFailedTests >= bail;
-    if (earlyTeardown) {
-      await unsafe_conductEarlyTeardown();
-    }
-
-    const aggregatedResult = await this._lastRunComplete.promise;
-    const lostTests = aggregatedResult.numTotalTestSuites - aggregatedResult.testResults.length;
-
-    // @ts-expect-error TS2554
-    super.onRunComplete(testContexts, aggregatedResult);
-
-    if (earlyTeardown && lostTests > 0 && config.testRunner.retries > 0) {
-      console.warn(
-        'Jest aborted the test execution before all scheduled test files have been reported.\n' +
-        'This brings us to a dilemma: retry the whole test run, or retry only known failed test files.\n' +
-        'Both options are bad in their own way, this is why we have decided to not support this edge case.\n' +
-        'If you want to retry the whole test run, please disable Jest\'s --bail option.'
-      );
-    }
-
-    await reportTestResults(aggregatedResult.testResults.map(r => ({
-      success: !r.failureMessage,
-      testFilePath: r.testFilePath,
-      testExecError: r.testExecError,
-      isPermanentFailure: lostTests > 0 || this._isPermanentFailure(r),
-    })));
-
-    if (this._summaryReporter) {
-      this._summaryReporter.onRunComplete(testContexts, aggregatedResult);
-    }
-
-    if (earlyTeardown) {
-      await cleanup();
-    }
-  }
-
-  /**
-   * @returns {import('@jest/reporters').SummaryReporter | null}
-   * @private
-   */
-  _initSummaryReporter() {
-    /** @type {(config: import('@jest/types').Config.ReporterConfig) => boolean} */
-    const isSummaryReporter = (config) => config[0] === 'summary';
-    if (this._globalConfig.reporters.some(isSummaryReporter)) {
-      return null;
-    }
-
-    return new SummaryReporter(this._globalConfig);
-  }
-
-  /**
-   * @param {import('@jest/test-result').TestResult} testResult
-   */
-  _isPermanentFailure(testResult) {
-    if (config.testRunner.jest.retryAfterCircusRetries) {
-      return false;
-    }
-
-    return testResult.testResults.some(r => r.status === 'failed' && r.invocations > 1);
-  }
-}
-
-module.exports = DetoxReporter;
diff --git a/detox/runners/jest/reporters/DetoxReporterDispatcher.js b/detox/runners/jest/reporters/DetoxReporterDispatcher.js
new file mode 100644
index 000000000..250c4ef2a
--- /dev/null
+++ b/detox/runners/jest/reporters/DetoxReporterDispatcher.js
@@ -0,0 +1,144 @@
+/** @typedef {import('@jest/reporters').Reporter} Reporter */
+
+const { config, unsafe_conductEarlyTeardown } = require('../../../internals');
+const Deferred = require('../../../src/utils/Deferred');
+
+/** @implements {Reporter} */
+class DetoxReporterDispatcher {
+  /**
+   * @param {import('@jest/types').Config.GlobalConfig} globalConfig
+   * @param {Record<string, new (globalConfig: import('@jest/types').Config.GlobalConfig) => Partial<Reporter>>} reporters
+   */
+  constructor(globalConfig, reporters) {
+    this._bail = globalConfig.bail;
+    /** @type {Deferred | null} */
+    this._lastRunComplete = null;
+    /** @type {Set<string>} */
+    this._pendingTestFiles = new Set();
+    /** @type {Promise<any> | null} */
+    this._onRunCompletePromise = null;
+    /** @type {Partial<Reporter>[]} */
+    this._reporters = Object.values(reporters).map((Reporter) => new Reporter(globalConfig));
+  }
+
+  getLastError() {
+    for (const reporter of this._reporters) {
+      let error = typeof reporter.getLastError === 'function'
+        ? reporter.getLastError()
+        : undefined;
+
+      if (error) {
+        return error;
+      }
+    }
+
+    return;
+  }
+
+  onRunStart(aggregatedResult, options) {
+    return this._dispatch('onRunStart', aggregatedResult, options);
+  }
+
+  onTestFileStart(test) {
+    this._pendingTestFiles.add(test.path);
+    return this._dispatch(['onTestFileStart', 'onTestStart'], test);
+  }
+
+  onTestStart(test) {
+    // Legacy method
+    return this.onTestFileStart(test);
+  }
+
+  // NEW! Supported only since Jest 29.6.0
+  onTestCaseStart(test, testCaseStartInfo) {
+    return this._dispatch('onTestCaseStart', test, testCaseStartInfo);
+  }
+
+  onTestCaseResult(test, testCaseResult) {
+    return this._dispatch('onTestCaseResult', test, testCaseResult);
+  }
+
+  async onTestFileResult(test, testResult, aggregatedResult) {
+    this._pendingTestFiles.delete(test.path);
+
+    await this._dispatch(['onTestFileResult', 'onTestResult'], test, testResult, aggregatedResult);
+
+    if (this._lastRunComplete && this._pendingTestFiles.size === 0) {
+      this._lastRunComplete.resolve(aggregatedResult);
+    }
+  }
+
+  onTestResult(test, testResult, aggregatedResult) {
+    // Legacy method
+    return this.onTestFileResult(test, testResult, aggregatedResult);
+  }
+
+  onRunComplete(testContexts, aggregatedResult) {
+    if (!this._lastRunComplete) {
+      // Both `_lastRunComplete` and `_onRunCompletePromise` are used to prevent
+      // a bug in Jest, where `onRunComplete` is called multiple times when
+      // Jest runs with `--bail` and multiple workers, and `onRunComplete`
+      // is implemented as an asynchronous long-running operation.
+      this._lastRunComplete = this._pendingTestFiles.size === 0
+        ? Deferred.resolved(aggregatedResult)
+        : new Deferred();
+    }
+
+    if (!this._onRunCompletePromise) {
+      this._onRunCompletePromise = this._doRunComplete(testContexts, aggregatedResult);
+    }
+
+    return this._onRunCompletePromise;
+  }
+
+  /**
+   * @private
+   * @param {Set<import('@jest/reporters').TestContext>} testContexts
+   * @param {import('@jest/test-result').AggregatedResult} aggregatedResult
+   * @returns {Promise<void>}
+   */
+  async _doRunComplete(testContexts, aggregatedResult) {
+    const earlyTeardown = this._bail > 0 && aggregatedResult.numFailedTests >= this._bail;
+    if (earlyTeardown) {
+      const lostTests = aggregatedResult.numTotalTestSuites - aggregatedResult.testResults.length;
+      if (lostTests > 0 && config.testRunner.retries > 0) {
+        console.warn(
+          'Jest aborted the test execution before all scheduled test files have been reported.\n' +
+          'If you want to retry the whole test run, please disable Jest\'s --bail option.'
+        );
+      }
+
+      await unsafe_conductEarlyTeardown();
+    }
+
+    await this._lastRunComplete.promise;
+    await this._dispatch('onRunComplete', testContexts, aggregatedResult);
+  }
+
+  /**
+   * @private
+   * @param {string | string[]} rawMethodNames
+   * @param {...any} args
+   * @returns {Promise<void>}
+   */
+  _dispatch(rawMethodNames, ...args) {
+    const methodNames = Array.isArray(rawMethodNames)
+      ? rawMethodNames
+      : [rawMethodNames];
+
+    const maybePromises = [];
+
+    for (const reporter of this._reporters) {
+      for (const methodName of methodNames) {
+        if (typeof reporter[methodName] === 'function') {
+          maybePromises.push(reporter[methodName](...args));
+          break;
+        }
+      }
+    }
+
+    return Promise.all(maybePromises).then(() => void 0);
+  }
+}
+
+module.exports = DetoxReporterDispatcher;
diff --git a/detox/runners/jest/reporters/DetoxSummaryReporter.js b/detox/runners/jest/reporters/DetoxSummaryReporter.js
new file mode 100644
index 000000000..5f507ee94
--- /dev/null
+++ b/detox/runners/jest/reporters/DetoxSummaryReporter.js
@@ -0,0 +1,16 @@
+/** @typedef {import('@jest/reporters').Reporter} Reporter */
+
+const resolveFrom = require('resolve-from');
+/** @type {new (globalConfig: any) => import('@jest/reporters').SummaryReporter} */
+const SummaryReporter = require(resolveFrom(process.cwd(), '@jest/reporters')).SummaryReporter;
+
+/** @implements {Partial<Reporter>} */
+class DetoxSummaryReporter {
+  constructor(globalConfig) {
+    if (globalConfig.reporters.every(([name]) => name !== 'default' && name !== 'summary')) {
+      return new SummaryReporter(globalConfig);
+    }
+  }
+}
+
+module.exports = DetoxSummaryReporter;
diff --git a/detox/runners/jest/reporters/DetoxVerboseReporter.js b/detox/runners/jest/reporters/DetoxVerboseReporter.js
new file mode 100644
index 000000000..fbc24799b
--- /dev/null
+++ b/detox/runners/jest/reporters/DetoxVerboseReporter.js
@@ -0,0 +1,16 @@
+/** @typedef {import('@jest/reporters').Reporter} Reporter */
+
+const resolveFrom = require('resolve-from');
+/** @type {new (globalConfig: any) => import('@jest/reporters').VerboseReporter} */
+const VerboseReporter = require(resolveFrom(process.cwd(), '@jest/reporters')).VerboseReporter;
+
+/** @implements {Partial<Reporter>} */
+class DetoxVerboseReporter {
+  constructor(globalConfig) {
+    if (globalConfig.reporters.every(([name]) => name !== 'default')) {
+      return new VerboseReporter(globalConfig);
+    }
+  }
+}
+
+module.exports = DetoxVerboseReporter;
diff --git a/detox/runners/jest/reporters/index.js b/detox/runners/jest/reporters/index.js
new file mode 100644
index 000000000..acd343cc0
--- /dev/null
+++ b/detox/runners/jest/reporters/index.js
@@ -0,0 +1,6 @@
+module.exports = {
+  DetoxIPCReporter: require('./DetoxIPCReporter'),
+  DetoxReporterDispatcher: require('./DetoxReporterDispatcher'),
+  DetoxSummaryReporter: require('./DetoxSummaryReporter'),
+  DetoxVerboseReporter: require('./DetoxVerboseReporter'),
+};


Test patch:
diff --git a/detox/runners/__tests__/reporter.test.js b/detox/runners/__tests__/reporter.test.js
new file mode 100644
index 000000000..7a93ff706
--- /dev/null
+++ b/detox/runners/__tests__/reporter.test.js
@@ -0,0 +1,18 @@
+const DetoxReporter = require('../jest/reporter');
+
+test('DetoxReporter', () => {
+  const reporter = new DetoxReporter({
+    bail: 0,
+    reporters: [['default', {}]],
+  });
+
+  expect(reporter.getLastError).toBeInstanceOf(Function);
+  expect(reporter.onRunStart).toBeInstanceOf(Function);
+  expect(reporter.onTestFileStart).toBeInstanceOf(Function);
+  expect(reporter.onTestStart).toBeInstanceOf(Function);
+  expect(reporter.onTestCaseStart).toBeInstanceOf(Function);
+  expect(reporter.onTestCaseResult).toBeInstanceOf(Function);
+  expect(reporter.onTestFileResult).toBeInstanceOf(Function);
+  expect(reporter.onTestResult).toBeInstanceOf(Function);
+  expect(reporter.onRunComplete).toBeInstanceOf(Function);
+});
diff --git a/detox/runners/jest/reporters/DetoxReporterDispatcher.test.js b/detox/runners/jest/reporters/DetoxReporterDispatcher.test.js
new file mode 100644
index 000000000..6fa52fc7c
--- /dev/null
+++ b/detox/runners/jest/reporters/DetoxReporterDispatcher.test.js
@@ -0,0 +1,157 @@
+jest.mock('../../../internals', () => ({
+  config: { testRunner: {} },
+  unsafe_conductEarlyTeardown: jest.fn(),
+}));
+
+describe('DetoxReporterDispatcher', () => {
+  /** @type {import('./DetoxReporterDispatcher')} */
+  let dispatcher;
+  /** @type {any} */
+  let mockGlobalConfig;
+  /** @type {LegacyReporter} */
+  let legacyReporter;
+  /** @type {ModernReporter} */
+  let modernReporter;
+
+  class LegacyReporter {
+    constructor(globalConfig) {
+      if (globalConfig) {
+        legacyReporter = this;
+      }
+    }
+
+    getLastError = jest.fn();
+    onRunStart = jest.fn();
+    onTestStart = jest.fn();
+    onTestCaseStart = jest.fn();
+    onTestCaseResult = jest.fn();
+    onTestResult = jest.fn();
+    onRunComplete = jest.fn();
+  }
+
+  class ModernReporter extends LegacyReporter {
+    constructor() {
+      super(null);
+      modernReporter = this;
+    }
+
+    onTestFileStart = jest.fn();
+    onTestFileResult = jest.fn();
+  }
+
+  beforeEach(() => {
+    mockGlobalConfig = { bail: 2 };
+
+    const DetoxReporterDispatcher = require('./DetoxReporterDispatcher');
+    dispatcher = new DetoxReporterDispatcher(mockGlobalConfig, {
+      LegacyReporter,
+      ModernReporter,
+    });
+  });
+
+  it('should instantiate reporters', () => {
+    expect(legacyReporter).toBeInstanceOf(LegacyReporter);
+    expect(modernReporter).toBeInstanceOf(ModernReporter);
+  });
+
+  it('should return first error from getLastError method', () => {
+    const [error1, error2] = [new Error('1'), new Error('2')];
+    legacyReporter.getLastError.mockReturnValue(error1);
+    modernReporter.getLastError.mockReturnValue(error2);
+    expect(dispatcher.getLastError()).toBe(error1);
+  });
+
+  it.each([
+    ['onRunStart'],
+    ['onTestCaseStart'],
+    ['onTestCaseResult'],
+    ['onRunComplete'],
+  ])('should dispatch %s calls to all reporters', async (methodName) => {
+    const args = [{ path: 'test' }, { arg: 2 }, { arg: 3 }].slice(0, dispatcher[methodName].length);
+    await dispatcher[methodName](...args);
+    expect(legacyReporter[methodName]).toHaveBeenCalledWith(...args);
+    expect(modernReporter[methodName]).toHaveBeenCalledWith(...args);
+  });
+
+  it.each([
+    ['onTestFileStart', 'onTestStart'],
+    ['onTestFileResult', 'onTestResult'],
+  ])('should dispatch %s/%s calls to all reporters', async (modernMethod, legacyMethod) => {
+    const args = [{ path: 'test' }, { arg: 2 }, { arg: 3 }].slice(0, dispatcher[modernMethod].length);
+
+    for (const methodName of [modernMethod, legacyMethod]) {
+      await dispatcher[methodName](...args);
+
+      expect(legacyReporter[legacyMethod]).toHaveBeenCalledWith(...args);
+      expect(modernReporter[modernMethod]).toHaveBeenCalledWith(...args);
+      expect(modernReporter[legacyMethod]).not.toHaveBeenCalled();
+
+      legacyReporter[legacyMethod].mockClear();
+      modernReporter[modernMethod].mockClear();
+    }
+  });
+
+  describe('onRunComplete', () => {
+    let unsafe_conductEarlyTeardown;
+
+    beforeEach(() => {
+      jest.spyOn(console, 'warn');
+      unsafe_conductEarlyTeardown = require('../../../internals').unsafe_conductEarlyTeardown;
+    });
+
+    describe('upon a regular exit', () => {
+      const testContexts = new Set();
+      const aggregatedResult = { numFailedTests: 1, numTotalSuites: 0, testResults: [] };
+
+      it('should not call unsafe cleanup on regular exit', async () => {
+        await dispatcher.onRunComplete(testContexts, aggregatedResult);
+        expect(unsafe_conductEarlyTeardown).not.toHaveBeenCalled();
+        expect(console.warn).not.toHaveBeenCalled();
+      });
+
+      it('can be called only once per reporter lifecycle', async () => {
+        const promise1 = dispatcher.onRunComplete(testContexts, aggregatedResult);
+        const promise2 = dispatcher.onRunComplete(testContexts, aggregatedResult);
+
+        expect(promise1).toBe(promise2);
+      });
+    });
+
+    describe('upon an early exit', () => {
+      const testContexts = new Set();
+      let aggregatedResult;
+      let config;
+
+      beforeEach(() => {
+        // 2 > 1 (numFailedTests > bail)
+        aggregatedResult = { numFailedTests: 2, numTotalTestSuites: 1, testResults: [] };
+        config = require('../../../internals').config;
+      });
+
+      it('should call unsafe cleanup on early exit', async () => {
+        await dispatcher.onRunComplete(testContexts, aggregatedResult);
+
+        expect(unsafe_conductEarlyTeardown).toHaveBeenCalled();
+        expect(console.warn).not.toHaveBeenCalled();
+      });
+
+      it('should wait for all test files to finish', async () => {
+        await dispatcher.onTestFileStart({ path: 'a' });
+        const runCompletePromise = dispatcher.onRunComplete(testContexts, aggregatedResult);
+        const timeoutPromise = new Promise((resolve) => setTimeout(resolve, 100, 'timeout'));
+
+        await expect(Promise.race([runCompletePromise, timeoutPromise])).resolves.toBe('timeout');
+        await dispatcher.onTestFileResult({ path: 'a' }, {});
+        await expect(runCompletePromise).resolves.toBe(undefined);
+      });
+
+      it('should warn about mismatching number of failed test suites if -R, --retries > 0', async () => {
+        config.testRunner.retries = 1;
+
+        await dispatcher.onRunComplete(testContexts, aggregatedResult);
+        expect(unsafe_conductEarlyTeardown).toHaveBeenCalled();
+        expect(console.warn).toHaveBeenCalledWith(expect.stringContaining('aborted'));
+      });
+    });
+  });
+});