Commit 9c8f090c authored by Simon Knox's avatar Simon Knox

Merge branch 'fix-card-hightlighting-in-new-boards' into 'master'

Fix selected item not being highlighted in the new group boards

See merge request gitlab-org/gitlab!53113
parents 4fe894dc b64093f8
<script> <script>
import sidebarEventHub from '~/sidebar/event_hub'; import { mapActions, mapGetters, mapState } from 'vuex';
import eventHub from '../eventhub'; import IssueCardInner from './issue_card_inner.vue';
import boardsStore from '../stores/boards_store';
import BoardCardLayout from './board_card_layout.vue';
import BoardCardLayoutDeprecated from './board_card_layout_deprecated.vue';
export default { export default {
name: 'BoardsIssueCard', name: 'BoardCard',
components: { components: {
BoardCardLayout: gon.features?.graphqlBoardLists ? BoardCardLayout : BoardCardLayoutDeprecated, IssueCardInner,
}, },
props: { props: {
list: { list: {
...@@ -21,29 +18,41 @@ export default { ...@@ -21,29 +18,41 @@ export default {
default: () => ({}), default: () => ({}),
required: false, required: false,
}, },
disabled: {
type: Boolean,
default: false,
required: false,
},
index: {
type: Number,
default: 0,
required: false,
},
}, },
methods: { computed: {
// These are methods instead of computed's, because boardsStore is not reactive. ...mapState(['selectedBoardItems', 'activeId']),
...mapGetters(['isSwimlanesOn']),
isActive() { isActive() {
return this.getActiveId() === this.issue.id; return this.issue.id === this.activeId;
}, },
getActiveId() { multiSelectVisible() {
return boardsStore.detail?.issue?.id; return (
!this.activeId &&
this.selectedBoardItems.findIndex((boardItem) => boardItem.id === this.issue.id) > -1
);
}, },
showIssue({ isMultiSelect }) { },
// If no issues are opened, close all sidebars first methods: {
if (!this.getActiveId()) { ...mapActions(['toggleBoardItemMultiSelection', 'toggleBoardItem']),
sidebarEventHub.$emit('sidebar.closeAll'); toggleIssue(e) {
} // Don't do anything if this happened on a no trigger element
if (this.isActive()) { if (e.target.classList.contains('js-no-trigger')) return;
eventHub.$emit('clearDetailIssue', isMultiSelect);
if (isMultiSelect) { const isMultiSelect = e.ctrlKey || e.metaKey;
eventHub.$emit('newDetailIssue', this.issue, isMultiSelect); if (isMultiSelect) {
} this.toggleBoardItemMultiSelection(this.issue);
} else { } else {
eventHub.$emit('newDetailIssue', this.issue, isMultiSelect); this.toggleBoardItem({ boardItem: this.issue });
boardsStore.setListDetail(this.list);
} }
}, },
}, },
...@@ -51,12 +60,22 @@ export default { ...@@ -51,12 +60,22 @@ export default {
</script> </script>
<template> <template>
<board-card-layout <li
data-qa-selector="board_card" data-qa-selector="board_card"
:issue="issue" :class="{
:list="list" 'multi-select': multiSelectVisible,
:is-active="isActive()" 'user-can-drag': !disabled && issue.id,
v-bind="$attrs" 'is-disabled': disabled || !issue.id,
@show="showIssue" 'is-active': isActive,
/> }"
:index="index"
:data-issue-id="issue.id"
:data-issue-iid="issue.iid"
:data-issue-path="issue.referencePath"
data-testid="board_card"
class="board-card gl-p-5 gl-rounded-base"
@mouseup="toggleIssue($event)"
>
<issue-card-inner :list="list" :issue="issue" :update-filters="true" />
</li>
</template> </template>
<script>
// This component is being replaced in favor of './board_card.vue' for GraphQL boards
import sidebarEventHub from '~/sidebar/event_hub';
import eventHub from '../eventhub';
import boardsStore from '../stores/boards_store';
import BoardCardLayoutDeprecated from './board_card_layout_deprecated.vue';
export default {
components: {
BoardCardLayout: BoardCardLayoutDeprecated,
},
props: {
list: {
type: Object,
default: () => ({}),
required: false,
},
issue: {
type: Object,
default: () => ({}),
required: false,
},
},
methods: {
// These are methods instead of computed's, because boardsStore is not reactive.
isActive() {
return this.getActiveId() === this.issue.id;
},
getActiveId() {
return boardsStore.detail?.issue?.id;
},
showIssue({ isMultiSelect }) {
// If no issues are opened, close all sidebars first
if (!this.getActiveId()) {
sidebarEventHub.$emit('sidebar.closeAll');
}
if (this.isActive()) {
eventHub.$emit('clearDetailIssue', isMultiSelect);
if (isMultiSelect) {
eventHub.$emit('newDetailIssue', this.issue, isMultiSelect);
}
} else {
eventHub.$emit('newDetailIssue', this.issue, isMultiSelect);
boardsStore.setListDetail(this.list);
}
},
},
};
</script>
<template>
<board-card-layout
data-qa-selector="board_card"
:issue="issue"
:list="list"
:is-active="isActive()"
v-bind="$attrs"
@show="showIssue"
/>
</template>
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import { ISSUABLE } from '~/boards/constants';
import IssueCardInner from './issue_card_inner.vue';
export default {
name: 'BoardCardLayout',
components: {
IssueCardInner,
},
props: {
list: {
type: Object,
default: () => ({}),
required: false,
},
issue: {
type: Object,
default: () => ({}),
required: false,
},
disabled: {
type: Boolean,
default: false,
required: false,
},
index: {
type: Number,
default: 0,
required: false,
},
isActive: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
showDetail: false,
};
},
computed: {
...mapState(['selectedBoardItems']),
...mapGetters(['isSwimlanesOn']),
multiSelectVisible() {
return this.selectedBoardItems.findIndex((boardItem) => boardItem.id === this.issue.id) > -1;
},
},
methods: {
...mapActions(['setActiveId', 'toggleBoardItemMultiSelection']),
mouseDown() {
this.showDetail = true;
},
mouseMove() {
this.showDetail = false;
},
showIssue(e) {
// Don't do anything if this happened on a no trigger element
if (e.target.classList.contains('js-no-trigger')) return;
const isMultiSelect = e.ctrlKey || e.metaKey;
if (!isMultiSelect) {
this.setActiveId({ id: this.issue.id, sidebarType: ISSUABLE });
} else {
this.toggleBoardItemMultiSelection(this.issue);
}
if (this.showDetail || isMultiSelect) {
this.showDetail = false;
}
},
},
};
</script>
<template>
<li
:class="{
'multi-select': multiSelectVisible,
'user-can-drag': !disabled && issue.id,
'is-disabled': disabled || !issue.id,
'is-active': isActive,
}"
:index="index"
:data-issue-id="issue.id"
:data-issue-iid="issue.iid"
:data-issue-path="issue.referencePath"
data-testid="board_card"
class="board-card gl-p-5 gl-rounded-base"
@mousedown="mouseDown"
@mousemove="mouseMove"
@mouseup="showIssue($event)"
>
<issue-card-inner :list="list" :issue="issue" :update-filters="true" />
</li>
</template>
...@@ -230,7 +230,11 @@ export default { ...@@ -230,7 +230,11 @@ export default {
:disabled="disabled" :disabled="disabled"
/> />
<li v-if="showCount" class="board-list-count gl-text-center" data-issue-id="-1"> <li v-if="showCount" class="board-list-count gl-text-center" data-issue-id="-1">
<gl-loading-icon v-if="loadingMore" :label="$options.i18n.loadingMoreissues" /> <gl-loading-icon
v-if="loadingMore"
:label="$options.i18n.loadingMoreissues"
data-testid="count-loading-icon"
/>
<span v-if="showingAllIssues">{{ $options.i18n.showingAllIssues }}</span> <span v-if="showingAllIssues">{{ $options.i18n.showingAllIssues }}</span>
<span v-else>{{ paginatedIssueText }}</span> <span v-else>{{ paginatedIssueText }}</span>
</li> </li>
......
...@@ -11,7 +11,7 @@ import { ...@@ -11,7 +11,7 @@ import {
sortableEnd, sortableEnd,
} from '../mixins/sortable_default_options'; } from '../mixins/sortable_default_options';
import boardsStore from '../stores/boards_store'; import boardsStore from '../stores/boards_store';
import boardCard from './board_card.vue'; import boardCard from './board_card_deprecated.vue';
import boardNewIssue from './board_new_issue_deprecated.vue'; import boardNewIssue from './board_new_issue_deprecated.vue';
// This component is being replaced in favor of './board_list.vue' for GraphQL boards // This component is being replaced in favor of './board_list.vue' for GraphQL boards
......
import { pick } from 'lodash'; import { pick } from 'lodash';
import boardListsQuery from 'ee_else_ce/boards/graphql/board_lists.query.graphql'; import boardListsQuery from 'ee_else_ce/boards/graphql/board_lists.query.graphql';
import { BoardType, ListType, inactiveId, flashAnimationDuration } from '~/boards/constants'; import {
BoardType,
ListType,
inactiveId,
flashAnimationDuration,
ISSUABLE,
} from '~/boards/constants';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import createGqClient, { fetchPolicies } from '~/lib/graphql'; import createGqClient, { fetchPolicies } from '~/lib/graphql';
import { convertObjectPropsToCamelCase, urlParamsToObject } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase, urlParamsToObject } from '~/lib/utils/common_utils';
...@@ -536,10 +542,17 @@ export default { ...@@ -536,10 +542,17 @@ export default {
commit(types.SET_SELECTED_PROJECT, project); commit(types.SET_SELECTED_PROJECT, project);
}, },
toggleBoardItemMultiSelection: ({ commit, state }, boardItem) => { toggleBoardItemMultiSelection: ({ commit, state, dispatch, getters }, boardItem) => {
const { selectedBoardItems } = state; const { selectedBoardItems } = state;
const index = selectedBoardItems.indexOf(boardItem); const index = selectedBoardItems.indexOf(boardItem);
// If user already selected an item (activeIssue) without using mult-select,
// include that item in the selection and unset state.ActiveId to hide the sidebar.
if (getters.activeIssue) {
commit(types.ADD_BOARD_ITEM_TO_SELECTION, getters.activeIssue);
dispatch('unsetActiveId');
}
if (index === -1) { if (index === -1) {
commit(types.ADD_BOARD_ITEM_TO_SELECTION, boardItem); commit(types.ADD_BOARD_ITEM_TO_SELECTION, boardItem);
} else { } else {
...@@ -551,6 +564,20 @@ export default { ...@@ -551,6 +564,20 @@ export default {
commit(types.SET_ADD_COLUMN_FORM_VISIBLE, visible); commit(types.SET_ADD_COLUMN_FORM_VISIBLE, visible);
}, },
resetBoardItemMultiSelection: ({ commit }) => {
commit(types.RESET_BOARD_ITEM_SELECTION);
},
toggleBoardItem: ({ state, dispatch }, { boardItem, sidebarType = ISSUABLE }) => {
dispatch('resetBoardItemMultiSelection');
if (boardItem.id === state.activeId) {
dispatch('unsetActiveId');
} else {
dispatch('setActiveId', { id: boardItem.id, sidebarType });
}
},
fetchBacklog: () => { fetchBacklog: () => {
notImplemented(); notImplemented();
}, },
......
...@@ -45,3 +45,4 @@ export const REMOVE_BOARD_ITEM_FROM_SELECTION = 'REMOVE_BOARD_ITEM_FROM_SELECTIO ...@@ -45,3 +45,4 @@ export const REMOVE_BOARD_ITEM_FROM_SELECTION = 'REMOVE_BOARD_ITEM_FROM_SELECTIO
export const SET_ADD_COLUMN_FORM_VISIBLE = 'SET_ADD_COLUMN_FORM_VISIBLE'; export const SET_ADD_COLUMN_FORM_VISIBLE = 'SET_ADD_COLUMN_FORM_VISIBLE';
export const ADD_LIST_TO_HIGHLIGHTED_LISTS = 'ADD_LIST_TO_HIGHLIGHTED_LISTS'; export const ADD_LIST_TO_HIGHLIGHTED_LISTS = 'ADD_LIST_TO_HIGHLIGHTED_LISTS';
export const REMOVE_LIST_FROM_HIGHLIGHTED_LISTS = 'REMOVE_LIST_FROM_HIGHLIGHTED_LISTS'; export const REMOVE_LIST_FROM_HIGHLIGHTED_LISTS = 'REMOVE_LIST_FROM_HIGHLIGHTED_LISTS';
export const RESET_BOARD_ITEM_SELECTION = 'RESET_BOARD_ITEM_SELECTION';
...@@ -111,6 +111,7 @@ export default { ...@@ -111,6 +111,7 @@ export default {
[mutationTypes.RECEIVE_ITEMS_FOR_LIST_SUCCESS]: (state, { listIssues, listPageInfo, listId }) => { [mutationTypes.RECEIVE_ITEMS_FOR_LIST_SUCCESS]: (state, { listIssues, listPageInfo, listId }) => {
const { listData, issues } = listIssues; const { listData, issues } = listIssues;
Vue.set(state, 'issues', { ...state.issues, ...issues }); Vue.set(state, 'issues', { ...state.issues, ...issues });
Vue.set( Vue.set(
state.issuesByListId, state.issuesByListId,
listId, listId,
...@@ -280,4 +281,8 @@ export default { ...@@ -280,4 +281,8 @@ export default {
[mutationTypes.REMOVE_LIST_FROM_HIGHLIGHTED_LISTS]: (state, listId) => { [mutationTypes.REMOVE_LIST_FROM_HIGHLIGHTED_LISTS]: (state, listId) => {
state.highlightedLists = state.highlightedLists.filter((id) => id !== listId); state.highlightedLists = state.highlightedLists.filter((id) => id !== listId);
}, },
[mutationTypes.RESET_BOARD_ITEM_SELECTION]: (state) => {
state.selectedBoardItems = [];
},
}; };
...@@ -34,16 +34,22 @@ export default { ...@@ -34,16 +34,22 @@ export default {
computed: { computed: {
...mapGetters(['isSidebarOpen', 'activeIssue']), ...mapGetters(['isSidebarOpen', 'activeIssue']),
...mapState(['sidebarType']), ...mapState(['sidebarType']),
showSidebar() { isIssuableSidebar() {
return this.sidebarType === ISSUABLE; return this.sidebarType === ISSUABLE;
}, },
showSidebar() {
return this.isIssuableSidebar && this.isSidebarOpen;
},
}, },
methods: { methods: {
...mapActions(['unsetActiveId', 'setAssignees']), ...mapActions(['toggleBoardItem', 'setAssignees']),
updateAssignees(data) { updateAssignees(data) {
const assignees = data.issueSetAssignees?.issue?.assignees?.nodes || []; const assignees = data.issueSetAssignees?.issue?.assignees?.nodes || [];
this.setAssignees(assignees); this.setAssignees(assignees);
}, },
handleClose() {
this.toggleBoardItem({ boardItem: this.activeIssue, sidebarType: this.sidebarType });
},
}, },
}; };
</script> </script>
...@@ -51,9 +57,10 @@ export default { ...@@ -51,9 +57,10 @@ export default {
<template> <template>
<gl-drawer <gl-drawer
v-if="showSidebar" v-if="showSidebar"
data-testid="sidebar-drawer"
:open="isSidebarOpen" :open="isSidebarOpen"
:header-height="$options.headerHeight" :header-height="$options.headerHeight"
@close="unsetActiveId" @close="handleClose"
> >
<template #header>{{ __('Issue details') }}</template> <template #header>{{ __('Issue details') }}</template>
<template #default> <template #default>
......
...@@ -2,14 +2,14 @@ ...@@ -2,14 +2,14 @@
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
import Draggable from 'vuedraggable'; import Draggable from 'vuedraggable';
import { mapState, mapActions } from 'vuex'; import { mapState, mapActions } from 'vuex';
import BoardCardLayout from '~/boards/components/board_card_layout.vue'; import BoardCard from '~/boards/components/board_card.vue';
import BoardNewIssue from '~/boards/components/board_new_issue.vue'; import BoardNewIssue from '~/boards/components/board_new_issue.vue';
import eventHub from '~/boards/eventhub'; import eventHub from '~/boards/eventhub';
import defaultSortableConfig from '~/sortable/sortable_config'; import defaultSortableConfig from '~/sortable/sortable_config';
export default { export default {
components: { components: {
BoardCardLayout, BoardCard,
BoardNewIssue, BoardNewIssue,
GlLoadingIcon, GlLoadingIcon,
}, },
...@@ -181,7 +181,7 @@ export default { ...@@ -181,7 +181,7 @@ export default {
@start="handleDragOnStart" @start="handleDragOnStart"
@end="handleDragOnEnd" @end="handleDragOnEnd"
> >
<board-card-layout <board-card
v-for="(issue, index) in issues" v-for="(issue, index) in issues"
ref="issue" ref="issue"
:key="issue.id" :key="issue.id"
...@@ -189,7 +189,6 @@ export default { ...@@ -189,7 +189,6 @@ export default {
:list="list" :list="list"
:issue="issue" :issue="issue"
:disabled="disabled || !canAdminEpic" :disabled="disabled || !canAdminEpic"
:is-active="isActiveIssue(issue)"
/> />
<gl-loading-icon v-if="isLoadingMore && isUnassignedIssuesLane" size="sm" class="gl-py-3" /> <gl-loading-icon v-if="isLoadingMore && isUnassignedIssuesLane" size="sm" class="gl-py-3" />
</component> </component>
......
...@@ -25,9 +25,24 @@ RSpec.describe 'epics swimlanes sidebar', :js do ...@@ -25,9 +25,24 @@ RSpec.describe 'epics swimlanes sidebar', :js do
wait_for_requests wait_for_requests
end end
context 'when closing sidebar' do
let(:issue_card) { first("[data-testid='board-epic-lane-issues'] [data-testid='board_card']") }
it 'unhighlights the active issue card' do
load_epic_swimlanes
issue_card.click
find("[data-testid='sidebar-drawer'] .gl-drawer-close-button").click
expect(issue_card[:class]).not_to include('is-active')
expect(issue_card[:class]).not_to include('multi-select')
end
end
context 'notifications subscription' do context 'notifications subscription' do
it 'displays notifications toggle' do it 'displays notifications toggle' do
load_epic_boards load_epic_swimlanes
click_first_issue_card click_first_issue_card
page.within('[data-testid="sidebar-notifications"]') do page.within('[data-testid="sidebar-notifications"]') do
...@@ -38,7 +53,7 @@ RSpec.describe 'epics swimlanes sidebar', :js do ...@@ -38,7 +53,7 @@ RSpec.describe 'epics swimlanes sidebar', :js do
end end
it 'shows toggle as on then as off as user toggles to subscribe and unsubscribe' do it 'shows toggle as on then as off as user toggles to subscribe and unsubscribe' do
load_epic_boards load_epic_swimlanes
click_first_issue_card click_first_issue_card
toggle = find('[data-testid="notification-subscribe-toggle"]') toggle = find('[data-testid="notification-subscribe-toggle"]')
...@@ -56,7 +71,7 @@ RSpec.describe 'epics swimlanes sidebar', :js do ...@@ -56,7 +71,7 @@ RSpec.describe 'epics swimlanes sidebar', :js do
before do before do
project.update_attribute(:emails_disabled, true) project.update_attribute(:emails_disabled, true)
load_epic_boards load_epic_swimlanes
end end
it 'displays a message that notifications have been disabled' do it 'displays a message that notifications have been disabled' do
...@@ -72,7 +87,7 @@ RSpec.describe 'epics swimlanes sidebar', :js do ...@@ -72,7 +87,7 @@ RSpec.describe 'epics swimlanes sidebar', :js do
context 'time tracking' do context 'time tracking' do
it 'displays time tracking feature with default message' do it 'displays time tracking feature with default message' do
load_epic_boards load_epic_swimlanes
click_first_issue_card click_first_issue_card
page.within('[data-testid="time-tracker"]') do page.within('[data-testid="time-tracker"]') do
...@@ -85,7 +100,7 @@ RSpec.describe 'epics swimlanes sidebar', :js do ...@@ -85,7 +100,7 @@ RSpec.describe 'epics swimlanes sidebar', :js do
before do before do
issue1.timelogs.create!(time_spent: 3600, user: user) issue1.timelogs.create!(time_spent: 3600, user: user)
load_epic_boards load_epic_swimlanes
click_first_issue_card click_first_issue_card
end end
...@@ -102,7 +117,7 @@ RSpec.describe 'epics swimlanes sidebar', :js do ...@@ -102,7 +117,7 @@ RSpec.describe 'epics swimlanes sidebar', :js do
before do before do
issue1.update!(time_estimate: 3600) issue1.update!(time_estimate: 3600)
load_epic_boards load_epic_swimlanes
click_first_issue_card click_first_issue_card
end end
...@@ -120,7 +135,7 @@ RSpec.describe 'epics swimlanes sidebar', :js do ...@@ -120,7 +135,7 @@ RSpec.describe 'epics swimlanes sidebar', :js do
issue1.update!(time_estimate: 3600) issue1.update!(time_estimate: 3600)
issue1.timelogs.create!(time_spent: 1800, user: user) issue1.timelogs.create!(time_spent: 1800, user: user)
load_epic_boards load_epic_swimlanes
click_first_issue_card click_first_issue_card
end end
...@@ -146,7 +161,7 @@ RSpec.describe 'epics swimlanes sidebar', :js do ...@@ -146,7 +161,7 @@ RSpec.describe 'epics swimlanes sidebar', :js do
# 3600+3600*24 = 1d 1h or 25h # 3600+3600*24 = 1d 1h or 25h
issue1.timelogs.create!(time_spent: 3600 + 3600 * 24, user: user) issue1.timelogs.create!(time_spent: 3600 + 3600 * 24, user: user)
load_epic_boards load_epic_swimlanes
click_first_issue_card click_first_issue_card
end end
...@@ -165,7 +180,7 @@ RSpec.describe 'epics swimlanes sidebar', :js do ...@@ -165,7 +180,7 @@ RSpec.describe 'epics swimlanes sidebar', :js do
end end
end end
def load_epic_boards def load_epic_swimlanes
page.within('.board-swimlanes-toggle-wrapper') do page.within('.board-swimlanes-toggle-wrapper') do
page.find('.dropdown-toggle').click page.find('.dropdown-toggle').click
page.find('.dropdown-item', text: 'Epic').click page.find('.dropdown-item', text: 'Epic').click
......
...@@ -76,6 +76,28 @@ RSpec.describe 'epics swimlanes', :js do ...@@ -76,6 +76,28 @@ RSpec.describe 'epics swimlanes', :js do
end end
end end
context 'issue cards' do
let(:issue_card) { first("[data-testid='board-epic-lane-issues'] [data-testid='board_card']") }
before do
wait_for_all_requests
issue_card.click
end
it 'highlights an issue card on click' do
expect(issue_card[:class]).to include('is-active')
expect(issue_card[:class]).not_to include('multi-select')
end
it 'unhighlights a selected issue card on click' do
issue_card.click
expect(issue_card[:class]).not_to include('is-active')
expect(issue_card[:class]).not_to include('multi-select')
end
end
context 'add issue to swimlanes list' do context 'add issue to swimlanes list' do
it 'displays new issue button' do it 'displays new issue button' do
expect(first('.board')).to have_selector('.issue-count-badge-add-button', count: 1) expect(first('.board')).to have_selector('.issue-count-badge-add-button', count: 1)
......
import { GlDrawer } from '@gitlab/ui'; import { GlDrawer } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import BoardContentSidebar from 'ee_component/boards/components/board_content_sidebar.vue'; import BoardContentSidebar from 'ee_component/boards/components/board_content_sidebar.vue';
import BoardSidebarIterationSelect from 'ee_component/boards/components/sidebar/board_sidebar_iteration_select.vue'; import BoardSidebarIterationSelect from 'ee_component/boards/components/sidebar/board_sidebar_iteration_select.vue';
import { stubComponent } from 'helpers/stub_component'; import { stubComponent } from 'helpers/stub_component';
import waitForPromises from 'helpers/wait_for_promises';
import BoardSidebarDueDate from '~/boards/components/sidebar/board_sidebar_due_date.vue'; import BoardSidebarDueDate from '~/boards/components/sidebar/board_sidebar_due_date.vue';
import BoardSidebarIssueTitle from '~/boards/components/sidebar/board_sidebar_issue_title.vue'; import BoardSidebarIssueTitle from '~/boards/components/sidebar/board_sidebar_issue_title.vue';
import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue'; import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue';
import BoardSidebarMilestoneSelect from '~/boards/components/sidebar/board_sidebar_milestone_select.vue'; import BoardSidebarMilestoneSelect from '~/boards/components/sidebar/board_sidebar_milestone_select.vue';
import BoardSidebarSubscription from '~/boards/components/sidebar/board_sidebar_subscription.vue'; import BoardSidebarSubscription from '~/boards/components/sidebar/board_sidebar_subscription.vue';
import { ISSUABLE } from '~/boards/constants'; import { ISSUABLE } from '~/boards/constants';
import { createStore } from '~/boards/stores'; import { mockIssue } from '../mock_data';
describe('ee/BoardContentSidebar', () => { describe('ee/BoardContentSidebar', () => {
let wrapper; let wrapper;
let store; let store;
const createStore = ({ mockGetters = {}, mockActions = {} } = {}) => {
store = new Vuex.Store({
state: {
sidebarType: ISSUABLE,
issues: { [mockIssue.id]: mockIssue },
activeId: mockIssue.id,
},
getters: {
activeIssue: () => mockIssue,
isSidebarOpen: () => true,
...mockGetters,
},
actions: mockActions,
});
};
const createComponent = () => { const createComponent = () => {
wrapper = shallowMount(BoardContentSidebar, { wrapper = shallowMount(BoardContentSidebar, {
provide: { provide: {
...@@ -42,12 +58,7 @@ describe('ee/BoardContentSidebar', () => { ...@@ -42,12 +58,7 @@ describe('ee/BoardContentSidebar', () => {
}; };
beforeEach(() => { beforeEach(() => {
store = createStore(); createStore();
store.state.sidebarType = ISSUABLE;
store.state.issues = { 1: { title: 'One', referencePath: 'path#2', assignees: [], iid: '2' } };
store.state.activeIssue = { title: 'One', referencePath: 'path#2', assignees: [], iid: '2' };
store.state.activeId = '1';
createComponent(); createComponent();
}); });
...@@ -60,6 +71,13 @@ describe('ee/BoardContentSidebar', () => { ...@@ -60,6 +71,13 @@ describe('ee/BoardContentSidebar', () => {
expect(wrapper.find(GlDrawer).exists()).toBe(true); expect(wrapper.find(GlDrawer).exists()).toBe(true);
}); });
it('does not render GlDrawer when isSidebarOpen is false', () => {
createStore({ mockGetters: { isSidebarOpen: () => false } });
createComponent();
expect(wrapper.find(GlDrawer).exists()).toBe(false);
});
it('applies an open attribute', () => { it('applies an open attribute', () => {
expect(wrapper.find(GlDrawer).props('open')).toBe(true); expect(wrapper.find(GlDrawer).props('open')).toBe(true);
}); });
...@@ -89,14 +107,22 @@ describe('ee/BoardContentSidebar', () => { ...@@ -89,14 +107,22 @@ describe('ee/BoardContentSidebar', () => {
}); });
describe('when we emit close', () => { describe('when we emit close', () => {
it('hides GlDrawer', async () => { let toggleBoardItem;
expect(wrapper.find(GlDrawer).props('open')).toBe(true);
wrapper.find(GlDrawer).vm.$emit('close'); beforeEach(() => {
toggleBoardItem = jest.fn();
createStore({ mockActions: { toggleBoardItem } });
createComponent();
});
await waitForPromises(); it('calls toggleBoardItem with correct parameters', async () => {
wrapper.find(GlDrawer).vm.$emit('close');
expect(wrapper.find(GlDrawer).exists()).toBe(false); expect(toggleBoardItem).toHaveBeenCalledTimes(1);
expect(toggleBoardItem).toHaveBeenCalledWith(expect.any(Object), {
boardItem: mockIssue,
sidebarType: ISSUABLE,
});
}); });
}); });
}); });
...@@ -2,7 +2,7 @@ import { shallowMount } from '@vue/test-utils'; ...@@ -2,7 +2,7 @@ import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import IssuesLaneList from 'ee/boards/components/issues_lane_list.vue'; import IssuesLaneList from 'ee/boards/components/issues_lane_list.vue';
import { mockList } from 'jest/boards/mock_data'; import { mockList } from 'jest/boards/mock_data';
import BoardCard from '~/boards/components/board_card_layout.vue'; import BoardCard from '~/boards/components/board_card.vue';
import { ListType } from '~/boards/constants'; import { ListType } from '~/boards/constants';
import { createStore } from '~/boards/stores'; import { createStore } from '~/boards/stores';
import { mockIssues } from '../mock_data'; import { mockIssues } from '../mock_data';
......
...@@ -96,7 +96,7 @@ export const rawIssue = { ...@@ -96,7 +96,7 @@ export const rawIssue = {
export const mockIssue = { export const mockIssue = {
id: '436', id: '436',
iid: 27, iid: '27',
title: 'Issue 1', title: 'Issue 1',
referencePath: '#27', referencePath: '#27',
dueDate: null, dueDate: null,
......
import { createLocalVue, mount } from '@vue/test-utils'; import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex'; import Vuex from 'vuex';
import { useFakeRequestAnimationFrame } from 'helpers/fake_request_animation_frame'; import { useFakeRequestAnimationFrame } from 'helpers/fake_request_animation_frame';
import BoardCard from '~/boards/components/board_card.vue'; import BoardCard from '~/boards/components/board_card.vue';
import BoardList from '~/boards/components/board_list.vue'; import BoardList from '~/boards/components/board_list.vue';
import BoardNewIssue from '~/boards/components/board_new_issue.vue';
import eventHub from '~/boards/eventhub'; import eventHub from '~/boards/eventhub';
import defaultState from '~/boards/stores/state'; import defaultState from '~/boards/stores/state';
import { mockList, mockIssuesByListId, issues, mockIssues } from './mock_data'; import { mockList, mockIssuesByListId, issues, mockIssues } from './mock_data';
...@@ -38,6 +39,7 @@ const createComponent = ({ ...@@ -38,6 +39,7 @@ const createComponent = ({
'gid://gitlab/List/1': {}, 'gid://gitlab/List/1': {},
'gid://gitlab/List/2': {}, 'gid://gitlab/List/2': {},
}, },
selectedBoardItems: [],
...state, ...state,
}); });
...@@ -58,7 +60,7 @@ const createComponent = ({ ...@@ -58,7 +60,7 @@ const createComponent = ({
list.issuesCount = 1; list.issuesCount = 1;
} }
const component = mount(BoardList, { const component = shallowMount(BoardList, {
localVue, localVue,
propsData: { propsData: {
disabled: false, disabled: false,
...@@ -74,6 +76,10 @@ const createComponent = ({ ...@@ -74,6 +76,10 @@ const createComponent = ({
weightFeatureAvailable: false, weightFeatureAvailable: false,
boardWeight: null, boardWeight: null,
}, },
stubs: {
BoardCard,
BoardNewIssue,
},
}); });
return component; return component;
...@@ -81,7 +87,10 @@ const createComponent = ({ ...@@ -81,7 +87,10 @@ const createComponent = ({
describe('Board list component', () => { describe('Board list component', () => {
let wrapper; let wrapper;
const findByTestId = (testId) => wrapper.find(`[data-testid="${testId}"]`); const findByTestId = (testId) => wrapper.find(`[data-testid="${testId}"]`);
const findIssueCountLoadingIcon = () => wrapper.find('[data-testid="count-loading-icon"]');
useFakeRequestAnimationFrame(); useFakeRequestAnimationFrame();
afterEach(() => { afterEach(() => {
...@@ -189,7 +198,8 @@ describe('Board list component', () => { ...@@ -189,7 +198,8 @@ describe('Board list component', () => {
wrapper.vm.showCount = true; wrapper.vm.showCount = true;
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
expect(wrapper.find('.board-list-count .gl-spinner').exists()).toBe(true);
expect(findIssueCountLoadingIcon().exists()).toBe(true);
}); });
}); });
......
/* global List */
/* global ListAssignee */
/* global ListLabel */
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import BoardCardDeprecated from '~/boards/components/board_card_deprecated.vue';
import issueCardInner from '~/boards/components/issue_card_inner.vue';
import eventHub from '~/boards/eventhub';
import store from '~/boards/stores';
import boardsStore from '~/boards/stores/boards_store';
import axios from '~/lib/utils/axios_utils';
import sidebarEventHub from '~/sidebar/event_hub';
import '~/boards/models/label';
import '~/boards/models/assignee';
import '~/boards/models/list';
import userAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import { listObj, boardsMockInterceptor, setMockEndpoints } from '../mock_data';
describe('BoardCard', () => {
let wrapper;
let mock;
let list;
const findIssueCardInner = () => wrapper.find(issueCardInner);
const findUserAvatarLink = () => wrapper.find(userAvatarLink);
// this particular mount component needs to be used after the root beforeEach because it depends on list being initialized
const mountComponent = (propsData) => {
wrapper = mount(BoardCardDeprecated, {
stubs: {
issueCardInner,
},
store,
propsData: {
list,
issue: list.issues[0],
disabled: false,
index: 0,
...propsData,
},
provide: {
groupId: null,
rootPath: '/',
scopedLabelsAvailable: false,
},
});
};
const setupData = async () => {
list = new List(listObj);
boardsStore.create();
boardsStore.detail.issue = {};
const label1 = new ListLabel({
id: 3,
title: 'testing 123',
color: '#000cff',
text_color: 'white',
description: 'test',
});
await waitForPromises();
list.issues[0].labels.push(label1);
};
beforeEach(() => {
mock = new MockAdapter(axios);
mock.onAny().reply(boardsMockInterceptor);
setMockEndpoints();
return setupData();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
list = null;
mock.restore();
});
it('when details issue is empty does not show the element', () => {
mountComponent();
expect(wrapper.find('[data-testid="board_card"').classes()).not.toContain('is-active');
});
it('when detailIssue is equal to card issue shows the element', () => {
[boardsStore.detail.issue] = list.issues;
mountComponent();
expect(wrapper.classes()).toContain('is-active');
});
it('when multiSelect does not contain issue removes multi select class', () => {
mountComponent();
expect(wrapper.classes()).not.toContain('multi-select');
});
it('when multiSelect contain issue add multi select class', () => {
boardsStore.multiSelect.list = [list.issues[0]];
mountComponent();
expect(wrapper.classes()).toContain('multi-select');
});
it('adds user-can-drag class if not disabled', () => {
mountComponent();
expect(wrapper.classes()).toContain('user-can-drag');
});
it('does not add user-can-drag class disabled', () => {
mountComponent({ disabled: true });
expect(wrapper.classes()).not.toContain('user-can-drag');
});
it('does not add disabled class', () => {
mountComponent();
expect(wrapper.classes()).not.toContain('is-disabled');
});
it('adds disabled class is disabled is true', () => {
mountComponent({ disabled: true });
expect(wrapper.classes()).toContain('is-disabled');
});
describe('mouse events', () => {
it('does not set detail issue if showDetail is false', () => {
mountComponent();
expect(boardsStore.detail.issue).toEqual({});
});
it('does not set detail issue if link is clicked', () => {
mountComponent();
findIssueCardInner().find('a').trigger('mouseup');
expect(boardsStore.detail.issue).toEqual({});
});
it('does not set detail issue if img is clicked', () => {
mountComponent({
issue: {
...list.issues[0],
assignees: [
new ListAssignee({
id: 1,
name: 'testing 123',
username: 'test',
avatar: 'test_image',
}),
],
},
});
findUserAvatarLink().trigger('mouseup');
expect(boardsStore.detail.issue).toEqual({});
});
it('does not set detail issue if showDetail is false after mouseup', () => {
mountComponent();
wrapper.trigger('mouseup');
expect(boardsStore.detail.issue).toEqual({});
});
it('sets detail issue to card issue on mouse up', () => {
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
mountComponent();
wrapper.trigger('mousedown');
wrapper.trigger('mouseup');
expect(eventHub.$emit).toHaveBeenCalledWith('newDetailIssue', wrapper.vm.issue, false);
expect(boardsStore.detail.list).toEqual(wrapper.vm.list);
});
it('resets detail issue to empty if already set', () => {
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
const [issue] = list.issues;
boardsStore.detail.issue = issue;
mountComponent();
wrapper.trigger('mousedown');
wrapper.trigger('mouseup');
expect(eventHub.$emit).toHaveBeenCalledWith('clearDetailIssue', false);
});
});
describe('sidebarHub events', () => {
it('closes all sidebars before showing an issue if no issues are opened', () => {
jest.spyOn(sidebarEventHub, '$emit').mockImplementation(() => {});
boardsStore.detail.issue = {};
mountComponent();
// sets conditional so that event is emitted.
wrapper.trigger('mousedown');
wrapper.trigger('mouseup');
expect(sidebarEventHub.$emit).toHaveBeenCalledWith('sidebar.closeAll');
});
it('it does not closes all sidebars before showing an issue if an issue is opened', () => {
jest.spyOn(sidebarEventHub, '$emit').mockImplementation(() => {});
const [issue] = list.issues;
boardsStore.detail.issue = issue;
mountComponent();
wrapper.trigger('mousedown');
expect(sidebarEventHub.$emit).not.toHaveBeenCalledWith('sidebar.closeAll');
});
});
});
import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import BoardCardLayout from '~/boards/components/board_card_layout.vue';
import IssueCardInner from '~/boards/components/issue_card_inner.vue';
import { ISSUABLE } from '~/boards/constants';
import defaultState from '~/boards/stores/state';
import { mockLabelList, mockIssue } from '../mock_data';
describe('Board card layout', () => {
let wrapper;
let store;
const localVue = createLocalVue();
localVue.use(Vuex);
const createStore = ({ getters = {}, actions = {} } = {}) => {
store = new Vuex.Store({
state: defaultState,
actions,
getters,
});
};
// this particular mount component needs to be used after the root beforeEach because it depends on list being initialized
const mountComponent = ({ propsData = {}, provide = {} } = {}) => {
wrapper = shallowMount(BoardCardLayout, {
localVue,
stubs: {
IssueCardInner,
},
store,
propsData: {
list: mockLabelList,
issue: mockIssue,
disabled: false,
index: 0,
...propsData,
},
provide: {
groupId: null,
rootPath: '/',
scopedLabelsAvailable: false,
...provide,
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('mouse events', () => {
it('sets showDetail to true on mousedown', async () => {
createStore();
mountComponent();
wrapper.trigger('mousedown');
await wrapper.vm.$nextTick();
expect(wrapper.vm.showDetail).toBe(true);
});
it('sets showDetail to false on mousemove', async () => {
createStore();
mountComponent();
wrapper.trigger('mousedown');
await wrapper.vm.$nextTick();
expect(wrapper.vm.showDetail).toBe(true);
wrapper.trigger('mousemove');
await wrapper.vm.$nextTick();
expect(wrapper.vm.showDetail).toBe(false);
});
it("calls 'setActiveId'", async () => {
const setActiveId = jest.fn();
createStore({
actions: {
setActiveId,
},
});
mountComponent();
wrapper.trigger('mouseup');
await wrapper.vm.$nextTick();
expect(setActiveId).toHaveBeenCalledTimes(1);
expect(setActiveId).toHaveBeenCalledWith(expect.any(Object), {
id: mockIssue.id,
sidebarType: ISSUABLE,
});
});
it("calls 'setActiveId' when epic swimlanes is active", async () => {
const setActiveId = jest.fn();
const isSwimlanesOn = () => true;
createStore({
getters: { isSwimlanesOn },
actions: {
setActiveId,
},
});
mountComponent();
wrapper.trigger('mouseup');
await wrapper.vm.$nextTick();
expect(setActiveId).toHaveBeenCalledTimes(1);
expect(setActiveId).toHaveBeenCalledWith(expect.any(Object), {
id: mockIssue.id,
sidebarType: ISSUABLE,
});
});
});
});
/* global List */ import { createLocalVue, shallowMount } from '@vue/test-utils';
/* global ListAssignee */ import Vuex from 'vuex';
/* global ListLabel */
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import BoardCard from '~/boards/components/board_card.vue'; import BoardCard from '~/boards/components/board_card.vue';
import issueCardInner from '~/boards/components/issue_card_inner.vue'; import IssueCardInner from '~/boards/components/issue_card_inner.vue';
import eventHub from '~/boards/eventhub'; import { inactiveId } from '~/boards/constants';
import store from '~/boards/stores'; import { mockLabelList, mockIssue } from '../mock_data';
import boardsStore from '~/boards/stores/boards_store';
import axios from '~/lib/utils/axios_utils';
import sidebarEventHub from '~/sidebar/event_hub';
import '~/boards/models/label';
import '~/boards/models/assignee';
import '~/boards/models/list';
import userAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import { listObj, boardsMockInterceptor, setMockEndpoints } from '../mock_data';
describe('BoardCard', () => {
let wrapper;
let mock;
let list;
const findIssueCardInner = () => wrapper.find(issueCardInner); describe('Board card layout', () => {
const findUserAvatarLink = () => wrapper.find(userAvatarLink); let wrapper;
let store;
let mockActions;
const localVue = createLocalVue();
localVue.use(Vuex);
const createStore = ({ initialState = {}, isSwimlanesOn = false } = {}) => {
mockActions = {
toggleBoardItem: jest.fn(),
toggleBoardItemMultiSelection: jest.fn(),
};
store = new Vuex.Store({
state: {
activeId: inactiveId,
selectedBoardItems: [],
...initialState,
},
actions: mockActions,
getters: {
isSwimlanesOn: () => isSwimlanesOn,
},
});
};
// this particular mount component needs to be used after the root beforeEach because it depends on list being initialized // this particular mount component needs to be used after the root beforeEach because it depends on list being initialized
const mountComponent = (propsData) => { const mountComponent = ({ propsData = {}, provide = {} } = {}) => {
wrapper = mount(BoardCard, { wrapper = shallowMount(BoardCard, {
localVue,
stubs: { stubs: {
issueCardInner, IssueCardInner,
}, },
store, store,
propsData: { propsData: {
list, list: mockLabelList,
issue: list.issues[0], issue: mockIssue,
disabled: false, disabled: false,
index: 0, index: 0,
...propsData, ...propsData,
...@@ -46,174 +52,94 @@ describe('BoardCard', () => { ...@@ -46,174 +52,94 @@ describe('BoardCard', () => {
groupId: null, groupId: null,
rootPath: '/', rootPath: '/',
scopedLabelsAvailable: false, scopedLabelsAvailable: false,
...provide,
}, },
}); });
}; };
const setupData = async () => { const selectCard = async () => {
list = new List(listObj); wrapper.trigger('mouseup');
boardsStore.create(); await wrapper.vm.$nextTick();
boardsStore.detail.issue = {};
const label1 = new ListLabel({
id: 3,
title: 'testing 123',
color: '#000cff',
text_color: 'white',
description: 'test',
});
await waitForPromises();
list.issues[0].labels.push(label1);
}; };
beforeEach(() => { const multiSelectCard = async () => {
mock = new MockAdapter(axios); wrapper.trigger('mouseup', { ctrlKey: true });
mock.onAny().reply(boardsMockInterceptor); await wrapper.vm.$nextTick();
setMockEndpoints(); };
return setupData();
});
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null; wrapper = null;
list = null; store = null;
mock.restore();
});
it('when details issue is empty does not show the element', () => {
mountComponent();
expect(wrapper.find('[data-testid="board_card"').classes()).not.toContain('is-active');
});
it('when detailIssue is equal to card issue shows the element', () => {
[boardsStore.detail.issue] = list.issues;
mountComponent();
expect(wrapper.classes()).toContain('is-active');
});
it('when multiSelect does not contain issue removes multi select class', () => {
mountComponent();
expect(wrapper.classes()).not.toContain('multi-select');
});
it('when multiSelect contain issue add multi select class', () => {
boardsStore.multiSelect.list = [list.issues[0]];
mountComponent();
expect(wrapper.classes()).toContain('multi-select');
});
it('adds user-can-drag class if not disabled', () => {
mountComponent();
expect(wrapper.classes()).toContain('user-can-drag');
});
it('does not add user-can-drag class disabled', () => {
mountComponent({ disabled: true });
expect(wrapper.classes()).not.toContain('user-can-drag');
});
it('does not add disabled class', () => {
mountComponent();
expect(wrapper.classes()).not.toContain('is-disabled');
}); });
it('adds disabled class is disabled is true', () => { describe.each`
mountComponent({ disabled: true }); isSwimlanesOn
${true} | ${false}
expect(wrapper.classes()).toContain('is-disabled'); `('when isSwimlanesOn is $isSwimlanesOn', ({ isSwimlanesOn }) => {
}); it('should not highlight the card by default', async () => {
createStore({ isSwimlanesOn });
describe('mouse events', () => {
it('does not set detail issue if showDetail is false', () => {
mountComponent(); mountComponent();
expect(boardsStore.detail.issue).toEqual({});
});
it('does not set detail issue if link is clicked', () => { expect(wrapper.classes()).not.toContain('is-active');
mountComponent(); expect(wrapper.classes()).not.toContain('multi-select');
findIssueCardInner().find('a').trigger('mouseup');
expect(boardsStore.detail.issue).toEqual({});
}); });
it('does not set detail issue if img is clicked', () => { it('should highlight the card with a correct style when selected', async () => {
mountComponent({ createStore({
issue: { initialState: {
...list.issues[0], activeId: mockIssue.id,
assignees: [
new ListAssignee({
id: 1,
name: 'testing 123',
username: 'test',
avatar: 'test_image',
}),
],
}, },
isSwimlanesOn,
}); });
findUserAvatarLink().trigger('mouseup');
expect(boardsStore.detail.issue).toEqual({});
});
it('does not set detail issue if showDetail is false after mouseup', () => {
mountComponent();
wrapper.trigger('mouseup');
expect(boardsStore.detail.issue).toEqual({});
});
it('sets detail issue to card issue on mouse up', () => {
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
mountComponent(); mountComponent();
wrapper.trigger('mousedown'); expect(wrapper.classes()).toContain('is-active');
wrapper.trigger('mouseup'); expect(wrapper.classes()).not.toContain('multi-select');
expect(eventHub.$emit).toHaveBeenCalledWith('newDetailIssue', wrapper.vm.issue, false);
expect(boardsStore.detail.list).toEqual(wrapper.vm.list);
}); });
it('resets detail issue to empty if already set', () => { it('should highlight the card with a correct style when multi-selected', async () => {
jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); createStore({
const [issue] = list.issues; initialState: {
boardsStore.detail.issue = issue; activeId: inactiveId,
selectedBoardItems: [mockIssue],
},
isSwimlanesOn,
});
mountComponent(); mountComponent();
wrapper.trigger('mousedown'); expect(wrapper.classes()).toContain('multi-select');
wrapper.trigger('mouseup'); expect(wrapper.classes()).not.toContain('is-active');
expect(eventHub.$emit).toHaveBeenCalledWith('clearDetailIssue', false);
}); });
});
describe('sidebarHub events', () => {
it('closes all sidebars before showing an issue if no issues are opened', () => {
jest.spyOn(sidebarEventHub, '$emit').mockImplementation(() => {});
boardsStore.detail.issue = {};
mountComponent();
// sets conditional so that event is emitted.
wrapper.trigger('mousedown');
wrapper.trigger('mouseup'); describe('when mouseup event is called on the issue card', () => {
beforeEach(() => {
createStore({ isSwimlanesOn });
mountComponent();
});
expect(sidebarEventHub.$emit).toHaveBeenCalledWith('sidebar.closeAll'); describe('when not using multi-select', () => {
}); it('should call vuex action "toggleBoardItem" with correct parameters', async () => {
await selectCard();
it('it does not closes all sidebars before showing an issue if an issue is opened', () => { expect(mockActions.toggleBoardItem).toHaveBeenCalledTimes(1);
jest.spyOn(sidebarEventHub, '$emit').mockImplementation(() => {}); expect(mockActions.toggleBoardItem).toHaveBeenCalledWith(expect.any(Object), {
const [issue] = list.issues; boardItem: mockIssue,
boardsStore.detail.issue = issue; });
mountComponent(); });
});
wrapper.trigger('mousedown'); describe('when using multi-select', () => {
it('should call vuex action "multiSelectBoardItem" with correct parameters', async () => {
await multiSelectCard();
expect(sidebarEventHub.$emit).not.toHaveBeenCalledWith('sidebar.closeAll'); expect(mockActions.toggleBoardItemMultiSelection).toHaveBeenCalledTimes(1);
expect(mockActions.toggleBoardItemMultiSelection).toHaveBeenCalledWith(
expect.any(Object),
mockIssue,
);
});
});
}); });
}); });
}); });
...@@ -5,7 +5,7 @@ import { ...@@ -5,7 +5,7 @@ import {
formatBoardLists, formatBoardLists,
formatIssueInput, formatIssueInput,
} from '~/boards/boards_util'; } from '~/boards/boards_util';
import { inactiveId } from '~/boards/constants'; import { inactiveId, ISSUABLE } from '~/boards/constants';
import destroyBoardListMutation from '~/boards/graphql/board_list_destroy.mutation.graphql'; import destroyBoardListMutation from '~/boards/graphql/board_list_destroy.mutation.graphql';
import issueCreateMutation from '~/boards/graphql/issue_create.mutation.graphql'; import issueCreateMutation from '~/boards/graphql/issue_create.mutation.graphql';
import issueMoveListMutation from '~/boards/graphql/issue_move_list.mutation.graphql'; import issueMoveListMutation from '~/boards/graphql/issue_move_list.mutation.graphql';
...@@ -1246,6 +1246,7 @@ describe('setSelectedProject', () => { ...@@ -1246,6 +1246,7 @@ describe('setSelectedProject', () => {
describe('toggleBoardItemMultiSelection', () => { describe('toggleBoardItemMultiSelection', () => {
const boardItem = mockIssue; const boardItem = mockIssue;
const boardItem2 = mockIssue2;
it('should commit mutation ADD_BOARD_ITEM_TO_SELECTION if item is not on selection state', () => { it('should commit mutation ADD_BOARD_ITEM_TO_SELECTION if item is not on selection state', () => {
testAction( testAction(
...@@ -1276,6 +1277,66 @@ describe('toggleBoardItemMultiSelection', () => { ...@@ -1276,6 +1277,66 @@ describe('toggleBoardItemMultiSelection', () => {
[], [],
); );
}); });
it('should additionally commit mutation ADD_BOARD_ITEM_TO_SELECTION for active issue and dispatch unsetActiveId', () => {
testAction(
actions.toggleBoardItemMultiSelection,
boardItem2,
{ activeId: mockActiveIssue.id, activeIssue: mockActiveIssue, selectedBoardItems: [] },
[
{
type: types.ADD_BOARD_ITEM_TO_SELECTION,
payload: mockActiveIssue,
},
{
type: types.ADD_BOARD_ITEM_TO_SELECTION,
payload: boardItem2,
},
],
[{ type: 'unsetActiveId' }],
);
});
});
describe('resetBoardItemMultiSelection', () => {
it('should commit mutation RESET_BOARD_ITEM_SELECTION', () => {
testAction({
action: actions.resetBoardItemMultiSelection,
state: { selectedBoardItems: [mockIssue] },
expectedMutations: [
{
type: types.RESET_BOARD_ITEM_SELECTION,
},
],
});
});
});
describe('toggleBoardItem', () => {
it('should dispatch resetBoardItemMultiSelection and unsetActiveId when boardItem is the active item', () => {
testAction({
action: actions.toggleBoardItem,
payload: { boardItem: mockIssue },
state: {
activeId: mockIssue.id,
},
expectedActions: [{ type: 'resetBoardItemMultiSelection' }, { type: 'unsetActiveId' }],
});
});
it('should dispatch resetBoardItemMultiSelection and setActiveId when boardItem is not the active item', () => {
testAction({
action: actions.toggleBoardItem,
payload: { boardItem: mockIssue },
state: {
activeId: inactiveId,
},
expectedActions: [
{ type: 'resetBoardItemMultiSelection' },
{ type: 'setActiveId', payload: { id: mockIssue.id, sidebarType: ISSUABLE } },
],
});
});
}); });
describe('fetchBacklog', () => { describe('fetchBacklog', () => {
......
...@@ -610,14 +610,21 @@ describe('Board Store Mutations', () => { ...@@ -610,14 +610,21 @@ describe('Board Store Mutations', () => {
describe('REMOVE_BOARD_ITEM_FROM_SELECTION', () => { describe('REMOVE_BOARD_ITEM_FROM_SELECTION', () => {
it('Should remove boardItem to selectedBoardItems state', () => { it('Should remove boardItem to selectedBoardItems state', () => {
state = { state.selectedBoardItems = [mockIssue];
...state,
selectedBoardItems: [mockIssue],
};
mutations[types.REMOVE_BOARD_ITEM_FROM_SELECTION](state, mockIssue); mutations[types.REMOVE_BOARD_ITEM_FROM_SELECTION](state, mockIssue);
expect(state.selectedBoardItems).toEqual([]); expect(state.selectedBoardItems).toEqual([]);
}); });
}); });
describe('RESET_BOARD_ITEM_SELECTION', () => {
it('Should reset selectedBoardItems state', () => {
state.selectedBoardItems = [mockIssue];
mutations[types.RESET_BOARD_ITEM_SELECTION](state, mockIssue);
expect(state.selectedBoardItems).toEqual([]);
});
});
}); });
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment