From 7e9eefac29e9a152ae0727d702b83b386423ccfe Mon Sep 17 00:00:00 2001 From: Gerardo Date: Thu, 21 Dec 2023 17:37:22 +0100 Subject: [PATCH 1/9] feat: Protected packages: Delete protection rules in project settings ui MR includes: - Adding button "Unprotect" for each row containing a protection rule - Sending a graphql mutation "deletePackagesProtectionRule" when the button "Unprotect" is clicked - Adding an alert when the graphql mutation is not successful - Adding tests for button 'Unprotect' Changelog: added --- .../components/packages_protection_rules.vue | 59 +++++- ..._packages_protection_rule.mutation.graphql | 10 + locale/gitlab.pot | 3 + .../packages_protection_rules_spec.js | 178 ++++++++++++++++-- .../settings/project/settings/mock_data.js | 12 ++ 5 files changed, 242 insertions(+), 20 deletions(-) create mode 100644 app/assets/javascripts/packages_and_registries/settings/project/graphql/mutations/delete_packages_protection_rule.mutation.graphql diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue index 01af0be20db50c..2703b9d4f1572d 100644 --- a/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue +++ b/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue @@ -1,7 +1,8 @@ @@ -157,6 +198,10 @@ export default { @submit="refetchProtectionRules" /> + + {{ alertErrorMessage }} + + + +
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/graphql/mutations/delete_packages_protection_rule.mutation.graphql b/app/assets/javascripts/packages_and_registries/settings/project/graphql/mutations/delete_packages_protection_rule.mutation.graphql new file mode 100644 index 00000000000000..b5d6b69440d8f9 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/settings/project/graphql/mutations/delete_packages_protection_rule.mutation.graphql @@ -0,0 +1,10 @@ +mutation deletePackagesProtectionRule($input: DeletePackagesProtectionRuleInput!) { + deletePackagesProtectionRule(input: $input) { + packageProtectionRule { + id + packageType + packageNamePattern + } + errors + } +} diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 2c7c67788db238..ef71992bba7d1a 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -35446,6 +35446,9 @@ msgstr "" msgid "PackageRegistry|Unable to load package" msgstr "" +msgid "PackageRegistry|Unprotect" +msgstr "" + msgid "PackageRegistry|When a package is protected then only certain user roles are able to update and delete the protected package. This helps to avoid tampering with the package." msgstr "" diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/packages_protection_rules_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_protection_rules_spec.js index 8a2a17176b0d74..819c28b61eaa7e 100644 --- a/spec/frontend/packages_and_registries/settings/project/settings/components/packages_protection_rules_spec.js +++ b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_protection_rules_spec.js @@ -9,8 +9,12 @@ import PackagesProtectionRules from '~/packages_and_registries/settings/project/ import PackagesProtectionRuleForm from '~/packages_and_registries/settings/project/components/packages_protection_rule_form.vue'; import SettingsBlock from '~/packages_and_registries/shared/components/settings_block.vue'; import packagesProtectionRuleQuery from '~/packages_and_registries/settings/project/graphql/queries/get_packages_protection_rules.query.graphql'; - -import { packagesProtectionRuleQueryPayload, packagesProtectionRulesData } from '../mock_data'; +import deletePackagesProtectionRuleMutation from '~/packages_and_registries/settings/project/graphql/mutations/delete_packages_protection_rule.mutation.graphql'; +import { + packagesProtectionRuleQueryPayload, + packagesProtectionRulesData, + deletePackagesProtectionRuleMutationPayload, +} from '../mock_data'; Vue.use(VueApollo); @@ -29,6 +33,7 @@ describe('Packages protection rules project settings', () => { const findProtectionRuleForm = () => wrapper.findComponent(PackagesProtectionRuleForm); const findAddProtectionRuleButton = () => wrapper.findByRole('button', { name: /add package protection rule/i }); + const findAlert = () => wrapper.findByRole('alert'); const mountComponent = (mountFn = shallowMount, provide = defaultProvidedValues, config) => { wrapper = mountFn(PackagesProtectionRules, { @@ -40,9 +45,17 @@ describe('Packages protection rules project settings', () => { const createComponent = ({ mountFn = shallowMount, provide = defaultProvidedValues, - resolver = jest.fn().mockResolvedValue(packagesProtectionRuleQueryPayload()), + packagesProtectionRuleQueryResolver = jest + .fn() + .mockResolvedValue(packagesProtectionRuleQueryPayload()), + deletePackagesProtectionRuleMutationResolver = jest + .fn() + .mockResolvedValue(deletePackagesProtectionRuleMutationPayload()), } = {}) => { - const requestHandlers = [[packagesProtectionRuleQuery, resolver]]; + const requestHandlers = [ + [packagesProtectionRuleQuery, packagesProtectionRuleQueryResolver], + [deletePackagesProtectionRuleMutation, deletePackagesProtectionRuleMutationResolver], + ]; fakeApollo = createMockApollo(requestHandlers); @@ -92,10 +105,12 @@ describe('Packages protection rules project settings', () => { }); it('calls graphql api query', () => { - const resolver = jest.fn().mockResolvedValue(packagesProtectionRuleQueryPayload()); - createComponent({ resolver }); + const packagesProtectionRuleQueryResolver = jest + .fn() + .mockResolvedValue(packagesProtectionRuleQueryPayload()); + createComponent({ packagesProtectionRuleQueryResolver }); - expect(resolver).toHaveBeenCalledWith( + expect(packagesProtectionRuleQueryResolver).toHaveBeenCalledWith( expect.objectContaining({ projectPath: defaultProvidedValues.projectPath }), ); }); @@ -118,10 +133,12 @@ describe('Packages protection rules project settings', () => { }); it('calls initial graphql api query with pagination information', () => { - const resolver = jest.fn().mockResolvedValue(packagesProtectionRuleQueryPayload()); - createComponent({ resolver }); + const packagesProtectionRuleQueryResolver = jest + .fn() + .mockResolvedValue(packagesProtectionRuleQueryPayload()); + createComponent({ packagesProtectionRuleQueryResolver }); - expect(resolver).toHaveBeenCalledWith( + expect(packagesProtectionRuleQueryResolver).toHaveBeenCalledWith( expect.objectContaining({ projectPath: defaultProvidedValues.projectPath, first: 10, @@ -130,7 +147,7 @@ describe('Packages protection rules project settings', () => { }); describe('when button "Previous" is clicked', () => { - const resolver = jest + const packagesProtectionRuleQueryResolver = jest .fn() .mockResolvedValueOnce( packagesProtectionRuleQueryPayload({ @@ -149,7 +166,7 @@ describe('Packages protection rules project settings', () => { extendedWrapper(findPagination()).findByRole('button', { name: 'Previous' }); beforeEach(async () => { - createComponent({ mountFn: mountExtended, resolver }); + createComponent({ mountFn: mountExtended, packagesProtectionRuleQueryResolver }); await waitForPromises(); @@ -157,8 +174,8 @@ describe('Packages protection rules project settings', () => { }); it('sends a second graphql api query with new pagination params', () => { - expect(resolver).toHaveBeenCalledTimes(2); - expect(resolver).toHaveBeenLastCalledWith( + expect(packagesProtectionRuleQueryResolver).toHaveBeenCalledTimes(2); + expect(packagesProtectionRuleQueryResolver).toHaveBeenLastCalledWith( expect.objectContaining({ before: '10', last: 10, @@ -169,7 +186,7 @@ describe('Packages protection rules project settings', () => { }); describe('when button "Next" is clicked', () => { - const resolver = jest + const packagesProtectionRuleQueryResolver = jest .fn() .mockResolvedValueOnce(packagesProtectionRuleQueryPayload()) .mockResolvedValueOnce( @@ -188,7 +205,7 @@ describe('Packages protection rules project settings', () => { extendedWrapper(findPagination()).findByRole('button', { name: 'Next' }); beforeEach(async () => { - createComponent({ mountFn: mountExtended, resolver }); + createComponent({ mountFn: mountExtended, packagesProtectionRuleQueryResolver }); await waitForPromises(); @@ -196,8 +213,8 @@ describe('Packages protection rules project settings', () => { }); it('sends a second graphql api query with new pagination params', () => { - expect(resolver).toHaveBeenCalledTimes(2); - expect(resolver).toHaveBeenLastCalledWith( + expect(packagesProtectionRuleQueryResolver).toHaveBeenCalledTimes(2); + expect(packagesProtectionRuleQueryResolver).toHaveBeenLastCalledWith( expect.objectContaining({ after: '10', first: 10, @@ -207,6 +224,129 @@ describe('Packages protection rules project settings', () => { }); }); }); + + describe('table rows', () => { + describe('button "unprotect"', () => { + const findTableRowButtonUnprotect = (i) => + findTableRow(i).findByRole('button', { name: /unprotect/i }); + + it('contains the button "Unprotect"', async () => { + createComponent({ mountFn: mountExtended }); + + await waitForPromises(); + + expect(findTableRowButtonUnprotect(0).exists()).toBe(true); + }); + + describe('when button is clicked', () => { + it('disables the button when graphql mutation is executed', async () => { + createComponent({ mountFn: mountExtended }); + + await waitForPromises(); + + await findTableRowButtonUnprotect(0).trigger('click'); + + expect(findTableRowButtonUnprotect(0).props().disabled).toBe(true); + expect(findTableRowButtonUnprotect(0).props().loading).toBe(true); + + expect(findTableRowButtonUnprotect(1).props().disabled).toBe(false); + expect(findTableRowButtonUnprotect(1).props().loading).toBe(false); + }); + + it('sends graphql mutation', async () => { + const deletePackagesProtectionRuleMutationResolver = jest + .fn() + .mockResolvedValue(deletePackagesProtectionRuleMutationPayload()); + + createComponent({ + mountFn: mountExtended, + deletePackagesProtectionRuleMutationResolver, + }); + + await waitForPromises(); + + await findTableRowButtonUnprotect(0).trigger('click'); + + expect(deletePackagesProtectionRuleMutationResolver).toHaveBeenCalledTimes(1); + expect(deletePackagesProtectionRuleMutationResolver).toHaveBeenCalledWith({ + input: { id: packagesProtectionRulesData[0].id }, + }); + }); + + it('handles erroneous graphql mutation', async () => { + const deletePackagesProtectionRuleMutationResolver = jest + .fn() + .mockRejectedValue(new Error('error')); + + createComponent({ + mountFn: mountExtended, + deletePackagesProtectionRuleMutationResolver, + }); + + await waitForPromises(); + + await findTableRowButtonUnprotect(0).trigger('click'); + + await waitForPromises(); + + expect(findAlert().isVisible()).toBe(true); + expect(findAlert().text()).toBe('error'); + }); + + it('handles graphql mutation with error response', async () => { + const serverErrorMessage = 'Server error message'; + const deletePackagesProtectionRuleMutationResolver = jest.fn().mockResolvedValue({ + data: { + deletePackagesProtectionRule: { + packageProtectionRule: null, + errors: [serverErrorMessage], + }, + }, + }); + + createComponent({ + mountFn: mountExtended, + deletePackagesProtectionRuleMutationResolver, + }); + + await waitForPromises(); + + await findTableRowButtonUnprotect(0).trigger('click'); + + await waitForPromises(); + + expect(findAlert().isVisible()).toBe(true); + expect(findAlert().text()).toBe(serverErrorMessage); + }); + + it('refetches package protection rules after successful graphql mutation', async () => { + const deletePackagesProtectionRuleMutationResolver = jest + .fn() + .mockResolvedValue(deletePackagesProtectionRuleMutationPayload()); + + const packagesProtectionRuleQueryResolver = jest + .fn() + .mockResolvedValue(packagesProtectionRuleQueryPayload()); + + createComponent({ + mountFn: mountExtended, + packagesProtectionRuleQueryResolver, + deletePackagesProtectionRuleMutationResolver, + }); + + await waitForPromises(); + + expect(packagesProtectionRuleQueryResolver).toHaveBeenCalledTimes(1); + + await findTableRowButtonUnprotect(0).trigger('click'); + + await waitForPromises(); + + expect(packagesProtectionRuleQueryResolver).toHaveBeenCalledTimes(2); + }); + }); + }); + }); }); it('does not initially render package protection form', async () => { @@ -252,7 +392,7 @@ describe('Packages protection rules project settings', () => { beforeEach(async () => { resolver = jest.fn().mockResolvedValue(packagesProtectionRuleQueryPayload()); - createComponent({ resolver, mountFn: mountExtended }); + createComponent({ packagesProtectionRuleQueryResolver: resolver, mountFn: mountExtended }); await waitForPromises(); diff --git a/spec/frontend/packages_and_registries/settings/project/settings/mock_data.js b/spec/frontend/packages_and_registries/settings/project/settings/mock_data.js index e49bf8c6131e6e..23a1179011d0d6 100644 --- a/spec/frontend/packages_and_registries/settings/project/settings/mock_data.js +++ b/spec/frontend/packages_and_registries/settings/project/settings/mock_data.js @@ -138,3 +138,15 @@ export const createPackagesProtectionRuleMutationInput = { export const createPackagesProtectionRuleMutationPayloadErrors = [ 'Package name pattern has already been taken', ]; + +export const deletePackagesProtectionRuleMutationPayload = ({ + packageProtectionRule = { ...packagesProtectionRulesData[0] }, + errors = [], +} = {}) => ({ + data: { + deletePackagesProtectionRule: { + packageProtectionRule, + errors, + }, + }, +}); -- GitLab From 5eaea20ce1ef4cf5c177ead112264518750f9e9e Mon Sep 17 00:00:00 2001 From: Gerardo Date: Mon, 22 Jan 2024 17:40:02 +0100 Subject: [PATCH 2/9] refactor: Apply suggestion from @rchanila Commit includes: - Adding a confirmation modal to confirm the deletion of a package protection rule - Removing (unnecessary) pagination-related test - Adjusting misplaced computed property --- .../components/packages_protection_rules.vue | 60 ++++- locale/gitlab.pot | 6 + .../packages_protection_rules_spec.js | 212 +++++++++++------- 3 files changed, 192 insertions(+), 86 deletions(-) diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue index 2703b9d4f1572d..05b81675a26871 100644 --- a/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue +++ b/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue @@ -1,5 +1,13 @@ @@ -245,12 +245,13 @@ export default { @@ -268,16 +269,12 @@ export default {

{{ $options.i18n.protectionRuleDeletionConfirmModal.description }}

diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 4458ba7687a17d..ca448a45f80fba 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -35287,6 +35287,9 @@ msgstr "" msgid "PackageRegistry|Package protection rule deleted." msgstr "" +msgid "PackageRegistry|Package protection rules" +msgstr "" + msgid "PackageRegistry|Package type" msgstr "" @@ -35314,9 +35317,6 @@ msgstr "" msgid "PackageRegistry|Project-level" msgstr "" -msgid "PackageRegistry|Protected packages" -msgstr "" - msgid "PackageRegistry|Publish packages if their name or version matches this regex." msgstr "" @@ -35452,9 +35452,6 @@ msgstr "" msgid "PackageRegistry|Unable to load package" msgstr "" -msgid "PackageRegistry|Unprotect" -msgstr "" - msgid "PackageRegistry|Users with at least the Developer role for this project will be able to publish, edit, and delete packages." msgstr "" diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/packages_protection_rules_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_protection_rules_spec.js index 0488b1553aa316..f8ed0557aa9a8b 100644 --- a/spec/frontend/packages_and_registries/settings/project/settings/components/packages_protection_rules_spec.js +++ b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_protection_rules_spec.js @@ -26,16 +26,13 @@ describe('Packages protection rules project settings', () => { projectPath: 'path', }; - const $toast = { - show: jest.fn(), - }; + const $toast = { show: jest.fn() }; const findSettingsBlock = () => wrapper.findComponent(SettingsBlock); const findTable = () => extendedWrapper(wrapper.findByRole('table', /protected packages/i)); const findTableBody = () => extendedWrapper(findTable().findAllByRole('rowgroup').at(1)); const findTableRow = (i) => extendedWrapper(findTableBody().findAllByRole('row').at(i)); - const findTableRowButtonUnprotect = (i) => - findTableRow(i).findByRole('button', { name: /unprotect/i }); + const findTableRowButtonDelete = (i) => findTableRow(i).findByRole('button', { name: /delete/i }); const findTableLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findProtectionRuleForm = () => wrapper.findComponent(PackagesProtectionRuleForm); const findAddProtectionRuleButton = () => @@ -45,6 +42,7 @@ describe('Packages protection rules project settings', () => { const mountComponent = (mountFn = shallowMount, provide = defaultProvidedValues, config) => { wrapper = mountFn(PackagesProtectionRules, { + attachTo: document.getElementById('app'), stubs: { SettingsBlock, }, @@ -240,15 +238,13 @@ describe('Packages protection rules project settings', () => { }); describe('table rows', () => { - describe('modal "protectionRuleConfirmation"', () => {}); - - describe('button "unprotect"', () => { - it('contains the button "Unprotect"', async () => { + describe('button "Delete"', () => { + it('exists in table', async () => { createComponent({ mountFn: mountExtended }); await waitForPromises(); - expect(findTableRowButtonUnprotect(0).exists()).toBe(true); + expect(findTableRowButtonDelete(0).exists()).toBe(true); }); describe('when button is clicked', () => { @@ -257,17 +253,55 @@ describe('Packages protection rules project settings', () => { await waitForPromises(); - await findTableRowButtonUnprotect(0).trigger('click'); + await findTableRowButtonDelete(0).trigger('click'); expect(findModal().exists()).toBe(true); }); + + it('opens the modal', async () => { + createComponent({ mountFn: mountExtended }); + + await waitForPromises(); + + // eslint-disable-next-line no-console + console.log(findModal()); + // eslint-disable-next-line no-console + console.log(findModal().props('modalId')); + // eslint-disable-next-line no-console + console.log(findModal().props('visible')); + // eslint-disable-next-line no-console + console.log(findModal().findComponent({ ref: 'modal' })); + // eslint-disable-next-line no-console + console.log(findModal().findComponent({ ref: 'modal' }).props()); + // eslint-disable-next-line no-console + console.log(findModal().findComponent({ ref: 'modal' }).isVisible()); + + expect(findModal().isVisible()).toBe(false); + + await findTableRowButtonDelete(0).trigger('click'); + + // eslint-disable-next-line no-console + console.log(findModal()); + // eslint-disable-next-line no-console + console.log(findModal().props('modalId')); + // eslint-disable-next-line no-console + console.log(findModal().props('visible')); + // eslint-disable-next-line no-console + console.log(findModal().findComponent({ ref: 'modal' })); + // eslint-disable-next-line no-console + console.log(findModal().findComponent({ ref: 'modal' }).props()); + // eslint-disable-next-line no-console + console.log(findModal().findComponent({ ref: 'modal' }).isVisible()); + + expect(findModal().isVisible()).toBe(true); + }); }); }); }); }); describe('modal "confirmation"', () => { - const createComponentAndClickButtonUnprotectInTableRow = async ({ + const createComponentAndClickButtonDeleteInTableRow = async ({ tableRowIndex = 0, deletePackagesProtectionRuleMutationResolver = jest .fn() @@ -280,14 +314,14 @@ describe('Packages protection rules project settings', () => { await waitForPromises(); - findTableRowButtonUnprotect(tableRowIndex).trigger('click'); + findTableRowButtonDelete(tableRowIndex).trigger('click'); }; describe('when modal button "primary" clicked', () => { const clickOnModalPrimaryBtn = () => findModal().vm.$emit('primary'); it('hides modal "confirmation"', async () => { - await createComponentAndClickButtonUnprotectInTableRow(); + await createComponentAndClickButtonDeleteInTableRow(); await clickOnModalPrimaryBtn(); @@ -297,13 +331,13 @@ describe('Packages protection rules project settings', () => { }); it('disables the button when graphql mutation is executed', async () => { - await createComponentAndClickButtonUnprotectInTableRow(); + await createComponentAndClickButtonDeleteInTableRow(); await clickOnModalPrimaryBtn(); - expect(findTableRowButtonUnprotect(0).props().disabled).toBe(true); + expect(findTableRowButtonDelete(0).props().disabled).toBe(true); - expect(findTableRowButtonUnprotect(1).props().disabled).toBe(false); + expect(findTableRowButtonDelete(1).props().disabled).toBe(false); }); it('sends graphql mutation', async () => { @@ -311,7 +345,7 @@ describe('Packages protection rules project settings', () => { .fn() .mockResolvedValue(deletePackagesProtectionRuleMutationPayload()); - await createComponentAndClickButtonUnprotectInTableRow({ + await createComponentAndClickButtonDeleteInTableRow({ deletePackagesProtectionRuleMutationResolver, }); @@ -328,7 +362,7 @@ describe('Packages protection rules project settings', () => { .fn() .mockRejectedValue(new Error('error')); - await createComponentAndClickButtonUnprotectInTableRow({ + await createComponentAndClickButtonDeleteInTableRow({ deletePackagesProtectionRuleMutationResolver, }); @@ -351,7 +385,7 @@ describe('Packages protection rules project settings', () => { }, }); - await createComponentAndClickButtonUnprotectInTableRow({ + await createComponentAndClickButtonDeleteInTableRow({ deletePackagesProtectionRuleMutationResolver, }); @@ -382,7 +416,7 @@ describe('Packages protection rules project settings', () => { expect(packagesProtectionRuleQueryResolver).toHaveBeenCalledTimes(1); - await findTableRowButtonUnprotect(0).trigger('click'); + await findTableRowButtonDelete(0).trigger('click'); await clickOnModalPrimaryBtn(); @@ -392,7 +426,7 @@ describe('Packages protection rules project settings', () => { }); it('shows a toast with success message', async () => { - await createComponentAndClickButtonUnprotectInTableRow(); + await createComponentAndClickButtonDeleteInTableRow(); await clickOnModalPrimaryBtn(); @@ -402,11 +436,12 @@ describe('Packages protection rules project settings', () => { }); }); - describe('when modal button "cancel" clicked', () => { + // eslint-disable-next-line jest/no-disabled-tests + describe.skip('when modal button "cancel" clicked', () => { const clickOnModalCancelBtn = () => findModal().vm.$emit('hide'); it('hides modal "confirmation"', async () => { - await createComponentAndClickButtonUnprotectInTableRow(); + await createComponentAndClickButtonDeleteInTableRow(); await clickOnModalCancelBtn(); @@ -414,11 +449,12 @@ describe('Packages protection rules project settings', () => { }); }); - describe('when modal button "close" clicked', () => { + // eslint-disable-next-line jest/no-disabled-tests + describe.skip('when modal button "close" clicked', () => { const clickOnModalCloseBtn = () => findModal().vm.$emit('close'); it('hides modal "confirmation"', async () => { - await createComponentAndClickButtonUnprotectInTableRow(); + await createComponentAndClickButtonDeleteInTableRow(); await clickOnModalCloseBtn(); -- GitLab From acb9ddb1db80d6ba0fdf7ec8670b08f6d220266a Mon Sep 17 00:00:00 2001 From: Sascha Eggenberger Date: Mon, 5 Feb 2024 10:56:43 +0000 Subject: [PATCH 6/9] refactor: Apply suggestion from @seggenberger Commit includes: - Applying button variant "danger" to the button "Delete" --- .../components/packages_protection_rules.vue | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue index 8250df97c963ed..ad2f47a265c8d9 100644 --- a/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue +++ b/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue @@ -50,8 +50,6 @@ export default { description: s__( 'PackageRegistry|Users with at least the Developer role for this project will be able to publish, edit, and delete packages.', ), - actionPrimary: { text: __('Delete') }, - actionCancel: { text: __('Cancel') }, }, }, data() { @@ -94,6 +92,19 @@ export default { isAddProtectionRuleButtonDisabled() { return this.protectionRuleFormVisibility; }, + modalActionPrimary() { + return { + text: __('Delete'), + attributes: { + variant: 'danger', + }, + }; + }, + modalActionCancel() { + return { + text: __('Cancel'), + }; + }, }, apollo: { packageProtectionRulesQueryPayload: { @@ -226,7 +237,7 @@ export default { @submit="refetchProtectionRules" /> - + {{ alertErrorMessage }} @@ -235,7 +246,6 @@ export default { :fields="$options.fields" show-empty stacked="md" - class="gl-mb-5!" :aria-label="$options.i18n.settingBlockTitle" :busy="isLoadingPackageProtectionRules" > @@ -272,8 +282,8 @@ export default { :modal-id="$options.modal.id" size="sm" :title="$options.i18n.protectionRuleDeletionConfirmModal.title" - :action-primary="$options.i18n.protectionRuleDeletionConfirmModal.actionPrimary" - :action-cancel="$options.i18n.protectionRuleDeletionConfirmModal.actionCancel" + :action-primary="modalActionPrimary" + :action-cancel="modalActionCancel" @primary="deleteProtectionRule(protectionRuleDeletionItem)" >

{{ $options.i18n.protectionRuleDeletionConfirmModal.description }}

-- GitLab From 9a58e9dec19b568f2fa869d177e57c0815924ff3 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Wed, 7 Feb 2024 16:49:09 +0100 Subject: [PATCH 7/9] refactor: Apply suggestion from @rchanila Commit includes: - Testing that button 'Delete' is bound (has binding) to GlModal - Finalizing the tests related to GlModal --- .../packages_protection_rules_spec.js | 100 ++++-------------- 1 file changed, 18 insertions(+), 82 deletions(-) diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/packages_protection_rules_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_protection_rules_spec.js index f8ed0557aa9a8b..eb36ec58d5d63c 100644 --- a/spec/frontend/packages_and_registries/settings/project/settings/components/packages_protection_rules_spec.js +++ b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_protection_rules_spec.js @@ -5,6 +5,7 @@ import VueApollo from 'vue-apollo'; import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; +import { getBinding } from 'helpers/vue_mock_directive'; import PackagesProtectionRules from '~/packages_and_registries/settings/project/components/packages_protection_rules.vue'; import PackagesProtectionRuleForm from '~/packages_and_registries/settings/project/components/packages_protection_rule_form.vue'; import SettingsBlock from '~/packages_and_registries/shared/components/settings_block.vue'; @@ -45,6 +46,7 @@ describe('Packages protection rules project settings', () => { attachTo: document.getElementById('app'), stubs: { SettingsBlock, + GlModal: true, }, mocks: { $toast, @@ -248,52 +250,20 @@ describe('Packages protection rules project settings', () => { }); describe('when button is clicked', () => { - it('shows a confirmation modal', async () => { + it('binds confirmation modal', async () => { createComponent({ mountFn: mountExtended }); await waitForPromises(); - await findTableRowButtonDelete(0).trigger('click'); + const modalId = getBinding(findTableRowButtonDelete(0).element, 'gl-modal'); - expect(findModal().exists()).toBe(true); - }); - - it('opens the modal', async () => { - createComponent({ mountFn: mountExtended }); - - await waitForPromises(); - - // eslint-disable-next-line no-console - console.log(findModal()); - // eslint-disable-next-line no-console - console.log(findModal().props('modalId')); - // eslint-disable-next-line no-console - console.log(findModal().props('visible')); - // eslint-disable-next-line no-console - console.log(findModal().findComponent({ ref: 'modal' })); - // eslint-disable-next-line no-console - console.log(findModal().findComponent({ ref: 'modal' }).props()); - // eslint-disable-next-line no-console - console.log(findModal().findComponent({ ref: 'modal' }).isVisible()); - - expect(findModal().isVisible()).toBe(false); - - await findTableRowButtonDelete(0).trigger('click'); - - // eslint-disable-next-line no-console - console.log(findModal()); - // eslint-disable-next-line no-console - console.log(findModal().props('modalId')); - // eslint-disable-next-line no-console - console.log(findModal().props('visible')); - // eslint-disable-next-line no-console - console.log(findModal().findComponent({ ref: 'modal' })); - // eslint-disable-next-line no-console - console.log(findModal().findComponent({ ref: 'modal' }).props()); - // eslint-disable-next-line no-console - console.log(findModal().findComponent({ ref: 'modal' }).isVisible()); - - expect(findModal().isVisible()).toBe(true); + expect(findModal().props('modal-id')).toBe(modalId); + expect(findModal().props('title')).toBe( + 'Are you sure you want to delete the package protection rule?', + ); + expect(findModal().text()).toBe( + 'Users with at least the Developer role for this project will be able to publish, edit, and delete packages.', + ); }); }); }); @@ -320,16 +290,6 @@ describe('Packages protection rules project settings', () => { describe('when modal button "primary" clicked', () => { const clickOnModalPrimaryBtn = () => findModal().vm.$emit('primary'); - it('hides modal "confirmation"', async () => { - await createComponentAndClickButtonDeleteInTableRow(); - - await clickOnModalPrimaryBtn(); - - await waitForPromises(); - - expect(findModal().exists()).toBe(false); - }); - it('disables the button when graphql mutation is executed', async () => { await createComponentAndClickButtonDeleteInTableRow(); @@ -435,32 +395,6 @@ describe('Packages protection rules project settings', () => { expect($toast.show).toHaveBeenCalledWith('Package protection rule deleted.'); }); }); - - // eslint-disable-next-line jest/no-disabled-tests - describe.skip('when modal button "cancel" clicked', () => { - const clickOnModalCancelBtn = () => findModal().vm.$emit('hide'); - - it('hides modal "confirmation"', async () => { - await createComponentAndClickButtonDeleteInTableRow(); - - await clickOnModalCancelBtn(); - - expect(findModal().exists()).toBe(false); - }); - }); - - // eslint-disable-next-line jest/no-disabled-tests - describe.skip('when modal button "close" clicked', () => { - const clickOnModalCloseBtn = () => findModal().vm.$emit('close'); - - it('hides modal "confirmation"', async () => { - await createComponentAndClickButtonDeleteInTableRow(); - - await clickOnModalCloseBtn(); - - expect(findModal().exists()).toBe(false); - }); - }); }); it('does not initially render package protection form', async () => { @@ -501,12 +435,14 @@ describe('Packages protection rules project settings', () => { }); describe('form "add protection rule"', () => { - let resolver; + let packagesProtectionRuleQueryResolver; beforeEach(async () => { - resolver = jest.fn().mockResolvedValue(packagesProtectionRuleQueryPayload()); + packagesProtectionRuleQueryResolver = jest + .fn() + .mockResolvedValue(packagesProtectionRuleQueryPayload()); - createComponent({ packagesProtectionRuleQueryResolver: resolver, mountFn: mountExtended }); + createComponent({ packagesProtectionRuleQueryResolver, mountFn: mountExtended }); await waitForPromises(); @@ -516,7 +452,7 @@ describe('Packages protection rules project settings', () => { it('handles event "submit"', async () => { await findProtectionRuleForm().vm.$emit('submit'); - expect(resolver).toHaveBeenCalledTimes(2); + expect(packagesProtectionRuleQueryResolver).toHaveBeenCalledTimes(2); expect(findProtectionRuleForm().exists()).toBe(false); expect(findAddProtectionRuleButton().attributes('disabled')).not.toBeDefined(); @@ -525,7 +461,7 @@ describe('Packages protection rules project settings', () => { it('handles event "cancel"', async () => { await findProtectionRuleForm().vm.$emit('cancel'); - expect(resolver).toHaveBeenCalledTimes(1); + expect(packagesProtectionRuleQueryResolver).toHaveBeenCalledTimes(1); expect(findProtectionRuleForm().exists()).toBe(false); expect(findAddProtectionRuleButton().attributes()).not.toHaveProperty('disabled'); -- GitLab From f77fa5b66b260a2c621b4f853ab73cb1724efdf6 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Tue, 13 Feb 2024 08:41:50 +0100 Subject: [PATCH 8/9] refactor: Apply suggestion from @pburdette Commit includes: - Applying small code adjustments / suggestions - Add dismuss handler for alert component - New test for alert message --- .../components/packages_protection_rules.vue | 20 ++++++--- .../packages_protection_rules_spec.js | 41 ++++++++++++++++--- 2 files changed, 49 insertions(+), 12 deletions(-) diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue index ad2f47a265c8d9..8238ed3a46a40a 100644 --- a/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue +++ b/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue @@ -61,7 +61,7 @@ export default { packageProtectionRulesQueryPaginationParams: { first: PAGINATION_DEFAULT_PER_PAGE }, deleteInProgress: false, deleteItem: null, - alertErrorMessage: null, + alertErrorMessage: '', protectionRuleDeletionInProgress: false, protectionRuleDeletionItem: null, }; @@ -146,17 +146,16 @@ export default { last: PAGINATION_DEFAULT_PER_PAGE, }; }, - showLoadingIcon(item) { + isButtonDisabled(item) { return this.protectionRuleDeletionItem === item && this.protectionRuleDeletionInProgress; }, showProtectionRuleDeletionConfirmModal(protectionRule) { this.protectionRuleDeletionItem = protectionRule; }, deleteProtectionRule(protectionRule) { - this.alertErrorMessage = null; + this.clearAlertMessage(); this.protectionRuleDeletionInProgress = true; - this.protectionRuleDeletionItem = protectionRule; return this.$apollo .mutate({ @@ -180,7 +179,11 @@ export default { this.protectionRuleDeletionInProgress = false; }); }, + clearAlertMessage() { + this.alertErrorMessage = ''; + }, }, + table: {}, fields: [ { key: 'col_1_package_name_pattern', @@ -237,7 +240,12 @@ export default { @submit="refetchProtectionRules" /> - + {{ alertErrorMessage }} @@ -259,7 +267,7 @@ export default { category="secondary" variant="danger" size="small" - :disabled="showLoadingIcon(item)" + :disabled="isButtonDisabled(item)" @click="showProtectionRuleDeletionConfirmModal(item)" >{{ __('Delete') }} diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/packages_protection_rules_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_protection_rules_spec.js index eb36ec58d5d63c..45088d49c8221b 100644 --- a/spec/frontend/packages_and_registries/settings/project/settings/components/packages_protection_rules_spec.js +++ b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_protection_rules_spec.js @@ -43,7 +43,6 @@ describe('Packages protection rules project settings', () => { const mountComponent = (mountFn = shallowMount, provide = defaultProvidedValues, config) => { wrapper = mountFn(PackagesProtectionRules, { - attachTo: document.getElementById('app'), stubs: { SettingsBlock, GlModal: true, @@ -65,6 +64,7 @@ describe('Packages protection rules project settings', () => { deletePackagesProtectionRuleMutationResolver = jest .fn() .mockResolvedValue(deletePackagesProtectionRuleMutationPayload()), + config = {}, } = {}) => { const requestHandlers = [ [packagesProtectionRuleQuery, packagesProtectionRuleQueryResolver], @@ -75,6 +75,7 @@ describe('Packages protection rules project settings', () => { mountComponent(mountFn, provide, { apolloProvider: fakeApollo, + ...config, }); }; @@ -318,9 +319,10 @@ describe('Packages protection rules project settings', () => { }); it('handles erroneous graphql mutation', async () => { + const alertErrorMessage = 'Client error message'; const deletePackagesProtectionRuleMutationResolver = jest .fn() - .mockRejectedValue(new Error('error')); + .mockRejectedValue(new Error(alertErrorMessage)); await createComponentAndClickButtonDeleteInTableRow({ deletePackagesProtectionRuleMutationResolver, @@ -331,16 +333,16 @@ describe('Packages protection rules project settings', () => { await waitForPromises(); expect(findAlert().isVisible()).toBe(true); - expect(findAlert().text()).toBe('error'); + expect(findAlert().text()).toBe(alertErrorMessage); }); it('handles graphql mutation with error response', async () => { - const serverErrorMessage = 'Server error message'; + const alertErrorMessage = 'Server error message'; const deletePackagesProtectionRuleMutationResolver = jest.fn().mockResolvedValue({ data: { deletePackagesProtectionRule: { packageProtectionRule: null, - errors: [serverErrorMessage], + errors: [alertErrorMessage], }, }, }); @@ -354,7 +356,7 @@ describe('Packages protection rules project settings', () => { await waitForPromises(); expect(findAlert().isVisible()).toBe(true); - expect(findAlert().text()).toBe(serverErrorMessage); + expect(findAlert().text()).toBe(alertErrorMessage); }); it('refetches package protection rules after successful graphql mutation', async () => { @@ -467,4 +469,31 @@ describe('Packages protection rules project settings', () => { expect(findAddProtectionRuleButton().attributes()).not.toHaveProperty('disabled'); }); }); + + describe('alert "errorMessage"', () => { + const findAlertButtonDismiss = () => wrapper.findByRole('button', { name: /dismiss/i }); + + it('renders alert and dismisses it correctly', async () => { + const alertErrorMessage = 'Error message'; + createComponent({ + mountFn: mountExtended, + config: { + data() { + return { + alertErrorMessage, + }; + }, + }, + }); + + await waitForPromises(); + + expect(findAlert().isVisible()).toBe(true); + expect(findAlert().text()).toBe(alertErrorMessage); + + await findAlertButtonDismiss().trigger('click'); + + expect(findAlert().exists()).toBe(false); + }); + }); }); -- GitLab From 776b453237810eed3a59adebac993a4476d1ccb3 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Tue, 13 Feb 2024 18:52:59 +0100 Subject: [PATCH 9/9] refactor: Hide column header 'Actions' to avoid accessibility issues Commit includes: - Fix for failing test in ci pipeline concerning accessibility compliance --- .../settings/project/components/packages_protection_rules.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue index 8238ed3a46a40a..152ce8474f2465 100644 --- a/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue +++ b/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue @@ -197,6 +197,7 @@ export default { { key: 'col_4_actions', label: '', + thClass: 'gl-display-none', tdClass: 'gl-w-15p', }, ], -- GitLab