Skip to content
代码片段 群组 项目
未验证 提交 ac591cf1 编辑于 作者: Jacques Erasmus's avatar Jacques Erasmus 提交者: GitLab
浏览文件

Merge branch '438970-edit-branch-rules-branch-protection-implement-toggles-2' into 'master'

No related branches found
No related tags found
无相关合并请求
...@@ -27,10 +27,15 @@ export const I18N = { ...@@ -27,10 +27,15 @@ export const I18N = {
statusChecksHeader: s__('BranchRules|Status checks (%{total})'), statusChecksHeader: s__('BranchRules|Status checks (%{total})'),
allowedToPushHeader: s__('BranchRules|Allowed to push and merge (%{total})'), allowedToPushHeader: s__('BranchRules|Allowed to push and merge (%{total})'),
allowedToMergeHeader: s__('BranchRules|Allowed to merge (%{total})'), allowedToMergeHeader: s__('BranchRules|Allowed to merge (%{total})'),
allowForcePushLabel: s__('BranchRules|Allow force push'),
allowForcePushTitle: s__('BranchRules|Allows force push'), allowForcePushTitle: s__('BranchRules|Allows force push'),
doesNotAllowForcePushTitle: s__('BranchRules|Does not allow force push'), doesNotAllowForcePushTitle: s__('BranchRules|Does not allow force push'),
forcePushDescription: s__('BranchRules|From users with push access.'), forcePushIconDescription: s__('BranchRules|From users with push access.'),
requiresCodeOwnerApprovalTitle: s__('BranchRules|Requires approval from code owners'), 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__( doesNotRequireCodeOwnerApprovalTitle: s__(
'BranchRules|Does not require approval from code owners', 'BranchRules|Does not require approval from code owners',
), ),
...@@ -40,6 +45,9 @@ export const I18N = { ...@@ -40,6 +45,9 @@ export const I18N = {
doesNotRequireCodeOwnerApprovalDescription: s__( doesNotRequireCodeOwnerApprovalDescription: s__(
'BranchRules|Also accepts code pushes that change files listed in CODEOWNERS file.', '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'), noData: s__('BranchRules|No data to display'),
deleteRuleModalTitle: s__('BranchRules|Delete branch rule?'), deleteRuleModalTitle: s__('BranchRules|Delete branch rule?'),
deleteRuleModalText: s__( deleteRuleModalText: s__(
...@@ -70,6 +78,10 @@ export const APPROVALS_HELP_PATH = 'user/project/merge_requests/approvals/index. ...@@ -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 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 REQUIRED_ICON = 'check-circle-filled';
export const NOT_REQUIRED_ICON = 'status-failed'; export const NOT_REQUIRED_ICON = 'status-failed';
......
...@@ -5,7 +5,6 @@ import { ...@@ -5,7 +5,6 @@ import {
GlSprintf, GlSprintf,
GlLink, GlLink,
GlLoadingIcon, GlLoadingIcon,
GlIcon,
GlCard, GlCard,
GlButton, GlButton,
GlModal, GlModal,
...@@ -28,36 +27,39 @@ import { getAccessLevels } from '../../../utils'; ...@@ -28,36 +27,39 @@ import { getAccessLevels } from '../../../utils';
import BranchRuleModal from '../../../components/branch_rule_modal.vue'; import BranchRuleModal from '../../../components/branch_rule_modal.vue';
import Protection from './protection.vue'; import Protection from './protection.vue';
import RuleDrawer from './rule_drawer.vue'; import RuleDrawer from './rule_drawer.vue';
import ProtectionToggle from './protection_toggle.vue';
import { import {
I18N, I18N,
ALL_BRANCHES_WILDCARD, ALL_BRANCHES_WILDCARD,
BRANCH_PARAM_NAME, BRANCH_PARAM_NAME,
PROTECTED_BRANCHES_HELP_PATH, PROTECTED_BRANCHES_HELP_PATH,
REQUIRED_ICON, CODE_OWNERS_HELP_PATH,
NOT_REQUIRED_ICON, PUSH_RULES_HELP_PATH,
REQUIRED_ICON_CLASS,
NOT_REQUIRED_ICON_CLASS,
DELETE_RULE_MODAL_ID, DELETE_RULE_MODAL_ID,
EDIT_RULE_MODAL_ID, EDIT_RULE_MODAL_ID,
} from './constants'; } from './constants';
const protectedBranchesHelpDocLink = helpPagePath(PROTECTED_BRANCHES_HELP_PATH); const protectedBranchesHelpDocLink = helpPagePath(PROTECTED_BRANCHES_HELP_PATH);
const codeOwnersHelpDocLink = helpPagePath(CODE_OWNERS_HELP_PATH);
const pushRulesHelpDocLink = helpPagePath(PUSH_RULES_HELP_PATH);
export default { export default {
name: 'RuleView', name: 'RuleView',
i18n: I18N, i18n: I18N,
deleteModalId: DELETE_RULE_MODAL_ID, deleteModalId: DELETE_RULE_MODAL_ID,
protectedBranchesHelpDocLink, protectedBranchesHelpDocLink,
codeOwnersHelpDocLink,
pushRulesHelpDocLink,
directives: { directives: {
GlModal: GlModalDirective, GlModal: GlModalDirective,
}, },
editModalId: EDIT_RULE_MODAL_ID, editModalId: EDIT_RULE_MODAL_ID,
components: { components: {
Protection, Protection,
ProtectionToggle,
GlSprintf, GlSprintf,
GlLink, GlLink,
GlLoadingIcon, GlLoadingIcon,
GlIcon,
GlCard, GlCard,
GlModal, GlModal,
GlButton, GlButton,
...@@ -124,26 +126,37 @@ export default { ...@@ -124,26 +126,37 @@ export default {
computed: { computed: {
forcePushAttributes() { forcePushAttributes() {
const { allowForcePush } = this.branchProtection || {}; 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 const title = allowForcePush
? this.$options.i18n.allowForcePushTitle ? this.$options.i18n.allowForcePushTitle
: this.$options.i18n.doesNotAllowForcePushTitle; : 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() { codeOwnersApprovalAttributes() {
const { codeOwnerApprovalRequired } = this.branchProtection || {}; 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 const title = codeOwnerApprovalRequired
? this.$options.i18n.requiresCodeOwnerApprovalTitle ? this.$options.i18n.requiresCodeOwnerApprovalTitle
: this.$options.i18n.doesNotRequireCodeOwnerApprovalTitle; : 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() { mergeAccessLevels() {
const { mergeAccessLevels } = this.branchProtection || {}; const { mergeAccessLevels } = this.branchProtection || {};
...@@ -353,32 +366,26 @@ export default { ...@@ -353,32 +366,26 @@ export default {
/> />
<!-- Force push --> <!-- Force push -->
<div class="gl-display-flex gl-align-items-center"> <protection-toggle
<gl-icon data-test-id="force-push"
:size="14" :is-protected="branchProtection.allowForcePush"
data-testid="force-push-icon" :label="$options.i18n.allowForcePushLabel"
:name="forcePushAttributes.icon" :icon-title="forcePushAttributes.title"
:class="forcePushAttributes.iconClass" :description="forcePushAttributes.description"
/> :description-link="$options.pushRulesHelpDocLink"
<strong class="gl-ml-2">{{ forcePushAttributes.title }}</strong> />
</div>
<div class="gl-text-secondary gl-mb-2">{{ $options.i18n.forcePushDescription }}</div>
<!-- EE start --> <!-- EE start -->
<!-- Code Owners --> <!-- Code Owners -->
<div v-if="showCodeOwners"> <div v-if="showCodeOwners">
<div class="gl-display-flex gl-align-items-center"> <protection-toggle
<gl-icon data-test-id="code-owners"
data-testid="code-owners-icon" :is-protected="branchProtection.codeOwnerApprovalRequired"
:size="14" :label="$options.i18n.requiresCodeOwnerApprovalLabel"
:name="codeOwnersApprovalAttributes.icon" :icon-title="codeOwnersApprovalAttributes.title"
:class="codeOwnersApprovalAttributes.iconClass" :description="codeOwnersApprovalAttributes.description"
/> :description-link="$options.codeOwnersHelpDocLink"
<strong class="gl-ml-2">{{ codeOwnersApprovalAttributes.title }}</strong> />
</div>
<div class="gl-text-secondary">{{ codeOwnersApprovalAttributes.description }}</div>
</div> </div>
</section> </section>
......
<script>
import { GlToggle, GlIcon, GlSprintf, GlLink } from '@gitlab/ui';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
REQUIRED_ICON,
NOT_REQUIRED_ICON,
REQUIRED_ICON_CLASS,
NOT_REQUIRED_ICON_CLASS,
} from './constants';
export default {
components: {
GlToggle,
GlIcon,
GlSprintf,
GlLink,
},
mixins: [glFeatureFlagsMixin()],
props: {
dataTestId: {
type: String,
required: true,
},
label: {
type: String,
required: true,
},
description: {
type: String,
required: false,
default: '',
},
descriptionLink: {
type: String,
required: false,
default: '',
},
help: {
type: String,
required: false,
default: '',
},
iconTitle: {
type: String,
required: true,
},
isProtected: {
type: Boolean,
required: true,
},
},
computed: {
iconName() {
return this.isProtected ? REQUIRED_ICON : NOT_REQUIRED_ICON;
},
iconClass() {
return this.isProtected ? REQUIRED_ICON_CLASS : NOT_REQUIRED_ICON_CLASS;
},
iconDataTestId() {
// eslint-disable-next-line @gitlab/require-i18n-strings
return this.dataTestId ? `${this.dataTestId}-icon` : '';
},
hasDescription() {
if (!this.glFeatures.editBranchRules) {
return Boolean(this.description);
}
return this.isProtected ? Boolean(this.description) : false;
},
},
};
</script>
<template>
<div v-if="glFeatures.editBranchRules">
<gl-toggle :label="label" :help="help" :value="isProtected" class="gl-mb-5">
<template v-if="hasDescription" #description>
<gl-sprintf :message="description">
<template #link="{ content }">
<gl-link :href="descriptionLink">{{ content }}</gl-link>
</template>
</gl-sprintf>
</template>
</gl-toggle>
</div>
<div v-else class="gl-mb-5">
<div class="gl-display-flex gl-align-items-center">
<gl-icon :data-testid="iconDataTestId" :size="14" :name="iconName" :class="iconClass" />
<strong class="gl-ml-2">{{ iconTitle }}</strong>
</div>
<gl-sprintf v-if="hasDescription" :message="description" data-testid="protection-description">
<template #link="{ content }">
<gl-link :href="descriptionLink">{{ content }}</gl-link>
</template>
</gl-sprintf>
</div>
</template>
...@@ -14,14 +14,9 @@ import deleteBranchRuleMutation from '~/projects/settings/branch_rules/mutations ...@@ -14,14 +14,9 @@ import deleteBranchRuleMutation from '~/projects/settings/branch_rules/mutations
import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import { import { I18N } from '~/projects/settings/branch_rules/components/view/constants';
I18N,
REQUIRED_ICON,
NOT_REQUIRED_ICON,
REQUIRED_ICON_CLASS,
NOT_REQUIRED_ICON_CLASS,
} from '~/projects/settings/branch_rules/components/view/constants';
import Protection from '~/projects/settings/branch_rules/components/view/protection.vue'; 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 { sprintf } from '~/locale';
import { import {
deleteBranchRuleMockResponse, deleteBranchRuleMockResponse,
...@@ -60,6 +55,7 @@ describe('View branch rules in enterprise edition', () => { ...@@ -60,6 +55,7 @@ describe('View branch rules in enterprise edition', () => {
jest.fn().mockResolvedValue(response); jest.fn().mockResolvedValue(response);
const createComponent = async ( const createComponent = async (
glFeatures = { editBranchRules: true },
{ showApprovers, showStatusChecks, showCodeOwners } = {}, { showApprovers, showStatusChecks, showCodeOwners } = {},
mockResponse, mockResponse,
mutationMockResponse, mutationMockResponse,
...@@ -85,6 +81,7 @@ describe('View branch rules in enterprise edition', () => { ...@@ -85,6 +81,7 @@ describe('View branch rules in enterprise edition', () => {
showApprovers, showApprovers,
showStatusChecks, showStatusChecks,
showCodeOwners, showCodeOwners,
glFeatures,
}, },
}); });
...@@ -99,9 +96,7 @@ describe('View branch rules in enterprise edition', () => { ...@@ -99,9 +96,7 @@ describe('View branch rules in enterprise edition', () => {
const findApprovalsApp = () => wrapper.findComponent(ApprovalRulesApp); const findApprovalsApp = () => wrapper.findComponent(ApprovalRulesApp);
const findProjectRules = () => wrapper.findComponent(ProjectRules); const findProjectRules = () => wrapper.findComponent(ProjectRules);
const findStatusChecksTitle = () => wrapper.findByText(I18N.statusChecksTitle); const findStatusChecksTitle = () => wrapper.findByText(I18N.statusChecksTitle);
const findCodeOwnerApprovalIcon = () => wrapper.findByTestId('code-owners-icon'); const findProtectionToggles = () => wrapper.findAllComponents(ProtectionToggle);
const findCodeOwnerApprovalTitle = (title) => wrapper.findByText(title);
const findCodeOwnerApprovalDescription = (description) => wrapper.findByText(description);
it('renders a branch protection component for push rules', () => { it('renders a branch protection component for push rules', () => {
expect(findBranchProtections().at(0).props()).toMatchObject({ expect(findBranchProtections().at(0).props()).toMatchObject({
...@@ -119,28 +114,22 @@ describe('View branch rules in enterprise edition', () => { ...@@ -119,28 +114,22 @@ describe('View branch rules in enterprise edition', () => {
describe('Code owner approvals', () => { describe('Code owner approvals', () => {
it('does not render a code owner approval section by default', () => { it('does not render a code owner approval section by default', () => {
expect(findCodeOwnerApprovalIcon().exists()).toBe(false); expect(findProtectionToggles().length).toBe(1);
expect(findCodeOwnerApprovalTitle(I18N.requiresCodeOwnerApprovalTitle).exists()).toBe(false);
expect(
findCodeOwnerApprovalDescription(I18N.requiresCodeOwnerApprovalDescription).exists(),
).toBe(false);
}); });
it.each` it.each`
codeOwnerApprovalRequired | iconName | iconClass | title | description codeOwnerApprovalRequired | iconTitle | description
${true} | ${REQUIRED_ICON} | ${REQUIRED_ICON_CLASS} | ${I18N.requiresCodeOwnerApprovalTitle} | ${I18N.requiresCodeOwnerApprovalDescription} ${true} | ${I18N.requiresCodeOwnerApprovalTitle} | ${I18N.codeOwnerApprovalDescription}
${false} | ${NOT_REQUIRED_ICON} | ${NOT_REQUIRED_ICON_CLASS} | ${I18N.doesNotRequireCodeOwnerApprovalTitle} | ${I18N.doesNotRequireCodeOwnerApprovalDescription} ${false} | ${I18N.doesNotRequireCodeOwnerApprovalTitle} | ${I18N.codeOwnerApprovalDescription}
`( `(
'code owners with the correct icon, title and description', 'renders code owners approval section with the correct iconTitle and description',
async ({ codeOwnerApprovalRequired, iconName, iconClass, title, description }) => { async ({ codeOwnerApprovalRequired, iconTitle, description }) => {
const mockResponse = branchProtectionsMockResponse; const mockResponse = branchProtectionsMockResponse;
mockResponse.data.project.branchRules.nodes[0].branchProtection.codeOwnerApprovalRequired = codeOwnerApprovalRequired; 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(findProtectionToggles().at(1).props('iconTitle')).toEqual(iconTitle);
expect(findCodeOwnerApprovalIcon().attributes('class')).toBe(iconClass); expect(findProtectionToggles().at(1).props('description')).toEqual(description);
expect(findCodeOwnerApprovalTitle(title).exists()).toBe(true);
expect(findCodeOwnerApprovalTitle(description).exists()).toBe(true);
}, },
); );
}); });
...@@ -151,7 +140,7 @@ describe('View branch rules in enterprise edition', () => { ...@@ -151,7 +140,7 @@ describe('View branch rules in enterprise edition', () => {
}); });
describe('if "showApprovers" is true', () => { describe('if "showApprovers" is true', () => {
beforeEach(() => createComponent({ showApprovers: true })); beforeEach(() => createComponent({}, { showApprovers: true }));
it('sets an approval rules filter', () => { it('sets an approval rules filter', () => {
expect(store.modules.approvals.actions.setRulesFilter).toHaveBeenCalledWith( expect(store.modules.approvals.actions.setRulesFilter).toHaveBeenCalledWith(
...@@ -182,7 +171,7 @@ describe('View branch rules in enterprise edition', () => { ...@@ -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 () => { 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); expect(findStatusChecksTitle().exists()).toBe(true);
...@@ -193,4 +182,22 @@ describe('View branch rules in enterprise edition', () => { ...@@ -193,4 +182,22 @@ describe('View branch rules in enterprise edition', () => {
statusChecks: statusChecksRulesMock, 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);
},
);
});
}); });
...@@ -9023,6 +9023,9 @@ msgstr "" ...@@ -9023,6 +9023,9 @@ msgstr ""
msgid "BranchRules|Allow all users with push access to %{linkStart}force push%{linkEnd}." msgid "BranchRules|Allow all users with push access to %{linkStart}force push%{linkEnd}."
msgstr "" msgstr ""
   
msgid "BranchRules|Allow force push"
msgstr ""
msgid "BranchRules|Allowed to force push" msgid "BranchRules|Allowed to force push"
msgstr "" msgstr ""
   
...@@ -9074,6 +9077,9 @@ msgstr "" ...@@ -9074,6 +9077,9 @@ msgstr ""
msgid "BranchRules|Cancel" msgid "BranchRules|Cancel"
msgstr "" 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." msgid "BranchRules|Changes require a merge request. The following users can push and merge directly."
msgstr "" msgstr ""
   
...@@ -9161,10 +9167,13 @@ msgstr "" ...@@ -9161,10 +9167,13 @@ msgstr ""
msgid "BranchRules|Require approval from code owners." msgid "BranchRules|Require approval from code owners."
msgstr "" msgstr ""
   
msgid "BranchRules|Require code owner approval"
msgstr ""
msgid "BranchRules|Requires CODEOWNERS approval" msgid "BranchRules|Requires CODEOWNERS approval"
msgstr "" msgstr ""
   
msgid "BranchRules|Requires approval from code owners" msgid "BranchRules|Requires code owner approval"
msgstr "" msgstr ""
   
msgid "BranchRules|Roles" msgid "BranchRules|Roles"
...@@ -13,16 +13,13 @@ import RuleView from '~/projects/settings/branch_rules/components/view/index.vue ...@@ -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 RuleDrawer from '~/projects/settings/branch_rules/components/view/rule_drawer.vue';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper'; import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import Protection from '~/projects/settings/branch_rules/components/view/protection.vue'; 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 BranchRuleModal from '~/projects/settings/components/branch_rule_modal.vue';
import getProtectableBranches from '~/projects/settings/graphql/queries/protectable_branches.query.graphql'; import getProtectableBranches from '~/projects/settings/graphql/queries/protectable_branches.query.graphql';
import { import {
I18N, I18N,
ALL_BRANCHES_WILDCARD, ALL_BRANCHES_WILDCARD,
REQUIRED_ICON,
NOT_REQUIRED_ICON,
REQUIRED_ICON_CLASS,
NOT_REQUIRED_ICON_CLASS,
DELETE_RULE_MODAL_ID, DELETE_RULE_MODAL_ID,
EDIT_RULE_MODAL_ID, EDIT_RULE_MODAL_ID,
} from '~/projects/settings/branch_rules/components/view/constants'; } from '~/projects/settings/branch_rules/components/view/constants';
...@@ -81,12 +78,12 @@ describe('View branch rules', () => { ...@@ -81,12 +78,12 @@ describe('View branch rules', () => {
const errorHandler = jest.fn().mockRejectedValue('error'); const errorHandler = jest.fn().mockRejectedValue('error');
const toastMock = { show: jest.fn() }; const toastMock = { show: jest.fn() };
const createComponent = async ( const createComponent = async ({
glFeatures = { editBranchRules: true }, glFeatures = { editBranchRules: true },
branchRulesQueryHandler = branchRulesMockRequestHandler, branchRulesQueryHandler = branchRulesMockRequestHandler,
deleteMutationHandler = deleteBranchRuleSuccessHandler, deleteMutationHandler = deleteBranchRuleSuccessHandler,
editMutationHandler = editBranchRuleSuccessHandler, editMutationHandler = editBranchRuleSuccessHandler,
) => { } = {}) => {
fakeApollo = createMockApollo([ fakeApollo = createMockApollo([
[branchRulesQuery, branchRulesQueryHandler], [branchRulesQuery, branchRulesQueryHandler],
[getProtectableBranches, protectableBranchesMockRequestHandler], [getProtectableBranches, protectableBranchesMockRequestHandler],
...@@ -96,9 +93,15 @@ describe('View branch rules', () => { ...@@ -96,9 +93,15 @@ describe('View branch rules', () => {
wrapper = shallowMountExtended(RuleView, { wrapper = shallowMountExtended(RuleView, {
apolloProvider: fakeApollo, apolloProvider: fakeApollo,
provide: { projectPath, protectedBranchesPath, branchRulesPath, glFeatures }, provide: {
projectPath,
protectedBranchesPath,
branchRulesPath,
glFeatures,
},
stubs: { stubs: {
Protection, Protection,
ProtectionToggle,
BranchRuleModal, BranchRuleModal,
RuleDrawer, RuleDrawer,
GlCard: stubComponent(GlCard, { template: RENDER_ALL_SLOTS_TEMPLATE }), GlCard: stubComponent(GlCard, { template: RENDER_ALL_SLOTS_TEMPLATE }),
...@@ -119,9 +122,7 @@ describe('View branch rules', () => { ...@@ -119,9 +122,7 @@ describe('View branch rules', () => {
const findAllBranches = () => wrapper.findByTestId('all-branches'); const findAllBranches = () => wrapper.findByTestId('all-branches');
const findBranchProtectionTitle = () => wrapper.findByText(I18N.protectBranchTitle); const findBranchProtectionTitle = () => wrapper.findByText(I18N.protectBranchTitle);
const findBranchProtections = () => wrapper.findAllComponents(Protection); const findBranchProtections = () => wrapper.findAllComponents(Protection);
const findForcePushIcon = () => wrapper.findByTestId('force-push-icon'); const findProtectionToggles = () => wrapper.findAllComponents(ProtectionToggle);
const findForcePushTitle = (title) => wrapper.findByText(title);
const findForcePushDescription = () => wrapper.findByText(I18N.forcePushDescription);
const findApprovalsTitle = () => wrapper.findByText(I18N.approvalsTitle); const findApprovalsTitle = () => wrapper.findByText(I18N.approvalsTitle);
const findpageTitle = () => wrapper.findByText(I18N.pageTitle); const findpageTitle = () => wrapper.findByText(I18N.pageTitle);
const findStatusChecksTitle = () => wrapper.findByText(I18N.statusChecksTitle); const findStatusChecksTitle = () => wrapper.findByText(I18N.statusChecksTitle);
...@@ -192,20 +193,21 @@ describe('View branch rules', () => { ...@@ -192,20 +193,21 @@ describe('View branch rules', () => {
}); });
it.each` it.each`
allowForcePush | iconName | iconClass | title allowForcePush | iconTitle | description
${true} | ${REQUIRED_ICON} | ${REQUIRED_ICON_CLASS} | ${I18N.allowForcePushTitle} ${true} | ${I18N.allowForcePushTitle} | ${I18N.forcePushDescriptionWithDocs}
${false} | ${NOT_REQUIRED_ICON} | ${NOT_REQUIRED_ICON_CLASS} | ${I18N.doesNotAllowForcePushTitle} ${false} | ${I18N.doesNotAllowForcePushTitle} | ${I18N.forcePushDescriptionWithDocs}
`( `(
'renders force push section with the correct icon, title and description', 'renders force push section with the correct title and description',
async ({ allowForcePush, iconName, iconClass, title }) => { async ({ allowForcePush, iconTitle, description }) => {
const mockResponse = branchProtectionsMockResponse; const mockResponse = branchProtectionsMockResponse;
mockResponse.data.project.branchRules.nodes[0].branchProtection.allowForcePush = allowForcePush; 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(findProtectionToggles().at(0).props('iconTitle')).toEqual(iconTitle);
expect(findForcePushIcon().attributes('class')).toBe(iconClass); expect(findProtectionToggles().at(0).props('description')).toEqual(description);
expect(findForcePushTitle(title).exists()).toBe(true);
expect(findForcePushDescription().exists()).toBe(true);
}, },
); );
...@@ -238,6 +240,10 @@ describe('View branch rules', () => { ...@@ -238,6 +240,10 @@ describe('View branch rules', () => {
}); });
describe('Editing branch rule', () => { describe('Editing branch rule', () => {
beforeEach(async () => {
await createComponent();
});
it('renders edit branch rule button', () => { it('renders edit branch rule button', () => {
expect(findEditRuleNameButton().text()).toBe('Edit'); expect(findEditRuleNameButton().text()).toBe('Edit');
}); });
...@@ -277,6 +283,10 @@ describe('View branch rules', () => { ...@@ -277,6 +283,10 @@ describe('View branch rules', () => {
'/project/Project/-/settings/repository/branch_rules?branch=main', '/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', () => { describe('Deleting branch rule', () => {
...@@ -314,7 +324,11 @@ describe('View branch rules', () => { ...@@ -314,7 +324,11 @@ describe('View branch rules', () => {
}); });
it('if error happens it shows an alert', async () => { 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'); findDeleteRuleModal().vm.$emit('ok');
await nextTick(); await nextTick();
await waitForPromises(); await waitForPromises();
...@@ -332,7 +346,10 @@ describe('View branch rules', () => { ...@@ -332,7 +346,10 @@ describe('View branch rules', () => {
beforeEach(async () => { beforeEach(async () => {
jest.spyOn(util, 'getParameterByName').mockReturnValueOnce('All branches'); 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', () => { it('renders the correct branch rule title', () => {
...@@ -381,7 +398,7 @@ describe('View branch rules', () => { ...@@ -381,7 +398,7 @@ describe('View branch rules', () => {
describe('When rendered for a non-existing rule', () => { describe('When rendered for a non-existing rule', () => {
beforeEach(async () => { beforeEach(async () => {
jest.spyOn(util, 'getParameterByName').mockReturnValueOnce('non-existing-rule'); jest.spyOn(util, 'getParameterByName').mockReturnValueOnce('non-existing-rule');
await createComponent({ editBranchRules: true }); await createComponent({ glFeatures: { editBranchRules: true } });
}); });
it('shows empty state', () => { it('shows empty state', () => {
...@@ -389,8 +406,9 @@ describe('View branch rules', () => { ...@@ -389,8 +406,9 @@ describe('View branch rules', () => {
}); });
}); });
describe('When add_branch_rules feature flag is disabled', () => { describe('When edit_branch_rules feature flag is disabled', () => {
beforeEach(() => createComponent({ editBranchRules: false })); beforeEach(() => createComponent({ glFeatures: { editBranchRules: false } }));
it('does not render delete rule button and modal', () => { it('does not render delete rule button and modal', () => {
expect(findDeleteRuleButton().exists()).toBe(false); expect(findDeleteRuleButton().exists()).toBe(false);
expect(findDeleteRuleModal().exists()).toBe(false); expect(findDeleteRuleModal().exists()).toBe(false);
...@@ -400,5 +418,25 @@ describe('View branch rules', () => { ...@@ -400,5 +418,25 @@ describe('View branch rules', () => {
expect(findEditRuleNameButton().exists()).toBe(false); expect(findEditRuleNameButton().exists()).toBe(false);
expect(findBranchRuleModal().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);
},
);
}); });
}); });
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);
});
});
});
0% 加载中 .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册