Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
85 changes: 85 additions & 0 deletions src/components/__tests__/ErrorLayout.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// @vitest-environment happy-dom

import '@testing-library/jest-dom/vitest'
import React from 'react'
import type { ReactNode } from 'react'
import { fireEvent, render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import ErrorButton from '@/components/ErrorButton'
import ErrorLayout from '@/components/ErrorLayout'

vi.mock('next/link', () => ({
default: ({
href,
children,
className,
}: {
href: string
children: ReactNode
className?: string
}) => React.createElement('a', { href, className }, children),
}))

describe('ErrorLayout', () => {
it('renders error page content inside the shared layout shell', () => {
render(
React.createElement(
ErrorLayout,
null,
React.createElement('h1', null, 'Recoverable error'),
),
)

expect(screen.getByRole('heading', { level: 1, name: 'Recoverable error' })).toBeInTheDocument()
})
})

describe('ErrorButton', () => {
it('renders an internal recovery link', () => {
render(React.createElement(ErrorButton, { href: '/dashboard' }, 'Dashboard'))

expect(screen.getByRole('link', { name: 'Dashboard' })).toHaveAttribute('href', '/dashboard')
})

it('renders an external support link safely', () => {
render(
React.createElement(
ErrorButton,
{
href: 'https://stellar.org/contact',
isExternal: true,
variant: 'secondary',
},
'Report Issue',
),
)

const link = screen.getByRole('link', { name: 'Report Issue' })
expect(link).toHaveAttribute('href', 'https://stellar.org/contact')
expect(link).toHaveAttribute('target', '_blank')
expect(link).toHaveAttribute('rel', 'noopener noreferrer')
})

it('calls click handlers for button recovery actions', () => {
const onClick = vi.fn()

render(React.createElement(ErrorButton, { onClick }, 'Try Again'))

fireEvent.click(screen.getByRole('button', { name: 'Try Again' }))

expect(onClick).toHaveBeenCalledTimes(1)
})

it('supports disabled recovery buttons', () => {
const onClick = vi.fn()

render(React.createElement(ErrorButton, { onClick, disabled: true }, 'Retrying...'))

const button = screen.getByRole('button', { name: 'Retrying...' })
expect(button).toBeDisabled()

fireEvent.click(button)

expect(onClick).not.toHaveBeenCalled()
})
})
54 changes: 54 additions & 0 deletions tests/error-page.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// @vitest-environment happy-dom

import '@testing-library/jest-dom/vitest'
import React from 'react'
import type { ReactNode } from 'react'
import { fireEvent, render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import ErrorPage from '@/app/error'

vi.mock('next/link', () => ({
default: ({
href,
children,
className,
}: {
href: string
children: ReactNode
className?: string
}) => React.createElement('a', { href, className }, children),
}))

describe('app error page', () => {
it('renders server error details and recovery actions', () => {
const reset = vi.fn()
const error = Object.assign(new Error('Database connection failed'), {
digest: 'ERR-123',
})

render(React.createElement(ErrorPage, { error, reset }))

expect(screen.getByText('500')).toBeInTheDocument()
expect(screen.getByRole('heading', { level: 1, name: 'Something Went Wrong' })).toBeInTheDocument()
expect(screen.getByText('Database connection failed')).toBeInTheDocument()
expect(screen.getByText('Error ID: ERR-123')).toBeInTheDocument()
expect(screen.getByRole('link', { name: 'Go Home' })).toHaveAttribute('href', '/')
expect(screen.getByRole('link', { name: 'Report Issue' })).toHaveAttribute(
'href',
'https://stellar.org/contact',
)

fireEvent.click(screen.getByRole('button', { name: 'Try Again' }))

expect(reset).toHaveBeenCalledTimes(1)
})

it('falls back when an error has no message', () => {
render(React.createElement(ErrorPage, {
error: Object.assign(new Error(''), { message: '' }),
reset: vi.fn(),
}))

expect(screen.getByText('An unexpected error occurred')).toBeInTheDocument()
})
})
61 changes: 61 additions & 0 deletions tests/network-error-page.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// @vitest-environment happy-dom

import '@testing-library/jest-dom/vitest'
import React from 'react'
import type { ReactNode } from 'react'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import NetworkError from '@/app/network-error/page'

vi.mock('next/link', () => ({
default: ({
href,
children,
className,
}: {
href: string
children: ReactNode
className?: string
}) => React.createElement('a', { href, className }, children),
}))

describe('network error page', () => {
afterEach(() => {
vi.unstubAllGlobals()
})

it('renders troubleshooting guidance and recovery actions', () => {
render(React.createElement(NetworkError))

expect(screen.getByRole('heading', { level: 1, name: 'Connection Error' })).toBeInTheDocument()
expect(screen.getByRole('heading', { level: 2, name: 'What you can do:' })).toBeInTheDocument()
expect(screen.getByText("Check that you're connected to the internet")).toBeInTheDocument()
expect(screen.getByText('No internet connection detected')).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'Retry' })).toBeEnabled()
expect(screen.getByRole('link', { name: 'Go Home' })).toHaveAttribute('href', '/')
})

it('shows retrying status and disables retry while connectivity is being checked', () => {
vi.stubGlobal('fetch', vi.fn(() => new Promise<Response>(() => undefined)))

render(React.createElement(NetworkError))

fireEvent.click(screen.getByRole('button', { name: 'Retry' }))

expect(screen.getByText('Checking connection...')).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'Retrying...' })).toBeDisabled()
})

it('restores the idle status when the connectivity check fails', async () => {
vi.stubGlobal('fetch', vi.fn(() => Promise.reject(new Error('offline'))))

render(React.createElement(NetworkError))

fireEvent.click(screen.getByRole('button', { name: 'Retry' }))

await waitFor(() => {
expect(screen.getByText('No internet connection detected')).toBeInTheDocument()
})
expect(screen.getByRole('button', { name: 'Retry' })).toBeEnabled()
})
})
60 changes: 60 additions & 0 deletions tests/not-found-page.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// @vitest-environment happy-dom

import '@testing-library/jest-dom/vitest'
import React from 'react'
import type { ReactNode } from 'react'
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import NotFound from '@/app/not-found'

const routerBack = vi.fn()

vi.mock('next/navigation', () => ({
useRouter: () => ({
back: routerBack,
}),
}))

vi.mock('next/link', () => ({
default: ({
href,
children,
className,
}: {
href: string
children: ReactNode
className?: string
}) => React.createElement('a', { href, className }, children),
}))

describe('not found page', () => {
beforeEach(() => {
routerBack.mockClear()
})

it('renders the 404 message, search input, and recovery links', () => {
render(React.createElement(NotFound))

expect(screen.getByText('404')).toBeInTheDocument()
expect(screen.getByRole('heading', { level: 1, name: 'Page Not Found' })).toBeInTheDocument()
expect(screen.getByPlaceholderText('Search the site...')).toBeInTheDocument()
expect(screen.getByRole('link', { name: 'Go Home' })).toHaveAttribute('href', '/')
})

it('routes back from the secondary recovery button', () => {
render(React.createElement(NotFound))

fireEvent.click(screen.getByRole('button', { name: 'Go Back' }))

expect(routerBack).toHaveBeenCalledTimes(1)
})

it('accepts search text in the site search box', () => {
render(React.createElement(NotFound))

const search = screen.getByPlaceholderText('Search the site...')
fireEvent.change(search, { target: { value: 'escrow status' } })

expect(search).toHaveValue('escrow status')
})
})