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.test
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ PARAGON_THEME_URLS={}
HOMEPAGE_PROMO_VIDEO_YOUTUBE_ID='test-youtube-id'
ENABLE_COURSE_DISCOVERY=true
ENABLE_PROGRAMS=true
INFO_EMAIL=support@example.com
20 changes: 10 additions & 10 deletions src/App.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { mockCourseDiscoveryResponse } from './catalog/__mocks__';
import { mockCourseListSearchResponse } from './__mocks__';
import messages from './catalog/messages';
import { useCourseDiscovery } from './catalog/data/hooks';
import { useCourseListSearch } from './data/course-list-search/hooks';
import {
render, within, waitFor, screen,
} from './setupTest';
Expand All @@ -16,11 +16,11 @@ jest.mock('@edx/frontend-platform', () => ({
})),
}));

jest.mock('./catalog/data/hooks', () => ({
useCourseDiscovery: jest.fn(),
jest.mock('./data/course-list-search/hooks', () => ({
useCourseListSearch: jest.fn(),
}));

const mockCourseDiscovery = useCourseDiscovery as jest.Mock;
const mockCourseListSearch = useCourseListSearch as jest.Mock;

jest.mock('@edx/frontend-platform/react', () => ({
AppProvider: ({ children }: { children: React.ReactNode }) => <div data-testid="app-provider">{children}</div>,
Expand All @@ -39,8 +39,8 @@ describe('App', () => {
document.body.innerHTML = '';
});

mockCourseDiscovery.mockReturnValue({
data: mockCourseDiscoveryResponse,
mockCourseListSearch.mockReturnValue({
data: mockCourseListSearchResponse,
isLoading: false,
isError: false,
});
Expand Down Expand Up @@ -70,16 +70,16 @@ describe('App', () => {
screen.getByText(
messages.totalCoursesHeading.defaultMessage.replace(
'{totalCourses}',
mockCourseDiscoveryResponse.results.length,
mockCourseListSearchResponse.results.length,
),
),
).toBeInTheDocument();

const courseCards = screen.getAllByRole('link');
expect(courseCards.length).toBe(mockCourseDiscoveryResponse.results.length);
expect(courseCards.length).toBe(mockCourseListSearchResponse.results.length);

courseCards.forEach((card, index) => {
const course = mockCourseDiscoveryResponse.results[index];
const course = mockCourseListSearchResponse.results[index];
const cardContent = within(card);

expect(card).toHaveAttribute('href', `/courses/${course.id}/about`);
Expand Down
7 changes: 5 additions & 2 deletions src/__mocks__/course.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
export const mockCourseResponse = {
import { Course } from '@src/generic/course-card/types';

export const mockCourseResponse: Course = {
id: 'course-v1:edX+DemoX+Demo_Course',
data: {
id: 'course-v1:edX+DemoX+Demo_Course',
course: 'Demo Course',
start: '2024-04-01T00:00:00Z',
imageUrl: '/asset-v1:edX+DemoX+Demo_Course+type@asset+block@course_image.jpg',
org: 'edX',
orgImg: '/asset-v1:edX+DemoX+Demo_Course+type@asset+block@org_image.jpg',
orgImageUrl: '/asset-v1:edX+DemoX+Demo_Course+type@asset+block@org_image.jpg',
advertisedStart: 'Winter 2025',
content: {
displayName: 'Demonstration Course',
overview: 'Course overview',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const mockCourseDiscoveryResponse = {
export const mockCourseListSearchResponse = {
took: 1,
total: 3,
results: [
Expand All @@ -18,6 +18,8 @@ export const mockCourseDiscoveryResponse = {
start: '2030-01-01T00:00:00',
number: '123',
org: 'OpenEdx',
orgImageUrl: '/asset-v1:OpenEdx+123+2023+type@asset+block@org_image.jpg',
advertisedStart: 'Winter 2025',
modes: [
'audit',
],
Expand All @@ -41,6 +43,8 @@ export const mockCourseDiscoveryResponse = {
start: '2030-01-01T00:00:00',
number: '312',
org: 'OpenEdx',
orgImageUrl: '/asset-v1:OpenEdx+312+2024+type@asset+block@org_image.jpg',
advertisedStart: 'Winter 2025',
modes: [
'audit',
],
Expand All @@ -64,6 +68,8 @@ export const mockCourseDiscoveryResponse = {
start: '2030-01-01T00:00:00',
number: '654',
org: 'dev',
orgImageUrl: '/asset-v1:dev+654+2024+type@asset+block@org_image.jpg',
advertisedStart: 'Winter 2025',
modes: [
'audit',
],
Expand Down
1 change: 1 addition & 0 deletions src/__mocks__/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { mockCourseResponse } from './course';
export { mockCourseListSearchResponse } from './courseListSearch';
9 changes: 0 additions & 9 deletions src/assets/images/no-org-image.svg

This file was deleted.

24 changes: 12 additions & 12 deletions src/catalog/CatalogPage.test.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import { render, within, screen } from '../setupTest';
import { useCourseDiscovery } from './data/hooks';
import { mockCourseDiscoveryResponse } from './__mocks__';
import { useCourseListSearch } from '../data/course-list-search/hooks';
import { mockCourseListSearchResponse } from '../__mocks__';
import CatalogPage from './CatalogPage';
import messages from './messages';

jest.mock('./data/hooks', () => ({
useCourseDiscovery: jest.fn(),
jest.mock('../data/course-list-search/hooks', () => ({
useCourseListSearch: jest.fn(),
}));

const mockUseCourseDiscovery = useCourseDiscovery as jest.Mock;
const mockUseCourseListSearch = useCourseListSearch as jest.Mock;

describe('CatalogPage', () => {
beforeEach(() => jest.clearAllMocks());

it('should show loading state', () => {
mockUseCourseDiscovery.mockReturnValue({
mockUseCourseListSearch.mockReturnValue({
isLoading: true,
isError: false,
data: null,
Expand All @@ -25,11 +25,11 @@ describe('CatalogPage', () => {
});

it('should show empty courses state', () => {
mockUseCourseDiscovery.mockReturnValue({
mockUseCourseListSearch.mockReturnValue({
isLoading: false,
isError: false,
data: {
...mockCourseDiscoveryResponse,
...mockCourseListSearchResponse,
results: [],
},
});
Expand All @@ -42,19 +42,19 @@ describe('CatalogPage', () => {
});

it('should display courses when data is available', () => {
mockUseCourseDiscovery.mockReturnValue({
mockUseCourseListSearch.mockReturnValue({
isLoading: false,
isError: false,
data: mockCourseDiscoveryResponse,
data: mockCourseListSearchResponse,
});

render(<CatalogPage />);
expect(screen.getByText(
messages.totalCoursesHeading.defaultMessage.replace('{totalCourses}', mockCourseDiscoveryResponse.results.length),
messages.totalCoursesHeading.defaultMessage.replace('{totalCourses}', mockCourseListSearchResponse.results.length),
)).toBeInTheDocument();

// Verify all courses are displayed
mockCourseDiscoveryResponse.results.forEach(course => {
mockCourseListSearchResponse.results.forEach(course => {
expect(screen.getByText(course.data.content.displayName)).toBeInTheDocument();
});
});
Expand Down
4 changes: 2 additions & 2 deletions src/catalog/CatalogPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { useIntl } from '@edx/frontend-platform/i18n';
import {
AlertNotification, CourseCard, Loading, SubHeader,
} from '../generic';
import { useCourseDiscovery } from './data/hooks';
import { useCourseListSearch } from '../data/course-list-search/hooks';
import messages from './messages';

const GRID_LAYOUT = { xl: [{ span: 9 }, { span: 3 }] };
Expand All @@ -19,7 +19,7 @@ const CatalogPage = () => {
data: courseData,
isLoading,
isError,
} = useCourseDiscovery();
} = useCourseListSearch();

if (isLoading) {
return (
Expand Down
1 change: 0 additions & 1 deletion src/catalog/__mocks__/index.ts

This file was deleted.

24 changes: 0 additions & 24 deletions src/catalog/data/api.ts

This file was deleted.

15 changes: 0 additions & 15 deletions src/catalog/data/hooks.ts

This file was deleted.

2 changes: 2 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@ export const DEFAULT_VIDEO_MODAL_HEIGHT = 500;
export const DEFAULT_VIDEO_MODAL_WIDTH = 'auto';

export const DEFAULT_VIDEO_MODAL_SIZE = 'lg';

export const DATE_FORMAT_OPTIONS = { month: 'short', day: 'numeric', year: 'numeric' } as const;
Original file line number Diff line number Diff line change
Expand Up @@ -2,61 +2,61 @@ import { ReactNode } from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';

import { renderHook, waitFor } from '../../../setupTest';
import { mockCourseDiscoveryResponse } from '../../__mocks__';
import { fetchCourseDiscovery } from '../api';
import { useCourseDiscovery } from '../hooks';
import { DEFAULT_PAGE_SIZE, DEFAULT_PAGE_INDEX } from '../constants';
import { getCourseDiscoveryUrl } from '../urls';
import { renderHook, waitFor } from '@src/setupTest';
import { mockCourseListSearchResponse } from '@src/__mocks__';
import { fetchCourseListSearch } from '../api';
import { useCourseListSearch } from '../hooks';
import { getCourseListSearchUrl } from '../urls';

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

const mockGetAuthenticatedHttpClient = getAuthenticatedHttpClient as jest.Mock;

describe('Course Discovery Data Layer', () => {
describe('fetchCourseDiscovery', () => {
const CUSTOM_PAGE_SIZE = 21;
const CUSTOM_PAGE_INDEX = 2;

describe('Course List Search Data Layer', () => {
describe('fetchCourseListSearch', () => {
beforeEach(() => jest.clearAllMocks());

it('should fetch course discovery data with default parameters', async () => {
const mockPost = jest.fn().mockResolvedValue({ data: mockCourseDiscoveryResponse });
it('should fetch course list search data with default parameters', async () => {
const mockPost = jest.fn().mockResolvedValue({ data: mockCourseListSearchResponse });
mockGetAuthenticatedHttpClient.mockReturnValue({ post: mockPost });

const result = await fetchCourseDiscovery();
const result = await fetchCourseListSearch();

expect(mockPost).toHaveBeenCalledWith(getCourseDiscoveryUrl(), {
page_size: DEFAULT_PAGE_SIZE,
page_index: DEFAULT_PAGE_INDEX,
});
expect(result).toEqual(mockCourseDiscoveryResponse);
expect(mockPost).toHaveBeenCalledTimes(1);
const [url] = mockPost.mock.calls[0];
expect(url).toBe(getCourseListSearchUrl());
expect(result).toEqual(mockCourseListSearchResponse);
});

it('should fetch course discovery data with custom parameters', async () => {
const mockPost = jest.fn().mockResolvedValue({ data: mockCourseDiscoveryResponse });
it('should fetch course list search data with custom parameters', async () => {
const mockPost = jest.fn().mockResolvedValue({ data: mockCourseListSearchResponse });
mockGetAuthenticatedHttpClient.mockReturnValue({ post: mockPost });

const customPageSize = 21;
const customPageIndex = 2;
await fetchCourseListSearch(CUSTOM_PAGE_SIZE, CUSTOM_PAGE_INDEX, true);

await fetchCourseDiscovery(customPageSize, customPageIndex);
const [url, formData] = mockPost.mock.calls[0];

expect(mockPost).toHaveBeenCalledWith(getCourseDiscoveryUrl(), {
page_size: customPageSize,
page_index: customPageIndex,
});
expect(url).toBe(getCourseListSearchUrl());
expect((formData as FormData).get('page_size')).toBe(String(CUSTOM_PAGE_SIZE));
expect((formData as FormData).get('page_index')).toBe(String(CUSTOM_PAGE_INDEX));
expect((formData as FormData).get('enable_course_sorting_by_start_date')).toBe('true');
});

it('should handle API errors', async () => {
const error = new Error('API Error');
const mockPost = jest.fn().mockRejectedValue(error);
mockGetAuthenticatedHttpClient.mockReturnValue({ post: mockPost });

await expect(fetchCourseDiscovery()).rejects.toThrow('API Error');
await expect(fetchCourseListSearch()).rejects.toThrow('API Error');
});
});

describe('useCourseDiscovery', () => {
describe('useCourseListSearch', () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
Expand All @@ -77,26 +77,26 @@ describe('Course Discovery Data Layer', () => {
});

it('should return loading state initially', () => {
const mockPost = jest.fn().mockResolvedValue({ data: mockCourseDiscoveryResponse });
const mockPost = jest.fn().mockResolvedValue({ data: mockCourseListSearchResponse });
mockGetAuthenticatedHttpClient.mockReturnValue({ post: mockPost });

const { result } = renderHook(() => useCourseDiscovery(), { wrapper });
const { result } = renderHook(() => useCourseListSearch(), { wrapper });

expect(result.current.isLoading).toBe(true);
expect(result.current.data).toBeUndefined();
});

it('should return data when fetch is successful', async () => {
const mockPost = jest.fn().mockResolvedValue({ data: mockCourseDiscoveryResponse });
const mockPost = jest.fn().mockResolvedValue({ data: mockCourseListSearchResponse });
mockGetAuthenticatedHttpClient.mockReturnValue({ post: mockPost });

const { result } = renderHook(() => useCourseDiscovery(), { wrapper });
const { result } = renderHook(() => useCourseListSearch(), { wrapper });

await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});

expect(result.current.data).toEqual(mockCourseDiscoveryResponse);
expect(result.current.data).toEqual(mockCourseListSearchResponse);
expect(result.current.isError).toBe(false);
});

Expand All @@ -105,7 +105,7 @@ describe('Course Discovery Data Layer', () => {
const mockPost = jest.fn().mockRejectedValue(error);
mockGetAuthenticatedHttpClient.mockReturnValue({ post: mockPost });

const { result } = renderHook(() => useCourseDiscovery(), { wrapper });
const { result } = renderHook(() => useCourseListSearch(), { wrapper });

await waitFor(() => {
expect(result.current.isLoading).toBe(false);
Expand Down
Loading