summaryrefslogtreecommitdiff
path: root/spec/frontend/jira_connect/subscriptions
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/jira_connect/subscriptions')
-rw-r--r--spec/frontend/jira_connect/subscriptions/api_spec.js103
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/__snapshots__/group_item_name_spec.js.snap44
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/app_spec.js130
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/group_item_name_spec.js28
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/groups_list_item_spec.js112
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/groups_list_spec.js303
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js122
-rw-r--r--spec/frontend/jira_connect/subscriptions/index_spec.js24
-rw-r--r--spec/frontend/jira_connect/subscriptions/mock_data.js23
-rw-r--r--spec/frontend/jira_connect/subscriptions/store/mutations_spec.js28
-rw-r--r--spec/frontend/jira_connect/subscriptions/utils_spec.js140
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();
+ });
+ });
+ });
+});