diff --git a/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_menu/duo_chat_context_item_menu.spec.js b/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_menu/duo_chat_context_item_menu.spec.js index 9e3633e5319347e5835ee63c4481e919e8dc955e..e93972db4261a0cee3e7613392b7cfafb66a7489 100644 --- a/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_menu/duo_chat_context_item_menu.spec.js +++ b/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_menu/duo_chat_context_item_menu.spec.js @@ -1,5 +1,5 @@ import { shallowMount } from '@vue/test-utils'; -import { getMockContextItems, MOCK_CATEGORIES } from '../mock_context_data'; +import { getMockCategory, getMockContextItems, MOCK_CATEGORIES } from '../mock_context_data'; import { CONTEXT_ITEM_TYPE_ISSUE, CONTEXT_ITEM_TYPE_MERGE_REQUEST, @@ -98,7 +98,7 @@ describe('GlDuoChatContextItemMenu', () => { let category; let results; beforeEach(() => { - category = MOCK_CATEGORIES.find((cat) => cat.value === categoryValue); + category = getMockCategory(categoryValue); results = getMockContextItems() .filter((item) => item.type === categoryValue) .map((item, index) => ({ diff --git a/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_menu/duo_chat_context_item_menu_search_item.spec.js b/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_menu/duo_chat_context_item_menu_search_item.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..9ddf410b2518e086a7122b689f6569a6986ca3eb --- /dev/null +++ b/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_menu/duo_chat_context_item_menu_search_item.spec.js @@ -0,0 +1,64 @@ +import { shallowMount } from '@vue/test-utils'; +import { + getMockCategory, + MOCK_CONTEXT_ITEM_FILE, + MOCK_CONTEXT_ITEM_ISSUE, + MOCK_CONTEXT_ITEM_MERGE_REQUEST, +} from '../mock_context_data'; +import { + CONTEXT_ITEM_TYPE_ISSUE, + CONTEXT_ITEM_TYPE_MERGE_REQUEST, + CONTEXT_ITEM_TYPE_PROJECT_FILE, +} from '../constants'; +import GlDuoChatContextItemPopover from '../duo_chat_context_item_popover/duo_chat_context_item_popover.vue'; +import GlDuoChatContextItemMenuSearchItem from './duo_chat_context_item_menu_search_item.vue'; + +describe('GlDuoChatContextItemMenuContextSearchItem', () => { + let wrapper; + + const createWrapper = (props) => { + wrapper = shallowMount(GlDuoChatContextItemMenuSearchItem, { + propsData: { + ...props, + }, + }); + }; + + const findByTestId = (testId) => wrapper.find(`[data-testid="${testId}"]`); + const findCategoryIcon = () => findByTestId('category-icon'); + const findContextItemPopover = () => wrapper.findComponent(GlDuoChatContextItemPopover); + + describe.each([ + { + category: getMockCategory(CONTEXT_ITEM_TYPE_PROJECT_FILE), + contextItem: MOCK_CONTEXT_ITEM_FILE, + }, + { + category: getMockCategory(CONTEXT_ITEM_TYPE_ISSUE), + contextItem: MOCK_CONTEXT_ITEM_ISSUE, + }, + { + category: getMockCategory(CONTEXT_ITEM_TYPE_MERGE_REQUEST), + contextItem: MOCK_CONTEXT_ITEM_MERGE_REQUEST, + }, + ])('for "$category"', ({ category, contextItem }) => { + beforeEach(() => createWrapper({ category, contextItem })); + + it('renders the category icon', () => { + expect(findCategoryIcon().props('name')).toBe(category.icon); + }); + + it('renders the context item popover', () => { + expect(findContextItemPopover().props()).toEqual( + expect.objectContaining({ + contextItem, + target: `info-icon-${contextItem.id}`, + }) + ); + }); + + it('renders the default context item title', () => { + expect(wrapper.text()).toContain(contextItem.metadata.name); + }); + }); +}); diff --git a/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_menu/duo_chat_context_item_menu_search_item.vue b/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_menu/duo_chat_context_item_menu_search_item.vue new file mode 100644 index 0000000000000000000000000000000000000000..df4175b15921126db059790e72f7f1588ebd3b55 --- /dev/null +++ b/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_menu/duo_chat_context_item_menu_search_item.vue @@ -0,0 +1,71 @@ + + diff --git a/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_menu/duo_chat_context_item_menu_search_items.spec.js b/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_menu/duo_chat_context_item_menu_search_items.spec.js index 00787560f12cfc56ea9c27704bf8e39e242a9f82..e2c28c98f1c300c8b912a3eb212d8652380f2754 100644 --- a/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_menu/duo_chat_context_item_menu_search_items.spec.js +++ b/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_menu/duo_chat_context_item_menu_search_items.spec.js @@ -1,5 +1,5 @@ import { shallowMount } from '@vue/test-utils'; -import { MOCK_CATEGORIES, getMockContextItems } from '../mock_context_data'; +import { MOCK_CATEGORIES, getMockContextItems, getMockCategory } from '../mock_context_data'; import { CONTEXT_ITEM_TYPE_ISSUE, CONTEXT_ITEM_TYPE_MERGE_REQUEST, @@ -7,6 +7,7 @@ import { } from '../constants'; import GlDuoChatContextItemMenuSearchItems from './duo_chat_context_item_menu_search_items.vue'; import GlDuoChatContextItemMenuSearchItemsLoading from './duo_chat_context_item_menu_search_items_loading.vue'; +import GlDuoChatContextItemMenuSearchItem from './duo_chat_context_item_menu_search_item.vue'; describe('GlDuoChatContextItemMenuSearchItems', () => { let wrapper; @@ -93,12 +94,9 @@ describe('GlDuoChatContextItemMenuSearchItems', () => { { numResults: 5, expectedRows: 5 }, ])('when there are $numResults results', ({ numResults, expectedRows }) => { beforeEach(async () => { - await wrapper.setProps({ - loading: false, - results: getMockContextItems().slice(0, numResults), - }); await wrapper.setProps({ loading: true, + results: getMockContextItems().slice(0, numResults), }); }); @@ -165,19 +163,22 @@ describe('GlDuoChatContextItemMenuSearchItems', () => { }); expect(findResultItems()).toHaveLength(1); - expect(findByTestId('search-result-item-details').text()).toEqual( - matchingResult.metadata.name + expect(wrapper.findComponent(GlDuoChatContextItemMenuSearchItem).props()).toEqual( + expect.objectContaining({ + contextItem: matchingResult, + category, + }) ); }); it('marks the correct item as active', async () => { - expect(findActiveItemDetails().text()).toEqual(results.at(0).metadata.name); + expect(findActiveItemDetails().props('contextItem')).toEqual(results.at(0)); await wrapper.setProps({ activeIndex: 1, }); - expect(findActiveItemDetails().text()).toEqual(results.at(1).metadata.name); + expect(findActiveItemDetails().props('contextItem')).toEqual(results.at(1)); }); it('emits "active-index-change" event when hovering over an item', async () => { @@ -209,7 +210,7 @@ describe('GlDuoChatContextItemMenuSearchItems', () => { it('disables the item', () => { expect(disabledItem.attributes('tabindex')).toBe('-1'); - expect(disabledItem.classes()).toContain('disabled'); + expect(disabledItem.classes()).toContain('gl-cursor-not-allowed'); }); }); }); @@ -217,15 +218,15 @@ describe('GlDuoChatContextItemMenuSearchItems', () => { describe.each([ { - testCase: MOCK_CATEGORIES.find((cat) => cat.value === CONTEXT_ITEM_TYPE_PROJECT_FILE), + testCase: getMockCategory(CONTEXT_ITEM_TYPE_PROJECT_FILE), expectedPlaceholder: 'Search files...', }, { - testCase: MOCK_CATEGORIES.find((cat) => cat.value === CONTEXT_ITEM_TYPE_ISSUE), + testCase: getMockCategory(CONTEXT_ITEM_TYPE_ISSUE), expectedPlaceholder: 'Search issues...', }, { - testCase: MOCK_CATEGORIES.find((cat) => cat.value === CONTEXT_ITEM_TYPE_MERGE_REQUEST), + testCase: getMockCategory(CONTEXT_ITEM_TYPE_MERGE_REQUEST), expectedPlaceholder: 'Search merge requests...', }, ])('when category is "$testCase.label"', ({ testCase, expectedPlaceholder }) => { diff --git a/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_menu/duo_chat_context_item_menu_search_items.vue b/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_menu/duo_chat_context_item_menu_search_items.vue index 2a5badf650ebde95cea90a08d1e98488b8444aae..84fd89d3bc22b7ac15eaca9315a0cd0023577763 100644 --- a/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_menu/duo_chat_context_item_menu_search_items.vue +++ b/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_menu/duo_chat_context_item_menu_search_items.vue @@ -5,14 +5,16 @@ import GlAlert from '../../../../../../base/alert/alert.vue'; import { sprintf, translate } from '../../../../../../../utils/i18n'; import { categoryValidator, contextItemsValidator } from '../utils'; import GlDuoChatContextItemMenuSearchItemsLoading from './duo_chat_context_item_menu_search_items_loading.vue'; +import GlDuoChatContextItemMenuSearchItem from './duo_chat_context_item_menu_search_item.vue'; export default { name: 'GlDuoChatContextItemMenuSearchItems', components: { GlAlert, - GlFormInput, GlDropdownItem, + GlDuoChatContextItemMenuSearchItem, GlDuoChatContextItemMenuSearchItemsLoading, + GlFormInput, }, model: { prop: 'searchQuery', @@ -77,8 +79,8 @@ export default { }, }, methods: { - selectItem(item) { - this.$emit('select', item); + selectItem(contextItem) { + this.$emit('select', contextItem); this.userInitiatedSearch = false; }, setActiveIndex(index) { @@ -114,20 +116,25 @@ export default { diff --git a/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_popover/duo_chat_context_item_popover.spec.js b/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_popover/duo_chat_context_item_popover.spec.js index 68160276e98037ba7a43e2489605fb5b010f7e07..992f8eab458f4026599713ac7fa084a5118b9c46 100644 --- a/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_popover/duo_chat_context_item_popover.spec.js +++ b/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_popover/duo_chat_context_item_popover.spec.js @@ -15,7 +15,7 @@ describe('GlDuoChatContextItemPopover', () => { const createComponent = (props = {}, options = {}) => { wrapper = shallowMount(GlDuoChatContextItemPopover, { propsData: { - item: MOCK_CONTEXT_ITEM_FILE, + contextItem: MOCK_CONTEXT_ITEM_FILE, target: 'test-target', placement: 'top', ...props, @@ -67,16 +67,16 @@ describe('GlDuoChatContextItemPopover', () => { ['file', MOCK_CONTEXT_ITEM_FILE], ['issue', MOCK_CONTEXT_ITEM_ISSUE], ['merge request', MOCK_CONTEXT_ITEM_MERGE_REQUEST], - ])('renders correct project and type for %s', (_, item) => { - createComponent({ item }); + ])('renders correct project and type for %s', (_, contextItem) => { + createComponent({ contextItem }); const content = findPopover().text(); - expect(content).toContain(item.metadata.info.project); - expect(content).toContain(item.type); + expect(content).toContain(contextItem.metadata.info.project); + expect(content).toContain(contextItem.type); }); it('renders file path for file items', () => { - createComponent({ item: MOCK_CONTEXT_ITEM_FILE }); + createComponent({ contextItem: MOCK_CONTEXT_ITEM_FILE }); const content = findPopover().text(); expect(content).toContain(MOCK_CONTEXT_ITEM_FILE.metadata.info.relFilePath); @@ -85,8 +85,8 @@ describe('GlDuoChatContextItemPopover', () => { it.each([ ['issue', MOCK_CONTEXT_ITEM_ISSUE, '#1234'], ['merge request', MOCK_CONTEXT_ITEM_MERGE_REQUEST, '!1122'], - ])('renders ID for %s items with correct prefix', (_, item, expected) => { - createComponent({ item }); + ])('renders ID for %s items with correct prefix', (_, contextItem, expected) => { + createComponent({ contextItem }); const content = findPopover().text(); expect(content).toContain(expected); @@ -95,7 +95,7 @@ describe('GlDuoChatContextItemPopover', () => { describe('disabled items', () => { it('renders disabled message', () => { - createComponent({ item: MOCK_CONTEXT_ITEM_ISSUE_DISABLED }); + createComponent({ contextItem: MOCK_CONTEXT_ITEM_ISSUE_DISABLED }); expect(findDisabledMessage().text()).toContain( 'This foo is not available to bar, Lorem something something wow?' @@ -107,7 +107,7 @@ describe('GlDuoChatContextItemPopover', () => { ...MOCK_CONTEXT_ITEM_FILE_DISABLED, info: { ...MOCK_CONTEXT_ITEM_FILE_DISABLED.metadata.info, disabledReasons: undefined }, }; - createComponent({ item: itemWithoutReasons }); + createComponent({ contextItem: itemWithoutReasons }); expect(findDisabledMessage().text()).toContain('This item is disabled'); }); diff --git a/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_popover/duo_chat_context_item_popover.vue b/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_popover/duo_chat_context_item_popover.vue index f7d40016de834304d632790c66764dcf83eb5d83..b00498842e324d4fff64ff8664220315bb9ebf2b 100644 --- a/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_popover/duo_chat_context_item_popover.vue +++ b/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_popover/duo_chat_context_item_popover.vue @@ -6,11 +6,7 @@ import { CONTEXT_ITEM_TYPE_MERGE_REQUEST, CONTEXT_ITEM_TYPE_PROJECT_FILE, } from '../constants'; - -const ID_PREFIXES = { - issue: '#', - merge_request: '!', -}; +import { formatIssueId, formatMergeRequestId } from '../utils'; export default { name: 'DuoChatContextItemPopover', @@ -19,9 +15,9 @@ export default { }, props: { /** - * The context item to display in the popover. + * The context contextItem to display in the popover. */ - item: { + contextItem: { type: Object, required: true, }, @@ -43,28 +39,36 @@ export default { }, computed: { itemInfo() { - return this.item.metadata?.info || {}; + return this.contextItem.metadata?.info || {}; }, id() { const isIssuable = - this.item.type === CONTEXT_ITEM_TYPE_ISSUE || - this.item.type === CONTEXT_ITEM_TYPE_MERGE_REQUEST; + this.contextItem.type === CONTEXT_ITEM_TYPE_ISSUE || + this.contextItem.type === CONTEXT_ITEM_TYPE_MERGE_REQUEST; return isIssuable ? this.itemInfo.iid || '' : null; }, - idPrefix() { - return ID_PREFIXES[this.item.type] || ''; + formattedId() { + switch (this.contextItem.type) { + case CONTEXT_ITEM_TYPE_ISSUE: + return formatIssueId(this.id); + case CONTEXT_ITEM_TYPE_MERGE_REQUEST: + return formatMergeRequestId(this.id); + default: + return ''; + } }, filePath() { - return this.item.type === CONTEXT_ITEM_TYPE_PROJECT_FILE + return this.contextItem.type === CONTEXT_ITEM_TYPE_PROJECT_FILE ? this.itemInfo.relFilePath || '' : null; }, isEnabled() { - return this.item.isEnabled !== false; + return this.contextItem.isEnabled !== false; }, disabledMessage() { - return Array.isArray(this.item.disabledReasons) && this.item.disabledReasons.length > 0 - ? this.item.disabledReasons.join(', ') + return Array.isArray(this.contextItem.disabledReasons) && + this.contextItem.disabledReasons.length > 0 + ? this.contextItem.disabledReasons.join(', ') : translate('DuoChatContextItemPopover.DisabledReason', 'This item is disabled'); }, }, @@ -78,12 +82,12 @@ export default { :target="target" triggers="hover focus" :placement="placement" - :title="item.metadata.name" + :title="contextItem.metadata.name" custom-class="gl-duo-chat-item-popover" >
@@ -101,13 +105,13 @@ export default {
{{ translate('DuoChatContextItemPopover.IdLabel', 'ID:') }} - {{ idPrefix }}{{ id }} + {{ formattedId }}
{{ translate('DuoChatContextItemPopover.TypeLabel', 'Type:') }} - {{ item.type }} + {{ contextItem.type }}
{{ diff --git a/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_selections/duo_chat_context_item_selections.spec.js b/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_selections/duo_chat_context_item_selections.spec.js index 4ad71f15223419a7ffebb7285202736aa05c0a3a..e3227d1d9b0536e765b73303d04d625770511995 100644 --- a/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_selections/duo_chat_context_item_selections.spec.js +++ b/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_selections/duo_chat_context_item_selections.spec.js @@ -134,10 +134,10 @@ describe('GlDuoChatContextItemSelections', () => { createComponent(); const index = 0; - const item = mockSelections.at(index); + const contextItem = mockSelections.at(index); const popover = findPopovers().at(index); - expect(popover.props('item')).toEqual(item); + expect(popover.props('contextItem')).toEqual(contextItem); expect(popover.props('target')).toMatch( /^context-item-123e4567-e89b-12d3-a456-426614174000-\d+$/ ); diff --git a/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_selections/duo_chat_context_item_selections.vue b/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_selections/duo_chat_context_item_selections.vue index b2a017404447951806783e3211c00782555b51f2..514996f07a370797060759de2fc32bc6e2b88404 100644 --- a/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_selections/duo_chat_context_item_selections.vue +++ b/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_selections/duo_chat_context_item_selections.vue @@ -72,12 +72,12 @@ export default { toggleCollapse() { this.isCollapsed = !this.isCollapsed; }, - onRemoveItem(item) { + onRemoveItem(contextItem) { /** - * Emitted when a context item should be removed. - * @property {Object} item - The context item to be removed + * Emitted when a context contextItem should be removed. + * @property {Object} item - The context contextItem to be removed */ - this.$emit('remove', item); + this.$emit('remove', contextItem); }, }, }; @@ -99,20 +99,20 @@ export default { data-testid="chat-context-tokens-wrapper" > -
- - {{ item.metadata.name }} +
+ + {{ contextItem.metadata.name }}
diff --git a/src/components/experimental/duo/chat/components/duo_chat_context/mock_context_data.js b/src/components/experimental/duo/chat/components/duo_chat_context/mock_context_data.js index 95d6cb2cec1c11788f930a949669577e3509fcd3..2c940ad81509de30a889fdbbb9982c85d4bf719e 100644 --- a/src/components/experimental/duo/chat/components/duo_chat_context/mock_context_data.js +++ b/src/components/experimental/duo/chat/components/duo_chat_context/mock_context_data.js @@ -10,6 +10,10 @@ export const MOCK_CATEGORIES = [ { label: 'Merge Requests', value: CONTEXT_ITEM_TYPE_MERGE_REQUEST, icon: 'merge-request' }, ]; +export function getMockCategory(categoryValue) { + return MOCK_CATEGORIES.find((cat) => cat.value === categoryValue); +} + export const MOCK_CONTEXT_ITEM_FILE = { id: '123e4567-e89b-12d3-a456-426614174000', isEnabled: true, diff --git a/src/components/experimental/duo/chat/components/duo_chat_context/utils.js b/src/components/experimental/duo/chat/components/duo_chat_context/utils.js index d8baa557125040a5294792296bc85fe1a5698a3f..93cce8a00097e21dab7419c39d0b6ec1e35f1550 100644 --- a/src/components/experimental/duo/chat/components/duo_chat_context/utils.js +++ b/src/components/experimental/duo/chat/components/duo_chat_context/utils.js @@ -27,3 +27,15 @@ export function contextItemValidator(item) { export function contextItemsValidator(items) { return Array.isArray(items) && items.every((item) => contextItemValidator(item)); } + +export function formatIssueId(iid) { + if (!iid) return ''; + + return `#${iid}`; +} + +export function formatMergeRequestId(iid) { + if (!iid) return ''; + + return `!${iid}`; +} diff --git a/src/components/experimental/duo/chat/components/duo_chat_context/utils.spec.js b/src/components/experimental/duo/chat/components/duo_chat_context/utils.spec.js index 330cfe1cf17ad3e797b98c40019b773d98eb7213..b68f013ce6442f38bb3f82396479f0b608e8507a 100644 --- a/src/components/experimental/duo/chat/components/duo_chat_context/utils.spec.js +++ b/src/components/experimental/duo/chat/components/duo_chat_context/utils.spec.js @@ -3,6 +3,8 @@ import { categoryValidator, contextItemsValidator, contextItemValidator, + formatIssueId, + formatMergeRequestId, } from './utils'; import { MOCK_CATEGORIES, @@ -170,4 +172,34 @@ describe('duo_chat_context utils', () => { }); }); }); + + describe('formatIssueId', () => { + it.each([ + { id: '123', expected: '#123' }, + { id: 123, expected: '#123' }, + ])('formats "$id" as "$expected"', ({ id, expected }) => { + expect(formatIssueId(id)).toBe(expected); + }); + + it('returns empty string for falsy values', () => { + expect(formatIssueId()).toBe(''); + expect(formatIssueId(null)).toBe(''); + expect(formatIssueId('')).toBe(''); + }); + }); + + describe('formatMergeRequestId', () => { + it.each([ + { id: '123', expected: '!123' }, + { id: 123, expected: '!123' }, + ])('formats "$id" as "$expected"', ({ id, expected }) => { + expect(formatMergeRequestId(id)).toBe(expected); + }); + + it('returns empty string for falsy values', () => { + expect(formatMergeRequestId()).toBe(''); + expect(formatMergeRequestId(null)).toBe(''); + expect(formatMergeRequestId('')).toBe(''); + }); + }); });