diff --git a/ee/app/assets/javascripts/security_dashboard/components/shared/filtered_search/components/search_suggestion.vue b/ee/app/assets/javascripts/security_dashboard/components/shared/filtered_search/components/search_suggestion.vue index 4d40e877de3e8ffca35680337e3313f89f7b4106..f07705420170d89046883ae87a454d7d67d148aa 100644 --- a/ee/app/assets/javascripts/security_dashboard/components/shared/filtered_search/components/search_suggestion.vue +++ b/ee/app/assets/javascripts/security_dashboard/components/shared/filtered_search/components/search_suggestion.vue @@ -1,10 +1,11 @@ <script> -import { GlFilteredSearchSuggestion, GlIcon } from '@gitlab/ui'; +import { GlFilteredSearchSuggestion, GlIcon, GlTruncate } from '@gitlab/ui'; export default { components: { GlFilteredSearchSuggestion, GlIcon, + GlTruncate, }, props: { /** @@ -31,6 +32,11 @@ export default { type: Boolean, required: true, }, + truncate: { + type: Boolean, + required: false, + default: false, + }, }, }; </script> @@ -43,7 +49,8 @@ export default { :class="{ 'gl-invisible': !selected }" :data-testid="`${name}-icon-${value}`" /> - {{ text }} + <gl-truncate v-if="truncate" position="middle" :text="text" /> + <template v-else>{{ text }}</template> </div> </gl-filtered-search-suggestion> </template> diff --git a/ee/app/assets/javascripts/security_dashboard/components/shared/filtered_search/tokens/activity_token.vue b/ee/app/assets/javascripts/security_dashboard/components/shared/filtered_search/tokens/activity_token.vue index dd7e1fad91090ed074ee351ec68e6f10c6be97a9..6fc5d2ffc68f62fbd1b71faa638a559e79befce6 100644 --- a/ee/app/assets/javascripts/security_dashboard/components/shared/filtered_search/tokens/activity_token.vue +++ b/ee/app/assets/javascripts/security_dashboard/components/shared/filtered_search/tokens/activity_token.vue @@ -250,7 +250,7 @@ export default { @complete="emitFiltersChanged" > <template #view> - {{ toggleText }} + <span data-testid="activity-token-placeholder">{{ toggleText }}</span> </template> <template #suggestions> <template v-for="(group, index) in activityTokenGroups"> diff --git a/ee/app/assets/javascripts/security_dashboard/components/shared/filtered_search/tokens/cluster_token.vue b/ee/app/assets/javascripts/security_dashboard/components/shared/filtered_search/tokens/cluster_token.vue index 6463c7b89bf91a73722587833481a4e4bffa5fc7..812b8590acf9743c4cd99c63e08e2867df960af3 100644 --- a/ee/app/assets/javascripts/security_dashboard/components/shared/filtered_search/tokens/cluster_token.vue +++ b/ee/app/assets/javascripts/security_dashboard/components/shared/filtered_search/tokens/cluster_token.vue @@ -155,7 +155,7 @@ export default { @complete="emitFiltersChanged" > <template #view> - {{ toggleText }} + <span data-testid="cluster-token-placeholder">{{ toggleText }}</span> </template> <template #suggestions> <gl-loading-icon v-if="isLoading" size="sm" /> diff --git a/ee/app/assets/javascripts/security_dashboard/components/shared/filtered_search/tokens/image_token.vue b/ee/app/assets/javascripts/security_dashboard/components/shared/filtered_search/tokens/image_token.vue index 0d375d1858f16fc7bc9727391a8233bd62b01e7b..13c11fdc0834c6cd2149209c408ee11a3a95c482 100644 --- a/ee/app/assets/javascripts/security_dashboard/components/shared/filtered_search/tokens/image_token.vue +++ b/ee/app/assets/javascripts/security_dashboard/components/shared/filtered_search/tokens/image_token.vue @@ -1,28 +1,21 @@ <script> -import { - GlIcon, - GlFilteredSearchToken, - GlFilteredSearchSuggestion, - GlLoadingIcon, - GlTruncate, -} from '@gitlab/ui'; +import { GlFilteredSearchToken, GlLoadingIcon } from '@gitlab/ui'; import { getSelectedOptionsText } from '~/lib/utils/listbox_helpers'; import { s__ } from '~/locale'; import { createAlert } from '~/alert'; import agentImagesQuery from 'ee/security_dashboard/graphql/queries/agent_images.query.graphql'; import projectImagesQuery from 'ee/security_dashboard/graphql/queries/project_images.query.graphql'; +import SearchSuggestion from '../components/search_suggestion.vue'; import QuerystringSync from '../../filters/querystring_sync.vue'; import { ALL_ID as ALL_IMAGES_VALUE } from '../../filters/constants'; import eventHub from '../event_hub'; export default { components: { - GlIcon, GlFilteredSearchToken, - GlFilteredSearchSuggestion, GlLoadingIcon, - GlTruncate, QuerystringSync, + SearchSuggestion, }, inject: { agentName: { default: '' }, @@ -163,22 +156,16 @@ export default { </template> <template #suggestions> <gl-loading-icon v-if="isLoading" size="sm" /> - <gl-filtered-search-suggestion + <search-suggestion v-for="image in items" v-else :key="image.value" :value="image.value" - > - <div class="gl-flex gl-items-center"> - <gl-icon - name="check" - class="gl-mr-3 gl-shrink-0 gl-text-gray-700" - :class="{ 'gl-invisible': !isImageSelected(image.value) }" - :data-testid="`image-icon-${image.value}`" - /> - <gl-truncate position="middle" :text="image.text" data-testid="truncate-image" /> - </div> - </gl-filtered-search-suggestion> + :text="image.text" + :selected="isImageSelected(image.value)" + :data-testid="`suggestion-${image.value}`" + truncate + /> </template> </gl-filtered-search-token> </querystring-sync> diff --git a/ee/app/assets/javascripts/security_dashboard/components/shared/filtered_search/tokens/status_token.vue b/ee/app/assets/javascripts/security_dashboard/components/shared/filtered_search/tokens/status_token.vue index 984724d000d0de10fe0fe24adf1d584f03009ee9..5841d1777047ac0f213f0b9ce8d66d70b9080e66 100644 --- a/ee/app/assets/javascripts/security_dashboard/components/shared/filtered_search/tokens/status_token.vue +++ b/ee/app/assets/javascripts/security_dashboard/components/shared/filtered_search/tokens/status_token.vue @@ -186,7 +186,7 @@ export default { @complete="emitFiltersChanged" > <template #view> - {{ toggleText }} + <span data-testid="status-token-placeholder">{{ toggleText }}</span> </template> <template #suggestions> <gl-dropdown-section-header>{{ $options.i18n.statusLabel }}</gl-dropdown-section-header> diff --git a/ee/spec/frontend/security_dashboard/components/shared/filtered_search/components/search_suggestion_spec.js b/ee/spec/frontend/security_dashboard/components/shared/filtered_search/components/search_suggestion_spec.js index 871ee4fa3d96bd4f97ff55f5c996cd4615c20329..134d043ab4b8f8b492657f496aaec0aa28f866f4 100644 --- a/ee/spec/frontend/security_dashboard/components/shared/filtered_search/components/search_suggestion_spec.js +++ b/ee/spec/frontend/security_dashboard/components/shared/filtered_search/components/search_suggestion_spec.js @@ -1,17 +1,18 @@ -import { GlFilteredSearchSuggestion } from '@gitlab/ui'; +import { GlFilteredSearchSuggestion, GlTruncate } from '@gitlab/ui'; import SearchSuggestion from 'ee/security_dashboard/components/shared/filtered_search/components/search_suggestion.vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; describe('Search Suggestion', () => { let wrapper; - const createWrapper = ({ text, name, value, selected }) => { + const createWrapper = ({ text, name, value, selected, truncate }) => { wrapper = shallowMountExtended(SearchSuggestion, { propsData: { text, name, value, selected, + truncate, }, }); }; @@ -35,4 +36,18 @@ describe('Search Suggestion', () => { expect(findGlSearchSuggestion().props('value')).toBe('my_value'); expect(wrapper.findByTestId('test-icon-my_value').classes('gl-invisible')).toBe(!selected); }); + + it.each` + truncate + ${true} + ${false} + `('truncates the text when `truncate` property is $truncate', ({ truncate }) => { + createWrapper({ text: 'My text', value: 'My value', selected: false, truncate }); + expect(wrapper.findComponent(GlTruncate).exists()).toBe(truncate); + }); + + it('truncates the text when `truncate` property is $truncate', () => { + createWrapper({ text: 'My text', value: 'My value', selected: false, truncate: true }); + expect(wrapper.findComponent(GlTruncate).props('text')).toBe('My text'); + }); }); diff --git a/ee/spec/frontend/security_dashboard/components/shared/filtered_search/tokens/activity_token_spec.js b/ee/spec/frontend/security_dashboard/components/shared/filtered_search/tokens/activity_token_spec.js index a570bb060b7ff794c508bca9abd6860fb91e6ac0..4cc49f7df4b4cc031a3e7fc53e997521959179b2 100644 --- a/ee/spec/frontend/security_dashboard/components/shared/filtered_search/tokens/activity_token_spec.js +++ b/ee/spec/frontend/security_dashboard/components/shared/filtered_search/tokens/activity_token_spec.js @@ -1,4 +1,4 @@ -import { GlFilteredSearchToken, GlBadge } from '@gitlab/ui'; +import { GlFilteredSearchToken, GlDropdownSectionHeader, GlBadge } from '@gitlab/ui'; import Vue, { nextTick } from 'vue'; import VueRouter from 'vue-router'; import ActivityToken from 'ee/security_dashboard/components/shared/filtered_search/tokens/activity_token.vue'; @@ -76,23 +76,9 @@ describe('ActivityToken', () => { }; describe('default view', () => { - const findViewSlot = () => wrapper.findByTestId('slot-view'); const findAllBadges = () => wrapper.findAllComponents(GlBadge); const createWrapperWithAbility = ({ resolveVulnerabilityWithAi } = {}) => { createWrapper({ - stubs: { - GlFilteredSearchToken: stubComponent(GlFilteredSearchToken, { - template: ` - <div> - <div data-testid="slot-view"> - <slot name="view"></slot> - </div> - <div data-testid="slot-suggestions"> - <slot name="suggestions"></slot> - </div> - </div>`, - }), - }, provide: { glFeatures: { vulnerabilityReportVrFilter: true, @@ -106,31 +92,34 @@ describe('ActivityToken', () => { it('shows the label', () => { createWrapperWithAbility(); - expect(findViewSlot().text()).toBe('Still detected'); + expect(findFilteredSearchToken().props('value')).toEqual({ + data: ['STILL_DETECTED'], + operator: '||', + }); + expect(wrapper.findByTestId('activity-token-placeholder').text()).toBe('Still detected'); }); const baseOptions = [ 'All activity', - 'Detection', // group header 'Still detected', 'No longer detected', - 'Issue', // group header 'Has issue', 'Does not have issue', - 'Merge Request', // group header 'Has merge request', 'Does not have merge request', - 'Solution available', // group header 'Has a solution', 'Does not have a solution', ]; const aiOptions = [ - 'GitLab Duo (AI)', // group header 'Vulnerability Resolution available', 'Vulnerability Resolution unavailable', ]; + const baseGroupHeaders = ['Detection', 'Issue', 'Merge Request', 'Solution available']; + + const aiGroupHeaders = ['GitLab Duo (AI)']; + it.each` resolveVulnerabilityWithAi | expectedOptions ${true} | ${[...baseOptions, ...aiOptions]} @@ -140,16 +129,26 @@ describe('ActivityToken', () => { ({ resolveVulnerabilityWithAi, expectedOptions }) => { createWrapperWithAbility({ resolveVulnerabilityWithAi }); - // All options are rendered in the #suggestions slot of GlFilteredSearchToken - const findDropdownOptions = () => wrapper.findByTestId('slot-suggestions'); + const findDropdownOptions = () => + wrapper.findAllComponents(SearchSuggestion).wrappers.map((c) => c.text()); + + expect(findDropdownOptions()).toEqual(expectedOptions); + }, + ); + + it.each` + resolveVulnerabilityWithAi | expectedGroups + ${true} | ${[...baseGroupHeaders, ...aiGroupHeaders]} + ${false} | ${baseGroupHeaders} + `( + 'shows the group headers correctly resolveVulnerabilityWithAi=$resolveVulnerabilityWithAi', + ({ resolveVulnerabilityWithAi, expectedGroups }) => { + createWrapperWithAbility({ resolveVulnerabilityWithAi }); + + const findDropdownGroupHeaders = () => + wrapper.findAllComponents(GlDropdownSectionHeader).wrappers.map((c) => c.text()); - expect( - findDropdownOptions() - .text() - .split('\n') - .map((s) => s.trim()) - .filter((i) => i), - ).toEqual(expectedOptions); + expect(findDropdownGroupHeaders()).toEqual(expectedGroups); }, ); diff --git a/ee/spec/frontend/security_dashboard/components/shared/filtered_search/tokens/cluster_token_spec.js b/ee/spec/frontend/security_dashboard/components/shared/filtered_search/tokens/cluster_token_spec.js index 3cd2b3fe7376dd4e2d57f784fd0632c691848c11..fcdcc06eaf4cdb12b8efbcf29e5ec9c3190c50c5 100644 --- a/ee/spec/frontend/security_dashboard/components/shared/filtered_search/tokens/cluster_token_spec.js +++ b/ee/spec/frontend/security_dashboard/components/shared/filtered_search/tokens/cluster_token_spec.js @@ -11,7 +11,6 @@ import SearchSuggestion from 'ee/security_dashboard/components/shared/filtered_s import QuerystringSync from 'ee/security_dashboard/components/shared/filters/querystring_sync.vue'; import eventHub from 'ee/security_dashboard/components/shared/filtered_search/event_hub'; import { OPERATORS_OR } from '~/vue_shared/components/filtered_search_bar/constants'; -import { stubComponent } from 'helpers/stub_component'; import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { projectClusters } from 'ee_jest/security_dashboard/components/mock_data'; @@ -78,44 +77,29 @@ describe('Cluster Token component', () => { }; describe('default view', () => { - const findViewSlot = () => wrapper.findByTestId('slot-view'); - const findSuggestionsSlot = () => wrapper.findByTestId('slot-suggestions'); const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); - const stubs = { - GlFilteredSearchToken: stubComponent(GlFilteredSearchToken, { - template: ` - <div> - <div data-testid="slot-view"> - <slot name="view"></slot> - </div> - <div data-testid="slot-suggestions"> - <slot name="suggestions"></slot> - </div> - </div>`, - }), - }; - beforeEach(() => { - createWrapper({ - stubs, - }); + createWrapper(); }); it('shows the label', () => { - expect(findViewSlot().text()).toBe('All clusters'); + expect(findFilteredSearchToken().props('value')).toEqual({ data: ['ALL'] }); + expect(wrapper.findByTestId('cluster-token-placeholder').text()).toBe('All clusters'); }); it('shows the dropdown with correct options', async () => { await waitForPromises(); - expect( - findSuggestionsSlot() - .text() - .split('\n') - .map((s) => s.trim()) - .filter((i) => i), - ).toEqual(['All clusters', 'primary-agent', 'james-bond-agent', 'jason-bourne-agent']); + const findDropdownOptions = () => + wrapper.findAllComponents(SearchSuggestion).wrappers.map((c) => c.text()); + + expect(findDropdownOptions()).toEqual([ + 'All clusters', + 'primary-agent', + 'james-bond-agent', + 'jason-bourne-agent', + ]); }); it('shows the loading icon when cluster agents are not yet loaded', async () => { diff --git a/ee/spec/frontend/security_dashboard/components/shared/filtered_search/tokens/image_token_spec.js b/ee/spec/frontend/security_dashboard/components/shared/filtered_search/tokens/image_token_spec.js index 86186329c5eae3ad97837dade91589a1004513b8..4659aecb53f666c2ba1153bd75b211c2986f465e 100644 --- a/ee/spec/frontend/security_dashboard/components/shared/filtered_search/tokens/image_token_spec.js +++ b/ee/spec/frontend/security_dashboard/components/shared/filtered_search/tokens/image_token_spec.js @@ -8,6 +8,7 @@ import createMockApollo from 'helpers/mock_apollo_helper'; import agentImagesQuery from 'ee/security_dashboard/graphql/queries/agent_images.query.graphql'; import projectImagesQuery from 'ee/security_dashboard/graphql/queries/project_images.query.graphql'; import ImageToken from 'ee/security_dashboard/components/shared/filtered_search/tokens/image_token.vue'; +import SearchSuggestion from 'ee/security_dashboard/components/shared/filtered_search/components/search_suggestion.vue'; import QuerystringSync from 'ee/security_dashboard/components/shared/filters/querystring_sync.vue'; import eventHub from 'ee/security_dashboard/components/shared/filtered_search/event_hub'; import { OPERATORS_OR } from '~/vue_shared/components/filtered_search_bar/constants'; @@ -68,14 +69,16 @@ describe('Image Token component', () => { projectFullPath, ...provide, }, - stubs, + stubs: { + SearchSuggestion, + ...stubs, + }, }); }; const findQuerystringSync = () => wrapper.findComponent(QuerystringSync); const findFilteredSearchToken = () => wrapper.findComponent(GlFilteredSearchToken); - const findCheckedIcon = (value) => wrapper.findByTestId(`image-icon-${value}`); - const isOptionChecked = (v) => !findCheckedIcon(v).classes('gl-invisible'); + const isOptionChecked = (v) => wrapper.findByTestId(`suggestion-${v}`).props('selected') === true; const clickDropdownItem = async (...ids) => { await Promise.all( @@ -94,8 +97,9 @@ describe('Image Token component', () => { const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findTruncateTexts = () => wrapper - .findAllByTestId('truncate-image') - .wrappers.map((component) => component.props('text')); + .findAllComponents(SearchSuggestion) + .wrappers.filter((component) => component.props('truncate')) + .map((component) => component.props('text')); const stubs = { GlFilteredSearchToken: stubComponent(GlFilteredSearchToken, { diff --git a/ee/spec/frontend/security_dashboard/components/shared/filtered_search/tokens/status_token_spec.js b/ee/spec/frontend/security_dashboard/components/shared/filtered_search/tokens/status_token_spec.js index aca2526ee04aa2d771e47d4cd81ae34c6cf29630..e9aec62f7c4dd31ff3de787997301814cbc9e4c0 100644 --- a/ee/spec/frontend/security_dashboard/components/shared/filtered_search/tokens/status_token_spec.js +++ b/ee/spec/frontend/security_dashboard/components/shared/filtered_search/tokens/status_token_spec.js @@ -6,7 +6,6 @@ import SearchSuggestion from 'ee/security_dashboard/components/shared/filtered_s import QuerystringSync from 'ee/security_dashboard/components/shared/filters/querystring_sync.vue'; import eventHub from 'ee/security_dashboard/components/shared/filtered_search/event_hub'; import { OPERATORS_IS } from '~/vue_shared/components/filtered_search_bar/constants'; -import { stubComponent } from 'helpers/stub_component'; import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper'; Vue.use(VueRouter); @@ -74,45 +73,29 @@ describe('Status Token component', () => { }; describe('default view', () => { - const findSlotView = () => wrapper.findByTestId('slot-view'); - const findSlotSuggestions = () => wrapper.findByTestId('slot-suggestions'); + const findDropdownOptions = () => + wrapper.findAllComponents(SearchSuggestion).wrappers.map((c) => c.props('text')); beforeEach(() => { - createWrapper({ - stubs: { - GlFilteredSearchToken: stubComponent(GlFilteredSearchToken, { - template: ` - <div> - <div data-testid="slot-view"> - <slot name="view"></slot> - </div> - <div data-testid="slot-suggestions"> - <slot name="suggestions"></slot> - </div> - </div>`, - }), - }, - }); + createWrapper(); }); it('shows the label', () => { - expect(findSlotView().text()).toBe('Needs triage, Confirmed'); + expect(findFilteredSearchToken().props('value')).toEqual({ + data: ['DETECTED', 'CONFIRMED'], + operator: '=', + }); + expect(wrapper.findByTestId('status-token-placeholder').text()).toBe( + 'Needs triage, Confirmed', + ); }); it('shows the dropdown with correct options', () => { - expect( - findSlotSuggestions() - .text() - .split('\n') - .map((s) => s.trim()) - .filter((i) => i), - ).toEqual([ - 'Status', // subheader + expect(findDropdownOptions()).toEqual([ 'All statuses', 'Needs triage', 'Confirmed', 'Resolved', - 'Dismissed as...', // subheader 'All dismissal reasons', 'Acceptable risk', 'False positive', diff --git a/ee/spec/frontend/security_dashboard/components/shared/filtered_search/tokens/tool_token_spec.js b/ee/spec/frontend/security_dashboard/components/shared/filtered_search/tokens/tool_token_spec.js index 790594379a8b3786bac05fc23848287d1ba4027a..5f76af78fa348b8dfe03878b118656e46a725761 100644 --- a/ee/spec/frontend/security_dashboard/components/shared/filtered_search/tokens/tool_token_spec.js +++ b/ee/spec/frontend/security_dashboard/components/shared/filtered_search/tokens/tool_token_spec.js @@ -1,4 +1,4 @@ -import { GlFilteredSearchToken } from '@gitlab/ui'; +import { GlFilteredSearchToken, GlDropdownSectionHeader } from '@gitlab/ui'; import Vue, { nextTick } from 'vue'; import VueRouter from 'vue-router'; import ToolToken from 'ee/security_dashboard/components/shared/filtered_search/tokens/tool_token.vue'; @@ -6,7 +6,6 @@ import QuerystringSync from 'ee/security_dashboard/components/shared/filters/que import SearchSuggestion from 'ee/security_dashboard/components/shared/filtered_search/components/search_suggestion.vue'; import eventHub from 'ee/security_dashboard/components/shared/filtered_search/event_hub'; import { OPERATORS_OR } from '~/vue_shared/components/filtered_search_bar/constants'; -import { stubComponent } from 'helpers/stub_component'; import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { MOCK_SCANNERS } from './mock_data'; @@ -81,61 +80,49 @@ describe('ToolToken', () => { }; describe('default view', () => { - const findViewSlot = () => wrapper.findByTestId('slot-view'); - beforeEach(() => { - createWrapper({ - stubs: { - GlFilteredSearchToken: stubComponent(GlFilteredSearchToken, { - template: ` - <div> - <div data-testid="slot-view"> - <slot name="view"></slot> - </div> - <div data-testid="slot-suggestions"> - <slot name="suggestions"></slot> - </div> - </div>`, - }), - }, - }); + createWrapper(); }); it('shows the label', () => { - expect(findViewSlot().text()).toBe('All tools'); + expect(findFilteredSearchToken().props('value')).toEqual({ + data: ['ALL'], + operator: '||', + }); + expect(wrapper.findByTestId('tool-token-value').text()).toBe('All tools'); }); it('shows the dropdown with correct options', () => { - // All options are rendered in the #suggestions slot of GlFilteredSearchToken - const findDropdownOptions = () => wrapper.findByTestId('slot-suggestions'); - - expect( - findDropdownOptions() - .text() - .split('\n') - .map((s) => s.trim()) - .filter((i) => i), - ).toEqual([ - 'Tool', // group header + const findDropdownOptions = () => + wrapper.findAllComponents(SearchSuggestion).wrappers.map((c) => c.text()); + + const findDropdownGroupHeaders = () => + wrapper.findAllComponents(GlDropdownSectionHeader).wrappers.map((c) => c.text()); + + expect(findDropdownOptions()).toEqual([ 'All tools', 'Manually added', - 'API Fuzzing', // group header 'GitLab API Fuzzing', - 'Container Scanning', // group header 'Trivy', - 'Coverage Fuzzing', // group header 'libfuzzer', - 'DAST', // group header 'OWASP Zed Attack Proxy (ZAP)', - 'Dependency Scanning', // group header 'Gemnasium', - 'SAST', // group header 'ESLint', 'Find Security Bugs', 'A Custom Scanner (SamScan)', - 'Secret Detection', // group header 'GitLeaks', ]); + + expect(findDropdownGroupHeaders()).toEqual([ + 'Tool', + 'API Fuzzing', + 'Container Scanning', + 'Coverage Fuzzing', + 'DAST', + 'Dependency Scanning', + 'SAST', + 'Secret Detection', + ]); }); });