-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(not-found)!: implement 404 page with dynamic link behavior (#20)
* feat(not-found): implement 404 page with dynamic link behavior - Create not-found.tsx to display 404 errors. - Disable SSR in not-found.tsx for better UI experience. - Add dynamic Link atom to not-found.tsx for navigation. - Add testing suite for not-found.tsx * feat(not-found)!: improve navigation link and styling on 404 page - Enhance 404 page navigation by dynamically setting `backLink` based on document referrer. - Update heading styles, setting "404" as an `h1` and the descriptive text as an `h2'. - Add `ariaLabel` for improved accessibility on the navigation link. - Refactor the code to use `useEffect` to manage `backLink`, `linkLabel`, and `ariaLabel` setup. - Update tests to mock `useRouter` and validate new navigation behaviors. * fix(not-found): correct link label text to 'Go to Login'
- Loading branch information
Showing
2 changed files
with
204 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
'use client'; | ||
import { Container, Divider } from '@mui/material'; | ||
import React, { useEffect, useState } from 'react'; | ||
import Link from '../../src/components/atoms/Link'; | ||
import Text from '../../src/components/atoms/Text'; | ||
import dynamic from 'next/dynamic'; | ||
import { useRouter } from 'next/navigation'; | ||
|
||
export const NotFoundContent = () => { | ||
const router = useRouter(); | ||
|
||
const [backLink, setBackLink] = useState('/'); | ||
const [linkLabel, setLinkLabel] = useState('Go to Login'); | ||
const [ariaLabel, setAriaLabel] = useState('Go to Login'); | ||
|
||
useEffect(() => { | ||
if (typeof window !== 'undefined' && document.referrer) { | ||
const referrerOrigin = new URL(document.referrer).origin; | ||
const currentOrigin = window.location.origin; | ||
|
||
const initialBackLink = | ||
referrerOrigin === currentOrigin ? document.referrer : '/'; | ||
const initialLinkLabel = | ||
initialBackLink === '/' ? 'Go to Login' : 'Return to Previous Page'; | ||
|
||
setBackLink(initialBackLink); | ||
setLinkLabel(initialLinkLabel); | ||
|
||
const initialAriaLabel = | ||
initialBackLink === '/' ? 'Go to Login' : 'Return to Previous Page'; | ||
setAriaLabel(initialAriaLabel); | ||
} | ||
}, []); | ||
|
||
const handleBack = (event: React.MouseEvent) => { | ||
event.preventDefault(); | ||
if (backLink === document.referrer) { | ||
router.back(); | ||
} else { | ||
router.push(backLink); | ||
} | ||
}; | ||
|
||
return ( | ||
<Container | ||
maxWidth="md" | ||
style={{ | ||
padding: '2rem', | ||
minHeight: '100vh', | ||
display: 'flex', | ||
flexDirection: 'column', | ||
alignItems: 'center', | ||
justifyContent: 'center', | ||
textAlign: 'center', | ||
}} | ||
> | ||
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}> | ||
<Text variant="h1">404</Text> | ||
<Divider flexItem orientation="vertical" style={{ height: '40px' }} /> | ||
<Text variant="h2">This page could not be found.</Text> | ||
</div> | ||
<Link | ||
aria-label={ariaLabel} | ||
href={backLink} | ||
onClick={handleBack} | ||
sx={{ | ||
marginTop: '1rem', | ||
color: 'primary.main', | ||
'&:hover': { | ||
color: 'primary.dark', | ||
}, | ||
}} | ||
> | ||
{linkLabel} | ||
</Link> | ||
</Container> | ||
); | ||
}; | ||
|
||
const NotFound = dynamic(() => Promise.resolve(NotFoundContent), { | ||
ssr: false, | ||
}); | ||
|
||
export default NotFound; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
import '@testing-library/jest-dom'; | ||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'; | ||
import { NotFoundContent } from '../../src/app/not-found'; | ||
import React from 'react'; | ||
import { useRouter } from 'next/navigation'; | ||
|
||
jest.mock('next/navigation', () => ({ | ||
useRouter: jest.fn(), | ||
})); | ||
|
||
describe('NotFoundContent Component', () => { | ||
const mockBack = jest.fn(); | ||
const mockPush = jest.fn(); | ||
|
||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
(useRouter as jest.Mock).mockReturnValue({ | ||
back: mockBack, | ||
push: mockPush, | ||
}); | ||
}); | ||
|
||
afterEach(() => { | ||
jest.resetAllMocks(); | ||
}); | ||
|
||
it('should render the 404 message', async () => { | ||
render(<NotFoundContent />); | ||
|
||
await waitFor(() => { | ||
expect(screen.getByText('404')).toBeInTheDocument(); | ||
expect( | ||
screen.getByText('This page could not be found.') | ||
).toBeInTheDocument(); | ||
}); | ||
}); | ||
|
||
it('should render "Go to Login" when there is no referrer', async () => { | ||
Object.defineProperty(window, 'location', { | ||
value: { origin: 'http://localhost' }, | ||
writable: true, | ||
}); | ||
Object.defineProperty(document, 'referrer', { value: '', writable: true }); | ||
|
||
render(<NotFoundContent />); | ||
|
||
expect(await screen.findByText('Go to Login')).toBeInTheDocument(); | ||
}); | ||
|
||
it('should render "Return to Previous Page" when referrer is from the same origin', async () => { | ||
Object.defineProperty(window, 'location', { | ||
value: { origin: 'http://localhost' }, | ||
writable: true, | ||
}); | ||
Object.defineProperty(document, 'referrer', { | ||
value: 'http://localhost/previous-page', | ||
writable: true, | ||
}); | ||
|
||
render(<NotFoundContent />); | ||
|
||
expect( | ||
await screen.findByText('Return to Previous Page') | ||
).toBeInTheDocument(); | ||
}); | ||
|
||
it('should render "Go to Login" when referrer is from a different origin', async () => { | ||
Object.defineProperty(window, 'location', { | ||
value: { origin: 'http://localhost' }, | ||
writable: true, | ||
}); | ||
Object.defineProperty(document, 'referrer', { | ||
value: 'https://external.com', | ||
writable: true, | ||
}); | ||
|
||
render(<NotFoundContent />); | ||
|
||
expect(await screen.findByText('Go to Login')).toBeInTheDocument(); | ||
}); | ||
|
||
it('clicking the link calls router.back() when backLink matches document.referrer', async () => { | ||
Object.defineProperty(window, 'location', { | ||
value: { origin: 'http://localhost' }, | ||
writable: true, | ||
}); | ||
const referrer = 'http://localhost/previous-page'; | ||
Object.defineProperty(document, 'referrer', { | ||
value: referrer, | ||
writable: true, | ||
}); | ||
|
||
render(<NotFoundContent />); | ||
|
||
const linkElement = await screen.findByText('Return to Previous Page'); | ||
fireEvent.click(linkElement); | ||
|
||
expect(mockBack).toHaveBeenCalled(); | ||
expect(mockPush).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it('clicking the link calls router.push("/") when backLink does not match document.referrer', async () => { | ||
Object.defineProperty(window, 'location', { | ||
value: { origin: 'http://localhost' }, | ||
writable: true, | ||
}); | ||
Object.defineProperty(document, 'referrer', { | ||
value: '', | ||
writable: true, | ||
}); | ||
|
||
render(<NotFoundContent />); | ||
|
||
const linkElement = await screen.findByText('Go to Login'); | ||
fireEvent.click(linkElement); | ||
|
||
expect(mockPush).toHaveBeenCalledWith('/'); | ||
expect(mockBack).not.toHaveBeenCalled(); | ||
}); | ||
}); |