# Pruebas y rendimiento

Esta página cubre estrategias de prueba para aplicaciones Redux/RTK Query y técnicas de optimización de rendimiento.

## Pruebas de Redux Slices

### Pruebas básicas de Reducer

```tsx
// user.slice.test.ts
import reducer, { userLoggedIn, userLoggedOut } from './user.slice';

describe('user slice', () => {
  const initialState = {
    account: null,
    isAuthenticated: false,
  };

  it('should handle userLoggedIn', () => {
    const account = '0x123...';
    const actual = reducer(initialState, userLoggedIn({ account }));
    
    expect(actual.account).toBe(account);
    expect(actual.isAuthenticated).toBe(true);
  });

  it('should handle userLoggedOut', () => {
    const loggedInState = {
      account: '0x123...',
      isAuthenticated: true,
    };
    
    const actual = reducer(loggedInState, userLoggedOut());
    
    expect(actual.account).toBeNull();
    expect(actual.isAuthenticated).toBe(false);
  });
});
```

### Pruebas de Entity Adapters

```tsx
// credits.slice.test.ts
import reducer, { txAdded, txsCleared, creditsSelectors } from './credits.slice';

describe('credits slice with entity adapter', () => {
  it('should add a transaction', () => {
    const initialState = reducer(undefined, { type: 'unknown' });
    const tx = { id: '1', address: '0x123', amount: 100, type: 'grant', timestamp: Date.now() };
    
    const actual = reducer(initialState, txAdded(tx));
    
    expect(creditsSelectors.selectById({ credits: actual }, '1')).toEqual(tx);
    expect(creditsSelectors.selectTotal({ credits: actual })).toBe(1);
  });

  it('should clear all transactions', () => {
    const initialState = reducer(undefined, { type: 'unknown' });
    const withTx = reducer(initialState, txAdded({ id: '1', /* ... */ }));
    
    const actual = reducer(withTx, txsCleared());
    
    expect(creditsSelectors.selectTotal({ credits: actual })).toBe(0);
  });
});
```

## Pruebas de Selectors

### Selectores simples

```tsx
// user.selectors.test.ts
import { selectAccount, selectIsAuthenticated } from './user.slice';

describe('user selectors', () => {
  const mockState = {
    user: {
      account: '0x123...',
      isAuthenticated: true,
    },
    // ... other slices
  };

  it('should select account', () => {
    expect(selectAccount(mockState)).toBe('0x123...');
  });

  it('should select authentication status', () => {
    expect(selectIsAuthenticated(mockState)).toBe(true);
  });
});
```

### Selectores Memoizados

```tsx
// land.selectors.test.ts
import { selectFilteredParcels, selectActiveFiltersCount } from './land.selectors';

describe('land selectors', () => {
  const mockState = {
    land: {
      filters: { owner: '0x123', minPrice: 100 },
      parcels: [
        { id: '1', owner: '0x123', price: 150 },
        { id: '2', owner: '0x456', price: 200 },
      ],
    },
  };

  it('should count active filters', () => {
    expect(selectActiveFiltersCount(mockState)).toBe(2);
  });

  it('should filter parcels', () => {
    const result = selectFilteredParcels(mockState);
    expect(result).toHaveLength(1);
    expect(result[0].id).toBe('1');
  });

  it('should memoize results', () => {
    const result1 = selectFilteredParcels(mockState);
    const result2 = selectFilteredParcels(mockState);
    
    // Same reference = memoized
    expect(result1).toBe(result2);
  });
});
```

## Pruebas de RTK Query con MSW

### Configurar MSW

```tsx
// src/test/server.ts
import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';

const handlers = [
  http.get('/api/v1/parcels/:id', ({ params }) => {
    return HttpResponse.json({
      id: params.id,
      x: 10,
      y: 20,
      owner: '0x123',
      name: 'Test Parcel',
    });
  }),

  http.post('/api/v1/credits/grant', async ({ request }) => {
    const body = await request.json();
    return HttpResponse.json({
      ok: true,
      newBalance: 1000,
    });
  }),
];

export const server = setupServer(...handlers);
```

```tsx
// src/test/setup.ts
import { server } from './server';

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
```

### Pruebas de Queries

```tsx
// land.client.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import { Provider } from 'react-redux';
import { setupStore } from '@/app/store';
import { useGetParcelQuery } from './land.client';

function TestComponent({ id }: { id: string }) {
  const { data, isLoading, isError } = useGetParcelQuery({ id });

  if (isLoading) return <div>Loading...</div>;
  if (isError) return <div>Error</div>;
  if (!data) return null;

  return <div>{data.name}</div>;
}

describe('land client', () => {
  it('should fetch and display parcel data', async () => {
    const store = setupStore();

    render(
      <Provider store={store}>
        <TestComponent id="1" />
      </Provider>
    );

    expect(screen.getByText('Loading...')).toBeInTheDocument();

    await waitFor(() => {
      expect(screen.getByText('Test Parcel')).toBeInTheDocument();
    });
  });
});
```

### Pruebas de Mutations

```tsx
// credits.client.test.tsx
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { Provider } from 'react-redux';
import { setupStore } from '@/app/store';
import { useGrantCreditsMutation } from './credits.client';

function TestComponent() {
  const [grant, { isLoading, isSuccess }] = useGrantCreditsMutation();

  return (
    <div>
      <button onClick={() => grant({ address: '0x123', amount: 100 })}>
        Grant
      </button>
      {isLoading && <div>Loading...</div>}
      {isSuccess && <div>Success!</div>}
    </div>
  );
}

describe('credits client mutations', () => {
  it('should grant credits successfully', async () => {
    const store = setupStore();

    render(
      <Provider store={store}>
        <TestComponent />
      </Provider>
    );

    fireEvent.click(screen.getByText('Grant'));

    await waitFor(() => {
      expect(screen.getByText('Success!')).toBeInTheDocument();
    });
  });
});
```

### Pruebas de Actualizaciones Optimistas

```tsx
// credits.client.test.ts
import { server } from '@/test/server';
import { http, HttpResponse } from 'msw';
import { setupStore } from '@/app/store';
import { creditsClient } from './credits.client';

describe('optimistic updates', () => {
  it('should update cache optimistically and rollback on error', async () => {
    const store = setupStore();
    const address = '0x123';

    // Prefetch initial balance
    await store.dispatch(
      creditsClient.endpoints.getBalance.initiate({ address })
    );

    const initialBalance = creditsClient.endpoints.getBalance.select({ address })(
      store.getState()
    ).data?.amount;

    expect(initialBalance).toBe(100); // from MSW handler

    // Mock failure
    server.use(
      http.post('/api/v1/credits/grant', () => {
        return HttpResponse.json({ error: 'Failed' }, { status: 500 });
      })
    );

    // Trigger mutation
    const mutation = store.dispatch(
      creditsClient.endpoints.grantCredits.initiate({
        address,
        amount: 50,
      })
    );

    // Check optimistic update
    const optimisticBalance = creditsClient.endpoints.getBalance.select({
      address,
    })(store.getState()).data?.amount;

    expect(optimisticBalance).toBe(150); // 100 + 50

    // Wait for mutation to fail
    await expect(mutation).rejects.toThrow();

    // Check rollback
    const rolledBackBalance = creditsClient.endpoints.getBalance.select({
      address,
    })(store.getState()).data?.amount;

    expect(rolledBackBalance).toBe(100); // Back to original
  });
});
```

## Optimización de Rendimiento

### Usa `selectFromResult` para Prevenir Re-renderizados

```tsx
// ✅ Bueno: Solo se suscribe a campos específicos
const { owner } = useGetParcelQuery(
  { id },
  {
    selectFromResult: ({ data }) => ({
      owner: data?.owner,
    }),
  }
);

// El componente solo vuelve a renderizarse cuando owner cambia
```

### Evitar Seleccionar Todo el Estado

```tsx
// ✅ Bueno: Seleccionar valores específicos
const viewMode = useAppSelector(selectViewMode);
const filters = useAppSelector(selectFilters);

// ❌ Malo: Selecciona todo el slice
const ui = useAppSelector((state) => state.ui);
```

### Memoizar Selectores Costosos

```tsx
import { createSelector } from '@reduxjs/toolkit';

// ✅ Bueno: Selector memoizado
export const selectFilteredParcels = createSelector(
  [selectAllParcels, selectFilters],
  (parcels, filters) => {
    // Lógica de filtrado costosa
    return parcels.filter(/* ... */);
  }
);

// ❌ Malo: Computado en el componente
function Component() {
  const parcels = useAppSelector(selectAllParcels);
  const filters = useAppSelector(selectFilters);
  
  // ¡Se recalcula en cada renderizado!
  const filtered = parcels.filter(/* ... */);
}
```

### Usar Entity Adapters para Datos Normalizados

```tsx
// ✅ Bueno: Normalizado con entity adapter
const adapter = createEntityAdapter<Parcel>();

// Búsquedas eficientes por ID
const parcel = adapter.getSelectors().selectById(state, id);

// ❌ Malo: Búsqueda en array
const parcel = state.parcels.find((p) => p.id === id);
```

### Ajustar Configuración de Caché de RTK Query

```tsx
export const client = createApi({
  // ...
  keepUnusedDataFor: 60, // Mantener datos durante 60 segundos
  refetchOnMountOrArgChange: 30, // Volver a obtener si los datos tienen más de 30s
  refetchOnFocus: true, // Volver a obtener cuando la ventana recupere el foco
  refetchOnReconnect: true, // Volver a obtener al reconectar
});
```

### Prefetch para Mejorar la UX

```tsx
function ParcelListItem({ parcel }: { parcel: Parcel }) {
  const dispatch = useAppDispatch();

  const handleMouseEnter = () => {
    // Prefetch al pasar el ratón
    dispatch(
      client.util.prefetch('getParcel', { id: parcel.id }, { force: false })
    );
  };

  return (
    <Link to={`/parcels/${parcel.id}`} onMouseEnter={handleMouseEnter}>
      {parcel.name}
    </Link>
  );
}
```

### Estrategia de Polling

```tsx
// Hacer polling solo cuando sea necesario
const { data } = useGetBalanceQuery(
  { address },
  {
    pollingInterval: isActive ? 10000 : 0, // Hacer polling solo cuando está activo
    skipPollingIfUnfocused: true, // Pausar cuando la pestaña no está enfocada
  }
);
```

## Redux DevTools

### Habilitar en Desarrollo

```tsx
export const store = configureStore({
  // ...
  devTools: process.env.NODE_ENV !== 'production',
});
```

### Action Sanitizer

Sanitizar datos sensibles en DevTools:

```tsx
const actionSanitizer = (action: any) => {
  if (action.type === 'user/loggedIn') {
    return {
      ...action,
      payload: {
        ...action.payload,
        authToken: '***REDACTED***',
      },
    };
  }
  return action;
};

export const store = configureStore({
  // ...
  devTools: {
    actionSanitizer,
  },
});
```

## Lista de Verificación de Mejores Prácticas

### Rendimiento

* [ ] Usa `selectFromResult` para resultados de consultas grandes
* [ ] Memoizar selectores costosos con `createSelector`
* [ ] Usar entity adapters para colecciones normalizadas
* [ ] Evitar seleccionar slices completos en componentes
* [ ] Ajustar `keepUnusedDataFor` según tu caso de uso
* [ ] Prefetch de datos antes de la navegación
* [ ] Usar polling estratégicamente (solo cuando sea necesario)

### Pruebas

* [ ] Probar unitariamente todos los reducers y actions
* [ ] Probar selectores memoizados para corrección y rendimiento
* [ ] Usar MSW para pruebas de endpoints de RTK Query
* [ ] Probar actualizaciones optimistas y la lógica de rollback
* [ ] Probar manejo de errores en componentes
* [ ] Escribir pruebas de integración para flujos críticos

### Calidad de Código

* [ ] Usar hooks tipados (`useAppSelector`, `useAppDispatch`)
* [ ] Manejar todos los estados de consulta (loading, error, success)
* [ ] Usa `.unwrap()` para manejo de errores en mutaciones
* [ ] Invalidar o actualizar la caché después de mutaciones
* [ ] Mantener datos no serializables fuera de Redux
* [ ] Documentar selectores y lógica compleja

## Anti-Patrones a Evitar

{% hint style="danger" %}
**No hagas esto:**

1. Almacenar objetos no serializables (providers, signers) en Redux
2. Duplicar datos en slices y RTK Query
3. Despachar acciones durante el renderizado
4. Crear selectores que devuelvan nuevos objetos sin memoización
5. Ignorar estados de loading y error
6. Obtener los mismos datos en múltiples componentes sin RTK Query
7. Hacer polling en exceso o hacer polling sin `skipPollingIfUnfocused`
   {% endhint %}

## Monitorear Rendimiento

### Rastrear llamadas a Selectors

```tsx
import { createSelector } from '@reduxjs/toolkit';

const selectExpensiveData = createSelector(
  [selectData],
  (data) => {
    console.log('Selector called'); // Debería registrarse solo cuando los datos cambien
    return expensiveOperation(data);
  }
);
```

### Monitorear Re-renders

```tsx
import { useEffect, useRef } from 'react';

function useRenderCount() {
  const renderCount = useRef(0);
  
  useEffect(() => {
    renderCount.current += 1;
    console.log('Render count:', renderCount.current);
  });
}

function Component() {
  useRenderCount(); // Rastrear re-renderizados
  // ...
}
```

## Siguientes pasos

* Revisar [Patrones de Componentes](/contributor/contributor-es/guias-para-colaboradores/estandares-de-ui/component-patterns.md) para ejemplos de uso
* Ver [RTK Query](/contributor/contributor-es/guias-para-colaboradores/estandares-de-ui/rtk-query.md) para estrategias de caching
* Entender [Gestión de Estado](/contributor/contributor-es/guias-para-colaboradores/estandares-de-ui/state-management.md) para optimización de slices


---

# 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/estandares-de-ui/testing-and-performance.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.
