diff options
Diffstat (limited to 'spec/frontend/vue_shared/components/color_select_dropdown')
7 files changed, 499 insertions, 0 deletions
diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/color_item_spec.js b/spec/frontend/vue_shared/components/color_select_dropdown/color_item_spec.js new file mode 100644 index 00000000000..fe614f03119 --- /dev/null +++ b/spec/frontend/vue_shared/components/color_select_dropdown/color_item_spec.js @@ -0,0 +1,35 @@ +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { hexToRgb } from '~/lib/utils/color_utils'; +import ColorItem from '~/vue_shared/components/color_select_dropdown/color_item.vue'; +import { color } from './mock_data'; + +describe('ColorItem', () => { + let wrapper; + + const propsData = color; + + const createComponent = () => { + wrapper = shallowMountExtended(ColorItem, { + propsData, + }); + }; + + const findColorItem = () => wrapper.findByTestId('color-item'); + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders the correct title', () => { + expect(wrapper.text()).toBe(propsData.title); + }); + + it('renders the correct background color for the color item', () => { + const convertedColor = hexToRgb(propsData.color).join(', '); + expect(findColorItem().attributes('style')).toBe(`background-color: rgb(${convertedColor});`); + }); +}); diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/color_select_root_spec.js b/spec/frontend/vue_shared/components/color_select_dropdown/color_select_root_spec.js new file mode 100644 index 00000000000..93b59800c27 --- /dev/null +++ b/spec/frontend/vue_shared/components/color_select_dropdown/color_select_root_spec.js @@ -0,0 +1,192 @@ +import { shallowMount } from '@vue/test-utils'; +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 SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue'; +import DropdownContents from '~/vue_shared/components/color_select_dropdown/dropdown_contents.vue'; +import DropdownValue from '~/vue_shared/components/color_select_dropdown/dropdown_value.vue'; +import epicColorQuery from '~/vue_shared/components/color_select_dropdown/graphql/epic_color.query.graphql'; +import updateEpicColorMutation from '~/vue_shared/components/color_select_dropdown/graphql/epic_update_color.mutation.graphql'; +import ColorSelectRoot from '~/vue_shared/components/color_select_dropdown/color_select_root.vue'; +import { DROPDOWN_VARIANT } from '~/vue_shared/components/color_select_dropdown/constants'; +import { colorQueryResponse, updateColorMutationResponse, color } from './mock_data'; + +jest.mock('~/flash'); + +Vue.use(VueApollo); + +const successfulQueryHandler = jest.fn().mockResolvedValue(colorQueryResponse); +const successfulMutationHandler = jest.fn().mockResolvedValue(updateColorMutationResponse); +const errorQueryHandler = jest.fn().mockRejectedValue('Error fetching epic color.'); +const errorMutationHandler = jest.fn().mockRejectedValue('An error occurred while updating color.'); + +const defaultProps = { + allowEdit: true, + iid: '1', + fullPath: 'workspace-1', +}; + +describe('LabelsSelectRoot', () => { + let wrapper; + + const findSidebarEditableItem = () => wrapper.findComponent(SidebarEditableItem); + const findDropdownValue = () => wrapper.findComponent(DropdownValue); + const findDropdownContents = () => wrapper.findComponent(DropdownContents); + + const createComponent = ({ + queryHandler = successfulQueryHandler, + mutationHandler = successfulMutationHandler, + propsData, + } = {}) => { + const mockApollo = createMockApollo([ + [epicColorQuery, queryHandler], + [updateEpicColorMutation, mutationHandler], + ]); + + wrapper = shallowMount(ColorSelectRoot, { + apolloProvider: mockApollo, + propsData: { + ...defaultProps, + ...propsData, + }, + provide: { + canUpdate: true, + }, + stubs: { + SidebarEditableItem, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('template', () => { + const defaultClasses = ['labels-select-wrapper', 'gl-relative']; + + it.each` + variant | cssClass + ${'sidebar'} | ${defaultClasses} + ${'embedded'} | ${[...defaultClasses, 'is-embedded']} + `( + 'renders component root element with CSS class `$cssClass` when variant is "$variant"', + async ({ variant, cssClass }) => { + createComponent({ + propsData: { variant }, + }); + + expect(wrapper.classes()).toEqual(cssClass); + }, + ); + }); + + describe('if the variant is `sidebar`', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders SidebarEditableItem component', () => { + expect(findSidebarEditableItem().exists()).toBe(true); + }); + + it('renders correct props for the SidebarEditableItem component', () => { + expect(findSidebarEditableItem().props()).toMatchObject({ + title: wrapper.vm.$options.i18n.widgetTitle, + canEdit: defaultProps.allowEdit, + loading: true, + }); + }); + + describe('when colors are loaded', () => { + beforeEach(async () => { + createComponent(); + await waitForPromises(); + }); + + it('passes false `loading` prop to sidebar editable item', () => { + expect(findSidebarEditableItem().props('loading')).toBe(false); + }); + + it('renders dropdown value component when query colors is resolved', () => { + expect(findDropdownValue().props('selectedColor')).toMatchObject(color); + }); + }); + }); + + describe('if the variant is `embedded`', () => { + beforeEach(() => { + createComponent({ propsData: { iid: undefined, variant: DROPDOWN_VARIANT.Embedded } }); + }); + + it('renders DropdownContents component', () => { + expect(findDropdownContents().exists()).toBe(true); + }); + + it('renders correct props for the DropdownContents component', () => { + expect(findDropdownContents().props()).toMatchObject({ + variant: DROPDOWN_VARIANT.Embedded, + dropdownTitle: wrapper.vm.$options.i18n.assignColor, + dropdownButtonText: wrapper.vm.$options.i18n.dropdownButtonText, + }); + }); + + it('handles DropdownContents setColor', () => { + findDropdownContents().vm.$emit('setColor', color); + expect(wrapper.emitted('updateSelectedColor')).toEqual([[color]]); + }); + }); + + describe('when epicColorQuery errored', () => { + beforeEach(async () => { + createComponent({ queryHandler: errorQueryHandler }); + await waitForPromises(); + }); + + it('creates flash with error message', () => { + expect(createFlash).toHaveBeenCalledWith({ + captureError: true, + message: 'Error fetching epic color.', + }); + }); + }); + + it('emits `updateSelectedColor` event on dropdown contents `setColor` event if iid is not set', () => { + createComponent({ propsData: { iid: undefined } }); + + findDropdownContents().vm.$emit('setColor', color); + expect(wrapper.emitted('updateSelectedColor')).toEqual([[color]]); + }); + + describe('when updating color for epic', () => { + beforeEach(() => { + createComponent(); + findDropdownContents().vm.$emit('setColor', color); + }); + + it('sets the loading state', () => { + expect(findSidebarEditableItem().props('loading')).toBe(true); + }); + + it('updates color correctly after successful mutation', async () => { + await waitForPromises(); + expect(findDropdownValue().props('selectedColor').color).toEqual( + updateColorMutationResponse.data.updateIssuableColor.issuable.color, + ); + }); + + it('displays an error if mutation was rejected', async () => { + createComponent({ mutationHandler: errorMutationHandler }); + findDropdownContents().vm.$emit('setColor', color); + await waitForPromises(); + + expect(createFlash).toHaveBeenCalledWith({ + captureError: true, + error: expect.anything(), + message: 'An error occurred while updating color.', + }); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_color_view_spec.js b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_color_view_spec.js new file mode 100644 index 00000000000..303824c77b3 --- /dev/null +++ b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_color_view_spec.js @@ -0,0 +1,43 @@ +import { GlDropdownForm } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import DropdownContentsColorView from '~/vue_shared/components/color_select_dropdown/dropdown_contents_color_view.vue'; +import ColorItem from '~/vue_shared/components/color_select_dropdown/color_item.vue'; +import { ISSUABLE_COLORS } from '~/vue_shared/components/color_select_dropdown/constants'; +import { color as defaultColor } from './mock_data'; + +const propsData = { + selectedColor: defaultColor, +}; + +describe('DropdownContentsColorView', () => { + let wrapper; + + const createComponent = () => { + wrapper = shallowMount(DropdownContentsColorView, { + propsData, + }); + }; + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + const findColors = () => wrapper.findAllComponents(ColorItem); + const findColorList = () => wrapper.findComponent(GlDropdownForm); + + it('renders color list', async () => { + expect(findColorList().exists()).toBe(true); + expect(findColors()).toHaveLength(ISSUABLE_COLORS.length); + }); + + it.each(ISSUABLE_COLORS)('emits an `input` event with %o on click on the option %#', (color) => { + const colorIndex = ISSUABLE_COLORS.indexOf(color); + findColors().at(colorIndex).trigger('click'); + + expect(wrapper.emitted('input')[0][0]).toMatchObject(color); + }); +}); diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_spec.js b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_spec.js new file mode 100644 index 00000000000..74f50b878e2 --- /dev/null +++ b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_spec.js @@ -0,0 +1,113 @@ +import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; +import { DROPDOWN_VARIANT } from '~/vue_shared/components/color_select_dropdown/constants'; +import DropdownContents from '~/vue_shared/components/color_select_dropdown/dropdown_contents.vue'; +import DropdownContentsColorView from '~/vue_shared/components/color_select_dropdown/dropdown_contents_color_view.vue'; + +import { color } from './mock_data'; + +const showDropdown = jest.fn(); +const focusInput = jest.fn(); + +const defaultProps = { + dropdownTitle: '', + selectedColor: color, + dropdownButtonText: '', + variant: '', + isVisible: false, +}; + +const GlDropdownStub = { + template: ` + <div> + <slot name="header"></slot> + <slot></slot> + </div> + `, + methods: { + show: showDropdown, + hide: jest.fn(), + }, +}; + +const DropdownHeaderStub = { + template: ` + <div>Hello, I am a header</div> + `, + methods: { + focusInput, + }, +}; + +describe('DropdownContent', () => { + let wrapper; + + const createComponent = ({ propsData = {} } = {}) => { + wrapper = shallowMount(DropdownContents, { + propsData: { + ...defaultProps, + ...propsData, + }, + stubs: { + GlDropdown: GlDropdownStub, + DropdownHeader: DropdownHeaderStub, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + const findColorView = () => wrapper.findComponent(DropdownContentsColorView); + const findDropdownHeader = () => wrapper.findComponent(DropdownHeaderStub); + const findDropdown = () => wrapper.findComponent(GlDropdownStub); + + it('calls dropdown `show` method on `isVisible` prop change', async () => { + createComponent(); + await wrapper.setProps({ + isVisible: true, + }); + + expect(showDropdown).toHaveBeenCalledTimes(1); + }); + + it('does not emit `setColor` event on dropdown hide if color did not change', () => { + createComponent(); + findDropdown().vm.$emit('hide'); + + expect(wrapper.emitted('setColor')).toBeUndefined(); + }); + + it('emits `setColor` event on dropdown hide if color changed on non-sidebar widget', async () => { + createComponent({ propsData: { variant: DROPDOWN_VARIANT.Embedded } }); + const updatedColor = { + title: 'Blue-gray', + color: '#6699cc', + }; + findColorView().vm.$emit('input', updatedColor); + await nextTick(); + findDropdown().vm.$emit('hide'); + + expect(wrapper.emitted('setColor')).toEqual([[updatedColor]]); + }); + + it('emits `setColor` event on visibility change if color changed on sidebar widget', async () => { + createComponent({ propsData: { variant: DROPDOWN_VARIANT.Sidebar, isVisible: true } }); + const updatedColor = { + title: 'Blue-gray', + color: '#6699cc', + }; + findColorView().vm.$emit('input', updatedColor); + wrapper.setProps({ isVisible: false }); + await nextTick(); + + expect(wrapper.emitted('setColor')).toEqual([[updatedColor]]); + }); + + it('renders header', () => { + createComponent(); + + expect(findDropdownHeader().exists()).toBe(true); + }); +}); diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_header_spec.js b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_header_spec.js new file mode 100644 index 00000000000..d203d78477f --- /dev/null +++ b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_header_spec.js @@ -0,0 +1,40 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlButton } from '@gitlab/ui'; +import DropdownHeader from '~/vue_shared/components/color_select_dropdown/dropdown_header.vue'; + +const propsData = { + dropdownTitle: 'Epic color', +}; + +describe('DropdownHeader', () => { + let wrapper; + + const createComponent = () => { + wrapper = shallowMount(DropdownHeader, { propsData }); + }; + + const findButton = () => wrapper.findComponent(GlButton); + + afterEach(() => { + wrapper.destroy(); + }); + + beforeEach(() => { + createComponent(); + }); + + it('renders the correct title', () => { + expect(wrapper.text()).toBe(propsData.dropdownTitle); + }); + + it('renders a close button', () => { + expect(findButton().attributes('aria-label')).toBe('Close'); + }); + + it('emits `closeDropdown` event on button click', () => { + expect(wrapper.emitted('closeDropdown')).toBeUndefined(); + findButton().vm.$emit('click'); + + expect(wrapper.emitted('closeDropdown')).toEqual([[]]); + }); +}); diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_value_spec.js b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_value_spec.js new file mode 100644 index 00000000000..f22592dd604 --- /dev/null +++ b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_value_spec.js @@ -0,0 +1,46 @@ +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; + +import ColorItem from '~/vue_shared/components/color_select_dropdown/color_item.vue'; +import DropdownValue from '~/vue_shared/components/color_select_dropdown/dropdown_value.vue'; + +import { color } from './mock_data'; + +const propsData = { + selectedColor: color, +}; + +describe('DropdownValue', () => { + let wrapper; + + const findColorItems = () => wrapper.findAllComponents(ColorItem); + + const createComponent = () => { + wrapper = shallowMountExtended(DropdownValue, { propsData }); + }; + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('when there is a color set', () => { + it('renders the color', () => { + expect(findColorItems()).toHaveLength(2); + }); + + it.each` + index | cssClass + ${0} | ${['gl-font-base', 'gl-line-height-24']} + ${1} | ${['hide-collapsed']} + `( + 'passes correct props to the ColorItem with CSS class `$cssClass`', + async ({ index, cssClass }) => { + expect(findColorItems().at(index).props()).toMatchObject(propsData.selectedColor); + expect(findColorItems().at(index).classes()).toEqual(cssClass); + }, + ); + }); +}); diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/mock_data.js b/spec/frontend/vue_shared/components/color_select_dropdown/mock_data.js new file mode 100644 index 00000000000..097f47cc731 --- /dev/null +++ b/spec/frontend/vue_shared/components/color_select_dropdown/mock_data.js @@ -0,0 +1,30 @@ +export const color = { + color: '#217645', + title: 'Green', +}; + +export const colorQueryResponse = { + data: { + workspace: { + id: 'gid://gitlab/Workspace/1', + issuable: { + __typename: 'Epic', + id: 'gid://gitlab/Epic/1', + color: '#217645', + }, + }, + }, +}; + +export const updateColorMutationResponse = { + data: { + updateIssuableColor: { + issuable: { + __typename: 'Epic', + id: 'gid://gitlab/Epic/1', + color: '#217645', + }, + errors: [], + }, + }, +}; |