> For the complete documentation index, see [llms.txt](https://docs.decentraland.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.decentraland.org/contributor/contributor-pt/guias-do-contribuidor/padroes-de-ui/testing-and-performance.md).

# Testes e Desempenho

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](/contributor/contributor-pt/guias-do-contribuidor/padroes-de-ui/component-patterns.md) para exemplos de uso
* Veja [RTK Query](/contributor/contributor-pt/guias-do-contribuidor/padroes-de-ui/rtk-query.md) para estratégias de cache
* Entender [Gerenciamento de Estado](/contributor/contributor-pt/guias-do-contribuidor/padroes-de-ui/state-management.md) para otimização de slice


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## 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, and the optional `goal` query parameter:

```
GET https://docs.decentraland.org/contributor/contributor-pt/guias-do-contribuidor/padroes-de-ui/testing-and-performance.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

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.
