Commit cf6e5687 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Refactored create label component

Added a mutation to create label

Added error handling

Removed duplicated code from label views

Removed Vuex from createLabelView

Changelog: changed
Added and fixed tests

Copied tests for labels widget
Specs changes

Added required prop

Fixed labels view spec

Fixed labels select root spec

Scaffolded a create label component test

Fixed classes to be GitLab UI utilities

Added tests for picking a color

Added specs for create and cancel buttons

Added tests for disabling a button

Added mutation specs
Apply 1 suggestion(s) to 1 file(s)
Added a spec for loader

Removed createLabel action

Apply 1 suggestion(s) to 1 file(s)
Replaced data-testid with GlLink

Apply 1 suggestion(s) to 1 file(s)
Changed import to relative

Apply 1 suggestion(s) to 1 file(s)
Moved createFlash text to a variable
parent c991fc6f
<script> <script>
import { mapGetters, mapState } from 'vuex'; import { GlButton } from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
import DropdownContentsCreateView from './dropdown_contents_create_view.vue'; import DropdownContentsCreateView from './dropdown_contents_create_view.vue';
import DropdownContentsLabelsView from './dropdown_contents_labels_view.vue'; import DropdownContentsLabelsView from './dropdown_contents_labels_view.vue';
...@@ -8,6 +9,7 @@ export default { ...@@ -8,6 +9,7 @@ export default {
components: { components: {
DropdownContentsLabelsView, DropdownContentsLabelsView,
DropdownContentsCreateView, DropdownContentsCreateView,
GlButton,
}, },
props: { props: {
renderOnTop: { renderOnTop: {
...@@ -15,10 +17,14 @@ export default { ...@@ -15,10 +17,14 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
labelsCreateTitle: {
type: String,
required: true,
},
}, },
computed: { computed: {
...mapState(['showDropdownContentsCreateView']), ...mapState(['showDropdownContentsCreateView', 'labelsListTitle']),
...mapGetters(['isDropdownVariantSidebar']), ...mapGetters(['isDropdownVariantSidebar', 'isDropdownVariantEmbedded']),
dropdownContentsView() { dropdownContentsView() {
if (this.showDropdownContentsCreateView) { if (this.showDropdownContentsCreateView) {
return 'dropdown-contents-create-view'; return 'dropdown-contents-create-view';
...@@ -29,6 +35,12 @@ export default { ...@@ -29,6 +35,12 @@ export default {
const bottom = this.isDropdownVariantSidebar ? '3rem' : '2rem'; const bottom = this.isDropdownVariantSidebar ? '3rem' : '2rem';
return this.renderOnTop ? { bottom } : {}; return this.renderOnTop ? { bottom } : {};
}, },
dropdownTitle() {
return this.showDropdownContentsCreateView ? this.labelsCreateTitle : this.labelsListTitle;
},
},
methods: {
...mapActions(['toggleDropdownContentsCreateView', 'toggleDropdownContents']),
}, },
}; };
</script> </script>
...@@ -39,6 +51,30 @@ export default { ...@@ -39,6 +51,30 @@ export default {
data-qa-selector="labels_dropdown_content" data-qa-selector="labels_dropdown_content"
:style="directionStyle" :style="directionStyle"
> >
<component :is="dropdownContentsView" /> <div
v-if="isDropdownVariantSidebar || isDropdownVariantEmbedded"
class="dropdown-title gl-display-flex gl-align-items-center gl-pt-0 gl-pb-3!"
data-testid="dropdown-title"
>
<gl-button
v-if="showDropdownContentsCreateView"
:aria-label="__('Go back')"
variant="link"
size="small"
class="js-btn-back dropdown-header-button p-0"
icon="arrow-left"
@click="toggleDropdownContentsCreateView"
/>
<span class="flex-grow-1">{{ dropdownTitle }}</span>
<gl-button
:aria-label="__('Close')"
variant="link"
size="small"
class="dropdown-header-button gl-p-0!"
icon="close"
@click="toggleDropdownContents"
/>
</div>
<component :is="dropdownContentsView" @hideCreateView="toggleDropdownContentsCreateView" />
</div> </div>
</template> </template>
<script> <script>
import { GlTooltipDirective, GlButton, GlFormInput, GlLink, GlLoadingIcon } from '@gitlab/ui'; import { GlTooltipDirective, GlButton, GlFormInput, GlLink, GlLoadingIcon } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex'; import createFlash from '~/flash';
import { __ } from '~/locale';
import createLabelMutation from './graphql/create_label.mutation.graphql';
const errorMessage = __('Error creating label.');
export default { export default {
components: { components: {
...@@ -12,14 +16,19 @@ export default { ...@@ -12,14 +16,19 @@ export default {
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
}, },
inject: {
projectPath: {
default: '',
},
},
data() { data() {
return { return {
labelTitle: '', labelTitle: '',
selectedColor: '', selectedColor: '',
labelCreateInProgress: false,
}; };
}, },
computed: { computed: {
...mapState(['labelsCreateTitle', 'labelCreateInProgress']),
disableCreate() { disableCreate() {
return !this.labelTitle.length || !this.selectedColor.length || this.labelCreateInProgress; return !this.labelTitle.length || !this.selectedColor.length || this.labelCreateInProgress;
}, },
...@@ -29,7 +38,6 @@ export default { ...@@ -29,7 +38,6 @@ export default {
}, },
}, },
methods: { methods: {
...mapActions(['toggleDropdownContents', 'toggleDropdownContentsCreateView', 'createLabel']),
getColorCode(color) { getColorCode(color) {
return Object.keys(color).pop(); return Object.keys(color).pop();
}, },
...@@ -39,11 +47,27 @@ export default { ...@@ -39,11 +47,27 @@ export default {
handleColorClick(color) { handleColorClick(color) {
this.selectedColor = this.getColorCode(color); this.selectedColor = this.getColorCode(color);
}, },
handleCreateClick() { async createLabel() {
this.createLabel({ this.labelCreateInProgress = true;
title: this.labelTitle, try {
color: this.selectedColor, const {
}); data: { labelCreate },
} = await this.$apollo.mutate({
mutation: createLabelMutation,
variables: {
title: this.labelTitle,
color: this.selectedColor,
projectPath: this.projectPath,
},
});
if (labelCreate.errors.length) {
createFlash({ message: errorMessage });
}
} catch {
createFlash({ message: errorMessage });
}
this.labelCreateInProgress = false;
this.$emit('hideCreateView');
}, },
}, },
}; };
...@@ -51,34 +75,16 @@ export default { ...@@ -51,34 +75,16 @@ export default {
<template> <template>
<div class="labels-select-contents-create js-labels-create"> <div class="labels-select-contents-create js-labels-create">
<div class="dropdown-title d-flex align-items-center pt-0 pb-2">
<gl-button
:aria-label="__('Go back')"
variant="link"
size="small"
class="js-btn-back dropdown-header-button p-0"
icon="arrow-left"
@click="toggleDropdownContentsCreateView"
/>
<span class="flex-grow-1">{{ labelsCreateTitle }}</span>
<gl-button
:aria-label="__('Close')"
variant="link"
size="small"
class="dropdown-header-button p-0"
icon="close"
@click="toggleDropdownContents"
/>
</div>
<div class="dropdown-input"> <div class="dropdown-input">
<gl-form-input <gl-form-input
v-model.trim="labelTitle" v-model.trim="labelTitle"
:placeholder="__('Name new label')" :placeholder="__('Name new label')"
:autofocus="true" :autofocus="true"
data-testid="label-title-input"
/> />
</div> </div>
<div class="dropdown-content px-2"> <div class="dropdown-content gl-px-3">
<div class="suggest-colors suggest-colors-dropdown mt-0 mb-2"> <div class="suggest-colors suggest-colors-dropdown gl-mt-0! gl-mb-3!">
<gl-link <gl-link
v-for="(color, index) in suggestedColors" v-for="(color, index) in suggestedColors"
:key="index" :key="index"
...@@ -90,28 +96,35 @@ export default { ...@@ -90,28 +96,35 @@ export default {
</div> </div>
<div class="color-input-container gl-display-flex"> <div class="color-input-container gl-display-flex">
<span <span
class="dropdown-label-color-preview position-relative position-relative d-inline-block" class="dropdown-label-color-preview gl-relative gl-display-inline-block"
data-testid="selected-color"
:style="{ backgroundColor: selectedColor }" :style="{ backgroundColor: selectedColor }"
></span> ></span>
<gl-form-input <gl-form-input
v-model.trim="selectedColor" v-model.trim="selectedColor"
class="gl-rounded-top-left-none gl-rounded-bottom-left-none" class="gl-rounded-top-left-none gl-rounded-bottom-left-none"
:placeholder="__('Use custom color #FF0000')" :placeholder="__('Use custom color #FF0000')"
data-testid="selected-color-text"
/> />
</div> </div>
</div> </div>
<div class="dropdown-actions clearfix pt-2 px-2"> <div class="dropdown-actions gl-display-flex gl-justify-content-space-between gl-pt-3 gl-px-3">
<gl-button <gl-button
:disabled="disableCreate" :disabled="disableCreate"
category="primary" category="primary"
variant="success" variant="success"
class="float-left d-flex align-items-center" class="gl-display-flex gl-align-items-center"
@click="handleCreateClick" data-testid="create-button"
@click="createLabel"
> >
<gl-loading-icon v-show="labelCreateInProgress" :inline="true" class="mr-1" /> <gl-loading-icon v-if="labelCreateInProgress" :inline="true" class="mr-1" />
{{ __('Create') }} {{ __('Create') }}
</gl-button> </gl-button>
<gl-button class="float-right js-btn-cancel-create" @click="toggleDropdownContentsCreateView"> <gl-button
class="js-btn-cancel-create"
data-testid="cancel-button"
@click="$emit('hideCreateView')"
>
{{ __('Cancel') }} {{ __('Cancel') }}
</gl-button> </gl-button>
</div> </div>
......
<script> <script>
import { import { GlIntersectionObserver, GlLoadingIcon, GlSearchBoxByType, GlLink } from '@gitlab/ui';
GlIntersectionObserver,
GlLoadingIcon,
GlButton,
GlSearchBoxByType,
GlLink,
} from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus'; import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { mapState, mapGetters, mapActions } from 'vuex'; import { mapState, mapGetters, mapActions } from 'vuex';
...@@ -17,7 +11,6 @@ export default { ...@@ -17,7 +11,6 @@ export default {
components: { components: {
GlIntersectionObserver, GlIntersectionObserver,
GlLoadingIcon, GlLoadingIcon,
GlButton,
GlSearchBoxByType, GlSearchBoxByType,
GlLink, GlLink,
LabelItem, LabelItem,
...@@ -149,21 +142,6 @@ export default { ...@@ -149,21 +142,6 @@ export default {
<template> <template>
<gl-intersection-observer @appear="handleComponentAppear" @disappear="handleComponentDisappear"> <gl-intersection-observer @appear="handleComponentAppear" @disappear="handleComponentDisappear">
<div class="labels-select-contents-list js-labels-list" @keydown="handleKeyDown"> <div class="labels-select-contents-list js-labels-list" @keydown="handleKeyDown">
<div
v-if="isDropdownVariantSidebar || isDropdownVariantEmbedded"
class="dropdown-title gl-display-flex gl-align-items-center gl-pt-0 gl-pb-3!"
data-testid="dropdown-title"
>
<span class="flex-grow-1">{{ labelsListTitle }}</span>
<gl-button
:aria-label="__('Close')"
variant="link"
size="small"
class="dropdown-header-button gl-p-0!"
icon="close"
@click="toggleDropdownContents"
/>
</div>
<div class="dropdown-input" @click.stop="() => {}"> <div class="dropdown-input" @click.stop="() => {}">
<gl-search-box-by-type <gl-search-box-by-type
ref="searchInput" ref="searchInput"
......
<script>
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
export default {
directives: {
GlTooltip: GlTooltipDirective,
},
components: {
GlIcon,
},
props: {
labels: {
type: Array,
required: true,
},
},
computed: {
labelsList() {
const labelsString = this.labels.length
? this.labels
.slice(0, 5)
.map((label) => label.title)
.join(', ')
: s__('LabelSelect|Labels');
if (this.labels.length > 5) {
return sprintf(s__('LabelSelect|%{labelsString}, and %{remainingLabelCount} more'), {
labelsString,
remainingLabelCount: this.labels.length - 5,
});
}
return labelsString;
},
},
methods: {
handleClick() {
this.$emit('onValueClick');
},
},
};
</script>
<template>
<div
v-gl-tooltip.left.viewport
:title="labelsList"
class="sidebar-collapsed-icon"
@click="handleClick"
>
<gl-icon name="labels" />
<span>{{ labels.length }}</span>
</div>
</template>
mutation createLabel($title: String!, $color: String, $projectPath: ID, $groupPath: ID) {
labelCreate(
input: { title: $title, color: $color, projectPath: $projectPath, groupPath: $groupPath }
) {
label {
id
color
description
descriptionHtml
title
textColor
}
errors
}
}
...@@ -5,13 +5,12 @@ import Vuex, { mapState, mapActions, mapGetters } from 'vuex'; ...@@ -5,13 +5,12 @@ import Vuex, { mapState, mapActions, mapGetters } from 'vuex';
import { isInViewport } from '~/lib/utils/common_utils'; import { isInViewport } from '~/lib/utils/common_utils';
import { __ } from '~/locale'; import { __ } from '~/locale';
import DropdownValueCollapsed from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed.vue';
import { DropdownVariant } from './constants'; import { DropdownVariant } from './constants';
import DropdownButton from './dropdown_button.vue'; import DropdownButton from './dropdown_button.vue';
import DropdownContents from './dropdown_contents.vue'; import DropdownContents from './dropdown_contents.vue';
import DropdownTitle from './dropdown_title.vue'; import DropdownTitle from './dropdown_title.vue';
import DropdownValue from './dropdown_value.vue'; import DropdownValue from './dropdown_value.vue';
import DropdownValueCollapsed from './dropdown_value_collapsed.vue';
import labelsSelectModule from './store'; import labelsSelectModule from './store';
Vue.use(Vuex); Vue.use(Vuex);
...@@ -163,7 +162,6 @@ export default { ...@@ -163,7 +162,6 @@ export default {
labelsFilterBasePath: this.labelsFilterBasePath, labelsFilterBasePath: this.labelsFilterBasePath,
labelsFilterParam: this.labelsFilterParam, labelsFilterParam: this.labelsFilterParam,
labelsListTitle: this.labelsListTitle, labelsListTitle: this.labelsListTitle,
labelsCreateTitle: this.labelsCreateTitle,
footerCreateLabelTitle: this.footerCreateLabelTitle, footerCreateLabelTitle: this.footerCreateLabelTitle,
footerManageLabelTitle: this.footerManageLabelTitle, footerManageLabelTitle: this.footerManageLabelTitle,
}); });
...@@ -313,6 +311,7 @@ export default { ...@@ -313,6 +311,7 @@ export default {
v-show="dropdownButtonVisible && showDropdownContents" v-show="dropdownButtonVisible && showDropdownContents"
ref="dropdownContents" ref="dropdownContents"
:render-on-top="!contentIsOnViewport" :render-on-top="!contentIsOnViewport"
:labels-create-title="labelsCreateTitle"
/> />
</template> </template>
<template v-if="isDropdownVariantStandalone || isDropdownVariantEmbedded"> <template v-if="isDropdownVariantStandalone || isDropdownVariantEmbedded">
......
...@@ -28,31 +28,5 @@ export const fetchLabels = ({ state, dispatch }) => { ...@@ -28,31 +28,5 @@ export const fetchLabels = ({ state, dispatch }) => {
.catch(() => dispatch('receiveLabelsFailure')); .catch(() => dispatch('receiveLabelsFailure'));
}; };
export const requestCreateLabel = ({ commit }) => commit(types.REQUEST_CREATE_LABEL);
export const receiveCreateLabelSuccess = ({ commit }) => commit(types.RECEIVE_CREATE_LABEL_SUCCESS);
export const receiveCreateLabelFailure = ({ commit }) => {
commit(types.RECEIVE_CREATE_LABEL_FAILURE);
flash(__('Error creating label.'));
};
export const createLabel = ({ state, dispatch }, label) => {
dispatch('requestCreateLabel');
axios
.post(state.labelsManagePath, {
label,
})
.then(({ data }) => {
if (data.id) {
dispatch('receiveCreateLabelSuccess');
dispatch('toggleDropdownContentsCreateView');
} else {
// eslint-disable-next-line @gitlab/require-i18n-strings
throw new Error('Error Creating Label');
}
})
.catch(() => {
dispatch('receiveCreateLabelFailure');
});
};
export const updateSelectedLabels = ({ commit }, labels) => export const updateSelectedLabels = ({ commit }, labels) =>
commit(types.UPDATE_SELECTED_LABELS, { labels }); commit(types.UPDATE_SELECTED_LABELS, { labels });
...@@ -8,10 +8,6 @@ export const REQUEST_SET_LABELS = 'REQUEST_SET_LABELS'; ...@@ -8,10 +8,6 @@ export const REQUEST_SET_LABELS = 'REQUEST_SET_LABELS';
export const RECEIVE_SET_LABELS_SUCCESS = 'RECEIVE_SET_LABELS_SUCCESS'; export const RECEIVE_SET_LABELS_SUCCESS = 'RECEIVE_SET_LABELS_SUCCESS';
export const RECEIVE_SET_LABELS_FAILURE = 'RECEIVE_SET_LABELS_FAILURE'; export const RECEIVE_SET_LABELS_FAILURE = 'RECEIVE_SET_LABELS_FAILURE';
export const REQUEST_CREATE_LABEL = 'REQUEST_CREATE_LABEL';
export const RECEIVE_CREATE_LABEL_SUCCESS = 'RECEIVE_CREATE_LABEL_SUCCESS';
export const RECEIVE_CREATE_LABEL_FAILURE = 'RECEIVE_CREATE_LABEL_FAILURE';
export const TOGGLE_DROPDOWN_BUTTON = 'TOGGLE_DROPDOWN_VISIBILITY'; export const TOGGLE_DROPDOWN_BUTTON = 'TOGGLE_DROPDOWN_VISIBILITY';
export const TOGGLE_DROPDOWN_CONTENTS = 'TOGGLE_DROPDOWN_CONTENTS'; export const TOGGLE_DROPDOWN_CONTENTS = 'TOGGLE_DROPDOWN_CONTENTS';
......
...@@ -46,17 +46,6 @@ export default { ...@@ -46,17 +46,6 @@ export default {
[types.RECEIVE_SET_LABELS_FAILURE](state) { [types.RECEIVE_SET_LABELS_FAILURE](state) {
state.labelsFetchInProgress = false; state.labelsFetchInProgress = false;
}, },
[types.REQUEST_CREATE_LABEL](state) {
state.labelCreateInProgress = true;
},
[types.RECEIVE_CREATE_LABEL_SUCCESS](state) {
state.labelCreateInProgress = false;
},
[types.RECEIVE_CREATE_LABEL_FAILURE](state) {
state.labelCreateInProgress = false;
},
[types.UPDATE_SELECTED_LABELS](state, { labels }) { [types.UPDATE_SELECTED_LABELS](state, { labels }) {
// Find the label to update from all the labels // Find the label to update from all the labels
// and change `set` prop value to represent their current state. // and change `set` prop value to represent their current state.
......
...@@ -3,7 +3,6 @@ export default () => ({ ...@@ -3,7 +3,6 @@ export default () => ({
labels: [], labels: [],
selectedLabels: [], selectedLabels: [],
labelsListTitle: '', labelsListTitle: '',
labelsCreateTitle: '',
footerCreateLabelTitle: '', footerCreateLabelTitle: '',
footerManageLabelTitle: '', footerManageLabelTitle: '',
dropdownButtonText: '', dropdownButtonText: '',
......
import { GlIcon, GlButton } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import DropdownButton from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_button.vue';
import labelSelectModule from '~/vue_shared/components/sidebar/labels_select_widget/store';
import { mockConfig } from './mock_data';
let store;
const localVue = createLocalVue();
localVue.use(Vuex);
const createComponent = (initialState = mockConfig) => {
store = new Vuex.Store(labelSelectModule());
store.dispatch('setInitialState', initialState);
return shallowMount(DropdownButton, {
localVue,
store,
});
};
describe('DropdownButton', () => {
let wrapper;
beforeEach(() => {
wrapper = createComponent();
});
afterEach(() => {
wrapper.destroy();
});
const findDropdownButton = () => wrapper.find(GlButton);
const findDropdownText = () => wrapper.find('.dropdown-toggle-text');
const findDropdownIcon = () => wrapper.find(GlIcon);
describe('methods', () => {
describe('handleButtonClick', () => {
it.each`
variant | expectPropagationStopped
${'standalone'} | ${true}
${'embedded'} | ${false}
`(
'toggles dropdown content and handles event propagation when `state.variant` is "$variant"',
({ variant, expectPropagationStopped }) => {
const event = { stopPropagation: jest.fn() };
wrapper = createComponent({ ...mockConfig, variant });
findDropdownButton().vm.$emit('click', event);
expect(store.state.showDropdownContents).toBe(true);
expect(event.stopPropagation).toHaveBeenCalledTimes(expectPropagationStopped ? 1 : 0);
},
);
});
});
describe('template', () => {
it('renders component container element', () => {
expect(wrapper.find(GlButton).element).toBe(wrapper.element);
});
it('renders default button text element', () => {
const dropdownTextEl = findDropdownText();
expect(dropdownTextEl.exists()).toBe(true);
expect(dropdownTextEl.text()).toBe('Label');
});
it('renders provided button text element', () => {
store.state.dropdownButtonText = 'Custom label';
const dropdownTextEl = findDropdownText();
return wrapper.vm.$nextTick().then(() => {
expect(dropdownTextEl.text()).toBe('Custom label');
});
});
it('renders chevron icon element', () => {
const iconEl = findDropdownIcon();
expect(iconEl.exists()).toBe(true);
expect(iconEl.props('name')).toBe('chevron-down');
});
});
});
import { GlLoadingIcon, GlLink } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import DropdownContentsCreateView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue';
import createLabelMutation from '~/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql';
import { mockSuggestedColors, createLabelSuccessfulResponse } from './mock_data';
jest.mock('~/flash');
const colors = Object.keys(mockSuggestedColors);
const localVue = createLocalVue();
Vue.use(VueApollo);
const userRecoverableError = {
...createLabelSuccessfulResponse,
errors: ['Houston, we have a problem'],
};
const createLabelSuccessHandler = jest.fn().mockResolvedValue(createLabelSuccessfulResponse);
const createLabelUserRecoverableErrorHandler = jest.fn().mockResolvedValue(userRecoverableError);
const createLabelErrorHandler = jest.fn().mockRejectedValue('Houston, we have a problem');
describe('DropdownContentsCreateView', () => {
let wrapper;
const findAllColors = () => wrapper.findAllComponents(GlLink);
const findSelectedColor = () => wrapper.find('[data-testid="selected-color"]');
const findSelectedColorText = () => wrapper.find('[data-testid="selected-color-text"]');
const findCreateButton = () => wrapper.find('[data-testid="create-button"]');
const findCancelButton = () => wrapper.find('[data-testid="cancel-button"]');
const findLabelTitleInput = () => wrapper.find('[data-testid="label-title-input"]');
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const fillLabelAttributes = () => {
findLabelTitleInput().vm.$emit('input', 'Test title');
findAllColors().at(0).vm.$emit('click', new Event('mouseclick'));
};
const createComponent = ({ mutationHandler = createLabelSuccessHandler } = {}) => {
const mockApollo = createMockApollo([[createLabelMutation, mutationHandler]]);
wrapper = shallowMount(DropdownContentsCreateView, {
localVue,
apolloProvider: mockApollo,
});
};
beforeEach(() => {
gon.suggested_label_colors = mockSuggestedColors;
});
afterEach(() => {
wrapper.destroy();
});
it('renders a palette of 21 colors', () => {
createComponent();
expect(findAllColors()).toHaveLength(21);
});
it('selects a color after clicking on colored block', async () => {
createComponent();
expect(findSelectedColor().attributes('style')).toBeUndefined();
findAllColors().at(0).vm.$emit('click', new Event('mouseclick'));
await nextTick();
expect(findSelectedColor().attributes('style')).toBe('background-color: rgb(0, 153, 102);');
});
it('shows correct color hex code after selecting a color', async () => {
createComponent();
expect(findSelectedColorText().attributes('value')).toBe('');
findAllColors().at(0).vm.$emit('click', new Event('mouseclick'));
await nextTick();
expect(findSelectedColorText().attributes('value')).toBe(colors[0]);
});
it('disables a Create button if label title is not set', async () => {
createComponent();
findAllColors().at(0).vm.$emit('click', new Event('mouseclick'));
await nextTick();
expect(findCreateButton().props('disabled')).toBe(true);
});
it('disables a Create button if color is not set', async () => {
createComponent();
findLabelTitleInput().vm.$emit('input', 'Test title');
await nextTick();
expect(findCreateButton().props('disabled')).toBe(true);
});
it('does not render a loader spinner', () => {
createComponent();
expect(findLoadingIcon().exists()).toBe(false);
});
it('emits a `hideCreateView` event on Cancel button click', () => {
createComponent();
findCancelButton().vm.$emit('click');
expect(wrapper.emitted('hideCreateView')).toHaveLength(1);
});
describe('when label title and selected color are set', () => {
beforeEach(() => {
createComponent();
fillLabelAttributes();
});
it('enables a Create button', () => {
expect(findCreateButton().props('disabled')).toBe(false);
});
it('calls a mutation with correct parameters on Create button click', () => {
findCreateButton().vm.$emit('click');
expect(createLabelSuccessHandler).toHaveBeenCalledWith({
color: '#009966',
projectPath: '',
title: 'Test title',
});
});
it('renders a loader spinner after Create button click', async () => {
findCreateButton().vm.$emit('click');
await nextTick();
expect(findLoadingIcon().exists()).toBe(true);
});
it('does not loader spinner after mutation is resolved', async () => {
findCreateButton().vm.$emit('click');
await nextTick();
expect(findLoadingIcon().exists()).toBe(true);
await waitForPromises();
expect(findLoadingIcon().exists()).toBe(false);
});
});
it('calls createFlash is mutation has a user-recoverable error', async () => {
createComponent({ mutationHandler: createLabelUserRecoverableErrorHandler });
fillLabelAttributes();
await nextTick();
findCreateButton().vm.$emit('click');
await waitForPromises();
expect(createFlash).toHaveBeenCalled();
});
it('calls createFlash is mutation was rejected', async () => {
createComponent({ mutationHandler: createLabelErrorHandler });
fillLabelAttributes();
await nextTick();
findCreateButton().vm.$emit('click');
await waitForPromises();
expect(createFlash).toHaveBeenCalled();
});
});
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
import DropdownContents from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue';
import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_widget/store';
import { mockConfig } from './mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
const createComponent = (initialState = mockConfig, defaultProps = {}) => {
const store = new Vuex.Store(labelsSelectModule());
store.dispatch('setInitialState', initialState);
return shallowMount(DropdownContents, {
propsData: {
...defaultProps,
labelsCreateTitle: 'test',
},
localVue,
store,
});
};
describe('DropdownContent', () => {
let wrapper;
beforeEach(() => {
wrapper = createComponent();
});
afterEach(() => {
wrapper.destroy();
});
describe('computed', () => {
describe('dropdownContentsView', () => {
it('returns string "dropdown-contents-create-view" when `showDropdownContentsCreateView` prop is `true`', () => {
wrapper.vm.$store.dispatch('toggleDropdownContentsCreateView');
expect(wrapper.vm.dropdownContentsView).toBe('dropdown-contents-create-view');
});
it('returns string "dropdown-contents-labels-view" when `showDropdownContentsCreateView` prop is `false`', () => {
expect(wrapper.vm.dropdownContentsView).toBe('dropdown-contents-labels-view');
});
});
});
describe('template', () => {
it('renders component container element with class `labels-select-dropdown-contents` and no styles', () => {
expect(wrapper.attributes('class')).toContain('labels-select-dropdown-contents');
expect(wrapper.attributes('style')).toBeUndefined();
});
describe('when `renderOnTop` is true', () => {
it.each`
variant | expected
${DropdownVariant.Sidebar} | ${'bottom: 3rem'}
${DropdownVariant.Standalone} | ${'bottom: 2rem'}
${DropdownVariant.Embedded} | ${'bottom: 2rem'}
`('renders upward for $variant variant', ({ variant, expected }) => {
wrapper = createComponent({ ...mockConfig, variant }, { renderOnTop: true });
expect(wrapper.attributes('style')).toContain(expected);
});
});
});
});
import { GlButton, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import DropdownTitle from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_title.vue';
import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_widget/store';
import { mockConfig } from './mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
const createComponent = (initialState = mockConfig) => {
const store = new Vuex.Store(labelsSelectModule());
store.dispatch('setInitialState', initialState);
return shallowMount(DropdownTitle, {
localVue,
store,
propsData: {
labelsSelectInProgress: false,
},
});
};
describe('DropdownTitle', () => {
let wrapper;
beforeEach(() => {
wrapper = createComponent();
});
afterEach(() => {
wrapper.destroy();
});
describe('template', () => {
it('renders component container element with string "Labels"', () => {
expect(wrapper.text()).toContain('Labels');
});
it('renders edit link', () => {
const editBtnEl = wrapper.find(GlButton);
expect(editBtnEl.exists()).toBe(true);
expect(editBtnEl.text()).toBe('Edit');
});
it('renders loading icon element when `labelsSelectInProgress` prop is true', () => {
wrapper.setProps({
labelsSelectInProgress: true,
});
return wrapper.vm.$nextTick(() => {
expect(wrapper.find(GlLoadingIcon).isVisible()).toBe(true);
});
});
});
});
import { GlLabel } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import DropdownValue from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue';
import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_widget/store';
import { mockConfig, mockRegularLabel, mockScopedLabel } from './mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('DropdownValue', () => {
let wrapper;
const createComponent = (initialState = {}, slots = {}) => {
const store = new Vuex.Store(labelsSelectModule());
store.dispatch('setInitialState', { ...mockConfig, ...initialState });
wrapper = shallowMount(DropdownValue, {
localVue,
store,
slots,
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('methods', () => {
describe('labelFilterUrl', () => {
it('returns a label filter URL based on provided label param', () => {
createComponent();
expect(wrapper.vm.labelFilterUrl(mockRegularLabel)).toBe(
'/gitlab-org/my-project/issues?label_name[]=Foo%20Label',
);
});
});
describe('scopedLabel', () => {
beforeEach(() => {
createComponent();
});
it('returns `true` when provided label param is a scoped label', () => {
expect(wrapper.vm.scopedLabel(mockScopedLabel)).toBe(true);
});
it('returns `false` when provided label param is a regular label', () => {
expect(wrapper.vm.scopedLabel(mockRegularLabel)).toBe(false);
});
});
});
describe('template', () => {
it('renders class `has-labels` on component container element when `selectedLabels` is not empty', () => {
createComponent();
expect(wrapper.attributes('class')).toContain('has-labels');
});
it('renders element containing `None` when `selectedLabels` is empty', () => {
createComponent(
{
selectedLabels: [],
},
{
default: 'None',
},
);
const noneEl = wrapper.find('span.text-secondary');
expect(noneEl.exists()).toBe(true);
expect(noneEl.text()).toBe('None');
});
it('renders labels when `selectedLabels` is not empty', () => {
createComponent();
expect(wrapper.findAll(GlLabel).length).toBe(2);
});
});
});
import { GlIcon, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import LabelItem from '~/vue_shared/components/sidebar/labels_select_widget/label_item.vue';
import { mockRegularLabel } from './mock_data';
const mockLabel = { ...mockRegularLabel, set: true };
const createComponent = ({
label = mockLabel,
isLabelSet = mockLabel.set,
highlight = true,
} = {}) =>
shallowMount(LabelItem, {
propsData: {
label,
isLabelSet,
highlight,
},
});
describe('LabelItem', () => {
let wrapper;
beforeEach(() => {
wrapper = createComponent();
});
afterEach(() => {
wrapper.destroy();
});
describe('template', () => {
it('renders gl-link component', () => {
expect(wrapper.find(GlLink).exists()).toBe(true);
});
it('renders component root with class `is-focused` when `highlight` prop is true', () => {
const wrapperTemp = createComponent({
highlight: true,
});
expect(wrapperTemp.classes()).toContain('is-focused');
wrapperTemp.destroy();
});
it('renders visible gl-icon component when `isLabelSet` prop is true', () => {
const wrapperTemp = createComponent({
isLabelSet: true,
});
const iconEl = wrapperTemp.find(GlIcon);
expect(iconEl.isVisible()).toBe(true);
expect(iconEl.props('name')).toBe('mobile-issue-close');
wrapperTemp.destroy();
});
it('renders visible span element as placeholder instead of gl-icon when `isLabelSet` prop is false', () => {
const wrapperTemp = createComponent({
isLabelSet: false,
});
const placeholderEl = wrapperTemp.find('[data-testid="no-icon"]');
expect(placeholderEl.isVisible()).toBe(true);
wrapperTemp.destroy();
});
it('renders label color element', () => {
const colorEl = wrapper.find('[data-testid="label-color-box"]');
expect(colorEl.exists()).toBe(true);
expect(colorEl.attributes('style')).toBe('background-color: rgb(186, 218, 85);');
});
it('renders label title', () => {
expect(wrapper.text()).toContain(mockLabel.title);
});
});
});
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import { isInViewport } from '~/lib/utils/common_utils';
import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
import DropdownButton from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_button.vue';
import DropdownContents from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue';
import DropdownTitle from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_title.vue';
import DropdownValue from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue';
import DropdownValueCollapsed from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_value_collapsed.vue';
import LabelsSelectRoot from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue';
import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_widget/store';
import { mockConfig } from './mock_data';
jest.mock('~/lib/utils/common_utils', () => ({
isInViewport: jest.fn().mockReturnValue(true),
}));
const localVue = createLocalVue();
localVue.use(Vuex);
describe('LabelsSelectRoot', () => {
let wrapper;
let store;
const createComponent = (config = mockConfig, slots = {}) => {
wrapper = shallowMount(LabelsSelectRoot, {
localVue,
slots,
store,
propsData: config,
stubs: {
'dropdown-contents': DropdownContents,
},
});
};
beforeEach(() => {
store = new Vuex.Store(labelsSelectModule());
});
afterEach(() => {
wrapper.destroy();
});
describe('methods', () => {
describe('handleVuexActionDispatch', () => {
it('calls `handleDropdownClose` when params `action.type` is `toggleDropdownContents` and state has `showDropdownButton` & `showDropdownContents` props `false`', () => {
createComponent();
jest.spyOn(wrapper.vm, 'handleDropdownClose').mockImplementation();
wrapper.vm.handleVuexActionDispatch(
{ type: 'toggleDropdownContents' },
{
showDropdownButton: false,
showDropdownContents: false,
labels: [{ id: 1 }, { id: 2, touched: true }],
},
);
expect(wrapper.vm.handleDropdownClose).toHaveBeenCalledWith(
expect.arrayContaining([
{
id: 2,
touched: true,
},
]),
);
});
it('calls `handleDropdownClose` with state.labels filterd using `set` prop when dropdown variant is `embedded`', () => {
createComponent({
...mockConfig,
variant: 'embedded',
});
jest.spyOn(wrapper.vm, 'handleDropdownClose').mockImplementation();
wrapper.vm.handleVuexActionDispatch(
{ type: 'toggleDropdownContents' },
{
showDropdownButton: false,
showDropdownContents: false,
labels: [{ id: 1 }, { id: 2, set: true }],
},
);
expect(wrapper.vm.handleDropdownClose).toHaveBeenCalledWith(
expect.arrayContaining([
{
id: 2,
set: true,
},
]),
);
});
});
describe('handleDropdownClose', () => {
beforeEach(() => {
createComponent();
});
it('emits `updateSelectedLabels` & `onDropdownClose` events on component when provided `labels` param is not empty', () => {
wrapper.vm.handleDropdownClose([{ id: 1 }, { id: 2 }]);
expect(wrapper.emitted().updateSelectedLabels).toBeTruthy();
expect(wrapper.emitted().onDropdownClose).toBeTruthy();
});
it('emits only `onDropdownClose` event on component when provided `labels` param is empty', () => {
wrapper.vm.handleDropdownClose([]);
expect(wrapper.emitted().updateSelectedLabels).toBeFalsy();
expect(wrapper.emitted().onDropdownClose).toBeTruthy();
});
});
describe('handleCollapsedValueClick', () => {
it('emits `toggleCollapse` event on component', () => {
createComponent();
wrapper.vm.handleCollapsedValueClick();
expect(wrapper.emitted().toggleCollapse).toBeTruthy();
});
});
});
describe('template', () => {
it('renders component with classes `labels-select-wrapper position-relative`', () => {
createComponent();
expect(wrapper.attributes('class')).toContain('labels-select-wrapper position-relative');
});
it.each`
variant | cssClass
${'standalone'} | ${'is-standalone'}
${'embedded'} | ${'is-embedded'}
`(
'renders component root element with CSS class `$cssClass` when `state.variant` is "$variant"',
({ variant, cssClass }) => {
createComponent({
...mockConfig,
variant,
});
return wrapper.vm.$nextTick(() => {
expect(wrapper.classes()).toContain(cssClass);
});
},
);
it('renders `dropdown-value-collapsed` component when `allowLabelCreate` prop is `true`', async () => {
createComponent();
await wrapper.vm.$nextTick;
expect(wrapper.find(DropdownValueCollapsed).exists()).toBe(true);
});
it('renders `dropdown-title` component', async () => {
createComponent();
await wrapper.vm.$nextTick;
expect(wrapper.find(DropdownTitle).exists()).toBe(true);
});
it('renders `dropdown-value` component', async () => {
createComponent(mockConfig, {
default: 'None',
});
await wrapper.vm.$nextTick;
const valueComp = wrapper.find(DropdownValue);
expect(valueComp.exists()).toBe(true);
expect(valueComp.text()).toBe('None');
});
it('renders `dropdown-button` component when `showDropdownButton` prop is `true`', async () => {
createComponent();
wrapper.vm.$store.dispatch('toggleDropdownButton');
await wrapper.vm.$nextTick;
expect(wrapper.find(DropdownButton).exists()).toBe(true);
});
it('renders `dropdown-contents` component when `showDropdownButton` & `showDropdownContents` prop is `true`', async () => {
createComponent();
wrapper.vm.$store.dispatch('toggleDropdownContents');
await wrapper.vm.$nextTick;
expect(wrapper.find(DropdownContents).exists()).toBe(true);
});
describe('sets content direction based on viewport', () => {
describe.each(Object.values(DropdownVariant))(
'when labels variant is "%s"',
({ variant }) => {
beforeEach(() => {
createComponent({ ...mockConfig, variant });
wrapper.vm.$store.dispatch('toggleDropdownContents');
});
it('set direction when out of viewport', () => {
isInViewport.mockImplementation(() => false);
wrapper.vm.setContentIsOnViewport(wrapper.vm.$store.state);
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.find(DropdownContents).props('renderOnTop')).toBe(true);
});
});
it('does not set direction when inside of viewport', () => {
isInViewport.mockImplementation(() => true);
wrapper.vm.setContentIsOnViewport(wrapper.vm.$store.state);
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.find(DropdownContents).props('renderOnTop')).toBe(false);
});
});
},
);
});
});
it('calls toggleDropdownContents action when isEditing prop is changing to true', async () => {
createComponent();
jest.spyOn(store, 'dispatch').mockResolvedValue();
await wrapper.setProps({ isEditing: true });
expect(store.dispatch).toHaveBeenCalledWith('toggleDropdownContents');
});
it('does not call toggleDropdownContents action when isEditing prop is changing to false', async () => {
createComponent();
jest.spyOn(store, 'dispatch').mockResolvedValue();
await wrapper.setProps({ isEditing: false });
expect(store.dispatch).not.toHaveBeenCalled();
});
});
export const mockRegularLabel = {
id: 26,
title: 'Foo Label',
description: 'Foobar',
color: '#BADA55',
textColor: '#FFFFFF',
};
export const mockScopedLabel = {
id: 27,
title: 'Foo::Bar',
description: 'Foobar',
color: '#0033CC',
textColor: '#FFFFFF',
};
export const mockLabels = [
mockRegularLabel,
mockScopedLabel,
{
id: 28,
title: 'Bug',
description: 'Label for bugs',
color: '#FF0000',
textColor: '#FFFFFF',
},
{
id: 29,
title: 'Boog',
description: 'Label for bugs',
color: '#FF0000',
textColor: '#FFFFFF',
},
];
export const mockConfig = {
allowLabelEdit: true,
allowLabelCreate: true,
allowScopedLabels: true,
allowMultiselect: true,
labelsListTitle: 'Assign labels',
labelsCreateTitle: 'Create label',
variant: 'sidebar',
dropdownOnly: false,
selectedLabels: [mockRegularLabel, mockScopedLabel],
labelsSelectInProgress: false,
labelsFetchPath: '/gitlab-org/my-project/-/labels.json',
labelsManagePath: '/gitlab-org/my-project/-/labels',
labelsFilterBasePath: '/gitlab-org/my-project/issues',
labelsFilterParam: 'label_name',
};
export const mockSuggestedColors = {
'#009966': 'Green-cyan',
'#8fbc8f': 'Dark sea green',
'#3cb371': 'Medium sea green',
'#00b140': 'Green screen',
'#013220': 'Dark green',
'#6699cc': 'Blue-gray',
'#0000ff': 'Blue',
'#e6e6fa': 'Lavendar',
'#9400d3': 'Dark violet',
'#330066': 'Deep violet',
'#808080': 'Gray',
'#36454f': 'Charcoal grey',
'#f7e7ce': 'Champagne',
'#c21e56': 'Rose red',
'#cc338b': 'Magenta-pink',
'#dc143c': 'Crimson',
'#ff0000': 'Red',
'#cd5b45': 'Dark coral',
'#eee600': 'Titanium yellow',
'#ed9121': 'Carrot orange',
'#c39953': 'Aztec Gold',
};
export const createLabelSuccessfulResponse = {
data: {
labelCreate: {
label: {
id: 'gid://gitlab/ProjectLabel/126',
color: '#dc143c',
description: null,
descriptionHtml: '',
title: 'ewrwrwer',
textColor: '#FFFFFF',
__typename: 'Label',
},
errors: [],
__typename: 'LabelCreatePayload',
},
},
};
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import axios from '~/lib/utils/axios_utils';
import * as actions from '~/vue_shared/components/sidebar/labels_select_widget/store/actions';
import * as types from '~/vue_shared/components/sidebar/labels_select_widget/store/mutation_types';
import defaultState from '~/vue_shared/components/sidebar/labels_select_widget/store/state';
describe('LabelsSelect Actions', () => {
let state;
const mockInitialState = {
labels: [],
selectedLabels: [],
};
beforeEach(() => {
state = { ...defaultState() };
});
describe('setInitialState', () => {
it('sets initial store state', (done) => {
testAction(
actions.setInitialState,
mockInitialState,
state,
[{ type: types.SET_INITIAL_STATE, payload: mockInitialState }],
[],
done,
);
});
});
describe('toggleDropdownButton', () => {
it('toggles dropdown button', (done) => {
testAction(
actions.toggleDropdownButton,
{},
state,
[{ type: types.TOGGLE_DROPDOWN_BUTTON }],
[],
done,
);
});
});
describe('toggleDropdownContents', () => {
it('toggles dropdown contents', (done) => {
testAction(
actions.toggleDropdownContents,
{},
state,
[{ type: types.TOGGLE_DROPDOWN_CONTENTS }],
[],
done,
);
});
});
describe('toggleDropdownContentsCreateView', () => {
it('toggles dropdown create view', (done) => {
testAction(
actions.toggleDropdownContentsCreateView,
{},
state,
[{ type: types.TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW }],
[],
done,
);
});
});
describe('requestLabels', () => {
it('sets value of `state.labelsFetchInProgress` to `true`', (done) => {
testAction(actions.requestLabels, {}, state, [{ type: types.REQUEST_LABELS }], [], done);
});
});
describe('receiveLabelsSuccess', () => {
it('sets provided labels to `state.labels`', (done) => {
const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
testAction(
actions.receiveLabelsSuccess,
labels,
state,
[{ type: types.RECEIVE_SET_LABELS_SUCCESS, payload: labels }],
[],
done,
);
});
});
describe('receiveLabelsFailure', () => {
beforeEach(() => {
setFixtures('<div class="flash-container"></div>');
});
it('sets value `state.labelsFetchInProgress` to `false`', (done) => {
testAction(
actions.receiveLabelsFailure,
{},
state,
[{ type: types.RECEIVE_SET_LABELS_FAILURE }],
[],
done,
);
});
it('shows flash error', () => {
actions.receiveLabelsFailure({ commit: () => {} });
expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
'Error fetching labels.',
);
});
});
describe('fetchLabels', () => {
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
state.labelsFetchPath = 'labels.json';
});
afterEach(() => {
mock.restore();
});
describe('on success', () => {
it('dispatches `requestLabels` & `receiveLabelsSuccess` actions', (done) => {
const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
mock.onGet(/labels.json/).replyOnce(200, labels);
testAction(
actions.fetchLabels,
{},
state,
[],
[{ type: 'requestLabels' }, { type: 'receiveLabelsSuccess', payload: labels }],
done,
);
});
});
describe('on failure', () => {
it('dispatches `requestLabels` & `receiveLabelsFailure` actions', (done) => {
mock.onGet(/labels.json/).replyOnce(500, {});
testAction(
actions.fetchLabels,
{},
state,
[],
[{ type: 'requestLabels' }, { type: 'receiveLabelsFailure' }],
done,
);
});
});
});
describe('updateSelectedLabels', () => {
it('updates `state.labels` based on provided `labels` param', (done) => {
const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
testAction(
actions.updateSelectedLabels,
labels,
state,
[{ type: types.UPDATE_SELECTED_LABELS, payload: { labels } }],
[],
done,
);
});
});
});
import * as getters from '~/vue_shared/components/sidebar/labels_select_widget/store/getters';
describe('LabelsSelect Getters', () => {
describe('dropdownButtonText', () => {
it.each`
labelType | dropdownButtonText | expected
${'default'} | ${''} | ${'Label'}
${'custom'} | ${'Custom label'} | ${'Custom label'}
`(
'returns $labelType text when state.labels has no selected labels',
({ dropdownButtonText, expected }) => {
const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
const selectedLabels = [];
const state = { labels, selectedLabels, dropdownButtonText };
expect(getters.dropdownButtonText(state, {})).toBe(expected);
},
);
it('returns label title when state.labels has only 1 label', () => {
const labels = [{ id: 1, title: 'Foobar', set: true }];
expect(getters.dropdownButtonText({ labels }, { isDropdownVariantSidebar: true })).toBe(
'Foobar',
);
});
it('returns first label title and remaining labels count when state.labels has more than 1 label', () => {
const labels = [
{ id: 1, title: 'Foo', set: true },
{ id: 2, title: 'Bar', set: true },
];
expect(getters.dropdownButtonText({ labels }, { isDropdownVariantSidebar: true })).toBe(
'Foo +1 more',
);
});
});
describe('selectedLabelsList', () => {
it('returns array of IDs of all labels within `state.selectedLabels`', () => {
const selectedLabels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
expect(getters.selectedLabelsList({ selectedLabels })).toEqual([1, 2, 3, 4]);
});
});
describe('isDropdownVariantSidebar', () => {
it('returns `true` when `state.variant` is "sidebar"', () => {
expect(getters.isDropdownVariantSidebar({ variant: 'sidebar' })).toBe(true);
});
});
describe('isDropdownVariantStandalone', () => {
it('returns `true` when `state.variant` is "standalone"', () => {
expect(getters.isDropdownVariantStandalone({ variant: 'standalone' })).toBe(true);
});
});
});
import * as types from '~/vue_shared/components/sidebar/labels_select_widget/store/mutation_types';
import mutations from '~/vue_shared/components/sidebar/labels_select_widget/store/mutations';
describe('LabelsSelect Mutations', () => {
describe(`${types.SET_INITIAL_STATE}`, () => {
it('initializes provided props to store state', () => {
const state = {};
mutations[types.SET_INITIAL_STATE](state, {
labels: 'foo',
});
expect(state.labels).toEqual('foo');
});
});
describe(`${types.TOGGLE_DROPDOWN_BUTTON}`, () => {
it('toggles value of `state.showDropdownButton`', () => {
const state = {
showDropdownButton: false,
};
mutations[types.TOGGLE_DROPDOWN_BUTTON](state);
expect(state.showDropdownButton).toBe(true);
});
});
describe(`${types.TOGGLE_DROPDOWN_CONTENTS}`, () => {
it('toggles value of `state.showDropdownButton` when `state.dropdownOnly` is false', () => {
const state = {
dropdownOnly: false,
showDropdownButton: false,
variant: 'sidebar',
};
mutations[types.TOGGLE_DROPDOWN_CONTENTS](state);
expect(state.showDropdownButton).toBe(true);
});
it('toggles value of `state.showDropdownContents`', () => {
const state = {
showDropdownContents: false,
};
mutations[types.TOGGLE_DROPDOWN_CONTENTS](state);
expect(state.showDropdownContents).toBe(true);
});
it('sets value of `state.showDropdownContentsCreateView` to `false` when `showDropdownContents` is true', () => {
const state = {
showDropdownContents: false,
showDropdownContentsCreateView: true,
};
mutations[types.TOGGLE_DROPDOWN_CONTENTS](state);
expect(state.showDropdownContentsCreateView).toBe(false);
});
});
describe(`${types.TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW}`, () => {
it('toggles value of `state.showDropdownContentsCreateView`', () => {
const state = {
showDropdownContentsCreateView: false,
};
mutations[types.TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW](state);
expect(state.showDropdownContentsCreateView).toBe(true);
});
});
describe(`${types.REQUEST_LABELS}`, () => {
it('sets value of `state.labelsFetchInProgress` to true', () => {
const state = {
labelsFetchInProgress: false,
};
mutations[types.REQUEST_LABELS](state);
expect(state.labelsFetchInProgress).toBe(true);
});
});
describe(`${types.RECEIVE_SET_LABELS_SUCCESS}`, () => {
const selectedLabels = [{ id: 2 }, { id: 4 }];
const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
it('sets value of `state.labelsFetchInProgress` to false', () => {
const state = {
selectedLabels,
labelsFetchInProgress: true,
};
mutations[types.RECEIVE_SET_LABELS_SUCCESS](state, labels);
expect(state.labelsFetchInProgress).toBe(false);
});
it('sets provided `labels` to `state.labels` along with `set` prop based on `state.selectedLabels`', () => {
const selectedLabelIds = selectedLabels.map((label) => label.id);
const state = {
selectedLabels,
labelsFetchInProgress: true,
};
mutations[types.RECEIVE_SET_LABELS_SUCCESS](state, labels);
state.labels.forEach((label) => {
if (selectedLabelIds.includes(label.id)) {
expect(label.set).toBe(true);
}
});
});
});
describe(`${types.RECEIVE_SET_LABELS_FAILURE}`, () => {
it('sets value of `state.labelsFetchInProgress` to false', () => {
const state = {
labelsFetchInProgress: true,
};
mutations[types.RECEIVE_SET_LABELS_FAILURE](state);
expect(state.labelsFetchInProgress).toBe(false);
});
});
describe(`${types.UPDATE_SELECTED_LABELS}`, () => {
const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
it('updates `state.labels` to include `touched` and `set` props based on provided `labels` param', () => {
const updatedLabelIds = [2];
const state = {
labels,
};
mutations[types.UPDATE_SELECTED_LABELS](state, { labels: [{ id: 2 }] });
state.labels.forEach((label) => {
if (updatedLabelIds.includes(label.id)) {
expect(label.touched).toBe(true);
expect(label.set).toBe(true);
}
});
});
});
});
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