Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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.
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