diff --git a/backend/tests/domain/services/test_pdf_rules.py b/backend/tests/domain/services/test_pdf_rules.py new file mode 100644 index 0000000..fc2e49e --- /dev/null +++ b/backend/tests/domain/services/test_pdf_rules.py @@ -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 diff --git a/backend/tests/infrastructure/test_pdf_selos.py b/backend/tests/infrastructure/test_pdf_selos.py new file mode 100644 index 0000000..a6bac46 --- /dev/null +++ b/backend/tests/infrastructure/test_pdf_selos.py @@ -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 diff --git a/frontend/package.json b/frontend/package.json index 274b6de..e92abf3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,7 +6,8 @@ "scripts": { "dev": "vite", "build": "vite build", - "preview": "vite preview" + "preview": "vite preview", + "test": "vitest" }, "dependencies": { "react": "^19.2.0", @@ -17,6 +18,10 @@ "@types/react": "^18.2.0", "@types/react-dom": "^18.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" } } diff --git a/frontend/src/components/__tests__/FiltroSelos.test.jsx b/frontend/src/components/__tests__/FiltroSelos.test.jsx new file mode 100644 index 0000000..02bb5b5 --- /dev/null +++ b/frontend/src/components/__tests__/FiltroSelos.test.jsx @@ -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(); + + 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(); + + fireEvent.click(screen.getByTitle('Limpar filtros')); + + expect(handleChange).toHaveBeenCalledWith([]); + }); +}); diff --git a/frontend/src/components/__tests__/Header.test.jsx b/frontend/src/components/__tests__/Header.test.jsx new file mode 100644 index 0000000..620a23c --- /dev/null +++ b/frontend/src/components/__tests__/Header.test.jsx @@ -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(
); + + 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(
); + + fireEvent.click(screen.getByText('A - Coordenacao CAPES')); + + expect(screen.getByText(/Coordena/)).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/setupTests.js b/frontend/src/setupTests.js new file mode 100644 index 0000000..7b0828b --- /dev/null +++ b/frontend/src/setupTests.js @@ -0,0 +1 @@ +import '@testing-library/jest-dom'; diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 3f4c31b..e4ccb80 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -3,6 +3,11 @@ import react from '@vitejs/plugin-react'; export default defineConfig({ plugins: [react()], + test: { + environment: 'jsdom', + setupFiles: './src/setupTests.js', + globals: true, + }, server: { host: '0.0.0.0', port: 5173,