Skip to main content
Glama

BrowserStack MCP server

Official
detect-test-files.ts6.41 kB
import fs from "fs"; import path from "path"; import { SDKSupportedLanguage, SDKSupportedTestingFrameworkEnum, } from "../sdk-utils/common/types.js"; import { EXCLUDED_DIRS, TEST_FILE_DETECTION, backendIndicators, strongUIIndicators, } from "../percy-snapshot-utils/constants.js"; import { DetectionConfig } from "../percy-snapshot-utils/types.js"; import logger from "../../logger.js"; async function walkDir( dir: string, extensions: string[], depth: number = 6, ): Promise<string[]> { const result: string[] = []; if (depth < 0) return result; try { const entries = await fs.promises.readdir(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { if (!EXCLUDED_DIRS.has(entry.name) && !entry.name.startsWith(".")) { result.push(...(await walkDir(fullPath, extensions, depth - 1))); } } else if (extensions.some((ext) => entry.name.endsWith(ext))) { result.push(fullPath); } } } catch { logger.info("Failed to read user directory"); } return result; } async function fileContainsRegex( filePath: string, regexes: RegExp[], ): Promise<boolean> { if (!regexes.length) return false; try { const content = await fs.promises.readFile(filePath, "utf8"); return regexes.some((re) => re.test(content)); } catch { return false; } } async function batchRegexCheck( filePath: string, regexGroups: RegExp[][], ): Promise<boolean[]> { try { const content = await fs.promises.readFile(filePath, "utf8"); return regexGroups.map((regexes) => regexes.length > 0 ? regexes.some((re) => re.test(content)) : false, ); } catch { return regexGroups.map(() => false); } } async function isLikelyUITest(filePath: string): Promise<boolean> { try { const content = await fs.promises.readFile(filePath, "utf8"); if (backendIndicators.some((pattern) => pattern.test(content))) { return false; } return strongUIIndicators.some((pattern) => pattern.test(content)); } catch { return false; } } function getFileScore(fileName: string, config: DetectionConfig): number { let score = 0; // Higher score for explicit UI test naming if (/ui|web|e2e|integration|functional/i.test(fileName)) score += 3; if (config.namePatterns.some((pattern) => pattern.test(fileName))) score += 2; return score; } export interface ListTestFilesOptions { language: SDKSupportedLanguage; framework?: SDKSupportedTestingFrameworkEnum; baseDir: string; strictMode?: boolean; } export async function listTestFiles( options: ListTestFilesOptions, ): Promise<string[]> { const { language, framework, baseDir, strictMode = false } = options; const config = TEST_FILE_DETECTION[language]; if (!config) { return []; } // Step 1: Collect all files with matching extensions let files: string[] = []; try { files = await walkDir(baseDir, config.extensions, 6); } catch { return []; } if (files.length === 0) { throw new Error("No files found with the specified extensions"); } const candidateFiles: Map<string, number> = new Map(); // Step 2: Fast name-based identification with scoring for (const file of files) { const fileName = path.basename(file); const score = getFileScore(fileName, config); if (config.namePatterns.some((pattern) => pattern.test(fileName))) { candidateFiles.set(file, score); } } // Step 3: Content-based test detection for remaining files const remainingFiles = files.filter((file) => !candidateFiles.has(file)); const contentCheckPromises = remainingFiles.map(async (file) => { const hasTestContent = await fileContainsRegex(file, config.contentRegex); if (hasTestContent) { const fileName = path.basename(file); const score = getFileScore(fileName, config); candidateFiles.set(file, score); } }); await Promise.all(contentCheckPromises); // Step 4: Handle SpecFlow .feature files for C# + SpecFlow if (language === "csharp" && framework === "specflow") { try { const featureFiles = await walkDir(baseDir, [".feature"], 6); featureFiles.forEach((file) => candidateFiles.set(file, 2)); } catch { // ignore } } if (candidateFiles.size === 0) { return []; } // Step 6: UI Detection with fallback patterns const uiFiles: string[] = []; const filesToCheck = Array.from(candidateFiles.keys()); // Batch process UI detection for better performance const batchSize = 10; for (let i = 0; i < filesToCheck.length; i += batchSize) { const batch = filesToCheck.slice(i, i + batchSize); const batchPromises = batch.map(async (file) => { // First, use the new precise UI detection const isUITest = await isLikelyUITest(file); if (isUITest) { return file; } // If not clearly UI, run the traditional checks const [hasExplicitUI, hasUIIndicators, hasBackend, shouldExclude] = await batchRegexCheck(file, [ config.uiDriverRegex, config.uiIndicatorRegex, config.backendRegex, config.excludeRegex || [], ]); if (shouldExclude) { return null; } if (hasBackend) { return null; } if (hasExplicitUI) { return file; } if (hasUIIndicators) { return file; } if (!strictMode) { const score = candidateFiles.get(file) || 0; if (score >= 3) { return file; } } return null; }); const batchResults = await Promise.all(batchPromises); uiFiles.push( ...batchResults.filter((file): file is string => file !== null), ); } // Step 7: Sort by score (higher confidence files first) uiFiles.sort((a, b) => { const scoreA = candidateFiles.get(a) || 0; const scoreB = candidateFiles.get(b) || 0; return scoreB - scoreA; }); return uiFiles; } export async function listUITestFilesStrict( options: Omit<ListTestFilesOptions, "strictMode">, ): Promise<string[]> { return listTestFiles({ ...options, strictMode: true }); } export async function listUITestFilesRelaxed( options: Omit<ListTestFilesOptions, "strictMode">, ): Promise<string[]> { return listTestFiles({ ...options, strictMode: false }); }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/browserstack/mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server