From 628f94d3e2621102fcf48fcaf0660265ff4c5825 Mon Sep 17 00:00:00 2001 From: Abban Dunne <abban.dunne@wikimedia.de> Date: Thu, 19 Dec 2024 11:14:53 +0100 Subject: [PATCH] Implement VAR for C24_WMDE_Desktop_DE_23 VAR has a donation receipt checkbox that pre-fills the option on the main donation form. Ticket: https://phabricator.wikimedia.org/T382307 --- .../C24_WMDE_Desktop_DE_23/banner_var.ts | 2 +- .../components/BannerVar.vue | 13 +- ...inDonationFormPaymentsAndReceiptButton.vue | 37 +++ .../MainDonationFormReceiptAboveValue.vue | 172 ++++++++++++++ .../C24_WMDE_Desktop_DE_23/messages.ts | 3 +- .../styles/styles_var.scss | 60 +++++ .../C24_WMDE_Desktop_DE_23/useFormAction.ts | 22 ++ .../SubComponents/SubmitValues.vue | 3 +- src/components/composables/useFormModel.ts | 6 +- src/themes/Treedip/swatches/color_light.scss | 7 +- src/utils/FormModel/FormModel.ts | 1 + .../components/BannerVar.spec.ts | 46 +++- ...nationFormPaymentsAndReceiptButton.spec.ts | 109 +++++++++ .../MainDonationFormReceiptAboveValue.spec.ts | 217 ++++++++++++++++++ test/resetFormModel.ts | 1 + 15 files changed, 689 insertions(+), 10 deletions(-) create mode 100644 banners/desktop/C24_WMDE_Desktop_DE_23/components/MainDonationFormPaymentsAndReceiptButton.vue create mode 100644 banners/desktop/C24_WMDE_Desktop_DE_23/components/MainDonationFormReceiptAboveValue.vue create mode 100644 banners/desktop/C24_WMDE_Desktop_DE_23/styles/styles_var.scss create mode 100644 banners/desktop/C24_WMDE_Desktop_DE_23/useFormAction.ts create mode 100644 test/banners/desktop/C24_WMDE_Desktop_DE_23/components/MainDonationFormPaymentsAndReceiptButton.spec.ts create mode 100644 test/banners/desktop/C24_WMDE_Desktop_DE_23/components/MainDonationFormReceiptAboveValue.spec.ts diff --git a/banners/desktop/C24_WMDE_Desktop_DE_23/banner_var.ts b/banners/desktop/C24_WMDE_Desktop_DE_23/banner_var.ts index c49bfc19d..dd8c20eaa 100644 --- a/banners/desktop/C24_WMDE_Desktop_DE_23/banner_var.ts +++ b/banners/desktop/C24_WMDE_Desktop_DE_23/banner_var.ts @@ -1,6 +1,6 @@ import { createVueApp } from '@src/createVueApp'; -import './styles/styles.scss'; +import './styles/styles_var.scss'; import BannerConductor from '@src/components/BannerConductor/FallbackBannerConductor.vue'; import Banner from './components/BannerVar.vue'; diff --git a/banners/desktop/C24_WMDE_Desktop_DE_23/components/BannerVar.vue b/banners/desktop/C24_WMDE_Desktop_DE_23/components/BannerVar.vue index e1cae3382..d149c6562 100644 --- a/banners/desktop/C24_WMDE_Desktop_DE_23/components/BannerVar.vue +++ b/banners/desktop/C24_WMDE_Desktop_DE_23/components/BannerVar.vue @@ -34,12 +34,13 @@ <template #donation-form="{ formInteraction }: any"> <MultiStepDonation :step-controllers="stepControllers" - @form-interaction="formInteraction" :submit-callback="onSubmit" + :form-action-override="formAction" + @form-interaction="formInteraction" > <template #[FormStepNames.MainDonationFormStep]="{ pageIndex, submit, isCurrent, previous }: any"> - <MainDonationForm :page-index="pageIndex" @submit="submit" :is-current="isCurrent" @previous="previous"/> + <MainDonationForm :page-index="pageIndex" @submit="submit" :is-current="isCurrent" :show-receipt-checkbox-below="minimumAmount" @previous="previous"/> </template> <template #[FormStepNames.UpgradeToYearlyFormStep]="{ pageIndex, submit, isCurrent, previous }: any"> @@ -88,7 +89,7 @@ import FundsModal from '@src/components/UseOfFunds/FundsModal.vue'; import BannerText from '../content/BannerText.vue'; import BannerSlides from '../content/BannerSlides.vue'; import MultiStepDonation from '@src/components/DonationForm/MultiStepDonation.vue'; -import MainDonationForm from '@src/components/DonationForm/Forms/MainDonationForm.vue'; +import MainDonationForm from './MainDonationFormReceiptAboveValue.vue'; import UpgradeToYearlyButtonForm from '@src/components/DonationForm/Forms/UpgradeToYearlyButtonForm.vue'; import KeenSlider from '@src/components/Slider/KeenSlider.vue'; import { useFormModel } from '@src/components/composables/useFormModel'; @@ -111,6 +112,10 @@ import { BannerSubmitOnReturnEvent } from '@src/tracking/events/BannerSubmitOnRe import { Tracker } from '@src/tracking/Tracker'; import { useBannerHider } from '@src/components/composables/useBannerHider'; import BannerTitle from '@banners/desktop/C24_WMDE_Desktop_DE_15/content/BannerTitle.vue'; +import { useFormAction } from '../useFormAction'; +import { FormActions } from '@src/domain/FormActions'; + +const minimumAmount = 10; enum ContentStates { Main = 'wmde-banner-wrapper--main', @@ -143,6 +148,8 @@ const stepControllers = [ createSubmittableUpgradeToYearly( formModel, FormStepNames.MainDonationFormStep, FormStepNames.MainDonationFormStep ) ]; +const { formAction } = useFormAction( inject<FormActions>( 'formActions' ), minimumAmount ); + watch( contentState, async () => { emit( 'bannerContentChanged' ); } ); diff --git a/banners/desktop/C24_WMDE_Desktop_DE_23/components/MainDonationFormPaymentsAndReceiptButton.vue b/banners/desktop/C24_WMDE_Desktop_DE_23/components/MainDonationFormPaymentsAndReceiptButton.vue new file mode 100644 index 000000000..aa8637bc8 --- /dev/null +++ b/banners/desktop/C24_WMDE_Desktop_DE_23/components/MainDonationFormPaymentsAndReceiptButton.vue @@ -0,0 +1,37 @@ +<template> + <button class="wmde-banner-form-button t-submit-main-donation" type="submit"> + {{ submitButtonLabel }} + </button> +</template> + +<script setup lang="ts"> +import { computed, inject } from 'vue'; +import { PaymentMethods } from '@src/utils/FormItemsBuilder/fields/PaymentMethods'; +import { useFormModel } from '@src/components/composables/useFormModel'; +import { Translator } from '@src/Translator'; +import { AddressTypes } from '@src/utils/FormItemsBuilder/fields/AddressTypes'; + +interface Props { + paymentLabelsBelow: number; +} +const props = defineProps<Props>(); + +const formModel = useFormModel(); +const translator = inject<Translator>( 'translator' ); +const { paymentMethod, addressType, receipt, numericAmount } = formModel; + +const submitButtonLabel = computed( (): string => { + if ( numericAmount.value < props.paymentLabelsBelow && !receipt.value && [ AddressTypes.ANONYMOUS.value, '' ].includes( addressType.value ) ) { + if ( paymentMethod.value === PaymentMethods.PAYPAL.value ) { + return translator.translate( 'submit-label-paypal' ); + } else if ( paymentMethod.value === PaymentMethods.CREDIT_CARD.value ) { + return translator.translate( 'submit-label-credit-card' ); + } else if ( paymentMethod.value === PaymentMethods.SOFORT.value ) { + return translator.translate( 'submit-label-sofort' ); + } else if ( paymentMethod.value === PaymentMethods.BANK_TRANSFER.value ) { + return translator.translate( 'submit-label-bank-transfer' ); + } + } + return translator.translate( 'submit-label' ); +} ); +</script> diff --git a/banners/desktop/C24_WMDE_Desktop_DE_23/components/MainDonationFormReceiptAboveValue.vue b/banners/desktop/C24_WMDE_Desktop_DE_23/components/MainDonationFormReceiptAboveValue.vue new file mode 100644 index 000000000..3a12f7580 --- /dev/null +++ b/banners/desktop/C24_WMDE_Desktop_DE_23/components/MainDonationFormReceiptAboveValue.vue @@ -0,0 +1,172 @@ +<template> + <form + method="post" + class="wmde-banner-sub-form wmde-banner-sub-form-donation" + @submit.prevent="validate" + > + <fieldset class="wmde-banner-form-field-group"> + <legend class="wmde-banner-form-field-group-legend">{{ $translate( 'intervals-header' ) }}</legend> + <SelectGroup + :field-name="'select-interval'" + :selectionItems="formItems.intervals" + :isValid="isValidOrUnset( intervalValidity )" + :errorMessage="$translate( 'no-interval-message' )" + v-model:inputValue="interval" + :disabledOptions="disabledIntervals" + /> + </fieldset> + + <fieldset class="wmde-banner-form-field-group"> + <legend class="wmde-banner-form-field-group-legend">{{ $translate( 'amounts-header' ) }}</legend> + <SelectGroup + fieldName="select-amount" + :selectionItems="formItems.amounts" + :isValid="isValidOrUnset( amountValidity )" + :errorMessage="$translate( amountValidityMessageKey( amountValidity ) )" + v-model:inputValue="selectedAmount" + > + <SelectCustomAmount + fieldName="select-amount" + v-model:inputValue="customAmount" + @focus="clearSelectedAmount" + @blur="formatCustomAmount" + :placeholder="$translate( customAmountPlaceholderKey )" + /> + </SelectGroup> + </fieldset> + + <fieldset class="wmde-banner-form-field-group"> + <legend class="wmde-banner-form-field-group-legend">{{ $translate( 'payments-header' ) }}</legend> + <SelectGroup + :field-name="'select-payment-method'" + :selectionItems="formItems.paymentMethods" + :isValid="isValidOrUnset( paymentMethodValidity )" + :errorMessage="$translate( 'no-payment-type-message' )" + v-model:inputValue="paymentMethod" + :disabledOptions="disabledPaymentMethods" + > + <template #select-group-label="{ label, slotName }: any"> + <slot :name="'label-' + slotName" :label="label"/> + </template> + <SmsBox> + <template #sms-icon> + <slot name="sms-icon"/> + </template> + </SmsBox> + </SelectGroup> + </fieldset> + + <div class="wmde-banner-form-donation-receipt-checkbox" v-if="showReceiptCheckbox"> + <input + class="wmde-banner-form-field-checkbox" + type="checkbox" + value="person" + id="wmde-banner-form-donation-receipt" + v-model="receipt" + > + <label for="wmde-banner-form-donation-receipt"> + {{ $translate( 'donation-receipt-checkbox-label' ) }} + </label> + </div> + + <div class="wmde-banner-form-button-container"> + <slot name="button"> + <MainDonationFormButtonMultiStep :payment-labels-below="showReceiptCheckboxBelow"/> + </slot> + <button v-if="!isFormValid && showErrorScrollLink" class="wmde-banner-form-button-error"> + {{ $translate( 'global-error' ) }} + </button> + </div> + </form> +</template> + +<script lang="ts"> +// All form components must have names +export default { + name: 'MainDonationFormDonationReceipt' +}; +</script> +<script setup lang="ts"> + +import { computed, inject, onMounted, ref, watch } from 'vue'; +import SelectGroup from '@src/components/DonationForm/SubComponents/SelectGroup.vue'; +import { DonationFormItems } from '@src/utils/FormItemsBuilder/DonationFormItems'; +import SelectCustomAmount from '@src/components/DonationForm/SubComponents/SelectCustomAmount.vue'; +import SmsBox from '@src/components/DonationForm/SubComponents/SmsBox.vue'; +import { useFormModel } from '@src/components/composables/useFormModel'; +import { newDonationFormValidator } from '@src/validation/DonationFormValidator'; +import { amountValidityMessageKey } from '@src/utils/amountValidityMessageKey'; +import { isValidOrUnset } from '@src/components/DonationForm/Forms/isValidOrUnset'; +import { Currency } from '@src/utils/DynamicContent/formatters/Currency'; +import MainDonationFormButtonMultiStep from './MainDonationFormPaymentsAndReceiptButton.vue'; +import { AddressTypes } from '@src/utils/FormItemsBuilder/fields/AddressTypes'; +import { PaymentMethods } from '@src/utils/FormItemsBuilder/fields/PaymentMethods'; + +interface Props { + showReceiptCheckboxBelow: number; + showErrorScrollLink?: boolean; + customAmountPlaceholderKey?: string; +} + +const props = withDefaults( defineProps<Props>(), { + showErrorScrollLink: false, + customAmountPlaceholderKey: 'custom-amount-placeholder' +} ); +const emit = defineEmits( [ 'submit' ] ); + +const currencyFormatter = inject<Currency>( 'currencyFormatter' ); +const formItems = inject<DonationFormItems>( 'formItems' ); +const formModel = useFormModel(); +const validator = newDonationFormValidator( formModel ); +const showReceiptCheckbox = computed<boolean>( () => { + if ( interval.value === '' ) { + return false; + } + if ( paymentMethod.value === '' || paymentMethod.value === PaymentMethods.DIRECT_DEBIT.value ) { + return false; + } + + if ( numericAmount.value >= props.showReceiptCheckboxBelow ) { + return false; + } + + return true; +} ); +const isFormValid = ref<boolean>( true ); + +const validate = (): void => { + isFormValid.value = validator.validate(); + + if ( isFormValid.value ) { + emit( 'submit' ); + } +}; + +const { + interval, intervalValidity, disabledIntervals, + selectedAmount, customAmount, numericAmount, amountValidity, + paymentMethod, paymentMethodValidity, disabledPaymentMethods, + addressType, receipt +} = formModel; + +const clearSelectedAmount = (): void => { + selectedAmount.value = ''; +}; + +const formatCustomAmount = (): void => { + if ( customAmount.value !== '' ) { + customAmount.value = currencyFormatter.customAmountInput( numericAmount.value ); + } +}; + +watch( [ interval, paymentMethod, numericAmount ], () => { + if ( !showReceiptCheckbox.value ) { + receipt.value = false; + } +} ); + +onMounted( () => { + addressType.value = AddressTypes.ANONYMOUS.value; +} ); + +</script> diff --git a/banners/desktop/C24_WMDE_Desktop_DE_23/messages.ts b/banners/desktop/C24_WMDE_Desktop_DE_23/messages.ts index 6af854a63..a708091b0 100644 --- a/banners/desktop/C24_WMDE_Desktop_DE_23/messages.ts +++ b/banners/desktop/C24_WMDE_Desktop_DE_23/messages.ts @@ -29,7 +29,8 @@ const messages: TranslationMessages = { 'upgrade-to-yearly-yes': 'Ja, ich spende {{amount}} jährlich', 'campaign-day-only-n-days': 'Heute sind es nur noch {{days}} Tage bis zum Ende unserer Spendenkampagne.', 'custom-amount-placeholder': 'Wahlbetrag', - 'upgrade-to-yearly-header': 'Bitte spenden Sie {{amount}} jährlich!' + 'upgrade-to-yearly-header': 'Bitte spenden Sie {{amount}} jährlich!', + 'donation-receipt-checkbox-label': 'Bitte senden Sie mir eine steuerlich absetzbare Spendenbescheinigung an meine Postanschrift.' }; export default messages; diff --git a/banners/desktop/C24_WMDE_Desktop_DE_23/styles/styles_var.scss b/banners/desktop/C24_WMDE_Desktop_DE_23/styles/styles_var.scss new file mode 100644 index 000000000..9064eff80 --- /dev/null +++ b/banners/desktop/C24_WMDE_Desktop_DE_23/styles/styles_var.scss @@ -0,0 +1,60 @@ +// This is the file where we import the theme-specific component styles +@use 'src/themes/Treedip/swatches/skin_default' with ( + $slider: true, + $select-group: true, + $upgrade-to-yearly: true, + $fallback-banner: true, + $double-progress-bar: true, + $soft-close: true, +); +@use 'src/components/BannerConductor/banner-transition'; +@use 'src/themes/UseOfFunds/swatches/skin_default' as uof-default; +@use 'Banner'; +@use 'src/themes/UseOfFunds/UseOfFunds'; +@use 'MainBanner' with ( + $banner-height: 357px, + $form-width: 300px +); +@use 'src/themes/Treedip/defaults'; +@use 'src/themes/Treedip/ButtonClose/ButtonClose'; +@use 'src/themes/Treedip/ProgressBar/DoubleProgressBar'; +@use 'src/themes/Treedip/DonationForm/DonationForm'; +@use 'src/themes/Treedip/DonationForm/MultiStepDonation'; +@use 'src/themes/Treedip/DonationForm/SubComponents/SelectGroup'; +@use 'src/themes/Treedip/DonationForm/SubComponents/SelectGroupRadios'; +@use 'src/themes/Treedip/DonationForm/SubComponents/SelectCustomAmountRadio'; +@use 'src/themes/Treedip/DonationForm/SubComponents/SmsBox'; +@use 'src/themes/Treedip/DonationForm/Forms/MainDonationFormDonationReceipt' with ( + $padding: 14px 0 0 +); +@use 'src/themes/Treedip/DonationForm/Forms/UpgradeToYearlyButtonForm' with ( + $font-size: 14px +); +@use 'src/themes/Treedip/DonationForm/Forms/CustomAmountForm'; +@use 'src/themes/Treedip/Footer/FooterAlreadyDonated'; +@use 'src/themes/Treedip/Footer/SelectionInput'; +@use 'src/themes/Treedip/Message/Message' with ( + $slider-main-headline-font-size: 25px, + $message-header-padding-bottom: 8px, + $message-header-small-up-padding-bottom: 8px +); +@use 'src/themes/Treedip/SoftClose/SoftClose'; +@use 'src/themes/Treedip/Slider/KeenSlider' with ( + $slider-padding: 0 +); + +/** + * Fallback banner with "Fijitiv" theme + * All selectors in Fijitiv theme are prefixed with the ".wmde-banner-fallback" class selector, + so they override the "default" styles with the same selector + */ +@use 'FallbackBanner'; +@use 'src/themes/Fijitiv/FallbackBanner/FallbackButton'; +@use 'src/themes/Fijitiv/FallbackBanner/LargeFooter' with ( + $fallback-large-footer-right-before-border-color: white +); +@use 'src/themes/Fijitiv/FallbackBanner/SmallFooter'; +@use 'src/themes/Fijitiv/ProgressBar/ProgressBar' as FallbackProgressBar with ( + $progress-bar-margin: 0 15px +); +@use 'src/themes/Fijitiv/Slider/KeenSlider' as FallbackSlider; diff --git a/banners/desktop/C24_WMDE_Desktop_DE_23/useFormAction.ts b/banners/desktop/C24_WMDE_Desktop_DE_23/useFormAction.ts new file mode 100644 index 000000000..079f08a64 --- /dev/null +++ b/banners/desktop/C24_WMDE_Desktop_DE_23/useFormAction.ts @@ -0,0 +1,22 @@ +import { FormActions } from '@src/domain/FormActions'; +import { computed, Ref } from 'vue'; +import { useFormModel } from '@src/components/composables/useFormModel'; +import { PaymentMethods } from '@src/utils/FormItemsBuilder/fields/PaymentMethods'; + +export function useFormAction( formActions: FormActions, minimumAmount: number ): { formAction: Ref<string> } { + const formModel = useFormModel(); + const formAction = computed( (): string => { + + let URL: string = formActions.donateAnonymouslyAction; + + if ( formModel.numericAmount.value >= minimumAmount || formModel.receipt.value || formModel.paymentMethod.value === PaymentMethods.DIRECT_DEBIT.value ) { + URL = formActions.donateWithAddressAction + '&ap=1'; + } + + return URL; + } ); + + return { + formAction + }; +} diff --git a/src/components/DonationForm/SubComponents/SubmitValues.vue b/src/components/DonationForm/SubComponents/SubmitValues.vue index 6de3e2c71..320ccad5f 100644 --- a/src/components/DonationForm/SubComponents/SubmitValues.vue +++ b/src/components/DonationForm/SubComponents/SubmitValues.vue @@ -4,6 +4,7 @@ <input type="hidden" name="amount" :value="formattedAmountForServer" /> <input type="hidden" name="interval" :value="interval" /> <input type="hidden" name="paymentType" :value="paymentMethod" /> + <input type="hidden" name="receipt" :value="receipt" /> </div> </template> @@ -12,7 +13,7 @@ import { useFormModel } from '@src/components/composables/useFormModel'; import { computed } from 'vue'; -const { addressType, totalNumericAmount, interval, paymentMethod } = useFormModel(); +const { addressType, totalNumericAmount, interval, paymentMethod, receipt } = useFormModel(); const formattedAmountForServer = computed( (): number => { return parseFloat( totalNumericAmount.value.toFixed( 2 ) ) * 100; diff --git a/src/components/composables/useFormModel.ts b/src/components/composables/useFormModel.ts index ed6719070..26ce3254b 100644 --- a/src/components/composables/useFormModel.ts +++ b/src/components/composables/useFormModel.ts @@ -32,6 +32,8 @@ const paymentMethodValidity = ref<Validity>( Validity.Unset ); const addressType = ref<string>( '' ); const addressTypeValidity = ref<Validity>( Validity.Unset ); +const receipt = ref<boolean|null>( null ); + const hasTransactionFee = ref<boolean>( false ); const disabledIntervals = computed( (): string[] => { @@ -121,8 +123,10 @@ export function useFormModel(): FormModel { paymentMethodValidity, disabledPaymentMethods, addressType, - addressTypeValidity, + + receipt, + disabledAddressTypes, hasTransactionFee, diff --git a/src/themes/Treedip/swatches/color_light.scss b/src/themes/Treedip/swatches/color_light.scss index 869f4712e..174738ab1 100644 --- a/src/themes/Treedip/swatches/color_light.scss +++ b/src/themes/Treedip/swatches/color_light.scss @@ -58,8 +58,8 @@ $blue700: #2a4b8d; --input-checkbox-background: transparent; --input-checkbox-border: #{$grey500}; - --input-checkbox-background-checked: #{$blue700}; - --input-checkbox-border-checked: #{$blue700}; + --input-checkbox-background-checked: #{$red600}; + --input-checkbox-border-checked: #{$red600}; --animated-highlight-color: #{$grey700}; /* stylelint-disable */ @@ -131,6 +131,9 @@ $blue700: #2a4b8d; --select-group-bubble-radio-background-checked: #{$white}; --select-group-bubble-radio-border-checked: #{$blue700}; --select-group-bubble-radio-checkmark: #{$blue700}; + + --input-checkbox-background-checked: #{$blue700}; + --input-checkbox-border-checked: #{$blue700}; } @if $select-group-button { diff --git a/src/utils/FormModel/FormModel.ts b/src/utils/FormModel/FormModel.ts index 193c03ed3..2e583afc4 100644 --- a/src/utils/FormModel/FormModel.ts +++ b/src/utils/FormModel/FormModel.ts @@ -30,6 +30,7 @@ export interface FormModel { disabledPaymentMethods: ComputedRef<string[]>; addressType: Ref<string>; addressTypeValidity: Ref<Validity>; + receipt: Ref<boolean|null>; disabledAddressTypes: Ref<string[]>; /** * Flag to indicate that a payment-provider-specific transaction fee should be added to the total amount diff --git a/test/banners/desktop/C24_WMDE_Desktop_DE_23/components/BannerVar.spec.ts b/test/banners/desktop/C24_WMDE_Desktop_DE_23/components/BannerVar.spec.ts index faab3dbc0..e4f0f6239 100644 --- a/test/banners/desktop/C24_WMDE_Desktop_DE_23/components/BannerVar.spec.ts +++ b/test/banners/desktop/C24_WMDE_Desktop_DE_23/components/BannerVar.spec.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, test, vi } from 'vitest'; +import { beforeEach, describe, expect, it, test, vi } from 'vitest'; import { mount, VueWrapper } from '@vue/test-utils'; import Banner from '@banners/desktop/C24_WMDE_Desktop_DE_23/components/BannerVar.vue'; import { BannerStates } from '@src/components/BannerConductor/StateMachine/BannerStates'; @@ -134,6 +134,50 @@ describe( 'BannerVar.vue', () => { ] )( '%s', async ( testName: string ) => { await formActionSwitchFeatures[ testName ]( getWrapper() ); } ); + + it( 'Set the correct action when amount is above 10', async (): Promise<void> => { + const wrapper = getWrapper(); + + await wrapper.find( '.interval-0 input' ).trigger( 'change' ); + await wrapper.find( '.amount-10 input' ).trigger( 'change' ); + await wrapper.find( '.payment-ppl input' ).trigger( 'change' ); + + expect( wrapper.find<HTMLFormElement>( '.wmde-banner-submit-form' ).element.action ).toContain( 'with-address&ap=1' ); + } ); + + it( 'Set the correct action when amount is below 10 and receipt is not checked', async (): Promise<void> => { + const wrapper = getWrapper(); + + await wrapper.find( '.interval-0 input' ).trigger( 'change' ); + await wrapper.find( '.amount-5 input' ).trigger( 'change' ); + await wrapper.find( '.payment-ppl input' ).trigger( 'change' ); + + expect( wrapper.find<HTMLFormElement>( '.wmde-banner-submit-form' ).element.action ).toContain( 'without-address' ); + } ); + + it( 'Set the correct action when amount is below 10 and receipt is checked', async (): Promise<void> => { + const wrapper = getWrapper(); + + await wrapper.find( '.interval-0 input' ).trigger( 'change' ); + await wrapper.find( '.amount-5 input' ).trigger( 'change' ); + await wrapper.find( '.payment-ppl input' ).trigger( 'change' ); + await wrapper.find( '#wmde-banner-form-donation-receipt' ).trigger( 'click' ); + + expect( wrapper.find<HTMLFormElement>( '.wmde-banner-submit-form' ).element.action ).toContain( 'with-address&ap=1' ); + } ); + + it( 'Puts the receipt option into the submit form', async (): Promise<void> => { + const wrapper = getWrapper(); + + expect( wrapper.find<HTMLInputElement>( '.wmde-banner-submit-form [name="receipt"]' ).element.value ).toStrictEqual( 'false' ); + + await wrapper.find( '.interval-0 input' ).trigger( 'change' ); + await wrapper.find( '.amount-5 input' ).trigger( 'change' ); + await wrapper.find( '.payment-ppl input' ).trigger( 'change' ); + await wrapper.find( '#wmde-banner-form-donation-receipt' ).trigger( 'click' ); + + expect( wrapper.find<HTMLInputElement>( '.wmde-banner-submit-form [name="receipt"]' ).element.value ).toStrictEqual( 'true' ); + } ); } ); describe( 'Soft Close', () => { diff --git a/test/banners/desktop/C24_WMDE_Desktop_DE_23/components/MainDonationFormPaymentsAndReceiptButton.spec.ts b/test/banners/desktop/C24_WMDE_Desktop_DE_23/components/MainDonationFormPaymentsAndReceiptButton.spec.ts new file mode 100644 index 000000000..d1228f5eb --- /dev/null +++ b/test/banners/desktop/C24_WMDE_Desktop_DE_23/components/MainDonationFormPaymentsAndReceiptButton.spec.ts @@ -0,0 +1,109 @@ +import { beforeEach, describe, expect, it, test } from 'vitest'; +import { useFormModel } from '@src/components/composables/useFormModel'; +import { shallowMount } from '@vue/test-utils'; +import { PaymentMethods } from '@src/utils/FormItemsBuilder/fields/PaymentMethods'; +import MainDonationFormPaymentsAndReceiptButton from '@banners/desktop/C24_WMDE_Desktop_DE_23/components/MainDonationFormPaymentsAndReceiptButton.vue'; +import { AddressTypes } from '@src/utils/FormItemsBuilder/fields/AddressTypes'; +import { resetFormModel } from '@test/resetFormModel'; + +const translate = ( key: string ): string => key; +const formModel = useFormModel(); + +describe( 'MainDonationFormPaymentsAndReceiptButton.vue', () => { + + beforeEach( () => resetFormModel( formModel ) ); + + test.each( [ + [ AddressTypes.ANONYMOUS.value, 'submit-label-paypal' ], + [ '', 'submit-label-paypal' ], + [ AddressTypes.EMAIL.value, 'submit-label' ], + [ AddressTypes.FULL.value, 'submit-label' ] + ] )( 'shows the correct label for address type %s', ( addressType: string, label: string ) => { + formModel.addressType.value = addressType; + formModel.paymentMethod.value = PaymentMethods.PAYPAL.value; + formModel.customAmount.value = '9.99'; + + const wrapper = shallowMount( MainDonationFormPaymentsAndReceiptButton, { + props: { + paymentLabelsBelow: 10 + }, + global: { + provide: { + translator: { translate } + } + } + } ); + + expect( wrapper.text() ).toStrictEqual( label ); + } ); + + test.each( [ + [ true, 'submit-label' ], + [ false, 'submit-label-paypal' ], + [ null, 'submit-label-paypal' ] + ] )( 'shows the correct label for receipt %s', ( receipt: boolean|null, label: string ) => { + formModel.receipt.value = receipt; + formModel.paymentMethod.value = PaymentMethods.PAYPAL.value; + formModel.addressType.value = AddressTypes.ANONYMOUS.value; + formModel.customAmount.value = '9.99'; + + const wrapper = shallowMount( MainDonationFormPaymentsAndReceiptButton, { + props: { + paymentLabelsBelow: 10 + }, + global: { + provide: { + translator: { translate } + } + } + } ); + + expect( wrapper.text() ).toStrictEqual( label ); + } ); + + test.each( [ + [ PaymentMethods.PAYPAL.value, 'submit-label-paypal' ], + [ PaymentMethods.BANK_TRANSFER.value, 'submit-label-bank-transfer' ], + [ PaymentMethods.CREDIT_CARD.value, 'submit-label-credit-card' ], + [ PaymentMethods.SOFORT.value, 'submit-label-sofort' ], + [ PaymentMethods.DIRECT_DEBIT.value, 'submit-label' ] + ] )( 'shows the correct label when anonymous for payment method %s', ( paymentMethod: string, label: string ) => { + formModel.addressType.value = AddressTypes.ANONYMOUS.value; + formModel.paymentMethod.value = paymentMethod; + formModel.customAmount.value = '9.99'; + + const wrapper = shallowMount( MainDonationFormPaymentsAndReceiptButton, { + props: { + paymentLabelsBelow: 10 + }, + global: { + provide: { + translator: { translate } + } + } + } ); + + expect( wrapper.text() ).toStrictEqual( label ); + } ); + + it( 'shows the correct label when amount is 10 or higher', () => { + formModel.receipt.value = false; + formModel.paymentMethod.value = PaymentMethods.PAYPAL.value; + formModel.addressType.value = AddressTypes.ANONYMOUS.value; + formModel.customAmount.value = '10'; + + const wrapper = shallowMount( MainDonationFormPaymentsAndReceiptButton, { + props: { + paymentLabelsBelow: 10 + }, + global: { + provide: { + translator: { translate } + } + } + } ); + + expect( wrapper.text() ).toStrictEqual( 'submit-label' ); + } ); + +} ); diff --git a/test/banners/desktop/C24_WMDE_Desktop_DE_23/components/MainDonationFormReceiptAboveValue.spec.ts b/test/banners/desktop/C24_WMDE_Desktop_DE_23/components/MainDonationFormReceiptAboveValue.spec.ts new file mode 100644 index 000000000..fdf8d6dfe --- /dev/null +++ b/test/banners/desktop/C24_WMDE_Desktop_DE_23/components/MainDonationFormReceiptAboveValue.spec.ts @@ -0,0 +1,217 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { mount, VueWrapper } from '@vue/test-utils'; +import DonationForm from '@banners/desktop/C24_WMDE_Desktop_DE_23/components/MainDonationFormReceiptAboveValue.vue'; +import { DonationFormItems } from '@src/utils/FormItemsBuilder/DonationFormItems'; +import { Intervals } from '@src/utils/FormItemsBuilder/fields/Intervals'; +import { PaymentMethods } from '@src/utils/FormItemsBuilder/fields/PaymentMethods'; +import { newDonationFormValidator } from '@src/validation/DonationFormValidator'; +import { useFormModel } from '@src/components/composables/useFormModel'; +import { resetFormModel } from '@test/resetFormModel'; +import { CurrencyEn } from '@src/utils/DynamicContent/formatters/CurrencyEn'; +import { TrackerSpy } from '@test/fixtures/TrackerSpy'; +import { AddressTypes } from '@src/utils/FormItemsBuilder/fields/AddressTypes'; + +const formItems: DonationFormItems = { + addressType: [], + amounts: [ + { value: '1', label: '€1', className: 'amount-1' }, + { value: '5', label: '€5', className: 'amount-5' } + ], + intervals: [ Intervals.ONCE, Intervals.MONTHLY ], + paymentMethods: [ PaymentMethods.PAYPAL, PaymentMethods.CREDIT_CARD, PaymentMethods.DIRECT_DEBIT ] +}; + +vi.mock( '@src/validation/DonationFormValidator', () => { + return { + newDonationFormValidator: vi.fn() + }; +} ); + +const formModel = useFormModel(); +const translate = ( key: string ): string => key; + +describe( 'MainDonationFormReceiptAboveValue.vue', () => { + + // The model values are in the global scope, and they need to be reset before each test + beforeEach( () => resetFormModel( formModel ) ); + + const getWrapper = ( showErrorScrollLink: boolean = false ): VueWrapper<any> => { + return mount( DonationForm, { + props: { + showErrorScrollLink, + showReceiptCheckboxBelow: 10 + }, + global: { + mocks: { + $translate: translate + }, + provide: { + currencyFormatter: new CurrencyEn(), + formActions: { donateWithAddressAction: 'https://example.com', donateWithoutAddressAction: 'https://example.com' }, + formItems: formItems, + translator: { translate }, + tracker: new TrackerSpy() + } + }, + attachTo: document.body + } ); + }; + + it( 'should clear selected amount when input field is focused', () => { + const input = getWrapper().find<HTMLInputElement>( '.wmde-banner-select-custom-amount-input' ); + formModel.selectedAmount.value = '100'; + + input.trigger( 'focus' ); + + expect( formModel.selectedAmount.value ).toBe( '' ); + } ); + + it( 'should format custom amount when custom amount input field is blurred and it has a value', async () => { + const input = getWrapper().find<HTMLInputElement>( '.wmde-banner-select-custom-amount-input' ); + + await input.setValue( '3,14' ); + await input.trigger( 'blur' ); + + expect( input.element.value ).toBe( '3.14' ); + } ); + + it( 'should not format custom amount to 0.00 when input field is blurred and is blank', async () => { + const input = getWrapper().find<HTMLInputElement>( '.wmde-banner-select-custom-amount-input' ); + + await input.trigger( 'blur' ); + + expect( input.element.value ).toBe( '' ); + } ); + + it( 'shows invalid fields on submit when fields are invalid', async () => { + vi.mocked( newDonationFormValidator ).mockReturnValue( { validate: () => false } ); + const wrapper = getWrapper(); + + await wrapper.trigger( 'submit' ); + + expect( wrapper.find( '.select-interval .wmde-banner-select-group-error-message' ).exists() ).toBeTruthy(); + expect( wrapper.find( '.select-amount .wmde-banner-select-group-error-message' ).exists() ).toBeTruthy(); + expect( wrapper.find( '.select-payment-method .wmde-banner-select-group-error-message' ).exists() ).toBeTruthy(); + } ); + + it( 'shows the error scroll link when form fields are invalid', async () => { + vi.mocked( newDonationFormValidator ).mockReturnValue( { validate: () => false } ); + const wrapper = getWrapper( true ); + + await wrapper.trigger( 'submit' ); + + expect( wrapper.find( '.wmde-banner-form-button-error' ).exists() ).toBeTruthy(); + } ); + + it( 'emits an event on submit when fields are valid', () => { + vi.mocked( newDonationFormValidator ).mockReturnValue( { validate: () => true } ); + const wrapper = getWrapper(); + + wrapper.trigger( 'submit' ); + + expect( wrapper.emitted( 'submit' ).length ).toBe( 1 ); + } ); + + it( 'does not emit our own submit event when form fields are invalid', () => { + vi.mocked( newDonationFormValidator ).mockReturnValue( { validate: () => false } ); + const wrapper = getWrapper(); + + wrapper.trigger( 'submit' ); + + expect( wrapper.emitted( 'submit' ) ).toBeUndefined(); + } ); + + it( 'passes payment label slots dynamically to select group', () => { + const wrapper = mount( DonationForm, { + props: { + showErrorScrollLink: false, + showReceiptCheckboxBelow: 10 + }, + slots: { + 'label-payment-ppl': `<template #label-payment-ppl><span class="custom-label-paypal"></span></template>`, + 'label-payment-mcp': `<template #label-payment-mcp><span class="custom-label-credit-cards"></span></template>` + }, + global: { + mocks: { + $translate: translate + }, + provide: { + currencyFormatter: new CurrencyEn(), + formActions: { donateWithAddressAction: 'https://example.com', donateWithoutAddressAction: 'https://example.com' }, + formItems: formItems, + translator: { translate }, + tracker: new TrackerSpy() + } + } + } ); + + expect( wrapper.find( '.custom-label-paypal' ).exists() ).toBeTruthy(); + expect( wrapper.find( '.custom-label-credit-cards' ).exists() ).toBeTruthy(); + } ); + + it( 'sets the address type to anonymous when mounted', async () => { + expect( formModel.addressType.value ).toStrictEqual( '' ); + getWrapper(); + expect( formModel.addressType.value ).toStrictEqual( AddressTypes.ANONYMOUS.value ); + } ); + + it( 'shows the donation receipt checkbox', async () => { + const wrapper = getWrapper(); + + expect( wrapper.find( '.wmde-banner-form-donation-receipt-checkbox' ).exists() ).toBeFalsy(); + + await wrapper.find( '.interval-0 .wmde-banner-select-group-input' ).trigger( 'change' ); + await wrapper.find( '.amount-5 .wmde-banner-select-group-input' ).trigger( 'change' ); + await wrapper.find( '.payment-ppl .wmde-banner-select-group-input' ).trigger( 'change' ); + + expect( wrapper.find( '.wmde-banner-form-donation-receipt-checkbox' ).exists() ).toBeTruthy(); + } ); + + it( 'does not show the donation receipt checkbox when address type is direct debit', async () => { + const wrapper = getWrapper(); + + expect( wrapper.find( '.wmde-banner-form-donation-receipt-checkbox' ).exists() ).toBeFalsy(); + + await wrapper.find( '.interval-0 .wmde-banner-select-group-input' ).trigger( 'change' ); + await wrapper.find( '.amount-5 .wmde-banner-select-group-input' ).trigger( 'change' ); + await wrapper.find( '.payment-bez .wmde-banner-select-group-input' ).trigger( 'change' ); + + expect( wrapper.find( '.wmde-banner-form-donation-receipt-checkbox' ).exists() ).toBeFalsy(); + } ); + + it( 'updates the form model receipt', async () => { + const wrapper = getWrapper(); + + await wrapper.find( '.interval-0 .wmde-banner-select-group-input' ).trigger( 'change' ); + await wrapper.find( '.amount-5 .wmde-banner-select-group-input' ).trigger( 'change' ); + await wrapper.find( '.payment-ppl .wmde-banner-select-group-input' ).trigger( 'change' ); + + expect( formModel.receipt.value ).toStrictEqual( false ); + + await wrapper.find( '#wmde-banner-form-donation-receipt' ).trigger( 'click' ); + + expect( formModel.receipt.value ).toStrictEqual( true ); + + await wrapper.find( '#wmde-banner-form-donation-receipt' ).trigger( 'click' ); + + expect( formModel.receipt.value ).toStrictEqual( false ); + } ); + + it( 'clears the receipt option when radio field becomes hidden', async () => { + const wrapper = getWrapper(); + + await wrapper.find( '.interval-0 .wmde-banner-select-group-input' ).trigger( 'change' ); + await wrapper.find( '.amount-5 .wmde-banner-select-group-input' ).trigger( 'change' ); + await wrapper.find( '.payment-ppl .wmde-banner-select-group-input' ).trigger( 'change' ); + + expect( formModel.receipt.value ).toStrictEqual( false ); + + await wrapper.find( '#wmde-banner-form-donation-receipt' ).trigger( 'click' ); + + expect( formModel.receipt.value ).toStrictEqual( true ); + + await wrapper.find( '.payment-bez .wmde-banner-select-group-input' ).trigger( 'change' ); + + expect( formModel.receipt.value ).toStrictEqual( false ); + } ); +} ); diff --git a/test/resetFormModel.ts b/test/resetFormModel.ts index 9eddcda68..87e1e0108 100644 --- a/test/resetFormModel.ts +++ b/test/resetFormModel.ts @@ -12,5 +12,6 @@ export function resetFormModel( formModel: FormModel ): void { formModel.paymentMethodValidity.value = Validity.Unset; formModel.addressType.value = ''; formModel.addressTypeValidity.value = Validity.Unset; + formModel.receipt.value = null; formModel.hasTransactionFee.value = false; }