Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ ECOMMERCE_BASE_URL=''
LEARNING_BASE_URL=''
LANGUAGE_PREFERENCE_COOKIE_NAME=''
LMS_BASE_URL=''
STUDIO_BASE_URL=''
LOGIN_URL=''
LOGOUT_URL=''
LOGO_URL=''
Expand Down
1 change: 1 addition & 0 deletions .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ ECOMMERCE_BASE_URL='http://localhost:18130'
LEARNING_BASE_URL='http://localhost:2000'
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
LMS_BASE_URL='http://localhost:18000'
STUDIO_BASE_URL='http://localhost:18001'
LOGIN_URL='http://localhost:18000/login'
LOGOUT_URL='http://localhost:18000/logout'
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
Expand Down
1 change: 1 addition & 0 deletions .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ ECOMMERCE_BASE_URL='http://localhost:18130'
LEARNING_BASE_URL='http://localhost:2000'
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
LMS_BASE_URL='http://localhost:18000'
STUDIO_BASE_URL='http://localhost:18001'
LOGIN_URL='http://localhost:18000/login'
LOGOUT_URL='http://localhost:18000/logout'
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
Expand Down
134 changes: 133 additions & 1 deletion src/course-about/CourseAboutPage.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useLocation } from 'react-router-dom';
import { useMediaQuery } from '@openedx/paragon';
import { getConfig } from '@edx/frontend-platform';

import genericMessages from '../generic/video-modal/messages';
import {
Expand All @@ -13,13 +14,18 @@ import courseMediaMessages from './course-intro/course-media/messages';
import sidebarDetailsMessages from './course-sidebar/sidebar-details/messages';
import sidebarSocialMessages from './course-sidebar/sidebar-social/messages';
import { ROUTES } from '../routes';
import courseAboutMessages from './messages';

const mockGetAuthenticatedUser = jest.fn();

jest.mock('@edx/frontend-platform/auth', () => ({
getAuthenticatedUser: () => mockGetAuthenticatedUser(),
}));

jest.mock('@edx/frontend-platform', () => ({
getConfig: jest.fn(),
}));

jest.mock('./data/api', () => ({
fetchCourseAboutData: jest.fn(),
}));
Expand All @@ -34,7 +40,7 @@ jest.mock('@openedx/paragon', () => ({
}));

const mockUseMediaQuery = useMediaQuery as jest.Mock;

const mockGetConfig = getConfig as jest.Mock;
const mockFetchCourseAboutData = fetchCourseAboutData as jest.Mock;
const mockUseLocation = useLocation as jest.Mock;

Expand All @@ -46,6 +52,10 @@ describe('CourseAboutPage Integration Tests', () => {
});
mockGetAuthenticatedUser.mockReturnValue(null);
mockUseMediaQuery.mockReturnValue(false);
mockGetConfig.mockReturnValue({
LMS_BASE_URL: process.env.LMS_BASE_URL,
STUDIO_BASE_URL: process.env.STUDIO_BASE_URL,
});
});

it('should show loading state when data is being fetched', async () => {
Expand Down Expand Up @@ -412,4 +422,126 @@ describe('CourseAboutPage Integration Tests', () => {
});
});
});

describe('Course overview', () => {
it('should render course overview with content', async () => {
const courseOverviewText = 'Course overview content';
const courseData = {
...mockCourseAboutResponse,
overview: `<p>${courseOverviewText}</p>`,
};

mockFetchCourseAboutData.mockReturnValue(courseData);
render(<CourseAboutPage />);

await waitFor(() => {
expect(screen.getByText(courseOverviewText)).toBeInTheDocument();
});
});

it('should not render course overview for non-staff user when overview is empty', async () => {
const courseData = {
...mockCourseAboutResponse,
overview: '',
};

mockFetchCourseAboutData.mockReturnValue(courseData);
render(<CourseAboutPage />);

await waitFor(() => {
expect(screen.queryByRole('link', {
name: courseAboutMessages.viewAboutPageInStudio.defaultMessage,
})).not.toBeInTheDocument();
});
});

it('should process overview content to replace image paths', async () => {
const courseData = {
...mockCourseAboutResponse,
overview: '<img src="/static/images/test.jpg" alt="Test Image" />',
};

mockFetchCourseAboutData.mockReturnValue(courseData);
render(<CourseAboutPage />);

await waitFor(() => {
const img = screen.getByAltText('Test Image');
expect(img).toHaveAttribute('src', `${getConfig().LMS_BASE_URL}/static/images/test.jpg`);
});
});

it('should process overview content with asset paths', async () => {
const courseData = {
...mockCourseAboutResponse,
overview: '<img src="/asset/test.jpg" alt="Test Asset" />',
};

mockFetchCourseAboutData.mockReturnValue(courseData);
render(<CourseAboutPage />);

await waitFor(() => {
const img = screen.getByAltText('Test Asset');
expect(img).toHaveAttribute('src', `${getConfig().LMS_BASE_URL}/asset/test.jpg`);
});
});

it('should show Studio button for global staff user', async () => {
mockGetAuthenticatedUser.mockReturnValue({ administrator: true });

const courseData = {
...mockCourseAboutResponse,
overview: '<p>Course overview content</p>',
};

mockFetchCourseAboutData.mockReturnValue(courseData);
render(<CourseAboutPage />);

await waitFor(() => {
const studioButton = screen.getByRole('link', {
name: courseAboutMessages.viewAboutPageInStudio.defaultMessage,
});
expect(studioButton).toBeInTheDocument();
expect(studioButton).toHaveAttribute(
'href',
expect.stringContaining(`${getConfig().STUDIO_BASE_URL}/settings/details/`),
);
});
});

it('should hide Studio button for non-staff user', async () => {
mockGetAuthenticatedUser.mockReturnValue(null);

const courseData = {
...mockCourseAboutResponse,
overview: '<p>Course overview content</p>',
};

mockFetchCourseAboutData.mockReturnValue(courseData);
render(<CourseAboutPage />);

await waitFor(() => {
expect(screen.queryByRole('link', {
name: courseAboutMessages.viewAboutPageInStudio.defaultMessage,
})).not.toBeInTheDocument();
});
});

it('should hide Studio button for authenticated user without administrator role', async () => {
mockGetAuthenticatedUser.mockReturnValue({ username: 'testuser', administrator: false });

const courseData = {
...mockCourseAboutResponse,
overview: '<p>Course overview content</p>',
};

mockFetchCourseAboutData.mockReturnValue(courseData);
render(<CourseAboutPage />);

await waitFor(() => {
expect(screen.queryByRole('link', {
name: courseAboutMessages.viewAboutPageInStudio.defaultMessage,
})).not.toBeInTheDocument();
});
});
});
});
12 changes: 9 additions & 3 deletions src/course-about/CourseAboutPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import { useIntl } from '@edx/frontend-platform/i18n';
import { Loading } from '@src/generic';
import CourseAboutIntroSlot from '@src/plugin-slots/CourseAboutIntroSlot';
import CourseAboutCourseMediaSlot from '@src/plugin-slots/CourseAboutCourseMediaSlot';
import CourseAboutOverviewSlot from '@src/plugin-slots/CourseAboutOverviewSlot';
import { useCourseAboutData } from './data/hooks';
import CourseSidebar from './course-sidebar/CourseSidebar';
import { CourseOverview } from './course-overview';
import messages from './messages';
import { GRID_LAYOUT } from './layout';

Expand Down Expand Up @@ -53,15 +53,21 @@ const CourseAboutPage = () => {
<CourseAboutCourseMediaSlot courseAboutData={courseAboutData} />
</Layout.Element>
<CourseAboutIntroSlot courseAboutData={courseAboutData} />
<CourseOverview />
<CourseAboutOverviewSlot
overviewData={courseAboutData.overview}
courseId={courseId}
/>
<aside>
<CourseSidebar courseAboutData={courseAboutData} />
</aside>
</Stack>
) : (
<Stack gap={4}>
<CourseAboutIntroSlot courseAboutData={courseAboutData} />
<CourseOverview />
<CourseAboutOverviewSlot
overviewData={courseAboutData.overview}
courseId={courseId}
/>
</Stack>
)}
</Layout.Element>
Expand Down
117 changes: 117 additions & 0 deletions src/course-about/course-overview/CourseOverview.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { getConfig } from '@edx/frontend-platform';

import { render, screen } from '@src/setupTest';
import messages from '../messages';
import { CourseOverview } from '.';

jest.mock('@edx/frontend-platform/auth', () => ({
getAuthenticatedUser: jest.fn(),
}));

jest.mock('@edx/frontend-platform', () => ({
getAuthenticatedUser: jest.fn(() => ({ username: 'test-user', roles: [] })),
getConfig: jest.fn(),
}));

const mockGetAuthenticatedUser = getAuthenticatedUser as jest.Mock;
const mockGetConfig = getConfig as jest.Mock;

const mockCourseId = 'course-v1:TestX+Test101+2023';

describe('CourseOverview', () => {
beforeEach(() => {
jest.clearAllMocks();
mockGetConfig.mockReturnValue({
LMS_BASE_URL: process.env.LMS_BASE_URL,
STUDIO_BASE_URL: process.env.STUDIO_BASE_URL,
});
mockGetAuthenticatedUser.mockReturnValue(null);
});

describe('Content rendering', () => {
it('renders overview content when provided', () => {
const overviewText = 'Course overview content';
const overviewData = `<p>${overviewText}</p>`;

render(<CourseOverview overviewData={overviewData} courseId={mockCourseId} />);
expect(screen.getByText(overviewText)).toBeInTheDocument();
});

it('renders nothing for non-staff users', () => {
const { container } = render(<CourseOverview overviewData="" courseId={mockCourseId} />);

expect(container.firstChild).toBeNull();
});

it('renders Studio button for global staff users', () => {
mockGetAuthenticatedUser.mockReturnValue({ administrator: true });

render(<CourseOverview overviewData=" " courseId={mockCourseId} />);

const studioButton = screen.getByRole('link', {
name: messages.viewAboutPageInStudio.defaultMessage,
});

expect(studioButton).toBeInTheDocument();
expect(studioButton).toHaveAttribute(
'href',
`${getConfig().STUDIO_BASE_URL}/settings/details/${mockCourseId}`,
);
});

it('processes overview content to replace image paths', () => {
const overviewData = '<img src="/static/images/test.jpg" alt="Test" />';
render(<CourseOverview overviewData={overviewData} courseId={mockCourseId} />);

const img = screen.getByAltText('Test');
expect(img).toHaveAttribute('src', `${getConfig().LMS_BASE_URL}/static/images/test.jpg`);
});

it('processes overview content with asset paths', () => {
const overviewData = '<img src="/asset/test.jpg" alt="Test" />';
render(<CourseOverview overviewData={overviewData} courseId={mockCourseId} />);

const img = screen.getByAltText('Test');
expect(img).toHaveAttribute('src', `${getConfig().LMS_BASE_URL}/asset/test.jpg`);
});
});

describe('Global staff features', () => {
it('shows Studio button for global staff user', () => {
mockGetAuthenticatedUser.mockReturnValue({ administrator: true });
render(<CourseOverview overviewData="<p>Content</p>" courseId={mockCourseId} />);

const studioButton = screen.getByRole('link', {
name: messages.viewAboutPageInStudio.defaultMessage,
});
expect(studioButton).toBeInTheDocument();
expect(studioButton).toHaveAttribute(
'href',
`${getConfig().STUDIO_BASE_URL}/settings/details/${mockCourseId}`,
);
});

it('hides Studio button for non-staff user', () => {
mockGetAuthenticatedUser.mockReturnValue(null);
render(<CourseOverview overviewData="<p>Content</p>" courseId={mockCourseId} />);

expect(
screen.queryByRole('link', {
name: messages.viewAboutPageInStudio.defaultMessage,
}),
).not.toBeInTheDocument();
});

it('hides Studio button for authenticated user without administrator role', () => {
mockGetAuthenticatedUser.mockReturnValue({ username: 'testuser', administrator: false });
render(<CourseOverview overviewData="<p>Content</p>" courseId={mockCourseId} />);

expect(
screen.queryByRole('link', {
name: messages.viewAboutPageInStudio.defaultMessage,
}),
).not.toBeInTheDocument();
});
});
});
Loading