Vídeo de Ricardo López Arriaga Bueno Clase 5 JavaScript 1 parte 1/2 Sintaxis Básica y Funciones@rickylobu en YouTube

JavaScript: Historia y Evolución

Escrito por: Ricardo López Arriaga Bueno

JavaScript es un lenguaje de programación que permite implementar funcionalidades complejas en páginas web, desde contenido dinámico hasta interacción con el usuario. MDN Web Docs

Nacimiento de JavaScript

En 1995, Brendan Eich creó JavaScript en solo 10 días mientras trabajaba en Netscape, con el objetivo de desarrollar un lenguaje de scripting fácil de usar tanto para diseñadores como para programadores. MDN Web Docs

La Guerra de los Navegadores

En los años 90, la competencia entre Netscape y Microsoft llevó a la fragmentación de JavaScript debido a diferentes implementaciones en los navegadores. Wikipedia

Estándar ECMAScript

En 1997, JavaScript fue estandarizado como ECMAScript por ECMA International, estableciendo un marco para su desarrollo futuro. ECMA International

Evolución del Lenguaje

  • ECMAScript 3 (1999): Mejoras en cadenas y expresiones regulares.
  • ECMAScript 5 (2009): Introducción del modo estricto y nuevos métodos de array.
  • ECMAScript 6 (2015): let/const, Clases, módulos, promesas, y arrow functions
  • ECMAScript 2016 (ES7): Introdujo el operador de exponenciación y el método Array.prototype.includes.
  • ECMAScript 2017 (ES8): Añadió async/await y mejoras en la manipulación de objetos.
  • ECMAScript 2018 (ES9): Introdujo rest/spread properties y mejoras en las expresiones regulares.
  • ECMAScript 2019 (ES10): Añadió flat/flatMap para arrays y mejoras en el manejo de errores.
  • ECMAScript 2020 (ES11): Introdujo el operador de encadenamiento opcional y nullish coalescing.
  • ECMAScript 2021 (ES12): Añadió métodos como replaceAll y mejoras en las promesas.
  • ECMAScript 2022 (ES13): Introdujo top-level await y mejoras en las clases.
  • ECMAScript 2023 (ES14): Añadió mejoras en los módulos y nuevas funciones de array.

El estándar actual

JavaScript (JS) es un lenguaje de programación ligero, interpretado, o compilado justo-a-tiempo (just-in-time) con funciones de primera clase. Si bien es más conocido como un lenguaje de scripting (secuencias de comandos) para páginas web, y es usado en muchos entornos fuera del navegador como Node.js, JavaScript es un lenguaje de programación basada en prototipos, multiparadigma, de un solo hilo, dinámico, con soporte para programación orientada a objetos, imperativa y declarativa (por ejemplo programación funcional). MDN Web Docs

En la actualidad, la web funciona con HTML5, CSS3, y ECMAScript, que se renueva anualmente, siendo la versión actual ECMAScript 2024. Angular está actualmente en Angular 18, utiliza TypeScript en su versión 5.4 y transpila por defecto a ECMAScript 2020.

Los sitios web pueden ser estáticos, dinámicos, plataformas, ERP (Enterprise Resource Planning), y demás términos para referirse a plataformas de gestión de recursos empresariales como recursos humanos, financieros, de gestión de proyectos, etc. También pueden ser aplicaciones web que funcionan con múltiples servidores en la nube que trabajan dando soporte a tu aplicación en todo el mundo (CDN). Con Node.js, lanzado en 2009, podemos usar JavaScript tanto en el frontend como en el backend, lo que permite aplicaciones isomórficas. Node.js ha abierto el camino para la creación de APIs que se ejecutan en el servidor, respondiendo a solicitudes que pueden dar solución por sí mismas o trabajar en conjunto con múltiples servicios que incluso pueden estar en distintos lenguajes dentro de contenedores como Docker o Kubernetes, comunicados entre sí mediante el protocolo HTTP, enviando y recibiendo JSON. Cada servicio los procesa en su propio lenguaje, que puede ser JavaScript, Java, PHP, Python, C++, C#, etc. (microservicios).
También pueden consultar otras APIs externas dentro de sus mismos procesos. Estos servidores CDN dan respuesta por medio de nuestra API (backend) a solicitudes de los clientes (frontend).

La respuesta es enviada a aplicaciones web en el navegador, en desktop, o en aplicaciones móviles de múltiples sistemas operativos. El desarrollador web en la actualidad utiliza frameworks como Angular, React y Vue.js, que son escritos en TypeScript y transpilados a JavaScript para que puedan ser interpretados por el navegador o PWA mediante Ionic o desktop mediante Electron. Si su aplicación requiere de usar funciones nativas del dispositivo móvil para algo fundamental o si requiere de un mejor rendimiento, deberá hacer otro cliente (frontend) para la versión de Android con Kotlin o en Swift para iOS (multiplataforma).

Por último mencionar que, el término “Full Stack” se refiere al desarrollador que domina tanto la parte del frontend como el backend. Sin embargo, el stack de tecnologías, lenguajes y frameworks puede ser muy diverso. Le recomiendo investigar sobre el “roadmap” del stack conforme a la decisión de especialización con la que quiera continuar sus estudios.

Historia y fundamentos de la Web


Añadir JavaScript a HTML

Existen tres formas principales de añadir JavaScript a un documento HTML: directamente en línea dentro de un atributo HTML como onclick, dentro de etiquetas <script> en el documento HTML antes de cerrar la etiqueta </body>, o vinculando un archivo externo .js usando la etiqueta <script src="archivo.js"> de igual forma antes de cerrar la etiqueta </body>.

  • Con un atributo de evento inline:

    Este método permite ejecutar JavaScript directamente dentro de un atributo de evento de un elemento HTML.

    <button onclick="alert('Hola Mundo!');">Haz clic aquí</button>
  • Con la etiqueta <script>:

    Este método permite escribir código JavaScript directamente en el documento HTML.

    
    <body>
    <!-- cuerpo del body-->
    	<button onclick="mostrarMensaje()">Mostrar Mensaje</button>
    	.
    	.
    	.
    	<!-- Antes de cerrar el body-->
    
    	<script>
    		function mostrarMensaje() {
    			alert('Este mensaje proviene de una función en un script inline.');
    		}
    	</script>
    	
    </body>
    	
  • Con un archivo JavaScript externo:

    Este método permite mantener el código JavaScript separado del HTML, lo que mejora la organización y el mantenimiento del proyecto.

    
    	<script src="script.js"></script>
    		

    Crea un archivo llamado script.js con el siguiente contenido:

    
    // Contenido de script.js
    function saludar() {
    	alert('Hola desde un archivo JavaScript externo.');
    }
    	

    Luego, puedes vincularlo al documento HTML para usar las funciones definidas en el archivo.


Declaración de Variables: var, let y const

En JavaScript, existen tres formas principales de declarar variables:

  • var: Se utilizaba ampliamente antes de ES6, pero presenta problemas de alcance y redeclaración. Es de alcance global o de función.
  • let: Introducido en ES6, permite declarar variables con alcance de bloque y evita la redeclaración en el mismo ámbito.
  • const: También introducido en ES6, se utiliza para declarar constantes, es decir, variables cuyo valor no puede cambiar. Su alcance también es de bloque.

Tipos Primitivos en JavaScript

JavaScript cuenta con los siguientes tipos primitivos:

  • number: Representa tanto números enteros como de punto flotante.
  • string: Representa secuencias de caracteres, delimitadas por comillas simples, dobles o backticks.
  • boolean: Solo tiene dos valores posibles: true o false.
  • null: Representa la ausencia intencional de un valor.
  • undefined: Indica que una variable no ha sido asignada.
  • symbol: Introducido en ES6, representa un identificador único.
  • bigint: Introducido en ES11, permite representar números enteros muy grandes.

Ejemplo Variables tipos primitivos

╭☞( ͡ ͡° ͜ ʖ ͡͡°)╭☞ Presiona F12 para ver la consola

Verás sólo el primer "Aloha Curioso!..."
Si presionas el botón: Ejecutar verás cómo se ejecuta el código anterior ←

Comparación con Tipos de Datos Primitivos

== (igualdad débil): Compara dos valores después de convertirlos a un tipo común. Por ejemplo, 5 == '5' es true porque convierte la cadena '5' a número antes de comparar.

=== (igualdad estricta): Compara dos valores sin realizar ninguna conversión de tipo. Por ejemplo, 5 === '5' es false porque uno es número y el otro es cadena.

Comparación con Tipos de Datos de Referencia (Objetos, Funciones, Arreglos)

== y ===: Ambos operadores comparan referencias, no los valores reales. Dos objetos son iguales solo si apuntan a la misma ubicación en memoria. Por ejemplo:


let obj1 = { a: 1 };
let obj2 = { a: 1 };
console.log(obj1 == obj2); // false
console.log(obj1 === obj2); // false
let obj3 = obj1;
console.log(obj1 == obj3); // true
console.log(obj1 === obj3); // true
						

Ejemplo: Operadores en JavaScript

╭☞( ͡ ͡° ͜ ʖ ͡͡°)╭☞ Presiona F12 para ver la consola

Presiona el botón para ejecutar el archivo de operadores en JavaScript.



Estructuras de Control en JavaScript

Condicional: if

El condicional `if` permite ejecutar un bloque de código si una condición es verdadera.

Condicional: else if

El condicional `else if` permite evaluar múltiples condiciones en un bloque de código.

Condicional: switch

El condicional `switch` permite evaluar una variable contra múltiples casos específicos.

Bucle: for

El bucle `for` se utiliza para iterar un bloque de código un número definido de veces.

Bucle: while

El bucle `while` ejecuta un bloque de código mientras una condición sea verdadera.

Bucle: do while

El bucle `do while` ejecuta un bloque de código al menos una vez antes de verificar la condición.


Vídeo de Ricardo López Arriaga Bueno Clase 6 JavaScript 1 parte 2/2 Funciones hasta POO@rickylobu en YouTube

Funciones en JavaScript

Las funciones en JavaScript son la razón por la que JavaScript es un lenguaje tán versatil y flexible, pueden ser creadas mediante la palabra reservada function.

En JavaScript, las funciones son objetos de primera clase. Esto significa que pueden ser asignadas a variables, pasadas como argumentos a otras funciones y devueltas por otras funciones.

Función Global

Las funciones globales pueden ser llamadas desde cualquier lugar del documento.

Funciones Anónimas

Las funciones anónimas no tienen un nombre definido y pueden ser asignadas a variables o constantes.

Funciones asignadas a constantes

Las funciones pueden asignarse a constantes para ser reutilizadas.

Funciones flecha

Las funciones flecha ofrecen una sintaxis más corta y no vinculan su propio this.

Funciones como parámetros

En JavaScript, las funciones pueden pasarse como argumentos a otras funciones.

Funciones como métodos de objetos

En JavaScript, las funciones pueden formar parte de objetos como métodos.


Uso de this

El contexto de this puede variar dependiendo de cómo se llame una función y el ámbito donde es se utiliza this.

En funciones anidadas, es común usar una variable para almacenar el valor de this del contexto exterior o utilizar funciones flecha para heredar el this del contexto en el que fueron definidas.

Contexto Global

En el contexto global, this se refiere al objeto global (window en navegadores).

╭☞( ͡ ͡° ͜ ʖ ͡͡°)╭☞ Presiona F12 para ver la consola, escribe this; y preciona Enter

Contexto de Objeto

Dentro de un método de un objeto, this se refiere al objeto al que pertenece el método.

Con ejemplos es más fácil entender, preciona el botón y análiza con atención los siguientes ejemplos:


Closure

Un closure es una función que recuerda el ámbito (scope) en el que fue creada, incluso después de que ese ámbito haya terminado. Esto significa que una función puede acceder a variables de su ámbito exterior, incluso después de que la función exterior haya terminado de ejecutarse.
Contexto Léxico: Es el contexto en el que una función fue creada. Un closure "recuerda" este contexto léxico.

Ámbito (Scope): Es el contexto en el que las variables y funciones son accesibles. En JavaScript, hay tres tipos principales de ámbitos:

  • Global: Variables accesibles en cualquier parte del código.
  • Local: Variables accesibles solo dentro de una función.
  • Bloque: Variables accesibles solo dentro de un bloque de código (por ejemplo, dentro de un if o for).

Cronómetro

Controles del Cronómetro


Programación Orientada a Objetos en JavaScript

JavaScript no es orientado a objetos como tal, sino que usa un sistema llamado prototipos (prototype).

Los prototipos son como una especie de moldes. Cuando creas un objeto en JavaScript, este hereda propiedades y métodos de otro objeto llamado prototipo. Es decir, el objeto "hijo" comparte características del objeto "padre".

Imagina que estás construyendo una casa. En lenguajes puramente orientados a objetos como Java, sería como tener un plano detallado (la clase) que usas para construir cada casa (los objetos). En JavaScript, es más como copiar una casa existente (el prototipo) y hacer algunos cambios para crear una nueva (el objeto).

La ventaja es que puedes modificar el prototipo original y todos los objetos que se basaron en él también cambian. como actualizar el plano original y que todas las casas nuevas tengan los cambios incluidos.

Cuando utilizas la sintaxis de class y extends en JavaScript, por debajo el motor del lenguaje lo traduce y ejecuta utilizando prototype. Es decir, aunque escribas código como si estuvieras usando clases, JavaScript internamente está trabajando con prototipos y la herencia prototípica.
Esto significa que la forma en que defines tus "clases" y la herencia con extends se convierte en operaciones sobre prototipos. La sintaxis de class y extends es simplemente una forma más legible y amigable para los desarrolladores que vienen de lenguajes orientados a objetos basados en clases, pero la esencia de JavaScript sigue siendo la herencia prototípica.

Clases: Plantillas para crear objetos. Definen propiedades y métodos.

Objetos: Instancias de clases. Contienen datos y comportamientos.

Herencia: Permite que una clase herede propiedades y métodos de otra clase debido al encadenamiento de prototipos, los prototipos pueden heredar de otros prototipos, creando una cadena de prototipos. Cuando accedes a una propiedad o método de un objeto, JavaScript busca en la cadena de prototipos hasta encontrarlo.

Es importante recordar que para poder Agregar una herencia a un prototypo del lenguaje javascript como Array.prototype.CustomMethod = function (miArray) {};/p> se utiliza function para crear su propio this.

Encapsulamiento: Oculta los detalles internos de un objeto y expone solo lo necesario.

En JavaScript, no existe una palabra clave private como en otros lenguajes totalmente orientados a objetos.

Con ECMAScript 2020, se introdujeron los campos privados de clase utilizando el símbolo #.

Polimorfismo: Permite que diferentes clases utilicen el mismo método de diferentes maneras.

El polimorfismo es la capacidad de un objeto de tomar muchas formas. En JavaScript, esto se logra a través de la herencia prototípica y la capacidad de sobrescribir métodos en la cadena de prototipos.

Imagina que tienes un prototipo llamado "Animal" con un método "hacerSonido()". Luego, creas dos prototipos "hijos" llamados "Perro" y "Gato", ambos heredando de "Animal". Cada uno de estos prototipos "hijos" puede sobrescribir el método "hacerSonido()" para que haga un sonido diferente (por ejemplo, "Guau" para "Perro" y "Miau" para "Gato").

Ahora, puedes tener un array de objetos, algunos "Perros" y algunos "Gatos". Cuando llamas al método "hacerSonido()" en cada objeto, se ejecutará la versión correcta del método según el prototipo del objeto (ya sea "Guau" o "Miau"). Esto es polimorfismo: el mismo método ("hacerSonido()") se comporta de manera diferente según el tipo de objeto.

Abstracción: Simplifica la complejidad ocultando los detalles innecesarios y mostrando solo la funcionalidad esencial.

En JavaScript, no existen las palabras reservadas abstract e interface como en otros lenguajes de programación orientada a objetos (POO). Aunque JavaScript no tiene soporte nativo para clases abstractas e interfaces, se pueden simular mediante patrones de diseño y convenciones. Las clases abstractas se pueden crear lanzando errores en el constructor si se intenta instanciarlas directamente, y las interfaces se pueden definir mediante objetos que especifican métodos que deben ser implementados.

Ejemplo de Clases y Objetos

Imagina que debes crear un sistema de Vehiculos y necesitas crear objetos de tipo Coche pero también Motos donde estos objetos hereden de Vehiculo e implementen multiples interfaces que obliguen a que arranquen y frenen de forma diferente entre motos y coches y el Coche que implemente una interfaz que haga un carro convertible cosa que las motos no hacen

Ahora que sabes los conceptos básicos de Programación Orientada a Objetos (POO) te recomiendo que terminando esta clase te des el tiempo de investigar y sobre todo practicar POO con principios SOLID.

Otra tarea iteresante que te recomiendo es investigar sobre el patrón de diseño creacional Factory y comenzar a profundizar en patrones de diseño en general.
Un excelente punto de partida es saber que existen múltiples tipos de patrones de diseño como creacionales, estructurales y de comportamiento.


Vídeo de Ricardo López Arriaga Bueno Clase 7 JavaScript 2 Arrays, P. Funcional y Métodos de orden Superior de Arrays@rickylobu en YouTube

Arrays en JavaScript

En JavaScript, un array es una estructura de datos utilizada para almacenar múltiples valores en una sola variable. Puedes declarar un array vacío y asignarlo a una variable utilizando const miArray = [];. También es posible crear un array inicializado con valores de diferentes tipos de datos: const miArray = [42, "texto", true, null, undefined];.

Generalmente, los arreglos contienen elementos del mismo tipo o estructuras de objetos similares, ya que esto facilita aplicar lógica uniforme sobre ellos mediante bucles o métodos nativos del lenguaje. Por ejemplo, un array de objetos puede representar datos complejos como usuarios, productos o configuraciones personalizadas: const usuarios = [{nombre: "Juan", edad: 30}, {nombre: "Ana", edad: 25}];.

Los objetos dentro de un array pueden tener arreglos internos o estructuras más complejas. Por ejemplo, podrías modelar un sistema de pasajeros y un conductor para un coche o un autobús, lo que podría llevar a la construcción de un sistema más grande compuesto por múltiples objetos. Para construir sistemas así de complejos donde el constructor se hace enorme, te recomiendo investigar el patrón de diseño creacional Builder y explorar más patrones de diseño en general.

Métodos para agregar y eliminar elementos

push()

Agrega uno o más elementos al final del array y devuelve la nueva longitud del array.

pop()

Elimina el último elemento del array y lo devuelve.

unshift()

Agrega uno o más elementos al principio del array y devuelve la nueva longitud del array.

shift()

Elimina el primer elemento del array y lo devuelve.

Métodos para transformar y ordenar

concat()

Combina dos o más arrays y devuelve un nuevo array resultante.

sort()

Ordena los elementos del array alfabéticamente o según una función de comparación.

slice()

Devuelve una copia superficial de una porción del array en un nuevo array.

reverse()

Invierte el orden de los elementos en el array.

flat()

Devuelve un nuevo array con todos los elementos de subarrays concatenados en un solo nivel.

Métodos para obtener información sobre el array

includes()

Comprueba si un elemento está presente en el array: const array = [1, 2, 3, 4, "Ricardo", "Willy", "Patricio", "Bob", "Alan", "Francisco" ];

indexOf()

Devuelve el primer índice en el que se encuentra un elemento especificado, o -1 si no se encuentra.

lastIndexOf()

Devuelve el último índice en el que se encuentra un elemento especificado, o -1 si no se encuentra.

length

Devuelve la cantidad de elementos en el array.


Programación funcional en JavaScript

Historia de la Programación Funcional

La programación funcional tiene sus raíces en las matemáticas, específicamente en el cálculo lambda (λ), desarrollado por Alonzo Church en la década de 1930. Este sistema formal define las funciones como ciudadanos de primera clase y enfatiza la aplicación de funciones y la composición, en lugar de cambios en el estado o la mutabilidad.

A medida que los lenguajes de programación han evolucionado, muchos han adoptado elementos funcionales. JavaScript, aunque originalmente no fue diseñado como un lenguaje puramente funcional, ha integrado conceptos clave, especialmente con la llegada de ES6.

Conceptos fundamentales:

Funciones de Primera Clase: Como vimos anteriormente las funciones son "ciudadanos de primera clase", lo que significa que pueden asignarse a variables, pasarse como argumentos y devolverse desde otras funciones. Observa con atención el siguiente ejemplo:


const saludo = () => "Hola Curioso!"; // asignamos una función anónima a variable 
const decirHola = saludo; // Asignar una función a otra variable
console.log(decirHola()); // ejecutamos la función decirHola() y el resultado se lo mandamos a .log. Salida: Hola Curioso!
console.log(decirHola); // 	mandamos la función decirHola a la función .log. Salida: () => "Hola, Mundo!" 
					

Inmutabilidad: En programación funcional, los datos no se modifican. En su lugar, se crean nuevas versiones con los cambios necesarios. Esto evita efectos secundarios y hace que el código sea más predecible.


const array = [1, 2, 3];
const newArray = [...array, 4]; // Se crea un nuevo arreglo con spread operator (...)
console.log(array);  // [1, 2, 3]
console.log(newArray);  // [1, 2, 3, 4]
				

Transparencia Referencial: Una función es transparente si, dada la misma entrada, siempre produce la misma salida, sin efectos secundarios.


const suma = (a, b) => a + b;
console.log(suma(2, 3));  // Siempre devuelve: 5
console.log(suma("Hola", " Mundo"));  // Siempre devuelve: Hola Mundo
				

Programación Funcional en JavaScript

Funciones de Orden Superior

Una función de orden superior es aquella que toma una o más funciones como parámetros/argumentos, o devuelve una función. Por ejemplo:

intervalo = setInterval(() => {
	tiempo++;
	console.log(tiempo);
}, 1000);

Funciones Callbacks

Son funciones que se pasan como argumentos a otras funciones y se ejecutan posteriormente.

Generalmente se utilizan funciones Callback cuando estamos trabajando con funciones asincronas como veremos más adelante.

Funciones Puras

Una función pura es aquella que no tiene efectos secundarios y siempre devuelve el mismo resultado para los mismos argumentos.

En programación funcional, las funciones puras son aquellas que cumplen dos condiciones principales:

  1. Determinismo: Dadas las mismas entradas, siempre producen la misma salida. No dependen de ningún estado externo o variable global que pueda cambiar entre llamadas.
  2. Sin efectos secundarios: No modifican ningún estado externo ni producen efectos observables fuera de su propio ámbito. No alteran variables globales, objetos mutables ni realizan operaciones de entrada/salida.

Composición de Funciones

Combinar funciones simples para crear funciones más complejas. (f, g) => x => f(g(x));


Métodos de Orden Superior de Arrays

forEach()

Ejecuta una función para cada elemento del array.

Es una función de orden superior porque acepta otra función como argumento, pero no es una función pura. Esto se debe a que forEach() se utiliza para provocar efectos secundarios (por ejemplo, modificar variables externas o imprimir en consola) y no devuelve un nuevo array.

sort()

Ordena los elementos del mismo array según una función de comparación, modificando el array original. Por ello, aunque es de orden superior, no es pura ya que altera el estado del array.


Funciones Puras de Orden Superior de Arrays

map()

Aplica la función callback a cada elemento del array y devuelve un nuevo array con los resultados. No modifica el array original, lo que la convierte en una función pura, respetando el principio de inmutabilidad y de transparencia referencial.

filter()

Devuelve un nuevo array con todos los elementos que cumplen la condición especificada en la función callback. El array original permanece sin cambios, por lo que es una función pura.

reduce()

Recorre el array y acumula sus elementos en un único valor mediante una función acumuladora, devuelto sin modificar el array original. Siempre que la función callback sea pura, reduce() es una función pura.

some()

Comprueba si al menos un elemento del array cumple la condición definida en el callback. Devuelve un valor booleano sin modificar el array, por lo que es pura.

every()

Verifica si todos los elementos del array cumplen la condición especificada por el callback. Devuelve un valor booleano y no altera el array original, siendo así una función pura.

find()

Devuelve el primer elemento del array que cumpla la condición definida en el callback. No modifica el array original, por lo que es una función pura.

findIndex()

Devuelve el índice del primer elemento que cumple la condición del callback o -1 si ninguno la cumple. Al no alterar el array original, es una función pura.

flatMap()

Aplica una función a cada elemento del array y luego aplana el resultado un nivel en un nuevo array. No modifica el array original, lo que la hace pura y acorde con la inmutabilidad.


Recursividad y Composición de Funciones en Programación Funcional

La recursividad es una técnica en la que una función se llama a sí misma para resolver problemas que pueden dividirse en subproblemas más pequeños. En programación funcional, esta característica es muy valiosa para iterar sobre estructuras complejas, como arrays que contienen objetos anidados o arrays dentro de objetos. Gracias a la recursividad, es posible recorrer de manera concisa cualquier nivel de anidamiento. Además, la composición de funciones permite encadenar varias transformaciones de datos en un solo flujo de operaciones, respetando el principio de inmutabilidad. De este modo, cada función es pura (sin efectos secundarios) y devuelve un nuevo valor basado en su entrada, lo que facilita el razonamiento y la reutilización del código.

Dado que en JavaScript se pueden procesar estructuras de datos complejas donde cada elemento puede ser un objeto con atributos y métodos, o incluso contener arrays anidados. Este enfoque se potencia cuando se combinan la recursividad y la composición de funciones, ya que permite transformar cada nivel de la estructura sin alterar los datos originales. Es importante tener presente que en JavaScript “todo es un prototipo”: los objetos, las funciones y los arrays son, a su vez, objetos que heredan de un prototipo. Esta característica es esencial para entender cómo se estructuran y manipulan los datos, ya que permite que cada objeto tenga sus propios métodos y atributos, facilitando la extensión y la reutilización de código a través de la cadena de prototipos.


Vídeo de Ricardo López Arriaga Bueno Clase 8 JavaScript 3 parte 1/2 Manipulacion del DOM@rickylobu en YouTube

Manipulación del DOM en JavaScript

Como ya sabemos JavaScript es el lenguaje que entiende el navegador y es un lenguaje que esta 100% ligado a la Web, es por esto que es tán importante saber manipular el DOM (Document Object Model) formado por document vinculado a la etiqueta <body>, del cual se desprenden todos los elementos del contenido visible de la página Web cargada, generando el DOM, de esta forma podemos gestionar los eventos y su propagación por el DOM por medio del método addEventListener como veremos en la siguiente sección.

El DOM y la Interpretación del HTML en el Navegador

Cuando se carga una página web, el navegador interpreta las etiquetas HTML (por ejemplo, las etiquetas <html>, <body>, <div>, etc.) y construye un árbol de nodos, conocido como el Document Object Model (DOM). Cada uno de estos nodos es un objeto que, al igual que cualquier otro objeto en JavaScript, se basa en un prototipo y dispone de propiedades y métodos. Esta estructura jerárquica permite identificar fácilmente relaciones de padre, hijo, hermano, etc. y es fundamental para la manipulación dinámica de la interfaz mediante JavaScript. La capacidad de recorrer y transformar estas estructuras mediante recursividad y composición de funciones de la programación funcional se vuelve muy poderosa a la hora de aplicar cambios, buscar elementos o actualizar información en el DOM sin alterar el estado global de la aplicación, entendiendo que al modificar elementos no se utilizan las funciones puras en la manipulación del DOM pero si en las transformaciones de los datos.

El Objeto Global y el Manejo de Eventos

El objeto global window es el contenedor de todo el entorno de ejecución en el navegador, y dentro de él se encuentra el objeto document, que representa el DOM. Dado que tanto arrays como funciones y objetos en JavaScript se basan en prototipos, tanto window, como document, como cada elemento del DOM son instancias de objetos de HTMLDivElement o HTMLParagraphElement etc. heredan de HTMLElement que tiene el protipo Element.prototype que a su vez hereda de Node que hereda de EventTarget hasta Object

Dado que los objetos del DOM son, en esencia, objetos comunes (typeof Object retorna "object"). Esto implica que heredan de Object.prototype y, por lo tanto, tienen acceso a sus métodos y propiedades. Por ejemplo con Object.setPrototypeOf(obj, prototype); podemos indicar que un tipo de prototipo cambie, lo cual no es muy recomendado pero con ello podemos lograr un polimorfismo dinámico donde podemos hacer que un Objeto que sobrecarga un método para que, en lugar de que se comporte diferente en cada tipo de instancia como en el polimorfismo normal, se comporte como uno de los otros tipos que implmentan de forma diferente el mismo método. Te invito a pensar un escenario donde esto sea necesario.

Además, el manejo de eventos en la web, mediante métodos como addEventListener, se apoya en el concepto de “burbujeo de eventos” (event bubbling), donde los eventos generados en un nodo se propagan a sus nodos padres. Lo que quiero que comprendas de esta sección es que el DOM y todos sus elementos son objetos basados en prototipos que se comportan como objetos, es decir que heredan propiedades y métodos a lo largo de la jerarquía, lo que resulta sumemente importante para la manipulación del DOM y la captura de eventos en aplicaciones web, por lo que vamos a profundizar en esto:

La Cadena de Prototipos en la API del DOM

Cuando hablamos de que un elemento del DOM es un objeto, nos referimos a que:

  • window: Es el objeto global en el navegador que contiene a document.
  • document: Es una propiedad de window que representa el documento HTML y es el punto de entrada al DOM. Esto significa que window.document es una instancia de Document
  • Nodos y Elementos: Cada elemento en el DOM es un objeto que forma parte de una jerarquía de prototipos. Por ejemplo, si tienes un <div>, este es una instancia de HTMLDivElement, que hereda de HTMLElement, que a su vez hereda de Element, luego de Node, EventTarget y finalmente de Object. Esto significa que todos los métodos y propiedades comunes (como appendChild, removeChild, etc.) se definen en los prototipos y son accesibles a cada instancia. Cuando seleccionas un elemento con document.getElementById, simplemente recuperas la referencia al objeto que ya fue instanciado al renderizar la página. No se crea una nueva instancia; se utiliza la existente que forma parte de la estructura del DOM. Así, el DOM y el objeto window junto con su respectivo objeto document están profundamente integrados a través de la cadena de prototipos.

Cuando utilizas un método como document.getElementById('miImagen'), el navegador devuelve la referencia a un objeto que ya fue creado durante el renderizado del HTML. Este objeto es, por ejemplo, una instancia de HTMLImageElement.

HTMLImageElement (o HTMLDivElement, etc.)
   └── HTMLElement
         └── Element
               └── Node
                     └── EventTarget
                           └── Object

En este ejemplo, cada objeto hereda de su prototipo. Por ejemplo, si llamas a img.getAttribute('src'), el método getAttribute se encuentra en el prototipo de Element. Esto es posible gracias a la cadena de herencia, lo que permite que todos los elementos tengan acceso a funcionalidades comunes sin necesidad de que cada uno defina sus propios métodos.

Siguiendo con el ejemplo de seleccionar una imágen, este elemento tendría la siguiente herencia de prototipos simplificada comenzando por el elemento seleccionado y sus ancestros hasta Object:

  1. HTMLImageElement (o cualquier otro elemento específico como HTMLDivElement, HTMLParagraphElement, etc.)

  2. Es la interfaz que representa elementos <img> en el DOM.

    • Atributos y Propiedades Específicas:
      • src: URL de la imagen.
      • alt: Texto alternativo.
      • height / width: Altura y anchura de la imagen.
      • naturalHeight / naturalWidth: Dimensiones originales de la imagen.
      • complete: Indica si la imagen ha terminado de cargarse.
      • crossOrigin, decoding, loading, etc.: Propiedades específicas para controlar el comportamiento de carga y la seguridad.
    • Métodos Especiales: No añade métodos propios adicionales a los heredados, pero su existencia como HTMLImageElement permite que el navegador trate las imágenes de manera optimizada.
  3. HTMLElement

  4. Representa elementos HTML genéricos (como <div>, <p>, <span>, etc.). Es una extensión de Element y agrega funcionalidades específicas de la presentación y comportamiento en la web.

    • Propiedades y Atributos Específicos:
      • style: Objeto CSSStyleDeclaration para manipular estilos en línea.
      • title: Texto de información adicional.
      • accessKey: Permite definir una tecla de acceso rápido.
      • dataset: Permite acceder a atributos de datos (data-*).
      • contentEditable, dir, lang, hidden, tabIndex, etc.: Otros atributos que controlan la presentación y comportamiento del elemento.
    • Métodos Específicos: focus(), blur(), click(): Para interactuar con el elemento de manera programática.
  5. Element

  6. Es la interfaz base para todos los elementos de HTML y XML en el DOM.

    • Propiedades y Atributos Comunes:
      • id: Identificador único del elemento.
      • className: La o las clases CSS asignadas.
      • tagName: Nombre de la etiqueta (por ejemplo, "DIV", "IMG").
      • innerHTML, outerHTML: Contenido HTML interno y externo.
    • Métodos Comunes: getAttribute(), setAttribute(), removeAttribute(): Para gestionar atributos. querySelector(), querySelectorAll(): Para buscar descendientes que coincidan con selectores CSS. closest(), matches(): Para buscar elementos relacionados en la jerarquía del DOM.
  7. Node

  8. Es la interfaz base de la cual heredan todos los nodos en el DOM, incluidos elementos, nodos de texto y comentarios.

    • Propiedades:
      • nodeName, nodeType, nodeValue: Información básica sobre el nodo.
      • childNodes: Colección de nodos hijos (incluye nodos de texto).
      • firstChild, lastChild, nextSibling, previousSibling: Para navegar entre nodos (hay que tener en cuenta que estos pueden ser nodos de texto).
      • firstElementChild, lastElementChild, nextElementSibling, previousElementSibling: Versión filtrada que solo incluye elementos (sin nodos de texto).
    • Métodos: appendChild(), insertBefore(), removeChild(), replaceChild(), cloneNode(), normalize(), etc.: Para manipular la estructura del DOM.
  9. EventTarget

  10. Es la interfaz que proporciona la capacidad de registrar y gestionar listeners de eventos. Prácticamente, todos los elementos del DOM (y el objeto document, window) son instancias de EventTarget.

    • Métodos Clave: addEventListener(type, listener, [options]): Registra un listener para un evento. removeEventListener(type, listener, [options]): Remueve un listener. dispatchEvent(event): Despacha un evento, activando todos los listeners asociados.
  11. Object

  12. La raíz de la cadena de prototipos en JavaScript. Todos los objetos, incluidos los del DOM, heredan de Object.prototype.

    • Métodos Comunes: toString(), valueOf(), hasOwnProperty(), isPrototypeOf(), propertyIsEnumerable(), etc.: Métodos generales para todos los objetos.

Manipulación de Elementos del DOM

En JavaScript, el DOM (Document Object Model) es la representación estructural de la página web y permite interactuar de forma dinámica con los elementos HTML. Mediante métodos de selección, modificación y manipulación, podemos acceder y cambiar el contenido, atributos o estructura de los elementos.

  • Seleccionar elementos:
    • document.getElementById
    • document.getElementsByTagName
    • document.getElementsByClassName
    • document.querySelector
    • document.querySelectorAll
  • Modificar contenido y atributos:
    • innerText
    • innerHTML
    • setAttribute.
  • Añadir y eliminar elementos:
    • document.createElement
    • document.createTextNode
    • appendChild
    • removeChild.

Veamos cada una en detalle para comprender con ejemplos:

Seleccionar elementos

document.getElementById

Este método devuelve el elemento del DOM cuyo id coincide con el proporcionado. Es ideal para acceder de forma directa a un único elemento.

document.getElementsByTagName

Devuelve una colección de elementos que comparten la misma etiqueta HTML. Es útil para obtener todos los elementos de un tipo específico.

document.getElementsByClassName

Retorna una colección de elementos que tienen la clase CSS especificada, permitiendo seleccionar múltiples elementos a la vez.

document.querySelector

Permite seleccionar el primer elemento que coincide con el selector CSS dado, ofreciendo una sintaxis muy flexible.

document.querySelectorAll

Devuelve una NodeList de todos los elementos que coinciden con el selector CSS, lo que facilita iterar sobre ellos.

Modificar contenido y atributos

innerText / innerHTML

Estos atributos permiten modificar el contenido textual (innerText) o el contenido HTML (innerHTML) de un elemento. Con innerHTML puedes incluso insertar etiquetas HTML.

setAttribute

Permite establecer o modificar un atributo de un elemento, lo que es útil para actualizar propiedades o agregar datos personalizados.

Añadir y eliminar elementos

document.createElement / document.createTextNode

Estos métodos se usan para crear nuevos nodos de elementos y nodos de texto, respectivamente. Son la base para generar contenido dinámico.

Presionalo muchas veces! veras que se cambia el backgroundColor del primero y el ultimo y los demas permanecen en blanco porque se va restableciendo el color.

appendChild / removeChild

appendChild añade un nodo como hijo de un elemento, mientras que removeChild elimina un nodo hijo específico. Son esenciales para modificar la estructura del DOM.

Presionalo muchas veces! tienes 5 segundos antes de que se borren el primero y el último elemento, pero se cambia el backgroundColor del primero y el ultimo y sus hermanos respectivamente.


Vídeo de Ricardo López Arriaga Bueno Clase 9 JavaScript 3 parte 2/2 addEventListener@rickylobu en YouTube

Manejo de Eventos con addEventListener en JavaScript

Esperando que ya esté completamente claro, todo objeto que sea un EventTarget (como nodos, elementos, document, window, etc.) tiene acceso a addEventListener sin necesidad de redefinirlo.

Conceptos Básicos:

  • Trigger (Desencadenante): Es la acción o estímulo que inicia un evento. Por ejemplo, un clic, el envío de un formulario o el movimiento del ratón son triggers que pueden generar eventos en el DOM.
  • Evento: Es la notificación de que el trigger ha ocurrido, generando un objeto Event por addEventListener. Cada evento tiene propiedades específicas que describen qué sucedió y en qué elemento mediante type y target.
  • Listener/Handler: Función callback que se ejecuta en respuesta al evento.
  • EventListener (Oyente de Eventos): Mecanismo para registrar un listener en un elemento del DOM mediante la función addEventListener.

addEventListener es un método de EventTarget que permite asociar un evento a partir de un trigger o desencadenante (como un clic, desplazamiento, envío de formulario, etc.) a un elemento específico del Document Object Model (DOM) asignandole una función "Callback".
Qué en este contexto o paradigma es la función cuya responsabiidad es estar a la "escucha" (listener) y "controlar" o ejecutar la respuesta al evento que puede ser diversa (handler).
Es decir, este método escucha el evento y ejecuta una función especificada (listener/handler) cuando el evento ocurre.

Parámetros y Funcionamiento:

La firma del método es:

	element.addEventListener(type, listener[, options]);
  • type: Cadena que representa el tipo de evento (ej. "click", "mouseover", "keydown").
  • listener: Función callback que se ejecuta cuando ocurre el evento. Recibe un objeto Event.
  • options: Parámetro opcional que puede ser un booleano que indica la fase en que se ejecutará, o un objeto con propiedades avanzadas (capture, once, passive, signal).

Ejemplo Básico:

	elementoSeleccionadoDelDOM.addEventListener('click', (event) => {
			console.log('Se hizo clic en', event.target);
		});

El Objeto Event: Atributos y Métodos Clave

Cuando ocurre un evento, el navegador crea un objeto Event que contiene información importante sobre el evento y es mandado como argumento a la función (listener/handler). Algunos de sus atributos y métodos más relevantes son:

Atributos:

  • event.type: Tipo de evento (por ejemplo, "click").
  • event.target: Elemento que disparó el evento.
  • event.currentTarget: Elemento al que se asignó el listener, útil para delegación de eventos.
  • event.eventPhase: Indica la fase del evento (captura, target o burbujeo).
  • event.bubbles: Booleano que indica si el evento se propaga en la fase de burbujeo.
  • event.cancelable: Indica si se puede cancelar el comportamiento por defecto.
  • event.defaultPrevented: Booleano que muestra si se ha llamado a preventDefault().
  • event.timeStamp: El tiempo transcurrido desde que carga la página hasta que se creó el evento.

Métodos:

  • preventDefault(): Evita el comportamiento por defecto del evento.
  • stopPropagation(): Detiene la propagación del evento a otros elementos en el DOM.
  • stopImmediatePropagation(): Detiene la propagación del evento y evita que se ejecuten otros listeners en el mismo elemento.

Propagación de Eventos en el DOM:

Los eventos se propagan en tres fases:

Fase de Captura:

  • El evento se inicia en el objeto window y desciende por el árbol del DOM hasta el elemento objetivo.
  • Los listeners registrados con capture: true se ejecutan en esta fase.
  • Permite a los elementos ancestros interceptar el evento antes de que llegue al elemento que lo disparó.
  • Control de la Propagación: Puedes detener la propagación en cualquier fase utilizando stopPropagation() o stopImmediatePropagation() en el objeto Event.

Fase de Target:

El evento llega al elemento objetivo y se ejecutan los listeners registrados en ese elemento, sin importar si fueron configurados con capture o no.

Fase de Burbujeo:

  • El evento se propaga hacia arriba, desde el elemento objetivo hasta el objeto window.
  • Los listeners registrados sin la opción capture (o con capture: false) se ejecutan durante esta fase.
  • Control de la Propagación: Puedes detener la propagación en cualquier fase utilizando stopPropagation() o stopImmediatePropagation() en el objeto Event.

Ejemplo de Propagación de eventos en el DOM:

Presta atención al siguiente ejemplo de listeners en fase de captura, target y burbujeo para comprender por completo la propagación de eventos en el DOM


Ejemplo de Delegación de Eventos:

En lugar de asignar múltiples onclick a cada elemento, se asigna un único listener a un contenedor padre para manejar los clics en sus elementos hijos.

En este ejemplo usamos delegación de eventos para validar un formulario con tres campos: correo (con @ y .), contraseña (mayor a 6 caracterés) y edad (mayor a 18). En lugar de asignar un listener a cada campo o botón, asignamos un único listener al contenedor padre (en este caso, el propio formulario) que se encarga de manejar el evento de envío. Según la validación, se resalta el campo en error (con una clase CSS) y se muestra un mensaje error o de éxito.


Eventos principales con addEventListener en JavaScript

Los eventos en JavaScript permiten responder a la interacción del usuario con la interfaz. A continuación se describen algunos de los eventos más comunes:

  1. Click

  2. El evento click se dispara cuando se hace clic en un elemento.

  3. Doble Click (dblclick)

  4. El evento dblclick se dispara cuando se hace doble clic en un elemento.

  5. Mouse Over

  6. El evento mouseover se dispara cuando el puntero del ratón se mueve sobre un elemento.

  7. Mouse Out

  8. El evento mouseout se dispara cuando el puntero del ratón se mueve fuera de un elemento.

  9. Mouse Down

  10. El evento mousedown se dispara cuando se presiona un botón del ratón sobre un elemento.

  11. Mouse Up

  12. El evento mouseup se dispara cuando se suelta un botón del ratón sobre un elemento.

  13. Mouse Move

  14. El evento mousemove se dispara cuando se mueve el ratón dentro de un elemento.

  15. Key Down

  16. El evento keydown se dispara cuando se presiona una tecla.

  17. Key Up

  18. El evento keyup se dispara cuando se suelta una tecla.

  19. Submit

  20. El evento submit se dispara cuando se envía un formulario.

  21. Focus

  22. El evento focus se dispara cuando un elemento gana el foco.

  23. Blur

  24. El evento blur se dispara cuando un elemento pierde el foco.

  25. Change

  26. El evento change se dispara cuando el valor de un elemento cambia.

  27. Input

  28. El evento input se dispara cuando el valor de un elemento de entrada cambia.

  29. Load

  30. El evento load se dispara cuando un recurso y sus recursos dependientes han terminado de cargar.

  31. DOMContentLoaded

  32. El evento DOMContentLoaded se dispara cuando el documento HTML ha sido completamente cargado y parseado.

  33. Resize

  34. El evento resize se dispara cuando se cambia el tamaño de la ventana del navegador.

  35. Scroll

  36. El evento scroll se dispara cuando se desplaza la barra de desplazamiento de un elemento.


Opciones Avanzadas en addEventListener:

Después del primer parámetro “type” y el segundo “Handler” el tercer parámetro (opcional) puede ser un objeto de configuración:

  • capture: (Booleano) que define si el listener se ejecutará en la fase de captura (true) o de burbujeo (false, por defecto).
  • once: Si es true, el listener se elimina automáticamente después de su primera ejecución.
  • passive: Si es true, indica que el listener no llamará a preventDefault(), lo que puede mejorar el rendimiento en ciertos eventos (por ejemplo, en touchstart o wheel).
  • signal: Permite cancelar el listener mediante un objeto AbortController.

Estas opciones se pueden combinar para crear listeners muy específicos y optimizados para casos de uso avanzados.

Ejemplo Avanzado de addEventListener:

Crearemos un div con un listener avanzado que reciba un objeto con las opciones:

  • capture: (Boolean) en true por lo que se ejecutará en la fase de captura.
  • once: (Boolean) en true por lo que el listener se eliminará automáticamente tras su primera ejecución.
  • passive: (Boolean) en true por lo que el listener no llamará a preventDefault().
  • signal: Permite cancelar el listener mediante un objeto AbortController, en este caso lo ocuparemos para cancelar el listener tras 10 segundos.

Haz clic en el botón para crear un div con un listener avanzado con estas opciones.


Casos de Uso en la Fase de Captura y Burbujeo

Fase de Captura:

  1. Validar formularios antes de que los datos se envíen.
  2. Verificar permisos de acceso al hacer clic en enlaces.
  3. Registrar interacciones de usuario para análisis global.
  4. Gestionar eventos de focus en formularios.
  5. Capturar eventos personalizados a nivel de contenedor.
  6. Controlar accesos antes de ejecutar acciones específicas.
  7. Modificar comportamientos predeterminados de elementos de navegación.
  8. Iniciar operaciones de arrastrar y soltar.
  9. Gestionar eventos en componentes de forma centralizada.
  10. Mostrar tooltips de forma anticipada.

Fase de Burbujeo:

  1. Delegación de eventos en listas o menús.
  2. Actualización de interfaces en respuesta a interacciones.
  3. Gestión centralizada de clics en contenedores.
  4. Ejecución de validaciones de datos tras la interacción del usuario.
  5. Registro de estadísticas de clics en elementos individuales.
  6. Manejo de eventos en componentes anidados.
  7. Optimización de listeners mediante delegación.
  8. Capturar acciones en formularios para actualizar el estado.
  9. Responder a eventos de navegación sin interceptar el trigger original.
  10. Actualizar la UI en respuesta a eventos provenientes de elementos secundarios.

Recuerda que en ambos casos, puedes detener la propagación del evento usando stopPropagation() y stopImmediatePropagation() para controlar cómo y dónde se ejecutan los listeners.


Vídeo de Ricardo López Arriaga Bueno Clase 10 JavaScript 4 parte 1/2 Asincronismo, XMLHttpRequest y más@rickylobu en YouTube

Fundamentos de Asincronismo en JavaScript

El asincronismo en JavaScript es un componente esencial que permite la ejecución de tareas en segundo plano sin bloquear el hilo principal. Este enfoque no solo mejora la eficiencia y la capacidad de respuesta de las aplicaciones web, sino que también optimiza la experiencia del usuario al permitir que múltiples operaciones ocurran simultáneamente.

El Event Loop y el Funcionamiento Interno de V8 en los Navegadores

El motor JavaScript V8, utilizado en navegadores como Google Chrome, es responsable de la ejecución de código JavaScript de manera eficiente. En el corazón de este funcionamiento está el Event Loop, una estructura que gestiona la ejecución de código, la recogida de eventos y las tareas asincrónicas. El Event Loop asegura que las operaciones se realicen de manera ordenada y no bloquee el hilo principal, permitiendo a JavaScript mantener su modelo de ejecución en un solo hilo sin sacrificar la capacidad de respuesta y eficiencia.

JavaScript y su Modelo de Hilo Único

Recordando la definición de JavaScript (JS) en MDN, lo describen como un lenguaje de programación ligero, interpretado, o compilado justo-a-tiempo (just-in-time) con funciones de primera clase. Si bien es más conocido como un lenguaje de scripting (secuencias de comandos) para páginas web, y es usado en muchos entornos fuera del navegador, tal como Node.js, Apache CouchDB y Adobe Acrobat JavaScript es un lenguaje de programación basada en prototipos, multiparadigma, de un solo hilo, dinámico, con soporte para programación orientada a objetos, imperativa y declarativa (por ejemplo programación funcional).

Implicaciones de tener un Solo Hilo en JavaScript

Ejecución Síncrona:

JavaScript utiliza un único call stack en el que se apilan las funciones a medida que se invocan y se desapilan al finalizar.

  1. Ventajas:
    1. Simplicidad y previsibilidad: No se deben gestionar condiciones de carrera propias de múltiples hilos y es más fácil el mantenimiento de código al no tener que preocuparse por concurrencia.
    2. Modelo de programación basado en eventos: JavaScript utiliza un modelo de programación basado en eventos y un bucle de eventos. Esto permite que el lenguaje maneje múltiples tareas de manera eficiente, sin la necesidad de múltiples hilos. Cuando una tarea es asincrónica (como una solicitud a un servidor), JavaScript puede continuar ejecutando otras tareas mientras espera la respuesta, sin bloquear el hilo principal.
  2. Limitación:
    1. Riesgo de bloqueo: El subproceso principal es el que usa el navegador para controlar los eventos del usuario, representar y pintar la pantalla, y para ejecutar la mayor parte del código que comprende una página web o aplicación típica. Debido a que todas estas cosas suceden en un único subproceso; el "hilo principal", un sitio web lento o un script de aplicación ralentiza todo el navegador; Peor aún, si el script de un sitio o aplicación entra en un bucle infinito, todo el navegador se bloqueará. Esto da como resultado una experiencia de usuario frustrante, lenta (o peor).
Implicaciones y Mitigación de Bloqueos

Delegación Asíncrona: Para evitar bloqueos, JavaScript delega operaciones costosas (como temporizadores, peticiones de red o manipulación de eventos) a mecanismos asíncronos que son gestionados por las Web APIs del navegador o por Node.js.

Resultado: Aunque el lenguaje se ejecuta en un solo hilo, estas operaciones se realizan en “segundo plano” y solo se “devuelven” al hilo principal cuando han terminado, evitando congelamientos de la interfaz.


El Event Loop: Definición y Funcionamiento

El Event Loop es un bucle infinito que, una vez que el call stack se encuentra vacío, revisa las colas de tareas y mueve las funciones pendientes al call stack para su ejecución.

Funcionamiento Básico:

  1. Ejecuta el código síncrono (se llena y vacía el call stack).
  2. Una vez vacío, el Event Loop transfiere primero todas las tareas de la Microtask Queue al call stack.
  3. Cuando la Microtask Queue está vacía, transfiere la siguiente tarea de la Task Queue (o cola de macrotareas).

Estructura del Event Loop

  1. Call Stack: Área donde se ejecuta el código sincrónico.
  2. Microtask Queue: Prioritaria respecto a la Task Queue. Aquí se encolan los callbacks de promesas (métodos .then(), .catch(), .finally()), así como los resultados de queueMicrotask().
  3. Task Queue (Cola de Tareas/Macrotareas): Donde se encolan los callbacks de funciones asíncronas como las de setTimeout, setInterval, y eventos del DOM. Se procesan en orden FIFO cuando el call stack y la Microtask Queue están vacíos.
  • Memory Heap: Aunque no es parte del Event Loop per se, es fundamental: es el espacio en memoria donde se almacenan objetos y variables. V8 gestiona este heap de forma eficiente mediante un recolector de basura.

Ejemplo Práctico


	console.log('Inicio');
	
	setTimeout(() => {
	console.log('Callback de setTimeout');
	}, 0);
	
	Promise.resolve().then(() => {
	console.log('Callback de la Promesa');
	});
	
	console.log('Fin');
				  

Flujo:

  1. Ejecución síncrona:
    • Se imprime "Inicio".
    • Se registra el callback de setTimeout en la Task Queue (con un retardo mínimo).
    • Se encola el callback de la promesa en la Microtask Queue.
    • Se imprime "Fin".
  2. Procesamiento del Event Loop:
    • Al quedar vacío el call stack, se vacía primero la Microtask Queue: se imprime "Callback de la Promesa".
    • Luego, se transfiere el callback de la Task Queue al call stack: se imprime "Callback de setTimeout".

Resultado final en consola: Inicio → Fin → Callback de la Promesa → Callback de setTimeout.


Operaciones Asíncronas y Delegación a las Web APIs

Operaciones Comunes

  • Temporizadores: setTimeout y setInterval registran sus callbacks en la Task Queue tras cumplir su retardo mínimo.
  • Eventos del DOM: Las interacciones del usuario (clicks, movimientos, etc.) se encolan de manera similar, permitiendo la respuesta a eventos sin bloquear el hilo principal.
  • Operaciones de I/O: Peticiones de red (por ejemplo, mediante fetch o XMLHttpRequest) se delegan a Web APIs que trabajan en segundo plano.

Delegación a las Web APIs

¿Cómo Funciona la delegación a APIs del EventLoop?:

Al detectar una operación asíncrona, el motor JavaScript delega la tarea a la API nativa del navegador (o Node.js).

Ejemplo: Al invocar fetch(), el navegador lanza una petición HTTP en un hilo o proceso aparte, sin bloquear el call stack. Una vez que la respuesta llega, el callback se encola (usualmente en la Microtask Queue si se usa con promesas).

Recursos y Subprocesos:

Aunque JavaScript es de un solo hilo, las Web APIs pueden utilizar hilos o mecanismos de concurrencia propios del entorno para gestionar estas operaciones sin afectar el hilo principal. Una vez completada la tarea, se “devuelve” la respuesta al Event Loop para que se ejecute la función correspondiente.


Flujo Completo de Ejecución en V8

Inicio de la Ejecución

  1. Parsing y Compilación:

    V8 analiza el código, genera un Árbol de Sintaxis Abstracta (AST) y lo compila a bytecode.

  2. Call Stack:

    El código síncrono se ejecuta en el call stack, donde se agregan y eliminan marcos de funciones.

  3. Memory Heap:

    Los objetos y variables se asignan en el heap, gestionado por V8 con técnicas de recolección de basura.

Delegación de Tareas Asíncronas

  • Al encontrar operaciones asíncronas, V8 delega estas tareas a las Web APIs del navegador (como temporizadores, peticiones HTTP, o eventos del DOM).
  • Estas APIs funcionan en hilos o procesos externos al hilo principal, de modo que no bloquean el call stack.

Colas y el Event Loop

  1. Microtask Queue:

    Prioritaria para tareas derivadas de promesas y queueMicrotask().

  2. Task Queue (Macrotareas):

    Se encolan callbacks de temporizadores, eventos y operaciones de I/O.

  3. Ciclo del Event Loop:

    Mientras el call stack esté vacío, el Event Loop primero procesa todas las microtareas y luego, si no hay ninguna pendiente, transfiere la siguiente tarea de la Task Queue.

Ejecución de un Escenario Completo

  • Paso 1: El código síncrono se ejecuta y se agregan las operaciones asíncronas.
  • Paso 2: Las Web APIs gestionan la operación asíncrona (por ejemplo, una petición con fetch()).
  • Paso 3: Al completarse, la respuesta se envía al Event Loop; su callback se encola en la Microtask Queue (si es una promesa) o en la Task Queue.
  • Paso 4: El Event Loop, al detectar un call stack vacío, procesa primero la Microtask Queue, pasando su cola de tareas FIFO al Call Stack y retornando el control al hilo principal para su ejecución.
  • Paso 5: El Event Loop, al detectar un call stack vacío y ver que la Microtask Queue también está vacío, procesa la Task Queue, pasando su cola de tareas FIFO al Call Stack y retornando el control al hilo principal para su ejecución.

Manejo de Eventos y Tareas Pesadas

  • Eventos del DOM: Se encolan en la Task Queue y se procesan cuando el call stack está libre.
  • Tareas Pesadas: Para evitar bloquear el hilo principal, se pueden dividir en fragmentos mediante técnicas como "chunking" o delegar a Web Workers.
╭☞( ͡ ͡° ͜ ʖ ͡͡°)╭☞ Ver ejemplo de chunking

Ejemplo de Chunking: Dividir una tarea pesada en trozos para no bloquear el hilo principal.
En este ejemplo, se realiza una operación matemática intensiva que utiliza Math.PI, Math.E y Math.random() para simular una carga pesada. La tarea se divide en múltiples "chunks" utilizando setTimeout, lo que permite que el Event Loop ejecute otras tareas entre cada chunk y mantenga la UI responsiva.


// Función que simula una operación pesada dividiéndola en chunks
function heavyComputation(totalIterations, chunkSize) {
	let sum = 0;
	let i = 0;
	
	// Función interna que procesa un chunk de iteraciones
	function processChunk() {
	// Calcula el límite para este chunk
	const end = Math.min(i + chunkSize, totalIterations);
	
	// Procesa el chunk realizando operaciones matemáticas intensivas
	for (; i < end; i++) {
		// Realiza una operación matemática utilizando Math.PI, Math.E y Math.random()
		const value = Math.sin(Math.PI * Math.random()) * Math.cos(Math.E * Math.random());
		sum += value;
	}
	
	console.log(`Iteraciones completadas: ${i} / ${totalIterations}`);
	
	// Si no hemos completado todas las iteraciones, agenda el siguiente chunk
	if (i < totalIterations) {
		setTimeout(processChunk, 0);
	} else {
		// Al finalizar, muestra el resultado final
		console.log("Cálculo pesado completado. Suma final:", sum);
	}
	}
	
	// Inicia el procesamiento de chunks
	processChunk();
}

// Ejecuta la tarea pesada con 10 millones de iteraciones en chunks de 100,000
heavyComputation(10000000, 100000);

Casos Comunes en Desarrollo Web de Delegación Asíncrona

  • Con XMLHttpRequest (XHR) (Año: 1999):
    • Utilizado tradicionalmente para peticiones asíncronas. Su callback se encola en la Task Queue al completarse la respuesta.
    • Se realiza la petición y el navegador la gestiona en segundo plano.
    • Cuando llega la respuesta, el callback se añade a la Task Queue.
  • Con Promesas y Fetch (Año: 2015):
    • Al utilizar fetch(), se retorna una promesa.
    • Al resolverse, el callback asociado se agrega a la Microtask Queue, garantizando su ejecución tan pronto como termine el código síncrono.
  • Con Async/Await (2017) y Top-Level Await (2022):
    • Sintaxis que permite escribir código asíncrono de forma “síncrona”.
    • Bajo el capó, el código después del await se encola como microtarea.
    • Sintaxis moderna para trabajar con operaciones asíncronas. Permite escribir código más legible, encapsulando la asincronía en bloques try/catch y transformando el código después de await en microtareas.

Tecnologías Adicionales

  • Web Workers: Permiten ejecutar scripts en hilos paralelos, útiles para tareas CPU-intensivas sin afectar la UI.
  • WebSockets: Proveen comunicación bidireccional en tiempo real. Sus mensajes se manejan de forma asíncrona y se integran al flujo del Event Loop mediante callbacks.
  • Otras APIs del Navegador: Como Service Workers o APIs de notificaciones, que también delegan operaciones asíncronas al entorno nativo.

Resumen del Flujo

  1. Ejecución Síncrona: Código en el call stack y asignación en el Memory Heap.
  2. Delegación Asíncrona: Tareas como temporizadores, peticiones HTTP y eventos son delegadas a las Web APIs.
  3. Procesamiento de Colas: El Event Loop gestiona la Microtask Queue (promesas, async/await) con prioridad, y luego la Task Queue (setTimeout, eventos).
  4. Actualización de UI: Una vez procesadas las tareas, el navegador actualiza la interfaz de usuario.

Buenas Prácticas

  • Evitar operaciones síncronas pesadas.
  • Utilizar promesas y async/await para un código más legible y manejable.
  • Delegar operaciones costosas a Web Workers cuando sea posible.
  • Implementar un manejo robusto de errores en operaciones asíncronas (con .catch() y bloques try/catch).

Contextualización Histórica

Brendan Eich creó JavaScript en 1995 para Netscape con el objetivo de permitir interactividad mediante callbacks y manejo de eventos, con un enfoque en la simplicidad que no incluía un sistema de colas complejas. Operaciones con tiempo, Eventos, y XHR utilizan una cola posterior al Call Stack del hilo principal. El modelo de tareas con distinción clara entre Task Queue y Microtask Queue evolucionó más tarde, a medida que el lenguaje y su uso para el desarrollo de aplicaciones Web crecieron en complejidad y robustez.

Evolución: El concepto del Event Loop y la delegación a Web APIs se han ido estandarizando y refinando a través de ECMAScript y la W3C. La Microtask Queue se introdujo de manera más formal con la llegada de las promesas y la especificación de ECMAScript 2015 (ES6). Las promesas requirieron un mecanismo para manejar microtareas de forma más eficiente y predecible. Por lo tanto, la Microtask Queue se convirtió en una parte integral del event loop, permitiendo que las promesas y otras operaciones asíncronas como solicitudes HTTP basadas en promesas se ejecuten de manera más rápida y ordenada. Al ejecutarse este tipo de operaciones, la clasica función getData(), (cuya responsablidad es obtener los datos necesarios, procesar los datos recibidos para ser información útil y funcional), logramos que esta función se ejecute y tenga lista la información, antes de que se ejecuten eventos y actualizaciones de UI.


HTTP

¿Qué es HTTP?
HTTP (HyperText Transfer Protocol) es el protocolo base que permite la comunicación entre clientes y servidores en la World Wide Web. Se utiliza para intercambiar información, como documentos HTML, imágenes, vídeos y datos estructurados, a través de una red. HTTP es un protocolo sin estado, lo que significa que cada solicitud se trata de manera independiente sin conocimiento previo de las transacciones anteriores.

Estructura Completa de una Solicitud HTTP

Una solicitud HTTP consta de varias partes:

  • Línea de solicitud (Request Line):
    • Método HTTP: Indica la acción a realizar (por ejemplo, GET, POST, PUT, DELETE).
    • URL o URI: Especifica el recurso al que se desea acceder.
    • Versión del Protocolo: Generalmente HTTP/1.1 o HTTP/2.
  • Cabeceras (Headers):
    • Host: El dominio del servidor.
    • User-Agent: Información sobre el cliente (navegador).
    • Accept: Tipos de contenido que el cliente puede manejar.
    • Content-Type: Tipo de contenido enviado en el cuerpo (en solicitudes POST o PUT).
    • Authorization: Información para autenticación, si es necesaria.
  • Línea en blanco: Separa las cabeceras del cuerpo de la solicitud.
  • Cuerpo (Body): Es opcional y se utiliza principalmente en solicitudes POST, PUT y DELETE. Contiene los datos que se envían al servidor (por ejemplo, datos de formulario o JSON).

Ejemplo JSON de body de solicitud HTTP, (en una petición POST, PUT, DELETE):

{
"nombre": "Juan",
"edad": 30
}

Métodos HTTP indispensables

  • GET: Solicita la representación de un recurso. Se utiliza para obtener datos sin modificar el estado del servidor.
  • POST: Envía datos al servidor para crear un nuevo recurso. El cuerpo de la solicitud contiene la información que se desea enviar.
  • PUT: Envía datos al servidor para actualizar un recurso existente. Se utiliza cuando se quiere reemplazar por completo el recurso.
  • DELETE: Solicita la eliminación de un recurso específico.

Estados de la Solicitud y Códigos de Respuesta HTTP

Estados de la petición (readyState en XHR)

  • 0 (UNSENT): Objeto XHR creado, pero no inicializado.
  • 1 (OPENED): open() ha sido llamado.
  • 2 (HEADERS_RECEIVED): Se han recibido los encabezados de la respuesta.
  • 3 (LOADING): La respuesta está en proceso de descarga.
  • 4 (DONE): La operación se completó.

Códigos de respuesta HTTP

  • 1xx (Informativos):
    • 100 continue: Indica que el servidor ha recibido la parte inicial de la solicitud y le dice al usuario que puede continuar con el resto de la misma.
    • 101 Switching Protocols: Indica que el servidor está cambiando el protocolo de comunicación en función de una solicitud del cliente (como cambiar de HTTP/1.1 a WebSockets).
    • 102 Processing: Esto le dice al usuario que el servidor ha aceptado la solicitud pero aún está trabajando en procesarla.
  • 2xx (Éxito):
    • 200 OK: La solicitud se completó exitosamente.
    • 201 Created: Un recurso fue creado como resultado de la solicitud.
  • 3xx (Redirección):
    • 301 Moved Permanently: El recurso se ha movido de forma permanente a una nueva URL.
    • 302 Found: Redirección temporal.
  • 4xx (Errores del Cliente):
    • 400 Bad Request: La solicitud no se puede procesar por error del cliente.
    • 401 Unauthorized: Requiere autenticación.
    • 404 Not Found: El recurso no fue encontrado.
  • 5xx (Errores del Servidor):
    • 500 Internal Server Error: Error genérico del servidor.
    • 503 Service Unavailable: El servidor no puede procesar la solicitud actualmente.

XMLHttpRequest (XHR) (Año: 1999)

XMLHttpRequest (XHR) fue desarrollado inicialmente por Microsoft y presentado en Internet Explorer 5 en 1999 como parte de la tecnología ActiveX. Su finalidad era permitir la comunicación asíncrona entre el navegador y el servidor, lo que marcó el comienzo del desarrollo de aplicaciones web interactivas.

Con el tiempo, XHR fue estandarizado y se integró en el ecosistema de JavaScript. Fue adoptado por otros navegadores y eventualmente se incluyó en las especificaciones de ECMAScript, consolidándose como una herramienta clave para el desarrollo web asíncrono.

Funcionamiento de XMLHttpRequest

El objeto XHR permite enviar solicitudes HTTP y recibir respuestas de forma asíncrona. Su funcionamiento se basa en dos componentes principales: el objeto Request (solicitud) y el objeto Response (respuesta).

Objeto Request (Solicitud)

  1. Inicialización:

    Se crea una instancia de XMLHttpRequest.
    const xhr = new XMLHttpRequest();

  2. Método open():

    Configura la solicitud especificando el método (GET, POST, PUT, DELETE), la URL y si la solicitud debe ser asíncrona.
    xhr.open("GET", "https://api.ejemplo.com/data", true);

  3. Configuración de cabeceras:

    Se pueden añadir cabeceras personalizadas utilizando el método setRequestHeader.
    xhr.setRequestHeader("Content-Type", "application/json");

  4. Envío de la solicitud:

    Con send() se inicia la petición. Si se envían datos (en POST o PUT), se incluyen como parámetro.
    xhr.send();

Objeto Response (Respuesta)

  • readyState y status:

    La propiedad readyState indica el estado de la solicitud, mientras que status proporciona el código de estado HTTP de la respuesta.

  • responseText y responseXML:

    Dependiendo del tipo de datos, se puede acceder a la respuesta como texto o XML.
    responseText: Contiene la respuesta en formato de texto.
    responseXML: Contiene la respuesta en formato XML (si aplica).

Me parece importante mencionar que además de onreadystatechange, existen otros eventos útiles como onload (cuando la solicitud se completa exitosamente), onerror (cuando ocurre un error), y ontimeout (si la solicitud excede el tiempo límite).

Timeout: Se puede establecer un tiempo límite para la solicitud mediante la propiedad timeout y gestionar el evento ontimeout.

xhr.timeout = 5000; // 5 segundos
xhr.ontimeout = function() {
console.error("La solicitud excedió el tiempo límite.");
};

AJAX (Asynchronous JavaScript and XML)

AJAX es más una técnica o patrón de desarrollo que una tecnología en sí misma. Su principal objetivo es permitir que una página web actualice solo las partes necesarias sin necesidad de recargarla completamente. Esto se logra mediante la combinación de:

  • JavaScript: Para enviar y manejar las solicitudes.
  • XMLHttpRequest (XHR): La API que permite realizar solicitudes HTTP de forma asíncrona.
  • XML (y hoy en día JSON): Formatos de datos para intercambiar información entre el cliente y el servidor.

El término AJAX se popularizó alrededor del año 2005, impulsado por aplicaciones web innovadoras como Gmail y Google Maps, que demostraron cómo era posible hacer interfaces más dinámicas y responsivas. Aunque originalmente se usaba XML, actualmente es común utilizar JSON por su mayor simplicidad y eficiencia.

En resumen, AJAX encaja como la técnica que utiliza XHR (y ahora también Fetch) para hacer solicitudes asíncronas, permitiendo actualizar dinámicamente partes de la página web sin recargarla por completo.

AJAX utiliza XHR para realizar peticiones al servidor en segundo plano. Cuando el servidor responde, JavaScript puede actualizar dinámicamente partes de la página web sin necesidad de recargarla completamente.

Con la evolución hacia APIs modernas, AJAX se ha enriquecido mediante el uso de promesas, Fetch, async/await y Top-Level await, facilitando la actualización dinámica de la UI con mayor fluidez y eficiencia gracias al EventLoop que encola las CallBack de promesas en la Microtask Queue garantizando que se ejecuten antes que la Task Queue con los eventos para actualizar el DOM.


JSON

JSON (JavaScript Object Notation) es un formato de texto ligero para el intercambio de datos. Se basa en la sintaxis de los objetos de JavaScript, lo que lo hace fácil de leer y escribir tanto para humanos como para máquinas. Actualmente, es el formato de datos preferido en el desarrollo web por su simplicidad, interoperabilidad y amplio soporte en diversos lenguajes de programación.

Ventajas de JSON

  • Ligero y legible: Su estructura simple lo hace fácil de entender.
  • Facilidad de integración: La mayoría de los lenguajes tienen funciones o librerías nativas para parsear y serializar JSON.
  • Interoperabilidad: Es ideal para el intercambio de datos entre el cliente y el servidor.

Procesamiento de JSON en JavaScript

JSON se basa en la sintaxis de los objetos literales de JavaScript, lo que permite utilizar funciones nativas como JSON.parse() para convertir una cadena JSON en un objeto y JSON.stringify() para convertir un objeto en una cadena.

Parseo y Serialización

  • Parseo: Convertir una cadena JSON en un objeto JavaScript.
  • const jsonString = '{ "nombre": "Juan", "edad": 30 }';
    const obj = JSON.parse(jsonString);
    console.log(obj.nombre); // "Juan"
  • Serialización: Convertir un objeto JavaScript en una cadena JSON.
  • const user = { nombre: "Ana", edad: 25 };
    const jsonStr = JSON.stringify(user);
    console.log(jsonStr); // { "nombre": "Ana", "edad": 25 }

Estructura y Sintaxis de JSON

  • Objetos: Conjunto de pares clave-valor, delimitados por llaves { }.
  • Arreglos: Listas ordenadas de valores, delimitados por corchetes [ ].
╭☞( ͡ ͡° ͜ ʖ ͡͡°)╭☞ Ejemplo JSON: Representación de un Partido de Fútbol

Imaginemos que queremos representar toda la información relevante de un partido de fútbol: detalles del partido, equipos, jugadores, marcador y eventos significativos (goles, tarjetas).


{
"matchId": 12345,
"date": "2025-03-07T18:30:00Z",
"stadium": "Estadio Nacional",
"referee": "Carlos Pérez",
"teams": {
	"home": {
	"name": "Los Leones",
	"coach": "Miguel Hernández",
	"players": [
		{ "number": 1, "name": "Juan Martínez", "position": "Portero" },
		{ "number": 4, "name": "Pedro Gómez", "position": "Defensa" },
		{ "number": 8, "name": "Luis Ramírez", "position": "Centrocampista" },
		{ "number": 10, "name": "Andrés López", "position": "Delantero" }
	]
	},
	"away": {
	"name": "Los Tigres",
	"coach": "Ricardo Silva",
	"players": [
		{ "number": 1, "name": "Carlos Ruiz", "position": "Portero" },
		{ "number": 5, "name": "Diego Fernández", "position": "Defensa" },
		{ "number": 7, "name": "José Castro", "position": "Centrocampista" },
		{ "number": 9, "name": "Fernando Díaz", "position": "Delantero" }
	]
	}
},
"score": {
	"home": 2,
	"away": 1
},
"events": [
	{
	"minute": 15,
	"type": "goal",
	"team": "home",
	"player": "Andrés López",
	"description": "Gol desde fuera del área."
	},
	{
	"minute": 35,
	"type": "yellow_card",
	"team": "away",
	"player": "Diego Fernández",
	"description": "Falta dura."
	},
	{
	"minute": 60,
	"type": "goal",
	"team": "home",
	"player": "Luis Ramírez",
	"description": "Cabeceo perfecto."
	},
	{
	"minute": 78,
	"type": "goal",
	"team": "away",
	"player": "Fernando Díaz",
	"description": "Remate cruzado."
	}
]
}
				  

En este ejemplo, se modela la información de un partido incluyendo datos generales, equipos, jugadores, marcador y eventos, utilizando la estructura de objetos y arreglos propia de JSON.

Ejemplo: Comparación de Porteros

Te invito a que a partir del JSON que representa un partido de fútbol, obtener un resultado con la cantidad de goles que recibió el portero del equipo local (porteroA) y del equipo visitante (porteroB).

Utiliza la API JSON para parsear los datos y mostrar en pantalla:

  • Al porteroA (del equipo local) le metieron X goles a favor del equipo visitante.
  • Al porteroB (del equipo visitante) le metieron X goles a favor del equipo local.
  • El ganador es: el equipo que anotó más goles.

Ejemplos de XMLHttpRequest (XHR) con jsonplaceholder

En este ejemplo utilizaremos XMLHttpRequest para probar los métodos GET, POST, PUT y DELETE utilizando la URL correspondiente al método por ejemplo GET con: https://jsonplaceholder.typicode.com/todos/1 cada botón ejecutará la operación con la URL correspondiente.
Puedes ver la documentación de la API en: JSONPlaceholder API

  • GET: https://jsonplaceholder.typicode.com/todos/1
  • POST: https://jsonplaceholder.typicode.com/posts
  • PUT: https://jsonplaceholder.typicode.com/posts/1
  • DELETE: https://jsonplaceholder.typicode.com/posts/1

Vídeo de Ricardo López Arriaga Bueno Clase 11 JavaScript 4 parte 2/2 Asincronismo actual: promesas, fetch, async/await y Top level Await@rickylobu en YouTube

Promesas "Promise" (Año: 2015 ES6)

Las promesas son objetos que pueden nunca ejecutarse y quedarse en estado pendiente, pero fueron diseñadas para representar la eventual finalización (Exitosa o fallida) de una operación asíncrona y el manejo de su valor resultante.
Surgieron como solución al conocido "callback hell" o infierno de callbacks, donde la anidación excesiva de funciones callback dificultaba la legibilidad y el mantenimiento del código.

¿Qué son las promesas?

Las promesas permiten encadenar operaciones asíncronas mediante el método .then(), facilitando así la lectura del flujo de ejecución de manera secuencial. Además, se centraliza el manejo de errores utilizando .catch(), evitando la dispersión de múltiples callbacks de error en diferentes niveles de anidación.

Estados de una promesa

Una promesa puede estar en uno de estos tres estados:

  • Pendiente (pending): Estado inicial, en el que la operación aún no ha concluido.
  • Resuelta o Cumplida (fulfilled): La operación se completó exitosamente y la promesa se resuelve con un valor.
  • Rechazada (rejected): La operación falló y la promesa se rechaza con un error o motivo.

Parámetros y métodos fundamentales

Una Promise es un objeto cuyo constructor recibe una función, conocida como executor, que a su vez recibe dos funciones: resolve y reject.

  • resolve: Función que se invoca para indicar que la operación se completó exitosamente, pasando el valor resultante.
  • reject: Función que se invoca para indicar que la operación falló, pasando el error o motivo.
const miPromesa = new Promise((resolve, reject) => {
// Aquí va el código asíncrono
let exito = true; // Simula el resultado de una operación asíncrona

if (exito) {
	resolve('La operación fue exitosa');
} else {
	reject('La operación falló');
}
});

miPromesa.then((mensaje) => {
console.log(mensaje); // 'La operación fue exitosa'
}).catch((error) => {
console.error(error); // 'La operación falló'
});

En este ejemplo, resolve se llama si la operación es exitosa, y reject se llama si la operación falla. .then() maneja el caso exitoso y .catch() maneja el error.

Manejo de errores y encadenamiento

En JavaScript, el método .then() se utiliza para manejar promesas cumplidas y rechazadas. Existen dos variantes:

then(onFulfilled): Se ejecuta cuando la promesa se cumple. onFulfilled es una función que recibe el valor de la promesa cumplida.

El resultado de la promesa se suele trabajar de dos formas diferentes:

Con then(onFulfilled, onRejected): Similar al anterior, pero además incluye onRejected, una función que se ejecuta cuando la promesa es rechazada. La similitud con la promesa original new Promise((resolve, reject) => {}) radica en que onFulfilled actúa como resolve y onRejected como reject.

Ejemplo:

const p1 = new Promise((resolve, reject) => {
resolve("Success!");
// o
// reject(new Error("Error!")); // dependiendo del caso de uso
});

p1.then(
(value) => {
	console.log(value); // Success!
},
(reason) => {
	console.error(reason); // Error!
}
);


Separando el .then, .catch y .finally

.catch(): Es una forma de manejar errores similar a then(undefined, onRejected). Se utiliza para atrapar cualquier error que se produzca en cualquier parte del proceso asíncrono.

p1.catch((reason) => {
	console.error(reason); // Error!
	});

.finally(): Se ejecuta al final de la cadena de promesas, independientemente de si la promesa se cumplió o fue rechazada. Es útil para realizar tareas de limpieza.

p1.finally(() => {
	console.log("Operación completada, éxito o fallo.");
	});

Ejemplo:

miPromesa
.then((valor) => {
	console.log('Promesa cumplida con:', valor);
})
.catch((error) => {
	console.error('Error capturado:', error);
})
.finally(() => {
	console.log('Operación finalizada.');
});

Por lo tanto:

  • .then(onFulfilled, onRejected?): Permite especificar funciones callback para manejar el valor cuando la promesa se cumple o para manejar errores (opcionalmente).
  • .catch(onRejected): Se utiliza para capturar errores en la cadena de promesas, centralizando el manejo de excepciones.
  • .finally(onFinally): El método .finally() es especialmente útil para ejecutar código de limpieza o actualizar la interfaz, ya que se ejecuta independientemente del resultado de la promesa. Esto es crucial en escenarios en los que se necesita liberar recursos o notificar al usuario que la operación asíncrona ha terminado.

Al encadenar múltiples .then(), cada uno devuelve una nueva promesa, lo que permite realizar operaciones secuenciales sin necesidad de utilizar múltiples callbacks anidados, lo cual mejora la legibilidad y mantenibilidad del código.

Promesas y el Event Loop

Las promesas se integran al flujo de ejecución de JavaScript mediante la Microtask Queue. Cuando una promesa se resuelve o rechaza, el callback especificado en .then(), .catch() o .finally() se coloca en dicha cola. Antes de que el Event Loop procese la Task Queue, vacía la Microtask Queue, garantizando que las operaciones basadas en promesas se ejecuten de manera prioritaria y tan pronto como el call stack esté libre.

Métodos avanzados de las Promesas

Existen otros métodos estáticos en el objeto Promise que permiten trabajar con múltiples promesas:

  • Promise.all(iterable): Recibe un iterable de promesas y devuelve una nueva promesa que se resuelve cuando todas las promesas se han resuelto, o se rechaza si alguna de ellas falla.
  • Promise.race(iterable): Devuelve una promesa que se resuelve o rechaza tan pronto como la primera promesa del iterable se resuelva o se rechace.
  • Promise.allSettled(iterable): Devuelve una promesa que se resuelve después de que todas las promesas se han cumplido o rechazado, con un array de resultados que indica el estado de cada promesa.
  • Promise.any(iterable): Devuelve una promesa que se resuelve tan pronto como cualquier promesa del iterable se resuelva; si todas son rechazadas, se rechaza con un AggregateError.

// Ejemplo de Promise.all
const promesas = [
Promise.resolve(1),
Promise.resolve(2),
Promise.resolve(3)
];

Promise.all(promesas)
.then(resultados => {
	console.log('Resultados:', resultados); // [1, 2, 3]
})
.catch(error => {
	console.error('Error en Promise.all:', error);
});
	

¡Presta atención!

Nota que las promesas son para trabajar con operaciones asincronas pero no necesariamente de red mediante solicitudes HTTP, la API Fetch utiliza promesas para trabajar específicamente solicitudes HTTP con promesas asegurando trabajar con la Microtask Queue en lugar de la Task Queue como XHR.

Tanto las promesas como la API fetch son de 2015 (ES6) y naturalmente en lo que más se utilizan las promesas es por debajo de la Fetch API, pero me parece necesario saber trabajar con promesas para saber gestionar el EventLoop con la Microtask Queue y la Task Queue en conjunto dentro del asincronismo en JavaScript. Independientemente de si es para trabajar con solicitudes HTTP, para lo cual esta la API fetch.

Las promesas revolucionaron el manejo de operaciones asíncronas en JavaScript al ofrecer un flujo secuencial y más legible en comparación con los callbacks anidados. Considera estudiar en profundidad métodos como Promise.all, Promise.race, Promise.allSettled y Promise.any, estos métodos amplían las posibilidades para trabajar con múltiples operaciones asíncronas de manera simultánea.


Fetch API (2015 ES6)

La Fetch API es una interfaz moderna que permite realizar peticiones HTTP de manera sencilla, limpia y basada en promesas. Está diseñada para reemplazar el tradicional objeto XMLHttpRequest (XHR), ofreciendo una sintaxis más legible y una integración nativa con las nuevas características de JavaScript, como las Promise y async/await. Esto se traduce en un código menos anidado (evitando el "callback hell") y en un manejo de errores más centralizado.

Ventajas frente a XHR

  • Sintaxis clara y moderna: La API utiliza promesas, lo que permite encadenar operaciones asíncronas con .then(), .catch() y .finally().
  • Mejor manejo de errores: La promesa devuelta por fetch() se rechaza solo en caso de errores de red o de CORS, lo que obliga a verificar el estado de la respuesta (usando response.ok).
    1. Errores de red: Esto ocurre cuando hay un problema al intentar conectarse con el servidor. En estos casos, la promesa devuelta por `fetch()` será rechazada con un objeto de error del tipo `TypeError`.
      • El servidor está inaccesible (fuera de línea).
      • No tienes conexión a Internet.
      • El DNS no puede resolver el nombre de dominio del servidor.
      • Hay un error de tiempo de espera (timeout).
    2. Errores de CORS (Cross-Origin Resource Sharing): Si se intenta realizar una solicitud que infringe las políticas de seguridad de CORS, el navegador bloquea la solicitud antes de que se envíen datos al servidor, y la promesa será rechazada como un `TypeError`.
      • Cuando el servidor no incluye los encabezados correctos para permitir la comunicación desde un origen diferente.
      • Si estás haciendo una solicitud desde un sitio a otro dominio y no hay permisos explícitos establecidos.
    Importante: Si la solicitud HTTP se completa exitosamente pero la respuesta es un código de error HTTP (como 404 o 500), la promesa no se rechaza. En este caso, la promesa se resuelve correctamente, pero debes verificar la propiedad ok del objeto de respuesta para determinar si la solicitud fue exitosa.
  • Mayor flexibilidad: Proporciona objetos Request y Response, que permiten configurar con precisión la petición y manipular la respuesta de forma detallada.
  • Integración con async/await: Permite escribir código asíncrono de forma más natural y legible.

Fetch API y window.fetch

La función fetch() se utiliza para iniciar una petición HTTP. Recibe como parámetro obligatorio la URL del recurso y, opcionalmente, un objeto de configuración.

Objetos Request y Response

Objeto Request

El objeto Request encapsula toda la información relativa a la petición HTTP:

  • URL y Método: Especifica a qué recurso se accede y qué método HTTP se utiliza (GET, POST, PUT, DELETE, etc.).
  • Headers: Permite definir cabeceras personalizadas como Content-Type, Authorization, entre otras. La interfaz Headers ofrece métodos para agregar, modificar o consultar los encabezados.
  • Body: Para métodos como POST o PUT, se puede enviar un cuerpo que contenga datos (por ejemplo, en formato JSON o FormData).
  • Configuración avanzada:
    • mode: Controla el comportamiento de la solicitud en cuanto a seguridad y CORS (cors, no-cors, same-origin).
    • credentials: Indica si se deben enviar cookies junto a la petición (omit, same-origin, include).
    • cache: Define estrategias de caché (default, no-cache, reload, etc.).
    • Abort Signal: Mediante el uso de AbortController, es posible cancelar una petición en curso.

Ejemplo de creación explícita:

const request = new Request('https://api.example.com/data', {
	method: 'POST',
	headers: {
		'Content-Type': 'application/json'
	},
	body: JSON.stringify({ nombre: 'Ana', edad: 25 })
});

Objeto Response

El objeto Response representa la respuesta HTTP obtenida y ofrece múltiples utilidades:

  • Estado y Validación:
    • status: Código HTTP (ej. 200, 404).
    • statusText: Texto descriptivo del estado.
    • ok: Booleano que indica si el status está en el rango 200-299.
  • Procesamiento del cuerpo de la respuesta en diferentes formatos de datos:

    Con el objeto response que devuelve la API Fetch se pueden hacer operaciones comunes como:

    • response.json(): Indica que la respuesta es un JSON. Convierte el cuerpo a un objeto JavaScript, observa que por debajo utiliza JSON.parse().
    • response.text(): Devuelve el cuerpo como texto o HTML.
    • response.blob() y response.arrayBuffer(): Útiles para contenido binario como imágenes, videos, juegos, gráficos, etc.
    • response.formData(): Para respuestas en formato de datos de formulario.
  • Headers: Se puede acceder a los encabezados de la respuesta mediante response.headers.get('Header-Name').
  • Clonación: Como el cuerpo se consume una única vez, se puede clonar la respuesta usando response.clone().

Uso de window.fetch

La función fetch() se utiliza para iniciar una petición HTTP. Recibe como parámetro la URL y, opcionalmente, un objeto de configuración.

Ejemplo GET:

fetch('https://api.example.com/data')
.then(response => {
	if (!response.ok) {
	throw new Error(`Error HTTP: ${response.status}`);
	}
	return response.json();
})
.then(data => {
	console.log('Datos recibidos:', data);
})
.catch(error => {
	console.error('Error en la petición GET:', error);
});

Ejemplo POST:

fetch('https://api.example.com/data', {
method: 'POST',
headers: {
	'Content-Type': 'application/json'
},
body: JSON.stringify({ nombre: 'Juan', edad: 30 })
})
.then(response => {
	if (!response.ok) {
	throw new Error(`Error HTTP: ${response.status}`);
	}
	return response.json();
})
.then(data => {
	console.log('Recurso creado:', data);
})
.catch(error => {
	console.error('Error en la petición POST:', error);
});

Procesamiento de Diferentes Formatos de Datos

La implementación de window de la Fetch API no cambia nada en el procesamiento del cuerpo de la respuesta, se maneja como un ReadableStream y se puede transformar a varios formatos:

  • response.json() para obtener un objeto JavaScript.
  • response.text() para obtener texto plano o HTML.
  • response.blob() o response.arrayBuffer() para contenido binario.
  • response.formData() para trabajar con datos de formularios.

Encadenamiento de Promesas con Fetch y Manejo de Respuestas

La función fetch() devuelve una promesa que se resuelve con un objeto Response. Esto permite encadenar operaciones de transformación y procesamiento de datos utilizando .then() y centralizar el manejo de errores con .catch().


fetch('https://api.example.com/data')
.then(response => {
	if (!response.ok) {
	throw new Error(`Error HTTP: ${response.status}`);
	}
	return response.json(); // Primera transformación
})
.then(jsonData => {
	console.log('JSON recibido:', jsonData);
	// Se puede realizar otra petición 
	// utilizando los datos de la primera
	return fetch('https://api.example.com/another-endpoint');
})
.then(response => response.text())
.then(textData => {
	console.log('Texto recibido:', textData);
})
.catch(error => {
	console.error('Error en la cadena de promesas:', error);
});

Manejo de Headers y Configuraciones Avanzadas

Al enviar una petición con fetch(), se pueden incluir headers personalizados, por ejemplo: Bearer = Portador

fetch('https://api.example.com/data', {
method: 'GET',
headers: {
	'Authorization': 'Bearer miToken123',
	'Accept': 'application/json'
}
});

Asimismo, se puede acceder a los encabezados de la respuesta:

fetch('https://api.example.com/data')
.then(response => {
	console.log('Content-Type de la respuesta:', response.headers.get('Content-Type'));
	return response.json();
})
.then(data => {
	console.log('Datos:', data);
})
.catch(error => {
	console.error('Error en la petición:', error);
});

Notas importantes de la Fetch API y HTTP

La Fetch API representa un avance significativo en el manejo de peticiones HTTP en JavaScript, ofreciendo:

  • Sintaxis moderna y clara para encadenar operaciones asíncronas.
  • Control detallado de la solicitud a través de objetos Request y Response.
  • Procesamiento flexible del cuerpo de la respuesta mediante diversos métodos de transformación.
  • Integración con el Event Loop mediante la Microtask Queue, asegurando una ejecución prioritaria.
  • ☞( ͡ ͡° ͜ ʖ ͡͡°)╭☞ Existen otros métodos HTTP que te podrían ser útiles si los conoces:
    • HEAD: Similar a GET, pero solo solicita los encabezados de respuesta sin el cuerpo. Útil para verificar metadatos de un recurso.
    • OPTIONS: Permite al cliente consultar qué métodos HTTP y encabezados son admitidos por el servidor para un recurso específico.
    • PATCH: Utilizado para realizar actualizaciones parciales a un recurso, en lugar de reemplazarlo por completo como ocurre con PUT.
    • TRACE: Permite al cliente realizar un seguimiento de la ruta que sigue una solicitud a través del servidor. Generalmente no se usa en producción por razones de seguridad.
    • CONNECT: Usado para establecer conexiones de túnel, como cuando se utiliza HTTPS a través de un proxy HTTP.
    • PROPFIND (WebDAV): Utilizado para recuperar propiedades (metadatos) de un recurso. Muy común en extensiones como Web Distributed Authoring and Versioning (WebDAV).
    • PROPPATCH (WebDAV): Similar a PATCH, pero específicamente para modificar las propiedades de un recurso.
    • LOCK y UNLOCK (WebDAV): Permiten bloquear y desbloquear recursos para evitar que múltiples usuarios los modifiquen al mismo tiempo.
    • MKCOL (WebDAV): Crea colecciones (carpetas) en un servidor.
    • SEARCH: Usado en implementaciones específicas para realizar búsquedas en recursos según ciertos criterios.

Async/Await "async/await" (Año: 2017)

Las palabras clave async y await se introdujeron en 2017 (ECMAScript 2017), aproximadamente dos años después de la integración de las promesas y la Fetch API (ECMAScript 2015), entendiendo que Promise es la base sobre la cual el EventLoop gestiona la Microtask Queue y tanto fetch, como async, await y Top-level await (2022) trabajan mediante promesas.

Las palabras reservadas async y await surgieron para trabajar de forma más sencilla con operaciones asíncronas, facilitando la lectura y escritura de código. La palabra async sirve para indicar que una función se va a comportar de manera asíncrona, lo que hace posible que dentro de su ámbito o scope se pueda utilizar await para pausar la ejecución y esperar la resolución de la promesa, logrando un código que se puede leer como “haz esto y espera la respuesta”, “luego, haz esto otro y espera la respuesta”.

1. Función Asíncrona (async)

Al anteponer async a una función, ésta retorna siempre una promesa, incluso si se retorna un valor primitivo.

2. Pausar la Ejecución (await)

Dentro de una función async, await detiene la ejecución hasta que la promesa se resuelva o se rechace, lo que permite escribir código secuencial que se asemeja a código síncrono.

Ejemplo

// Declaración de una función asíncrona
async function someAsyncFunction() {
  console.log('Iniciando la petición...');
  // 'await' pausa la ejecución hasta que la promesa se resuelva
  const respuesta = await someFunction(); // función que requiere asincronismo
  // Procesamos la respuesta como JSON u otro proceso intensivo
  const datos = await respuesta.json();
  console.log('Datos recibidos:', datos);
  return datos;
}

// Llamada a la función asíncrona manejada con promesas
obtenerDatos()
  .then(data => console.log('Operación completada:', data))
  .catch(error => console.error('Error en la petición:', error));

También podríamos hacerlo con async, await y try/catch, lo cual se considera buena práctica al centralizar el manejo de errores:

Manejo de Errores con Async/Await

Uso de try/catch

Captura los errores dentro de una función asíncrona con un bloque try/catch. Se comienza con try { // código que puede fallar } y con catch (error) { // manejo de error }, capturando errores que se produzcan, por ejemplo, al esperar la resolución de promesas. Esto es equivalente a utilizar .catch() en cadenas de promesas, pero de una forma más intuitiva.

// Declaración de una función asíncrona con try/catch
async function someAsyncFunction() {
  try {
    console.log('Iniciando la petición...');
    // 'await' pausa la ejecución hasta que la promesa se resuelva
    const respuesta = await someFunction(); // función que requiere asincronismo

    // Procesamos la respuesta como JSON u otro proceso intensivo
    const datos = await respuesta.json();
    console.log('Datos recibidos:', datos);

    // Retornamos los datos procesados
    return datos;
  } catch (error) {
    // Capturamos errores, ya sea al ejecutar someFunction() o al procesar los datos
    console.error('Error en la función asincrónica someAsyncFunction():', error);
    throw error; // Propaga el error si necesitas manejarlo en otro nivel
  }
}

// Llamada a la función asíncrona
(async function() {
  try {
    const data = await someAsyncFunction();
    console.log('Operación completada:', data);
  } catch (error) {
    console.error('Error al llamar a obtenerDatos():', error);
  }
})();

Con try/catch puedes capturar cualquier error que ocurra durante la ejecución de await, ya sea al resolver la promesa o por excepciones dentro del bloque try.

Uso de finally – Código de Limpieza

Al igual que en el manejo de errores sincrónico, el bloque finally se ejecuta siempre, sin importar si se utilizó el .catch() o si se completó con éxito. Esto es útil para realizar tareas de limpieza o actualizar la interfaz de usuario, y en el back-end para cerrar conexiones a bases de datos.

async function obtenerDatosConLimpieza() {
  try {
    const respuesta = await someAsyncFunction();
    const datos = await respuesta.json();
    console.log('Datos recibidos:', datos);
  } catch (error) {
    console.error('Error:', error);
  } finally {
    console.log('La operación asíncrona ha finalizado.');
  }
}
obtenerDatosConLimpieza();

Por lo tanto, la función que contiene await debe ser declarada como async, lo que hace que siempre retorne una promesa, sin importar si devuelve un valor sincrónico o una promesa. El uso de await detiene la ejecución en ese punto específico hasta que la promesa se resuelva (fulfilled) o se rechace (rejected), sin bloquear el hilo principal. Una vez que se resuelve la promesa, la continuación de la función se coloca en la Microtask Queue y se ejecuta tan pronto como el call stack esté vacío.

El uso de async/await permite escribir código asíncrono en un estilo secuencial y, mediante try/catch, facilita el seguimiento del flujo de ejecución y la detección de errores, en comparación con el encadenamiento de múltiples .then().

Encadenamiento de Promesas con async/await y procesamiento en paralelo

Si llamas a las promesas de forma secuencial con await dentro de un bucle o una cadena:

async function processSequentially() {
  const result1 = await promise1;
  const result2 = await promise2;
}

Cada await espera la resolución de la promesa anterior antes de pasar a la siguiente, Esto genera un procesamiento secuencial.

Ahora, si bien el procesamiento secuencial es bastante útil. Si necesitas disparar las promesas en paralelo y procesar los resultados una vez que todas estén resueltas, puedes usar Promise.all. Recuerda que este método recibe un array de promesas o iterable y devuelve una promesa que se resuelve cuando todas se han cumplido, pero si una es rechazada la promesa de retorno se rechaza descartando todas las demás promesas hayan sido o no cumplidas.

async ()=>{
	try {
		const [usuarios, productos, pedidos] = await Promise.all([
		fetch('/api/usuarios').then(res => res.json()),
		fetch('/api/productos').then(res => res.json()),
		fetch('/api/pedidos').then(res => res.json())
	]);
	console.log('Usuarios:', usuarios);
	} catch (error) {
	console.error('Error en una de las solicitudes:', error);
	}
}

En este caso, las promesas se ejecutan casi al mismo tiempo y su resolución sigue el flujo del Event Loop.


¡Presta atención!

Observa el último ejemplo donde utilizamos async ()=>{}, es importante mencionar que las arrow functions, o funciones flecha no tienen su propio this, sino que lo heredan del contexto en el que fueron creadas. Esto es diferente a las funciones declaradas con function que sí tienen su propio binding dinámico de this, por ello, normalmente se utiliza function. Sin embargo, si entiendes el comportamiento de this y no necesitas este binding, es totalmente válido utilizar arrow functions. También se puede declarar un método async dentro de un objeto, los métodos son funciones basadas en el prototipo Function y tienen su propio binding de this dinámico con function. Por otro lado, si se declaran como myMethod: async ()=>{} arrow functions, this será heredado del contexto donde fueron creadas, por lo que no siempre apuntarán al objeto que las contiene, dependiendo del lugar donde fueron definidas.

Nota que las async/await, al estar basadas en promesas, son para trabajar con operaciones asíncronas, pero no necesariamente de red mediante solicitudes HTTP. La API Fetch utiliza promesas para trabajar específicamente con solicitudes HTTP, asegurando trabajar con la Microtask Queue en lugar de la Task Queue como XHR.

Tanto las promesas como la API fetch son de 2015 (ES6) y async/await es de 2017 (ES8), trabajando perfectamente en conjunto, ya que todo se basa en promesas. Y como mencioné anteriormente, considero fundamental saber trabajar con promesas y async/await junto con bloques try/catch para gestionar el EventLoop con la Microtask Queue y la Task Queue en el asincronismo de JavaScript. Independientemente de si es para trabajar con solicitudes HTTP, para lo cual está la API fetch.

Las promesas revolucionaron el manejo de operaciones asíncronas en JavaScript al ofrecer un flujo secuencial y más legible, mejorando la sintaxis al parecerse al código síncrono mediante el uso de await dentro de funciones async. Considera estudiar en profundidad métodos como Promise.all, Promise.race, Promise.allSettled y Promise.any, ya que amplían las posibilidades para trabajar con múltiples operaciones asíncronas de forma simultánea.


Async/Await y Fetch

La integración de async/await con la Fetch API permite escribir código para realizar peticiones HTTP de forma más natural. Al combinar ambos, se pueden realizar operaciones secuenciales de forma limpia y legible.

Ejemplo de Uso con Fetch

// Función asíncrona que realiza una petición GET utilizando fetch y async/await
async function fetchData() {
  try {
    console.log('Iniciando petición GET...');
    const respuesta = await fetch('https://api.example.com/data');
    if (!respuesta.ok) {
      throw new Error(`Error HTTP: ${respuesta.status}`);
    }
    // Procesa la respuesta como JSON
    const datos = await respuesta.json();
    console.log('Datos recibidos:', datos);
    return datos;
  } catch (error) {
    console.error('Error en fetchData:', error);
    throw error;
  } finally {
    console.log('Finalizó la petición GET.');
  }
}

// Llamada a la función
fetchData()
  .then(data => {
    console.log('Operación completada, datos:', data);
  })
  .catch(error => {
    console.error('Error en la cadena de operaciones:', error);
  });

Encadenamiento de Operaciones Secuenciales

Con async/await se pueden encadenar múltiples operaciones asíncronas de forma secuencial, lo que es especialmente útil cuando se depende de la respuesta de una petición para realizar la siguiente.

async function procesarDatos() {
  try {
    // Primera petición: Obtener datos
    const respuesta1 = await fetch('https://api.example.com/data');
    if (!respuesta1.ok) {
      throw new Error(`Error HTTP: ${respuesta1.status}`);
    }
    const datos = await respuesta1.json();
    console.log('Datos iniciales:', datos);

    // Segunda petición: Enviar datos para procesamiento
    const respuesta2 = await fetch('https://api.example.com/procesar', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ datos })
    });
    if (!respuesta2.ok) {
      throw new Error(`Error HTTP: ${respuesta2.status}`);
    }
    const resultado = await respuesta2.json();
    console.log('Resultado del procesamiento:', resultado);
    
    return resultado;
  } catch (error) {
    console.error('Error en procesarDatos:', error);
  }
}

procesarDatos();

Ejemplo Async/Await con fetch consultando la API de Rick y Morty

En este ejemplo utilizaremos la API de Rick y Morty para hacer una primera solicitud a https://rickandmortyapi.com/api/location/3 para obtener los datos de los personajes que viven en la Citadel of Ricks. Rick and Morty API


Top-Level Await (Año:2022)

¿Qué es Top-Level Await?
Top-Level Await es una característica de ECMAScript 2022 ES(13) que permite utilizar la palabra clave await fuera de las funciones async, es decir, en el nivel superior de tus módulos JavaScript. Anteriormente, para esperar el resultado de una promesa era necesario encapsular el código asíncrono dentro de una función marcada como async o emplear una IIFE (Immediately Invoked Function Expression). Con Top-Level Await, se elimina esta necesidad, lo que resulta en un código más limpio y legible.

// Llamada a la función asíncrona IIFE

/* IIFE (Immediately Invoked Function Expression), 
en español "Expresión de Función Invocada Inmediatamente" */

(async function() {
  try {
    const data = await someAsyncFunction();
    console.log('Operación completada:', data);
  } catch (error) {
    console.error('Error al llamar a obtenerDatos():', error);
  }
})();

¿Cómo Funciona?

  1. Cuando el motor JavaScript encuentra un await en el nivel superior de un módulo, la ejecución del módulo se pausa hasta que la promesa asociada se resuelva.
  2. Durante esta pausa, el motor puede continuar procesando otros módulos o tareas, sin bloquear la ejecución global.
  3. Una vez que la promesa se resuelve (o rechaza), la ejecución del módulo se reanuda y el resultado de la promesa queda disponible para el resto del código.

Casos de Uso Comunes

  • Carga Dinámica de Módulos: Permite esperar la carga de datos esenciales o configuraciones antes de importar otros módulos que dependen de dichos datos.
  • Inicialización de Recursos: Es útil para inicializar recursos asíncronos, como conexiones a bases de datos o clientes de API, antes de ejecutar el resto del código.
  • Recuperación de Datos Iniciales: En aplicaciones web, se puede emplear para obtener datos del servidor antes de renderizar la interfaz de usuario.

Implicaciones y Consideraciones

  • Módulos: Top-Level Await está disponible únicamente en módulos JavaScript (archivos con extensión .mjs o archivos configurados con "type": "module" en el package.json).
  • Rendimiento: Un uso excesivo de Top-Level Await puede ralentizar la ejecución, ya que retrasa la evaluación de otros módulos hasta que se resuelvan las promesas.
  • Manejo de Errores: Es fundamental utilizar bloques try...catch para capturar y gestionar posibles errores durante la resolución de las promesas.
  • Alcance: Las variables declaradas en el nivel superior de un módulo (con var, let o const) mantienen su alcance al módulo, lo que favorece la encapsulación y evita conflictos globales.

Ejemplo Práctico

// module.mjs
try {
// Se espera la resolución de la promesa y se obtienen los datos
const datos = await fetch('https://api.ejemplo.com/datos')
	.then(res => {
	if (!res.ok) {
		throw new Error(`Error HTTP: ${res.status}`);
	}
	return res.json();
	});
console.log('Datos cargados:', datos);

// El resto del código del módulo puede utilizar 'datos'
export function procesarDatos() {
	return datos.map(item => item.valor * 2);
}
} catch (error) {
console.error('Error al cargar datos:', error);
}

En este ejemplo, el módulo espera a que la promesa se resuelva y los datos sean cargados antes de continuar con la ejecución. Así, la variable datos está completamente disponible para el resto del módulo, permitiendo que la función procesarDatos() trabaje con información actualizada.

Beneficios Clave

  • Código más limpio y legible, sin necesidad de envoltorios innecesarios.
  • Simplifica la carga dinámica de módulos y la inicialización de recursos asíncronos.
  • Facilita la recuperación de datos iniciales antes de que se ejecute el resto del código.

Ejemplo Completo de Asincronismo en JavaScript

En este ejemplo se realizará una primera solicitud a la API de Rick and Morty para obtener la ubicación "Citadel of Ricks" en la URL: https://rickandmortyapi.com/api/location/3. A partir de la respuesta, se extraerán los primeros 4 URLs de residentes y se harán 4 solicitudes individuales empleando métodos diferentes:
1. Fetch con promesas (then/catch).
2. Fetch con async/await y try/catch.
3. Fetch con Top-Level Await (simulado dentro de la función, recordando que Top-Level Await se usa a nivel global en módulos con type="module" o archivos .mjs).
4. Solicitud XHR.

Además se integrará un dispatchEvent para demostrar la propagación de eventos en el DOM (la manipulación del DOM se realiza en la Task Queue) y se usará setTimeout para simular tareas delegadas al Event Loop.

Antes de comenzar, Me gustaría explicar que todo el código se ejecuta en el Call Stack, donde se asigna memoria en el Heap (gestión de objetos y variables), y luego se delegan las operaciones asíncronas a las colas de Microtareas (Microtask Queue) y Macrotareas (Task Queue). Una vez que el Call Stack se vacía, el Event Loop procesa primero la Microtask Queue (FIFO) y luego la Task Queue, permitiendo la interacción responsiva con la UI.

En este ejemplo utilizaremos la API de Rick y Morty para hacer una primera solicitud a https://rickandmortyapi.com/api/location/3 para obtener los datos de los primeros 4 personajes que viven en la Citadel of Ricks. Rick and Morty API


Vídeo de Ricardo López Arriaga Bueno Clase 12 JavaScript 5 Pruebas Unitarias.@rickylobu en YouTube

Testing en el Desarrollo de Software

¿Por qué hacer pruebas?

  • Verificación y Calidad: Las pruebas aseguran que cada parte del código funcione según lo esperado, previniendo errores y regresiones.
  • Documentación Viva: Los tests actúan como una forma de documentación que describe el comportamiento esperado de las funciones, facilitando la comprensión del código a nuevos desarrolladores.
  • Facilita el Refactoring: Con una suite de tests robusta, se pueden hacer cambios y mejoras en el código con la confianza de que cualquier error será detectado rápidamente.
  • Integración en CI/CD: Las pruebas automatizadas se integran en pipelines de integración continua, permitiendo feedback inmediato y manteniendo la estabilidad del software en cada ciclo de desarrollo o sprint.


Tipos de Pruebas y su Categorización

1. Pruebas Unitarias

Las pruebas unitarias verifican el comportamiento de la unidad más pequeña del código (funciones, métodos o clases). Son rápidas de ejecutar y se enfocan en un alcance muy específico. Se emplean mocks, stubs y spies para aislar el componente bajo prueba. Se recomienda probar funciones que contengan lógica de negocio, interacciones con el DOM o cálculos complejos.

2. Pruebas de Integración

Estas pruebas evalúan la interacción entre múltiples componentes o módulos, asegurando que se integren correctamente. Se utilizan escenarios reales o simulados para comprobar la comunicación entre distintas partes del sistema.

3. Pruebas End-to-End (E2E) y de Sistema

Simulan el comportamiento del usuario y validan el sistema completo, desde la interfaz hasta la base de datos. Aunque son más lentas y complejas, son fundamentales para la validación final del producto. En entornos de producción se recomienda investigar frameworks como Cypress, Selenium, Protractor o TestCafe.


Estrategias de Testing

En el desarrollo de software, las estrategias de testing son fundamentales para garantizar la calidad, la funcionalidad y la estabilidad del código. A continuación, se describen las principales estrategias:

  • TDD (Test Driven Development): (Desarrollo Guiado por Pruebas)

    Se basa en escribir primero las pruebas antes del código. El flujo típico de TDD sigue estos pasos:

    1. Escribir una prueba mínima que inicialmente falle (porque el código aún no existe).
    2. Escribir el código necesario para que la prueba pase.
    3. Refactorizar el código para hacerlo más eficiente o limpio, asegurándose de que las pruebas sigan pasando.

    Este enfoque asegura que el código esté respaldado por pruebas desde el principio, facilitando la refactorización con confianza y reduciendo defectos en etapas posteriores. TDD se centra en el cumplimiento de los requisitos funcionales de manera estrictamente técnica.

  • BDD (Behavior Driven Development): (Desarrollo Guiado por Comportamiento)

    Se enfoca en el comportamiento esperado del sistema desde la perspectiva del negocio o del usuario final. Se utilizan herramientas como Cucumber o lenguaje semiestructurado (por ejemplo, Gherkin: Given, When, Then) para describir escenarios claros y entendibles por todas las partes interesadas.

    Ejemplo de un escenario en BDD:

    Feature: Suma de números
    Scenario: Sumar dos números
        Given tengo los números 2 y 3
        When los sumo
        Then el resultado debería ser 5

    BDD fomenta la colaboración entre desarrolladores, testers y stakeholders para garantizar que el producto cumpla con los requerimientos esperados.

  • Automatización:

    Consiste en implementar pruebas automatizadas que se ejecutan en cada commit, integración o despliegue, comúnmente a través de pipelines de CI/CD (Integración y Entrega Continua). Esto permite:

    • Detectar errores rápidamente antes de que lleguen a producción.
    • Reducir esfuerzos manuales y optimizar el tiempo.
    • Mantener un sistema confiable y libre de regresiones.

    Las pruebas automatizadas incluyen pruebas unitarias (con TDD), pruebas de integración y pruebas E2E (End-to-End), cubriendo así todo el espectro funcional del sistema.

Cómo se complementan TDD, BDD y Automatización

El flujo ideal combina estas estrategias:

  1. Inicio del ciclo: Se definen los requerimientos funcionales y no funcionales, y se planea la arquitectura del sistema.
  2. Escenarios en BDD: A partir de los requerimientos, se escriben escenarios de comportamiento para detallar cómo debería funcionar el sistema desde el punto de vista del usuario final.
  3. Implementación con TDD: Para cada escenario, se escriben pruebas unitarias específicas y luego el código que haga pasar esas pruebas. Este enfoque asegura que cada unidad del sistema cumpla su propósito técnico.
  4. Automatización: Las pruebas (unitarias, de integración, E2E) se integran en pipelines automatizados, ejecutándose con cada cambio para garantizar estabilidad continua.

Combinando estas estrategias, se logra un desarrollo más colaborativo (BDD), técnicamente sólido (TDD) y confiable (automatización), lo que facilita el mantenimiento, escalabilidad y calidad del producto.

Integración en CI/CD y Metodologías Ágiles:

Continuous Integration and Continuous Delivery/Deployment (CI/CD) Significa Integración Continua y Entrega/Implementación Continua. Estas prácticas buscan optimizar y acelerar el ciclo de vida del desarrollo de software mediante la automatización de la integración, las pruebas y la implementación de los cambios de código.

Integración Continua (CI)

La Integración Continua (CI): consiste en integrar de forma automática y frecuente los cambios de código de varios desarrolladores en un repositorio de código fuente compartido. Este proceso implica herramientas automatizadas que compilan el código recién confirmado, ejecutan pruebas unitarias y realizan revisiones de código. Los objetivos principales de la CI son identificar y corregir errores rápidamente, facilitar la integración del código, mejorar la calidad del software y reducir el tiempo necesario para lanzar nuevas funcionalidades.

Pipelines de CI: Herramientas como Jenkins, CircleCI, Travis CI, Bamboo o los propios pipelines de GitLab/Azure DevOps ejecutan la suite de pruebas (unitarias, integración y en algunos casos E2E) en cada commit o pull request.

Pruebas de Commit: Los tests de TDD/TDB se ejecutan en cada commit para detectar errores tempranos y garantizar que las nuevas implementaciones no rompan la funcionalidad existente.

Entrega Continua (CD):

La Entrega Continua (CD): amplía la CI al automatizar el proceso de lanzamiento, garantizando que los cambios en el código se prueben en un entorno similar al de producción antes del despliegue. Esta práctica ayuda a evitar sorpresas de última hora y garantiza que el código siempre esté listo para su implementación. La CD implica ejecutar pruebas de integración y regresión en un entorno de pruebas, lo que hace que el proceso de lanzamiento final sea más fluido y confiable.

Despliegue Continuo: (Automatiza el despliegue en producción tras pasar todas las pruebas). En entornos de Staging y Producción, una vez que las pruebas en CI se aprueban, el código se despliega en entornos de staging para validaciones más completas (incluyendo pruebas de integración y E2E) y, finalmente, en producción.

¿Qué pruebas llegan a producción?
  • Pruebas en el Pipeline: Generalmente, las pruebas unitarias, de integración y E2E se ejecutan en los entornos de CI/CD y staging.
  • Smoke Tests y Sanity Checks: En producción se pueden ejecutar pruebas de humo (smoke tests) o chequeos de sanidad que validen rápidamente que la aplicación está operativa, pero las pruebas completas se realizan antes del despliegue.
  • Excepción: No se “despliegan” pruebas en producción, sino que se utilizan para validar que el build es estable antes del despliegue. Con esto me refiero a que No se despliega la suite completa de pruebas en producción.
    Los tests se usan en las fases de desarrollo, integración y staging para garantizar la estabilidad del build. En producción se pueden ejecutar pruebas de humo o sanity checks para asegurarse de que el sistema está operativo, pero las pruebas detalladas (unitarias, integración y E2E) se ejecutan en entornos controlados antes del despliegue.

Pruebas en el Proceso CI/CD: En entornos de desarrollo frontend con JavaScript se utilizan herramientas como Jasmine, Mocha/Chai, Jest o QUnit para validar el comportamiento de las funciones y la interacción del DOM (usando mocks, stubs y spies).Mientras que en el Back-End, dependiendo del lenguaje, se pueden usar frameworks específicos (por ejemplo, JUnit en Java o NUnit en .NET). El objetivo es validar que los diferentes módulos o componentes se integren correctamente mediante herramientas como (Postman, Newman o frameworks de pruebas integradas en el entorno de desarrollo).


Beneficios del Testing para Equipos y Proyectos

¡Recuerda porqué es importante que codifiques tus test: pruebas unitarias y en su momento de integración y E2E!

  • Documentación y Transferencia de Conocimiento: Los tests actúan como una “documentación viva” que describe cómo se espera que funcione cada parte del código, facilitando la incorporación de nuevos desarrolladores.
  • Confianza en el Código: Contar con pruebas unitarias e integradas brinda seguridad para realizar cambios y refactorizaciones sin temor a romper funcionalidades.
  • Calidad Continua: La integración de tests en pipelines CI/CD garantiza una calidad constante y la detección temprana de problemas, lo que se traduce en un software más estable y confiable.

Pruebas unitarias en JavaScript

Nosotros abordaremos en detalle las pruebas unitarias que es lo correspondiente a nuestro tema de estudio: “Fundamentos de Desarrollo Web”. Considero que para ser un desarrollador Web o “front-end developer” es fundamental saber hacer pruebas unitarias de tu código de forma competente y profesional, por lo que profundizaremos en las pruebas unitarias en JavaScript y tu responsabilidad es aprender lo correspondiente al “Testing “ en el futuro de tu carrera profesional.

Conceptos Básicos: Mocks, Stubs y Spies

Stubs: Son funciones o métodos que reemplazan una función o dependencia real para devolver resultados fijos. Su principal objetivo es aislar la función en prueba, controlando el comportamiento de sus dependencias. Así, se garantiza que, ante ciertos inputs, la función retorne un valor fijo, facilitando la validación del comportamiento, sin depender de la lógica real de la dependencia, sin la complejidad completa de un mock.

Spies: Son envoltorios (wrappers) Su función principal es envolver ( "espiar") o interceptar llamadas a funciones reales. Registran información (número de invocaciones, argumentos, contexto, etc.) y, en algunos casos, también el resultado devuelto. sin modificar el comportamiento original, lo que ayuda a verificar que la función se comporte como una función pura en el sentido de que sus interacciones sean predecibles y observables. Por ejemplo, los spies son útiles para verificar que se hayan llamado funciones específicas durante la ejecución de una prueba.

Mocks: Un mock es un objeto que en muchos casos, combina las funcionalidades de stubs y spies. Es decir, un mock no solo devuelve valores predefinidos (como un stub) sino que también registra las interacciones (como un spy). Además, los mocks se configuran con expectativas específicas: pueden validar que se hayan llamado con ciertos parámetros, que se hayan producido las interacciones en un orden determinado o que se hayan lanzado errores esperados en situaciones particulares. Si esas expectativas no se cumplen, la prueba falla.

Son objetos simulados que imitan el comportamiento de objetos reales. Además de definir respuestas preestablecidas, registran cómo fueron llamados para luego verificar interacciones. Suelen usarse para validar que una función haya llamado a otro componente de forma correcta.

¿Por qué y Cuándo Usar Mocks?

  • Aislamiento de la Prueba: Permiten que la prueba se ejecute sin depender del comportamiento de una dependencia externa.
  • Verificación de Interacciones: Registran información sobre las llamadas realizadas, facilitando la validación del flujo de interacción entre módulos.
  • Control del Comportamiento: Permiten simular escenarios difíciles de reproducir con la dependencia real, como errores de red.
  • Documentación del Contrato: Actúan como una documentación viva que especifica cómo se espera que interactúe un módulo con sus dependencias.

Ejemplos Prácticos

Analicemos primero un ejemplo con JavaScript puro (Vanilla JavaScript).

Ejemplo de Mocks con JavaScript

Podemos crear un mock manual para una función que, por ejemplo, envíe un mensaje. Imaginemos que tenemos un módulo que debe enviar un mensaje y queremos asegurarnos de que se llame correctamente a la función de envío: enviarMensaje() se muestra cómo simular la función y validar que se haya llamado con el mensaje esperado.

// Función real que usaríamos para enviar un mensaje 
//(podría ser una llamada HTTP, etc.)
function enviarMensaje(mensaje) {
console.log("Mensaje enviado:", mensaje);
return true;
}

// Mock de enviarMensaje
function crearMockEnviarMensaje() {
const mock = function(mensaje) {
	mock.llamadas.push(mensaje);
	return true;
};
mock.llamadas = [];
mock.esperarLlamadaCon = function(esperado) {
	if (!mock.llamadas.includes(esperado)) {
	throw new Error(`Se esperaba que se llamara con: ${esperado}`);
	}
};
return mock;
}

// Ejemplo de uso del mock
const mockEnviarMensaje = crearMockEnviarMensaje();
function procesarYEnviar(mensaje) {
// Lógica adicional puede ir aquí
return mockEnviarMensaje(mensaje);
}

procesarYEnviar("Hola mundo");
mockEnviarMensaje.esperarLlamadaCon("Hola mundo");

En este ejemplo se crea un mock para la función enviarMensaje que registra cada llamada en un array (llamadas) y se añade un método esperarLlamadaCon para validar que la función haya sido llamada con un argumento específico.


Guía para Instalar NVM y Node.js

Llegado este punto es necesario instalar Node.js para poder ejecutar las pruebas correctamente. Pero espera!, no instales Node todavía, te recomiendo hacerlo mediante Node Version Manager (NVM)

Node.js es un entorno de ejecución para JavaScript que te permite ejecutar código JavaScript fuera de un navegador, por ejemplo, en servidores. Está basado en el motor V8 de Chrome, lo que lo hace rápido y eficiente. Node.js es usado principalmente para desarrollar aplicaciones backend, pero también puede servir para herramientas de desarrollo frontend.

npm es el gestor de paquetes (Node Package Manager) que viene incluido con Node.js. Es una herramienta que facilita la instalación, gestión y uso de librerías y dependencias (módulos) en tus proyectos.

Con Node.js y npm puedes:

  • Configurar herramientas de desarrollo como Webpack, Babel, Jest, Jasmine etc..
  • Usar frameworks como Angular, React o Vue.
  • Administrar dependencias de manera eficiente en proyectos grandes.
  • Crear servidores backend.

Por ejemplo, si necesitas usar una librería externa como un marco de pruebas, un framework web o cualquier tipo de utilidad, puedes instalarlo desde npm en tu proyecto con unos pocos comandos, incluso puedes publicar tus módulos en el registro de npm.

¿Por qué NVM?

NVM (Node Version Manager) te permite instalar y cambiar fácilmente entre diferentes versiones de Node.js. Esto es útil para proyectos que requieren versiones específicas de Node.js.

NVM es esencial para trabajar con diferentes versiones de Node.js en un mismo sistema. Esto evita conflictos y facilita la gestión de proyectos con dependencias específicas.

La instalación varía ligeramente entre macOS/Linux y Windows:

Instalación de NVM en Windows

  1. Descarga el instalador de NVM para Windows desde el repositorio de GitHub: nvm-windows.
  2. Ejecuta el instalador y sigue las instrucciones.
  3. Abre una nueva terminal como Administrador y verifica la instalación ejecutando:
    nvm --version

Instalación de NVM en macOS y Linux

  1. Abre tu terminal.
  2. Ejecuta el siguiente comando:
    curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash
  3. Cierra y vuelve a abrir tu terminal para que los cambios surtan efecto.
  4. Verifica la instalación ejecutando:
    nvm --version

Instalación de Node.js con NVM

  1. Ejecuta el siguiente comando para instalar la versión LTS (Long Term Support) más reciente de Node.js:
    nvm install --lts
  2. Ejecuta el siguiente comando para usar la versión LTS (Long Term Support) instalada de Node.js:
    nvm use xx.xx.x
  3. Verifica la instalación ejecutando:
    node --version
    npm --version
    1. Si no pudiste ejecutar npm --version. Ver la política actual de ejecución: Get-ExecutionPolicy Esto mostrará la política actual. Probablemente estará configurada en Restricted.
    2. Cambiar la política de ejecución: Para permitir la ejecución de scripts, usa este comando: Set-ExecutionPolicy RemoteSigned Esto permite ejecutar scripts locales sin firmar, pero requiere que los scripts descargados de Internet estén firmados digitalmente.
    3. Confirmar el cambio: Escribe Y (Yes) o S (Si) y presiona Enter cuando se te pida confirmación.
    4. Ejecuta nuevamente: Get-ExecutionPolicy Asegúrate de que ahora diga RemoteSigned.
    5. Probar nuevamente el comando npm: npm --version Ahora debería funcionar correctamente.
  4. Para ver la lista de versiones puedes utilizar nvm list. Si quieres utilizar una versión específica de Node.js, ejecuta:
    nvm use [versión]

Instalación de frameworks de pruebas

Para instalar jest ejecuta el siguiente comando:

npm install --save-dev jest

para instalar Jasmine ejecuta:

npm install --save-dev jasmine

para instalar Mocha y Chai ejecuta:

npm install --save-dev mocha chai

para instalar QUnit ejecuta:

npm install --save-dev qunit

Entendiendo un poco package.json

Imagina que quieres tener Jest y Jasmine en el mismo proyecto. Tu package.json quedaría algo así:

{
"name": "fundametos-de-desarrollo-Web",
	"version": "1.0.0",
	"description": "Pruebas unitarias con Jest y Jasmine para ejemplos del proyecto",
	"type": "module",
	"scripts": {
	"test:jasmine": "jasmine",
	"test:jest": "jest",
	"test": "npm run test:jasmine && npm run test:jest"
	},
	"devDependencies": {
	"jest": "^29.7.0",
	"babel-jest": "^29.7.0",
	"@babel/core": "^7.21.0",
	"@babel/preset-env": "^7.21.0"
	}
}
Significado de package.json:
  • name: Nombre del proyecto (fundametos-desarrollo-Web).
  • version: Versión actual del proyecto (1.0.0).
  • description: Breve descripción del proyecto.
  • scripts:
    • test:jasmine: Ejecuta las pruebas de Jasmine.
    • test:jest: Ejecuta las pruebas de Jest.
    • test: Ejecuta las pruebas de Jasmine y Jest en secuencia.
  • devDependencies: Dependencias necesarias para el desarrollo del proyecto:
    • jasmine: Framework para pruebas unitarias.
    • jest: Otro framework para pruebas unitarias.
  • type: Especifica que el proyecto utiliza módulos ES6.

Con esta configuración, puedes ejecutar:

  • npm run test:jasmine: Para ejecutar las pruebas únicamente con Jasmine.
  • npm run test:jest: Para ejecutar las pruebas únicamente con Jest.
  • npm run test: Para ejecutar todas las pruebas en conjunto.

Configuración de Jest con Babel

Para que Jest pueda interpretar y ejecutar código JavaScript moderno (como ES Modules con import/export), es necesario configurarlo con Babel. Babel transpila la sintaxis moderna a una compatible con Node.js.

¿Por qué es necesario Babel?

  • El entorno predeterminado de Node.js no soporta módulos ES6 de forma completa sin configuración adicional.
  • Jest, por defecto, utiliza CommonJS (require/module.exports), lo cual causa problemas al usar ES6 (import/export).
  • Babel permite transpilar el código moderno para garantizar compatibilidad con Jest.

Pasos para configurar Jest con Babel

  1. Instala las dependencias necesarias:
  2. npm install --save-dev jest babel-jest @babel/core @babel/preset-env
  3. Crea un archivo de configuración de Babel:
  4. En la raíz del proyecto, crea un archivo llamado babel.config.json y agrega lo siguiente:

    {
    "presets": ["@babel/preset-env"]
    }
  5. Crea un archivo de configuración de Jest:
  6. En la raíz del proyecto, crea un archivo llamado jest.config.js y agrega lo siguiente:

    export default {
    transform: {
    	"^.+\\.js$": "babel-jest"
    }
    };
  7. Asegúrate de que package.json esté configurado correctamente:
  8. Asegúrate de incluir "type": "module" si estás utilizando módulos ES6:

    {
    "name": "fundametos-de-desarrollo-Web",
    	"version": "1.0.0",
    	"description": "Pruebas unitarias con Jest y Jasmine para ejemplos del proyecto",
    	"type": "module",
    	"scripts": {
    	"test:jasmine": "jasmine",
    	"test:jest": "jest",
    	"test": "npm run test:jasmine && npm run test:jest"
    	},
    	"devDependencies": {
    	"jest": "^29.7.0",
    	"babel-jest": "^29.7.0",
    	"@babel/core": "^7.21.0",
    	"@babel/preset-env": "^7.21.0"
    	}
    }
  9. Ejecuta las pruebas:
  10. Finalmente, puedes ejecutar tus pruebas con el siguiente comando:

    npm run test

Configurar Jest con Babel es fundamental para trabajar con sintaxis moderna de JavaScript en proyectos que utilicen ES Modules. La integración de Babel asegura que el código sea compatible con Jest y que las pruebas se ejecuten sin problemas.

Estructura del Proyecto

├── tests/
│   ├── jasmine/
│   │   └── example.spec.js
│   ├── jest/
│   │   └── example.test.js
├── package.json
├── babel.config.json
├── jest.config.json
├── app.js
Descripción de la Estructura
  • tests/jasmine/: Carpeta que contiene los archivos de pruebas específicas para Jasmine. Por ejemplo, example.spec.js.
  • tests/jest/: Carpeta que contiene los archivos de pruebas específicas para Jest. Por ejemplo, example.test.js.
  • package.json: Archivo de configuración del proyecto.
  • app.js: Archivo principal del proyecto.

Pruebas unitarias: Ejemplo de configuración con Jest

En tu archivo package.json, agrega un script para ejecutar Jest:

"scripts": { "test": "jest" }

Ejecutar las pruebas

Ejecuta npm run test en la terminal para ejecutar las pruebas. Jest buscará archivos con el sufijo .test.js y ejecutará las pruebas definidas en ellos.

Conceptos clave de Jest

  • describe: Agrupa pruebas relacionadas.
  • it (o test): Define una prueba individual.
  • expect: Crea una afirmación para verificar un valor.
  • toBe: Verifica igualdad estricta.
  • toEqual: Verifica igualdad profunda para objetos y arrays.
  • beforeEach y afterEach: Ejecutan código antes y después de cada prueba.
  • beforeAll y afterAll: Ejecutan código antes y después de todas las pruebas.
  • Los spies, en Jest, se hace con métodos como jest.spyOn() o jest.fn().

Ejemplo con describe y toEqual

Imagina que tu código a evaluar es una suma:

export const sum = (x, y) => x + y;

Su prueba con jest podría quedar algo así:

// sum.test.js
import sum from './js/sum.js';

describe('sum function', () => {
  it('adds two numbers', () => {
    expect(sum(1, 2)).toBe(3);
  });

  it('handles negative numbers', () => {
    expect(sum(-1, 5)).toBe(4);
  });

  it('handles objects', () => {
    expect(sum({ a: 1 }, { b: 2 })).toEqual({ a: 1, b: 2 });
  });
});
  

Podrías utilizar el archivo Funciones.js para hacer una prueba a la función ejecutarFuncionComoParametro para hacer las pruebas unitarias necesarias. Ejecuta npm run test o npm run test:jest en la terminal para ejecutar las pruebas con jest.

¿Qué hace la función?

  1. La función sum toma dos argumentos y devuelve su suma.
  2. prueba con describe y toEqual:
    • describe agrupa las pruebas relacionadas con la función sum.
    • it define casos de prueba individuales.
    • toBe verifica la igualdad estricta para valores primitivos.
    • toEqual verifica la igualdad profunda para objetos.

Pruebas asíncronas con jest

Para probar código asíncrono, como promesas o callbacks, Jest proporciona varias herramientas:

  • async/await: La forma más moderna y legible de probar código asíncrono.
  • done: Una función de callback que indica que una prueba asíncrona ha finalizado.
  • resolves y rejects: Afirmaciones para promesas.

Ejemplo con async/await sustituyendo done

Código a evaluar:

// fetchData.js
export async function fetchData(url) {
  const response = await fetch(url);
  return response.json();
}

Prueba en test/jest/fetchData.test.js

// fetchData.test.js
import fetchData from './js/fetchData';

test('fetches data from an API', async () => {
  const data = await fetchData('https://jsonplaceholder.typicode.com/todos/1');
  expect(data.userId).toBe(1);
  expect(data.id).toBe(1);
});

Puedes crear el archivo fetchData.js y su respectiva prueba fetchData.test.js, con Ctrl + ñ puedes abrir la terminal en VSCode ejecutarla con npm run test o npm run test:jest.

¿Qué hace la función?

  1. La función fetchData realiza una petición HTTP a una API y devuelve los datos en formato JSON.
  2. La prueba con async/await:
    • async/await simplifica la escritura de pruebas para código asíncrono.
    • La prueba espera a que la petición a la API se complete y luego verifica los datos recibidos.
    • expect(data.userId).toBe(1); evalua la prueba con la expectativa de que el JSON convertido en objeto JavaScript contenga la propiedad userId === 1 de forma estricta por lo que no convierte el texto en número
    • Me parece importante mencionar que done también es compatible con Jest en caso de trabajar con callbacks.
      • En pruebas con Jasmine o Jest, done se usa cuando trabajas con operaciones que dependen del Event Loop pero no están basadas en promesas, como:
        • Eventos del DOM.
        • setTimeout o setInterval.
      • Con done, puedes señalar manualmente cuándo debe concluir la prueba al llamar a done() después de que la operación se complete, asegurándote de que el Event Loop haya manejado todas las tareas relevantes.

Ejemplo de Pruebas Unitarias con describe y expect en Jest

En el siguiente ejemplo probaremos el funcionamiento de ejecutarEjemploPOO en EjemploPoo.js junto a sus dependencias en un entorno de POO utilizando Jest. Se realizan mocks de las clases y módulos necesarios (como Vehiculo, Coche, Moto y Utils) para poder verificar que:

  1. Maneje correctamente el error al intentar instanciar la clase abstracta Vehiculo.
  2. Se cree un coche y se llamen a sus métodos (arrancar, frenar y convertir).
  3. Se cree una moto y se llamen a sus métodos (arrancar y frenar).
  4. Se muestre el código generado dinámicamente mediante Utils.mostrarCodigo.

Código a evaluar de (js/POO/EjemploPOO.js):

//importaciones se suelen hacer al principio del documento

	import Vehiculo from './Vehiculo.js';
	import Coche from './Coche.js';
	import Moto from './Moto.js';
	import Utils from '../utils.js';
	
	export default function ejecutarEjemploPOO(){
	try {
		const miVehiculo = new Vehiculo('Genérico', 'Modelo'); // Error: No se puede instanciar una clase abstracta Vehiculo.
	} catch (error) {
		Utils.mostrarResultado('resultado-ejemplo-poo', error.message);
	}
	
	const miCoche = new Coche('Toyota', 'Corolla');
	miCoche.arrancar(); // El coche Toyota Corolla está arrancando.
	miCoche.frenar(); // El coche Toyota Corolla está frenando.
	miCoche.convertir(); // El coche Toyota Corolla se está convirtiendo.
	
	const miMoto = new Moto('Yamaha', 'MT-07');
	miMoto.arrancar(); // La moto Yamaha MT-07 está arrancando.
	miMoto.frenar(); // La moto Yamaha MT-07 está frenando.
	
	// Mostrar código dinámicamente con utils.js ahora que sabemos module.exports y require
	const codigo= `
	======== ARCHIVO interfaces.js ========
	
	// Definición de las "interfaces"
	export const Arrancable = {
		arrancar: function() {
			throw new Error("Método 'arrancar()' debe ser implementado.");
		}
	};
	
	export const Frenable = {
		frenar: function() {
			throw new Error("Método 'frenar()' debe ser implementado.");
		}
	};
	
	export const Convertible = {
		convertir: function() {
			throw new Error("Método 'convertir()' debe ser implementado.");
		}
	};
	
	
	======== ARCHIVO Vehiculo.js ========
	
	export default class Vehiculo {
		constructor(marca, modelo) {
			if (this.constructor === Vehiculo) {
				throw new Error("No se puede instanciar una clase abstracta Vehiculo.");
			}
			this.marca = marca;
			this.modelo = modelo;
		}
	
		verificarImplementacion(interfaces) {
			interfaces.forEach(interfaz => {
				for (let method in interfaz) {
					if (typeof this[method] !== 'function') {
						throw new Error(\`La clase \${this.constructor.name} debe implementar el método \${method}\`);
					}
				}
			});
		}
	}
	
	======== ARCHIVO Coche.js ========
	
	import Vehiculo from './Vehiculo.js';
	import { Arrancable, Frenable, Convertible } from './interfaces/interfaces.js';
	import Utils from '../utils.js';
	
	export default class Coche extends Vehiculo {
		constructor(marca, modelo) {
			super(marca, modelo);
			this.verificarImplementacion([Arrancable, Frenable, Convertible]);
		}
	
		arrancar() {
			Utils.mostrarResultado('resultado-ejemplo-poo', \`El coche \${this.marca} \${this.modelo} está arrancando.\`);
		}
	
		frenar() {
			Utils.mostrarResultado('resultado-ejemplo-poo', \`El coche \${this.marca} \${this.modelo} está frenando.\`);
		}
	
		convertir() {
			Utils.mostrarResultado('resultado-ejemplo-poo', \`El coche \${this.marca} \${this.modelo} se está convirtiendo.\`);
		}
	}
	
	======== ARCHIVO Moto.js =========
	
	import Vehiculo from './Vehiculo.js';
	import { Arrancable, Frenable } from './interfaces/interfaces.js';
	import Utils from '../utils.js';
	
	export default class Moto extends Vehiculo {
		constructor(marca, modelo) {
			super(marca, modelo);
			this.verificarImplementacion([Arrancable, Frenable]);
		}
	
		arrancar() {
			Utils.mostrarResultado('resultado-ejemplo-poo', \`La moto \${this.marca} \${this.modelo} está arrancando.\`);
		}
	
		frenar() {
			Utils.mostrarResultado('resultado-ejemplo-poo', \`La moto \${this.marca} \${this.modelo} está frenando.\`);
		}
	}
	\
	
	
	======== ARCHIVO EjemploPOO.js ========
	
	import Coche from './Coche.js';
	import Moto from './Moto.js';
	import Utils from '../utils.js';
	
	export default function ejecutarEjemploPOO(){
	try {
		const miVehiculo = new Vehiculo('Genérico', 'Modelo'); // Error: No se puede instanciar una clase abstracta Vehiculo.
	} catch (error) {
		Utils.mostrarResultado('resultado-ejemplo-poo', error.message);
	}
	
	const miCoche = new Coche('Toyota', 'Corolla');
	miCoche.arrancar(); // El coche Toyota Corolla está arrancando.
	miCoche.frenar(); // El coche Toyota Corolla está frenando.
	miCoche.convertir(); // El coche Toyota Corolla se está convirtiendo.
	
	const miMoto = new Moto('Yamaha', 'MT-07');
	miMoto.arrancar(); // La moto Yamaha MT-07 está arrancando.
	miMoto.frenar(); // La moto Yamaha MT-07 está frenando.
	`;
	Utils.mostrarCodigo('code-ejemplo-poo-mostrar',codigo);
	}

Código de Prueba (test/jest/EjemploPOO.test.js):

// Importación de módulos
import ejecutarEjemploPOO from '../../js/POO/EjemploPOO.js';
import Coche from '../../js/POO/Coche.js';
import Moto from '../../js/POO/Moto.js';
import Utils from '../../js/utils.js';

// Mock de la clase abstracta Vehiculo
jest.mock('../../js/POO/Vehiculo.js', () => {
return jest.fn().mockImplementation(() => {
	throw new Error("No se puede instanciar una clase abstracta Vehiculo.");
});
});

// Mock de la clase Coche
jest.mock('../../js/POO/Coche.js', () => {
return jest.fn().mockImplementation((marca, modelo) => {
	return {
	arrancar: jest.fn(() => `El coche ${marca} ${modelo} está arrancando.`),
	frenar: jest.fn(() => `El coche ${marca} ${modelo} está frenando.`),
	convertir: jest.fn(() => `El coche ${marca} ${modelo} se está convirtiendo.`)
	};
});
});

// Mock de la clase Moto
jest.mock('../../js/POO/Moto.js', () => {
return jest.fn().mockImplementation((marca, modelo) => {
	return {
	arrancar: jest.fn(() => `La moto ${marca} ${modelo} está arrancando.`),
	frenar: jest.fn(() => `La moto ${marca} ${modelo} está frenando.`)
	};
});
});

// Mock de Utils
jest.mock('../../js/utils.js', () => ({
mostrarResultado: jest.fn(),
mostrarCodigo: jest.fn()
}));

describe('ejecutarEjemploPOO', () => {
it('debería manejar el error al intentar instanciar la clase Vehiculo', () => {
	ejecutarEjemploPOO();
	expect(Utils.mostrarResultado).toHaveBeenCalledWith('resultado-ejemplo-poo', "No se puede instanciar una clase abstracta Vehiculo.");
});

it('debería crear un coche y llamar a sus métodos', () => {
	ejecutarEjemploPOO();
	expect(Coche).toHaveBeenCalledWith('Toyota', 'Corolla');
	const cocheMockInstance = Coche.mock.results[0].value;
	expect(cocheMockInstance.arrancar).toHaveBeenCalled();
	expect(cocheMockInstance.frenar).toHaveBeenCalled();
	expect(cocheMockInstance.convertir).toHaveBeenCalled();
});

it('debería crear una moto y llamar a sus métodos', () => {
	ejecutarEjemploPOO();
	expect(Moto).toHaveBeenCalledWith('Yamaha', 'MT-07');
	const motoMockInstance = Moto.mock.results[0].value;
	expect(motoMockInstance.arrancar).toHaveBeenCalled();
	expect(motoMockInstance.frenar).toHaveBeenCalled();
});

it('debería mostrar el código generado dinámicamente', () => {
	ejecutarEjemploPOO();
	expect(Utils.mostrarCodigo).toHaveBeenCalledWith(
	'code-ejemplo-poo-mostrar',
	expect.stringContaining('======== ARCHIVO interfaces.js ========')
	);
});
});

Para ejecutar las pruebas, abre la terminal y ejecuta:

npm run test:jest

¿Qué hace la prueba?

  1. Simula el comportamiento de clases y métodos utilizando mocks: La prueba utiliza jest.mock para simular la funcionalidad de clases como Vehiculo, Coche, y Moto, así como métodos de utilidades (Utils). Esto permite comprobar cómo interactúa la función ejecutarEjemploPOO con estas clases y métodos sin depender de implementaciones reales.
  2. Maneja errores: La prueba verifica que al intentar instanciar la clase abstracta Vehiculo, se maneje correctamente el error y se llame a Utils.mostrarResultado con el mensaje: "No se puede instanciar una clase abstracta Vehiculo.".
  3. Comprueba la creación y uso de objetos: Evalúa que ejecutarEjemploPOO instancie correctamente las clases Coche y Moto, llamando a sus respectivos métodos (arrancar, frenar, y convertir).
  4. Valida la generación de código dinámico: La prueba verifica que se llame a Utils.mostrarCodigo con el código generado dinámicamente, incluyendo el archivo interfaces.js, lo que demuestra que la función maneja adecuadamente la visualización de código.
  5. Organiza las pruebas en bloques: Cada bloque it verifica un aspecto específico del comportamiento de la función ejecutarEjemploPOO:
    • it('debería manejar el error al intentar instanciar la clase Vehiculo'): Evalúa la correcta gestión de errores.
    • it('debería crear un coche y llamar a sus métodos'): Comprueba la creación y uso de un objeto coche.
    • it('debería crear una moto y llamar a sus métodos'): Valida la creación y uso de un objeto moto.
    • it('debería mostrar el código generado dinámicamente'): Asegura que el código dinámico se muestre correctamente.

Estas pruebas garantizan que ejecutarEjemploPOO funcione correctamente en diferentes escenarios: manejo de errores, creación de objetos, y generación de código. Se utilizan jest.mock para simular el comportamiento de clases y métodos, y Utils.mostrarResultado y Utils.mostrarCodigo para validar que los resultados y el código se muestran correctamente.

Salida al ejecutar los tests:

> fundametos-de-desarrollo-web@1.0.0 test:jest
> jest

PASS  test/jest/EjemploPOO.test.js
ejecutarEjemploPOO
	√ debería manejar el error al intentar instanciar la clase Vehiculo (14 ms)
	√ debería crear un coche y llamar a sus métodos (2 ms)
	√ debería crear una moto y llamar a sus métodos (1 ms)
	√ debería mostrar el código generado dinámicamente (1 ms)

Test Suites: 1 passed, 1 total
Tests:       4 passed, 4 total
Snapshots:   0 total
Time:        1.698 s
Ran all test suites.

Este ejemplo muestra cómo se pueden utilizar jest.mock para simular dependencias y verificar que las funciones se comporten de forma correcta sin necesidad de instanciar clases abstractas o realizar llamadas reales a los métodos.


Integración de JSDOM para pruebas con el DOM

Por defecto Jest utiliza el entorno Node para ejecutar las pruebas, y este entorno no tiene acceso al objeto document, que es específico del navegador. Para resolver esto, necesitas configurar Jest para que use el entorno JSDOM, que simula el DOM del navegador.

Asegúrate de configurar Jest para que utilice jsdom como su entorno de pruebas. La configuración de Jest ya utiliza JSDOM de manera predeterminada, pero para asegurarte de que está configurado correctamente, agrega el archivo de configuración jest.config.js (si no lo tienes).

export default {
testEnvironment: "jest-environment-jsdom",
transform: {
	"^.+\\.js$": "babel-jest"
}
};

Instalar JSDOM manualmente (si es necesario) Aunque Jest incluye JSDOM por defecto, si por alguna razón no está instalado correctamente, puedes agregarlo manualmente con:

npm install --save-dev jest-environment-jsdom

Ejemplo de Pruebas Unitarias con describe y expect en Jest para Arrays.test.js

En este ejemplo se realizan pruebas unitarias para validar el correcto funcionamiento de operaciones sobre arrays, utilizando Jest y el entorno jest-environment-jsdom para simular el DOM cuando sea necesario. Se utiliza babel-jest para transformar los archivos JavaScript.

La configuración de Jest se define en un archivo de configuración (por ejemplo, jest.config.js) de la siguiente manera:

export default {
testEnvironment: "jest-environment-jsdom",
setupFiles: ['<rootDir>/jest.setup.js'], //debemos crear este archivo
transform: {
  "^.+\\.js$": "babel-jest"
}
};

Para poder simular el comportamiento de utils.js que utilizamos para mostrarResultado y mostrarCodigo al cual le haremos sus pruebas a continuación, pero al ser una dependencia en Arrays.js se simula con JSDOM que requiere TextEncoder y TextDecoder de node, por lo que debemos crear (jest.setup.js) como lo acabamos de especificar en jest.config.js en la propiedad setupFiles quedando jest.setup.js de la siguiente forma:

import { TextEncoder, TextDecoder } from 'util';

	global.TextEncoder = TextEncoder;
	global.TextDecoder = TextDecoder;
	

Esta configuración le indica a Jest que:

  1. Use jest-environment-jsdom como entorno de pruebas, lo que permite simular el objeto document y otros elementos del DOM.
  2. Utilice babel-jest para transformar los archivos JavaScript, facilitando la compatibilidad con la sintaxis moderna.

Código a evaluar de (Arrays.js):

import Utils from './utils.js';
const idResultado = 'resultado-ejemplo-arrays';
const idCode = 'code-ejemplo-arrays-mostrar';

// Push
function ejecutarPush() {
	const array = [1, 2, 3];
	const elementoAAgregar =document.getElementById("input-push").value;
	if (elementoAAgregar){
		array.push(elementoAAgregar); //agrega al final
	}else {
		Utils.mostrarResultado(idResultado, `No agregaste nada, se agregará un 4`);
		array.push(4);
	}
	
	const codigo = `const array = [1, 2, 3];
	const elementoAAgregar =document.getElementById("input-push").value;
	if (elementoAAgregar){
		array.push(elementoAAgregar);
	}else {
		Utils.mostrarResultado(idResultado, \`No agregaste nada, se agregará un 4\`);
		array.push(4);
	}`;

	Utils.mostrarResultado(idResultado, `Array después de push: ${array}`);
	Utils.mostrarCodigo(idCode, codigo);
}

// Pop
function ejecutarPop() {
	const array = [1, 2, 3];
	const elementoExtraido = array.pop(); //último elemento

	const codigo = `const array = [1, 2, 3];
const elementoExtraido = array.pop(); // elementoExtraido es 3, array es [1, 2]`;

	Utils.mostrarResultado(idResultado, `Elemento eliminado: ${elementoExtraido}, Array restante: ${array}`);
	Utils.mostrarCodigo(idCode, codigo);
}

//Unshift
function ejecutarUnshift() {
	const array = [2, 3];
	const elementoAAgregar =document.getElementById("input-unshift").value;
	if (elementoAAgregar){
		array.unshift(elementoAAgregar); //agrega al principio
	}else {
		Utils.mostrarResultado(idResultado, `No agregaste nada, se agregará un 1`);
		array.unshift(1); //agrega al principio
	}
	const codigo = `const array = [2, 3];
	const elementoAAgregar =document.getElementById("input-unshift").value;
	if (elementoAAgregar){
		array.unshift(elementoAAgregar); //agrega al principio
	}else {
		Utils.mostrarResultado(idResultado, 'No agregaste nada, se agregará un 1');
		array.unshift(1); //agrega al principio
	}`;

	Utils.mostrarResultado(idResultado, `Array después de unshift: ${array}`);
	Utils.mostrarCodigo(idCode, codigo);
}

//shift
function ejecutarShift() {
	const array = [1, 2, 3];
	const elementoExtraido = array.shift(); // último elemento

	const codigo = `const array = [1, 2, 3];
const elementoExtraido = array.shift(); // elementoExtraido es 1, array es [2, 3]`;

	Utils.mostrarResultado(idResultado, `Elemento eliminado: ${elementoExtraido}, Array restante: ${array}`);
	Utils.mostrarCodigo(idCode, codigo);
}

function ejecutarConcat() {
	const array1 = [1, 2];
	const array2 = [3, 4];
	const result = array1.concat(array2);

	const codigo = `const array1 = [1, 2];
const array2 = [3, 4];
const result = array1.concat(array2); // result es [1, 2, 3, 4]`;

	Utils.mostrarResultado(idResultado, `Array concatenado: ${result}`);
	Utils.mostrarCodigo(idCode, codigo);
}

//Sort
function ejecutarSort() {
	const array = [3, 1, 4, 2];
	array.sort(); //ordena ascendentemente o alfabéticamente

	const codigo = `const array = [3, 1, 4, 2];
array.sort(); // Ahora array es [1, 2, 3, 4]`;

	Utils.mostrarResultado(idResultado, `Array ordenado: ${array}`);
	Utils.mostrarCodigo(idCode, codigo);
}

// Slice
function ejecutarSlice() {
	const array = [1, 2, 3, 4];
	const cortado = array.slice(1, 3); //(inicio incluido, final no incluido) para poder utilizar .length

	const codigo = `const array = [1, 2, 3, 4]; 
const cortado = array.slice(1, 3); // cortado es [2, 3]
//(inicio incluido, final no incluido) para poder utilizar .length`;

	Utils.mostrarResultado(idResultado, `Resultado de slice: ${cortado}`);
	Utils.mostrarCodigo(idCode, codigo);
}

//Reverse
function ejecutarReverse() {
	const array = [1, 2, 3];
	array.reverse(); // Ahora array es [3, 2, 1]
	const nombres =["Ricardo", "Willy", "Patricio","Bob", "Alan", "Francisco"]
	nombres.sort(); //ordenado alfabéticamente A - Z
	nombres.reverse(); //ordenado de Z - A
	

	const codigo = `const array = [1, 2, 3];
	array.reverse(); // Ahora array es [3, 2, 1]
	const nombres =["Ricardo", "Willy", "Patricio","Bob", "Alan", "Francisco"]
	nombres.sort(); //ordenado alfabéticamente A - Z
	nombres.reverse(); //ordenado de Z - A`;

	Utils.mostrarResultado(idResultado, `Array invertido: ${array}\nNombres de Z - A: ${nombres}`);
	Utils.mostrarCodigo(idCode, codigo);
}

//Flat
function ejecutarFlat() {
	const array = [1, [2, [3, [4]]]];
	const aplanado = array.flat(3);

	const codigo = `const array = [1, [2, [3, [4]]]];
const aplanado = array.flat(3); // aplanado es [1, 2, 3, 4]`;

	Utils.mostrarResultado(idResultado, `Array aplanado: ${aplanado}`);
	Utils.mostrarCodigo(idCode, codigo);
}

//Includes
function ejecutarIncludes() {
	const array = [1, 2, 3, 4, "Ricardo", "Willy", "Patricio", "Bob", "Alan", "Francisco"];
	let elementoBuscado = document.getElementById("input-includes").value;
	let hasValue = false;

	if (elementoBuscado) {
		elementoBuscado = isNaN(elementoBuscado) ? elementoBuscado : Number(elementoBuscado);
		hasValue = array.includes(elementoBuscado);
	} else {
		Utils.mostrarResultado(idResultado, "No buscaste nada, se buscará Ricardo");
		elementoBuscado = "Ricardo";
		hasValue = array.includes(elementoBuscado);
	}

	const codigo = `const array = [1, 2, 3, 4, "Ricardo", "Willy", "Patricio", "Bob", "Alan", "Francisco"];
const elementoBuscado = document.getElementById("input-includes").value;
let hasValue = false;

if (elementoBuscado) {
	elementoBuscado = isNaN(elementoBuscado) ? elementoBuscado : Number(elementoBuscado);
	hasValue = array.includes(elementoBuscado);
} else {
	Utils.mostrarResultado(idResultado, "No buscaste nada, se buscará Ricardo");
	elementoBuscado = "Ricardo";
	hasValue = array.includes(elementoBuscado);
}`;

	Utils.mostrarResultado(idResultado, `¿Contiene el valor ${elementoBuscado}?: ${hasValue}`);
	Utils.mostrarCodigo(idCode, codigo);

	/* Bien por ver esto curioso! 
	Cómo utilizar includes sin que distinga mayusculas y minusculas?
	Todavía no sabemos programación funcional
	pero se podría utilizar map para volverlas toUperCase = mayusculas
	toLowerCase =minusculas y comparar en igualdad:
	
	elementoBuscado = isNaN(elementoBuscado) ? elementoBuscado.toLowerCase() : Number(elementoBuscado);
	hasValue = array.map(item => typeof item === 'string' ? item.toLowerCase() : item).includes(elementoBuscado);*/
}

// IndexOf
function ejecutarIndexOf() {
	const array = [1, 2, 3];
	const index = array.indexOf(2);

	const codigo = `const array = [1, 2, 3];
const index = array.indexOf(2); // index es 1`;

	Utils.mostrarResultado(idResultado, `Índice del valor 2: ${index}`);
	Utils.mostrarCodigo(idCode, codigo);
}

//LastIndexOf
function ejecutarLastIndexOf() {
	const array = [1, 2, 3, 2];
	const index = array.lastIndexOf(2);

	const codigo = `const array = [1, 2, 3, 2];
const index = array.lastIndexOf(2); // index es 3`;

	Utils.mostrarResultado(idResultado, `Último índice del valor 2: ${index}`);
	Utils.mostrarCodigo(idCode, codigo);
}

// Length
function ejecutarLength() {
	const array = [1, 2, 3];
	const length = array.length;

	const codigo = `const array = [1, 2, 3];
const length = array.length; // length es 3`;

	Utils.mostrarResultado(idResultado, `Longitud del array: ${length}`);
	Utils.mostrarCodigo(idCode, codigo);
}

export {
	ejecutarPush,
	ejecutarPop,
	ejecutarShift,
	ejecutarUnshift,
	ejecutarConcat,
	ejecutarSort,
	ejecutarSlice,
	ejecutarReverse,
	ejecutarFlat,
	ejecutarIncludes,
	ejecutarIndexOf,
	ejecutarLastIndexOf,
	ejecutarLength
};

Código de Prueba (Arrays.test.js):

// Simulación del DOM con JSDOM
import { JSDOM } from 'jsdom';
const dom = new JSDOM(`
	<!DOCTYPE html>
	<html>
		<body>
			<div id="resultado-ejemplo-arrays"></div>
			<div id="code-ejemplo-arrays-mostrar"></div>
		</body>
	</html>
`);
global.document = dom.window.document;


// Polifill para TextEncoder y TextDecoder en Node
import { TextEncoder, TextDecoder } from 'util';
global.TextEncoder = TextEncoder;
global.TextDecoder = TextDecoder;



// Importación de módulos
import * as Arrays from '../../js/Arrays.js';
import Utils from '../../js/utils.js';


describe('Pruebas de funciones de Arrays.js', () => {
	let mostrarResultadoSpy;

	beforeEach(() => {
	// Espiar la función mostrarResultado de Utils
	mostrarResultadoSpy = jest.spyOn(Utils, 'mostrarResultado').mockImplementation(() => { });

	//volvemos a colocar los ellementos de DOM para que no falle la proxoma prueba con Utils
	document.body.innerHTML = `
	<div id="resultado-ejemplo-arrays"></div>
	<div id="code-ejemplo-arrays-mostrar"></div>
	`;
	});

	afterEach(() => {
	jest.restoreAllMocks();
	// Limpiar inputs creados en el DOM si existen
	['input-push', 'input-unshift', 'input-includes'].forEach(id => {
		const elem = document.getElementById(id);
		if (elem) elem.remove();
	});
	//volvemos a colocar los ellementos de DOM para que no falle la proxoma prueba con Utils
	document.body.innerHTML = `
	<div id="resultado-ejemplo-arrays"></div>
	<div id="code-ejemplo-arrays-mostrar"></div>
	`;
	});

	// Tests para cada función
	describe('ejecutarPush', () => {
	it('debe agregar el valor ingresado al final del array', () => {
		const inputPush = document.createElement('input');
		inputPush.id = 'input-push';
		inputPush.value = '10';
		document.body.appendChild(inputPush);

		Arrays.ejecutarPush();

		expect(mostrarResultadoSpy).toHaveBeenLastCalledWith(
		'resultado-ejemplo-arrays',
		'Array después de push: 1,2,3,10'
		);

		inputPush.remove();
	});

	it('debe agregar el valor por defecto 4 cuando no se ingresa nada', () => {
		const inputPush = document.createElement('input');
		inputPush.id = 'input-push';
		inputPush.value = '';
		document.body.appendChild(inputPush);

		Arrays.ejecutarPush();

		expect(mostrarResultadoSpy).toHaveBeenLastCalledWith(
		'resultado-ejemplo-arrays',
		'Array después de push: 1,2,3,4'
		);

		inputPush.remove();
	});
	});

	describe('ejecutarPop', () => {
	it('debe eliminar el último elemento del array', () => {
		Arrays.ejecutarPop();

		expect(mostrarResultadoSpy).toHaveBeenLastCalledWith(
		'resultado-ejemplo-arrays',
		'Elemento eliminado: 3, Array restante: 1,2'
		);
	});
	});

	describe('ejecutarUnshift', () => {
	it('debe agregar el valor ingresado al principio del array', () => {
		const inputUnshift = document.createElement('input');
		inputUnshift.id = 'input-unshift';
		inputUnshift.value = '0';
		document.body.appendChild(inputUnshift);

		Arrays.ejecutarUnshift();

		expect(mostrarResultadoSpy).toHaveBeenLastCalledWith(
		'resultado-ejemplo-arrays',
		'Array después de unshift: 0,2,3'
		);

		inputUnshift.remove();
	});

	it('debe agregar el valor por defecto 1 cuando no se ingresa nada', () => {
		const inputUnshift = document.createElement('input');
		inputUnshift.id = 'input-unshift';
		inputUnshift.value = '';
		document.body.appendChild(inputUnshift);

		Arrays.ejecutarUnshift();

		expect(mostrarResultadoSpy).toHaveBeenLastCalledWith(
		'resultado-ejemplo-arrays',
		'Array después de unshift: 1,2,3'
		);

		inputUnshift.remove();
	});
	});

	describe('ejecutarShift', () => {
	it('debe eliminar el primer elemento del array', () => {
		Arrays.ejecutarShift();

		expect(mostrarResultadoSpy).toHaveBeenLastCalledWith(
		'resultado-ejemplo-arrays',
		'Elemento eliminado: 1, Array restante: 2,3'
		);
	});
	});

	describe('ejecutarConcat', () => {
	it('debe concatenar los dos arrays', () => {
		Arrays.ejecutarConcat();

		expect(mostrarResultadoSpy).toHaveBeenLastCalledWith(
		'resultado-ejemplo-arrays',
		'Array concatenado: 1,2,3,4'
		);
	});
	});

	describe('ejecutarSort', () => {
	it('debe ordenar el array de forma ascendente', () => {
		Arrays.ejecutarSort();

		expect(mostrarResultadoSpy).toHaveBeenLastCalledWith(
		'resultado-ejemplo-arrays',
		'Array ordenado: 1,2,3,4'
		);
	});
	});

	describe('ejecutarSlice', () => {
	it('debe extraer el segmento del array', () => {
		Arrays.ejecutarSlice();

		expect(mostrarResultadoSpy).toHaveBeenLastCalledWith(
		'resultado-ejemplo-arrays',
		'Resultado de slice: 2,3'
		);
	});
	});

	describe('ejecutarReverse', () => {
	it('debe invertir el array y mostrar los nombres en orden Z-A', () => {
		Arrays.ejecutarReverse();

		const expectedResultado =
		'Array invertido: 3,2,1\nNombres de Z - A: Willy,Ricardo,Patricio,Francisco,Bob,Alan';

		expect(mostrarResultadoSpy).toHaveBeenLastCalledWith(
		'resultado-ejemplo-arrays',
		expectedResultado
		);
	});
	});

	describe('ejecutarFlat', () => {
	it('debe aplanar el array', () => {
		Arrays.ejecutarFlat();

		expect(mostrarResultadoSpy).toHaveBeenLastCalledWith(
		'resultado-ejemplo-arrays',
		'Array aplanado: 1,2,3,4'
		);
	});
	});

	describe('ejecutarIncludes', () => {
	it('debe verificar que el array contenga el valor ingresado (string)', () => {
		const inputIncludes = document.createElement('input');
		inputIncludes.id = 'input-includes';
		inputIncludes.value = 'Patricio';
		document.body.appendChild(inputIncludes);

		Arrays.ejecutarIncludes();

		expect(mostrarResultadoSpy).toHaveBeenLastCalledWith(
		'resultado-ejemplo-arrays',
		'¿Contiene el valor Patricio?: true'
		);

		inputIncludes.remove();
	});

	it('debe buscar el valor por defecto "Ricardo" cuando no se ingresa nada', () => {
		const inputIncludes = document.createElement('input');
		inputIncludes.id = 'input-includes';
		inputIncludes.value = '';
		document.body.appendChild(inputIncludes);

		Arrays.ejecutarIncludes();

		expect(mostrarResultadoSpy).toHaveBeenLastCalledWith(
		'resultado-ejemplo-arrays',
		'¿Contiene el valor Ricardo?: true'
		);

		inputIncludes.remove();
	});
	});

	describe('ejecutarIndexOf', () => {
	it('debe retornar el índice del valor 2', () => {
		Arrays.ejecutarIndexOf();

		expect(mostrarResultadoSpy).toHaveBeenLastCalledWith(
		'resultado-ejemplo-arrays',
		'Índice del valor 2: 1'
		);
	});
	});

	describe('ejecutarLastIndexOf', () => {
	it('debe retornar el último índice del valor 2', () => {
		Arrays.ejecutarLastIndexOf();

		expect(mostrarResultadoSpy).toHaveBeenLastCalledWith(
		'resultado-ejemplo-arrays',
		'Último índice del valor 2: 3'
		);
	});
	});

	describe('ejecutarLength', () => {
	it('debe mostrar la longitud del array', () => {
		Arrays.ejecutarLength();

		expect(mostrarResultadoSpy).toHaveBeenLastCalledWith(
		'resultado-ejemplo-arrays',
		'Longitud del array: 3'
		);
	});
	});
});

Para ejecutar las pruebas, abre la terminal y ejecuta el siguiente comando:

npm run test:jest

Salida al ejecutar los tests:

> fundametos-de-desarrollo-web@1.0.0 test:jest
> jest

PASS  test/jest/EjemploPOO.test.js
PASS  test/jest/Arrays.test.js    

Test Suites: 2 passed, 2 total
Tests:       14 passed, 14 total
Snapshots:   0 total
Time:        18.326 s
Ran all test suites.
  

Este ejemplo muestra cómo, además de las pruebas sobre POO, se pueden realizar pruebas unitarias enfocadas en operaciones con arrays. Se utiliza la configuración de Jest para asegurar que el entorno simule un navegador mediante jsdom y se transforma el código JavaScript moderno con babel-jest.

Con esta estructura, se puede comprobar que las funciones que manipulan arrays se comportan como se espera, validando tanto el contenido, la longitud y el filtrado de los elementos.


Pruebas a utils.js

Podemos hacerle las pruebas unitarias a utils.js que sólo tiene dos métodos para mostrarResultado y mostrarCodigo.

const Utils = {
mostrarResultado: function(idEtiqueta, resultado) {
	const textarea = document.getElementById(idEtiqueta);
	if (textarea) {
		textarea.value += resultado + "\n";
	} else {
		console.error(`Elemento con id "${idEtiqueta}" no encontrado.`);
	}
},

mostrarCodigo: function(idEtiqueta, codigo) {
	const codeFunciones = document.getElementById(idEtiqueta);
	if (codeFunciones) {
		codeFunciones.textContent = codigo;
	} else {
		console.error(`Elemento con id "${idEtiqueta}" no encontrado.`);
	}
}
};

export default Utils;

Nos quedaría un archivo de pruebas utils.test.js:

// Importa el módulo Utils
import Utils from '../../js/utils.js';

// Mock de console.error para verificar llamadas a errores
global.console = {
	error: jest.fn(),
};

describe('Pruebas para las funciones de Utils', () => {
	// Limpiar el DOM y los mocks después de cada prueba
	afterEach(() => {
	document.body.innerHTML = '';
	jest.clearAllMocks();
	});

	describe('mostrarResultado', () => {
	it('debe agregar el resultado al valor del textarea cuando el elemento existe', () => {
		// Configura el DOM con un textarea
		document.body.innerHTML = '<textarea id="resultado"></textarea>';
		const textarea = document.getElementById('resultado');

		// Llama a la función
		Utils.mostrarResultado('resultado', 'Prueba de resultado');

		// Verifica que el valor del textarea se haya actualizado correctamente
		expect(textarea.value).toBe('Prueba de resultado\n');
	});

	it('debe registrar un error en la consola cuando el elemento no existe', () => {
		// Llama a la función sin configurar el DOM
		Utils.mostrarResultado('resultado', 'Prueba de resultado');

		// Verifica que se haya llamado a console.error con el mensaje adecuado
		expect(console.error).toHaveBeenCalledWith('Elemento con id "resultado" no encontrado.');
	});
	});

	describe('mostrarCodigo', () => {
	it('debe establecer el contenido de texto del elemento cuando este existe', () => {
		// Configura el DOM con un elemento div
		document.body.innerHTML = '
'; const div = document.getElementById('codigo'); // Llama a la función Utils.mostrarCodigo('codigo', 'Código de ejemplo'); // Verifica que el contenido de texto del div se haya establecido correctamente expect(div.textContent).toBe('Código de ejemplo'); }); it('debe registrar un error en la consola cuando el elemento no existe', () => { // Llama a la función sin configurar el DOM Utils.mostrarCodigo('codigo', 'Código de ejemplo'); // Verifica que se haya llamado a console.error con el mensaje adecuado expect(console.error).toHaveBeenCalledWith('Elemento con id "codigo" no encontrado.'); }); }); });

Cobertura de código

Jest puede generar informes de cobertura de código para mostrar qué partes del código están cubiertas por las pruebas. Para habilitar la cobertura, ejecuta jest --coverage o en nuestro caso: npm run test:jest -- --coverage.

y nos queda el jest.config.js de la siguiente forma:

export default {
testEnvironment: "jest-environment-jsdom", // esto habilita un DOM simulado para las pruebas en Node
setupFiles: ['/jest.setup.js'], // esto indica el archivo setup para el TextEncoder de JSDOM
collectCoverage: true, // Esto habilita la generación de cobertura
coverageDirectory: "coverage", // Carpeta donde se almacenará el informe de cobertura
coverageReporters: ["text", "lcov"], // Formatos de reporte (en consola y en HTML)
roots: ["/test/jest/"], // Solo mira este directorio correspondiente a Jest por tener jasmine en el mismo proyecto causa error spyOn
transform: {
	"^.+\\.js$": "babel-jest"
}  

};

Salida al ejecutar los tests con cobertura de código:

> npm run test:jest -- --coverage

> fundametos-de-desarrollo-web@1.0.0 test:jest
> jest

	PASS  test/jest/EjemploPOO.test.js
	PASS  test/jest/utils.test.js     
	PASS  test/jest/Arrays.test.js    
------------------|---------|----------|---------|---------|-------------------
File              | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
------------------|---------|----------|---------|---------|-------------------
All files         |     100 |    91.66 |     100 |     100 |                   
  js              |     100 |    91.66 |     100 |     100 |                   
  Arrays.js       |     100 |     87.5 |     100 |     100 | 152               
  utils.js        |     100 |      100 |     100 |     100 |                   
  js/POO          |     100 |      100 |     100 |     100 |                   
  EjemploPOO.js   |     100 |      100 |     100 |     100 | 
------------------|---------|----------|---------|---------|-------------------

Test Suites: 3 passed, 3 total
Tests:       24 passed, 24 total
Snapshots:   0 total
Time:        6.939 s
Ran all test suites.

Se crea una página web en la carpeta recién creada coverage con el informe de Jest sobre la cobertura de tu código, abriendo la página index.html dentro de coverage/lcov-report/index.html:

jest-coverage

Pruebas unitarias: Ejemplo de configuración con Jasmine

Jasmine requiere que después de intalarlo con npm install --save-dev jasmine inicialice Jasmine para que te genere la ruta con el archivospec/support/jasmine.mjs

Para nuestro caso al trabajar con jest y jasmine, es necesario cambiar en jasmine.mjs la linea spec_dir: "test/jasmine", para indicarle a Jasmine la ruta que revise para archivos .spec.js sin tener problemas con jest.

Por otro lado es necesario indicarle a Jest que ejecute sólo las pruebas en su carpeta en el archivo jest.config.js con la linea roots: ["<rootDir>/test/jest/"],

Jasmine facilita la creación de mocks utilizando spyOn, que permite simular una función y verificar sus interacciones.

Ejemplo de configuración de Jasmine

Si vas a trabajar unicamente con jasmine y quieres ejecutar las pruebas con npm run test en el archivo package.json coloca "scripts": { "test": "jasmine"}.

Para nuestro caso al tener Jest y Jasmine tenemos el package.json de la siguiente forma:

{
    "name": "fundametos-de-desarrollo-web",
    "version": "1.0.0",
    "description": "Pruebas unitarias con Jest y Jasmine para ejemplos del proyecto",
    "type": "module",
    "scripts": {
        "test:jasmine": "jasmine",
        "test:jest": "jest",
        "test": "npm run test:jasmine && npm run test:jest"
    },
    "devDependencies": {
        "@babel/core": "^7.21.0",
        "@babel/preset-env": "^7.21.0",
        "babel-jest": "^29.7.0",
        "jasmine": "^5.6.0",
        "jest": "^29.7.0",
        "jest-environment-jsdom": "^29.7.0"
    }
}

Ejecutar las pruebas

Ejecuta npm run test o npm run test:jasmine en la terminal para ejecutar las pruebas. Jasmine buscará archivos con el sufijo .spec.js y ejecutará las pruebas definidas en ellos.

Conceptos clave de Jasmine

  • describe: Agrupa pruebas relacionadas.
  • it: Define una prueba individual.
  • expect: Crea afirmaciones para validar el comportamiento.
  • toBe: Comprueba igualdad estricta.
  • toEqual: Comprueba igualdad profunda para objetos o arrays.
  • beforeEach y afterEach: Ejecutan código antes y después de cada prueba individual.
  • spyOn: Crea un espía para simular y rastrear funciones.

Veamos un ejemplo sencillo antes de hacer un ejemplo real

Ejemplo con describe y spyOn

Código a evaluar:

// notificaciones.js
function enviarNotificacion(mensaje) {
  console.log("Notificación enviada:", mensaje);
  return true;
}

Prueba en test/jasmine/notificaciones.spec.js

// notifiaciones.spec.js
	describe("Pruebas de notificaciones", function() {
  it("debe llamar a enviarNotificacion con el mensaje correcto", function() {
    spyOn(window, "enviarNotificacion").and.returnValue(true);
    const resultado = enviarNotificacion("Hola, Jasmine");
    expect(enviarNotificacion).toHaveBeenCalled();
    expect(enviarNotificacion).toHaveBeenCalledWith("Hola, Jasmine");
    expect(resultado).toBe(true);
  });
});

Ejecuta npm run test o npm run test:jasmine en la terminal para ejecutar las pruebas.

¿Qué hace la función?

  1. La función enviarNotificacionsimula el envío de una notificación.
  2. prueba con describe y spyOn:
    • aspyOn crea un "espía" que permite simular el comportamiento de una función.
    • La prueba verifica que la función enviarNotificacion se haya llamado con el mensaje correcto y que devuelva el valor esperado.

Ejemplo pruebas unitarias con jasmine: Métodos de Orden superior de Arrays

Para el primer ejemplo con Jasmine podemos hacer las pruebas para funcionesOdenSuperiorArrays.js

Código a evaluar de métodos de orden superior de arrays:

import Utils from './utils.js';
const idResultado = 'resultado-metodos-arrays-orden-superior';
const idCode = 'code-metodos-arrays-orden-superior-mostrar';

	
//   Funciones Impuras de Orden Superior de Arrays


/* forEach()
	- Ejemplo impuro: recorre cada elemento y concatena un string (efecto secundario).
	- No devuelve un nuevo array.
*/
function ejecutarForEach() {
	const array = [1, 2, 3, 4];
	let resultado = "";
	array.forEach((element, index) => {
		resultado += `Elemento ${index}: ${element}\n`;
	});
	const codigo = `const array = [1, 2, 3, 4];
let resultado = "";
array.forEach((element, index) => {
	resultado += \`Elemento \${index}: \${element}\\n\`;
});
console.log(resultado);`;
	Utils.mostrarResultado(idResultado, resultado);
	Utils.mostrarCodigo(idCode, codigo);
}

/* sort()
	- Ejemplo impuro: ordena el array original según una función de comparación.
	- Modifica el array original, por lo que no es pura.
*/
function ejecutarArraySort() {
	const array = [3, 1, 4, 2];
	array.sort((a, b) => a - b);
	const codigo = `const array = [3, 1, 4, 2];
array.sort((a, b) => a - b); // Ahora array es [1, 2, 3, 4]`;
	Utils.mostrarResultado(idResultado, `Array ordenado: ${array}`);
	Utils.mostrarCodigo(idCode, codigo);
}

	
//   Funciones Puros de Orden Superior de Arrays


/* map()
	- Aplica una función a cada elemento del array y devuelve un nuevo array.
	- No modifica el array original.
*/
function ejecutarArrayMap() {
	const array = [1, 2, 3, 4];
	const mapped = array.map(x => x * 2);
	const codigo = `const array = [1, 2, 3, 4];
const mapped = array.map(x => x * 2); // mapped es [2, 4, 6, 8]`;
	Utils.mostrarResultado(idResultado, `Array mapeado (cada elemento * 2): ${mapped}`);
	Utils.mostrarCodigo(idCode, codigo);
}

/* filter()
	- Devuelve un nuevo array con los elementos que cumplen la condición.
	- No altera el array original.
*/
function ejecutarArrayFilter() {
	const array = [1, 2, 3, 4, 5, 6];
	const filtered = array.filter(x => x % 2 === 0);
	const codigo = `const array = [1, 2, 3, 4, 5, 6];
const filtered = array.filter(x => x % 2 === 0); // filtered es [2, 4, 6]`;
	Utils.mostrarResultado(idResultado, `Array filtrado (solo pares): ${filtered}`);
	Utils.mostrarCodigo(idCode, codigo);
}

/* reduce()
	- Recorre el array y acumula sus elementos en un único valor mediante una función acumuladora.
	- Devuelve el resultado sin modificar el array original.
*/
function ejecutarArrayReduce() {
	const array = [1, 2, 3, 4];
	const sum = array.reduce((acc, curr) => acc + curr, 0);
	const codigo = `const array = [1, 2, 3, 4];
const sum = array.reduce((acc, curr) => acc + curr, 0); // sum es 10`;
	Utils.mostrarResultado(idResultado, `Suma de elementos con reduce: ${sum}`);
	Utils.mostrarCodigo(idCode, codigo);
}

/* some()
	- Comprueba si al menos un elemento cumple con la condición del callback.
	- Devuelve un booleano sin modificar el array.
*/
function ejecutarArraySome() {
	const array = [1, 3, 5, 7];
	const hasEven = array.some(x => x % 2 === 0);
	const codigo = `const array = [1, 3, 5, 7];
const hasEven = array.some(x => x % 2 === 0); // hasEven es false`;
	Utils.mostrarResultado(idResultado, `¿Algún elemento es par? ${hasEven}`);
	Utils.mostrarCodigo(idCode, codigo);
}

/* every()
	- Verifica si todos los elementos cumplen la condición.
	- Devuelve un booleano sin alterar el array.
*/
function ejecutarArrayEvery() {
	const array = [2, 4, 6, 8];
	const allEven = array.every(x => x % 2 === 0);
	const codigo = `const array = [2, 4, 6, 8];
const allEven = array.every(x => x % 2 === 0); // allEven es true`;
	Utils.mostrarResultado(idResultado, `¿Todos los elementos son pares? ${allEven}`);
	Utils.mostrarCodigo(idCode, codigo);
}

/* find()
	- Devuelve el primer elemento que cumple la condición.
	- No modifica el array original.
*/
function ejecutarArrayFind() {
	const array = [5, 12, 8, 130, 44];
	const found = array.find(x => x > 10);
	const codigo = `const array = [5, 12, 8, 130, 44];
const found = array.find(x => x > 10); // found es 12, el primer elemento mayor que 10`;
	Utils.mostrarResultado(idResultado, `Primer elemento mayor a 10: ${found}`);
	Utils.mostrarCodigo(idCode, codigo);
}

/* findIndex()
	- Devuelve el índice del primer elemento que cumple la condición o -1 si ninguno la cumple.
	- Es una función pura.
*/
function ejecutarArrayFindIndex() {
	const array = [5, 12, 8, 130, 44];
	const index = array.findIndex(x => x > 10);
	const codigo = `const array = [5, 12, 8, 130, 44];
const index = array.findIndex(x => x > 10); // index es 1`;
	Utils.mostrarResultado(idResultado, `Índice del primer elemento mayor a 10: ${index}`);
	Utils.mostrarCodigo(idCode, codigo);
}

/* flatMap()
	- Aplica una función a cada elemento y luego aplana el resultado en un nuevo array.
	- No modifica el array original.
*/
function ejecutarArrayFlatMap() {
	const array = [1, 2, 3];
	const flatMapped = array.flatMap(x => [x, x * 2]);
	const codigo = `const array = [1, 2, 3];
const flatMapped = array.flatMap(x => [x, x * 2]);
// flatMapped es [1, 2, 2, 4, 3, 6]`;
	Utils.mostrarResultado(idResultado, `Array flatMap (cada elemento y su doble): ${flatMapped}`);
	Utils.mostrarCodigo(idCode, codigo);
}

export {
	ejecutarForEach,
	ejecutarArraySort,
	ejecutarArrayMap,
	ejecutarArrayFilter,
	ejecutarArrayReduce,
	ejecutarArraySome,
	ejecutarArrayEvery,
	ejecutarArrayFind,
	ejecutarArrayFindIndex,
	ejecutarArrayFlatMap
};

Pruebas unitarias con funcionesOrdenSuperiorArrays.spec.js:

import * as Funciones from '../../js/funcionesOrdenSuperiorArrays.js';
	import Utils from '../../js/utils.js';
	
	// Simula las funciones de Utils con Jasmine (similar a Jest pero con spies)
	beforeEach(() => {
	  spyOn(Utils, 'mostrarResultado'); // Crea un espía para `mostrarResultado`
	  spyOn(Utils, 'mostrarCodigo');    // Crea un espía para `mostrarCodigo`
	});
	
	describe('Funciones de Orden Superior para Arrays', () => {
	  describe('ejecutarForEach', () => {
		it('debería recorrer el array y mostrar los elementos con índices', () => {
		  Funciones.ejecutarForEach();
	
		  expect(Utils.mostrarResultado).toHaveBeenCalledWith(
			'resultado-metodos-arrays-orden-superior',
			'Elemento 0: 1\nElemento 1: 2\nElemento 2: 3\nElemento 3: 4\n'
		  );
		});
	  });
	
	  describe('ejecutarArraySort', () => {
		it('debería ordenar el array de forma ascendente', () => {
		  Funciones.ejecutarArraySort();
	
		  expect(Utils.mostrarResultado).toHaveBeenCalledWith(
			'resultado-metodos-arrays-orden-superior',
			'Array ordenado: 1,2,3,4'
		  );
		});
	  });
	
	  describe('ejecutarArrayMap', () => {
		it('debería mapear el array y multiplicar cada elemento por 2', () => {
		  Funciones.ejecutarArrayMap();
	
		  expect(Utils.mostrarResultado).toHaveBeenCalledWith(
			'resultado-metodos-arrays-orden-superior',
			'Array mapeado (cada elemento * 2): 2,4,6,8'
		  );
		});
	  });
	
	  describe('ejecutarArrayFilter', () => {
		it('debería filtrar los elementos pares del array', () => {
		  Funciones.ejecutarArrayFilter();
	
		  expect(Utils.mostrarResultado).toHaveBeenCalledWith(
			'resultado-metodos-arrays-orden-superior',
			'Array filtrado (solo pares): 2,4,6'
		  );
		});
	  });
	
	  describe('ejecutarArrayReduce', () => {
		it('debería reducir el array sumando sus elementos', () => {
		  Funciones.ejecutarArrayReduce();
	
		  expect(Utils.mostrarResultado).toHaveBeenCalledWith(
			'resultado-metodos-arrays-orden-superior',
			'Suma de elementos con reduce: 10'
		  );
		});
	  });
	
	  describe('ejecutarArraySome', () => {
		it('debería verificar si al menos un elemento es par', () => {
		  Funciones.ejecutarArraySome();
	
		  expect(Utils.mostrarResultado).toHaveBeenCalledWith(
			'resultado-metodos-arrays-orden-superior',
			'¿Algún elemento es par? false'
		  );
		});
	  });
	
	  describe('ejecutarArrayEvery', () => {
		it('debería verificar si todos los elementos son pares', () => {
		  Funciones.ejecutarArrayEvery();
	
		  expect(Utils.mostrarResultado).toHaveBeenCalledWith(
			'resultado-metodos-arrays-orden-superior',
			'¿Todos los elementos son pares? true'
		  );
		});
	  });
	
	  describe('ejecutarArrayFind', () => {
		it('debería encontrar el primer elemento mayor a 10', () => {
		  Funciones.ejecutarArrayFind();
	
		  expect(Utils.mostrarResultado).toHaveBeenCalledWith(
			'resultado-metodos-arrays-orden-superior',
			'Primer elemento mayor a 10: 12'
		  );
		});
	  });
	
	  describe('ejecutarArrayFindIndex', () => {
		it('debería devolver el índice del primer elemento mayor a 10', () => {
		  Funciones.ejecutarArrayFindIndex();
	
		  expect(Utils.mostrarResultado).toHaveBeenCalledWith(
			'resultado-metodos-arrays-orden-superior',
			'Índice del primer elemento mayor a 10: 1'
		  );
		});
	  });
	
	  describe('ejecutarArrayFlatMap', () => {
		it('debería aplanar el array después de duplicar cada elemento', () => {
		  Funciones.ejecutarArrayFlatMap();
	
		  expect(Utils.mostrarResultado).toHaveBeenCalledWith(
			'resultado-metodos-arrays-orden-superior',
			'Array flatMap (cada elemento y su doble): 1,2,2,4,3,6'
		  );
		});
	  });
	});
	

Ejecuta npm run test o npm run test:jasmine en la terminal para ejecutar las pruebas.

¿Qué hace la función?

  1. La función ejecutarForEach recorre un array y realiza una acción sobre cada elemento.
  2. Pruebas clave utilizando Jasmine:
    • describe agrupa las pruebas relacionadas, como todas las pruebas de métodos de orden superior.
    • spyOn crea un "espía" para simular y rastrear el comportamiento de funciones como mostrarResultado.
    • Las pruebas validan que las funciones como ejecutarForEach y otras sean llamadas correctamente y generen los resultados esperados.

Resultado de la prueba con Jasmine de métodos de orden superior de arrays:


	> fundametos-de-desarrollo-web@1.0.0 test:jasmine
	> jasmine
	
	Randomized with seed 04805
	Started
	..........
	
	
	10 specs, 0 failures
	Finished in 0.024 seconds
	Randomized with seed 04805 (jasmine --random=true --seed=04805)

Pruebas asíncronas

Jasmine también permite probar código asíncrono utilizando done o promesas:

Código a evaluar de función que solicita los datos de la localidad 3: "Citadel of Ricks" en la API de Rick y Morty.

// asyncAwaitFetch-RickMorty.js
// Función para obtener la ubicación (Citadel of Ricks)
export async function obtenerUbicacion() {
	try {
		Utils.mostrarResultado(idResultado, 'Solicitando ubicación a la API de Rick y Morty...'); // debería ser console.log
		const respuesta = await fetch('https://rickandmortyapi.com/api/location/3');
		if (!respuesta.ok) {
			throw new Error(`Error HTTP: ${respuesta.status}`);
		}
		const ubicacion = await respuesta.json();

		Utils.mostrarResultado(idResultado,'Ubicación obtenida: ' + ubicacion); // ${JSON.stringify(ubicacion)} mostrar respuesta completa
		return ubicacion;
	} catch (error) {
		Utils.mostrarResultado(idResultado,'Error al obtener la ubicación: ' + error); // debería ser console.error
		throw error;
	}
}

Prueba en test/jasmine/asyncAwaitFetch-RickMorty.spec.js

import { obtenerUbicacion } from '../../js/asyncAwaitFetch-RickyMorty.js';
import Utils from '../../js/utils.js';


// asyncAwaitFetch-RickMorty.spec.js
//prueba sólo a la primera solicitud obtenerUbicación
describe("Pruebas para la función obtenerUbicacion", () => {
	let originalFetch;
	let originalMostrarResultado;
	const idResultado = 'resultado-Rick-y-Morty';

	beforeEach(() => {
		// Guardamos los métodos originales para restaurarlos luego
		originalFetch = global.fetch;
		originalMostrarResultado = Utils.mostrarResultado;

		// Mock de la función fetch
		global.fetch = jasmine.createSpy("fetch");

		// Mock de Utils.mostrarResultado
		Utils.mostrarResultado = jasmine.createSpy("mostrarResultado");
	});

	afterEach(() => {
		// Restauramos los métodos originales después de cada prueba
		global.fetch = originalFetch;
		Utils.mostrarResultado = originalMostrarResultado;
	});

	it("debería obtener la ubicación correctamente y llamar a mostrarResultado con la ubicación", async () => {
		const mockResponse = {
			ok: true,
			json: async () => ({
				id: 3,
				name: "Citadel of Ricks",
				type: "Space station",
				dimension: "Unknown dimension"
			})
		};

		global.fetch.and.returnValue(Promise.resolve(mockResponse));

		const ubicacion = await obtenerUbicacion();

		// Verifica que fetch fue llamado con la URL correcta
		expect(global.fetch).toHaveBeenCalledWith("https://rickandmortyapi.com/api/location/3");

			// Verifica que mostrarResultado se llamó con el mensaje correcto para la ubicación obtenida
			expect(Utils.mostrarResultado).toHaveBeenCalledWith(
			'resultado-Rick-y-Morty',
			"Ubicación obtenida: [object Object]"
		);

		// Verifica que la función devolvió la ubicación correcta
		expect(ubicacion).toEqual(await mockResponse.json());
	});

	it("debería manejar un error HTTP y llamar a mostrarResultado con el mensaje de error", async () => {
		const mockErrorResponse = {
			ok: false,
			status: 404
		};

		global.fetch.and.returnValue(Promise.resolve(mockErrorResponse));

		try {
			await obtenerUbicacion();
			fail("Se esperaba que lanzara un error HTTP");
		} catch (error) {
			// Verifica que se lanzó el error esperado
			expect(error.message).toBe("Error HTTP: 404");

			// Verifica que fetch fue llamado con la URL correcta
			expect(global.fetch).toHaveBeenCalledWith("https://rickandmortyapi.com/api/location/3");

			// Verifica que mostrarResultado se llamó con el mensaje de error
			expect(Utils.mostrarResultado).toHaveBeenCalledWith(
				idResultado,
				"Error al obtener la ubicación: " + error
			);
		}
	});

	it("debería manejar un error de red y llamar a mostrarResultado con el mensaje de error", async () => {
		const networkError = new Error("Fallo en la red");
		global.fetch.and.returnValue(Promise.reject(networkError));

		try {
			await obtenerUbicacion();
			fail("Se esperaba que lanzara un error de red");
		} catch (error) {
			// Verifica que se lanzó el error esperado
			expect(error.message).toBe("Fallo en la red");

			// Verifica que fetch fue llamado con la URL correcta
			expect(global.fetch).toHaveBeenCalledWith("https://rickandmortyapi.com/api/location/3");

			// Verifica que mostrarResultado se llamó con el mensaje de error
			expect(Utils.mostrarResultado).toHaveBeenCalledWith(
				idResultado,
				"Error al obtener la ubicación: " + error
			);
		}
	});
});

Ejecuta npm run test o npm run test:jasmine en la terminal para ejecutar las pruebas.

¿Qué hace la función?

  1. La función obtenerUbicacion realiza una solicitud HTTP GET a la API de Rick y Morty para obtener información sobre la ubicación con ID 3 (Citadel of Ricks).
  2. Maneja tres escenarios principales:
    • Respuesta exitosa de la API: Muestra el mensaje "Ubicación obtenida: [object Object]" con el resultado de la solicitud.
    • Error HTTP (por ejemplo, código de estado 404): Lanza un mensaje de error como "Error HTTP: 404".
    • Error de red (por ejemplo, fallo de conexión): Lanza un mensaje como "Error al obtener la ubicación: Fallo en la red".
  3. Usa Utils.mostrarResultado para mostrar resultados o errores en la interfaz de usuario.

Resultado de la ejecución de las pruebas:

npm run test:jasmine   

	> fundametos-de-desarrollo-web@1.0.0 test:jasmine
	> jasmine
	
	Randomized with seed 37758
	Started
	.............
	
	
	13 specs, 0 failures
	Finished in 0.023 seconds
	Randomized with seed 37758 (jasmine --random=true --seed=37758)

Ejemplo completo de prueba unitaria con Jasmine

En el siguiente ejemplo vamos a hacer un Mock con Jasmine para hacer una prueba unitaria que verifique el correcto funcionamiento del ejemplo de async await con fetch para hacer una solicitud a la API de Rick y Morty.

Para poder simular el DOM recuerda que podemos utilizar JSDOM ejecutando en la terminal: npm install --save-dev jsdom

En archivo: asyncAwait-RickMorty.js


// En archivo: asyncAwait-RickMorty.js
// --- Código a probar (simulación del módulo) ---
import Utils from './utils.js';
const idResultado = 'resultado-Rick-y-Morty';
const idCode = 'code-Rick-y-Morty-mostrar';

// Función para generar un color aleatorio para cada card
function generarColorAleatorio() {
	const letras = '0123456789ABCDEF';
	let color = '#';
	for (let i = 0; i < 6; i++) {
		color += letras[Math.floor(Math.random() * 16)];
	}
	return color;
}

// Función para obtener la ubicación (Citadel of Ricks)
async function obtenerUbicacion() {
	try {
		Utils.mostrarResultado(idResultado, 'Solicitando ubicación a la API de Rick and Morty...');
		const respuesta = await fetch('https://rickandmortyapi.com/api/location/3');
		if (!respuesta.ok) {
			throw new Error(`Error HTTP: ${respuesta.status}`);
		}
		const ubicacion = await respuesta.json();
		Utils.mostrarResultado(idResultado, 'Ubicación obtenida: ' + JSON.stringify(ubicacion));
		return ubicacion;
	} catch (error) {
		Utils.mostrarResultado(idResultado, 'Error al obtener la ubicación: ' + error);
		throw error;
	}
}

// Función para obtener la información de cada residente usando Promise.all
async function obtenerResidentes(urls) {
	try {
		const fetchPromises = urls.map(url => fetch(url).then(res => {
			if (!res.ok) {
				throw new Error(`Error HTTP en ${url}: ${res.status}`);
			}
			return res.json();
		}));
		const residentes = await Promise.all(fetchPromises);
		Utils.mostrarResultado(idResultado, 'Residentes obtenidos: ' + residentes.map(residente => `${residente.id} ${residente.name}`).join(', '));
		return residentes;
	} catch (error) {
		Utils.mostrarResultado(idResultado, 'Error al obtener los residentes: ' + error);
		throw error;
	}
}

// Función para crear y mostrar las cards de cada residente
function mostrarCards(residentes) {
	const container = document.getElementById('container-Rick-y-Morty');
	residentes.forEach(residente => {
		const card = document.createElement('div');
		card.classList.add('card');
		card.style.backgroundColor = generarColorAleatorio();
		card.innerHTML = `
			<img src="${residente.image}" alt="${residente.name}">
			<h5>ID: ${residente.id}</h5>
			<h4>${residente.name}</h4>
			<p><strong>Location:</strong> ${residente.location.name}</p>
			<p><strong>Status:</strong> ${residente.status}</p>
		`;
		container.appendChild(card);
	});
}

// Función principal que orquesta la solicitud y muestra los resultados
async function ejecutarEjemploRickYMorty() {
	try {
		const container = document.getElementById('container-Rick-y-Morty');
		container.innerHTML = ''; // Limpiar contenedor
		const ubicacion = await obtenerUbicacion();
		const residentes = await obtenerResidentes(ubicacion.residents);
		mostrarCards(residentes);
		Utils.mostrarResultado(idResultado, 'Ejemplo ejecutado correctamente.');
	} catch (error) {
		Utils.mostrarResultado(idResultado, 'Error: ' + error);
	} finally {
		const codigo = ''; // Código para mostrar en el editor (si se desea)
		Utils.mostrarCodigo(idCode, codigo);
	}
}

// --- Fin del código a probar ---

En Archivo: asyncAwaitFetch-completoRickyMorty.spec

import { JSDOM } from 'jsdom';
import { ejecutarEjemploRickYMorty } from '../../js/asyncAwaitFetch-RickyMorty.js';
import Utils from '../../js/utils.js';

describe("Pruebas para la función ejecutarEjemploRickYMorty", () => {
	let originalFetch;
	let originalMostrarResultado;
	let originalMostrarCodigo;
	let dom;

	beforeEach(() => {
		// Simula un DOM con jsdom
		dom = new JSDOM(`
`); global.document = dom.window.document; global.window = dom.window; // Mock de métodos originales originalFetch = global.fetch; originalMostrarResultado = Utils.mostrarResultado; originalMostrarCodigo = Utils.mostrarCodigo; // Mock de Utils Utils.mostrarResultado = jasmine.createSpy("mostrarResultado"); Utils.mostrarCodigo = jasmine.createSpy("mostrarCodigo"); }); afterEach(() => { // Restauramos métodos originales global.fetch = originalFetch; Utils.mostrarResultado = originalMostrarResultado; Utils.mostrarCodigo = originalMostrarCodigo; delete global.document; delete global.window; }); it("debería ejecutar correctamente la función y mostrar los resultados", async () => { // Mock de fetch para obtener ubicación y residentes global.fetch = jasmine.createSpy("fetch").and.callFake((url) => { if (url === "https://rickandmortyapi.com/api/location/3") { return Promise.resolve({ ok: true, json: async () => ({ residents: ["https://rickandmortyapi.com/api/character/1"] }) }); } else if (url === "https://rickandmortyapi.com/api/character/1") { return Promise.resolve({ ok: true, json: async () => ({ id: 1, name: "Rick Sanchez", status: "Alive", location: { name: "Earth" }, image: "https://rickandmortyapi.com/api/character/avatar/1.jpeg" }) }); } }); // Ejecuta la función principal await ejecutarEjemploRickYMorty(); // Verifica que se generó correctamente una card const cards = document.getElementsByClassName('card'); expect(cards.length).toBe(1); expect(cards[0].querySelector('h4').textContent).toBe("Rick Sanchez"); // Verifica que Utils.mostrarResultado se llamó con éxito expect(Utils.mostrarResultado).toHaveBeenCalledWith( 'resultado-Rick-y-Morty', 'Ejemplo ejecutado correctamente.' ); }); it("debería manejar errores al obtener la ubicación", async () => { // Mock de fetch para fallar al obtener la ubicación global.fetch = jasmine.createSpy("fetch").and.returnValue( Promise.reject(new Error("Fallo en obtener la ubicación")) ); // Ejecuta la función principal await ejecutarEjemploRickYMorty(); // Verifica que Utils.mostrarResultado se llamó con el error expect(Utils.mostrarResultado).toHaveBeenCalledWith( 'resultado-Rick-y-Morty', 'Error: Error: Fallo en obtener la ubicación' ); }); it("debería manejar errores al obtener residentes", async () => { // Mock de fetch para obtener ubicación, pero fallar al obtener residentes global.fetch = jasmine.createSpy("fetch").and.callFake((url) => { if (url === "https://rickandmortyapi.com/api/location/3") { return Promise.resolve({ ok: true, json: async () => ({ residents: ["https://rickandmortyapi.com/api/character/1"] }) }); } else if (url === "https://rickandmortyapi.com/api/character/1") { return Promise.reject(new Error("Fallo en obtener residentes")); } }); // Ejecuta la función principal await ejecutarEjemploRickYMorty(); // Verifica que Utils.mostrarResultado se llamó con el error expect(Utils.mostrarResultado).toHaveBeenCalledWith( 'resultado-Rick-y-Morty', 'Error: Error: Fallo en obtener residentes' ); }); });

Ejecuta npm run test o npm run test:jasmine en la terminal para ejecutar las pruebas.

¿Qué hace la función?

  1. Simula un DOM para las pruebas: La función utiliza jsdom para crear un entorno DOM virtual, generando un contenedor llamado container-Rick-y-Morty. Esto permite probar interacciones con el DOM, como la creación de cards.
  2. Obtiene datos de la API de Rick and Morty: La función realiza solicitudes a dos endpoints principales:
    • /api/location/3: Para obtener los detalles de la ubicación llamada "Citadel of Ricks" y sus residentes.
    • /api/character/1: Para obtener información detallada sobre el residente "Rick Sanchez".
  3. Genera elementos visuales: Con la información obtenida de la API, crea y agrega cards en el contenedor del DOM. Cada card incluye datos como imagen, nombre, ID, ubicación y estado.
  4. Maneja errores: En caso de errores, ya sea al obtener la ubicación o los residentes, muestra mensajes de error usando Utils.mostrarResultado.
  5. Registra el progreso: Muestra mensajes como "Ejemplo ejecutado correctamente" cuando todo se ejecuta sin problemas, o mensajes detallados de error en caso contrario.

Las pruebas verifican la correcta ejecución de la función ejecutarEjemploRickYMorty en tres escenarios principales:

  • Ejecución exitosa: Comprueba que la función obtenga datos de la API y genere correctamente cards en el DOM.
  • Error al obtener la ubicación: Verifica cómo la función maneja fallos al realizar la solicitud a /api/location/3.
  • Error al obtener residentes: Valida que la función maneje adecuadamente errores durante la solicitud a /api/character/1.
Detalles clave del código
  1. Simulación de la API:
    • Se utiliza spyOn(window, 'fetch').and.callFake(...) para interceptar las llamadas a fetch y simular la respuesta de la API.
    • Se utiliza Promise.resolve para simular una respuesta exitosa de la API.
  2. Verificación de mensajes: Se utiliza expect(Utils.mostrarResultado).toHaveBeenCalledWith(...) para verificar que se hayan llamado a Utils.mostrarResultado con los mensajes esperados.
  3. Se utiliza JSDOM para simular el DOM para mostrarCards.
Comportamiento del test
  1. Se simula la respuesta de la API para la URL de la ubicación.
  2. Se ejecuta la función ejecutarEjemploRickYMorty.
  3. Se verifica que se hayan llamado a Utils.mostrarResultado con los mensajes esperados.
  4. La prueba pasa si todas las verificaciones son exitosas.
Explicación del Test con jasmine
  1. Configuración del Spy: Se utiliza spyOn(Utils, 'mostrarResultado').and.callThrough() para interceptar todas las llamadas a la función Utils.mostrarResultado y permitir su ejecución normal. De esta manera, podemos verificar que se hayan emitido los mensajes esperados durante la ejecución.
  2. Simulación de la API (Mocking de fetch): Con spyOn(window, 'fetch').and.callFake(...) se intercepta la llamada a fetch para la URL de la ubicación, devolviendo una respuesta simulada Promise.resolve(). Esto permite controlar el flujo sin realizar llamadas reales a la API.
  3. Ejecución y Verificación: Se ejecuta ejecutarEjemploRickYMorty() y se verifica mediante expectativas (expect) que se hayan llamado a Utils.mostrarResultado con mensajes que incluyan "Solicitando ubicación", "Ubicación obtenida" y "Ejemplo ejecutado correctamente". Esto confirma que la función se comporta como se espera.
> fundametos-de-desarrollo-web@1.0.0 test:jasmine
> jasmine

Randomized with seed 30286
Started
................


16 specs, 0 failures
Finished in 0.172 seconds
Randomized with seed 30286 (jasmine --random=true --seed=30286)

Considera que se muestran el archivo asyncAwaitFetch-RickyMorty.js y asyncAwaitFetch-completoRickyMorty.spec.js para hacer el ejemplo de Jasmine que requiere el package.json configurado correctamente.

Generar reporte de pruebas

Jasmine también se integra con herramientas para generar reportes de pruebas. Configura tu reporter favorito en jasmine.mjs.

  1. Instalar un paquete de reporte (opcional pero recomendado):
    npm install jasmine-spec-reporter --save-dev
  2. Configurar jasmine.mjs
    export default {
    	spec_dir: "test/jasmine",
    	spec_files: [
    		"**/*[sS]pec.?(m)js"
    	],
    	helpers: [
    		"helpers/**/*.?(m)js" // Debemos crear el archivo de configuraciónes adicionales
    	],
    	env: {
    		stopSpecOnExpectationFailure: false,
    		random: true,
    		forbidDuplicateNames: true
    	}
    }
    	
  3. Configura el archivo reporter.mjs
    Crea el archivo helpers/reporter.mjs con el siguiente contenido para integrar jasmine-spec-reporter:
    import { SpecReporter } from 'jasmine-spec-reporter';
    
    // Configuración de SpecReporter para resultados en consola
    jasmine.getEnv().clearReporters(); // Limpia los reportes predeterminados
    jasmine.getEnv().addReporter(new SpecReporter({
    	spec: {
    	displayStacktrace: "pretty"
    	}
    }));
  4. Ejecuta los test con npm run test o npm run test:jasmine
npm run test:jasmine   

> fundametos-de-desarrollo-web@1.0.0 test:jasmine
> jasmine

Jasmine started

	Pruebas para la función ejecutarEjemploRickYMorty
	√ debería ejecutar correctamente la función y mostrar los resultados
	√ debería manejar errores al obtener la ubicación
	√ debería manejar errores al obtener residentes

	Funciones de Orden Superior para Arrays

	ejecutarArrayReduce
		√ debería reducir el array sumando sus elementos

	ejecutarArraySome
		√ debería verificar si al menos un elemento es par

	ejecutarArrayMap
		√ debería mapear el array y multiplicar cada elemento por 2

	ejecutarArrayFilter
		√ debería filtrar los elementos pares del array

	ejecutarForEach
		√ debería recorrer el array y mostrar los elementos con índices

	ejecutarArraySort
		√ debería ordenar el array de forma ascendente

	ejecutarArrayFindIndex
		√ debería devolver el índice del primer elemento mayor a 10

	ejecutarArrayFind
		√ debería encontrar el primer elemento mayor a 10

	ejecutarArrayEvery
		√ debería verificar si todos los elementos son pares

	ejecutarArrayFlatMap
		√ debería aplanar el array después de duplicar cada elemento

	Pruebas para la función obtenerUbicacion
	√ debería manejar un error de red y llamar a mostrarResultado con el mensaje de error
	√ debería obtener la ubicación correctamente y llamar a mostrarResultado con la ubicación
	√ debería manejar un error HTTP y llamar a mostrarResultado con el mensaje de error

Executed 16 of 16 specs SUCCESS in 0.184 sec.
Randomized with seed 30399.

El resultado de todas las pruebas unitarias ejecutando npm run test es el siguiente:

npm run test

> fundametos-de-desarrollo-web@1.0.0 test
> npm run test:jasmine && npm run test:jest


> fundametos-de-desarrollo-web@1.0.0 test:jasmine
> jasmine

Jasmine started

	Pruebas para la función obtenerUbicacion
	√ debería manejar un error de red y llamar a mostrarResultado con el mensaje de error
	√ debería obtener la ubicación correctamente y llamar a mostrarResultado con la ubicación
	√ debería manejar un error HTTP y llamar a mostrarResultado con el mensaje de error

	Pruebas para la función ejecutarEjemploRickYMorty
	√ debería manejar errores al obtener residentes
	√ debería manejar errores al obtener la ubicación
	√ debería ejecutar correctamente la función y mostrar los resultados

	Funciones de Orden Superior para Arrays

	ejecutarArrayFlatMap
		√ debería aplanar el array después de duplicar cada elemento

	ejecutarArrayFindIndex
		√ debería devolver el índice del primer elemento mayor a 10

	ejecutarArrayFind
		√ debería encontrar el primer elemento mayor a 10

	ejecutarArrayEvery
		√ debería verificar si todos los elementos son pares

	ejecutarArraySort
		√ debería ordenar el array de forma ascendente

	ejecutarArrayMap
		√ debería mapear el array y multiplicar cada elemento por 2

	ejecutarArraySome
		√ debería verificar si al menos un elemento es par

	ejecutarArrayFilter
		√ debería filtrar los elementos pares del array

	ejecutarArrayReduce
		√ debería reducir el array sumando sus elementos

	ejecutarForEach
		√ debería recorrer el array y mostrar los elementos con índices

Executed 16 of 16 specs SUCCESS in 0.216 sec.
Randomized with seed 66126.

> fundametos-de-desarrollo-web@1.0.0 test:jest
> jest

	PASS  test/jest/EjemploPOO.test.js (8.959 s)
	PASS  test/jest/utils.test.js (10.3 s)
	PASS  test/jest/Arrays.test.js (10.572 s)
----------------|---------|----------|---------|---------|-------------------
File            | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
----------------|---------|----------|---------|---------|-------------------
All files       |     100 |    91.66 |     100 |     100 |                   
   js           |     100 |    91.66 |     100 |     100 |                   
   Arrays.js    |     100 |     87.5 |     100 |     100 | 152               
   utils.js     |     100 |      100 |     100 |     100 |                   
   js/POO       |     100 |      100 |     100 |     100 | 
   EjemploPOO.js|     100 |      100 |     100 |     100 | 
----------------|---------|----------|---------|---------|-------------------

Test Suites: 3 passed, 3 total
Tests:       24 passed, 24 total
Snapshots:   0 total
Time:        14.58 s
Ran all test suites.