From 591c9bc98cbf6c048f808b7b1e6b14c21f8d55ee Mon Sep 17 00:00:00 2001 From: Bart Enkelaar Date: Thu, 29 Apr 2021 22:18:58 +0200 Subject: [PATCH] #12781 Add a unified component for small modals (#12987) * 12781 - Add common component for small modal windows * 12781 - Fix drop cap layout and hide footer * 12781 - Implement drop cap reached and won challenge modals * 12781 - Couple small fixes * 12781 - Remove added drop cap modal test convenience * Less assumptions in small-modal component * 12781 - Fix already migrated small modals * Figured out how to properly test Bootstrap modals * 12781 - Add unit test for success modal * 12781 - Some more steps towards integrating successModal.vue * 12781 - Use small modal mixin in success modal * 12781 - Optimize templating in successModal * 12781 - Revert AppFooter additions * 12781 - Add test case and remove unnecessary method --- website/client/src/assets/scss/mixins.scss | 56 +++ .../achievements/dropCapReached.vue | 206 +++-------- .../src/components/achievements/levelUp.vue | 187 ++-------- .../components/achievements/wonChallenge.vue | 180 ++-------- .../src/components/payments/successModal.vue | 333 ++++++++---------- .../src/components/ui/modal/smallModal.vue | 56 +++ .../src/components/ui/modal/starred.vue | 40 +++ .../src/components/ui/modal/your-rewards.vue | 51 +++ .../components/achievements/levelUp.spec.js | 79 ++--- .../components/payments/successModal.spec.js | 141 ++++++++ .../components/ui/modal/smallModal.spec.js | 33 ++ 11 files changed, 668 insertions(+), 694 deletions(-) create mode 100644 website/client/src/components/ui/modal/smallModal.vue create mode 100644 website/client/src/components/ui/modal/starred.vue create mode 100644 website/client/src/components/ui/modal/your-rewards.vue create mode 100644 website/client/tests/unit/components/payments/successModal.spec.js create mode 100644 website/client/tests/unit/components/ui/modal/smallModal.spec.js diff --git a/website/client/src/assets/scss/mixins.scss b/website/client/src/assets/scss/mixins.scss index 561be306ed..b9efbd4b72 100644 --- a/website/client/src/assets/scss/mixins.scss +++ b/website/client/src/assets/scss/mixins.scss @@ -1,3 +1,5 @@ +@import '~@/assets/scss/colors.scss'; + @mixin centeredModal() { display: flex; justify-content: center; @@ -6,4 +8,58 @@ header, footer { border: 0; } +} + +@mixin smallModal() { + .modal-content { + border-radius: 8px; + box-shadow: 0 14px 28px 0 rgba($black, 0.24), 0 10px 10px 0 rgba($black, 0.28); + } + + @media (min-width: 576px) { + max-width: 20.625rem; + } + + header { + padding: 0; + border: none; + + h5 { + margin: 2rem auto 1rem; + color: $purple-200; + } + + button { + position: absolute; + right: 18px; + top: 12px; + font-weight: 100; + } + } + + footer { + padding: 0; + border: none; + + button { + margin: 0 auto 2rem; + } + } + + .greyed { + background-color: $gray-700; + } + + .modal-body { + padding: 0; + } + + .modal-text { + margin: 1.5rem; + min-height: auto !important; + } + + footer.greyed { + padding: 0 1.5rem 1rem; + } } \ No newline at end of file diff --git a/website/client/src/components/achievements/dropCapReached.vue b/website/client/src/components/achievements/dropCapReached.vue index a804fd78fa..b9a1072c77 100644 --- a/website/client/src/components/achievements/dropCapReached.vue +++ b/website/client/src/components/achievements/dropCapReached.vue @@ -1,135 +1,63 @@ - - diff --git a/website/client/src/components/achievements/wonChallenge.vue b/website/client/src/components/achievements/wonChallenge.vue index 53ab170d63..4f7375299f 100644 --- a/website/client/src/components/achievements/wonChallenge.vue +++ b/website/client/src/components/achievements/wonChallenge.vue @@ -1,177 +1,72 @@ - - diff --git a/website/client/src/components/payments/successModal.vue b/website/client/src/components/payments/successModal.vue index c816a3300d..fd1e0a91b0 100644 --- a/website/client/src/components/payments/successModal.vue +++ b/website/client/src/components/payments/successModal.vue @@ -1,11 +1,12 @@ @@ -208,6 +122,10 @@ import checkIcon from '@/assets/svg/check.svg'; import gemIcon from '@/assets/svg/gem.svg'; import subscriptionBlocks from '@/../../common/script/content/subscriptionBlocks'; +function groupPlanCost (price, memberCount = 1) { + return price + 3 * (memberCount - 1); +} + export default { data () { return { @@ -215,35 +133,66 @@ export default { check: checkIcon, gem: gemIcon, }), - paymentData: {}, + isFromBalance: false, + gems: false, + boldSubTitle: false, + subTitle: '', + details: '', + renew: false, }; }, computed: { - groupPlanCost () { - const sub = this.paymentData.subscription; - const memberCount = this.paymentData.group.memberCount || 1; - return sub.price + 3 * (memberCount - 1); - }, - isFromBalance () { - return this.paymentData.paymentType === 'gift-gems-balance'; + title () { + return this.$t(this.isFromBalance ? 'success' : 'paymentSuccessful'); }, }, mounted () { this.$root.$on('habitica:payment-success', data => { - if (['subscription', 'groupPlan', 'gift-subscription'].indexOf(data.paymentType) !== -1) { - data.subscription = subscriptionBlocks[data.subscriptionKey || data.gift.subscription.key]; + const type = data.paymentType; + this.isFromBalance = type.includes('balance'); + this.gems = type.includes('gems'); + + if (this.gems) { + this.setGemData(type === 'gems', data); + } else { + this.setSubscriptionData(type, data); } - this.paymentData = data; this.$root.$emit('bv::show::modal', 'payments-success-modal'); }); }, beforeDestroy () { - this.paymentData = {}; this.$root.$off('habitica:payments-success'); }, methods: { + setGemData (boughtGems, data) { + this.subTitle = boughtGems ? this.$t('paymentYouReceived') + : this.$t('paymentYouSentGems', { name: data.giftReceiver }); + this.boldSubTitle = boughtGems; + this.details = boughtGems ? data.gemsBlock.gems : data.gift.gems.amount; + this.renew = false; + }, + setSubscriptionData (type, data) { + const { price, months } = subscriptionBlocks[ + data.subscriptionKey || data.gift.subscription.key + ]; + const gift = type.includes('gift'); + const groupPlan = type.includes('group'); + const subbed = type === 'subscription'; + const amount = groupPlan ? groupPlanCost(price, data.group.memberCount) : price; + + if (groupPlan) { + this.subTitle = this.$t(data.newGroup ? 'groupPlanCreated' + : 'groupPlanUpgraded', { groupName: data.group.name }); + } else { + this.subTitle = subbed ? this.$t('nowSubscribed') + : this.$t('paymentYouSentSubscription', { name: data.giftReceiver, months }); + } + + this.boldSubTitle = subbed; + this.details = gift ? '' : this.$t('paymentSubBilling', { amount, months }); + this.renew = !gift; + }, close () { - this.paymentData = {}; this.$root.$emit('bv::hide::modal', 'payments-success-modal'); }, }, diff --git a/website/client/src/components/ui/modal/smallModal.vue b/website/client/src/components/ui/modal/smallModal.vue new file mode 100644 index 0000000000..612dd06a20 --- /dev/null +++ b/website/client/src/components/ui/modal/smallModal.vue @@ -0,0 +1,56 @@ + + + + + + diff --git a/website/client/src/components/ui/modal/starred.vue b/website/client/src/components/ui/modal/starred.vue new file mode 100644 index 0000000000..93afd81914 --- /dev/null +++ b/website/client/src/components/ui/modal/starred.vue @@ -0,0 +1,40 @@ + + + + + diff --git a/website/client/src/components/ui/modal/your-rewards.vue b/website/client/src/components/ui/modal/your-rewards.vue new file mode 100644 index 0000000000..cd265a806a --- /dev/null +++ b/website/client/src/components/ui/modal/your-rewards.vue @@ -0,0 +1,51 @@ + + + + + diff --git a/website/client/tests/unit/components/achievements/levelUp.spec.js b/website/client/tests/unit/components/achievements/levelUp.spec.js index 1888f5cf68..fbf2a850c3 100644 --- a/website/client/tests/unit/components/achievements/levelUp.spec.js +++ b/website/client/tests/unit/components/achievements/levelUp.spec.js @@ -1,78 +1,61 @@ -import LevelUp from '@/components/achievements/levelUp.vue'; - -/* -// I couldn't get rendering to work for the modal, this is what I tried. -// Now the testing is done by overriding `this` for the exported computed -// functions directly. I intend to come back to this later to test it -// properly in a rendered component. - -import { mount, createLocalVue } from '@vue/test-utils'; -import BootstrapVue from 'bootstrap-vue'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; import Store from '@/libs/store'; +import LevelUp from '@/components/achievements/levelUp'; + const localVue = createLocalVue(); localVue.use(Store); -localVue.use(BootstrapVue); - -function createContainer () { - const container = document.createElement('div'); - document.body.appendChild(container); - return container; -} -*/ describe('LevelUp', () => { - function testFunction (name, level = 10) { - return LevelUp.computed[name].bind({ - user: { stats: { lvl: level } }, - $t: (...args) => args.map(JSON.stringify).join(' '), - }); - } - - /* - // More potential rendering code - let wrapper; - beforeEach(async () => { - wrapper = mount(LevelUp, { + function createWrapper (level = 10) { + const wrapper = shallowMount(LevelUp, { store: new Store({ - state: { user: { data: createUser() } }, - getters: {}, + state: { + user: { + data: { + stats: { + lvl: level, + buffs: {}, + }, + preferences: { hair: {} }, + items: { gear: { equipped: {} } }, + }, + }, + }, + getters: { 'members:hasClass': () => () => false }, actions: {}, }), - propsData: { - static: true, - visible: true, - }, localVue, - mocks: { $t: string => string }, - attachTo: createContainer(), + mocks: { $t: (...args) => args.map(JSON.stringify).join(' ') }, + stubs: ['b-modal'], }); - }); - */ + + return wrapper; + } it('displays the right level in the title', () => { - const title = testFunction('title', 12); + const wrapper = createWrapper(12); - expect(title()).to.equal('"reachedLevel" {"level":12}'); + expect(wrapper.vm.title).to.equal('"reachedLevel" {"level":12}'); }); it('does not display rewards for level 10', () => { - const displayRewardQuest = testFunction('displayRewardQuest', 10); + const wrapper = createWrapper(); - expect(displayRewardQuest()).to.be.false; + expect(wrapper.vm.displayRewardQuest).to.be.false; }); [15, 30, 40, 60].forEach(level => { it(`does display rewards for level ${level}`, () => { - const displayRewardQuest = testFunction('displayRewardQuest', level); + const wrapper = createWrapper(level); - expect(displayRewardQuest()).to.be.true; + expect(wrapper.vm.displayRewardQuest).to.be.true; }); }); it('generates the right test class for level 15', () => { - const questClass = testFunction('questClass', 15); + const wrapper = createWrapper(15); - expect(questClass()).to.equal('scroll inventory_quest_scroll_atom1'); + expect(wrapper.vm.questClass).to.equal('scroll inventory_quest_scroll_atom1'); }); }); diff --git a/website/client/tests/unit/components/payments/successModal.spec.js b/website/client/tests/unit/components/payments/successModal.spec.js new file mode 100644 index 0000000000..5beed18f4e --- /dev/null +++ b/website/client/tests/unit/components/payments/successModal.spec.js @@ -0,0 +1,141 @@ +import { shallowMount, createLocalVue } from '@vue/test-utils'; + +import Store from '@/libs/store'; + +import successModal from '@/components/payments/successModal'; + +const localVue = createLocalVue(); +localVue.use(Store); + +describe('Success modal', () => { + let wrapper; + + function firePaymentSuccess (data) { + wrapper.vm.$root.$emit('habitica:payment-success', data); + } + + beforeEach(async () => { + wrapper = shallowMount(successModal, { + store: new Store({ + state: {}, + getters: {}, + actions: {}, + }), + localVue, + mocks: { $t: (...args) => JSON.stringify(args) }, + stubs: { + 'b-modal': { + template: '
', + }, + }, + }); + }); + + it('Displays payment success when buying gems', () => { + firePaymentSuccess({ + paymentType: 'gems', + gemsBlock: { gems: 5 }, + }); + + expect(wrapper.find('h2').text()).to.equal('["paymentSuccessful"]'); + expect(wrapper.find('section :first-child').text()).to.equal('["paymentYouReceived"]'); + expect(wrapper.find('.details-block').text()).to.equal('5'); + expect(wrapper.find('footer').text()).to.equal('["giftSubscriptionText4"]'); + }); + + it('Displays success when gifting gems from balance', () => { + firePaymentSuccess({ + paymentType: 'gift-gems-balance', + giftReceiver: 'Lucky User', + gift: { gems: { amount: 3 } }, + }); + + expect(wrapper.find('h2').text()).to.equal('["success"]'); + expect(wrapper.find('section :first-child').text()).to.equal('["paymentYouSentGems",{"name":"Lucky User"}]'); + expect(wrapper.find('.details-block').text()).to.equal('3'); + }); + + it('Displays payment success when gifting gems through payment', () => { + firePaymentSuccess({ + paymentType: 'gift-gems', + giftReceiver: 'Lucky User', + gift: { gems: { amount: 7 } }, + }); + + expect(wrapper.find('h2').text()).to.equal('["paymentSuccessful"]'); + expect(wrapper.find('section :first-child').text()).to.equal('["paymentYouSentGems",{"name":"Lucky User"}]'); + expect(wrapper.find('.details-block').text()).to.equal('7'); + expect(wrapper.find('footer').text()).to.equal('["giftSubscriptionText4"]'); + }); + + it('Displays payment success when gifting subscription', () => { + firePaymentSuccess({ + paymentType: 'gift-subscription', + giftReceiver: 'Very lucky User', + subscriptionKey: 'basic_6mo', + }); + + expect(wrapper.find('h2').text()).to.equal('["paymentSuccessful"]'); + expect(wrapper.find('section :first-child').text()).to.equal('["paymentYouSentSubscription",{"name":"Very lucky User","months":6}]'); + expect(wrapper.find('.details-block').exists()).to.be.false; + expect(wrapper.find('footer').text()).to.equal('["giftSubscriptionText4"]'); + }); + + it('Displays payment success when subscribing', () => { + firePaymentSuccess({ + paymentType: 'subscription', + subscriptionKey: 'basic_6mo', + }); + + expect(wrapper.find('h2').text()).to.equal('["paymentSuccessful"]'); + expect(wrapper.find('section :first-child').text()).to.equal('["nowSubscribed"]'); + expect(wrapper.find('.details-block').text()).to.equal('["paymentSubBilling",{"amount":30,"months":6}]'); + expect(wrapper.find('footer').text()).to.equal('["giftSubscriptionText4"]'); + }); + + it('Displays payment success when creating new group plan', () => { + firePaymentSuccess({ + paymentType: 'groupPlan', + subscriptionKey: 'basic_6mo', + newGroup: true, + group: { + name: 'The best group', + memberCount: 5, + }, + }); + + expect(wrapper.find('h2').text()).to.equal('["paymentSuccessful"]'); + expect(wrapper.find('section :first-child').text()).to.equal('["groupPlanCreated",{"groupName":"The best group"}]'); + expect(wrapper.find('.details-block').text()).to.equal('["paymentSubBilling",{"amount":42,"months":6}]'); + expect(wrapper.find('footer').text()).to.equal('["giftSubscriptionText4"]'); + }); + + it('Displays payment success when upgrading group plan', () => { + firePaymentSuccess({ + paymentType: 'groupPlan', + subscriptionKey: 'basic_6mo', + group: { + name: 'The best group', + memberCount: 2, + }, + }); + + expect(wrapper.find('h2').text()).to.equal('["paymentSuccessful"]'); + expect(wrapper.find('section :first-child').text()).to.equal('["groupPlanUpgraded",{"groupName":"The best group"}]'); + expect(wrapper.find('.details-block').text()).to.equal('["paymentSubBilling",{"amount":33,"months":6}]'); + expect(wrapper.find('footer').text()).to.equal('["giftSubscriptionText4"]'); + }); + + it('Displays payment success when upgrading group plan without memberCount', () => { + firePaymentSuccess({ + paymentType: 'groupPlan', + subscriptionKey: 'basic_3mo', + group: { name: 'The solo group' }, + }); + + expect(wrapper.find('h2').text()).to.equal('["paymentSuccessful"]'); + expect(wrapper.find('section :first-child').text()).to.equal('["groupPlanUpgraded",{"groupName":"The solo group"}]'); + expect(wrapper.find('.details-block').text()).to.equal('["paymentSubBilling",{"amount":15,"months":3}]'); + expect(wrapper.find('footer').text()).to.equal('["giftSubscriptionText4"]'); + }); +}); diff --git a/website/client/tests/unit/components/ui/modal/smallModal.spec.js b/website/client/tests/unit/components/ui/modal/smallModal.spec.js new file mode 100644 index 0000000000..a6875722e6 --- /dev/null +++ b/website/client/tests/unit/components/ui/modal/smallModal.spec.js @@ -0,0 +1,33 @@ +import { mount, createLocalVue } from '@vue/test-utils'; +import BootstrapVue from 'bootstrap-vue'; +import Store from '@/libs/store'; + +import smallModal from '@/components/ui/modal/smallModal'; + +const localVue = createLocalVue(); +localVue.use(Store); +localVue.use(BootstrapVue); + +describe('Small modal', () => { + let wrapper; + beforeEach(async () => { + wrapper = mount(smallModal, { + store: new Store({ + state: {}, + getters: {}, + actions: {}, + }), + propsData: { + id: 'test-small-modal', + title: 'Test title', + }, + localVue, + mocks: { $t: (s, args) => s + JSON.stringify(args) }, + }); + wrapper.setData({ disableLazyRender: true }); + }); + + it('Displays passed in title in Bootstrap modal with passed in ID', () => { + expect(wrapper.find('#test-small-modal .modal-sm .modal-title').text()).to.equal('Test title'); + }); +});