# Probar UIs

Esta sección describe cómo los desarrolladores probarán la base de código relacionada con la UI.

## Stack de pruebas

Todas nuestras pruebas de UI DEBEN realizarse usando [Jest](https://jestjs.io/) como el framework de pruebas principal con [redux-saga-test-plan](https://github.com/jfairbank/redux-saga-test-plan) como la herramienta de pruebas para sagas y [react testing library](https://testing-library.com/docs/react-testing-library/intro/) como herramienta para probar componentes de UI. El código de las pruebas DEBE escribirse usando [Typescript](https://www.typescriptlang.org/) y ejecutarse usando [ts-jest](https://github.com/kulshekhar/ts-jest) para tener soporte de comprobación de tipos.

## Qué probar

* Componentes de UI
* Sagas
* Reducers
* Selectors
* Creadores de acciones
* Funciones utilitarias

## Probando sagas

Las sagas nos permiten manejar efectos secundarios creando generadores que gestionan acciones de redux. Una de las principales ventajas de las sagas es su capacidad de ser probadas, ya que los efectos que se usan ampliamente en las sagas son altamente testeables.

Estos handlers DEBEN ser probados usando [redux-saga-test-plan](https://github.com/jfairbank/redux-saga-test-plan), y la prueba DEBE ejercer toda la ejecución, desde el handler del módulo completo (su handler principal o sagas) a través de toda la ejecución de los handlers que reaccionarán a la acción que vamos a dispatch. Este enfoque hace que las pruebas de sagas sean fáciles de construir y mantener, y aporta valor a las pruebas al recorrer tanto como sea posible del flujo completo.

Las pruebas de los handlers DEBEN realizarse a través de su interacción con los effects, principalmente con el `put` uno, ya que su propósito es manejar acciones y producir nuevas. La mayoría de las pruebas seguramente contendrán un `provide` call, usado para mockear effects, una o múltiples `put` calls, para verificar que los handlers dispatchen las acciones como se espera y un único `dispatch` seguido por un `run` call para ejecutar las sagas completas hasta la finalización.

El mocking de recursos externos al módulo que se está probando DEBERÍA hacerse usando los providers de redux-saga-test-plan aprovechando los distintos effects provistos por redux-sagas. Effects como call, apply u otros DEBERÍAN usarse siempre que sea posible cuando se invoca una función en una prueba para permitir mockearlos fácilmente. Jest NO DEBERÍA usarse para mockear módulos o funciones.

El texto en el describe y sus DEBEN seguir estas pautas:

* El describe principal de una saga o handler principal DEBE comenzar con *when handling*, indicando que lo que se va a probar es un handler. Tras la *when handling* frase, DEBE escribirse una descripción de la acción (o la intención que la acción) que se va a manejar. Ej. *when handling the action that signals a successful fetch.*
* Los describes internos diferenciarán contextos de ejecución, siguen las mismas reglas descritas en la [Describing and building context](/contributor/contributor-es/guias-para-colaboradores/testing-standards/writing-tests.md#describing-and-building-context) sección.
* El **it**s DEBEN describir lo que se espera de la prueba, comenzando con un *should put* y la descripción de las acciones que deberían ser puestas al ejecutar el handler.

### Ejemplo de sagas

```tsx
...
export function* tiersSaga(builder: BuilderAPI) {
	function* handleBuyThirdPartyItemTierRequest(action: BuyThirdPartyItemTiersRequestAction) {
    const { thirdParty, tier } = action.payload
    try {
			// Obtener el chain id puede fallar, se debe hacer una prueba para asegurar que este escenario se ejercita
      const maticChainId: ChainId = yield call(getChainIdByNetwork, Network.MATIC)
      const thirdPartyContract = yield call(getContract, ContractName.ThirdPartyRegistry, maticChainId)
			// Enviar la transacción puede tener éxito o fallar, ambos casos deben ejercitarse en una prueba
      const txHash: string = yield call(sendTransaction, thirdPartyContract, instantiatedThirdPartyContractContract =>
        instantiatedThirdPartyContractContract.buyItemSlots(thirdParty.id, tier.id, tier.price)
      )
      yield put(buyThirdPartyItemTiersSuccess(txHash, maticChainId, thirdParty, tier))
    } catch (error) {
      yield put(buyThirdPartyItemTiersFailure(error.message, thirdParty.id, tier))
    }
  }
}
```

### Ejemplo de prueba de sagas

```tsx
// El describe principal comienza con 'when handling...''
describe('when handling the request to buy a third party item tier', () => {
	// Se ejercitan todos los casos, negativos y positivos
  describe('and the chain id couldn\'t be retrieved', () => {
    it('should put the action signaling the failure of the purchase of an item slots tier with the tier and the third party id', () => {
      return expectSaga(tiersSaga, mockedBuilderApi)
				// Provide calls mockean los diferentes effects, construyendo diferentes escenarios
        .provide([[call(getChainIdByNetwork, Network.MATIC), Promise.reject(new Error(defaultError))]])
				// Las llamadas put verifican que lo que se esperaba como resultado final ocurrió
        .put(buyThirdPartyItemTiersFailure(defaultError, thirdParty.id, thirdPartyItemTier))
				// Se hace un único dispatch por prueba para ejercitar el manejo de la acción que estamos probando
        .dispatch(buyThirdPartyItemTiersRequest(thirdParty, thirdPartyItemTier))
        .run({ silenceTimeout: true })
    })
  })

  describe('and sending the transaction fails', () => {
    it('should put the action signaling the failure of the purchase of an item slots tier with the tier and the third party id', () => {
      return expectSaga(tiersSaga, mockedBuilderApi)
        .provide([
					// Call coincide con el effect call exacto, teniendo en cuenta los parámetros
					// Esto hace que nuestra prueba asegure que el código ejecutado antes de un put hizo lo que esperábamos
          [call(getChainIdByNetwork, Network.MATIC), ChainId.MATIC_MUMBAI],
          [call(getContract, ContractName.ThirdPartyRegistry, ChainId.MATIC_MUMBAI), contract],
					// Se usan matchers para coincidir solo con la función y no con los parámetros
          [matchers.call.fn(sendTransaction), Promise.reject(new Error(defaultError))]
        ])
        .put(buyThirdPartyItemTiersFailure(defaultError, thirdParty.id, thirdPartyItemTier))
        .dispatch(buyThirdPartyItemTiersRequest(thirdParty, thirdPartyItemTier))
        .run({ silenceTimeout: true })
    })
  })

  describe('and sending the transaction succeeds', () => {
    let contract: any
    beforeEach(() => {
      contract = { buyItemSlots: jest.fn() }
    })

    it('should put the action signaling the successful purchase of an item slots tier with the tier, the third party and the transaction details', () => {
      return expectSaga(tiersSaga, mockedBuilderApi)
        .provide([
          [call(getChainIdByNetwork, Network.MATIC), ChainId.MATIC_MUMBAI],
          [call(getContract, ContractName.ThirdPartyRegistry, ChainId.MATIC_MUMBAI), contract],
          [matchers.call.fn(sendTransaction), Promise.resolve(txHash)]
        ])
        .put(buyThirdPartyItemTiersSuccess(txHash, ChainId.MATIC_MUMBAI, thirdParty, thirdPartyItemTier))
        .dispatch(buyThirdPartyItemTiersRequest(thirdParty, thirdPartyItemTier))
        .run({ silenceTimeout: true })
    })
  })
})
```

## Probando reducers

Reducers son funciones que toman un state y una action y devuelven un nuevo state. Este nuevo state PUEDE contener cambios programados para ocurrir por la acción dada.

Las pruebas DEBERÍAN variar los estados iniciales y los diferentes parámetros de la acción a probar si es necesario. Las aserciones DEBEN hacerse sobre todo el state retornado ya que una acción podría modificar cualquier parte del state inicial dado.

Al reducir una acción, DEBERÍAMOS usar el creador de acciones, haciendo que las pruebas sean más fáciles de mantener.

Para estandarizar la forma en que escribimos **describe**s y **it**s:

* Los describes principales DEBERÍAN comenzar describiendo qué acciones van a ser reducidas usando la frase *when reducing the action* seguida de una descripción de la acción. NO DEBERÍAMOS usar el tipo de la acción para describir la acción a probar, ya que el type podría cambiar.
* El **it**s DEBERÍAN describir de manera clara cómo cambió el state retornado. Ej.: *it should return a state with the error nulled and the fruits set*.

### Ejemplo de reducer

```tsx
const INITIAL_STATE: FruitsState = {
  data: {
    fruits: []
  },
  loading: []
}

export function fruitsReducer(state = INITIAL_STATE, action: any): FruitsState {
  switch (action.type) {
    case FETCH_FRUIT_REQUEST: {
      return {
        ...state,
        loading: loadingReducer(state.loading, action),
				error: null
      }
    }
		case FETCH_FUIT_ERROR: {
			const { error } = action
      return {
        ...state,
				error,
        loading: loadingReducer(state.loading, action)
      }
		}
		case FETCH_FRUIT_SUCCESS: {
			const { fruits } = action
      return {
        ...state,
				fruits,
        loading: loadingReducer(state.loading, action)
      }
		}
}
```

### Ejemplo de prueba de Reducers

```tsx
let state: FruitsState

beforeEach(() => {
	state = {
	  data: {
	    fruits: []
	  },
	  loading: [],
		error: null
	}
})

describe('when reducing the action that signals the fetching of fruits', () => {
	beforeEach(() => {
		// Se establece un error para asegurar que se limpia al reducir la acción.
		state.error = 'anError'
	})

	it('should return a state where the error is cleared and the loading state is set to loading', () => {
		// Se usan creadores de acciones para probar el reducer para que pueda mantenerse con facilidad.
		const action = fetchFruitsRequest()

		expect(fruitsReducer(state, action)).toEqual({
			...state,
			error: null,
			/* Como el loading reducer forma parte de otro reducer (y no necesita ser probado),
				 PODRÍAMOS elegir usarlo tal cual o PODRÍAMOS usar un helper que lo emule.
			*/
			loading: loadingReducer(state.loading, action),
		})
	})
})

describe('when reducing the action that signals the failure of fetching fruits', () => {
	const error = 'anError'

	it('should return a state where the error is set and the loading state is cleared from the fruits fetch action', () => {
		const action = fetchFruitsFailure(error)

		expect(fruitsReducer(state, action)).toEqual({
			...state,
			error,
			loading: loadingReducer(state.loading, action),
		})
	})
})

describe('when reducing the action that signals the success of fetching fruits', () => {
	let fruits: string[]
	beforeEach(() => {
		fruits = ['apples', 'pears']
	})

	it('should return a state where the fruits are set and the loading reducer is cleared from the fruits fetch action', () => {
		const action = fetchFruitsSuccess(fruits)

		expect(fruitsReducer(state, action)).toEqual({
			...state,
			fruits,
			loading: loadingReducer(state.loading, action),
		})
	})
})
```

## Probando selectors

Selectors son métodos que toman un state y devuelven una sección de ese state o una sección transformada del state.

Hay dos tipos de selectors, selectors memoizados y selectors normales o comunes. Los selectors normales o comunes DEBEN probarse como cualquier otra función de unidad y los selectors memoizados DEBERÍAN probarse usando el `resultFunc` o la función que recibe las partes memoizadas del state y devuelve el valor deseado.

**Describe**s y **it**s DEBEN escribirse como se describe en la [Describing and building context](/contributor/contributor-es/guias-para-colaboradores/testing-standards/writing-tests.md#describing-and-building-context) sección.

### Ejemplo de Selectors

```tsx
// Selector común
export const collectionExists = (state: RootState, id: string): boolean => {
	return state.collections.data.some((collection) => collection.id === id)
}

// Selector memoizado usando reselect que devuelve una lista de collections enriquecidas
export const getEnhancedCollections = createSelector(
	getCollections,
	getExtraData,
	(collections, extraData) => {
		return collections.map((collection) =>
			({ ...collection, ...extraData[collection[id]] }))
	}
)
```

### Ejemplo de prueba de Selectors

```tsx
let state: RootState
let collection: { id: string }
beforeEach(() => {
	collection = {
		id: 'anId'
	}
	state = {
		collections: {
			data: []
		}
	}
})

beforeEach(() => {
	state.collections.data = [collection]
})

// Los selectors comunes se prueban como funciones unitarias normales
describe('when checking if a collection exists', () => {
	describe('and there\'s no collection with the given id', () => {
		it('should return false', () => {
			expect(collectionExists(state, "anotherId")).toBe(false)
		})
	})

	describe('and there\'s a collection with the given id', () => {
		it('should return true', () => {
			expect(collectionExists(state, collection.id)).toBe(true)
		})
	})
})

describe('when getting the enhanced collections', () => {
	let enhacements: Record<string, Enhacement>

	describe('and there\'s no data to enhance them', () => {
		beforeEach(() => {
			enhacements = {}
		})

		// Los selectors memoizados deberían probarse unitariamente usando resultFunc
		it('should retrieve all the collections without enhancing them', () => {
			expect(getEnhancedCollections.resultFunc(collections, enhacements)).toEqual([
				collection
			])
		})
	})

	describe('and there\'s data to enhance them', () => {
		beforeEach(() => {
			enhacements = { [collection.id]: { aNewProperty: true } }
		})

		it('should retrieve all the enhanced collections', () => {
			expect(getEnhancedCollections.resultFunc(collections, enhacements)).toEqual([
				{ ...collection, ...enhacements[collection.id] }
			])
		})
	})
})
```

## Probando creadores de acciones

Los creadores de acciones son los encargados de crear las actions que luego serán procesadas en los reducers. Aunque los creadores de acciones suelen ser simples, DEBEN probarse para asegurar que hacen lo que esperamos.

**Describe**s y **it**s DEBEN escribirse como se describe en la [Describing and building context](/contributor/contributor-es/guias-para-colaboradores/testing-standards/writing-tests.md#describing-and-building-context) sección.

### Ejemplo de creadores de acciones

```tsx
export const BUY_THIRD_PARTY_ITEM_TIERS_REQUEST = '[Request] Buy a third party item tier'
export const BUY_THIRD_PARTY_ITEM_TIERS_SUCCESS = '[Success] Buy a third party item tier'
export const BUY_THIRD_PARTY_ITEM_TIERS_FAILURE = '[Failure] Buy a third party item tier'

export const buyThirdPartyItemTiersRequest = (thirdParty: ThirdParty, tier: ThirdPartyItemTier) =>
  action(BUY_THIRD_PARTY_ITEM_TIERS_REQUEST, { thirdParty, tier })
export const buyThirdPartyItemTiersSuccess = (txHash: string, chainId: ChainId, thirdParty: ThirdParty, tier: ThirdPartyItemTier) =>
  action(BUY_THIRD_PARTY_ITEM_TIERS_SUCCESS, { thirdParty, tier, ...buildTransactionPayload(chainId, txHash, { tier, thirdParty }) })
export const buyThirdPartyItemTiersFailure = (error: string, thirdPartyId: string, tier: ThirdPartyItemTier) =>
  action(BUY_THIRD_PARTY_ITEM_TIERS_FAILURE, { error, thirdPartyId, tier })
```

### Ejemplo de prueba de creadores de acciones

```tsx
describe('when creating the action that signals the start of a tiers fetch request', () => {
  it('should return an action signaling the start of the tiers fetch', () => {
    expect(fetchThirdPartyItemTiersRequest()).toEqual({ type: FETCH_THIRD_PARTY_ITEM_TIERS_REQUEST })
  })
})

describe('when creating the action that signals the successful fetch of tiers', () => {
  it('should return an action signaling the success of the tiers fetch', () => {
    expect(fetchThirdPartyItemTiersSuccess([thirdPartyItemTier])).toEqual({
      type: FETCH_THIRD_PARTY_ITEM_TIERS_SUCCESS,
      payload: { tiers: [thirdPartyItemTier], error: undefined, meta: undefined }
    })
  })
})

describe('when creating the action that signals the unsuccessful fetch of tiers', () => {
  it('should return an action signaling the failure of the tiers fetch', () => {
    expect(fetchThirdPartyItemTiersFailure(defaultError)).toEqual({
      type: FETCH_THIRD_PARTY_ITEM_TIERS_FAILURE,
      payload: { error: defaultError }
    })
  })
})
```

## Probando componentes de UI

Para probar componentes de UI estamos usando [React Testing Library](https://testing-library.com/docs). Estamos usando esta librería en lugar de otras opciones como enzyme ya que nos permite probar componentes teniendo en cuenta cómo el usuario interactuará con ellos en lugar de la implementación en sí. No interactuaremos con instancias de componentes de react, en su lugar interactuamos con elementos del DOM tal como se renderizan.

### Qué probar

El propósito de esta prueba de componentes de UI es probar el comportamiento del componente y cómo el usuario interactúa con él. En muchos casos puede haber componentes que estén conectados al store de redux para obtener algunas propiedades y llamar a algunas acciones. Normalmente manejamos esto haciendo un archivo `container` separado que interactuará con el store. Estos archivos están fuera del alcance de este tipo de pruebas. Estamos probando cómo se comporta el componente con diferentes valores de props; no deberíamos preocuparnos por la fuente de esos valores (redux store, context, componente padre), sino por cómo impactan el render final.

### Renderizado

Cada prueba debe comenzar llamando a la función render. Esto renderizará no solo el componente que pasamos como parámetro, sino también todos los hijos. Debemos evitar hacer esto en la configuración de la prueba (`beforeEach`) ya que puede causar problemas haciendo que el árbol del DOM tenga más componentes de los que queremos. Debemos guardar el resultado en una propiedad llamada **screen**. Hacer esto nos permitirá obtener todas las propiedades que necesitamos en la prueba sin la necesidad de cambiar constantemente la destructuración.

```jsx
it('should render hello world component', () => {
	const screen = render(<HelloWorld />)
})
```

Si el documento tendrá múltiples pruebas y el componente tiene múltiples props podemos crear una función al principio del archivo que se encargue de establecer props por defecto y renderizar el componente.

```jsx
function renderSelectedFilters(props: Partial<Props> = {}) {
  return render(
    <SelectedFilters
      isLandSection={false}
      category={undefined}
      browseOptions={{}}
      onBrowse={jest.fn()}
      {...props}
    />
  )
}
```

### Queries

Las Queries son los métodos que Testing Library te da para encontrar elementos en la página. Hay diferentes [tipos de queries](https://testing-library.com/docs/queries/about/#priority) que podemos usar para acceder a los elementos. Debemos intentar siempre usar las queries que son `Accessible to everyone` ya que son las que reflejan la experiencia para todos los usuarios (visual/con ratón y usuarios de tecnología asistiva).

Esas queries son `getByRole`, `getByLabelText`, `getByPlaceholderText`, `getByText`, `getByDisplayValue`. Si podemos acceder al elemento usando estas queries probablemente significa que el componente no es accesible.

### Probando eventos de usuario

No podremos acceder a funciones dentro del componente por lo que deberíamos imitar el comportamiento del usuario que conduce a que esas funciones sean llamadas. Para ello usamos `userEvent` library.

```jsx
const screen = renderSelectedFilters({
      browseOptions: raritiesOptions,
      onBrowse: onBrowseMock
    })
const commonPill = screen.getByTestId('pill-common')
await userEvent.click(within(commonPill).getByRole('button'))
```

### Depuración

Para depurar la prueba y ver cuál es el estado actual del DOM en un momento específico podemos usar `debug` función que es una de las propiedades devueltas al llamar a render

```jsx
const { debug } = render(<HelloWorld />)

debug()
```

### Ejemplo completo de prueba de componente de UI

```jsx
function renderSelectedFilters(props: Partial<Props> = {}) {
  return render(
    <SelectedFilters
      isLandSection={false}
      category={undefined}
      browseOptions={{}}
      onBrowse={jest.fn()}
      {...props}
    />
  )
}

describe('rarities filter', () => {
  it('should render rarities', () => {
    const raritiesOptions = { rarities: [Rarity.COMMON, Rarity.EPIC] }
    const screen = renderSelectedFilters({
      browseOptions: raritiesOptions
    })
    expect(screen.getByText(Rarity.COMMON)).toBeInTheDocument()
    expect(screen.getByText(Rarity.EPIC)).toBeInTheDocument()
  })

  it('should call onBrowse without deleted rarity', async () => {
    const raritiesOptions = { rarities: [Rarity.COMMON, Rarity.EPIC] }
    const onBrowseMock = jest.fn()

    const screen = renderSelectedFilters({
      browseOptions: raritiesOptions,
      onBrowse: onBrowseMock
    })
    const commonPill = screen.getByTestId('pill-common')
    await userEvent.click(within(commonPill).getByRole('button'))
    expect(onBrowseMock).toHaveBeenCalledWith({ rarities: [Rarity.EPIC] })
  })
})
```

## Estructura de directorios

Las pruebas DEBEN colocarse junto a los archivos o módulos que van a ejercitar, con el mismo nombre, pero con la extensión `spec.ts` en lugar de `ts`.

Una estructura de archivos DEBE verse así:

```
actions.ts
action.spec.ts
reducer.ts
reducer.spec.ts
sagas.ts
sagas.spec.ts
selectors.ts
selectors.spec.ts
utils.ts
utils.spec.ts
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.decentraland.org/contributor/contributor-es/guias-para-colaboradores/testing-standards/testing-uis.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
