From 1e0dab82de7d104ce0d39bb78e6a13bfcfae504f Mon Sep 17 00:00:00 2001 From: Zamir Martins Filho Date: Mon, 14 Feb 2022 16:02:52 -0500 Subject: [PATCH 1/3] Add approvers select dropdown which allows users and groups to be added as approvers. EE: true --- .../approvers_select_dropdown.vue | 163 +++++++++++++ .../policy_action_builder.vue | 21 +- .../approvers_select_dropdown_spec.js | 220 ++++++++++++++++++ .../policy_action_builder_spec.js | 37 ++- locale/gitlab.pot | 9 + 5 files changed, 448 insertions(+), 2 deletions(-) create mode 100644 ee/app/assets/javascripts/threat_monitoring/components/policy_editor/approvers_select_dropdown.vue create mode 100644 ee/spec/frontend/threat_monitoring/components/policy_editor/approvers_select_dropdown_spec.js diff --git a/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/approvers_select_dropdown.vue b/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/approvers_select_dropdown.vue new file mode 100644 index 00000000000000..d9ceff100af92a --- /dev/null +++ b/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/approvers_select_dropdown.vue @@ -0,0 +1,163 @@ + + + diff --git a/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/scan_result_policy/policy_action_builder.vue b/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/scan_result_policy/policy_action_builder.vue index c1fd280427af1f..f9f96fed92bb67 100644 --- a/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/scan_result_policy/policy_action_builder.vue +++ b/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/scan_result_policy/policy_action_builder.vue @@ -9,13 +9,15 @@ import { } from '@gitlab/ui'; import { s__ } from '~/locale'; import { AVATAR_SHAPE_OPTION_CIRCLE, AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants'; -import { groupApprovers, decomposeApprovers, USER_TYPE } from './lib/actions'; +import ApproversSelectDropdown from '../approvers_select_dropdown.vue'; +import { groupApprovers, decomposeApprovers, groupIds, userIds, USER_TYPE } from './lib/actions'; export default { components: { GlSprintf, GlForm, GlFormInput, + ApproversSelectDropdown, GlToken, GlAvatarLabeled, }, @@ -39,6 +41,14 @@ export default { approvers: groupApprovers(this.existingApprovers), }; }, + computed: { + groupIds() { + return groupIds(this.approvers); + }, + userIds() { + return userIds(this.approvers); + }, + }, watch: { approvers(values) { this.action = decomposeApprovers(this.action, values); @@ -59,6 +69,9 @@ export default { (approver) => approver.type !== removedApprover.type || approver.id !== removedApprover.id, ); }, + selectedApprover(approver) { + this.approvers.push(approver); + }, avatarShape(approver) { return this.isUser(approver) ? AVATAR_SHAPE_OPTION_CIRCLE : AVATAR_SHAPE_OPTION_RECT; }, @@ -117,6 +130,12 @@ export default { :alt="approver.name" /> + diff --git a/ee/spec/frontend/threat_monitoring/components/policy_editor/approvers_select_dropdown_spec.js b/ee/spec/frontend/threat_monitoring/components/policy_editor/approvers_select_dropdown_spec.js new file mode 100644 index 00000000000000..fe959263252b6f --- /dev/null +++ b/ee/spec/frontend/threat_monitoring/components/policy_editor/approvers_select_dropdown_spec.js @@ -0,0 +1,220 @@ +import { + GlDropdown, + GlSearchBoxByType, + GlSkeletonLoader, + GlAvatarLabeled, + GlDropdownItem, +} from '@gitlab/ui'; +import { mount, shallowMount } from '@vue/test-utils'; +import MockAdapter from 'axios-mock-adapter'; +import ApproversSelectDropdown from 'ee/threat_monitoring/components/policy_editor/approvers_select_dropdown.vue'; +import axios from '~/lib/utils/axios_utils'; +import waitForPromises from 'helpers/wait_for_promises'; +import Api from 'ee/api'; + +const USER_DATA_RAW = { + id: 1, + name: 'userName', + username: 'userName', + state: 'active', + avatar_url: '', + web_url: '', +}; +const GROUP_DATA_RAW = { + id: 1, + name: 'name', + full_path: 'full_path', + full_name: 'full_name', + avatar_url: '', + web_url: '', +}; + +const USER_DATA = { ...USER_DATA_RAW, type: 'user' }; +const GROUP_DATA = { ...GROUP_DATA_RAW, type: 'group' }; + +const AVATAR_USER_DATA = { + alt: 'userName', + 'entity-name': 'userName', + label: 'userName', + shape: 'circle', + sublabel: '@userName', + labellink: '', + size: '32', + src: '', + sublabellink: '', +}; +const AVATAR_GROUP_DATA = { + alt: 'name', + 'entity-name': 'name', + label: 'full_name', + shape: 'rect', + sublabel: 'full_path', + labellink: '', + size: '32', + src: '', + sublabellink: '', +}; + +const SKIP_GROUP_IDS = [2]; +const SKIP_USER_IDS = [3]; + +const PROJECT_ID = '1'; + +describe('ApproversSelectDropdown', () => { + let mock; + + let wrapper; + + const factory = (propsData = {}, data = {}, mountFunc = shallowMount) => { + wrapper = mountFunc(ApproversSelectDropdown, { + propsData: { + skipGroupIds: SKIP_GROUP_IDS, + skipUserIds: SKIP_USER_IDS, + ...propsData, + }, + data() { + return { + ...data, + }; + }, + provide: { + projectId: PROJECT_ID, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); + const findAllAvatarLabeled = () => wrapper.findAllComponents(GlAvatarLabeled); + const findDropdown = () => wrapper.findComponent(GlDropdown); + const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType); + const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader); + const findNoResultText = () => wrapper.find('[data-testid="no-results"]'); + + describe.each` + isLoading | showsNoResult | showsSkeletonLoader | showsItems | existingItems + ${false} | ${true} | ${false} | ${false} | ${[]} + ${false} | ${false} | ${false} | ${true} | ${[USER_DATA]} + ${true} | ${false} | ${true} | ${false} | ${[]} + ${true} | ${false} | ${true} | ${false} | ${[USER_DATA]} + `( + 'when loading equals to $isLoading and there are $existingItems.length existing items', + ({ isLoading, showsNoResult, showsSkeletonLoader, showsItems, existingItems }) => { + beforeEach(() => { + factory({}, { loading: isLoading, returnedItems: existingItems }); + }); + + it(`renders the dropdown ${showsItems ? 'with' : 'without'} items`, () => { + expect(findDropdown().exists()).toBe(true); + expect(findAllDropdownItems().exists()).toBe(showsItems); + }); + + it(`does ${showsNoResult ? '' : 'not'} render no matching results text`, () => { + expect(findNoResultText().exists()).toBe(showsNoResult); + }); + + it(`does ${showsSkeletonLoader ? '' : 'not'} render skeleton loader`, () => { + expect(findSkeletonLoader().exists()).toBe(showsSkeletonLoader); + }); + + it(`does ${showsItems ? '' : 'not'} render the avatar labeled`, () => { + expect(findAllAvatarLabeled().exists()).toBe(showsItems); + }); + }, + ); + + describe.each` + existingItems | expectedAvatarAttributes + ${[USER_DATA]} | ${[AVATAR_USER_DATA]} + ${[GROUP_DATA]} | ${[AVATAR_GROUP_DATA]} + ${[USER_DATA, GROUP_DATA]} | ${[AVATAR_USER_DATA, AVATAR_GROUP_DATA]} + `('with user and/or group data', ({ existingItems, expectedAvatarAttributes }) => { + it('renders the avatar labeled for $existingItems[0].type', () => { + factory({}, { returnedItems: existingItems }); + + expect(findAllAvatarLabeled()).toHaveLength(existingItems.length); + expect(findAllAvatarLabeled().wrappers.map((avatar) => avatar.attributes())).toEqual( + expectedAvatarAttributes, + ); + }); + }); + + it('triggers selected when a dropdown item is clicked', async () => { + factory({}, { returnedItems: [USER_DATA] }); + + await findAllDropdownItems().at(0).vm.$emit('click'); + + expect(wrapper.emitted().selected).toEqual([[USER_DATA]]); + }); + + describe('when search box input is changed', () => { + beforeEach(() => { + mock = new MockAdapter(axios); + mock.onGet('/api/undefined/projects/1/users').reply(200, [USER_DATA_RAW]); + mock.onGet('/api/undefined/projects/1/groups.json').reply(200, [GROUP_DATA_RAW]); + factory({}, {}, mount); + }); + + afterEach(() => { + mock.restore(); + }); + + it('updates the dropdown items', async () => { + expect(findAllDropdownItems()).toHaveLength(0); + + await findSearchBox().vm.$emit('input', 'name'); + await waitForPromises(); + + expect(findAllDropdownItems()).toHaveLength(2); + }); + + it('renders skeleton loader before returning the promises', async () => { + expect(findSkeletonLoader().exists()).toBe(false); + + await findSearchBox().vm.$emit('input', 'name'); + + expect(findSkeletonLoader().exists()).toBe(true); + }); + + it('triggers api calls using skipGroupIds and skipUserIds', async () => { + const searchTerm = 'name'; + jest.spyOn(Api, 'projectUsers'); + jest.spyOn(Api, 'projectGroups'); + + await findSearchBox().vm.$emit('input', searchTerm); + + expect(Api.projectGroups).toHaveBeenCalledWith( + PROJECT_ID, + expect.objectContaining({ skip_groups: SKIP_GROUP_IDS }), + ); + expect(Api.projectUsers).toHaveBeenCalledWith( + PROJECT_ID, + searchTerm, + expect.objectContaining({ skip_users: SKIP_USER_IDS }), + ); + }); + }); + + describe('when the Api response is a 404', () => { + beforeEach(() => { + mock = new MockAdapter(axios); + mock.onGet('/api/undefined/projects/1/users').reply(404); + mock.onGet('/api/undefined/projects/1/groups.json').reply(404); + factory({}, {}, mount); + }); + + afterEach(() => { + mock.restore(); + }); + + it('emits an apiError with the error message', async () => { + await findSearchBox().vm.$emit('input', 'name'); + await waitForPromises(); + + expect(wrapper.emitted().apiError).toEqual([['Request failed with status code 404']]); + }); + }); +}); diff --git a/ee/spec/frontend/threat_monitoring/components/policy_editor/scan_result_policy/policy_action_builder_spec.js b/ee/spec/frontend/threat_monitoring/components/policy_editor/scan_result_policy/policy_action_builder_spec.js index 7ea45de359046b..dfaf0267bdc1a4 100644 --- a/ee/spec/frontend/threat_monitoring/components/policy_editor/scan_result_policy/policy_action_builder_spec.js +++ b/ee/spec/frontend/threat_monitoring/components/policy_editor/scan_result_policy/policy_action_builder_spec.js @@ -2,6 +2,7 @@ import { GlFormInput, GlToken } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import { nextTick } from 'vue'; import PolicyActionBuilder from 'ee/threat_monitoring/components/policy_editor/scan_result_policy/policy_action_builder.vue'; +import ApproversSelectDropdown from 'ee/threat_monitoring/components/policy_editor/approvers_select_dropdown.vue'; const APPROVER_1 = { id: 1, @@ -21,6 +22,15 @@ const APPROVER_2 = { avatar_url: '', }; +const NEW_APPROVER = { + id: 3, + name: 'name2', + state: 'active', + username: 'username2', + web_url: '', + avatar_url: '', +}; + const APPROVERS = [APPROVER_1, APPROVER_2]; const APPROVERS_IDS = APPROVERS.map((approver) => approver.id); @@ -48,13 +58,15 @@ describe('PolicyActionBuilder', () => { const findApprovalsRequiredInput = () => wrapper.findComponent(GlFormInput); const findAllGlTokens = () => wrapper.findAllComponents(GlToken); + const findAddApproverDropdown = () => wrapper.findComponent(ApproversSelectDropdown); - it('renders approvals required form input, gl-tokens', async () => { + it('renders approvals required form input, gl-tokens and add approver button', async () => { factory(); await nextTick(); expect(findApprovalsRequiredInput().exists()).toBe(true); expect(findAllGlTokens().length).toBe(APPROVERS.length); + expect(findAddApproverDropdown().exists()).toBe(true); }); it('triggers an update when changing approvals required', async () => { @@ -94,4 +106,27 @@ describe('PolicyActionBuilder', () => { ]); expect(findAllGlTokens()).toHaveLength(approversLengthMinusOne); }); + + it('adds one approver gl-token when triggering a new user is selected', async () => { + factory(); + await nextTick(); + + const allGlTokens = findAllGlTokens(); + const approversLengthPlusOne = APPROVERS.length + 1; + + expect(allGlTokens.length).toBe(APPROVERS.length); + + await findAddApproverDropdown().vm.$emit('selected', { ...NEW_APPROVER, type: 'user' }); + + expect(wrapper.emitted().changed).toEqual([ + [ + { + approvals_required: ACTION.approvals_required, + user_approvers_ids: [APPROVER_1.id, APPROVER_2.id, NEW_APPROVER.id], + group_approvers_ids: [], + }, + ], + ]); + expect(findAllGlTokens()).toHaveLength(approversLengthPlusOne); + }); }); diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 60f59211b289ae..1132ecef555d7c 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -4633,6 +4633,15 @@ msgstr "" msgid "Approvers from private group(s) not shown" msgstr "" +msgid "ApproversSelectDropdown|No matching results" +msgstr "" + +msgid "ApproversSelectDropdown|Search users or groups" +msgstr "" + +msgid "ApproversSelectDropdown|add an approver" +msgstr "" + msgid "Apr" msgstr "" -- GitLab From a43a52e51b17570c7d5e4bf8b2bca20ebc952476 Mon Sep 17 00:00:00 2001 From: Zamir Martins Filho Date: Thu, 17 Feb 2022 11:18:17 -0500 Subject: [PATCH 2/3] Address reviewer comments by fixing inconsistency between chrome and firefox, triggering fetch on mount and removing search term related limit. --- .../approvers_select_dropdown.vue | 16 ++--- .../pages/security/policy_editor.scss | 3 + .../approvers_select_dropdown_spec.js | 58 ++++++++----------- .../policy_action_builder_spec.js | 14 +++++ 4 files changed, 47 insertions(+), 44 deletions(-) create mode 100644 ee/app/assets/stylesheets/pages/security/policy_editor.scss diff --git a/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/approvers_select_dropdown.vue b/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/approvers_select_dropdown.vue index d9ceff100af92a..fb4519e2702a3d 100644 --- a/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/approvers_select_dropdown.vue +++ b/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/approvers_select_dropdown.vue @@ -12,7 +12,6 @@ import Api from 'ee/api'; import { AVATAR_SHAPE_OPTION_CIRCLE, AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants'; import { groupApprovers, USER_TYPE } from './scan_result_policy/lib/actions'; -const MINIMUM_ALLOWED = 1; const DEVELOPER_ACCESS_LEVEL = 30; const DEBOUNCE_DELAY = 250; @@ -54,6 +53,9 @@ export default { return this.returnedItems.length > 0; }, }, + mounted() { + this.filterItems(''); + }, methods: { label(item) { return this.isUser(item) ? item.name : item.full_name; @@ -64,10 +66,6 @@ export default { avatarShape(item) { return this.isUser(item) ? AVATAR_SHAPE_OPTION_CIRCLE : AVATAR_SHAPE_OPTION_RECT; }, - onHide() { - this.searchValue = ''; - this.returnedItems = []; - }, isUser(item) { return item.type === USER_TYPE; }, @@ -94,7 +92,7 @@ export default { return [...response[0], ...response[1]]; }, async fetchGroups(term) { - const hasTerm = term.trim().length > MINIMUM_ALLOWED; + const hasTerm = term.trim().length > 0; return Api.projectGroups(this.projectId, { skip_groups: this.skipGroupIds, @@ -105,8 +103,7 @@ export default { }); }, async fetchUsers(term) { - const newTerm = term.trim().length > MINIMUM_ALLOWED ? term.trim() : ''; - return Api.projectUsers(this.projectId, newTerm, { + return Api.projectUsers(this.projectId, term, { skip_users: this.skipUserIds, }); }, @@ -120,13 +117,12 @@ export default { class="gl-max-w-full" toggle-class="gl-max-w-full gl-align-items-center gl-text-truncate" boundary="viewport" - @hide="onHide" > - - +
+ +
+
+ +
diff --git a/ee/app/assets/stylesheets/pages/security/policy_editor.scss b/ee/app/assets/stylesheets/pages/security/policy_editor.scss deleted file mode 100644 index 9d88894d70cf5a..00000000000000 --- a/ee/app/assets/stylesheets/pages/security/policy_editor.scss +++ /dev/null @@ -1,3 +0,0 @@ -.approvers-search-box .gl-form-input { - @include gl-overflow-auto; -} diff --git a/ee/spec/frontend/threat_monitoring/components/policy_editor/approvers_select_dropdown_spec.js b/ee/spec/frontend/threat_monitoring/components/policy_editor/approvers_select_dropdown_spec.js deleted file mode 100644 index e521f3319e1c96..00000000000000 --- a/ee/spec/frontend/threat_monitoring/components/policy_editor/approvers_select_dropdown_spec.js +++ /dev/null @@ -1,210 +0,0 @@ -import { - GlDropdown, - GlSearchBoxByType, - GlSkeletonLoader, - GlAvatarLabeled, - GlDropdownItem, -} from '@gitlab/ui'; -import { mount, shallowMount } from '@vue/test-utils'; -import MockAdapter from 'axios-mock-adapter'; -import ApproversSelectDropdown from 'ee/threat_monitoring/components/policy_editor/approvers_select_dropdown.vue'; -import axios from '~/lib/utils/axios_utils'; -import waitForPromises from 'helpers/wait_for_promises'; -import Api from 'ee/api'; - -const USER_DATA_RAW = { - id: 1, - name: 'userName', - username: 'userName', - state: 'active', - avatar_url: '', - web_url: '', -}; -const GROUP_DATA_RAW = { - id: 1, - name: 'name', - full_path: 'full_path', - full_name: 'full_name', - avatar_url: '', - web_url: '', -}; - -const USER_DATA = { ...USER_DATA_RAW, type: 'user' }; -const GROUP_DATA = { ...GROUP_DATA_RAW, type: 'group' }; - -const AVATAR_USER_DATA = { - alt: 'userName', - 'entity-name': 'userName', - label: 'userName', - shape: 'circle', - sublabel: '@userName', - labellink: '', - size: '32', - src: '', - sublabellink: '', -}; -const AVATAR_GROUP_DATA = { - alt: 'name', - 'entity-name': 'name', - label: 'full_name', - shape: 'rect', - sublabel: 'full_path', - labellink: '', - size: '32', - src: '', - sublabellink: '', -}; - -const SKIP_GROUP_IDS = [2]; -const SKIP_USER_IDS = [3]; - -const PROJECT_ID = '1'; - -describe('ApproversSelectDropdown', () => { - let mock; - - let wrapper; - - const factory = (propsData = {}, data = {}, mountFunc = shallowMount) => { - wrapper = mountFunc(ApproversSelectDropdown, { - propsData: { - skipGroupIds: SKIP_GROUP_IDS, - skipUserIds: SKIP_USER_IDS, - ...propsData, - }, - data() { - return { - ...data, - }; - }, - provide: { - projectId: PROJECT_ID, - }, - }); - }; - - beforeEach(() => { - mock = new MockAdapter(axios); - mock.onGet('/api/undefined/projects/1/users').reply(200, [USER_DATA_RAW]); - mock.onGet('/api/undefined/projects/1/groups.json').reply(200, [GROUP_DATA_RAW]); - }); - - afterEach(() => { - wrapper.destroy(); - mock.restore(); - }); - - const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); - const findAllAvatarLabeled = () => wrapper.findAllComponents(GlAvatarLabeled); - const findDropdown = () => wrapper.findComponent(GlDropdown); - const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType); - const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader); - const findNoResultText = () => wrapper.find('[data-testid="no-results"]'); - - describe.each` - showsNoResult | showsItems | existingUsers | existingGroups - ${true} | ${false} | ${[]} | ${[]} - ${false} | ${true} | ${[USER_DATA_RAW]} | ${[]} - ${false} | ${true} | ${[]} | ${[GROUP_DATA_RAW]} - `( - 'when $showsNoResult and $showsItems there are $existingItems.length existing items', - ({ showsNoResult, showsItems, existingUsers, existingGroups }) => { - beforeEach(async () => { - mock = new MockAdapter(axios); - mock.onGet('/api/undefined/projects/1/users').reply(200, existingUsers); - mock.onGet('/api/undefined/projects/1/groups.json').reply(200, existingGroups); - factory(); - await waitForPromises(); - }); - - it(`renders the dropdown ${showsItems ? 'with' : 'without'} items`, () => { - expect(findDropdown().exists()).toBe(true); - expect(findAllDropdownItems().exists()).toBe(showsItems); - }); - - it(`does ${showsNoResult ? '' : 'not'} render no matching results text`, () => { - expect(findNoResultText().exists()).toBe(showsNoResult); - }); - - it(`does ${showsItems ? '' : 'not'} render the avatar labeled`, () => { - expect(findAllAvatarLabeled().exists()).toBe(showsItems); - }); - }, - ); - - describe.each` - existingItems | expectedAvatarAttributes - ${[USER_DATA]} | ${[AVATAR_USER_DATA]} - ${[GROUP_DATA]} | ${[AVATAR_GROUP_DATA]} - ${[USER_DATA, GROUP_DATA]} | ${[AVATAR_USER_DATA, AVATAR_GROUP_DATA]} - `('with user and/or group data', ({ existingItems, expectedAvatarAttributes }) => { - it('renders the avatar labeled for $existingItems[0].type', () => { - factory({}, { returnedItems: existingItems }); - - expect(findAllAvatarLabeled()).toHaveLength(existingItems.length); - expect(findAllAvatarLabeled().wrappers.map((avatar) => avatar.attributes())).toEqual( - expectedAvatarAttributes, - ); - }); - }); - - it('triggers selected when a dropdown item is clicked', async () => { - factory({}, { returnedItems: [USER_DATA] }); - - await findAllDropdownItems().at(0).vm.$emit('click'); - - expect(wrapper.emitted().selected).toEqual([[USER_DATA]]); - }); - - describe('when search box input is changed', () => { - beforeEach(async () => { - factory({}, {}, mount); - await waitForPromises(); - }); - - it('renders skeleton loader before returning the promises', async () => { - expect(findSkeletonLoader().exists()).toBe(false); - - await findSearchBox().vm.$emit('input', 'name'); - - expect(findSkeletonLoader().exists()).toBe(true); - }); - - it('triggers api calls using skipGroupIds and skipUserIds', async () => { - const searchTerm = 'name'; - jest.spyOn(Api, 'projectUsers'); - jest.spyOn(Api, 'projectGroups'); - - await findSearchBox().vm.$emit('input', searchTerm); - - expect(Api.projectGroups).toHaveBeenCalledWith( - PROJECT_ID, - expect.objectContaining({ skip_groups: SKIP_GROUP_IDS }), - ); - expect(Api.projectUsers).toHaveBeenCalledWith( - PROJECT_ID, - searchTerm, - expect.objectContaining({ skip_users: SKIP_USER_IDS }), - ); - }); - }); - - describe('when the Api response is a 404', () => { - beforeEach(() => { - mock = new MockAdapter(axios); - mock.onGet('/api/undefined/projects/1/users').reply(404); - mock.onGet('/api/undefined/projects/1/groups.json').reply(404); - factory({}, {}, mount); - }); - - it('emits an apiError with the error message', async () => { - await findSearchBox().vm.$emit('input', 'name'); - await waitForPromises(); - - expect(wrapper.emitted().apiError).toEqual([ - ['Request failed with status code 404'], - ['Request failed with status code 404'], - ]); - }); - }); -}); diff --git a/ee/spec/frontend/threat_monitoring/components/policy_editor/scan_result_policy/policy_action_builder_spec.js b/ee/spec/frontend/threat_monitoring/components/policy_editor/scan_result_policy/policy_action_builder_spec.js index 939e3dc20a993e..f7fc701d148229 100644 --- a/ee/spec/frontend/threat_monitoring/components/policy_editor/scan_result_policy/policy_action_builder_spec.js +++ b/ee/spec/frontend/threat_monitoring/components/policy_editor/scan_result_policy/policy_action_builder_spec.js @@ -1,9 +1,11 @@ -import { GlFormInput, GlToken } from '@gitlab/ui'; +import { GlFormInput } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import { nextTick } from 'vue'; import MockAdapter from 'axios-mock-adapter'; import PolicyActionBuilder from 'ee/threat_monitoring/components/policy_editor/scan_result_policy/policy_action_builder.vue'; -import ApproversSelectDropdown from 'ee/threat_monitoring/components/policy_editor/approvers_select_dropdown.vue'; +import ApproversSelect from 'ee/approvals/components/approvers_select.vue'; +import ApproversList from 'ee/approvals/components/approvers_list.vue'; +import ApproversListItem from 'ee/approvals/components/approvers_list_item.vue'; import axios from '~/lib/utils/axios_utils'; const APPROVER_1 = { @@ -60,8 +62,9 @@ describe('PolicyActionBuilder', () => { }; const findApprovalsRequiredInput = () => wrapper.findComponent(GlFormInput); - const findAllGlTokens = () => wrapper.findAllComponents(GlToken); - const findAddApproverDropdown = () => wrapper.findComponent(ApproversSelectDropdown); + const findApproversList = () => wrapper.findComponent(ApproversList); + const findAddApproversSelect = () => wrapper.findComponent(ApproversSelect); + const findAllApproversItem = () => wrapper.findAllComponents(ApproversListItem); beforeEach(() => { mock = new MockAdapter(axios); @@ -74,13 +77,13 @@ describe('PolicyActionBuilder', () => { mock.restore(); }); - it('renders approvals required form input, gl-tokens and add approver button', async () => { + it('renders approvals required form input, approvers list and approvers select', async () => { factory(); await nextTick(); expect(findApprovalsRequiredInput().exists()).toBe(true); - expect(findAllGlTokens().length).toBe(APPROVERS.length); - expect(findAddApproverDropdown().exists()).toBe(true); + expect(findApproversList().exists()).toBe(true); + expect(findAddApproversSelect().exists()).toBe(true); }); it('triggers an update when changing approvals required', async () => { @@ -97,17 +100,17 @@ describe('PolicyActionBuilder', () => { ]); }); - it('removes one approver when triggering a gl-token', async () => { + it('removes one approver when triggering a remove button click', async () => { factory(); await nextTick(); - const allGlTokens = findAllGlTokens(); - const glToken = allGlTokens.at(0); + const allApproversItems = findAllApproversItem(); + const approversItem = allApproversItems.at(0); const approversLengthMinusOne = APPROVERS.length - 1; - expect(allGlTokens.length).toBe(APPROVERS.length); + expect(allApproversItems.length).toBe(APPROVERS.length); - await glToken.vm.$emit('close', { ...APPROVER_1, type: 'user' }); + await approversItem.vm.$emit('remove', { ...APPROVER_1, type: 'user' }); expect(wrapper.emitted().changed).toEqual([ [ @@ -118,19 +121,19 @@ describe('PolicyActionBuilder', () => { }, ], ]); - expect(findAllGlTokens()).toHaveLength(approversLengthMinusOne); + expect(findAllApproversItem()).toHaveLength(approversLengthMinusOne); }); - it('adds one approver gl-token when triggering a new user is selected', async () => { + it('adds one approver when triggering a new user is selected', async () => { factory(); await nextTick(); - const allGlTokens = findAllGlTokens(); + const allApproversItems = findAllApproversItem(); const approversLengthPlusOne = APPROVERS.length + 1; - expect(allGlTokens.length).toBe(APPROVERS.length); + expect(allApproversItems.length).toBe(APPROVERS.length); - await findAddApproverDropdown().vm.$emit('selected', { ...NEW_APPROVER, type: 'user' }); + await findAddApproversSelect().vm.$emit('input', [{ ...NEW_APPROVER, type: 'user' }]); expect(wrapper.emitted().changed).toEqual([ [ @@ -141,6 +144,6 @@ describe('PolicyActionBuilder', () => { }, ], ]); - expect(findAllGlTokens()).toHaveLength(approversLengthPlusOne); + expect(findAllApproversItem()).toHaveLength(approversLengthPlusOne); }); }); diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 1132ecef555d7c..4097382253a4fe 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -4633,15 +4633,6 @@ msgstr "" msgid "Approvers from private group(s) not shown" msgstr "" -msgid "ApproversSelectDropdown|No matching results" -msgstr "" - -msgid "ApproversSelectDropdown|Search users or groups" -msgstr "" - -msgid "ApproversSelectDropdown|add an approver" -msgstr "" - msgid "Apr" msgstr "" @@ -31974,7 +31965,7 @@ msgstr "" msgid "ScanResultPolicy|%{ifLabelStart}if%{ifLabelEnd} %{scanners} scan in an open merge request targeting the %{branches} branch(es) finds %{vulnerabilitiesAllowed} or more %{severities} vulnerabilities that are %{vulnerabilityStates}" msgstr "" -msgid "ScanResultPolicy|%{thenLabelStart}Then%{thenLabelEnd} Require approval from %{approvalsRequired} of the following approvers: %{approvers}" +msgid "ScanResultPolicy|%{thenLabelStart}Then%{thenLabelEnd} Require approval from %{approvalsRequired} of the following approvers:" msgstr "" msgid "ScanResultPolicy|add an approver" -- GitLab