{headerLogo}
-
-
+
+
+ {enableOrgLogo && courseOrg && logoOrg && (
+

+ )}
+
+ {courseTitle}
+
+
+
+
+ {getConfig().ENABLE_HEADER_LANG_SELECTOR && (
+
+
+
+
+
+
+
+
+ )}
+ {showUserDropdown && authenticatedUser && (
+ <>
+
+
+ >
+ )}
+ {showUserDropdown && !authenticatedUser && (
+
+ )}
- {showUserDropdown && authenticatedUser && (
- <>
-
-
- >
- )}
- {showUserDropdown && !authenticatedUser && (
-
- )}
);
};
LearningHeader.propTypes = {
- courseOrg: courseInfoDataShape.courseOrg,
- courseNumber: courseInfoDataShape.courseNumber,
- courseTitle: courseInfoDataShape.courseTitle,
+ courseOrg: PropTypes.string,
+ courseTitle: PropTypes.string,
intl: intlShape.isRequired,
showUserDropdown: PropTypes.bool,
};
LearningHeader.defaultProps = {
courseOrg: null,
- courseNumber: null,
courseTitle: null,
showUserDropdown: true,
};
diff --git a/src/learning-header/LearningHeader.test.jsx b/src/learning-header/LearningHeader.test.jsx
index 3d80888efe..b9b75c06a7 100644
--- a/src/learning-header/LearningHeader.test.jsx
+++ b/src/learning-header/LearningHeader.test.jsx
@@ -1,9 +1,13 @@
import React from 'react';
import {
- authenticatedUser, initializeMockApp, render, screen,
+ authenticatedUser, initializeMockApp, render, screen, waitFor,
} from '../setupTest';
import { LearningHeader as Header } from '../index';
+jest.mock('./data/api', () => ({
+ getCourseLogoOrg: jest.fn().mockResolvedValue(Promise.resolve('logo-url')),
+}));
+
describe('Header', () => {
beforeAll(async () => {
// We need to mock AuthService to implicitly use `getAuthenticatedUser` within `AppContext.Provider`.
@@ -18,12 +22,15 @@ describe('Header', () => {
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();
+ waitFor(
+ () => {
+ expect(screen.getByAltText(`${courseData.courseOrg} logo`)).toHaveAttribute('src', 'logo-url');
+ expect(screen.getByText(`${courseData.courseOrg}`)).toBeInTheDocument();
+ expect(screen.getByText(courseData.courseTitle)).toBeInTheDocument();
+ },
+ );
});
});
diff --git a/src/learning-header/_header.scss b/src/learning-header/_header.scss
new file mode 100644
index 0000000000..357cc6b3a9
--- /dev/null
+++ b/src/learning-header/_header.scss
@@ -0,0 +1,10 @@
+@import "../../node_modules/bootstrap/scss/bootstrap-grid";
+@import "../../node_modules/bootstrap/scss/mixins/breakpoints";
+
+.logo {
+ img {
+ @include media-breakpoint-down(sm) {
+ max-width: 85% !important;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/learning-header/data/api.js b/src/learning-header/data/api.js
new file mode 100644
index 0000000000..cf4abc3904
--- /dev/null
+++ b/src/learning-header/data/api.js
@@ -0,0 +1,24 @@
+import { getConfig } from '@edx/frontend-platform';
+import { getAuthenticatedHttpClient, getAuthenticatedUser } from '@edx/frontend-platform/auth';
+import { logError } from '@edx/frontend-platform/logging';
+
+const getCourseLogoOrg = async () => {
+ try {
+ const orgId = window.location.pathname.match(/course-(.*?):([^+]+)/)[2];
+ const { username } = getAuthenticatedUser() ?? {};
+ if (username) {
+ const { data } = await getAuthenticatedHttpClient()
+ .get(
+ `${getConfig().LMS_BASE_URL}/api/organizations/v0/organizations/${orgId}/`,
+ { useCache: true },
+ );
+ return data.logo;
+ }
+ } catch (error) {
+ // do not throw the error, just log it, the Course Org Logo is not critical
+ logError(error);
+ }
+ return null;
+};
+
+export default getCourseLogoOrg;
diff --git a/src/learning-header/data/api.test.js b/src/learning-header/data/api.test.js
new file mode 100644
index 0000000000..fd0a47a1af
--- /dev/null
+++ b/src/learning-header/data/api.test.js
@@ -0,0 +1,94 @@
+import { logError } from '@edx/frontend-platform/logging';
+import { getAuthenticatedHttpClient, getAuthenticatedUser } from '@edx/frontend-platform/auth';
+import getCourseLogoOrg from './api';
+import { initializeMockApp } from '../../setupTest';
+
+jest.mock('@edx/frontend-platform/auth');
+jest.mock('@edx/frontend-platform/logging', () => ({
+ logError: jest.fn(),
+}));
+
+class CustomError extends Error {
+ constructor(httpErrorStatus) {
+ super();
+ this.customAttributes = {
+ httpErrorStatus,
+ };
+ }
+}
+
+describe('getCourseLogoOrg', () => {
+ beforeAll(async () => {
+ // We need to mock AuthService to implicitly use `getAuthenticatedHttpClient` within `AppContext.Provider`.
+ await initializeMockApp();
+ });
+
+ beforeEach(() => {
+ getAuthenticatedHttpClient.mockReset();
+ getAuthenticatedUser.mockReset();
+ logError.mockReset();
+ });
+
+ it('should return the organization logo when the URL is valid', async () => {
+ // Use history.pushState to change the URL
+ window.history.pushState({}, '', '/learning/course/course-v1:edX+DemoX+Demo_Course/home');
+
+ getAuthenticatedUser.mockImplementation(() => ({ username: 'someone' }));
+ const mockGet = jest.fn().mockResolvedValue({
+ data: {
+ logo: 'https://example.com/logo.svg',
+ },
+ });
+ getAuthenticatedHttpClient.mockReturnValue({
+ get: mockGet,
+ });
+ const logoOrg = await getCourseLogoOrg();
+ expect(mockGet).toHaveBeenCalledWith(
+ expect.stringContaining('/api/organizations/v0/organizations/edX/'),
+ { useCache: true },
+ );
+ expect(logoOrg).toBe('https://example.com/logo.svg');
+ });
+
+ it('should return null when the organization logo is not found', async () => {
+ window.history.pushState({}, '', '/learning/course/course-v1:edX+DemoX+Nonexistent_Course/home');
+ getAuthenticatedUser.mockImplementation(() => ({ username: 'someone' }));
+ getAuthenticatedHttpClient.mockReturnValue({
+ get: async () => {
+ throw new CustomError(404);
+ },
+ });
+ const logoOrg = await getCourseLogoOrg();
+ expect(logoOrg).toBeNull();
+ });
+
+ it('should return null if the user is not authenticated', async () => {
+ window.history.pushState({}, '', '/learning/course/course-v1:edX+DemoX+Demo_Course/home');
+ getAuthenticatedUser.mockImplementation(() => ({}));
+ getAuthenticatedHttpClient.mockReturnValue({
+ get: async () => Promise.resolve({
+ data: {
+ logo: 'https://example.com/logo.svg',
+ },
+ }),
+ });
+ const logoOrg = await getCourseLogoOrg();
+ expect(getAuthenticatedHttpClient).not.toHaveBeenCalled();
+ expect(logoOrg).toBeNull();
+ });
+
+ it('should throw an error when an unexpected error occurs', async () => {
+ window.history.pushState({}, '', '/learning/course/course-v1:edX+DemoX+Demo_Course/home');
+
+ getAuthenticatedUser.mockImplementation(() => ({ username: 'someone' }));
+ const customError = new CustomError(500);
+ getAuthenticatedHttpClient.mockReturnValue({
+ get: async () => {
+ throw customError;
+ },
+ });
+ const logoOrg = await getCourseLogoOrg();
+ expect(logoOrg).toBeNull();
+ expect(logError).toHaveBeenCalledWith(customError);
+ });
+});
diff --git a/src/studio-header/HeaderBody.tsx b/src/studio-header/HeaderBody.tsx
index 598aec3819..7837984291 100644
--- a/src/studio-header/HeaderBody.tsx
+++ b/src/studio-header/HeaderBody.tsx
@@ -80,7 +80,7 @@ const HeaderBody = ({
) : (
-
+
{renderBrandNav}