Skip to content

Instantly share code, notes, and snippets.

@mikeapr4
Created November 24, 2020 11:49
Show Gist options
  • Save mikeapr4/e331ff94a59ba325e705b0ae8616cbd2 to your computer and use it in GitHub Desktop.
Save mikeapr4/e331ff94a59ba325e705b0ae8616cbd2 to your computer and use it in GitHub Desktop.
Sample Code for Review
import Vue from 'vue'
// Convert the API Postage Option format into the NEXT APP Postage option format
const convertPostage = (locationGroups) => (postage) => {
const [add, addQty] = postage.price_addl.split('/')
const option = {
flat: true,
cost: Number(postage.price),
add: Number(add),
addQty: addQty ? Number(addQty) : 1,
label: postage.method,
}
// There may be multiple location groups in the option, but NEXT only supports
// one group per option, so we create multiple options.
return postage.locations.map((index) => ({
dest: locationGroups[Number(index)],
...option,
}))
}
// By default any field with a truthy value will be mapped as is,
// here we add custom validation/mapping functions, if the output of
// this is truthy it will be applied to local data.
const validate = {
description: (v) => {
if (v) {
// Strip HTML
const el = document.createElement('div')
el.innerHTML = v
return el.textContent
}
return v
},
enable_make_offer: (v) => v === '1',
duration: Number,
nb_relists: Number,
country: Number,
state: (v) => Number(v) || v,
accept_returns: (v) => v === '1',
}
const inputSet = (field) => (input, val) => (input[field] = val)
const prefSet = (field) => (input, val) => (input.preferences[field] = val)
const pricingSet = (field) => (input, val) => (input.pricing[field] = val)
// Configure the placement of each field in the frontend data
const placement = {
description: inputSet('desc'),
currency: prefSet('currency'),
enable_make_offer: pricingSet('allowOffers'),
duration: pricingSet('duration'),
nb_relists: (input, val) => {
input.pricing.autoRelist = true
input.pricing.relistQty = val
},
country: prefSet('country'),
state: prefSet('state'),
postal_code: prefSet('zip'),
accept_returns: prefSet('acceptReturns'),
returns_policy: prefSet('returnPolicy'),
}
// uses: $axios, $store, $auth, $t, input
export default {
data() {
return {
defaultsLoading: true,
}
},
async mounted() {
try {
await this.loadDefaults()
} finally {
this.defaultsLoading = false
}
},
methods: {
async loadDefaults() {
try {
const {
results: [rs],
} = await this.$axios.$get('/papi/listings-default')
const input = JSON.parse(JSON.stringify(this.input))
Object.keys(placement).forEach((field) => {
const mapped = validate[field]
? validate[field](rs[field])
: rs[field]
if (mapped) {
placement[field](input, mapped)
}
})
// Wait for location groups to be fully loaded
await this.$store.dispatch('postage/locationGroups')
if (rs.postage && rs.postage.length) {
const mapper = convertPostage(
this.$store.state.postage.locationGroups,
)
input.shipping.services = rs.postage.reduce(
(srvs, srv) => [...srvs, ...mapper(srv)],
[],
)
}
Vue.set(this, 'input', input)
} catch (err) {
if (err.response && err.response.status) {
if (err.response.status === 401) {
setTimeout(() => this.$auth.redirect('login'), 3000)
throw this.$t('401')
}
}
}
},
},
}
/* eslint-disable prefer-promise-reject-errors */
import { mount, createLocalVue } from '@vue/test-utils'
import DefaultListing from '@/mixins/default-listing.js'
import Vuex from 'vuex'
import { passive } from '@/utils/promise'
const localVue = createLocalVue()
localVue.use(Vuex)
let locationGroupPromise = passive()
const store = new Vuex.Store({
modules: {
postage: {
namespaced: true,
state: {
locationGroups: [
{ id: 1, name: 'United States', countries: [2084] },
{ id: 2, name: 'Everywhere Else', countries: [2316] },
{
id: 3,
name: 'South America',
countries: [1874, 1890, 1894, 1908],
},
{ id: 4, name: 'Europe', countries: [2083, 1939, 2058] },
],
},
actions: { locationGroups: () => locationGroupPromise },
},
},
})
let getResp = {
description: 'ACTION COMICS #340 FIRST ...',
currency: 'USD',
enable_make_offer: '1',
duration: '3',
nb_relists: '0',
country: '2084',
state: '2135',
postal_code: '03464',
shipping_details: '',
accept_returns: '',
returns_policy: '',
postage: [
{
price: '6.75',
price_addl: '0',
method: 'PRIORITY FLAT RATE ENVELOPE',
locations: ['0', '2'],
},
],
}
const $axios = {
$get: () => Promise.resolve({ results: [getResp] }),
}
const data = () => ({
input: {
keep: 'this',
pricing: { starting: 'data' },
shipping: { a: 1 },
preferences: { check: 'this' },
},
})
describe('Default Listing Mixin', () => {
beforeEach(() => {
jest.useFakeTimers()
locationGroupPromise = passive()
})
afterEach(() => jest.useRealTimers())
test('happy path', async () => {
const wrapper = mount(
{ template: '<div/>', mixins: [DefaultListing], data },
{ localVue, store, mocks: { $axios } },
)
expect(wrapper.vm.defaultsLoading).toBe(true)
expect(wrapper.vm.input).toEqual(data().input)
locationGroupPromise.resolve()
await wrapper.vm.$nextTick() // store action completes
await wrapper.vm.$nextTick() // loadDefaults completes
await wrapper.vm.$nextTick() // mounted finally completes
expect(wrapper.vm.defaultsLoading).toBe(false)
expect(wrapper.vm.input).toEqual({
keep: 'this', // preexisting
desc: 'ACTION COMICS #340 FIRST ...',
preferences: {
check: 'this', // preexisting
country: 2084,
currency: 'USD',
state: 2135,
zip: '03464',
},
pricing: {
starting: 'data', // preexisting
allowOffers: true,
duration: 3,
},
shipping: {
a: 1, // preexisting
services: [
{
add: 0,
addQty: 1,
cost: 6.75,
dest: {
countries: [2084],
id: 1,
name: 'United States',
},
flat: true,
label: 'PRIORITY FLAT RATE ENVELOPE',
},
{
add: 0,
addQty: 1,
cost: 6.75,
dest: {
id: 3,
name: 'South America',
countries: [1874, 1890, 1894, 1908],
},
flat: true,
label: 'PRIORITY FLAT RATE ENVELOPE',
},
],
},
})
})
test('description with html', async () => {
getResp = {
description:
'<p>something here in <b>bold</b> &amp; a &lt; less than</p>',
}
const wrapper = mount(
{ template: '<div/>', mixins: [DefaultListing], data },
{ localVue, store, mocks: { $axios } },
)
locationGroupPromise.resolve()
await wrapper.vm.$nextTick() // store action completes
await wrapper.vm.$nextTick() // loadDefaults completes
await wrapper.vm.$nextTick() // mounted finally completes
expect(wrapper.vm.defaultsLoading).toBe(false)
expect(wrapper.vm.input.desc).toEqual(
'something here in bold & a < less than',
)
})
test('relists', async () => {
getResp = {
nb_relists: '3',
}
const wrapper = mount(
{ template: '<div/>', mixins: [DefaultListing], data },
{ localVue, store, mocks: { $axios } },
)
locationGroupPromise.resolve()
await wrapper.vm.$nextTick() // store action completes
await wrapper.vm.$nextTick() // loadDefaults completes
await wrapper.vm.$nextTick() // mounted finally completes
expect(wrapper.vm.defaultsLoading).toBe(false)
expect(wrapper.vm.input.pricing).toEqual({
starting: 'data', // preexisting
autoRelist: true,
relistQty: 3,
})
})
test('freetext state', async () => {
getResp = {
country: '123',
state: 'Leinster',
}
const wrapper = mount(
{ template: '<div/>', mixins: [DefaultListing], data },
{ localVue, store, mocks: { $axios } },
)
locationGroupPromise.resolve()
await wrapper.vm.$nextTick() // store action completes
await wrapper.vm.$nextTick() // loadDefaults completes
await wrapper.vm.$nextTick() // mounted finally completes
expect(wrapper.vm.defaultsLoading).toBe(false)
expect(wrapper.vm.input.preferences).toEqual({
check: 'this', // preexisting
country: 123,
state: 'Leinster',
})
})
test('accept returns', async () => {
getResp = {
accept_returns: '1',
returns_policy: 'policy',
}
const wrapper = mount(
{ template: '<div/>', mixins: [DefaultListing], data },
{ localVue, store, mocks: { $axios } },
)
locationGroupPromise.resolve()
await wrapper.vm.$nextTick() // store action completes
await wrapper.vm.$nextTick() // loadDefaults completes
await wrapper.vm.$nextTick() // mounted finally completes
expect(wrapper.vm.defaultsLoading).toBe(false)
expect(wrapper.vm.input.preferences).toEqual({
check: 'this', // preexisting
acceptReturns: true,
returnPolicy: 'policy',
})
})
test('postage variant', async () => {
getResp = {
postage: [
{
price: '5.5',
price_addl: '4/5',
method: 'Another',
locations: ['1'],
},
],
}
const wrapper = mount(
{ template: '<div/>', mixins: [DefaultListing], data },
{ localVue, store, mocks: { $axios } },
)
locationGroupPromise.resolve()
await wrapper.vm.$nextTick() // store action completes
await wrapper.vm.$nextTick() // loadDefaults completes
await wrapper.vm.$nextTick() // mounted finally completes
expect(wrapper.vm.defaultsLoading).toBe(false)
expect(wrapper.vm.input.shipping.services).toEqual([
{
add: 4,
addQty: 5,
cost: 5.5,
dest: {
countries: [2316],
id: 2,
name: 'Everywhere Else',
},
flat: true,
label: 'Another',
},
])
})
// If the API fails, the user has the option to continue and manually set their fields,
// or exit and re-enter the page. This provides flexibility both ways.
test('500 error', async () => {
$axios.$get = () => Promise.reject({ response: { status: 500 } })
const wrapper = mount(
{ template: '<div/>', mixins: [DefaultListing], data },
{ localVue, store, mocks: { $axios } },
)
await wrapper.vm.$nextTick() // loadDefaults completes
await wrapper.vm.$nextTick() // mounted finally completes
expect(wrapper.vm.defaultsLoading).toBe(false)
expect(wrapper.vm.input).toEqual(data().input)
})
// Even though we have the XHR encapsulated refresh token retry, maybe
// that doesn't work, so if not, we need to redirect the user to login.
test('401 error', async () => {
// this test logs a lot
const spy = jest.spyOn(global.console, 'error').mockImplementation(() => {})
$axios.$get = () => Promise.reject({ response: { status: 401 } })
const $auth = { redirect: jest.fn() }
try {
mount(
{ template: '<div/>', mixins: [DefaultListing], data },
{ localVue, store, mocks: { $axios, $auth } },
)
} catch (err) {
expect(err).toBe('i18n:401')
await jest.runAllTimers()
expect($auth.redirect).toHaveBeenCalledWith('login')
spy.mockRestore()
}
})
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment