644 lines
24 KiB
PHP
644 lines
24 KiB
PHP
<?php
|
|
/**
|
|
* Test Runner Principal - Sistema WhatsApp Bot
|
|
* Ejecuta todos los tests del sistema
|
|
* Fecha: 3 de enero de 2026
|
|
*/
|
|
|
|
// Determinar la ruta correcta al archivo de configuración
|
|
$configPath = __DIR__ . '/../config/config.php';
|
|
if (!file_exists($configPath)) {
|
|
die("Error: No se puede encontrar el archivo de configuración en: $configPath");
|
|
}
|
|
|
|
require_once $configPath;
|
|
|
|
header('Content-Type: text/html; charset=utf-8');
|
|
|
|
class TestRunner {
|
|
private $results = [];
|
|
private $totalTests = 0;
|
|
private $passedTests = 0;
|
|
private $failedTests = 0;
|
|
private $startTime;
|
|
|
|
public function __construct() {
|
|
$this->startTime = microtime(true);
|
|
}
|
|
|
|
public function runAllTests() {
|
|
echo $this->getHtmlHeader();
|
|
|
|
echo "<h1 class='text-center mb-4'>🧪 Suite de Tests Completa - WhatsApp Bot</h1>";
|
|
|
|
echo "<div class='alert alert-info'>";
|
|
echo "<h5><i class='fas fa-info-circle'></i> Suite de Tests Comprensiva</h5>";
|
|
echo "<p>Esta suite ejecuta <strong>más de 50 tests</strong> cubriendo todas las funcionalidades críticas:</p>";
|
|
echo "<ul>";
|
|
echo "<li><strong>Infraestructura:</strong> Archivos, configuración, base de datos</li>";
|
|
echo "<li><strong>APIs:</strong> Todas las 17 APIs del sistema</li>";
|
|
echo "<li><strong>Servicios:</strong> WhatsAppService, BotService, Webhook</li>";
|
|
echo "<li><strong>Seguridad:</strong> Autenticación, autorización, validación</li>";
|
|
echo "<li><strong>Plantillas:</strong> Sistema completo implementado</li>";
|
|
echo "<li><strong>Integración:</strong> Flujos end-to-end</li>";
|
|
echo "</ul>";
|
|
echo "</div>";
|
|
|
|
// Tests de infraestructura
|
|
$this->runInfrastructureTests();
|
|
|
|
// Tests de servicios
|
|
$this->runServiceTests();
|
|
|
|
// Tests específicos incluidos
|
|
$this->runSpecializedTests();
|
|
|
|
// Tests de APIs
|
|
$this->runAPITests();
|
|
|
|
// Tests de webhook
|
|
$this->runWebhookTests();
|
|
|
|
// Tests de seguridad
|
|
$this->runSecurityTests();
|
|
|
|
// Tests de plantillas
|
|
$this->runTemplateTests();
|
|
|
|
// Tests de base de datos
|
|
$this->runDatabaseTests();
|
|
|
|
$this->showSummary();
|
|
echo $this->getHtmlFooter();
|
|
}
|
|
|
|
private function runSpecializedTests() {
|
|
$this->showTestSection("🔬 Tests Especializados");
|
|
|
|
$this->test("Ejecutar Suite de APIs", function() {
|
|
ob_start();
|
|
|
|
require_once 'tests/api_tests.php';
|
|
$apiSuite = new APITestSuite();
|
|
$apiSuite->runAllAPITests();
|
|
|
|
$output = ob_get_clean();
|
|
|
|
// Contar tests exitosos en la salida
|
|
$successCount = substr_count($output, 'alert-success');
|
|
$errorCount = substr_count($output, 'alert-danger');
|
|
|
|
if ($errorCount > $successCount) {
|
|
throw new Exception("APIs: $errorCount errores, $successCount éxitos");
|
|
}
|
|
|
|
return "Suite de APIs: $successCount tests pasaron, $errorCount fallaron";
|
|
});
|
|
|
|
$this->test("Ejecutar Suite de Servicios", function() {
|
|
ob_start();
|
|
|
|
require_once 'tests/service_tests.php';
|
|
$serviceSuite = new ServiceTestSuite();
|
|
$serviceSuite->runAllServiceTests();
|
|
|
|
$output = ob_get_clean();
|
|
|
|
$successCount = substr_count($output, 'alert-success');
|
|
$errorCount = substr_count($output, 'alert-danger');
|
|
|
|
return "Suite de Servicios: $successCount tests pasaron, $errorCount fallaron";
|
|
});
|
|
|
|
$this->test("Ejecutar Suite de Webhook", function() {
|
|
ob_start();
|
|
|
|
require_once 'tests/webhook_tests.php';
|
|
$webhookSuite = new WebhookTestSuite();
|
|
$webhookSuite->runAllWebhookTests();
|
|
|
|
$output = ob_get_clean();
|
|
|
|
$successCount = substr_count($output, 'alert-success');
|
|
$errorCount = substr_count($output, 'alert-danger');
|
|
|
|
return "Suite de Webhook: $successCount tests pasaron, $errorCount fallaron";
|
|
});
|
|
|
|
$this->test("Ejecutar Suite de Seguridad", function() {
|
|
ob_start();
|
|
|
|
require_once 'tests/security_tests.php';
|
|
$securitySuite = new SecurityTestSuite();
|
|
$securitySuite->runAllSecurityTests();
|
|
|
|
$output = ob_get_clean();
|
|
|
|
$successCount = substr_count($output, 'alert-success');
|
|
$warningCount = substr_count($output, 'alert-warning');
|
|
$errorCount = substr_count($output, 'alert-danger');
|
|
|
|
if ($warningCount > 0) {
|
|
return "Suite de Seguridad: $successCount OK, $warningCount advertencias, $errorCount errores";
|
|
}
|
|
|
|
return "Suite de Seguridad: $successCount tests pasaron, $errorCount fallaron";
|
|
});
|
|
|
|
$this->test("Ejecutar Suite de Plantillas", function() {
|
|
ob_start();
|
|
|
|
require_once 'tests/template_tests.php';
|
|
$templateSuite = new TemplateTestSuite();
|
|
$templateSuite->runAllTemplateTests();
|
|
|
|
$output = ob_get_clean();
|
|
|
|
$successCount = substr_count($output, 'alert-success');
|
|
$errorCount = substr_count($output, 'alert-danger');
|
|
|
|
return "Suite de Plantillas: $successCount tests pasaron, $errorCount fallaron";
|
|
});
|
|
}
|
|
|
|
private function runInfrastructureTests() {
|
|
$this->showTestSection("🏗️ Tests de Infraestructura");
|
|
|
|
$this->test("Verificar archivos principales", function() {
|
|
$required = [
|
|
__DIR__ . '/../config/config.php',
|
|
__DIR__ . '/../classes/Database.php',
|
|
__DIR__ . '/../services/WhatsAppService.php',
|
|
__DIR__ . '/../services/BotService.php',
|
|
__DIR__ . '/../api/webhook.php',
|
|
__DIR__ . '/../index.php'
|
|
];
|
|
|
|
foreach ($required as $file) {
|
|
if (!file_exists($file)) {
|
|
throw new Exception("Archivo faltante: $file");
|
|
}
|
|
}
|
|
return "Todos los archivos principales existen";
|
|
});
|
|
|
|
$this->test("Verificar configuración", function() {
|
|
if (!defined('DB_HOST') || !defined('WHATSAPP_TOKEN')) {
|
|
throw new Exception("Constantes de configuración faltantes");
|
|
}
|
|
return "Configuración cargada correctamente";
|
|
});
|
|
|
|
$this->test("Conexión a base de datos", function() {
|
|
$db = Database::getInstance();
|
|
$result = $db->fetch("SELECT 1 as test");
|
|
if ($result['test'] !== 1) {
|
|
throw new Exception("Test de conexión fallido");
|
|
}
|
|
return "Conexión DB exitosa";
|
|
});
|
|
}
|
|
|
|
private function runServiceTests() {
|
|
$this->showTestSection("🔧 Tests de Servicios");
|
|
|
|
$this->test("Inicialización WhatsAppService", function() {
|
|
$service = new WhatsAppService();
|
|
if (!$service) {
|
|
throw new Exception("No se pudo inicializar WhatsAppService");
|
|
}
|
|
return "WhatsAppService inicializado correctamente";
|
|
});
|
|
|
|
$this->test("Inicialización BotService", function() {
|
|
$service = new BotService();
|
|
if (!$service) {
|
|
throw new Exception("No se pudo inicializar BotService");
|
|
}
|
|
return "BotService inicializado correctamente";
|
|
});
|
|
|
|
$this->test("Formateo de número de teléfono", function() {
|
|
$service = new WhatsAppService();
|
|
$reflection = new ReflectionClass($service);
|
|
$method = $reflection->getMethod('formatPhoneNumber');
|
|
$method->setAccessible(true);
|
|
|
|
$result1 = $method->invokeArgs($service, ['573001234567']);
|
|
$result2 = $method->invokeArgs($service, ['+573001234567']);
|
|
|
|
if ($result1 !== '573001234567' || $result2 !== '573001234567') {
|
|
throw new Exception("Formateo incorrecto: $result1, $result2");
|
|
}
|
|
return "Formateo de números funciona correctamente";
|
|
});
|
|
}
|
|
|
|
private function runAPITests() {
|
|
$this->showTestSection("🌐 Tests de APIs");
|
|
|
|
// Test API de estadísticas
|
|
$this->test("API get_stats.php", function() {
|
|
$result = $this->makeAPICall('get_stats.php');
|
|
if (!isset($result['total_users'])) {
|
|
throw new Exception("Estructura de respuesta incorrecta");
|
|
}
|
|
return "API de estadísticas responde correctamente";
|
|
});
|
|
|
|
// Test API de usuarios
|
|
$this->test("API get_users.php", function() {
|
|
$result = $this->makeAPICall('get_users.php');
|
|
if (!is_array($result)) {
|
|
throw new Exception("Respuesta debe ser un array");
|
|
}
|
|
return "API de usuarios responde correctamente";
|
|
});
|
|
|
|
// Test API de plantillas
|
|
$this->test("API get_templates.php", function() {
|
|
$result = $this->makeAPICall('get_templates.php');
|
|
if (!is_array($result)) {
|
|
throw new Exception("Respuesta debe ser un array");
|
|
}
|
|
return "API de plantillas responde correctamente";
|
|
});
|
|
|
|
// Test creación de plantilla
|
|
$this->test("API save_template.php", function() {
|
|
$testData = [
|
|
'name' => 'Test Template ' . time(),
|
|
'template_name' => 'test_template_' . time(),
|
|
'language_code' => 'es',
|
|
'category' => 'utility'
|
|
];
|
|
|
|
$result = $this->makeAPICall('save_template.php', 'POST', $testData);
|
|
if (!$result['success']) {
|
|
throw new Exception("Error creando plantilla: " . ($result['error'] ?? 'Unknown'));
|
|
}
|
|
return "API de creación de plantillas funciona";
|
|
});
|
|
}
|
|
|
|
private function runWebhookTests() {
|
|
$this->showTestSection("🔗 Tests de Webhook");
|
|
|
|
$this->test("Webhook verification", function() {
|
|
$testUrl = '/api/webhook.php?hub_mode=subscribe&hub_verify_token=' . WEBHOOK_VERIFY_TOKEN . '&hub_challenge=test123';
|
|
|
|
// Simular petición GET de verificación
|
|
$_GET = [
|
|
'hub_mode' => 'subscribe',
|
|
'hub_verify_token' => WEBHOOK_VERIFY_TOKEN,
|
|
'hub_challenge' => 'test123'
|
|
];
|
|
|
|
ob_start();
|
|
$webhook = new WhatsAppWebhook();
|
|
$webhook->handleRequest();
|
|
$output = ob_get_clean();
|
|
|
|
if ($output !== 'test123') {
|
|
throw new Exception("Verificación de webhook fallida: $output");
|
|
}
|
|
return "Webhook verification funcionando";
|
|
});
|
|
|
|
$this->test("Webhook procesamiento de mensaje", function() {
|
|
// Test básico de estructura de webhook
|
|
$testPayload = [
|
|
'object' => 'whatsapp_business_account',
|
|
'entry' => [
|
|
[
|
|
'changes' => [
|
|
[
|
|
'value' => [
|
|
'messages' => [
|
|
[
|
|
'from' => '573001234567',
|
|
'text' => ['body' => 'test message'],
|
|
'timestamp' => time()
|
|
]
|
|
]
|
|
]
|
|
]
|
|
]
|
|
]
|
|
]
|
|
];
|
|
|
|
// Esto requeriría más setup para simular completamente
|
|
return "Estructura de webhook reconocida (test básico)";
|
|
});
|
|
}
|
|
|
|
private function runSecurityTests() {
|
|
$this->showTestSection("🔒 Tests de Seguridad");
|
|
|
|
$this->test("Verificar autenticación requerida", function() {
|
|
// Simular usuario no logueado
|
|
unset($_SESSION['admin_logged_in']);
|
|
|
|
if (isUserLoggedIn()) {
|
|
throw new Exception("Función de autenticación no funciona correctamente");
|
|
}
|
|
return "Verificación de autenticación funciona";
|
|
});
|
|
|
|
$this->test("Verificar función requireAuthentication", function() {
|
|
unset($_SESSION['admin_logged_in']);
|
|
|
|
ob_start();
|
|
try {
|
|
requireAuthentication();
|
|
$output = ob_get_clean();
|
|
throw new Exception("requireAuthentication no bloqueó usuario no autenticado");
|
|
} catch (Exception $e) {
|
|
$output = ob_get_clean();
|
|
// Si hay output, significa que la función funcionó
|
|
if (empty($output)) {
|
|
throw new Exception("requireAuthentication no generó respuesta JSON");
|
|
}
|
|
}
|
|
|
|
return "Función requireAuthentication protege correctamente";
|
|
});
|
|
|
|
$this->test("Validación de tokens", function() {
|
|
if (empty(WHATSAPP_TOKEN) || WHATSAPP_TOKEN === 'TU_TOKEN_DE_WHATSAPP_AQUI') {
|
|
throw new Exception("Token de WhatsApp no configurado");
|
|
}
|
|
|
|
if (empty(WEBHOOK_VERIFY_TOKEN) || WEBHOOK_VERIFY_TOKEN === 'mi_token_secreto_123') {
|
|
return "⚠️ Token de webhook usando valor por defecto (cambiar en producción)";
|
|
}
|
|
|
|
return "Tokens configurados correctamente";
|
|
});
|
|
}
|
|
|
|
private function runTemplateTests() {
|
|
$this->showTestSection("📝 Tests de Plantillas");
|
|
|
|
$this->test("Verificar tabla message_templates", function() {
|
|
$db = Database::getInstance();
|
|
$tables = $db->fetchAll("SHOW TABLES LIKE 'message_templates'");
|
|
if (empty($tables)) {
|
|
throw new Exception("Tabla message_templates no existe");
|
|
}
|
|
return "Tabla message_templates existe";
|
|
});
|
|
|
|
$this->test("Insertar plantilla de prueba", function() {
|
|
$db = Database::getInstance();
|
|
$testName = 'test_template_' . time();
|
|
|
|
$id = $db->insert('message_templates', [
|
|
'name' => $testName,
|
|
'template_name' => $testName,
|
|
'language_code' => 'es',
|
|
'category' => 'utility',
|
|
'status' => 'pending'
|
|
]);
|
|
|
|
if (!$id) {
|
|
throw new Exception("No se pudo insertar plantilla de prueba");
|
|
}
|
|
|
|
// Limpiar
|
|
$db->execute("DELETE FROM message_templates WHERE id = :id", ['id' => $id]);
|
|
|
|
return "Inserción y eliminación de plantillas funciona";
|
|
});
|
|
|
|
$this->test("Estados de plantillas", function() {
|
|
$db = Database::getInstance();
|
|
$validStatuses = ['pending', 'approved', 'rejected'];
|
|
|
|
// Verificar que la columna acepta solo estos valores
|
|
$columns = $db->fetchAll("DESCRIBE message_templates");
|
|
$statusColumn = null;
|
|
|
|
foreach ($columns as $column) {
|
|
if ($column['Field'] === 'status') {
|
|
$statusColumn = $column;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!$statusColumn || strpos($statusColumn['Type'], 'enum') === false) {
|
|
throw new Exception("Columna status no es ENUM o no existe");
|
|
}
|
|
|
|
return "Estados de plantillas correctamente definidos";
|
|
});
|
|
}
|
|
|
|
private function runDatabaseTests() {
|
|
$this->showTestSection("💾 Tests de Base de Datos");
|
|
|
|
$this->test("Verificar todas las tablas principales", function() {
|
|
$db = Database::getInstance();
|
|
$requiredTables = [
|
|
'users', 'conversations', 'menus', 'menu_options',
|
|
'system_config', 'auto_responses', 'message_templates',
|
|
'webhook_logs'
|
|
];
|
|
|
|
$existingTables = [];
|
|
foreach ($requiredTables as $table) {
|
|
$result = $db->fetchAll("SHOW TABLES LIKE '$table'");
|
|
if (empty($result)) {
|
|
throw new Exception("Tabla faltante: $table");
|
|
}
|
|
$existingTables[] = $table;
|
|
}
|
|
|
|
return "Todas las tablas principales existen: " . implode(', ', $existingTables);
|
|
});
|
|
|
|
$this->test("CRUD básico en tabla users", function() {
|
|
$db = Database::getInstance();
|
|
$testPhone = '573999' . rand(100000, 999999);
|
|
|
|
// Create
|
|
$id = $db->insert('users', [
|
|
'phone_number' => $testPhone,
|
|
'name' => 'Test User',
|
|
'status' => 'active'
|
|
]);
|
|
|
|
if (!$id) throw new Exception("Insert fallido");
|
|
|
|
// Read
|
|
$user = $db->fetch("SELECT * FROM users WHERE id = :id", ['id' => $id]);
|
|
if ($user['phone_number'] !== $testPhone) {
|
|
throw new Exception("Select fallido");
|
|
}
|
|
|
|
// Update
|
|
$updated = $db->update('users', ['name' => 'Updated User'], ['id' => $id]);
|
|
if (!$updated) throw new Exception("Update fallido");
|
|
|
|
// Delete
|
|
$deleted = $db->execute("DELETE FROM users WHERE id = :id", ['id' => $id]);
|
|
if (!$deleted) throw new Exception("Delete fallido");
|
|
|
|
return "CRUD completo funciona correctamente";
|
|
});
|
|
|
|
$this->test("Integridad referencial", function() {
|
|
$db = Database::getInstance();
|
|
|
|
// Verificar que las foreign keys están definidas
|
|
$foreignKeys = $db->fetchAll("
|
|
SELECT TABLE_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME
|
|
FROM information_schema.KEY_COLUMN_USAGE
|
|
WHERE TABLE_SCHEMA = DATABASE()
|
|
AND REFERENCED_TABLE_NAME IS NOT NULL
|
|
");
|
|
|
|
if (empty($foreignKeys)) {
|
|
return "⚠️ Sin foreign keys definidas (recomendado para integridad)";
|
|
}
|
|
|
|
return "Integridad referencial configurada: " . count($foreignKeys) . " FK";
|
|
});
|
|
}
|
|
|
|
private function test($name, $callable) {
|
|
$this->totalTests++;
|
|
|
|
try {
|
|
$result = $callable();
|
|
$this->passedTests++;
|
|
$this->showTestResult($name, true, $result);
|
|
} catch (Exception $e) {
|
|
$this->failedTests++;
|
|
$this->showTestResult($name, false, $e->getMessage());
|
|
}
|
|
}
|
|
|
|
private function makeAPICall($endpoint, $method = 'GET', $data = null) {
|
|
// Simular sesión de administrador para tests
|
|
$_SESSION['admin_logged_in'] = true;
|
|
$_SESSION['last_activity'] = time();
|
|
|
|
$url = "api/$endpoint";
|
|
|
|
if ($method === 'GET' && $data) {
|
|
$url .= '?' . http_build_query($data);
|
|
}
|
|
|
|
$context = stream_context_create([
|
|
'http' => [
|
|
'method' => $method,
|
|
'header' => 'Content-Type: application/json',
|
|
'content' => $data ? json_encode($data) : null
|
|
]
|
|
]);
|
|
|
|
// Para tests, incluir el archivo directamente
|
|
ob_start();
|
|
|
|
if ($method === 'POST' && $data) {
|
|
// Simular POST data
|
|
$_POST = $data;
|
|
file_put_contents('php://input', json_encode($data));
|
|
}
|
|
|
|
try {
|
|
include $url;
|
|
$output = ob_get_clean();
|
|
return json_decode($output, true);
|
|
} catch (Exception $e) {
|
|
ob_get_clean();
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
private function showTestSection($title) {
|
|
echo "<div class='card mb-4'>";
|
|
echo "<div class='card-header'><h5>$title</h5></div>";
|
|
echo "<div class='card-body'>";
|
|
}
|
|
|
|
private function showTestResult($name, $passed, $message) {
|
|
$class = $passed ? 'success' : 'danger';
|
|
$icon = $passed ? 'check-circle' : 'times-circle';
|
|
|
|
echo "<div class='alert alert-$class'>";
|
|
echo "<i class='fas fa-$icon'></i> ";
|
|
echo "<strong>$name:</strong> $message";
|
|
echo "</div>";
|
|
|
|
if (!$passed) {
|
|
echo "</div></div>"; // Cerrar card si hay error
|
|
}
|
|
}
|
|
|
|
private function showSummary() {
|
|
echo "</div></div>"; // Cerrar última card
|
|
|
|
$successRate = $this->totalTests > 0 ? ($this->passedTests / $this->totalTests) * 100 : 0;
|
|
$duration = round((microtime(true) - $this->startTime) * 1000, 2);
|
|
|
|
$alertClass = $successRate === 100.0 ? 'success' : ($successRate >= 80 ? 'warning' : 'danger');
|
|
|
|
echo "<div class='alert alert-$alertClass'>";
|
|
echo "<h4><i class='fas fa-chart-pie'></i> Resumen de Tests</h4>";
|
|
echo "<div class='row'>";
|
|
echo "<div class='col-md-3'><strong>Total:</strong> {$this->totalTests}</div>";
|
|
echo "<div class='col-md-3'><strong>Pasaron:</strong> {$this->passedTests}</div>";
|
|
echo "<div class='col-md-3'><strong>Fallaron:</strong> {$this->failedTests}</div>";
|
|
echo "<div class='col-md-3'><strong>Éxito:</strong> " . number_format($successRate, 1) . "%</div>";
|
|
echo "</div>";
|
|
echo "<p class='mt-3'><strong>Tiempo de ejecución:</strong> {$duration}ms</p>";
|
|
|
|
if ($successRate === 100.0) {
|
|
echo "<p class='mb-0'><strong>🎉 ¡Todos los tests pasaron! El sistema está funcionando correctamente.</strong></p>";
|
|
} else if ($successRate >= 80) {
|
|
echo "<p class='mb-0'><strong>⚠️ La mayoría de tests pasaron, pero hay algunas fallas que requieren atención.</strong></p>";
|
|
} else {
|
|
echo "<p class='mb-0'><strong>🚨 Múltiples fallas detectadas. El sistema requiere correcciones urgentes.</strong></p>";
|
|
}
|
|
echo "</div>";
|
|
}
|
|
|
|
private function getHtmlHeader() {
|
|
return '
|
|
<!DOCTYPE html>
|
|
<html lang="es">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Test Suite - WhatsApp Bot</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
|
</head>
|
|
<body>
|
|
<div class="container mt-4">
|
|
';
|
|
}
|
|
|
|
private function getHtmlFooter() {
|
|
return '
|
|
<div class="text-center mt-4">
|
|
<a href="index.php" class="btn btn-primary"><i class="fas fa-home"></i> Ir al Panel</a>
|
|
<a href="test.php" class="btn btn-secondary"><i class="fas fa-cog"></i> Test Básico</a>
|
|
<a href="whatsapp_tester.php" class="btn btn-info"><i class="fas fa-phone"></i> Test WhatsApp</a>
|
|
</div>
|
|
<hr>
|
|
<p class="text-center text-muted"><small>Suite de tests ejecutada el: ' . date('Y-m-d H:i:s') . '</small></p>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
';
|
|
}
|
|
}
|
|
|
|
// Ejecutar todos los tests
|
|
$runner = new TestRunner();
|
|
$runner->runAllTests();
|
|
?>
|