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