├── .browserslistrc ├── db.json ├── .prettierrc.js ├── babel.config.js ├── public ├── favicon.ico └── index.html ├── src ├── assets │ └── logo.png ├── views │ ├── About.vue │ └── Home.vue ├── store │ └── index.js ├── services │ └── axios.js ├── main.js ├── components │ ├── MessageContainer.vue │ ├── AppHeader.vue │ ├── LoginForm.vue │ ├── MessageDisplay.vue │ └── RandomNumber.vue ├── App.vue └── router │ └── index.js ├── jest.config.js ├── .gitignore ├── README.md ├── tests └── unit │ ├── AppHeader.spec.js │ ├── MessageContainer.spec.js │ ├── LoginForm.spec.js │ ├── RandomNumber.spec.js │ └── MessageDisplay.spec.js ├── .eslintrc.js └── package.json /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /db.json: -------------------------------------------------------------------------------- 1 | { 2 | "message": { "text": "Hello from the db!" } 3 | } -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | semi: false 4 | } 5 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@vue/cli-plugin-babel/preset"] 3 | }; 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-Pop/Unit-Testing-Vue-3/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-Pop/Unit-Testing-Vue-3/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /src/views/About.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "@vue/cli-plugin-unit-jest", 3 | transform: { 4 | "^.+\\.vue$": "vue-jest" 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore } from "vuex"; 2 | 3 | export default createStore({ 4 | state: {}, 5 | mutations: {}, 6 | actions: {}, 7 | modules: {} 8 | }); 9 | -------------------------------------------------------------------------------- /src/services/axios.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | export function getMessage() { 4 | return axios.get('http://localhost:3000/message').then(response => { 5 | return response.data 6 | }) 7 | } 8 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import App from "./App.vue"; 3 | import router from "./router"; 4 | import store from "./store"; 5 | 6 | createApp(App) 7 | .use(store) 8 | .use(router) 9 | .mount("#app"); 10 | -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | -------------------------------------------------------------------------------- /src/components/MessageContainer.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /src/components/AppHeader.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | 17 | 27 | -------------------------------------------------------------------------------- /src/components/LoginForm.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # unit-testing-vue 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Run your unit tests 19 | ``` 20 | npm run test:unit 21 | ``` 22 | 23 | ### Lints and fixes files 24 | ``` 25 | npm run lint 26 | ``` 27 | 28 | ### Customize configuration 29 | See [Configuration Reference](https://cli.vuejs.org/config/). 30 | -------------------------------------------------------------------------------- /src/components/MessageDisplay.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 25 | -------------------------------------------------------------------------------- /tests/unit/AppHeader.spec.js: -------------------------------------------------------------------------------- 1 | import AppHeader from '@/components/AppHeader' 2 | import { mount } from '@vue/test-utils' 3 | 4 | describe('AppHeader', () => { 5 | test('if user is not logged in, do not show logout button', () => { 6 | const wrapper = mount(AppHeader) 7 | expect(wrapper.find('button').isVisible()).toBe(false) 8 | }) 9 | 10 | test('if logged in, show logout button', async () => { 11 | const wrapper = mount(AppHeader) 12 | await wrapper.setData({ loggedIn: true }) 13 | 14 | expect(wrapper.find('button').isVisible()).toBe(true) 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 31 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: ["plugin:vue/vue3-essential", "eslint:recommended", "@vue/prettier"], 7 | parserOptions: { 8 | parser: "babel-eslint" 9 | }, 10 | rules: { 11 | "no-console": process.env.NODE_ENV === "production" ? "warn" : "off", 12 | "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off" 13 | }, 14 | overrides: [ 15 | { 16 | files: [ 17 | "**/__tests__/*.{j,t}s?(x)", 18 | "**/tests/unit/**/*.spec.{j,t}s?(x)" 19 | ], 20 | env: { 21 | jest: true 22 | } 23 | } 24 | ] 25 | }; 26 | -------------------------------------------------------------------------------- /src/components/RandomNumber.vue: -------------------------------------------------------------------------------- 1 | 7 | 32 | -------------------------------------------------------------------------------- /tests/unit/MessageContainer.spec.js: -------------------------------------------------------------------------------- 1 | import MessageContainer from '@/components/MessageContainer' 2 | import { mount } from '@vue/test-utils' 3 | 4 | describe('MessageContainer', () => { 5 | it('Wraps MessageDisplay component', () => { 6 | const wrapper = mount(MessageContainer, { 7 | global: { 8 | stubs: { 9 | MessageDisplay: { 10 | template: '

Hello from the db!

' 11 | } 12 | } 13 | } 14 | }) 15 | 16 | const stubMessage = 'Hello from the db!' 17 | const message = wrapper.find('[data-testid="message"]').text() 18 | expect(message).toEqual(stubMessage) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from "vue-router"; 2 | import Home from "../views/Home.vue"; 3 | 4 | const routes = [ 5 | { 6 | path: "/", 7 | name: "Home", 8 | component: Home 9 | }, 10 | { 11 | path: "/about", 12 | name: "About", 13 | // route level code-splitting 14 | // this generates a separate chunk (about.[hash].js) for this route 15 | // which is lazy-loaded when the route is visited. 16 | component: () => 17 | import(/* webpackChunkName: "about" */ "../views/About.vue") 18 | } 19 | ]; 20 | 21 | const router = createRouter({ 22 | history: createWebHistory(process.env.BASE_URL), 23 | routes 24 | }); 25 | 26 | export default router; 27 | -------------------------------------------------------------------------------- /tests/unit/LoginForm.spec.js: -------------------------------------------------------------------------------- 1 | import LoginForm from '@/components/LoginForm.vue' 2 | import { mount } from '@vue/test-utils' 3 | 4 | describe('LoginForm', () => { 5 | it('emits an event with user data payload', () => { 6 | const wrapper = mount(LoginForm) 7 | const input = wrapper.find('input[type="text"]') // Find text input 8 | 9 | input.setValue('Adam Jahr') // Set value for text input 10 | wrapper.trigger('submit') // Simulate form submission 11 | 12 | // Assert event has been emitted 13 | const formSubmittedCalls = wrapper.emitted('formSubmitted') 14 | expect(formSubmittedCalls).toHaveLength(1) 15 | 16 | // Assert payload is correct 17 | const expectedPayload = { name: 'Adam Jahr' } 18 | expect(wrapper.emitted('formSubmitted')[0][0]).toMatchObject( 19 | expectedPayload 20 | ) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unit-testing-vue", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "test:unit": "vue-cli-service test:unit", 9 | "lint": "vue-cli-service lint" 10 | }, 11 | "dependencies": { 12 | "axios": "^0.21.1", 13 | "core-js": "^3.6.5", 14 | "vue": "^3.0.0", 15 | "vue-router": "^4.0.0-0", 16 | "vuex": "^4.0.0-0" 17 | }, 18 | "devDependencies": { 19 | "@vue/cli-plugin-babel": "~4.5.0", 20 | "@vue/cli-plugin-eslint": "~4.5.0", 21 | "@vue/cli-plugin-router": "~4.5.0", 22 | "@vue/cli-plugin-unit-jest": "~4.5.0", 23 | "@vue/cli-plugin-vuex": "~4.5.0", 24 | "@vue/cli-service": "~4.5.0", 25 | "@vue/compiler-sfc": "^3.0.0", 26 | "@vue/eslint-config-prettier": "^6.0.0", 27 | "@vue/test-utils": "^2.0.0-0", 28 | "babel-eslint": "^10.1.0", 29 | "eslint": "^6.7.2", 30 | "eslint-plugin-prettier": "^3.1.3", 31 | "eslint-plugin-vue": "^7.0.0-0", 32 | "flush-promises": "^1.0.2", 33 | "prettier": "^1.19.1", 34 | "typescript": "~3.9.3", 35 | "vue-jest": "^5.0.0-0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/unit/RandomNumber.spec.js: -------------------------------------------------------------------------------- 1 | import RandomNumber from '@/components/RandomNumber' 2 | import { mount } from '@vue/test-utils' 3 | 4 | describe('RandomNumber', () => { 5 | test('By default, randomNumber data value should be 0', () => { 6 | const wrapper = mount(RandomNumber) 7 | expect(wrapper.html()).toContain('0') 8 | }) 9 | 10 | test('If button is clicked, randomNumber should be between 1 and 10', async () => { 11 | const wrapper = mount(RandomNumber) 12 | await wrapper.find('button').trigger('click') 13 | const randomNumber = parseInt(wrapper.find('span').text()) 14 | expect(randomNumber).toBeGreaterThanOrEqual(1) 15 | expect(randomNumber).toBeLessThanOrEqual(10) 16 | }) 17 | 18 | test('If button is clicked, randomNumber should be between 200 and 300', async () => { 19 | const wrapper = mount(RandomNumber, { 20 | props: { 21 | min: 200, 22 | max: 300 23 | } 24 | }) 25 | await wrapper.find('button').trigger('click') 26 | const randomNumber = parseInt(wrapper.find('span').text()) 27 | expect(randomNumber).toBeGreaterThanOrEqual(200) 28 | expect(randomNumber).toBeLessThanOrEqual(300) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /tests/unit/MessageDisplay.spec.js: -------------------------------------------------------------------------------- 1 | import MessageDisplay from '@/components/MessageDisplay' 2 | import { mount } from '@vue/test-utils' 3 | import { getMessage } from '@/services/axios' 4 | import flushPromises from 'flush-promises' 5 | 6 | jest.mock('@/services/axios') 7 | 8 | beforeEach(() => { 9 | jest.clearAllMocks() 10 | }) 11 | 12 | describe('MessageDisplay', () => { 13 | it('Calls getMessage once and displays message', async () => { 14 | const mockMessage = 'Hello from the db' 15 | getMessage.mockResolvedValueOnce({ text: mockMessage }) 16 | const wrapper = mount(MessageDisplay) 17 | 18 | await flushPromises() 19 | expect(getMessage).toHaveBeenCalledTimes(1) 20 | 21 | const message = wrapper.find('[data-testid="message"]').text() 22 | expect(message).toEqual(mockMessage) 23 | }) 24 | 25 | it('Displays an error when getMessage call fails', async () => { 26 | const mockError = 'Oops! Something went wrong.' 27 | getMessage.mockRejectedValueOnce(mockError) 28 | const wrapper = mount(MessageDisplay) 29 | 30 | await flushPromises() 31 | expect(getMessage).toHaveBeenCalledTimes(1) 32 | const displayedError = wrapper.find('[data-testid="message-error"]').text() 33 | expect(displayedError).toEqual(mockError) 34 | }) 35 | }) 36 | --------------------------------------------------------------------------------