Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-07-20 15:26:25 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-07-20 15:26:25 +0300
commita09983ae35713f5a2bbb100981116d31ce99826e (patch)
tree2ee2af7bd104d57086db360a7e6d8c9d5d43667a /spec/frontend/vue_shared
parent18c5ab32b738c0b6ecb4d0df3994000482f34bd8 (diff)
Add latest changes from gitlab-org/gitlab@13-2-stable-ee
Diffstat (limited to 'spec/frontend/vue_shared')
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap63
-rw-r--r--spec/frontend/vue_shared/components/file_icon_spec.js27
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js60
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js23
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js23
-rw-r--r--spec/frontend/vue_shared/components/gl_modal_vuex_spec.js28
-rw-r--r--spec/frontend/vue_shared/components/issue/issue_assignees_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/issue/issue_milestone_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/issue/issue_warning_spec.js105
-rw-r--r--spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js3
-rw-r--r--spec/frontend/vue_shared/components/issue/related_issuable_mock_data.js18
-rw-r--r--spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js25
-rw-r--r--spec/frontend/vue_shared/components/notes/__snapshots__/noteable_warning_spec.js.snap (renamed from spec/frontend/vue_shared/components/issue/__snapshots__/issue_warning_spec.js.snap)40
-rw-r--r--spec/frontend/vue_shared/components/notes/noteable_warning_spec.js196
-rw-r--r--spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js10
-rw-r--r--spec/frontend/vue_shared/components/remove_member_modal_spec.js65
-rw-r--r--spec/frontend/vue_shared/components/resizable_chart/__snapshots__/resizable_chart_container_spec.js.snap (renamed from spec/frontend/vue_shared/components/__snapshots__/resizable_chart_container_spec.js.snap)0
-rw-r--r--spec/frontend/vue_shared/components/resizable_chart/__snapshots__/skeleton_loader_spec.js.snap324
-rw-r--r--spec/frontend/vue_shared/components/resizable_chart/resizable_chart_container_spec.js (renamed from spec/frontend/vue_shared/components/resizable_chart_container_spec.js)0
-rw-r--r--spec/frontend/vue_shared/components/resizable_chart/skeleton_loader_spec.js55
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/editor_service_spec.js62
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal_spec.js76
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/modals/add_image/upload_image_tab_spec.js41
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/modals/add_image_modal_spec.js41
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js43
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/build_custom_renderer_spec.js29
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer_spec.js50
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token_spec.js88
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/renderers/mock_data.js58
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_embedded_ruby_spec.js30
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_font_awesome_html_inline_spec.js33
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_html_block_spec.js38
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_identifier_instance_text_spec.js55
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph_spec.js65
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list_spec.js55
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text_spec.js30
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js3
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js62
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js29
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/getters_spec.js21
-rw-r--r--spec/frontend/vue_shared/components/user_popover/user_popover_spec.js46
43 files changed, 1668 insertions, 376 deletions
diff --git a/spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap
index df4b30f1cb8..19671d425a9 100644
--- a/spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap
+++ b/spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap
@@ -18,15 +18,8 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
<gl-emoji
- data-fallback-src="/assets/emoji/thumbsup-59ec2457ab33e8897261d01a495f6cf5c668d0004807dc541c3b1be5294b1e61.png"
data-name="thumbsup"
- data-unicode-version="6.0"
- title="thumbs up sign"
- >
-
- 👍
-
- </gl-emoji>
+ />
</span>
@@ -51,15 +44,8 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
<gl-emoji
- data-fallback-src="/assets/emoji/thumbsdown-5954334e2dae5357312b3d629f10a496c728029e02216f8c8b887f9b51561c61.png"
data-name="thumbsdown"
- data-unicode-version="6.0"
- title="thumbs down sign"
- >
-
- 👎
-
- </gl-emoji>
+ />
</span>
@@ -84,15 +70,8 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
<gl-emoji
- data-fallback-src="/assets/emoji/smile-14905c372d5bf7719bd727c9efae31a03291acec79801652a23710c6848c5d14.png"
data-name="smile"
- data-unicode-version="6.0"
- title="smiling face with open mouth and smiling eyes"
- >
-
- 😄
-
- </gl-emoji>
+ />
</span>
@@ -117,15 +96,8 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
<gl-emoji
- data-fallback-src="/assets/emoji/ok_hand-d63002dce3cc3655b67b8765b7c28d370edba0e3758b2329b60e0e61c4d8e78d.png"
data-name="ok_hand"
- data-unicode-version="6.0"
- title="ok hand sign"
- >
-
- 👌
-
- </gl-emoji>
+ />
</span>
@@ -150,15 +122,8 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
<gl-emoji
- data-fallback-src="/assets/emoji/cactus-2c5c4c35f26c7046fdc002b337e0d939729b33a26980e675950f9934c91e40fd.png"
data-name="cactus"
- data-unicode-version="6.0"
- title="cactus"
- >
-
- 🌵
-
- </gl-emoji>
+ />
</span>
@@ -183,15 +148,8 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
<gl-emoji
- data-fallback-src="/assets/emoji/a-bddbb39e8a1d35d42b7c08e7d47f63988cb4d8614b79f74e70b9c67c221896cc.png"
data-name="a"
- data-unicode-version="6.0"
- title="negative squared latin capital letter a"
- >
-
- 🅰
-
- </gl-emoji>
+ />
</span>
@@ -216,15 +174,8 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
<gl-emoji
- data-fallback-src="/assets/emoji/b-722f9db9442e7c0fc0d0ac0f5291fbf47c6a0ac4d8abd42e97957da705fb82bf.png"
data-name="b"
- data-unicode-version="6.0"
- title="negative squared latin capital letter b"
- >
-
- 🅱
-
- </gl-emoji>
+ />
</span>
diff --git a/spec/frontend/vue_shared/components/file_icon_spec.js b/spec/frontend/vue_shared/components/file_icon_spec.js
index 5a385eee60c..adf0da21f9f 100644
--- a/spec/frontend/vue_shared/components/file_icon_spec.js
+++ b/spec/frontend/vue_shared/components/file_icon_spec.js
@@ -1,12 +1,14 @@
import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
import FileIcon from '~/vue_shared/components/file_icon.vue';
+import { FILE_SYMLINK_MODE } from '~/vue_shared/constants';
describe('File Icon component', () => {
let wrapper;
- const findIcon = () => wrapper.find('svg');
+ const findSvgIcon = () => wrapper.find('svg');
+ const findGlIcon = () => wrapper.find(GlIcon);
const getIconName = () =>
- findIcon()
+ findSvgIcon()
.find('use')
.element.getAttribute('xlink:href')
.replace(`${gon.sprite_file_icons}#`, '');
@@ -27,7 +29,7 @@ describe('File Icon component', () => {
});
expect(wrapper.element.tagName).toEqual('SPAN');
- expect(findIcon().exists()).toBeDefined();
+ expect(findSvgIcon().exists()).toBeDefined();
});
it.each`
@@ -46,8 +48,8 @@ describe('File Icon component', () => {
folder: true,
});
- expect(findIcon().exists()).toBe(false);
- expect(wrapper.find(GlIcon).classes()).toContain('folder-icon');
+ expect(findSvgIcon().exists()).toBe(false);
+ expect(findGlIcon().classes()).toContain('folder-icon');
});
it('should render a loading icon', () => {
@@ -66,8 +68,19 @@ describe('File Icon component', () => {
cssClasses: 'extraclasses',
size,
});
+ const classes = findSvgIcon().classes();
- expect(findIcon().classes()).toContain(`s${size}`);
- expect(findIcon().classes()).toContain('extraclasses');
+ expect(classes).toContain(`s${size}`);
+ expect(classes).toContain('extraclasses');
+ });
+
+ it('should render a symlink icon', () => {
+ createComponent({
+ fileName: 'anything',
+ fileMode: FILE_SYMLINK_MODE,
+ });
+
+ expect(findSvgIcon().exists()).toBe(false);
+ expect(findGlIcon().attributes('name')).toBe('symlink');
});
});
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 eded5b87abc..05508d14209 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
@@ -13,7 +13,7 @@ import { SortDirection } from '~/vue_shared/components/filtered_search_bar/const
import RecentSearchesStore from '~/filtered_search/stores/recent_searches_store';
import RecentSearchesService from '~/filtered_search/services/recent_searches_service';
-import { mockAvailableTokens, mockSortOptions } from './mock_data';
+import { mockAvailableTokens, mockSortOptions, mockHistoryItems } from './mock_data';
const createComponent = ({
namespace = 'gitlab-org/gitlab-test',
@@ -53,11 +53,17 @@ describe('FilteredSearchBarRoot', () => {
describe('computed', () => {
describe('tokenSymbols', () => {
- it('returns array of map containing type and symbols from `tokens` prop', () => {
+ it('returns a map containing type and symbols from `tokens` prop', () => {
expect(wrapper.vm.tokenSymbols).toEqual({ author_username: '@' });
});
});
+ describe('tokenTitles', () => {
+ it('returns a map containing type and title from `tokens` prop', () => {
+ expect(wrapper.vm.tokenTitles).toEqual({ author_username: 'Author' });
+ });
+ });
+
describe('sortDirectionIcon', () => {
it('returns string "sort-lowest" when `selectedSortDirection` is "ascending"', () => {
wrapper.setData({
@@ -133,14 +139,6 @@ describe('FilteredSearchBarRoot', () => {
});
});
- describe('getRecentSearches', () => {
- it('returns array of strings representing recent searches', () => {
- wrapper.vm.recentSearchesStore.setRecentSearches(['foo']);
-
- expect(wrapper.vm.getRecentSearches()).toEqual(['foo']);
- });
- });
-
describe('handleSortOptionClick', () => {
it('emits component event `onSort` with selected sort by value', () => {
wrapper.vm.handleSortOptionClick(mockSortOptions[1]);
@@ -172,6 +170,27 @@ describe('FilteredSearchBarRoot', () => {
});
});
+ describe('handleHistoryItemSelected', () => {
+ it('emits `onFilter` event with provided filters param', () => {
+ wrapper.vm.handleHistoryItemSelected(mockHistoryItems[0]);
+
+ expect(wrapper.emitted('onFilter')[0]).toEqual([mockHistoryItems[0]]);
+ });
+ });
+
+ describe('handleClearHistory', () => {
+ it('clears search history from recent searches store', () => {
+ jest.spyOn(wrapper.vm.recentSearchesStore, 'setRecentSearches').mockReturnValue([]);
+ jest.spyOn(wrapper.vm.recentSearchesService, 'save');
+
+ wrapper.vm.handleClearHistory();
+
+ expect(wrapper.vm.recentSearchesStore.setRecentSearches).toHaveBeenCalledWith([]);
+ expect(wrapper.vm.recentSearchesService.save).toHaveBeenCalledWith([]);
+ expect(wrapper.vm.recentSearches).toEqual([]);
+ });
+ });
+
describe('handleFilterSubmit', () => {
const mockFilters = [
{
@@ -186,14 +205,11 @@ describe('FilteredSearchBarRoot', () => {
it('calls `recentSearchesStore.addRecentSearch` with serialized value of provided `filters` param', () => {
jest.spyOn(wrapper.vm.recentSearchesStore, 'addRecentSearch');
- // jest.spyOn(wrapper.vm.recentSearchesService, 'save');
wrapper.vm.handleFilterSubmit(mockFilters);
return wrapper.vm.recentSearchesPromise.then(() => {
- expect(wrapper.vm.recentSearchesStore.addRecentSearch).toHaveBeenCalledWith(
- 'author_username:=@root foo',
- );
+ expect(wrapper.vm.recentSearchesStore.addRecentSearch).toHaveBeenCalledWith(mockFilters);
});
});
@@ -203,9 +219,17 @@ describe('FilteredSearchBarRoot', () => {
wrapper.vm.handleFilterSubmit(mockFilters);
return wrapper.vm.recentSearchesPromise.then(() => {
- expect(wrapper.vm.recentSearchesService.save).toHaveBeenCalledWith([
- 'author_username:=@root foo',
- ]);
+ expect(wrapper.vm.recentSearchesService.save).toHaveBeenCalledWith([mockFilters]);
+ });
+ });
+
+ it('sets `recentSearches` data prop with array of searches', () => {
+ jest.spyOn(wrapper.vm.recentSearchesService, 'save');
+
+ wrapper.vm.handleFilterSubmit(mockFilters);
+
+ return wrapper.vm.recentSearchesPromise.then(() => {
+ expect(wrapper.vm.recentSearches).toEqual([mockFilters]);
});
});
@@ -222,6 +246,7 @@ describe('FilteredSearchBarRoot', () => {
wrapper.setData({
selectedSortOption: mockSortOptions[0],
selectedSortDirection: SortDirection.descending,
+ recentSearches: mockHistoryItems,
});
return wrapper.vm.$nextTick();
@@ -232,6 +257,7 @@ describe('FilteredSearchBarRoot', () => {
expect(glFilteredSearchEl.props('placeholder')).toBe('Filter requirements');
expect(glFilteredSearchEl.props('availableTokens')).toEqual(mockAvailableTokens);
+ expect(glFilteredSearchEl.props('historyItems')).toEqual(mockHistoryItems);
});
it('renders sort dropdown component', () => {
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 edc0f119262..7e28c4e11e1 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
@@ -44,6 +44,29 @@ export const mockAuthorToken = {
export const mockAvailableTokens = [mockAuthorToken];
+export const mockHistoryItems = [
+ [
+ {
+ type: 'author_username',
+ value: {
+ data: 'toby',
+ operator: '=',
+ },
+ },
+ 'duo',
+ ],
+ [
+ {
+ type: 'author_username',
+ value: {
+ data: 'root',
+ operator: '=',
+ },
+ },
+ 'si',
+ ],
+];
+
export const mockSortOptions = [
{
id: 1,
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 3650ef79136..45294096eda 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
@@ -11,11 +11,12 @@ import { mockAuthorToken, mockAuthors } from '../mock_data';
jest.mock('~/flash');
-const createComponent = ({ config = mockAuthorToken, value = { data: '' } } = {}) =>
+const createComponent = ({ config = mockAuthorToken, value = { data: '' }, active = false } = {}) =>
mount(AuthorToken, {
propsData: {
config,
value,
+ active,
},
provide: {
portalName: 'fake target',
@@ -51,29 +52,23 @@ describe('AuthorToken', () => {
describe('computed', () => {
describe('currentValue', () => {
it('returns lowercase string for `value.data`', () => {
- wrapper.setProps({
- value: { data: 'FOO' },
- });
+ wrapper = createComponent({ value: { data: 'FOO' } });
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.vm.currentValue).toBe('foo');
- });
+ expect(wrapper.vm.currentValue).toBe('foo');
});
});
describe('activeAuthor', () => {
- it('returns object for currently present `value.data`', () => {
+ it('returns object for currently present `value.data`', async () => {
+ wrapper = createComponent({ value: { data: mockAuthors[0].username } });
+
wrapper.setData({
authors: mockAuthors,
});
- wrapper.setProps({
- value: { data: mockAuthors[0].username },
- });
+ await wrapper.vm.$nextTick();
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.vm.activeAuthor).toEqual(mockAuthors[0]);
- });
+ expect(wrapper.vm.activeAuthor).toEqual(mockAuthors[0]);
});
});
});
diff --git a/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js b/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js
index 8437e68d73c..93f4db5df18 100644
--- a/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js
+++ b/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js
@@ -38,6 +38,9 @@ describe('GlModalVuex', () => {
localVue,
store,
propsData,
+ stubs: {
+ GlModal,
+ },
});
};
@@ -148,4 +151,29 @@ describe('GlModalVuex', () => {
.then(done)
.catch(done.fail);
});
+
+ it.each(['ok', 'cancel'])(
+ 'passes an "%s" handler to the "modal-footer" slot scope',
+ handlerName => {
+ state.isVisible = true;
+
+ const modalFooterSlotContent = jest.fn();
+
+ factory({
+ scopedSlots: {
+ 'modal-footer': modalFooterSlotContent,
+ },
+ });
+
+ const handler = modalFooterSlotContent.mock.calls[0][0][handlerName];
+
+ expect(wrapper.emitted(handlerName)).toBeFalsy();
+ expect(actions.hide).not.toHaveBeenCalled();
+
+ handler();
+
+ expect(actions.hide).toHaveBeenCalledTimes(1);
+ expect(wrapper.emitted(handlerName)).toBeTruthy();
+ },
+ );
});
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 ca75c55df26..548d4476c0f 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 UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import IssueAssignees from '~/vue_shared/components/issue/issue_assignees.vue';
-import { mockAssigneesList } from '../../../../javascripts/boards/mock_data';
+import { mockAssigneesList } from 'jest/boards/mock_data';
const TEST_CSS_CLASSES = 'test-classes';
const TEST_MAX_VISIBLE = 4;
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 90c3fe54901..69d8c1a5918 100644
--- a/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js
+++ b/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js
@@ -4,7 +4,7 @@ import { shallowMount } from '@vue/test-utils';
import IssueMilestone from '~/vue_shared/components/issue/issue_milestone.vue';
import Icon from '~/vue_shared/components/icon.vue';
-import { mockMilestone } from '../../../../javascripts/boards/mock_data';
+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/issue_warning_spec.js b/spec/frontend/vue_shared/components/issue/issue_warning_spec.js
deleted file mode 100644
index 891c70bcb5c..00000000000
--- a/spec/frontend/vue_shared/components/issue/issue_warning_spec.js
+++ /dev/null
@@ -1,105 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import IssueWarning from '~/vue_shared/components/issue/issue_warning.vue';
-import Icon from '~/vue_shared/components/icon.vue';
-
-describe('Issue Warning Component', () => {
- let wrapper;
-
- const findIcon = () => wrapper.find(Icon);
- const findLockedBlock = () => wrapper.find({ ref: 'locked' });
- const findConfidentialBlock = () => wrapper.find({ ref: 'confidential' });
- const findLockedAndConfidentialBlock = () => wrapper.find({ ref: 'lockedAndConfidential' });
-
- const createComponent = props => {
- wrapper = shallowMount(IssueWarning, {
- propsData: {
- ...props,
- },
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- describe('when issue is locked but not confidential', () => {
- beforeEach(() => {
- createComponent({
- isLocked: true,
- lockedIssueDocsPath: 'locked-path',
- isConfidential: false,
- });
- });
-
- it('renders information about locked issue', () => {
- expect(findLockedBlock().exists()).toBe(true);
- expect(findLockedBlock().element).toMatchSnapshot();
- });
-
- it('renders warning icon', () => {
- expect(findIcon().exists()).toBe(true);
- });
-
- it('does not render information about locked and confidential issue', () => {
- expect(findLockedAndConfidentialBlock().exists()).toBe(false);
- });
-
- it('does not render information about confidential issue', () => {
- expect(findConfidentialBlock().exists()).toBe(false);
- });
- });
-
- describe('when issue is confidential but not locked', () => {
- beforeEach(() => {
- createComponent({
- isLocked: false,
- isConfidential: true,
- confidentialIssueDocsPath: 'confidential-path',
- });
- });
-
- it('renders information about confidential issue', () => {
- expect(findConfidentialBlock().exists()).toBe(true);
- expect(findConfidentialBlock().element).toMatchSnapshot();
- });
-
- it('renders warning icon', () => {
- expect(wrapper.find(Icon).exists()).toBe(true);
- });
-
- it('does not render information about locked issue', () => {
- expect(findLockedBlock().exists()).toBe(false);
- });
-
- it('does not render information about locked and confidential issue', () => {
- expect(findLockedAndConfidentialBlock().exists()).toBe(false);
- });
- });
-
- describe('when issue is locked and confidential', () => {
- beforeEach(() => {
- createComponent({
- isLocked: true,
- isConfidential: true,
- });
- });
-
- it('renders information about locked and confidential issue', () => {
- expect(findLockedAndConfidentialBlock().exists()).toBe(true);
- expect(findLockedAndConfidentialBlock().element).toMatchSnapshot();
- });
-
- it('does not render warning icon', () => {
- expect(wrapper.find(Icon).exists()).toBe(false);
- });
-
- it('does not render information about locked issue', () => {
- expect(findLockedBlock().exists()).toBe(false);
- });
-
- it('does not render information about confidential issue', () => {
- expect(findConfidentialBlock().exists()).toBe(false);
- });
- });
-});
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 9be0a67e4fa..fe9a5156539 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
@@ -3,6 +3,7 @@ import { mount } from '@vue/test-utils';
import { formatDate } from '~/lib/utils/datetime_utility';
import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue';
import { defaultAssignees, defaultMilestone } from './related_issuable_mock_data';
+import { TEST_HOST } from 'jest/helpers/test_constants';
describe('RelatedIssuableItem', () => {
let wrapper;
@@ -19,7 +20,7 @@ describe('RelatedIssuableItem', () => {
idKey: 1,
displayReference: 'gitlab-org/gitlab-test#1',
pathIdSeparator: '#',
- path: `${gl.TEST_HOST}/path`,
+ path: `${TEST_HOST}/path`,
title: 'title',
confidential: true,
dueDate: '1990-12-31',
diff --git a/spec/frontend/vue_shared/components/issue/related_issuable_mock_data.js b/spec/frontend/vue_shared/components/issue/related_issuable_mock_data.js
index 5f69d761fdf..17813f2833d 100644
--- a/spec/frontend/vue_shared/components/issue/related_issuable_mock_data.js
+++ b/spec/frontend/vue_shared/components/issue/related_issuable_mock_data.js
@@ -1,3 +1,5 @@
+import { TEST_HOST } from 'jest/helpers/test_constants';
+
export const defaultProps = {
endpoint: '/foo/bar/issues/1/related_issues',
currentNamespacePath: 'foo',
@@ -83,8 +85,8 @@ export const defaultAssignees = [
name: 'Administrator',
username: 'root',
state: 'active',
- avatar_url: `${gl.TEST_HOST}`,
- web_url: `${gl.TEST_HOST}/root`,
+ avatar_url: `${TEST_HOST}`,
+ web_url: `${TEST_HOST}/root`,
status_tooltip_html: null,
path: '/root',
},
@@ -93,8 +95,8 @@ export const defaultAssignees = [
name: 'Brooks Beatty',
username: 'brynn_champlin',
state: 'active',
- avatar_url: `${gl.TEST_HOST}`,
- web_url: `${gl.TEST_HOST}/brynn_champlin`,
+ avatar_url: `${TEST_HOST}`,
+ web_url: `${TEST_HOST}/brynn_champlin`,
status_tooltip_html: null,
path: '/brynn_champlin',
},
@@ -103,8 +105,8 @@ export const defaultAssignees = [
name: 'Bryce Turcotte',
username: 'melynda',
state: 'active',
- avatar_url: `${gl.TEST_HOST}`,
- web_url: `${gl.TEST_HOST}/melynda`,
+ avatar_url: `${TEST_HOST}`,
+ web_url: `${TEST_HOST}/melynda`,
status_tooltip_html: null,
path: '/melynda',
},
@@ -113,8 +115,8 @@ export const defaultAssignees = [
name: 'Conchita Eichmann',
username: 'juliana_gulgowski',
state: 'active',
- avatar_url: `${gl.TEST_HOST}`,
- web_url: `${gl.TEST_HOST}/juliana_gulgowski`,
+ avatar_url: `${TEST_HOST}`,
+ web_url: `${TEST_HOST}/juliana_gulgowski`,
status_tooltip_html: null,
path: '/juliana_gulgowski',
},
diff --git a/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js b/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js
index 9a5b95b555f..c6e147899e4 100644
--- a/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js
@@ -69,11 +69,6 @@ describe('Suggestion Diff component', () => {
expect(addToBatchBtn.html().includes('Add suggestion to batch')).toBe(true);
});
- it('renders correct tooltip message for apply button', () => {
- createComponent();
- expect(wrapper.vm.tooltipMessage).toBe('This also resolves the discussion');
- });
-
describe('when apply suggestion is clicked', () => {
beforeEach(() => {
createComponent();
@@ -227,17 +222,23 @@ describe('Suggestion Diff component', () => {
createComponent({ canApply: false });
});
- it('disables apply suggestion and add to batch buttons', () => {
+ it('disables apply suggestion and hides add to batch button', () => {
expect(findApplyButton().exists()).toBe(true);
- expect(findAddToBatchButton().exists()).toBe(true);
+ expect(findAddToBatchButton().exists()).toBe(false);
expect(findApplyButton().attributes('disabled')).toBe('true');
- expect(findAddToBatchButton().attributes('disabled')).toBe('true');
+ });
+ });
+
+ describe('tooltip message for apply button', () => {
+ it('renders correct tooltip message when button is applicable', () => {
+ createComponent();
+ expect(wrapper.vm.tooltipMessage).toBe('This also resolves this thread');
});
- it('renders correct tooltip message for apply button', () => {
- expect(wrapper.vm.tooltipMessage).toBe(
- "Can't apply as this line has changed or the suggestion already matches its content.",
- );
+ it('renders the inapplicable reason in the tooltip when button is not applicable', () => {
+ const inapplicableReason = 'lorem';
+ createComponent({ canApply: false, inapplicableReason });
+ expect(wrapper.vm.tooltipMessage).toBe(inapplicableReason);
});
});
});
diff --git a/spec/frontend/vue_shared/components/issue/__snapshots__/issue_warning_spec.js.snap b/spec/frontend/vue_shared/components/notes/__snapshots__/noteable_warning_spec.js.snap
index 49b18d3e106..573bc9abe4d 100644
--- a/spec/frontend/vue_shared/components/issue/__snapshots__/issue_warning_spec.js.snap
+++ b/spec/frontend/vue_shared/components/notes/__snapshots__/noteable_warning_spec.js.snap
@@ -1,6 +1,21 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`Issue Warning Component when issue is confidential but not locked renders information about confidential issue 1`] = `
+exports[`Issue Warning Component when issue is locked but not confidential renders information about locked issue 1`] = `
+<span>
+
+ This issue is locked.
+ Only project members can comment.
+
+ <gl-link-stub
+ href="locked-path"
+ target="_blank"
+ >
+ Learn more
+ </gl-link-stub>
+</span>
+`;
+
+exports[`Issue Warning Component when noteable is confidential but not locked renders information about confidential issue 1`] = `
<span>
This is a confidential issue.
@@ -10,14 +25,12 @@ exports[`Issue Warning Component when issue is confidential but not locked rende
href="confidential-path"
target="_blank"
>
-
- Learn more
-
+ Learn more
</gl-link-stub>
</span>
`;
-exports[`Issue Warning Component when issue is locked and confidential renders information about locked and confidential issue 1`] = `
+exports[`Issue Warning Component when noteable is locked and confidential renders information about locked and confidential noteable 1`] = `
<span>
<span>
This issue is
@@ -43,20 +56,3 @@ exports[`Issue Warning Component when issue is locked and confidential renders i
</span>
`;
-
-exports[`Issue Warning Component when issue is locked but not confidential renders information about locked issue 1`] = `
-<span>
-
- This issue is locked.
- Only project members can comment.
-
- <gl-link-stub
- href="locked-path"
- target="_blank"
- >
-
- Learn more
-
- </gl-link-stub>
-</span>
-`;
diff --git a/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js b/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js
new file mode 100644
index 00000000000..ae8c9a0928e
--- /dev/null
+++ b/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js
@@ -0,0 +1,196 @@
+import { shallowMount } from '@vue/test-utils';
+import NoteableWarning from '~/vue_shared/components/notes/noteable_warning.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+
+describe('Issue Warning Component', () => {
+ let wrapper;
+
+ const findIcon = (w = wrapper) => w.find(Icon);
+ const findLockedBlock = (w = wrapper) => w.find({ ref: 'locked' });
+ const findConfidentialBlock = (w = wrapper) => w.find({ ref: 'confidential' });
+ const findLockedAndConfidentialBlock = (w = wrapper) => w.find({ ref: 'lockedAndConfidential' });
+
+ const createComponent = props =>
+ shallowMount(NoteableWarning, {
+ propsData: {
+ ...props,
+ },
+ });
+
+ afterEach(() => {
+ if (wrapper) {
+ wrapper.destroy();
+ wrapper = null;
+ }
+ });
+
+ describe('when issue is locked but not confidential', () => {
+ beforeEach(() => {
+ wrapper = createComponent({
+ isLocked: true,
+ lockedNoteableDocsPath: 'locked-path',
+ isConfidential: false,
+ });
+ });
+
+ it('renders information about locked issue', () => {
+ expect(findLockedBlock().exists()).toBe(true);
+ expect(findLockedBlock().element).toMatchSnapshot();
+ });
+
+ it('renders warning icon', () => {
+ expect(findIcon().exists()).toBe(true);
+ });
+
+ it('does not render information about locked and confidential issue', () => {
+ expect(findLockedAndConfidentialBlock().exists()).toBe(false);
+ });
+
+ it('does not render information about confidential issue', () => {
+ expect(findConfidentialBlock().exists()).toBe(false);
+ });
+ });
+
+ describe('when noteable is confidential but not locked', () => {
+ beforeEach(() => {
+ wrapper = createComponent({
+ isLocked: false,
+ isConfidential: true,
+ confidentialNoteableDocsPath: 'confidential-path',
+ });
+ });
+
+ it('renders information about confidential issue', async () => {
+ expect(findConfidentialBlock().exists()).toBe(true);
+ expect(findConfidentialBlock().element).toMatchSnapshot();
+
+ await wrapper.vm.$nextTick();
+ expect(findConfidentialBlock(wrapper).text()).toContain('This is a confidential issue.');
+ });
+
+ it('renders warning icon', () => {
+ expect(wrapper.find(Icon).exists()).toBe(true);
+ });
+
+ it('does not render information about locked noteable', () => {
+ expect(findLockedBlock().exists()).toBe(false);
+ });
+
+ it('does not render information about locked and confidential noteable', () => {
+ expect(findLockedAndConfidentialBlock().exists()).toBe(false);
+ });
+ });
+
+ describe('when noteable is locked and confidential', () => {
+ beforeEach(() => {
+ wrapper = createComponent({
+ isLocked: true,
+ isConfidential: true,
+ });
+ });
+
+ it('renders information about locked and confidential noteable', () => {
+ expect(findLockedAndConfidentialBlock().exists()).toBe(true);
+ expect(findLockedAndConfidentialBlock().element).toMatchSnapshot();
+ });
+
+ it('does not render warning icon', () => {
+ expect(wrapper.find(Icon).exists()).toBe(false);
+ });
+
+ it('does not render information about locked noteable', () => {
+ expect(findLockedBlock().exists()).toBe(false);
+ });
+
+ it('does not render information about confidential noteable', () => {
+ expect(findConfidentialBlock().exists()).toBe(false);
+ });
+ });
+
+ describe('when noteableType prop is defined', () => {
+ let wrapperLocked;
+ let wrapperConfidential;
+ let wrapperLockedAndConfidential;
+
+ beforeEach(() => {
+ wrapperLocked = createComponent({
+ isLocked: true,
+ isConfidential: false,
+ });
+ wrapperConfidential = createComponent({
+ isLocked: false,
+ isConfidential: true,
+ });
+ wrapperLockedAndConfidential = createComponent({
+ isLocked: true,
+ isConfidential: true,
+ });
+ });
+
+ afterEach(() => {
+ wrapperLocked.destroy();
+ wrapperConfidential.destroy();
+ wrapperLockedAndConfidential.destroy();
+ });
+
+ it('renders confidential & locked messages with noteable "issue"', () => {
+ expect(findLockedBlock(wrapperLocked).text()).toContain('This issue is locked.');
+ expect(findConfidentialBlock(wrapperConfidential).text()).toContain(
+ 'This is a confidential issue.',
+ );
+ expect(findLockedAndConfidentialBlock(wrapperLockedAndConfidential).text()).toContain(
+ 'This issue is confidential and locked.',
+ );
+ });
+
+ it('renders confidential & locked messages with noteable "epic"', async () => {
+ wrapperLocked.setProps({
+ noteableType: 'Epic',
+ });
+ wrapperConfidential.setProps({
+ noteableType: 'Epic',
+ });
+ wrapperLockedAndConfidential.setProps({
+ noteableType: 'Epic',
+ });
+
+ await wrapperLocked.vm.$nextTick();
+ expect(findLockedBlock(wrapperLocked).text()).toContain('This epic is locked.');
+
+ await wrapperConfidential.vm.$nextTick();
+ expect(findConfidentialBlock(wrapperConfidential).text()).toContain(
+ 'This is a confidential epic.',
+ );
+
+ await wrapperLockedAndConfidential.vm.$nextTick();
+ expect(findLockedAndConfidentialBlock(wrapperLockedAndConfidential).text()).toContain(
+ 'This epic is confidential and locked.',
+ );
+ });
+
+ it('renders confidential & locked messages with noteable "merge request"', async () => {
+ wrapperLocked.setProps({
+ noteableType: 'MergeRequest',
+ });
+ wrapperConfidential.setProps({
+ noteableType: 'MergeRequest',
+ });
+ wrapperLockedAndConfidential.setProps({
+ noteableType: 'MergeRequest',
+ });
+
+ await wrapperLocked.vm.$nextTick();
+ expect(findLockedBlock(wrapperLocked).text()).toContain('This merge request is locked.');
+
+ await wrapperConfidential.vm.$nextTick();
+ expect(findConfidentialBlock(wrapperConfidential).text()).toContain(
+ 'This is a confidential merge request.',
+ );
+
+ await wrapperLockedAndConfidential.vm.$nextTick();
+ expect(findLockedAndConfidentialBlock(wrapperLockedAndConfidential).text()).toContain(
+ 'This merge request is confidential and locked.',
+ );
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js b/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js
index eb1d9e93634..385134c4a3f 100644
--- a/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js
+++ b/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js
@@ -74,6 +74,16 @@ describe('ProjectListItem component', () => {
expect(renderedNamespace).toBe('a / ... / e /');
});
+ it(`renders a simple namespace name of a GraphQL project`, () => {
+ options.propsData.project.name_with_namespace = undefined;
+ options.propsData.project.nameWithNamespace = 'test';
+
+ wrapper = shallowMount(Component, options);
+ const renderedNamespace = trimText(wrapper.find('.js-project-namespace').text());
+
+ expect(renderedNamespace).toBe('test /');
+ });
+
it(`renders the project name`, () => {
options.propsData.project.name = 'my-test-project';
diff --git a/spec/frontend/vue_shared/components/remove_member_modal_spec.js b/spec/frontend/vue_shared/components/remove_member_modal_spec.js
new file mode 100644
index 00000000000..2d380b25a0a
--- /dev/null
+++ b/spec/frontend/vue_shared/components/remove_member_modal_spec.js
@@ -0,0 +1,65 @@
+import { GlFormCheckbox, GlModal } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue';
+
+describe('RemoveMemberModal', () => {
+ const memberPath = '/gitlab-org/gitlab-test/-/project_members/90';
+ let wrapper;
+
+ const findForm = () => wrapper.find({ ref: 'form' });
+ const findGlModal = () => wrapper.find(GlModal);
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe.each`
+ state | isAccessRequest | actionText | checkboxTestDescription | checkboxExpected | message
+ ${'removing a member'} | ${'false'} | ${'Remove member'} | ${'shows a checkbox to allow removal from related issues and MRs'} | ${true} | ${'Are you sure you want to remove Jane Doe from the Gitlab Org / Gitlab Test project?'}
+ ${'denying an access request'} | ${'true'} | ${'Deny access request'} | ${'does not show a checkbox'} | ${false} | ${"Are you sure you want to deny Jane Doe's request to join the Gitlab Org / Gitlab Test project?"}
+ `(
+ 'when $state',
+ ({ actionText, isAccessRequest, message, checkboxTestDescription, checkboxExpected }) => {
+ beforeEach(() => {
+ wrapper = shallowMount(RemoveMemberModal, {
+ data() {
+ return {
+ modalData: {
+ isAccessRequest,
+ message,
+ memberPath,
+ },
+ };
+ },
+ });
+ });
+
+ it(`has the title ${actionText}`, () => {
+ expect(findGlModal().attributes('title')).toBe(actionText);
+ });
+
+ it('contains a form action', () => {
+ expect(findForm().attributes('action')).toBe(memberPath);
+ });
+
+ it('displays a message to the user', () => {
+ expect(wrapper.find('[data-testid=modal-message]').text()).toBe(message);
+ });
+
+ it(`${checkboxTestDescription}`, () => {
+ expect(wrapper.contains(GlFormCheckbox)).toBe(checkboxExpected);
+ });
+
+ it('submits the form when the modal is submitted', () => {
+ const spy = jest.spyOn(findForm().element, 'submit');
+
+ findGlModal().vm.$emit('primary');
+
+ expect(spy).toHaveBeenCalled();
+
+ spy.mockRestore();
+ });
+ },
+ );
+});
diff --git a/spec/frontend/vue_shared/components/__snapshots__/resizable_chart_container_spec.js.snap b/spec/frontend/vue_shared/components/resizable_chart/__snapshots__/resizable_chart_container_spec.js.snap
index add0c36a120..add0c36a120 100644
--- a/spec/frontend/vue_shared/components/__snapshots__/resizable_chart_container_spec.js.snap
+++ b/spec/frontend/vue_shared/components/resizable_chart/__snapshots__/resizable_chart_container_spec.js.snap
diff --git a/spec/frontend/vue_shared/components/resizable_chart/__snapshots__/skeleton_loader_spec.js.snap b/spec/frontend/vue_shared/components/resizable_chart/__snapshots__/skeleton_loader_spec.js.snap
new file mode 100644
index 00000000000..103b53cb280
--- /dev/null
+++ b/spec/frontend/vue_shared/components/resizable_chart/__snapshots__/skeleton_loader_spec.js.snap
@@ -0,0 +1,324 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Resizable Skeleton Loader default setup renders the bars, labels, and grid with correct position, size, and rx percentages 1`] = `
+<gl-skeleton-loader-stub
+ baseurl=""
+ height="130"
+ preserveaspectratio="xMidYMid meet"
+ width="400"
+>
+ <rect
+ data-testid="skeleton-chart-grid"
+ height="1px"
+ width="100%"
+ x="0"
+ y="30%"
+ />
+ <rect
+ data-testid="skeleton-chart-grid"
+ height="1px"
+ width="100%"
+ x="0"
+ y="60%"
+ />
+ <rect
+ data-testid="skeleton-chart-grid"
+ height="1px"
+ width="100%"
+ x="0"
+ y="90%"
+ />
+
+ <rect
+ data-testid="skeleton-chart-bar"
+ height="5%"
+ rx="0.4%"
+ width="6%"
+ x="5.875%"
+ y="85%"
+ />
+ <rect
+ data-testid="skeleton-chart-bar"
+ height="7%"
+ rx="0.4%"
+ width="6%"
+ x="17.625%"
+ y="83%"
+ />
+ <rect
+ data-testid="skeleton-chart-bar"
+ height="9%"
+ rx="0.4%"
+ width="6%"
+ x="29.375%"
+ y="81%"
+ />
+ <rect
+ data-testid="skeleton-chart-bar"
+ height="14%"
+ rx="0.4%"
+ width="6%"
+ x="41.125%"
+ y="76%"
+ />
+ <rect
+ data-testid="skeleton-chart-bar"
+ height="21%"
+ rx="0.4%"
+ width="6%"
+ x="52.875%"
+ y="69%"
+ />
+ <rect
+ data-testid="skeleton-chart-bar"
+ height="35%"
+ rx="0.4%"
+ width="6%"
+ x="64.625%"
+ y="55%"
+ />
+ <rect
+ data-testid="skeleton-chart-bar"
+ height="50%"
+ rx="0.4%"
+ width="6%"
+ x="76.375%"
+ y="40%"
+ />
+ <rect
+ data-testid="skeleton-chart-bar"
+ height="80%"
+ rx="0.4%"
+ width="6%"
+ x="88.125%"
+ y="10%"
+ />
+
+ <rect
+ data-testid="skeleton-chart-label"
+ height="5%"
+ rx="0.4%"
+ width="4%"
+ x="6.875%"
+ y="95%"
+ />
+ <rect
+ data-testid="skeleton-chart-label"
+ height="5%"
+ rx="0.4%"
+ width="4%"
+ x="18.625%"
+ y="95%"
+ />
+ <rect
+ data-testid="skeleton-chart-label"
+ height="5%"
+ rx="0.4%"
+ width="4%"
+ x="30.375%"
+ y="95%"
+ />
+ <rect
+ data-testid="skeleton-chart-label"
+ height="5%"
+ rx="0.4%"
+ width="4%"
+ x="42.125%"
+ y="95%"
+ />
+ <rect
+ data-testid="skeleton-chart-label"
+ height="5%"
+ rx="0.4%"
+ width="4%"
+ x="53.875%"
+ y="95%"
+ />
+ <rect
+ data-testid="skeleton-chart-label"
+ height="5%"
+ rx="0.4%"
+ width="4%"
+ x="65.625%"
+ y="95%"
+ />
+ <rect
+ data-testid="skeleton-chart-label"
+ height="5%"
+ rx="0.4%"
+ width="4%"
+ x="77.375%"
+ y="95%"
+ />
+ <rect
+ data-testid="skeleton-chart-label"
+ height="5%"
+ rx="0.4%"
+ width="4%"
+ x="89.125%"
+ y="95%"
+ />
+</gl-skeleton-loader-stub>
+`;
+
+exports[`Resizable Skeleton Loader with custom settings renders the correct position, and size percentages for bars and labels with different settings 1`] = `
+<gl-skeleton-loader-stub
+ baseurl=""
+ height="130"
+ preserveaspectratio="xMidYMid meet"
+ uniquekey=""
+ width="400"
+>
+ <rect
+ data-testid="skeleton-chart-grid"
+ height="1px"
+ width="100%"
+ x="0"
+ y="30%"
+ />
+ <rect
+ data-testid="skeleton-chart-grid"
+ height="1px"
+ width="100%"
+ x="0"
+ y="60%"
+ />
+ <rect
+ data-testid="skeleton-chart-grid"
+ height="1px"
+ width="100%"
+ x="0"
+ y="90%"
+ />
+
+ <rect
+ data-testid="skeleton-chart-bar"
+ height="5%"
+ rx="0.6%"
+ width="3%"
+ x="6.0625%"
+ y="85%"
+ />
+ <rect
+ data-testid="skeleton-chart-bar"
+ height="7%"
+ rx="0.6%"
+ width="3%"
+ x="18.1875%"
+ y="83%"
+ />
+ <rect
+ data-testid="skeleton-chart-bar"
+ height="9%"
+ rx="0.6%"
+ width="3%"
+ x="30.3125%"
+ y="81%"
+ />
+ <rect
+ data-testid="skeleton-chart-bar"
+ height="14%"
+ rx="0.6%"
+ width="3%"
+ x="42.4375%"
+ y="76%"
+ />
+ <rect
+ data-testid="skeleton-chart-bar"
+ height="21%"
+ rx="0.6%"
+ width="3%"
+ x="54.5625%"
+ y="69%"
+ />
+ <rect
+ data-testid="skeleton-chart-bar"
+ height="35%"
+ rx="0.6%"
+ width="3%"
+ x="66.6875%"
+ y="55%"
+ />
+ <rect
+ data-testid="skeleton-chart-bar"
+ height="50%"
+ rx="0.6%"
+ width="3%"
+ x="78.8125%"
+ y="40%"
+ />
+ <rect
+ data-testid="skeleton-chart-bar"
+ height="80%"
+ rx="0.6%"
+ width="3%"
+ x="90.9375%"
+ y="10%"
+ />
+
+ <rect
+ data-testid="skeleton-chart-label"
+ height="2%"
+ rx="0.6%"
+ width="7%"
+ x="4.0625%"
+ y="98%"
+ />
+ <rect
+ data-testid="skeleton-chart-label"
+ height="2%"
+ rx="0.6%"
+ width="7%"
+ x="16.1875%"
+ y="98%"
+ />
+ <rect
+ data-testid="skeleton-chart-label"
+ height="2%"
+ rx="0.6%"
+ width="7%"
+ x="28.3125%"
+ y="98%"
+ />
+ <rect
+ data-testid="skeleton-chart-label"
+ height="2%"
+ rx="0.6%"
+ width="7%"
+ x="40.4375%"
+ y="98%"
+ />
+ <rect
+ data-testid="skeleton-chart-label"
+ height="2%"
+ rx="0.6%"
+ width="7%"
+ x="52.5625%"
+ y="98%"
+ />
+ <rect
+ data-testid="skeleton-chart-label"
+ height="2%"
+ rx="0.6%"
+ width="7%"
+ x="64.6875%"
+ y="98%"
+ />
+ <rect
+ data-testid="skeleton-chart-label"
+ height="2%"
+ rx="0.6%"
+ width="7%"
+ x="76.8125%"
+ y="98%"
+ />
+ <rect
+ data-testid="skeleton-chart-label"
+ height="2%"
+ rx="0.6%"
+ width="7%"
+ x="88.9375%"
+ y="98%"
+ />
+</gl-skeleton-loader-stub>
+`;
diff --git a/spec/frontend/vue_shared/components/resizable_chart_container_spec.js b/spec/frontend/vue_shared/components/resizable_chart/resizable_chart_container_spec.js
index 3a5514ef318..3a5514ef318 100644
--- a/spec/frontend/vue_shared/components/resizable_chart_container_spec.js
+++ b/spec/frontend/vue_shared/components/resizable_chart/resizable_chart_container_spec.js
diff --git a/spec/frontend/vue_shared/components/resizable_chart/skeleton_loader_spec.js b/spec/frontend/vue_shared/components/resizable_chart/skeleton_loader_spec.js
new file mode 100644
index 00000000000..7facd02e596
--- /dev/null
+++ b/spec/frontend/vue_shared/components/resizable_chart/skeleton_loader_spec.js
@@ -0,0 +1,55 @@
+import { shallowMount } from '@vue/test-utils';
+import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue';
+
+describe('Resizable Skeleton Loader', () => {
+ let wrapper;
+
+ const createComponent = (propsData = {}) => {
+ wrapper = shallowMount(ChartSkeletonLoader, {
+ propsData,
+ });
+ };
+
+ const verifyElementsPresence = () => {
+ const gridItems = wrapper.findAll('[data-testid="skeleton-chart-grid"]').wrappers;
+ const barItems = wrapper.findAll('[data-testid="skeleton-chart-bar"]').wrappers;
+ const labelItems = wrapper.findAll('[data-testid="skeleton-chart-label"]').wrappers;
+ expect(gridItems.length).toBe(3);
+ expect(barItems.length).toBe(8);
+ expect(labelItems.length).toBe(8);
+ };
+
+ afterEach(() => {
+ if (wrapper?.destroy) {
+ wrapper.destroy();
+ }
+ });
+
+ describe('default setup', () => {
+ beforeEach(() => {
+ createComponent({ uniqueKey: null });
+ });
+
+ it('renders the bars, labels, and grid with correct position, size, and rx percentages', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('renders the correct number of grid items, bars, and labels', () => {
+ verifyElementsPresence();
+ });
+ });
+
+ describe('with custom settings', () => {
+ beforeEach(() => {
+ createComponent({ uniqueKey: '', rx: 0.6, barWidth: 3, labelWidth: 7, labelHeight: 2 });
+ });
+
+ it('renders the correct position, and size percentages for bars and labels with different settings', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('renders the correct number of grid items, bars, and labels', () => {
+ verifyElementsPresence();
+ });
+ });
+});
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 faa32131fab..78f27c9948b 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
@@ -2,18 +2,35 @@ import {
generateToolbarItem,
addCustomEventListener,
removeCustomEventListener,
+ registerHTMLToMarkdownRenderer,
addImage,
getMarkdown,
-} from '~/vue_shared/components/rich_content_editor/editor_service';
+} 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';
+
+jest.mock('~/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer');
describe('Editor Service', () => {
- const mockInstance = {
- eventManager: { addEventType: jest.fn(), removeEventHandler: jest.fn(), listen: jest.fn() },
- editor: { exec: jest.fn() },
- invoke: jest.fn(),
- };
- const event = 'someCustomEvent';
- const handler = jest.fn();
+ let mockInstance;
+ let event;
+ let handler;
+
+ beforeEach(() => {
+ mockInstance = {
+ eventManager: { addEventType: jest.fn(), removeEventHandler: jest.fn(), listen: jest.fn() },
+ editor: { exec: jest.fn() },
+ invoke: jest.fn(),
+ toMarkOptions: {
+ renderer: {
+ constructor: {
+ factory: jest.fn(),
+ },
+ },
+ },
+ };
+ event = 'someCustomEvent';
+ handler = jest.fn();
+ });
describe('generateToolbarItem', () => {
const config = {
@@ -74,4 +91,33 @@ describe('Editor Service', () => {
expect(mockInstance.invoke).toHaveBeenCalledWith('getMarkdown');
});
});
+
+ describe('registerHTMLToMarkdownRenderer', () => {
+ let baseRenderer;
+ const htmlToMarkdownRenderer = {};
+ const extendedRenderer = {};
+
+ beforeEach(() => {
+ baseRenderer = mockInstance.toMarkOptions.renderer;
+ buildHTMLToMarkdownRenderer.mockReturnValueOnce(htmlToMarkdownRenderer);
+ baseRenderer.constructor.factory.mockReturnValueOnce(extendedRenderer);
+
+ registerHTMLToMarkdownRenderer(mockInstance);
+ });
+
+ it('builds a new instance of the HTML to Markdown renderer', () => {
+ expect(buildHTMLToMarkdownRenderer).toHaveBeenCalledWith(baseRenderer);
+ });
+
+ it('extends base renderer with the HTML to Markdown renderer', () => {
+ expect(baseRenderer.constructor.factory).toHaveBeenCalledWith(
+ baseRenderer,
+ htmlToMarkdownRenderer,
+ );
+ });
+
+ it('replaces the default renderer with extended renderer', () => {
+ expect(mockInstance.toMarkOptions.renderer).toBe(extendedRenderer);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal_spec.js
new file mode 100644
index 00000000000..0c2ac53aa52
--- /dev/null
+++ b/spec/frontend/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal_spec.js
@@ -0,0 +1,76 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlModal, GlTabs } from '@gitlab/ui';
+import AddImageModal from '~/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal.vue';
+import UploadImageTab from '~/vue_shared/components/rich_content_editor/modals/add_image/upload_image_tab.vue';
+import { IMAGE_TABS } from '~/vue_shared/components/rich_content_editor/constants';
+
+describe('Add Image Modal', () => {
+ let wrapper;
+ const propsData = { imageRoot: 'path/to/root/' };
+
+ const findModal = () => wrapper.find(GlModal);
+ const findTabs = () => wrapper.find(GlTabs);
+ const findUploadImageTab = () => wrapper.find(UploadImageTab);
+ const findUrlInput = () => wrapper.find({ ref: 'urlInput' });
+ const findDescriptionInput = () => wrapper.find({ ref: 'descriptionInput' });
+
+ beforeEach(() => {
+ wrapper = shallowMount(AddImageModal, {
+ provide: { glFeatures: { sseImageUploads: true } },
+ propsData,
+ });
+ });
+
+ describe('when content is loaded', () => {
+ it('renders a modal component', () => {
+ expect(findModal().exists()).toBe(true);
+ });
+
+ it('renders a Tabs component', () => {
+ expect(findTabs().exists()).toBe(true);
+ });
+
+ it('renders an upload image tab', () => {
+ expect(findUploadImageTab().exists()).toBe(true);
+ });
+
+ it('renders an input to add an image URL', () => {
+ expect(findUrlInput().exists()).toBe(true);
+ });
+
+ it('renders an input to add an image description', () => {
+ expect(findDescriptionInput().exists()).toBe(true);
+ });
+ });
+
+ describe('add image', () => {
+ describe('Upload', () => {
+ it('validates the file', () => {
+ const preventDefault = jest.fn();
+ const description = 'some description';
+ const file = { name: 'some_file.png' };
+
+ wrapper.vm.$refs.uploadImageTab = { validateFile: jest.fn() };
+ wrapper.setData({ file, description, tabIndex: IMAGE_TABS.UPLOAD_TAB });
+
+ findModal().vm.$emit('ok', { preventDefault });
+
+ expect(wrapper.vm.$refs.uploadImageTab.validateFile).toHaveBeenCalled();
+ });
+ });
+
+ describe('URL', () => {
+ it('emits an addImage event when a valid URL is specified', () => {
+ const preventDefault = jest.fn();
+ const mockImage = { imageUrl: '/some/valid/url.png', description: 'some description' };
+ wrapper.setData({ ...mockImage, tabIndex: IMAGE_TABS.URL_TAB });
+
+ findModal().vm.$emit('ok', { preventDefault });
+ expect(preventDefault).not.toHaveBeenCalled();
+ expect(wrapper.emitted('addImage')).toEqual([
+ [{ imageUrl: mockImage.imageUrl, altText: mockImage.description }],
+ ]);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/modals/add_image/upload_image_tab_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/modals/add_image/upload_image_tab_spec.js
new file mode 100644
index 00000000000..ded490b2568
--- /dev/null
+++ b/spec/frontend/vue_shared/components/rich_content_editor/modals/add_image/upload_image_tab_spec.js
@@ -0,0 +1,41 @@
+import { shallowMount } from '@vue/test-utils';
+import UploadImageTab from '~/vue_shared/components/rich_content_editor/modals/add_image/upload_image_tab.vue';
+
+describe('Upload Image Tab', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = shallowMount(UploadImageTab);
+ });
+
+ afterEach(() => wrapper.destroy());
+
+ const triggerInputEvent = size => {
+ const file = { size, name: 'file-name.png' };
+ const mockEvent = new Event('input');
+
+ Object.defineProperty(mockEvent, 'target', { value: { files: [file] } });
+
+ wrapper.find({ ref: 'fileInput' }).element.dispatchEvent(mockEvent);
+
+ return file;
+ };
+
+ describe('onInput', () => {
+ it.each`
+ size | fileError
+ ${2000000000} | ${'Maximum file size is 2MB. Please select a smaller file.'}
+ ${200} | ${null}
+ `('validates the file correctly', ({ size, fileError }) => {
+ triggerInputEvent(size);
+
+ expect(wrapper.vm.fileError).toBe(fileError);
+ });
+ });
+
+ it('emits input event when file is valid', () => {
+ const file = triggerInputEvent(200);
+
+ expect(wrapper.emitted('input')).toEqual([[file]]);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/modals/add_image_modal_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/modals/add_image_modal_spec.js
deleted file mode 100644
index 4889bc8538d..00000000000
--- a/spec/frontend/vue_shared/components/rich_content_editor/modals/add_image_modal_spec.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { GlModal } from '@gitlab/ui';
-import AddImageModal from '~/vue_shared/components/rich_content_editor/modals/add_image_modal.vue';
-
-describe('Add Image Modal', () => {
- let wrapper;
-
- const findModal = () => wrapper.find(GlModal);
- const findUrlInput = () => wrapper.find({ ref: 'urlInput' });
- const findDescriptionInput = () => wrapper.find({ ref: 'descriptionInput' });
-
- beforeEach(() => {
- wrapper = shallowMount(AddImageModal);
- });
-
- describe('when content is loaded', () => {
- it('renders a modal component', () => {
- expect(findModal().exists()).toBe(true);
- });
-
- it('renders an input to add an image URL', () => {
- expect(findUrlInput().exists()).toBe(true);
- });
-
- it('renders an input to add an image description', () => {
- expect(findDescriptionInput().exists()).toBe(true);
- });
- });
-
- describe('add image', () => {
- it('emits an addImage event when a valid URL is specified', () => {
- const preventDefault = jest.fn();
- const mockImage = { imageUrl: '/some/valid/url.png', altText: 'some description' };
- wrapper.setData({ ...mockImage });
-
- findModal().vm.$emit('ok', { preventDefault });
- expect(preventDefault).not.toHaveBeenCalled();
- expect(wrapper.emitted('addImage')).toEqual([[mockImage]]);
- });
- });
-});
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 0db10389df4..b6ff6aa767c 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
@@ -1,6 +1,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_modal.vue';
+import AddImageModal from '~/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal.vue';
import {
EDITOR_OPTIONS,
EDITOR_TYPES,
@@ -13,25 +13,28 @@ import {
addCustomEventListener,
removeCustomEventListener,
addImage,
-} from '~/vue_shared/components/rich_content_editor/editor_service';
+ registerHTMLToMarkdownRenderer,
+} from '~/vue_shared/components/rich_content_editor/services/editor_service';
-jest.mock('~/vue_shared/components/rich_content_editor/editor_service', () => ({
- ...jest.requireActual('~/vue_shared/components/rich_content_editor/editor_service'),
+jest.mock('~/vue_shared/components/rich_content_editor/services/editor_service', () => ({
+ ...jest.requireActual('~/vue_shared/components/rich_content_editor/services/editor_service'),
addCustomEventListener: jest.fn(),
removeCustomEventListener: jest.fn(),
addImage: jest.fn(),
+ registerHTMLToMarkdownRenderer: jest.fn(),
}));
describe('Rich Content Editor', () => {
let wrapper;
- const value = '## Some Markdown';
+ const content = '## Some Markdown';
+ const imageRoot = 'path/to/root/';
const findEditor = () => wrapper.find({ ref: 'editor' });
const findAddImageModal = () => wrapper.find(AddImageModal);
beforeEach(() => {
wrapper = shallowMount(RichContentEditor, {
- propsData: { value },
+ propsData: { content, imageRoot },
});
});
@@ -41,7 +44,7 @@ describe('Rich Content Editor', () => {
});
it('renders the correct content', () => {
- expect(findEditor().props().initialValue).toBe(value);
+ expect(findEditor().props().initialValue).toBe(content);
});
it('provides the correct editor options', () => {
@@ -73,17 +76,37 @@ describe('Rich Content Editor', () => {
});
});
+ describe('when content is reset', () => {
+ it('should reset the content via setMarkdown', () => {
+ const newContent = 'Just the body content excluding the front matter for example';
+ const mockInstance = { invoke: jest.fn() };
+ wrapper.vm.$refs.editor = mockInstance;
+
+ wrapper.vm.resetInitialValue(newContent);
+
+ expect(mockInstance.invoke).toHaveBeenCalledWith('setMarkdown', newContent);
+ });
+ });
+
describe('when editor is loaded', () => {
- it('adds the CUSTOM_EVENTS.openAddImageModal custom event listener', () => {
- const mockEditorApi = { eventManager: { addEventType: jest.fn(), listen: jest.fn() } };
+ let mockEditorApi;
+
+ beforeEach(() => {
+ mockEditorApi = { eventManager: { addEventType: jest.fn(), listen: jest.fn() } };
findEditor().vm.$emit('load', mockEditorApi);
+ });
+ it('adds the CUSTOM_EVENTS.openAddImageModal custom event listener', () => {
expect(addCustomEventListener).toHaveBeenCalledWith(
mockEditorApi,
CUSTOM_EVENTS.openAddImageModal,
wrapper.vm.onOpenAddImageModal,
);
});
+
+ it('registers HTML to markdown renderer', () => {
+ expect(registerHTMLToMarkdownRenderer).toHaveBeenCalledWith(mockEditorApi);
+ });
});
describe('when editor is destroyed', () => {
@@ -107,7 +130,7 @@ describe('Rich Content Editor', () => {
});
it('calls the onAddImage method when the addImage event is emitted', () => {
- const mockImage = { imageUrl: 'some/url.png', description: 'some description' };
+ const mockImage = { imageUrl: 'some/url.png', altText: 'some description' };
const mockInstance = { exec: jest.fn() };
wrapper.vm.$refs.editor = mockInstance;
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/build_custom_renderer_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/build_custom_renderer_spec.js
new file mode 100644
index 00000000000..cafe53e6bb2
--- /dev/null
+++ b/spec/frontend/vue_shared/components/rich_content_editor/services/build_custom_renderer_spec.js
@@ -0,0 +1,29 @@
+import buildCustomHTMLRenderer from '~/vue_shared/components/rich_content_editor/services/build_custom_renderer';
+
+describe('Build Custom Renderer Service', () => {
+ describe('buildCustomHTMLRenderer', () => {
+ it('should return an object with the default renderer functions when lacking arguments', () => {
+ expect(buildCustomHTMLRenderer()).toEqual(
+ expect.objectContaining({
+ list: expect.any(Function),
+ text: expect.any(Function),
+ }),
+ );
+ });
+
+ it('should return an object with both custom and default renderer functions when passed customRenderers', () => {
+ const mockHtmlCustomRenderer = jest.fn();
+ const customRenderers = {
+ html: [mockHtmlCustomRenderer],
+ };
+
+ expect(buildCustomHTMLRenderer(customRenderers)).toEqual(
+ expect.objectContaining({
+ html: expect.any(Function),
+ list: expect.any(Function),
+ text: expect.any(Function),
+ }),
+ );
+ });
+ });
+});
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
new file mode 100644
index 00000000000..0e8610a22f5
--- /dev/null
+++ b/spec/frontend/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer_spec.js
@@ -0,0 +1,50 @@
+import buildHTMLToMarkdownRenderer from '~/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer';
+
+describe('HTMLToMarkdownRenderer', () => {
+ let baseRenderer;
+ let htmlToMarkdownRenderer;
+ const NODE = { nodeValue: 'mock_node' };
+
+ beforeEach(() => {
+ baseRenderer = {
+ trim: jest.fn(input => `trimmed ${input}`),
+ getSpaceCollapsedText: jest.fn(input => `space collapsed ${input}`),
+ getSpaceControlled: jest.fn(input => `space controlled ${input}`),
+ convert: jest.fn(),
+ };
+ });
+
+ describe('TEXT_NODE visitor', () => {
+ it('composes getSpaceControlled, getSpaceCollapsedText, and trim services', () => {
+ htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer);
+
+ expect(htmlToMarkdownRenderer.TEXT_NODE(NODE)).toBe(
+ `space controlled trimmed space collapsed ${NODE.nodeValue}`,
+ );
+ });
+ });
+
+ describe('LI OL, LI UL visitor', () => {
+ const oneLevelNestedList = '\n * List item 1\n * List item 2';
+ const twoLevelNestedList = '\n * List item 1\n * List item 2';
+ const spaceInContentList = '\n * List item 1\n * List item 2';
+
+ it.each`
+ list | indentSpaces | result
+ ${oneLevelNestedList} | ${2} | ${'\n * List item 1\n * List item 2'}
+ ${oneLevelNestedList} | ${3} | ${'\n * List item 1\n * List item 2'}
+ ${oneLevelNestedList} | ${6} | ${'\n * List item 1\n * List item 2'}
+ ${twoLevelNestedList} | ${4} | ${'\n * List item 1\n * List item 2'}
+ ${spaceInContentList} | ${1} | ${'\n * List item 1\n * List item 2'}
+ `('changes the list indentation to $indentSpaces spaces', ({ list, indentSpaces, result }) => {
+ htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer, {
+ subListIndentSpaces: indentSpaces,
+ });
+
+ baseRenderer.convert.mockReturnValueOnce(list);
+
+ expect(htmlToMarkdownRenderer['LI OL, LI UL'](NODE, list)).toBe(result);
+ expect(baseRenderer.convert).toHaveBeenCalledWith(NODE, list);
+ });
+ });
+});
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
new file mode 100644
index 00000000000..18dff0a39bb
--- /dev/null
+++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token_spec.js
@@ -0,0 +1,88 @@
+import {
+ buildTextToken,
+ buildUneditableOpenTokens,
+ buildUneditableCloseToken,
+ buildUneditableCloseTokens,
+ buildUneditableTokens,
+ buildUneditableInlineTokens,
+ buildUneditableHtmlAsTextTokens,
+} from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token';
+
+import {
+ originInlineToken,
+ originToken,
+ uneditableOpenTokens,
+ uneditableCloseToken,
+ uneditableCloseTokens,
+ uneditableBlockTokens,
+ uneditableInlineTokens,
+ uneditableTokens,
+} from './mock_data';
+
+describe('Build Uneditable Token renderer helper', () => {
+ describe('buildTextToken', () => {
+ it('returns an object literal representing a text token', () => {
+ const text = originToken.content;
+ expect(buildTextToken(text)).toStrictEqual(originToken);
+ });
+ });
+
+ describe('buildUneditableOpenTokens', () => {
+ it('returns a 2-item array of tokens with the originToken appended to an open token', () => {
+ const result = buildUneditableOpenTokens(originToken);
+
+ expect(result).toHaveLength(2);
+ expect(result).toStrictEqual(uneditableOpenTokens);
+ });
+ });
+
+ describe('buildUneditableCloseToken', () => {
+ it('returns an object literal representing the uneditable close token', () => {
+ expect(buildUneditableCloseToken()).toStrictEqual(uneditableCloseToken);
+ });
+ });
+
+ describe('buildUneditableCloseTokens', () => {
+ it('returns a 2-item array of tokens with the originToken prepended to a close token', () => {
+ const result = buildUneditableCloseTokens(originToken);
+
+ expect(result).toHaveLength(2);
+ expect(result).toStrictEqual(uneditableCloseTokens);
+ });
+ });
+
+ describe('buildUneditableTokens', () => {
+ it('returns a 3-item array of tokens with the originToken wrapped in the middle of block tokens', () => {
+ const result = buildUneditableTokens(originToken);
+
+ expect(result).toHaveLength(3);
+ expect(result).toStrictEqual(uneditableTokens);
+ });
+ });
+
+ describe('buildUneditableInlineTokens', () => {
+ it('returns a 3-item array of tokens with the originInlineToken wrapped in the middle of inline tokens', () => {
+ const result = buildUneditableInlineTokens(originInlineToken);
+
+ expect(result).toHaveLength(3);
+ expect(result).toStrictEqual(uneditableInlineTokens);
+ });
+ });
+
+ describe('buildUneditableHtmlAsTextTokens', () => {
+ it('returns a 3-item array of tokens with the htmlBlockNode wrapped as a text token in the middle of block tokens', () => {
+ const htmlBlockNode = {
+ type: 'htmlBlock',
+ literal: '<div data-tomark-pass ><h1>Some header</h1><p>Some paragraph</p></div>',
+ };
+ const result = buildUneditableHtmlAsTextTokens(htmlBlockNode);
+ const { type, content } = result[1];
+
+ expect(type).toBe('text');
+ expect(content).not.toMatch(/ data-tomark-pass /);
+
+ expect(result).toHaveLength(3);
+ expect(result).toStrictEqual(uneditableBlockTokens);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/mock_data.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/mock_data.js
new file mode 100644
index 00000000000..660c21281fd
--- /dev/null
+++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/mock_data.js
@@ -0,0 +1,58 @@
+// Node spec helpers
+
+export const buildMockTextNode = literal => {
+ return {
+ firstChild: null,
+ literal,
+ type: 'text',
+ };
+};
+
+export const normalTextNode = buildMockTextNode('This is just normal text.');
+
+// Token spec helpers
+
+const buildMockUneditableOpenToken = type => {
+ return {
+ type: 'openTag',
+ tagName: type,
+ attributes: { contenteditable: false },
+ classNames: [
+ 'gl-px-4 gl-py-2 gl-my-5 gl-opacity-5 gl-bg-gray-100 gl-user-select-none gl-cursor-not-allowed',
+ ],
+ };
+};
+
+const buildMockUneditableCloseToken = type => {
+ return { type: 'closeTag', tagName: type };
+};
+
+export const originToken = {
+ type: 'text',
+ tagName: null,
+ content: '{:.no_toc .hidden-md .hidden-lg}',
+};
+export const uneditableCloseToken = buildMockUneditableCloseToken('div');
+export const uneditableOpenTokens = [buildMockUneditableOpenToken('div'), originToken];
+export const uneditableCloseTokens = [originToken, uneditableCloseToken];
+export const uneditableTokens = [...uneditableOpenTokens, uneditableCloseToken];
+
+export const originInlineToken = {
+ type: 'text',
+ content: '<i>Inline</i> content',
+};
+export const uneditableInlineTokens = [
+ buildMockUneditableOpenToken('a'),
+ originInlineToken,
+ buildMockUneditableCloseToken('a'),
+];
+
+export const uneditableBlockTokens = [
+ buildMockUneditableOpenToken('div'),
+ {
+ type: 'text',
+ tagName: null,
+ content: '<div><h1>Some header</h1><p>Some paragraph</p></div>',
+ },
+ buildMockUneditableCloseToken('div'),
+];
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
new file mode 100644
index 00000000000..b723ee8c8a0
--- /dev/null
+++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_embedded_ruby_spec.js
@@ -0,0 +1,30 @@
+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 { buildMockTextNode, normalTextNode } from './mock_data';
+
+const embeddedRubyTextNode = buildMockTextNode('<%= partial("some/path") %>');
+
+describe('Render Embedded Ruby Text renderer', () => {
+ describe('canRender', () => {
+ it('should return true when the argument `literal` has embedded ruby syntax', () => {
+ expect(renderer.canRender(embeddedRubyTextNode)).toBe(true);
+ });
+
+ it('should return false when the argument `literal` lacks embedded ruby syntax', () => {
+ expect(renderer.canRender(normalTextNode)).toBe(false);
+ });
+ });
+
+ describe('render', () => {
+ const origin = jest.fn();
+
+ it('should return uneditable tokens', () => {
+ const context = { origin };
+
+ expect(renderer.render(embeddedRubyTextNode, context)).toStrictEqual(
+ buildUneditableTokens(origin()),
+ );
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_font_awesome_html_inline_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_font_awesome_html_inline_spec.js
new file mode 100644
index 00000000000..d6bb01259bb
--- /dev/null
+++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_font_awesome_html_inline_spec.js
@@ -0,0 +1,33 @@
+import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_font_awesome_html_inline';
+import { buildUneditableInlineTokens } from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token';
+
+import { normalTextNode } from './mock_data';
+
+const fontAwesomeInlineHtmlNode = {
+ firstChild: null,
+ literal: '<i class="far fa-paper-plane" id="biz-tech-icons">',
+ type: 'html',
+};
+
+describe('Render Font Awesome Inline HTML renderer', () => {
+ describe('canRender', () => {
+ it('should return true when the argument `literal` has font awesome inline html syntax', () => {
+ expect(renderer.canRender(fontAwesomeInlineHtmlNode)).toBe(true);
+ });
+
+ it('should return false when the argument `literal` lacks font awesome inline html syntax', () => {
+ expect(renderer.canRender(normalTextNode)).toBe(false);
+ });
+ });
+
+ describe('render', () => {
+ it('should return uneditable inline tokens', () => {
+ const token = { type: 'text', tagName: null, content: fontAwesomeInlineHtmlNode.literal };
+ const context = { origin: () => token };
+
+ expect(renderer.render(fontAwesomeInlineHtmlNode, context)).toStrictEqual(
+ buildUneditableInlineTokens(token),
+ );
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_html_block_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_html_block_spec.js
new file mode 100644
index 00000000000..a6c712eeb31
--- /dev/null
+++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_html_block_spec.js
@@ -0,0 +1,38 @@
+import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_html_block';
+import { buildUneditableHtmlAsTextTokens } from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token';
+
+import { normalTextNode } from './mock_data';
+
+const htmlBlockNode = {
+ firstChild: null,
+ literal: '<div><h1>Heading</h1><p>Paragraph.</p></div>',
+ type: 'htmlBlock',
+};
+
+describe('Render HTML renderer', () => {
+ describe('canRender', () => {
+ it('should return true when the argument is an html block', () => {
+ expect(renderer.canRender(htmlBlockNode)).toBe(true);
+ });
+
+ it('should return false when the argument is not an html block', () => {
+ expect(renderer.canRender(normalTextNode)).toBe(false);
+ });
+ });
+
+ describe('render', () => {
+ const htmlBlockNodeToMark = {
+ firstChild: null,
+ literal: '<div data-to-mark ></div>',
+ type: 'htmlBlock',
+ };
+
+ it.each`
+ node
+ ${htmlBlockNode}
+ ${htmlBlockNodeToMark}
+ `('should return uneditable tokens wrapping the $node as a token', ({ node }) => {
+ expect(renderer.render(node)).toStrictEqual(buildUneditableHtmlAsTextTokens(node));
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_identifier_instance_text_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_identifier_instance_text_spec.js
new file mode 100644
index 00000000000..2897929f1bf
--- /dev/null
+++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_identifier_instance_text_spec.js
@@ -0,0 +1,55 @@
+import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_identifier_instance_text';
+import { buildUneditableInlineTokens } from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token';
+
+import { buildMockTextNode, normalTextNode } from './mock_data';
+
+const mockTextStart = 'Majority example ';
+const mockTextMiddle = '[environment terraform plans][terraform]';
+const mockTextEnd = '.';
+const identifierInstanceStartTextNode = buildMockTextNode(mockTextStart);
+const identifierInstanceEndTextNode = buildMockTextNode(mockTextEnd);
+
+describe('Render Identifier Instance Text renderer', () => {
+ describe('canRender', () => {
+ it.each`
+ node | target
+ ${normalTextNode} | ${false}
+ ${identifierInstanceStartTextNode} | ${false}
+ ${identifierInstanceEndTextNode} | ${false}
+ ${buildMockTextNode(mockTextMiddle)} | ${true}
+ ${buildMockTextNode('Minority example [environment terraform plans][]')} | ${true}
+ ${buildMockTextNode('Minority example [environment terraform plans]')} | ${true}
+ `(
+ 'should return $target when the $node validates against identifier instance syntax',
+ ({ node, target }) => {
+ expect(renderer.canRender(node)).toBe(target);
+ },
+ );
+ });
+
+ describe('render', () => {
+ it.each`
+ start | middle | end
+ ${mockTextStart} | ${mockTextMiddle} | ${mockTextEnd}
+ ${mockTextStart} | ${'[environment terraform plans][]'} | ${mockTextEnd}
+ ${mockTextStart} | ${'[environment terraform plans]'} | ${mockTextEnd}
+ `(
+ 'should return inline editable, uneditable, and editable tokens in sequence',
+ ({ start, middle, end }) => {
+ const buildMockTextToken = content => ({ type: 'text', tagName: null, content });
+
+ const startToken = buildMockTextToken(start);
+ const middleToken = buildMockTextToken(middle);
+ const endToken = buildMockTextToken(end);
+
+ const content = `${start}${middle}${end}`;
+ const contentToken = buildMockTextToken(content);
+ const contentNode = buildMockTextNode(content);
+ const context = { origin: jest.fn().mockReturnValueOnce(contentToken) };
+ expect(renderer.render(contentNode, context)).toStrictEqual(
+ [startToken, buildUneditableInlineTokens(middleToken), endToken].flat(),
+ );
+ },
+ );
+ });
+});
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
new file mode 100644
index 00000000000..320589e4de3
--- /dev/null
+++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph_spec.js
@@ -0,0 +1,65 @@
+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 { buildMockTextNode } from './mock_data';
+
+const buildMockParagraphNode = literal => {
+ return {
+ firstChild: buildMockTextNode(literal),
+ type: 'paragraph',
+ };
+};
+
+const normalParagraphNode = buildMockParagraphNode(
+ 'This is just normal paragraph. It has multiple sentences.',
+);
+const identifierParagraphNode = buildMockParagraphNode(
+ `[another-identifier]: https://example.com "This example has a title" [identifier]: http://example1.com [this link]: http://example2.com`,
+);
+
+describe('Render Identifier Paragraph renderer', () => {
+ describe('canRender', () => {
+ it.each`
+ node | paragraph | target
+ ${identifierParagraphNode} | ${'[Some text]: https://link.com'} | ${true}
+ ${normalParagraphNode} | ${'Normal non-identifier text. Another sentence.'} | ${false}
+ `(
+ 'should return $target when the $node matches $paragraph syntax',
+ ({ node, paragraph, target }) => {
+ const context = {
+ entering: true,
+ getChildrenText: jest.fn().mockReturnValueOnce(paragraph),
+ };
+
+ expect(renderer.canRender(node, context)).toBe(target);
+ },
+ );
+ });
+
+ 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()),
+ );
+ });
+ });
+});
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
new file mode 100644
index 00000000000..e60bf1c8c92
--- /dev/null
+++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list_spec.js
@@ -0,0 +1,55 @@
+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 { buildMockTextNode } from './mock_data';
+
+const buildMockListNode = literal => {
+ return {
+ firstChild: {
+ firstChild: {
+ firstChild: buildMockTextNode(literal),
+ type: 'paragraph',
+ },
+ type: 'item',
+ },
+ type: 'list',
+ };
+};
+
+const normalListNode = buildMockListNode('Just another bullet point');
+const kramdownListNode = buildMockListNode('TOC');
+
+describe('Render Kramdown List renderer', () => {
+ describe('canRender', () => {
+ it('should return true when the argument is a special kramdown TOC ordered/unordered list', () => {
+ expect(renderer.canRender(kramdownListNode)).toBe(true);
+ });
+
+ it('should return false when the argument is a normal ordered/unordered list', () => {
+ expect(renderer.canRender(normalListNode)).toBe(false);
+ });
+ });
+
+ 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()),
+ );
+ });
+ });
+});
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
new file mode 100644
index 00000000000..97ff9794e69
--- /dev/null
+++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text_spec.js
@@ -0,0 +1,30 @@
+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 { buildMockTextNode, normalTextNode } from './mock_data';
+
+const kramdownTextNode = buildMockTextNode('{:toc}');
+
+describe('Render Kramdown Text renderer', () => {
+ describe('canRender', () => {
+ it('should return true when the argument `literal` has kramdown syntax', () => {
+ expect(renderer.canRender(kramdownTextNode)).toBe(true);
+ });
+
+ it('should return false when the argument `literal` lacks kramdown syntax', () => {
+ expect(renderer.canRender(normalTextNode)).toBe(false);
+ });
+ });
+
+ describe('render', () => {
+ const origin = jest.fn();
+
+ it('should return uneditable tokens', () => {
+ const context = { origin };
+
+ expect(renderer.render(kramdownTextNode, context)).toStrictEqual(
+ buildUneditableTokens(origin()),
+ );
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js
index d02d924bd2b..79851e5db05 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js
@@ -82,10 +82,9 @@ describe('DropdownButtonComponent', () => {
});
it('renders dropdown button icon', () => {
- const dropdownIconEl = vm.$el.querySelector('i.fa');
+ const dropdownIconEl = vm.$el.querySelector('.dropdown-menu-toggle .gl-icon');
expect(dropdownIconEl).not.toBeNull();
- expect(dropdownIconEl.classList.contains('fa-chevron-down')).toBe(true);
});
});
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js
index 035af946d75..510e537b1cd 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js
@@ -29,13 +29,11 @@ describe('DropdownSearchInputComponent', () => {
});
it('renders search icon element', () => {
- expect(vm.$el.querySelector('.fa-search.dropdown-input-search')).not.toBeNull();
+ expect(vm.$el.querySelector('.dropdown-input-search')).not.toBeNull();
});
it('renders clear search icon element', () => {
- expect(
- vm.$el.querySelector('.fa-times.dropdown-input-clear.js-dropdown-input-clear'),
- ).not.toBeNull();
+ expect(vm.$el.querySelector('.dropdown-input-clear.js-dropdown-input-clear')).not.toBeNull();
});
});
});
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 214eb239432..68c9d26bb1a 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
@@ -1,18 +1,19 @@
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
-import { GlIcon } from '@gitlab/ui';
+import { GlIcon, GlButton } from '@gitlab/ui';
import DropdownButton from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue';
import labelSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store';
import { mockConfig } from './mock_data';
+let store;
const localVue = createLocalVue();
localVue.use(Vuex);
const createComponent = (initialState = mockConfig) => {
- const store = new Vuex.Store(labelSelectModule());
+ store = new Vuex.Store(labelSelectModule());
store.dispatch('setInitialState', initialState);
@@ -33,26 +34,32 @@ describe('DropdownButton', () => {
wrapper.destroy();
});
+ const findDropdownButton = () => wrapper.find(GlButton);
+ const findDropdownText = () => wrapper.find('.dropdown-toggle-text');
+ const findDropdownIcon = () => wrapper.find(GlIcon);
+
describe('methods', () => {
describe('handleButtonClick', () => {
- it('calls action `toggleDropdownContents` and stops event propagation when `state.variant` is "standalone"', () => {
- const event = {
- stopPropagation: jest.fn(),
- };
- wrapper = createComponent({
- ...mockConfig,
- variant: 'standalone',
- });
-
- jest.spyOn(wrapper.vm, 'toggleDropdownContents');
-
- wrapper.vm.handleButtonClick(event);
-
- expect(wrapper.vm.toggleDropdownContents).toHaveBeenCalled();
- expect(event.stopPropagation).toHaveBeenCalled();
-
- wrapper.destroy();
- });
+ it.each`
+ variant
+ ${'standalone'}
+ ${'embedded'}
+ `(
+ 'toggles dropdown content and stops event propagation when `state.variant` is "$variant"',
+ ({ variant }) => {
+ const event = { stopPropagation: jest.fn() };
+
+ wrapper = createComponent({
+ ...mockConfig,
+ variant,
+ });
+
+ findDropdownButton().vm.$emit('click', event);
+
+ expect(store.state.showDropdownContents).toBe(true);
+ expect(event.stopPropagation).toHaveBeenCalled();
+ },
+ );
});
});
@@ -61,15 +68,24 @@ describe('DropdownButton', () => {
expect(wrapper.is('gl-button-stub')).toBe(true);
});
- it('renders button text element', () => {
- const dropdownTextEl = wrapper.find('.dropdown-toggle-text');
+ it('renders default button text element', () => {
+ const dropdownTextEl = findDropdownText();
expect(dropdownTextEl.exists()).toBe(true);
expect(dropdownTextEl.text()).toBe('Label');
});
+ it('renders provided button text element', () => {
+ store.state.dropdownButtonText = 'Custom label';
+ const dropdownTextEl = findDropdownText();
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(dropdownTextEl.text()).toBe('Custom label');
+ });
+ });
+
it('renders chevron icon element', () => {
- const iconEl = wrapper.find(GlIcon);
+ const iconEl = findDropdownIcon();
expect(iconEl.exists()).toBe(true);
expect(iconEl.props('name')).toBe('chevron-down');
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 1504e1521d3..9b01e0b9637 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
@@ -44,6 +44,7 @@ const createComponent = (initialState = mockConfig) => {
describe('DropdownContentsLabelsView', () => {
let wrapper;
let wrapperStandalone;
+ let wrapperEmbedded;
beforeEach(() => {
wrapper = createComponent();
@@ -51,11 +52,16 @@ describe('DropdownContentsLabelsView', () => {
...mockConfig,
variant: 'standalone',
});
+ wrapperEmbedded = createComponent({
+ ...mockConfig,
+ variant: 'embedded',
+ });
});
afterEach(() => {
wrapper.destroy();
wrapperStandalone.destroy();
+ wrapperEmbedded.destroy();
});
describe('computed', () => {
@@ -211,6 +217,10 @@ describe('DropdownContentsLabelsView', () => {
expect(wrapperStandalone.find('.dropdown-title').exists()).toBe(false);
});
+ it('renders dropdown title element when `state.variant` is "embedded"', () => {
+ expect(wrapperEmbedded.find('.dropdown-title').exists()).toBe(true);
+ });
+
it('renders dropdown close button element', () => {
const closeButtonEl = wrapper.find('.dropdown-title').find(GlButton);
@@ -291,5 +301,9 @@ describe('DropdownContentsLabelsView', () => {
it('does not render footer list items when `state.variant` is "standalone"', () => {
expect(wrapperStandalone.find('.dropdown-footer').exists()).toBe(false);
});
+
+ it('renders footer list items when `state.variant` is "embedded"', () => {
+ expect(wrapperEmbedded.find('.dropdown-footer').exists()).toBe(true);
+ });
});
});
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 ee4e9090e5d..6e97b046be2 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
@@ -89,18 +89,23 @@ describe('LabelsSelectRoot', () => {
expect(wrapper.attributes('class')).toContain('labels-select-wrapper position-relative');
});
- it('renders component root element with CSS class `is-standalone` when `state.variant` is "standalone"', () => {
- const wrapperStandalone = createComponent({
- ...mockConfig,
- variant: 'standalone',
- });
-
- return wrapperStandalone.vm.$nextTick(() => {
- expect(wrapperStandalone.classes()).toContain('is-standalone');
-
- wrapperStandalone.destroy();
- });
- });
+ it.each`
+ variant | cssClass
+ ${'standalone'} | ${'is-standalone'}
+ ${'embedded'} | ${'is-embedded'}
+ `(
+ 'renders component root element with CSS class `$cssClass` when `state.variant` is "$variant"',
+ ({ variant, cssClass }) => {
+ wrapper = createComponent({
+ ...mockConfig,
+ variant,
+ });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.classes()).toContain(cssClass);
+ });
+ },
+ );
it('renders `dropdown-value-collapsed` component when `allowLabelCreate` prop is `true`', () => {
expect(wrapper.find(DropdownValueCollapsed).exists()).toBe(true);
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/getters_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/getters_spec.js
index b866117efcf..52116f757c5 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/getters_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/getters_spec.js
@@ -2,13 +2,20 @@ import * as getters from '~/vue_shared/components/sidebar/labels_select_vue/stor
describe('LabelsSelect Getters', () => {
describe('dropdownButtonText', () => {
- it('returns string "Label" when state.labels has no selected labels', () => {
- const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
-
- expect(getters.dropdownButtonText({ labels }, { isDropdownVariantSidebar: true })).toBe(
- 'Label',
- );
- });
+ it.each`
+ labelType | dropdownButtonText | expected
+ ${'default'} | ${''} | ${'Label'}
+ ${'custom'} | ${'Custom label'} | ${'Custom label'}
+ `(
+ 'returns $labelType text when state.labels has no selected labels',
+ ({ dropdownButtonText, expected }) => {
+ const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
+ const selectedLabels = [];
+ const state = { labels, selectedLabels, dropdownButtonText };
+
+ expect(getters.dropdownButtonText(state, {})).toBe(expected);
+ },
+ );
it('returns label title when state.labels has only 1 label', () => {
const labels = [{ id: 1, title: 'Foobar', set: true }];
diff --git a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
index 2c7fce714f0..a4ff6ac0c16 100644
--- a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
+++ b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
@@ -4,7 +4,6 @@ import UserPopover from '~/vue_shared/components/user_popover/user_popover.vue';
import Icon from '~/vue_shared/components/icon.vue';
const DEFAULT_PROPS = {
- loaded: true,
user: {
username: 'root',
name: 'Administrator',
@@ -12,6 +11,7 @@ const DEFAULT_PROPS = {
bio: null,
workInformation: null,
status: null,
+ loaded: true,
},
};
@@ -46,28 +46,21 @@ describe('User Popover Component', () => {
});
};
- describe('Empty', () => {
- beforeEach(() => {
- createWrapper(
- {},
- {
- propsData: {
- target: findTarget(),
- user: {
- name: null,
- username: null,
- location: null,
- bio: null,
- workInformation: null,
- status: null,
- },
- },
+ describe('when user is loading', () => {
+ it('displays skeleton loaders', () => {
+ createWrapper({
+ user: {
+ name: null,
+ username: null,
+ location: null,
+ bio: null,
+ workInformation: null,
+ status: null,
+ loaded: false,
},
- );
- });
+ });
- it('should return skeleton loaders', () => {
- expect(wrapper.find(GlSkeletonLoading).exists()).toBe(true);
+ expect(wrapper.findAll(GlSkeletonLoading)).toHaveLength(4);
});
});
@@ -90,9 +83,10 @@ describe('User Popover Component', () => {
describe('job data', () => {
const findWorkInformation = () => wrapper.find({ ref: 'workInformation' });
const findBio = () => wrapper.find({ ref: 'bio' });
+ const bio = 'My super interesting bio';
it('should show only bio if work information is not available', () => {
- const user = { ...DEFAULT_PROPS.user, bio: 'My super interesting bio' };
+ const user = { ...DEFAULT_PROPS.user, bio, bioHtml: bio };
createWrapper({ user });
@@ -114,7 +108,8 @@ describe('User Popover Component', () => {
it('should display bio and work information in separate lines', () => {
const user = {
...DEFAULT_PROPS.user,
- bio: 'My super interesting bio',
+ bio,
+ bioHtml: bio,
workInformation: 'Frontend Engineer at GitLab',
};
@@ -127,12 +122,13 @@ describe('User Popover Component', () => {
it('should not encode special characters in bio', () => {
const user = {
...DEFAULT_PROPS.user,
- bio: 'I like <html> & CSS',
+ bio: 'I like CSS',
+ bioHtml: 'I like <b>CSS</b>',
};
createWrapper({ user });
- expect(findBio().text()).toBe('I like <html> & CSS');
+ expect(findBio().html()).toContain('I like <b>CSS</b>');
});
it('shows icon for bio', () => {