diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-10-20 08:43:02 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-10-20 08:43:02 +0000 |
commit | d9ab72d6080f594d0b3cae15f14b3ef2c6c638cb (patch) | |
tree | 2341ef426af70ad1e289c38036737e04b0aa5007 /spec/frontend/vue_shared/components | |
parent | d6e514dd13db8947884cd58fe2a9c2a063400a9b (diff) | |
download | gitlab-ce-d9ab72d6080f594d0b3cae15f14b3ef2c6c638cb.tar.gz |
Add latest changes from gitlab-org/gitlab@14-4-stable-eev14.4.0-rc42
Diffstat (limited to 'spec/frontend/vue_shared/components')
32 files changed, 864 insertions, 241 deletions
diff --git a/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap index c7758b0faef..44b4c0398cd 100644 --- a/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap +++ b/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap @@ -4,12 +4,12 @@ exports[`Clone Dropdown Button rendering matches the snapshot 1`] = ` <gl-dropdown-stub category="primary" clearalltext="Clear all" + clearalltextclass="gl-px-5" headertext="" hideheaderborder="true" highlighteditemstitle="Selected" highlighteditemstitleclass="gl-px-5" right="true" - showhighlighteditemstitle="true" size="medium" text="Clone" variant="info" @@ -35,6 +35,7 @@ exports[`Clone Dropdown Button rendering matches the snapshot 1`] = ` <b-form-input-stub class="gl-form-input" debounce="0" + formatter="[Function]" readonly="true" type="text" value="ssh://foo.bar" @@ -78,6 +79,7 @@ exports[`Clone Dropdown Button rendering matches the snapshot 1`] = ` <b-form-input-stub class="gl-form-input" debounce="0" + formatter="[Function]" readonly="true" type="text" value="http://foo.bar" diff --git a/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap index f2ff12b2acd..2b89e36344d 100644 --- a/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap +++ b/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap @@ -4,12 +4,12 @@ exports[`SplitButton renders actionItems 1`] = ` <gl-dropdown-stub category="primary" clearalltext="Clear all" + clearalltextclass="gl-px-5" headertext="" hideheaderborder="true" highlighteditemstitle="Selected" highlighteditemstitleclass="gl-px-5" menu-class="" - showhighlighteditemstitle="true" size="medium" split="true" text="professor" diff --git a/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js b/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js index c6c351a7f3f..3277aab43f0 100644 --- a/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js +++ b/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js @@ -1,25 +1,16 @@ import { shallowMount } from '@vue/test-utils'; -import waitForPromises from 'helpers/wait_for_promises'; import { HIGHLIGHT_CLASS_NAME } from '~/vue_shared/components/blob_viewers/constants'; import SimpleViewer from '~/vue_shared/components/blob_viewers/simple_viewer.vue'; -import SourceEditor from '~/vue_shared/components/source_editor.vue'; describe('Blob Simple Viewer component', () => { let wrapper; const contentMock = `<span id="LC1">First</span>\n<span id="LC2">Second</span>\n<span id="LC3">Third</span>`; const blobHash = 'foo-bar'; - function createComponent( - content = contentMock, - isRawContent = false, - isRefactorFlagEnabled = false, - ) { + function createComponent(content = contentMock, isRawContent = false) { wrapper = shallowMount(SimpleViewer, { provide: { blobHash, - glFeatures: { - refactorBlobViewer: isRefactorFlagEnabled, - }, }, propsData: { content, @@ -94,32 +85,4 @@ describe('Blob Simple Viewer component', () => { }); }); }); - - describe('Vue refactoring to use Source Editor', () => { - const findSourceEditor = () => wrapper.find(SourceEditor); - - it.each` - doesRender | condition | isRawContent | isRefactorFlagEnabled - ${'Does not'} | ${'rawContent is not specified'} | ${false} | ${true} - ${'Does not'} | ${'feature flag is disabled is not specified'} | ${true} | ${false} - ${'Does not'} | ${'both, the FF and rawContent are not specified'} | ${false} | ${false} - ${'Does'} | ${'both, the FF and rawContent are specified'} | ${true} | ${true} - `( - '$doesRender render Source Editor component in readonly mode when $condition', - async ({ isRawContent, isRefactorFlagEnabled } = {}) => { - createComponent('raw content', isRawContent, isRefactorFlagEnabled); - await waitForPromises(); - - if (isRawContent && isRefactorFlagEnabled) { - expect(findSourceEditor().exists()).toBe(true); - - expect(findSourceEditor().props('value')).toBe('raw content'); - expect(findSourceEditor().props('fileName')).toBe('test.js'); - expect(findSourceEditor().props('editorOptions')).toEqual({ readOnly: true }); - } else { - expect(findSourceEditor().exists()).toBe(false); - } - }, - ); - }); }); diff --git a/spec/frontend/vue_shared/components/color_picker/color_picker_spec.js b/spec/frontend/vue_shared/components/color_picker/color_picker_spec.js index d30f36ec63c..fef50bdaccc 100644 --- a/spec/frontend/vue_shared/components/color_picker/color_picker_spec.js +++ b/spec/frontend/vue_shared/components/color_picker/color_picker_spec.js @@ -111,15 +111,13 @@ describe('ColorPicker', () => { gon.suggested_label_colors = {}; createComponent(shallowMount); - expect(description()).toBe('Choose any color'); + expect(description()).toBe('Enter any color.'); expect(presetColors().exists()).toBe(false); }); it('shows the suggested colors', () => { createComponent(shallowMount); - expect(description()).toBe( - 'Choose any color. Or you can choose one of the suggested colors below', - ); + expect(description()).toBe('Enter any color or choose one of the suggested colors below.'); expect(presetColors()).toHaveLength(4); }); diff --git a/spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js b/spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js index 175d79dd1c2..194681a6138 100644 --- a/spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js +++ b/spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js @@ -1,4 +1,4 @@ -import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui'; +import { GlAlert, GlSprintf } from '@gitlab/ui'; import { mount, shallowMount } from '@vue/test-utils'; import { useLocalStorageSpy } from 'helpers/local_storage_helper'; import Component from '~/vue_shared/components/dismissible_feedback_alert.vue'; @@ -8,20 +8,13 @@ describe('Dismissible Feedback Alert', () => { let wrapper; - const defaultProps = { - featureName: 'Dependency List', - feedbackLink: 'https://gitlab.link', - }; - + const featureName = 'Dependency List'; const STORAGE_DISMISSAL_KEY = 'dependency_list_feedback_dismissed'; - const createComponent = ({ props, shallow } = {}) => { - const mountFn = shallow ? shallowMount : mount; - + const createComponent = ({ mountFn = shallowMount } = {}) => { wrapper = mountFn(Component, { propsData: { - ...defaultProps, - ...props, + featureName, }, stubs: { GlSprintf, @@ -34,8 +27,8 @@ describe('Dismissible Feedback Alert', () => { wrapper = null; }); - const findAlert = () => wrapper.find(GlAlert); - const findLink = () => wrapper.find(GlLink); + const createFullComponent = () => createComponent({ mountFn: mount }); + const findAlert = () => wrapper.findComponent(GlAlert); describe('with default', () => { beforeEach(() => { @@ -46,17 +39,6 @@ describe('Dismissible Feedback Alert', () => { expect(findAlert().exists()).toBe(true); }); - it('contains feature name', () => { - expect(findAlert().text()).toContain(defaultProps.featureName); - }); - - it('contains provided link', () => { - const link = findLink(); - - expect(link.attributes('href')).toBe(defaultProps.feedbackLink); - expect(link.attributes('target')).toBe('_blank'); - }); - it('should have the storage key set', () => { expect(wrapper.vm.storageKey).toBe(STORAGE_DISMISSAL_KEY); }); @@ -65,7 +47,7 @@ describe('Dismissible Feedback Alert', () => { describe('dismissible', () => { describe('after dismissal', () => { beforeEach(() => { - createComponent({ shallow: false }); + createFullComponent(); findAlert().vm.$emit('dismiss'); }); @@ -81,7 +63,7 @@ describe('Dismissible Feedback Alert', () => { describe('already dismissed', () => { it('should not show the alert once dismissed', async () => { localStorage.setItem(STORAGE_DISMISSAL_KEY, 'true'); - createComponent({ shallow: false }); + createFullComponent(); await wrapper.vm.$nextTick(); expect(findAlert().exists()).toBe(false); diff --git a/spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js b/spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js new file mode 100644 index 00000000000..996df34f2ff --- /dev/null +++ b/spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js @@ -0,0 +1,141 @@ +import { shallowMount } from '@vue/test-utils'; +import DropdownKeyboardNavigation from '~/vue_shared/components/dropdown_keyboard_navigation.vue'; +import { UP_KEY_CODE, DOWN_KEY_CODE, TAB_KEY_CODE } from '~/lib/utils/keycodes'; + +const MOCK_INDEX = 0; +const MOCK_MAX = 10; +const MOCK_MIN = 0; +const MOCK_DEFAULT_INDEX = 0; + +describe('DropdownKeyboardNavigation', () => { + let wrapper; + + const defaultProps = { + index: MOCK_INDEX, + max: MOCK_MAX, + min: MOCK_MIN, + defaultIndex: MOCK_DEFAULT_INDEX, + }; + + const createComponent = (props) => { + wrapper = shallowMount(DropdownKeyboardNavigation, { + propsData: { + ...defaultProps, + ...props, + }, + }); + }; + + const helpers = { + arrowDown: () => { + document.dispatchEvent(new KeyboardEvent('keydown', { keyCode: DOWN_KEY_CODE })); + }, + arrowUp: () => { + document.dispatchEvent(new KeyboardEvent('keydown', { keyCode: UP_KEY_CODE })); + }, + tab: () => { + document.dispatchEvent(new KeyboardEvent('keydown', { keyCode: TAB_KEY_CODE })); + }, + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('onInit', () => { + beforeEach(() => { + createComponent(); + }); + + it('should $emit @change with the default index', async () => { + expect(wrapper.emitted('change')[0]).toStrictEqual([MOCK_DEFAULT_INDEX]); + }); + + it('should $emit @change with the default index when max changes', async () => { + wrapper.setProps({ max: 20 }); + await wrapper.vm.$nextTick(); + // The first @change`call happens on created() so we test for the second [1] + expect(wrapper.emitted('change')[1]).toStrictEqual([MOCK_DEFAULT_INDEX]); + }); + }); + + describe('keydown events', () => { + let incrementSpy; + + beforeEach(() => { + createComponent(); + incrementSpy = jest.spyOn(wrapper.vm, 'increment'); + }); + + afterEach(() => { + incrementSpy.mockRestore(); + }); + + it('onKeydown-Down calls increment(1)', () => { + helpers.arrowDown(); + + expect(incrementSpy).toHaveBeenCalledWith(1); + }); + + it('onKeydown-Up calls increment(-1)', () => { + helpers.arrowUp(); + + expect(incrementSpy).toHaveBeenCalledWith(-1); + }); + + it('onKeydown-Tab $emits @tab event', () => { + helpers.tab(); + + expect(wrapper.emitted('tab')).toHaveLength(1); + }); + }); + + describe('increment', () => { + describe('when max is 0', () => { + beforeEach(() => { + createComponent({ max: 0 }); + }); + + it('does not $emit any @change events', () => { + helpers.arrowDown(); + + // The first @change`call happens on created() so we test that we only have 1 call + expect(wrapper.emitted('change')).toHaveLength(1); + }); + }); + + describe.each` + keyboardAction | direction | index | max | min + ${helpers.arrowDown} | ${1} | ${10} | ${10} | ${0} + ${helpers.arrowUp} | ${-1} | ${0} | ${10} | ${0} + `('moving out of bounds', ({ keyboardAction, direction, index, max, min }) => { + beforeEach(() => { + createComponent({ index, max, min }); + keyboardAction(); + }); + + it(`in ${direction} direction does not $emit any @change events`, () => { + // The first @change`call happens on created() so we test that we only have 1 call + expect(wrapper.emitted('change')).toHaveLength(1); + }); + }); + + describe.each` + keyboardAction | direction | index | max | min + ${helpers.arrowDown} | ${1} | ${0} | ${10} | ${0} + ${helpers.arrowUp} | ${-1} | ${10} | ${10} | ${0} + `('moving in bounds', ({ keyboardAction, direction, index, max, min }) => { + beforeEach(() => { + createComponent({ index, max, min }); + keyboardAction(); + }); + + it(`in ${direction} direction $emits @change event with the correct index ${ + index + direction + }`, () => { + // The first @change`call happens on created() so we test for the second [1] + expect(wrapper.emitted('change')[1]).toStrictEqual([index + direction]); + }); + }); + }); +}); 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 134c6c8b929..ae02c554e13 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 @@ -141,7 +141,62 @@ export const mockEpicToken = { token: EpicToken, operators: OPERATOR_IS_ONLY, idProperty: 'iid', - fetchEpics: () => Promise.resolve({ data: mockEpics }), + fullPath: 'gitlab-org', +}; + +export const mockEpicNode1 = { + __typename: 'Epic', + parent: null, + id: 'gid://gitlab/Epic/40', + iid: '2', + title: 'Marketing epic', + description: 'Mock epic description', + state: 'opened', + startDate: '2017-12-25', + dueDate: '2018-02-15', + webUrl: 'http://gdk.test:3000/groups/gitlab-org/marketing/-/epics/1', + hasChildren: false, + hasParent: false, + confidential: false, +}; + +export const mockEpicNode2 = { + __typename: 'Epic', + parent: null, + id: 'gid://gitlab/Epic/41', + iid: '3', + title: 'Another marketing', + startDate: '2017-12-26', + dueDate: '2018-03-10', + state: 'opened', + webUrl: 'http://gdk.test:3000/groups/gitlab-org/marketing/-/epics/2', +}; + +export const mockGroupEpicsQueryResponse = { + data: { + group: { + id: 'gid://gitlab/Group/1', + name: 'Gitlab Org', + epics: { + edges: [ + { + node: { + ...mockEpicNode1, + }, + __typename: 'EpicEdge', + }, + { + node: { + ...mockEpicNode2, + }, + __typename: 'EpicEdge', + }, + ], + __typename: 'EpicConnection', + }, + __typename: 'Group', + }, + }, }; export const mockReactionEmojiToken = { diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js index d3e1bfef561..14fcffd3c50 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js @@ -57,7 +57,7 @@ function createComponent(options = {}) { provide: { portalName: 'fake target', alignSuggestions: function fakeAlignSuggestions() {}, - suggestionsListClass: 'custom-class', + suggestionsListClass: () => 'custom-class', }, data() { return { ...data }; diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js index eb1dbed52cc..f9ce0338d2f 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js @@ -67,7 +67,7 @@ function createComponent({ provide: { portalName: 'fake target', alignSuggestions: jest.fn(), - suggestionsListClass: 'custom-class', + suggestionsListClass: () => 'custom-class', }, stubs, slots, @@ -206,26 +206,50 @@ describe('BaseToken', () => { describe('events', () => { let wrapperWithNoStubs; - beforeEach(() => { - wrapperWithNoStubs = createComponent({ - stubs: { Portal: true }, - }); - }); - afterEach(() => { wrapperWithNoStubs.destroy(); }); - it('emits `fetch-suggestions` event on component after a delay when component emits `input` event', async () => { - jest.useFakeTimers(); + describe('when activeToken has been selected', () => { + beforeEach(() => { + wrapperWithNoStubs = createComponent({ + props: { + ...mockProps, + getActiveTokenValue: () => ({ title: '' }), + suggestionsLoading: true, + }, + stubs: { Portal: true }, + }); + }); + it('does not emit `fetch-suggestions` event on component after a delay when component emits `input` event', async () => { + jest.useFakeTimers(); - wrapperWithNoStubs.find(GlFilteredSearchToken).vm.$emit('input', { data: 'foo' }); - await wrapperWithNoStubs.vm.$nextTick(); + wrapperWithNoStubs.find(GlFilteredSearchToken).vm.$emit('input', { data: 'foo' }); + await wrapperWithNoStubs.vm.$nextTick(); - jest.runAllTimers(); + jest.runAllTimers(); - expect(wrapperWithNoStubs.emitted('fetch-suggestions')).toBeTruthy(); - expect(wrapperWithNoStubs.emitted('fetch-suggestions')[2]).toEqual(['foo']); + expect(wrapperWithNoStubs.emitted('fetch-suggestions')).toEqual([['']]); + }); + }); + + describe('when activeToken has not been selected', () => { + beforeEach(() => { + wrapperWithNoStubs = createComponent({ + stubs: { Portal: true }, + }); + }); + it('emits `fetch-suggestions` event on component after a delay when component emits `input` event', async () => { + jest.useFakeTimers(); + + wrapperWithNoStubs.find(GlFilteredSearchToken).vm.$emit('input', { data: 'foo' }); + await wrapperWithNoStubs.vm.$nextTick(); + + jest.runAllTimers(); + + expect(wrapperWithNoStubs.emitted('fetch-suggestions')).toBeTruthy(); + expect(wrapperWithNoStubs.emitted('fetch-suggestions')[2]).toEqual(['foo']); + }); }); }); }); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js index 09eac636cae..f3e8b2d0c1b 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js @@ -42,7 +42,7 @@ function createComponent(options = {}) { provide: { portalName: 'fake target', alignSuggestions: function fakeAlignSuggestions() {}, - suggestionsListClass: 'custom-class', + suggestionsListClass: () => 'custom-class', }, stubs, }); 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 index c2d61fd9f05..36071c900df 100644 --- 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 @@ -48,7 +48,7 @@ function createComponent(options = {}) { provide: { portalName: 'fake target', alignSuggestions: function fakeAlignSuggestions() {}, - suggestionsListClass: 'custom-class', + suggestionsListClass: () => 'custom-class', }, stubs, }); 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 index 68ed46fc3a2..6ee5d50d396 100644 --- 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 @@ -1,15 +1,21 @@ -import { GlFilteredSearchToken, GlFilteredSearchTokenSegment } from '@gitlab/ui'; +import { GlFilteredSearchTokenSegment } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; +import searchEpicsQuery from '~/vue_shared/components/filtered_search_bar/queries/search_epics.query.graphql'; import EpicToken from '~/vue_shared/components/filtered_search_bar/tokens/epic_token.vue'; +import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue'; -import { mockEpicToken, mockEpics } from '../mock_data'; +import { mockEpicToken, mockEpics, mockGroupEpicsQueryResponse } from '../mock_data'; jest.mock('~/flash'); +Vue.use(VueApollo); const defaultStubs = { Portal: true, @@ -21,31 +27,39 @@ const defaultStubs = { }, }; -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; + let fakeApollo; + + const findBaseToken = () => wrapper.findComponent(BaseToken); + + function createComponent( + options = {}, + epicsQueryHandler = jest.fn().mockResolvedValue(mockGroupEpicsQueryResponse), + ) { + fakeApollo = createMockApollo([[searchEpicsQuery, epicsQueryHandler]]); + const { + config = mockEpicToken, + value = { data: '' }, + active = false, + stubs = defaultStubs, + } = options; + return mount(EpicToken, { + apolloProvider: fakeApollo, + propsData: { + config, + value, + active, + }, + provide: { + portalName: 'fake target', + alignSuggestions: function fakeAlignSuggestions() {}, + suggestionsListClass: 'custom-class', + }, + stubs, + }); + } beforeEach(() => { mock = new MockAdapter(axios); @@ -71,23 +85,20 @@ describe('EpicToken', () => { describe('methods', () => { describe('fetchEpicsBySearchTerm', () => { - it('calls `config.fetchEpics` with provided searchTerm param', () => { - jest.spyOn(wrapper.vm.config, 'fetchEpics'); + it('calls fetchEpics with provided searchTerm param', () => { + jest.spyOn(wrapper.vm, 'fetchEpics'); - wrapper.vm.fetchEpicsBySearchTerm({ search: 'foo' }); + findBaseToken().vm.$emit('fetch-suggestions', 'foo'); - expect(wrapper.vm.config.fetchEpics).toHaveBeenCalledWith({ - epicPath: '', - search: 'foo', - }); + expect(wrapper.vm.fetchEpics).toHaveBeenCalledWith('foo'); }); it('sets response to `epics` when request is successful', async () => { - jest.spyOn(wrapper.vm.config, 'fetchEpics').mockResolvedValue({ + jest.spyOn(wrapper.vm, 'fetchEpics').mockResolvedValue({ data: mockEpics, }); - wrapper.vm.fetchEpicsBySearchTerm({}); + findBaseToken().vm.$emit('fetch-suggestions'); await waitForPromises(); @@ -95,9 +106,9 @@ describe('EpicToken', () => { }); it('calls `createFlash` with flash error message when request fails', async () => { - jest.spyOn(wrapper.vm.config, 'fetchEpics').mockRejectedValue({}); + jest.spyOn(wrapper.vm, 'fetchEpics').mockRejectedValue({}); - wrapper.vm.fetchEpicsBySearchTerm({ search: 'foo' }); + findBaseToken().vm.$emit('fetch-suggestions', 'foo'); await waitForPromises(); @@ -107,9 +118,9 @@ describe('EpicToken', () => { }); it('sets `loading` to false when request completes', async () => { - jest.spyOn(wrapper.vm.config, 'fetchEpics').mockRejectedValue({}); + jest.spyOn(wrapper.vm, 'fetchEpics').mockRejectedValue({}); - wrapper.vm.fetchEpicsBySearchTerm({ search: 'foo' }); + findBaseToken().vm.$emit('fetch-suggestions', 'foo'); await waitForPromises(); @@ -123,15 +134,15 @@ describe('EpicToken', () => { beforeEach(async () => { wrapper = createComponent({ - value: { data: `${mockEpics[0].group_full_path}::&${mockEpics[0].iid}` }, + value: { data: `${mockEpics[0].title}::&${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 BaseToken component', () => { + expect(findBaseToken().exists()).toBe(true); }); it('renders token item when value is selected', () => { @@ -142,9 +153,9 @@ describe('EpicToken', () => { }); it.each` - value | valueType | tokenValueString - ${`${mockEpics[0].group_full_path}::&${mockEpics[0].iid}`} | ${'string'} | ${`${mockEpics[0].title}::&${mockEpics[0].iid}`} - ${`${mockEpics[1].group_full_path}::&${mockEpics[1].iid}`} | ${'number'} | ${`${mockEpics[1].title}::&${mockEpics[1].iid}`} + value | valueType | tokenValueString + ${`${mockEpics[0].title}::&${mockEpics[0].iid}`} | ${'string'} | ${`${mockEpics[0].title}::&${mockEpics[0].iid}`} + ${`${mockEpics[1].title}::&${mockEpics[1].iid}`} | ${'number'} | ${`${mockEpics[1].title}::&${mockEpics[1].iid}`} `('renders token item when selection is a $valueType', async ({ value, tokenValueString }) => { wrapper.setProps({ value: { data: value }, diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/iteration_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/iteration_token_spec.js index a609aaa1c4e..af90ee93543 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/iteration_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/iteration_token_spec.js @@ -21,7 +21,7 @@ describe('IterationToken', () => { provide: { portalName: 'fake target', alignSuggestions: function fakeAlignSuggestions() {}, - suggestionsListClass: 'custom-class', + suggestionsListClass: () => 'custom-class', }, }); 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 a348344b9dd..f55fb2836e3 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 @@ -48,7 +48,7 @@ function createComponent(options = {}) { provide: { portalName: 'fake target', alignSuggestions: function fakeAlignSuggestions() {}, - suggestionsListClass: 'custom-class', + suggestionsListClass: () => 'custom-class', }, stubs, listeners, diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js index bfb593bf82d..936841651d1 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js @@ -48,7 +48,7 @@ function createComponent(options = {}) { provide: { portalName: 'fake target', alignSuggestions: function fakeAlignSuggestions() {}, - suggestionsListClass: 'custom-class', + suggestionsListClass: () => 'custom-class', }, stubs, }); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/weight_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/weight_token_spec.js index e788c742736..4277899f8db 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/weight_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/weight_token_spec.js @@ -19,7 +19,7 @@ describe('WeightToken', () => { provide: { portalName: 'fake target', alignSuggestions: function fakeAlignSuggestions() {}, - suggestionsListClass: 'custom-class', + suggestionsListClass: () => 'custom-class', }, }); diff --git a/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js b/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js index 2658fa4a706..f74b9b37197 100644 --- a/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js +++ b/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js @@ -94,10 +94,6 @@ describe('IssueAssigneesComponent', () => { expect(vm.avatarUrlTitle(mockAssigneesList[0])).toBe('Assigned to Terrell Graham'); }); - it('renders component root element with class `issue-assignees`', () => { - expect(wrapper.element.classList.contains('issue-assignees')).toBe(true); - }); - it('renders assignee', () => { const data = findAvatars().wrappers.map((x) => ({ ...x.props(), diff --git a/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js b/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js index ba2450b56c9..9bc2aad1895 100644 --- a/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js +++ b/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js @@ -60,7 +60,7 @@ describe('Suggestion Diff component', () => { expect(findHelpButton().exists()).toBe(true); }); - it('renders apply suggestion and add to batch buttons', () => { + it('renders add to batch button when more than 1 suggestion', () => { createComponent({ suggestionsCount: 2, }); @@ -68,8 +68,7 @@ describe('Suggestion Diff component', () => { const applyBtn = findApplyButton(); const addToBatchBtn = findAddToBatchButton(); - expect(applyBtn.exists()).toBe(true); - expect(applyBtn.html().includes('Apply suggestion')).toBe(true); + expect(applyBtn.exists()).toBe(false); expect(addToBatchBtn.exists()).toBe(true); expect(addToBatchBtn.html().includes('Add suggestion to batch')).toBe(true); @@ -85,7 +84,7 @@ describe('Suggestion Diff component', () => { describe('when apply suggestion is clicked', () => { beforeEach(() => { - createComponent(); + createComponent({ batchSuggestionsCount: 0 }); findApplyButton().vm.$emit('apply'); }); @@ -140,11 +139,11 @@ describe('Suggestion Diff component', () => { describe('apply suggestions is clicked', () => { it('emits applyBatch', () => { - createComponent({ isBatched: true }); + createComponent({ isBatched: true, batchSuggestionsCount: 2 }); - findApplyBatchButton().vm.$emit('click'); + findApplyButton().vm.$emit('apply'); - expect(wrapper.emitted().applyBatch).toEqual([[]]); + expect(wrapper.emitted().applyBatch).toEqual([[undefined]]); }); }); @@ -155,23 +154,24 @@ describe('Suggestion Diff component', () => { isBatched: true, }); - const applyBatchBtn = findApplyBatchButton(); + const applyBatchBtn = findApplyButton(); const removeFromBatchBtn = findRemoveFromBatchButton(); expect(removeFromBatchBtn.exists()).toBe(true); expect(removeFromBatchBtn.html().includes('Remove from batch')).toBe(true); expect(applyBatchBtn.exists()).toBe(true); - expect(applyBatchBtn.html().includes('Apply suggestions')).toBe(true); + expect(applyBatchBtn.html().includes('Apply suggestion')).toBe(true); expect(applyBatchBtn.html().includes(String('9'))).toBe(true); }); it('hides add to batch and apply buttons', () => { createComponent({ isBatched: true, + batchSuggestionsCount: 9, }); - expect(findApplyButton().exists()).toBe(false); + expect(findApplyButton().exists()).toBe(true); expect(findAddToBatchButton().exists()).toBe(false); }); @@ -215,9 +215,8 @@ describe('Suggestion Diff component', () => { }); it('disables apply suggestion and hides add to batch button', () => { - expect(findApplyButton().exists()).toBe(true); + expect(findApplyButton().exists()).toBe(false); expect(findAddToBatchButton().exists()).toBe(false); - expect(findApplyButton().attributes('disabled')).toBe('true'); }); }); @@ -225,7 +224,7 @@ describe('Suggestion Diff component', () => { const findTooltip = () => getBinding(findApplyButton().element, 'gl-tooltip'); it('renders correct tooltip message when button is applicable', () => { - createComponent(); + createComponent({ batchSuggestionsCount: 0 }); const tooltip = findTooltip(); expect(tooltip.modifiers.viewport).toBe(true); @@ -234,7 +233,7 @@ describe('Suggestion Diff component', () => { it('renders the inapplicable reason in the tooltip when button is not applicable', () => { const inapplicableReason = 'lorem'; - createComponent({ canApply: false, inapplicableReason }); + createComponent({ canApply: false, inapplicableReason, batchSuggestionsCount: 0 }); const tooltip = findTooltip(); expect(tooltip.modifiers.viewport).toBe(true); diff --git a/spec/frontend/vue_shared/components/markdown/suggestion_diff_spec.js b/spec/frontend/vue_shared/components/markdown/suggestion_diff_spec.js index 5bd6bda2d2c..af27e953776 100644 --- a/spec/frontend/vue_shared/components/markdown/suggestion_diff_spec.js +++ b/spec/frontend/vue_shared/components/markdown/suggestion_diff_spec.js @@ -77,7 +77,7 @@ describe('Suggestion Diff component', () => { it.each` event | childArgs | args ${'apply'} | ${['test-event']} | ${[{ callback: 'test-event', suggestionId }]} - ${'applyBatch'} | ${[]} | ${[]} + ${'applyBatch'} | ${['test-event']} | ${['test-event']} ${'addToBatch'} | ${[]} | ${[suggestionId]} ${'removeFromBatch'} | ${[]} | ${[suggestionId]} `('emits $event event on sugestion diff header $event', ({ event, childArgs, args }) => { diff --git a/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js b/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js index ab028ea52b7..1ed7844b395 100644 --- a/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js +++ b/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js @@ -1,4 +1,6 @@ import { shallowMount, createLocalVue } from '@vue/test-utils'; +// eslint-disable-next-line import/no-deprecated +import { getJSONFixture } from 'helpers/fixtures'; import { trimText } from 'helpers/text_helper'; import ProjectAvatar from '~/vue_shared/components/deprecated_project_avatar/default.vue'; import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue'; @@ -11,6 +13,7 @@ describe('ProjectListItem component', () => { let vm; let options; + // eslint-disable-next-line import/no-deprecated const project = getJSONFixture('static/projects.json')[0]; beforeEach(() => { diff --git a/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js b/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js index 06b00a8e196..1f97d3ff3fa 100644 --- a/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js +++ b/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js @@ -2,6 +2,8 @@ import { GlSearchBoxByType, GlInfiniteScroll } from '@gitlab/ui'; import { mount, createLocalVue } from '@vue/test-utils'; import { head } from 'lodash'; import Vue from 'vue'; +// eslint-disable-next-line import/no-deprecated +import { getJSONFixture } from 'helpers/fixtures'; import { trimText } from 'helpers/text_helper'; import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue'; import ProjectSelector from '~/vue_shared/components/project_selector/project_selector.vue'; @@ -11,6 +13,7 @@ const localVue = createLocalVue(); describe('ProjectSelector component', () => { let wrapper; let vm; + // eslint-disable-next-line import/no-deprecated const allProjects = getJSONFixture('static/projects.json'); const searchResults = allProjects.slice(0, 5); let selected = []; diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js index 14e0c8a2278..d9b7cd5afa2 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js @@ -157,9 +157,9 @@ describe('LabelsSelect Mutations', () => { beforeEach(() => { labels = [ - { id: 1, title: 'scoped::test', set: true }, - { id: 2, set: false, title: 'scoped::one' }, - { id: 3, title: '' }, + { id: 1, title: 'scoped' }, + { id: 2, title: 'scoped::one', set: false }, + { id: 3, title: 'scoped::test', set: true }, { id: 4, title: '' }, ]; }); @@ -189,9 +189,9 @@ describe('LabelsSelect Mutations', () => { }); expect(state.labels).toEqual([ - { id: 1, title: 'scoped::test', set: false }, - { id: 2, set: true, title: 'scoped::one', touched: true }, - { id: 3, title: '' }, + { id: 1, title: 'scoped' }, + { id: 2, title: 'scoped::one', set: true, touched: true }, + { id: 3, title: 'scoped::test', set: false }, { id: 4, title: '' }, ]); }); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js index 843298a1406..8931584e12c 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js @@ -5,13 +5,14 @@ import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import createFlash from '~/flash'; +import { IssuableType } from '~/issue_show/constants'; +import { labelsQueries } from '~/sidebar/constants'; import DropdownContentsCreateView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue'; import createLabelMutation from '~/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql'; -import projectLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql'; import { mockSuggestedColors, createLabelSuccessfulResponse, - labelsQueryResponse, + workspaceLabelsQueryResponse, } from './mock_data'; jest.mock('~/flash'); @@ -47,11 +48,14 @@ describe('DropdownContentsCreateView', () => { findAllColors().at(0).vm.$emit('click', new Event('mouseclick')); }; - const createComponent = ({ mutationHandler = createLabelSuccessHandler } = {}) => { + const createComponent = ({ + mutationHandler = createLabelSuccessHandler, + issuableType = IssuableType.Issue, + } = {}) => { const mockApollo = createMockApollo([[createLabelMutation, mutationHandler]]); mockApollo.clients.defaultClient.cache.writeQuery({ - query: projectLabelsQuery, - data: labelsQueryResponse.data, + query: labelsQueries[issuableType].workspaceQuery, + data: workspaceLabelsQueryResponse.data, variables: { fullPath: '', searchTerm: '', @@ -61,6 +65,10 @@ describe('DropdownContentsCreateView', () => { wrapper = shallowMount(DropdownContentsCreateView, { localVue, apolloProvider: mockApollo, + propsData: { + issuableType, + fullPath: '', + }, }); }; @@ -135,15 +143,6 @@ describe('DropdownContentsCreateView', () => { expect(findCreateButton().props('disabled')).toBe(false); }); - it('calls a mutation with correct parameters on Create button click', () => { - findCreateButton().vm.$emit('click'); - expect(createLabelSuccessHandler).toHaveBeenCalledWith({ - color: '#009966', - projectPath: '', - title: 'Test title', - }); - }); - it('renders a loader spinner after Create button click', async () => { findCreateButton().vm.$emit('click'); await nextTick(); @@ -162,6 +161,30 @@ describe('DropdownContentsCreateView', () => { }); }); + it('calls a mutation with `projectPath` variable on the issue', () => { + createComponent(); + fillLabelAttributes(); + findCreateButton().vm.$emit('click'); + + expect(createLabelSuccessHandler).toHaveBeenCalledWith({ + color: '#009966', + projectPath: '', + title: 'Test title', + }); + }); + + it('calls a mutation with `groupPath` variable on the epic', () => { + createComponent({ issuableType: IssuableType.Epic }); + fillLabelAttributes(); + findCreateButton().vm.$emit('click'); + + expect(createLabelSuccessHandler).toHaveBeenCalledWith({ + color: '#009966', + groupPath: '', + title: 'Test title', + }); + }); + it('calls createFlash is mutation has a user-recoverable error', async () => { createComponent({ mutationHandler: createLabelUserRecoverableErrorHandler }); fillLabelAttributes(); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js index 537bbc8e71e..fac3331a2b8 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js @@ -1,36 +1,43 @@ -import { GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui'; +import { + GlLoadingIcon, + GlSearchBoxByType, + GlDropdownItem, + GlIntersectionObserver, +} from '@gitlab/ui'; import { shallowMount, createLocalVue } from '@vue/test-utils'; import { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import createFlash from '~/flash'; +import { IssuableType } from '~/issue_show/constants'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_widget/constants'; import DropdownContentsLabelsView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue'; import projectLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql'; import LabelItem from '~/vue_shared/components/sidebar/labels_select_widget/label_item.vue'; -import { mockConfig, labelsQueryResponse } from './mock_data'; +import { mockConfig, workspaceLabelsQueryResponse } from './mock_data'; jest.mock('~/flash'); const localVue = createLocalVue(); localVue.use(VueApollo); -const selectedLabels = [ +const localSelectedLabels = [ { - id: 28, - title: 'Bug', - description: 'Label for bugs', - color: '#FF0000', - textColor: '#FFFFFF', + color: '#2f7b2e', + description: null, + id: 'gid://gitlab/ProjectLabel/2', + title: 'Label2', }, ]; describe('DropdownContentsLabelsView', () => { let wrapper; - const successfulQueryHandler = jest.fn().mockResolvedValue(labelsQueryResponse); + const successfulQueryHandler = jest.fn().mockResolvedValue(workspaceLabelsQueryResponse); + + const findFirstLabel = () => wrapper.findAllComponents(GlDropdownItem).at(0); const createComponent = ({ initialState = mockConfig, @@ -43,14 +50,13 @@ describe('DropdownContentsLabelsView', () => { localVue, apolloProvider: mockApollo, provide: { - projectPath: 'test', - iid: 1, variant: DropdownVariant.Sidebar, ...injected, }, propsData: { ...initialState, - selectedLabels, + localSelectedLabels, + issuableType: IssuableType.Issue, }, stubs: { GlSearchBoxByType, @@ -65,23 +71,31 @@ describe('DropdownContentsLabelsView', () => { const findSearchInput = () => wrapper.findComponent(GlSearchBoxByType); const findLabels = () => wrapper.findAllComponents(LabelItem); const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); + const findObserver = () => wrapper.findComponent(GlIntersectionObserver); const findLabelsList = () => wrapper.find('[data-testid="labels-list"]'); const findNoResultsMessage = () => wrapper.find('[data-testid="no-results"]'); + async function makeObserverAppear() { + await findObserver().vm.$emit('appear'); + } + describe('when loading labels', () => { it('renders disabled search input field', async () => { createComponent(); + await makeObserverAppear(); expect(findSearchInput().props('disabled')).toBe(true); }); it('renders loading icon', async () => { createComponent(); + await makeObserverAppear(); expect(findLoadingIcon().exists()).toBe(true); }); it('does not render labels list', async () => { createComponent(); + await makeObserverAppear(); expect(findLabelsList().exists()).toBe(false); }); }); @@ -89,6 +103,7 @@ describe('DropdownContentsLabelsView', () => { describe('when labels are loaded', () => { beforeEach(async () => { createComponent(); + await makeObserverAppear(); await waitForPromises(); }); @@ -118,6 +133,7 @@ describe('DropdownContentsLabelsView', () => { }, }), }); + await makeObserverAppear(); findSearchInput().vm.$emit('input', '123'); await waitForPromises(); await nextTick(); @@ -127,8 +143,26 @@ describe('DropdownContentsLabelsView', () => { it('calls `createFlash` when fetching labels failed', async () => { createComponent({ queryHandler: jest.fn().mockRejectedValue('Houston, we have a problem!') }); + await makeObserverAppear(); jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS); await waitForPromises(); + expect(createFlash).toHaveBeenCalled(); }); + + it('emits an `input` event on label click', async () => { + createComponent(); + await makeObserverAppear(); + await waitForPromises(); + findFirstLabel().trigger('click'); + + expect(wrapper.emitted('input')[0][0]).toEqual(expect.arrayContaining(localSelectedLabels)); + }); + + it('does not trigger query when component did not appear', () => { + createComponent(); + expect(findLoadingIcon().exists()).toBe(false); + expect(findLabelsList().exists()).toBe(false); + expect(successfulQueryHandler).not.toHaveBeenCalled(); + }); }); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js index a1b40a891ec..36704ac5ef3 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js @@ -1,6 +1,5 @@ -import { GlDropdown } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; - +import { nextTick } from 'vue'; import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_widget/constants'; import DropdownContents from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue'; import DropdownContentsCreateView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue'; @@ -8,10 +7,26 @@ import DropdownContentsLabelsView from '~/vue_shared/components/sidebar/labels_s import { mockLabels } from './mock_data'; +const showDropdown = jest.fn(); + +const GlDropdownStub = { + template: ` + <div data-testid="dropdown"> + <slot name="header"></slot> + <slot></slot> + <slot name="footer"></slot> + </div> + `, + methods: { + show: showDropdown, + hide: jest.fn(), + }, +}; + describe('DropdownContent', () => { let wrapper; - const createComponent = ({ props = {}, injected = {} } = {}) => { + const createComponent = ({ props = {}, injected = {}, data = {} } = {}) => { wrapper = shallowMount(DropdownContents, { propsData: { labelsCreateTitle: 'test', @@ -22,38 +37,112 @@ describe('DropdownContent', () => { footerManageLabelTitle: 'manage', dropdownButtonText: 'Labels', variant: 'sidebar', + issuableType: 'issue', + fullPath: 'test', ...props, }, + data() { + return { + ...data, + }; + }, provide: { allowLabelCreate: true, labelsManagePath: 'foo/bar', ...injected, }, stubs: { - GlDropdown, + GlDropdown: GlDropdownStub, }, }); }; - beforeEach(() => { - createComponent(); - }); - afterEach(() => { wrapper.destroy(); }); + const findCreateView = () => wrapper.findComponent(DropdownContentsCreateView); + const findLabelsView = () => wrapper.findComponent(DropdownContentsLabelsView); + const findDropdown = () => wrapper.findComponent(GlDropdownStub); + const findDropdownFooter = () => wrapper.find('[data-testid="dropdown-footer"]'); + const findDropdownHeader = () => wrapper.find('[data-testid="dropdown-header"]'); const findCreateLabelButton = () => wrapper.find('[data-testid="create-label-button"]'); const findGoBackButton = () => wrapper.find('[data-testid="go-back-button"]'); + it('calls dropdown `show` method on `isVisible` prop change', async () => { + createComponent(); + await wrapper.setProps({ + isVisible: true, + }); + + expect(findDropdown().emitted('show')).toBeUndefined(); + }); + + it('does not emit `setLabels` event on dropdown hide if labels did not change', () => { + createComponent(); + findDropdown().vm.$emit('hide'); + + expect(wrapper.emitted('setLabels')).toBeUndefined(); + }); + + it('emits `setLabels` event on dropdown hide if labels changed on non-sidebar widget', async () => { + createComponent({ props: { variant: DropdownVariant.Standalone } }); + const updatedLabel = { + id: 28, + title: 'Bug', + description: 'Label for bugs', + color: '#FF0000', + textColor: '#FFFFFF', + }; + findLabelsView().vm.$emit('input', [updatedLabel]); + await nextTick(); + findDropdown().vm.$emit('hide'); + + expect(wrapper.emitted('setLabels')).toEqual([[[updatedLabel]]]); + }); + + it('emits `setLabels` event on visibility change if labels changed on sidebar widget', async () => { + createComponent({ props: { variant: DropdownVariant.Standalone, isVisible: true } }); + const updatedLabel = { + id: 28, + title: 'Bug', + description: 'Label for bugs', + color: '#FF0000', + textColor: '#FFFFFF', + }; + findLabelsView().vm.$emit('input', [updatedLabel]); + wrapper.setProps({ isVisible: false }); + await nextTick(); + + expect(wrapper.emitted('setLabels')).toEqual([[[updatedLabel]]]); + }); + + it('does not render header on standalone variant', () => { + createComponent({ props: { variant: DropdownVariant.Standalone } }); + + expect(findDropdownHeader().exists()).toBe(false); + }); + + it('renders header on embedded variant', () => { + createComponent({ props: { variant: DropdownVariant.Embedded } }); + + expect(findDropdownHeader().exists()).toBe(true); + }); + + it('renders header on sidebar variant', () => { + createComponent(); + + expect(findDropdownHeader().exists()).toBe(true); + }); + describe('Create view', () => { beforeEach(() => { - wrapper.vm.toggleDropdownContentsCreateView(); + createComponent({ data: { showDropdownContentsCreateView: true } }); }); it('renders create view when `showDropdownContentsCreateView` prop is `true`', () => { - expect(wrapper.findComponent(DropdownContentsCreateView).exists()).toBe(true); + expect(findCreateView().exists()).toBe(true); }); it('does not render footer', () => { @@ -67,11 +156,31 @@ describe('DropdownContent', () => { it('renders go back button', () => { expect(findGoBackButton().exists()).toBe(true); }); + + it('changes the view to Labels view on back button click', async () => { + findGoBackButton().vm.$emit('click', new MouseEvent('click')); + await nextTick(); + + expect(findCreateView().exists()).toBe(false); + expect(findLabelsView().exists()).toBe(true); + }); + + it('changes the view to Labels view on `hideCreateView` event', async () => { + findCreateView().vm.$emit('hideCreateView'); + await nextTick(); + + expect(findCreateView().exists()).toBe(false); + expect(findLabelsView().exists()).toBe(true); + }); }); describe('Labels view', () => { + beforeEach(() => { + createComponent(); + }); + it('renders labels view when `showDropdownContentsCreateView` when `showDropdownContentsCreateView` prop is `false`', () => { - expect(wrapper.findComponent(DropdownContentsLabelsView).exists()).toBe(true); + expect(findLabelsView().exists()).toBe(true); }); it('renders footer on sidebar dropdown', () => { @@ -109,19 +218,12 @@ describe('DropdownContent', () => { expect(findCreateLabelButton().exists()).toBe(true); }); - it('triggers `toggleDropdownContent` method on create label button click', () => { - jest.spyOn(wrapper.vm, 'toggleDropdownContent').mockImplementation(() => {}); + it('changes the view to Create on create label button click', async () => { findCreateLabelButton().trigger('click'); - expect(wrapper.vm.toggleDropdownContent).toHaveBeenCalled(); + await nextTick(); + expect(findLabelsView().exists()).toBe(false); }); }); }); - - describe('template', () => { - it('renders component container element with classes `gl-w-full gl-mt-2` and no styles', () => { - expect(wrapper.attributes('class')).toContain('gl-w-full gl-mt-2'); - expect(wrapper.attributes('style')).toBeUndefined(); - }); - }); }); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js index a18511fa21d..b5441d711a5 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js @@ -1,28 +1,55 @@ -import { shallowMount } from '@vue/test-utils'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import createFlash from '~/flash'; +import { IssuableType } from '~/issue_show/constants'; import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue'; import DropdownContents from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue'; import DropdownValue from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue'; -import DropdownValueCollapsed from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_value_collapsed.vue'; +import issueLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql'; import LabelsSelectRoot from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue'; +import { mockConfig, issuableLabelsQueryResponse } from './mock_data'; -import { mockConfig } from './mock_data'; +jest.mock('~/flash'); + +const localVue = createLocalVue(); +localVue.use(VueApollo); + +const successfulQueryHandler = jest.fn().mockResolvedValue(issuableLabelsQueryResponse); +const errorQueryHandler = jest.fn().mockRejectedValue('Houston, we have a problem'); describe('LabelsSelectRoot', () => { let wrapper; - const createComponent = (config = mockConfig, slots = {}) => { + const findSidebarEditableItem = () => wrapper.findComponent(SidebarEditableItem); + const findDropdownValue = () => wrapper.findComponent(DropdownValue); + const findDropdownContents = () => wrapper.findComponent(DropdownContents); + + const createComponent = ({ + config = mockConfig, + slots = {}, + queryHandler = successfulQueryHandler, + } = {}) => { + const mockApollo = createMockApollo([[issueLabelsQuery, queryHandler]]); + wrapper = shallowMount(LabelsSelectRoot, { slots, - propsData: config, + apolloProvider: mockApollo, + localVue, + propsData: { + ...config, + issuableType: IssuableType.Issue, + }, stubs: { - DropdownContents, SidebarEditableItem, }, provide: { - iid: '1', - projectPath: 'test', canUpdate: true, allowLabelEdit: true, + allowLabelCreate: true, + labelsManagePath: 'test', }, }); }; @@ -42,33 +69,63 @@ describe('LabelsSelectRoot', () => { ${'embedded'} | ${'is-embedded'} `( 'renders component root element with CSS class `$cssClass` when `state.variant` is "$variant"', - ({ variant, cssClass }) => { + async ({ variant, cssClass }) => { createComponent({ - ...mockConfig, - variant, + config: { ...mockConfig, variant }, }); - return wrapper.vm.$nextTick(() => { - expect(wrapper.classes()).toContain(cssClass); - }); + await nextTick(); + expect(wrapper.classes()).toContain(cssClass); }, ); - it('renders `dropdown-value-collapsed` component when `allowLabelCreate` prop is `true`', async () => { - createComponent(); - await wrapper.vm.$nextTick; - expect(wrapper.find(DropdownValueCollapsed).exists()).toBe(true); - }); + describe('if dropdown variant is `sidebar`', () => { + it('renders sidebar editable item', () => { + createComponent(); + expect(findSidebarEditableItem().exists()).toBe(true); + }); + + it('passes true `loading` prop to sidebar editable item when loading labels', () => { + createComponent(); + expect(findSidebarEditableItem().props('loading')).toBe(true); + }); - it('renders `dropdown-value` component', async () => { - createComponent(mockConfig, { - default: 'None', + describe('when labels are fetched successfully', () => { + beforeEach(async () => { + createComponent(); + await waitForPromises(); + }); + + it('passes true `loading` prop to sidebar editable item', () => { + expect(findSidebarEditableItem().props('loading')).toBe(false); + }); + + it('renders dropdown value component when query labels is resolved', () => { + expect(findDropdownValue().exists()).toBe(true); + expect(findDropdownValue().props('selectedLabels')).toEqual( + issuableLabelsQueryResponse.data.workspace.issuable.labels.nodes, + ); + }); + + it('emits `onLabelRemove` event on dropdown value label remove event', () => { + const label = { id: 'gid://gitlab/ProjectLabel/1' }; + findDropdownValue().vm.$emit('onLabelRemove', label); + expect(wrapper.emitted('onLabelRemove')).toEqual([[label]]); + }); }); - await wrapper.vm.$nextTick; - const valueComp = wrapper.find(DropdownValue); + it('creates flash with error message when query is rejected', async () => { + createComponent({ queryHandler: errorQueryHandler }); + await waitForPromises(); + expect(createFlash).toHaveBeenCalledWith({ message: 'Error fetching labels.' }); + }); + }); + + it('emits `updateSelectedLabels` event on dropdown contents `setLabels` event', async () => { + const label = { id: 'gid://gitlab/ProjectLabel/1' }; + createComponent(); - expect(valueComp.exists()).toBe(true); - expect(valueComp.text()).toBe('None'); + findDropdownContents().vm.$emit('setLabels', [label]); + expect(wrapper.emitted('updateSelectedLabels')).toEqual([[[label]]]); }); }); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js index fceaabec2d0..23a457848d9 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js @@ -34,6 +34,8 @@ export const mockLabels = [ ]; export const mockConfig = { + iid: '1', + fullPath: 'test', allowMultiselect: true, labelsListTitle: 'Assign labels', labelsCreateTitle: 'Create label', @@ -86,7 +88,7 @@ export const createLabelSuccessfulResponse = { }, }; -export const labelsQueryResponse = { +export const workspaceLabelsQueryResponse = { data: { workspace: { labels: { @@ -108,3 +110,23 @@ export const labelsQueryResponse = { }, }, }; + +export const issuableLabelsQueryResponse = { + data: { + workspace: { + issuable: { + id: '1', + labels: { + nodes: [ + { + color: '#330066', + description: null, + id: 'gid://gitlab/ProjectLabel/1', + title: 'Label1', + }, + ], + }, + }, + }, + }, +}; diff --git a/spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap b/spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap index af4fa462cbf..0f1e118d44c 100644 --- a/spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap +++ b/spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap @@ -45,6 +45,7 @@ exports[`Upload dropzone component correctly overrides description and drop mess > <div class="mw-50 gl-text-center" + style="display: none;" > <h3 class="" @@ -61,7 +62,6 @@ exports[`Upload dropzone component correctly overrides description and drop mess <div class="mw-50 gl-text-center" - style="display: none;" > <h3 class="" @@ -146,7 +146,6 @@ exports[`Upload dropzone component when dragging renders correct template when d <div class="mw-50 gl-text-center" - style="" > <h3 class="" @@ -231,7 +230,6 @@ exports[`Upload dropzone component when dragging renders correct template when d <div class="mw-50 gl-text-center" - style="" > <h3 class="" @@ -299,6 +297,7 @@ exports[`Upload dropzone component when dragging renders correct template when d > <div class="mw-50 gl-text-center" + style="" > <h3 class="" @@ -383,6 +382,7 @@ exports[`Upload dropzone component when dragging renders correct template when d > <div class="mw-50 gl-text-center" + style="" > <h3 class="" @@ -467,6 +467,7 @@ exports[`Upload dropzone component when dragging renders correct template when d > <div class="mw-50 gl-text-center" + style="" > <h3 class="" @@ -551,6 +552,7 @@ exports[`Upload dropzone component when no slot provided renders default dropzon > <div class="mw-50 gl-text-center" + style="display: none;" > <h3 class="" @@ -567,7 +569,6 @@ exports[`Upload dropzone component when no slot provided renders default dropzon <div class="mw-50 gl-text-center" - style="display: none;" > <h3 class="" @@ -603,6 +604,7 @@ exports[`Upload dropzone component when slot provided renders dropzone with slot > <div class="mw-50 gl-text-center" + style="display: none;" > <h3 class="" @@ -619,7 +621,6 @@ exports[`Upload dropzone component when slot provided renders dropzone with slot <div class="mw-50 gl-text-center" - style="display: none;" > <h3 class="" diff --git a/spec/frontend/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list_spec.js b/spec/frontend/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list_spec.js new file mode 100644 index 00000000000..a92f058f311 --- /dev/null +++ b/spec/frontend/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list_spec.js @@ -0,0 +1,116 @@ +import { GlLink, GlSprintf } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import { OBSTACLE_TYPES } from '~/vue_shared/components/user_deletion_obstacles/constants'; +import UserDeletionObstaclesList from '~/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.vue'; + +const mockSchedules = [ + { + type: OBSTACLE_TYPES.oncallSchedules, + name: 'Schedule 1', + url: 'http://gitlab.com/gitlab-org/gitlab-shell/-/oncall_schedules', + projectName: 'Shell', + projectUrl: 'http://gitlab.com/gitlab-org/gitlab-shell/', + }, + { + type: OBSTACLE_TYPES.oncallSchedules, + name: 'Schedule 2', + url: 'http://gitlab.com/gitlab-org/gitlab-ui/-/oncall_schedules', + projectName: 'UI', + projectUrl: 'http://gitlab.com/gitlab-org/gitlab-ui/', + }, +]; +const mockPolicies = [ + { + type: OBSTACLE_TYPES.escalationPolicies, + name: 'Policy 1', + url: 'http://gitlab.com/gitlab-org/gitlab-ui/-/escalation-policies', + projectName: 'UI', + projectUrl: 'http://gitlab.com/gitlab-org/gitlab-ui/', + }, +]; +const mockObstacles = mockSchedules.concat(mockPolicies); + +const userName = "O'User"; + +describe('User deletion obstacles list', () => { + let wrapper; + + function createComponent(props) { + wrapper = extendedWrapper( + shallowMount(UserDeletionObstaclesList, { + propsData: { + obstacles: mockObstacles, + userName, + ...props, + }, + stubs: { + GlSprintf, + }, + }), + ); + } + + afterEach(() => { + wrapper.destroy(); + }); + + const findLinks = () => wrapper.findAllComponents(GlLink); + const findTitle = () => wrapper.findByTestId('title'); + const findFooter = () => wrapper.findByTestId('footer'); + const findObstacles = () => wrapper.findByTestId('obstacles-list'); + + describe.each` + isCurrentUser | titleText | footerText + ${true} | ${'You are currently a part of:'} | ${'Removing yourself may put your on-call team at risk of missing a notification.'} + ${false} | ${`User ${userName} is currently part of:`} | ${'Removing this user may put their on-call team at risk of missing a notification.'} + `('when current user', ({ isCurrentUser, titleText, footerText }) => { + it(`${isCurrentUser ? 'is' : 'is not'} a part of on-call management`, async () => { + createComponent({ + isCurrentUser, + }); + + expect(findTitle().text()).toBe(titleText); + expect(findFooter().text()).toBe(footerText); + }); + }); + + describe.each(mockObstacles)( + 'renders all obstacles', + ({ type, name, url, projectName, projectUrl }) => { + it(`includes the project name and link for ${name}`, () => { + createComponent({ obstacles: [{ type, name, url, projectName, projectUrl }] }); + const msg = findObstacles().text(); + + expect(msg).toContain(`in Project ${projectName}`); + expect(findLinks().at(1).attributes('href')).toBe(projectUrl); + }); + }, + ); + + describe.each(mockSchedules)( + 'renders on-call schedules', + ({ type, name, url, projectName, projectUrl }) => { + it(`includes the schedule name and link for ${name}`, () => { + createComponent({ obstacles: [{ type, name, url, projectName, projectUrl }] }); + const msg = findObstacles().text(); + + expect(msg).toContain(`On-call schedule ${name}`); + expect(findLinks().at(0).attributes('href')).toBe(url); + }); + }, + ); + + describe.each(mockPolicies)( + 'renders escalation policies', + ({ type, name, url, projectName, projectUrl }) => { + it(`includes the policy name and link for ${name}`, () => { + createComponent({ obstacles: [{ type, name, url, projectName, projectUrl }] }); + const msg = findObstacles().text(); + + expect(msg).toContain(`Escalation policy ${name}`); + expect(findLinks().at(0).attributes('href')).toBe(url); + }); + }, + ); +}); diff --git a/spec/frontend/vue_shared/components/user_deletion_obstacles/utils_spec.js b/spec/frontend/vue_shared/components/user_deletion_obstacles/utils_spec.js new file mode 100644 index 00000000000..99f739098f7 --- /dev/null +++ b/spec/frontend/vue_shared/components/user_deletion_obstacles/utils_spec.js @@ -0,0 +1,43 @@ +import { OBSTACLE_TYPES } from '~/vue_shared/components/user_deletion_obstacles/constants'; +import { parseUserDeletionObstacles } from '~/vue_shared/components/user_deletion_obstacles/utils'; + +describe('parseUserDeletionObstacles', () => { + const mockObstacles = [{ name: 'Obstacle' }]; + const expectedSchedule = { name: 'Obstacle', type: OBSTACLE_TYPES.oncallSchedules }; + const expectedPolicy = { name: 'Obstacle', type: OBSTACLE_TYPES.escalationPolicies }; + + it('is undefined when user is not available', () => { + expect(parseUserDeletionObstacles()).toHaveLength(0); + }); + + it('is empty when obstacles are not available for user', () => { + expect(parseUserDeletionObstacles({})).toHaveLength(0); + }); + + it('is empty when user has no obstacles to deletion', () => { + const input = { oncallSchedules: [], escalationPolicies: [] }; + + expect(parseUserDeletionObstacles(input)).toHaveLength(0); + }); + + it('returns obstacles with type when user is part of on-call schedules', () => { + const input = { oncallSchedules: mockObstacles, escalationPolicies: [] }; + const expectedOutput = [expectedSchedule]; + + expect(parseUserDeletionObstacles(input)).toEqual(expectedOutput); + }); + + it('returns obstacles with type when user is part of escalation policies', () => { + const input = { oncallSchedules: [], escalationPolicies: mockObstacles }; + const expectedOutput = [expectedPolicy]; + + expect(parseUserDeletionObstacles(input)).toEqual(expectedOutput); + }); + + it('returns obstacles with type when user have every obstacle type', () => { + const input = { oncallSchedules: mockObstacles, escalationPolicies: mockObstacles }; + const expectedOutput = [expectedSchedule, expectedPolicy]; + + expect(parseUserDeletionObstacles(input)).toEqual(expectedOutput); + }); +}); diff --git a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js index 926223e0670..09633daf587 100644 --- a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js +++ b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js @@ -9,6 +9,7 @@ const DEFAULT_PROPS = { username: 'root', name: 'Administrator', location: 'Vienna', + localTime: '2:30 PM', bot: false, bio: null, workInformation: null, @@ -31,10 +32,11 @@ describe('User Popover Component', () => { wrapper.destroy(); }); - const findUserStatus = () => wrapper.find('.js-user-status'); + const findUserStatus = () => wrapper.findByTestId('user-popover-status'); const findTarget = () => document.querySelector('.js-user-link'); const findUserName = () => wrapper.find(UserNameWithStatus); const findSecurityBotDocsLink = () => wrapper.findByTestId('user-popover-bot-docs-link'); + const findUserLocalTime = () => wrapper.findByTestId('user-popover-local-time'); const createWrapper = (props = {}, options = {}) => { wrapper = mountExtended(UserPopover, { @@ -71,7 +73,6 @@ describe('User Popover Component', () => { expect(wrapper.text()).toContain(DEFAULT_PROPS.user.name); expect(wrapper.text()).toContain(DEFAULT_PROPS.user.username); - expect(wrapper.text()).toContain(DEFAULT_PROPS.user.location); }); it('shows icon for location', () => { @@ -164,6 +165,25 @@ describe('User Popover Component', () => { }); }); + describe('local time', () => { + it('should show local time when it is available', () => { + createWrapper(); + + expect(findUserLocalTime().exists()).toBe(true); + }); + + it('should not show local time when it is not available', () => { + const user = { + ...DEFAULT_PROPS.user, + localTime: null, + }; + + createWrapper({ user }); + + expect(findUserLocalTime().exists()).toBe(false); + }); + }); + describe('status data', () => { it('should show only message', () => { const user = { ...DEFAULT_PROPS.user, status: { message_html: 'Hello World' } }; @@ -256,5 +276,11 @@ describe('User Popover Component', () => { const securityBotDocsLink = findSecurityBotDocsLink(); expect(securityBotDocsLink.text()).toBe('Learn more about %<>\';"'); }); + + it('does not display local time', () => { + createWrapper({ user: SECURITY_BOT_USER }); + + expect(findUserLocalTime().exists()).toBe(false); + }); }); }); diff --git a/spec/frontend/vue_shared/components/web_ide_link_spec.js b/spec/frontend/vue_shared/components/web_ide_link_spec.js index 5fe4eeb6061..92938b2717f 100644 --- a/spec/frontend/vue_shared/components/web_ide_link_spec.js +++ b/spec/frontend/vue_shared/components/web_ide_link_spec.js @@ -160,4 +160,26 @@ describe('Web IDE link component', () => { expect(findLocalStorageSync().props('value')).toBe(ACTION_GITPOD.key); }); }); + + describe('edit actions', () => { + it.each([ + { + props: { showWebIdeButton: true, showEditButton: false }, + expectedEventPayload: 'ide', + }, + { + props: { showWebIdeButton: false, showEditButton: true }, + expectedEventPayload: 'simple', + }, + ])( + 'emits the correct event when an action handler is called', + async ({ props, expectedEventPayload }) => { + createComponent({ ...props, needsToFork: true }); + + findActionsButton().props('actions')[0].handle(); + + expect(wrapper.emitted('edit')).toEqual([[expectedEventPayload]]); + }, + ); + }); }); |