mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
fix(client): show donation modal on module completion (#57583)
Co-authored-by: ahmad abdolsaheb <ahmad.abdolsaheb@gmail.com> Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com> Co-authored-by: Tom <20648924+moT01@users.noreply.github.com>
This commit is contained in:
@@ -112,6 +112,8 @@ exports.createPages = async function createPages({
|
|||||||
superOrder
|
superOrder
|
||||||
template
|
template
|
||||||
usesMultifileEditor
|
usesMultifileEditor
|
||||||
|
chapter
|
||||||
|
module
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,20 +3,19 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { useFeature } from '@growthbook/growthbook-react';
|
import { useFeature } from '@growthbook/growthbook-react';
|
||||||
import { Col, Row, Modal, Spacer } from '@freecodecamp/ui';
|
import { Col, Row, Modal, Spacer } from '@freecodecamp/ui';
|
||||||
import { closeDonationModal } from '../../redux/actions';
|
import { closeDonationModal } from '../../redux/actions';
|
||||||
import { PaymentContext } from '../../../../shared/config/donation-settings'; //
|
import { PaymentContext } from '../../../../shared/config/donation-settings';
|
||||||
import donationAnimation from '../../assets/images/donation-bear-animation.svg';
|
import donationAnimation from '../../assets/images/donation-bear-animation.svg';
|
||||||
import donationAnimationB from '../../assets/images/new-bear-animation.svg';
|
import donationAnimationB from '../../assets/images/new-bear-animation.svg';
|
||||||
import supporterBear from '../../assets/images/supporter-bear.svg';
|
import supporterBear from '../../assets/images/supporter-bear.svg';
|
||||||
import callGA from '../../analytics/call-ga';
|
import callGA from '../../analytics/call-ga';
|
||||||
import MultiTierDonationForm from './multi-tier-donation-form';
|
import MultiTierDonationForm from './multi-tier-donation-form';
|
||||||
import { ModalBenefitList } from './donation-text-components';
|
import { ModalBenefitList } from './donation-text-components';
|
||||||
|
import { DonatableSectionRecentlyCompleted } from './types';
|
||||||
type RecentlyClaimedBlock = null | { block: string; superBlock: string };
|
|
||||||
|
|
||||||
type DonationModalBodyProps = {
|
type DonationModalBodyProps = {
|
||||||
activeDonors?: number;
|
activeDonors?: number;
|
||||||
closeDonationModal: typeof closeDonationModal;
|
closeDonationModal: typeof closeDonationModal;
|
||||||
recentlyClaimedBlock: RecentlyClaimedBlock;
|
donatableSectionRecentlyCompleted: DonatableSectionRecentlyCompleted;
|
||||||
setCanClose: (canClose: boolean) => void;
|
setCanClose: (canClose: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -32,17 +31,19 @@ const Illustration = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function ModalHeader({
|
function ModalHeader({
|
||||||
recentlyClaimedBlock,
|
|
||||||
showHeaderAndFooter,
|
showHeaderAndFooter,
|
||||||
donationAttempted,
|
donationAttempted,
|
||||||
showForm
|
showForm,
|
||||||
|
donatableSectionRecentlyCompleted
|
||||||
}: {
|
}: {
|
||||||
recentlyClaimedBlock: RecentlyClaimedBlock;
|
donatableSectionRecentlyCompleted: DonatableSectionRecentlyCompleted;
|
||||||
showHeaderAndFooter: boolean;
|
showHeaderAndFooter: boolean;
|
||||||
donationAttempted: boolean;
|
donationAttempted: boolean;
|
||||||
showForm: boolean;
|
showForm: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { section, superBlock, title } =
|
||||||
|
donatableSectionRecentlyCompleted || {};
|
||||||
|
|
||||||
if (!showHeaderAndFooter || donationAttempted) {
|
if (!showHeaderAndFooter || donationAttempted) {
|
||||||
return null;
|
return null;
|
||||||
@@ -50,19 +51,19 @@ function ModalHeader({
|
|||||||
return (
|
return (
|
||||||
<Row className='text-center'>
|
<Row className='text-center'>
|
||||||
<Col sm={10} smOffset={1} xs={12}>
|
<Col sm={10} smOffset={1} xs={12}>
|
||||||
{recentlyClaimedBlock !== null && (
|
{donatableSectionRecentlyCompleted && (
|
||||||
<>
|
<>
|
||||||
<b>
|
<b>
|
||||||
{t('donate.nicely-done', {
|
{t('donate.nicely-done', {
|
||||||
block: t(
|
block:
|
||||||
`intro:${recentlyClaimedBlock.superBlock}.blocks.${recentlyClaimedBlock.block}.title`
|
section === 'module'
|
||||||
)
|
? t(`intro:${superBlock}.${section}s.${title}`)
|
||||||
|
: t(`intro:${superBlock}.${section}s.${title}.title`)
|
||||||
})}
|
})}
|
||||||
</b>
|
</b>
|
||||||
<Spacer size='xs' />
|
<Spacer size='xs' />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Modal.Header showCloseButton={false} borderless>
|
<Modal.Header showCloseButton={false} borderless>
|
||||||
{t('donate.modal-benefits-title')}
|
{t('donate.modal-benefits-title')}
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
@@ -188,16 +189,16 @@ const AnimationContainer = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const BecomeASupporterConfirmation = ({
|
const BecomeASupporterConfirmation = ({
|
||||||
recentlyClaimedBlock,
|
|
||||||
showHeaderAndFooter,
|
showHeaderAndFooter,
|
||||||
closeDonationModal,
|
closeDonationModal,
|
||||||
donationAttempted,
|
donationAttempted,
|
||||||
showForm,
|
showForm,
|
||||||
setShowHeaderAndFooter,
|
setShowHeaderAndFooter,
|
||||||
handleProcessing,
|
handleProcessing,
|
||||||
setShowForm
|
setShowForm,
|
||||||
|
donatableSectionRecentlyCompleted
|
||||||
}: {
|
}: {
|
||||||
recentlyClaimedBlock: RecentlyClaimedBlock;
|
donatableSectionRecentlyCompleted: DonatableSectionRecentlyCompleted;
|
||||||
showHeaderAndFooter: boolean;
|
showHeaderAndFooter: boolean;
|
||||||
closeDonationModal: () => void;
|
closeDonationModal: () => void;
|
||||||
donationAttempted: boolean;
|
donationAttempted: boolean;
|
||||||
@@ -212,7 +213,7 @@ const BecomeASupporterConfirmation = ({
|
|||||||
<Illustration />
|
<Illustration />
|
||||||
</div>
|
</div>
|
||||||
<ModalHeader
|
<ModalHeader
|
||||||
recentlyClaimedBlock={recentlyClaimedBlock}
|
donatableSectionRecentlyCompleted={donatableSectionRecentlyCompleted}
|
||||||
showHeaderAndFooter={showHeaderAndFooter}
|
showHeaderAndFooter={showHeaderAndFooter}
|
||||||
donationAttempted={donationAttempted}
|
donationAttempted={donationAttempted}
|
||||||
showForm={showForm}
|
showForm={showForm}
|
||||||
@@ -240,7 +241,7 @@ const BecomeASupporterConfirmation = ({
|
|||||||
|
|
||||||
function DonationModalBody({
|
function DonationModalBody({
|
||||||
closeDonationModal,
|
closeDonationModal,
|
||||||
recentlyClaimedBlock,
|
donatableSectionRecentlyCompleted,
|
||||||
setCanClose
|
setCanClose
|
||||||
}: DonationModalBodyProps): JSX.Element {
|
}: DonationModalBodyProps): JSX.Element {
|
||||||
const [donationAttempted, setDonationAttempted] = useState(false);
|
const [donationAttempted, setDonationAttempted] = useState(false);
|
||||||
@@ -273,7 +274,9 @@ function DonationModalBody({
|
|||||||
<AnimationContainer secondsRemaining={secondsRemaining} />
|
<AnimationContainer secondsRemaining={secondsRemaining} />
|
||||||
) : (
|
) : (
|
||||||
<BecomeASupporterConfirmation
|
<BecomeASupporterConfirmation
|
||||||
recentlyClaimedBlock={recentlyClaimedBlock}
|
donatableSectionRecentlyCompleted={
|
||||||
|
donatableSectionRecentlyCompleted
|
||||||
|
}
|
||||||
showHeaderAndFooter={showHeaderAndFooter}
|
showHeaderAndFooter={showHeaderAndFooter}
|
||||||
closeDonationModal={closeDonationModal}
|
closeDonationModal={closeDonationModal}
|
||||||
donationAttempted={donationAttempted}
|
donationAttempted={donationAttempted}
|
||||||
|
|||||||
@@ -10,21 +10,24 @@ import { useFeature } from '@growthbook/growthbook-react';
|
|||||||
import { closeDonationModal } from '../../redux/actions';
|
import { closeDonationModal } from '../../redux/actions';
|
||||||
import {
|
import {
|
||||||
isDonationModalOpenSelector,
|
isDonationModalOpenSelector,
|
||||||
recentlyClaimedBlockSelector
|
donatableSectionRecentlyCompletedSelector
|
||||||
} from '../../redux/selectors';
|
} from '../../redux/selectors';
|
||||||
|
|
||||||
import { isLocationSuperBlock } from '../../utils/path-parsers';
|
import { isLocationSuperBlock } from '../../utils/path-parsers';
|
||||||
import { playTone } from '../../utils/tone';
|
import { playTone } from '../../utils/tone';
|
||||||
import callGA from '../../analytics/call-ga';
|
import callGA from '../../analytics/call-ga';
|
||||||
|
import { DonatableSectionRecentlyCompleted } from './types';
|
||||||
import DonationModalBody from './donation-modal-body';
|
import DonationModalBody from './donation-modal-body';
|
||||||
|
|
||||||
type RecentlyClaimedBlock = null | { block: string; superBlock: string };
|
|
||||||
|
|
||||||
const mapStateToProps = createSelector(
|
const mapStateToProps = createSelector(
|
||||||
isDonationModalOpenSelector,
|
isDonationModalOpenSelector,
|
||||||
recentlyClaimedBlockSelector,
|
donatableSectionRecentlyCompletedSelector,
|
||||||
(show: boolean, recentlyClaimedBlock: RecentlyClaimedBlock) => ({
|
(
|
||||||
|
show: boolean,
|
||||||
|
donatableSectionRecentlyCompleted: DonatableSectionRecentlyCompleted
|
||||||
|
) => ({
|
||||||
show,
|
show,
|
||||||
recentlyClaimedBlock
|
donatableSectionRecentlyCompleted
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -35,7 +38,7 @@ type DonateModalProps = {
|
|||||||
activeDonors?: number;
|
activeDonors?: number;
|
||||||
closeDonationModal: typeof closeDonationModal;
|
closeDonationModal: typeof closeDonationModal;
|
||||||
location?: WindowLocation;
|
location?: WindowLocation;
|
||||||
recentlyClaimedBlock: RecentlyClaimedBlock;
|
donatableSectionRecentlyCompleted: DonatableSectionRecentlyCompleted;
|
||||||
show: boolean;
|
show: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -43,7 +46,7 @@ function DonateModal({
|
|||||||
show,
|
show,
|
||||||
closeDonationModal,
|
closeDonationModal,
|
||||||
location,
|
location,
|
||||||
recentlyClaimedBlock
|
donatableSectionRecentlyCompleted
|
||||||
}: DonateModalProps): JSX.Element {
|
}: DonateModalProps): JSX.Element {
|
||||||
const [canClose, setCanClose] = useState(false);
|
const [canClose, setCanClose] = useState(false);
|
||||||
const isA11yFeatureEnabled = useFeature('a11y-donation-modal').on;
|
const isA11yFeatureEnabled = useFeature('a11y-donation-modal').on;
|
||||||
@@ -55,11 +58,11 @@ function DonateModal({
|
|||||||
callGA({
|
callGA({
|
||||||
event: 'donation_view',
|
event: 'donation_view',
|
||||||
action: `Displayed ${
|
action: `Displayed ${
|
||||||
recentlyClaimedBlock !== null ? 'Block' : 'Progress'
|
donatableSectionRecentlyCompleted !== null ? 'Block' : 'Progress'
|
||||||
} Donation Modal`
|
} Donation Modal`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [show, recentlyClaimedBlock]);
|
}, [show, donatableSectionRecentlyCompleted]);
|
||||||
|
|
||||||
const handleModalHide = () => {
|
const handleModalHide = () => {
|
||||||
// If modal is open on a SuperBlock page
|
// If modal is open on a SuperBlock page
|
||||||
@@ -76,7 +79,7 @@ function DonateModal({
|
|||||||
<Modal size='large' onClose={handleModalHide} open={show}>
|
<Modal size='large' onClose={handleModalHide} open={show}>
|
||||||
<DonationModalBody
|
<DonationModalBody
|
||||||
closeDonationModal={closeDonationModal}
|
closeDonationModal={closeDonationModal}
|
||||||
recentlyClaimedBlock={recentlyClaimedBlock}
|
donatableSectionRecentlyCompleted={donatableSectionRecentlyCompleted}
|
||||||
setCanClose={setCanClose}
|
setCanClose={setCanClose}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { PaymentIntentResult } from '@stripe/stripe-js';
|
import type { PaymentIntentResult } from '@stripe/stripe-js';
|
||||||
|
import { SuperBlocks } from '../../../../shared/config/curriculum';
|
||||||
|
|
||||||
export type PaymentContext = 'modal' | 'donate page' | 'certificate';
|
export type PaymentContext = 'modal' | 'donate page' | 'certificate';
|
||||||
export type PaymentProvider = 'patreon' | 'paypal' | 'stripe' | 'stripe card';
|
export type PaymentProvider = 'patreon' | 'paypal' | 'stripe' | 'stripe card';
|
||||||
@@ -28,3 +29,9 @@ export interface DonationApprovalData {
|
|||||||
paypal: boolean;
|
paypal: boolean;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DonatableSectionRecentlyCompleted = null | {
|
||||||
|
section: string;
|
||||||
|
title: string;
|
||||||
|
superBlock: SuperBlocks;
|
||||||
|
};
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ import './map.css';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
isSignedInSelector,
|
isSignedInSelector,
|
||||||
currentCertsSelector
|
currentCertsSelector,
|
||||||
|
completedChallengesIdsSelector
|
||||||
} from '../../redux/selectors';
|
} from '../../redux/selectors';
|
||||||
|
|
||||||
import { RibbonIcon } from '../../assets/icons/completion-ribbon';
|
import { RibbonIcon } from '../../assets/icons/completion-ribbon';
|
||||||
@@ -31,7 +32,6 @@ import {
|
|||||||
certSlugTypeMap,
|
certSlugTypeMap,
|
||||||
superBlockCertTypeMap
|
superBlockCertTypeMap
|
||||||
} from '../../../../shared/config/certification-settings';
|
} from '../../../../shared/config/certification-settings';
|
||||||
import { completedChallengesIdsSelector } from '../../templates/Challenges/redux/selectors';
|
|
||||||
|
|
||||||
interface MapProps {
|
interface MapProps {
|
||||||
forLanding?: boolean;
|
forLanding?: boolean;
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ export const actionTypes = createTypes(
|
|||||||
'toggleTheme',
|
'toggleTheme',
|
||||||
'appMount',
|
'appMount',
|
||||||
'hardGoTo',
|
'hardGoTo',
|
||||||
'allowBlockDonationRequests',
|
'allowSectionDonationRequests',
|
||||||
'setRenderStartTime',
|
'setRenderStartTime',
|
||||||
'preventBlockDonationRequests',
|
'preventSectionDonationRequests',
|
||||||
'setIsRandomCompletionThreshold',
|
'setIsRandomCompletionThreshold',
|
||||||
'openDonationModal',
|
'openDonationModal',
|
||||||
'closeDonationModal',
|
'closeDonationModal',
|
||||||
|
|||||||
@@ -8,14 +8,14 @@ export const tryToShowDonationModal = createAction(
|
|||||||
actionTypes.tryToShowDonationModal
|
actionTypes.tryToShowDonationModal
|
||||||
);
|
);
|
||||||
|
|
||||||
export const allowBlockDonationRequests = createAction(
|
export const allowSectionDonationRequests = createAction(
|
||||||
actionTypes.allowBlockDonationRequests
|
actionTypes.allowSectionDonationRequests
|
||||||
);
|
);
|
||||||
export const setRenderStartTime = createAction(actionTypes.setRenderStartTime);
|
export const setRenderStartTime = createAction(actionTypes.setRenderStartTime);
|
||||||
export const closeDonationModal = createAction(actionTypes.closeDonationModal);
|
export const closeDonationModal = createAction(actionTypes.closeDonationModal);
|
||||||
export const openDonationModal = createAction(actionTypes.openDonationModal);
|
export const openDonationModal = createAction(actionTypes.openDonationModal);
|
||||||
export const preventBlockDonationRequests = createAction(
|
export const preventSectionDonationRequests = createAction(
|
||||||
actionTypes.preventBlockDonationRequests
|
actionTypes.preventSectionDonationRequests
|
||||||
);
|
);
|
||||||
export const setIsRandomCompletionThreshold = createAction(
|
export const setIsRandomCompletionThreshold = createAction(
|
||||||
actionTypes.setIsRandomCompletionThreshold
|
actionTypes.setIsRandomCompletionThreshold
|
||||||
|
|||||||
@@ -23,19 +23,20 @@ import {
|
|||||||
getSessionChallengeData,
|
getSessionChallengeData,
|
||||||
saveCurrentCount
|
saveCurrentCount
|
||||||
} from '../utils/session-storage';
|
} from '../utils/session-storage';
|
||||||
|
|
||||||
import { actionTypes as appTypes } from './action-types';
|
import { actionTypes as appTypes } from './action-types';
|
||||||
import {
|
import {
|
||||||
openDonationModal,
|
openDonationModal,
|
||||||
postChargeComplete,
|
postChargeComplete,
|
||||||
postChargeProcessing,
|
postChargeProcessing,
|
||||||
postChargeError,
|
postChargeError,
|
||||||
preventBlockDonationRequests,
|
preventSectionDonationRequests,
|
||||||
updateCardError,
|
updateCardError,
|
||||||
updateCardRedirecting
|
updateCardRedirecting
|
||||||
} from './actions';
|
} from './actions';
|
||||||
import {
|
import {
|
||||||
isDonatingSelector,
|
isDonatingSelector,
|
||||||
recentlyClaimedBlockSelector,
|
donatableSectionRecentlyCompletedSelector,
|
||||||
shouldRequestDonationSelector,
|
shouldRequestDonationSelector,
|
||||||
isSignedInSelector,
|
isSignedInSelector,
|
||||||
completedChallengesSelector
|
completedChallengesSelector
|
||||||
@@ -46,27 +47,31 @@ const updateCardErrorMessage = i18next.t('donate.error-3');
|
|||||||
|
|
||||||
function* showDonateModalSaga() {
|
function* showDonateModalSaga() {
|
||||||
let shouldRequestDonation = yield select(shouldRequestDonationSelector);
|
let shouldRequestDonation = yield select(shouldRequestDonationSelector);
|
||||||
const recentlyClaimedBlock = yield select(recentlyClaimedBlockSelector);
|
|
||||||
const MODAL_SHOWN_KEY = 'modalShownTimestamp';
|
const MODAL_SHOWN_KEY = 'modalShownTimestamp';
|
||||||
const modalShownTimestamp = sessionStorage.getItem(MODAL_SHOWN_KEY);
|
const modalShownTimestamp = sessionStorage.getItem(MODAL_SHOWN_KEY);
|
||||||
const isModalRecentlyShown = Date.now() - modalShownTimestamp < 20000;
|
// If the modal has been shown in the last 20 seconds, the animation should
|
||||||
if (
|
// still be running:
|
||||||
shouldRequestDonation &&
|
const isAnimationRunning = Date.now() - modalShownTimestamp < 20000;
|
||||||
recentlyClaimedBlock &&
|
const shouldShowModal = shouldRequestDonation || isAnimationRunning;
|
||||||
chapterBasedSuperBlocks.includes(recentlyClaimedBlock.superBlock)
|
const donatableSectionRecentlyCompleted = yield select(
|
||||||
) {
|
donatableSectionRecentlyCompletedSelector
|
||||||
yield put(preventBlockDonationRequests());
|
);
|
||||||
} else if (shouldRequestDonation || isModalRecentlyShown) {
|
|
||||||
|
if (shouldShowModal) {
|
||||||
yield delay(200);
|
yield delay(200);
|
||||||
yield put(openDonationModal());
|
yield put(openDonationModal());
|
||||||
sessionStorage.setItem(MODAL_SHOWN_KEY, Date.now());
|
sessionStorage.setItem(MODAL_SHOWN_KEY, Date.now());
|
||||||
yield take(appTypes.closeDonationModal);
|
yield take(appTypes.closeDonationModal);
|
||||||
if (recentlyClaimedBlock) {
|
if (!donatableSectionRecentlyCompleted) {
|
||||||
yield put(preventBlockDonationRequests());
|
|
||||||
} else {
|
|
||||||
yield call(saveCurrentCount);
|
yield call(saveCurrentCount);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* users can complete donatable section but have less than 10 completed challenge
|
||||||
|
to show the donation modal.*/
|
||||||
|
if (donatableSectionRecentlyCompleted) {
|
||||||
|
yield put(preventSectionDonationRequests());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function* postChargeSaga({
|
export function* postChargeSaga({
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export const defaultDonationFormState = {
|
|||||||
const initialState = {
|
const initialState = {
|
||||||
appUsername: '',
|
appUsername: '',
|
||||||
isRandomCompletionThreshold: false,
|
isRandomCompletionThreshold: false,
|
||||||
recentlyClaimedBlock: null,
|
donatableSectionRecentlyCompleted: null,
|
||||||
currentChallengeId: store.get(CURRENT_CHALLENGE_KEY),
|
currentChallengeId: store.get(CURRENT_CHALLENGE_KEY),
|
||||||
examInProgress: false,
|
examInProgress: false,
|
||||||
isProcessing: false,
|
isProcessing: false,
|
||||||
@@ -138,10 +138,14 @@ export const reducer = handleActions(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[actionTypes.allowBlockDonationRequests]: (state, { payload }) => {
|
[actionTypes.allowSectionDonationRequests]: (state, { payload }) => {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
recentlyClaimedBlock: payload
|
donatableSectionRecentlyCompleted: {
|
||||||
|
block: payload.block,
|
||||||
|
module: payload.module,
|
||||||
|
superBlock: payload.superBlock
|
||||||
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[actionTypes.setRenderStartTime]: (state, { payload }) => {
|
[actionTypes.setRenderStartTime]: (state, { payload }) => {
|
||||||
@@ -277,9 +281,9 @@ export const reducer = handleActions(
|
|||||||
...state,
|
...state,
|
||||||
showDonationModal: true
|
showDonationModal: true
|
||||||
}),
|
}),
|
||||||
[actionTypes.preventBlockDonationRequests]: state => ({
|
[actionTypes.preventSectionDonationRequests]: state => ({
|
||||||
...state,
|
...state,
|
||||||
recentlyClaimedBlock: null
|
donatableSectionRecentlyCompleted: null
|
||||||
}),
|
}),
|
||||||
[actionTypes.setIsRandomCompletionThreshold]: (state, { payload }) => ({
|
[actionTypes.setIsRandomCompletionThreshold]: (state, { payload }) => ({
|
||||||
...state,
|
...state,
|
||||||
|
|||||||
@@ -391,6 +391,7 @@ export type ChallengeMeta = {
|
|||||||
superBlock: SuperBlocks;
|
superBlock: SuperBlocks;
|
||||||
title?: string;
|
title?: string;
|
||||||
challengeType?: number;
|
challengeType?: number;
|
||||||
|
blockType?: BlockTypes;
|
||||||
helpCategory: string;
|
helpCategory: string;
|
||||||
disableLoopProtectTests: boolean;
|
disableLoopProtectTests: boolean;
|
||||||
disableLoopProtectPreview: boolean;
|
disableLoopProtectPreview: boolean;
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
import { Certification } from '../../../shared/config/certification-settings';
|
import { Certification } from '../../../shared/config/certification-settings';
|
||||||
|
import superBlockStructure from '../../../curriculum/superblock-structure/full-stack.json';
|
||||||
import { randomBetween } from '../utils/random-between';
|
import { randomBetween } from '../utils/random-between';
|
||||||
import { getSessionChallengeData } from '../utils/session-storage';
|
import { getSessionChallengeData } from '../utils/session-storage';
|
||||||
import { ns as MainApp } from './action-types';
|
import { ns as MainApp } from './action-types';
|
||||||
@@ -22,8 +25,20 @@ export const isDonationModalOpenSelector = state =>
|
|||||||
state[MainApp].showDonationModal;
|
state[MainApp].showDonationModal;
|
||||||
export const isSignoutModalOpenSelector = state =>
|
export const isSignoutModalOpenSelector = state =>
|
||||||
state[MainApp].showSignoutModal;
|
state[MainApp].showSignoutModal;
|
||||||
export const recentlyClaimedBlockSelector = state =>
|
export const donatableSectionRecentlyCompletedSelector = state => {
|
||||||
state[MainApp].recentlyClaimedBlock;
|
const donatableSectionRecentlyCompletedState =
|
||||||
|
state[MainApp].donatableSectionRecentlyCompleted;
|
||||||
|
|
||||||
|
if (donatableSectionRecentlyCompletedState) {
|
||||||
|
const { block, module, superBlock } =
|
||||||
|
donatableSectionRecentlyCompletedState;
|
||||||
|
if (module) return { section: 'module', title: module, superBlock };
|
||||||
|
else if (block) return { section: 'block', title: block, superBlock };
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
export const donationFormStateSelector = state =>
|
export const donationFormStateSelector = state =>
|
||||||
state[MainApp].donationFormState;
|
state[MainApp].donationFormState;
|
||||||
export const updateCardStateSelector = state => state[MainApp].updateCardState;
|
export const updateCardStateSelector = state => state[MainApp].updateCardState;
|
||||||
@@ -35,7 +50,8 @@ export const showCertFetchStateSelector = state =>
|
|||||||
export const shouldRequestDonationSelector = state => {
|
export const shouldRequestDonationSelector = state => {
|
||||||
const completedChallengeCount = completedChallengesSelector(state).length;
|
const completedChallengeCount = completedChallengesSelector(state).length;
|
||||||
const isDonating = isDonatingSelector(state);
|
const isDonating = isDonatingSelector(state);
|
||||||
const recentlyClaimedBlock = recentlyClaimedBlockSelector(state);
|
const donatableSectionRecentlyCompleted =
|
||||||
|
donatableSectionRecentlyCompletedSelector(state);
|
||||||
const isRandomCompletionThreshold =
|
const isRandomCompletionThreshold =
|
||||||
isRandomCompletionThresholdSelector(state);
|
isRandomCompletionThresholdSelector(state);
|
||||||
|
|
||||||
@@ -46,8 +62,8 @@ export const shouldRequestDonationSelector = state => {
|
|||||||
// not before the 11th challenge has mounted)
|
// not before the 11th challenge has mounted)
|
||||||
if (completedChallengeCount < 10) return false;
|
if (completedChallengeCount < 10) return false;
|
||||||
|
|
||||||
// a block has been completed
|
// a block or module has been completed
|
||||||
if (recentlyClaimedBlock) return true;
|
if (donatableSectionRecentlyCompleted) return true;
|
||||||
|
|
||||||
const sessionChallengeData = getSessionChallengeData();
|
const sessionChallengeData = getSessionChallengeData();
|
||||||
/*
|
/*
|
||||||
@@ -256,6 +272,75 @@ export const certificatesByNameSelector = username => state => {
|
|||||||
export const userFetchStateSelector = state => state[MainApp].userFetchState;
|
export const userFetchStateSelector = state => state[MainApp].userFetchState;
|
||||||
export const allChallengesInfoSelector = state =>
|
export const allChallengesInfoSelector = state =>
|
||||||
state[MainApp].allChallengesInfo;
|
state[MainApp].allChallengesInfo;
|
||||||
|
|
||||||
|
export const completedChallengesIdsSelector = createSelector(
|
||||||
|
completedChallengesSelector,
|
||||||
|
completedChallenges => completedChallenges.map(node => node.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const completionStateSelector = createSelector(
|
||||||
|
[allChallengesInfoSelector, completedChallengesIdsSelector],
|
||||||
|
(allChallengesInfo, completedChallengesIds) => {
|
||||||
|
const chapters = superBlockStructure.chapters;
|
||||||
|
const { challengeNodes } = allChallengesInfo;
|
||||||
|
|
||||||
|
const getCompletionState = ({
|
||||||
|
chapters,
|
||||||
|
challenges,
|
||||||
|
completedChallengesIds
|
||||||
|
}) => {
|
||||||
|
const populateBlocks = blocks =>
|
||||||
|
blocks.map(block => {
|
||||||
|
const blockChallenges = challenges.filter(
|
||||||
|
({ block: blockName }) => blockName === block.dashedName
|
||||||
|
);
|
||||||
|
|
||||||
|
const completedBlockChallenges = blockChallenges.every(({ id }) =>
|
||||||
|
completedChallengesIds.includes(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: block.dashedName,
|
||||||
|
isCompleted:
|
||||||
|
completedBlockChallenges.length === blockChallenges.length
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const populateModules = modules =>
|
||||||
|
modules.map(module => {
|
||||||
|
const blocks = populateBlocks(module.blocks);
|
||||||
|
const isCompleted = blocks.every(block => block.isCompleted === true);
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: module.dashedName,
|
||||||
|
blocks,
|
||||||
|
isCompleted
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const allChapters = chapters.map(chapter => {
|
||||||
|
const modules = populateModules(chapter.modules);
|
||||||
|
const isCompleted = modules.every(
|
||||||
|
module => module.isCompleted === true
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: chapter.dashedName,
|
||||||
|
modules: populateModules(chapter.modules),
|
||||||
|
isCompleted
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return allChapters;
|
||||||
|
};
|
||||||
|
|
||||||
|
return getCompletionState({
|
||||||
|
chapters,
|
||||||
|
challenges: challengeNodes.map(({ challenge }) => challenge),
|
||||||
|
completedChallengesIds
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
export const userProfileFetchStateSelector = state =>
|
export const userProfileFetchStateSelector = state =>
|
||||||
state[MainApp].userProfileFetchState;
|
state[MainApp].userProfileFetchState;
|
||||||
export const usernameSelector = state => state[MainApp].appUsername;
|
export const usernameSelector = state => state[MainApp].appUsername;
|
||||||
|
|||||||
@@ -7,11 +7,13 @@ import { createSelector } from 'reselect';
|
|||||||
import { Button, Modal, Spacer } from '@freecodecamp/ui';
|
import { Button, Modal, Spacer } from '@freecodecamp/ui';
|
||||||
|
|
||||||
import Login from '../../../components/Header/components/login';
|
import Login from '../../../components/Header/components/login';
|
||||||
import { isSignedInSelector } from '../../../redux/selectors';
|
import {
|
||||||
|
isSignedInSelector,
|
||||||
|
completedChallengesIdsSelector
|
||||||
|
} from '../../../redux/selectors';
|
||||||
import { ChallengeFiles } from '../../../redux/prop-types';
|
import { ChallengeFiles } from '../../../redux/prop-types';
|
||||||
import { closeModal, submitChallenge } from '../redux/actions';
|
import { closeModal, submitChallenge } from '../redux/actions';
|
||||||
import {
|
import {
|
||||||
completedChallengesIdsSelector,
|
|
||||||
isCompletionModalOpenSelector,
|
isCompletionModalOpenSelector,
|
||||||
successMessageSelector,
|
successMessageSelector,
|
||||||
challengeFilesSelector,
|
challengeFilesSelector,
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ const ShowGeneric = ({
|
|||||||
title,
|
title,
|
||||||
challengeType,
|
challengeType,
|
||||||
helpCategory,
|
helpCategory,
|
||||||
|
blockType,
|
||||||
...challengePaths
|
...challengePaths
|
||||||
});
|
});
|
||||||
challengeMounted(challengeMeta.id);
|
challengeMounted(challengeMeta.id);
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
} from '../../../../../shared/config/challenge-types';
|
} from '../../../../../shared/config/challenge-types';
|
||||||
import { actionTypes as submitActionTypes } from '../../../redux/action-types';
|
import { actionTypes as submitActionTypes } from '../../../redux/action-types';
|
||||||
import {
|
import {
|
||||||
allowBlockDonationRequests,
|
allowSectionDonationRequests,
|
||||||
setIsProcessing,
|
setIsProcessing,
|
||||||
setRenderStartTime,
|
setRenderStartTime,
|
||||||
submitComplete,
|
submitComplete,
|
||||||
@@ -32,6 +32,7 @@ import { isSignedInSelector, userSelector } from '../../../redux/selectors';
|
|||||||
import { mapFilesToChallengeFiles } from '../../../utils/ajax';
|
import { mapFilesToChallengeFiles } from '../../../utils/ajax';
|
||||||
import { standardizeRequestBody } from '../../../utils/challenge-request-helpers';
|
import { standardizeRequestBody } from '../../../utils/challenge-request-helpers';
|
||||||
import postUpdate$ from '../utils/post-update';
|
import postUpdate$ from '../utils/post-update';
|
||||||
|
import { SuperBlocks } from '../../../../../shared/config/curriculum';
|
||||||
import { actionTypes } from './action-types';
|
import { actionTypes } from './action-types';
|
||||||
import {
|
import {
|
||||||
closeModal,
|
closeModal,
|
||||||
@@ -46,7 +47,8 @@ import {
|
|||||||
challengeTestsSelector,
|
challengeTestsSelector,
|
||||||
userCompletedExamSelector,
|
userCompletedExamSelector,
|
||||||
projectFormValuesSelector,
|
projectFormValuesSelector,
|
||||||
isBlockNewlyCompletedSelector
|
isBlockNewlyCompletedSelector,
|
||||||
|
isModuleNewlyCompletedSelector
|
||||||
} from './selectors';
|
} from './selectors';
|
||||||
|
|
||||||
function postChallenge(update) {
|
function postChallenge(update) {
|
||||||
@@ -230,7 +232,9 @@ export default function completionEpic(action$, state$) {
|
|||||||
nextChallengePath,
|
nextChallengePath,
|
||||||
challengeType,
|
challengeType,
|
||||||
superBlock,
|
superBlock,
|
||||||
|
blockType,
|
||||||
block,
|
block,
|
||||||
|
module,
|
||||||
blockHashSlug
|
blockHashSlug
|
||||||
} = challengeMetaSelector(state);
|
} = challengeMetaSelector(state);
|
||||||
// Default to submitChallengeComplete since we do not want the user to
|
// Default to submitChallengeComplete since we do not want the user to
|
||||||
@@ -254,19 +258,30 @@ export default function completionEpic(action$, state$) {
|
|||||||
? blockHashSlug
|
? blockHashSlug
|
||||||
: nextChallengePath;
|
: nextChallengePath;
|
||||||
|
|
||||||
const canAllowDonationRequest = (state, action) =>
|
const canAllowDonationRequest = (state, action) => {
|
||||||
isBlockNewlyCompletedSelector(state) &&
|
if (action.type !== submitActionTypes.submitComplete) return null;
|
||||||
action.type === submitActionTypes.submitComplete;
|
|
||||||
|
const donationData =
|
||||||
|
superBlock === SuperBlocks.FullStackDeveloper &&
|
||||||
|
blockType !== 'review' &&
|
||||||
|
isModuleNewlyCompletedSelector(state)
|
||||||
|
? { module, superBlock }
|
||||||
|
: superBlock !== SuperBlocks.FullStackDeveloper &&
|
||||||
|
isBlockNewlyCompletedSelector(state)
|
||||||
|
? { block, superBlock }
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return donationData ? allowSectionDonationRequests(donationData) : null;
|
||||||
|
};
|
||||||
|
|
||||||
return submitter(type, state).pipe(
|
return submitter(type, state).pipe(
|
||||||
concat(
|
concat(
|
||||||
of(setIsAdvancing(!isLastChallengeInBlock), setIsProcessing(false))
|
of(setIsAdvancing(!isLastChallengeInBlock), setIsProcessing(false))
|
||||||
),
|
),
|
||||||
mergeMap(x =>
|
mergeMap(x => {
|
||||||
canAllowDonationRequest(state, x)
|
const donationAction = canAllowDonationRequest(state, x);
|
||||||
? of(x, allowBlockDonationRequests({ superBlock, block }))
|
return donationAction ? of(x, donationAction) : of(x);
|
||||||
: of(x)
|
}),
|
||||||
),
|
|
||||||
mergeMap(x => of(x, setRenderStartTime(Date.now()))),
|
mergeMap(x => of(x, setRenderStartTime(Date.now()))),
|
||||||
tap(res => {
|
tap(res => {
|
||||||
if (res.type !== submitActionTypes.updateFailed) {
|
if (res.type !== submitActionTypes.updateFailed) {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ const initialState = {
|
|||||||
superBlock: '',
|
superBlock: '',
|
||||||
block: '',
|
block: '',
|
||||||
blockHashSlug: '/',
|
blockHashSlug: '/',
|
||||||
|
blockType: '',
|
||||||
id: '',
|
id: '',
|
||||||
isLastChallengeInBlock: false,
|
isLastChallengeInBlock: false,
|
||||||
nextChallengePath: '/',
|
nextChallengePath: '/',
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import { challengeTypes } from '../../../../../shared/config/challenge-types';
|
|||||||
import {
|
import {
|
||||||
completedChallengesSelector,
|
completedChallengesSelector,
|
||||||
allChallengesInfoSelector,
|
allChallengesInfoSelector,
|
||||||
isSignedInSelector
|
isSignedInSelector,
|
||||||
|
completionStateSelector,
|
||||||
|
completedChallengesIdsSelector
|
||||||
} from '../../../redux/selectors';
|
} from '../../../redux/selectors';
|
||||||
import {
|
import {
|
||||||
getCurrentBlockIds,
|
getCurrentBlockIds,
|
||||||
@@ -17,10 +19,6 @@ export const challengeMetaSelector = state => state[ns].challengeMeta;
|
|||||||
export const challengeHooksSelector = state => state[ns].challengeHooks;
|
export const challengeHooksSelector = state => state[ns].challengeHooks;
|
||||||
export const challengeTestsSelector = state => state[ns].challengeTests;
|
export const challengeTestsSelector = state => state[ns].challengeTests;
|
||||||
export const consoleOutputSelector = state => state[ns].consoleOut;
|
export const consoleOutputSelector = state => state[ns].consoleOut;
|
||||||
export const completedChallengesIdsSelector = createSelector(
|
|
||||||
completedChallengesSelector,
|
|
||||||
completedChallenges => completedChallenges.map(node => node.id)
|
|
||||||
);
|
|
||||||
export const isChallengeCompletedSelector = createSelector(
|
export const isChallengeCompletedSelector = createSelector(
|
||||||
[completedChallengesIdsSelector, challengeMetaSelector],
|
[completedChallengesIdsSelector, challengeMetaSelector],
|
||||||
(ids, meta) => ids.includes(meta.id)
|
(ids, meta) => ids.includes(meta.id)
|
||||||
@@ -152,6 +150,27 @@ export const isBlockNewlyCompletedSelector = state => {
|
|||||||
return completedPercentage === 100 && !completedChallengesIds.includes(id);
|
return completedPercentage === 100 && !completedChallengesIds.includes(id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isModuleNewlyCompletedSelector = state => {
|
||||||
|
const isBlockNewlyCompleted = isBlockNewlyCompletedSelector(state);
|
||||||
|
const { chapter, module, block } = challengeMetaSelector(state);
|
||||||
|
|
||||||
|
if (!isBlockNewlyCompleted || !chapter || !module) return;
|
||||||
|
|
||||||
|
const completionState = completionStateSelector(state);
|
||||||
|
|
||||||
|
const incompleteBlocksInModule = completionState
|
||||||
|
.find(({ name }) => name === chapter)
|
||||||
|
?.modules.find(({ name }) => name === module)
|
||||||
|
?.blocks.filter(({ isCompleted }) => !isCompleted);
|
||||||
|
|
||||||
|
// The module is completed if the newly completed block
|
||||||
|
// is the last block that has `isCompleted === false`.
|
||||||
|
return (
|
||||||
|
incompleteBlocksInModule?.length === 1 &&
|
||||||
|
incompleteBlocksInModule.some(({ name }) => name === block)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const attemptsSelector = state => state[ns].attempts;
|
export const attemptsSelector = state => state[ns].attempts;
|
||||||
export const canFocusEditorSelector = state => state[ns].canFocusEditor;
|
export const canFocusEditorSelector = state => state[ns].canFocusEditor;
|
||||||
export const visibleEditorsSelector = state => state[ns].visibleEditors;
|
export const visibleEditorsSelector = state => state[ns].visibleEditors;
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ interface Challenge {
|
|||||||
superBlock: SuperBlocks;
|
superBlock: SuperBlocks;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SuperBlockTreeViewProps {
|
interface SuperBlockAccordionPropsViewProps {
|
||||||
challenges: Challenge[];
|
challenges: Challenge[];
|
||||||
superBlock: SuperBlocks;
|
superBlock: SuperBlocks;
|
||||||
chosenBlock: string;
|
chosenBlock: string;
|
||||||
@@ -222,7 +222,7 @@ export const SuperBlockAccordion = ({
|
|||||||
superBlock,
|
superBlock,
|
||||||
chosenBlock,
|
chosenBlock,
|
||||||
completedChallengeIds
|
completedChallengeIds
|
||||||
}: SuperBlockTreeViewProps) => {
|
}: SuperBlockAccordionPropsViewProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { allChapters } = useMemo(() => {
|
const { allChapters } = useMemo(() => {
|
||||||
const populateBlocks = (blocks: { dashedName: string }[]) =>
|
const populateBlocks = (blocks: { dashedName: string }[]) =>
|
||||||
|
|||||||
@@ -86,6 +86,8 @@ exports.createChallengePages = function (
|
|||||||
disableLoopProtectPreview,
|
disableLoopProtectPreview,
|
||||||
certification,
|
certification,
|
||||||
superBlock,
|
superBlock,
|
||||||
|
chapter,
|
||||||
|
module,
|
||||||
block,
|
block,
|
||||||
fields: { slug, blockHashSlug },
|
fields: { slug, blockHashSlug },
|
||||||
required = [],
|
required = [],
|
||||||
@@ -108,6 +110,8 @@ exports.createChallengePages = function (
|
|||||||
disableLoopProtectTests,
|
disableLoopProtectTests,
|
||||||
disableLoopProtectPreview,
|
disableLoopProtectPreview,
|
||||||
superBlock,
|
superBlock,
|
||||||
|
chapter,
|
||||||
|
module,
|
||||||
block,
|
block,
|
||||||
isFirstStep: getIsFirstStepInBlock(index, allChallengeEdges),
|
isFirstStep: getIsFirstStepInBlock(index, allChallengeEdges),
|
||||||
template,
|
template,
|
||||||
|
|||||||
@@ -313,6 +313,90 @@ test.describe('Donation modal appearance logic - Certified user claiming a new b
|
|||||||
await completeFrontEndCert(page, 1);
|
await completeFrontEndCert(page, 1);
|
||||||
await expect(donationModal).toBeHidden();
|
await expect(donationModal).toBeHidden();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("should not appear if the user has completed a new FSD block, but the block's module is not completed", async ({
|
||||||
|
page
|
||||||
|
}) => {
|
||||||
|
await page.goto(
|
||||||
|
'/learn/full-stack-developer/review-basic-html/basic-html-review'
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.getByRole('checkbox', { name: /Review/ }).click();
|
||||||
|
await page.getByRole('button', { name: 'Submit', exact: true }).click();
|
||||||
|
await page.getByRole('button', { name: /Submit and go/ }).click();
|
||||||
|
|
||||||
|
const donationModal = page
|
||||||
|
.getByRole('dialog')
|
||||||
|
.filter({ hasText: 'Become a Supporter' });
|
||||||
|
await expect(donationModal).toBeHidden();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not appear if FSD review module is completed', async ({
|
||||||
|
page
|
||||||
|
}) => {
|
||||||
|
await page.goto('/learn/full-stack-developer/review-html/review-html');
|
||||||
|
await page.getByRole('checkbox', { name: /Review/ }).click();
|
||||||
|
await page.getByRole('button', { name: 'Submit', exact: true }).click();
|
||||||
|
await page.getByRole('button', { name: /Submit and go/ }).click();
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
const donationModal = page
|
||||||
|
.getByRole('dialog')
|
||||||
|
.filter({ hasText: 'Become a Supporter' });
|
||||||
|
await expect(donationModal).toBeHidden();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Donation modal appearance logic - Certified user claiming a new module', () => {
|
||||||
|
test.use({ storageState: 'playwright/.auth/certified-user.json' });
|
||||||
|
execSync('node ./tools/scripts/seed/seed-demo-user --almost-certified-user');
|
||||||
|
|
||||||
|
test('should appear if the user has just completed a new module', async ({
|
||||||
|
page
|
||||||
|
}) => {
|
||||||
|
test.setTimeout(40000);
|
||||||
|
|
||||||
|
// Go to the last lecture of the Welcome to freeCodeCamp block.
|
||||||
|
// This lecture is not added to the seed data, so it is not completed.
|
||||||
|
// By completing this lecture, we claim both the block and its module.
|
||||||
|
await page.goto(
|
||||||
|
'/learn/full-stack-developer/lecture-welcome-to-freecodecamp/how-can-you-build-effective-learning-habits-and-work-smarter'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wait for the page content to render
|
||||||
|
// TODO: Change the selector to `getByRole('radiogroup')` when we have migrated the MCQ component to fcc/ui
|
||||||
|
await expect(page.locator("div[class='video-quiz-options']")).toHaveCount(
|
||||||
|
3
|
||||||
|
);
|
||||||
|
|
||||||
|
const radioGroups = await page
|
||||||
|
.locator("div[class='video-quiz-options']")
|
||||||
|
.all();
|
||||||
|
|
||||||
|
await radioGroups[0].getByRole('radio').nth(2).click({ force: true });
|
||||||
|
await radioGroups[1].getByRole('radio').nth(2).click({ force: true });
|
||||||
|
await radioGroups[2].getByRole('radio').nth(3).click({ force: true });
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: /Check your answer/ }).click();
|
||||||
|
await page.getByRole('button', { name: /Submit and go/ }).click();
|
||||||
|
|
||||||
|
const donationModal = page
|
||||||
|
.getByRole('dialog')
|
||||||
|
.filter({ hasText: 'Become a Supporter' });
|
||||||
|
await expect(donationModal).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
donationModal.getByText(
|
||||||
|
'This is a 20 second animated advertisement to encourage campers to become supporters of freeCodeCamp. The animation starts with a teddy bear who becomes a supporter. As a result, distracting pop-ups disappear and the bear gets to complete all of its goals. Then, it graduates and becomes an education super hero helping people around the world.'
|
||||||
|
)
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
// Second part of the modal.
|
||||||
|
// Use `slowExpect` as we need to wait 20s for this part to show up.
|
||||||
|
await slowExpect(
|
||||||
|
donationModal.getByText(
|
||||||
|
'Nicely done. You just completed Getting Started with freeCodeCamp.'
|
||||||
|
)
|
||||||
|
).toBeVisible();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe('Donation modal appearance logic - Certified user', () => {
|
test.describe('Donation modal appearance logic - Certified user', () => {
|
||||||
|
|||||||
@@ -12251,6 +12251,18 @@ module.exports.fullyCertifiedUser = {
|
|||||||
id: '671144cdcc01d73f7dd79dc9',
|
id: '671144cdcc01d73f7dd79dc9',
|
||||||
challengeType: 0,
|
challengeType: 0,
|
||||||
files: []
|
files: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
completedDate: 1729240849345,
|
||||||
|
id: '6734ddabad59e593a49afafe'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
completedDate: 1729240849345,
|
||||||
|
id: '6734e2c5780912abd874e79c'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
completedDate: 1729240849345,
|
||||||
|
id: '6763500bd5a85d5898cc21a9'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
completedExams: [
|
completedExams: [
|
||||||
|
|||||||
Reference in New Issue
Block a user