feat(tests): adicionar testes de regras PDF e componentes frontend

Backend:
- test_pdf_rules.py: 108 testes para regras de pontuação do PDF
- test_pdf_selos.py: validação de selos disponíveis

Frontend:
- Configuração Vitest para testes de componentes React
- FiltroSelos.test.jsx: testes do componente de filtro
- Header.test.jsx: testes do componente de cabeçalho
This commit is contained in:
Frederico Castro
2025-12-29 09:16:19 -03:00
parent d48fff2236
commit 7d02289605
7 changed files with 344 additions and 2 deletions

View File

@@ -0,0 +1,232 @@
from datetime import datetime
from dateutil.relativedelta import relativedelta
import pytest
from src.domain.services.calculador_pontuacao import CalculadorPontuacao
from src.domain.value_objects.criterios_pontuacao import CRITERIOS
from src.domain.value_objects.periodo import Periodo
from src.domain.entities.consultor import (
Consultoria,
CoordenacaoCapes,
Inscricao,
AvaliacaoComissao,
Participacao,
)
PDF_BASE_TETO = {
"CA": (200, 450),
"CAJ": (150, 370),
"CAJ_MP": (120, 315),
"CAM": (100, 280),
"PPG_COORD": (0, 0),
"CONS_ATIVO": (150, 230),
"CONS_HIST": (100, 230),
"CONS_FALECIDO": (100, 230),
"INSC_AUTOR": (10, 20),
"INSC_INST_AUTOR": (20, 50),
"AVAL_COMIS_PREMIO": (30, 60),
"AVAL_COMIS_GP": (40, 80),
"COORD_COMIS_PREMIO": (40, 100),
"COORD_COMIS_GP": (50, 120),
"BOL_BPQ_NIVEL": (30, 60),
"PREMIACAO_GP_AUTOR": (100, 300),
"PREMIACAO_AUTOR": (50, 150),
"MENCAO_AUTOR": (30, 90),
"EVENTO": (1, 5),
"PROJ": (10, 30),
"IDIOMA_BILINGUE": (0, 0),
"IDIOMA_MULTILINGUE": (0, 0),
"TITULACAO_MESTRE": (0, 0),
"TITULACAO_DOUTOR": (0, 0),
"TITULACAO_POS_DOUTOR": (0, 0),
"ORIENT_POS_DOC": (0, 0),
"ORIENT_POS_DOC_PREM": (0, 0),
"ORIENT_TESE": (0, 0),
"ORIENT_TESE_PREM": (0, 0),
"ORIENT_DISS": (0, 0),
"ORIENT_DISS_PREM": (0, 0),
"CO_ORIENT_POS_DOC": (0, 0),
"CO_ORIENT_POS_DOC_PREM": (0, 0),
"CO_ORIENT_TESE": (0, 0),
"CO_ORIENT_TESE_PREM": (0, 0),
"CO_ORIENT_DISS": (0, 0),
"CO_ORIENT_DISS_PREM": (0, 0),
"MB_BANCA_POS_DOC": (0, 0),
"MB_BANCA_POS_DOC_PREM": (0, 0),
"MB_BANCA_TESE": (0, 0),
"MB_BANCA_TESE_PREM": (0, 0),
"MB_BANCA_DISS": (0, 0),
"MB_BANCA_DISS_PREM": (0, 0),
}
PDF_TEMPO = {
"CA": (10, 100),
"CAJ": (8, 80),
"CAJ_MP": (6, 60),
"CAM": (5, 50),
"PPG_COORD": (0, 0),
"CONS_ATIVO": (5, 50),
"CONS_HIST": (5, 50),
"CONS_FALECIDO": (5, 50),
}
PDF_BONUS = {
"CA": {"atualidade": 30, "retorno": 20},
"CAJ": {"atualidade": 20, "retorno": 15},
"CAJ_MP": {"atualidade": 15, "retorno": 10},
"CAM": {"atualidade": 20, "retorno": 10},
"PPG_COORD": {"atualidade": 15, "retorno": 10, "continuidade": 15},
"CONS_ATIVO": {"atualidade": 20, "retorno": 15, "continuidade": 20},
"CONS_HIST": {"retorno": 20, "continuidade": 20},
"CONS_FALECIDO": {"continuidade": 20},
}
PDF_RECORRENCIA = {
"INSC_AUTOR": {"por_participacao": 2, "teto_participacao": 10},
"INSC_INST_AUTOR": {"por_participacao": 5, "teto_participacao": 10},
"AVAL_COMIS_PREMIO": {"por_ano": 2, "teto_ano": 15},
"AVAL_COMIS_GP": {"por_ano": 3, "teto_ano": 20},
"COORD_COMIS_PREMIO": {"por_ano": 4, "teto_ano": 20},
"COORD_COMIS_GP": {"por_ano": 6, "teto_ano": 20},
"EVENTO": {"por_participacao": 1, "teto_participacao": 10},
"PROJ": {"por_participacao": 2, "teto_participacao": 10},
}
def periodo_anos(anos: int, ativo: bool = False) -> Periodo:
inicio = datetime.now() - relativedelta(years=anos)
fim = None if ativo else datetime.now()
return Periodo(inicio=inicio, fim=fim)
@pytest.mark.parametrize("codigo,base_teto", PDF_BASE_TETO.items())
def test_pdf_base_teto(codigo, base_teto):
criterio = CRITERIOS.get(codigo)
assert criterio is not None
base, teto = base_teto
assert criterio.base == base
assert criterio.teto == teto
def test_pdf_criterios_cobrem_43_codigos():
assert set(CRITERIOS.keys()) == set(PDF_BASE_TETO.keys())
assert len(CRITERIOS) == 43
@pytest.mark.parametrize("codigo,tempo", PDF_TEMPO.items())
def test_pdf_regras_tempo(codigo, tempo):
criterio = CRITERIOS[codigo]
multiplicador, teto_tempo = tempo
assert criterio.pontua_tempo is True
assert criterio.multiplicador_tempo == multiplicador
assert criterio.teto_tempo == teto_tempo
@pytest.mark.parametrize("codigo", sorted(set(PDF_BASE_TETO) - set(PDF_TEMPO)))
def test_pdf_codigos_sem_tempo(codigo):
criterio = CRITERIOS[codigo]
assert criterio.pontua_tempo is False
assert criterio.multiplicador_tempo == 0
assert criterio.teto_tempo == 0
@pytest.mark.parametrize("codigo,bonus", PDF_BONUS.items())
def test_pdf_bonus_configurados(codigo, bonus):
criterio = CRITERIOS[codigo]
assert criterio.bonus_atualidade == bonus.get("atualidade", 0)
assert criterio.bonus_retorno == bonus.get("retorno", 0)
assert criterio.bonus_continuidade_8anos == bonus.get("continuidade", 0)
@pytest.mark.parametrize("codigo,rec", PDF_RECORRENCIA.items())
def test_pdf_regras_recorrencia(codigo, rec):
criterio = CRITERIOS[codigo]
assert criterio.bonus_recorrencia_anual == rec.get("por_ano", 0)
assert criterio.teto_recorrencia == rec.get("teto_ano", 0)
assert criterio.bonus_recorrencia_participacao == rec.get("por_participacao", 0)
assert criterio.teto_recorrencia_participacao == rec.get("teto_participacao", 0)
def test_calculo_tempo_e_bonus_ca():
periodo_historico = Periodo(
inicio=datetime.now() - relativedelta(years=12),
fim=datetime.now() - relativedelta(years=2),
)
periodo_ativo = periodo_anos(2, ativo=True)
coords = [
CoordenacaoCapes(
codigo="CA",
tipo="Coordenador de Area",
area_avaliacao="AREA",
periodo=periodo_historico,
),
CoordenacaoCapes(
codigo="CA",
tipo="Coordenador de Area",
area_avaliacao="AREA",
periodo=periodo_ativo,
),
]
resultado = CalculadorPontuacao.calcular_bloco_a(coords)
atuacao = resultado.atuacoes[0]
assert atuacao.tempo == 100
assert atuacao.bonus == 50
def test_calculo_bonus_consultoria_completo():
consultoria = Consultoria(
codigo="CONS_ATIVO",
situacao="Ativo",
periodo=periodo_anos(9, ativo=True),
periodos=[periodo_anos(9, ativo=True)],
anos_consecutivos=9,
retornos=1,
)
resultado = CalculadorPontuacao.calcular_bloco_b(consultoria)
atuacao = resultado.atuacoes[0]
assert atuacao.bonus == 55
def test_calculo_recorrencia_inscricao_autor_bonus():
inscricoes = [
Inscricao(codigo="INSC_AUTOR", tipo="Autor", premio="PCT", ano=2020),
Inscricao(codigo="INSC_AUTOR", tipo="Autor", premio="PCT", ano=2021),
Inscricao(codigo="INSC_AUTOR", tipo="Autor", premio="PCT", ano=2022),
Inscricao(codigo="INSC_AUTOR", tipo="Autor", premio="PCT", ano=2023),
Inscricao(codigo="INSC_AUTOR", tipo="Autor", premio="PCT", ano=2024),
]
resultado = CalculadorPontuacao.calcular_bloco_c(inscricoes, [], [], [], [])
atuacao = next(a for a in resultado.atuacoes if a.codigo == "INSC_AUTOR")
assert atuacao.bonus == 10
def test_calculo_recorrencia_avaliacao_gp_bonus():
avaliacoes = [
AvaliacaoComissao(codigo="AVAL_COMIS_GP", tipo="Membro", premio="GP", ano=2021),
AvaliacaoComissao(codigo="AVAL_COMIS_GP", tipo="Membro", premio="GP", ano=2022),
AvaliacaoComissao(codigo="AVAL_COMIS_GP", tipo="Membro", premio="GP", ano=2023),
]
resultado = CalculadorPontuacao.calcular_bloco_c([], avaliacoes, [], [], [])
atuacao = next(a for a in resultado.atuacoes if a.codigo == "AVAL_COMIS_GP")
assert atuacao.bonus == 9
def test_calculo_recorrencia_evento_bonus():
eventos = [
Participacao(codigo="EVENTO", tipo="Evento", ano=2020),
Participacao(codigo="EVENTO", tipo="Evento", ano=2021),
Participacao(codigo="EVENTO", tipo="Evento", ano=2022),
Participacao(codigo="EVENTO", tipo="Evento", ano=2023),
Participacao(codigo="EVENTO", tipo="Evento", ano=2024),
Participacao(codigo="EVENTO", tipo="Evento", ano=2025),
Participacao(codigo="EVENTO", tipo="Evento", ano=2026),
Participacao(codigo="EVENTO", tipo="Evento", ano=2027),
Participacao(codigo="EVENTO", tipo="Evento", ano=2028),
Participacao(codigo="EVENTO", tipo="Evento", ano=2029),
Participacao(codigo="EVENTO", tipo="Evento", ano=2030),
]
resultado = CalculadorPontuacao.calcular_bloco_d([], eventos)
atuacao = next(a for a in resultado.atuacoes if a.codigo == "EVENTO")
assert atuacao.bonus == 10

View File

@@ -0,0 +1,53 @@
from src.infrastructure.ranking_store import SELOS_DISPONIVEIS
PDF_SELOS = {
"CA",
"CAJ",
"CAJ_MP",
"CAM",
"PRESID_CAMARA",
"CONS_ATIVO",
"AVAL_COMIS",
"COORD_COMIS",
"AUTOR_GP",
"AUTOR_PREMIO",
"AUTOR_MENCAO",
"ORIENT_GP",
"ORIENT_PREMIO",
"ORIENT_MENCAO",
"COORIENT_GP",
"COORIENT_PREMIO",
"COORIENT_MENCAO",
"ORIENT_POS_DOC",
"ORIENT_POS_DOC_PREM",
"ORIENT_TESE",
"ORIENT_TESE_PREM",
"ORIENT_DISS",
"ORIENT_DISS_PREM",
"CO_ORIENT_POS_DOC",
"CO_ORIENT_POS_DOC_PREM",
"CO_ORIENT_TESE",
"CO_ORIENT_TESE_PREM",
"CO_ORIENT_DISS",
"CO_ORIENT_DISS_PREM",
"MB_BANCA_POS_DOC",
"MB_BANCA_POS_DOC_PREM",
"MB_BANCA_TESE",
"MB_BANCA_TESE_PREM",
"MB_BANCA_DISS",
"MB_BANCA_DISS_PREM",
"IDIOMA_BILINGUE",
"IDIOMA_MULTILINGUE",
"TITULACAO_MESTRE",
"TITULACAO_DOUTOR",
"TITULACAO_POS_DOUTOR",
"BOL_BPQ_NIVEL",
"PPG_COORD",
"EVENTO",
"PROJ",
}
def test_pdf_selos_disponiveis():
assert set(SELOS_DISPONIVEIS) == PDF_SELOS

View File

@@ -6,7 +6,8 @@
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
"preview": "vite preview" "preview": "vite preview",
"test": "vitest"
}, },
"dependencies": { "dependencies": {
"react": "^19.2.0", "react": "^19.2.0",
@@ -17,6 +18,10 @@
"@types/react": "^18.2.0", "@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0", "@types/react-dom": "^18.2.0",
"@vitejs/plugin-react": "^4.2.0", "@vitejs/plugin-react": "^4.2.0",
"vite": "^5.0.0" "@testing-library/jest-dom": "^6.2.0",
"@testing-library/react": "^14.1.2",
"jsdom": "^24.0.0",
"vite": "^5.0.0",
"vitest": "^1.6.0"
} }
} }

View File

@@ -0,0 +1,27 @@
import { render, screen, fireEvent } from '@testing-library/react';
import FiltroSelos from '../FiltroSelos';
describe('FiltroSelos', () => {
it('applies selected seals', () => {
const handleChange = vi.fn();
render(<FiltroSelos selecionados={[]} onChange={handleChange} />);
fireEvent.click(screen.getByRole('button', { name: /Filtrar por selos/i }));
const checkbox = screen.getByLabelText(/Coord\./i);
fireEvent.click(checkbox);
fireEvent.click(screen.getByRole('button', { name: /Aplicar/i }));
expect(handleChange).toHaveBeenCalledWith(['CA']);
});
it('clears filters from the trigger when active', () => {
const handleChange = vi.fn();
render(<FiltroSelos selecionados={['CA']} onChange={handleChange} />);
fireEvent.click(screen.getByTitle('Limpar filtros'));
expect(handleChange).toHaveBeenCalledWith([]);
});
});

View File

@@ -0,0 +1,19 @@
import { render, screen, fireEvent } from '@testing-library/react';
import Header from '../Header';
describe('Header', () => {
it('renders the title and total count', () => {
render(<Header total={1000} />);
expect(screen.getByText('Ranking de Consultores CAPES')).toBeInTheDocument();
expect(screen.getByText(/Total:\s+1\.000 consultores/)).toBeInTheDocument();
});
it('opens the criteria modal when a block is clicked', () => {
render(<Header total={0} />);
fireEvent.click(screen.getByText('A - Coordenacao CAPES'));
expect(screen.getByText(/Coordena/)).toBeInTheDocument();
});
});

View File

@@ -0,0 +1 @@
import '@testing-library/jest-dom';

View File

@@ -3,6 +3,11 @@ import react from '@vitejs/plugin-react';
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
test: {
environment: 'jsdom',
setupFiles: './src/setupTests.js',
globals: true,
},
server: { server: {
host: '0.0.0.0', host: '0.0.0.0',
port: 5173, port: 5173,