diff --git a/.env b/.env index d57909ef..1c57b441 100644 --- a/.env +++ b/.env @@ -30,3 +30,5 @@ ENTERPRISE_MARKETING_URL='' ENTERPRISE_MARKETING_UTM_SOURCE='' ENTERPRISE_MARKETING_UTM_CAMPAIGN='' ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM='' +APP_ID='' +MFE_CONFIG_API_URL='' diff --git a/.env.development b/.env.development index a0225609..d037e36a 100644 --- a/.env.development +++ b/.env.development @@ -37,3 +37,5 @@ ENTERPRISE_MARKETING_URL='http://example.com' ENTERPRISE_MARKETING_UTM_SOURCE='example.com' ENTERPRISE_MARKETING_UTM_CAMPAIGN='example.com Referral' ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM='Footer' +APP_ID='' +MFE_CONFIG_API_URL='' diff --git a/package-lock.json b/package-lock.json index 8c3c26f3..20b4ee5c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@edx/brand": "npm:@edx/brand-edx.org@^1.3.2", "@edx/frontend-component-footer": "^11.1.1", "@edx/frontend-component-header": "^3.1.1", - "@edx/frontend-platform": "2.3.0", + "@edx/frontend-platform": "2.5.0", "@edx/paragon": "19.6.0", "@fortawesome/fontawesome-svg-core": "^1.2.25", "@fortawesome/free-brands-svg-icons": "^5.11.2", @@ -31,6 +31,7 @@ "query-string": "6.13.0", "react": "16.14.0", "react-dom": "16.14.0", + "react-helmet": "^6.1.0", "react-intl": "^2.9.0", "react-redux": "^7.1.1", "react-router": "5.2.0", @@ -3986,9 +3987,9 @@ } }, "node_modules/@edx/frontend-platform": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-2.3.0.tgz", - "integrity": "sha512-vZAw3eKJgUvD3wu8QOlCbNvuhe9YOGhdVuiTiFGMJKsYagJNMuQZxTJ2DwPCr7/gprJ65mboisJ3BF5IoFzVJA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-2.5.0.tgz", + "integrity": "sha512-Ws40TMkxrF9Fz71K8bqp+qui7kXYOBvl8+PYLa1K0lmzwD70FFU73mQBTvgTJKKWcw8VsjK9oJCxmjGvz6Qe1Q==", "dependencies": { "@cospired/i18n-iso-languages": "2.2.0", "@formatjs/intl-pluralrules": "^4.3.3", @@ -4014,7 +4015,7 @@ "transifex-utils.js": "i18n/scripts/transifex-utils.js" }, "peerDependencies": { - "@edx/paragon": ">= 10.0.0 < 20.0.0", + "@edx/paragon": ">= 10.0.0 < 21.0.0", "prop-types": "^15.7.2", "react": "^16.9.0", "react-dom": "^16.9.0", @@ -28541,6 +28542,20 @@ } } }, + "node_modules/react-helmet": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-6.1.0.tgz", + "integrity": "sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==", + "dependencies": { + "object-assign": "^4.1.1", + "prop-types": "^15.7.2", + "react-fast-compare": "^3.1.1", + "react-side-effect": "^2.1.0" + }, + "peerDependencies": { + "react": ">=16.3.0" + } + }, "node_modules/react-intl": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/react-intl/-/react-intl-2.9.0.tgz", @@ -28803,6 +28818,14 @@ "isarray": "0.0.1" } }, + "node_modules/react-side-effect": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.2.tgz", + "integrity": "sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==", + "peerDependencies": { + "react": "^16.3.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-style-singleton": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.1.1.tgz", @@ -37680,9 +37703,9 @@ } }, "@edx/frontend-platform": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-2.3.0.tgz", - "integrity": "sha512-vZAw3eKJgUvD3wu8QOlCbNvuhe9YOGhdVuiTiFGMJKsYagJNMuQZxTJ2DwPCr7/gprJ65mboisJ3BF5IoFzVJA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-2.5.0.tgz", + "integrity": "sha512-Ws40TMkxrF9Fz71K8bqp+qui7kXYOBvl8+PYLa1K0lmzwD70FFU73mQBTvgTJKKWcw8VsjK9oJCxmjGvz6Qe1Q==", "requires": { "@cospired/i18n-iso-languages": "2.2.0", "@formatjs/intl-pluralrules": "^4.3.3", @@ -56733,6 +56756,17 @@ "use-sidecar": "^1.0.5" } }, + "react-helmet": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-6.1.0.tgz", + "integrity": "sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==", + "requires": { + "object-assign": "^4.1.1", + "prop-types": "^15.7.2", + "react-fast-compare": "^3.1.1", + "react-side-effect": "^2.1.0" + } + }, "react-intl": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/react-intl/-/react-intl-2.9.0.tgz", @@ -56936,6 +56970,12 @@ } } }, + "react-side-effect": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.2.tgz", + "integrity": "sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==", + "requires": {} + }, "react-style-singleton": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.1.1.tgz", diff --git a/package.json b/package.json index 5abf27f5..31e4cbf6 100755 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "@edx/brand": "npm:@edx/brand-edx.org@^1.3.2", "@edx/frontend-component-footer": "^11.1.1", "@edx/frontend-component-header": "^3.1.1", - "@edx/frontend-platform": "2.3.0", + "@edx/frontend-platform": "2.5.0", "@edx/paragon": "19.6.0", "@fortawesome/fontawesome-svg-core": "^1.2.25", "@fortawesome/free-brands-svg-icons": "^5.11.2", @@ -52,6 +52,7 @@ "query-string": "6.13.0", "react": "16.14.0", "react-dom": "16.14.0", + "react-helmet": "^6.1.0", "react-intl": "^2.9.0", "react-redux": "^7.1.1", "react-router": "5.2.0", diff --git a/src/App.jsx b/src/App.jsx index d1cc74f1..f154c6ae 100755 --- a/src/App.jsx +++ b/src/App.jsx @@ -10,9 +10,11 @@ import { routePath } from 'data/constants/app'; import store from 'data/store'; import GradebookPage from 'containers/GradebookPage'; import './App.scss'; +import Head from './head/Head'; const App = () => ( +
diff --git a/src/App.test.jsx b/src/App.test.jsx index 1985f473..91aadf4d 100644 --- a/src/App.test.jsx +++ b/src/App.test.jsx @@ -12,6 +12,7 @@ import store from 'data/store'; import GradebookPage from 'containers/GradebookPage'; import App from './App'; +import Head from './head/Head'; jest.mock('react-router-dom', () => ({ BrowserRouter: () => 'BrowserRouter', @@ -41,7 +42,7 @@ describe('App router component', () => { beforeEach(() => { process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG = logo; el = shallow(); - router = el.childAt(0); + router = el.childAt(1); }); describe('AppProvider', () => { test('AppProvider is the parent component, passed the redux store props', () => { @@ -49,8 +50,13 @@ describe('App router component', () => { expect(el.props().store).toEqual(store); }); }); - describe('Router', () => { + describe('Head', () => { test('first child of AppProvider', () => { + expect(el.childAt(0).type()).toBe(Head); + }); + }); + describe('Router', () => { + test('second child of AppProvider', () => { expect(router.type()).toBe(Router); }); test('Header is above/outside-of the routing', () => { diff --git a/src/__snapshots__/App.test.jsx.snap b/src/__snapshots__/App.test.jsx.snap index fb1a22ac..5a87d300 100644 --- a/src/__snapshots__/App.test.jsx.snap +++ b/src/__snapshots__/App.test.jsx.snap @@ -4,6 +4,7 @@ exports[`App router component snapshot 1`] = ` +
diff --git a/src/head/Head.jsx b/src/head/Head.jsx new file mode 100644 index 00000000..f7513d4b --- /dev/null +++ b/src/head/Head.jsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { Helmet } from 'react-helmet'; +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import { getConfig } from '@edx/frontend-platform'; + +import messages from './messages'; + +const Head = ({ intl }) => ( + + + {intl.formatMessage(messages['gradebook.page.title'], { siteName: getConfig().SITE_NAME })} + + + +); + +Head.propTypes = { + intl: intlShape.isRequired, +}; + +export default injectIntl(Head); diff --git a/src/head/Head.test.jsx b/src/head/Head.test.jsx new file mode 100644 index 00000000..c0d9e5f7 --- /dev/null +++ b/src/head/Head.test.jsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; +import { Helmet } from 'react-helmet'; +import { mount } from 'enzyme'; +import { getConfig } from '@edx/frontend-platform'; +import Head from './Head'; + +describe('Head', () => { + const props = {}; + it('should match render title tag and favicon with the site configuration values', () => { + mount(); + const helmet = Helmet.peek(); + expect(helmet.title).toEqual(`Gradebook | ${getConfig().SITE_NAME}`); + expect(helmet.linkTags[0].rel).toEqual('shortcut icon'); + expect(helmet.linkTags[0].href).toEqual(getConfig().FAVICON_URL); + }); +}); diff --git a/src/head/messages.js b/src/head/messages.js new file mode 100644 index 00000000..62d5484b --- /dev/null +++ b/src/head/messages.js @@ -0,0 +1,11 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + 'gradebook.page.title': { + id: 'gradebook.page.title', + defaultMessage: 'Gradebook | {siteName}', + description: 'Title tag', + }, +}); + +export default messages; diff --git a/src/setupTest.js b/src/setupTest.js index 83dca80b..478a13fd 100755 --- a/src/setupTest.js +++ b/src/setupTest.js @@ -8,6 +8,8 @@ Enzyme.configure({ adapter: new Adapter() }); // These configuration values are usually set in webpack's EnvironmentPlugin however // Jest does not use webpack so we need to set these so for testing process.env.LMS_BASE_URL = 'http://localhost:18000'; +process.env.SITE_NAME = 'localhost'; +process.env.FAVICON_URL = 'http://localhost:18000/favicon.ico'; jest.mock('@edx/frontend-platform/i18n', () => { const i18n = jest.requireActual('@edx/frontend-platform/i18n');