La difícil tarea de cambiar tu asiento cuando compras un billete de tren online

Actualmente vivo en Portugal. El país tiene una buena red ferroviaria y uno de los medios de transporte que más me gusta cuando viajo es el tren, lo que la hace una gran opción para desplazarme.

Quería viajar hacia el sur del país así que me decidí a comprar mi billete de tren con la compañía de trenes del estado: Comboios de Portugal y aquí es cuando empieza mi aventura.

El proceso de búsqueda y selección de horario es muy sencillo y cómodo pero llegado el momento de seleccionar mi asiento fue cuando la cosa se volvió interesante: ¡no funciona!

Intentando seleccionar otro asiento

Me negaba a tener que comprar mi billete con el asiento que te asignan automáticamente, siempre me gusta elegir uno en el que los dos estén libres, a sabiendas de que pueden comprar el billete del asiento de al lado, pero si tengo suerte no me tocará nadie y podré ir más cómodo.

Como buen programador abrí una de las mejores herramientas que tenemos a nuestra disposición cuando estamos construyendo una web: Chrome Developer Tools con la esperanza de poder indagar un poco en el código y ver cuál era el problema. Por suerte para mí, el código Javascript de la web no tiene ningún tipo de minificado u ofuscación.

Lo primero que hice fue ver los event listeners de click de los asientos:

Click Event Listeners para el elemento <a href="#?!"> de cada asiento

Veo que hay dos eventos a la escucha del click en este botón (no me interesan todos los otros de jQuery o Bootstrap). Este es el código de cada uno:

// href # no jump to top
$(".slide-seats a").on("click", function(event) {
  event.preventDefault();
});

$(".slide-seats .item table a").on("click", function(event) {
  pickSeat($(this).find("img").attr("data-id"));
});

De los dos que veo, el que me llama la atención es el segundo ya que llama a una función pickSeat()

function pickSeat(seat) {
  if (changeable) {
    var idx = getSeatIndex(seat);
    isOver = false;
    var stat = getStatus(seat);
    if (!changing && idx != -1) {
      changing = true;
      toChangeIndex = idx;
      seats[toChangeIndex] = seat;
      img = $(".slide-seats .active").find("[data-id='" + seat + "']")[0];
      if (img != null) {// select
        var name = getImg(seat) + '.4';
        img.src = prefix + name + suffix;
      }
    } else if (changing && isSameTrain(seats[toChangeIndex], seat)
      && (stat == '0' || stat == '2')
      && (idx == -1 || idx == toChangeIndex)) {
      changing = false;
      resetSeat(seats[toChangeIndex]);
      img = $(".slide-seats .active").find("[data-id='" + seat + "']")[0];
      if (img != null) {// selected
        var name = getImg(seat) + '.2';
        img.src = prefix + name + suffix;
      }
      seats[toChangeIndex] = seat;
      toChangeIndex = -1;
    }
  }
}

Poniendo un punto de interrupción en la primera línea de esta función, al hacer click en uno de los asientos veo que la variable changeable es true y entra dentro del bloque condicional. La primera función llamada es getSeatIndex() pasando como parámetro el asiento:

function getSeatIndex(id) {
  var id2 = id.split(':')[0];
  for (var x = 0; x < seats.length; x++) {
    var id1 = seats[x].split(':')[0];
    if (id1 == id2)
      return x;
  }
  return -1;
}

Si el asiento no se encuentra dentro del array seats esta función devolverá -1. Como mi intención es entrar dentro de la siguiente condición en la función principal pickSeat(), cuando está dentro de la función de getSeatIndex() añado manualmente el asiento que quiero seleccionar dentro del array seats:

Modificando el array en tiempo de ejecución

Cuando la ejecución del código llega a la línea if (!changing && idx != -1) {, como hemos modificado la función getSeatIndex() la variable idx tiene un valor de 1 lo cual es distinto de -1 y la variable changing sigue siendo false con lo cual se ejecuta el código dentro del condicional, que era lo que estaba buscando. Cuando finaliza la ejecución de la función pickSeat() puedo ver como el color del asiento cambia a un gris claro:

Asiento seleccionado mediante el código

Tras estos pasos, iluso de mí, pensaba que ya podría continuar con mi nuevo asiento seleccionado pero al pulsar siguiente salta este mensaje de error:

🤔 Parece ser que no iba a ser tan fácil como esperaba.

Como este mensaje se muestra una vez haces click en Next route (ya que es un billete de ida y vuelta, también tengo que seleccionar el asiento para el viaje de vuelta) lo siguiente que hago es inspeccionar el botón. Tal y como hice con los botones de los asientos, reviso la parte de los Event Listeners pero para este botón no encuentro nada interesante.

Sin embargo, al mirar el elemento HTML encuentro que se está llamando a una función cuando se hace click en el botón:

Evento onclick en el elemento HTML

Sin tiempo que perder, vuelvo al código Javascript a ver que es lo que está pasando dentro de esta función change()

function change() {
  if (changing) {
    AlertMessage(msg_select);
    return false;
  }
  focusNo = true;
  if (sameSeats())
    ConfirmMessage(msg_not_changed, 'doChange()');
  else
    ConfirmMessage(msg_changed, 'doChange()');
  return false;
}

function doChange() {
  changeTripSeatsValue();	
  enableUnload();
  document.getElementById('next').click();
}

El mensaje que estaba viendo antes era debido a que la variable changing tiene el valor true, entonces dentro de la función change() se está ejecutando ese primer condicional mostrando el mensaje y devolviendo false.
Justo antes de este primer if cambio el valor a false:

Finalizo la ejecución del código con la esperanza de que ahora si iba a poder pasar al siguiente paso con mi asiento seleccionado pero... ¡no!

Ahora aparece un mensaje de error en la consola:

Algo se ha roto. Veo que el fallo viene de la función sameSeats() que es la que se llama dentro de la función change() en el segundo if

function sameSeats() {
  for (var x = 0; x < seats.length; x++) {
    var a1 = seats[x].split('_');
    var s1 = a1[0] + "_" + a1[1] + "_" + a1[2];
    var a2 = oldSeats[x].split('_');
    var s2 = a2[0] + "_" + a2[1] + "_" + a2[2];
    if (s2 != s1)
      return false;
  }
  return true;
}

Viendo el código de esta función y el error parece claro lo que está pasando. Se está intentando usar split en uno de los elementos del array seats o del array oldSeats, y posiblemente está usando un indice fuera del array. Como el for se está haciendo desde 0 hasta el tamaño del array seats, voy a suponer que este array es correcto, con lo cual el fallo estaría en la línea

var a2 = oldSeats[x].split('_');

Para confirmar esto vuelvo a hacer debug al código.
Si añado estas dos variables a la sección Watch puedo ver como, efectivamente, mi sospecha es cierta:

Con el punto de interrupción en la línea 199, como se muestra en la captura, voy a la consola y hago un oldSeats.push('184_4_64') para que la función sameSeats() no falle esta vez y devuelva false.

Finalmente la función doChange() se ejecuta. Esta función se encarga de llamar a otra, que es la encargada de hacer el cambio de asiento:

function changeTripSeatsValue() {
  var newValue = '';
  for (var x = 0; x < seats.length; x++) {
    newValue = newValue + seats[x];
    if(x != seats.length - 1) {
      newValue = newValue + ';';
    }
  }
  document.getElementById('tripSeats').value = newValue;
}

En esta función pongo un punto de interrupción en la última linea, que parece ser la importante, ya que se encarga de asignar el nuevo asiento como valor de un elemento con id tripSeats (supongo que es un input (posiblemente oculto) dentro de un form).

Cuando el debugger para en esta linea, veo como newValue tiene asignado un valor un poco extraño, con lo que decido cambiarlo al valor del asiento que estoy intentado seleccionar, y al continuar con la ejecución del código... ¡¡bingo!!

Por fin he podido seleccionar mi asiento :)

Finalmente mi nuevo asiento es asignado

Conclusiones

Mi intención con este post es la de documentar como hago un proceso de debugging de código Javascript usando las herramientas para developers de Google Chrome. Creo que es un proceso interesante y muy importante para cualquier desarrollador front-end.

En este caso en concreto, con la web de Comboios de Portugal, al usar vanilla Javascript (y algo de jQuery) y con un código bastante fácil de seguir, me ha resultado una tarea divertida ya que es un código que no he escrito yo, esto lo hace más interesante y me lo tomé como un reto.

Lo más gracioso viene ahora: después de haber hecho todo este proceso y haber podido seleccionar mi asiento he seguido probando cosas en esta web y en un momento dado vi este mensaje:

Realmente sí había una forma de seleccionar mi asiento

En resumen... que la selección de asientos funciona de forma que hay que hacer click primero en el asiento que te han asignado automáticamente y después hacer click en uno de los asientos libres. Y sí, haciéndolo así funciona perfectamente 🤦🏽‍♂️

Puede que yo sea muy torpe, pero como a nadie le gusta aceptar su torpeza empecé a revisar otras páginas de compra de billetes de trenes y aviones. Resulta que en todas las que probé la selección de asientos se hace de la forma que yo, instintivamente, estaba intentándolo: solo necesitas hacer click en el asiento libre.

En mi opinión, esta es la forma más amigable o más fácil de hacerlo y la que instintivamente una persona va a intentar. Creo que es un gran fallo de UX de esta página.

De todas formas y pese a que todo mi proceso para seleccionar el asiento no era necesario, me divertí mucho y acabé satisfecho al haber sido capaz de conseguir ese "reto".