Skip to content

Commit 32dea34

Browse files
committed
feat: added 404 and 500 error pages
1 parent eadeb9d commit 32dea34

File tree

8 files changed

+135
-3
lines changed

8 files changed

+135
-3
lines changed

playwright.config.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export default defineConfig({
88
workers: process.env.CI ? 1 : undefined,
99
reporter: 'html',
1010
use: {
11-
baseURL: 'http://localhost:3000',
11+
baseURL: 'http://localhost:3100',
1212
trace: 'on-first-retry',
1313
},
1414

@@ -20,8 +20,8 @@ export default defineConfig({
2020
],
2121

2222
webServer: {
23-
command: 'npm run dev',
24-
url: 'http://localhost:3000',
23+
command: 'PORT=3100 npm run dev',
24+
url: 'http://localhost:3100',
2525
reuseExistingServer: !process.env.CI,
2626
timeout: 120000,
2727
env: {

src/app/e2e-error-api/route.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export const dynamic = 'force-static'
2+
3+
export async function GET() {
4+
if (process.env.PLAYWRIGHT_TEST === '1') {
5+
return new Response('E2E Test Error', { status: 500 })
6+
}
7+
return new Response('OK', { status: 200 })
8+
}

src/app/e2e-error/page.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export default function E2EErrorPage() {
2+
return (
3+
<div className="container py-4">
4+
<h1>E2E Error Test Helper</h1>
5+
<p>This page is used during development. The E2E tests use the API route /e2e-error-api to trigger HTTP 500.</p>
6+
</div>
7+
)
8+
}
9+

src/app/error.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
'use client'
2+
3+
import ErrorPage from '@/components/ErrorPage'
4+
5+
export default function Error({
6+
error,
7+
reset,
8+
}: {
9+
error: Error & { digest?: string }
10+
reset: () => void
11+
}) {
12+
return <ErrorPage error={error} reset={reset} />
13+
}

src/app/not-found.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import PageLayout from '@/components/PageLayout'
2+
3+
export default function NotFound() {
4+
return (
5+
<PageLayout title="Page not found" description="Sorry, we couldn't find the page you're looking for.">
6+
<div className="row">
7+
<div className="col-12 col-md-8">
8+
<p>The page may have been moved or deleted.</p>
9+
<a className="btn btn-primary" href="/">Back to homepage</a>
10+
</div>
11+
</div>
12+
</PageLayout>
13+
)
14+
}
15+

src/components/ErrorPage.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
'use client'
2+
3+
export default function ErrorPage({
4+
error,
5+
reset,
6+
}: {
7+
error: Error & { digest?: string }
8+
reset: () => void
9+
}) {
10+
const showDetails = process.env.NODE_ENV !== 'production'
11+
return (
12+
<div className="container py-4">
13+
<div className="row">
14+
<div className="col-12 col-md-8">
15+
<h1>Something went wrong</h1>
16+
<p>An unexpected error occurred. We're sorry for the inconvenience. Please try again or go back to the homepage.</p>
17+
{showDetails && (
18+
<div className="alert alert-warning" role="alert">
19+
<strong>Details:</strong> {error.message}
20+
</div>
21+
)}
22+
<div className="d-flex gap-2">
23+
<button className="btn btn-primary" onClick={() => reset()}>Try again</button>
24+
<a className="btn btn-outline-secondary" href="/">Back to homepage</a>
25+
</div>
26+
</div>
27+
</div>
28+
</div>
29+
)
30+
}

src/test/error-pages.test.tsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { describe, it, expect, vi } from 'vitest'
2+
import { render, screen, fireEvent } from '@testing-library/react'
3+
import NotFound from '@/app/not-found'
4+
import ErrorPage from '@/components/ErrorPage'
5+
6+
// Mock PageLayout to avoid layout dependencies in unit tests
7+
vi.mock('@/components/PageLayout', () => ({
8+
default: ({ title, description, children }: any) => (
9+
<div>
10+
{title && <h1>{title}</h1>}
11+
{description && <p>{description}</p>}
12+
<div>{children}</div>
13+
</div>
14+
)
15+
}))
16+
17+
describe('Custom NotFound page', () => {
18+
it('renders title, description and home link', () => {
19+
render(<NotFound />)
20+
expect(screen.getByRole('heading', { name: 'Page not found' })).toBeInTheDocument()
21+
expect(screen.getByText(/couldn't find the page/i)).toBeInTheDocument()
22+
const back = screen.getByRole('link', { name: /back to homepage/i })
23+
expect(back).toHaveAttribute('href', '/')
24+
})
25+
})
26+
27+
describe('Custom Error page', () => {
28+
it('renders message and allows retry via reset()', () => {
29+
const reset = vi.fn()
30+
const error = new Error('Boom!')
31+
render(<ErrorPage error={error} reset={reset} />)
32+
expect(screen.getByRole('heading', { name: 'Something went wrong' })).toBeInTheDocument()
33+
// In test env NODE_ENV is usually 'test', details should be visible
34+
expect(screen.getByText(/details:/i)).toBeInTheDocument()
35+
expect(screen.getByText(/boom!/i)).toBeInTheDocument()
36+
const btn = screen.getByRole('button', { name: /try again/i })
37+
fireEvent.click(btn)
38+
expect(reset).toHaveBeenCalled()
39+
})
40+
})

tests/e2e/error-pages.spec.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { test, expect } from '@playwright/test'
2+
3+
test.describe('Error pages', () => {
4+
test('renders custom 404 page for non-existing route', async ({ page }) => {
5+
const response = await page.goto('/this-page-does-not-exist')
6+
// Next.js may serve 404 with 404 status in dev
7+
expect(response?.status()).toBe(404)
8+
await expect(page.getByRole('heading', { name: 'Page not found' })).toBeVisible()
9+
await expect(page.getByRole('link', { name: /back to homepage/i })).toHaveAttribute('href', '/')
10+
})
11+
12+
test('returns 500 when an error is thrown (UI may be Next dev overlay)', async ({ page }) => {
13+
const response = await page.goto('/e2e-error-api')
14+
// In a dev server, Next should return 500 on the error boundary
15+
expect(response?.status()).toBe(500)
16+
})
17+
})

0 commit comments

Comments
 (0)