Skip to content
Closed
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
3 changes: 3 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,8 @@ SITE_NAME=''
USER_INFO_COOKIE_NAME=''
APP_ID='catalog'
MFE_CONFIG_API_URL=''
ENABLE_PROGRAMS=false
SUPPORT_URL=''
INFO_EMAIL=''
# Fallback in local style files
PARAGON_THEME_URLS={}
3 changes: 3 additions & 0 deletions .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,8 @@ SITE_NAME=localhost
USER_INFO_COOKIE_NAME='edx-user-info'
APP_ID='catalog'
MFE_CONFIG_API_URL=''
ENABLE_PROGRAMS=false
SUPPORT_URL=''
INFO_EMAIL='[email protected]'
# Fallback in local style files
PARAGON_THEME_URLS={}
4 changes: 4 additions & 0 deletions .env.test
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
BASE_URL='http://localhost:1998'
CREDENTIALS_BASE_URL='http://localhost:18150'
SUPPORT_URL='http://support.example.com'
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
ECOMMERCE_BASE_URL='http://localhost:18130'
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
Expand All @@ -19,4 +20,7 @@ SITE_NAME=localhost
USER_INFO_COOKIE_NAME='edx-user-info'
APP_ID='catalog'
MFE_CONFIG_API_URL=''
ENABLE_PROGRAMS=false
SUPPORT_URL='[email protected]'
INFO_EMAIL=''
PARAGON_THEME_URLS={}
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ module.exports = createConfig(
'no-restricted-exports': 'off',
// There is no reason to disallow this syntax anymore; we don't use regenerator-runtime in new browsers
'no-restricted-syntax': 'off',
'react/prop-types': 'off',
},
},
);
3 changes: 2 additions & 1 deletion .stylelintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"property-no-unknown": [true, {
"ignoreProperties": ["xs", "sm", "md", "lg", "xl", "xxl"]
}],
"alpha-value-notation": "number"
"alpha-value-notation": "number",
"string-quotes": "double"
}
}
2,543 changes: 1,335 additions & 1,208 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "16.3.0",
"@testing-library/user-event": "^14.6.1",
"glob": "7.2.3"
"glob": "7.2.3",
"ts-node": "^10.9.2"
}
}
62 changes: 59 additions & 3 deletions src/App.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';

import { mockCourseDiscoveryResponse } from './сatalog/__mocks__';
import { mockHomeSettingsResponse } from './home/__mocks__';
import { mockCourseAboutResponse } from './__mocks__';
import messages from './сatalog/messages';
import { useHomeSettingsQuery } from './home/data/hooks';
import { useCourseDiscovery } from './сatalog/data/hooks';
import { useCourseAboutData } from './course-about/data/hooks';
import {
render, within, waitFor, screen,
} from './setupTest';
Expand All @@ -16,11 +22,26 @@ jest.mock('@edx/frontend-platform', () => ({
})),
}));

jest.mock('./home/data/hooks', () => ({
useHomeSettingsQuery: jest.fn(),
}));

jest.mock('./сatalog/data/hooks', () => ({
useCourseDiscovery: 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 mockHomeSettings = useHomeSettingsQuery as jest.Mock;
const mockCourseDiscovery = useCourseDiscovery 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 @@ -34,9 +55,21 @@ 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();
});

mockHomeSettings.mockReturnValue({
data: mockHomeSettingsResponse,
isLoading: false,
isError: false,
});

mockCourseDiscovery.mockReturnValue({
Expand All @@ -45,11 +78,22 @@ describe('App', () => {
isError: false,
});

it('renders HomePage on "/" route', () => {
mockCourseAbout.mockReturnValue({
data: mockCourseAboutResponse,
isLoading: false,
isError: false,
});

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

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

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

expect(screen.getByTestId('home-banner')).toBeInTheDocument();
});

it('renders CatalogPage with course cards at /courses route', async () => {
Expand Down Expand Up @@ -83,11 +127,23 @@ 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 />);

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

expect(screen.getByTestId('course-about-page')).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: 'Enroll now' })).toBeInTheDocument();
expect(screen.getByRole('img', { name: mockCourseAboutResponse.name })).toBeInTheDocument();
});

it('renders NotFoundPage on unknown route', () => {
Expand Down
19 changes: 8 additions & 11 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { AppProvider } from '@edx/frontend-platform/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { Route, Routes } from 'react-router-dom';
import { Container } from '@openedx/paragon';
import { FooterSlot } from '@edx/frontend-component-footer';
import Header from '@edx/frontend-component-header';
import CatalogHeader from './header/CatalogHeader';

import HomePage from './home/HomePage';
import CatalogPage from './сatalog/CatalogPage';
Expand All @@ -16,16 +15,14 @@ const queryClient = new QueryClient();
const App = () => (
<AppProvider>
<QueryClientProvider client={queryClient}>
<Header />
<CatalogHeader />
<main className="d-flex flex-column flex-grow-1">
<Container className="container-xl">
<Routes>
<Route path={ROUTES.HOME} element={<HomePage />} />
<Route path={ROUTES.COURSES} element={<CatalogPage />} />
<Route path={ROUTES.COURSE_ABOUT} element={<CourseAboutPage />} />
<Route path={ROUTES.NOT_FOUND} element={<NotFoundPage />} />
</Routes>
</Container>
<Routes>
<Route path={ROUTES.HOME} element={<HomePage />} />
<Route path={ROUTES.COURSES} element={<CatalogPage />} />
<Route path={ROUTES.COURSE_ABOUT} element={<CourseAboutPage />} />
<Route path={ROUTES.NOT_FOUND} element={<NotFoundPage />} />
</Routes>
</main>
<FooterSlot />
</QueryClientProvider>
Expand Down
91 changes: 91 additions & 0 deletions src/__mocks__/course-about.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
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: null,
},
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: null,
courseImageUrls: [
'raw',
'small',
'large',
],
startDateIsStillDefault: true,
advertisedStart: null,
coursePrice: 'Free',
preRequisiteCourses: [],
sidebarHtmlEnabled: false,
courseAboutSectionHtml: null,
};
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 { mockCourseAboutResponse } from './course-about';
Binary file added src/assets/images/home-banner.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes
File renamed without changes
23 changes: 23 additions & 0 deletions src/assets/scss/_animations.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
@keyframes home-banner-info {
0% {
opacity: 0;
transform: translateY(18.75rem);
}

45% {
opacity: 1;
}

65% {
transform: translateY(-2.5rem);
}

85% {
transform: translateY(.625rem);
}

100% {
top: 0;
transform: translateY(0);
}
}
12 changes: 12 additions & 0 deletions src/config/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const configuration = {
LMS_BASE_URL: process.env.LMS_BASE_URL,
SITE_NAME: process.env.SITE_NAME,
// SUPPORT_URL: process.env.SUPPORT_URL || null,
INFO_EMAIL: process.env.INFO_EMAIL || '',
LOGO_URL: process.env.LOGO_URL,
ENABLE_PROGRAMS: process.env.ENABLE_PROGRAMS === 'true',
};

const features = {};

export { configuration, features };
15 changes: 15 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Feature policy for iframe, allowing access to certain courseware-related media.
*
* We must use the wildcard (*) origin for each feature, as courseware content
* may be embedded in external iframes. Notably, xblock-lti-consumer is a popular
* block that iframes external course content.

* This policy was selected in conference with the edX Security Working Group.
* Changes to it should be vetted by them ([email protected]).
*/
export const IFRAME_FEATURE_POLICY = (
'microphone *; camera *; midi *; geolocation *; encrypted-media *; clipboard-write *'
);

export const DEFAULT_VIDEO_MODAL_HEIGHT = 500;
41 changes: 41 additions & 0 deletions src/course-about/CourseAboutPage.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
@import "course-intro/course-media/CourseMedia";

.course-about-intro {
.pgn__card {
min-height: var(--course-about-intro-card-min-height, 15.75rem);
}

.course-about-intro-heading {
font-size: var(--pgn-typography-font-size-h2-base);
line-height: var(--course-about-intro-heading-line-height, 2.25rem);
}

.pgn__card .pgn__card-header {
.pgn__card-header-content {
flex-direction: column-reverse;
}

.pgn__card-header-subtitle-md {
margin: var(--pgn-spacing-spacer-0);
}
}

.alert.course-about-intro-alert {
box-shadow: var(--course-about-intro-alert-box-shadow, none);
padding: var(--pgn-spacing-spacer-0);
margin: var(--pgn-spacing-spacer-0);
background-color: var(--course-about-intro-alert-background-color, transparent);

&.alert-success .alert-heading {
color: var(--pgn-color-success-500);
}

&.alert-info .alert-heading {
color: var(--pgn-color-info-500);
}

&.alert-danger .alert-heading {
color: var(--pgn-color-danger-500);
}
}
}
Loading