diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/view/constants.js b/app/assets/javascripts/projects/settings/branch_rules/components/view/constants.js index 051e56c94f36d6ee338ce1c2c5011db4c6db80e1..46d577a5750741719e389174f834b96a40877a8b 100644 --- a/app/assets/javascripts/projects/settings/branch_rules/components/view/constants.js +++ b/app/assets/javascripts/projects/settings/branch_rules/components/view/constants.js @@ -27,10 +27,15 @@ export const I18N = { statusChecksHeader: s__('BranchRules|Status checks (%{total})'), allowedToPushHeader: s__('BranchRules|Allowed to push and merge (%{total})'), allowedToMergeHeader: s__('BranchRules|Allowed to merge (%{total})'), + allowForcePushLabel: s__('BranchRules|Allow force push'), allowForcePushTitle: s__('BranchRules|Allows force push'), doesNotAllowForcePushTitle: s__('BranchRules|Does not allow force push'), - forcePushDescription: s__('BranchRules|From users with push access.'), - requiresCodeOwnerApprovalTitle: s__('BranchRules|Requires approval from code owners'), + forcePushIconDescription: s__('BranchRules|From users with push access.'), + forcePushDescriptionWithDocs: s__( + 'BranchRules|Allow all users with push access to %{linkStart}force push%{linkEnd}.', + ), + requiresCodeOwnerApprovalLabel: s__('BranchRules|Require code owner approval'), + requiresCodeOwnerApprovalTitle: s__('BranchRules|Requires code owner approval'), doesNotRequireCodeOwnerApprovalTitle: s__( 'BranchRules|Does not require approval from code owners', ), @@ -40,6 +45,9 @@ export const I18N = { doesNotRequireCodeOwnerApprovalDescription: s__( 'BranchRules|Also accepts code pushes that change files listed in CODEOWNERS file.', ), + codeOwnerApprovalDescription: s__( + 'BranchRules|Changed files listed in %{linkStart}CODEOWNERS%{linkEnd} require an approval for merge requests and will be rejected for code pushes.', + ), noData: s__('BranchRules|No data to display'), deleteRuleModalTitle: s__('BranchRules|Delete branch rule?'), deleteRuleModalText: s__( @@ -70,6 +78,10 @@ export const APPROVALS_HELP_PATH = 'user/project/merge_requests/approvals/index. export const STATUS_CHECKS_HELP_PATH = 'user/project/merge_requests/status_checks.md'; +export const CODE_OWNERS_HELP_PATH = 'user/project/code_owners.md'; + +export const PUSH_RULES_HELP_PATH = 'user/project/repository/push_rules.md'; + export const REQUIRED_ICON = 'check-circle-filled'; export const NOT_REQUIRED_ICON = 'status-failed'; diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue b/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue index 3cdb3f5c7d4b43e0294504ed3164dae1aeb448c2..6371318e4d66af10ffd2aa214c1daf0b5426d9e3 100644 --- a/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue +++ b/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue @@ -5,7 +5,6 @@ import { GlSprintf, GlLink, GlLoadingIcon, - GlIcon, GlCard, GlButton, GlModal, @@ -28,36 +27,39 @@ import { getAccessLevels } from '../../../utils'; import BranchRuleModal from '../../../components/branch_rule_modal.vue'; import Protection from './protection.vue'; import RuleDrawer from './rule_drawer.vue'; +import ProtectionToggle from './protection_toggle.vue'; import { I18N, ALL_BRANCHES_WILDCARD, BRANCH_PARAM_NAME, PROTECTED_BRANCHES_HELP_PATH, - REQUIRED_ICON, - NOT_REQUIRED_ICON, - REQUIRED_ICON_CLASS, - NOT_REQUIRED_ICON_CLASS, + CODE_OWNERS_HELP_PATH, + PUSH_RULES_HELP_PATH, DELETE_RULE_MODAL_ID, EDIT_RULE_MODAL_ID, } from './constants'; const protectedBranchesHelpDocLink = helpPagePath(PROTECTED_BRANCHES_HELP_PATH); +const codeOwnersHelpDocLink = helpPagePath(CODE_OWNERS_HELP_PATH); +const pushRulesHelpDocLink = helpPagePath(PUSH_RULES_HELP_PATH); export default { name: 'RuleView', i18n: I18N, deleteModalId: DELETE_RULE_MODAL_ID, protectedBranchesHelpDocLink, + codeOwnersHelpDocLink, + pushRulesHelpDocLink, directives: { GlModal: GlModalDirective, }, editModalId: EDIT_RULE_MODAL_ID, components: { Protection, + ProtectionToggle, GlSprintf, GlLink, GlLoadingIcon, - GlIcon, GlCard, GlModal, GlButton, @@ -124,26 +126,37 @@ export default { computed: { forcePushAttributes() { const { allowForcePush } = this.branchProtection || {}; - const icon = allowForcePush ? REQUIRED_ICON : NOT_REQUIRED_ICON; - const iconClass = allowForcePush ? REQUIRED_ICON_CLASS : NOT_REQUIRED_ICON_CLASS; const title = allowForcePush ? this.$options.i18n.allowForcePushTitle : this.$options.i18n.doesNotAllowForcePushTitle; - return { icon, iconClass, title }; + if (!this.glFeatures.editBranchRules) { + return { title, description: this.$options.i18n.forcePushIconDescription }; + } + + return { + title, + description: this.$options.i18n.forcePushDescriptionWithDocs, + }; }, codeOwnersApprovalAttributes() { const { codeOwnerApprovalRequired } = this.branchProtection || {}; - const icon = codeOwnerApprovalRequired ? REQUIRED_ICON : NOT_REQUIRED_ICON; - const iconClass = codeOwnerApprovalRequired ? REQUIRED_ICON_CLASS : NOT_REQUIRED_ICON_CLASS; const title = codeOwnerApprovalRequired ? this.$options.i18n.requiresCodeOwnerApprovalTitle : this.$options.i18n.doesNotRequireCodeOwnerApprovalTitle; - const description = codeOwnerApprovalRequired - ? this.$options.i18n.requiresCodeOwnerApprovalDescription - : this.$options.i18n.doesNotRequireCodeOwnerApprovalDescription; - return { icon, iconClass, title, description }; + if (!this.glFeatures.editBranchRules) { + const description = codeOwnerApprovalRequired + ? this.$options.i18n.requiresCodeOwnerApprovalDescription + : this.$options.i18n.doesNotRequireCodeOwnerApprovalDescription; + + return { title, description }; + } + + return { + title, + description: this.$options.i18n.codeOwnerApprovalDescription, + }; }, mergeAccessLevels() { const { mergeAccessLevels } = this.branchProtection || {}; @@ -353,32 +366,26 @@ export default { /> -
- - {{ forcePushAttributes.title }} -
- -
{{ $options.i18n.forcePushDescription }}
+
-
- - {{ codeOwnersApprovalAttributes.title }} -
- -
{{ codeOwnersApprovalAttributes.description }}
+
diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/view/protection_toggle.vue b/app/assets/javascripts/projects/settings/branch_rules/components/view/protection_toggle.vue new file mode 100644 index 0000000000000000000000000000000000000000..86b133c8bf1759a9d41c3815a8d215b8fd84181b --- /dev/null +++ b/app/assets/javascripts/projects/settings/branch_rules/components/view/protection_toggle.vue @@ -0,0 +1,97 @@ + + + diff --git a/ee/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js b/ee/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js index a9e17b5537e52fddf06cd6d3d9792da5287f8319..62feb0fd649f3f68ee07c7314bf32aae338c1eae 100644 --- a/ee/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js +++ b/ee/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js @@ -14,14 +14,9 @@ import deleteBranchRuleMutation from '~/projects/settings/branch_rules/mutations import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import { - I18N, - REQUIRED_ICON, - NOT_REQUIRED_ICON, - REQUIRED_ICON_CLASS, - NOT_REQUIRED_ICON_CLASS, -} from '~/projects/settings/branch_rules/components/view/constants'; +import { I18N } from '~/projects/settings/branch_rules/components/view/constants'; import Protection from '~/projects/settings/branch_rules/components/view/protection.vue'; +import ProtectionToggle from '~/projects/settings/branch_rules/components/view/protection_toggle.vue'; import { sprintf } from '~/locale'; import { deleteBranchRuleMockResponse, @@ -60,6 +55,7 @@ describe('View branch rules in enterprise edition', () => { jest.fn().mockResolvedValue(response); const createComponent = async ( + glFeatures = { editBranchRules: true }, { showApprovers, showStatusChecks, showCodeOwners } = {}, mockResponse, mutationMockResponse, @@ -85,6 +81,7 @@ describe('View branch rules in enterprise edition', () => { showApprovers, showStatusChecks, showCodeOwners, + glFeatures, }, }); @@ -99,9 +96,7 @@ describe('View branch rules in enterprise edition', () => { const findApprovalsApp = () => wrapper.findComponent(ApprovalRulesApp); const findProjectRules = () => wrapper.findComponent(ProjectRules); const findStatusChecksTitle = () => wrapper.findByText(I18N.statusChecksTitle); - const findCodeOwnerApprovalIcon = () => wrapper.findByTestId('code-owners-icon'); - const findCodeOwnerApprovalTitle = (title) => wrapper.findByText(title); - const findCodeOwnerApprovalDescription = (description) => wrapper.findByText(description); + const findProtectionToggles = () => wrapper.findAllComponents(ProtectionToggle); it('renders a branch protection component for push rules', () => { expect(findBranchProtections().at(0).props()).toMatchObject({ @@ -119,28 +114,22 @@ describe('View branch rules in enterprise edition', () => { describe('Code owner approvals', () => { it('does not render a code owner approval section by default', () => { - expect(findCodeOwnerApprovalIcon().exists()).toBe(false); - expect(findCodeOwnerApprovalTitle(I18N.requiresCodeOwnerApprovalTitle).exists()).toBe(false); - expect( - findCodeOwnerApprovalDescription(I18N.requiresCodeOwnerApprovalDescription).exists(), - ).toBe(false); + expect(findProtectionToggles().length).toBe(1); }); it.each` - codeOwnerApprovalRequired | iconName | iconClass | title | description - ${true} | ${REQUIRED_ICON} | ${REQUIRED_ICON_CLASS} | ${I18N.requiresCodeOwnerApprovalTitle} | ${I18N.requiresCodeOwnerApprovalDescription} - ${false} | ${NOT_REQUIRED_ICON} | ${NOT_REQUIRED_ICON_CLASS} | ${I18N.doesNotRequireCodeOwnerApprovalTitle} | ${I18N.doesNotRequireCodeOwnerApprovalDescription} + codeOwnerApprovalRequired | iconTitle | description + ${true} | ${I18N.requiresCodeOwnerApprovalTitle} | ${I18N.codeOwnerApprovalDescription} + ${false} | ${I18N.doesNotRequireCodeOwnerApprovalTitle} | ${I18N.codeOwnerApprovalDescription} `( - 'code owners with the correct icon, title and description', - async ({ codeOwnerApprovalRequired, iconName, iconClass, title, description }) => { + 'renders code owners approval section with the correct iconTitle and description', + async ({ codeOwnerApprovalRequired, iconTitle, description }) => { const mockResponse = branchProtectionsMockResponse; mockResponse.data.project.branchRules.nodes[0].branchProtection.codeOwnerApprovalRequired = codeOwnerApprovalRequired; - await createComponent({ showCodeOwners: true }, mockResponse); + await createComponent({ editBranchRules: true }, { showCodeOwners: true }, mockResponse); - expect(findCodeOwnerApprovalIcon().props('name')).toBe(iconName); - expect(findCodeOwnerApprovalIcon().attributes('class')).toBe(iconClass); - expect(findCodeOwnerApprovalTitle(title).exists()).toBe(true); - expect(findCodeOwnerApprovalTitle(description).exists()).toBe(true); + expect(findProtectionToggles().at(1).props('iconTitle')).toEqual(iconTitle); + expect(findProtectionToggles().at(1).props('description')).toEqual(description); }, ); }); @@ -151,7 +140,7 @@ describe('View branch rules in enterprise edition', () => { }); describe('if "showApprovers" is true', () => { - beforeEach(() => createComponent({ showApprovers: true })); + beforeEach(() => createComponent({}, { showApprovers: true })); it('sets an approval rules filter', () => { expect(store.modules.approvals.actions.setRulesFilter).toHaveBeenCalledWith( @@ -182,7 +171,7 @@ describe('View branch rules in enterprise edition', () => { }); it('renders a branch protection component for status checks if "showStatusChecks" is true', async () => { - await createComponent({ showStatusChecks: true }); + await createComponent({}, { showStatusChecks: true }); expect(findStatusChecksTitle().exists()).toBe(true); @@ -193,4 +182,22 @@ describe('View branch rules in enterprise edition', () => { statusChecks: statusChecksRulesMock, }); }); + + describe('When edit_branch_rules feature flag is disabled', () => { + it.each` + codeOwnerApprovalRequired | title | description + ${true} | ${I18N.requiresCodeOwnerApprovalTitle} | ${I18N.requiresCodeOwnerApprovalDescription} + ${false} | ${I18N.doesNotRequireCodeOwnerApprovalTitle} | ${I18N.doesNotRequireCodeOwnerApprovalDescription} + `( + 'renders code owners approval section with the correct title and description', + async ({ codeOwnerApprovalRequired, title, description }) => { + const mockResponse = branchProtectionsMockResponse; + mockResponse.data.project.branchRules.nodes[0].branchProtection.codeOwnerApprovalRequired = codeOwnerApprovalRequired; + await createComponent({ editBranchRules: false }, { showCodeOwners: true }, mockResponse); + + expect(findProtectionToggles().at(1).props('iconTitle')).toEqual(title); + expect(findProtectionToggles().at(1).props('description')).toEqual(description); + }, + ); + }); }); diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 29cc5a54a01d25c8c95c9f4df652abf2cf5548fd..862dfc6cee1a3d25fd99f9fda96c1ff337bd8963 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -9023,6 +9023,9 @@ msgstr "" msgid "BranchRules|Allow all users with push access to %{linkStart}force push%{linkEnd}." msgstr "" +msgid "BranchRules|Allow force push" +msgstr "" + msgid "BranchRules|Allowed to force push" msgstr "" @@ -9074,6 +9077,9 @@ msgstr "" msgid "BranchRules|Cancel" msgstr "" +msgid "BranchRules|Changed files listed in %{linkStart}CODEOWNERS%{linkEnd} require an approval for merge requests and will be rejected for code pushes." +msgstr "" + msgid "BranchRules|Changes require a merge request. The following users can push and merge directly." msgstr "" @@ -9161,10 +9167,13 @@ msgstr "" msgid "BranchRules|Require approval from code owners." msgstr "" +msgid "BranchRules|Require code owner approval" +msgstr "" + msgid "BranchRules|Requires CODEOWNERS approval" msgstr "" -msgid "BranchRules|Requires approval from code owners" +msgid "BranchRules|Requires code owner approval" msgstr "" msgid "BranchRules|Roles" 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 index 84ebeb3de046aafe822bbfe38b55d4b8d1313bf1..e3e2973381fbe473644dcf55fc2f62d6dc63a3ee 100644 --- a/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js +++ b/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js @@ -13,16 +13,13 @@ import RuleView from '~/projects/settings/branch_rules/components/view/index.vue import RuleDrawer from '~/projects/settings/branch_rules/components/view/rule_drawer.vue'; import { useMockLocationHelper } from 'helpers/mock_window_location_helper'; import Protection from '~/projects/settings/branch_rules/components/view/protection.vue'; +import ProtectionToggle from '~/projects/settings/branch_rules/components/view/protection_toggle.vue'; import BranchRuleModal from '~/projects/settings/components/branch_rule_modal.vue'; import getProtectableBranches from '~/projects/settings/graphql/queries/protectable_branches.query.graphql'; import { I18N, ALL_BRANCHES_WILDCARD, - REQUIRED_ICON, - NOT_REQUIRED_ICON, - REQUIRED_ICON_CLASS, - NOT_REQUIRED_ICON_CLASS, DELETE_RULE_MODAL_ID, EDIT_RULE_MODAL_ID, } from '~/projects/settings/branch_rules/components/view/constants'; @@ -81,12 +78,12 @@ describe('View branch rules', () => { const errorHandler = jest.fn().mockRejectedValue('error'); const toastMock = { show: jest.fn() }; - const createComponent = async ( + const createComponent = async ({ glFeatures = { editBranchRules: true }, branchRulesQueryHandler = branchRulesMockRequestHandler, deleteMutationHandler = deleteBranchRuleSuccessHandler, editMutationHandler = editBranchRuleSuccessHandler, - ) => { + } = {}) => { fakeApollo = createMockApollo([ [branchRulesQuery, branchRulesQueryHandler], [getProtectableBranches, protectableBranchesMockRequestHandler], @@ -96,9 +93,15 @@ describe('View branch rules', () => { wrapper = shallowMountExtended(RuleView, { apolloProvider: fakeApollo, - provide: { projectPath, protectedBranchesPath, branchRulesPath, glFeatures }, + provide: { + projectPath, + protectedBranchesPath, + branchRulesPath, + glFeatures, + }, stubs: { Protection, + ProtectionToggle, BranchRuleModal, RuleDrawer, GlCard: stubComponent(GlCard, { template: RENDER_ALL_SLOTS_TEMPLATE }), @@ -119,9 +122,7 @@ describe('View branch rules', () => { const findAllBranches = () => wrapper.findByTestId('all-branches'); const findBranchProtectionTitle = () => wrapper.findByText(I18N.protectBranchTitle); const findBranchProtections = () => wrapper.findAllComponents(Protection); - const findForcePushIcon = () => wrapper.findByTestId('force-push-icon'); - const findForcePushTitle = (title) => wrapper.findByText(title); - const findForcePushDescription = () => wrapper.findByText(I18N.forcePushDescription); + const findProtectionToggles = () => wrapper.findAllComponents(ProtectionToggle); const findApprovalsTitle = () => wrapper.findByText(I18N.approvalsTitle); const findpageTitle = () => wrapper.findByText(I18N.pageTitle); const findStatusChecksTitle = () => wrapper.findByText(I18N.statusChecksTitle); @@ -192,20 +193,21 @@ describe('View branch rules', () => { }); it.each` - allowForcePush | iconName | iconClass | title - ${true} | ${REQUIRED_ICON} | ${REQUIRED_ICON_CLASS} | ${I18N.allowForcePushTitle} - ${false} | ${NOT_REQUIRED_ICON} | ${NOT_REQUIRED_ICON_CLASS} | ${I18N.doesNotAllowForcePushTitle} + allowForcePush | iconTitle | description + ${true} | ${I18N.allowForcePushTitle} | ${I18N.forcePushDescriptionWithDocs} + ${false} | ${I18N.doesNotAllowForcePushTitle} | ${I18N.forcePushDescriptionWithDocs} `( - 'renders force push section with the correct icon, title and description', - async ({ allowForcePush, iconName, iconClass, title }) => { + 'renders force push section with the correct title and description', + async ({ allowForcePush, iconTitle, description }) => { const mockResponse = branchProtectionsMockResponse; mockResponse.data.project.branchRules.nodes[0].branchProtection.allowForcePush = allowForcePush; - await createComponent(mockResponse); + await createComponent({ + glFeatures: { editBranchRules: true }, + branchRulesQueryHandler: jest.fn().mockResolvedValue(mockResponse), + }); - expect(findForcePushIcon().props('name')).toBe(iconName); - expect(findForcePushIcon().attributes('class')).toBe(iconClass); - expect(findForcePushTitle(title).exists()).toBe(true); - expect(findForcePushDescription().exists()).toBe(true); + expect(findProtectionToggles().at(0).props('iconTitle')).toEqual(iconTitle); + expect(findProtectionToggles().at(0).props('description')).toEqual(description); }, ); @@ -238,6 +240,10 @@ describe('View branch rules', () => { }); describe('Editing branch rule', () => { + beforeEach(async () => { + await createComponent(); + }); + it('renders edit branch rule button', () => { expect(findEditRuleNameButton().text()).toBe('Edit'); }); @@ -277,6 +283,10 @@ describe('View branch rules', () => { '/project/Project/-/settings/repository/branch_rules?branch=main', ); }); + + it('renders force push section with the correct toggle label and description', () => { + expect(findProtectionToggles().at(0).props('label')).toEqual('Allow force push'); + }); }); describe('Deleting branch rule', () => { @@ -314,7 +324,11 @@ describe('View branch rules', () => { }); it('if error happens it shows an alert', async () => { - await createComponent({ editBranchRules: true }, branchRulesMockRequestHandler, errorHandler); + await createComponent({ + glFeatures: { editBranchRules: true }, + branchRulesQueryHandler: branchRulesMockRequestHandler, + deleteMutationHandler: errorHandler, + }); findDeleteRuleModal().vm.$emit('ok'); await nextTick(); await waitForPromises(); @@ -332,7 +346,10 @@ describe('View branch rules', () => { beforeEach(async () => { jest.spyOn(util, 'getParameterByName').mockReturnValueOnce('All branches'); - await createComponent({ editBranchRules: true }, predefinedBranchRulesMockRequestHandler); + await createComponent({ + glFeatures: { editBranchRules: true }, + branchRulesQueryHandler: predefinedBranchRulesMockRequestHandler, + }); }); it('renders the correct branch rule title', () => { @@ -381,7 +398,7 @@ describe('View branch rules', () => { describe('When rendered for a non-existing rule', () => { beforeEach(async () => { jest.spyOn(util, 'getParameterByName').mockReturnValueOnce('non-existing-rule'); - await createComponent({ editBranchRules: true }); + await createComponent({ glFeatures: { editBranchRules: true } }); }); it('shows empty state', () => { @@ -389,8 +406,9 @@ describe('View branch rules', () => { }); }); - describe('When add_branch_rules feature flag is disabled', () => { - beforeEach(() => createComponent({ editBranchRules: false })); + describe('When edit_branch_rules feature flag is disabled', () => { + beforeEach(() => createComponent({ glFeatures: { editBranchRules: false } })); + it('does not render delete rule button and modal', () => { expect(findDeleteRuleButton().exists()).toBe(false); expect(findDeleteRuleModal().exists()).toBe(false); @@ -400,5 +418,25 @@ describe('View branch rules', () => { expect(findEditRuleNameButton().exists()).toBe(false); expect(findBranchRuleModal().exists()).toBe(false); }); + + it.each` + allowForcePush | title | description + ${true} | ${I18N.allowForcePushTitle} | ${I18N.forcePushIconDescription} + ${false} | ${I18N.doesNotAllowForcePushTitle} | ${I18N.forcePushIconDescription} + `( + 'renders force push section with the correct title and description, when rule is `$allowForcePush`', + async ({ allowForcePush, title, description }) => { + const mockResponse = branchProtectionsMockResponse; + mockResponse.data.project.branchRules.nodes[0].branchProtection.allowForcePush = allowForcePush; + + await createComponent({ + glFeatures: { editBranchRules: false }, + branchRulesQueryHandler: jest.fn().mockResolvedValue(mockResponse), + }); + + expect(findProtectionToggles().at(0).props('iconTitle')).toEqual(title); + expect(findProtectionToggles().at(0).props('description')).toEqual(description); + }, + ); }); }); diff --git a/spec/frontend/projects/settings/branch_rules/components/view/protection_toggle_spec.js b/spec/frontend/projects/settings/branch_rules/components/view/protection_toggle_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..596c19db6e29b114849877b680028a7005261e60 --- /dev/null +++ b/spec/frontend/projects/settings/branch_rules/components/view/protection_toggle_spec.js @@ -0,0 +1,74 @@ +import { GlToggle, GlIcon, GlSprintf, GlLink } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import ProtectionToggle from '~/projects/settings/branch_rules/components/view/protection_toggle.vue'; + +describe('ProtectionToggle', () => { + let wrapper; + + const createComponent = ({ + props = {}, + provided = {}, + glFeatures = { editBranchRules: true }, + } = {}) => { + wrapper = shallowMountExtended(ProtectionToggle, { + stubs: { + GlToggle, + GlIcon, + GlLink, + GlSprintf, + }, + provide: { + glFeatures, + ...provided, + }, + propsData: { + dataTestId: 'force-push', + label: 'Force Push', + iconTitle: 'icon title', + isProtected: false, + ...props, + }, + }); + }; + + const findToggle = () => wrapper.findComponent(GlToggle); + const findIcon = () => wrapper.findByTestId('force-push-icon'); + + describe('when user can edit', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders the toggle', () => { + expect(findToggle().exists()).toBe(true); + }); + + it('does not render the protection icon', () => { + expect(findIcon().exists()).toBe(false); + }); + + it('does not render the toggle description when not provided', () => { + expect(wrapper.findComponent(GlSprintf).exists()).toBe(false); + }); + + it('renders the toggle description, when protection is on', () => { + createComponent({ props: { isProtected: true, description: 'Some description' } }); + + expect(wrapper.findComponent(GlSprintf).exists()).toBe(true); + }); + }); + + describe('when glFeatures.editBranchRules is false', () => { + beforeEach(() => { + createComponent({ glFeatures: { editBranchRules: false } }); + }); + + it('does not render the toggle even for users with edit privileges', () => { + expect(findToggle().exists()).toBe(false); + }); + + it('does not render the toggle description when not provided', () => { + expect(wrapper.findComponent(GlSprintf).exists()).toBe(false); + }); + }); +});