diff options
Diffstat (limited to 'spec/frontend/jira_connect/subscriptions')
11 files changed, 1057 insertions, 0 deletions
diff --git a/spec/frontend/jira_connect/subscriptions/api_spec.js b/spec/frontend/jira_connect/subscriptions/api_spec.js new file mode 100644 index 00000000000..57b11bdbc27 --- /dev/null +++ b/spec/frontend/jira_connect/subscriptions/api_spec.js @@ -0,0 +1,103 @@ +import MockAdapter from 'axios-mock-adapter'; +import { addSubscription, removeSubscription, fetchGroups } from '~/jira_connect/subscriptions/api'; +import { getJwt } from '~/jira_connect/subscriptions/utils'; +import axios from '~/lib/utils/axios_utils'; +import httpStatus from '~/lib/utils/http_status'; + +jest.mock('~/jira_connect/subscriptions/utils', () => ({ + getJwt: jest.fn().mockResolvedValue('jwt'), +})); + +describe('JiraConnect API', () => { + let mock; + let response; + + const mockAddPath = 'addPath'; + const mockRemovePath = 'removePath'; + const mockNamespace = 'namespace'; + const mockJwt = 'jwt'; + const mockResponse = { success: true }; + + beforeEach(() => { + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + response = null; + }); + + describe('addSubscription', () => { + const makeRequest = () => addSubscription(mockAddPath, mockNamespace); + + it('returns success response', async () => { + jest.spyOn(axios, 'post'); + mock + .onPost(mockAddPath, { + jwt: mockJwt, + namespace_path: mockNamespace, + }) + .replyOnce(httpStatus.OK, mockResponse); + + response = await makeRequest(); + + expect(getJwt).toHaveBeenCalled(); + expect(axios.post).toHaveBeenCalledWith(mockAddPath, { + jwt: mockJwt, + namespace_path: mockNamespace, + }); + expect(response.data).toEqual(mockResponse); + }); + }); + + describe('removeSubscription', () => { + const makeRequest = () => removeSubscription(mockRemovePath); + + it('returns success response', async () => { + jest.spyOn(axios, 'delete'); + mock.onDelete(mockRemovePath).replyOnce(httpStatus.OK, mockResponse); + + response = await makeRequest(); + + expect(getJwt).toHaveBeenCalled(); + expect(axios.delete).toHaveBeenCalledWith(mockRemovePath, { + params: { + jwt: mockJwt, + }, + }); + expect(response.data).toEqual(mockResponse); + }); + }); + + describe('fetchGroups', () => { + const mockGroupsPath = 'groupsPath'; + const mockPage = 1; + const mockPerPage = 10; + + const makeRequest = () => + fetchGroups(mockGroupsPath, { + page: mockPage, + perPage: mockPerPage, + }); + + it('returns success response', async () => { + jest.spyOn(axios, 'get'); + mock + .onGet(mockGroupsPath, { + page: mockPage, + per_page: mockPerPage, + }) + .replyOnce(httpStatus.OK, mockResponse); + + response = await makeRequest(); + + expect(axios.get).toHaveBeenCalledWith(mockGroupsPath, { + params: { + page: mockPage, + per_page: mockPerPage, + }, + }); + expect(response.data).toEqual(mockResponse); + }); + }); +}); diff --git a/spec/frontend/jira_connect/subscriptions/components/__snapshots__/group_item_name_spec.js.snap b/spec/frontend/jira_connect/subscriptions/components/__snapshots__/group_item_name_spec.js.snap new file mode 100644 index 00000000000..21c903f064d --- /dev/null +++ b/spec/frontend/jira_connect/subscriptions/components/__snapshots__/group_item_name_spec.js.snap @@ -0,0 +1,44 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`GroupItemName template matches the snapshot 1`] = ` +<div + class="gl-display-flex gl-align-items-center" +> + <gl-icon-stub + class="gl-mr-3" + name="folder-o" + size="16" + /> + + <div + class="gl-display-none gl-flex-shrink-0 gl-sm-display-flex gl-mr-3" + > + <gl-avatar-stub + alt="avatar" + entityid="0" + entityname="Gitlab Org" + shape="rect" + size="32" + src="avatar.png" + /> + </div> + + <div> + <span + class="gl-mr-3 gl-text-gray-900! gl-font-weight-bold" + > + + Gitlab Org + + </span> + + <div> + <p + class="gl-mt-2! gl-mb-0 gl-text-gray-600" + > + Open source software to collaborate on code + </p> + </div> + </div> +</div> +`; diff --git a/spec/frontend/jira_connect/subscriptions/components/app_spec.js b/spec/frontend/jira_connect/subscriptions/components/app_spec.js new file mode 100644 index 00000000000..8915a7697a5 --- /dev/null +++ b/spec/frontend/jira_connect/subscriptions/components/app_spec.js @@ -0,0 +1,130 @@ +import { GlAlert, GlButton, GlModal, GlLink } from '@gitlab/ui'; +import { mount, shallowMount } from '@vue/test-utils'; + +import JiraConnectApp from '~/jira_connect/subscriptions/components/app.vue'; +import createStore from '~/jira_connect/subscriptions/store'; +import { SET_ALERT } from '~/jira_connect/subscriptions/store/mutation_types'; +import { __ } from '~/locale'; + +jest.mock('~/jira_connect/subscriptions/utils', () => ({ + retrieveAlert: jest.fn().mockReturnValue({ message: 'error message' }), + getLocation: jest.fn(), +})); + +describe('JiraConnectApp', () => { + let wrapper; + let store; + + const findAlert = () => wrapper.findComponent(GlAlert); + const findAlertLink = () => findAlert().findComponent(GlLink); + const findGlButton = () => wrapper.findComponent(GlButton); + const findGlModal = () => wrapper.findComponent(GlModal); + + const createComponent = ({ provide, mountFn = shallowMount } = {}) => { + store = createStore(); + + wrapper = mountFn(JiraConnectApp, { + store, + provide, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('template', () => { + describe('when user is not logged in', () => { + beforeEach(() => { + createComponent({ + provide: { + usersPath: '/users', + }, + }); + }); + + it('renders "Sign in" button', () => { + expect(findGlButton().text()).toBe('Sign in to add namespaces'); + expect(findGlModal().exists()).toBe(false); + }); + }); + + describe('when user is logged in', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders "Add" button and modal', () => { + expect(findGlButton().text()).toBe('Add namespace'); + expect(findGlModal().exists()).toBe(true); + }); + }); + + describe('alert', () => { + it.each` + message | variant | alertShouldRender + ${'Test error'} | ${'danger'} | ${true} + ${'Test notice'} | ${'info'} | ${true} + ${''} | ${undefined} | ${false} + ${undefined} | ${undefined} | ${false} + `( + 'renders correct alert when message is `$message` and variant is `$variant`', + async ({ message, alertShouldRender, variant }) => { + createComponent(); + + store.commit(SET_ALERT, { message, variant }); + await wrapper.vm.$nextTick(); + + const alert = findAlert(); + + expect(alert.exists()).toBe(alertShouldRender); + if (alertShouldRender) { + expect(alert.isVisible()).toBe(alertShouldRender); + expect(alert.html()).toContain(message); + expect(alert.props('variant')).toBe(variant); + expect(findAlertLink().exists()).toBe(false); + } + }, + ); + + it('hides alert on @dismiss event', async () => { + createComponent(); + + store.commit(SET_ALERT, { message: 'test message' }); + await wrapper.vm.$nextTick(); + + findAlert().vm.$emit('dismiss'); + await wrapper.vm.$nextTick(); + + expect(findAlert().exists()).toBe(false); + }); + + it('renders link when `linkUrl` is set', async () => { + createComponent({ mountFn: mount }); + + store.commit(SET_ALERT, { + message: __('test message %{linkStart}test link%{linkEnd}'), + linkUrl: 'https://gitlab.com', + }); + await wrapper.vm.$nextTick(); + + const alertLink = findAlertLink(); + + expect(alertLink.exists()).toBe(true); + expect(alertLink.text()).toContain('test link'); + expect(alertLink.attributes('href')).toBe('https://gitlab.com'); + }); + + describe('when alert is set in localStoage', () => { + it('renders alert on mount', () => { + createComponent(); + + const alert = findAlert(); + + expect(alert.exists()).toBe(true); + expect(alert.html()).toContain('error message'); + }); + }); + }); + }); +}); diff --git a/spec/frontend/jira_connect/subscriptions/components/group_item_name_spec.js b/spec/frontend/jira_connect/subscriptions/components/group_item_name_spec.js new file mode 100644 index 00000000000..b5fe08486b1 --- /dev/null +++ b/spec/frontend/jira_connect/subscriptions/components/group_item_name_spec.js @@ -0,0 +1,28 @@ +import { shallowMount } from '@vue/test-utils'; + +import GroupItemName from '~/jira_connect/subscriptions/components/group_item_name.vue'; +import { mockGroup1 } from '../mock_data'; + +describe('GroupItemName', () => { + let wrapper; + + const createComponent = () => { + wrapper = shallowMount(GroupItemName, { + propsData: { + group: mockGroup1, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('template', () => { + it('matches the snapshot', () => { + createComponent(); + + expect(wrapper.element).toMatchSnapshot(); + }); + }); +}); diff --git a/spec/frontend/jira_connect/subscriptions/components/groups_list_item_spec.js b/spec/frontend/jira_connect/subscriptions/components/groups_list_item_spec.js new file mode 100644 index 00000000000..b69435df83a --- /dev/null +++ b/spec/frontend/jira_connect/subscriptions/components/groups_list_item_spec.js @@ -0,0 +1,112 @@ +import { GlButton } from '@gitlab/ui'; +import { mount, shallowMount } from '@vue/test-utils'; +import waitForPromises from 'helpers/wait_for_promises'; + +import * as JiraConnectApi from '~/jira_connect/subscriptions/api'; +import GroupItemName from '~/jira_connect/subscriptions/components/group_item_name.vue'; +import GroupsListItem from '~/jira_connect/subscriptions/components/groups_list_item.vue'; +import { persistAlert, reloadPage } from '~/jira_connect/subscriptions/utils'; +import { mockGroup1 } from '../mock_data'; + +jest.mock('~/jira_connect/subscriptions/utils'); + +describe('GroupsListItem', () => { + let wrapper; + const mockSubscriptionPath = 'subscriptionPath'; + + const createComponent = ({ mountFn = shallowMount } = {}) => { + wrapper = mountFn(GroupsListItem, { + propsData: { + group: mockGroup1, + }, + provide: { + subscriptionsPath: mockSubscriptionPath, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + const findGroupItemName = () => wrapper.findComponent(GroupItemName); + const findLinkButton = () => wrapper.findComponent(GlButton); + const clickLinkButton = () => findLinkButton().trigger('click'); + + describe('template', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders GroupItemName', () => { + expect(findGroupItemName().exists()).toBe(true); + expect(findGroupItemName().props('group')).toBe(mockGroup1); + }); + + it('renders Link button', () => { + expect(findLinkButton().exists()).toBe(true); + expect(findLinkButton().text()).toBe('Link'); + }); + }); + + describe('on Link button click', () => { + let addSubscriptionSpy; + + beforeEach(() => { + createComponent({ mountFn: mount }); + + addSubscriptionSpy = jest.spyOn(JiraConnectApi, 'addSubscription').mockResolvedValue(); + }); + + it('sets button to loading and sends request', async () => { + expect(findLinkButton().props('loading')).toBe(false); + + clickLinkButton(); + + await wrapper.vm.$nextTick(); + + expect(findLinkButton().props('loading')).toBe(true); + + await waitForPromises(); + + expect(addSubscriptionSpy).toHaveBeenCalledWith(mockSubscriptionPath, mockGroup1.full_path); + expect(persistAlert).toHaveBeenCalledWith({ + linkUrl: '/help/integration/jira_development_panel.html#usage', + message: + 'You should now see GitLab.com activity inside your Jira Cloud issues. %{linkStart}Learn more%{linkEnd}', + title: 'Namespace successfully linked', + variant: 'success', + }); + }); + + describe('when request is successful', () => { + it('reloads the page', async () => { + clickLinkButton(); + + await waitForPromises(); + + expect(reloadPage).toHaveBeenCalled(); + }); + }); + + describe('when request has errors', () => { + const mockErrorMessage = 'error message'; + const mockError = { response: { data: { error: mockErrorMessage } } }; + + beforeEach(() => { + addSubscriptionSpy = jest + .spyOn(JiraConnectApi, 'addSubscription') + .mockRejectedValue(mockError); + }); + + it('emits `error` event', async () => { + clickLinkButton(); + + await waitForPromises(); + + expect(reloadPage).not.toHaveBeenCalled(); + expect(wrapper.emitted('error')[0][0]).toBe(mockErrorMessage); + }); + }); + }); +}); diff --git a/spec/frontend/jira_connect/subscriptions/components/groups_list_spec.js b/spec/frontend/jira_connect/subscriptions/components/groups_list_spec.js new file mode 100644 index 00000000000..d3a9a3bfd41 --- /dev/null +++ b/spec/frontend/jira_connect/subscriptions/components/groups_list_spec.js @@ -0,0 +1,303 @@ +import { GlAlert, GlLoadingIcon, GlSearchBoxByType, GlPagination } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import { fetchGroups } from '~/jira_connect/subscriptions/api'; +import GroupsList from '~/jira_connect/subscriptions/components/groups_list.vue'; +import GroupsListItem from '~/jira_connect/subscriptions/components/groups_list_item.vue'; +import { DEFAULT_GROUPS_PER_PAGE } from '~/jira_connect/subscriptions/constants'; +import { mockGroup1, mockGroup2 } from '../mock_data'; + +const createMockGroup = (groupId) => { + return { + ...mockGroup1, + id: groupId, + }; +}; + +const createMockGroups = (count) => { + return [...new Array(count)].map((_, idx) => createMockGroup(idx)); +}; + +jest.mock('~/jira_connect/subscriptions/api', () => { + return { + fetchGroups: jest.fn(), + }; +}); + +const mockGroupsPath = '/groups'; + +describe('GroupsList', () => { + let wrapper; + + const mockEmptyResponse = { data: [] }; + + const createComponent = (options = {}) => { + wrapper = extendedWrapper( + shallowMount(GroupsList, { + provide: { + groupsPath: mockGroupsPath, + }, + ...options, + }), + ); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + const findGlAlert = () => wrapper.findComponent(GlAlert); + const findGlLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); + const findAllItems = () => wrapper.findAll(GroupsListItem); + const findFirstItem = () => findAllItems().at(0); + const findSecondItem = () => findAllItems().at(1); + const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType); + const findGroupsList = () => wrapper.findByTestId('groups-list'); + const findPagination = () => wrapper.findComponent(GlPagination); + + describe('when groups are loading', () => { + it('renders loading icon', async () => { + fetchGroups.mockReturnValue(new Promise(() => {})); + createComponent(); + + await wrapper.vm.$nextTick(); + + expect(findGlLoadingIcon().exists()).toBe(true); + }); + }); + + describe('when groups fetch fails', () => { + it('renders error message', async () => { + fetchGroups.mockRejectedValue(); + createComponent(); + + await waitForPromises(); + + expect(findGlLoadingIcon().exists()).toBe(false); + expect(findGlAlert().exists()).toBe(true); + expect(findGlAlert().text()).toBe('Failed to load namespaces. Please try again.'); + }); + }); + + describe('with no groups returned', () => { + it('renders empty state', async () => { + fetchGroups.mockResolvedValue(mockEmptyResponse); + createComponent(); + + await waitForPromises(); + + expect(findGlLoadingIcon().exists()).toBe(false); + expect(wrapper.text()).toContain('No available namespaces'); + }); + }); + + describe('with groups returned', () => { + beforeEach(async () => { + fetchGroups.mockResolvedValue({ + headers: { 'X-PAGE': 1, 'X-TOTAL': 2 }, + data: [mockGroup1, mockGroup2], + }); + createComponent(); + + await waitForPromises(); + }); + + it('renders groups list', () => { + expect(findAllItems()).toHaveLength(2); + expect(findFirstItem().props('group')).toBe(mockGroup1); + expect(findSecondItem().props('group')).toBe(mockGroup2); + }); + + it('sets GroupListItem `disabled` prop to `false`', () => { + findAllItems().wrappers.forEach((groupListItem) => { + expect(groupListItem.props('disabled')).toBe(false); + }); + }); + + it('does not set opacity of the groups list', () => { + expect(findGroupsList().classes()).not.toContain('gl-opacity-5'); + }); + + it('shows error message on $emit from item', async () => { + const errorMessage = 'error message'; + + findFirstItem().vm.$emit('error', errorMessage); + + await wrapper.vm.$nextTick(); + + expect(findGlAlert().exists()).toBe(true); + expect(findGlAlert().text()).toContain(errorMessage); + }); + + describe('when searching groups', () => { + const mockSearchTeam = 'mock search term'; + + describe('while groups are loading', () => { + beforeEach(async () => { + fetchGroups.mockClear(); + fetchGroups.mockReturnValue(new Promise(() => {})); + + findSearchBox().vm.$emit('input', mockSearchTeam); + await wrapper.vm.$nextTick(); + }); + + it('calls `fetchGroups` with search term', () => { + expect(fetchGroups).toHaveBeenLastCalledWith(mockGroupsPath, { + page: 1, + perPage: DEFAULT_GROUPS_PER_PAGE, + search: mockSearchTeam, + }); + }); + + it('disables GroupListItems', () => { + findAllItems().wrappers.forEach((groupListItem) => { + expect(groupListItem.props('disabled')).toBe(true); + }); + }); + + it('sets opacity of the groups list', () => { + expect(findGroupsList().classes()).toContain('gl-opacity-5'); + }); + + it('sets loading prop of the search box', () => { + expect(findSearchBox().props('isLoading')).toBe(true); + }); + + it('sets value prop of the search box to the search term', () => { + expect(findSearchBox().props('value')).toBe(mockSearchTeam); + }); + }); + + describe('when group search finishes loading', () => { + beforeEach(async () => { + fetchGroups.mockResolvedValue({ data: [mockGroup1] }); + findSearchBox().vm.$emit('input'); + + await waitForPromises(); + }); + + it('renders new groups list', () => { + expect(findAllItems()).toHaveLength(1); + expect(findFirstItem().props('group')).toBe(mockGroup1); + }); + }); + + it.each` + userSearchTerm | finalSearchTerm + ${'gitl'} | ${'gitl'} + ${'git'} | ${'git'} + ${'gi'} | ${''} + ${'g'} | ${''} + ${''} | ${''} + ${undefined} | ${undefined} + `( + 'searches for "$finalSearchTerm" when user enters "$userSearchTerm"', + async ({ userSearchTerm, finalSearchTerm }) => { + fetchGroups.mockResolvedValue({ + data: [mockGroup1], + headers: { 'X-PAGE': 1, 'X-TOTAL': 1 }, + }); + + createComponent(); + await waitForPromises(); + + const searchBox = findSearchBox(); + searchBox.vm.$emit('input', userSearchTerm); + + expect(fetchGroups).toHaveBeenLastCalledWith(mockGroupsPath, { + page: 1, + perPage: DEFAULT_GROUPS_PER_PAGE, + search: finalSearchTerm, + }); + }, + ); + }); + + describe('when page=2', () => { + beforeEach(async () => { + const totalItems = DEFAULT_GROUPS_PER_PAGE + 1; + const mockGroups = createMockGroups(totalItems); + fetchGroups.mockResolvedValue({ + headers: { 'X-TOTAL': totalItems, 'X-PAGE': 1 }, + data: mockGroups, + }); + createComponent(); + await waitForPromises(); + + const paginationEl = findPagination(); + paginationEl.vm.$emit('input', 2); + }); + + it('should load results for page 2', () => { + expect(fetchGroups).toHaveBeenLastCalledWith(mockGroupsPath, { + page: 2, + perPage: DEFAULT_GROUPS_PER_PAGE, + search: '', + }); + }); + + it('resets page to 1 on search `input` event', () => { + const mockSearchTerm = 'gitlab'; + const searchBox = findSearchBox(); + + searchBox.vm.$emit('input', mockSearchTerm); + + expect(fetchGroups).toHaveBeenLastCalledWith(mockGroupsPath, { + page: 1, + perPage: DEFAULT_GROUPS_PER_PAGE, + search: mockSearchTerm, + }); + }); + }); + }); + + describe('pagination', () => { + it.each` + scenario | totalItems | shouldShowPagination + ${'renders pagination'} | ${DEFAULT_GROUPS_PER_PAGE + 1} | ${true} + ${'does not render pagination'} | ${DEFAULT_GROUPS_PER_PAGE} | ${false} + ${'does not render pagination'} | ${2} | ${false} + ${'does not render pagination'} | ${0} | ${false} + `('$scenario with $totalItems groups', async ({ totalItems, shouldShowPagination }) => { + const mockGroups = createMockGroups(totalItems); + fetchGroups.mockResolvedValue({ + headers: { 'X-TOTAL': totalItems, 'X-PAGE': 1 }, + data: mockGroups, + }); + createComponent(); + await waitForPromises(); + + const paginationEl = findPagination(); + + expect(paginationEl.exists()).toBe(shouldShowPagination); + if (shouldShowPagination) { + expect(paginationEl.props('totalItems')).toBe(totalItems); + } + }); + + describe('when `input` event triggered', () => { + beforeEach(async () => { + const MOCK_TOTAL_ITEMS = DEFAULT_GROUPS_PER_PAGE + 1; + fetchGroups.mockResolvedValue({ + headers: { 'X-TOTAL': MOCK_TOTAL_ITEMS, 'X-PAGE': 1 }, + data: createMockGroups(MOCK_TOTAL_ITEMS), + }); + + createComponent(); + await waitForPromises(); + }); + + it('executes `fetchGroups` with correct arguments', () => { + const paginationEl = findPagination(); + paginationEl.vm.$emit('input', 2); + + expect(fetchGroups).toHaveBeenLastCalledWith(mockGroupsPath, { + page: 2, + perPage: DEFAULT_GROUPS_PER_PAGE, + search: '', + }); + }); + }); + }); +}); diff --git a/spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js b/spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js new file mode 100644 index 00000000000..32b43765843 --- /dev/null +++ b/spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js @@ -0,0 +1,122 @@ +import { GlButton, GlEmptyState, GlTable } from '@gitlab/ui'; +import { mount, shallowMount } from '@vue/test-utils'; +import waitForPromises from 'helpers/wait_for_promises'; + +import * as JiraConnectApi from '~/jira_connect/subscriptions/api'; +import SubscriptionsList from '~/jira_connect/subscriptions/components/subscriptions_list.vue'; +import createStore from '~/jira_connect/subscriptions/store'; +import { SET_ALERT } from '~/jira_connect/subscriptions/store/mutation_types'; +import { reloadPage } from '~/jira_connect/subscriptions/utils'; +import { mockSubscription } from '../mock_data'; + +jest.mock('~/jira_connect/subscriptions/utils'); + +describe('SubscriptionsList', () => { + let wrapper; + let store; + + const createComponent = ({ mountFn = shallowMount, provide = {} } = {}) => { + store = createStore(); + + wrapper = mountFn(SubscriptionsList, { + provide, + store, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + const findGlEmptyState = () => wrapper.findComponent(GlEmptyState); + const findGlTable = () => wrapper.findComponent(GlTable); + const findUnlinkButton = () => findGlTable().findComponent(GlButton); + const clickUnlinkButton = () => findUnlinkButton().trigger('click'); + + describe('template', () => { + it('renders GlEmptyState when subscriptions is empty', () => { + createComponent(); + + expect(findGlEmptyState().exists()).toBe(true); + expect(findGlTable().exists()).toBe(false); + }); + + it('renders GlTable when subscriptions are present', () => { + createComponent({ + provide: { + subscriptions: [mockSubscription], + }, + }); + + expect(findGlEmptyState().exists()).toBe(false); + expect(findGlTable().exists()).toBe(true); + }); + }); + + describe('on "Unlink" button click', () => { + let removeSubscriptionSpy; + + beforeEach(() => { + createComponent({ + mountFn: mount, + provide: { + subscriptions: [mockSubscription], + }, + }); + removeSubscriptionSpy = jest.spyOn(JiraConnectApi, 'removeSubscription').mockResolvedValue(); + }); + + it('sets button to loading and sends request', async () => { + expect(findUnlinkButton().props('loading')).toBe(false); + + clickUnlinkButton(); + + await wrapper.vm.$nextTick(); + + expect(findUnlinkButton().props('loading')).toBe(true); + + await waitForPromises(); + + expect(removeSubscriptionSpy).toHaveBeenCalledWith(mockSubscription.unlink_path); + }); + + describe('when request is successful', () => { + it('reloads the page', async () => { + clickUnlinkButton(); + + await waitForPromises(); + + expect(reloadPage).toHaveBeenCalled(); + }); + }); + + describe('when request has errors', () => { + const mockErrorMessage = 'error message'; + const mockError = { response: { data: { error: mockErrorMessage } } }; + + beforeEach(() => { + jest.spyOn(JiraConnectApi, 'removeSubscription').mockRejectedValue(mockError); + jest.spyOn(store, 'commit'); + }); + + it('sets alert', async () => { + clickUnlinkButton(); + + await waitForPromises(); + + expect(reloadPage).not.toHaveBeenCalled(); + expect(store.commit.mock.calls).toEqual( + expect.arrayContaining([ + [ + SET_ALERT, + { + message: mockErrorMessage, + variant: 'danger', + }, + ], + ]), + ); + }); + }); + }); +}); diff --git a/spec/frontend/jira_connect/subscriptions/index_spec.js b/spec/frontend/jira_connect/subscriptions/index_spec.js new file mode 100644 index 00000000000..786f3b4a7d3 --- /dev/null +++ b/spec/frontend/jira_connect/subscriptions/index_spec.js @@ -0,0 +1,24 @@ +import { initJiraConnect } from '~/jira_connect/subscriptions'; + +jest.mock('~/jira_connect/subscriptions/utils', () => ({ + getLocation: jest.fn().mockResolvedValue('test/location'), +})); + +describe('initJiraConnect', () => { + beforeEach(async () => { + setFixtures(` + <a class="js-jira-connect-sign-in" href="https://gitlab.com">Sign In</a> + <a class="js-jira-connect-sign-in" href="https://gitlab.com">Another Sign In</a> + `); + + await initJiraConnect(); + }); + + describe('Sign in links', () => { + it('have `return_to` query parameter', () => { + Array.from(document.querySelectorAll('.js-jira-connect-sign-in')).forEach((el) => { + expect(el.href).toContain('return_to=test/location'); + }); + }); + }); +}); diff --git a/spec/frontend/jira_connect/subscriptions/mock_data.js b/spec/frontend/jira_connect/subscriptions/mock_data.js new file mode 100644 index 00000000000..5247a3dc522 --- /dev/null +++ b/spec/frontend/jira_connect/subscriptions/mock_data.js @@ -0,0 +1,23 @@ +export const mockGroup1 = { + id: 1, + avatar_url: 'avatar.png', + name: 'Gitlab Org', + full_name: 'Gitlab Org', + full_path: 'gitlab-org', + description: 'Open source software to collaborate on code', +}; + +export const mockGroup2 = { + id: 2, + avatar_url: 'avatar.png', + name: 'Gitlab Com', + full_name: 'Gitlab Com', + full_path: 'gitlab-com', + description: 'For GitLab company related projects', +}; + +export const mockSubscription = { + group: mockGroup1, + created_at: '2021-04-14T08:52:23.115Z', + unlink_path: '/-/jira_connect/subscriptions/1', +}; diff --git a/spec/frontend/jira_connect/subscriptions/store/mutations_spec.js b/spec/frontend/jira_connect/subscriptions/store/mutations_spec.js new file mode 100644 index 00000000000..84a33dbf0b5 --- /dev/null +++ b/spec/frontend/jira_connect/subscriptions/store/mutations_spec.js @@ -0,0 +1,28 @@ +import mutations from '~/jira_connect/subscriptions/store/mutations'; +import state from '~/jira_connect/subscriptions/store/state'; + +describe('JiraConnect store mutations', () => { + let localState; + + beforeEach(() => { + localState = state(); + }); + + describe('SET_ALERT', () => { + it('sets alert state', () => { + mutations.SET_ALERT(localState, { + message: 'test error', + variant: 'danger', + title: 'test title', + linkUrl: 'linkUrl', + }); + + expect(localState.alert).toMatchObject({ + message: 'test error', + variant: 'danger', + title: 'test title', + linkUrl: 'linkUrl', + }); + }); + }); +}); diff --git a/spec/frontend/jira_connect/subscriptions/utils_spec.js b/spec/frontend/jira_connect/subscriptions/utils_spec.js new file mode 100644 index 00000000000..2dd95de1b8c --- /dev/null +++ b/spec/frontend/jira_connect/subscriptions/utils_spec.js @@ -0,0 +1,140 @@ +import { useLocalStorageSpy } from 'helpers/local_storage_helper'; +import { useMockLocationHelper } from 'helpers/mock_window_location_helper'; +import { ALERT_LOCALSTORAGE_KEY } from '~/jira_connect/subscriptions/constants'; +import { + persistAlert, + retrieveAlert, + getJwt, + getLocation, + reloadPage, + sizeToParent, +} from '~/jira_connect/subscriptions/utils'; + +describe('JiraConnect utils', () => { + describe('alert utils', () => { + useLocalStorageSpy(); + + it.each` + arg | expectedRetrievedValue + ${{ title: 'error' }} | ${{ title: 'error' }} + ${{ title: 'error', randomKey: 'test' }} | ${{ title: 'error' }} + ${{ title: 'error', message: 'error message', linkUrl: 'link', variant: 'danger' }} | ${{ title: 'error', message: 'error message', linkUrl: 'link', variant: 'danger' }} + ${undefined} | ${{}} + `( + 'persists and retrieves alert data from localStorage when arg is $arg', + ({ arg, expectedRetrievedValue }) => { + persistAlert(arg); + + expect(localStorage.setItem).toHaveBeenCalledWith( + ALERT_LOCALSTORAGE_KEY, + JSON.stringify(expectedRetrievedValue), + ); + + const retrievedValue = retrieveAlert(); + + expect(localStorage.getItem).toHaveBeenCalledWith(ALERT_LOCALSTORAGE_KEY); + expect(retrievedValue).toEqual(expectedRetrievedValue); + }, + ); + }); + + describe('AP object utils', () => { + afterEach(() => { + global.AP = null; + }); + + describe('getJwt', () => { + const mockJwt = 'jwt'; + const getTokenSpy = jest.fn((callback) => callback(mockJwt)); + + it('resolves to the function call when AP.context.getToken is a function', async () => { + global.AP = { + context: { + getToken: getTokenSpy, + }, + }; + + const jwt = await getJwt(); + + expect(getTokenSpy).toHaveBeenCalled(); + expect(jwt).toBe(mockJwt); + }); + + it('resolves to undefined when AP.context.getToken is not a function', async () => { + const jwt = await getJwt(); + + expect(getTokenSpy).not.toHaveBeenCalled(); + expect(jwt).toBeUndefined(); + }); + }); + + describe('getLocation', () => { + const mockLocation = 'test/location'; + const getLocationSpy = jest.fn((callback) => callback(mockLocation)); + + it('resolves to the function call when AP.getLocation is a function', async () => { + global.AP = { + getLocation: getLocationSpy, + }; + + const location = await getLocation(); + + expect(getLocationSpy).toHaveBeenCalled(); + expect(location).toBe(mockLocation); + }); + + it('resolves to undefined when AP.getLocation is not a function', async () => { + const location = await getLocation(); + + expect(getLocationSpy).not.toHaveBeenCalled(); + expect(location).toBeUndefined(); + }); + }); + + describe('reloadPage', () => { + const reloadSpy = jest.fn(); + + useMockLocationHelper(); + + it('calls the function when AP.navigator.reload is a function', async () => { + global.AP = { + navigator: { + reload: reloadSpy, + }, + }; + + await reloadPage(); + + expect(reloadSpy).toHaveBeenCalled(); + expect(window.location.reload).not.toHaveBeenCalled(); + }); + + it('calls window.location.reload when AP.navigator.reload is not a function', async () => { + await reloadPage(); + + expect(reloadSpy).not.toHaveBeenCalled(); + expect(window.location.reload).toHaveBeenCalled(); + }); + }); + + describe('sizeToParent', () => { + const sizeToParentSpy = jest.fn(); + + it('calls the function when AP.sizeToParent is a function', async () => { + global.AP = { + sizeToParent: sizeToParentSpy, + }; + + await sizeToParent(); + + expect(sizeToParentSpy).toHaveBeenCalled(); + }); + + it('does nothing when AP.navigator.reload is not a function', async () => { + await sizeToParent(); + + expect(sizeToParentSpy).not.toHaveBeenCalled(); + }); + }); + }); +}); |