diff options
Diffstat (limited to 'spec/frontend/projects/settings')
14 files changed, 676 insertions, 121 deletions
diff --git a/spec/frontend/projects/settings/branch_rules/branch_dropdown_spec.js b/spec/frontend/projects/settings/branch_rules/components/edit/branch_dropdown_spec.js index 79bce5a4b3f..11f219c1f90 100644 --- a/spec/frontend/projects/settings/branch_rules/branch_dropdown_spec.js +++ b/spec/frontend/projects/settings/branch_rules/components/edit/branch_dropdown_spec.js @@ -4,7 +4,7 @@ import { GlDropdown, GlSearchBoxByType, GlDropdownItem, GlSprintf } from '@gitla import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import BranchDropdown, { i18n, -} from '~/projects/settings/branch_rules/components/branch_dropdown.vue'; +} from '~/projects/settings/branch_rules/components/edit/branch_dropdown.vue'; import createMockApollo from 'helpers/mock_apollo_helper'; import branchesQuery from '~/projects/settings/branch_rules/queries/branches.query.graphql'; import waitForPromises from 'helpers/wait_for_promises'; diff --git a/spec/frontend/projects/settings/branch_rules/rule_edit_spec.js b/spec/frontend/projects/settings/branch_rules/components/edit/index_spec.js index b0b2b9191d4..21e63fdb24d 100644 --- a/spec/frontend/projects/settings/branch_rules/rule_edit_spec.js +++ b/spec/frontend/projects/settings/branch_rules/components/edit/index_spec.js @@ -1,9 +1,9 @@ import { nextTick } from 'vue'; import { getParameterByName } from '~/lib/utils/url_utility'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import RuleEdit from '~/projects/settings/branch_rules/components/rule_edit.vue'; -import BranchDropdown from '~/projects/settings/branch_rules/components/branch_dropdown.vue'; -import Protections from '~/projects/settings/branch_rules/components/protections/index.vue'; +import RuleEdit from '~/projects/settings/branch_rules/components/edit/index.vue'; +import BranchDropdown from '~/projects/settings/branch_rules/components/edit/branch_dropdown.vue'; +import Protections from '~/projects/settings/branch_rules/components/edit/protections/index.vue'; jest.mock('~/lib/utils/url_utility', () => ({ getParameterByName: jest.fn().mockImplementation(() => 'main'), diff --git a/spec/frontend/projects/settings/branch_rules/components/protections/index_spec.js b/spec/frontend/projects/settings/branch_rules/components/edit/protections/index_spec.js index 3592fa50622..ee90ff8318f 100644 --- a/spec/frontend/projects/settings/branch_rules/components/protections/index_spec.js +++ b/spec/frontend/projects/settings/branch_rules/components/edit/protections/index_spec.js @@ -3,10 +3,10 @@ import { GlLink } from '@gitlab/ui'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import Protections, { i18n, -} from '~/projects/settings/branch_rules/components/protections/index.vue'; -import PushProtections from '~/projects/settings/branch_rules/components/protections/push_protections.vue'; -import MergeProtections from '~/projects/settings/branch_rules/components/protections/merge_protections.vue'; -import { protections } from '../../mock_data'; +} from '~/projects/settings/branch_rules/components/edit/protections/index.vue'; +import PushProtections from '~/projects/settings/branch_rules/components/edit/protections/push_protections.vue'; +import MergeProtections from '~/projects/settings/branch_rules/components/edit/protections/merge_protections.vue'; +import { protections } from '../../../mock_data'; describe('Branch Protections', () => { let wrapper; diff --git a/spec/frontend/projects/settings/branch_rules/components/protections/merge_protections_spec.js b/spec/frontend/projects/settings/branch_rules/components/edit/protections/merge_protections_spec.js index 0e168a2ad78..b5fdc46d600 100644 --- a/spec/frontend/projects/settings/branch_rules/components/protections/merge_protections_spec.js +++ b/spec/frontend/projects/settings/branch_rules/components/edit/protections/merge_protections_spec.js @@ -2,8 +2,8 @@ import { GlFormGroup, GlFormCheckbox } from '@gitlab/ui'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import MergeProtections, { i18n, -} from '~/projects/settings/branch_rules/components/protections/merge_protections.vue'; -import { membersAllowedToMerge, requireCodeOwnersApproval } from '../../mock_data'; +} from '~/projects/settings/branch_rules/components/edit/protections/merge_protections.vue'; +import { membersAllowedToMerge, requireCodeOwnersApproval } from '../../../mock_data'; describe('Merge Protections', () => { let wrapper; diff --git a/spec/frontend/projects/settings/branch_rules/components/protections/push_protections_spec.js b/spec/frontend/projects/settings/branch_rules/components/edit/protections/push_protections_spec.js index d54dad08338..60bb7a51dcb 100644 --- a/spec/frontend/projects/settings/branch_rules/components/protections/push_protections_spec.js +++ b/spec/frontend/projects/settings/branch_rules/components/edit/protections/push_protections_spec.js @@ -2,8 +2,8 @@ import { GlFormGroup, GlSprintf, GlFormCheckbox } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import PushProtections, { i18n, -} from '~/projects/settings/branch_rules/components/protections/push_protections.vue'; -import { membersAllowedToPush, allowForcePush } from '../../mock_data'; +} from '~/projects/settings/branch_rules/components/edit/protections/push_protections.vue'; +import { membersAllowedToPush, allowForcePush } from '../../../mock_data'; describe('Push Protections', () => { let wrapper; diff --git a/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js b/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js new file mode 100644 index 00000000000..bf4026b65db --- /dev/null +++ b/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js @@ -0,0 +1,113 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import * as util from '~/lib/utils/url_utility'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import RuleView from '~/projects/settings/branch_rules/components/view/index.vue'; +import { + I18N, + ALL_BRANCHES_WILDCARD, +} from '~/projects/settings/branch_rules/components/view/constants'; +import Protection from '~/projects/settings/branch_rules/components/view/protection.vue'; +import branchRulesQuery from '~/projects/settings/branch_rules/queries/branch_rules_details.query.graphql'; +import { sprintf } from '~/locale'; +import { branchProtectionsMockResponse } from './mock_data'; + +jest.mock('~/lib/utils/url_utility', () => ({ + getParameterByName: jest.fn().mockReturnValue('main'), + joinPaths: jest.fn(), +})); + +Vue.use(VueApollo); + +const protectionMockProps = { + headerLinkHref: 'protected/branches', + headerLinkTitle: 'Manage in Protected Branches', + roles: [{ accessLevelDescription: 'Maintainers' }], + users: [{ avatarUrl: 'test.com/user.png', name: 'peter', webUrl: 'test.com' }], +}; + +describe('View branch rules', () => { + let wrapper; + let fakeApollo; + const projectPath = 'test/testing'; + const protectedBranchesPath = 'protected/branches'; + const approvalRulesPath = 'approval/rules'; + const branchProtectionsMockRequestHandler = jest + .fn() + .mockResolvedValue(branchProtectionsMockResponse); + + const createComponent = async () => { + fakeApollo = createMockApollo([[branchRulesQuery, branchProtectionsMockRequestHandler]]); + + wrapper = shallowMountExtended(RuleView, { + apolloProvider: fakeApollo, + provide: { projectPath, protectedBranchesPath, approvalRulesPath }, + }); + + await waitForPromises(); + }; + + beforeEach(() => createComponent()); + + afterEach(() => wrapper.destroy()); + + const findBranchName = () => wrapper.findByTestId('branch'); + const findBranchTitle = () => wrapper.findByTestId('branch-title'); + const findBranchProtectionTitle = () => wrapper.findByText(I18N.protectBranchTitle); + const findBranchProtections = () => wrapper.findAllComponents(Protection); + const findForcePushTitle = () => wrapper.findByText(I18N.allowForcePushDescription); + const findApprovalsTitle = () => wrapper.findByText(I18N.approvalsTitle); + + it('gets the branch param from url and renders it in the view', () => { + expect(util.getParameterByName).toHaveBeenCalledWith('branch'); + expect(findBranchName().text()).toBe('main'); + expect(findBranchTitle().text()).toBe(I18N.branchNameOrPattern); + }); + + it('renders the correct label if all branches are targeted', async () => { + jest.spyOn(util, 'getParameterByName').mockReturnValueOnce(ALL_BRANCHES_WILDCARD); + await createComponent(); + + expect(findBranchName().text()).toBe(I18N.allBranches); + expect(findBranchTitle().text()).toBe(I18N.targetBranch); + jest.restoreAllMocks(); + }); + + it('renders the correct branch title', () => { + expect(findBranchTitle().exists()).toBe(true); + }); + + it('renders a branch protection title', () => { + expect(findBranchProtectionTitle().exists()).toBe(true); + }); + + it('renders a branch protection component for push rules', () => { + expect(findBranchProtections().at(0).props()).toMatchObject({ + header: sprintf(I18N.allowedToPushHeader, { total: 2 }), + ...protectionMockProps, + }); + }); + + it('renders force push protection', () => { + expect(findForcePushTitle().exists()).toBe(true); + }); + + it('renders a branch protection component for merge rules', () => { + expect(findBranchProtections().at(1).props()).toMatchObject({ + header: sprintf(I18N.allowedToMergeHeader, { total: 2 }), + ...protectionMockProps, + }); + }); + + it('renders a branch protection component for approvals', () => { + expect(findApprovalsTitle().exists()).toBe(true); + + expect(findBranchProtections().at(2).props()).toMatchObject({ + header: sprintf(I18N.approvalsHeader, { total: 0 }), + headerLinkHref: approvalRulesPath, + headerLinkTitle: I18N.manageApprovalsLinkTitle, + }); + }); +}); diff --git a/spec/frontend/projects/settings/branch_rules/components/view/mock_data.js b/spec/frontend/projects/settings/branch_rules/components/view/mock_data.js new file mode 100644 index 00000000000..c3f573061da --- /dev/null +++ b/spec/frontend/projects/settings/branch_rules/components/view/mock_data.js @@ -0,0 +1,141 @@ +const usersMock = [ + { + username: 'usr1', + webUrl: 'http://test.test/usr1', + name: 'User 1', + avatarUrl: 'http://test.test/avt1.png', + }, + { + username: 'usr2', + webUrl: 'http://test.test/usr2', + name: 'User 2', + avatarUrl: 'http://test.test/avt2.png', + }, + { + username: 'usr3', + webUrl: 'http://test.test/usr3', + name: 'User 3', + avatarUrl: 'http://test.test/avt3.png', + }, + { + username: 'usr4', + webUrl: 'http://test.test/usr4', + name: 'User 4', + avatarUrl: 'http://test.test/avt4.png', + }, + { + username: 'usr5', + webUrl: 'http://test.test/usr5', + name: 'User 5', + avatarUrl: 'http://test.test/avt5.png', + }, +]; + +const accessLevelsMock = [ + { accessLevelDescription: 'Administrator' }, + { accessLevelDescription: 'Maintainer' }, +]; + +const approvalsRequired = 3; + +const groupsMock = [{ name: 'test_group_1' }, { name: 'test_group_2' }]; + +export const protectionPropsMock = { + header: 'Test protection', + headerLinkTitle: 'Test link title', + headerLinkHref: 'Test link href', + roles: accessLevelsMock, + users: usersMock, + groups: groupsMock, + approvals: [ + { + name: 'test', + eligibleApprovers: { nodes: usersMock }, + approvalsRequired, + }, + ], +}; + +export const protectionRowPropsMock = { + title: 'Test title', + users: usersMock, + accessLevels: accessLevelsMock, + approvalsRequired, +}; + +export const accessLevelsMockResponse = [ + { + __typename: 'PushAccessLevelEdge', + node: { + __typename: 'PushAccessLevel', + accessLevel: 40, + accessLevelDescription: 'Jona Langworth', + group: null, + user: { + __typename: 'UserCore', + id: '123', + webUrl: 'test.com', + name: 'peter', + avatarUrl: 'test.com/user.png', + }, + }, + }, + { + __typename: 'PushAccessLevelEdge', + node: { + __typename: 'PushAccessLevel', + accessLevel: 40, + accessLevelDescription: 'Maintainers', + group: null, + user: null, + }, + }, +]; + +export const branchProtectionsMockResponse = { + data: { + project: { + id: 'gid://gitlab/Project/6', + __typename: 'Project', + branchRules: { + __typename: 'BranchRuleConnection', + nodes: [ + { + __typename: 'BranchRule', + name: 'main', + branchProtection: { + __typename: 'BranchProtection', + allowForcePush: true, + codeOwnerApprovalRequired: true, + mergeAccessLevels: { + __typename: 'MergeAccessLevelConnection', + edges: accessLevelsMockResponse, + }, + pushAccessLevels: { + __typename: 'PushAccessLevelConnection', + edges: accessLevelsMockResponse, + }, + }, + }, + { + __typename: 'BranchRule', + name: '*', + branchProtection: { + __typename: 'BranchProtection', + allowForcePush: true, + codeOwnerApprovalRequired: true, + mergeAccessLevels: { + __typename: 'MergeAccessLevelConnection', + edges: [], + }, + pushAccessLevels: { + __typename: 'PushAccessLevelConnection', + edges: [], + }, + }, + }, + ], + }, + }, + }, +}; diff --git a/spec/frontend/projects/settings/branch_rules/components/view/protection_row_spec.js b/spec/frontend/projects/settings/branch_rules/components/view/protection_row_spec.js new file mode 100644 index 00000000000..b0a69bedd3e --- /dev/null +++ b/spec/frontend/projects/settings/branch_rules/components/view/protection_row_spec.js @@ -0,0 +1,71 @@ +import { GlAvatarsInline, GlAvatar, GlAvatarLink } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import ProtectionRow, { + MAX_VISIBLE_AVATARS, + AVATAR_SIZE, +} from '~/projects/settings/branch_rules/components/view/protection_row.vue'; +import { protectionRowPropsMock } from './mock_data'; + +describe('Branch rule protection row', () => { + let wrapper; + + const createComponent = () => { + wrapper = shallowMountExtended(ProtectionRow, { + propsData: protectionRowPropsMock, + stubs: { GlAvatarsInline }, + }); + }; + + beforeEach(() => createComponent()); + + afterEach(() => wrapper.destroy()); + + const findTitle = () => wrapper.findByText(protectionRowPropsMock.title); + const findAvatarsInline = () => wrapper.findComponent(GlAvatarsInline); + const findAvatarLinks = () => wrapper.findAllComponents(GlAvatarLink); + const findAvatars = () => wrapper.findAllComponents(GlAvatar); + const findAccessLevels = () => wrapper.findAllByTestId('access-level'); + const findApprovalsRequired = () => + wrapper.findByText(`${protectionRowPropsMock.approvalsRequired} approvals required`); + + it('renders a title', () => { + expect(findTitle().exists()).toBe(true); + }); + + it('renders an avatars-inline component', () => { + expect(findAvatarsInline().props('avatars')).toMatchObject(protectionRowPropsMock.users); + expect(findAvatarsInline().props('badgeSrOnlyText')).toBe('1 additional user'); + }); + + it('renders avatar-link components', () => { + expect(findAvatarLinks().length).toBe(MAX_VISIBLE_AVATARS); + + expect(findAvatarLinks().at(1).attributes('href')).toBe(protectionRowPropsMock.users[1].webUrl); + expect(findAvatarLinks().at(1).attributes('title')).toBe(protectionRowPropsMock.users[1].name); + }); + + it('renders avatar components', () => { + expect(findAvatars().length).toBe(MAX_VISIBLE_AVATARS); + + expect(findAvatars().at(1).attributes('src')).toBe(protectionRowPropsMock.users[1].avatarUrl); + expect(findAvatars().at(1).attributes('label')).toBe(protectionRowPropsMock.users[1].name); + expect(findAvatars().at(1).props('size')).toBe(AVATAR_SIZE); + }); + + it('renders access level descriptions', () => { + expect(findAccessLevels().length).toBe(protectionRowPropsMock.accessLevels.length); + + expect(findAccessLevels().at(0).text()).toBe( + protectionRowPropsMock.accessLevels[0].accessLevelDescription, + ); + expect(findAccessLevels().at(1).text()).toContain(','); + + expect(findAccessLevels().at(1).text()).toContain( + protectionRowPropsMock.accessLevels[1].accessLevelDescription, + ); + }); + + it('renders the number of approvals required', () => { + expect(findApprovalsRequired().exists()).toBe(true); + }); +}); diff --git a/spec/frontend/projects/settings/branch_rules/components/view/protection_spec.js b/spec/frontend/projects/settings/branch_rules/components/view/protection_spec.js new file mode 100644 index 00000000000..e2fbb4f5bbb --- /dev/null +++ b/spec/frontend/projects/settings/branch_rules/components/view/protection_spec.js @@ -0,0 +1,68 @@ +import { GlCard, GlLink } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import Protection, { i18n } from '~/projects/settings/branch_rules/components/view/protection.vue'; +import ProtectionRow from '~/projects/settings/branch_rules/components/view/protection_row.vue'; +import { protectionPropsMock } from './mock_data'; + +describe('Branch rule protection', () => { + let wrapper; + + const createComponent = () => { + wrapper = shallowMountExtended(Protection, { + propsData: protectionPropsMock, + stubs: { GlCard }, + }); + }; + + beforeEach(() => createComponent()); + + afterEach(() => wrapper.destroy()); + + const findCard = () => wrapper.findComponent(GlCard); + const findHeader = () => wrapper.findByText(protectionPropsMock.header); + const findLink = () => wrapper.findComponent(GlLink); + const findProtectionRows = () => wrapper.findAllComponents(ProtectionRow); + + it('renders a card component', () => { + expect(findCard().exists()).toBe(true); + }); + + it('renders a header with a link', () => { + expect(findHeader().exists()).toBe(true); + expect(findLink().text()).toBe(protectionPropsMock.headerLinkTitle); + expect(findLink().attributes('href')).toBe(protectionPropsMock.headerLinkHref); + }); + + it('renders a protection row for roles', () => { + expect(findProtectionRows().at(0).props()).toMatchObject({ + accessLevels: protectionPropsMock.roles, + showDivider: false, + title: i18n.rolesTitle, + }); + }); + + it('renders a protection row for users', () => { + expect(findProtectionRows().at(1).props()).toMatchObject({ + users: protectionPropsMock.users, + showDivider: true, + title: i18n.usersTitle, + }); + }); + + it('renders a protection row for groups', () => { + expect(findProtectionRows().at(2).props()).toMatchObject({ + accessLevels: protectionPropsMock.groups, + showDivider: true, + title: i18n.groupsTitle, + }); + }); + + it('renders a protection row for approvals', () => { + const approval = protectionPropsMock.approvals[0]; + expect(findProtectionRows().at(3).props()).toMatchObject({ + title: approval.name, + users: approval.eligibleApprovers.nodes, + approvalsRequired: approval.approvalsRequired, + }); + }); +}); diff --git a/spec/frontend/projects/settings/components/default_branch_selector_spec.js b/spec/frontend/projects/settings/components/default_branch_selector_spec.js new file mode 100644 index 00000000000..94648d87524 --- /dev/null +++ b/spec/frontend/projects/settings/components/default_branch_selector_spec.js @@ -0,0 +1,46 @@ +import { shallowMount } from '@vue/test-utils'; +import DefaultBranchSelector from '~/projects/settings/components/default_branch_selector.vue'; +import RefSelector from '~/ref/components/ref_selector.vue'; +import { REF_TYPE_BRANCHES } from '~/ref/constants'; + +describe('projects/settings/components/default_branch_selector', () => { + const persistedDefaultBranch = 'main'; + const projectId = '123'; + let wrapper; + + const findRefSelector = () => wrapper.findComponent(RefSelector); + + const buildWrapper = () => { + wrapper = shallowMount(DefaultBranchSelector, { + propsData: { + persistedDefaultBranch, + projectId, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + beforeEach(() => { + buildWrapper(); + }); + + it('displays a RefSelector component', () => { + expect(findRefSelector().props()).toEqual({ + value: persistedDefaultBranch, + enabledRefTypes: [REF_TYPE_BRANCHES], + projectId, + state: true, + translations: { + dropdownHeader: expect.any(String), + searchPlaceholder: expect.any(String), + }, + useSymbolicRefNames: false, + name: 'project[default_branch]', + }); + + expect(findRefSelector().classes()).toContain('gl-w-full'); + }); +}); diff --git a/spec/frontend/projects/settings/components/transfer_project_form_spec.js b/spec/frontend/projects/settings/components/transfer_project_form_spec.js index bde7148078d..6e639f895a8 100644 --- a/spec/frontend/projects/settings/components/transfer_project_form_spec.js +++ b/spec/frontend/projects/settings/components/transfer_project_form_spec.js @@ -1,41 +1,65 @@ import Vue, { nextTick } from 'vue'; +import { GlAlert } from '@gitlab/ui'; import VueApollo from 'vue-apollo'; -import searchNamespacesWhereUserCanTransferProjectsQueryResponsePage1 from 'test_fixtures/graphql/projects/settings/search_namespaces_where_user_can_transfer_projects_page_1.query.graphql.json'; -import searchNamespacesWhereUserCanTransferProjectsQueryResponsePage2 from 'test_fixtures/graphql/projects/settings/search_namespaces_where_user_can_transfer_projects_page_2.query.graphql.json'; -import { - groupNamespaces, - userNamespaces, -} from 'jest/vue_shared/components/namespace_select/mock_data'; +import currentUserNamespaceQueryResponse from 'test_fixtures/graphql/projects/settings/current_user_namespace.query.graphql.json'; +import transferLocationsResponsePage1 from 'test_fixtures/api/projects/transfer_locations_page_1.json'; +import transferLocationsResponsePage2 from 'test_fixtures/api/projects/transfer_locations_page_2.json'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import createMockApollo from 'helpers/mock_apollo_helper'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import TransferProjectForm from '~/projects/settings/components/transfer_project_form.vue'; -import NamespaceSelect from '~/vue_shared/components/namespace_select/namespace_select.vue'; +import NamespaceSelect from '~/vue_shared/components/namespace_select/namespace_select_deprecated.vue'; import ConfirmDanger from '~/vue_shared/components/confirm_danger/confirm_danger.vue'; -import searchNamespacesWhereUserCanTransferProjectsQuery from '~/projects/settings/graphql/queries/search_namespaces_where_user_can_transfer_projects.query.graphql'; +import currentUserNamespaceQuery from '~/projects/settings/graphql/queries/current_user_namespace.query.graphql'; +import { getTransferLocations } from '~/api/projects_api'; import waitForPromises from 'helpers/wait_for_promises'; +jest.mock('~/api/projects_api', () => ({ + getTransferLocations: jest.fn(), +})); + describe('Transfer project form', () => { let wrapper; + const projectId = '1'; const confirmButtonText = 'Confirm'; const confirmationPhrase = 'You must construct additional pylons!'; - const runDebounce = () => jest.runAllTimers(); - Vue.use(VueApollo); - const defaultQueryHandler = jest - .fn() - .mockResolvedValue(searchNamespacesWhereUserCanTransferProjectsQueryResponsePage1); + const defaultQueryHandler = jest.fn().mockResolvedValue(currentUserNamespaceQueryResponse); + const mockResolvedGetTransferLocations = ({ + data = transferLocationsResponsePage1, + page = '1', + nextPage = '2', + prevPage = null, + } = {}) => { + getTransferLocations.mockResolvedValueOnce({ + data, + headers: { + 'x-per-page': '2', + 'x-page': page, + 'x-total': '4', + 'x-total-pages': '2', + 'x-next-page': nextPage, + 'x-prev-page': prevPage, + }, + }); + }; + const mockRejectedGetTransferLocations = () => { + const error = new Error(); + + getTransferLocations.mockRejectedValueOnce(error); + }; const createComponent = ({ - requestHandlers = [[searchNamespacesWhereUserCanTransferProjectsQuery, defaultQueryHandler]], + requestHandlers = [[currentUserNamespaceQuery, defaultQueryHandler]], } = {}) => { wrapper = shallowMountExtended(TransferProjectForm, { + provide: { + projectId, + }, propsData: { - userNamespaces, - groupNamespaces, confirmButtonText, confirmationPhrase, }, @@ -44,7 +68,12 @@ describe('Transfer project form', () => { }; const findNamespaceSelect = () => wrapper.findComponent(NamespaceSelect); + const showNamespaceSelect = async () => { + findNamespaceSelect().vm.$emit('show'); + await waitForPromises(); + }; const findConfirmDanger = () => wrapper.findComponent(ConfirmDanger); + const findAlert = () => wrapper.findComponent(GlAlert); afterEach(() => { wrapper.destroy(); @@ -69,66 +98,113 @@ describe('Transfer project form', () => { }); describe('with a selected namespace', () => { - const [selectedItem] = groupNamespaces; + const [selectedItem] = transferLocationsResponsePage1; - beforeEach(() => { + const arrange = async () => { + mockResolvedGetTransferLocations(); createComponent(); - + await showNamespaceSelect(); findNamespaceSelect().vm.$emit('select', selectedItem); - }); + }; + + it('emits the `selectNamespace` event when a namespace is selected', async () => { + await arrange(); - it('emits the `selectNamespace` event when a namespace is selected', () => { const args = [selectedItem.id]; expect(wrapper.emitted('selectNamespace')).toEqual([args]); }); - it('enables the confirm button', () => { + it('enables the confirm button', async () => { + await arrange(); + expect(findConfirmDanger().attributes('disabled')).toBeUndefined(); }); - it('clicking the confirm button emits the `confirm` event', () => { + it('clicking the confirm button emits the `confirm` event', async () => { + await arrange(); + findConfirmDanger().vm.$emit('confirm'); expect(wrapper.emitted('confirm')).toBeDefined(); }); }); - it('passes correct props to `NamespaceSelect` component', async () => { - createComponent(); + describe('when `NamespaceSelect` is opened', () => { + it('fetches user and group namespaces and passes correct props to `NamespaceSelect` component', async () => { + mockResolvedGetTransferLocations(); + createComponent(); + await showNamespaceSelect(); + + const { namespace } = currentUserNamespaceQueryResponse.data.currentUser; + + expect(findNamespaceSelect().props()).toMatchObject({ + userNamespaces: [ + { + id: getIdFromGraphQLId(namespace.id), + humanName: namespace.fullName, + }, + ], + groupNamespaces: transferLocationsResponsePage1.map(({ id, full_name: humanName }) => ({ + id, + humanName, + })), + hasNextPageOfGroups: true, + isLoading: false, + isSearchLoading: false, + shouldFilterNamespaces: false, + }); + }); - runDebounce(); - await waitForPromises(); + describe('when namespaces have already been fetched', () => { + beforeEach(async () => { + mockResolvedGetTransferLocations(); + createComponent(); + await showNamespaceSelect(); + }); + + it('does not fetch namespaces', async () => { + getTransferLocations.mockClear(); + defaultQueryHandler.mockClear(); + + await showNamespaceSelect(); - const { - namespace, - groups, - } = searchNamespacesWhereUserCanTransferProjectsQueryResponsePage1.data.currentUser; - - expect(findNamespaceSelect().props()).toMatchObject({ - userNamespaces: [ - { - id: getIdFromGraphQLId(namespace.id), - humanName: namespace.fullName, - }, - ], - groupNamespaces: groups.nodes.map((node) => ({ - id: getIdFromGraphQLId(node.id), - humanName: node.fullName, - })), - hasNextPageOfGroups: true, - isLoadingMoreGroups: false, - isSearchLoading: false, - shouldFilterNamespaces: false, + expect(getTransferLocations).not.toHaveBeenCalled(); + expect(defaultQueryHandler).not.toHaveBeenCalled(); + }); + }); + + describe('when `getTransferLocations` API call fails', () => { + it('displays error alert', async () => { + mockRejectedGetTransferLocations(); + createComponent(); + await showNamespaceSelect(); + + expect(findAlert().exists()).toBe(true); + }); + }); + + describe('when `currentUser` GraphQL query fails', () => { + it('displays error alert', async () => { + mockResolvedGetTransferLocations(); + const error = new Error(); + createComponent({ + requestHandlers: [[currentUserNamespaceQuery, jest.fn().mockRejectedValueOnce(error)]], + }); + await showNamespaceSelect(); + + expect(findAlert().exists()).toBe(true); + }); }); }); describe('when `search` event is fired', () => { const arrange = async () => { + mockResolvedGetTransferLocations(); createComponent(); - + await showNamespaceSelect(); + mockResolvedGetTransferLocations(); findNamespaceSelect().vm.$emit('search', 'foo'); - await nextTick(); }; @@ -138,87 +214,106 @@ describe('Transfer project form', () => { expect(findNamespaceSelect().props('isSearchLoading')).toBe(true); }); - it('passes `search` variable to query', async () => { + it('passes `search` param to API call', async () => { await arrange(); - runDebounce(); await waitForPromises(); - expect(defaultQueryHandler).toHaveBeenCalledWith(expect.objectContaining({ search: 'foo' })); + expect(getTransferLocations).toHaveBeenCalledWith( + projectId, + expect.objectContaining({ search: 'foo' }), + ); + }); + + describe('when `getTransferLocations` API call fails', () => { + it('displays dismissible error alert', async () => { + mockResolvedGetTransferLocations(); + createComponent(); + await showNamespaceSelect(); + mockRejectedGetTransferLocations(); + findNamespaceSelect().vm.$emit('search', 'foo'); + await waitForPromises(); + + const alert = findAlert(); + + expect(alert.exists()).toBe(true); + + alert.vm.$emit('dismiss'); + await nextTick(); + + expect(alert.exists()).toBe(false); + }); }); }); describe('when `load-more-groups` event is fired', () => { - let queryHandler; - const arrange = async () => { - queryHandler = jest.fn(); - queryHandler.mockResolvedValueOnce( - searchNamespacesWhereUserCanTransferProjectsQueryResponsePage1, - ); - queryHandler.mockResolvedValueOnce( - searchNamespacesWhereUserCanTransferProjectsQueryResponsePage2, - ); + mockResolvedGetTransferLocations(); + createComponent(); + await showNamespaceSelect(); - createComponent({ - requestHandlers: [[searchNamespacesWhereUserCanTransferProjectsQuery, queryHandler]], + mockResolvedGetTransferLocations({ + data: transferLocationsResponsePage2, + page: '2', + nextPage: null, + prevPage: '1', }); - runDebounce(); - await waitForPromises(); - findNamespaceSelect().vm.$emit('load-more-groups'); await nextTick(); }; - it('sets `isLoadingMoreGroups` prop to `true`', async () => { + it('sets `isLoading` prop to `true`', async () => { await arrange(); - expect(findNamespaceSelect().props('isLoadingMoreGroups')).toBe(true); + expect(findNamespaceSelect().props('isLoading')).toBe(true); }); - it('passes `after` and `first` variables to query', async () => { + it('passes `page` param to API call', async () => { await arrange(); - runDebounce(); await waitForPromises(); - expect(queryHandler).toHaveBeenCalledWith( - expect.objectContaining({ - first: 25, - after: - searchNamespacesWhereUserCanTransferProjectsQueryResponsePage1.data.currentUser.groups - .pageInfo.endCursor, - }), + expect(getTransferLocations).toHaveBeenCalledWith( + projectId, + expect.objectContaining({ page: 2 }), ); }); it('updates `groupNamespaces` prop with new groups', async () => { await arrange(); - runDebounce(); await waitForPromises(); - expect(findNamespaceSelect().props('groupNamespaces')).toEqual( - [ - ...searchNamespacesWhereUserCanTransferProjectsQueryResponsePage1.data.currentUser.groups - .nodes, - ...searchNamespacesWhereUserCanTransferProjectsQueryResponsePage2.data.currentUser.groups - .nodes, - ].map((node) => ({ - id: getIdFromGraphQLId(node.id), - humanName: node.fullName, - })), + expect(findNamespaceSelect().props('groupNamespaces')).toMatchObject( + [...transferLocationsResponsePage1, ...transferLocationsResponsePage2].map( + ({ id, full_name: humanName }) => ({ + id, + humanName, + }), + ), ); }); it('updates `hasNextPageOfGroups` prop', async () => { await arrange(); - runDebounce(); await waitForPromises(); expect(findNamespaceSelect().props('hasNextPageOfGroups')).toBe(false); }); + + describe('when `getTransferLocations` API call fails', () => { + it('displays error alert', async () => { + mockResolvedGetTransferLocations(); + createComponent(); + await showNamespaceSelect(); + mockRejectedGetTransferLocations(); + findNamespaceSelect().vm.$emit('load-more-groups'); + await waitForPromises(); + + expect(findAlert().exists()).toBe(true); + }); + }); }); }); diff --git a/spec/frontend/projects/settings/repository/branch_rules/app_spec.js b/spec/frontend/projects/settings/repository/branch_rules/app_spec.js index e920cd48163..4603436c40a 100644 --- a/spec/frontend/projects/settings/repository/branch_rules/app_spec.js +++ b/spec/frontend/projects/settings/repository/branch_rules/app_spec.js @@ -6,8 +6,8 @@ import { mountExtended } from 'helpers/vue_test_utils_helper'; import BranchRules, { i18n } from '~/projects/settings/repository/branch_rules/app.vue'; import BranchRule from '~/projects/settings/repository/branch_rules/components/branch_rule.vue'; import branchRulesQuery from '~/projects/settings/repository/branch_rules/graphql/queries/branch_rules.query.graphql'; -import createFlash from '~/flash'; -import { branchRulesMockResponse, propsDataMock } from './mock_data'; +import { createAlert } from '~/flash'; +import { branchRulesMockResponse, appProvideMock } from './mock_data'; jest.mock('~/flash'); @@ -24,9 +24,7 @@ describe('Branch rules app', () => { wrapper = mountExtended(BranchRules, { apolloProvider: fakeApollo, - propsData: { - ...propsDataMock, - }, + provide: appProvideMock, }); await waitForPromises(); @@ -39,7 +37,7 @@ describe('Branch rules app', () => { it('displays an error if branch rules query fails', async () => { await createComponent({ queryHandler: jest.fn().mockRejectedValue() }); - expect(createFlash).toHaveBeenCalledWith({ message: i18n.queryError }); + expect(createAlert).toHaveBeenCalledWith({ message: i18n.queryError }); }); it('displays an empty state if no branch rules are present', async () => { @@ -49,7 +47,11 @@ describe('Branch rules app', () => { it('renders branch rules', () => { const { nodes } = branchRulesMockResponse.data.project.branchRules; - expect(findAllBranchRules().at(0).text()).toBe(nodes[0].name); - expect(findAllBranchRules().at(1).text()).toBe(nodes[1].name); + + expect(findAllBranchRules().length).toBe(nodes.length); + + expect(findAllBranchRules().at(0).props('name')).toBe(nodes[0].name); + + expect(findAllBranchRules().at(1).props('name')).toBe(nodes[1].name); }); }); diff --git a/spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js b/spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js index 924dab60704..2bc705f538b 100644 --- a/spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js +++ b/spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js @@ -2,26 +2,24 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import BranchRule, { i18n, } from '~/projects/settings/repository/branch_rules/components/branch_rule.vue'; - -const defaultProps = { - name: 'main', - isDefault: true, - isProtected: true, - approvalDetails: ['requires approval from TEST', '2 status checks'], -}; +import { branchRuleProvideMock, branchRulePropsMock } from '../mock_data'; describe('Branch rule', () => { let wrapper; const createComponent = (props = {}) => { - wrapper = shallowMountExtended(BranchRule, { propsData: { ...defaultProps, ...props } }); + wrapper = shallowMountExtended(BranchRule, { + provide: branchRuleProvideMock, + propsData: { ...branchRulePropsMock, ...props }, + }); }; const findDefaultBadge = () => wrapper.findByText(i18n.defaultLabel); const findProtectedBadge = () => wrapper.findByText(i18n.protectedLabel); - const findBranchName = () => wrapper.findByText(defaultProps.name); + const findBranchName = () => wrapper.findByText(branchRulePropsMock.name); const findProtectionDetailsList = () => wrapper.findByRole('list'); const findProtectionDetailsListItems = () => wrapper.findAllByRole('listitem'); + const findDetailsButton = () => wrapper.findByText(i18n.detailsButtonLabel); beforeEach(() => createComponent()); @@ -52,7 +50,17 @@ describe('Branch rule', () => { }); it('renders the protection details list items', () => { - expect(findProtectionDetailsListItems().at(0).text()).toBe(defaultProps.approvalDetails[0]); - expect(findProtectionDetailsListItems().at(1).text()).toBe(defaultProps.approvalDetails[1]); + expect(findProtectionDetailsListItems().at(0).text()).toBe( + branchRulePropsMock.approvalDetails[0], + ); + expect(findProtectionDetailsListItems().at(1).text()).toBe( + branchRulePropsMock.approvalDetails[1], + ); + }); + + it('renders a detail button with the correct href', () => { + expect(findDetailsButton().attributes('href')).toBe( + `${branchRuleProvideMock.branchRulesPath}?branch=${branchRulePropsMock.name}`, + ); }); }); diff --git a/spec/frontend/projects/settings/repository/branch_rules/mock_data.js b/spec/frontend/projects/settings/repository/branch_rules/mock_data.js index 14ed35f047d..bac82992c4d 100644 --- a/spec/frontend/projects/settings/repository/branch_rules/mock_data.js +++ b/spec/frontend/projects/settings/repository/branch_rules/mock_data.js @@ -20,6 +20,17 @@ export const branchRulesMockResponse = { }, }; -export const propsDataMock = { +export const appProvideMock = { projectPath: 'some/project/path', }; + +export const branchRuleProvideMock = { + branchRulesPath: 'settings/repository/branch_rules', +}; + +export const branchRulePropsMock = { + name: 'main', + isDefault: true, + isProtected: true, + approvalDetails: ['requires approval from TEST', '2 status checks'], +}; |