diff options
Diffstat (limited to 'spec/frontend/vue_shared/components/filtered_search_bar')
4 files changed, 460 insertions, 0 deletions
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js b/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js index 7606b3bd91c..c24528ba4d2 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js @@ -3,6 +3,8 @@ import { mockLabels } from 'jest/vue_shared/components/sidebar/labels_select_vue import Api from '~/api'; import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue'; import BranchToken from '~/vue_shared/components/filtered_search_bar/tokens/branch_token.vue'; +import EmojiToken from '~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue'; +import EpicToken from '~/vue_shared/components/filtered_search_bar/tokens/epic_token.vue'; import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue'; import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue'; @@ -59,6 +61,21 @@ export const mockMilestones = [ mockEscapedMilestone, ]; +export const mockEpics = [ + { iid: 1, id: 1, title: 'Foo' }, + { iid: 2, id: 2, title: 'Bar' }, +]; + +export const mockEmoji1 = { + name: 'thumbsup', +}; + +export const mockEmoji2 = { + name: 'star', +}; + +export const mockEmojis = [mockEmoji1, mockEmoji2]; + export const mockBranchToken = { type: 'source_branch', icon: 'branch', @@ -103,6 +120,28 @@ export const mockMilestoneToken = { fetchMilestones: () => Promise.resolve({ data: mockMilestones }), }; +export const mockEpicToken = { + type: 'epic_iid', + icon: 'clock', + title: 'Epic', + unique: true, + symbol: '&', + token: EpicToken, + operators: [{ value: '=', description: 'is', default: 'true' }], + fetchEpics: () => Promise.resolve({ data: mockEpics }), + fetchSingleEpic: () => Promise.resolve({ data: mockEpics[0] }), +}; + +export const mockReactionEmojiToken = { + type: 'my_reaction_emoji', + icon: 'thumb-up', + title: 'My-Reaction', + unique: true, + token: EmojiToken, + operators: [{ value: '=', description: 'is', default: 'true' }], + fetchEmojis: () => Promise.resolve(mockEmojis), +}; + export const mockMembershipToken = { type: 'with_inherited_permissions', icon: 'group', @@ -168,6 +207,14 @@ export const tokenValuePlain = { value: { data: 'foo' }, }; +export const tokenValueEpic = { + type: 'epic_iid', + value: { + operator: '=', + data: '"foo"::&42', + }, +}; + export const mockHistoryItems = [ [tokenValueAuthor, tokenValueLabel, tokenValueMilestone, 'duo'], [tokenValueAuthor, 'si'], diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js new file mode 100644 index 00000000000..231f2f01428 --- /dev/null +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js @@ -0,0 +1,217 @@ +import { + GlFilteredSearchToken, + GlFilteredSearchSuggestion, + GlFilteredSearchTokenSegment, + GlDropdownDivider, +} from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; +import MockAdapter from 'axios-mock-adapter'; +import waitForPromises from 'helpers/wait_for_promises'; +import { deprecatedCreateFlash as createFlash } from '~/flash'; +import axios from '~/lib/utils/axios_utils'; + +import { + DEFAULT_LABEL_NONE, + DEFAULT_LABEL_ANY, +} from '~/vue_shared/components/filtered_search_bar/constants'; +import EmojiToken from '~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue'; + +import { mockReactionEmojiToken, mockEmojis } from '../mock_data'; + +jest.mock('~/flash'); +const GlEmoji = { template: '<img/>' }; +const defaultStubs = { + Portal: true, + GlFilteredSearchSuggestionList: { + template: '<div></div>', + methods: { + getValue: () => '=', + }, + }, + GlEmoji, +}; + +function createComponent(options = {}) { + const { + config = mockReactionEmojiToken, + value = { data: '' }, + active = false, + stubs = defaultStubs, + } = options; + return mount(EmojiToken, { + propsData: { + config, + value, + active, + }, + provide: { + portalName: 'fake target', + alignSuggestions: function fakeAlignSuggestions() {}, + suggestionsListClass: 'custom-class', + }, + stubs, + }); +} + +describe('EmojiToken', () => { + let mock; + let wrapper; + + beforeEach(() => { + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + wrapper.destroy(); + }); + + describe('computed', () => { + beforeEach(async () => { + wrapper = createComponent({ value: { data: mockEmojis[0].name } }); + + wrapper.setData({ + emojis: mockEmojis, + }); + + await wrapper.vm.$nextTick(); + }); + + describe('currentValue', () => { + it('returns lowercase string for `value.data`', () => { + expect(wrapper.vm.currentValue).toBe(mockEmojis[0].name); + }); + }); + + describe('activeEmoji', () => { + it('returns object for currently present `value.data`', () => { + expect(wrapper.vm.activeEmoji).toEqual(mockEmojis[0]); + }); + }); + }); + + describe('methods', () => { + beforeEach(() => { + wrapper = createComponent(); + }); + + describe('fetchEmojiBySearchTerm', () => { + it('calls `config.fetchEmojis` with provided searchTerm param', () => { + jest.spyOn(wrapper.vm.config, 'fetchEmojis'); + + wrapper.vm.fetchEmojiBySearchTerm('foo'); + + expect(wrapper.vm.config.fetchEmojis).toHaveBeenCalledWith('foo'); + }); + + it('sets response to `emojis` when request is successful', () => { + jest.spyOn(wrapper.vm.config, 'fetchEmojis').mockResolvedValue(mockEmojis); + + wrapper.vm.fetchEmojiBySearchTerm('foo'); + + return waitForPromises().then(() => { + expect(wrapper.vm.emojis).toEqual(mockEmojis); + }); + }); + + it('calls `createFlash` with flash error message when request fails', () => { + jest.spyOn(wrapper.vm.config, 'fetchEmojis').mockRejectedValue({}); + + wrapper.vm.fetchEmojiBySearchTerm('foo'); + + return waitForPromises().then(() => { + expect(createFlash).toHaveBeenCalledWith('There was a problem fetching emojis.'); + }); + }); + + it('sets `loading` to false when request completes', () => { + jest.spyOn(wrapper.vm.config, 'fetchEmojis').mockRejectedValue({}); + + wrapper.vm.fetchEmojiBySearchTerm('foo'); + + return waitForPromises().then(() => { + expect(wrapper.vm.loading).toBe(false); + }); + }); + }); + }); + + describe('template', () => { + const defaultEmojis = [DEFAULT_LABEL_NONE, DEFAULT_LABEL_ANY]; + + beforeEach(async () => { + wrapper = createComponent({ + value: { data: `"${mockEmojis[0].name}"` }, + }); + + wrapper.setData({ + emojis: mockEmojis, + }); + + await wrapper.vm.$nextTick(); + }); + + it('renders gl-filtered-search-token component', () => { + expect(wrapper.find(GlFilteredSearchToken).exists()).toBe(true); + }); + + it('renders token item when value is selected', () => { + const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); + + expect(tokenSegments).toHaveLength(3); // My Reaction, =, "thumbsup" + expect(tokenSegments.at(2).find(GlEmoji).attributes('data-name')).toEqual('thumbsup'); + }); + + it('renders provided defaultEmojis as suggestions', async () => { + wrapper = createComponent({ + active: true, + config: { ...mockReactionEmojiToken, defaultEmojis }, + stubs: { Portal: true, GlEmoji }, + }); + const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); + const suggestionsSegment = tokenSegments.at(2); + suggestionsSegment.vm.$emit('activate'); + await wrapper.vm.$nextTick(); + + const suggestions = wrapper.findAll(GlFilteredSearchSuggestion); + + expect(suggestions).toHaveLength(defaultEmojis.length); + defaultEmojis.forEach((emoji, index) => { + expect(suggestions.at(index).text()).toBe(emoji.text); + }); + }); + + it('does not render divider when no defaultEmojis', async () => { + wrapper = createComponent({ + active: true, + config: { ...mockReactionEmojiToken, defaultEmojis: [] }, + stubs: { Portal: true, GlEmoji }, + }); + const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); + const suggestionsSegment = tokenSegments.at(2); + suggestionsSegment.vm.$emit('activate'); + await wrapper.vm.$nextTick(); + + expect(wrapper.find(GlFilteredSearchSuggestion).exists()).toBe(false); + expect(wrapper.find(GlDropdownDivider).exists()).toBe(false); + }); + + it('renders `DEFAULT_LABEL_NONE` and `DEFAULT_LABEL_ANY` as default suggestions', async () => { + wrapper = createComponent({ + active: true, + config: { ...mockReactionEmojiToken }, + stubs: { Portal: true, GlEmoji }, + }); + const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); + const suggestionsSegment = tokenSegments.at(2); + suggestionsSegment.vm.$emit('activate'); + await wrapper.vm.$nextTick(); + + const suggestions = wrapper.findAll(GlFilteredSearchSuggestion); + + expect(suggestions).toHaveLength(2); + expect(suggestions.at(0).text()).toBe(DEFAULT_LABEL_NONE.text); + expect(suggestions.at(1).text()).toBe(DEFAULT_LABEL_ANY.text); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/epic_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/epic_token_spec.js new file mode 100644 index 00000000000..0c3f9e1363f --- /dev/null +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/epic_token_spec.js @@ -0,0 +1,180 @@ +import { GlFilteredSearchToken, GlFilteredSearchTokenSegment } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; +import MockAdapter from 'axios-mock-adapter'; +import waitForPromises from 'helpers/wait_for_promises'; +import createFlash from '~/flash'; +import axios from '~/lib/utils/axios_utils'; + +import EpicToken from '~/vue_shared/components/filtered_search_bar/tokens/epic_token.vue'; + +import { mockEpicToken, mockEpics } from '../mock_data'; + +jest.mock('~/flash'); + +const defaultStubs = { + Portal: true, + GlFilteredSearchSuggestionList: { + template: '<div></div>', + methods: { + getValue: () => '=', + }, + }, +}; + +function createComponent(options = {}) { + const { + config = mockEpicToken, + value = { data: '' }, + active = false, + stubs = defaultStubs, + } = options; + return mount(EpicToken, { + propsData: { + config, + value, + active, + }, + provide: { + portalName: 'fake target', + alignSuggestions: function fakeAlignSuggestions() {}, + suggestionsListClass: 'custom-class', + }, + stubs, + }); +} + +describe('EpicToken', () => { + let mock; + let wrapper; + + beforeEach(() => { + mock = new MockAdapter(axios); + wrapper = createComponent(); + }); + + afterEach(() => { + mock.restore(); + wrapper.destroy(); + }); + + describe('computed', () => { + beforeEach(async () => { + wrapper = createComponent({ + data: { + epics: mockEpics, + }, + }); + + await wrapper.vm.$nextTick(); + }); + + describe('currentValue', () => { + it.each` + data | id + ${`${mockEpics[0].title}::&${mockEpics[0].iid}`} | ${mockEpics[0].iid} + ${mockEpics[0].iid} | ${mockEpics[0].iid} + ${'foobar'} | ${'foobar'} + `('$data returns $id', async ({ data, id }) => { + wrapper.setProps({ value: { data } }); + + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.currentValue).toBe(id); + }); + }); + + describe('activeEpic', () => { + it('returns object for currently present `value.data`', async () => { + wrapper.setProps({ + value: { data: `${mockEpics[0].iid}` }, + }); + + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.activeEpic).toEqual(mockEpics[0]); + }); + }); + }); + + describe('methods', () => { + describe('fetchEpicsBySearchTerm', () => { + it('calls `config.fetchEpics` with provided searchTerm param', () => { + jest.spyOn(wrapper.vm.config, 'fetchEpics'); + + wrapper.vm.fetchEpicsBySearchTerm('foo'); + + expect(wrapper.vm.config.fetchEpics).toHaveBeenCalledWith('foo'); + }); + + it('sets response to `epics` when request is successful', async () => { + jest.spyOn(wrapper.vm.config, 'fetchEpics').mockResolvedValue({ + data: mockEpics, + }); + + wrapper.vm.fetchEpicsBySearchTerm(); + + await waitForPromises(); + + expect(wrapper.vm.epics).toEqual(mockEpics); + }); + + it('calls `createFlash` with flash error message when request fails', async () => { + jest.spyOn(wrapper.vm.config, 'fetchEpics').mockRejectedValue({}); + + wrapper.vm.fetchEpicsBySearchTerm('foo'); + + await waitForPromises(); + + expect(createFlash).toHaveBeenCalledWith({ + message: 'There was a problem fetching epics.', + }); + }); + + it('sets `loading` to false when request completes', async () => { + jest.spyOn(wrapper.vm.config, 'fetchEpics').mockRejectedValue({}); + + wrapper.vm.fetchEpicsBySearchTerm('foo'); + + await waitForPromises(); + + expect(wrapper.vm.loading).toBe(false); + }); + }); + + describe('fetchSingleEpic', () => { + it('calls `config.fetchSingleEpic` with provided iid param', async () => { + jest.spyOn(wrapper.vm.config, 'fetchSingleEpic'); + + wrapper.vm.fetchSingleEpic(1); + + expect(wrapper.vm.config.fetchSingleEpic).toHaveBeenCalledWith(1); + + await waitForPromises(); + + expect(wrapper.vm.epics).toEqual([mockEpics[0]]); + }); + }); + }); + + describe('template', () => { + beforeEach(async () => { + wrapper = createComponent({ + value: { data: `${mockEpics[0].iid}` }, + data: { epics: mockEpics }, + }); + + await wrapper.vm.$nextTick(); + }); + + it('renders gl-filtered-search-token component', () => { + expect(wrapper.find(GlFilteredSearchToken).exists()).toBe(true); + }); + + it('renders token item when value is selected', () => { + const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); + + expect(tokenSegments).toHaveLength(3); + expect(tokenSegments.at(2).text()).toBe(`${mockEpics[0].title}::&${mockEpics[0].iid}`); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js index 7676ce10ce0..8528c062426 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js @@ -118,6 +118,22 @@ describe('LabelToken', () => { wrapper = createComponent(); }); + describe('getLabelName', () => { + it('returns value of `name` or `title` property present in provided label param', () => { + let mockLabel = { + title: 'foo', + }; + + expect(wrapper.vm.getLabelName(mockLabel)).toBe(mockLabel.title); + + mockLabel = { + name: 'foo', + }; + + expect(wrapper.vm.getLabelName(mockLabel)).toBe(mockLabel.name); + }); + }); + describe('fetchLabelBySearchTerm', () => { it('calls `config.fetchLabels` with provided searchTerm param', () => { jest.spyOn(wrapper.vm.config, 'fetchLabels'); |