diff options
Diffstat (limited to 'spec/frontend/vue_shared/components')
33 files changed, 1034 insertions, 437 deletions
diff --git a/spec/frontend/vue_shared/components/alerts_deprecation_warning_spec.js b/spec/frontend/vue_shared/components/alerts_deprecation_warning_spec.js deleted file mode 100644 index b73f4d6a396..00000000000 --- a/spec/frontend/vue_shared/components/alerts_deprecation_warning_spec.js +++ /dev/null @@ -1,48 +0,0 @@ -import { GlAlert, GlLink } from '@gitlab/ui'; -import { mount } from '@vue/test-utils'; -import AlertDeprecationWarning from '~/vue_shared/components/alerts_deprecation_warning.vue'; - -describe('AlertDetails', () => { - let wrapper; - - function mountComponent(hasManagedPrometheus = false) { - wrapper = mount(AlertDeprecationWarning, { - provide: { - hasManagedPrometheus, - }, - }); - } - - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - - const findAlert = () => wrapper.findComponent(GlAlert); - const findLink = () => wrapper.findComponent(GlLink); - - describe('Alert details', () => { - describe('with no manual prometheus', () => { - beforeEach(() => { - mountComponent(); - }); - - it('renders nothing', () => { - expect(findAlert().exists()).toBe(false); - }); - }); - - describe('with manual prometheus', () => { - beforeEach(() => { - mountComponent(true); - }); - - it('renders a deprecation notice', () => { - expect(findAlert().text()).toContain('GitLab-managed Prometheus is deprecated'); - expect(findLink().attributes('href')).toContain( - 'operations/metrics/alerts.html#managed-prometheus-instances', - ); - }); - }); - }); -}); diff --git a/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js b/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js new file mode 100644 index 00000000000..f75694bd504 --- /dev/null +++ b/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js @@ -0,0 +1,99 @@ +import { GlModal, GlSprintf } from '@gitlab/ui'; +import { + CONFIRM_DANGER_WARNING, + CONFIRM_DANGER_MODAL_BUTTON, + CONFIRM_DANGER_MODAL_ID, +} from '~/vue_shared/components/confirm_danger/constants'; +import ConfirmDangerModal from '~/vue_shared/components/confirm_danger/confirm_danger_modal.vue'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; + +describe('Confirm Danger Modal', () => { + const confirmDangerMessage = 'This is a dangerous activity'; + const confirmButtonText = 'Confirm button text'; + const phrase = 'You must construct additional pylons'; + const modalId = CONFIRM_DANGER_MODAL_ID; + + let wrapper; + + const findModal = () => wrapper.findComponent(GlModal); + const findConfirmationPhrase = () => wrapper.findByTestId('confirm-danger-phrase'); + const findConfirmationInput = () => wrapper.findByTestId('confirm-danger-input'); + const findDefaultWarning = () => wrapper.findByTestId('confirm-danger-warning'); + const findAdditionalMessage = () => wrapper.findByTestId('confirm-danger-message'); + const findPrimaryAction = () => findModal().props('actionPrimary'); + const findPrimaryActionAttributes = (attr) => findPrimaryAction().attributes[0][attr]; + + const createComponent = ({ provide = {} } = {}) => + shallowMountExtended(ConfirmDangerModal, { + propsData: { + modalId, + phrase, + }, + provide, + stubs: { GlSprintf }, + }); + + beforeEach(() => { + wrapper = createComponent({ provide: { confirmDangerMessage, confirmButtonText } }); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders the default warning message', () => { + expect(findDefaultWarning().text()).toBe(CONFIRM_DANGER_WARNING); + }); + + it('renders any additional messages', () => { + expect(findAdditionalMessage().text()).toBe(confirmDangerMessage); + }); + + it('renders the confirm button', () => { + expect(findPrimaryAction().text).toBe(confirmButtonText); + expect(findPrimaryActionAttributes('variant')).toBe('danger'); + }); + + it('renders the correct confirmation phrase', () => { + expect(findConfirmationPhrase().text()).toBe( + `Please type ${phrase} to proceed or close this modal to cancel.`, + ); + }); + + describe('without injected data', () => { + beforeEach(() => { + wrapper = createComponent(); + }); + + it('does not render any additional messages', () => { + expect(findAdditionalMessage().exists()).toBe(false); + }); + + it('renders the default confirm button', () => { + expect(findPrimaryAction().text).toBe(CONFIRM_DANGER_MODAL_BUTTON); + }); + }); + + describe('with a valid confirmation phrase', () => { + beforeEach(() => { + wrapper = createComponent(); + }); + + it('enables the confirm button', async () => { + expect(findPrimaryActionAttributes('disabled')).toBe(true); + + await findConfirmationInput().vm.$emit('input', phrase); + + expect(findPrimaryActionAttributes('disabled')).toBe(false); + }); + + it('emits a `confirm` event when the button is clicked', async () => { + expect(wrapper.emitted('confirm')).toBeUndefined(); + + await findConfirmationInput().vm.$emit('input', phrase); + await findModal().vm.$emit('primary'); + + expect(wrapper.emitted('confirm')).not.toBeUndefined(); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_spec.js b/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_spec.js new file mode 100644 index 00000000000..220f897c035 --- /dev/null +++ b/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_spec.js @@ -0,0 +1,61 @@ +import { GlButton } from '@gitlab/ui'; +import ConfirmDanger from '~/vue_shared/components/confirm_danger/confirm_danger.vue'; +import ConfirmDangerModal from '~/vue_shared/components/confirm_danger/confirm_danger_modal.vue'; +import { CONFIRM_DANGER_MODAL_ID } from '~/vue_shared/components/confirm_danger/constants'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; + +describe('Confirm Danger Modal', () => { + let wrapper; + + const phrase = 'En Taro Adun'; + const buttonText = 'Click me!'; + const modalId = CONFIRM_DANGER_MODAL_ID; + + const findBtn = () => wrapper.findComponent(GlButton); + const findModal = () => wrapper.findComponent(ConfirmDangerModal); + const findModalProps = () => findModal().props(); + + const createComponent = (props = {}) => + shallowMountExtended(ConfirmDanger, { + propsData: { + buttonText, + phrase, + ...props, + }, + }); + + beforeEach(() => { + wrapper = createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders the button', () => { + expect(wrapper.html()).toContain(buttonText); + }); + + it('sets the modal properties', () => { + expect(findModalProps()).toMatchObject({ + modalId, + phrase, + }); + }); + + it('will disable the button if `disabled=true`', () => { + expect(findBtn().attributes('disabled')).toBeUndefined(); + + wrapper = createComponent({ disabled: true }); + + expect(findBtn().attributes('disabled')).toBe('true'); + }); + + it('will emit `confirm` when the modal confirms', () => { + expect(wrapper.emitted('confirm')).toBeUndefined(); + + findModal().vm.$emit('confirm'); + + expect(wrapper.emitted('confirm')).not.toBeUndefined(); + }); +}); diff --git a/spec/frontend/vue_shared/components/content_viewer/content_viewer_spec.js b/spec/frontend/vue_shared/components/content_viewer/content_viewer_spec.js index 16e7e4dd5cc..f28805471f8 100644 --- a/spec/frontend/vue_shared/components/content_viewer/content_viewer_spec.js +++ b/spec/frontend/vue_shared/components/content_viewer/content_viewer_spec.js @@ -16,6 +16,6 @@ describe('ContentViewer', () => { propsData: { path, fileSize: 1024, type }, }); - expect(wrapper.find(selector).element).toExist(); + expect(wrapper.find(selector).exists()).toBe(true); }); }); diff --git a/spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js b/spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js index 3ffb23dc7a0..1397fb0405e 100644 --- a/spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js +++ b/spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js @@ -42,7 +42,7 @@ describe('MarkdownViewer', () => { it('renders an animation container while the markdown is loading', () => { createComponent(); - expect(wrapper.find('.animation-container')).toExist(); + expect(wrapper.find('.animation-container').exists()).toBe(true); }); it('renders markdown preview preview renders and loads rendered markdown from server', () => { diff --git a/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js b/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js index 016fe1f131e..b3af5fd3feb 100644 --- a/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js +++ b/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js @@ -34,6 +34,7 @@ describe('DropdownWidget component', () => { // 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(); + jest.spyOn(findDropdown().vm, 'hide').mockImplementation(); }; beforeEach(() => { @@ -67,10 +68,7 @@ describe('DropdownWidget component', () => { }); 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')); + wrapper.findAll('[data-testid="unselected-option"]').at(1).trigger('click'); await wrapper.vm.$nextTick(); expect(wrapper.emitted('set-option')).toEqual([[wrapper.props().options[1]]]); 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 8e931aebfe0..64d15884333 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 @@ -25,6 +25,7 @@ import { tokenValueMilestone, tokenValueMembership, tokenValueConfidential, + tokenValueEmpty, } from './mock_data'; jest.mock('~/vue_shared/components/filtered_search_bar/filtered_search_utils', () => ({ @@ -43,6 +44,7 @@ const createComponent = ({ recentSearchesStorageKey = 'requirements', tokens = mockAvailableTokens, sortOptions, + initialFilterValue = [], showCheckbox = false, checkboxChecked = false, searchInputPlaceholder = 'Filter requirements', @@ -55,6 +57,7 @@ const createComponent = ({ recentSearchesStorageKey, tokens, sortOptions, + initialFilterValue, showCheckbox, checkboxChecked, searchInputPlaceholder, @@ -193,19 +196,27 @@ describe('FilteredSearchBarRoot', () => { describe('watchers', () => { describe('filterValue', () => { - it('emits component event `onFilter` with empty array when `filterValue` is cleared by GlFilteredSearch', () => { + it('emits component event `onFilter` with empty array and false when filter was never selected', () => { + wrapper = createComponent({ initialFilterValue: [tokenValueEmpty] }); wrapper.setData({ initialRender: false, - filterValue: [ - { - type: 'filtered-search-term', - value: { data: '' }, - }, - ], + filterValue: [tokenValueEmpty], + }); + + return wrapper.vm.$nextTick(() => { + expect(wrapper.emitted('onFilter')[0]).toEqual([[], false]); + }); + }); + + it('emits component event `onFilter` with empty array and true when initially selected filter value was cleared', () => { + wrapper = createComponent({ initialFilterValue: [tokenValueLabel] }); + wrapper.setData({ + initialRender: false, + filterValue: [tokenValueEmpty], }); return wrapper.vm.$nextTick(() => { - expect(wrapper.emitted('onFilter')[0]).toEqual([[]]); + expect(wrapper.emitted('onFilter')[0]).toEqual([[], true]); }); }); }); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js b/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js index ae02c554e13..238c5d16db5 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js @@ -9,6 +9,7 @@ import EpicToken from '~/vue_shared/components/filtered_search_bar/tokens/epic_t import IterationToken from '~/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue'; import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue'; import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue'; +import ReleaseToken from '~/vue_shared/components/filtered_search_bar/tokens/release_token.vue'; import WeightToken from '~/vue_shared/components/filtered_search_bar/tokens/weight_token.vue'; export const mockAuthor1 = { @@ -110,6 +111,18 @@ export const mockIterationToken = { fetchIterations: () => Promise.resolve(), }; +export const mockIterations = [ + { + id: 1, + title: 'Iteration 1', + startDate: '2021-11-05', + dueDate: '2021-11-10', + iterationCadence: { + title: 'Cadence 1', + }, + }, +]; + export const mockLabelToken = { type: 'label_name', icon: 'labels', @@ -132,6 +145,14 @@ export const mockMilestoneToken = { fetchMilestones: () => Promise.resolve({ data: mockMilestones }), }; +export const mockReleaseToken = { + type: 'release', + icon: 'rocket', + title: 'Release', + token: ReleaseToken, + fetchReleases: () => Promise.resolve(), +}; + export const mockEpicToken = { type: 'epic_iid', icon: 'clock', @@ -282,6 +303,11 @@ export const tokenValuePlain = { value: { data: 'foo' }, }; +export const tokenValueEmpty = { + type: 'filtered-search-term', + value: { data: '' }, +}; + export const tokenValueEpic = { type: 'epic_iid', value: { 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 14fcffd3c50..b29c394e7ae 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 @@ -112,6 +112,35 @@ describe('AuthorToken', () => { }); }); + // TODO: rm when completed https://gitlab.com/gitlab-org/gitlab/-/issues/345756 + describe('when there are null users presents', () => { + const mockAuthorsWithNullUser = mockAuthors.concat([null]); + + beforeEach(() => { + jest + .spyOn(wrapper.vm.config, 'fetchAuthors') + .mockResolvedValue({ data: mockAuthorsWithNullUser }); + + getBaseToken().vm.$emit('fetch-suggestions', 'root'); + }); + + describe('when res.data is present', () => { + it('filters the successful response when null values are present', () => { + return waitForPromises().then(() => { + expect(getBaseToken().props('suggestions')).toEqual(mockAuthors); + }); + }); + }); + + describe('when response is an array', () => { + it('filters the successful response when null values are present', () => { + return waitForPromises().then(() => { + expect(getBaseToken().props('suggestions')).toEqual(mockAuthors); + }); + }); + }); + }); + it('calls `createFlash` with flash error message when request fails', () => { jest.spyOn(wrapper.vm.config, 'fetchAuthors').mockRejectedValue({}); 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 af90ee93543..44bc16adb97 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,9 +1,13 @@ -import { GlFilteredSearchToken, GlFilteredSearchTokenSegment } from '@gitlab/ui'; +import { + GlFilteredSearchToken, + GlFilteredSearchTokenSegment, + GlFilteredSearchSuggestion, +} 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'; +import { mockIterationToken, mockIterations } from '../mock_data'; jest.mock('~/flash'); @@ -11,10 +15,16 @@ describe('IterationToken', () => { const id = 123; let wrapper; - const createComponent = ({ config = mockIterationToken, value = { data: '' } } = {}) => + const createComponent = ({ + config = mockIterationToken, + value = { data: '' }, + active = false, + stubs = {}, + provide = {}, + } = {}) => mount(IterationToken, { propsData: { - active: false, + active, config, value, }, @@ -22,13 +32,39 @@ describe('IterationToken', () => { portalName: 'fake target', alignSuggestions: function fakeAlignSuggestions() {}, suggestionsListClass: () => 'custom-class', + ...provide, }, + stubs, }); afterEach(() => { wrapper.destroy(); }); + describe('when iteration cadence feature is available', () => { + beforeEach(async () => { + wrapper = createComponent({ + active: true, + config: { ...mockIterationToken, initialIterations: mockIterations }, + value: { data: 'i' }, + stubs: { Portal: true }, + provide: { + glFeatures: { + iterationCadences: true, + }, + }, + }); + + await wrapper.setData({ loading: false }); + }); + + it('renders iteration start date and due date', () => { + const suggestions = wrapper.findAll(GlFilteredSearchSuggestion); + + expect(suggestions.at(3).text()).toContain('Nov 5, 2021 - Nov 10, 2021'); + }); + }); + it('renders iteration value', async () => { wrapper = createComponent({ value: { data: id } }); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js new file mode 100644 index 00000000000..b804ff97b82 --- /dev/null +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js @@ -0,0 +1,78 @@ +import { GlFilteredSearchToken, GlFilteredSearchTokenSegment } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; +import waitForPromises from 'helpers/wait_for_promises'; +import createFlash from '~/flash'; +import ReleaseToken from '~/vue_shared/components/filtered_search_bar/tokens/release_token.vue'; +import { mockReleaseToken } from '../mock_data'; + +jest.mock('~/flash'); + +describe('ReleaseToken', () => { + const id = 123; + let wrapper; + + const createComponent = ({ config = mockReleaseToken, value = { data: '' } } = {}) => + mount(ReleaseToken, { + propsData: { + active: false, + config, + value, + }, + provide: { + portalName: 'fake target', + alignSuggestions: function fakeAlignSuggestions() {}, + suggestionsListClass: () => 'custom-class', + }, + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders release value', async () => { + wrapper = createComponent({ value: { data: id } }); + await wrapper.vm.$nextTick(); + + const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment); + + expect(tokenSegments).toHaveLength(3); // `Release` `=` `v1` + expect(tokenSegments.at(2).text()).toBe(id.toString()); + }); + + it('fetches initial values', () => { + const fetchReleasesSpy = jest.fn().mockResolvedValue(); + + wrapper = createComponent({ + config: { ...mockReleaseToken, fetchReleases: fetchReleasesSpy }, + value: { data: id }, + }); + + expect(fetchReleasesSpy).toHaveBeenCalledWith(id); + }); + + it('fetches releases on user input', () => { + const search = 'hello'; + const fetchReleasesSpy = jest.fn().mockResolvedValue(); + + wrapper = createComponent({ + config: { ...mockReleaseToken, fetchReleases: fetchReleasesSpy }, + }); + + wrapper.findComponent(GlFilteredSearchToken).vm.$emit('input', { data: search }); + + expect(fetchReleasesSpy).toHaveBeenCalledWith(search); + }); + + it('renders error message when request fails', async () => { + const fetchReleasesSpy = jest.fn().mockRejectedValue(); + + wrapper = createComponent({ + config: { ...mockReleaseToken, fetchReleases: fetchReleasesSpy }, + }); + await waitForPromises(); + + expect(createFlash).toHaveBeenCalledWith({ + message: 'There was a problem fetching releases.', + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/header_ci_component_spec.js b/spec/frontend/vue_shared/components/header_ci_component_spec.js index 42f4439df51..b76f475a6fb 100644 --- a/spec/frontend/vue_shared/components/header_ci_component_spec.js +++ b/spec/frontend/vue_shared/components/header_ci_component_spec.js @@ -1,4 +1,4 @@ -import { GlButton, GlLink } from '@gitlab/ui'; +import { GlButton, GlAvatarLink } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import CiIconBadge from '~/vue_shared/components/ci_badge_link.vue'; @@ -18,6 +18,7 @@ describe('Header CI Component', () => { }, time: '2017-05-08T14:57:39.781Z', user: { + id: 1234, web_url: 'path', name: 'Foo', username: 'foobar', @@ -29,7 +30,7 @@ describe('Header CI Component', () => { const findIconBadge = () => wrapper.findComponent(CiIconBadge); const findTimeAgo = () => wrapper.findComponent(TimeagoTooltip); - const findUserLink = () => wrapper.findComponent(GlLink); + const findUserLink = () => wrapper.findComponent(GlAvatarLink); const findSidebarToggleBtn = () => wrapper.findComponent(GlButton); const findActionButtons = () => wrapper.findByTestId('ci-header-action-buttons'); const findHeaderItemText = () => wrapper.findByTestId('ci-header-item-text'); @@ -64,10 +65,6 @@ describe('Header CI Component', () => { expect(findTimeAgo().exists()).toBe(true); }); - it('should render user icon and name', () => { - expect(findUserLink().text()).toContain(defaultProps.user.name); - }); - it('should render sidebar toggle button', () => { expect(findSidebarToggleBtn().exists()).toBe(true); }); @@ -77,6 +74,45 @@ describe('Header CI Component', () => { }); }); + describe('user avatar', () => { + beforeEach(() => { + createComponent({ itemName: 'Pipeline' }); + }); + + it('contains the username', () => { + expect(findUserLink().text()).toContain(defaultProps.user.username); + }); + + it('has the correct data attributes', () => { + expect(findUserLink().attributes()).toMatchObject({ + 'data-user-id': defaultProps.user.id.toString(), + 'data-username': defaultProps.user.username, + 'data-name': defaultProps.user.name, + }); + }); + + describe('with data from GraphQL', () => { + const userId = 1; + + beforeEach(() => { + createComponent({ + itemName: 'Pipeline', + user: { ...defaultProps.user, id: `gid://gitlab/User/${1}` }, + }); + }); + + it('has the correct user id', () => { + expect(findUserLink().attributes('data-user-id')).toBe(userId.toString()); + }); + }); + + describe('with data from REST', () => { + it('has the correct user id', () => { + expect(findUserLink().attributes('data-user-id')).toBe(defaultProps.user.id.toString()); + }); + }); + }); + describe('with item id', () => { beforeEach(() => { createComponent({ itemName: 'Pipeline', itemId: '123' }); diff --git a/spec/frontend/vue_shared/components/notes/system_note_spec.js b/spec/frontend/vue_shared/components/notes/system_note_spec.js index 48dacc50923..65f79bab005 100644 --- a/spec/frontend/vue_shared/components/notes/system_note_spec.js +++ b/spec/frontend/vue_shared/components/notes/system_note_spec.js @@ -1,13 +1,27 @@ +import MockAdapter from 'axios-mock-adapter'; import { mount } from '@vue/test-utils'; +import waitForPromises from 'helpers/wait_for_promises'; import initMRPopovers from '~/mr_popover/index'; import createStore from '~/notes/stores'; import IssueSystemNote from '~/vue_shared/components/notes/system_note.vue'; +import axios from '~/lib/utils/axios_utils'; jest.mock('~/mr_popover/index', () => jest.fn()); describe('system note component', () => { let vm; let props; + let mock; + + function createComponent(propsData = {}) { + const store = createStore(); + store.dispatch('setTargetNoteHash', `note_${props.note.id}`); + + vm = mount(IssueSystemNote, { + store, + propsData, + }); + } beforeEach(() => { props = { @@ -27,28 +41,29 @@ describe('system note component', () => { }, }; - const store = createStore(); - store.dispatch('setTargetNoteHash', `note_${props.note.id}`); - - vm = mount(IssueSystemNote, { - store, - propsData: props, - }); + mock = new MockAdapter(axios); }); afterEach(() => { vm.destroy(); + mock.restore(); }); it('should render a list item with correct id', () => { + createComponent(props); + expect(vm.attributes('id')).toEqual(`note_${props.note.id}`); }); it('should render target class is note is target note', () => { + createComponent(props); + expect(vm.classes()).toContain('target'); }); it('should render svg icon', () => { + createComponent(props); + expect(vm.find('.timeline-icon svg').exists()).toBe(true); }); @@ -56,10 +71,31 @@ describe('system note component', () => { // we need to strip them because they break layout of commit lists in system notes: // https://gitlab.com/gitlab-org/gitlab-foss/uploads/b07a10670919254f0220d3ff5c1aa110/jqzI.png it('removes wrapping paragraph from note HTML', () => { + createComponent(props); + expect(vm.find('.system-note-message').html()).toContain('<span>closed</span>'); }); it('should initMRPopovers onMount', () => { + createComponent(props); + expect(initMRPopovers).toHaveBeenCalled(); }); + + it('renders outdated code lines', async () => { + mock + .onGet('/outdated_line_change_path') + .reply(200, [ + { rich_text: 'console.log', type: 'new', line_code: '123', old_line: null, new_line: 1 }, + ]); + + createComponent({ + note: { ...props.note, outdated_line_change_path: '/outdated_line_change_path' }, + }); + + await vm.find("[data-testid='outdated-lines-change-btn']").trigger('click'); + await waitForPromises(); + + expect(vm.find("[data-testid='outdated-lines']").exists()).toBe(true); + }); }); diff --git a/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js b/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js index 1ed7844b395..7fdacbe83a2 100644 --- a/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js +++ b/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js @@ -1,6 +1,5 @@ import { shallowMount, createLocalVue } from '@vue/test-utils'; -// eslint-disable-next-line import/no-deprecated -import { getJSONFixture } from 'helpers/fixtures'; +import mockProjects from 'test_fixtures_static/projects.json'; import { trimText } from 'helpers/text_helper'; import ProjectAvatar from '~/vue_shared/components/deprecated_project_avatar/default.vue'; import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue'; @@ -13,8 +12,7 @@ describe('ProjectListItem component', () => { let vm; let options; - // eslint-disable-next-line import/no-deprecated - const project = getJSONFixture('static/projects.json')[0]; + const project = JSON.parse(JSON.stringify(mockProjects))[0]; beforeEach(() => { options = { diff --git a/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js b/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js index 1f97d3ff3fa..de5cee846a1 100644 --- a/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js +++ b/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js @@ -2,8 +2,7 @@ import { GlSearchBoxByType, GlInfiniteScroll } from '@gitlab/ui'; import { mount, createLocalVue } from '@vue/test-utils'; import { head } from 'lodash'; import Vue from 'vue'; -// eslint-disable-next-line import/no-deprecated -import { getJSONFixture } from 'helpers/fixtures'; +import mockProjects from 'test_fixtures_static/projects.json'; import { trimText } from 'helpers/text_helper'; import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue'; import ProjectSelector from '~/vue_shared/components/project_selector/project_selector.vue'; @@ -13,8 +12,7 @@ const localVue = createLocalVue(); describe('ProjectSelector component', () => { let wrapper; let vm; - // eslint-disable-next-line import/no-deprecated - const allProjects = getJSONFixture('static/projects.json'); + const allProjects = mockProjects; const searchResults = allProjects.slice(0, 5); let selected = []; selected = selected.concat(allProjects.slice(0, 3)).concat(allProjects.slice(5, 8)); diff --git a/spec/frontend/vue_shared/components/registry/title_area_spec.js b/spec/frontend/vue_shared/components/registry/title_area_spec.js index 75aa3bc7096..b62676b35be 100644 --- a/spec/frontend/vue_shared/components/registry/title_area_spec.js +++ b/spec/frontend/vue_shared/components/registry/title_area_spec.js @@ -1,5 +1,6 @@ import { GlAvatar, GlSprintf, GlLink, GlSkeletonLoader } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import component from '~/vue_shared/components/registry/title_area.vue'; describe('title area', () => { @@ -7,18 +8,18 @@ describe('title area', () => { const DYNAMIC_SLOT = 'metadata-dynamic-slot'; - const findSubHeaderSlot = () => wrapper.find('[data-testid="sub-header"]'); - const findRightActionsSlot = () => wrapper.find('[data-testid="right-actions"]'); - const findMetadataSlot = (name) => wrapper.find(`[data-testid="${name}"]`); - const findTitle = () => wrapper.find('[data-testid="title"]'); - const findAvatar = () => wrapper.find(GlAvatar); - const findInfoMessages = () => wrapper.findAll('[data-testid="info-message"]'); - const findDynamicSlot = () => wrapper.find(`[data-testid="${DYNAMIC_SLOT}`); + const findSubHeaderSlot = () => wrapper.findByTestId('sub-header'); + const findRightActionsSlot = () => wrapper.findByTestId('right-actions'); + const findMetadataSlot = (name) => wrapper.findByTestId(name); + const findTitle = () => wrapper.findByTestId('title'); + const findAvatar = () => wrapper.findComponent(GlAvatar); + const findInfoMessages = () => wrapper.findAllByTestId('info-message'); + const findDynamicSlot = () => wrapper.findByTestId(DYNAMIC_SLOT); const findSlotOrderElements = () => wrapper.findAll('[slot-test]'); - const findSkeletonLoader = () => wrapper.find(GlSkeletonLoader); + const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader); const mountComponent = ({ propsData = { title: 'foo' }, slots } = {}) => { - wrapper = shallowMount(component, { + wrapper = shallowMountExtended(component, { propsData, stubs: { GlSprintf }, slots: { @@ -29,6 +30,12 @@ describe('title area', () => { }); }; + const generateSlotMocks = (names) => + names.reduce((acc, current) => { + acc[current] = `<div data-testid="${current}" />`; + return acc; + }, {}); + afterEach(() => { wrapper.destroy(); wrapper = null; @@ -40,6 +47,7 @@ describe('title area', () => { expect(findTitle().text()).toBe('foo'); }); + it('if slot is present uses slot', () => { mountComponent({ slots: { @@ -88,24 +96,21 @@ describe('title area', () => { ${['metadata-foo', 'metadata-bar']} ${['metadata-foo', 'metadata-bar', 'metadata-baz']} `('$slotNames metadata slots', ({ slotNames }) => { - const slotMocks = slotNames.reduce((acc, current) => { - acc[current] = `<div data-testid="${current}" />`; - return acc; - }, {}); + const slots = generateSlotMocks(slotNames); it('exist when the slot is present', async () => { - mountComponent({ slots: slotMocks }); + mountComponent({ slots }); - await wrapper.vm.$nextTick(); + await nextTick(); slotNames.forEach((name) => { expect(findMetadataSlot(name).exists()).toBe(true); }); }); it('is/are hidden when metadata-loading is true', async () => { - mountComponent({ slots: slotMocks, propsData: { title: 'foo', metadataLoading: true } }); + mountComponent({ slots, propsData: { title: 'foo', metadataLoading: true } }); - await wrapper.vm.$nextTick(); + await nextTick(); slotNames.forEach((name) => { expect(findMetadataSlot(name).exists()).toBe(false); }); @@ -113,14 +118,20 @@ describe('title area', () => { }); describe('metadata skeleton loader', () => { - it('is hidden when metadata loading is false', () => { - mountComponent(); + const slots = generateSlotMocks(['metadata-foo']); + + it('is hidden when metadata loading is false', async () => { + mountComponent({ slots }); + + await nextTick(); expect(findSkeletonLoader().exists()).toBe(false); }); - it('is shown when metadata loading is true', () => { - mountComponent({ propsData: { metadataLoading: true } }); + it('is shown when metadata loading is true', async () => { + mountComponent({ propsData: { metadataLoading: true }, slots }); + + await nextTick(); expect(findSkeletonLoader().exists()).toBe(true); }); @@ -143,7 +154,7 @@ describe('title area', () => { // updating the slots like we do on line 141 does not cause the updated lifecycle-hook to be triggered wrapper.vm.$forceUpdate(); - await wrapper.vm.$nextTick(); + await nextTick(); expect(findDynamicSlot().exists()).toBe(true); }); @@ -163,7 +174,7 @@ describe('title area', () => { // updating the slots like we do on line 159 does not cause the updated lifecycle-hook to be triggered wrapper.vm.$forceUpdate(); - await wrapper.vm.$nextTick(); + await nextTick(); expect(findSlotOrderElements().at(0).attributes('data-testid')).toBe(DYNAMIC_SLOT); expect(findSlotOrderElements().at(1).attributes('data-testid')).toBe('metadata-foo'); diff --git a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js index 32ef2d27ba7..8536ffed573 100644 --- a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js +++ b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js @@ -1,4 +1,4 @@ -import { GlAlert, GlButton, GlLoadingIcon, GlSkeletonLoader } from '@gitlab/ui'; +import { GlAlert, GlModal, GlButton, GlLoadingIcon, GlSkeletonLoader } from '@gitlab/ui'; import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils'; import { shallowMount, createLocalVue } from '@vue/test-utils'; import { nextTick } from 'vue'; @@ -52,7 +52,7 @@ describe('RunnerInstructionsModal component', () => { const findBinaryInstructions = () => wrapper.findByTestId('binary-instructions'); const findRegisterCommand = () => wrapper.findByTestId('register-command'); - const createComponent = () => { + const createComponent = ({ props, ...options } = {}) => { const requestHandlers = [ [getRunnerPlatformsQuery, runnerPlatformsHandler], [getRunnerSetupInstructionsQuery, runnerSetupInstructionsHandler], @@ -64,9 +64,12 @@ describe('RunnerInstructionsModal component', () => { shallowMount(RunnerInstructionsModal, { propsData: { modalId: 'runner-instructions-modal', + registrationToken: 'MY_TOKEN', + ...props, }, localVue, apolloProvider: fakeApollo, + ...options, }), ); }; @@ -118,18 +121,30 @@ describe('RunnerInstructionsModal component', () => { expect(instructions).toBe(installInstructions); }); - it('register command is shown', () => { + it('register command is shown with a replaced token', () => { const instructions = findRegisterCommand().text(); - expect(instructions).toBe(registerInstructions); + expect(instructions).toBe( + 'sudo gitlab-runner register --url http://gdk.test:3000/ --registration-token MY_TOKEN', + ); + }); + + describe('when a register token is not shown', () => { + beforeEach(async () => { + createComponent({ props: { registrationToken: undefined } }); + await nextTick(); + }); + + it('register command is shown without a defined registration token', () => { + const instructions = findRegisterCommand().text(); + + expect(instructions).toBe(registerInstructions); + }); }); }); describe('after a platform and architecture are selected', () => { - const { - installInstructions, - registerInstructions, - } = mockGraphqlInstructionsWindows.data.runnerSetup; + const { installInstructions } = mockGraphqlInstructionsWindows.data.runnerSetup; beforeEach(async () => { runnerSetupInstructionsHandler.mockResolvedValue(mockGraphqlInstructionsWindows); @@ -157,7 +172,9 @@ describe('RunnerInstructionsModal component', () => { it('register command is shown', () => { const command = findRegisterCommand().text(); - expect(command).toBe(registerInstructions); + expect(command).toBe( + './gitlab-runner.exe register --url http://gdk.test:3000/ --registration-token MY_TOKEN', + ); }); }); @@ -217,4 +234,36 @@ describe('RunnerInstructionsModal component', () => { expect(findRegisterCommand().exists()).toBe(false); }); }); + + describe('GlModal API', () => { + const getGlModalStub = (methods) => { + return { + ...GlModal, + methods: { + ...GlModal.methods, + ...methods, + }, + }; + }; + + describe('show()', () => { + let mockShow; + + beforeEach(() => { + mockShow = jest.fn(); + + createComponent({ + stubs: { + GlModal: getGlModalStub({ show: mockShow }), + }, + }); + }); + + it('delegates show()', () => { + wrapper.vm.show(); + + expect(mockShow).toHaveBeenCalledTimes(1); + }); + }); + }); }); diff --git a/spec/frontend/vue_shared/components/settings/__snapshots__/settings_block_spec.js.snap b/spec/frontend/vue_shared/components/settings/__snapshots__/settings_block_spec.js.snap index 165caea2751..a0f46f07d6a 100644 --- a/spec/frontend/vue_shared/components/settings/__snapshots__/settings_block_spec.js.snap +++ b/spec/frontend/vue_shared/components/settings/__snapshots__/settings_block_spec.js.snap @@ -50,6 +50,7 @@ exports[`Settings Block renders the correct markup 1`] = ` class="settings-content" id="settings_content_3" role="region" + style="display: none;" tabindex="-1" > <div diff --git a/spec/frontend/vue_shared/components/settings/settings_block_spec.js b/spec/frontend/vue_shared/components/settings/settings_block_spec.js index 528dfd89690..5e829653c13 100644 --- a/spec/frontend/vue_shared/components/settings/settings_block_spec.js +++ b/spec/frontend/vue_shared/components/settings/settings_block_spec.js @@ -1,12 +1,12 @@ import { GlButton } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue'; describe('Settings Block', () => { let wrapper; const mountComponent = (propsData) => { - wrapper = shallowMount(SettingsBlock, { + wrapper = shallowMountExtended(SettingsBlock, { propsData, slots: { title: '<div data-testid="title-slot"></div>', @@ -20,11 +20,13 @@ describe('Settings Block', () => { wrapper.destroy(); }); - const findDefaultSlot = () => wrapper.find('[data-testid="default-slot"]'); - const findTitleSlot = () => wrapper.find('[data-testid="title-slot"]'); - const findDescriptionSlot = () => wrapper.find('[data-testid="description-slot"]'); + const findDefaultSlot = () => wrapper.findByTestId('default-slot'); + const findTitleSlot = () => wrapper.findByTestId('title-slot'); + const findDescriptionSlot = () => wrapper.findByTestId('description-slot'); const findExpandButton = () => wrapper.findComponent(GlButton); - const findSectionTitleButton = () => wrapper.find('[data-testid="section-title-button"]'); + const findSectionTitleButton = () => wrapper.findByTestId('section-title-button'); + // we are using a non js class for this finder because this class determine the component structure + const findSettingsContent = () => wrapper.find('.settings-content'); const expectExpandedState = ({ expanded = true } = {}) => { const settingsExpandButton = findExpandButton(); @@ -62,6 +64,26 @@ describe('Settings Block', () => { expect(findDescriptionSlot().exists()).toBe(true); }); + it('content is hidden before first expansion', async () => { + // this is a regression test for the bug described here: https://gitlab.com/gitlab-org/gitlab/-/issues/331774 + mountComponent(); + + // content is hidden + expect(findDefaultSlot().isVisible()).toBe(false); + + // expand + await findSectionTitleButton().trigger('click'); + + // content is visible + expect(findDefaultSlot().isVisible()).toBe(true); + + // collapse + await findSectionTitleButton().trigger('click'); + + // content is still visible (and we have a closing animation) + expect(findDefaultSlot().isVisible()).toBe(true); + }); + describe('slide animation behaviour', () => { it('is animated by default', () => { mountComponent(); @@ -81,6 +103,20 @@ describe('Settings Block', () => { expect(wrapper.classes('no-animate')).toBe(noAnimatedClass); }, ); + + it('sets the animating class only during the animation', async () => { + mountComponent(); + + expect(wrapper.classes('animating')).toBe(false); + + await findSectionTitleButton().trigger('click'); + + expect(wrapper.classes('animating')).toBe(true); + + await findSettingsContent().trigger('animationend'); + + expect(wrapper.classes('animating')).toBe(false); + }); }); describe('expanded behaviour', () => { diff --git a/spec/frontend/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js b/spec/frontend/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js index 240d6cb5a34..79e41ed0c9e 100644 --- a/spec/frontend/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js @@ -1,36 +1,68 @@ -import Vue from 'vue'; -import mountComponent from 'helpers/vue_mount_component_helper'; -import collapsedCalendarIcon from '~/vue_shared/components/sidebar/collapsed_calendar_icon.vue'; +import { shallowMount } from '@vue/test-utils'; +import { GlIcon } from '@gitlab/ui'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; -describe('collapsedCalendarIcon', () => { - let vm; - beforeEach(() => { - const CollapsedCalendarIcon = Vue.extend(collapsedCalendarIcon); - vm = mountComponent(CollapsedCalendarIcon, { - containerClass: 'test-class', - text: 'text', - showIcon: false, +import CollapsedCalendarIcon from '~/vue_shared/components/sidebar/collapsed_calendar_icon.vue'; + +describe('CollapsedCalendarIcon', () => { + let wrapper; + + const defaultProps = { + containerClass: 'test-class', + text: 'text', + tooltipText: 'tooltip text', + showIcon: false, + }; + + const createComponent = ({ props = {} } = {}) => { + wrapper = shallowMount(CollapsedCalendarIcon, { + propsData: { ...defaultProps, ...props }, + directives: { + GlTooltip: createMockDirective(), + }, }); + }; + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); }); - it('should add class to container', () => { - expect(vm.$el.classList.contains('test-class')).toEqual(true); + const findGlIcon = () => wrapper.findComponent(GlIcon); + const getTooltip = () => getBinding(wrapper.element, 'gl-tooltip'); + + it('adds class to container', () => { + expect(wrapper.classes()).toContain(defaultProps.containerClass); + }); + + it('does not render calendar icon when showIcon is false', () => { + expect(findGlIcon().exists()).toBe(false); + }); + + it('renders calendar icon when showIcon is true', () => { + createComponent({ + props: { showIcon: true }, + }); + + expect(findGlIcon().exists()).toBe(true); }); - it('should hide calendar icon if showIcon', () => { - expect(vm.$el.querySelector('[data-testid="calendar-icon"]')).toBeNull(); + it('renders text', () => { + expect(wrapper.text()).toBe(defaultProps.text); }); - it('should render text', () => { - expect(vm.$el.querySelector('span').innerText.trim()).toEqual('text'); + it('renders tooltipText as tooltip', () => { + expect(getTooltip().value).toBe(defaultProps.tooltipText); }); - it('should emit click event when container is clicked', () => { - const click = jest.fn(); - vm.$on('click', click); + it('emits click event when container is clicked', async () => { + wrapper.trigger('click'); - vm.$el.click(); + await wrapper.vm.$nextTick(); - expect(click).toHaveBeenCalled(); + expect(wrapper.emitted('click')[0]).toBeDefined(); }); }); diff --git a/spec/frontend/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js b/spec/frontend/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js index 230442ec547..e72b3bf45c4 100644 --- a/spec/frontend/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js @@ -1,86 +1,103 @@ -import Vue from 'vue'; -import mountComponent from 'helpers/vue_mount_component_helper'; -import collapsedGroupedDatePicker from '~/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue'; - -describe('collapsedGroupedDatePicker', () => { - let vm; - beforeEach(() => { - const CollapsedGroupedDatePicker = Vue.extend(collapsedGroupedDatePicker); - vm = mountComponent(CollapsedGroupedDatePicker, { - showToggleSidebar: true, +import { shallowMount } from '@vue/test-utils'; + +import CollapsedGroupedDatePicker from '~/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue'; +import CollapsedCalendarIcon from '~/vue_shared/components/sidebar/collapsed_calendar_icon.vue'; + +describe('CollapsedGroupedDatePicker', () => { + let wrapper; + + const defaultProps = { + showToggleSidebar: true, + }; + + const minDate = new Date('07/17/2016'); + const maxDate = new Date('07/17/2017'); + + const createComponent = ({ props = {} } = {}) => { + wrapper = shallowMount(CollapsedGroupedDatePicker, { + propsData: { ...defaultProps, ...props }, }); + }; + + afterEach(() => { + wrapper.destroy(); }); - describe('toggleCollapse events', () => { - beforeEach((done) => { - jest.spyOn(vm, 'toggleSidebar').mockImplementation(() => {}); - vm.minDate = new Date('07/17/2016'); - Vue.nextTick(done); - }); + const findCollapsedCalendarIcon = () => wrapper.findComponent(CollapsedCalendarIcon); + const findAllCollapsedCalendarIcons = () => wrapper.findAllComponents(CollapsedCalendarIcon); + describe('toggleCollapse events', () => { it('should emit when collapsed-calendar-icon is clicked', () => { - vm.$el.querySelector('.sidebar-collapsed-icon').click(); + createComponent(); - expect(vm.toggleSidebar).toHaveBeenCalled(); + findCollapsedCalendarIcon().trigger('click'); + + expect(wrapper.emitted('toggleCollapse')[0]).toBeDefined(); }); }); describe('minDate and maxDate', () => { - beforeEach((done) => { - vm.minDate = new Date('07/17/2016'); - vm.maxDate = new Date('07/17/2017'); - Vue.nextTick(done); - }); - it('should render both collapsed-calendar-icon', () => { - const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon'); - - expect(icons.length).toEqual(2); - expect(icons[0].innerText.trim()).toEqual('Jul 17 2016'); - expect(icons[1].innerText.trim()).toEqual('Jul 17 2017'); + createComponent({ + props: { + minDate, + maxDate, + }, + }); + + const icons = findAllCollapsedCalendarIcons(); + + expect(icons.length).toBe(2); + expect(icons.at(0).text()).toBe('Jul 17 2016'); + expect(icons.at(1).text()).toBe('Jul 17 2017'); }); }); describe('minDate', () => { - beforeEach((done) => { - vm.minDate = new Date('07/17/2016'); - Vue.nextTick(done); - }); - it('should render minDate in collapsed-calendar-icon', () => { - const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon'); + createComponent({ + props: { + minDate, + }, + }); + + const icons = findAllCollapsedCalendarIcons(); - expect(icons.length).toEqual(1); - expect(icons[0].innerText.trim()).toEqual('From Jul 17 2016'); + expect(icons.length).toBe(1); + expect(icons.at(0).text()).toBe('From Jul 17 2016'); }); }); describe('maxDate', () => { - beforeEach((done) => { - vm.maxDate = new Date('07/17/2017'); - Vue.nextTick(done); - }); - it('should render maxDate in collapsed-calendar-icon', () => { - const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon'); - - expect(icons.length).toEqual(1); - expect(icons[0].innerText.trim()).toEqual('Until Jul 17 2017'); + createComponent({ + props: { + maxDate, + }, + }); + const icons = findAllCollapsedCalendarIcons(); + + expect(icons.length).toBe(1); + expect(icons.at(0).text()).toBe('Until Jul 17 2017'); }); }); describe('no dates', () => { + beforeEach(() => { + createComponent(); + }); + it('should render None', () => { - const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon'); + const icons = findAllCollapsedCalendarIcons(); - expect(icons.length).toEqual(1); - expect(icons[0].innerText.trim()).toEqual('None'); + expect(icons.length).toBe(1); + expect(icons.at(0).text()).toBe('None'); }); it('should have tooltip as `Start and due date`', () => { - const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon'); + const icons = findAllCollapsedCalendarIcons(); - expect(icons[0].title).toBe('Start and due date'); + expect(icons.at(0).props('tooltipText')).toBe('Start and due date'); }); }); }); diff --git a/spec/frontend/vue_shared/components/sidebar/date_picker_spec.js b/spec/frontend/vue_shared/components/sidebar/date_picker_spec.js index 3221e88192b..263d1e9d947 100644 --- a/spec/frontend/vue_shared/components/sidebar/date_picker_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/date_picker_spec.js @@ -1,3 +1,4 @@ +import { GlLoadingIcon } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import DatePicker from '~/vue_shared/components/pikaday.vue'; import SidebarDatePicker from '~/vue_shared/components/sidebar/date_picker.vue'; @@ -5,14 +6,8 @@ import SidebarDatePicker from '~/vue_shared/components/sidebar/date_picker.vue'; describe('SidebarDatePicker', () => { let wrapper; - const mountComponent = (propsData = {}, data = {}) => { - if (wrapper) { - throw new Error('tried to call mountComponent without d'); - } + const createComponent = (propsData = {}, data = {}) => { wrapper = mount(SidebarDatePicker, { - stubs: { - DatePicker: true, - }, propsData, data: () => data, }); @@ -20,87 +15,93 @@ describe('SidebarDatePicker', () => { afterEach(() => { wrapper.destroy(); - wrapper = null; }); + const findDatePicker = () => wrapper.findComponent(DatePicker); + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); + const findEditButton = () => wrapper.find('.title .btn-blank'); + const findRemoveButton = () => wrapper.find('.value-content .btn-blank'); + const findSidebarToggle = () => wrapper.find('.title .gutter-toggle'); + const findValueContent = () => wrapper.find('.value-content'); + it('should emit toggleCollapse when collapsed toggle sidebar is clicked', () => { - mountComponent(); + createComponent(); - wrapper.find('.issuable-sidebar-header .gutter-toggle').element.click(); + wrapper.find('.issuable-sidebar-header .gutter-toggle').trigger('click'); expect(wrapper.emitted('toggleCollapse')).toEqual([[]]); }); it('should render collapsed-calendar-icon', () => { - mountComponent(); + createComponent(); - expect(wrapper.find('.sidebar-collapsed-icon').element).toBeDefined(); + expect(wrapper.find('.sidebar-collapsed-icon').exists()).toBe(true); }); it('should render value when not editing', () => { - mountComponent(); + createComponent(); - expect(wrapper.find('.value-content').element).toBeDefined(); + expect(findValueContent().exists()).toBe(true); }); it('should render None if there is no selectedDate', () => { - mountComponent(); + createComponent(); - expect(wrapper.find('.value-content span').text().trim()).toEqual('None'); + expect(findValueContent().text()).toBe('None'); }); it('should render date-picker when editing', () => { - mountComponent({}, { editing: true }); + createComponent({}, { editing: true }); - expect(wrapper.find(DatePicker).element).toBeDefined(); + expect(findDatePicker().exists()).toBe(true); }); it('should render label', () => { const label = 'label'; - mountComponent({ label }); - expect(wrapper.find('.title').text().trim()).toEqual(label); + createComponent({ label }); + expect(wrapper.find('.title').text()).toBe(label); }); it('should render loading-icon when isLoading', () => { - mountComponent({ isLoading: true }); - expect(wrapper.find('.gl-spinner').element).toBeDefined(); + createComponent({ isLoading: true }); + expect(findLoadingIcon().exists()).toBe(true); }); describe('editable', () => { beforeEach(() => { - mountComponent({ editable: true }); + createComponent({ editable: true }); }); it('should render edit button', () => { - expect(wrapper.find('.title .btn-blank').text().trim()).toEqual('Edit'); + expect(findEditButton().text()).toBe('Edit'); }); it('should enable editing when edit button is clicked', async () => { - wrapper.find('.title .btn-blank').element.click(); + findEditButton().trigger('click'); await wrapper.vm.$nextTick(); - expect(wrapper.vm.editing).toEqual(true); + expect(wrapper.vm.editing).toBe(true); }); }); it('should render date if selectedDate', () => { - mountComponent({ selectedDate: new Date('07/07/2017') }); + createComponent({ selectedDate: new Date('07/07/2017') }); - expect(wrapper.find('.value-content strong').text().trim()).toEqual('Jul 7, 2017'); + expect(wrapper.find('.value-content strong').text()).toBe('Jul 7, 2017'); }); describe('selectedDate and editable', () => { beforeEach(() => { - mountComponent({ selectedDate: new Date('07/07/2017'), editable: true }); + createComponent({ selectedDate: new Date('07/07/2017'), editable: true }); }); it('should render remove button if selectedDate and editable', () => { - expect(wrapper.find('.value-content .btn-blank').text().trim()).toEqual('remove'); + expect(findRemoveButton().text()).toBe('remove'); }); it('should emit saveDate with null when remove button is clicked', () => { - wrapper.find('.value-content .btn-blank').element.click(); + findRemoveButton().trigger('click'); expect(wrapper.emitted('saveDate')).toEqual([[null]]); }); @@ -108,15 +109,15 @@ describe('SidebarDatePicker', () => { describe('showToggleSidebar', () => { beforeEach(() => { - mountComponent({ showToggleSidebar: true }); + createComponent({ showToggleSidebar: true }); }); it('should render toggle-sidebar when showToggleSidebar', () => { - expect(wrapper.find('.title .gutter-toggle').element).toBeDefined(); + expect(findSidebarToggle().exists()).toBe(true); }); it('should emit toggleCollapse when toggle sidebar is clicked', () => { - wrapper.find('.title .gutter-toggle').element.click(); + findSidebarToggle().trigger('click'); expect(wrapper.emitted('toggleCollapse')).toEqual([[]]); }); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed_spec.js index 8c1693e8dcc..a7f9391cb5f 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed_spec.js @@ -1,95 +1,74 @@ -import Vue from 'vue'; +import { shallowMount } from '@vue/test-utils'; +import { GlIcon } from '@gitlab/ui'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; +import DropdownValueCollapsedComponent from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed.vue'; -import mountComponent from 'helpers/vue_mount_component_helper'; -import dropdownValueCollapsedComponent from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed.vue'; +import { mockCollapsedLabels as mockLabels, mockRegularLabel } from './mock_data'; -import { mockCollapsedLabels as mockLabels } from './mock_data'; - -const createComponent = (labels = mockLabels) => { - const Component = Vue.extend(dropdownValueCollapsedComponent); +describe('DropdownValueCollapsedComponent', () => { + let wrapper; - return mountComponent(Component, { - labels, - }); -}; + const defaultProps = { + labels: [], + }; -describe('DropdownValueCollapsedComponent', () => { - let vm; + const mockManyLabels = [...mockLabels, ...mockLabels, ...mockLabels]; - beforeEach(() => { - vm = createComponent(); - }); + const createComponent = ({ props = {} } = {}) => { + wrapper = shallowMount(DropdownValueCollapsedComponent, { + propsData: { ...defaultProps, ...props }, + directives: { + GlTooltip: createMockDirective(), + }, + }); + }; afterEach(() => { - vm.$destroy(); + wrapper.destroy(); }); - describe('computed', () => { - describe('labelsList', () => { - it('returns default text when `labels` prop is empty array', () => { - const vmEmptyLabels = createComponent([]); + const findGlIcon = () => wrapper.findComponent(GlIcon); + const getTooltip = () => getBinding(wrapper.element, 'gl-tooltip'); - expect(vmEmptyLabels.labelsList).toBe('Labels'); - vmEmptyLabels.$destroy(); - }); - - it('returns labels names separated by coma when `labels` prop has more than one item', () => { - const labels = mockLabels.concat(mockLabels); - const vmMoreLabels = createComponent(labels); + describe('template', () => { + it('renders tags icon element', () => { + createComponent(); - const expectedText = labels.map((label) => label.title).join(', '); + expect(findGlIcon().exists()).toBe(true); + }); - expect(vmMoreLabels.labelsList).toBe(expectedText); - vmMoreLabels.$destroy(); - }); + it('emits onValueClick event on click', async () => { + createComponent(); - it('returns labels names separated by coma with remaining labels count and `and more` phrase when `labels` prop has more than five items', () => { - const mockMoreLabels = Object.assign([], mockLabels); - for (let i = 0; i < 6; i += 1) { - mockMoreLabels.unshift(mockLabels[0]); - } + wrapper.trigger('click'); - const vmMoreLabels = createComponent(mockMoreLabels); + await wrapper.vm.$nextTick(); - const expectedText = `${mockMoreLabels - .slice(0, 5) - .map((label) => label.title) - .join(', ')}, and ${mockMoreLabels.length - 5} more`; + expect(wrapper.emitted('onValueClick')[0]).toBeDefined(); + }); - expect(vmMoreLabels.labelsList).toBe(expectedText); - vmMoreLabels.$destroy(); + describe.each` + scenario | labels | expectedResult | expectedText + ${'`labels` is empty'} | ${[]} | ${'default text'} | ${'Labels'} + ${'`labels` has 1 item'} | ${[mockRegularLabel]} | ${'label name'} | ${'Foo Label'} + ${'`labels` has 2 items'} | ${mockLabels} | ${'comma separated label names'} | ${'Foo Label, Foo::Bar'} + ${'`labels` has more than 5 items'} | ${mockManyLabels} | ${'comma separated label names with "and more" phrase'} | ${'Foo Label, Foo::Bar, Foo Label, Foo::Bar, Foo Label, and 1 more'} + `('when $scenario', ({ labels, expectedResult, expectedText }) => { + beforeEach(() => { + createComponent({ + props: { + labels, + }, + }); }); - it('returns first label name when `labels` prop has only one item present', () => { - const text = mockLabels.map((label) => label.title).join(', '); - - expect(vm.labelsList).toBe(text); + it('renders labels count', () => { + expect(wrapper.text()).toBe(`${labels.length}`); }); - }); - }); - - describe('methods', () => { - describe('handleClick', () => { - it('emits onValueClick event on component', () => { - jest.spyOn(vm, '$emit').mockImplementation(() => {}); - vm.handleClick(); - expect(vm.$emit).toHaveBeenCalledWith('onValueClick'); + it(`renders "${expectedResult}" as tooltip`, () => { + expect(getTooltip().value).toBe(expectedText); }); }); }); - - describe('template', () => { - it('renders component container element with tooltip`', () => { - expect(vm.$el.title).toBe(vm.labelsList); - }); - - it('renders tags icon element', () => { - expect(vm.$el.querySelector('[data-testid="labels-icon"]')).not.toBeNull(); - }); - - it('renders labels count', () => { - expect(vm.$el.querySelector('span').innerText.trim()).toBe(`${vm.labels.length}`); - }); - }); }); 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 d9b7cd5afa2..a60e6f52862 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 @@ -1,3 +1,4 @@ +import { cloneDeep } from 'lodash'; import * as types from '~/vue_shared/components/sidebar/labels_select_vue/store/mutation_types'; import mutations from '~/vue_shared/components/sidebar/labels_select_vue/store/mutations'; @@ -153,47 +154,40 @@ describe('LabelsSelect Mutations', () => { }); describe(`${types.UPDATE_SELECTED_LABELS}`, () => { - let labels; - - beforeEach(() => { - labels = [ - { id: 1, title: 'scoped' }, - { id: 2, title: 'scoped::one', set: false }, - { id: 3, title: 'scoped::test', set: true }, - { id: 4, title: '' }, - ]; - }); - - it('updates `state.labels` to include `touched` and `set` props based on provided `labels` param', () => { - const updatedLabelIds = [2]; - const state = { - labels, - }; - mutations[types.UPDATE_SELECTED_LABELS](state, { labels: [{ id: 2 }] }); - - state.labels.forEach((label) => { - if (updatedLabelIds.includes(label.id)) { - expect(label.touched).toBe(true); - expect(label.set).toBe(true); - } + const labels = [ + { id: 1, title: 'scoped' }, + { id: 2, title: 'scoped::label::one', set: false }, + { id: 3, title: 'scoped::label::two', set: false }, + { id: 4, title: 'scoped::label::three', set: true }, + { id: 5, title: 'scoped::one', set: false }, + { id: 6, title: 'scoped::two', set: false }, + { id: 7, title: 'scoped::three', set: true }, + { id: 8, title: '' }, + ]; + + it.each` + label | labelGroupIds + ${labels[0]} | ${[]} + ${labels[1]} | ${[labels[2], labels[3]]} + ${labels[2]} | ${[labels[1], labels[3]]} + ${labels[3]} | ${[labels[1], labels[2]]} + ${labels[4]} | ${[labels[5], labels[6]]} + ${labels[5]} | ${[labels[4], labels[6]]} + ${labels[6]} | ${[labels[4], labels[5]]} + ${labels[7]} | ${[]} + `('updates `touched` and `set` props for $label.title', ({ label, labelGroupIds }) => { + const state = { labels: cloneDeep(labels) }; + + mutations[types.UPDATE_SELECTED_LABELS](state, { labels: [{ id: label.id }] }); + + expect(state.labels[label.id - 1]).toMatchObject({ + touched: true, + set: !labels[label.id - 1].set, }); - }); - describe('when label is scoped', () => { - it('unsets the currently selected scoped label and sets the current label', () => { - const state = { - labels, - }; - mutations[types.UPDATE_SELECTED_LABELS](state, { - labels: [{ id: 2, title: 'scoped::one' }], - }); - - expect(state.labels).toEqual([ - { id: 1, title: 'scoped' }, - { id: 2, title: 'scoped::one', set: true, touched: true }, - { id: 3, title: 'scoped::test', set: false }, - { id: 4, title: '' }, - ]); + labelGroupIds.forEach((l) => { + expect(state.labels[l.id - 1].touched).toBeFalsy(); + expect(state.labels[l.id - 1].set).toBe(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 8931584e12c..bf873f9162b 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 @@ -5,8 +5,7 @@ import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import createFlash from '~/flash'; -import { IssuableType } from '~/issue_show/constants'; -import { labelsQueries } from '~/sidebar/constants'; +import { workspaceLabelsQueries } from '~/sidebar/constants'; import DropdownContentsCreateView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue'; import createLabelMutation from '~/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql'; import { @@ -50,11 +49,12 @@ describe('DropdownContentsCreateView', () => { const createComponent = ({ mutationHandler = createLabelSuccessHandler, - issuableType = IssuableType.Issue, + labelCreateType = 'project', + workspaceType = 'project', } = {}) => { const mockApollo = createMockApollo([[createLabelMutation, mutationHandler]]); mockApollo.clients.defaultClient.cache.writeQuery({ - query: labelsQueries[issuableType].workspaceQuery, + query: workspaceLabelsQueries[workspaceType].query, data: workspaceLabelsQueryResponse.data, variables: { fullPath: '', @@ -66,8 +66,10 @@ describe('DropdownContentsCreateView', () => { localVue, apolloProvider: mockApollo, propsData: { - issuableType, fullPath: '', + attrWorkspacePath: '', + labelCreateType, + workspaceType, }, }); }; @@ -128,9 +130,11 @@ describe('DropdownContentsCreateView', () => { it('emits a `hideCreateView` event on Cancel button click', () => { createComponent(); - findCancelButton().vm.$emit('click'); + const event = { stopPropagation: jest.fn() }; + findCancelButton().vm.$emit('click', event); expect(wrapper.emitted('hideCreateView')).toHaveLength(1); + expect(event.stopPropagation).toHaveBeenCalled(); }); describe('when label title and selected color are set', () => { @@ -174,7 +178,7 @@ describe('DropdownContentsCreateView', () => { }); it('calls a mutation with `groupPath` variable on the epic', () => { - createComponent({ issuableType: IssuableType.Epic }); + createComponent({ labelCreateType: 'group', workspaceType: 'group' }); fillLabelAttributes(); findCreateButton().vm.$emit('click'); 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 fac3331a2b8..2980409fdce 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 @@ -10,7 +10,6 @@ import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import createFlash from '~/flash'; -import { IssuableType } from '~/issue_show/constants'; 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'; @@ -43,6 +42,7 @@ describe('DropdownContentsLabelsView', () => { initialState = mockConfig, queryHandler = successfulQueryHandler, injected = {}, + searchKey = '', } = {}) => { const mockApollo = createMockApollo([[projectLabelsQuery, queryHandler]]); @@ -56,7 +56,9 @@ describe('DropdownContentsLabelsView', () => { propsData: { ...initialState, localSelectedLabels, - issuableType: IssuableType.Issue, + searchKey, + labelCreateType: 'project', + workspaceType: 'project', }, stubs: { GlSearchBoxByType, @@ -68,7 +70,6 @@ describe('DropdownContentsLabelsView', () => { wrapper.destroy(); }); - const findSearchInput = () => wrapper.findComponent(GlSearchBoxByType); const findLabels = () => wrapper.findAllComponents(LabelItem); const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findObserver = () => wrapper.findComponent(GlIntersectionObserver); @@ -81,12 +82,6 @@ describe('DropdownContentsLabelsView', () => { } describe('when loading labels', () => { - it('renders disabled search input field', async () => { - createComponent(); - await makeObserverAppear(); - expect(findSearchInput().props('disabled')).toBe(true); - }); - it('renders loading icon', async () => { createComponent(); await makeObserverAppear(); @@ -107,10 +102,6 @@ describe('DropdownContentsLabelsView', () => { await waitForPromises(); }); - it('renders enabled search input field', async () => { - expect(findSearchInput().props('disabled')).toBe(false); - }); - it('does not render loading icon', async () => { expect(findLoadingIcon().exists()).toBe(false); }); @@ -132,9 +123,9 @@ describe('DropdownContentsLabelsView', () => { }, }, }), + searchKey: '123', }); await makeObserverAppear(); - findSearchInput().vm.$emit('input', '123'); await waitForPromises(); await nextTick(); 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 36704ac5ef3..8bcef347c96 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 @@ -4,6 +4,8 @@ import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_w 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 DropdownHeader from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue'; +import DropdownFooter from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_footer.vue'; import { mockLabels } from './mock_data'; @@ -26,7 +28,7 @@ const GlDropdownStub = { describe('DropdownContent', () => { let wrapper; - const createComponent = ({ props = {}, injected = {}, data = {} } = {}) => { + const createComponent = ({ props = {}, data = {} } = {}) => { wrapper = shallowMount(DropdownContents, { propsData: { labelsCreateTitle: 'test', @@ -37,8 +39,10 @@ describe('DropdownContent', () => { footerManageLabelTitle: 'manage', dropdownButtonText: 'Labels', variant: 'sidebar', - issuableType: 'issue', fullPath: 'test', + workspaceType: 'project', + labelCreateType: 'project', + attrWorkspacePath: 'path', ...props, }, data() { @@ -46,11 +50,6 @@ describe('DropdownContent', () => { ...data, }; }, - provide: { - allowLabelCreate: true, - labelsManagePath: 'foo/bar', - ...injected, - }, stubs: { GlDropdown: GlDropdownStub, }, @@ -63,13 +62,10 @@ describe('DropdownContent', () => { const findCreateView = () => wrapper.findComponent(DropdownContentsCreateView); const findLabelsView = () => wrapper.findComponent(DropdownContentsLabelsView); + const findDropdownHeader = () => wrapper.findComponent(DropdownHeader); + const findDropdownFooter = () => wrapper.findComponent(DropdownFooter); const findDropdown = () => wrapper.findComponent(GlDropdownStub); - const findDropdownFooter = () => wrapper.find('[data-testid="dropdown-footer"]'); - const findDropdownHeader = () => wrapper.find('[data-testid="dropdown-header"]'); - const findCreateLabelButton = () => wrapper.find('[data-testid="create-label-button"]'); - const findGoBackButton = () => wrapper.find('[data-testid="go-back-button"]'); - it('calls dropdown `show` method on `isVisible` prop change', async () => { createComponent(); await wrapper.setProps({ @@ -136,6 +132,16 @@ describe('DropdownContent', () => { expect(findDropdownHeader().exists()).toBe(true); }); + it('sets searchKey for labels view on input event from header', async () => { + createComponent(); + + expect(wrapper.vm.searchKey).toEqual(''); + findDropdownHeader().vm.$emit('input', '123'); + await nextTick(); + + expect(findLabelsView().props('searchKey')).toEqual('123'); + }); + describe('Create view', () => { beforeEach(() => { createComponent({ data: { showDropdownContentsCreateView: true } }); @@ -149,16 +155,8 @@ describe('DropdownContent', () => { 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); - }); - - it('changes the view to Labels view on back button click', async () => { - findGoBackButton().vm.$emit('click', new MouseEvent('click')); + it('changes the view to Labels view on `toggleDropdownContentsCreateView` event', async () => { + findDropdownHeader().vm.$emit('toggleDropdownContentsCreateView'); await nextTick(); expect(findCreateView().exists()).toBe(false); @@ -198,32 +196,5 @@ describe('DropdownContent', () => { expect(findDropdownFooter().exists()).toBe(true); }); - - it('does not render go back button', () => { - expect(findGoBackButton().exists()).toBe(false); - }); - - 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('changes the view to Create on create label button click', async () => { - findCreateLabelButton().trigger('click'); - - await nextTick(); - expect(findLabelsView().exists()).toBe(false); - }); - }); }); }); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_footer_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_footer_spec.js new file mode 100644 index 00000000000..0508a059195 --- /dev/null +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_footer_spec.js @@ -0,0 +1,57 @@ +import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; +import DropdownFooter from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_footer.vue'; + +describe('DropdownFooter', () => { + let wrapper; + + const createComponent = ({ props = {}, injected = {} } = {}) => { + wrapper = shallowMount(DropdownFooter, { + propsData: { + footerCreateLabelTitle: 'create', + footerManageLabelTitle: 'manage', + ...props, + }, + provide: { + allowLabelCreate: true, + labelsManagePath: 'foo/bar', + ...injected, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + const findCreateLabelButton = () => wrapper.find('[data-testid="create-label-button"]'); + + describe('Labels view', () => { + beforeEach(() => { + createComponent(); + }); + + 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', async () => { + findCreateLabelButton().trigger('click'); + + await nextTick(); + expect(wrapper.emitted('toggleDropdownContentsCreateView')).toEqual([[]]); + }); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_header_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_header_spec.js new file mode 100644 index 00000000000..592559ef305 --- /dev/null +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_header_spec.js @@ -0,0 +1,75 @@ +import { GlSearchBoxByType } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import DropdownHeader from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue'; + +describe('DropdownHeader', () => { + let wrapper; + + const createComponent = ({ + showDropdownContentsCreateView = false, + labelsFetchInProgress = false, + } = {}) => { + wrapper = extendedWrapper( + shallowMount(DropdownHeader, { + propsData: { + showDropdownContentsCreateView, + labelsFetchInProgress, + labelsCreateTitle: 'Create label', + labelsListTitle: 'Select label', + searchKey: '', + }, + stubs: { + GlSearchBoxByType, + }, + }), + ); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + const findSearchInput = () => wrapper.findComponent(GlSearchBoxByType); + const findGoBackButton = () => wrapper.findByTestId('go-back-button'); + + beforeEach(() => { + createComponent(); + }); + + describe('Create view', () => { + beforeEach(() => { + createComponent({ showDropdownContentsCreateView: true }); + }); + + it('renders go back button', () => { + expect(findGoBackButton().exists()).toBe(true); + }); + + it('does not render search input field', async () => { + expect(findSearchInput().exists()).toBe(false); + }); + }); + + describe('Labels view', () => { + beforeEach(() => { + createComponent(); + }); + + it('does not render go back button', () => { + expect(findGoBackButton().exists()).toBe(false); + }); + + it.each` + labelsFetchInProgress | disabled + ${true} | ${true} + ${false} | ${false} + `( + 'when labelsFetchInProgress is $labelsFetchInProgress, renders search input with disabled prop to $disabled', + ({ labelsFetchInProgress, disabled }) => { + createComponent({ labelsFetchInProgress }); + expect(findSearchInput().props('disabled')).toBe(disabled); + }, + ); + }); +}); 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 b5441d711a5..d4203528874 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 @@ -41,6 +41,8 @@ describe('LabelsSelectRoot', () => { propsData: { ...config, issuableType: IssuableType.Issue, + labelCreateType: 'project', + workspaceType: 'project', }, stubs: { SidebarEditableItem, @@ -121,11 +123,11 @@ describe('LabelsSelectRoot', () => { }); }); - it('emits `updateSelectedLabels` event on dropdown contents `setLabels` event', async () => { + it('emits `updateSelectedLabels` event on dropdown contents `setLabels` event if iid is not set', async () => { const label = { id: 'gid://gitlab/ProjectLabel/1' }; - createComponent(); + createComponent({ config: { ...mockConfig, iid: undefined } }); findDropdownContents().vm.$emit('setLabels', [label]); - expect(wrapper.emitted('updateSelectedLabels')).toEqual([[[label]]]); + expect(wrapper.emitted('updateSelectedLabels')).toEqual([[{ labels: [label] }]]); }); }); 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 23a457848d9..5c5bf5f2187 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 @@ -40,12 +40,12 @@ export const mockConfig = { labelsListTitle: 'Assign labels', labelsCreateTitle: 'Create label', variant: 'sidebar', - selectedLabels: [mockRegularLabel, mockScopedLabel], labelsSelectInProgress: false, labelsFilterBasePath: '/gitlab-org/my-project/issues', labelsFilterParam: 'label_name', footerCreateLabelTitle: 'create', footerManageLabelTitle: 'manage', + attrWorkspacePath: 'test', }; export const mockSuggestedColors = { @@ -80,6 +80,7 @@ export const createLabelSuccessfulResponse = { color: '#dc143c', description: null, title: 'ewrwrwer', + textColor: '#000000', __typename: 'Label', }, errors: [], @@ -91,6 +92,7 @@ export const createLabelSuccessfulResponse = { export const workspaceLabelsQueryResponse = { data: { workspace: { + id: 'gid://gitlab/Project/126', labels: { nodes: [ { @@ -98,12 +100,14 @@ export const workspaceLabelsQueryResponse = { description: null, id: 'gid://gitlab/ProjectLabel/1', title: 'Label1', + textColor: '#000000', }, { color: '#2f7b2e', description: null, id: 'gid://gitlab/ProjectLabel/2', title: 'Label2', + textColor: '#000000', }, ], }, @@ -123,6 +127,7 @@ export const issuableLabelsQueryResponse = { description: null, id: 'gid://gitlab/ProjectLabel/1', title: 'Label1', + textColor: '#000000', }, ], }, diff --git a/spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js b/spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js index f1c3e8a1ddc..a6c9bda1aa2 100644 --- a/spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js @@ -1,31 +1,45 @@ -import Vue from 'vue'; -import mountComponent from 'helpers/vue_mount_component_helper'; -import toggleSidebar from '~/vue_shared/components/sidebar/toggle_sidebar.vue'; - -describe('toggleSidebar', () => { - let vm; - beforeEach(() => { - const ToggleSidebar = Vue.extend(toggleSidebar); - vm = mountComponent(ToggleSidebar, { - collapsed: true, +import { GlButton } from '@gitlab/ui'; +import { mount, shallowMount } from '@vue/test-utils'; + +import ToggleSidebar from '~/vue_shared/components/sidebar/toggle_sidebar.vue'; + +describe('ToggleSidebar', () => { + let wrapper; + + const defaultProps = { + collapsed: true, + }; + + const createComponent = ({ mountFn = shallowMount, props = {} } = {}) => { + wrapper = mountFn(ToggleSidebar, { + propsData: { ...defaultProps, ...props }, }); + }; + + afterEach(() => { + wrapper.destroy(); }); + const findGlButton = () => wrapper.findComponent(GlButton); + it('should render the "chevron-double-lg-left" icon when collapsed', () => { - expect(vm.$el.querySelector('[data-testid="chevron-double-lg-left-icon"]')).not.toBeNull(); + createComponent(); + + expect(findGlButton().props('icon')).toBe('chevron-double-lg-left'); }); it('should render the "chevron-double-lg-right" icon when expanded', async () => { - vm.collapsed = false; - await Vue.nextTick(); - expect(vm.$el.querySelector('[data-testid="chevron-double-lg-right-icon"]')).not.toBeNull(); + createComponent({ props: { collapsed: false } }); + + expect(findGlButton().props('icon')).toBe('chevron-double-lg-right'); }); - it('should emit toggle event when button clicked', () => { - const toggle = jest.fn(); - vm.$on('toggle', toggle); - vm.$el.click(); + it('should emit toggle event when button clicked', async () => { + createComponent({ mountFn: mount }); + + findGlButton().trigger('click'); + await wrapper.vm.$nextTick(); - expect(toggle).toHaveBeenCalled(); + expect(wrapper.emitted('toggle')[0]).toBeDefined(); }); }); diff --git a/spec/frontend/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list_spec.js b/spec/frontend/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list_spec.js index a92f058f311..78abb89e7b8 100644 --- a/spec/frontend/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list_spec.js +++ b/spec/frontend/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list_spec.js @@ -82,7 +82,7 @@ describe('User deletion obstacles list', () => { createComponent({ obstacles: [{ type, name, url, projectName, projectUrl }] }); const msg = findObstacles().text(); - expect(msg).toContain(`in Project ${projectName}`); + expect(msg).toContain(`in project ${projectName}`); expect(findLinks().at(1).attributes('href')).toBe(projectUrl); }); }, |