Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
b67b013
Initial project setup
JorgeMadson Aug 11, 2025
e376c3a
chore: add shadcn to the project
JorgeMadson Aug 11, 2025
38cb3e4
chore: install shadcn
JorgeMadson Aug 11, 2025
c18302a
feat: update layout and home page with GitHub static search; add butt…
JorgeMadson Aug 11, 2025
610d1b1
refact: home & implement search functionality and repository card dis…
JorgeMadson Aug 11, 2025
1ab3b9b
feat: add header and footer components; refact main template layout a…
JorgeMadson Aug 11, 2025
09c2b89
feat: repo details page
JorgeMadson Aug 11, 2025
c126bd3
refact: details page following atomic design
JorgeMadson Aug 11, 2025
60c1239
feat: improve SEO, generateMetadata on main pages
JorgeMadson Aug 12, 2025
cc0c379
feat: add error boundary with friendly error page
JorgeMadson Aug 12, 2025
124f5b9
build: add test configuration
JorgeMadson Aug 12, 2025
64168d2
test: add tests
JorgeMadson Aug 12, 2025
10a5b07
refact: simplify atoms and molecules components
JorgeMadson Aug 12, 2025
2f41284
refact: github service & remove unecessary comments
JorgeMadson Aug 12, 2025
f062980
build: installing nuqs
JorgeMadson Aug 12, 2025
13e7c53
refact: using nuqs for search and creating custom hooks
JorgeMadson Aug 12, 2025
21249d0
fix: back button was going back to clear home page
JorgeMadson Aug 12, 2025
9df159c
fix: back button test fixed
JorgeMadson Aug 12, 2025
35924d2
feat: home pagination
JorgeMadson Aug 12, 2025
ea3f9a5
feat: add keyboard navigation support; improve accessibility in pagin…
JorgeMadson Aug 12, 2025
fba2ce3
feat: add auto-focus to search-box
JorgeMadson Aug 12, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files (can opt-in for committing if needed)
.env*

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
26 changes: 26 additions & 0 deletions __tests__/atoms/button-atom.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { render, screen, fireEvent } from "@testing-library/react"
import "@testing-library/jest-dom"
import ButtonAtom from "@/components/atoms/button-atom"
import { describe, expect, it, jest } from '@jest/globals'

describe("ButtonAtom", () => {
it("renders and clicks", () => {
const onClick = jest.fn()
render(<ButtonAtom onClick={onClick}>Clique</ButtonAtom>)
const btn = screen.getByRole("button", { name: "Clique" })
expect(btn).toBeInTheDocument()
fireEvent.click(btn)
expect(onClick).toHaveBeenCalledTimes(1)
})

it("respects disabled state", () => {
const onClick = jest.fn()
render(
<ButtonAtom disabled onClick={onClick}>
Desabilitado
</ButtonAtom>,
)
const btn = screen.getByRole("button", { name: "Desabilitado" })
expect(btn).toBeDisabled()
})
})
14 changes: 14 additions & 0 deletions __tests__/atoms/input-atom.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { render, screen, fireEvent } from "@testing-library/react"
import "@testing-library/jest-dom"
import InputAtom from "@/components/atoms/input-atom"
import { describe, expect, it } from '@jest/globals'

describe("InputAtom", () => {
it("renders with label (sr-only) and updates value", () => {
render(<InputAtom label="Usuário" id="user" placeholder="Digite" />)
const input = screen.getByPlaceholderText("Digite") as HTMLInputElement
expect(input).toBeInTheDocument()
fireEvent.change(input, { target: { value: "vercel" } })
expect(input.value).toBe("vercel")
})
})
92 changes: 92 additions & 0 deletions __tests__/components/error-boundary.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { render, screen, fireEvent } from '@testing-library/react'
import { describe, expect, it, jest } from '@jest/globals'
import ErrorPage from '@/app/error'

describe('Error Page (Next.js Error Boundary)', () => {
const mockError = new Error('Test error message')
const mockReset = jest.fn()

beforeEach(() => {
mockReset.mockClear()
})

it('should render error message and reset button', () => {
render(<ErrorPage error={mockError} reset={mockReset} />)

expect(screen.getByText('Algo deu errado')).toBeInTheDocument()
expect(screen.getByText(/Ocorreu um erro inesperado/)).toBeInTheDocument()
expect(screen.getByRole('button', { name: /tentar novamente/i })).toBeInTheDocument()
})

it('should display error details when expanded', () => {
render(<ErrorPage error={mockError} reset={mockReset} />)

const detailsSummary = screen.getByText('Detalhes técnicos')
fireEvent.click(detailsSummary)

expect(screen.getByText('Test error message')).toBeInTheDocument()
})

it('should call reset function when button is clicked', () => {
render(<ErrorPage error={mockError} reset={mockReset} />)

const resetButton = screen.getByRole('button', { name: /tentar novamente/i })
fireEvent.click(resetButton)

expect(mockReset).toHaveBeenCalledTimes(1)
})

it('should render without error details when no error provided', () => {
// @ts-ignore - Testing edge case
render(<ErrorPage error={null} reset={mockReset} />)

expect(screen.getByText('Algo deu errado')).toBeInTheDocument()
expect(screen.queryByText('Detalhes técnicos')).not.toBeInTheDocument()
})

it('should have proper styling classes', () => {
const { container } = render(<ErrorPage error={mockError} reset={mockReset} />)

expect(container.firstChild).toHaveClass('min-h-[400px]', 'flex', 'items-center', 'justify-center', 'p-4')
})

it('should have alert triangle icon', () => {
const { container } = render(<ErrorPage error={mockError} reset={mockReset} />)

const alertIcon = container.querySelector('svg')
expect(alertIcon).toBeInTheDocument()
})

it('should have refresh icon in reset button', () => {
render(<ErrorPage error={mockError} reset={mockReset} />)

const resetButton = screen.getByRole('button', { name: /tentar novamente/i })
const refreshIcon = resetButton.querySelector('svg')
expect(refreshIcon).toBeInTheDocument()
})

it('should render card structure correctly', () => {
render(<ErrorPage error={mockError} reset={mockReset} />)

const cardTitle = screen.getByText('Algo deu errado')
expect(cardTitle).toBeInTheDocument()
})

it('should handle long error messages', () => {
const longError = new Error('A'.repeat(1000))
render(<ErrorPage error={longError} reset={mockReset} />)

const detailsSummary = screen.getByText('Detalhes técnicos')
fireEvent.click(detailsSummary)

expect(screen.getByText('A'.repeat(1000))).toBeInTheDocument()
})

it('should be accessible via keyboard navigation', () => {
render(<ErrorPage error={mockError} reset={mockReset} />)

const resetButton = screen.getByRole('button', { name: /tentar novamente/i })
expect(resetButton.tagName.toLowerCase()).toBe('button')
expect(resetButton).not.toHaveAttribute('tabindex', '-1')
})
})
82 changes: 82 additions & 0 deletions __tests__/components/molecules/pagination-controls.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { render, screen, fireEvent } from '@testing-library/react'
import PaginationControls from '@/components/molecules/pagination-controls'

describe('PaginationControls', () => {
const defaultProps = {
currentPage: 2,
hasNextPage: true,
hasPrevPage: true,
onNextPage: jest.fn(),
onPrevPage: jest.fn(),
}

beforeEach(() => {
jest.clearAllMocks()
})

it('renders current page correctly', () => {
render(<PaginationControls {...defaultProps} />)

expect(screen.getByText('Página 2')).toBeInTheDocument()
})

it('calls onPrevPage when previous button is clicked', () => {
render(<PaginationControls {...defaultProps} />)

const prevButton = screen.getByRole('button', { name: /ir para página anterior/i })
fireEvent.click(prevButton)

expect(defaultProps.onPrevPage).toHaveBeenCalledTimes(1)
})

it('calls onNextPage when next button is clicked', () => {
render(<PaginationControls {...defaultProps} />)

const nextButton = screen.getByRole('button', { name: /ir para próxima página/i })
fireEvent.click(nextButton)

expect(defaultProps.onNextPage).toHaveBeenCalledTimes(1)
})

it('disables previous button when hasPrevPage is false', () => {
render(<PaginationControls {...defaultProps} hasPrevPage={false} />)

const prevButton = screen.getByRole('button', { name: /ir para página anterior/i })
expect(prevButton).toBeDisabled()
})

it('disables next button when hasNextPage is false', () => {
render(<PaginationControls {...defaultProps} hasNextPage={false} />)

const nextButton = screen.getByRole('button', { name: /ir para próxima página/i })
expect(nextButton).toBeDisabled()
})

it('disables both buttons when loading', () => {
render(<PaginationControls {...defaultProps} isLoading={true} />)

const prevButton = screen.getByRole('button', { name: /ir para página anterior/i })
const nextButton = screen.getByRole('button', { name: /ir para próxima página/i })

expect(prevButton).toBeDisabled()
expect(nextButton).toBeDisabled()
})

it('shows page 1 correctly', () => {
render(<PaginationControls {...defaultProps} currentPage={1} hasPrevPage={false} />)

expect(screen.getByText('Página 1')).toBeInTheDocument()

const prevButton = screen.getByRole('button', { name: /ir para página anterior/i })
expect(prevButton).toBeDisabled()
})

it('handles last page correctly', () => {
render(<PaginationControls {...defaultProps} currentPage={5} hasNextPage={false} />)

expect(screen.getByText('Página 5')).toBeInTheDocument()

const nextButton = screen.getByRole('button', { name: /ir para próxima página/i })
expect(nextButton).toBeDisabled()
})
})
Loading