diff options
Diffstat (limited to 'spec/frontend/jira_connect')
12 files changed, 478 insertions, 186 deletions
diff --git a/spec/frontend/jira_connect/branches/components/new_branch_form_spec.js b/spec/frontend/jira_connect/branches/components/new_branch_form_spec.js index 7326b84ad54..b9fed5f34f1 100644 --- a/spec/frontend/jira_connect/branches/components/new_branch_form_spec.js +++ b/spec/frontend/jira_connect/branches/components/new_branch_form_spec.js @@ -1,5 +1,6 @@ -import { GlAlert, GlForm, GlFormInput, GlButton } from '@gitlab/ui'; -import { shallowMount, createLocalVue } from '@vue/test-utils'; +import { GlAlert, GlForm, GlFormInput, GlButton, GlSprintf } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import Vue from 'vue'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; @@ -9,17 +10,12 @@ import SourceBranchDropdown from '~/jira_connect/branches/components/source_bran import { CREATE_BRANCH_ERROR_GENERIC, CREATE_BRANCH_ERROR_WITH_CONTEXT, + I18N_NEW_BRANCH_PERMISSION_ALERT, } from '~/jira_connect/branches/constants'; import createBranchMutation from '~/jira_connect/branches/graphql/mutations/create_branch.mutation.graphql'; +import { mockProjects } from '../mock_data'; -const mockProject = { - id: 'test', - fullPath: 'test-path', - repository: { - branchNames: ['main', 'f-test', 'release'], - rootRef: 'main', - }, -}; +const mockProject = mockProjects[0]; const mockCreateBranchMutationResponse = { data: { createBranch: { @@ -45,28 +41,27 @@ const mockCreateBranchMutationWithErrors = jest const mockCreateBranchMutationFailed = jest.fn().mockRejectedValue(new Error('GraphQL error')); const mockMutationLoading = jest.fn().mockReturnValue(new Promise(() => {})); -const localVue = createLocalVue(); - describe('NewBranchForm', () => { let wrapper; const findSourceBranchDropdown = () => wrapper.findComponent(SourceBranchDropdown); const findProjectDropdown = () => wrapper.findComponent(ProjectDropdown); const findAlert = () => wrapper.findComponent(GlAlert); + const findAlertSprintf = () => findAlert().findComponent(GlSprintf); const findForm = () => wrapper.findComponent(GlForm); const findInput = () => wrapper.findComponent(GlFormInput); const findButton = () => wrapper.findComponent(GlButton); const completeForm = async () => { - await findInput().vm.$emit('input', 'cool-branch-name'); await findProjectDropdown().vm.$emit('change', mockProject); await findSourceBranchDropdown().vm.$emit('change', 'source-branch'); + await findInput().vm.$emit('input', 'cool-branch-name'); }; function createMockApolloProvider({ mockCreateBranchMutation = mockCreateBranchMutationSuccess, } = {}) { - localVue.use(VueApollo); + Vue.use(VueApollo); const mockApollo = createMockApollo([[createBranchMutation, mockCreateBranchMutation]]); @@ -75,7 +70,6 @@ describe('NewBranchForm', () => { function createComponent({ mockApollo, provide } = {}) { wrapper = shallowMount(NewBranchForm, { - localVue, apolloProvider: mockApollo || createMockApolloProvider(), provide: { initialBranchName: '', @@ -89,27 +83,107 @@ describe('NewBranchForm', () => { }); describe('when selecting items from dropdowns', () => { - describe('when a project is selected', () => { - it('sets the `selectedProject` prop for ProjectDropdown and SourceBranchDropdown', async () => { + describe('when no project selected', () => { + beforeEach(() => { createComponent(); + }); - const projectDropdown = findProjectDropdown(); - await projectDropdown.vm.$emit('change', mockProject); + it('hides source branch selection and branch name input', () => { + expect(findSourceBranchDropdown().exists()).toBe(false); + expect(findInput().exists()).toBe(false); + }); - expect(projectDropdown.props('selectedProject')).toEqual(mockProject); - expect(findSourceBranchDropdown().props('selectedProject')).toEqual(mockProject); + it('disables the submit button', () => { + expect(findButton().props('disabled')).toBe(true); }); }); - describe('when a source branch is selected', () => { - it('sets the `selectedBranchName` prop for SourceBranchDropdown', async () => { + describe('when a valid project is selected', () => { + describe("when a source branch isn't selected", () => { + beforeEach(async () => { + createComponent(); + await findProjectDropdown().vm.$emit('change', mockProject); + }); + + it('sets the `selectedProject` prop for ProjectDropdown and SourceBranchDropdown', () => { + expect(findProjectDropdown().props('selectedProject')).toEqual(mockProject); + expect(findSourceBranchDropdown().exists()).toBe(true); + expect(findSourceBranchDropdown().props('selectedProject')).toEqual(mockProject); + }); + + it('disables the submit button', () => { + expect(findButton().props('disabled')).toBe(true); + }); + + it('renders branch input field', () => { + expect(findInput().exists()).toBe(true); + }); + }); + + describe('when `initialBranchName` is provided', () => { + it('sets value of branch name input to `initialBranchName` by default', async () => { + const mockInitialBranchName = 'ap1-test-branch-name'; + + createComponent({ provide: { initialBranchName: mockInitialBranchName } }); + await findProjectDropdown().vm.$emit('change', mockProject); + + expect(findInput().attributes('value')).toBe(mockInitialBranchName); + }); + }); + + describe('when a source branch is selected', () => { + it('sets the `selectedBranchName` prop for SourceBranchDropdown', async () => { + createComponent(); + await completeForm(); + + const mockBranchName = 'main'; + const sourceBranchDropdown = findSourceBranchDropdown(); + await sourceBranchDropdown.vm.$emit('change', mockBranchName); + + expect(sourceBranchDropdown.props('selectedBranchName')).toBe(mockBranchName); + }); + + describe.each` + branchName | submitButtonDisabled + ${undefined} | ${true} + ${''} | ${true} + ${' '} | ${true} + ${'test-branch'} | ${false} + `('when branch name is $branchName', ({ branchName, submitButtonDisabled }) => { + it(`sets submit button 'disabled' prop to ${submitButtonDisabled}`, async () => { + createComponent(); + await completeForm(); + await findInput().vm.$emit('input', branchName); + + expect(findButton().props('disabled')).toBe(submitButtonDisabled); + }); + }); + }); + }); + + describe("when user doesn't have push permissions for the selected project", () => { + beforeEach(async () => { createComponent(); - const mockBranchName = 'main'; - const sourceBranchDropdown = findSourceBranchDropdown(); - await sourceBranchDropdown.vm.$emit('change', mockBranchName); + const projectDropdown = findProjectDropdown(); + await projectDropdown.vm.$emit('change', { + ...mockProject, + userPermissions: { pushCode: false }, + }); + }); + + it('displays an alert', () => { + const alert = findAlert(); + + expect(alert.exists()).toBe(true); + expect(findAlertSprintf().attributes('message')).toBe(I18N_NEW_BRANCH_PERMISSION_ALERT); + expect(alert.props('variant')).toBe('warning'); + expect(alert.props('dismissible')).toBe(false); + }); - expect(sourceBranchDropdown.props('selectedBranchName')).toBe(mockBranchName); + it('hides source branch selection and branch name input', () => { + expect(findSourceBranchDropdown().exists()).toBe(false); + expect(findInput().exists()).toBe(false); }); }); }); @@ -181,7 +255,7 @@ describe('NewBranchForm', () => { it('displays an alert', () => { const alert = findAlert(); expect(alert.exists()).toBe(true); - expect(alert.text()).toBe(alertText); + expect(findAlertSprintf().attributes('message')).toBe(alertText); expect(alert.props()).toMatchObject({ title: alertTitle, variant: 'danger' }); }); @@ -192,15 +266,6 @@ describe('NewBranchForm', () => { }); }); - describe('when `initialBranchName` is specified', () => { - it('sets value of branch name input to `initialBranchName` by default', () => { - const mockInitialBranchName = 'ap1-test-branch-name'; - - createComponent({ provide: { initialBranchName: mockInitialBranchName } }); - expect(findInput().attributes('value')).toBe(mockInitialBranchName); - }); - }); - describe('error handling', () => { describe.each` component | componentName @@ -211,13 +276,15 @@ describe('NewBranchForm', () => { beforeEach(async () => { createComponent(); + await completeForm(); await wrapper.findComponent(component).vm.$emit('error', { message: mockErrorMessage }); }); it('displays an alert', () => { const alert = findAlert(); + expect(alert.exists()).toBe(true); - expect(alert.text()).toBe(mockErrorMessage); + expect(findAlertSprintf().attributes('message')).toBe(mockErrorMessage); expect(alert.props('variant')).toBe('danger'); }); diff --git a/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js b/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js index ec4cb2739f8..136a5967ee4 100644 --- a/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js +++ b/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js @@ -1,5 +1,12 @@ -import { GlDropdown, GlDropdownItem, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui'; -import { mount, shallowMount, createLocalVue } from '@vue/test-utils'; +import { + GlAvatarLabeled, + GlDropdown, + GlDropdownItem, + GlLoadingIcon, + GlSearchBoxByType, +} from '@gitlab/ui'; +import { mount, shallowMount } from '@vue/test-utils'; +import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; @@ -7,32 +14,7 @@ import ProjectDropdown from '~/jira_connect/branches/components/project_dropdown import { PROJECTS_PER_PAGE } from '~/jira_connect/branches/constants'; import getProjectsQuery from '~/jira_connect/branches/graphql/queries/get_projects.query.graphql'; -const localVue = createLocalVue(); - -const mockProjects = [ - { - id: 'test', - name: 'test', - nameWithNamespace: 'test', - avatarUrl: 'https://gitlab.com', - path: 'test-path', - fullPath: 'test-path', - repository: { - empty: false, - }, - }, - { - id: 'gitlab', - name: 'GitLab', - nameWithNamespace: 'gitlab-org/gitlab', - avatarUrl: 'https://gitlab.com', - path: 'gitlab', - fullPath: 'gitlab-org/gitlab', - repository: { - empty: false, - }, - }, -]; +import { mockProjects } from '../mock_data'; const mockProjectsQueryResponse = { data: { @@ -57,12 +39,12 @@ describe('ProjectDropdown', () => { const findDropdown = () => wrapper.findComponent(GlDropdown); const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); - const findDropdownItemByText = (text) => - findAllDropdownItems().wrappers.find((item) => item.text() === text); + const findDropdownItemByProjectId = (projectId) => + wrapper.find(`[data-testid="test-project-${projectId}"]`); const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType); function createMockApolloProvider({ mockGetProjectsQuery = mockGetProjectsQuerySuccess } = {}) { - localVue.use(VueApollo); + Vue.use(VueApollo); const mockApollo = createMockApollo([[getProjectsQuery, mockGetProjectsQuery]]); @@ -71,7 +53,6 @@ describe('ProjectDropdown', () => { function createComponent({ mockApollo, props, mountFn = shallowMount } = {}) { wrapper = mountFn(ProjectDropdown, { - localVue, apolloProvider: mockApollo || createMockApolloProvider(), propsData: props, }); @@ -101,25 +82,38 @@ describe('ProjectDropdown', () => { beforeEach(async () => { createComponent(); await waitForPromises(); - await wrapper.vm.$nextTick(); + await nextTick(); }); it('sets dropdown `loading` prop to `false`', () => { expect(findDropdown().props('loading')).toBe(false); }); - it('renders dropdown items', () => { + it('renders dropdown items with correct props', () => { const dropdownItems = findAllDropdownItems(); + const avatars = dropdownItems.wrappers.map((item) => item.findComponent(GlAvatarLabeled)); + const avatarAttributes = avatars.map((avatar) => avatar.attributes()); + const avatarProps = avatars.map((avatar) => avatar.props()); + expect(dropdownItems.wrappers).toHaveLength(mockProjects.length); - expect(dropdownItems.wrappers.map((item) => item.text())).toEqual( - mockProjects.map((project) => project.nameWithNamespace), + expect(avatarProps).toMatchObject( + mockProjects.map((project) => ({ + label: project.name, + subLabel: project.nameWithNamespace, + })), + ); + expect(avatarAttributes).toMatchObject( + mockProjects.map((project) => ({ + src: project.avatarUrl, + 'entity-name': project.name, + })), ); }); describe('when selecting a dropdown item', () => { - it('emits `change` event with the selected project name', async () => { + it('emits `change` event with the selected project', async () => { const mockProject = mockProjects[0]; - const itemToSelect = findDropdownItemByText(mockProject.nameWithNamespace); + const itemToSelect = findDropdownItemByProjectId(mockProject.id); await itemToSelect.vm.$emit('click'); expect(wrapper.emitted('change')[0]).toEqual([mockProject]); @@ -129,14 +123,14 @@ describe('ProjectDropdown', () => { describe('when `selectedProject` prop is specified', () => { const mockProject = mockProjects[0]; - beforeEach(async () => { + beforeEach(() => { wrapper.setProps({ selectedProject: mockProject, }); }); it('sets `isChecked` prop of the corresponding dropdown item to `true`', () => { - expect(findDropdownItemByText(mockProject.nameWithNamespace).props('isChecked')).toBe(true); + expect(findDropdownItemByProjectId(mockProject.id).props('isChecked')).toBe(true); }); it('sets dropdown text to `selectedBranchName` value', () => { diff --git a/spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js b/spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js index 9dd11dd6345..56eb6d75def 100644 --- a/spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js +++ b/spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js @@ -1,22 +1,22 @@ import { GlDropdown, GlDropdownItem, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui'; -import { mount, shallowMount, createLocalVue } from '@vue/test-utils'; +import { mount, shallowMount } from '@vue/test-utils'; +import Vue from 'vue'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import SourceBranchDropdown from '~/jira_connect/branches/components/source_branch_dropdown.vue'; import { BRANCHES_PER_PAGE } from '~/jira_connect/branches/constants'; import getProjectQuery from '~/jira_connect/branches/graphql/queries/get_project.query.graphql'; - -const localVue = createLocalVue(); +import { mockProjects } from '../mock_data'; const mockProject = { id: 'test', - fullPath: 'test-path', repository: { branchNames: ['main', 'f-test', 'release'], rootRef: 'main', }, }; +const mockSelectedProject = mockProjects[0]; const mockProjectQueryResponse = { data: { @@ -45,7 +45,7 @@ describe('SourceBranchDropdown', () => { }; function createMockApolloProvider({ getProjectQueryLoading = false } = {}) { - localVue.use(VueApollo); + Vue.use(VueApollo); const mockApollo = createMockApollo([ [getProjectQuery, getProjectQueryLoading ? mockQueryLoading : mockGetProjectQuery], @@ -56,7 +56,6 @@ describe('SourceBranchDropdown', () => { function createComponent({ mockApollo, props, mountFn = shallowMount } = {}) { wrapper = mountFn(SourceBranchDropdown, { - localVue, apolloProvider: mockApollo || createMockApolloProvider(), propsData: props, }); @@ -78,7 +77,7 @@ describe('SourceBranchDropdown', () => { describe('when `selectedProject` becomes specified', () => { beforeEach(async () => { wrapper.setProps({ - selectedProject: mockProject, + selectedProject: mockSelectedProject, }); await waitForPromises(); @@ -103,7 +102,7 @@ describe('SourceBranchDropdown', () => { it('renders loading icon in dropdown', () => { createComponent({ mockApollo: createMockApolloProvider({ getProjectQueryLoading: true }), - props: { selectedProject: mockProject }, + props: { selectedProject: mockSelectedProject }, }); expect(findLoadingIcon().isVisible()).toBe(true); @@ -113,7 +112,7 @@ describe('SourceBranchDropdown', () => { describe('when branches have loaded', () => { describe('when searching branches', () => { it('triggers a refetch', async () => { - createComponent({ mountFn: mount, props: { selectedProject: mockProject } }); + createComponent({ mountFn: mount, props: { selectedProject: mockSelectedProject } }); await waitForPromises(); jest.clearAllMocks(); @@ -131,7 +130,7 @@ describe('SourceBranchDropdown', () => { describe('template', () => { beforeEach(async () => { - createComponent({ props: { selectedProject: mockProject } }); + createComponent({ props: { selectedProject: mockSelectedProject } }); await waitForPromises(); }); diff --git a/spec/frontend/jira_connect/branches/mock_data.js b/spec/frontend/jira_connect/branches/mock_data.js new file mode 100644 index 00000000000..742ab5392c8 --- /dev/null +++ b/spec/frontend/jira_connect/branches/mock_data.js @@ -0,0 +1,30 @@ +export const mockProjects = [ + { + id: 'test', + name: 'test', + nameWithNamespace: 'test', + avatarUrl: 'https://gitlab.com', + path: 'test-path', + fullPath: 'test-path', + repository: { + empty: false, + }, + userPermissions: { + pushCode: true, + }, + }, + { + id: 'gitlab', + name: 'GitLab', + nameWithNamespace: 'gitlab-org/gitlab', + avatarUrl: 'https://gitlab.com', + path: 'gitlab', + fullPath: 'gitlab-org/gitlab', + repository: { + empty: false, + }, + userPermissions: { + pushCode: true, + }, + }, +]; diff --git a/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item_spec.js b/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item_spec.js index 15e9a740c83..b0d5859cd31 100644 --- a/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item_spec.js +++ b/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item_spec.js @@ -1,5 +1,6 @@ import { GlButton } from '@gitlab/ui'; import { mount, shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import waitForPromises from 'helpers/wait_for_promises'; import * as JiraConnectApi from '~/jira_connect/subscriptions/api'; @@ -63,7 +64,7 @@ describe('GroupsListItem', () => { clickLinkButton(); - await wrapper.vm.$nextTick(); + await nextTick(); expect(findLinkButton().props('loading')).toBe(true); diff --git a/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_spec.js b/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_spec.js index 04aba8bda23..d871b1e1dcc 100644 --- a/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_spec.js +++ b/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_spec.js @@ -1,5 +1,6 @@ import { GlAlert, GlLoadingIcon, GlSearchBoxByType, GlPagination } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; import { fetchGroups } from '~/jira_connect/subscriptions/api'; @@ -61,7 +62,7 @@ describe('GroupsList', () => { fetchGroups.mockReturnValue(new Promise(() => {})); createComponent(); - await wrapper.vm.$nextTick(); + await nextTick(); expect(findGlLoadingIcon().exists()).toBe(true); }); @@ -100,6 +101,8 @@ describe('GroupsList', () => { }); createComponent(); + // wait for the initial loadGroups + // to finish. await waitForPromises(); }); @@ -124,7 +127,7 @@ describe('GroupsList', () => { findFirstItem().vm.$emit('error', errorMessage); - await wrapper.vm.$nextTick(); + await nextTick(); expect(findGlAlert().exists()).toBe(true); expect(findGlAlert().text()).toContain(errorMessage); @@ -136,10 +139,12 @@ describe('GroupsList', () => { describe('while groups are loading', () => { beforeEach(async () => { fetchGroups.mockClear(); + // return a never-ending promise to make test + // deterministic. fetchGroups.mockReturnValue(new Promise(() => {})); findSearchBox().vm.$emit('input', mockSearchTeam); - await wrapper.vm.$nextTick(); + await nextTick(); }); it('calls `fetchGroups` with search term', () => { @@ -172,7 +177,7 @@ describe('GroupsList', () => { describe('when group search finishes loading', () => { beforeEach(async () => { fetchGroups.mockResolvedValue({ data: [mockGroup1] }); - findSearchBox().vm.$emit('input'); + findSearchBox().vm.$emit('input', mockSearchTeam); await waitForPromises(); }); @@ -183,32 +188,48 @@ describe('GroupsList', () => { }); }); - it.each` - userSearchTerm | finalSearchTerm - ${'gitl'} | ${'gitl'} - ${'git'} | ${'git'} - ${'gi'} | ${''} - ${'g'} | ${''} - ${''} | ${''} - ${undefined} | ${undefined} + describe.each` + previousSearch | newSearch | shouldSearch | expectedSearchValue + ${''} | ${'git'} | ${true} | ${'git'} + ${'g'} | ${'git'} | ${true} | ${'git'} + ${'git'} | ${'gitl'} | ${true} | ${'gitl'} + ${'git'} | ${'gi'} | ${true} | ${''} + ${'gi'} | ${'g'} | ${false} | ${undefined} + ${'g'} | ${''} | ${false} | ${undefined} + ${''} | ${'g'} | ${false} | ${undefined} `( - 'searches for "$finalSearchTerm" when user enters "$userSearchTerm"', - async ({ userSearchTerm, finalSearchTerm }) => { - fetchGroups.mockResolvedValue({ - data: [mockGroup1], - headers: { 'X-PAGE': 1, 'X-TOTAL': 1 }, + 'when previous search was "$previousSearch" and user enters "$newSearch"', + ({ previousSearch, newSearch, shouldSearch, expectedSearchValue }) => { + beforeEach(async () => { + fetchGroups.mockResolvedValue({ + data: [mockGroup1], + headers: { 'X-PAGE': 1, 'X-TOTAL': 1 }, + }); + + // wait for initial load + createComponent(); + await waitForPromises(); + + // set up the "previous search" + findSearchBox().vm.$emit('input', previousSearch); + await waitForPromises(); + + fetchGroups.mockClear(); }); - createComponent(); - await waitForPromises(); - - const searchBox = findSearchBox(); - searchBox.vm.$emit('input', userSearchTerm); - - expect(fetchGroups).toHaveBeenLastCalledWith(mockGroupsPath, { - page: 1, - perPage: DEFAULT_GROUPS_PER_PAGE, - search: finalSearchTerm, + it(`${shouldSearch ? 'should' : 'should not'} execute fetch new results`, () => { + // enter the new search + findSearchBox().vm.$emit('input', newSearch); + + if (shouldSearch) { + expect(fetchGroups).toHaveBeenCalledWith(mockGroupsPath, { + page: 1, + perPage: DEFAULT_GROUPS_PER_PAGE, + search: expectedSearchValue, + }); + } else { + expect(fetchGroups).not.toHaveBeenCalled(); + } }); }, ); @@ -226,7 +247,13 @@ describe('GroupsList', () => { await waitForPromises(); const paginationEl = findPagination(); - paginationEl.vm.$emit('input', 2); + + // mock the response from page 2 + fetchGroups.mockResolvedValue({ + headers: { 'X-TOTAL': totalItems, 'X-PAGE': 2 }, + data: mockGroups, + }); + await paginationEl.vm.$emit('input', 2); }); it('should load results for page 2', () => { @@ -237,18 +264,23 @@ describe('GroupsList', () => { }); }); - it('resets page to 1 on search `input` event', () => { - const mockSearchTerm = 'gitlab'; - const searchBox = findSearchBox(); - - searchBox.vm.$emit('input', mockSearchTerm); + it.each` + scenario | searchTerm | expectedPage | expectedSearchTerm + ${'preserves current page'} | ${'gi'} | ${2} | ${''} + ${'resets page to 1'} | ${'gitlab'} | ${1} | ${'gitlab'} + `( + '$scenario when search term is $searchTerm', + ({ searchTerm, expectedPage, expectedSearchTerm }) => { + const searchBox = findSearchBox(); + searchBox.vm.$emit('input', searchTerm); - expect(fetchGroups).toHaveBeenLastCalledWith(mockGroupsPath, { - page: 1, - perPage: DEFAULT_GROUPS_PER_PAGE, - search: mockSearchTerm, - }); - }); + expect(fetchGroups).toHaveBeenLastCalledWith(mockGroupsPath, { + page: expectedPage, + perPage: DEFAULT_GROUPS_PER_PAGE, + search: expectedSearchTerm, + }); + }, + ); }); }); diff --git a/spec/frontend/jira_connect/subscriptions/components/app_spec.js b/spec/frontend/jira_connect/subscriptions/components/app_spec.js index 47fe96262ee..aa0f1440b20 100644 --- a/spec/frontend/jira_connect/subscriptions/components/app_spec.js +++ b/spec/frontend/jira_connect/subscriptions/components/app_spec.js @@ -1,10 +1,10 @@ -import { GlAlert, GlLink, GlEmptyState } from '@gitlab/ui'; -import { mount, shallowMount } from '@vue/test-utils'; +import { GlLink } from '@gitlab/ui'; +import { nextTick } from 'vue'; +import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper'; import JiraConnectApp from '~/jira_connect/subscriptions/components/app.vue'; -import AddNamespaceButton from '~/jira_connect/subscriptions/components/add_namespace_button.vue'; -import SignInButton from '~/jira_connect/subscriptions/components/sign_in_button.vue'; -import SubscriptionsList from '~/jira_connect/subscriptions/components/subscriptions_list.vue'; +import SignInPage from '~/jira_connect/subscriptions/pages/sign_in.vue'; +import SubscriptionsPage from '~/jira_connect/subscriptions/pages/subscriptions.vue'; import UserLink from '~/jira_connect/subscriptions/components/user_link.vue'; import createStore from '~/jira_connect/subscriptions/store'; import { SET_ALERT } from '~/jira_connect/subscriptions/store/mutation_types'; @@ -20,14 +20,12 @@ describe('JiraConnectApp', () => { let wrapper; let store; - const findAlert = () => wrapper.findComponent(GlAlert); + const findAlert = () => wrapper.findByTestId('jira-connect-persisted-alert'); const findAlertLink = () => findAlert().findComponent(GlLink); - const findSignInButton = () => wrapper.findComponent(SignInButton); - const findAddNamespaceButton = () => wrapper.findComponent(AddNamespaceButton); - const findSubscriptionsList = () => wrapper.findComponent(SubscriptionsList); - const findEmptyState = () => wrapper.findComponent(GlEmptyState); + const findSignInPage = () => wrapper.findComponent(SignInPage); + const findSubscriptionsPage = () => wrapper.findComponent(SubscriptionsPage); - const createComponent = ({ provide, mountFn = shallowMount } = {}) => { + const createComponent = ({ provide, mountFn = shallowMountExtended } = {}) => { store = createStore(); wrapper = mountFn(JiraConnectApp, { @@ -42,49 +40,35 @@ describe('JiraConnectApp', () => { describe('template', () => { describe.each` - scenario | usersPath | subscriptions | expectSignInButton | expectEmptyState | expectNamespaceButton | expectSubscriptionsList - ${'user is not signed in with subscriptions'} | ${'/users'} | ${[mockSubscription]} | ${true} | ${false} | ${false} | ${true} - ${'user is not signed in without subscriptions'} | ${'/users'} | ${undefined} | ${true} | ${false} | ${false} | ${false} - ${'user is signed in with subscriptions'} | ${undefined} | ${[mockSubscription]} | ${false} | ${false} | ${true} | ${true} - ${'user is signed in without subscriptions'} | ${undefined} | ${undefined} | ${false} | ${true} | ${false} | ${false} - `( - 'when $scenario', - ({ - usersPath, - expectSignInButton, - subscriptions, - expectEmptyState, - expectNamespaceButton, - expectSubscriptionsList, - }) => { - beforeEach(() => { - createComponent({ - provide: { - usersPath, - subscriptions, - }, - }); - }); - - it(`${expectSignInButton ? 'renders' : 'does not render'} sign in button`, () => { - expect(findSignInButton().exists()).toBe(expectSignInButton); - }); - - it(`${expectEmptyState ? 'renders' : 'does not render'} empty state`, () => { - expect(findEmptyState().exists()).toBe(expectEmptyState); + scenario | usersPath | shouldRenderSignInPage | shouldRenderSubscriptionsPage + ${'user is not signed in'} | ${'/users'} | ${true} | ${false} + ${'user is signed in'} | ${undefined} | ${false} | ${true} + `('when $scenario', ({ usersPath, shouldRenderSignInPage, shouldRenderSubscriptionsPage }) => { + beforeEach(() => { + createComponent({ + provide: { + usersPath, + subscriptions: [mockSubscription], + }, }); + }); - it(`${ - expectNamespaceButton ? 'renders' : 'does not render' - } button to add namespace`, () => { - expect(findAddNamespaceButton().exists()).toBe(expectNamespaceButton); - }); + it(`${shouldRenderSignInPage ? 'renders' : 'does not render'} sign in page`, () => { + expect(findSignInPage().exists()).toBe(shouldRenderSignInPage); + if (shouldRenderSignInPage) { + expect(findSignInPage().props('hasSubscriptions')).toBe(true); + } + }); - it(`${expectSubscriptionsList ? 'renders' : 'does not render'} subscriptions list`, () => { - expect(findSubscriptionsList().exists()).toBe(expectSubscriptionsList); - }); - }, - ); + it(`${ + shouldRenderSubscriptionsPage ? 'renders' : 'does not render' + } subscriptions page`, () => { + expect(findSubscriptionsPage().exists()).toBe(shouldRenderSubscriptionsPage); + if (shouldRenderSubscriptionsPage) { + expect(findSubscriptionsPage().props('hasSubscriptions')).toBe(true); + } + }); + }); it('renders UserLink component', () => { createComponent({ @@ -116,7 +100,7 @@ describe('JiraConnectApp', () => { createComponent(); store.commit(SET_ALERT, { message, variant }); - await wrapper.vm.$nextTick(); + await nextTick(); const alert = findAlert(); @@ -134,22 +118,22 @@ describe('JiraConnectApp', () => { createComponent(); store.commit(SET_ALERT, { message: 'test message' }); - await wrapper.vm.$nextTick(); + await nextTick(); findAlert().vm.$emit('dismiss'); - await wrapper.vm.$nextTick(); + await nextTick(); expect(findAlert().exists()).toBe(false); }); it('renders link when `linkUrl` is set', async () => { - createComponent({ mountFn: mount }); + createComponent({ mountFn: mountExtended }); store.commit(SET_ALERT, { message: __('test message %{linkStart}test link%{linkEnd}'), linkUrl: 'https://gitlab.com', }); - await wrapper.vm.$nextTick(); + await nextTick(); const alertLink = findAlertLink(); diff --git a/spec/frontend/jira_connect/subscriptions/components/compatibility_alert_spec.js b/spec/frontend/jira_connect/subscriptions/components/compatibility_alert_spec.js new file mode 100644 index 00000000000..f8ee8c2c664 --- /dev/null +++ b/spec/frontend/jira_connect/subscriptions/components/compatibility_alert_spec.js @@ -0,0 +1,56 @@ +import { GlAlert, GlLink } from '@gitlab/ui'; +import { shallowMount, mount } from '@vue/test-utils'; +import CompatibilityAlert from '~/jira_connect/subscriptions/components/compatibility_alert.vue'; + +import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; + +describe('CompatibilityAlert', () => { + let wrapper; + + const createComponent = ({ mountFn = shallowMount } = {}) => { + wrapper = mountFn(CompatibilityAlert); + }; + + const findAlert = () => wrapper.findComponent(GlAlert); + const findLink = () => wrapper.findComponent(GlLink); + const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync); + + afterEach(() => { + wrapper.destroy(); + }); + + it('displays an alert', () => { + createComponent(); + + expect(findAlert().exists()).toBe(true); + }); + + it('renders help link with target="_blank" and rel="noopener noreferrer"', () => { + createComponent({ mountFn: mount }); + expect(findLink().attributes()).toMatchObject({ + target: '_blank', + rel: 'noopener noreferrer', + }); + }); + + it('`local-storage-sync` value prop is initially false', () => { + createComponent(); + + expect(findLocalStorageSync().props('value')).toBe(false); + }); + + describe('when dismissed', () => { + beforeEach(async () => { + createComponent(); + await findAlert().vm.$emit('dismiss'); + }); + + it('hides alert', () => { + expect(findAlert().exists()).toBe(false); + }); + + it('updates value prop of `local-storage-sync`', () => { + expect(findLocalStorageSync().props('value')).toBe(true); + }); + }); +}); diff --git a/spec/frontend/jira_connect/subscriptions/components/sign_in_button_spec.js b/spec/frontend/jira_connect/subscriptions/components/sign_in_button_spec.js index cb5ae877c47..94dcf9decec 100644 --- a/spec/frontend/jira_connect/subscriptions/components/sign_in_button_spec.js +++ b/spec/frontend/jira_connect/subscriptions/components/sign_in_button_spec.js @@ -11,11 +11,12 @@ jest.mock('~/jira_connect/subscriptions/utils'); describe('SignInButton', () => { let wrapper; - const createComponent = () => { + const createComponent = ({ slots } = {}) => { wrapper = shallowMount(SignInButton, { propsData: { usersPath: MOCK_USERS_PATH, }, + slots, }); }; @@ -29,6 +30,7 @@ describe('SignInButton', () => { createComponent(); expect(findButton().exists()).toBe(true); + expect(findButton().text()).toBe(SignInButton.i18n.defaultButtonText); }); describe.each` @@ -45,4 +47,12 @@ describe('SignInButton', () => { expect(findButton().attributes('href')).toBe(expectedHref); }); }); + + describe('with slot', () => { + const mockSlotContent = 'custom button content!'; + it('renders slot content in button', () => { + createComponent({ slots: { default: mockSlotContent } }); + expect(wrapper.text()).toMatchInterpolatedText(mockSlotContent); + }); + }); }); diff --git a/spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js b/spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js index 4e4a2b58600..2aad533f677 100644 --- a/spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js +++ b/spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js @@ -1,5 +1,6 @@ import { GlButton } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import waitForPromises from 'helpers/wait_for_promises'; import * as JiraConnectApi from '~/jira_connect/subscriptions/api'; @@ -71,7 +72,7 @@ describe('SubscriptionsList', () => { clickUnlinkButton(); - await wrapper.vm.$nextTick(); + await nextTick(); expect(findUnlinkButton().props('loading')).toBe(true); diff --git a/spec/frontend/jira_connect/subscriptions/pages/sign_in_spec.js b/spec/frontend/jira_connect/subscriptions/pages/sign_in_spec.js new file mode 100644 index 00000000000..4e3297506f1 --- /dev/null +++ b/spec/frontend/jira_connect/subscriptions/pages/sign_in_spec.js @@ -0,0 +1,62 @@ +import { mount } from '@vue/test-utils'; + +import SignInPage from '~/jira_connect/subscriptions/pages/sign_in.vue'; +import SignInButton from '~/jira_connect/subscriptions/components/sign_in_button.vue'; +import SubscriptionsList from '~/jira_connect/subscriptions/components/subscriptions_list.vue'; +import createStore from '~/jira_connect/subscriptions/store'; + +jest.mock('~/jira_connect/subscriptions/utils'); + +describe('SignInPage', () => { + let wrapper; + let store; + + const findSignInButton = () => wrapper.findComponent(SignInButton); + const findSubscriptionsList = () => wrapper.findComponent(SubscriptionsList); + + const createComponent = ({ provide, props } = {}) => { + store = createStore(); + + wrapper = mount(SignInPage, { + store, + provide, + propsData: props, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('template', () => { + const mockUsersPath = '/test'; + describe.each` + scenario | expectSubscriptionsList | signInButtonText + ${'with subscriptions'} | ${true} | ${SignInPage.i18n.signinButtonTextWithSubscriptions} + ${'without subscriptions'} | ${false} | ${SignInButton.i18n.defaultButtonText} + `('$scenario', ({ expectSubscriptionsList, signInButtonText }) => { + beforeEach(() => { + createComponent({ + provide: { + usersPath: mockUsersPath, + }, + props: { + hasSubscriptions: expectSubscriptionsList, + }, + }); + }); + + it(`renders sign in button with text ${signInButtonText}`, () => { + expect(findSignInButton().text()).toMatchInterpolatedText(signInButtonText); + }); + + it('renders sign in button with `usersPath` prop', () => { + expect(findSignInButton().props('usersPath')).toBe(mockUsersPath); + }); + + it(`${expectSubscriptionsList ? 'renders' : 'does not render'} subscriptions list`, () => { + expect(findSubscriptionsList().exists()).toBe(expectSubscriptionsList); + }); + }); + }); +}); diff --git a/spec/frontend/jira_connect/subscriptions/pages/subscriptions_spec.js b/spec/frontend/jira_connect/subscriptions/pages/subscriptions_spec.js new file mode 100644 index 00000000000..198278efc1f --- /dev/null +++ b/spec/frontend/jira_connect/subscriptions/pages/subscriptions_spec.js @@ -0,0 +1,56 @@ +import { GlEmptyState } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import SubscriptionsPage from '~/jira_connect/subscriptions/pages/subscriptions.vue'; +import AddNamespaceButton from '~/jira_connect/subscriptions/components/add_namespace_button.vue'; +import SubscriptionsList from '~/jira_connect/subscriptions/components/subscriptions_list.vue'; +import createStore from '~/jira_connect/subscriptions/store'; + +describe('SubscriptionsPage', () => { + let wrapper; + let store; + + const findAddNamespaceButton = () => wrapper.findComponent(AddNamespaceButton); + const findSubscriptionsList = () => wrapper.findComponent(SubscriptionsList); + const findEmptyState = () => wrapper.findComponent(GlEmptyState); + + const createComponent = ({ props } = {}) => { + store = createStore(); + + wrapper = shallowMount(SubscriptionsPage, { + store, + propsData: props, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('template', () => { + describe.each` + scenario | expectSubscriptionsList | expectEmptyState + ${'with subscriptions'} | ${true} | ${false} + ${'without subscriptions'} | ${false} | ${true} + `('$scenario', ({ expectEmptyState, expectSubscriptionsList }) => { + beforeEach(() => { + createComponent({ + props: { + hasSubscriptions: expectSubscriptionsList, + }, + }); + }); + + it('renders button to add namespace', () => { + expect(findAddNamespaceButton().exists()).toBe(true); + }); + + it(`${expectEmptyState ? 'renders' : 'does not render'} empty state`, () => { + expect(findEmptyState().exists()).toBe(expectEmptyState); + }); + + it(`${expectSubscriptionsList ? 'renders' : 'does not render'} subscriptions list`, () => { + expect(findSubscriptionsList().exists()).toBe(expectSubscriptionsList); + }); + }); + }); +}); |