diff options
Diffstat (limited to 'spec/frontend/vue_shared/components')
29 files changed, 438 insertions, 749 deletions
diff --git a/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js b/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js new file mode 100644 index 00000000000..016fe1f131e --- /dev/null +++ b/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js @@ -0,0 +1,97 @@ +import { GlDropdown, GlSearchBoxByType, GlDropdownItem } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; + +import DropdownWidget from '~/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue'; + +describe('DropdownWidget component', () => { + let wrapper; + + const findDropdown = () => wrapper.findComponent(GlDropdown); + const findDropdownItems = () => wrapper.findAll(GlDropdownItem); + const findSearch = () => wrapper.findComponent(GlSearchBoxByType); + + const createComponent = ({ props = {} } = {}) => { + wrapper = shallowMount(DropdownWidget, { + propsData: { + options: [ + { + id: '1', + title: 'Option 1', + }, + { + id: '2', + title: 'Option 2', + }, + ], + ...props, + }, + stubs: { + GlDropdown, + }, + }); + + // We need to mock out `showDropdown` which + // invokes `show` method of BDropdown used inside GlDropdown. + // Context: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54895#note_524281679 + jest.spyOn(wrapper.vm, 'showDropdown').mockImplementation(); + }; + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it('passes default selectText prop to dropdown', () => { + expect(findDropdown().props('text')).toBe('Select'); + }); + + describe('when dropdown is open', () => { + beforeEach(async () => { + findDropdown().vm.$emit('show'); + await wrapper.vm.$nextTick(); + }); + + it('emits search event when typing in search box', () => { + const searchTerm = 'searchTerm'; + findSearch().vm.$emit('input', searchTerm); + + expect(wrapper.emitted('set-search')).toEqual([[searchTerm]]); + }); + + it('renders one selectable item per passed option', async () => { + expect(findDropdownItems()).toHaveLength(2); + }); + + it('emits set-option event when clicking on an option', async () => { + wrapper + .findAll('[data-testid="unselected-option"]') + .at(1) + .vm.$emit('click', new Event('click')); + await wrapper.vm.$nextTick(); + + expect(wrapper.emitted('set-option')).toEqual([[wrapper.props().options[1]]]); + }); + }); + + describe('when options are users', () => { + const mockUser = { + id: 1, + name: 'User name', + username: 'username', + avatarUrl: 'foo/bar', + }; + + beforeEach(() => { + createComponent({ props: { options: [mockUser] } }); + }); + + it('passes user related props to dropdown item', () => { + expect(findDropdownItems().at(0).props('avatarUrl')).toBe(mockUser.avatarUrl); + expect(findDropdownItems().at(0).props('secondaryText')).toBe(mockUser.username); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js index 9fa9d35e3e2..8e931aebfe0 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js @@ -32,6 +32,9 @@ jest.mock('~/vue_shared/components/filtered_search_bar/filtered_search_utils', ( stripQuotes: jest.requireActual( '~/vue_shared/components/filtered_search_bar/filtered_search_utils', ).stripQuotes, + filterEmptySearchTerm: jest.requireActual( + '~/vue_shared/components/filtered_search_bar/filtered_search_utils', + ).filterEmptySearchTerm, })); const createComponent = ({ diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js index 74f579e77ed..d3e1bfef561 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js @@ -86,7 +86,7 @@ describe('AuthorToken', () => { }); describe('methods', () => { - describe('fetchAuthorBySearchTerm', () => { + describe('fetchAuthors', () => { beforeEach(() => { wrapper = createComponent(); }); @@ -155,7 +155,7 @@ describe('AuthorToken', () => { expect(baseTokenEl.exists()).toBe(true); expect(baseTokenEl.props()).toMatchObject({ suggestions: mockAuthors, - fnActiveTokenValue: wrapper.vm.getActiveAuthor, + getActiveTokenValue: wrapper.vm.getActiveAuthor, }); }); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js index cd6ffd679d0..eb1dbed52cc 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js @@ -5,7 +5,7 @@ import { mockLabels, } from 'jest/vue_shared/components/sidebar/labels_select_vue/mock_data'; -import { DEFAULT_LABELS } from '~/vue_shared/components/filtered_search_bar/constants'; +import { DEFAULT_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants'; import { getRecentlyUsedSuggestions, setTokenValueToRecentlyUsed, @@ -51,9 +51,8 @@ const mockProps = { active: false, suggestions: [], suggestionsLoading: false, - defaultSuggestions: DEFAULT_LABELS, + defaultSuggestions: DEFAULT_NONE_ANY, recentSuggestionsStorageKey: mockStorageKey, - fnCurrentTokenValue: jest.fn(), }; function createComponent({ @@ -99,31 +98,20 @@ describe('BaseToken', () => { }); describe('computed', () => { - describe('currentTokenValue', () => { - it('calls `fnCurrentTokenValue` when it is provided', () => { - // We're disabling lint to trigger computed prop execution for this test. - // eslint-disable-next-line no-unused-vars - const { currentTokenValue } = wrapper.vm; - - expect(wrapper.vm.fnCurrentTokenValue).toHaveBeenCalledWith(`"${mockRegularLabel.title}"`); - }); - }); - describe('activeTokenValue', () => { - it('calls `fnActiveTokenValue` when it is provided', async () => { - const mockFnActiveTokenValue = jest.fn(); + it('calls `getActiveTokenValue` when it is provided', async () => { + const mockGetActiveTokenValue = jest.fn(); wrapper.setProps({ - fnActiveTokenValue: mockFnActiveTokenValue, - fnCurrentTokenValue: undefined, + getActiveTokenValue: mockGetActiveTokenValue, }); await wrapper.vm.$nextTick(); - expect(mockFnActiveTokenValue).toHaveBeenCalledTimes(1); - expect(mockFnActiveTokenValue).toHaveBeenCalledWith( + expect(mockGetActiveTokenValue).toHaveBeenCalledTimes(1); + expect(mockGetActiveTokenValue).toHaveBeenCalledWith( mockLabels, - `"${mockRegularLabel.title.toLowerCase()}"`, + `"${mockRegularLabel.title}"`, ); }); }); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js index 331c9c2c14d..09eac636cae 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js @@ -61,40 +61,16 @@ describe('BranchToken', () => { wrapper.destroy(); }); - describe('computed', () => { - beforeEach(async () => { - wrapper = createComponent({ value: { data: mockBranches[0].name } }); - - wrapper.setData({ - branches: mockBranches, - }); - - await wrapper.vm.$nextTick(); - }); - - describe('currentValue', () => { - it('returns lowercase string for `value.data`', () => { - expect(wrapper.vm.currentValue).toBe('main'); - }); - }); - - describe('activeBranch', () => { - it('returns object for currently present `value.data`', () => { - expect(wrapper.vm.activeBranch).toEqual(mockBranches[0]); - }); - }); - }); - describe('methods', () => { beforeEach(() => { wrapper = createComponent(); }); - describe('fetchBranchBySearchTerm', () => { + describe('fetchBranches', () => { it('calls `config.fetchBranches` with provided searchTerm param', () => { jest.spyOn(wrapper.vm.config, 'fetchBranches'); - wrapper.vm.fetchBranchBySearchTerm('foo'); + wrapper.vm.fetchBranches('foo'); expect(wrapper.vm.config.fetchBranches).toHaveBeenCalledWith('foo'); }); @@ -102,7 +78,7 @@ describe('BranchToken', () => { it('sets response to `branches` when request is succesful', () => { jest.spyOn(wrapper.vm.config, 'fetchBranches').mockResolvedValue({ data: mockBranches }); - wrapper.vm.fetchBranchBySearchTerm('foo'); + wrapper.vm.fetchBranches('foo'); return waitForPromises().then(() => { expect(wrapper.vm.branches).toEqual(mockBranches); @@ -112,7 +88,7 @@ describe('BranchToken', () => { it('calls `createFlash` with flash error message when request fails', () => { jest.spyOn(wrapper.vm.config, 'fetchBranches').mockRejectedValue({}); - wrapper.vm.fetchBranchBySearchTerm('foo'); + wrapper.vm.fetchBranches('foo'); return waitForPromises().then(() => { expect(createFlash).toHaveBeenCalledWith({ @@ -124,7 +100,7 @@ describe('BranchToken', () => { it('sets `loading` to false when request completes', () => { jest.spyOn(wrapper.vm.config, 'fetchBranches').mockRejectedValue({}); - wrapper.vm.fetchBranchBySearchTerm('foo'); + wrapper.vm.fetchBranches('foo'); return waitForPromises().then(() => { expect(wrapper.vm.loading).toBe(false); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js index 778a214f97e..c2d61fd9f05 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js @@ -67,40 +67,16 @@ describe('EmojiToken', () => { wrapper.destroy(); }); - describe('computed', () => { - beforeEach(async () => { - wrapper = createComponent({ value: { data: mockEmojis[0].name } }); - - wrapper.setData({ - emojis: mockEmojis, - }); - - await wrapper.vm.$nextTick(); - }); - - describe('currentValue', () => { - it('returns lowercase string for `value.data`', () => { - expect(wrapper.vm.currentValue).toBe(mockEmojis[0].name); - }); - }); - - describe('activeEmoji', () => { - it('returns object for currently present `value.data`', () => { - expect(wrapper.vm.activeEmoji).toEqual(mockEmojis[0]); - }); - }); - }); - describe('methods', () => { beforeEach(() => { wrapper = createComponent(); }); - describe('fetchEmojiBySearchTerm', () => { + describe('fetchEmojis', () => { it('calls `config.fetchEmojis` with provided searchTerm param', () => { jest.spyOn(wrapper.vm.config, 'fetchEmojis'); - wrapper.vm.fetchEmojiBySearchTerm('foo'); + wrapper.vm.fetchEmojis('foo'); expect(wrapper.vm.config.fetchEmojis).toHaveBeenCalledWith('foo'); }); @@ -108,7 +84,7 @@ describe('EmojiToken', () => { it('sets response to `emojis` when request is successful', () => { jest.spyOn(wrapper.vm.config, 'fetchEmojis').mockResolvedValue(mockEmojis); - wrapper.vm.fetchEmojiBySearchTerm('foo'); + wrapper.vm.fetchEmojis('foo'); return waitForPromises().then(() => { expect(wrapper.vm.emojis).toEqual(mockEmojis); @@ -118,7 +94,7 @@ describe('EmojiToken', () => { it('calls `createFlash` with flash error message when request fails', () => { jest.spyOn(wrapper.vm.config, 'fetchEmojis').mockRejectedValue({}); - wrapper.vm.fetchEmojiBySearchTerm('foo'); + wrapper.vm.fetchEmojis('foo'); return waitForPromises().then(() => { expect(createFlash).toHaveBeenCalledWith({ @@ -130,7 +106,7 @@ describe('EmojiToken', () => { it('sets `loading` to false when request completes', () => { jest.spyOn(wrapper.vm.config, 'fetchEmojis').mockRejectedValue({}); - wrapper.vm.fetchEmojiBySearchTerm('foo'); + wrapper.vm.fetchEmojis('foo'); return waitForPromises().then(() => { expect(wrapper.vm.loading).toBe(false); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/iteration_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/iteration_token_spec.js index bd654c5a9cb..a609aaa1c4e 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/iteration_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/iteration_token_spec.js @@ -1,5 +1,6 @@ import { GlFilteredSearchToken, GlFilteredSearchTokenSegment } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; +import waitForPromises from 'helpers/wait_for_promises'; import createFlash from '~/flash'; import IterationToken from '~/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue'; import { mockIterationToken } from '../mock_data'; @@ -13,6 +14,7 @@ describe('IterationToken', () => { const createComponent = ({ config = mockIterationToken, value = { data: '' } } = {}) => mount(IterationToken, { propsData: { + active: false, config, value, }, @@ -69,7 +71,7 @@ describe('IterationToken', () => { config: { ...mockIterationToken, fetchIterations: fetchIterationsSpy }, }); - await wrapper.vm.$nextTick(); + await waitForPromises(); expect(createFlash).toHaveBeenCalledWith({ message: 'There was a problem fetching iterations.', diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js index ec9458f64d2..a348344b9dd 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js @@ -13,10 +13,7 @@ import { import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; -import { - DEFAULT_LABELS, - DEFAULT_NONE_ANY, -} from '~/vue_shared/components/filtered_search_bar/constants'; +import { DEFAULT_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants'; import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue'; import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue'; @@ -98,11 +95,11 @@ describe('LabelToken', () => { }); }); - describe('fetchLabelBySearchTerm', () => { + describe('fetchLabels', () => { it('calls `config.fetchLabels` with provided searchTerm param', () => { jest.spyOn(wrapper.vm.config, 'fetchLabels'); - wrapper.vm.fetchLabelBySearchTerm('foo'); + wrapper.vm.fetchLabels('foo'); expect(wrapper.vm.config.fetchLabels).toHaveBeenCalledWith('foo'); }); @@ -110,7 +107,7 @@ describe('LabelToken', () => { it('sets response to `labels` when request is succesful', () => { jest.spyOn(wrapper.vm.config, 'fetchLabels').mockResolvedValue(mockLabels); - wrapper.vm.fetchLabelBySearchTerm('foo'); + wrapper.vm.fetchLabels('foo'); return waitForPromises().then(() => { expect(wrapper.vm.labels).toEqual(mockLabels); @@ -120,7 +117,7 @@ describe('LabelToken', () => { it('calls `createFlash` with flash error message when request fails', () => { jest.spyOn(wrapper.vm.config, 'fetchLabels').mockRejectedValue({}); - wrapper.vm.fetchLabelBySearchTerm('foo'); + wrapper.vm.fetchLabels('foo'); return waitForPromises().then(() => { expect(createFlash).toHaveBeenCalledWith({ @@ -132,7 +129,7 @@ describe('LabelToken', () => { it('sets `loading` to false when request completes', () => { jest.spyOn(wrapper.vm.config, 'fetchLabels').mockRejectedValue({}); - wrapper.vm.fetchLabelBySearchTerm('foo'); + wrapper.vm.fetchLabels('foo'); return waitForPromises().then(() => { expect(wrapper.vm.loading).toBe(false); @@ -160,7 +157,7 @@ describe('LabelToken', () => { expect(baseTokenEl.exists()).toBe(true); expect(baseTokenEl.props()).toMatchObject({ suggestions: mockLabels, - fnActiveTokenValue: wrapper.vm.getActiveLabel, + getActiveTokenValue: wrapper.vm.getActiveLabel, }); }); @@ -208,7 +205,7 @@ describe('LabelToken', () => { expect(wrapper.find(GlDropdownDivider).exists()).toBe(false); }); - it('renders `DEFAULT_LABELS` as default suggestions', () => { + it('renders `DEFAULT_NONE_ANY` as default suggestions', () => { wrapper = createComponent({ active: true, config: { ...mockLabelToken }, @@ -220,8 +217,8 @@ describe('LabelToken', () => { const suggestions = wrapper.findAll(GlFilteredSearchSuggestion); - expect(suggestions).toHaveLength(DEFAULT_LABELS.length); - DEFAULT_LABELS.forEach((label, index) => { + expect(suggestions).toHaveLength(DEFAULT_NONE_ANY.length); + DEFAULT_NONE_ANY.forEach((label, index) => { expect(suggestions.at(index).text()).toBe(label.text); }); }); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js index 74ceb03bb96..529844817d3 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js @@ -14,12 +14,7 @@ import { sortMilestonesByDueDate } from '~/milestones/milestone_utils'; import { DEFAULT_MILESTONES } from '~/vue_shared/components/filtered_search_bar/constants'; import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue'; -import { - mockMilestoneToken, - mockMilestones, - mockRegularMilestone, - mockEscapedMilestone, -} from '../mock_data'; +import { mockMilestoneToken, mockMilestones, mockRegularMilestone } from '../mock_data'; jest.mock('~/flash'); jest.mock('~/milestones/milestone_utils'); @@ -70,37 +65,12 @@ describe('MilestoneToken', () => { wrapper.destroy(); }); - describe('computed', () => { - beforeEach(async () => { - // Milestone title with spaces is always enclosed in quotations by component. - wrapper = createComponent({ value: { data: `"${mockEscapedMilestone.title}"` } }); - - wrapper.setData({ - milestones: mockMilestones, - }); - - await wrapper.vm.$nextTick(); - }); - - describe('currentValue', () => { - it('returns lowercase string for `value.data`', () => { - expect(wrapper.vm.currentValue).toBe('"5.0 rc1"'); - }); - }); - - describe('activeMilestone', () => { - it('returns object for currently present `value.data`', () => { - expect(wrapper.vm.activeMilestone).toEqual(mockEscapedMilestone); - }); - }); - }); - describe('methods', () => { - describe('fetchMilestoneBySearchTerm', () => { + describe('fetchMilestones', () => { it('calls `config.fetchMilestones` with provided searchTerm param', () => { jest.spyOn(wrapper.vm.config, 'fetchMilestones'); - wrapper.vm.fetchMilestoneBySearchTerm('foo'); + wrapper.vm.fetchMilestones('foo'); expect(wrapper.vm.config.fetchMilestones).toHaveBeenCalledWith('foo'); }); @@ -110,7 +80,7 @@ describe('MilestoneToken', () => { data: mockMilestones, }); - wrapper.vm.fetchMilestoneBySearchTerm(); + wrapper.vm.fetchMilestones(); return waitForPromises().then(() => { expect(wrapper.vm.milestones).toEqual(mockMilestones); @@ -121,7 +91,7 @@ describe('MilestoneToken', () => { it('calls `createFlash` with flash error message when request fails', () => { jest.spyOn(wrapper.vm.config, 'fetchMilestones').mockRejectedValue({}); - wrapper.vm.fetchMilestoneBySearchTerm('foo'); + wrapper.vm.fetchMilestones('foo'); return waitForPromises().then(() => { expect(createFlash).toHaveBeenCalledWith({ @@ -133,7 +103,7 @@ describe('MilestoneToken', () => { it('sets `loading` to false when request completes', () => { jest.spyOn(wrapper.vm.config, 'fetchMilestones').mockRejectedValue({}); - wrapper.vm.fetchMilestoneBySearchTerm('foo'); + wrapper.vm.fetchMilestones('foo'); return waitForPromises().then(() => { expect(wrapper.vm.loading).toBe(false); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/weight_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/weight_token_spec.js index 9a72be636cd..e788c742736 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/weight_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/weight_token_spec.js @@ -12,6 +12,7 @@ describe('WeightToken', () => { const createComponent = ({ config = mockWeightToken, value = { data: '' } } = {}) => mount(WeightToken, { propsData: { + active: false, config, value, }, diff --git a/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js b/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js index 8738924f717..6ab828efebe 100644 --- a/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js +++ b/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js @@ -144,23 +144,6 @@ describe('RelatedIssuableItem', () => { expect(wrapper.find(IssueDueDate).props('closed')).toBe(true); }); - - it('should not contain the `.text-danger` css class for overdue issue that is closed', async () => { - mountComponent({ - props: { - ...props, - closedAt: '2018-12-01T00:00:00.00Z', - }, - }); - await wrapper.vm.$nextTick(); - - expect(wrapper.find(IssueDueDate).find('.board-card-info-icon').classes('text-danger')).toBe( - false, - ); - expect(wrapper.find(IssueDueDate).find('.board-card-info-text').classes('text-danger')).toBe( - false, - ); - }); }); describe('token assignees', () => { diff --git a/spec/frontend/vue_shared/components/markdown/toolbar_button_spec.js b/spec/frontend/vue_shared/components/markdown/toolbar_button_spec.js index 786dfabb990..19e4f2d8c92 100644 --- a/spec/frontend/vue_shared/components/markdown/toolbar_button_spec.js +++ b/spec/frontend/vue_shared/components/markdown/toolbar_button_spec.js @@ -1,3 +1,4 @@ +import { GlButton } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import ToolbarButton from '~/vue_shared/components/markdown/toolbar_button.vue'; @@ -25,7 +26,7 @@ describe('toolbar_button', () => { }); const getButtonShortcutsAttr = () => { - return wrapper.find('button').attributes('data-md-shortcuts'); + return wrapper.find(GlButton).attributes('data-md-shortcuts'); }; describe('keyboard shortcuts', () => { diff --git a/spec/frontend/vue_shared/components/papa_parse_alert_spec.js b/spec/frontend/vue_shared/components/papa_parse_alert_spec.js new file mode 100644 index 00000000000..9be2de17d01 --- /dev/null +++ b/spec/frontend/vue_shared/components/papa_parse_alert_spec.js @@ -0,0 +1,44 @@ +import { GlAlert } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; +import PapaParseAlert from '~/vue_shared/components/papa_parse_alert.vue'; + +describe('app/assets/javascripts/vue_shared/components/papa_parse_alert.vue', () => { + let wrapper; + + const createComponent = ({ errorMessages } = {}) => { + wrapper = shallowMount(PapaParseAlert, { + propsData: { + papaParseErrors: errorMessages, + }, + }); + }; + + const findAlert = () => wrapper.findComponent(GlAlert); + + afterEach(() => { + wrapper.destroy(); + }); + + it('should render alert with correct props', async () => { + createComponent({ errorMessages: [{ code: 'MissingQuotes' }] }); + await nextTick; + + expect(findAlert().props()).toMatchObject({ + variant: 'danger', + }); + expect(findAlert().text()).toContain( + 'Failed to render the CSV file for the following reasons:', + ); + expect(findAlert().text()).toContain('Quoted field unterminated'); + }); + + it('should render original message if no translation available', async () => { + createComponent({ + errorMessages: [{ code: 'NotDefined', message: 'Error code is undefined' }], + }); + await nextTick; + + expect(findAlert().text()).toContain('Error code is undefined'); + }); +}); diff --git a/spec/frontend/vue_shared/components/remove_member_modal_spec.js b/spec/frontend/vue_shared/components/remove_member_modal_spec.js deleted file mode 100644 index ce9de28d53c..00000000000 --- a/spec/frontend/vue_shared/components/remove_member_modal_spec.js +++ /dev/null @@ -1,110 +0,0 @@ -import { GlModal } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import OncallSchedulesList from '~/vue_shared/components/oncall_schedules_list.vue'; -import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue'; - -const mockSchedules = JSON.stringify({ - schedules: [ - { - id: 1, - name: 'Schedule 1', - }, - ], - name: 'User1', -}); - -describe('RemoveMemberModal', () => { - const memberPath = '/gitlab-org/gitlab-test/-/project_members/90'; - let wrapper; - - const findForm = () => wrapper.find({ ref: 'form' }); - const findGlModal = () => wrapper.findComponent(GlModal); - const findOnCallSchedulesList = () => wrapper.findComponent(OncallSchedulesList); - - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - - describe.each` - state | memberType | isAccessRequest | isInvite | actionText | removeSubMembershipsCheckboxExpected | unassignIssuablesCheckboxExpected | message | onCallSchedules - ${'removing a group member'} | ${'GroupMember'} | ${false} | ${'false'} | ${'Remove member'} | ${true} | ${true} | ${'Are you sure you want to remove Jane Doe from the Gitlab Org / Gitlab Test project?'} | ${`{}`} - ${'removing a project member'} | ${'ProjectMember'} | ${false} | ${'false'} | ${'Remove member'} | ${false} | ${true} | ${'Are you sure you want to remove Jane Doe from the Gitlab Org / Gitlab Test project?'} | ${mockSchedules} - ${'denying an access request'} | ${'ProjectMember'} | ${true} | ${'false'} | ${'Deny access request'} | ${false} | ${false} | ${"Are you sure you want to deny Jane Doe's request to join the Gitlab Org / Gitlab Test project?"} | ${`{}`} - ${'revoking invite'} | ${'ProjectMember'} | ${false} | ${'true'} | ${'Revoke invite'} | ${false} | ${false} | ${'Are you sure you want to revoke the invitation for foo@bar.com to join the Gitlab Org / Gitlab Test project?'} | ${mockSchedules} - `( - 'when $state', - ({ - actionText, - memberType, - isAccessRequest, - isInvite, - message, - removeSubMembershipsCheckboxExpected, - unassignIssuablesCheckboxExpected, - onCallSchedules, - }) => { - beforeEach(() => { - wrapper = shallowMount(RemoveMemberModal, { - data() { - return { - modalData: { - isAccessRequest, - isInvite, - message, - memberPath, - memberType, - onCallSchedules, - }, - }; - }, - }); - }); - - const parsedSchedules = JSON.parse(onCallSchedules); - const isPartOfOncallSchedules = Boolean(isAccessRequest && parsedSchedules.schedules?.length); - - it(`has the title ${actionText}`, () => { - expect(findGlModal().attributes('title')).toBe(actionText); - }); - - it('contains a form action', () => { - expect(findForm().attributes('action')).toBe(memberPath); - }); - - it('displays a message to the user', () => { - expect(wrapper.find('[data-testid=modal-message]').text()).toBe(message); - }); - - it(`shows ${ - removeSubMembershipsCheckboxExpected ? 'a' : 'no' - } checkbox to remove direct memberships of subgroups/projects`, () => { - expect(wrapper.find('[name=remove_sub_memberships]').exists()).toBe( - removeSubMembershipsCheckboxExpected, - ); - }); - - it(`shows ${ - unassignIssuablesCheckboxExpected ? 'a' : 'no' - } checkbox to allow removal from related issues and MRs`, () => { - expect(wrapper.find('[name=unassign_issuables]').exists()).toBe( - unassignIssuablesCheckboxExpected, - ); - }); - - it(`shows ${isPartOfOncallSchedules ? 'all' : 'no'} related on-call schedules`, () => { - expect(findOnCallSchedulesList().exists()).toBe(isPartOfOncallSchedules); - }); - - it('submits the form when the modal is submitted', () => { - const spy = jest.spyOn(findForm().element, 'submit'); - - findGlModal().vm.$emit('primary'); - - expect(spy).toHaveBeenCalled(); - - spy.mockRestore(); - }); - }, - ); -}); diff --git a/spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js b/spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js index 395c74dcba6..71ebe561def 100644 --- a/spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js +++ b/spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js @@ -13,7 +13,7 @@ import { REPORT_TYPE_SAST, REPORT_TYPE_SECRET_DETECTION, } from '~/vue_shared/security_reports/constants'; -import securityReportMergeRequestDownloadPathsQuery from '~/vue_shared/security_reports/queries/security_report_merge_request_download_paths.query.graphql'; +import securityReportMergeRequestDownloadPathsQuery from '~/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql'; jest.mock('~/flash'); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js index 06ea88c09a0..a1942e59571 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js @@ -116,6 +116,8 @@ describe('DropdownContentsLabelsView', () => { }); describe('methods', () => { + const fakePreventDefault = jest.fn(); + describe('isLabelSelected', () => { it('returns true when provided `label` param is one of the selected labels', () => { expect(wrapper.vm.isLabelSelected(mockRegularLabel)).toBe(true); @@ -191,9 +193,11 @@ describe('DropdownContentsLabelsView', () => { wrapper.vm.handleKeyDown({ keyCode: ENTER_KEY_CODE, + preventDefault: fakePreventDefault, }); expect(wrapper.vm.searchKey).toBe(''); + expect(fakePreventDefault).toHaveBeenCalled(); }); it('calls action `updateSelectedLabels` with currently highlighted label when Enter key is pressed', () => { @@ -204,6 +208,7 @@ describe('DropdownContentsLabelsView', () => { wrapper.vm.handleKeyDown({ keyCode: ENTER_KEY_CODE, + preventDefault: fakePreventDefault, }); expect(wrapper.vm.updateSelectedLabels).toHaveBeenCalledWith([ diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js index be849789667..bc1ec8b812b 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js @@ -238,4 +238,14 @@ describe('LabelsSelectRoot', () => { expect(store.dispatch).not.toHaveBeenCalled(); }); + + it('calls updateLabelsSetState after selected labels were updated', async () => { + createComponent(); + + jest.spyOn(store, 'dispatch').mockResolvedValue(); + await wrapper.setProps({ selectedLabels: [] }); + jest.advanceTimersByTime(100); + + expect(store.dispatch).toHaveBeenCalledWith('updateLabelsSetState'); + }); }); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js index 46ade5d5857..2e4c056df61 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js @@ -214,7 +214,7 @@ describe('LabelsSelect Actions', () => { }); describe('on success', () => { - it('dispatches `requestCreateLabel`, `receiveCreateLabelSuccess` & `toggleDropdownContentsCreateView` actions', (done) => { + it('dispatches `requestCreateLabel`, `fetchLabels` & `receiveCreateLabelSuccess` & `toggleDropdownContentsCreateView` actions', (done) => { const label = { id: 1 }; mock.onPost(/labels.json/).replyOnce(200, label); @@ -225,6 +225,7 @@ describe('LabelsSelect Actions', () => { [], [ { type: 'requestCreateLabel' }, + { payload: { refetch: true }, type: 'fetchLabels' }, { type: 'receiveCreateLabelSuccess' }, { type: 'toggleDropdownContentsCreateView' }, ], @@ -263,4 +264,16 @@ describe('LabelsSelect Actions', () => { ); }); }); + + describe('updateLabelsSetState', () => { + it('updates labels `set` state to match `selectedLabels`', () => { + testAction( + actions.updateLabelsSetState, + {}, + state, + [{ type: types.UPDATE_LABELS_SET_STATE }], + [], + ); + }); + }); }); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js index 1d2a9c34599..14e0c8a2278 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js @@ -197,4 +197,26 @@ describe('LabelsSelect Mutations', () => { }); }); }); + + describe(`${types.UPDATE_LABELS_SET_STATE}`, () => { + it('updates labels `set` state to match selected labels', () => { + const state = { + labels: [ + { id: 1, title: 'scoped::test', set: false }, + { id: 2, set: true, title: 'scoped::one', touched: true }, + { id: 3, title: '' }, + { id: 4, title: '' }, + ], + selectedLabels: [{ id: 1 }, { id: 3 }], + }; + mutations[types.UPDATE_LABELS_SET_STATE](state); + + expect(state.labels).toEqual([ + { id: 1, title: 'scoped::test', set: true }, + { id: 2, set: false, title: 'scoped::one', touched: true }, + { id: 3, title: '', set: true }, + { id: 4, title: '', set: false }, + ]); + }); + }); }); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js index 46a11bc28d8..90bc1980ac3 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js @@ -1,6 +1,6 @@ import { GlLoadingIcon, GlLink } from '@gitlab/ui'; import { shallowMount, createLocalVue } from '@vue/test-utils'; -import Vue, { nextTick } from 'vue'; +import { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; @@ -14,7 +14,7 @@ jest.mock('~/flash'); const colors = Object.keys(mockSuggestedColors); const localVue = createLocalVue(); -Vue.use(VueApollo); +localVue.use(VueApollo); const userRecoverableError = { ...createLabelSuccessfulResponse, diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js index 51301387c99..8bd944a3d54 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js @@ -1,357 +1,213 @@ -import { GlIntersectionObserver, GlLoadingIcon, GlSearchBoxByType, GlLink } from '@gitlab/ui'; +import { GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui'; import { shallowMount, createLocalVue } from '@vue/test-utils'; -import Vuex from 'vuex'; -import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes'; +import { 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 { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; +import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_widget/constants'; import DropdownContentsLabelsView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue'; +import projectLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql'; import LabelItem from '~/vue_shared/components/sidebar/labels_select_widget/label_item.vue'; +import { mockConfig, labelsQueryResponse } from './mock_data'; -import * as actions from '~/vue_shared/components/sidebar/labels_select_widget/store/actions'; -import * as getters from '~/vue_shared/components/sidebar/labels_select_widget/store/getters'; -import mutations from '~/vue_shared/components/sidebar/labels_select_widget/store/mutations'; -import defaultState from '~/vue_shared/components/sidebar/labels_select_widget/store/state'; - -import { mockConfig, mockLabels, mockRegularLabel } from './mock_data'; +jest.mock('~/flash'); const localVue = createLocalVue(); -localVue.use(Vuex); +localVue.use(VueApollo); + +const selectedLabels = [ + { + id: 28, + title: 'Bug', + description: 'Label for bugs', + color: '#FF0000', + textColor: '#FFFFFF', + }, +]; describe('DropdownContentsLabelsView', () => { let wrapper; - const createComponent = (initialState = mockConfig) => { - const store = new Vuex.Store({ - getters, - mutations, - state: { - ...defaultState(), - footerCreateLabelTitle: 'Create label', - footerManageLabelTitle: 'Manage labels', - }, - actions: { - ...actions, - fetchLabels: jest.fn(), - }, - }); + const successfulQueryHandler = jest.fn().mockResolvedValue(labelsQueryResponse); - store.dispatch('setInitialState', initialState); - store.dispatch('receiveLabelsSuccess', mockLabels); + const createComponent = ({ + initialState = mockConfig, + queryHandler = successfulQueryHandler, + injected = {}, + } = {}) => { + const mockApollo = createMockApollo([[projectLabelsQuery, queryHandler]]); wrapper = shallowMount(DropdownContentsLabelsView, { localVue, - store, + apolloProvider: mockApollo, + provide: { + projectPath: 'test', + iid: 1, + allowLabelCreate: true, + labelsManagePath: '/gitlab-org/my-project/-/labels', + variant: DropdownVariant.Sidebar, + ...injected, + }, + propsData: { + ...initialState, + selectedLabels, + }, + stubs: { + GlSearchBoxByType, + }, }); }; - beforeEach(() => { - createComponent(); - }); - afterEach(() => { wrapper.destroy(); - wrapper = null; }); - const findDropdownContent = () => wrapper.find('[data-testid="dropdown-content"]'); + const findSearchInput = () => wrapper.findComponent(GlSearchBoxByType); + const findLabels = () => wrapper.findAllComponents(LabelItem); + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); + + 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 findLoadingIcon = () => wrapper.find(GlLoadingIcon); - - describe('computed', () => { - describe('visibleLabels', () => { - it('returns matching labels filtered with `searchKey`', () => { - wrapper.setData({ - searchKey: 'bug', - }); - - expect(wrapper.vm.visibleLabels.length).toBe(1); - expect(wrapper.vm.visibleLabels[0].title).toBe('Bug'); - }); - - it('returns matching labels with fuzzy filtering', () => { - wrapper.setData({ - searchKey: 'bg', - }); - - expect(wrapper.vm.visibleLabels.length).toBe(2); - expect(wrapper.vm.visibleLabels[0].title).toBe('Bug'); - expect(wrapper.vm.visibleLabels[1].title).toBe('Boog'); - }); - - it('returns all labels when `searchKey` is empty', () => { - wrapper.setData({ - searchKey: '', - }); - - expect(wrapper.vm.visibleLabels.length).toBe(mockLabels.length); - }); - }); + const findNoResultsMessage = () => wrapper.find('[data-testid="no-results"]'); + const findCreateLabelButton = () => wrapper.find('[data-testid="create-label-button"]'); - describe('showNoMatchingResultsMessage', () => { - it.each` - searchKey | labels | labelsDescription | returnValue - ${''} | ${[]} | ${'empty'} | ${false} - ${'bug'} | ${[]} | ${'empty'} | ${true} - ${''} | ${mockLabels} | ${'not empty'} | ${false} - ${'bug'} | ${mockLabels} | ${'not empty'} | ${false} - `( - 'returns $returnValue when searchKey is "$searchKey" and visibleLabels is $labelsDescription', - async ({ searchKey, labels, returnValue }) => { - wrapper.setData({ - searchKey, - }); - - wrapper.vm.$store.dispatch('receiveLabelsSuccess', labels); - - await wrapper.vm.$nextTick(); - - expect(wrapper.vm.showNoMatchingResultsMessage).toBe(returnValue); - }, - ); + describe('when loading labels', () => { + it('renders disabled search input field', async () => { + createComponent(); + expect(findSearchInput().props('disabled')).toBe(true); }); - }); - - describe('methods', () => { - describe('isLabelSelected', () => { - it('returns true when provided `label` param is one of the selected labels', () => { - expect(wrapper.vm.isLabelSelected(mockRegularLabel)).toBe(true); - }); - it('returns false when provided `label` param is not one of the selected labels', () => { - expect(wrapper.vm.isLabelSelected(mockLabels[2])).toBe(false); - }); + it('renders loading icon', async () => { + createComponent(); + expect(findLoadingIcon().exists()).toBe(true); }); - describe('handleComponentAppear', () => { - it('calls `focusInput` on searchInput field', async () => { - wrapper.vm.$refs.searchInput.focusInput = jest.fn(); - - await wrapper.vm.handleComponentAppear(); - - expect(wrapper.vm.$refs.searchInput.focusInput).toHaveBeenCalled(); - }); + it('does not render labels list', async () => { + createComponent(); + expect(findLabelsList().exists()).toBe(false); }); + }); - describe('handleComponentDisappear', () => { - it('calls action `receiveLabelsSuccess` with empty array', () => { - jest.spyOn(wrapper.vm, 'receiveLabelsSuccess'); - - wrapper.vm.handleComponentDisappear(); - - expect(wrapper.vm.receiveLabelsSuccess).toHaveBeenCalledWith([]); - }); + describe('when labels are loaded', () => { + beforeEach(async () => { + createComponent(); + await waitForPromises(); }); - describe('handleCreateLabelClick', () => { - it('calls actions `receiveLabelsSuccess` with empty array and `toggleDropdownContentsCreateView`', () => { - jest.spyOn(wrapper.vm, 'receiveLabelsSuccess'); - jest.spyOn(wrapper.vm, 'toggleDropdownContentsCreateView'); - - wrapper.vm.handleCreateLabelClick(); - - expect(wrapper.vm.receiveLabelsSuccess).toHaveBeenCalledWith([]); - expect(wrapper.vm.toggleDropdownContentsCreateView).toHaveBeenCalled(); - }); + it('renders enabled search input field', async () => { + expect(findSearchInput().props('disabled')).toBe(false); }); - describe('handleKeyDown', () => { - it('decreases `currentHighlightItem` value by 1 when Up arrow key is pressed', () => { - wrapper.setData({ - currentHighlightItem: 1, - }); - - wrapper.vm.handleKeyDown({ - keyCode: UP_KEY_CODE, - }); - - expect(wrapper.vm.currentHighlightItem).toBe(0); - }); - - it('increases `currentHighlightItem` value by 1 when Down arrow key is pressed', () => { - wrapper.setData({ - currentHighlightItem: 1, - }); - - wrapper.vm.handleKeyDown({ - keyCode: DOWN_KEY_CODE, - }); - - expect(wrapper.vm.currentHighlightItem).toBe(2); - }); - - it('resets the search text when the Enter key is pressed', () => { - wrapper.setData({ - currentHighlightItem: 1, - searchKey: 'bug', - }); - - wrapper.vm.handleKeyDown({ - keyCode: ENTER_KEY_CODE, - }); - - expect(wrapper.vm.searchKey).toBe(''); - }); - - it('calls action `updateSelectedLabels` with currently highlighted label when Enter key is pressed', () => { - jest.spyOn(wrapper.vm, 'updateSelectedLabels').mockImplementation(); - wrapper.setData({ - currentHighlightItem: 1, - }); - - wrapper.vm.handleKeyDown({ - keyCode: ENTER_KEY_CODE, - }); - - expect(wrapper.vm.updateSelectedLabels).toHaveBeenCalledWith([ - { - ...mockLabels[1], - set: true, - }, - ]); - }); - - it('calls action `toggleDropdownContents` when Esc key is pressed', () => { - jest.spyOn(wrapper.vm, 'toggleDropdownContents').mockImplementation(); - wrapper.setData({ - currentHighlightItem: 1, - }); - - wrapper.vm.handleKeyDown({ - keyCode: ESC_KEY_CODE, - }); - - expect(wrapper.vm.toggleDropdownContents).toHaveBeenCalled(); - }); - - it('calls action `scrollIntoViewIfNeeded` in next tick when any key is pressed', () => { - jest.spyOn(wrapper.vm, 'scrollIntoViewIfNeeded').mockImplementation(); - wrapper.setData({ - currentHighlightItem: 1, - }); - - wrapper.vm.handleKeyDown({ - keyCode: DOWN_KEY_CODE, - }); - - return wrapper.vm.$nextTick(() => { - expect(wrapper.vm.scrollIntoViewIfNeeded).toHaveBeenCalled(); - }); - }); + it('does not render loading icon', async () => { + expect(findLoadingIcon().exists()).toBe(false); }); - describe('handleLabelClick', () => { - beforeEach(() => { - jest.spyOn(wrapper.vm, 'updateSelectedLabels').mockImplementation(); - }); - - it('calls action `updateSelectedLabels` with provided `label` param', () => { - wrapper.vm.handleLabelClick(mockRegularLabel); - - expect(wrapper.vm.updateSelectedLabels).toHaveBeenCalledWith([mockRegularLabel]); - }); + it('renders labels list', async () => { + expect(findLabelsList().exists()).toBe(true); + expect(findLabels()).toHaveLength(2); + }); - it('calls action `toggleDropdownContents` when `state.allowMultiselect` is false', () => { - jest.spyOn(wrapper.vm, 'toggleDropdownContents'); - wrapper.vm.$store.state.allowMultiselect = false; + it('changes highlighted label correctly on pressing down button', async () => { + expect(findLabels().at(0).attributes('highlight')).toBeUndefined(); - wrapper.vm.handleLabelClick(mockRegularLabel); + await findDropdownWrapper().trigger('keydown.down'); + expect(findLabels().at(0).attributes('highlight')).toBe('true'); - expect(wrapper.vm.toggleDropdownContents).toHaveBeenCalled(); - }); + await findDropdownWrapper().trigger('keydown.down'); + expect(findLabels().at(1).attributes('highlight')).toBe('true'); + expect(findLabels().at(0).attributes('highlight')).toBeUndefined(); }); - }); - describe('template', () => { - it('renders gl-intersection-observer as component root', () => { - expect(wrapper.find(GlIntersectionObserver).exists()).toBe(true); - }); + 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'); - it('renders gl-loading-icon component when `labelsFetchInProgress` prop is true', () => { - wrapper.vm.$store.dispatch('requestLabels'); + await findDropdownWrapper().trigger('keydown.up'); + expect(findLabels().at(0).attributes('highlight')).toBe('true'); + }); - return wrapper.vm.$nextTick(() => { - const loadingIconEl = findLoadingIcon(); + 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(loadingIconEl.exists()).toBe(true); - expect(loadingIconEl.attributes('class')).toContain('labels-fetch-loading'); - }); + expect(findLabels().at(0).attributes('islabelset')).toBe('true'); }); - it('renders label search input element', () => { - const searchInputEl = wrapper.find(GlSearchBoxByType); + it('emits `closeDropdown event` when Esc button is pressed', () => { + findDropdownWrapper().trigger('keydown.esc'); - expect(searchInputEl.exists()).toBe(true); + expect(wrapper.emitted('closeDropdown')).toEqual([[selectedLabels]]); }); + }); - it('renders label elements for all labels', () => { - expect(wrapper.findAll(LabelItem)).toHaveLength(mockLabels.length); + it('when search returns 0 results', async () => { + createComponent({ + queryHandler: jest.fn().mockResolvedValue({ + data: { + workspace: { + labels: { + nodes: [], + }, + }, + }, + }), }); + findSearchInput().vm.$emit('input', '123'); + await waitForPromises(); + await nextTick(); - it('renders label element with `highlight` set to true when value of `currentHighlightItem` is more than -1', () => { - wrapper.setData({ - currentHighlightItem: 0, - }); + expect(findNoResultsMessage().isVisible()).toBe(true); + }); - return wrapper.vm.$nextTick(() => { - const labelItemEl = findDropdownContent().find(LabelItem); + it('calls `createFlash` when fetching labels failed', async () => { + createComponent({ queryHandler: jest.fn().mockRejectedValue('Houston, we have a problem!') }); + jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS); + await waitForPromises(); + expect(createFlash).toHaveBeenCalled(); + }); - expect(labelItemEl.attributes('highlight')).toBe('true'); - }); - }); + it('does not render footer on standalone dropdown', () => { + createComponent({ injected: { variant: DropdownVariant.Standalone } }); - it('renders element containing "No matching results" when `searchKey` does not match with any label', () => { - wrapper.setData({ - searchKey: 'abc', - }); + expect(findDropdownFooter().exists()).toBe(false); + }); - return wrapper.vm.$nextTick(() => { - const noMatchEl = findDropdownContent().find('li'); + it('renders footer on sidebar dropdown', () => { + createComponent(); - expect(noMatchEl.isVisible()).toBe(true); - expect(noMatchEl.text()).toContain('No matching results'); - }); - }); + expect(findDropdownFooter().exists()).toBe(true); + }); - it('renders empty content while loading', () => { - wrapper.vm.$store.state.labelsFetchInProgress = true; + it('renders footer on embedded dropdown', () => { + createComponent({ injected: { variant: DropdownVariant.Embedded } }); - return wrapper.vm.$nextTick(() => { - const dropdownContent = findDropdownContent(); - const loadingIcon = findLoadingIcon(); + expect(findDropdownFooter().exists()).toBe(true); + }); - expect(dropdownContent.exists()).toBe(true); - expect(dropdownContent.isVisible()).toBe(true); - expect(loadingIcon.exists()).toBe(true); - expect(loadingIcon.isVisible()).toBe(true); - }); - }); + it('does not render create label button if `allowLabelCreate` is false', () => { + createComponent({ injected: { allowLabelCreate: false } }); - it('renders footer list items', () => { - const footerLinks = findDropdownFooter().findAll(GlLink); - const createLabelLink = footerLinks.at(0); - const manageLabelsLink = footerLinks.at(1); + expect(findCreateLabelButton().exists()).toBe(false); + }); - expect(createLabelLink.exists()).toBe(true); - expect(createLabelLink.text()).toBe('Create label'); - expect(manageLabelsLink.exists()).toBe(true); - expect(manageLabelsLink.text()).toBe('Manage labels'); + describe('when `allowLabelCreate` is true', () => { + beforeEach(() => { + createComponent(); }); - it('does not render "Create label" footer link when `state.allowLabelCreate` is `false`', () => { - wrapper.vm.$store.state.allowLabelCreate = false; - - return wrapper.vm.$nextTick(() => { - const createLabelLink = findDropdownFooter().findAll(GlLink).at(0); - - expect(createLabelLink.text()).not.toBe('Create label'); - }); + it('renders create label button', () => { + expect(findCreateLabelButton().exists()).toBe(true); }); - it('does not render footer list items when `state.variant` is "standalone"', () => { - createComponent({ ...mockConfig, variant: 'standalone' }); - expect(findDropdownFooter().exists()).toBe(false); - }); + it('emits `toggleDropdownContentsCreateView` event on create label button click', () => { + findCreateLabelButton().vm.$emit('click'); - it('renders footer list items when `state.variant` is "embedded"', () => { - expect(findDropdownFooter().exists()).toBe(true); + expect(wrapper.emitted('toggleDropdownContentsCreateView')).toEqual([[]]); }); }); }); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js index 8273bbdf7a7..3c2fd0c5acc 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js @@ -5,7 +5,7 @@ import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_w 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'; +import { mockConfig, mockLabels } from './mock_data'; const localVue = createLocalVue(); localVue.use(Vuex); @@ -19,6 +19,11 @@ const createComponent = (initialState = mockConfig, defaultProps = {}) => { propsData: { ...defaultProps, labelsCreateTitle: 'test', + selectedLabels: mockLabels, + allowMultiselect: true, + labelsListTitle: 'Assign labels', + footerCreateLabelTitle: 'create', + footerManageLabelTitle: 'manage', }, localVue, store, diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js index 66971446f47..e17dfd93efc 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js @@ -50,58 +50,6 @@ describe('LabelsSelectRoot', () => { }); 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(); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js index 9e29030fb56..5dd8fc1b8b2 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js @@ -48,6 +48,8 @@ export const mockConfig = { labelsManagePath: '/gitlab-org/my-project/-/labels', labelsFilterBasePath: '/gitlab-org/my-project/issues', labelsFilterParam: 'label_name', + footerCreateLabelTitle: 'create', + footerManageLabelTitle: 'manage', }; export const mockSuggestedColors = { @@ -91,3 +93,26 @@ export const createLabelSuccessfulResponse = { }, }, }; + +export const labelsQueryResponse = { + data: { + workspace: { + labels: { + nodes: [ + { + color: '#330066', + description: null, + id: 'gid://gitlab/ProjectLabel/1', + title: 'Label1', + }, + { + color: '#2f7b2e', + description: null, + id: 'gid://gitlab/ProjectLabel/2', + title: 'Label2', + }, + ], + }, + }, + }, +}; diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/store/actions_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/store/actions_spec.js index 27de7de2411..ee905410ffa 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/store/actions_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/store/actions_spec.js @@ -1,8 +1,4 @@ -import MockAdapter from 'axios-mock-adapter'; - import testAction from 'helpers/vuex_action_helper'; -import createFlash from '~/flash'; -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'; @@ -72,90 +68,6 @@ describe('LabelsSelect Actions', () => { }); }); - 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', () => { - 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(createFlash).toHaveBeenCalledWith({ message: '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 }]; diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/store/mutations_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/store/mutations_spec.js index 9e965cb33e8..1f0e0eee420 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/store/mutations_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/store/mutations_spec.js @@ -67,58 +67,6 @@ describe('LabelsSelect Mutations', () => { }); }); - 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}`, () => { let labels; diff --git a/spec/frontend/vue_shared/components/url_sync_spec.js b/spec/frontend/vue_shared/components/url_sync_spec.js index 86bbc146c5f..aefe6a5c3e8 100644 --- a/spec/frontend/vue_shared/components/url_sync_spec.js +++ b/spec/frontend/vue_shared/components/url_sync_spec.js @@ -1,5 +1,4 @@ import { shallowMount } from '@vue/test-utils'; -import setWindowLocation from 'helpers/set_window_location_helper'; import { historyPushState } from '~/lib/utils/common_utils'; import { mergeUrlParams } from '~/lib/utils/url_utility'; import UrlSyncComponent from '~/vue_shared/components/url_sync.vue'; @@ -15,9 +14,6 @@ jest.mock('~/lib/utils/common_utils', () => ({ describe('url sync component', () => { let wrapper; const mockQuery = { group_id: '5014437163714', project_ids: ['5014437608314'] }; - const TEST_HOST = 'http://testhost/'; - - setWindowLocation(TEST_HOST); const findButton = () => wrapper.find('button'); @@ -35,7 +31,9 @@ describe('url sync component', () => { const expectUrlSync = (query, times, mergeUrlParamsReturnValue) => { expect(mergeUrlParams).toHaveBeenCalledTimes(times); - expect(mergeUrlParams).toHaveBeenCalledWith(query, TEST_HOST, { spreadArrays: true }); + expect(mergeUrlParams).toHaveBeenCalledWith(query, window.location.href, { + spreadArrays: true, + }); expect(historyPushState).toHaveBeenCalledTimes(times); expect(historyPushState).toHaveBeenCalledWith(mergeUrlParamsReturnValue); diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js index d62c4a98b10..d3fec680b54 100644 --- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js +++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js @@ -104,4 +104,15 @@ describe('User Avatar Link Component', () => { ); }); }); + + describe('lazy', () => { + it('passes lazy prop to avatar image', () => { + createWrapper({ + username: '', + lazy: true, + }); + + expect(wrapper.find(UserAvatarImage).props('lazy')).toBe(true); + }); + }); }); diff --git a/spec/frontend/vue_shared/components/web_ide_link_spec.js b/spec/frontend/vue_shared/components/web_ide_link_spec.js index 0fd4d0dab87..5fe4eeb6061 100644 --- a/spec/frontend/vue_shared/components/web_ide_link_spec.js +++ b/spec/frontend/vue_shared/components/web_ide_link_spec.js @@ -85,6 +85,10 @@ describe('Web IDE link component', () => { expectedActions: [ACTION_WEB_IDE, ACTION_EDIT], }, { + props: { webIdeText: 'Test Web IDE' }, + expectedActions: [{ ...ACTION_WEB_IDE_EDIT_FORK, text: 'Test Web IDE' }, ACTION_EDIT], + }, + { props: { isFork: true }, expectedActions: [ACTION_WEB_IDE_EDIT_FORK, ACTION_EDIT], }, @@ -105,6 +109,10 @@ describe('Web IDE link component', () => { expectedActions: [ACTION_WEB_IDE, ACTION_EDIT, ACTION_GITPOD_ENABLE], }, { + props: { showEditButton: false, showGitpodButton: true, gitpodText: 'Test Gitpod' }, + expectedActions: [ACTION_WEB_IDE, { ...ACTION_GITPOD_ENABLE, text: 'Test Gitpod' }], + }, + { props: { showEditButton: false }, expectedActions: [ACTION_WEB_IDE], }, |