From ee664acb356f8123f4f6b00b73c1e1cf0866c7fb Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Thu, 20 Oct 2022 09:40:42 +0000 Subject: Add latest changes from gitlab-org/gitlab@15-5-stable-ee --- .../settings/branch_rules/branch_dropdown_spec.js | 110 --------- .../components/edit/branch_dropdown_spec.js | 110 +++++++++ .../branch_rules/components/edit/index_spec.js | 108 ++++++++ .../components/edit/protections/index_spec.js | 57 +++++ .../edit/protections/merge_protections_spec.js | 53 ++++ .../edit/protections/push_protections_spec.js | 50 ++++ .../components/protections/index_spec.js | 57 ----- .../protections/merge_protections_spec.js | 53 ---- .../protections/push_protections_spec.js | 50 ---- .../branch_rules/components/view/index_spec.js | 113 +++++++++ .../branch_rules/components/view/mock_data.js | 141 +++++++++++ .../components/view/protection_row_spec.js | 71 ++++++ .../components/view/protection_spec.js | 68 +++++ .../settings/branch_rules/rule_edit_spec.js | 108 -------- .../components/default_branch_selector_spec.js | 46 ++++ .../components/transfer_project_form_spec.js | 273 ++++++++++++++------- .../settings/repository/branch_rules/app_spec.js | 18 +- .../branch_rules/components/branch_rule_spec.js | 30 ++- .../settings/repository/branch_rules/mock_data.js | 13 +- 19 files changed, 1042 insertions(+), 487 deletions(-) delete mode 100644 spec/frontend/projects/settings/branch_rules/branch_dropdown_spec.js create mode 100644 spec/frontend/projects/settings/branch_rules/components/edit/branch_dropdown_spec.js create mode 100644 spec/frontend/projects/settings/branch_rules/components/edit/index_spec.js create mode 100644 spec/frontend/projects/settings/branch_rules/components/edit/protections/index_spec.js create mode 100644 spec/frontend/projects/settings/branch_rules/components/edit/protections/merge_protections_spec.js create mode 100644 spec/frontend/projects/settings/branch_rules/components/edit/protections/push_protections_spec.js delete mode 100644 spec/frontend/projects/settings/branch_rules/components/protections/index_spec.js delete mode 100644 spec/frontend/projects/settings/branch_rules/components/protections/merge_protections_spec.js delete mode 100644 spec/frontend/projects/settings/branch_rules/components/protections/push_protections_spec.js create mode 100644 spec/frontend/projects/settings/branch_rules/components/view/index_spec.js create mode 100644 spec/frontend/projects/settings/branch_rules/components/view/mock_data.js create mode 100644 spec/frontend/projects/settings/branch_rules/components/view/protection_row_spec.js create mode 100644 spec/frontend/projects/settings/branch_rules/components/view/protection_spec.js delete mode 100644 spec/frontend/projects/settings/branch_rules/rule_edit_spec.js create mode 100644 spec/frontend/projects/settings/components/default_branch_selector_spec.js (limited to 'spec/frontend/projects/settings') diff --git a/spec/frontend/projects/settings/branch_rules/branch_dropdown_spec.js b/spec/frontend/projects/settings/branch_rules/branch_dropdown_spec.js deleted file mode 100644 index 79bce5a4b3f..00000000000 --- a/spec/frontend/projects/settings/branch_rules/branch_dropdown_spec.js +++ /dev/null @@ -1,110 +0,0 @@ -import Vue, { nextTick } from 'vue'; -import VueApollo from 'vue-apollo'; -import { GlDropdown, GlSearchBoxByType, GlDropdownItem, GlSprintf } from '@gitlab/ui'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import BranchDropdown, { - i18n, -} from '~/projects/settings/branch_rules/components/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'; -import { createAlert } from '~/flash'; - -Vue.use(VueApollo); -jest.mock('~/flash'); - -describe('Branch dropdown', () => { - let wrapper; - - const projectPath = 'test/project'; - const value = 'main'; - const mockBranchNames = ['test 1', 'test 2']; - - const createComponent = async ({ branchNames = mockBranchNames, resolver } = {}) => { - const mockResolver = - resolver || - jest.fn().mockResolvedValue({ - data: { project: { id: '1', repository: { branchNames } } }, - }); - const apolloProvider = createMockApollo([[branchesQuery, mockResolver]]); - - wrapper = shallowMountExtended(BranchDropdown, { - apolloProvider, - propsData: { projectPath, value }, - }); - - await waitForPromises(); - }; - - const findGlDropdown = () => wrapper.findComponent(GlDropdown); - const findAllBranches = () => wrapper.findAllComponents(GlDropdownItem); - const findNoDataMsg = () => wrapper.findByTestId('no-data'); - const findGlSearchBoxByType = () => wrapper.findComponent(GlSearchBoxByType); - const findWildcardButton = () => wrapper.findByTestId('create-wildcard-button'); - const findHelpText = () => wrapper.findComponent(GlSprintf); - const setSearchTerm = (searchTerm) => findGlSearchBoxByType().vm.$emit('input', searchTerm); - - beforeEach(() => createComponent()); - - afterEach(() => { - wrapper.destroy(); - }); - - it('renders a GlDropdown component with the correct props', () => { - expect(findGlDropdown().props()).toMatchObject({ text: value }); - }); - - it('renders GlDropdownItem components for each branch', () => { - expect(findAllBranches().length).toBe(mockBranchNames.length); - - mockBranchNames.forEach((branchName, index) => - expect(findAllBranches().at(index).text()).toBe(branchName), - ); - }); - - it('emits `select` with the branch name when a branch is clicked', () => { - findAllBranches().at(0).vm.$emit('click'); - expect(wrapper.emitted('input')).toEqual([[mockBranchNames[0]]]); - }); - - describe('branch searching', () => { - it('displays a message if no branches can be found', async () => { - await createComponent({ branchNames: [] }); - - expect(findNoDataMsg().text()).toBe(i18n.noMatch); - }); - - it('displays a loading state while search request is in flight', async () => { - setSearchTerm('test'); - await nextTick(); - - expect(findGlSearchBoxByType().props()).toMatchObject({ isLoading: true }); - }); - - it('renders a wildcard button', async () => { - const searchTerm = 'test-*'; - setSearchTerm(searchTerm); - await nextTick(); - - expect(findWildcardButton().exists()).toBe(true); - findWildcardButton().vm.$emit('click'); - expect(wrapper.emitted('createWildcard')).toEqual([[searchTerm]]); - }); - - it('renders help text', () => { - expect(findHelpText().attributes('message')).toBe(i18n.branchHelpText); - }); - }); - - it('displays an error message if fetch failed', async () => { - const error = new Error('an error occurred'); - const resolver = jest.fn().mockRejectedValueOnce(error); - await createComponent({ resolver }); - - expect(createAlert).toHaveBeenCalledWith({ - message: i18n.fetchBranchesError, - captureError: true, - error, - }); - }); -}); diff --git a/spec/frontend/projects/settings/branch_rules/components/edit/branch_dropdown_spec.js b/spec/frontend/projects/settings/branch_rules/components/edit/branch_dropdown_spec.js new file mode 100644 index 00000000000..11f219c1f90 --- /dev/null +++ b/spec/frontend/projects/settings/branch_rules/components/edit/branch_dropdown_spec.js @@ -0,0 +1,110 @@ +import Vue, { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; +import { GlDropdown, GlSearchBoxByType, GlDropdownItem, GlSprintf } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import BranchDropdown, { + i18n, +} 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'; +import { createAlert } from '~/flash'; + +Vue.use(VueApollo); +jest.mock('~/flash'); + +describe('Branch dropdown', () => { + let wrapper; + + const projectPath = 'test/project'; + const value = 'main'; + const mockBranchNames = ['test 1', 'test 2']; + + const createComponent = async ({ branchNames = mockBranchNames, resolver } = {}) => { + const mockResolver = + resolver || + jest.fn().mockResolvedValue({ + data: { project: { id: '1', repository: { branchNames } } }, + }); + const apolloProvider = createMockApollo([[branchesQuery, mockResolver]]); + + wrapper = shallowMountExtended(BranchDropdown, { + apolloProvider, + propsData: { projectPath, value }, + }); + + await waitForPromises(); + }; + + const findGlDropdown = () => wrapper.findComponent(GlDropdown); + const findAllBranches = () => wrapper.findAllComponents(GlDropdownItem); + const findNoDataMsg = () => wrapper.findByTestId('no-data'); + const findGlSearchBoxByType = () => wrapper.findComponent(GlSearchBoxByType); + const findWildcardButton = () => wrapper.findByTestId('create-wildcard-button'); + const findHelpText = () => wrapper.findComponent(GlSprintf); + const setSearchTerm = (searchTerm) => findGlSearchBoxByType().vm.$emit('input', searchTerm); + + beforeEach(() => createComponent()); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders a GlDropdown component with the correct props', () => { + expect(findGlDropdown().props()).toMatchObject({ text: value }); + }); + + it('renders GlDropdownItem components for each branch', () => { + expect(findAllBranches().length).toBe(mockBranchNames.length); + + mockBranchNames.forEach((branchName, index) => + expect(findAllBranches().at(index).text()).toBe(branchName), + ); + }); + + it('emits `select` with the branch name when a branch is clicked', () => { + findAllBranches().at(0).vm.$emit('click'); + expect(wrapper.emitted('input')).toEqual([[mockBranchNames[0]]]); + }); + + describe('branch searching', () => { + it('displays a message if no branches can be found', async () => { + await createComponent({ branchNames: [] }); + + expect(findNoDataMsg().text()).toBe(i18n.noMatch); + }); + + it('displays a loading state while search request is in flight', async () => { + setSearchTerm('test'); + await nextTick(); + + expect(findGlSearchBoxByType().props()).toMatchObject({ isLoading: true }); + }); + + it('renders a wildcard button', async () => { + const searchTerm = 'test-*'; + setSearchTerm(searchTerm); + await nextTick(); + + expect(findWildcardButton().exists()).toBe(true); + findWildcardButton().vm.$emit('click'); + expect(wrapper.emitted('createWildcard')).toEqual([[searchTerm]]); + }); + + it('renders help text', () => { + expect(findHelpText().attributes('message')).toBe(i18n.branchHelpText); + }); + }); + + it('displays an error message if fetch failed', async () => { + const error = new Error('an error occurred'); + const resolver = jest.fn().mockRejectedValueOnce(error); + await createComponent({ resolver }); + + expect(createAlert).toHaveBeenCalledWith({ + message: i18n.fetchBranchesError, + captureError: true, + error, + }); + }); +}); diff --git a/spec/frontend/projects/settings/branch_rules/components/edit/index_spec.js b/spec/frontend/projects/settings/branch_rules/components/edit/index_spec.js new file mode 100644 index 00000000000..21e63fdb24d --- /dev/null +++ b/spec/frontend/projects/settings/branch_rules/components/edit/index_spec.js @@ -0,0 +1,108 @@ +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/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'), + joinPaths: jest.fn(), + setUrlFragment: jest.fn(), +})); + +describe('Edit branch rule', () => { + let wrapper; + const projectPath = 'test/testing'; + + const createComponent = () => { + wrapper = shallowMountExtended(RuleEdit, { propsData: { projectPath } }); + }; + + const findBranchDropdown = () => wrapper.findComponent(BranchDropdown); + const findProtections = () => wrapper.findComponent(Protections); + + beforeEach(() => createComponent()); + + afterEach(() => { + wrapper.destroy(); + }); + + it('gets the branch param from url', () => { + expect(getParameterByName).toHaveBeenCalledWith('branch'); + }); + + describe('BranchDropdown', () => { + it('renders a BranchDropdown component with the correct props', () => { + expect(findBranchDropdown().props()).toMatchObject({ + projectPath, + value: 'main', + }); + }); + + it('sets the correct value when `input` is emitted', async () => { + const branch = 'test'; + findBranchDropdown().vm.$emit('input', branch); + await nextTick(); + expect(findBranchDropdown().props('value')).toBe(branch); + }); + + it('sets the correct value when `createWildcard` is emitted', async () => { + const wildcard = 'test-*'; + findBranchDropdown().vm.$emit('createWildcard', wildcard); + await nextTick(); + expect(findBranchDropdown().props('value')).toBe(wildcard); + }); + }); + + describe('Protections', () => { + it('renders a Protections component with the correct props', () => { + expect(findProtections().props('protections')).toMatchObject({ + membersAllowedToPush: [], + allowForcePush: false, + membersAllowedToMerge: [], + requireCodeOwnersApproval: false, + }); + }); + + it('updates protections when change-allowed-to-push-members is emitted', async () => { + const membersAllowedToPush = ['test']; + findProtections().vm.$emit('change-allowed-to-push-members', membersAllowedToPush); + await nextTick(); + + expect(findProtections().props('protections')).toEqual( + expect.objectContaining({ membersAllowedToPush }), + ); + }); + + it('updates protections when change-allow-force-push is emitted', async () => { + const allowForcePush = true; + findProtections().vm.$emit('change-allow-force-push', allowForcePush); + await nextTick(); + + expect(findProtections().props('protections')).toEqual( + expect.objectContaining({ allowForcePush }), + ); + }); + + it('updates protections when change-allowed-to-merge-members is emitted', async () => { + const membersAllowedToMerge = ['test']; + findProtections().vm.$emit('change-allowed-to-merge-members', membersAllowedToMerge); + await nextTick(); + + expect(findProtections().props('protections')).toEqual( + expect.objectContaining({ membersAllowedToMerge }), + ); + }); + + it('updates protections when change-require-code-owners-approval is emitted', async () => { + const requireCodeOwnersApproval = true; + findProtections().vm.$emit('change-require-code-owners-approval', requireCodeOwnersApproval); + await nextTick(); + + expect(findProtections().props('protections')).toEqual( + expect.objectContaining({ requireCodeOwnersApproval }), + ); + }); + }); +}); diff --git a/spec/frontend/projects/settings/branch_rules/components/edit/protections/index_spec.js b/spec/frontend/projects/settings/branch_rules/components/edit/protections/index_spec.js new file mode 100644 index 00000000000..ee90ff8318f --- /dev/null +++ b/spec/frontend/projects/settings/branch_rules/components/edit/protections/index_spec.js @@ -0,0 +1,57 @@ +import { nextTick } from 'vue'; +import { GlLink } from '@gitlab/ui'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import Protections, { + i18n, +} 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; + + const createComponent = async () => { + wrapper = mountExtended(Protections, { + propsData: { protections }, + }); + await nextTick(); + }; + + const findHeading = () => wrapper.find('h4'); + const findHelpText = () => wrapper.findByTestId('protections-help-text'); + const findHelpLink = () => wrapper.findComponent(GlLink); + const findPushProtections = () => wrapper.findComponent(PushProtections); + const findMergeProtections = () => wrapper.findComponent(MergeProtections); + + beforeEach(() => createComponent()); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders a heading', () => { + expect(findHeading().text()).toBe(i18n.protections); + }); + + it('renders help text', () => { + expect(findHelpText().text()).toMatchInterpolatedText(i18n.protectionsHelpText); + expect(findHelpLink().attributes('href')).toBe('/help/user/project/protected_branches'); + }); + + it('renders a PushProtections component with correct props', () => { + expect(findPushProtections().props('membersAllowedToPush')).toStrictEqual( + protections.membersAllowedToPush, + ); + expect(findPushProtections().props('allowForcePush')).toBe(protections.allowForcePush); + }); + + it('renders a MergeProtections component with correct props', () => { + expect(findMergeProtections().props('membersAllowedToMerge')).toStrictEqual( + protections.membersAllowedToMerge, + ); + expect(findMergeProtections().props('requireCodeOwnersApproval')).toBe( + protections.requireCodeOwnersApproval, + ); + }); +}); diff --git a/spec/frontend/projects/settings/branch_rules/components/edit/protections/merge_protections_spec.js b/spec/frontend/projects/settings/branch_rules/components/edit/protections/merge_protections_spec.js new file mode 100644 index 00000000000..b5fdc46d600 --- /dev/null +++ b/spec/frontend/projects/settings/branch_rules/components/edit/protections/merge_protections_spec.js @@ -0,0 +1,53 @@ +import { GlFormGroup, GlFormCheckbox } from '@gitlab/ui'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import MergeProtections, { + i18n, +} from '~/projects/settings/branch_rules/components/edit/protections/merge_protections.vue'; +import { membersAllowedToMerge, requireCodeOwnersApproval } from '../../../mock_data'; + +describe('Merge Protections', () => { + let wrapper; + + const propsData = { + membersAllowedToMerge, + requireCodeOwnersApproval, + }; + + const createComponent = () => { + wrapper = mountExtended(MergeProtections, { + propsData, + }); + }; + + const findFormGroup = () => wrapper.findComponent(GlFormGroup); + const findCodeOwnersApprovalCheckbox = () => wrapper.findComponent(GlFormCheckbox); + + beforeEach(() => createComponent()); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders a form group with the correct label', () => { + expect(findFormGroup().text()).toContain(i18n.allowedToMerge); + }); + + describe('Require code owners approval checkbox', () => { + it('renders a checkbox with the correct props', () => { + expect(findCodeOwnersApprovalCheckbox().vm.$attrs.checked).toBe( + propsData.requireCodeOwnersApproval, + ); + }); + + it('renders help text', () => { + expect(findCodeOwnersApprovalCheckbox().text()).toContain(i18n.requireApprovalTitle); + expect(findCodeOwnersApprovalCheckbox().text()).toContain(i18n.requireApprovalHelpText); + }); + + it('emits a change-allow-force-push event when changed', () => { + findCodeOwnersApprovalCheckbox().vm.$emit('change', false); + + expect(wrapper.emitted('change-require-code-owners-approval')[0]).toEqual([false]); + }); + }); +}); diff --git a/spec/frontend/projects/settings/branch_rules/components/edit/protections/push_protections_spec.js b/spec/frontend/projects/settings/branch_rules/components/edit/protections/push_protections_spec.js new file mode 100644 index 00000000000..60bb7a51dcb --- /dev/null +++ b/spec/frontend/projects/settings/branch_rules/components/edit/protections/push_protections_spec.js @@ -0,0 +1,50 @@ +import { GlFormGroup, GlSprintf, GlFormCheckbox } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import PushProtections, { + i18n, +} from '~/projects/settings/branch_rules/components/edit/protections/push_protections.vue'; +import { membersAllowedToPush, allowForcePush } from '../../../mock_data'; + +describe('Push Protections', () => { + let wrapper; + const propsData = { + membersAllowedToPush, + allowForcePush, + }; + + const createComponent = () => { + wrapper = shallowMountExtended(PushProtections, { + propsData, + }); + }; + + const findFormGroup = () => wrapper.findComponent(GlFormGroup); + const findAllowForcePushCheckbox = () => wrapper.findComponent(GlFormCheckbox); + const findHelpText = () => wrapper.findComponent(GlSprintf); + + beforeEach(() => createComponent()); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders a form group with the correct label', () => { + expect(findFormGroup().attributes('label')).toBe(i18n.allowedToPush); + }); + + describe('Allow force push checkbox', () => { + it('renders a checkbox with the correct props', () => { + expect(findAllowForcePushCheckbox().vm.$attrs.checked).toBe(propsData.allowForcePush); + }); + + it('renders help text', () => { + expect(findHelpText().attributes('message')).toBe(i18n.forcePushTitle); + }); + + it('emits a change-allow-force-push event when changed', () => { + findAllowForcePushCheckbox().vm.$emit('change', false); + + expect(wrapper.emitted('change-allow-force-push')[0]).toEqual([false]); + }); + }); +}); diff --git a/spec/frontend/projects/settings/branch_rules/components/protections/index_spec.js b/spec/frontend/projects/settings/branch_rules/components/protections/index_spec.js deleted file mode 100644 index 3592fa50622..00000000000 --- a/spec/frontend/projects/settings/branch_rules/components/protections/index_spec.js +++ /dev/null @@ -1,57 +0,0 @@ -import { nextTick } from 'vue'; -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'; - -describe('Branch Protections', () => { - let wrapper; - - const createComponent = async () => { - wrapper = mountExtended(Protections, { - propsData: { protections }, - }); - await nextTick(); - }; - - const findHeading = () => wrapper.find('h4'); - const findHelpText = () => wrapper.findByTestId('protections-help-text'); - const findHelpLink = () => wrapper.findComponent(GlLink); - const findPushProtections = () => wrapper.findComponent(PushProtections); - const findMergeProtections = () => wrapper.findComponent(MergeProtections); - - beforeEach(() => createComponent()); - - afterEach(() => { - wrapper.destroy(); - }); - - it('renders a heading', () => { - expect(findHeading().text()).toBe(i18n.protections); - }); - - it('renders help text', () => { - expect(findHelpText().text()).toMatchInterpolatedText(i18n.protectionsHelpText); - expect(findHelpLink().attributes('href')).toBe('/help/user/project/protected_branches'); - }); - - it('renders a PushProtections component with correct props', () => { - expect(findPushProtections().props('membersAllowedToPush')).toStrictEqual( - protections.membersAllowedToPush, - ); - expect(findPushProtections().props('allowForcePush')).toBe(protections.allowForcePush); - }); - - it('renders a MergeProtections component with correct props', () => { - expect(findMergeProtections().props('membersAllowedToMerge')).toStrictEqual( - protections.membersAllowedToMerge, - ); - expect(findMergeProtections().props('requireCodeOwnersApproval')).toBe( - protections.requireCodeOwnersApproval, - ); - }); -}); diff --git a/spec/frontend/projects/settings/branch_rules/components/protections/merge_protections_spec.js b/spec/frontend/projects/settings/branch_rules/components/protections/merge_protections_spec.js deleted file mode 100644 index 0e168a2ad78..00000000000 --- a/spec/frontend/projects/settings/branch_rules/components/protections/merge_protections_spec.js +++ /dev/null @@ -1,53 +0,0 @@ -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'; - -describe('Merge Protections', () => { - let wrapper; - - const propsData = { - membersAllowedToMerge, - requireCodeOwnersApproval, - }; - - const createComponent = () => { - wrapper = mountExtended(MergeProtections, { - propsData, - }); - }; - - const findFormGroup = () => wrapper.findComponent(GlFormGroup); - const findCodeOwnersApprovalCheckbox = () => wrapper.findComponent(GlFormCheckbox); - - beforeEach(() => createComponent()); - - afterEach(() => { - wrapper.destroy(); - }); - - it('renders a form group with the correct label', () => { - expect(findFormGroup().text()).toContain(i18n.allowedToMerge); - }); - - describe('Require code owners approval checkbox', () => { - it('renders a checkbox with the correct props', () => { - expect(findCodeOwnersApprovalCheckbox().vm.$attrs.checked).toBe( - propsData.requireCodeOwnersApproval, - ); - }); - - it('renders help text', () => { - expect(findCodeOwnersApprovalCheckbox().text()).toContain(i18n.requireApprovalTitle); - expect(findCodeOwnersApprovalCheckbox().text()).toContain(i18n.requireApprovalHelpText); - }); - - it('emits a change-allow-force-push event when changed', () => { - findCodeOwnersApprovalCheckbox().vm.$emit('change', false); - - expect(wrapper.emitted('change-require-code-owners-approval')[0]).toEqual([false]); - }); - }); -}); diff --git a/spec/frontend/projects/settings/branch_rules/components/protections/push_protections_spec.js b/spec/frontend/projects/settings/branch_rules/components/protections/push_protections_spec.js deleted file mode 100644 index d54dad08338..00000000000 --- a/spec/frontend/projects/settings/branch_rules/components/protections/push_protections_spec.js +++ /dev/null @@ -1,50 +0,0 @@ -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'; - -describe('Push Protections', () => { - let wrapper; - const propsData = { - membersAllowedToPush, - allowForcePush, - }; - - const createComponent = () => { - wrapper = shallowMountExtended(PushProtections, { - propsData, - }); - }; - - const findFormGroup = () => wrapper.findComponent(GlFormGroup); - const findAllowForcePushCheckbox = () => wrapper.findComponent(GlFormCheckbox); - const findHelpText = () => wrapper.findComponent(GlSprintf); - - beforeEach(() => createComponent()); - - afterEach(() => { - wrapper.destroy(); - }); - - it('renders a form group with the correct label', () => { - expect(findFormGroup().attributes('label')).toBe(i18n.allowedToPush); - }); - - describe('Allow force push checkbox', () => { - it('renders a checkbox with the correct props', () => { - expect(findAllowForcePushCheckbox().vm.$attrs.checked).toBe(propsData.allowForcePush); - }); - - it('renders help text', () => { - expect(findHelpText().attributes('message')).toBe(i18n.forcePushTitle); - }); - - it('emits a change-allow-force-push event when changed', () => { - findAllowForcePushCheckbox().vm.$emit('change', false); - - expect(wrapper.emitted('change-allow-force-push')[0]).toEqual([false]); - }); - }); -}); 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/branch_rules/rule_edit_spec.js b/spec/frontend/projects/settings/branch_rules/rule_edit_spec.js deleted file mode 100644 index b0b2b9191d4..00000000000 --- a/spec/frontend/projects/settings/branch_rules/rule_edit_spec.js +++ /dev/null @@ -1,108 +0,0 @@ -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'; - -jest.mock('~/lib/utils/url_utility', () => ({ - getParameterByName: jest.fn().mockImplementation(() => 'main'), - joinPaths: jest.fn(), - setUrlFragment: jest.fn(), -})); - -describe('Edit branch rule', () => { - let wrapper; - const projectPath = 'test/testing'; - - const createComponent = () => { - wrapper = shallowMountExtended(RuleEdit, { propsData: { projectPath } }); - }; - - const findBranchDropdown = () => wrapper.findComponent(BranchDropdown); - const findProtections = () => wrapper.findComponent(Protections); - - beforeEach(() => createComponent()); - - afterEach(() => { - wrapper.destroy(); - }); - - it('gets the branch param from url', () => { - expect(getParameterByName).toHaveBeenCalledWith('branch'); - }); - - describe('BranchDropdown', () => { - it('renders a BranchDropdown component with the correct props', () => { - expect(findBranchDropdown().props()).toMatchObject({ - projectPath, - value: 'main', - }); - }); - - it('sets the correct value when `input` is emitted', async () => { - const branch = 'test'; - findBranchDropdown().vm.$emit('input', branch); - await nextTick(); - expect(findBranchDropdown().props('value')).toBe(branch); - }); - - it('sets the correct value when `createWildcard` is emitted', async () => { - const wildcard = 'test-*'; - findBranchDropdown().vm.$emit('createWildcard', wildcard); - await nextTick(); - expect(findBranchDropdown().props('value')).toBe(wildcard); - }); - }); - - describe('Protections', () => { - it('renders a Protections component with the correct props', () => { - expect(findProtections().props('protections')).toMatchObject({ - membersAllowedToPush: [], - allowForcePush: false, - membersAllowedToMerge: [], - requireCodeOwnersApproval: false, - }); - }); - - it('updates protections when change-allowed-to-push-members is emitted', async () => { - const membersAllowedToPush = ['test']; - findProtections().vm.$emit('change-allowed-to-push-members', membersAllowedToPush); - await nextTick(); - - expect(findProtections().props('protections')).toEqual( - expect.objectContaining({ membersAllowedToPush }), - ); - }); - - it('updates protections when change-allow-force-push is emitted', async () => { - const allowForcePush = true; - findProtections().vm.$emit('change-allow-force-push', allowForcePush); - await nextTick(); - - expect(findProtections().props('protections')).toEqual( - expect.objectContaining({ allowForcePush }), - ); - }); - - it('updates protections when change-allowed-to-merge-members is emitted', async () => { - const membersAllowedToMerge = ['test']; - findProtections().vm.$emit('change-allowed-to-merge-members', membersAllowedToMerge); - await nextTick(); - - expect(findProtections().props('protections')).toEqual( - expect.objectContaining({ membersAllowedToMerge }), - ); - }); - - it('updates protections when change-require-code-owners-approval is emitted', async () => { - const requireCodeOwnersApproval = true; - findProtections().vm.$emit('change-require-code-owners-approval', requireCodeOwnersApproval); - await nextTick(); - - expect(findProtections().props('protections')).toEqual( - expect.objectContaining({ requireCodeOwnersApproval }), - ); - }); - }); -}); 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'], +}; -- cgit v1.2.3