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 </style>102 </head>103 <body>104 <!-- Barra de navegación -->105 <nav class="navbar navbar-expand-lg navbar-dark bg-primary">106 <div class="container-fluid">107 <a class="navbar-brand" href="#">108 <!-- Logo -->109 <img src="https://via.placeholder.com/150x40?text=Logo" alt="Logo">110 </a>111 <span class="navbar-text text-white">112 Plataforma de Testing Automatizado de APIs113 </span>114 <button class="btn btn-secondary ms-auto theme-toggle" id="themeToggle">115 <i class="bi bi-moon-fill"></i>116 </button>117 </div>118 </nav>119 120 <!-- Contenido principal -->121 <div id="app" class="container my-4">122 <!-- Lista de pasos -->123 <div v-for="(step, index) in steps" :key="index" class="step">124 <span class="step-type">{{ getStepTypeLabel(step.type, step.data.method) }}</span>125 126 <div class="step-content">127 <div class="main-content">128 <!-- Contenido según el tipo de paso -->129 <component :is="getStepComponent(step.type)" v-model="step.data" :step="step"></component>130 <!-- Checkbox para "Deferred" -->131 <div v-if="step.type === 'request' || step.type === 'assertion'">132 <input type="checkbox" v-model="step.deferred" :id="'deferred-' + index">133 <label :for="'deferred-' + index" class="deferred-label">Deferred</label>134 </div>135 </div>136 <!-- Resultados dentro del bloque de solicitud -->137 <div v-if="step.type === 'request' && step.result" class="result-panel">138 <button class="result-toggle" @click="step.showResult = !step.showResult">139 <i class="bi" :class="step.showResult ? 'bi-eye-slash' : 'bi-eye'"></i>140 {{ step.showResult ? 'Ocultar' : 'Mostrar' }} Resultado141 </button>142 <div v-if="step.showResult">143 <div class="alert mt-2" :class="{'alert-success': step.result.success, 'alert-danger': !step.result.success}">144 <strong>Resultado:</strong> {{ step.result.message }}145 </div>146 <div v-if="step.result.details">147 <p><strong>Status Code:</strong> {{ step.result.details.status }}</p>148 <p><strong>Headers:</strong></p>149 <pre>{{ step.result.details.headers }}</pre>150 <p><strong>Body:</strong></p>151 <pre style="max-height: 200px; overflow-y: auto;">{{ step.result.details.body }}</pre>152 </div>153 </div>154 </div>155 </div>156 <div class="step-controls">157 <button @click="moveUp(index)" :disabled="index === 0"><i class="bi bi-arrow-up"></i></button>158 <button @click="moveDown(index)" :disabled="index === steps.length - 1"><i class="bi bi-arrow-down"></i></button>159 <button v-if="step.type === 'request'" @click="executeRequestStep(index)" class="execute-btn"><i class="bi bi-play-circle"></i></button>160 <button @click="removeStep(index)"><i class="bi bi-trash"></i></button>161 </div>162 </div>163 164 <!-- Botones para añadir nuevos pasos -->165 <div class="add-step">166 <button class="btn btn-outline-primary me-2" @click="addStep('variable')">167 <i class="bi bi-sliders"></i> Variable168 </button>169 <button class="btn btn-outline-primary me-2" @click="addStep('request')">170 <i class="bi bi-arrow-up-right-circle"></i> Solicitud HTTP171 </button>172 <button class="btn btn-outline-primary me-2" @click="addStep('assertion')">173 <i class="bi bi-check-circle"></i> Aserción174 </button>175 <button class="btn btn-outline-primary me-2" @click="addStep('wait')">176 <i class="bi bi-hourglass-split"></i> Wait177 </button>178 <!-- Agrega más botones si es necesario -->179 </div>180 181 <!-- Botón de Ejecución -->182 <div class="text-center my-4">183 <button class="btn btn-lg btn-success" @click="executeTest" :disabled="!isExecutable">184 <i class="bi bi-play-fill"></i> Ejecutar Prueba Completa185 </button>186 </div>187 </div>188 189 <!-- Enlaces a Vue.js, Axios y Bootstrap JS -->190 <script src="https://cdn.jsdelivr.net/npm/vue@3.3.4/dist/vue.global.prod.js"></script>191 <script src="https://cdn.jsdelivr.net/npm/axios@1.4.0/dist/axios.min.js"></script>192 <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>193 194 <!-- Componentes para los pasos -->195 <script>196 const VariableStep = {197 template: `198 <div class="d-flex align-items-center">199 <input type="text" class="form-control me-2" v-model="data.name" placeholder="Nombre" style="max-width: 150px;">200 <span>=</span>201 <input type="text" class="form-control ms-2" v-model="data.value" placeholder="Valor" style="max-width: 150px;">202 </div>203 `,204 props: ['modelValue', 'step'],205 computed: {206 data: {207 get() { return this.modelValue; },208 set(value) { this.$emit('update:modelValue', value); }209 }210 }211 };212 213 const RequestStep = {214 template: `215 <div>216 <div class="d-flex align-items-center flex-wrap">217 <select class="form-select me-2" v-model="data.method" style="max-width: 100px;">218 <option>GET</option>219 <option>POST</option>220 <option>PUT</option>221 <option>DELETE</option>222 </select>223 <input type="text" class="form-control me-2" v-model="data.url" placeholder="URL" style="flex: 1;">224 <!-- Botón para mostrar detalles -->225 <button class="btn btn-sm btn-outline-secondary me-2" @click="toggleDetails">226 <i class="bi" :class="step.expanded ? 'bi-chevron-up' : 'bi-chevron-down'"></i>227 </button>228 </div>229 <!-- Detalles opcionales -->230 <div v-if="step.expanded" class="details mt-2">231 <!-- Headers -->232 <button class="btn btn-sm btn-outline-secondary me-2" @click="toggleHeaders">233 <i class="bi" :class="showHeaders ? 'bi-dash' : 'bi-plus'"></i> Headers234 </button>235 <!-- Body -->236 <button class="btn btn-sm btn-outline-secondary me-2" @click="toggleBody" v-if="data.method !== 'GET'">237 <i class="bi" :class="showBody ? 'bi-dash' : 'bi-plus'"></i> Body238 </button>239 <!-- Repeat Config -->240 <button class="btn btn-sm btn-outline-secondary" @click="toggleRepeat">241 <i class="bi" :class="showRepeat ? 'bi-dash' : 'bi-plus'"></i> Repeat242 </button>243 <!-- Headers -->244 <div v-if="showHeaders" class="mt-2">245 <div v-for="(header, index) in data.headers" :key="index" class="d-flex align-items-center mb-2">246 <input type="text" class="form-control me-2" v-model="header.key" placeholder="Header Key" style="max-width: 150px;">247 <input type="text" class="form-control me-2" v-model="header.value" placeholder="Header Value" style="max-width: 150px;">248 <button class="btn btn-sm btn-outline-danger" @click="removeHeader(index)">249 <i class="bi bi-trash"></i>250 </button>251 </div>252 <button class="btn btn-outline-primary btn-sm" @click="addHeader">253 <i class="bi bi-plus-lg"></i> Agregar Header254 </button>255 </div>256 <!-- Body -->257 <div v-if="showBody" class="mt-2">258 <label class="form-label">Body</label>259 <textarea class="form-control" rows="3" v-model="data.body" placeholder="Cuerpo de la solicitud"></textarea>260 </div>261 <!-- Repeat -->262 <div v-if="showRepeat" class="mt-2">263 <div class="d-flex align-items-center">264 <label class="form-label me-2">Intervalo (s):</label>265 <input type="number" class="form-control me-2" v-model.number="data.repeatInterval" style="max-width: 100px;">266 <label class="form-label me-2">Máximo de Intentos:</label>267 <input type="number" class="form-control me-2" v-model.number="data.repeatMaxAttempts" style="max-width: 100px;">268 </div>269 <div class="mt-2">270 <label class="form-label">Condición para Repetir:</label>271 <input type="text" class="form-control" v-model="data.repeatCondition" placeholder="Ejemplo: response.status == 404">272 </div>273 <div class="mt-2">274 <label class="form-label">Condición de Éxito:</label>275 <input type="text" class="form-control" v-model="data.successCondition" placeholder="Ejemplo: response.status == 200 || response.status == 201">276 </div>277 </div>278 </div>279 </div>280 `,281 props: ['modelValue', 'step'],282 data() {283 return {284 showHeaders: false,285 showBody: false,286 showRepeat: false,287 }288 },289 computed: {290 data: {291 get() { return this.modelValue; },292 set(value) { this.$emit('update:modelValue', value); }293 }294 },295 methods: {296 toggleDetails() {297 this.step.expanded = !this.step.expanded;298 },299 toggleHeaders() {300 this.showHeaders = !this.showHeaders;301 },302 toggleBody() {303 this.showBody = !this.showBody;304 },305 toggleRepeat() {306 this.showRepeat = !this.showRepeat;307 },308 addHeader() {309 this.data.headers.push({ key: '', value: '' });310 },311 removeHeader(index) {312 this.data.headers.splice(index, 1);313 }314 }315 };316 317 const AssertionStep = {318 template: `319 <div class="d-flex align-items-center">320 <input type="text" class="form-control" v-model="data.condition" placeholder="Condición">321 </div>322 `,323 props: ['modelValue', 'step'],324 computed: {325 data: {326 get() { return this.modelValue; },327 set(value) { this.$emit('update:modelValue', value); }328 }329 }330 };331 332 const WaitStep = {333 template: `334 <div class="d-flex align-items-center">335 <label class="form-label me-2">Esperar</label>336 <input type="number" class="form-control me-2" v-model.number="data.seconds" placeholder="Segundos" style="max-width: 100px;">337 </div>338 `,339 props: ['modelValue', 'step'],340 computed: {341 data: {342 get() { return this.modelValue; },343 set(value) { this.$emit('update:modelValue', value); }344 }345 }346 };347 348 // Instancia de Vue349 const { createApp } = Vue;350 351 createApp({352 components: {353 'variable-step': VariableStep,354 'request-step': RequestStep,355 'assertion-step': AssertionStep,356 'wait-step': WaitStep,357 // Agrega más componentes si es necesario358 },359 data() {360 return {361 steps: [],362 darkTheme: false,363 }364 },365 computed: {366 isExecutable() {367 // Verifica si hay al menos un paso para ejecutar368 return this.steps.length > 0;369 }370 },371 methods: {372 getStepComponent(type) {373 switch (type) {374 case 'variable':375 return 'variable-step';376 case 'request':377 return 'request-step';378 case 'assertion':379 return 'assertion-step';380 case 'wait':381 return 'wait-step';382 // Otros casos...383 }384 },385 getStepTypeLabel(type, method) {386 switch (type) {387 case 'variable':388 return 'VAR';389 case 'request':390 return method;391 case 'assertion':392 return 'ASSERT';393 case 'wait':394 return 'WAIT';395 // Otros casos...396 }397 },398 moveUp(index) {399 if (index > 0) {400 [this.steps[index - 1], this.steps[index]] = [this.steps[index], this.steps[index - 1]];401 }402 },403 moveDown(index) {404 if (index < this.steps.length - 1) {405 [this.steps[index], this.steps[index + 1]] = [this.steps[index + 1], this.steps[index]];406 }407 },408 removeStep(index) {409 this.steps.splice(index, 1);410 },411 addStep(type) {412 let newStep = {413 type: type,414 data: {},415 deferred: false,416 expanded: false,417 result: null,418 showResult: false,419 };420 switch (type) {421 case 'variable':422 newStep.data = { name: '', value: '' };423 break;424 case 'request':425 newStep.data = {426 method: 'GET',427 url: '',428 headers: [],429 body: '',430 repeatInterval: 0,431 repeatMaxAttempts: 1,432 repeatCondition: '',433 successCondition: '',434 };435 break;436 case 'assertion':437 newStep.data = { condition: '' };438 break;439 case 'wait':440 newStep.data = { seconds: 1 };441 break;442 // Otros casos...443 }444 this.steps.push(newStep);445 },446 async executeTest() {447 // Reiniciar resultados448 this.steps.forEach(step => {449 if (step.type === 'request') {450 step.result = null;451 step.showResult = false;452 }453 });454 // Filtrar los pasos que no son "Deferred"455 const stepsToExecute = this.steps.filter(step => !step.deferred);456 // Ejecutar los pasos secuencialmente457 for (const step of stepsToExecute) {458 const res = await this.executeStep(step);459 if (!res.success) {460 return;461 }462 }463 // Ejecutar los pasos "Deferred"464 const deferredSteps = this.steps.filter(step => step.deferred);465 for (const step of deferredSteps) {466 await this.executeStep(step);467 }468 },469 async executeStep(step) {470 switch (step.type) {471 case 'variable':472 // Aquí podrías almacenar la variable en un contexto si es necesario473 return { success: true };474 case 'request':475 return await this.executeRequest(step);476 case 'assertion':477 // Implementar lógica de aserciones si es necesario478 return { success: true };479 case 'wait':480 await new Promise(resolve => setTimeout(resolve, step.data.seconds * 1000));481 return { success: true };482 // Otros casos...483 }484 },485 async executeRequest(step) {486 const data = step.data;487 try {488 let attempts = 0;489 let maxAttempts = data.repeatMaxAttempts || 1;490 let interval = data.repeatInterval || 0;491 let conditionMet = false;492 let response;493 do {494 attempts++;495 // Configurar la solicitud496 const config = {497 method: data.method.toLowerCase(),498 url: data.url,499 headers: {},500 };501 data.headers.forEach(header => {502 config.headers[header.key] = header.value;503 });504 if (data.method !== 'GET' && data.body) {505 config.data = data.body;506 }507 // Realizar la solicitud508 response = await axios(config);509 // Evaluar condiciones510 if (data.successCondition) {511 conditionMet = eval(data.successCondition);512 } else {513 conditionMet = true;514 }515 if (!conditionMet && attempts < maxAttempts) {516 await new Promise(resolve => setTimeout(resolve, interval * 1000));517 }518 } while (!conditionMet && attempts < maxAttempts);519 // Mostrar resultado dentro del paso520 step.result = {521 success: true,522 message: 'Solicitud ejecutada exitosamente.',523 details: {524 status: response.status,525 headers: JSON.stringify(response.headers, null, 2),526 body: JSON.stringify(response.data, null, 2)527 }528 };529 return { success: true };530 } catch (error) {531 step.result = {532 success: false,533 message: 'Error al ejecutar la solicitud.',534 details: {535 status: error.response ? error.response.status : 'Sin respuesta',536 headers: error.response ? JSON.stringify(error.response.headers, null, 2) : 'No disponible',537 body: error.response ? JSON.stringify(error.response.data, null, 2) : error.message538 }539 };540 return { success: false };541 }542 },543 executeRequestStep(index) {544 const step = this.steps[index];545 // Reiniciar resultado previo546 step.result = null;547 step.showResult = false;548 this.executeRequest(step);549 }550 },551 mounted() {552 // Inicializar el tema según las preferencias del usuario553 const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;554 if (prefersDark) {555 this.toggleTheme();556 }557 }558 }).mount('#app');559 560 // Manejo del botón de cambio de tema fuera de Vue561 document.getElementById('themeToggle').addEventListener('click', () => {562 document.body.classList.toggle('bg-dark');563 document.body.classList.toggle('text-white');564 const icon = document.getElementById('themeToggle').querySelector('i');565 icon.classList.toggle('bi-moon-fill');566 icon.classList.toggle('bi-sun-fill');567 });568 </script>569 </body>570 </html>571
Enlace
El enlace para compartir es: