summaryrefslogtreecommitdiff
path: root/spec/frontend/vue_shared/components
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/vue_shared/components')
-rw-r--r--spec/frontend/vue_shared/components/alerts_deprecation_warning_spec.js48
-rw-r--r--spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js99
-rw-r--r--spec/frontend/vue_shared/components/confirm_danger/confirm_danger_spec.js61
-rw-r--r--spec/frontend/vue_shared/components/content_viewer/content_viewer_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js27
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js26
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js29
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/iteration_token_spec.js44
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js78
-rw-r--r--spec/frontend/vue_shared/components/header_ci_component_spec.js48
-rw-r--r--spec/frontend/vue_shared/components/notes/system_note_spec.js50
-rw-r--r--spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/project_selector/project_selector_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/registry/title_area_spec.js59
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js67
-rw-r--r--spec/frontend/vue_shared/components/settings/__snapshots__/settings_block_spec.js.snap1
-rw-r--r--spec/frontend/vue_shared/components/settings/settings_block_spec.js48
-rw-r--r--spec/frontend/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js76
-rw-r--r--spec/frontend/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js119
-rw-r--r--spec/frontend/vue_shared/components/sidebar/date_picker_spec.js69
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed_spec.js121
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js72
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js18
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js19
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js69
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_footer_spec.js57
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_header_spec.js75
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js8
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js7
-rw-r--r--spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js52
-rw-r--r--spec/frontend/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list_spec.js2
33 files changed, 1034 insertions, 437 deletions
diff --git a/spec/frontend/vue_shared/components/alerts_deprecation_warning_spec.js b/spec/frontend/vue_shared/components/alerts_deprecation_warning_spec.js
deleted file mode 100644
index b73f4d6a396..00000000000
--- a/spec/frontend/vue_shared/components/alerts_deprecation_warning_spec.js
+++ /dev/null
@@ -1,48 +0,0 @@
-import { GlAlert, GlLink } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
-import AlertDeprecationWarning from '~/vue_shared/components/alerts_deprecation_warning.vue';
-
-describe('AlertDetails', () => {
- let wrapper;
-
- function mountComponent(hasManagedPrometheus = false) {
- wrapper = mount(AlertDeprecationWarning, {
- provide: {
- hasManagedPrometheus,
- },
- });
- }
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- const findAlert = () => wrapper.findComponent(GlAlert);
- const findLink = () => wrapper.findComponent(GlLink);
-
- describe('Alert details', () => {
- describe('with no manual prometheus', () => {
- beforeEach(() => {
- mountComponent();
- });
-
- it('renders nothing', () => {
- expect(findAlert().exists()).toBe(false);
- });
- });
-
- describe('with manual prometheus', () => {
- beforeEach(() => {
- mountComponent(true);
- });
-
- it('renders a deprecation notice', () => {
- expect(findAlert().text()).toContain('GitLab-managed Prometheus is deprecated');
- expect(findLink().attributes('href')).toContain(
- 'operations/metrics/alerts.html#managed-prometheus-instances',
- );
- });
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js b/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js
new file mode 100644
index 00000000000..f75694bd504
--- /dev/null
+++ b/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js
@@ -0,0 +1,99 @@
+import { GlModal, GlSprintf } from '@gitlab/ui';
+import {
+ CONFIRM_DANGER_WARNING,
+ CONFIRM_DANGER_MODAL_BUTTON,
+ CONFIRM_DANGER_MODAL_ID,
+} from '~/vue_shared/components/confirm_danger/constants';
+import ConfirmDangerModal from '~/vue_shared/components/confirm_danger/confirm_danger_modal.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+describe('Confirm Danger Modal', () => {
+ const confirmDangerMessage = 'This is a dangerous activity';
+ const confirmButtonText = 'Confirm button text';
+ const phrase = 'You must construct additional pylons';
+ const modalId = CONFIRM_DANGER_MODAL_ID;
+
+ let wrapper;
+
+ const findModal = () => wrapper.findComponent(GlModal);
+ const findConfirmationPhrase = () => wrapper.findByTestId('confirm-danger-phrase');
+ const findConfirmationInput = () => wrapper.findByTestId('confirm-danger-input');
+ const findDefaultWarning = () => wrapper.findByTestId('confirm-danger-warning');
+ const findAdditionalMessage = () => wrapper.findByTestId('confirm-danger-message');
+ const findPrimaryAction = () => findModal().props('actionPrimary');
+ const findPrimaryActionAttributes = (attr) => findPrimaryAction().attributes[0][attr];
+
+ const createComponent = ({ provide = {} } = {}) =>
+ shallowMountExtended(ConfirmDangerModal, {
+ propsData: {
+ modalId,
+ phrase,
+ },
+ provide,
+ stubs: { GlSprintf },
+ });
+
+ beforeEach(() => {
+ wrapper = createComponent({ provide: { confirmDangerMessage, confirmButtonText } });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders the default warning message', () => {
+ expect(findDefaultWarning().text()).toBe(CONFIRM_DANGER_WARNING);
+ });
+
+ it('renders any additional messages', () => {
+ expect(findAdditionalMessage().text()).toBe(confirmDangerMessage);
+ });
+
+ it('renders the confirm button', () => {
+ expect(findPrimaryAction().text).toBe(confirmButtonText);
+ expect(findPrimaryActionAttributes('variant')).toBe('danger');
+ });
+
+ it('renders the correct confirmation phrase', () => {
+ expect(findConfirmationPhrase().text()).toBe(
+ `Please type ${phrase} to proceed or close this modal to cancel.`,
+ );
+ });
+
+ describe('without injected data', () => {
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ it('does not render any additional messages', () => {
+ expect(findAdditionalMessage().exists()).toBe(false);
+ });
+
+ it('renders the default confirm button', () => {
+ expect(findPrimaryAction().text).toBe(CONFIRM_DANGER_MODAL_BUTTON);
+ });
+ });
+
+ describe('with a valid confirmation phrase', () => {
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ it('enables the confirm button', async () => {
+ expect(findPrimaryActionAttributes('disabled')).toBe(true);
+
+ await findConfirmationInput().vm.$emit('input', phrase);
+
+ expect(findPrimaryActionAttributes('disabled')).toBe(false);
+ });
+
+ it('emits a `confirm` event when the button is clicked', async () => {
+ expect(wrapper.emitted('confirm')).toBeUndefined();
+
+ await findConfirmationInput().vm.$emit('input', phrase);
+ await findModal().vm.$emit('primary');
+
+ expect(wrapper.emitted('confirm')).not.toBeUndefined();
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_spec.js b/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_spec.js
new file mode 100644
index 00000000000..220f897c035
--- /dev/null
+++ b/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_spec.js
@@ -0,0 +1,61 @@
+import { GlButton } from '@gitlab/ui';
+import ConfirmDanger from '~/vue_shared/components/confirm_danger/confirm_danger.vue';
+import ConfirmDangerModal from '~/vue_shared/components/confirm_danger/confirm_danger_modal.vue';
+import { CONFIRM_DANGER_MODAL_ID } from '~/vue_shared/components/confirm_danger/constants';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+describe('Confirm Danger Modal', () => {
+ let wrapper;
+
+ const phrase = 'En Taro Adun';
+ const buttonText = 'Click me!';
+ const modalId = CONFIRM_DANGER_MODAL_ID;
+
+ const findBtn = () => wrapper.findComponent(GlButton);
+ const findModal = () => wrapper.findComponent(ConfirmDangerModal);
+ const findModalProps = () => findModal().props();
+
+ const createComponent = (props = {}) =>
+ shallowMountExtended(ConfirmDanger, {
+ propsData: {
+ buttonText,
+ phrase,
+ ...props,
+ },
+ });
+
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders the button', () => {
+ expect(wrapper.html()).toContain(buttonText);
+ });
+
+ it('sets the modal properties', () => {
+ expect(findModalProps()).toMatchObject({
+ modalId,
+ phrase,
+ });
+ });
+
+ it('will disable the button if `disabled=true`', () => {
+ expect(findBtn().attributes('disabled')).toBeUndefined();
+
+ wrapper = createComponent({ disabled: true });
+
+ expect(findBtn().attributes('disabled')).toBe('true');
+ });
+
+ it('will emit `confirm` when the modal confirms', () => {
+ expect(wrapper.emitted('confirm')).toBeUndefined();
+
+ findModal().vm.$emit('confirm');
+
+ expect(wrapper.emitted('confirm')).not.toBeUndefined();
+ });
+});
diff --git a/spec/frontend/vue_shared/components/content_viewer/content_viewer_spec.js b/spec/frontend/vue_shared/components/content_viewer/content_viewer_spec.js
index 16e7e4dd5cc..f28805471f8 100644
--- a/spec/frontend/vue_shared/components/content_viewer/content_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/content_viewer/content_viewer_spec.js
@@ -16,6 +16,6 @@ describe('ContentViewer', () => {
propsData: { path, fileSize: 1024, type },
});
- expect(wrapper.find(selector).element).toExist();
+ expect(wrapper.find(selector).exists()).toBe(true);
});
});
diff --git a/spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js b/spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js
index 3ffb23dc7a0..1397fb0405e 100644
--- a/spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js
@@ -42,7 +42,7 @@ describe('MarkdownViewer', () => {
it('renders an animation container while the markdown is loading', () => {
createComponent();
- expect(wrapper.find('.animation-container')).toExist();
+ expect(wrapper.find('.animation-container').exists()).toBe(true);
});
it('renders markdown preview preview renders and loads rendered markdown from server', () => {
diff --git a/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js b/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js
index 016fe1f131e..b3af5fd3feb 100644
--- a/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js
+++ b/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js
@@ -34,6 +34,7 @@ describe('DropdownWidget component', () => {
// invokes `show` method of BDropdown used inside GlDropdown.
// Context: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54895#note_524281679
jest.spyOn(wrapper.vm, 'showDropdown').mockImplementation();
+ jest.spyOn(findDropdown().vm, 'hide').mockImplementation();
};
beforeEach(() => {
@@ -67,10 +68,7 @@ describe('DropdownWidget component', () => {
});
it('emits set-option event when clicking on an option', async () => {
- wrapper
- .findAll('[data-testid="unselected-option"]')
- .at(1)
- .vm.$emit('click', new Event('click'));
+ wrapper.findAll('[data-testid="unselected-option"]').at(1).trigger('click');
await wrapper.vm.$nextTick();
expect(wrapper.emitted('set-option')).toEqual([[wrapper.props().options[1]]]);
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
index 8e931aebfe0..64d15884333 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
@@ -25,6 +25,7 @@ import {
tokenValueMilestone,
tokenValueMembership,
tokenValueConfidential,
+ tokenValueEmpty,
} from './mock_data';
jest.mock('~/vue_shared/components/filtered_search_bar/filtered_search_utils', () => ({
@@ -43,6 +44,7 @@ const createComponent = ({
recentSearchesStorageKey = 'requirements',
tokens = mockAvailableTokens,
sortOptions,
+ initialFilterValue = [],
showCheckbox = false,
checkboxChecked = false,
searchInputPlaceholder = 'Filter requirements',
@@ -55,6 +57,7 @@ const createComponent = ({
recentSearchesStorageKey,
tokens,
sortOptions,
+ initialFilterValue,
showCheckbox,
checkboxChecked,
searchInputPlaceholder,
@@ -193,19 +196,27 @@ describe('FilteredSearchBarRoot', () => {
describe('watchers', () => {
describe('filterValue', () => {
- it('emits component event `onFilter` with empty array when `filterValue` is cleared by GlFilteredSearch', () => {
+ it('emits component event `onFilter` with empty array and false when filter was never selected', () => {
+ wrapper = createComponent({ initialFilterValue: [tokenValueEmpty] });
wrapper.setData({
initialRender: false,
- filterValue: [
- {
- type: 'filtered-search-term',
- value: { data: '' },
- },
- ],
+ filterValue: [tokenValueEmpty],
+ });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.emitted('onFilter')[0]).toEqual([[], false]);
+ });
+ });
+
+ it('emits component event `onFilter` with empty array and true when initially selected filter value was cleared', () => {
+ wrapper = createComponent({ initialFilterValue: [tokenValueLabel] });
+ wrapper.setData({
+ initialRender: false,
+ filterValue: [tokenValueEmpty],
});
return wrapper.vm.$nextTick(() => {
- expect(wrapper.emitted('onFilter')[0]).toEqual([[]]);
+ expect(wrapper.emitted('onFilter')[0]).toEqual([[], true]);
});
});
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js b/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js
index ae02c554e13..238c5d16db5 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js
@@ -9,6 +9,7 @@ import EpicToken from '~/vue_shared/components/filtered_search_bar/tokens/epic_t
import IterationToken from '~/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue';
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
+import ReleaseToken from '~/vue_shared/components/filtered_search_bar/tokens/release_token.vue';
import WeightToken from '~/vue_shared/components/filtered_search_bar/tokens/weight_token.vue';
export const mockAuthor1 = {
@@ -110,6 +111,18 @@ export const mockIterationToken = {
fetchIterations: () => Promise.resolve(),
};
+export const mockIterations = [
+ {
+ id: 1,
+ title: 'Iteration 1',
+ startDate: '2021-11-05',
+ dueDate: '2021-11-10',
+ iterationCadence: {
+ title: 'Cadence 1',
+ },
+ },
+];
+
export const mockLabelToken = {
type: 'label_name',
icon: 'labels',
@@ -132,6 +145,14 @@ export const mockMilestoneToken = {
fetchMilestones: () => Promise.resolve({ data: mockMilestones }),
};
+export const mockReleaseToken = {
+ type: 'release',
+ icon: 'rocket',
+ title: 'Release',
+ token: ReleaseToken,
+ fetchReleases: () => Promise.resolve(),
+};
+
export const mockEpicToken = {
type: 'epic_iid',
icon: 'clock',
@@ -282,6 +303,11 @@ export const tokenValuePlain = {
value: { data: 'foo' },
};
+export const tokenValueEmpty = {
+ type: 'filtered-search-term',
+ value: { data: '' },
+};
+
export const tokenValueEpic = {
type: 'epic_iid',
value: {
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 14fcffd3c50..b29c394e7ae 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
@@ -112,6 +112,35 @@ describe('AuthorToken', () => {
});
});
+ // TODO: rm when completed https://gitlab.com/gitlab-org/gitlab/-/issues/345756
+ describe('when there are null users presents', () => {
+ const mockAuthorsWithNullUser = mockAuthors.concat([null]);
+
+ beforeEach(() => {
+ jest
+ .spyOn(wrapper.vm.config, 'fetchAuthors')
+ .mockResolvedValue({ data: mockAuthorsWithNullUser });
+
+ getBaseToken().vm.$emit('fetch-suggestions', 'root');
+ });
+
+ describe('when res.data is present', () => {
+ it('filters the successful response when null values are present', () => {
+ return waitForPromises().then(() => {
+ expect(getBaseToken().props('suggestions')).toEqual(mockAuthors);
+ });
+ });
+ });
+
+ describe('when response is an array', () => {
+ it('filters the successful response when null values are present', () => {
+ return waitForPromises().then(() => {
+ expect(getBaseToken().props('suggestions')).toEqual(mockAuthors);
+ });
+ });
+ });
+ });
+
it('calls `createFlash` with flash error message when request fails', () => {
jest.spyOn(wrapper.vm.config, 'fetchAuthors').mockRejectedValue({});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/iteration_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/iteration_token_spec.js
index af90ee93543..44bc16adb97 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/iteration_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/iteration_token_spec.js
@@ -1,9 +1,13 @@
-import { GlFilteredSearchToken, GlFilteredSearchTokenSegment } from '@gitlab/ui';
+import {
+ GlFilteredSearchToken,
+ GlFilteredSearchTokenSegment,
+ GlFilteredSearchSuggestion,
+} from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import IterationToken from '~/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue';
-import { mockIterationToken } from '../mock_data';
+import { mockIterationToken, mockIterations } from '../mock_data';
jest.mock('~/flash');
@@ -11,10 +15,16 @@ describe('IterationToken', () => {
const id = 123;
let wrapper;
- const createComponent = ({ config = mockIterationToken, value = { data: '' } } = {}) =>
+ const createComponent = ({
+ config = mockIterationToken,
+ value = { data: '' },
+ active = false,
+ stubs = {},
+ provide = {},
+ } = {}) =>
mount(IterationToken, {
propsData: {
- active: false,
+ active,
config,
value,
},
@@ -22,13 +32,39 @@ describe('IterationToken', () => {
portalName: 'fake target',
alignSuggestions: function fakeAlignSuggestions() {},
suggestionsListClass: () => 'custom-class',
+ ...provide,
},
+ stubs,
});
afterEach(() => {
wrapper.destroy();
});
+ describe('when iteration cadence feature is available', () => {
+ beforeEach(async () => {
+ wrapper = createComponent({
+ active: true,
+ config: { ...mockIterationToken, initialIterations: mockIterations },
+ value: { data: 'i' },
+ stubs: { Portal: true },
+ provide: {
+ glFeatures: {
+ iterationCadences: true,
+ },
+ },
+ });
+
+ await wrapper.setData({ loading: false });
+ });
+
+ it('renders iteration start date and due date', () => {
+ const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
+
+ expect(suggestions.at(3).text()).toContain('Nov 5, 2021 - Nov 10, 2021');
+ });
+ });
+
it('renders iteration value', async () => {
wrapper = createComponent({ value: { data: id } });
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
new file mode 100644
index 00000000000..b804ff97b82
--- /dev/null
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js
@@ -0,0 +1,78 @@
+import { GlFilteredSearchToken, GlFilteredSearchTokenSegment } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import waitForPromises from 'helpers/wait_for_promises';
+import createFlash from '~/flash';
+import ReleaseToken from '~/vue_shared/components/filtered_search_bar/tokens/release_token.vue';
+import { mockReleaseToken } from '../mock_data';
+
+jest.mock('~/flash');
+
+describe('ReleaseToken', () => {
+ const id = 123;
+ let wrapper;
+
+ const createComponent = ({ config = mockReleaseToken, value = { data: '' } } = {}) =>
+ mount(ReleaseToken, {
+ propsData: {
+ active: false,
+ config,
+ value,
+ },
+ provide: {
+ portalName: 'fake target',
+ alignSuggestions: function fakeAlignSuggestions() {},
+ suggestionsListClass: () => 'custom-class',
+ },
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders release value', async () => {
+ wrapper = createComponent({ value: { data: id } });
+ await wrapper.vm.$nextTick();
+
+ const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment);
+
+ expect(tokenSegments).toHaveLength(3); // `Release` `=` `v1`
+ expect(tokenSegments.at(2).text()).toBe(id.toString());
+ });
+
+ it('fetches initial values', () => {
+ const fetchReleasesSpy = jest.fn().mockResolvedValue();
+
+ wrapper = createComponent({
+ config: { ...mockReleaseToken, fetchReleases: fetchReleasesSpy },
+ value: { data: id },
+ });
+
+ expect(fetchReleasesSpy).toHaveBeenCalledWith(id);
+ });
+
+ it('fetches releases on user input', () => {
+ const search = 'hello';
+ const fetchReleasesSpy = jest.fn().mockResolvedValue();
+
+ wrapper = createComponent({
+ config: { ...mockReleaseToken, fetchReleases: fetchReleasesSpy },
+ });
+
+ wrapper.findComponent(GlFilteredSearchToken).vm.$emit('input', { data: search });
+
+ expect(fetchReleasesSpy).toHaveBeenCalledWith(search);
+ });
+
+ it('renders error message when request fails', async () => {
+ const fetchReleasesSpy = jest.fn().mockRejectedValue();
+
+ wrapper = createComponent({
+ config: { ...mockReleaseToken, fetchReleases: fetchReleasesSpy },
+ });
+ await waitForPromises();
+
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'There was a problem fetching releases.',
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/header_ci_component_spec.js b/spec/frontend/vue_shared/components/header_ci_component_spec.js
index 42f4439df51..b76f475a6fb 100644
--- a/spec/frontend/vue_shared/components/header_ci_component_spec.js
+++ b/spec/frontend/vue_shared/components/header_ci_component_spec.js
@@ -1,4 +1,4 @@
-import { GlButton, GlLink } from '@gitlab/ui';
+import { GlButton, GlAvatarLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import CiIconBadge from '~/vue_shared/components/ci_badge_link.vue';
@@ -18,6 +18,7 @@ describe('Header CI Component', () => {
},
time: '2017-05-08T14:57:39.781Z',
user: {
+ id: 1234,
web_url: 'path',
name: 'Foo',
username: 'foobar',
@@ -29,7 +30,7 @@ describe('Header CI Component', () => {
const findIconBadge = () => wrapper.findComponent(CiIconBadge);
const findTimeAgo = () => wrapper.findComponent(TimeagoTooltip);
- const findUserLink = () => wrapper.findComponent(GlLink);
+ const findUserLink = () => wrapper.findComponent(GlAvatarLink);
const findSidebarToggleBtn = () => wrapper.findComponent(GlButton);
const findActionButtons = () => wrapper.findByTestId('ci-header-action-buttons');
const findHeaderItemText = () => wrapper.findByTestId('ci-header-item-text');
@@ -64,10 +65,6 @@ describe('Header CI Component', () => {
expect(findTimeAgo().exists()).toBe(true);
});
- it('should render user icon and name', () => {
- expect(findUserLink().text()).toContain(defaultProps.user.name);
- });
-
it('should render sidebar toggle button', () => {
expect(findSidebarToggleBtn().exists()).toBe(true);
});
@@ -77,6 +74,45 @@ describe('Header CI Component', () => {
});
});
+ describe('user avatar', () => {
+ beforeEach(() => {
+ createComponent({ itemName: 'Pipeline' });
+ });
+
+ it('contains the username', () => {
+ expect(findUserLink().text()).toContain(defaultProps.user.username);
+ });
+
+ it('has the correct data attributes', () => {
+ expect(findUserLink().attributes()).toMatchObject({
+ 'data-user-id': defaultProps.user.id.toString(),
+ 'data-username': defaultProps.user.username,
+ 'data-name': defaultProps.user.name,
+ });
+ });
+
+ describe('with data from GraphQL', () => {
+ const userId = 1;
+
+ beforeEach(() => {
+ createComponent({
+ itemName: 'Pipeline',
+ user: { ...defaultProps.user, id: `gid://gitlab/User/${1}` },
+ });
+ });
+
+ it('has the correct user id', () => {
+ expect(findUserLink().attributes('data-user-id')).toBe(userId.toString());
+ });
+ });
+
+ describe('with data from REST', () => {
+ it('has the correct user id', () => {
+ expect(findUserLink().attributes('data-user-id')).toBe(defaultProps.user.id.toString());
+ });
+ });
+ });
+
describe('with item id', () => {
beforeEach(() => {
createComponent({ itemName: 'Pipeline', itemId: '123' });
diff --git a/spec/frontend/vue_shared/components/notes/system_note_spec.js b/spec/frontend/vue_shared/components/notes/system_note_spec.js
index 48dacc50923..65f79bab005 100644
--- a/spec/frontend/vue_shared/components/notes/system_note_spec.js
+++ b/spec/frontend/vue_shared/components/notes/system_note_spec.js
@@ -1,13 +1,27 @@
+import MockAdapter from 'axios-mock-adapter';
import { mount } from '@vue/test-utils';
+import waitForPromises from 'helpers/wait_for_promises';
import initMRPopovers from '~/mr_popover/index';
import createStore from '~/notes/stores';
import IssueSystemNote from '~/vue_shared/components/notes/system_note.vue';
+import axios from '~/lib/utils/axios_utils';
jest.mock('~/mr_popover/index', () => jest.fn());
describe('system note component', () => {
let vm;
let props;
+ let mock;
+
+ function createComponent(propsData = {}) {
+ const store = createStore();
+ store.dispatch('setTargetNoteHash', `note_${props.note.id}`);
+
+ vm = mount(IssueSystemNote, {
+ store,
+ propsData,
+ });
+ }
beforeEach(() => {
props = {
@@ -27,28 +41,29 @@ describe('system note component', () => {
},
};
- const store = createStore();
- store.dispatch('setTargetNoteHash', `note_${props.note.id}`);
-
- vm = mount(IssueSystemNote, {
- store,
- propsData: props,
- });
+ mock = new MockAdapter(axios);
});
afterEach(() => {
vm.destroy();
+ mock.restore();
});
it('should render a list item with correct id', () => {
+ createComponent(props);
+
expect(vm.attributes('id')).toEqual(`note_${props.note.id}`);
});
it('should render target class is note is target note', () => {
+ createComponent(props);
+
expect(vm.classes()).toContain('target');
});
it('should render svg icon', () => {
+ createComponent(props);
+
expect(vm.find('.timeline-icon svg').exists()).toBe(true);
});
@@ -56,10 +71,31 @@ describe('system note component', () => {
// we need to strip them because they break layout of commit lists in system notes:
// https://gitlab.com/gitlab-org/gitlab-foss/uploads/b07a10670919254f0220d3ff5c1aa110/jqzI.png
it('removes wrapping paragraph from note HTML', () => {
+ createComponent(props);
+
expect(vm.find('.system-note-message').html()).toContain('<span>closed</span>');
});
it('should initMRPopovers onMount', () => {
+ createComponent(props);
+
expect(initMRPopovers).toHaveBeenCalled();
});
+
+ it('renders outdated code lines', async () => {
+ mock
+ .onGet('/outdated_line_change_path')
+ .reply(200, [
+ { rich_text: 'console.log', type: 'new', line_code: '123', old_line: null, new_line: 1 },
+ ]);
+
+ createComponent({
+ note: { ...props.note, outdated_line_change_path: '/outdated_line_change_path' },
+ });
+
+ await vm.find("[data-testid='outdated-lines-change-btn']").trigger('click');
+ await waitForPromises();
+
+ expect(vm.find("[data-testid='outdated-lines']").exists()).toBe(true);
+ });
});
diff --git a/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js b/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js
index 1ed7844b395..7fdacbe83a2 100644
--- a/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js
+++ b/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js
@@ -1,6 +1,5 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
-// eslint-disable-next-line import/no-deprecated
-import { getJSONFixture } from 'helpers/fixtures';
+import mockProjects from 'test_fixtures_static/projects.json';
import { trimText } from 'helpers/text_helper';
import ProjectAvatar from '~/vue_shared/components/deprecated_project_avatar/default.vue';
import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue';
@@ -13,8 +12,7 @@ describe('ProjectListItem component', () => {
let vm;
let options;
- // eslint-disable-next-line import/no-deprecated
- const project = getJSONFixture('static/projects.json')[0];
+ const project = JSON.parse(JSON.stringify(mockProjects))[0];
beforeEach(() => {
options = {
diff --git a/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js b/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js
index 1f97d3ff3fa..de5cee846a1 100644
--- a/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js
+++ b/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js
@@ -2,8 +2,7 @@ import { GlSearchBoxByType, GlInfiniteScroll } from '@gitlab/ui';
import { mount, createLocalVue } from '@vue/test-utils';
import { head } from 'lodash';
import Vue from 'vue';
-// eslint-disable-next-line import/no-deprecated
-import { getJSONFixture } from 'helpers/fixtures';
+import mockProjects from 'test_fixtures_static/projects.json';
import { trimText } from 'helpers/text_helper';
import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue';
import ProjectSelector from '~/vue_shared/components/project_selector/project_selector.vue';
@@ -13,8 +12,7 @@ const localVue = createLocalVue();
describe('ProjectSelector component', () => {
let wrapper;
let vm;
- // eslint-disable-next-line import/no-deprecated
- const allProjects = getJSONFixture('static/projects.json');
+ const allProjects = mockProjects;
const searchResults = allProjects.slice(0, 5);
let selected = [];
selected = selected.concat(allProjects.slice(0, 3)).concat(allProjects.slice(5, 8));
diff --git a/spec/frontend/vue_shared/components/registry/title_area_spec.js b/spec/frontend/vue_shared/components/registry/title_area_spec.js
index 75aa3bc7096..b62676b35be 100644
--- a/spec/frontend/vue_shared/components/registry/title_area_spec.js
+++ b/spec/frontend/vue_shared/components/registry/title_area_spec.js
@@ -1,5 +1,6 @@
import { GlAvatar, GlSprintf, GlLink, GlSkeletonLoader } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import component from '~/vue_shared/components/registry/title_area.vue';
describe('title area', () => {
@@ -7,18 +8,18 @@ describe('title area', () => {
const DYNAMIC_SLOT = 'metadata-dynamic-slot';
- const findSubHeaderSlot = () => wrapper.find('[data-testid="sub-header"]');
- const findRightActionsSlot = () => wrapper.find('[data-testid="right-actions"]');
- const findMetadataSlot = (name) => wrapper.find(`[data-testid="${name}"]`);
- const findTitle = () => wrapper.find('[data-testid="title"]');
- const findAvatar = () => wrapper.find(GlAvatar);
- const findInfoMessages = () => wrapper.findAll('[data-testid="info-message"]');
- const findDynamicSlot = () => wrapper.find(`[data-testid="${DYNAMIC_SLOT}`);
+ const findSubHeaderSlot = () => wrapper.findByTestId('sub-header');
+ const findRightActionsSlot = () => wrapper.findByTestId('right-actions');
+ const findMetadataSlot = (name) => wrapper.findByTestId(name);
+ const findTitle = () => wrapper.findByTestId('title');
+ const findAvatar = () => wrapper.findComponent(GlAvatar);
+ const findInfoMessages = () => wrapper.findAllByTestId('info-message');
+ const findDynamicSlot = () => wrapper.findByTestId(DYNAMIC_SLOT);
const findSlotOrderElements = () => wrapper.findAll('[slot-test]');
- const findSkeletonLoader = () => wrapper.find(GlSkeletonLoader);
+ const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
const mountComponent = ({ propsData = { title: 'foo' }, slots } = {}) => {
- wrapper = shallowMount(component, {
+ wrapper = shallowMountExtended(component, {
propsData,
stubs: { GlSprintf },
slots: {
@@ -29,6 +30,12 @@ describe('title area', () => {
});
};
+ const generateSlotMocks = (names) =>
+ names.reduce((acc, current) => {
+ acc[current] = `<div data-testid="${current}" />`;
+ return acc;
+ }, {});
+
afterEach(() => {
wrapper.destroy();
wrapper = null;
@@ -40,6 +47,7 @@ describe('title area', () => {
expect(findTitle().text()).toBe('foo');
});
+
it('if slot is present uses slot', () => {
mountComponent({
slots: {
@@ -88,24 +96,21 @@ describe('title area', () => {
${['metadata-foo', 'metadata-bar']}
${['metadata-foo', 'metadata-bar', 'metadata-baz']}
`('$slotNames metadata slots', ({ slotNames }) => {
- const slotMocks = slotNames.reduce((acc, current) => {
- acc[current] = `<div data-testid="${current}" />`;
- return acc;
- }, {});
+ const slots = generateSlotMocks(slotNames);
it('exist when the slot is present', async () => {
- mountComponent({ slots: slotMocks });
+ mountComponent({ slots });
- await wrapper.vm.$nextTick();
+ await nextTick();
slotNames.forEach((name) => {
expect(findMetadataSlot(name).exists()).toBe(true);
});
});
it('is/are hidden when metadata-loading is true', async () => {
- mountComponent({ slots: slotMocks, propsData: { title: 'foo', metadataLoading: true } });
+ mountComponent({ slots, propsData: { title: 'foo', metadataLoading: true } });
- await wrapper.vm.$nextTick();
+ await nextTick();
slotNames.forEach((name) => {
expect(findMetadataSlot(name).exists()).toBe(false);
});
@@ -113,14 +118,20 @@ describe('title area', () => {
});
describe('metadata skeleton loader', () => {
- it('is hidden when metadata loading is false', () => {
- mountComponent();
+ const slots = generateSlotMocks(['metadata-foo']);
+
+ it('is hidden when metadata loading is false', async () => {
+ mountComponent({ slots });
+
+ await nextTick();
expect(findSkeletonLoader().exists()).toBe(false);
});
- it('is shown when metadata loading is true', () => {
- mountComponent({ propsData: { metadataLoading: true } });
+ it('is shown when metadata loading is true', async () => {
+ mountComponent({ propsData: { metadataLoading: true }, slots });
+
+ await nextTick();
expect(findSkeletonLoader().exists()).toBe(true);
});
@@ -143,7 +154,7 @@ describe('title area', () => {
// updating the slots like we do on line 141 does not cause the updated lifecycle-hook to be triggered
wrapper.vm.$forceUpdate();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findDynamicSlot().exists()).toBe(true);
});
@@ -163,7 +174,7 @@ describe('title area', () => {
// updating the slots like we do on line 159 does not cause the updated lifecycle-hook to be triggered
wrapper.vm.$forceUpdate();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findSlotOrderElements().at(0).attributes('data-testid')).toBe(DYNAMIC_SLOT);
expect(findSlotOrderElements().at(1).attributes('data-testid')).toBe('metadata-foo');
diff --git a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js
index 32ef2d27ba7..8536ffed573 100644
--- a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js
+++ b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js
@@ -1,4 +1,4 @@
-import { GlAlert, GlButton, GlLoadingIcon, GlSkeletonLoader } from '@gitlab/ui';
+import { GlAlert, GlModal, GlButton, GlLoadingIcon, GlSkeletonLoader } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { nextTick } from 'vue';
@@ -52,7 +52,7 @@ describe('RunnerInstructionsModal component', () => {
const findBinaryInstructions = () => wrapper.findByTestId('binary-instructions');
const findRegisterCommand = () => wrapper.findByTestId('register-command');
- const createComponent = () => {
+ const createComponent = ({ props, ...options } = {}) => {
const requestHandlers = [
[getRunnerPlatformsQuery, runnerPlatformsHandler],
[getRunnerSetupInstructionsQuery, runnerSetupInstructionsHandler],
@@ -64,9 +64,12 @@ describe('RunnerInstructionsModal component', () => {
shallowMount(RunnerInstructionsModal, {
propsData: {
modalId: 'runner-instructions-modal',
+ registrationToken: 'MY_TOKEN',
+ ...props,
},
localVue,
apolloProvider: fakeApollo,
+ ...options,
}),
);
};
@@ -118,18 +121,30 @@ describe('RunnerInstructionsModal component', () => {
expect(instructions).toBe(installInstructions);
});
- it('register command is shown', () => {
+ it('register command is shown with a replaced token', () => {
const instructions = findRegisterCommand().text();
- expect(instructions).toBe(registerInstructions);
+ expect(instructions).toBe(
+ 'sudo gitlab-runner register --url http://gdk.test:3000/ --registration-token MY_TOKEN',
+ );
+ });
+
+ describe('when a register token is not shown', () => {
+ beforeEach(async () => {
+ createComponent({ props: { registrationToken: undefined } });
+ await nextTick();
+ });
+
+ it('register command is shown without a defined registration token', () => {
+ const instructions = findRegisterCommand().text();
+
+ expect(instructions).toBe(registerInstructions);
+ });
});
});
describe('after a platform and architecture are selected', () => {
- const {
- installInstructions,
- registerInstructions,
- } = mockGraphqlInstructionsWindows.data.runnerSetup;
+ const { installInstructions } = mockGraphqlInstructionsWindows.data.runnerSetup;
beforeEach(async () => {
runnerSetupInstructionsHandler.mockResolvedValue(mockGraphqlInstructionsWindows);
@@ -157,7 +172,9 @@ describe('RunnerInstructionsModal component', () => {
it('register command is shown', () => {
const command = findRegisterCommand().text();
- expect(command).toBe(registerInstructions);
+ expect(command).toBe(
+ './gitlab-runner.exe register --url http://gdk.test:3000/ --registration-token MY_TOKEN',
+ );
});
});
@@ -217,4 +234,36 @@ describe('RunnerInstructionsModal component', () => {
expect(findRegisterCommand().exists()).toBe(false);
});
});
+
+ describe('GlModal API', () => {
+ const getGlModalStub = (methods) => {
+ return {
+ ...GlModal,
+ methods: {
+ ...GlModal.methods,
+ ...methods,
+ },
+ };
+ };
+
+ describe('show()', () => {
+ let mockShow;
+
+ beforeEach(() => {
+ mockShow = jest.fn();
+
+ createComponent({
+ stubs: {
+ GlModal: getGlModalStub({ show: mockShow }),
+ },
+ });
+ });
+
+ it('delegates show()', () => {
+ wrapper.vm.show();
+
+ expect(mockShow).toHaveBeenCalledTimes(1);
+ });
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/settings/__snapshots__/settings_block_spec.js.snap b/spec/frontend/vue_shared/components/settings/__snapshots__/settings_block_spec.js.snap
index 165caea2751..a0f46f07d6a 100644
--- a/spec/frontend/vue_shared/components/settings/__snapshots__/settings_block_spec.js.snap
+++ b/spec/frontend/vue_shared/components/settings/__snapshots__/settings_block_spec.js.snap
@@ -50,6 +50,7 @@ exports[`Settings Block renders the correct markup 1`] = `
class="settings-content"
id="settings_content_3"
role="region"
+ style="display: none;"
tabindex="-1"
>
<div
diff --git a/spec/frontend/vue_shared/components/settings/settings_block_spec.js b/spec/frontend/vue_shared/components/settings/settings_block_spec.js
index 528dfd89690..5e829653c13 100644
--- a/spec/frontend/vue_shared/components/settings/settings_block_spec.js
+++ b/spec/frontend/vue_shared/components/settings/settings_block_spec.js
@@ -1,12 +1,12 @@
import { GlButton } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
describe('Settings Block', () => {
let wrapper;
const mountComponent = (propsData) => {
- wrapper = shallowMount(SettingsBlock, {
+ wrapper = shallowMountExtended(SettingsBlock, {
propsData,
slots: {
title: '<div data-testid="title-slot"></div>',
@@ -20,11 +20,13 @@ describe('Settings Block', () => {
wrapper.destroy();
});
- const findDefaultSlot = () => wrapper.find('[data-testid="default-slot"]');
- const findTitleSlot = () => wrapper.find('[data-testid="title-slot"]');
- const findDescriptionSlot = () => wrapper.find('[data-testid="description-slot"]');
+ const findDefaultSlot = () => wrapper.findByTestId('default-slot');
+ const findTitleSlot = () => wrapper.findByTestId('title-slot');
+ const findDescriptionSlot = () => wrapper.findByTestId('description-slot');
const findExpandButton = () => wrapper.findComponent(GlButton);
- const findSectionTitleButton = () => wrapper.find('[data-testid="section-title-button"]');
+ const findSectionTitleButton = () => wrapper.findByTestId('section-title-button');
+ // we are using a non js class for this finder because this class determine the component structure
+ const findSettingsContent = () => wrapper.find('.settings-content');
const expectExpandedState = ({ expanded = true } = {}) => {
const settingsExpandButton = findExpandButton();
@@ -62,6 +64,26 @@ describe('Settings Block', () => {
expect(findDescriptionSlot().exists()).toBe(true);
});
+ it('content is hidden before first expansion', async () => {
+ // this is a regression test for the bug described here: https://gitlab.com/gitlab-org/gitlab/-/issues/331774
+ mountComponent();
+
+ // content is hidden
+ expect(findDefaultSlot().isVisible()).toBe(false);
+
+ // expand
+ await findSectionTitleButton().trigger('click');
+
+ // content is visible
+ expect(findDefaultSlot().isVisible()).toBe(true);
+
+ // collapse
+ await findSectionTitleButton().trigger('click');
+
+ // content is still visible (and we have a closing animation)
+ expect(findDefaultSlot().isVisible()).toBe(true);
+ });
+
describe('slide animation behaviour', () => {
it('is animated by default', () => {
mountComponent();
@@ -81,6 +103,20 @@ describe('Settings Block', () => {
expect(wrapper.classes('no-animate')).toBe(noAnimatedClass);
},
);
+
+ it('sets the animating class only during the animation', async () => {
+ mountComponent();
+
+ expect(wrapper.classes('animating')).toBe(false);
+
+ await findSectionTitleButton().trigger('click');
+
+ expect(wrapper.classes('animating')).toBe(true);
+
+ await findSettingsContent().trigger('animationend');
+
+ expect(wrapper.classes('animating')).toBe(false);
+ });
});
describe('expanded behaviour', () => {
diff --git a/spec/frontend/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js b/spec/frontend/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js
index 240d6cb5a34..79e41ed0c9e 100644
--- a/spec/frontend/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js
@@ -1,36 +1,68 @@
-import Vue from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
-import collapsedCalendarIcon from '~/vue_shared/components/sidebar/collapsed_calendar_icon.vue';
+import { shallowMount } from '@vue/test-utils';
+import { GlIcon } from '@gitlab/ui';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-describe('collapsedCalendarIcon', () => {
- let vm;
- beforeEach(() => {
- const CollapsedCalendarIcon = Vue.extend(collapsedCalendarIcon);
- vm = mountComponent(CollapsedCalendarIcon, {
- containerClass: 'test-class',
- text: 'text',
- showIcon: false,
+import CollapsedCalendarIcon from '~/vue_shared/components/sidebar/collapsed_calendar_icon.vue';
+
+describe('CollapsedCalendarIcon', () => {
+ let wrapper;
+
+ const defaultProps = {
+ containerClass: 'test-class',
+ text: 'text',
+ tooltipText: 'tooltip text',
+ showIcon: false,
+ };
+
+ const createComponent = ({ props = {} } = {}) => {
+ wrapper = shallowMount(CollapsedCalendarIcon, {
+ propsData: { ...defaultProps, ...props },
+ directives: {
+ GlTooltip: createMockDirective(),
+ },
});
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
});
- it('should add class to container', () => {
- expect(vm.$el.classList.contains('test-class')).toEqual(true);
+ const findGlIcon = () => wrapper.findComponent(GlIcon);
+ const getTooltip = () => getBinding(wrapper.element, 'gl-tooltip');
+
+ it('adds class to container', () => {
+ expect(wrapper.classes()).toContain(defaultProps.containerClass);
+ });
+
+ it('does not render calendar icon when showIcon is false', () => {
+ expect(findGlIcon().exists()).toBe(false);
+ });
+
+ it('renders calendar icon when showIcon is true', () => {
+ createComponent({
+ props: { showIcon: true },
+ });
+
+ expect(findGlIcon().exists()).toBe(true);
});
- it('should hide calendar icon if showIcon', () => {
- expect(vm.$el.querySelector('[data-testid="calendar-icon"]')).toBeNull();
+ it('renders text', () => {
+ expect(wrapper.text()).toBe(defaultProps.text);
});
- it('should render text', () => {
- expect(vm.$el.querySelector('span').innerText.trim()).toEqual('text');
+ it('renders tooltipText as tooltip', () => {
+ expect(getTooltip().value).toBe(defaultProps.tooltipText);
});
- it('should emit click event when container is clicked', () => {
- const click = jest.fn();
- vm.$on('click', click);
+ it('emits click event when container is clicked', async () => {
+ wrapper.trigger('click');
- vm.$el.click();
+ await wrapper.vm.$nextTick();
- expect(click).toHaveBeenCalled();
+ expect(wrapper.emitted('click')[0]).toBeDefined();
});
});
diff --git a/spec/frontend/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js b/spec/frontend/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js
index 230442ec547..e72b3bf45c4 100644
--- a/spec/frontend/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js
@@ -1,86 +1,103 @@
-import Vue from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
-import collapsedGroupedDatePicker from '~/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue';
-
-describe('collapsedGroupedDatePicker', () => {
- let vm;
- beforeEach(() => {
- const CollapsedGroupedDatePicker = Vue.extend(collapsedGroupedDatePicker);
- vm = mountComponent(CollapsedGroupedDatePicker, {
- showToggleSidebar: true,
+import { shallowMount } from '@vue/test-utils';
+
+import CollapsedGroupedDatePicker from '~/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue';
+import CollapsedCalendarIcon from '~/vue_shared/components/sidebar/collapsed_calendar_icon.vue';
+
+describe('CollapsedGroupedDatePicker', () => {
+ let wrapper;
+
+ const defaultProps = {
+ showToggleSidebar: true,
+ };
+
+ const minDate = new Date('07/17/2016');
+ const maxDate = new Date('07/17/2017');
+
+ const createComponent = ({ props = {} } = {}) => {
+ wrapper = shallowMount(CollapsedGroupedDatePicker, {
+ propsData: { ...defaultProps, ...props },
});
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
});
- describe('toggleCollapse events', () => {
- beforeEach((done) => {
- jest.spyOn(vm, 'toggleSidebar').mockImplementation(() => {});
- vm.minDate = new Date('07/17/2016');
- Vue.nextTick(done);
- });
+ const findCollapsedCalendarIcon = () => wrapper.findComponent(CollapsedCalendarIcon);
+ const findAllCollapsedCalendarIcons = () => wrapper.findAllComponents(CollapsedCalendarIcon);
+ describe('toggleCollapse events', () => {
it('should emit when collapsed-calendar-icon is clicked', () => {
- vm.$el.querySelector('.sidebar-collapsed-icon').click();
+ createComponent();
- expect(vm.toggleSidebar).toHaveBeenCalled();
+ findCollapsedCalendarIcon().trigger('click');
+
+ expect(wrapper.emitted('toggleCollapse')[0]).toBeDefined();
});
});
describe('minDate and maxDate', () => {
- beforeEach((done) => {
- vm.minDate = new Date('07/17/2016');
- vm.maxDate = new Date('07/17/2017');
- Vue.nextTick(done);
- });
-
it('should render both collapsed-calendar-icon', () => {
- const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon');
-
- expect(icons.length).toEqual(2);
- expect(icons[0].innerText.trim()).toEqual('Jul 17 2016');
- expect(icons[1].innerText.trim()).toEqual('Jul 17 2017');
+ createComponent({
+ props: {
+ minDate,
+ maxDate,
+ },
+ });
+
+ const icons = findAllCollapsedCalendarIcons();
+
+ expect(icons.length).toBe(2);
+ expect(icons.at(0).text()).toBe('Jul 17 2016');
+ expect(icons.at(1).text()).toBe('Jul 17 2017');
});
});
describe('minDate', () => {
- beforeEach((done) => {
- vm.minDate = new Date('07/17/2016');
- Vue.nextTick(done);
- });
-
it('should render minDate in collapsed-calendar-icon', () => {
- const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon');
+ createComponent({
+ props: {
+ minDate,
+ },
+ });
+
+ const icons = findAllCollapsedCalendarIcons();
- expect(icons.length).toEqual(1);
- expect(icons[0].innerText.trim()).toEqual('From Jul 17 2016');
+ expect(icons.length).toBe(1);
+ expect(icons.at(0).text()).toBe('From Jul 17 2016');
});
});
describe('maxDate', () => {
- beforeEach((done) => {
- vm.maxDate = new Date('07/17/2017');
- Vue.nextTick(done);
- });
-
it('should render maxDate in collapsed-calendar-icon', () => {
- const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon');
-
- expect(icons.length).toEqual(1);
- expect(icons[0].innerText.trim()).toEqual('Until Jul 17 2017');
+ createComponent({
+ props: {
+ maxDate,
+ },
+ });
+ const icons = findAllCollapsedCalendarIcons();
+
+ expect(icons.length).toBe(1);
+ expect(icons.at(0).text()).toBe('Until Jul 17 2017');
});
});
describe('no dates', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
it('should render None', () => {
- const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon');
+ const icons = findAllCollapsedCalendarIcons();
- expect(icons.length).toEqual(1);
- expect(icons[0].innerText.trim()).toEqual('None');
+ expect(icons.length).toBe(1);
+ expect(icons.at(0).text()).toBe('None');
});
it('should have tooltip as `Start and due date`', () => {
- const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon');
+ const icons = findAllCollapsedCalendarIcons();
- expect(icons[0].title).toBe('Start and due date');
+ expect(icons.at(0).props('tooltipText')).toBe('Start and due date');
});
});
});
diff --git a/spec/frontend/vue_shared/components/sidebar/date_picker_spec.js b/spec/frontend/vue_shared/components/sidebar/date_picker_spec.js
index 3221e88192b..263d1e9d947 100644
--- a/spec/frontend/vue_shared/components/sidebar/date_picker_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/date_picker_spec.js
@@ -1,3 +1,4 @@
+import { GlLoadingIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import DatePicker from '~/vue_shared/components/pikaday.vue';
import SidebarDatePicker from '~/vue_shared/components/sidebar/date_picker.vue';
@@ -5,14 +6,8 @@ import SidebarDatePicker from '~/vue_shared/components/sidebar/date_picker.vue';
describe('SidebarDatePicker', () => {
let wrapper;
- const mountComponent = (propsData = {}, data = {}) => {
- if (wrapper) {
- throw new Error('tried to call mountComponent without d');
- }
+ const createComponent = (propsData = {}, data = {}) => {
wrapper = mount(SidebarDatePicker, {
- stubs: {
- DatePicker: true,
- },
propsData,
data: () => data,
});
@@ -20,87 +15,93 @@ describe('SidebarDatePicker', () => {
afterEach(() => {
wrapper.destroy();
- wrapper = null;
});
+ const findDatePicker = () => wrapper.findComponent(DatePicker);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findEditButton = () => wrapper.find('.title .btn-blank');
+ const findRemoveButton = () => wrapper.find('.value-content .btn-blank');
+ const findSidebarToggle = () => wrapper.find('.title .gutter-toggle');
+ const findValueContent = () => wrapper.find('.value-content');
+
it('should emit toggleCollapse when collapsed toggle sidebar is clicked', () => {
- mountComponent();
+ createComponent();
- wrapper.find('.issuable-sidebar-header .gutter-toggle').element.click();
+ wrapper.find('.issuable-sidebar-header .gutter-toggle').trigger('click');
expect(wrapper.emitted('toggleCollapse')).toEqual([[]]);
});
it('should render collapsed-calendar-icon', () => {
- mountComponent();
+ createComponent();
- expect(wrapper.find('.sidebar-collapsed-icon').element).toBeDefined();
+ expect(wrapper.find('.sidebar-collapsed-icon').exists()).toBe(true);
});
it('should render value when not editing', () => {
- mountComponent();
+ createComponent();
- expect(wrapper.find('.value-content').element).toBeDefined();
+ expect(findValueContent().exists()).toBe(true);
});
it('should render None if there is no selectedDate', () => {
- mountComponent();
+ createComponent();
- expect(wrapper.find('.value-content span').text().trim()).toEqual('None');
+ expect(findValueContent().text()).toBe('None');
});
it('should render date-picker when editing', () => {
- mountComponent({}, { editing: true });
+ createComponent({}, { editing: true });
- expect(wrapper.find(DatePicker).element).toBeDefined();
+ expect(findDatePicker().exists()).toBe(true);
});
it('should render label', () => {
const label = 'label';
- mountComponent({ label });
- expect(wrapper.find('.title').text().trim()).toEqual(label);
+ createComponent({ label });
+ expect(wrapper.find('.title').text()).toBe(label);
});
it('should render loading-icon when isLoading', () => {
- mountComponent({ isLoading: true });
- expect(wrapper.find('.gl-spinner').element).toBeDefined();
+ createComponent({ isLoading: true });
+ expect(findLoadingIcon().exists()).toBe(true);
});
describe('editable', () => {
beforeEach(() => {
- mountComponent({ editable: true });
+ createComponent({ editable: true });
});
it('should render edit button', () => {
- expect(wrapper.find('.title .btn-blank').text().trim()).toEqual('Edit');
+ expect(findEditButton().text()).toBe('Edit');
});
it('should enable editing when edit button is clicked', async () => {
- wrapper.find('.title .btn-blank').element.click();
+ findEditButton().trigger('click');
await wrapper.vm.$nextTick();
- expect(wrapper.vm.editing).toEqual(true);
+ expect(wrapper.vm.editing).toBe(true);
});
});
it('should render date if selectedDate', () => {
- mountComponent({ selectedDate: new Date('07/07/2017') });
+ createComponent({ selectedDate: new Date('07/07/2017') });
- expect(wrapper.find('.value-content strong').text().trim()).toEqual('Jul 7, 2017');
+ expect(wrapper.find('.value-content strong').text()).toBe('Jul 7, 2017');
});
describe('selectedDate and editable', () => {
beforeEach(() => {
- mountComponent({ selectedDate: new Date('07/07/2017'), editable: true });
+ createComponent({ selectedDate: new Date('07/07/2017'), editable: true });
});
it('should render remove button if selectedDate and editable', () => {
- expect(wrapper.find('.value-content .btn-blank').text().trim()).toEqual('remove');
+ expect(findRemoveButton().text()).toBe('remove');
});
it('should emit saveDate with null when remove button is clicked', () => {
- wrapper.find('.value-content .btn-blank').element.click();
+ findRemoveButton().trigger('click');
expect(wrapper.emitted('saveDate')).toEqual([[null]]);
});
@@ -108,15 +109,15 @@ describe('SidebarDatePicker', () => {
describe('showToggleSidebar', () => {
beforeEach(() => {
- mountComponent({ showToggleSidebar: true });
+ createComponent({ showToggleSidebar: true });
});
it('should render toggle-sidebar when showToggleSidebar', () => {
- expect(wrapper.find('.title .gutter-toggle').element).toBeDefined();
+ expect(findSidebarToggle().exists()).toBe(true);
});
it('should emit toggleCollapse when toggle sidebar is clicked', () => {
- wrapper.find('.title .gutter-toggle').element.click();
+ findSidebarToggle().trigger('click');
expect(wrapper.emitted('toggleCollapse')).toEqual([[]]);
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed_spec.js
index 8c1693e8dcc..a7f9391cb5f 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed_spec.js
@@ -1,95 +1,74 @@
-import Vue from 'vue';
+import { shallowMount } from '@vue/test-utils';
+import { GlIcon } from '@gitlab/ui';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import DropdownValueCollapsedComponent from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed.vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
-import dropdownValueCollapsedComponent from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed.vue';
+import { mockCollapsedLabels as mockLabels, mockRegularLabel } from './mock_data';
-import { mockCollapsedLabels as mockLabels } from './mock_data';
-
-const createComponent = (labels = mockLabels) => {
- const Component = Vue.extend(dropdownValueCollapsedComponent);
+describe('DropdownValueCollapsedComponent', () => {
+ let wrapper;
- return mountComponent(Component, {
- labels,
- });
-};
+ const defaultProps = {
+ labels: [],
+ };
-describe('DropdownValueCollapsedComponent', () => {
- let vm;
+ const mockManyLabels = [...mockLabels, ...mockLabels, ...mockLabels];
- beforeEach(() => {
- vm = createComponent();
- });
+ const createComponent = ({ props = {} } = {}) => {
+ wrapper = shallowMount(DropdownValueCollapsedComponent, {
+ propsData: { ...defaultProps, ...props },
+ directives: {
+ GlTooltip: createMockDirective(),
+ },
+ });
+ };
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
- describe('computed', () => {
- describe('labelsList', () => {
- it('returns default text when `labels` prop is empty array', () => {
- const vmEmptyLabels = createComponent([]);
+ const findGlIcon = () => wrapper.findComponent(GlIcon);
+ const getTooltip = () => getBinding(wrapper.element, 'gl-tooltip');
- expect(vmEmptyLabels.labelsList).toBe('Labels');
- vmEmptyLabels.$destroy();
- });
-
- it('returns labels names separated by coma when `labels` prop has more than one item', () => {
- const labels = mockLabels.concat(mockLabels);
- const vmMoreLabels = createComponent(labels);
+ describe('template', () => {
+ it('renders tags icon element', () => {
+ createComponent();
- const expectedText = labels.map((label) => label.title).join(', ');
+ expect(findGlIcon().exists()).toBe(true);
+ });
- expect(vmMoreLabels.labelsList).toBe(expectedText);
- vmMoreLabels.$destroy();
- });
+ it('emits onValueClick event on click', async () => {
+ createComponent();
- it('returns labels names separated by coma with remaining labels count and `and more` phrase when `labels` prop has more than five items', () => {
- const mockMoreLabels = Object.assign([], mockLabels);
- for (let i = 0; i < 6; i += 1) {
- mockMoreLabels.unshift(mockLabels[0]);
- }
+ wrapper.trigger('click');
- const vmMoreLabels = createComponent(mockMoreLabels);
+ await wrapper.vm.$nextTick();
- const expectedText = `${mockMoreLabels
- .slice(0, 5)
- .map((label) => label.title)
- .join(', ')}, and ${mockMoreLabels.length - 5} more`;
+ expect(wrapper.emitted('onValueClick')[0]).toBeDefined();
+ });
- expect(vmMoreLabels.labelsList).toBe(expectedText);
- vmMoreLabels.$destroy();
+ describe.each`
+ scenario | labels | expectedResult | expectedText
+ ${'`labels` is empty'} | ${[]} | ${'default text'} | ${'Labels'}
+ ${'`labels` has 1 item'} | ${[mockRegularLabel]} | ${'label name'} | ${'Foo Label'}
+ ${'`labels` has 2 items'} | ${mockLabels} | ${'comma separated label names'} | ${'Foo Label, Foo::Bar'}
+ ${'`labels` has more than 5 items'} | ${mockManyLabels} | ${'comma separated label names with "and more" phrase'} | ${'Foo Label, Foo::Bar, Foo Label, Foo::Bar, Foo Label, and 1 more'}
+ `('when $scenario', ({ labels, expectedResult, expectedText }) => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ labels,
+ },
+ });
});
- it('returns first label name when `labels` prop has only one item present', () => {
- const text = mockLabels.map((label) => label.title).join(', ');
-
- expect(vm.labelsList).toBe(text);
+ it('renders labels count', () => {
+ expect(wrapper.text()).toBe(`${labels.length}`);
});
- });
- });
-
- describe('methods', () => {
- describe('handleClick', () => {
- it('emits onValueClick event on component', () => {
- jest.spyOn(vm, '$emit').mockImplementation(() => {});
- vm.handleClick();
- expect(vm.$emit).toHaveBeenCalledWith('onValueClick');
+ it(`renders "${expectedResult}" as tooltip`, () => {
+ expect(getTooltip().value).toBe(expectedText);
});
});
});
-
- describe('template', () => {
- it('renders component container element with tooltip`', () => {
- expect(vm.$el.title).toBe(vm.labelsList);
- });
-
- it('renders tags icon element', () => {
- expect(vm.$el.querySelector('[data-testid="labels-icon"]')).not.toBeNull();
- });
-
- it('renders labels count', () => {
- expect(vm.$el.querySelector('span').innerText.trim()).toBe(`${vm.labels.length}`);
- });
- });
});
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 d9b7cd5afa2..a60e6f52862 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
@@ -1,3 +1,4 @@
+import { cloneDeep } from 'lodash';
import * as types from '~/vue_shared/components/sidebar/labels_select_vue/store/mutation_types';
import mutations from '~/vue_shared/components/sidebar/labels_select_vue/store/mutations';
@@ -153,47 +154,40 @@ describe('LabelsSelect Mutations', () => {
});
describe(`${types.UPDATE_SELECTED_LABELS}`, () => {
- let labels;
-
- beforeEach(() => {
- labels = [
- { id: 1, title: 'scoped' },
- { id: 2, title: 'scoped::one', set: false },
- { id: 3, title: 'scoped::test', set: true },
- { id: 4, title: '' },
- ];
- });
-
- it('updates `state.labels` to include `touched` and `set` props based on provided `labels` param', () => {
- const updatedLabelIds = [2];
- const state = {
- labels,
- };
- mutations[types.UPDATE_SELECTED_LABELS](state, { labels: [{ id: 2 }] });
-
- state.labels.forEach((label) => {
- if (updatedLabelIds.includes(label.id)) {
- expect(label.touched).toBe(true);
- expect(label.set).toBe(true);
- }
+ const labels = [
+ { id: 1, title: 'scoped' },
+ { id: 2, title: 'scoped::label::one', set: false },
+ { id: 3, title: 'scoped::label::two', set: false },
+ { id: 4, title: 'scoped::label::three', set: true },
+ { id: 5, title: 'scoped::one', set: false },
+ { id: 6, title: 'scoped::two', set: false },
+ { id: 7, title: 'scoped::three', set: true },
+ { id: 8, title: '' },
+ ];
+
+ it.each`
+ label | labelGroupIds
+ ${labels[0]} | ${[]}
+ ${labels[1]} | ${[labels[2], labels[3]]}
+ ${labels[2]} | ${[labels[1], labels[3]]}
+ ${labels[3]} | ${[labels[1], labels[2]]}
+ ${labels[4]} | ${[labels[5], labels[6]]}
+ ${labels[5]} | ${[labels[4], labels[6]]}
+ ${labels[6]} | ${[labels[4], labels[5]]}
+ ${labels[7]} | ${[]}
+ `('updates `touched` and `set` props for $label.title', ({ label, labelGroupIds }) => {
+ const state = { labels: cloneDeep(labels) };
+
+ mutations[types.UPDATE_SELECTED_LABELS](state, { labels: [{ id: label.id }] });
+
+ expect(state.labels[label.id - 1]).toMatchObject({
+ touched: true,
+ set: !labels[label.id - 1].set,
});
- });
- describe('when label is scoped', () => {
- it('unsets the currently selected scoped label and sets the current label', () => {
- const state = {
- labels,
- };
- mutations[types.UPDATE_SELECTED_LABELS](state, {
- labels: [{ id: 2, title: 'scoped::one' }],
- });
-
- expect(state.labels).toEqual([
- { id: 1, title: 'scoped' },
- { id: 2, title: 'scoped::one', set: true, touched: true },
- { id: 3, title: 'scoped::test', set: false },
- { id: 4, title: '' },
- ]);
+ labelGroupIds.forEach((l) => {
+ expect(state.labels[l.id - 1].touched).toBeFalsy();
+ expect(state.labels[l.id - 1].set).toBe(false);
});
});
});
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 8931584e12c..bf873f9162b 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
@@ -5,8 +5,7 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
-import { IssuableType } from '~/issue_show/constants';
-import { labelsQueries } from '~/sidebar/constants';
+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';
import {
@@ -50,11 +49,12 @@ describe('DropdownContentsCreateView', () => {
const createComponent = ({
mutationHandler = createLabelSuccessHandler,
- issuableType = IssuableType.Issue,
+ labelCreateType = 'project',
+ workspaceType = 'project',
} = {}) => {
const mockApollo = createMockApollo([[createLabelMutation, mutationHandler]]);
mockApollo.clients.defaultClient.cache.writeQuery({
- query: labelsQueries[issuableType].workspaceQuery,
+ query: workspaceLabelsQueries[workspaceType].query,
data: workspaceLabelsQueryResponse.data,
variables: {
fullPath: '',
@@ -66,8 +66,10 @@ describe('DropdownContentsCreateView', () => {
localVue,
apolloProvider: mockApollo,
propsData: {
- issuableType,
fullPath: '',
+ attrWorkspacePath: '',
+ labelCreateType,
+ workspaceType,
},
});
};
@@ -128,9 +130,11 @@ describe('DropdownContentsCreateView', () => {
it('emits a `hideCreateView` event on Cancel button click', () => {
createComponent();
- findCancelButton().vm.$emit('click');
+ const event = { stopPropagation: jest.fn() };
+ findCancelButton().vm.$emit('click', event);
expect(wrapper.emitted('hideCreateView')).toHaveLength(1);
+ expect(event.stopPropagation).toHaveBeenCalled();
});
describe('when label title and selected color are set', () => {
@@ -174,7 +178,7 @@ describe('DropdownContentsCreateView', () => {
});
it('calls a mutation with `groupPath` variable on the epic', () => {
- createComponent({ issuableType: IssuableType.Epic });
+ createComponent({ labelCreateType: 'group', workspaceType: 'group' });
fillLabelAttributes();
findCreateButton().vm.$emit('click');
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 fac3331a2b8..2980409fdce 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
@@ -10,7 +10,6 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
-import { IssuableType } from '~/issue_show/constants';
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';
@@ -43,6 +42,7 @@ describe('DropdownContentsLabelsView', () => {
initialState = mockConfig,
queryHandler = successfulQueryHandler,
injected = {},
+ searchKey = '',
} = {}) => {
const mockApollo = createMockApollo([[projectLabelsQuery, queryHandler]]);
@@ -56,7 +56,9 @@ describe('DropdownContentsLabelsView', () => {
propsData: {
...initialState,
localSelectedLabels,
- issuableType: IssuableType.Issue,
+ searchKey,
+ labelCreateType: 'project',
+ workspaceType: 'project',
},
stubs: {
GlSearchBoxByType,
@@ -68,7 +70,6 @@ describe('DropdownContentsLabelsView', () => {
wrapper.destroy();
});
- const findSearchInput = () => wrapper.findComponent(GlSearchBoxByType);
const findLabels = () => wrapper.findAllComponents(LabelItem);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findObserver = () => wrapper.findComponent(GlIntersectionObserver);
@@ -81,12 +82,6 @@ describe('DropdownContentsLabelsView', () => {
}
describe('when loading labels', () => {
- it('renders disabled search input field', async () => {
- createComponent();
- await makeObserverAppear();
- expect(findSearchInput().props('disabled')).toBe(true);
- });
-
it('renders loading icon', async () => {
createComponent();
await makeObserverAppear();
@@ -107,10 +102,6 @@ describe('DropdownContentsLabelsView', () => {
await waitForPromises();
});
- it('renders enabled search input field', async () => {
- expect(findSearchInput().props('disabled')).toBe(false);
- });
-
it('does not render loading icon', async () => {
expect(findLoadingIcon().exists()).toBe(false);
});
@@ -132,9 +123,9 @@ describe('DropdownContentsLabelsView', () => {
},
},
}),
+ searchKey: '123',
});
await makeObserverAppear();
- findSearchInput().vm.$emit('input', '123');
await waitForPromises();
await nextTick();
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js
index 36704ac5ef3..8bcef347c96 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js
@@ -4,6 +4,8 @@ import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_w
import DropdownContents from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue';
import DropdownContentsCreateView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue';
import DropdownContentsLabelsView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue';
+import DropdownHeader from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue';
+import DropdownFooter from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_footer.vue';
import { mockLabels } from './mock_data';
@@ -26,7 +28,7 @@ const GlDropdownStub = {
describe('DropdownContent', () => {
let wrapper;
- const createComponent = ({ props = {}, injected = {}, data = {} } = {}) => {
+ const createComponent = ({ props = {}, data = {} } = {}) => {
wrapper = shallowMount(DropdownContents, {
propsData: {
labelsCreateTitle: 'test',
@@ -37,8 +39,10 @@ describe('DropdownContent', () => {
footerManageLabelTitle: 'manage',
dropdownButtonText: 'Labels',
variant: 'sidebar',
- issuableType: 'issue',
fullPath: 'test',
+ workspaceType: 'project',
+ labelCreateType: 'project',
+ attrWorkspacePath: 'path',
...props,
},
data() {
@@ -46,11 +50,6 @@ describe('DropdownContent', () => {
...data,
};
},
- provide: {
- allowLabelCreate: true,
- labelsManagePath: 'foo/bar',
- ...injected,
- },
stubs: {
GlDropdown: GlDropdownStub,
},
@@ -63,13 +62,10 @@ describe('DropdownContent', () => {
const findCreateView = () => wrapper.findComponent(DropdownContentsCreateView);
const findLabelsView = () => wrapper.findComponent(DropdownContentsLabelsView);
+ const findDropdownHeader = () => wrapper.findComponent(DropdownHeader);
+ const findDropdownFooter = () => wrapper.findComponent(DropdownFooter);
const findDropdown = () => wrapper.findComponent(GlDropdownStub);
- const findDropdownFooter = () => wrapper.find('[data-testid="dropdown-footer"]');
- const findDropdownHeader = () => wrapper.find('[data-testid="dropdown-header"]');
- const findCreateLabelButton = () => wrapper.find('[data-testid="create-label-button"]');
- const findGoBackButton = () => wrapper.find('[data-testid="go-back-button"]');
-
it('calls dropdown `show` method on `isVisible` prop change', async () => {
createComponent();
await wrapper.setProps({
@@ -136,6 +132,16 @@ describe('DropdownContent', () => {
expect(findDropdownHeader().exists()).toBe(true);
});
+ it('sets searchKey for labels view on input event from header', async () => {
+ createComponent();
+
+ expect(wrapper.vm.searchKey).toEqual('');
+ findDropdownHeader().vm.$emit('input', '123');
+ await nextTick();
+
+ expect(findLabelsView().props('searchKey')).toEqual('123');
+ });
+
describe('Create view', () => {
beforeEach(() => {
createComponent({ data: { showDropdownContentsCreateView: true } });
@@ -149,16 +155,8 @@ describe('DropdownContent', () => {
expect(findDropdownFooter().exists()).toBe(false);
});
- it('does not render create label button', () => {
- expect(findCreateLabelButton().exists()).toBe(false);
- });
-
- it('renders go back button', () => {
- expect(findGoBackButton().exists()).toBe(true);
- });
-
- it('changes the view to Labels view on back button click', async () => {
- findGoBackButton().vm.$emit('click', new MouseEvent('click'));
+ it('changes the view to Labels view on `toggleDropdownContentsCreateView` event', async () => {
+ findDropdownHeader().vm.$emit('toggleDropdownContentsCreateView');
await nextTick();
expect(findCreateView().exists()).toBe(false);
@@ -198,32 +196,5 @@ describe('DropdownContent', () => {
expect(findDropdownFooter().exists()).toBe(true);
});
-
- it('does not render go back button', () => {
- expect(findGoBackButton().exists()).toBe(false);
- });
-
- it('does not render create label button if `allowLabelCreate` is false', () => {
- createComponent({ injected: { allowLabelCreate: false } });
-
- expect(findCreateLabelButton().exists()).toBe(false);
- });
-
- describe('when `allowLabelCreate` is true', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('renders create label button', () => {
- expect(findCreateLabelButton().exists()).toBe(true);
- });
-
- it('changes the view to Create on create label button click', async () => {
- findCreateLabelButton().trigger('click');
-
- await nextTick();
- expect(findLabelsView().exists()).toBe(false);
- });
- });
});
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_footer_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_footer_spec.js
new file mode 100644
index 00000000000..0508a059195
--- /dev/null
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_footer_spec.js
@@ -0,0 +1,57 @@
+import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import DropdownFooter from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_footer.vue';
+
+describe('DropdownFooter', () => {
+ let wrapper;
+
+ const createComponent = ({ props = {}, injected = {} } = {}) => {
+ wrapper = shallowMount(DropdownFooter, {
+ propsData: {
+ footerCreateLabelTitle: 'create',
+ footerManageLabelTitle: 'manage',
+ ...props,
+ },
+ provide: {
+ allowLabelCreate: true,
+ labelsManagePath: 'foo/bar',
+ ...injected,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findCreateLabelButton = () => wrapper.find('[data-testid="create-label-button"]');
+
+ describe('Labels view', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('does not render create label button if `allowLabelCreate` is false', () => {
+ createComponent({ injected: { allowLabelCreate: false } });
+
+ expect(findCreateLabelButton().exists()).toBe(false);
+ });
+
+ describe('when `allowLabelCreate` is true', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders create label button', () => {
+ expect(findCreateLabelButton().exists()).toBe(true);
+ });
+
+ it('emits `toggleDropdownContentsCreateView` event on create label button click', async () => {
+ findCreateLabelButton().trigger('click');
+
+ await nextTick();
+ expect(wrapper.emitted('toggleDropdownContentsCreateView')).toEqual([[]]);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_header_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_header_spec.js
new file mode 100644
index 00000000000..592559ef305
--- /dev/null
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_header_spec.js
@@ -0,0 +1,75 @@
+import { GlSearchBoxByType } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import DropdownHeader from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue';
+
+describe('DropdownHeader', () => {
+ let wrapper;
+
+ const createComponent = ({
+ showDropdownContentsCreateView = false,
+ labelsFetchInProgress = false,
+ } = {}) => {
+ wrapper = extendedWrapper(
+ shallowMount(DropdownHeader, {
+ propsData: {
+ showDropdownContentsCreateView,
+ labelsFetchInProgress,
+ labelsCreateTitle: 'Create label',
+ labelsListTitle: 'Select label',
+ searchKey: '',
+ },
+ stubs: {
+ GlSearchBoxByType,
+ },
+ }),
+ );
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findSearchInput = () => wrapper.findComponent(GlSearchBoxByType);
+ const findGoBackButton = () => wrapper.findByTestId('go-back-button');
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ describe('Create view', () => {
+ beforeEach(() => {
+ createComponent({ showDropdownContentsCreateView: true });
+ });
+
+ it('renders go back button', () => {
+ expect(findGoBackButton().exists()).toBe(true);
+ });
+
+ it('does not render search input field', async () => {
+ expect(findSearchInput().exists()).toBe(false);
+ });
+ });
+
+ describe('Labels view', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('does not render go back button', () => {
+ expect(findGoBackButton().exists()).toBe(false);
+ });
+
+ it.each`
+ labelsFetchInProgress | disabled
+ ${true} | ${true}
+ ${false} | ${false}
+ `(
+ 'when labelsFetchInProgress is $labelsFetchInProgress, renders search input with disabled prop to $disabled',
+ ({ labelsFetchInProgress, disabled }) => {
+ createComponent({ labelsFetchInProgress });
+ expect(findSearchInput().props('disabled')).toBe(disabled);
+ },
+ );
+ });
+});
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 b5441d711a5..d4203528874 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
@@ -41,6 +41,8 @@ describe('LabelsSelectRoot', () => {
propsData: {
...config,
issuableType: IssuableType.Issue,
+ labelCreateType: 'project',
+ workspaceType: 'project',
},
stubs: {
SidebarEditableItem,
@@ -121,11 +123,11 @@ describe('LabelsSelectRoot', () => {
});
});
- it('emits `updateSelectedLabels` event on dropdown contents `setLabels` event', async () => {
+ it('emits `updateSelectedLabels` event on dropdown contents `setLabels` event if iid is not set', async () => {
const label = { id: 'gid://gitlab/ProjectLabel/1' };
- createComponent();
+ createComponent({ config: { ...mockConfig, iid: undefined } });
findDropdownContents().vm.$emit('setLabels', [label]);
- expect(wrapper.emitted('updateSelectedLabels')).toEqual([[[label]]]);
+ expect(wrapper.emitted('updateSelectedLabels')).toEqual([[{ labels: [label] }]]);
});
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js
index 23a457848d9..5c5bf5f2187 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js
@@ -40,12 +40,12 @@ export const mockConfig = {
labelsListTitle: 'Assign labels',
labelsCreateTitle: 'Create label',
variant: 'sidebar',
- selectedLabels: [mockRegularLabel, mockScopedLabel],
labelsSelectInProgress: false,
labelsFilterBasePath: '/gitlab-org/my-project/issues',
labelsFilterParam: 'label_name',
footerCreateLabelTitle: 'create',
footerManageLabelTitle: 'manage',
+ attrWorkspacePath: 'test',
};
export const mockSuggestedColors = {
@@ -80,6 +80,7 @@ export const createLabelSuccessfulResponse = {
color: '#dc143c',
description: null,
title: 'ewrwrwer',
+ textColor: '#000000',
__typename: 'Label',
},
errors: [],
@@ -91,6 +92,7 @@ export const createLabelSuccessfulResponse = {
export const workspaceLabelsQueryResponse = {
data: {
workspace: {
+ id: 'gid://gitlab/Project/126',
labels: {
nodes: [
{
@@ -98,12 +100,14 @@ export const workspaceLabelsQueryResponse = {
description: null,
id: 'gid://gitlab/ProjectLabel/1',
title: 'Label1',
+ textColor: '#000000',
},
{
color: '#2f7b2e',
description: null,
id: 'gid://gitlab/ProjectLabel/2',
title: 'Label2',
+ textColor: '#000000',
},
],
},
@@ -123,6 +127,7 @@ export const issuableLabelsQueryResponse = {
description: null,
id: 'gid://gitlab/ProjectLabel/1',
title: 'Label1',
+ textColor: '#000000',
},
],
},
diff --git a/spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js b/spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js
index f1c3e8a1ddc..a6c9bda1aa2 100644
--- a/spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js
@@ -1,31 +1,45 @@
-import Vue from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
-import toggleSidebar from '~/vue_shared/components/sidebar/toggle_sidebar.vue';
-
-describe('toggleSidebar', () => {
- let vm;
- beforeEach(() => {
- const ToggleSidebar = Vue.extend(toggleSidebar);
- vm = mountComponent(ToggleSidebar, {
- collapsed: true,
+import { GlButton } from '@gitlab/ui';
+import { mount, shallowMount } from '@vue/test-utils';
+
+import ToggleSidebar from '~/vue_shared/components/sidebar/toggle_sidebar.vue';
+
+describe('ToggleSidebar', () => {
+ let wrapper;
+
+ const defaultProps = {
+ collapsed: true,
+ };
+
+ const createComponent = ({ mountFn = shallowMount, props = {} } = {}) => {
+ wrapper = mountFn(ToggleSidebar, {
+ propsData: { ...defaultProps, ...props },
});
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
});
+ const findGlButton = () => wrapper.findComponent(GlButton);
+
it('should render the "chevron-double-lg-left" icon when collapsed', () => {
- expect(vm.$el.querySelector('[data-testid="chevron-double-lg-left-icon"]')).not.toBeNull();
+ createComponent();
+
+ expect(findGlButton().props('icon')).toBe('chevron-double-lg-left');
});
it('should render the "chevron-double-lg-right" icon when expanded', async () => {
- vm.collapsed = false;
- await Vue.nextTick();
- expect(vm.$el.querySelector('[data-testid="chevron-double-lg-right-icon"]')).not.toBeNull();
+ createComponent({ props: { collapsed: false } });
+
+ expect(findGlButton().props('icon')).toBe('chevron-double-lg-right');
});
- it('should emit toggle event when button clicked', () => {
- const toggle = jest.fn();
- vm.$on('toggle', toggle);
- vm.$el.click();
+ it('should emit toggle event when button clicked', async () => {
+ createComponent({ mountFn: mount });
+
+ findGlButton().trigger('click');
+ await wrapper.vm.$nextTick();
- expect(toggle).toHaveBeenCalled();
+ expect(wrapper.emitted('toggle')[0]).toBeDefined();
});
});
diff --git a/spec/frontend/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list_spec.js b/spec/frontend/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list_spec.js
index a92f058f311..78abb89e7b8 100644
--- a/spec/frontend/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list_spec.js
+++ b/spec/frontend/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list_spec.js
@@ -82,7 +82,7 @@ describe('User deletion obstacles list', () => {
createComponent({ obstacles: [{ type, name, url, projectName, projectUrl }] });
const msg = findObstacles().text();
- expect(msg).toContain(`in Project ${projectName}`);
+ expect(msg).toContain(`in project ${projectName}`);
expect(findLinks().at(1).attributes('href')).toBe(projectUrl);
});
},