From daf0f11c4968cf198486bd20307e6a0f2f41ae6d Mon Sep 17 00:00:00 2001 From: "dimitar.nizamov" Date: Fri, 10 Feb 2023 17:19:35 +0100 Subject: [PATCH 01/12] 1272-finalization - fix error translations and add loading states on session loading --- public/locales/bg/donation-flow.json | 13 +++- public/locales/en/donation-flow.json | 10 +++- src/components/campaigns/CampaignCard.tsx | 2 +- src/components/campaigns/InlineDonation.tsx | 2 +- src/components/common/form/RadioButton.tsx | 59 ++++++++++++------- .../common/form/RadioButtonGroup.tsx | 3 + .../donation-flow/DonationFlowForm.tsx | 12 ++-- .../donation-flow/common/RadioCardGroup.tsx | 19 +++++- src/components/donation-flow/steps/Amount.tsx | 7 ++- .../steps/authentication/InlineLoginForm.tsx | 2 +- .../PaymentDetailsStripeForm.tsx | 7 ++- .../steps/payment-method/PaymentMethod.tsx | 9 ++- src/components/one-time-donation/Steps.tsx | 6 +- 13 files changed, 111 insertions(+), 40 deletions(-) diff --git a/public/locales/bg/donation-flow.json b/public/locales/bg/donation-flow.json index 9cfe2d6c6..e0ca4bf7c 100644 --- a/public/locales/bg/donation-flow.json +++ b/public/locales/bg/donation-flow.json @@ -83,7 +83,12 @@ "label": "Продължете без регистрация ", "description": "Продължавайки без регистрация, нямате възможност да запазите дарението в историята на профила си както и да правите месечни дарения по избрана кампания" }, - "password": "Парола", + "field": { + "password": "Парола", + "email": { + "error": "Трябва да въведете валиден имейл" + } + }, "alert": { "authenticate": { "title": "Избирайки да се впишете, ще можете да", @@ -104,8 +109,14 @@ "anonymous": { "label": "Искам да съм анонимен", "description": "Ако останете анонимен името ви няма да бъде показано на кампанията" + }, + "privacy": { + "error": "Трябва да приемете политиката за поверителност" } }, + "alerts": { + "error": "Нещо се обърка, моля опитайте пак или презаредете страницата" + }, "total": "Общо" } }, diff --git a/public/locales/en/donation-flow.json b/public/locales/en/donation-flow.json index 0f8554cf5..c5ebb4c1b 100644 --- a/public/locales/en/donation-flow.json +++ b/public/locales/en/donation-flow.json @@ -87,7 +87,12 @@ "label": "Continue without registration", "description": "You will not be able to get a donation certificate or a list of your donations. If you still want to receive a receipt, please share your email - it will not be visible in the platform" }, - "password": "Password", + "field": { + "password": "Password", + "email": { + "error": "You have to enter a valid email" + } + }, "alert": { "authenticate": { "title": "Choosing to login you will be able to", @@ -109,6 +114,9 @@ "anonymous": { "label": "I want to be anonymous", "description": "If you choose to be anonymous, your name will not be visible in the campaign" + }, + "privacy": { + "error": "You have to accept the privacy policy" } } } diff --git a/src/components/campaigns/CampaignCard.tsx b/src/components/campaigns/CampaignCard.tsx index fc50d6e95..644f2f08b 100644 --- a/src/components/campaigns/CampaignCard.tsx +++ b/src/components/campaigns/CampaignCard.tsx @@ -164,7 +164,7 @@ export default function CampaignCard({ campaign, index }: Props) { } diff --git a/src/components/common/form/RadioButton.tsx b/src/components/common/form/RadioButton.tsx index 2375890f5..941bc55ac 100644 --- a/src/components/common/form/RadioButton.tsx +++ b/src/components/common/form/RadioButton.tsx @@ -1,6 +1,6 @@ import { Check } from '@mui/icons-material' import { styled, lighten } from '@mui/material/styles' -import { FormControlLabel, Radio, Typography, RadioProps } from '@mui/material' +import { FormControlLabel, Radio, Typography, RadioProps, Skeleton } from '@mui/material' import theme from 'common/theme' import React from 'react' @@ -84,6 +84,7 @@ type RadioButtonProps = { label: string value: string | number disabled?: boolean + loading?: boolean muiRadioButtonProps?: Partial } @@ -103,27 +104,45 @@ const disabledCheckStlyes = { borderRadius: theme.borders.round, color: theme.palette.text.disabled, } -function RadioButton({ checked, label, muiRadioButtonProps, value, disabled }: RadioButtonProps) { +function RadioButton({ + checked, + label, + muiRadioButtonProps, + value, + disabled, + loading, +}: RadioButtonProps) { return ( - {label}} - control={ - } - checkedIcon={ - - } - {...muiRadioButtonProps} - /> - } - /> + {loading ? ( + + ) : ( + {label}} + control={ + } + checkedIcon={ + + } + {...muiRadioButtonProps} + /> + } + /> + )} ) } diff --git a/src/components/common/form/RadioButtonGroup.tsx b/src/components/common/form/RadioButtonGroup.tsx index 47fb1ecdb..18458bcb4 100644 --- a/src/components/common/form/RadioButtonGroup.tsx +++ b/src/components/common/form/RadioButtonGroup.tsx @@ -22,6 +22,7 @@ export type RadioButtonGroup = { name: string options: RadioButtonGroupOptions[] disabled?: boolean + loading?: boolean columns?: number muiRadioGroupProps?: Partial /** @@ -39,6 +40,7 @@ export default function RadioButtonGroup({ name, options, disabled, + loading, columns = 2, muiRadioGroupProps, muiRadioButtonGridProps, @@ -69,6 +71,7 @@ export default function RadioButtonGroup({ { if (session?.user) { formikRef.current?.setFieldValue('email', session.user.email) + formikRef.current?.setFieldValue('authentication', DonationFormAuthState.AUTHENTICATED) + formikRef.current?.setFieldValue('isAnonymous', false) return } formikRef.current?.setFieldValue('email', '') @@ -140,7 +142,7 @@ export function DonationFlowForm() { setSubmitPaymentLoading(false) setPaymentError({ type: 'invalid_request_error', - message: t('alerts.error'), + message: t('step.summary.alerts.error'), }) return } @@ -161,7 +163,7 @@ export function DonationFlowForm() { setSubmitPaymentLoading(false) setPaymentError({ type: 'invalid_request_error', - message: t('alerts.error'), + message: t('step.summary.alerts.error'), }) return } @@ -180,7 +182,7 @@ export function DonationFlowForm() { setSubmitPaymentLoading(false) setPaymentError({ type: 'invalid_request_error', - message: t('alerts.error'), + message: t('step.summary.alerts.error'), }) return } diff --git a/src/components/donation-flow/common/RadioCardGroup.tsx b/src/components/donation-flow/common/RadioCardGroup.tsx index 23695939d..128c5ee8f 100644 --- a/src/components/donation-flow/common/RadioCardGroup.tsx +++ b/src/components/donation-flow/common/RadioCardGroup.tsx @@ -10,6 +10,7 @@ import { RadioGroupProps, Stack, Grid, + Skeleton, } from '@mui/material' import { styled, lighten } from '@mui/material/styles' import theme from 'common/theme' @@ -29,10 +30,18 @@ interface StyledRadioCardItemProps extends CardProps { control: React.ReactNode icon: React.ReactNode disabled?: boolean + loading?: boolean selected?: boolean } -function RadioCardItem({ control, icon, selected, disabled, ...rest }: StyledRadioCardItemProps) { +function RadioCardItem({ + control, + icon, + selected, + disabled, + loading, + ...rest +}: StyledRadioCardItemProps) { const selectedStyles = { backgroundColor: selected ? lighten(theme.palette.primary.light, 0.7) : 'inherit', } @@ -50,7 +59,9 @@ function RadioCardItem({ control, icon, selected, disabled, ...rest }: StyledRad styles = selectedStyles } - return ( + return loading ? ( + + ) : ( {icon} @@ -71,6 +82,7 @@ export interface RadioCardGroupProps extends RadioGroupProps { options: Option[] name: string columns: 1 | 2 | 3 | 4 | 6 | 12 + loading?: boolean } /** @@ -91,7 +103,7 @@ export interface RadioCardGroupProps extends RadioGroupProps { * icon: , * }, */ -function RadioCardGroup({ options, name, columns }: RadioCardGroupProps) { +function RadioCardGroup({ options, name, columns, loading }: RadioCardGroupProps) { const [field, meta, { setValue }] = useField(name) const handleChange = (event: React.ChangeEvent) => { setValue(event.target.value) @@ -129,6 +141,7 @@ function RadioCardGroup({ options, name, columns }: RadioCardGroupProps) { icon={option.icon} selected={field.value === option.value && !option.disabled} disabled={option.disabled} + loading={loading} /> ))} diff --git a/src/components/donation-flow/steps/Amount.tsx b/src/components/donation-flow/steps/Amount.tsx index ee9f42305..653378ef9 100644 --- a/src/components/donation-flow/steps/Amount.tsx +++ b/src/components/donation-flow/steps/Amount.tsx @@ -14,6 +14,7 @@ import FormTextField from 'components/common/form/FormTextField' import { stripeFeeCalculator, stripeIncludeFeeCalculator } from '../helpers/stripe-fee-calculator' import { DonationFormData } from '../helpers/types' +import { useSession } from 'next-auth/react' export const initialAmountFormValues = { amountChosen: '', @@ -57,9 +58,10 @@ export default function Amount({ disabled?: boolean sectionRef?: React.MutableRefObject }) { - const { data: prices } = useSinglePriceList() - const formik = useFormikContext() const { t } = useTranslation('donation-flow') + const { status } = useSession() + const formik = useFormikContext() + const { data: prices } = useSinglePriceList() const mobile = useMediaQuery('(max-width:600px)') const [{ value }] = useField('amountChosen') @@ -97,6 +99,7 @@ export default function Amount({ diff --git a/src/components/donation-flow/steps/payment-method/PaymentDetailsStripeForm.tsx b/src/components/donation-flow/steps/payment-method/PaymentDetailsStripeForm.tsx index 2a5970224..836b22ce5 100644 --- a/src/components/donation-flow/steps/payment-method/PaymentDetailsStripeForm.tsx +++ b/src/components/donation-flow/steps/payment-method/PaymentDetailsStripeForm.tsx @@ -1,6 +1,7 @@ import { useSession } from 'next-auth/react' import { LinkAuthenticationElement, PaymentElement } from '@stripe/react-stripe-js' import { Box, BoxProps } from '@mui/material' +import { useEffect, useState } from 'react' export type PaymentDetailsStripeFormProps = { containerProps?: BoxProps @@ -9,12 +10,16 @@ export default function PaymentDetailsStripeForm({ containerProps, }: PaymentDetailsStripeFormProps) { const { data: session } = useSession() + const [email, setEmail] = useState(session?.user?.email || '') + useEffect(() => { + setEmail(session?.user?.email || '') + }, [session]) return ( diff --git a/src/components/donation-flow/steps/payment-method/PaymentMethod.tsx b/src/components/donation-flow/steps/payment-method/PaymentMethod.tsx index 8f0d82c24..5cb33fa4d 100644 --- a/src/components/donation-flow/steps/payment-method/PaymentMethod.tsx +++ b/src/components/donation-flow/steps/payment-method/PaymentMethod.tsx @@ -1,5 +1,6 @@ import React from 'react' import { useTranslation } from 'react-i18next' +import { useSession } from 'next-auth/react' import { Alert, Box, Collapse, Typography, useMediaQuery } from '@mui/material' import { useField } from 'formik' @@ -22,6 +23,7 @@ export default function PaymentMethod({ const { t } = useTranslation('donation-flow') const isSmall = useMediaQuery(theme.breakpoints.down('md')) const [payment] = useField('payment') + const { status } = useSession() const options = [ { value: 'card', @@ -83,7 +85,12 @@ export default function PaymentMethod({ ) : ( <> - + diff --git a/src/components/one-time-donation/Steps.tsx b/src/components/one-time-donation/Steps.tsx index 63b77518c..98cb255e6 100644 --- a/src/components/one-time-donation/Steps.tsx +++ b/src/components/one-time-donation/Steps.tsx @@ -83,10 +83,10 @@ export default function DonationStepper({ onStepChange }: DonationStepperProps) personEmail: values?.personsEmail ? values.personsEmail : userEmail, isAnonymous: values?.isAnonymous !== undefined ? values.isAnonymous : true, phone: values?.personsPhone ? values.personsPhone : null, - successUrl: `${baseUrl}/${i18n.language}/${routes.campaigns.oneTimeDonation( + successUrl: `${baseUrl}/${i18n.language}/${routes.campaigns.donation( campaign.slug, )}?success=true`, - cancelUrl: `${baseUrl}/${i18n.language}/${routes.campaigns.oneTimeDonation( + cancelUrl: `${baseUrl}/${i18n.language}/${routes.campaigns.donation( campaign.slug, )}?success=false`, message: values?.message, @@ -116,7 +116,7 @@ export default function DonationStepper({ onStepChange }: DonationStepperProps) personId: !values.isAnonymous && person?.id ? person.id : null, }) } - router.push(`${baseUrl}${routes.campaigns.oneTimeDonation(campaign.slug)}?success=true`) + router.push(`${baseUrl}${routes.campaigns.donation(campaign.slug)}?success=true`) return } From 49e600f00e9417ae61174950e1258b3102b73ddf Mon Sep 17 00:00:00 2001 From: "dimitar.nizamov" Date: Fri, 10 Feb 2023 19:23:24 +0100 Subject: [PATCH 02/12] 1272-finalization - adjust default values, fix form state when the session expires --- e2e/data/localization.ts | 8 +- .../web-pages/campaigns/donation.page.ts | 124 +++++++++++++++ .../anon-donation-custom.spec.ts | 134 +++++++++++++++++ .../anon-donation-fixed.spec.ts | 142 ++++++++++++++++++ .../anon-donation-custom.spec.ts | 60 +------- .../donation-flow/anon-donation-fixed.spec.ts | 58 +------ .../donation-flow/DonationFlowForm.tsx | 8 +- 7 files changed, 416 insertions(+), 118 deletions(-) create mode 100644 e2e/pages/web-pages/campaigns/donation.page.ts create mode 100644 e2e/tests/regression/donation-flow-old/anon-donation-custom.spec.ts create mode 100644 e2e/tests/regression/donation-flow-old/anon-donation-fixed.spec.ts diff --git a/e2e/data/localization.ts b/e2e/data/localization.ts index 6397cbabd..bf0716d81 100644 --- a/e2e/data/localization.ts +++ b/e2e/data/localization.ts @@ -16,6 +16,9 @@ import enLocalizationCampaignsJson from '../../public/locales/en/campaigns.json' import bgLocalizationOneTimeDonationJson from '../../public/locales/bg/one-time-donation.json' import enLocalizationOneTimeDonationJson from '../../public/locales/en/one-time-donation.json' +import bgLocalizationDonationFlowJson from '../../public/locales/bg/donation-flow.json' +import enLocalizationDonationFlowJson from '../../public/locales/en/donation-flow.json' + // All these constants are used in the E2E test pages to manipulate web elements in a respective language // Common localization terms export const bgLocalizationCommon = bgLocalizationCommonJson @@ -29,9 +32,12 @@ export const enLocalizationSupport = enLocalizationSupportJson // Campaigns page export const bgLocalizationCampaigns = bgLocalizationCampaignsJson export const enLocalizationCampaigns = enLocalizationCampaignsJson -// Donations +// Donations - old export const bgLocalizationOneTimeDonation = bgLocalizationOneTimeDonationJson export const enLocalizationOneTimeDonation = enLocalizationOneTimeDonationJson +// Donations +export const bgLocalizationDonationFlow = bgLocalizationDonationFlowJson +export const enLocalizationDonationFlow = enLocalizationDonationFlowJson // Validations export const bgLocalizationValidation = bgLocalizationValidationJson export const enLocalizationValidation = enLocalizationValidationJson diff --git a/e2e/pages/web-pages/campaigns/donation.page.ts b/e2e/pages/web-pages/campaigns/donation.page.ts new file mode 100644 index 000000000..aafde7831 --- /dev/null +++ b/e2e/pages/web-pages/campaigns/donation.page.ts @@ -0,0 +1,124 @@ +import { Page, expect } from '@playwright/test' +import { DonationRegions } from '../../../data/enums/donation-regions.enum' +import { LanguagesEnum } from '../../../data/enums/languages.enum' +import { bgLocalizationDonationFlow, enLocalizationDonationFlow } from '../../../data/localization' +import { SLUG_REGEX } from '../../../utils/helpers' +import { CampaignsPage } from './campaigns.page' + +export class DonationPage extends CampaignsPage { + constructor(page: Page) { + super(page) + } + + // -> Select amount section <- + private readonly otherAmountInputField = ".MuiCollapse-entered input[name='otherAmount']" + private readonly allAmountsSelector = '.MuiBox-root strong' + private readonly regionsDropdownRootElement = '.MuiInputBase-root .MuiSelect-select' + private readonly regionsMenuList = '#menu-cardRegion ul.MuiMenu-list li' + // Section labels + private readonly bgSelectAmountSectionText = bgLocalizationDonationFlow.step.amount.title + private readonly enSelectAmountSectionText = enLocalizationDonationFlow.step.amount.title + private readonly bgPaymentMethodSectionText = + bgLocalizationDonationFlow.step['payment-method'].title + private readonly enPaymentMethodSectionText = + enLocalizationDonationFlow.step['payment-method'].title + private readonly bgAuthenticationSectionText = + bgLocalizationDonationFlow.step.authentication.title + private readonly enAuthenticationSectionText = + enLocalizationDonationFlow.step.authentication.title + // TODO Add these three IDs into the component (if possible) and update the test methods + private readonly donationAmount = this.allAmountsSelector + ' #donationAmount' + private readonly feeAmount = this.allAmountsSelector + ' #feeAmount' + private readonly totalChargedAmount = this.allAmountsSelector + ' #totalChargedAmount' + + private readonly inputRootSelector = '.MuiInputBase-root' + + // -> Send a wish section <- + // Section labels + private readonly sendAWishField = this.inputRootSelector + " textarea[name='wish']" + private readonly bgSendAWishSectionText = bgLocalizationDonationFlow.status.success.wish.title + private readonly enSendAWishSectionText = enLocalizationDonationFlow.status.success.wish.title + + // -> Payment <- + // Section labels + private readonly bgSuccessfulDonationTitle = bgLocalizationDonationFlow.status.success.title + private readonly enSuccessfulDonationTitle = enLocalizationDonationFlow.status.success.title + + async checkPageUrlByRegExp(urlRegExpAsString?: string, timeoutParam = 10000): Promise { + await this.page.waitForTimeout(1000) + await expect(this.page, 'The URL is not correct!').toHaveURL( + new RegExp(urlRegExpAsString || `^(.*?)/campaigns/donation/${SLUG_REGEX}`), + { + timeout: timeoutParam, + }, + ) + } + + /** + * Fill in the desired amount of money for donation into the Other Amount input field + * @param {string} amountMoney + */ + async fillOtherAmountInputField(amountMoney: string): Promise { + await this.waitForElementToBePresentedBySelector(this.otherAmountInputField) + await this.setInputFieldBySelector(this.otherAmountInputField, amountMoney) + } + + /** + * Set donation region from the dropdown menu + * @param {string} desiredRegion + */ + async setDonationRegionFromTheDropdown(desiredRegion: DonationRegions): Promise { + await this.clickElement(this.regionsDropdownRootElement) + await this.clickElement(this.regionsMenuList + `[data-value=${desiredRegion}]`) + } + + /** + * Get Total charged amounts as text + */ + async getTotalChargedAmountsAsText(): Promise { + const donationAmount = this.page.locator(this.allAmountsSelector).nth(0) + return this.getTextOfElementByLocator(donationAmount) + // TODO Uncomment when the IDs are added + // return this.getTextOfElementBySelector(this.totalChargedAmount); + } + + /** + * Get Fee amounts as text + */ + async getFeeAmountsAsText(): Promise { + const donationAmount = this.page.locator(this.allAmountsSelector).nth(1) + return this.getTextOfElementByLocator(donationAmount) + } + + /** + * Get Donation amounts as text + */ + async getDonationAmountsAsText(): Promise { + const donationAmount = this.page.locator(this.allAmountsSelector).nth(2) + return this.getTextOfElementByLocator(donationAmount) + // TODO Uncomment when the IDs are added + // return this.getTextOfElementBySelector(this.donationAmount); + } + + /** + * Fill Send a wish input field + * @param {string} wishText + */ + async fillSendAWishField(wishText: string): Promise { + await this.setInputFieldBySelector(this.sendAWishField, wishText) + } + + /** + * Is "We thank you for your help and trust!" title visible + * @param {LanguagesEnum} language - the default value is BG + */ + async isSuccessfulDonationTitleVisible( + language: LanguagesEnum = LanguagesEnum.BG, + ): Promise { + return this.isH4HeadingVisible( + language, + this.bgSuccessfulDonationTitle, + this.enSuccessfulDonationTitle, + ) + } +} diff --git a/e2e/tests/regression/donation-flow-old/anon-donation-custom.spec.ts b/e2e/tests/regression/donation-flow-old/anon-donation-custom.spec.ts new file mode 100644 index 000000000..6ae95857e --- /dev/null +++ b/e2e/tests/regression/donation-flow-old/anon-donation-custom.spec.ts @@ -0,0 +1,134 @@ +import { test, expect, Page } from '@playwright/test' +import { HeaderPage } from '../../../pages/web-pages/header.page' +import { HomePage } from '../../../pages/web-pages/home.page' +import { CampaignsPage } from '../../../pages/web-pages/campaigns/campaigns.page' +import { bgLocalizationOneTimeDonation } from '../../../data/localization' +import { DonationPage } from '../../../pages/web-pages/campaigns/donation-old.page' +import { DonationRegions } from '../../../data/enums/donation-regions.enum' +import { StripeCheckoutPage } from '../../../pages/web-pages/external/stripe-checkout.page' +import { anonDonationTestData } from '../../../data/support-page-tests.data' + +// This spec contains E2E tests related to anonymous donation flow - custom amount +// The tests are dependent, the whole describe should be runned +test.describe.serial( + 'Anonymous contributor is able to donate custom amount - BG language version', + async () => { + let page: Page + let homepage: HomePage + let headerPage: HeaderPage + let campaignsPage: CampaignsPage + let donationPage: DonationPage + let stripeCheckoutPage: StripeCheckoutPage + const testEmail = 'E2E_Test_Anon_Donation@e2etest.com' + // Localization texts + const otherAmountText = bgLocalizationOneTimeDonation['first-step'].other + const bgCardIncludeFeesText = bgLocalizationOneTimeDonation['third-step']['card-include-fees'] + + test.beforeAll(async ({ browser }) => { + page = await browser.newPage() + homepage = new HomePage(page) + headerPage = new HeaderPage(page) + campaignsPage = new CampaignsPage(page) + donationPage = new DonationPage(page) + stripeCheckoutPage = new StripeCheckoutPage(page) + // For local executions use method navigateToLocalhostHomepage(); + // await homepage.navigateToLocalhostHomepage(); + await homepage.navigateToEnvHomepage() + }) + + test.afterAll(async () => { + await page.close() + }) + + test('Particular campaign can be opened through the Campaign page', async () => { + await headerPage.clickDonateHeaderNavButton() + await campaignsPage.clickCampaignCardByIndex(0) + // We move from the common Campaigns page to the particular campain page + // check if the url is changed only based on the url pattern http://localhost:3040/campaigns/{slug-based-regexp} + expect( + await campaignsPage.checkPageUrlByRegExp(), + 'The url is not changed after clicking on the campaign card.', + ) + }) + + test('The total charge, fee tax and donation amount are visible on the Campaign page', async () => { + await campaignsPage.clickDonationSupportButton() + await donationPage.checkPageUrlByRegExp() + expect + .soft(await donationPage.isSelectAmountStepActive(), 'Select Amount step is not active.') + .toBeTruthy() + await donationPage.selectRadioButtonByLabelText([otherAmountText]) + await donationPage.fillOtherAmountInputField('7.50') + await donationPage.setDonationRegionFromTheDropdown(DonationRegions.EUROPE) + await donationPage.selectCheckboxByLabelText([bgCardIncludeFeesText]) + // Expected pattern: + // За вашия превод от {totalChargedAmountText} лв., таксата на Stripe ще е {feeAmountText} лв., а кампанията ще получи {donationAmountText} лв. + const totalChargedAmountText = await donationPage.getTotalChargedAmountsAsText() + const feeAmountText = await donationPage.getFeeAmountsAsText() + const donationAmountText = await donationPage.getDonationAmountsAsText() + expect.soft(totalChargedAmountText).toEqual('8,10 лв.') + expect.soft(feeAmountText).toEqual('0,60 лв.') + expect(donationAmountText).toEqual('7,50 лв.') + }) + + test('The total charge, fee tax and donation amount are recalculated correctly when the donation amount is changed', async () => { + await donationPage.fillOtherAmountInputField('12.90') + // Expected pattern: + // За вашия превод от {totalChargedAmountText} лв., таксата на Stripe ще е {feeAmountText} лв., а кампанията ще получи {donationAmountText} лв. + const totalChargedAmountText = await donationPage.getTotalChargedAmountsAsText() + const feeAmountText = await donationPage.getFeeAmountsAsText() + const donationAmountText = await donationPage.getDonationAmountsAsText() + expect.soft(totalChargedAmountText).toEqual('13,56 лв.') + expect.soft(feeAmountText).toEqual('0,66 лв.') + expect(donationAmountText).toEqual('12,90 лв.') + }) + + test('The user is able to fill in e-mail for anonymous donation', async () => { + await donationPage.clickForwardButton() + expect + .soft( + await donationPage.isPersonalProfileStepActive(), + 'Personal Profile step is not active.', + ) + .toBeTruthy() + await donationPage.clickDonateAnonymouslyButton() + await donationPage.fillDonateAnonymouslyEmailField(testEmail) + await donationPage.clickForwardButton() + expect( + await donationPage.isSendAWishStepActive(), + 'Send a wish step is not active.', + ).toBeTruthy() + }) + + test('After sending a wish, the user is redirected to Stripe', async () => { + await donationPage.fillSendAWishField('E2E test - anonymous donation.') + await donationPage.clickFinishButton() + const stripeTotalAmount = await stripeCheckoutPage.getTotalAmountText() + const actualStripeEmail = await stripeCheckoutPage.getReadonlyEmailText() + expect + .soft(stripeTotalAmount, 'The Stripe total donation amount is not correct.') + .toContain('13.56') + expect(actualStripeEmail, 'The user e-mail is not sent correctly to Stripe.').toEqual( + testEmail, + ) + }) + + test('The user is able to pay via Stripe', async () => { + await stripeCheckoutPage.fillPaymentForm([ + anonDonationTestData.cardNumber, + anonDonationTestData.cardExpDate, + anonDonationTestData.cardCvc, + anonDonationTestData.billingName, + anonDonationTestData.country, + ]) + + expect + .soft( + await donationPage.isSuccessfulDonationTitleVisible(), + "'We thank you for your help and trust!' title is not visible.", + ) + .toBeTruthy() + expect(await donationPage.isPaymentStepActive(), 'Payment step is not active.').toBeTruthy() + }) + }, +) diff --git a/e2e/tests/regression/donation-flow-old/anon-donation-fixed.spec.ts b/e2e/tests/regression/donation-flow-old/anon-donation-fixed.spec.ts new file mode 100644 index 000000000..0244dfc5b --- /dev/null +++ b/e2e/tests/regression/donation-flow-old/anon-donation-fixed.spec.ts @@ -0,0 +1,142 @@ +import { test, expect, Page } from '@playwright/test' +import { HeaderPage } from '../../../pages/web-pages/header.page' +import { HomePage } from '../../../pages/web-pages/home.page' +import { CampaignsPage } from '../../../pages/web-pages/campaigns/campaigns.page' +import { enLocalizationOneTimeDonation } from '../../../data/localization' +import { DonationPage } from '../../../pages/web-pages/campaigns/donation-old.page' +import { DonationRegions } from '../../../data/enums/donation-regions.enum' +import { StripeCheckoutPage } from '../../../pages/web-pages/external/stripe-checkout.page' +import { anonDonationTestData } from '../../../data/support-page-tests.data' +import { LanguagesEnum } from '../../../data/enums/languages.enum' + +// This spec contains E2E tests related to anonymous donation flow - fixed amount +// The tests are dependent, the whole describe should be runned +test.describe.serial( + 'Anonymous contributor is able to donate fixed amount - EN language version', + async () => { + let page: Page + let homepage: HomePage + let headerPage: HeaderPage + let campaignsPage: CampaignsPage + let donationPage: DonationPage + let stripeCheckoutPage: StripeCheckoutPage + const testEmail = 'E2E_Test_Anon_Donation@e2etest.com' + // Localization texts + const enCardIncludeFeesText = enLocalizationOneTimeDonation['third-step']['card-include-fees'] + + test.beforeAll(async ({ browser, baseURL }) => { + page = await browser.newPage() + homepage = new HomePage(page) + headerPage = new HeaderPage(page) + campaignsPage = new CampaignsPage(page) + donationPage = new DonationPage(page) + stripeCheckoutPage = new StripeCheckoutPage(page) + // For local executions use method navigateToLocalhostHomepage(); + // await homepage.navigateToLocalhostHomepage(); + await homepage.navigateToEnvHomepage() + await headerPage.changeanguageHeaderButtonToBe(LanguagesEnum.EN) + }) + + test.afterAll(async () => { + await page.close() + }) + + test('Particular campaign can be opened through the Campaign page', async () => { + await headerPage.clickDonateHeaderNavButton() + await campaignsPage.clickCampaignCardByIndex(0) + // We move from the common Campaigns page to the particular campain page + // check if the url is changed only based on the url pattern http://localhost:3040/campaigns/{slug-based-regexp} + // expect to not break + + expect( + await campaignsPage.checkPageUrlByRegExp(), + 'The url is not changed after clicking on the campaign card.', + ) + }) + + test('The total charge, fee tax and donation amount are visible on the Campaign page', async () => { + await campaignsPage.clickDonationSupportButton() + await donationPage.checkPageUrlByRegExp() + expect + .soft( + await donationPage.isSelectAmountStepActive(LanguagesEnum.EN), + 'Select Amount step is not active.', + ) + .toBeTruthy() + await donationPage.selectRadioButtonByLabelText(['10']) + await donationPage.setDonationRegionFromTheDropdown(DonationRegions.EUROPE) + await donationPage.selectCheckboxByLabelText([enCardIncludeFeesText]) + // Expected pattern: + // For your donation of {donationAmountText}, the fee from Stripe will be {feeAmountText}, and the total charged amount will be {totalChargedAmountText}. + const donationAmountText = await donationPage.getTotalChargedAmountsAsText() + const feeAmountText = await donationPage.getFeeAmountsAsText() + const totalChargedAmountText = await donationPage.getDonationAmountsAsText() + expect.soft(donationAmountText).toMatch('10.00') + expect.soft(feeAmountText).toMatch('0.63') + expect(totalChargedAmountText).toMatch('10.63') + }) + + test('The total charge, fee tax and donation amount are recalculated correctly when the donation amount is changed', async () => { + await donationPage.selectRadioButtonByLabelText(['20']) + // Expected pattern: + // For your donation of {donationAmountText}, the fee from Stripe will be {feeAmountText}, and the total charged amount will be {totalChargedAmountText}. + const donationAmountText = await donationPage.getTotalChargedAmountsAsText() + const feeAmountText = await donationPage.getFeeAmountsAsText() + const totalChargedAmountText = await donationPage.getDonationAmountsAsText() + expect.soft(donationAmountText).toMatch('20.00') + expect.soft(feeAmountText).toMatch('0.75') + expect(totalChargedAmountText).toMatch('20.75') + }) + + test('The user is able to fill in e-mail for anonymous donation', async () => { + await donationPage.clickForwardButton(LanguagesEnum.EN) + expect + .soft( + await donationPage.isPersonalProfileStepActive(LanguagesEnum.EN), + 'Personal Profile step is not active.', + ) + .toBeTruthy() + await donationPage.clickDonateAnonymouslyButton(LanguagesEnum.EN) + await donationPage.fillDonateAnonymouslyEmailField(testEmail) + await donationPage.clickForwardButton(LanguagesEnum.EN) + expect( + await donationPage.isSendAWishStepActive(LanguagesEnum.EN), + 'Send a wish step is not active.', + ).toBeTruthy() + }) + + test('After sending a wish, the user is redirected to Stripe', async () => { + await donationPage.fillSendAWishField('E2E test - anonymous donation.') + await donationPage.clickFinishButton(LanguagesEnum.EN) + const stripeTotalAmount = await stripeCheckoutPage.getTotalAmountText() + const actualStripeEmail = await stripeCheckoutPage.getReadonlyEmailText() + expect + .soft(stripeTotalAmount, 'The Stripe total donation amount is not correct.') + .toContain('20.75') + expect(actualStripeEmail, 'The user e-mail is not sent correctly to Stripe.').toEqual( + testEmail, + ) + }) + + test('The user is able to pay via Stripe', async () => { + await stripeCheckoutPage.fillPaymentForm([ + anonDonationTestData.cardNumber, + anonDonationTestData.cardExpDate, + anonDonationTestData.cardCvc, + anonDonationTestData.billingName, + anonDonationTestData.country, + ]) + // Now we're redirected to the Donation page + expect + .soft( + await donationPage.isSuccessfulDonationTitleVisible(LanguagesEnum.EN), + "'We thank you for your help and trust!' title is not visible.", + ) + .toBeTruthy() + expect( + await donationPage.isPaymentStepActive(LanguagesEnum.EN), + 'Payment step is not active.', + ).toBeTruthy() + }) + }, +) diff --git a/e2e/tests/regression/donation-flow/anon-donation-custom.spec.ts b/e2e/tests/regression/donation-flow/anon-donation-custom.spec.ts index 6ae95857e..ec43e979e 100644 --- a/e2e/tests/regression/donation-flow/anon-donation-custom.spec.ts +++ b/e2e/tests/regression/donation-flow/anon-donation-custom.spec.ts @@ -3,10 +3,8 @@ import { HeaderPage } from '../../../pages/web-pages/header.page' import { HomePage } from '../../../pages/web-pages/home.page' import { CampaignsPage } from '../../../pages/web-pages/campaigns/campaigns.page' import { bgLocalizationOneTimeDonation } from '../../../data/localization' -import { DonationPage } from '../../../pages/web-pages/campaigns/donation-old.page' +import { DonationPage } from '../../../pages/web-pages/campaigns/donation.page' import { DonationRegions } from '../../../data/enums/donation-regions.enum' -import { StripeCheckoutPage } from '../../../pages/web-pages/external/stripe-checkout.page' -import { anonDonationTestData } from '../../../data/support-page-tests.data' // This spec contains E2E tests related to anonymous donation flow - custom amount // The tests are dependent, the whole describe should be runned @@ -18,8 +16,6 @@ test.describe.serial( let headerPage: HeaderPage let campaignsPage: CampaignsPage let donationPage: DonationPage - let stripeCheckoutPage: StripeCheckoutPage - const testEmail = 'E2E_Test_Anon_Donation@e2etest.com' // Localization texts const otherAmountText = bgLocalizationOneTimeDonation['first-step'].other const bgCardIncludeFeesText = bgLocalizationOneTimeDonation['third-step']['card-include-fees'] @@ -30,9 +26,6 @@ test.describe.serial( headerPage = new HeaderPage(page) campaignsPage = new CampaignsPage(page) donationPage = new DonationPage(page) - stripeCheckoutPage = new StripeCheckoutPage(page) - // For local executions use method navigateToLocalhostHomepage(); - // await homepage.navigateToLocalhostHomepage(); await homepage.navigateToEnvHomepage() }) @@ -54,9 +47,6 @@ test.describe.serial( test('The total charge, fee tax and donation amount are visible on the Campaign page', async () => { await campaignsPage.clickDonationSupportButton() await donationPage.checkPageUrlByRegExp() - expect - .soft(await donationPage.isSelectAmountStepActive(), 'Select Amount step is not active.') - .toBeTruthy() await donationPage.selectRadioButtonByLabelText([otherAmountText]) await donationPage.fillOtherAmountInputField('7.50') await donationPage.setDonationRegionFromTheDropdown(DonationRegions.EUROPE) @@ -82,53 +72,5 @@ test.describe.serial( expect.soft(feeAmountText).toEqual('0,66 лв.') expect(donationAmountText).toEqual('12,90 лв.') }) - - test('The user is able to fill in e-mail for anonymous donation', async () => { - await donationPage.clickForwardButton() - expect - .soft( - await donationPage.isPersonalProfileStepActive(), - 'Personal Profile step is not active.', - ) - .toBeTruthy() - await donationPage.clickDonateAnonymouslyButton() - await donationPage.fillDonateAnonymouslyEmailField(testEmail) - await donationPage.clickForwardButton() - expect( - await donationPage.isSendAWishStepActive(), - 'Send a wish step is not active.', - ).toBeTruthy() - }) - - test('After sending a wish, the user is redirected to Stripe', async () => { - await donationPage.fillSendAWishField('E2E test - anonymous donation.') - await donationPage.clickFinishButton() - const stripeTotalAmount = await stripeCheckoutPage.getTotalAmountText() - const actualStripeEmail = await stripeCheckoutPage.getReadonlyEmailText() - expect - .soft(stripeTotalAmount, 'The Stripe total donation amount is not correct.') - .toContain('13.56') - expect(actualStripeEmail, 'The user e-mail is not sent correctly to Stripe.').toEqual( - testEmail, - ) - }) - - test('The user is able to pay via Stripe', async () => { - await stripeCheckoutPage.fillPaymentForm([ - anonDonationTestData.cardNumber, - anonDonationTestData.cardExpDate, - anonDonationTestData.cardCvc, - anonDonationTestData.billingName, - anonDonationTestData.country, - ]) - - expect - .soft( - await donationPage.isSuccessfulDonationTitleVisible(), - "'We thank you for your help and trust!' title is not visible.", - ) - .toBeTruthy() - expect(await donationPage.isPaymentStepActive(), 'Payment step is not active.').toBeTruthy() - }) }, ) diff --git a/e2e/tests/regression/donation-flow/anon-donation-fixed.spec.ts b/e2e/tests/regression/donation-flow/anon-donation-fixed.spec.ts index 0244dfc5b..8ff061a25 100644 --- a/e2e/tests/regression/donation-flow/anon-donation-fixed.spec.ts +++ b/e2e/tests/regression/donation-flow/anon-donation-fixed.spec.ts @@ -5,8 +5,6 @@ import { CampaignsPage } from '../../../pages/web-pages/campaigns/campaigns.page import { enLocalizationOneTimeDonation } from '../../../data/localization' import { DonationPage } from '../../../pages/web-pages/campaigns/donation-old.page' import { DonationRegions } from '../../../data/enums/donation-regions.enum' -import { StripeCheckoutPage } from '../../../pages/web-pages/external/stripe-checkout.page' -import { anonDonationTestData } from '../../../data/support-page-tests.data' import { LanguagesEnum } from '../../../data/enums/languages.enum' // This spec contains E2E tests related to anonymous donation flow - fixed amount @@ -19,18 +17,15 @@ test.describe.serial( let headerPage: HeaderPage let campaignsPage: CampaignsPage let donationPage: DonationPage - let stripeCheckoutPage: StripeCheckoutPage - const testEmail = 'E2E_Test_Anon_Donation@e2etest.com' // Localization texts const enCardIncludeFeesText = enLocalizationOneTimeDonation['third-step']['card-include-fees'] - test.beforeAll(async ({ browser, baseURL }) => { + test.beforeAll(async ({ browser }) => { page = await browser.newPage() homepage = new HomePage(page) headerPage = new HeaderPage(page) campaignsPage = new CampaignsPage(page) donationPage = new DonationPage(page) - stripeCheckoutPage = new StripeCheckoutPage(page) // For local executions use method navigateToLocalhostHomepage(); // await homepage.navigateToLocalhostHomepage(); await homepage.navigateToEnvHomepage() @@ -87,56 +82,5 @@ test.describe.serial( expect.soft(feeAmountText).toMatch('0.75') expect(totalChargedAmountText).toMatch('20.75') }) - - test('The user is able to fill in e-mail for anonymous donation', async () => { - await donationPage.clickForwardButton(LanguagesEnum.EN) - expect - .soft( - await donationPage.isPersonalProfileStepActive(LanguagesEnum.EN), - 'Personal Profile step is not active.', - ) - .toBeTruthy() - await donationPage.clickDonateAnonymouslyButton(LanguagesEnum.EN) - await donationPage.fillDonateAnonymouslyEmailField(testEmail) - await donationPage.clickForwardButton(LanguagesEnum.EN) - expect( - await donationPage.isSendAWishStepActive(LanguagesEnum.EN), - 'Send a wish step is not active.', - ).toBeTruthy() - }) - - test('After sending a wish, the user is redirected to Stripe', async () => { - await donationPage.fillSendAWishField('E2E test - anonymous donation.') - await donationPage.clickFinishButton(LanguagesEnum.EN) - const stripeTotalAmount = await stripeCheckoutPage.getTotalAmountText() - const actualStripeEmail = await stripeCheckoutPage.getReadonlyEmailText() - expect - .soft(stripeTotalAmount, 'The Stripe total donation amount is not correct.') - .toContain('20.75') - expect(actualStripeEmail, 'The user e-mail is not sent correctly to Stripe.').toEqual( - testEmail, - ) - }) - - test('The user is able to pay via Stripe', async () => { - await stripeCheckoutPage.fillPaymentForm([ - anonDonationTestData.cardNumber, - anonDonationTestData.cardExpDate, - anonDonationTestData.cardCvc, - anonDonationTestData.billingName, - anonDonationTestData.country, - ]) - // Now we're redirected to the Donation page - expect - .soft( - await donationPage.isSuccessfulDonationTitleVisible(LanguagesEnum.EN), - "'We thank you for your help and trust!' title is not visible.", - ) - .toBeTruthy() - expect( - await donationPage.isPaymentStepActive(LanguagesEnum.EN), - 'Payment step is not active.', - ).toBeTruthy() - }) }, ) diff --git a/src/components/donation-flow/DonationFlowForm.tsx b/src/components/donation-flow/DonationFlowForm.tsx index 12c43ecb4..05848b819 100644 --- a/src/components/donation-flow/DonationFlowForm.tsx +++ b/src/components/donation-flow/DonationFlowForm.tsx @@ -94,7 +94,12 @@ export const validationSchema: yup.SchemaOf = yup export function DonationFlowForm() { const formikRef = useRef | null>(null) const { t, i18n } = useTranslation('donation-flow') - const { data: session } = useSession() + const { data: session } = useSession({ + required: false, + onUnauthenticated: () => { + formikRef.current?.setFieldValue('authentication', null) + }, + }) useEffect(() => { if (session?.user) { formikRef.current?.setFieldValue('email', session.user.email) @@ -103,6 +108,7 @@ export function DonationFlowForm() { return } formikRef.current?.setFieldValue('email', '') + formikRef.current?.setFieldValue('isAnonymous', true) }, [session]) const { campaign, stripePaymentIntent, paymentError, setPaymentError } = useDonationFlow() const stripe = useStripe() From ebd6514c5927caba3556ae5a351774a6bc384f64 Mon Sep 17 00:00:00 2001 From: "dimitar.nizamov" Date: Fri, 10 Feb 2023 19:54:01 +0100 Subject: [PATCH 03/12] 1272-finalization - remove old donation implementation and e2e tests --- .../web-pages/campaigns/donation.page.ts | 124 ------- .../anon-donation-custom.spec.ts | 2 +- .../anon-donation-fixed.spec.ts | 2 +- .../anon-donation-custom.spec.ts | 76 ---- .../donation-flow/anon-donation-fixed.spec.ts | 86 ----- public/locales/bg/one-time-donation.json | 95 ----- public/locales/en/one-time-donation.json | 95 ----- src/common/routes.ts | 1 - .../common/form/CircleCheckboxField.tsx | 2 +- .../common/form/RadioButtonGroup.tsx | 2 +- .../one-time-donation/AnonymousForm.tsx | 24 -- .../one-time-donation/FormikStepper.tsx | 143 -------- .../one-time-donation/LoggedUserDialog.tsx | 34 -- .../one-time-donation/LoginForm.tsx | 91 ----- .../one-time-donation/OneTimeDonationPage.tsx | 152 -------- .../one-time-donation/RegisterDialog.tsx | 110 ------ src/components/one-time-donation/Steps.tsx | 181 ---------- .../helpers/paypalDonationButton.tsx | 88 ----- .../helpers/stepperContext.ts | 9 - .../helpers/stripe-fee-calculator.ts | 59 ---- .../helpers/validation-schema.ts | 37 -- .../one-time-donation/steps/Fail.tsx | 62 ---- .../one-time-donation/steps/FirstStep.tsx | 329 ------------------ .../one-time-donation/steps/SecondStep.tsx | 71 ---- .../one-time-donation/steps/Success.tsx | 74 ---- .../one-time-donation/steps/ThirdStep.tsx | 26 -- .../support-us-form/SupportUsForm.tsx | 28 +- src/pages/campaigns/donation-old/[slug].tsx | 32 -- 28 files changed, 14 insertions(+), 2021 deletions(-) delete mode 100644 e2e/pages/web-pages/campaigns/donation.page.ts delete mode 100644 e2e/tests/regression/donation-flow/anon-donation-custom.spec.ts delete mode 100644 e2e/tests/regression/donation-flow/anon-donation-fixed.spec.ts delete mode 100644 public/locales/bg/one-time-donation.json delete mode 100644 public/locales/en/one-time-donation.json delete mode 100644 src/components/one-time-donation/AnonymousForm.tsx delete mode 100644 src/components/one-time-donation/FormikStepper.tsx delete mode 100644 src/components/one-time-donation/LoggedUserDialog.tsx delete mode 100644 src/components/one-time-donation/LoginForm.tsx delete mode 100644 src/components/one-time-donation/OneTimeDonationPage.tsx delete mode 100644 src/components/one-time-donation/RegisterDialog.tsx delete mode 100644 src/components/one-time-donation/Steps.tsx delete mode 100644 src/components/one-time-donation/helpers/paypalDonationButton.tsx delete mode 100644 src/components/one-time-donation/helpers/stepperContext.ts delete mode 100644 src/components/one-time-donation/helpers/stripe-fee-calculator.ts delete mode 100644 src/components/one-time-donation/helpers/validation-schema.ts delete mode 100644 src/components/one-time-donation/steps/Fail.tsx delete mode 100644 src/components/one-time-donation/steps/FirstStep.tsx delete mode 100644 src/components/one-time-donation/steps/SecondStep.tsx delete mode 100644 src/components/one-time-donation/steps/Success.tsx delete mode 100644 src/components/one-time-donation/steps/ThirdStep.tsx delete mode 100644 src/pages/campaigns/donation-old/[slug].tsx diff --git a/e2e/pages/web-pages/campaigns/donation.page.ts b/e2e/pages/web-pages/campaigns/donation.page.ts deleted file mode 100644 index aafde7831..000000000 --- a/e2e/pages/web-pages/campaigns/donation.page.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { Page, expect } from '@playwright/test' -import { DonationRegions } from '../../../data/enums/donation-regions.enum' -import { LanguagesEnum } from '../../../data/enums/languages.enum' -import { bgLocalizationDonationFlow, enLocalizationDonationFlow } from '../../../data/localization' -import { SLUG_REGEX } from '../../../utils/helpers' -import { CampaignsPage } from './campaigns.page' - -export class DonationPage extends CampaignsPage { - constructor(page: Page) { - super(page) - } - - // -> Select amount section <- - private readonly otherAmountInputField = ".MuiCollapse-entered input[name='otherAmount']" - private readonly allAmountsSelector = '.MuiBox-root strong' - private readonly regionsDropdownRootElement = '.MuiInputBase-root .MuiSelect-select' - private readonly regionsMenuList = '#menu-cardRegion ul.MuiMenu-list li' - // Section labels - private readonly bgSelectAmountSectionText = bgLocalizationDonationFlow.step.amount.title - private readonly enSelectAmountSectionText = enLocalizationDonationFlow.step.amount.title - private readonly bgPaymentMethodSectionText = - bgLocalizationDonationFlow.step['payment-method'].title - private readonly enPaymentMethodSectionText = - enLocalizationDonationFlow.step['payment-method'].title - private readonly bgAuthenticationSectionText = - bgLocalizationDonationFlow.step.authentication.title - private readonly enAuthenticationSectionText = - enLocalizationDonationFlow.step.authentication.title - // TODO Add these three IDs into the component (if possible) and update the test methods - private readonly donationAmount = this.allAmountsSelector + ' #donationAmount' - private readonly feeAmount = this.allAmountsSelector + ' #feeAmount' - private readonly totalChargedAmount = this.allAmountsSelector + ' #totalChargedAmount' - - private readonly inputRootSelector = '.MuiInputBase-root' - - // -> Send a wish section <- - // Section labels - private readonly sendAWishField = this.inputRootSelector + " textarea[name='wish']" - private readonly bgSendAWishSectionText = bgLocalizationDonationFlow.status.success.wish.title - private readonly enSendAWishSectionText = enLocalizationDonationFlow.status.success.wish.title - - // -> Payment <- - // Section labels - private readonly bgSuccessfulDonationTitle = bgLocalizationDonationFlow.status.success.title - private readonly enSuccessfulDonationTitle = enLocalizationDonationFlow.status.success.title - - async checkPageUrlByRegExp(urlRegExpAsString?: string, timeoutParam = 10000): Promise { - await this.page.waitForTimeout(1000) - await expect(this.page, 'The URL is not correct!').toHaveURL( - new RegExp(urlRegExpAsString || `^(.*?)/campaigns/donation/${SLUG_REGEX}`), - { - timeout: timeoutParam, - }, - ) - } - - /** - * Fill in the desired amount of money for donation into the Other Amount input field - * @param {string} amountMoney - */ - async fillOtherAmountInputField(amountMoney: string): Promise { - await this.waitForElementToBePresentedBySelector(this.otherAmountInputField) - await this.setInputFieldBySelector(this.otherAmountInputField, amountMoney) - } - - /** - * Set donation region from the dropdown menu - * @param {string} desiredRegion - */ - async setDonationRegionFromTheDropdown(desiredRegion: DonationRegions): Promise { - await this.clickElement(this.regionsDropdownRootElement) - await this.clickElement(this.regionsMenuList + `[data-value=${desiredRegion}]`) - } - - /** - * Get Total charged amounts as text - */ - async getTotalChargedAmountsAsText(): Promise { - const donationAmount = this.page.locator(this.allAmountsSelector).nth(0) - return this.getTextOfElementByLocator(donationAmount) - // TODO Uncomment when the IDs are added - // return this.getTextOfElementBySelector(this.totalChargedAmount); - } - - /** - * Get Fee amounts as text - */ - async getFeeAmountsAsText(): Promise { - const donationAmount = this.page.locator(this.allAmountsSelector).nth(1) - return this.getTextOfElementByLocator(donationAmount) - } - - /** - * Get Donation amounts as text - */ - async getDonationAmountsAsText(): Promise { - const donationAmount = this.page.locator(this.allAmountsSelector).nth(2) - return this.getTextOfElementByLocator(donationAmount) - // TODO Uncomment when the IDs are added - // return this.getTextOfElementBySelector(this.donationAmount); - } - - /** - * Fill Send a wish input field - * @param {string} wishText - */ - async fillSendAWishField(wishText: string): Promise { - await this.setInputFieldBySelector(this.sendAWishField, wishText) - } - - /** - * Is "We thank you for your help and trust!" title visible - * @param {LanguagesEnum} language - the default value is BG - */ - async isSuccessfulDonationTitleVisible( - language: LanguagesEnum = LanguagesEnum.BG, - ): Promise { - return this.isH4HeadingVisible( - language, - this.bgSuccessfulDonationTitle, - this.enSuccessfulDonationTitle, - ) - } -} diff --git a/e2e/tests/regression/donation-flow-old/anon-donation-custom.spec.ts b/e2e/tests/regression/donation-flow-old/anon-donation-custom.spec.ts index 6ae95857e..e72addab8 100644 --- a/e2e/tests/regression/donation-flow-old/anon-donation-custom.spec.ts +++ b/e2e/tests/regression/donation-flow-old/anon-donation-custom.spec.ts @@ -10,7 +10,7 @@ import { anonDonationTestData } from '../../../data/support-page-tests.data' // This spec contains E2E tests related to anonymous donation flow - custom amount // The tests are dependent, the whole describe should be runned -test.describe.serial( +test.describe.skip( 'Anonymous contributor is able to donate custom amount - BG language version', async () => { let page: Page diff --git a/e2e/tests/regression/donation-flow-old/anon-donation-fixed.spec.ts b/e2e/tests/regression/donation-flow-old/anon-donation-fixed.spec.ts index 0244dfc5b..701267f0b 100644 --- a/e2e/tests/regression/donation-flow-old/anon-donation-fixed.spec.ts +++ b/e2e/tests/regression/donation-flow-old/anon-donation-fixed.spec.ts @@ -11,7 +11,7 @@ import { LanguagesEnum } from '../../../data/enums/languages.enum' // This spec contains E2E tests related to anonymous donation flow - fixed amount // The tests are dependent, the whole describe should be runned -test.describe.serial( +test.describe.skip( 'Anonymous contributor is able to donate fixed amount - EN language version', async () => { let page: Page diff --git a/e2e/tests/regression/donation-flow/anon-donation-custom.spec.ts b/e2e/tests/regression/donation-flow/anon-donation-custom.spec.ts deleted file mode 100644 index ec43e979e..000000000 --- a/e2e/tests/regression/donation-flow/anon-donation-custom.spec.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { test, expect, Page } from '@playwright/test' -import { HeaderPage } from '../../../pages/web-pages/header.page' -import { HomePage } from '../../../pages/web-pages/home.page' -import { CampaignsPage } from '../../../pages/web-pages/campaigns/campaigns.page' -import { bgLocalizationOneTimeDonation } from '../../../data/localization' -import { DonationPage } from '../../../pages/web-pages/campaigns/donation.page' -import { DonationRegions } from '../../../data/enums/donation-regions.enum' - -// This spec contains E2E tests related to anonymous donation flow - custom amount -// The tests are dependent, the whole describe should be runned -test.describe.serial( - 'Anonymous contributor is able to donate custom amount - BG language version', - async () => { - let page: Page - let homepage: HomePage - let headerPage: HeaderPage - let campaignsPage: CampaignsPage - let donationPage: DonationPage - // Localization texts - const otherAmountText = bgLocalizationOneTimeDonation['first-step'].other - const bgCardIncludeFeesText = bgLocalizationOneTimeDonation['third-step']['card-include-fees'] - - test.beforeAll(async ({ browser }) => { - page = await browser.newPage() - homepage = new HomePage(page) - headerPage = new HeaderPage(page) - campaignsPage = new CampaignsPage(page) - donationPage = new DonationPage(page) - await homepage.navigateToEnvHomepage() - }) - - test.afterAll(async () => { - await page.close() - }) - - test('Particular campaign can be opened through the Campaign page', async () => { - await headerPage.clickDonateHeaderNavButton() - await campaignsPage.clickCampaignCardByIndex(0) - // We move from the common Campaigns page to the particular campain page - // check if the url is changed only based on the url pattern http://localhost:3040/campaigns/{slug-based-regexp} - expect( - await campaignsPage.checkPageUrlByRegExp(), - 'The url is not changed after clicking on the campaign card.', - ) - }) - - test('The total charge, fee tax and donation amount are visible on the Campaign page', async () => { - await campaignsPage.clickDonationSupportButton() - await donationPage.checkPageUrlByRegExp() - await donationPage.selectRadioButtonByLabelText([otherAmountText]) - await donationPage.fillOtherAmountInputField('7.50') - await donationPage.setDonationRegionFromTheDropdown(DonationRegions.EUROPE) - await donationPage.selectCheckboxByLabelText([bgCardIncludeFeesText]) - // Expected pattern: - // За вашия превод от {totalChargedAmountText} лв., таксата на Stripe ще е {feeAmountText} лв., а кампанията ще получи {donationAmountText} лв. - const totalChargedAmountText = await donationPage.getTotalChargedAmountsAsText() - const feeAmountText = await donationPage.getFeeAmountsAsText() - const donationAmountText = await donationPage.getDonationAmountsAsText() - expect.soft(totalChargedAmountText).toEqual('8,10 лв.') - expect.soft(feeAmountText).toEqual('0,60 лв.') - expect(donationAmountText).toEqual('7,50 лв.') - }) - - test('The total charge, fee tax and donation amount are recalculated correctly when the donation amount is changed', async () => { - await donationPage.fillOtherAmountInputField('12.90') - // Expected pattern: - // За вашия превод от {totalChargedAmountText} лв., таксата на Stripe ще е {feeAmountText} лв., а кампанията ще получи {donationAmountText} лв. - const totalChargedAmountText = await donationPage.getTotalChargedAmountsAsText() - const feeAmountText = await donationPage.getFeeAmountsAsText() - const donationAmountText = await donationPage.getDonationAmountsAsText() - expect.soft(totalChargedAmountText).toEqual('13,56 лв.') - expect.soft(feeAmountText).toEqual('0,66 лв.') - expect(donationAmountText).toEqual('12,90 лв.') - }) - }, -) diff --git a/e2e/tests/regression/donation-flow/anon-donation-fixed.spec.ts b/e2e/tests/regression/donation-flow/anon-donation-fixed.spec.ts deleted file mode 100644 index 8ff061a25..000000000 --- a/e2e/tests/regression/donation-flow/anon-donation-fixed.spec.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { test, expect, Page } from '@playwright/test' -import { HeaderPage } from '../../../pages/web-pages/header.page' -import { HomePage } from '../../../pages/web-pages/home.page' -import { CampaignsPage } from '../../../pages/web-pages/campaigns/campaigns.page' -import { enLocalizationOneTimeDonation } from '../../../data/localization' -import { DonationPage } from '../../../pages/web-pages/campaigns/donation-old.page' -import { DonationRegions } from '../../../data/enums/donation-regions.enum' -import { LanguagesEnum } from '../../../data/enums/languages.enum' - -// This spec contains E2E tests related to anonymous donation flow - fixed amount -// The tests are dependent, the whole describe should be runned -test.describe.serial( - 'Anonymous contributor is able to donate fixed amount - EN language version', - async () => { - let page: Page - let homepage: HomePage - let headerPage: HeaderPage - let campaignsPage: CampaignsPage - let donationPage: DonationPage - // Localization texts - const enCardIncludeFeesText = enLocalizationOneTimeDonation['third-step']['card-include-fees'] - - test.beforeAll(async ({ browser }) => { - page = await browser.newPage() - homepage = new HomePage(page) - headerPage = new HeaderPage(page) - campaignsPage = new CampaignsPage(page) - donationPage = new DonationPage(page) - // For local executions use method navigateToLocalhostHomepage(); - // await homepage.navigateToLocalhostHomepage(); - await homepage.navigateToEnvHomepage() - await headerPage.changeanguageHeaderButtonToBe(LanguagesEnum.EN) - }) - - test.afterAll(async () => { - await page.close() - }) - - test('Particular campaign can be opened through the Campaign page', async () => { - await headerPage.clickDonateHeaderNavButton() - await campaignsPage.clickCampaignCardByIndex(0) - // We move from the common Campaigns page to the particular campain page - // check if the url is changed only based on the url pattern http://localhost:3040/campaigns/{slug-based-regexp} - // expect to not break - - expect( - await campaignsPage.checkPageUrlByRegExp(), - 'The url is not changed after clicking on the campaign card.', - ) - }) - - test('The total charge, fee tax and donation amount are visible on the Campaign page', async () => { - await campaignsPage.clickDonationSupportButton() - await donationPage.checkPageUrlByRegExp() - expect - .soft( - await donationPage.isSelectAmountStepActive(LanguagesEnum.EN), - 'Select Amount step is not active.', - ) - .toBeTruthy() - await donationPage.selectRadioButtonByLabelText(['10']) - await donationPage.setDonationRegionFromTheDropdown(DonationRegions.EUROPE) - await donationPage.selectCheckboxByLabelText([enCardIncludeFeesText]) - // Expected pattern: - // For your donation of {donationAmountText}, the fee from Stripe will be {feeAmountText}, and the total charged amount will be {totalChargedAmountText}. - const donationAmountText = await donationPage.getTotalChargedAmountsAsText() - const feeAmountText = await donationPage.getFeeAmountsAsText() - const totalChargedAmountText = await donationPage.getDonationAmountsAsText() - expect.soft(donationAmountText).toMatch('10.00') - expect.soft(feeAmountText).toMatch('0.63') - expect(totalChargedAmountText).toMatch('10.63') - }) - - test('The total charge, fee tax and donation amount are recalculated correctly when the donation amount is changed', async () => { - await donationPage.selectRadioButtonByLabelText(['20']) - // Expected pattern: - // For your donation of {donationAmountText}, the fee from Stripe will be {feeAmountText}, and the total charged amount will be {totalChargedAmountText}. - const donationAmountText = await donationPage.getTotalChargedAmountsAsText() - const feeAmountText = await donationPage.getFeeAmountsAsText() - const totalChargedAmountText = await donationPage.getDonationAmountsAsText() - expect.soft(donationAmountText).toMatch('20.00') - expect.soft(feeAmountText).toMatch('0.75') - expect(totalChargedAmountText).toMatch('20.75') - }) - }, -) diff --git a/public/locales/bg/one-time-donation.json b/public/locales/bg/one-time-donation.json deleted file mode 100644 index 014d90d1e..000000000 --- a/public/locales/bg/one-time-donation.json +++ /dev/null @@ -1,95 +0,0 @@ -{ - "step-labels": { - "amount": "Изберете сума", - "personal-profile": "Личен профил", - "wish": "Пожелайте нещо", - "payment": "Плащане" - }, - "anonymous-menu": { - "checkbox-label": "Дарение без регистрация", - "info-start": "При дарение без регистрация, няма да можем да Ви изпратим сертификат за дарение, който да използвате за данъчни облекчения. Ако искате да получите сертификат, регистрирайте се или влезте в профила си.", - "firstName": "Име", - "lastName": "Фамилия", - "phone": "Телефон", - "info-end": "Данните Ви няма да бъдат споделяни с никого.", - "want-anonymous-donation": "Искам да даря анонимно!" - }, - "first-step": { - "wish": "Искате ли да пожелаете нещо на бенефициента?", - "message": "Вашето послание", - "check-box-label": "Анонимно дарение", - "info-anonymous": "Анонимно дарение означава, че организаторът и бенефициентът на кампания няма да узнаят Вашето име", - "amount": "Каква сума желаете да дарите?", - "other": "Друга сума", - "BGN": "лв." - }, - "second-step": { - "login": "Влизане в профил", - "password": "Парола", - "checkbox-label": "Запомни", - "btn-login": "Влизане", - "new-create": "Или", - "new-create-profile": "Създайте нов профил", - "donate-anonymously": "Дарете анонимно", - "intro-text": "Можете да дарите с личен профил или анонимно.", - "logged-user": "Вече сте влезли във Вашия профил", - "info-logged-user": "Вашето дарение ще бъде свързано с име: {{fullName}} и email: {{email}}, освен ако не решите да дарите анонимно." - }, - "success": { - "title": "Благодарим за доверието и подкрепата!", - "title-bank": "Ще очакваме Вашето дарение!", - "subtitle": "Вашето дарение ще помогне на кампанията по-бързо да постигне своята цел!", - "subtitle-bank": "Благодарим за доверието и запомнете да впишете кода на дарението в основанието на превода си!", - "say-to-us": "Вашата обратна връзка е важна за нас!", - "share-to": "Подкрепете кампанията, като споделите информация за нея в социалните мрежи.", - "btn-generate": "Генерирай Сертификат", - "btn-say-to-us": "Oбратна връзка", - "btn-other-campaign": "Oще кампании", - "btn-back-to-campaign": "Виж кампанията" - }, - "third-step": { - "title": "Как желаете да дарите?", - "card": "Карта", - "card-include-fees": "Искам да покрия таксата за карта издадена в: ", - "card-fees": "При дарение с карта използваме услугите на Stripe, като за всеки трансфер Stripe начисляват такса според региона на картоиздателя ви. За да се ориентирате за нетното дарение, след като изберете желаната сума, ще ви покажем изчислената такса. Повече за таксите от Страйп вижте тук: ", - "card-calculated-fees": "За вашия превод от {{totalAmount}}, таксата на Stripe ще е {{fees}}, а кампанията ще получи {{amount}}", - "card-region": { - "title": "регион", - "EU": "Европа", - "UK": "Великобритания", - "Other": "други" - }, - "bank-payment": "Банков превод", - "bank-instructions1": "За дарение по банков път, моля използвайте приложението препоръчано от Вашата банка като въведете данните посочени по-долу. Дарението Ви няма да се отрази веднага в системата, тъй като все още предстои разработка на интеграцията с банката.", - "bank-instructions2": "Разчитаме на Вас да си направите профил или да ни оставите email на следващaтa стъпкa, за да можем да свържем дарението с Вашия потребителски профил. Благодарим предварително!!!", - "bank-details": "Детайли на банкова сметка:", - "btn-copy": "Копирай", - "owner": "Сдружение Подкрепи БГ", - "bank": "Уникредит Булбанк", - "reason-donation": "Като основание за превод въведете:", - "message-warning": "Ако не въведете точно основанието, може да не успеем да разпределим парите към предназначената кампания.", - "recurring-donation": "Дарявай повторно всеки месец тази сума до края на кампанията! Може да се откажете по всяко време." - }, - "alerts": { - "success": "Дарението е направено успешно!", - "error": "Възникна грешка в процеса на обработка!" - }, - "fail": { - "title": "За съжаление, възникна проблем!", - "subtitle": "Трансакцията не можа да бъде осъществена. Причините могат да бъдат няколко, включително проблем с Вашата интернет връзка.", - "btn-again": "Опитайте пак", - "btn-connect": "Пишете ни", - "btn-back-to-campaign": "Виж кампанията" - }, - "errors-fields": { - "checkbox-anonymous": "Моля изберете опция Анонимно дарение или Дарение без регистрация ако искате да продължите!", - "bank-payment": "Съжаляваме за създаденото неудобство временно може да дарите само чрез банков превод!", - "amount": "Моля изберете сумата, която желаете да дарите!", - "other-amount": "Минималната сума която може да дарите с карта е 1лв." - }, - "btns": { - "back": "Назад", - "next": "Напред", - "end": "Премини към плащане" - } -} diff --git a/public/locales/en/one-time-donation.json b/public/locales/en/one-time-donation.json deleted file mode 100644 index e20f2eece..000000000 --- a/public/locales/en/one-time-donation.json +++ /dev/null @@ -1,95 +0,0 @@ -{ - "step-labels": { - "amount": "Select amount", - "personal-profile": "Personal profile", - "wish": "Send a wish", - "payment": "Payment" - }, - "anonymous-menu": { - "checkbox-label": "Donate anonymously", - "info-start": "When donating without registration we won't be able to send you back a donation certificate or a list of your donations. If you still want to receive a certificate, please share at least your email - it will not be visible in the platform.", - "firstName": "First name", - "lastName": "Last name", - "phone": "Telephone", - "info-end": "Your data would not be shared with anyone", - "want-anonymous-donation": "I want to donate anonymously!" - }, - "first-step": { - "wish": "Would you like to wish something to the beneficiary?", - "message": "Your wish", - "check-box-label": "Anonymous donation", - "info-anonymous": "Anonymous donation means that the organisator and beneficiery would not be aware of your identity", - "amount": "How much would you like to donate?", - "other": "Other amount", - "BGN": "BGN" - }, - "second-step": { - "login": "Log in", - "password": "Password", - "checkbox-label": "Remember me", - "btn-login": "Log in", - "new-create": "Or", - "new-create-profile": "Create new profile", - "donate-anonymously": "Donate anonymously", - "intro-text": "You can donate with personal profile or anonymously.", - "logged-user": "You are already logged in to your account", - "info-logged-user": "Your donation would be connected with name: {{fullName}} and email: {{email}}, unless you decide to donate anonymously." - }, - "success": { - "title": "We thank you for your help and trust!", - "title-bank": "We look forward to your donation!", - "subtitle": "Your donation would help the campaign get to it's target sooner!", - "subtitle-bank": "Thank you for your trust and don't forget to enter the donation code as the reason for your transfer!", - "say-to-us": "Your feedback is important to us!", - "share-to": "Please support this campaign by sharing its link to social networks for reaching more people!", - "btn-generate": "Generate a certificate", - "btn-say-to-us": "Feedback", - "btn-other-campaign": "See more campaigns", - "btn-back-to-campaign": "See the campaign" - }, - "third-step": { - "title": "How would you like to pay", - "card": "Card", - "card-include-fees": "I want to cover transaction fees for card issued in:", - "card-fees": "For donations by card we use the services of Stripe and for every transfer they charge a fee depending on region of your card issuer. To orient you about the net donation, after you choose the desired amount, we will show you the calculated fee below. More details for Stripe fees can be found here: ", - "card-calculated-fees": "For your donation of {{amount}}, the fee from Stripe will be {{fees}}, and the total charged amount will be {{totalAmount}}", - "card-region": { - "title": "region", - "EU": "Europe", - "UK": "Great Britain", - "Other": "other" - }, - "bank-payment": "Bank transfer", - "bank-instructions1": "To donate via bank transfer, please use your bank recommonded application with entering our bank details from below. The donation will not be updated immediatelly as the implemnetation of the automated bank integration is still pending.", - "bank-instructions2": "We trust you to register or to leave an email on the next step, so that we can link your donation to your account. Thank you!!!", - "bank-details": "Details of our bank account:", - "btn-copy": "Copy", - "owner": "Association Podkrepi BG", - "bank": "Unicredit Bulbank", - "reason-donation": "For payment reference use:", - "message-warning": "If you don't enter the exact reference we may not be able to assign the money to the desired campaign.", - "recurring-donation": "Donate the same amount every month until the end of the campaign! Cancel anytime." - }, - "alerts": { - "success": "Donation was processed successfully!", - "error": "Error ocurred during processing of the donation!" - }, - "fail": { - "title": "Unfortunately there was a problem!", - "subtitle": "The transaction could not be done. The reasons for that many, including a problem with your internet connection.", - "btn-again": "Try again", - "btn-connect": "Contact us", - "btn-back-to-campaign": "See the campaign" - }, - "errors-fields": { - "checkbox-anonymous": "Please select an option for anonymous donation or donation without registering if you would like to continue!", - "bank-payment": "We are sorry for the inconvenience, but you can currently donate only through a bank transfer", - "amount": "Please select the amount you would like to donate!", - "other-amount": "The minimum amount you can donate with a card is BGN 1." - }, - "btns": { - "back": "Back", - "next": "Next", - "end": "Finish" - } -} diff --git a/src/common/routes.ts b/src/common/routes.ts index 50fff4509..0811c972c 100644 --- a/src/common/routes.ts +++ b/src/common/routes.ts @@ -55,7 +55,6 @@ export const routes = { index: '/campaigns', create: '/campaigns/create', viewCampaignBySlug: (slug: string) => `/campaigns/${slug}`, - oneTimeDonation: (slug: string) => `/campaigns/donation-old/${slug}`, donation: (slug: string) => `/campaigns/donation/${slug}`, donationStatus: (slug: string) => `/campaigns/donation/${slug}/status`, }, diff --git a/src/components/common/form/CircleCheckboxField.tsx b/src/components/common/form/CircleCheckboxField.tsx index 36fcf09f2..193f28f9d 100644 --- a/src/components/common/form/CircleCheckboxField.tsx +++ b/src/components/common/form/CircleCheckboxField.tsx @@ -22,7 +22,7 @@ export type CircleCheckboxField = { } export default function CircleCheckboxField({ name, label, labelProps }: CircleCheckboxField) { - const { t } = useTranslation('one-time-donation') + const { t } = useTranslation() const [field, meta] = useField(name) const helperText = meta.touched ? translateError(meta.error as TranslatableField, t) : '' return ( diff --git a/src/components/common/form/RadioButtonGroup.tsx b/src/components/common/form/RadioButtonGroup.tsx index 18458bcb4..69035cd00 100644 --- a/src/components/common/form/RadioButtonGroup.tsx +++ b/src/components/common/form/RadioButtonGroup.tsx @@ -45,7 +45,7 @@ export default function RadioButtonGroup({ muiRadioGroupProps, muiRadioButtonGridProps, }: RadioButtonGroup) { - const { t } = useTranslation('one-time-donation') + const { t } = useTranslation() const [field, meta, { setValue }] = useField(name) const helperText = meta.touched ? translateError(meta.error as TranslatableField, t) : '' return ( diff --git a/src/components/one-time-donation/AnonymousForm.tsx b/src/components/one-time-donation/AnonymousForm.tsx deleted file mode 100644 index fdd735ea1..000000000 --- a/src/components/one-time-donation/AnonymousForm.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import * as React from 'react' -import { useTranslation } from 'next-i18next' -import { Grid, Typography } from '@mui/material' -import EmailField from 'components/common/form/EmailField' - -export default function AnonymousForm() { - const { t } = useTranslation('one-time-donation') - return ( - <> - - {t('anonymous-menu.checkbox-label')} - - - - - {t('anonymous-menu.info-start')} - - - - - - - ) -} diff --git a/src/components/one-time-donation/FormikStepper.tsx b/src/components/one-time-donation/FormikStepper.tsx deleted file mode 100644 index 3a270b8fa..000000000 --- a/src/components/one-time-donation/FormikStepper.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import React, { PropsWithChildren, useCallback, useContext, useEffect } from 'react' -import { styled } from '@mui/material/styles' -import { useTranslation } from 'next-i18next' -import { useRouter } from 'next/router' -import { Form, Formik, FormikConfig, FormikValues } from 'formik' -import { LoadingButton } from '@mui/lab' -import { Box, Button, Grid, Step, StepLabel, Stepper, useMediaQuery } from '@mui/material' -import { StepsContext } from './helpers/stepperContext' -import { OneTimeDonation } from 'gql/donations' -import { useSession } from 'next-auth/react' - -const PREFIX = 'FormikStepper' - -const classes = { - container: `${PREFIX}-container`, - stepIcon: `${PREFIX}-stepIcon`, -} - -const StyledStepper = styled('div')(() => ({ - [`& .${classes.stepIcon}`]: { - transform: 'scale(1.15)', - }, -})) - -export interface FormikStepProps - extends Pick, 'children' | 'validationSchema'> { - label?: string -} - -export function FormikStep({ children }: FormikStepProps) { - return <>{children} -} - -export type GenericFormProps = PropsWithChildren> - -export function FormikStepper({ children, ...props }: GenericFormProps) { - const childrenArray = React.Children.toArray(children) as React.ReactElement[] - const { step, setStep } = useContext(StepsContext) - const router = useRouter() - const mobile = useMediaQuery('(max-width:568px)') - useEffect(() => { - router.query.success === 'false' || router.query.success === 'true' ? setStep(3) : null - }, [router.query.success]) - const currentChild = childrenArray[step] - const { data: session } = useSession() - - function isLoginStep() { - return step === childrenArray.length - 3 - } - - function isLastStep() { - return step === childrenArray.length - 2 - } - - function isLogged() { - if (!session?.accessToken) { - return false - } - - return true - } - const { t } = useTranslation('one-time-donation') - const hideNextButton = useCallback( - (isAnonymous: boolean) => { - if (isLoginStep() && !isLogged() && !isAnonymous) { - return true - } - return false - }, - [step], - ) - return ( - { - if (isLastStep()) { - values.isAnonymous = isLogged() === false ? true : values.isAnonymous ?? !isLogged() - await props.onSubmit(values, helpers) - } else { - setStep((s) => s + 1) - helpers.setTouched({}) - } - }} - validateOnMount - validateOnBlur> - {({ isSubmitting, handleSubmit, isValid, values: { isAnonymous } }) => ( -
- - - {childrenArray.map((child, index) => ( - - - {!mobile && child.props.label} - - - ))} - - - {currentChild} - {/* Controls of the form */} - {step === 3 ? null : ( - - - - - - - {isSubmitting ? 'Потвърждение' : isLastStep() ? t('btns.end') : t('btns.next')} - - - - )} -
- )} -
- ) -} diff --git a/src/components/one-time-donation/LoggedUserDialog.tsx b/src/components/one-time-donation/LoggedUserDialog.tsx deleted file mode 100644 index afda80c86..000000000 --- a/src/components/one-time-donation/LoggedUserDialog.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react' -import { useSession } from 'next-auth/react' -import { useTranslation } from 'next-i18next' -import { Grid, Typography } from '@mui/material' -import theme from 'common/theme' - -function LoggedUserDialog() { - const { t } = useTranslation('one-time-donation') - const { data: session } = useSession() - - return ( - - - - {t('second-step.logged-user')} - - - - {session && session.user ? ( - - {t('second-step.info-logged-user', { - fullName: session.user.name, - email: session.user.email, - })} - - ) : ( - '' - )} - - - ) -} - -export default LoggedUserDialog diff --git a/src/components/one-time-donation/LoginForm.tsx b/src/components/one-time-donation/LoginForm.tsx deleted file mode 100644 index 615af598a..000000000 --- a/src/components/one-time-donation/LoginForm.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import React, { useContext, useState } from 'react' -import { useTranslation } from 'next-i18next' -import { useFormikContext } from 'formik' -import { Box, Button, CircularProgress, Grid, Typography } from '@mui/material' -import theme from 'common/theme' -import Google from 'common/icons/Google' -import { OneTimeDonation } from 'gql/donations' -import EmailField from '../common/form/EmailField' -import { signIn } from 'next-auth/react' -import { StepsContext } from './helpers/stepperContext' -import { AlertStore } from 'stores/AlertStore' -import PasswordField from 'components/common/form/PasswordField' - -const onGoogleLogin = () => { - const resp = signIn('google') -} - -function LoginForm() { - const { t } = useTranslation('one-time-donation') - const [loading, setLoading] = useState(false) - const { setStep } = useContext(StepsContext) - const formik = useFormikContext() - - const onClick = async () => { - try { - setLoading(true) - - const resp = await signIn<'credentials'>('credentials', { - email: formik.values.loginEmail, - password: formik.values.loginPassword, - redirect: false, - }) - if (resp?.error) { - throw new Error(resp.error) - } - if (resp?.ok) { - setLoading(false) - formik.setFieldValue('isAnonymous', false) - setStep(2) - AlertStore.show(t('auth:alerts.welcome'), 'success') - } - } catch (error) { - console.error(error) - setLoading(false) - AlertStore.show(t('auth:alerts.invalid-login'), 'error') - } - } - return ( - - - - {t('second-step.login')} - - - - - - - - - - - - ) -} - -export default LoginForm diff --git a/src/components/one-time-donation/OneTimeDonationPage.tsx b/src/components/one-time-donation/OneTimeDonationPage.tsx deleted file mode 100644 index 1a046fdeb..000000000 --- a/src/components/one-time-donation/OneTimeDonationPage.tsx +++ /dev/null @@ -1,152 +0,0 @@ -import Link from 'next/link' -import Image from 'next/image' -import { styled } from '@mui/material/styles' -import { Box, Grid, Typography, useMediaQuery } from '@mui/material' - -import theme from 'common/theme' -import { routes } from 'common/routes' -import { - backgroundCampaignPictureUrl, - beneficiaryCampaignPictureUrl, -} from 'common/util/campaignImageUrls' -import Layout from 'components/layout/Layout' -import { useViewCampaign } from 'common/hooks/campaigns' -import CenteredSpinner from 'components/common/CenteredSpinner' - -import DonationStepper from './Steps' -// import RadioAccordionGroup, { testRadioOptions } from 'components/donation-flow/common/RadioAccordionGroup' -// import RadioCardGroup, { testRadioOptions } from 'components/donation-flow/common/RadioCardGroup' -// import PaymentDetailsStripeForm from 'components/donations/stripe/PaymentDetailsStripeForm' - -const PREFIX = 'OneTimeDonationPage' - -const classes = { - bannerWrapper: `${PREFIX}-bannerWrapper`, - banner: `${PREFIX}-banner`, - beneficiaryAvatarWrapper: `${PREFIX}-beneficiaryAvatarWrapper`, - beneficiaryAvatar: `${PREFIX}-beneficiaryAvatar`, - stepperWrapper: `${PREFIX}-stepperWrapper`, -} - -const StyledLayout = styled(Layout)(({ theme }) => ({ - [`& .${classes.bannerWrapper}`]: { - '& span': { - position: 'inherit !important', - }, - }, - - [`& .${classes.banner}`]: { - zIndex: -1, - maxHeight: '350px !important', - marginTop: `${theme.spacing(10)} !important`, - [theme.breakpoints.up('md')]: { - marginTop: `${theme.spacing(14)} !important`, - }, - objectFit: 'cover', - }, - - [`& .${classes.beneficiaryAvatarWrapper}`]: { - textAlign: 'center', - [theme.breakpoints.up('md')]: { - textAlign: 'center', - }, - }, - - [`& .${classes.beneficiaryAvatar}`]: { - borderRadius: '50%', - border: `4px solid ${theme.palette.common.white} !important`, - textAlign: 'center', - }, - - [`& .${classes.stepperWrapper}`]: { - gap: theme.spacing(2), - display: 'grid', - }, -})) - -const scrollWindow = () => { - const bannerWrapper = document.getElementsByClassName(classes.bannerWrapper)[0] - const avatarWrapper = document.getElementsByClassName(classes.beneficiaryAvatarWrapper)[0] - let calculatedScrollY = 0 - if (bannerWrapper && avatarWrapper) { - calculatedScrollY = bannerWrapper.clientHeight + avatarWrapper.clientHeight / 2 - } - window.scrollTo({ top: calculatedScrollY, behavior: 'smooth' }) -} - -export default function OneTimeDonation({ slug }: { slug: string }) { - const { data, isLoading } = useViewCampaign(slug) - const matches = useMediaQuery('sm') - // const paymentIntentMutation = useCreatePaymentIntent({ - // amount: 100, - // currency: 'BGN', - // }) - // useEffect(() => { - // paymentIntentMutation.mutate() - // }, []) - if (isLoading || !data) return - const { campaign } = data - - const bannerSource = backgroundCampaignPictureUrl(campaign) - const beneficiaryAvatarSource = beneficiaryCampaignPictureUrl(campaign) - - return ( - - - - {/* A11Y TODO: Translate alt text */} - Campaign banner image - - - - - - - - - {campaign.title} - - - {/* {paymentIntentMutation.isLoading ? ( - - ) : ( - - )} */} - - {/* */} - {/* */} - - - - ) -} diff --git a/src/components/one-time-donation/RegisterDialog.tsx b/src/components/one-time-donation/RegisterDialog.tsx deleted file mode 100644 index 564277aaf..000000000 --- a/src/components/one-time-donation/RegisterDialog.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { Button, CircularProgress, Grid, Typography } from '@mui/material' -import React, { useContext, useState } from 'react' -import { signIn } from 'next-auth/react' -import { useTranslation } from 'next-i18next' -import theme from 'common/theme' -import { useRegister } from 'service/auth' -import { AlertStore } from 'stores/AlertStore' -import FormTextField from 'components/common/form/FormTextField' -import PasswordField from 'components/common/form/PasswordField' -import EmailField from 'components/common/form/EmailField' -import { useFormikContext } from 'formik' -import { OneTimeDonation } from 'gql/donations' -import { RegisterFormData } from 'components/auth/register/RegisterForm' -import { StepsContext } from './helpers/stepperContext' - -export default function RegisterForm() { - const { t } = useTranslation() - const [loading, setLoading] = useState(false) - const { mutateAsync: register } = useRegister() - const formik = useFormikContext() - const { setStep } = useContext(StepsContext) - - const values: RegisterFormData = { - firstName: formik.values.registerFirstName as string, - lastName: formik.values.registerLastName as string, - email: formik.values.registerEmail as string, - password: formik.values.registerPassword as string, - confirmPassword: formik.values.confirmPassword as string, - terms: formik.values.terms as boolean, - gdpr: formik.values.gdpr as boolean, - } - const onClick = async () => { - try { - setLoading(true) - - // Register in Keycloak - await register(values) - - // Authenticate - const resp = await signIn<'credentials'>('credentials', { - email: values.email, - password: values.password, - redirect: false, - }) - if (resp?.error) { - throw new Error(resp.error) - } - if (resp?.ok) { - setLoading(false) - AlertStore.show(t('auth:alerts.welcome'), 'success') - formik.setFieldValue('isAnonymous', false) - setStep(2) - } - } catch (error) { - console.error(error) - setLoading(false) - AlertStore.show(t('auth:alerts.invalid-login'), 'error') - } - } - - return ( - <> - - {t('one-time-donation:second-step.new-create-profile')} - - - - - - - - - - - - - - - - - - - - - - - ) -} diff --git a/src/components/one-time-donation/Steps.tsx b/src/components/one-time-donation/Steps.tsx deleted file mode 100644 index 98cb255e6..000000000 --- a/src/components/one-time-donation/Steps.tsx +++ /dev/null @@ -1,181 +0,0 @@ -import * as React from 'react' -import { useTranslation } from 'next-i18next' -import { useRouter } from 'next/router' -import { useSession } from 'next-auth/react' -import { CircularProgress } from '@mui/material' -import { AxiosError } from 'axios' -import { FormikHelpers } from 'formik' - -import { CardRegion, PaymentProvider } from 'gql/donations.enums' -import { OneTimeDonation, DonationStep as StepType } from 'gql/donations' -import { createDonationWish } from 'service/donationWish' -import { ApiErrors, isAxiosError, matchValidator } from 'service/apiErrors' -import { useCurrentPerson } from 'common/util/useCurrentPerson' -import CenteredSpinner from 'components/common/CenteredSpinner' -import { useDonationSession } from 'common/hooks/donation' -import { useViewCampaign } from 'common/hooks/campaigns' -import { baseUrl, routes } from 'common/routes' - -import FirstStep from './steps/FirstStep' -import SecondStep from './steps/SecondStep' -import ThirdStep from './steps/ThirdStep' -import Success from './steps/Success' -import Fail from './steps/Fail' -import { FormikStep, FormikStepper } from './FormikStepper' -import { validateFirst, validateSecond, validateThird } from './helpers/validation-schema' -import { StepsContext } from './helpers/stepperContext' - -const initialValues: OneTimeDonation = { - message: '', - isAnonymous: false, - amount: '', - amountWithFees: 0, - cardIncludeFees: false, - cardRegion: CardRegion.EU, - otherAmount: 0, - personsFirstName: '', - personsLastName: '', - personsEmail: '', - personsPhone: '', - payment: 'card', - loginEmail: '', - loginPassword: '', - registerEmail: '', - registerLastName: '', - registerFirstName: '', - registerPassword: '', - isRecurring: false, -} -interface DonationStepperProps { - onStepChange: () => void -} - -export default function DonationStepper({ onStepChange }: DonationStepperProps) { - const { t, i18n } = useTranslation('one-time-donation') - const router = useRouter() - const success = router.query.success === 'true' ? true : false - initialValues.amount = (router.query.price as string) || '' - const slug = String(router.query.slug) - const { data, isLoading } = useViewCampaign(slug) - const mutation = useDonationSession() - const { data: session } = useSession() - const { data: { user: person } = { user: null } } = useCurrentPerson() - if (isLoading || !data) return - const { campaign } = data - - function isLogged() { - return session && session.accessToken ? true : false - } - - initialValues.isAnonymous = !isLogged() - initialValues.isRecurring = false - - const userEmail = session?.user?.email - const donate = React.useCallback( - async (amount?: number, values?: OneTimeDonation) => { - const { data } = await mutation.mutateAsync({ - mode: values?.isRecurring ? 'subscription' : 'payment', - amount, - campaignId: campaign.id, - personId: person ? person?.id : '', - firstName: values?.personsFirstName ? values.personsFirstName : 'Anonymous', - lastName: values?.personsLastName ? values.personsLastName : 'Donor', - personEmail: values?.personsEmail ? values.personsEmail : userEmail, - isAnonymous: values?.isAnonymous !== undefined ? values.isAnonymous : true, - phone: values?.personsPhone ? values.personsPhone : null, - successUrl: `${baseUrl}/${i18n.language}/${routes.campaigns.donation( - campaign.slug, - )}?success=true`, - cancelUrl: `${baseUrl}/${i18n.language}/${routes.campaigns.donation( - campaign.slug, - )}?success=false`, - message: values?.message, - }) - if (values?.payment === PaymentProvider.bank) { - // Do not redirect for bank payments - return - } - if (data.session.url) { - //send the user to payment provider - window.location.href = data.session.url - } - }, - [mutation], - ) - - const onSubmit = async ( - values: OneTimeDonation, - { setFieldError, resetForm }: FormikHelpers, - ) => { - try { - if (values?.payment === PaymentProvider.bank) { - if (values?.message) { - await createDonationWish({ - message: values.message, - campaignId: campaign.id, - personId: !values.isAnonymous && person?.id ? person.id : null, - }) - } - router.push(`${baseUrl}${routes.campaigns.donation(campaign.slug)}?success=true`) - return - } - - const data = { - currency: campaign.currency, - amount: Math.round(values.amountWithFees), - } - await donate(data.amount, values) - resetForm() - } catch (error) { - if (isAxiosError(error)) { - const { response } = error as AxiosError - response?.data.message.map(({ property, constraints }) => { - setFieldError(property, t(matchValidator(constraints))) - }) - } - } - } - const steps: StepType[] = [ - { - label: 'amount', - component: , - validate: validateFirst, - }, - { - label: 'personal-profile', - component: , - validate: validateSecond, - }, - { - label: 'wish', - component: , - validate: validateThird, - }, - { - label: 'payment', - component: success ? : , - validate: null, - }, - ] - const [step, setStep] = React.useState(0) - - React.useEffect(() => { - onStepChange() - }, [step]) - - return ( - - {isLoading ? ( - - ) : ( - - {steps.map(({ label, component, validate }) => ( - - {component} - - ))} - - )} - - ) -} diff --git a/src/components/one-time-donation/helpers/paypalDonationButton.tsx b/src/components/one-time-donation/helpers/paypalDonationButton.tsx deleted file mode 100644 index 125980641..000000000 --- a/src/components/one-time-donation/helpers/paypalDonationButton.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { useEffect } from 'react' - -import { PayPalButtons, usePayPalScriptReducer } from '@paypal/react-paypal-js' -import { Box } from '@mui/material' -import { AlertStore } from 'stores/AlertStore' -import { useTranslation } from 'next-i18next' - -export type PaypalDonationButtonOptions = { - campaignId: string - amount: number - currency: string -} - -// Custom component to wrap the PayPalButtons and handle amount¤cy changes -export default function PaypalDonationButton({ - campaignId, - amount, - currency, -}: PaypalDonationButtonOptions) { - // usePayPalScriptReducer can be used only inside children of PayPalScriptProviders - // This is the main reason to wrap the PayPalButtons in a new component - const [{ options, isPending }, dispatch] = usePayPalScriptReducer() - - const { t } = useTranslation('one-time-donation') - - useEffect(() => { - dispatch({ - type: 'resetOptions', - value: { - ...options, - currency: currency, - }, - }) - }, [amount, currency]) - - return ( - - {isPending ?
: null} - { - return actions.order.create({ - purchase_units: [ - { - amount: { - value: amount.toString(), - currency_code: currency, - breakdown: { - item_total: { - currency_code: currency, - value: amount.toString(), - }, - }, - }, - custom_id: campaignId, // Paypal will send this in the webhook too - description: 'donation for campaign: ' + campaignId, - items: [ - { - category: 'DONATION', - name: 'Име на кампания', - description: 'Дарение за кампания', - quantity: '1', - unit_amount: { - currency_code: currency, - value: amount.toString(), - }, - }, - ], - }, - ], - }) - }} - onApprove={(data, actions) => { - if (actions.order) { - return actions.order.capture().then((details) => { - AlertStore.show(t('alerts.success'), 'success') - }) - } else { - return new Promise(() => { - AlertStore.show(t('alerts.error'), 'error') - }) - } - }} - /> - - ) -} diff --git a/src/components/one-time-donation/helpers/stepperContext.ts b/src/components/one-time-donation/helpers/stepperContext.ts deleted file mode 100644 index 327981246..000000000 --- a/src/components/one-time-donation/helpers/stepperContext.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { createContext } from 'react' -import { CampaignResponse } from 'gql/campaigns' - -type Steps = { - step: number - setStep: React.Dispatch> - campaign: CampaignResponse -} -export const StepsContext = createContext({} as Steps) diff --git a/src/components/one-time-donation/helpers/stripe-fee-calculator.ts b/src/components/one-time-donation/helpers/stripe-fee-calculator.ts deleted file mode 100644 index ed71ff2f2..000000000 --- a/src/components/one-time-donation/helpers/stripe-fee-calculator.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { CardRegion } from 'gql/donations.enums' - -/** - * Calculates total charge amount in stotinki so that donation of netAmount is received after deducting Stripe fees - * References: - * - applied fees https://stripe.com/en-bg/pricing - * - formula for including fees: https://support.stripe.com/questions/passing-the-stripe-fee-on-to-customers - * - testing with International cards: https://stripe.com/docs/testing#international-cards - * @param netAmount expected in stotinki - * @returns - */ -export function stripeIncludeFeeCalculator(netAmount: number, region: CardRegion) { - switch (region) { - case CardRegion.EU: { - return stripeIncludeFeeCalculatorEU(netAmount) - } - case CardRegion.UK: { - return stripeIncludeFeeCalculatorUK(netAmount) - } - case CardRegion.Other: { - return stripeIncludeFeeCalculatorOther(netAmount) - } - } -} - -export function stripeIncludeFeeCalculatorEU(netAmount: number) { - return (netAmount + 50) / (1 - 0.012) -} - -export function stripeIncludeFeeCalculatorUK(netAmount: number) { - return (netAmount + 50) / (1 - 0.025) -} - -export function stripeIncludeFeeCalculatorOther(netAmount: number) { - return (netAmount + 50) / (1 - 0.029) -} - -/** - * Calculates Stripe fees based on card region - * References: - * - applied fees https://stripe.com/en-bg/pricing - * - formula for including fees: https://support.stripe.com/questions/passing-the-stripe-fee-on-to-customers - * - testing with International cards: https://stripe.com/docs/testing#international-cards - * @param chargedAmount expected in stotinki - * @returns - */ -export function stripeFeeCalculator(chargedAmount: number, region: CardRegion) { - switch (region) { - case CardRegion.EU: { - return chargedAmount * 0.012 + 50 - } - case CardRegion.UK: { - return chargedAmount * 0.025 + 50 - } - case CardRegion.Other: { - return chargedAmount * 0.029 + 50 - } - } -} diff --git a/src/components/one-time-donation/helpers/validation-schema.ts b/src/components/one-time-donation/helpers/validation-schema.ts deleted file mode 100644 index ed5f09e20..000000000 --- a/src/components/one-time-donation/helpers/validation-schema.ts +++ /dev/null @@ -1,37 +0,0 @@ -import * as yup from 'yup' -import { name, phone, email, password } from 'common/form/validation' -import { FirstStep, SecondStep, ThirdStep } from 'gql/donations' - -export const validateFirst: yup.SchemaOf = yup - .object() - .defined() - .shape({ - payment: yup.string().required().oneOf(['card', 'bank']), - amount: yup.string().when('payment', { - is: 'card', - // Here we should fetch the possible payments to put into the oneOf, but it's not that important - then: yup.string().required(), - }), - otherAmount: yup.number().when('amount', { - is: 'other', - then: yup.number().min(1, 'one-time-donation:errors-fields.other-amount').required(), - }), - }) - -export const validateSecond: yup.SchemaOf = yup.object().defined().shape({ - isAnonymous: yup.boolean().required(), - personsEmail: email.notRequired(), - personsFirstName: name.notRequired(), - personsLastName: name.notRequired(), - personsPhone: phone.notRequired(), - loginEmail: email.notRequired(), - loginPassword: password.notRequired(), - registerEmail: email.notRequired(), - registerFirstName: yup.string().notRequired(), - registerLastName: yup.string().notRequired(), - registerPassword: password.notRequired(), -}) - -export const validateThird: yup.SchemaOf = yup.object().defined().shape({ - message: yup.string().notRequired(), -}) diff --git a/src/components/one-time-donation/steps/Fail.tsx b/src/components/one-time-donation/steps/Fail.tsx deleted file mode 100644 index 86e92d0ef..000000000 --- a/src/components/one-time-donation/steps/Fail.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { useContext, useEffect } from 'react' -import { useTranslation } from 'next-i18next' -import { useRouter } from 'next/router' -import { Grid, Typography, Button } from '@mui/material' -import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline' -import theme from 'common/theme' -import { routes } from 'common/routes' -import LinkButton from 'components/common/LinkButton' -import { StepsContext } from '../helpers/stepperContext' - -type Props = { - campaignSlug: string -} -export default function Fail({ campaignSlug }: Props) { - const { t } = useTranslation('one-time-donation') - const { setStep } = useContext(StepsContext) - const router = useRouter() - // Clear query so that the first step renders instead of success or fail page - useEffect(() => { - router.push(`${router.asPath.split('?')[0]}`) - }, []) - return ( - - - - - - - - {t('fail.title')} - - - - - - - - - - {t('fail.btn-back-to-campaign')} - - - - - {t('fail.btn-connect')} - - - - - ) -} diff --git a/src/components/one-time-donation/steps/FirstStep.tsx b/src/components/one-time-donation/steps/FirstStep.tsx deleted file mode 100644 index b363cdfc2..000000000 --- a/src/components/one-time-donation/steps/FirstStep.tsx +++ /dev/null @@ -1,329 +0,0 @@ -import React, { useContext, useEffect } from 'react' -import { styled } from '@mui/material/styles' -import { Trans, useTranslation } from 'next-i18next' -import { useField, useFormikContext } from 'formik' -import { Box, Collapse, Divider, Grid, InputAdornment, List, Typography } from '@mui/material' -import theme from 'common/theme' -import { useSinglePriceList } from 'common/hooks/donation' -import RadioButtonGroup from 'components/common/form/RadioButtonGroup' -import { moneyPublic, moneyPublicDecimals2, toMoney } from 'common/util/money' -import { ibanNumber } from 'common/iban' -import { CopyTextButton } from 'components/common/CopyTextButton' -import { StepsContext } from '../helpers/stepperContext' -import FormTextField from 'components/common/form/FormTextField' -import { useMediaQuery } from '@mui/material' -import { OneTimeDonation } from 'gql/donations' -import ExternalLink from 'components/common/ExternalLink' -import { stripeFeeCalculator, stripeIncludeFeeCalculator } from '../helpers/stripe-fee-calculator' -import CheckboxField from 'components/common/form/CheckboxField' -import FormSelectField from 'components/common/form/FormSelectField' -import { CardRegion } from 'gql/donations.enums' -import { PayPalScriptProvider } from '@paypal/react-paypal-js' -import { isAdmin } from 'common/util/roles' -import { useSession } from 'next-auth/react' -import getConfig from 'next/config' - -import dynamic from 'next/dynamic' -const PaypalDonationButton = dynamic(() => import('../helpers/paypalDonationButton'), { - ssr: false, -}) - -const PREFIX = 'FirstStep' - -const classes = { - divider: `${PREFIX}-divider`, -} - -// TODO jss-to-styled codemod: The Fragment root was replaced by div. Change the tag if needed. -const Root = styled('div')(() => ({ - [`& .${classes.divider}`]: { - border: '1px solid #000000', - }, -})) - -export default function FirstStep() { - const { data: session } = useSession() - const { data: oneTimePrices } = useSinglePriceList() - const { t } = useTranslation('one-time-donation') - const mobile = useMediaQuery('(max-width:600px)') - const paymentOptions = [ - { value: 'card', label: t('third-step.card') }, - { value: 'paypal', label: 'PayPal', hidden: !isAdmin(session) }, - { value: 'bank', label: t('third-step.bank-payment') }, - ] - - const [paymentField] = useField('payment') - const [amount] = useField('amount') - const [otherAmount] = useField('otherAmount') - const [amountWithFees] = useField('amountWithFees') - const [amountWithoutFees] = useField('amountWithoutFees') - - const formik = useFormikContext() - - const { campaign } = useContext(StepsContext) - const bankAccountInfo = { - owner: t('third-step.owner'), - bank: t('third-step.bank'), - iban: ibanNumber, - } - - useEffect(() => { - const chosenAmount = - amount.value === 'other' ? toMoney(formik.values.otherAmount) : Number(formik.values.amount) - - if (formik.values.cardIncludeFees) { - formik.setFieldValue('amountWithoutFees', chosenAmount) - formik.setFieldValue( - 'amountWithFees', - stripeIncludeFeeCalculator(chosenAmount, formik.values.cardRegion), - ) - } else { - formik.setFieldValue( - 'amountWithoutFees', - chosenAmount - stripeFeeCalculator(chosenAmount, formik.values.cardRegion), - ) - formik.setFieldValue('amountWithFees', chosenAmount) - } - }, [ - formik.values.otherAmount, - formik.values.amount, - formik.values.cardIncludeFees, - formik.values.cardRegion, - formik.values.isRecurring, - ]) - - function isLogged() { - return session && session.accessToken ? true : false - } - - return ( - - {t('third-step.title')} - - option.hidden != true).length} - options={paymentOptions} - /> - - - - - {t('third-step.bank-details')} - - - {t('third-step.bank-instructions1')} - - - {t('third-step.bank-instructions2')} - - - - - {bankAccountInfo.owner} - - - - {bankAccountInfo.bank} - - - - {ibanNumber} - - - - - - {t('third-step.reason-donation')} - - - - - - {campaign.paymentReference} - - - - - - {t('third-step.message-warning')} - - - - - {t('third-step.card-fees')} - - https://stripe.com/en-bg/pricing - - - - - {t('first-step.amount')} - - - Number(a.unit_amount) - Number(b.unit_amount)) - .map((v) => ({ - label: moneyPublic(Number(v.unit_amount)), - value: String(Number(v.unit_amount)), - })) - .concat({ label: t('first-step.other'), value: 'other' }) || [] - } - /> - - - - {t('first-step.BGN')} - - ), - }} - /> - - - {amount.value ? ( - - - - {t('third-step.card-include-fees')} - } - /> - - - - - - - - {t('third-step.recurring-donation')} - } - /> - - - ) : null} - - - - - - - - Note 1: This is a test Paypal implementation visible only to logged users with admin - rights. Using real cards will not charge any money. Note 2: Paypal transaction fee - is 3.4% + 0.35 euro cents. - - - - - - {t('first-step.BGN')} - - ), - }} - /> - - - - - - - - - - ) -} diff --git a/src/components/one-time-donation/steps/SecondStep.tsx b/src/components/one-time-donation/steps/SecondStep.tsx deleted file mode 100644 index e32111432..000000000 --- a/src/components/one-time-donation/steps/SecondStep.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { TabContext, TabList } from '@mui/lab' -import TabPanel from '@mui/lab/TabPanel' -import { Box, Tab, Typography, useMediaQuery } from '@mui/material' -import { OneTimeDonation } from 'gql/donations' -import { useSession } from 'next-auth/react' -import { useTranslation } from 'next-i18next' -import React, { useState } from 'react' -import AnonymousMenu from '../AnonymousForm' -import LoggedUserDialog from '../LoggedUserDialog' -import LoginForm from '../LoginForm' -import RegisterForm from '../RegisterDialog' -import { useFormikContext } from 'formik' - -enum Tabs { - Login = '1', - Register = '2', - Anonymous = '3', -} -export default function SecondStep() { - const { t } = useTranslation('one-time-donation') - const mobile = useMediaQuery('(max-width:575px)') - const { data: session } = useSession() - - const [value, setValue] = useState('1') - const formik = useFormikContext() - const handleChange = (event: React.SyntheticEvent, newTab: string) => { - if (newTab === Tabs.Anonymous) { - formik.setFieldValue('isAnonymous', true) - } else { - formik.setFieldValue('isAnonymous', false) - } - setValue(newTab) - } - - return ( - - {t('step-labels.personal-profile')} - {t('second-step.intro-text')} - - - - - {formik.values.isRecurring ? null : ( - - )} - - - - {session && session.accessToken ? : } - - - - - {formik.values.isRecurring ? null : ( - - - - )} - - ) -} diff --git a/src/components/one-time-donation/steps/Success.tsx b/src/components/one-time-donation/steps/Success.tsx deleted file mode 100644 index 3b15f6515..000000000 --- a/src/components/one-time-donation/steps/Success.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { useTranslation } from 'next-i18next' -import { Grid, Typography } from '@mui/material' -import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline' -import { routes } from 'common/routes' -import LinkButton from 'components/common/LinkButton' -import ExternalLinkButton from 'components/common/ExternalLinkButton' -import { useField } from 'formik' -import { PaymentProvider } from 'gql/donations.enums' - -type Props = { - campaignSlug: string - donationId?: string -} -export default function Success({ campaignSlug, donationId }: Props) { - const { t } = useTranslation('one-time-donation') - const [field] = useField('payment') - return ( - - - - - - - - {(field.value === PaymentProvider.bank && t('success.title-bank')) || - t('success.title')} - - - - - {(field.value === PaymentProvider.bank && t('success.subtitle-bank')) || - t('success.subtitle')} - - - - {t('success.share-to')} - - - {t('success.say-to-us')} - - - - {donationId && ( - - - {t('success.btn-generate')} - - - )} - - - {t('success.btn-back-to-campaign')} - - - - - {t('success.btn-say-to-us')} - - - - - {t('success.btn-other-campaign')} - - - - - ) -} diff --git a/src/components/one-time-donation/steps/ThirdStep.tsx b/src/components/one-time-donation/steps/ThirdStep.tsx deleted file mode 100644 index 750890298..000000000 --- a/src/components/one-time-donation/steps/ThirdStep.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { useTranslation } from 'next-i18next' -import { Grid, Typography } from '@mui/material' -import theme from 'common/theme' -import FormTextField from 'components/common/form/FormTextField' - -export default function ThirdStep() { - const { t } = useTranslation('one-time-donation') - return ( - - - - {t('first-step.wish')} - - - - - - - ) -} diff --git a/src/components/support-us-form/SupportUsForm.tsx b/src/components/support-us-form/SupportUsForm.tsx index 9f85f6aed..b90da67f0 100644 --- a/src/components/support-us-form/SupportUsForm.tsx +++ b/src/components/support-us-form/SupportUsForm.tsx @@ -1,6 +1,5 @@ -import { useTranslation } from 'next-i18next' import React from 'react' -import { styled } from '@mui/material/styles' +import { useTranslation } from 'next-i18next' import { Grid, List, Typography, Divider } from '@mui/material' import theme from 'common/theme' @@ -12,19 +11,12 @@ const classes = { divider: `${PREFIX}-divider`, } -// TODO jss-to-styled codemod: The Fragment root was replaced by div. Change the tag if needed. -const Root = styled('div')(() => ({ - [`& .${classes.divider}`]: { - border: '1px solid #000000', - }, -})) - export default function SupportUsForm() { - const { t } = useTranslation('one-time-donation') + const { t } = useTranslation('donation-flow') const bankAccountInfo = { - owner: t('third-step.owner'), - bank: t('third-step.bank'), + owner: t('step.payment-method.bank.owner'), + bank: t('step.payment-method.bank.bank'), iban: ibanNumber, } @@ -33,13 +25,13 @@ export default function SupportUsForm() { {t('support_us:support-info')} - {t('third-step.bank-details')} + {t('step.payment-method.bank.bank-details')} {bankAccountInfo.owner} {bankAccountInfo.bank} {ibanNumber} - {t('third-step.reason-donation')} + {t('step.payment-method.bank.reason-donation')} @@ -77,7 +69,7 @@ export default function SupportUsForm() { variant="contained" color="info" size="small" - label={t('third-step.btn-copy')} + label={t('step.payment-method.bank.btn-copy')} /> diff --git a/src/pages/campaigns/donation-old/[slug].tsx b/src/pages/campaigns/donation-old/[slug].tsx deleted file mode 100644 index 774dc9413..000000000 --- a/src/pages/campaigns/donation-old/[slug].tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { QueryClient, dehydrate } from '@tanstack/react-query' -import { GetServerSideProps, GetServerSidePropsContext } from 'next' -import { serverSideTranslations } from 'next-i18next/serverSideTranslations' - -import OneTimeDonation from 'components/one-time-donation/OneTimeDonationPage' -import { endpoints } from 'service/apiEndpoints' -import { queryFnFactory } from 'service/restRequests' -import { CampaignResponse } from 'gql/campaigns' - -export const getServerSideProps: GetServerSideProps = async (ctx: GetServerSidePropsContext) => { - const { slug } = ctx.query - const client = new QueryClient() - await client.prefetchQuery( - [endpoints.campaign.viewCampaign(slug as string)], - queryFnFactory(), - ) - return { - props: { - slug, - dehydratedState: dehydrate(client), - ...(await serverSideTranslations(ctx.locale ?? 'bg', [ - 'common', - 'auth', - 'validation', - 'campaigns', - 'one-time-donation', - ])), - }, - } -} - -export default OneTimeDonation From 9237dc6cf5144eed0f61c230f39a9dcdbd6ea604 Mon Sep 17 00:00:00 2001 From: "dimitar.nizamov" Date: Sat, 11 Feb 2023 14:10:58 +0100 Subject: [PATCH 04/12] 1272-finalization - remove one-time donation server side translations --- src/components/donation-flow/alerts/PaymentSummaryAlert.tsx | 2 +- src/pages/support_us.tsx | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/components/donation-flow/alerts/PaymentSummaryAlert.tsx b/src/components/donation-flow/alerts/PaymentSummaryAlert.tsx index 7f31b206f..192fa5eb5 100644 --- a/src/components/donation-flow/alerts/PaymentSummaryAlert.tsx +++ b/src/components/donation-flow/alerts/PaymentSummaryAlert.tsx @@ -5,7 +5,7 @@ import { Alert, Box, BoxProps, IconButton, Stack, Tooltip, Typography } from '@m import { styled } from '@mui/styles' import theme from 'common/theme' import { moneyPublicDecimals2 } from 'common/util/money' -import { stripeFeeCalculator } from 'components/one-time-donation/helpers/stripe-fee-calculator' +import { stripeFeeCalculator } from 'components/donation-flow/helpers/stripe-fee-calculator' import { CardRegion } from 'gql/donations.enums' const StyledTypography = styled(Typography)(({ theme }) => ({ diff --git a/src/pages/support_us.tsx b/src/pages/support_us.tsx index be093b62c..e7b79742e 100644 --- a/src/pages/support_us.tsx +++ b/src/pages/support_us.tsx @@ -5,11 +5,7 @@ import SupportUsFormPage from 'components/support-us-form/SupportUsPage' export const getStaticProps: GetStaticProps = async ({ locale }) => ({ props: { - ...(await serverSideTranslations(locale ?? 'bg', [ - 'common', - 'support_us', - 'one-time-donation', - ])), + ...(await serverSideTranslations(locale ?? 'bg', ['common', 'support_us', 'donation-flow'])), }, }) From 3e1ab9593cf1a4b1d6c2bf9cbe6b6b81772865d8 Mon Sep 17 00:00:00 2001 From: "dimitar.nizamov" Date: Sun, 12 Feb 2023 14:40:35 +0100 Subject: [PATCH 05/12] 1317 - add new donation page POM --- e2e/data/donation-test.data.ts | 7 + e2e/data/localization.ts | 6 - e2e/data/support-page-tests.data.ts | 8 - .../web-pages/campaigns/donation-old.page.ts | 245 ------------------ .../web-pages/campaigns/donation.page.ts | 142 ++++++++++ .../anon-donation-custom.spec.ts | 134 ---------- .../anon-donation-fixed.spec.ts | 142 ---------- .../anon-donation-custom.spec.ts | 73 ++++++ .../donation-flow/anon-donation-fixed.spec.ts | 71 +++++ 9 files changed, 293 insertions(+), 535 deletions(-) create mode 100644 e2e/data/donation-test.data.ts delete mode 100644 e2e/pages/web-pages/campaigns/donation-old.page.ts create mode 100644 e2e/pages/web-pages/campaigns/donation.page.ts delete mode 100644 e2e/tests/regression/donation-flow-old/anon-donation-custom.spec.ts delete mode 100644 e2e/tests/regression/donation-flow-old/anon-donation-fixed.spec.ts create mode 100644 e2e/tests/regression/donation-flow/anon-donation-custom.spec.ts create mode 100644 e2e/tests/regression/donation-flow/anon-donation-fixed.spec.ts diff --git a/e2e/data/donation-test.data.ts b/e2e/data/donation-test.data.ts new file mode 100644 index 000000000..0ab003ef0 --- /dev/null +++ b/e2e/data/donation-test.data.ts @@ -0,0 +1,7 @@ +export const stripeFormData = { + cardNumber: '4242 4242 4242 4242', + email: 'e2e_test_mail@test.bg', + expiryDate: '04 / 42', + cvc: '424', + country: 'BG', +} diff --git a/e2e/data/localization.ts b/e2e/data/localization.ts index bf0716d81..8ce2194c2 100644 --- a/e2e/data/localization.ts +++ b/e2e/data/localization.ts @@ -13,9 +13,6 @@ import enLocalizationValidationJson from '../../public/locales/en/validation.jso import bgLocalizationCampaignsJson from '../../public/locales/bg/campaigns.json' import enLocalizationCampaignsJson from '../../public/locales/en/campaigns.json' -import bgLocalizationOneTimeDonationJson from '../../public/locales/bg/one-time-donation.json' -import enLocalizationOneTimeDonationJson from '../../public/locales/en/one-time-donation.json' - import bgLocalizationDonationFlowJson from '../../public/locales/bg/donation-flow.json' import enLocalizationDonationFlowJson from '../../public/locales/en/donation-flow.json' @@ -32,9 +29,6 @@ export const enLocalizationSupport = enLocalizationSupportJson // Campaigns page export const bgLocalizationCampaigns = bgLocalizationCampaignsJson export const enLocalizationCampaigns = enLocalizationCampaignsJson -// Donations - old -export const bgLocalizationOneTimeDonation = bgLocalizationOneTimeDonationJson -export const enLocalizationOneTimeDonation = enLocalizationOneTimeDonationJson // Donations export const bgLocalizationDonationFlow = bgLocalizationDonationFlowJson export const enLocalizationDonationFlow = enLocalizationDonationFlowJson diff --git a/e2e/data/support-page-tests.data.ts b/e2e/data/support-page-tests.data.ts index d93cddc8e..d08c6b1ad 100644 --- a/e2e/data/support-page-tests.data.ts +++ b/e2e/data/support-page-tests.data.ts @@ -5,11 +5,3 @@ export const supportPageVolutneerTestData = { phone: '+359888000000', comment: 'E2E Test comment', } - -export const anonDonationTestData = { - cardNumber: '4242 4242 4242 4242', - cardExpDate: '04 / 42', - cardCvc: '424', - billingName: 'E2E Test Anonymous Donation', - country: 'BG', -} diff --git a/e2e/pages/web-pages/campaigns/donation-old.page.ts b/e2e/pages/web-pages/campaigns/donation-old.page.ts deleted file mode 100644 index 2ee58807b..000000000 --- a/e2e/pages/web-pages/campaigns/donation-old.page.ts +++ /dev/null @@ -1,245 +0,0 @@ -import { Page, expect } from '@playwright/test' -import { DonationRegions } from '../../../data/enums/donation-regions.enum' -import { LanguagesEnum } from '../../../data/enums/languages.enum' -import { - bgLocalizationOneTimeDonation, - enLocalizationOneTimeDonation, -} from '../../../data/localization' -import { SLUG_REGEX } from '../../../utils/helpers' -import { CampaignsPage } from './campaigns.page' - -export class DonationPage extends CampaignsPage { - constructor(page: Page) { - super(page) - } - - // -> Select amount section <- - private readonly otherAmountInputField = ".MuiCollapse-entered input[name='otherAmount']" - private readonly allAmountsSelector = '.MuiBox-root strong' - private readonly regionsDropdownRootElement = '.MuiInputBase-root .MuiSelect-select' - private readonly regionsMenuList = '#menu-cardRegion ul.MuiMenu-list li' - private readonly gridRootSelector = '.MuiGrid-root' - private readonly forwardGridButton = this.gridRootSelector + ' button.MuiButton-contained' - // Section labels - private readonly bgSelectAmountSectionText = bgLocalizationOneTimeDonation['step-labels'].amount - private readonly enSelectAmountSectionText = enLocalizationOneTimeDonation['step-labels'].amount - // TODO Add these three IDs into the component (if possible) and update the test methods - private readonly donationAmount = this.allAmountsSelector + ' #donationAmount' - private readonly feeAmount = this.allAmountsSelector + ' #feeAmount' - private readonly totalChargedAmount = this.allAmountsSelector + ' #totalChargedAmount' - // Grid navigation buttons localization - private readonly bgForwardButtonText = bgLocalizationOneTimeDonation.btns.next - private readonly enForwardButtonText = enLocalizationOneTimeDonation.btns.next - - // -> Personal profile section <- - private readonly buttonsContainer = '.MuiTabs-flexContainer button' - private readonly bgDonateAnonymouslyText = - bgLocalizationOneTimeDonation['second-step']['donate-anonymously'] - private readonly enDonateAnonymouslyText = - enLocalizationOneTimeDonation['second-step']['donate-anonymously'] - private readonly inputRootSelector = '.MuiInputBase-root' - private readonly donateAnonymouslyEmailField = - this.inputRootSelector + " input[name='personsEmail']" - // Section labels - private readonly bgPersonalProfileSectionText = - bgLocalizationOneTimeDonation['step-labels']['personal-profile'] - private readonly enPersonalProfileSectionText = - enLocalizationOneTimeDonation['step-labels']['personal-profile'] - - // -> Send a wish section <- - // Section labels - private readonly sendAWishField = this.inputRootSelector + " textarea[name='message']" - private readonly bgSendAWishSectionText = bgLocalizationOneTimeDonation['step-labels'].wish - private readonly enSendAWishSectionText = enLocalizationOneTimeDonation['step-labels'].wish - - // -> Payment <- - // Section labels - private readonly bgPaymentSectionText = bgLocalizationOneTimeDonation['step-labels'].payment - private readonly enPaymentSectionText = enLocalizationOneTimeDonation['step-labels'].payment - private readonly bgFinishButtonText = bgLocalizationOneTimeDonation.btns.end - private readonly enFinishButtonText = enLocalizationOneTimeDonation.btns.end - private readonly bgSuccessfulDonationTitle = bgLocalizationOneTimeDonation.success.title - private readonly enSuccessfulDonationTitle = enLocalizationOneTimeDonation.success.title - - async checkPageUrlByRegExp(urlRegExpAsString?: string, timeoutParam = 10000): Promise { - await this.page.waitForTimeout(1000) - await expect(this.page, 'The URL is not correct!').toHaveURL( - new RegExp(urlRegExpAsString || `^(.*?)/campaigns/donation-old/${SLUG_REGEX}`), - { - timeout: timeoutParam, - }, - ) - } - - /** - * Is "Select amount" step active - */ - async isSelectAmountStepActive(language: LanguagesEnum = LanguagesEnum.BG): Promise { - if (language === LanguagesEnum.BG) { - return this.isStepActiveByLabelText(this.bgSelectAmountSectionText) - } else if (language === LanguagesEnum.EN) { - return this.isStepActiveByLabelText(this.enSelectAmountSectionText) - } else { - throw new Error('Language not found!') - } - } - - /** - * Is "Personal profile" step active - */ - async isPersonalProfileStepActive(language: LanguagesEnum = LanguagesEnum.BG): Promise { - if (language === LanguagesEnum.BG) { - return this.isStepActiveByLabelText(this.bgPersonalProfileSectionText) - } else if (language === LanguagesEnum.EN) { - return this.isStepActiveByLabelText(this.enPersonalProfileSectionText) - } else { - throw new Error('Language not found!') - } - } - - /** - * Is "Send a wish" step active - */ - async isSendAWishStepActive(language: LanguagesEnum = LanguagesEnum.BG): Promise { - if (language === LanguagesEnum.BG) { - return this.isStepActiveByLabelText(this.bgSendAWishSectionText) - } else if (language === LanguagesEnum.EN) { - return this.isStepActiveByLabelText(this.enSendAWishSectionText) - } else { - throw new Error('Language not found!') - } - } - - /** - * Is "Payment" step active - */ - async isPaymentStepActive(language: LanguagesEnum = LanguagesEnum.BG): Promise { - if (language === LanguagesEnum.BG) { - return this.isStepActiveByLabelText(this.bgPaymentSectionText) - } else if (language === LanguagesEnum.EN) { - return this.isStepActiveByLabelText(this.enPaymentSectionText) - } else { - throw new Error('Language not found!') - } - } - - /** - * Fill in the desired amount of money for donation into the Other Amount input field - * @param {string} amountMoney - */ - async fillOtherAmountInputField(amountMoney: string): Promise { - await this.waitForElementToBePresentedBySelector(this.otherAmountInputField) - await this.setInputFieldBySelector(this.otherAmountInputField, amountMoney) - } - - /** - * Set donation region from the dropdown menu - * @param {string} desiredRegion - */ - async setDonationRegionFromTheDropdown(desiredRegion: DonationRegions): Promise { - await this.clickElement(this.regionsDropdownRootElement) - await this.clickElement(this.regionsMenuList + `[data-value=${desiredRegion}]`) - } - - /** - * Get Total charged amounts as text - */ - async getTotalChargedAmountsAsText(): Promise { - const donationAmount = this.page.locator(this.allAmountsSelector).nth(0) - return this.getTextOfElementByLocator(donationAmount) - // TODO Uncomment when the IDs are added - // return this.getTextOfElementBySelector(this.totalChargedAmount); - } - - /** - * Get Fee amounts as text - */ - async getFeeAmountsAsText(): Promise { - const donationAmount = this.page.locator(this.allAmountsSelector).nth(1) - return this.getTextOfElementByLocator(donationAmount) - // TODO Uncomment when the IDs are added - // return this.getTextOfElementBySelector(this.feeAmount); - } - - /** - * Get Donation amounts as text - */ - async getDonationAmountsAsText(): Promise { - const donationAmount = this.page.locator(this.allAmountsSelector).nth(2) - return this.getTextOfElementByLocator(donationAmount) - // TODO Uncomment when the IDs are added - // return this.getTextOfElementBySelector(this.donationAmount); - } - - /** - * Click Forward/Next button into the donation grid - * @param {LanguagesEnum} language - */ - async clickForwardButton(language: LanguagesEnum = LanguagesEnum.BG): Promise { - if (language === LanguagesEnum.BG) { - await this.clickElement(this.forwardGridButton, { hasText: this.bgForwardButtonText }) - } else if (language === LanguagesEnum.EN) { - await this.clickElement(this.forwardGridButton, { hasText: this.enForwardButtonText }) - } else { - throw new Error('Language not found!') - } - } - - /** - * Click Finish/Go to payment button into the donation grid - * @param {LanguagesEnum} language - */ - async clickFinishButton(language: LanguagesEnum = LanguagesEnum.BG): Promise { - if (language === LanguagesEnum.BG) { - await this.clickElement(this.forwardGridButton, { hasText: this.bgFinishButtonText }) - } else if (language === LanguagesEnum.EN) { - await this.clickElement(this.forwardGridButton, { hasText: this.enFinishButtonText }) - } else { - throw new Error('Language not found!') - } - } - - /** - * Click Donate Anonymously button into the donation grid (Personal Profile step) - * @param {LanguagesEnum} language - */ - async clickDonateAnonymouslyButton(language: LanguagesEnum = LanguagesEnum.BG): Promise { - if (language === LanguagesEnum.BG) { - await this.clickElement(this.buttonsContainer, { hasText: this.bgDonateAnonymouslyText }) - } else if (language === LanguagesEnum.EN) { - await this.clickElement(this.buttonsContainer, { hasText: this.enDonateAnonymouslyText }) - } else { - throw new Error('Language not found!') - } - } - - /** - * Fill Donate anonymously E-mail input field - * @param {string} emailText - */ - async fillDonateAnonymouslyEmailField(emailText: string): Promise { - await this.setInputFieldBySelector(this.donateAnonymouslyEmailField, emailText) - } - - /** - * Fill Send a wish input field - * @param {string} wishText - */ - async fillSendAWishField(wishText: string): Promise { - await this.setInputFieldBySelector(this.sendAWishField, wishText) - } - - /** - * Is "We thank you for your help and trust!" title visible - * @param {LanguagesEnum} language - the default value is BG - */ - async isSuccessfulDonationTitleVisible( - language: LanguagesEnum = LanguagesEnum.BG, - ): Promise { - return this.isH4HeadingVisible( - language, - this.bgSuccessfulDonationTitle, - this.enSuccessfulDonationTitle, - ) - } -} diff --git a/e2e/pages/web-pages/campaigns/donation.page.ts b/e2e/pages/web-pages/campaigns/donation.page.ts new file mode 100644 index 000000000..e5d8ee483 --- /dev/null +++ b/e2e/pages/web-pages/campaigns/donation.page.ts @@ -0,0 +1,142 @@ +import { Page, expect } from '@playwright/test' +import { + DonationFormAuthState, + DonationFormPaymentMethod, +} from 'components/donation-flow/helpers/types' +import { stripeFormData } from '../../../data/donation-test.data' +import { DonationRegions } from '../../../data/enums/donation-regions.enum' +import { LanguagesEnum } from '../../../data/enums/languages.enum' +import { bgLocalizationDonationFlow, enLocalizationDonationFlow } from '../../../data/localization' +import { SLUG_REGEX } from '../../../utils/helpers' +import { CampaignsPage } from './campaigns.page' +export class DonationPage extends CampaignsPage { + constructor(page: Page) { + super(page) + } + + // -> Select amount section <- + private readonly bgSelectAmountSectionText = bgLocalizationDonationFlow.step.amount.title + private readonly enSelectAmountSectionText = enLocalizationDonationFlow.step.amount.title + private readonly otherAmountInputField = ".MuiCollapse-entered input[name='otherAmount']" + + // -> Payment method section <- + private readonly regionsDropdownRootElement = '.MuiInputBase-root .MuiSelect-select' + private readonly regionsMenuList = '#menu-cardRegion ul.MuiMenu-list li' + private readonly bgBankTransferText = + bgLocalizationDonationFlow.step['payment-method'].field.method.bank + private readonly bgCardText = bgLocalizationDonationFlow.step['payment-method'].field.method.card + + // -> Authentication section <- + private readonly bgLoginText = bgLocalizationDonationFlow.step.authentication.login.label + private readonly bgRegisterText = bgLocalizationDonationFlow.step.authentication.register.label + private readonly bgNoRegitserText = + bgLocalizationDonationFlow.step.authentication.noregister.label + + // -> Summary section <- + private readonly totalAmountSelector = '[data-testid="total-amount"]' + private readonly bgSubmitButtonText = bgLocalizationDonationFlow.action.submit + + async checkPageUrlByRegExp(urlRegExpAsString?: string, timeoutParam = 10000): Promise { + await this.page.waitForTimeout(1000) + await expect(this.page, 'The URL is not correct!').toHaveURL( + new RegExp(urlRegExpAsString || `^(.*?)/campaigns/donation/${SLUG_REGEX}`), + { + timeout: timeoutParam, + }, + ) + } + + /** + * Is "Select amount" step active + */ + async isSelectAmountStepActive(language: LanguagesEnum = LanguagesEnum.BG): Promise { + if (language === LanguagesEnum.BG) { + return this.isStepActiveByLabelText(this.bgSelectAmountSectionText) + } else if (language === LanguagesEnum.EN) { + return this.isStepActiveByLabelText(this.enSelectAmountSectionText) + } else { + throw new Error('Language not found!') + } + } + + /** + * Fill in the desired amount of money for donation into the Other Amount input field + * @param {string} amountMoney + */ + async fillOtherAmountInputField(amountMoney: string): Promise { + await this.waitForElementToBePresentedBySelector(this.otherAmountInputField) + await this.setInputFieldBySelector(this.otherAmountInputField, amountMoney) + } + + /** + * Set donation region from the dropdown menu + * @param {string} desiredRegion + */ + async setDonationRegionFromTheDropdown(desiredRegion: DonationRegions): Promise { + await this.clickElement(this.regionsDropdownRootElement) + await this.clickElement(this.regionsMenuList + `[data-value=${desiredRegion}]`) + } + + /** + * Select payment method + * @param {DonationFormPaymentMethod} method + */ + async selectPaymentMethod(method: DonationFormPaymentMethod): Promise { + if (method === DonationFormPaymentMethod.BANK) { + await this.clickElement(`label:has-text(${this.bgBankTransferText})`) + } else if (method === DonationFormPaymentMethod.CARD) { + await this.clickElement(`label:has-text(${this.bgCardText})`) + } else { + throw new Error('Payment method not found!') + } + } + + /** + * Fill in the Stripe form with the test card data + */ + async fillCardForm(): Promise { + const baseLocator = this.page.locator('[data-testid="stripe-payment-form"]') + const emailField = baseLocator.locator('input[name="email"]') + const cardNumberField = baseLocator.locator('input[name="number"]') + const cardExpiryField = baseLocator.locator('input[name="expiry"]') + const cvcField = baseLocator.locator('input[name="cvc"]') + const countrySelect = baseLocator.locator('input[name="country"]') + emailField.fill(stripeFormData.email) + cardNumberField.fill(stripeFormData.cardNumber) + cardExpiryField.fill(stripeFormData.expiryDate) + cvcField.fill(stripeFormData.cvc) + countrySelect.fill(stripeFormData.country) + } + + /** + * Select authentication method + * @param {DonationFormAuthState} auth + */ + async selectAuthentication(auth: DonationFormAuthState): Promise { + if (auth === DonationFormAuthState.LOGIN) { + await this.clickElement(`span.MuiFormControlLabel-label:has-text(${this.bgLoginText})`) + } else if (auth === DonationFormAuthState.REGISTER) { + await this.clickElement(`span.MuiFormControlLabel-label:has-text(${this.bgRegisterText})`) + } else if (auth === DonationFormAuthState.NOREGISTER) { + await this.clickElement(`span.MuiFormControlLabel-label:has-text(${this.bgNoRegitserText})`) + } + } + + /** + * Set donation region from the radio cards + * @param {number} amount + */ + async checkTotalAmount(amount: number): Promise { + const totalAmount = await this.page.locator(this.totalAmountSelector).textContent() + expect(totalAmount).toBe(`${amount} лв.`) + } + + async checkPrivacyCheckbox(): Promise { + this.page.locator('[data-testid="privacy-checkbox"]').check() + } + + async submitForm(): Promise { + const button = this.page.locator(`button:has-text("${this.bgSubmitButtonText}")`) + button.click() + } +} diff --git a/e2e/tests/regression/donation-flow-old/anon-donation-custom.spec.ts b/e2e/tests/regression/donation-flow-old/anon-donation-custom.spec.ts deleted file mode 100644 index e72addab8..000000000 --- a/e2e/tests/regression/donation-flow-old/anon-donation-custom.spec.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { test, expect, Page } from '@playwright/test' -import { HeaderPage } from '../../../pages/web-pages/header.page' -import { HomePage } from '../../../pages/web-pages/home.page' -import { CampaignsPage } from '../../../pages/web-pages/campaigns/campaigns.page' -import { bgLocalizationOneTimeDonation } from '../../../data/localization' -import { DonationPage } from '../../../pages/web-pages/campaigns/donation-old.page' -import { DonationRegions } from '../../../data/enums/donation-regions.enum' -import { StripeCheckoutPage } from '../../../pages/web-pages/external/stripe-checkout.page' -import { anonDonationTestData } from '../../../data/support-page-tests.data' - -// This spec contains E2E tests related to anonymous donation flow - custom amount -// The tests are dependent, the whole describe should be runned -test.describe.skip( - 'Anonymous contributor is able to donate custom amount - BG language version', - async () => { - let page: Page - let homepage: HomePage - let headerPage: HeaderPage - let campaignsPage: CampaignsPage - let donationPage: DonationPage - let stripeCheckoutPage: StripeCheckoutPage - const testEmail = 'E2E_Test_Anon_Donation@e2etest.com' - // Localization texts - const otherAmountText = bgLocalizationOneTimeDonation['first-step'].other - const bgCardIncludeFeesText = bgLocalizationOneTimeDonation['third-step']['card-include-fees'] - - test.beforeAll(async ({ browser }) => { - page = await browser.newPage() - homepage = new HomePage(page) - headerPage = new HeaderPage(page) - campaignsPage = new CampaignsPage(page) - donationPage = new DonationPage(page) - stripeCheckoutPage = new StripeCheckoutPage(page) - // For local executions use method navigateToLocalhostHomepage(); - // await homepage.navigateToLocalhostHomepage(); - await homepage.navigateToEnvHomepage() - }) - - test.afterAll(async () => { - await page.close() - }) - - test('Particular campaign can be opened through the Campaign page', async () => { - await headerPage.clickDonateHeaderNavButton() - await campaignsPage.clickCampaignCardByIndex(0) - // We move from the common Campaigns page to the particular campain page - // check if the url is changed only based on the url pattern http://localhost:3040/campaigns/{slug-based-regexp} - expect( - await campaignsPage.checkPageUrlByRegExp(), - 'The url is not changed after clicking on the campaign card.', - ) - }) - - test('The total charge, fee tax and donation amount are visible on the Campaign page', async () => { - await campaignsPage.clickDonationSupportButton() - await donationPage.checkPageUrlByRegExp() - expect - .soft(await donationPage.isSelectAmountStepActive(), 'Select Amount step is not active.') - .toBeTruthy() - await donationPage.selectRadioButtonByLabelText([otherAmountText]) - await donationPage.fillOtherAmountInputField('7.50') - await donationPage.setDonationRegionFromTheDropdown(DonationRegions.EUROPE) - await donationPage.selectCheckboxByLabelText([bgCardIncludeFeesText]) - // Expected pattern: - // За вашия превод от {totalChargedAmountText} лв., таксата на Stripe ще е {feeAmountText} лв., а кампанията ще получи {donationAmountText} лв. - const totalChargedAmountText = await donationPage.getTotalChargedAmountsAsText() - const feeAmountText = await donationPage.getFeeAmountsAsText() - const donationAmountText = await donationPage.getDonationAmountsAsText() - expect.soft(totalChargedAmountText).toEqual('8,10 лв.') - expect.soft(feeAmountText).toEqual('0,60 лв.') - expect(donationAmountText).toEqual('7,50 лв.') - }) - - test('The total charge, fee tax and donation amount are recalculated correctly when the donation amount is changed', async () => { - await donationPage.fillOtherAmountInputField('12.90') - // Expected pattern: - // За вашия превод от {totalChargedAmountText} лв., таксата на Stripe ще е {feeAmountText} лв., а кампанията ще получи {donationAmountText} лв. - const totalChargedAmountText = await donationPage.getTotalChargedAmountsAsText() - const feeAmountText = await donationPage.getFeeAmountsAsText() - const donationAmountText = await donationPage.getDonationAmountsAsText() - expect.soft(totalChargedAmountText).toEqual('13,56 лв.') - expect.soft(feeAmountText).toEqual('0,66 лв.') - expect(donationAmountText).toEqual('12,90 лв.') - }) - - test('The user is able to fill in e-mail for anonymous donation', async () => { - await donationPage.clickForwardButton() - expect - .soft( - await donationPage.isPersonalProfileStepActive(), - 'Personal Profile step is not active.', - ) - .toBeTruthy() - await donationPage.clickDonateAnonymouslyButton() - await donationPage.fillDonateAnonymouslyEmailField(testEmail) - await donationPage.clickForwardButton() - expect( - await donationPage.isSendAWishStepActive(), - 'Send a wish step is not active.', - ).toBeTruthy() - }) - - test('After sending a wish, the user is redirected to Stripe', async () => { - await donationPage.fillSendAWishField('E2E test - anonymous donation.') - await donationPage.clickFinishButton() - const stripeTotalAmount = await stripeCheckoutPage.getTotalAmountText() - const actualStripeEmail = await stripeCheckoutPage.getReadonlyEmailText() - expect - .soft(stripeTotalAmount, 'The Stripe total donation amount is not correct.') - .toContain('13.56') - expect(actualStripeEmail, 'The user e-mail is not sent correctly to Stripe.').toEqual( - testEmail, - ) - }) - - test('The user is able to pay via Stripe', async () => { - await stripeCheckoutPage.fillPaymentForm([ - anonDonationTestData.cardNumber, - anonDonationTestData.cardExpDate, - anonDonationTestData.cardCvc, - anonDonationTestData.billingName, - anonDonationTestData.country, - ]) - - expect - .soft( - await donationPage.isSuccessfulDonationTitleVisible(), - "'We thank you for your help and trust!' title is not visible.", - ) - .toBeTruthy() - expect(await donationPage.isPaymentStepActive(), 'Payment step is not active.').toBeTruthy() - }) - }, -) diff --git a/e2e/tests/regression/donation-flow-old/anon-donation-fixed.spec.ts b/e2e/tests/regression/donation-flow-old/anon-donation-fixed.spec.ts deleted file mode 100644 index 701267f0b..000000000 --- a/e2e/tests/regression/donation-flow-old/anon-donation-fixed.spec.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { test, expect, Page } from '@playwright/test' -import { HeaderPage } from '../../../pages/web-pages/header.page' -import { HomePage } from '../../../pages/web-pages/home.page' -import { CampaignsPage } from '../../../pages/web-pages/campaigns/campaigns.page' -import { enLocalizationOneTimeDonation } from '../../../data/localization' -import { DonationPage } from '../../../pages/web-pages/campaigns/donation-old.page' -import { DonationRegions } from '../../../data/enums/donation-regions.enum' -import { StripeCheckoutPage } from '../../../pages/web-pages/external/stripe-checkout.page' -import { anonDonationTestData } from '../../../data/support-page-tests.data' -import { LanguagesEnum } from '../../../data/enums/languages.enum' - -// This spec contains E2E tests related to anonymous donation flow - fixed amount -// The tests are dependent, the whole describe should be runned -test.describe.skip( - 'Anonymous contributor is able to donate fixed amount - EN language version', - async () => { - let page: Page - let homepage: HomePage - let headerPage: HeaderPage - let campaignsPage: CampaignsPage - let donationPage: DonationPage - let stripeCheckoutPage: StripeCheckoutPage - const testEmail = 'E2E_Test_Anon_Donation@e2etest.com' - // Localization texts - const enCardIncludeFeesText = enLocalizationOneTimeDonation['third-step']['card-include-fees'] - - test.beforeAll(async ({ browser, baseURL }) => { - page = await browser.newPage() - homepage = new HomePage(page) - headerPage = new HeaderPage(page) - campaignsPage = new CampaignsPage(page) - donationPage = new DonationPage(page) - stripeCheckoutPage = new StripeCheckoutPage(page) - // For local executions use method navigateToLocalhostHomepage(); - // await homepage.navigateToLocalhostHomepage(); - await homepage.navigateToEnvHomepage() - await headerPage.changeanguageHeaderButtonToBe(LanguagesEnum.EN) - }) - - test.afterAll(async () => { - await page.close() - }) - - test('Particular campaign can be opened through the Campaign page', async () => { - await headerPage.clickDonateHeaderNavButton() - await campaignsPage.clickCampaignCardByIndex(0) - // We move from the common Campaigns page to the particular campain page - // check if the url is changed only based on the url pattern http://localhost:3040/campaigns/{slug-based-regexp} - // expect to not break - - expect( - await campaignsPage.checkPageUrlByRegExp(), - 'The url is not changed after clicking on the campaign card.', - ) - }) - - test('The total charge, fee tax and donation amount are visible on the Campaign page', async () => { - await campaignsPage.clickDonationSupportButton() - await donationPage.checkPageUrlByRegExp() - expect - .soft( - await donationPage.isSelectAmountStepActive(LanguagesEnum.EN), - 'Select Amount step is not active.', - ) - .toBeTruthy() - await donationPage.selectRadioButtonByLabelText(['10']) - await donationPage.setDonationRegionFromTheDropdown(DonationRegions.EUROPE) - await donationPage.selectCheckboxByLabelText([enCardIncludeFeesText]) - // Expected pattern: - // For your donation of {donationAmountText}, the fee from Stripe will be {feeAmountText}, and the total charged amount will be {totalChargedAmountText}. - const donationAmountText = await donationPage.getTotalChargedAmountsAsText() - const feeAmountText = await donationPage.getFeeAmountsAsText() - const totalChargedAmountText = await donationPage.getDonationAmountsAsText() - expect.soft(donationAmountText).toMatch('10.00') - expect.soft(feeAmountText).toMatch('0.63') - expect(totalChargedAmountText).toMatch('10.63') - }) - - test('The total charge, fee tax and donation amount are recalculated correctly when the donation amount is changed', async () => { - await donationPage.selectRadioButtonByLabelText(['20']) - // Expected pattern: - // For your donation of {donationAmountText}, the fee from Stripe will be {feeAmountText}, and the total charged amount will be {totalChargedAmountText}. - const donationAmountText = await donationPage.getTotalChargedAmountsAsText() - const feeAmountText = await donationPage.getFeeAmountsAsText() - const totalChargedAmountText = await donationPage.getDonationAmountsAsText() - expect.soft(donationAmountText).toMatch('20.00') - expect.soft(feeAmountText).toMatch('0.75') - expect(totalChargedAmountText).toMatch('20.75') - }) - - test('The user is able to fill in e-mail for anonymous donation', async () => { - await donationPage.clickForwardButton(LanguagesEnum.EN) - expect - .soft( - await donationPage.isPersonalProfileStepActive(LanguagesEnum.EN), - 'Personal Profile step is not active.', - ) - .toBeTruthy() - await donationPage.clickDonateAnonymouslyButton(LanguagesEnum.EN) - await donationPage.fillDonateAnonymouslyEmailField(testEmail) - await donationPage.clickForwardButton(LanguagesEnum.EN) - expect( - await donationPage.isSendAWishStepActive(LanguagesEnum.EN), - 'Send a wish step is not active.', - ).toBeTruthy() - }) - - test('After sending a wish, the user is redirected to Stripe', async () => { - await donationPage.fillSendAWishField('E2E test - anonymous donation.') - await donationPage.clickFinishButton(LanguagesEnum.EN) - const stripeTotalAmount = await stripeCheckoutPage.getTotalAmountText() - const actualStripeEmail = await stripeCheckoutPage.getReadonlyEmailText() - expect - .soft(stripeTotalAmount, 'The Stripe total donation amount is not correct.') - .toContain('20.75') - expect(actualStripeEmail, 'The user e-mail is not sent correctly to Stripe.').toEqual( - testEmail, - ) - }) - - test('The user is able to pay via Stripe', async () => { - await stripeCheckoutPage.fillPaymentForm([ - anonDonationTestData.cardNumber, - anonDonationTestData.cardExpDate, - anonDonationTestData.cardCvc, - anonDonationTestData.billingName, - anonDonationTestData.country, - ]) - // Now we're redirected to the Donation page - expect - .soft( - await donationPage.isSuccessfulDonationTitleVisible(LanguagesEnum.EN), - "'We thank you for your help and trust!' title is not visible.", - ) - .toBeTruthy() - expect( - await donationPage.isPaymentStepActive(LanguagesEnum.EN), - 'Payment step is not active.', - ).toBeTruthy() - }) - }, -) diff --git a/e2e/tests/regression/donation-flow/anon-donation-custom.spec.ts b/e2e/tests/regression/donation-flow/anon-donation-custom.spec.ts new file mode 100644 index 000000000..9e2cb7a52 --- /dev/null +++ b/e2e/tests/regression/donation-flow/anon-donation-custom.spec.ts @@ -0,0 +1,73 @@ +import { test, expect, Page } from '@playwright/test' +import { HeaderPage } from '../../../pages/web-pages/header.page' +import { HomePage } from '../../../pages/web-pages/home.page' +import { CampaignsPage } from '../../../pages/web-pages/campaigns/campaigns.page' +import { DonationPage } from '../../../pages/web-pages/campaigns/donation.page' +import { DonationRegions } from '../../../data/enums/donation-regions.enum' +import { bgLocalizationDonationFlow } from '../../../data/localization' +import { DonationFormPaymentMethod } from 'components/donation-flow/helpers/types' + +// This spec contains E2E tests related to anonymous donation flow - custom amount +// The tests are dependent, the whole describe should be runned +test.describe.skip( + 'Anonymous contributor is able to donate custom amount - BG language version', + async () => { + let page: Page + let homepage: HomePage + let headerPage: HeaderPage + let campaignsPage: CampaignsPage + let donationPage: DonationPage + // Localization texts + const otherAmountText = bgLocalizationDonationFlow.step.amount.field['other-amount'].label + const bgCardIncludeFeesText = + bgLocalizationDonationFlow.step['payment-method'].field['include-fees'].label + + test.beforeAll(async ({ browser }) => { + page = await browser.newPage() + homepage = new HomePage(page) + headerPage = new HeaderPage(page) + campaignsPage = new CampaignsPage(page) + donationPage = new DonationPage(page) + // For local executions use method navigateToLocalhostHomepage(); + // await homepage.navigateToLocalhostHomepage(); + await homepage.navigateToEnvHomepage() + }) + + test.afterAll(async () => { + await page.close() + }) + + test('Particular campaign can be opened through the Campaign page', async () => { + await headerPage.clickDonateHeaderNavButton() + await campaignsPage.clickCampaignCardByIndex(0) + // We move from the common Campaigns page to the particular campain page + // check if the url is changed only based on the url pattern http://localhost:3040/campaigns/{slug-based-regexp} + expect( + await campaignsPage.checkPageUrlByRegExp(), + 'The url is not changed after clicking on the campaign card.', + ) + }) + + test('The total charge, fee tax and donation amount are visible on the Campaign page', async () => { + await campaignsPage.clickDonationSupportButton() + await donationPage.checkPageUrlByRegExp() + expect + .soft(await donationPage.isSelectAmountStepActive(), 'Select Amount step is not active.') + .toBeTruthy() + await donationPage.selectRadioButtonByLabelText([otherAmountText]) + await donationPage.fillOtherAmountInputField('7.50') + await donationPage.selectPaymentMethod(DonationFormPaymentMethod.CARD) + await donationPage.setDonationRegionFromTheDropdown(DonationRegions.EUROPE) + await donationPage.selectCheckboxByLabelText([bgCardIncludeFeesText]) + }) + + test('The total charge, fee tax and donation amount are recalculated correctly when the donation amount is changed', async () => { + await donationPage.fillOtherAmountInputField('12.90') + await donationPage.checkTotalAmount(12.9) + }) + + test('The user is able to fill in e-mail for anonymous donation', async () => { + return + }) + }, +) diff --git a/e2e/tests/regression/donation-flow/anon-donation-fixed.spec.ts b/e2e/tests/regression/donation-flow/anon-donation-fixed.spec.ts new file mode 100644 index 000000000..3fbfffbef --- /dev/null +++ b/e2e/tests/regression/donation-flow/anon-donation-fixed.spec.ts @@ -0,0 +1,71 @@ +import { test, expect, Page } from '@playwright/test' +import { HeaderPage } from '../../../pages/web-pages/header.page' +import { HomePage } from '../../../pages/web-pages/home.page' +import { CampaignsPage } from '../../../pages/web-pages/campaigns/campaigns.page' +import { enLocalizationDonationFlow } from '../../../data/localization' +import { DonationPage } from '../../../pages/web-pages/campaigns/donation.page' +import { DonationRegions } from '../../../data/enums/donation-regions.enum' +import { LanguagesEnum } from '../../../data/enums/languages.enum' + +// This spec contains E2E tests related to anonymous donation flow - fixed amount +// The tests are dependent, the whole describe should be runned +test.describe.skip( + 'Anonymous contributor is able to donate fixed amount - EN language version', + async () => { + let page: Page + let homepage: HomePage + let headerPage: HeaderPage + let campaignsPage: CampaignsPage + let donationPage: DonationPage + // Localization texts + const enCardIncludeFeesText = + enLocalizationDonationFlow.step['payment-method'].alert['card-fee'] + + test.beforeAll(async ({ browser }) => { + page = await browser.newPage() + homepage = new HomePage(page) + headerPage = new HeaderPage(page) + campaignsPage = new CampaignsPage(page) + donationPage = new DonationPage(page) + // For local executions use method navigateToLocalhostHomepage(); + // await homepage.navigateToLocalhostHomepage(); + await homepage.navigateToEnvHomepage() + await headerPage.changeanguageHeaderButtonToBe(LanguagesEnum.EN) + }) + + test.afterAll(async () => { + await page.close() + }) + + test('Particular campaign can be opened through the Campaign page', async () => { + await headerPage.clickDonateHeaderNavButton() + await campaignsPage.clickCampaignCardByIndex(0) + // We move from the common Campaigns page to the particular campain page + // check if the url is changed only based on the url pattern http://localhost:3040/campaigns/{slug-based-regexp} + // expect to not break + + expect( + await campaignsPage.checkPageUrlByRegExp(), + 'The url is not changed after clicking on the campaign card.', + ) + }) + + test('The total charge, fee tax and donation amount are visible on the Campaign page', async () => { + await campaignsPage.clickDonationSupportButton() + await donationPage.checkPageUrlByRegExp() + expect + .soft( + await donationPage.isSelectAmountStepActive(LanguagesEnum.EN), + 'Select Amount step is not active.', + ) + .toBeTruthy() + await donationPage.selectRadioButtonByLabelText(['10']) + await donationPage.setDonationRegionFromTheDropdown(DonationRegions.EUROPE) + await donationPage.selectCheckboxByLabelText([enCardIncludeFeesText]) + }) + + test('The total charge, fee tax and donation amount are recalculated correctly when the donation amount is changed', async () => { + await donationPage.selectRadioButtonByLabelText(['20']) + }) + }, +) From ffbba34773bce7e3473028d85a1b212d030a78e3 Mon Sep 17 00:00:00 2001 From: "dimitar.nizamov" Date: Sun, 12 Feb 2023 15:41:07 +0100 Subject: [PATCH 06/12] 1317 - add tests for the form submission with custom value --- .../web-pages/campaigns/donation.page.ts | 78 ++++++++++++++----- .../anon-donation-custom.spec.ts | 24 ++++-- 2 files changed, 76 insertions(+), 26 deletions(-) diff --git a/e2e/pages/web-pages/campaigns/donation.page.ts b/e2e/pages/web-pages/campaigns/donation.page.ts index e5d8ee483..7231817f7 100644 --- a/e2e/pages/web-pages/campaigns/donation.page.ts +++ b/e2e/pages/web-pages/campaigns/donation.page.ts @@ -6,7 +6,11 @@ import { import { stripeFormData } from '../../../data/donation-test.data' import { DonationRegions } from '../../../data/enums/donation-regions.enum' import { LanguagesEnum } from '../../../data/enums/languages.enum' -import { bgLocalizationDonationFlow, enLocalizationDonationFlow } from '../../../data/localization' +import { + bgLocalizationDonationFlow, + bgLocalizationValidation, + enLocalizationDonationFlow, +} from '../../../data/localization' import { SLUG_REGEX } from '../../../utils/helpers' import { CampaignsPage } from './campaigns.page' export class DonationPage extends CampaignsPage { @@ -35,6 +39,8 @@ export class DonationPage extends CampaignsPage { // -> Summary section <- private readonly totalAmountSelector = '[data-testid="total-amount"]' private readonly bgSubmitButtonText = bgLocalizationDonationFlow.action.submit + private readonly privacyCheckboxText = + bgLocalizationValidation['informed-agree-with'] + ' ' + bgLocalizationValidation.gdpr async checkPageUrlByRegExp(urlRegExpAsString?: string, timeoutParam = 10000): Promise { await this.page.waitForTimeout(1000) @@ -83,9 +89,17 @@ export class DonationPage extends CampaignsPage { */ async selectPaymentMethod(method: DonationFormPaymentMethod): Promise { if (method === DonationFormPaymentMethod.BANK) { - await this.clickElement(`label:has-text(${this.bgBankTransferText})`) + await this.page + .getByText(this.bgBankTransferText, { + exact: true, + }) + .click() } else if (method === DonationFormPaymentMethod.CARD) { - await this.clickElement(`label:has-text(${this.bgCardText})`) + await this.page + .getByText(this.bgCardText, { + exact: true, + }) + .click() } else { throw new Error('Payment method not found!') } @@ -95,17 +109,32 @@ export class DonationPage extends CampaignsPage { * Fill in the Stripe form with the test card data */ async fillCardForm(): Promise { - const baseLocator = this.page.locator('[data-testid="stripe-payment-form"]') - const emailField = baseLocator.locator('input[name="email"]') - const cardNumberField = baseLocator.locator('input[name="number"]') - const cardExpiryField = baseLocator.locator('input[name="expiry"]') - const cvcField = baseLocator.locator('input[name="cvc"]') - const countrySelect = baseLocator.locator('input[name="country"]') - emailField.fill(stripeFormData.email) - cardNumberField.fill(stripeFormData.cardNumber) - cardExpiryField.fill(stripeFormData.expiryDate) - cvcField.fill(stripeFormData.cvc) - countrySelect.fill(stripeFormData.country) + const baseEmailLocator = this.page + .locator('[data-testid="stripe-payment-form"]') + .frameLocator('iframe') + .first() + const baseCardPaymentLocator = this.page + .locator('[data-testid="stripe-payment-form"]') + .frameLocator('iframe') + .last() + const emailField = baseEmailLocator.locator('input[name="email"]') + const cardNumberField = baseCardPaymentLocator.locator('input[name="number"]') + const cardExpiryField = baseCardPaymentLocator.locator('input[name="expiry"]') + const cvcField = baseCardPaymentLocator.locator('input[name="cvc"]') + const countrySelect = baseCardPaymentLocator.locator('select[name="country"]') + await emailField.fill(stripeFormData.email) + await cardNumberField.fill(stripeFormData.cardNumber) + await cardExpiryField.fill(stripeFormData.expiryDate) + await cvcField.fill(stripeFormData.cvc) + await countrySelect.selectOption(stripeFormData.country) + } + + /** + * Fill in email field on a NOREGISTER authentication step + */ + async fillEmailField(): Promise { + const emailField = this.page.locator('.MuiInputBase-root>input[name="email"]') + await emailField.fill(stripeFormData.email) } /** @@ -113,12 +142,23 @@ export class DonationPage extends CampaignsPage { * @param {DonationFormAuthState} auth */ async selectAuthentication(auth: DonationFormAuthState): Promise { + const baseLocator = this.page.locator('span.MuiFormControlLabel-label') if (auth === DonationFormAuthState.LOGIN) { - await this.clickElement(`span.MuiFormControlLabel-label:has-text(${this.bgLoginText})`) + await baseLocator + .getByText(this.bgLoginText, { + exact: true, + }) + .click() } else if (auth === DonationFormAuthState.REGISTER) { - await this.clickElement(`span.MuiFormControlLabel-label:has-text(${this.bgRegisterText})`) + await baseLocator.getByText(this.bgRegisterText, { + exact: true, + }) } else if (auth === DonationFormAuthState.NOREGISTER) { - await this.clickElement(`span.MuiFormControlLabel-label:has-text(${this.bgNoRegitserText})`) + await baseLocator + .getByText(this.bgNoRegitserText, { + exact: true, + }) + .click() } } @@ -128,11 +168,11 @@ export class DonationPage extends CampaignsPage { */ async checkTotalAmount(amount: number): Promise { const totalAmount = await this.page.locator(this.totalAmountSelector).textContent() - expect(totalAmount).toBe(`${amount} лв.`) + expect(totalAmount).toBe(`${amount.toLocaleString('BG')} лв.`) } async checkPrivacyCheckbox(): Promise { - this.page.locator('[data-testid="privacy-checkbox"]').check() + await this.selectCheckboxByLabelText([this.privacyCheckboxText]) } async submitForm(): Promise { diff --git a/e2e/tests/regression/donation-flow/anon-donation-custom.spec.ts b/e2e/tests/regression/donation-flow/anon-donation-custom.spec.ts index 9e2cb7a52..de57fb013 100644 --- a/e2e/tests/regression/donation-flow/anon-donation-custom.spec.ts +++ b/e2e/tests/regression/donation-flow/anon-donation-custom.spec.ts @@ -5,11 +5,14 @@ import { CampaignsPage } from '../../../pages/web-pages/campaigns/campaigns.page import { DonationPage } from '../../../pages/web-pages/campaigns/donation.page' import { DonationRegions } from '../../../data/enums/donation-regions.enum' import { bgLocalizationDonationFlow } from '../../../data/localization' -import { DonationFormPaymentMethod } from 'components/donation-flow/helpers/types' +import { + DonationFormAuthState, + DonationFormPaymentMethod, +} from 'components/donation-flow/helpers/types' // This spec contains E2E tests related to anonymous donation flow - custom amount // The tests are dependent, the whole describe should be runned -test.describe.skip( +test.describe.serial( 'Anonymous contributor is able to donate custom amount - BG language version', async () => { let page: Page @@ -51,9 +54,6 @@ test.describe.skip( test('The total charge, fee tax and donation amount are visible on the Campaign page', async () => { await campaignsPage.clickDonationSupportButton() await donationPage.checkPageUrlByRegExp() - expect - .soft(await donationPage.isSelectAmountStepActive(), 'Select Amount step is not active.') - .toBeTruthy() await donationPage.selectRadioButtonByLabelText([otherAmountText]) await donationPage.fillOtherAmountInputField('7.50') await donationPage.selectPaymentMethod(DonationFormPaymentMethod.CARD) @@ -63,11 +63,21 @@ test.describe.skip( test('The total charge, fee tax and donation amount are recalculated correctly when the donation amount is changed', async () => { await donationPage.fillOtherAmountInputField('12.90') - await donationPage.checkTotalAmount(12.9) + await donationPage.checkTotalAmount(13.56) + }) + + test('Fill in the stripe card form', async () => { + await donationPage.fillCardForm() }) test('The user is able to fill in e-mail for anonymous donation', async () => { - return + await donationPage.selectAuthentication(DonationFormAuthState.NOREGISTER) + await donationPage.fillEmailField() + }) + + test('The user can submit the form', async () => { + await donationPage.checkPrivacyCheckbox() + await donationPage.submitForm() }) }, ) From 00e3d5278a3566eb632648c1fa2b6f5705bab68c Mon Sep 17 00:00:00 2001 From: "dimitar.nizamov" Date: Sun, 12 Feb 2023 18:27:30 +0100 Subject: [PATCH 07/12] 1317 - add tests for the status page --- .../donation/donation-status.page.ts | 41 +++++++++++++++++++ .../{campaigns => donation}/donation.page.ts | 2 +- .../anon-donation-custom.spec.ts | 11 ++++- .../donation-flow/anon-donation-fixed.spec.ts | 2 +- e2e/utils/helpers.ts | 2 +- 5 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 e2e/pages/web-pages/donation/donation-status.page.ts rename e2e/pages/web-pages/{campaigns => donation}/donation.page.ts (99%) diff --git a/e2e/pages/web-pages/donation/donation-status.page.ts b/e2e/pages/web-pages/donation/donation-status.page.ts new file mode 100644 index 000000000..f1c1f6c04 --- /dev/null +++ b/e2e/pages/web-pages/donation/donation-status.page.ts @@ -0,0 +1,41 @@ +import { Page, expect } from '@playwright/test' +import { LanguagesEnum } from '../../../data/enums/languages.enum' +import { bgLocalizationDonationFlow, enLocalizationDonationFlow } from '../../../data/localization' +import { SLUG_REGEX } from '../../../utils/helpers' +import { CampaignsPage } from '../campaigns/campaigns.page' +export class DonationStatusPage extends CampaignsPage { + constructor(page: Page) { + super(page) + } + + // -> Status titles <- + private readonly bgSuccessTitle = bgLocalizationDonationFlow.status.success.title + private readonly enSuccessTitle = enLocalizationDonationFlow.status.success.title + + // -> Wish form <- + private readonly wishPlaceholderText = bgLocalizationDonationFlow.status.success.wish.write + private readonly wishSendText = bgLocalizationDonationFlow.status.success.wish.send + + async checkPageUrlByRegExp(urlRegExpAsString?: string, timeoutParam = 10000): Promise { + await expect(this.page, 'The URL is not correct!').toHaveURL( + new RegExp(urlRegExpAsString || `^(.*?)/campaigns/donation/${SLUG_REGEX}/status?.+$`), + { + timeout: timeoutParam, + }, + ) + } + + async isSucceededStatusTitleDisplayed(): Promise { + return this.isH4HeadingVisible(LanguagesEnum.BG, this.bgSuccessTitle, this.enSuccessTitle) + } + + async submitWishForm(): Promise { + const wishAreaLocator = await this.page.locator('textarea[name="wish"]') + await this.waitForElementToBeReadyByLocator(wishAreaLocator) + await wishAreaLocator.fill('e2e_test_wish') + const buttonLocator = await this.page.locator('button[type="submit"]', { + hasText: this.wishSendText, + }) + await this.clickElementByLocator(buttonLocator) + } +} diff --git a/e2e/pages/web-pages/campaigns/donation.page.ts b/e2e/pages/web-pages/donation/donation.page.ts similarity index 99% rename from e2e/pages/web-pages/campaigns/donation.page.ts rename to e2e/pages/web-pages/donation/donation.page.ts index 7231817f7..4f2a12b9e 100644 --- a/e2e/pages/web-pages/campaigns/donation.page.ts +++ b/e2e/pages/web-pages/donation/donation.page.ts @@ -12,7 +12,7 @@ import { enLocalizationDonationFlow, } from '../../../data/localization' import { SLUG_REGEX } from '../../../utils/helpers' -import { CampaignsPage } from './campaigns.page' +import { CampaignsPage } from '../campaigns/campaigns.page' export class DonationPage extends CampaignsPage { constructor(page: Page) { super(page) diff --git a/e2e/tests/regression/donation-flow/anon-donation-custom.spec.ts b/e2e/tests/regression/donation-flow/anon-donation-custom.spec.ts index de57fb013..f41d7e078 100644 --- a/e2e/tests/regression/donation-flow/anon-donation-custom.spec.ts +++ b/e2e/tests/regression/donation-flow/anon-donation-custom.spec.ts @@ -2,13 +2,14 @@ import { test, expect, Page } from '@playwright/test' import { HeaderPage } from '../../../pages/web-pages/header.page' import { HomePage } from '../../../pages/web-pages/home.page' import { CampaignsPage } from '../../../pages/web-pages/campaigns/campaigns.page' -import { DonationPage } from '../../../pages/web-pages/campaigns/donation.page' +import { DonationPage } from '../../../pages/web-pages/donation/donation.page' import { DonationRegions } from '../../../data/enums/donation-regions.enum' import { bgLocalizationDonationFlow } from '../../../data/localization' import { DonationFormAuthState, DonationFormPaymentMethod, } from 'components/donation-flow/helpers/types' +import { DonationStatusPage } from '../../../pages/web-pages/donation/donation-status.page' // This spec contains E2E tests related to anonymous donation flow - custom amount // The tests are dependent, the whole describe should be runned @@ -20,6 +21,7 @@ test.describe.serial( let headerPage: HeaderPage let campaignsPage: CampaignsPage let donationPage: DonationPage + let statusPage: DonationStatusPage // Localization texts const otherAmountText = bgLocalizationDonationFlow.step.amount.field['other-amount'].label const bgCardIncludeFeesText = @@ -31,6 +33,7 @@ test.describe.serial( headerPage = new HeaderPage(page) campaignsPage = new CampaignsPage(page) donationPage = new DonationPage(page) + statusPage = new DonationStatusPage(page) // For local executions use method navigateToLocalhostHomepage(); // await homepage.navigateToLocalhostHomepage(); await homepage.navigateToEnvHomepage() @@ -78,6 +81,12 @@ test.describe.serial( test('The user can submit the form', async () => { await donationPage.checkPrivacyCheckbox() await donationPage.submitForm() + await page.waitForEvent('domcontentloaded') + }) + + test('The user is redirected to succes page', async () => { + await statusPage.checkPageUrlByRegExp() + expect(await statusPage.isSucceededStatusTitleDisplayed()).toBe(true) }) }, ) diff --git a/e2e/tests/regression/donation-flow/anon-donation-fixed.spec.ts b/e2e/tests/regression/donation-flow/anon-donation-fixed.spec.ts index 3fbfffbef..8f14eb19b 100644 --- a/e2e/tests/regression/donation-flow/anon-donation-fixed.spec.ts +++ b/e2e/tests/regression/donation-flow/anon-donation-fixed.spec.ts @@ -3,7 +3,7 @@ import { HeaderPage } from '../../../pages/web-pages/header.page' import { HomePage } from '../../../pages/web-pages/home.page' import { CampaignsPage } from '../../../pages/web-pages/campaigns/campaigns.page' import { enLocalizationDonationFlow } from '../../../data/localization' -import { DonationPage } from '../../../pages/web-pages/campaigns/donation.page' +import { DonationPage } from '../../../pages/web-pages/donation/donation.page' import { DonationRegions } from '../../../data/enums/donation-regions.enum' import { LanguagesEnum } from '../../../data/enums/languages.enum' diff --git a/e2e/utils/helpers.ts b/e2e/utils/helpers.ts index 285e098a0..24ceda9d9 100644 --- a/e2e/utils/helpers.ts +++ b/e2e/utils/helpers.ts @@ -27,4 +27,4 @@ export const expectCopied = async (page: Page, textToCheck: string) => { * - (?:-[a-z0-9]+)* matches the characters - and a-z0-9 between one and unlimited times, as many times as possible, giving back as needed (greedy) and does not remember the match * - $ asserts position at the end of the string */ -export const SLUG_REGEX = `[a-z0-9]+(?:-[a-z0-9]+)*$` +export const SLUG_REGEX = `[a-z0-9]+(?:-[a-z0-9]+)*` From d77e8418281d84022ff4255b1ea645df2152ae0e Mon Sep 17 00:00:00 2001 From: "dimitar.nizamov" Date: Sun, 12 Feb 2023 18:28:06 +0100 Subject: [PATCH 08/12] 1317 - add data-testids to the needed components --- src/components/donation-flow/DonationFlowStatusPage.tsx | 4 ++-- src/components/donation-flow/alerts/PaymentSummaryAlert.tsx | 4 +++- .../steps/payment-method/PaymentDetailsStripeForm.tsx | 2 +- .../donation-flow/steps/payment-method/PaymentMethod.tsx | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/components/donation-flow/DonationFlowStatusPage.tsx b/src/components/donation-flow/DonationFlowStatusPage.tsx index 4ca7a9726..98ac482b5 100644 --- a/src/components/donation-flow/DonationFlowStatusPage.tsx +++ b/src/components/donation-flow/DonationFlowStatusPage.tsx @@ -82,7 +82,7 @@ export default function DonationFlowStatusPage({ slug }: { slug: string }) { if (bank_payment === 'true') { // If we are redirected on that page means that the payment is a bank payment and we can clear the form state sessionStorage.removeItem('donation-form') - setStatus(DonationFormPaymentStatus.REQUIRES_PAYMENT) + setStatus(DonationFormPaymentStatus.SUCCEEDED) return } if (!stripe || !payment_intent_client_secret) { @@ -113,7 +113,7 @@ export default function DonationFlowStatusPage({ slug }: { slug: string }) { const Success = () => ( - + {session.data?.user ? `${session.data?.user?.given_name} ${session.data.user.family_name}, ${t( 'status.success.title-logged', diff --git a/src/components/donation-flow/alerts/PaymentSummaryAlert.tsx b/src/components/donation-flow/alerts/PaymentSummaryAlert.tsx index 192fa5eb5..c37540406 100644 --- a/src/components/donation-flow/alerts/PaymentSummaryAlert.tsx +++ b/src/components/donation-flow/alerts/PaymentSummaryAlert.tsx @@ -69,7 +69,9 @@ function PaymentSummaryAlert({ icon={false}> {t('step.summary.total')}: - {moneyPublicDecimals2(donationAmount)} + + {moneyPublicDecimals2(donationAmount)} + diff --git a/src/components/donation-flow/steps/payment-method/PaymentDetailsStripeForm.tsx b/src/components/donation-flow/steps/payment-method/PaymentDetailsStripeForm.tsx index 836b22ce5..d2a477128 100644 --- a/src/components/donation-flow/steps/payment-method/PaymentDetailsStripeForm.tsx +++ b/src/components/donation-flow/steps/payment-method/PaymentDetailsStripeForm.tsx @@ -15,7 +15,7 @@ export default function PaymentDetailsStripeForm({ setEmail(session?.user?.email || '') }, [session]) return ( - + - + From c438b991d1951fa8c44b7b94ff171d721c20eac9 Mon Sep 17 00:00:00 2001 From: "dimitar.nizamov" Date: Sun, 12 Feb 2023 18:39:13 +0100 Subject: [PATCH 09/12] 1317 - add fail tests --- e2e/data/donation-test.data.ts | 18 +++- e2e/pages/web-pages/donation/donation.page.ts | 30 +++++-- .../anon-donation-custom.spec.ts | 4 +- .../donation-flow/anon-donation-fixed.spec.ts | 55 ++++++++---- .../donation-flow/donation-fail.spec.ts | 87 +++++++++++++++++++ 5 files changed, 167 insertions(+), 27 deletions(-) create mode 100644 e2e/tests/regression/donation-flow/donation-fail.spec.ts diff --git a/e2e/data/donation-test.data.ts b/e2e/data/donation-test.data.ts index 0ab003ef0..65e306565 100644 --- a/e2e/data/donation-test.data.ts +++ b/e2e/data/donation-test.data.ts @@ -1,7 +1,23 @@ -export const stripeFormData = { +export const stripeSuccessFormData = { cardNumber: '4242 4242 4242 4242', email: 'e2e_test_mail@test.bg', expiryDate: '04 / 42', cvc: '424', country: 'BG', } + +export const stripeErrorNoBalanceFormData = { + cardNumber: '4000 0000 0000 9995', + email: 'e2e_test_mail@test.bg', + expiryDate: '04 / 42', + cvc: '424', + country: 'BG', +} + +export const stripeAuthenticationRequiredFormData = { + cardNumber: '4000 0027 6000 3184', + email: 'e2e_test_mail@test.bg', + expiryDate: '04 / 42', + cvc: '424', + country: 'BG', +} diff --git a/e2e/pages/web-pages/donation/donation.page.ts b/e2e/pages/web-pages/donation/donation.page.ts index 4f2a12b9e..bf2d02e9f 100644 --- a/e2e/pages/web-pages/donation/donation.page.ts +++ b/e2e/pages/web-pages/donation/donation.page.ts @@ -3,7 +3,10 @@ import { DonationFormAuthState, DonationFormPaymentMethod, } from 'components/donation-flow/helpers/types' -import { stripeFormData } from '../../../data/donation-test.data' +import { + stripeSuccessFormData, + stripeErrorNoBalanceFormData, +} from '../../../data/donation-test.data' import { DonationRegions } from '../../../data/enums/donation-regions.enum' import { LanguagesEnum } from '../../../data/enums/languages.enum' import { @@ -41,6 +44,7 @@ export class DonationPage extends CampaignsPage { private readonly bgSubmitButtonText = bgLocalizationDonationFlow.action.submit private readonly privacyCheckboxText = bgLocalizationValidation['informed-agree-with'] + ' ' + bgLocalizationValidation.gdpr + private readonly bgStripeErrorNoBalanceText = 'Картата Ви не разполага с достатъчно средства.' async checkPageUrlByRegExp(urlRegExpAsString?: string, timeoutParam = 10000): Promise { await this.page.waitForTimeout(1000) @@ -108,7 +112,8 @@ export class DonationPage extends CampaignsPage { /** * Fill in the Stripe form with the test card data */ - async fillCardForm(): Promise { + async fillCardForm(options: { fail?: boolean }): Promise { + const data = options.fail ? stripeErrorNoBalanceFormData : stripeSuccessFormData const baseEmailLocator = this.page .locator('[data-testid="stripe-payment-form"]') .frameLocator('iframe') @@ -122,11 +127,11 @@ export class DonationPage extends CampaignsPage { const cardExpiryField = baseCardPaymentLocator.locator('input[name="expiry"]') const cvcField = baseCardPaymentLocator.locator('input[name="cvc"]') const countrySelect = baseCardPaymentLocator.locator('select[name="country"]') - await emailField.fill(stripeFormData.email) - await cardNumberField.fill(stripeFormData.cardNumber) - await cardExpiryField.fill(stripeFormData.expiryDate) - await cvcField.fill(stripeFormData.cvc) - await countrySelect.selectOption(stripeFormData.country) + await emailField.fill(data.email) + await cardNumberField.fill(data.cardNumber) + await cardExpiryField.fill(data.expiryDate) + await cvcField.fill(data.cvc) + await countrySelect.selectOption(data.country) } /** @@ -134,7 +139,16 @@ export class DonationPage extends CampaignsPage { */ async fillEmailField(): Promise { const emailField = this.page.locator('.MuiInputBase-root>input[name="email"]') - await emailField.fill(stripeFormData.email) + await emailField.fill(stripeSuccessFormData.email) + } + + /** + * Set donation region from the radio cards + * @param {number} amount + */ + async hasPaymentErrorMessage(): Promise { + const errorMessage = this.page.getByText(this.bgStripeErrorNoBalanceText) + return errorMessage.isVisible() } /** diff --git a/e2e/tests/regression/donation-flow/anon-donation-custom.spec.ts b/e2e/tests/regression/donation-flow/anon-donation-custom.spec.ts index f41d7e078..6632fdb01 100644 --- a/e2e/tests/regression/donation-flow/anon-donation-custom.spec.ts +++ b/e2e/tests/regression/donation-flow/anon-donation-custom.spec.ts @@ -70,7 +70,9 @@ test.describe.serial( }) test('Fill in the stripe card form', async () => { - await donationPage.fillCardForm() + await donationPage.fillCardForm({ + fail: false, + }) }) test('The user is able to fill in e-mail for anonymous donation', async () => { diff --git a/e2e/tests/regression/donation-flow/anon-donation-fixed.spec.ts b/e2e/tests/regression/donation-flow/anon-donation-fixed.spec.ts index 8f14eb19b..d0524a155 100644 --- a/e2e/tests/regression/donation-flow/anon-donation-fixed.spec.ts +++ b/e2e/tests/regression/donation-flow/anon-donation-fixed.spec.ts @@ -2,24 +2,29 @@ import { test, expect, Page } from '@playwright/test' import { HeaderPage } from '../../../pages/web-pages/header.page' import { HomePage } from '../../../pages/web-pages/home.page' import { CampaignsPage } from '../../../pages/web-pages/campaigns/campaigns.page' -import { enLocalizationDonationFlow } from '../../../data/localization' import { DonationPage } from '../../../pages/web-pages/donation/donation.page' import { DonationRegions } from '../../../data/enums/donation-regions.enum' -import { LanguagesEnum } from '../../../data/enums/languages.enum' +import { bgLocalizationDonationFlow } from '../../../data/localization' +import { + DonationFormAuthState, + DonationFormPaymentMethod, +} from 'components/donation-flow/helpers/types' +import { DonationStatusPage } from '../../../pages/web-pages/donation/donation-status.page' -// This spec contains E2E tests related to anonymous donation flow - fixed amount +// This spec contains E2E tests related to anonymous donation flow - custom amount // The tests are dependent, the whole describe should be runned -test.describe.skip( - 'Anonymous contributor is able to donate fixed amount - EN language version', +test.describe.serial( + 'Anonymous contributor is able to donate custom amount - BG language version', async () => { let page: Page let homepage: HomePage let headerPage: HeaderPage let campaignsPage: CampaignsPage let donationPage: DonationPage + let statusPage: DonationStatusPage // Localization texts - const enCardIncludeFeesText = - enLocalizationDonationFlow.step['payment-method'].alert['card-fee'] + const bgCardIncludeFeesText = + bgLocalizationDonationFlow.step['payment-method'].field['include-fees'].label test.beforeAll(async ({ browser }) => { page = await browser.newPage() @@ -27,10 +32,10 @@ test.describe.skip( headerPage = new HeaderPage(page) campaignsPage = new CampaignsPage(page) donationPage = new DonationPage(page) + statusPage = new DonationStatusPage(page) // For local executions use method navigateToLocalhostHomepage(); // await homepage.navigateToLocalhostHomepage(); await homepage.navigateToEnvHomepage() - await headerPage.changeanguageHeaderButtonToBe(LanguagesEnum.EN) }) test.afterAll(async () => { @@ -42,8 +47,6 @@ test.describe.skip( await campaignsPage.clickCampaignCardByIndex(0) // We move from the common Campaigns page to the particular campain page // check if the url is changed only based on the url pattern http://localhost:3040/campaigns/{slug-based-regexp} - // expect to not break - expect( await campaignsPage.checkPageUrlByRegExp(), 'The url is not changed after clicking on the campaign card.', @@ -53,19 +56,37 @@ test.describe.skip( test('The total charge, fee tax and donation amount are visible on the Campaign page', async () => { await campaignsPage.clickDonationSupportButton() await donationPage.checkPageUrlByRegExp() - expect - .soft( - await donationPage.isSelectAmountStepActive(LanguagesEnum.EN), - 'Select Amount step is not active.', - ) - .toBeTruthy() await donationPage.selectRadioButtonByLabelText(['10']) + await donationPage.selectPaymentMethod(DonationFormPaymentMethod.CARD) await donationPage.setDonationRegionFromTheDropdown(DonationRegions.EUROPE) - await donationPage.selectCheckboxByLabelText([enCardIncludeFeesText]) + await donationPage.selectCheckboxByLabelText([bgCardIncludeFeesText]) }) test('The total charge, fee tax and donation amount are recalculated correctly when the donation amount is changed', async () => { await donationPage.selectRadioButtonByLabelText(['20']) + await donationPage.checkTotalAmount(20.75) + }) + + test('Fill in the stripe card form', async () => { + await donationPage.fillCardForm({ + fail: false, + }) + }) + + test('The user is able to fill in e-mail for anonymous donation', async () => { + await donationPage.selectAuthentication(DonationFormAuthState.NOREGISTER) + await donationPage.fillEmailField() + }) + + test('The user can submit the form', async () => { + await donationPage.checkPrivacyCheckbox() + await donationPage.submitForm() + await page.waitForEvent('domcontentloaded') + }) + + test('The user is redirected to succes page', async () => { + await statusPage.checkPageUrlByRegExp() + expect(await statusPage.isSucceededStatusTitleDisplayed()).toBe(true) }) }, ) diff --git a/e2e/tests/regression/donation-flow/donation-fail.spec.ts b/e2e/tests/regression/donation-flow/donation-fail.spec.ts new file mode 100644 index 000000000..69502e69e --- /dev/null +++ b/e2e/tests/regression/donation-flow/donation-fail.spec.ts @@ -0,0 +1,87 @@ +import { test, expect, Page } from '@playwright/test' +import { HeaderPage } from '../../../pages/web-pages/header.page' +import { HomePage } from '../../../pages/web-pages/home.page' +import { CampaignsPage } from '../../../pages/web-pages/campaigns/campaigns.page' +import { DonationPage } from '../../../pages/web-pages/donation/donation.page' +import { DonationRegions } from '../../../data/enums/donation-regions.enum' +import { bgLocalizationDonationFlow } from '../../../data/localization' +import { + DonationFormAuthState, + DonationFormPaymentMethod, +} from 'components/donation-flow/helpers/types' + +// This spec contains E2E tests related to anonymous donation flow - custom amount +// The tests are dependent, the whole describe should be runned +test.describe.serial( + 'Anonymous contributor is able to donate custom amount - BG language version', + async () => { + let page: Page + let homepage: HomePage + let headerPage: HeaderPage + let campaignsPage: CampaignsPage + let donationPage: DonationPage + // Localization texts + const bgCardIncludeFeesText = + bgLocalizationDonationFlow.step['payment-method'].field['include-fees'].label + + test.beforeAll(async ({ browser }) => { + page = await browser.newPage() + homepage = new HomePage(page) + headerPage = new HeaderPage(page) + campaignsPage = new CampaignsPage(page) + donationPage = new DonationPage(page) + // For local executions use method navigateToLocalhostHomepage(); + // await homepage.navigateToLocalhostHomepage(); + await homepage.navigateToEnvHomepage() + }) + + test.afterAll(async () => { + await page.close() + }) + + test('Particular campaign can be opened through the Campaign page', async () => { + await headerPage.clickDonateHeaderNavButton() + await campaignsPage.clickCampaignCardByIndex(0) + // We move from the common Campaigns page to the particular campain page + // check if the url is changed only based on the url pattern http://localhost:3040/campaigns/{slug-based-regexp} + expect( + await campaignsPage.checkPageUrlByRegExp(), + 'The url is not changed after clicking on the campaign card.', + ) + }) + + test('The total charge, fee tax and donation amount are visible on the Campaign page', async () => { + await campaignsPage.clickDonationSupportButton() + await donationPage.checkPageUrlByRegExp() + await donationPage.selectRadioButtonByLabelText(['10']) + await donationPage.selectPaymentMethod(DonationFormPaymentMethod.CARD) + await donationPage.setDonationRegionFromTheDropdown(DonationRegions.EUROPE) + await donationPage.selectCheckboxByLabelText([bgCardIncludeFeesText]) + }) + + test('The total charge, fee tax and donation amount are recalculated correctly when the donation amount is changed', async () => { + await donationPage.selectRadioButtonByLabelText(['20']) + await donationPage.checkTotalAmount(20.75) + }) + + test('Fill in the stripe card form', async () => { + await donationPage.fillCardForm({ + fail: true, + }) + }) + + test('The user is able to fill in e-mail for anonymous donation', async () => { + await donationPage.selectAuthentication(DonationFormAuthState.NOREGISTER) + await donationPage.fillEmailField() + }) + + test('The user can submit the form', async () => { + await donationPage.checkPrivacyCheckbox() + await donationPage.submitForm() + }) + + test('Submit error is visible', async () => { + expect(await donationPage.hasPaymentErrorMessage()).toBe(true) + }) + }, +) From 9e4748fc3d7665278f858ea0aff527a82499dd77 Mon Sep 17 00:00:00 2001 From: "dimitar.nizamov" Date: Sun, 12 Feb 2023 19:00:15 +0100 Subject: [PATCH 10/12] 1317 - fix failing error alert locator --- e2e/pages/web-pages/donation/donation.page.ts | 7 +++++-- e2e/tests/regression/donation-flow/donation-fail.spec.ts | 4 +++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/e2e/pages/web-pages/donation/donation.page.ts b/e2e/pages/web-pages/donation/donation.page.ts index bf2d02e9f..b5e571ff1 100644 --- a/e2e/pages/web-pages/donation/donation.page.ts +++ b/e2e/pages/web-pages/donation/donation.page.ts @@ -147,8 +147,11 @@ export class DonationPage extends CampaignsPage { * @param {number} amount */ async hasPaymentErrorMessage(): Promise { - const errorMessage = this.page.getByText(this.bgStripeErrorNoBalanceText) - return errorMessage.isVisible() + const errorAlert = await this.page.locator('.MuiAlert-message', { + hasText: this.bgStripeErrorNoBalanceText, + }) + await this.waitForElementToBePresentedByLocator(errorAlert) + return errorAlert.isVisible() } /** diff --git a/e2e/tests/regression/donation-flow/donation-fail.spec.ts b/e2e/tests/regression/donation-flow/donation-fail.spec.ts index 69502e69e..1e487a2ec 100644 --- a/e2e/tests/regression/donation-flow/donation-fail.spec.ts +++ b/e2e/tests/regression/donation-flow/donation-fail.spec.ts @@ -81,7 +81,9 @@ test.describe.serial( }) test('Submit error is visible', async () => { - expect(await donationPage.hasPaymentErrorMessage()).toBe(true) + await donationPage.submitForm() + const message = await donationPage.hasPaymentErrorMessage() + expect(message).toBe(true) }) }, ) From 80347e2a6d3937aa64fbbcf2fa954d52d9e4af08 Mon Sep 17 00:00:00 2001 From: "dimitar.nizamov" Date: Sun, 12 Feb 2023 19:06:10 +0100 Subject: [PATCH 11/12] 1317 - return naming on the fixed tests --- e2e/pages/web-pages/donation/donation-status.page.ts | 1 - e2e/tests/regression/donation-flow/anon-donation-fixed.spec.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/e2e/pages/web-pages/donation/donation-status.page.ts b/e2e/pages/web-pages/donation/donation-status.page.ts index f1c1f6c04..6061a711b 100644 --- a/e2e/pages/web-pages/donation/donation-status.page.ts +++ b/e2e/pages/web-pages/donation/donation-status.page.ts @@ -13,7 +13,6 @@ export class DonationStatusPage extends CampaignsPage { private readonly enSuccessTitle = enLocalizationDonationFlow.status.success.title // -> Wish form <- - private readonly wishPlaceholderText = bgLocalizationDonationFlow.status.success.wish.write private readonly wishSendText = bgLocalizationDonationFlow.status.success.wish.send async checkPageUrlByRegExp(urlRegExpAsString?: string, timeoutParam = 10000): Promise { diff --git a/e2e/tests/regression/donation-flow/anon-donation-fixed.spec.ts b/e2e/tests/regression/donation-flow/anon-donation-fixed.spec.ts index d0524a155..0fad485c8 100644 --- a/e2e/tests/regression/donation-flow/anon-donation-fixed.spec.ts +++ b/e2e/tests/regression/donation-flow/anon-donation-fixed.spec.ts @@ -14,7 +14,7 @@ import { DonationStatusPage } from '../../../pages/web-pages/donation/donation-s // This spec contains E2E tests related to anonymous donation flow - custom amount // The tests are dependent, the whole describe should be runned test.describe.serial( - 'Anonymous contributor is able to donate custom amount - BG language version', + 'Anonymous contributor is able to donate fixed amount - BG language version', async () => { let page: Page let homepage: HomePage From ee66e572cfe4f13274945f87a97b385debfcc671 Mon Sep 17 00:00:00 2001 From: "dimitar.nizamov" Date: Sun, 12 Feb 2023 19:28:42 +0100 Subject: [PATCH 12/12] 1272-finalization - fix translation on the privacy error field --- src/components/donation-flow/DonationFlowForm.tsx | 2 +- .../donation-flow/steps/payment-method/PaymentMethod.tsx | 1 + .../donation-flow/steps/payment-method/TaxesCheckbox.tsx | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/donation-flow/DonationFlowForm.tsx b/src/components/donation-flow/DonationFlowForm.tsx index 05848b819..56f29fd93 100644 --- a/src/components/donation-flow/DonationFlowForm.tsx +++ b/src/components/donation-flow/DonationFlowForm.tsx @@ -78,7 +78,7 @@ const generalValidation = { is: 'NOREGISTER', then: yup.string().email('donation-flow:step.authentication.field.email.error').required(), }), - privacy: yup.bool().required().isTrue('donation-flow:errors-fields.privacy'), + privacy: yup.bool().required().isTrue('donation-flow:step.summary.field.privacy.error'), } export const validationSchema: yup.SchemaOf = yup diff --git a/src/components/donation-flow/steps/payment-method/PaymentMethod.tsx b/src/components/donation-flow/steps/payment-method/PaymentMethod.tsx index 66f739398..6de453ac9 100644 --- a/src/components/donation-flow/steps/payment-method/PaymentMethod.tsx +++ b/src/components/donation-flow/steps/payment-method/PaymentMethod.tsx @@ -57,6 +57,7 @@ export default function PaymentMethod({ + ), }, diff --git a/src/components/donation-flow/steps/payment-method/TaxesCheckbox.tsx b/src/components/donation-flow/steps/payment-method/TaxesCheckbox.tsx index c90cf6d1b..f5b2cd8e2 100644 --- a/src/components/donation-flow/steps/payment-method/TaxesCheckbox.tsx +++ b/src/components/donation-flow/steps/payment-method/TaxesCheckbox.tsx @@ -15,7 +15,7 @@ export const TaxesCheckbox = () => { return ( <> - + { } /> - +