diff options
Diffstat (limited to 'spec/frontend/vue_shared')
51 files changed, 1309 insertions, 285 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 408f9d57147..e84eb7789d3 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,6 +4,7 @@ exports[`Clone Dropdown Button rendering matches the snapshot 1`] = ` <gl-new-dropdown-stub category="primary" headertext="" + right="" size="medium" text="Clone" variant="info" @@ -38,7 +39,7 @@ exports[`Clone Dropdown Button rendering matches the snapshot 1`] = ` tag="div" > <gl-button-stub - category="tertiary" + category="primary" class="d-inline-flex" data-clipboard-text="ssh://foo.bar" data-qa-selector="copy_ssh_url_button" @@ -79,7 +80,7 @@ exports[`Clone Dropdown Button rendering matches the snapshot 1`] = ` tag="div" > <gl-button-stub - category="tertiary" + category="primary" class="d-inline-flex" data-clipboard-text="http://foo.bar" data-qa-selector="copy_http_url_button" diff --git a/spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap index 1f54405928b..cd4728baeaa 100644 --- a/spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap +++ b/spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap @@ -4,20 +4,22 @@ exports[`Expand button on click when short text is provided renders button after <span> <button aria-label="Click to expand text" - class="btn js-text-expander-prepend text-expander btn-blank btn-secondary btn-md" + class="btn js-text-expander-prepend text-expander btn-blank btn-default btn-md btn-icon button-ellipsis-horizontal gl-button" style="display: none;" type="button" > <!----> <svg - aria-hidden="true" - class="s12 ic-ellipsis_h" + class="gl-icon s16" + data-testid="ellipsis_h-icon" > <use - xlink:href="#ellipsis_h" + href="#ellipsis_h" /> </svg> + + <!----> </button> <!----> @@ -30,20 +32,22 @@ exports[`Expand button on click when short text is provided renders button after <button aria-label="Click to expand text" - class="btn js-text-expander-append text-expander btn-blank btn-secondary btn-md" + class="btn js-text-expander-append text-expander btn-blank btn-default btn-md btn-icon button-ellipsis-horizontal gl-button" style="" type="button" > <!----> <svg - aria-hidden="true" - class="s12 ic-ellipsis_h" + class="gl-icon s16" + data-testid="ellipsis_h-icon" > <use - xlink:href="#ellipsis_h" + href="#ellipsis_h" /> </svg> + + <!----> </button> </span> `; @@ -52,19 +56,21 @@ exports[`Expand button when short text is provided renders button before text 1` <span> <button aria-label="Click to expand text" - class="btn js-text-expander-prepend text-expander btn-blank btn-secondary btn-md" + class="btn js-text-expander-prepend text-expander btn-blank btn-default btn-md btn-icon button-ellipsis-horizontal gl-button" type="button" > <!----> <svg - aria-hidden="true" - class="s12 ic-ellipsis_h" + class="gl-icon s16" + data-testid="ellipsis_h-icon" > <use - xlink:href="#ellipsis_h" + href="#ellipsis_h" /> </svg> + + <!----> </button> <span> @@ -77,20 +83,22 @@ exports[`Expand button when short text is provided renders button before text 1` <button aria-label="Click to expand text" - class="btn js-text-expander-append text-expander btn-blank btn-secondary btn-md" + class="btn js-text-expander-append text-expander btn-blank btn-default btn-md btn-icon button-ellipsis-horizontal gl-button" style="display: none;" type="button" > <!----> <svg - aria-hidden="true" - class="s12 ic-ellipsis_h" + class="gl-icon s16" + data-testid="ellipsis_h-icon" > <use - xlink:href="#ellipsis_h" + href="#ellipsis_h" /> </svg> + + <!----> </button> </span> `; 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 74f71c23d02..fcb9c4b8b02 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 @@ -1,13 +1,13 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`SplitButton renders actionItems 1`] = ` -<gl-dropdown-stub +<gl-deprecated-dropdown-stub menu-class="dropdown-menu-selectable " split="true" text="professor" variant="secondary" > - <gl-dropdown-item-stub + <gl-deprecated-dropdown-item-stub active="true" active-class="is-active" > @@ -18,10 +18,10 @@ exports[`SplitButton renders actionItems 1`] = ` <div> very symphonic </div> - </gl-dropdown-item-stub> + </gl-deprecated-dropdown-item-stub> - <gl-dropdown-divider-stub /> - <gl-dropdown-item-stub + <gl-deprecated-dropdown-divider-stub /> + <gl-deprecated-dropdown-item-stub active-class="is-active" > <strong> @@ -31,8 +31,8 @@ exports[`SplitButton renders actionItems 1`] = ` <div> warp drive </div> - </gl-dropdown-item-stub> + </gl-deprecated-dropdown-item-stub> <!----> -</gl-dropdown-stub> +</gl-deprecated-dropdown-stub> `; diff --git a/spec/frontend/vue_shared/components/clone_dropdown_spec.js b/spec/frontend/vue_shared/components/clone_dropdown_spec.js index 38e0cadfe83..d9829874b93 100644 --- a/spec/frontend/vue_shared/components/clone_dropdown_spec.js +++ b/spec/frontend/vue_shared/components/clone_dropdown_spec.js @@ -1,6 +1,6 @@ -import CloneDropdown from '~/vue_shared/components/clone_dropdown.vue'; import { shallowMount } from '@vue/test-utils'; import { GlFormInputGroup, GlNewDropdownHeader } from '@gitlab/ui'; +import CloneDropdown from '~/vue_shared/components/clone_dropdown.vue'; describe('Clone Dropdown Button', () => { let wrapper; 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 8d3fcdd48d2..c75891c9ed3 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,8 +1,8 @@ import $ from 'jquery'; -import axios from '~/lib/utils/axios_utils'; import MockAdapter from 'axios-mock-adapter'; import { mount } from '@vue/test-utils'; import waitForPromises from 'helpers/wait_for_promises'; +import axios from '~/lib/utils/axios_utils'; import MarkdownViewer from '~/vue_shared/components/content_viewer/viewers/markdown_viewer.vue'; describe('MarkdownViewer', () => { diff --git a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js index ceea8d2fa92..223e22d650b 100644 --- a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js +++ b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js @@ -13,9 +13,9 @@ describe('DateTimePicker', () => { const dropdownToggle = () => wrapper.find('.dropdown-toggle'); const dropdownMenu = () => wrapper.find('.dropdown-menu'); + const cancelButton = () => wrapper.find('[data-testid="cancelButton"]'); const applyButtonElement = () => wrapper.find('button.btn-success').element; const findQuickRangeItems = () => wrapper.findAll('.dropdown-item'); - const cancelButtonElement = () => wrapper.find('button.btn-secondary').element; const createComponent = props => { wrapper = mount(DateTimePicker, { @@ -260,7 +260,7 @@ describe('DateTimePicker', () => { dropdownToggle().trigger('click'); return wrapper.vm.$nextTick(() => { - cancelButtonElement().click(); + cancelButton().trigger('click'); return wrapper.vm.$nextTick(() => { expect(dropdownMenu().classes('show')).toBe(false); diff --git a/spec/frontend/vue_shared/components/dismissible_container_spec.js b/spec/frontend/vue_shared/components/dismissible_container_spec.js new file mode 100644 index 00000000000..e49ca1e2285 --- /dev/null +++ b/spec/frontend/vue_shared/components/dismissible_container_spec.js @@ -0,0 +1,58 @@ +import MockAdapter from 'axios-mock-adapter'; +import { shallowMount } from '@vue/test-utils'; +import axios from '~/lib/utils/axios_utils'; +import dismissibleContainer from '~/vue_shared/components/dismissible_container.vue'; + +describe('DismissibleContainer', () => { + let wrapper; + const propsData = { + path: 'some/path', + featureId: 'some-feature-id', + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('template', () => { + const findBtn = () => wrapper.find('[data-testid="close"]'); + let mockAxios; + + beforeEach(() => { + mockAxios = new MockAdapter(axios); + wrapper = shallowMount(dismissibleContainer, { propsData }); + }); + + afterEach(() => { + mockAxios.restore(); + }); + + it('successfully dismisses', () => { + mockAxios.onPost(propsData.path).replyOnce(200); + const button = findBtn(); + + button.trigger('click'); + + expect(wrapper.emitted().dismiss).toBeTruthy(); + }); + }); + + describe('slots', () => { + const slots = { + title: 'Foo Title', + default: 'default slot', + }; + + it.each(Object.keys(slots))('renders the %s slot', slot => { + const slotContent = slots[slot]; + wrapper = shallowMount(dismissibleContainer, { + propsData, + slots: { + [slot]: `<span>${slotContent}</span>`, + }, + }); + + expect(wrapper.text()).toContain(slotContent); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js b/spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js new file mode 100644 index 00000000000..4c4baf23120 --- /dev/null +++ b/spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js @@ -0,0 +1,91 @@ +import { mount, shallowMount } from '@vue/test-utils'; +import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui'; +import { useLocalStorageSpy } from 'helpers/local_storage_helper'; +import Component from '~/vue_shared/components/dismissible_feedback_alert.vue'; + +describe('Dismissible Feedback Alert', () => { + useLocalStorageSpy(); + + let wrapper; + + const defaultProps = { + featureName: 'Dependency List', + feedbackLink: 'https://gitlab.link', + }; + + const STORAGE_DISMISSAL_KEY = 'dependency_list_feedback_dismissed'; + + const createComponent = ({ props, shallow } = {}) => { + const mountFn = shallow ? shallowMount : mount; + + wrapper = mountFn(Component, { + propsData: { + ...defaultProps, + ...props, + }, + stubs: { + GlSprintf, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + const findAlert = () => wrapper.find(GlAlert); + const findLink = () => wrapper.find(GlLink); + + describe('with default', () => { + beforeEach(() => { + createComponent(); + }); + + it('shows 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); + }); + }); + + describe('dismissible', () => { + describe('after dismissal', () => { + beforeEach(() => { + createComponent({ shallow: false }); + findAlert().vm.$emit('dismiss'); + }); + + it('hides the alert', () => { + expect(findAlert().exists()).toBe(false); + }); + + it('should remember the dismissal state', () => { + expect(localStorage.setItem).toHaveBeenCalledWith(STORAGE_DISMISSAL_KEY, 'true'); + }); + }); + + describe('already dismissed', () => { + it('should not show the alert once dismissed', async () => { + localStorage.setItem(STORAGE_DISMISSAL_KEY, 'true'); + createComponent({ shallow: false }); + await wrapper.vm.$nextTick(); + + expect(findAlert().exists()).toBe(false); + }); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/file_finder/item_spec.js b/spec/frontend/vue_shared/components/file_finder/item_spec.js index 63f2614106d..5a45a5dbba1 100644 --- a/spec/frontend/vue_shared/components/file_finder/item_spec.js +++ b/spec/frontend/vue_shared/components/file_finder/item_spec.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import { file } from 'jest/ide/helpers'; -import ItemComponent from '~/vue_shared/components/file_finder/item.vue'; import createComponent from 'helpers/vue_mount_component_helper'; +import ItemComponent from '~/vue_shared/components/file_finder/item.vue'; describe('File finder item spec', () => { const Component = Vue.extend(ItemComponent); diff --git a/spec/frontend/vue_shared/components/file_icon_spec.js b/spec/frontend/vue_shared/components/file_icon_spec.js index adf0da21f9f..e55449dc684 100644 --- a/spec/frontend/vue_shared/components/file_icon_spec.js +++ b/spec/frontend/vue_shared/components/file_icon_spec.js @@ -36,6 +36,9 @@ describe('File Icon component', () => { fileName | iconName ${'test.js'} | ${'javascript'} ${'test.png'} | ${'image'} + ${'test.PNG'} | ${'image'} + ${'.npmrc'} | ${'npm'} + ${'.Npmrc'} | ${'file'} ${'webpack.js'} | ${'webpack'} `('should render a $iconName icon based on file ending', ({ fileName, iconName }) => { createComponent({ fileName }); diff --git a/spec/frontend/vue_shared/components/file_row_spec.js b/spec/frontend/vue_shared/components/file_row_spec.js index 46df2d2aaf1..1acd2e05464 100644 --- a/spec/frontend/vue_shared/components/file_row_spec.js +++ b/spec/frontend/vue_shared/components/file_row_spec.js @@ -1,8 +1,8 @@ import { file } from 'jest/ide/helpers'; -import FileRow from '~/vue_shared/components/file_row.vue'; -import FileHeader from '~/vue_shared/components/file_row_header.vue'; import { shallowMount } from '@vue/test-utils'; import { nextTick } from 'vue'; +import FileRow from '~/vue_shared/components/file_row.vue'; +import FileHeader from '~/vue_shared/components/file_row_header.vue'; import { escapeFileUrl } from '~/lib/utils/url_utility'; describe('File row component', () => { diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js index 05508d14209..73dbecadd89 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js @@ -1,4 +1,4 @@ -import { shallowMount } from '@vue/test-utils'; +import { shallowMount, mount } from '@vue/test-utils'; import { GlFilteredSearch, GlButtonGroup, @@ -16,13 +16,16 @@ import RecentSearchesService from '~/filtered_search/services/recent_searches_se import { mockAvailableTokens, mockSortOptions, mockHistoryItems } from './mock_data'; const createComponent = ({ + shallow = true, namespace = 'gitlab-org/gitlab-test', recentSearchesStorageKey = 'requirements', tokens = mockAvailableTokens, - sortOptions = mockSortOptions, + sortOptions, searchInputPlaceholder = 'Filter requirements', -} = {}) => - shallowMount(FilteredSearchBarRoot, { +} = {}) => { + const mountMethod = shallow ? shallowMount : mount; + + return mountMethod(FilteredSearchBarRoot, { propsData: { namespace, recentSearchesStorageKey, @@ -31,12 +34,13 @@ const createComponent = ({ searchInputPlaceholder, }, }); +}; describe('FilteredSearchBarRoot', () => { let wrapper; beforeEach(() => { - wrapper = createComponent(); + wrapper = createComponent({ sortOptions: mockSortOptions }); }); afterEach(() => { @@ -44,23 +48,38 @@ describe('FilteredSearchBarRoot', () => { }); describe('data', () => { - it('initializes `filterValue`, `selectedSortOption` and `selectedSortDirection` data props', () => { + it('initializes `filterValue`, `selectedSortOption` and `selectedSortDirection` data props and displays the sort dropdown', () => { expect(wrapper.vm.filterValue).toEqual([]); expect(wrapper.vm.selectedSortOption).toBe(mockSortOptions[0].sortDirection.descending); expect(wrapper.vm.selectedSortDirection).toBe(SortDirection.descending); + expect(wrapper.contains(GlButtonGroup)).toBe(true); + expect(wrapper.contains(GlButton)).toBe(true); + expect(wrapper.contains(GlDropdown)).toBe(true); + expect(wrapper.contains(GlDropdownItem)).toBe(true); + }); + + it('does not initialize `selectedSortOption` and `selectedSortDirection` when `sortOptions` is not applied and hides the sort dropdown', () => { + const wrapperNoSort = createComponent(); + + expect(wrapperNoSort.vm.filterValue).toEqual([]); + expect(wrapperNoSort.vm.selectedSortOption).toBe(undefined); + expect(wrapperNoSort.contains(GlButtonGroup)).toBe(false); + expect(wrapperNoSort.contains(GlButton)).toBe(false); + expect(wrapperNoSort.contains(GlDropdown)).toBe(false); + expect(wrapperNoSort.contains(GlDropdownItem)).toBe(false); }); }); describe('computed', () => { describe('tokenSymbols', () => { it('returns a map containing type and symbols from `tokens` prop', () => { - expect(wrapper.vm.tokenSymbols).toEqual({ author_username: '@' }); + expect(wrapper.vm.tokenSymbols).toEqual({ author_username: '@', label_name: '~' }); }); }); describe('tokenTitles', () => { it('returns a map containing type and title from `tokens` prop', () => { - expect(wrapper.vm.tokenTitles).toEqual({ author_username: 'Author' }); + expect(wrapper.vm.tokenTitles).toEqual({ author_username: 'Author', label_name: 'Label' }); }); }); @@ -99,6 +118,29 @@ describe('FilteredSearchBarRoot', () => { expect(wrapper.vm.sortDirectionTooltip).toBe('Sort direction: Descending'); }); }); + + describe('filteredRecentSearches', () => { + it('returns array of recent searches filtering out any string type (unsupported) items', async () => { + wrapper.setData({ + recentSearches: [{ foo: 'bar' }, 'foo'], + }); + + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.filteredRecentSearches).toHaveLength(1); + expect(wrapper.vm.filteredRecentSearches[0]).toEqual({ foo: 'bar' }); + }); + + it('returns undefined when recentSearchesStorageKey prop is not set on component', async () => { + wrapper.setProps({ + recentSearchesStorageKey: '', + }); + + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.filteredRecentSearches).not.toBeDefined(); + }); + }); }); describe('watchers', () => { @@ -139,6 +181,46 @@ describe('FilteredSearchBarRoot', () => { }); }); + describe('removeQuotesEnclosure', () => { + const mockFilters = [ + { + type: 'author_username', + value: { + data: 'root', + operator: '=', + }, + }, + { + type: 'label_name', + value: { + data: '"Documentation Update"', + operator: '=', + }, + }, + 'foo', + ]; + + it('returns filter array with unescaped strings for values which have spaces', () => { + expect(wrapper.vm.removeQuotesEnclosure(mockFilters)).toEqual([ + { + type: 'author_username', + value: { + data: 'root', + operator: '=', + }, + }, + { + type: 'label_name', + value: { + data: 'Documentation Update', + operator: '=', + }, + }, + 'foo', + ]); + }); + }); + describe('handleSortOptionClick', () => { it('emits component event `onSort` with selected sort by value', () => { wrapper.vm.handleSortOptionClick(mockSortOptions[1]); @@ -172,9 +254,12 @@ describe('FilteredSearchBarRoot', () => { describe('handleHistoryItemSelected', () => { it('emits `onFilter` event with provided filters param', () => { + jest.spyOn(wrapper.vm, 'removeQuotesEnclosure'); + wrapper.vm.handleHistoryItemSelected(mockHistoryItems[0]); expect(wrapper.emitted('onFilter')[0]).toEqual([mockHistoryItems[0]]); + expect(wrapper.vm.removeQuotesEnclosure).toHaveBeenCalledWith(mockHistoryItems[0]); }); }); @@ -233,10 +318,21 @@ describe('FilteredSearchBarRoot', () => { }); }); + it('calls `blurSearchInput` method to remove focus from filter input field', () => { + jest.spyOn(wrapper.vm, 'blurSearchInput'); + + wrapper.find(GlFilteredSearch).vm.$emit('submit', mockFilters); + + expect(wrapper.vm.blurSearchInput).toHaveBeenCalled(); + }); + it('emits component event `onFilter` with provided filters param', () => { + jest.spyOn(wrapper.vm, 'removeQuotesEnclosure'); + wrapper.vm.handleFilterSubmit(mockFilters); expect(wrapper.emitted('onFilter')[0]).toEqual([mockFilters]); + expect(wrapper.vm.removeQuotesEnclosure).toHaveBeenCalledWith(mockFilters); }); }); }); @@ -260,13 +356,28 @@ describe('FilteredSearchBarRoot', () => { expect(glFilteredSearchEl.props('historyItems')).toEqual(mockHistoryItems); }); + it('renders search history items dropdown with formatting done using token symbols', async () => { + const wrapperFullMount = createComponent({ sortOptions: mockSortOptions, shallow: false }); + wrapperFullMount.vm.recentSearchesStore.addRecentSearch(mockHistoryItems[0]); + + await wrapperFullMount.vm.$nextTick(); + + const searchHistoryItemsEl = wrapperFullMount.findAll( + '.gl-search-box-by-click-menu .gl-search-box-by-click-history-item', + ); + + expect(searchHistoryItemsEl.at(0).text()).toBe('Author := @tobyLabel := ~Bug"duo"'); + + wrapperFullMount.destroy(); + }); + it('renders sort dropdown component', () => { expect(wrapper.find(GlButtonGroup).exists()).toBe(true); expect(wrapper.find(GlDropdown).exists()).toBe(true); expect(wrapper.find(GlDropdown).props('text')).toBe(mockSortOptions[0].title); }); - it('renders dropdown items', () => { + it('renders sort dropdown items', () => { const dropdownItemsEl = wrapper.findAll(GlDropdownItem); expect(dropdownItemsEl).toHaveLength(mockSortOptions.length); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js new file mode 100644 index 00000000000..a857f84adf1 --- /dev/null +++ b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js @@ -0,0 +1,19 @@ +import * as filteredSearchUtils from '~/vue_shared/components/filtered_search_bar/filtered_search_utils'; + +describe('Filtered Search Utils', () => { + describe('stripQuotes', () => { + it.each` + inputValue | outputValue + ${'"Foo Bar"'} | ${'Foo Bar'} + ${"'Foo Bar'"} | ${'Foo Bar'} + ${'FooBar'} | ${'FooBar'} + ${"Foo'Bar"} | ${"Foo'Bar"} + ${'Foo"Bar'} | ${'Foo"Bar'} + `( + 'returns string $outputValue when called with string $inputValue', + ({ inputValue, outputValue }) => { + expect(filteredSearchUtils.stripQuotes(inputValue)).toBe(outputValue); + }, + ); + }); +}); 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 7e28c4e11e1..dcccb1f49b6 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 @@ -1,5 +1,8 @@ +import { mockLabels } from 'jest/vue_shared/components/sidebar/labels_select_vue/mock_data'; import Api from '~/api'; import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue'; +import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue'; +import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue'; export const mockAuthor1 = { id: 1, @@ -30,6 +33,28 @@ export const mockAuthor3 = { export const mockAuthors = [mockAuthor1, mockAuthor2, mockAuthor3]; +export const mockRegularMilestone = { + id: 1, + name: '4.0', + title: '4.0', +}; + +export const mockEscapedMilestone = { + id: 3, + name: '5.0 RC1', + title: '5.0 RC1', +}; + +export const mockMilestones = [ + { + id: 2, + name: '5.0', + title: '5.0', + }, + mockRegularMilestone, + mockEscapedMilestone, +]; + export const mockAuthorToken = { type: 'author_username', icon: 'user', @@ -42,7 +67,29 @@ export const mockAuthorToken = { fetchAuthors: Api.projectUsers.bind(Api), }; -export const mockAvailableTokens = [mockAuthorToken]; +export const mockLabelToken = { + type: 'label_name', + icon: 'labels', + title: 'Label', + unique: false, + symbol: '~', + token: LabelToken, + operators: [{ value: '=', description: 'is', default: 'true' }], + fetchLabels: () => Promise.resolve(mockLabels), +}; + +export const mockMilestoneToken = { + type: 'milestone_title', + icon: 'clock', + title: 'Milestone', + unique: true, + symbol: '%', + token: MilestoneToken, + operators: [{ value: '=', description: 'is', default: 'true' }], + fetchMilestones: () => Promise.resolve({ data: mockMilestones }), +}; + +export const mockAvailableTokens = [mockAuthorToken, mockLabelToken]; export const mockHistoryItems = [ [ @@ -53,6 +100,13 @@ export const mockHistoryItems = [ operator: '=', }, }, + { + type: 'label_name', + value: { + data: 'Bug', + operator: '=', + }, + }, 'duo', ], [ 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 45294096eda..160febf9d06 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 @@ -4,7 +4,7 @@ import MockAdapter from 'axios-mock-adapter'; import waitForPromises from 'helpers/wait_for_promises'; import axios from '~/lib/utils/axios_utils'; -import createFlash from '~/flash'; +import { deprecatedCreateFlash as createFlash } from '~/flash'; import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue'; import { mockAuthorToken, mockAuthors } from '../mock_data'; 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 new file mode 100644 index 00000000000..0e60ee99327 --- /dev/null +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js @@ -0,0 +1,170 @@ +import { mount } from '@vue/test-utils'; +import { GlFilteredSearchToken, GlFilteredSearchTokenSegment } from '@gitlab/ui'; +import MockAdapter from 'axios-mock-adapter'; +import waitForPromises from 'helpers/wait_for_promises'; +import { + mockRegularLabel, + mockLabels, +} from 'jest/vue_shared/components/sidebar/labels_select_vue/mock_data'; +import axios from '~/lib/utils/axios_utils'; + +import { deprecatedCreateFlash as createFlash } from '~/flash'; +import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue'; + +import { mockLabelToken } from '../mock_data'; + +jest.mock('~/flash'); + +const createComponent = ({ config = mockLabelToken, value = { data: '' }, active = false } = {}) => + mount(LabelToken, { + propsData: { + config, + value, + active, + }, + provide: { + portalName: 'fake target', + alignSuggestions: function fakeAlignSuggestions() {}, + }, + stubs: { + Portal: { + template: '<div><slot></slot></div>', + }, + GlFilteredSearchSuggestionList: { + template: '<div></div>', + methods: { + getValue: () => '=', + }, + }, + }, + }); + +describe('LabelToken', () => { + let mock; + let wrapper; + + beforeEach(() => { + mock = new MockAdapter(axios); + wrapper = createComponent(); + }); + + afterEach(() => { + mock.restore(); + wrapper.destroy(); + }); + + describe('computed', () => { + beforeEach(async () => { + // Label title with spaces is always enclosed in quotations by component. + wrapper = createComponent({ value: { data: `"${mockRegularLabel.title}"` } }); + + wrapper.setData({ + labels: mockLabels, + }); + + await wrapper.vm.$nextTick(); + }); + + describe('currentValue', () => { + it('returns lowercase string for `value.data`', () => { + expect(wrapper.vm.currentValue).toBe('"foo label"'); + }); + }); + + describe('activeLabel', () => { + it('returns object for currently present `value.data`', () => { + expect(wrapper.vm.activeLabel).toEqual(mockRegularLabel); + }); + }); + + describe('containerStyle', () => { + it('returns object containing `backgroundColor` and `color` properties based on `activeLabel` value', () => { + expect(wrapper.vm.containerStyle).toEqual({ + backgroundColor: mockRegularLabel.color, + color: mockRegularLabel.textColor, + }); + }); + + it('returns empty object when `activeLabel` is not set', async () => { + wrapper.setData({ + labels: [], + }); + + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.containerStyle).toEqual({}); + }); + }); + }); + + describe('methods', () => { + describe('fetchLabelBySearchTerm', () => { + it('calls `config.fetchLabels` with provided searchTerm param', () => { + jest.spyOn(wrapper.vm.config, 'fetchLabels'); + + wrapper.vm.fetchLabelBySearchTerm('foo'); + + expect(wrapper.vm.config.fetchLabels).toHaveBeenCalledWith('foo'); + }); + + it('sets response to `labels` when request is succesful', () => { + jest.spyOn(wrapper.vm.config, 'fetchLabels').mockResolvedValue(mockLabels); + + wrapper.vm.fetchLabelBySearchTerm('foo'); + + return waitForPromises().then(() => { + expect(wrapper.vm.labels).toEqual(mockLabels); + }); + }); + + it('calls `createFlash` with flash error message when request fails', () => { + jest.spyOn(wrapper.vm.config, 'fetchLabels').mockRejectedValue({}); + + wrapper.vm.fetchLabelBySearchTerm('foo'); + + return waitForPromises().then(() => { + expect(createFlash).toHaveBeenCalledWith('There was a problem fetching labels.'); + }); + }); + + it('sets `loading` to false when request completes', () => { + jest.spyOn(wrapper.vm.config, 'fetchLabels').mockRejectedValue({}); + + wrapper.vm.fetchLabelBySearchTerm('foo'); + + return waitForPromises().then(() => { + expect(wrapper.vm.loading).toBe(false); + }); + }); + }); + }); + + describe('template', () => { + beforeEach(async () => { + wrapper = createComponent({ value: { data: `"${mockRegularLabel.title}"` } }); + + wrapper.setData({ + labels: mockLabels, + }); + + await wrapper.vm.$nextTick(); + }); + + it('renders gl-filtered-search-token component', () => { + expect(wrapper.find(GlFilteredSearchToken).exists()).toBe(true); + }); + + it('renders token item when value is selected', () => { + const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); + + expect(tokenSegments).toHaveLength(3); // Label, =, "Foo Label" + expect(tokenSegments.at(2).text()).toBe(`~${mockRegularLabel.title}`); // "Foo Label" + expect( + tokenSegments + .at(2) + .find('.gl-token') + .attributes('style'), + ).toBe('background-color: rgb(186, 218, 85); color: rgb(255, 255, 255);'); + }); + }); +}); 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 new file mode 100644 index 00000000000..de893bf44c8 --- /dev/null +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js @@ -0,0 +1,152 @@ +import { mount } from '@vue/test-utils'; +import { GlFilteredSearchToken, GlFilteredSearchTokenSegment } from '@gitlab/ui'; +import MockAdapter from 'axios-mock-adapter'; +import waitForPromises from 'helpers/wait_for_promises'; +import axios from '~/lib/utils/axios_utils'; + +import createFlash from '~/flash'; +import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue'; + +import { + mockMilestoneToken, + mockMilestones, + mockRegularMilestone, + mockEscapedMilestone, +} from '../mock_data'; + +jest.mock('~/flash'); + +const createComponent = ({ + config = mockMilestoneToken, + value = { data: '' }, + active = false, +} = {}) => + mount(MilestoneToken, { + propsData: { + config, + value, + active, + }, + provide: { + portalName: 'fake target', + alignSuggestions: function fakeAlignSuggestions() {}, + }, + stubs: { + Portal: { + template: '<div><slot></slot></div>', + }, + GlFilteredSearchSuggestionList: { + template: '<div></div>', + methods: { + getValue: () => '=', + }, + }, + }, + }); + +describe('MilestoneToken', () => { + let mock; + let wrapper; + + beforeEach(() => { + mock = new MockAdapter(axios); + wrapper = createComponent(); + }); + + afterEach(() => { + mock.restore(); + wrapper.destroy(); + }); + + describe('computed', () => { + beforeEach(async () => { + // Milestone title with spaces is always enclosed in quotations by component. + wrapper = createComponent({ value: { data: `"${mockEscapedMilestone.title}"` } }); + + wrapper.setData({ + milestones: mockMilestones, + }); + + await wrapper.vm.$nextTick(); + }); + + describe('currentValue', () => { + it('returns lowercase string for `value.data`', () => { + expect(wrapper.vm.currentValue).toBe('"5.0 rc1"'); + }); + }); + + describe('activeMilestone', () => { + it('returns object for currently present `value.data`', () => { + expect(wrapper.vm.activeMilestone).toEqual(mockEscapedMilestone); + }); + }); + }); + + describe('methods', () => { + describe('fetchMilestoneBySearchTerm', () => { + it('calls `config.fetchMilestones` with provided searchTerm param', () => { + jest.spyOn(wrapper.vm.config, 'fetchMilestones'); + + wrapper.vm.fetchMilestoneBySearchTerm('foo'); + + expect(wrapper.vm.config.fetchMilestones).toHaveBeenCalledWith('foo'); + }); + + it('sets response to `milestones` when request is successful', () => { + jest.spyOn(wrapper.vm.config, 'fetchMilestones').mockResolvedValue({ + data: mockMilestones, + }); + + wrapper.vm.fetchMilestoneBySearchTerm(); + + return waitForPromises().then(() => { + expect(wrapper.vm.milestones).toEqual(mockMilestones); + }); + }); + + it('calls `createFlash` with flash error message when request fails', () => { + jest.spyOn(wrapper.vm.config, 'fetchMilestones').mockRejectedValue({}); + + wrapper.vm.fetchMilestoneBySearchTerm('foo'); + + return waitForPromises().then(() => { + expect(createFlash).toHaveBeenCalledWith('There was a problem fetching milestones.'); + }); + }); + + it('sets `loading` to false when request completes', () => { + jest.spyOn(wrapper.vm.config, 'fetchMilestones').mockRejectedValue({}); + + wrapper.vm.fetchMilestoneBySearchTerm('foo'); + + return waitForPromises().then(() => { + expect(wrapper.vm.loading).toBe(false); + }); + }); + }); + }); + + describe('template', () => { + beforeEach(async () => { + wrapper = createComponent({ value: { data: `"${mockRegularMilestone.title}"` } }); + + wrapper.setData({ + milestones: mockMilestones, + }); + + await wrapper.vm.$nextTick(); + }); + + it('renders gl-filtered-search-token component', () => { + expect(wrapper.find(GlFilteredSearchToken).exists()).toBe(true); + }); + + it('renders token item when value is selected', () => { + const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); + + expect(tokenSegments).toHaveLength(3); // Milestone, =, '%"4.0"' + expect(tokenSegments.at(2).text()).toBe(`%"${mockRegularMilestone.title}"`); // "4.0 RC1" + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/form/form_footer_actions_spec.js b/spec/frontend/vue_shared/components/form/form_footer_actions_spec.js index 30e16bd12da..361b162b6a0 100644 --- a/spec/frontend/vue_shared/components/form/form_footer_actions_spec.js +++ b/spec/frontend/vue_shared/components/form/form_footer_actions_spec.js @@ -1,5 +1,5 @@ -import FormFooterActions from '~/vue_shared/components/form/form_footer_actions.vue'; import { shallowMount } from '@vue/test-utils'; +import FormFooterActions from '~/vue_shared/components/form/form_footer_actions.vue'; describe('Form Footer Actions', () => { let wrapper; diff --git a/spec/frontend/vue_shared/components/form/title_spec.js b/spec/frontend/vue_shared/components/form/title_spec.js index 38ef1bb3aa7..452f3723e76 100644 --- a/spec/frontend/vue_shared/components/form/title_spec.js +++ b/spec/frontend/vue_shared/components/form/title_spec.js @@ -1,5 +1,5 @@ -import TitleField from '~/vue_shared/components/form/title.vue'; import { shallowMount } from '@vue/test-utils'; +import TitleField from '~/vue_shared/components/form/title.vue'; describe('Title edit field', () => { let wrapper; diff --git a/spec/frontend/vue_shared/components/header_ci_component_spec.js b/spec/frontend/vue_shared/components/header_ci_component_spec.js index 216563165d6..5233a64ce5e 100644 --- a/spec/frontend/vue_shared/components/header_ci_component_spec.js +++ b/spec/frontend/vue_shared/components/header_ci_component_spec.js @@ -35,7 +35,7 @@ describe('Header CI Component', () => { vm.$destroy(); }); - const findActionButtons = () => vm.$el.querySelector('.header-action-buttons'); + const findActionButtons = () => vm.$el.querySelector('[data-testid="headerButtons"]'); describe('render', () => { beforeEach(() => { diff --git a/spec/frontend/vue_shared/components/icon_spec.js b/spec/frontend/vue_shared/components/icon_spec.js index a448953cc8e..16728e1705a 100644 --- a/spec/frontend/vue_shared/components/icon_spec.js +++ b/spec/frontend/vue_shared/components/icon_spec.js @@ -1,8 +1,8 @@ import Vue from 'vue'; import { mount } from '@vue/test-utils'; import mountComponent from 'helpers/vue_mount_component_helper'; -import Icon from '~/vue_shared/components/icon.vue'; import iconsPath from '@gitlab/svgs/dist/icons.svg'; +import Icon from '~/vue_shared/components/icon.vue'; jest.mock('@gitlab/svgs/dist/icons.svg', () => 'testing'); diff --git a/spec/frontend/vue_shared/components/identicon_spec.js b/spec/frontend/vue_shared/components/identicon_spec.js index 53a55dcd6bd..24fc3713e2b 100644 --- a/spec/frontend/vue_shared/components/identicon_spec.js +++ b/spec/frontend/vue_shared/components/identicon_spec.js @@ -25,7 +25,7 @@ describe('Identicon', () => { }); describe('entity id is a number', () => { - beforeEach(createComponent); + beforeEach(() => createComponent()); it('matches snapshot', () => { expect(wrapper.element).toMatchSnapshot(); diff --git a/spec/frontend/vue_shared/components/issuable/issuable_header_warnings_spec.js b/spec/frontend/vue_shared/components/issuable/issuable_header_warnings_spec.js new file mode 100644 index 00000000000..2f910a10bc6 --- /dev/null +++ b/spec/frontend/vue_shared/components/issuable/issuable_header_warnings_spec.js @@ -0,0 +1,73 @@ +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import Vuex from 'vuex'; +import IssuableHeaderWarnings from '~/vue_shared/components/issuable/issuable_header_warnings.vue'; +import createIssueStore from '~/notes/stores'; +import { createStore as createMrStore } from '~/mr_notes/stores'; + +const ISSUABLE_TYPE_ISSUE = 'issue'; +const ISSUABLE_TYPE_MR = 'merge request'; + +const localVue = createLocalVue(); +localVue.use(Vuex); + +describe('IssuableHeaderWarnings', () => { + let wrapper; + let store; + + const findConfidentialIcon = () => wrapper.find('[data-testid="confidential"]'); + const findLockedIcon = () => wrapper.find('[data-testid="locked"]'); + + const renderTestMessage = renders => (renders ? 'renders' : 'does not render'); + + const setLock = locked => { + store.getters.getNoteableData.discussion_locked = locked; + }; + + const setConfidential = confidential => { + store.getters.getNoteableData.confidential = confidential; + }; + + const createComponent = () => { + wrapper = shallowMount(IssuableHeaderWarnings, { store, localVue }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + store = null; + }); + + describe.each` + issuableType + ${ISSUABLE_TYPE_ISSUE} | ${ISSUABLE_TYPE_MR} + `(`when issuableType=$issuableType`, ({ issuableType }) => { + beforeEach(() => { + store = issuableType === ISSUABLE_TYPE_ISSUE ? createIssueStore() : createMrStore(); + createComponent(); + }); + + describe.each` + lockStatus | confidentialStatus + ${true} | ${true} + ${true} | ${false} + ${false} | ${true} + ${false} | ${false} + `( + `when locked=$lockStatus and confidential=$confidentialStatus`, + ({ lockStatus, confidentialStatus }) => { + beforeEach(() => { + setLock(lockStatus); + setConfidential(confidentialStatus); + }); + + it(`${renderTestMessage(lockStatus)} the locked icon`, () => { + expect(findLockedIcon().exists()).toBe(lockStatus); + }); + + it(`${renderTestMessage(confidentialStatus)} the confidential icon`, () => { + expect(findConfidentialIcon().exists()).toBe(confidentialStatus); + }); + }, + ); + }); +}); 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 548d4476c0f..192e33d8b00 100644 --- a/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js +++ b/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js @@ -1,7 +1,7 @@ import { shallowMount } from '@vue/test-utils'; +import { mockAssigneesList } from 'jest/boards/mock_data'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; import IssueAssignees from '~/vue_shared/components/issue/issue_assignees.vue'; -import { mockAssigneesList } from 'jest/boards/mock_data'; const TEST_CSS_CLASSES = 'test-classes'; const TEST_MAX_VISIBLE = 4; @@ -21,6 +21,11 @@ describe('IssueAssigneesComponent', () => { vm = wrapper.vm; }; + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + const findTooltipText = () => wrapper.find('.js-assignee-tooltip').text(); const findAvatars = () => wrapper.findAll(UserAvatarLink); const findOverflowCounter = () => wrapper.find('.avatar-counter'); @@ -123,6 +128,22 @@ describe('IssueAssigneesComponent', () => { it('renders assignee @username', () => { expect(findTooltipText()).toContain('@monserrate.gleichner'); }); + + it('does not render `@` when username not available', () => { + const userName = 'User without username'; + factory({ + assignees: [ + { + name: userName, + }, + ], + }); + + const tooltipText = findTooltipText(); + + expect(tooltipText).toContain(userName); + expect(tooltipText).not.toContain('@'); + }); }); }); }); diff --git a/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js b/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js index 69d8c1a5918..b72f78c4f60 100644 --- a/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js +++ b/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js @@ -1,11 +1,10 @@ import Vue from 'vue'; import { shallowMount } from '@vue/test-utils'; +import { mockMilestone } from 'jest/boards/mock_data'; import IssueMilestone from '~/vue_shared/components/issue/issue_milestone.vue'; import Icon from '~/vue_shared/components/icon.vue'; -import { mockMilestone } from 'jest/boards/mock_data'; - const createComponent = (milestone = mockMilestone) => { const Component = Vue.extend(IssueMilestone); diff --git a/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js b/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js index fe9a5156539..fb9487d0bf8 100644 --- a/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js +++ b/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js @@ -1,9 +1,9 @@ -import Vue from 'vue'; import { mount } from '@vue/test-utils'; +import { TEST_HOST } from 'jest/helpers/test_constants'; import { formatDate } from '~/lib/utils/datetime_utility'; import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue'; +import IssueDueDate from '~/boards/components/issue_due_date.vue'; import { defaultAssignees, defaultMilestone } from './related_issuable_mock_data'; -import { TEST_HOST } from 'jest/helpers/test_constants'; describe('RelatedIssuableItem', () => { let wrapper; @@ -71,85 +71,65 @@ describe('RelatedIssuableItem', () => { }); describe('token state', () => { - let tokenState; + const tokenState = () => wrapper.find({ ref: 'iconElementXL' }); - beforeEach(done => { + beforeEach(() => { wrapper.setProps({ state: 'opened' }); - - Vue.nextTick(() => { - tokenState = wrapper.find('.issue-token-state-icon-open'); - - done(); - }); }); it('renders if hasState', () => { - expect(tokenState.exists()).toBe(true); + expect(tokenState().exists()).toBe(true); }); it('renders state title', () => { - const stateTitle = tokenState.attributes('title'); + const stateTitle = tokenState().attributes('title'); const formattedCreateDate = formatDate(props.createdAt); expect(stateTitle).toContain('<span class="bold">Opened</span>'); - expect(stateTitle).toContain(`<span class="text-tertiary">${formattedCreateDate}</span>`); }); it('renders aria label', () => { - expect(tokenState.attributes('aria-label')).toEqual('opened'); + expect(tokenState().attributes('aria-label')).toEqual('opened'); }); it('renders open icon when open state', () => { - expect(tokenState.classes('issue-token-state-icon-open')).toBe(true); + expect(tokenState().classes('issue-token-state-icon-open')).toBe(true); }); - it('renders close icon when close state', done => { + it('renders close icon when close state', async () => { wrapper.setProps({ state: 'closed', closedAt: '2018-12-01T00:00:00.00Z', }); + await wrapper.vm.$nextTick(); - Vue.nextTick(() => { - expect(tokenState.classes('issue-token-state-icon-closed')).toBe(true); - - done(); - }); + expect(tokenState().classes('issue-token-state-icon-closed')).toBe(true); }); }); describe('token metadata', () => { - let tokenMetadata; - - beforeEach(done => { - Vue.nextTick(() => { - tokenMetadata = wrapper.find('.item-meta'); - - done(); - }); - }); + const tokenMetadata = () => wrapper.find('.item-meta'); it('renders item path and ID', () => { - const pathAndID = tokenMetadata.find('.item-path-id').text(); + const pathAndID = tokenMetadata() + .find('.item-path-id') + .text(); expect(pathAndID).toContain('gitlab-org/gitlab-test'); expect(pathAndID).toContain('#1'); }); it('renders milestone icon and name', () => { - const milestoneIcon = tokenMetadata.find('.item-milestone svg use'); - const milestoneTitle = tokenMetadata.find('.item-milestone .milestone-title'); + const milestoneIcon = tokenMetadata().find('.item-milestone svg use'); + const milestoneTitle = tokenMetadata().find('.item-milestone .milestone-title'); expect(milestoneIcon.attributes('href')).toContain('clock'); expect(milestoneTitle.text()).toContain('Milestone title'); }); - it('renders due date component', () => { - expect(tokenMetadata.find('.js-due-date-slot').exists()).toBe(true); - }); - - it('renders weight component', () => { - expect(tokenMetadata.find('.js-weight-slot').exists()).toBe(true); + it('renders due date component with correct due date', () => { + expect(wrapper.find(IssueDueDate).props('date')).toBe(props.dueDate); }); }); @@ -163,40 +143,30 @@ describe('RelatedIssuableItem', () => { }); describe('remove button', () => { - let removeBtn; + const removeButton = () => wrapper.find({ ref: 'removeButton' }); - beforeEach(done => { + beforeEach(() => { wrapper.setProps({ canRemove: true }); - Vue.nextTick(() => { - removeBtn = wrapper.find({ ref: 'removeButton' }); - - done(); - }); }); it('renders if canRemove', () => { - expect(removeBtn.exists()).toBe(true); + expect(removeButton().exists()).toBe(true); }); - it('renders disabled button when removeDisabled', done => { - wrapper.vm.removeDisabled = true; - - Vue.nextTick(() => { - expect(removeBtn.attributes('disabled')).toEqual('disabled'); + it('renders disabled button when removeDisabled', async () => { + wrapper.setData({ removeDisabled: true }); + await wrapper.vm.$nextTick(); - done(); - }); + expect(removeButton().attributes('disabled')).toEqual('disabled'); }); - it('triggers onRemoveRequest when clicked', () => { - removeBtn.trigger('click'); + it('triggers onRemoveRequest when clicked', async () => { + removeButton().trigger('click'); + await wrapper.vm.$nextTick(); + const { relatedIssueRemoveRequest } = wrapper.emitted(); - return wrapper.vm.$nextTick().then(() => { - const { relatedIssueRemoveRequest } = wrapper.emitted(); - - expect(relatedIssueRemoveRequest.length).toBe(1); - expect(relatedIssueRemoveRequest[0]).toEqual([props.idKey]); - }); + expect(relatedIssueRemoveRequest.length).toBe(1); + expect(relatedIssueRemoveRequest[0]).toEqual([props.idKey]); }); }); }); diff --git a/spec/frontend/vue_shared/components/markdown/field_spec.js b/spec/frontend/vue_shared/components/markdown/field_spec.js index 74be5f8230e..3da0a35f05a 100644 --- a/spec/frontend/vue_shared/components/markdown/field_spec.js +++ b/spec/frontend/vue_shared/components/markdown/field_spec.js @@ -1,8 +1,8 @@ import { mount } from '@vue/test-utils'; -import fieldComponent from '~/vue_shared/components/markdown/field.vue'; import { TEST_HOST, FIXTURES_PATH } from 'spec/test_constants'; import AxiosMockAdapter from 'axios-mock-adapter'; import $ from 'jquery'; +import fieldComponent from '~/vue_shared/components/markdown/field.vue'; import axios from '~/lib/utils/axios_utils'; const markdownPreviewPath = `${TEST_HOST}/preview`; diff --git a/spec/frontend/vue_shared/components/rich_content_editor/editor_service_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/editor_service_spec.js index 78f27c9948b..16f60b5ff21 100644 --- a/spec/frontend/vue_shared/components/rich_content_editor/editor_service_spec.js +++ b/spec/frontend/vue_shared/components/rich_content_editor/editor_service_spec.js @@ -5,10 +5,13 @@ import { registerHTMLToMarkdownRenderer, addImage, getMarkdown, + getEditorOptions, } from '~/vue_shared/components/rich_content_editor/services/editor_service'; import buildHTMLToMarkdownRenderer from '~/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer'; +import buildCustomRenderer from '~/vue_shared/components/rich_content_editor/services/build_custom_renderer'; jest.mock('~/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer'); +jest.mock('~/vue_shared/components/rich_content_editor/services/build_custom_renderer'); describe('Editor Service', () => { let mockInstance; @@ -120,4 +123,25 @@ describe('Editor Service', () => { expect(mockInstance.toMarkOptions.renderer).toBe(extendedRenderer); }); }); + + describe('getEditorOptions', () => { + const externalOptions = { + customRenderers: {}, + }; + const renderer = {}; + + beforeEach(() => { + buildCustomRenderer.mockReturnValueOnce(renderer); + }); + + it('generates a configuration object with a custom HTML renderer and toolbarItems', () => { + expect(getEditorOptions()).toHaveProp('customHTMLRenderer', renderer); + expect(getEditorOptions()).toHaveProp('toolbarItems'); + }); + + it('passes external renderers to the buildCustomRenderers function', () => { + getEditorOptions(externalOptions); + expect(buildCustomRenderer).toHaveBeenCalledWith(externalOptions.customRenderers); + }); + }); }); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_integration_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_integration_spec.js new file mode 100644 index 00000000000..b9b93b274d2 --- /dev/null +++ b/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_integration_spec.js @@ -0,0 +1,69 @@ +import Editor from '@toast-ui/editor'; +import { registerHTMLToMarkdownRenderer } from '~/vue_shared/components/rich_content_editor/services/editor_service'; +import buildMarkdownToHTMLRenderer from '~/vue_shared/components/rich_content_editor/services/build_custom_renderer'; + +describe('vue_shared/components/rich_content_editor', () => { + let editor; + + const buildEditor = () => { + editor = new Editor({ + el: document.body, + customHTMLRenderer: buildMarkdownToHTMLRenderer(), + }); + + registerHTMLToMarkdownRenderer(editor); + }; + + beforeEach(() => { + buildEditor(); + }); + + describe('HTML to Markdown', () => { + it('uses "-" character list marker in unordered lists', () => { + editor.setHtml('<ul><li>List item 1</li><li>List item 2</li></ul>'); + + const markdown = editor.getMarkdown(); + + expect(markdown).toBe('- List item 1\n- List item 2'); + }); + + it('does not increment the list marker in ordered lists', () => { + editor.setHtml('<ol><li>List item 1</li><li>List item 2</li></ol>'); + + const markdown = editor.getMarkdown(); + + expect(markdown).toBe('1. List item 1\n1. List item 2'); + }); + + it('indents lists using four spaces', () => { + editor.setHtml('<ul><li>List item 1</li><ul><li>List item 2</li></ul></ul>'); + + const markdown = editor.getMarkdown(); + + expect(markdown).toBe('- List item 1\n - List item 2'); + }); + + it('uses * for strong and _ for emphasis text', () => { + editor.setHtml('<strong>strong text</strong><i>emphasis text</i>'); + + const markdown = editor.getMarkdown(); + + expect(markdown).toBe('**strong text**_emphasis text_'); + }); + }); + + describe('Markdown to HTML', () => { + it.each` + input | output + ${'markdown with _emphasized\ntext_'} | ${'<p>markdown with <em>emphasized text</em></p>\n'} + ${'markdown with **strong\ntext**'} | ${'<p>markdown with <strong>strong text</strong></p>\n'} + `( + 'does not transform softbreaks inside (_) and strong (**) nodes into <br/> tags', + ({ input, output }) => { + editor.setMarkdown(input); + + expect(editor.getHtml()).toBe(output); + }, + ); + }); +}); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js index b6ff6aa767c..3d54db7fe5c 100644 --- a/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js +++ b/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js @@ -2,7 +2,6 @@ import { shallowMount } from '@vue/test-utils'; import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue'; import AddImageModal from '~/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal.vue'; import { - EDITOR_OPTIONS, EDITOR_TYPES, EDITOR_HEIGHT, EDITOR_PREVIEW_STYLE, @@ -14,6 +13,7 @@ import { removeCustomEventListener, addImage, registerHTMLToMarkdownRenderer, + getEditorOptions, } from '~/vue_shared/components/rich_content_editor/services/editor_service'; jest.mock('~/vue_shared/components/rich_content_editor/services/editor_service', () => ({ @@ -22,6 +22,7 @@ jest.mock('~/vue_shared/components/rich_content_editor/services/editor_service', removeCustomEventListener: jest.fn(), addImage: jest.fn(), registerHTMLToMarkdownRenderer: jest.fn(), + getEditorOptions: jest.fn(), })); describe('Rich Content Editor', () => { @@ -32,13 +33,25 @@ describe('Rich Content Editor', () => { const findEditor = () => wrapper.find({ ref: 'editor' }); const findAddImageModal = () => wrapper.find(AddImageModal); - beforeEach(() => { + const buildWrapper = () => { wrapper = shallowMount(RichContentEditor, { propsData: { content, imageRoot }, }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; }); describe('when content is loaded', () => { + const editorOptions = {}; + + beforeEach(() => { + getEditorOptions.mockReturnValueOnce(editorOptions); + buildWrapper(); + }); + it('renders an editor', () => { expect(findEditor().exists()).toBe(true); }); @@ -47,8 +60,8 @@ describe('Rich Content Editor', () => { expect(findEditor().props().initialValue).toBe(content); }); - it('provides the correct editor options', () => { - expect(findEditor().props().options).toEqual(EDITOR_OPTIONS); + it('provides options generated by the getEditorOptions service', () => { + expect(findEditor().props().options).toBe(editorOptions); }); it('has the correct preview style', () => { @@ -65,6 +78,10 @@ describe('Rich Content Editor', () => { }); describe('when content is changed', () => { + beforeEach(() => { + buildWrapper(); + }); + it('emits an input event with the changed content', () => { const changedMarkdown = '## Changed Markdown'; const getMarkdownMock = jest.fn().mockReturnValueOnce(changedMarkdown); @@ -77,6 +94,10 @@ describe('Rich Content Editor', () => { }); describe('when content is reset', () => { + beforeEach(() => { + buildWrapper(); + }); + it('should reset the content via setMarkdown', () => { const newContent = 'Just the body content excluding the front matter for example'; const mockInstance = { invoke: jest.fn() }; @@ -89,35 +110,33 @@ describe('Rich Content Editor', () => { }); describe('when editor is loaded', () => { - let mockEditorApi; - beforeEach(() => { - mockEditorApi = { eventManager: { addEventType: jest.fn(), listen: jest.fn() } }; - findEditor().vm.$emit('load', mockEditorApi); + buildWrapper(); }); it('adds the CUSTOM_EVENTS.openAddImageModal custom event listener', () => { expect(addCustomEventListener).toHaveBeenCalledWith( - mockEditorApi, + wrapper.vm.editorApi, CUSTOM_EVENTS.openAddImageModal, wrapper.vm.onOpenAddImageModal, ); }); it('registers HTML to markdown renderer', () => { - expect(registerHTMLToMarkdownRenderer).toHaveBeenCalledWith(mockEditorApi); + expect(registerHTMLToMarkdownRenderer).toHaveBeenCalledWith(wrapper.vm.editorApi); }); }); describe('when editor is destroyed', () => { - it('removes the CUSTOM_EVENTS.openAddImageModal custom event listener', () => { - const mockEditorApi = { eventManager: { removeEventHandler: jest.fn() } }; + beforeEach(() => { + buildWrapper(); + }); - wrapper.vm.editorApi = mockEditorApi; + it('removes the CUSTOM_EVENTS.openAddImageModal custom event listener', () => { wrapper.vm.$destroy(); expect(removeCustomEventListener).toHaveBeenCalledWith( - mockEditorApi, + wrapper.vm.editorApi, CUSTOM_EVENTS.openAddImageModal, wrapper.vm.onOpenAddImageModal, ); @@ -125,6 +144,10 @@ describe('Rich Content Editor', () => { }); describe('add image modal', () => { + beforeEach(() => { + buildWrapper(); + }); + it('renders an addImageModal component', () => { expect(findAddImageModal().exists()).toBe(true); }); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer_spec.js index 0e8610a22f5..a90d3528d60 100644 --- a/spec/frontend/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer_spec.js +++ b/spec/frontend/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer_spec.js @@ -47,4 +47,87 @@ describe('HTMLToMarkdownRenderer', () => { expect(baseRenderer.convert).toHaveBeenCalledWith(NODE, list); }); }); + + describe('UL LI visitor', () => { + it.each` + listItem | unorderedListBulletChar | result | bulletChar + ${'* list item'} | ${undefined} | ${'- list item'} | ${'default'} + ${' - list item'} | ${'*'} | ${' * list item'} | ${'*'} + ${' * list item'} | ${'-'} | ${' - list item'} | ${'-'} + `( + 'uses $bulletChar bullet char in unordered list items when $unorderedListBulletChar is set in config', + ({ listItem, unorderedListBulletChar, result }) => { + htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer, { + unorderedListBulletChar, + }); + baseRenderer.convert.mockReturnValueOnce(listItem); + + expect(htmlToMarkdownRenderer['UL LI'](NODE, listItem)).toBe(result); + expect(baseRenderer.convert).toHaveBeenCalledWith(NODE, listItem); + }, + ); + }); + + describe('OL LI visitor', () => { + it.each` + listItem | result | incrementListMarker | action + ${'2. list item'} | ${'1. list item'} | ${false} | ${'increments'} + ${' 3. list item'} | ${' 1. list item'} | ${false} | ${'increments'} + ${' 123. list item'} | ${' 1. list item'} | ${false} | ${'increments'} + ${'3. list item'} | ${'3. list item'} | ${true} | ${'does not increment'} + `( + '$action a list item counter when incrementListMaker is $incrementListMarker', + ({ listItem, result, incrementListMarker }) => { + const subContent = null; + + htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer, { + incrementListMarker, + }); + baseRenderer.convert.mockReturnValueOnce(listItem); + + expect(htmlToMarkdownRenderer['OL LI'](NODE, subContent)).toBe(result); + expect(baseRenderer.convert).toHaveBeenCalledWith(NODE, subContent); + }, + ); + }); + + describe('STRONG, B visitor', () => { + it.each` + input | strongCharacter | result + ${'**strong text**'} | ${'_'} | ${'__strong text__'} + ${'__strong text__'} | ${'*'} | ${'**strong text**'} + `( + 'converts $input to $result when strong character is $strongCharacter', + ({ input, strongCharacter, result }) => { + htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer, { + strong: strongCharacter, + }); + + baseRenderer.convert.mockReturnValueOnce(input); + + expect(htmlToMarkdownRenderer['STRONG, B'](NODE, input)).toBe(result); + expect(baseRenderer.convert).toHaveBeenCalledWith(NODE, input); + }, + ); + }); + + describe('EM, I visitor', () => { + it.each` + input | emphasisCharacter | result + ${'*strong text*'} | ${'_'} | ${'_strong text_'} + ${'_strong text_'} | ${'*'} | ${'*strong text*'} + `( + 'converts $input to $result when emphasis character is $emphasisCharacter', + ({ input, emphasisCharacter, result }) => { + htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer, { + emphasis: emphasisCharacter, + }); + + baseRenderer.convert.mockReturnValueOnce(input); + + expect(htmlToMarkdownRenderer['EM, I'](NODE, input)).toBe(result); + expect(baseRenderer.convert).toHaveBeenCalledWith(NODE, input); + }, + ); + }); }); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token_spec.js index 18dff0a39bb..7a7e3055520 100644 --- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token_spec.js +++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token_spec.js @@ -3,7 +3,7 @@ import { buildUneditableOpenTokens, buildUneditableCloseToken, buildUneditableCloseTokens, - buildUneditableTokens, + buildUneditableBlockTokens, buildUneditableInlineTokens, buildUneditableHtmlAsTextTokens, } from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token'; @@ -51,9 +51,9 @@ describe('Build Uneditable Token renderer helper', () => { }); }); - describe('buildUneditableTokens', () => { + describe('buildUneditableBlockTokens', () => { it('returns a 3-item array of tokens with the originToken wrapped in the middle of block tokens', () => { - const result = buildUneditableTokens(originToken); + const result = buildUneditableBlockTokens(originToken); expect(result).toHaveLength(3); expect(result).toStrictEqual(uneditableTokens); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_embedded_ruby_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_embedded_ruby_spec.js index b723ee8c8a0..0c59d9f569b 100644 --- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_embedded_ruby_spec.js +++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_embedded_ruby_spec.js @@ -1,5 +1,5 @@ import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_embedded_ruby_text'; -import { buildUneditableTokens } from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token'; +import { renderUneditableLeaf } from '~/vue_shared/components/rich_content_editor/services/renderers/render_utils'; import { buildMockTextNode, normalTextNode } from './mock_data'; @@ -17,14 +17,8 @@ describe('Render Embedded Ruby Text renderer', () => { }); describe('render', () => { - const origin = jest.fn(); - - it('should return uneditable tokens', () => { - const context = { origin }; - - expect(renderer.render(embeddedRubyTextNode, context)).toStrictEqual( - buildUneditableTokens(origin()), - ); + it('should delegate rendering to the renderUneditableLeaf util', () => { + expect(renderer.render).toBe(renderUneditableLeaf); }); }); }); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph_spec.js index 320589e4de3..f4a06b91a10 100644 --- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph_spec.js +++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph_spec.js @@ -1,8 +1,5 @@ import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph'; -import { - buildUneditableOpenTokens, - buildUneditableCloseToken, -} from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token'; +import { renderUneditableBranch } from '~/vue_shared/components/rich_content_editor/services/renderers/render_utils'; import { buildMockTextNode } from './mock_data'; @@ -40,26 +37,8 @@ describe('Render Identifier Paragraph renderer', () => { }); describe('render', () => { - let origin; - - beforeEach(() => { - origin = jest.fn(); - }); - - it('should return uneditable open tokens when entering', () => { - const context = { entering: true, origin }; - - expect(renderer.render(identifierParagraphNode, context)).toStrictEqual( - buildUneditableOpenTokens(origin()), - ); - }); - - it('should return an uneditable close tokens when exiting', () => { - const context = { entering: false, origin }; - - expect(renderer.render(identifierParagraphNode, context)).toStrictEqual( - buildUneditableCloseToken(origin()), - ); + it('should delegate rendering to the renderUneditableBranch util', () => { + expect(renderer.render).toBe(renderUneditableBranch); }); }); }); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list_spec.js index e60bf1c8c92..7d427108ba6 100644 --- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list_spec.js +++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list_spec.js @@ -1,8 +1,5 @@ import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list'; -import { - buildUneditableOpenTokens, - buildUneditableCloseToken, -} from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token'; +import { renderUneditableBranch } from '~/vue_shared/components/rich_content_editor/services/renderers/render_utils'; import { buildMockTextNode } from './mock_data'; @@ -34,22 +31,8 @@ describe('Render Kramdown List renderer', () => { }); describe('render', () => { - const origin = jest.fn(); - - it('should return uneditable open tokens when entering', () => { - const context = { entering: true, origin }; - - expect(renderer.render(kramdownListNode, context)).toStrictEqual( - buildUneditableOpenTokens(origin()), - ); - }); - - it('should return an uneditable close tokens when exiting', () => { - const context = { entering: false, origin }; - - expect(renderer.render(kramdownListNode, context)).toStrictEqual( - buildUneditableCloseToken(origin()), - ); + it('should delegate rendering to the renderUneditableBranch util', () => { + expect(renderer.render).toBe(renderUneditableBranch); }); }); }); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text_spec.js index 97ff9794e69..1d2d152ffc3 100644 --- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text_spec.js +++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text_spec.js @@ -1,5 +1,5 @@ import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text'; -import { buildUneditableTokens } from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token'; +import { renderUneditableLeaf } from '~/vue_shared/components/rich_content_editor/services/renderers/render_utils'; import { buildMockTextNode, normalTextNode } from './mock_data'; @@ -17,14 +17,8 @@ describe('Render Kramdown Text renderer', () => { }); describe('render', () => { - const origin = jest.fn(); - - it('should return uneditable tokens', () => { - const context = { origin }; - - expect(renderer.render(kramdownTextNode, context)).toStrictEqual( - buildUneditableTokens(origin()), - ); + it('should delegate rendering to the renderUneditableLeaf util', () => { + expect(renderer.render).toBe(renderUneditableLeaf); }); }); }); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_softbreak_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_softbreak_spec.js new file mode 100644 index 00000000000..3c3d2354cb9 --- /dev/null +++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_softbreak_spec.js @@ -0,0 +1,23 @@ +import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_softbreak'; + +describe('Render softbreak renderer', () => { + describe('canRender', () => { + it.each` + node | parentType | result + ${{ parent: { type: 'emph' } }} | ${'emph'} | ${true} + ${{ parent: { type: 'strong' } }} | ${'strong'} | ${true} + ${{ parent: { type: 'paragraph' } }} | ${'paragraph'} | ${false} + `('returns $result when node parent type is $parentType ', ({ node, result }) => { + expect(renderer.canRender(node)).toBe(result); + }); + }); + + describe('render', () => { + it('returns text node with a break line', () => { + expect(renderer.render()).toEqual({ + type: 'text', + content: ' ', + }); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_utils_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_utils_spec.js new file mode 100644 index 00000000000..92435b3e4e3 --- /dev/null +++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_utils_spec.js @@ -0,0 +1,44 @@ +import { + renderUneditableLeaf, + renderUneditableBranch, +} from '~/vue_shared/components/rich_content_editor/services/renderers/render_utils'; + +import { + buildUneditableBlockTokens, + buildUneditableOpenTokens, +} from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token'; + +import { originToken, uneditableCloseToken } from './mock_data'; + +describe('Render utils', () => { + describe('renderUneditableLeaf', () => { + it('should return uneditable block tokens around an origin token', () => { + const context = { origin: jest.fn().mockReturnValueOnce(originToken) }; + const result = renderUneditableLeaf({}, context); + + expect(result).toStrictEqual(buildUneditableBlockTokens(originToken)); + }); + }); + + describe('renderUneditableBranch', () => { + let origin; + + beforeEach(() => { + origin = jest.fn().mockReturnValueOnce(originToken); + }); + + it('should return uneditable block open token followed by the origin token when entering', () => { + const context = { entering: true, origin }; + const result = renderUneditableBranch({}, context); + + expect(result).toStrictEqual(buildUneditableOpenTokens(originToken)); + }); + + it('should return uneditable block closing token when exiting', () => { + const context = { entering: false, origin }; + const result = renderUneditableBranch({}, context); + + expect(result).toStrictEqual(uneditableCloseToken); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js index c33cffb421d..53e8a0e1278 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js @@ -1,6 +1,6 @@ import { mount } from '@vue/test-utils'; -import DropdownValueComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value.vue'; import { GlLabel } from '@gitlab/ui'; +import DropdownValueComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value.vue'; import { mockConfig, mockLabels } from './mock_data'; diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js index 68c9d26bb1a..cb758797c63 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js @@ -41,23 +41,20 @@ describe('DropdownButton', () => { describe('methods', () => { describe('handleButtonClick', () => { it.each` - variant - ${'standalone'} - ${'embedded'} + variant | expectPropagationStopped + ${'standalone'} | ${true} + ${'embedded'} | ${false} `( - 'toggles dropdown content and stops event propagation when `state.variant` is "$variant"', - ({ variant }) => { + 'toggles dropdown content and handles event propagation when `state.variant` is "$variant"', + ({ variant, expectPropagationStopped }) => { const event = { stopPropagation: jest.fn() }; - wrapper = createComponent({ - ...mockConfig, - variant, - }); + wrapper = createComponent({ ...mockConfig, variant }); findDropdownButton().vm.$emit('click', event); expect(store.state.showDropdownContents).toBe(true); - expect(event.stopPropagation).toHaveBeenCalled(); + expect(event.stopPropagation).toHaveBeenCalledTimes(expectPropagationStopped ? 1 : 0); }, ); }); 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 9b01e0b9637..589be0ad7a4 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 @@ -17,53 +17,47 @@ import { mockConfig, mockLabels, mockRegularLabel } from './mock_data'; const localVue = createLocalVue(); localVue.use(Vuex); -const createComponent = (initialState = mockConfig) => { - const store = new Vuex.Store({ - getters, - mutations, - state: { - ...defaultState(), - footerCreateLabelTitle: 'Create label', - footerManageLabelTitle: 'Manage labels', - }, - actions: { - ...actions, - fetchLabels: jest.fn(), - }, - }); - - store.dispatch('setInitialState', initialState); - store.dispatch('receiveLabelsSuccess', mockLabels); - - return shallowMount(DropdownContentsLabelsView, { - localVue, - store, - }); -}; - describe('DropdownContentsLabelsView', () => { let wrapper; - let wrapperStandalone; - let wrapperEmbedded; - beforeEach(() => { - wrapper = createComponent(); - wrapperStandalone = createComponent({ - ...mockConfig, - variant: 'standalone', + const createComponent = (initialState = mockConfig) => { + const store = new Vuex.Store({ + getters, + mutations, + state: { + ...defaultState(), + footerCreateLabelTitle: 'Create label', + footerManageLabelTitle: 'Manage labels', + }, + actions: { + ...actions, + fetchLabels: jest.fn(), + }, }); - wrapperEmbedded = createComponent({ - ...mockConfig, - variant: 'embedded', + + store.dispatch('setInitialState', initialState); + store.dispatch('receiveLabelsSuccess', mockLabels); + + wrapper = shallowMount(DropdownContentsLabelsView, { + localVue, + store, }); + }; + + beforeEach(() => { + createComponent(); }); afterEach(() => { wrapper.destroy(); - wrapperStandalone.destroy(); - wrapperEmbedded.destroy(); + wrapper = null; }); + const findDropdownContent = () => wrapper.find('[data-testid="dropdown-content"]'); + const findDropdownTitle = () => wrapper.find('[data-testid="dropdown-title"]'); + const findDropdownFooter = () => wrapper.find('[data-testid="dropdown-footer"]'); + const findLoadingIcon = () => wrapper.find(GlLoadingIcon); + describe('computed', () => { describe('visibleLabels', () => { it('returns matching labels filtered with `searchKey`', () => { @@ -83,6 +77,24 @@ describe('DropdownContentsLabelsView', () => { expect(wrapper.vm.visibleLabels.length).toBe(mockLabels.length); }); }); + + describe('showListContainer', () => { + it.each` + variant | loading | showList + ${'sidebar'} | ${false} | ${true} + ${'sidebar'} | ${true} | ${false} + ${'not-sidebar'} | ${true} | ${true} + ${'not-sidebar'} | ${false} | ${true} + `( + 'returns $showList if `state.variant` is "$variant" and `labelsFetchInProgress` is $loading', + ({ variant, loading, showList }) => { + createComponent({ ...mockConfig, variant }); + wrapper.vm.$store.state.labelsFetchInProgress = loading; + + expect(wrapper.vm.showListContainer).toBe(showList); + }, + ); + }); }); describe('methods', () => { @@ -199,7 +211,7 @@ describe('DropdownContentsLabelsView', () => { wrapper.vm.$store.dispatch('requestLabels'); return wrapper.vm.$nextTick(() => { - const loadingIconEl = wrapper.find(GlLoadingIcon); + const loadingIconEl = findLoadingIcon(); expect(loadingIconEl.exists()).toBe(true); expect(loadingIconEl.attributes('class')).toContain('labels-fetch-loading'); @@ -207,22 +219,24 @@ describe('DropdownContentsLabelsView', () => { }); it('renders dropdown title element', () => { - const titleEl = wrapper.find('.dropdown-title > span'); + const titleEl = findDropdownTitle(); expect(titleEl.exists()).toBe(true); expect(titleEl.text()).toBe('Assign labels'); }); it('does not render dropdown title element when `state.variant` is "standalone"', () => { - expect(wrapperStandalone.find('.dropdown-title').exists()).toBe(false); + createComponent({ ...mockConfig, variant: 'standalone' }); + expect(findDropdownTitle().exists()).toBe(false); }); it('renders dropdown title element when `state.variant` is "embedded"', () => { - expect(wrapperEmbedded.find('.dropdown-title').exists()).toBe(true); + createComponent({ ...mockConfig, variant: 'embedded' }); + expect(findDropdownTitle().exists()).toBe(true); }); it('renders dropdown close button element', () => { - const closeButtonEl = wrapper.find('.dropdown-title').find(GlButton); + const closeButtonEl = findDropdownTitle().find(GlButton); expect(closeButtonEl.exists()).toBe(true); expect(closeButtonEl.props('icon')).toBe('close'); @@ -249,8 +263,7 @@ describe('DropdownContentsLabelsView', () => { }); return wrapper.vm.$nextTick(() => { - const labelsEl = wrapper.findAll('.dropdown-content li'); - const labelItemEl = labelsEl.at(0).find(LabelItem); + const labelItemEl = findDropdownContent().find(LabelItem); expect(labelItemEl.props('highlight')).toBe(true); }); @@ -262,22 +275,28 @@ describe('DropdownContentsLabelsView', () => { }); return wrapper.vm.$nextTick(() => { - const noMatchEl = wrapper.find('.dropdown-content li'); + const noMatchEl = findDropdownContent().find('li'); expect(noMatchEl.isVisible()).toBe(true); expect(noMatchEl.text()).toContain('No matching results'); }); }); + it('renders empty content while loading', () => { + wrapper.vm.$store.state.labelsFetchInProgress = true; + + return wrapper.vm.$nextTick(() => { + const dropdownContent = findDropdownContent(); + + expect(dropdownContent.exists()).toBe(true); + expect(dropdownContent.isVisible()).toBe(false); + }); + }); + it('renders footer list items', () => { - const createLabelLink = wrapper - .find('.dropdown-footer') - .findAll(GlLink) - .at(0); - const manageLabelsLink = wrapper - .find('.dropdown-footer') - .findAll(GlLink) - .at(1); + const footerLinks = findDropdownFooter().findAll(GlLink); + const createLabelLink = footerLinks.at(0); + const manageLabelsLink = footerLinks.at(1); expect(createLabelLink.exists()).toBe(true); expect(createLabelLink.text()).toBe('Create label'); @@ -289,8 +308,7 @@ describe('DropdownContentsLabelsView', () => { wrapper.vm.$store.state.allowLabelCreate = false; return wrapper.vm.$nextTick(() => { - const createLabelLink = wrapper - .find('.dropdown-footer') + const createLabelLink = findDropdownFooter() .findAll(GlLink) .at(0); @@ -299,11 +317,12 @@ describe('DropdownContentsLabelsView', () => { }); it('does not render footer list items when `state.variant` is "standalone"', () => { - expect(wrapperStandalone.find('.dropdown-footer').exists()).toBe(false); + createComponent({ ...mockConfig, variant: 'standalone' }); + expect(findDropdownFooter().exists()).toBe(false); }); it('renders footer list items when `state.variant` is "embedded"', () => { - expect(wrapperEmbedded.find('.dropdown-footer').exists()).toBe(true); + expect(findDropdownFooter().exists()).toBe(true); }); }); }); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_spec.js index bb462acf11c..97946993857 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_spec.js @@ -10,12 +10,13 @@ import { mockConfig } from './mock_data'; const localVue = createLocalVue(); localVue.use(Vuex); -const createComponent = (initialState = mockConfig) => { +const createComponent = (initialState = mockConfig, propsData = {}) => { const store = new Vuex.Store(labelsSelectModule()); store.dispatch('setInitialState', initialState); return shallowMount(DropdownContents, { + propsData, localVue, store, }); @@ -47,8 +48,15 @@ describe('DropdownContent', () => { }); describe('template', () => { - it('renders component container element with class `labels-select-dropdown-contents`', () => { + it('renders component container element with class `labels-select-dropdown-contents` and no styles', () => { expect(wrapper.attributes('class')).toContain('labels-select-dropdown-contents'); + expect(wrapper.attributes('style')).toBe(undefined); + }); + + it('renders component container element with styles when `renderOnTop` is true', () => { + wrapper = createComponent(mockConfig, { renderOnTop: true }); + + expect(wrapper.attributes('style')).toContain('bottom: 100%'); }); }); }); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_title_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_title_spec.js index 0717fd829a0..c1d9be7393c 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_title_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_title_spec.js @@ -1,7 +1,7 @@ import Vuex from 'vuex'; import { shallowMount, createLocalVue } from '@vue/test-utils'; -import { GlDeprecatedButton, GlLoadingIcon } from '@gitlab/ui'; +import { GlButton, GlLoadingIcon } from '@gitlab/ui'; import DropdownTitle from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue'; import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store'; @@ -42,7 +42,7 @@ describe('DropdownTitle', () => { }); it('renders edit link', () => { - const editBtnEl = wrapper.find(GlDeprecatedButton); + const editBtnEl = wrapper.find(GlButton); expect(editBtnEl.exists()).toBe(true); expect(editBtnEl.text()).toBe('Edit'); 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 6e97b046be2..a1e0db4d29e 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 @@ -9,9 +9,14 @@ import DropdownButton from '~/vue_shared/components/sidebar/labels_select_vue/dr import DropdownContents from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue'; import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store'; +import { isInViewport } from '~/lib/utils/common_utils'; import { mockConfig } from './mock_data'; +jest.mock('~/lib/utils/common_utils', () => ({ + isInViewport: jest.fn().mockReturnValue(true), +})); + const localVue = createLocalVue(); localVue.use(Vuex); @@ -21,6 +26,9 @@ const createComponent = (config = mockConfig, slots = {}) => slots, store: new Vuex.Store(labelsSelectModule()), propsData: config, + stubs: { + 'dropdown-contents': DropdownContents, + }, }); describe('LabelsSelectRoot', () => { @@ -144,5 +152,42 @@ describe('LabelsSelectRoot', () => { expect(wrapper.find(DropdownContents).exists()).toBe(true); }); }); + + describe('sets content direction based on viewport', () => { + it('does not set direction when `state.variant` is not "embedded"', () => { + wrapper.vm.$store.dispatch('toggleDropdownContents'); + + wrapper.vm.setContentIsOnViewport(wrapper.vm.$store.state); + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.find(DropdownContents).props('renderOnTop')).toBe(false); + }); + }); + + describe('when `state.variant` is "embedded"', () => { + beforeEach(() => { + wrapper = createComponent({ ...mockConfig, variant: 'embedded' }); + wrapper.vm.$store.dispatch('toggleDropdownContents'); + }); + + it('set direction when out of viewport', () => { + isInViewport.mockImplementation(() => false); + wrapper.vm.setContentIsOnViewport(wrapper.vm.$store.state); + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.find(DropdownContents).props('renderOnTop')).toBe(true); + }); + }); + + it('does not set direction when inside of viewport', () => { + isInViewport.mockImplementation(() => true); + wrapper.vm.setContentIsOnViewport(wrapper.vm.$store.state); + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.find(DropdownContents).props('renderOnTop')).toBe(false); + }); + }); + }); + }); }); }); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js index 072d8fe2fe2..c742220ba8a 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js @@ -1,10 +1,10 @@ import MockAdapter from 'axios-mock-adapter'; +import testAction from 'helpers/vuex_action_helper'; import defaultState from '~/vue_shared/components/sidebar/labels_select_vue/store/state'; import * as types from '~/vue_shared/components/sidebar/labels_select_vue/store/mutation_types'; import * as actions from '~/vue_shared/components/sidebar/labels_select_vue/store/actions'; -import testAction from 'helpers/vuex_action_helper'; import axios from '~/lib/utils/axios_utils'; describe('LabelsSelect Actions', () => { diff --git a/spec/frontend/vue_shared/components/split_button_spec.js b/spec/frontend/vue_shared/components/split_button_spec.js index e09bc073042..f3bd4c14717 100644 --- a/spec/frontend/vue_shared/components/split_button_spec.js +++ b/spec/frontend/vue_shared/components/split_button_spec.js @@ -1,4 +1,4 @@ -import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; +import { GlDeprecatedDropdown, GlDeprecatedDropdownItem } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import SplitButton from '~/vue_shared/components/split_button.vue'; @@ -25,10 +25,10 @@ describe('SplitButton', () => { }); }; - const findDropdown = () => wrapper.find(GlDropdown); + const findDropdown = () => wrapper.find(GlDeprecatedDropdown); const findDropdownItem = (index = 0) => findDropdown() - .findAll(GlDropdownItem) + .findAll(GlDeprecatedDropdownItem) .at(index); const selectItem = index => { findDropdownItem(index).vm.$emit('click'); diff --git a/spec/frontend/vue_shared/components/table_pagination_spec.js b/spec/frontend/vue_shared/components/table_pagination_spec.js index 56ffffc7f0f..ef3ae088eec 100644 --- a/spec/frontend/vue_shared/components/table_pagination_spec.js +++ b/spec/frontend/vue_shared/components/table_pagination_spec.js @@ -1,6 +1,6 @@ import { shallowMount } from '@vue/test-utils'; -import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue'; import { GlPagination } from '@gitlab/ui'; +import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue'; describe('Pagination component', () => { let wrapper; diff --git a/spec/frontend/vue_shared/components/time_ago_tooltip_spec.js b/spec/frontend/vue_shared/components/time_ago_tooltip_spec.js index 46fcb92455b..691e19473c1 100644 --- a/spec/frontend/vue_shared/components/time_ago_tooltip_spec.js +++ b/spec/frontend/vue_shared/components/time_ago_tooltip_spec.js @@ -1,16 +1,19 @@ import { shallowMount } from '@vue/test-utils'; -import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; + import { formatDate, getTimeago } from '~/lib/utils/datetime_utility'; +import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; describe('Time ago with tooltip component', () => { let vm; - const buildVm = (propsData = {}) => { + const buildVm = (propsData = {}, scopedSlots = {}) => { vm = shallowMount(TimeAgoTooltip, { propsData, + scopedSlots, }); }; const timestamp = '2017-05-08T14:57:39.781Z'; + const timeAgoTimestamp = getTimeago().format(timestamp); afterEach(() => { vm.destroy(); @@ -20,10 +23,9 @@ describe('Time ago with tooltip component', () => { buildVm({ time: timestamp, }); - const timeago = getTimeago(); expect(vm.attributes('title')).toEqual(formatDate(timestamp)); - expect(vm.text()).toEqual(timeago.format(timestamp)); + expect(vm.text()).toEqual(timeAgoTimestamp); }); it('should render provided html class', () => { @@ -34,4 +36,16 @@ describe('Time ago with tooltip component', () => { expect(vm.classes()).toContain('foo'); }); + + it('should render with the datetime attribute', () => { + buildVm({ time: timestamp }); + + expect(vm.attributes('datetime')).toEqual(timestamp); + }); + + it('should render provided scope content with the correct timeAgo string', () => { + buildVm({ time: timestamp }, { default: `<span>The time is {{ props.timeAgo }}</span>` }); + + expect(vm.text()).toEqual(`The time is ${timeAgoTimestamp}`); + }); }); diff --git a/spec/frontend/vue_shared/components/toggle_button_spec.js b/spec/frontend/vue_shared/components/toggle_button_spec.js index 83bbb37a89a..f58647ff12b 100644 --- a/spec/frontend/vue_shared/components/toggle_button_spec.js +++ b/spec/frontend/vue_shared/components/toggle_button_spec.js @@ -32,7 +32,7 @@ describe('Toggle Button', () => { it('renders input status icon', () => { expect(vm.$el.querySelectorAll('span.toggle-icon').length).toEqual(1); - expect(vm.$el.querySelectorAll('svg.s16.toggle-icon-svg').length).toEqual(1); + expect(vm.$el.querySelectorAll('svg.s18').length).toEqual(1); }); }); diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js index 1db1114f9ba..6f66d1cafb9 100644 --- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js +++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js @@ -1,5 +1,5 @@ import { shallowMount } from '@vue/test-utils'; -import { GlDeprecatedButton } from '@gitlab/ui'; +import { GlButton } from '@gitlab/ui'; import { TEST_HOST } from 'spec/test_constants'; import UserAvatarList from '~/vue_shared/components/user_avatar/user_avatar_list.vue'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; @@ -37,7 +37,7 @@ describe('UserAvatarList', () => { }; const clickButton = () => { - const button = wrapper.find(GlDeprecatedButton); + const button = wrapper.find(GlButton); button.vm.$emit('click'); }; @@ -112,7 +112,7 @@ describe('UserAvatarList', () => { it('does not show button', () => { factory(); - expect(wrapper.find(GlDeprecatedButton).exists()).toBe(false); + expect(wrapper.find(GlButton).exists()).toBe(false); }); }); diff --git a/spec/frontend/vue_shared/directives/autofocusonshow_spec.js b/spec/frontend/vue_shared/directives/autofocusonshow_spec.js index 90530b7d5c2..1c9e89f99e9 100644 --- a/spec/frontend/vue_shared/directives/autofocusonshow_spec.js +++ b/spec/frontend/vue_shared/directives/autofocusonshow_spec.js @@ -1,3 +1,4 @@ +import { useMockIntersectionObserver } from 'helpers/mock_dom_observer'; import autofocusonshow from '~/vue_shared/directives/autofocusonshow'; /** @@ -6,20 +7,14 @@ import autofocusonshow from '~/vue_shared/directives/autofocusonshow'; * on underlying DOM methods. */ describe('AutofocusOnShow directive', () => { + useMockIntersectionObserver(); + describe('with input invisible on component render', () => { let el; beforeEach(() => { setFixtures('<div id="container" style="display: none;"><input id="inputel"/></div>'); el = document.querySelector('#inputel'); - - window.IntersectionObserver = class { - observe = jest.fn(); - }; - }); - - afterEach(() => { - delete window.IntersectionObserver; }); it('should bind IntersectionObserver on input element', () => { |