estructura_automation
Version:
Paquete de estructura de automation
199 lines (170 loc) • 6.44 kB
JavaScript
/* eslint-disable @typescript-eslint/no-require-imports */
/**
* ========================================================================================
* TEST SCANNER - Escaneo y Parsing de Archivos de Prueba
* ----------------------------------------------------------------------------------------
* Responsabilidades:
* - Parsear archivos .feature (Gherkin)
* - Escanear directorios recursivamente
* - Construir estructura jerárquica de datos
* - Formatear nombres para visualización
* ========================================================================================
*/
const fs = require('fs');
const path = require('path');
/**
* Analiza el contenido de un archivo .feature y extrae su estructura
* @param {string} content - Contenido del archivo .feature
* @returns {Object} Objeto estructurado con escenarios y sus pasos
*/
const parseFeatureFile = (content) => {
const lines = content.split('\n');
const feature = {
scenarios: []
};
let currentScenario = null;
let inScenario = false;
let pendingTags = [];
lines.forEach(line => {
const trimmedLine = line.trim();
// Detectar tags como @manual
if (trimmedLine.startsWith('@')) {
pendingTags.push(trimmedLine);
return;
}
if (trimmedLine.startsWith('Scenario:') || trimmedLine.startsWith('Scenario Outline:')) {
inScenario = true;
if (currentScenario) {
feature.scenarios.push(currentScenario);
}
currentScenario = {
name: trimmedLine.substring(trimmedLine.indexOf(':') + 1).trim(),
steps: [],
tags: pendingTags.slice() // Copiar tags pendientes
};
pendingTags = []; // Limpiar tags para el próximo escenario
} else if (inScenario && (
trimmedLine.startsWith('Given ') ||
trimmedLine.startsWith('When ') ||
trimmedLine.startsWith('Then ') ||
trimmedLine.startsWith('And ') ||
trimmedLine.startsWith('But ')
)) {
// Eliminar las palabras reservadas de Gherkin completamente
let stepText = trimmedLine;
// Eliminar palabras reservadas al inicio de la línea
stepText = stepText.replace(/^Given /, '');
stepText = stepText.replace(/^When /, '');
stepText = stepText.replace(/^Then /, '');
stepText = stepText.replace(/^And /, '');
stepText = stepText.replace(/^But /, '');
currentScenario.steps.push(stepText);
}
});
if (currentScenario) {
feature.scenarios.push(currentScenario);
}
return feature;
};
/**
* Formatea un nombre para visualización
* @param {string} name - Nombre a formatear
* @returns {string} Nombre formateado
*/
const formatName = (name) => {
return name.charAt(0).toUpperCase() + name.slice(1).replace(/_/g, ' ');
};
/**
* Recorre recursivamente el directorio de features y construye la estructura jerárquica
* @param {string} featuresDir - Directorio base de features
* @returns {Object} Estructura de datos organizada jerárquicamente
*/
const buildHierarchicalData = (featuresDir) => {
const hierarchicalData = {
apps: {}
};
/**
* Función recursiva mejorada para procesar directorios de cualquier profundidad
* @param {string} currentPath - Ruta actual
* @param {string} parentId - ID del padre
* @param {Array} pathArray - Array con la ruta completa para breadcrumbs
* @returns {Object} Nodo procesado
*/
const processDirectory = (currentPath, parentId = '', pathArray = []) => {
const entries = fs.readdirSync(currentPath, { withFileTypes: true });
const baseName = path.basename(currentPath);
const result = {
id: parentId || baseName.toLowerCase().replace(/\s+/g, '-'),
name: formatName(baseName),
path: pathArray.slice(), // Copia del path completo
testCases: [],
children: {},
hasSubfolders: false,
hasFeatures: false
};
// 1. ORDENAR: Carpetas primero, luego archivos .feature
const directories = entries.filter(entry => entry.isDirectory()).sort((a, b) => a.name.localeCompare(b.name));
const featureFiles = entries.filter(entry => !entry.isDirectory() && entry.name.endsWith('.feature')).sort((a, b) => a.name.localeCompare(b.name));
// 2. PROCESAR ARCHIVOS .FEATURE (casos de prueba en este nivel)
if (featureFiles.length > 0) {
result.hasFeatures = true;
let globalTestCaseCounter = 1; // Contador global para este directorio
featureFiles.forEach(featureFile => {
const featureContent = fs.readFileSync(path.join(currentPath, featureFile.name), 'utf8');
const feature = parseFeatureFile(featureContent);
feature.scenarios.forEach((scenario) => {
const testCaseId = `${result.id}_tc_${globalTestCaseCounter}`;
const isManual = scenario.tags && scenario.tags.includes('@manual');
result.testCases.push({
id: testCaseId,
name: scenario.name,
isManual: isManual,
tags: scenario.tags || [],
featureFile: featureFile.name,
steps: scenario.steps.map((step, stepIndex) => ({
id: `${testCaseId}_step_${stepIndex + 1}`,
text: step
}))
});
globalTestCaseCounter++;
});
});
}
// 3. PROCESAR SUBDIRECTORIOS RECURSIVAMENTE
if (directories.length > 0) {
result.hasSubfolders = true;
directories.forEach(dir => {
const childId = `${result.id}_${dir.name.toLowerCase().replace(/\s+/g, '-')}`;
const childPath = [...pathArray, result.name];
result.children[childId] = processDirectory(
path.join(currentPath, dir.name),
childId,
childPath
);
});
}
return result;
};
// Verificar que el directorio existe
if (!fs.existsSync(featuresDir)) {
throw new Error(`El directorio ${featuresDir} no existe`);
}
// Procesar cada app como raíz
const appDirs = fs.readdirSync(featuresDir, { withFileTypes: true })
.filter(entry => entry.isDirectory())
.sort((a, b) => a.name.localeCompare(b.name))
.map(dir => dir.name);
if (appDirs.length === 0) {
throw new Error('No se encontraron carpetas dentro del directorio de features');
}
appDirs.forEach(appName => {
const appId = appName.toLowerCase().replace(/\s+/g, '-');
hierarchicalData.apps[appId] = processDirectory(path.join(featuresDir, appName), appId, []);
});
return hierarchicalData;
};
module.exports = {
parseFeatureFile,
formatName,
buildHierarchicalData
};