Usando selectores correctos en Angular Testing Library
Tags: testing, angular, javascript
Published: 2022-04-28 14:19:00

> Traducción en español del artículo original de Tim Deschryver [Making sure you're using the correct query](https://timdeschryver.dev/blog/making-sure-youre-using-the-correct-query) publicado el 28 marzo 2022

Uno de los principales puntos de enfoque de Testing Library es la accesibilidad. nosotros queremos asegurarnos de que los tests sean fáciles de escribir manteniendo la experiencia del usuario en primer lugar. En lugar de seleccionar elementos DOM a través de sus atributos o clases de identificación, Testing Library utiliza selectores `querys` que son fáciles de usar.

En las últimas versiones de DOM Testing Library y también de Angular Testing Library (versión 9), se han realizado mejoras en estos selectores. Aparece un selector en particular (`get|find|query`) ByRole.

Info la documentación:

> Se puede usar para buscar cada elemento que se expone en el [árbol de accesibilidad](https://developer.mozilla.org/en-US/docs/glossary/accessibility_treehttps://developer.mozilla.org/en-US/docs/glossary/accessibility_tree) usando opción de `name` puede buscar los elementos devueltos por su nombre accesible. 

> Esta debería ser nuestra principal forma de buscar para casi todo. No hay nada que no se pueda conseguir con esto. (si no puedes, es posible tu interfaz tiene problemas de accesibilidad). En la mayoría de los casos la utilizaras de la siguiente manera:  `getByRole('button', {name: /submit/i})`. Si quieres saber más puedes ver la [lista de roles.](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques#roles)

La respuesta corta a la pregunta "qué selector debo usar" en la mayoría de las veces es el selector *ByRole*, hay algunos casos en los que este selector no podrá encontrar el elemento, afortunadamente Testing Library proporciona un par de formas de encontrar un selector alternativo o "fallback". Antes de echar un vistazo a cómo a las alternativas, echemos un vistazo a otros beneficios, además de la accesibilidad, que resuelve *ByRole*.

## Soluciones de *ByRole* 

### Seleccionar texto roto o con multiple tags

Los selectores *ByText y *ByLabelText* no pueden encontrar elementos que se dividen en varios elementos. Por ejemplo, dado el siguiente código HTML, es posible consultar el texto "Hello World".

```html
<h3>Hello <span>World</span></h3>
```
Se puede resolver con la consulta *ByRole* de la siguiente manera:

```typescript
// Before 
screen.getByText(/hello world/i)  
// |>Error: TestingLibraryElementError: Unable to find an element with the text: /hello world/i. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.  
// Using getByRole in version 9 
screen.getByRole('heading',  { name:  /hello world/i  })  
// |> HTMLHeadingElement
```
### Buscando multiple elementos
Hubo momentos en que el selector usamos principalmente fue `GetByText`  y este retorna más de un elemento. Esto provoca que algunas de nuestros tests fueran frágiles, que es lo que queremos evitar.
```html
<h3>Login now</h3>
<button>Login</button>
```
Por ejemplo, para encontrar el botón de Login en el HTML anterior, seria de la siguiente forma:

```typescript
const logins = screen.getAllByText(/login/i) 
const loginButton = logins[1]
```
Este test fallaría cuando se agregara un nuevo texto de "Login" en el componente. Esto no es bueno, ya que Testing Library tiene como objetivo escribir test mantenibles. Un mejor enfoque para este problema era utilizar la opción de selector.

```typescript
const loginButton = screen.getByText(/login/i,  { selector:  'button'  })
```
Para este caso simple, este enfoque está bien. Pero esto podría causar problemas si se utilizan clases CSS como selector. Debido a que el selector *ByRole* es más explícita, reduce la posibilidad de encontrar varios elementos.

Si echamos un vistazo a la opción del selector, se parece a la consulta *ByRole* pero forzado usando con el getBy y selector. El selector *ByRole* es más robusta ya que no es demasiado específica, ejemplo: es posible encontrar etiquetas de encabezado con 'heading' en lugar de las etiquetas `h1` a `h6`.

### TIPS PARA ENCONTRAR EL SELECTOR CORRECTO
Lamentablemente, el *ByRole no es la solución a todos tus problemas. Hay escenarios en los que no se puede utilizar la consulta *ByRole*. Un ejemplo de esto son los elementos sin un rol, ejemplo. un campo de tipo `password`. Pero no nos preocuparemos, los selectores originales aún se pueden usar y también se aseguran de que la aplicación sea accesible.

Para encontrar el selector que necesita, hay varios recursos para usar.

#### La documentación
El sitio web de Testing Library tiene su propia página sobre [los selectores](https://testing-library.com/docs/queries/about/#priority). Si no has leído esto, te recomiendo que lo leas. Es un breve resumen, pero definitivamente lo ayudará a escribir mejores tests.

### Testing Library Playground
[Stephan Meijer](https://twitter.com/meijer_s) creó [testing-playground](https://testing-playground.com/) para ayudar a encontrar el "mejor" selector disponible. Para usarlo, copie y pegue el html en el editor y haga clic en los elementos representados o escriba los querys usted mismo.

![Probando playground sugiriendo usar otra consulta](https://timdeschryver.dev/blog/making-sure-youre-using-the-correct-query/images/testing-playground.png)

> Tambien esta disponible la extension [Chrome](https://chrome.google.com/webstore/detail/testing-playground/hejbmebodbijjdhflfknehhcgaklhano) y [Firefox](https://addons.mozilla.org/en-US/firefox/addon/testing-playground/#:~:text=Testing%20Playground%20is%20an%20extension,while%20encouraging%20good%20testing%20practices.)

### Método configure de Testing Library
Recientemente se refactorizaron los [ejemplos de Angular Testing Library](https://github.com/testing-library/angular-testing-library/pull/101/commits/8acaebb51ac2093768fad058fc150d63ca667e34) para hacer uso de los selectores *ByRole*. Hice esto usando la opción recién agregada *throwSuggestions* al método de configure. Esta desactivado por defecto, pero es posible activarlo a nivel global o por selector. Como su nombre lo indica, arrojará un error si hay una consulta mejor/más segura disponible.

Como ejemplo, podemos tomar el siguiente HTML y hacer el test de hacer clic en el botón de Increment.

```html
<button (click)="value = value - 1">Decrement</button>  
<span data-testid="value">{{ value }}</span>  
<button (click)="value = value + 1">Increment</button>
```
```typescript
import  { render, screen, fireEvent, configure }  from  '@testing-library/angular'  
import  { CounterComponent }  from  './counter.component'  
	
	beforeEach(()  =>  {  
		configure({ throwSuggestions:  true,  }) 
	})  
	test('renders the current value and can increment',  async  ()  =>  {  
		await  render(CounterComponent)  
		const incrementControl = screen.getByText('Increment')
		fireEvent.click(incrementControl) 
	})
```

Debido a que `throwSuggestions` está activado, obtenemos el siguiente error mientras ejecutamos él test.

```typescript
TestingLibraryElementError: A better query is available, try this: 
getByRole("button", {name: /increment/i})
```
Testing Library nos brinda un mejor selector que podemos copiar y pegar en el test para reemplazar el uso de selectores "incorrectos".

```typescript
test('renders the current value and can increment',  
async  ()  =>  {  
	await  render(CounterComponent)  
	const incrementControl = screen.getByRole('button', { name: /increment/i }) 
	fireEvent.click(incrementControl) 
})
```
¿Simple verdad? Para activarlo o desactivarlo para un selector específico podemos hacer lo siguiente, que tendrá el mismo resultado.

```typescript
    test('renders the current value and can increment', async () => {
      await render(CounterComponent)
     const incrementControl = screen.getByText('Increment', { suggest: true })  
     fireEvent.click(incrementControl)
    })
```

### Mensajes de Testing Library

Testing Library muestra un mensaje útil cuando no encuentra un elemento en el selector *ByRole* . Veamos el ejemplo a continuación, donde cada elemento accesible se registra con su selector correspondiente.

```typescript
TestingLibraryElementError: Unable to find an accessible element with the role "textbox" and name `/increment/i`
Here are the accessible roles:

    button:

    Name "Decrement":
    <button />

    Name "Increment":
    <button />

    Name "Login":
    <button />

    --------------------------------------------------
    heading:

    Name "Login now":
    <h3 />

    --------------------------------------------------
```
## TIPS PARA ANGULAR TESTING LIBRARY

### screen
En versiones anteriores de Angular Testing Library, sólo era posible consultar elementos a través de objectos devueltos por el método render. En la versión 9 de Angular Testing Library, se exporta un objeto screen que tiene todos los selectores disponibles. Esto tiene como beneficio que tus tests serán más simples. Un segundo beneficio es que también busca elementos fuera del HTML del componente. Esto puede ser particularmente útil si estás usando Angular Material porque añadirán elementos al cuerpo del documento y fuera del árbol del componente.

```typescript
import { screen } from '@testing-library/angular'

test('renders the current value and can increment', async () => {
-  const { getByRole } = await render(CounterComponent)
+  await render(CounterComponent)

-  const incrementControl = getByRole('button', { name: /increment/i })
+  const incrementControl = screen.getByRole('button', { name: /increment/i })
})
```

### fireEvent
Para disparar eventos se recomienda utilizar el nuevo objeto fireEvent en lugar de utilizar los eventos devueltos por el método render. Los eventos disparados por fireEvent también ejecutarán un ciclo de detección de cambios, al igual que antes.

```typescript
import { fireEvent } from '@testing-library/angular'

test('renders the current value and can increment', async () => {
-  const { click } = await render(CounterComponent)
+  await render(CounterComponent)

-  click(incrementControl)
+  fireEvent.click(incrementControl)
})
```

### find
La versión 9 Testing Library, también arregla los selectores de búsqueda e invocará un ciclo de detección de cambios antes de invocar la consulta. Esto puede ser útil para el código asíncrono que impacta el DOM. Por ejemplo, si input de texto modificado y va a renderizar algo después. Mientras que antes había que llamar manualmente a detectChanges.
```typescript
- test('shows the load button after text input', fakeAsync(async () => {
+ test('shows the load button after text input', async () => {
  const { detectChanges } = await render(FixtureComponent, {
    imports: [ReactiveFormsModule],
  });

  userEvent.type(screen.getByRole('textbox'), 'What a great day!');

-  tick(200);
-  detectChanges();
-  screen.getByRole('button');
+  screen.findByRole('button');
-}));
+});
```
### Extras

Kent C. Dodds escribió [Errores comunes con React Testing Library](https://kentcdodds.com/blog/common-mistakes-with-react-testing-library) , la mayoría de los consejos también se aplican a Angular Testing Library. Así que, ¡léanlo para obtener más consejos útiles para escribir sus test con Testing Library!

> Nota: Si te ha gustado compartelo.

Foto de [Masjid Pogung Dalangan](https://unsplash.com/@masjidmpd?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) en [Unsplash](https://unsplash.com/es/s/fotos/pick?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)