summaryrefslogtreecommitdiff
path: root/spec/frontend/vue_shared
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-10-20 09:40:42 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2022-10-20 09:40:42 +0000
commitee664acb356f8123f4f6b00b73c1e1cf0866c7fb (patch)
treef8479f94a28f66654c6a4f6fb99bad6b4e86a40e /spec/frontend/vue_shared
parent62f7d5c5b69180e82ae8196b7b429eeffc8e7b4f (diff)
downloadgitlab-ce-ee664acb356f8123f4f6b00b73c1e1cf0866c7fb.tar.gz
Add latest changes from gitlab-org/gitlab@15-5-stable-eev15.5.0-rc42
Diffstat (limited to 'spec/frontend/vue_shared')
-rw-r--r--spec/frontend/vue_shared/components/ci_badge_link_spec.js12
-rw-r--r--spec/frontend/vue_shared/components/color_select_dropdown/color_select_root_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/date_time_picker/date_time_picker_lib_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/diff_viewer/diff_viewer_spec.js51
-rw-r--r--spec/frontend/vue_shared/components/file_finder/item_spec.js118
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/actions_spec.js16
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/gitlab_version_check_spec.js33
-rw-r--r--spec/frontend/vue_shared/components/gl_countdown_spec.js36
-rw-r--r--spec/frontend/vue_shared/components/group_select/utils_spec.js24
-rw-r--r--spec/frontend/vue_shared/components/markdown/header_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js289
-rw-r--r--spec/frontend/vue_shared/components/metric_images/metric_images_table_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/metric_images/store/actions_spec.js10
-rw-r--r--spec/frontend/vue_shared/components/modal_copy_button_spec.js7
-rw-r--r--spec/frontend/vue_shared/components/namespace_select/namespace_select_deprecated_spec.js (renamed from spec/frontend/vue_shared/components/namespace_select/namespace_select_spec.js)18
-rw-r--r--spec/frontend/vue_shared/components/notes/__snapshots__/placeholder_note_spec.js.snap23
-rw-r--r--spec/frontend/vue_shared/components/notes/placeholder_note_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/pagination_bar/pagination_bar_spec.js26
-rw-r--r--spec/frontend/vue_shared/components/panel_resizer_spec.js81
-rw-r--r--spec/frontend/vue_shared/components/registry/__snapshots__/history_item_spec.js.snap8
-rw-r--r--spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js11
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js12
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js10
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/components/chunk_line_spec.js22
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/highlight_util_spec.js44
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/index_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/wrap_bidi_chars_spec.js17
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/wrap_child_nodes_spec.js22
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/wrap_comments_spec.js29
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js21
-rw-r--r--spec/frontend/vue_shared/components/stacked_progress_bar_spec.js124
-rw-r--r--spec/frontend/vue_shared/components/timezone_dropdown/helpers.js6
-rw-r--r--spec/frontend/vue_shared/components/timezone_dropdown/timezone_dropdown_spec.js111
-rw-r--r--spec/frontend/vue_shared/components/url_sync_spec.js62
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_image_new_spec.js134
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_image_old_spec.js127
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_image_spec.js143
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_link_new_spec.js103
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_link_old_spec.js103
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js119
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js25
-rw-r--r--spec/frontend/vue_shared/components/user_popover/user_popover_spec.js70
-rw-r--r--spec/frontend/vue_shared/directives/safe_html_spec.js116
-rw-r--r--spec/frontend/vue_shared/issuable/__snapshots__/issuable_blocked_icon_spec.js.snap30
-rw-r--r--spec/frontend/vue_shared/issuable/issuable_blocked_icon_spec.js265
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js8
-rw-r--r--spec/frontend/vue_shared/security_reports/security_reports_app_spec.js6
61 files changed, 1634 insertions, 998 deletions
diff --git a/spec/frontend/vue_shared/components/ci_badge_link_spec.js b/spec/frontend/vue_shared/components/ci_badge_link_spec.js
index 27b6718fb8e..07cbfe1e79b 100644
--- a/spec/frontend/vue_shared/components/ci_badge_link_spec.js
+++ b/spec/frontend/vue_shared/components/ci_badge_link_spec.js
@@ -1,7 +1,7 @@
+import { GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
-import { visitUrl } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn(),
@@ -86,18 +86,14 @@ describe('CI Badge Link Component', () => {
wrapper.destroy();
});
- it.each(Object.keys(statuses))('should render badge for status: %s', async (status) => {
+ it.each(Object.keys(statuses))('should render badge for status: %s', (status) => {
createComponent({ status: statuses[status] });
- expect(wrapper.attributes('href')).toBe();
+ expect(wrapper.attributes('href')).toBe(statuses[status].details_path);
expect(wrapper.text()).toBe(statuses[status].text);
expect(wrapper.classes()).toContain('ci-status');
expect(wrapper.classes()).toContain(`ci-${statuses[status].group}`);
expect(findIcon().exists()).toBe(true);
-
- await wrapper.trigger('click');
-
- expect(visitUrl).toHaveBeenCalledWith(statuses[status].details_path);
});
it('should not render label', () => {
@@ -109,7 +105,7 @@ describe('CI Badge Link Component', () => {
it('should emit ciStatusBadgeClick event', async () => {
createComponent({ status: statuses.success });
- await wrapper.trigger('click');
+ await wrapper.findComponent(GlLink).vm.$emit('click');
expect(wrapper.emitted('ciStatusBadgeClick')).toEqual([[]]);
});
diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/color_select_root_spec.js b/spec/frontend/vue_shared/components/color_select_dropdown/color_select_root_spec.js
index 441e21ee905..5b0772f6e34 100644
--- a/spec/frontend/vue_shared/components/color_select_dropdown/color_select_root_spec.js
+++ b/spec/frontend/vue_shared/components/color_select_dropdown/color_select_root_spec.js
@@ -3,7 +3,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import DropdownContents from '~/vue_shared/components/color_select_dropdown/dropdown_contents.vue';
import DropdownValue from '~/vue_shared/components/color_select_dropdown/dropdown_value.vue';
@@ -146,7 +146,7 @@ describe('LabelsSelectRoot', () => {
});
it('creates flash with error message', () => {
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
captureError: true,
message: 'Error fetching epic color.',
});
@@ -186,7 +186,7 @@ describe('LabelsSelectRoot', () => {
findDropdownContents().vm.$emit('setColor', color);
await waitForPromises();
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
captureError: true,
error: expect.anything(),
message: 'An error occurred while updating color.',
diff --git a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_lib_spec.js b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_lib_spec.js
index 10eacff630d..7a8f94b3746 100644
--- a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_lib_spec.js
+++ b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_lib_spec.js
@@ -121,7 +121,7 @@ describe('date time picker lib', () => {
const utcResult = '2019-09-08T01:01:01Z';
const localResult = '2019-09-08T08:01:01Z';
- test.each`
+ it.each`
val | locatTimezone | utc | result
${value} | ${'UTC'} | ${undefined} | ${utcResult}
${value} | ${'UTC'} | ${false} | ${utcResult}
@@ -167,7 +167,7 @@ describe('date time picker lib', () => {
const utcResult = '2019-09-08 08:01:01';
const localResult = '2019-09-08 01:01:01';
- test.each`
+ it.each`
val | locatTimezone | utc | result
${value} | ${'UTC'} | ${undefined} | ${utcResult}
${value} | ${'UTC'} | ${false} | ${utcResult}
diff --git a/spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js b/spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js
index 68684004b82..99c973bdd26 100644
--- a/spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js
+++ b/spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js
@@ -106,11 +106,11 @@ describe('Diff Stats Dropdown', () => {
expectedAddedDeletedExpanded,
expectedAddedDeletedCollapsed,
}) => {
- beforeAll(() => {
+ beforeEach(() => {
createComponent({ changed, added, deleted });
});
- afterAll(() => {
+ afterEach(() => {
wrapper.destroy();
});
diff --git a/spec/frontend/vue_shared/components/diff_viewer/diff_viewer_spec.js b/spec/frontend/vue_shared/components/diff_viewer/diff_viewer_spec.js
index 69964b2687d..6e0717c29d7 100644
--- a/spec/frontend/vue_shared/components/diff_viewer/diff_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/diff_viewer/diff_viewer_spec.js
@@ -1,8 +1,6 @@
-import Vue, { nextTick } from 'vue';
-
-import mountComponent from 'helpers/vue_mount_component_helper';
+import { mount } from '@vue/test-utils';
import { GREEN_BOX_IMAGE_URL, RED_BOX_IMAGE_URL } from 'spec/test_constants';
-import diffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
+import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
describe('DiffViewer', () => {
const requiredProps = {
@@ -14,37 +12,28 @@ describe('DiffViewer', () => {
oldPath: RED_BOX_IMAGE_URL,
oldSha: 'DEF',
};
- let vm;
-
- function createComponent(props) {
- const DiffViewer = Vue.extend(diffViewer);
+ let wrapper;
- vm = mountComponent(DiffViewer, props);
+ function createComponent(propsData) {
+ wrapper = mount(DiffViewer, { propsData });
}
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
- it('renders image diff', async () => {
+ it('renders image diff', () => {
window.gon = {
relative_url_root: '',
};
createComponent({ ...requiredProps, projectPath: '' });
- await nextTick();
-
- expect(vm.$el.querySelector('.deleted img').getAttribute('src')).toBe(
- `//-/raw/DEF/${RED_BOX_IMAGE_URL}`,
- );
-
- expect(vm.$el.querySelector('.added img').getAttribute('src')).toBe(
- `//-/raw/ABC/${GREEN_BOX_IMAGE_URL}`,
- );
+ expect(wrapper.find('.deleted img').attributes('src')).toBe(`//-/raw/DEF/${RED_BOX_IMAGE_URL}`);
+ expect(wrapper.find('.added img').attributes('src')).toBe(`//-/raw/ABC/${GREEN_BOX_IMAGE_URL}`);
});
- it('renders fallback download diff display', async () => {
+ it('renders fallback download diff display', () => {
createComponent({
...requiredProps,
diffViewerMode: 'added',
@@ -52,18 +41,10 @@ describe('DiffViewer', () => {
oldPath: 'testold.abc',
});
- await nextTick();
-
- expect(vm.$el.querySelector('.deleted .file-info').textContent.trim()).toContain('testold.abc');
-
- expect(vm.$el.querySelector('.deleted .btn.btn-default').textContent.trim()).toContain(
- 'Download',
- );
-
- expect(vm.$el.querySelector('.added .file-info').textContent.trim()).toContain('test.abc');
- expect(vm.$el.querySelector('.added .btn.btn-default').textContent.trim()).toContain(
- 'Download',
- );
+ expect(wrapper.find('.deleted .file-info').text()).toContain('testold.abc');
+ expect(wrapper.find('.deleted .btn.btn-default').text()).toContain('Download');
+ expect(wrapper.find('.added .file-info').text()).toContain('test.abc');
+ expect(wrapper.find('.added .btn.btn-default').text()).toContain('Download');
});
describe('renamed file', () => {
@@ -85,7 +66,7 @@ describe('DiffViewer', () => {
oldPath: 'testold.abc',
});
- expect(vm.$el.textContent).toContain('File renamed with no changes.');
+ expect(wrapper.text()).toContain('File renamed with no changes.');
});
});
@@ -99,6 +80,6 @@ describe('DiffViewer', () => {
bMode: '321',
});
- expect(vm.$el.textContent).toContain('File mode changed from 123 to 321');
+ expect(wrapper.text()).toContain('File mode changed from 123 to 321');
});
});
diff --git a/spec/frontend/vue_shared/components/file_finder/item_spec.js b/spec/frontend/vue_shared/components/file_finder/item_spec.js
index b69c33055c1..f0998b1b5c6 100644
--- a/spec/frontend/vue_shared/components/file_finder/item_spec.js
+++ b/spec/frontend/vue_shared/components/file_finder/item_spec.js
@@ -1,127 +1,119 @@
-import Vue, { nextTick } from 'vue';
-import createComponent from 'helpers/vue_mount_component_helper';
+import { mount } from '@vue/test-utils';
import { file } from 'jest/ide/helpers';
import ItemComponent from '~/vue_shared/components/file_finder/item.vue';
describe('File finder item spec', () => {
- const Component = Vue.extend(ItemComponent);
- let vm;
- let localFile;
-
- beforeEach(() => {
- localFile = {
- ...file(),
- name: 'test file',
- path: 'test/file',
- };
-
- vm = createComponent(Component, {
- file: localFile,
- focused: true,
- searchText: '',
- index: 0,
+ let wrapper;
+
+ const createComponent = ({ file: customFileFields = {}, ...otherProps } = {}) => {
+ wrapper = mount(ItemComponent, {
+ propsData: {
+ file: {
+ ...file(),
+ name: 'test file',
+ path: 'test/file',
+ ...customFileFields,
+ },
+ focused: true,
+ searchText: '',
+ index: 0,
+ ...otherProps,
+ },
});
- });
+ };
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
it('renders file name & path', () => {
- expect(vm.$el.textContent).toContain('test file');
- expect(vm.$el.textContent).toContain('test/file');
+ createComponent();
+
+ expect(wrapper.text()).toContain('test file');
+ expect(wrapper.text()).toContain('test/file');
});
describe('focused', () => {
it('adds is-focused class', () => {
- expect(vm.$el.classList).toContain('is-focused');
+ createComponent();
+
+ expect(wrapper.classes()).toContain('is-focused');
});
it('does not have is-focused class when not focused', async () => {
- vm.focused = false;
+ createComponent({ focused: false });
- await nextTick();
- expect(vm.$el.classList).not.toContain('is-focused');
+ expect(wrapper.classes()).not.toContain('is-focused');
});
});
describe('changed file icon', () => {
it('does not render when not a changed or temp file', () => {
- expect(vm.$el.querySelector('.diff-changed-stats')).toBe(null);
+ createComponent();
+
+ expect(wrapper.find('.diff-changed-stats').exists()).toBe(false);
});
it('renders when a changed file', async () => {
- vm.file.changed = true;
+ createComponent({ file: { changed: true } });
- await nextTick();
- expect(vm.$el.querySelector('.diff-changed-stats')).not.toBe(null);
+ expect(wrapper.find('.diff-changed-stats').exists()).toBe(true);
});
it('renders when a temp file', async () => {
- vm.file.tempFile = true;
+ createComponent({ file: { tempFile: true } });
- await nextTick();
- expect(vm.$el.querySelector('.diff-changed-stats')).not.toBe(null);
+ expect(wrapper.find('.diff-changed-stats').exists()).toBe(true);
});
});
- it('emits event when clicked', () => {
- jest.spyOn(vm, '$emit').mockImplementation(() => {});
+ it('emits event when clicked', async () => {
+ createComponent();
- vm.$el.click();
+ await wrapper.find('*').trigger('click');
- expect(vm.$emit).toHaveBeenCalledWith('click', vm.file);
+ expect(wrapper.emitted('click')[0]).toStrictEqual([wrapper.props('file')]);
});
describe('path', () => {
- let el;
-
- beforeEach(async () => {
- vm.searchText = 'file';
-
- el = vm.$el.querySelector('.diff-changed-file-path');
-
- nextTick();
- });
+ const findChangedFilePath = () => wrapper.find('.diff-changed-file-path');
it('highlights text', () => {
- expect(el.querySelectorAll('.highlighted').length).toBe(4);
+ createComponent({ searchText: 'file' });
+
+ expect(findChangedFilePath().findAll('.highlighted')).toHaveLength(4);
});
it('adds ellipsis to long text', async () => {
- vm.file.path = new Array(70)
+ const path = new Array(70)
.fill()
.map((_, i) => `${i}-`)
.join('');
- await nextTick();
- expect(el.textContent).toBe(`...${vm.file.path.substr(vm.file.path.length - 60)}`);
+ createComponent({ searchText: 'file', file: { path } });
+
+ expect(findChangedFilePath().text()).toBe(`...${path.substring(path.length - 60)}`);
});
});
describe('name', () => {
- let el;
-
- beforeEach(async () => {
- vm.searchText = 'file';
-
- el = vm.$el.querySelector('.diff-changed-file-name');
-
- await nextTick();
- });
+ const findChangedFileName = () => wrapper.find('.diff-changed-file-name');
it('highlights text', () => {
- expect(el.querySelectorAll('.highlighted').length).toBe(4);
+ createComponent({ searchText: 'file' });
+
+ expect(findChangedFileName().findAll('.highlighted')).toHaveLength(4);
});
it('does not add ellipsis to long text', async () => {
- vm.file.name = new Array(70)
+ const name = new Array(70)
.fill()
.map((_, i) => `${i}-`)
.join('');
- await nextTick();
- expect(el.textContent).not.toBe(`...${vm.file.name.substr(vm.file.name.length - 60)}`);
+ createComponent({ searchText: 'file', file: { name } });
+
+ expect(findChangedFileName().text()).not.toBe(`...${name.substring(name.length - 60)}`);
});
});
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/actions_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/actions_spec.js
index 4140ec09b4e..66ef473f368 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/actions_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/actions_spec.js
@@ -3,7 +3,7 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import { mockBranches } from 'jest/vue_shared/components/filtered_search_bar/mock_data';
import Api from '~/api';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import httpStatusCodes from '~/lib/utils/http_status';
import * as actions from '~/vue_shared/components/filtered_search_bar/store/modules/filters/actions';
import * as types from '~/vue_shared/components/filtered_search_bar/store/modules/filters/mutation_types';
@@ -159,7 +159,7 @@ describe('Filters actions', () => {
},
],
[],
- ).then(() => expect(createFlash).toHaveBeenCalled());
+ ).then(() => expect(createAlert).toHaveBeenCalled());
});
});
});
@@ -233,7 +233,7 @@ describe('Filters actions', () => {
[],
).then(() => {
expect(mock.history.get[0].url).toBe('/api/v1/groups/fake_group_endpoint/members');
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
});
});
@@ -252,7 +252,7 @@ describe('Filters actions', () => {
[],
).then(() => {
expect(mock.history.get[0].url).toBe('/api/v1/projects/fake_project_endpoint/users');
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
});
});
});
@@ -298,7 +298,7 @@ describe('Filters actions', () => {
},
],
[],
- ).then(() => expect(createFlash).toHaveBeenCalled());
+ ).then(() => expect(createAlert).toHaveBeenCalled());
});
});
});
@@ -376,7 +376,7 @@ describe('Filters actions', () => {
[],
).then(() => {
expect(mock.history.get[0].url).toBe('/api/v1/groups/fake_group_endpoint/members');
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
});
});
@@ -395,7 +395,7 @@ describe('Filters actions', () => {
[],
).then(() => {
expect(mock.history.get[0].url).toBe('/api/v1/projects/fake_project_endpoint/users');
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
});
});
});
@@ -441,7 +441,7 @@ describe('Filters actions', () => {
},
],
[],
- ).then(() => expect(createFlash).toHaveBeenCalled());
+ ).then(() => expect(createAlert).toHaveBeenCalled());
});
});
});
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 302dfabffb2..5371b9af475 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
@@ -8,7 +8,7 @@ import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { DEFAULT_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants';
@@ -140,13 +140,13 @@ describe('AuthorToken', () => {
});
});
- it('calls `createFlash` with flash error message when request fails', () => {
+ it('calls `createAlert` with flash error message when request fails', () => {
jest.spyOn(wrapper.vm.config, 'fetchAuthors').mockRejectedValue({});
getBaseToken().vm.$emit('fetch-suggestions', 'root');
return waitForPromises().then(() => {
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: 'There was a problem fetching users.',
});
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js
index 1de35daa3a5..05b42011fe1 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js
@@ -9,7 +9,7 @@ import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { DEFAULT_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants';
import BranchToken from '~/vue_shared/components/filtered_search_bar/tokens/branch_token.vue';
@@ -87,13 +87,13 @@ describe('BranchToken', () => {
});
});
- it('calls `createFlash` with flash error message when request fails', () => {
+ it('calls `createAlert` with flash error message when request fails', () => {
jest.spyOn(wrapper.vm.config, 'fetchBranches').mockRejectedValue({});
wrapper.vm.fetchBranches('foo');
return waitForPromises().then(() => {
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: 'There was a problem fetching branches.',
});
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js
index c9879987931..5b744521979 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js
@@ -8,7 +8,7 @@ import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { DEFAULT_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
@@ -94,7 +94,7 @@ describe('CrmContactToken', () => {
getBaseToken().vm.$emit('fetch-suggestions', 'foo');
await waitForPromises();
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
expect(searchGroupCrmContactsQueryHandler).toHaveBeenCalledWith({
fullPath: 'group',
isProject: false,
@@ -108,7 +108,7 @@ describe('CrmContactToken', () => {
getBaseToken().vm.$emit('fetch-suggestions', '5');
await waitForPromises();
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
expect(searchGroupCrmContactsQueryHandler).toHaveBeenCalledWith({
fullPath: 'group',
isProject: false,
@@ -134,7 +134,7 @@ describe('CrmContactToken', () => {
getBaseToken().vm.$emit('fetch-suggestions', 'foo');
await waitForPromises();
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
expect(searchProjectCrmContactsQueryHandler).toHaveBeenCalledWith({
fullPath: 'project',
isProject: true,
@@ -148,7 +148,7 @@ describe('CrmContactToken', () => {
getBaseToken().vm.$emit('fetch-suggestions', '5');
await waitForPromises();
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
expect(searchProjectCrmContactsQueryHandler).toHaveBeenCalledWith({
fullPath: 'project',
isProject: true,
@@ -159,7 +159,7 @@ describe('CrmContactToken', () => {
});
});
- it('calls `createFlash` with flash error message when request fails', async () => {
+ it('calls `createAlert` with flash error message when request fails', async () => {
mountComponent();
jest.spyOn(wrapper.vm.$apollo, 'query').mockRejectedValue({});
@@ -167,7 +167,7 @@ describe('CrmContactToken', () => {
getBaseToken().vm.$emit('fetch-suggestions');
await waitForPromises();
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: 'There was a problem fetching CRM contacts.',
});
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js
index 16333b052e6..3a3e96032e8 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js
@@ -8,7 +8,7 @@ import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { DEFAULT_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
@@ -93,7 +93,7 @@ describe('CrmOrganizationToken', () => {
getBaseToken().vm.$emit('fetch-suggestions', 'foo');
await waitForPromises();
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
expect(searchGroupCrmOrganizationsQueryHandler).toHaveBeenCalledWith({
fullPath: 'group',
isProject: false,
@@ -107,7 +107,7 @@ describe('CrmOrganizationToken', () => {
getBaseToken().vm.$emit('fetch-suggestions', '5');
await waitForPromises();
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
expect(searchGroupCrmOrganizationsQueryHandler).toHaveBeenCalledWith({
fullPath: 'group',
isProject: false,
@@ -133,7 +133,7 @@ describe('CrmOrganizationToken', () => {
getBaseToken().vm.$emit('fetch-suggestions', 'foo');
await waitForPromises();
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
expect(searchProjectCrmOrganizationsQueryHandler).toHaveBeenCalledWith({
fullPath: 'project',
isProject: true,
@@ -147,7 +147,7 @@ describe('CrmOrganizationToken', () => {
getBaseToken().vm.$emit('fetch-suggestions', '5');
await waitForPromises();
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
expect(searchProjectCrmOrganizationsQueryHandler).toHaveBeenCalledWith({
fullPath: 'project',
isProject: true,
@@ -158,7 +158,7 @@ describe('CrmOrganizationToken', () => {
});
});
- it('calls `createFlash` with flash error message when request fails', async () => {
+ it('calls `createAlert` with flash error message when request fails', async () => {
mountComponent();
jest.spyOn(wrapper.vm.$apollo, 'query').mockRejectedValue({});
@@ -166,7 +166,7 @@ describe('CrmOrganizationToken', () => {
getBaseToken().vm.$emit('fetch-suggestions');
await waitForPromises();
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: 'There was a problem fetching CRM organizations.',
});
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js
index bf4a6eb7635..e8436d2db17 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js
@@ -8,7 +8,7 @@ import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import {
@@ -93,13 +93,13 @@ describe('EmojiToken', () => {
});
});
- it('calls `createFlash` with flash error message when request fails', () => {
+ it('calls `createAlert` with flash error message when request fails', () => {
jest.spyOn(wrapper.vm.config, 'fetchEmojis').mockRejectedValue({});
wrapper.vm.fetchEmojis('foo');
return waitForPromises().then(() => {
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: 'There was a problem fetching emojis.',
});
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
index 01e281884ed..8ca12afacec 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
@@ -11,7 +11,7 @@ import {
mockRegularLabel,
mockLabels,
} from 'jest/vue_shared/components/sidebar/labels_select_vue/mock_data';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { DEFAULT_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants';
@@ -116,13 +116,13 @@ describe('LabelToken', () => {
});
});
- it('calls `createFlash` with flash error message when request fails', () => {
+ it('calls `createAlert` with flash error message when request fails', () => {
jest.spyOn(wrapper.vm.config, 'fetchLabels').mockRejectedValue({});
wrapper.vm.fetchLabels('foo');
return waitForPromises().then(() => {
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: 'There was a problem fetching labels.',
});
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js
index f71ba51fc5b..589697fe542 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js
@@ -8,7 +8,7 @@ import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { sortMilestonesByDueDate } from '~/milestones/utils';
@@ -112,13 +112,13 @@ describe('MilestoneToken', () => {
});
});
- it('calls `createFlash` with flash error message when request fails', () => {
+ it('calls `createAlert` with flash error message when request fails', () => {
jest.spyOn(wrapper.vm.config, 'fetchMilestones').mockRejectedValue({});
wrapper.vm.fetchMilestones('foo');
return waitForPromises().then(() => {
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: 'There was a problem fetching milestones.',
});
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js
index 4bbbaab9b7a..0e5fa0f66d4 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js
@@ -2,7 +2,7 @@ import { GlFilteredSearchToken, GlFilteredSearchTokenSegment } from '@gitlab/ui'
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import ReleaseToken from '~/vue_shared/components/filtered_search_bar/tokens/release_token.vue';
import { mockReleaseToken } from '../mock_data';
@@ -73,7 +73,7 @@ describe('ReleaseToken', () => {
});
await waitForPromises();
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: 'There was a problem fetching releases.',
});
});
diff --git a/spec/frontend/vue_shared/components/gitlab_version_check_spec.js b/spec/frontend/vue_shared/components/gitlab_version_check_spec.js
index 6699ae5fb69..38f28837cc1 100644
--- a/spec/frontend/vue_shared/components/gitlab_version_check_spec.js
+++ b/spec/frontend/vue_shared/components/gitlab_version_check_spec.js
@@ -1,7 +1,9 @@
import { GlBadge } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import { mockTracking } from 'helpers/tracking_helper';
+import { helpPagePath } from '~/helpers/help_page_helper';
import axios from '~/lib/utils/axios_utils';
import GitlabVersionCheck from '~/vue_shared/components/gitlab_version_check.vue';
@@ -9,6 +11,8 @@ describe('GitlabVersionCheck', () => {
let wrapper;
let mock;
+ const UPGRADE_DOCS_URL = helpPagePath('update/index');
+
const defaultResponse = {
code: 200,
res: { severity: 'success' },
@@ -23,7 +27,7 @@ describe('GitlabVersionCheck', () => {
mock = new MockAdapter(axios);
mock.onGet().replyOnce(response.code, response.res);
- wrapper = shallowMount(GitlabVersionCheck);
+ wrapper = shallowMountExtended(GitlabVersionCheck);
};
const dummyGon = {
@@ -38,6 +42,7 @@ describe('GitlabVersionCheck', () => {
window.gon = originalGon;
});
+ const findGlBadgeClickWrapper = () => wrapper.findByTestId('badge-click-wrapper');
const findGlBadge = () => wrapper.findComponent(GlBadge);
describe.each`
@@ -77,7 +82,8 @@ describe('GitlabVersionCheck', () => {
await waitForPromises(); // Ensure we wrap up the axios call
});
- it(`does${renders ? '' : ' not'} render GlBadge`, () => {
+ it(`does${renders ? '' : ' not'} render Badge Click Wrapper and GlBadge`, () => {
+ expect(findGlBadgeClickWrapper().exists()).toBe(renders);
expect(findGlBadge().exists()).toBe(renders);
});
});
@@ -90,8 +96,11 @@ describe('GitlabVersionCheck', () => {
${{ code: 200, res: { severity: 'danger' } }} | ${{ title: 'Update ASAP', variant: 'danger' }}
`('badge ui', ({ mockResponse, expectedUI }) => {
describe(`when response is ${mockResponse.res.severity}`, () => {
+ let trackingSpy;
+
beforeEach(async () => {
createComponent(mockResponse);
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
await waitForPromises(); // Ensure we wrap up the axios call
});
@@ -102,6 +111,24 @@ describe('GitlabVersionCheck', () => {
it(`variant is ${expectedUI.variant}`, () => {
expect(findGlBadge().attributes('variant')).toBe(expectedUI.variant);
});
+
+ it(`tracks rendered_version_badge with label ${expectedUI.title}`, () => {
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'rendered_version_badge', {
+ label: expectedUI.title,
+ });
+ });
+
+ it(`link is ${UPGRADE_DOCS_URL}`, () => {
+ expect(findGlBadge().attributes('href')).toBe(UPGRADE_DOCS_URL);
+ });
+
+ it(`tracks click_version_badge with label ${expectedUI.title} when badge is clicked`, async () => {
+ await findGlBadgeClickWrapper().trigger('click');
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_version_badge', {
+ label: expectedUI.title,
+ });
+ });
});
});
});
diff --git a/spec/frontend/vue_shared/components/gl_countdown_spec.js b/spec/frontend/vue_shared/components/gl_countdown_spec.js
index 0d1d42082ab..af53d256236 100644
--- a/spec/frontend/vue_shared/components/gl_countdown_spec.js
+++ b/spec/frontend/vue_shared/components/gl_countdown_spec.js
@@ -1,10 +1,9 @@
import Vue, { nextTick } from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
+import { mount } from '@vue/test-utils';
import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
describe('GlCountdown', () => {
- const Component = Vue.extend(GlCountdown);
- let vm;
+ let wrapper;
let now = '2000-01-01T00:00:00Z';
beforeEach(() => {
@@ -12,21 +11,20 @@ describe('GlCountdown', () => {
});
afterEach(() => {
- vm.$destroy();
- jest.clearAllTimers();
+ wrapper.destroy();
});
describe('when there is time remaining', () => {
beforeEach(async () => {
- vm = mountComponent(Component, {
- endDateString: '2000-01-01T01:02:03Z',
+ wrapper = mount(GlCountdown, {
+ propsData: {
+ endDateString: '2000-01-01T01:02:03Z',
+ },
});
-
- await nextTick();
});
it('displays remaining time', () => {
- expect(vm.$el.textContent).toContain('01:02:03');
+ expect(wrapper.text()).toContain('01:02:03');
});
it('updates remaining time', async () => {
@@ -34,21 +32,21 @@ describe('GlCountdown', () => {
jest.advanceTimersByTime(1000);
await nextTick();
- expect(vm.$el.textContent).toContain('01:02:02');
+ expect(wrapper.text()).toContain('01:02:02');
});
});
describe('when there is no time remaining', () => {
beforeEach(async () => {
- vm = mountComponent(Component, {
- endDateString: '1900-01-01T00:00:00Z',
+ wrapper = mount(GlCountdown, {
+ propsData: {
+ endDateString: '1900-01-01T00:00:00Z',
+ },
});
-
- await nextTick();
});
it('displays 00:00:00', () => {
- expect(vm.$el.textContent).toContain('00:00:00');
+ expect(wrapper.text()).toContain('00:00:00');
});
});
@@ -62,8 +60,10 @@ describe('GlCountdown', () => {
});
it('throws a validation error', () => {
- vm = mountComponent(Component, {
- endDateString: 'this is invalid',
+ wrapper = mount(GlCountdown, {
+ propsData: {
+ endDateString: 'this is invalid',
+ },
});
expect(Vue.config.warnHandler).toHaveBeenCalledTimes(1);
diff --git a/spec/frontend/vue_shared/components/group_select/utils_spec.js b/spec/frontend/vue_shared/components/group_select/utils_spec.js
new file mode 100644
index 00000000000..5188e1aabf1
--- /dev/null
+++ b/spec/frontend/vue_shared/components/group_select/utils_spec.js
@@ -0,0 +1,24 @@
+import { groupsPath } from '~/vue_shared/components/group_select/utils';
+
+describe('group_select utils', () => {
+ describe('groupsPath', () => {
+ it.each`
+ groupsFilter | parentGroupID | expectedPath
+ ${undefined} | ${undefined} | ${'/api/:version/groups.json'}
+ ${undefined} | ${1} | ${'/api/:version/groups.json'}
+ ${'descendant_groups'} | ${1} | ${'/api/:version/groups/1/descendant_groups'}
+ ${'subgroups'} | ${1} | ${'/api/:version/groups/1/subgroups'}
+ `(
+ 'returns $expectedPath with groupsFilter = $groupsFilter and parentGroupID = $parentGroupID',
+ ({ groupsFilter, parentGroupID, expectedPath }) => {
+ expect(groupsPath(groupsFilter, parentGroupID)).toBe(expectedPath);
+ },
+ );
+ });
+
+ it('throws if groupsFilter is passed but parentGroupID is undefined', () => {
+ expect(() => {
+ groupsPath('descendant_groups');
+ }).toThrow('Cannot use groupsFilter without a parentGroupID');
+ });
+});
diff --git a/spec/frontend/vue_shared/components/markdown/header_spec.js b/spec/frontend/vue_shared/components/markdown/header_spec.js
index 9831908f806..ed417097e1e 100644
--- a/spec/frontend/vue_shared/components/markdown/header_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/header_spec.js
@@ -54,6 +54,8 @@ describe('Markdown field header component', () => {
'Add a bullet list',
'Add a numbered list',
'Add a checklist',
+ 'Indent line (⌘])',
+ 'Outdent line (⌘[)',
'Add a collapsible section',
'Add a table',
'Go full screen',
@@ -140,7 +142,7 @@ describe('Markdown field header component', () => {
const tableButton = findToolbarButtonByProp('icon', 'table');
expect(tableButton.props('tag')).toEqual(
- '| header | header |\n| ------ | ------ |\n| cell | cell |\n| cell | cell |',
+ '| header | header |\n| ------ | ------ |\n| | |\n| | |',
);
});
diff --git a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
new file mode 100644
index 00000000000..f7e93f45148
--- /dev/null
+++ b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
@@ -0,0 +1,289 @@
+import { GlSegmentedControl } from '@gitlab/ui';
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import { nextTick } from 'vue';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { EDITING_MODE_MARKDOWN_FIELD, EDITING_MODE_CONTENT_EDITOR } from '~/vue_shared/constants';
+import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
+import ContentEditor from '~/content_editor/components/content_editor.vue';
+import BubbleMenu from '~/content_editor/components/bubble_menus/bubble_menu.vue';
+import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
+import MarkdownField from '~/vue_shared/components/markdown/field.vue';
+import { stubComponent } from 'helpers/stub_component';
+
+jest.mock('~/emoji');
+
+describe('vue_shared/component/markdown/markdown_editor', () => {
+ let wrapper;
+ const value = 'test markdown';
+ const renderMarkdownPath = '/api/markdown';
+ const markdownDocsPath = '/help/markdown';
+ const quickActionsDocsPath = '/help/quickactions';
+ const enableAutocomplete = true;
+ const enablePreview = false;
+ const formFieldId = 'markdown_field';
+ const formFieldName = 'form[markdown_field]';
+ const formFieldPlaceholder = 'Write some markdown';
+ const formFieldAriaLabel = 'Edit your content';
+ let mock;
+
+ const buildWrapper = ({ propsData = {}, attachTo } = {}) => {
+ wrapper = mountExtended(MarkdownEditor, {
+ attachTo,
+ propsData: {
+ value,
+ renderMarkdownPath,
+ markdownDocsPath,
+ quickActionsDocsPath,
+ enableAutocomplete,
+ enablePreview,
+ formFieldId,
+ formFieldName,
+ formFieldPlaceholder,
+ formFieldAriaLabel,
+ ...propsData,
+ },
+ stubs: {
+ BubbleMenu: stubComponent(BubbleMenu),
+ },
+ });
+ };
+ const findSegmentedControl = () => wrapper.findComponent(GlSegmentedControl);
+ const findMarkdownField = () => wrapper.findComponent(MarkdownField);
+ const findTextarea = () => wrapper.find('textarea');
+ const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
+ const findContentEditor = () => wrapper.findComponent(ContentEditor);
+
+ beforeEach(() => {
+ window.uploads_path = 'uploads';
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ mock.restore();
+ });
+
+ it('displays markdown field by default', () => {
+ buildWrapper({ propsData: { supportsQuickActions: true } });
+
+ expect(findMarkdownField().props()).toEqual(
+ expect.objectContaining({
+ markdownPreviewPath: renderMarkdownPath,
+ quickActionsDocsPath,
+ canAttachFile: true,
+ enableAutocomplete,
+ textareaValue: value,
+ markdownDocsPath,
+ uploadsPath: window.uploads_path,
+ enablePreview,
+ }),
+ );
+ });
+
+ it('renders markdown field textarea', () => {
+ buildWrapper();
+
+ expect(findTextarea().attributes()).toEqual(
+ expect.objectContaining({
+ id: formFieldId,
+ name: formFieldName,
+ placeholder: formFieldPlaceholder,
+ 'aria-label': formFieldAriaLabel,
+ }),
+ );
+
+ expect(findTextarea().element.value).toBe(value);
+ });
+
+ it('renders switch segmented control', () => {
+ buildWrapper();
+
+ expect(findSegmentedControl().props()).toEqual({
+ checked: EDITING_MODE_MARKDOWN_FIELD,
+ options: [
+ {
+ text: expect.any(String),
+ value: EDITING_MODE_MARKDOWN_FIELD,
+ },
+ {
+ text: expect.any(String),
+ value: EDITING_MODE_CONTENT_EDITOR,
+ },
+ ],
+ });
+ });
+
+ describe.each`
+ editingMode
+ ${EDITING_MODE_CONTENT_EDITOR}
+ ${EDITING_MODE_MARKDOWN_FIELD}
+ `('when segmented control emits change event with $editingMode value', ({ editingMode }) => {
+ it(`emits ${editingMode} event`, () => {
+ buildWrapper();
+
+ findSegmentedControl().vm.$emit('change', editingMode);
+
+ expect(wrapper.emitted(editingMode)).toHaveLength(1);
+ });
+ });
+
+ describe(`when editingMode is ${EDITING_MODE_MARKDOWN_FIELD}`, () => {
+ it('emits input event when markdown field textarea changes', async () => {
+ buildWrapper();
+ const newValue = 'new value';
+
+ await findTextarea().setValue(newValue);
+
+ expect(wrapper.emitted('input')).toEqual([[newValue]]);
+ });
+
+ describe('when initOnAutofocus is true', () => {
+ beforeEach(async () => {
+ buildWrapper({ attachTo: document.body, propsData: { initOnAutofocus: true } });
+
+ await nextTick();
+ });
+
+ it('sets the markdown field as the active element in the document', () => {
+ expect(document.activeElement).toBe(findTextarea().element);
+ });
+ });
+
+ it('bubbles up keydown event', async () => {
+ buildWrapper();
+
+ await findTextarea().trigger('keydown');
+
+ expect(wrapper.emitted('keydown')).toHaveLength(1);
+ });
+
+ describe(`when segmented control triggers input event with ${EDITING_MODE_CONTENT_EDITOR} value`, () => {
+ beforeEach(() => {
+ buildWrapper();
+ findSegmentedControl().vm.$emit('input', EDITING_MODE_CONTENT_EDITOR);
+ findSegmentedControl().vm.$emit('change', EDITING_MODE_CONTENT_EDITOR);
+ });
+
+ it('displays the content editor', () => {
+ expect(findContentEditor().props()).toEqual(
+ expect.objectContaining({
+ renderMarkdown: expect.any(Function),
+ uploadsPath: window.uploads_path,
+ markdown: value,
+ autofocus: 'end',
+ }),
+ );
+ });
+
+ it('adds hidden field with current markdown', () => {
+ const hiddenField = wrapper.find(`#${formFieldId}`);
+
+ expect(hiddenField.attributes()).toEqual(
+ expect.objectContaining({
+ id: formFieldId,
+ name: formFieldName,
+ }),
+ );
+ expect(hiddenField.element.value).toBe(value);
+ });
+
+ it('hides the markdown field', () => {
+ expect(findMarkdownField().exists()).toBe(false);
+ });
+
+ it('updates localStorage value', () => {
+ expect(findLocalStorageSync().props().value).toBe(EDITING_MODE_CONTENT_EDITOR);
+ });
+ });
+ });
+
+ describe(`when editingMode is ${EDITING_MODE_CONTENT_EDITOR}`, () => {
+ beforeEach(() => {
+ buildWrapper();
+ findSegmentedControl().vm.$emit('input', EDITING_MODE_CONTENT_EDITOR);
+ });
+
+ describe('when initOnAutofocus is true', () => {
+ beforeEach(() => {
+ buildWrapper({ propsData: { initOnAutofocus: true } });
+ findLocalStorageSync().vm.$emit('input', EDITING_MODE_CONTENT_EDITOR);
+ });
+
+ it('sets the content editor autofocus property to end', () => {
+ expect(findContentEditor().props().autofocus).toBe('end');
+ });
+ });
+
+ it('emits input event when content editor emits change event', async () => {
+ const newValue = 'new value';
+
+ await findContentEditor().vm.$emit('change', { markdown: newValue });
+
+ expect(wrapper.emitted('input')).toEqual([[newValue]]);
+ });
+
+ it('bubbles up keydown event', () => {
+ const event = new Event('keydown');
+
+ findContentEditor().vm.$emit('keydown', event);
+
+ expect(wrapper.emitted('keydown')).toEqual([[event]]);
+ });
+
+ describe(`when segmented control triggers input event with ${EDITING_MODE_MARKDOWN_FIELD} value`, () => {
+ beforeEach(() => {
+ findSegmentedControl().vm.$emit('input', EDITING_MODE_MARKDOWN_FIELD);
+ });
+
+ it('hides the content editor', () => {
+ expect(findContentEditor().exists()).toBe(false);
+ });
+
+ it('shows the markdown field', () => {
+ expect(findMarkdownField().exists()).toBe(true);
+ });
+
+ it('updates localStorage value', () => {
+ expect(findLocalStorageSync().props().value).toBe(EDITING_MODE_MARKDOWN_FIELD);
+ });
+
+ it('sets the textarea as the activeElement in the document', async () => {
+ // The component should be rebuilt to attach it to the document body
+ buildWrapper({ attachTo: document.body });
+ await findSegmentedControl().vm.$emit('input', EDITING_MODE_CONTENT_EDITOR);
+
+ expect(findContentEditor().exists()).toBe(true);
+
+ await findSegmentedControl().vm.$emit('input', EDITING_MODE_MARKDOWN_FIELD);
+ await findSegmentedControl().vm.$emit('change', EDITING_MODE_MARKDOWN_FIELD);
+
+ expect(document.activeElement).toBe(findTextarea().element);
+ });
+ });
+
+ describe('when content editor emits loading event', () => {
+ beforeEach(() => {
+ findContentEditor().vm.$emit('loading');
+ });
+
+ it('disables switch editing mode control', () => {
+ // This is the only way that I found to check the segmented control is disabled
+ expect(findSegmentedControl().find('input[disabled]').exists()).toBe(true);
+ });
+
+ describe.each`
+ event
+ ${'loadingSuccess'}
+ ${'loadingError'}
+ `('when content editor emits $event event', ({ event }) => {
+ beforeEach(() => {
+ findContentEditor().vm.$emit(event);
+ });
+ it('enables the switch editing mode control', () => {
+ expect(findSegmentedControl().find('input[disabled]').exists()).toBe(false);
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/metric_images/metric_images_table_spec.js b/spec/frontend/vue_shared/components/metric_images/metric_images_table_spec.js
index d792bd46ccd..9c91dc9b5fc 100644
--- a/spec/frontend/vue_shared/components/metric_images/metric_images_table_spec.js
+++ b/spec/frontend/vue_shared/components/metric_images/metric_images_table_spec.js
@@ -139,8 +139,7 @@ describe('Metrics upload item', () => {
closeModal();
await waitForPromises();
-
- expect(findModal().attributes('visible')).toBeFalsy();
+ expect(findModal().attributes('visible')).toBeUndefined();
});
it('should delete the image when selected', async () => {
@@ -189,8 +188,7 @@ describe('Metrics upload item', () => {
closeEditModal();
await waitForPromises();
-
- expect(findEditModal().attributes('visible')).toBeFalsy();
+ expect(findEditModal().attributes('visible')).toBeUndefined();
});
it('should delete the image when selected', async () => {
diff --git a/spec/frontend/vue_shared/components/metric_images/store/actions_spec.js b/spec/frontend/vue_shared/components/metric_images/store/actions_spec.js
index 518cf354675..537367940e0 100644
--- a/spec/frontend/vue_shared/components/metric_images/store/actions_spec.js
+++ b/spec/frontend/vue_shared/components/metric_images/store/actions_spec.js
@@ -4,7 +4,7 @@ import actionsFactory from '~/vue_shared/components/metric_images/store/actions'
import * as types from '~/vue_shared/components/metric_images/store/mutation_types';
import createStore from '~/vue_shared/components/metric_images/store';
import testAction from 'helpers/vuex_action_helper';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { fileList, initialData } from '../mock_data';
@@ -35,7 +35,7 @@ describe('Metrics tab store actions', () => {
});
afterEach(() => {
- createFlash.mockClear();
+ createAlert.mockClear();
});
describe('fetching metric images', () => {
@@ -61,7 +61,7 @@ describe('Metrics tab store actions', () => {
[{ type: types.REQUEST_METRIC_IMAGES }, { type: types.RECEIVE_METRIC_IMAGES_ERROR }],
[],
);
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
});
});
@@ -98,7 +98,7 @@ describe('Metrics tab store actions', () => {
[{ type: types.REQUEST_METRIC_UPLOAD }, { type: types.RECEIVE_METRIC_UPLOAD_ERROR }],
[],
);
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
});
});
@@ -129,7 +129,7 @@ describe('Metrics tab store actions', () => {
[{ type: types.REQUEST_METRIC_UPLOAD }, { type: types.RECEIVE_METRIC_UPLOAD_ERROR }],
[],
);
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
});
});
diff --git a/spec/frontend/vue_shared/components/modal_copy_button_spec.js b/spec/frontend/vue_shared/components/modal_copy_button_spec.js
index b57efc88d57..61e4e774420 100644
--- a/spec/frontend/vue_shared/components/modal_copy_button_spec.js
+++ b/spec/frontend/vue_shared/components/modal_copy_button_spec.js
@@ -17,9 +17,16 @@ describe('modal copy button', () => {
title: 'Copy this value',
id: 'test-id',
},
+ slots: {
+ default: 'test',
+ },
});
});
+ it('should show the default slot', () => {
+ expect(wrapper.text()).toBe('test');
+ });
+
describe('clipboard', () => {
it('should fire a `success` event on click', async () => {
const root = createWrapper(wrapper.vm.$root);
diff --git a/spec/frontend/vue_shared/components/namespace_select/namespace_select_spec.js b/spec/frontend/vue_shared/components/namespace_select/namespace_select_deprecated_spec.js
index 2c14d65186b..d930ef63dad 100644
--- a/spec/frontend/vue_shared/components/namespace_select/namespace_select_spec.js
+++ b/spec/frontend/vue_shared/components/namespace_select/namespace_select_deprecated_spec.js
@@ -11,14 +11,14 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import NamespaceSelect, {
i18n,
EMPTY_NAMESPACE_ID,
-} from '~/vue_shared/components/namespace_select/namespace_select.vue';
+} from '~/vue_shared/components/namespace_select/namespace_select_deprecated.vue';
import { userNamespaces, groupNamespaces } from './mock_data';
const FLAT_NAMESPACES = [...userNamespaces, ...groupNamespaces];
const EMPTY_NAMESPACE_TITLE = 'Empty namespace TEST';
const EMPTY_NAMESPACE_ITEM = { id: EMPTY_NAMESPACE_ID, humanName: EMPTY_NAMESPACE_TITLE };
-describe('Namespace Select', () => {
+describe('NamespaceSelectDeprecated', () => {
let wrapper;
const createComponent = (props = {}) =>
@@ -207,9 +207,9 @@ describe('Namespace Select', () => {
expect(wrapper.emitted('load-more-groups')).toEqual([[]]);
});
- describe('when `isLoadingMoreGroups` prop is `true`', () => {
+ describe('when `isLoading` prop is `true`', () => {
it('renders a loading icon', () => {
- wrapper = createComponent({ hasNextPageOfGroups: true, isLoadingMoreGroups: true });
+ wrapper = createComponent({ hasNextPageOfGroups: true, isLoading: true });
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
@@ -223,4 +223,14 @@ describe('Namespace Select', () => {
expect(wrapper.findComponent(GlSearchBoxByType).props('isLoading')).toBe(true);
});
});
+
+ describe('when dropdown is opened', () => {
+ it('emits `show` event', () => {
+ wrapper = createComponent();
+
+ findDropdown().vm.$emit('show');
+
+ expect(wrapper.emitted('show')).toEqual([[]]);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/notes/__snapshots__/placeholder_note_spec.js.snap b/spec/frontend/vue_shared/components/notes/__snapshots__/placeholder_note_spec.js.snap
index bf6c8e8c704..3bac96069ec 100644
--- a/spec/frontend/vue_shared/components/notes/__snapshots__/placeholder_note_spec.js.snap
+++ b/spec/frontend/vue_shared/components/notes/__snapshots__/placeholder_note_spec.js.snap
@@ -2,13 +2,12 @@
exports[`Issue placeholder note component matches snapshot 1`] = `
<timeline-entry-item-stub
- class="note note-wrapper being-posted fade-in-half"
+ class="note note-wrapper note-comment being-posted fade-in-half"
>
<div
- class="timeline-icon"
+ class="timeline-avatar gl-float-left"
>
<gl-avatar-link-stub
- class="gl-mr-3"
href="/root"
>
<gl-avatar-stub
@@ -16,7 +15,7 @@ exports[`Issue placeholder note component matches snapshot 1`] = `
entityid="0"
entityname="root"
shape="circle"
- size="[object Object]"
+ size="32"
src="mock_path"
/>
</gl-avatar-link-stub>
@@ -50,16 +49,20 @@ exports[`Issue placeholder note component matches snapshot 1`] = `
</div>
<div
- class="note-body"
+ class="timeline-discussion-body"
>
<div
- class="note-text md"
+ class="note-body"
>
- <p>
- Foo
- </p>
-
+ <div
+ class="note-text md"
+ >
+ <p>
+ Foo
+ </p>
+
+ </div>
</div>
</div>
</div>
diff --git a/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js b/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js
index b86c8946e96..8f9f1bb336f 100644
--- a/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js
+++ b/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js
@@ -1,5 +1,4 @@
import { shallowMount } from '@vue/test-utils';
-import { GlAvatar } from '@gitlab/ui';
import Vue from 'vue';
import Vuex from 'vuex';
import IssuePlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue';
@@ -53,17 +52,4 @@ describe('Issue placeholder note component', () => {
expect(findNote().classes()).toContain('discussion');
});
-
- describe('avatar size', () => {
- it.each`
- size | line | isOverviewTab
- ${{ default: 24, md: 32 }} | ${null} | ${false}
- ${24} | ${{ line_code: '123' }} | ${false}
- ${{ default: 24, md: 32 }} | ${{ line_code: '123' }} | ${true}
- `('renders avatar $size for $line and $isOverviewTab', ({ size, line, isOverviewTab }) => {
- createComponent(false, { line, isOverviewTab });
-
- expect(wrapper.findComponent(GlAvatar).props('size')).toEqual(size);
- });
- });
});
diff --git a/spec/frontend/vue_shared/components/pagination_bar/pagination_bar_spec.js b/spec/frontend/vue_shared/components/pagination_bar/pagination_bar_spec.js
index b3be2f8a775..112cdaf74c6 100644
--- a/spec/frontend/vue_shared/components/pagination_bar/pagination_bar_spec.js
+++ b/spec/frontend/vue_shared/components/pagination_bar/pagination_bar_spec.js
@@ -2,6 +2,7 @@ import { GlPagination, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import PaginationBar from '~/vue_shared/components/pagination_bar/pagination_bar.vue';
import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
+import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
describe('Pagination bar', () => {
const DEFAULT_PROPS = {
@@ -20,6 +21,7 @@ describe('Pagination bar', () => {
...DEFAULT_PROPS,
...propsData,
},
+ stubs: { LocalStorageSync: true },
});
};
@@ -90,4 +92,28 @@ describe('Pagination bar', () => {
'Showing 21 - 40 of 1000+',
);
});
+
+ describe('local storage sync', () => {
+ it('does not perform local storage sync when no storage key is provided', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(LocalStorageSync).exists()).toBe(false);
+ });
+
+ it('passes current page size to local storage sync when storage key is provided', () => {
+ const STORAGE_KEY = 'fakeStorageKey';
+ createComponent({ storageKey: STORAGE_KEY });
+
+ expect(wrapper.getComponent(LocalStorageSync).props('storageKey')).toBe(STORAGE_KEY);
+ });
+
+ it('emits set-page event when local storage sync provides new value', () => {
+ const SAVED_SIZE = 50;
+ createComponent({ storageKey: 'some storage key' });
+
+ wrapper.getComponent(LocalStorageSync).vm.$emit('input', SAVED_SIZE);
+
+ expect(wrapper.emitted('set-page-size')).toEqual([[SAVED_SIZE]]);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/panel_resizer_spec.js b/spec/frontend/vue_shared/components/panel_resizer_spec.js
index d8b903e5bfd..0e261124cbf 100644
--- a/spec/frontend/vue_shared/components/panel_resizer_spec.js
+++ b/spec/frontend/vue_shared/components/panel_resizer_spec.js
@@ -1,12 +1,10 @@
-import Vue from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
-import panelResizer from '~/vue_shared/components/panel_resizer.vue';
+import { mount } from '@vue/test-utils';
+import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
describe('Panel Resizer component', () => {
- let vm;
- let PanelResizer;
+ let wrapper;
- const triggerEvent = (eventName, el = vm.$el, clientX = 0) => {
+ const triggerEvent = (eventName, el = wrapper.element, clientX = 0) => {
const event = document.createEvent('MouseEvents');
event.initMouseEvent(
eventName,
@@ -29,57 +27,64 @@ describe('Panel Resizer component', () => {
el.dispatchEvent(event);
};
- beforeEach(() => {
- PanelResizer = Vue.extend(panelResizer);
- });
-
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
it('should render a div element with the correct classes and styles', () => {
- vm = mountComponent(PanelResizer, {
- startSize: 100,
- side: 'left',
+ wrapper = mount(PanelResizer, {
+ propsData: {
+ startSize: 100,
+ side: 'left',
+ },
});
- expect(vm.$el.tagName).toEqual('DIV');
- expect(vm.$el.getAttribute('class')).toBe(
- 'position-absolute position-top-0 position-bottom-0 drag-handle position-left-0',
- );
+ expect(wrapper.element.tagName).toEqual('DIV');
+ expect(wrapper.classes().sort()).toStrictEqual([
+ 'drag-handle',
+ 'position-absolute',
+ 'position-bottom-0',
+ 'position-left-0',
+ 'position-top-0',
+ ]);
- expect(vm.$el.getAttribute('style')).toBe('cursor: ew-resize;');
+ expect(wrapper.element.getAttribute('style')).toBe('cursor: ew-resize;');
});
it('should render a div element with the correct classes for a right side panel', () => {
- vm = mountComponent(PanelResizer, {
- startSize: 100,
- side: 'right',
+ wrapper = mount(PanelResizer, {
+ propsData: {
+ startSize: 100,
+ side: 'right',
+ },
});
- expect(vm.$el.tagName).toEqual('DIV');
- expect(vm.$el.getAttribute('class')).toBe(
- 'position-absolute position-top-0 position-bottom-0 drag-handle position-right-0',
- );
+ expect(wrapper.element.tagName).toEqual('DIV');
+ expect(wrapper.classes().sort()).toStrictEqual([
+ 'drag-handle',
+ 'position-absolute',
+ 'position-bottom-0',
+ 'position-right-0',
+ 'position-top-0',
+ ]);
});
it('drag the resizer', () => {
- vm = mountComponent(PanelResizer, {
- startSize: 100,
- side: 'left',
+ wrapper = mount(PanelResizer, {
+ propsData: {
+ startSize: 100,
+ side: 'left',
+ },
});
- jest.spyOn(vm, '$emit').mockImplementation(() => {});
- triggerEvent('mousedown', vm.$el);
+ triggerEvent('mousedown');
triggerEvent('mousemove', document);
triggerEvent('mouseup', document);
- expect(vm.$emit.mock.calls).toEqual([
- ['resize-start', 100],
- ['update:size', 100],
- ['resize-end', 100],
- ]);
-
- expect(vm.size).toBe(100);
+ expect(wrapper.emitted()).toEqual({
+ 'resize-start': [[100]],
+ 'update:size': [[100]],
+ 'resize-end': [[100]],
+ });
});
});
diff --git a/spec/frontend/vue_shared/components/registry/__snapshots__/history_item_spec.js.snap b/spec/frontend/vue_shared/components/registry/__snapshots__/history_item_spec.js.snap
index 2abae33bc19..66cf2354bc7 100644
--- a/spec/frontend/vue_shared/components/registry/__snapshots__/history_item_spec.js.snap
+++ b/spec/frontend/vue_shared/components/registry/__snapshots__/history_item_spec.js.snap
@@ -2,7 +2,7 @@
exports[`History Item renders the correct markup 1`] = `
<li
- class="timeline-entry system-note note-wrapper gl-mb-6!"
+ class="timeline-entry system-note note-wrapper"
>
<div
class="timeline-entry-inner"
@@ -22,11 +22,13 @@ exports[`History Item renders the correct markup 1`] = `
<div
class="note-header"
>
- <span>
+ <div
+ class="note-header-info"
+ >
<div
data-testid="default-slot"
/>
- </span>
+ </div>
</div>
<div
diff --git a/spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js b/spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js
index c5672bc28cc..09b0b3d43ad 100644
--- a/spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js
+++ b/spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js
@@ -6,7 +6,7 @@ import {
expectedDownloadDropdownPropsWithTitle,
securityReportMergeRequestDownloadPathsQueryResponse,
} from 'jest/vue_shared/security_reports/mock_data';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import Component from '~/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue';
import SecurityReportDownloadDropdown from '~/vue_shared/security_reports/components/security_report_download_dropdown.vue';
import {
@@ -93,8 +93,8 @@ describe('Merge request artifact Download', () => {
});
});
- it('calls createFlash correctly', () => {
- expect(createFlash).toHaveBeenCalledWith({
+ it('calls createAlert correctly', () => {
+ expect(createAlert).toHaveBeenCalledWith({
message: Component.i18n.apiError,
captureError: true,
error: expect.any(Error),
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 4c7ac6e9a6f..30c1a4b7d2f 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
@@ -67,9 +67,9 @@ describe('LabelsSelectRoot', () => {
// We're utilizing `onDropdownClose` event emitted from the component to always include `touchedLabels`
// while the first param of the method is the labels list which were added/removed.
- expect(wrapper.emitted('updateSelectedLabels')).toBeTruthy();
+ expect(wrapper.emitted('updateSelectedLabels')).toHaveLength(1);
expect(wrapper.emitted('updateSelectedLabels')[0]).toEqual([touchedLabels]);
- expect(wrapper.emitted('onDropdownClose')).toBeTruthy();
+ expect(wrapper.emitted('onDropdownClose')).toHaveLength(1);
expect(wrapper.emitted('onDropdownClose')[0]).toEqual([touchedLabels]);
});
@@ -88,7 +88,7 @@ describe('LabelsSelectRoot', () => {
},
);
- expect(wrapper.emitted('updateSelectedLabels')).toBeTruthy();
+ expect(wrapper.emitted('updateSelectedLabels')).toHaveLength(1);
expect(wrapper.emitted('updateSelectedLabels')[0]).toEqual([
[
{
@@ -97,7 +97,7 @@ describe('LabelsSelectRoot', () => {
},
],
]);
- expect(wrapper.emitted('onDropdownClose')).toBeTruthy();
+ expect(wrapper.emitted('onDropdownClose')).toHaveLength(1);
expect(wrapper.emitted('onDropdownClose')[0]).toEqual([[]]);
});
});
@@ -106,8 +106,7 @@ describe('LabelsSelectRoot', () => {
it('emits `toggleCollapse` event on component', () => {
createComponent();
wrapper.vm.handleCollapsedValueClick();
-
- expect(wrapper.emitted().toggleCollapse).toBeTruthy();
+ expect(wrapper.emitted().toggleCollapse).toHaveLength(1);
});
});
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js
index 2bc513e87bf..edd044bd754 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js
@@ -1,7 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import * as actions from '~/vue_shared/components/sidebar/labels_select_vue/store/actions';
import * as types from '~/vue_shared/components/sidebar/labels_select_vue/store/mutation_types';
@@ -102,7 +102,7 @@ describe('LabelsSelect Actions', () => {
it('shows flash error', () => {
actions.receiveLabelsFailure({ commit: () => {} });
- expect(createFlash).toHaveBeenCalledWith({ message: 'Error fetching labels.' });
+ expect(createAlert).toHaveBeenCalledWith({ message: 'Error fetching labels.' });
});
});
@@ -186,7 +186,7 @@ describe('LabelsSelect Actions', () => {
it('shows flash error', () => {
actions.receiveCreateLabelFailure({ commit: () => {} });
- expect(createFlash).toHaveBeenCalledWith({ message: 'Error creating label.' });
+ expect(createAlert).toHaveBeenCalledWith({ message: 'Error creating label.' });
});
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js
index 1819e750324..2b2508b5e11 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js
@@ -189,10 +189,20 @@ describe('LabelsSelect Mutations', () => {
});
labelGroupIds.forEach((l) => {
- expect(state.labels[l.id - 1].touched).toBeFalsy();
+ expect(state.labels[l.id - 1].touched).toBeUndefined();
expect(state.labels[l.id - 1].set).toBe(false);
});
});
+ it('allows selection of multiple scoped labels', () => {
+ const state = { labels: cloneDeep(labels), allowMultipleScopedLabels: true };
+
+ mutations[types.UPDATE_SELECTED_LABELS](state, { labels: [{ id: labels[4].id }] });
+ mutations[types.UPDATE_SELECTED_LABELS](state, { labels: [{ id: labels[5].id }] });
+
+ expect(state.labels[4].set).toBe(true);
+ expect(state.labels[5].set).toBe(true);
+ expect(state.labels[6].set).toBe(true);
+ });
});
describe(`${types.UPDATE_LABELS_SET_STATE}`, () => {
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js
index 9c29f304c71..237f174e048 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js
@@ -4,7 +4,7 @@ import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { workspaceLabelsQueries } from '~/sidebar/constants';
import DropdownContentsCreateView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue';
import createLabelMutation from '~/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql';
@@ -202,7 +202,7 @@ describe('DropdownContentsCreateView', () => {
});
});
- it('calls createFlash is mutation has a user-recoverable error', async () => {
+ it('calls createAlert is mutation has a user-recoverable error', async () => {
createComponent({ mutationHandler: createLabelUserRecoverableErrorHandler });
fillLabelAttributes();
await nextTick();
@@ -210,10 +210,10 @@ describe('DropdownContentsCreateView', () => {
findCreateButton().vm.$emit('click');
await waitForPromises();
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
});
- it('calls createFlash is mutation was rejected', async () => {
+ it('calls createAlert is mutation was rejected', async () => {
createComponent({ mutationHandler: createLabelErrorHandler });
fillLabelAttributes();
await nextTick();
@@ -221,7 +221,7 @@ describe('DropdownContentsCreateView', () => {
findCreateButton().vm.$emit('click');
await waitForPromises();
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
});
it('displays error in alert if label title is already taken', async () => {
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js
index 7f6770e0bea..5d8ad5ddee5 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js
@@ -9,7 +9,7 @@ import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
import DropdownContentsLabelsView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue';
@@ -143,13 +143,13 @@ describe('DropdownContentsLabelsView', () => {
expect(findNoResultsMessage().isVisible()).toBe(true);
});
- it('calls `createFlash` when fetching labels failed', async () => {
+ it('calls `createAlert` when fetching labels failed', async () => {
createComponent({ queryHandler: jest.fn().mockRejectedValue('Houston, we have a problem!') });
await makeObserverAppear();
jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
await waitForPromises();
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
});
it('emits an `input` event on label click', async () => {
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js
index cad401e0013..b58c44645d6 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js
@@ -3,7 +3,7 @@ import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { IssuableType } from '~/issues/constants';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import DropdownContents from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue';
@@ -151,7 +151,7 @@ describe('LabelsSelectRoot', () => {
it('creates flash with error message when query is rejected', async () => {
createComponent({ queryHandler: errorQueryHandler });
await waitForPromises();
- expect(createFlash).toHaveBeenCalledWith({ message: 'Error fetching labels.' });
+ expect(createAlert).toHaveBeenCalledWith({ message: 'Error fetching labels.' });
});
});
@@ -197,7 +197,7 @@ describe('LabelsSelectRoot', () => {
findDropdownContents().vm.$emit('setLabels', [label]);
await waitForPromises();
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
captureError: true,
error: expect.anything(),
message: 'An error occurred while updating labels.',
diff --git a/spec/frontend/vue_shared/components/source_viewer/components/chunk_line_spec.js b/spec/frontend/vue_shared/components/source_viewer/components/chunk_line_spec.js
index fd3ff9ce892..f661bd6747a 100644
--- a/spec/frontend/vue_shared/components/source_viewer/components/chunk_line_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/components/chunk_line_spec.js
@@ -1,10 +1,5 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ChunkLine from '~/vue_shared/components/source_viewer/components/chunk_line.vue';
-import {
- BIDI_CHARS,
- BIDI_CHARS_CLASS_LIST,
- BIDI_CHAR_TOOLTIP,
-} from '~/vue_shared/components/source_viewer/constants';
const DEFAULT_PROPS = {
number: 2,
@@ -31,7 +26,6 @@ describe('Chunk Line component', () => {
const findLineLink = () => wrapper.find('.file-line-num');
const findBlameLink = () => wrapper.find('.file-line-blame');
const findContent = () => wrapper.findByTestId('content');
- const findWrappedBidiChars = () => wrapper.findAllByTestId('bidi-wrapper');
beforeEach(() => {
createComponent();
@@ -40,22 +34,6 @@ describe('Chunk Line component', () => {
afterEach(() => wrapper.destroy());
describe('rendering', () => {
- it('wraps BiDi characters', () => {
- const content = `// some content ${BIDI_CHARS.toString()} with BiDi chars`;
- createComponent({ content });
- const wrappedBidiChars = findWrappedBidiChars();
-
- expect(wrappedBidiChars.length).toBe(BIDI_CHARS.length);
-
- wrappedBidiChars.wrappers.forEach((_, i) => {
- expect(wrappedBidiChars.at(i).text()).toBe(BIDI_CHARS[i]);
- expect(wrappedBidiChars.at(i).attributes()).toMatchObject({
- class: BIDI_CHARS_CLASS_LIST,
- title: BIDI_CHAR_TOOLTIP,
- });
- });
- });
-
it('renders a blame link', () => {
expect(findBlameLink().attributes()).toMatchObject({
href: `${DEFAULT_PROPS.blamePath}#L${DEFAULT_PROPS.number}`,
diff --git a/spec/frontend/vue_shared/components/source_viewer/highlight_util_spec.js b/spec/frontend/vue_shared/components/source_viewer/highlight_util_spec.js
new file mode 100644
index 00000000000..4a995e2fde1
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/highlight_util_spec.js
@@ -0,0 +1,44 @@
+import hljs from 'highlight.js/lib/core';
+import languageLoader from '~/content_editor/services/highlight_js_language_loader';
+import { registerPlugins } from '~/vue_shared/components/source_viewer/plugins/index';
+import { highlight } from '~/vue_shared/components/source_viewer/workers/highlight_utils';
+
+jest.mock('highlight.js/lib/core', () => ({
+ highlight: jest.fn().mockReturnValue({}),
+ registerLanguage: jest.fn(),
+}));
+
+jest.mock('~/content_editor/services/highlight_js_language_loader', () => ({
+ javascript: jest.fn().mockReturnValue({ default: jest.fn() }),
+}));
+
+jest.mock('~/vue_shared/components/source_viewer/plugins/index', () => ({
+ registerPlugins: jest.fn(),
+}));
+
+const fileType = 'text';
+const content = 'function test() { return true };';
+const language = 'javascript';
+
+describe('Highlight utility', () => {
+ beforeEach(() => highlight(fileType, content, language));
+
+ it('loads the language', () => {
+ expect(languageLoader.javascript).toHaveBeenCalled();
+ });
+
+ it('registers the plugins', () => {
+ expect(registerPlugins).toHaveBeenCalled();
+ });
+
+ it('registers the language', () => {
+ expect(hljs.registerLanguage).toHaveBeenCalledWith(
+ language,
+ languageLoader[language]().default,
+ );
+ });
+
+ it('highlights the content', () => {
+ expect(hljs.highlight).toHaveBeenCalledWith(content, { language });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/index_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/index_spec.js
index 83fdc5d669d..57045ca54ae 100644
--- a/spec/frontend/vue_shared/components/source_viewer/plugins/index_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/index_spec.js
@@ -1,14 +1,18 @@
-import { registerPlugins } from '~/vue_shared/components/source_viewer/plugins/index';
-import { HLJS_ON_AFTER_HIGHLIGHT } from '~/vue_shared/components/source_viewer/constants';
-import wrapComments from '~/vue_shared/components/source_viewer/plugins/wrap_comments';
+import {
+ registerPlugins,
+ HLJS_ON_AFTER_HIGHLIGHT,
+} from '~/vue_shared/components/source_viewer/plugins/index';
+import wrapChildNodes from '~/vue_shared/components/source_viewer/plugins/wrap_child_nodes';
+import wrapBidiChars from '~/vue_shared/components/source_viewer/plugins/wrap_bidi_chars';
-jest.mock('~/vue_shared/components/source_viewer/plugins/wrap_comments');
+jest.mock('~/vue_shared/components/source_viewer/plugins/wrap_child_nodes');
const hljsMock = { addPlugin: jest.fn() };
describe('Highlight.js plugin registration', () => {
beforeEach(() => registerPlugins(hljsMock));
it('registers our plugins', () => {
- expect(hljsMock.addPlugin).toHaveBeenCalledWith({ [HLJS_ON_AFTER_HIGHLIGHT]: wrapComments });
+ expect(hljsMock.addPlugin).toHaveBeenCalledWith({ [HLJS_ON_AFTER_HIGHLIGHT]: wrapBidiChars });
+ expect(hljsMock.addPlugin).toHaveBeenCalledWith({ [HLJS_ON_AFTER_HIGHLIGHT]: wrapChildNodes });
});
});
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util_spec.js
index 8079d5ad99a..e4ce07ec668 100644
--- a/spec/frontend/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util_spec.js
@@ -15,7 +15,7 @@ describe('createLink', () => {
it('escapes the user-controlled content', () => {
const unescapedXSS = '<script>XSS</script>';
const escapedPackageName = '&lt;script&gt;XSS&lt;/script&gt;';
- const escapedHref = '&amp;lt;script&amp;gt;XSS&amp;lt;/script&amp;gt;';
+ const escapedHref = '&lt;script&gt;XSS&lt;/script&gt;';
const href = `http://test.com/${unescapedXSS}`;
const innerText = `testing${unescapedXSS}`;
const result = `<a href="http://test.com/${escapedHref}" rel="nofollow noreferrer noopener">testing${escapedPackageName}</a>`;
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_bidi_chars_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_bidi_chars_spec.js
new file mode 100644
index 00000000000..f40f8b22627
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_bidi_chars_spec.js
@@ -0,0 +1,17 @@
+import wrapBidiChars from '~/vue_shared/components/source_viewer/plugins/wrap_bidi_chars';
+import {
+ BIDI_CHARS,
+ BIDI_CHARS_CLASS_LIST,
+ BIDI_CHAR_TOOLTIP,
+} from '~/vue_shared/components/source_viewer/constants';
+
+describe('Highlight.js plugin for wrapping BiDi characters', () => {
+ it.each(BIDI_CHARS)('wraps %s BiDi char', (bidiChar) => {
+ const inputValue = `// some content ${bidiChar} with BiDi chars`;
+ const outputValue = `// some content <span class="${BIDI_CHARS_CLASS_LIST}" title="${BIDI_CHAR_TOOLTIP}">${bidiChar}</span>`;
+ const hljsResultMock = { value: inputValue };
+
+ wrapBidiChars(hljsResultMock);
+ expect(hljsResultMock.value).toContain(outputValue);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_child_nodes_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_child_nodes_spec.js
new file mode 100644
index 00000000000..bc6df1a2565
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_child_nodes_spec.js
@@ -0,0 +1,22 @@
+import wrapChildNodes from '~/vue_shared/components/source_viewer/plugins/wrap_child_nodes';
+
+describe('Highlight.js plugin for wrapping _emitter nodes', () => {
+ it('mutates the input value by wrapping each node in a span tag', () => {
+ const hljsResultMock = {
+ _emitter: {
+ rootNode: {
+ children: [
+ { kind: 'string', children: ['Text 1'] },
+ { kind: 'string', children: ['Text 2', { kind: 'comment', children: ['Text 3'] }] },
+ 'Text4\nText5',
+ ],
+ },
+ },
+ };
+
+ const outputValue = `<span class="hljs-string">Text 1</span><span class="hljs-string"><span class="hljs-string">Text 2</span><span class="hljs-comment">Text 3</span></span><span class="">Text4</span>\n<span class="">Text5</span>`;
+
+ wrapChildNodes(hljsResultMock);
+ expect(hljsResultMock.value).toBe(outputValue);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_comments_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_comments_spec.js
deleted file mode 100644
index 5fd4182da29..00000000000
--- a/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_comments_spec.js
+++ /dev/null
@@ -1,29 +0,0 @@
-import { HLJS_COMMENT_SELECTOR } from '~/vue_shared/components/source_viewer/constants';
-import wrapComments from '~/vue_shared/components/source_viewer/plugins/wrap_comments';
-
-describe('Highlight.js plugin for wrapping comments', () => {
- it('mutates the input value by wrapping each line in a span tag', () => {
- const inputValue = `<span class="${HLJS_COMMENT_SELECTOR}">/* Line 1 \n* Line 2 \n*/</span>`;
- const outputValue = `<span class="${HLJS_COMMENT_SELECTOR}">/* Line 1 \n<span class="${HLJS_COMMENT_SELECTOR}">* Line 2 </span>\n<span class="${HLJS_COMMENT_SELECTOR}">*/</span>`;
- const hljsResultMock = { value: inputValue };
-
- wrapComments(hljsResultMock);
- expect(hljsResultMock.value).toBe(outputValue);
- });
-
- it('does not mutate the input value if the hljs comment selector is not present', () => {
- const inputValue = '<span class="hljs-keyword">const</span>';
- const hljsResultMock = { value: inputValue };
-
- wrapComments(hljsResultMock);
- expect(hljsResultMock.value).toBe(inputValue);
- });
-
- it('does not mutate the input value if the hljs comment line includes a closing tag', () => {
- const inputValue = `<span class="${HLJS_COMMENT_SELECTOR}">/* Line 1 </span> \n* Line 2 \n*/`;
- const hljsResultMock = { value: inputValue };
-
- wrapComments(hljsResultMock);
- expect(hljsResultMock.value).toBe(inputValue);
- });
-});
diff --git a/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js
index e020d9a557e..6d319b37b02 100644
--- a/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js
@@ -22,10 +22,10 @@ jest.mock('~/vue_shared/components/source_viewer/plugins/index');
Vue.use(VueRouter);
const router = new VueRouter();
-const generateContent = (content, totalLines = 1) => {
+const generateContent = (content, totalLines = 1, delimiter = '\n') => {
let generatedContent = '';
for (let i = 0; i < totalLines; i += 1) {
- generatedContent += `Line: ${i + 1} = ${content}\n`;
+ generatedContent += `Line: ${i + 1} = ${content}${delimiter}`;
}
return generatedContent;
};
@@ -38,7 +38,9 @@ describe('Source Viewer component', () => {
const mappedLanguage = ROUGE_TO_HLJS_LANGUAGE_MAP[language];
const chunk1 = generateContent('// Some source code 1', 70);
const chunk2 = generateContent('// Some source code 2', 70);
- const content = chunk1 + chunk2;
+ const chunk3 = generateContent('// Some source code 3', 70, '\r\n');
+ const chunk3Result = generateContent('// Some source code 3', 70, '\n');
+ const content = chunk1 + chunk2 + chunk3;
const path = 'some/path.js';
const blamePath = 'some/blame/path.js';
const fileType = 'javascript';
@@ -152,6 +154,19 @@ describe('Source Viewer component', () => {
startingFrom: 70,
});
});
+
+ it('renders the third chunk', async () => {
+ const thirdChunk = findChunks().at(2);
+
+ expect(thirdChunk.props('content')).toContain(chunk3Result.trim());
+
+ expect(chunk3Result).toEqual(chunk3.replace(/\r?\n/g, '\n'));
+
+ expect(thirdChunk.props()).toMatchObject({
+ totalLines: 70,
+ startingFrom: 140,
+ });
+ });
});
it('emits showBlobInteractionZones on the eventHub when chunk appears', () => {
diff --git a/spec/frontend/vue_shared/components/stacked_progress_bar_spec.js b/spec/frontend/vue_shared/components/stacked_progress_bar_spec.js
index c6f01efa71a..79b1f17afa0 100644
--- a/spec/frontend/vue_shared/components/stacked_progress_bar_spec.js
+++ b/spec/frontend/vue_shared/components/stacked_progress_bar_spec.js
@@ -1,121 +1,109 @@
-import Vue from 'vue';
-
-import mountComponent from 'helpers/vue_mount_component_helper';
-import stackedProgressBarComponent from '~/vue_shared/components/stacked_progress_bar.vue';
-
-const createComponent = (config) => {
- const Component = Vue.extend(stackedProgressBarComponent);
- const defaultConfig = {
- successLabel: 'Synced',
- failureLabel: 'Failed',
- neutralLabel: 'Out of sync',
- successCount: 25,
- failureCount: 10,
- totalCount: 5000,
- ...config,
- };
-
- return mountComponent(Component, defaultConfig);
-};
+import { mount } from '@vue/test-utils';
+import StackedProgressBarComponent from '~/vue_shared/components/stacked_progress_bar.vue';
describe('StackedProgressBarComponent', () => {
- let vm;
-
- beforeEach(() => {
- vm = createComponent();
- });
+ let wrapper;
+
+ const createComponent = (config) => {
+ const defaultConfig = {
+ successLabel: 'Synced',
+ failureLabel: 'Failed',
+ neutralLabel: 'Out of sync',
+ successCount: 25,
+ failureCount: 10,
+ totalCount: 5000,
+ ...config,
+ };
+
+ wrapper = mount(StackedProgressBarComponent, { propsData: defaultConfig });
+ };
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
- const findSuccessBarText = (wrapper) =>
- wrapper.$el.querySelector('.status-green').innerText.trim();
- const findNeutralBarText = (wrapper) =>
- wrapper.$el.querySelector('.status-neutral').innerText.trim();
- const findFailureBarText = (wrapper) => wrapper.$el.querySelector('.status-red').innerText.trim();
- const findUnavailableBarText = (wrapper) =>
- wrapper.$el.querySelector('.status-unavailable').innerText.trim();
-
- describe('computed', () => {
- describe('neutralCount', () => {
- it('returns neutralCount based on totalCount, successCount and failureCount', () => {
- expect(vm.neutralCount).toBe(4965); // 5000 - 25 - 10
- });
- });
- });
+ const findSuccessBar = () => wrapper.find('.status-green');
+ const findNeutralBar = () => wrapper.find('.status-neutral');
+ const findFailureBar = () => wrapper.find('.status-red');
+ const findUnavailableBar = () => wrapper.find('.status-unavailable');
describe('template', () => {
it('renders container element', () => {
- expect(vm.$el.classList.contains('stacked-progress-bar')).toBeTruthy();
+ createComponent();
+
+ expect(wrapper.classes()).toContain('stacked-progress-bar');
});
it('renders empty state when count is unavailable', () => {
- const vmX = createComponent({ totalCount: 0, successCount: 0, failureCount: 0 });
+ createComponent({ totalCount: 0, successCount: 0, failureCount: 0 });
- expect(findUnavailableBarText(vmX)).not.toBeUndefined();
+ expect(findUnavailableBar()).not.toBeUndefined();
});
it('renders bar elements when count is available', () => {
- expect(findSuccessBarText(vm)).not.toBeUndefined();
- expect(findNeutralBarText(vm)).not.toBeUndefined();
- expect(findFailureBarText(vm)).not.toBeUndefined();
+ createComponent();
+
+ expect(findSuccessBar().exists()).toBe(true);
+ expect(findNeutralBar().exists()).toBe(true);
+ expect(findFailureBar().exists()).toBe(true);
});
describe('getPercent', () => {
it('returns correct percentages from provided count based on `totalCount`', () => {
- vm = createComponent({ totalCount: 100, successCount: 25, failureCount: 10 });
+ createComponent({ totalCount: 100, successCount: 25, failureCount: 10 });
- expect(findSuccessBarText(vm)).toBe('25%');
- expect(findNeutralBarText(vm)).toBe('65%');
- expect(findFailureBarText(vm)).toBe('10%');
+ expect(findSuccessBar().text()).toBe('25%');
+ expect(findNeutralBar().text()).toBe('65%');
+ expect(findFailureBar().text()).toBe('10%');
});
it('returns percentage with decimal place when decimal is greater than 1', () => {
- vm = createComponent({ successCount: 67 });
+ createComponent({ successCount: 67 });
- expect(findSuccessBarText(vm)).toBe('1.3%');
+ expect(findSuccessBar().text()).toBe('1.3%');
});
it('returns percentage as `< 1%` from provided count based on `totalCount` when evaluated value is less than 1', () => {
- vm = createComponent({ successCount: 10 });
+ createComponent({ successCount: 10 });
- expect(findSuccessBarText(vm)).toBe('< 1%');
+ expect(findSuccessBar().text()).toBe('< 1%');
});
it('returns not available if totalCount is falsy', () => {
- vm = createComponent({ totalCount: 0 });
+ createComponent({ totalCount: 0 });
- expect(findUnavailableBarText(vm)).toBe('Not available');
+ expect(findUnavailableBar().text()).toBe('Not available');
});
it('returns 99.9% when numbers are extreme decimals', () => {
- vm = createComponent({ totalCount: 1000000 });
+ createComponent({ totalCount: 1000000 });
- expect(findNeutralBarText(vm)).toBe('99.9%');
+ expect(findNeutralBar().text()).toBe('99.9%');
});
});
- describe('barStyle', () => {
- it('returns style string based on percentage provided', () => {
- expect(vm.barStyle(50)).toBe('width: 50%;');
+ describe('bar style', () => {
+ it('renders width based on percentage provided', () => {
+ createComponent({ totalCount: 100, successCount: 25 });
+
+ expect(findSuccessBar().element.style.width).toBe('25%');
});
});
- describe('getTooltip', () => {
+ describe('tooltip', () => {
describe('when hideTooltips is false', () => {
it('returns label string based on label and count provided', () => {
- expect(vm.getTooltip('Synced', 10)).toBe('Synced: 10');
+ createComponent({ successCount: 10, successLabel: 'Synced', hideTooltips: false });
+
+ expect(findSuccessBar().attributes('title')).toBe('Synced: 10');
});
});
describe('when hideTooltips is true', () => {
- beforeEach(() => {
- vm = createComponent({ hideTooltips: true });
- });
-
it('returns an empty string', () => {
- expect(vm.getTooltip('Synced', 10)).toBe('');
+ createComponent({ successCount: 10, successLabel: 'Synced', hideTooltips: true });
+
+ expect(findSuccessBar().attributes('title')).toBe('');
});
});
});
diff --git a/spec/frontend/vue_shared/components/timezone_dropdown/helpers.js b/spec/frontend/vue_shared/components/timezone_dropdown/helpers.js
new file mode 100644
index 00000000000..dee4c92add4
--- /dev/null
+++ b/spec/frontend/vue_shared/components/timezone_dropdown/helpers.js
@@ -0,0 +1,6 @@
+import timezoneDataFixture from 'test_fixtures/timezones/short.json';
+
+export { timezoneDataFixture };
+
+export const findTzByName = (identifier = '') =>
+ timezoneDataFixture.find(({ name }) => name.toLowerCase() === identifier.toLowerCase());
diff --git a/spec/frontend/vue_shared/components/timezone_dropdown/timezone_dropdown_spec.js b/spec/frontend/vue_shared/components/timezone_dropdown/timezone_dropdown_spec.js
new file mode 100644
index 00000000000..e5f56c63031
--- /dev/null
+++ b/spec/frontend/vue_shared/components/timezone_dropdown/timezone_dropdown_spec.js
@@ -0,0 +1,111 @@
+import { GlDropdownItem, GlDropdown } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown/timezone_dropdown.vue';
+import { formatTimezone } from '~/lib/utils/datetime_utility';
+import { findTzByName, timezoneDataFixture } from './helpers';
+
+describe('Deploy freeze timezone dropdown', () => {
+ let wrapper;
+ let store;
+
+ const createComponent = (searchTerm, selectedTimezone) => {
+ wrapper = shallowMountExtended(TimezoneDropdown, {
+ store,
+ propsData: {
+ value: selectedTimezone,
+ timezoneData: timezoneDataFixture,
+ name: 'user[timezone]',
+ },
+ });
+
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
+ wrapper.setData({ searchTerm });
+ };
+
+ const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
+ const findDropdownItemByIndex = (index) => wrapper.findAllComponents(GlDropdownItem).at(index);
+ const findEmptyResultsItem = () => wrapper.findByTestId('noMatchingResults');
+ const findHiddenInput = () => wrapper.find('input');
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('No time zones found', () => {
+ beforeEach(() => {
+ createComponent('UTC timezone');
+ });
+
+ it('renders empty results message', () => {
+ expect(findDropdownItemByIndex(0).text()).toBe('No matching results');
+ });
+ });
+
+ describe('Search term is empty', () => {
+ beforeEach(() => {
+ createComponent('');
+ });
+
+ it('renders all timezones when search term is empty', () => {
+ expect(findAllDropdownItems()).toHaveLength(timezoneDataFixture.length);
+ });
+ });
+
+ describe('Time zones found', () => {
+ beforeEach(() => {
+ createComponent('Alaska');
+ });
+
+ it('renders only the time zone searched for', () => {
+ const selectedTz = findTzByName('Alaska');
+ expect(findAllDropdownItems()).toHaveLength(1);
+ expect(findDropdownItemByIndex(0).text()).toBe(formatTimezone(selectedTz));
+ });
+
+ it('should not display empty results message', () => {
+ expect(findEmptyResultsItem().exists()).toBe(false);
+ });
+
+ describe('Custom events', () => {
+ const selectedTz = findTzByName('Alaska');
+
+ it('should emit input if a time zone is clicked', () => {
+ findDropdownItemByIndex(0).vm.$emit('click');
+ expect(wrapper.emitted('input')).toEqual([
+ [
+ {
+ formattedTimezone: formatTimezone(selectedTz),
+ identifier: selectedTz.identifier,
+ },
+ ],
+ ]);
+ });
+ });
+ });
+
+ describe('Selected time zone not found', () => {
+ beforeEach(() => {
+ createComponent('', 'Berlin');
+ });
+
+ it('renders empty selections', () => {
+ expect(wrapper.findComponent(GlDropdown).props().text).toBe('Select timezone');
+ });
+
+ it('preserves initial value in the associated input', () => {
+ expect(findHiddenInput().attributes('value')).toBe('Berlin');
+ });
+ });
+
+ describe('Selected time zone found', () => {
+ beforeEach(() => {
+ createComponent('', 'Europe/Berlin');
+ });
+
+ it('renders selected time zone as dropdown label', () => {
+ expect(wrapper.findComponent(GlDropdown).props().text).toBe('[UTC + 2] Berlin');
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/url_sync_spec.js b/spec/frontend/vue_shared/components/url_sync_spec.js
index aefe6a5c3e8..acda1a64a75 100644
--- a/spec/frontend/vue_shared/components/url_sync_spec.js
+++ b/spec/frontend/vue_shared/components/url_sync_spec.js
@@ -1,10 +1,11 @@
import { shallowMount } from '@vue/test-utils';
import { historyPushState } from '~/lib/utils/common_utils';
-import { mergeUrlParams } from '~/lib/utils/url_utility';
-import UrlSyncComponent from '~/vue_shared/components/url_sync.vue';
+import { mergeUrlParams, setUrlParams } from '~/lib/utils/url_utility';
+import UrlSyncComponent, { URL_SET_PARAMS_STRATEGY } from '~/vue_shared/components/url_sync.vue';
jest.mock('~/lib/utils/url_utility', () => ({
- mergeUrlParams: jest.fn((query, url) => `urlParams: ${query} ${url}`),
+ mergeUrlParams: jest.fn((query, url) => `urlParams: ${JSON.stringify(query)} ${url}`),
+ setUrlParams: jest.fn((query, url) => `urlParams: ${JSON.stringify(query)} ${url}`),
}));
jest.mock('~/lib/utils/common_utils', () => ({
@@ -17,9 +18,14 @@ describe('url sync component', () => {
const findButton = () => wrapper.find('button');
- const createComponent = ({ query = mockQuery, scopedSlots, slots } = {}) => {
+ const createComponent = ({
+ query = mockQuery,
+ scopedSlots,
+ slots,
+ urlParamsUpdateStrategy,
+ } = {}) => {
wrapper = shallowMount(UrlSyncComponent, {
- propsData: { query },
+ propsData: { query, ...(urlParamsUpdateStrategy && { urlParamsUpdateStrategy }) },
scopedSlots,
slots,
});
@@ -29,21 +35,39 @@ describe('url sync component', () => {
wrapper.destroy();
});
- const expectUrlSync = (query, times, mergeUrlParamsReturnValue) => {
- expect(mergeUrlParams).toHaveBeenCalledTimes(times);
- expect(mergeUrlParams).toHaveBeenCalledWith(query, window.location.href, {
- spreadArrays: true,
- });
+ const expectUrlSyncFactory = (
+ query,
+ times,
+ urlParamsUpdateStrategy,
+ urlOptions,
+ urlReturnValue,
+ ) => {
+ expect(urlParamsUpdateStrategy).toHaveBeenCalledTimes(times);
+ expect(urlParamsUpdateStrategy).toHaveBeenCalledWith(query, window.location.href, urlOptions);
expect(historyPushState).toHaveBeenCalledTimes(times);
- expect(historyPushState).toHaveBeenCalledWith(mergeUrlParamsReturnValue);
+ expect(historyPushState).toHaveBeenCalledWith(urlReturnValue);
+ };
+
+ const expectUrlSyncWithMergeUrlParams = (query, times, mergeUrlParamsReturnValue) => {
+ expectUrlSyncFactory(
+ query,
+ times,
+ mergeUrlParams,
+ { spreadArrays: true },
+ mergeUrlParamsReturnValue,
+ );
+ };
+
+ const expectUrlSyncWithSetUrlParams = (query, times, setUrlParamsReturnValue) => {
+ expectUrlSyncFactory(query, times, setUrlParams, true, setUrlParamsReturnValue);
};
describe('with query as a props', () => {
it('immediately syncs the query to the URL', () => {
createComponent();
- expectUrlSync(mockQuery, 1, mergeUrlParams.mock.results[0].value);
+ expectUrlSyncWithMergeUrlParams(mockQuery, 1, mergeUrlParams.mock.results[0].value);
});
describe('when the query is modified', () => {
@@ -54,11 +78,21 @@ describe('url sync component', () => {
// using setProps to test the watcher
await wrapper.setProps({ query: newQuery });
- expectUrlSync(mockQuery, 2, mergeUrlParams.mock.results[1].value);
+ expectUrlSyncWithMergeUrlParams(mockQuery, 2, mergeUrlParams.mock.results[1].value);
});
});
});
+ describe('with url-params-update-strategy equals to URL_SET_PARAMS_STRATEGY', () => {
+ it('uses setUrlParams to generate URL', () => {
+ createComponent({
+ urlParamsUpdateStrategy: URL_SET_PARAMS_STRATEGY,
+ });
+
+ expectUrlSyncWithSetUrlParams(mockQuery, 1, setUrlParams.mock.results[0].value);
+ });
+ });
+
describe('with scoped slot', () => {
const scopedSlots = {
default: `
@@ -77,7 +111,7 @@ describe('url sync component', () => {
findButton().trigger('click');
- expectUrlSync({ bar: 'baz' }, 1, mergeUrlParams.mock.results[0].value);
+ expectUrlSyncWithMergeUrlParams({ bar: 'baz' }, 1, mergeUrlParams.mock.results[0].value);
});
});
diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_new_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_new_spec.js
deleted file mode 100644
index f87737ca86a..00000000000
--- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_new_spec.js
+++ /dev/null
@@ -1,134 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { GlAvatar, GlTooltip } from '@gitlab/ui';
-import defaultAvatarUrl from 'images/no_avatar.png';
-import { placeholderImage } from '~/lazy_loader';
-import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image_new.vue';
-
-jest.mock('images/no_avatar.png', () => 'default-avatar-url');
-
-const PROVIDED_PROPS = {
- size: 32,
- imgSrc: 'myavatarurl.com',
- imgAlt: 'mydisplayname',
- cssClasses: 'myextraavatarclass',
- tooltipText: 'tooltip text',
- tooltipPlacement: 'bottom',
-};
-
-describe('User Avatar Image Component', () => {
- let wrapper;
-
- const findAvatar = () => wrapper.findComponent(GlAvatar);
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('Initialization', () => {
- beforeEach(() => {
- wrapper = shallowMount(UserAvatarImage, {
- propsData: {
- ...PROVIDED_PROPS,
- },
- });
- });
-
- it('should render `GlAvatar` and provide correct properties to it', () => {
- expect(findAvatar().attributes('data-src')).toBe(
- `${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
- );
- expect(findAvatar().props()).toMatchObject({
- src: `${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
- alt: PROVIDED_PROPS.imgAlt,
- size: PROVIDED_PROPS.size,
- });
- });
-
- it('should add correct CSS classes', () => {
- const classes = wrapper.findComponent(GlAvatar).classes();
- expect(classes).toContain(PROVIDED_PROPS.cssClasses);
- expect(classes).not.toContain('lazy');
- });
- });
-
- describe('Initialization when lazy', () => {
- beforeEach(() => {
- wrapper = shallowMount(UserAvatarImage, {
- propsData: {
- ...PROVIDED_PROPS,
- lazy: true,
- },
- });
- });
-
- it('should add lazy attributes', () => {
- expect(findAvatar().classes()).toContain('lazy');
- expect(findAvatar().attributes()).toMatchObject({
- src: placeholderImage,
- 'data-src': `${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
- });
- });
-
- it('should use maximum number when size is provided as an object', () => {
- wrapper = shallowMount(UserAvatarImage, {
- propsData: {
- ...PROVIDED_PROPS,
- size: { default: 16, md: 64, lg: 24 },
- lazy: true,
- },
- });
-
- expect(findAvatar().attributes('data-src')).toBe(`${PROVIDED_PROPS.imgSrc}?width=${64}`);
- });
- });
-
- describe('Initialization without src', () => {
- beforeEach(() => {
- wrapper = shallowMount(UserAvatarImage, {
- propsData: {
- ...PROVIDED_PROPS,
- imgSrc: null,
- },
- });
- });
-
- it('should have default avatar image', () => {
- expect(findAvatar().props('src')).toBe(`${defaultAvatarUrl}?width=${PROVIDED_PROPS.size}`);
- });
- });
-
- describe('Dynamic tooltip content', () => {
- const slots = {
- default: ['Action!'],
- };
-
- describe('when `tooltipText` is provided and no default slot', () => {
- beforeEach(() => {
- wrapper = shallowMount(UserAvatarImage, {
- propsData: { ...PROVIDED_PROPS },
- });
- });
-
- it('renders the tooltip with `tooltipText` as content', () => {
- expect(wrapper.findComponent(GlTooltip).text()).toBe(PROVIDED_PROPS.tooltipText);
- });
- });
-
- describe('when `tooltipText` and default slot is provided', () => {
- beforeEach(() => {
- wrapper = shallowMount(UserAvatarImage, {
- propsData: { ...PROVIDED_PROPS },
- slots,
- });
- });
-
- it('does not render `tooltipText` inside the tooltip', () => {
- expect(wrapper.findComponent(GlTooltip).text()).not.toBe(PROVIDED_PROPS.tooltipText);
- });
-
- it('renders the content provided via default slot', () => {
- expect(wrapper.findComponent(GlTooltip).text()).toContain(slots.default[0]);
- });
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_old_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_old_spec.js
deleted file mode 100644
index 2c1be6ec47e..00000000000
--- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_old_spec.js
+++ /dev/null
@@ -1,127 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { GlTooltip } from '@gitlab/ui';
-import defaultAvatarUrl from 'images/no_avatar.png';
-import { placeholderImage } from '~/lazy_loader';
-import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image_old.vue';
-
-jest.mock('images/no_avatar.png', () => 'default-avatar-url');
-
-const PROVIDED_PROPS = {
- size: 32,
- imgSrc: 'myavatarurl.com',
- imgAlt: 'mydisplayname',
- cssClasses: 'myextraavatarclass',
- tooltipText: 'tooltip text',
- tooltipPlacement: 'bottom',
-};
-
-const DEFAULT_PROPS = {
- size: 20,
-};
-
-describe('User Avatar Image Component', () => {
- let wrapper;
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('Initialization', () => {
- beforeEach(() => {
- wrapper = shallowMount(UserAvatarImage, {
- propsData: {
- ...PROVIDED_PROPS,
- },
- });
- });
-
- it('should have <img> as a child element', () => {
- const imageElement = wrapper.find('img');
-
- expect(imageElement.exists()).toBe(true);
- expect(imageElement.attributes('src')).toBe(
- `${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
- );
- expect(imageElement.attributes('data-src')).toBe(
- `${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
- );
- expect(imageElement.attributes('alt')).toBe(PROVIDED_PROPS.imgAlt);
- });
-
- it('should properly render img css', () => {
- const classes = wrapper.find('img').classes();
- expect(classes).toEqual(['avatar', 's32', PROVIDED_PROPS.cssClasses]);
- expect(classes).not.toContain('lazy');
- });
- });
-
- describe('Initialization when lazy', () => {
- beforeEach(() => {
- wrapper = shallowMount(UserAvatarImage, {
- propsData: {
- ...PROVIDED_PROPS,
- lazy: true,
- },
- });
- });
-
- it('should add lazy attributes', () => {
- const imageElement = wrapper.find('img');
-
- expect(imageElement.classes()).toContain('lazy');
- expect(imageElement.attributes('src')).toBe(placeholderImage);
- expect(imageElement.attributes('data-src')).toBe(
- `${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
- );
- });
- });
-
- describe('Initialization without src', () => {
- beforeEach(() => {
- wrapper = shallowMount(UserAvatarImage);
- });
-
- it('should have default avatar image', () => {
- const imageElement = wrapper.find('img');
-
- expect(imageElement.attributes('src')).toBe(
- `${defaultAvatarUrl}?width=${DEFAULT_PROPS.size}`,
- );
- });
- });
-
- describe('Dynamic tooltip content', () => {
- const slots = {
- default: ['Action!'],
- };
-
- describe('when `tooltipText` is provided and no default slot', () => {
- beforeEach(() => {
- wrapper = shallowMount(UserAvatarImage, {
- propsData: { ...PROVIDED_PROPS },
- });
- });
-
- it('renders the tooltip with `tooltipText` as content', () => {
- expect(wrapper.findComponent(GlTooltip).text()).toBe(PROVIDED_PROPS.tooltipText);
- });
- });
-
- describe('when `tooltipText` and default slot is provided', () => {
- beforeEach(() => {
- wrapper = shallowMount(UserAvatarImage, {
- propsData: { ...PROVIDED_PROPS },
- slots,
- });
- });
-
- it('does not render `tooltipText` inside the tooltip', () => {
- expect(wrapper.findComponent(GlTooltip).text()).not.toBe(PROVIDED_PROPS.tooltipText);
- });
-
- it('renders the content provided via default slot', () => {
- expect(wrapper.findComponent(GlTooltip).text()).toContain(slots.default[0]);
- });
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_spec.js
index 6ad2ef226c2..d63b13981ac 100644
--- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_spec.js
+++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_spec.js
@@ -1,7 +1,10 @@
import { shallowMount } from '@vue/test-utils';
+import { GlAvatar, GlTooltip } from '@gitlab/ui';
+import defaultAvatarUrl from 'images/no_avatar.png';
+import { placeholderImage } from '~/lazy_loader';
import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
-import UserAvatarImageNew from '~/vue_shared/components/user_avatar/user_avatar_image_new.vue';
-import UserAvatarImageOld from '~/vue_shared/components/user_avatar/user_avatar_image_old.vue';
+
+jest.mock('images/no_avatar.png', () => 'default-avatar-url');
const PROVIDED_PROPS = {
size: 32,
@@ -15,37 +18,117 @@ const PROVIDED_PROPS = {
describe('User Avatar Image Component', () => {
let wrapper;
- const createWrapper = (props = {}, { glAvatarForAllUserAvatars } = {}) => {
- wrapper = shallowMount(UserAvatarImage, {
- propsData: {
- ...PROVIDED_PROPS,
- ...props,
- },
- provide: {
- glFeatures: {
- glAvatarForAllUserAvatars,
- },
- },
- });
- };
+ const findAvatar = () => wrapper.findComponent(GlAvatar);
afterEach(() => {
wrapper.destroy();
});
- describe.each([
- [false, true, true],
- [true, false, true],
- [true, true, true],
- [false, false, false],
- ])(
- 'when glAvatarForAllUserAvatars=%s and enforceGlAvatar=%s',
- (glAvatarForAllUserAvatars, enforceGlAvatar, isUsingNewVersion) => {
- it(`will render ${isUsingNewVersion ? 'new' : 'old'} version`, () => {
- createWrapper({ enforceGlAvatar }, { glAvatarForAllUserAvatars });
- expect(wrapper.findComponent(UserAvatarImageNew).exists()).toBe(isUsingNewVersion);
- expect(wrapper.findComponent(UserAvatarImageOld).exists()).toBe(!isUsingNewVersion);
- });
- },
- );
+ describe('Initialization', () => {
+ beforeEach(() => {
+ wrapper = shallowMount(UserAvatarImage, {
+ propsData: {
+ ...PROVIDED_PROPS,
+ },
+ });
+ });
+
+ it('should render `GlAvatar` and provide correct properties to it', () => {
+ expect(findAvatar().attributes('data-src')).toBe(
+ `${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
+ );
+ expect(findAvatar().props()).toMatchObject({
+ src: `${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
+ alt: PROVIDED_PROPS.imgAlt,
+ size: PROVIDED_PROPS.size,
+ });
+ });
+
+ it('should add correct CSS classes', () => {
+ const classes = wrapper.findComponent(GlAvatar).classes();
+ expect(classes).toContain(PROVIDED_PROPS.cssClasses);
+ expect(classes).not.toContain('lazy');
+ });
+ });
+
+ describe('Initialization when lazy', () => {
+ beforeEach(() => {
+ wrapper = shallowMount(UserAvatarImage, {
+ propsData: {
+ ...PROVIDED_PROPS,
+ lazy: true,
+ },
+ });
+ });
+
+ it('should add lazy attributes', () => {
+ expect(findAvatar().classes()).toContain('lazy');
+ expect(findAvatar().attributes()).toMatchObject({
+ src: placeholderImage,
+ 'data-src': `${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
+ });
+ });
+
+ it('should use maximum number when size is provided as an object', () => {
+ wrapper = shallowMount(UserAvatarImage, {
+ propsData: {
+ ...PROVIDED_PROPS,
+ size: { default: 16, md: 64, lg: 24 },
+ lazy: true,
+ },
+ });
+
+ expect(findAvatar().attributes('data-src')).toBe(`${PROVIDED_PROPS.imgSrc}?width=${64}`);
+ });
+ });
+
+ describe('Initialization without src', () => {
+ beforeEach(() => {
+ wrapper = shallowMount(UserAvatarImage, {
+ propsData: {
+ ...PROVIDED_PROPS,
+ imgSrc: null,
+ },
+ });
+ });
+
+ it('should have default avatar image', () => {
+ expect(findAvatar().props('src')).toBe(`${defaultAvatarUrl}?width=${PROVIDED_PROPS.size}`);
+ });
+ });
+
+ describe('Dynamic tooltip content', () => {
+ const slots = {
+ default: ['Action!'],
+ };
+
+ describe('when `tooltipText` is provided and no default slot', () => {
+ beforeEach(() => {
+ wrapper = shallowMount(UserAvatarImage, {
+ propsData: { ...PROVIDED_PROPS },
+ });
+ });
+
+ it('renders the tooltip with `tooltipText` as content', () => {
+ expect(wrapper.findComponent(GlTooltip).text()).toBe(PROVIDED_PROPS.tooltipText);
+ });
+ });
+
+ describe('when `tooltipText` and default slot is provided', () => {
+ beforeEach(() => {
+ wrapper = shallowMount(UserAvatarImage, {
+ propsData: { ...PROVIDED_PROPS },
+ slots,
+ });
+ });
+
+ it('does not render `tooltipText` inside the tooltip', () => {
+ expect(wrapper.findComponent(GlTooltip).text()).not.toBe(PROVIDED_PROPS.tooltipText);
+ });
+
+ it('renders the content provided via default slot', () => {
+ expect(wrapper.findComponent(GlTooltip).text()).toContain(slots.default[0]);
+ });
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_new_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_new_spec.js
deleted file mode 100644
index f485a14cfea..00000000000
--- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_new_spec.js
+++ /dev/null
@@ -1,103 +0,0 @@
-import { GlAvatarLink } from '@gitlab/ui';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { TEST_HOST } from 'spec/test_constants';
-import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
-import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link_new.vue';
-
-describe('User Avatar Link Component', () => {
- let wrapper;
-
- const findUserName = () => wrapper.findByTestId('user-avatar-link-username');
-
- const defaultProps = {
- linkHref: `${TEST_HOST}/myavatarurl.com`,
- imgSize: 32,
- imgSrc: `${TEST_HOST}/myavatarurl.com`,
- imgAlt: 'mydisplayname',
- imgCssClasses: 'myextraavatarclass',
- tooltipText: 'tooltip text',
- tooltipPlacement: 'bottom',
- username: 'username',
- };
-
- const createWrapper = (props, slots) => {
- wrapper = shallowMountExtended(UserAvatarLink, {
- propsData: {
- ...defaultProps,
- ...props,
- ...slots,
- },
- });
- };
-
- beforeEach(() => {
- createWrapper();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('should render GlLink with correct props', () => {
- const link = wrapper.findComponent(GlAvatarLink);
- expect(link.exists()).toBe(true);
- expect(link.attributes('href')).toBe(defaultProps.linkHref);
- });
-
- it('should render UserAvatarImage and provide correct props to it', () => {
- expect(wrapper.findComponent(UserAvatarImage).exists()).toBe(true);
- expect(wrapper.findComponent(UserAvatarImage).props()).toEqual({
- cssClasses: defaultProps.imgCssClasses,
- imgAlt: defaultProps.imgAlt,
- imgSrc: defaultProps.imgSrc,
- lazy: false,
- size: defaultProps.imgSize,
- tooltipPlacement: defaultProps.tooltipPlacement,
- tooltipText: '',
- enforceGlAvatar: false,
- });
- });
-
- describe('when username provided', () => {
- beforeEach(() => {
- createWrapper({ username: defaultProps.username });
- });
-
- it('should render provided username', () => {
- expect(findUserName().text()).toBe(defaultProps.username);
- });
-
- it('should provide the tooltip data for the username', () => {
- expect(findUserName().attributes()).toEqual(
- expect.objectContaining({
- title: defaultProps.tooltipText,
- 'tooltip-placement': defaultProps.tooltipPlacement,
- }),
- );
- });
- });
-
- describe('when username is NOT provided', () => {
- beforeEach(() => {
- createWrapper({ username: '' });
- });
-
- it('should NOT render username', () => {
- expect(findUserName().exists()).toBe(false);
- });
- });
-
- describe('avatar-badge slot', () => {
- const badge = '<span>User badge</span>';
-
- beforeEach(() => {
- createWrapper(defaultProps, {
- 'avatar-badge': badge,
- });
- });
-
- it('should render provided `avatar-badge` slot content', () => {
- expect(wrapper.html()).toContain(badge);
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_old_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_old_spec.js
deleted file mode 100644
index cf7a1025dba..00000000000
--- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_old_spec.js
+++ /dev/null
@@ -1,103 +0,0 @@
-import { GlLink } from '@gitlab/ui';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { TEST_HOST } from 'spec/test_constants';
-import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
-import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link_old.vue';
-
-describe('User Avatar Link Component', () => {
- let wrapper;
-
- const findUserName = () => wrapper.find('[data-testid="user-avatar-link-username"]');
-
- const defaultProps = {
- linkHref: `${TEST_HOST}/myavatarurl.com`,
- imgSize: 32,
- imgSrc: `${TEST_HOST}/myavatarurl.com`,
- imgAlt: 'mydisplayname',
- imgCssClasses: 'myextraavatarclass',
- tooltipText: 'tooltip text',
- tooltipPlacement: 'bottom',
- username: 'username',
- };
-
- const createWrapper = (props, slots) => {
- wrapper = shallowMountExtended(UserAvatarLink, {
- propsData: {
- ...defaultProps,
- ...props,
- ...slots,
- },
- });
- };
-
- beforeEach(() => {
- createWrapper();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('should render GlLink with correct props', () => {
- const link = wrapper.findComponent(GlLink);
- expect(link.exists()).toBe(true);
- expect(link.attributes('href')).toBe(defaultProps.linkHref);
- });
-
- it('should render UserAvatarImage and povide correct props to it', () => {
- expect(wrapper.findComponent(UserAvatarImage).exists()).toBe(true);
- expect(wrapper.findComponent(UserAvatarImage).props()).toEqual({
- cssClasses: defaultProps.imgCssClasses,
- imgAlt: defaultProps.imgAlt,
- imgSrc: defaultProps.imgSrc,
- lazy: false,
- size: defaultProps.imgSize,
- tooltipPlacement: defaultProps.tooltipPlacement,
- tooltipText: '',
- enforceGlAvatar: false,
- });
- });
-
- describe('when username provided', () => {
- beforeEach(() => {
- createWrapper({ username: defaultProps.username });
- });
-
- it('should render provided username', () => {
- expect(findUserName().text()).toBe(defaultProps.username);
- });
-
- it('should provide the tooltip data for the username', () => {
- expect(findUserName().attributes()).toEqual(
- expect.objectContaining({
- title: defaultProps.tooltipText,
- 'tooltip-placement': defaultProps.tooltipPlacement,
- }),
- );
- });
- });
-
- describe('when username is NOT provided', () => {
- beforeEach(() => {
- createWrapper({ username: '' });
- });
-
- it('should NOT render username', () => {
- expect(findUserName().exists()).toBe(false);
- });
- });
-
- describe('avatar-badge slot', () => {
- const badge = '<span>User badge</span>';
-
- beforeEach(() => {
- createWrapper(defaultProps, {
- 'avatar-badge': badge,
- });
- });
-
- it('should render provided `avatar-badge` slot content', () => {
- expect(wrapper.html()).toContain(badge);
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js
index fd3f59008ec..df7ce449678 100644
--- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js
+++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js
@@ -1,51 +1,102 @@
-import { shallowMount } from '@vue/test-utils';
+import { GlAvatarLink } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { TEST_HOST } from 'spec/test_constants';
+import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
-import UserAvatarLinkNew from '~/vue_shared/components/user_avatar/user_avatar_link_new.vue';
-import UserAvatarLinkOld from '~/vue_shared/components/user_avatar/user_avatar_link_old.vue';
-
-const PROVIDED_PROPS = {
- size: 32,
- imgSrc: 'myavatarurl.com',
- imgAlt: 'mydisplayname',
- cssClasses: 'myextraavatarclass',
- tooltipText: 'tooltip text',
- tooltipPlacement: 'bottom',
-};
describe('User Avatar Link Component', () => {
let wrapper;
- const createWrapper = (props = {}, { glAvatarForAllUserAvatars } = {}) => {
- wrapper = shallowMount(UserAvatarLink, {
+ const findUserName = () => wrapper.findByTestId('user-avatar-link-username');
+
+ const defaultProps = {
+ linkHref: `${TEST_HOST}/myavatarurl.com`,
+ imgSize: 32,
+ imgSrc: `${TEST_HOST}/myavatarurl.com`,
+ imgAlt: 'mydisplayname',
+ imgCssClasses: 'myextraavatarclass',
+ tooltipText: 'tooltip text',
+ tooltipPlacement: 'bottom',
+ username: 'username',
+ };
+
+ const createWrapper = (props, slots) => {
+ wrapper = shallowMountExtended(UserAvatarLink, {
propsData: {
- ...PROVIDED_PROPS,
+ ...defaultProps,
...props,
- },
- provide: {
- glFeatures: {
- glAvatarForAllUserAvatars,
- },
+ ...slots,
},
});
};
+ beforeEach(() => {
+ createWrapper();
+ });
+
afterEach(() => {
wrapper.destroy();
});
- describe.each([
- [false, true, true],
- [true, false, true],
- [true, true, true],
- [false, false, false],
- ])(
- 'when glAvatarForAllUserAvatars=%s and enforceGlAvatar=%s',
- (glAvatarForAllUserAvatars, enforceGlAvatar, isUsingNewVersion) => {
- it(`will render ${isUsingNewVersion ? 'new' : 'old'} version`, () => {
- createWrapper({ enforceGlAvatar }, { glAvatarForAllUserAvatars });
- expect(wrapper.findComponent(UserAvatarLinkNew).exists()).toBe(isUsingNewVersion);
- expect(wrapper.findComponent(UserAvatarLinkOld).exists()).toBe(!isUsingNewVersion);
+ it('should render GlLink with correct props', () => {
+ const link = wrapper.findComponent(GlAvatarLink);
+ expect(link.exists()).toBe(true);
+ expect(link.attributes('href')).toBe(defaultProps.linkHref);
+ });
+
+ it('should render UserAvatarImage and provide correct props to it', () => {
+ expect(wrapper.findComponent(UserAvatarImage).exists()).toBe(true);
+ expect(wrapper.findComponent(UserAvatarImage).props()).toEqual({
+ cssClasses: defaultProps.imgCssClasses,
+ imgAlt: defaultProps.imgAlt,
+ imgSrc: defaultProps.imgSrc,
+ lazy: false,
+ size: defaultProps.imgSize,
+ tooltipPlacement: defaultProps.tooltipPlacement,
+ tooltipText: '',
+ });
+ });
+
+ describe('when username provided', () => {
+ beforeEach(() => {
+ createWrapper({ username: defaultProps.username });
+ });
+
+ it('should render provided username', () => {
+ expect(findUserName().text()).toBe(defaultProps.username);
+ });
+
+ it('should provide the tooltip data for the username', () => {
+ expect(findUserName().attributes()).toEqual(
+ expect.objectContaining({
+ title: defaultProps.tooltipText,
+ 'tooltip-placement': defaultProps.tooltipPlacement,
+ }),
+ );
+ });
+ });
+
+ describe('when username is NOT provided', () => {
+ beforeEach(() => {
+ createWrapper({ username: '' });
+ });
+
+ it('should NOT render username', () => {
+ expect(findUserName().exists()).toBe(false);
+ });
+ });
+
+ describe('avatar-badge slot', () => {
+ const badge = '<span>User badge</span>';
+
+ beforeEach(() => {
+ createWrapper(defaultProps, {
+ 'avatar-badge': badge,
});
- },
- );
+ });
+
+ it('should render provided `avatar-badge` slot content', () => {
+ expect(wrapper.html()).toContain(badge);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js
index b9accbf0373..1ad6d043399 100644
--- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js
+++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js
@@ -153,29 +153,4 @@ describe('UserAvatarList', () => {
});
});
});
-
- describe('additional styling for the image', () => {
- it('should not add CSS class when feature flag `glAvatarForAllUserAvatars` is disabled', () => {
- factory({
- propsData: { items: createList(1) },
- });
-
- const link = wrapper.findComponent(UserAvatarLink);
- expect(link.props('imgCssClasses')).not.toBe('gl-mr-3');
- });
-
- it('should add CSS class when feature flag `glAvatarForAllUserAvatars` is enabled', () => {
- factory({
- propsData: { items: createList(1) },
- provide: {
- glFeatures: {
- glAvatarForAllUserAvatars: true,
- },
- },
- });
-
- const link = wrapper.findComponent(UserAvatarLink);
- expect(link.props('imgCssClasses')).toBe('gl-mr-3');
- });
- });
});
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 6d48000beb0..f6316af6ad8 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
@@ -8,10 +8,12 @@ import {
I18N_USER_BLOCKED,
I18N_USER_LEARN,
I18N_USER_FOLLOW,
+ I18N_ERROR_FOLLOW,
I18N_USER_UNFOLLOW,
+ I18N_ERROR_UNFOLLOW,
} from '~/vue_shared/components/user_popover/constants';
import axios from '~/lib/utils/axios_utils';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { followUser, unfollowUser } from '~/api/user_api';
import { mockTracking } from 'helpers/tracking_helper';
@@ -239,6 +241,18 @@ describe('User Popover Component', () => {
expect(wrapper.html()).toContain('<gl-emoji data-name="basketball_player"');
});
+ it('should show only emoji', () => {
+ const user = {
+ ...DEFAULT_PROPS.user,
+ status: { emoji: 'basketball_player' },
+ };
+
+ createWrapper({ user });
+
+ expect(findUserStatus().exists()).toBe(true);
+ expect(wrapper.html()).toContain('<gl-emoji data-name="basketball_player"');
+ });
+
it('hides the div when status is null', () => {
const user = { ...DEFAULT_PROPS.user, status: null };
@@ -367,27 +381,49 @@ describe('User Popover Component', () => {
itTracksToggleFollowButtonClick('follow_from_user_popover');
describe('when an error occurs', () => {
- beforeEach(() => {
- followUser.mockRejectedValue({});
+ describe('api send error message', () => {
+ const mockedMessage = sprintf(I18N_ERROR_UNFOLLOW, { limit: 300 });
+ const apiResponse = { response: { data: { message: mockedMessage } } };
- findToggleFollowButton().trigger('click');
- });
+ beforeEach(() => {
+ followUser.mockRejectedValue(apiResponse);
+ findToggleFollowButton().trigger('click');
+ });
- it('shows an error message', async () => {
- await axios.waitForAll();
+ it('show an error message from api response', async () => {
+ await axios.waitForAll();
- expect(createFlash).toHaveBeenCalledWith({
- message: 'An error occurred while trying to follow this user, please try again.',
- error: {},
- captureError: true,
+ expect(createAlert).toHaveBeenCalledWith({
+ message: mockedMessage,
+ error: apiResponse,
+ captureError: true,
+ });
});
});
- it('emits no events', async () => {
- await axios.waitForAll();
+ describe('api did not send error message', () => {
+ beforeEach(() => {
+ followUser.mockRejectedValue({});
- expect(wrapper.emitted().follow).toBeUndefined();
- expect(wrapper.emitted().unfollow).toBeUndefined();
+ findToggleFollowButton().trigger('click');
+ });
+
+ it('shows an error message', async () => {
+ await axios.waitForAll();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: I18N_ERROR_FOLLOW,
+ error: {},
+ captureError: true,
+ });
+ });
+
+ it('emits no events', async () => {
+ await axios.waitForAll();
+
+ expect(wrapper.emitted().follow).toBeUndefined();
+ expect(wrapper.emitted().unfollow).toBeUndefined();
+ });
});
});
});
@@ -425,8 +461,8 @@ describe('User Popover Component', () => {
});
it('shows an error message', () => {
- expect(createFlash).toHaveBeenCalledWith({
- message: 'An error occurred while trying to unfollow this user, please try again.',
+ expect(createAlert).toHaveBeenCalledWith({
+ message: I18N_ERROR_UNFOLLOW,
error: {},
captureError: true,
});
diff --git a/spec/frontend/vue_shared/directives/safe_html_spec.js b/spec/frontend/vue_shared/directives/safe_html_spec.js
new file mode 100644
index 00000000000..ba1de8e4596
--- /dev/null
+++ b/spec/frontend/vue_shared/directives/safe_html_spec.js
@@ -0,0 +1,116 @@
+import { shallowMount } from '@vue/test-utils';
+import safeHtml from '~/vue_shared/directives/safe_html';
+import { defaultConfig } from '~/lib/dompurify';
+/* eslint-disable no-script-url */
+const invalidProtocolUrls = [
+ 'javascript:alert(1)',
+ 'jAvascript:alert(1)',
+ 'data:text/html,<script>alert(1);</script>',
+ ' javascript:',
+ 'javascript :',
+];
+/* eslint-enable no-script-url */
+const validProtocolUrls = ['slack://open', 'x-devonthink-item://90909', 'x-devonthink-item:90909'];
+
+describe('safe html directive', () => {
+ let wrapper;
+
+ const createComponent = ({ template, html, config } = {}) => {
+ const defaultTemplate = `<div v-safe-html="rawHtml"></div>`;
+ const defaultHtml = 'hello <script>alert(1)</script>world';
+
+ const component = {
+ directives: {
+ safeHtml,
+ },
+ data() {
+ return {
+ rawHtml: html || defaultHtml,
+ config: config || {},
+ };
+ },
+ template: template || defaultTemplate,
+ };
+
+ wrapper = shallowMount(component);
+ };
+
+ describe('default', () => {
+ it('should remove the script tag', () => {
+ createComponent();
+
+ expect(wrapper.html()).toEqual('<div>hello world</div>');
+ });
+
+ it('should remove javascript hrefs', () => {
+ createComponent({ html: '<a href="javascript:prompt(1)">click here</a>' });
+
+ expect(wrapper.html()).toEqual('<div><a>click here</a></div>');
+ });
+
+ it('should remove any existing children', () => {
+ createComponent({
+ template: `<div v-safe-html="rawHtml">foo <i>bar</i></div>`,
+ });
+
+ expect(wrapper.html()).toEqual('<div>hello world</div>');
+ });
+
+ describe('with non-http links', () => {
+ it.each(validProtocolUrls)('should allow %s', (url) => {
+ createComponent({
+ html: `<a href="${url}">internal link</a>`,
+ });
+ expect(wrapper.html()).toContain(`<a href="${url}">internal link</a>`);
+ });
+
+ it.each(invalidProtocolUrls)('should not allow %s', (url) => {
+ createComponent({
+ html: `<a href="${url}">internal link</a>`,
+ });
+ expect(wrapper.html()).toContain(`<a>internal link</a>`);
+ });
+ });
+
+ describe('handles data attributes correctly', () => {
+ const allowedDataAttrs = ['data-safe', 'data-random'];
+
+ it.each(defaultConfig.FORBID_ATTR)('removes dangerous `%s` attribute', (attr) => {
+ const html = `<a ${attr}="true"></a>`;
+ createComponent({ html });
+
+ expect(wrapper.html()).not.toContain(html);
+ });
+
+ it.each(allowedDataAttrs)('does not remove allowed `%s` attribute', (attr) => {
+ const html = `<a ${attr}="true"></a>`;
+ createComponent({ html });
+
+ expect(wrapper.html()).toContain(html);
+ });
+ });
+ });
+
+ describe('advance config', () => {
+ const template = '<div v-safe-html:[config]="rawHtml"></div>';
+ it('should only allow <b> tags', () => {
+ createComponent({
+ template,
+ html: '<a href="javascript:prompt(1)"><b>click here</b></a>',
+ config: { ALLOWED_TAGS: ['b'] },
+ });
+
+ expect(wrapper.html()).toEqual('<div><b>click here</b></div>');
+ });
+
+ it('should strip all html tags', () => {
+ createComponent({
+ template,
+ html: '<a href="javascript:prompt(1)"><u>click here</u></a>',
+ config: { ALLOWED_TAGS: [] },
+ });
+
+ expect(wrapper.html()).toEqual('<div>click here</div>');
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/issuable/__snapshots__/issuable_blocked_icon_spec.js.snap b/spec/frontend/vue_shared/issuable/__snapshots__/issuable_blocked_icon_spec.js.snap
new file mode 100644
index 00000000000..dd011b9d84e
--- /dev/null
+++ b/spec/frontend/vue_shared/issuable/__snapshots__/issuable_blocked_icon_spec.js.snap
@@ -0,0 +1,30 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`IssuableBlockedIcon on mouseenter on blocked icon with more than three blocking issues matches the snapshot 1`] = `
+"<div class=\\"gl-display-inline\\"><svg data-testid=\\"issuable-blocked-icon\\" role=\\"img\\" aria-hidden=\\"true\\" class=\\"issuable-blocked-icon gl-mr-2 gl-cursor-pointer gl-text-red-500 gl-icon s16\\" id=\\"blocked-icon-uniqueId\\">
+ <use href=\\"#issue-block\\"></use>
+ </svg>
+ <div class=\\"gl-popover\\">
+ <ul class=\\"gl-list-style-none gl-p-0 gl-mb-0\\">
+ <li><a href=\\"http://gdk.test:3000/gitlab-org/my-project-1/-/issues/6\\" class=\\"gl-link gl-text-blue-500! gl-font-sm\\">my-project-1#6</a>
+ <p data-testid=\\"issuable-title\\" class=\\"gl-display-block! gl-mb-3\\">
+ blocking issue title 1
+ </p>
+ </li>
+ <li><a href=\\"http://gdk.test:3000/gitlab-org/my-project-1/-/issues/5\\" class=\\"gl-link gl-text-blue-500! gl-font-sm\\">my-project-1#5</a>
+ <p data-testid=\\"issuable-title\\" class=\\"gl-display-block! gl-mb-3\\">
+ blocking issue title 2 + blocking issue title 2 + blocking issue title 2 + bloc…
+ </p>
+ </li>
+ <li><a href=\\"http://gdk.test:3000/gitlab-org/my-project-1/-/issues/4\\" class=\\"gl-link gl-text-blue-500! gl-font-sm\\">my-project-1#4</a>
+ <p data-testid=\\"issuable-title\\" class=\\"gl-display-block! gl-mb-0\\">
+ blocking issue title 3
+ </p>
+ </li>
+ </ul>
+ <div class=\\"gl-mt-4\\">
+ <p data-testid=\\"hidden-blocking-count\\" class=\\"gl-mb-3\\">+ 1 more issue</p> <a data-testid=\\"view-all-issues\\" href=\\"http://gdk.test:3000/gitlab-org/my-project-1/-/issues/0#related-issues\\" class=\\"gl-link gl-text-blue-500! gl-font-sm\\">View all blocking issues</a>
+ </div><span data-testid=\\"popover-title\\">Blocked by 4 issues</span>
+ </div>
+</div>"
+`;
diff --git a/spec/frontend/vue_shared/issuable/issuable_blocked_icon_spec.js b/spec/frontend/vue_shared/issuable/issuable_blocked_icon_spec.js
new file mode 100644
index 00000000000..d59cbce6633
--- /dev/null
+++ b/spec/frontend/vue_shared/issuable/issuable_blocked_icon_spec.js
@@ -0,0 +1,265 @@
+import { GlIcon, GlLink, GlPopover, GlLoadingIcon } from '@gitlab/ui';
+import { shallowMount, mount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import IssuableBlockedIcon from '~/vue_shared/components/issuable_blocked_icon/issuable_blocked_icon.vue';
+import { blockingIssuablesQueries } from '~/vue_shared/components/issuable_blocked_icon/constants';
+import { issuableTypes } from '~/boards/constants';
+import { truncate } from '~/lib/utils/text_utility';
+import {
+ mockIssue,
+ mockEpic,
+ mockBlockingIssue1,
+ mockBlockingIssue2,
+ mockBlockingEpic1,
+ mockBlockingIssuablesResponse1,
+ mockBlockingIssuablesResponse2,
+ mockBlockingIssuablesResponse3,
+ mockBlockedIssue1,
+ mockBlockedIssue2,
+ mockBlockedEpic1,
+ mockBlockingEpicIssuablesResponse1,
+} from '../../boards/mock_data';
+
+describe('IssuableBlockedIcon', () => {
+ let wrapper;
+ let mockApollo;
+
+ const findGlIcon = () => wrapper.findComponent(GlIcon);
+ const findGlPopover = () => wrapper.findComponent(GlPopover);
+ const findGlLink = () => wrapper.findComponent(GlLink);
+ const findPopoverTitle = () => wrapper.findByTestId('popover-title');
+ const findIssuableTitle = () => wrapper.findByTestId('issuable-title');
+ const findHiddenBlockingCount = () => wrapper.findByTestId('hidden-blocking-count');
+ const findViewAllIssuableLink = () => wrapper.findByTestId('view-all-issues');
+
+ const waitForApollo = async () => {
+ jest.runOnlyPendingTimers();
+ await waitForPromises();
+ };
+
+ const mouseenter = async () => {
+ findGlIcon().vm.$emit('mouseenter');
+
+ await nextTick();
+ await waitForApollo();
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ const createWrapperWithApollo = ({
+ item = mockBlockedIssue1,
+ blockingIssuablesSpy = jest.fn().mockResolvedValue(mockBlockingIssuablesResponse1),
+ issuableItem = mockIssue,
+ issuableType = issuableTypes.issue,
+ } = {}) => {
+ mockApollo = createMockApollo([
+ [blockingIssuablesQueries[issuableType].query, blockingIssuablesSpy],
+ ]);
+
+ Vue.use(VueApollo);
+ wrapper = extendedWrapper(
+ mount(IssuableBlockedIcon, {
+ apolloProvider: mockApollo,
+ propsData: {
+ item: {
+ ...issuableItem,
+ ...item,
+ },
+ uniqueId: 'uniqueId',
+ issuableType,
+ },
+ attachTo: document.body,
+ }),
+ );
+ };
+
+ const createWrapper = ({
+ item = {},
+ queries = {},
+ data = {},
+ loading = false,
+ mockIssuable = mockIssue,
+ issuableType = issuableTypes.issue,
+ } = {}) => {
+ wrapper = extendedWrapper(
+ shallowMount(IssuableBlockedIcon, {
+ propsData: {
+ item: {
+ ...mockIssuable,
+ ...item,
+ },
+ uniqueId: 'uniqueid',
+ issuableType,
+ },
+ data() {
+ return {
+ ...data,
+ };
+ },
+ mocks: {
+ $apollo: {
+ queries: {
+ blockingIssuables: { loading },
+ ...queries,
+ },
+ },
+ },
+ stubs: {
+ GlPopover,
+ },
+ attachTo: document.body,
+ }),
+ );
+ };
+
+ it.each`
+ mockIssuable | issuableType | expectedIcon
+ ${mockIssue} | ${issuableTypes.issue} | ${'issue-block'}
+ ${mockEpic} | ${issuableTypes.epic} | ${'entity-blocked'}
+ `(
+ 'should render blocked icon for $issuableType',
+ ({ mockIssuable, issuableType, expectedIcon }) => {
+ createWrapper({
+ mockIssuable,
+ issuableType,
+ });
+
+ expect(findGlIcon().exists()).toBe(true);
+ const icon = findGlIcon();
+ expect(icon.exists()).toBe(true);
+ expect(icon.props('name')).toBe(expectedIcon);
+ },
+ );
+
+ it('should display a loading spinner while loading', () => {
+ createWrapper({ loading: true });
+
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
+ });
+
+ it('should not query for blocking issuables by default', async () => {
+ createWrapperWithApollo();
+
+ expect(findGlPopover().text()).not.toContain(mockBlockingIssue1.title);
+ });
+
+ describe('on mouseenter on blocked icon', () => {
+ it.each`
+ item | issuableType | mockBlockingIssuable | issuableItem | blockingIssuablesSpy
+ ${mockBlockedIssue1} | ${issuableTypes.issue} | ${mockBlockingIssue1} | ${mockIssue} | ${jest.fn().mockResolvedValue(mockBlockingIssuablesResponse1)}
+ ${mockBlockedEpic1} | ${issuableTypes.epic} | ${mockBlockingEpic1} | ${mockEpic} | ${jest.fn().mockResolvedValue(mockBlockingEpicIssuablesResponse1)}
+ `(
+ 'should query for blocking issuables and render the result for $issuableType',
+ async ({ item, issuableType, issuableItem, mockBlockingIssuable, blockingIssuablesSpy }) => {
+ createWrapperWithApollo({
+ item,
+ issuableType,
+ issuableItem,
+ blockingIssuablesSpy,
+ });
+
+ expect(findGlPopover().text()).not.toContain(mockBlockingIssuable.title);
+
+ await mouseenter();
+
+ expect(findGlPopover().exists()).toBe(true);
+ expect(findIssuableTitle().text()).toContain(mockBlockingIssuable.title);
+ expect(wrapper.vm.skip).toBe(true);
+ },
+ );
+
+ it('should emit "blocking-issuables-error" event on query error', async () => {
+ const mockError = new Error('mayday');
+ createWrapperWithApollo({ blockingIssuablesSpy: jest.fn().mockRejectedValue(mockError) });
+
+ await mouseenter();
+
+ const [
+ [
+ {
+ message,
+ error: { networkError },
+ },
+ ],
+ ] = wrapper.emitted('blocking-issuables-error');
+ expect(message).toBe('Failed to fetch blocking issues');
+ expect(networkError).toBe(mockError);
+ });
+
+ describe('with a single blocking issue', () => {
+ beforeEach(async () => {
+ createWrapperWithApollo();
+
+ await mouseenter();
+ });
+
+ it('should render a title of the issuable', async () => {
+ expect(findIssuableTitle().text()).toBe(mockBlockingIssue1.title);
+ });
+
+ it('should render issuable reference and link to the issuable', async () => {
+ const formattedRef = mockBlockingIssue1.reference.split('/')[1];
+
+ expect(findGlLink().text()).toBe(formattedRef);
+ expect(findGlLink().attributes('href')).toBe(mockBlockingIssue1.webUrl);
+ });
+
+ it('should render popover title with correct blocking issuable count', async () => {
+ expect(findPopoverTitle().text()).toBe('Blocked by 1 issue');
+ });
+ });
+
+ describe('when issue has a long title', () => {
+ it('should render a truncated title', async () => {
+ createWrapperWithApollo({
+ blockingIssuablesSpy: jest.fn().mockResolvedValue(mockBlockingIssuablesResponse2),
+ });
+
+ await mouseenter();
+
+ const truncatedTitle = truncate(
+ mockBlockingIssue2.title,
+ wrapper.vm.$options.textTruncateWidth,
+ );
+ expect(findIssuableTitle().text()).toBe(truncatedTitle);
+ });
+ });
+
+ describe('with more than three blocking issues', () => {
+ beforeEach(async () => {
+ createWrapperWithApollo({
+ item: mockBlockedIssue2,
+ blockingIssuablesSpy: jest.fn().mockResolvedValue(mockBlockingIssuablesResponse3),
+ });
+
+ await mouseenter();
+ });
+
+ it('matches the snapshot', () => {
+ expect(wrapper.html()).toMatchSnapshot();
+ });
+
+ it('should render popover title with correct blocking issuable count', async () => {
+ expect(findPopoverTitle().text()).toBe('Blocked by 4 issues');
+ });
+
+ it('should render the number of hidden blocking issuables', () => {
+ expect(findHiddenBlockingCount().text()).toBe('+ 1 more issue');
+ });
+
+ it('should link to the blocked issue page at the related issue anchor', async () => {
+ expect(findViewAllIssuableLink().text()).toBe('View all blocking issues');
+ expect(findViewAllIssuableLink().attributes('href')).toBe(
+ `${mockBlockedIssue2.webUrl}#related-issues`,
+ );
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js
index 39a76a51191..6b20f0c77a3 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js
@@ -138,7 +138,7 @@ describe('IssuableBody', () => {
wrapper.vm.handleTaskListUpdateSuccess(updatedIssuable);
- expect(wrapper.emitted('task-list-update-success')).toBeTruthy();
+ expect(wrapper.emitted('task-list-update-success')).toHaveLength(1);
expect(wrapper.emitted('task-list-update-success')[0]).toEqual([updatedIssuable]);
});
});
@@ -147,7 +147,7 @@ describe('IssuableBody', () => {
it('emits `task-list-update-failure` event on component', () => {
wrapper.vm.handleTaskListUpdateFailure();
- expect(wrapper.emitted('task-list-update-failure')).toBeTruthy();
+ expect(wrapper.emitted('task-list-update-failure')).toHaveLength(1);
});
});
});
@@ -202,7 +202,7 @@ describe('IssuableBody', () => {
issuableTitle.vm.$emit('edit-issuable');
- expect(wrapper.emitted('edit-issuable')).toBeTruthy();
+ expect(wrapper.emitted('edit-issuable')).toHaveLength(1);
});
it.each(['keydown-title', 'keydown-description'])(
@@ -227,7 +227,7 @@ describe('IssuableBody', () => {
issuableEditForm.vm.$emit(eventName, eventObj, issuableMeta);
- expect(wrapper.emitted(eventName)).toBeTruthy();
+ expect(wrapper.emitted(eventName)).toHaveLength(1);
expect(wrapper.emitted(eventName)[0]).toMatchObject([eventObj, issuableMeta]);
},
);
diff --git a/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js b/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js
index a9651cf8bac..43ff68e30b5 100644
--- a/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js
+++ b/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js
@@ -14,7 +14,7 @@ import {
sastDiffSuccessMock,
secretDetectionDiffSuccessMock,
} from 'jest/vue_shared/security_reports/mock_data';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import HelpIcon from '~/vue_shared/security_reports/components/help_icon.vue';
import SecurityReportDownloadDropdown from '~/vue_shared/security_reports/components/security_report_download_dropdown.vue';
@@ -135,8 +135,8 @@ describe('Security reports app', () => {
});
});
- it('calls createFlash correctly', () => {
- expect(createFlash).toHaveBeenCalledWith({
+ it('calls createAlert correctly', () => {
+ expect(createAlert).toHaveBeenCalledWith({
message: SecurityReportsApp.i18n.apiError,
captureError: true,
error: expect.any(Error),