-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #15 from rebeccaalpert/error-states
Add error states and some tests
- Loading branch information
Showing
15 changed files
with
524 additions
and
40 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
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,6 @@ | ||
global.Response = class { | ||
constructor(public body) {} | ||
json() { | ||
return Promise.resolve(this.body); | ||
} | ||
} as unknown as typeof Response; |
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,100 @@ | ||
import React from 'react'; | ||
import { render, screen } from '@testing-library/react'; | ||
import '@testing-library/jest-dom'; | ||
import { ErrorBoundary } from './ErrorBoundary'; | ||
import { isRouteErrorResponse, useRouteError } from 'react-router-dom'; | ||
|
||
jest.mock('react-router-dom', () => ({ | ||
...jest.requireActual('react-router-dom'), | ||
useRouteError: jest.fn(), | ||
useNavigate: () => jest.fn, | ||
isRouteErrorResponse: jest.fn(), | ||
})); | ||
|
||
const mockedUseRouteError = useRouteError as jest.Mock; | ||
const mockedIsRouteErrorResponse = isRouteErrorResponse as unknown as jest.Mock; | ||
const MOCK_401 = { status: 401 }; | ||
const MOCK_403 = { status: 403 }; | ||
const MOCK_404 = { status: 404 }; | ||
const MOCK_500 = { status: 500 }; | ||
const MOCK_503 = { status: 503 }; | ||
|
||
const checkHome = () => { | ||
const home = screen.getByRole('link', { name: /Go back home/i }); | ||
expect(home).toBeTruthy(); | ||
expect(home).toHaveAttribute('href', '/'); | ||
}; | ||
|
||
const checkRetry = () => { | ||
const retry = screen.getByRole('button', { name: /Retry/i }); | ||
expect(retry).toBeTruthy(); | ||
}; | ||
|
||
describe('Error boundary', () => { | ||
afterEach(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
it('should render 401 correctly', () => { | ||
mockedIsRouteErrorResponse.mockReturnValueOnce(true); | ||
mockedUseRouteError.mockReturnValueOnce(MOCK_401); | ||
|
||
render(<ErrorBoundary />); | ||
expect( | ||
screen.getByText( | ||
'Accelerate innovation with a unified platform that transforms ideas into impactful AI solutions, from concept to deployment. Seamless integration, top-tier security, and full control over your data for fast, efficient AI development.', | ||
), | ||
).toBeTruthy(); | ||
expect(screen.getByText('Sign in')).toBeTruthy(); | ||
}); | ||
it('should render 403 correctly', () => { | ||
mockedUseRouteError.mockReturnValueOnce(MOCK_403); | ||
mockedIsRouteErrorResponse.mockReturnValueOnce(true); | ||
render(<ErrorBoundary />); | ||
expect(screen.getByText('Error Code 403')).toBeTruthy(); | ||
expect(screen.getByText('Access denied')).toBeTruthy(); | ||
expect( | ||
screen.getByText( | ||
"You don't have permission to access this page. Please check your credentials or contact an administrator for assistance.", | ||
), | ||
).toBeTruthy(); | ||
checkHome(); | ||
}); | ||
it('should render 404 correctly', () => { | ||
mockedUseRouteError.mockReturnValue(MOCK_404); | ||
mockedIsRouteErrorResponse.mockReturnValueOnce(true); | ||
render(<ErrorBoundary />); | ||
expect(screen.getByText('Error Code 404')).toBeTruthy(); | ||
expect(screen.getByText('Page not found')).toBeTruthy(); | ||
expect( | ||
screen.getByText("The page you're looking for doesn't exit or may have been moved. Let's get you back on track!"), | ||
).toBeTruthy(); | ||
checkHome(); | ||
}); | ||
it('should render 500 correctly', () => { | ||
mockedUseRouteError.mockReturnValue(MOCK_500); | ||
mockedIsRouteErrorResponse.mockReturnValueOnce(false); | ||
render(<ErrorBoundary />); | ||
expect(screen.getByText('Error Code 500')).toBeTruthy(); | ||
expect(screen.getByText('Something went wrong')).toBeTruthy(); | ||
expect( | ||
screen.getByText( | ||
"We're experiencing a server issue. Please try again later or contact support if the problem persists.", | ||
), | ||
).toBeTruthy(); | ||
checkHome(); | ||
}); | ||
it('should render 503 correctly', () => { | ||
mockedUseRouteError.mockReturnValue(MOCK_503); | ||
mockedIsRouteErrorResponse.mockReturnValueOnce(true); | ||
render(<ErrorBoundary />); | ||
expect(screen.getByText('Error Code 503')).toBeTruthy(); | ||
expect(screen.getByText('Service unavailable')).toBeTruthy(); | ||
expect( | ||
screen.getByText( | ||
'Our servers are currently down for maintenance or experiencing high demand. Please check back later.', | ||
), | ||
).toBeTruthy(); | ||
checkHome(); | ||
checkRetry(); | ||
}); | ||
}); |
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,67 @@ | ||
import * as React from 'react'; | ||
import { isRouteErrorResponse, useRouteError } from 'react-router-dom'; | ||
import { ErrorLayout } from './ErrorLayout'; | ||
import { LoginPage } from '@app/LoginPage/LoginPage'; | ||
|
||
const ErrorBoundary: React.FunctionComponent = () => { | ||
const error = useRouteError(); | ||
|
||
if (isRouteErrorResponse(error)) { | ||
if (error.status === 401) { | ||
return <LoginPage />; | ||
} | ||
|
||
if (error.status === 403) { | ||
return ( | ||
<ErrorLayout | ||
errorCode="403" | ||
title="Access denied" | ||
body="You don't have permission to access this page. Please check your credentials or contact an administrator for assistance." | ||
/> | ||
); | ||
} | ||
|
||
if (error.status === 404) { | ||
return ( | ||
<ErrorLayout | ||
errorCode="404" | ||
title="Page not found" | ||
body="The page you're looking for doesn't exit or may have been moved. Let's get you back on track!" | ||
/> | ||
); | ||
} | ||
|
||
if (error.status === 503) { | ||
return ( | ||
<ErrorLayout | ||
hasRetry | ||
errorCode="503" | ||
title="Service unavailable" | ||
body="Our servers are currently down for maintenance or experiencing high demand. Please check back later." | ||
/> | ||
); | ||
} | ||
} | ||
|
||
if (typeof error === 'object' && error !== null && 'data' in error) { | ||
if ( | ||
typeof error.data === 'object' && | ||
error.data !== null && | ||
'status' in error.data && | ||
error.data.status === 'Misconfigured' | ||
) { | ||
return <ErrorLayout title="App not configured" body="Please update your environment variables" hasHome={false} />; | ||
} | ||
} | ||
|
||
return ( | ||
<ErrorLayout | ||
hasRetry | ||
errorCode="500" | ||
title="Something went wrong" | ||
body="We're experiencing a server issue. Please try again later or contact support if the problem persists." | ||
/> | ||
); | ||
}; | ||
|
||
export { ErrorBoundary }; |
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,26 @@ | ||
import React from 'react'; | ||
import { render, screen } from '@testing-library/react'; | ||
import '@testing-library/jest-dom'; | ||
import { ErrorLayout } from './ErrorLayout'; | ||
|
||
jest.mock('react-router-dom', () => ({ | ||
...jest.requireActual('react-router-dom'), | ||
useNavigate: () => jest.fn, | ||
})); | ||
|
||
describe('Error layout', () => { | ||
it('should render correctly', () => { | ||
render(<ErrorLayout errorCode="500" body="Body" title="Title" />); | ||
expect(screen.getByText('Error Code 500')).toBeTruthy(); | ||
expect(screen.getByText('Title')).toBeTruthy(); | ||
expect(screen.getByText('Body')).toBeTruthy(); | ||
const button = screen.getByRole('link', { name: /Go back home/i }); | ||
expect(button).toBeTruthy(); | ||
expect(button).toHaveAttribute('href', '/'); | ||
}); | ||
it('should render retry button if hasRetry prop is passed in', () => { | ||
render(<ErrorLayout errorCode="500" body="Body" title="Title" hasRetry />); | ||
const button = screen.getByRole('button', { name: /Retry/i }); | ||
expect(button).toBeTruthy(); | ||
}); | ||
}); |
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,68 @@ | ||
import * as React from 'react'; | ||
import { Brand, Button } from '@patternfly/react-core'; | ||
import { useNavigate } from 'react-router-dom'; | ||
import logo from '@app/bgimages/Logo-Red_Hat-Composer_AI_Studio-A-Standard-RGB.svg'; | ||
import logoDark from '@app/bgimages/Logo-Red_Hat-Composer_AI_Studio-A-Reverse.svg'; | ||
|
||
interface ErrorLayoutProps { | ||
hasRetry?: boolean; | ||
title: string; | ||
body: string; | ||
errorCode?: string; | ||
hasHome?: boolean; | ||
} | ||
const ErrorLayout: React.FunctionComponent<ErrorLayoutProps> = ({ | ||
hasRetry, | ||
title, | ||
body, | ||
errorCode, | ||
hasHome = true, | ||
}) => { | ||
const supportLink = process.env.SUPPORT_LINK ?? ''; | ||
const navigate = useNavigate(); | ||
|
||
const handleReload = () => { | ||
navigate(0); | ||
}; | ||
|
||
return ( | ||
<div className="error-page"> | ||
<div className="error-page__main-layout"> | ||
<div className="error-page__image"> | ||
<div className="show-light"> | ||
<Brand src={logo} alt="Red Hat Composer AI Studio" heights={{ default: '36px' }} /> | ||
</div> | ||
<div className="show-dark"> | ||
<Brand src={logoDark} alt="Red Hat Composer AI Studio" heights={{ default: '36px' }} /> | ||
</div> | ||
</div> | ||
<div className="error-page__text-and-buttons"> | ||
<div className="error-page__text"> | ||
{errorCode && <span className="error-page__error-code">Error Code {errorCode}</span>} | ||
<h1>{title}</h1> | ||
<p>{body}</p> | ||
</div> | ||
<div className="error-page__buttons"> | ||
{hasRetry && ( | ||
<Button onClick={handleReload} size="sm"> | ||
Retry | ||
</Button> | ||
)} | ||
{hasHome && ( | ||
<Button component="a" href="/" size="sm" variant="secondary"> | ||
Go back home | ||
</Button> | ||
)} | ||
{supportLink && ( | ||
<Button component="a" href={supportLink} size="sm" variant="secondary"> | ||
Contact support | ||
</Button> | ||
)} | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
}; | ||
|
||
export { ErrorLayout }; |
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,59 @@ | ||
import React from 'react'; | ||
import { render, screen } from '@testing-library/react'; | ||
import '@testing-library/jest-dom'; | ||
import { HeaderDropdown } from './HeaderDropdown'; | ||
import userEvent from '@testing-library/user-event'; | ||
|
||
const MOCK_CHATBOTS = [ | ||
{ name: 'test1', displayName: 'Test1' }, | ||
{ name: 'test2', displayName: 'Test2' }, | ||
]; | ||
describe('Header dropdown', () => { | ||
it('should render correctly', async () => { | ||
render(<HeaderDropdown chatbots={MOCK_CHATBOTS} onSelect={jest.fn} />); | ||
const toggle = screen.getByRole('button', { name: /Red Hat AI Assistant/i }); | ||
await userEvent.click(toggle); | ||
expect(screen.getByRole('textbox', { name: /Search assistants.../i })).toBeTruthy(); | ||
expect(screen.getByRole('menuitem', { name: 'Test1' })).toBeTruthy(); | ||
expect(screen.getByRole('menuitem', { name: 'Test2' })).toBeTruthy(); | ||
}); | ||
it('should be able to select', async () => { | ||
const spy = jest.fn(); | ||
render(<HeaderDropdown chatbots={MOCK_CHATBOTS} onSelect={spy} />); | ||
const toggle = screen.getByRole('button', { name: /Red Hat AI Assistant/i }); | ||
await userEvent.click(toggle); | ||
await userEvent.click(screen.getByRole('menuitem', { name: 'Test1' })); | ||
expect(spy).toHaveBeenCalledTimes(1); | ||
}); | ||
it('should be able to search', async () => { | ||
render(<HeaderDropdown chatbots={MOCK_CHATBOTS} onSelect={jest.fn} />); | ||
const toggle = screen.getByRole('button', { name: /Red Hat AI Assistant/i }); | ||
await userEvent.click(toggle); | ||
const textbox = screen.getByRole('textbox', { name: /Search assistants.../i }); | ||
expect(textbox).toBeTruthy(); | ||
await userEvent.type(textbox, 'Test1'); | ||
expect(screen.getByRole('menuitem', { name: 'Test1' })).toBeTruthy(); | ||
expect(screen.queryByRole('menuitem', { name: 'Test2' })).toBeFalsy(); | ||
}); | ||
it('should be able to search and see no results', async () => { | ||
render(<HeaderDropdown chatbots={MOCK_CHATBOTS} onSelect={jest.fn} />); | ||
const toggle = screen.getByRole('button', { name: /Red Hat AI Assistant/i }); | ||
await userEvent.click(toggle); | ||
const textbox = screen.getByRole('textbox', { name: /Search assistants.../i }); | ||
expect(textbox).toBeTruthy(); | ||
await userEvent.type(textbox, 'ffff'); | ||
screen.getByRole('menuitem', { name: /No results found/i }); | ||
}); | ||
it('should be able to clear input field after search', async () => { | ||
render(<HeaderDropdown chatbots={MOCK_CHATBOTS} onSelect={jest.fn} />); | ||
const toggle = screen.getByRole('button', { name: /Red Hat AI Assistant/i }); | ||
await userEvent.click(toggle); | ||
const textbox = screen.getByRole('textbox', { name: /Search assistants.../i }); | ||
expect(textbox).toBeTruthy(); | ||
await userEvent.type(textbox, 'f'); | ||
screen.getByRole('menuitem', { name: /No results found/i }); | ||
await userEvent.clear(textbox); | ||
expect(screen.getByRole('menuitem', { name: 'Test1' })).toBeTruthy(); | ||
expect(screen.getByRole('menuitem', { name: 'Test2' })).toBeTruthy(); | ||
}); | ||
}); |
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,27 @@ | ||
import { Brand, Button } from '@patternfly/react-core'; | ||
import * as React from 'react'; | ||
import logo from '@app/bgimages/Logo-Red_Hat-Composer_AI_Studio-A-Standard-RGB.svg'; | ||
import logoDark from '@app/bgimages/Logo-Red_Hat-Composer_AI_Studio-A-Reverse.svg'; | ||
|
||
const LoginPage: React.FunctionComponent = () => { | ||
return ( | ||
<div className="login-page"> | ||
<div className="show-light"> | ||
<Brand src={logo} alt="Red Hat Composer AI Studio" heights={{ default: '36px' }} /> | ||
</div> | ||
<div className="show-dark"> | ||
<Brand src={logoDark} alt="Red Hat Composer AI Studio" heights={{ default: '36px' }} /> | ||
</div> | ||
<div className="login-page__text"> | ||
Accelerate innovation with a unified platform that transforms ideas into impactful AI solutions, from concept to | ||
deployment. Seamless integration, top-tier security, and full control over your data for fast, efficient AI | ||
development. | ||
</div> | ||
<div className="login-page__button"> | ||
<Button>Sign in</Button> | ||
</div> | ||
</div> | ||
); | ||
}; | ||
|
||
export { LoginPage }; |
Oops, something went wrong.