Commit 764ef548 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '332260-refactor-dropdownbutton-dropdowncontents-to-gldropdown' into 'master'

Refactor labels dropdown to GlDropdown

See merge request gitlab-org/gitlab!69263
parents 7641233f e51b3eed
...@@ -259,6 +259,7 @@ export function mountSidebarLabels() { ...@@ -259,6 +259,7 @@ export function mountSidebarLabels() {
initiallySelectedLabels: JSON.parse(el.dataset.selectedLabels), initiallySelectedLabels: JSON.parse(el.dataset.selectedLabels),
variant: DropdownVariant.Sidebar, variant: DropdownVariant.Sidebar,
canUpdate: parseBoolean(el.dataset.canEdit), canUpdate: parseBoolean(el.dataset.canEdit),
isClassicSidebar: true,
}, },
render: (createElement) => createElement(SidebarLabels), render: (createElement) => createElement(SidebarLabels),
}); });
......
<script>
import { GlButton, GlIcon } from '@gitlab/ui';
import { mapActions, mapGetters } from 'vuex';
export default {
components: {
GlButton,
GlIcon,
},
computed: {
...mapGetters([
'dropdownButtonText',
'isDropdownVariantStandalone',
'isDropdownVariantEmbedded',
]),
},
methods: {
...mapActions(['toggleDropdownContents']),
handleButtonClick(e) {
if (this.isDropdownVariantStandalone || this.isDropdownVariantEmbedded) {
this.toggleDropdownContents();
}
if (this.isDropdownVariantStandalone) {
e.stopPropagation();
}
},
},
};
</script>
<template>
<gl-button
class="labels-select-dropdown-button js-dropdown-button w-100 text-left"
@click="handleButtonClick"
>
<span class="dropdown-toggle-text gl-pointer-events-none flex-fill">
{{ dropdownButtonText }}
</span>
<gl-icon name="chevron-down" class="gl-pointer-events-none float-right" />
</gl-button>
</template>
<script> <script>
import { GlButton } from '@gitlab/ui'; import { GlButton, GlDropdown, GlDropdownItem, GlLink } from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex'; import { mapActions, mapGetters, mapState } from 'vuex';
import DropdownContentsCreateView from './dropdown_contents_create_view.vue'; import DropdownContentsCreateView from './dropdown_contents_create_view.vue';
...@@ -10,13 +10,12 @@ export default { ...@@ -10,13 +10,12 @@ export default {
DropdownContentsLabelsView, DropdownContentsLabelsView,
DropdownContentsCreateView, DropdownContentsCreateView,
GlButton, GlButton,
GlDropdown,
GlDropdownItem,
GlLink,
}, },
inject: ['allowLabelCreate', 'labelsManagePath'],
props: { props: {
renderOnTop: {
type: Boolean,
required: false,
default: false,
},
labelsCreateTitle: { labelsCreateTitle: {
type: String, type: String,
required: true, required: true,
...@@ -44,67 +43,90 @@ export default { ...@@ -44,67 +43,90 @@ export default {
}, },
computed: { computed: {
...mapState(['showDropdownContentsCreateView']), ...mapState(['showDropdownContentsCreateView']),
...mapGetters(['isDropdownVariantSidebar', 'isDropdownVariantEmbedded']), ...mapGetters(['dropdownButtonText', 'isDropdownVariantSidebar', 'isDropdownVariantEmbedded']),
dropdownContentsView() { dropdownContentsView() {
if (this.showDropdownContentsCreateView) { if (this.showDropdownContentsCreateView) {
return 'dropdown-contents-create-view'; return 'dropdown-contents-create-view';
} }
return 'dropdown-contents-labels-view'; return 'dropdown-contents-labels-view';
}, },
directionStyle() {
const bottom = this.isDropdownVariantSidebar ? '3rem' : '2rem';
return this.renderOnTop ? { bottom } : {};
},
dropdownTitle() { dropdownTitle() {
return this.showDropdownContentsCreateView ? this.labelsCreateTitle : this.labelsListTitle; return this.showDropdownContentsCreateView ? this.labelsCreateTitle : this.labelsListTitle;
}, },
showDropdownFooter() {
return (
!this.showDropdownContentsCreateView &&
(this.isDropdownVariantSidebar || this.isDropdownVariantEmbedded)
);
},
}, },
methods: { methods: {
...mapActions(['toggleDropdownContentsCreateView']), ...mapActions(['toggleDropdownContentsCreateView']),
showDropdown() {
this.$refs.dropdown.show();
},
toggleDropdownContent() {
this.toggleDropdownContentsCreateView();
// Required to recalculate dropdown position as its size changes
this.$refs.dropdown.$refs.dropdown.$_popper.scheduleUpdate();
},
}, },
}; };
</script> </script>
<template> <template>
<div <gl-dropdown
class="labels-select-dropdown-contents gl-w-full gl-my-2 gl-py-3 gl-rounded-base gl-absolute" ref="dropdown"
:text="dropdownButtonText"
class="gl-w-full gl-mt-2"
data-qa-selector="labels_dropdown_content" data-qa-selector="labels_dropdown_content"
:style="directionStyle"
> >
<div <template #header>
v-if="isDropdownVariantSidebar || isDropdownVariantEmbedded" <div
class="dropdown-title gl-display-flex gl-align-items-center gl-pt-0 gl-pb-3!" v-if="isDropdownVariantSidebar || isDropdownVariantEmbedded"
data-testid="dropdown-title" class="dropdown-title gl-display-flex gl-align-items-center gl-pt-0 gl-pb-3!"
> >
<gl-button <gl-button
v-if="showDropdownContentsCreateView" v-if="showDropdownContentsCreateView"
:aria-label="__('Go back')" :aria-label="__('Go back')"
variant="link" variant="link"
size="small" size="small"
class="js-btn-back dropdown-header-button p-0" class="js-btn-back dropdown-header-button gl-p-0"
icon="arrow-left" icon="arrow-left"
@click.stop="toggleDropdownContentsCreateView" data-testid="go-back-button"
/> @click.stop="toggleDropdownContent"
<span class="flex-grow-1">{{ dropdownTitle }}</span> />
<gl-button <span class="gl-flex-grow-1">{{ dropdownTitle }}</span>
:aria-label="__('Close')" <gl-button
variant="link" :aria-label="__('Close')"
size="small" variant="link"
class="dropdown-header-button gl-p-0!" size="small"
icon="close" class="dropdown-header-button gl-p-0!"
@click="$emit('closeDropdown')" icon="close"
/> @click="$emit('closeDropdown')"
</div> />
</div>
</template>
<component <component
:is="dropdownContentsView" :is="dropdownContentsView"
:selected-labels="selectedLabels" :selected-labels="selectedLabels"
:allow-multiselect="allowMultiselect" :allow-multiselect="allowMultiselect"
:labels-list-title="labelsListTitle"
:footer-create-label-title="footerCreateLabelTitle"
:footer-manage-label-title="footerManageLabelTitle"
@hideCreateView="toggleDropdownContentsCreateView" @hideCreateView="toggleDropdownContentsCreateView"
@setLabels="$emit('setLabels', $event)" @setLabels="$emit('setLabels', $event)"
@toggleDropdownContentsCreateView="toggleDropdownContentsCreateView"
/> />
</div> <template #footer>
<div v-if="showDropdownFooter" data-testid="dropdown-footer">
<gl-dropdown-item
v-if="allowLabelCreate"
data-testid="create-label-button"
@click.native.capture.stop="toggleDropdownContent"
>
{{ footerCreateLabelTitle }}
</gl-dropdown-item>
<gl-dropdown-item :href="labelsManagePath">
{{ footerManageLabelTitle }}
</gl-dropdown-item>
</div>
</template>
</gl-dropdown>
</template> </template>
<script> <script>
import { GlLoadingIcon, GlSearchBoxByType, GlLink } from '@gitlab/ui'; import { GlDropdownForm, GlDropdownItem, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus'; import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { DropdownVariant } from './constants';
import projectLabelsQuery from './graphql/project_labels.query.graphql'; import projectLabelsQuery from './graphql/project_labels.query.graphql';
import LabelItem from './label_item.vue'; import LabelItem from './label_item.vue';
export default { export default {
components: { components: {
GlDropdownForm,
GlDropdownItem,
GlLoadingIcon, GlLoadingIcon,
GlSearchBoxByType, GlSearchBoxByType,
GlLink,
LabelItem, LabelItem,
}, },
inject: ['projectPath', 'allowLabelCreate', 'labelsManagePath', 'variant'], inject: ['projectPath'],
props: { props: {
selectedLabels: { selectedLabels: {
type: Array, type: Array,
...@@ -28,18 +27,6 @@ export default { ...@@ -28,18 +27,6 @@ export default {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
labelsListTitle: {
type: String,
required: true,
},
footerCreateLabelTitle: {
type: String,
required: true,
},
footerManageLabelTitle: {
type: String,
required: true,
},
}, },
data() { data() {
return { return {
...@@ -74,12 +61,6 @@ export default { ...@@ -74,12 +61,6 @@ export default {
}, },
}, },
computed: { computed: {
isDropdownVariantSidebar() {
return this.variant === DropdownVariant.Sidebar;
},
isDropdownVariantEmbedded() {
return this.variant === DropdownVariant.Embedded;
},
labelsFetchInProgress() { labelsFetchInProgress() {
return this.$apollo.queries.labels.loading; return this.$apollo.queries.labels.loading;
}, },
...@@ -150,37 +131,10 @@ export default { ...@@ -150,37 +131,10 @@ export default {
}); });
} }
}, },
/**
* This method enables keyboard navigation support for
* the dropdown.
*/
handleKeyDown(e) {
if (e.keyCode === UP_KEY_CODE && this.currentHighlightItem > 0) {
this.currentHighlightItem -= 1;
} else if (
e.keyCode === DOWN_KEY_CODE &&
this.currentHighlightItem < this.visibleLabels.length - 1
) {
this.currentHighlightItem += 1;
} else if (e.keyCode === ENTER_KEY_CODE && this.currentHighlightItem > -1) {
this.updateSelectedLabels(this.visibleLabels[this.currentHighlightItem]);
this.searchKey = '';
} else if (e.keyCode === ESC_KEY_CODE) {
this.$emit('setLabels', this.localSelectedLabels);
}
if (e.keyCode !== ESC_KEY_CODE) {
// Scroll the list only after highlighting
// styles are rendered completely.
this.$nextTick(() => {
this.scrollIntoViewIfNeeded();
});
}
},
handleLabelClick(label) { handleLabelClick(label) {
this.updateSelectedLabels(label); this.updateSelectedLabels(label);
if (!this.allowMultiselect) { if (!this.allowMultiselect) {
this.$emit('setLabels', this.localSelectedLabels); this.$emit('closeDropdown', this.localSelectedLabels);
} }
}, },
setSearchKey(value) { setSearchKey(value) {
...@@ -191,69 +145,42 @@ export default { ...@@ -191,69 +145,42 @@ export default {
</script> </script>
<template> <template>
<div <gl-dropdown-form class="labels-select-contents-list js-labels-list">
class="labels-select-contents-list js-labels-list" <gl-search-box-by-type
data-testid="dropdown-wrapper" ref="searchInput"
@keydown="handleKeyDown" :value="searchKey"
> :disabled="labelsFetchInProgress"
<div class="dropdown-input" @click.stop="() => {}"> data-qa-selector="dropdown_input_field"
<gl-search-box-by-type data-testid="dropdown-input-field"
ref="searchInput" @input="debouncedSearchKeyUpdate"
:value="searchKey" />
:disabled="labelsFetchInProgress" <div ref="labelsListContainer" data-testid="dropdown-content">
data-qa-selector="dropdown_input_field"
data-testid="dropdown-input-field"
@input="debouncedSearchKeyUpdate"
/>
</div>
<div ref="labelsListContainer" class="dropdown-content" data-testid="dropdown-content">
<gl-loading-icon <gl-loading-icon
v-if="labelsFetchInProgress" v-if="labelsFetchInProgress"
class="labels-fetch-loading gl-align-items-center gl-w-full gl-h-full" class="labels-fetch-loading gl-align-items-center gl-w-full gl-h-full"
size="md" size="md"
/> />
<ul v-else class="list-unstyled gl-mb-0 gl-word-break-word" data-testid="labels-list"> <template v-else>
<label-item <gl-dropdown-item
v-for="(label, index) in visibleLabels" v-for="(label, index) in visibleLabels"
:key="label.id" :key="label.id"
:label="label" data-testid="labels-list"
:is-label-set="isLabelSelected(label)" @click.native.capture.stop="handleLabelClick(label)"
:highlight="index === currentHighlightItem" >
@clickLabel="handleLabelClick(label)" <label-item
/> :label="label"
<li :is-label-set="isLabelSelected(label)"
:highlight="index === currentHighlightItem"
/>
</gl-dropdown-item>
<gl-dropdown-item
v-show="showNoMatchingResultsMessage" v-show="showNoMatchingResultsMessage"
class="gl-p-3 gl-text-center" class="gl-p-3 gl-text-center"
data-testid="no-results" data-testid="no-results"
> >
{{ __('No matching results') }} {{ __('No matching results') }}
</li> </gl-dropdown-item>
</ul> </template>
</div>
<div
v-if="isDropdownVariantSidebar || isDropdownVariantEmbedded"
class="dropdown-footer"
data-testid="dropdown-footer"
>
<ul class="list-unstyled">
<li v-if="allowLabelCreate">
<gl-link
class="gl-display-flex gl-flex-direction-row gl-w-full gl-overflow-break-word label-item"
data-testid="create-label-button"
@click.stop="$emit('toggleDropdownContentsCreateView')"
>
{{ footerCreateLabelTitle }}
</gl-link>
</li>
<li>
<gl-link
:href="labelsManagePath"
class="gl-display-flex gl-flex-direction-row gl-w-full gl-overflow-break-word label-item"
>
{{ footerManageLabelTitle }}
</gl-link>
</li>
</ul>
</div> </div>
</div> </gl-dropdown-form>
</template> </template>
...@@ -18,7 +18,7 @@ export default { ...@@ -18,7 +18,7 @@ export default {
default: false, default: false,
}, },
}, },
render(h, { props, listeners }) { render(h, { props }) {
const { label, highlight, isLabelSet } = props; const { label, highlight, isLabelSet } = props;
const labelColorBox = h('span', { const labelColorBox = h('span', {
...@@ -53,18 +53,7 @@ export default { ...@@ -53,18 +53,7 @@ export default {
const labelTitle = h('span', label.title); const labelTitle = h('span', label.title);
const labelLink = h( const labelLink = h(GlLink, [noIcon, checkedIcon, labelColorBox, labelTitle]);
GlLink,
{
class: 'gl-display-flex gl-align-items-center label-item gl-text-black-normal',
on: {
click: () => {
listeners.clickLabel(label);
},
},
},
[noIcon, checkedIcon, labelColorBox, labelTitle],
);
return h( return h(
'li', 'li',
......
<script> <script>
import Vue from 'vue'; import Vue from 'vue';
import Vuex, { mapState, mapActions, mapGetters } from 'vuex'; import Vuex, { mapActions, mapGetters } from 'vuex';
import { isInViewport } from '~/lib/utils/common_utils';
import { __ } from '~/locale'; import { __ } from '~/locale';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue'; import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import { DropdownVariant } from './constants'; import { DropdownVariant } from './constants';
import DropdownButton from './dropdown_button.vue';
import DropdownContents from './dropdown_contents.vue'; import DropdownContents from './dropdown_contents.vue';
import DropdownValue from './dropdown_value.vue'; import DropdownValue from './dropdown_value.vue';
import DropdownValueCollapsed from './dropdown_value_collapsed.vue'; import DropdownValueCollapsed from './dropdown_value_collapsed.vue';
...@@ -18,7 +16,6 @@ export default { ...@@ -18,7 +16,6 @@ export default {
store: new Vuex.Store(labelsSelectModule()), store: new Vuex.Store(labelsSelectModule()),
components: { components: {
DropdownValue, DropdownValue,
DropdownButton,
DropdownContents, DropdownContents,
DropdownValueCollapsed, DropdownValueCollapsed,
SidebarEditableItem, SidebarEditableItem,
...@@ -137,7 +134,6 @@ export default { ...@@ -137,7 +134,6 @@ export default {
}, },
}, },
computed: { computed: {
...mapState(['showDropdownContents']),
...mapGetters([ ...mapGetters([
'isDropdownVariantSidebar', 'isDropdownVariantSidebar',
'isDropdownVariantStandalone', 'isDropdownVariantStandalone',
...@@ -150,9 +146,6 @@ export default { ...@@ -150,9 +146,6 @@ export default {
selectedLabels, selectedLabels,
}); });
}, },
showDropdownContents(showDropdownContents) {
this.setContentIsOnViewport(showDropdownContents);
},
isEditing(newVal) { isEditing(newVal) {
if (newVal) { if (newVal) {
this.toggleDropdownContents(); this.toggleDropdownContents();
...@@ -190,11 +183,9 @@ export default { ...@@ -190,11 +183,9 @@ export default {
handleCollapsedValueClick() { handleCollapsedValueClick() {
this.$emit('toggleCollapse'); this.$emit('toggleCollapse');
}, },
setContentIsOnViewport() { showDropdown() {
this.$nextTick(() => { this.$nextTick(() => {
if (this.$refs.dropdownContents) { this.$refs.dropdownContents.showDropdown();
this.contentIsOnViewport = isInViewport(this.$refs.dropdownContents.$el);
}
}); });
}, },
}, },
...@@ -219,8 +210,7 @@ export default { ...@@ -219,8 +210,7 @@ export default {
ref="editable" ref="editable"
:title="__('Labels')" :title="__('Labels')"
:loading="labelsSelectInProgress" :loading="labelsSelectInProgress"
@open="setContentIsOnViewport" @open="showDropdown"
@close="contentIsOnViewport = true"
> >
<template #collapsed> <template #collapsed>
<dropdown-value <dropdown-value
...@@ -248,7 +238,6 @@ export default { ...@@ -248,7 +238,6 @@ export default {
> >
<slot></slot> <slot></slot>
</dropdown-value> </dropdown-value>
<dropdown-button />
<dropdown-contents <dropdown-contents
v-if="edit" v-if="edit"
ref="dropdownContents" ref="dropdownContents"
...@@ -256,7 +245,6 @@ export default { ...@@ -256,7 +245,6 @@ export default {
:labels-list-title="labelsListTitle" :labels-list-title="labelsListTitle"
:footer-create-label-title="footerCreateLabelTitle" :footer-create-label-title="footerCreateLabelTitle"
:footer-manage-label-title="footerManageLabelTitle" :footer-manage-label-title="footerManageLabelTitle"
:render-on-top="!contentIsOnViewport"
:labels-create-title="labelsCreateTitle" :labels-create-title="labelsCreateTitle"
:selected-labels="selectedLabels" :selected-labels="selectedLabels"
@closeDropdown="collapseDropdown" @closeDropdown="collapseDropdown"
......
...@@ -15,6 +15,7 @@ RSpec.describe "Issues > User edits issue", :js do ...@@ -15,6 +15,7 @@ RSpec.describe "Issues > User edits issue", :js do
context 'with authorized user' do context 'with authorized user' do
before do before do
stub_feature_flags(labels_widget: false)
project.add_developer(user) project.add_developer(user)
project_with_milestones.add_developer(user) project_with_milestones.add_developer(user)
sign_in(user) sign_in(user)
......
...@@ -17,6 +17,7 @@ RSpec.describe 'Labels Hierarchy', :js do ...@@ -17,6 +17,7 @@ RSpec.describe 'Labels Hierarchy', :js do
let!(:project_label_1) { create(:label, project: project_1, title: 'Label_4') } let!(:project_label_1) { create(:label, project: project_1, title: 'Label_4') }
before do before do
stub_feature_flags(labels_widget: false)
grandparent.add_owner(user) grandparent.add_owner(user)
sign_in(user) sign_in(user)
......
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');
});
});
});
...@@ -45,8 +45,6 @@ describe('DropdownContentsLabelsView', () => { ...@@ -45,8 +45,6 @@ describe('DropdownContentsLabelsView', () => {
provide: { provide: {
projectPath: 'test', projectPath: 'test',
iid: 1, iid: 1,
allowLabelCreate: true,
labelsManagePath: '/gitlab-org/my-project/-/labels',
variant: DropdownVariant.Sidebar, variant: DropdownVariant.Sidebar,
...injected, ...injected,
}, },
...@@ -69,10 +67,7 @@ describe('DropdownContentsLabelsView', () => { ...@@ -69,10 +67,7 @@ describe('DropdownContentsLabelsView', () => {
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findLabelsList = () => wrapper.find('[data-testid="labels-list"]'); const findLabelsList = () => wrapper.find('[data-testid="labels-list"]');
const findDropdownWrapper = () => wrapper.find('[data-testid="dropdown-wrapper"]');
const findDropdownFooter = () => wrapper.find('[data-testid="dropdown-footer"]');
const findNoResultsMessage = () => wrapper.find('[data-testid="no-results"]'); const findNoResultsMessage = () => wrapper.find('[data-testid="no-results"]');
const findCreateLabelButton = () => wrapper.find('[data-testid="create-label-button"]');
describe('when loading labels', () => { describe('when loading labels', () => {
it('renders disabled search input field', async () => { it('renders disabled search input field', async () => {
...@@ -109,34 +104,6 @@ describe('DropdownContentsLabelsView', () => { ...@@ -109,34 +104,6 @@ describe('DropdownContentsLabelsView', () => {
expect(findLabelsList().exists()).toBe(true); expect(findLabelsList().exists()).toBe(true);
expect(findLabels()).toHaveLength(2); expect(findLabels()).toHaveLength(2);
}); });
it('changes highlighted label correctly on pressing down button', async () => {
expect(findLabels().at(0).attributes('highlight')).toBeUndefined();
await findDropdownWrapper().trigger('keydown.down');
expect(findLabels().at(0).attributes('highlight')).toBe('true');
await findDropdownWrapper().trigger('keydown.down');
expect(findLabels().at(1).attributes('highlight')).toBe('true');
expect(findLabels().at(0).attributes('highlight')).toBeUndefined();
});
it('changes highlighted label correctly on pressing up button', async () => {
await findDropdownWrapper().trigger('keydown.down');
await findDropdownWrapper().trigger('keydown.down');
expect(findLabels().at(1).attributes('highlight')).toBe('true');
await findDropdownWrapper().trigger('keydown.up');
expect(findLabels().at(0).attributes('highlight')).toBe('true');
});
it('changes label selected state when Enter is pressed', async () => {
expect(findLabels().at(0).attributes('islabelset')).toBeUndefined();
await findDropdownWrapper().trigger('keydown.down');
await findDropdownWrapper().trigger('keydown.enter');
expect(findLabels().at(0).attributes('islabelset')).toBe('true');
});
}); });
it('when search returns 0 results', async () => { it('when search returns 0 results', async () => {
...@@ -164,44 +131,4 @@ describe('DropdownContentsLabelsView', () => { ...@@ -164,44 +131,4 @@ describe('DropdownContentsLabelsView', () => {
await waitForPromises(); await waitForPromises();
expect(createFlash).toHaveBeenCalled(); expect(createFlash).toHaveBeenCalled();
}); });
it('does not render footer on standalone dropdown', () => {
createComponent({ injected: { variant: DropdownVariant.Standalone } });
expect(findDropdownFooter().exists()).toBe(false);
});
it('renders footer on sidebar dropdown', () => {
createComponent();
expect(findDropdownFooter().exists()).toBe(true);
});
it('renders footer on embedded dropdown', () => {
createComponent({ injected: { variant: DropdownVariant.Embedded } });
expect(findDropdownFooter().exists()).toBe(true);
});
it('does not render create label button if `allowLabelCreate` is false', () => {
createComponent({ injected: { allowLabelCreate: false } });
expect(findCreateLabelButton().exists()).toBe(false);
});
describe('when `allowLabelCreate` is true', () => {
beforeEach(() => {
createComponent();
});
it('renders create label button', () => {
expect(findCreateLabelButton().exists()).toBe(true);
});
it('emits `toggleDropdownContentsCreateView` event on create label button click', () => {
findCreateLabelButton().vm.$emit('click', new MouseEvent('click'));
expect(wrapper.emitted('toggleDropdownContentsCreateView')).toEqual([[]]);
});
});
}); });
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { GlDropdown } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_widget/constants'; 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 DropdownContents from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue';
import DropdownContentsCreateView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue';
import DropdownContentsLabelsView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue';
import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_widget/store'; import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_widget/store';
import { mockConfig, mockLabels } from './mock_data'; import { mockConfig, mockLabels } from './mock_data';
const localVue = createLocalVue(); Vue.use(Vuex);
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',
selectedLabels: mockLabels,
allowMultiselect: true,
labelsListTitle: 'Assign labels',
footerCreateLabelTitle: 'create',
footerManageLabelTitle: 'manage',
},
localVue,
store,
});
};
describe('DropdownContent', () => { describe('DropdownContent', () => {
let wrapper; let wrapper;
const createComponent = ({
initialState = mockConfig,
defaultProps = {},
injected = {},
} = {}) => {
const store = new Vuex.Store(labelsSelectModule());
store.dispatch('setInitialState', initialState);
wrapper = shallowMount(DropdownContents, {
propsData: {
...defaultProps,
labelsCreateTitle: 'test',
selectedLabels: mockLabels,
allowMultiselect: true,
labelsListTitle: 'Assign labels',
footerCreateLabelTitle: 'create',
footerManageLabelTitle: 'manage',
},
provide: {
allowLabelCreate: true,
labelsManagePath: 'foo/bar',
...injected,
},
store,
stubs: {
GlDropdown,
},
});
};
beforeEach(() => { beforeEach(() => {
wrapper = createComponent(); createComponent();
}); });
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
}); });
describe('computed', () => { const findDropdownFooter = () => wrapper.find('[data-testid="dropdown-footer"]');
describe('dropdownContentsView', () => { const findCreateLabelButton = () => wrapper.find('[data-testid="create-label-button"]');
it('returns string "dropdown-contents-create-view" when `showDropdownContentsCreateView` prop is `true`', () => { const findGoBackButton = () => wrapper.find('[data-testid="go-back-button"]');
wrapper.vm.$store.dispatch('toggleDropdownContentsCreateView');
expect(wrapper.vm.dropdownContentsView).toBe('dropdown-contents-create-view'); describe('Create view', () => {
}); beforeEach(() => {
wrapper.vm.$store.dispatch('toggleDropdownContentsCreateView');
});
it('returns string "dropdown-contents-labels-view" when `showDropdownContentsCreateView` prop is `false`', () => { it('renders create view when `showDropdownContentsCreateView` prop is `true`', () => {
expect(wrapper.vm.dropdownContentsView).toBe('dropdown-contents-labels-view'); expect(wrapper.findComponent(DropdownContentsCreateView).exists()).toBe(true);
}); });
it('does not render footer', () => {
expect(findDropdownFooter().exists()).toBe(false);
});
it('does not render create label button', () => {
expect(findCreateLabelButton().exists()).toBe(false);
});
it('renders go back button', () => {
expect(findGoBackButton().exists()).toBe(true);
}); });
}); });
describe('template', () => { describe('Labels view', () => {
it('renders component container element with class `labels-select-dropdown-contents` and no styles', () => { it('renders labels view when `showDropdownContentsCreateView` when `showDropdownContentsCreateView` prop is `false`', () => {
expect(wrapper.attributes('class')).toContain('labels-select-dropdown-contents'); expect(wrapper.findComponent(DropdownContentsLabelsView).exists()).toBe(true);
expect(wrapper.attributes('style')).toBeUndefined(); });
it('renders footer on sidebar dropdown', () => {
expect(findDropdownFooter().exists()).toBe(true);
});
it('does not render footer on standalone dropdown', () => {
createComponent({ initialState: { ...mockConfig, variant: DropdownVariant.Standalone } });
expect(findDropdownFooter().exists()).toBe(false);
});
it('renders footer on embedded dropdown', () => {
createComponent({ initialState: { ...mockConfig, variant: DropdownVariant.Embedded } });
expect(findDropdownFooter().exists()).toBe(true);
}); });
describe('when `renderOnTop` is true', () => { it('does not render go back button', () => {
it.each` expect(findGoBackButton().exists()).toBe(false);
variant | expected });
${DropdownVariant.Sidebar} | ${'bottom: 3rem'}
${DropdownVariant.Standalone} | ${'bottom: 2rem'} it('does not render create label button if `allowLabelCreate` is false', () => {
${DropdownVariant.Embedded} | ${'bottom: 2rem'} createComponent({ injected: { allowLabelCreate: false } });
`('renders upward for $variant variant', ({ variant, expected }) => {
wrapper = createComponent({ ...mockConfig, variant }, { renderOnTop: true });
expect(wrapper.attributes('style')).toContain(expected); expect(findCreateLabelButton().exists()).toBe(false);
});
describe('when `allowLabelCreate` is true', () => {
beforeEach(() => {
createComponent();
}); });
it('renders create label button', () => {
expect(findCreateLabelButton().exists()).toBe(true);
});
it('triggers `toggleDropdownContent` method on create label button click', () => {
jest.spyOn(wrapper.vm, 'toggleDropdownContent').mockImplementation(() => {});
findCreateLabelButton().trigger('click');
expect(wrapper.vm.toggleDropdownContent).toHaveBeenCalled();
});
});
});
describe('template', () => {
it('renders component container element with classes `gl-w-full gl-mt-2` and no styles', () => {
expect(wrapper.attributes('class')).toContain('gl-w-full gl-mt-2');
expect(wrapper.attributes('style')).toBeUndefined();
}); });
}); });
}); });
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