Skip to content

Commit

Permalink
Merge branch 'master' into mashal-m/react-upgrade-to-v17
Browse files Browse the repository at this point in the history
  • Loading branch information
Mashal-m authored Jul 12, 2023
2 parents 42213a0 + 103a676 commit 4fec753
Show file tree
Hide file tree
Showing 41 changed files with 1,154 additions and 292 deletions.
7 changes: 6 additions & 1 deletion src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
import { reduxHooks } from 'hooks';
import Dashboard from 'containers/Dashboard';
import ZendeskFab from 'components/ZendeskFab';
import { ExperimentProvider } from 'ExperimentContext';

import track from 'tracking';

Expand Down Expand Up @@ -84,7 +85,11 @@ export const App = () => {
<Alert variant="danger">
<ErrorPage message={formatMessage(messages.errorMessage, { supportEmail })} />
</Alert>
) : (<Dashboard />)}
) : (
<ExperimentProvider>
<Dashboard />
</ExperimentProvider>
)}
</main>
<Footer logo={process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG} />
<ZendeskFab />
Expand Down
6 changes: 5 additions & 1 deletion src/App.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { RequestKeys } from 'data/constants/requests';
import { reduxHooks } from 'hooks';
import Dashboard from 'containers/Dashboard';
import LearnerDashboardHeader from 'containers/LearnerDashboardHeader';
import { ExperimentProvider } from 'ExperimentContext';
import { App } from './App';
import messages from './messages';

Expand All @@ -21,6 +22,9 @@ jest.mock('@edx/frontend-component-footer', () => 'Footer');
jest.mock('containers/Dashboard', () => 'Dashboard');
jest.mock('containers/LearnerDashboardHeader', () => 'LearnerDashboardHeader');
jest.mock('components/ZendeskFab', () => 'ZendeskFab');
jest.mock('ExperimentContext', () => ({
ExperimentProvider: 'ExperimentProvider',
}));
jest.mock('data/redux', () => ({
selectors: 'redux.selectors',
actions: 'redux.actions',
Expand Down Expand Up @@ -71,7 +75,7 @@ describe('App router component', () => {
runBasicTests();
it('loads dashboard', () => {
expect(el.find('main')).toMatchObject(shallow(
<main><Dashboard /></main>,
<main><ExperimentProvider><Dashboard /></ExperimentProvider></main>,
));
});
});
Expand Down
64 changes: 64 additions & 0 deletions src/ExperimentContext.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useWindowSize, breakpoints } from '@edx/paragon';
import { StrictDict } from 'utils';
import api from 'widgets/ProductRecommendations/api';
import * as module from './ExperimentContext';

export const state = StrictDict({
experiment: (val) => React.useState(val), // eslint-disable-line
countryCode: (val) => React.useState(val), // eslint-disable-line
});

export const useCountryCode = (setCountryCode) => {
React.useEffect(() => {
api
.fetchRecommendationsContext()
.then((response) => {
setCountryCode(response.data.countryCode);
})
.catch(() => {
setCountryCode('');
});
/* eslint-disable */
}, []);
};

export const ExperimentContext = React.createContext();

export const ExperimentProvider = ({ children }) => {
const [countryCode, setCountryCode] = module.state.countryCode(null);
const [experiment, setExperiment] = module.state.experiment({
isExperimentActive: false,
inRecommendationsVariant: true,
});

module.useCountryCode(setCountryCode);
const { width } = useWindowSize();
const isMobile = width < breakpoints.small.minWidth;

const contextValue = React.useMemo(
() => ({
experiment,
countryCode,
setExperiment,
setCountryCode,
isMobile,
}),
[experiment, countryCode, setExperiment, setCountryCode, isMobile]
);

return (
<ExperimentContext.Provider value={contextValue}>
{children}
</ExperimentContext.Provider>
);
};

export const useExperimentContext = () => React.useContext(ExperimentContext);

ExperimentProvider.propTypes = {
children: PropTypes.node.isRequired,
};

export default { useCountryCode, useExperimentContext };
123 changes: 123 additions & 0 deletions src/ExperimentContext.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import React from 'react';
import { mount } from 'enzyme';
import { waitFor } from '@testing-library/react';
import { useWindowSize } from '@edx/paragon';

import api from 'widgets/ProductRecommendations/api';
import { MockUseState } from 'testUtils';

import * as experiment from 'ExperimentContext';

const state = new MockUseState(experiment);

jest.unmock('react');
jest.spyOn(React, 'useEffect').mockImplementation((cb, prereqs) => ({ useEffect: { cb, prereqs } }));

jest.mock('widgets/ProductRecommendations/api', () => ({
fetchRecommendationsContext: jest.fn(),
}));

describe('experiments context', () => {
beforeEach(() => {
jest.resetAllMocks();
});

describe('useCountryCode', () => {
describe('behaviour', () => {
describe('useEffect call', () => {
let calls;
let cb;
const setCountryCode = jest.fn();
const successfulFetch = { data: { countryCode: 'ZA' } };

beforeEach(() => {
experiment.useCountryCode(setCountryCode);

({ calls } = React.useEffect.mock);
[[cb]] = calls;
});

it('calls useEffect once', () => {
expect(calls.length).toEqual(1);
});
describe('successfull fetch', () => {
it('sets the country code', async () => {
let resolveFn;
api.fetchRecommendationsContext.mockReturnValueOnce(
new Promise((resolve) => {
resolveFn = resolve;
}),
);

cb();
expect(api.fetchRecommendationsContext).toHaveBeenCalled();
expect(setCountryCode).not.toHaveBeenCalled();
resolveFn(successfulFetch);
await waitFor(() => {
expect(setCountryCode).toHaveBeenCalledWith(successfulFetch.data.countryCode);
});
});
});
describe('unsuccessfull fetch', () => {
it('sets the country code to an empty string', async () => {
let rejectFn;
api.fetchRecommendationsContext.mockReturnValueOnce(
new Promise((resolve, reject) => {
rejectFn = reject;
}),
);
cb();
expect(api.fetchRecommendationsContext).toHaveBeenCalled();
expect(setCountryCode).not.toHaveBeenCalled();
rejectFn();
await waitFor(() => {
expect(setCountryCode).toHaveBeenCalledWith('');
});
});
});
});
});
});

describe('ExperimentProvider', () => {
const { ExperimentProvider } = experiment;

const TestComponent = () => {
const {
experiment: exp,
setExperiment,
countryCode,
setCountryCode,
isMobile,
} = experiment.useExperimentContext();

expect(exp.isExperimentActive).toBeFalsy();
expect(exp.inRecommendationsVariant).toBeTruthy();
expect(countryCode).toBeNull();
expect(isMobile).toBe(false);
expect(setExperiment).toBeDefined();
expect(setCountryCode).toBeDefined();

return (
<div />
);
};

it('allows access to child components with the context stateful values', () => {
const countryCodeSpy = jest.spyOn(experiment, 'useCountryCode').mockImplementationOnce(() => {});
useWindowSize.mockImplementationOnce(() => ({ width: 577, height: 943 }));

state.mock();

mount(
<ExperimentProvider>
<TestComponent />
</ExperimentProvider>,
);

expect(countryCodeSpy).toHaveBeenCalledWith(state.setState.countryCode);
state.expectInitializedWith(state.keys.countryCode, null);
state.expectInitializedWith(state.keys.experiment, { isExperimentActive: false, inRecommendationsVariant: true });
});
});
});
4 changes: 3 additions & 1 deletion src/__snapshots__/App.test.jsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ exports[`App router component component no network failure snapshot 1`] = `
<div>
<LearnerDashboardHeader />
<main>
<Dashboard />
<ExperimentProvider>
<Dashboard />
</ExperimentProvider>
</main>
<Footer
logo="fakeLogo.png"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`CourseCardMenu enrolled, share enabled, email setting enable snapshot 1`] = `
exports[`CourseCardMenu default snapshot 1`] = `
<Fragment>
<Dropdown>
<Dropdown
onToggle={[MockFunction mockHandleToggleDropdown]}
>
<Dropdown.Toggle
alt="Course actions dropdown"
as="IconButton"
Expand All @@ -28,7 +30,7 @@ exports[`CourseCardMenu enrolled, share enabled, email setting enable snapshot 1
</Dropdown.Item>
<FacebookShareButton
className="pgn__dropdown-item dropdown-item"
onClick={[MockFunction facebookShareClick]}
onClick={[MockFunction handleFacebookShare]}
resetButtonStyle={false}
title="I'm taking test-course-name online with facebook-social-brand. Check it out!"
url="facebook-share-url"
Expand All @@ -37,7 +39,7 @@ exports[`CourseCardMenu enrolled, share enabled, email setting enable snapshot 1
</FacebookShareButton>
<TwitterShareButton
className="pgn__dropdown-item dropdown-item"
onClick={[MockFunction twitterShareClick]}
onClick={[MockFunction handleTwitterShare]}
resetButtonStyle={false}
title="I'm taking test-course-name online with twitter-social-brand. Check it out!"
url="twitter-share-url"
Expand All @@ -61,7 +63,9 @@ exports[`CourseCardMenu enrolled, share enabled, email setting enable snapshot 1

exports[`CourseCardMenu masquerading snapshot 1`] = `
<Fragment>
<Dropdown>
<Dropdown
onToggle={[MockFunction mockHandleToggleDropdown]}
>
<Dropdown.Toggle
alt="Course actions dropdown"
as="IconButton"
Expand All @@ -87,7 +91,7 @@ exports[`CourseCardMenu masquerading snapshot 1`] = `
</Dropdown.Item>
<FacebookShareButton
className="pgn__dropdown-item dropdown-item"
onClick={[MockFunction facebookShareClick]}
onClick={[MockFunction handleFacebookShare]}
resetButtonStyle={false}
title="I'm taking test-course-name online with facebook-social-brand. Check it out!"
url="facebook-share-url"
Expand All @@ -96,7 +100,7 @@ exports[`CourseCardMenu masquerading snapshot 1`] = `
</FacebookShareButton>
<TwitterShareButton
className="pgn__dropdown-item dropdown-item"
onClick={[MockFunction twitterShareClick]}
onClick={[MockFunction handleTwitterShare]}
resetButtonStyle={false}
title="I'm taking test-course-name online with twitter-social-brand. Check it out!"
url="twitter-share-url"
Expand All @@ -118,23 +122,4 @@ exports[`CourseCardMenu masquerading snapshot 1`] = `
</Fragment>
`;

exports[`CourseCardMenu not enrolled, share disabled, email setting disabled snapshot 1`] = `
<Fragment>
<Dropdown>
<Dropdown.Toggle
alt="Course actions dropdown"
as="IconButton"
iconAs="Icon"
id="course-actions-dropdown-test-card-id"
src={[MockFunction icons.MoreVert]}
variant="primary"
/>
<Dropdown.Menu />
</Dropdown>
<UnenrollConfirmModal
cardId="test-card-id"
closeModal={[MockFunction unenrollHide]}
show={false}
/>
</Fragment>
`;
exports[`CourseCardMenu renders null if showDropdown is false 1`] = `""`;
33 changes: 33 additions & 0 deletions src/containers/CourseCard/components/CourseCardMenu/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,36 @@ export const useHandleToggleDropdown = (cardId) => {
if (isOpen) { trackCourseEvent(); }
};
};

export const useCourseCardMenu = (cardId) => {
const { courseName } = reduxHooks.useCardCourseData(cardId);
const { isEnrolled, isEmailEnabled } = reduxHooks.useCardEnrollmentData(cardId);
const { twitter, facebook } = reduxHooks.useCardSocialSettingsData(cardId);
const { isMasquerading } = reduxHooks.useMasqueradeData();
const { isEarned } = reduxHooks.useCardCertificateData(cardId);
const handleTwitterShare = reduxHooks.useTrackCourseEvent(
track.socialShare,
cardId,
'twitter',
);
const handleFacebookShare = reduxHooks.useTrackCourseEvent(
track.socialShare,
cardId,
'facebook',
);

const showUnenrollItem = isEnrolled && !isEarned;
const showDropdown = showUnenrollItem || isEmailEnabled || facebook.isEnabled || twitter.isEnabled;

return {
courseName,
isMasquerading,
isEmailEnabled,
showUnenrollItem,
showDropdown,
facebook,
twitter,
handleTwitterShare,
handleFacebookShare,
};
};
Loading

0 comments on commit 4fec753

Please sign in to comment.