<file_sep>import AppHeader from "@/components/AppHeader.vue"
import { shallowMount, createLocalVue, RouterLinkStub } from "@vue/test-utils"
import Vuex from "vuex"
const localVue = createLocalVue()
localVue.use(Vuex)

describe("AppHeader.vue", () => {
  let $store
  let wrapper

  it("Displays board name if activeBoard is set", () => {
    $store = {
      dispatch: jest.fn(),
      state: {},
      getters: {
        activeBoard: {
          id: 222,
          name: "tracker",
          description: "tracker board",
          archived: false,
          lists: []
        },
        isLoading: jest.fn()
      }
    }
    wrapper = shallowMount(AppHeader, {
      mocks: { $store },
      stubs: { RouterLink: RouterLinkStub }
    })
    expect(wrapper.text()).toContain("tracker")
  })

  it("display child component stubs when isLoading is false", () => {
    $store = {
      dispatch: jest.fn(),
      state: {},
      getters: {
        isLoading: false,
        activeBoard: {
          id: 222,
          name: "tracker",
          description: "tracker board",
          archived: false,
          lists: [{ id: "222-1", name: "todo" }, { id: "222-2", name: "doing" }]
        }
      }
    }

    wrapper = shallowMount(AppHeader, {
      mocks: { $store },
      stubs: {
        RouterLink: RouterLinkStub
      }
    })

    expect(wrapper.contains("tasklistrestore-stub")).toBe(true)
    expect(wrapper.contains("taskboardedit-stub")).toBe(true)
    expect(wrapper.contains("tasklistarchive-stub")).toBe(true)
    expect(wrapper.contains("tasklistedit-stub")).toBe(true)
  })

  it("does not render child component stubs when isLoading is true", () => {
    $store = {
      dispatch: jest.fn(),
      state: {},
      getters: {
        isLoading: true,
        activeBoard: {
          id: 222,
          name: "tracker",
          description: "tracker board",
          archived: false,
          lists: [{ id: "222-1", name: "todo" }, { id: "222-2", name: "doing" }]
        }
      }
    }

    wrapper = shallowMount(AppHeader, {
      mocks: { $store },
      stubs: {
        RouterLink: RouterLinkStub
      }
    })

    expect(wrapper.contains("tasklistrestore-stub")).toBe(false)
    expect(wrapper.contains("taskboardedit-stub")).toBe(false)
    expect(wrapper.contains("tasklistarchive-stub")).toBe(false)
    expect(wrapper.contains("tasklistedit-stub")).toBe(false)
  })
})
<file_sep>import TaskListActions from "@/components/Lists/TaskListActions.vue"
import { mount, createLocalVue } from "@vue/test-utils"
import { Bus } from "@/utils/bus"

Bus.$emit = jest.fn()

describe("TaskListActions.vue", () => {
  let wrapper
  let propsData

  beforeEach(() => {
    propsData = {
      board: { id: "222" },
      list: { id: "222-2" }
    }

    wrapper = mount(TaskListActions, {
      propsData: { board: propsData.board, list: propsData.list },
      mocks: {},
      stubs: {}
    })
  })

  it("tasklist-editing event is triggered", () => {
    wrapper.vm.showListEditPopup()
    expect(Bus.$emit).toHaveBeenCalledWith("tasklist-editing", wrapper.vm.list)
  })

  it("tasklist-archiving event is triggered", () => {
    wrapper.vm.showArchiveListPopup()
    expect(Bus.$emit).toHaveBeenCalledWith("tasklist-archiving", { board: wrapper.vm.board, list: wrapper.vm.list })
  })
})
<file_sep>import axios from "axios"
import actions from "@/store/actions"
jest.mock("axios")

describe("Test actions", () => {
  const commit = jest.fn()
  const payload = {}

  it("fetchData", async () => {
    const boards = [{}, {}]
    const response = { data: { boards } }
    axios.get.mockResolvedValue(response)

    await actions.fetchData({ commit })
    expect(commit).toHaveBeenCalledWith("SET_LOADING_STATE", true)
    expect(commit).toHaveBeenCalledWith("SET_INITIAL_DATA", { boards })
    expect(commit).toHaveBeenCalledWith("SET_LOADING_STATE", false)
  })

  it("saveTaskBoard", async () => {
    await actions.saveTaskBoard({ commit }, payload)
    expect(commit).toHaveBeenCalledWith("SAVE_TASKBOARD", payload)
  })

  it("archiveTaskBoard", async () => {
    await actions.archiveTaskBoard({ commit }, payload)
    expect(commit).toHaveBeenCalledWith("ARCHIVE_TASKBOARD", payload)
  })
  it("restoreTaskBoard", async () => {
    await actions.restoreTaskBoard({ commit }, payload)
    expect(commit).toHaveBeenCalledWith("RESTORE_TASKBOARD", payload)
  })

  it("setActiveTaskBoard", async () => {
    await actions.setActiveTaskBoard({ commit }, payload)
    expect(commit).toHaveBeenCalledWith("SET_ACTIVE_TASKBOARD", payload)
  })

  it("saveTaskList", async () => {
    await actions.saveTaskList({ commit }, payload)
    expect(commit).toHaveBeenCalledWith("SAVE_TASKLIST", payload)
  })

  it("archiveTaskList", async () => {
    await actions.archiveTaskList({ commit }, payload)
    expect(commit).toHaveBeenCalledWith("ARCHIVE_TASKLIST", payload)
  })

  it("restoreTaskList", async () => {
    await actions.restoreTaskList({ commit }, payload)
    expect(commit).toHaveBeenCalledWith("RESTORE_TASKLIST", payload)
  })

  it("reorderTaskLists", async () => {
    await actions.reorderTaskLists({ commit }, payload)
    expect(commit).toHaveBeenCalledWith("REORDER_TASKLISTS", payload)
  })

  it("reorderTaskListItems", async () => {
    await actions.reorderTaskListItems({ commit }, payload)
    expect(commit).toHaveBeenCalledWith("REORDER_TASKLIST_ITEMS", payload)
  })

  it("saveTaskListItem", async () => {
    await actions.saveTaskListItem({ commit }, payload)
    expect(commit).toHaveBeenCalledWith("SAVE_TASKLIST_ITEM", payload)
  })

  it("deleteTaskListItem", async () => {
    await actions.deleteTaskListItem({ commit }, payload)
    expect(commit).toHaveBeenCalledWith("DELETE_TASKLIST_ITEM", payload)
  })
})
<file_sep>import TaskList from "@/components/Lists/TaskList.vue"
import { shallowMount, createLocalVue, RouterLinkStub } from "@vue/test-utils"
import Vuex from "vuex"
const localVue = createLocalVue()
localVue.use(Vuex)

describe("TaskList.vue", () => {
  let $store
  let wrapper
  let board

  beforeEach(() => {
    board = {
      id: 222,
      name: "tracker",
      description: "tracker board",
      archived: false,
      lists: [
        { id: "222-1", name: "todo", archived: false, headerColor: "#ddd", items: [] },
        {
          id: "222-2",
          name: "doing",
          archived: false,
          headerColor: "#ddd",
          items: [
            { id: "222-2-1", text: "This is a list item #1" },
            { id: "222-2-2", text: "This is a list item #2" },
            { id: "222-2-3", text: "This is a list item #3" }
          ]
        }
      ]
    }

    $store = {
      dispatch: jest.fn()
    }

    wrapper = shallowMount(TaskList, {
      data() {
        return {
          isDesktop: true,
          isTablet: false
        }
      },
      propsData: { board: board, list: board.lists[1] },
      mocks: { $store },
      stubs: { RouterLink: RouterLinkStub }
    })
  })

  it("dispatch reorderTaskListItems action when items computed property is updated", () => {
    const listItems = [
      { id: "222-2-2", text: "This is a list item #2" },
      { id: "222-2-1", text: "This is a list item #1" },
      { id: "222-2-3", text: "This is a list item #3" }
    ]
    wrapper.vm.items = listItems
    expect($store.dispatch).toHaveBeenCalledWith("reorderTaskListItems", {
      boardId: board.id,
      listId: board.lists[1].id,
      items: listItems
    })
  })

  it("Should allow task items reorder on desktop or tablet", () => {
    wrapper.vm.isDesktop = true
    wrapper.vm.isTablet = false
    expect(wrapper.vm.shouldAllowTaskItemsReorder).toBe(true)
  })

  it("Should not allow task items reorder if not on desktop or tablet", () => {
    wrapper.vm.isDesktop = false
    wrapper.vm.isTablet = false
    expect(wrapper.vm.shouldAllowTaskItemsReorder).toBe(false)
  })

  it("Disable drag option while editing an list item", () => {
    wrapper.vm.itemEditing()
    wrapper.vm.isDesktop = true
    wrapper.vm.isTablet = true
    expect(wrapper.vm.dragOptions.disabled).toBe(true)
  })

  it("Enable drag option when list item is edited", () => {
    wrapper.vm.itemEdited()
    wrapper.vm.isDesktop = true
    wrapper.vm.isTablet = true
    expect(wrapper.vm.dragOptions.disabled).toBe(false)
  })

  it("Enable drag option when list item is edited", () => {
    wrapper.vm.itemCancelled()
    wrapper.vm.isDesktop = true
    wrapper.vm.isTablet = true
    expect(wrapper.vm.dragOptions.disabled).toBe(false)
  })
})
<file_sep>import DetailsDropdown from "@/components/Details/DetailsDropdown"
import { shallowMount, createLocalVue, RouterLinkStub } from "@vue/test-utils"

describe("DetailsDropdown.vue", () => {
  let wrapper

  beforeEach(() => {
    wrapper = shallowMount(DetailsDropdown, {
      mocks: {},
      stubs: {}
    })
  })

  it("Open dropdown", () => {
    wrapper.vm.open()
    expect(
      wrapper
        .find("details")
        .html()
        .includes("open")
    ).toBe(true)
  })

  it("Close dropdown", () => {
    wrapper.vm.close()

    expect(
      wrapper
        .find("details")
        .html()
        .includes("open")
    ).toBe(false)
  })

  it("toggle check - open", () => {
    wrapper.vm.open()
    wrapper.find("details").trigger("toggle")
    expect(wrapper.emitted("popup-toggled")).toBeTruthy()
  })

  it("toggle check - close", () => {
    wrapper.vm.close()
    wrapper.find("details").trigger("toggle")
    expect(wrapper.emitted("popup-toggled")).toBeTruthy()
  })
})
<file_sep>import TaskListItem from "@/components/Items/TaskListItem.vue"
import { shallowMount, createLocalVue, RouterLinkStub } from "@vue/test-utils"
import flushPromises from "flush-promises"
import Vuex from "vuex"
import VeeValidate from "vee-validate"
const localVue = createLocalVue()

localVue.use(Vuex)
localVue.use(VeeValidate)

describe("TasListItem.vue", () => {
  let $store
  let wrapper
  let propsData

  beforeEach(() => {
    $store = {
      dispatch: jest.fn()
    }
    propsData = {
      item: { id: "222-1-1", text: "This is a list item" },
      list: { id: "222-1" },
      board: { id: "222" }
    }

    wrapper = shallowMount(TaskListItem, {
      localVue,
      propsData: propsData,
      mocks: { $store },
      stubs: { RouterLink: RouterLinkStub }
    })
  })

  it("clears the form data", () => {
    wrapper.setData({
      form: {
        id: "222-1-1",
        text: "This is a list item"
      }
    })

    wrapper.vm.clearForm()

    expect(wrapper.vm.form.id).toBe("")
    expect(wrapper.vm.form.text).toBe("")
  })

  it("saveTaskListItem action is called on save", async () => {
    wrapper.vm.$validator.validateAll = jest.fn(() => Promise.resolve(true))
    wrapper.vm.startEditing()
    wrapper.vm.save()
    await flushPromises()
    expect(wrapper.vm.$validator.validateAll).toHaveBeenCalled()
    expect($store.dispatch).toHaveBeenCalledWith("saveTaskListItem", {
      boardId: propsData.board.id,
      listId: propsData.list.id,
      item: propsData.item
    })
    expect(wrapper.emitted("item-edited")).toBeTruthy()
  })

  it("does not call saveTaskListItem action, when form validation is failed", async () => {
    wrapper.vm.$validator.validateAll = jest.fn(() => Promise.resolve(false))
    wrapper.vm.startEditing()
    wrapper.vm.save()
    await flushPromises()
    expect(wrapper.vm.$validator.validateAll).toHaveBeenCalled()
    expect($store.dispatch).not.toHaveBeenCalled()
    expect(wrapper.emitted("item-edited")).not.toBeTruthy()
  })

  it("emits 'item-editing' event when moving into edit mode", () => {
    wrapper.vm.startEditing()
    // Test if "item-editing event is emitted or not"
    expect(wrapper.emitted("item-editing")).toBeTruthy()
  })

  it("emits 'item-cancelled' event when edit mode is cancelled", () => {
    wrapper.vm.cancel()
    // Test if "item-cancelled event is emitted or not"
    expect(wrapper.emitted("item-cancelled")).toBeTruthy()
  })

  it("Calls 'deleteTaskListItem' action and emit 'item-deleted' event when remove method is called", () => {
    wrapper.vm.remove()

    // Test if "deleteTaskListItem" action have been called
    // with appropriate arguments or not
    expect($store.dispatch).toHaveBeenCalledWith("deleteTaskListItem", {
      boardId: propsData.board.id,
      listId: propsData.list.id,
      item: propsData.item
    })

    // Test if "item-deleted event is emitted or not"
    expect(wrapper.emitted("item-deleted")).toBeTruthy()
  })
})

describe("Test computed properties", () => {
  let wrapper
  let propsData

  beforeEach(() => {
    propsData = {
      item: { id: "", text: "" },
      list: { id: "222-1" },
      board: { id: "222" }
    }
  })

  it("Displays '+ New Item'", () => {
    wrapper = shallowMount(TaskListItem, {
      localVue,
      propsData: propsData,
      stubs: { RouterLink: RouterLinkStub }
    })

    expect(wrapper.vm.displayText).toBe("+ New Item")
  })

  it("Displays 'This is a list item' when item is passed", () => {
    propsData.item = { id: "222-1-1", text: "This is a list item" }

    wrapper = shallowMount(TaskListItem, {
      localVue,
      propsData: propsData,
      stubs: { RouterLink: RouterLinkStub }
    })
    expect(wrapper.vm.displayText).toBe("This is a list item")
  })
})
<file_sep>import TaskBoardEdit from "@/components/Boards/TaskBoardEdit.vue"
import { mount, createLocalVue, RouterLinkStub } from "@vue/test-utils"
import flushPromises from "flush-promises"
import Vuex from "vuex"
import VeeValidate from "vee-validate"
const localVue = createLocalVue()
localVue.use(Vuex)
localVue.use(VeeValidate)

import { Bus } from "@/utils/bus"
Bus.$on = jest.fn()

describe("TaskBoardEdit.vue", () => {
  let $store
  let wrapper

  beforeEach(() => {
    $store = {
      dispatch: jest.fn(),
      getters: {
        activeBoard: { id: "222" }
      }
    }
    wrapper = mount(TaskBoardEdit, {
      sync: false,
      localVue,
      mocks: { $store },
      stubs: { RouterLink: RouterLinkStub }
    })
  })

  it("clear boardForm data on handlePopupToggled", () => {
    wrapper.vm.handlePopupToggled(false)
    expect(wrapper.vm.boardForm.id).toBe(0)
    expect(wrapper.vm.boardForm.name).toBe("")
    expect(wrapper.vm.boardForm.description).toBe("")
  })

  it("does not clear boardForm data on handlePopupToggled", () => {
    wrapper.setData({
      boardForm: {
        id: "222",
        name: "tracker",
        description: "tracker board"
      }
    })
    wrapper.vm.handlePopupToggled(true)
    expect(wrapper.vm.boardForm.id).toBe("222")
    expect(wrapper.vm.boardForm.name).toBe("tracker")
    expect(wrapper.vm.boardForm.description).toBe("tracker board")
  })

  it("listens for 'taskboard-editing' on event bus", () => {
    Bus.$emit("taskboard-editing", {
      id: "222",
      name: "tracker",
      description: "tracker board"
    })

    expect(Bus.$on).toHaveBeenCalledWith("taskboard-editing", wrapper.vm.handleTaskBoardEditing)
  })

  it("Heading property returns 'Create new board' if creating a new board", () => {
    expect(wrapper.vm.heading).toBe("Create new board")
  })

  it("Heading property returns 'Update board information' if editing existing board", () => {
    wrapper.setData({
      boardForm: {
        id: 222
      }
    })

    expect(wrapper.vm.heading).toBe("Update board information")
  })

  it("saveTaskBoard action is called on handleSaveBoard", async () => {
    const boardForm = {
      id: "222",
      name: "tracker",
      description: "tracker board"
    }

    wrapper.vm.$validator.validateAll = jest.fn(() => Promise.resolve(true))
    wrapper.vm.handleTaskBoardEditing(boardForm)
    wrapper.vm.handleSaveBoard()
    await flushPromises()

    expect(wrapper.vm.$validator.validateAll).toHaveBeenCalled()

    expect($store.dispatch).toHaveBeenCalledWith("saveTaskBoard", {
      id: boardForm.id,
      name: boardForm.name,
      description: boardForm.description
    })
  })
})
<file_sep>import DetailsInline from "@/components/Details/DetailsInline"
import { shallowMount, createLocalVue, RouterLinkStub } from "@vue/test-utils"

describe("DetailsInline.vue", () => {
  let wrapper

  beforeEach(() => {
    wrapper = shallowMount(DetailsInline, {
      mocks: {},
      stubs: {}
    })
  })

  it("Open dropdown", () => {
    wrapper.vm.open()
    expect(
      wrapper
        .find("details")
        .html()
        .includes("open")
    ).toBe(true)
  })

  it("Close dropdown", () => {
    wrapper.vm.close()

    expect(
      wrapper
        .find("details")
        .html()
        .includes("open")
    ).toBe(false)
  })

  it("toggle check - open", () => {
    wrapper.vm.open()
    wrapper.find("details").trigger("toggle")
    expect(wrapper.emitted("popup-toggled")).toBeTruthy()
  })

  it("toggle check - close", () => {
    wrapper.vm.close()
    wrapper.find("details").trigger("toggle")
    expect(wrapper.emitted("popup-toggled")).toBeTruthy()
  })
})
<file_sep># Task Management Application using Vue.js

A task managment board app built using Vue.js (Trello, JIRA style) for a project during my course. 
I've added features such as user authentication system, assigning members and made some UX improvements apart from those covered in the tutorial.

Followed from the tutorial by techlab23 [Article Link](https://levelup.gitconnected.com/task-management-application-using-vue-js-part-1-df607ca30f48?gi=c51e20fc8189).

![Project Image](/docs/images/Screenshot.png)

## Build Setup

```bash
# install dependencies
npm install

# serve with hot reload at localhost:8080
npm run serve

# build for production with minification
npm run build

# run unit tests in watch mode
npm run test:unit

# run test coverage
npm run test:coverage

# To deploy your app on surge make sure you have surge cli
# installed globally on your machine.
# If you have it insalled already then feel free to skip this step
npm install -g surge

# Build and deploy the app on surge.sh in staging environment
# Note: Before running this command, please change the site url
# used for this command in package.json file.
npm run deploy-staging

# Build and deploy the app on surge.sh in production environment
# Note: Before running this command, please change the site url
# used for this command in package.json file.
npm run deploy
```
<file_sep>import getters from "@/store/getters"

describe("Vuex Getters", () => {
  let state

  beforeEach(() => {
    state = {
      isLoading: true,
      activeBoard: {
        id: "1234",
        archived: false,
        lists: [
          {
            id: "1234-1",
            name: "Todo",
            headerColor: "#607d8b",
            archived: false,
            items: []
          }
        ]
      },
      boards: [
        {
          id: "123",
          archived: true,
          lists: [
            {
              id: "123-1",
              name: "Todo",
              headerColor: "#607d8b",
              archived: false,
              items: []
            },
            {
              id: "123-2",
              name: "Doing",
              headerColor: "#607d8b",
              archived: false,
              items: []
            }
          ]
        },
        {
          id: "1234",
          archived: false,
          lists: [
            {
              id: "1234-1",
              name: "Todo",
              headerColor: "#607d8b",
              archived: false,
              items: []
            }
          ]
        }
      ]
    }
  })

  it("isLoading", () => {
    expect(getters.isLoading(state)).toBe(true)
  })
  it("allBoards", () => {
    expect(getters.allBoards(state)).toBe(state.boards)
  })
  it("activeBoard", () => {
    expect(getters.activeBoard(state)).toBe(state.activeBoard)
  })
  it("unarchivedBoards", () => {
    const received = getters.unarchivedBoards(state)
    const expected = state.boards.filter(b => !b.archived)
    expect(received).toEqual(expected)
  })
  it("archivedBoards", () => {
    const received = getters.archivedBoards(state)
    const expected = state.boards.filter(b => b.archived)
    expect(received).toEqual(expected)
  })
  it("archivedLists", () => {
    const received = getters.archivedLists(state)
    const expected = state.activeBoard.lists.filter(l => l.archived)
    expect(received).toEqual(expected)
  })
  it("unarchivedLists", () => {
    const received = getters.unarchivedLists(state)
    const expected = state.activeBoard.lists.filter(l => !l.archived)
    expect(received).toEqual(expected)
  })
})

test("archivdLists returns blank array if activeBoard is not set", () => {
  let state = { activeBoard: null }
  const received = getters.archivedLists(state)
  const expected = []
  expect(received).toEqual(expected)
})

test("unarchivdLists returns blank array if activeBoard is not set", () => {
  let state = { activeBoard: null }
  const received = getters.unarchivedLists(state)
  const expected = []
  expect(received).toEqual(expected)
})
