Este tutorial lo guía, usando VSCode
, en la construcción de una aplicación Angular compuesta del módulo principal y de un módulo llamado BookModule
el cual declara un componente BookListComponent
para desplegar un catálogo de libros.
Una aplicación desarrollada en el framework Angular que despliega la lista descrita en la Figura 1.
Figura 1. Aplicación que se desarrolla en este tutorial. |
En particular Ud. debe:
Haga un fork al proyecto base indicado en el paso anterior y clone el repositorio bifurcado en su máquina local.
node_modules
en el nivel del root borrela.code .
npm install
desde la carpeta principal del proyectong s
Esto debe desplegar una aplicación por defecto que escucha peticiones en el puerto 4200. Para verificar abra un navegador y vaya a la URL http://localhost:4200
.
Para crear el nuevo módulo utilizamos la aplicación Angular Files
que está integrada dentro de VSCode
. Verifique que tiene instaladas las extensiones Angular Essentials (by John Papa) y Angular Files (by Alexander Ivanichev).
Si las extensiones están instaladas vaya a la carpeta src/app
, clic derecho, Generate Module
. El nombre del nuevo módulo es book
.
Al crear el nuevo módulo book
, se generó un componente por defecto BookComponent
.
Para eliminar ese componente que no vamos a utilizar debe borrar los siguientes archivos:
book.component.ts
book.component.css
book.component.html
Borre las referencias a este componente en el archivo book.module.ts
que debe quedar así:
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
@NgModule({
imports: [
CommonModule
],
declarations: []
})
export class BookModule { }
/* Archivo src/app/book/book.module.ts*/
Para que la aplicación pueda utilizar el nuevo módulo, este se debe importar en el módulo principal AppModule
.
Para importar en el módulo principal el módulo de BookModule
debe:
src/app/app.module.ts
@NgModule
e incluir en el arreglo del atributo imports
el nombre del módulo, es decir, de la clase BookModule
.Al final el archivo app.module.ts debe quedar así:
import { NgModule } from '@angular/core';
import { BrowserModule, provideClientHydration } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { BookModule } from './book/book.module';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
AppRoutingModule,
BookModule
],
providers: [
provideClientHydration()
],
bootstrap: [AppComponent]
})
export class AppModule { }
/* archivo src/app/app.module.ts */
En la carpeta del módulo src/app/book
, haga clic derecho Generate Component
y escriba por nombre book-list.
Dentro de la carpeta src/app/book
se debió crear una nueva carpeta para el componente book-list
(ver Figura 2).
Figura 2. Nuevo componente generado. |
BookModule
(archivo book.module.ts
) y agregue en el decorador, en el atributo declarations, el nombre del nuevo componente que en este caso es BookListComponent
. exports
cuyo valor es un arreglo que contiene el nombre del nuevo componente (lo exportamos porque lo vamos a utilizar desde el componente principal de la aplicación).El resultado es:
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { BookListComponent } from './book-list/book-list.component';
@NgModule({
imports: [
CommonModule
],
exports: [BookListComponent],
declarations: [BookListComponent]
})
export class BookModule { }
/* archivo src/app/book/book.module.ts*/
Para invocar el componente de los libros dentro del HTML del componente principal, tenemos que seguir los siguientes pasos:
BookListComponent
. Vamos al archivo book-list-component.ts
y en el decorador del componente buscamos el valor del atributo selector
. Para este caso el nombre es app-book-list
. app.component.html.
Para esto borramos el contenido del archivo app.component.html y agregamos una nueva etiqueta HTML con el valor correspondiente al nombre del nuevo componente, así:<app-book-list></app-book-list>
<!--archivo src/app/app.component.html -->
Al guardar los cambios y visualizar la página en el navegador obtenemos la salida descrita en la Figura 4. Significa que ya nuestra aplicación está invocando al componente de listar los libros y podemos seguir desarrollandolo.
src/app/book/book.module.ts
Figura 4. Salida del navegador. |
El modelo se refiere a la información que será desplegada o ingresada en o por la vista del componente y que será mantenida por el componente de forma sincronizada (binding). Significa que si el modelo cambia la vista se actualiza de forma automática.
En nuestro ejemplo, debemos definir las clases de la estructura de los libros. El diseño de esta estructura, obviamente, depende de la estructura de los objetos JSON que nos va a retornar el API Rest que estamos utilizando. En nuestro caso, los objetos que retorna el API REST al solicitar Get books
son los descritos en la Figura 5.
Figura 5. Objetos que retorna el API. |
En este ejemplo, solo vamos a crear la representación básica de Book
. Es decir, las clases que están encerradas en la línea amarilla: Book
y Editorial
.
La clase Book
la creamos dentro de la carpeta del módulo book. Para esto hacemos clic derecho sobre la carpeta book
> Generate Class y definimos el nombre book
. Esta clase define los atributos y su constructor.
import { Editorial } from "../editorial/editorial";
export class Book {
id: number;
name: string;
isbn: string;
description: string;
image: string;
publishingDate: any;
editorial: Editorial;
constructor(
id: number,
name: string,
isbn: string,
description: string,
image: string,
publishingDate: any,
editorial: Editorial
) {
this.id = id;
this.name = name;
this.isbn = isbn;
this.description = description;
this.image = image;
this.publishingDate = publishingDate;
this.editorial = editorial;
}
}
/* archivo src/app/book/book.ts */
Al crear la clase Book
aparece un error que indica que no se puede encontrar el nombre Editorial; por tanto, debemos crear la clase Editorial
. Para hacerlo de forma ordenada, debemos crear el módulo editorial
, dado que en nuestro ejemplo este es un módulo funcional distinto.
Creamos el módulo, utilizando el Angular Files
, que debe quedar en su propia carpeta. Borramos la referencia al componente y actualizamos el módulo. Luego, dentro la carpeta del módulo creamos la clase Editorial
. Los archivos del nuevo módulo deben quedar así:
Dentro del módulo creamos la clase Editorial
, que tiene dos atributos y el correspondiente constructor:
export class Editorial {
id: number;
name: string;
constructor(id: number, name: string) {
this.id = id;
this.name = name;
}
}
/*archivo src/app/editorial/editorial.ts*/
No hay que olvidar asociar el módulo EditorialModule
en el módulo principal AppModule
quien lo debe importar:
...
import { EditorialModule } from './editorial/editorial.module';
@NgModule({
...
imports: [
...
EditorialModule
],
...
/*archivo src/app/app.module.ts*/
Luego actualizamos la clase Book
incluyendo la referencia a la clase Editorial
para corregir el error.
Ahora que tenemos la clase que representa los libros, podemos declarar, dentro de la clase del componente book-list.component.ts
un arreglo para los libros:
books: Array<Book> = [];
Al incluir esta línea nos aparece que Book
no está definido, entonces debemos importarlo:
import { Component, OnInit } from '@angular/core';
import { Book } from '../book';
@Component({
selector: 'app-book-list',
templateUrl: './book-list.component.html',
styleUrls: ['./book-list.component.css']
})
export class BookListComponent implements OnInit {
books: Array<Book> = [];
constructor() { }
ngOnInit() {
}
}
Vamos a crear la clase del servicio desde la carpeta book
. De nuevo usamos Angular Files
y seleccionamos Generate Service.
Como nombre, solo escribimos book
ya que Angular Files
completa con la palabra Service.
En la clase BookService
inyectamos en el constructor un atributo privado http
de tipo HttpClient
e importamos el archivo de la clase correspondiente:
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Injectable({
providedIn: 'root'
})
export class BookService {
constructor(private http: HttpClient) { }
}
Antes de seguir, vamos al módulo principal AppModule
e incluimos en el atributo imports
del decorador del módulo, HttpClientModule
para que BookService
pueda usar HttpClient.
El módulo debe quedar de la siguiente forma:
import { NgModule } from '@angular/core';
import { BrowserModule, provideClientHydration } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { BookModule } from './book/book.module';
import { EditorialModule } from './editorial/editorial.module';
import { HttpClientModule } from '@angular/common/http';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
AppRoutingModule,
BookModule,
EditorialModule,
HttpClientModule
],
providers: [
provideClientHydration()
],
bootstrap: [AppComponent]
})
export class AppModule { }
Para esto se necesita conocer la URL donde está el servidor que provee los libros (el back-end).
El valor de la URL de base donde se encuentra el back-end lo vamos a declarar dentro del archivo environment.development.ts
que está en la carpeta environments
(ver Figura 6).
Si aún no tiene la carpeta de environments puede generarla con el siguiente comando:
ng generate environments
Figura 6. Carpeta environments. |
Luego de ejecutar el backend de ejemplo, este quedará desplegado en la siguiente dirección, que de acá en adelante conoceremos como la URL base (baseUrl):
http://localhost:8080/api/
De este modo, el archivo environment.development.ts
quedará así:
const baseUrl =
'http://localhost:8080/api/';
export const environment = {
production: false,
baseUrl
};
En la clase del servicio (book.service.ts
) vamos a importar la url del API definida en el archivo environment.development.ts
. Para eso agregamos este import:
import { environment } from '../../environments/environment.development';
Luego creamos un atributo de clase denominado apiUrl y lo inicializamos así:
private apiUrl: string = environment.baseUrl + 'books';
En el siguiente paso declaramos una función getBooks
() que va a utilizar el servicio http
para invocar el http.get
. Estas funciones de http retornan objetos Observable, entonces la declaración completa de la función es:
getBooks(): Observable<Book[]> {
return this.http.get<Book[]>(this.apiUrl);
}
Para completar este código debemos importar el archivo de Observable
y el archivo de Book
.
El código completo del servicio es el siguiente:
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { environment }from '../../environments/environment.development';
import { Observable } from 'rxjs';
import { Book } from './book';
@Injectable({
providedIn: 'root'
})
export class BookService {
private apiUrl: string = environment.baseUrl + 'books';
constructor(private http: HttpClient) { }
getBooks(): Observable<Book[]> {
return this.http.get<Book[]>(this.apiUrl);
}
}
Para completar el componente y que se puedan visualizar los libros, se debe importar el servicio book-service
en el componente de book-list
e invocarlo en el constructor:
...
import { BookService } from '../book.service';
...
constructor(private bookService: BookService) {}
...
Adicionalmente, se debe declarar el método getBooks(). Este método
usa el servicio book.service
para llamar al método getBooks()
dentro de la clase del servicio con el fin de suscribirse y asignar el valor de esa lista al Array
de books definido en el componente:
...
getBooks(): void {
this.bookService.getBooks().subscribe((books) => {
this.books = books;
});
}
...
Finalmente, se debe llamar al método getBooks()
dentro del método ngOnInit()
que se llama cuando se inicia el componente:
...
ngOnInit(): void {
this.getBooks();
}
Por lo tanto, el código del componente queda de la siguiente manera:
import { Component, OnInit } from '@angular/core';
import { Book } from '../book';
import { BookService } from '../book.service';
@Component({
selector: 'app-book-list',
templateUrl: './book-list.component.html',
styleUrls: ['./book-list.component.css']
})
export class BookListComponent implements OnInit {
books: Array<Book> = [];
constructor(private bookService: BookService) { }
getBooks(): void {
this.bookService.getBooks().subscribe((books) => {
this.books = books;
});
}
ngOnInit() {
this.getBooks();
}
}
Como ya hemos explicado, la vista es el HTML asociado con el componente. El objetivo es desplegar la lista de libros en una galería con las imágenes de las portadas de los libros.
Vamos a hacer primero un despliegue muy básico de las imágenes de los libros utilizando la grilla de Bootstrap. Cada imagen irá en una columna y debajo de la imagen va el nombre del libro. Las imágenes son responsive y todo está dentro de un container-fluid
.
Antes de hacer la vista es importante que instale la dependencia de bootstrap. Para esto ejecute en la terminal el comando npm install bootstrap
. Luego de la instalación vaya al archivo angular.json
, y en el atributo styles -línea 26- agregue el siguiente valor:
"node_modules/bootstrap/dist/css/bootstrap.min.css"
El archivo quedará así:
"tsConfig": "tsconfig.app.json",
"assets": ["src/favicon.ico", "src/assets"],
"styles": [
"src/styles.css",
"node_modules/bootstrap/dist/css/bootstrap.min.css"
],
"scripts": []
Para que los cambios surtan efecto debe parar el servidor web con la combinación de teclas CTRL + C en la terminar y volver a ejecutarlo con el comando ng serve (o ng s)
Ahora veamos el código de la vista (archivo book-list.component.html
), empezando solo con el HTML básico y el uso de Bootstrap. En el siguiente código HTML, tenemos un container-fluid, una fila y una columna con un margen en la parte inferior.
<div class="container-fluid">
<div class="row">
<div class="col mb-2">
</div>
</div>
</div>
Queremos crear una columna por cada libro que se encuentra en el arreglo books
. Entonces, necesitamos definir un ciclo sobre la etiqueta div
que define la columna para que itere sobre books y cree una nueva columna en cada iteración. También vamos a mostrar el nombre del libro en cada columna utilizando la expresión {{book.name}}
<div class="container-fluid">
<div class="row">
<div class="col mb-2" *ngFor="let book of books">
{{book.name}}
</div>
</div>
</div>
Repasemos la sintaxis para definir un ciclo dentro del HTML utilizando las directivas de Angular:
*ngFor= "let book of books"
La directiva *ngFor
es un atributo que se define en la etiqueta donde queremos iniciar el ciclo. El ciclo se termina donde se termina esa etiqueta. El valor del atributo debe definir:
book
books
que debe estar obligatoriamente definida en la clase del componente (en este caso, BookListComponent
).Ahora vamos a incluir la imagen del libro. Hay muchas formas de hacerlo, utilizando etiquetas como figure
o como card
.
Vamos a crear una etiqueta card
al interior de cada columna. Como se observa, se configura un padding (con la clase p-2) y un ancho y alto a cada card.
Adicionalmente, dentro de card
tenemos la etiqueta para la imagen, el atributo src donde define la URL de la imagen, el atributo alt contiene el texto alternativo que describe la imagen y que será mostrado cuando la imagen no pueda cargar. Finalmente, se tiene una etiqueta h5
y una etiqueta p
las cuales hacen parte del cuerpo de card
y sirven para mostrar el nombre del libro y su editorial respectivamente:
<div class="container-fluid">
<div class="row">
<div class="col mb-2" *ngFor="let book of books">
<div class="card p-2" style="width: 15rem; height: 33rem">
<img
class="card-img-top"
src="{{ book.image }}"
alt="{{ book.name }}"
/>
<div class="card-body">
<h5 class="card-title">{{ book.name }}</h5>
<p class="card-text">[{{ book.editorial.name }}]</p>
</div>
</div>
</div>
</div>
</div>
Dado que las imágenes en el back pueden tener tamaños diferentes y esto podría afectar la visualización vamos a incluir el siguiente código en el archivo book-list.component.css
que deja un tamaño fijo para cada imagen:
img {
height: 350px;
width: auto;
max-width: 400px;
}
Después de ejecutar el back, el nuevo despliegue es el que se observa en la Figura 7.
Figura 7. Aplicación final. |
En este punto nos vamos a enfocar en la implementación de pruebas End to End (E2E) utilizando Cypress.
Te recomendamos hacer este tutorial básico de Cypress antes de continuar: https://misovirtual.virtual.uniandes.edu.co/codelabs/ISIS2603_Angular_PruebasE2E/index.html#0
Si deseas información complementaria de Cypress, te sugerimos revisarla acá: https://www.cypress.io/
Ahora sí, continuemos con nuestra prueba la cual obtendrá dinámicamente los datos desde el backend mediante una solicitud GET a http://localhost:8080/api/books. La prueba hace lo siguiente:
Antes de comenzar, asegúrate de tener:
ng version
El comando creará en el proyecto una nueva carpeta denominada Cypress la cual contiene:
En la carpeta `cypress/e2e`, crea un archivo llamado `book-list.cy.ts` y agrega las siguientes pruebas:
describe('Prueba Book List', () => {
beforeEach(() => {
cy.visit('/')
})
let booksFromBackend: string | any[] = [];
it('get back',()=> {
cy.request('GET', 'http://localhost:8080/api/books')
.then((response) => {
expect(response.status).to.eq(200);
booksFromBackend = response.body;
cy.log(`Se obtuvieron ${booksFromBackend.length} libros del backend`);
});
});
it('should display the book list', () => {
cy.get('app-book-list').should('exist');
cy.log('La página de libros cargó correctamente');
});
it('should display the correct number of books', () => {
cy.get('div.col.mb-2')
.should('have.length', booksFromBackend.length)
.then(() => {
cy.log(`Se muestran ${booksFromBackend.length} libros en la vista`);
});
});
it('should have the correct number of <div.card.p-2> elements', () => {
cy.get('div.card.p-2').should('have.length', booksFromBackend.length);
});
it('should have the correct number of <img> elements', () => {
cy.get('img').should('have.length', booksFromBackend.length);
});
it('should have the correct number of <div.card-body> elements', () => {
cy.get('div.card-body').should('have.length', booksFromBackend.length);
});
it('should display correct image src and alt attributes', () => {
cy.get('img').each(($img, index) => {
cy.wrap($img)
.should('have.attr', 'src', booksFromBackend[index].image)
.should('have.attr', 'alt', booksFromBackend[index].name)
.then(() => {
cy.log(`Imagen correcta para el libro: ${booksFromBackend[index].name}`);
});
});
});
it('should have h5 tag with the book.name', () => {
cy.get('h5.card-title').each(($title, index) => {
cy.wrap($title)
.should('contain.text', booksFromBackend[index].name)
.then(() => {
cy.log(`Título correcto: ${booksFromBackend[index].name}`);
});
});
});
it('should have p tag with the book.editorial.name', () => {
cy.get('p.card-text').each(($p, index) => {
cy.wrap($p)
.should('contain.text', booksFromBackend[index].editorial.name)
.then(() => {
cy.log(`Editorial correcta para el libro ${booksFromBackend[index].name}: ${booksFromBackend[index].editorial.name}`);
});
});
});
//No se pueden borrar libros si tiene autor asociado
it('should correctly update the book list if a book is removed', () => {
cy.request('GET', 'http://localhost:8080/api/books')
.then((response) => {
expect(response.status).to.eq(200);
const books = response.body;
cy.log(`Total de libros antes de eliminar: ${books.length}`);
// Filtrar los libros que no tienen autores
const bookWithoutAuthors = books.find((book: { authors: string | any[]; }) => !book.authors || book.authors.length === 0);
if (!bookWithoutAuthors) {
cy.log('No hay libros sin autores disponibles para eliminar');
return;
}
cy.log(`Eliminando el libro: ${bookWithoutAuthors.name} (ID: ${bookWithoutAuthors.id})`);
// Intentamos eliminar el libro sin autores
cy.request({
method: 'DELETE',
url: `http://localhost:8080/api/books/${bookWithoutAuthors.id}`,
failOnStatusCode: false,
}).then((deleteResponse) => {
expect(deleteResponse.status).to.be.oneOf([200, 204]);
cy.log(`Libro eliminado con éxito: ${bookWithoutAuthors.name}`);
//Verificamos que el libro ya no esté en la lista
cy.reload();
cy.request('GET', 'http://localhost:8080/api/books')
.then((updatedResponse) => {
expect(updatedResponse.status).to.eq(200);
const updatedBooks = updatedResponse.body;
cy.log(`Total de libros después de eliminar: ${updatedBooks.length}`);
cy.get('div.col.mb-2').should('have.length', updatedBooks.length);
// Verificar que el libro eliminado ya no está en la vista
cy.get('div.col.mb-2').each(($book) => {
cy.wrap($book).should('not.contain.text', bookWithoutAuthors.name);
});
});
});
});
});
})
/* Archivo cypress/e2e/book-list.cy.ts */
Cypress permite interactuar con el DOM usando selectores CSS, similar a como lo hace Angular con By.css(). Utilizamos cy.get(selector) para seleccionar elementos y .should(condición) para hacer afirmaciones sobre ellos.
Para ejecutar la prueba debes:
Ahora desde VSCode, haz clic en la prueba ‘book-list' (como se ve en la captura que sigue) y ejecútala.
Ahora, analicemos todos los elementos de la prueba.
Luego de inicializar la configuración, se usa beforeEach, el cual se ejecuta antes de cada prueba. Aquí, Cypress visita la página de localhost (asignada como baseUrl en el archivo cypress.config.ts) para asegurarse de que estamos en el contexto correcto.
beforeEach(() => {
cy.visit('/')
})
La primera prueba que se realiza consiste en hacer una petición GET a http://localhost:8080/api/books para obtener la lista de libros. Se verifica que la respuesta tenga un código 200 (éxito). Se guarda la lista de libros en booksFromBackend para usarla en las pruebas y se muestra un mensaje en el log de Cypress con la cantidad de libros obtenidos.
it('get back',()=> {
cy.request('GET', 'http://localhost:8080/api/books')
.then((response) => {
expect(response.status).to.eq(200);
booksFromBackend = response.body;
cy.log(`Se obtuvieron ${booksFromBackend.length} libros del backend`);
});
});
Ahora analicemos las demás pruebas que se han definido. Recordemos que en la vista se hace una iteración usando la directiva *ngFor. Esta iteración se hará dependiendo del número de libros que haya en la variable books del componente. Por cada iteración se crea un div que contiene las clases col y mb-2:
<div *ngFor="let book of books" class="col mb-2">
Por tanto, en la primera prueba verificaremos que el componente de listar libros esté presente en la vista:
it('should display the book list', () => {
cy.get('app-book-list').should('exist');
cy.log('La página de libros cargó correctamente');
});
Para la segunda prueba, vamos a verificar que se muestra la cantidad de libros correctos. Para esto la prueba busca todos los elementos <div class="col mb-2"> en la interfaz (cada uno representa un libro) y verifica que su número coincida con la cantidad de libros obtenidos del backend.
it('should display the correct number of books', () => {
cy.get('div.col.mb-2')
.should('have.length', booksFromBackend.length)
.then(() => {
cy.log(`Se muestran ${booksFromBackend.length} libros en la vista`);
});
});
En la vista del componente tendremos por cada libro una tarjeta de bootstrap. Esta tarjeta se representa por un contenedor con las clases card y p-2.
<div class="card p-2" style="width: 15rem; height: 33rem">
Esa será entonces la siguiente prueba que especificaremos a continuación:
it('should have the correct number of <div.card.p-2> elements', () => {
cy.get('div.card.p-2').should('have.length', booksFromBackend.length);
});
Además, cada libro tiene una imagen asociada. Esto implica que habrá en la vista etiquetas img (una por cada libro).
<img
class="card-img-top"
src="{{ book.image }}"
alt="{{ book.name }}"
/>
La prueba se ve de la siguiente forma:
it('should have the correct number of <img> elements', () => {
cy.get('img').should('have.length', booksFromBackend.length);
});
Por cada libro tendremos también un contenedor en donde se renderizarán el nombre del libro y la editorial. El contenedor tiene asociada la clase card-body.
<div class="card-body">
Esa será entonces la siguiente prueba que especificaremos.
it('should have the correct number of <div.card-body> elements', () => {
cy.get('div.card-body').should('have.length', booksFromBackend.length);
});
Hasta acá hemos verificado la estructura del componente. No obstante, también debemos verificar que los datos específicos de cada libro se muestran de forma correcta en el componente.
Hemos indicado en la vista que para la imagen de cada libro en el atributo src
se asignará el contenido de la variable book.image y que en el atributo alt se asignará el contenido de la variable book.name.
La prueba que sigue verifica esa información. Nota que se recorre cada imagen y se verifica que su atributo src (URL de la imagen) coincida con el dato del backend y que el atributo alt contenga el nombre del libro:
it('should display correct image src and alt attributes', () => {
cy.get('img').each(($img, index) => {
cy.wrap($img)
.should('have.attr', 'src', booksFromBackend[index].image)
.should('have.attr', 'alt', booksFromBackend[index].name)
.then(() => {
cy.log(`Imagen correcta para el libro: ${booksFromBackend[index].name}`);
});
});
});
En la vista el nombre de cada libro aparece como un título de quinto nivel con la clase card-title:
<h5 class="card-title">{{ book.name }}</h5>
Además, la editorial de cada libro aparece como el contenido de un párrafo con la clase card-text:
<p class="card-text">[{{ book.editorial.name }}]</p>
Estas son las pruebas para verificar que los títulos y las editoriales coincidan con los datos del backend:
it('should have h5 tag with the book.name', () => {
cy.get('h5.card-title').each(($title, index) => {
cy.wrap($title)
.should('contain.text', booksFromBackend[index].name)
.then(() => {
cy.log(`Título correcto: ${booksFromBackend[index].name}`);
});
});
});
it('should have p tag with the book.editorial.name', () => {
cy.get('p.card-text').each(($p, index) => {
cy.wrap($p)
.should('contain.text', booksFromBackend[index].editorial.name)
.then(() => {
cy.log(`Editorial correcta para el libro ${booksFromBackend[index].name}: ${booksFromBackend[index].editorial.name}`);
});
});
});
Finalmente, queremos comprobar que si se elimina un elemento del arreglo books del componente, la vista se actualiza con ese cambio. La siguiente prueba verifica que la eliminación de libros en la aplicación funciona correctamente y que la lista se actualiza después de eliminar un libro sin autores (ya que esa es la regla establecida en el backend). Primero, se hace una petición GET al backend para obtener la lista de libros y se busca uno que no tenga autores asociados, ya que los libros con autores no pueden ser eliminados. Si no se encuentra un libro elegible, la prueba finaliza con un mensaje. Si se encuentra, se envía una petición DELETE para eliminarlo y se verifica que la respuesta del servidor tenga un estado 200 o 204, indicando éxito. Luego, se recarga la página y se hace una nueva petición GET para obtener la lista de libros actualizada, asegurando que la cantidad de libros mostrada en la interfaz coincida con la nueva lista del backend. También se comprueba que el libro eliminado ya no aparezca en la vista.
it('should correctly update the book list if a book is removed', () => {
cy.request('GET', 'http://localhost:8080/api/books')
.then((response) => {
expect(response.status).to.eq(200);
const books = response.body;
cy.log(`Total de libros antes de eliminar: ${books.length}`);
// Filtrar los libros que no tienen autores
const bookWithoutAuthors = books.find((book: { authors: string | any[]; }) => !book.authors || book.authors.length === 0);
if (!bookWithoutAuthors) {
cy.log('No hay libros sin autores disponibles para eliminar');
return;
}
cy.log(`Eliminando el libro: ${bookWithoutAuthors.name} (ID: ${bookWithoutAuthors.id})`);
// Intentamos eliminar el libro sin autores
cy.request({
method: 'DELETE',
url: `http://localhost:8080/api/books/${bookWithoutAuthors.id}`,
failOnStatusCode: false,
}).then((deleteResponse) => {
expect(deleteResponse.status).to.be.oneOf([200, 204]);
cy.log(`Libro eliminado con éxito: ${bookWithoutAuthors.name}`);
//Verificamos que el libro ya no esté en la lista
cy.reload();
cy.request('GET', 'http://localhost:8080/api/books')
.then((updatedResponse) => {
expect(updatedResponse.status).to.eq(200);
const updatedBooks = updatedResponse.body;
cy.log(`Total de libros después de eliminar: ${updatedBooks.length}`);
cy.get('div.col.mb-2').should('have.length', updatedBooks.length);
// Verificar que el libro eliminado ya no está en la vista
cy.get('div.col.mb-2').each(($book) => {
cy.wrap($book).should('not.contain.text', bookWithoutAuthors.name);
});
});
});
});
});