Commit 5ea9167b authored by Paul Slaughter's avatar Paul Slaughter

Merge branch 'ss/swimlane-sidebar' into 'master'

RUN AS-IF-FOSS Swimlanes Sidebar Container

See merge request gitlab-org/gitlab!38527
parents 52a000e4 00f7c443
<script>
/* eslint-disable vue/require-default-prop */
import IssueCardInner from './issue_card_inner.vue';
import BoardCardLayout from './board_card_layout.vue';
import eventHub from '../eventhub';
import sidebarEventHub from '~/sidebar/event_hub';
import boardsStore from '../stores/boards_store';
......@@ -8,7 +7,7 @@ import boardsStore from '../stores/boards_store';
export default {
name: 'BoardsIssueCard',
components: {
IssueCardInner,
BoardCardLayout,
},
props: {
list: {
......@@ -21,80 +20,29 @@ export default {
default: () => ({}),
required: false,
},
issueLinkBase: {
type: String,
default: '',
required: false,
},
disabled: {
type: Boolean,
default: false,
required: false,
},
index: {
type: Number,
default: 0,
required: false,
},
rootPath: {
type: String,
default: '',
required: false,
},
groupId: {
type: Number,
required: false,
},
},
data() {
return {
showDetail: false,
detailIssue: boardsStore.detail,
multiSelect: boardsStore.multiSelect,
};
},
computed: {
issueDetailVisible() {
return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id;
},
multiSelectVisible() {
return this.multiSelect.list.findIndex(issue => issue.id === this.issue.id) > -1;
},
canMultiSelect() {
return gon.features && gon.features.multiSelectBoard;
},
},
methods: {
mouseDown() {
this.showDetail = true;
// These are methods instead of computed's, because boardsStore is not reactive.
isActive() {
return this.getActiveId() === this.issue.id;
},
mouseMove() {
this.showDetail = false;
getActiveId() {
return boardsStore.detail?.issue?.id;
},
showIssue(e) {
if (e.target.classList.contains('js-no-trigger')) return;
showIssue({ isMultiSelect }) {
// If no issues are opened, close all sidebars first
if (!boardsStore.detail?.issue?.id) {
if (!this.getActiveId()) {
sidebarEventHub.$emit('sidebar.closeAll');
}
if (this.isActive()) {
eventHub.$emit('clearDetailIssue', isMultiSelect);
// If CMD or CTRL is clicked
const isMultiSelect = this.canMultiSelect && (e.ctrlKey || e.metaKey);
if (this.showDetail || isMultiSelect) {
this.showDetail = false;
if (boardsStore.detail.issue && boardsStore.detail.issue.id === this.issue.id) {
eventHub.$emit('clearDetailIssue', isMultiSelect);
if (isMultiSelect) {
eventHub.$emit('newDetailIssue', this.issue, isMultiSelect);
}
} else {
if (isMultiSelect) {
eventHub.$emit('newDetailIssue', this.issue, isMultiSelect);
boardsStore.setListDetail(this.list);
}
} else {
eventHub.$emit('newDetailIssue', this.issue, isMultiSelect);
boardsStore.setListDetail(this.list);
}
},
},
......@@ -102,28 +50,12 @@ export default {
</script>
<template>
<li
:class="{
'multi-select': multiSelectVisible,
'user-can-drag': !disabled && issue.id,
'is-disabled': disabled || !issue.id,
'is-active': issueDetailVisible,
}"
:index="index"
:data-issue-id="issue.id"
<board-card-layout
data-qa-selector="board_card"
class="board-card p-3 rounded"
@mousedown="mouseDown"
@mousemove="mouseMove"
@mouseup="showIssue($event)"
>
<issue-card-inner
:list="list"
:issue="issue"
:issue-link-base="issueLinkBase"
:group-id="groupId"
:root-path="rootPath"
:update-filters="true"
/>
</li>
:issue="issue"
:list="list"
:is-active="isActive()"
v-bind="$attrs"
@show="showIssue"
/>
</template>
<script>
/* eslint-disable vue/require-default-prop */
import IssueCardInner from './issue_card_inner.vue';
import boardsStore from '../stores/boards_store';
export default {
name: 'BoardsIssueCard',
components: {
IssueCardInner,
},
props: {
list: {
type: Object,
default: () => ({}),
required: false,
},
issue: {
type: Object,
default: () => ({}),
required: false,
},
issueLinkBase: {
type: String,
default: '',
required: false,
},
disabled: {
type: Boolean,
default: false,
required: false,
},
index: {
type: Number,
default: 0,
required: false,
},
rootPath: {
type: String,
default: '',
required: false,
},
groupId: {
type: Number,
required: false,
},
isActive: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
showDetail: false,
multiSelect: boardsStore.multiSelect,
};
},
computed: {
multiSelectVisible() {
return this.multiSelect.list.findIndex(issue => issue.id === this.issue.id) > -1;
},
canMultiSelect() {
return gon.features && gon.features.multiSelectBoard;
},
},
methods: {
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 = this.canMultiSelect && (e.ctrlKey || e.metaKey);
if (this.showDetail || isMultiSelect) {
this.showDetail = false;
this.$emit('show', { event: e, isMultiSelect });
}
},
},
};
</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-testid="board_card"
class="board-card p-3 rounded"
@mousedown="mouseDown"
@mousemove="mouseMove"
@mouseup="showIssue($event)"
>
<issue-card-inner
:list="list"
:issue="issue"
:issue-link-base="issueLinkBase"
:group-id="groupId"
:root-path="rootPath"
:update-filters="true"
/>
</li>
</template>
<script>
import { mapActions, mapState } from 'vuex';
import { GlAlert } from '@gitlab/ui';
import { mapState, mapGetters, mapActions } from 'vuex';
import BoardColumn from 'ee_else_ce/boards/components/board_column.vue';
import { GlAlert } from '@gitlab/ui';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
components: {
BoardColumn,
BoardContentSidebar: () => import('ee_component/boards/components/board_content_sidebar.vue'),
EpicsSwimlanes: () => import('ee_component/boards/components/epics_swimlanes.vue'),
GlAlert,
},
......@@ -43,10 +44,8 @@ export default {
},
},
computed: {
...mapState(['isShowingEpicsSwimlanes', 'boardLists', 'error']),
isSwimlanesOn() {
return this.glFeatures.boardsWithSwimlanes && this.isShowingEpicsSwimlanes;
},
...mapState(['boardLists', 'error']),
...mapGetters(['isSwimlanesOn']),
boardListsToUse() {
return this.glFeatures.graphqlBoardLists ? this.boardLists : this.lists;
},
......@@ -86,15 +85,18 @@ export default {
:board-id="boardId"
/>
</div>
<epics-swimlanes
v-else
ref="swimlanes"
:lists="boardListsToUse"
:can-admin-list="canAdminList"
:disabled="disabled"
:board-id="boardId"
:group-id="groupId"
:root-path="rootPath"
/>
<template v-else>
<epics-swimlanes
ref="swimlanes"
:lists="boardLists"
:can-admin-list="canAdminList"
:disabled="disabled"
:board-id="boardId"
:group-id="groupId"
:root-path="rootPath"
/>
<board-content-sidebar />
</template>
</div>
</template>
<script>
import { GlDrawer, GlLabel } from '@gitlab/ui';
import { mapActions, mapState } from 'vuex';
import { mapActions, mapState, mapGetters } from 'vuex';
import { __ } from '~/locale';
import boardsStore from '~/boards/stores/boards_store';
import eventHub from '~/sidebar/event_hub';
import { isScopedLabel } from '~/lib/utils/common_utils';
import { inactiveId } from '~/boards/constants';
import { LIST } from '~/boards/constants';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
// NOTE: need to revisit how we handle headerHeight, because we have so many different header and footer options.
......@@ -26,7 +26,8 @@ export default {
},
mixins: [glFeatureFlagMixin()],
computed: {
...mapState(['activeId', 'boardLists']),
...mapGetters(['isSidebarOpen']),
...mapState(['activeId', 'sidebarType', 'boardLists']),
activeList() {
/*
Warning: Though a computed property it is not reactive because we are
......@@ -37,9 +38,6 @@ export default {
}
return boardsStore.state.lists.find(({ id }) => id === this.activeId);
},
isSidebarOpen() {
return this.activeId !== inactiveId;
},
activeListLabel() {
return this.activeList.label;
},
......@@ -49,18 +47,18 @@ export default {
listTypeTitle() {
return this.$options.labelListText;
},
showSidebar() {
return this.sidebarType === LIST;
},
},
created() {
eventHub.$on('sidebar.closeAll', this.closeSidebar);
eventHub.$on('sidebar.closeAll', this.unsetActiveId);
},
beforeDestroy() {
eventHub.$off('sidebar.closeAll', this.closeSidebar);
eventHub.$off('sidebar.closeAll', this.unsetActiveId);
},
methods: {
...mapActions(['setActiveId']),
closeSidebar() {
this.setActiveId(inactiveId);
},
...mapActions(['unsetActiveId']),
showScopedLabels(label) {
return boardsStore.scopedLabels.enabled && isScopedLabel(label);
},
......@@ -70,10 +68,11 @@ export default {
<template>
<gl-drawer
v-if="showSidebar"
class="js-board-settings-sidebar"
:open="isSidebarOpen"
:header-height="$options.headerHeight"
@close="closeSidebar"
@close="unsetActiveId"
>
<template #header>{{ $options.listSettingsText }}</template>
<template v-if="isSidebarOpen">
......
......@@ -15,6 +15,9 @@ export const ListType = {
export const inactiveId = 0;
export const ISSUABLE = 'issuable';
export const LIST = 'list';
export default {
BoardType,
ListType,
......
......@@ -5,7 +5,7 @@ import { __ } from '~/locale';
import { parseBoolean } from '~/lib/utils/common_utils';
import createDefaultClient from '~/lib/graphql';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { BoardType, ListType } from '~/boards/constants';
import { BoardType, ListType, inactiveId } from '~/boards/constants';
import * as types from './mutation_types';
import { formatListIssues, fullBoardId } from '../boards_util';
import boardStore from '~/boards/stores/boards_store';
......@@ -28,8 +28,12 @@ export default {
commit(types.SET_INITIAL_BOARD_DATA, data);
},
setActiveId({ commit }, id) {
commit(types.SET_ACTIVE_ID, id);
setActiveId({ commit }, { id, sidebarType }) {
commit(types.SET_ACTIVE_ID, { id, sidebarType });
},
unsetActiveId({ dispatch }) {
dispatch('setActiveId', { id: inactiveId, sidebarType: '' });
},
setFilters: ({ commit }, filters) => {
......
import { inactiveId } from '../constants';
export default {
getLabelToggleState: state => (state.isShowingLabels ? 'on' : 'off'),
isSidebarOpen: state => state.activeId !== inactiveId,
isSwimlanesOn: state => {
if (!gon?.features?.boardsWithSwimlanes) {
return false;
}
return state.isShowingEpicsSwimlanes;
},
};
......@@ -19,8 +19,9 @@ export default {
state.boardLists = lists;
},
[mutationTypes.SET_ACTIVE_ID](state, id) {
[mutationTypes.SET_ACTIVE_ID](state, { id, sidebarType }) {
state.activeId = id;
state.sidebarType = sidebarType;
},
[mutationTypes.SET_FILTERS](state, filterParams) {
......
......@@ -7,6 +7,7 @@ export default () => ({
showPromotion: false,
isShowingLabels: true,
activeId: inactiveId,
sidebarType: '',
boardLists: [],
issuesByListId: {},
isLoadingIssues: false,
......
<script>
import { mapState, mapActions, mapGetters } from 'vuex';
import { GlDrawer } from '@gitlab/ui';
import { ISSUABLE } from '~/boards/constants';
import { contentTop } from '~/lib/utils/common_utils';
export default {
headerHeight: `${contentTop()}px`,
components: {
GlDrawer,
},
computed: {
...mapGetters(['isSidebarOpen']),
...mapState(['sidebarType']),
showSidebar() {
return this.sidebarType === ISSUABLE;
},
},
methods: {
...mapActions(['unsetActiveId']),
},
};
</script>
<template>
<gl-drawer
v-if="showSidebar"
:open="isSidebarOpen"
:header-height="$options.headerHeight"
@close="unsetActiveId"
/>
</template>
......@@ -3,7 +3,7 @@ import { mapState, mapActions } from 'vuex';
import BoardListHeaderFoss from '~/boards/components/board_list_header.vue';
import { __, sprintf, s__ } from '~/locale';
import boardsStore from '~/boards/stores/boards_store';
import { inactiveId } from '~/boards/constants';
import { inactiveId, LIST } from '~/boards/constants';
import eventHub from '~/sidebar/event_hub';
export default {
......@@ -45,7 +45,7 @@ export default {
eventHub.$emit('sidebar.closeAll');
}
this.setActiveId(this.list.id);
this.setActiveId({ id: this.list.id, sidebarType: LIST });
},
},
};
......
......@@ -48,7 +48,7 @@ export default {
},
},
methods: {
...mapActions(['setActiveId', 'updateListWipLimit']),
...mapActions(['unsetActiveId', 'updateListWipLimit']),
showInput() {
this.edit = true;
this.currentWipLimit = this.maxIssueCount > 0 ? this.maxIssueCount : null;
......@@ -80,7 +80,7 @@ export default {
boardsStoreEE.setMaxIssueCountOnList(id, wipLimit);
})
.catch(() => {
this.setActiveId(0);
this.unsetActiveId();
flash(__('Something went wrong while updating your list settings'));
})
.finally(() => {
......@@ -96,7 +96,7 @@ export default {
boardsStoreEE.setMaxIssueCountOnList(this.activeId, inactiveId);
})
.catch(() => {
this.setActiveId(inactiveId);
this.unsetActiveId();
flash(__('Something went wrong while updating your list settings'));
})
.finally(() => {
......
<script>
import { mapState, mapActions } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
import BoardCardLayout from '~/boards/components/board_card_layout.vue';
import eventHub from '~/boards/eventhub';
import BoardCard from '~/boards/components/board_card.vue';
import BoardNewIssue from '~/boards/components/board_new_issue.vue';
import { ISSUABLE } from '~/boards/constants';
export default {
components: {
BoardCard,
BoardCardLayout,
BoardNewIssue,
GlLoadingIcon,
},
......@@ -49,6 +51,9 @@ export default {
showIssueForm: false,
};
},
computed: {
...mapState(['activeId']),
},
created() {
eventHub.$on(`toggle-issue-form-${this.list.id}`, this.toggleForm);
},
......@@ -56,12 +61,19 @@ export default {
eventHub.$off(`toggle-issue-form-${this.list.id}`, this.toggleForm);
},
methods: {
...mapActions(['setActiveId']),
toggleForm() {
this.showIssueForm = !this.showIssueForm;
if (this.showIssueForm && this.isUnassignedIssuesLane) {
this.$el.scrollIntoView(false);
}
},
isActiveIssue(issue) {
return this.activeId === issue.id;
},
showIssue(issue) {
this.setActiveId({ id: issue.id, sidebarType: ISSUABLE });
},
},
};
</script>
......@@ -79,7 +91,7 @@ export default {
:list="list"
/>
<ul v-if="list.isExpanded" class="gl-p-2 gl-m-0">
<board-card
<board-card-layout
v-for="(issue, index) in issues"
ref="issue"
:key="issue.id"
......@@ -87,6 +99,8 @@ export default {
:list="list"
:issue="issue"
:root-path="rootPath"
:is-active="isActiveIssue(issue)"
@show="showIssue(issue)"
/>
</ul>
</div>
......
import { shallowMount } from '@vue/test-utils';
import { GlDrawer } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
import BoardContentSidebar from 'ee_component/boards/components/board_content_sidebar.vue';
import { createStore } from '~/boards/stores';
import { ISSUABLE } from '~/boards/constants';
describe('ee/BoardContentSidebar', () => {
let wrapper;
let store;
const createComponent = () => {
wrapper = shallowMount(BoardContentSidebar, {
store,
});
};
beforeEach(() => {
store = createStore();
store.state.sidebarType = ISSUABLE;
store.state.activeId = 1;
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
it('confirms we render GlDrawer', () => {
expect(wrapper.find(GlDrawer).exists()).toBe(true);
});
it('applies an open attribute', () => {
expect(wrapper.find(GlDrawer).props('open')).toBe(true);
});
describe('when we emit close', () => {
it('hides GlDrawer', async () => {
expect(wrapper.find(GlDrawer).props('open')).toBe(true);
wrapper.find(GlDrawer).vm.$emit('close');
await waitForPromises();
expect(wrapper.find(GlDrawer).exists()).toBe(false);
});
});
});
import Vuex from 'vuex';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { GlAlert } from '@gitlab/ui';
import EpicsSwimlanes from 'ee_component/boards/components/epics_swimlanes.vue';
import BoardColumn from 'ee_else_ce/boards/components/board_column.vue';
import getters from 'ee/boards/stores/getters';
import { mockListsWithModel, mockIssuesByListId } from '../mock_data';
import { shallowMount } from '@vue/test-utils';
import BoardContentSidebar from 'ee/boards/components/board_content_sidebar.vue';
import BoardContent from '~/boards/components/board_content.vue';
import { createStore } from '~/boards/stores';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('BoardContent', () => {
describe('ee/BoardContent', () => {
let wrapper;
let store;
window.gon = { features: {} };
const defaultState = {
isShowingEpicsSwimlanes: false,
boardLists: mockListsWithModel,
error: undefined,
issuesByListId: mockIssuesByListId,
};
const createStore = (state = defaultState) => {
return new Vuex.Store({
state,
actions: {
fetchIssuesForAllLists: () => {},
},
getters,
});
};
const createComponent = state => {
const store = createStore({
...defaultState,
...state,
});
const createComponent = () => {
wrapper = shallowMount(BoardContent, {
localVue,
store,
propsData: {
lists: mockListsWithModel,
canAdminList: true,
groupId: 1,
lists: [],
canAdminList: false,
disabled: false,
issueLinkBase: '/',
rootPath: '/',
boardId: '1',
issueLinkBase: '',
rootPath: '',
boardId: '',
},
store,
provide: {
glFeatures: { boardsWithSwimlanes: true },
stubs: {
'board-content-sidebar': BoardContentSidebar,
},
});
};
beforeEach(() => {
store = createStore();
});
afterEach(() => {
window.gon.features = {};
wrapper.destroy();
});
describe('Swimlanes off', () => {
describe.each`
featureFlag | state | result
${true} | ${{ isShowingEpicsSwimlanes: true }} | ${true}
${true} | ${{ isShowingEpicsSwimlanes: false }} | ${false}
${false} | ${{ isShowingEpicsSwimlanes: true }} | ${false}
${false} | ${{ isShowingEpicsSwimlanes: false }} | ${false}
`('with featureFlag=$featureFlag and state=$state', ({ featureFlag, state, result }) => {
beforeEach(() => {
gon.features.boardsWithSwimlanes = featureFlag;
Object.assign(store.state, state);
createComponent();
});
it('renders a BoardColumn component per list', () => {
expect(wrapper.findAll(BoardColumn)).toHaveLength(mockListsWithModel.length);
});
it('does not display EpicsSwimlanes component', () => {
expect(wrapper.contains(EpicsSwimlanes)).toBe(false);
expect(wrapper.contains(GlAlert)).toBe(false);
});
});
describe('Swimlanes on', () => {
beforeEach(() => {
createComponent({ isShowingEpicsSwimlanes: true });
});
it('does not display BoardColumn component', () => {
expect(wrapper.findAll(BoardColumn)).toHaveLength(0);
});
it('displays EpicsSwimlanes component', () => {
expect(wrapper.contains('.board-swimlanes')).toBe(true);
expect(wrapper.contains(GlAlert)).toBe(false);
});
it('displays alert if an error occurs when fetching swimlanes', () => {
createComponent({
isShowingEpicsSwimlanes: true,
error: 'An error occurred while fetching the board swimlanes. Please reload the page.',
});
expect(wrapper.contains(GlAlert)).toBe(true);
it(`renders BoardContentSidebar = ${result}`, () => {
expect(wrapper.find(BoardContentSidebar).exists()).toBe(result);
});
});
});
......@@ -7,7 +7,8 @@ import BoardSettingsWipLimit from 'ee_component/boards/components/board_settings
import BoardSettingsListTypes from 'ee_component/boards/components/board_settings_list_types.vue';
import BoardSettingsSidebar from '~/boards/components/board_settings_sidebar.vue';
import boardsStore from '~/boards/stores/boards_store';
import { inactiveId } from '~/boards/constants';
import getters from '~/boards/stores/getters';
import { LIST } from '~/boards/constants';
const localVue = createLocalVue();
......@@ -21,11 +22,12 @@ describe('ee/BoardSettingsSidebar', () => {
const listId = 1;
let mock;
const createComponent = (state = { activeId: inactiveId }, actions = {}) => {
const createComponent = (actions = {}) => {
storeActions = actions;
const store = new Vuex.Store({
state,
state: { sidebarType: LIST, activeId: listId },
getters,
actions: storeActions,
});
......@@ -57,7 +59,7 @@ describe('ee/BoardSettingsSidebar', () => {
list_type: 'label',
});
createComponent({ activeId: listId });
createComponent();
expect(wrapper.find(BoardSettingsWipLimit).exists()).toBe(true);
});
......@@ -73,7 +75,7 @@ describe('ee/BoardSettingsSidebar', () => {
list_type: 'milestone',
});
createComponent({ activeId: listId });
createComponent();
expect(wrapper.find(BoardSettingsListTypes).exists()).toBe(true);
});
......
......@@ -285,7 +285,7 @@ describe('BoardSettingsWipLimit', () => {
const spy = jest.fn().mockRejectedValue();
createComponent({
vuexState: { activeId: listId },
actions: { updateListWipLimit: spy, setActiveId: noop },
actions: { updateListWipLimit: spy, unsetActiveId: noop },
localState: { edit: true, currentWipLimit },
});
......
......@@ -4,15 +4,17 @@ import { shallowMount } from '@vue/test-utils';
import IssuesLaneList from 'ee/boards/components/issues_lane_list.vue';
import { listObj } from 'jest/boards/mock_data';
import { TEST_HOST } from 'helpers/test_constants';
import BoardCard from '~/boards/components/board_card_layout.vue';
import axios from '~/lib/utils/axios_utils';
import BoardCard from '~/boards/components/board_card.vue';
import { mockIssues } from '../mock_data';
import List from '~/boards/models/list';
import { createStore } from '~/boards/stores';
import { ListType } from '~/boards/constants';
describe('IssuesLaneList', () => {
let wrapper;
let axiosMock;
let store;
beforeEach(() => {
axiosMock = new AxiosMockAdapter(axios);
......@@ -48,6 +50,7 @@ describe('IssuesLaneList', () => {
}
wrapper = shallowMount(IssuesLaneList, {
store,
propsData: {
list,
issues: mockIssues,
......@@ -65,6 +68,8 @@ describe('IssuesLaneList', () => {
describe('if list is expanded', () => {
beforeEach(() => {
store = createStore();
createComponent();
});
......@@ -79,6 +84,8 @@ describe('IssuesLaneList', () => {
describe('if list is collapsed', () => {
beforeEach(() => {
store = createStore();
createComponent({ collapsed: true });
});
......
/* global List */
/* global ListLabel */
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import '~/boards/models/label';
import '~/boards/models/assignee';
import '~/boards/models/list';
import store from '~/boards/stores';
import boardsStore from '~/boards/stores/boards_store';
import BoardCardLayout from '~/boards/components/board_card_layout.vue';
import issueCardInner from '~/boards/components/issue_card_inner.vue';
import { listObj, boardsMockInterceptor, setMockEndpoints } from '../mock_data';
describe('Board card layout', () => {
let wrapper;
let mock;
let list;
// this particular mount component needs to be used after the root beforeEach because it depends on list being initialized
const mountComponent = propsData => {
wrapper = shallowMount(BoardCardLayout, {
stubs: {
issueCardInner,
},
store,
propsData: {
list,
issue: list.issues[0],
issueLinkBase: '/',
disabled: false,
index: 0,
rootPath: '/',
...propsData,
},
});
};
const setupData = () => {
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',
});
return waitForPromises().then(() => {
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();
});
describe('mouse events', () => {
it('sets showDetail to true on mousedown', async () => {
mountComponent();
wrapper.trigger('mousedown');
await wrapper.vm.$nextTick();
expect(wrapper.vm.showDetail).toBe(true);
});
it('sets showDetail to false on mousemove', async () => {
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);
});
});
});
......@@ -2,7 +2,7 @@
/* global ListAssignee */
/* global ListLabel */
import { shallowMount } from '@vue/test-utils';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
......@@ -15,12 +15,12 @@ import '~/boards/models/assignee';
import '~/boards/models/list';
import store from '~/boards/stores';
import boardsStore from '~/boards/stores/boards_store';
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 userAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import { listObj, boardsMockInterceptor, setMockEndpoints } from './mock_data';
import { listObj, boardsMockInterceptor, setMockEndpoints } from '../mock_data';
describe('Board card', () => {
describe('BoardCard', () => {
let wrapper;
let mock;
let list;
......@@ -30,7 +30,7 @@ describe('Board card', () => {
// this particular mount component needs to be used after the root beforeEach because it depends on list being initialized
const mountComponent = propsData => {
wrapper = shallowMount(boardCard, {
wrapper = mount(BoardCard, {
stubs: {
issueCardInner,
},
......@@ -47,7 +47,7 @@ describe('Board card', () => {
});
};
const setupData = () => {
const setupData = async () => {
list = new List(listObj);
boardsStore.create();
boardsStore.detail.issue = {};
......@@ -58,9 +58,9 @@ describe('Board card', () => {
text_color: 'white',
description: 'test',
});
return waitForPromises().then(() => {
list.issues[0].labels.push(label1);
});
await waitForPromises();
list.issues[0].labels.push(label1);
};
beforeEach(() => {
......@@ -79,7 +79,7 @@ describe('Board card', () => {
it('when details issue is empty does not show the element', () => {
mountComponent();
expect(wrapper.classes()).not.toContain('is-active');
expect(wrapper.find('[data-testid="board_card"').classes()).not.toContain('is-active');
});
it('when detailIssue is equal to card issue shows the element', () => {
......@@ -124,29 +124,6 @@ describe('Board card', () => {
});
describe('mouse events', () => {
it('sets showDetail to true on mousedown', () => {
mountComponent();
wrapper.trigger('mousedown');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.showDetail).toBe(true);
});
});
it('sets showDetail to false on mousemove', () => {
mountComponent();
wrapper.trigger('mousedown');
return wrapper.vm
.$nextTick()
.then(() => {
expect(wrapper.vm.showDetail).toBe(true);
wrapper.trigger('mousemove');
return wrapper.vm.$nextTick();
})
.then(() => {
expect(wrapper.vm.showDetail).toBe(false);
});
});
it('does not set detail issue if showDetail is false', () => {
mountComponent();
expect(boardsStore.detail.issue).toEqual({});
......@@ -219,6 +196,9 @@ describe('Board card', () => {
boardsStore.detail.issue = {};
mountComponent();
// sets conditional so that event is emitted.
wrapper.trigger('mousedown');
wrapper.trigger('mouseup');
expect(sidebarEventHub.$emit).toHaveBeenCalledWith('sidebar.closeAll');
......
......@@ -3,6 +3,7 @@ import { createLocalVue, shallowMount } from '@vue/test-utils';
import { GlAlert } from '@gitlab/ui';
import EpicsSwimlanes from 'ee_component/boards/components/epics_swimlanes.vue';
import BoardColumn from 'ee_else_ce/boards/components/board_column.vue';
import getters from 'ee_else_ce/boards/stores/getters';
import { mockListsWithModel } from '../mock_data';
import BoardContent from '~/boards/components/board_content.vue';
......@@ -20,6 +21,7 @@ describe('BoardContent', () => {
const createStore = (state = defaultState) => {
return new Vuex.Store({
getters,
state,
actions: {
fetchIssuesForAllLists: () => {},
......
......@@ -6,8 +6,9 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlDrawer, GlLabel } from '@gitlab/ui';
import BoardSettingsSidebar from '~/boards/components/board_settings_sidebar.vue';
import boardsStore from '~/boards/stores/boards_store';
import { createStore } from '~/boards/stores';
import sidebarEventHub from '~/sidebar/event_hub';
import { inactiveId } from '~/boards/constants';
import { inactiveId, LIST } from '~/boards/constants';
const localVue = createLocalVue();
......@@ -16,19 +17,12 @@ localVue.use(Vuex);
describe('BoardSettingsSidebar', () => {
let wrapper;
let mock;
let storeActions;
let store;
const labelTitle = 'test';
const labelColor = '#FFFF';
const listId = 1;
const createComponent = (state = { activeId: inactiveId }, actions = {}) => {
storeActions = actions;
const store = new Vuex.Store({
state,
actions: storeActions,
});
const createComponent = () => {
wrapper = shallowMount(BoardSettingsSidebar, {
store,
localVue,
......@@ -38,6 +32,9 @@ describe('BoardSettingsSidebar', () => {
const findDrawer = () => wrapper.find(GlDrawer);
beforeEach(() => {
store = createStore();
store.state.activeId = inactiveId;
store.state.sidebarType = LIST;
boardsStore.create();
});
......@@ -46,114 +43,125 @@ describe('BoardSettingsSidebar', () => {
wrapper.destroy();
});
it('finds a GlDrawer component', () => {
createComponent();
describe('when sidebarType is "list"', () => {
it('finds a GlDrawer component', () => {
createComponent();
expect(findDrawer().exists()).toBe(true);
});
expect(findDrawer().exists()).toBe(true);
});
describe('on close', () => {
it('calls closeSidebar', async () => {
const spy = jest.fn();
createComponent({ activeId: inactiveId }, { setActiveId: spy });
describe('on close', () => {
it('closes the sidebar', async () => {
createComponent();
findDrawer().vm.$emit('close');
findDrawer().vm.$emit('close');
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
expect(storeActions.setActiveId).toHaveBeenCalledWith(
expect.anything(),
inactiveId,
undefined,
);
});
expect(wrapper.find(GlDrawer).exists()).toBe(false);
});
it('calls closeSidebar on sidebar.closeAll event', async () => {
createComponent({ activeId: inactiveId }, { setActiveId: jest.fn() });
it('closes the sidebar when emitting the correct event', async () => {
createComponent();
sidebarEventHub.$emit('sidebar.closeAll');
sidebarEventHub.$emit('sidebar.closeAll');
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
expect(storeActions.setActiveId).toHaveBeenCalledWith(
expect.anything(),
inactiveId,
undefined,
);
expect(wrapper.find(GlDrawer).exists()).toBe(false);
});
});
});
describe('when activeId is zero', () => {
it('renders GlDrawer with open false', () => {
createComponent();
describe('when activeId is zero', () => {
it('renders GlDrawer with open false', () => {
createComponent();
expect(findDrawer().props('open')).toBe(false);
expect(findDrawer().props('open')).toBe(false);
});
});
});
describe('when activeId is greater than zero', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
describe('when activeId is greater than zero', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
boardsStore.addList({
id: listId,
label: { title: labelTitle, color: labelColor },
list_type: 'label',
});
store.state.activeId = 1;
store.state.sidebarType = LIST;
});
boardsStore.addList({
id: listId,
label: { title: labelTitle, color: labelColor },
list_type: 'label',
afterEach(() => {
boardsStore.removeList(listId);
});
});
afterEach(() => {
boardsStore.removeList(listId);
it('renders GlDrawer with open false', () => {
createComponent();
expect(findDrawer().props('open')).toBe(true);
});
});
it('renders GlDrawer with open false', () => {
createComponent({ activeId: 1 });
describe('when activeId is in boardsStore', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
expect(findDrawer().props('open')).toBe(true);
});
});
boardsStore.addList({
id: listId,
label: { title: labelTitle, color: labelColor },
list_type: 'label',
});
describe('when activeId is in boardsStore', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
store.state.activeId = listId;
store.state.sidebarType = LIST;
boardsStore.addList({
id: listId,
label: { title: labelTitle, color: labelColor },
list_type: 'label',
createComponent();
});
createComponent({ activeId: listId });
});
afterEach(() => {
mock.restore();
});
afterEach(() => {
mock.restore();
});
it('renders label title', () => {
expect(findLabel().props('title')).toBe(labelTitle);
});
it('renders label title', () => {
expect(findLabel().props('title')).toBe(labelTitle);
it('renders label background color', () => {
expect(findLabel().props('backgroundColor')).toBe(labelColor);
});
});
it('renders label background color', () => {
expect(findLabel().props('backgroundColor')).toBe(labelColor);
});
});
describe('when activeId is not in boardsStore', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
describe('when activeId is not in boardsStore', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
boardsStore.addList({ id: listId, label: { title: labelTitle, color: labelColor } });
store.state.activeId = inactiveId;
boardsStore.addList({ id: listId, label: { title: labelTitle, color: labelColor } });
createComponent();
});
afterEach(() => {
mock.restore();
});
createComponent({ activeId: inactiveId });
it('does not render GlLabel', () => {
expect(findLabel().exists()).toBe(false);
});
});
});
afterEach(() => {
mock.restore();
describe('when sidebarType is not List', () => {
beforeEach(() => {
store.state.sidebarType = '';
createComponent();
});
it('does not render GlLabel', () => {
expect(findLabel().exists()).toBe(false);
it('does not render GlDrawer', () => {
expect(findDrawer().exists()).toBe(false);
});
});
});
......@@ -53,9 +53,9 @@ describe('setActiveId', () => {
testAction(
actions.setActiveId,
1,
{ id: 1, sidebarType: 'something' },
state,
[{ type: types.SET_ACTIVE_ID, payload: 1 }],
[{ type: types.SET_ACTIVE_ID, payload: { id: 1, sidebarType: 'something' } }],
[],
done,
);
......
import getters from '~/boards/stores/getters';
import { inactiveId } from '~/boards/constants';
describe('Boards - Getters', () => {
describe('getLabelToggleState', () => {
......@@ -18,4 +19,76 @@ describe('Boards - Getters', () => {
expect(getters.getLabelToggleState(state)).toBe('off');
});
});
describe('isSidebarOpen', () => {
it('returns true when activeId is not equal to 0', () => {
const state = {
activeId: 1,
};
expect(getters.isSidebarOpen(state)).toBe(true);
});
it('returns false when activeId is equal to 0', () => {
const state = {
activeId: inactiveId,
};
expect(getters.isSidebarOpen(state)).toBe(false);
});
});
describe('isSwimlanesOn', () => {
afterEach(() => {
window.gon = { features: {} };
});
describe('when boardsWithSwimlanes is true', () => {
beforeEach(() => {
window.gon = { features: { boardsWithSwimlanes: true } };
});
describe('when isShowingEpicsSwimlanes is true', () => {
it('returns true', () => {
const state = {
isShowingEpicsSwimlanes: true,
};
expect(getters.isSwimlanesOn(state)).toBe(true);
});
});
describe('when isShowingEpicsSwimlanes is false', () => {
it('returns false', () => {
const state = {
isShowingEpicsSwimlanes: false,
};
expect(getters.isSwimlanesOn(state)).toBe(false);
});
});
});
describe('when boardsWithSwimlanes is false', () => {
describe('when isShowingEpicsSwimlanes is true', () => {
it('returns false', () => {
const state = {
isShowingEpicsSwimlanes: true,
};
expect(getters.isSwimlanesOn(state)).toBe(false);
});
});
describe('when isShowingEpicsSwimlanes is false', () => {
it('returns false', () => {
const state = {
isShowingEpicsSwimlanes: false,
};
expect(getters.isSwimlanesOn(state)).toBe(false);
});
});
});
});
});
......@@ -55,12 +55,18 @@ describe('Board Store Mutations', () => {
});
describe('SET_ACTIVE_ID', () => {
it('updates activeListId to be the value that is passed', () => {
const expectedId = 1;
const expected = { id: 1, sidebarType: '' };
mutations.SET_ACTIVE_ID(state, expectedId);
beforeEach(() => {
mutations.SET_ACTIVE_ID(state, expected);
});
it('updates aciveListId to be the value that is passed', () => {
expect(state.activeId).toBe(expected.id);
});
expect(state.activeId).toBe(expectedId);
it('updates sidebarType to be the value that is passed', () => {
expect(state.sidebarType).toBe(expected.sidebarType);
});
});
......
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