diff options
Diffstat (limited to 'spec/frontend/vue_shared/components')
28 files changed, 983 insertions, 302 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 44b4c0398cd..30e15595193 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 @@ -12,7 +12,7 @@ exports[`Clone Dropdown Button rendering matches the snapshot 1`] = ` right="true" size="medium" text="Clone" - variant="info" + variant="confirm" > <div class="pb-2 mx-1" @@ -24,41 +24,38 @@ exports[`Clone Dropdown Button rendering matches the snapshot 1`] = ` <div class="mx-3" > - <div - readonly="readonly" + <b-input-group-stub + readonly="" + tag="div" > - <b-input-group-stub + <!----> + + <b-form-input-stub + class="gl-form-input" + debounce="0" + formatter="[Function]" + readonly="true" + type="text" + value="ssh://foo.bar" + /> + + <b-input-group-append-stub tag="div" > - <!----> - - <b-form-input-stub - class="gl-form-input" - debounce="0" - formatter="[Function]" - readonly="true" - type="text" - value="ssh://foo.bar" + <gl-button-stub + aria-label="Copy URL" + buttontextclasses="" + category="primary" + class="d-inline-flex" + data-clipboard-text="ssh://foo.bar" + data-qa-selector="copy_ssh_url_button" + icon="copy-to-clipboard" + size="medium" + title="Copy URL" + variant="default" /> - - <b-input-group-append-stub - tag="div" - > - <gl-button-stub - aria-label="Copy URL" - buttontextclasses="" - category="primary" - class="d-inline-flex" - data-clipboard-text="ssh://foo.bar" - data-qa-selector="copy_ssh_url_button" - icon="copy-to-clipboard" - size="medium" - title="Copy URL" - variant="default" - /> - </b-input-group-append-stub> - </b-input-group-stub> - </div> + </b-input-group-append-stub> + </b-input-group-stub> </div> <gl-dropdown-section-header-stub> @@ -68,41 +65,38 @@ exports[`Clone Dropdown Button rendering matches the snapshot 1`] = ` <div class="mx-3" > - <div - readonly="readonly" + <b-input-group-stub + readonly="" + tag="div" > - <b-input-group-stub + <!----> + + <b-form-input-stub + class="gl-form-input" + debounce="0" + formatter="[Function]" + readonly="true" + type="text" + value="http://foo.bar" + /> + + <b-input-group-append-stub tag="div" > - <!----> - - <b-form-input-stub - class="gl-form-input" - debounce="0" - formatter="[Function]" - readonly="true" - type="text" - value="http://foo.bar" + <gl-button-stub + aria-label="Copy URL" + buttontextclasses="" + category="primary" + class="d-inline-flex" + data-clipboard-text="http://foo.bar" + data-qa-selector="copy_http_url_button" + icon="copy-to-clipboard" + size="medium" + title="Copy URL" + variant="default" /> - - <b-input-group-append-stub - tag="div" - > - <gl-button-stub - aria-label="Copy URL" - buttontextclasses="" - category="primary" - class="d-inline-flex" - data-clipboard-text="http://foo.bar" - data-qa-selector="copy_http_url_button" - icon="copy-to-clipboard" - size="medium" - title="Copy URL" - variant="default" - /> - </b-input-group-append-stub> - </b-input-group-stub> - </div> + </b-input-group-append-stub> + </b-input-group-stub> </div> </div> </gl-dropdown-stub> diff --git a/spec/frontend/vue_shared/components/ci_icon_spec.js b/spec/frontend/vue_shared/components/ci_icon_spec.js index 6d52db7ae65..1b502f9587c 100644 --- a/spec/frontend/vue_shared/components/ci_icon_spec.js +++ b/spec/frontend/vue_shared/components/ci_icon_spec.js @@ -5,6 +5,8 @@ import ciIcon from '~/vue_shared/components/ci_icon.vue'; describe('CI Icon component', () => { let wrapper; + const findIconWrapper = () => wrapper.find('[data-testid="ci-icon-wrapper"]'); + afterEach(() => { wrapper.destroy(); wrapper = null; @@ -23,6 +25,52 @@ describe('CI Icon component', () => { expect(wrapper.find(GlIcon).exists()).toBe(true); }); + describe('active icons', () => { + it.each` + isActive | cssClass + ${true} | ${'active'} + ${false} | ${'active'} + `('active should be $isActive', ({ isActive, cssClass }) => { + wrapper = shallowMount(ciIcon, { + propsData: { + status: { + icon: 'status_success', + }, + isActive, + }, + }); + + if (isActive) { + expect(findIconWrapper().classes()).toContain(cssClass); + } else { + expect(findIconWrapper().classes()).not.toContain(cssClass); + } + }); + }); + + describe('interactive icons', () => { + it.each` + isInteractive | cssClass + ${true} | ${'interactive'} + ${false} | ${'interactive'} + `('interactive should be $isInteractive', ({ isInteractive, cssClass }) => { + wrapper = shallowMount(ciIcon, { + propsData: { + status: { + icon: 'status_success', + }, + isInteractive, + }, + }); + + if (isInteractive) { + expect(findIconWrapper().classes()).toContain(cssClass); + } else { + expect(findIconWrapper().classes()).not.toContain(cssClass); + } + }); + }); + describe('rendering a status', () => { it.each` icon | group | cssClass 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: [], + }, + }, +}; diff --git a/spec/frontend/vue_shared/components/confidentiality_badge_spec.js b/spec/frontend/vue_shared/components/confidentiality_badge_spec.js index 9d11fbbaf55..e1860d3399b 100644 --- a/spec/frontend/vue_shared/components/confidentiality_badge_spec.js +++ b/spec/frontend/vue_shared/components/confidentiality_badge_spec.js @@ -29,8 +29,8 @@ describe('ConfidentialityBadge', () => { it.each` workspaceType | issuableType | expectedTooltip - ${WorkspaceType.project} | ${IssuableType.Issue} | ${'Only project members with at least Reporter role can view or be notified about this issue.'} - ${WorkspaceType.group} | ${IssuableType.Epic} | ${'Only group members with at least Reporter role can view or be notified about this epic.'} + ${WorkspaceType.project} | ${IssuableType.Issue} | ${'Only project members with at least the Reporter role, the author, and assignees can view or be notified about this issue.'} + ${WorkspaceType.group} | ${IssuableType.Epic} | ${'Only group members with at least the Reporter role can view or be notified about this epic.'} `( 'should render gl-badge with correct tooltip when workspaceType is $workspaceType and issuableType is $issuableType', ({ workspaceType, issuableType, expectedTooltip }) => { diff --git a/spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js b/spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js index 1397fb0405e..01ef52c6af9 100644 --- a/spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js +++ b/spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js @@ -1,3 +1,4 @@ +import { GlSkeletonLoader } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; import $ from 'jquery'; @@ -39,10 +40,10 @@ describe('MarkdownViewer', () => { }); }); - it('renders an animation container while the markdown is loading', () => { + it('renders a skeleton loader while the markdown is loading', () => { createComponent(); - expect(wrapper.find('.animation-container').exists()).toBe(true); + expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(true); }); it('renders markdown preview preview renders and loads rendered markdown from server', () => { 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 f03a2e7934f..51161a1a0ef 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 @@ -77,7 +77,7 @@ describe('LabelToken', () => { describe('getActiveLabel', () => { it('returns label object from labels array based on provided `currentValue` param', () => { - expect(wrapper.vm.getActiveLabel(mockLabels, 'foo label')).toEqual(mockRegularLabel); + expect(wrapper.vm.getActiveLabel(mockLabels, 'Foo Label')).toEqual(mockRegularLabel); }); }); diff --git a/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js b/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js index e636f58d868..e1da8b690af 100644 --- a/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js +++ b/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js @@ -66,7 +66,7 @@ describe('InputCopyToggleVisibility', () => { }); it('displays value as hidden', () => { - expect(findFormInputGroup().props('value')).toBe('********************'); + expect(findFormInput().element.value).toBe('********************'); }); it('saves actual value to clipboard when manually copied', () => { @@ -77,6 +77,16 @@ describe('InputCopyToggleVisibility', () => { expect(event.preventDefault).toHaveBeenCalled(); }); + it('emits `copy` event when manually copied the token', () => { + expect(wrapper.emitted('copy')).toBeUndefined(); + + findFormInput().element.dispatchEvent(createCopyEvent()); + + expect(wrapper.emitted()).toHaveProperty('copy'); + expect(wrapper.emitted('copy')).toHaveLength(1); + expect(wrapper.emitted('copy')[0]).toEqual([]); + }); + describe('visibility toggle button', () => { it('renders a reveal button', () => { const revealButton = findRevealButton(); @@ -97,7 +107,7 @@ describe('InputCopyToggleVisibility', () => { }); it('displays value', () => { - expect(findFormInputGroup().props('value')).toBe(valueProp); + expect(findFormInput().element.value).toBe(valueProp); }); it('renders a hide button', () => { @@ -135,6 +145,8 @@ describe('InputCopyToggleVisibility', () => { }); it('emits `copy` event', () => { + expect(wrapper.emitted()).toHaveProperty('copy'); + expect(wrapper.emitted('copy')).toHaveLength(1); expect(wrapper.emitted('copy')[0]).toEqual([]); }); }); @@ -147,25 +159,52 @@ describe('InputCopyToggleVisibility', () => { }); it('displays value as hidden with 20 asterisks', () => { - expect(findFormInputGroup().props('value')).toBe('********************'); + expect(findFormInput().element.value).toBe('********************'); }); }); describe('when `initialVisibility` prop is `true`', () => { + const label = 'My label'; + beforeEach(() => { createComponent({ propsData: { value: valueProp, initialVisibility: true, + label, + 'label-for': 'my-input', + formInputGroupProps: { + id: 'my-input', + }, }, }); }); it('displays value', () => { - expect(findFormInputGroup().props('value')).toBe(valueProp); + expect(findFormInput().element.value).toBe(valueProp); }); itDoesNotModifyCopyEvent(); + + describe('when input is clicked', () => { + it('selects input value', async () => { + const mockSelect = jest.fn(); + wrapper.vm.$refs.input.$el.select = mockSelect; + await wrapper.findByLabelText(label).trigger('click'); + + expect(mockSelect).toHaveBeenCalled(); + }); + }); + + describe('when label is clicked', () => { + it('selects input value', async () => { + const mockSelect = jest.fn(); + wrapper.vm.$refs.input.$el.select = mockSelect; + await wrapper.find('label').trigger('click'); + + expect(mockSelect).toHaveBeenCalled(); + }); + }); }); describe('when `showToggleVisibilityButton` is `false`', () => { @@ -184,7 +223,7 @@ describe('InputCopyToggleVisibility', () => { }); it('displays value', () => { - expect(findFormInputGroup().props('value')).toBe(valueProp); + expect(findFormInput().element.value).toBe(valueProp); }); itDoesNotModifyCopyEvent(); @@ -204,16 +243,30 @@ describe('InputCopyToggleVisibility', () => { }); }); - it('passes `formInputGroupProps` prop to `GlFormInputGroup`', () => { + it('passes `formInputGroupProps` prop only to the input', () => { createComponent({ propsData: { formInputGroupProps: { - label: 'Foo bar', + name: 'Foo bar', + 'data-qa-selector': 'Foo bar', + class: 'Foo bar', + id: 'Foo bar', }, }, }); - expect(findFormInputGroup().props('label')).toBe('Foo bar'); + expect(findFormInput().attributes()).toMatchObject({ + name: 'Foo bar', + 'data-qa-selector': 'Foo bar', + class: expect.stringContaining('Foo bar'), + id: 'Foo bar', + }); + + const attributesInputGroup = findFormInputGroup().attributes(); + expect(attributesInputGroup.name).toBeUndefined(); + expect(attributesInputGroup['data-qa-selector']).toBeUndefined(); + expect(attributesInputGroup.class).not.toContain('Foo bar'); + expect(attributesInputGroup.id).toBeUndefined(); }); it('passes `copyButtonTitle` prop to `ClipboardButton`', () => { diff --git a/spec/frontend/vue_shared/components/notes/__snapshots__/noteable_warning_spec.js.snap b/spec/frontend/vue_shared/components/notes/__snapshots__/noteable_warning_spec.js.snap index f878d685b6d..8a187f3cb1f 100644 --- a/spec/frontend/vue_shared/components/notes/__snapshots__/noteable_warning_spec.js.snap +++ b/spec/frontend/vue_shared/components/notes/__snapshots__/noteable_warning_spec.js.snap @@ -10,7 +10,7 @@ exports[`Issue Warning Component when issue is locked but not confidential rende href="locked-path" target="_blank" > - Learn more + Learn more. </gl-link-stub> </span> `; @@ -25,7 +25,7 @@ exports[`Issue Warning Component when noteable is confidential but not locked re href="confidential-path" target="_blank" > - Learn more + Learn more. </gl-link-stub> </span> `; diff --git a/spec/frontend/vue_shared/components/notes/system_note_spec.js b/spec/frontend/vue_shared/components/notes/system_note_spec.js index 65f79bab005..98b04ede943 100644 --- a/spec/frontend/vue_shared/components/notes/system_note_spec.js +++ b/spec/frontend/vue_shared/components/notes/system_note_spec.js @@ -1,13 +1,11 @@ import MockAdapter from 'axios-mock-adapter'; import { mount } from '@vue/test-utils'; +import $ from 'jquery'; import waitForPromises from 'helpers/wait_for_promises'; -import initMRPopovers from '~/mr_popover/index'; import createStore from '~/notes/stores'; import IssueSystemNote from '~/vue_shared/components/notes/system_note.vue'; import axios from '~/lib/utils/axios_utils'; -jest.mock('~/mr_popover/index', () => jest.fn()); - describe('system note component', () => { let vm; let props; @@ -76,10 +74,12 @@ describe('system note component', () => { expect(vm.find('.system-note-message').html()).toContain('<span>closed</span>'); }); - it('should initMRPopovers onMount', () => { + it('should renderGFM onMount', () => { + const renderGFMSpy = jest.spyOn($.fn, 'renderGFM'); + createComponent(props); - expect(initMRPopovers).toHaveBeenCalled(); + expect(renderGFMSpy).toHaveBeenCalled(); }); it('renders outdated code lines', async () => { diff --git a/spec/frontend/vue_shared/components/papa_parse_alert_spec.js b/spec/frontend/vue_shared/components/papa_parse_alert_spec.js index 9be2de17d01..ff4febd647e 100644 --- a/spec/frontend/vue_shared/components/papa_parse_alert_spec.js +++ b/spec/frontend/vue_shared/components/papa_parse_alert_spec.js @@ -22,7 +22,7 @@ describe('app/assets/javascripts/vue_shared/components/papa_parse_alert.vue', () it('should render alert with correct props', async () => { createComponent({ errorMessages: [{ code: 'MissingQuotes' }] }); - await nextTick; + await nextTick(); expect(findAlert().props()).toMatchObject({ variant: 'danger', @@ -37,7 +37,7 @@ describe('app/assets/javascripts/vue_shared/components/papa_parse_alert.vue', () createComponent({ errorMessages: [{ code: 'NotDefined', message: 'Error code is undefined' }], }); - await nextTick; + await nextTick(); expect(findAlert().text()).toContain('Error code is undefined'); }); diff --git a/spec/frontend/vue_shared/components/registry/registry_search_spec.js b/spec/frontend/vue_shared/components/registry/registry_search_spec.js index f5ef5b3d443..20716e79a04 100644 --- a/spec/frontend/vue_shared/components/registry/registry_search_spec.js +++ b/spec/frontend/vue_shared/components/registry/registry_search_spec.js @@ -11,7 +11,7 @@ describe('Registry Search', () => { const findFilteredSearch = () => wrapper.find(GlFilteredSearch); const defaultProps = { - filter: [], + filters: [], sorting: { sort: 'asc', orderBy: 'name' }, tokens: [{ type: 'foo' }], sortableFields: [ @@ -123,7 +123,7 @@ describe('Registry Search', () => { }); describe('query string calculation', () => { - const filter = [ + const filters = [ { type: FILTERED_SEARCH_TERM, value: { data: 'one' } }, { type: FILTERED_SEARCH_TERM, value: { data: 'two' } }, { type: 'typeOne', value: { data: 'value_one' } }, @@ -131,7 +131,7 @@ describe('Registry Search', () => { ]; it('aggregates the filter in the correct object', () => { - mountComponent({ ...defaultProps, filter }); + mountComponent({ ...defaultProps, filters }); findFilteredSearch().vm.$emit('submit'); diff --git a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js index 001b6ee4a6f..7173abe1316 100644 --- a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js +++ b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js @@ -48,12 +48,12 @@ describe('RunnerInstructionsModal component', () => { const findModal = () => wrapper.findComponent(GlModal); const findPlatformButtonGroup = () => wrapper.findByTestId('platform-buttons'); const findPlatformButtons = () => findPlatformButtonGroup().findAllComponents(GlButton); - const findOsxPlatformButton = () => wrapper.find({ ref: 'osx' }); const findArchitectureDropdownItems = () => wrapper.findAllByTestId('architecture-dropdown-item'); + const findBinaryDownloadButton = () => wrapper.findByTestId('binary-download-button'); const findBinaryInstructions = () => wrapper.findByTestId('binary-instructions'); const findRegisterCommand = () => wrapper.findByTestId('register-command'); - const createComponent = ({ props, ...options } = {}) => { + const createComponent = ({ props, shown = true, ...options } = {}) => { const requestHandlers = [ [getRunnerPlatformsQuery, runnerPlatformsHandler], [getRunnerSetupInstructionsQuery, runnerSetupInstructionsHandler], @@ -72,169 +72,202 @@ describe('RunnerInstructionsModal component', () => { ...options, }), ); + + // trigger open modal + if (shown) { + findModal().vm.$emit('shown'); + } }; beforeEach(async () => { runnerPlatformsHandler = jest.fn().mockResolvedValue(mockGraphqlRunnerPlatforms); runnerSetupInstructionsHandler = jest.fn().mockResolvedValue(mockGraphqlInstructions); - - createComponent(); - await waitForPromises(); }); afterEach(() => { wrapper.destroy(); }); - it('should not show alert', () => { - expect(findAlert().exists()).toBe(false); - }); - - it('should contain a number of platforms buttons', () => { - expect(runnerPlatformsHandler).toHaveBeenCalledWith({}); + describe('when the modal is shown', () => { + beforeEach(async () => { + createComponent(); + await waitForPromises(); + }); - const buttons = findPlatformButtons(); + it('should not show alert', async () => { + expect(findAlert().exists()).toBe(false); + }); - expect(buttons).toHaveLength(mockGraphqlRunnerPlatforms.data.runnerPlatforms.nodes.length); - }); + it('should contain a number of platforms buttons', () => { + expect(runnerPlatformsHandler).toHaveBeenCalledWith({}); - it('should contain a number of dropdown items for the architecture options', () => { - expect(findArchitectureDropdownItems()).toHaveLength( - mockGraphqlRunnerPlatforms.data.runnerPlatforms.nodes[0].architectures.nodes.length, - ); - }); + const buttons = findPlatformButtons(); - describe('should display default instructions', () => { - const { installInstructions, registerInstructions } = mockGraphqlInstructions.data.runnerSetup; + expect(buttons).toHaveLength(mockGraphqlRunnerPlatforms.data.runnerPlatforms.nodes.length); + }); - it('runner instructions are requested', () => { - expect(runnerSetupInstructionsHandler).toHaveBeenCalledWith({ - platform: 'linux', - architecture: 'amd64', - }); + it('should contain a number of dropdown items for the architecture options', () => { + expect(findArchitectureDropdownItems()).toHaveLength( + mockGraphqlRunnerPlatforms.data.runnerPlatforms.nodes[0].architectures.nodes.length, + ); }); - it('binary instructions are shown', async () => { - await waitForPromises(); - const instructions = findBinaryInstructions().text(); + describe('should display default instructions', () => { + const { + installInstructions, + registerInstructions, + } = mockGraphqlInstructions.data.runnerSetup; - expect(instructions).toBe(installInstructions); - }); + it('runner instructions are requested', () => { + expect(runnerSetupInstructionsHandler).toHaveBeenCalledWith({ + platform: 'linux', + architecture: 'amd64', + }); + }); - it('register command is shown with a replaced token', async () => { - await waitForPromises(); - const instructions = findRegisterCommand().text(); + it('binary instructions are shown', async () => { + const instructions = findBinaryInstructions().text(); - expect(instructions).toBe( - 'sudo gitlab-runner register --url http://gdk.test:3000/ --registration-token MY_TOKEN', - ); - }); + expect(instructions).toBe(installInstructions); + }); - describe('when a register token is not shown', () => { - beforeEach(async () => { - createComponent({ props: { registrationToken: undefined } }); - await waitForPromises(); + it('register command is shown with a replaced token', async () => { + const command = findRegisterCommand().text(); + + expect(command).toBe( + 'sudo gitlab-runner register --url http://gdk.test:3000/ --registration-token MY_TOKEN', + ); }); - it('register command is shown without a defined registration token', () => { - const instructions = findRegisterCommand().text(); + describe('when a register token is not shown', () => { + beforeEach(async () => { + createComponent({ props: { registrationToken: undefined } }); + await waitForPromises(); + }); + + it('register command is shown without a defined registration token', () => { + const instructions = findRegisterCommand().text(); - expect(instructions).toBe(registerInstructions); + expect(instructions).toBe(registerInstructions); + }); }); - }); - describe('when the modal is shown', () => { - it('sets the focus on the selected platform', () => { - findPlatformButtons().at(0).element.focus = jest.fn(); + describe('when providing a defaultPlatformName', () => { + beforeEach(async () => { + createComponent({ props: { defaultPlatformName: 'osx' } }); + await waitForPromises(); + }); + + it('runner instructions for the default selected platform are requested', () => { + expect(runnerSetupInstructionsHandler).toHaveBeenCalledWith({ + platform: 'osx', + architecture: 'amd64', + }); + }); + + it('sets the focus on the default selected platform', () => { + const findOsxPlatformButton = () => wrapper.find({ ref: 'osx' }); + + findOsxPlatformButton().element.focus = jest.fn(); - findModal().vm.$emit('shown'); + findModal().vm.$emit('shown'); - expect(findPlatformButtons().at(0).element.focus).toHaveBeenCalled(); + expect(findOsxPlatformButton().element.focus).toHaveBeenCalled(); + }); }); }); - describe('when providing a defaultPlatformName', () => { + describe('after a platform and architecture are selected', () => { + const windowsIndex = 2; + const { installInstructions } = mockGraphqlInstructionsWindows.data.runnerSetup; + beforeEach(async () => { - createComponent({ props: { defaultPlatformName: 'osx' } }); + runnerSetupInstructionsHandler.mockResolvedValue(mockGraphqlInstructionsWindows); + + findPlatformButtons().at(windowsIndex).vm.$emit('click'); await waitForPromises(); }); - it('runner instructions for the default selected platform are requested', () => { - expect(runnerSetupInstructionsHandler).toHaveBeenCalledWith({ - platform: 'osx', + it('runner instructions are requested', () => { + expect(runnerSetupInstructionsHandler).toHaveBeenLastCalledWith({ + platform: 'windows', architecture: 'amd64', }); }); - it('sets the focus on the default selected platform', () => { - findOsxPlatformButton().element.focus = jest.fn(); - - findModal().vm.$emit('shown'); + it('architecture download link is updated', () => { + const architectures = + mockGraphqlRunnerPlatforms.data.runnerPlatforms.nodes[windowsIndex].architectures.nodes; - expect(findOsxPlatformButton().element.focus).toHaveBeenCalled(); + expect(findBinaryDownloadButton().attributes('href')).toBe( + architectures[0].downloadLocation, + ); }); - }); - }); - describe('after a platform and architecture are selected', () => { - const { installInstructions } = mockGraphqlInstructionsWindows.data.runnerSetup; + it('other binary instructions are shown', () => { + const instructions = findBinaryInstructions().text(); - beforeEach(async () => { - runnerSetupInstructionsHandler.mockResolvedValue(mockGraphqlInstructionsWindows); + expect(instructions).toBe(installInstructions); + }); - findPlatformButtons().at(2).vm.$emit('click'); // another option, happens to be windows - await nextTick(); + it('register command is shown', () => { + const command = findRegisterCommand().text(); - findArchitectureDropdownItems().at(1).vm.$emit('click'); // another option - await nextTick(); - }); + expect(command).toBe( + './gitlab-runner.exe register --url http://gdk.test:3000/ --registration-token MY_TOKEN', + ); + }); + + it('runner instructions are requested with another architecture', async () => { + findArchitectureDropdownItems().at(1).vm.$emit('click'); + await waitForPromises(); - it('runner instructions are requested', () => { - expect(runnerSetupInstructionsHandler).toHaveBeenCalledWith({ - platform: 'windows', - architecture: '386', + expect(runnerSetupInstructionsHandler).toHaveBeenLastCalledWith({ + platform: 'windows', + architecture: '386', + }); }); }); - it('other binary instructions are shown', () => { - const instructions = findBinaryInstructions().text(); + describe('when the modal resizes', () => { + it('to an xs viewport', async () => { + MockResizeObserver.mockResize('xs'); + await nextTick(); - expect(instructions).toBe(installInstructions); - }); + expect(findPlatformButtonGroup().attributes('vertical')).toBeTruthy(); + }); - it('register command is shown', () => { - const command = findRegisterCommand().text(); + it('to a non-xs viewport', async () => { + MockResizeObserver.mockResize('sm'); + await nextTick(); - expect(command).toBe( - './gitlab-runner.exe register --url http://gdk.test:3000/ --registration-token MY_TOKEN', - ); + expect(findPlatformButtonGroup().props('vertical')).toBeFalsy(); + }); }); }); - describe('when the modal resizes', () => { - it('to an xs viewport', async () => { - MockResizeObserver.mockResize('xs'); - await nextTick(); - - expect(findPlatformButtonGroup().attributes('vertical')).toBeTruthy(); + describe('when the modal is not shown', () => { + beforeEach(async () => { + createComponent({ shown: false }); + await waitForPromises(); }); - it('to a non-xs viewport', async () => { - MockResizeObserver.mockResize('sm'); - await nextTick(); - - expect(findPlatformButtonGroup().props('vertical')).toBeFalsy(); + it('does not fetch instructions', () => { + expect(runnerPlatformsHandler).not.toHaveBeenCalled(); + expect(runnerSetupInstructionsHandler).not.toHaveBeenCalled(); }); }); describe('when apollo is loading', () => { - it('should show a skeleton loader', async () => { + beforeEach(() => { createComponent(); + }); + + it('should show a skeleton loader', async () => { expect(findSkeletonLoader().exists()).toBe(true); expect(findGlLoadingIcon().exists()).toBe(false); - await nextTick(); - jest.runOnlyPendingTimers(); + // wait on fetch of both `platforms` and `instructions` await nextTick(); await nextTick(); @@ -242,7 +275,6 @@ describe('RunnerInstructionsModal component', () => { }); it('once loaded, should not show a loading state', async () => { - createComponent(); await waitForPromises(); expect(findSkeletonLoader().exists()).toBe(false); @@ -255,7 +287,6 @@ describe('RunnerInstructionsModal component', () => { runnerSetupInstructionsHandler.mockRejectedValue(); createComponent(); - await waitForPromises(); }); @@ -287,6 +318,7 @@ describe('RunnerInstructionsModal component', () => { mockShow = jest.fn(); createComponent({ + shown: false, stubs: { GlModal: getGlModalStub({ show: mockShow }), }, diff --git a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js index 9a95a838291..986d76d2b95 100644 --- a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js +++ b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js @@ -1,6 +1,5 @@ -import { shallowMount } from '@vue/test-utils'; -import { nextTick } from 'vue'; -import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import RunnerInstructions from '~/vue_shared/components/runner_instructions/runner_instructions.vue'; import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue'; @@ -11,7 +10,11 @@ describe('RunnerInstructions component', () => { const findModal = () => wrapper.findComponent(RunnerInstructionsModal); const createComponent = () => { - wrapper = extendedWrapper(shallowMount(RunnerInstructions)); + wrapper = shallowMountExtended(RunnerInstructions, { + directives: { + GlModal: createMockDirective(), + }, + }); }; beforeEach(() => { @@ -23,19 +26,12 @@ describe('RunnerInstructions component', () => { }); it('should show the "Show runner installation instructions" button', () => { - expect(findModalButton().exists()).toBe(true); expect(findModalButton().text()).toBe('Show runner installation instructions'); }); - it('should not render the modal once mounted', () => { - expect(findModal().exists()).toBe(false); - }); - - it('should render the modal once clicked', async () => { - findModalButton().vm.$emit('click'); - - await nextTick(); + it('should render the modal', () => { + const modalId = getBinding(findModal().element, 'gl-modal'); - expect(findModal().exists()).toBe(true); + expect(findModalButton().attributes('modal-id')).toBe(modalId); }); }); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js index 42202db4935..00c8e3a814a 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js @@ -226,12 +226,7 @@ describe('DropdownContentsLabelsView', () => { preventDefault: fakePreventDefault, }); - expect(wrapper.vm.updateSelectedLabels).toHaveBeenCalledWith([ - { - ...mockLabels[2], - set: true, - }, - ]); + expect(wrapper.vm.updateSelectedLabels).toHaveBeenCalledWith([mockLabels[2]]); }); it('calls action `toggleDropdownContents` when Esc key is pressed', () => { diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/label_item_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/label_item_spec.js index bd1705e7693..bedb6204088 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/label_item_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/label_item_spec.js @@ -1,4 +1,4 @@ -import { GlIcon, GlLink } from '@gitlab/ui'; +import { GlLink } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import LabelItem from '~/vue_shared/components/sidebar/labels_select_vue/label_item.vue'; @@ -45,18 +45,26 @@ describe('LabelItem', () => { wrapperTemp.destroy(); }); - it('renders visible gl-icon component when `isLabelSet` prop is true', () => { - const wrapperTemp = createComponent({ - isLabelSet: true, - }); - - const iconEl = wrapperTemp.find(GlIcon); - - expect(iconEl.isVisible()).toBe(true); - expect(iconEl.props('name')).toBe('mobile-issue-close'); - - wrapperTemp.destroy(); - }); + it.each` + isLabelSet | isLabelIndeterminate | testId | iconName + ${true} | ${false} | ${'checked-icon'} | ${'mobile-issue-close'} + ${false} | ${true} | ${'indeterminate-icon'} | ${'dash'} + `( + 'renders visible gl-icon component when `isLabelSet` prop is $isLabelSet and `isLabelIndeterminate` is $isLabelIndeterminate', + ({ isLabelSet, isLabelIndeterminate, testId, iconName }) => { + const wrapperTemp = createComponent({ + isLabelSet, + isLabelIndeterminate, + }); + + const iconEl = wrapperTemp.find(`[data-testid="${testId}"]`); + + expect(iconEl.isVisible()).toBe(true); + expect(iconEl.props('name')).toBe(iconName); + + wrapperTemp.destroy(); + }, + ); it('renders visible span element as placeholder instead of gl-icon when `isLabelSet` prop is false', () => { const wrapperTemp = createComponent({ diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js index 31819d0e2f7..c150410ff8e 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js @@ -46,9 +46,15 @@ describe('LabelsSelectRoot', () => { describe('methods', () => { describe('handleVuexActionDispatch', () => { + const touchedLabels = [ + { + id: 2, + touched: true, + }, + ]; + it('calls `handleDropdownClose` when params `action.type` is `toggleDropdownContents` and state has `showDropdownButton` & `showDropdownContents` props `false`', () => { createComponent(); - jest.spyOn(wrapper.vm, 'handleDropdownClose').mockImplementation(); wrapper.vm.handleVuexActionDispatch( { type: 'toggleDropdownContents' }, @@ -59,14 +65,12 @@ describe('LabelsSelectRoot', () => { }, ); - expect(wrapper.vm.handleDropdownClose).toHaveBeenCalledWith( - expect.arrayContaining([ - { - id: 2, - touched: true, - }, - ]), - ); + // We're utilizing `onDropdownClose` event emitted from the component to always include `touchedLabels` + // while the first param of the method is the labels list which were added/removed. + expect(wrapper.emitted('updateSelectedLabels')).toBeTruthy(); + expect(wrapper.emitted('updateSelectedLabels')[0]).toEqual([touchedLabels]); + expect(wrapper.emitted('onDropdownClose')).toBeTruthy(); + expect(wrapper.emitted('onDropdownClose')[0]).toEqual([touchedLabels]); }); it('calls `handleDropdownClose` with state.labels filterd using `set` prop when dropdown variant is `embedded`', () => { @@ -75,8 +79,6 @@ describe('LabelsSelectRoot', () => { variant: 'embedded', }); - jest.spyOn(wrapper.vm, 'handleDropdownClose').mockImplementation(); - wrapper.vm.handleVuexActionDispatch( { type: 'toggleDropdownContents' }, { @@ -86,34 +88,17 @@ describe('LabelsSelectRoot', () => { }, ); - expect(wrapper.vm.handleDropdownClose).toHaveBeenCalledWith( - expect.arrayContaining([ + expect(wrapper.emitted('updateSelectedLabels')).toBeTruthy(); + expect(wrapper.emitted('updateSelectedLabels')[0]).toEqual([ + [ { id: 2, set: true, }, - ]), - ); - }); - }); - - describe('handleDropdownClose', () => { - beforeEach(() => { - createComponent(); - }); - - it('emits `updateSelectedLabels` & `onDropdownClose` events on component when provided `labels` param is not empty', () => { - wrapper.vm.handleDropdownClose([{ id: 1 }, { id: 2 }]); - - expect(wrapper.emitted().updateSelectedLabels).toBeTruthy(); - expect(wrapper.emitted().onDropdownClose).toBeTruthy(); - }); - - it('emits only `onDropdownClose` event on component when provided `labels` param is empty', () => { - wrapper.vm.handleDropdownClose([]); - - expect(wrapper.emitted().updateSelectedLabels).toBeFalsy(); - expect(wrapper.emitted().onDropdownClose).toBeTruthy(); + ], + ]); + expect(wrapper.emitted('onDropdownClose')).toBeTruthy(); + expect(wrapper.emitted('onDropdownClose')[0]).toEqual([[]]); }); }); @@ -152,13 +137,13 @@ describe('LabelsSelectRoot', () => { it('renders `dropdown-value-collapsed` component when `allowLabelCreate` prop is `true`', async () => { createComponent(); - await nextTick; + await nextTick(); expect(wrapper.find(DropdownValueCollapsed).exists()).toBe(true); }); it('renders `dropdown-title` component', async () => { createComponent(); - await nextTick; + await nextTick(); expect(wrapper.find(DropdownTitle).exists()).toBe(true); }); @@ -166,7 +151,7 @@ describe('LabelsSelectRoot', () => { createComponent(mockConfig, { default: 'None', }); - await nextTick; + await nextTick(); const valueComp = wrapper.find(DropdownValue); @@ -177,14 +162,14 @@ describe('LabelsSelectRoot', () => { it('renders `dropdown-button` component when `showDropdownButton` prop is `true`', async () => { createComponent(); wrapper.vm.$store.dispatch('toggleDropdownButton'); - await nextTick; + await nextTick(); expect(wrapper.find(DropdownButton).exists()).toBe(true); }); it('renders `dropdown-contents` component when `showDropdownButton` & `showDropdownContents` prop is `true`', async () => { createComponent(); wrapper.vm.$store.dispatch('toggleDropdownContents'); - await nextTick; + await nextTick(); expect(wrapper.find(DropdownContents).exists()).toBe(true); }); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/getters_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/getters_spec.js index 1f899e84897..6ad46dbe898 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/getters_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/getters_spec.js @@ -17,24 +17,39 @@ describe('LabelsSelect Getters', () => { }, ); - it('returns label title when state.labels has only 1 label', () => { - const labels = [{ id: 1, title: 'Foobar', set: true }]; + describe.each` + dropdownVariant | isDropdownVariantSidebar | isDropdownVariantEmbedded + ${'sidebar'} | ${true} | ${false} + ${'embedded'} | ${false} | ${true} + `( + 'when dropdown variant is $dropdownVariant', + ({ isDropdownVariantSidebar, isDropdownVariantEmbedded }) => { + it('returns label title when state.labels has only 1 label', () => { + const labels = [{ id: 1, title: 'Foobar', set: true }]; - expect(getters.dropdownButtonText({ labels }, { isDropdownVariantSidebar: true })).toBe( - 'Foobar', - ); - }); + expect( + getters.dropdownButtonText( + { labels }, + { isDropdownVariantSidebar, isDropdownVariantEmbedded }, + ), + ).toBe('Foobar'); + }); - it('returns first label title and remaining labels count when state.labels has more than 1 label', () => { - const labels = [ - { id: 1, title: 'Foo', set: true }, - { id: 2, title: 'Bar', set: true }, - ]; + it('returns first label title and remaining labels count when state.labels has more than 1 label', () => { + const labels = [ + { id: 1, title: 'Foo', set: true }, + { id: 2, title: 'Bar', set: true }, + ]; - expect(getters.dropdownButtonText({ labels }, { isDropdownVariantSidebar: true })).toBe( - 'Foo +1 more', - ); - }); + expect( + getters.dropdownButtonText( + { labels }, + { isDropdownVariantSidebar, isDropdownVariantEmbedded }, + ), + ).toBe('Foo +1 more'); + }); + }, + ); }); describe('selectedLabelsList', () => { 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 a60e6f52862..1819e750324 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 @@ -80,7 +80,10 @@ describe('LabelsSelect Mutations', () => { }); describe(`${types.RECEIVE_SET_LABELS_SUCCESS}`, () => { - const selectedLabels = [{ id: 2 }, { id: 4 }]; + const selectedLabels = [ + { id: 2, set: true }, + { id: 4, set: true }, + ]; const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }]; it('sets value of `state.labelsFetchInProgress` to false', () => { @@ -196,20 +199,23 @@ describe('LabelsSelect Mutations', () => { it('updates labels `set` state to match selected labels', () => { const state = { labels: [ - { id: 1, title: 'scoped::test', set: false }, - { id: 2, set: true, title: 'scoped::one', touched: true }, - { id: 3, title: '' }, - { id: 4, title: '' }, + { id: 1, title: 'scoped::test', set: false, indeterminate: false }, + { id: 2, title: 'scoped::one', set: true, indeterminate: false, touched: true }, + { id: 3, title: '', set: false, indeterminate: false }, + { id: 4, title: '', set: false, indeterminate: false }, + ], + selectedLabels: [ + { id: 1, set: true }, + { id: 3, set: true }, ], - selectedLabels: [{ id: 1 }, { id: 3 }], }; mutations[types.UPDATE_LABELS_SET_STATE](state); expect(state.labels).toEqual([ - { id: 1, title: 'scoped::test', set: true }, - { id: 2, set: false, title: 'scoped::one', touched: true }, - { id: 3, title: '', set: true }, - { id: 4, title: '', set: false }, + { id: 1, title: 'scoped::test', set: true, indeterminate: false }, + { id: 2, title: 'scoped::one', set: false, indeterminate: false, touched: true }, + { id: 3, title: '', set: true, indeterminate: false }, + { id: 4, title: '', set: false, indeterminate: false }, ]); }); }); diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/index_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/index_spec.js new file mode 100644 index 00000000000..83fdc5d669d --- /dev/null +++ b/spec/frontend/vue_shared/components/source_viewer/plugins/index_spec.js @@ -0,0 +1,14 @@ +import { registerPlugins } from '~/vue_shared/components/source_viewer/plugins/index'; +import { HLJS_ON_AFTER_HIGHLIGHT } from '~/vue_shared/components/source_viewer/constants'; +import wrapComments from '~/vue_shared/components/source_viewer/plugins/wrap_comments'; + +jest.mock('~/vue_shared/components/source_viewer/plugins/wrap_comments'); +const hljsMock = { addPlugin: jest.fn() }; + +describe('Highlight.js plugin registration', () => { + beforeEach(() => registerPlugins(hljsMock)); + + it('registers our plugins', () => { + expect(hljsMock.addPlugin).toHaveBeenCalledWith({ [HLJS_ON_AFTER_HIGHLIGHT]: wrapComments }); + }); +}); diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_comments_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_comments_spec.js new file mode 100644 index 00000000000..5fd4182da29 --- /dev/null +++ b/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_comments_spec.js @@ -0,0 +1,29 @@ +import { HLJS_COMMENT_SELECTOR } from '~/vue_shared/components/source_viewer/constants'; +import wrapComments from '~/vue_shared/components/source_viewer/plugins/wrap_comments'; + +describe('Highlight.js plugin for wrapping comments', () => { + it('mutates the input value by wrapping each line in a span tag', () => { + const inputValue = `<span class="${HLJS_COMMENT_SELECTOR}">/* Line 1 \n* Line 2 \n*/</span>`; + const outputValue = `<span class="${HLJS_COMMENT_SELECTOR}">/* Line 1 \n<span class="${HLJS_COMMENT_SELECTOR}">* Line 2 </span>\n<span class="${HLJS_COMMENT_SELECTOR}">*/</span>`; + const hljsResultMock = { value: inputValue }; + + wrapComments(hljsResultMock); + expect(hljsResultMock.value).toBe(outputValue); + }); + + it('does not mutate the input value if the hljs comment selector is not present', () => { + const inputValue = '<span class="hljs-keyword">const</span>'; + const hljsResultMock = { value: inputValue }; + + wrapComments(hljsResultMock); + expect(hljsResultMock.value).toBe(inputValue); + }); + + it('does not mutate the input value if the hljs comment line includes a closing tag', () => { + const inputValue = `<span class="${HLJS_COMMENT_SELECTOR}">/* Line 1 </span> \n* Line 2 \n*/`; + const hljsResultMock = { value: inputValue }; + + wrapComments(hljsResultMock); + expect(hljsResultMock.value).toBe(inputValue); + }); +}); diff --git a/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js index 6a9ea75127d..bb0945a1f3e 100644 --- a/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js +++ b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js @@ -3,6 +3,7 @@ import Vue from 'vue'; import VueRouter from 'vue-router'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer.vue'; +import { registerPlugins } from '~/vue_shared/components/source_viewer/plugins/index'; import Chunk from '~/vue_shared/components/source_viewer/components/chunk.vue'; import { ROUGE_TO_HLJS_LANGUAGE_MAP } from '~/vue_shared/components/source_viewer/constants'; import waitForPromises from 'helpers/wait_for_promises'; @@ -11,6 +12,7 @@ import eventHub from '~/notes/event_hub'; jest.mock('~/blob/line_highlighter'); jest.mock('highlight.js/lib/core'); +jest.mock('~/vue_shared/components/source_viewer/plugins/index'); Vue.use(VueRouter); const router = new VueRouter(); @@ -59,6 +61,10 @@ describe('Source Viewer component', () => { describe('highlight.js', () => { beforeEach(() => createComponent({ language: mappedLanguage })); + it('registers our plugins for Highlight.js', () => { + expect(registerPlugins).toHaveBeenCalledWith(hljs); + }); + it('registers the language definition', async () => { const languageDefinition = await import(`highlight.js/lib/languages/${mappedLanguage}`); 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 a613b325462..1798ca5ccde 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 @@ -5,7 +5,7 @@ exports[`Upload dropzone component correctly overrides description and drop mess class="gl-w-full gl-relative" > <button - class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3" + class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4" type="button" > <div @@ -41,7 +41,7 @@ exports[`Upload dropzone component correctly overrides description and drop mess name="upload-dropzone-fade" > <div - class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-3 gl-bg-white" + class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-4" style="display: none;" > <div @@ -86,7 +86,7 @@ exports[`Upload dropzone component when dragging renders correct template when d class="gl-w-full gl-relative" > <button - class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3" + class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4" type="button" > <div @@ -126,7 +126,7 @@ exports[`Upload dropzone component when dragging renders correct template when d name="upload-dropzone-fade" > <div - class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-3 gl-bg-white" + class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-4" style="" > <div @@ -171,7 +171,7 @@ exports[`Upload dropzone component when dragging renders correct template when d class="gl-w-full gl-relative" > <button - class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3" + class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4" type="button" > <div @@ -211,7 +211,7 @@ exports[`Upload dropzone component when dragging renders correct template when d name="upload-dropzone-fade" > <div - class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-3 gl-bg-white" + class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-4" style="" > <div @@ -256,7 +256,7 @@ exports[`Upload dropzone component when dragging renders correct template when d class="gl-w-full gl-relative" > <button - class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3" + class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4" type="button" > <div @@ -296,7 +296,7 @@ exports[`Upload dropzone component when dragging renders correct template when d name="upload-dropzone-fade" > <div - class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-3 gl-bg-white" + class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-4" style="" > <div @@ -342,7 +342,7 @@ exports[`Upload dropzone component when dragging renders correct template when d class="gl-w-full gl-relative" > <button - class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3" + class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4" type="button" > <div @@ -382,7 +382,7 @@ exports[`Upload dropzone component when dragging renders correct template when d name="upload-dropzone-fade" > <div - class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-3 gl-bg-white" + class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-4" style="" > <div @@ -428,7 +428,7 @@ exports[`Upload dropzone component when dragging renders correct template when d class="gl-w-full gl-relative" > <button - class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3" + class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4" type="button" > <div @@ -468,7 +468,7 @@ exports[`Upload dropzone component when dragging renders correct template when d name="upload-dropzone-fade" > <div - class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-3 gl-bg-white" + class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-4" style="display: none;" > <div @@ -514,7 +514,7 @@ exports[`Upload dropzone component when no slot provided renders default dropzon class="gl-w-full gl-relative" > <button - class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3" + class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4" type="button" > <div @@ -554,7 +554,7 @@ exports[`Upload dropzone component when no slot provided renders default dropzon name="upload-dropzone-fade" > <div - class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-3 gl-bg-white" + class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-4" style="display: none;" > <div @@ -606,7 +606,7 @@ exports[`Upload dropzone component when slot provided renders dropzone with slot name="upload-dropzone-fade" > <div - class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-3 gl-bg-white" + class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-4" style="display: none;" > <div |