# Testing & Performance

Esta página aborda estratégias de teste para aplicações Redux/RTK Query e técnicas de otimização de desempenho.

## Testando Slices do Redux

### Testes Básicos 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);
  });
});
```

### Testando 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);
  });
});
```

## Testando Selectors

### Selectors 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);
  });
});
```

### Seletores 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);
  });
});
```

## Testando RTK Query com 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());
```

### Testando 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();
    });
  });
});
```

### Testando 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();
    });
  });
});
```

### Testando Atualizações Otimistas

```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
  });
});
```

## Otimização de Desempenho

### Use `selectFromResult` para Prevenir Re-renderizações

```tsx
// ✅ Bom: Inscreve-se apenas em campos específicos
const { owner } = useGetParcelQuery(
  { id },
  {
    selectFromResult: ({ data }) => ({
      owner: data?.owner,
    }),
  }
);

// Componente só re-renderiza quando owner muda
```

### Evite Selecionar o Estado Inteiro

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

// ❌ Ruim: Seleciona a slice inteira
const ui = useAppSelector((state) => state.ui);
```

### Memoizar Selectors Custosos

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

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

// ❌ Ruim: Calculado no componente
function Component() {
  const parcels = useAppSelector(selectAllParcels);
  const filters = useAppSelector(selectFilters);
  
  // Re-calcula em cada render!
  const filtered = parcels.filter(/* ... */);
}
```

### Use Entity Adapters para Dados Normalizados

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

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

// ❌ Ruim: Pesquisa em array
const parcel = state.parcels.find((p) => p.id === id);
```

### Ajustar Configurações de Cache do RTK Query

```tsx
export const client = createApi({
  // ...
  keepUnusedDataFor: 60, // Mantém os dados por 60 segundos
  refetchOnMountOrArgChange: 30, // Re-faz fetch se os dados tiverem mais de 30s
  refetchOnFocus: true, // Re-faz fetch quando a janela volta a ter foco
  refetchOnReconnect: true, // Re-faz fetch ao reconectar
});
```

### Pré-carregamento para Melhor UX

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

  const handleMouseEnter = () => {
    // Pré-carregar ao passar o mouse
    dispatch(
      client.util.prefetch('getParcel', { id: parcel.id }, { force: false })
    );
  };

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

### Estratégia de Polling

```tsx
// Poll apenas quando necessário
const { data } = useGetBalanceQuery(
  { address },
  {
    pollingInterval: isActive ? 10000 : 0, // Poll apenas quando ativo
    skipPollingIfUnfocused: true, // Pausar quando a aba não está focada
  }
);
```

## Redux DevTools

### Habilitar em Desenvolvimento

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

### Sanitizador de Ações

Sanitizar dados sensíveis no 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 Verificação de Boas Práticas

### Desempenho

* [ ] Use `selectFromResult` para grandes resultados de query
* [ ] Memoizar selectors custosos com `createSelector`
* [ ] Usar entity adapters para coleções normalizadas
* [ ] Evitar selecionar slices inteiras em componentes
* [ ] Ajustar `keepUnusedDataFor` com base no seu caso de uso
* [ ] Pré-carregar dados antes da navegação
* [ ] Usar polling estrategicamente (somente quando necessário)

### Testes

* [ ] Testar unitariamente todos os reducers e actions
* [ ] Testar selectors memoizados quanto à correção e desempenho
* [ ] Usar MSW para testes de endpoints do RTK Query
* [ ] Testar atualizações otimistas e lógica de rollback
* [ ] Testar tratamento de erros nos componentes
* [ ] Escrever testes de integração para fluxos críticos

### Qualidade de Código

* [ ] Usar hooks tipados (`useAppSelector`, `useAppDispatch`)
* [ ] Lidar com todos os estados de query (loading, error, success)
* [ ] Use `.unwrap()` para tratamento de erro em mutations
* [ ] Invalidar ou atualizar cache após mutations
* [ ] Manter dados não-serializáveis fora do Redux
* [ ] Documentar selectors e lógica complexa

## Anti-padrões a Evitar

{% hint style="danger" %}
**Não faça isto:**

1. Armazenar objetos não-serializáveis (providers, signers) no Redux
2. Duplicar dados tanto em slices quanto no RTK Query
3. Despachar ações durante o render
4. Criar selectors que retornam novos objetos sem memoização
5. Ignorar estados de loading e error
6. Buscar os mesmos dados em múltiplos componentes sem RTK Query
7. Fazer polling em excesso ou fazer polling sem `skipPollingIfUnfocused`
   {% endhint %}

## Monitorando Desempenho

### Rastrear Chamadas de Selector

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

const selectExpensiveData = createSelector(
  [selectData],
  (data) => {
    console.log('Selector called'); // Deve logar apenas quando os dados mudam
    return expensiveOperation(data);
  }
);
```

### Monitorar 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-renders
  // ...
}
```

## Próximos Passos

* Rever [Padrões de Componentes](https://docs.decentraland.org/contributor/contributor-pt/guias-para-contribuidores/ui-standards/component-patterns) para exemplos de uso
* Veja [RTK Query](https://docs.decentraland.org/contributor/contributor-pt/guias-para-contribuidores/ui-standards/rtk-query) para estratégias de cache
* Entender [Gerenciamento de Estado](https://docs.decentraland.org/contributor/contributor-pt/guias-para-contribuidores/ui-standards/state-management) para otimização de slice
