diff options
Diffstat (limited to 'spec/frontend/sidebar/components')
7 files changed, 413 insertions, 33 deletions
diff --git a/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js b/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js index def46255994..5fd364afbe4 100644 --- a/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js +++ b/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js @@ -1,4 +1,4 @@ -import { GlSearchBoxByType, GlDropdown } from '@gitlab/ui'; +import { GlSearchBoxByType } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; @@ -76,7 +76,16 @@ describe('Sidebar assignees widget', () => { SidebarEditableItem, UserSelect, GlSearchBoxByType, - GlDropdown, + GlDropdown: { + template: ` + <div> + <slot name="footer"></slot> + </div> + `, + methods: { + show: jest.fn(), + }, + }, }, }); }; @@ -340,21 +349,9 @@ describe('Sidebar assignees widget', () => { }); }); - it('when realtime feature flag is disabled', async () => { + it('includes the real-time assignees component', async () => { createComponent(); await waitForPromises(); - expect(findRealtimeAssignees().exists()).toBe(false); - }); - - it('when realtime feature flag is enabled', async () => { - createComponent({ - provide: { - glFeatures: { - realTimeIssueSidebar: true, - }, - }, - }); - await waitForPromises(); expect(findRealtimeAssignees().exists()).toBe(true); }); diff --git a/spec/frontend/sidebar/components/assignees/sidebar_participant_spec.js b/spec/frontend/sidebar/components/assignees/sidebar_participant_spec.js index 88a5f4ea8b7..71424aaead3 100644 --- a/spec/frontend/sidebar/components/assignees/sidebar_participant_spec.js +++ b/spec/frontend/sidebar/components/assignees/sidebar_participant_spec.js @@ -1,5 +1,6 @@ -import { GlAvatarLabeled } from '@gitlab/ui'; +import { GlAvatarLabeled, GlIcon } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import { IssuableType } from '~/issues/constants'; import SidebarParticipant from '~/sidebar/components/assignees/sidebar_participant.vue'; const user = { @@ -13,14 +14,24 @@ describe('Sidebar participant component', () => { let wrapper; const findAvatar = () => wrapper.findComponent(GlAvatarLabeled); + const findIcon = () => wrapper.findComponent(GlIcon); - const createComponent = (status = null) => { + const createComponent = ({ + status = null, + issuableType = IssuableType.Issue, + canMerge = false, + } = {}) => { wrapper = shallowMount(SidebarParticipant, { propsData: { user: { ...user, + canMerge, status, }, + issuableType, + }, + stubs: { + GlAvatarLabeled, }, }); }; @@ -29,15 +40,35 @@ describe('Sidebar participant component', () => { wrapper.destroy(); }); - it('when user is not busy', () => { + it('does not show `Busy` status when user is not busy', () => { createComponent(); expect(findAvatar().props('label')).toBe(user.name); }); - it('when user is busy', () => { - createComponent({ availability: 'BUSY' }); + it('shows `Busy` status when user is busy', () => { + createComponent({ status: { availability: 'BUSY' } }); expect(findAvatar().props('label')).toBe(`${user.name} (Busy)`); }); + + it('does not render a warning icon', () => { + createComponent(); + + expect(findIcon().exists()).toBe(false); + }); + + describe('when on merge request sidebar', () => { + it('when project member cannot merge', () => { + createComponent({ issuableType: IssuableType.MergeRequest }); + + expect(findIcon().exists()).toBe(true); + }); + + it('when project member can merge', () => { + createComponent({ issuableType: IssuableType.MergeRequest, canMerge: true }); + + expect(findIcon().exists()).toBe(false); + }); + }); }); diff --git a/spec/frontend/sidebar/components/attention_requested_toggle_spec.js b/spec/frontend/sidebar/components/attention_requested_toggle_spec.js index 0939297a754..a9ae23c1624 100644 --- a/spec/frontend/sidebar/components/attention_requested_toggle_spec.js +++ b/spec/frontend/sidebar/components/attention_requested_toggle_spec.js @@ -16,7 +16,10 @@ describe('Attention require toggle', () => { }); it('renders button', () => { - factory({ type: 'reviewer', user: { attention_requested: false } }); + factory({ + type: 'reviewer', + user: { attention_requested: false, can_update_merge_request: true }, + }); expect(findToggle().exists()).toBe(true); }); @@ -28,7 +31,10 @@ describe('Attention require toggle', () => { `( 'renders $icon icon when attention_requested is $attentionRequested', ({ attentionRequested, icon }) => { - factory({ type: 'reviewer', user: { attention_requested: attentionRequested } }); + factory({ + type: 'reviewer', + user: { attention_requested: attentionRequested, can_update_merge_request: true }, + }); expect(findToggle().props('icon')).toBe(icon); }, @@ -41,27 +47,47 @@ describe('Attention require toggle', () => { `( 'renders button with variant $variant when attention_requested is $attentionRequested', ({ attentionRequested, variant }) => { - factory({ type: 'reviewer', user: { attention_requested: attentionRequested } }); + factory({ + type: 'reviewer', + user: { attention_requested: attentionRequested, can_update_merge_request: true }, + }); expect(findToggle().props('variant')).toBe(variant); }, ); it('emits toggle-attention-requested on click', async () => { - factory({ type: 'reviewer', user: { attention_requested: true } }); + factory({ + type: 'reviewer', + user: { attention_requested: true, can_update_merge_request: true }, + }); await findToggle().trigger('click'); expect(wrapper.emitted('toggle-attention-requested')[0]).toEqual([ { - user: { attention_requested: true }, + user: { attention_requested: true, can_update_merge_request: true }, callback: expect.anything(), }, ]); }); + it('does not emit toggle-attention-requested on click if can_update_merge_request is false', async () => { + factory({ + type: 'reviewer', + user: { attention_requested: true, can_update_merge_request: false }, + }); + + await findToggle().trigger('click'); + + expect(wrapper.emitted('toggle-attention-requested')).toBe(undefined); + }); + it('sets loading on click', async () => { - factory({ type: 'reviewer', user: { attention_requested: true } }); + factory({ + type: 'reviewer', + user: { attention_requested: true, can_update_merge_request: true }, + }); await findToggle().trigger('click'); @@ -69,14 +95,24 @@ describe('Attention require toggle', () => { }); it.each` - type | attentionRequested | tooltip - ${'reviewer'} | ${true} | ${AttentionRequestedToggle.i18n.removeAttentionRequested} - ${'reviewer'} | ${false} | ${AttentionRequestedToggle.i18n.attentionRequestedReviewer} - ${'assignee'} | ${false} | ${AttentionRequestedToggle.i18n.attentionRequestedAssignee} + type | attentionRequested | tooltip | canUpdateMergeRequest + ${'reviewer'} | ${true} | ${AttentionRequestedToggle.i18n.removeAttentionRequested} | ${true} + ${'reviewer'} | ${false} | ${AttentionRequestedToggle.i18n.attentionRequestedReviewer} | ${true} + ${'assignee'} | ${false} | ${AttentionRequestedToggle.i18n.attentionRequestedAssignee} | ${true} + ${'reviewer'} | ${true} | ${AttentionRequestedToggle.i18n.attentionRequestedNoPermission} | ${false} + ${'reviewer'} | ${false} | ${AttentionRequestedToggle.i18n.noAttentionRequestedNoPermission} | ${false} + ${'assignee'} | ${true} | ${AttentionRequestedToggle.i18n.attentionRequestedNoPermission} | ${false} + ${'assignee'} | ${false} | ${AttentionRequestedToggle.i18n.noAttentionRequestedNoPermission} | ${false} `( - 'sets tooltip as $tooltip when attention_requested is $attentionRequested and type is $type', - ({ type, attentionRequested, tooltip }) => { - factory({ type, user: { attention_requested: attentionRequested } }); + 'sets tooltip as $tooltip when attention_requested is $attentionRequested, type is $type and, can_update_merge_request is $canUpdateMergeRequest', + ({ type, attentionRequested, tooltip, canUpdateMergeRequest }) => { + factory({ + type, + user: { + attention_requested: attentionRequested, + can_update_merge_request: canUpdateMergeRequest, + }, + }); expect(findToggle().attributes('aria-label')).toBe(tooltip); }, diff --git a/spec/frontend/sidebar/components/incidents/escalation_status_spec.js b/spec/frontend/sidebar/components/incidents/escalation_status_spec.js new file mode 100644 index 00000000000..7a736624fc0 --- /dev/null +++ b/spec/frontend/sidebar/components/incidents/escalation_status_spec.js @@ -0,0 +1,52 @@ +import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import EscalationStatus from '~/sidebar/components/incidents/escalation_status.vue'; +import { + STATUS_LABELS, + STATUS_TRIGGERED, + STATUS_ACKNOWLEDGED, +} from '~/sidebar/components/incidents/constants'; + +describe('EscalationStatus', () => { + let wrapper; + + function createComponent(props) { + wrapper = mountExtended(EscalationStatus, { + propsData: { + value: STATUS_TRIGGERED, + ...props, + }, + }); + } + + afterEach(() => { + wrapper.destroy(); + }); + + const findDropdownComponent = () => wrapper.findComponent(GlDropdown); + const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); + + describe('status', () => { + it('shows the current status', () => { + createComponent({ value: STATUS_ACKNOWLEDGED }); + + expect(findDropdownComponent().props('text')).toBe(STATUS_LABELS[STATUS_ACKNOWLEDGED]); + }); + + it('shows the None option when status is null', () => { + createComponent({ value: null }); + + expect(findDropdownComponent().props('text')).toBe('None'); + }); + }); + + describe('events', () => { + it('selects an item', async () => { + createComponent(); + + await findDropdownItems().at(1).vm.$emit('click'); + + expect(wrapper.emitted().input[0][0]).toBe(STATUS_ACKNOWLEDGED); + }); + }); +}); diff --git a/spec/frontend/sidebar/components/incidents/escalation_utils_spec.js b/spec/frontend/sidebar/components/incidents/escalation_utils_spec.js new file mode 100644 index 00000000000..edd65db0325 --- /dev/null +++ b/spec/frontend/sidebar/components/incidents/escalation_utils_spec.js @@ -0,0 +1,18 @@ +import { STATUS_ACKNOWLEDGED } from '~/sidebar/components/incidents/constants'; +import { getStatusLabel } from '~/sidebar/components/incidents/utils'; + +describe('EscalationUtils', () => { + describe('getStatusLabel', () => { + it('returns a label when provided with a valid status', () => { + const label = getStatusLabel(STATUS_ACKNOWLEDGED); + + expect(label).toEqual('Acknowledged'); + }); + + it("returns 'None' when status is null", () => { + const label = getStatusLabel(null); + + expect(label).toEqual('None'); + }); + }); +}); diff --git a/spec/frontend/sidebar/components/incidents/mock_data.js b/spec/frontend/sidebar/components/incidents/mock_data.js new file mode 100644 index 00000000000..bbb6c61b162 --- /dev/null +++ b/spec/frontend/sidebar/components/incidents/mock_data.js @@ -0,0 +1,39 @@ +import { STATUS_TRIGGERED, STATUS_ACKNOWLEDGED } from '~/sidebar/components/incidents/constants'; + +export const fetchData = { + workspace: { + __typename: 'Project', + id: 'gid://gitlab/Project/2', + issuable: { + __typename: 'Issue', + id: 'gid://gitlab/Issue/4', + escalationStatus: STATUS_TRIGGERED, + }, + }, +}; + +export const mutationData = { + issueSetEscalationStatus: { + __typename: 'IssueSetEscalationStatusPayload', + errors: [], + clientMutationId: null, + issue: { + __typename: 'Issue', + id: 'gid://gitlab/Issue/4', + escalationStatus: STATUS_ACKNOWLEDGED, + }, + }, +}; + +export const fetchError = { + workspace: { + __typename: 'Project', + }, +}; + +export const mutationError = { + issueSetEscalationStatus: { + __typename: 'IssueSetEscalationStatusPayload', + errors: ['hello'], + }, +}; diff --git a/spec/frontend/sidebar/components/incidents/sidebar_escalation_status_spec.js b/spec/frontend/sidebar/components/incidents/sidebar_escalation_status_spec.js new file mode 100644 index 00000000000..a8dc610672c --- /dev/null +++ b/spec/frontend/sidebar/components/incidents/sidebar_escalation_status_spec.js @@ -0,0 +1,207 @@ +import { createLocalVue } from '@vue/test-utils'; +import { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import SidebarEscalationStatus from '~/sidebar/components/incidents/sidebar_escalation_status.vue'; +import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue'; +import { escalationStatusQuery, escalationStatusMutation } from '~/sidebar/constants'; +import waitForPromises from 'helpers/wait_for_promises'; +import EscalationStatus from 'ee_else_ce/sidebar/components/incidents/escalation_status.vue'; +import { STATUS_ACKNOWLEDGED } from '~/sidebar/components/incidents/constants'; +import { createAlert } from '~/flash'; +import { logError } from '~/lib/logger'; +import { fetchData, fetchError, mutationData, mutationError } from './mock_data'; + +jest.mock('~/lib/logger'); +jest.mock('~/flash'); + +const localVue = createLocalVue(); + +describe('SidebarEscalationStatus', () => { + let wrapper; + const queryResolverMock = jest.fn(); + const mutationResolverMock = jest.fn(); + + function createMockApolloProvider({ hasFetchError = false, hasMutationError = false } = {}) { + localVue.use(VueApollo); + + queryResolverMock.mockResolvedValue({ data: hasFetchError ? fetchError : fetchData }); + mutationResolverMock.mockResolvedValue({ + data: hasMutationError ? mutationError : mutationData, + }); + + const requestHandlers = [ + [escalationStatusQuery, queryResolverMock], + [escalationStatusMutation, mutationResolverMock], + ]; + + return createMockApollo(requestHandlers); + } + + function createComponent({ mockApollo } = {}) { + let config; + + if (mockApollo) { + config = { apolloProvider: mockApollo }; + } else { + config = { mocks: { $apollo: { queries: { status: { loading: false } } } } }; + } + + wrapper = mountExtended(SidebarEscalationStatus, { + propsData: { + iid: '1', + projectPath: 'gitlab-org/gitlab', + issuableType: 'issue', + }, + provide: { + canUpdate: true, + }, + directives: { + GlTooltip: createMockDirective(), + }, + localVue, + ...config, + }); + } + + afterEach(() => { + wrapper.destroy(); + }); + + const findSidebarComponent = () => wrapper.findComponent(SidebarEditableItem); + const findStatusComponent = () => wrapper.findComponent(EscalationStatus); + const findEditButton = () => wrapper.findByTestId('edit-button'); + const findIcon = () => wrapper.findByTestId('status-icon'); + + const clickEditButton = async () => { + findEditButton().vm.$emit('click'); + await nextTick(); + }; + const selectAcknowledgedStatus = async () => { + findStatusComponent().vm.$emit('input', STATUS_ACKNOWLEDGED); + // wait for apollo requests + await waitForPromises(); + }; + + describe('sidebar', () => { + it('renders the sidebar component', () => { + createComponent(); + expect(findSidebarComponent().exists()).toBe(true); + }); + + describe('status icon', () => { + it('is visible', () => { + createComponent(); + + expect(findIcon().exists()).toBe(true); + expect(findIcon().isVisible()).toBe(true); + }); + + it('has correct tooltip', async () => { + const mockApollo = createMockApolloProvider(); + createComponent({ mockApollo }); + + // wait for apollo requests + await waitForPromises(); + + const tooltip = getBinding(findIcon().element, 'gl-tooltip'); + + expect(tooltip).toBeDefined(); + expect(tooltip.value).toBe('Status: Triggered'); + }); + }); + + describe('status dropdown', () => { + beforeEach(async () => { + const mockApollo = createMockApolloProvider(); + createComponent({ mockApollo }); + + // wait for apollo requests + await waitForPromises(); + }); + + it('is closed by default', () => { + expect(findStatusComponent().exists()).toBe(true); + expect(findStatusComponent().isVisible()).toBe(false); + }); + + it('is shown after clicking the edit button', async () => { + await clickEditButton(); + + expect(findStatusComponent().isVisible()).toBe(true); + }); + + it('is hidden after clicking the edit button, when open already', async () => { + await clickEditButton(); + await clickEditButton(); + + expect(findStatusComponent().isVisible()).toBe(false); + }); + }); + + describe('update Status event', () => { + beforeEach(async () => { + const mockApollo = createMockApolloProvider(); + createComponent({ mockApollo }); + + // wait for apollo requests + await waitForPromises(); + + await clickEditButton(); + await selectAcknowledgedStatus(); + }); + + it('calls the mutation', () => { + const mutationVariables = { + iid: '1', + projectPath: 'gitlab-org/gitlab', + status: STATUS_ACKNOWLEDGED, + }; + + expect(mutationResolverMock).toHaveBeenCalledWith(mutationVariables); + }); + + it('closes the dropdown', () => { + expect(findStatusComponent().isVisible()).toBe(false); + }); + + it('updates the status', () => { + // Sometimes status has a intermediate wrapping component. A quirk of vue-test-utils + // means that in that case 'value' is exposed as a prop. If no wrapping component + // exists it is exposed as an attribute. + const statusValue = + findStatusComponent().props('value') || findStatusComponent().attributes('value'); + expect(statusValue).toBe(STATUS_ACKNOWLEDGED); + }); + }); + + describe('mutation errors', () => { + it('should error upon fetch', async () => { + const mockApollo = createMockApolloProvider({ hasFetchError: true }); + createComponent({ mockApollo }); + + // wait for apollo requests + await waitForPromises(); + + expect(createAlert).toHaveBeenCalled(); + expect(logError).toHaveBeenCalled(); + }); + + it('should error upon mutation', async () => { + const mockApollo = createMockApolloProvider({ hasMutationError: true }); + createComponent({ mockApollo }); + + // wait for apollo requests + await waitForPromises(); + + await clickEditButton(); + await selectAcknowledgedStatus(); + + expect(createAlert).toHaveBeenCalled(); + expect(logError).toHaveBeenCalled(); + }); + }); + }); +}); |