Skip to content

Unit test route components #339

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
12 changes: 10 additions & 2 deletions .babelrc
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@
["env", { "modules": false }]
],
"plugins": [
"syntax-dynamic-import"
]
"syntax-dynamic-import",
"transform-runtime"
],
"env": {
"test": {
"presets": [
["env", { "targets": { "node": "current" }}]
]
}
}
}
25 changes: 25 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
module.exports = {
"moduleFileExtensions": [
"js",
"json",
// tell Jest to handle *.vue files
"vue"
],
"transform": {
// process js with babel-jest
"^.+\\.js$": "<rootDir>/node_modules/babel-jest",
// process *.vue files with vue-jest
".*\\.(vue)$": "<rootDir>/node_modules/vue-jest"
},
// support the same @ -> src alias mapping in source code
"moduleNameMapper": {
"^@/(.*)$": "<rootDir>/src/$1",
'../api': '<rootDir>/src/api/__mocks__/fake-api.js'
},
// serializer for snapshots
"snapshotSerializers": [
"<rootDir>/node_modules/jest-serializer-vue"
],
"setupTestFrameworkScriptFile": "<rootDir>/src/jest-setup.js"

}
9 changes: 8 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"start": "cross-env NODE_ENV=production node server",
"build": "rimraf dist && npm run build:client && npm run build:server",
"build:client": "cross-env NODE_ENV=production webpack --config build/webpack.client.config.js --progress --hide-modules",
"build:server": "cross-env NODE_ENV=production webpack --config build/webpack.server.config.js --progress --hide-modules"
"build:server": "cross-env NODE_ENV=production webpack --config build/webpack.server.config.js --progress --hide-modules",
"test": "NODE_ENV=test jest -c jest.config.js"
},
"engines": {
"node": ">=7.0",
Expand All @@ -31,20 +32,26 @@
"vuex-router-sync": "^5.0.0"
},
"devDependencies": {
"@babel/plugin-transform-runtime": "^7.0.0",
"@vue/test-utils": "^1.0.0-beta.24",
"autoprefixer": "^7.1.6",
"babel-core": "^6.26.0",
"babel-loader": "^7.1.2",
"babel-plugin-syntax-dynamic-import": "^6.18.0",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-env": "^1.6.1",
"chokidar": "^1.7.0",
"css-loader": "^0.28.7",
"file-loader": "^1.1.5",
"friendly-errors-webpack-plugin": "^1.6.1",
"jest": "^23.5.0",
"jest-serializer-vue": "^2.0.2",
"rimraf": "^2.6.2",
"stylus": "^0.54.5",
"stylus-loader": "^3.0.1",
"sw-precache-webpack-plugin": "^0.11.4",
"url-loader": "^0.6.2",
"vue-jest": "^2.6.0",
"vue-loader": "^15.0.0-beta.1",
"vue-template-compiler": "^2.5.16",
"webpack": "^3.8.1",
Expand Down
24 changes: 24 additions & 0 deletions src/api/__mocks__/fake-api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { addedItemId, fakeItemList, fakeUser, newItemListAfterAddingNewItem } from '../../../test/fake-data'

export function fetchIdsByType(type) {
return Promise.resolve(Object.keys(fakeItemList))
}

export function fetchItem (id) {
return Promise.resolve(fakeItemList[id])
}

export function watchList (type, cb) {
cb(newItemListAfterAddingNewItem)
}

export function fetchItems (ids) {
return Promise.all(ids.map(id => fetchItem(id)))
}

export function fetchUser (id) {
if (id === fakeUser.id) return Promise.resolve(fakeUser)
return Promise.reject('User not found')
}


9 changes: 9 additions & 0 deletions src/jest-setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Vue from 'vue'
import * as filters from './util/filters'

// We would extract this to a function that would be reused by both app.js and jest-setup but,
// we didn't want to change original production code
Object.keys(filters).forEach(key => {
Vue.filter(key, filters[key])
})

108 changes: 108 additions & 0 deletions src/views/__tests__/CreateListView.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { mount } from '@vue/test-utils'
import { addedItemId, fakeItemList, newItemListAfterAddingNewItem } from '../../../test/fake-data'
import { resolvePromises } from '../../../test/test-utils'
import { createStore } from '../../store'
import CreateListView from '../CreateListView'

const PAGE_TYPES = ['top', 'new', 'show', 'ask', 'job']

let wrapper, store, routerSpy
describe('CreateListView.vue', () => {

beforeEach(()=> {
store = createStore()
routerSpy = jest.fn()
})

it('shows number of available pages', async () => {
const page = 1
wrapper = await renderComponent('top', page)

expect(wrapper.find('.news-list-nav').text()).toBe('< prev 1/3 more >')
})

it('shows current page in paginator', async () => {
const currentPage = 2
wrapper = await renderComponent('top', currentPage)

expect(wrapper.find('.news-list-nav').text()).toBe('< prev 2/3 more >')
})

PAGE_TYPES.forEach(async (type) => {
it('calls FETCH_LIST_DATA action for page ' + type, async () => {
const dispatchSpy = jest.spyOn(store, 'dispatch')

wrapper = await renderComponent(type)

expect(dispatchSpy).toHaveBeenCalledWith('FETCH_LIST_DATA', {"type": type})
expect(dispatchSpy.mock.calls.length).toBe(3)
})
})

it('loads 20 items', async () => {
wrapper = await renderComponent('top')

expect(wrapper.findAll('.news-item')).toHaveLength(20)
})

describe('When new item is added in real time', ()=> {

it('ENSURE_ACTIVE_ITEMS action is dispatched', async () => {
const dispatchSpy = jest.spyOn(store, 'dispatch')

wrapper = await renderComponent('top')

expect(dispatchSpy).toHaveBeenCalledWith('ENSURE_ACTIVE_ITEMS')
})

it('The new list is set', async () => {
const commitSpy = jest.spyOn(store, 'commit')

wrapper = await renderComponent('top')
expect(commitSpy).toHaveBeenCalledWith('SET_LIST', {"ids": newItemListAfterAddingNewItem, "type": "top"})
expect(wrapper.text()).toContain(fakeItemList[addedItemId].title)
})

it('The title of the new added item is rendered', async () => {
wrapper = await renderComponent('top')

expect(wrapper.text()).toContain(fakeItemList[addedItemId].title)
})
})

})

async function renderComponent(type, page) {
const $route = {
path: '/some/path',
params: { page }
}
store.state.route = $route

const mixin = {
beforeMount: function () {
this.$root = {
_isMounted: true
}
}
}

const wrapper = mount(CreateListView(type), { store,
propsData: {
type: 'type',
},
mocks: {
$route,
$bar: { start: jest.fn(), finish: jest.fn() }
},
stubs: ['router-link'],
mixins: [mixin]
})

wrapper.vm.$options.asyncData({ store })
await resolvePromises()

return wrapper
}


64 changes: 64 additions & 0 deletions src/views/__tests__/ItemView.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { mount } from '@vue/test-utils'
import { resolvePromises } from '../../../test/test-utils'
import { createStore } from '../../store'
import { fakeItem } from '../../../test/fake-data'
import ItemView from '../ItemView'


let wrapper, store
describe('ItemView.vue', () => {

beforeEach(()=> {
store = createStore()
})

it('Renders item title', async () => {
wrapper = await renderComponent(fakeItem.id)
expect(wrapper.text()).toContain(fakeItem.title)
})

it('Renders item host', async () => {
fakeItem.url = 'https://www.fake.domain.com/link/fake-uri'
wrapper = await renderComponent(fakeItem.id)
expect(wrapper.text()).toContain('fake.domain.com')
})

it('Renders item user', async () => {
wrapper = await renderComponent(fakeItem.id)
expect(wrapper.text()).toContain('| by ' + fakeItem.by)
})

it('Calls the action to fetch the item by id', async () => {
const dispatchSpy = jest.spyOn(store, 'dispatch')
wrapper = renderComponent(fakeItem.id)
expect(dispatchSpy).toHaveBeenCalledWith('FETCH_ITEMS', { ids: [fakeItem.id] })
})

it('Calls the action to fetch the comments by id', async () => {
const dispatchSpy = jest.spyOn(store, 'dispatch')
wrapper = await renderComponent(fakeItem.id)
expect(dispatchSpy).toHaveBeenCalledWith('FETCH_ITEMS', { ids: fakeItem.kids })
})
})

async function renderComponent(id) {
const route = {
path: '/item',
params: { id }
}
store.state.route = route

const wrapper = mount(ItemView, { store,
mocks: {
$route: route,
},
stubs: ['router-link']
})

wrapper.vm.$options.asyncData({ store, route })
await resolvePromises()

return wrapper
}


55 changes: 55 additions & 0 deletions src/views/__tests__/UserView.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { mount } from '@vue/test-utils'
import { fakeUser } from '../../../test/fake-data'
import { resolvePromises } from '../../../test/test-utils'
import { createStore } from '../../store'
import UserView from '../UserView'

let wrapper, store, route
describe('UserView.vue', () => {

beforeEach(() => {
route = userRoute(fakeUser.id)
store = createStore()
})

it('Renders user id', async () => {
wrapper = await renderComponent(route)
expect(wrapper.text()).toContain(`User : ${fakeUser.id}`)
})

it('Renders time since creation', async () => {
fakeUser.created = new Date('September 07 2018')/1000
Date.now = jest.fn(() => new Date('September 09 2018'))

wrapper = await renderComponent(route)

expect(wrapper.text()).toContain('2 days ago')
})

it('Calls the action to fetch the user by id', async () => {
const dispatchSpy = jest.spyOn(store, 'dispatch')

await renderComponent(route)

expect(dispatchSpy).toHaveBeenCalledWith('FETCH_USER', { id: fakeUser.id })
expect(dispatchSpy.mock.calls.length).toBe(1)
})
})

const userRoute = (id) => ({
path: '/user',
params: { id }
})

const renderComponent = async route => {
const wrapper = mount(UserView, { store,
mocks: {
$route: route,
}
})

wrapper.vm.$options.asyncData({ store, route })
await resolvePromises()

return wrapper
}
17 changes: 17 additions & 0 deletions test/fake-data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import news from './fake-news.json'

const arrayToObject = (array) =>
array.reduce((obj, item) => {
obj[item.id] = item
return obj
}, {})

export const fakeItemList = arrayToObject(news)

const anItemId = 17944752
export const fakeItem = fakeItemList[anItemId]

export const fakeUser = { id: 17944752}

export const addedItemId = 17938548
export const newItemListAfterAddingNewItem = [ 1, 2, 3, 4, 5, addedItemId, 6, 7, 8, 9 ]
Loading