@probolabs/playwright 1.4.0-rc.5 → 1.4.0-rc.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/probo.js +2 -2
- package/dist/.tsbuildinfo +1 -1
- package/dist/cli.js +408 -22
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +352 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +41 -1
- package/dist/index.js +352 -20
- package/dist/index.js.map +1 -1
- package/dist/types/cli.d.ts.map +1 -1
- package/dist/types/codegen-api.d.ts.map +1 -1
- package/dist/types/test-suite-runner.d.ts.map +1 -1
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -1006,6 +1006,7 @@ class ProboCodeGenerator {
|
|
|
1006
1006
|
*/
|
|
1007
1007
|
static async fetchScenarioData(scenarioId, apiToken, apiUrl) {
|
|
1008
1008
|
const normalizedUrl = this.normalizeApiUrl(apiUrl);
|
|
1009
|
+
// Scenario interactions endpoint is under /api/ (special endpoint)
|
|
1009
1010
|
const url = `${normalizedUrl}/api/scenarios/${scenarioId}/interactions`;
|
|
1010
1011
|
const response = await fetch(url, {
|
|
1011
1012
|
method: 'GET',
|
|
@@ -1040,6 +1041,54 @@ class ProboCodeGenerator {
|
|
|
1040
1041
|
}
|
|
1041
1042
|
return response.json();
|
|
1042
1043
|
}
|
|
1044
|
+
/**
|
|
1045
|
+
* Fetch project data from the backend API
|
|
1046
|
+
*/
|
|
1047
|
+
static async fetchProjectData(projectId, apiToken, apiUrl) {
|
|
1048
|
+
const normalizedUrl = this.normalizeApiUrl(apiUrl);
|
|
1049
|
+
// Projects endpoint is at root level, not under /api/
|
|
1050
|
+
const url = `${normalizedUrl}/projects/${projectId}/`;
|
|
1051
|
+
const response = await fetch(url, {
|
|
1052
|
+
method: 'GET',
|
|
1053
|
+
headers: {
|
|
1054
|
+
'Authorization': `Token ${apiToken}`,
|
|
1055
|
+
'Content-Type': 'application/json',
|
|
1056
|
+
},
|
|
1057
|
+
});
|
|
1058
|
+
if (!response.ok) {
|
|
1059
|
+
const errorText = await this.readResponseErrorText(response);
|
|
1060
|
+
throw new Error(`Failed to fetch project ${projectId}: ${response.status} ${errorText}`);
|
|
1061
|
+
}
|
|
1062
|
+
return response.json();
|
|
1063
|
+
}
|
|
1064
|
+
/**
|
|
1065
|
+
* Fetch all scenarios for a project from the backend API
|
|
1066
|
+
*/
|
|
1067
|
+
static async fetchProjectScenarios(projectId, apiToken, apiUrl) {
|
|
1068
|
+
const normalizedUrl = this.normalizeApiUrl(apiUrl);
|
|
1069
|
+
// Scenarios endpoint is at root level, not under /api/
|
|
1070
|
+
const url = `${normalizedUrl}/scenarios/?project_id=${projectId}`;
|
|
1071
|
+
const response = await fetch(url, {
|
|
1072
|
+
method: 'GET',
|
|
1073
|
+
headers: {
|
|
1074
|
+
'Authorization': `Token ${apiToken}`,
|
|
1075
|
+
'Content-Type': 'application/json',
|
|
1076
|
+
},
|
|
1077
|
+
});
|
|
1078
|
+
if (!response.ok) {
|
|
1079
|
+
const errorText = await this.readResponseErrorText(response);
|
|
1080
|
+
throw new Error(`Failed to fetch scenarios for project ${projectId}: ${response.status} ${errorText}`);
|
|
1081
|
+
}
|
|
1082
|
+
const data = await response.json();
|
|
1083
|
+
// Handle both array and paginated responses
|
|
1084
|
+
if (Array.isArray(data)) {
|
|
1085
|
+
return data;
|
|
1086
|
+
}
|
|
1087
|
+
if (data.results && Array.isArray(data.results)) {
|
|
1088
|
+
return data.results;
|
|
1089
|
+
}
|
|
1090
|
+
return [];
|
|
1091
|
+
}
|
|
1043
1092
|
/**
|
|
1044
1093
|
* Convert backend interaction format to Interaction[] format
|
|
1045
1094
|
*/
|
|
@@ -1190,6 +1239,48 @@ class ProboCodeGenerator {
|
|
|
1190
1239
|
scenarios: scenarioResults,
|
|
1191
1240
|
};
|
|
1192
1241
|
}
|
|
1242
|
+
/**
|
|
1243
|
+
* Generate code for all scenarios in a project
|
|
1244
|
+
* Returns map of scenario names to generated code
|
|
1245
|
+
*/
|
|
1246
|
+
static async generateCodeForProject(projectId, apiToken, apiUrl, options) {
|
|
1247
|
+
// Validate inputs
|
|
1248
|
+
if (!apiToken) {
|
|
1249
|
+
throw new Error('API token is required');
|
|
1250
|
+
}
|
|
1251
|
+
if (!apiUrl) {
|
|
1252
|
+
throw new Error('API URL is required');
|
|
1253
|
+
}
|
|
1254
|
+
// Fetch project data and scenarios
|
|
1255
|
+
const [projectData, scenarios] = await Promise.all([
|
|
1256
|
+
this.fetchProjectData(projectId, apiToken, apiUrl),
|
|
1257
|
+
this.fetchProjectScenarios(projectId, apiToken, apiUrl),
|
|
1258
|
+
]);
|
|
1259
|
+
if (scenarios.length === 0) {
|
|
1260
|
+
throw new Error(`No scenarios found in project "${projectData.name}" (ID: ${projectId})`);
|
|
1261
|
+
}
|
|
1262
|
+
// Generate code for each scenario
|
|
1263
|
+
const scenarioResults = await Promise.all(scenarios.map(async (scenario) => {
|
|
1264
|
+
try {
|
|
1265
|
+
const code = await this.generateCodeForScenario(scenario.id, apiToken, apiUrl, options);
|
|
1266
|
+
return {
|
|
1267
|
+
scenarioId: scenario.id,
|
|
1268
|
+
scenarioName: scenario.name,
|
|
1269
|
+
code: code,
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
catch (error) {
|
|
1273
|
+
// Log error but continue with other scenarios
|
|
1274
|
+
console.error(`Failed to generate code for scenario ${scenario.id}: ${error.message}`);
|
|
1275
|
+
throw error; // Re-throw to fail fast for now
|
|
1276
|
+
}
|
|
1277
|
+
}));
|
|
1278
|
+
return {
|
|
1279
|
+
projectId: projectData.id,
|
|
1280
|
+
projectName: projectData.name,
|
|
1281
|
+
scenarios: scenarioResults,
|
|
1282
|
+
};
|
|
1283
|
+
}
|
|
1193
1284
|
}
|
|
1194
1285
|
|
|
1195
1286
|
const execAsync = promisify(exec);
|
|
@@ -1312,11 +1403,12 @@ async function updateRunStatus(runId, testSuiteId, updates, apiToken, apiUrl) {
|
|
|
1312
1403
|
*/
|
|
1313
1404
|
class TestSuiteRunner {
|
|
1314
1405
|
/**
|
|
1315
|
-
* Lookup
|
|
1406
|
+
* Lookup project ID by name
|
|
1316
1407
|
*/
|
|
1317
|
-
static async
|
|
1408
|
+
static async lookupProjectByName(projectName, apiToken, apiUrl) {
|
|
1318
1409
|
const baseUrl = apiUrl.endsWith('/') ? apiUrl.slice(0, -1) : apiUrl;
|
|
1319
|
-
|
|
1410
|
+
// Projects endpoint is at root level, not under /api/
|
|
1411
|
+
const url = `${baseUrl}/projects/?name=${encodeURIComponent(projectName)}`;
|
|
1320
1412
|
const response = await fetch$1(url, {
|
|
1321
1413
|
method: 'GET',
|
|
1322
1414
|
headers: {
|
|
@@ -1326,25 +1418,35 @@ class TestSuiteRunner {
|
|
|
1326
1418
|
});
|
|
1327
1419
|
if (!response.ok) {
|
|
1328
1420
|
const errorText = await response.text();
|
|
1329
|
-
throw new Error(`Failed to lookup
|
|
1421
|
+
throw new Error(`Failed to lookup project: ${response.status} ${errorText}`);
|
|
1330
1422
|
}
|
|
1331
1423
|
const data = await response.json();
|
|
1332
|
-
// TEMPORARY DEBUG: Output the full response
|
|
1333
|
-
console.log('🔍 DEBUG: API Response URL:', url);
|
|
1334
|
-
console.log('🔍 DEBUG: API Response Status:', response.status);
|
|
1335
|
-
console.log('🔍 DEBUG: API Response Data:', JSON.stringify(data, null, 2));
|
|
1336
|
-
console.log('🔍 DEBUG: Is Array?', Array.isArray(data));
|
|
1337
|
-
console.log('🔍 DEBUG: Array length:', Array.isArray(data) ? data.length : 'N/A');
|
|
1338
|
-
console.log('🔍 DEBUG: Has results?', !!data.results);
|
|
1339
|
-
console.log('🔍 DEBUG: Results is Array?', Array.isArray(data.results));
|
|
1340
|
-
console.log('🔍 DEBUG: Results length:', Array.isArray(data.results) ? data.results.length : 'N/A');
|
|
1341
1424
|
if (Array.isArray(data) && data.length > 0) {
|
|
1342
|
-
|
|
1425
|
+
return data[0].id;
|
|
1343
1426
|
}
|
|
1344
1427
|
if (data.results && Array.isArray(data.results) && data.results.length > 0) {
|
|
1345
|
-
|
|
1428
|
+
return data.results[0].id;
|
|
1429
|
+
}
|
|
1430
|
+
throw new Error(`Project "${projectName}" not found`);
|
|
1431
|
+
}
|
|
1432
|
+
/**
|
|
1433
|
+
* Lookup test suite ID by name and project
|
|
1434
|
+
*/
|
|
1435
|
+
static async lookupTestSuiteByName(testSuiteName, projectName, apiToken, apiUrl) {
|
|
1436
|
+
const baseUrl = apiUrl.endsWith('/') ? apiUrl.slice(0, -1) : apiUrl;
|
|
1437
|
+
const url = `${baseUrl}/test-suites/?name=${encodeURIComponent(testSuiteName)}&project=${encodeURIComponent(projectName)}`;
|
|
1438
|
+
const response = await fetch$1(url, {
|
|
1439
|
+
method: 'GET',
|
|
1440
|
+
headers: {
|
|
1441
|
+
'Authorization': `Token ${apiToken}`,
|
|
1442
|
+
'Content-Type': 'application/json',
|
|
1443
|
+
},
|
|
1444
|
+
});
|
|
1445
|
+
if (!response.ok) {
|
|
1446
|
+
const errorText = await response.text();
|
|
1447
|
+
throw new Error(`Failed to lookup test suite: ${response.status} ${errorText}`);
|
|
1346
1448
|
}
|
|
1347
|
-
|
|
1449
|
+
const data = await response.json();
|
|
1348
1450
|
if (Array.isArray(data) && data.length > 0) {
|
|
1349
1451
|
return data[0].id;
|
|
1350
1452
|
}
|
|
@@ -1462,7 +1564,10 @@ class TestSuiteRunner {
|
|
|
1462
1564
|
}
|
|
1463
1565
|
}
|
|
1464
1566
|
}
|
|
1465
|
-
|
|
1567
|
+
// Type guard: check for projectId to distinguish ProjectCodeGenResult from TestSuiteCodeGenResult
|
|
1568
|
+
const sanitizedName = 'projectId' in codeGenResult && 'projectName' in codeGenResult
|
|
1569
|
+
? slugify(codeGenResult.projectName)
|
|
1570
|
+
: slugify(codeGenResult.testSuiteName);
|
|
1466
1571
|
const packageJsonContent = generatePackageJson({
|
|
1467
1572
|
name: sanitizedName,
|
|
1468
1573
|
hasSecrets: hasSecrets,
|
|
@@ -1495,12 +1600,12 @@ class TestSuiteRunner {
|
|
|
1495
1600
|
* Generates files, installs dependencies, and executes tests
|
|
1496
1601
|
*/
|
|
1497
1602
|
static async runTestSuite(testSuiteId, apiToken, apiUrl, testSuiteName, runId, options = {}) {
|
|
1498
|
-
const { outputDir, includeReporter = true, playwrightArgs = [], onStatusUpdate, onStdout, onStderr, onReporterEvent, } = options;
|
|
1603
|
+
const { outputDir, includeReporter = true, playwrightArgs = [], skipRunCreation = false, onStatusUpdate, onStdout, onStderr, onReporterEvent, } = options;
|
|
1499
1604
|
const testSuiteDir = outputDir || getDefaultTestSuiteDir(testSuiteId, testSuiteName);
|
|
1500
1605
|
let currentRunId = runId;
|
|
1501
1606
|
try {
|
|
1502
|
-
// Create run record if not provided
|
|
1503
|
-
if (!currentRunId) {
|
|
1607
|
+
// Create run record if not provided and not skipping run creation
|
|
1608
|
+
if (!currentRunId && !skipRunCreation) {
|
|
1504
1609
|
try {
|
|
1505
1610
|
const createResponse = await fetch$1(`${apiUrl}/test-suites/${testSuiteId}/runs/`, {
|
|
1506
1611
|
method: 'POST',
|
|
@@ -1925,6 +2030,233 @@ class TestSuiteRunner {
|
|
|
1925
2030
|
};
|
|
1926
2031
|
}
|
|
1927
2032
|
}
|
|
2033
|
+
/**
|
|
2034
|
+
* Generate all files for a project (all scenarios)
|
|
2035
|
+
*/
|
|
2036
|
+
static async generateProjectFiles(projectId, apiToken, apiUrl, outputDir, projectName, includeReporter = true) {
|
|
2037
|
+
const projectDir = outputDir || getDefaultTestSuiteDir(projectId, projectName || `project-${projectId}`);
|
|
2038
|
+
// Generate code for all scenarios
|
|
2039
|
+
const codeGenResult = await ProboCodeGenerator.generateCodeForProject(projectId, apiToken, apiUrl);
|
|
2040
|
+
if (codeGenResult.scenarios.length === 0) {
|
|
2041
|
+
throw new Error(`No scenarios found in project "${codeGenResult.projectName}" (ID: ${projectId}). Cannot generate test files.`);
|
|
2042
|
+
}
|
|
2043
|
+
// Delete everything in the project directory except node_modules
|
|
2044
|
+
if (fs.existsSync(projectDir)) {
|
|
2045
|
+
try {
|
|
2046
|
+
const nodeModulesPath = path.join(projectDir, 'node_modules');
|
|
2047
|
+
const hasNodeModules = fs.existsSync(nodeModulesPath);
|
|
2048
|
+
// Temporarily move node_modules out of the way if it exists
|
|
2049
|
+
let tempNodeModulesPath = null;
|
|
2050
|
+
if (hasNodeModules) {
|
|
2051
|
+
tempNodeModulesPath = path.join(projectDir, '..', `node_modules.temp.${projectId}`);
|
|
2052
|
+
// Remove temp directory if it exists from a previous failed run
|
|
2053
|
+
if (fs.existsSync(tempNodeModulesPath)) {
|
|
2054
|
+
fs.rmSync(tempNodeModulesPath, { recursive: true, force: true });
|
|
2055
|
+
}
|
|
2056
|
+
fs.renameSync(nodeModulesPath, tempNodeModulesPath);
|
|
2057
|
+
console.log(`📦 Preserved node_modules temporarily`);
|
|
2058
|
+
}
|
|
2059
|
+
// Delete everything in the directory
|
|
2060
|
+
const entries = fs.readdirSync(projectDir, { withFileTypes: true });
|
|
2061
|
+
for (const entry of entries) {
|
|
2062
|
+
const entryPath = path.join(projectDir, entry.name);
|
|
2063
|
+
try {
|
|
2064
|
+
if (entry.isDirectory()) {
|
|
2065
|
+
fs.rmSync(entryPath, { recursive: true, force: true });
|
|
2066
|
+
}
|
|
2067
|
+
else {
|
|
2068
|
+
fs.unlinkSync(entryPath);
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
catch (error) {
|
|
2072
|
+
console.warn(`⚠️ Failed to delete ${entry.name}:`, error);
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
console.log(`🗑️ Cleaned project directory (preserved node_modules)`);
|
|
2076
|
+
// Move node_modules back
|
|
2077
|
+
if (hasNodeModules && tempNodeModulesPath) {
|
|
2078
|
+
fs.renameSync(tempNodeModulesPath, nodeModulesPath);
|
|
2079
|
+
console.log(`📦 Restored node_modules`);
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
catch (error) {
|
|
2083
|
+
console.warn(`⚠️ Failed to clean project directory ${projectDir}:`, error);
|
|
2084
|
+
// Continue anyway - we'll overwrite files as needed
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
// Ensure directories exist (will recreate after deletion)
|
|
2088
|
+
ensureDirectoryExists(projectDir);
|
|
2089
|
+
const testsDir = path.join(projectDir, 'tests');
|
|
2090
|
+
ensureDirectoryExists(testsDir);
|
|
2091
|
+
// Save each scenario's code to a .spec.ts file
|
|
2092
|
+
for (const scenario of codeGenResult.scenarios) {
|
|
2093
|
+
const fileName = `${slugify(scenario.scenarioName)}.spec.ts`;
|
|
2094
|
+
const filePath = path.join(testsDir, fileName);
|
|
2095
|
+
fs.writeFileSync(filePath, scenario.code, 'utf-8');
|
|
2096
|
+
console.log(`✅ Generated test file: ${filePath}`);
|
|
2097
|
+
}
|
|
2098
|
+
// Generate package.json
|
|
2099
|
+
await this.generatePackageJson(projectDir, codeGenResult);
|
|
2100
|
+
// Generate playwright.config.ts (no runId for project runs)
|
|
2101
|
+
await this.generatePlaywrightConfig(projectDir, includeReporter, undefined);
|
|
2102
|
+
// Generate custom reporter file for live progress streaming (only if requested)
|
|
2103
|
+
if (includeReporter) {
|
|
2104
|
+
await this.generateProboReporter(projectDir);
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
/**
|
|
2108
|
+
* Run all scenarios in a project
|
|
2109
|
+
* Generates files, installs dependencies, and executes tests
|
|
2110
|
+
* Does not create run records in the database (CLI mode)
|
|
2111
|
+
*/
|
|
2112
|
+
static async runProjectScenarios(projectId, apiToken, apiUrl, projectName, options = {}) {
|
|
2113
|
+
const { outputDir, includeReporter = false, // CLI mode - no custom reporter
|
|
2114
|
+
playwrightArgs = [], onStdout, onStderr, } = options;
|
|
2115
|
+
const projectDir = outputDir || getDefaultTestSuiteDir(projectId, projectName || `project-${projectId}`);
|
|
2116
|
+
try {
|
|
2117
|
+
// Generate all files
|
|
2118
|
+
console.log(`📝 Generating test files for project ${projectId}...`);
|
|
2119
|
+
await this.generateProjectFiles(projectId, apiToken, apiUrl, projectDir, projectName, includeReporter);
|
|
2120
|
+
// Count total tests
|
|
2121
|
+
const testsTotal = countTotalTests(projectDir);
|
|
2122
|
+
if (testsTotal === 0) {
|
|
2123
|
+
throw new Error(`No test files generated for project ${projectId}`);
|
|
2124
|
+
}
|
|
2125
|
+
// Install dependencies
|
|
2126
|
+
console.log(`📦 Installing dependencies in ${projectDir}...`);
|
|
2127
|
+
try {
|
|
2128
|
+
const { stdout: installStdout, stderr: installStderr } = await execAsync('npm install', { cwd: projectDir, timeout: 300000 } // 5 minute timeout for install
|
|
2129
|
+
);
|
|
2130
|
+
console.log('✅ Dependencies installed successfully');
|
|
2131
|
+
if (installStdout)
|
|
2132
|
+
console.log(installStdout);
|
|
2133
|
+
if (installStderr)
|
|
2134
|
+
console.warn(installStderr);
|
|
2135
|
+
}
|
|
2136
|
+
catch (installError) {
|
|
2137
|
+
console.error('❌ Failed to install dependencies:', installError);
|
|
2138
|
+
const errorMsg = `Failed to install dependencies: ${installError.message}`;
|
|
2139
|
+
return {
|
|
2140
|
+
success: false,
|
|
2141
|
+
exitCode: installError.code || 1,
|
|
2142
|
+
stdout: installError.stdout || '',
|
|
2143
|
+
stderr: installError.stderr || installError.message || '',
|
|
2144
|
+
error: errorMsg,
|
|
2145
|
+
};
|
|
2146
|
+
}
|
|
2147
|
+
// Run Playwright tests with streaming output
|
|
2148
|
+
console.log(`🚀 Running Playwright tests in ${projectDir}...`);
|
|
2149
|
+
if (playwrightArgs.length > 0) {
|
|
2150
|
+
console.log(`🧪 Playwright extra args: ${playwrightArgs.join(' ')}`);
|
|
2151
|
+
}
|
|
2152
|
+
return new Promise(async (resolve) => {
|
|
2153
|
+
var _a, _b;
|
|
2154
|
+
let stdout = '';
|
|
2155
|
+
let stderr = '';
|
|
2156
|
+
let hasResolved = false;
|
|
2157
|
+
let stdoutLineBuffer = '';
|
|
2158
|
+
// Use spawn for streaming output
|
|
2159
|
+
const testProcess = spawn('npx', ['playwright', 'test', ...playwrightArgs], {
|
|
2160
|
+
cwd: projectDir,
|
|
2161
|
+
shell: true,
|
|
2162
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
2163
|
+
});
|
|
2164
|
+
// Stream stdout line by line
|
|
2165
|
+
(_a = testProcess.stdout) === null || _a === void 0 ? void 0 : _a.on('data', async (data) => {
|
|
2166
|
+
var _a;
|
|
2167
|
+
const chunk = data.toString();
|
|
2168
|
+
stdoutLineBuffer += chunk;
|
|
2169
|
+
// Process complete lines
|
|
2170
|
+
const parts = stdoutLineBuffer.split(/\n/);
|
|
2171
|
+
stdoutLineBuffer = (_a = parts.pop()) !== null && _a !== void 0 ? _a : '';
|
|
2172
|
+
for (const rawLine of parts) {
|
|
2173
|
+
const line = rawLine.endsWith('\r') ? rawLine.slice(0, -1) : rawLine;
|
|
2174
|
+
stdout += rawLine + '\n';
|
|
2175
|
+
// Call stdout callback if provided
|
|
2176
|
+
if (onStdout) {
|
|
2177
|
+
await onStdout(rawLine + '\n');
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
});
|
|
2181
|
+
// Stream stderr line by line
|
|
2182
|
+
(_b = testProcess.stderr) === null || _b === void 0 ? void 0 : _b.on('data', async (data) => {
|
|
2183
|
+
const chunk = data.toString();
|
|
2184
|
+
stderr += chunk;
|
|
2185
|
+
// Call stderr callback if provided
|
|
2186
|
+
if (onStderr) {
|
|
2187
|
+
await onStderr(chunk);
|
|
2188
|
+
}
|
|
2189
|
+
});
|
|
2190
|
+
// Handle process completion
|
|
2191
|
+
testProcess.on('close', async (code) => {
|
|
2192
|
+
if (hasResolved)
|
|
2193
|
+
return;
|
|
2194
|
+
hasResolved = true;
|
|
2195
|
+
// Flush any remaining buffered stdout
|
|
2196
|
+
if (stdoutLineBuffer) {
|
|
2197
|
+
stdout += stdoutLineBuffer;
|
|
2198
|
+
if (onStdout) {
|
|
2199
|
+
await onStdout(stdoutLineBuffer);
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
const exitCode = code !== null && code !== void 0 ? code : 1;
|
|
2203
|
+
const success = exitCode === 0;
|
|
2204
|
+
const isTestFailure = exitCode === 1 && stdout.length > 0;
|
|
2205
|
+
console.log(success ? '✅ Tests completed successfully' : (isTestFailure ? '⚠️ Tests completed with failures' : '❌ Test execution failed'));
|
|
2206
|
+
resolve({
|
|
2207
|
+
success: success,
|
|
2208
|
+
exitCode: exitCode,
|
|
2209
|
+
stdout: stdout,
|
|
2210
|
+
stderr: stderr,
|
|
2211
|
+
error: success || isTestFailure ? undefined : `Test execution failed with exit code ${exitCode}`,
|
|
2212
|
+
});
|
|
2213
|
+
});
|
|
2214
|
+
// Handle process errors
|
|
2215
|
+
testProcess.on('error', async (error) => {
|
|
2216
|
+
if (hasResolved)
|
|
2217
|
+
return;
|
|
2218
|
+
hasResolved = true;
|
|
2219
|
+
const errorMessage = error.message || String(error);
|
|
2220
|
+
stderr += errorMessage;
|
|
2221
|
+
console.error('❌ Test execution failed:', error);
|
|
2222
|
+
resolve({
|
|
2223
|
+
success: false,
|
|
2224
|
+
exitCode: 1,
|
|
2225
|
+
stdout: stdout,
|
|
2226
|
+
stderr: stderr,
|
|
2227
|
+
error: `Test execution failed: ${errorMessage}`,
|
|
2228
|
+
});
|
|
2229
|
+
});
|
|
2230
|
+
// Set timeout (1 hour)
|
|
2231
|
+
setTimeout(async () => {
|
|
2232
|
+
if (!hasResolved) {
|
|
2233
|
+
hasResolved = true;
|
|
2234
|
+
testProcess.kill();
|
|
2235
|
+
const errorMessage = 'Test execution timed out after 1 hour';
|
|
2236
|
+
stderr += errorMessage;
|
|
2237
|
+
resolve({
|
|
2238
|
+
success: false,
|
|
2239
|
+
exitCode: 1,
|
|
2240
|
+
stdout: stdout,
|
|
2241
|
+
stderr: stderr,
|
|
2242
|
+
error: errorMessage,
|
|
2243
|
+
});
|
|
2244
|
+
}
|
|
2245
|
+
}, 3600000); // 1 hour timeout
|
|
2246
|
+
});
|
|
2247
|
+
}
|
|
2248
|
+
catch (error) {
|
|
2249
|
+
console.error('❌ Error running project scenarios:', error);
|
|
2250
|
+
const errorMsg = error.message || String(error);
|
|
2251
|
+
return {
|
|
2252
|
+
success: false,
|
|
2253
|
+
exitCode: 1,
|
|
2254
|
+
stdout: '',
|
|
2255
|
+
stderr: errorMsg,
|
|
2256
|
+
error: errorMsg,
|
|
2257
|
+
};
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
1928
2260
|
/**
|
|
1929
2261
|
* Upload artifacts for a test suite run
|
|
1930
2262
|
*
|
|
@@ -1945,6 +2277,59 @@ async function runCLI(options) {
|
|
|
1945
2277
|
process.exit(1);
|
|
1946
2278
|
}
|
|
1947
2279
|
const apiUrl = options.apiEndpoint || process.env.PROBO_API_ENDPOINT || 'https://api.probolabs.ai';
|
|
2280
|
+
// Check if running project-only mode (--project without test suite)
|
|
2281
|
+
if (options.project && !options.testSuiteId && !options.testSuiteName) {
|
|
2282
|
+
// Run all scenarios in the project
|
|
2283
|
+
try {
|
|
2284
|
+
console.log(`🔍 Looking up project "${options.project}"...`);
|
|
2285
|
+
const projectId = await TestSuiteRunner.lookupProjectByName(options.project, apiToken, apiUrl);
|
|
2286
|
+
console.log(`✅ Found project ID: ${projectId}`);
|
|
2287
|
+
const result = await TestSuiteRunner.runProjectScenarios(projectId, apiToken, apiUrl, options.project, {
|
|
2288
|
+
outputDir: options.outputDir,
|
|
2289
|
+
includeReporter: false, // CI/CD mode - no custom reporter
|
|
2290
|
+
playwrightArgs: options.playwrightArgs,
|
|
2291
|
+
skipRunCreation: true, // CLI never creates run records
|
|
2292
|
+
// Stream stdout/stderr to console in real-time
|
|
2293
|
+
onStdout: (chunk) => {
|
|
2294
|
+
process.stdout.write(chunk);
|
|
2295
|
+
},
|
|
2296
|
+
onStderr: (chunk) => {
|
|
2297
|
+
process.stderr.write(chunk);
|
|
2298
|
+
},
|
|
2299
|
+
});
|
|
2300
|
+
if (result.success) {
|
|
2301
|
+
console.log('✅ Project scenarios completed successfully');
|
|
2302
|
+
process.exit(0);
|
|
2303
|
+
}
|
|
2304
|
+
else {
|
|
2305
|
+
console.error(`❌ Project scenarios failed with exit code ${result.exitCode}`);
|
|
2306
|
+
if (result.error) {
|
|
2307
|
+
console.error(`Error: ${result.error}`);
|
|
2308
|
+
}
|
|
2309
|
+
// Print stdout/stderr to help debug failures
|
|
2310
|
+
console.log(`\n--- STDOUT (${result.stdout.length} chars) ---`);
|
|
2311
|
+
if (result.stdout.trim()) {
|
|
2312
|
+
console.log(result.stdout);
|
|
2313
|
+
}
|
|
2314
|
+
else {
|
|
2315
|
+
console.log('(empty)');
|
|
2316
|
+
}
|
|
2317
|
+
console.error(`\n--- STDERR (${result.stderr.length} chars) ---`);
|
|
2318
|
+
if (result.stderr.trim()) {
|
|
2319
|
+
console.error(result.stderr);
|
|
2320
|
+
}
|
|
2321
|
+
else {
|
|
2322
|
+
console.error('(empty)');
|
|
2323
|
+
}
|
|
2324
|
+
process.exit(result.exitCode || 1);
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
catch (error) {
|
|
2328
|
+
console.error(`❌ Error running project scenarios: ${error.message}`);
|
|
2329
|
+
process.exit(1);
|
|
2330
|
+
}
|
|
2331
|
+
return; // Exit early for project-only mode
|
|
2332
|
+
}
|
|
1948
2333
|
let testSuiteId;
|
|
1949
2334
|
// Resolve test suite ID
|
|
1950
2335
|
if (options.testSuiteId) {
|
|
@@ -1966,16 +2351,17 @@ async function runCLI(options) {
|
|
|
1966
2351
|
}
|
|
1967
2352
|
}
|
|
1968
2353
|
else {
|
|
1969
|
-
console.error('❌ Error: Either --test-suite-id
|
|
2354
|
+
console.error('❌ Error: Either --test-suite-id, --test-suite-name, or --project must be provided');
|
|
1970
2355
|
process.exit(1);
|
|
1971
2356
|
}
|
|
1972
2357
|
// Run the test suite
|
|
1973
2358
|
try {
|
|
1974
|
-
const result = await TestSuiteRunner.runTestSuite(testSuiteId, apiToken, apiUrl, options.testSuiteName, undefined, // runId - will be created
|
|
2359
|
+
const result = await TestSuiteRunner.runTestSuite(testSuiteId, apiToken, apiUrl, options.testSuiteName, undefined, // runId - will not be created (CLI mode)
|
|
1975
2360
|
{
|
|
1976
2361
|
outputDir: options.outputDir,
|
|
1977
2362
|
includeReporter: false, // CI/CD mode - no custom reporter
|
|
1978
2363
|
playwrightArgs: options.playwrightArgs,
|
|
2364
|
+
skipRunCreation: true, // CLI never creates run records
|
|
1979
2365
|
// Stream stdout/stderr to console in real-time
|
|
1980
2366
|
onStdout: (chunk) => {
|
|
1981
2367
|
process.stdout.write(chunk);
|