diff --git a/src/App.jsx b/src/App.jsx index 3204cae..3c6105c 100755 --- a/src/App.jsx +++ b/src/App.jsx @@ -10,9 +10,9 @@ import selectors from 'data/selectors'; import ListView from 'containers/ListView'; import './App.scss'; -import { Header } from 'containers/CourseHeader'; +import Header from 'containers/CourseHeader'; -const App = ({ courseMetadata }) => ( +export const App = ({ courseMetadata }) => (
({ - BrowserRouter: () => 'BrowserRouter', - Route: () => 'Route', - Switch: () => 'Switch', -})); -jest.mock('react-redux', () => ({ - Provider: () => 'Provider', -})); -jest.mock('react-intl', () => ({ - IntlProvider: () => 'IntlProvider', -})); -jest.mock('data/constants/app', () => ({ - routePath: '/:courseId', -})); jest.mock('@edx/frontend-component-footer', () => 'Footer'); -jest.mock('data/store', () => 'testStore'); jest.mock('containers/ListView', () => 'ListView'); +jest.mock('containers/CourseHeader', () => 'CourseHeader'); const logo = 'fakeLogo.png'; let el; let router; describe('App router component', () => { + const props = { + courseMetadata: { + org: 'course-org', + number: 'course-number', + title: 'course-title', + }, + }; test('snapshot', () => { - expect(shallow()).toMatchSnapshot(); + expect(shallow()).toMatchSnapshot(); }); describe('component', () => { beforeEach(() => { process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG = logo; - el = shallow(); - router = el.childAt(0).childAt(0); - }); - describe('IntlProvider', () => { - test('outer-wrapper component', () => { - expect(el.type()).toBe(IntlProvider); - }); - test('"en" locale', () => { - expect(el.props().locale).toEqual('en'); - }); - }); - describe('Provider, inside IntlProvider', () => { - test('first child, passed the redux store props', () => { - expect(el.childAt(0).type()).toBe(Provider); - expect(el.childAt(0).props().store).toEqual(store); - }); + el = shallow(); + router = el.childAt(0); }); describe('Router', () => { - test('first child of Provider', () => { - expect(router.type()).toBe(Router); - }); test('Routing - ListView is only route', () => { expect(router.find('main')).toEqual(shallow( -
- - - -
, +
, )); }); }); diff --git a/src/__snapshots__/App.test.jsx.snap b/src/__snapshots__/App.test.jsx.snap index 924a583..3069065 100644 --- a/src/__snapshots__/App.test.jsx.snap +++ b/src/__snapshots__/App.test.jsx.snap @@ -1,26 +1,19 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`App router component snapshot 1`] = ` - - - -
-
- - - -
-
-
-
-
-
+ +
+ +
+ +
+
+
+
`; diff --git a/src/containers/CourseHeader/AnonymousUserMenu.jsx b/src/containers/CourseHeader/AnonymousUserMenu.jsx index 72156cd..ee1ea71 100644 --- a/src/containers/CourseHeader/AnonymousUserMenu.jsx +++ b/src/containers/CourseHeader/AnonymousUserMenu.jsx @@ -5,27 +5,31 @@ import { getLoginRedirectUrl } from '@edx/frontend-platform/auth'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { Button } from '@edx/paragon'; -import message from './AnonymousUserMenu.messages'; +import message from './messages'; -function AnonymousUserMenu({ intl }) { - return ( -
- - -
- ); -} +export const getRegisterUrl = () => { + const { LMS_BASE_URL } = getConfig(); + const locationHref = encodeURIComponent(global.location.href); + return `${LMS_BASE_URL}/register?next=${locationHref}`; +}; + +export const AnonymousUserMenu = ({ intl }) => ( +
+ + +
+); AnonymousUserMenu.propTypes = { intl: intlShape.isRequired, diff --git a/src/containers/CourseHeader/AnonymousUserMenu.test.jsx b/src/containers/CourseHeader/AnonymousUserMenu.test.jsx new file mode 100644 index 0000000..7a564a6 --- /dev/null +++ b/src/containers/CourseHeader/AnonymousUserMenu.test.jsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import { AnonymousUserMenu } from './AnonymousUserMenu'; + +jest.mock('@edx/frontend-platform', () => ({ + getConfig: () => ({ + LMS_BASE_URL: '', + }), +})); +jest.mock('@edx/frontend-platform/auth', () => ({ + getLoginRedirectUrl: (url) => `redirect:${url}`, +})); +jest.mock('@edx/paragon', () => ({ + Button: () => 'Button', +})); + +describe('Header AnonymousUserMenu component', () => { + const props = { + intl: { formatMessage: (msg) => msg.defaultMessage }, + }; + test('snapshot', () => { + expect( + shallow(), + ).toMatchSnapshot(); + }); +}); diff --git a/src/containers/CourseHeader/AuthenticatedUserDropdown.jsx b/src/containers/CourseHeader/AuthenticatedUserDropdown.jsx deleted file mode 100644 index c9b1704..0000000 --- a/src/containers/CourseHeader/AuthenticatedUserDropdown.jsx +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faUserCircle } from '@fortawesome/free-solid-svg-icons'; - -import { getConfig } from '@edx/frontend-platform'; -import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; -import { Dropdown } from '@edx/paragon'; - -import messages from './messages'; - -function AuthenticatedUserDropdown({ intl, username }) { - let dashboardMenuItem = ( - - {intl.formatMessage(messages.dashboard)} - - ); - return ( - <> - {intl.formatMessage(messages.help)} - - - - - {username} - - - - {dashboardMenuItem} - - {intl.formatMessage(messages.profile)} - - - {intl.formatMessage(messages.account)} - - - {intl.formatMessage(messages.signOut)} - - - - - ); -} - -AuthenticatedUserDropdown.propTypes = { - intl: intlShape.isRequired, - username: PropTypes.string.isRequired, -}; - -AuthenticatedUserDropdown.defaultProps = {}; - -export default injectIntl(AuthenticatedUserDropdown); diff --git a/src/containers/CourseHeader/AuthenticatedUserDropdown.messages.js b/src/containers/CourseHeader/AuthenticatedUserDropdown.messages.js deleted file mode 100644 index c9b1704..0000000 --- a/src/containers/CourseHeader/AuthenticatedUserDropdown.messages.js +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faUserCircle } from '@fortawesome/free-solid-svg-icons'; - -import { getConfig } from '@edx/frontend-platform'; -import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; -import { Dropdown } from '@edx/paragon'; - -import messages from './messages'; - -function AuthenticatedUserDropdown({ intl, username }) { - let dashboardMenuItem = ( - - {intl.formatMessage(messages.dashboard)} - - ); - return ( - <> - {intl.formatMessage(messages.help)} - - - - - {username} - - - - {dashboardMenuItem} - - {intl.formatMessage(messages.profile)} - - - {intl.formatMessage(messages.account)} - - - {intl.formatMessage(messages.signOut)} - - - - - ); -} - -AuthenticatedUserDropdown.propTypes = { - intl: intlShape.isRequired, - username: PropTypes.string.isRequired, -}; - -AuthenticatedUserDropdown.defaultProps = {}; - -export default injectIntl(AuthenticatedUserDropdown); diff --git a/src/containers/CourseHeader/AuthenticatedUserDropdown/UserAvatar.jsx b/src/containers/CourseHeader/AuthenticatedUserDropdown/UserAvatar.jsx new file mode 100644 index 0000000..47eb320 --- /dev/null +++ b/src/containers/CourseHeader/AuthenticatedUserDropdown/UserAvatar.jsx @@ -0,0 +1,27 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faUserCircle } from '@fortawesome/free-solid-svg-icons'; + +import { Dropdown } from '@edx/paragon'; + +export const UserAvatar = ({ username }) => ( + + + + {username} + + +); +UserAvatar.propTypes = { + username: PropTypes.string.isRequired, +}; + +UserAvatar.defaultProps = {}; + +export default UserAvatar; diff --git a/src/containers/CourseHeader/AuthenticatedUserDropdown/UserAvatar.test.jsx b/src/containers/CourseHeader/AuthenticatedUserDropdown/UserAvatar.test.jsx new file mode 100644 index 0000000..a28a655 --- /dev/null +++ b/src/containers/CourseHeader/AuthenticatedUserDropdown/UserAvatar.test.jsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import UserAvatar from './UserAvatar'; + +jest.mock('@edx/frontend-platform', () => ({ + getConfig: () => ({ + LMS_BASE_URL: '', + LOGOUT_URL: '', + SUPPORT_URL: '', + }), +})); +jest.mock('@edx/paragon', () => ({ + Dropdown: { + Toggle: () => 'Dropdown.Toggle', + Item: () => 'Dropdown.Item', + }, +})); +jest.mock('@fortawesome/react-fontawesome', () => ({ + FontAwesomeIcon: () => 'FontAwesomeIcon', +})); + +describe('Header AuthenticatedUserDropdown UserAvatar component', () => { + const props = { + username: 'test-username', + }; + test('snapshot', () => { + expect( + shallow(), + ).toMatchSnapshot(); + }); +}); diff --git a/src/containers/CourseHeader/AuthenticatedUserDropdown/UserMenu.jsx b/src/containers/CourseHeader/AuthenticatedUserDropdown/UserMenu.jsx new file mode 100644 index 0000000..2c28c3a --- /dev/null +++ b/src/containers/CourseHeader/AuthenticatedUserDropdown/UserMenu.jsx @@ -0,0 +1,40 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { getConfig } from '@edx/frontend-platform'; +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import { Dropdown } from '@edx/paragon'; + +import messages from '../messages'; + +export class UserMenu extends React.Component { + menuItem(href, message) { + return ( + + {this.props.intl.formatMessage(message)} + + ); + } + + render() { + const { username } = this.props; + const { LMS_BASE_URL, LOGOUT_URL } = getConfig(); + return ( + + {this.menuItem(`${LMS_BASE_URL}/dashboard`, messages.dashboard)} + {this.menuItem(`${LMS_BASE_URL}/u/${username}`, messages.profile)} + {this.menuItem(`${LMS_BASE_URL}/account/settings`, messages.account)} + {this.menuItem(LOGOUT_URL, messages.signOut)} + + ); + } +} + +UserMenu.propTypes = { + intl: intlShape.isRequired, + username: PropTypes.string.isRequired, +}; + +UserMenu.defaultProps = {}; + +export default injectIntl(UserMenu); diff --git a/src/containers/CourseHeader/AuthenticatedUserDropdown/UserMenu.test.jsx b/src/containers/CourseHeader/AuthenticatedUserDropdown/UserMenu.test.jsx new file mode 100644 index 0000000..f340ea4 --- /dev/null +++ b/src/containers/CourseHeader/AuthenticatedUserDropdown/UserMenu.test.jsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import { UserMenu } from './UserMenu'; + +jest.mock('@edx/frontend-platform', () => ({ + getConfig: () => ({ + LMS_BASE_URL: '', + LOGOUT_URL: '', + SUPPORT_URL: '', + }), +})); +jest.mock('@edx/paragon', () => { + const Dropdown = () => 'Dropdown'; + Dropdown.Toggle = () => 'Dropdown.Toggle'; + Dropdown.Menu = () => 'Dropdown.Menu'; + Dropdown.Item = () => 'Dropdown.Item'; + return { Dropdown }; +}); +jest.mock('@fortawesome/react-fontawesome', () => ({ + FontAwesomeIcon: () => 'FontAwesomeIcon', +})); +jest.mock('@fortawesome/free-solid-svg-icons', () => ({ + faUserCircle: 'fa-user-circle-icon', +})); + +describe('Header AuthenticatedUserDropdown UserMenu component', () => { + const props = { + intl: { formatMessage: (msg) => msg.defaultMessage }, + username: 'test-username', + }; + test('snapshot', () => { + expect( + shallow(), + ).toMatchSnapshot(); + }); +}); diff --git a/src/containers/CourseHeader/AuthenticatedUserDropdown/__snapshots__/UserAvatar.test.jsx.snap b/src/containers/CourseHeader/AuthenticatedUserDropdown/__snapshots__/UserAvatar.test.jsx.snap new file mode 100644 index 0000000..9737821 --- /dev/null +++ b/src/containers/CourseHeader/AuthenticatedUserDropdown/__snapshots__/UserAvatar.test.jsx.snap @@ -0,0 +1,31 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Header AuthenticatedUserDropdown UserAvatar component snapshot 1`] = ` + + + + test-username + + +`; diff --git a/src/containers/CourseHeader/AuthenticatedUserDropdown/__snapshots__/UserMenu.test.jsx.snap b/src/containers/CourseHeader/AuthenticatedUserDropdown/__snapshots__/UserMenu.test.jsx.snap new file mode 100644 index 0000000..fb30595 --- /dev/null +++ b/src/containers/CourseHeader/AuthenticatedUserDropdown/__snapshots__/UserMenu.test.jsx.snap @@ -0,0 +1,28 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Header AuthenticatedUserDropdown UserMenu component snapshot 1`] = ` + + + Dashboard + + + Profile + + + Account + + + Sign Out + + +`; diff --git a/src/containers/CourseHeader/AuthenticatedUserDropdown/__snapshots__/index.test.jsx.snap b/src/containers/CourseHeader/AuthenticatedUserDropdown/__snapshots__/index.test.jsx.snap new file mode 100644 index 0000000..769a582 --- /dev/null +++ b/src/containers/CourseHeader/AuthenticatedUserDropdown/__snapshots__/index.test.jsx.snap @@ -0,0 +1,20 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Header AuthenticatedUserDropdown component snapshot 1`] = ` + + + Help + + + + + + +`; diff --git a/src/containers/CourseHeader/AuthenticatedUserDropdown/index.jsx b/src/containers/CourseHeader/AuthenticatedUserDropdown/index.jsx new file mode 100644 index 0000000..10998b6 --- /dev/null +++ b/src/containers/CourseHeader/AuthenticatedUserDropdown/index.jsx @@ -0,0 +1,35 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { getConfig } from '@edx/frontend-platform'; +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import { Dropdown } from '@edx/paragon'; + +import UserMenu from './UserMenu'; +import UserAvatar from './UserAvatar'; + +import messages from '../messages'; + +export const AuthenticatedUserDropdown = ({ + intl, + username, +}) => ( + <> + + {intl.formatMessage(messages.help)} + + + + + + +); + +AuthenticatedUserDropdown.propTypes = { + intl: intlShape.isRequired, + username: PropTypes.string.isRequired, +}; + +AuthenticatedUserDropdown.defaultProps = {}; + +export default injectIntl(AuthenticatedUserDropdown); diff --git a/src/containers/CourseHeader/AuthenticatedUserDropdown/index.test.jsx b/src/containers/CourseHeader/AuthenticatedUserDropdown/index.test.jsx new file mode 100644 index 0000000..5e1dc71 --- /dev/null +++ b/src/containers/CourseHeader/AuthenticatedUserDropdown/index.test.jsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import { AuthenticatedUserDropdown } from '.'; + +jest.mock('@edx/frontend-platform', () => ({ + getConfig: () => ({ + SUPPORT_URL: '', + }), +})); +jest.mock('@edx/paragon', () => ({ + Dropdown: () => 'Dropdown', +})); +jest.mock('./UserAvatar', () => 'UserAvatar'); +jest.mock('./UserMenu', () => 'UserMenu'); + +describe('Header AuthenticatedUserDropdown component', () => { + const props = { + intl: { formatMessage: (msg) => msg.defaultMessage }, + username: 'test-username', + }; + test('snapshot', () => { + expect( + shallow(), + ).toMatchSnapshot(); + }); +}); diff --git a/src/containers/CourseHeader/CourseLabel.jsx b/src/containers/CourseHeader/CourseLabel.jsx new file mode 100644 index 0000000..e946a72 --- /dev/null +++ b/src/containers/CourseHeader/CourseLabel.jsx @@ -0,0 +1,32 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +export const CourseLabel = ({ + courseOrg, + courseNumber, + courseTitle, +}) => ( +
+ + {courseOrg} {courseNumber} + + + {courseTitle} + +
+); +CourseLabel.propTypes = { + courseOrg: PropTypes.string, + courseNumber: PropTypes.string, + courseTitle: PropTypes.string, +}; +CourseLabel.defaultProps = { + courseOrg: null, + courseNumber: null, + courseTitle: null, +}; + +export default CourseLabel; diff --git a/src/containers/CourseHeader/CourseLabel.test.jsx b/src/containers/CourseHeader/CourseLabel.test.jsx new file mode 100644 index 0000000..1c774cb --- /dev/null +++ b/src/containers/CourseHeader/CourseLabel.test.jsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import CourseLabel from './CourseLabel'; + +const courseData = { + courseOrg: 'course-org', + courseNumber: 'course-number', + courseTitle: 'course-title', +}; + +describe('Header CourseLabel component', () => { + test('snapshot', () => { + expect( + shallow(), + ).toMatchSnapshot(); + }); +}); diff --git a/src/containers/CourseHeader/Header.jsx b/src/containers/CourseHeader/Header.jsx deleted file mode 100644 index 7dac9b7..0000000 --- a/src/containers/CourseHeader/Header.jsx +++ /dev/null @@ -1,81 +0,0 @@ -import React, { useContext } from 'react'; -import PropTypes from 'prop-types'; -import { getConfig } from '@edx/frontend-platform'; -import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; -import { AppContext } from '@edx/frontend-platform/react'; - -import AnonymousUserMenu from './AnonymousUserMenu'; -import AuthenticatedUserDropdown from './AuthenticatedUserDropdown'; -import messages from './messages'; - -function LinkedLogo({ - href, - src, - alt, - ...attributes -}) { - return ( - - {alt} - - ); -} - -LinkedLogo.propTypes = { - href: PropTypes.string.isRequired, - src: PropTypes.string.isRequired, - alt: PropTypes.string.isRequired, -}; - -function Header({ - courseOrg, courseNumber, courseTitle, intl, showUserDropdown, -}) { - const { authenticatedUser } = useContext(AppContext); - - let headerLogo = ( - - ); - - return ( -
- {intl.formatMessage(messages.skipNavLink)} -
- {headerLogo} -
- {courseOrg} {courseNumber} - {courseTitle} -
- {showUserDropdown && authenticatedUser && ( - - )} - {showUserDropdown && !authenticatedUser && ( - - )} -
-
- ); -} - -Header.propTypes = { - courseOrg: PropTypes.string, - courseNumber: PropTypes.string, - courseTitle: PropTypes.string, - intl: intlShape.isRequired, - showUserDropdown: PropTypes.bool, -}; - -Header.defaultProps = { - courseOrg: null, - courseNumber: null, - courseTitle: null, - showUserDropdown: true, -}; - -export default injectIntl(Header); diff --git a/src/containers/CourseHeader/Header.test.jsx b/src/containers/CourseHeader/Header.test.jsx deleted file mode 100644 index 2889aa7..0000000 --- a/src/containers/CourseHeader/Header.test.jsx +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react'; -import { - authenticatedUser, initializeMockApp, render, screen, -} from '../setupTest'; -import { Header } from './index'; - -describe('Header', () => { - beforeAll(async () => { - // We need to mock AuthService to implicitly use `getAuthenticatedUser` within `AppContext.Provider`. - await initializeMockApp(); - }); - - it('displays user button', () => { - render(
); - expect(screen.getByRole('button')).toHaveTextContent(authenticatedUser.username); - }); - - it('displays course data', () => { - const courseData = { - courseOrg: 'course-org', - courseNumber: 'course-number', - courseTitle: 'course-title', - }; - render(
); - - expect(screen.getByText(`${courseData.courseOrg} ${courseData.courseNumber}`)).toBeInTheDocument(); - expect(screen.getByText(courseData.courseTitle)).toBeInTheDocument(); - }); -}); diff --git a/src/containers/CourseHeader/LinkedLogo.jsx b/src/containers/CourseHeader/LinkedLogo.jsx new file mode 100644 index 0000000..f385602 --- /dev/null +++ b/src/containers/CourseHeader/LinkedLogo.jsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { getConfig } from '@edx/frontend-platform'; + +const LinkedLogo = () => ( + + {getConfig().SITE_NAME} + +); + +export default LinkedLogo; diff --git a/src/containers/CourseHeader/LinkedLogo.test.jsx b/src/containers/CourseHeader/LinkedLogo.test.jsx new file mode 100644 index 0000000..19592bc --- /dev/null +++ b/src/containers/CourseHeader/LinkedLogo.test.jsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import LinkedLogo from './LinkedLogo'; + +jest.mock('@edx/frontend-platform', () => ({ + getConfig: () => ({ + LMS_BASE_URL: '', + LOGO_URL: '', + SITE_NAME: '', + }), +})); + +describe('Header CourseLabel component', () => { + test('snapshot', () => { + expect( + shallow(), + ).toMatchSnapshot(); + }); +}); diff --git a/src/containers/CourseHeader/__snapshots__/AnonymousUserMenu.test.jsx.snap b/src/containers/CourseHeader/__snapshots__/AnonymousUserMenu.test.jsx.snap new file mode 100644 index 0000000..e042921 --- /dev/null +++ b/src/containers/CourseHeader/__snapshots__/AnonymousUserMenu.test.jsx.snap @@ -0,0 +1,19 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Header AnonymousUserMenu component snapshot 1`] = ` +
+ + +
+`; diff --git a/src/containers/CourseHeader/__snapshots__/CourseLabel.test.jsx.snap b/src/containers/CourseHeader/__snapshots__/CourseLabel.test.jsx.snap new file mode 100644 index 0000000..fdf9dd0 --- /dev/null +++ b/src/containers/CourseHeader/__snapshots__/CourseLabel.test.jsx.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Header CourseLabel component snapshot 1`] = ` +
+ + course-org + + course-number + + + course-title + +
+`; diff --git a/src/containers/CourseHeader/__snapshots__/LinkedLogo.test.jsx.snap b/src/containers/CourseHeader/__snapshots__/LinkedLogo.test.jsx.snap new file mode 100644 index 0000000..c26dda8 --- /dev/null +++ b/src/containers/CourseHeader/__snapshots__/LinkedLogo.test.jsx.snap @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Header CourseLabel component snapshot 1`] = ` + + <getConfig().SITE_NAME> + +`; diff --git a/src/containers/CourseHeader/__snapshots__/index.test.js.snap b/src/containers/CourseHeader/__snapshots__/index.test.js.snap new file mode 100644 index 0000000..57a5c72 --- /dev/null +++ b/src/containers/CourseHeader/__snapshots__/index.test.js.snap @@ -0,0 +1,51 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Header component snapshot 1`] = ` +
+ + Skip to main content. + +
+ + + +
+
+`; + +exports[`Header component snapshot with authenticatedUser 1`] = ` +
+ + Skip to main content. + +
+ + + +
+
+`; diff --git a/src/containers/CourseHeader/index.js b/src/containers/CourseHeader/index.js deleted file mode 100644 index 5653319..0000000 --- a/src/containers/CourseHeader/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default as Header } from './Header'; diff --git a/src/containers/CourseHeader/index.jsx b/src/containers/CourseHeader/index.jsx new file mode 100644 index 0000000..afcc76f --- /dev/null +++ b/src/containers/CourseHeader/index.jsx @@ -0,0 +1,47 @@ +import React, { useContext } from 'react'; +import PropTypes from 'prop-types'; +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import { AppContext } from '@edx/frontend-platform/react'; + +import AnonymousUserMenu from './AnonymousUserMenu'; +import AuthenticatedUserDropdown from './AuthenticatedUserDropdown'; +import LinkedLogo from './LinkedLogo'; +import CourseLabel from './CourseLabel'; + +import messages from './messages'; + +export const Header = ({ + courseOrg, + courseNumber, + courseTitle, + intl, +}) => { + const { authenticatedUser } = useContext(AppContext); + return ( +
+ + {intl.formatMessage(messages.skipNavLink)} + +
+ + + {authenticatedUser + ? () + : ()} +
+
+ ); +}; +Header.propTypes = { + courseOrg: PropTypes.string, + courseNumber: PropTypes.string, + courseTitle: PropTypes.string, + intl: intlShape.isRequired, +}; +Header.defaultProps = { + courseOrg: null, + courseNumber: null, + courseTitle: null, +}; + +export default injectIntl(Header); diff --git a/src/containers/CourseHeader/index.test.jsx b/src/containers/CourseHeader/index.test.jsx new file mode 100644 index 0000000..becdf47 --- /dev/null +++ b/src/containers/CourseHeader/index.test.jsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import { AppContext } from '@edx/frontend-platform/react'; +import { Header } from '.'; + +jest.mock('./AnonymousUserMenu', () => 'AnonymousUserMenu'); +jest.mock('./AuthenticatedUserDropdown', () => 'AuthenticatedUserDropdown'); +jest.mock('./LinkedLogo', () => 'LinkedLogo'); +jest.mock('./CourseLabel', () => 'CourseLabel'); + +jest.mock('@edx/frontend-platform/react', () => ({ + AppContext: { authenticatedUser: null }, +})); +jest.mock('react', () => ({ + ...jest.requireActual('react'), + useContext: (context) => context, +})); + +const courseData = { + courseOrg: 'course-org', + courseNumber: 'course-number', + courseTitle: 'course-title', +}; + +describe('Header component', () => { + const props = { + ...courseData, + intl: { formatMessage: (msg) => msg.defaultMessage }, + }; + test('snapshot', () => { + expect(shallow(
)).toMatchSnapshot(); + }); + test('snapshot with authenticatedUser', () => { + AppContext.authenticatedUser = { username: 'test' }; + expect(shallow(
)).toMatchSnapshot(); + }); +}); diff --git a/src/containers/CourseHeader/messages.js b/src/containers/CourseHeader/messages.js index 0750159..0c03373 100644 --- a/src/containers/CourseHeader/messages.js +++ b/src/containers/CourseHeader/messages.js @@ -1,6 +1,11 @@ import { defineMessages } from '@edx/frontend-platform/i18n'; const messages = defineMessages({ + courseMaterial: { + id: 'learn.navigation.course.tabs.label', + defaultMessage: 'Course Material', + description: 'The accessible label for course tabs navigation', + }, dashboard: { id: 'header.menu.dashboard.label', defaultMessage: 'Dashboard', @@ -21,6 +26,11 @@ const messages = defineMessages({ defaultMessage: 'Account', description: 'The text for the user menu Account navigation link.', }, + orderHistory: { + id: 'header.menu.orderHistory.label', + defaultMessage: 'Order History', + description: 'The text for the user menu Order History navigation link.', + }, skipNavLink: { id: 'header.navigation.skipNavLink', defaultMessage: 'Skip to main content.', @@ -31,6 +41,16 @@ const messages = defineMessages({ defaultMessage: 'Sign Out', description: 'The label for the user menu Sign Out action.', }, + registerSentenceCase: { + id: 'header.register.sentenceCase', + defaultMessage: 'Register', + description: 'Text in a button, prompting the user to register.', + }, + signInSentenceCase: { + id: 'header.signIn.sentenceCase', + defaultMessage: 'Sign in', + description: 'Text in a button, prompting the user to log in.', + }, }); export default messages; diff --git a/src/containers/CourseHeader/AnonymousUserMenu.messages.js b/src/containers/CourseHeader/messages2.js similarity index 100% rename from src/containers/CourseHeader/AnonymousUserMenu.messages.js rename to src/containers/CourseHeader/messages2.js diff --git a/src/containers/CriterionContainer/ReviewCriterion.jsx b/src/containers/CriterionContainer/ReviewCriterion.jsx index 091e0e9..6e160bb 100644 --- a/src/containers/CriterionContainer/ReviewCriterion.jsx +++ b/src/containers/CriterionContainer/ReviewCriterion.jsx @@ -14,7 +14,7 @@ import selectors from 'data/selectors'; */ export const ReviewCriterion = ({ config, - data, + // data, }) => (
{ config.options.map(option => ( diff --git a/src/containers/ListView/ListViewBreadcrumb.jsx b/src/containers/ListView/ListViewBreadcrumb.jsx index 8786dbd..b6ace80 100644 --- a/src/containers/ListView/ListViewBreadcrumb.jsx +++ b/src/containers/ListView/ListViewBreadcrumb.jsx @@ -3,10 +3,10 @@ import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { ArrowBack } from '@edx/paragon/icons'; -import { Hyperlink } from '@edx/paragon' +import { Hyperlink } from '@edx/paragon'; -import { locationId } from '../../data/constants/app'; import selectors from 'data/selectors'; +import { locationId } from '../../data/constants/app'; /** * @@ -23,15 +23,18 @@ export const ListViewBreadcrumb = ({ courseId, oraName }) => {

{oraName}

+ /> +

); }; ListViewBreadcrumb.defaultProps = { courseId: '', + oraName: '', }; ListViewBreadcrumb.propTypes = { courseId: PropTypes.string, + oraName: PropTypes.string, }; export const mapStateToProps = (state) => ({ @@ -42,4 +45,4 @@ export const mapStateToProps = (state) => ({ export const mapDispatchToProps = { }; -export default connect(mapStateToProps, mapDispatchToProps)(ListViewBreadcrumb); \ No newline at end of file +export default connect(mapStateToProps, mapDispatchToProps)(ListViewBreadcrumb); diff --git a/src/containers/ListView/index.jsx b/src/containers/ListView/index.jsx index b9b078c..97e91ec 100644 --- a/src/containers/ListView/index.jsx +++ b/src/containers/ListView/index.jsx @@ -23,7 +23,7 @@ import './ListView.scss'; const gradeStatusOptions = Object.keys(gradingStatusDisplay).map(key => ({ name: gradingStatusDisplay[key], - value: key + value: key, })); /** @@ -59,7 +59,7 @@ export class ListView extends React.Component { buttonText: `View selected responses (${selectedFlatRows.length})`, handleClick: this.handleViewAllResponsesClick, variant: 'primary', - } + }; } render() { diff --git a/src/data/constants/app.js b/src/data/constants/app.js index f59fa3a..0bf0e0a 100644 --- a/src/data/constants/app.js +++ b/src/data/constants/app.js @@ -2,4 +2,4 @@ import { getConfig } from '@edx/frontend-platform'; // eslint-disable-next-line import/prefer-default-export export const routePath = `${getConfig().PUBLIC_PATH}:courseId`; -export const locationId = window.location.pathname.slice(1) \ No newline at end of file +export const locationId = window.location.pathname.slice(1); diff --git a/src/data/services/lms/fakeData/course.js b/src/data/services/lms/fakeData/course.js index 99b1338..b78f56e 100644 --- a/src/data/services/lms/fakeData/course.js +++ b/src/data/services/lms/fakeData/course.js @@ -3,4 +3,6 @@ export const number = '101'; export const title = 'Time Travel 101'; export const courseId = 'course-v1:Foo+TT101+R0'; -export default { org, number, title, courseId }; +export default { + org, number, title, courseId, +}; diff --git a/src/data/services/lms/fakeData/index.js b/src/data/services/lms/fakeData/index.js index 796152e..ba8d002 100644 --- a/src/data/services/lms/fakeData/index.js +++ b/src/data/services/lms/fakeData/index.js @@ -4,15 +4,6 @@ import oraMetadata from './ora'; import courseMetadata from './course'; import ids from './ids'; -console.log({ - submissions, - oraMetadata, - courseMetadata, - mockSubmission, - mockSubmissionStatus, - ids, -}); - export default { submissions, oraMetadata, diff --git a/src/data/services/lms/fakeData/ora.js b/src/data/services/lms/fakeData/ora.js index eab8886..31f522b 100644 --- a/src/data/services/lms/fakeData/ora.js +++ b/src/data/services/lms/fakeData/ora.js @@ -1,4 +1,3 @@ -import ids from './ids'; export const prompt = ` Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin scelerisque finibus sem in aliquam. Cras volutpat ipsum sit amet porttitor bibendum. Nunc tempor ex neque, sed faucibus nisl accumsan vitae. Proin et sem nisl. Aenean placerat justo a ligula eleifend, in imperdiet sem sodales. Maecenas eget aliquet purus, ac ornare risus. Nullam eget interdum erat. Mauris semper porta sapien et egestas. Aliquam viverra convallis pulvinar. Aliquam suscipit ligula felis, eu viverra ligula dignissim ut. Vivamus sit amet commodo sem. Nullam a viverra nibh. diff --git a/src/data/services/lms/utils.js b/src/data/services/lms/utils.js index 562da3d..9e01ee4 100644 --- a/src/data/services/lms/utils.js +++ b/src/data/services/lms/utils.js @@ -1,6 +1,5 @@ import queryString from 'query-string'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; -import { filters } from 'data/constants/filters'; /** * get(url) @@ -26,17 +25,3 @@ export const stringifyUrl = (url, query) => queryString.stringifyUrl( { url, query }, { skipNull: true, skipEmptyString: true }, ); - -/** - * filterQuery(options) - * Takes current filter object and returns it with only valid filters that are - * set and have non-'All' values - * @param {object} options - filter values - * @return {object} - valid filters that are set and do not equal 'All' - */ -export const filterQuery = (options) => Object.values(filters) - .filter(filter => options[filter] && options[filter] !== 'All') - .reduce( - (obj, filter) => ({ ...obj, [filter]: options[filter] }), - {}, - ); diff --git a/src/data/services/lms/utils.test.js b/src/data/services/lms/utils.test.js index 7aec18c..a033bba 100644 --- a/src/data/services/lms/utils.test.js +++ b/src/data/services/lms/utils.test.js @@ -1,6 +1,5 @@ import queryString from 'query-string'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; -import { filters } from 'data/constants/filters'; import * as utils from './utils'; jest.mock('query-string', () => ({ @@ -37,61 +36,4 @@ describe('lms service utils', () => { ); }); }); - describe('filterQuery', () => { - it('returns all filters included in validated list that are not "All"', () => { - const goodOptions = { - [filters.assignmentType]: 'quiz', - [filters.courseGradeMax]: 100, - [filters.courseGradeMin]: 1, - }; - const extraOptions = { - fake: 'option', - another: 'fake one', - }; - expect(utils.filterQuery({ - ...goodOptions, - ...extraOptions, - [filters.includeCourseRoleMembers]: 'All', - })).toEqual(goodOptions); - }); - }); }); - -/** - * get(url) - * simple wrapper providing an authenticated Http client get action - * @param {string} url - target url - */ -export const get = (...args) => getAuthenticatedHttpClient().get(...args); -/** - * post(url, data) - * simple wrapper providing an authenticated Http client post action - * @param {string} url - target url - * @param {object|string} data - post payload - */ -export const post = (...args) => getAuthenticatedHttpClient().post(...args); - -/** - * stringifyUrl(url, query) - * simple wrapper around queryString.stringifyUrl that sets skip behavior - * @param {string} url - base url string - * @param {object} query - query parameters - */ -export const stringifyUrl = (url, query) => queryString.stringifyUrl( - { url, query }, - { skipNull: true, skipEmptyString: true }, -); - -/** - * filterQuery(options) - * Takes current filter object and returns it with only valid filters that are - * set and have non-'All' values - * @param {object} options - filter values - * @return {object} - valid filters that are set and do not equal 'All' - */ -export const filterQuery = (options) => Object.values(filters) - .filter(filter => options[filter] && options[filter] !== 'All') - .reduce( - (obj, filter) => ({ ...obj, [filter]: options[filter] }), - {}, - ); diff --git a/src/data/store.js b/src/data/store.js index 8ca7d2f..43b278e 100755 --- a/src/data/store.js +++ b/src/data/store.js @@ -3,8 +3,6 @@ import thunkMiddleware from 'redux-thunk'; import { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProduction'; import { createLogger } from 'redux-logger'; -import fakeData from './services/lms/fakeData'; - import actions from './actions'; import selectors from './selectors'; import reducers from './reducers'; diff --git a/src/setupTest.js b/src/setupTest.js index 5b06656..89fdeab 100755 --- a/src/setupTest.js +++ b/src/setupTest.js @@ -5,100 +5,16 @@ import '@testing-library/jest-dom/extend-expect'; import Enzyme from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; -import AppProvider from '@edx/frontend-platform/react/AppProvider'; -import { IntlProvider } from 'react-intl'; -import { render as rtlRender } from '@testing-library/react'; -import PropTypes from 'prop-types'; - -import { getConfig, mergeConfig } from '@edx/frontend-platform'; -import { configure as configureAuth, MockAuthService } from '@edx/frontend-platform/auth'; -import { configure as configureI18n } from '@edx/frontend-platform/i18n'; - -import appMessages from './i18n'; -import { messages as footerMessages } from '@edx/frontend-component-footer'; - Enzyme.configure({ adapter: new Adapter() }); jest.mock('@edx/frontend-platform/i18n', () => { const i18n = jest.requireActual('@edx/frontend-platform/i18n'); - const PropTypes = jest.requireActual('prop-types'); return { ...i18n, - intlShape: PropTypes.shape({ + intlShape: jest.requireActual('prop-types').shape({ formatMessage: jest.fn(msg => msg.defaultMessage), }), defineMessages: m => m, FormattedMessage: () => 'FormattedMessage', }; }); - -export const authenticatedUser = { - userId: 'abc123', - username: 'Mock User', - roles: [], - administrator: false, -}; - -export function initializeMockApp() { - mergeConfig({ - CONTACT_URL: process.env.CONTACT_URL || null, - INSIGHTS_BASE_URL: process.env.INSIGHTS_BASE_URL || null, - STUDIO_BASE_URL: process.env.STUDIO_BASE_URL || null, - TWITTER_URL: process.env.TWITTER_URL || null, - authenticatedUser: { - userId: 'abc123', - username: 'Mock User', - roles: [], - administrator: false, - }, - SUPPORT_URL_ID_VERIFICATION: 'http://example.com', - }); - - const authService = configureAuth(MockAuthService, { - config: getConfig() - }); - - // i18n doesn't have a service class to return. - configureI18n({ - config: getConfig(), - messages: [appMessages, footerMessages], - requireAuthenticatedUser: true, - }); - - return { authService }; -} - - - -function render( - ui, - { - store = null, - ...renderOptions - } = {}, -) { - function Wrapper({ children }) { - return ( - // eslint-disable-next-line react/jsx-filename-extension - - - {children} - - - ); - } - - Wrapper.propTypes = { - children: PropTypes.node.isRequired, - }; - - return rtlRender(ui, { wrapper: Wrapper, ...renderOptions }); -} - -// Re-export everything. -export * from '@testing-library/react'; - -// Override `render` method. -export { - render, -};