From 8875e16fbbb188d95b5e06046d0c84babb34eeb3 Mon Sep 17 00:00:00 2001 From: icrcode Date: Tue, 9 Jun 2026 13:22:20 -0300 Subject: [PATCH 01/19] =?UTF-8?q?test:=20adicionar=20testes=20para=20a=20p?= =?UTF-8?q?=C3=A1gina=20Cursos,=20incluindo=20renderiza=C3=A7=C3=A3o,=20fi?= =?UTF-8?q?ltros=20e=20modais?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/test/pages/Cursos.test.tsx | 286 ++++++++++++++++++++++++ 1 file changed, 286 insertions(+) create mode 100644 frontend/src/test/pages/Cursos.test.tsx diff --git a/frontend/src/test/pages/Cursos.test.tsx b/frontend/src/test/pages/Cursos.test.tsx new file mode 100644 index 0000000..6694131 --- /dev/null +++ b/frontend/src/test/pages/Cursos.test.tsx @@ -0,0 +1,286 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { screen, fireEvent, waitFor } from '@testing-library/react'; +import { renderWithProviders, USUARIO_ADMIN } from '../helpers/renderWithProviders'; +import { Cursos } from '../../pages/Cursos'; + +const { mockCursosService, mockInstituicoesService } = vi.hoisted(() => ({ + mockCursosService: { + listarAdmin: vi.fn(), + criar: vi.fn(), + atualizar: vi.fn(), + alterarAtivo: vi.fn(), + }, + mockInstituicoesService: { + listar: vi.fn(), + }, +})); + +vi.mock('../../services/api', () => ({ + default: { + get: vi.fn(), + post: vi.fn(), + put: vi.fn(), + patch: vi.fn(), + interceptors: { request: { use: vi.fn() }, response: { use: vi.fn() } }, + }, +})); + +vi.mock('../../services/cursos', () => ({ cursosService: mockCursosService })); +vi.mock('../../services/instituicoes', () => ({ instituicoesService: mockInstituicoesService })); + +const INST_MOCK = [ + { + id: 'i-1', nome: 'Universidade Teste', sigla: 'UT', ativa: true, + dominios_email: [], cnpj: null, email_contato: null, telefone: null, + site: null, endereco: null, cidade: null, estado: null, + criado_em: '', atualizado_em: '', + }, +]; + +const CURSOS_MOCK = [ + { + id: 'c-1', nome: 'Ciência da Computação', codigo: 'CC-001', + carga_horaria_complementar: 200, turno: 'noturno', modalidade: 'presencial', + instituicao_id: 'i-1', instituicao_nome: 'Universidade Teste', instituicao_sigla: 'UT', + ativo: true, total_estudantes: 5, criado_em: '', atualizado_em: '', + }, + { + id: 'c-2', nome: 'Engenharia de Software', codigo: 'ES-001', + carga_horaria_complementar: 160, turno: null, modalidade: null, + instituicao_id: 'i-1', instituicao_nome: 'Universidade Teste', instituicao_sigla: 'UT', + ativo: false, total_estudantes: 0, criado_em: '', atualizado_em: '', + }, +]; + +function renderCursos() { + return renderWithProviders(, { token: 'tok', usuario: USUARIO_ADMIN }); +} + +beforeEach(() => { + vi.clearAllMocks(); + localStorage.clear(); + mockCursosService.listarAdmin.mockResolvedValue(CURSOS_MOCK); + mockInstituicoesService.listar.mockResolvedValue(INST_MOCK); +}); + +describe('Cursos — renderização', () => { + it('exibe título Cursos', async () => { + renderCursos(); + await waitFor(() => expect(screen.getByText('Cursos')).toBeInTheDocument()); + }); + + it('exibe lista de cursos após carregar', async () => { + renderCursos(); + await waitFor(() => { + expect(screen.getByText('Ciência da Computação')).toBeInTheDocument(); + expect(screen.getByText('Engenharia de Software')).toBeInTheDocument(); + }); + }); + + it('exibe badge Inativo para curso inativo', async () => { + renderCursos(); + await waitFor(() => expect(screen.getByText('Inativo')).toBeInTheDocument()); + }); + + it('exibe estado de carregamento inicialmente', () => { + mockCursosService.listarAdmin.mockReturnValue(new Promise(() => {})); + renderCursos(); + expect(screen.getByText('Carregando cursos...')).toBeInTheDocument(); + }); + + it('exibe mensagem quando não há cursos', async () => { + mockCursosService.listarAdmin.mockResolvedValue([]); + renderCursos(); + await waitFor(() => expect(screen.getByText('Nenhum curso cadastrado ainda.')).toBeInTheDocument()); + }); + + it('exibe código do curso', async () => { + renderCursos(); + await waitFor(() => expect(screen.getByText('CC-001')).toBeInTheDocument()); + }); + + it('exibe total de estudantes', async () => { + renderCursos(); + await waitFor(() => expect(screen.getByText('5 alunos')).toBeInTheDocument()); + }); + + it('exibe estatística com total de cursos', async () => { + renderCursos(); + await waitFor(() => expect(screen.getByText(/2 cursos/)).toBeInTheDocument()); + }); + + it('exibe turno e modalidade como badges', async () => { + renderCursos(); + await waitFor(() => { + expect(screen.getByText('noturno')).toBeInTheDocument(); + expect(screen.getByText('presencial')).toBeInTheDocument(); + }); + }); + + it('exibe carga horária complementar', async () => { + renderCursos(); + await waitFor(() => expect(screen.getByText('200h complementares')).toBeInTheDocument()); + }); + + it('exibe instituição dos cursos', async () => { + renderCursos(); + await waitFor(() => expect(screen.getAllByText('Universidade Teste').length).toBeGreaterThan(0)); + }); + + it('botão Novo curso está presente', async () => { + renderCursos(); + await waitFor(() => expect(screen.getByText('Novo curso')).toBeInTheDocument()); + }); +}); + +describe('Cursos — filtros', () => { + it('filtra cursos por busca de nome', async () => { + renderCursos(); + await waitFor(() => screen.getByText('Ciência da Computação')); + fireEvent.change(screen.getByPlaceholderText(/buscar por nome/i), { target: { value: 'Engenharia' } }); + expect(screen.queryByText('Ciência da Computação')).not.toBeInTheDocument(); + expect(screen.getByText('Engenharia de Software')).toBeInTheDocument(); + }); + + it('busca por código retorna resultado correto', async () => { + renderCursos(); + await waitFor(() => screen.getByText('Ciência da Computação')); + fireEvent.change(screen.getByPlaceholderText(/buscar por nome/i), { target: { value: 'CC-001' } }); + expect(screen.getByText('Ciência da Computação')).toBeInTheDocument(); + expect(screen.queryByText('Engenharia de Software')).not.toBeInTheDocument(); + }); + + it('exibe mensagem de nenhum resultado após filtro', async () => { + renderCursos(); + await waitFor(() => screen.getByText('Ciência da Computação')); + fireEvent.change(screen.getByPlaceholderText(/buscar por nome/i), { target: { value: 'xyzinexistente' } }); + await waitFor(() => { + expect(screen.getByText(/Nenhum curso encontrado para os filtros aplicados/)).toBeInTheDocument(); + }); + }); + + it('exibe campo de filtro por instituição', async () => { + renderCursos(); + await waitFor(() => expect(screen.getByText('Todas as instituições')).toBeInTheDocument()); + }); +}); + +describe('Cursos — modal criar', () => { + it('abre modal ao clicar em Novo curso', async () => { + renderCursos(); + await waitFor(() => screen.getByText('Novo curso')); + fireEvent.click(screen.getByText('Novo curso')); + expect(screen.getByText('Novo curso', { selector: 'h2' })).toBeInTheDocument(); + }); + + it('fecha modal ao clicar em Cancelar', async () => { + renderCursos(); + await waitFor(() => screen.getByText('Novo curso')); + fireEvent.click(screen.getByText('Novo curso')); + fireEvent.click(screen.getByText('Cancelar')); + expect(screen.queryByText('Criar curso')).not.toBeInTheDocument(); + }); + + it('não chama criar quando nome está vazio', async () => { + renderCursos(); + await waitFor(() => screen.getByText('Novo curso')); + fireEvent.click(screen.getByText('Novo curso')); + fireEvent.click(screen.getByText('Criar curso')); + expect(mockCursosService.criar).not.toHaveBeenCalled(); + }); + + it('não chama criar quando instituição não está selecionada', async () => { + renderCursos(); + await waitFor(() => screen.getByText('Novo curso')); + fireEvent.click(screen.getByText('Novo curso')); + fireEvent.change(screen.getByPlaceholderText('Ex: Ciência da Computação'), { + target: { value: 'Novo Curso' }, + }); + fireEvent.click(screen.getByText('Criar curso')); + expect(mockCursosService.criar).not.toHaveBeenCalled(); + }); + + it('chama cursosService.criar com dados válidos', async () => { + mockCursosService.criar.mockResolvedValue({ id: 'c-3', nome: 'Novo Curso' }); + renderCursos(); + await waitFor(() => screen.getByText('Novo curso')); + fireEvent.click(screen.getByText('Novo curso')); + + const selects = screen.getAllByRole('combobox'); + const selectInstituicao = selects.find((s) => s.querySelector('option[value="i-1"]')); + fireEvent.change(selectInstituicao!, { target: { value: 'i-1' } }); + fireEvent.change(screen.getByPlaceholderText('Ex: Ciência da Computação'), { + target: { value: 'Novo Curso Teste' }, + }); + fireEvent.click(screen.getByText('Criar curso')); + await waitFor(() => expect(mockCursosService.criar).toHaveBeenCalled()); + }); + + it('modal exibe select de turno e modalidade', async () => { + renderCursos(); + await waitFor(() => screen.getByText('Novo curso')); + fireEvent.click(screen.getByText('Novo curso')); + expect(screen.getByText('Não definido')).toBeInTheDocument(); + expect(screen.getByText('Não definida')).toBeInTheDocument(); + }); +}); + +describe('Cursos — modal editar', () => { + it('abre modal de edição ao clicar em Editar', async () => { + renderCursos(); + await waitFor(() => screen.getAllByText('Editar')); + fireEvent.click(screen.getAllByText('Editar')[0]); + expect(screen.getByText('Salvar alterações')).toBeInTheDocument(); + }); + + it('preenche modal com nome do curso', async () => { + renderCursos(); + await waitFor(() => screen.getAllByText('Editar')); + fireEvent.click(screen.getAllByText('Editar')[0]); + expect(screen.getByDisplayValue('Ciência da Computação')).toBeInTheDocument(); + }); + + it('fecha modal de edição ao clicar em Cancelar', async () => { + renderCursos(); + await waitFor(() => screen.getAllByText('Editar')); + fireEvent.click(screen.getAllByText('Editar')[0]); + fireEvent.click(screen.getByText('Cancelar')); + expect(screen.queryByText('Salvar alterações')).not.toBeInTheDocument(); + }); + + it('chama cursosService.atualizar ao salvar edição válida', async () => { + mockCursosService.atualizar.mockResolvedValue({ id: 'c-1' }); + renderCursos(); + await waitFor(() => screen.getAllByText('Editar')); + fireEvent.click(screen.getAllByText('Editar')[0]); + fireEvent.click(screen.getByText('Salvar alterações')); + await waitFor(() => + expect(mockCursosService.atualizar).toHaveBeenCalledWith( + 'c-1', + expect.objectContaining({ nome: 'Ciência da Computação' }), + ), + ); + }); +}); + +describe('Cursos — alterar ativo', () => { + it('chama cursosService.alterarAtivo ao desativar curso ativo', async () => { + mockCursosService.alterarAtivo.mockResolvedValue({ id: 'c-1', ativo: false }); + renderCursos(); + await waitFor(() => screen.getByText('Desativar')); + fireEvent.click(screen.getByText('Desativar')); + await waitFor(() => + expect(mockCursosService.alterarAtivo).toHaveBeenCalledWith('c-1', false), + ); + }); + + it('chama cursosService.alterarAtivo ao ativar curso inativo', async () => { + mockCursosService.alterarAtivo.mockResolvedValue({ id: 'c-2', ativo: true }); + renderCursos(); + await waitFor(() => screen.getByText('Ativar')); + fireEvent.click(screen.getByText('Ativar')); + await waitFor(() => + expect(mockCursosService.alterarAtivo).toHaveBeenCalledWith('c-2', true), + ); + }); +}); From 41d7670738f14940dc1512a7925c513de545c18c Mon Sep 17 00:00:00 2001 From: icrcode Date: Tue, 9 Jun 2026 13:22:29 -0300 Subject: [PATCH 02/19] =?UTF-8?q?test:=20adicionar=20testes=20para=20a=20p?= =?UTF-8?q?=C3=A1gina=20Usu=C3=A1rios,=20incluindo=20renderiza=C3=A7=C3=A3?= =?UTF-8?q?o,=20busca=20e=20modais?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/test/pages/Instituicoes.test.tsx | 338 ++++++++++++++++ frontend/src/test/pages/Usuarios.test.tsx | 372 ++++++++++++++++++ 2 files changed, 710 insertions(+) create mode 100644 frontend/src/test/pages/Instituicoes.test.tsx create mode 100644 frontend/src/test/pages/Usuarios.test.tsx diff --git a/frontend/src/test/pages/Instituicoes.test.tsx b/frontend/src/test/pages/Instituicoes.test.tsx new file mode 100644 index 0000000..f622166 --- /dev/null +++ b/frontend/src/test/pages/Instituicoes.test.tsx @@ -0,0 +1,338 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { screen, fireEvent, waitFor } from '@testing-library/react'; +import { renderWithProviders, USUARIO_ADMIN } from '../helpers/renderWithProviders'; +import { Instituicoes } from '../../pages/Instituicoes'; + +const { mockInstituicoesService } = vi.hoisted(() => ({ + mockInstituicoesService: { + listar: vi.fn(), + criar: vi.fn(), + atualizar: vi.fn(), + alterarAtiva: vi.fn(), + }, +})); + +vi.mock('../../services/api', () => ({ + default: { + get: vi.fn(), + post: vi.fn(), + put: vi.fn(), + patch: vi.fn(), + interceptors: { request: { use: vi.fn() }, response: { use: vi.fn() } }, + }, +})); + +vi.mock('../../services/instituicoes', () => ({ instituicoesService: mockInstituicoesService })); + +const INST_MOCK = [ + { + id: 'i-1', nome: 'Universidade Federal', sigla: 'UFSC', + cnpj: '00.000.000/0001-00', email_contato: 'contato@ufsc.br', + telefone: '(48) 3333-4444', site: 'https://ufsc.br', + endereco: 'Campus Trindade', cidade: 'Florianópolis', estado: 'SC', + dominios_email: ['ufsc.br', 'grad.ufsc.br'], ativa: true, + criado_em: '', atualizado_em: '', + }, + { + id: 'i-2', nome: 'Instituto Estadual', sigla: 'IFE', + cnpj: null, email_contato: null, telefone: null, site: null, + endereco: null, cidade: null, estado: null, + dominios_email: [], ativa: false, + criado_em: '', atualizado_em: '', + }, +]; + +function renderInstituicoes() { + return renderWithProviders(, { token: 'tok', usuario: USUARIO_ADMIN }); +} + +beforeEach(() => { + vi.clearAllMocks(); + localStorage.clear(); + mockInstituicoesService.listar.mockResolvedValue(INST_MOCK); +}); + +describe('Instituicoes — renderização', () => { + it('exibe título Instituições', async () => { + renderInstituicoes(); + await waitFor(() => expect(screen.getByText('Instituições')).toBeInTheDocument()); + }); + + it('exibe lista de instituições após carregar', async () => { + renderInstituicoes(); + await waitFor(() => { + expect(screen.getByText('Universidade Federal')).toBeInTheDocument(); + expect(screen.getByText('Instituto Estadual')).toBeInTheDocument(); + }); + }); + + it('exibe siglas das instituições', async () => { + renderInstituicoes(); + await waitFor(() => { + expect(screen.getByText('UFSC')).toBeInTheDocument(); + expect(screen.getByText('IFE')).toBeInTheDocument(); + }); + }); + + it('exibe badge Inativa para instituição inativa', async () => { + renderInstituicoes(); + await waitFor(() => expect(screen.getByText('Inativa')).toBeInTheDocument()); + }); + + it('exibe estado de carregamento', () => { + mockInstituicoesService.listar.mockReturnValue(new Promise(() => {})); + renderInstituicoes(); + expect(screen.getByText('Carregando instituições...')).toBeInTheDocument(); + }); + + it('exibe mensagem quando lista está vazia', async () => { + mockInstituicoesService.listar.mockResolvedValue([]); + renderInstituicoes(); + await waitFor(() => + expect(screen.getByText('Nenhuma instituição cadastrada ainda.')).toBeInTheDocument(), + ); + }); + + it('exibe domínios de e-mail da instituição', async () => { + renderInstituicoes(); + await waitFor(() => { + expect(screen.getByText('@ufsc.br')).toBeInTheDocument(); + expect(screen.getByText('@grad.ufsc.br')).toBeInTheDocument(); + }); + }); + + it('exibe Qualquer domínio aceito para instituição sem domínios', async () => { + renderInstituicoes(); + await waitFor(() => expect(screen.getByText('Qualquer domínio aceito')).toBeInTheDocument()); + }); + + it('exibe localização da instituição', async () => { + renderInstituicoes(); + await waitFor(() => expect(screen.getByText(/Florianópolis/)).toBeInTheDocument()); + }); + + it('exibe e-mail de contato como link mailto', async () => { + renderInstituicoes(); + await waitFor(() => { + const link = screen.getByText('contato@ufsc.br'); + expect(link.closest('a')).toHaveAttribute('href', 'mailto:contato@ufsc.br'); + }); + }); + + it('exibe site como link externo', async () => { + renderInstituicoes(); + await waitFor(() => { + const link = screen.getByText('https://ufsc.br'); + expect(link.closest('a')).toHaveAttribute('href', 'https://ufsc.br'); + }); + }); + + it('exibe telefone da instituição', async () => { + renderInstituicoes(); + await waitFor(() => expect(screen.getByText('(48) 3333-4444')).toBeInTheDocument()); + }); + + it('botão Nova instituição está presente', async () => { + renderInstituicoes(); + await waitFor(() => expect(screen.getByText('Nova instituição')).toBeInTheDocument()); + }); +}); + +describe('Instituicoes — busca', () => { + it('filtra por nome', async () => { + renderInstituicoes(); + await waitFor(() => screen.getByText('Universidade Federal')); + fireEvent.change(screen.getByPlaceholderText(/buscar por nome/i), { target: { value: 'Federal' } }); + expect(screen.queryByText('Instituto Estadual')).not.toBeInTheDocument(); + expect(screen.getByText('Universidade Federal')).toBeInTheDocument(); + }); + + it('filtra por sigla', async () => { + renderInstituicoes(); + await waitFor(() => screen.getByText('Universidade Federal')); + fireEvent.change(screen.getByPlaceholderText(/buscar por nome/i), { target: { value: 'IFE' } }); + expect(screen.queryByText('Universidade Federal')).not.toBeInTheDocument(); + expect(screen.getByText('Instituto Estadual')).toBeInTheDocument(); + }); + + it('exibe mensagem quando busca não encontra resultado', async () => { + renderInstituicoes(); + await waitFor(() => screen.getByText('Universidade Federal')); + fireEvent.change(screen.getByPlaceholderText(/buscar por nome/i), { target: { value: 'xyzinexistente' } }); + await waitFor(() => + expect(screen.getByText('Nenhuma instituição encontrada.')).toBeInTheDocument(), + ); + }); +}); + +describe('Instituicoes — modal criar', () => { + it('abre modal ao clicar em Nova instituição', async () => { + renderInstituicoes(); + await waitFor(() => screen.getByText('Nova instituição')); + fireEvent.click(screen.getByText('Nova instituição')); + expect(screen.getByText('Criar instituição')).toBeInTheDocument(); + }); + + it('fecha modal ao clicar em Cancelar', async () => { + renderInstituicoes(); + await waitFor(() => screen.getByText('Nova instituição')); + fireEvent.click(screen.getByText('Nova instituição')); + fireEvent.click(screen.getByText('Cancelar')); + expect(screen.queryByText('Criar instituição')).not.toBeInTheDocument(); + }); + + it('não chama criar quando nome e sigla estão vazios', async () => { + renderInstituicoes(); + await waitFor(() => screen.getByText('Nova instituição')); + fireEvent.click(screen.getByText('Nova instituição')); + fireEvent.click(screen.getByText('Criar instituição')); + expect(mockInstituicoesService.criar).not.toHaveBeenCalled(); + }); + + it('chama instituicoesService.criar com nome e sigla preenchidos', async () => { + mockInstituicoesService.criar.mockResolvedValue({ id: 'i-3' }); + renderInstituicoes(); + await waitFor(() => screen.getByText('Nova instituição')); + fireEvent.click(screen.getByText('Nova instituição')); + + fireEvent.change(screen.getByPlaceholderText('Ex: Universidade Federal de SC'), { + target: { value: 'Nova Universidade' }, + }); + fireEvent.change(screen.getByPlaceholderText('UFSC'), { target: { value: 'NU' } }); + fireEvent.click(screen.getByText('Criar instituição')); + await waitFor(() => expect(mockInstituicoesService.criar).toHaveBeenCalled()); + }); + + it('permite adicionar domínio de e-mail via botão Adicionar', async () => { + renderInstituicoes(); + await waitFor(() => screen.getByText('Nova instituição')); + fireEvent.click(screen.getByText('Nova instituição')); + + fireEvent.change(screen.getByPlaceholderText('ex: ufsc.br'), { + target: { value: 'novo.edu.br' }, + }); + fireEvent.click(screen.getByText('Adicionar')); + expect(screen.getByText('@novo.edu.br')).toBeInTheDocument(); + }); + + it('permite adicionar domínio via tecla Enter', async () => { + renderInstituicoes(); + await waitFor(() => screen.getByText('Nova instituição')); + fireEvent.click(screen.getByText('Nova instituição')); + + const input = screen.getByPlaceholderText('ex: ufsc.br'); + fireEvent.change(input, { target: { value: 'pressenter.edu' } }); + fireEvent.keyDown(input, { key: 'Enter' }); + expect(screen.getByText('@pressenter.edu')).toBeInTheDocument(); + }); + + it('permite remover domínio adicionado', async () => { + renderInstituicoes(); + await waitFor(() => screen.getByText('Nova instituição')); + fireEvent.click(screen.getByText('Nova instituição')); + + fireEvent.change(screen.getByPlaceholderText('ex: ufsc.br'), { + target: { value: 'remover.edu' }, + }); + fireEvent.click(screen.getByText('Adicionar')); + expect(screen.getByText('@remover.edu')).toBeInTheDocument(); + + fireEvent.click(screen.getByText('×')); + expect(screen.queryByText('@remover.edu')).not.toBeInTheDocument(); + }); + + it('não adiciona domínio duplicado', async () => { + renderInstituicoes(); + await waitFor(() => screen.getByText('Nova instituição')); + fireEvent.click(screen.getByText('Nova instituição')); + + const input = screen.getByPlaceholderText('ex: ufsc.br'); + fireEvent.change(input, { target: { value: 'duplo.edu' } }); + fireEvent.click(screen.getByText('Adicionar')); + fireEvent.change(input, { target: { value: 'duplo.edu' } }); + fireEvent.click(screen.getByText('Adicionar')); + + const tags = screen.getAllByText('@duplo.edu'); + expect(tags).toHaveLength(1); + }); + + it('exibe campos opcionais no formulário', async () => { + renderInstituicoes(); + await waitFor(() => screen.getByText('Nova instituição')); + fireEvent.click(screen.getByText('Nova instituição')); + expect(screen.getByPlaceholderText('contato@instituicao.edu.br')).toBeInTheDocument(); + expect(screen.getByPlaceholderText('(48) 3333-4444')).toBeInTheDocument(); + expect(screen.getByPlaceholderText('00.000.000/0001-00')).toBeInTheDocument(); + expect(screen.getByPlaceholderText('https://www.instituicao.edu.br')).toBeInTheDocument(); + }); +}); + +describe('Instituicoes — modal editar', () => { + it('abre modal de edição ao clicar em Editar', async () => { + renderInstituicoes(); + await waitFor(() => screen.getAllByText('Editar')); + fireEvent.click(screen.getAllByText('Editar')[0]); + expect(screen.getByText('Salvar alterações')).toBeInTheDocument(); + }); + + it('preenche modal com dados da instituição', async () => { + renderInstituicoes(); + await waitFor(() => screen.getAllByText('Editar')); + fireEvent.click(screen.getAllByText('Editar')[0]); + expect(screen.getByDisplayValue('Universidade Federal')).toBeInTheDocument(); + expect(screen.getByDisplayValue('UFSC')).toBeInTheDocument(); + }); + + it('fecha modal de edição ao clicar em Cancelar', async () => { + renderInstituicoes(); + await waitFor(() => screen.getAllByText('Editar')); + fireEvent.click(screen.getAllByText('Editar')[0]); + fireEvent.click(screen.getByText('Cancelar')); + expect(screen.queryByText('Salvar alterações')).not.toBeInTheDocument(); + }); + + it('chama instituicoesService.atualizar ao salvar', async () => { + mockInstituicoesService.atualizar.mockResolvedValue({ id: 'i-1' }); + renderInstituicoes(); + await waitFor(() => screen.getAllByText('Editar')); + fireEvent.click(screen.getAllByText('Editar')[0]); + fireEvent.click(screen.getByText('Salvar alterações')); + await waitFor(() => + expect(mockInstituicoesService.atualizar).toHaveBeenCalledWith( + 'i-1', + expect.objectContaining({ nome: 'Universidade Federal', sigla: 'UFSC' }), + ), + ); + }); + + it('não chama atualizar quando nome é apagado', async () => { + renderInstituicoes(); + await waitFor(() => screen.getAllByText('Editar')); + fireEvent.click(screen.getAllByText('Editar')[0]); + fireEvent.change(screen.getByDisplayValue('Universidade Federal'), { target: { value: '' } }); + fireEvent.click(screen.getByText('Salvar alterações')); + expect(mockInstituicoesService.atualizar).not.toHaveBeenCalled(); + }); +}); + +describe('Instituicoes — alterar ativa', () => { + it('chama instituicoesService.alterarAtiva ao desativar instituição ativa', async () => { + mockInstituicoesService.alterarAtiva.mockResolvedValue({ id: 'i-1', ativa: false }); + renderInstituicoes(); + await waitFor(() => screen.getByText('Desativar')); + fireEvent.click(screen.getByText('Desativar')); + await waitFor(() => + expect(mockInstituicoesService.alterarAtiva).toHaveBeenCalledWith('i-1', false), + ); + }); + + it('chama instituicoesService.alterarAtiva ao ativar instituição inativa', async () => { + mockInstituicoesService.alterarAtiva.mockResolvedValue({ id: 'i-2', ativa: true }); + renderInstituicoes(); + await waitFor(() => screen.getByText('Ativar')); + fireEvent.click(screen.getByText('Ativar')); + await waitFor(() => + expect(mockInstituicoesService.alterarAtiva).toHaveBeenCalledWith('i-2', true), + ); + }); +}); diff --git a/frontend/src/test/pages/Usuarios.test.tsx b/frontend/src/test/pages/Usuarios.test.tsx new file mode 100644 index 0000000..82b367e --- /dev/null +++ b/frontend/src/test/pages/Usuarios.test.tsx @@ -0,0 +1,372 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { screen, fireEvent, waitFor } from '@testing-library/react'; +import { renderWithProviders, USUARIO_ADMIN } from '../helpers/renderWithProviders'; +import { Usuarios } from '../../pages/Usuarios'; + +const { mockUsuariosService, mockCursosService } = vi.hoisted(() => ({ + mockUsuariosService: { + listar: vi.fn(), + criar: vi.fn(), + atualizar: vi.fn(), + alterarAtivo: vi.fn(), + }, + mockCursosService: { + listar: vi.fn(), + }, +})); + +vi.mock('../../services/api', () => ({ + default: { + get: vi.fn(), + post: vi.fn(), + put: vi.fn(), + patch: vi.fn(), + interceptors: { request: { use: vi.fn() }, response: { use: vi.fn() } }, + }, +})); + +vi.mock('../../services/usuarios', () => ({ usuariosService: mockUsuariosService })); +vi.mock('../../services/cursos', () => ({ cursosService: mockCursosService })); + +const USUARIOS_MOCK = [ + { + id: 'u-1', nome: 'João Silva', email: 'joao@ufsc.br', perfil: 'estudante' as const, + matricula: '2021001', cpf: '000.000.000-00', endereco: 'Rua A, 123', + curso_id: 'c-1', instituicao_id: 'i-1', instituicao_nome: 'UFSC', ativo: true, + }, + { + id: 'u-2', nome: 'Maria Coord', email: 'maria@ufsc.br', perfil: 'coordenador' as const, + matricula: null, cpf: null, endereco: null, + curso_id: null, instituicao_id: 'i-1', instituicao_nome: 'UFSC', ativo: false, + }, + { + id: 'u-3', nome: 'Admin User', email: 'admin@ufsc.br', perfil: 'admin' as const, + matricula: null, cpf: null, endereco: null, + curso_id: null, instituicao_id: null, instituicao_nome: null, ativo: true, + }, +]; + +const CURSOS_MOCK = [ + { + id: 'c-1', nome: 'Ciência da Computação', codigo: 'CC-001', + carga_horaria_complementar: 200, turno: null, modalidade: null, + instituicao_id: 'i-1', instituicao_nome: 'UFSC', instituicao_sigla: 'UFSC', + ativo: true, criado_em: '', atualizado_em: '', + }, +]; + +function renderUsuarios() { + return renderWithProviders(, { token: 'tok', usuario: USUARIO_ADMIN }); +} + +beforeEach(() => { + vi.clearAllMocks(); + localStorage.clear(); + mockUsuariosService.listar.mockResolvedValue(USUARIOS_MOCK); + mockCursosService.listar.mockResolvedValue(CURSOS_MOCK); +}); + +describe('Usuarios — renderização', () => { + it('exibe título Usuários', async () => { + renderUsuarios(); + await waitFor(() => expect(screen.getByText('Usuários')).toBeInTheDocument()); + }); + + it('exibe lista de usuários após carregar', async () => { + renderUsuarios(); + await waitFor(() => { + expect(screen.getByText('João Silva')).toBeInTheDocument(); + expect(screen.getByText('Maria Coord')).toBeInTheDocument(); + expect(screen.getByText('Admin User')).toBeInTheDocument(); + }); + }); + + it('exibe perfil label dos usuários', async () => { + renderUsuarios(); + await waitFor(() => { + expect(screen.getByText('Estudante')).toBeInTheDocument(); + expect(screen.getByText('Coordenador')).toBeInTheDocument(); + expect(screen.getByText('Administrador')).toBeInTheDocument(); + }); + }); + + it('exibe badge Inativo para usuário inativo', async () => { + renderUsuarios(); + await waitFor(() => expect(screen.getByText('Inativo')).toBeInTheDocument()); + }); + + it('exibe estado de carregamento', () => { + mockUsuariosService.listar.mockReturnValue(new Promise(() => {})); + renderUsuarios(); + expect(screen.getByText('Carregando usuários...')).toBeInTheDocument(); + }); + + it('exibe mensagem quando não há usuários', async () => { + mockUsuariosService.listar.mockResolvedValue([]); + renderUsuarios(); + await waitFor(() => + expect(screen.getByText('Nenhum usuário cadastrado.')).toBeInTheDocument(), + ); + }); + + it('exibe matrícula do estudante', async () => { + renderUsuarios(); + await waitFor(() => expect(screen.getByText('Matrícula: 2021001')).toBeInTheDocument()); + }); + + it('exibe CPF quando disponível', async () => { + renderUsuarios(); + await waitFor(() => expect(screen.getByText('CPF: 000.000.000-00')).toBeInTheDocument()); + }); + + it('exibe endereço quando disponível', async () => { + renderUsuarios(); + await waitFor(() => expect(screen.getByText('Rua A, 123')).toBeInTheDocument()); + }); + + it('exibe iniciais no avatar', async () => { + renderUsuarios(); + await waitFor(() => expect(screen.getByText('JS')).toBeInTheDocument()); + }); + + it('exibe nome da instituição', async () => { + renderUsuarios(); + await waitFor(() => expect(screen.getAllByText('UFSC').length).toBeGreaterThan(0)); + }); + + it('botão Novo usuário está presente', async () => { + renderUsuarios(); + await waitFor(() => expect(screen.getByText('Novo usuário')).toBeInTheDocument()); + }); +}); + +describe('Usuarios — busca', () => { + it('filtra por nome', async () => { + renderUsuarios(); + await waitFor(() => screen.getByText('João Silva')); + fireEvent.change(screen.getByPlaceholderText(/buscar por nome/i), { target: { value: 'Maria' } }); + expect(screen.queryByText('João Silva')).not.toBeInTheDocument(); + expect(screen.getByText('Maria Coord')).toBeInTheDocument(); + }); + + it('filtra por e-mail', async () => { + renderUsuarios(); + await waitFor(() => screen.getByText('João Silva')); + fireEvent.change(screen.getByPlaceholderText(/buscar por nome/i), { target: { value: 'admin@ufsc' } }); + expect(screen.queryByText('João Silva')).not.toBeInTheDocument(); + expect(screen.getByText('Admin User')).toBeInTheDocument(); + }); + + it('filtra por matrícula', async () => { + renderUsuarios(); + await waitFor(() => screen.getByText('João Silva')); + fireEvent.change(screen.getByPlaceholderText(/buscar por nome/i), { target: { value: '2021001' } }); + expect(screen.getByText('João Silva')).toBeInTheDocument(); + expect(screen.queryByText('Maria Coord')).not.toBeInTheDocument(); + }); + + it('exibe mensagem quando busca não encontra nada', async () => { + renderUsuarios(); + await waitFor(() => screen.getByText('João Silva')); + fireEvent.change(screen.getByPlaceholderText(/buscar por nome/i), { target: { value: 'xyzinexistente' } }); + await waitFor(() => + expect(screen.getByText('Nenhum usuário encontrado.')).toBeInTheDocument(), + ); + }); +}); + +describe('Usuarios — modal criar', () => { + it('abre modal ao clicar em Novo usuário', async () => { + renderUsuarios(); + await waitFor(() => screen.getByText('Novo usuário')); + fireEvent.click(screen.getByText('Novo usuário')); + expect(screen.getByText('Criar usuário')).toBeInTheDocument(); + }); + + it('exibe campo de senha inicial no modo criar', async () => { + renderUsuarios(); + await waitFor(() => screen.getByText('Novo usuário')); + fireEvent.click(screen.getByText('Novo usuário')); + expect(screen.getByText('Senha inicial')).toBeInTheDocument(); + }); + + it('fecha modal ao clicar em Cancelar', async () => { + renderUsuarios(); + await waitFor(() => screen.getByText('Novo usuário')); + fireEvent.click(screen.getByText('Novo usuário')); + fireEvent.click(screen.getByText('Cancelar')); + expect(screen.queryByText('Criar usuário')).not.toBeInTheDocument(); + }); + + it('não chama criar quando nome está vazio', async () => { + renderUsuarios(); + await waitFor(() => screen.getByText('Novo usuário')); + fireEvent.click(screen.getByText('Novo usuário')); + fireEvent.click(screen.getByText('Criar usuário')); + expect(mockUsuariosService.criar).not.toHaveBeenCalled(); + }); + + it('não chama criar quando senha tem menos de 6 caracteres', async () => { + renderUsuarios(); + await waitFor(() => screen.getByText('Novo usuário')); + fireEvent.click(screen.getByText('Novo usuário')); + + fireEvent.change(screen.getByPlaceholderText('Nome completo'), { target: { value: 'Novo User' } }); + fireEvent.change(screen.getByPlaceholderText('email@instituicao.edu.br'), { + target: { value: 'novo@ufsc.br' }, + }); + fireEvent.change(screen.getByPlaceholderText('Mínimo 6 caracteres'), { target: { value: '123' } }); + fireEvent.click(screen.getByText('Criar usuário')); + expect(mockUsuariosService.criar).not.toHaveBeenCalled(); + }); + + it('chama usuariosService.criar com dados válidos', async () => { + mockUsuariosService.criar.mockResolvedValue({ id: 'u-4' }); + renderUsuarios(); + await waitFor(() => screen.getByText('Novo usuário')); + fireEvent.click(screen.getByText('Novo usuário')); + + fireEvent.change(screen.getByPlaceholderText('Nome completo'), { target: { value: 'Novo Usuário' } }); + fireEvent.change(screen.getByPlaceholderText('email@instituicao.edu.br'), { + target: { value: 'novo@ufsc.br' }, + }); + fireEvent.change(screen.getByPlaceholderText('Mínimo 6 caracteres'), { + target: { value: 'senha123' }, + }); + fireEvent.click(screen.getByText('Criar usuário')); + await waitFor(() => expect(mockUsuariosService.criar).toHaveBeenCalled()); + }); + + it('exibe campo de matrícula quando perfil é estudante', async () => { + renderUsuarios(); + await waitFor(() => screen.getByText('Novo usuário')); + fireEvent.click(screen.getByText('Novo usuário')); + expect(screen.getByPlaceholderText('Número de matrícula')).toBeInTheDocument(); + }); + + it('exibe campo de CPF quando perfil é coordenador', async () => { + renderUsuarios(); + await waitFor(() => screen.getByText('Novo usuário')); + fireEvent.click(screen.getByText('Novo usuário')); + + const selectPerfil = screen.getByDisplayValue('Estudante'); + fireEvent.change(selectPerfil, { target: { value: 'coordenador' } }); + expect(screen.getByPlaceholderText('000.000.000-00')).toBeInTheDocument(); + }); + + it('exibe campo de endereço quando perfil é coordenador', async () => { + renderUsuarios(); + await waitFor(() => screen.getByText('Novo usuário')); + fireEvent.click(screen.getByText('Novo usuário')); + + const selectPerfil = screen.getByDisplayValue('Estudante'); + fireEvent.change(selectPerfil, { target: { value: 'coordenador' } }); + expect(screen.getByPlaceholderText(/Rua, número, bairro/i)).toBeInTheDocument(); + }); + + it('não exibe CPF quando perfil é admin', async () => { + renderUsuarios(); + await waitFor(() => screen.getByText('Novo usuário')); + fireEvent.click(screen.getByText('Novo usuário')); + + const selectPerfil = screen.getByDisplayValue('Estudante'); + fireEvent.change(selectPerfil, { target: { value: 'admin' } }); + expect(screen.queryByPlaceholderText('000.000.000-00')).not.toBeInTheDocument(); + }); + + it('exibe cursos agrupados por instituição no select', async () => { + renderUsuarios(); + await waitFor(() => screen.getByText('Novo usuário')); + fireEvent.click(screen.getByText('Novo usuário')); + const optgroup = document.querySelector('optgroup[label="UFSC"]'); + expect(optgroup).toBeTruthy(); + }); +}); + +describe('Usuarios — modal editar', () => { + it('abre modal de edição ao clicar em Editar', async () => { + renderUsuarios(); + await waitFor(() => screen.getAllByText('Editar')); + fireEvent.click(screen.getAllByText('Editar')[0]); + expect(screen.getByText('Salvar')).toBeInTheDocument(); + }); + + it('preenche modal com dados do usuário', async () => { + renderUsuarios(); + await waitFor(() => screen.getAllByText('Editar')); + fireEvent.click(screen.getAllByText('Editar')[0]); + expect(screen.getByDisplayValue('João Silva')).toBeInTheDocument(); + expect(screen.getByDisplayValue('joao@ufsc.br')).toBeInTheDocument(); + }); + + it('não exibe campo de senha no modo edição', async () => { + renderUsuarios(); + await waitFor(() => screen.getAllByText('Editar')); + fireEvent.click(screen.getAllByText('Editar')[0]); + expect(screen.queryByText('Senha inicial')).not.toBeInTheDocument(); + }); + + it('fecha modal de edição ao clicar em Cancelar', async () => { + renderUsuarios(); + await waitFor(() => screen.getAllByText('Editar')); + fireEvent.click(screen.getAllByText('Editar')[0]); + fireEvent.click(screen.getByText('Cancelar')); + expect(screen.queryByText('Salvar')).not.toBeInTheDocument(); + }); + + it('chama usuariosService.atualizar ao salvar edição válida', async () => { + mockUsuariosService.atualizar.mockResolvedValue({ id: 'u-1' }); + renderUsuarios(); + await waitFor(() => screen.getAllByText('Editar')); + fireEvent.click(screen.getAllByText('Editar')[0]); + fireEvent.click(screen.getByText('Salvar')); + await waitFor(() => + expect(mockUsuariosService.atualizar).toHaveBeenCalledWith( + 'u-1', + expect.objectContaining({ nome: 'João Silva' }), + ), + ); + }); + + it('não chama atualizar quando nome é apagado', async () => { + renderUsuarios(); + await waitFor(() => screen.getAllByText('Editar')); + fireEvent.click(screen.getAllByText('Editar')[0]); + fireEvent.change(screen.getByDisplayValue('João Silva'), { target: { value: '' } }); + fireEvent.click(screen.getByText('Salvar')); + expect(mockUsuariosService.atualizar).not.toHaveBeenCalled(); + }); + + it('não chama atualizar quando e-mail é inválido', async () => { + renderUsuarios(); + await waitFor(() => screen.getAllByText('Editar')); + fireEvent.click(screen.getAllByText('Editar')[0]); + fireEvent.change(screen.getByDisplayValue('joao@ufsc.br'), { + target: { value: 'emailsemarrobase' }, + }); + fireEvent.click(screen.getByText('Salvar')); + expect(mockUsuariosService.atualizar).not.toHaveBeenCalled(); + }); +}); + +describe('Usuarios — alterar ativo', () => { + it('chama usuariosService.alterarAtivo ao desativar usuário ativo', async () => { + mockUsuariosService.alterarAtivo.mockResolvedValue({ id: 'u-1', ativo: false }); + renderUsuarios(); + await waitFor(() => screen.getAllByText('Desativar')); + fireEvent.click(screen.getAllByText('Desativar')[0]); + await waitFor(() => + expect(mockUsuariosService.alterarAtivo).toHaveBeenCalledWith('u-1', false), + ); + }); + + it('chama usuariosService.alterarAtivo ao ativar usuário inativo', async () => { + mockUsuariosService.alterarAtivo.mockResolvedValue({ id: 'u-2', ativo: true }); + renderUsuarios(); + await waitFor(() => screen.getByText('Ativar')); + fireEvent.click(screen.getByText('Ativar')); + await waitFor(() => + expect(mockUsuariosService.alterarAtivo).toHaveBeenCalledWith('u-2', true), + ); + }); +}); From 6610a9ea90d7f1c1e00b5c150614bae4465c2e57 Mon Sep 17 00:00:00 2001 From: icrcode Date: Tue, 9 Jun 2026 13:26:14 -0300 Subject: [PATCH 03/19] =?UTF-8?q?test:=20atualizar=20sele=C3=A7=C3=A3o=20d?= =?UTF-8?q?e=20institui=C3=A7=C3=A3o=20no=20modal=20de=20cria=C3=A7=C3=A3o?= =?UTF-8?q?=20de=20curso?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/test/pages/Cursos.test.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/test/pages/Cursos.test.tsx b/frontend/src/test/pages/Cursos.test.tsx index 6694131..cfa1f9b 100644 --- a/frontend/src/test/pages/Cursos.test.tsx +++ b/frontend/src/test/pages/Cursos.test.tsx @@ -206,9 +206,9 @@ describe('Cursos — modal criar', () => { await waitFor(() => screen.getByText('Novo curso')); fireEvent.click(screen.getByText('Novo curso')); - const selects = screen.getAllByRole('combobox'); - const selectInstituicao = selects.find((s) => s.querySelector('option[value="i-1"]')); - fireEvent.change(selectInstituicao!, { target: { value: 'i-1' } }); + // Usa o texto único do placeholder do select dentro do modal + const instSelect = screen.getByText('Selecione uma instituição...').closest('select')!; + fireEvent.change(instSelect, { target: { value: 'i-1' } }); fireEvent.change(screen.getByPlaceholderText('Ex: Ciência da Computação'), { target: { value: 'Novo Curso Teste' }, }); From ea9f5b6fb4154918083e6d7cf14061a25380a9bc Mon Sep 17 00:00:00 2001 From: icrcode Date: Tue, 9 Jun 2026 13:28:31 -0300 Subject: [PATCH 04/19] =?UTF-8?q?test:=20adicionar=20testes=20para=20trata?= =?UTF-8?q?mento=20de=20erros=20nas=20opera=C3=A7=C3=B5es=20de=20cria?= =?UTF-8?q?=C3=A7=C3=A3o,=20atualiza=C3=A7=C3=A3o=20e=20altera=C3=A7=C3=A3?= =?UTF-8?q?o=20de=20status=20de=20cursos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/test/pages/Cursos.test.tsx | 78 +++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/frontend/src/test/pages/Cursos.test.tsx b/frontend/src/test/pages/Cursos.test.tsx index cfa1f9b..3b9639d 100644 --- a/frontend/src/test/pages/Cursos.test.tsx +++ b/frontend/src/test/pages/Cursos.test.tsx @@ -284,3 +284,81 @@ describe('Cursos — alterar ativo', () => { ); }); }); + +describe('Cursos — erros de mutation', () => { + it('exibe erro personalizado da API ao falhar criação', async () => { + mockCursosService.criar.mockRejectedValue({ + response: { data: { erro: 'Código duplicado' } }, + }); + renderCursos(); + await waitFor(() => screen.getByText('Novo curso')); + fireEvent.click(screen.getByText('Novo curso')); + + const instSelect = screen.getByText('Selecione uma instituição...').closest('select')!; + fireEvent.change(instSelect, { target: { value: 'i-1' } }); + fireEvent.change(screen.getByPlaceholderText('Ex: Ciência da Computação'), { + target: { value: 'Curso Duplicado' }, + }); + fireEvent.click(screen.getByText('Criar curso')); + await waitFor(() => expect(mockCursosService.criar).toHaveBeenCalled()); + }); + + it('exibe mensagem genérica ao falhar criação sem detalhe da API', async () => { + mockCursosService.criar.mockRejectedValue(new Error('Network error')); + renderCursos(); + await waitFor(() => screen.getByText('Novo curso')); + fireEvent.click(screen.getByText('Novo curso')); + + const instSelect = screen.getByText('Selecione uma instituição...').closest('select')!; + fireEvent.change(instSelect, { target: { value: 'i-1' } }); + fireEvent.change(screen.getByPlaceholderText('Ex: Ciência da Computação'), { + target: { value: 'Curso Erro' }, + }); + fireEvent.click(screen.getByText('Criar curso')); + await waitFor(() => expect(mockCursosService.criar).toHaveBeenCalled()); + }); + + it('exibe erro ao falhar atualização de curso', async () => { + mockCursosService.atualizar.mockRejectedValue(new Error('fail')); + renderCursos(); + await waitFor(() => screen.getAllByText('Editar')); + fireEvent.click(screen.getAllByText('Editar')[0]); + fireEvent.click(screen.getByText('Salvar alterações')); + await waitFor(() => expect(mockCursosService.atualizar).toHaveBeenCalled()); + }); + + it('exibe erro ao falhar alteração de status', async () => { + mockCursosService.alterarAtivo.mockRejectedValue(new Error('fail')); + renderCursos(); + await waitFor(() => screen.getByText('Desativar')); + fireEvent.click(screen.getByText('Desativar')); + await waitFor(() => expect(mockCursosService.alterarAtivo).toHaveBeenCalled()); + }); +}); + +describe('Cursos — fechar modal pelo botão ×', () => { + it('fecha modal de criação pelo botão × do cabeçalho', async () => { + renderCursos(); + await waitFor(() => screen.getByText('Novo curso')); + fireEvent.click(screen.getByText('Novo curso')); + + // Clica no botão × do cabeçalho do modal (SVG button sem texto) + const overlay = document.querySelector('.fixed.inset-0'); + const closeBtn = overlay?.querySelector('button'); + expect(closeBtn).toBeTruthy(); + fireEvent.click(closeBtn!); + expect(screen.queryByText('Criar curso')).not.toBeInTheDocument(); + }); + + it('fecha modal de edição pelo botão × do cabeçalho', async () => { + renderCursos(); + await waitFor(() => screen.getAllByText('Editar')); + fireEvent.click(screen.getAllByText('Editar')[0]); + + const overlay = document.querySelector('.fixed.inset-0'); + const closeBtn = overlay?.querySelector('button'); + expect(closeBtn).toBeTruthy(); + fireEvent.click(closeBtn!); + expect(screen.queryByText('Salvar alterações')).not.toBeInTheDocument(); + }); +}); From 9bea9e24363ba76d6e42eac4f9dd484784208c20 Mon Sep 17 00:00:00 2001 From: icrcode Date: Tue, 9 Jun 2026 13:28:40 -0300 Subject: [PATCH 05/19] =?UTF-8?q?test:=20adicionar=20testes=20para=20trata?= =?UTF-8?q?mento=20de=20erros=20nas=20opera=C3=A7=C3=B5es=20de=20cria?= =?UTF-8?q?=C3=A7=C3=A3o,=20atualiza=C3=A7=C3=A3o=20e=20altera=C3=A7=C3=A3?= =?UTF-8?q?o=20de=20status=20de=20institui=C3=A7=C3=B5es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/test/pages/Instituicoes.test.tsx | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/frontend/src/test/pages/Instituicoes.test.tsx b/frontend/src/test/pages/Instituicoes.test.tsx index f622166..1d17574 100644 --- a/frontend/src/test/pages/Instituicoes.test.tsx +++ b/frontend/src/test/pages/Instituicoes.test.tsx @@ -336,3 +336,76 @@ describe('Instituicoes — alterar ativa', () => { ); }); }); + +describe('Instituicoes — erros de mutation', () => { + it('exibe erro personalizado da API ao falhar criação', async () => { + mockInstituicoesService.criar.mockRejectedValue({ + response: { data: { erro: 'CNPJ já cadastrado' } }, + }); + renderInstituicoes(); + await waitFor(() => screen.getByText('Nova instituição')); + fireEvent.click(screen.getByText('Nova instituição')); + fireEvent.change(screen.getByPlaceholderText('Ex: Universidade Federal de SC'), { + target: { value: 'Instituição Duplicada' }, + }); + fireEvent.change(screen.getByPlaceholderText('UFSC'), { target: { value: 'ID' } }); + fireEvent.click(screen.getByText('Criar instituição')); + await waitFor(() => expect(mockInstituicoesService.criar).toHaveBeenCalled()); + }); + + it('exibe mensagem genérica ao falhar criação sem detalhe da API', async () => { + mockInstituicoesService.criar.mockRejectedValue(new Error('Network error')); + renderInstituicoes(); + await waitFor(() => screen.getByText('Nova instituição')); + fireEvent.click(screen.getByText('Nova instituição')); + fireEvent.change(screen.getByPlaceholderText('Ex: Universidade Federal de SC'), { + target: { value: 'Nova Inst' }, + }); + fireEvent.change(screen.getByPlaceholderText('UFSC'), { target: { value: 'NI' } }); + fireEvent.click(screen.getByText('Criar instituição')); + await waitFor(() => expect(mockInstituicoesService.criar).toHaveBeenCalled()); + }); + + it('exibe erro ao falhar atualização de instituição', async () => { + mockInstituicoesService.atualizar.mockRejectedValue(new Error('fail')); + renderInstituicoes(); + await waitFor(() => screen.getAllByText('Editar')); + fireEvent.click(screen.getAllByText('Editar')[0]); + fireEvent.click(screen.getByText('Salvar alterações')); + await waitFor(() => expect(mockInstituicoesService.atualizar).toHaveBeenCalled()); + }); + + it('exibe erro ao falhar alteração de status', async () => { + mockInstituicoesService.alterarAtiva.mockRejectedValue(new Error('fail')); + renderInstituicoes(); + await waitFor(() => screen.getByText('Desativar')); + fireEvent.click(screen.getByText('Desativar')); + await waitFor(() => expect(mockInstituicoesService.alterarAtiva).toHaveBeenCalled()); + }); +}); + +describe('Instituicoes — fechar modal pelo botão ×', () => { + it('fecha modal de criação pelo botão × do cabeçalho', async () => { + renderInstituicoes(); + await waitFor(() => screen.getByText('Nova instituição')); + fireEvent.click(screen.getByText('Nova instituição')); + + const overlay = document.querySelector('.fixed.inset-0'); + const closeBtn = overlay?.querySelector('button'); + expect(closeBtn).toBeTruthy(); + fireEvent.click(closeBtn!); + expect(screen.queryByText('Criar instituição')).not.toBeInTheDocument(); + }); + + it('fecha modal de edição pelo botão × do cabeçalho', async () => { + renderInstituicoes(); + await waitFor(() => screen.getAllByText('Editar')); + fireEvent.click(screen.getAllByText('Editar')[0]); + + const overlay = document.querySelector('.fixed.inset-0'); + const closeBtn = overlay?.querySelector('button'); + expect(closeBtn).toBeTruthy(); + fireEvent.click(closeBtn!); + expect(screen.queryByText('Salvar alterações')).not.toBeInTheDocument(); + }); +}); From c3165f3a0b623c55af52d0df8470e6780f2ee0ea Mon Sep 17 00:00:00 2001 From: icrcode Date: Tue, 9 Jun 2026 13:29:47 -0300 Subject: [PATCH 06/19] =?UTF-8?q?test:=20adicionar=20testes=20para=20trata?= =?UTF-8?q?mento=20de=20erros=20nas=20opera=C3=A7=C3=B5es=20de=20cria?= =?UTF-8?q?=C3=A7=C3=A3o=20e=20atualiza=C3=A7=C3=A3o=20na=20p=C3=A1gina=20?= =?UTF-8?q?Usu=C3=A1rios=20e=20verificar=20estados=20na=20p=C3=A1gina=20Ve?= =?UTF-8?q?rificar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/test/pages/Usuarios.test.tsx | 92 +++++++++++++++++++++ frontend/src/test/pages/Verificar.test.tsx | 95 ++++++++++++++++++++-- frontend/src/test/services/api.test.ts | 79 ++++++++++++++++++ 3 files changed, 260 insertions(+), 6 deletions(-) create mode 100644 frontend/src/test/services/api.test.ts diff --git a/frontend/src/test/pages/Usuarios.test.tsx b/frontend/src/test/pages/Usuarios.test.tsx index 82b367e..f47f729 100644 --- a/frontend/src/test/pages/Usuarios.test.tsx +++ b/frontend/src/test/pages/Usuarios.test.tsx @@ -370,3 +370,95 @@ describe('Usuarios — alterar ativo', () => { ); }); }); + +describe('Usuarios — erros de mutation', () => { + it('exibe erro personalizado da API ao falhar criação', async () => { + mockUsuariosService.criar.mockRejectedValue({ + response: { data: { erro: 'E-mail já cadastrado' } }, + }); + renderUsuarios(); + await waitFor(() => screen.getByText('Novo usuário')); + fireEvent.click(screen.getByText('Novo usuário')); + + fireEvent.change(screen.getByPlaceholderText('Nome completo'), { target: { value: 'Duplicado' } }); + fireEvent.change(screen.getByPlaceholderText('email@instituicao.edu.br'), { + target: { value: 'dup@ufsc.br' }, + }); + fireEvent.change(screen.getByPlaceholderText('Mínimo 6 caracteres'), { + target: { value: 'senha123' }, + }); + fireEvent.click(screen.getByText('Criar usuário')); + await waitFor(() => expect(mockUsuariosService.criar).toHaveBeenCalled()); + }); + + it('exibe mensagem genérica ao falhar criação sem detalhe da API', async () => { + mockUsuariosService.criar.mockRejectedValue(new Error('Network error')); + renderUsuarios(); + await waitFor(() => screen.getByText('Novo usuário')); + fireEvent.click(screen.getByText('Novo usuário')); + + fireEvent.change(screen.getByPlaceholderText('Nome completo'), { target: { value: 'Novo User' } }); + fireEvent.change(screen.getByPlaceholderText('email@instituicao.edu.br'), { + target: { value: 'novo@ufsc.br' }, + }); + fireEvent.change(screen.getByPlaceholderText('Mínimo 6 caracteres'), { + target: { value: 'senha123' }, + }); + fireEvent.click(screen.getByText('Criar usuário')); + await waitFor(() => expect(mockUsuariosService.criar).toHaveBeenCalled()); + }); + + it('exibe erro personalizado da API ao falhar atualização', async () => { + mockUsuariosService.atualizar.mockRejectedValue({ + response: { data: { erro: 'E-mail em uso' } }, + }); + renderUsuarios(); + await waitFor(() => screen.getAllByText('Editar')); + fireEvent.click(screen.getAllByText('Editar')[0]); + fireEvent.click(screen.getByText('Salvar')); + await waitFor(() => expect(mockUsuariosService.atualizar).toHaveBeenCalled()); + }); + + it('exibe erro genérico ao falhar atualização', async () => { + mockUsuariosService.atualizar.mockRejectedValue(new Error('fail')); + renderUsuarios(); + await waitFor(() => screen.getAllByText('Editar')); + fireEvent.click(screen.getAllByText('Editar')[0]); + fireEvent.click(screen.getByText('Salvar')); + await waitFor(() => expect(mockUsuariosService.atualizar).toHaveBeenCalled()); + }); + + it('exibe erro ao falhar alteração de status', async () => { + mockUsuariosService.alterarAtivo.mockRejectedValue(new Error('fail')); + renderUsuarios(); + await waitFor(() => screen.getAllByText('Desativar')); + fireEvent.click(screen.getAllByText('Desativar')[0]); + await waitFor(() => expect(mockUsuariosService.alterarAtivo).toHaveBeenCalled()); + }); +}); + +describe('Usuarios — fechar modal pelo botão ×', () => { + it('fecha modal de criação pelo botão × do cabeçalho', async () => { + renderUsuarios(); + await waitFor(() => screen.getByText('Novo usuário')); + fireEvent.click(screen.getByText('Novo usuário')); + + const overlay = document.querySelector('.fixed.inset-0'); + const closeBtn = overlay?.querySelector('button'); + expect(closeBtn).toBeTruthy(); + fireEvent.click(closeBtn!); + expect(screen.queryByText('Criar usuário')).not.toBeInTheDocument(); + }); + + it('fecha modal de edição pelo botão × do cabeçalho', async () => { + renderUsuarios(); + await waitFor(() => screen.getAllByText('Editar')); + fireEvent.click(screen.getAllByText('Editar')[0]); + + const overlay = document.querySelector('.fixed.inset-0'); + const closeBtn = overlay?.querySelector('button'); + expect(closeBtn).toBeTruthy(); + fireEvent.click(closeBtn!); + expect(screen.queryByText('Salvar')).not.toBeInTheDocument(); + }); +}); diff --git a/frontend/src/test/pages/Verificar.test.tsx b/frontend/src/test/pages/Verificar.test.tsx index a1bf34f..a2f68ac 100644 --- a/frontend/src/test/pages/Verificar.test.tsx +++ b/frontend/src/test/pages/Verificar.test.tsx @@ -1,19 +1,23 @@ -import { describe, it, expect, vi } from 'vitest'; -import { screen } from '@testing-library/react'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { screen, waitFor } from '@testing-library/react'; import { render } from '@testing-library/react'; import { MemoryRouter, Routes, Route } from 'react-router-dom'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { Verificar } from '../../pages/Verificar'; +import axios from 'axios'; vi.mock('axios', () => ({ default: { - get: vi.fn().mockRejectedValue({ isAxiosError: true, response: { status: 404 } }), - isAxiosError: vi.fn().mockReturnValue(true), + get: vi.fn(), + isAxiosError: vi.fn(), + create: vi.fn(), }, })); +const mockAxios = vi.mocked(axios); + function renderVerificar(hash?: string) { - const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + const qc = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: 0 } } }); return render( @@ -26,14 +30,93 @@ function renderVerificar(hash?: string) { ); } -describe('Verificar', () => { +beforeEach(() => { + vi.clearAllMocks(); + mockAxios.isAxiosError.mockReturnValue(false); +}); + +describe('Verificar — estados da página', () => { it('renderiza o cabeçalho da página', () => { + mockAxios.get.mockReturnValue(new Promise(() => {})); renderVerificar('abc123'); expect(screen.getByText(/Valida/i)).toBeInTheDocument(); }); it('exibe texto de verificação de certificado', () => { + mockAxios.get.mockReturnValue(new Promise(() => {})); renderVerificar('abc123'); expect(screen.getByText(/Verificação de Certificado/i)).toBeInTheDocument(); }); + + it('exibe estado de carregamento enquanto verifica', () => { + mockAxios.get.mockReturnValue(new Promise(() => {})); + renderVerificar('abc123'); + expect(screen.getByText('Verificando certificado...')).toBeInTheDocument(); + }); + + it('exibe certificado inválido quando hash não encontrado (404)', async () => { + const err = { response: { status: 404 } }; + mockAxios.get.mockRejectedValue(err); + mockAxios.isAxiosError.mockReturnValue(true); + renderVerificar('hash-invalido'); + await waitFor(() => + expect(screen.getByText('Certificado Inválido')).toBeInTheDocument(), + ); + }); + + it('exibe erro genérico para falhas que não sejam 404', async () => { + const err = { response: { status: 500 } }; + mockAxios.get.mockRejectedValue(err); + mockAxios.isAxiosError.mockReturnValue(true); + renderVerificar('hash-erro'); + await waitFor(() => + expect(screen.getByText(/Erro ao verificar/i)).toBeInTheDocument(), + ); + }); + + it('exibe erro genérico quando não é erro do axios', async () => { + mockAxios.get.mockRejectedValue(new Error('Network error')); + mockAxios.isAxiosError.mockReturnValue(false); + renderVerificar('hash-rede'); + await waitFor(() => + expect(screen.getByText(/Erro ao verificar/i)).toBeInTheDocument(), + ); + }); + + it('exibe certificado válido com dados do estudante e documento', async () => { + const dados = { + valido: true, + certificado: { id: 'cert-1', hash: 'abc123hash', emitido_em: '2024-01-15T10:00:00Z' }, + estudante: { nome: 'João Silva', email: 'joao@ufsc.br' }, + documento: { titulo: 'Certificado de Curso', tipo: 'certificado_curso', carga_horaria: 40, status: 'aprovado' }, + }; + mockAxios.get.mockResolvedValue({ data: dados }); + renderVerificar('abc123hash'); + await waitFor(() => + expect(screen.getByText('Certificado Válido')).toBeInTheDocument(), + ); + expect(screen.getByText('João Silva')).toBeInTheDocument(); + expect(screen.getByText('joao@ufsc.br')).toBeInTheDocument(); + expect(screen.getByText('Certificado de Curso')).toBeInTheDocument(); + expect(screen.getByText('abc123hash')).toBeInTheDocument(); + }); + + it('exibe tipo de documento legível quando disponível no mapa', async () => { + const dados = { + valido: true, + certificado: { id: 'cert-2', hash: 'xyz456', emitido_em: '2024-03-01T00:00:00Z' }, + estudante: { nome: 'Maria', email: 'maria@ufsc.br' }, + documento: { titulo: 'TCC Final', tipo: 'tcc', carga_horaria: 60, status: 'aprovado' }, + }; + mockAxios.get.mockResolvedValue({ data: dados }); + renderVerificar('xyz456'); + await waitFor(() => + expect(screen.getByText('Trabalho de Conclusão de Curso · 60h')).toBeInTheDocument(), + ); + }); + + it('não executa a query quando hash está ausente', () => { + renderVerificar(); + expect(mockAxios.get).not.toHaveBeenCalled(); + }); }); diff --git a/frontend/src/test/services/api.test.ts b/frontend/src/test/services/api.test.ts new file mode 100644 index 0000000..558c26d --- /dev/null +++ b/frontend/src/test/services/api.test.ts @@ -0,0 +1,79 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; + +// Testa os interceptors do axios diretamente sem mockar o módulo api +// O queryClient precisa ser mockado para evitar imports circulares +vi.mock('../../lib/queryClient', () => ({ + queryClient: { clear: vi.fn() }, +})); + +describe('api interceptors', () => { + beforeEach(() => { + localStorage.clear(); + vi.stubGlobal('location', { replace: vi.fn() }); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it('adiciona header Authorization quando token existe no localStorage', async () => { + localStorage.setItem('token', 'meu-token-123'); + const { default: api } = await import('../../services/api'); + + // Pega o handler do request interceptor e executa diretamente + const handlers = (api.interceptors.request as unknown as { + handlers: Array<{ fulfilled: (c: unknown) => unknown }>; + }).handlers; + const handler = handlers[handlers.length - 1]?.fulfilled; + expect(handler).toBeDefined(); + + const fakeConfig = { headers: {} as Record }; + const result = handler(fakeConfig) as typeof fakeConfig; + expect(result.headers.Authorization).toBe('Bearer meu-token-123'); + }); + + it('não adiciona header Authorization quando token não existe', async () => { + const { default: api } = await import('../../services/api'); + + const handlers = (api.interceptors.request as unknown as { + handlers: Array<{ fulfilled: (c: unknown) => unknown }>; + }).handlers; + const handler = handlers[handlers.length - 1]?.fulfilled; + + const fakeConfig = { headers: {} as Record }; + const result = handler(fakeConfig) as typeof fakeConfig; + expect(result.headers.Authorization).toBeUndefined(); + }); + + it('limpa token e redireciona ao receber 401', async () => { + localStorage.setItem('token', 'tok'); + localStorage.setItem('usuario', '{}'); + const { default: api } = await import('../../services/api'); + const { queryClient } = await import('../../lib/queryClient'); + + const responseHandlers = (api.interceptors.response as unknown as { + handlers: Array<{ rejected: (e: unknown) => unknown }>; + }).handlers; + const errorHandler = responseHandlers[responseHandlers.length - 1]?.rejected; + expect(errorHandler).toBeDefined(); + + await expect(errorHandler({ response: { status: 401 } })).rejects.toBeTruthy(); + expect(queryClient.clear).toHaveBeenCalled(); + expect(localStorage.getItem('token')).toBeNull(); + expect(localStorage.getItem('usuario')).toBeNull(); + }); + + it('rejeita o erro sem redirecionar para status diferente de 401', async () => { + const { default: api } = await import('../../services/api'); + + const responseHandlers = (api.interceptors.response as unknown as { + handlers: Array<{ rejected: (e: unknown) => unknown }>; + }).handlers; + const errorHandler = responseHandlers[responseHandlers.length - 1]?.rejected; + + await expect(errorHandler({ response: { status: 500 } })).rejects.toMatchObject({ + response: { status: 500 }, + }); + expect(globalThis.location.replace).not.toHaveBeenCalled(); + }); +}); From ea8bb427fd9a238c685c62d61192f1363519bec0 Mon Sep 17 00:00:00 2001 From: icrcode Date: Tue, 9 Jun 2026 13:33:37 -0300 Subject: [PATCH 07/19] =?UTF-8?q?test:=20adicionar=20testes=20para=20a=20p?= =?UTF-8?q?=C3=A1gina=20Dashboard,=20incluindo=20renderiza=C3=A7=C3=A3o=20?= =?UTF-8?q?e=20exibi=C3=A7=C3=A3o=20de=20documentos=20para=20usu=C3=A1rios?= =?UTF-8?q?=20estudantes,=20coordenadores=20e=20administradores?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/test/pages/Dashboard.test.tsx | 139 +++++++++++++++++++-- 1 file changed, 131 insertions(+), 8 deletions(-) diff --git a/frontend/src/test/pages/Dashboard.test.tsx b/frontend/src/test/pages/Dashboard.test.tsx index 5301778..92554d6 100644 --- a/frontend/src/test/pages/Dashboard.test.tsx +++ b/frontend/src/test/pages/Dashboard.test.tsx @@ -1,12 +1,54 @@ -import { describe, it, expect, vi } from 'vitest'; -import { renderWithProviders, USUARIO_ESTUDANTE } from '../helpers/renderWithProviders'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { screen, waitFor } from '@testing-library/react'; +import { renderWithProviders, USUARIO_ESTUDANTE, USUARIO_COORD, USUARIO_ADMIN } from '../helpers/renderWithProviders'; import { Dashboard } from '../../pages/Dashboard'; +const { mockDocumentosService } = vi.hoisted(() => ({ + mockDocumentosService: { listar: vi.fn() }, +})); + vi.mock('../../services/api', () => ({ - default: { get: vi.fn().mockResolvedValue({ data: { total: 0, dados: [] } }) }, + default: { + get: vi.fn(), + interceptors: { request: { use: vi.fn() }, response: { use: vi.fn() } }, + }, })); -describe('Dashboard', () => { +vi.mock('../../services/documentos', () => ({ + documentosService: mockDocumentosService, +})); + +const RESPOSTA_VAZIA = { total: 0, dados: [], pagina: 1, total_paginas: 1 }; + +const DOCS_MOCK = { + total: 2, + pagina: 1, + total_paginas: 1, + dados: [ + { + id: 'd-1', titulo: 'Certificado de Python', tipo: 'certificado_curso', + carga_horaria: 40, status: 'pendente', + estudante_id: 'u-1', estudante_nome: 'João', + criado_em: '2024-01-15T10:00:00Z', atualizado_em: '2024-01-15T10:00:00Z', + arquivo_url: null, observacoes: null, curso_id: null, instituicao_id: null, + }, + { + id: 'd-2', titulo: 'Artigo de Pesquisa', tipo: 'artigo_publicado', + carga_horaria: 20, status: 'aprovado', + estudante_id: 'u-1', estudante_nome: 'João', + criado_em: '2024-02-10T08:00:00Z', atualizado_em: '2024-02-10T08:00:00Z', + arquivo_url: null, observacoes: null, curso_id: null, instituicao_id: null, + }, + ], +}; + +beforeEach(() => { + vi.clearAllMocks(); + localStorage.clear(); + mockDocumentosService.listar.mockResolvedValue(RESPOSTA_VAZIA); +}); + +describe('Dashboard — estudante', () => { it('renderiza a página sem erros', () => { const { container } = renderWithProviders(, { token: 'tok-123', usuario: USUARIO_ESTUDANTE, @@ -14,10 +56,91 @@ describe('Dashboard', () => { expect(container).toBeTruthy(); }); - it('exibe algum conteúdo do dashboard', () => { - renderWithProviders(, { - token: 'tok-123', usuario: USUARIO_ESTUDANTE, + it('exibe o primeiro nome do usuário', async () => { + renderWithProviders(, { token: 'tok', usuario: USUARIO_ESTUDANTE }); + await waitFor(() => expect(screen.getByText(/João/)).toBeInTheDocument()); + }); + + it('exibe texto para estudante', async () => { + renderWithProviders(, { token: 'tok', usuario: USUARIO_ESTUDANTE }); + await waitFor(() => + expect(screen.getByText(/Acompanhe o status dos seus documentos/)).toBeInTheDocument(), + ); + }); + + it('exibe link Submeter Documento para estudante', async () => { + renderWithProviders(, { token: 'tok', usuario: USUARIO_ESTUDANTE }); + await waitFor(() => + expect(screen.getByText('Submeter Documento')).toBeInTheDocument(), + ); + }); + + it('exibe link Meus Certificados para estudante', async () => { + renderWithProviders(, { token: 'tok', usuario: USUARIO_ESTUDANTE }); + await waitFor(() => + expect(screen.getByText('Meus Certificados')).toBeInTheDocument(), + ); + }); + + it('exibe mensagem quando não há documentos recentes', async () => { + renderWithProviders(, { token: 'tok', usuario: USUARIO_ESTUDANTE }); + await waitFor(() => + expect(screen.getByText('Nenhum documento encontrado.')).toBeInTheDocument(), + ); + }); + + it('exibe documentos recentes quando há dados', async () => { + mockDocumentosService.listar.mockResolvedValue(DOCS_MOCK); + renderWithProviders(, { token: 'tok', usuario: USUARIO_ESTUDANTE }); + await waitFor(() => { + expect(screen.getByText('Certificado de Python')).toBeInTheDocument(); + expect(screen.getByText('Artigo de Pesquisa')).toBeInTheDocument(); + }); + }); + + it('exibe link Ver para cada documento recente', async () => { + mockDocumentosService.listar.mockResolvedValue(DOCS_MOCK); + renderWithProviders(, { token: 'tok', usuario: USUARIO_ESTUDANTE }); + await waitFor(() => { + const links = screen.getAllByText('Ver'); + expect(links.length).toBeGreaterThanOrEqual(2); + }); + }); +}); + +describe('Dashboard — coordenador/admin', () => { + it('exibe texto de coordenador/admin', async () => { + renderWithProviders(, { token: 'tok', usuario: USUARIO_COORD }); + await waitFor(() => + expect(screen.getByText(/Visão geral dos documentos do seu curso/)).toBeInTheDocument(), + ); + }); + + it('exibe link Fila de Análise para coordenador', async () => { + renderWithProviders(, { token: 'tok', usuario: USUARIO_COORD }); + await waitFor(() => + expect(screen.getByText('Revisar documentos pendentes')).toBeInTheDocument(), + ); + }); + + it('não exibe link Submeter Documento para coordenador', async () => { + renderWithProviders(, { token: 'tok', usuario: USUARIO_COORD }); + await waitFor(() => screen.getByText(/Visão geral/)); + expect(screen.queryByText('Submeter Documento')).not.toBeInTheDocument(); + }); + + it('exibe texto correto para admin', async () => { + renderWithProviders(, { token: 'tok', usuario: USUARIO_ADMIN }); + await waitFor(() => + expect(screen.getByText(/Visão geral dos documentos do seu curso/)).toBeInTheDocument(), + ); + }); + + it('exibe cards de status para coordenador', async () => { + renderWithProviders(, { token: 'tok', usuario: USUARIO_COORD }); + await waitFor(() => { + expect(screen.getByText('Pendentes')).toBeInTheDocument(); + expect(screen.getByText('Em Revisão')).toBeInTheDocument(); }); - expect(document.body).toBeTruthy(); }); }); From cf913f8bbabef5f4a79f62d353727193e1462721 Mon Sep 17 00:00:00 2001 From: icrcode Date: Tue, 9 Jun 2026 13:33:44 -0300 Subject: [PATCH 08/19] =?UTF-8?q?test:=20corrigir=20texto=20do=20link=20na?= =?UTF-8?q?=20p=C3=A1gina=20Dashboard=20para=20coordenador?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/test/pages/Dashboard.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/test/pages/Dashboard.test.tsx b/frontend/src/test/pages/Dashboard.test.tsx index 92554d6..66e482a 100644 --- a/frontend/src/test/pages/Dashboard.test.tsx +++ b/frontend/src/test/pages/Dashboard.test.tsx @@ -119,7 +119,7 @@ describe('Dashboard — coordenador/admin', () => { it('exibe link Fila de Análise para coordenador', async () => { renderWithProviders(, { token: 'tok', usuario: USUARIO_COORD }); await waitFor(() => - expect(screen.getByText('Revisar documentos pendentes')).toBeInTheDocument(), + expect(screen.getByText('Fila de Análise')).toBeInTheDocument(), ); }); From 88370ff542784cfda11b98627a6f6573ac522951 Mon Sep 17 00:00:00 2001 From: icrcode Date: Tue, 9 Jun 2026 13:35:41 -0300 Subject: [PATCH 09/19] =?UTF-8?q?test:=20adicionar=20testes=20para=20exibi?= =?UTF-8?q?=C3=A7=C3=A3o=20de=20erros=20na=20altera=C3=A7=C3=A3o=20de=20se?= =?UTF-8?q?nha=20e=20formata=C3=A7=C3=A3o=20de=20CPF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/test/pages/Perfil.test.tsx | 59 +++++++++++++++++++++++ frontend/src/test/pages/Usuarios.test.tsx | 38 +++++++++++++++ 2 files changed, 97 insertions(+) diff --git a/frontend/src/test/pages/Perfil.test.tsx b/frontend/src/test/pages/Perfil.test.tsx index bad01d4..8c2585b 100644 --- a/frontend/src/test/pages/Perfil.test.tsx +++ b/frontend/src/test/pages/Perfil.test.tsx @@ -245,4 +245,63 @@ describe('Perfil — modo EDITAR SENHA', () => { nova_senha: 'novaSenha123', })); }); + + it('exibe erro da API quando mutSenha falha com erro personalizado', async () => { + mockUsuariosService.atualizarPerfil.mockRejectedValue({ + response: { data: { erro: 'Senha atual incorreta' } }, + }); + renderPerfil(); + await waitFor(() => screen.getByText('Alterar senha')); + fireEvent.click(screen.getByText('Alterar senha')); + fireEvent.change(screen.getByPlaceholderText('Sua senha atual'), { target: { value: 'errada' } }); + fireEvent.change(screen.getByPlaceholderText('Mínimo 6 caracteres'), { target: { value: 'novaSenha123' } }); + fireEvent.change(screen.getByPlaceholderText('Repita a nova senha'), { target: { value: 'novaSenha123' } }); + fireEvent.click(screen.getByText('Salvar nova senha')); + await waitFor(() => + expect(screen.getByText('Senha atual incorreta')).toBeInTheDocument(), + ); + }); + + it('exibe mensagem genérica quando mutSenha falha sem detalhe da API', async () => { + mockUsuariosService.atualizarPerfil.mockRejectedValue(new Error('network error')); + renderPerfil(); + await waitFor(() => screen.getByText('Alterar senha')); + fireEvent.click(screen.getByText('Alterar senha')); + fireEvent.change(screen.getByPlaceholderText('Sua senha atual'), { target: { value: 'senhaAtual' } }); + fireEvent.change(screen.getByPlaceholderText('Mínimo 6 caracteres'), { target: { value: 'novaSenha123' } }); + fireEvent.change(screen.getByPlaceholderText('Repita a nova senha'), { target: { value: 'novaSenha123' } }); + fireEvent.click(screen.getByText('Salvar nova senha')); + await waitFor(() => + expect(screen.getByText('Erro ao alterar senha')).toBeInTheDocument(), + ); + }); +}); + +describe('Perfil — campos opcionais', () => { + it('aceita digitação no campo de matrícula no modo editar (estudante)', async () => { + renderPerfil(); + await waitFor(() => screen.getByText('Editar perfil')); + fireEvent.click(screen.getByText('Editar perfil')); + const inputMatricula = screen.getByPlaceholderText('Número de matrícula'); + fireEvent.change(inputMatricula, { target: { value: '2022999' } }); + expect(inputMatricula).toHaveValue('2022999'); + }); + + it('aceita digitação no campo de CPF no modo editar (coordenador)', async () => { + renderPerfil(USUARIO_COORD); + await waitFor(() => screen.getByText('Editar perfil')); + fireEvent.click(screen.getByText('Editar perfil')); + const inputCpf = screen.getByPlaceholderText('000.000.000-00'); + fireEvent.change(inputCpf, { target: { value: '12345678901' } }); + expect(inputCpf).toHaveValue('123.456.789-01'); + }); + + it('aceita digitação no campo de endereço no modo editar (coordenador)', async () => { + renderPerfil(USUARIO_COORD); + await waitFor(() => screen.getByText('Editar perfil')); + fireEvent.click(screen.getByText('Editar perfil')); + const inputEndereco = screen.getByPlaceholderText(/Rua, número/i); + fireEvent.change(inputEndereco, { target: { value: 'Rua das Flores, 42' } }); + expect(inputEndereco).toHaveValue('Rua das Flores, 42'); + }); }); diff --git a/frontend/src/test/pages/Usuarios.test.tsx b/frontend/src/test/pages/Usuarios.test.tsx index f47f729..9bed5e3 100644 --- a/frontend/src/test/pages/Usuarios.test.tsx +++ b/frontend/src/test/pages/Usuarios.test.tsx @@ -437,6 +437,44 @@ describe('Usuarios — erros de mutation', () => { }); }); +describe('Usuarios — formatação de CPF', () => { + it('formata CPF com 4 dígitos (d.length <= 6)', async () => { + renderUsuarios(); + await waitFor(() => screen.getAllByText('Editar')); + fireEvent.click(screen.getAllByText('Editar')[0]); + const cpf = screen.getByDisplayValue('000.000.000-00'); + fireEvent.change(cpf, { target: { value: '1234' } }); + expect(cpf).toHaveValue('123.4'); + }); + + it('formata CPF com 7 dígitos (d.length <= 9)', async () => { + renderUsuarios(); + await waitFor(() => screen.getAllByText('Editar')); + fireEvent.click(screen.getAllByText('Editar')[0]); + const cpf = screen.getByDisplayValue('000.000.000-00'); + fireEvent.change(cpf, { target: { value: '1234567' } }); + expect(cpf).toHaveValue('123.456.7'); + }); + + it('formata CPF com 3 dígitos ou menos (retorna direto)', async () => { + renderUsuarios(); + await waitFor(() => screen.getAllByText('Editar')); + fireEvent.click(screen.getAllByText('Editar')[0]); + const cpf = screen.getByDisplayValue('000.000.000-00'); + fireEvent.change(cpf, { target: { value: '12' } }); + expect(cpf).toHaveValue('12'); + }); + + it('formata CPF completo com 11 dígitos', async () => { + renderUsuarios(); + await waitFor(() => screen.getAllByText('Editar')); + fireEvent.click(screen.getAllByText('Editar')[0]); + const cpf = screen.getByDisplayValue('000.000.000-00'); + fireEvent.change(cpf, { target: { value: '12345678901' } }); + expect(cpf).toHaveValue('123.456.789-01'); + }); +}); + describe('Usuarios — fechar modal pelo botão ×', () => { it('fecha modal de criação pelo botão × do cabeçalho', async () => { renderUsuarios(); From 49ab374edc31470ce7ca88e7a628db92b5061bfd Mon Sep 17 00:00:00 2001 From: icrcode Date: Tue, 9 Jun 2026 13:42:10 -0300 Subject: [PATCH 10/19] =?UTF-8?q?test:=20adicionar=20testes=20para=20exibi?= =?UTF-8?q?=C3=A7=C3=A3o=20de=20erros=20ao=20atualizar=20perfil=20e=20edi?= =?UTF-8?q?=C3=A7=C3=A3o=20de=20campos=20opcionais=20na=20p=C3=A1gina=20Us?= =?UTF-8?q?u=C3=A1rios?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/test/pages/Perfil.test.tsx | 24 ++++++++++++++++++ frontend/src/test/pages/Usuarios.test.tsx | 31 +++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/frontend/src/test/pages/Perfil.test.tsx b/frontend/src/test/pages/Perfil.test.tsx index 8c2585b..f901031 100644 --- a/frontend/src/test/pages/Perfil.test.tsx +++ b/frontend/src/test/pages/Perfil.test.tsx @@ -146,6 +146,30 @@ describe('Perfil — modo EDITAR DADOS', () => { }); }); + it('exibe erro da API quando mutDados falha com erro personalizado', async () => { + mockUsuariosService.atualizarPerfil.mockRejectedValue({ + response: { data: { erro: 'E-mail já em uso' } }, + }); + renderPerfil(); + await waitFor(() => screen.getByText('Editar perfil')); + fireEvent.click(screen.getByText('Editar perfil')); + fireEvent.click(screen.getByText('Salvar alterações')); + await waitFor(() => + expect(mockUsuariosService.atualizarPerfil).toHaveBeenCalled(), + ); + }); + + it('exibe mensagem genérica quando mutDados falha sem detalhe da API', async () => { + mockUsuariosService.atualizarPerfil.mockRejectedValue(new Error('Network error')); + renderPerfil(); + await waitFor(() => screen.getByText('Editar perfil')); + fireEvent.click(screen.getByText('Editar perfil')); + fireEvent.click(screen.getByText('Salvar alterações')); + await waitFor(() => + expect(mockUsuariosService.atualizarPerfil).toHaveBeenCalled(), + ); + }); + it('salva dados com sucesso e volta ao modo ver', async () => { mockUsuariosService.atualizarPerfil.mockResolvedValue({ ...USUARIO_ESTUDANTE, nome: 'Nome Atualizado' }); renderPerfil(); diff --git a/frontend/src/test/pages/Usuarios.test.tsx b/frontend/src/test/pages/Usuarios.test.tsx index 9bed5e3..33b47d8 100644 --- a/frontend/src/test/pages/Usuarios.test.tsx +++ b/frontend/src/test/pages/Usuarios.test.tsx @@ -437,6 +437,37 @@ describe('Usuarios — erros de mutation', () => { }); }); +describe('Usuarios — editar campos opcionais', () => { + it('permite alterar curso no select do modal de edição', async () => { + renderUsuarios(); + await waitFor(() => screen.getAllByText('Editar')); + fireEvent.click(screen.getAllByText('Editar')[0]); + + // O select de curso tem a opção "Sem curso vinculado" + cursos + const courseSelect = screen.getByDisplayValue(/\[CC-001\]/); + fireEvent.change(courseSelect, { target: { value: '' } }); + expect(screen.queryByDisplayValue(/\[CC-001\]/)).not.toBeInTheDocument(); + }); + + it('permite alterar matrícula no modal de edição (estudante)', async () => { + renderUsuarios(); + await waitFor(() => screen.getAllByText('Editar')); + fireEvent.click(screen.getAllByText('Editar')[0]); + const matricula = screen.getByDisplayValue('2021001'); + fireEvent.change(matricula, { target: { value: '2022999' } }); + expect(screen.getByDisplayValue('2022999')).toBeInTheDocument(); + }); + + it('permite alterar endereço no modal de edição (estudante)', async () => { + renderUsuarios(); + await waitFor(() => screen.getAllByText('Editar')); + fireEvent.click(screen.getAllByText('Editar')[0]); + const endereco = screen.getByDisplayValue('Rua A, 123'); + fireEvent.change(endereco, { target: { value: 'Rua B, 456' } }); + expect(screen.getByDisplayValue('Rua B, 456')).toBeInTheDocument(); + }); +}); + describe('Usuarios — formatação de CPF', () => { it('formata CPF com 4 dígitos (d.length <= 6)', async () => { renderUsuarios(); From ea2b18a5781f51eb80f3abffe5c0279ad567b4cc Mon Sep 17 00:00:00 2001 From: icrcode Date: Tue, 9 Jun 2026 13:42:23 -0300 Subject: [PATCH 11/19] =?UTF-8?q?test:=20adicionar=20testes=20para=20rejei?= =?UTF-8?q?=C3=A7=C3=A3o=20de=20erros=20sem=20resposta=20e=20filtros=20adi?= =?UTF-8?q?cionais=20na=20listagem=20de=20documentos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/test/services/api.test.ts | 15 +++++++++++ frontend/src/test/services/documentos.test.ts | 26 ++++++++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/frontend/src/test/services/api.test.ts b/frontend/src/test/services/api.test.ts index 558c26d..307d849 100644 --- a/frontend/src/test/services/api.test.ts +++ b/frontend/src/test/services/api.test.ts @@ -76,4 +76,19 @@ describe('api interceptors', () => { }); expect(globalThis.location.replace).not.toHaveBeenCalled(); }); + + it('rejeita sem redirecionar quando erro não tem response (ex: network error)', async () => { + const { default: api } = await import('../../services/api'); + + const responseHandlers = (api.interceptors.response as unknown as { + handlers: Array<{ rejected: (e: unknown) => unknown }>; + }).handlers; + const errorHandler = responseHandlers[responseHandlers.length - 1]?.rejected; + + // Erro sem response (timeout, CORS, etc.) + await expect(errorHandler({ message: 'Network Error' })).rejects.toMatchObject({ + message: 'Network Error', + }); + expect(globalThis.location.replace).not.toHaveBeenCalled(); + }); }); diff --git a/frontend/src/test/services/documentos.test.ts b/frontend/src/test/services/documentos.test.ts index a0a3942..f17e054 100644 --- a/frontend/src/test/services/documentos.test.ts +++ b/frontend/src/test/services/documentos.test.ts @@ -18,13 +18,37 @@ describe('documentosService.listar', () => { expect(resultado.total).toBe(1); }); - it('passa filtros como query params', async () => { + it('passa filtros como query params (status e page)', async () => { mockApi.get.mockResolvedValueOnce({ data: { dados: [], total: 0 } }); await documentosService.listar({ status: 'aprovado', page: 2 }); expect(mockApi.get).toHaveBeenCalledWith('/api/documentos', { params: { status: 'aprovado', page: 2 }, }); }); + + it('passa filtro de tipo', async () => { + mockApi.get.mockResolvedValueOnce({ data: { dados: [], total: 0 } }); + await documentosService.listar({ tipo: 'certificado_curso' }); + expect(mockApi.get).toHaveBeenCalledWith('/api/documentos', { + params: { tipo: 'certificado_curso' }, + }); + }); + + it('passa filtro de estudante_id', async () => { + mockApi.get.mockResolvedValueOnce({ data: { dados: [], total: 0 } }); + await documentosService.listar({ estudante_id: 'u-1' }); + expect(mockApi.get).toHaveBeenCalledWith('/api/documentos', { + params: { estudante_id: 'u-1' }, + }); + }); + + it('passa filtro de limite', async () => { + mockApi.get.mockResolvedValueOnce({ data: { dados: [], total: 0 } }); + await documentosService.listar({ limite: 10 }); + expect(mockApi.get).toHaveBeenCalledWith('/api/documentos', { + params: { limite: 10 }, + }); + }); }); describe('documentosService.buscarPorId', () => { From f8616f3532660e663d64f3b5a1229e28a9117b35 Mon Sep 17 00:00:00 2001 From: icrcode Date: Tue, 9 Jun 2026 13:52:05 -0300 Subject: [PATCH 12/19] =?UTF-8?q?test:=20adicionar=20mock=20de=20usu=C3=A1?= =?UTF-8?q?rio=20com=20perfil=20desconhecido=20nos=20testes=20da=20p=C3=A1?= =?UTF-8?q?gina=20Usu=C3=A1rios?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/test/pages/Usuarios.test.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frontend/src/test/pages/Usuarios.test.tsx b/frontend/src/test/pages/Usuarios.test.tsx index 33b47d8..32d1c38 100644 --- a/frontend/src/test/pages/Usuarios.test.tsx +++ b/frontend/src/test/pages/Usuarios.test.tsx @@ -44,6 +44,11 @@ const USUARIOS_MOCK = [ matricula: null, cpf: null, endereco: null, curso_id: null, instituicao_id: null, instituicao_nome: null, ativo: true, }, + { + id: 'u-4', nome: 'Perfil Desconhecido', email: 'x@ufsc.br', perfil: 'desconhecido' as any, + matricula: null, cpf: null, endereco: null, + curso_id: null, instituicao_id: null, instituicao_nome: null, ativo: true, + }, ]; const CURSOS_MOCK = [ From 67bae9ada6a28433c97c01a92ac9aaaabbaacc76 Mon Sep 17 00:00:00 2001 From: icrcode Date: Tue, 9 Jun 2026 13:52:17 -0300 Subject: [PATCH 13/19] =?UTF-8?q?test:=20adicionar=20testes=20para=20atual?= =?UTF-8?q?iza=C3=A7=C3=A3o=20do=20meta=20color-scheme=20e=20fallback=20de?= =?UTF-8?q?=20tipo=20em=20MeusCertificados?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/test/contexts/ThemeContext.test.tsx | 13 ++++++++++ .../src/test/pages/MeusCertificados.test.tsx | 25 +++++++++++++++++++ frontend/src/test/pages/Usuarios.test.tsx | 16 ++++++++++++ 3 files changed, 54 insertions(+) diff --git a/frontend/src/test/contexts/ThemeContext.test.tsx b/frontend/src/test/contexts/ThemeContext.test.tsx index 78c9c7e..e54e2ad 100644 --- a/frontend/src/test/contexts/ThemeContext.test.tsx +++ b/frontend/src/test/contexts/ThemeContext.test.tsx @@ -50,4 +50,17 @@ describe('ThemeContext', () => { await act(async () => { screen.getByText('alternar').click(); }); expect(localStorage.getItem('tema')).toBe('light'); }); + + it('atualiza o meta color-scheme quando o elemento existe', async () => { + const meta = document.createElement('meta'); + meta.setAttribute('name', 'color-scheme'); + document.head.appendChild(meta); + try { + render(); + await act(async () => { screen.getByText('alternar').click(); }); + expect(meta.content).toBe('light'); + } finally { + document.head.removeChild(meta); + } + }); }); diff --git a/frontend/src/test/pages/MeusCertificados.test.tsx b/frontend/src/test/pages/MeusCertificados.test.tsx index 2204511..e70eb8d 100644 --- a/frontend/src/test/pages/MeusCertificados.test.tsx +++ b/frontend/src/test/pages/MeusCertificados.test.tsx @@ -141,4 +141,29 @@ describe('MeusCertificados — com certificados', () => { expect(screen.getByText('Baixar PDF')).toBeInTheDocument(); }); }); + + it('usa tipo bruto como fallback quando tipo não está em TIPO_LEGIVEL', async () => { + const certTipoDesconhecido = { + ...CERT_MOCK, + documento: { ...CERT_MOCK.documento, tipo: 'certificado_curso' }, + }; + mockCertificadosService.listar.mockResolvedValue([certTipoDesconhecido]); + renderWithProviders(, { token: 'tok', usuario: USUARIO_ESTUDANTE }); + await waitFor(() => { + expect(screen.getByText(/certificado_curso/)).toBeInTheDocument(); + }); + }); + + it('aplica delay-300 para o 5º certificado ou mais', async () => { + const certs = Array.from({ length: 5 }, (_, i) => ({ + ...CERT_MOCK, + id: `cert-${i + 1}`, + hash: `hash${i}`, + documento: { ...CERT_MOCK.documento, titulo: `Curso ${i}` }, + })); + mockCertificadosService.listar.mockResolvedValue(certs); + const { container } = renderWithProviders(, { token: 'tok', usuario: USUARIO_ESTUDANTE }); + await waitFor(() => screen.getByText('Curso 4')); + expect(container.querySelector('.delay-300')).toBeInTheDocument(); + }); }); diff --git a/frontend/src/test/pages/Usuarios.test.tsx b/frontend/src/test/pages/Usuarios.test.tsx index 32d1c38..dc814fa 100644 --- a/frontend/src/test/pages/Usuarios.test.tsx +++ b/frontend/src/test/pages/Usuarios.test.tsx @@ -442,6 +442,22 @@ describe('Usuarios — erros de mutation', () => { }); }); +describe('Usuarios — editar admin (sem cpf/endereço)', () => { + it('salva edição de admin sem cpf, endereço e matrícula', async () => { + mockUsuariosService.atualizar.mockResolvedValue({ id: 'u-3' }); + renderUsuarios(); + await waitFor(() => screen.getAllByText('Editar')); + fireEvent.click(screen.getAllByText('Editar')[2]); // Admin User (índice 2) + fireEvent.click(screen.getByText('Salvar')); + await waitFor(() => + expect(mockUsuariosService.atualizar).toHaveBeenCalledWith( + 'u-3', + expect.objectContaining({ perfil: 'admin', cpf: null, endereco: null, curso_id: null }), + ), + ); + }); +}); + describe('Usuarios — editar campos opcionais', () => { it('permite alterar curso no select do modal de edição', async () => { renderUsuarios(); From 3acb2de71af988e01b08978285a07afac84f0809 Mon Sep 17 00:00:00 2001 From: icrcode Date: Tue, 9 Jun 2026 13:52:35 -0300 Subject: [PATCH 14/19] =?UTF-8?q?test:=20adicionar=20teste=20para=20render?= =?UTF-8?q?iza=C3=A7=C3=A3o=20de=20Outlet=20em=20PrivateRoute=20sem=20chil?= =?UTF-8?q?dren?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/test/components/PrivateRoute.test.tsx | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/frontend/src/test/components/PrivateRoute.test.tsx b/frontend/src/test/components/PrivateRoute.test.tsx index c0acd57..3a9a75d 100644 --- a/frontend/src/test/components/PrivateRoute.test.tsx +++ b/frontend/src/test/components/PrivateRoute.test.tsx @@ -62,4 +62,21 @@ describe('PrivateRoute', () => { renderComAuth('tok-123', USUARIO_MOCK, ['admin']); expect(screen.getByText('Conteúdo Protegido')).toBeInTheDocument(); }); + + it('renderiza Outlet quando usado como elemento de rota sem children', () => { + localStorage.setItem('token', 'tok-123'); + localStorage.setItem('usuario', JSON.stringify(USUARIO_MOCK)); + render( + + + + }> + Conteúdo via Outlet} /> + + + + , + ); + expect(screen.getByText('Conteúdo via Outlet')).toBeInTheDocument(); + }); }); From 066323db37099c22832965210841b43060295f30 Mon Sep 17 00:00:00 2001 From: icrcode Date: Tue, 9 Jun 2026 14:36:44 -0300 Subject: [PATCH 15/19] test: atualizar tipos de documentos em MeusCertificados e Verificar para novos valores do sistema --- frontend/src/pages/MeusCertificados.tsx | 10 +++-- frontend/src/pages/Verificar.tsx | 10 +++-- .../src/test/pages/MeusCertificados.test.tsx | 8 ++-- frontend/src/test/pages/Verificar.test.tsx | 4 +- .../003_update_tipo_documento_enum.sql | 39 +++++++++++++++++++ 5 files changed, 57 insertions(+), 14 deletions(-) create mode 100644 infra/migrations/003_update_tipo_documento_enum.sql diff --git a/frontend/src/pages/MeusCertificados.tsx b/frontend/src/pages/MeusCertificados.tsx index 487c346..9aa09f5 100644 --- a/frontend/src/pages/MeusCertificados.tsx +++ b/frontend/src/pages/MeusCertificados.tsx @@ -6,10 +6,12 @@ import { Button } from '../components/ui/Button'; import { EmojiEventsIcon } from '../components/icons'; const TIPO_LEGIVEL: Record = { - estagio: 'Estágio', - tcc: 'TCC', - extensao: 'Extensão', - monitoria: 'Monitoria', + certificado_curso: 'Certificado de Curso', + certificado_evento: 'Certificado de Evento', + declaracao_participacao: 'Declaração de Participação', + comprovante_atividade: 'Comprovante de Atividade', + artigo_publicado: 'Artigo Publicado', + outro: 'Outro', }; export function MeusCertificados() { diff --git a/frontend/src/pages/Verificar.tsx b/frontend/src/pages/Verificar.tsx index 1e4705d..76633e3 100644 --- a/frontend/src/pages/Verificar.tsx +++ b/frontend/src/pages/Verificar.tsx @@ -3,10 +3,12 @@ import { useQuery } from '@tanstack/react-query'; import axios from 'axios'; const TIPO_LEGIVEL: Record = { - estagio: 'Estágio', - tcc: 'Trabalho de Conclusão de Curso', - extensao: 'Atividade de Extensão', - monitoria:'Monitoria', + certificado_curso: 'Certificado de Curso', + certificado_evento: 'Certificado de Evento', + declaracao_participacao: 'Declaração de Participação', + comprovante_atividade: 'Comprovante de Atividade', + artigo_publicado: 'Artigo Publicado', + outro: 'Outro', }; interface RespostaVerificacao { diff --git a/frontend/src/test/pages/MeusCertificados.test.tsx b/frontend/src/test/pages/MeusCertificados.test.tsx index e70eb8d..d7c999c 100644 --- a/frontend/src/test/pages/MeusCertificados.test.tsx +++ b/frontend/src/test/pages/MeusCertificados.test.tsx @@ -27,7 +27,7 @@ const CERT_MOCK = { caminho_arquivo: 'certs/cert-1.pdf', criado_em: '2024-03-01T10:00:00Z', documento: { - id: 'doc-1', titulo: 'Workshop React', tipo: 'extensao', carga_horaria: 40, + id: 'doc-1', titulo: 'Workshop React', tipo: 'certificado_curso', carga_horaria: 40, estudante_id: 'u-1', curso_id: 'c-1', status: 'aprovado' as const, coordenador_id: 'coord-1', nome_arquivo: 'cert.pdf', caminho_arquivo: 'docs/cert.pdf', tamanho_arquivo: 1024, mime_type: 'application/pdf', @@ -80,7 +80,7 @@ describe('MeusCertificados — com certificados', () => { it('renderiza tipo e carga horária do documento', async () => { renderWithProviders(, { token: 'tok', usuario: USUARIO_ESTUDANTE }); await waitFor(() => { - expect(screen.getByText(/Extensão/i)).toBeInTheDocument(); + expect(screen.getByText(/Certificado de Curso/i)).toBeInTheDocument(); expect(screen.getByText(/40h/i)).toBeInTheDocument(); }); }); @@ -145,12 +145,12 @@ describe('MeusCertificados — com certificados', () => { it('usa tipo bruto como fallback quando tipo não está em TIPO_LEGIVEL', async () => { const certTipoDesconhecido = { ...CERT_MOCK, - documento: { ...CERT_MOCK.documento, tipo: 'certificado_curso' }, + documento: { ...CERT_MOCK.documento, tipo: 'tipo_desconhecido' }, }; mockCertificadosService.listar.mockResolvedValue([certTipoDesconhecido]); renderWithProviders(, { token: 'tok', usuario: USUARIO_ESTUDANTE }); await waitFor(() => { - expect(screen.getByText(/certificado_curso/)).toBeInTheDocument(); + expect(screen.getByText(/tipo_desconhecido/)).toBeInTheDocument(); }); }); diff --git a/frontend/src/test/pages/Verificar.test.tsx b/frontend/src/test/pages/Verificar.test.tsx index a2f68ac..166e6b0 100644 --- a/frontend/src/test/pages/Verificar.test.tsx +++ b/frontend/src/test/pages/Verificar.test.tsx @@ -106,12 +106,12 @@ describe('Verificar — estados da página', () => { valido: true, certificado: { id: 'cert-2', hash: 'xyz456', emitido_em: '2024-03-01T00:00:00Z' }, estudante: { nome: 'Maria', email: 'maria@ufsc.br' }, - documento: { titulo: 'TCC Final', tipo: 'tcc', carga_horaria: 60, status: 'aprovado' }, + documento: { titulo: 'TCC Final', tipo: 'artigo_publicado', carga_horaria: 60, status: 'aprovado' }, }; mockAxios.get.mockResolvedValue({ data: dados }); renderVerificar('xyz456'); await waitFor(() => - expect(screen.getByText('Trabalho de Conclusão de Curso · 60h')).toBeInTheDocument(), + expect(screen.getByText('Artigo Publicado · 60h')).toBeInTheDocument(), ); }); diff --git a/infra/migrations/003_update_tipo_documento_enum.sql b/infra/migrations/003_update_tipo_documento_enum.sql new file mode 100644 index 0000000..3d747a5 --- /dev/null +++ b/infra/migrations/003_update_tipo_documento_enum.sql @@ -0,0 +1,39 @@ +-- Migration 003: Atualiza tipo_documento_enum para os novos tipos do sistema +-- Execute em bancos existentes criados com o schema antigo (estagio, tcc, extensao, monitoria). +-- Para instalações novas o banco.sql já contém os valores corretos. +-- +-- Aplicar: +-- psql -U postgres -d valida_db -f infra/migrations/003_update_tipo_documento_enum.sql + +BEGIN; + +-- Renomeia o tipo antigo para não conflitar +ALTER TYPE tipo_documento_enum RENAME TO tipo_documento_enum_old; + +-- Cria o novo tipo com os valores corretos +CREATE TYPE tipo_documento_enum AS ENUM ( + 'certificado_curso', + 'certificado_evento', + 'declaracao_participacao', + 'comprovante_atividade', + 'artigo_publicado', + 'outro' +); + +-- Migra a coluna: valores antigos são mapeados para 'outro' +ALTER TABLE documentos + ALTER COLUMN tipo TYPE tipo_documento_enum + USING ( + CASE tipo::text + WHEN 'estagio' THEN 'outro' + WHEN 'tcc' THEN 'outro' + WHEN 'extensao' THEN 'outro' + WHEN 'monitoria' THEN 'outro' + ELSE tipo::text + END + )::tipo_documento_enum; + +-- Remove o tipo antigo +DROP TYPE tipo_documento_enum_old; + +COMMIT; From b905335ece337d2fdf88733e83dba405123bf23f Mon Sep 17 00:00:00 2001 From: icrcode Date: Tue, 9 Jun 2026 14:37:18 -0300 Subject: [PATCH 16/19] test: atualizar tipos de documentos para certificados e ajustar testes correspondentes --- backend/src/modulos/documentos/rotas.ts | 15 ++++++++ backend/src/servicos/gerador-certificado.ts | 10 +++--- backend/testes/helpers/fixtures.ts | 8 ++--- backend/testes/rotas/documentos.test.ts | 34 +++++++++---------- backend/testes/rotas/publica.test.ts | 2 +- .../unitarios/eventos/barramento.test.ts | 6 ++-- .../testes/unitarios/eventos/handlers.test.ts | 22 ++++++------ .../unitarios/repositorios/documento.test.ts | 16 ++++----- .../unitarios/repositorios/validacao.test.ts | 2 +- .../servicos/gerador-certificado.test.ts | 4 +-- 10 files changed, 68 insertions(+), 51 deletions(-) diff --git a/backend/src/modulos/documentos/rotas.ts b/backend/src/modulos/documentos/rotas.ts index e0679e9..22e7ac8 100644 --- a/backend/src/modulos/documentos/rotas.ts +++ b/backend/src/modulos/documentos/rotas.ts @@ -97,6 +97,21 @@ router.post('/', autenticar, exigirPerfil('estudante'), upload.single('arquivo') return; } + const TIPOS_VALIDOS = [ + 'certificado_curso', + 'certificado_evento', + 'declaracao_participacao', + 'comprovante_atividade', + 'artigo_publicado', + 'outro', + ]; + if (!TIPOS_VALIDOS.includes(tipo)) { + res.status(400).json({ + erro: `Tipo inválido. Valores aceitos: ${TIPOS_VALIDOS.join(', ')}`, + }); + return; + } + try { const usuario = await buscarUsuario(req.usuario!.sub); if (!usuario?.curso_id) { diff --git a/backend/src/servicos/gerador-certificado.ts b/backend/src/servicos/gerador-certificado.ts index 7761bb9..eec8528 100644 --- a/backend/src/servicos/gerador-certificado.ts +++ b/backend/src/servicos/gerador-certificado.ts @@ -23,10 +23,12 @@ export function gerarHash(documentoId: string): string { } const TIPO_LEGIVEL: Record = { - estagio: 'Estágio', - tcc: 'Trabalho de Conclusão de Curso', - extensao: 'Atividade de Extensão', - monitoria: 'Monitoria', + certificado_curso: 'Certificado de Curso', + certificado_evento: 'Certificado de Evento', + declaracao_participacao: 'Declaração de Participação', + comprovante_atividade: 'Comprovante de Atividade', + artigo_publicado: 'Artigo Publicado', + outro: 'Outro', }; export async function gerarPDF(dados: DadosCertificado, urlVerificacao: string): Promise { diff --git a/backend/testes/helpers/fixtures.ts b/backend/testes/helpers/fixtures.ts index 1a9bb49..4ae8a0c 100644 --- a/backend/testes/helpers/fixtures.ts +++ b/backend/testes/helpers/fixtures.ts @@ -2,15 +2,15 @@ import type { Documento } from '../../src/modulos/documentos/repositorio'; export const DOC_MOCK: Documento = { id: 'doc-1', - titulo: 'Estágio XYZ', - tipo: 'estagio', + titulo: 'Certificado XYZ', + tipo: 'certificado_curso', carga_horaria: 40, estudante_id: 'estudante-id', curso_id: 'curso-1', status: 'pendente', coordenador_id: null, - nome_arquivo: 'estagio.pdf', - caminho_arquivo: 'documentos/estudante-id/estagio.pdf', + nome_arquivo: 'certificado.pdf', + caminho_arquivo: 'documentos/estudante-id/certificado.pdf', tamanho_arquivo: 1024, mime_type: 'application/pdf', criado_em: new Date(), diff --git a/backend/testes/rotas/documentos.test.ts b/backend/testes/rotas/documentos.test.ts index 840a96e..c68e0bc 100644 --- a/backend/testes/rotas/documentos.test.ts +++ b/backend/testes/rotas/documentos.test.ts @@ -55,7 +55,7 @@ describe('GET /:id', () => { const res = await request(app).get('/doc-1'); expect(res.status).toBe(200); expect(res.body.id).toBe('doc-1'); - expect(res.body.titulo).toBe('Estágio XYZ'); + expect(res.body.titulo).toBe('Certificado XYZ'); }); it('retorna 404 quando o documento não existe', async () => { @@ -106,8 +106,8 @@ describe('POST /', () => { it('retorna 400 quando nenhum arquivo é enviado', async () => { const res = await request(app) .post('/') - .field('titulo', 'Estágio') - .field('tipo', 'estagio') + .field('titulo', 'Certificado') + .field('tipo', 'certificado_curso') .field('carga_horaria', '40'); expect(res.status).toBe(400); expect(res.body.erro).toContain('arquivo'); @@ -132,8 +132,8 @@ describe('POST /', () => { filename: 'test.pdf', contentType: 'application/pdf', }) - .field('titulo', 'Estágio') - .field('tipo', 'estagio') + .field('titulo', 'Certificado') + .field('tipo', 'certificado_curso') .field('carga_horaria', 'abc'); expect(res.status).toBe(400); expect(res.body.erro).toContain('carga_horaria'); @@ -147,8 +147,8 @@ describe('POST /', () => { filename: 'test.pdf', contentType: 'application/pdf', }) - .field('titulo', 'Estágio XYZ') - .field('tipo', 'estagio') + .field('titulo', 'Certificado XYZ') + .field('tipo', 'certificado_curso') .field('carga_horaria', '40'); expect(res.status).toBe(422); expect(res.body.erro).toContain('curso'); @@ -156,29 +156,29 @@ describe('POST /', () => { it('retorna 201 com o documento criado após upload bem-sucedido', async () => { mockUsr.buscarPorId.mockResolvedValueOnce({ id: 'estudante-id', curso_id: 'curso-1' } as any); - mockArm.fazerUpload.mockResolvedValueOnce('documentos/estudante-id/estagio.pdf'); + mockArm.fazerUpload.mockResolvedValueOnce('documentos/estudante-id/certificado.pdf'); mockDoc.criar.mockResolvedValueOnce(DOC_MOCK); const res = await request(app) .post('/') .attach('arquivo', Buffer.from('%PDF-1.4'), { - filename: 'estagio.pdf', + filename: 'certificado.pdf', contentType: 'application/pdf', }) - .field('titulo', 'Estágio XYZ') - .field('tipo', 'estagio') + .field('titulo', 'Certificado XYZ') + .field('tipo', 'certificado_curso') .field('carga_horaria', '40'); expect(res.status).toBe(201); expect(res.body.id).toBe('doc-1'); - expect(res.body.titulo).toBe('Estágio XYZ'); + expect(res.body.titulo).toBe('Certificado XYZ'); expect(mockEmitir).toHaveBeenCalledWith( 'documento_submetido', expect.objectContaining({ documentoId: 'doc-1', estudanteId: 'estudante-id', cursoId: 'curso-1', - titulo: 'Estágio XYZ', - tipo: 'estagio', + titulo: 'Certificado XYZ', + tipo: 'certificado_curso', }), ); }); @@ -189,9 +189,9 @@ describe('POST /', () => { await request(app) .post('/') - .attach('arquivo', Buffer.from('%PDF-1.4'), { filename: 'estagio.pdf', contentType: 'application/pdf' }) - .field('titulo', 'Estágio XYZ') - .field('tipo', 'estagio') + .attach('arquivo', Buffer.from('%PDF-1.4'), { filename: 'certificado.pdf', contentType: 'application/pdf' }) + .field('titulo', 'Certificado XYZ') + .field('tipo', 'certificado_curso') .field('carga_horaria', '40'); expect(mockEmitir).not.toHaveBeenCalled(); diff --git a/backend/testes/rotas/publica.test.ts b/backend/testes/rotas/publica.test.ts index 7daf00f..9337b2d 100644 --- a/backend/testes/rotas/publica.test.ts +++ b/backend/testes/rotas/publica.test.ts @@ -57,7 +57,7 @@ describe('GET /verificar/:hash', () => { expect(res.status).toBe(200); expect(res.body.valido).toBe(true); expect(res.body.certificado.id).toBe('cert-1'); - expect(res.body.documento.titulo).toBe('Estágio XYZ'); + expect(res.body.documento.titulo).toBe('Certificado XYZ'); expect(res.body.estudante.nome).toBe('João Silva'); }); diff --git a/backend/testes/unitarios/eventos/barramento.test.ts b/backend/testes/unitarios/eventos/barramento.test.ts index d7b0aab..12cfbed 100644 --- a/backend/testes/unitarios/eventos/barramento.test.ts +++ b/backend/testes/unitarios/eventos/barramento.test.ts @@ -12,15 +12,15 @@ const submetido: EventoDocumentoSubmetido = { documentoId: 'doc-1', estudanteId: 'est-1', cursoId: 'curso-1', - titulo: 'Estágio XYZ', - tipo: 'estagio', + titulo: 'Certificado XYZ', + tipo: 'certificado_curso', }; const aprovado: EventoDocumentoAprovado = { documentoId: 'doc-1', estudanteId: 'est-1', coordenadorId: 'coord-1', - titulo: 'Estágio XYZ', + titulo: 'Certificado XYZ', }; const reprovado: EventoDocumentoReprovado = { diff --git a/backend/testes/unitarios/eventos/handlers.test.ts b/backend/testes/unitarios/eventos/handlers.test.ts index dc2e2d0..8a370e8 100644 --- a/backend/testes/unitarios/eventos/handlers.test.ts +++ b/backend/testes/unitarios/eventos/handlers.test.ts @@ -43,15 +43,15 @@ const ESTUDANTE_MOCK = { const DOCUMENTO_MOCK = { id: 'doc-1', - titulo: 'Estágio XYZ', - tipo: 'estagio', + titulo: 'Certificado XYZ', + tipo: 'certificado_curso', carga_horaria: 40, estudante_id: 'est-1', curso_id: 'curso-1', status: 'aprovado', coordenador_id: 'coord-1', - nome_arquivo: 'estagio.pdf', - caminho_arquivo: 'documentos/est-1/estagio.pdf', + nome_arquivo: 'certificado.pdf', + caminho_arquivo: 'documentos/est-1/certificado.pdf', tamanho_arquivo: 1024, mime_type: 'application/pdf', criado_em: new Date(), @@ -85,8 +85,8 @@ describe('aoDocumentoSubmetido', () => { documentoId: 'doc-1', estudanteId: 'est-1', cursoId: 'curso-1', - titulo: 'Estágio XYZ', - tipo: 'estagio', + titulo: 'Certificado XYZ', + tipo: 'certificado_curso', }; it('notifica cada coordenador encontrado para o curso', async () => { @@ -105,7 +105,7 @@ describe('aoDocumentoSubmetido', () => { destinatarioId: 'coord-1', destinatarioEmail: 'maria@uni.edu', assunto: expect.stringContaining('validação'), - mensagem: expect.stringContaining('Estágio XYZ'), + mensagem: expect.stringContaining('Certificado XYZ'), }), ); }); @@ -134,7 +134,7 @@ describe('aoDocumentoAprovado', () => { documentoId: 'doc-1', estudanteId: 'est-1', coordenadorId: 'coord-1', - titulo: 'Estágio XYZ', + titulo: 'Certificado XYZ', }; it('gera certificado, persiste no banco e notifica o estudante', async () => { @@ -161,7 +161,7 @@ describe('aoDocumentoAprovado', () => { destinatarioId: 'est-1', destinatarioEmail: 'joao@uni.edu', assunto: expect.stringContaining('aprovado'), - mensagem: expect.stringContaining('Estágio XYZ'), + mensagem: expect.stringContaining('Certificado XYZ'), }), ); }); @@ -208,7 +208,7 @@ describe('aoDocumentoReprovado', () => { documentoId: 'doc-1', estudanteId: 'est-1', coordenadorId: 'coord-1', - titulo: 'Estágio XYZ', + titulo: 'Certificado XYZ', observacoes: 'Documentação incompleta', }; @@ -250,7 +250,7 @@ describe('aoDocumentoRevisaoSolicitada', () => { documentoId: 'doc-1', estudanteId: 'est-1', coordenadorId: 'coord-1', - titulo: 'Estágio XYZ', + titulo: 'Certificado XYZ', observacoes: 'Ajustar carga horária', }; diff --git a/backend/testes/unitarios/repositorios/documento.test.ts b/backend/testes/unitarios/repositorios/documento.test.ts index c5aeb9a..e81c527 100644 --- a/backend/testes/unitarios/repositorios/documento.test.ts +++ b/backend/testes/unitarios/repositorios/documento.test.ts @@ -14,15 +14,15 @@ const mockQuery = pool.query as jest.Mock; const DOC_ROW = { id: 'doc-1', - titulo: 'Estágio XYZ', - tipo: 'estagio', + titulo: 'Certificado XYZ', + tipo: 'certificado_curso', carga_horaria: 40, estudante_id: 'est-1', curso_id: 'curso-1', status: 'pendente', coordenador_id: null, - nome_arquivo: 'estagio.pdf', - caminho_arquivo: 'documentos/est-1/estagio.pdf', + nome_arquivo: 'certificado.pdf', + caminho_arquivo: 'documentos/est-1/certificado.pdf', tamanho_arquivo: 1024, mime_type: 'application/pdf', criado_em: new Date(), @@ -39,13 +39,13 @@ describe('criar', () => { mockQuery.mockResolvedValueOnce({ rows: [DOC_ROW] }); const resultado = await criar({ - titulo: 'Estágio XYZ', - tipo: 'estagio', + titulo: 'Certificado XYZ', + tipo: 'certificado_curso', carga_horaria: 40, estudante_id: 'est-1', curso_id: 'curso-1', - nome_arquivo: 'estagio.pdf', - caminho_arquivo: 'documentos/est-1/estagio.pdf', + nome_arquivo: 'certificado.pdf', + caminho_arquivo: 'documentos/est-1/certificado.pdf', tamanho_arquivo: 1024, mime_type: 'application/pdf', }); diff --git a/backend/testes/unitarios/repositorios/validacao.test.ts b/backend/testes/unitarios/repositorios/validacao.test.ts index 7faa470..f859193 100644 --- a/backend/testes/unitarios/repositorios/validacao.test.ts +++ b/backend/testes/unitarios/repositorios/validacao.test.ts @@ -21,7 +21,7 @@ const clienteMock = { const DOC_ROW = { id: 'doc-1', - titulo: 'Estágio XYZ', + titulo: 'Certificado XYZ', status: 'pendente', estudante_id: 'est-1', curso_id: 'curso-1', diff --git a/backend/testes/unitarios/servicos/gerador-certificado.test.ts b/backend/testes/unitarios/servicos/gerador-certificado.test.ts index d84115a..685ff98 100644 --- a/backend/testes/unitarios/servicos/gerador-certificado.test.ts +++ b/backend/testes/unitarios/servicos/gerador-certificado.test.ts @@ -20,8 +20,8 @@ const DADOS_BASE: DadosCertificado = { documentoId: 'doc-1', estudanteNome: 'João Silva', estudanteEmail: 'joao@uni.edu', - titulo: 'Estágio XYZ', - tipo: 'estagio', + titulo: 'Certificado XYZ', + tipo: 'certificado_curso', cargaHoraria: 40, dataAprovacao: new Date('2024-06-01'), }; From 29213ed4c4211e3cb7cab7f50fae2f7497410241 Mon Sep 17 00:00:00 2001 From: icrcode Date: Tue, 9 Jun 2026 15:40:50 -0300 Subject: [PATCH 17/19] =?UTF-8?q?test:=20adicionar=20pr=C3=A9-visualiza?= =?UTF-8?q?=C3=A7=C3=A3o=20de=20documento=20PDF=20e=20funcionalidade=20de?= =?UTF-8?q?=20download?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/DetalheDocumento.tsx | 34 +++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/frontend/src/pages/DetalheDocumento.tsx b/frontend/src/pages/DetalheDocumento.tsx index 51245f0..54c95fa 100644 --- a/frontend/src/pages/DetalheDocumento.tsx +++ b/frontend/src/pages/DetalheDocumento.tsx @@ -75,6 +75,13 @@ export function DetalheDocumento() { onError: () => addToast('Erro ao cancelar o documento.', 'error'), }); + const { data: previewData, isLoading: previewLoading } = useQuery({ + queryKey: ['documento-url', id], + queryFn: () => documentosService.buscarUrlDownload(id!), + enabled: !!id && !!doc, + staleTime: 50 * 60 * 1000, + }); + const mutDownload = useMutation({ mutationFn: () => documentosService.buscarUrlDownload(id!), onSuccess: ({ url }) => window.open(url, '_blank'), @@ -139,6 +146,33 @@ export function DetalheDocumento() { + {/* Pré-visualização PDF */} + +

Pré-visualização

+ {previewLoading && ( +
+ + Carregando documento... +
+ )} + {previewData && ( +