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 position: relative;35 }36 .step-content {37 flex: 1;38 display: flex;39 flex-direction: column;40 }41 .step-content .main-content {42 display: flex;43 align-items: center;44 flex-wrap: wrap;45 }46 .step-content input,47 .step-content select,48 .step-content textarea {49 margin-right: 8px;50 }51 .step-controls {52 display: none;53 align-items: center;54 margin-left: 5px;55 }56 .step-controls button {57 background: none;58 border: none;59 margin-left: 5px;60 color: #6c757d;61 }62 .step-controls button:hover {63 color: #000;64 }65 .add-step {66 text-align: center;67 margin: 20px 0;68 }69 .theme-toggle {70 cursor: pointer;71 }72 .execute-btn {73 background: none;74 border: none;75 color: #198754;76 font-size: 1.2em;77 }78 .execute-btn:hover {79 color: #145c32;80 }81 .result-panel {82 margin-top: 10px;83 }84 .deferred-label {85 margin-left: 10px;86 font-size: 0.9em;87 color: #6c757d;88 }89 .vertical-line {90 position: absolute;91 left: 70px;92 top: 32px; /* Ajuste para que no corte el texto */93 bottom: 0;94 width: 2px;95 background-color: #0d6efd;96 }97 .step .vertical-line {98 display: none;99 }100 .step.expandable.expanded .vertical-line {101 display: block;102 }103 .step-content .details {104 margin-left: 10px;105 border-left: 2px solid #0d6efd;106 padding-left: 10px;107 }108 .result-toggle {109 background: none;110 border: none;111 color: #0d6efd;112 padding: 0;113 font-size: 0.9em;114 }115 </style>116 </head>117 <body>118 <!-- Barra de navegación -->119 <nav class="navbar navbar-expand-lg navbar-dark bg-primary">120 <div class="container-fluid">121 <a class="navbar-brand" href="#">122 <!-- Logo -->123 <img src="https://via.placeholder.com/150x40?text=Logo" alt="Logo">124 </a>125 <span class="navbar-text text-white">126 Plataforma de Testing Automatizado de APIs127 </span>128 <button class="btn btn-secondary ms-auto theme-toggle" id="themeToggle">129 <i class="bi bi-moon-fill"></i>130 </button>131 </div>132 </nav>133 134 <!-- Contenido principal -->135 <div id="app" class="container my-4">136 <!-- Lista de pasos -->137 <div v-for="(step, index) in steps" :key="index" class="step" :class="{'expandable': step.type === 'request', 'expanded': step.expanded}">138 <span class="step-type">{{ getStepTypeLabel(step.type, step.data.method) }}</span>139 <div class="vertical-line"></div>140 141 <div class="step-content">142 <div class="main-content">143 <!-- Contenido según el tipo de paso -->144 <component :is="getStepComponent(step.type)" v-model="step.data" :step="step"></component>145 <!-- Checkbox para "Deferred" -->146 <div v-if="step.type === 'request' || step.type === 'assertion'">147 <input type="checkbox" v-model="step.deferred" :id="'deferred-' + index">148 <label :for="'deferred-' + index" class="deferred-label">Deferred</label>149 </div>150 </div>151 <!-- Resultados dentro del bloque de solicitud -->152 <div v-if="step.type === 'request' && step.result" class="result-panel">153 <button class="result-toggle" @click="step.showResult = !step.showResult">154 <i class="bi" :class="step.showResult ? 'bi-eye-slash' : 'bi-eye'"></i>155 {{ step.showResult ? 'Ocultar' : 'Mostrar' }} Resultado156 </button>157 <div v-if="step.showResult">158 <div class="alert mt-2" :class="{'alert-success': step.result.success, 'alert-danger': !step.result.success}">159 <strong>Resultado:</strong> {{ step.result.message }}160 </div>161 <div v-if="step.result.details">162 <p><strong>Status Code:</strong> {{ step.result.details.status }}</p>163 <p><strong>Headers:</strong></p>164 <pre>{{ step.result.details.headers }}</pre>165 <p><strong>Body:</strong></p>166 <pre style="max-height: 200px; overflow-y: auto;">{{ step.result.details.body }}</pre>167 </div>168 </div>169 </div>170 </div>171 <div class="step-controls">172 <button @click="moveUp(index)" :disabled="index === 0"><i class="bi bi-arrow-up"></i></button>173 <button @click="moveDown(index)" :disabled="index === steps.length - 1"><i class="bi bi-arrow-down"></i></button>174 <button v-if="step.type === 'request'" @click="executeRequestStep(index)" class="execute-btn"><i class="bi bi-play-circle"></i></button>175 <button @click="removeStep(index)"><i class="bi bi-trash"></i></button>176 </div>177 </div>178 179 <!-- Botones para añadir nuevos pasos -->180 <div class="add-step">181 <button class="btn btn-outline-primary me-2" @click="addStep('variable')">182 <i class="bi bi-sliders"></i> Variable183 </button>184 <button class="btn btn-outline-primary me-2" @click="addStep('request')">185 <i class="bi bi-arrow-up-right-circle"></i> Solicitud HTTP186 </button>187 <button class="btn btn-outline-primary me-2" @click="addStep('assertion')">188 <i class="bi bi-check-circle"></i> Aserción189 </button>190 <button class="btn btn-outline-primary me-2" @click="addStep('wait')">191 <i class="bi bi-hourglass-split"></i> Wait192 </button>193 <!-- Agrega más botones si es necesario -->194 </div>195 196 <!-- Botón de Ejecución -->197 <div class="text-center my-4">198 <button class="btn btn-lg btn-success" @click="executeTest" :disabled="!isExecutable">199 <i class="bi bi-play-fill"></i> Ejecutar Prueba Completa200 </button>201 </div>202 </div>203 204 <!-- Enlaces a Vue.js, Axios y Bootstrap JS -->205 <script src="https://cdn.jsdelivr.net/npm/vue@3.3.4/dist/vue.global.prod.js"></script>206 <script src="https://cdn.jsdelivr.net/npm/axios@1.4.0/dist/axios.min.js"></script>207 <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>208 209 <!-- Componentes para los pasos -->210 <script>211 const VariableStep = {212 template: `213 <div class="d-flex align-items-center">214 <input type="text" class="form-control me-2" v-model="data.name" placeholder="Nombre" style="max-width: 150px;">215 <span>=</span>216 <input type="text" class="form-control ms-2" v-model="data.value" placeholder="Valor" style="max-width: 150px;">217 </div>218 `,219 props: ['modelValue', 'step'],220 computed: {221 data: {222 get() { return this.modelValue; },223 set(value) { this.$emit('update:modelValue', value); }224 }225 }226 };227 228 const RequestStep = {229 template: `230 <div>231 <div class="d-flex align-items-center flex-wrap">232 <select class="form-select me-2" v-model="data.method" style="max-width: 100px;">233 <option>GET</option>234 <option>POST</option>235 <option>PUT</option>236 <option>DELETE</option>237 </select>238 <input type="text" class="form-control me-2" v-model="data.url" placeholder="URL" style="flex: 1;">239 <!-- Botón para mostrar detalles -->240 <button class="btn btn-sm btn-outline-secondary me-2" @click="toggleDetails">241 <i class="bi" :class="step.expanded ? 'bi-chevron-up' : 'bi-chevron-down'"></i>242 </button>243 </div>244 <!-- Detalles opcionales -->245 <div v-if="step.expanded" class="details mt-2">246 <!-- Headers -->247 <button class="btn btn-sm btn-outline-secondary me-2" @click="toggleHeaders">248 <i class="bi" :class="showHeaders ? 'bi-dash' : 'bi-plus'"></i> Headers249 </button>250 <!-- Body -->251 <button class="btn btn-sm btn-outline-secondary me-2" @click="toggleBody" v-if="data.method !== 'GET'">252 <i class="bi" :class="showBody ? 'bi-dash' : 'bi-plus'"></i> Body253 </button>254 <!-- Repeat Config -->255 <button class="btn btn-sm btn-outline-secondary" @click="toggleRepeat">256 <i class="bi" :class="showRepeat ? 'bi-dash' : 'bi-plus'"></i> Repeat257 </button>258 <!-- Headers -->259 <div v-if="showHeaders" class="mt-2">260 <div v-for="(header, index) in data.headers" :key="index" class="d-flex align-items-center mb-2">261 <input type="text" class="form-control me-2" v-model="header.key" placeholder="Header Key" style="max-width: 150px;">262 <input type="text" class="form-control me-2" v-model="header.value" placeholder="Header Value" style="max-width: 150px;">263 <button class="btn btn-sm btn-outline-danger" @click="removeHeader(index)">264 <i class="bi bi-trash"></i>265 </button>266 </div>267 <button class="btn btn-outline-primary btn-sm" @click="addHeader">268 <i class="bi bi-plus-lg"></i> Agregar Header269 </button>270 </div>271 <!-- Body -->272 <div v-if="showBody" class="mt-2">273 <label class="form-label">Body</label>274 <textarea class="form-control" rows="3" v-model="data.body" placeholder="Cuerpo de la solicitud"></textarea>275 </div>276 <!-- Repeat -->277 <div v-if="showRepeat" class="mt-2">278 <div class="d-flex align-items-center">279 <label class="form-label me-2">Intervalo (s):</label>280 <input type="number" class="form-control me-2" v-model.number="data.repeatInterval" style="max-width: 100px;">281 <label class="form-label me-2">Máximo de Intentos:</label>282 <input type="number" class="form-control me-2" v-model.number="data.repeatMaxAttempts" style="max-width: 100px;">283 </div>284 <div class="mt-2">285 <label class="form-label">Condición para Repetir:</label>286 <input type="text" class="form-control" v-model="data.repeatCondition" placeholder="Ejemplo: response.status == 404">287 </div>288 <div class="mt-2">289 <label class="form-label">Condición de Éxito:</label>290 <input type="text" class="form-control" v-model="data.successCondition" placeholder="Ejemplo: response.status == 200 || response.status == 201">291 </div>292 </div>293 </div>294 </div>295 `,296 props: ['modelValue', 'step'],297 data() {298 return {299 showHeaders: false,300 showBody: false,301 showRepeat: false,302 }303 },304 computed: {305 data: {306 get() { return this.modelValue; },307 set(value) { this.$emit('update:modelValue', value); }308 }309 },310 methods: {311 toggleDetails() {312 this.step.expanded = !this.step.expanded;313 },314 toggleHeaders() {315 this.showHeaders = !this.showHeaders;316 },317 toggleBody() {318 this.showBody = !this.showBody;319 },320 toggleRepeat() {321 this.showRepeat = !this.showRepeat;322 },323 addHeader() {324 this.data.headers.push({ key: '', value: '' });325 },326 removeHeader(index) {327 this.data.headers.splice(index, 1);328 }329 }330 };331 332 const AssertionStep = {333 template: `334 <div class="d-flex align-items-center">335 <input type="text" class="form-control" v-model="data.condition" placeholder="Condición">336 </div>337 `,338 props: ['modelValue', 'step'],339 computed: {340 data: {341 get() { return this.modelValue; },342 set(value) { this.$emit('update:modelValue', value); }343 }344 }345 };346 347 const WaitStep = {348 template: `349 <div class="d-flex align-items-center">350 <label class="form-label me-2">Esperar</label>351 <input type="number" class="form-control me-2" v-model.number="data.seconds" placeholder="Segundos" style="max-width: 100px;">352 </div>353 `,354 props: ['modelValue', 'step'],355 computed: {356 data: {357 get() { return this.modelValue; },358 set(value) { this.$emit('update:modelValue', value); }359 }360 }361 };362 363 // Instancia de Vue364 const { createApp } = Vue;365 366 createApp({367 components: {368 'variable-step': VariableStep,369 'request-step': RequestStep,370 'assertion-step': AssertionStep,371 'wait-step': WaitStep,372 // Agrega más componentes si es necesario373 },374 data() {375 return {376 steps: [],377 darkTheme: false,378 }379 },380 computed: {381 isExecutable() {382 // Verifica si hay al menos un paso para ejecutar383 return this.steps.length > 0;384 }385 },386 methods: {387 getStepComponent(type) {388 switch (type) {389 case 'variable':390 return 'variable-step';391 case 'request':392 return 'request-step';393 case 'assertion':394 return 'assertion-step';395 case 'wait':396 return 'wait-step';397 // Otros casos...398 }399 },400 getStepTypeLabel(type, method) {401 switch (type) {402 case 'variable':403 return 'VAR';404 case 'request':405 return method;406 case 'assertion':407 return 'ASSERT';408 case 'wait':409 return 'WAIT';410 // Otros casos...411 }412 },413 moveUp(index) {414 if (index > 0) {415 [this.steps[index - 1], this.steps[index]] = [this.steps[index], this.steps[index - 1]];416 }417 },418 moveDown(index) {419 if (index < this.steps.length - 1) {420 [this.steps[index], this.steps[index + 1]] = [this.steps[index + 1], this.steps[index]];421 }422 },423 removeStep(index) {424 this.steps.splice(index, 1);425 },426 addStep(type) {427 let newStep = {428 type: type,429 data: {},430 deferred: false,431 expanded: false,432 result: null,433 showResult: false,434 };435 switch (type) {436 case 'variable':437 newStep.data = { name: '', value: '' };438 break;439 case 'request':440 newStep.data = {441 method: 'GET',442 url: '',443 headers: [],444 body: '',445 repeatInterval: 0,446 repeatMaxAttempts: 1,447 repeatCondition: '',448 successCondition: '',449 };450 break;451 case 'assertion':452 newStep.data = { condition: '' };453 break;454 case 'wait':455 newStep.data = { seconds: 1 };456 break;457 // Otros casos...458 }459 this.steps.push(newStep);460 },461 async executeTest() {462 // Reiniciar resultados463 this.steps.forEach(step => {464 if (step.type === 'request') {465 step.result = null;466 step.showResult = false;467 }468 });469 // Filtrar los pasos que no son "Deferred"470 const stepsToExecute = this.steps.filter(step => !step.deferred);471 // Ejecutar los pasos secuencialmente472 for (const step of stepsToExecute) {473 const res = await this.executeStep(step);474 if (!res.success) {475 return;476 }477 }478 // Ejecutar los pasos "Deferred"479 const deferredSteps = this.steps.filter(step => step.deferred);480 for (const step of deferredSteps) {481 await this.executeStep(step);482 }483 },484 async executeStep(step) {485 switch (step.type) {486 case 'variable':487 // Aquí podrías almacenar la variable en un contexto si es necesario488 return { success: true };489 case 'request':490 return await this.executeRequest(step);491 case 'assertion':492 // Implementar lógica de aserciones si es necesario493 return { success: true };494 case 'wait':495 await new Promise(resolve => setTimeout(resolve, step.data.seconds * 1000));496 return { success: true };497 // Otros casos...498 }499 },500 async executeRequest(step) {501 const data = step.data;502 try {503 let attempts = 0;504 let maxAttempts = data.repeatMaxAttempts || 1;505 let interval = data.repeatInterval || 0;506 let conditionMet = false;507 let response;508 do {509 attempts++;510 // Configurar la solicitud511 const config = {512 method: data.method.toLowerCase(),513 url: data.url,514 headers: {},515 };516 data.headers.forEach(header => {517 config.headers[header.key] = header.value;518 });519 if (data.method !== 'GET' && data.body) {520 config.data = data.body;521 }522 // Realizar la solicitud523 response = await axios(config);524 // Evaluar condiciones525 if (data.successCondition) {526 conditionMet = eval(data.successCondition);527 } else {528 conditionMet = true;529 }530 if (!conditionMet && attempts < maxAttempts) {531 await new Promise(resolve => setTimeout(resolve, interval * 1000));532 }533 } while (!conditionMet && attempts < maxAttempts);534 // Mostrar resultado dentro del paso535 step.result = {536 success: true,537 message: 'Solicitud ejecutada exitosamente.',538 details: {539 status: response.status,540 headers: JSON.stringify(response.headers, null, 2),541 body: JSON.stringify(response.data, null, 2)542 }543 };544 return { success: true };545 } catch (error) {546 step.result = {547 success: false,548 message: 'Error al ejecutar la solicitud.',549 details: {550 status: error.response ? error.response.status : 'Sin respuesta',551 headers: error.response ? JSON.stringify(error.response.headers, null, 2) : 'No disponible',552 body: error.response ? JSON.stringify(error.response.data, null, 2) : error.message553 }554 };555 return { success: false };556 }557 },558 executeRequestStep(index) {559 const step = this.steps[index];560 // Reiniciar resultado previo561 step.result = null;562 step.showResult = false;563 this.executeRequest(step);564 }565 },566 mounted() {567 // Inicializar el tema según las preferencias del usuario568 const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;569 if (prefersDark) {570 this.toggleTheme();571 }572 }573 }).mount('#app');574 575 // Manejo del botón de cambio de tema fuera de Vue576 document.getElementById('themeToggle').addEventListener('click', () => {577 document.body.classList.toggle('bg-dark');578 document.body.classList.toggle('text-white');579 const icon = document.getElementById('themeToggle').querySelector('i');580 icon.classList.toggle('bi-moon-fill');581 icon.classList.toggle('bi-sun-fill');582 });583 </script>584 </body>585 </html>586 587
Enlace
El enlace para compartir es: