diff --git a/index.ts b/index.ts index 39afa6b9..9e9c2b76 100644 --- a/index.ts +++ b/index.ts @@ -98,6 +98,8 @@ export { subscribe, unsubscribe, useAppEvent, + useAuthenticatedUser, + useConfig, useIntl } from './runtime'; diff --git a/runtime/index.ts b/runtime/index.ts index e26ce7f9..cc80d80b 100644 --- a/runtime/index.ts +++ b/runtime/index.ts @@ -116,7 +116,9 @@ export { ErrorPage, LoginRedirect, PageWrap, - useAppEvent + useAppEvent, + useAuthenticatedUser, + useConfig } from './react'; export { diff --git a/runtime/react/AuthenticatedPageRoute.jsx b/runtime/react/AuthenticatedPageRoute.jsx index 99ebc721..2e5afec2 100644 --- a/runtime/react/AuthenticatedPageRoute.jsx +++ b/runtime/react/AuthenticatedPageRoute.jsx @@ -1,9 +1,8 @@ import PropTypes from 'prop-types'; -import { useContext } from 'react'; import { getLoginRedirectUrl } from '../auth'; -import AppContext from './AppContext'; import PageWrap from './PageWrap'; +import { useAuthenticatedUser } from './hooks'; /** * A react-router route that redirects to the login page when the route becomes active and the user @@ -23,7 +22,7 @@ import PageWrap from './PageWrap'; * viewing the route's contents. */ export default function AuthenticatedPageRoute({ redirectUrl, children }) { - const { authenticatedUser } = useContext(AppContext); + const authenticatedUser = useAuthenticatedUser(); if (authenticatedUser === null) { const destination = redirectUrl || getLoginRedirectUrl(global.location.href); global.location.assign(destination); diff --git a/runtime/react/hooks.js b/runtime/react/hooks.js index a3b10cb9..74aca9e8 100644 --- a/runtime/react/hooks.js +++ b/runtime/react/hooks.js @@ -1,6 +1,7 @@ /* eslint-disable import/prefer-default-export */ -import { useEffect } from 'react'; +import { useContext, useEffect } from 'react'; import { sendTrackEvent } from '../analytics'; +import AppContext from './AppContext'; /** * A React hook that allows functional components to subscribe to application events. This should @@ -47,3 +48,13 @@ export const useTrackColorSchemeChoice = () => { }; }, []); }; + +export function useAuthenticatedUser() { + const { authenticatedUser } = useContext(AppContext); + return authenticatedUser; +} + +export function useConfig() { + const { config } = useContext(AppContext); + return config; +} diff --git a/runtime/react/hooks.test.jsx b/runtime/react/hooks.test.jsx index 8cae0ab3..b42359e4 100644 --- a/runtime/react/hooks.test.jsx +++ b/runtime/react/hooks.test.jsx @@ -1,6 +1,10 @@ import { renderHook } from '@testing-library/react-hooks'; -import { useTrackColorSchemeChoice } from './hooks'; +import { EnvironmentTypes } from '../../types'; import { sendTrackEvent } from '../analytics'; +import { setAuthenticatedUser } from '../auth'; +import { initializeMockApp } from '../testing'; +import AppProvider from './AppProvider'; +import { useAuthenticatedUser, useConfig, useTrackColorSchemeChoice } from './hooks'; jest.mock('../analytics'); @@ -52,3 +56,46 @@ describe('useTrackColorSchemeChoice hook', () => { expect(mockAddEventListener).toHaveBeenCalledWith('change', expect.any(Function)); }); }); + +describe('useAuthenticatedUser', () => { + it('returns null when the user is anonymous', () => { + const { result } = renderHook(() => useAuthenticatedUser()); + expect(result.current).toBeNull(); + }); + + describe('with a user', () => { + const user = { + administrator: true, + email: 'admin@example.com', + name: 'Admin', + roles: ['admin'], + userId: 1, + username: 'admin-user', + avatar: 'http://localhost/admin.png', + }; + + beforeEach(() => { + initializeMockApp({ + authenticatedUser: user, + }); + }); + + afterEach(() => { + setAuthenticatedUser(null); + }); + + it('returns a User when the user exists', () => { + const { result } = renderHook(() => useAuthenticatedUser(), { wrapper: AppProvider }); + expect(result.current).toBe(user); + }); + }); +}); + +describe('useConfig', () => { + it('returns the site config', () => { + const { result } = renderHook(() => useConfig()); + expect(result.current).toHaveProperty('apps', []); + expect(result.current).toHaveProperty('ENVIRONMENT', EnvironmentTypes.TEST); + expect(result.current).toHaveProperty('BASE_URL', 'http://localhost:8080'); + }); +}); diff --git a/runtime/react/index.js b/runtime/react/index.js index 52f7d7c5..045dcdd0 100644 --- a/runtime/react/index.js +++ b/runtime/react/index.js @@ -11,6 +11,6 @@ export { default as AppProvider } from './AppProvider'; export { default as AuthenticatedPageRoute } from './AuthenticatedPageRoute'; export { default as ErrorBoundary } from './ErrorBoundary'; export { default as ErrorPage } from './ErrorPage'; -export { useAppEvent } from './hooks'; +export { useAppEvent, useAuthenticatedUser, useConfig } from './hooks'; export { default as LoginRedirect } from './LoginRedirect'; export { default as PageWrap } from './PageWrap'; diff --git a/shell/header/learning-header/LearningHeader.tsx b/shell/header/learning-header/LearningHeader.tsx index 371177c4..8a959f4a 100644 --- a/shell/header/learning-header/LearningHeader.tsx +++ b/shell/header/learning-header/LearningHeader.tsx @@ -1,7 +1,6 @@ -import { useContext } from 'react'; import { - AppContext, getConfig, + useAuthenticatedUser, useIntl } from '../../../runtime'; @@ -21,7 +20,7 @@ export default function LearningHeader({ courseOrg, courseNumber, courseTitle, showUserDropdown = true, }: LearningHeaderProps) { const intl = useIntl(); - const { authenticatedUser } = useContext(AppContext); + const authenticatedUser = useAuthenticatedUser(); return (
@@ -38,12 +37,12 @@ export default function LearningHeader({ {courseTitle} {showUserDropdown && authenticatedUser && ( - + )} {showUserDropdown && !authenticatedUser && ( - + )}
diff --git a/shell/header/studio-header/StudioHeader.tsx b/shell/header/studio-header/StudioHeader.tsx index 6e29eebc..30c4b930 100644 --- a/shell/header/studio-header/StudioHeader.tsx +++ b/shell/header/studio-header/StudioHeader.tsx @@ -1,6 +1,5 @@ -import { useContext } from 'react'; import Responsive from 'react-responsive'; -import { AppContext } from '../../../runtime'; +import { useAuthenticatedUser, useConfig } from '../../../runtime'; import HeaderBody from './HeaderBody'; import MobileHeader from './MobileHeader'; @@ -31,7 +30,8 @@ export default function StudioHeader({ outlineLink, searchButtonAction, }: StudioHeaderProps) { - const { authenticatedUser, config } = useContext(AppContext); + const authenticatedUser = useAuthenticatedUser(); + const config = useConfig(); const props = { logo: config.LOGO_URL, logoAltText: `Studio ${config.SITE_NAME}`, diff --git a/test-project/src/authenticated-page/AuthenticatedPage.tsx b/test-project/src/authenticated-page/AuthenticatedPage.tsx index 1f9cb61c..cfb45aea 100644 --- a/test-project/src/authenticated-page/AuthenticatedPage.tsx +++ b/test-project/src/authenticated-page/AuthenticatedPage.tsx @@ -1,10 +1,10 @@ -import { useContext } from 'react'; import { Link } from 'react-router-dom'; -import { AppContext } from '@openedx/frontend-base'; +import { useAuthenticatedUser, useConfig } from '@openedx/frontend-base'; export default function AuthenticatedPage() { - const { authenticatedUser, config } = useContext(AppContext); + const authenticatedUser = useAuthenticatedUser(); + const config = useConfig(); return (
diff --git a/test-project/src/example-page/ExamplePage.tsx b/test-project/src/example-page/ExamplePage.tsx index 37649e39..1d1f49bb 100644 --- a/test-project/src/example-page/ExamplePage.tsx +++ b/test-project/src/example-page/ExamplePage.tsx @@ -1,15 +1,15 @@ import { - AppContext, getAuthenticatedUser, getConfig, logInfo, mergeConfig, + useAuthenticatedUser, + useConfig, useIntl } from '@openedx/frontend-base'; import { Container } from '@openedx/paragon'; -import { useContext, useEffect } from 'react'; +import { useEffect } from 'react'; import { Link } from 'react-router-dom'; - import messages from '../messages'; import Image from './Image'; import ParagonPreview from './ParagonPreview'; @@ -27,7 +27,8 @@ mergeConfig({ }); export default function ExamplePage() { - const context = useContext(AppContext); + const config = useConfig(); + const authenticatedUser = useAuthenticatedUser(); const intl = useIntl(); @@ -37,7 +38,7 @@ export default function ExamplePage() { return ( -

{context.config.SITE_NAME} test page

+

{config.SITE_NAME} test page

Links

Visit authenticated page.

@@ -45,8 +46,8 @@ export default function ExamplePage() {

Visit error page.

Context Config Test

-

Is context.config equal to getConfig()? {printTestResult(context.config === getConfig())}

-

Is context.authenticatedUser equal to getAuthenticatedUser()? {printTestResult(context.authenticatedUser === getAuthenticatedUser())}

+

Is context.config equal to getConfig()? {printTestResult(config === getConfig())}

+

Is context.authenticatedUser equal to getAuthenticatedUser()? {printTestResult(authenticatedUser === getAuthenticatedUser())}

SCSS parsing tests

"The Apples" should be red (color: red; via .red-text CSS class in SCSS stylesheet)

@@ -65,17 +66,17 @@ export default function ExamplePage() {

{intl.formatMessage(messages['test-project.message'])}

Authenticated User Test

- {context.authenticatedUser !== null ? ( + {authenticatedUser !== null ? (
-

Authenticated Username: {context.authenticatedUser.username}

+

Authenticated Username: {authenticatedUser.username}

Authenticated user's name: - {context.authenticatedUser.username} + {authenticatedUser.username} (Only available if user account has been fetched)

) : ( -

Unauthenticated {printTestResult(context.authenticatedUser === null)}

+

Unauthenticated {printTestResult(authenticatedUser === null)}

)}

Config tests