Parcourir la source

Allow for more retries and add a summary reporter

Steven Silvester il y a 5 ans
Parent
commit
e2611b263b

+ 2 - 1
buildutils/src/ensure-repo.ts

@@ -39,7 +39,8 @@ const UNUSED: Dict<string[]> = {
     'identity-obj-proxy',
     'jest-raw-loader',
     'markdown-loader-jest',
-    'jest-junit'
+    'jest-junit',
+    'jest-summary-reporter'
   ],
   '@jupyterlab/test-csvviewer': ['csv-spectrum'],
   '@jupyterlab/vega5-extension': ['vega', 'vega-lite'],

+ 1 - 0
testutils/package.json

@@ -51,6 +51,7 @@
     "jest": "^25.2.3",
     "jest-junit": "^10.0.0",
     "jest-raw-loader": "^1.0.1",
+    "jest-summary-reporter": "^0.0.2",
     "json-to-html": "~0.1.2",
     "markdown-loader-jest": "^0.1.1",
     "node-fetch": "^2.6.0",

+ 371 - 0
testutils/src/common.ts

@@ -0,0 +1,371 @@
+import { simulate } from 'simulate-event';
+
+import { ServiceManager, Session } from '@jupyterlab/services';
+
+import { SessionContext } from '@jupyterlab/apputils';
+
+import { PromiseDelegate, UUID } from '@lumino/coreutils';
+
+import { ISignal, Signal } from '@lumino/signaling';
+import {
+  TextModelFactory,
+  DocumentRegistry,
+  Context
+} from '@jupyterlab/docregistry';
+
+import { INotebookModel, NotebookModelFactory } from '@jupyterlab/notebook';
+
+/**
+ * Test a single emission from a signal.
+ *
+ * @param signal - The signal we are listening to.
+ * @param find - An optional function to determine which emission to test,
+ * defaulting to the first emission.
+ * @param test - An optional function which contains the tests for the emission, and should throw an error if the tests fail.
+ * @param value - An optional value that the promise resolves to if the test is
+ * successful.
+ *
+ * @returns a promise that rejects if the function throws an error (e.g., if an
+ * expect test doesn't pass), and resolves otherwise.
+ *
+ * #### Notes
+ * The first emission for which the find function returns true will be tested in
+ * the test function. If the find function is not given, the first signal
+ * emission will be tested.
+ *
+ * You can test to see if any signal comes which matches a criteria by just
+ * giving a find function. You can test the very first signal by just giving a
+ * test function. And you can test the first signal matching the find criteria
+ * by giving both.
+ *
+ * The reason this function is asynchronous is so that the thing causing the
+ * signal emission (such as a websocket message) can be asynchronous.
+ */
+export async function testEmission<T, U, V>(
+  signal: ISignal<T, U>,
+  options: {
+    find?: (a: T, b: U) => boolean;
+    test?: (a: T, b: U) => void;
+    value?: V;
+  } = {}
+): Promise<V | undefined> {
+  const done = new PromiseDelegate<V | undefined>();
+  const object = {};
+  signal.connect((sender: T, args: U) => {
+    if (options.find?.(sender, args) ?? true) {
+      try {
+        Signal.disconnectReceiver(object);
+        if (options.test) {
+          options.test(sender, args);
+        }
+      } catch (e) {
+        done.reject(e);
+      }
+      done.resolve(options.value ?? undefined);
+    }
+  }, object);
+  return done.promise;
+}
+
+/**
+ * Expect a failure on a promise with the given message.
+ */
+export async function expectFailure(
+  promise: Promise<any>,
+  message?: string
+): Promise<void> {
+  let called = false;
+  try {
+    await promise;
+    called = true;
+  } catch (err) {
+    if (message && err.message.indexOf(message) === -1) {
+      throw Error(`Error "${message}" not in: "${err.message}"`);
+    }
+  }
+  if (called) {
+    throw Error(`Failure was not triggered, message was: ${message}`);
+  }
+}
+
+/**
+ * Do something in the future ensuring total ordering with respect to promises.
+ */
+export async function doLater(cb: () => void): Promise<void> {
+  await Promise.resolve(void 0);
+  cb();
+}
+
+/**
+ * Convert a signal into an array of promises.
+ *
+ * @param signal - The signal we are listening to.
+ * @param numberValues - The number of values to store.
+ *
+ * @returns a Promise that resolves with an array of `(sender, args)` pairs.
+ */
+export function signalToPromises<T, U>(
+  signal: ISignal<T, U>,
+  numberValues: number
+): Promise<[T, U]>[] {
+  const values: Promise<[T, U]>[] = new Array(numberValues);
+  const resolvers: Array<(value: [T, U]) => void> = new Array(numberValues);
+
+  for (let i = 0; i < numberValues; i++) {
+    values[i] = new Promise<[T, U]>(resolve => {
+      resolvers[i] = resolve;
+    });
+  }
+
+  let current = 0;
+  function slot(sender: T, args: U) {
+    resolvers[current++]([sender, args]);
+    if (current === numberValues) {
+      cleanup();
+    }
+  }
+  signal.connect(slot);
+
+  function cleanup() {
+    signal.disconnect(slot);
+  }
+  return values;
+}
+
+/**
+ * Convert a signal into a promise for the first emitted value.
+ *
+ * @param signal - The signal we are listening to.
+ *
+ * @returns a Promise that resolves with a `(sender, args)` pair.
+ */
+export function signalToPromise<T, U>(signal: ISignal<T, U>): Promise<[T, U]> {
+  return signalToPromises(signal, 1)[0];
+}
+
+/**
+ * Test to see if a promise is fulfilled.
+ *
+ * @param delay - optional delay in milliseconds before checking
+ * @returns true if the promise is fulfilled (either resolved or rejected), and
+ * false if the promise is still pending.
+ */
+export async function isFulfilled<T>(
+  p: PromiseLike<T>,
+  delay = 0
+): Promise<boolean> {
+  const x = Object.create(null);
+  let race: any;
+  if (delay > 0) {
+    race = sleep(delay, x);
+  } else {
+    race = x;
+  }
+  const result = await Promise.race([p, race]).catch(() => false);
+  return result !== x;
+}
+
+/**
+ * Convert a requestAnimationFrame into a Promise.
+ */
+export function framePromise(): Promise<void> {
+  const done = new PromiseDelegate<void>();
+  requestAnimationFrame(() => {
+    done.resolve(void 0);
+  });
+  return done.promise;
+}
+
+/**
+ * Return a promise that resolves in the given milliseconds with the given value.
+ */
+export function sleep<T>(milliseconds: number = 0, value?: T): Promise<T> {
+  return new Promise((resolve, reject) => {
+    setTimeout(() => {
+      resolve(value);
+    }, milliseconds);
+  });
+}
+
+/**
+ * Create a client session object.
+ */
+export async function createSessionContext(
+  options: Partial<SessionContext.IOptions> = {}
+): Promise<SessionContext> {
+  const manager = options.sessionManager ?? Private.getManager().sessions;
+  const specsManager = options.specsManager ?? Private.getManager().kernelspecs;
+
+  await Promise.all([manager.ready, specsManager.ready]);
+  return new SessionContext({
+    sessionManager: manager,
+    specsManager,
+    path: options.path ?? UUID.uuid4(),
+    name: options.name,
+    type: options.type,
+    kernelPreference: options.kernelPreference ?? {
+      shouldStart: true,
+      canStart: true,
+      name: specsManager.specs?.default
+    }
+  });
+}
+
+/**
+ * Create a session and return a session connection.
+ */
+export async function createSession(
+  options: Session.ISessionOptions
+): Promise<Session.ISessionConnection> {
+  const manager = Private.getManager().sessions;
+  await manager.ready;
+  return manager.startNew(options);
+}
+
+/**
+ * Create a context for a file.
+ */
+export function createFileContext(
+  path: string = UUID.uuid4() + '.txt',
+  manager: ServiceManager.IManager = Private.getManager()
+): Context<DocumentRegistry.IModel> {
+  const factory = Private.textFactory;
+  return new Context({ manager, factory, path });
+}
+
+export async function createFileContextWithKernel(
+  path: string = UUID.uuid4() + '.txt',
+  manager: ServiceManager.IManager = Private.getManager()
+) {
+  const factory = Private.textFactory;
+  const specsManager = manager.kernelspecs;
+  await specsManager.ready;
+
+  return new Context({
+    manager,
+    factory,
+    path,
+    kernelPreference: {
+      shouldStart: true,
+      canStart: true,
+      name: specsManager.specs?.default
+    }
+  });
+}
+
+/**
+ * Create and initialize context for a notebook.
+ */
+export async function initNotebookContext(
+  options: {
+    path?: string;
+    manager?: ServiceManager.IManager;
+    startKernel?: boolean;
+  } = {}
+): Promise<Context<INotebookModel>> {
+  const factory = Private.notebookFactory;
+
+  const manager = options.manager || Private.getManager();
+  const path = options.path || UUID.uuid4() + '.ipynb';
+  const startKernel =
+    options.startKernel === undefined ? false : options.startKernel;
+  await manager.ready;
+
+  const context = new Context({
+    manager,
+    factory,
+    path,
+    kernelPreference: {
+      shouldStart: startKernel,
+      canStart: startKernel,
+      shutdownOnDispose: true,
+      name: manager.kernelspecs.specs?.default
+    }
+  });
+  await context.initialize(true);
+
+  if (startKernel) {
+    await context.sessionContext.initialize();
+    await context.sessionContext.session?.kernel?.info;
+  }
+
+  return context;
+}
+
+/**
+ * Wait for a dialog to be attached to an element.
+ */
+export async function waitForDialog(
+  host: HTMLElement = document.body,
+  timeout: number = 250
+): Promise<void> {
+  const interval = 25;
+  const limit = Math.floor(timeout / interval);
+  for (let counter = 0; counter < limit; counter++) {
+    if (host.getElementsByClassName('jp-Dialog')[0]) {
+      return;
+    }
+    await sleep(interval);
+  }
+  throw new Error('Dialog not found');
+}
+
+/**
+ * Accept a dialog after it is attached by accepting the default button.
+ */
+export async function acceptDialog(
+  host: HTMLElement = document.body,
+  timeout: number = 250
+): Promise<void> {
+  await waitForDialog(host, timeout);
+
+  const node = host.getElementsByClassName('jp-Dialog')[0];
+
+  if (node) {
+    simulate(node as HTMLElement, 'keydown', { keyCode: 13 });
+  }
+}
+
+/**
+ * Dismiss a dialog after it is attached.
+ *
+ * #### Notes
+ * This promise will always resolve successfully.
+ */
+export async function dismissDialog(
+  host: HTMLElement = document.body,
+  timeout: number = 250
+): Promise<void> {
+  try {
+    await waitForDialog(host, timeout);
+  } catch (error) {
+    return; // Ignore calls to dismiss the dialog if there is no dialog.
+  }
+
+  const node = host.getElementsByClassName('jp-Dialog')[0];
+
+  if (node) {
+    simulate(node as HTMLElement, 'keydown', { keyCode: 27 });
+  }
+}
+
+/**
+ * A namespace for private data.
+ */
+namespace Private {
+  let manager: ServiceManager;
+
+  export const textFactory = new TextModelFactory();
+
+  export const notebookFactory = new NotebookModelFactory({});
+
+  /**
+   * Get or create the service manager singleton.
+   */
+  export function getManager(): ServiceManager {
+    if (!manager) {
+      manager = new ServiceManager({ standby: 'never' });
+    }
+    return manager;
+  }
+}

+ 51 - 0
testutils/src/flakyIt.ts

@@ -0,0 +1,51 @@
+// Adapted from https://github.com/bluzi/jest-retries/blob/01a9713a7379edcfd2d1bccec7c0fbc66d4602da/src/retry.js
+
+import 'jest';
+
+import { sleep } from './common';
+
+/**
+ * Run a test function.
+ *
+ * @param fn The function of the test
+ */
+async function runTest(fn: any): Promise<void> {
+  return new Promise((resolve, reject) => {
+    const result = fn((err: Error) => (err ? reject(err) : resolve()));
+
+    if (result && result.then) {
+      result.catch(reject).then(resolve);
+    } else {
+      resolve();
+    }
+  });
+}
+
+/**
+ * Run a flaky test with retries.
+ *
+ * @param name The name of the test
+ * @param fn The function of the test
+ * @param retries The number of retries
+ * @param wait The time to wait in milliseconds between retries
+ */
+export async function flakyIt(
+  name: string,
+  fn: any,
+  retries = 3,
+  wait = 1000
+): Promise<void> {
+  test(name, async () => {
+    let latestError;
+    for (let tries = 0; tries < retries; tries++) {
+      try {
+        await runTest(fn);
+        return;
+      } catch (error) {
+        latestError = error;
+        await sleep(wait);
+      }
+    }
+    throw latestError;
+  });
+}

+ 19 - 375
testutils/src/index.ts

@@ -1,384 +1,28 @@
 // Copyright (c) Jupyter Development Team.
 // Distributed under the terms of the Modified BSD License.
 
-import { simulate } from 'simulate-event';
-
-import { ServiceManager, Session } from '@jupyterlab/services';
-
-import { SessionContext } from '@jupyterlab/apputils';
-
-import { PromiseDelegate, UUID } from '@lumino/coreutils';
-
-import { ISignal, Signal } from '@lumino/signaling';
-import {
-  TextModelFactory,
-  DocumentRegistry,
-  Context
-} from '@jupyterlab/docregistry';
-
-import { INotebookModel, NotebookModelFactory } from '@jupyterlab/notebook';
-
 export { NBTestUtils } from './notebook-utils';
 
 export { defaultRenderMime } from './rendermime';
 
 export { JupyterServer } from './start_jupyter_server';
 
-const jestRetries = require('jest-retries');
-
-export const flakyIt: jest.It = jestRetries;
-
-/**
- * Test a single emission from a signal.
- *
- * @param signal - The signal we are listening to.
- * @param find - An optional function to determine which emission to test,
- * defaulting to the first emission.
- * @param test - An optional function which contains the tests for the emission, and should throw an error if the tests fail.
- * @param value - An optional value that the promise resolves to if the test is
- * successful.
- *
- * @returns a promise that rejects if the function throws an error (e.g., if an
- * expect test doesn't pass), and resolves otherwise.
- *
- * #### Notes
- * The first emission for which the find function returns true will be tested in
- * the test function. If the find function is not given, the first signal
- * emission will be tested.
- *
- * You can test to see if any signal comes which matches a criteria by just
- * giving a find function. You can test the very first signal by just giving a
- * test function. And you can test the first signal matching the find criteria
- * by giving both.
- *
- * The reason this function is asynchronous is so that the thing causing the
- * signal emission (such as a websocket message) can be asynchronous.
- */
-export async function testEmission<T, U, V>(
-  signal: ISignal<T, U>,
-  options: {
-    find?: (a: T, b: U) => boolean;
-    test?: (a: T, b: U) => void;
-    value?: V;
-  } = {}
-): Promise<V | undefined> {
-  const done = new PromiseDelegate<V | undefined>();
-  const object = {};
-  signal.connect((sender: T, args: U) => {
-    if (options.find?.(sender, args) ?? true) {
-      try {
-        Signal.disconnectReceiver(object);
-        if (options.test) {
-          options.test(sender, args);
-        }
-      } catch (e) {
-        done.reject(e);
-      }
-      done.resolve(options.value ?? undefined);
-    }
-  }, object);
-  return done.promise;
-}
-
-/**
- * Expect a failure on a promise with the given message.
- */
-export async function expectFailure(
-  promise: Promise<any>,
-  message?: string
-): Promise<void> {
-  let called = false;
-  try {
-    await promise;
-    called = true;
-  } catch (err) {
-    if (message && err.message.indexOf(message) === -1) {
-      throw Error(`Error "${message}" not in: "${err.message}"`);
-    }
-  }
-  if (called) {
-    throw Error(`Failure was not triggered, message was: ${message}`);
-  }
-}
-
-/**
- * Do something in the future ensuring total ordering with respect to promises.
- */
-export async function doLater(cb: () => void): Promise<void> {
-  await Promise.resolve(void 0);
-  cb();
-}
-
-/**
- * Convert a signal into an array of promises.
- *
- * @param signal - The signal we are listening to.
- * @param numberValues - The number of values to store.
- *
- * @returns a Promise that resolves with an array of `(sender, args)` pairs.
- */
-export function signalToPromises<T, U>(
-  signal: ISignal<T, U>,
-  numberValues: number
-): Promise<[T, U]>[] {
-  const values: Promise<[T, U]>[] = new Array(numberValues);
-  const resolvers: Array<(value: [T, U]) => void> = new Array(numberValues);
-
-  for (let i = 0; i < numberValues; i++) {
-    values[i] = new Promise<[T, U]>(resolve => {
-      resolvers[i] = resolve;
-    });
-  }
-
-  let current = 0;
-  function slot(sender: T, args: U) {
-    resolvers[current++]([sender, args]);
-    if (current === numberValues) {
-      cleanup();
-    }
-  }
-  signal.connect(slot);
-
-  function cleanup() {
-    signal.disconnect(slot);
-  }
-  return values;
-}
-
-/**
- * Convert a signal into a promise for the first emitted value.
- *
- * @param signal - The signal we are listening to.
- *
- * @returns a Promise that resolves with a `(sender, args)` pair.
- */
-export function signalToPromise<T, U>(signal: ISignal<T, U>): Promise<[T, U]> {
-  return signalToPromises(signal, 1)[0];
-}
-
-/**
- * Test to see if a promise is fulfilled.
- *
- * @param delay - optional delay in milliseconds before checking
- * @returns true if the promise is fulfilled (either resolved or rejected), and
- * false if the promise is still pending.
- */
-export async function isFulfilled<T>(
-  p: PromiseLike<T>,
-  delay = 0
-): Promise<boolean> {
-  const x = Object.create(null);
-  let race: any;
-  if (delay > 0) {
-    race = sleep(delay, x);
-  } else {
-    race = x;
-  }
-  const result = await Promise.race([p, race]).catch(() => false);
-  return result !== x;
-}
-
-/**
- * Convert a requestAnimationFrame into a Promise.
- */
-export function framePromise(): Promise<void> {
-  const done = new PromiseDelegate<void>();
-  requestAnimationFrame(() => {
-    done.resolve(void 0);
-  });
-  return done.promise;
-}
-
-/**
- * Return a promise that resolves in the given milliseconds with the given value.
- */
-export function sleep<T>(milliseconds: number = 0, value?: T): Promise<T> {
-  return new Promise((resolve, reject) => {
-    setTimeout(() => {
-      resolve(value);
-    }, milliseconds);
-  });
-}
-
-/**
- * Create a client session object.
- */
-export async function createSessionContext(
-  options: Partial<SessionContext.IOptions> = {}
-): Promise<SessionContext> {
-  const manager = options.sessionManager ?? Private.getManager().sessions;
-  const specsManager = options.specsManager ?? Private.getManager().kernelspecs;
-
-  await Promise.all([manager.ready, specsManager.ready]);
-  return new SessionContext({
-    sessionManager: manager,
-    specsManager,
-    path: options.path ?? UUID.uuid4(),
-    name: options.name,
-    type: options.type,
-    kernelPreference: options.kernelPreference ?? {
-      shouldStart: true,
-      canStart: true,
-      name: specsManager.specs?.default
-    }
-  });
-}
-
-/**
- * Create a session and return a session connection.
- */
-export async function createSession(
-  options: Session.ISessionOptions
-): Promise<Session.ISessionConnection> {
-  const manager = Private.getManager().sessions;
-  await manager.ready;
-  return manager.startNew(options);
-}
-
-/**
- * Create a context for a file.
- */
-export function createFileContext(
-  path: string = UUID.uuid4() + '.txt',
-  manager: ServiceManager.IManager = Private.getManager()
-): Context<DocumentRegistry.IModel> {
-  const factory = Private.textFactory;
-  return new Context({ manager, factory, path });
-}
-
-export async function createFileContextWithKernel(
-  path: string = UUID.uuid4() + '.txt',
-  manager: ServiceManager.IManager = Private.getManager()
-) {
-  const factory = Private.textFactory;
-  const specsManager = manager.kernelspecs;
-  await specsManager.ready;
-
-  return new Context({
-    manager,
-    factory,
-    path,
-    kernelPreference: {
-      shouldStart: true,
-      canStart: true,
-      name: specsManager.specs?.default
-    }
-  });
-}
-
-/**
- * Create and initialize context for a notebook.
- */
-export async function initNotebookContext(
-  options: {
-    path?: string;
-    manager?: ServiceManager.IManager;
-    startKernel?: boolean;
-  } = {}
-): Promise<Context<INotebookModel>> {
-  const factory = Private.notebookFactory;
-
-  const manager = options.manager || Private.getManager();
-  const path = options.path || UUID.uuid4() + '.ipynb';
-  const startKernel =
-    options.startKernel === undefined ? false : options.startKernel;
-  await manager.ready;
-
-  const context = new Context({
-    manager,
-    factory,
-    path,
-    kernelPreference: {
-      shouldStart: startKernel,
-      canStart: startKernel,
-      shutdownOnDispose: true,
-      name: manager.kernelspecs.specs?.default
-    }
-  });
-  await context.initialize(true);
-
-  if (startKernel) {
-    await context.sessionContext.initialize();
-    await context.sessionContext.session?.kernel?.info;
-  }
-
-  return context;
-}
-
-/**
- * Wait for a dialog to be attached to an element.
- */
-export async function waitForDialog(
-  host: HTMLElement = document.body,
-  timeout: number = 250
-): Promise<void> {
-  const interval = 25;
-  const limit = Math.floor(timeout / interval);
-  for (let counter = 0; counter < limit; counter++) {
-    if (host.getElementsByClassName('jp-Dialog')[0]) {
-      return;
-    }
-    await sleep(interval);
-  }
-  throw new Error('Dialog not found');
-}
-
-/**
- * Accept a dialog after it is attached by accepting the default button.
- */
-export async function acceptDialog(
-  host: HTMLElement = document.body,
-  timeout: number = 250
-): Promise<void> {
-  await waitForDialog(host, timeout);
-
-  const node = host.getElementsByClassName('jp-Dialog')[0];
-
-  if (node) {
-    simulate(node as HTMLElement, 'keydown', { keyCode: 13 });
-  }
-}
-
-/**
- * Dismiss a dialog after it is attached.
- *
- * #### Notes
- * This promise will always resolve successfully.
- */
-export async function dismissDialog(
-  host: HTMLElement = document.body,
-  timeout: number = 250
-): Promise<void> {
-  try {
-    await waitForDialog(host, timeout);
-  } catch (error) {
-    return; // Ignore calls to dismiss the dialog if there is no dialog.
-  }
-
-  const node = host.getElementsByClassName('jp-Dialog')[0];
-
-  if (node) {
-    simulate(node as HTMLElement, 'keydown', { keyCode: 27 });
-  }
-}
-
-/**
- * A namespace for private data.
- */
-namespace Private {
-  let manager: ServiceManager;
-
-  export const textFactory = new TextModelFactory();
-
-  export const notebookFactory = new NotebookModelFactory({});
-
-  /**
-   * Get or create the service manager singleton.
-   */
-  export function getManager(): ServiceManager {
-    if (!manager) {
-      manager = new ServiceManager({ standby: 'never' });
-    }
-    return manager;
-  }
-}
+export {
+  testEmission,
+  expectFailure,
+  signalToPromises,
+  signalToPromise,
+  isFulfilled,
+  framePromise,
+  sleep,
+  createSessionContext,
+  createSession,
+  createFileContext,
+  createFileContextWithKernel,
+  initNotebookContext,
+  waitForDialog,
+  acceptDialog,
+  dismissDialog
+} from './common';
+
+export { flakyIt } from './flakyIt';

+ 1 - 1
testutils/src/jest-config.ts

@@ -14,7 +14,7 @@ module.exports = function(baseDir: string) {
     setupFiles: ['@jupyterlab/testutils/lib/jest-shim.js'],
     testPathIgnorePatterns: ['/lib/', '/node_modules/'],
     moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
-    reporters: ['default', 'jest-junit'],
+    reporters: ['default', 'jest-junit', 'jest-summary-reporter'],
     coverageReporters: ['json', 'lcov', 'text', 'html'],
     coverageDirectory: path.join(baseDir, 'coverage'),
     testRegex: '/test/.*.spec.ts[x]?$',

+ 7 - 0
yarn.lock

@@ -10706,6 +10706,13 @@ jest-snapshot@^25.2.3:
     pretty-format "^25.2.3"
     semver "^6.3.0"
 
+jest-summary-reporter@^0.0.2:
+  version "0.0.2"
+  resolved "https://registry.yarnpkg.com/jest-summary-reporter/-/jest-summary-reporter-0.0.2.tgz#53b9997b56f343a0dd9af24199c68d371e01f534"
+  integrity sha512-rZ3ThO57l+ZJCxF74cXIGQU3cV9I7bSBe1ElBp0taE3x2JghgD69bNCKt0LvpVQX5azTRHG7LmcjIpwriVnTng==
+  dependencies:
+    chalk "^2.4.1"
+
 jest-util@^25.2.3:
   version "25.2.3"
   resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-25.2.3.tgz#0abf95a1d6b96f2de5a3ecd61b36c40a182dc256"