diff --git a/static/src/css/initial/index.scss b/static/src/css/initial/index.scss
index 2c5335828..eca978853 100644
--- a/static/src/css/initial/index.scss
+++ b/static/src/css/initial/index.scss
@@ -37,7 +37,9 @@ body {
position: absolute;
z-index: 9999;
display: flex;
+ overflow: hidden;
width: 100vw;
+ max-width: 100vw;
height: 100vh;
align-items: center;
justify-content: center;
diff --git a/static/src/css/vendor/bootstrap/_variables.scss b/static/src/css/vendor/bootstrap/_variables.scss
index 9a991d314..8ce13e573 100644
--- a/static/src/css/vendor/bootstrap/_variables.scss
+++ b/static/src/css/vendor/bootstrap/_variables.scss
@@ -22,6 +22,8 @@
* Included after `variables` and before `maps` as noted in the Bootstrap documentation
*/
+$grid-gutter-width: 2rem;
+
/*
* Color
*/
@@ -79,15 +81,18 @@ $link-hover-color: map.get($theme-colors, "blue");
* Badge
*/
-$btn-padding-y: 0.5rem;
-$btn-padding-x: 1rem;
-$btn-border-radius: 0.125rem;
+$badge-border-radius: 0.25rem;
/*
* Button
*/
-$badge-border-radius: 0.25rem;
+$btn-padding-y: 0.5rem;
+$btn-padding-x: 1rem;
+$btn-border-radius: 0.125rem;
+$btn-border-radius-lg: 0.2rem;
+$btn-padding-y-lg: 0.625rem;
+$btn-padding-x-lg: 1.25rem;
/*
* Accordion
@@ -112,6 +117,14 @@ $nav-link-hover-color: map.get($theme-colors, "blue");
$nav-pills-link-active-bg: map.get($theme-colors, "blue");
$nav-pills-border-radius: 0;
+/*
+ * Border Radius
+*/
+
+$border-radius: .25rem;
+$border-radius-sm: 0.125rem;
+$border-radius-lg: 0.2rem;
+
/*
* Bootstrap utilities
*/
diff --git a/static/src/css/vendor/bootstrap/overrides/_navbar.scss b/static/src/css/vendor/bootstrap/overrides/_navbar.scss
index cf4383c01..53be6e3c6 100644
--- a/static/src/css/vendor/bootstrap/overrides/_navbar.scss
+++ b/static/src/css/vendor/bootstrap/overrides/_navbar.scss
@@ -3,20 +3,27 @@
}
.navbar-brand {
+ font-size: 1rem;
font-weight: 100;
&.nasa {
- padding-left: 4.375rem;
+ padding-left: 2.5rem;
background: url("@/assets/images/logos/nasa-outline-new.svg") no-repeat;
background-position: left center;
- background-size: 3.75rem 3.75rem;
+ background-size: 2rem 2rem;
&:hover {
background: url("@/assets/images/logos/nasa-meatball-new.svg") no-repeat;
background-position: left center;
- background-size: 3.75rem 3.75rem;
+ background-size: 2rem 2rem;
cursor: pointer;
}
+
+ @include media-breakpoint-up(md) {
+ padding-left: 4.375rem;
+ background-size: 3.75rem 3.75rem;
+ font-size: var(--bs-navbar-brand-font-size);
+ }
}
}
diff --git a/static/src/js/App.jsx b/static/src/js/App.jsx
index d954195dd..e856d977c 100644
--- a/static/src/js/App.jsx
+++ b/static/src/js/App.jsx
@@ -23,6 +23,7 @@ import REDIRECTS from './constants/redirectsMap/redirectsMap'
import { getApplicationConfig } from './utils/getConfig'
import '../css/index.scss'
+import HomePage from './pages/HomePage/HomePage'
const redirectKeys = Object.keys(REDIRECTS)
@@ -97,6 +98,7 @@ const App = () => {
}>
{Redirects}
+ } />
} />
} />
} />
diff --git a/static/src/js/components/Button/Button.jsx b/static/src/js/components/Button/Button.jsx
index 9fa4926b6..7670da252 100644
--- a/static/src/js/components/Button/Button.jsx
+++ b/static/src/js/components/Button/Button.jsx
@@ -4,11 +4,14 @@ import BootstrapButton from 'react-bootstrap/Button'
import classNames from 'classnames'
import './Button.scss'
+import { FaExternalLinkAlt } from 'react-icons/fa'
/**
* @typedef {Object} ButtonProps
* @property {String} className Class name to apply to the button
* @property {ReactNode} children The children of the button
+ * @property {Boolean} external An optional boolean which sets `target="_blank"` and an external link icon
+ * @property {String} href An optional string which triggers the use of an `` tag with the designated href
* @property {Function} [Icon] An optional icon `react-icons` icon
* @property {Boolean} [naked] An optional boolean passed to render a button with no background or border
* @property {Function} onClick A callback function to be called when the button is clicked
@@ -38,40 +41,73 @@ import './Button.scss'
const Button = ({
className,
children,
+ external,
+ href,
Icon,
naked,
onClick,
size,
variant
-}) => (
-
- {
- Icon && (
-
- )
- }
+}) => {
+ // Create an object to pass any conditional properties. These are ultimately spread on the component.
+ const conditionalProps = {}
- {children}
-
-)
+ if (onClick) {
+ conditionalProps.onClick = onClick
+ }
+
+ if (href) {
+ conditionalProps.href = href
+
+ if (external) conditionalProps.target = '_blank'
+ }
+
+ return (
+
+ {
+ Icon && (
+
+ )
+ }
+ {children}
+ {
+ external && (
+
+ )
+ }
+
+ )
+}
Button.defaultProps = {
className: '',
Icon: null,
+ external: false,
+ href: null,
naked: false,
+ onClick: null,
size: '',
variant: ''
}
@@ -79,9 +115,11 @@ Button.defaultProps = {
Button.propTypes = {
className: PropTypes.string,
children: PropTypes.node.isRequired,
+ external: PropTypes.bool,
+ href: PropTypes.string,
Icon: PropTypes.func,
naked: PropTypes.bool,
- onClick: PropTypes.func.isRequired,
+ onClick: PropTypes.func,
size: PropTypes.string,
variant: PropTypes.string
}
diff --git a/static/src/js/components/Header/Header.jsx b/static/src/js/components/Header/Header.jsx
index 21e1baf16..3d511c8bf 100644
--- a/static/src/js/components/Header/Header.jsx
+++ b/static/src/js/components/Header/Header.jsx
@@ -1,16 +1,25 @@
import React from 'react'
import { Link } from 'react-router-dom'
import Badge from 'react-bootstrap/Badge'
-import Button from 'react-bootstrap/Button'
import Container from 'react-bootstrap/Container'
import Dropdown from 'react-bootstrap/Dropdown'
-import DropdownButton from 'react-bootstrap/DropdownButton'
import Form from 'react-bootstrap/Form'
import FormControl from 'react-bootstrap/FormControl'
import FormGroup from 'react-bootstrap/FormGroup'
import InputGroup from 'react-bootstrap/InputGroup'
+import ButtonGroup from 'react-bootstrap/ButtonGroup'
+import DropdownMenu from 'react-bootstrap/DropdownMenu'
import Navbar from 'react-bootstrap/Navbar'
-import { FaSearch } from 'react-icons/fa'
+import {
+ FaExternalLinkAlt,
+ FaQuestionCircle,
+ FaSearch,
+ FaSignInAlt,
+ FaSignOutAlt
+} from 'react-icons/fa'
+
+import useAppContext from '../../hooks/useAppContext'
+import Button from '../Button/Button'
import './Header.scss'
@@ -23,60 +32,139 @@ import './Header.scss'
*
* )
*/
-const Header = () => (
-
-
-
-
- Earthdata
- Metadata Management Tool
-
-
-
+const Header = () => {
+ const { user, login, logout } = useAppContext()
-
-
- Hi, User
-
+ return (
+
+
+
+
+ Earthdata
+ Metadata Management Tool
+
-
-
-
-
-
-)
+ )
+ }
+
+ {
+ user?.name && (
+
+ )
+ }
+
+
+
+
+ )
+}
export default Header
diff --git a/static/src/js/components/Header/Header.scss b/static/src/js/components/Header/Header.scss
index 88766402e..2d75c7d07 100644
--- a/static/src/js/components/Header/Header.scss
+++ b/static/src/js/components/Header/Header.scss
@@ -1,9 +1,22 @@
+@import '../../../css/vendor/bootstrap/variables';
+
.header {
&__brand-earthdata {
margin-bottom: 0.25rem;
- font-size: 0.925rem;
+ font-size: 0.75rem;
font-weight: 700;
line-height: 1rem;
+
+ @include media-breakpoint-up(md) {
+ font-size: 0.925rem;
+ }
+ }
+
+ &__navbar-collapse {
+ @include media-breakpoint-up(md) {
+ max-width: 30rem;
+ margin-left: 7rem;
+ }
}
&__dropdown-button-title {
diff --git a/static/src/js/components/Header/__tests__/Header.test.js b/static/src/js/components/Header/__tests__/Header.test.js
index 7cbb8a2fb..d4fc895fc 100644
--- a/static/src/js/components/Header/__tests__/Header.test.js
+++ b/static/src/js/components/Header/__tests__/Header.test.js
@@ -4,15 +4,27 @@ import { BrowserRouter } from 'react-router-dom'
import userEvent from '@testing-library/user-event'
import Header from '../Header'
-
-const setup = () => {
+import AppContext from '../../../context/AppContext'
+
+const setup = ({
+ overrideContext = {}
+} = {}) => {
+ const context = {
+ user: {},
+ login: jest.fn(),
+ logout: jest.fn(),
+ ...overrideContext
+ }
render(
-
-
-
+
+
+
+
+
)
return {
+ context,
user: userEvent.setup()
}
}
@@ -26,42 +38,70 @@ describe('Header component', () => {
expect(screen.getByText('Metadata Management Tool').textContent).toEqual('EarthdataMetadata Management Tool')
})
- test('displays the user name badge', () => {
- setup()
+ describe('when the user is logged out', () => {
+ test('shows the log in button', () => {
+ setup()
- expect(screen.getByText('Hi, User')).toBeInTheDocument()
- expect(screen.getByText('Hi, User').className).toContain('badge')
- expect(screen.getByText('Hi, User').className).toContain('bg-blue-light')
- })
+ const button = screen.getByRole('button', { name: 'Log in with Launchpad' })
+ expect(button).toBeInTheDocument()
+ })
- test('displays the search form', () => {
- setup()
+ describe('when the login button is clicked', () => {
+ test('calls the login function on the context', async () => {
+ const { context } = setup()
- expect(screen.getByRole('textbox')).toBeInTheDocument()
- expect(screen.getByRole('textbox')).toHaveAttribute('placeholder', 'Search MMT')
- })
+ const user = userEvent.setup()
+ const button = screen.getByRole('button', { name: 'Log in with Launchpad' })
- test('displays the search submit button', () => {
- setup()
+ await user.click(button)
- expect(screen.getByRole('button', { name: 'Search Collections' })).toBeInTheDocument()
+ expect(context.login).toHaveBeenCalledTimes(1)
+ })
+ })
})
- test('does not display the search options dropdown', () => {
- setup()
+ describe('when the user is logged in', () => {
+ beforeEach(async () => {
+ setup({
+ overrideContext: {
+ user: {
+ name: 'User Name'
+ },
+ login: jest.fn(),
+ logout: jest.fn()
+ }
+ })
+ })
- expect(screen.getByText('Search collections')).not.toHaveClass('show')
- })
+ test('displays the user name badge', () => {
+ expect(screen.getByText('User Name')).toBeInTheDocument()
+ expect(screen.getByText('User Name').className).toContain('badge')
+ expect(screen.getByText('User Name').className).toContain('bg-blue-light')
+ })
+
+ test('displays the search form', () => {
+ expect(screen.getByRole('textbox')).toBeInTheDocument()
+ expect(screen.getByRole('textbox')).toHaveAttribute('placeholder', 'Enter a search term')
+ })
+
+ test('displays the search submit button', () => {
+ expect(screen.getByRole('button', { name: 'Search Collections' })).toBeInTheDocument()
+ })
+
+ test('does not display the search options dropdown', () => {
+ expect(screen.getByText('Search Collections')).not.toHaveClass('show')
+ })
- describe('when the search submit dropdown button is clicked', () => {
- test('displays the search submit button', async () => {
- const { user } = setup()
+ describe('when the search submit dropdown button is clicked', () => {
+ test('displays the search submit button', async () => {
+ const user = userEvent.setup()
- const searchOptionsButton = screen.getByRole('button', { name: 'Search Options' })
+ const searchOptionsButton = screen.queryByRole('button', { name: 'Search Options' })
- await user.click(searchOptionsButton)
+ await user.click(searchOptionsButton)
- expect(screen.getByText('Search collections')).toHaveClass('show')
+ expect(searchOptionsButton).toHaveAttribute('aria-expanded', 'true')
+ })
})
})
})
diff --git a/static/src/js/components/ManageSection/ManageSection.jsx b/static/src/js/components/ManageSection/ManageSection.jsx
index 63dfd517f..25ad37096 100644
--- a/static/src/js/components/ManageSection/ManageSection.jsx
+++ b/static/src/js/components/ManageSection/ManageSection.jsx
@@ -7,6 +7,8 @@ import Row from 'react-bootstrap/Row'
import For from '../For/For'
+import './ManageSection.scss'
+
/**
* @typedef {Object} SectionEntry
* @property {ReactNode} children A required React node to be used as the section content.
@@ -50,17 +52,7 @@ const ManageSection = ({
>
{title}
diff --git a/static/src/js/components/ManageSection/ManageSection.scss b/static/src/js/components/ManageSection/ManageSection.scss
new file mode 100644
index 000000000..69c6f57e5
--- /dev/null
+++ b/static/src/js/components/ManageSection/ManageSection.scss
@@ -0,0 +1,9 @@
+.manage-section {
+ &__heading {
+ position: relative;
+ left: -2rem;
+ width: calc(100% + 2rem);
+ padding: 0.75rem 1rem 0.75rem 2rem;
+ font-size: 1.125rem;
+ }
+}
\ No newline at end of file
diff --git a/static/src/js/components/Page/Page.jsx b/static/src/js/components/Page/Page.jsx
index 9d65a0d44..9912121e3 100644
--- a/static/src/js/components/Page/Page.jsx
+++ b/static/src/js/components/Page/Page.jsx
@@ -58,7 +58,9 @@ import './Page.scss'
const Page = ({
breadcrumbs,
children,
+ hasBackgroundImage,
headerActions,
+ navigation,
pageType,
title
}) => {
@@ -75,45 +77,50 @@ const Page = ({
classNames([
'pb-5 flex-grow-1',
{
- 'bg-light': pageType === 'primary'
+ 'bg-light': pageType === 'primary',
+ 'page--has-background-image': hasBackgroundImage
}
])
}
>
-
+ />
+
+
+ )
+ }
@@ -187,7 +194,9 @@ const Page = ({
Page.defaultProps = {
breadcrumbs: [],
+ hasBackgroundImage: false,
headerActions: [],
+ navigation: true,
pageType: 'primary',
title: null
}
@@ -197,12 +206,14 @@ Page.propTypes = {
// eslint-disable-next-line react/forbid-prop-types
breadcrumbs: PropTypes.array,
children: PropTypes.node.isRequired,
+ hasBackgroundImage: PropTypes.bool,
headerActions: PropTypes.arrayOf(
PropTypes.shape({
label: PropTypes.string.isRequired,
to: PropTypes.string.isRequired
}).isRequired
),
+ navigation: PropTypes.bool,
pageType: PropTypes.string,
title: PropTypes.string
}
diff --git a/static/src/js/components/Page/Page.scss b/static/src/js/components/Page/Page.scss
index 37004adcd..eb802c7ed 100644
--- a/static/src/js/components/Page/Page.scss
+++ b/static/src/js/components/Page/Page.scss
@@ -1,4 +1,17 @@
.page {
+ &--has-background-image {
+ position: relative;
+ background: url('@/assets/images/yellow-sea-swirls.jpg');
+ background-repeat: no-repeat;
+ background-size: auto 20rem;
+
+
+ .container {
+ position: relative;
+ z-index: 1;
+ }
+ }
+
&__header {
position: relative;
background:
diff --git a/static/src/js/components/Panel/Panel.jsx b/static/src/js/components/Panel/Panel.jsx
new file mode 100644
index 000000000..415d71db8
--- /dev/null
+++ b/static/src/js/components/Panel/Panel.jsx
@@ -0,0 +1,47 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import Col from 'react-bootstrap/Col'
+import { FaQuestionCircle } from 'react-icons/fa'
+
+import './Panel.scss'
+
+/**
+ * Renders a `Panel` component.
+ *
+ * This is intended be used within the `HomePage` component.
+ *
+ * @component
+ * @example Renders a `Panel` component
+ * return (
+ *
+ * This text will be in the body of the panel
+ *
+ * )
+ */
+const Panel = ({
+ children,
+ title
+}) => (
+
+
+
+
+
+ {' '}
+ {title}
+
+
+
{children}
+
+
+)
+
+Panel.propTypes = {
+ children: PropTypes.node.isRequired,
+ title: PropTypes.string.isRequired
+}
+
+export default Panel
diff --git a/static/src/js/components/Panel/Panel.scss b/static/src/js/components/Panel/Panel.scss
new file mode 100644
index 000000000..d84b7547a
--- /dev/null
+++ b/static/src/js/components/Panel/Panel.scss
@@ -0,0 +1,7 @@
+.panel {
+ &__content {
+ --bs-bg-opacity: 0.8;
+
+ text-shadow: 0 0 10px rgb(0 0 0 / 30%);
+ }
+}
\ No newline at end of file
diff --git a/static/src/js/components/Panel/__tests__/Panel.test.js b/static/src/js/components/Panel/__tests__/Panel.test.js
new file mode 100644
index 000000000..c67b88a37
--- /dev/null
+++ b/static/src/js/components/Panel/__tests__/Panel.test.js
@@ -0,0 +1,34 @@
+import React from 'react'
+import {
+ render,
+ screen,
+ within
+} from '@testing-library/react'
+
+import Panel from '../Panel'
+
+const setup = () => (
+ render(
+
+ This is the test panel content
+
+ )
+)
+
+describe('Panel component', () => {
+ beforeEach(() => {
+ setup()
+ })
+
+ test('displays the title', () => {
+ const title = screen.getByText('This is the test title')
+ expect(title).toBeInTheDocument()
+ })
+
+ test('displays the correct content', () => {
+ const parent = screen.getByText('This is the test title').parentElement.parentElement
+
+ const text = within(parent).getByText('This is the test panel content')
+ expect(text).toBeInTheDocument()
+ })
+})
diff --git a/static/src/js/pages/HomePage/HomePage.jsx b/static/src/js/pages/HomePage/HomePage.jsx
new file mode 100644
index 000000000..24edbd2a5
--- /dev/null
+++ b/static/src/js/pages/HomePage/HomePage.jsx
@@ -0,0 +1,48 @@
+import React from 'react'
+import Row from 'react-bootstrap/Row'
+
+import For from '../../components/For/For'
+import Page from '../../components/Page/Page'
+import Panel from '../../components/Panel/Panel'
+
+import './HomePage.scss'
+
+/**
+ * Renders a `HomePage` component
+ *
+ * @component
+ * @example Renders a `HomePage` component
+ * return (
+ *
+ * )
+ */
+const HomePage = () => {
+ const panels = [
+ {
+ title: 'About the Metadata Management Tool (MMT)',
+ body: 'The MMT is a web-based user interface to the NASA EOSDIS Common Metadata Repository (CMR). The MMT allows metadata authors to create and update CMR metadata records by using a data entry form based on the metadata fields in the CMR Unified Metadata Model (UMM). Metadata authors may also publish, view, delete, and manage revisions of CMR metadata records using the MMT.'
+ },
+ {
+ title: 'About the Common Metadata Repository (CMR)',
+ body: 'The CMR is a high-performance, high-quality metadata repository for earth science metadata records. The CMR manages the evolution of NASA Earth Science metadata in a unified and consistent way by providing a central storage and access capability that streamlines current workflows while increasing overall metadata quality and anticipating future capabilities.'
+ }
+ ]
+
+ return (
+
+
+
+ {
+ ({ title, body }) => (
+
+ {body}
+
+ )
+ }
+
+
+
+ )
+}
+
+export default HomePage
diff --git a/static/src/js/pages/HomePage/HomePage.scss b/static/src/js/pages/HomePage/HomePage.scss
new file mode 100644
index 000000000..c870cb5ef
--- /dev/null
+++ b/static/src/js/pages/HomePage/HomePage.scss
@@ -0,0 +1,7 @@
+.home-page {
+ &__panel-content {
+ --bs-bg-opacity: 0.8;
+
+ text-shadow: 0 0 10px rgb(0 0 0 / 30%);
+ }
+}
diff --git a/static/src/js/pages/HomePage/__tests__/HomePage.test.js b/static/src/js/pages/HomePage/__tests__/HomePage.test.js
new file mode 100644
index 000000000..1f7deee1e
--- /dev/null
+++ b/static/src/js/pages/HomePage/__tests__/HomePage.test.js
@@ -0,0 +1,71 @@
+import React from 'react'
+import {
+ render,
+ screen,
+ within
+} from '@testing-library/react'
+
+import HomePage from '../HomePage'
+import AppContext from '../../../context/AppContext'
+
+const setup = ({
+ overrideContext = {}
+} = {}) => {
+ const context = {
+ user: {
+ name: 'User Name',
+ token: 'ABC-1',
+ providerId: 'MMT-2'
+ },
+ login: jest.fn(),
+ logout: jest.fn(),
+ ...overrideContext
+ }
+ render(
+
+
+
+ )
+
+ return {
+ context
+ }
+}
+
+describe('HomePage component', () => {
+ describe('when the about MMT section is displayed', () => {
+ beforeEach(() => {
+ setup()
+ })
+
+ test('displays the title', () => {
+ const title = screen.getByText('About the Metadata Management Tool (MMT)')
+ expect(title).toBeInTheDocument()
+ })
+
+ test('displays the correct content', () => {
+ const parent = screen.getByText('About the Metadata Management Tool (MMT)').parentElement.parentElement
+
+ const text = within(parent).getByText('The MMT is a web-based user interface to the NASA EOSDIS Common Metadata Repository (CMR). The MMT allows metadata authors to create and update CMR metadata records by using a data entry form based on the metadata fields in the CMR Unified Metadata Model (UMM). Metadata authors may also publish, view, delete, and manage revisions of CMR metadata records using the MMT.')
+ expect(text).toBeInTheDocument()
+ })
+ })
+
+ describe('when the about CMR section is displayed', () => {
+ beforeEach(() => {
+ setup()
+ })
+
+ test('displays the title', () => {
+ const title = screen.getByText('About the Common Metadata Repository (CMR)')
+ expect(title).toBeInTheDocument()
+ })
+
+ test('displays the correct content', () => {
+ const parent = screen.getByText('About the Common Metadata Repository (CMR)').parentElement.parentElement
+
+ const text = within(parent).getByText('The CMR is a high-performance, high-quality metadata repository for earth science metadata records. The CMR manages the evolution of NASA Earth Science metadata in a unified and consistent way by providing a central storage and access capability that streamlines current workflows while increasing overall metadata quality and anticipating future capabilities.')
+ expect(text).toBeInTheDocument()
+ })
+ })
+})
diff --git a/static/src/js/providers/AppContextProvider/AppContextProvider.jsx b/static/src/js/providers/AppContextProvider/AppContextProvider.jsx
index 5ce388d0a..08c60f595 100644
--- a/static/src/js/providers/AppContextProvider/AppContextProvider.jsx
+++ b/static/src/js/providers/AppContextProvider/AppContextProvider.jsx
@@ -1,4 +1,5 @@
import React, {
+ useCallback,
useEffect,
useMemo,
useState
@@ -32,18 +33,33 @@ const AppContextProvider = ({ children }) => {
const [savedDraft, setSavedDraft] = useState()
const [user, setUser] = useState({})
+ const { keywords } = keywordsContext
+
useEffect(() => {
setUser({
+ name: 'User Name',
token: 'ABC-1',
providerId: 'MMT_2'
})
}, [])
- const { keywords } = keywordsContext
+ const login = useCallback(() => {
+ setUser({
+ name: 'User Name',
+ token: 'ABC-1',
+ providerId: 'MMT_2'
+ })
+ })
+
+ const logout = useCallback(() => {
+ setUser({})
+ })
const providerValue = useMemo(() => ({
...keywordsContext,
draft,
+ login,
+ logout,
originalDraft,
savedDraft,
setDraft,
@@ -55,7 +71,9 @@ const AppContextProvider = ({ children }) => {
originalDraft,
keywords,
savedDraft,
- user
+ user,
+ login,
+ logout
])
return (
diff --git a/static/src/js/providers/AppContextProvider/__tests__/AppContextProvider.test.js b/static/src/js/providers/AppContextProvider/__tests__/AppContextProvider.test.js
new file mode 100644
index 000000000..89e5e0cb2
--- /dev/null
+++ b/static/src/js/providers/AppContextProvider/__tests__/AppContextProvider.test.js
@@ -0,0 +1,90 @@
+import React from 'react'
+import { render, screen } from '@testing-library/react'
+
+import userEvent from '@testing-library/user-event'
+import useAppContext from '../../../hooks/useAppContext'
+import AppContextProvider from '../AppContextProvider'
+
+const MockComponent = () => {
+ const { user, login, logout } = useAppContext()
+
+ return (
+
+
+ User Name:
+ {' '}
+ {user?.name}
+
+
+
+
+ )
+}
+
+const setup = () => {
+ render(
+
+
+
+ )
+}
+
+describe('ManagePage component', () => {
+ describe('when all metadata is provided', () => {
+ beforeEach(() => {
+ setup()
+ jest.resetAllMocks()
+ })
+
+ describe('when log in is triggered', () => {
+ test('logs the user in', async () => {
+ const user = userEvent.setup()
+ const button = screen.getByRole('button', { name: 'Log in' })
+
+ await user.click(button)
+
+ const userName = screen.getByText('User Name: User Name', { exact: true })
+ expect(userName).toBeInTheDocument()
+ })
+ })
+
+ describe('when log out is triggered', () => {
+ test('logs the user out', async () => {
+ const user = userEvent.setup()
+ const loginButton = screen.getByRole('button', { name: 'Log in' })
+
+ await user.click(loginButton)
+
+ const userName = screen.getByText('User Name: User Name', { exact: true })
+
+ expect(userName).toBeInTheDocument()
+
+ const logoutButton = screen.getByRole('button', { name: 'Log out' })
+
+ await user.click(logoutButton)
+
+ const newUserName = screen.queryByText('User Name: User Name', { exact: true })
+
+ expect(newUserName).not.toBeInTheDocument()
+ })
+ })
+ })
+})