@raiken/cli
Version:
CLI tool for Raiken - AI-powered Playwright test generator
564 lines • 28.6 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.LocalFileSystemAdapter = void 0;
const fs = __importStar(require("fs/promises"));
const path = __importStar(require("path"));
const glob_1 = require("glob");
const project_detector_1 = require("./project-detector");
const ai_service_1 = require("./ai-service");
class LocalFileSystemAdapter {
constructor(projectPath, projectInfo) {
this.projectPath = projectPath;
this.projectInfo = projectInfo;
this.testDirectory = path.join(projectPath, projectInfo.testDir);
this.aiService = new ai_service_1.AIService();
}
async getTestFiles() {
await (0, project_detector_1.ensureTestDirectory)(this.projectPath, this.projectInfo.testDir);
const testPattern = path.join(this.testDirectory, '**/*.{spec,test}.{ts,js}');
const testFilePaths = await (0, glob_1.glob)(testPattern);
const testFiles = [];
for (const filePath of testFilePaths) {
try {
const content = await fs.readFile(filePath, 'utf-8');
const stats = await fs.stat(filePath);
const relativePath = path.relative(this.projectPath, filePath);
testFiles.push({
name: path.basename(filePath),
path: relativePath,
content,
createdAt: stats.birthtime.toISOString(),
modifiedAt: stats.mtime.toISOString()
});
}
catch (error) {
console.error(`Failed to read test file ${filePath}:`, error);
}
}
return testFiles.sort((a, b) => a.name.localeCompare(b.name));
}
async saveTestFile(content, filename, tabId) {
await (0, project_detector_1.ensureTestDirectory)(this.projectPath, this.projectInfo.testDir);
// Ensure filename has proper extension
if (!filename.endsWith('.spec.ts') && !filename.endsWith('.test.ts')) {
filename = filename.replace(/\.(js|ts)$/, '') + '.spec.ts';
}
// Sanitize filename
const sanitizedFilename = filename.replace(/[^a-zA-Z0-9.-]/g, '_');
// Create new file
const fullPath = path.join(this.testDirectory, sanitizedFilename);
await fs.writeFile(fullPath, content, 'utf-8');
const relativePath = path.relative(this.projectPath, fullPath);
console.log(`✅ Test saved: ${relativePath}`);
return relativePath;
}
async deleteTestFile(testPath) {
// Security check - ensure path is within project directory
const fullPath = path.resolve(this.projectPath, testPath);
const normalizedProjectPath = path.resolve(this.projectPath);
if (!fullPath.startsWith(normalizedProjectPath)) {
throw new Error('Invalid test path - outside project directory');
}
await fs.unlink(fullPath);
console.log(`🗑️ Test deleted: ${testPath}`);
}
async executeTest(testPath, config) {
try {
const { spawn } = require('child_process');
// Resolve test path - handle various path formats
let resolvedTestPath = testPath;
console.log(`🔍 Resolving test path: "${testPath}"`);
console.log(`📁 Project path: ${this.projectPath}`);
console.log(`📁 Test directory: ${this.testDirectory}`);
// Extract just the filename from the path (remove any directory prefixes)
const filename = path.basename(testPath);
console.log(`📄 Extracted filename: "${filename}"`);
// Try to find the test file in the project's test directory
const testFileInTestDir = path.join(this.testDirectory, filename);
console.log(`🎯 Looking for test file at: ${testFileInTestDir}`);
try {
await fs.access(testFileInTestDir);
// File exists in test directory, use relative path from project root
resolvedTestPath = path.relative(this.projectPath, testFileInTestDir);
console.log(`✅ Found test file! Using resolved path: "${resolvedTestPath}"`);
}
catch (accessError) {
console.log(`❌ Test file not found at: ${testFileInTestDir}`);
console.log(` Access error:`, accessError?.code || accessError);
// List all files in the test directory to see what's actually there
try {
const testDirContents = await fs.readdir(this.testDirectory);
console.log(`📂 Files in test directory (${this.testDirectory}):`);
testDirContents.forEach(file => console.log(` - ${file}`));
// Check if there's a similar filename (case-insensitive or partial match)
const similarFiles = testDirContents.filter(file => file.toLowerCase().includes(filename.toLowerCase().substring(0, 20)) ||
filename.toLowerCase().includes(file.toLowerCase().substring(0, 20)));
if (similarFiles.length > 0) {
console.log(`🔍 Found similar files:`, similarFiles);
// Use the first similar file
resolvedTestPath = path.relative(this.projectPath, path.join(this.testDirectory, similarFiles[0]));
console.log(`✅ Using similar file: "${resolvedTestPath}"`);
}
else {
// Try to find the file anywhere in the project
const glob = require('glob');
const searchPattern = `**/${filename}`;
console.log(`🔍 Searching for file with pattern: ${searchPattern}`);
try {
const matches = glob.sync(searchPattern, { cwd: this.projectPath });
console.log(`🔍 Glob search results:`, matches);
if (matches.length > 0) {
resolvedTestPath = matches[0];
console.log(`✅ Found test file via glob search: "${resolvedTestPath}"`);
}
else {
console.log(`❌ No test file found matching: ${filename}`);
// Try a broader search with just the first part of the filename
const shortName = filename.split('_')[0];
const broadPattern = `**/*${shortName}*.spec.ts`;
console.log(`🔍 Trying broader search with pattern: ${broadPattern}`);
const broadMatches = glob.sync(broadPattern, { cwd: this.projectPath });
console.log(`🔍 Broad search results:`, broadMatches);
if (broadMatches.length > 0) {
resolvedTestPath = broadMatches[0];
console.log(`✅ Found test file via broad search: "${resolvedTestPath}"`);
}
else {
// Keep original path and let Playwright handle the error
resolvedTestPath = filename;
console.log(`❌ All search attempts failed, using original filename: "${resolvedTestPath}"`);
}
}
}
catch (globError) {
console.log(`❌ Glob search failed:`, globError);
resolvedTestPath = filename;
}
}
}
catch (readdirError) {
console.log(`❌ Could not read test directory:`, readdirError);
resolvedTestPath = filename;
}
}
// Ensure test-results directory exists
const testResultsDir = path.join(this.projectPath, 'test-results');
const reportsDir = path.join(testResultsDir, 'reports');
await fs.mkdir(reportsDir, { recursive: true });
// Build Playwright command with JSON reporter
const args = ['playwright', 'test', resolvedTestPath, '--reporter=json'];
if (config.headless === false) {
args.push('--headed');
}
// Always specify browser project to avoid running all browsers
const browserType = config.browserType || 'chromium';
args.push(`--project=${browserType}`);
// Force single worker to prevent parallel execution
args.push('--workers=1');
// Also add --grep to ensure we only run the specific test
const testName = path.basename(resolvedTestPath, '.spec.ts');
args.push(`--grep=${testName}`);
console.log(`Executing: npx ${args.join(' ')}`);
console.log(`Working directory: ${this.projectPath}`);
console.log(`Original test path: ${testPath}`);
console.log(`Resolved test path: ${resolvedTestPath}`);
console.log(`Test directory: ${this.testDirectory}`);
const startTime = Date.now();
return new Promise((resolve) => {
const child = spawn('npx', args, {
cwd: this.projectPath,
stdio: ['ignore', 'pipe', 'pipe']
});
let stdout = '';
let stderr = '';
child.stdout.on('data', (data) => {
stdout += data.toString();
});
child.stderr.on('data', (data) => {
stderr += data.toString();
});
child.on('close', async (code) => {
const success = code === 0;
const duration = Date.now() - startTime;
// Generate report ID
const reportId = `report-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
try {
// Parse Playwright JSON output to extract artifacts
let playwrightResult = {};
try {
playwrightResult = JSON.parse(stdout);
}
catch {
// If JSON parsing fails, create a basic structure
playwrightResult = {
config: {},
suites: [],
errors: stderr ? [{ message: stderr }] : [],
stats: { duration: duration }
};
}
// Collect artifacts (screenshots, videos, traces)
const artifacts = await this.collectTestArtifacts(testResultsDir, resolvedTestPath);
// Perform AI analysis for failed tests
let aiAnalysis = null;
if (!success) {
console.log('🤖 Analyzing test failure with AI...');
try {
const analysisRequest = {
testName: path.basename(testPath, '.spec.ts'),
testPath: resolvedTestPath,
rawOutput: stdout,
rawError: stderr,
playwrightResult: playwrightResult,
detailedErrors: this.parsePlaywrightErrors(playwrightResult, stderr),
screenshots: artifacts.screenshots,
videos: artifacts.videos
};
aiAnalysis = await this.aiService.analyzeTestFailure(analysisRequest);
console.log(`✅ AI analysis completed (confidence: ${aiAnalysis.confidence}%)`);
}
catch (error) {
console.warn('⚠️ AI analysis failed:', error);
}
}
// Create comprehensive report
const report = {
id: reportId,
testName: path.basename(testPath, '.spec.ts'),
testPath: resolvedTestPath,
timestamp: new Date().toISOString(),
success: success,
duration: duration,
exitCode: code || 0,
rawOutput: stdout,
rawError: stderr,
playwrightResult: playwrightResult,
summary: aiAnalysis?.summary || (success ? 'Test passed successfully' : 'Test failed - check details below'),
suggestions: aiAnalysis?.suggestions || (success ? undefined : 'Review the error details, screenshots, and videos for debugging information'),
rootCause: aiAnalysis?.rootCause,
fixRecommendations: aiAnalysis?.fixRecommendations,
aiConfidence: aiAnalysis?.confidence,
detailedErrors: this.parsePlaywrightErrors(playwrightResult, stderr),
screenshots: artifacts.screenshots,
videos: artifacts.videos,
traces: artifacts.traces,
browserLogs: artifacts.browserLogs,
networkLogs: artifacts.networkLogs,
needsAIAnalysis: !success && !aiAnalysis
};
// Save report to project directory
// Ensure reports directory exists (double-check)
await fs.mkdir(reportsDir, { recursive: true });
const reportPath = path.join(reportsDir, `${reportId}.json`);
await fs.writeFile(reportPath, JSON.stringify(report, null, 2));
console.log(`📊 Test report saved: ${reportPath}`);
resolve({
success,
output: stdout,
error: stderr,
reportId: reportId
});
}
catch (reportError) {
console.error('Failed to generate test report:', reportError);
resolve({
success,
output: stdout,
error: stderr
});
}
});
child.on('error', (error) => {
resolve({
success: false,
error: error.message
});
});
});
}
catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
};
}
}
async collectTestArtifacts(testResultsDir, testPath) {
const artifacts = {
screenshots: [],
videos: [],
traces: [],
browserLogs: [],
networkLogs: []
};
try {
// Look for test result directories
const entries = await fs.readdir(testResultsDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory() && entry.name !== 'reports') {
const artifactDir = path.join(testResultsDir, entry.name);
const artifactFiles = await fs.readdir(artifactDir);
for (const file of artifactFiles) {
const filePath = path.join(artifactDir, file);
const relativePath = path.relative(testResultsDir, filePath);
if (file.endsWith('.png') || file.endsWith('.jpg') || file.endsWith('.jpeg')) {
artifacts.screenshots.push({
name: file,
path: filePath,
relativePath: relativePath,
url: `/api/artifacts/${relativePath}`
});
}
else if (file.endsWith('.webm') || file.endsWith('.mp4')) {
artifacts.videos.push({
name: file,
path: filePath,
relativePath: relativePath,
url: `/api/artifacts/${relativePath}`
});
}
else if (file.endsWith('.zip') && file.includes('trace')) {
artifacts.traces.push({
name: file,
path: filePath,
relativePath: relativePath,
url: `/api/artifacts/${relativePath}`
});
}
}
}
}
}
catch (error) {
console.warn('Failed to collect test artifacts:', error);
}
return artifacts;
}
parsePlaywrightErrors(playwrightResult, stderr) {
const errors = [];
// Extract errors from Playwright JSON result
if (playwrightResult.errors && Array.isArray(playwrightResult.errors)) {
errors.push(...playwrightResult.errors.map((error) => ({
message: error.message || 'Configuration error',
stack: error.stack || error.message,
type: 'configuration'
})));
}
// Extract errors from test results with more detail
if (playwrightResult.suites && Array.isArray(playwrightResult.suites)) {
for (const suite of playwrightResult.suites) {
if (suite.specs) {
for (const spec of suite.specs) {
if (spec.tests) {
for (const test of spec.tests) {
if (test.results) {
for (const result of test.results) {
if (result.error) {
errors.push({
message: result.error.message || 'Test execution error',
stack: result.error.stack || result.error.message,
type: 'test_execution',
testTitle: test.title,
specFile: spec.file,
status: result.status,
duration: result.duration,
retry: result.retry || 0
});
}
// Also capture failed assertions and timeouts
if (result.status === 'failed' || result.status === 'timedOut') {
const errorMessage = result.error?.message ||
(result.status === 'timedOut' ? 'Test timed out' : 'Test failed');
errors.push({
message: errorMessage,
stack: result.error?.stack || stderr,
type: result.status === 'timedOut' ? 'timeout' : 'assertion_failure',
testTitle: test.title,
specFile: spec.file,
status: result.status,
duration: result.duration,
retry: result.retry || 0
});
}
}
}
}
}
}
}
}
}
// Parse stderr for additional context if no structured errors found
if (errors.length === 0 && stderr) {
const stderrErrors = this.parseStderrErrors(stderr);
errors.push(...stderrErrors);
}
return errors;
}
parseStderrErrors(stderr) {
const errors = [];
if (stderr.includes('No tests found')) {
errors.push({
message: 'No tests found. Make sure that arguments are regular expressions matching test files.',
stack: stderr,
type: 'no_tests_found'
});
}
if (stderr.includes('Error: browserType.launch: Executable doesn\'t exist')) {
errors.push({
message: 'Browser executable not found. Run "npx playwright install" to install browsers.',
stack: stderr,
type: 'browser_not_installed'
});
}
if (stderr.includes('ECONNREFUSED') || stderr.includes('connection refused')) {
const urlMatch = stderr.match(/https?:\/\/[^\s]+/);
const url = urlMatch ? urlMatch[0] : 'target server';
errors.push({
message: `Connection refused to ${url}. Make sure the server is running.`,
stack: stderr,
type: 'connection_refused',
url: url
});
}
if (stderr.includes('timeout') || stderr.includes('timed out')) {
const timeoutMatch = stderr.match(/timeout (\d+)ms/);
const timeout = timeoutMatch ? timeoutMatch[1] : 'unknown';
errors.push({
message: `Test timed out after ${timeout}ms. Consider increasing timeout or optimizing test performance.`,
stack: stderr,
type: 'timeout',
timeout: timeout
});
}
if (stderr.includes('net::ERR_')) {
const networkErrorMatch = stderr.match(/net::(ERR_[A-Z_]+)/);
const networkError = networkErrorMatch ? networkErrorMatch[1] : 'NETWORK_ERROR';
errors.push({
message: `Network error: ${networkError.replace(/_/g, ' ')}`,
stack: stderr,
type: 'network_error',
networkError: networkError
});
}
// Generic fallback
if (errors.length === 0) {
errors.push({
message: 'Test execution failed - see raw output for details',
stack: stderr,
type: 'generic_failure'
});
}
return errors;
}
async getReports() {
try {
const reportsDir = path.join(this.projectPath, 'test-results', 'reports');
try {
await fs.access(reportsDir);
}
catch {
// Reports directory doesn't exist yet
return [];
}
const reportFiles = await fs.readdir(reportsDir);
const reports = [];
for (const file of reportFiles) {
if (file.endsWith('.json')) {
try {
const reportPath = path.join(reportsDir, file);
const reportContent = await fs.readFile(reportPath, 'utf-8');
const report = JSON.parse(reportContent);
// Update artifact paths to be relative to the bridge server
if (report.screenshots) {
report.screenshots = report.screenshots.map((screenshot) => ({
...screenshot,
url: `/api/artifacts/${screenshot.relativePath}`
}));
}
if (report.videos) {
report.videos = report.videos.map((video) => ({
...video,
url: `/api/artifacts/${video.relativePath}`
}));
}
if (report.traces) {
report.traces = report.traces.map((trace) => ({
...trace,
url: `/api/artifacts/${trace.relativePath}`
}));
}
reports.push(report);
}
catch (error) {
console.warn(`Failed to read report file ${file}:`, error);
}
}
}
// Sort by timestamp (newest first)
return reports.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
}
catch (error) {
console.error('Failed to get reports:', error);
return [];
}
}
async deleteReport(reportId) {
try {
const reportsDir = path.join(this.projectPath, 'test-results', 'reports');
const reportFiles = await fs.readdir(reportsDir);
for (const file of reportFiles) {
if (file.endsWith('.json')) {
const reportPath = path.join(reportsDir, file);
const reportContent = await fs.readFile(reportPath, 'utf-8');
const report = JSON.parse(reportContent);
if (report.id === reportId) {
await fs.unlink(reportPath);
console.log(`Deleted report: ${reportId}`);
return;
}
}
}
throw new Error(`Report not found: ${reportId}`);
}
catch (error) {
console.error(`Failed to delete report ${reportId}:`, error);
throw error;
}
}
}
exports.LocalFileSystemAdapter = LocalFileSystemAdapter;
//# sourceMappingURL=filesystem-adapter.js.map