feat: replace donate-form with multi-tier-donation-form (#52091)

This commit is contained in:
Ahmad Abdolsaheb
2023-11-02 12:37:53 +03:00
committed by GitHub
parent bce1b9915b
commit 16e7a04a99
13 changed files with 320 additions and 410 deletions
@@ -12,8 +12,6 @@ import envData from '../../config/env.json';
import { getLangCode } from '../../../shared/config/i18n';
import FreeCodeCampLogo from '../assets/icons/freecodecamp';
import MicrosoftLogo from '../assets/icons/microsoft-logo';
import DonateForm from '../components/Donation/donate-form';
import { createFlashMessage } from '../components/Flash/redux';
import { Loader, Spacer } from '../components/helpers';
import RedirectHome from '../components/redirect-home';
@@ -41,6 +39,7 @@ import {
certTypes,
certTypeTitleMap
} from '../../../shared/config/certification-settings';
import MultiTierDonationForm from '../components/Donation/multi-tier-donation-form';
import ShowProjectLinks from './show-project-links';
const { clientLocale } = envData;
@@ -271,7 +270,7 @@ const ShowCertification = (props: ShowCertificationProps): JSX.Element => {
xs={12}
data-playwright-test-label='donation-form'
>
<DonateForm
<MultiTierDonationForm
defaultTheme={Themes.Default}
handleProcessing={handleProcessing}
isMinimalForm={true}
+6 -19
View File
@@ -27,11 +27,7 @@ import Spacer from '../helpers/spacer';
import { Themes } from '../settings/theme';
import { DonateFormState } from '../../redux/types';
import type { CompletedChallenge } from '../../redux/prop-types';
import {
CENTS_IN_DOLLAR,
convertToTimeContributed,
formattedAmountLabel
} from './utils';
import { CENTS_IN_DOLLAR, formattedAmountLabel } from './utils';
import DonateCompletion from './donate-completion';
import PatreonButton from './patreon-button';
import PaypalButton from './paypal-button';
@@ -220,9 +216,6 @@ class DonateForm extends Component<DonateFormProps, DonateFormComponentState> {
const showMinimalPayments =
isSignedIn && (isMinimalForm || !isDonating) && threeChallengesCompleted;
const confirmationMessage = t('donate.confirm-monthly', {
usd: formattedAmountLabel(donationAmount)
});
const confirmationWithEditAmount = (
<>
{t('donate.confirm-multitier', {
@@ -242,11 +235,12 @@ class DonateForm extends Component<DonateFormProps, DonateFormComponentState> {
};
return (
<>
<b className={confirmationClass()}>
{editAmount ? confirmationWithEditAmount : confirmationMessage}
</b>
<b className={confirmationClass()}>{confirmationWithEditAmount}</b>
<Spacer size={editAmount ? 'small' : 'medium'} />
<fieldset className={'donate-btn-group security-legend'}>
<fieldset
data-playwright-test-label='donation-form'
className={'donate-btn-group security-legend'}
>
<legend>
<SecurityLockIcon />
{t('donate.secure-donation')}
@@ -296,15 +290,8 @@ class DonateForm extends Component<DonateFormProps, DonateFormComponentState> {
}
renderPageForm() {
const { donationAmount } = this.state;
const { t } = this.props;
const usd = formattedAmountLabel(donationAmount);
const hours = convertToTimeContributed(donationAmount);
const donationDescription = t('donate.your-donation-2', { usd, hours });
return (
<>
<p className='donation-description'>{donationDescription}</p>
<div>{this.renderButtonGroup()}</div>
</>
);
+34 -365
View File
@@ -1,28 +1,13 @@
import { Modal, Button } from '@freecodecamp/react-bootstrap';
import {
Tabs,
TabsContent,
TabsTrigger,
TabsList,
Col,
Row
} from '@freecodecamp/ui';
import { Col, Row } from '@freecodecamp/ui';
import { WindowLocation } from '@reach/router';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { useFeature } from '@growthbook/growthbook-react';
import { goToAnchor } from 'react-scrollable-anchor';
import { bindActionCreators, Dispatch, AnyAction } from 'redux';
import { createSelector } from 'reselect';
import {
PaymentContext,
subscriptionAmounts,
defaultDonation,
defaultTierAmount,
type DonationAmount
} from '../../../../shared/config/donation-settings';
import BearProgressModal from '../../assets/images/components/bear-progress-modal';
import BearBlockCompletion from '../../assets/images/components/bear-block-completion-modal';
import { closeDonationModal, executeGA } from '../../redux/actions';
@@ -33,8 +18,8 @@ import {
import { isLocationSuperBlock } from '../../utils/path-parsers';
import { playTone } from '../../utils/tone';
import { Spacer } from '../helpers';
import DonateForm from './donate-form';
import { formattedAmountLabel, convertToTimeContributed } from './utils';
import { PaymentContext } from '../../../../shared/config/donation-settings'; //
import MultiTierDonationForm from './multi-tier-donation-form';
type RecentlyClaimedBlock = null | { block: string; superBlock: string };
@@ -77,321 +62,55 @@ const Illustration = ({
);
};
function getctaNumberBetween1To10() {
const min = 1;
const max = 10;
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function ModalHeader({
closeLabel,
ctaNumber,
showMultiTier,
recentlyClaimedBlock
}: {
closeLabel: boolean;
ctaNumber: number;
showMultiTier: boolean;
recentlyClaimedBlock: RecentlyClaimedBlock;
}) {
const { t } = useTranslation();
return (
<div className='text-center block-modal-text'>
<Row>
{!closeLabel && (
<Col sm={10} smOffset={1} xs={12}>
{recentlyClaimedBlock !== null && (
<b>
{t('donate.nicely-done', {
block: t(
`intro:${recentlyClaimedBlock.superBlock}.blocks.${recentlyClaimedBlock.block}.title`
)
})}
</b>
)}
{showMultiTier ? (
<h2>{t('donate.help-us-develop')}</h2>
) : (
<b>{t(`donate.progress-modal-cta-${ctaNumber}`)}</b>
)}
</Col>
<Row className='text-center block-modal-text'>
<Col sm={10} smOffset={1} xs={12}>
{recentlyClaimedBlock !== null && (
<b>
{t('donate.nicely-done', {
block: t(
`intro:${recentlyClaimedBlock.superBlock}.blocks.${recentlyClaimedBlock.block}.title`
)
})}
</b>
)}
</Row>
<Spacer size='small' />
</div>
);
}
function SelectionTabs({
loadElementsIndividually,
donationAmount,
setDonationAmount,
setShowDonateForm
}: {
loadElementsIndividually: boolean;
donationAmount: DonationAmount;
setDonationAmount: React.Dispatch<React.SetStateAction<DonationAmount>>;
setShowDonateForm: React.Dispatch<React.SetStateAction<boolean>>;
}) {
const { t } = useTranslation();
return (
<Row className={'donate-btn-group'}>
<Col
xs={12}
{...(loadElementsIndividually && {
className: 'two-seconds-delay-fade-in'
})}
>
<b>
{t('donate.confirm-monthly', {
usd: formattedAmountLabel(donationAmount)
})}
</b>
<Spacer size='small' />
<Tabs
className={'donate-btn-group'}
defaultValue={donationAmount.toString()}
>
<TabsList className='nav-lists'>
{subscriptionAmounts.map(value => (
<TabsTrigger
key={value}
value={value.toString()}
onClick={() => setDonationAmount(value)}
>
${formattedAmountLabel(value)}
</TabsTrigger>
))}
</TabsList>
<Spacer size='small' />
{subscriptionAmounts.map(value => {
const usd = formattedAmountLabel(donationAmount);
const hours = convertToTimeContributed(donationAmount);
const donationDescription = t('donate.your-donation-2', {
usd,
hours
});
return (
<TabsContent
key={value}
className='tab-content'
value={value.toString()}
>
<p>{donationDescription}</p>
</TabsContent>
);
})}
</Tabs>
<Button
block={true}
bsStyle='primary'
className='text-center confirm-donation-btn donate-btn-group'
type='submit'
onClick={() => setShowDonateForm(true)}
>
{t('buttons.donate')}
</Button>
<Spacer size='medium' />
<h2>{t('donate.help-us-develop')}</h2>
</Col>
</Row>
);
}
function CloseButtonRow({
showSkipButton,
isDisabled,
closeLabel,
donationAttempted,
closeDonationModal
}: {
showSkipButton: boolean;
isDisabled: boolean;
closeLabel: boolean;
donationAttempted: boolean;
closeDonationModal: () => void;
}) {
const { t } = useTranslation();
return (
<Row>
<Col
sm={4}
smOffset={4}
xs={8}
xsOffset={2}
className={showSkipButton ? 'no-delay-fade-in' : 'no-opacity'}
>
<Col sm={4} smOffset={4} xs={8} xsOffset={2}>
<Button
bsSize='sm'
bsStyle='primary'
className='btn-link close-button'
onClick={closeDonationModal}
tabIndex='0'
disabled={isDisabled}
>
{closeLabel ? t('buttons.close') : t('buttons.ask-later')}
{donationAttempted ? t('buttons.close') : t('buttons.ask-later')}
</Button>
</Col>
</Row>
);
}
function DonationBody({
closeLabel,
ctaNumber,
showMultiTier,
recentlyClaimedBlock,
donationAmount,
loadElementsIndividually,
handleProcessing,
setShowDonateForm,
closeDonationModal,
isDisabled,
showSkipButton
}: {
closeLabel: boolean;
ctaNumber: number;
showMultiTier: boolean;
recentlyClaimedBlock: RecentlyClaimedBlock;
donationAmount: DonationAmount;
loadElementsIndividually: boolean;
handleProcessing: () => void;
setShowDonateForm: React.Dispatch<React.SetStateAction<boolean>>;
closeDonationModal: () => void;
isDisabled: boolean;
showSkipButton: boolean;
}) {
return (
<>
<ModalHeader
closeLabel={closeLabel}
ctaNumber={ctaNumber}
recentlyClaimedBlock={recentlyClaimedBlock}
showMultiTier={showMultiTier}
/>
<DonationFormRow
donationAmount={donationAmount}
handleProcessing={handleProcessing}
showMultiTier={showMultiTier}
loadElementsIndividually={loadElementsIndividually}
setShowDonateForm={setShowDonateForm}
/>
<CloseButtonRow
closeDonationModal={closeDonationModal}
closeLabel={closeLabel}
isDisabled={isDisabled}
showSkipButton={showSkipButton}
/>
</>
);
}
function DonationFormRow({
handleProcessing,
loadElementsIndividually,
showMultiTier,
setShowDonateForm,
donationAmount
}: {
handleProcessing: () => void;
loadElementsIndividually: boolean;
showMultiTier: boolean;
setShowDonateForm: React.Dispatch<React.SetStateAction<boolean>>;
donationAmount: DonationAmount;
}) {
return (
<Row>
<Col
xs={12}
{...(loadElementsIndividually && {
className: 'two-seconds-delay-fade-in'
})}
>
<DonateForm
handleProcessing={handleProcessing}
isMinimalForm={true}
paymentContext={PaymentContext.Modal}
editAmount={
showMultiTier ? () => setShowDonateForm(false) : undefined
}
selectedDonationAmount={donationAmount}
/>
<Spacer size='medium' />
</Col>
</Row>
);
}
function MultiTierDonationBody({
closeLabel,
ctaNumber,
showMultiTier,
recentlyClaimedBlock,
donationAmount,
loadElementsIndividually,
setDonationAmount,
setShowDonateForm,
closeDonationModal,
isDisabled,
showSkipButton,
showDonateForm,
handleProcessing
}: {
closeLabel: boolean;
ctaNumber: number;
showMultiTier: boolean;
recentlyClaimedBlock: RecentlyClaimedBlock;
donationAmount: DonationAmount;
loadElementsIndividually: boolean;
setDonationAmount: React.Dispatch<React.SetStateAction<DonationAmount>>;
setShowDonateForm: React.Dispatch<React.SetStateAction<boolean>>;
closeDonationModal: () => void;
isDisabled: boolean;
showSkipButton: boolean;
showDonateForm: boolean;
handleProcessing: () => void;
}) {
return (
<>
<div {...(showDonateForm && { className: 'hide' })}>
<ModalHeader
closeLabel={closeLabel}
ctaNumber={ctaNumber}
recentlyClaimedBlock={recentlyClaimedBlock}
showMultiTier={showMultiTier}
/>
<SelectionTabs
donationAmount={donationAmount}
loadElementsIndividually={loadElementsIndividually}
setDonationAmount={setDonationAmount}
setShowDonateForm={setShowDonateForm}
/>
<CloseButtonRow
closeDonationModal={closeDonationModal}
closeLabel={closeLabel}
isDisabled={isDisabled}
showSkipButton={showSkipButton}
/>
</div>
<div {...(!showDonateForm && { className: 'hide' })}>
<DonationFormRow
donationAmount={donationAmount}
handleProcessing={handleProcessing}
showMultiTier={showMultiTier}
loadElementsIndividually={loadElementsIndividually}
setShowDonateForm={setShowDonateForm}
/>
{closeLabel && (
<CloseButtonRow
closeDonationModal={closeDonationModal}
closeLabel={closeLabel}
isDisabled={isDisabled}
showSkipButton={showSkipButton}
/>
)}
</div>
</>
);
}
function DonateModal({
show,
closeDonationModal,
@@ -399,37 +118,13 @@ function DonateModal({
location,
recentlyClaimedBlock
}: DonateModalProps): JSX.Element {
const [closeLabel, setCloseLabel] = useState(false);
const [ctaNumber, setCtaNumber] = useState(0);
const [isDisabled, setIsDisabled] = useState(true);
const [showSkipButton, setShowSkipButton] = useState(false);
const [showDonateForm, setShowDonateForm] = useState(true);
const [donationAmount, setDonationAmount] = useState(
defaultDonation.donationAmount
);
const loadElementsIndividually = useFeature('load_elements_individually').on;
const showMultiTier = useFeature('multi-tier').on;
// test whether the conversions are being distributed properly
useFeature('aa-test-in-component');
const [donationAttempted, setDonationAttempted] = useState(false);
const [showHeaderAndFooter, setShowHeaderAndFooter] = useState(true);
const handleProcessing = () => {
setCloseLabel(true);
setDonationAttempted(true);
};
useEffect(() => {
if (loadElementsIndividually) {
const timer = setTimeout(() => {
setIsDisabled(false);
setShowSkipButton(true);
}, 4000);
return () => clearTimeout(timer);
} else {
setIsDisabled(false);
setShowSkipButton(true);
}
}, [loadElementsIndividually]);
useEffect(() => {
if (show) {
void playTone('donation');
@@ -443,17 +138,6 @@ function DonateModal({
}
}, [show, recentlyClaimedBlock, executeGA]);
useEffect(() => {
if (show) setCtaNumber(getctaNumberBetween1To10());
}, [show]);
useEffect(() => {
if (showMultiTier) {
setShowDonateForm(false);
setDonationAmount(defaultTierAmount);
}
}, [showMultiTier]);
const handleModalHide = () => {
// If modal is open on a SuperBlock page
if (isLocationSuperBlock(location)) {
@@ -472,35 +156,20 @@ function DonateModal({
<div className='donation-icon-container'>
<Illustration recentlyClaimedBlock={recentlyClaimedBlock} />
</div>
{showMultiTier ? (
<MultiTierDonationBody
recentlyClaimedBlock={recentlyClaimedBlock}
setDonationAmount={setDonationAmount}
setShowDonateForm={setShowDonateForm}
showDonateForm={showDonateForm}
showMultiTier={showMultiTier}
showSkipButton={showSkipButton}
{showHeaderAndFooter && !donationAttempted && (
<ModalHeader recentlyClaimedBlock={recentlyClaimedBlock} />
)}
<Spacer size='small' />
<MultiTierDonationForm
setShowHeaderAndFooter={setShowHeaderAndFooter}
handleProcessing={handleProcessing}
paymentContext={PaymentContext.Modal}
isMinimalForm={true}
/>
{(showHeaderAndFooter || donationAttempted) && (
<CloseButtonRow
closeDonationModal={closeDonationModal}
closeLabel={closeLabel}
ctaNumber={ctaNumber}
donationAmount={donationAmount}
handleProcessing={handleProcessing}
isDisabled={isDisabled}
loadElementsIndividually={loadElementsIndividually}
/>
) : (
<DonationBody
closeDonationModal={closeDonationModal}
closeLabel={closeLabel}
ctaNumber={ctaNumber}
donationAmount={donationAmount}
isDisabled={isDisabled}
handleProcessing={handleProcessing}
loadElementsIndividually={loadElementsIndividually}
recentlyClaimedBlock={recentlyClaimedBlock}
setShowDonateForm={setShowDonateForm}
showMultiTier={showMultiTier}
showSkipButton={showSkipButton}
donationAttempted={donationAttempted}
/>
)}
</Modal.Body>
+5 -15
View File
@@ -261,13 +261,6 @@ li.disabled > a {
width: auto;
}
.two-seconds-delay-fade-in {
opacity: 0;
pointer-events: none;
animation: opacity-animation 1s linear 100ms forwards;
animation-delay: 2s;
}
.no-delay-fade-in {
opacity: 0;
pointer-events: none;
@@ -285,11 +278,6 @@ li.disabled > a {
}
}
.donation-modal .btn-link:focus {
outline-width: 1px;
outline-style: solid;
}
.donation-modal .modal-title {
text-align: center;
font-weight: 600;
@@ -335,15 +323,17 @@ li.disabled > a {
}
}
.donation-modal [role='tablist'] button {
.donation-tier-selection [role='tablist'] button {
background-color: transparent;
}
.donation-modal [role='tablist'] button:hover:not([data-state='active']) {
.donation-tier-selection
[role='tablist']
button:hover:not([data-state='active']) {
background-color: var(--quaternary-background);
color: var(--quaternary-color);
}
.donation-modal [role='tablist'] button[data-state='active'] {
.donation-tier-selection [role='tablist'] button[data-state='active'] {
background-color: var(--quaternary-color);
}
@@ -0,0 +1,169 @@
import React, { useState, useEffect } from 'react';
import { Button } from '@freecodecamp/react-bootstrap';
import {
Tabs,
TabsContent,
TabsTrigger,
TabsList,
Col,
Row
} from '@freecodecamp/ui';
import { useTranslation } from 'react-i18next';
import { Spacer } from '../helpers';
import {
PaymentContext,
subscriptionAmounts,
defaultTierAmount,
type DonationAmount
} from '../../../../shared/config/donation-settings'; // You can further extract these into separate components and import them
import { Themes } from '../settings/theme';
import { formattedAmountLabel, convertToTimeContributed } from './utils';
import DonateForm from './donate-form';
type MultiTierDonationFormProps = {
setShowHeaderAndFooter?: (show: boolean) => void;
handleProcessing?: () => void;
paymentContext: PaymentContext;
isMinimalForm?: boolean;
defaultTheme?: Themes;
};
function SelectionTabs({
donationAmount,
setDonationAmount,
setShowDonateForm
}: {
donationAmount: DonationAmount;
setDonationAmount: React.Dispatch<React.SetStateAction<DonationAmount>>;
setShowDonateForm: React.Dispatch<React.SetStateAction<boolean>>;
}) {
const { t } = useTranslation();
const switchTab = (value: string): void => {
setDonationAmount(Number(value) as DonationAmount);
};
return (
<Row
className={'donate-btn-group donation-tier-selection'}
data-playwright-test-label='donation-tier-selector'
>
<Col xs={12}>
<b>
{t('donate.confirm-monthly', {
usd: formattedAmountLabel(donationAmount)
})}
</b>
<Spacer size='small' />
<Tabs
className={'donate-btn-group'}
defaultValue={donationAmount.toString()}
onValueChange={switchTab}
>
<TabsList className='nav-lists'>
{subscriptionAmounts.map(value => (
<TabsTrigger
key={value}
value={value.toString()}
onClick={() => setDonationAmount(value)}
>
${formattedAmountLabel(value)}
</TabsTrigger>
))}
</TabsList>
<Spacer size='small' />
{subscriptionAmounts.map(value => {
const usd = formattedAmountLabel(donationAmount);
const hours = convertToTimeContributed(donationAmount);
const donationDescription = t('donate.your-donation-2', {
usd,
hours
});
return (
<TabsContent
key={value}
className='tab-content'
value={value.toString()}
>
<p>{donationDescription}</p>
</TabsContent>
);
})}
</Tabs>
<Button
block={true}
bsStyle='primary'
className='text-center confirm-donation-btn donate-btn-group'
type='submit'
data-cy='donation-tier-selection-button'
onClick={() => setShowDonateForm(true)}
>
{t('buttons.donate')}
</Button>
<Spacer size='medium' />
</Col>
</Row>
);
}
function DonationFormRow({
handleProcessing,
isMinimalForm,
setShowDonateForm,
donationAmount
}: {
handleProcessing?: () => void;
isMinimalForm?: boolean;
setShowDonateForm: React.Dispatch<React.SetStateAction<boolean>>;
donationAmount: DonationAmount;
}) {
return (
<Row>
<Col xs={12}>
<DonateForm
handleProcessing={handleProcessing}
isMinimalForm={isMinimalForm}
paymentContext={PaymentContext.Modal}
editAmount={() => setShowDonateForm(false)}
selectedDonationAmount={donationAmount}
/>
<Spacer size='medium' />
</Col>
</Row>
);
}
const MultiTierDonationForm: React.FC<MultiTierDonationFormProps> = ({
handleProcessing,
setShowHeaderAndFooter,
isMinimalForm
}) => {
const [donationAmount, setDonationAmount] = useState(defaultTierAmount);
const [showDonateForm, setShowDonateForm] = useState(false);
useEffect(() => {
if (setShowHeaderAndFooter) setShowHeaderAndFooter(!showDonateForm);
}, [showDonateForm, setShowHeaderAndFooter]);
return (
<>
<div {...(showDonateForm && { className: 'hide' })}>
<SelectionTabs
donationAmount={donationAmount}
setDonationAmount={setDonationAmount}
setShowDonateForm={setShowDonateForm}
/>
</div>
<div {...(!showDonateForm && { className: 'hide' })}>
<DonationFormRow
donationAmount={donationAmount}
handleProcessing={handleProcessing}
setShowDonateForm={setShowDonateForm}
isMinimalForm={isMinimalForm}
/>
</div>
</>
);
};
export default MultiTierDonationForm;
@@ -163,6 +163,7 @@ const StripeCardForm = ({
bsStyle='primary'
className='confirm-donation-btn'
disabled={!stripe || !elements || isSubmitting}
data-cy='donation-confirmation-button'
type='submit'
>
{t('buttons.donate')}
-1
View File
@@ -310,7 +310,6 @@ fieldset[disabled] .btn-primary.focus {
text-decoration: underline;
background: transparent;
padding: 0;
outline-color: transparent;
}
.btn-link:hover,
.btn-link:focus:active {
+2 -2
View File
@@ -8,7 +8,7 @@ import { bindActionCreators } from 'redux';
import type { Dispatch } from 'redux';
import { createSelector } from 'reselect';
import DonateForm from '../components/Donation/donate-form';
import MultiTierDonationForm from '../components/Donation/multi-tier-donation-form';
import {
DonationText,
DonationOptionsAlertText,
@@ -93,7 +93,7 @@ function DonatePage({
</Alert>
) : null}
<DonationText />
<DonateForm paymentContext={PaymentContext.DonatePage} />
<MultiTierDonationForm paymentContext={PaymentContext.DonatePage} />
<Spacer size='exLarge' />
<hr />
<h2 data-playwright-test-label='faq-head' className={'text-center'}>
@@ -3,7 +3,10 @@ describe('Donate page', () => {
cy.visit('/donate');
cy.title().should('eq', 'Support our charity | freeCodeCamp.org');
cy.contains('Confirm your donation of $5 / month:').should('be.visible');
cy.contains('Confirm your donation of $20 / month:').should('be.visible');
cy.contains(
'Your $20 donation will provide 1,000 hours of learning to people around the world each month.'
).should('be.visible');
cy.contains('Frequently asked questions');
cy.contains('How can I get help with my donations?');
+3 -1
View File
@@ -3,11 +3,13 @@ describe('Donate page', () => {
cy.task('seed', ['certified-user']);
cy.login('certified-user');
cy.visit('/donate');
cy.get("[data-cy='donation-tier-selection-button']").click();
cy.get('.donation-elements', { timeout: 10000 }).within(() => {
cy.fillElementsInput('cardNumber', '4242424242424242');
cy.fillElementsInput('cardExpiry', '1025');
});
cy.get('.confirm-donation-btn').click();
cy.get("[data-cy='donation-confirmation-button']").click();
cy.contains('We are processing your donation.').should('be.visible');
cy.contains('Thank you for being a supporter.', { timeout: 10000 }).should(
'be.visible'
+2 -2
View File
@@ -15,7 +15,7 @@ test.describe('Certification page - Non Microsoft', () => {
const donationText = donationSection.getByTestId('donation-text');
await expect(donationText).toHaveText(translations.donate['only-you']);
const donationForm = donationSection.getByTestId('donation-form');
const donationForm = donationSection.getByTestId('donation-tier-selector');
await expect(donationForm).toBeVisible();
});
@@ -129,7 +129,7 @@ test.describe('Certification page - Microsoft', () => {
const donationText = donationSection.getByTestId('donation-text');
await expect(donationText).toHaveText(translations.donate['only-you']);
const donationForm = donationSection.getByTestId('donation-form');
const donationForm = donationSection.getByTestId('donation-tier-selector');
await expect(donationForm).toBeVisible();
});
+91
View File
@@ -1,6 +1,8 @@
import { test, expect, type Page } from '@playwright/test';
import translations from '../client/i18n/locales/english/translations.json';
test.use({ storageState: 'playwright/.auth/certified-user.json' });
const pageElements = {
mainHeading: 'main-head',
donateText1: 'donate-text-1',
@@ -25,6 +27,30 @@ const frequentlyAskedQuestions = [
translations.donate['anything-else']
];
const donationStringReplacements = {
usdPlaceHolder: '{{usd}}',
hoursPlaceHolder: '{{hours}}'
};
const donationFormStrings = {
conformTwentyDollar: translations.donate['confirm-monthly'].replace(
donationStringReplacements.usdPlaceHolder,
'20'
),
confirmFiveDollars: translations.donate['confirm-monthly'].replace(
donationStringReplacements.usdPlaceHolder,
'5'
),
twentyDollarsLearningContribution: translations.donate['your-donation-2']
.replace(donationStringReplacements.usdPlaceHolder, '20')
.replace(donationStringReplacements.hoursPlaceHolder, '1,000'),
fiveDollarsLearningContribution: translations.donate['your-donation-2']
.replace(donationStringReplacements.usdPlaceHolder, '5')
.replace(donationStringReplacements.hoursPlaceHolder, '250'),
editAmount: translations.donate['edit-amount'],
donate: translations.buttons.donate
};
let page: Page;
test.beforeAll(async ({ browser }) => {
page = await browser.newPage();
@@ -38,6 +64,71 @@ test.describe('Donate Page', () => {
);
});
test('should select $20 tier by default', async () => {
await expect(
page.getByText(donationFormStrings.conformTwentyDollar)
).toBeVisible();
const tabs = await page.$$('[role="tab"]');
expect(tabs.length).toBe(4);
for (const tab of tabs) {
const tabText = await tab.innerText();
expect(['$5', '$10', '$20', '$40']).toContain(tabText);
if (tabText === '$20') {
const isActive = await tab.getAttribute('data-state');
expect(isActive).toBe('active');
} else {
const isActive = await tab.getAttribute('data-state');
expect(isActive).not.toBe('active');
}
}
await expect(
page.getByText(donationFormStrings.twentyDollarsLearningContribution)
).toBeVisible();
});
test('should make $5 tier selectable', async () => {
await page.click('[role="tab"]:has-text("$5")');
await expect(
page.getByText(donationFormStrings.confirmFiveDollars)
).toBeVisible();
await expect(
page.getByText(donationFormStrings.fiveDollarsLearningContribution)
).toBeVisible();
});
test('should switch between tier selection and payment options', async () => {
// Tier selection
await page.click('[role="tab"]:has-text("$5")');
await expect(
page.getByText(donationFormStrings.confirmFiveDollars)
).toBeVisible();
await expect(
page.getByText(donationFormStrings.fiveDollarsLearningContribution)
).toBeVisible();
await page.click(`button:has-text("${donationFormStrings.donate}")`);
// Donation form
const isEditButtonVisible = await page.isVisible(
`button:has-text("${donationFormStrings.editAmount}")`
);
expect(isEditButtonVisible).toBeTruthy();
await expect(page.getByTestId('donation-form')).toBeVisible();
await page.click(`button:has-text("${donationFormStrings.editAmount}")`);
// Tier selection
await expect(
page.getByText(donationFormStrings.confirmFiveDollars)
).toBeVisible();
await expect(
page.getByText(donationFormStrings.fiveDollarsLearningContribution)
).toBeVisible();
});
test('should display the main heading', async () => {
const mainHeading = page.getByTestId(pageElements.mainHeading);
await expect(mainHeading).toHaveText(translations.donate['help-more']);
+1 -1
View File
@@ -14,7 +14,7 @@ export const defaultDonation: DonationConfig = {
donationDuration: 'month'
};
export const defaultTierAmount = 2000;
export const defaultTierAmount: DonationAmount = 2000;
export const onetimeSKUConfig = {
live: [