1 <!DOCTYPE html>2 <html lang="es">3 <head>4 <meta charset="UTF-8">5 <title>Plataforma de Testing Automatizado de APIs</title>6 <!-- Enlaces a Bootstrap CSS y Bootstrap Icons -->7 <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">8 <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css" rel="stylesheet">9 <!-- Estilos personalizados -->10 <style>11 body {12 background-color: var(--bs-body-bg);13 color: var(--bs-body-color);14 transition: background-color 0.3s, color 0.3s;15 }16 .step {17 display: flex;18 align-items: flex-start;19 padding: 8px 12px;20 margin-bottom: 8px;21 position: relative;22 }23 .step:hover {24 background-color: #f8f9fa;25 }26 .step:hover .step-controls {27 display: flex;28 }29 .step-type {30 font-weight: bold;31 color: #0d6efd;32 margin-right: 10px;33 min-width: 60px;34 }35 .step-content {36 flex: 1;37 display: flex;38 flex-direction: column;39 }40 .step-content .main-content {41 display: flex;42 align-items: center;43 flex-wrap: wrap;44 }45 .step-content input,46 .step-content select,47 .step-content textarea {48 margin-right: 8px;49 }50 .step-controls {51 display: none;52 align-items: center;53 margin-left: 5px;54 }55 .step-controls button {56 background: none;57 border: none;58 margin-left: 5px;59 color: #6c757d;60 }61 .step-controls button:hover {62 color: #000;63 }64 .add-step {65 text-align: center;66 margin: 20px 0;67 }68 .theme-toggle {69 cursor: pointer;70 }71 .execute-btn {72 background: none;73 border: none;74 color: #198754;75 font-size: 1.2em;76 }77 .execute-btn:hover {78 color: #145c32;79 }80 .result-panel {81 margin-top: 10px;82 }83 .deferred-label {84 margin-left: 10px;85 font-size: 0.9em;86 color: #6c757d;87 }88 /* Estilo para el borde izquierdo de los detalles */89 .step-content .details {90 border-left: 2px solid #0d6efd;91 padding-left: 10px;92 margin-left: 10px;93 }94 .result-toggle {95 background: none;96 border: none;97 color: #0d6efd;98 padding: 0;99 font-size: 0.9em;100 }101 /* Estilos para los botones de Guardar y Cargar */102 .flow-controls {103 text-align: center;104 margin-bottom: 20px;105 }106 .flow-controls .btn {107 margin-right: 10px;108 }109 /* Ocultar input file */110 #fileInput {111 display: none;112 }113 </style>114 </head>115 <body>116 <!-- Barra de navegación -->117 <nav class="navbar navbar-expand-lg navbar-dark bg-primary">118 <div class="container-fluid">119 <a class="navbar-brand" href="#">120 <!-- Logo -->121 <img src="https://via.placeholder.com/150x40?text=Logo" alt="Logo">122 </a>123 <span class="navbar-text text-white">124 Plataforma de Testing Automatizado de APIs125 </span>126 <button class="btn btn-secondary ms-auto theme-toggle" id="themeToggle">127 <i class="bi bi-moon-fill"></i>128 </button>129 </div>130 </nav>131 132 <!-- Contenido principal -->133 <div id="app" class="container my-4">134 <!-- Botones para Guardar y Cargar Flujos -->135 <div class="flow-controls">136 <button class="btn btn-outline-secondary" @click="saveFlow">137 <i class="bi bi-save"></i> Guardar Flujo138 </button>139 <button class="btn btn-outline-secondary" @click="loadFlow">140 <i class="bi bi-folder2-open"></i> Cargar Flujo141 </button>142 <!-- Input file oculto para cargar archivos -->143 <input type="file" id="fileInput" @change="handleFileUpload" accept=".json">144 </div>145 146 <!-- Lista de pasos -->147 <div v-for="(step, index) in steps" :key="index" class="step">148 <span class="step-type">{{ getStepTypeLabel(step.type, step.data.method) }}</span>149 150 <div class="step-content">151 <div class="main-content">152 <!-- Contenido según el tipo de paso -->153 <component :is="getStepComponent(step.type)" v-model="step.data" :step="step"></component>154 <!-- Checkbox para "Deferred" -->155 <div v-if="step.type === 'request' || step.type === 'assertion'">156 <input type="checkbox" v-model="step.deferred" :id="'deferred-' + index">157 <label :for="'deferred-' + index" class="deferred-label">Deferred</label>158 </div>159 </div>160 <!-- Resultados dentro del bloque de solicitud -->161 <div v-if="step.type === 'request' && step.result" class="result-panel">162 <button class="result-toggle" @click="step.showResult = !step.showResult">163 <i class="bi" :class="step.showResult ? 'bi-eye-slash' : 'bi-eye'"></i>164 {{ step.showResult ? 'Ocultar' : 'Mostrar' }} Resultado165 </button>166 <div v-if="step.showResult">167 <div class="alert mt-2" :class="{'alert-success': step.result.success, 'alert-danger': !step.result.success}">168 <strong>Resultado:</strong> {{ step.result.message }}169 </div>170 <div v-if="step.result.details">171 <p><strong>Status Code:</strong> {{ step.result.details.status }}</p>172 <p><strong>Headers:</strong></p>173 <pre>{{ step.result.details.headers }}</pre>174 <p><strong>Body:</strong></p>175 <pre style="max-height: 200px; overflow-y: auto;">{{ step.result.details.body }}</pre>176 </div>177 </div>178 </div>179 </div>180 <div class="step-controls">181 <button @click="moveUp(index)" :disabled="index === 0"><i class="bi bi-arrow-up"></i></button>182 <button @click="moveDown(index)" :disabled="index === steps.length - 1"><i class="bi bi-arrow-down"></i></button>183 <button v-if="step.type === 'request'" @click="executeRequestStep(index)" class="execute-btn"><i class="bi bi-play-circle"></i></button>184 <button @click="removeStep(index)"><i class="bi bi-trash"></i></button>185 </div>186 </div>187 188 <!-- Botones para añadir nuevos pasos -->189 <div class="add-step">190 <button class="btn btn-outline-primary me-2" @click="addStep('variable')">191 <i class="bi bi-sliders"></i> Variable192 </button>193 <button class="btn btn-outline-primary me-2" @click="addStep('request')">194 <i class="bi bi-arrow-up-right-circle"></i> Solicitud HTTP195 </button>196 <button class="btn btn-outline-primary me-2" @click="addStep('assertion')">197 <i class="bi bi-check-circle"></i> Aserción198 </button>199 <button class="btn btn-outline-primary me-2" @click="addStep('wait')">200 <i class="bi bi-hourglass-split"></i> Wait201 </button>202 <!-- Agrega más botones si es necesario -->203 </div>204 205 <!-- Botón de Ejecución -->206 <div class="text-center my-4">207 <button class="btn btn-lg btn-success" @click="executeTest" :disabled="!isExecutable">208 <i class="bi bi-play-fill"></i> Ejecutar Prueba Completa209 </button>210 </div>211 </div>212 213 <!-- Enlaces a Vue.js, Axios y Bootstrap JS -->214 <script src="https://cdn.jsdelivr.net/npm/vue@3.3.4/dist/vue.global.prod.js"></script>215 <script src="https://cdn.jsdelivr.net/npm/axios@1.4.0/dist/axios.min.js"></script>216 <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>217 218 <!-- Componentes para los pasos -->219 <script>220 const VariableStep = {221 template: `222 <div class="d-flex align-items-center">223 <input type="text" class="form-control me-2" v-model="data.name" placeholder="Nombre" style="max-width: 150px;">224 <span>=</span>225 <input type="text" class="form-control ms-2" v-model="data.value" placeholder="Valor" style="max-width: 150px;">226 </div>227 `,228 props: ['modelValue', 'step'],229 computed: {230 data: {231 get() { return this.modelValue; },232 set(value) { this.$emit('update:modelValue', value); }233 }234 }235 };236 237 const RequestStep = {238 template: `239 <div>240 <div class="d-flex align-items-center flex-wrap">241 <select class="form-select me-2" v-model="data.method" style="max-width: 100px;">242 <option>GET</option>243 <option>POST</option>244 <option>PUT</option>245 <option>DELETE</option>246 </select>247 <input type="text" class="form-control me-2" v-model="data.url" placeholder="URL" style="flex: 1;">248 <!-- Botón para mostrar detalles -->249 <button class="btn btn-sm btn-outline-secondary me-2" @click="toggleDetails">250 <i class="bi" :class="step.expanded ? 'bi-chevron-up' : 'bi-chevron-down'"></i>251 </button>252 </div>253 <!-- Detalles opcionales -->254 <div v-if="step.expanded" class="details mt-2">255 <!-- Headers -->256 <button class="btn btn-sm btn-outline-secondary me-2" @click="toggleHeaders">257 <i class="bi" :class="showHeaders ? 'bi-dash' : 'bi-plus'"></i> Headers258 </button>259 <!-- Body -->260 <button class="btn btn-sm btn-outline-secondary me-2" @click="toggleBody" v-if="data.method !== 'GET'">261 <i class="bi" :class="showBody ? 'bi-dash' : 'bi-plus'"></i> Body262 </button>263 <!-- Repeat Config -->264 <button class="btn btn-sm btn-outline-secondary" @click="toggleRepeat">265 <i class="bi" :class="showRepeat ? 'bi-dash' : 'bi-plus'"></i> Repeat266 </button>267 <!-- Headers -->268 <div v-if="showHeaders" class="mt-2">269 <div v-for="(header, index) in data.headers" :key="index" class="d-flex align-items-center mb-2">270 <input type="text" class="form-control me-2" v-model="header.key" placeholder="Header Key" style="max-width: 150px;">271 <input type="text" class="form-control me-2" v-model="header.value" placeholder="Header Value" style="max-width: 150px;">272 <button class="btn btn-sm btn-outline-danger" @click="removeHeader(index)">273 <i class="bi bi-trash"></i>274 </button>275 </div>276 <button class="btn btn-outline-primary btn-sm" @click="addHeader">277 <i class="bi bi-plus-lg"></i> Agregar Header278 </button>279 </div>280 <!-- Body -->281 <div v-if="showBody" class="mt-2">282 <label class="form-label">Body</label>283 <textarea class="form-control" rows="3" v-model="data.body" placeholder="Cuerpo de la solicitud"></textarea>284 </div>285 <!-- Repeat -->286 <div v-if="showRepeat" class="mt-2">287 <div class="d-flex align-items-center">288 <label class="form-label me-2">Intervalo (s):</label>289 <input type="number" class="form-control me-2" v-model.number="data.repeatInterval" style="max-width: 100px;">290 <label class="form-label me-2">Máximo de Intentos:</label>291 <input type="number" class="form-control me-2" v-model.number="data.repeatMaxAttempts" style="max-width: 100px;">292 </div>293 <div class="mt-2">294 <label class="form-label">Condición para Repetir:</label>295 <input type="text" class="form-control" v-model="data.repeatCondition" placeholder="Ejemplo: response.status == 404">296 </div>297 <div class="mt-2">298 <label class="form-label">Condición de Éxito:</label>299 <input type="text" class="form-control" v-model="data.successCondition" placeholder="Ejemplo: response.status == 200 || response.status == 201">300 </div>301 </div>302 </div>303 </div>304 `,305 props: ['modelValue', 'step'],306 data() {307 return {308 showHeaders: false,309 showBody: false,310 showRepeat: false,311 }312 },313 computed: {314 data: {315 get() { return this.modelValue; },316 set(value) { this.$emit('update:modelValue', value); }317 }318 },319 methods: {320 toggleDetails() {321 this.step.expanded = !this.step.expanded;322 },323 toggleHeaders() {324 this.showHeaders = !this.showHeaders;325 },326 toggleBody() {327 this.showBody = !this.showBody;328 },329 toggleRepeat() {330 this.showRepeat = !this.showRepeat;331 },332 addHeader() {333 this.data.headers.push({ key: '', value: '' });334 },335 removeHeader(index) {336 this.data.headers.splice(index, 1);337 }338 }339 };340 341 const AssertionStep = {342 template: `343 <div class="d-flex align-items-center">344 <input type="text" class="form-control" v-model="data.condition" placeholder="Condición">345 </div>346 `,347 props: ['modelValue', 'step'],348 computed: {349 data: {350 get() { return this.modelValue; },351 set(value) { this.$emit('update:modelValue', value); }352 }353 }354 };355 356 const WaitStep = {357 template: `358 <div class="d-flex align-items-center">359 <label class="form-label me-2">Esperar</label>360 <input type="number" class="form-control me-2" v-model.number="data.seconds" placeholder="Segundos" style="max-width: 100px;">361 </div>362 `,363 props: ['modelValue', 'step'],364 computed: {365 data: {366 get() { return this.modelValue; },367 set(value) { this.$emit('update:modelValue', value); }368 }369 }370 };371 372 // Instancia de Vue373 const { createApp } = Vue;374 375 createApp({376 components: {377 'variable-step': VariableStep,378 'request-step': RequestStep,379 'assertion-step': AssertionStep,380 'wait-step': WaitStep,381 // Agrega más componentes si es necesario382 },383 data() {384 return {385 steps: [],386 darkTheme: false,387 }388 },389 computed: {390 isExecutable() {391 // Verifica si hay al menos un paso para ejecutar392 return this.steps.length > 0;393 }394 },395 methods: {396 getStepComponent(type) {397 switch (type) {398 case 'variable':399 return 'variable-step';400 case 'request':401 return 'request-step';402 case 'assertion':403 return 'assertion-step';404 case 'wait':405 return 'wait-step';406 // Otros casos...407 }408 },409 getStepTypeLabel(type, method) {410 switch (type) {411 case 'variable':412 return 'VAR';413 case 'request':414 return method;415 case 'assertion':416 return 'ASSERT';417 case 'wait':418 return 'WAIT';419 // Otros casos...420 }421 },422 moveUp(index) {423 if (index > 0) {424 [this.steps[index - 1], this.steps[index]] = [this.steps[index], this.steps[index - 1]];425 }426 },427 moveDown(index) {428 if (index < this.steps.length - 1) {429 [this.steps[index], this.steps[index + 1]] = [this.steps[index + 1], this.steps[index]];430 }431 },432 removeStep(index) {433 this.steps.splice(index, 1);434 },435 addStep(type) {436 let newStep = {437 type: type,438 data: {},439 deferred: false,440 expanded: false,441 result: null,442 showResult: false,443 };444 switch (type) {445 case 'variable':446 newStep.data = { name: '', value: '' };447 break;448 case 'request':449 newStep.data = {450 method: 'GET',451 url: '',452 headers: [],453 body: '',454 repeatInterval: 0,455 repeatMaxAttempts: 1,456 repeatCondition: '',457 successCondition: '',458 };459 break;460 case 'assertion':461 newStep.data = { condition: '' };462 break;463 case 'wait':464 newStep.data = { seconds: 1 };465 break;466 // Otros casos...467 }468 this.steps.push(newStep);469 },470 async executeTest() {471 // Reiniciar resultados472 this.steps.forEach(step => {473 if (step.type === 'request') {474 step.result = null;475 step.showResult = false;476 }477 });478 // Filtrar los pasos que no son "Deferred"479 const stepsToExecute = this.steps.filter(step => !step.deferred);480 // Ejecutar los pasos secuencialmente481 for (const step of stepsToExecute) {482 const res = await this.executeStep(step);483 if (!res.success) {484 return;485 }486 }487 // Ejecutar los pasos "Deferred"488 const deferredSteps = this.steps.filter(step => step.deferred);489 for (const step of deferredSteps) {490 await this.executeStep(step);491 }492 },493 async executeStep(step) {494 switch (step.type) {495 case 'variable':496 // Aquí podrías almacenar la variable en un contexto si es necesario497 return { success: true };498 case 'request':499 return await this.executeRequest(step);500 case 'assertion':501 // Implementar lógica de aserciones si es necesario502 return { success: true };503 case 'wait':504 await new Promise(resolve => setTimeout(resolve, step.data.seconds * 1000));505 return { success: true };506 // Otros casos...507 }508 },509 async executeRequest(step) {510 const data = step.data;511 try {512 let attempts = 0;513 let maxAttempts = data.repeatMaxAttempts || 1;514 let interval = data.repeatInterval || 0;515 let conditionMet = false;516 let response;517 do {518 attempts++;519 // Configurar la solicitud520 const config = {521 method: data.method.toLowerCase(),522 url: data.url,523 headers: {},524 };525 data.headers.forEach(header => {526 config.headers[header.key] = header.value;527 });528 if (data.method !== 'GET' && data.body) {529 config.data = data.body;530 }531 // Realizar la solicitud532 response = await axios(config);533 // Evaluar condiciones534 if (data.successCondition) {535 conditionMet = eval(data.successCondition);536 } else {537 conditionMet = true;538 }539 if (!conditionMet && attempts < maxAttempts) {540 await new Promise(resolve => setTimeout(resolve, interval * 1000));541 }542 } while (!conditionMet && attempts < maxAttempts);543 // Mostrar resultado dentro del paso544 step.result = {545 success: true,546 message: 'Solicitud ejecutada exitosamente.',547 details: {548 status: response.status,549 headers: JSON.stringify(response.headers, null, 2),550 body: JSON.stringify(response.data, null, 2)551 }552 };553 return { success: true };554 } catch (error) {555 step.result = {556 success: false,557 message: 'Error al ejecutar la solicitud.',558 details: {559 status: error.response ? error.response.status : 'Sin respuesta',560 headers: error.response ? JSON.stringify(error.response.headers, null, 2) : 'No disponible',561 body: error.response ? JSON.stringify(error.response.data, null, 2) : error.message562 }563 };564 return { success: false };565 }566 },567 executeRequestStep(index) {568 const step = this.steps[index];569 // Reiniciar resultado previo570 step.result = null;571 step.showResult = false;572 this.executeRequest(step);573 },574 saveFlow() {575 const dataStr = JSON.stringify(this.steps, null, 2);576 const blob = new Blob([dataStr], { type: "application/json" });577 const url = URL.createObjectURL(blob);578 const a = document.createElement('a');579 a.href = url;580 a.download = 'flujo_de_prueba.json';581 a.click();582 URL.revokeObjectURL(url);583 },584 loadFlow() {585 document.getElementById('fileInput').click();586 },587 handleFileUpload(event) {588 const file = event.target.files[0];589 if (file) {590 const reader = new FileReader();591 reader.onload = (e) => {592 try {593 const data = JSON.parse(e.target.result);594 // Validar que el archivo contiene un array de pasos595 if (Array.isArray(data)) {596 this.steps = data;597 } else {598 alert('El archivo no es válido.');599 }600 } catch (error) {601 alert('Error al cargar el archivo: ' + error.message);602 }603 };604 reader.readAsText(file);605 // Limpiar el valor del input para permitir cargar el mismo archivo nuevamente si es necesario606 event.target.value = '';607 }608 },609 },610 mounted() {611 // Inicializar el tema según las preferencias del usuario612 const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;613 if (prefersDark) {614 this.toggleTheme();615 }616 }617 }).mount('#app');618 619 // Manejo del botón de cambio de tema fuera de Vue620 document.getElementById('themeToggle').addEventListener('click', () => {621 document.body.classList.toggle('bg-dark');622 document.body.classList.toggle('text-white');623 const icon = document.getElementById('themeToggle').querySelector('i');624 icon.classList.toggle('bi-moon-fill');625 icon.classList.toggle('bi-sun-fill');626 });627 </script>628 </body>629 </html>630
Enlace
El enlace para compartir es: