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