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 @@
+
+
+
+
+
+
+
+
+ {{ content }}
+
+
+
+
+
+
+
+
+ {{ iconTitle }}
+
+
+
+ {{ content }}
+
+
+
+
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);
+ });
+ });
+});