Can lead to race conditions; consider Promises or async/await for better control
return new Promise(resolve => setTimeout(resolve, ms));
1/**2 * Copyright (c) Meta Platforms, Inc. and affiliates.3 *4 * This source code is licensed under the MIT license found in the5 * LICENSE file in the root directory of this source tree.6 *7 * @flow strict-local8 */910'use strict';1112const assert = require('assert');13const childProcess = require('child_process');14const fs = require('fs');15const http = require('http');16const net = require('net');17const os = require('os');18const path = require('path');1920// eslint-disable-next-line no-undef21type ChildProcess = child_process$ChildProcess;22type JSONSchemaObject = {23 type?: string,24 properties?: {[string]: JSONSchemaObject, ...},25 required?: Array<string>,26 ...27};28type CommandResult = {29 stdout: string,30 stderr: string,31};32type SpawnOptions = {33 cwd: string,34 detached?: boolean,35 env: {[string]: string | void},36 logFile: string,37};38type CommandOptions = {39 cwd: string,40 env: {[string]: string | void},41 logFile: string,42 timeout?: number,43};44type Chrome = {45 run: (args: Array<string>) => Promise<CommandResult>,46 json: (args: Array<string>) => Promise<mixed>,47};48type TreeNode = {49 uid: string,50 type: string,51 name: string,52 key?: string | null,53 firstChild?: string | null,54 nextSibling?: string | null,55};56type SnapshotNode = {57 id?: string,58 role?: string,59 name?: string,60 children?: Array<SnapshotNode>,61};62type ToolDefinition = {63 name: string,64 description: string,65 inputSchema: JSONSchemaObject,66};67type ToolGroup = {68 name: string,69 description: string,70 tools: Array<ToolDefinition>,71};72type ToolDiscovery = {73 thirdPartyDeveloperTools?: ToolGroup | Array<ToolGroup>,74 ...75};76type SourceResult = {77 source: null | {78 name: string,79 fileName: string,80 line: number,81 column: number,82 ...83 },84 ...85};86type PageReadiness = {87 hasApp: boolean,88 hasHook: boolean,89};90type ComponentDetails = {91 name: string,92 type: string,93 hooks: Array<{name: string, ...}>,94 ...95};96type SearchResult = {97 page: number,98 pageSize: number,99 totalCount: number,100 totalPages: number,101 results: Array<{name: string, ...}>,102 ...103};104type DomLookupResult = {105 type: string,106 name: string,107 ...108};109type OwnersStackResult = {110 stack: string,111 ...112};113type ComponentBranchEntry = {114 uid: string,115 name: string,116 type: string,117 ...118};119type ErrorPayload = {120 error: string,121 ...122};123type StartProfilingResult = {124 status: string,125 traceName: string,126 ...127};128type StopProfilingResult = {129 status: string,130 traceName: string,131 commits: number,132 ...133};134type TraceOverviewCommit = {135 commit: number,136 componentsChanged: number,137 ...138};139type CommitReport = {140 components: Array<{name: string, ...}>,141 ...142};143144const TOOL_NAMES = [145 'react_get_component_tree',146 'react_get_component_by_uid',147 'react_get_component_by_dom_element',148 'react_find_components',149 'react_get_component_source',150 'react_get_owner_stack_trace',151 'react_get_parent_stack',152 'react_get_owner_stack',153 'react_start_profiling',154 'react_stop_profiling',155 'react_get_trace_overview',156 'react_get_commit_report',157];158159const PACKAGE_DIR = path.resolve(__dirname, '..');160const REPO_ROOT = path.resolve(PACKAGE_DIR, '..', '..');161const FIXTURE_DIR = path.join(PACKAGE_DIR, 'fixtures', 'app');162const BUILT_MODULES_DIR = path.join(REPO_ROOT, 'build', 'oss-experimental');163const LOG_DIR =164 process.env.E2E_LOG_DIR ||165 path.join(REPO_ROOT, 'tmp', 'react-devtools-cdt-mcp-e2e');166167const SESSION_ID = `react-devtools-cdt-mcp-${process.pid}-${Date.now()}`;168169function log(message: string): void {170 process.stdout.write(`${message}\n`);171}172173function sleep(ms: number): Promise<void> {174 return new Promise(resolve => setTimeout(resolve, ms));175}176177function createError(message: string): Error {178 // eslint-disable-next-line react-internal/prod-error-codes179 return new Error(message);180}181182function ensureBuiltModules(): void {183 if (!fs.existsSync(BUILT_MODULES_DIR)) {184 throw createError(185 'Missing build/oss-experimental. Run `yarn build-for-devtools` from ' +186 'the repo root before running react-devtools-cdt-mcp E2E tests.'187 );188 }189}190191function getFreePort(): Promise<number> {192 return new Promise((resolve, reject) => {193 const server = net.createServer();194 server.unref();195 server.on('error', reject);196 server.listen(0, '127.0.0.1', undefined, () => {197 const address = server.address();198 if (address == null || typeof address === 'string') {199 reject(createError('Failed to allocate a TCP port'));200 return;201 }202 server.close(() => resolve(address.port));203 });204 });205}206207function appendLog(logFile: string, text: string | Buffer): void {208 fs.appendFileSync(logFile, text);209}210211function formatCommand(command: string, args: Array<string>): string {212 return `$ ${[command, ...args].map(arg => JSON.stringify(arg)).join(' ')}\n`;213}214215function spawnLogged(216 command: string,217 args: Array<string>,218 options: SpawnOptions219): ChildProcess {220 const child = childProcess.spawn(command, args, {221 cwd: options.cwd,222 detached: options.detached === true,223 env: options.env,224 stdio: ['ignore', 'pipe', 'pipe'],225 });226 appendLog(options.logFile, formatCommand(command, args));227 child.on('error', error => {228 appendLog(229 options.logFile,230 `Failed to spawn ${command}: ${231 error.stack || error.message || String(error)232 }\n`233 );234 });235 child.stdout.on('data', chunk => appendLog(options.logFile, chunk));236 child.stderr.on('data', chunk => appendLog(options.logFile, chunk));237 return child;238}239240function waitForExit(child: ChildProcess, timeout: number): Promise<void> {241 return new Promise(resolve => {242 let done = false;243 const finish = () => {244 if (done) {245 return;246 }247 done = true;248 clearTimeout(timer);249 resolve();250 };251 const timer = setTimeout(() => {252 if (child.exitCode == null) {253 child.kill('SIGKILL');254 }255 finish();256 }, timeout);257258 if (child.exitCode != null) {259 finish();260 return;261 }262 child.once('exit', finish);263 child.once('error', finish);264 });265}266267async function removePathWithRetries(268 targetPath: string,269 logFile: string270): Promise<void> {271 for (let attempt = 0; attempt < 5; attempt++) {272 try {273 fs.rmSync(targetPath, {force: true, recursive: true});274 return;275 } catch (error) {276 if (attempt === 4) {277 appendLog(278 logFile,279 `Failed to remove ${targetPath}: ${280 error.stack || error.message || String(error)281 }\n`282 );283 return;284 }285 await sleep(250);286 }287 }288}289290function runCommand(291 command: string,292 args: Array<string>,293 options: CommandOptions294): Promise<CommandResult> {295 const timeout = options.timeout || 120000;296 const logFile = options.logFile;297 return new Promise((resolve, reject) => {298 let stdout = '';299 let stderr = '';300 appendLog(logFile, formatCommand(command, args));301 const child = childProcess.spawn(command, args, {302 cwd: options.cwd,303 env: options.env,304 stdio: ['ignore', 'pipe', 'pipe'],305 });306 const timer = setTimeout(() => {307 child.kill('SIGTERM');308 reject(createError(`Command timed out after ${timeout}ms: ${command}`));309 }, timeout);310311 child.stdout.on('data', chunk => {312 stdout += chunk.toString();313 appendLog(logFile, chunk);314 });315 child.stderr.on('data', chunk => {316 stderr += chunk.toString();317 appendLog(logFile, chunk);318 });319 child.on('error', error => {320 clearTimeout(timer);321 reject(error);322 });323 child.on('close', code => {324 clearTimeout(timer);325 if (code === 0) {326 resolve({stdout, stderr});327 } else {328 reject(329 createError(330 `Command failed with exit code ${code}: ${command} ${args.join(331 ' '332 )}\n${stderr || stdout}`333 )334 );335 }336 });337 });338}339340function startDebuggableChrome(341 chromeExecutablePath: string,342 remoteDebuggingPort: number,343 logFile: string344): {profileDir: string, process: ChildProcess} {345 const profileDir = fs.mkdtempSync(346 path.join(os.tmpdir(), 'react-devtools-cdt-mcp-chrome-')347 );348 appendLog(logFile, `chromeUserDataDir=${profileDir}\n`);349350 const args = [351 '--headless=new',352 `--remote-debugging-port=${remoteDebuggingPort}`,353 '--remote-debugging-address=127.0.0.1',354 `--user-data-dir=${profileDir}`,355 '--no-first-run',356 '--no-default-browser-check',357 'about:blank',358 ];359360 if (process.platform === 'linux') {361 args.splice(1, 0, '--no-sandbox', '--disable-setuid-sandbox');362 }363364 return {365 profileDir,366 process: spawnLogged(chromeExecutablePath, args, {367 cwd: PACKAGE_DIR,368 env: process.env,369 logFile,370 }),371 };372}373374function getChromeDevToolsBin(): string {375 let packageJsonPath: string;376 try {377 packageJsonPath = require.resolve('chrome-devtools-mcp/package.json', {378 paths: [PACKAGE_DIR],379 });380 } catch (error) {381 throw createError(382 'Missing chrome-devtools-mcp dependency. Run `yarn install` before ' +383 'running react-devtools-cdt-mcp E2E tests.'384 );385 }386387 const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));388 if (389 packageJson == null ||390 typeof packageJson !== 'object' ||391 packageJson.bin == null ||392 typeof packageJson.bin !== 'object' ||393 typeof packageJson.bin['chrome-devtools'] !== 'string'394 ) {395 throw createError(396 'chrome-devtools-mcp package.json is missing the chrome-devtools bin'397 );398 }399 return path.resolve(400 path.dirname(packageJsonPath),401 packageJson.bin['chrome-devtools']402 );403}404405async function waitForHttp(url: string, timeout: number): Promise<void> {406 const deadline = Date.now() + timeout;407 let lastError: Error = createError('No response yet');408 while (Date.now() < deadline) {409 try {410 const statusCode: number = await new Promise((resolve, reject) => {411 // eslint-disable-next-line no-undef412 const request = http.get(url, (response: http$IncomingMessage<>) => {413 response.resume();414 response.on('end', () => resolve(response.statusCode || 0));415 });416 request.on('error', reject);417 request.setTimeout(1000, () => {418 request.destroy(createError('HTTP request timed out'));419 });420 });421 if (statusCode >= 200 && statusCode < 400) {422 return;423 }424 lastError = createError(`HTTP ${statusCode}`);425 } catch (error) {426 lastError = error;427 }428 await sleep(250);429 }430 throw createError(`Timed out waiting for ${url}: ${lastError.message}`);431}432433async function waitForPageReady(434 chrome: Chrome,435 timeout: number436): Promise<void> {437 const deadline = Date.now() + timeout;438 let lastResult: mixed = null;439 while (Date.now() < deadline) {440 const result = await evaluatePageReadiness(441 chrome,442 `() => ({443 hasApp: document.querySelector('main.app') !== null,444 hasHook: window.__REACT_DEVTOOLS_GLOBAL_HOOK__ != null,445 hasDiscoveryListener: window.__dtmcp != null ||446 window.__REACT_DEVTOOLS_GLOBAL_HOOK__ != null,447 })`448 );449 lastResult = result;450 if (result.hasApp === true && result.hasHook === true) {451 return;452 }453 await sleep(250);454 }455 throw createError(456 `Timed out waiting for fixture readiness: ${457 JSON.stringify(lastResult) || String(lastResult)458 }`459 );460}461462function parseJsonOutput(stdout: string): mixed {463 const text = stdout.trim();464 assert.notStrictEqual(text, '', 'Expected command to print JSON');465 const parsed = JSON.parse(text);466 if (467 Array.isArray(parsed) &&468 parsed.length === 1 &&469 parsed[0] &&470 parsed[0].type === 'text' &&471 typeof parsed[0].text === 'string'472 ) {473 throw createError(parsed[0].text);474 }475 return parsed;476}477478function parseJsonFromText(text: string): mixed {479 const trimmed = text.trim();480 const fenced = trimmed.match(/```json\n([\s\S]*?)\n```/);481 if (fenced) {482 return JSON.parse(fenced[1]);483 }484 return JSON.parse(trimmed);485}486487function unwrapTextResponse(response: mixed): string {488 if (Array.isArray(response)) {489 return response.join('\n');490 }491 if (typeof response === 'string') {492 return response;493 }494 if (response != null && typeof response === 'object') {495 const message = response.message;496 if (typeof message === 'string') {497 return message;498 }499 }500 return JSON.stringify(response) || String(response);501}502503function formatValue(value: mixed): string {504 return JSON.stringify(value) || String(value);505}506507function expectObject(value: mixed, message: string): {+[string]: mixed, ...} {508 if (value == null || typeof value !== 'object' || Array.isArray(value)) {509 throw createError(`${message}. Saw: ${formatValue(value)}`);510 }511 return value;512}513514function expectString(value: mixed, message: string): string {515 if (typeof value !== 'string') {516 throw createError(`${message}. Saw: ${formatValue(value)}`);517 }518 return value;519}520521function expectNumber(value: mixed, message: string): number {522 if (typeof value !== 'number') {523 throw createError(`${message}. Saw: ${formatValue(value)}`);524 }525 return value;526}527528function expectArray(value: mixed, message: string): $ReadOnlyArray<mixed> {529 if (!Array.isArray(value)) {530 throw createError(`${message}. Saw: ${formatValue(value)}`);531 }532 return value;533}534535function expectOptionalString(value: mixed, message: string): string | null {536 if (value == null) {537 return null;538 }539 return expectString(value, message);540}541542function parseJSONSchemaObject(543 value: mixed,544 message: string545): JSONSchemaObject {546 const object = expectObject(value, message);547 const schema: JSONSchemaObject = {};548 if (object.type != null) {549 schema.type = expectString(object.type, `${message}.type must be a string`);550 }551 if (object.properties != null) {552 const rawProperties = expectObject(553 object.properties,554 `${message}.properties must be an object`555 );556 const properties: {[string]: JSONSchemaObject, ...} = {};557 // eslint-disable-next-line no-for-of-loops/no-for-of-loops558 for (const key of Object.keys(rawProperties)) {559 properties[key] = parseJSONSchemaObject(560 rawProperties[key],561 `${message}.properties.${key}`562 );563 }564 schema.properties = properties;565 }566 if (object.required != null) {567 schema.required = expectArray(568 object.required,569 `${message}.required must be an array`570 ).map((item, index) =>571 expectString(item, `${message}.required[${index}] must be a string`)572 );573 }574 return schema;575}576577function parseToolDefinition(value: mixed): ToolDefinition {578 const object = expectObject(value, 'Expected tool definition object');579 return {580 name: expectString(object.name, 'Expected tool definition name'),581 description: expectString(582 object.description,583 'Expected tool definition description'584 ),585 inputSchema: parseJSONSchemaObject(586 object.inputSchema,587 'Expected tool definition inputSchema'588 ),589 };590}591592function parseToolGroup(value: mixed): ToolGroup {593 const object = expectObject(value, 'Expected tool group object');594 return {595 name: expectString(object.name, 'Expected tool group name'),596 description: expectString(597 object.description,598 'Expected tool group description'599 ),600 tools: expectArray(object.tools, 'Expected tool group tools').map(601 parseToolDefinition602 ),603 };604}605606function parseToolDiscovery(value: mixed): ToolDiscovery {607 const object = expectObject(value, 'Expected tool discovery object');608 const rawToolGroups = object.thirdPartyDeveloperTools;609 if (rawToolGroups == null) {610 return {};611 }612 return {613 thirdPartyDeveloperTools: Array.isArray(rawToolGroups)614 ? rawToolGroups.map(parseToolGroup)615 : parseToolGroup(rawToolGroups),616 };617}618619function parsePageReadiness(value: mixed): PageReadiness {620 const object = expectObject(value, 'Expected page readiness object');621 return {622 hasApp: object.hasApp === true,623 hasHook: object.hasHook === true,624 };625}626627async function evaluatePageReadiness(628 chrome: Chrome,629 fn: string630): Promise<PageReadiness> {631 const output = await chrome.json(['evaluate_script', fn]);632 return parsePageReadiness(parseJsonFromText(unwrapTextResponse(output)));633}634635function parseToolResponse(output: mixed): mixed {636 return parseJsonFromText(unwrapTextResponse(output));637}638639function parseTreeNode(value: mixed): TreeNode {640 const object = expectObject(value, 'Expected tree node object');641 return {642 uid: expectString(object.uid, 'Expected tree node uid'),643 type: expectString(object.type, 'Expected tree node type'),644 name: expectString(object.name, 'Expected tree node name'),645 key: expectOptionalString(object.key, 'Expected tree node key'),646 firstChild: expectOptionalString(647 object.firstChild,648 'Expected tree node firstChild'649 ),650 nextSibling: expectOptionalString(651 object.nextSibling,652 'Expected tree node nextSibling'653 ),654 };655}656657function parseTree(value: mixed): Array<TreeNode> {658 const object = expectObject(value, 'Expected component tree response object');659 return expectArray(object.nodes, 'Expected component tree nodes array').map(660 parseTreeNode661 );662}663664function parseNamedObject(value: mixed, message: string): {name: string, ...} {665 const object = expectObject(value, message);666 return {667 name: expectString(object.name, `${message} name`),668 };669}670671function parseComponentBranchEntry(672 value: mixed,673 message: string674): ComponentBranchEntry {675 const object = expectObject(value, message);676 return {677 uid: expectString(object.uid, `${message} uid`),678 name: expectString(object.name, `${message} name`),679 type: expectString(object.type, `${message} type`),680 };681}682683function parseComponentDetails(value: mixed): ComponentDetails {684 const object = expectObject(value, 'Expected component details object');685 const details: ComponentDetails = {686 name: expectString(object.name, 'Expected component details name'),687 type: expectString(object.type, 'Expected component details type'),688 hooks: expectArray(object.hooks, 'Expected component details hooks').map(689 (hook, index) =>690 parseNamedObject(hook, `Expected component hook ${index}`)691 ),692 };693 return details;694}695696function parseComponentType(value: mixed): string {697 return expectString(698 expectObject(value, 'Expected component details object').type,699 'Expected component details type'700 );701}702703function parseSearchResult(value: mixed): SearchResult {704 const object = expectObject(value, 'Expected search result object');705 return {706 ...object,707 page: expectNumber(object.page, 'Expected search result page'),708 pageSize: expectNumber(object.pageSize, 'Expected search result pageSize'),709 totalCount: expectNumber(710 object.totalCount,711 'Expected search result totalCount'712 ),713 totalPages: expectNumber(714 object.totalPages,715 'Expected search result totalPages'716 ),717 results: expectArray(object.results, 'Expected search result results').map(718 (result, index) =>719 parseNamedObject(result, `Expected search result ${index}`)720 ),721 };722}723724function parseSnapshotNode(value: mixed): SnapshotNode {725 const object = expectObject(value, 'Expected snapshot node object');726 const snapshotNode: SnapshotNode = {};727 if (object.id != null) {728 snapshotNode.id = expectString(object.id, 'Expected snapshot node id');729 }730 if (object.role != null) {731 snapshotNode.role = expectString(732 object.role,733 'Expected snapshot node role'734 );735 }736 if (object.name != null) {737 snapshotNode.name = expectString(738 object.name,739 'Expected snapshot node name'740 );741 }742 if (object.children != null) {743 snapshotNode.children = expectArray(744 object.children,745 'Expected snapshot node children'746 ).map(parseSnapshotNode);747 }748 return snapshotNode;749}750751function parseSnapshotResponse(value: mixed): {snapshot: SnapshotNode, ...} {752 const object = expectObject(value, 'Expected snapshot response object');753 return {754 snapshot: parseSnapshotNode(object.snapshot),755 };756}757758function parseDomLookupResult(value: mixed): DomLookupResult {759 const object = expectObject(value, 'Expected DOM lookup result object');760 return {761 ...object,762 type: expectString(object.type, 'Expected DOM lookup result type'),763 name: expectString(object.name, 'Expected DOM lookup result name'),764 };765}766767function parseSourceResult(value: mixed): SourceResult {768 const object = expectObject(value, 'Expected source result object');769 if (object.source == null) {770 return {source: null};771 }772 const source = expectObject(object.source, 'Expected source object');773 return {774 ...object,775 source: {776 ...source,777 name: expectString(source.name, 'Expected source name'),778 fileName: expectString(source.fileName, 'Expected source fileName'),779 line: expectNumber(source.line, 'Expected source line'),780 column: expectNumber(source.column, 'Expected source column'),781 },782 };783}784785function parseOwnersStack(value: mixed): OwnersStackResult {786 const object = expectObject(value, 'Expected owners stack object');787 return {788 ...object,789 stack: expectString(object.stack, 'Expected owners stack string'),790 };791}792793function parseComponentBranch(794 value: mixed,795 label: string796): Array<ComponentBranchEntry> {797 return expectArray(value, `Expected ${label} branch array`).map(798 (entry, index) =>799 parseComponentBranchEntry(entry, `Expected ${label} ${index}`)800 );801}802803function parseErrorPayload(value: mixed): ErrorPayload {804 const object = expectObject(value, 'Expected error payload object');805 return {806 ...object,807 error: expectString(object.error, 'Expected error payload error'),808 };809}810811function parseStartProfilingResult(value: mixed): StartProfilingResult {812 const object = expectObject(value, 'Expected start profiling result object');813 return {814 ...object,815 status: expectString(object.status, 'Expected start profiling status'),816 traceName: expectString(817 object.traceName,818 'Expected start profiling traceName'819 ),820 };821}822823function parseStopProfilingResult(value: mixed): StopProfilingResult {824 const object = expectObject(value, 'Expected stop profiling result object');825 return {826 ...object,827 status: expectString(object.status, 'Expected stop profiling status'),828 traceName: expectString(829 object.traceName,830 'Expected stop profiling traceName'831 ),832 commits: expectNumber(object.commits, 'Expected stop profiling commits'),833 };834}835836function parseTraceOverview(value: mixed): Array<TraceOverviewCommit> {837 return expectArray(value, 'Expected trace overview array').map(838 (commit, index) => {839 const object = expectObject(commit, `Expected trace overview ${index}`);840 return {841 ...object,842 commit: expectNumber(843 object.commit,844 `Expected trace overview ${index} commit`845 ),846 componentsChanged: expectNumber(847 object.componentsChanged,848 `Expected trace overview ${index} componentsChanged`849 ),850 };851 }852 );853}854855function parseCommitReport(value: mixed): CommitReport {856 const object = expectObject(value, 'Expected commit report object');857 return {858 ...object,859 components: expectArray(860 object.components,861 'Expected commit report components'862 ).map((component, index) =>863 parseNamedObject(component, `Expected commit report component ${index}`)864 ),865 };866}867868function findNode(869 tree: Array<TreeNode>,870 predicate: (node: TreeNode) => boolean,871 message: string872): TreeNode {873 const node = tree.find(predicate);874 if (node == null) {875 throw createError(876 `${message}. Saw: ${tree877 .map(item => `${item.name}:${item.type}`)878 .join(', ')}`879 );880 }881 return node;882}883884function flattenSnapshot(885 node: SnapshotNode,886 result: Array<SnapshotNode>887): Array<SnapshotNode> {888 result.push(node);889 // eslint-disable-next-line no-for-of-loops/no-for-of-loops890 for (const child of node.children || []) {891 flattenSnapshot(child, result);892 }893 return result;894}895896function findSnapshotNode(897 snapshot: SnapshotNode,898 predicate: (node: SnapshotNode) => boolean,899 message: string900): SnapshotNode {901 const node = flattenSnapshot(snapshot, []).find(predicate);902 if (node == null) {903 throw createError(message);904 }905 return node;906}907908function assertHasSchema(tool: ToolDefinition): void {909 assert.strictEqual(typeof tool.description, 'string');910 assert(tool.description.length > 0);911 assert.strictEqual(tool.inputSchema.type, 'object');912}913914function getReactToolGroup(discovery: ToolDiscovery): ToolGroup | null {915 const toolGroups = discovery.thirdPartyDeveloperTools;916 if (Array.isArray(toolGroups)) {917 return toolGroups.find(group => group.name === 'react') || null;918 }919 if (920 toolGroups != null &&921 typeof toolGroups === 'object' &&922 toolGroups.name === 'react'923 ) {924 return toolGroups;925 }926 return null;927}928929function assertSourceReference(sourceResult: SourceResult): void {930 if (sourceResult.source === null) {931 return;932 }933 const source = sourceResult.source;934 assert.strictEqual(typeof source.name, 'string');935 assert.strictEqual(typeof source.fileName, 'string');936 assert(937 /App\.js|bundle\.js|webpack/.test(source.fileName),938 `Expected source file to reference App.js, bundle.js, or webpack. Saw: ${source.fileName}`939 );940 assert.strictEqual(typeof source.line, 'number');941 assert.strictEqual(typeof source.column, 'number');942}943944async function runE2E(chrome: Chrome, appUrl: string): Promise<void> {945 await chrome.json(['navigate_page', '--type', 'url', '--url', appUrl]);946 await waitForPageReady(chrome, 30000);947948 log('Checking third-party tool discovery...');949 const discovery = parseToolDiscovery(950 await chrome.json(['list_3p_developer_tools'])951 );952 const toolGroup = getReactToolGroup(discovery);953 if (toolGroup == null) {954 throw createError('Expected a third-party tool group');955 }956 assert.strictEqual(toolGroup.name, 'react');957 assert.deepStrictEqual(958 toolGroup.tools.map(tool => tool.name),959 TOOL_NAMES960 );961 // eslint-disable-next-line no-for-of-loops/no-for-of-loops962 for (const tool of toolGroup.tools) {963 assertHasSchema(tool);964 }965 const domTool = toolGroup.tools.find(966 tool => tool.name === 'react_get_component_by_dom_element'967 );968 if (domTool == null) {969 throw createError(970 'Expected react_get_component_by_dom_element in discovery'971 );972 }973 const inputSchemaProperties = domTool.inputSchema.properties;974 if (inputSchemaProperties == null) {975 throw createError('Expected DOM lookup tool schema properties');976 }977 const domElementSchema = inputSchemaProperties.element;978 if (domElementSchema == null) {979 throw createError('Expected DOM lookup tool element schema');980 }981 const domElementSchemaProperties = domElementSchema.properties;982 if (domElementSchemaProperties == null) {983 throw createError('Expected DOM lookup tool element schema properties');984 }985 assert.strictEqual(domElementSchema.type, 'object');986 assert.deepStrictEqual(domElementSchemaProperties, {uid: {type: 'string'}});987 assert.deepStrictEqual(domElementSchema.required, ['uid']);988989 log('Checking tree, details, search, and DOM lookup...');990 const callTool = (toolName: string, params?: {...}): Promise<mixed> =>991 chrome992 .json([993 'execute_3p_developer_tool',994 toolName,995 '--params',996 JSON.stringify(params || {}),997 ])998 .then(parseToolResponse);9991000 const tree = parseTree(await callTool('react_get_component_tree'));1001 const counter = findNode(1002 tree,1003 node => node.name === 'Counter' && node.type === 'function',1004 'Expected function component Counter'1005 );1006 const todo = findNode(1007 tree,1008 node => node.name === 'Todo' && node.type === 'function',1009 'Expected function component Todo'1010 );1011 const todoList = findNode(1012 tree,1013 node => node.name === 'TodoList' && node.type === 'function',1014 'Expected function component TodoList'1015 );1016 const todoListHost = findNode(1017 tree,1018 node => node.name === 'ul' && node.type === 'host',1019 'Expected host ul for TodoList'1020 );1021 const mainNode = findNode(1022 tree,1023 node => node.name === 'main' && node.type === 'host',1024 'Expected host main'1025 );1026 const app = findNode(1027 tree,1028 node => node.name === 'App' && node.type === 'function',1029 'Expected function component App'1030 );1031 const root = findNode(1032 tree,1033 node => node.type === 'root',1034 'Expected root node'1035 );1036 const memoBox = findNode(1037 tree,1038 node => node.name.includes('MemoBox') && node.type === 'memo',1039 'Expected memo component MemoBox'1040 );1041 const fancyInput = findNode(1042 tree,1043 node => node.name.includes('FancyInput') && node.type === 'forwardRef',1044 'Expected forwardRef component FancyInput'1045 );1046 const input = findNode(1047 tree,1048 node => node.name === 'input' && node.type === 'host',1049 'Expected host input'1050 );10511052 const counterDetails = parseComponentDetails(1053 await callTool('react_get_component_by_uid', {1054 uid: counter.uid,1055 includeHooks: true,1056 })1057 );1058 assert.strictEqual(counterDetails.name, 'Counter');1059 assert(1060 counterDetails.hooks.some(hook => hook.name === 'State'),1061 'Expected Counter details to include a State hook'1062 );10631064 assert.strictEqual(1065 parseComponentType(1066 await callTool('react_get_component_by_uid', {uid: memoBox.uid})1067 ),1068 'memo'1069 );1070 assert.strictEqual(1071 parseComponentType(1072 await callTool('react_get_component_by_uid', {uid: fancyInput.uid})1073 ),1074 'forwardRef'1075 );1076 assert.strictEqual(1077 parseComponentType(1078 await callTool('react_get_component_by_uid', {uid: input.uid})1079 ),1080 'host'1081 );10821083 const todoSearch = parseSearchResult(1084 await callTool('react_find_components', {1085 name: 'Todo',1086 pageSize: 2,1087 })1088 );1089 assert.strictEqual(todoSearch.page, 1);1090 assert.strictEqual(todoSearch.pageSize, 2);1091 assert.strictEqual(todoSearch.totalCount, 4);1092 assert.strictEqual(todoSearch.totalPages, 2);1093 assert.deepStrictEqual(1094 todoSearch.results.map(result => result.name),1095 ['TodoList', 'Todo']1096 );10971098 const snapshot = parseSnapshotResponse(await chrome.json(['take_snapshot']));1099 const buttonNode = findSnapshotNode(1100 snapshot.snapshot,1101 node => node.role === 'button' && node.name === '+1',1102 'Expected +1 button in snapshot'1103 );1104 const buttonUid = buttonNode.id;1105 if (buttonUid == null) {1106 throw createError('Expected +1 button uid');1107 }1108 const domLookup = parseDomLookupResult(1109 await callTool('react_get_component_by_dom_element', {1110 element: {uid: buttonUid},1111 })1112 );1113 assert.strictEqual(domLookup.type, 'host');1114 assert.strictEqual(domLookup.name, 'button');11151116 log('Checking source, parents, owners, and error payloads...');1117 const source = parseSourceResult(1118 await callTool('react_get_component_source', {1119 uid: counter.uid,1120 })1121 );1122 assertSourceReference(source);11231124 const ownersStack = parseOwnersStack(1125 await callTool('react_get_owner_stack_trace', {uid: todo.uid})1126 );1127 assert.strictEqual(typeof ownersStack.stack, 'string');1128 if (ownersStack.stack.length > 0) {1129 assert(1130 /Todo|TodoList|App\.js/.test(ownersStack.stack),1131 `Expected owners stack to reference Todo, TodoList, or App.js. Saw: ${ownersStack.stack}`1132 );1133 }11341135 const parentsBranch = parseComponentBranch(1136 await callTool('react_get_parent_stack', {1137 uid: todo.uid,1138 }),1139 'parents'1140 );1141 assert.deepStrictEqual(parentsBranch, [1142 {1143 uid: todoListHost.uid,1144 name: todoListHost.name,1145 type: todoListHost.type,1146 },1147 {1148 uid: todoList.uid,1149 name: todoList.name,1150 type: todoList.type,1151 },1152 {1153 uid: mainNode.uid,1154 name: mainNode.name,1155 type: mainNode.type,1156 },1157 {1158 uid: app.uid,1159 name: app.name,1160 type: app.type,1161 },1162 {1163 uid: root.uid,1164 name: root.name,1165 type: root.type,1166 },1167 ]);11681169 const ownersBranch = parseComponentBranch(1170 await callTool('react_get_owner_stack', {1171 uid: todo.uid,1172 }),1173 'owners'1174 );1175 assert(1176 ownersBranch.some(owner => owner.name === 'TodoList'),1177 'Expected Todo owners branch to include TodoList'1178 );11791180 const invalidUid = parseErrorPayload(1181 await callTool('react_get_component_by_uid', {1182 uid: 'r999999',1183 })1184 );1185 assert.deepStrictEqual(invalidUid, {1186 error: 'Component not found: "r999999"',1187 });11881189 log('Checking profiling through a real CLI click...');1190 const traceName = `e2e-${Date.now()}`;1191 assert.deepStrictEqual(1192 parseStartProfilingResult(1193 await callTool('react_start_profiling', {traceName})1194 ),1195 {1196 status: 'started',1197 traceName,1198 }1199 );1200 await chrome.json(['click', buttonUid]);1201 const stopResult = parseStopProfilingResult(1202 await callTool('react_stop_profiling')1203 );1204 assert.strictEqual(stopResult.status, 'stopped');1205 assert.strictEqual(stopResult.traceName, traceName);1206 assert(1207 stopResult.commits >= 1,1208 'Expected profiling to record at least one commit'1209 );12101211 const overview = parseTraceOverview(1212 await callTool('react_get_trace_overview', {traceName})1213 );1214 assert(overview.length >= 1, 'Expected at least one profiling commit');1215 assert(1216 overview.some(commit => commit.componentsChanged >= 1),1217 'Expected at least one changed component in trace overview'1218 );12191220 let foundCounterCommit = false;1221 // eslint-disable-next-line no-for-of-loops/no-for-of-loops1222 for (const commit of overview) {1223 const report = parseCommitReport(1224 await callTool('react_get_commit_report', {1225 traceName,1226 commitIndex: commit.commit,1227 })1228 );1229 if (report.components.some(component => component.name === 'Counter')) {1230 foundCounterCommit = true;1231 break;1232 }1233 }1234 assert(foundCounterCommit, 'Expected a profiling commit involving Counter');1235}12361237async function main(): Promise<void> {1238 ensureBuiltModules();1239 fs.mkdirSync(LOG_DIR, {recursive: true});12401241 const fixtureLog = path.join(LOG_DIR, 'fixture-server.log');1242 const chromeLog = path.join(LOG_DIR, 'chrome-devtools.log');1243 const cliLog = path.join(LOG_DIR, 'chrome-devtools-cli.log');1244 fs.writeFileSync(fixtureLog, '');1245 fs.writeFileSync(chromeLog, '');1246 fs.writeFileSync(cliLog, '');12471248 const chromeDevToolsBin = getChromeDevToolsBin();1249 const port = await getFreePort();1250 const appUrl = `http://127.0.0.1:${port}/`;12511252 let fixture: ?ChildProcess;1253 let debuggableChrome: ?ChildProcess;1254 let debuggableChromeProfile: ?string;1255 const runChrome = (args: Array<string>): Promise<CommandResult> =>1256 runCommand(1257 process.execPath,1258 [chromeDevToolsBin, ...args, '--sessionId', SESSION_ID],1259 {1260 cwd: PACKAGE_DIR,1261 env: process.env,1262 logFile: cliLog,1263 timeout: 120000,1264 }1265 );1266 const chrome: Chrome = {1267 run: runChrome,1268 async json(args: Array<string>): Promise<mixed> {1269 const result = await runChrome([...args, '--output-format', 'json']);1270 return parseJsonOutput(result.stdout);1271 },1272 };12731274 try {1275 log(`Starting fixture at ${appUrl}`);1276 fixture = spawnLogged('yarn', ['start'], {1277 cwd: FIXTURE_DIR,1278 detached: true,1279 env: {1280 ...process.env,1281 BROWSER: 'none',1282 CI: 'true',1283 E2E: 'true',1284 HOST: '127.0.0.1',1285 PORT: String(port),1286 },1287 logFile: fixtureLog,1288 });1289 await waitForHttp(appUrl, 60000);12901291 const startArgs = [1292 'start',1293 '--categoryExperimentalThirdParty=true',1294 '--usageStatistics=false',1295 '--logFile',1296 chromeLog,1297 ];1298 const chromeExecutablePath = process.env.CHROME_EXECUTABLE_PATH;1299 if (chromeExecutablePath != null) {1300 const remoteDebuggingPort = await getFreePort();1301 const launchedChrome = startDebuggableChrome(1302 chromeExecutablePath,1303 remoteDebuggingPort,1304 chromeLog1305 );1306 debuggableChrome = launchedChrome.process;1307 debuggableChromeProfile = launchedChrome.profileDir;1308 const browserUrl = `http://127.0.0.1:${remoteDebuggingPort}`;1309 await waitForHttp(`${browserUrl}/json/version`, 30000);1310 startArgs.push('--browserUrl', browserUrl);1311 } else {1312 startArgs.push('--headless=true', '--isolated=true');1313 }1314 log('Starting chrome-devtools daemon...');1315 await chrome.run(startArgs);13161317 await runE2E(chrome, appUrl);1318 log('react-devtools-cdt-mcp E2E passed.');1319 } finally {1320 try {1321 await chrome.run(['stop']);1322 } catch (error) {1323 appendLog(cliLog, `Failed to stop chrome-devtools: ${error.stack}\n`);1324 }1325 if (debuggableChrome) {1326 try {1327 debuggableChrome.kill('SIGTERM');1328 await waitForExit(debuggableChrome, 5000);1329 } catch (error) {1330 appendLog(chromeLog, `Failed to stop Chrome: ${error.stack}\n`);1331 }1332 }1333 if (debuggableChromeProfile) {1334 await removePathWithRetries(debuggableChromeProfile, chromeLog);1335 }1336 if (fixture && fixture.pid) {1337 try {1338 process.kill(-fixture.pid, 'SIGTERM');1339 } catch (error) {1340 try {1341 fixture.kill('SIGTERM');1342 } catch (innerError) {1343 appendLog(1344 fixtureLog,1345 `Failed to stop fixture server: ${innerError.stack}\n`1346 );1347 }1348 }1349 }1350 }1351}13521353module.exports = {main};
Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.