Repository: facebook/react
Pull request: [devtools][tests] add support for @gate pragma

Adds support for `// @gate` pragma in devtools tests
Adds a couple gate test assertions to ensure they continue to work

Changed files:
filename: .eslintrc.js
patch: @@ -381,6 +381,13 @@ module.exports = {
         'jest/valid-expect-in-promise': ERROR,
       },
     },
+    {
+      // disable no focused tests for test setup helper files even if they are inside __tests__ directory
+      files: ['**/setupTests.js'],
+      rules: {
+        'jest/no-focused-tests': OFF,
+      },
+    },
     {
       files: [
         '**/__tests__/**/*.js',

filename: packages/react-devtools-shared/src/__tests__/gate-test.js
patch: @@ -0,0 +1,18 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+describe('gate', () => {
+  //@gate false
+  it('should expect an error for this test', () => {
+    throw new Error('This test should fail');
+  });
+
+  //@gate true
+  it('should not an error for this test', () => {});
+});

filename: packages/react-devtools-shared/src/__tests__/setupTests.js
patch: @@ -13,6 +13,7 @@ import type {
   BackendBridge,
   FrontendBridge,
 } from 'react-devtools-shared/src/bridge';
+const {getTestFlags} = require('../../../../scripts/jest/TestFlags');
 
 // Argument is serialized when passed from jest-cli script through to setupTests.
 const compactConsole = process.env.compactConsole === 'true';
@@ -32,6 +33,76 @@ if (compactConsole) {
   global.console = new CustomConsole(process.stdout, process.stderr, formatter);
 }
 
+const expectTestToFail = async (callback, error) => {
+  if (callback.length > 0) {
+    throw Error(
+      'Gated test helpers do not support the `done` callback. Return a ' +
+        'promise instead.',
+    );
+  }
+  try {
+    const maybePromise = callback();
+    if (
+      maybePromise !== undefined &&
+      maybePromise !== null &&
+      typeof maybePromise.then === 'function'
+    ) {
+      await maybePromise;
+    }
+  } catch (testError) {
+    return;
+  }
+  throw error;
+};
+
+const gatedErrorMessage = 'Gated test was expected to fail, but it passed.';
+global._test_gate = (gateFn, testName, callback) => {
+  let shouldPass;
+  try {
+    const flags = getTestFlags();
+    shouldPass = gateFn(flags);
+  } catch (e) {
+    test(testName, () => {
+      throw e;
+    });
+    return;
+  }
+  if (shouldPass) {
+    test(testName, callback);
+  } else {
+    const error = new Error(gatedErrorMessage);
+    Error.captureStackTrace(error, global._test_gate);
+    test(`[GATED, SHOULD FAIL] ${testName}`, () =>
+      expectTestToFail(callback, error));
+  }
+};
+global._test_gate_focus = (gateFn, testName, callback) => {
+  let shouldPass;
+  try {
+    const flags = getTestFlags();
+    shouldPass = gateFn(flags);
+  } catch (e) {
+    test.only(testName, () => {
+      throw e;
+    });
+    return;
+  }
+  if (shouldPass) {
+    test.only(testName, callback);
+  } else {
+    const error = new Error(gatedErrorMessage);
+    Error.captureStackTrace(error, global._test_gate_focus);
+    test.only(`[GATED, SHOULD FAIL] ${testName}`, () =>
+      expectTestToFail(callback, error));
+  }
+};
+
+// Dynamic version of @gate pragma
+global.gate = fn => {
+  const flags = getTestFlags();
+  return fn(flags);
+};
+
 beforeEach(() => {
   global.mockClipboardCopy = jest.fn();