diff --git a/package-lock.json b/package-lock.json index e999b62810..f55b1b974e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -190,6 +190,15 @@ "@types/babel-types": "7.0.1" } }, + "@vue/test-utils": { + "version": "1.0.0-beta.12", + "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-1.0.0-beta.12.tgz", + "integrity": "sha512-457S/w+VuHnh4jw03ingrVAx8jMbxRz+jGGjoTeEFPZzv20GDzPUauQQqDy71EYw6BiNscC0RGOaLvAcS6BZ9Q==", + "dev": true, + "requires": { + "lodash": "4.17.5" + } + }, "JSONStream": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.2.tgz", diff --git a/package.json b/package.json index 61d0ca2ecc..c40824439b 100644 --- a/package.json +++ b/package.json @@ -140,6 +140,7 @@ "apidoc": "gulp apidoc" }, "devDependencies": { + "@vue/test-utils": "^1.0.0-beta.12", "babel-plugin-syntax-object-rest-spread": "^6.13.0", "babel-plugin-istanbul": "^4.1.6", "chai": "^4.1.2", diff --git a/test/client/unit/specs/components/tasks/column.js b/test/client/unit/specs/components/tasks/column.js new file mode 100644 index 0000000000..268fc08cd3 --- /dev/null +++ b/test/client/unit/specs/components/tasks/column.js @@ -0,0 +1,211 @@ +import { shallow, createLocalVue } from '@vue/test-utils'; + +import TaskColumn from 'client/components/tasks/column.vue'; + +import Store from 'client/libs/store'; + +// eslint-disable no-exclusive-tests + +const localVue = createLocalVue(); +localVue.use(Store); + +describe('Task Column', () => { + let wrapper; + let store, getters; + let habits, taskListOverride, tasks; + + function makeWrapper (additionalSetup = {}) { + let type = 'habit'; + let mocks = { + $t () {}, + }; + let stubs = ['b-modal']; // is a custom component and not tested here + + return shallow(TaskColumn, { + propsData: { + type, + }, + mocks, + stubs, + localVue, + ...additionalSetup, + }); + } + + it('returns a vue instance', () => { + wrapper = makeWrapper(); + expect(wrapper.isVueInstance()).to.be.true; + }); + + describe('Passed Properties', () => { + beforeEach(() => { + wrapper = makeWrapper(); + }); + + it('defaults isUser to false', () => { + expect(wrapper.vm.isUser).to.be.false; + }); + + it('passes isUser to component instance', () => { + wrapper.setProps({ isUser: false }); + + expect(wrapper.vm.isUser).to.be.false; + + wrapper.setProps({ isUser: true }); + + expect(wrapper.vm.isUser).to.be.true; + }); + }); + + describe('Computed Properties', () => { + beforeEach(() => { + habits = [ + { id: 1 }, + { id: 2 }, + ]; + + taskListOverride = [ + { id: 3 }, + { id: 4 }, + ]; + + getters = { + // (...) => { ... } will return a value + // (...) => (...) => { ... } will return a function + // Task Column expects a function + 'tasks:getFilteredTaskList': () => () => habits, + }; + + store = new Store({getters}); + + wrapper = makeWrapper({store}); + }); + + it('returns task list from props for group-plan', () => { + wrapper.setProps({ taskListOverride }); + + wrapper.vm.taskList.forEach((el, i) => { + expect(el).to.eq(taskListOverride[i]); + }); + + wrapper.setProps({ isUser: false, taskListOverride }); + + wrapper.vm.taskList.forEach((el, i) => { + expect(el).to.eq(taskListOverride[i]); + }); + }); + + it('returns task list from store for user', () => { + wrapper.setProps({ isUser: true, taskListOverride }); + + wrapper.vm.taskList.forEach((el, i) => { + expect(el).to.eq(habits[i]); + }); + }); + }); + + describe('Methods', () => { + describe('Filter By Tags', () => { + beforeEach(() => { + tasks = [ + { tags: [3, 4] }, + { tags: [2, 3] }, + { tags: [] }, + { tags: [1, 3] }, + ]; + }); + + it('returns all tasks if no tag is given', () => { + let returnedTasks = wrapper.vm.filterByTagList(tasks); + expect(returnedTasks).to.have.lengthOf(tasks.length); + tasks.forEach((task, i) => { + expect(returnedTasks[i]).to.eq(task); + }); + }); + + it('returns tasks for given single tag', () => { + let returnedTasks = wrapper.vm.filterByTagList(tasks, [3]); + + expect(returnedTasks).to.have.lengthOf(3); + expect(returnedTasks[0]).to.eq(tasks[0]); + expect(returnedTasks[1]).to.eq(tasks[1]); + expect(returnedTasks[2]).to.eq(tasks[3]); + }); + + it('returns tasks for given multiple tags', () => { + let returnedTasks = wrapper.vm.filterByTagList(tasks, [2, 3]); + + expect(returnedTasks).to.have.lengthOf(1); + expect(returnedTasks[0]).to.eq(tasks[1]); + }); + }); + + describe('Filter By Search Text', () => { + beforeEach(() => { + tasks = [ + { + text: 'Hello world 1', + note: '', + checklist: [], + }, + { + text: 'Hello world 2', + note: '', + checklist: [], + }, + { + text: 'Generic Task Title', + note: '', + checklist: [ + { text: 'Check 1' }, + { text: 'Check 2' }, + { text: 'Check 3' }, + ], + }, + { + text: 'Hello world 3', + note: 'Generic Task Note', + checklist: [ + { text: 'Checkitem 1' }, + { text: 'Checkitem 2' }, + { text: 'Checkitem 3' }, + ], + }, + ]; + }); + + it('returns all tasks for empty search term', () => { + let returnedTasks = wrapper.vm.filterBySearchText(tasks); + expect(returnedTasks).to.have.lengthOf(tasks.length); + tasks.forEach((task, i) => { + expect(returnedTasks[i]).to.eq(task); + }); + }); + + it('returns tasks for search term in title /i', () => { + ['Title', 'TITLE', 'title', 'tItLe'].forEach((term) => { + expect(wrapper.vm.filterBySearchText(tasks, term)[0]).to.eq(tasks[2]); + }); + }); + + it('returns tasks for search term in note /i', () => { + ['Note', 'NOTE', 'note', 'nOtE'].forEach((term) => { + expect(wrapper.vm.filterBySearchText(tasks, term)[0]).to.eq(tasks[3]); + }); + }); + + it('returns tasks for search term in checklist title /i', () => { + ['Check', 'CHECK', 'check', 'cHeCK'].forEach((term) => { + let returnedTasks = wrapper.vm.filterBySearchText(tasks, term); + + expect(returnedTasks[0]).to.eq(tasks[2]); + expect(returnedTasks[1]).to.eq(tasks[3]); + }); + + ['Checkitem', 'CHECKITEM', 'checkitem', 'cHeCKiTEm'].forEach((term) => { + expect(wrapper.vm.filterBySearchText(tasks, term)[0]).to.eq(tasks[3]); + }); + }); + }); + }); +}); diff --git a/test/client/unit/specs/libs/store/helpers/filterTasks.js b/test/client/unit/specs/libs/store/helpers/filterTasks.js new file mode 100644 index 0000000000..6537f122b2 --- /dev/null +++ b/test/client/unit/specs/libs/store/helpers/filterTasks.js @@ -0,0 +1,62 @@ +import { + getTypeLabel, + getFilterLabels, + getActiveFilter, +} from 'client/libs/store/helpers/filterTasks.js'; + +describe('Filter Category for Tasks', () => { + describe('getTypeLabel', () => { + it('should return correct task type labels', () => { + expect(getTypeLabel('habit')).to.eq('habits'); + expect(getTypeLabel('daily')).to.eq('dailies'); + expect(getTypeLabel('todo')).to.eq('todos'); + expect(getTypeLabel('reward')).to.eq('rewards'); + }); + }); + + describe('getFilterLabels', () => { + let habit, daily, todo, reward; + beforeEach(() => { + habit = ['all', 'yellowred', 'greenblue']; + daily = ['all', 'due', 'notDue']; + todo = ['remaining', 'scheduled', 'complete2']; + reward = ['all', 'custom', 'wishlist']; + }); + + it('should return all task type filter labels by type', () => { + // habits + getFilterLabels('habit').forEach((item, i) => { + expect(item).to.eq(habit[i]); + }); + // dailys + getFilterLabels('daily').forEach((item, i) => { + expect(item).to.eq(daily[i]); + }); + // todos + getFilterLabels('todo').forEach((item, i) => { + expect(item).to.eq(todo[i]); + }); + // rewards + getFilterLabels('reward').forEach((item, i) => { + expect(item).to.eq(reward[i]); + }); + }); + }); + + describe('getActiveFilter', () => { + it('should return single function by default', () => { + let activeFilter = getActiveFilter('habit'); + expect(activeFilter).to.be.an('object'); + expect(activeFilter).to.have.all.keys('label', 'filterFn', 'default'); + expect(activeFilter.default).to.be.true; + }); + + it('should return single function for given filter type', () => { + let activeFilterLabel = 'yellowred'; + let activeFilter = getActiveFilter('habit', activeFilterLabel); + expect(activeFilter).to.be.an('object'); + expect(activeFilter).to.have.all.keys('label', 'filterFn'); + expect(activeFilter.label).to.eq(activeFilterLabel); + }); + }); +}); diff --git a/test/client/unit/specs/libs/store/helpers/orderTasks.js b/test/client/unit/specs/libs/store/helpers/orderTasks.js new file mode 100644 index 0000000000..e293fc0f32 --- /dev/null +++ b/test/client/unit/specs/libs/store/helpers/orderTasks.js @@ -0,0 +1,43 @@ +import { + orderSingleTypeTasks, + // orderMultipleTypeTasks, +} from 'client/libs/store/helpers/orderTasks.js'; + +import shuffle from 'lodash/shuffle'; + +describe('Task Order Helper Function', () => { + let tasks, shuffledTasks, taskOrderList; + beforeEach(() => { + taskOrderList = [1, 2, 3, 4]; + tasks = []; + taskOrderList.forEach(i => tasks.push({ _id: i, id: i })); + shuffledTasks = shuffle(tasks); + }); + + it('should return tasks as is for no task order', () => { + expect(orderSingleTypeTasks(shuffledTasks)).to.eq(shuffledTasks); + }); + + it('should return tasks in expected order', () => { + let newOrderedTasks = orderSingleTypeTasks(shuffledTasks, taskOrderList); + newOrderedTasks.forEach((item, index) => { + expect(item).to.eq(tasks[index]); + }); + }); + + it('should return new tasks at end of expected order', () => { + let newTaskIds = [10, 15, 20]; + newTaskIds.forEach(i => tasks.push({ _id: i, id: i })); + shuffledTasks = shuffle(tasks); + + let newOrderedTasks = orderSingleTypeTasks(shuffledTasks, taskOrderList); + // checking tasks with order + newOrderedTasks.slice(0, taskOrderList.length).forEach((item, index) => { + expect(item).to.eq(tasks[index]); + }); + // check for new task ids + newOrderedTasks.slice(-3).forEach(item => { + expect(item.id).to.be.oneOf(newTaskIds); + }); + }); +}); diff --git a/test/client/unit/specs/store/getters/tasks/getTasks.js b/test/client/unit/specs/store/getters/tasks/getTasks.js new file mode 100644 index 0000000000..1de50b15cc --- /dev/null +++ b/test/client/unit/specs/store/getters/tasks/getTasks.js @@ -0,0 +1,118 @@ +import generateStore from 'client/store'; + +describe('Store Getters for Tasks', () => { + let store, habits, dailys, todos, rewards; + + beforeEach(() => { + store = generateStore(); + // Get user preference data and user tasks order data + store.state.user.data = { + preferences: {}, + tasksOrder: { + habits: [], + dailys: [], + todos: [], + rewards: [], + }, + }; + }); + + describe('Task List', () => { + beforeEach(() => { + habits = [ + { id: 1 }, + { id: 2 }, + ]; + dailys = [ + { id: 3 }, + { id: 4 }, + ]; + todos = [ + { id: 5 }, + { id: 6 }, + ]; + rewards = [ + { id: 7 }, + { id: 8 }, + ]; + store.state.tasks.data = { + habits, + dailys, + todos, + rewards, + }; + }); + + it('should returns all tasks by task type', () => { + let returnedTasks = store.getters['tasks:getUnfilteredTaskList']('habit'); + expect(returnedTasks).to.eq(habits); + + returnedTasks = store.getters['tasks:getUnfilteredTaskList']('daily'); + expect(returnedTasks).to.eq(dailys); + + returnedTasks = store.getters['tasks:getUnfilteredTaskList']('todo'); + expect(returnedTasks).to.eq(todos); + + returnedTasks = store.getters['tasks:getUnfilteredTaskList']('reward'); + expect(returnedTasks).to.eq(rewards); + }); + }); + + // @TODO add task filter check for rewards and dailys + describe('Task Filters', () => { + beforeEach(() => { + habits = [ + // weak habit + { value: 0 }, + // strong habit + { value: 2 }, + ]; + todos = [ + // scheduled todos + { completed: false, date: 'Mon, 15 Jan 2018 12:18:29 GMT' }, + // completed todos + { completed: true }, + ]; + store.state.tasks.data = { + habits, + todos, + }; + }); + + it('should return weak habits', () => { + let returnedTasks = store.getters['tasks:getFilteredTaskList']({ + type: 'habit', + filterType: 'yellowred', + }); + + expect(returnedTasks[0]).to.eq(habits[0]); + }); + + it('should return strong habits', () => { + let returnedTasks = store.getters['tasks:getFilteredTaskList']({ + type: 'habit', + filterType: 'greenblue', + }); + + expect(returnedTasks[0]).to.eq(habits[1]); + }); + + it('should return scheduled todos', () => { + let returnedTasks = store.getters['tasks:getFilteredTaskList']({ + type: 'todo', + filterType: 'scheduled', + }); + + expect(returnedTasks[0]).to.eq(todos[0]); + }); + + it('should return completed todos', () => { + let returnedTasks = store.getters['tasks:getFilteredTaskList']({ + type: 'todo', + filterType: 'complete2', + }); + + expect(returnedTasks[0]).to.eq(todos[1]); + }); + }); +}); diff --git a/website/client/components/tasks/column.vue b/website/client/components/tasks/column.vue index deda1bf34e..0197f2f6a8 100644 --- a/website/client/components/tasks/column.vue +++ b/website/client/components/tasks/column.vue @@ -8,14 +8,14 @@ v-if='type === "reward"') .d-flex h2.tasks-column-title - | {{ $t(types[type].label) }} + | {{ $t(typeLabel) }} .badge.badge-pill.badge-purple.column-badge(v-if="badgeCount > 0") {{ badgeCount }} .filters.d-flex.justify-content-end .filter.small-text( - v-for="filter in types[type].filters", - :class="{active: activeFilters[type].label === filter.label}", + v-for="filter in typeFilters", + :class="{active: activeFilter.label === filter}", @click="activateFilter(type, filter)", - ) {{ $t(filter.label) }} + ) {{ $t(filter) }} .tasks-list(ref="tasksWrapper") textarea.quick-add( :rows="quickAddRows", @@ -26,19 +26,19 @@ ) transition(name="quick-add-tip-slide") .quick-add-tip.small-text(v-show="quickAddFocused", v-html="$t('addMultipleTip')") - clear-completed-todos(v-if="activeFilters[type].label === 'complete2' && isUser === true") + clear-completed-todos(v-if="activeFilter.label === 'complete2' && isUser === true") .column-background( v-if="isUser === true", :class="{'initial-description': initialColumnDescription}", ref="columnBackground", ) .svg-icon(v-html="icons[type]", :class="`icon-${type}`", v-once) - h3(v-once) {{$t('theseAreYourTasks', {taskType: $t(types[type].label)})}} + h3(v-once) {{$t('theseAreYourTasks', {taskType: $t(typeLabel)})}} .small-text {{$t(`${type}sDesc`)}} draggable.sortable-tasks( ref="tasksList", @update='sorted', - :options='{disabled: activeFilters[type].label === "scheduled"}', + :options='{disabled: activeFilter.label === "scheduled"}', ) task( v-for="task in taskList", @@ -245,10 +245,10 @@