From 47d5d5b4b8247002d8efb497e3c88f3e75931630 Mon Sep 17 00:00:00 2001 From: Corinna Hillebrand Date: Thu, 29 Aug 2024 18:54:28 +0200 Subject: [PATCH] Add tests for new SinglePageForm Section components https://phabricator.wikimedia.org/T368525 --- .../single_page_form/PaymentSection.spec.ts | 81 +++++++ .../PersonalDataSection.spec.ts | 199 ++++++++++++++++++ ...PersonalDataSectionDonationReceipt.spec.ts | 188 +++++++++++++++++ .../SinglePageDonationForm.spec.ts | 63 ++++++ 4 files changed, 531 insertions(+) create mode 100644 tests/unit/components/pages/donation_form/single_page_form/PaymentSection.spec.ts create mode 100644 tests/unit/components/pages/donation_form/single_page_form/PersonalDataSection.spec.ts create mode 100644 tests/unit/components/pages/donation_form/single_page_form/PersonalDataSectionDonationReceipt.spec.ts create mode 100644 tests/unit/components/pages/donation_form/single_page_form/SinglePageDonationForm.spec.ts diff --git a/tests/unit/components/pages/donation_form/single_page_form/PaymentSection.spec.ts b/tests/unit/components/pages/donation_form/single_page_form/PaymentSection.spec.ts new file mode 100644 index 000000000..35418a6ef --- /dev/null +++ b/tests/unit/components/pages/donation_form/single_page_form/PaymentSection.spec.ts @@ -0,0 +1,81 @@ +import { action } from '@src/store/util'; +import { mount, VueWrapper } from '@vue/test-utils'; +import { Store } from 'vuex'; +import PaymentSection from '@src/components/pages/donation_form/singlePageFromSections/PaymentSection.vue'; +import { createStore } from '@src/store/donation_store'; +import { createFeatureToggle } from '@src/util/createFeatureToggle'; +import { nextTick } from 'vue'; + +jest.mock( '@src/util/tracking', () => { + return { + trackFormSubmission: jest.fn(), + trackDynamicForm: jest.fn(), + }; +} ); + +describe( 'PaymentSection.vue', () => { + + const getWrapper = ( store: Store = createStore() ): { wrapper: VueWrapper, store: Store } => { + const wrapper = mount( PaymentSection, { + props: { + paymentAmounts: [ 500 ], + paymentIntervals: [ 0, 1, 3, 6, 12 ], + paymentTypes: [ 'BEZ', 'PPL', 'UEB', 'BTC' ], + }, + global: { + plugins: [ store ], + }, + components: { + FeatureToggle: createFeatureToggle( [] ), + }, + } ); + return { wrapper, store }; + }; + + beforeEach( () => { + global.window.scrollTo = jest.fn(); + } ); + + it( 'validates the input on page submit', async () => { + const { wrapper, store } = getWrapper(); + store.dispatch = jest.fn().mockResolvedValue( true ); + + await wrapper.find( '#submit-btn' ).trigger( 'click' ); + + expect( store.dispatch ).toHaveBeenCalledWith( action( 'payment', 'markEmptyValuesAsInvalid' ) ); + } ); + + it.skip( 'emits event if there are no validation errors', async () => { + const { wrapper } = getWrapper(); + + await wrapper.find( '#amount-500' ).trigger( 'change' ); + await wrapper.find( '#paymentType-0' ).trigger( 'change' ); + await wrapper.find( '#submit-btn' ).trigger( 'click' ); + + expect( wrapper.emitted( 'next-page' ) ).toBeTruthy(); + } ); + + it.skip( 'doesn\'t emit event if there are validation errors', async () => { + const { wrapper, store } = getWrapper(); + store.dispatch = jest.fn().mockResolvedValue( true ); + + await wrapper.find( '#submit-btn' ).trigger( 'click' ); + + expect( wrapper.emitted( 'next-page' ) ).toBeUndefined(); + } ); + + it( 'shows and hides the error summary', async () => { + const { wrapper } = getWrapper(); + + await wrapper.find( '#submit-btn' ).trigger( 'click' ); + await nextTick(); + await nextTick(); + + expect( wrapper.find( '.error-summary' ).exists() ).toBeTruthy(); + + await wrapper.find( '#amount-500' ).trigger( 'change' ); + await wrapper.find( '#paymentType-0' ).trigger( 'change' ); + + expect( wrapper.find( '.error-summary' ).exists() ).toBeFalsy(); + } ); +} ); diff --git a/tests/unit/components/pages/donation_form/single_page_form/PersonalDataSection.spec.ts b/tests/unit/components/pages/donation_form/single_page_form/PersonalDataSection.spec.ts new file mode 100644 index 000000000..8c49e2afc --- /dev/null +++ b/tests/unit/components/pages/donation_form/single_page_form/PersonalDataSection.spec.ts @@ -0,0 +1,199 @@ +import { flushPromises, mount, VueWrapper } from '@vue/test-utils'; + +import axios from 'axios'; +import PersonalDataSection from '@src/components/pages/donation_form/singlePageFromSections/PersonalDataSection.vue'; +import { createStore, StoreKey } from '@src/store/donation_store'; +import { action } from '@src/store/util'; +import PaymentBankData from '@src/components/shared/PaymentBankData.vue'; +import { AddressTypeModel } from '@src/view_models/AddressTypeModel'; +import { createFeatureToggle } from '@src/util/createFeatureToggle'; +import { Store } from 'vuex'; +import { TrackingData } from '@src/view_models/TrackingData'; +import { CampaignValues } from '@src/view_models/CampaignValues'; +import { AddressValidation } from '@src/view_models/Validation'; +import { nextTick } from 'vue'; +import AddressTypeBasic from '@src/components/pages/donation_form/AddressTypeBasic.vue'; +import { Validity } from '@src/view_models/Validity'; +import { Salutation } from '@src/view_models/Salutation'; + +const testCountry = { + countryCode: 'de', + countryFullName: 'Germany', + group: '', + postCodeValidation: '', +}; + +const salutations: Salutation[] = [ + { + label: 'Herr', + value: 'Herr', + display: 'Herr', + greetings: { + formal: 'Herr', + informal: 'Herr', + lastNameInformal: 'Herr', + }, + }, +]; + +jest.mock( 'axios' ); +const mockedAxios = axios as jest.Mocked; + +describe( 'PersonalDataSection.vue', () => { + const getWrapper = ( store: Store = createStore() ): { wrapper: VueWrapper, store: Store } => { + const wrapper = mount( PersonalDataSection, { + props: { + assetsPath: '', + validateAddressUrl: '', + validateEmailUrl: '', + validateBankDataUrl: 'https://localhost:8082', + validateLegacyBankDataUrl: 'https://localhost:8082', + countries: [ testCountry ], + salutations, + trackingData: {} as TrackingData, + campaignValues: {} as CampaignValues, + addressValidationPatterns: { postcode: '' } as AddressValidation, + }, + global: { + plugins: [ store ], + stubs: { + Address: true, + }, + provide: { + [ StoreKey as symbol ]: store, + }, + components: { + FeatureToggle: createFeatureToggle( [ 'campaigns.address_type_steps.preselect' ] ), + }, + }, + } ); + + return { wrapper, store }; + }; + + const setPaymentType = ( store: Store, paymentType: string ): Promise => { + return store.dispatch( action( 'payment', 'initializePayment' ), { + allowedIntervals: [ 0 ], + allowedPaymentTypes: [ paymentType ], + initialValues: { + amount: '100', + type: paymentType, + paymentIntervalInMonths: '0', + isCustomAmount: false, + }, + } ); + }; + + it( 'shows bank data fields if payment type is direct debit', async () => { + const { wrapper, store } = getWrapper(); + + expect( wrapper.findComponent( PaymentBankData ).exists() ).toBeFalsy(); + + await setPaymentType( store, 'BEZ' ); + + expect( wrapper.findComponent( PaymentBankData ).exists() ).toBeTruthy(); + } ); + + it( 'hides bank data fields if payment type is not direct debit', async () => { + const { wrapper, store } = getWrapper(); + + await setPaymentType( store, 'BEZ' ); + + expect( wrapper.findComponent( PaymentBankData ).exists() ).toBeTruthy(); + + await setPaymentType( store, 'UEB' ); + + expect( wrapper.findComponent( PaymentBankData ).exists() ).toBeFalsy(); + } ); + + it( 'sets address type in store when it receives address-type event', () => { + const { wrapper, store } = getWrapper(); + + store.dispatch = jest.fn(); + const expectedAction = action( 'address', 'setAddressType' ); + const expectedPayload = AddressTypeModel.ANON; + + wrapper.findComponent( AddressTypeBasic ).vm.$emit( 'address-type', AddressTypeModel.ANON ); + + expect( store.dispatch ).toBeCalledWith( expectedAction, expectedPayload ); + } ); + + it( 'shows and hides the error summary', async () => { + const { wrapper } = getWrapper(); + + await wrapper.find( '#submit-btn' ).trigger( 'click' ); + await nextTick(); + await nextTick(); + + expect( wrapper.find( '.error-summary' ).exists() ).toBeTruthy(); + + await wrapper.find( '#addressType-0' ).trigger( 'change' ); + await wrapper.find( '#person-salutation-0' ).trigger( 'change' ); + + await wrapper.find( '#person-first-name' ).setValue( 'first-name' ); + await wrapper.find( '#person-first-name' ).trigger( 'blur' ); + + await wrapper.find( '#person-last-name' ).setValue( 'last-name' ); + await wrapper.find( '#person-last-name' ).trigger( 'blur' ); + + await wrapper.find( '#person-street' ).setValue( 'street' ); + await wrapper.find( '#person-street' ).trigger( 'blur' ); + + await wrapper.find( '#person-post-code' ).setValue( 'post-code' ); + await wrapper.find( '#person-post-code' ).trigger( 'blur' ); + + await wrapper.find( '#person-city' ).setValue( 'city' ); + await wrapper.find( '#person-city' ).trigger( 'blur' ); + + await wrapper.find( '#person-country' ).setValue( 'country' ); + await wrapper.find( '#person-country' ).trigger( 'blur' ); + + await wrapper.find( '#person-email' ).setValue( 'joe@dolan.com' ); + await wrapper.find( '#person-email' ).trigger( 'blur' ); + + expect( wrapper.find( '.error-summary' ).exists() ).toBeFalsy(); + } ); + + it( 'updates full selected', async () => { + const { wrapper } = getWrapper(); + + wrapper.findComponent( AddressTypeBasic ).vm.$emit( 'address-type', AddressTypeModel.PERSON ); + wrapper.findComponent( AddressTypeBasic ).vm.$emit( 'set-full-selected' ); + await nextTick(); + + expect( wrapper.find( '.address-type-person' ).exists() ).toBeTruthy(); + } ); + + it( 'submits the form', async () => { + const store = createStore(); + await store.dispatch( action( 'address', 'initializeAddress' ), { + addressType: AddressTypeModel.ANON, + newsletter: true, + receipt: true, + fields: [ + { name: 'salutation', value: 'Mr', validity: Validity.RESTORED }, + { name: 'title', value: 'Dr', validity: Validity.RESTORED }, + { name: 'firstName', value: 'value', validity: Validity.RESTORED }, + { name: 'lastName', value: 'value', validity: Validity.RESTORED }, + { name: 'street', value: 'value', validity: Validity.RESTORED }, + { name: 'postcode', value: '12345', validity: Validity.RESTORED }, + { name: 'city', value: 'value', validity: Validity.RESTORED }, + { name: 'country', value: 'value', validity: Validity.RESTORED }, + { name: 'email', value: 'value@gmail.com', validity: Validity.RESTORED }, + { name: 'companyName', value: 'value', validity: Validity.RESTORED }, + ], + } ); + + mockedAxios.post.mockResolvedValue( { data: { status: 'OK' } } ); + const { wrapper } = getWrapper( store ); + + const submitForm = wrapper.find( '#submit-form' ); + submitForm.element.submit = jest.fn(); + + await wrapper.find( '#submit-btn' ).trigger( 'click' ); + await flushPromises(); + + expect( submitForm.element.submit ).toHaveBeenCalled(); + } ); + +} ); diff --git a/tests/unit/components/pages/donation_form/single_page_form/PersonalDataSectionDonationReceipt.spec.ts b/tests/unit/components/pages/donation_form/single_page_form/PersonalDataSectionDonationReceipt.spec.ts new file mode 100644 index 000000000..6b9e4c07a --- /dev/null +++ b/tests/unit/components/pages/donation_form/single_page_form/PersonalDataSectionDonationReceipt.spec.ts @@ -0,0 +1,188 @@ +import { flushPromises, mount, VueWrapper } from '@vue/test-utils'; + +import axios from 'axios'; +import { createStore, StoreKey } from '@src/store/donation_store'; +import { action } from '@src/store/util'; +import PaymentBankData from '@src/components/shared/PaymentBankData.vue'; +import { AddressTypeModel } from '@src/view_models/AddressTypeModel'; +import { createFeatureToggle } from '@src/util/createFeatureToggle'; +import { Store } from 'vuex'; +import { TrackingData } from '@src/view_models/TrackingData'; +import { CampaignValues } from '@src/view_models/CampaignValues'; +import { AddressValidation } from '@src/view_models/Validation'; +import { nextTick } from 'vue'; +import { Validity } from '@src/view_models/Validity'; +import { Salutation } from '@src/view_models/Salutation'; +import PersonalDataSectionDonationReceipt from '@src/components/pages/donation_form/singlePageFromSections/PersonalDataSectionDonationReceipt.vue'; + +const testCountry = { + countryCode: 'de', + countryFullName: 'Germany', + group: '', + postCodeValidation: '', +}; + +const salutations: Salutation[] = [ + { + label: 'Herr', + value: 'Herr', + display: 'Herr', + greetings: { + formal: 'Herr', + informal: 'Herr', + lastNameInformal: 'Herr', + }, + }, +]; + +jest.mock( 'axios' ); +const mockedAxios = axios as jest.Mocked; + +describe( 'PersonalDataSectionDonationReceipt.vue', () => { + const getWrapper = ( store: Store = createStore() ): { wrapper: VueWrapper, store: Store } => { + const wrapper = mount( PersonalDataSectionDonationReceipt, { + props: { + assetsPath: '', + validateAddressUrl: '', + validateEmailUrl: '', + validateBankDataUrl: 'https://localhost:8082', + validateLegacyBankDataUrl: 'https://localhost:8082', + countries: [ testCountry ], + salutations, + trackingData: {} as TrackingData, + campaignValues: {} as CampaignValues, + addressValidationPatterns: { postcode: '' } as AddressValidation, + }, + global: { + plugins: [ store ], + stubs: { + Address: true, + }, + provide: { + [ StoreKey as symbol ]: store, + }, + components: { + FeatureToggle: createFeatureToggle( [ 'campaigns.address_type_steps.preselect' ] ), + }, + }, + } ); + + return { wrapper, store }; + }; + + const setPaymentType = ( store: Store, paymentType: string ): Promise => { + return store.dispatch( action( 'payment', 'initializePayment' ), { + allowedIntervals: [ 0 ], + allowedPaymentTypes: [ paymentType ], + initialValues: { + amount: '100', + type: paymentType, + paymentIntervalInMonths: '0', + isCustomAmount: false, + }, + } ); + }; + + it( 'shows bank data fields if payment type is direct debit', async () => { + const { wrapper, store } = getWrapper(); + + expect( wrapper.findComponent( PaymentBankData ).exists() ).toBeFalsy(); + + await setPaymentType( store, 'BEZ' ); + + expect( wrapper.findComponent( PaymentBankData ).exists() ).toBeTruthy(); + } ); + + it( 'hides bank data fields if payment type is not direct debit', async () => { + const { wrapper, store } = getWrapper(); + + await setPaymentType( store, 'BEZ' ); + + expect( wrapper.findComponent( PaymentBankData ).exists() ).toBeTruthy(); + + await setPaymentType( store, 'UEB' ); + + expect( wrapper.findComponent( PaymentBankData ).exists() ).toBeFalsy(); + } ); + + it( 'scrolls to payment section when button for changing payment data is clicked', async () => { + const focusElement = { focus: jest.fn() }; + const scrollElement = { scrollIntoView: jest.fn() }; + const { wrapper } = getWrapper(); + + await wrapper.find( '#previous-btn' ).trigger( 'click' ); + + //TODO test that payment section got scrolled to + expect( focusElement.focus ).toHaveBeenCalledWith( { preventScroll: true } ); + expect( scrollElement.scrollIntoView ).toHaveBeenCalledWith( { behavior: 'smooth' } ); + } ); + + it( 'shows and hides the error summary', async () => { + const { wrapper } = getWrapper(); + + await wrapper.find( '#donation-form' ).trigger( 'submit' ); + await nextTick(); + await nextTick(); + + expect( wrapper.find( '.error-summary' ).exists() ).toBeTruthy(); + + await wrapper.find( '#donationReceipt-0' ).trigger( 'change' ); + await wrapper.find( '#addressType-0' ).trigger( 'change' ); + await wrapper.find( '#salutation-0' ).trigger( 'change' ); + + await wrapper.find( '#first-name' ).setValue( 'first-name' ); + await wrapper.find( '#first-name' ).trigger( 'blur' ); + + await wrapper.find( '#last-name' ).setValue( 'last-name' ); + await wrapper.find( '#last-name' ).trigger( 'blur' ); + + await wrapper.find( '#street' ).setValue( 'street' ); + await wrapper.find( '#street' ).trigger( 'blur' ); + + await wrapper.find( '#post-code' ).setValue( 'post-code' ); + await wrapper.find( '#post-code' ).trigger( 'blur' ); + + await wrapper.find( '#city' ).setValue( 'city' ); + await wrapper.find( '#city' ).trigger( 'blur' ); + + await wrapper.find( '#country' ).setValue( 'country' ); + await wrapper.find( '#country' ).trigger( 'blur' ); + + await wrapper.find( '#email' ).setValue( 'joe@dolan.com' ); + await wrapper.find( '#email' ).trigger( 'blur' ); + + expect( wrapper.find( '.error-summary' ).exists() ).toBeFalsy(); + } ); + + it( 'submits the form', async () => { + const store = createStore(); + await store.dispatch( action( 'address', 'initializeAddress' ), { + addressType: AddressTypeModel.PERSON, + newsletter: true, + receipt: true, + fields: [ + { name: 'salutation', value: 'Mr', validity: Validity.RESTORED }, + { name: 'title', value: 'Dr', validity: Validity.RESTORED }, + { name: 'firstName', value: 'value', validity: Validity.RESTORED }, + { name: 'lastName', value: 'value', validity: Validity.RESTORED }, + { name: 'street', value: 'value', validity: Validity.RESTORED }, + { name: 'postcode', value: '12345', validity: Validity.RESTORED }, + { name: 'city', value: 'value', validity: Validity.RESTORED }, + { name: 'country', value: 'value', validity: Validity.RESTORED }, + { name: 'email', value: 'value@gmail.com', validity: Validity.RESTORED }, + { name: 'companyName', value: 'value', validity: Validity.RESTORED }, + ], + } ); + + mockedAxios.post.mockResolvedValue( { data: { status: 'OK' } } ); + const { wrapper } = getWrapper( store ); + + const submitForm = wrapper.find( '#donation-form-submit-values' ); + submitForm.element.submit = jest.fn(); + + await wrapper.find( '#donation-form' ).trigger( 'submit' ); + await flushPromises(); + + expect( submitForm.element.submit ).toHaveBeenCalled(); + } ); +} ); diff --git a/tests/unit/components/pages/donation_form/single_page_form/SinglePageDonationForm.spec.ts b/tests/unit/components/pages/donation_form/single_page_form/SinglePageDonationForm.spec.ts new file mode 100644 index 000000000..52ba94429 --- /dev/null +++ b/tests/unit/components/pages/donation_form/single_page_form/SinglePageDonationForm.spec.ts @@ -0,0 +1,63 @@ +import { mount, VueWrapper } from '@vue/test-utils'; +import SinglePageDonationForm from '@src/components/pages/SinglePageDonationForm.vue'; +import countries from '@src/../tests/data/countries'; +import { AddressValidation } from '@src/view_models/Validation'; +import { createFeatureToggle } from '@src/util/createFeatureToggle'; +import PaymentPage from '@test/data/DonationFormPages/PaymentPageStub.vue'; +import AddressPage from '@test/data/DonationFormPages/AddressPageStub.vue'; +import { nextTick } from 'vue'; + +declare global { + namespace NodeJS { + interface Global { + window: Window; + } + } +} + +describe( 'SinglePageDonationForm.vue', () => { + + beforeEach( () => { + global.window.scrollTo = jest.fn(); + } ); + + const getWrapper = ( startPageIndex: 0 | 1 = 0 ): VueWrapper => { + return mount( SinglePageDonationForm, { + props: { + assetsPath: '', + paymentAmounts: [ 5 ], + paymentIntervals: [ 0, 1, 3, 6, 12 ], + paymentTypes: [ 'BEZ', 'PPL', 'UEB', 'BTC' ], + validateAddressUrl: 'https://example.com/address-check', + countries: countries, + trackingData: { bannerImpressionCount: 0, impressionCount: 0 }, + campaignValues: { campaign: 'nicholas', keyword: 'cage' }, + validateEmailUrl: '', + validateBankDataUrl: '', + validateLegacyBankDataUrl: '', + salutations: [], + addressValidationPatterns: {} as AddressValidation, + startPageIndex, + }, + global: { + stubs: { + PaymentPage, + AddressPage, + }, + components: { + FeatureToggle: createFeatureToggle( [ 'campaigns.address_pages.legacy' ] ), + }, + }, + } ); + }; + + it( 'displays payment section and address data section', () => { + } ); + + it( 'displays Payment page by default ', () => { + const wrapper = getWrapper( 0 ); + expect( wrapper.find( '.i-am-payment' ).exists() ).toBe( true ); + } ); + + +} );