Feature/add pagination#59
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
This PR updates the jobs UI pagination experience in the frontend (numeric pagination controls + bounded page size) and adjusts backend Swagger initialization to be optional via dynamic import, alongside related test updates.
Changes:
- Frontend: clamp/limit page size in
useJobsPaginationand update the jobs table footer to show a results summary + numeric pagination with arrows. - Frontend: update unit tests to reflect the new pagination UI and page-size constraints.
- Backend: register Swagger docs via dynamic import and update server-entry tests accordingly.
Reviewed changes
Copilot reviewed 8 out of 10 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| Vagas Full Setup 1.0.0.exe | Packaged installer artifact included in the PR. |
| package.json | Adds Swagger dependencies to root dependencies list and formatting change in electron-builder config. |
| package-lock.json | Locks Swagger dependency additions. |
| frontend/src/hooks/useJobsPagination.ts | Introduces clampPageSize and changes default page size to 4 with bounds (1..10). |
| frontend/src/components/JobsTableCard.tsx | Replaces old pagination controls with results summary + numeric pagination + page-size input. |
| frontend/src/App.tsx | Removes unused jobs prop wiring to JobsTableCard. |
| frontend/tests/unit/utils/useJobsPagination.test.ts | Adds act() usage and tests for page-size clamping. |
| frontend/tests/unit/components/JobsTableCard.test.tsx | Updates tests for new pagination UI and results summary. |
| backend/src/server.js | Makes Swagger docs registration optional via dynamic import; wraps startup in async flow. |
| backend/tests/unit/services/server.test.js | Updates mocks/import flow to account for dynamic imports and startup behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Router (if used in future) works correctly. | ||
| app.use((req, res) => { | ||
| if (req.path.startsWith("/api")) { | ||
| return res.status(404).json({ error: "Rota não encontrada." }); |
There was a problem hiding this comment.
No fallback do SPA, a resposta 404 para rotas /api usa o campo error, mas o frontend lê mensagens de erro do campo message (ver frontend/src/services/jobsService.ts). Para manter consistência com o restante da API (jobsApiApp), retorne { message: "..." } (ou inclua ambos).
| return res.status(404).json({ error: "Rota não encontrada." }); | |
| return res | |
| .status(404) | |
| .json({ message: "Rota não encontrada.", error: "Rota não encontrada." }); |
| const [{ default: swaggerUi }, { default: swaggerSpec }] = await Promise.all([ | ||
| import("swagger-ui-express"), | ||
| import("./swagger.js"), | ||
| ]); | ||
|
|
||
| app.use("/docs", swaggerUi.serve, swaggerUi.setup(swaggerSpec)); |
There was a problem hiding this comment.
swaggerSpec vem de ./swagger.js, que hoje resolve apis: [path.resolve("src/**/*.js")] (dependente de process.cwd()). No Electron o cwd costuma ser o app root, então o glob pode não encontrar backend/src/**/*.js e a documentação fica incompleta. Sugestão: ajustar o swagger.js para resolver o diretório baseado em import.meta.url (ou passar um caminho absoluto daqui do server).
| <nav className="flex items-center gap-1 self-end md:self-auto" aria-label="Paginação"> | ||
| <button | ||
| type="button" | ||
| aria-label="Pagina anterior" | ||
| onClick={() => onPageChange(Math.max(1, currentPage - 1))} | ||
| disabled={currentPage === 1} | ||
| className="flex h-9 min-w-9 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:cursor-not-allowed disabled:opacity-40" | ||
| > | ||
| <FiChevronLeft className="h-4 w-4" /> | ||
| </button> | ||
|
|
||
| {paginationItems.map((item, index) => { | ||
| if (typeof item !== "number") { | ||
| return ( | ||
| <span key={`${item}-${index}`} className="px-2 text-sm text-muted-foreground" aria-hidden="true"> | ||
| … | ||
| </span> | ||
| ); | ||
| } | ||
|
|
||
| const isCurrent = item === currentPage; | ||
|
|
||
| return ( | ||
| <button | ||
| key={item} | ||
| type="button" | ||
| aria-current={isCurrent ? "page" : undefined} | ||
| onClick={() => onPageChange(item)} | ||
| className={cn( | ||
| "flex h-9 min-w-9 items-center justify-center rounded-full px-3 text-sm font-medium transition-colors", | ||
| isCurrent | ||
| ? "bg-[#0c6b35] text-white shadow-sm" | ||
| : "text-muted-foreground hover:bg-muted hover:text-foreground", | ||
| )} | ||
| > | ||
| {item} | ||
| </button> | ||
| ); | ||
| })} | ||
|
|
||
| <button | ||
| type="button" | ||
| aria-label="Pagina seguinte" | ||
| onClick={() => onPageChange(Math.min(totalPages, currentPage + 1))} | ||
| disabled={currentPage === totalPages} | ||
| className="flex h-9 min-w-9 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:cursor-not-allowed disabled:opacity-40" |
There was a problem hiding this comment.
Os aria-labels da paginação estão sem acento ("Pagina"). Como o restante do UI usa PT-BR com acentuação (ex.: "Título"), ajuste para "Página anterior" / "Página seguinte" para consistência e melhor leitura por leitores de tela.
| function clampPageSize(value: number) { | ||
| if (!Number.isFinite(value)) { | ||
| return 1; | ||
| } | ||
|
|
||
| return Math.min(10, Math.max(1, Math.trunc(value))); |
There was a problem hiding this comment.
O limite de page size (1..10) está “hardcoded” tanto no hook (clampPageSize) quanto no input (max={10}). Para evitar divergência futura, considere extrair esses limites para constantes compartilhadas (ex.: MIN_PAGE_SIZE/MAX_PAGE_SIZE) e reutilizá-las no hook, UI e testes.
| function clampPageSize(value: number) { | |
| if (!Number.isFinite(value)) { | |
| return 1; | |
| } | |
| return Math.min(10, Math.max(1, Math.trunc(value))); | |
| const MIN_PAGE_SIZE = 1; | |
| const MAX_PAGE_SIZE = 10; | |
| function clampPageSize(value: number) { | |
| if (!Number.isFinite(value)) { | |
| return MIN_PAGE_SIZE; | |
| } | |
| return Math.min(MAX_PAGE_SIZE, Math.max(MIN_PAGE_SIZE, Math.trunc(value))); |
| currentPage: 1, | ||
| totalPages: 2, | ||
| pageSize: 25, | ||
| totalPages: 5, | ||
| pageSize: 4, | ||
| onPageChange: vi.fn(), | ||
| onPageSizeChange: vi.fn(), | ||
| }; | ||
|
|
||
| describe("JobsTableCard", () => { | ||
| it("renderiza tabela com palavras-chave em tags e colunas de spam e lido", () => { | ||
| it("renderiza tabela com resumo, palavras-chave e colunas de spam e lido", () => { | ||
| render(<JobsTableCard {...baseProps} />); | ||
|
|
||
| expect(screen.getByText("Vagas Encontradas")).toBeInTheDocument(); | ||
| expect(screen.getByText(/Resultados: 1 vaga encontrada/i)).toBeInTheDocument(); | ||
| expect(screen.getByText("Abrir vaga")).toBeInTheDocument(); | ||
| expect(screen.getByText("JavaScript")).toBeInTheDocument(); | ||
| expect(screen.getByText("UX-UI")).toBeInTheDocument(); | ||
| expect(screen.getByText(/spam/i)).toBeInTheDocument(); | ||
| expect(screen.getByText(/lido/i)).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("aciona paginacao", () => { | ||
| it("aciona paginacao numerica", () => { | ||
| render(<JobsTableCard {...baseProps} />); | ||
| fireEvent.click(screen.getByRole("button", { name: "Proxima" })); | ||
| expect(baseProps.onPageChange).toHaveBeenCalled(); | ||
| fireEvent.click(screen.getByRole("button", { name: "2" })); | ||
| expect(baseProps.onPageChange).toHaveBeenCalledWith(2); | ||
| }); | ||
|
|
||
| it("exibe setas para voltar e avancar entre paginas", () => { | ||
| render(<JobsTableCard {...baseProps} currentPage={3} />); | ||
|
|
||
| fireEvent.click(screen.getByRole("button", { name: /pagina anterior/i })); | ||
| fireEvent.click(screen.getByRole("button", { name: /pagina seguinte/i })); | ||
|
|
||
| expect(baseProps.onPageChange).toHaveBeenCalledWith(2); | ||
| expect(baseProps.onPageChange).toHaveBeenCalledWith(4); | ||
| }); |
There was a problem hiding this comment.
Os testes reutilizam o mesmo mock baseProps.onPageChange entre casos sem limpar as chamadas. Isso pode mascarar falhas (um teste passar por causa de chamadas de um teste anterior). Sugestão: criar baseProps via factory por teste ou usar beforeEach(() => { baseProps.onPageChange.mockClear(); baseProps.onPageSizeChange.mockClear(); })/vi.clearAllMocks().
| app.listen(PORT, () => { | ||
| // eslint-disable-next-line no-console | ||
| console.log(`API de vagas rodando em http://localhost:${PORT}`); | ||
| console.log(`Documentação da API em http://localhost:${PORT}/docs`); | ||
| }); |
There was a problem hiding this comment.
O callback de listen sempre loga a URL de /docs, mesmo quando o Swagger falha ao registrar (o registerSwaggerDocs() apenas faz warn). Isso pode gerar logs enganosos e, com ELECTRON_STATIC_DIR, /docs pode cair no catch-all do SPA. Sugestão: fazer registerSwaggerDocs() retornar boolean e logar/registrar a rota apenas quando habilitada.
No description provided.