summaryrefslogtreecommitdiff
path: root/spec/frontend/vue_shared
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-08-20 18:42:06 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-08-20 18:42:06 +0000
commit6e4e1050d9dba2b7b2523fdd1768823ab85feef4 (patch)
tree78be5963ec075d80116a932011d695dd33910b4e /spec/frontend/vue_shared
parent1ce776de4ae122aba3f349c02c17cebeaa8ecf07 (diff)
downloadgitlab-ce-6e4e1050d9dba2b7b2523fdd1768823ab85feef4.tar.gz
Add latest changes from gitlab-org/gitlab@13-3-stable-ee
Diffstat (limited to 'spec/frontend/vue_shared')
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap5
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap40
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap14
-rw-r--r--spec/frontend/vue_shared/components/clone_dropdown_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/dismissible_container_spec.js58
-rw-r--r--spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js91
-rw-r--r--spec/frontend/vue_shared/components/file_finder/item_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/file_icon_spec.js3
-rw-r--r--spec/frontend/vue_shared/components/file_row_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js129
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js19
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js56
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js170
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js152
-rw-r--r--spec/frontend/vue_shared/components/form/form_footer_actions_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/form/title_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/header_ci_component_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/icon_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/identicon_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/issuable/issuable_header_warnings_spec.js73
-rw-r--r--spec/frontend/vue_shared/components/issue/issue_assignees_spec.js23
-rw-r--r--spec/frontend/vue_shared/components/issue/issue_milestone_spec.js3
-rw-r--r--spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js94
-rw-r--r--spec/frontend/vue_shared/components/markdown/field_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/editor_service_spec.js24
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_integration_spec.js69
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js51
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer_spec.js83
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_embedded_ruby_spec.js12
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph_spec.js27
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list_spec.js23
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text_spec.js12
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_softbreak_spec.js23
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_utils_spec.js44
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js17
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js131
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_spec.js12
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_title_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js45
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/split_button_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/table_pagination_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/time_ago_tooltip_spec.js22
-rw-r--r--spec/frontend/vue_shared/components/toggle_button_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js6
-rw-r--r--spec/frontend/vue_shared/directives/autofocusonshow_spec.js11
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', () => {