Repository: facebook/react
Pull request: Circle CI: Run DevTools Playwright e2e tests

Let's run the new DevTools e2e tests on Circle CI. They seem solid but if this turns out to be flaky or cause problems we can disable them.

Resolves #22646.

### Successful run

https://user-images.githubusercontent.com/29597/147143392-8454fb82-6a32-4bf6-bcfe-c3fa65716e93.mov

### Failed run

https://user-images.githubusercontent.com/29597/147143407-98d1f219-b3db-44f4-8f56-18fbb524965c.mov


Changed files:
filename: .circleci/config.yml
patch: @@ -200,6 +200,28 @@ jobs:
       - store_artifacts:
           path: ./build/devtools.tgz
 
+  run_devtools_e2e_tests:
+    docker: *docker
+    environment: *environment
+    steps:
+      - checkout
+      - attach_workspace:
+          at: .
+      - run: yarn workspaces info | head -n -1 > workspace_info.txt
+      - *restore_node_modules
+      - run:
+          name: Install Packages
+          command: yarn --frozen-lockfile --cache-folder ~/.cache/yarn
+      - run:
+          name: Playwright install deps
+          command: |
+            npx playwright install
+            sudo npx playwright install-deps
+      - run:
+          environment:
+            RELEASE_CHANNEL: experimental
+          command: ./scripts/circleci/run_devtools_e2e_tests.js
+
   yarn_lint_build:
     docker: *docker
     environment: *environment
@@ -452,6 +474,9 @@ workflows:
       - build_devtools_and_process_artifacts:
           requires:
             - yarn_build_combined
+      - run_devtools_e2e_tests:
+          requires:
+            - build_devtools_and_process_artifacts
 
   fuzz_tests:
     unless: << pipeline.parameters.prerelease_commit_sha >>

filename: scripts/circleci/run_devtools_e2e_tests.js
patch: @@ -0,0 +1,165 @@
+#!/usr/bin/env node
+
+'use strict';
+
+const {spawn} = require('child_process');
+const {join} = require('path');
+
+const ROOT_PATH = join(__dirname, '..', '..');
+
+const inlinePackagePath = join(ROOT_PATH, 'packages', 'react-devtools-inline');
+const shellPackagePath = join(ROOT_PATH, 'packages', 'react-devtools-shell');
+
+let buildProcess = null;
+let serverProcess = null;
+let testProcess = null;
+
+function format(loggable) {
+  return `${loggable}`
+    .split('\n')
+    .filter(line => {
+      return line.trim() !== '';
+    })
+    .map(line => `  ${line}`)
+    .join('\n');
+}
+
+function logBright(loggable) {
+  console.log(`\x1b[1m${loggable}\x1b[0m`);
+}
+
+function logDim(loggable) {
+  const formatted = format(loggable, 2);
+  if (formatted !== '') {
+    console.log(`\x1b[2m${formatted}\x1b[0m`);
+  }
+}
+
+function logError(loggable) {
+  const formatted = format(loggable, 2);
+  if (formatted !== '') {
+    console.error(`\x1b[31m${formatted}\x1b[0m`);
+  }
+}
+
+function buildInlinePackage() {
+  logBright('Building inline packages');
+
+  buildProcess = spawn('yarn', ['build'], {cwd: inlinePackagePath});
+  buildProcess.stdout.on('data', data => {
+    logDim(data);
+  });
+  buildProcess.stderr.on('data', data => {
+    if (`${data}`.includes('Warning')) {
+      logDim(data);
+    } else {
+      logError(`Error:\n${data}`);
+
+      exitWithCode(1);
+    }
+  });
+  buildProcess.on('close', code => {
+    logBright('Inline package built');
+
+    runTestShell();
+  });
+}
+
+function runTestShell() {
+  const timeoutID = setTimeout(() => {
+    // Assume the test shell server failed to start.
+    logError('Testing shell server failed to start');
+    exitWithCode(1);
+  }, 30000);
+
+  logBright('Starting testing shell server');
+
+  serverProcess = spawn('yarn', ['start'], {cwd: shellPackagePath});
+  serverProcess.stdout.on('data', data => {
+    if (`${data}`.includes('Compiled successfully.')) {
+      logBright('Testing shell server running');
+
+      clearTimeout(timeoutID);
+
+      runEndToEndTests();
+    }
+  });
+  serverProcess.stderr.on('data', data => {
+    if (`${data}`.includes('EADDRINUSE')) {
+      // Something is occuprying this port;
+      // We could kill the process and restart but probably better to prompt the user to do this.
+
+      logError('Free up the port and re-run tests:');
+      logBright('  kill -9 $(lsof -ti:8080)');
+
+      exitWithCode(1);
+    } else if (`${data}`.includes('ERROR')) {
+      logError(`Error:\n${data}`);
+
+      exitWithCode(1);
+    } else {
+      // Non-fatal stuff like Babel optimization warnings etc.
+      logDim(data);
+    }
+  });
+}
+
+async function runEndToEndTests() {
+  logBright('Running e2e tests');
+
+  testProcess = spawn('yarn', ['test:e2e'], {cwd: inlinePackagePath});
+  testProcess.stdout.on('data', data => {
+    // Log without formatting because Playwright applies its own formatting.
+    const formatted = format(data);
+    if (formatted !== '') {
+      console.log(formatted);
+    }
+  });
+  testProcess.stderr.on('data', data => {
+    // Log without formatting because Playwright applies its own formatting.
+    const formatted = format(data);
+    if (formatted !== '') {
+      console.error(formatted);
+    }
+
+    exitWithCode(1);
+  });
+  testProcess.on('close', code => {
+    logBright(`Tests completed with code: ${code}`);
+
+    exitWithCode(code);
+  });
+}
+
+function exitWithCode(code) {
+  if (buildProcess !== null) {
+    try {
+      logBright('Shutting down build process');
+      buildProcess.kill();
+    } catch (error) {
+      logError(error);
+    }
+  }
+
+  if (serverProcess !== null) {
+    try {
+      logBright('Shutting down shell server process');
+      serverProcess.kill();
+    } catch (error) {
+      logError(error);
+    }
+  }
+
+  if (testProcess !== null) {
+    try {
+      logBright('Shutting down test process');
+      testProcess.kill();
+    } catch (error) {
+      logError(error);
+    }
+  }
+
+  process.exit(code);
+}
+
+buildInlinePackage();