Skip to content
Open
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