diff --git a/archive/desktop/C23_WMDE_Desktop_DE_13/banner_ctrl.ts b/archive/desktop/C23_WMDE_Desktop_DE_13/banner_ctrl.ts new file mode 100644 index 000000000..770b65a85 --- /dev/null +++ b/archive/desktop/C23_WMDE_Desktop_DE_13/banner_ctrl.ts @@ -0,0 +1,67 @@ +import { createVueApp } from '@src/createVueApp'; + +import './styles/styles.scss'; + +import BannerConductor from '@src/components/BannerConductor/BannerConductor.vue'; +import Banner from './components/BannerCtrl.vue'; +import getBannerDelay from '@src/utils/getBannerDelay'; +import { WindowResizeHandler } from '@src/utils/ResizeHandler'; +import PageWPORG from '@src/page/PageWPORG'; +import { WindowMediaWiki } from '@src/page/MediaWiki/WindowMediaWiki'; +import { SkinFactory } from '@src/page/skin/SkinFactory'; +import { WindowSizeIssueChecker } from '@src/utils/SizeIssueChecker/WindowSizeIssueChecker'; +import TranslationPlugin from '@src/TranslationPlugin'; +import { Translator } from '@src/Translator'; +import DynamicTextPlugin from '@src/DynamicTextPlugin'; +import { LocalImpressionCount } from '@src/utils/LocalImpressionCount'; +import { LegacyTrackerWPORG } from '@src/tracking/LegacyTrackerWPORG'; +import eventMappings from './event_map'; + +// Locale-specific imports +import messages from './messages'; +import { LocaleFactoryDe } from '@src/utils/LocaleFactory/LocaleFactoryDe'; + +// Channel specific form setup +import { createFormItems } from './form_items'; +import { createFormActions } from '@src/createFormActions'; + +const localeFactory = new LocaleFactoryDe(); +const translator = new Translator( messages ); +const mediaWiki = new WindowMediaWiki(); +const page = new PageWPORG( mediaWiki, ( new SkinFactory( mediaWiki ) ).getSkin(), new WindowSizeIssueChecker( 800 ) ); +const impressionCount = new LocalImpressionCount( page.getTracking().keyword ); +const tracker = new LegacyTrackerWPORG( mediaWiki, page.getTracking().keyword, eventMappings ); +const remainingImpressions = Math.max( page.getMaxBannerImpressions( 'desktop' ) - impressionCount.overallCountIncremented, 0 ); + +const app = createVueApp( BannerConductor, { + page, + bannerConfig: { + delay: getBannerDelay( 7500 ), + transitionDuration: 1000 + }, + bannerProps: { + useOfFundsContent: localeFactory.getUseOfFundsLoader().getContent(), + remainingImpressions + }, + resizeHandler: new WindowResizeHandler(), + banner: Banner, + impressionCount +} ); + +app.use( TranslationPlugin, translator ); +app.use( DynamicTextPlugin, { + campaignParameters: page.getCampaignParameters(), + date: new Date(), + formatters: localeFactory.getFormatters(), + impressionCount, + translator +} ); + +const currencyFormatter = localeFactory.getCurrencyFormatter(); + +app.provide( 'currencyFormatter', currencyFormatter ); +app.provide( 'formItems', createFormItems( translator, currencyFormatter.euroAmount.bind( currencyFormatter ) ) ); +app.provide( 'formActions', createFormActions( page.getTracking(), impressionCount, { des: '1' } ) ); +app.provide( 'tracker', tracker ); + +app.mount( page.getBannerContainer() ); diff --git a/archive/desktop/C23_WMDE_Desktop_DE_13/banner_var.ts b/archive/desktop/C23_WMDE_Desktop_DE_13/banner_var.ts new file mode 100644 index 000000000..d4798085c --- /dev/null +++ b/archive/desktop/C23_WMDE_Desktop_DE_13/banner_var.ts @@ -0,0 +1,67 @@ +import { createVueApp } from '@src/createVueApp'; + +import './styles/styles.scss'; + +import BannerConductor from '@src/components/BannerConductor/BannerConductor.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'; +import { WindowMediaWiki } from '@src/page/MediaWiki/WindowMediaWiki'; +import { SkinFactory } from '@src/page/skin/SkinFactory'; +import { WindowSizeIssueChecker } from '@src/utils/SizeIssueChecker/WindowSizeIssueChecker'; +import TranslationPlugin from '@src/TranslationPlugin'; +import { Translator } from '@src/Translator'; +import DynamicTextPlugin from '@src/DynamicTextPlugin'; +import { LocalImpressionCount } from '@src/utils/LocalImpressionCount'; +import { LegacyTrackerWPORG } from '@src/tracking/LegacyTrackerWPORG'; +import eventMappings from './event_map'; + +// Locale-specific imports +import messages from './messages'; +import { LocaleFactoryDe } from '@src/utils/LocaleFactory/LocaleFactoryDe'; + +// Channel specific form setup +import { createFormItems } from './form_items'; +import { createFormActions } from '@src/createFormActions'; + +const localeFactory = new LocaleFactoryDe(); +const translator = new Translator( messages ); +const mediaWiki = new WindowMediaWiki(); +const page = new PageWPORG( mediaWiki, ( new SkinFactory( mediaWiki ) ).getSkin(), new WindowSizeIssueChecker( 800 ) ); +const impressionCount = new LocalImpressionCount( page.getTracking().keyword ); +const tracker = new LegacyTrackerWPORG( mediaWiki, page.getTracking().keyword, eventMappings ); +const remainingImpressions = Math.max( page.getMaxBannerImpressions( 'desktop' ) - impressionCount.overallCountIncremented, 0 ); + +const app = createVueApp( BannerConductor, { + page, + bannerConfig: { + delay: getBannerDelay( 7500 ), + transitionDuration: 1000 + }, + bannerProps: { + useOfFundsContent: localeFactory.getUseOfFundsLoader().getContent(), + remainingImpressions + }, + resizeHandler: new WindowResizeHandler(), + banner: Banner, + impressionCount +} ); + +app.use( TranslationPlugin, translator ); +app.use( DynamicTextPlugin, { + campaignParameters: page.getCampaignParameters(), + date: new Date(), + formatters: localeFactory.getFormatters(), + impressionCount, + translator +} ); + +const currencyFormatter = localeFactory.getCurrencyFormatter(); + +app.provide( 'currencyFormatter', currencyFormatter ); +app.provide( 'formItems', createFormItems( translator, currencyFormatter.euroAmount.bind( currencyFormatter ) ) ); +app.provide( 'formActions', createFormActions( page.getTracking(), impressionCount, { des: '1' } ) ); +app.provide( 'tracker', tracker ); + +app.mount( page.getBannerContainer() ); diff --git a/archive/desktop/C23_WMDE_Desktop_DE_13/components/BannerCtrl.vue b/archive/desktop/C23_WMDE_Desktop_DE_13/components/BannerCtrl.vue new file mode 100644 index 000000000..2fdbc79cd --- /dev/null +++ b/archive/desktop/C23_WMDE_Desktop_DE_13/components/BannerCtrl.vue @@ -0,0 +1,153 @@ + + + diff --git a/archive/desktop/C23_WMDE_Desktop_DE_13/components/BannerVar.vue b/archive/desktop/C23_WMDE_Desktop_DE_13/components/BannerVar.vue new file mode 100644 index 000000000..af63c5794 --- /dev/null +++ b/archive/desktop/C23_WMDE_Desktop_DE_13/components/BannerVar.vue @@ -0,0 +1,179 @@ + + + diff --git a/archive/desktop/C23_WMDE_Desktop_DE_13/components/MainBanner.vue b/archive/desktop/C23_WMDE_Desktop_DE_13/components/MainBanner.vue new file mode 100644 index 000000000..ab097bf67 --- /dev/null +++ b/archive/desktop/C23_WMDE_Desktop_DE_13/components/MainBanner.vue @@ -0,0 +1,44 @@ +tart + + diff --git a/banners/desktop/components/SoftCloseWithXButton.vue b/archive/desktop/C23_WMDE_Desktop_DE_13/components/SoftCloseWithXButton.vue similarity index 100% rename from banners/desktop/components/SoftCloseWithXButton.vue rename to archive/desktop/C23_WMDE_Desktop_DE_13/components/SoftCloseWithXButton.vue diff --git a/archive/desktop/C23_WMDE_Desktop_DE_13/content/AlreadyDonatedContent.vue b/archive/desktop/C23_WMDE_Desktop_DE_13/content/AlreadyDonatedContent.vue new file mode 100644 index 000000000..eb8eb8f8f --- /dev/null +++ b/archive/desktop/C23_WMDE_Desktop_DE_13/content/AlreadyDonatedContent.vue @@ -0,0 +1,14 @@ + diff --git a/archive/desktop/C23_WMDE_Desktop_DE_13/content/BannerSlides.vue b/archive/desktop/C23_WMDE_Desktop_DE_13/content/BannerSlides.vue new file mode 100644 index 000000000..1bda195d8 --- /dev/null +++ b/archive/desktop/C23_WMDE_Desktop_DE_13/content/BannerSlides.vue @@ -0,0 +1,51 @@ + + + diff --git a/archive/desktop/C23_WMDE_Desktop_DE_13/content/BannerText.vue b/archive/desktop/C23_WMDE_Desktop_DE_13/content/BannerText.vue new file mode 100644 index 000000000..388cdb47a --- /dev/null +++ b/archive/desktop/C23_WMDE_Desktop_DE_13/content/BannerText.vue @@ -0,0 +1,34 @@ + + + diff --git a/archive/desktop/C23_WMDE_Desktop_DE_13/event_map.ts b/archive/desktop/C23_WMDE_Desktop_DE_13/event_map.ts new file mode 100644 index 000000000..76469826d --- /dev/null +++ b/archive/desktop/C23_WMDE_Desktop_DE_13/event_map.ts @@ -0,0 +1,34 @@ +import { TrackingEventConverterFactory } from '@src/tracking/LegacyTrackerWPORG'; +import { WMDELegacyBannerEvent } from '@src/tracking/WPORG/WMDELegacyBannerEvent'; +import { WMDESizeIssueEvent } from '@src/tracking/WPORG/WMDEBannerSizeIssue'; +import { BannerSubmitEvent } from '@src/tracking/events/BannerSubmitEvent'; +import { FormStepShownEvent } from '@src/tracking/events/FormStepShownEvent'; +import { mapFormStepShownEvent } from '@src/tracking/LegacyEventTracking/mapFormStepShownEvent'; +import { CustomAmountChangedEvent } from '@src/tracking/events/CustomAmountChangedEvent'; +import { CloseEvent } from '@src/tracking/events/CloseEvent'; +import { mapCloseEvent } from '@src/tracking/LegacyEventTracking/mapCloseEvent'; +import { NotShownEvent } from '@src/tracking/events/NotShownEvent'; +import { mapNotShownEvent } from '@src/tracking/LegacyEventTracking/mapNotShownEvent'; +import { createViewportInfo } from '@src/tracking/LegacyEventTracking/createViewportInfo'; +import { AlreadyDonatedShownEvent } from '@src/tracking/events/AlreadyDonatedShownEvent'; + +export default new Map( [ + [ CloseEvent.EVENT_NAME, mapCloseEvent ], + [ FormStepShownEvent.EVENT_NAME, mapFormStepShownEvent ], + [ CustomAmountChangedEvent.EVENT_NAME, + ( e: CustomAmountChangedEvent ): WMDELegacyBannerEvent => + new WMDELegacyBannerEvent( e.userChoice + '-amount', 1 ) + ], + [ AlreadyDonatedShownEvent.EVENT_NAME, ( e: AlreadyDonatedShownEvent ): WMDELegacyBannerEvent => new WMDELegacyBannerEvent( e.eventName, 1 ) ], + [ NotShownEvent.EVENT_NAME, mapNotShownEvent ], + [ BannerSubmitEvent.EVENT_NAME, ( e: BannerSubmitEvent ): WMDESizeIssueEvent => { + switch ( e.feature ) { + case 'UpgradeToYearlyForm': + return new WMDESizeIssueEvent( `submit-${e.userChoice}`, createViewportInfo(), 1 ); + case 'UpgradeToMonthlyForm': + return new WMDESizeIssueEvent( `submit-${e.userChoice}`, createViewportInfo(), 1 ); + default: + return new WMDESizeIssueEvent( `submit`, createViewportInfo(), 1 ); + } + } ] +] ); diff --git a/archive/desktop/C23_WMDE_Desktop_DE_13/form_items.ts b/archive/desktop/C23_WMDE_Desktop_DE_13/form_items.ts new file mode 100644 index 000000000..b70650d19 --- /dev/null +++ b/archive/desktop/C23_WMDE_Desktop_DE_13/form_items.ts @@ -0,0 +1,23 @@ +import FormItemsBuilder from '@src/utils/FormItemsBuilder/FormItemsBuilder'; +import { Translator } from '@src/Translator'; +import { DonationFormItems } from '@src/utils/FormItemsBuilder/DonationFormItems'; +import { Intervals } from '@src/utils/FormItemsBuilder/fields/Intervals'; +import { PaymentMethods } from '@src/utils/FormItemsBuilder/fields/PaymentMethods'; +import { NumberFormatter } from '@src/utils/DynamicContent/formatters/NumberFormatter'; + +export function createFormItems( translations: Translator, amountFormatter: NumberFormatter ): DonationFormItems { + return new FormItemsBuilder( translations, amountFormatter ) + .setIntervals( + Intervals.ONCE, + Intervals.MONTHLY, + Intervals.QUARTERLY, + Intervals.YEARLY + ) + .setAmounts( 5, 10, 20, 25, 50, 100 ) + .setPaymentMethods( + PaymentMethods.PAYPAL, + PaymentMethods.BANK_TRANSFER, + PaymentMethods.CREDIT_CARD, + PaymentMethods.DIRECT_DEBIT + ).getItems(); +} diff --git a/archive/desktop/C23_WMDE_Desktop_DE_13/messages.ts b/archive/desktop/C23_WMDE_Desktop_DE_13/messages.ts new file mode 100644 index 000000000..4cde279fe --- /dev/null +++ b/archive/desktop/C23_WMDE_Desktop_DE_13/messages.ts @@ -0,0 +1,25 @@ +import CustomAmountFormDe from '@src/components/DonationForm/Forms/messages/CustomAmountForm.de'; +import DynamicCampaignTextDe from '@src/utils/DynamicContent/messages/DynamicCampaignText.de'; +import { TranslationMessages } from '@src/Translator'; +import UpgradeToYearlyDe from '@src/components/DonationForm/Forms/messages/UpgradeToYearly.de'; +import UpgradeToMonthlyDe from '@src/components/DonationForm/Forms/messages/UpgradeToMonthly.de'; +import SoftCloseDe from '@src/components/SoftClose/messages/SoftClose.de'; +import AddressFormDe from '@src/components/DonationForm/Forms/messages/AddressForm.de'; +import FooterDe from '@src/components/Footer/messages/Footer.de'; +import MainDonationFormDe from '@src/components/DonationForm/Forms/messages/MainDonationForm.de'; +import AlreadyDonatedModal from '@src/components/AlreadyDonatedModal/translations/AlreadyDonatedModal.de'; + +const messages: TranslationMessages = { + ...CustomAmountFormDe, + ...DynamicCampaignTextDe, + ...UpgradeToYearlyDe, + ...UpgradeToMonthlyDe, + ...SoftCloseDe, + ...AddressFormDe, + ...FooterDe, + ...MainDonationFormDe, + ...AlreadyDonatedModal, + 'already-donated-go-away-button': 'Im Moment nicht' +}; + +export default messages; diff --git a/archive/desktop/C23_WMDE_Desktop_DE_13/styles/Banner.scss b/archive/desktop/C23_WMDE_Desktop_DE_13/styles/Banner.scss new file mode 100644 index 000000000..06e30756a --- /dev/null +++ b/archive/desktop/C23_WMDE_Desktop_DE_13/styles/Banner.scss @@ -0,0 +1,17 @@ +@use 'src/themes/Treedip/variables/fonts'; +@use 'src/themes/Treedip/variables/colors'; + +.wmde-banner { + &-wrapper { + font-size: 14px; + font-family: fonts.$ui; + box-shadow: 0 3px 0.6em rgba( 60 60 60 / 40% ); + background-color: colors.$white; + } + + &--closed { + .wmde-banner-wrapper { + display: none; + } + } +} diff --git a/archive/desktop/C23_WMDE_Desktop_DE_13/styles/MainBanner.scss b/archive/desktop/C23_WMDE_Desktop_DE_13/styles/MainBanner.scss new file mode 100644 index 000000000..c328cbca9 --- /dev/null +++ b/archive/desktop/C23_WMDE_Desktop_DE_13/styles/MainBanner.scss @@ -0,0 +1,45 @@ +@use 'src/themes/Treedip/variables/colors'; + +$banner-height: 357px !default; +$form-width: 300px !default; + +.wmde-banner { + &-main { + min-height: $banner-height; + display: flex; + flex-direction: column; + padding: 12px 24px 0; + } + + &-content { + display: flex; + flex-direction: row; + flex-grow: 1; + } + + &-message { + padding: 0 15px; + } + + &-column-left { + display: flex; + flex-direction: column; + justify-content: center; + flex: 1 1 auto; + margin-bottom: 0; + overflow-y: hidden; + margin-right: 30px; + padding: 0 0 10px; + border: 5px solid colors.$primary; + border-radius: 9px; + } + + &-column-right { + order: 2; + flex: 0 0 $form-width; + display: flex; + flex-direction: column; + width: $form-width; + padding: 10px 0; + } +} diff --git a/banners/desktop/styles/SoftCloseWithXButtonOverwrites.scss b/archive/desktop/C23_WMDE_Desktop_DE_13/styles/SoftCloseWithXButtonOverwrites.scss similarity index 100% rename from banners/desktop/styles/SoftCloseWithXButtonOverwrites.scss rename to archive/desktop/C23_WMDE_Desktop_DE_13/styles/SoftCloseWithXButtonOverwrites.scss diff --git a/archive/desktop/C23_WMDE_Desktop_DE_13/styles/styles.scss b/archive/desktop/C23_WMDE_Desktop_DE_13/styles/styles.scss new file mode 100644 index 000000000..e8f805658 --- /dev/null +++ b/archive/desktop/C23_WMDE_Desktop_DE_13/styles/styles.scss @@ -0,0 +1,32 @@ +// This is the file where we import the theme-specific component styles + +@use 'src/components/BannerConductor/banner-transition'; +@use 'Banner'; +@use 'MainBanner' with ( + $banner-height: 357px, + $form-width: 300px +); +@use 'src/themes/UseOfFunds/UseOfFunds'; +@use 'src/themes/Treedip/defaults'; +@use 'src/themes/Treedip/ButtonClose/ButtonClose'; +@use 'src/themes/Treedip/ProgressBar/ProgressBar' with ( + $progress-bar-margin: 0 15px +); +@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/MainDonationForm'; +@use 'src/themes/Treedip/DonationForm/Forms/UpgradeToYearlyForm'; +@use 'src/themes/Treedip/DonationForm/Forms/CustomAmountForm'; +@use 'src/themes/Treedip/Footer/FooterAlreadyDonated' with ( + $right-column-width: 300px +); +@use 'src/themes/Treedip/Footer/SelectionInput'; +@use 'src/themes/Treedip/Message/Message'; +@use 'src/themes/Treedip/Slider/KeenSlider'; +@use 'src/themes/Treedip/SoftClose/SoftClose'; +@use './SoftCloseWithXButtonOverwrites'; +@use 'src/themes/Treedip/AlreadyDonatedModal/AlreadyDonatedModal'; diff --git a/banners/desktop/banner_ctrl.ts b/banners/desktop/banner_ctrl.ts index 3e5658a8c..e23b384a0 100644 --- a/banners/desktop/banner_ctrl.ts +++ b/banners/desktop/banner_ctrl.ts @@ -2,8 +2,9 @@ import { createVueApp } from '@src/createVueApp'; import './styles/styles.scss'; -import BannerConductor from '@src/components/BannerConductor/BannerConductor.vue'; +import BannerConductor from '@src/components/BannerConductor/FallbackBannerConductor.vue'; import Banner from './components/BannerCtrl.vue'; +import FallbackBanner from './components/FallbackBanner.vue'; import { UrlRuntimeEnvironment } from '@src/utils/RuntimeEnvironment'; import { WindowResizeHandler } from '@src/utils/ResizeHandler'; import PageWPORG from '@src/page/PageWPORG'; @@ -24,11 +25,12 @@ import { LocaleFactoryDe } from '@src/utils/LocaleFactory/LocaleFactoryDe'; // Channel specific form setup import { createFormItems } from './form_items'; import { createFormActions } from '@src/createFormActions'; +import { createFallbackDonationLink } from '@src/createFallbackDonationLink'; const localeFactory = new LocaleFactoryDe(); const translator = new Translator( messages ); const mediaWiki = new WindowMediaWiki(); -const page = new PageWPORG( mediaWiki, ( new SkinFactory( mediaWiki ) ).getSkin(), new WindowSizeIssueChecker( 800 ) ); +const page = new PageWPORG( mediaWiki, ( new SkinFactory( mediaWiki ) ).getSkin(), new WindowSizeIssueChecker( 400 ) ); const runtimeEnvironment = new UrlRuntimeEnvironment( window.location ); const impressionCount = new LocalImpressionCount( page.getTracking().keyword, runtimeEnvironment ); const tracker = new LegacyTrackerWPORG( mediaWiki, page.getTracking().keyword, eventMappings, runtimeEnvironment ); @@ -41,10 +43,13 @@ const app = createVueApp( BannerConductor, { }, bannerProps: { useOfFundsContent: localeFactory.getUseOfFundsLoader().getContent(), - remainingImpressions: impressionCount.getRemainingImpressions( page.getMaxBannerImpressions( 'desktop' ) ) + remainingImpressions: impressionCount.getRemainingImpressions( page.getMaxBannerImpressions( 'desktop' ) ), + donationLink: createFallbackDonationLink( page.getTracking(), impressionCount ) }, resizeHandler: new WindowResizeHandler(), banner: Banner, + fallbackBanner: FallbackBanner, + minWidthForMainBanner: 800, impressionCount } ); @@ -61,7 +66,7 @@ const currencyFormatter = localeFactory.getCurrencyFormatter(); app.provide( 'currencyFormatter', currencyFormatter ); app.provide( 'formItems', createFormItems( translator, currencyFormatter.euroAmount.bind( currencyFormatter ) ) ); -app.provide( 'formActions', createFormActions( page.getTracking(), impressionCount, { des: '1' } ) ); +app.provide( 'formActions', createFormActions( page.getTracking(), impressionCount ) ); app.provide( 'tracker', tracker ); app.mount( page.getBannerContainer() ); diff --git a/banners/desktop/banner_var.ts b/banners/desktop/banner_var.ts index e77ad2893..32efe12c6 100644 --- a/banners/desktop/banner_var.ts +++ b/banners/desktop/banner_var.ts @@ -2,8 +2,9 @@ import { createVueApp } from '@src/createVueApp'; import './styles/styles.scss'; -import BannerConductor from '@src/components/BannerConductor/BannerConductor.vue'; +import BannerConductor from '@src/components/BannerConductor/FallbackBannerConductor.vue'; import Banner from './components/BannerVar.vue'; +import FallbackBanner from './components/FallbackBanner.vue'; import { UrlRuntimeEnvironment } from '@src/utils/RuntimeEnvironment'; import { WindowResizeHandler } from '@src/utils/ResizeHandler'; import PageWPORG from '@src/page/PageWPORG'; @@ -24,11 +25,12 @@ import { LocaleFactoryDe } from '@src/utils/LocaleFactory/LocaleFactoryDe'; // Channel specific form setup import { createFormItems } from './form_items'; import { createFormActions } from '@src/createFormActions'; +import { createFallbackDonationLink } from '@src/createFallbackDonationLink'; const localeFactory = new LocaleFactoryDe(); const translator = new Translator( messages ); const mediaWiki = new WindowMediaWiki(); -const page = new PageWPORG( mediaWiki, ( new SkinFactory( mediaWiki ) ).getSkin(), new WindowSizeIssueChecker( 800 ) ); +const page = new PageWPORG( mediaWiki, ( new SkinFactory( mediaWiki ) ).getSkin(), new WindowSizeIssueChecker( 400 ) ); const runtimeEnvironment = new UrlRuntimeEnvironment( window.location ); const impressionCount = new LocalImpressionCount( page.getTracking().keyword, runtimeEnvironment ); const tracker = new LegacyTrackerWPORG( mediaWiki, page.getTracking().keyword, eventMappings, runtimeEnvironment ); @@ -41,10 +43,13 @@ const app = createVueApp( BannerConductor, { }, bannerProps: { useOfFundsContent: localeFactory.getUseOfFundsLoader().getContent(), - remainingImpressions: impressionCount.getRemainingImpressions( page.getMaxBannerImpressions( 'desktop' ) ) + remainingImpressions: impressionCount.getRemainingImpressions( page.getMaxBannerImpressions( 'desktop' ) ), + donationLink: createFallbackDonationLink( page.getTracking(), impressionCount ) }, resizeHandler: new WindowResizeHandler(), banner: Banner, + fallbackBanner: FallbackBanner, + minWidthForMainBanner: 800, impressionCount } ); diff --git a/banners/desktop/components/BannerCtrl.vue b/banners/desktop/components/BannerCtrl.vue index 2fdbc79cd..2e5987073 100644 --- a/banners/desktop/components/BannerCtrl.vue +++ b/banners/desktop/components/BannerCtrl.vue @@ -52,6 +52,7 @@ - - @@ -66,9 +52,11 @@ ( false ); const isAlreadyDonatedModalVisible = ref( false ); const contentState = ref( ContentStates.Main ); const formModel = useFormModel(); - -let stepControllers = [ - createSubmittableMainDonationFormUpgradeOptions( - formModel, - FormStepNames.UpgradeToYearlyFormStep, - FormStepNames.UpgradeToMonthlyFormStep, - 11, - 100 - ), - createSubmittableUpgradeToYearly( formModel, FormStepNames.MainDonationFormStep, FormStepNames.MainDonationFormStep ), - createSubmittableUpgradeToMonthly( formModel, FormStepNames.MainDonationFormStep, FormStepNames.MainDonationFormStep ) +const stepControllers = [ + createSubmittableMainDonationForm( formModel, FormStepNames.UpgradeToYearlyFormStep ), + createSubmittableUpgradeToYearly( formModel, FormStepNames.MainDonationFormStep, FormStepNames.MainDonationFormStep ) ]; watch( contentState, async () => { diff --git a/banners/desktop/components/FallbackBanner.vue b/banners/desktop/components/FallbackBanner.vue new file mode 100644 index 000000000..50c87d7a2 --- /dev/null +++ b/banners/desktop/components/FallbackBanner.vue @@ -0,0 +1,103 @@ + + + diff --git a/banners/desktop/content/BannerSlides.vue b/banners/desktop/content/BannerSlides.vue index 1bda195d8..f1055e76d 100644 --- a/banners/desktop/content/BannerSlides.vue +++ b/banners/desktop/content/BannerSlides.vue @@ -1,7 +1,7 @@ diff --git a/src/components/Slider/KeenSliderNavigation.vue b/src/components/Slider/KeenSliderNavigation.vue new file mode 100644 index 000000000..1eec41ec6 --- /dev/null +++ b/src/components/Slider/KeenSliderNavigation.vue @@ -0,0 +1,23 @@ + + + diff --git a/src/components/composables/useCurrentDateAndTime.ts b/src/components/composables/useCurrentDateAndTime.ts new file mode 100644 index 000000000..aae4b26ae --- /dev/null +++ b/src/components/composables/useCurrentDateAndTime.ts @@ -0,0 +1,28 @@ +import { ref, Ref } from 'vue'; + +type ReturnType = { + currentDateTime: Ref; + startTimer: () => void; + stopTimer: () => void; +} + +export function useCurrentDateAndTime( getCurrentDateAndTime: () => string ): ReturnType { + const timer = ref( 0 ); + const currentDateTime = ref( getCurrentDateAndTime() ); + + const startTimer = (): void => { + timer.value = window.setInterval( () => { + currentDateTime.value = getCurrentDateAndTime(); + }, 1000 ); + }; + + const stopTimer = (): void => { + window.clearInterval( timer.value ); + }; + + return { + currentDateTime, + startTimer, + stopTimer + }; +} diff --git a/src/createFallbackDonationLink.ts b/src/createFallbackDonationLink.ts new file mode 100644 index 000000000..58fefe5fc --- /dev/null +++ b/src/createFallbackDonationLink.ts @@ -0,0 +1,17 @@ +/* eslint-disable camelcase */ +import { TrackingParameters } from '@src/domain/TrackingParameters'; +import { ImpressionCount } from '@src/utils/ImpressionCount'; + +const DONATE_LINK_URL = 'https://spenden.wikimedia.de'; + +export function createFallbackDonationLink( tracking: TrackingParameters, impressionCount: ImpressionCount, extraUrlParameters: Record = {} ): string { + const urlParameters = new URLSearchParams( { + piwik_kwd: tracking.keyword.replace( /(ctrl|var)/g, 'mini' ), + piwik_campaign: tracking.campaign, + impCount: String( impressionCount.overallCountIncremented ), + bImpCount: String( impressionCount.bannerCountIncremented ), + ...extraUrlParameters + } ); + + return `${DONATE_LINK_URL}?${urlParameters}`; +} diff --git a/src/themes/Treedip/FallbackBanner/FallbackButton.scss b/src/themes/Treedip/FallbackBanner/FallbackButton.scss new file mode 100644 index 000000000..7f1c54662 --- /dev/null +++ b/src/themes/Treedip/FallbackBanner/FallbackButton.scss @@ -0,0 +1,23 @@ +@use 'src/themes/Treedip/variables/globals'; +@use 'src/themes/Treedip/variables/colors'; +@use 'sass:color'; + +.wmde-banner-fallback { + &-button { + width: 300px; + background: colors.$secondary; + color: colors.$white; + font-weight: bold; + line-height: 36px; + border: 0; + border-radius: 18px; + padding: 0 10px; + transition: background-color 500ms globals.$banner-easing; + cursor: pointer; + + &:hover, + &:focus { + background: color.adjust( colors.$secondary, $lightness: -5% ); + } + } +} diff --git a/src/themes/Treedip/FallbackBanner/LargeFooter.scss b/src/themes/Treedip/FallbackBanner/LargeFooter.scss new file mode 100644 index 000000000..de4b8fed0 --- /dev/null +++ b/src/themes/Treedip/FallbackBanner/LargeFooter.scss @@ -0,0 +1,42 @@ +@use 'src/themes/Treedip/variables/colors'; +@use 'src/components/FallbackBanner/LargeFooter'; + +.wmde-banner-fallback { + @include LargeFooter.layout; + + &-large-footer { + height: 54px; + padding: 10px 0 0; + + &-left { + font-size: 12px; + padding-left: 10px; + } + + &-right { + position: relative; + + &::before { + position: absolute; + content: ''; + top: -10px; + left: 50%; + margin-left: -6px; + width: 0; + height: 0; + border-style: solid; + border-width: 6px 6px 0; + border-color: colors.$primary transparent transparent transparent; + } + } + + &-usage-link { + color: colors.$gray; + } + + .wmde-banner-selection-input-text, + .wmde-banner-selection-input-input { + font-weight: normal; + } + } +} diff --git a/src/themes/Treedip/FallbackBanner/SmallFooter.scss b/src/themes/Treedip/FallbackBanner/SmallFooter.scss new file mode 100644 index 000000000..073184ac0 --- /dev/null +++ b/src/themes/Treedip/FallbackBanner/SmallFooter.scss @@ -0,0 +1,18 @@ +@use '../../../components/FallbackBanner/SmallFooter'; + +.wmde-banner-fallback { + @include SmallFooter.layout; + + &-small-footer { + .wmde-banner-slider-pagination, + .wmde-banner-fallback-mini-footer { + height: 15px; + } + + &-usage { + text-align: right; + margin-top: -15px; + font-size: 10px; + } + } +} diff --git a/src/tracking/LegacyEventTracking/mapShownEvent.ts b/src/tracking/LegacyEventTracking/mapShownEvent.ts new file mode 100644 index 000000000..2571e8fe8 --- /dev/null +++ b/src/tracking/LegacyEventTracking/mapShownEvent.ts @@ -0,0 +1,13 @@ +import { WMDESizeIssueEvent } from '@src/tracking/WPORG/WMDEBannerSizeIssue'; +import { WMDELegacyBannerEvent } from '@src/tracking/WPORG/WMDELegacyBannerEvent'; +import { ShownEvent } from '@src/tracking/events/ShownEvent'; +import { createViewportInfo } from '@src/tracking/LegacyEventTracking/createViewportInfo'; + +export function mapShownEvent( shownEvent: ShownEvent ): WMDESizeIssueEvent|WMDELegacyBannerEvent { + if ( shownEvent.feature === 'FallbackBanner' ) { + return new WMDESizeIssueEvent( `fallback-banner-shown`, createViewportInfo(), 1 ); + } + + // We don't track other "not shown" events, hence the trackingRate of 0 + return new WMDELegacyBannerEvent( 'untracked-not-shown-event', 0 ); +} diff --git a/src/tracking/TrackingEvent.ts b/src/tracking/TrackingEvent.ts index 9f2327a08..c1f108a91 100644 --- a/src/tracking/TrackingEvent.ts +++ b/src/tracking/TrackingEvent.ts @@ -7,6 +7,7 @@ export type TrackingFeatureName = '' | 'MainBanner' | 'MiniBanner' | 'FullPageBanner' | + 'FallbackBanner' | 'AlreadyDonatedModal' | 'MainDonationForm' | @@ -18,7 +19,7 @@ export type TrackingFeatureName = '' | /** * @param T - Defines the type of the customData property */ -export interface TrackingEvent { +export interface TrackingEvent { /** * What type of event this is * diff --git a/src/tracking/events/FallbackBannerShownEvent.ts b/src/tracking/events/FallbackBannerShownEvent.ts new file mode 100644 index 000000000..e69de29bb diff --git a/src/tracking/events/FallbackBannerSubmitEvent.ts b/src/tracking/events/FallbackBannerSubmitEvent.ts new file mode 100644 index 000000000..840f71bd2 --- /dev/null +++ b/src/tracking/events/FallbackBannerSubmitEvent.ts @@ -0,0 +1,10 @@ +import { TrackingEvent, TrackingFeatureName } from '@src/tracking/TrackingEvent'; + +export class FallbackBannerSubmitEvent implements TrackingEvent { + public static readonly EVENT_NAME = 'fallback-banner-submitted'; + + public eventName: string = FallbackBannerSubmitEvent.EVENT_NAME; + public customData: void; + public feature: TrackingFeatureName = 'FallbackBanner'; + public userChoice: string = ''; +} diff --git a/src/tracking/events/ShownEvent.ts b/src/tracking/events/ShownEvent.ts index 648b8262d..a0e1b5a16 100644 --- a/src/tracking/events/ShownEvent.ts +++ b/src/tracking/events/ShownEvent.ts @@ -5,6 +5,10 @@ export class ShownEvent implements TrackingEvent { public readonly eventName: string = ShownEvent.EVENT_NAME; public readonly customData: void; - public readonly feature: TrackingFeatureName = 'Page'; + public readonly feature: TrackingFeatureName; public readonly userChoice: string = ''; + + public constructor( feature: TrackingFeatureName ) { + this.feature = feature; + } } diff --git a/test/banners/desktop/components/BannerVar.spec.ts b/test/banners/desktop/components/BannerVar.spec.ts index 7332aa970..f6c25b5ce 100644 --- a/test/banners/desktop/components/BannerVar.spec.ts +++ b/test/banners/desktop/components/BannerVar.spec.ts @@ -10,11 +10,11 @@ import { TrackerStub } from '@test/fixtures/TrackerStub'; import { softCloseFeatures } from '@test/features/SoftCloseDesktop'; import { useOfFundsFeatures } from '@test/features/UseOfFunds'; import { - bannerContentAnimatedTextFeatures, + bannerContentAnimatedTextFeatures, bannerContentDateAndTimeFeatures, bannerContentDisplaySwitchFeatures, bannerContentFeatures } from '@test/features/BannerContent'; -import { donationFormFeatures } from '@test/features/forms/MainDonation_UpgradeToMonthlyOrYearly'; +import { donationFormFeatures } from '@test/features/forms/MainDonation_UpgradeToYearlyLink'; import { useFormModel } from '@src/components/composables/useFormModel'; import { resetFormModel } from '@test/resetFormModel'; import { DynamicContent } from '@src/utils/DynamicContent/DynamicContent'; @@ -89,23 +89,26 @@ describe( 'BannerVar.vue', () => { ] )( '%s', async ( testName: string ) => { await bannerContentAnimatedTextFeatures[ testName ]( getWrapper ); } ); + + test.each( [ + [ 'expectShowsLiveDateAndTimeInMessage' ], + [ 'expectShowsLiveDateAndTimeInSlideshow' ] + ] )( '%s', async ( testName: string ) => { + await bannerContentDateAndTimeFeatures[ testName ]( getWrapper ); + } ); + } ); describe( 'Donation Form Happy Paths', () => { test.each( [ [ 'expectMainDonationFormSubmitsWhenSofortIsSelected' ], [ 'expectMainDonationFormSubmitsWhenYearlyIsSelected' ], - [ 'expectMainDonationFormGoesToUpgradeYearlyOnVeryLowAmounts' ], - [ 'expectMainDonationFormGoesToUpgradeYearlyOnHighAmounts' ], - [ 'expectMainDonationFormGoesToUpgradeMonthly' ], + [ 'expectMainDonationFormGoesToUpgrade' ], [ 'expectUpgradeToYearlyFormSubmitsUpgrade' ], [ 'expectUpgradeToYearlyFormSubmitsDontUpgrade' ], [ 'expectUpgradeToYearlyFormGoesToMainDonation' ], [ 'expectUpgradeToYearlyFormSubmitsUpgrade' ], - [ 'expectUpgradeToYearlyFormSubmitsDontUpgrade' ], - [ 'expectUpgradeToMonthlyFormSubmitsUpgrade' ], - [ 'expectUpgradeToMonthlyFormSubmitsDontUpgrade' ], - [ 'expectUpgradeToMonthlyFormGoesToMainDonation' ] + [ 'expectUpgradeToYearlyFormSubmitsDontUpgrade' ] ] )( '%s', async ( testName: string ) => { await donationFormFeatures[ testName ]( getWrapper() ); } ); diff --git a/test/banners/desktop/components/FallbackBanner.spec.ts b/test/banners/desktop/components/FallbackBanner.spec.ts new file mode 100644 index 000000000..fe1ac1aea --- /dev/null +++ b/test/banners/desktop/components/FallbackBanner.spec.ts @@ -0,0 +1,145 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { mount, VueWrapper } from '@vue/test-utils'; +import FallbackBanner from '../../../../banners/desktop/components/FallbackBanner.vue'; +import { BannerStates } from '@src/components/BannerConductor/StateMachine/BannerStates'; +import { useOfFundsContent } from '@test/banners/useOfFundsContent'; +import { newDynamicContent } from '@test/banners/dynamicCampaignContent'; +import { CloseChoices } from '@src/domain/CloseChoices'; +import { CloseEvent } from '@src/tracking/events/CloseEvent'; +import { DynamicContent } from '@src/utils/DynamicContent/DynamicContent'; +import { Tracker } from '@src/tracking/Tracker'; +import { TrackerStub } from '@test/fixtures/TrackerStub'; +import { TrackerSpy } from '@test/fixtures/TrackerSpy'; +import { FallbackBannerSubmitEvent } from '@src/tracking/events/FallbackBannerSubmitEvent'; + +const translator = ( key: string ): string => key; + +describe( 'FallbackBanner.vue', () => { + + beforeEach( () => { + vi.useFakeTimers(); + } ); + + afterEach( () => { + vi.useRealTimers(); + } ); + + const getWrapperAtWidth = ( width: number, dynamicContent: DynamicContent = null, tracker: Tracker = null ): VueWrapper => { + Object.defineProperty( window, 'innerWidth', { writable: true, configurable: true, value: width } ); + return mount( FallbackBanner, { + props: { + bannerState: BannerStates.Pending, + useOfFundsContent, + donationLink: 'https://spenden.wikimedia.de' + }, + global: { + mocks: { + $translate: translator + }, + provide: { + translator: { translate: translator }, + dynamicCampaignText: dynamicContent ?? newDynamicContent(), + tracker: tracker ?? new TrackerStub() + } + } + } ); + }; + + it( 'shows the small banner under 800px', () => { + const wrapper = getWrapperAtWidth( 799 ); + + expect( wrapper.find( '.wmde-banner-fallback-small' ).exists() ).toBeTruthy(); + expect( wrapper.find( '.wmde-banner-fallback-large' ).exists() ).toBeFalsy(); + } ); + + it( 'shows the large banner over 800px', () => { + const wrapper = getWrapperAtWidth( 800 ); + + expect( wrapper.find( '.wmde-banner-fallback-small' ).exists() ).toBeFalsy(); + expect( wrapper.find( '.wmde-banner-fallback-large' ).exists() ).toBeTruthy(); + } ); + + it( 'emits the banner close event', async () => { + const wrapper = getWrapperAtWidth( 799 ); + + await wrapper.find( '.wmde-banner-close' ).trigger( 'click' ); + + expect( wrapper.emitted( 'bannerClosed' ).length ).toStrictEqual( 1 ); + expect( wrapper.emitted( 'bannerClosed' )[ 0 ][ 0 ] ).toStrictEqual( new CloseEvent( 'FallbackBanner', CloseChoices.Close ) ); + } ); + + it( 'plays the slideshow when the banner becomes visible', async () => { + const wrapper = getWrapperAtWidth( 799 ); + + await wrapper.setProps( { bannerState: BannerStates.Visible } ); + await vi.runOnlyPendingTimersAsync(); + + expect( wrapper.find( '.wmde-banner-slider--playing' ).exists() ).toBeTruthy(); + } ); + + it( 'shows the animated text in the message', async () => { + const dynamicContent = newDynamicContent(); + dynamicContent.visitorsVsDonorsSentence = 'Visitors vs donors sentence'; + const wrapper = getWrapperAtWidth( 800, dynamicContent ); + + expect( wrapper.find( '.wmde-banner-message .wmde-banner-text-animated-highlight' ).exists() ).toBeTruthy(); + } ); + + it( 'shows the use of funds from small banner', async () => { + const wrapper = getWrapperAtWidth( 799 ); + + await wrapper.find( '.wmde-banner-fallback-small .wmde-banner-fallback-usage-link' ).trigger( 'click' ); + + expect( wrapper.find( '.banner-modal' ).classes() ).toContain( 'is-visible' ); + } ); + + it( 'hides the use of funds from small banner', async () => { + const wrapper = getWrapperAtWidth( 799 ); + + await wrapper.find( '.wmde-banner-fallback-small .wmde-banner-fallback-usage-link' ).trigger( 'click' ); + await wrapper.find( '.banner-modal-close-link' ).trigger( 'click' ); + + expect( wrapper.find( '.banner-modal' ).classes() ).not.toContain( 'is-visible' ); + } ); + + it( 'shows the use of funds from large banner', async () => { + const wrapper = getWrapperAtWidth( 800 ); + + await wrapper.find( '.wmde-banner-fallback-large .wmde-banner-fallback-usage-link' ).trigger( 'click' ); + + expect( wrapper.find( '.banner-modal' ).classes() ).toContain( 'is-visible' ); + } ); + + it( 'hides the use of funds from large banner', async () => { + const wrapper = getWrapperAtWidth( 800 ); + + await wrapper.find( '.wmde-banner-fallback-large .wmde-banner-fallback-usage-link' ).trigger( 'click' ); + await wrapper.find( '.banner-modal-close-link' ).trigger( 'click' ); + + expect( wrapper.find( '.banner-modal' ).classes() ).not.toContain( 'is-visible' ); + } ); + + it( 'submits from large banner', async () => { + const location = { href: '' }; + Object.defineProperty( window, 'location', { writable: true, configurable: true, value: location } ); + const tracker = new TrackerSpy(); + const wrapper = getWrapperAtWidth( 800, null, tracker ); + + await wrapper.find( '.wmde-banner-fallback-large .wmde-banner-fallback-button' ).trigger( 'click' ); + + expect( tracker.hasTrackedEvent( FallbackBannerSubmitEvent.EVENT_NAME ) ); + expect( location.href ).toStrictEqual( 'https://spenden.wikimedia.de' ); + } ); + + it( 'submits from small banner', async () => { + const location = { href: '' }; + Object.defineProperty( window, 'location', { writable: true, configurable: true, value: location } ); + const tracker = new TrackerSpy(); + const wrapper = getWrapperAtWidth( 799, null, tracker ); + + await wrapper.find( '.wmde-banner-fallback-small .wmde-banner-fallback-button' ).trigger( 'click' ); + + expect( tracker.hasTrackedEvent( FallbackBannerSubmitEvent.EVENT_NAME ) ); + expect( location.href ).toStrictEqual( 'https://spenden.wikimedia.de' ); + } ); +} ); diff --git a/test/components/BannerConductor/FallbackBannerConductor.spec.ts b/test/components/BannerConductor/FallbackBannerConductor.spec.ts new file mode 100644 index 000000000..b0952531e --- /dev/null +++ b/test/components/BannerConductor/FallbackBannerConductor.spec.ts @@ -0,0 +1,280 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { mount, VueWrapper } from '@vue/test-utils'; +import BannerConductor from '@src/components/BannerConductor/FallbackBannerConductor.vue'; +import { PageStub } from '@test/fixtures/PageStub'; +import { ResizeHandlerStub } from '@test/fixtures/ResizeHandlerStub'; +import { ImpressionCountStub } from '@test/fixtures/ImpressionCountStub'; +import { defineComponent, markRaw, nextTick } from 'vue'; +import { newBannerStateMachine } from '@src/components/BannerConductor/StateMachine/BannerStateMachine'; +import { BannerStateMachineSpy } from '@test/fixtures/BannerStateMachineSpy'; +import { BannerStates } from '@src/components/BannerConductor/StateMachine/BannerStates'; +import { Page } from '@src/page/Page'; +import { BannerNotShownReasons } from '@src/page/BannerNotShownReasons'; +import { TrackerStub } from '@test/fixtures/TrackerStub'; +import { ReactiveProperty } from '@src/domain/StateMachine/ReactiveProperty'; +import { BannerState } from '@src/components/BannerConductor/StateMachine/states/BannerState'; +import { CloseEvent } from '@src/tracking/events/CloseEvent'; +import { ResizeHandler } from '@src/utils/ResizeHandler'; + +vi.mock( '@src/components/BannerConductor/StateMachine/BannerStateMachine', async () => { + const actual = await vi.importActual( '@src/components/BannerConductor/StateMachine/BannerStateMachine' ); + return { + // @ts-ignore + ...actual, + newBannerStateMachine: vi.fn() + }; +} ); + +describe( 'FallbackBannerConductor.vue', () => { + + let stateMachineSpy: BannerStateMachineSpy; + + const getBannerOptions = ( containerClass: string ): any => { + return { + props: { + bannerState: String + }, + emits: [ + 'bannerClosed', + 'bannerContentChanged' + ], + methods: { + onClose(): void { + this.$emit( 'bannerClosed', new CloseEvent( 'MainBanner', 'closed' ) ); + } + }, + template: `
+ Hello, world! + + +
` + }; + }; + + async function getShownBannerWrapper( page: Page|null = null, resizeHandler: ResizeHandler|null = null, bannerWidth: number = 800 ): Promise> { + const banner = defineComponent( getBannerOptions( 'test-banner' ) ); + const fallbackBanner = defineComponent( getBannerOptions( 'test-fallback-banner' ) ); + + const wrapper = mount( BannerConductor, { + props: { + page: page ?? new PageStub(), + bannerConfig: { delay: 42, transitionDuration: 5 }, + resizeHandler: resizeHandler ?? new ResizeHandlerStub(), + banner: markRaw( banner ), + fallbackBanner: markRaw( fallbackBanner ), + minWidthForMainBanner: 800, + impressionCount: new ImpressionCountStub() + }, + global: { + provide: { + tracker: new TrackerStub() + } + } + } ); + + Object.defineProperty( wrapper.element, 'offsetWidth', { value: bannerWidth, writable: true, configurable: true } ); + + // We need to await a few times and run out timers because the banner display flow is asynchronous + await nextTick(); + await nextTick(); + await nextTick(); + await vi.runAllTimersAsync(); + + return Promise.resolve( wrapper ); + } + + beforeEach( () => { + vi.mocked( newBannerStateMachine ).mockImplementation( ( stateRef: ReactiveProperty ) => { + stateMachineSpy = new BannerStateMachineSpy( stateRef ); + return stateMachineSpy; + } ); + vi.useFakeTimers(); + } ); + + afterEach( () => { + vi.restoreAllMocks(); + } ); + + it( 'shows main banner when over min width for main banner', async () => { + const wrapper = await getShownBannerWrapper( null, null, 800 ); + + expect( wrapper.find( '.test-banner' ).exists() ).toBeTruthy(); + expect( wrapper.find( '.test-fallback-banner' ).exists() ).toBeFalsy(); + } ); + + it( 'shows fallback banner when under min width for main banner', async () => { + const wrapper = await getShownBannerWrapper( null, null, 799 ); + + expect( wrapper.find( '.test-banner' ).exists() ).toBeFalsy(); + expect( wrapper.find( '.test-fallback-banner' ).exists() ).toBeTruthy(); + } ); + + it( 'shows main banner when main banner has no issue', async () => { + const page = new PageStub(); + page.getReasonToNotShowBanner = vi.fn().mockReturnValueOnce( null ); + const wrapper = await getShownBannerWrapper( null, null, 800 ); + + expect( wrapper.find( '.test-banner' ).exists() ).toBeTruthy(); + expect( wrapper.find( '.test-fallback-banner' ).exists() ).toBeFalsy(); + } ); + + it( 'shows fallback banner when main banner has size issue', async () => { + const page = new PageStub(); + page.getReasonToNotShowBanner = vi.fn().mockReturnValueOnce( BannerNotShownReasons.SizeIssue ) + .mockReturnValueOnce( null ); + const wrapper = await getShownBannerWrapper( page, null, 800 ); + + expect( wrapper.find( '.test-banner' ).exists() ).toBeFalsy(); + expect( wrapper.find( '.test-fallback-banner' ).exists() ).toBeTruthy(); + } ); + + it( 'shows neither banner when there is a different reason to not show banner and over min width for main banner', async () => { + const page = new PageStub(); + page.getReasonToNotShowBanner = vi.fn().mockReturnValue( BannerNotShownReasons.UserInteraction ); + + const wrapper = await getShownBannerWrapper( page, null, 800 ); + + expect( stateMachineSpy.statesCalled ).toEqual( [ + BannerStates.Pending, + BannerStates.NotShown + ] ); + + expect( wrapper.classes() ).toContain( BannerStates.NotShown ); + } ); + + it( 'shows neither banner when there is a different reason to not show banner and under min width for main banner', async () => { + const page = new PageStub(); + page.getReasonToNotShowBanner = vi.fn().mockReturnValue( BannerNotShownReasons.UserInteraction ); + + const wrapper = await getShownBannerWrapper( page, null, 799 ); + + expect( stateMachineSpy.statesCalled ).toEqual( [ + BannerStates.Pending, + BannerStates.NotShown + ] ); + + expect( wrapper.classes() ).toContain( BannerStates.NotShown ); + } ); + + it( 'runs through correct state flow on mounted', async () => { + const wrapper = await getShownBannerWrapper(); + + expect( stateMachineSpy.statesCalled ).toEqual( [ + BannerStates.Pending, + BannerStates.Showing, + BannerStates.Visible + ] ); + + expect( wrapper.classes() ).toContain( 'wmde-banner--visible' ); + } ); + + it( 'runs through correct state flow on mounted when there is a reason to not show banner', async () => { + const page = new PageStub(); + page.getReasonToNotShowBanner = vi.fn().mockReturnValue( BannerNotShownReasons.SizeIssue ); + + const wrapper = await getShownBannerWrapper( page ); + + expect( stateMachineSpy.statesCalled ).toEqual( [ + BannerStates.Pending, + BannerStates.NotShown + ] ); + + expect( wrapper.classes() ).toContain( BannerStates.NotShown ); + } ); + + it( 'passes the banner height to the page on load', async () => { + const page = new PageStub(); + page.setSpace = vi.fn(); + + await getShownBannerWrapper( page ); + + // This will be 0 because jsdom doesn't set this value + expect( page.setSpace ).toHaveBeenCalledWith( 0 ); + } ); + + it( 'shows the page animated with a transition duration', async () => { + const page = new PageStub(); + page.setAnimated = vi.fn().mockReturnValue( page ); + page.setTransitionDuration = vi.fn().mockReturnValue( page ); + page.showBanner = vi.fn(); + + await getShownBannerWrapper( page ); + + expect( page.setAnimated ).toHaveBeenCalled(); + expect( page.setTransitionDuration ).toHaveBeenCalledWith( 5 ); + expect( page.showBanner ).toHaveBeenCalled(); + } ); + + it( 'updates the page with a new height on window resize', async () => { + const page = new PageStub(); + page.setSpace = vi.fn().mockReturnValue( page ); + const resizeHandler = new ResizeHandlerStub(); + const wrapper = await getShownBannerWrapper( page, resizeHandler ); + + Object.defineProperty( wrapper.element, 'offsetHeight', { value: 100, writable: true, configurable: true } ); + resizeHandler.callOnResize(); + + Object.defineProperty( wrapper.element, 'offsetHeight', { value: 42, writable: true, configurable: true } ); + resizeHandler.callOnResize(); + + expect( page.setSpace ).toHaveBeenCalledTimes( 3 ); + expect( page.setSpace ).toHaveBeenCalledWith( 100 ); + expect( page.setSpace ).toHaveBeenCalledWith( 42 ); + } ); + + it( 'updates the page with a new height on content change', async () => { + const page = new PageStub(); + page.setSpace = vi.fn().mockReturnValue( page ); + const wrapper = await getShownBannerWrapper( page ); + + Object.defineProperty( wrapper.element, 'offsetHeight', { value: 100, writable: true, configurable: true } ); + await wrapper.find( '.emit-banner-content-changed' ).trigger( 'click' ); + + Object.defineProperty( wrapper.element, 'offsetHeight', { value: 42, writable: true, configurable: true } ); + await wrapper.find( '.emit-banner-content-changed' ).trigger( 'click' ); + + expect( page.setSpace ).toHaveBeenCalledTimes( 3 ); + expect( page.setSpace ).toHaveBeenCalledWith( 100 ); + expect( page.setSpace ).toHaveBeenCalledWith( 42 ); + } ); + + it( 'moves to closed state when donor closes banner', async () => { + const wrapper = await getShownBannerWrapper(); + await wrapper.find( '.emit-banner-closed' ).trigger( 'click' ); + + expect( stateMachineSpy.statesCalled ).toEqual( [ + BannerStates.Pending, + BannerStates.Showing, + BannerStates.Visible, + BannerStates.Closed + ] ); + + expect( wrapper.classes() ).toContain( BannerStates.Closed ); + } ); + + it( 'asks the page to set the close cookie when the donor closes banner', async () => { + const page = new PageStub(); + page.setCloseCookieIfNecessary = vi.fn().mockReturnValue( page ); + const wrapper = await getShownBannerWrapper( page ); + await wrapper.find( '.emit-banner-closed' ).trigger( 'click' ); + + expect( page.setCloseCookieIfNecessary ).toHaveBeenCalledWith( new CloseEvent( 'MainBanner', 'closed' ) ); + } ); + + it( 'moves to closed state when an page event that should hide the banner happens', async () => { + const page = new PageStub(); + const wrapper = await getShownBannerWrapper( page ); + + await page.hideBannerCallback(); + + expect( stateMachineSpy.statesCalled ).toEqual( [ + BannerStates.Pending, + BannerStates.Showing, + BannerStates.Visible, + BannerStates.Closed + ] ); + + expect( wrapper.classes() ).toContain( BannerStates.Closed ); + } ); + +} ); diff --git a/test/components/BannerConductor/StateMachine/states/VisibleState.spec.ts b/test/components/BannerConductor/StateMachine/states/VisibleState.spec.ts index 8c7ac29f5..ce9dcde59 100644 --- a/test/components/BannerConductor/StateMachine/states/VisibleState.spec.ts +++ b/test/components/BannerConductor/StateMachine/states/VisibleState.spec.ts @@ -15,7 +15,7 @@ describe( 'VisibleState', function () { it( 'sets banner size on resize', () => { const page = new PageStub(); page.setSpace = vitest.fn( () => page ); - const visibleState = new VisibleState( page, new ImpressionCountStub(), tracker ); + const visibleState = new VisibleState( 'Page', page, new ImpressionCountStub(), tracker ); visibleState.onResize( 42 ); @@ -26,7 +26,7 @@ describe( 'VisibleState', function () { it( 'sets banner size on content change', () => { const page = new PageStub(); page.setSpace = vitest.fn( () => page ); - const visibleState = new VisibleState( page, new ImpressionCountStub(), tracker ); + const visibleState = new VisibleState( 'Page', page, new ImpressionCountStub(), tracker ); visibleState.onContentChanged( 42 ); @@ -35,8 +35,8 @@ describe( 'VisibleState', function () { } ); it( 'fires shown event on enter', async () => { - const visibleState = new VisibleState( new PageStub(), new ImpressionCountStub(), tracker ); - const shownEvent = new ShownEvent(); + const visibleState = new VisibleState( 'Page', new PageStub(), new ImpressionCountStub(), tracker ); + const shownEvent = new ShownEvent( 'Page' ); await visibleState.enter(); @@ -47,7 +47,7 @@ describe( 'VisibleState', function () { it( 'increases banner impression count on enter', async () => { const impressionCountStub = new ImpressionCountStub(); impressionCountStub.incrementImpressionCounts = vitest.fn(); - const visibleState = new VisibleState( new PageStub(), impressionCountStub, tracker ); + const visibleState = new VisibleState( 'Page', new PageStub(), impressionCountStub, tracker ); await visibleState.enter(); diff --git a/test/components/DonationForm/StepControllers/SubmittableMainDonationFormUpgradeOptions.spec.ts b/test/components/DonationForm/StepControllers/SubmittableMainDonationFormUpgradeOptions.spec.ts index fc5829bd6..e429c00af 100644 --- a/test/components/DonationForm/StepControllers/SubmittableMainDonationFormUpgradeOptions.spec.ts +++ b/test/components/DonationForm/StepControllers/SubmittableMainDonationFormUpgradeOptions.spec.ts @@ -35,7 +35,7 @@ describe( 'SubmittableMainDonationFormUpgradeOptions', () => { [ PaymentMethods.BANK_TRANSFER.value, '12', 'monthly' ], [ PaymentMethods.CREDIT_CARD.value, '12', 'monthly' ], [ PaymentMethods.DIRECT_DEBIT.value, '12', 'monthly' ] - ] )( 'goes to correct step when payment method is not Sofort and amount is in upgrade range', async ( paymentMethod: string, selectedAmount: string, expectedPage: string ) => { + ] )( 'goes to correct step when payment method is not Sofort', async ( paymentMethod: string, selectedAmount: string, expectedPage: string ) => { formModel.paymentMethod.value = paymentMethod; formModel.interval.value = Intervals.ONCE.value; formModel.selectedAmount.value = selectedAmount; diff --git a/test/components/FallbackBanner/FallbackButton.spec.ts b/test/components/FallbackBanner/FallbackButton.spec.ts new file mode 100644 index 000000000..ac08cd9d6 --- /dev/null +++ b/test/components/FallbackBanner/FallbackButton.spec.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from 'vitest'; +import { shallowMount } from '@vue/test-utils'; +import FallbackButton from '@src/components/FallbackBanner/FallbackButton.vue'; + +describe( 'FallbackButton.vue', () => { + it( 'emits event on click', async () => { + const wrapper = shallowMount( FallbackButton, { + global: { + mocks: { + $translate: ( key: string ) => key + } + } + } ); + + await wrapper.trigger( 'click' ); + + expect( wrapper.emitted( 'button-clicked' ).length ).toStrictEqual( 1 ); + } ); +} ); diff --git a/test/components/FallbackBanner/LargeFooter.spec.ts b/test/components/FallbackBanner/LargeFooter.spec.ts new file mode 100644 index 000000000..0597f724a --- /dev/null +++ b/test/components/FallbackBanner/LargeFooter.spec.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from 'vitest'; +import { mount } from '@vue/test-utils'; +import LargeFooter from '@src/components/FallbackBanner/LargeFooter.vue'; + +describe( 'LargeFooter.vue', () => { + it( 'emits show use of funds event on use of funds link click', async () => { + const wrapper = mount( LargeFooter, { + global: { + mocks: { + $translate: ( key: string ) => key + } + } + } ); + + await wrapper.find( '.wmde-banner-fallback-usage-link' ).trigger( 'click' ); + + expect( wrapper.emitted( 'use-of-funds-button-clicked' ).length ).toStrictEqual( 1 ); + } ); + + it( 'emits submit event on submit button click', async () => { + const wrapper = mount( LargeFooter, { + global: { + mocks: { + $translate: ( key: string ) => key + } + } + } ); + + await wrapper.find( '.wmde-banner-fallback-button' ).trigger( 'click' ); + + expect( wrapper.emitted( 'submit-button-clicked' ).length ).toStrictEqual( 1 ); + } ); +} ); diff --git a/test/components/FallbackBanner/SmallFooter.spec.ts b/test/components/FallbackBanner/SmallFooter.spec.ts new file mode 100644 index 000000000..d6e5a6aa8 --- /dev/null +++ b/test/components/FallbackBanner/SmallFooter.spec.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from 'vitest'; +import { mount } from '@vue/test-utils'; +import SmallFooter from '@src/components/FallbackBanner/SmallFooter.vue'; + +describe( 'SmallFooter.vue', () => { + it( 'emits show use of funds event on use of funds link click', async () => { + const wrapper = mount( SmallFooter, { + props: { + slideIndex: 0, + slideCount: 5 + }, + global: { + mocks: { + $translate: ( key: string ) => key + } + } + } ); + + await wrapper.find( '.wmde-banner-fallback-usage-link' ).trigger( 'click' ); + + expect( wrapper.emitted( 'use-of-funds-button-clicked' ).length ).toStrictEqual( 1 ); + } ); +} ); diff --git a/test/components/FallbackBanner/UseOfFundsLink.spec.ts b/test/components/FallbackBanner/UseOfFundsLink.spec.ts new file mode 100644 index 000000000..380ac1bb7 --- /dev/null +++ b/test/components/FallbackBanner/UseOfFundsLink.spec.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from 'vitest'; +import { mount } from '@vue/test-utils'; +import UseOfFundsLink from '@src/components/FallbackBanner/UseOfFundsLink.vue'; + +describe( 'UseOfFundsLink.vue', () => { + it( 'emits show use of funds event on use of funds link click', async () => { + const wrapper = mount( UseOfFundsLink, { + global: { + mocks: { + $translate: ( key: string ) => key + } + } + } ); + + await wrapper.trigger( 'click' ); + + expect( wrapper.emitted( 'button-clicked' ).length ).toStrictEqual( 1 ); + } ); +} ); diff --git a/test/components/Slider/KeenSlider.spec.ts b/test/components/Slider/KeenSlider.spec.ts index 9d68313aa..bd510d4fb 100644 --- a/test/components/Slider/KeenSlider.spec.ts +++ b/test/components/Slider/KeenSlider.spec.ts @@ -56,6 +56,17 @@ describe( 'KeenSlider', () => { expect( wrapper.find( '.wmde-banner-slide:nth-child(2) .wmde-banner-slide--current' ).exists() ).toBeTruthy(); } ); + it.todo( 'should emit when the slide changes', async () => { + const wrapper = getWrapper(); + await wrapper.setProps( { play: true } ); + + await wrapper.find( '.wmde-banner-slider-navigation-next' ).trigger( 'click' ); + await vi.advanceTimersByTimeAsync( 200 ); + + expect( wrapper.emitted( 'slide-changed' ).length ).toBe( 1 ); + expect( wrapper.emitted( 'slide-changed' )[ 0 ][ 0 ] ).toBe( 1 ); + } ); + it( 'should start after a delay if one is passed', async () => { const wrapper = getWrapper(); @@ -93,6 +104,18 @@ describe( 'KeenSlider', () => { expect( wrapper.find( '.wmde-banner-slider-navigation-next' ).exists() ).toBe( true ); } ); + it( 'should render pagination', async function () { + const wrapper = getWrapper(); + + await wrapper.setProps( { withPagination: false } ); + + expect( wrapper.find( '.wmde-banner-slider-pagination' ).exists() ).toBeFalsy(); + + await wrapper.setProps( { withPagination: true } ); + + expect( wrapper.find( '.wmde-banner-slider-pagination' ).exists() ).toBeTruthy(); + } ); + it( 'should stop the auto play when the slider is clicked', async () => { const wrapper = getWrapper(); diff --git a/test/features/BannerContent.ts b/test/features/BannerContent.ts index 9ff634c73..d9b914fcb 100644 --- a/test/features/BannerContent.ts +++ b/test/features/BannerContent.ts @@ -16,7 +16,7 @@ const expectSlideShowStopsOnFormInteraction = async ( wrapper: VueWrapper ) await wrapper.setProps( { bannerState: BannerStates.Visible } ); await wrapper.find( '.wmde-banner-form' ).trigger( 'click' ); - await vi.runAllTimersAsync(); + await vi.runOnlyPendingTimersAsync(); expect( wrapper.find( '.wmde-banner-slider--stopped' ).exists() ).toBeTruthy(); }; @@ -65,6 +65,46 @@ const expectShowsAnimatedVisitorsVsDonorsSentenceInSlideShow = async ( getWrappe expect( wrapper.find( '.wmde-banner-slider .wmde-banner-text-animated-highlight' ).exists() ).toBeTruthy(); }; +const expectShowsLiveDateAndTimeInMessage = async ( getWrapper: ( dynamicContent: DynamicContent ) => VueWrapper ): Promise => { + Object.defineProperty( window, 'innerWidth', { writable: true, configurable: true, value: 1301 } ); + const dynamicContent = newDynamicContent(); + dynamicContent.getCurrentDateAndTime = vi.fn().mockReturnValueOnce( 'Initial Date and Time' ) + .mockReturnValueOnce( 'Second Date and Time' ) + .mockReturnValueOnce( 'Third Date and Time' ); + + const wrapper = getWrapper( dynamicContent ); + + expect( wrapper.find( '.wmde-banner-message' ).text() ).toContain( 'Initial Date and Time' ); + + await vi.advanceTimersByTimeAsync( 1000 ); + + expect( wrapper.find( '.wmde-banner-message' ).text() ).toContain( 'Second Date and Time' ); + + await vi.advanceTimersByTimeAsync( 1000 ); + + expect( wrapper.find( '.wmde-banner-message' ).text() ).toContain( 'Third Date and Time' ); +}; + +const expectShowsLiveDateAndTimeInSlideshow = async ( getWrapper: ( dynamicContent: DynamicContent ) => VueWrapper ): Promise => { + Object.defineProperty( window, 'innerWidth', { writable: true, configurable: true, value: 1300 } ); + const dynamicContent = newDynamicContent(); + dynamicContent.getCurrentDateAndTime = vi.fn().mockReturnValueOnce( 'Initial Date and Time' ) + .mockReturnValueOnce( 'Second Date and Time' ) + .mockReturnValueOnce( 'Third Date and Time' ); + + const wrapper = getWrapper( dynamicContent ); + + expect( wrapper.find( '.wmde-banner-slider' ).text() ).toContain( 'Initial Date and Time' ); + + await vi.advanceTimersByTimeAsync( 1000 ); + + expect( wrapper.find( '.wmde-banner-slider' ).text() ).toContain( 'Second Date and Time' ); + + await vi.advanceTimersByTimeAsync( 1000 ); + + expect( wrapper.find( '.wmde-banner-slider' ).text() ).toContain( 'Third Date and Time' ); +}; + export const bannerContentFeatures: Record ) => Promise> = { expectSlideShowPlaysWhenBecomesVisible, expectSlideShowStopsOnFormInteraction @@ -81,3 +121,8 @@ export const bannerContentAnimatedTextFeatures: Record VueWrapper ) => Promise> = { + expectShowsLiveDateAndTimeInMessage, + expectShowsLiveDateAndTimeInSlideshow +}; diff --git a/test/integration/tracking/TrackerWPDE.spec.ts b/test/integration/tracking/TrackerWPDE.spec.ts index a445034f9..2b681c628 100644 --- a/test/integration/tracking/TrackerWPDE.spec.ts +++ b/test/integration/tracking/TrackerWPDE.spec.ts @@ -32,7 +32,7 @@ describe( 'TrackerWPDE', function () { test.each( [ [ new CloseEvent( 'SoftClose', 'close' ), 'banner-closed-close' ], - [ new ShownEvent(), 'banner-shown' ], + [ new ShownEvent( 'Page' ), 'banner-shown' ], [ new CustomAmountChangedEvent( 'increased' ), 'increased-amount' ], [ new CustomAmountChangedEvent( 'decreased' ), 'decreased-amount' ], [ new FormStepShownEvent( 'UpgradeToYearlyForm' ), 'form-step-shown-UpgradeToYearlyForm' ], diff --git a/test/integration/utils/Tracking/LegacyEventTracking/mapShownEvent.spec.ts b/test/integration/utils/Tracking/LegacyEventTracking/mapShownEvent.spec.ts new file mode 100644 index 000000000..96162ec3a --- /dev/null +++ b/test/integration/utils/Tracking/LegacyEventTracking/mapShownEvent.spec.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from 'vitest'; + +import { mapShownEvent } from '@src/tracking/LegacyEventTracking/mapShownEvent'; +import { ShownEvent } from '@src/tracking/events/ShownEvent'; +import { WMDESizeIssueEvent } from '@src/tracking/WPORG/WMDEBannerSizeIssue'; +import { WMDELegacyBannerEvent } from '@src/tracking/WPORG/WMDELegacyBannerEvent'; + +describe( 'mapShownEvent', () => { + it( 'maps size issues to legacy SizeIssueEvent', () => { + const legacyEvent = mapShownEvent( new ShownEvent( 'FallbackBanner' ) ); + + expect( legacyEvent ).toStrictEqual( new WMDESizeIssueEvent( + 'fallback-banner-shown', + { + bannerHeight: 0, + viewportWidth: 1024, + viewportHeight: 768 + }, + 1 + ) ); + } ); + + it( 'maps other reasons to legacy event without tracking', () => { + const legacyEvent = mapShownEvent( new ShownEvent( 'Page' ) ); + + expect( legacyEvent ).toStrictEqual( + new WMDELegacyBannerEvent( 'untracked-not-shown-event', 0 ) + ); + } ); +} ); diff --git a/test/unit/createFallbackDonationLink.spec.ts b/test/unit/createFallbackDonationLink.spec.ts new file mode 100644 index 000000000..fb4ba565b --- /dev/null +++ b/test/unit/createFallbackDonationLink.spec.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from 'vitest'; +import { ImpressionCountStub } from '@test/fixtures/ImpressionCountStub'; +import { createFallbackDonationLink } from '@src/createFallbackDonationLink'; + +describe( 'createFallbackDonationLink', () => { + it( 'should create fallback donation link', () => { + const extraParameters = { locale: 'de_DE', ast: '1' }; + const ctrlLink = createFallbackDonationLink( { campaign: 'C1', keyword: 'banner-ctrl' }, new ImpressionCountStub(), extraParameters ); + const varLink = createFallbackDonationLink( { campaign: 'C1', keyword: 'banner-var' }, new ImpressionCountStub(), extraParameters ); + const expected = 'https://spenden.wikimedia.de?piwik_kwd=banner-mini&piwik_campaign=C1&impCount=1&bImpCount=1&locale=de_DE&ast=1'; + + expect( ctrlLink ).toStrictEqual( expected ); + expect( varLink ).toStrictEqual( expected ); + } ); +} );