No al cierre de webs
ShareCode
Permalink: http://www.treeweb.es/u/974/ 01/02/2011

ShareCode

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  /* Estilos para perfiles de entorno */114  .environment-controls {115  text-align: center;116  margin-bottom: 20px;117  }118  .environment-controls .btn {119  margin-right: 10px;120  }121  .environment-list {122  margin-bottom: 20px;123  }124  .environment-list .env-item {125  cursor: pointer;126  padding: 5px 10px;127  border: 1px solid #ced4da;128  border-radius: 4px;129  margin-bottom: 5px;130  }131  .environment-list .env-item.active {132  background-color: #0d6efd;133  color: white;134  }135  .modal-header {136  background-color: #0d6efd;137  color: white;138  }139  </style>140 </head>141 <body>142  <!-- Barra de navegación -->143  <nav class="navbar navbar-expand-lg navbar-dark bg-primary">144  <div class="container-fluid">145  <a class="navbar-brand" href="#">146  <!-- Logo -->147  <img src="https://via.placeholder.com/150x40?text=Logo" alt="Logo">148  </a>149  <span class="navbar-text text-white">150  Plataforma de Testing Automatizado de APIs151  </span>152  <button class="btn btn-secondary ms-auto theme-toggle" id="themeToggle">153  <i class="bi bi-moon-fill"></i>154  </button>155  </div>156  </nav>157 158  <!-- Contenido principal -->159  <div id="app" class="container my-4">160  <!-- Botones para Guardar y Cargar Flujos -->161  <div class="flow-controls">162  <button class="btn btn-outline-secondary" @click="saveFlow">163  <i class="bi bi-save"></i> Guardar Flujo164  </button>165  <button class="btn btn-outline-secondary" @click="loadFlow">166  <i class="bi bi-folder2-open"></i> Cargar Flujo167  </button>168  <!-- Input file oculto para cargar archivos -->169  <input type="file" id="fileInput" @change="handleFileUpload" accept=".json">170  </div>171 172  <!-- Controles de Perfiles de Entorno -->173  <div class="environment-controls">174  <button class="btn btn-outline-primary" @click="showEnvironmentModal">175  <i class="bi bi-gear"></i> Configurar Entornos176  </button>177  <span class="ms-3" v-if="currentEnvironment"><strong>Entorno Actual:</strong> {{ currentEnvironment.name }}</span>178  </div>179 180  <!-- Lista de pasos -->181  <div v-for="(step, index) in steps" :key="index" class="step">182  <span class="step-type">{{ getStepTypeLabel(step.type, step.data.method) }}</span>183 184  <div class="step-content">185  <div class="main-content">186  <!-- Contenido según el tipo de paso -->187  <component :is="getStepComponent(step.type)" v-model="step.data" :step="step"></component>188  <!-- Checkbox para "Deferred" -->189  <div v-if="step.type === 'request' || step.type === 'assertion'">190  <input type="checkbox" v-model="step.deferred" :id="'deferred-' + index">191  <label :for="'deferred-' + index" class="deferred-label">Deferred</label>192  </div>193  </div>194  <!-- Resultados dentro del bloque de solicitud -->195  <div v-if="step.type === 'request' && step.result" class="result-panel">196  <button class="result-toggle" @click="step.showResult = !step.showResult">197  <i class="bi" :class="step.showResult ? 'bi-eye-slash' : 'bi-eye'"></i>198  {{ step.showResult ? 'Ocultar' : 'Mostrar' }} Resultado199  </button>200  <div v-if="step.showResult">201  <div class="alert mt-2" :class="{'alert-success': step.result.success, 'alert-danger': !step.result.success}">202  <strong>Resultado:</strong> {{ step.result.message }}203  </div>204  <div v-if="step.result.details">205  <p><strong>Status Code:</strong> {{ step.result.details.status }}</p>206  <p><strong>Headers:</strong></p>207  <pre>{{ step.result.details.headers }}</pre>208  <p><strong>Body:</strong></p>209  <pre style="max-height: 200px; overflow-y: auto;">{{ step.result.details.body }}</pre>210  </div>211  </div>212  </div>213  </div>214  <div class="step-controls">215  <button @click="moveUp(index)" :disabled="index === 0"><i class="bi bi-arrow-up"></i></button>216  <button @click="moveDown(index)" :disabled="index === steps.length - 1"><i class="bi bi-arrow-down"></i></button>217  <button v-if="step.type === 'request'" @click="executeRequestStep(index)" class="execute-btn"><i class="bi bi-play-circle"></i></button>218  <button @click="removeStep(index)"><i class="bi bi-trash"></i></button>219  </div>220  </div>221 222  <!-- Botones para añadir nuevos pasos -->223  <div class="add-step">224  <button class="btn btn-outline-primary me-2" @click="addStep('variable')">225  <i class="bi bi-sliders"></i> Variable226  </button>227  <button class="btn btn-outline-primary me-2" @click="addStep('request')">228  <i class="bi bi-arrow-up-right-circle"></i> Solicitud HTTP229  </button>230  <button class="btn btn-outline-primary me-2" @click="addStep('assertion')">231  <i class="bi bi-check-circle"></i> Aserción232  </button>233  <button class="btn btn-outline-primary me-2" @click="addStep('wait')">234  <i class="bi bi-hourglass-split"></i> Wait235  </button>236  <!-- Agrega más botones si es necesario -->237  </div>238 239  <!-- Botón de Ejecución -->240  <div class="text-center my-4">241  <button class="btn btn-lg btn-success" @click="executeTest" :disabled="!isExecutable">242  <i class="bi bi-play-fill"></i> Ejecutar Prueba Completa243  </button>244  </div>245 246  <!-- Modal para Configurar Entornos -->247  <div class="modal fade" id="environmentModal" tabindex="-1" aria-labelledby="environmentModalLabel" aria-hidden="true">248  <div class="modal-dialog modal-lg">249  <div class="modal-content">250  <div class="modal-header">251  <h5 class="modal-title" id="environmentModalLabel">Perfiles de Entorno</h5>252  <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Cerrar"></button>253  </div>254  <div class="modal-body">255  <div class="row">256  <!-- Lista de entornos -->257  <div class="col-md-4">258  <h6>Entornos Disponibles</h6>259  <div class="environment-list">260  <div v-for="(env, index) in environments" :key="index" class="env-item" :class="{'active': env === selectedEnvironment}" @click="selectEnvironment(env)">261  {{ env.name }}262  </div>263  </div>264  <button class="btn btn-sm btn-primary" @click="addEnvironment">265  <i class="bi bi-plus-lg"></i> Añadir Entorno266  </button>267  </div>268  <!-- Detalles del entorno seleccionado -->269  <div class="col-md-8" v-if="selectedEnvironment">270  <h6>Variables del Entorno "{{ selectedEnvironment.name }}"</h6>271  <div class="mt-2">272  <div v-for="(variable, index) in selectedEnvironment.variables" :key="index" class="d-flex align-items-center mb-2">273  <input type="text" class="form-control me-2" v-model="variable.key" placeholder="Nombre de la variable" style="max-width: 150px;">274  <input type="text" class="form-control me-2" v-model="variable.value" placeholder="Valor" style="max-width: 150px;">275  <button class="btn btn-sm btn-outline-danger" @click="removeVariable(index)">276  <i class="bi bi-trash"></i>277  </button>278  </div>279  <button class="btn btn-outline-primary btn-sm" @click="addVariable">280  <i class="bi bi-plus-lg"></i> Agregar Variable281  </button>282  </div>283  </div>284  </div>285  </div>286  <div class="modal-footer">287  <button class="btn btn-secondary" data-bs-dismiss="modal">Cerrar</button>288  <button class="btn btn-primary" @click="saveEnvironments">Guardar Cambios</button>289  </div>290  </div>291  </div>292  </div>293  </div>294 295  <!-- Enlaces a Vue.js, Axios y Bootstrap JS -->296  <script src="https://cdn.jsdelivr.net/npm/vue@3.3.4/dist/vue.global.prod.js"></script>297  <script src="https://cdn.jsdelivr.net/npm/axios@1.4.0/dist/axios.min.js"></script>298  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>299 300  <!-- Componentes para los pasos -->301  <script>302  const VariableStep = {303  template: `304  <div class="d-flex align-items-center">305  <input type="text" class="form-control me-2" v-model="data.name" placeholder="Nombre" style="max-width: 150px;">306  <span>=</span>307  <input type="text" class="form-control ms-2" v-model="data.value" placeholder="Valor" style="max-width: 150px;">308  </div>309  `,310  props: ['modelValue', 'step'],311  computed: {312  data: {313  get() { return this.modelValue; },314  set(value) { this.$emit('update:modelValue', value); }315  }316  }317  };318 319  const RequestStep = {320  template: `321  <div>322  <div class="d-flex align-items-center flex-wrap">323  <select class="form-select me-2" v-model="data.method" style="max-width: 100px;">324  <option>GET</option>325  <option>POST</option>326  <option>PUT</option>327  <option>DELETE</option>328  </select>329  <input type="text" class="form-control me-2" v-model="data.url" placeholder="URL" style="flex: 1;">330  <!-- Botón para mostrar detalles -->331  <button class="btn btn-sm btn-outline-secondary me-2" @click="toggleDetails">332  <i class="bi" :class="step.expanded ? 'bi-chevron-up' : 'bi-chevron-down'"></i>333  </button>334  </div>335  <!-- Detalles opcionales -->336  <div v-if="step.expanded" class="details mt-2">337  <!-- Headers -->338  <button class="btn btn-sm btn-outline-secondary me-2" @click="toggleHeaders">339  <i class="bi" :class="showHeaders ? 'bi-dash' : 'bi-plus'"></i> Headers340  </button>341  <!-- Body -->342  <button class="btn btn-sm btn-outline-secondary me-2" @click="toggleBody" v-if="data.method !== 'GET'">343  <i class="bi" :class="showBody ? 'bi-dash' : 'bi-plus'"></i> Body344  </button>345  <!-- Repeat Config -->346  <button class="btn btn-sm btn-outline-secondary" @click="toggleRepeat">347  <i class="bi" :class="showRepeat ? 'bi-dash' : 'bi-plus'"></i> Repeat348  </button>349  <!-- Headers -->350  <div v-if="showHeaders" class="mt-2">351  <div v-for="(header, index) in data.headers" :key="index" class="d-flex align-items-center mb-2">352  <input type="text" class="form-control me-2" v-model="header.key" placeholder="Header Key" style="max-width: 150px;">353  <input type="text" class="form-control me-2" v-model="header.value" placeholder="Header Value" style="max-width: 150px;">354  <button class="btn btn-sm btn-outline-danger" @click="removeHeader(index)">355  <i class="bi bi-trash"></i>356  </button>357  </div>358  <button class="btn btn-outline-primary btn-sm" @click="addHeader">359  <i class="bi bi-plus-lg"></i> Agregar Header360  </button>361  </div>362  <!-- Body -->363  <div v-if="showBody" class="mt-2">364  <label class="form-label">Body</label>365  <textarea class="form-control" rows="3" v-model="data.body" placeholder="Cuerpo de la solicitud"></textarea>366  </div>367  <!-- Repeat -->368  <div v-if="showRepeat" class="mt-2">369  <div class="d-flex align-items-center">370  <label class="form-label me-2">Intervalo (s):</label>371  <input type="number" class="form-control me-2" v-model.number="data.repeatInterval" style="max-width: 100px;">372  <label class="form-label me-2">Máximo de Intentos:</label>373  <input type="number" class="form-control me-2" v-model.number="data.repeatMaxAttempts" style="max-width: 100px;">374  </div>375  <div class="mt-2">376  <label class="form-label">Condición para Repetir:</label>377  <input type="text" class="form-control" v-model="data.repeatCondition" placeholder="Ejemplo: response.status == 404">378  </div>379  <div class="mt-2">380  <label class="form-label">Condición de Éxito:</label>381  <input type="text" class="form-control" v-model="data.successCondition" placeholder="Ejemplo: response.status == 200 || response.status == 201">382  </div>383  </div>384  </div>385  </div>386  `,387  props: ['modelValue', 'step'],388  data() {389  return {390  showHeaders: false,391  showBody: false,392  showRepeat: false,393  }394  },395  computed: {396  data: {397  get() { return this.modelValue; },398  set(value) { this.$emit('update:modelValue', value); }399  }400  },401  methods: {402  toggleDetails() {403  this.step.expanded = !this.step.expanded;404  },405  toggleHeaders() {406  this.showHeaders = !this.showHeaders;407  },408  toggleBody() {409  this.showBody = !this.showBody;410  },411  toggleRepeat() {412  this.showRepeat = !this.showRepeat;413  },414  addHeader() {415  this.data.headers.push({ key: '', value: '' });416  },417  removeHeader(index) {418  this.data.headers.splice(index, 1);419  }420  }421  };422 423  const AssertionStep = {424  template: `425  <div class="d-flex align-items-center">426  <input type="text" class="form-control" v-model="data.condition" placeholder="Condición">427  </div>428  `,429  props: ['modelValue', 'step'],430  computed: {431  data: {432  get() { return this.modelValue; },433  set(value) { this.$emit('update:modelValue', value); }434  }435  }436  };437 438  const WaitStep = {439  template: `440  <div class="d-flex align-items-center">441  <label class="form-label me-2">Esperar</label>442  <input type="number" class="form-control me-2" v-model.number="data.seconds" placeholder="Segundos" style="max-width: 100px;">443  </div>444  `,445  props: ['modelValue', 'step'],446  computed: {447  data: {448  get() { return this.modelValue; },449  set(value) { this.$emit('update:modelValue', value); }450  }451  }452  };453 454  // Instancia de Vue455  const { createApp } = Vue;456 457  createApp({458  components: {459  'variable-step': VariableStep,460  'request-step': RequestStep,461  'assertion-step': AssertionStep,462  'wait-step': WaitStep,463  // Agrega más componentes si es necesario464  },465  data() {466  return {467  steps: [],468  darkTheme: false,469  environments: [], // Lista de entornos470  currentEnvironment: null, // Entorno seleccionado471  selectedEnvironment: null, // Para editar en el modal472  }473  },474  computed: {475  isExecutable() {476  // Verifica si hay al menos un paso para ejecutar477  return this.steps.length > 0;478  }479  },480  methods: {481  getStepComponent(type) {482  switch (type) {483  case 'variable':484  return 'variable-step';485  case 'request':486  return 'request-step';487  case 'assertion':488  return 'assertion-step';489  case 'wait':490  return 'wait-step';491  // Otros casos...492  }493  },494  getStepTypeLabel(type, method) {495  switch (type) {496  case 'variable':497  return 'VAR';498  case 'request':499  return method;500  case 'assertion':501  return 'ASSERT';502  case 'wait':503  return 'WAIT';504  // Otros casos...505  }506  },507  moveUp(index) {508  if (index > 0) {509  [this.steps[index - 1], this.steps[index]] = [this.steps[index], this.steps[index - 1]];510  }511  },512  moveDown(index) {513  if (index < this.steps.length - 1) {514  [this.steps[index], this.steps[index + 1]] = [this.steps[index + 1], this.steps[index]];515  }516  },517  removeStep(index) {518  this.steps.splice(index, 1);519  },520  addStep(type) {521  let newStep = {522  type: type,523  data: {},524  deferred: false,525  expanded: false,526  result: null,527  showResult: false,528  };529  switch (type) {530  case 'variable':531  newStep.data = { name: '', value: '' };532  break;533  case 'request':534  newStep.data = {535  method: 'GET',536  url: '',537  headers: [],538  body: '',539  repeatInterval: 0,540  repeatMaxAttempts: 1,541  repeatCondition: '',542  successCondition: '',543  };544  break;545  case 'assertion':546  newStep.data = { condition: '' };547  break;548  case 'wait':549  newStep.data = { seconds: 1 };550  break;551  // Otros casos...552  }553  this.steps.push(newStep);554  },555  async executeTest() {556  // Reiniciar resultados557  this.steps.forEach(step => {558  if (step.type === 'request') {559  step.result = null;560  step.showResult = false;561  }562  });563  // Filtrar los pasos que no son "Deferred"564  const stepsToExecute = this.steps.filter(step => !step.deferred);565  // Ejecutar los pasos secuencialmente566  for (const step of stepsToExecute) {567  const res = await this.executeStep(step);568  if (!res.success) {569  return;570  }571  }572  // Ejecutar los pasos "Deferred"573  const deferredSteps = this.steps.filter(step => step.deferred);574  for (const step of deferredSteps) {575  await this.executeStep(step);576  }577  },578  async executeStep(step) {579  switch (step.type) {580  case 'variable':581  // Aquí podrías almacenar la variable en un contexto si es necesario582  return { success: true };583  case 'request':584  return await this.executeRequest(step);585  case 'assertion':586  // Implementar lógica de aserciones si es necesario587  return { success: true };588  case 'wait':589  await new Promise(resolve => setTimeout(resolve, step.data.seconds * 1000));590  return { success: true };591  // Otros casos...592  }593  },594  async executeRequest(step) {595  const data = step.data;596  try {597  let attempts = 0;598  let maxAttempts = data.repeatMaxAttempts || 1;599  let interval = data.repeatInterval || 0;600  let conditionMet = false;601  let response;602  do {603  attempts++;604  // Configurar la solicitud605  const config = {606  method: data.method.toLowerCase(),607  url: this.replacePlaceholders(data.url),608  headers: {},609  };610  data.headers.forEach(header => {611  config.headers[this.replacePlaceholders(header.key)] = this.replacePlaceholders(header.value);612  });613  if (data.method !== 'GET' && data.body) {614  config.data = this.replacePlaceholders(data.body);615  }616  // Realizar la solicitud617  response = await axios(config);618  // Evaluar condiciones619  if (data.successCondition) {620  conditionMet = eval(data.successCondition);621  } else {622  conditionMet = true;623  }624  if (!conditionMet && attempts < maxAttempts) {625  await new Promise(resolve => setTimeout(resolve, interval * 1000));626  }627  } while (!conditionMet && attempts < maxAttempts);628  // Mostrar resultado dentro del paso629  step.result = {630  success: true,631  message: 'Solicitud ejecutada exitosamente.',632  details: {633  status: response.status,634  headers: JSON.stringify(response.headers, null, 2),635  body: JSON.stringify(response.data, null, 2)636  }637  };638  return { success: true };639  } catch (error) {640  step.result = {641  success: false,642  message: 'Error al ejecutar la solicitud.',643  details: {644  status: error.response ? error.response.status : 'Sin respuesta',645  headers: error.response ? JSON.stringify(error.response.headers, null, 2) : 'No disponible',646  body: error.response ? JSON.stringify(error.response.data, null, 2) : error.message647  }648  };649  return { success: false };650  }651  },652  executeRequestStep(index) {653  const step = this.steps[index];654  // Reiniciar resultado previo655  step.result = null;656  step.showResult = false;657  this.executeRequest(step);658  },659  saveFlow() {660  const dataStr = JSON.stringify(this.steps, null, 2);661  const blob = new Blob([dataStr], { type: "application/json" });662  const url = URL.createObjectURL(blob);663  const a = document.createElement('a');664  a.href = url;665  a.download = 'flujo_de_prueba.json';666  a.click();667  URL.revokeObjectURL(url);668  },669  loadFlow() {670  document.getElementById('fileInput').click();671  },672  handleFileUpload(event) {673  const file = event.target.files[0];674  if (file) {675  const reader = new FileReader();676  reader.onload = (e) => {677  try {678  const data = JSON.parse(e.target.result);679  // Validar que el archivo contiene un array de pasos680  if (Array.isArray(data)) {681  this.steps = data;682  } else {683  alert('El archivo no es válido.');684  }685  } catch (error) {686  alert('Error al cargar el archivo: ' + error.message);687  }688  };689  reader.readAsText(file);690  // Limpiar el valor del input para permitir cargar el mismo archivo nuevamente si es necesario691  event.target.value = '';692  }693  },694  // Métodos para Entornos695  showEnvironmentModal() {696  const modal = new bootstrap.Modal(document.getElementById('environmentModal'));697  modal.show();698  // Clonar el entorno seleccionado para editar699  this.selectedEnvironment = JSON.parse(JSON.stringify(this.currentEnvironment));700  },701  addEnvironment() {702  const envName = prompt('Nombre del nuevo entorno:');703  if (envName) {704  const newEnv = { name: envName, variables: [] };705  this.environments.push(newEnv);706  this.selectedEnvironment = newEnv;707  this.currentEnvironment = newEnv;708  }709  },710  selectEnvironment(env) {711  this.selectedEnvironment = env;712  },713  addVariable() {714  this.selectedEnvironment.variables.push({ key: '', value: '' });715  },716  removeVariable(index) {717  this.selectedEnvironment.variables.splice(index, 1);718  },719  saveEnvironments() {720  // Actualizar el entorno actual721  const index = this.environments.findIndex(env => env.name === this.selectedEnvironment.name);722  if (index !== -1) {723  this.environments.splice(index, 1, this.selectedEnvironment);724  this.currentEnvironment = this.selectedEnvironment;725  }726  // Cerrar el modal727  const modal = bootstrap.Modal.getInstance(document.getElementById('environmentModal'));728  modal.hide();729  },730  replacePlaceholders(text) {731  if (!text || !this.currentEnvironment) return text;732  let result = text;733  this.currentEnvironment.variables.forEach(variable => {734  const regex = new RegExp(`{{${variable.key}}}`, 'g');735  result = result.replace(regex, variable.value);736  });737  return result;738  },739  // Añadimos el método toggleTheme740  toggleTheme() {741  document.body.classList.toggle('bg-dark');742  document.body.classList.toggle('text-white');743  const icon = document.getElementById('themeToggle').querySelector('i');744  icon.classList.toggle('bi-moon-fill');745  icon.classList.toggle('bi-sun-fill');746  },747  },748  mounted() {749  // Inicializar el tema según las preferencias del usuario750  const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;751  if (prefersDark) {752  this.toggleTheme();753  }754  // Inicializar entorno por defecto755  const defaultEnv = { name: 'Default', variables: [] };756  this.environments.push(defaultEnv);757  this.currentEnvironment = defaultEnv;758  this.selectedEnvironment = defaultEnv;759  }760  }).mount('#app');761 762  // Manejo del botón de cambio de tema fuera de Vue763  document.getElementById('themeToggle').addEventListener('click', () => {764  const app = document.getElementById('app').__vue_app__.config.globalProperties;765  app.toggleTheme();766  });767  </script>768 </body>769 </html>770 


Este ShareCode tiene versiones:
  1. Plataforma de Testing Automa... (28/09/2024)
Enlace
El enlace para compartir es: