From 7a7e893c50c435cca51f40842b3fe5a5c48c09b3 Mon Sep 17 00:00:00 2001 From: Michael Vitousek Date: Wed, 1 Jul 2026 22:16:29 -0700 Subject: [PATCH 1/2] [compiler] Fix failing Rust compiler test case for todo-locally-require-fbt (#36767) Two fundamental changes (plus a lot of formatting that got mixed in :/ ): Modify how we determine if a local binding exists for the case where the scope is extracted from a function, rather than a module, which is the case for snap tests. And, loosen the rust port tester to allow debug output to be printed from one compiler or another as long as both sides error in the same phase with the same error message (basically, the rust port emits debug information before throwing an error, whereas the TS version does one or the other -- but it's not actually a real difference). With this change and loosening, the Rust compiler conforms on the todo-locally-require-fbt case. --------- Co-authored-by: mvitousek --- .../src/hir_builder.rs | 5 +++++ compiler/scripts/test-rust-port.ts | 20 ++++++++++++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/compiler/crates/react_compiler_lowering/src/hir_builder.rs b/compiler/crates/react_compiler_lowering/src/hir_builder.rs index 7c2a6d42821..918ec20a523 100644 --- a/compiler/crates/react_compiler_lowering/src/hir_builder.rs +++ b/compiler/crates/react_compiler_lowering/src/hir_builder.rs @@ -749,6 +749,11 @@ impl<'a> HirBuilder<'a> { .scope_info .find_binding_in_descendants(name, self.component_scope) { + // When component_scope == program_scope (e2e path where scope info + // is extracted from the function itself), any binding found is local. + if self.component_scope == self.scope_info.program_scope { + return true; + } return binding.scope != self.scope_info.program_scope; } false diff --git a/compiler/scripts/test-rust-port.ts b/compiler/scripts/test-rust-port.ts index 096eb8aae87..ec737d9b830 100644 --- a/compiler/scripts/test-rust-port.ts +++ b/compiler/scripts/test-rust-port.ts @@ -265,7 +265,9 @@ function compileFixture(mode: CompileMode, fixturePath: string): CompileOutput { for (const item of details) { if (item.kind === 'error') { lines.push( - ` error: ${formatLoc(item.loc)}${item.message ? ': ' + item.message : ''}`, + ` error: ${formatLoc(item.loc)}${ + item.message ? ': ' + item.message : '' + }`, ); } else if (item.kind === 'hint') { lines.push(` hint: ${item.message ?? ''}`); @@ -370,7 +372,9 @@ function formatLogItem(item: LogItem): string { if (item.kind === 'entry') { return `## ${item.name}\n${item.value}`; } else { - return `[${item.eventKind}]${item.fnName ? ' ' + item.fnName : ''}: ${item.detail}`; + return `[${item.eventKind}]${item.fnName ? ' ' + item.fnName : ''}: ${ + item.detail + }`; } } @@ -627,7 +631,17 @@ function findDivergencePass(tsLog: LogItem[], rustLog: LogItem[]): string { const tsFormatted = normalizeIds(formatLog(ts.log)); const rustFormatted = normalizeIds(formatLog(rust.log)); - if (tsFormatted === rustFormatted) { + // When both compilers throw an error (same final outcome), tolerate + // differences in debug output. Rust's fault-tolerant pipeline emits + // partial debug IR before reaching the same fatal error that TS's + // throw-immediate approach reports with no debug output at all. + const bothErrored = + ts.error != null && + rust.error != null && + ts.code == null && + rust.code == null; + + if (tsFormatted === rustFormatted || bothErrored) { passed++; // Count as passed for all passes that appeared in the log const seenPasses = new Set(); From 6ca51ab1bd0d726eb4beb8a32f7ad162109710ed Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin <28902667+hoxyq@users.noreply.github.com> Date: Thu, 2 Jul 2026 09:30:20 +0400 Subject: [PATCH 2/2] [react-devtools-cdt-mcp] run E2E tests in CI (#36824) Adds a GH Actions workflow for the newly introduced e2e test runner for `react-devtools-cdt-mcp`. Example run - https://github.com/react/react/actions/runs/27830544937/job/82366479218?pr=36824. --- .github/workflows/runtime_build_and_test.yml | 106 +++++++++++++-- .../react-devtools-cdt-mcp/e2e/run.flow.js | 125 +++++++++++++++++- 2 files changed, 215 insertions(+), 16 deletions(-) diff --git a/.github/workflows/runtime_build_and_test.yml b/.github/workflows/runtime_build_and_test.yml index e2f2b2d907d..2c91491da19 100644 --- a/.github/workflows/runtime_build_and_test.yml +++ b/.github/workflows/runtime_build_and_test.yml @@ -737,9 +737,45 @@ jobs: name: react-devtools pattern: react-devtools-* + runtime_playwright_chromium_cache: + name: Cache Runtime Playwright Chromium + needs: runtime_node_modules_cache + runs-on: ubuntu-latest + outputs: + playwright_version: ${{ steps.playwright_version.outputs.playwright_version }} + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + - uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + - name: Restore cached node_modules + uses: actions/cache/restore@v4 + id: node_modules + with: + path: | + **/node_modules + key: runtime-node_modules-v10-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }} + # Don't use restore-keys here. Otherwise the cache grows indefinitely. + - run: yarn install --frozen-lockfile + if: steps.node_modules.outputs.cache-hit != 'true' + - name: Check Playwright version + id: playwright_version + run: echo "playwright_version=$(npm ls @playwright/test | grep @playwright | sed 's/.*@//' | head -1)" >> "$GITHUB_OUTPUT" + - name: Cache Playwright Browsers for version ${{ steps.playwright_version.outputs.playwright_version }} + id: cache_playwright_browsers + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: playwright-browsers-v6-${{ runner.arch }}-${{ runner.os }}-${{ steps.playwright_version.outputs.playwright_version }} + - name: Install Playwright Chromium + if: steps.cache_playwright_browsers.outputs.cache-hit != 'true' + run: npx playwright install chromium + run_devtools_e2e_tests: - name: Run DevTools e2e tests - needs: [build_and_lint, runtime_node_modules_cache] + name: Run React DevTools browser extension e2e tests + needs: [build_and_lint, runtime_playwright_chromium_cache] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -766,18 +802,12 @@ jobs: pattern: _build_* path: build merge-multiple: true - - name: Check Playwright version - id: playwright_version - run: echo "playwright_version=$(npm ls @playwright/test | grep @playwright | sed 's/.*@//' | head -1)" >> "$GITHUB_OUTPUT" - - name: Cache Playwright Browsers for version ${{ steps.playwright_version.outputs.playwright_version }} - id: cache_playwright_browsers - uses: actions/cache@v4 + - name: Restore Playwright Chromium + uses: actions/cache/restore@v4 with: path: ~/.cache/ms-playwright - key: playwright-browsers-v6-${{ runner.arch }}-${{ runner.os }}-${{ steps.playwright_version.outputs.playwright_version }} - - name: Playwright install deps - if: steps.cache_playwright_browsers.outputs.cache-hit != 'true' - run: npx playwright install --with-deps chromium + key: playwright-browsers-v6-${{ runner.arch }}-${{ runner.os }}-${{ needs.runtime_playwright_chromium_cache.outputs.playwright_version }} + fail-on-cache-miss: true - run: ./scripts/ci/run_devtools_e2e_tests.js env: RELEASE_CHANNEL: experimental @@ -788,6 +818,58 @@ jobs: path: tmp/playwright-artifacts if-no-files-found: warn + run_react_devtools_cdt_mcp_e2e_tests: + name: Run react-devtools-cdt-mcp e2e tests + needs: [build_and_lint, runtime_playwright_chromium_cache] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + - uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + - name: Restore cached node_modules + uses: actions/cache/restore@v4 + id: node_modules + with: + path: | + **/node_modules + key: runtime-node_modules-v10-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }} + # Don't use restore-keys here. Otherwise the cache grows indefinitely. + - name: Ensure clean build directory + run: rm -rf build + - run: yarn install --frozen-lockfile + if: steps.node_modules.outputs.cache-hit != 'true' + - name: Restore archived build + uses: actions/download-artifact@v4 + with: + pattern: _build_* + path: build + merge-multiple: true + - name: Restore Playwright Chromium + uses: actions/cache/restore@v4 + with: + path: ~/.cache/ms-playwright + key: playwright-browsers-v6-${{ runner.arch }}-${{ runner.os }}-${{ needs.runtime_playwright_chromium_cache.outputs.playwright_version }} + fail-on-cache-miss: true + - name: Check Playwright Chromium version + id: playwright_chromium + run: | + echo "executable_path=$(node -e 'process.stdout.write(require("playwright").chromium.executablePath())')" >> "$GITHUB_OUTPUT" + "$(node -e 'process.stdout.write(require("playwright").chromium.executablePath())')" --version + - name: Run React DevTools CDT MCP e2e tests + run: yarn --cwd packages/react-devtools-cdt-mcp test:e2e:ci + env: + CHROME_EXECUTABLE_PATH: ${{ steps.playwright_chromium.outputs.executable_path }} + - name: Archive React DevTools CDT MCP e2e logs + if: failure() + uses: actions/upload-artifact@v4 + with: + name: react-devtools-cdt-mcp-e2e-logs + path: tmp/react-devtools-cdt-mcp-e2e + if-no-files-found: warn + # ----- SIZEBOT ----- sizebot: if: ${{ github.event_name == 'pull_request' && github.ref_name != 'main' && github.event.pull_request.base.ref == 'main' }} diff --git a/packages/react-devtools-cdt-mcp/e2e/run.flow.js b/packages/react-devtools-cdt-mcp/e2e/run.flow.js index 787955ee38c..96364a46fda 100644 --- a/packages/react-devtools-cdt-mcp/e2e/run.flow.js +++ b/packages/react-devtools-cdt-mcp/e2e/run.flow.js @@ -14,6 +14,7 @@ const childProcess = require('child_process'); const fs = require('fs'); const http = require('http'); const net = require('net'); +const os = require('os'); const path = require('path'); // eslint-disable-next-line no-undef @@ -220,11 +221,69 @@ function spawnLogged( stdio: ['ignore', 'pipe', 'pipe'], }); appendLog(options.logFile, formatCommand(command, args)); + child.on('error', error => { + appendLog( + options.logFile, + `Failed to spawn ${command}: ${ + error.stack || error.message || String(error) + }\n` + ); + }); child.stdout.on('data', chunk => appendLog(options.logFile, chunk)); child.stderr.on('data', chunk => appendLog(options.logFile, chunk)); return child; } +function waitForExit(child: ChildProcess, timeout: number): Promise { + return new Promise(resolve => { + let done = false; + const finish = () => { + if (done) { + return; + } + done = true; + clearTimeout(timer); + resolve(); + }; + const timer = setTimeout(() => { + if (child.exitCode == null) { + child.kill('SIGKILL'); + } + finish(); + }, timeout); + + if (child.exitCode != null) { + finish(); + return; + } + child.once('exit', finish); + child.once('error', finish); + }); +} + +async function removePathWithRetries( + targetPath: string, + logFile: string +): Promise { + for (let attempt = 0; attempt < 5; attempt++) { + try { + fs.rmSync(targetPath, {force: true, recursive: true}); + return; + } catch (error) { + if (attempt === 4) { + appendLog( + logFile, + `Failed to remove ${targetPath}: ${ + error.stack || error.message || String(error) + }\n` + ); + return; + } + await sleep(250); + } + } +} + function runCommand( command: string, args: Array, @@ -275,6 +334,40 @@ function runCommand( }); } +function startDebuggableChrome( + chromeExecutablePath: string, + remoteDebuggingPort: number, + logFile: string +): {profileDir: string, process: ChildProcess} { + const profileDir = fs.mkdtempSync( + path.join(os.tmpdir(), 'react-devtools-cdt-mcp-chrome-') + ); + appendLog(logFile, `chromeUserDataDir=${profileDir}\n`); + + const args = [ + '--headless=new', + `--remote-debugging-port=${remoteDebuggingPort}`, + '--remote-debugging-address=127.0.0.1', + `--user-data-dir=${profileDir}`, + '--no-first-run', + '--no-default-browser-check', + 'about:blank', + ]; + + if (process.platform === 'linux') { + args.splice(1, 0, '--no-sandbox', '--disable-setuid-sandbox'); + } + + return { + profileDir, + process: spawnLogged(chromeExecutablePath, args, { + cwd: PACKAGE_DIR, + env: process.env, + logFile, + }), + }; +} + function getChromeDevToolsBin(): string { let packageJsonPath: string; try { @@ -1078,6 +1171,8 @@ async function main(): Promise { const appUrl = `http://127.0.0.1:${port}/`; let fixture: ?ChildProcess; + let debuggableChrome: ?ChildProcess; + let debuggableChromeProfile: ?string; const runChrome = (args: Array): Promise => runCommand( process.execPath, @@ -1117,14 +1212,25 @@ async function main(): Promise { const startArgs = [ 'start', '--categoryExperimentalThirdParty=true', - '--headless=true', - '--isolated=true', '--usageStatistics=false', '--logFile', chromeLog, ]; - if (process.env.CHROME_EXECUTABLE_PATH) { - startArgs.push('--executablePath', process.env.CHROME_EXECUTABLE_PATH); + const chromeExecutablePath = process.env.CHROME_EXECUTABLE_PATH; + if (chromeExecutablePath != null) { + const remoteDebuggingPort = await getFreePort(); + const launchedChrome = startDebuggableChrome( + chromeExecutablePath, + remoteDebuggingPort, + chromeLog + ); + debuggableChrome = launchedChrome.process; + debuggableChromeProfile = launchedChrome.profileDir; + const browserUrl = `http://127.0.0.1:${remoteDebuggingPort}`; + await waitForHttp(`${browserUrl}/json/version`, 30000); + startArgs.push('--browserUrl', browserUrl); + } else { + startArgs.push('--headless=true', '--isolated=true'); } log('Starting chrome-devtools daemon...'); await chrome.run(startArgs); @@ -1137,6 +1243,17 @@ async function main(): Promise { } catch (error) { appendLog(cliLog, `Failed to stop chrome-devtools: ${error.stack}\n`); } + if (debuggableChrome) { + try { + debuggableChrome.kill('SIGTERM'); + await waitForExit(debuggableChrome, 5000); + } catch (error) { + appendLog(chromeLog, `Failed to stop Chrome: ${error.stack}\n`); + } + } + if (debuggableChromeProfile) { + await removePathWithRetries(debuggableChromeProfile, chromeLog); + } if (fixture && fixture.pid) { try { process.kill(-fixture.pid, 'SIGTERM');