Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/issue_show')
-rw-r--r--spec/frontend/issue_show/components/app_spec.js50
-rw-r--r--spec/frontend/issue_show/components/header_actions_spec.js328
-rw-r--r--spec/frontend/issue_show/issue_spec.js5
3 files changed, 369 insertions, 14 deletions
diff --git a/spec/frontend/issue_show/components/app_spec.js b/spec/frontend/issue_show/components/app_spec.js
index f4095d4de96..dde4e8458d5 100644
--- a/spec/frontend/issue_show/components/app_spec.js
+++ b/spec/frontend/issue_show/components/app_spec.js
@@ -17,6 +17,7 @@ import {
import IncidentTabs from '~/issue_show/components/incidents/incident_tabs.vue';
import DescriptionComponent from '~/issue_show/components/description.vue';
import PinnedLinks from '~/issue_show/components/pinned_links.vue';
+import { IssuableStatus, IssuableStatusText } from '~/issue_show/constants';
function formatText(text) {
return text.trim().replace(/\s\s+/g, ' ');
@@ -36,6 +37,10 @@ describe('Issuable output', () => {
const findStickyHeader = () => wrapper.find('[data-testid="issue-sticky-header"]');
+ const findLockedBadge = () => wrapper.find('[data-testid="locked"]');
+
+ const findConfidentialBadge = () => wrapper.find('[data-testid="confidential"]');
+
const mountComponent = (props = {}, options = {}) => {
wrapper = mount(IssuableApp, {
propsData: { ...appProps, ...props },
@@ -532,7 +537,7 @@ describe('Issuable output', () => {
describe('sticky header', () => {
describe('when title is in view', () => {
it('is not shown', () => {
- expect(wrapper.find('.issue-sticky-header').exists()).toBe(false);
+ expect(findStickyHeader().exists()).toBe(false);
});
});
@@ -542,24 +547,45 @@ describe('Issuable output', () => {
wrapper.find(GlIntersectionObserver).vm.$emit('disappear');
});
- it('is shown with title', () => {
+ it('shows with title', () => {
expect(findStickyHeader().text()).toContain('Sticky header title');
});
- it('is shown with Open when status is opened', () => {
- wrapper.setProps({ issuableStatus: 'opened' });
+ it.each`
+ title | state
+ ${'shows with Open when status is opened'} | ${IssuableStatus.Open}
+ ${'shows with Closed when status is closed'} | ${IssuableStatus.Closed}
+ ${'shows with Open when status is reopened'} | ${IssuableStatus.Reopened}
+ `('$title', async ({ state }) => {
+ wrapper.setProps({ issuableStatus: state });
- return wrapper.vm.$nextTick(() => {
- expect(findStickyHeader().text()).toContain('Open');
- });
+ await wrapper.vm.$nextTick();
+
+ expect(findStickyHeader().text()).toContain(IssuableStatusText[state]);
});
- it('is shown with Closed when status is closed', () => {
- wrapper.setProps({ issuableStatus: 'closed' });
+ it.each`
+ title | isConfidential
+ ${'does not show confidential badge when issue is not confidential'} | ${true}
+ ${'shows confidential badge when issue is confidential'} | ${false}
+ `('$title', async ({ isConfidential }) => {
+ wrapper.setProps({ isConfidential });
- return wrapper.vm.$nextTick(() => {
- expect(findStickyHeader().text()).toContain('Closed');
- });
+ await wrapper.vm.$nextTick();
+
+ expect(findConfidentialBadge().exists()).toBe(isConfidential);
+ });
+
+ it.each`
+ title | isLocked
+ ${'does not show locked badge when issue is not locked'} | ${true}
+ ${'shows locked badge when issue is locked'} | ${false}
+ `('$title', async ({ isLocked }) => {
+ wrapper.setProps({ isLocked });
+
+ await wrapper.vm.$nextTick();
+
+ expect(findLockedBadge().exists()).toBe(isLocked);
});
});
});
diff --git a/spec/frontend/issue_show/components/header_actions_spec.js b/spec/frontend/issue_show/components/header_actions_spec.js
new file mode 100644
index 00000000000..67b8665a889
--- /dev/null
+++ b/spec/frontend/issue_show/components/header_actions_spec.js
@@ -0,0 +1,328 @@
+import { GlButton, GlDropdown, GlDropdownItem, GlLink, GlModal } from '@gitlab/ui';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import Vuex from 'vuex';
+import createFlash, { FLASH_TYPES } from '~/flash';
+import { IssuableType } from '~/issuable_show/constants';
+import HeaderActions from '~/issue_show/components/header_actions.vue';
+import { IssuableStatus, IssueStateEvent } from '~/issue_show/constants';
+import promoteToEpicMutation from '~/issue_show/queries/promote_to_epic.mutation.graphql';
+import * as urlUtility from '~/lib/utils/url_utility';
+import createStore from '~/notes/stores';
+
+jest.mock('~/flash');
+
+describe('HeaderActions component', () => {
+ let dispatchEventSpy;
+ let mutateMock;
+ let wrapper;
+ let visitUrlSpy;
+
+ const localVue = createLocalVue();
+ localVue.use(Vuex);
+ const store = createStore();
+
+ const defaultProps = {
+ canCreateIssue: true,
+ canPromoteToEpic: true,
+ canReopenIssue: true,
+ canReportSpam: true,
+ canUpdateIssue: true,
+ iid: '32',
+ isIssueAuthor: true,
+ issueType: IssuableType.Issue,
+ newIssuePath: 'gitlab-org/gitlab-test/-/issues/new',
+ projectPath: 'gitlab-org/gitlab-test',
+ reportAbusePath:
+ '-/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%2Fgitlab-org%2Fgitlab-test%2F-%2Fissues%2F32&user_id=1',
+ submitAsSpamPath: 'gitlab-org/gitlab-test/-/issues/32/submit_as_spam',
+ };
+
+ const updateIssueMutationResponse = { data: { updateIssue: { errors: [] } } };
+
+ const promoteToEpicMutationResponse = {
+ data: {
+ promoteToEpic: {
+ errors: [],
+ epic: {
+ webPath: '/groups/gitlab-org/-/epics/1',
+ },
+ },
+ },
+ };
+
+ const promoteToEpicMutationErrorResponse = {
+ data: {
+ promoteToEpic: {
+ errors: ['The issue has already been promoted to an epic.'],
+ epic: {},
+ },
+ },
+ };
+
+ const findToggleIssueStateButton = () => wrapper.find(GlButton);
+
+ const findDropdownAt = index => wrapper.findAll(GlDropdown).at(index);
+
+ const findMobileDropdownItems = () => findDropdownAt(0).findAll(GlDropdownItem);
+
+ const findDesktopDropdownItems = () => findDropdownAt(1).findAll(GlDropdownItem);
+
+ const findModal = () => wrapper.find(GlModal);
+
+ const findModalLinkAt = index =>
+ findModal()
+ .findAll(GlLink)
+ .at(index);
+
+ const mountComponent = ({
+ props = {},
+ issueState = IssuableStatus.Open,
+ blockedByIssues = [],
+ mutateResponse = {},
+ } = {}) => {
+ mutateMock = jest.fn().mockResolvedValue(mutateResponse);
+
+ store.getters.getNoteableData.state = issueState;
+ store.getters.getNoteableData.blocked_by_issues = blockedByIssues;
+
+ return shallowMount(HeaderActions, {
+ localVue,
+ store,
+ provide: {
+ ...defaultProps,
+ ...props,
+ },
+ mocks: {
+ $apollo: {
+ mutate: mutateMock,
+ },
+ },
+ });
+ };
+
+ afterEach(() => {
+ if (dispatchEventSpy) {
+ dispatchEventSpy.mockRestore();
+ }
+ if (visitUrlSpy) {
+ visitUrlSpy.mockRestore();
+ }
+ wrapper.destroy();
+ });
+
+ describe.each`
+ issueType
+ ${IssuableType.Issue}
+ ${IssuableType.Incident}
+ `('when issue type is $issueType', ({ issueType }) => {
+ describe('close/reopen button', () => {
+ describe.each`
+ description | issueState | buttonText | newIssueState
+ ${`when the ${issueType} is open`} | ${IssuableStatus.Open} | ${`Close ${issueType}`} | ${IssueStateEvent.Close}
+ ${`when the ${issueType} is closed`} | ${IssuableStatus.Closed} | ${`Reopen ${issueType}`} | ${IssueStateEvent.Reopen}
+ `('$description', ({ issueState, buttonText, newIssueState }) => {
+ beforeEach(() => {
+ dispatchEventSpy = jest.spyOn(document, 'dispatchEvent');
+
+ wrapper = mountComponent({
+ props: { issueType },
+ issueState,
+ mutateResponse: updateIssueMutationResponse,
+ });
+ });
+
+ it(`has text "${buttonText}"`, () => {
+ expect(findToggleIssueStateButton().text()).toBe(buttonText);
+ });
+
+ it('calls apollo mutation', () => {
+ findToggleIssueStateButton().vm.$emit('click');
+
+ expect(mutateMock).toHaveBeenCalledWith(
+ expect.objectContaining({
+ variables: {
+ input: {
+ iid: defaultProps.iid,
+ projectPath: defaultProps.projectPath,
+ stateEvent: newIssueState,
+ },
+ },
+ }),
+ );
+ });
+
+ it('dispatches a custom event to update the issue page', async () => {
+ findToggleIssueStateButton().vm.$emit('click');
+
+ await wrapper.vm.$nextTick();
+
+ expect(dispatchEventSpy).toHaveBeenCalledTimes(1);
+ });
+ });
+ });
+
+ describe.each`
+ description | isCloseIssueItemVisible | findDropdownItems
+ ${'mobile dropdown'} | ${true} | ${findMobileDropdownItems}
+ ${'desktop dropdown'} | ${false} | ${findDesktopDropdownItems}
+ `('$description', ({ isCloseIssueItemVisible, findDropdownItems }) => {
+ describe.each`
+ description | itemText | isItemVisible | canUpdateIssue | canCreateIssue | isIssueAuthor | canReportSpam | canPromoteToEpic
+ ${`when user can update ${issueType}`} | ${`Close ${issueType}`} | ${isCloseIssueItemVisible} | ${true} | ${true} | ${true} | ${true} | ${true}
+ ${`when user cannot update ${issueType}`} | ${`Close ${issueType}`} | ${false} | ${false} | ${true} | ${true} | ${true} | ${true}
+ ${`when user can create ${issueType}`} | ${`New ${issueType}`} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
+ ${`when user cannot create ${issueType}`} | ${`New ${issueType}`} | ${false} | ${true} | ${false} | ${true} | ${true} | ${true}
+ ${'when user can promote to epic'} | ${'Promote to epic'} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
+ ${'when user cannot promote to epic'} | ${'Promote to epic'} | ${false} | ${true} | ${true} | ${true} | ${true} | ${false}
+ ${'when user can report abuse'} | ${'Report abuse'} | ${true} | ${true} | ${true} | ${false} | ${true} | ${true}
+ ${'when user cannot report abuse'} | ${'Report abuse'} | ${false} | ${true} | ${true} | ${true} | ${true} | ${true}
+ ${'when user can submit as spam'} | ${'Submit as spam'} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
+ ${'when user cannot submit as spam'} | ${'Submit as spam'} | ${false} | ${true} | ${true} | ${true} | ${false} | ${true}
+ `(
+ '$description',
+ ({
+ itemText,
+ isItemVisible,
+ canUpdateIssue,
+ canCreateIssue,
+ isIssueAuthor,
+ canReportSpam,
+ canPromoteToEpic,
+ }) => {
+ beforeEach(() => {
+ wrapper = mountComponent({
+ props: {
+ canUpdateIssue,
+ canCreateIssue,
+ isIssueAuthor,
+ issueType,
+ canReportSpam,
+ canPromoteToEpic,
+ },
+ });
+ });
+
+ it(`${isItemVisible ? 'shows' : 'hides'} "${itemText}" item`, () => {
+ expect(
+ findDropdownItems()
+ .filter(item => item.text() === itemText)
+ .exists(),
+ ).toBe(isItemVisible);
+ });
+ },
+ );
+ });
+ });
+
+ describe('when "Promote to epic" button is clicked', () => {
+ describe('when response is successful', () => {
+ beforeEach(() => {
+ visitUrlSpy = jest.spyOn(urlUtility, 'visitUrl').mockReturnValue({});
+
+ wrapper = mountComponent({
+ mutateResponse: promoteToEpicMutationResponse,
+ });
+
+ wrapper.find('[data-testid="promote-button"]').vm.$emit('click');
+ });
+
+ it('invokes GraphQL mutation when clicked', () => {
+ expect(mutateMock).toHaveBeenCalledWith(
+ expect.objectContaining({
+ mutation: promoteToEpicMutation,
+ variables: {
+ input: {
+ iid: defaultProps.iid,
+ projectPath: defaultProps.projectPath,
+ },
+ },
+ }),
+ );
+ });
+
+ it('shows a success message and tells the user they are being redirected', () => {
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'The issue was successfully promoted to an epic. Redirecting to epic...',
+ type: FLASH_TYPES.SUCCESS,
+ });
+ });
+
+ it('redirects to newly created epic path', () => {
+ expect(visitUrlSpy).toHaveBeenCalledWith(
+ promoteToEpicMutationResponse.data.promoteToEpic.epic.webPath,
+ );
+ });
+ });
+
+ describe('when response contains errors', () => {
+ beforeEach(() => {
+ visitUrlSpy = jest.spyOn(urlUtility, 'visitUrl').mockReturnValue({});
+
+ wrapper = mountComponent({
+ mutateResponse: promoteToEpicMutationErrorResponse,
+ });
+
+ wrapper.find('[data-testid="promote-button"]').vm.$emit('click');
+ });
+
+ it('shows an error message', () => {
+ expect(createFlash).toHaveBeenCalledWith({
+ message: promoteToEpicMutationErrorResponse.data.promoteToEpic.errors.join('; '),
+ });
+ });
+ });
+ });
+
+ describe('modal', () => {
+ const blockedByIssues = [
+ { iid: 13, web_url: 'gitlab-org/gitlab-test/-/issues/13' },
+ { iid: 79, web_url: 'gitlab-org/gitlab-test/-/issues/79' },
+ ];
+
+ beforeEach(() => {
+ wrapper = mountComponent({ blockedByIssues });
+ });
+
+ it('has title text', () => {
+ expect(findModal().attributes('title')).toBe(
+ 'Are you sure you want to close this blocked issue?',
+ );
+ });
+
+ it('has body text', () => {
+ expect(findModal().text()).toContain(
+ 'This issue is currently blocked by the following issues:',
+ );
+ });
+
+ it('calls apollo mutation when primary button is clicked', () => {
+ findModal().vm.$emit('primary');
+
+ expect(mutateMock).toHaveBeenCalledWith(
+ expect.objectContaining({
+ variables: {
+ input: {
+ iid: defaultProps.iid.toString(),
+ projectPath: defaultProps.projectPath,
+ stateEvent: IssueStateEvent.Close,
+ },
+ },
+ }),
+ );
+ });
+
+ describe.each`
+ ordinal | index
+ ${'first'} | ${0}
+ ${'second'} | ${1}
+ `('$ordinal blocked-by issue link', ({ index }) => {
+ it('has link text', () => {
+ expect(findModalLinkAt(index).text()).toBe(`#${blockedByIssues[index].iid}`);
+ });
+
+ it('has url', () => {
+ expect(findModalLinkAt(index).attributes('href')).toBe(blockedByIssues[index].web_url);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/issue_show/issue_spec.js b/spec/frontend/issue_show/issue_spec.js
index c0175e774a2..7a48353af94 100644
--- a/spec/frontend/issue_show/issue_spec.js
+++ b/spec/frontend/issue_show/issue_spec.js
@@ -2,9 +2,10 @@ import MockAdapter from 'axios-mock-adapter';
import { useMockIntersectionObserver } from 'helpers/mock_dom_observer';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
-import initIssuableApp from '~/issue_show/issue';
+import { initIssuableApp } from '~/issue_show/issue';
import * as parseData from '~/issue_show/utils/parse_data';
import { appProps } from './mock_data';
+import createStore from '~/notes/stores';
const mock = new MockAdapter(axios);
mock.onGet().reply(200);
@@ -30,7 +31,7 @@ describe('Issue show index', () => {
});
const issuableData = parseData.parseIssuableData();
- initIssuableApp(issuableData);
+ initIssuableApp(issuableData, createStore());
await waitForPromises();