diff options
Diffstat (limited to 'spec/frontend/sidebar')
4 files changed, 422 insertions, 194 deletions
diff --git a/spec/frontend/sidebar/components/milestone/milestone_dropdown_spec.js b/spec/frontend/sidebar/components/milestone/milestone_dropdown_spec.js new file mode 100644 index 00000000000..843ac1da4bb --- /dev/null +++ b/spec/frontend/sidebar/components/milestone/milestone_dropdown_spec.js @@ -0,0 +1,93 @@ +import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import { IssuableType, WorkspaceType } from '~/issues/constants'; +import { __ } from '~/locale'; +import MilestoneDropdown from '~/sidebar/components/milestone/milestone_dropdown.vue'; +import SidebarDropdown from '~/sidebar/components/sidebar_dropdown.vue'; + +describe('MilestoneDropdown component', () => { + let wrapper; + + const propsData = { + attrWorkspacePath: 'full/path', + issuableType: IssuableType.Issue, + workspaceType: WorkspaceType.project, + }; + + const findHiddenInput = () => wrapper.find('input'); + const findSidebarDropdown = () => wrapper.findComponent(SidebarDropdown); + + const createComponent = (props = {}) => { + wrapper = shallowMount(MilestoneDropdown, { propsData: { ...propsData, ...props } }); + }; + + it('renders SidebarDropdown', () => { + createComponent(); + + expect(findSidebarDropdown().props()).toMatchObject({ + attrWorkspacePath: propsData.attrWorkspacePath, + issuableAttribute: MilestoneDropdown.issuableAttribute, + issuableType: propsData.issuableType, + workspaceType: propsData.workspaceType, + }); + }); + + it('renders hidden input', () => { + createComponent(); + + expect(findHiddenInput().attributes()).toEqual({ + type: 'hidden', + name: 'update[milestone_id]', + value: undefined, + }); + }); + + describe('when milestone ID and title is provided', () => { + it('is used in the dropdown and hidden input', () => { + const milestone = { + id: 'gid://gitlab/Milestone/52', + title: __('Milestone 52'), + }; + createComponent({ milestoneId: milestone.id, milestoneTitle: milestone.title }); + + expect(findSidebarDropdown().props('currentAttribute')).toEqual(milestone); + expect(findHiddenInput().attributes('value')).toBe( + getIdFromGraphQLId(milestone.id).toString(), + ); + }); + }); + + describe('when SidebarDropdown emits `change` event', () => { + beforeEach(() => { + createComponent(); + }); + + describe('when valid milestone is emitted', () => { + it('updates the hidden input value', async () => { + const milestone = { + id: 'gid://gitlab/Milestone/52', + title: __('Milestone 52'), + }; + + findSidebarDropdown().vm.$emit('change', milestone); + await nextTick(); + + expect(findHiddenInput().attributes('value')).toBe( + getIdFromGraphQLId(milestone.id).toString(), + ); + }); + }); + + describe('when null milestone is emitted', () => { + it('updates the hidden input value to `0`', async () => { + const milestone = { id: null }; + + findSidebarDropdown().vm.$emit('change', milestone); + await nextTick(); + + expect(findHiddenInput().attributes('value')).toBe('0'); + }); + }); + }); +}); diff --git a/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_inputs_spec.js b/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_inputs_spec.js new file mode 100644 index 00000000000..277ef6d9561 --- /dev/null +++ b/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_inputs_spec.js @@ -0,0 +1,36 @@ +import { shallowMount } from '@vue/test-utils'; +import SidebarReviewersInputs from '~/sidebar/components/reviewers/sidebar_reviewers_inputs.vue'; +import { state } from '~/sidebar/components/reviewers/sidebar_reviewers.vue'; + +let wrapper; + +function factory() { + wrapper = shallowMount(SidebarReviewersInputs); +} + +describe('Sidebar reviewers inputs component', () => { + it('renders hidden input', () => { + state.issuable.reviewers = { + nodes: [ + { + id: 1, + avatarUrl: '', + name: 'root', + username: 'root', + mergeRequestInteraction: { canMerge: true }, + }, + { + id: 2, + avatarUrl: '', + name: 'root', + username: 'root', + mergeRequestInteraction: { canMerge: true }, + }, + ], + }; + + factory(); + + expect(wrapper.findAll('input[type="hidden"]').length).toBe(2); + }); +}); diff --git a/spec/frontend/sidebar/components/sidebar_dropdown_spec.js b/spec/frontend/sidebar/components/sidebar_dropdown_spec.js new file mode 100644 index 00000000000..83bc8cf7002 --- /dev/null +++ b/spec/frontend/sidebar/components/sidebar_dropdown_spec.js @@ -0,0 +1,285 @@ +import { + GlDropdown, + GlDropdownItem, + GlDropdownText, + GlFormInput, + GlSearchBoxByType, +} from '@gitlab/ui'; +import Vue, { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import { createAlert } from '~/flash'; +import { IssuableType } from '~/issues/constants'; +import SidebarDropdown from '~/sidebar/components/sidebar_dropdown.vue'; +import { IssuableAttributeType } from '~/sidebar/constants'; +import projectIssueMilestoneQuery from '~/sidebar/queries/project_issue_milestone.query.graphql'; +import projectMilestonesQuery from '~/sidebar/queries/project_milestones.query.graphql'; +import { + emptyProjectMilestonesResponse, + mockIssue, + mockProjectMilestonesResponse, + noCurrentMilestoneResponse, +} from '../mock_data'; + +jest.mock('~/flash'); + +describe('SidebarDropdown component', () => { + let wrapper; + + const promiseData = { issuableSetAttribute: { issue: { attribute: { id: '123' } } } }; + const mutationSuccess = () => jest.fn().mockResolvedValue({ data: promiseData }); + + const findDropdown = () => wrapper.findComponent(GlDropdown); + const findDropdownText = () => wrapper.findComponent(GlDropdownText); + const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType); + const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); + const findDropdownItemWithText = (text) => + findAllDropdownItems().wrappers.find((x) => x.text() === text); + const findAttributeItems = () => wrapper.findByTestId('milestone-items'); + const findNoAttributeItem = () => wrapper.findByTestId('no-milestone-item'); + const findLoadingIconDropdown = () => wrapper.findByTestId('loading-icon-dropdown'); + + const toggleDropdown = async () => { + wrapper.vm.$refs.dropdown.show(); + findDropdown().vm.$emit('show'); + + await nextTick(); + jest.runOnlyPendingTimers(); + await waitForPromises(); + }; + + const createComponentWithApollo = ({ + requestHandlers = [], + projectMilestonesSpy = jest.fn().mockResolvedValue(mockProjectMilestonesResponse), + currentMilestoneSpy = jest.fn().mockResolvedValue(noCurrentMilestoneResponse), + } = {}) => { + Vue.use(VueApollo); + + wrapper = mountExtended(SidebarDropdown, { + apolloProvider: createMockApollo([ + [projectMilestonesQuery, projectMilestonesSpy], + [projectIssueMilestoneQuery, currentMilestoneSpy], + ...requestHandlers, + ]), + propsData: { + attrWorkspacePath: mockIssue.projectPath, + currentAttribute: {}, + issuableType: IssuableType.Issue, + issuableAttribute: IssuableAttributeType.Milestone, + }, + attachTo: document.body, + }); + }; + + const createComponent = ({ + props = {}, + data = {}, + mutationPromise = mutationSuccess, + queries = {}, + } = {}) => { + wrapper = mountExtended(SidebarDropdown, { + propsData: { + attrWorkspacePath: mockIssue.projectPath, + currentAttribute: {}, + issuableType: IssuableType.Issue, + issuableAttribute: IssuableAttributeType.Milestone, + ...props, + }, + data() { + return data; + }, + mocks: { + $apollo: { + mutate: mutationPromise(), + queries: { + currentAttribute: { loading: false }, + attributesList: { loading: false }, + ...queries, + }, + }, + }, + }); + }; + + describe('when a user can edit', () => { + describe('when user is editing', () => { + describe('when rendering the dropdown', () => { + it('shows a loading spinner while fetching a list of attributes', async () => { + createComponent({ + queries: { + attributesList: { loading: true }, + }, + }); + + await toggleDropdown(); + + expect(findLoadingIconDropdown().exists()).toBe(true); + }); + + describe('GlDropdownItem with the right title and id', () => { + const id = 'id'; + const title = 'title'; + + beforeEach(async () => { + createComponent({ + props: { currentAttribute: { id, title } }, + data: { attributesList: [{ id, title }] }, + }); + + await toggleDropdown(); + }); + + it('does not show a loading spinner', () => { + expect(findLoadingIconDropdown().exists()).toBe(false); + }); + + it('renders title $title', () => { + expect(findDropdownItemWithText(title).exists()).toBe(true); + }); + + it('checks the correct dropdown item', () => { + expect( + findAllDropdownItems() + .filter((w) => w.props('isChecked') === true) + .at(0) + .text(), + ).toBe(title); + }); + }); + + describe('when no data is assigned', () => { + beforeEach(async () => { + createComponent(); + + await toggleDropdown(); + }); + + it('finds GlDropdownItem with "No milestone"', () => { + expect(findNoAttributeItem().text()).toBe('No milestone'); + }); + + it('"No milestone" is checked', () => { + expect(findAllDropdownItems('No milestone').at(0).props('isChecked')).toBe(true); + }); + + it('does not render any dropdown item', () => { + expect(findAttributeItems().exists()).toBe(false); + }); + }); + + describe('when clicking on dropdown item', () => { + describe('when currentAttribute is equal to attribute id', () => { + it('does not call setIssueAttribute mutation', async () => { + createComponent({ + props: { currentAttribute: { id: 'id', title: 'title' } }, + data: { attributesList: [{ id: 'id', title: 'title' }] }, + }); + + await toggleDropdown(); + + findDropdownItemWithText('title').vm.$emit('click'); + + expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(0); + }); + }); + }); + }); + + describe('when a user is searching', () => { + describe('when search result is not found', () => { + describe('when milestone', () => { + it('renders "No milestone found"', async () => { + createComponent(); + + await toggleDropdown(); + + findSearchBox().vm.$emit('input', 'non existing milestones'); + await nextTick(); + + expect(findDropdownText().text()).toBe('No milestone found'); + }); + }); + }); + }); + }); + }); + + describe('with mock apollo', () => { + describe("when issuable type is 'issue'", () => { + describe('when dropdown is expanded and user can edit', () => { + it('renders the dropdown on clicking edit', async () => { + createComponentWithApollo(); + + await toggleDropdown(); + + expect(findDropdown().isVisible()).toBe(true); + }); + + it('focuses on the input when dropdown is shown', async () => { + createComponentWithApollo(); + + await toggleDropdown(); + + expect(document.activeElement).toEqual(wrapper.findComponent(GlFormInput).element); + }); + + describe('milestones', () => { + it('should call createAlert if milestones query fails', async () => { + createComponentWithApollo({ + projectMilestonesSpy: jest.fn().mockRejectedValue(new Error()), + }); + + await toggleDropdown(); + + expect(createAlert).toHaveBeenCalledWith({ + message: wrapper.vm.i18n.listFetchError, + captureError: true, + error: expect.any(Error), + }); + }); + + it('only fetches attributes when dropdown is opened', async () => { + const projectMilestonesSpy = jest + .fn() + .mockResolvedValueOnce(emptyProjectMilestonesResponse); + createComponentWithApollo({ projectMilestonesSpy }); + + expect(projectMilestonesSpy).not.toHaveBeenCalled(); + + await toggleDropdown(); + + expect(projectMilestonesSpy).toHaveBeenNthCalledWith(1, { + fullPath: mockIssue.projectPath, + state: 'active', + title: '', + }); + }); + + describe('when a user is searching', () => { + it('sends a projectMilestones query with the entered search term "foo"', async () => { + const mockSearchTerm = 'foobar'; + const projectMilestonesSpy = jest + .fn() + .mockResolvedValueOnce(emptyProjectMilestonesResponse); + createComponentWithApollo({ projectMilestonesSpy }); + + await toggleDropdown(); + + findSearchBox().vm.$emit('input', mockSearchTerm); + await nextTick(); + jest.runOnlyPendingTimers(); // Account for debouncing + + expect(projectMilestonesSpy).toHaveBeenNthCalledWith(2, { + fullPath: mockIssue.projectPath, + state: 'active', + title: mockSearchTerm, + }); + }); + }); + }); + }); + }); + }); +}); diff --git a/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js b/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js index 8ab4d8ea051..cf5e220a705 100644 --- a/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js +++ b/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js @@ -1,12 +1,4 @@ -import { - GlDropdown, - GlDropdownItem, - GlDropdownText, - GlLink, - GlSearchBoxByType, - GlFormInput, - GlLoadingIcon, -} from '@gitlab/ui'; +import { GlDropdown, GlLink, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui'; import * as Sentry from '@sentry/browser'; import { shallowMount, mount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; @@ -19,6 +11,7 @@ import { createAlert } from '~/flash'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { IssuableType } from '~/issues/constants'; import { timeFor } from '~/lib/utils/datetime_utility'; +import SidebarDropdown from '~/sidebar/components/sidebar_dropdown.vue'; import SidebarDropdownWidget from '~/sidebar/components/sidebar_dropdown_widget.vue'; import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue'; import { IssuableAttributeType } from '~/sidebar/constants'; @@ -32,7 +25,6 @@ import { noCurrentMilestoneResponse, mockMilestoneMutationResponse, mockMilestone2, - emptyProjectMilestonesResponse, } from '../mock_data'; jest.mock('~/flash'); @@ -55,20 +47,11 @@ describe('SidebarDropdownWidget', () => { const findGlLink = () => wrapper.findComponent(GlLink); const findDateTooltip = () => getBinding(findGlLink().element, 'gl-tooltip'); - const findDropdown = () => wrapper.findComponent(GlDropdown); - const findDropdownText = () => wrapper.findComponent(GlDropdownText); - const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType); - const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); - const findDropdownItemWithText = (text) => - findAllDropdownItems().wrappers.find((x) => x.text() === text); - + const findSidebarDropdown = () => wrapper.findComponent(SidebarDropdown); const findSidebarEditableItem = () => wrapper.findComponent(SidebarEditableItem); const findEditButton = () => findSidebarEditableItem().find('[data-testid="edit-button"]'); const findEditableLoadingIcon = () => findSidebarEditableItem().findComponent(GlLoadingIcon); - const findAttributeItems = () => wrapper.findByTestId('milestone-items'); const findSelectedAttribute = () => wrapper.findByTestId('select-milestone'); - const findNoAttributeItem = () => wrapper.findByTestId('no-milestone-item'); - const findLoadingIconDropdown = () => wrapper.findByTestId('loading-icon-dropdown'); const waitForDropdown = async () => { // BDropdown first changes its `visible` property @@ -167,6 +150,8 @@ describe('SidebarDropdownWidget', () => { }), ); + wrapper.vm.$refs.dropdown.show = jest.fn(); + // We need to mock out `showDropdown` which // invokes `show` method of BDropdown used inside GlDropdown. jest.spyOn(wrapper.vm, 'showDropdown').mockImplementation(); @@ -261,86 +246,7 @@ describe('SidebarDropdownWidget', () => { describe('when a user can edit', () => { describe('when user is editing', () => { describe('when rendering the dropdown', () => { - it('shows a loading spinner while fetching a list of attributes', async () => { - createComponent({ - queries: { - attributesList: { loading: true }, - }, - }); - - await toggleDropdown(); - - expect(findLoadingIconDropdown().exists()).toBe(true); - }); - - describe('GlDropdownItem with the right title and id', () => { - const id = 'id'; - const title = 'title'; - - beforeEach(async () => { - createComponent({ - data: { attributesList: [{ id, title }], currentAttribute: { id, title } }, - }); - - await toggleDropdown(); - }); - - it('does not show a loading spinner', () => { - expect(findLoadingIconDropdown().exists()).toBe(false); - }); - - it('renders title $title', () => { - expect(findDropdownItemWithText(title).exists()).toBe(true); - }); - - it('checks the correct dropdown item', () => { - expect( - findAllDropdownItems() - .filter((w) => w.props('isChecked') === true) - .at(0) - .text(), - ).toBe(title); - }); - }); - - describe('when no data is assigned', () => { - beforeEach(async () => { - createComponent(); - - await toggleDropdown(); - }); - - it('finds GlDropdownItem with "No milestone"', () => { - expect(findNoAttributeItem().text()).toBe('No milestone'); - }); - - it('"No milestone" is checked', () => { - expect(findNoAttributeItem().props('isChecked')).toBe(true); - }); - - it('does not render any dropdown item', () => { - expect(findAttributeItems().exists()).toBe(false); - }); - }); - describe('when clicking on dropdown item', () => { - describe('when currentAttribute is equal to attribute id', () => { - it('does not call setIssueAttribute mutation', async () => { - createComponent({ - data: { - attributesList: [{ id: 'id', title: 'title' }], - currentAttribute: { id: 'id', title: 'title' }, - }, - }); - - await toggleDropdown(); - - findDropdownItemWithText('title').vm.$emit('click'); - - expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(0); - }); - }); - describe('when currentAttribute is not equal to attribute id', () => { describe('when error', () => { const bootstrapComponent = (mutationResp) => { @@ -350,7 +256,7 @@ describe('SidebarDropdownWidget', () => { { id: '123', title: '123' }, { id: 'id', title: 'title' }, ], - currentAttribute: '123', + currentAttribute: { id: '123' }, }, mutationPromise: mutationResp, }); @@ -366,7 +272,7 @@ describe('SidebarDropdownWidget', () => { await toggleDropdown(); - findDropdownItemWithText('title').vm.$emit('click'); + findSidebarDropdown().vm.$emit('change', { id: 'error' }); }); it(`calls createAlert with "${expectedMsg}"`, async () => { @@ -382,24 +288,6 @@ describe('SidebarDropdownWidget', () => { }); }); }); - - describe('when a user is searching', () => { - describe('when search result is not found', () => { - describe('when milestone', () => { - it('renders "No milestone found"', async () => { - createComponent(); - - await toggleDropdown(); - - findSearchBox().vm.$emit('input', 'non existing milestones'); - - await nextTick(); - - expect(findDropdownText().text()).toBe('No milestone found'); - }); - }); - }); - }); }); }); @@ -424,18 +312,10 @@ describe('SidebarDropdownWidget', () => { await clickEdit(); }); - it('renders the dropdown on clicking edit', async () => { - expect(findDropdown().isVisible()).toBe(true); - }); - - it('focuses on the input when dropdown is shown', async () => { - expect(document.activeElement).toEqual(wrapper.findComponent(GlFormInput).element); - }); - describe('when currentAttribute is not equal to attribute id', () => { describe('when update is successful', () => { it('calls setIssueAttribute mutation', () => { - findDropdownItemWithText(mockMilestone2.title).vm.$emit('click'); + findSidebarDropdown().vm.$emit('change', { id: mockMilestone2.id }); expect(milestoneMutationSpy).toHaveBeenCalledWith({ iid: mockIssue.iid, @@ -443,72 +323,6 @@ describe('SidebarDropdownWidget', () => { fullPath: mockIssue.projectPath, }); }); - - it('sets the value returned from the mutation to currentAttribute', async () => { - findDropdownItemWithText(mockMilestone2.title).vm.$emit('click'); - await nextTick(); - expect(findSelectedAttribute().text()).toBe(mockMilestone2.title); - }); - }); - }); - - describe('milestones', () => { - let projectMilestonesSpy; - - it('should call createAlert if milestones query fails', async () => { - await createComponentWithApollo({ - projectMilestonesSpy: jest.fn().mockRejectedValue(error), - }); - - await clickEdit(); - - expect(createAlert).toHaveBeenCalledWith({ - message: wrapper.vm.i18n.listFetchError, - captureError: true, - error: expect.any(Error), - }); - }); - - it('only fetches attributes when dropdown is opened', async () => { - projectMilestonesSpy = jest.fn().mockResolvedValueOnce(emptyProjectMilestonesResponse); - await createComponentWithApollo({ projectMilestonesSpy }); - - expect(projectMilestonesSpy).not.toHaveBeenCalled(); - - await clickEdit(); - - expect(projectMilestonesSpy).toHaveBeenNthCalledWith(1, { - fullPath: mockIssue.projectPath, - state: 'active', - title: '', - }); - }); - - describe('when a user is searching', () => { - const mockSearchTerm = 'foobar'; - - beforeEach(async () => { - projectMilestonesSpy = jest - .fn() - .mockResolvedValueOnce(emptyProjectMilestonesResponse); - await createComponentWithApollo({ projectMilestonesSpy }); - - await clickEdit(); - }); - - it('sends a projectMilestones query with the entered search term "foo"', async () => { - findSearchBox().vm.$emit('input', mockSearchTerm); - await nextTick(); - - // Account for debouncing - jest.runAllTimers(); - - expect(projectMilestonesSpy).toHaveBeenNthCalledWith(2, { - fullPath: mockIssue.projectPath, - state: 'active', - title: mockSearchTerm, - }); - }); }); }); }); |