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 @@ -4,6 +4,7 @@ BASE_URL=''
CREDENTIALS_BASE_URL=''
CSRF_TOKEN_API_PATH=''
ECOMMERCE_BASE_URL=''
LEARNING_BASE_URL=''
LANGUAGE_PREFERENCE_COOKIE_NAME=''
LMS_BASE_URL=''
LOGIN_URL=''
Expand Down
1 change: 1 addition & 0 deletions .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ BASE_URL='http://localhost:1998'
CREDENTIALS_BASE_URL='http://localhost:18150'
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
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'
LOGIN_URL='http://localhost:18000/login'
Expand Down
1 change: 1 addition & 0 deletions .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ BASE_URL='http://localhost:1998'
CREDENTIALS_BASE_URL='http://localhost:18150'
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
ECOMMERCE_BASE_URL='http://localhost:18130'
LEARNING_BASE_URL='http://localhost:2000'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[question] Should we add empty or pre-defined LEARNING_BASE_URL settings to .env and .env.development

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added, thanks

LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
LMS_BASE_URL='http://localhost:18000'
LOGIN_URL='http://localhost:18000/login'
Expand Down
3 changes: 2 additions & 1 deletion .stylelintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"ignoreProperties": ["xs", "sm", "md", "lg", "xl", "xxl"]
}],
"alpha-value-notation": "number",
"string-quotes": "double"
"string-quotes": "double",
"no-missing-end-of-source-newline": true
Comment thread
brian-smith-tcril marked this conversation as resolved.
}
}
39 changes: 34 additions & 5 deletions src/App.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { mockCourseListSearchResponse } from './__mocks__';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';

import { mockCourseListSearchResponse, mockCourseAboutResponse } from './__mocks__';
import messages from './catalog/messages';
import { useCourseListSearch } from './data/course-list-search/hooks';
import { useCourseAboutData } from './course-about/data/hooks';
import courseAboutIntroMessages from './course-about/course-intro/messages';
import {
render, within, waitFor, screen,
} from './setupTest';
Expand All @@ -20,15 +24,17 @@ jest.mock('./data/course-list-search/hooks', () => ({
useCourseListSearch: jest.fn(),
}));

jest.mock('./header/hooks/useMenuItems', () => ({
useMenuItems: jest.fn(() => ([])),
jest.mock('./course-about/data/hooks', () => ({
useCourseAboutData: jest.fn(),
useEnrollment: jest.fn(() => jest.fn()),
}));

jest.mock('./header/hooks/useMenuItems', () => ({
useMenuItems: jest.fn(() => ([])),
}));

const mockCourseListSearch = useCourseListSearch as jest.Mock;
const mockCourseAbout = useCourseAboutData as jest.Mock;

jest.mock('@edx/frontend-platform/react', () => ({
AppProvider: ({ children }: { children: React.ReactNode }) => <div data-testid="app-provider">{children}</div>,
Expand All @@ -42,9 +48,15 @@ jest.mock('@edx/frontend-component-footer', () => ({
FooterSlot: () => <div data-testid="footer" />,
}));

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

describe('App', () => {
beforeEach(() => {
document.body.innerHTML = '';
(getAuthenticatedUser as jest.Mock).mockReturnValue(null);
jest.clearAllMocks();
});

mockCourseListSearch.mockReturnValue({
Expand All @@ -53,6 +65,12 @@ describe('App', () => {
isError: false,
});

mockCourseAbout.mockReturnValue({
data: mockCourseAboutResponse,
isLoading: false,
isError: false,
});

it('renders HomePage on "/" route', async () => {
window.testHistory = [ROUTES.HOME];

Expand Down Expand Up @@ -89,11 +107,22 @@ describe('App', () => {
});
});

it('renders CourseAboutPage on "/courses/some-course-id/about"', () => {
it('renders CourseAboutPage on "/courses/some-course-id/about"', async () => {
window.testHistory = [ROUTES.COURSE_ABOUT];
const mockUser = { username: 'testuser' };
(getAuthenticatedUser as jest.Mock).mockReturnValue(mockUser);

render(<App />);
expect(screen.getByTestId('course-about-page')).toBeInTheDocument();

await waitFor(() => {
expect(screen.queryByTestId('spinner')).not.toBeInTheDocument();
});

expect(screen.getByRole('heading', { name: mockCourseAboutResponse.name })).toBeInTheDocument();
expect(screen.getByText(mockCourseAboutResponse.org)).toBeInTheDocument();
expect(screen.getByText(mockCourseAboutResponse.shortDescription)).toBeInTheDocument();
expect(screen.getByRole('button', { name: courseAboutIntroMessages.enrollNowBtn.defaultMessage })).toBeInTheDocument();
expect(screen.getByRole('img', { name: mockCourseAboutResponse.name })).toBeInTheDocument();
});

it('renders NotFoundPage on unknown route', () => {
Expand Down
97 changes: 97 additions & 0 deletions src/__mocks__/courseAbout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
export const mockCourseAboutResponse = {
accessExpiration: null,
contentTypeGatingEnabled: false,
courseGoals: {
selectedGoal: null,
weeklyLearningGoalEnabled: false,
},
effort: null,
end: null,
enrollment: {
mode: null,
isActive: false,
},
enrollmentStart: null,
enrollmentEnd: null,
entranceExamData: {
entranceExamCurrentScore: 0,
entranceExamEnabled: false,
entranceExamId: '',
entranceExamMinimumScorePct: 0.65,
entranceExamPassed: true,
},
id: 'course-v1:openedx+123+2024',
license: null,
language: 'en',
media: {
courseImage: {
uri: '/asset-v1:openedx+123+2024+type@asset+block@494a0bae6c7f4aef47b5c0f7d85414b49371151e82646cbbbc22289b4c100c5f.jpg',
},
courseVideo: {
uri: 'https://youtube.com/watch?v=test123',
},
image: {
raw: 'http://local.openedx.io:8000/asset-v1:openedx+123+2024+type@asset+block@494a0bae6c7f4aef47b5c0f7d85414b49371151e82646cbbbc22289b4c100c5f.jpg',
small: 'http://local.openedx.io:8000/asset-v1:openedx+123+2024+type@asset+block@494a0bae6c7f4aef47b5c0f7d85414b49371151e82646cbbbc22289b4c100c5f.jpg',
large: 'http://local.openedx.io:8000/asset-v1:openedx+123+2024+type@asset+block@494a0bae6c7f4aef47b5c0f7d85414b49371151e82646cbbbc22289b4c100c5f.jpg',
},
},
name: 'Test 2',
offer: null,
org: 'openedx',
relatedPrograms: null,
shortDescription: 'The first MOOC to teach positive psychology. Learn science-based principles and practices for a happy, meaningful life.',
start: '2030-01-01T00:00:00Z',
startDisplay: null,
startType: 'empty',
pacing: 'instructor',
userTimezone: null,
showCalculator: false,
canAccessProctoredExams: false,
notes: {
enabled: false,
visible: true,
},
marketingUrl: null,
celebrations: {
firstSection: false,
streakLengthToCelebrate: null,
streakDiscountEnabled: false,
weeklyGoal: false,
},
userHasPassingGrade: false,
courseExitPageIsActive: false,
certificateData: null,
verifyIdentityUrl: null,
verificationStatus: 'none',
linkedinAddToProfileUrl: null,
isIntegritySignatureEnabled: false,
userNeedsIntegritySignature: false,
learningAssistantEnabled: false,
showCoursewareLink: false,
isCourseFull: false,
canEnroll: true,
invitationOnly: false,
isShibCourse: false,
allowAnonymous: false,
ecommerceCheckout: false,
singlePaidMode: {},
ecommerceCheckoutLink: 'http://example.com/checkout',
courseImageUrls: [
'raw',
'small',
'large',
],
startDateIsStillDefault: true,
advertisedStart: null,
coursePrice: 'Free',
preRequisiteCourses: [],
sidebarHtmlEnabled: false,
courseAboutSectionHtml: null,
aboutSidebarHtml: null,
displayNumberWithDefault: '123',
displayOrgWithDefault: 'openedx',
overview: '<div>Course overview content</div>',
ocwLinks: [],
prerequisites: [],
};
1 change: 1 addition & 0 deletions src/__mocks__/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { mockCourseResponse } from './course';
export { mockCourseListSearchResponse } from './courseListSearch';
export { mockCourseAboutResponse } from './courseAbout';
2 changes: 1 addition & 1 deletion src/catalog/CatalogPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ const CatalogPage = () => {
const totalCourses = courseData?.results?.length ?? 0;

return (
<Container className="container-xl pt-5.5 mb-6">
<Container fluid={false} size="xl" className="pt-5.5 mb-6">
<SubHeader
title={intl.formatMessage(messages.exploreCourses)}
className={classNames({ 'mx-2.5': isMedium })}
Expand Down
5 changes: 5 additions & 0 deletions src/course-about/CourseAboutPage.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@import "./course-intro/course-media/CourseMedia";

.course-about-intro .pgn__card {
min-height: var(--catalog-course-about-page-intro-card-min-height, var(--catalog-course-about-page-course-media-image-height, 15.75rem));
}
162 changes: 162 additions & 0 deletions src/course-about/CourseAboutPage.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { useLocation } from 'react-router-dom';

import genericMessages from '../generic/video-modal/messages';
import {
render, waitFor, screen, userEvent,
} from '../setupTest';
import { mockCourseAboutResponse } from '../__mocks__';
import CourseAboutPage from './CourseAboutPage';
import { fetchCourseAboutData } from './data/api';
import messages from './course-intro/messages';
import courseMediaMessages from './course-intro/course-media/messages';

const mockGetAuthenticatedUser = jest.fn();

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

jest.mock('./data/api', () => ({
fetchCourseAboutData: jest.fn(),
}));

jest.mock('react-router-dom', () => ({
useLocation: jest.fn(),
}));

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

describe('CourseAboutPage Integration Tests', () => {
beforeEach(() => {
jest.clearAllMocks();
mockUseLocation.mockReturnValue({
pathname: '/catalog/course-v1:TestX+Test101+2023/about',
});
mockGetAuthenticatedUser.mockReturnValue(null);
});

it('should show loading state when data is being fetched', async () => {
mockFetchCourseAboutData.mockReturnValue(new Promise(() => {}));
render(<CourseAboutPage />);
expect(screen.getByRole('status')).toBeInTheDocument();
});

it('should render course page with all components', async () => {
mockFetchCourseAboutData.mockReturnValue(mockCourseAboutResponse);
render(<CourseAboutPage />);

await waitFor(() => {
expect(screen.getByText(mockCourseAboutResponse.name)).toBeInTheDocument();
expect(screen.getByText(mockCourseAboutResponse.displayOrgWithDefault)).toBeInTheDocument();
expect(screen.getByText(mockCourseAboutResponse.shortDescription)).toBeInTheDocument();

expect(screen.getByRole('heading', { level: 1 })).toBeInTheDocument();
expect(screen.getByAltText(mockCourseAboutResponse.name)).toBeInTheDocument();
});
});

it('should handle course with video correctly', async () => {
const courseWithVideo = {
...mockCourseAboutResponse,
media: {
...mockCourseAboutResponse.media,
courseVideo: {
uri: 'https://www.youtube.com/watch?v=test123',
},
},
};

mockFetchCourseAboutData.mockReturnValue(courseWithVideo);

render(<CourseAboutPage />);

await waitFor(() => {
const videoButton = screen.getByLabelText(courseMediaMessages.playCourseIntroductionVideo.defaultMessage);
expect(videoButton).toBeInTheDocument();
});

const videoButton = screen.getByLabelText(courseMediaMessages.playCourseIntroductionVideo.defaultMessage);

await userEvent.click(videoButton);

await waitFor(() => {
expect(screen.getByRole('dialog')).toBeInTheDocument();
expect(screen.getByTitle(genericMessages.videoIframeTitle.defaultMessage)).toBeInTheDocument();
});
});

it('should handle course without video correctly', async () => {
const courseWithoutVideo = {
...mockCourseAboutResponse,
media: {
...mockCourseAboutResponse.media,
courseVideo: {
uri: null,
},
},
};

mockFetchCourseAboutData.mockReturnValue(courseWithoutVideo);

render(<CourseAboutPage />);

await waitFor(() => {
expect(screen.queryByLabelText(
courseMediaMessages.playCourseIntroductionVideo.defaultMessage,
)).not.toBeInTheDocument();
});
});

it('should display enrollment button for non-enrolled user', async () => {
const courseData = {
...mockCourseAboutResponse,
enrollment: { isActive: false },
canEnroll: true,
};

mockFetchCourseAboutData.mockReturnValue(courseData);

render(<CourseAboutPage />);

await waitFor(() => {
expect(screen.getByRole('button', {
name: messages.enrollNowBtn.defaultMessage,
})).toBeInTheDocument();
});
});

it('should display enrolled status for enrolled user', async () => {
mockGetAuthenticatedUser.mockReturnValue({ username: 'testuser' });

const courseData = {
...mockCourseAboutResponse,
enrollment: { isActive: true },
showCoursewareLink: true,
};

mockFetchCourseAboutData.mockReturnValue(courseData);

render(<CourseAboutPage />);

await waitFor(() => {
expect(screen.getByText(messages.statusMessageEnrolled.defaultMessage)).toBeInTheDocument();
});
});

it('should handle course full scenario', async () => {
const courseData = {
...mockCourseAboutResponse,
isCourseFull: true,
canEnroll: false,
};

mockFetchCourseAboutData.mockReturnValue(courseData);

render(<CourseAboutPage />);

await waitFor(() => {
expect(screen.getByText(messages.statusMessageFull.defaultMessage)).toBeInTheDocument();
});
});
});
Loading