diff --git a/banners/mobile/banner_var.ts b/banners/mobile/banner_var.ts index 94a2a8fb9..5a0378743 100644 --- a/banners/mobile/banner_var.ts +++ b/banners/mobile/banner_var.ts @@ -1,9 +1,9 @@ import { createVueApp } from '@src/createVueApp'; -import './styles/styles.scss'; +import './styles/styles_var.scss'; import BannerConductor from '@src/components/BannerConductor/BannerConductor.vue'; -import Banner from './components/BannerCtrl.vue'; +import Banner from './components/BannerVar.vue'; import getBannerDelay from '@src/utils/getBannerDelay'; import { WindowResizeHandler } from '@src/utils/ResizeHandler'; import PageWPORG from '@src/page/PageWPORG'; diff --git a/banners/mobile/components/BannerVar.vue b/banners/mobile/components/BannerVar.vue new file mode 100644 index 000000000..c8d84a3bc --- /dev/null +++ b/banners/mobile/components/BannerVar.vue @@ -0,0 +1,161 @@ + + + diff --git a/banners/mobile/components/MiniBannerVar.vue b/banners/mobile/components/MiniBannerVar.vue new file mode 100644 index 000000000..9643cb180 --- /dev/null +++ b/banners/mobile/components/MiniBannerVar.vue @@ -0,0 +1,37 @@ + + + diff --git a/banners/mobile/content/BannerSlides.vue b/banners/mobile/content/BannerSlides.vue index ac0252c38..a3500535d 100644 --- a/banners/mobile/content/BannerSlides.vue +++ b/banners/mobile/content/BannerSlides.vue @@ -11,10 +11,8 @@

- - Millionen Menschen nutzen Wikipedia, aber 99 % spenden nicht – sie übergehen diesen Aufruf. - - Die meisten Menschen spenden, weil sie Wikipedia nützlich finden.

+ Millionen Menschen nutzen Wikipedia, aber 99 % spenden nicht + – sie übergehen diesen Aufruf. Die meisten Menschen spenden, weil sie Wikipedia nützlich finden.

diff --git a/banners/mobile/content/BannerText.vue b/banners/mobile/content/BannerText.vue index c553f50b6..0f6481d48 100644 --- a/banners/mobile/content/BannerText.vue +++ b/banners/mobile/content/BannerText.vue @@ -7,10 +7,8 @@

vielleicht kommen wir gerade ungelegen, aber dennoch: Klicken Sie jetzt bitte nicht weg! Am heutigen {{ currentDayName }} bitten wir Sie bescheiden, die Unabhängigkeit von Wikipedia zu - unterstützen. - - Millionen Menschen nutzen Wikipedia, aber 99 % spenden nicht – sie übergehen diesen Aufruf. - + unterstützen. Millionen Menschen nutzen Wikipedia, + aber 99 % spenden nicht – sie übergehen diesen Aufruf. Die meisten Menschen spenden, weil sie Wikipedia nützlich finden. Die durchschnittliche Spende beträgt 22,25 €, doch bereits 5 € helfen uns weiter. Hat Wikipedia Ihnen in diesem Jahr Wissen im Wert einer Tasse Kaffee geschenkt? Dann entscheiden Sie sich, eine der diff --git a/banners/mobile/event_map.ts b/banners/mobile/event_map.ts index 8596b1635..5e57ef7a5 100644 --- a/banners/mobile/event_map.ts +++ b/banners/mobile/event_map.ts @@ -13,7 +13,7 @@ import { WMDESizeIssueEvent } from '@src/tracking/WPORG/WMDEBannerSizeIssue'; export default new Map( [ [ CloseEvent.EVENT_NAME, mapCloseEvent ], [ MobileMiniBannerExpandedEvent.EVENT_NAME, - ( e: MobileMiniBannerExpandedEvent ): WMDELegacyBannerEvent => new WMDELegacyBannerEvent( e.eventName, 1 ) ], + ( e: MobileMiniBannerExpandedEvent ): WMDELegacyBannerEvent => new WMDELegacyBannerEvent( e.eventName + ( e.userChoice !== '' ? `-${e.userChoice}` : '' ), 1 ) ], [ FormStepShownEvent.EVENT_NAME, mapFormStepShownEvent ], [ NotShownEvent.EVENT_NAME, mapNotShownEvent ], diff --git a/banners/mobile/messages.ts b/banners/mobile/messages.ts index a9655b585..f24a88452 100644 --- a/banners/mobile/messages.ts +++ b/banners/mobile/messages.ts @@ -21,7 +21,8 @@ const messages: TranslationMessages = { 'Sie eine Bestätigung per E-Mail.', 'address-type-notice-none': 'Sie verzichten sowohl auf eine Spendenquittung als auch auf eine Bestätigung ' + 'per E-Mail. Sie erhalten von uns keine Information, wenn Wikipedia wieder Hilfe braucht.', - 'soft-close-prompt': 'Wikipedia später unterstützen?' + 'soft-close-prompt': 'Wikipedia später unterstützen?', + 'use-of-funds-link': 'Was Ihre Spende bewirkt' }; export default messages; diff --git a/banners/mobile/styles/MiniBannerVar.scss b/banners/mobile/styles/MiniBannerVar.scss new file mode 100644 index 000000000..48e0cd732 --- /dev/null +++ b/banners/mobile/styles/MiniBannerVar.scss @@ -0,0 +1,158 @@ +@use '@src/themes/Mikings/variables/colors'; +@use '@src/themes/Mikings/variables/globals'; +@use 'sass:color'; + +$height: 288px !default; +$green: #6e9e00; + +.wmde-banner { + &-mini { + display: flex; + flex-direction: column; + min-height: $height; + padding: 16px 0; + position: relative; + border: 2px solid colors.$primary; + background: colors.$white; + + &-close { + position: absolute; + height: 36px; + width: 36px; + top: 11px; + right: 16px; + text-align: center; + background: colors.$white; + padding: 10px; + z-index: 2; + + &-button { + margin-top: auto; + float: right; + height: 16px; + line-height: 16px; + width: 16px; + background: colors.$white; + z-index: 2; + + svg { + height: 16px; + width: 16px; + } + + &:hover { + cursor: pointer; + } + } + } + + &-headline { + height: 25px; + text-align: center; + margin: 0 16px 16px; + + &-background { + position: relative; + text-align: left; + + @media ( min-width: 400px ) { + text-align: center; + } + + /* single line above container */ + &::before { + content: ''; + display: block; + background: colors.$primary; + width: 100%; + height: 1px; + position: absolute; + top: 50%; + z-index: 1; + } + } + + &-content { + position: relative; + display: inline-block; + font-weight: bold; + font-size: 14px; + line-height: 25px; + color: colors.$black; + background: colors.$white; + padding: 0 5px; + z-index: 2; + + @media ( min-width: 330px ) { + font-size: 16px; + } + + @media ( min-width: 360px ) { + font-size: 18px; + } + } + } + + &-slideshow { + display: flex; + flex-direction: column; + flex: 1 1 auto; + } + + &-button-group { + display: flex; + justify-content: center; + } + + &-button, + &-button-preselect { + width: 50%; + height: 40px; + line-height: 40px; + border-radius: 20px; + font-weight: bold; + color: colors.$white; + margin: 0 16px; + font-size: 14px; + white-space: nowrap; + + @media ( min-width: 370px ) { + font-size: 16px; + } + } + + &-button { + background: colors.$secondary; + + &:hover, + &:focus { + background: colors.$secondary-hover; + } + } + + &-button-preselect { + background: $green; + + &:hover, + &:focus { + background: color.adjust( $green, $lightness: -5% ); + } + } + + .smallprint-mini { + text-align: center; + font-size: 11px; + margin-top: 12px; + margin-bottom: -5px; + + a { + color: colors.$gray; + + &:hover, + &:focus { + text-decoration: underline; + } + } + } + } +} diff --git a/banners/mobile/styles/styles_var.scss b/banners/mobile/styles/styles_var.scss new file mode 100644 index 000000000..c8599e22e --- /dev/null +++ b/banners/mobile/styles/styles_var.scss @@ -0,0 +1,17 @@ +@use 'src/components/BannerConductor/banner-transition'; +@use 'src/themes/UseOfFunds/UseOfFunds'; +@use 'src/themes/Mikings/defaults'; +@use './Banner'; +@use './MiniBannerVar' with ( + $height: 288px +); +@use './FullPageBanner'; +@use 'src/themes/Mikings/Footer/Footer'; +@use 'src/themes/Mikings/Footer/SelectionInput'; +@use 'src/themes/Mikings/DonationForm/MultiStepDonation'; +@use 'src/themes/Mikings/DonationForm/Forms/UpgradeToYearlyButtonForm'; +@use 'src/themes/Mikings/DonationForm/SubComponents/SelectGroup'; +@use 'src/themes/Mikings/DonationForm/SubComponents/SelectCustomAmount'; +@use 'src/themes/Mikings/DonationForm/SubComponents/SmsBox'; +@use 'src/themes/Mikings/Slider/Slider'; +@use 'src/themes/Mikings/SoftClose/SoftClose'; diff --git a/src/tracking/events/MobileMiniBannerExpandedEvent.ts b/src/tracking/events/MobileMiniBannerExpandedEvent.ts index b1b6fbf00..90b83bb9d 100644 --- a/src/tracking/events/MobileMiniBannerExpandedEvent.ts +++ b/src/tracking/events/MobileMiniBannerExpandedEvent.ts @@ -8,4 +8,8 @@ export class MobileMiniBannerExpandedEvent implements TrackingEvent { public readonly customData: Record = {}; public readonly feature: TrackingFeatureName = 'MiniBanner'; public readonly userChoice: string = ''; + + public constructor( userChoice: string = '' ) { + this.userChoice = userChoice; + } } diff --git a/test/banners/mobile/components/BannerVar.spec.ts b/test/banners/mobile/components/BannerVar.spec.ts new file mode 100644 index 000000000..4ec71f2a0 --- /dev/null +++ b/test/banners/mobile/components/BannerVar.spec.ts @@ -0,0 +1,156 @@ +import { afterEach, beforeEach, describe, test, vi } from 'vitest'; +import { mount, VueWrapper } from '@vue/test-utils'; +import Banner from '../../../../banners/mobile/components/BannerVar.vue'; +import { BannerStates } from '@src/components/BannerConductor/StateMachine/BannerStates'; +import { PageScroller } from '@src/utils/PageScroller/PageScroller'; +import { useOfFundsContent } from '@test/banners/useOfFundsContent'; +import { newDynamicContent } from '@test/banners/dynamicCampaignContent'; +import { CurrencyEn } from '@src/utils/DynamicContent/formatters/CurrencyEn'; +import { formItems } from '@test/banners/formItems'; +import { softCloseFeatures } from '@test/features/SoftCloseMobile'; +import { useOfFundsFeatures, useOfFundsScrollFeatures } from '@test/features/UseOfFunds'; +import { miniBannerFeatures } from '@test/features/MiniBanner'; +import { donationFormFeatures } from '@test/features/forms/MainDonation_UpgradeToYearlyButton'; +import { useFormModel } from '@src/components/composables/useFormModel'; +import { resetFormModel } from '@test/resetFormModel'; +import { DynamicContent } from '@src/utils/DynamicContent/DynamicContent'; +import { bannerContentAnimatedTextFeatures } from '@test/features/BannerContent'; +import { fullPageBannerFeatures } from '@test/features/FullPageBanner'; +import { miniBannerPreselectFeatures } from '@test/features/MiniBannerPreselect'; +import { Tracker } from '@src/tracking/Tracker'; + +let pageScroller: PageScroller; +let tracker: Tracker; +const formModel = useFormModel(); +const translator = ( key: string ): string => key; +describe( 'BannerVar.vue', () => { + + let wrapper: VueWrapper; + beforeEach( () => { + resetFormModel( formModel ); + + pageScroller = { + scrollIntoView: vi.fn(), + scrollToTop: vi.fn() + }; + + tracker = { + trackEvent: vi.fn() + }; + } ); + + const getWrapper = ( dynamicContent: DynamicContent = null ): VueWrapper => { + // attachTo the document body to fix an issue with Vue Test Utils where + // clicking a submit button in a form does not fire the submit event + wrapper = mount( Banner, { + attachTo: document.body, + props: { + bannerState: BannerStates.Pending, + useOfFundsContent, + pageScroller + }, + global: { + mocks: { + $translate: translator + }, + provide: { + translator: { translate: translator }, + dynamicCampaignText: dynamicContent ?? newDynamicContent(), + formActions: { donateWithAddressAction: 'https://example.com', donateWithoutAddressAction: 'https://example.com' }, + currencyFormatter: new CurrencyEn(), + formItems, + tracker + } + } + } ); + + return wrapper; + }; + + afterEach( () => { + wrapper.unmount(); + } ); + + // skipped because the sentence is not part of the current test + describe.skip( 'Content', () => { + test.each( [ + [ 'expectHidesAnimatedVisitorsVsDonorsSentenceInMessage' ], + [ 'expectShowsAnimatedVisitorsVsDonorsSentenceInMessage' ], + [ 'expectHidesAnimatedVisitorsVsDonorsSentenceInSlideShow' ], + [ 'expectShowsAnimatedVisitorsVsDonorsSentenceInSlideShow' ] + ] )( '%s', async ( testName: string ) => { + await bannerContentAnimatedTextFeatures[ testName ]( getWrapper ); + } ); + } ); + + describe( 'Donation Form Happy Paths', () => { + test.each( [ + [ 'expectMainDonationFormSubmitsWhenSofortIsSelected' ], + [ 'expectMainDonationFormSubmitsWhenYearlyIsSelected' ], + [ 'expectMainDonationFormGoesToUpgrade' ], + [ 'expectUpgradeToYearlyFormSubmitsUpgrade' ], + [ 'expectUpgradeToYearlyFormSubmitsDontUpgrade' ], + [ 'expectUpgradeToYearlyFormGoesToMainDonation' ] + ] )( '%s', async ( testName: string ) => { + await donationFormFeatures[ testName ]( getWrapper() ); + } ); + } ); + + describe( 'Soft Close', () => { + test.each( [ + [ 'expectShowsSoftCloseOnMiniBannerClose' ], + [ 'expectDoesNotShowSoftCloseOnFullBannerClose' ], + [ 'expectEmitsSoftCloseCloseEvent' ], + [ 'expectEmitsSoftCloseMaybeLaterEvent' ], + [ 'expectEmitsSoftCloseTimeOutEvent' ], + [ 'expectEmitsBannerContentChangedOnSoftClose' ] + ] )( '%s', async ( testName: string ) => { + await softCloseFeatures[ testName ]( getWrapper() ); + } ); + } ); + + describe( 'Use of Funds', () => { + test.each( [ + [ 'expectShowsUseOfFunds' ], + [ 'expectHidesUseOfFunds' ] + ] )( '%s', async ( testName: string ) => { + await useOfFundsFeatures[ testName ]( getWrapper() ); + } ); + + test.each( [ + [ 'expectScrollsToFormWhenCallToActionIsClicked' ], + [ 'expectScrollsToLinkWhenCloseIsClicked' ] + ] )( '%s', async ( testName: string ) => { + await useOfFundsScrollFeatures[ testName ]( getWrapper(), pageScroller ); + } ); + } ); + + describe( 'Mini Banner', () => { + test.each( [ + [ 'expectSlideShowPlaysWhenMiniBannerBecomesVisible' ], + [ 'expectSlideShowStopsWhenFullBannerBecomesVisible' ], + [ 'expectShowsFullPageWhenCallToActionIsClicked' ], + [ 'expectEmitsBannerContentChangedEventWhenCallToActionIsClicked' ] + ] )( '%s', async ( testName: string ) => { + await miniBannerFeatures[ testName ]( getWrapper() ); + } ); + + test.each( [ + [ 'expectShowsFullPageWhenPreselectIsClicked' ], + [ 'expectPreselectsAmountWhenPreselectIsClicked' ], + [ 'expectTrackingEventIsFiredWhenPreselectIsClicked' ] + ] )( '%s', async ( testName: string ) => { + await miniBannerPreselectFeatures[ testName ]( getWrapper(), tracker ); + } ); + + } ); + + describe( 'Full Page Banner', () => { + test.each( [ + [ 'expectEmitsCloseEvent' ] + ] )( '%s', async ( testName: string ) => { + await fullPageBannerFeatures[ testName ]( getWrapper() ); + } ); + } ); + +} ); diff --git a/test/features/MiniBannerPreselect.ts b/test/features/MiniBannerPreselect.ts new file mode 100644 index 000000000..b56322fc8 --- /dev/null +++ b/test/features/MiniBannerPreselect.ts @@ -0,0 +1,29 @@ +import { VueWrapper } from '@vue/test-utils'; +import { expect } from 'vitest'; +import { Tracker } from '@src/tracking/Tracker'; +import { MobileMiniBannerExpandedEvent } from '@src/tracking/events/MobileMiniBannerExpandedEvent'; + +const expectShowsFullPageWhenPreselectIsClicked = async ( wrapper: VueWrapper ): Promise => { + await wrapper.find( '.wmde-banner-mini-button-preselect' ).trigger( 'click' ); + + expect( wrapper.classes() ).toContain( 'wmde-banner-wrapper--full-page' ); +}; + +const expectPreselectsAmountWhenPreselectIsClicked = async ( wrapper: VueWrapper ): Promise => { + await wrapper.find( '.wmde-banner-mini-button-preselect' ).trigger( 'click' ); + + expect( wrapper.find( '.wmde-banner-select-group-input:checked' ).element.value ).toStrictEqual( '5' ); +}; + +const expectTrackingEventIsFiredWhenPreselectIsClicked = async ( wrapper: VueWrapper, tracker: Tracker ): Promise => { + await wrapper.find( '.wmde-banner-mini-button-preselect' ).trigger( 'click' ); + + expect( tracker.trackEvent ).toHaveBeenCalledOnce(); + expect( tracker.trackEvent ).toHaveBeenCalledWith( new MobileMiniBannerExpandedEvent( 'preselected' ) ); +}; + +export const miniBannerPreselectFeatures: Record, tracker: Tracker ) => Promise> = { + expectShowsFullPageWhenPreselectIsClicked, + expectPreselectsAmountWhenPreselectIsClicked, + expectTrackingEventIsFiredWhenPreselectIsClicked +};