|
|
@@ -0,0 +1,820 @@
|
|
|
+/* eslint-env jest */
|
|
|
+import Vue from 'vue'
|
|
|
+import ElementUI from 'element-ui'
|
|
|
+import path from 'path'
|
|
|
+import * as shop from '@/api/shop'
|
|
|
+import * as user from '@/api/user'
|
|
|
+import { getRegionList } from '@/api/config'
|
|
|
+import usersInfo from '@/utils/usersInfo'
|
|
|
+import router from '@/router'
|
|
|
+import { toThousandFilter } from '@/filters'
|
|
|
+import { load } from './loadComponent'
|
|
|
+
|
|
|
+jest.mock('@/api/shop', () => Object.fromEntries(['fetchProductList', 'fetchReconsumeProductList', 'fetchAutoMaintenanceProductList', 'fetchShoppingCart', 'createOrder', 'createApproachOrder', 'createReconsumeOrder', 'createApproachReconsumeOrder', 'createAutoMaintenanceOrder', 'createApproachAutoMaintenanceOrder', 'deleteApproachOrder'].map(key => [key, jest.fn()])))
|
|
|
+jest.mock('@/api/user', () => Object.fromEntries(['fetchFullInfo', 'fetchUpgrade', 'fetchUpgradeInfo', 'fetchSendUpgrade', 'getUserInfo'].map(key => [key, jest.fn()])))
|
|
|
+jest.mock('@/api/config', () => ({ getRegionList: jest.fn() }))
|
|
|
+jest.mock('@/utils/usersInfo', () => ({ userId: jest.fn(), loginTimestamp: jest.fn(), userName: () => 'OP1', userEmail: () => 'operator@example.test' }))
|
|
|
+jest.mock('@/router', () => ({ push: jest.fn(), replace: jest.fn(), afterEach: jest.fn() }))
|
|
|
+jest.mock('vue-paystack', () => ({
|
|
|
+ name: 'paystack',
|
|
|
+ props: ['amount', 'currency', 'email', 'metadata', 'reference', 'channels', 'callback', 'close'],
|
|
|
+ methods: { payWithPaystack: jest.fn() },
|
|
|
+ render: h => h('button')
|
|
|
+}))
|
|
|
+Vue.use(ElementUI)
|
|
|
+Vue.filter('toThousandFilter', toThousandFilter)
|
|
|
+Vue.prototype.$t = key => key
|
|
|
+Vue.prototype.$router = router
|
|
|
+Vue.prototype.$route = { query: {}}
|
|
|
+Vue.prototype.$message = Object.assign(jest.fn(), { error: jest.fn(), info: jest.fn() })
|
|
|
+Vue.prototype.$prompt = jest.fn()
|
|
|
+Vue.prototype.$confirm = jest.fn()
|
|
|
+
|
|
|
+const root = path.resolve(__dirname, '..')
|
|
|
+const adapters = load(path.join(root, 'adapters.js')).default
|
|
|
+const drafts = load(path.join(root, 'drafts.js'))
|
|
|
+const routes = load(path.join(root, 'routes.js'))
|
|
|
+const names = Object.keys(adapters)
|
|
|
+const product = { ID: 'P1', GOODS_NAME: 'Product one', GOODS_NO: 'SKU1', COVER: 'p1.png', SELL_PRICE: 100, MARKET_PRICE: 120, PRICE_PV: 60, TAX_RATE: 7.5, DISCOUNT: 80, STORE_NUMS: 9, COIN: 'NGN', CODE: 'NGN' }
|
|
|
+const payList = [{ label: 'cash', name: 'Ecoin' }, { label: 'pay_stack', name: 'Paystack' }, { label: 'prp', name: 'PRP' }]
|
|
|
+const settings = () => ({ data: { allAddress: [{ ID: 'A1', IS_DEFAULT: '1', ADDRESS: 'Street', MOBILE: '000', CONSIGNEE: 'Recipient' }], userBalance: { cash: 10000, prp: 10000 }, payList: Object.fromEntries(payList.map(item => [item.label, { name: item.name }])), sellType: [{ id: 1, sell_type: payList }], freeShipping: 500, freight: 10, coin: 'NGN', isDec: '0', ampDivideLine: '90' }})
|
|
|
+const instances = []
|
|
|
+const componentErrors = []
|
|
|
+Vue.config.errorHandler = error => componentErrors.push(error)
|
|
|
+async function settle() { for (let i = 0; i < 12; i++) { await Promise.resolve(); await Vue.nextTick() } }
|
|
|
+async function instance(source = names[0]) {
|
|
|
+ const vm = new (Vue.extend(adapters[source]))({ propsData: { stage: 'catalog' }}).$mount()
|
|
|
+ document.body.appendChild(vm.$el)
|
|
|
+ instances.push(vm)
|
|
|
+ await settle()
|
|
|
+ return vm
|
|
|
+}
|
|
|
+async function withProducts(source = names[0]) {
|
|
|
+ const vm = await instance(source)
|
|
|
+ if (!vm.isSelf) { vm.memberSearch = 'NG2'; await settle(); await vm.searchMember(); await settle() }
|
|
|
+ if (vm.isUpgrade) { vm.form.upgradeValue = { value: 'L2', diffPv: 999 }; vm.handleChangeLevel(vm.form.upgradeValue) }
|
|
|
+ vm.toggleProduct(vm.tableData[0])
|
|
|
+ vm.changeQuantity({ row: product, value: 2 })
|
|
|
+ vm.stage = 'checkout'
|
|
|
+ await settle()
|
|
|
+ if (vm.isSelf) vm.payForm.recUserName = 'RC1'
|
|
|
+ else if (!vm.isUpgrade) Object.assign(vm.newOrder, { decUserName: 'RC1', consignee: 'Recipient', mobile: '123', areaSelected: ['S1'], lgaName: 'District', cityName: 'City', address: 'Street' })
|
|
|
+ else Object.assign(vm.form, { consignee: 'Recipient', acceptMobile: '123', areaSelected: ['S1'], lgaName: 'District', cityName: 'City', address: 'Street', remark: 'Note' })
|
|
|
+ await settle()
|
|
|
+ return vm
|
|
|
+}
|
|
|
+
|
|
|
+it('uses the shared cart list and preserves quantity and remove events', async() => {
|
|
|
+ const vm = await withProducts('repeat-purchase')
|
|
|
+ vm.stage = 'cart'
|
|
|
+ await settle()
|
|
|
+ const cart = vm.$children.find(child => child.$options.name === 'PurchaseCartItems')
|
|
|
+ expect(cart).toBeDefined()
|
|
|
+ cart.$emit('quantity', { row: vm.cartRows[0], value: 3 })
|
|
|
+ await settle()
|
|
|
+ expect(vm.cartItems[0].quantity).toBe(3)
|
|
|
+ cart.$emit('remove', vm.cartRows[0])
|
|
|
+ await settle()
|
|
|
+ expect(vm.cartItems).toHaveLength(0)
|
|
|
+})
|
|
|
+
|
|
|
+beforeEach(() => {
|
|
|
+ jest.useFakeTimers()
|
|
|
+ jest.clearAllMocks()
|
|
|
+ componentErrors.length = 0
|
|
|
+ drafts.clearDrafts()
|
|
|
+ usersInfo.userId.mockReturnValue('OP1')
|
|
|
+ usersInfo.loginTimestamp.mockReturnValue('login-1')
|
|
|
+ router.push.mockResolvedValue()
|
|
|
+ router.replace.mockResolvedValue()
|
|
|
+ Vue.prototype.$prompt.mockResolvedValue({ value: 'fixture-password' })
|
|
|
+ Vue.prototype.$confirm.mockResolvedValue()
|
|
|
+ const list = { data: { list: [product], totalCount: 100 }}
|
|
|
+ ;[shop.fetchProductList, shop.fetchReconsumeProductList, shop.fetchAutoMaintenanceProductList].forEach(api => api.mockResolvedValue(list))
|
|
|
+ shop.fetchShoppingCart.mockImplementation(async() => settings())
|
|
|
+ user.getUserInfo.mockResolvedValue({ data: { IS_DEC: '1' }})
|
|
|
+ user.fetchFullInfo.mockResolvedValue({ data: { userId: 'U2', countryId: '1', REAL_NAME: 'Target member' }})
|
|
|
+ user.fetchUpgradeInfo.mockResolvedValue({ data: { baseInfo: { ADD_AT: '2020-01-01', REAL_NAME: 'Target member', DEC_NAME: 'Silver', IS_MAX: false, LEVEL_LIST: [{ ID: 'L2', LEVEL_NAME: 'Gold', REPAIR_PV: 999 }], UPGRADE_FUNC: 'Product', NOW_PERF: 10, NEXT_PERF: 1009 }}})
|
|
|
+ user.fetchUpgrade.mockResolvedValue({ data: { userBalance: { cash: 10000 }, decUserName: 'RC1', allDecPackage: {}, allGoods: [product], region: [], sellType: [{ sell_type: payList }] }})
|
|
|
+ getRegionList.mockResolvedValue({ data: [] })
|
|
|
+ ;[shop.createOrder, shop.createApproachOrder, shop.createReconsumeOrder, shop.createApproachReconsumeOrder, shop.createAutoMaintenanceOrder, shop.createApproachAutoMaintenanceOrder, user.fetchSendUpgrade].forEach(api => api.mockResolvedValue({ data: { SN: 'ORDER1', ORDER_AMOUNT: 200 }}))
|
|
|
+ shop.deleteApproachOrder.mockResolvedValue({})
|
|
|
+})
|
|
|
+afterEach(async() => {
|
|
|
+ await settle()
|
|
|
+ instances.splice(0).forEach(vm => { vm.$destroy(); vm.$el.remove() })
|
|
|
+ await settle()
|
|
|
+ jest.clearAllTimers()
|
|
|
+ jest.useRealTimers()
|
|
|
+ expect(componentErrors).toEqual([])
|
|
|
+})
|
|
|
+
|
|
|
+it('keeps selections from every product page in totals and checkout', async() => {
|
|
|
+ const vm = await instance()
|
|
|
+ vm.toggleProduct(product)
|
|
|
+ shop.fetchProductList.mockResolvedValue({ data: { list: [{ ...product, ID: 'P2' }], totalCount: 100 }})
|
|
|
+ vm.listQuery.page = 2
|
|
|
+ await vm.getList()
|
|
|
+ vm.toggleProduct(vm.tableData[0])
|
|
|
+ expect(vm.goodsId).toEqual(['P1', 'P2'])
|
|
|
+ expect(vm.goodsNum).toEqual([1, 1])
|
|
|
+ expect(vm.sellPriceSum).toBe('160.00')
|
|
|
+ expect(vm.pricePvSum).toBe('96.00')
|
|
|
+ expect(vm.cashSum).toBe('170.00')
|
|
|
+ vm.addressId = vm.selfPickUpAddressId
|
|
|
+ await settle()
|
|
|
+ expect(vm.cashSum).toBe('160.00')
|
|
|
+ vm.changeQuantity({ row: product, value: 7 })
|
|
|
+ expect(vm.cashSum).toBe('640.00')
|
|
|
+ expect(vm.pointFreight).toBe(0)
|
|
|
+})
|
|
|
+
|
|
|
+it('switches repeat-purchase delivery cards through the original address and freight logic', async() => {
|
|
|
+ const vm = await withProducts('repeat-purchase')
|
|
|
+ vm.shippingAddressList.push({ ID: 42, IS_DEFAULT: '0', ADDRESS: 'Second street', CITY_NAME: 'City', LGA_NAME: 'District', PROVINCE_NAME: 'State', MOBILE: '123', CONSIGNEE: 'Second recipient' })
|
|
|
+ await settle()
|
|
|
+ const fields = vm.$el.querySelector('.ng-purchase-checkout')
|
|
|
+ expect(fields.querySelectorAll('.ng-purchase-checkout-section')).toHaveLength(3)
|
|
|
+ expect(fields.querySelector('.ng-purchase-addresses .is-checked').textContent).toContain('DEFAULT')
|
|
|
+ expect(fields.querySelectorAll('.ng-purchase-addresses .el-radio')[1].textContent).toContain('Second street, City, District, State')
|
|
|
+ fields.querySelector('.ng-purchase-addresses input[value="42"]').click()
|
|
|
+ await settle()
|
|
|
+ expect(vm.addressId).toBe(42)
|
|
|
+ expect(fields.querySelector('.ng-purchase-addresses .is-checked').textContent).toContain('Second recipient')
|
|
|
+ expect(fields.querySelector('.ng-purchase-addresses .is-checked').textContent).toContain('Selected')
|
|
|
+ expect(vm.pointFreight).toBe(10)
|
|
|
+ fields.querySelector('.ng-purchase-delivery input[value="pickup"]').click()
|
|
|
+ await settle()
|
|
|
+ expect(vm.addressId).toBe(vm.selfPickUpAddressId)
|
|
|
+ expect(vm.pointFreight).toBe(0)
|
|
|
+ expect(vm.orderTotal).toBe('160.00')
|
|
|
+ expect(fields.querySelector('.ng-purchase-address-details')).toBeNull()
|
|
|
+ fields.querySelector('.ng-purchase-delivery input[value="delivery"]').click()
|
|
|
+ await settle()
|
|
|
+ expect(vm.addressId).toBe(42)
|
|
|
+ expect(vm.pointFreight).toBe(10)
|
|
|
+ expect(vm.orderTotal).toBe('170.00')
|
|
|
+ expect(fields.querySelector('.ng-purchase-addresses .is-checked').textContent).toContain('Second recipient')
|
|
|
+ expect(shop.createOrder).not.toHaveBeenCalled()
|
|
|
+})
|
|
|
+
|
|
|
+it('restores a pickup draft and uses the saved default when switching to delivery', async() => {
|
|
|
+ const vm = await withProducts('repeat-purchase')
|
|
|
+ vm.choseAddress(vm.selfPickUpAddressId)
|
|
|
+ vm.$destroy()
|
|
|
+ const restored = await instance('repeat-purchase')
|
|
|
+ restored.stage = 'checkout'
|
|
|
+ await settle()
|
|
|
+ expect(restored.$el.querySelector('.ng-purchase-address-details')).toBeNull()
|
|
|
+ expect(restored.$el.querySelector('.ng-purchase-delivery .is-checked').textContent).toContain('Self-pickup')
|
|
|
+ restored.$el.querySelector('.ng-purchase-delivery input[value="delivery"]').click()
|
|
|
+ await settle()
|
|
|
+ expect(restored.addressId).toBe('A1')
|
|
|
+ expect(restored.pointFreight).toBe(10)
|
|
|
+})
|
|
|
+
|
|
|
+it.each([
|
|
|
+ { addresses: [] },
|
|
|
+ { addresses: [{ ID: 'A2', IS_DEFAULT: '0', ADDRESS: 'Street' }] }
|
|
|
+])('does not invent a default delivery address when none is returned: %j', async({ addresses }) => {
|
|
|
+ const vm = await withProducts('repeat-purchase')
|
|
|
+ vm.shippingAddressList = addresses
|
|
|
+ vm.choseAddress(vm.selfPickUpAddressId)
|
|
|
+ await settle()
|
|
|
+ vm.$el.querySelector('.ng-purchase-delivery input[value="delivery"]').click()
|
|
|
+ await settle()
|
|
|
+ expect(vm.addressId).toBe('')
|
|
|
+ expect(vm.$el.querySelector('.ng-purchase-address-details')).not.toBeNull()
|
|
|
+ expect(vm.$el.querySelector('.ng-purchase-addresses .is-checked')).toBeNull()
|
|
|
+ if (!addresses.length) expect(vm.$el.querySelector('.ng-purchase-address-details').textContent).toContain('No saved addresses')
|
|
|
+})
|
|
|
+
|
|
|
+it('disables repeat-purchase delivery and address selection while processing', async() => {
|
|
|
+ const vm = await withProducts('repeat-purchase')
|
|
|
+ vm.submitButtonStat = true
|
|
|
+ await settle()
|
|
|
+ const radios = vm.$el.querySelectorAll('.ng-purchase-checkout input[type="radio"]')
|
|
|
+ expect(radios).toHaveLength(3)
|
|
|
+ radios.forEach(input => { expect(input.disabled).toBe(true); input.click() })
|
|
|
+ await settle()
|
|
|
+ expect(vm.addressId).toBe('A1')
|
|
|
+})
|
|
|
+
|
|
|
+it.each(names.filter(source => source !== 'repeat-purchase'))('separates %s shipping fields from member information without changing the fields', async source => {
|
|
|
+ const vm = await withProducts(source)
|
|
|
+ expect(vm.$el.querySelector('.ng-purchase-delivery')).toBeNull()
|
|
|
+ expect(vm.$el.querySelector('.ng-purchase-address-details')).toBeNull()
|
|
|
+ expect(vm.$el.querySelector('#ng-purchase-consignee').value).toBe('Recipient')
|
|
|
+ const memberCard = vm.$el.querySelector('.ng-purchase-member-information')
|
|
|
+ const shippingCard = vm.$el.querySelector('.ng-purchase-shipping-address')
|
|
|
+ expect(memberCard.nextElementSibling).toBe(shippingCard)
|
|
|
+ expect(memberCard.querySelector('#ng-purchase-member-readonly')).not.toBeNull()
|
|
|
+ expect(memberCard.querySelector('#ng-purchase-stockist')).not.toBeNull()
|
|
|
+ expect(memberCard.querySelector('#ng-purchase-consignee')).toBeNull()
|
|
|
+ expect(shippingCard.querySelector('h2').textContent).toBe('Shipping address')
|
|
|
+ expect(shippingCard.querySelector('#ng-purchase-consignee')).not.toBeNull()
|
|
|
+ expect(shippingCard.querySelector('#ng-purchase-areaSelected')).not.toBeNull()
|
|
|
+ expect(shippingCard.querySelector('#ng-purchase-address')).not.toBeNull()
|
|
|
+})
|
|
|
+
|
|
|
+it.each(names)('%s shows a full-width empty cart after the last product is removed', async source => {
|
|
|
+ const vm = await withProducts(source)
|
|
|
+ vm.stage = 'cart'
|
|
|
+ await settle()
|
|
|
+ expect(vm.$el.querySelector('.ng-purchase-summary')).not.toBeNull()
|
|
|
+ vm.$el.querySelector('.ng-purchase-remove').click()
|
|
|
+ await settle()
|
|
|
+ const empty = vm.$el.querySelector('.ng-purchase-empty')
|
|
|
+ expect(empty).not.toBeNull()
|
|
|
+ expect(empty.textContent).toContain('Your cart is empty')
|
|
|
+ expect(empty.textContent).toContain('Add products you love and they will appear here.')
|
|
|
+ expect(vm.$el.querySelector('.ng-purchase-columns-empty')).not.toBeNull()
|
|
|
+ expect(vm.$el.querySelector('.ng-purchase-summary')).toBeNull()
|
|
|
+ empty.querySelector('button').click()
|
|
|
+ expect(router.push).toHaveBeenLastCalledWith({ path: '/dashboard/' + source, query: { from: source }})
|
|
|
+ vm.toggleProduct(product)
|
|
|
+ await settle()
|
|
|
+ expect(vm.$el.querySelector('.ng-purchase-empty')).toBeNull()
|
|
|
+ expect(vm.$el.querySelector('.ng-purchase-columns-empty')).toBeNull()
|
|
|
+ expect(vm.$el.querySelector('.ng-purchase-summary')).not.toBeNull()
|
|
|
+})
|
|
|
+
|
|
|
+it.each(names)('returns %s to its explicit buying route and restores the draft', async source => {
|
|
|
+ const vm = await withProducts(source)
|
|
|
+ vm.navigate('catalog')
|
|
|
+ expect(router.push).toHaveBeenLastCalledWith({ path: '/dashboard/' + source, query: { from: source }})
|
|
|
+ vm.$destroy()
|
|
|
+ const restored = await instance(source)
|
|
|
+ expect(restored.cartItems[0].quantity).toBe(2)
|
|
|
+ expect(restored.selectedIds).toEqual(['P1'])
|
|
|
+ if (!vm.isSelf) expect(restored.memberKey).toBe('NG2')
|
|
|
+ if (vm.isSelf) expect(restored.payForm.recUserName).toBe('RC1')
|
|
|
+ expect(JSON.stringify(drafts.readDraft(source, 'OP1:login-1'))).not.toContain('fixture-password')
|
|
|
+})
|
|
|
+
|
|
|
+it('cannot retain a previous member or their products after a failed lookup', async() => {
|
|
|
+ const vm = await withProducts('member-repurchase')
|
|
|
+ vm.stage = 'catalog'
|
|
|
+ vm.memberSearch = 'NG3'
|
|
|
+ await settle()
|
|
|
+ expect(vm.cartItems).toEqual([])
|
|
|
+ expect(vm.newOrder.userName).toBe('')
|
|
|
+ user.fetchFullInfo.mockRejectedValue(new Error('Member not found'))
|
|
|
+ await vm.searchMember()
|
|
|
+ expect(vm.memberKey).toBe('')
|
|
|
+ expect(vm.tableData).toEqual([])
|
|
|
+ expect(vm.canCheckout).toBe(false)
|
|
|
+})
|
|
|
+
|
|
|
+it.each(['member-repurchase', 'member-auto-maintenance'])('%s accepts the legacy full-info response without a returned member number', async source => {
|
|
|
+ user.fetchFullInfo.mockResolvedValue({ data: { userId: 'U2', countryId: '1', REAL_NAME: 'Target member', isLocation: '(Left-Null,Right-Null)' }})
|
|
|
+ const vm = await instance(source)
|
|
|
+ vm.memberSearch = 'NG2'
|
|
|
+ await settle()
|
|
|
+ await vm.searchMember()
|
|
|
+ await settle()
|
|
|
+ expect(vm.loadError).toBe('')
|
|
|
+ expect(user.fetchFullInfo).toHaveBeenCalledWith({ userName: 'NG2' })
|
|
|
+ expect(vm.memberKey).toBe('NG2')
|
|
|
+ expect(vm.memberDetails.number).toBe('NG2')
|
|
|
+ expect(vm.newOrder.userName).toBe('NG2')
|
|
|
+ expect(vm.memberName).toBe('Target member')
|
|
|
+ const listApi = vm.isMaintenance ? shop.fetchAutoMaintenanceProductList : shop.fetchReconsumeProductList
|
|
|
+ expect(listApi).toHaveBeenCalledWith(expect.objectContaining({ userId: 'U2' }))
|
|
|
+ expect(shop.fetchShoppingCart).toHaveBeenCalledWith({ categoryType: vm.listQuery.categoryType, userId: 'U2' })
|
|
|
+ expect(vm.catalogReady).toBe(true)
|
|
|
+ vm.toggleProduct(vm.tableData[0])
|
|
|
+ vm.changeQuantity({ row: product, value: 2 })
|
|
|
+ vm.stage = 'checkout'
|
|
|
+ Object.assign(vm.newOrder, { decUserName: 'RC1', consignee: 'Recipient', mobile: '123', areaSelected: ['S1'], lgaName: 'District', cityName: 'City', address: 'Street' })
|
|
|
+ await settle()
|
|
|
+ expect(getRegionList).toHaveBeenCalledWith({ countryId: '1' })
|
|
|
+ vm.activePayType = 'cash'
|
|
|
+ vm.submitOrder()
|
|
|
+ await settle()
|
|
|
+ const orderApi = vm.isMaintenance ? shop.createAutoMaintenanceOrder : shop.createReconsumeOrder
|
|
|
+ expect(orderApi).toHaveBeenCalledWith(expect.objectContaining({ userName: 'NG2', goodsId: ['P1'], goodsNum: [2] }))
|
|
|
+})
|
|
|
+
|
|
|
+it.each(['member-repurchase', 'member-auto-maintenance'])('%s rejects a full-info response without userId and clears the previous order', async source => {
|
|
|
+ const vm = await withProducts(source)
|
|
|
+ vm.stage = 'catalog'
|
|
|
+ vm.memberSearch = 'NG3'
|
|
|
+ await settle()
|
|
|
+ user.fetchFullInfo.mockResolvedValue({ data: { REAL_NAME: 'Invalid member', countryId: '1' }})
|
|
|
+ shop.fetchShoppingCart.mockClear()
|
|
|
+ await vm.searchMember()
|
|
|
+ await settle()
|
|
|
+ expect(vm.memberKey).toBe('')
|
|
|
+ expect(vm.newOrder.userName).toBe('')
|
|
|
+ expect(vm.tableData).toEqual([])
|
|
|
+ expect(vm.cartItems).toEqual([])
|
|
|
+ expect(shop.fetchShoppingCart).not.toHaveBeenCalled()
|
|
|
+ expect(vm.canCheckout).toBe(false)
|
|
|
+})
|
|
|
+
|
|
|
+it.each([
|
|
|
+ ['member-repurchase', 'Choose products', 'Tap a product to add it to your cart. Prices shown are DP (distributor price) with BV, and the retail reference RP.', 'Tap the cart button in the bottom-right corner to review your items and place the order.'],
|
|
|
+ ['member-auto-maintenance', 'Choose products', "Tap a product to add it to the member's cart. Prices shown are DP (distributor price) with BV, and the retail reference RP. Auto-Maintenance requires the selected products' total BV to be at least 90 BV.", 'Tap the cart button in the bottom-right corner to review the order and place it.'],
|
|
|
+ ['member-upgrade', 'Upgrade products', "Tap a product to add it to the member's cart. Choose the upgrade level in your cart or at checkout. Prices shown are DP (distributor price) with BV, and the retail reference RP.", 'Tap the cart button in the bottom-right corner to pick the upgrade level and place the order.']
|
|
|
+])('%s uses the existing section card and requested product help', async(source, title, help, checkoutHint) => {
|
|
|
+ const vm = await withProducts(source)
|
|
|
+ vm.stage = 'catalog'
|
|
|
+ await settle()
|
|
|
+ const section = vm.$el.querySelector('.ng-purchase-products.ng-ui-section-card')
|
|
|
+ expect(section).not.toBeNull()
|
|
|
+ expect(section.querySelector('.ng-ui-section-card-title').textContent.trim()).toBe(title)
|
|
|
+ expect(section.querySelector('.ng-signup-help').textContent.trim()).toBe(help)
|
|
|
+ expect(section.querySelector('.ng-signup-totals')).toBeNull()
|
|
|
+ expect(section.querySelector('.ng-purchase-cart-hint p').textContent.trim()).toBe('Ready to check out? ' + checkoutHint)
|
|
|
+ expect(section.querySelector('.ng-purchase-cart-hint strong').textContent).toBe('Ready to check out?')
|
|
|
+ if (vm.isMaintenance) {
|
|
|
+ vm.ampDivideLine = '100'
|
|
|
+ await settle()
|
|
|
+ expect(section.querySelector('.ng-signup-help').textContent.trim()).toBe(help)
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+it('keeps the original totals footer for ordinary repeat purchases', async() => {
|
|
|
+ const vm = await instance('repeat-purchase')
|
|
|
+ expect(vm.$el.querySelector('.ng-signup-totals')).not.toBeNull()
|
|
|
+ expect(vm.$el.querySelector('.ng-purchase-cart-hint')).toBeNull()
|
|
|
+})
|
|
|
+
|
|
|
+it.each(names)('%s shows checkout product prices and BV using the existing purchase rules', async source => {
|
|
|
+ const vm = await withProducts(source)
|
|
|
+ vm.activePayType = 'cash'
|
|
|
+ await settle()
|
|
|
+ const summary = vm.$el.querySelector('.ng-purchase-summary')
|
|
|
+ expect(summary.classList.contains('is-checkout')).toBe(true)
|
|
|
+ expect(vm.$el.querySelector('.ng-purchase-maintenance') !== null).toBe(vm.isMaintenance)
|
|
|
+ expect(summary.querySelector('.ng-purchase-requirement')).toBeNull()
|
|
|
+ const row = summary.querySelector('.ng-purchase-summary-product')
|
|
|
+ const text = element => element.textContent.replace(/\s+/g, ' ').trim()
|
|
|
+ expect(row.querySelector('h3').textContent).toBe('Product one')
|
|
|
+ expect(row.querySelector('.ng-purchase-summary-image')).not.toBeNull()
|
|
|
+ expect(row.querySelector('.ng-purchase-summary-quantity').textContent).toBe('Qty 2')
|
|
|
+ expect(text(row.querySelector('.ng-purchase-summary-unit'))).toBe(vm.isUpgrade ? 'SKU1 · NGN 100 (60 BV) each' : 'SKU1 · NGN 80 (48 BV) each')
|
|
|
+ expect(text(row.querySelector('.ng-purchase-summary-line'))).toBe(vm.isUpgrade ? 'NGN 200 (120 BV)' : 'NGN 160 (96 BV)')
|
|
|
+ expect(text(summary.querySelector('.ng-purchase-total'))).toBe(vm.isUpgrade ? 'NGN 200 (120 BV)' : 'NGN 170 (96 BV)')
|
|
|
+ expect(text(summary.querySelector('.ng-purchase-checkout-totals'))).toContain('Tax NGN ' + Number(vm.taxSum))
|
|
|
+ expect(summary.querySelector('.ng-purchase-summary-channels')).toBeNull()
|
|
|
+ vm.activePayType = 'pay_stack'
|
|
|
+ await settle()
|
|
|
+ expect(text(summary.querySelector('.ng-purchase-checkout-totals'))).toContain('MethodPaystack')
|
|
|
+ expect(Array.from(summary.querySelectorAll('.ng-purchase-summary-channels .el-tag'), item => item.textContent)).toEqual(['VISA', 'MC', 'VERVE', 'USSD', 'BANK', 'VISA QR'])
|
|
|
+ vm.activePayType = 'cash'
|
|
|
+ await settle()
|
|
|
+ expect(text(summary.querySelector('.ng-purchase-checkout-totals'))).toContain('MethodEcoin')
|
|
|
+ expect(summary.querySelector('.ng-purchase-summary-channels')).toBeNull()
|
|
|
+ vm.changeQuantity({ row: product, value: 3 })
|
|
|
+ await settle()
|
|
|
+ expect(row.querySelector('.ng-purchase-summary-quantity').textContent).toBe('Qty 3')
|
|
|
+ expect(text(row.querySelector('.ng-purchase-summary-line'))).toBe(vm.isUpgrade ? 'NGN 300 (180 BV)' : 'NGN 240 (144 BV)')
|
|
|
+ expect(text(summary.querySelector('.ng-purchase-total'))).toBe(vm.isUpgrade ? 'NGN 300 (180 BV)' : 'NGN 250 (144 BV)')
|
|
|
+ vm.stage = 'cart'
|
|
|
+ await settle()
|
|
|
+ expect(summary.classList.contains('is-checkout')).toBe(false)
|
|
|
+ expect(summary.querySelector('.ng-purchase-summary-products')).toBeNull()
|
|
|
+ expect(summary.querySelector('.el-descriptions').textContent).toContain('Subtotal')
|
|
|
+})
|
|
|
+
|
|
|
+it('keeps the checkout order button bound to the maintenance requirement and original submission', async() => {
|
|
|
+ const vm = await withProducts('member-auto-maintenance')
|
|
|
+ vm.activePayType = 'cash'
|
|
|
+ vm.ampDivideLine = '100'
|
|
|
+ await settle()
|
|
|
+ const requirement = vm.$el.querySelector('.ng-purchase-main .ng-purchase-maintenance')
|
|
|
+ expect(requirement.previousElementSibling.querySelector('h2').textContent).toBe('Member information')
|
|
|
+ expect(requirement.nextElementSibling.querySelector('h2').textContent).toBe('Payment method')
|
|
|
+ expect(requirement.textContent).toContain('at least 100 BV')
|
|
|
+ expect(requirement.querySelector('.el-alert').textContent).toContain('at least 100 BV')
|
|
|
+ const button = vm.$el.querySelector('.ng-purchase-place-order')
|
|
|
+ expect(button.disabled).toBe(true)
|
|
|
+ button.click()
|
|
|
+ await settle()
|
|
|
+ expect(shop.createAutoMaintenanceOrder).not.toHaveBeenCalled()
|
|
|
+ vm.ampDivideLine = '90'
|
|
|
+ await settle()
|
|
|
+ const enabledButton = vm.$el.querySelector('.ng-purchase-place-order')
|
|
|
+ expect(enabledButton.disabled).toBe(false)
|
|
|
+ expect(requirement.querySelector('.el-alert')).toBeNull()
|
|
|
+ expect(requirement.querySelector('.is-sufficient').textContent).toContain('96 / 90 BV')
|
|
|
+ let confirmPassword
|
|
|
+ Vue.prototype.$prompt.mockReturnValue(new Promise(resolve => { confirmPassword = resolve }))
|
|
|
+ enabledButton.__vue__.$emit('click')
|
|
|
+ await settle()
|
|
|
+ expect(Vue.prototype.$prompt).toHaveBeenCalledTimes(1)
|
|
|
+ expect(vm.$el.querySelector('.ng-purchase-place-order').textContent.trim()).toBe('Processing...')
|
|
|
+ expect(vm.$el.querySelector('.ng-purchase-place-order').classList.contains('is-loading')).toBe(true)
|
|
|
+ confirmPassword({ value: 'fixture-password' })
|
|
|
+ await settle()
|
|
|
+ expect(shop.createAutoMaintenanceOrder).toHaveBeenCalledTimes(1)
|
|
|
+ expect(shop.createAutoMaintenanceOrder).toHaveBeenCalledWith(expect.objectContaining({ userName: 'NG2', goodsId: ['P1'], goodsNum: [2], payType: 'cash' }))
|
|
|
+})
|
|
|
+
|
|
|
+it('uses the returned maintenance threshold and excludes PRP', async() => {
|
|
|
+ const vm = await withProducts('member-auto-maintenance')
|
|
|
+ expect(vm.payList.map(item => item.label)).toEqual(['cash', 'pay_stack'])
|
|
|
+ expect(vm.maintenanceReady).toBe(true)
|
|
|
+ vm.stage = 'cart'
|
|
|
+ vm.ampDivideLine = '100'
|
|
|
+ await settle()
|
|
|
+ expect(vm.maintenanceReady).toBe(false)
|
|
|
+ const summary = vm.$el.querySelector('.ng-purchase-summary')
|
|
|
+ const requirement = summary.querySelector('.ng-purchase-requirement')
|
|
|
+ const warning = summary.querySelector('.ng-purchase-requirement-warning')
|
|
|
+ expect(requirement.parentElement).toBe(warning.parentElement)
|
|
|
+ expect(warning.textContent).toContain('at least 100 BV')
|
|
|
+ expect(summary.querySelector('.el-button--primary').disabled).toBe(true)
|
|
|
+ vm.ampDivideLine = '96'
|
|
|
+ await settle()
|
|
|
+ expect(vm.maintenanceReady).toBe(true)
|
|
|
+ expect(requirement.classList.contains('is-sufficient')).toBe(true)
|
|
|
+ expect(summary.querySelector('.ng-purchase-requirement-warning')).toBeNull()
|
|
|
+ expect(summary.querySelector('.el-button--primary').disabled).toBe(false)
|
|
|
+ vm.ampDivideLine = null
|
|
|
+ await settle()
|
|
|
+ expect(summary.querySelector('.ng-purchase-requirement-warning').textContent).toContain('Waiting for the maintenance requirement.')
|
|
|
+ expect(vm.canCheckout).toBe(false)
|
|
|
+ vm.submitOrder()
|
|
|
+ expect(shop.createAutoMaintenanceOrder).not.toHaveBeenCalled()
|
|
|
+})
|
|
|
+
|
|
|
+it('preserves upgrade information and does not introduce a difference-BV rejection', async() => {
|
|
|
+ const vm = await withProducts('member-upgrade')
|
|
|
+ expect(user.fetchUpgradeInfo).toHaveBeenCalledWith({ userName: 'NG2' })
|
|
|
+ expect(user.fetchUpgrade).toHaveBeenCalledWith({ userName: 'NG2' })
|
|
|
+ expect(vm.needDiffPv).toBe(999)
|
|
|
+ expect(vm.pricePvSum).toBe('120.00')
|
|
|
+ vm.activePayType = 'cash'
|
|
|
+ vm.submitOrder()
|
|
|
+ await settle()
|
|
|
+ expect(user.fetchSendUpgrade).toHaveBeenCalledWith(expect.objectContaining({ decLv: 'L2', insertUserName: 'NG2', goodsId: ['P1'], goodsNum: [2], province: 'S1', nowPerf: 10, nextPerf: 1009 }))
|
|
|
+})
|
|
|
+
|
|
|
+it.each([NaN, 0, -1, 1.5, 10, Infinity])('rejects invalid quantity %s without corrupting totals', async quantity => {
|
|
|
+ const vm = await withProducts()
|
|
|
+ vm.changeQuantity({ row: product, value: quantity })
|
|
|
+ expect(vm.cartItems[0].quantity).toBe(2)
|
|
|
+ expect(vm.cashSum).toBe('170.00')
|
|
|
+})
|
|
|
+
|
|
|
+const submissions = [
|
|
|
+ ['repeat-purchase', shop.createOrder, shop.createApproachOrder, '/shop/own-order'],
|
|
|
+ ['member-repurchase', shop.createReconsumeOrder, shop.createApproachReconsumeOrder, '/shop/member-order'],
|
|
|
+ ['member-auto-maintenance', shop.createAutoMaintenanceOrder, shop.createApproachAutoMaintenanceOrder, '/shop/own-order'],
|
|
|
+ ['member-upgrade', user.fetchSendUpgrade, user.fetchSendUpgrade, '/shop/member-order']
|
|
|
+]
|
|
|
+it.each(names)('%s keeps Reporting Center ID required and editable for every stockist status', async source => {
|
|
|
+ const vm = await withProducts(source)
|
|
|
+ for (const status of [0, '0', 1, '1', false, true]) {
|
|
|
+ vm.isDec = status
|
|
|
+ await settle()
|
|
|
+ const input = vm.$el.querySelector('#ng-purchase-stockist')
|
|
|
+ expect(input.disabled).toBe(false)
|
|
|
+ expect(input.closest('.el-form-item').classList.contains('is-required')).toBe(true)
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+it.each(submissions)('%s submits the Reporting Center ID entered in the form', async(source, api) => {
|
|
|
+ const vm = await withProducts(source)
|
|
|
+ vm.activePayType = 'cash'
|
|
|
+ const input = vm.$el.querySelector('#ng-purchase-stockist')
|
|
|
+ input.value = 'RC-EDITED'
|
|
|
+ input.dispatchEvent(new Event('input', { bubbles: true }))
|
|
|
+ if (vm.isUpgrade) {
|
|
|
+ const recipient = vm.$el.querySelector('#ng-purchase-consignee')
|
|
|
+ recipient.value = 'Edited recipient'
|
|
|
+ recipient.dispatchEvent(new Event('input', { bubbles: true }))
|
|
|
+ }
|
|
|
+ await settle()
|
|
|
+ expect(vm.$el.querySelector('#ng-purchase-stockist').value).toBe('RC-EDITED')
|
|
|
+ vm.submitOrder()
|
|
|
+ await settle()
|
|
|
+ expect(api).toHaveBeenCalledWith(expect.objectContaining({ [vm.isSelf ? 'userName' : 'decUserName']: 'RC-EDITED' }))
|
|
|
+ if (vm.isUpgrade) expect(api).toHaveBeenCalledWith(expect.objectContaining({ consignee: 'Edited recipient' }))
|
|
|
+})
|
|
|
+
|
|
|
+it.each(submissions)('%s blocks submission when the entered Reporting Center ID is empty', async(source, api) => {
|
|
|
+ const vm = await withProducts(source)
|
|
|
+ const input = vm.$el.querySelector('#ng-purchase-stockist')
|
|
|
+ input.value = ''
|
|
|
+ input.dispatchEvent(new Event('input', { bubbles: true }))
|
|
|
+ await settle()
|
|
|
+ vm.submitOrder()
|
|
|
+ await settle()
|
|
|
+ expect(api).not.toHaveBeenCalled()
|
|
|
+ expect(Vue.prototype.$prompt).not.toHaveBeenCalled()
|
|
|
+ expect(vm.busy).toBe(false)
|
|
|
+})
|
|
|
+
|
|
|
+it.each(submissions)('%s submits once, preserving the original member and delivery fields', async(source, api, unused, destination) => {
|
|
|
+ const vm = await withProducts(source)
|
|
|
+ vm.activePayType = 'cash'
|
|
|
+ vm.submitOrder()
|
|
|
+ vm.submitOrder()
|
|
|
+ await settle()
|
|
|
+ expect(api).toHaveBeenCalledTimes(1)
|
|
|
+ const sent = api.mock.calls[0][0]
|
|
|
+ expect(sent.goodsId).toEqual(['P1'])
|
|
|
+ expect(sent.goodsNum).toEqual([2])
|
|
|
+ expect(sent.payType).toBe('cash')
|
|
|
+ if (vm.isSelf) expect(sent).toEqual({ goodsId: ['P1'], goodsNum: [2], payPassword: 'fixture-password', email: 'operator@example.test', addressId: 'A1', address: undefined, payType: 'cash', userName: 'RC1' })
|
|
|
+ else if (!vm.isUpgrade) expect(sent).toEqual({ goodsId: ['P1'], goodsNum: [2], payPassword: 'fixture-password', email: 'operator@example.test', addressId: 'A1', payType: 'cash', userName: 'NG2', consignee: 'Recipient', acceptMobile: '123', province: ['S1'], lgaName: 'District', cityName: 'City', detailaddress: 'Street', decUserName: 'RC1' })
|
|
|
+ expect(router.push).toHaveBeenLastCalledWith({ path: destination })
|
|
|
+ expect(vm.cartItems).toEqual([])
|
|
|
+ expect(drafts.readDraft(source, 'OP1:login-1')).toBeNull()
|
|
|
+})
|
|
|
+
|
|
|
+it.each(submissions)('%s retains its draft on failed payment and cancels the correct Paystack order', async(source, unused, api) => {
|
|
|
+ const vm = await withProducts(source)
|
|
|
+ vm.activePayType = 'pay_stack'
|
|
|
+ api.mockRejectedValueOnce(new Error('Order failed'))
|
|
|
+ vm.submitOrder()
|
|
|
+ await settle()
|
|
|
+ expect(vm.cartItems).toHaveLength(1)
|
|
|
+ expect(vm.busy).toBe(false)
|
|
|
+ api.mockResolvedValueOnce({ data: { SN: 'ORDER1', ORDER_AMOUNT: 200 }})
|
|
|
+ vm.$el.querySelector('.ng-purchase-place-order').click()
|
|
|
+ await settle()
|
|
|
+ expect(vm.visible).toBe(true)
|
|
|
+ expect(vm.payment.orderSn).toBe('ORDER1')
|
|
|
+ expect(vm.payment.amount).toBe(vm.isUpgrade ? 200 : '170.00')
|
|
|
+ const dialog = vm.$children.find(child => child.$options.name === 'PaystackPaymentDialog').$el
|
|
|
+ expect(dialog.querySelector('.el-dialog__title').textContent).toBe('Paystack Payment')
|
|
|
+ expect(dialog.textContent).toContain('Confirm your email to continue to the Paystack secure checkout.')
|
|
|
+ expect(dialog.querySelector('.ng-paystack-payment-amount strong').textContent).toBe(vm.isUpgrade ? 'NGN 200' : 'NGN 170')
|
|
|
+ expect(vm.$refs.paystack.amount).toBe(Number(vm.payment.amount) * 100)
|
|
|
+ expect(vm.$refs.paystack.metadata).toBe(vm.payment.metadata)
|
|
|
+ expect(vm.$refs.paystack.reference).toBe(vm.reference + vm.paymentAttempt)
|
|
|
+ const email = dialog.querySelector('#ng-paystack-payment-email')
|
|
|
+ expect(email.readOnly).toBe(false)
|
|
|
+ email.value = 'confirmed@example.test'
|
|
|
+ email.dispatchEvent(new Event('input', { bubbles: true }))
|
|
|
+ await settle()
|
|
|
+ expect(vm.payment.email).toBe('confirmed@example.test')
|
|
|
+ expect(vm.$refs.paystack.email).toBe('confirmed@example.test')
|
|
|
+ dialog.querySelector('.ng-paystack-payment-continue').click()
|
|
|
+ expect(vm.$refs.paystack.$options.methods.payWithPaystack).toHaveBeenCalledTimes(1)
|
|
|
+ expect(vm.mayLeave({ path: '/dashboard/index' })).toBe(false)
|
|
|
+ expect(vm.mayLeave({ path: '/login' })).toBe(true)
|
|
|
+ vm.submitOrder()
|
|
|
+ expect(api).toHaveBeenCalledTimes(2)
|
|
|
+ dialog.querySelector('.el-dialog__headerbtn').click()
|
|
|
+ await settle()
|
|
|
+ expect(shop.deleteApproachOrder).toHaveBeenCalledWith({ orderSn: 'ORDER1' })
|
|
|
+ expect(vm.visible).toBe(false)
|
|
|
+ expect(vm.cartItems).toHaveLength(1)
|
|
|
+})
|
|
|
+
|
|
|
+it('handles cancellation failure and accepts a payment callback only once', async() => {
|
|
|
+ const vm = await withProducts('member-upgrade')
|
|
|
+ vm.activePayType = 'pay_stack'
|
|
|
+ vm.submitOrder()
|
|
|
+ await settle()
|
|
|
+ shop.deleteApproachOrder.mockRejectedValue(new Error('Cancellation failed'))
|
|
|
+ await vm.handleClose()
|
|
|
+ expect(vm.visible).toBe(true)
|
|
|
+ expect(vm.cartItems).toHaveLength(1)
|
|
|
+ vm.processPayment()
|
|
|
+ const timer = vm.countdownTimer
|
|
|
+ vm.processPayment()
|
|
|
+ expect(vm.countdownTimer).toBe(timer)
|
|
|
+ expect(vm.cartItems).toEqual([])
|
|
|
+ jest.advanceTimersByTime(5000)
|
|
|
+ expect(router.push).toHaveBeenLastCalledWith({ path: '/shop/member-order' })
|
|
|
+})
|
|
|
+
|
|
|
+it.each(submissions)('%s matches the actual legacy submission payload', async(source, api) => {
|
|
|
+ const vm = await withProducts(source)
|
|
|
+ vm.activePayType = 'cash'
|
|
|
+ vm.submitOrder()
|
|
|
+ await settle()
|
|
|
+ const actual = api.mock.calls[0][0]
|
|
|
+ const old = new Vue({ ...adapters[source].extends, render: h => h('div') })
|
|
|
+ const fields = ['multipleSelection', 'goods', 'goodsId', 'goodsNum', 'addressId', 'cashSum', 'payType', 'currency', 'payList', 'userBalance', 'newOrder', 'form', 'payForm', 'decUserName', 'toLevel', 'nowPerf', 'nextPerf', 'decWay']
|
|
|
+ fields.forEach(key => { if (vm[key] !== undefined) old[key] = JSON.parse(JSON.stringify(vm[key])) })
|
|
|
+ old.$refs.payForm = { validate: callback => callback(true) }
|
|
|
+ const debugLog = jest.spyOn(console, 'log').mockImplementation(() => {})
|
|
|
+ try {
|
|
|
+ old[vm.isUpgrade ? 'onSubmit' : 'orderSubmit']()
|
|
|
+ await settle()
|
|
|
+ expect(api.mock.calls[1][0]).toEqual(actual)
|
|
|
+ } finally { debugLog.mockRestore(); old.$destroy() }
|
|
|
+})
|
|
|
+
|
|
|
+it('rejects delayed callbacks from a previously cancelled transaction', async() => {
|
|
|
+ const vm = await withProducts()
|
|
|
+ vm.activePayType = 'pay_stack'
|
|
|
+ vm.submitOrder()
|
|
|
+ await settle()
|
|
|
+ const oldCallback = vm.paymentCallback
|
|
|
+ const oldClose = vm.paymentClosed
|
|
|
+ await vm.handleClose()
|
|
|
+ shop.createApproachOrder.mockResolvedValue({ data: { SN: 'ORDER2' }})
|
|
|
+ vm.submitOrder()
|
|
|
+ await settle()
|
|
|
+ oldCallback()
|
|
|
+ oldClose()
|
|
|
+ expect(vm.payment.orderSn).toBe('ORDER2')
|
|
|
+ expect(vm.visible).toBe(true)
|
|
|
+ expect(vm.cartItems).toHaveLength(1)
|
|
|
+ expect(shop.deleteApproachOrder).toHaveBeenCalledTimes(1)
|
|
|
+})
|
|
|
+
|
|
|
+it('preserves amount and BV invariants across stock quantities', async() => {
|
|
|
+ const vm = await withProducts()
|
|
|
+ for (let quantity = 1; quantity <= product.STORE_NUMS; quantity++) {
|
|
|
+ vm.changeQuantity({ row: product, value: quantity })
|
|
|
+ expect(Number(vm.sellPriceSum)).toBe(quantity * 80)
|
|
|
+ expect(Number(vm.pricePvSum)).toBe(quantity * 48)
|
|
|
+ expect(Number(vm.cashSum)).toBe(quantity * 80 + (quantity * 80 >= 500 ? 0 : 10))
|
|
|
+ expect(vm.goodsNum).toEqual([quantity])
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+it('fails closed on missing settings and on maximum-level or ineligible members', async() => {
|
|
|
+ shop.fetchShoppingCart.mockRejectedValue(new Error('Settings failed'))
|
|
|
+ const vm = await instance()
|
|
|
+ vm.toggleProduct(product)
|
|
|
+ expect(vm.canCheckout).toBe(false)
|
|
|
+ expect(vm.settingsLoading).toBe(false)
|
|
|
+ const upgrade = await instance('member-upgrade')
|
|
|
+ user.getUserInfo.mockResolvedValue({ data: { IS_DEC: '0' }})
|
|
|
+ await upgrade.fetchUserInfo()
|
|
|
+ expect(upgrade.catalogReady).toBe(false)
|
|
|
+ user.getUserInfo.mockResolvedValue({ data: { IS_DEC: '1' }})
|
|
|
+ await upgrade.fetchUserInfo()
|
|
|
+ const maximum = await user.fetchUpgradeInfo()
|
|
|
+ user.fetchUpgradeInfo.mockResolvedValue({ data: { baseInfo: { ...maximum.data.baseInfo, IS_MAX: true }}})
|
|
|
+ upgrade.memberSearch = 'MAX'
|
|
|
+ await upgrade.searchMember()
|
|
|
+ await settle()
|
|
|
+ expect(upgrade.catalogReady).toBe(false)
|
|
|
+ expect(upgrade.canCheckout).toBe(false)
|
|
|
+ expect(upgrade.$el.querySelector('.ng-purchase-max-level').textContent.replace(/\s+/g, ' ').trim()).toBe('Target member (MAX) is already Silver — the highest upgradeable level is Silver, so no upgrade is available.')
|
|
|
+ expect(upgrade.$el.querySelector('.ng-purchase-member')).toBeNull()
|
|
|
+ expect(upgrade.$el.querySelector('.ng-purchase-products')).toBeNull()
|
|
|
+})
|
|
|
+
|
|
|
+it.each(submissions)('%s blocks insufficient cash without losing the cart', async(source, api) => {
|
|
|
+ const vm = await withProducts(source)
|
|
|
+ vm.activePayType = 'cash'
|
|
|
+ vm.userBalance.cash = 0
|
|
|
+ vm.submitOrder()
|
|
|
+ await settle()
|
|
|
+ expect(api).not.toHaveBeenCalled()
|
|
|
+ expect(Vue.prototype.$prompt).not.toHaveBeenCalled()
|
|
|
+ expect(vm.cartItems).toHaveLength(1)
|
|
|
+ expect(vm.busy).toBe(false)
|
|
|
+})
|
|
|
+
|
|
|
+it('does not fall back to another member or a payment type when required data is absent', async() => {
|
|
|
+ const vm = await instance('member-repurchase')
|
|
|
+ user.fetchFullInfo.mockResolvedValue({ data: { REAL_NAME: 'Incomplete member' }})
|
|
|
+ vm.memberSearch = 'NG2'
|
|
|
+ await vm.searchMember()
|
|
|
+ expect(shop.fetchReconsumeProductList).not.toHaveBeenCalled()
|
|
|
+ expect(vm.memberKey).toBe('')
|
|
|
+ shop.fetchShoppingCart.mockResolvedValue({ data: { ...settings().data, sellType: [] }})
|
|
|
+ const own = await instance()
|
|
|
+ own.toggleProduct(product)
|
|
|
+ expect(own.payList).toEqual([])
|
|
|
+ expect(own.canCheckout).toBe(false)
|
|
|
+})
|
|
|
+
|
|
|
+it('keeps the legacy missing-member checks at submission', async() => {
|
|
|
+ const member = await withProducts('member-repurchase')
|
|
|
+ member.newOrder.recRealName = '-'
|
|
|
+ member.submitOrder()
|
|
|
+ await settle()
|
|
|
+ expect(shop.createReconsumeOrder).not.toHaveBeenCalled()
|
|
|
+ const upgrade = await withProducts('member-upgrade')
|
|
|
+ upgrade.form.insertUserName = ''
|
|
|
+ upgrade.submitOrder()
|
|
|
+ await settle()
|
|
|
+ expect(user.fetchSendUpgrade).not.toHaveBeenCalled()
|
|
|
+})
|
|
|
+
|
|
|
+it('ignores a product response arriving after the member context was cleared', async() => {
|
|
|
+ const vm = await withProducts('member-repurchase')
|
|
|
+ let resolve
|
|
|
+ shop.fetchReconsumeProductList.mockReturnValue(new Promise(done => { resolve = done }))
|
|
|
+ const loading = vm.getList()
|
|
|
+ vm.resetMember()
|
|
|
+ resolve({ data: { list: [product], totalCount: 1 }})
|
|
|
+ await loading
|
|
|
+ expect(vm.tableData).toEqual([])
|
|
|
+ expect(vm.cartItems).toEqual([])
|
|
|
+ expect(vm.fetching).toBe(false)
|
|
|
+})
|
|
|
+
|
|
|
+it('redirects an invalid origin and guards navigation during payment', async() => {
|
|
|
+ const Page = load(path.join(root, 'PurchasePage.vue'))
|
|
|
+ const invalid = new Vue({ ...Page, beforeCreate() { this.$route = { meta: { purchaseStage: 'cart' }, query: { from: '__proto__' }} } }).$mount()
|
|
|
+ expect(router.replace).toHaveBeenCalledWith('/dashboard/index')
|
|
|
+ invalid.$destroy()
|
|
|
+ const page = new Vue({ ...Page, beforeCreate() { this.$route = { meta: { purchaseStage: 'cart' }, query: { from: 'repeat-purchase' }} } }).$mount()
|
|
|
+ instances.push(page)
|
|
|
+ await settle()
|
|
|
+ page.$refs.flow.visible = true
|
|
|
+ const next = jest.fn()
|
|
|
+ page.checkNavigation({ path: '/dashboard/index' }, next)
|
|
|
+ expect(next).toHaveBeenLastCalledWith(false)
|
|
|
+ page.checkNavigation({ path: '/login' }, next)
|
|
|
+ expect(next).toHaveBeenLastCalledWith()
|
|
|
+})
|
|
|
+
|
|
|
+it('does not submit a password prompt resolved after the login session changed', async() => {
|
|
|
+ const vm = await withProducts()
|
|
|
+ let resolve
|
|
|
+ Vue.prototype.$prompt.mockReturnValue(new Promise(done => { resolve = done }))
|
|
|
+ vm.submitOrder()
|
|
|
+ await settle()
|
|
|
+ usersInfo.loginTimestamp.mockReturnValue('login-2')
|
|
|
+ resolve({ value: 'fixture-password' })
|
|
|
+ await settle()
|
|
|
+ expect(shop.createOrder).not.toHaveBeenCalled()
|
|
|
+ vm.$destroy()
|
|
|
+ expect(drafts.readDraft(names[0], 'OP1:login-2')).toBeNull()
|
|
|
+})
|
|
|
+
|
|
|
+it('restricts cart origins and keeps all six new routes out of the login whitelist', () => {
|
|
|
+ expect(routes.default).toHaveLength(6)
|
|
|
+ names.forEach(source => expect(routes.purchasePath(source)).toBe('/dashboard/' + source))
|
|
|
+ ;['__proto__', 'https://example.test', '', undefined].forEach(source => expect(routes.purchasePath(source)).toBe('/dashboard/index'))
|
|
|
+})
|
|
|
+
|
|
|
+it.each(['cart', 'checkout'])('selects the upgrade level through the %s UI and submits the original fields without a new BV gate', async stage => {
|
|
|
+ const vm = await withProducts('member-upgrade')
|
|
|
+ vm.form.upgradeValue = ''
|
|
|
+ vm.toLevel = ''
|
|
|
+ vm.needDiffPv = ''
|
|
|
+ vm.stage = stage
|
|
|
+ await settle()
|
|
|
+ expect(vm.$options.methods.handleChangeLevel).toBe(adapters['member-upgrade'].extends.methods.handleChangeLevel)
|
|
|
+ const input = vm.$el.querySelector('#ng-purchase-upgrade-level')
|
|
|
+ expect(input).not.toBeNull()
|
|
|
+ expect(vm.$el.querySelector('.ng-purchase-upgrade').textContent).toMatch(/Upgrade difference\s*—/)
|
|
|
+ input.click()
|
|
|
+ await settle()
|
|
|
+ const option = Array.from(document.querySelectorAll('.ng-refactor-popper .el-select-dropdown__item')).find(node => node.textContent.trim() === 'Gold')
|
|
|
+ expect(option).toBeDefined()
|
|
|
+ option.click()
|
|
|
+ await settle()
|
|
|
+ expect(vm.form.upgradeValue).toEqual({ value: 'L2', diffPv: 999 })
|
|
|
+ expect(vm.toLevel).toBe('L2')
|
|
|
+ expect(vm.needDiffPv).toBe(999)
|
|
|
+ expect(Number(vm.pricePvSum)).toBe(120)
|
|
|
+ expect(vm.canCheckout).toBe(true)
|
|
|
+ vm.stage = stage === 'cart' ? 'checkout' : 'cart'
|
|
|
+ await settle()
|
|
|
+ expect(vm.$el.querySelector('#ng-purchase-upgrade-level').value).toBe('Gold')
|
|
|
+ vm.submissionLocked = true
|
|
|
+ await settle()
|
|
|
+ expect(vm.$el.querySelector('#ng-purchase-upgrade-level').disabled).toBe(true)
|
|
|
+ vm.submissionLocked = false
|
|
|
+ vm.stage = 'checkout'
|
|
|
+ vm.activePayType = 'cash'
|
|
|
+ await settle()
|
|
|
+ vm.$el.querySelector('.ng-purchase-place-order').click()
|
|
|
+ await settle()
|
|
|
+ expect(user.fetchSendUpgrade).toHaveBeenCalledWith(expect.objectContaining({ decLv: 'L2', goodsId: ['P1'], goodsNum: [2], nowPerf: 10, nextPerf: 1009, province: 'S1', decWay: '2' }))
|
|
|
+})
|
|
|
+
|
|
|
+it('displays an interface-provided zero upgrade difference without replacing it or recalculating it', async() => {
|
|
|
+ const vm = await withProducts('member-upgrade')
|
|
|
+ vm.upgradeOption = [{ ID: 'ZERO', LEVEL_NAME: 'Zero difference', REPAIR_PV: 0 }]
|
|
|
+ vm.form.upgradeValue = ''
|
|
|
+ vm.stage = 'cart'
|
|
|
+ await settle()
|
|
|
+ vm.$el.querySelector('#ng-purchase-upgrade-level').click()
|
|
|
+ await settle()
|
|
|
+ const option = Array.from(document.querySelectorAll('.ng-refactor-popper .el-select-dropdown__item')).find(node => node.textContent.trim() === 'Zero difference')
|
|
|
+ option.click()
|
|
|
+ await settle()
|
|
|
+ expect(vm.toLevel).toBe('ZERO')
|
|
|
+ expect(vm.needDiffPv).toBe(0)
|
|
|
+ expect(vm.$el.querySelector('.ng-purchase-upgrade').textContent).toMatch(/Upgrade difference\s*0 BV/)
|
|
|
+})
|
|
|
+
|
|
|
+it.each(names)('%s connects the shared payment selection without changing totals or submitting', async source => {
|
|
|
+ const vm = await withProducts(source)
|
|
|
+ const radios = Array.from(vm.$el.querySelectorAll('.ng-payment-methods-options input'))
|
|
|
+ expect(radios.map(input => input.value)).toEqual(vm.payList.map(option => option.label))
|
|
|
+ const amount = vm.orderTotal
|
|
|
+ const balance = JSON.stringify(vm.userBalance)
|
|
|
+ radios.find(input => input.value === 'pay_stack').click()
|
|
|
+ await settle()
|
|
|
+ expect(vm.activePayType).toBe('pay_stack')
|
|
|
+ expect(vm.paymentSummary.amount).toBe(amount)
|
|
|
+ expect(JSON.stringify(vm.userBalance)).toBe(balance)
|
|
|
+ expect(shop.createApproachOrder).not.toHaveBeenCalled()
|
|
|
+ expect(shop.createApproachReconsumeOrder).not.toHaveBeenCalled()
|
|
|
+ expect(shop.createApproachAutoMaintenanceOrder).not.toHaveBeenCalled()
|
|
|
+ expect(user.fetchSendUpgrade).not.toHaveBeenCalled()
|
|
|
+ vm.submissionLocked = true
|
|
|
+ await settle()
|
|
|
+ expect(radios.every(input => input.disabled)).toBe(true)
|
|
|
+})
|