diff --git a/src/apps/work/src/lib/components/ErrorMessage/ErrorMessage.module.scss b/src/apps/work/src/lib/components/ErrorMessage/ErrorMessage.module.scss index 465ddd847..0012e5a2a 100644 --- a/src/apps/work/src/lib/components/ErrorMessage/ErrorMessage.module.scss +++ b/src/apps/work/src/lib/components/ErrorMessage/ErrorMessage.module.scss @@ -16,4 +16,9 @@ .message { font-size: 14px; margin: 0; + + a { + color: inherit; + text-decoration: underline; + } } diff --git a/src/apps/work/src/lib/components/ErrorMessage/ErrorMessage.spec.tsx b/src/apps/work/src/lib/components/ErrorMessage/ErrorMessage.spec.tsx new file mode 100644 index 000000000..4ffff7ea9 --- /dev/null +++ b/src/apps/work/src/lib/components/ErrorMessage/ErrorMessage.spec.tsx @@ -0,0 +1,30 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import { + render, + screen, +} from '@testing-library/react' + +import { ErrorMessage } from './ErrorMessage' + +jest.mock('~/libs/ui', () => ({ + Button: (props: { label: string }) => ( + {props.label} + ), +}), { + virtual: true, +}) + +describe('ErrorMessage', () => { + it('renders the Topcoder support email as a mailto link', () => { + const message = 'You don’t have access to this project. Please contact support@topcoder.com.' + + render() + + const supportLink = screen.getByRole('link', { name: 'support@topcoder.com' }) + + expect(supportLink.getAttribute('href')) + .toBe('mailto:support@topcoder.com') + expect(supportLink.closest('p')?.textContent) + .toBe(message) + }) +}) diff --git a/src/apps/work/src/lib/components/ErrorMessage/ErrorMessage.tsx b/src/apps/work/src/lib/components/ErrorMessage/ErrorMessage.tsx index ce554e97f..7453ff98e 100644 --- a/src/apps/work/src/lib/components/ErrorMessage/ErrorMessage.tsx +++ b/src/apps/work/src/lib/components/ErrorMessage/ErrorMessage.tsx @@ -1,17 +1,48 @@ -import { FC } from 'react' +import { + FC, + ReactNode, +} from 'react' import { Button } from '~/libs/ui' import styles from './ErrorMessage.module.scss' interface ErrorMessageProps { - message: string + message: ReactNode onRetry?: () => void } +const SUPPORT_EMAIL = 'support@topcoder.com' + +/** + * Renders the supplied error message with the Topcoder support email converted to a mailto link. + * + * @param message error message text or custom React content to display. + * @returns message content with the support email linked when the message is plain text. + * @remarks Used by ErrorMessage so project access denial messages can keep their configured copy while linking support. + * @throws Does not throw. + */ +function renderMessage(message: ReactNode): ReactNode { + if (typeof message !== 'string' || !message.includes(SUPPORT_EMAIL)) { + return message + } + + const emailIndex = message.indexOf(SUPPORT_EMAIL) + + return ( + <> + {message.slice(0, emailIndex)} + + {SUPPORT_EMAIL} + + {message.slice(emailIndex + SUPPORT_EMAIL.length)} + > + ) +} + export const ErrorMessage: FC = (props: ErrorMessageProps) => ( - {props.message} + {renderMessage(props.message)} {props.onRetry ? ( { expect(screen.getByRole('heading', { level: 1, name: 'Users' })) .toBeTruthy() - expect(screen.getByText(PROJECT_ACCESS_DENIED_MESSAGE)) - .toBeTruthy() + const supportLink = screen.getByRole('link', { name: 'support@topcoder.com' }) + + expect(supportLink.getAttribute('href')) + .toBe('mailto:support@topcoder.com') + expect(supportLink.closest('p')?.textContent) + .toBe(PROJECT_ACCESS_DENIED_MESSAGE) expect(screen.queryByText('Protected Project Users')) .toBeNull() }) @@ -211,8 +215,12 @@ describe('ProjectRouteAccessGuard', () => { expect(mockedCheckProjectAccess) .toHaveBeenCalledWith(defaultContextValue.userRoles, 12345, undefined) - expect(screen.getByText(PROJECT_ACCESS_DENIED_MESSAGE)) - .toBeTruthy() + const supportLink = screen.getByRole('link', { name: 'support@topcoder.com' }) + + expect(supportLink.getAttribute('href')) + .toBe('mailto:support@topcoder.com') + expect(supportLink.closest('p')?.textContent) + .toBe(PROJECT_ACCESS_DENIED_MESSAGE) expect(screen.queryByText('Protected Project Users')) .toBeNull() })
{props.message}
{renderMessage(props.message)}