├── .DS_Store ├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── README.md ├── babel.config.js ├── docs └── images │ └── project.png ├── jest.config.js ├── netlify.toml ├── package-lock.json ├── package.json ├── postcss.config.js ├── prettier.config.js ├── public ├── favicon.ico ├── img │ └── icons │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon-120x120.png │ │ ├── apple-touch-icon-152x152.png │ │ ├── apple-touch-icon-180x180.png │ │ ├── apple-touch-icon-60x60.png │ │ ├── apple-touch-icon-76x76.png │ │ ├── apple-touch-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── msapplication-icon-144x144.png │ │ ├── mstile-150x150.png │ │ └── safari-pinned-tab.svg ├── index.html ├── manifest.json └── robots.txt ├── src ├── .DS_Store ├── App.vue ├── assets │ ├── app.scss │ └── logo.png ├── components │ ├── .DS_Store │ ├── AppHeader.vue │ ├── AppLoadingIndicator.vue │ ├── Boards │ │ ├── .DS_Store │ │ └── TaskBoardEdit.vue │ ├── Details │ │ ├── DetailsDropdown.vue │ │ ├── DetailsInline.vue │ │ └── DetailsPopup.vue │ ├── Items │ │ └── TaskListItem.vue │ └── Lists │ │ ├── TaskList.vue │ │ ├── TaskListActions.vue │ │ ├── TaskListArchive.vue │ │ ├── TaskListEdit.vue │ │ └── TaskListRestore.vue ├── main.js ├── plugins │ ├── device-detect.js │ ├── index.js │ └── vee-validate.js ├── router │ ├── index.js │ └── routes.js ├── store │ ├── actions.js │ ├── getters.js │ ├── index.js │ ├── mutations.js │ └── state.js ├── utils │ └── bus.js └── views │ ├── Dashboard.vue │ └── TaskBoard.vue ├── tests └── unit │ ├── .eslintrc.js │ ├── App.spec.js │ ├── components │ ├── AppHeader.spec.js │ ├── AppLoadingIndicator.spec.js │ ├── Boards │ │ └── TaskBoardEdit.spec.js │ ├── Details │ │ ├── DetailsDropdown.spec.js │ │ ├── DetailsInline.spec.js │ │ └── DetailsPopup.spec.js │ ├── Items │ │ └── TaskListItem.spec.js │ └── Lists │ │ ├── TaskList.spec.js │ │ ├── TaskListActions.spec.js │ │ ├── TaskListArchive.spec.js │ │ ├── TaskListEdit.spec.js │ │ └── TaskListRestore.spec.js │ ├── store │ ├── actions.spec.js │ ├── getters.spec.js │ └── mutations.spec.js │ └── views │ ├── Dashboard.spec.js │ └── TaskBoard.spec.js └── vue.config.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techlab23/task-management-app/ef643da476490e89794a4477784421dea825deb5/.DS_Store -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: ["plugin:vue/essential", "@vue/prettier"], 7 | rules: { 8 | "no-console": process.env.NODE_ENV === "production" ? "error" : "off", 9 | "no-debugger": process.env.NODE_ENV === "production" ? "error" : "off" 10 | }, 11 | parserOptions: { 12 | parser: "babel-eslint" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | node_modules 3 | dist 4 | .netlify 5 | .DS_Store 6 | src/components/.DS_Store 7 | tests/coverage/** -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 8 4 | cache: 5 | directories: 6 | - node_modules 7 | script: 8 | - npm run test:unit 9 | after_script: 10 | - cat ./tests/coverage/lcov.info | coveralls 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Task Management Application using Vue.js 2 | 3 | [![Build Status](https://travis-ci.org/techlab23/task-management-app.svg?branch=master)](https://travis-ci.org/techlab23/task-management-app) [![Coverage Status](https://coveralls.io/repos/github/techlab23/task-management-app/badge.svg)](https://coveralls.io/github/techlab23/task-management-app) 4 | 5 | This is the supporting github repository for **Task Management Application** articles 6 | 7 | - [Task Management Application Using Vue.js - Part 1](https://medium.com/@_shirish/task-management-application-using-vue-js-part-1-df607ca30f48) 8 | - [Task Management Application Using Vue.js - Part 2](https://levelup.gitconnected.com/task-management-application-using-vue-js-part-2-d785a96acda6) 9 | 10 | The app allows you to manage projects and tasks visually using board. One board represents one project. A board contains one or more lists. Each list represents the category of tasks, such as Todo, Doing and Done. You can drag-n-drop to change the order of lists, the order of tasks inside the list, and also move task from one list to another as well- to manage the project as it moves through different stages. 11 | 12 | If you haven't read the earlier article in this series, then here is the article link 13 | [**Thinking in components with vue.js**](https://medium.com/@_shirish/thinking-in-components-with-vue-js-a35b5af12df) 14 | 15 | ![Project Image](/docs/images/project.png) 16 | 17 | ## Application features 18 | 19 | This is a multi-page application with a rich feature set implemented right from the start. Below is the feature list of this app. 20 | 21 | ### Boards 22 | 23 | Allow user to, 24 | 25 | - view existing boards in the dashboard 26 | - archive and restore the boards 27 | - view individual board contents 28 | - create a new board 29 | - edit existing board information 30 | 31 | ### Lists 32 | 33 | Allow user to, 34 | 35 | - create a new list 36 | - edit list name 37 | - archive and restore lists 38 | - rearrange the lists in the board using drag and drop 39 | 40 | ### List Items 41 | 42 | Allow user to, 43 | 44 | - create new and update existing items in the list 45 | - rearrange the items in the list by drag and drop 46 | - move tasks among the lists using drag and drop 47 | 48 | ## Application Demo 49 | 50 | - [Surge.sh](http://kanban-board-demo.surge.sh) 51 | - [Netlify.com](https://task-management-app.netlify.app) 52 | 53 | ##### One click deployment to netlify 54 | 55 | [![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/techlab23/task-management-app) 56 | 57 | ## Build Setup 58 | 59 | ```bash 60 | # install dependencies 61 | npm install 62 | 63 | # serve with hot reload at localhost:8080 64 | npm run serve 65 | 66 | # build for production with minification 67 | npm run build 68 | 69 | # run unit tests in watch mode 70 | npm run test:unit 71 | 72 | # run test coverage 73 | npm run test:coverage 74 | 75 | # To deploy your app on surge make sure you have surge cli 76 | # installed globally on your machine. 77 | # If you have it insalled already then feel free to skip this step 78 | npm install -g surge 79 | 80 | # Build and deploy the app on surge.sh in staging environment 81 | # Note: Before running this command, please change the site url 82 | # used for this command in package.json file. 83 | npm run deploy-staging 84 | 85 | # Build and deploy the app on surge.sh in production environment 86 | # Note: Before running this command, please change the site url 87 | # used for this command in package.json file. 88 | npm run deploy 89 | ``` 90 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@vue/cli-plugin-babel/preset"] 3 | } 4 | -------------------------------------------------------------------------------- /docs/images/project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techlab23/task-management-app/ef643da476490e89794a4477784421dea825deb5/docs/images/project.png -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ["js", "jsx", "json", "vue"], 3 | transform: { 4 | "^.+\\.vue$": "vue-jest", 5 | ".+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$": "jest-transform-stub", 6 | "^.+\\.jsx?$": "/node_modules/babel-jest" 7 | }, 8 | transformIgnorePatterns: ["/node_modules/"], 9 | moduleNameMapper: { 10 | "^@/(.*)$": "/src/$1" 11 | }, 12 | snapshotSerializers: ["jest-serializer-vue"], 13 | testMatch: ["**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"], 14 | testURL: "http://localhost/", 15 | watchPlugins: ["jest-watch-typeahead/filename", "jest-watch-typeahead/testname"], 16 | collectCoverage: true, 17 | collectCoverageFrom: [ 18 | "**/*.{js,vue}", 19 | "!**/node_modules/**", 20 | "!**/dist/**", 21 | "!**/tests/**", 22 | "!**/tests/unit/**", 23 | "!**/docs/**", 24 | "!**/public/**" 25 | ], 26 | coverageReporters: ["lcov", "text-summary"], 27 | coverageDirectory: "/tests/coverage" 28 | } 29 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | # example netlify.toml 2 | [build] 3 | command = "npm run build" 4 | publish = "dist" 5 | 6 | ## Uncomment to use this redirect for Single Page Applications like create-react-app. 7 | ## Not needed for static site generators. 8 | [[redirects]] 9 | from = "/*" 10 | to = "/index.html" 11 | status = 200 12 | 13 | ## (optional) Settings for Netlify Dev 14 | ## https://github.com/netlify/netlify-dev-plugin#project-detection 15 | #[dev] 16 | # command = "yarn start" # Command to start your dev server 17 | # port = 3000 # Port that the dev server will be listening on 18 | # publish = "dist" # Folder with the static content for _redirect file 19 | 20 | ## more info on configuring this file: https://www.netlify.com/docs/netlify-toml-reference/ 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "task-management-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "description": "Task Management Application using Vue.js", 6 | "author": "Shirish Nigam", 7 | "scripts": { 8 | "serve": "vue-cli-service serve", 9 | "build": "vue-cli-service build", 10 | "test:unit": "vue-cli-service test:unit", 11 | "lint": "vue-cli-service lint", 12 | "deploy": "vue-cli-service build && cd dist && surge ./ kanban-board-demo.surge.sh", 13 | "deploy-staging": "vue-cli-service build && cd dist && surge ./ kanban-board-demo-staging.surge.sh", 14 | "test:coverage": "vue-cli-service test:unit --coverage", 15 | "test:unit-watch": "vue-cli-service test:unit --watchAll" 16 | }, 17 | "dependencies": { 18 | "axios": "^0.19.2", 19 | "bootstrap": "^4.5.2", 20 | "core-js": "^3.6.5", 21 | "vee-validate": "^2.2.15", 22 | "vue": "^2.6.12", 23 | "vue-router": "^3.4.3", 24 | "vuedraggable": "^2.24.1", 25 | "vuex": "^3.5.1" 26 | }, 27 | "devDependencies": { 28 | "@fullhuman/postcss-purgecss": "^1.3.0", 29 | "@vue/cli-plugin-babel": "^4.5.4", 30 | "@vue/cli-plugin-eslint": "^4.5.4", 31 | "@vue/cli-plugin-unit-jest": "^4.5.4", 32 | "@vue/cli-service": "^4.5.4", 33 | "@vue/eslint-config-prettier": "^6.0.0", 34 | "@vue/test-utils": "1.0.0-beta.29", 35 | "babel-core": "7.0.0-bridge.0", 36 | "babel-eslint": "^10.1.0", 37 | "babel-jest": "^23.6.0", 38 | "coveralls": "^3.0.4", 39 | "eslint": "^6.7.2", 40 | "eslint-plugin-prettier": "^3.1.3", 41 | "eslint-plugin-vue": "^6.2.2", 42 | "flush-promises": "^1.0.2", 43 | "node-sass": "^4.14.1", 44 | "prettier": "^1.19.1", 45 | "purgecss": "^1.4.2", 46 | "sass-loader": "^7.3.1", 47 | "vue-template-compiler": "^2.6.12" 48 | }, 49 | "keywords": [ 50 | "JavaScript", 51 | "Vue.js", 52 | "Vuex", 53 | "vue-router", 54 | "axios", 55 | "vee-validate", 56 | "Bootstrap CSS 4" 57 | ], 58 | "repository": { 59 | "type": "git", 60 | "url": "https://github.com/techlab23/task-management-app.git" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | const purgecss = require("@fullhuman/postcss-purgecss") 2 | 3 | module.exports = { 4 | plugins: [ 5 | //Only add purgecss in production 6 | 7 | process.env.NODE_ENV === "production" 8 | ? purgecss({ 9 | content: ["./src/**/*.vue"] 10 | }) 11 | : "" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 120, 3 | tabWidth: 2, 4 | semi: false 5 | } 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techlab23/task-management-app/ef643da476490e89794a4477784421dea825deb5/public/favicon.ico -------------------------------------------------------------------------------- /public/img/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techlab23/task-management-app/ef643da476490e89794a4477784421dea825deb5/public/img/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/img/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techlab23/task-management-app/ef643da476490e89794a4477784421dea825deb5/public/img/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/img/icons/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techlab23/task-management-app/ef643da476490e89794a4477784421dea825deb5/public/img/icons/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /public/img/icons/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techlab23/task-management-app/ef643da476490e89794a4477784421dea825deb5/public/img/icons/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /public/img/icons/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techlab23/task-management-app/ef643da476490e89794a4477784421dea825deb5/public/img/icons/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /public/img/icons/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techlab23/task-management-app/ef643da476490e89794a4477784421dea825deb5/public/img/icons/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /public/img/icons/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techlab23/task-management-app/ef643da476490e89794a4477784421dea825deb5/public/img/icons/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /public/img/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techlab23/task-management-app/ef643da476490e89794a4477784421dea825deb5/public/img/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /public/img/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techlab23/task-management-app/ef643da476490e89794a4477784421dea825deb5/public/img/icons/favicon-16x16.png -------------------------------------------------------------------------------- /public/img/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techlab23/task-management-app/ef643da476490e89794a4477784421dea825deb5/public/img/icons/favicon-32x32.png -------------------------------------------------------------------------------- /public/img/icons/msapplication-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techlab23/task-management-app/ef643da476490e89794a4477784421dea825deb5/public/img/icons/msapplication-icon-144x144.png -------------------------------------------------------------------------------- /public/img/icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techlab23/task-management-app/ef643da476490e89794a4477784421dea825deb5/public/img/icons/mstile-150x150.png -------------------------------------------------------------------------------- /public/img/icons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Task Management App 9 | 10 | 11 | 12 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "task-management-app", 3 | "short_name": "task-app", 4 | "icons": [ 5 | { 6 | "src": "./img/icons/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "./img/icons/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "start_url": "./index.html", 17 | "display": "standalone", 18 | "background_color": "#000000", 19 | "theme_color": "#4DBA87" 20 | } 21 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techlab23/task-management-app/ef643da476490e89794a4477784421dea825deb5/src/.DS_Store -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 10 | 30 | 43 | -------------------------------------------------------------------------------- /src/assets/app.scss: -------------------------------------------------------------------------------- 1 | @import "../../node_modules/bootstrap/scss/bootstrap"; 2 | @import url("https://fonts.googleapis.com/css?family=Gugi"); 3 | 4 | .scrolling-wrapper { 5 | overflow-x: scroll; 6 | overflow-y: hidden; 7 | -webkit-overflow-scrolling: touch; 8 | } 9 | .scrolling-wrapper::-webkit-scrollbar { 10 | display: none; 11 | } 12 | 13 | .btn-app { 14 | background-color: rgba(96, 125, 139, 0.8) !important; 15 | border-color: rgba(96, 125, 139, 0.8) !important; 16 | color: #fff !important; 17 | text-decoration: none !important; 18 | } 19 | 20 | .btn-app:hover { 21 | background-color: rgba(96, 125, 139, 1) !important; 22 | border-color: rgba(96, 125, 139, 1) !important; 23 | } 24 | 25 | .indicator { 26 | position: fixed; 27 | top: 50%; 28 | left: 50%; 29 | transform: translate(-50%, -50%); 30 | max-width: 200px; 31 | } 32 | 33 | .navbar-brand { 34 | font-family: "Gugi", cursive; 35 | font-size: 50px; 36 | font-weight: 600; 37 | color: #2196f3 !important; 38 | padding-top: 0px; 39 | padding-bottom: 0px; 40 | line-height: 1em; 41 | } 42 | 43 | .navbar-brand > label { 44 | font-size: 20px; 45 | line-height: 1em; 46 | vertical-align: middle; 47 | color: #a2b3bb; 48 | } 49 | 50 | .navbar-brand label span { 51 | color: #39515d; 52 | } 53 | 54 | .card-body { 55 | padding: 1rem !important; 56 | } 57 | 58 | .board-item { 59 | background-color: rgba(96, 125, 139, 0.2) !important; 60 | font-size: 13px; 61 | } 62 | 63 | .tasklist-item { 64 | min-height: 50px; 65 | border-bottom: 0.01rem solid rgba(0, 0, 0, 0.9); 66 | font-size: 13px; 67 | background-color: rgba(255, 255, 255, 0.85); 68 | cursor: grab; 69 | } 70 | 71 | .tasklist-item:hover div.edit-icon { 72 | display: block; 73 | } 74 | 75 | .disable-select { 76 | -webkit-user-select: none; 77 | -moz-user-select: none; 78 | -ms-user-select: none; 79 | user-select: none; 80 | } 81 | 82 | /* Task List Item */ 83 | 84 | .heading-text { 85 | font-size: 1.4rem !important; 86 | } 87 | 88 | .heading { 89 | padding: 15px 0 5px 0; 90 | color: rgba(255, 255, 255, 0.8); 91 | font-family: "Gugi", cursive; 92 | border-top-left-radius: 10px; 93 | border-top-right-radius: 10px; 94 | text-transform: uppercase; 95 | cursor: grab; 96 | } 97 | 98 | .cards-list { 99 | min-height: 300px; 100 | height: 100vh; 101 | overflow: scroll; 102 | box-shadow: 1px 1px 1px 0px rgba(158, 158, 158, 0.25); 103 | background-color: rgba(223, 238, 242, 0.4); 104 | } 105 | 106 | .fixed-card { 107 | cursor: pointer; 108 | color: #ccc; 109 | border: 1px dotted #ccc; 110 | border-bottom-left-radius: 10px; 111 | border-bottom-right-radius: 10px; 112 | } 113 | 114 | .list-column { 115 | padding: 0 !important; 116 | margin: 0 15px; 117 | } 118 | 119 | .list-width { 120 | min-width: 300px; 121 | max-width: 300px; 122 | border-radius: 10px; 123 | } 124 | 125 | .ghost { 126 | opacity: 0.5; 127 | background-color: rgba(223, 238, 242, 0.4); 128 | } 129 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techlab23/task-management-app/ef643da476490e89794a4477784421dea825deb5/src/assets/logo.png -------------------------------------------------------------------------------- /src/components/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techlab23/task-management-app/ef643da476490e89794a4477784421dea825deb5/src/components/.DS_Store -------------------------------------------------------------------------------- /src/components/AppHeader.vue: -------------------------------------------------------------------------------- 1 | 17 | 41 | -------------------------------------------------------------------------------- /src/components/AppLoadingIndicator.vue: -------------------------------------------------------------------------------- 1 | 98 | 108 | -------------------------------------------------------------------------------- /src/components/Boards/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techlab23/task-management-app/ef643da476490e89794a4477784421dea825deb5/src/components/Boards/.DS_Store -------------------------------------------------------------------------------- /src/components/Boards/TaskBoardEdit.vue: -------------------------------------------------------------------------------- 1 | 36 | 97 | -------------------------------------------------------------------------------- /src/components/Details/DetailsDropdown.vue: -------------------------------------------------------------------------------- 1 | 14 | 30 | 76 | -------------------------------------------------------------------------------- /src/components/Details/DetailsInline.vue: -------------------------------------------------------------------------------- 1 | 11 | 27 | 66 | -------------------------------------------------------------------------------- /src/components/Details/DetailsPopup.vue: -------------------------------------------------------------------------------- 1 | 11 | 27 | 80 | -------------------------------------------------------------------------------- /src/components/Items/TaskListItem.vue: -------------------------------------------------------------------------------- 1 | 45 | 116 | -------------------------------------------------------------------------------- /src/components/Lists/TaskList.vue: -------------------------------------------------------------------------------- 1 | 32 | 97 | -------------------------------------------------------------------------------- /src/components/Lists/TaskListActions.vue: -------------------------------------------------------------------------------- 1 | 12 | 35 | -------------------------------------------------------------------------------- /src/components/Lists/TaskListArchive.vue: -------------------------------------------------------------------------------- 1 | 11 | 47 | -------------------------------------------------------------------------------- /src/components/Lists/TaskListEdit.vue: -------------------------------------------------------------------------------- 1 | 26 | 87 | -------------------------------------------------------------------------------- /src/components/Lists/TaskListRestore.vue: -------------------------------------------------------------------------------- 1 | 29 | 61 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue" 2 | import App from "./App.vue" 3 | import "@/assets/app.scss" 4 | import "./plugins" 5 | import store from "./store" 6 | import router from "./router" 7 | 8 | Vue.config.productionTip = false 9 | 10 | new Vue({ 11 | store, 12 | router, 13 | render: h => h(App) 14 | }).$mount("#app") 15 | -------------------------------------------------------------------------------- /src/plugins/device-detect.js: -------------------------------------------------------------------------------- 1 | // Ref: https://github.com/dotneet/nuxt-device-detect 2 | 3 | // these regular expressions are borrowed from below page. 4 | // https://stackoverflow.com/questions/11381673/detecting-a-mobile-browser 5 | 6 | // eslint-disable-next-line 7 | const REGEX_MOBILE1 = /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i 8 | 9 | // eslint-disable-next-line 10 | const REGEX_MOBILE2 = /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i 11 | 12 | function isMobile(a) { 13 | return REGEX_MOBILE1.test(a) || REGEX_MOBILE2.test(a.substr(0, 4)) 14 | } 15 | 16 | // eslint-disable-next-line 17 | const REGEX_MOBILE_OR_TABLET1 = /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i 18 | // eslint-disable-next-line 19 | const REGEX_MOBILE_OR_TABLET2 = /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i 20 | 21 | function isMobileOrTablet(a) { 22 | return REGEX_MOBILE_OR_TABLET1.test(a) || REGEX_MOBILE_OR_TABLET2.test(a.substr(0, 4)) 23 | } 24 | 25 | function isIos(a) { 26 | return /iPad|iPhone|iPod/.test(a) 27 | } 28 | 29 | const DEFAULT_USER_AGENT = 30 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.39 Safari/537.36" 31 | 32 | let userAgent = "" 33 | 34 | userAgent = typeof navigator !== "undefined" ? navigator.userAgent : DEFAULT_USER_AGENT 35 | 36 | let mobile = null 37 | let mobileOrTablet = null 38 | let ios = null 39 | mobile = isMobile(userAgent) 40 | mobileOrTablet = isMobileOrTablet(userAgent) 41 | ios = isIos(userAgent) 42 | 43 | /* eslint-disable */ 44 | export default { 45 | install(Vue, options) { 46 | Vue.mixin({ 47 | computed: { 48 | isMobile() { 49 | return mobile 50 | }, 51 | isMobileOrTablet() { 52 | return mobileOrTablet 53 | }, 54 | isTablet() { 55 | return !mobile && mobileOrTablet 56 | }, 57 | isDesktop() { 58 | return !mobileOrTablet 59 | }, 60 | isIos() { 61 | return ios 62 | } 63 | } 64 | }) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/plugins/index.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue" 2 | import "./vee-validate" 3 | import DeviceDetect from "./device-detect" 4 | Vue.use(DeviceDetect) 5 | -------------------------------------------------------------------------------- /src/plugins/vee-validate.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue" 2 | import { Validator, install as VeeValidate } from "vee-validate/dist/vee-validate.minimal.esm.js" 3 | import { required } from "vee-validate/dist/rules.esm.js" 4 | import veeEn from "vee-validate/dist/locale/en" 5 | 6 | // // Add the rules you need. 7 | Validator.extend("required", required) 8 | 9 | // // Merge the messages. 10 | Validator.localize("en", veeEn) 11 | 12 | // install the plugin 13 | Vue.use(VeeValidate) 14 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue" 2 | import Router from "vue-router" 3 | import routes from "./routes" 4 | 5 | Vue.use(Router) 6 | 7 | const router = new Router({ routes }) 8 | 9 | export default router 10 | -------------------------------------------------------------------------------- /src/router/routes.js: -------------------------------------------------------------------------------- 1 | import Dashboard from "@/views/Dashboard.vue" 2 | import TaskBoard from "@/views/TaskBoard.vue" 3 | 4 | export default [ 5 | { 6 | path: "/", 7 | name: "home", 8 | redirect: "/dashboard" 9 | }, 10 | { 11 | path: "/dashboard", 12 | name: "dashboard", 13 | component: Dashboard 14 | }, 15 | { 16 | path: "/boards/:id", 17 | name: "task-board", 18 | component: TaskBoard 19 | } 20 | ] 21 | -------------------------------------------------------------------------------- /src/store/actions.js: -------------------------------------------------------------------------------- 1 | const INITIAL_DATA_URL = "https://raw.githubusercontent.com/techlab23/data-repository/master/boards.json" 2 | import axios from "axios" 3 | 4 | export default { 5 | async fetchData({ commit }) { 6 | commit("SET_LOADING_STATE", true) 7 | return axios.get(INITIAL_DATA_URL).then(res => { 8 | commit("SET_INITIAL_DATA", res.data) 9 | commit("SET_LOADING_STATE", false) 10 | }) 11 | }, 12 | async saveTaskBoard({ commit }, payload) { 13 | commit("SAVE_TASKBOARD", payload) 14 | }, 15 | async archiveTaskBoard({ commit }, payload) { 16 | commit("ARCHIVE_TASKBOARD", payload) 17 | }, 18 | async restoreTaskBoard({ commit }, payload) { 19 | commit("RESTORE_TASKBOARD", payload) 20 | }, 21 | async setActiveTaskBoard({ commit }, payload) { 22 | commit("SET_ACTIVE_TASKBOARD", payload) 23 | }, 24 | 25 | async saveTaskList({ commit }, payload) { 26 | commit("SAVE_TASKLIST", payload) 27 | }, 28 | async archiveTaskList({ commit }, payload) { 29 | commit("ARCHIVE_TASKLIST", payload) 30 | }, 31 | async restoreTaskList({ commit }, payload) { 32 | commit("RESTORE_TASKLIST", payload) 33 | }, 34 | 35 | async reorderTaskLists({ commit }, payload) { 36 | commit("REORDER_TASKLISTS", payload) 37 | }, 38 | async reorderTaskListItems({ commit }, payload) { 39 | commit("REORDER_TASKLIST_ITEMS", payload) 40 | }, 41 | 42 | async saveTaskListItem({ commit }, payload) { 43 | commit("SAVE_TASKLIST_ITEM", payload) 44 | }, 45 | async deleteTaskListItem({ commit }, payload) { 46 | commit("DELETE_TASKLIST_ITEM", payload) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/store/getters.js: -------------------------------------------------------------------------------- 1 | export default { 2 | isLoading: state => state.isLoading, 3 | allBoards: state => state.boards, 4 | activeBoard: state => state.activeBoard, 5 | unarchivedBoards: state => state.boards.filter(b => !b.archived), 6 | archivedBoards: state => state.boards.filter(b => b.archived), 7 | archivedLists: state => (state.activeBoard ? state.activeBoard.lists.filter(l => l.archived) : []), 8 | unarchivedLists: state => (state.activeBoard ? state.activeBoard.lists.filter(l => !l.archived) : []) 9 | } 10 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue" 2 | import Vuex from "vuex" 3 | import state from "./state" 4 | import getters from "./getters" 5 | import mutations from "./mutations" 6 | import actions from "./actions" 7 | 8 | Vue.use(Vuex) 9 | 10 | export default new Vuex.Store({ 11 | state, 12 | getters, 13 | mutations, 14 | actions 15 | }) 16 | -------------------------------------------------------------------------------- /src/store/mutations.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue" 2 | 3 | // Lib to create guid 4 | const s4 = () => 5 | Math.floor((1 + Math.random()) * 0x10000) 6 | .toString(16) 7 | .substring(1) 8 | const guid = () => s4() + s4() + "-" + s4() + "-" + s4() + "-" + s4() + "-" + s4() + s4() + s4() 9 | 10 | export default { 11 | // Set Initial Data 12 | SET_INITIAL_DATA(state, payload) { 13 | state.boards = payload 14 | }, 15 | 16 | // Set Loading State 17 | SET_LOADING_STATE(state, payload) { 18 | state.isLoading = payload 19 | }, 20 | 21 | // Save Task Board 22 | SAVE_TASKBOARD(state, payload) { 23 | const board = state.boards.find(b => b.id == payload.id) 24 | const itemIdx = state.boards.findIndex(b => b.id == payload.id) 25 | 26 | // For existing item 27 | if (itemIdx > -1) { 28 | board.name = payload.name 29 | board.description = payload.description 30 | Vue.set(state.boards, itemIdx, board) 31 | } 32 | // For new item 33 | else { 34 | const board = { 35 | id: guid(), 36 | name: payload.name, 37 | description: payload.description, 38 | archived: false, 39 | lists: [] 40 | } 41 | state.boards.push(board) 42 | } 43 | }, 44 | 45 | // Archive Task Board 46 | ARCHIVE_TASKBOARD(state, payload) { 47 | const board = state.boards.find(b => b.id == payload.boardId) 48 | const boardIdx = state.boards.findIndex(b => b.id == payload.boardId) 49 | board.archived = true 50 | Vue.set(state.boards, boardIdx, board) 51 | }, 52 | 53 | // Restore Task Board 54 | RESTORE_TASKBOARD(state, payload) { 55 | const board = state.boards.find(b => b.id == payload.boardId) 56 | const boardIdx = state.boards.findIndex(b => b.id == payload.boardId) 57 | board.archived = false 58 | Vue.set(state.boards, boardIdx, board) 59 | }, 60 | 61 | // Save Task List 62 | SAVE_TASKLIST(state, payload) { 63 | const board = state.boards.find(b => b.id == payload.boardId) 64 | const list = board.lists.find(l => l.id == payload.listId) 65 | const listIdx = board.lists.findIndex(l => l.id == payload.listId) 66 | 67 | // For existing item 68 | if (listIdx > -1) { 69 | list.name = payload.name 70 | Vue.set(board.lists, listIdx, list) 71 | } 72 | // // For new item 73 | else { 74 | const list = { 75 | id: guid(), 76 | name: payload.name, 77 | headerColor: "#607d8b", 78 | archived: false, 79 | items: [] 80 | } 81 | board.lists.push(list) 82 | } 83 | }, 84 | 85 | // Archive Task List 86 | ARCHIVE_TASKLIST(state, payload) { 87 | const board = state.boards.find(b => b.id == payload.boardId) 88 | const list = board.lists.find(l => l.id == payload.listId) 89 | const listIdx = board.lists.findIndex(l => l.id == payload.listId) 90 | list.archived = true 91 | Vue.set(board.lists, listIdx, list) 92 | }, 93 | 94 | // Restore Task List 95 | RESTORE_TASKLIST(state, payload) { 96 | const board = state.boards.find(b => b.id == payload.boardId) 97 | const list = board.lists.find(l => l.id == payload.listId) 98 | const listIdx = board.lists.findIndex(l => l.id == payload.listId) 99 | list.archived = false 100 | Vue.set(board.lists, listIdx, list) 101 | }, 102 | 103 | // Reorder TaskBoad Lists 104 | REORDER_TASKLISTS(state, payload) { 105 | const board = state.boards.find(b => b.id == payload.boardId) 106 | Vue.set(board, "lists", payload.lists) 107 | }, 108 | 109 | // Reorder Task List Items 110 | REORDER_TASKLIST_ITEMS(state, payload) { 111 | const board = state.boards.find(b => b.id == payload.boardId) 112 | const listIdx = board.lists.findIndex(l => l.id == payload.listId) 113 | Vue.set(board.lists[listIdx], "items", payload.items) 114 | }, 115 | 116 | // Set Active Board 117 | SET_ACTIVE_TASKBOARD(state, payload) { 118 | state.activeBoard = payload.board 119 | }, 120 | 121 | // Save Task List Item 122 | SAVE_TASKLIST_ITEM(state, payload) { 123 | const board = state.boards.find(b => b.id == payload.boardId) 124 | const list = board.lists.find(l => l.id == payload.listId) 125 | const itemIdx = list.items.findIndex(item => item.id == payload.item.id) 126 | 127 | // For existing item 128 | if (itemIdx > -1) { 129 | Vue.set(list.items, itemIdx, payload.item) 130 | } 131 | // For new item 132 | else { 133 | payload.item.id = guid() 134 | list.items.push(payload.item) 135 | } 136 | }, 137 | 138 | // Delete Task List Item 139 | DELETE_TASKLIST_ITEM(state, payload) { 140 | const board = state.boards.find(b => b.id == payload.boardId) 141 | const list = board.lists.find(l => l.id == payload.listId) 142 | const itemIdx = list.items.findIndex(item => item.id == payload.item.id) 143 | // For existing item 144 | if (itemIdx > -1) { 145 | Vue.delete(list.items, itemIdx) 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/store/state.js: -------------------------------------------------------------------------------- 1 | export default { 2 | isLoading: true, 3 | activeBoard: null, 4 | boards: [] 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/bus.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue" 2 | export const Bus = new Vue() 3 | -------------------------------------------------------------------------------- /src/views/Dashboard.vue: -------------------------------------------------------------------------------- 1 | 51 | 84 | -------------------------------------------------------------------------------- /src/views/TaskBoard.vue: -------------------------------------------------------------------------------- 1 | 8 | 68 | -------------------------------------------------------------------------------- /tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jest: true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /tests/unit/App.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount, mount, createLocalVue } from "@vue/test-utils" 2 | import App from "@/App.vue" 3 | import Vuex from "vuex" 4 | import VueRouter from "vue-router" 5 | import Dashboard from "@/views/Dashboard.vue" 6 | import TaskBoard from "@/views/TaskBoard.vue" 7 | import routes from "@/router/routes" 8 | const localVue = createLocalVue() 9 | localVue.use(VueRouter) 10 | localVue.use(Vuex) 11 | 12 | describe("App", () => { 13 | let $store 14 | let wrapper 15 | const allBoards = [ 16 | { id: 111, name: "kanban", description: "kanban board", archived: true, lists: [] }, 17 | { 18 | id: 222, 19 | name: "tracker", 20 | description: "tracker board", 21 | archived: false, 22 | lists: [ 23 | { id: "222-1", name: "list 1", archived: false, items: [] }, 24 | { id: "222-2", name: "list 1", archived: false, items: [] } 25 | ] 26 | } 27 | ] 28 | 29 | beforeEach(() => { 30 | $store = { 31 | dispatch: jest.fn(), 32 | getters: { 33 | archivedBoards: [allBoards[0]], 34 | unarchivedBoards: [allBoards[0]], 35 | isLoading: false, 36 | allBoards: allBoards 37 | } 38 | } 39 | }) 40 | 41 | it("calls fetchData to load initial data", async () => { 42 | const router = new VueRouter({ routes }) 43 | const wrapper = shallowMount(App, { 44 | localVue, 45 | router, 46 | mocks: { $store }, 47 | stubs: { AppHeader: true, AppLoadingIndicator: true, Dashboard: true } 48 | }) 49 | expect($store.dispatch).toHaveBeenCalledWith("fetchData") 50 | }) 51 | 52 | it("renders dashboard component", () => { 53 | const router = new VueRouter({ routes }) 54 | const wrapper = mount(App, { 55 | localVue, 56 | router, 57 | mocks: { $store }, 58 | stubs: { AppHeader: true, AppLoadingIndicator: true } 59 | }) 60 | 61 | router.push("/dashboard") 62 | expect(wrapper.find(Dashboard).exists()).toBe(true) 63 | }) 64 | 65 | it("renders board component", () => { 66 | const router = new VueRouter({ routes }) 67 | const wrapper = mount(App, { 68 | localVue, 69 | router, 70 | mocks: { $store }, 71 | stubs: { AppHeader: true, AppLoadingIndicator: true } 72 | }) 73 | 74 | router.push("/boards/222") 75 | expect(wrapper.find(TaskBoard).exists()).toBe(true) 76 | }) 77 | }) 78 | -------------------------------------------------------------------------------- /tests/unit/components/AppHeader.spec.js: -------------------------------------------------------------------------------- 1 | import AppHeader from "@/components/AppHeader.vue" 2 | import { shallowMount, createLocalVue, RouterLinkStub } from "@vue/test-utils" 3 | import Vuex from "vuex" 4 | const localVue = createLocalVue() 5 | localVue.use(Vuex) 6 | 7 | describe("AppHeader.vue", () => { 8 | let $store 9 | let wrapper 10 | 11 | it("Displays board name if activeBoard is set", () => { 12 | $store = { 13 | dispatch: jest.fn(), 14 | state: {}, 15 | getters: { 16 | activeBoard: { 17 | id: 222, 18 | name: "tracker", 19 | description: "tracker board", 20 | archived: false, 21 | lists: [] 22 | }, 23 | isLoading: jest.fn() 24 | } 25 | } 26 | wrapper = shallowMount(AppHeader, { 27 | mocks: { $store }, 28 | stubs: { RouterLink: RouterLinkStub } 29 | }) 30 | expect(wrapper.text()).toContain("tracker") 31 | }) 32 | 33 | it("display child component stubs when isLoading is false", () => { 34 | $store = { 35 | dispatch: jest.fn(), 36 | state: {}, 37 | getters: { 38 | isLoading: false, 39 | activeBoard: { 40 | id: 222, 41 | name: "tracker", 42 | description: "tracker board", 43 | archived: false, 44 | lists: [{ id: "222-1", name: "todo" }, { id: "222-2", name: "doing" }] 45 | } 46 | } 47 | } 48 | 49 | wrapper = shallowMount(AppHeader, { 50 | mocks: { $store }, 51 | stubs: { 52 | RouterLink: RouterLinkStub 53 | } 54 | }) 55 | 56 | expect(wrapper.contains("tasklistrestore-stub")).toBe(true) 57 | expect(wrapper.contains("taskboardedit-stub")).toBe(true) 58 | expect(wrapper.contains("tasklistarchive-stub")).toBe(true) 59 | expect(wrapper.contains("tasklistedit-stub")).toBe(true) 60 | }) 61 | 62 | it("does not render child component stubs when isLoading is true", () => { 63 | $store = { 64 | dispatch: jest.fn(), 65 | state: {}, 66 | getters: { 67 | isLoading: true, 68 | activeBoard: { 69 | id: 222, 70 | name: "tracker", 71 | description: "tracker board", 72 | archived: false, 73 | lists: [{ id: "222-1", name: "todo" }, { id: "222-2", name: "doing" }] 74 | } 75 | } 76 | } 77 | 78 | wrapper = shallowMount(AppHeader, { 79 | mocks: { $store }, 80 | stubs: { 81 | RouterLink: RouterLinkStub 82 | } 83 | }) 84 | 85 | expect(wrapper.contains("tasklistrestore-stub")).toBe(false) 86 | expect(wrapper.contains("taskboardedit-stub")).toBe(false) 87 | expect(wrapper.contains("tasklistarchive-stub")).toBe(false) 88 | expect(wrapper.contains("tasklistedit-stub")).toBe(false) 89 | }) 90 | }) 91 | -------------------------------------------------------------------------------- /tests/unit/components/AppLoadingIndicator.spec.js: -------------------------------------------------------------------------------- 1 | import AppLoadingIndicator from "@/components/AppLoadingIndicator.vue" 2 | import { shallowMount, createLocalVue, RouterLinkStub } from "@vue/test-utils" 3 | import Vuex from "vuex" 4 | const localVue = createLocalVue() 5 | localVue.use(Vuex) 6 | 7 | describe("AppLoadingIndicator.vue", () => { 8 | let $store 9 | let wrapper 10 | 11 | it("Displays loading animation if isLoading set to true", () => { 12 | $store = { 13 | getters: { 14 | isLoading: true 15 | } 16 | } 17 | wrapper = shallowMount(AppLoadingIndicator, { 18 | mocks: { $store } 19 | }) 20 | expect(wrapper.text()).toContain("Loading...") 21 | }) 22 | 23 | it("hides loading animation if isLoading set to false", () => { 24 | $store = { 25 | getters: { 26 | isLoading: false 27 | } 28 | } 29 | wrapper = shallowMount(AppLoadingIndicator, { 30 | mocks: { $store } 31 | }) 32 | expect(wrapper.text()).not.toContain("Loading...") 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /tests/unit/components/Boards/TaskBoardEdit.spec.js: -------------------------------------------------------------------------------- 1 | import TaskBoardEdit from "@/components/Boards/TaskBoardEdit.vue" 2 | import { mount, createLocalVue, RouterLinkStub } from "@vue/test-utils" 3 | import flushPromises from "flush-promises" 4 | import Vuex from "vuex" 5 | import VeeValidate from "vee-validate" 6 | const localVue = createLocalVue() 7 | localVue.use(Vuex) 8 | localVue.use(VeeValidate) 9 | 10 | import { Bus } from "@/utils/bus" 11 | Bus.$on = jest.fn() 12 | 13 | describe("TaskBoardEdit.vue", () => { 14 | let $store 15 | let wrapper 16 | 17 | beforeEach(() => { 18 | $store = { 19 | dispatch: jest.fn(), 20 | getters: { 21 | activeBoard: { id: "222" } 22 | } 23 | } 24 | wrapper = mount(TaskBoardEdit, { 25 | sync: false, 26 | localVue, 27 | mocks: { $store }, 28 | stubs: { RouterLink: RouterLinkStub } 29 | }) 30 | }) 31 | 32 | it("clear boardForm data on handlePopupToggled", () => { 33 | wrapper.vm.handlePopupToggled(false) 34 | expect(wrapper.vm.boardForm.id).toBe(0) 35 | expect(wrapper.vm.boardForm.name).toBe("") 36 | expect(wrapper.vm.boardForm.description).toBe("") 37 | }) 38 | 39 | it("does not clear boardForm data on handlePopupToggled", () => { 40 | wrapper.setData({ 41 | boardForm: { 42 | id: "222", 43 | name: "tracker", 44 | description: "tracker board" 45 | } 46 | }) 47 | wrapper.vm.handlePopupToggled(true) 48 | expect(wrapper.vm.boardForm.id).toBe("222") 49 | expect(wrapper.vm.boardForm.name).toBe("tracker") 50 | expect(wrapper.vm.boardForm.description).toBe("tracker board") 51 | }) 52 | 53 | it("listens for 'taskboard-editing' on event bus", () => { 54 | Bus.$emit("taskboard-editing", { 55 | id: "222", 56 | name: "tracker", 57 | description: "tracker board" 58 | }) 59 | 60 | expect(Bus.$on).toHaveBeenCalledWith("taskboard-editing", wrapper.vm.handleTaskBoardEditing) 61 | }) 62 | 63 | it("Heading property returns 'Create new board' if creating a new board", () => { 64 | expect(wrapper.vm.heading).toBe("Create new board") 65 | }) 66 | 67 | it("Heading property returns 'Update board information' if editing existing board", () => { 68 | wrapper.setData({ 69 | boardForm: { 70 | id: 222 71 | } 72 | }) 73 | 74 | expect(wrapper.vm.heading).toBe("Update board information") 75 | }) 76 | 77 | it("saveTaskBoard action is called on handleSaveBoard", async () => { 78 | const boardForm = { 79 | id: "222", 80 | name: "tracker", 81 | description: "tracker board" 82 | } 83 | 84 | wrapper.vm.$validator.validateAll = jest.fn(() => Promise.resolve(true)) 85 | wrapper.vm.handleTaskBoardEditing(boardForm) 86 | wrapper.vm.handleSaveBoard() 87 | await flushPromises() 88 | 89 | expect(wrapper.vm.$validator.validateAll).toHaveBeenCalled() 90 | 91 | expect($store.dispatch).toHaveBeenCalledWith("saveTaskBoard", { 92 | id: boardForm.id, 93 | name: boardForm.name, 94 | description: boardForm.description 95 | }) 96 | }) 97 | }) 98 | -------------------------------------------------------------------------------- /tests/unit/components/Details/DetailsDropdown.spec.js: -------------------------------------------------------------------------------- 1 | import DetailsDropdown from "@/components/Details/DetailsDropdown" 2 | import { shallowMount, createLocalVue, RouterLinkStub } from "@vue/test-utils" 3 | 4 | describe("DetailsDropdown.vue", () => { 5 | let wrapper 6 | 7 | beforeEach(() => { 8 | wrapper = shallowMount(DetailsDropdown, { 9 | mocks: {}, 10 | stubs: {} 11 | }) 12 | }) 13 | 14 | it("Open dropdown", () => { 15 | wrapper.vm.open() 16 | expect( 17 | wrapper 18 | .find("details") 19 | .html() 20 | .includes("open") 21 | ).toBe(true) 22 | }) 23 | 24 | it("Close dropdown", () => { 25 | wrapper.vm.close() 26 | 27 | expect( 28 | wrapper 29 | .find("details") 30 | .html() 31 | .includes("open") 32 | ).toBe(false) 33 | }) 34 | 35 | it("toggle check - open", () => { 36 | wrapper.vm.open() 37 | wrapper.find("details").trigger("toggle") 38 | expect(wrapper.emitted("popup-toggled")).toBeTruthy() 39 | }) 40 | 41 | it("toggle check - close", () => { 42 | wrapper.vm.close() 43 | wrapper.find("details").trigger("toggle") 44 | expect(wrapper.emitted("popup-toggled")).toBeTruthy() 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /tests/unit/components/Details/DetailsInline.spec.js: -------------------------------------------------------------------------------- 1 | import DetailsInline from "@/components/Details/DetailsInline" 2 | import { shallowMount, createLocalVue, RouterLinkStub } from "@vue/test-utils" 3 | 4 | describe("DetailsInline.vue", () => { 5 | let wrapper 6 | 7 | beforeEach(() => { 8 | wrapper = shallowMount(DetailsInline, { 9 | mocks: {}, 10 | stubs: {} 11 | }) 12 | }) 13 | 14 | it("Open dropdown", () => { 15 | wrapper.vm.open() 16 | expect( 17 | wrapper 18 | .find("details") 19 | .html() 20 | .includes("open") 21 | ).toBe(true) 22 | }) 23 | 24 | it("Close dropdown", () => { 25 | wrapper.vm.close() 26 | 27 | expect( 28 | wrapper 29 | .find("details") 30 | .html() 31 | .includes("open") 32 | ).toBe(false) 33 | }) 34 | 35 | it("toggle check - open", () => { 36 | wrapper.vm.open() 37 | wrapper.find("details").trigger("toggle") 38 | expect(wrapper.emitted("popup-toggled")).toBeTruthy() 39 | }) 40 | 41 | it("toggle check - close", () => { 42 | wrapper.vm.close() 43 | wrapper.find("details").trigger("toggle") 44 | expect(wrapper.emitted("popup-toggled")).toBeTruthy() 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /tests/unit/components/Details/DetailsPopup.spec.js: -------------------------------------------------------------------------------- 1 | import DetailsPopup from "@/components/Details/DetailsPopup" 2 | import { shallowMount, createLocalVue, RouterLinkStub } from "@vue/test-utils" 3 | 4 | describe("DetailsPopup.vue", () => { 5 | let wrapper 6 | 7 | beforeEach(() => { 8 | wrapper = shallowMount(DetailsPopup, { 9 | mocks: {}, 10 | stubs: {} 11 | }) 12 | }) 13 | 14 | it("Open dropdown", () => { 15 | wrapper.vm.open() 16 | expect( 17 | wrapper 18 | .find("details") 19 | .html() 20 | .includes("open") 21 | ).toBe(true) 22 | }) 23 | 24 | it("Close dropdown", () => { 25 | wrapper.vm.close() 26 | 27 | expect( 28 | wrapper 29 | .find("details") 30 | .html() 31 | .includes("open") 32 | ).toBe(false) 33 | }) 34 | 35 | it("toggle check - open", () => { 36 | wrapper.vm.open() 37 | wrapper.find("details").trigger("toggle") 38 | expect(wrapper.emitted("popup-toggled")).toBeTruthy() 39 | }) 40 | 41 | it("toggle check - close", () => { 42 | wrapper.vm.close() 43 | wrapper.find("details").trigger("toggle") 44 | expect(wrapper.emitted("popup-toggled")).toBeTruthy() 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /tests/unit/components/Items/TaskListItem.spec.js: -------------------------------------------------------------------------------- 1 | import TaskListItem from "@/components/Items/TaskListItem.vue" 2 | import { shallowMount, createLocalVue, RouterLinkStub } from "@vue/test-utils" 3 | import flushPromises from "flush-promises" 4 | import Vuex from "vuex" 5 | import VeeValidate from "vee-validate" 6 | const localVue = createLocalVue() 7 | 8 | localVue.use(Vuex) 9 | localVue.use(VeeValidate) 10 | 11 | describe("TasListItem.vue", () => { 12 | let $store 13 | let wrapper 14 | let propsData 15 | 16 | beforeEach(() => { 17 | $store = { 18 | dispatch: jest.fn() 19 | } 20 | propsData = { 21 | item: { id: "222-1-1", text: "This is a list item" }, 22 | list: { id: "222-1" }, 23 | board: { id: "222" } 24 | } 25 | 26 | wrapper = shallowMount(TaskListItem, { 27 | localVue, 28 | propsData: propsData, 29 | mocks: { $store }, 30 | stubs: { RouterLink: RouterLinkStub } 31 | }) 32 | }) 33 | 34 | it("clears the form data", () => { 35 | wrapper.setData({ 36 | form: { 37 | id: "222-1-1", 38 | text: "This is a list item" 39 | } 40 | }) 41 | 42 | wrapper.vm.clearForm() 43 | 44 | expect(wrapper.vm.form.id).toBe("") 45 | expect(wrapper.vm.form.text).toBe("") 46 | }) 47 | 48 | it("saveTaskListItem action is called on save", async () => { 49 | wrapper.vm.$validator.validateAll = jest.fn(() => Promise.resolve(true)) 50 | wrapper.vm.startEditing() 51 | wrapper.vm.save() 52 | await flushPromises() 53 | expect(wrapper.vm.$validator.validateAll).toHaveBeenCalled() 54 | expect($store.dispatch).toHaveBeenCalledWith("saveTaskListItem", { 55 | boardId: propsData.board.id, 56 | listId: propsData.list.id, 57 | item: propsData.item 58 | }) 59 | expect(wrapper.emitted("item-edited")).toBeTruthy() 60 | }) 61 | 62 | it("does not call saveTaskListItem action, when form validation is failed", async () => { 63 | wrapper.vm.$validator.validateAll = jest.fn(() => Promise.resolve(false)) 64 | wrapper.vm.startEditing() 65 | wrapper.vm.save() 66 | await flushPromises() 67 | expect(wrapper.vm.$validator.validateAll).toHaveBeenCalled() 68 | expect($store.dispatch).not.toHaveBeenCalled() 69 | expect(wrapper.emitted("item-edited")).not.toBeTruthy() 70 | }) 71 | 72 | it("emits 'item-editing' event when moving into edit mode", () => { 73 | wrapper.vm.startEditing() 74 | // Test if "item-editing event is emitted or not" 75 | expect(wrapper.emitted("item-editing")).toBeTruthy() 76 | }) 77 | 78 | it("emits 'item-cancelled' event when edit mode is cancelled", () => { 79 | wrapper.vm.cancel() 80 | // Test if "item-cancelled event is emitted or not" 81 | expect(wrapper.emitted("item-cancelled")).toBeTruthy() 82 | }) 83 | 84 | it("Calls 'deleteTaskListItem' action and emit 'item-deleted' event when remove method is called", () => { 85 | wrapper.vm.remove() 86 | 87 | // Test if "deleteTaskListItem" action have been called 88 | // with appropriate arguments or not 89 | expect($store.dispatch).toHaveBeenCalledWith("deleteTaskListItem", { 90 | boardId: propsData.board.id, 91 | listId: propsData.list.id, 92 | item: propsData.item 93 | }) 94 | 95 | // Test if "item-deleted event is emitted or not" 96 | expect(wrapper.emitted("item-deleted")).toBeTruthy() 97 | }) 98 | }) 99 | 100 | describe("Test computed properties", () => { 101 | let wrapper 102 | let propsData 103 | 104 | beforeEach(() => { 105 | propsData = { 106 | item: { id: "", text: "" }, 107 | list: { id: "222-1" }, 108 | board: { id: "222" } 109 | } 110 | }) 111 | 112 | it("Displays '+ New Item'", () => { 113 | wrapper = shallowMount(TaskListItem, { 114 | localVue, 115 | propsData: propsData, 116 | stubs: { RouterLink: RouterLinkStub } 117 | }) 118 | 119 | expect(wrapper.vm.displayText).toBe("+ New Item") 120 | }) 121 | 122 | it("Displays 'This is a list item' when item is passed", () => { 123 | propsData.item = { id: "222-1-1", text: "This is a list item" } 124 | 125 | wrapper = shallowMount(TaskListItem, { 126 | localVue, 127 | propsData: propsData, 128 | stubs: { RouterLink: RouterLinkStub } 129 | }) 130 | expect(wrapper.vm.displayText).toBe("This is a list item") 131 | }) 132 | }) 133 | -------------------------------------------------------------------------------- /tests/unit/components/Lists/TaskList.spec.js: -------------------------------------------------------------------------------- 1 | import TaskList from "@/components/Lists/TaskList.vue" 2 | import { shallowMount, createLocalVue, RouterLinkStub } from "@vue/test-utils" 3 | import Vuex from "vuex" 4 | const localVue = createLocalVue() 5 | localVue.use(Vuex) 6 | 7 | describe("TaskList.vue", () => { 8 | let $store 9 | let wrapper 10 | let board 11 | 12 | beforeEach(() => { 13 | board = { 14 | id: 222, 15 | name: "tracker", 16 | description: "tracker board", 17 | archived: false, 18 | lists: [ 19 | { id: "222-1", name: "todo", archived: false, headerColor: "#ddd", items: [] }, 20 | { 21 | id: "222-2", 22 | name: "doing", 23 | archived: false, 24 | headerColor: "#ddd", 25 | items: [ 26 | { id: "222-2-1", text: "This is a list item #1" }, 27 | { id: "222-2-2", text: "This is a list item #2" }, 28 | { id: "222-2-3", text: "This is a list item #3" } 29 | ] 30 | } 31 | ] 32 | } 33 | 34 | $store = { 35 | dispatch: jest.fn() 36 | } 37 | 38 | wrapper = shallowMount(TaskList, { 39 | data() { 40 | return { 41 | isDesktop: true, 42 | isTablet: false 43 | } 44 | }, 45 | propsData: { board: board, list: board.lists[1] }, 46 | mocks: { $store }, 47 | stubs: { RouterLink: RouterLinkStub } 48 | }) 49 | }) 50 | 51 | it("dispatch reorderTaskListItems action when items computed property is updated", () => { 52 | const listItems = [ 53 | { id: "222-2-2", text: "This is a list item #2" }, 54 | { id: "222-2-1", text: "This is a list item #1" }, 55 | { id: "222-2-3", text: "This is a list item #3" } 56 | ] 57 | wrapper.vm.items = listItems 58 | expect($store.dispatch).toHaveBeenCalledWith("reorderTaskListItems", { 59 | boardId: board.id, 60 | listId: board.lists[1].id, 61 | items: listItems 62 | }) 63 | }) 64 | 65 | it("Should allow task items reorder on desktop or tablet", () => { 66 | wrapper.vm.isDesktop = true 67 | wrapper.vm.isTablet = false 68 | expect(wrapper.vm.shouldAllowTaskItemsReorder).toBe(true) 69 | }) 70 | 71 | it("Should not allow task items reorder if not on desktop or tablet", () => { 72 | wrapper.vm.isDesktop = false 73 | wrapper.vm.isTablet = false 74 | expect(wrapper.vm.shouldAllowTaskItemsReorder).toBe(false) 75 | }) 76 | 77 | it("Disable drag option while editing an list item", () => { 78 | wrapper.vm.itemEditing() 79 | wrapper.vm.isDesktop = true 80 | wrapper.vm.isTablet = true 81 | expect(wrapper.vm.dragOptions.disabled).toBe(true) 82 | }) 83 | 84 | it("Enable drag option when list item is edited", () => { 85 | wrapper.vm.itemEdited() 86 | wrapper.vm.isDesktop = true 87 | wrapper.vm.isTablet = true 88 | expect(wrapper.vm.dragOptions.disabled).toBe(false) 89 | }) 90 | 91 | it("Enable drag option when list item is edited", () => { 92 | wrapper.vm.itemCancelled() 93 | wrapper.vm.isDesktop = true 94 | wrapper.vm.isTablet = true 95 | expect(wrapper.vm.dragOptions.disabled).toBe(false) 96 | }) 97 | }) 98 | -------------------------------------------------------------------------------- /tests/unit/components/Lists/TaskListActions.spec.js: -------------------------------------------------------------------------------- 1 | import TaskListActions from "@/components/Lists/TaskListActions.vue" 2 | import { mount, createLocalVue } from "@vue/test-utils" 3 | import { Bus } from "@/utils/bus" 4 | 5 | Bus.$emit = jest.fn() 6 | 7 | describe("TaskListActions.vue", () => { 8 | let wrapper 9 | let propsData 10 | 11 | beforeEach(() => { 12 | propsData = { 13 | board: { id: "222" }, 14 | list: { id: "222-2" } 15 | } 16 | 17 | wrapper = mount(TaskListActions, { 18 | propsData: { board: propsData.board, list: propsData.list }, 19 | mocks: {}, 20 | stubs: {} 21 | }) 22 | }) 23 | 24 | it("tasklist-editing event is triggered", () => { 25 | wrapper.vm.showListEditPopup() 26 | expect(Bus.$emit).toHaveBeenCalledWith("tasklist-editing", wrapper.vm.list) 27 | }) 28 | 29 | it("tasklist-archiving event is triggered", () => { 30 | wrapper.vm.showArchiveListPopup() 31 | expect(Bus.$emit).toHaveBeenCalledWith("tasklist-archiving", { board: wrapper.vm.board, list: wrapper.vm.list }) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /tests/unit/components/Lists/TaskListArchive.spec.js: -------------------------------------------------------------------------------- 1 | import TaskListArchive from "@/components/Lists/TaskListArchive.vue" 2 | import { shallowMount, mount, createLocalVue, RouterLinkStub } from "@vue/test-utils" 3 | import Vuex from "vuex" 4 | const localVue = createLocalVue() 5 | localVue.use(Vuex) 6 | 7 | import { Bus } from "@/utils/bus" 8 | Bus.$on = jest.fn() 9 | 10 | describe("TaskListArchive.vue", () => { 11 | let $store 12 | let wrapper 13 | let data 14 | 15 | beforeEach(() => { 16 | $store = { 17 | dispatch: jest.fn() 18 | } 19 | 20 | data = { 21 | board: { id: "222" }, 22 | list: { id: "222-2" } 23 | } 24 | 25 | wrapper = mount(TaskListArchive, { 26 | localVue, 27 | mocks: { $store }, 28 | stubs: { RouterLink: RouterLinkStub } 29 | }) 30 | 31 | wrapper.setData(data) 32 | }) 33 | 34 | it("component is listening to 'tasklist-archiving' on event bus", () => { 35 | Bus.$emit("tasklist-archiving", data) 36 | expect(Bus.$on).toHaveBeenCalledWith("tasklist-archiving", wrapper.vm.handleTaskListArchiving) 37 | }) 38 | 39 | it("archiveTaskList action is called, when handleTaskListArchive method is executed", () => { 40 | wrapper.vm.handleTaskListArchive() 41 | 42 | expect($store.dispatch).toHaveBeenCalledWith("archiveTaskList", { 43 | boardId: data.board.id, 44 | listId: data.list.id 45 | }) 46 | }) 47 | 48 | it("use handleTaskListArchiving to set the data needed for popup", () => { 49 | wrapper.vm.handleTaskListArchiving(data) 50 | expect(wrapper.vm.board).toBe(data.board) 51 | expect(wrapper.vm.list).toBe(data.list) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /tests/unit/components/Lists/TaskListEdit.spec.js: -------------------------------------------------------------------------------- 1 | import TaskListEdit from "@/components/Lists/TaskListEdit.vue" 2 | import { mount, shallowMount, createLocalVue, RouterLinkStub } from "@vue/test-utils" 3 | import flushPromises from "flush-promises" 4 | import Vuex from "vuex" 5 | import VeeValidate from "vee-validate" 6 | const localVue = createLocalVue() 7 | 8 | localVue.use(Vuex) 9 | localVue.use(VeeValidate) 10 | 11 | describe("TaskListEdit.vue", () => { 12 | let $store 13 | let wrapper 14 | 15 | beforeEach(() => { 16 | $store = { 17 | dispatch: jest.fn(), 18 | getters: { 19 | activeBoard: { id: 222 } 20 | } 21 | } 22 | wrapper = mount(TaskListEdit, { 23 | sync: false, 24 | localVue, 25 | mocks: { $store }, 26 | stubs: { RouterLink: RouterLinkStub } 27 | }) 28 | }) 29 | 30 | it("Heading property returns 'Create new list' if creating a new list", () => { 31 | expect(wrapper.vm.heading).toBe("Create new list") 32 | }) 33 | 34 | it("Heading property returns 'Update list name' if editing existing list", () => { 35 | wrapper.setData({ 36 | listForm: { 37 | id: "222-2" 38 | } 39 | }) 40 | expect(wrapper.vm.heading).toBe("Update list name") 41 | }) 42 | 43 | it("saveTaskList action is called on handleTaskListSave", async () => { 44 | const listForm = { 45 | id: "222-2", 46 | name: "todo" 47 | } 48 | 49 | wrapper.vm.$validator.validateAll = jest.fn(() => Promise.resolve(true)) 50 | wrapper.vm.handleTaskListEditing(listForm) 51 | wrapper.vm.handleTaskListSave() 52 | await flushPromises() 53 | 54 | expect(wrapper.vm.$validator.validateAll).toHaveBeenCalled() 55 | 56 | expect($store.dispatch).toHaveBeenCalledWith("saveTaskList", { 57 | boardId: $store.getters.activeBoard.id, 58 | listId: listForm.id, 59 | name: listForm.name 60 | }) 61 | }) 62 | 63 | it("does not call saveTaskList action, when form validation fails", async () => { 64 | wrapper.setData({ 65 | listForm: { 66 | id: "222-2", 67 | name: "" 68 | } 69 | }) 70 | 71 | wrapper.vm.$validator.validateAll = jest.fn(() => Promise.resolve(false)) 72 | wrapper.vm.handleTaskListEditing({ id: wrapper.vm.listForm.id, name: wrapper.vm.listForm.name }) 73 | wrapper.vm.handleTaskListSave() 74 | await flushPromises() 75 | 76 | expect(wrapper.vm.$validator.validateAll).toHaveBeenCalled() 77 | expect($store.dispatch).not.toHaveBeenCalled() 78 | }) 79 | 80 | it("clears listForm data when popup is closed", () => { 81 | wrapper.setData({ 82 | listForm: { 83 | id: "222-2", 84 | name: "todo" 85 | } 86 | }) 87 | wrapper.vm.handlePopupToggled(false) 88 | expect(wrapper.vm.listForm.id).toBe(0) 89 | expect(wrapper.vm.listForm.name).toBe("") 90 | }) 91 | 92 | it("listForm data is not cleared when popup is opened", () => { 93 | wrapper.setData({ 94 | listForm: { 95 | id: "222-2", 96 | name: "todo" 97 | } 98 | }) 99 | wrapper.vm.handlePopupToggled(true) 100 | expect(wrapper.vm.listForm.id).toBe("222-2") 101 | expect(wrapper.vm.listForm.name).toBe("todo") 102 | }) 103 | }) 104 | 105 | describe("Test boardName computed Property", () => { 106 | let $store 107 | let wrapper 108 | 109 | it("boardName returns name of the active board", () => { 110 | $store = { 111 | getters: { 112 | activeBoard: { id: 222, name: "tracker", description: "tracker board", archived: false } 113 | } 114 | } 115 | 116 | wrapper = shallowMount(TaskListEdit, { 117 | mocks: { $store }, 118 | stubs: { RouterLink: RouterLinkStub } 119 | }) 120 | 121 | expect(wrapper.vm.boardName).toBe("tracker") 122 | }) 123 | 124 | it("boardName returns '' if active board is not found", () => { 125 | $store = { 126 | getters: { 127 | activeBoard: null 128 | } 129 | } 130 | 131 | wrapper = shallowMount(TaskListEdit, { 132 | mocks: { $store }, 133 | stubs: { RouterLink: RouterLinkStub } 134 | }) 135 | 136 | expect(wrapper.vm.boardName).toEqual("") 137 | }) 138 | }) 139 | -------------------------------------------------------------------------------- /tests/unit/components/Lists/TaskListRestore.spec.js: -------------------------------------------------------------------------------- 1 | import TaskListRestore from "@/components/Lists/TaskListRestore.vue" 2 | import DetailsPopup from "@/components/Details/DetailsPopup" 3 | import { shallowMount, mount, createLocalVue, RouterLinkStub } from "@vue/test-utils" 4 | import Vuex from "vuex" 5 | const localVue = createLocalVue() 6 | localVue.use(Vuex) 7 | 8 | describe("TaskListRestore.vue", () => { 9 | let $store 10 | let wrapper 11 | 12 | it("restoreTaskList action is called, when handleTaskListRestore method is executed", () => { 13 | const list = { id: "222-2", name: "doing", archived: true } 14 | 15 | $store = { 16 | dispatch: jest.fn(), 17 | getters: { 18 | activeBoard: { id: 222, name: "tracker", description: "tracker board", archived: false }, 19 | archivedLists: jest.fn() 20 | } 21 | } 22 | 23 | wrapper = shallowMount(TaskListRestore, { 24 | mocks: { $store }, 25 | stubs: { RouterLink: RouterLinkStub } 26 | }) 27 | 28 | wrapper.vm.handleTaskListRestore(list) 29 | expect($store.dispatch).toHaveBeenCalledWith("restoreTaskList", { 30 | boardId: $store.getters.activeBoard.id, 31 | listId: list.id 32 | }) 33 | }) 34 | 35 | it("Call to close() method is delegated to close method of DetailsPopup component", () => { 36 | wrapper = mount(TaskListRestore, { 37 | mocks: { $store }, 38 | stubs: { RouterLink: RouterLinkStub } 39 | }) 40 | 41 | // Find DetailsPopup component and open the popup 42 | let DetailsPopupComponentWrapper = wrapper.find(DetailsPopup) 43 | DetailsPopupComponentWrapper.vm.open() 44 | 45 | // Call close() method on TaskListRestore component 46 | wrapper.vm.close() 47 | 48 | // Check if open attribute is removed from details html tag 49 | // close method has delegated and called close() method on 50 | // DetailsPopup component 51 | expect( 52 | wrapper 53 | .find("details") 54 | .html() 55 | .includes("open") 56 | ).toBe(false) 57 | }) 58 | }) 59 | 60 | describe("Test boardName computed Property", () => { 61 | let $store 62 | let wrapper 63 | 64 | it("boardName returns name of the active board", () => { 65 | $store = { 66 | getters: { 67 | activeBoard: { id: 222, name: "tracker", description: "tracker board", archived: false } 68 | } 69 | } 70 | 71 | wrapper = shallowMount(TaskListRestore, { 72 | mocks: { $store }, 73 | stubs: { RouterLink: RouterLinkStub } 74 | }) 75 | 76 | expect(wrapper.vm.boardName).toBe("tracker") 77 | }) 78 | 79 | it("boardName returns '' if active board is not found", () => { 80 | $store = { 81 | getters: { 82 | activeBoard: null 83 | } 84 | } 85 | 86 | wrapper = shallowMount(TaskListRestore, { 87 | mocks: { $store }, 88 | stubs: { RouterLink: RouterLinkStub } 89 | }) 90 | 91 | expect(wrapper.vm.boardName).toEqual("") 92 | }) 93 | }) 94 | -------------------------------------------------------------------------------- /tests/unit/store/actions.spec.js: -------------------------------------------------------------------------------- 1 | import axios from "axios" 2 | import actions from "@/store/actions" 3 | jest.mock("axios") 4 | 5 | describe("Test actions", () => { 6 | const commit = jest.fn() 7 | const payload = {} 8 | 9 | it("fetchData", async () => { 10 | const boards = [{}, {}] 11 | const response = { data: { boards } } 12 | axios.get.mockResolvedValue(response) 13 | 14 | await actions.fetchData({ commit }) 15 | expect(commit).toHaveBeenCalledWith("SET_LOADING_STATE", true) 16 | expect(commit).toHaveBeenCalledWith("SET_INITIAL_DATA", { boards }) 17 | expect(commit).toHaveBeenCalledWith("SET_LOADING_STATE", false) 18 | }) 19 | 20 | it("saveTaskBoard", async () => { 21 | await actions.saveTaskBoard({ commit }, payload) 22 | expect(commit).toHaveBeenCalledWith("SAVE_TASKBOARD", payload) 23 | }) 24 | 25 | it("archiveTaskBoard", async () => { 26 | await actions.archiveTaskBoard({ commit }, payload) 27 | expect(commit).toHaveBeenCalledWith("ARCHIVE_TASKBOARD", payload) 28 | }) 29 | it("restoreTaskBoard", async () => { 30 | await actions.restoreTaskBoard({ commit }, payload) 31 | expect(commit).toHaveBeenCalledWith("RESTORE_TASKBOARD", payload) 32 | }) 33 | 34 | it("setActiveTaskBoard", async () => { 35 | await actions.setActiveTaskBoard({ commit }, payload) 36 | expect(commit).toHaveBeenCalledWith("SET_ACTIVE_TASKBOARD", payload) 37 | }) 38 | 39 | it("saveTaskList", async () => { 40 | await actions.saveTaskList({ commit }, payload) 41 | expect(commit).toHaveBeenCalledWith("SAVE_TASKLIST", payload) 42 | }) 43 | 44 | it("archiveTaskList", async () => { 45 | await actions.archiveTaskList({ commit }, payload) 46 | expect(commit).toHaveBeenCalledWith("ARCHIVE_TASKLIST", payload) 47 | }) 48 | 49 | it("restoreTaskList", async () => { 50 | await actions.restoreTaskList({ commit }, payload) 51 | expect(commit).toHaveBeenCalledWith("RESTORE_TASKLIST", payload) 52 | }) 53 | 54 | it("reorderTaskLists", async () => { 55 | await actions.reorderTaskLists({ commit }, payload) 56 | expect(commit).toHaveBeenCalledWith("REORDER_TASKLISTS", payload) 57 | }) 58 | 59 | it("reorderTaskListItems", async () => { 60 | await actions.reorderTaskListItems({ commit }, payload) 61 | expect(commit).toHaveBeenCalledWith("REORDER_TASKLIST_ITEMS", payload) 62 | }) 63 | 64 | it("saveTaskListItem", async () => { 65 | await actions.saveTaskListItem({ commit }, payload) 66 | expect(commit).toHaveBeenCalledWith("SAVE_TASKLIST_ITEM", payload) 67 | }) 68 | 69 | it("deleteTaskListItem", async () => { 70 | await actions.deleteTaskListItem({ commit }, payload) 71 | expect(commit).toHaveBeenCalledWith("DELETE_TASKLIST_ITEM", payload) 72 | }) 73 | }) 74 | -------------------------------------------------------------------------------- /tests/unit/store/getters.spec.js: -------------------------------------------------------------------------------- 1 | import getters from "@/store/getters" 2 | 3 | describe("Vuex Getters", () => { 4 | let state 5 | 6 | beforeEach(() => { 7 | state = { 8 | isLoading: true, 9 | activeBoard: { 10 | id: "1234", 11 | archived: false, 12 | lists: [ 13 | { 14 | id: "1234-1", 15 | name: "Todo", 16 | headerColor: "#607d8b", 17 | archived: false, 18 | items: [] 19 | } 20 | ] 21 | }, 22 | boards: [ 23 | { 24 | id: "123", 25 | archived: true, 26 | lists: [ 27 | { 28 | id: "123-1", 29 | name: "Todo", 30 | headerColor: "#607d8b", 31 | archived: false, 32 | items: [] 33 | }, 34 | { 35 | id: "123-2", 36 | name: "Doing", 37 | headerColor: "#607d8b", 38 | archived: false, 39 | items: [] 40 | } 41 | ] 42 | }, 43 | { 44 | id: "1234", 45 | archived: false, 46 | lists: [ 47 | { 48 | id: "1234-1", 49 | name: "Todo", 50 | headerColor: "#607d8b", 51 | archived: false, 52 | items: [] 53 | } 54 | ] 55 | } 56 | ] 57 | } 58 | }) 59 | 60 | it("isLoading", () => { 61 | expect(getters.isLoading(state)).toBe(true) 62 | }) 63 | it("allBoards", () => { 64 | expect(getters.allBoards(state)).toBe(state.boards) 65 | }) 66 | it("activeBoard", () => { 67 | expect(getters.activeBoard(state)).toBe(state.activeBoard) 68 | }) 69 | it("unarchivedBoards", () => { 70 | const received = getters.unarchivedBoards(state) 71 | const expected = state.boards.filter(b => !b.archived) 72 | expect(received).toEqual(expected) 73 | }) 74 | it("archivedBoards", () => { 75 | const received = getters.archivedBoards(state) 76 | const expected = state.boards.filter(b => b.archived) 77 | expect(received).toEqual(expected) 78 | }) 79 | it("archivedLists", () => { 80 | const received = getters.archivedLists(state) 81 | const expected = state.activeBoard.lists.filter(l => l.archived) 82 | expect(received).toEqual(expected) 83 | }) 84 | it("unarchivedLists", () => { 85 | const received = getters.unarchivedLists(state) 86 | const expected = state.activeBoard.lists.filter(l => !l.archived) 87 | expect(received).toEqual(expected) 88 | }) 89 | }) 90 | 91 | test("archivdLists returns blank array if activeBoard is not set", () => { 92 | let state = { activeBoard: null } 93 | const received = getters.archivedLists(state) 94 | const expected = [] 95 | expect(received).toEqual(expected) 96 | }) 97 | 98 | test("unarchivdLists returns blank array if activeBoard is not set", () => { 99 | let state = { activeBoard: null } 100 | const received = getters.unarchivedLists(state) 101 | const expected = [] 102 | expect(received).toEqual(expected) 103 | }) 104 | -------------------------------------------------------------------------------- /tests/unit/store/mutations.spec.js: -------------------------------------------------------------------------------- 1 | import mutations from "@/store/mutations" 2 | 3 | describe("SET_INITIAL_DATA", () => { 4 | const state = { 5 | boards: [] 6 | } 7 | it("Update state with boards information in payload", () => { 8 | const boards = [{}, {}] 9 | mutations.SET_INITIAL_DATA(state, boards) 10 | expect(state.boards).toBe(boards) 11 | }) 12 | }) 13 | 14 | describe("SET_LOADING_STATE", () => { 15 | const state = { 16 | isLoading: false 17 | } 18 | 19 | it("Set loading state", () => { 20 | const loadingState = true 21 | mutations.SET_LOADING_STATE(state, loadingState) 22 | expect(state.isLoading).toBe(loadingState) 23 | }) 24 | }) 25 | 26 | describe("SAVE_TASKBOARD", () => { 27 | let state 28 | 29 | beforeEach(() => { 30 | state = { 31 | boards: [ 32 | { 33 | id: "123", 34 | name: "project tracker", 35 | description: "project tracker board", 36 | archived: false, 37 | lists: [] 38 | } 39 | ] 40 | } 41 | }) 42 | 43 | it("Can save a new board", () => { 44 | const newBoard = { 45 | id: "321", 46 | name: "kanban board", 47 | description: "kanban project tracker board", 48 | archived: false, 49 | lists: [] 50 | } 51 | mutations.SAVE_TASKBOARD(state, newBoard) 52 | expect(state.boards.length).toBe(2) 53 | }) 54 | 55 | it("Can update an existing board", () => { 56 | const existingBoard = { 57 | id: "123", 58 | name: "project tracker board name", 59 | description: "project tracker board description", 60 | archived: false, 61 | lists: [] 62 | } 63 | mutations.SAVE_TASKBOARD(state, existingBoard) 64 | expect(state.boards.length).toBe(1) 65 | expect(state.boards[0].name).toBe("project tracker board name") 66 | expect(state.boards[0].description).toBe("project tracker board description") 67 | }) 68 | }) 69 | 70 | describe("Archive / Restore Taskboards", () => { 71 | let state 72 | const boardId = "123" 73 | 74 | beforeEach(() => { 75 | state = { 76 | boards: [ 77 | { 78 | id: "123", 79 | name: "project tracker", 80 | description: "project tracker board", 81 | archived: false, 82 | lists: [] 83 | } 84 | ] 85 | } 86 | }) 87 | 88 | it("Archive an existing task board", () => { 89 | mutations.ARCHIVE_TASKBOARD(state, { boardId: boardId }) 90 | expect(state.boards[0].archived).toBe(true) 91 | }) 92 | 93 | it("Restore an existing task board", () => { 94 | mutations.RESTORE_TASKBOARD(state, { boardId: boardId }) 95 | expect(state.boards[0].archived).toBe(false) 96 | }) 97 | }) 98 | 99 | describe("SAVE_TASKLIST", () => { 100 | let state 101 | 102 | beforeEach(() => { 103 | state = { 104 | boards: [ 105 | { 106 | id: "123", 107 | lists: [ 108 | { 109 | id: "123-1", 110 | name: "Todo", 111 | headerColor: "#607d8b", 112 | archived: false, 113 | items: [] 114 | } 115 | ] 116 | } 117 | ] 118 | } 119 | }) 120 | 121 | it("Add a new task list in existing board", () => { 122 | const boardId = "123" 123 | const listId = "0" 124 | const listName = "Doing" 125 | 126 | mutations.SAVE_TASKLIST(state, { boardId: boardId, listId: listId, name: listName }) 127 | expect(state.boards[0].lists.length).toBe(2) 128 | expect(state.boards[0].lists[1].name).toBe("Doing") 129 | }) 130 | 131 | it("Update an existing task list in a existing board", () => { 132 | const boardId = "123" 133 | const listId = "123-1" 134 | const listName = "Doing" 135 | 136 | mutations.SAVE_TASKLIST(state, { boardId: boardId, listId: listId, name: listName }) 137 | expect(state.boards[0].lists.length).toBe(1) 138 | expect(state.boards[0].lists[0].name).toBe("Doing") 139 | }) 140 | }) 141 | 142 | describe("Archive / Restore Tasklists", () => { 143 | let state 144 | const boardId = "123" 145 | const listId = "123-1" 146 | 147 | beforeEach(() => { 148 | state = { 149 | boards: [ 150 | { 151 | id: "123", 152 | lists: [ 153 | { 154 | id: "123-1", 155 | name: "Todo", 156 | headerColor: "#607d8b", 157 | archived: false, 158 | items: [] 159 | } 160 | ] 161 | } 162 | ] 163 | } 164 | }) 165 | 166 | it("Archive an existing task tasklist", () => { 167 | mutations.ARCHIVE_TASKLIST(state, { boardId, listId }) 168 | expect(state.boards[0].lists[0].archived).toBe(true) 169 | }) 170 | 171 | it("Restore an existing task list", () => { 172 | mutations.RESTORE_TASKLIST(state, { boardId, listId }) 173 | expect(state.boards[0].lists[0].archived).toBe(false) 174 | }) 175 | }) 176 | 177 | describe("Reorder task lists", () => { 178 | let state 179 | const boardId = "123" 180 | const listId = "123-1" 181 | 182 | beforeEach(() => { 183 | state = { 184 | boards: [ 185 | { 186 | id: "123", 187 | lists: [ 188 | { 189 | id: "123-1", 190 | name: "Todo", 191 | headerColor: "#607d8b", 192 | archived: false, 193 | items: [] 194 | }, 195 | { 196 | id: "123-2", 197 | name: "Doing", 198 | headerColor: "#607d8b", 199 | archived: false, 200 | items: [] 201 | } 202 | ] 203 | } 204 | ] 205 | } 206 | }) 207 | 208 | it("Reorders the task list", () => { 209 | const lists = [ 210 | { 211 | id: "123-2", 212 | name: "Doing", 213 | headerColor: "#607d8b", 214 | archived: false, 215 | items: [] 216 | }, 217 | { 218 | id: "123-1", 219 | name: "Todo", 220 | headerColor: "#607d8b", 221 | archived: false, 222 | items: [] 223 | } 224 | ] 225 | 226 | mutations.REORDER_TASKLISTS(state, { boardId, lists }) 227 | expect(state.boards[0].lists.length).toBe(2) 228 | expect(state.boards[0].lists).toBe(lists) 229 | }) 230 | }) 231 | 232 | describe("Reorder tasklist items", () => { 233 | let state 234 | const boardId = "123" 235 | const listId = "123-1" 236 | 237 | beforeEach(() => { 238 | state = { 239 | boards: [ 240 | { 241 | id: "123", 242 | lists: [ 243 | { 244 | id: "123-1", 245 | name: "Todo", 246 | headerColor: "#607d8b", 247 | archived: false, 248 | items: [{ id: "123-1-1", text: "First item" }, { id: "123-1-2", text: "Second item" }] 249 | } 250 | ] 251 | } 252 | ] 253 | } 254 | }) 255 | 256 | it("Reorders the task list items", () => { 257 | const items = [{ id: "123-1-2", text: "Second item" }, { id: "123-1-1", text: "First item" }] 258 | mutations.REORDER_TASKLIST_ITEMS(state, { boardId, listId, items }) 259 | 260 | expect(state.boards[0].lists[0].items.length).toBe(2) 261 | expect(state.boards[0].lists[0].items).toBe(items) 262 | }) 263 | }) 264 | 265 | describe("SET_ACTIVE_TASKBOARD", () => { 266 | let state 267 | 268 | beforeEach(() => { 269 | state = { 270 | activeBoard: null, 271 | boards: [ 272 | { 273 | id: "123", 274 | lists: [ 275 | { 276 | id: "123-1", 277 | name: "Todo", 278 | headerColor: "#607d8b", 279 | archived: false, 280 | items: [] 281 | } 282 | ] 283 | } 284 | ] 285 | } 286 | }) 287 | 288 | it("Sets the provided board active", () => { 289 | const board = state.boards[0] 290 | mutations.SET_ACTIVE_TASKBOARD(state, { board }) 291 | expect(state.activeBoard).toBe(board) 292 | }) 293 | }) 294 | 295 | describe("SAVE_TASKLIST_ITEM", () => { 296 | let state 297 | const boardId = "123" 298 | const listId = "123-1" 299 | 300 | beforeEach(() => { 301 | state = { 302 | boards: [ 303 | { 304 | id: "123", 305 | lists: [ 306 | { 307 | id: "123-1", 308 | name: "Todo", 309 | headerColor: "#607d8b", 310 | archived: false, 311 | items: [{ id: "123-1-1", text: "First item" }] 312 | } 313 | ] 314 | } 315 | ] 316 | } 317 | }) 318 | 319 | it("Adds a new item in the task list", () => { 320 | const item = { id: "0", text: "Second item" } 321 | mutations.SAVE_TASKLIST_ITEM(state, { boardId, listId, item }) 322 | 323 | expect(state.boards[0].lists[0].items.length).toBe(2) 324 | expect(state.boards[0].lists[0].items[1].text).toBe(item.text) 325 | }) 326 | 327 | it("Update an existing item in the existing task list", () => { 328 | const item = { id: "123-1-1", text: "Second item" } 329 | mutations.SAVE_TASKLIST_ITEM(state, { boardId, listId, item }) 330 | 331 | expect(state.boards[0].lists[0].items.length).toBe(1) 332 | expect(state.boards[0].lists[0].items[0].text).toBe(item.text) 333 | }) 334 | }) 335 | 336 | describe("DELETE_TASKLIST_ITEM", () => { 337 | let state 338 | const boardId = "123" 339 | const listId = "123-1" 340 | 341 | beforeEach(() => { 342 | state = { 343 | boards: [ 344 | { 345 | id: "123", 346 | lists: [ 347 | { 348 | id: "123-1", 349 | name: "Todo", 350 | headerColor: "#607d8b", 351 | archived: false, 352 | items: [{ id: "123-1-1", text: "First item" }] 353 | } 354 | ] 355 | } 356 | ] 357 | } 358 | }) 359 | 360 | it("Deletes an existing item in the task list", () => { 361 | const item = { id: "123-1-1", text: "First item" } 362 | mutations.DELETE_TASKLIST_ITEM(state, { boardId, listId, item }) 363 | expect(state.boards[0].lists[0].items.length).toBe(0) 364 | }) 365 | }) 366 | 367 | test("It does not delete invalid task list item", () => { 368 | const boardId = "123" 369 | const listId = "123-1" 370 | const item = { id: "123-1-2", text: "Second item" } 371 | let state = { 372 | boards: [ 373 | { 374 | id: "123", 375 | lists: [ 376 | { 377 | id: "123-1", 378 | name: "Todo", 379 | headerColor: "#607d8b", 380 | archived: false, 381 | items: [{ id: "123-1-1", text: "First item" }] 382 | } 383 | ] 384 | } 385 | ] 386 | } 387 | 388 | mutations.DELETE_TASKLIST_ITEM(state, { boardId, listId, item }) 389 | expect(state.boards[0].lists[0].items.length).toBe(1) 390 | }) 391 | -------------------------------------------------------------------------------- /tests/unit/views/Dashboard.spec.js: -------------------------------------------------------------------------------- 1 | import Dashboard from "@/views/Dashboard.vue" 2 | import { shallowMount, createLocalVue, RouterLinkStub } from "@vue/test-utils" 3 | import Vuex from "vuex" 4 | const localVue = createLocalVue() 5 | localVue.use(Vuex) 6 | 7 | describe("Dashboard.vue", () => { 8 | let $store 9 | let wrapper 10 | 11 | beforeEach(() => { 12 | $store = { 13 | dispatch: jest.fn(), 14 | state: {}, 15 | getters: { 16 | archivedBoards: [{ id: 111, name: "kanban", description: "kanban board", archived: true }], 17 | unarchivedBoards: [{ id: 222, name: "tracker", description: "tracker board", archived: false }] 18 | } 19 | } 20 | wrapper = shallowMount(Dashboard, { 21 | mocks: { $store }, 22 | stubs: { RouterLink: RouterLinkStub } 23 | }) 24 | }) 25 | 26 | it("dispatch setActiveTaskBoard action on created life cycle hook", () => { 27 | expect($store.dispatch).toHaveBeenCalledWith("setActiveTaskBoard", { board: null }) 28 | }) 29 | 30 | it("Renders the markup with the data", () => { 31 | expect(wrapper.text()).toContain("kanban") 32 | expect(wrapper.text()).toContain("tracker") 33 | expect(wrapper.find(RouterLinkStub).props().to).toEqual({ name: "task-board", params: { id: 222 } }) 34 | }) 35 | 36 | it("dispatch archiveTaskBoard action on Archive button click handler", () => { 37 | const boardItems = wrapper.findAll(".board-item") 38 | const archiveButton = boardItems.at(0).find("button") 39 | archiveButton.trigger("click") 40 | expect(archiveButton.text()).toContain("Archive") 41 | expect($store.dispatch).toHaveBeenCalledWith("archiveTaskBoard", { boardId: 222 }) 42 | }) 43 | 44 | it("dispatch unarchiveTaskBoard action on Restore button click handler", () => { 45 | const boardItems = wrapper.findAll(".board-item") 46 | const restoreButton = boardItems.at(1).find("button") 47 | restoreButton.trigger("click") 48 | expect(restoreButton.text()).toContain("Restore") 49 | expect($store.dispatch).toHaveBeenCalledWith("restoreTaskBoard", { boardId: 111 }) 50 | }) 51 | }) 52 | 53 | -------------------------------------------------------------------------------- /tests/unit/views/TaskBoard.spec.js: -------------------------------------------------------------------------------- 1 | import TaskBoard from "@/views/TaskBoard.vue" 2 | import { shallowMount, createLocalVue, RouterLinkStub } from "@vue/test-utils" 3 | import Vuex from "vuex" 4 | const localVue = createLocalVue() 5 | localVue.use(Vuex) 6 | 7 | describe("TaskBoard.vue", () => { 8 | let $store 9 | let wrapper 10 | let $route = { path: "/boards/:id", name: "task-board", params: { id: 222 } } 11 | 12 | beforeEach(() => { 13 | $store = { 14 | dispatch: jest.fn(), 15 | state: {}, 16 | getters: { 17 | allBoards: [ 18 | { id: 111, name: "kanban", description: "kanban board", archived: true, lists: [] }, 19 | { 20 | id: 222, 21 | name: "tracker", 22 | description: "tracker board", 23 | archived: false, 24 | lists: [ 25 | { id: "222-1", name: "list 1", archived: false, items: [] }, 26 | { id: "222-2", name: "list 1", archived: false, items: [] } 27 | ] 28 | } 29 | ] 30 | } 31 | } 32 | wrapper = shallowMount(TaskBoard, { 33 | mocks: { $store, $route }, 34 | stubs: { RouterLink: RouterLinkStub } 35 | }) 36 | }) 37 | 38 | it("has correct route link with params", () => { 39 | expect(wrapper.vm.$route.path).toBe($route.path) 40 | }) 41 | 42 | it("dispatch setActiveTaskBoard action on created life cycle hook", () => { 43 | expect($store.dispatch).toHaveBeenCalledWith("setActiveTaskBoard", { 44 | board: $store.getters.allBoards[1] 45 | }) 46 | }) 47 | 48 | it("dispatch reorderTaskLists action when lists computed property is updated", () => { 49 | const reorderedLists = [ 50 | { id: "222-2", name: "list 1", archived: false, items: [] }, 51 | { id: "222-1", name: "list 1", archived: false, items: [] } 52 | ] 53 | wrapper.vm.lists = reorderedLists 54 | expect($store.dispatch).toHaveBeenCalledWith("reorderTaskLists", { boardId: 222, lists: reorderedLists }) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | productionSourceMap: false 3 | } 4 | --------------------------------------------------------------------------------