summaryrefslogtreecommitdiff
path: root/spec/frontend/vue_shared
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-10-20 08:43:02 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-10-20 08:43:02 +0000
commitd9ab72d6080f594d0b3cae15f14b3ef2c6c638cb (patch)
tree2341ef426af70ad1e289c38036737e04b0aa5007 /spec/frontend/vue_shared
parentd6e514dd13db8947884cd58fe2a9c2a063400a9b (diff)
downloadgitlab-ce-d9ab72d6080f594d0b3cae15f14b3ef2c6c638cb.tar.gz
Add latest changes from gitlab-org/gitlab@14-4-stable-eev14.4.0-rc42
Diffstat (limited to 'spec/frontend/vue_shared')
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap4
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap2
-rw-r--r--spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js39
-rw-r--r--spec/frontend/vue_shared/components/color_picker/color_picker_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js34
-rw-r--r--spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js141
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js57
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js52
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/epic_token_spec.js97
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/iteration_token_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/weight_token_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/issue/issue_assignees_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js27
-rw-r--r--spec/frontend/vue_shared/components/markdown/suggestion_diff_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js3
-rw-r--r--spec/frontend/vue_shared/components/project_selector/project_selector_spec.js3
-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.js51
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js58
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js144
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js109
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js24
-rw-r--r--spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap11
-rw-r--r--spec/frontend/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list_spec.js116
-rw-r--r--spec/frontend/vue_shared/components/user_deletion_obstacles/utils_spec.js43
-rw-r--r--spec/frontend/vue_shared/components/user_popover/user_popover_spec.js30
-rw-r--r--spec/frontend/vue_shared/components/web_ide_link_spec.js22
-rw-r--r--spec/frontend/vue_shared/directives/validation_spec.js137
-rw-r--r--spec/frontend/vue_shared/oncall_schedules_list_spec.js87
-rw-r--r--spec/frontend/vue_shared/security_reports/mock_data.js2
-rw-r--r--spec/frontend/vue_shared/security_reports/security_reports_app_spec.js12
-rw-r--r--spec/frontend/vue_shared/security_reports/store/getters_spec.js4
37 files changed, 999 insertions, 348 deletions
diff --git a/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap
index c7758b0faef..44b4c0398cd 100644
--- a/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap
+++ b/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap
@@ -4,12 +4,12 @@ exports[`Clone Dropdown Button rendering matches the snapshot 1`] = `
<gl-dropdown-stub
category="primary"
clearalltext="Clear all"
+ clearalltextclass="gl-px-5"
headertext=""
hideheaderborder="true"
highlighteditemstitle="Selected"
highlighteditemstitleclass="gl-px-5"
right="true"
- showhighlighteditemstitle="true"
size="medium"
text="Clone"
variant="info"
@@ -35,6 +35,7 @@ exports[`Clone Dropdown Button rendering matches the snapshot 1`] = `
<b-form-input-stub
class="gl-form-input"
debounce="0"
+ formatter="[Function]"
readonly="true"
type="text"
value="ssh://foo.bar"
@@ -78,6 +79,7 @@ exports[`Clone Dropdown Button rendering matches the snapshot 1`] = `
<b-form-input-stub
class="gl-form-input"
debounce="0"
+ formatter="[Function]"
readonly="true"
type="text"
value="http://foo.bar"
diff --git a/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap
index f2ff12b2acd..2b89e36344d 100644
--- a/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap
+++ b/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap
@@ -4,12 +4,12 @@ exports[`SplitButton renders actionItems 1`] = `
<gl-dropdown-stub
category="primary"
clearalltext="Clear all"
+ clearalltextclass="gl-px-5"
headertext=""
hideheaderborder="true"
highlighteditemstitle="Selected"
highlighteditemstitleclass="gl-px-5"
menu-class=""
- showhighlighteditemstitle="true"
size="medium"
split="true"
text="professor"
diff --git a/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js b/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js
index c6c351a7f3f..3277aab43f0 100644
--- a/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js
@@ -1,25 +1,16 @@
import { shallowMount } from '@vue/test-utils';
-import waitForPromises from 'helpers/wait_for_promises';
import { HIGHLIGHT_CLASS_NAME } from '~/vue_shared/components/blob_viewers/constants';
import SimpleViewer from '~/vue_shared/components/blob_viewers/simple_viewer.vue';
-import SourceEditor from '~/vue_shared/components/source_editor.vue';
describe('Blob Simple Viewer component', () => {
let wrapper;
const contentMock = `<span id="LC1">First</span>\n<span id="LC2">Second</span>\n<span id="LC3">Third</span>`;
const blobHash = 'foo-bar';
- function createComponent(
- content = contentMock,
- isRawContent = false,
- isRefactorFlagEnabled = false,
- ) {
+ function createComponent(content = contentMock, isRawContent = false) {
wrapper = shallowMount(SimpleViewer, {
provide: {
blobHash,
- glFeatures: {
- refactorBlobViewer: isRefactorFlagEnabled,
- },
},
propsData: {
content,
@@ -94,32 +85,4 @@ describe('Blob Simple Viewer component', () => {
});
});
});
-
- describe('Vue refactoring to use Source Editor', () => {
- const findSourceEditor = () => wrapper.find(SourceEditor);
-
- it.each`
- doesRender | condition | isRawContent | isRefactorFlagEnabled
- ${'Does not'} | ${'rawContent is not specified'} | ${false} | ${true}
- ${'Does not'} | ${'feature flag is disabled is not specified'} | ${true} | ${false}
- ${'Does not'} | ${'both, the FF and rawContent are not specified'} | ${false} | ${false}
- ${'Does'} | ${'both, the FF and rawContent are specified'} | ${true} | ${true}
- `(
- '$doesRender render Source Editor component in readonly mode when $condition',
- async ({ isRawContent, isRefactorFlagEnabled } = {}) => {
- createComponent('raw content', isRawContent, isRefactorFlagEnabled);
- await waitForPromises();
-
- if (isRawContent && isRefactorFlagEnabled) {
- expect(findSourceEditor().exists()).toBe(true);
-
- expect(findSourceEditor().props('value')).toBe('raw content');
- expect(findSourceEditor().props('fileName')).toBe('test.js');
- expect(findSourceEditor().props('editorOptions')).toEqual({ readOnly: true });
- } else {
- expect(findSourceEditor().exists()).toBe(false);
- }
- },
- );
- });
});
diff --git a/spec/frontend/vue_shared/components/color_picker/color_picker_spec.js b/spec/frontend/vue_shared/components/color_picker/color_picker_spec.js
index d30f36ec63c..fef50bdaccc 100644
--- a/spec/frontend/vue_shared/components/color_picker/color_picker_spec.js
+++ b/spec/frontend/vue_shared/components/color_picker/color_picker_spec.js
@@ -111,15 +111,13 @@ describe('ColorPicker', () => {
gon.suggested_label_colors = {};
createComponent(shallowMount);
- expect(description()).toBe('Choose any color');
+ expect(description()).toBe('Enter any color.');
expect(presetColors().exists()).toBe(false);
});
it('shows the suggested colors', () => {
createComponent(shallowMount);
- expect(description()).toBe(
- 'Choose any color. Or you can choose one of the suggested colors below',
- );
+ expect(description()).toBe('Enter any color or choose one of the suggested colors below.');
expect(presetColors()).toHaveLength(4);
});
diff --git a/spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js b/spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js
index 175d79dd1c2..194681a6138 100644
--- a/spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js
+++ b/spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js
@@ -1,4 +1,4 @@
-import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
+import { GlAlert, GlSprintf } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import Component from '~/vue_shared/components/dismissible_feedback_alert.vue';
@@ -8,20 +8,13 @@ describe('Dismissible Feedback Alert', () => {
let wrapper;
- const defaultProps = {
- featureName: 'Dependency List',
- feedbackLink: 'https://gitlab.link',
- };
-
+ const featureName = 'Dependency List';
const STORAGE_DISMISSAL_KEY = 'dependency_list_feedback_dismissed';
- const createComponent = ({ props, shallow } = {}) => {
- const mountFn = shallow ? shallowMount : mount;
-
+ const createComponent = ({ mountFn = shallowMount } = {}) => {
wrapper = mountFn(Component, {
propsData: {
- ...defaultProps,
- ...props,
+ featureName,
},
stubs: {
GlSprintf,
@@ -34,8 +27,8 @@ describe('Dismissible Feedback Alert', () => {
wrapper = null;
});
- const findAlert = () => wrapper.find(GlAlert);
- const findLink = () => wrapper.find(GlLink);
+ const createFullComponent = () => createComponent({ mountFn: mount });
+ const findAlert = () => wrapper.findComponent(GlAlert);
describe('with default', () => {
beforeEach(() => {
@@ -46,17 +39,6 @@ describe('Dismissible Feedback Alert', () => {
expect(findAlert().exists()).toBe(true);
});
- it('contains feature name', () => {
- expect(findAlert().text()).toContain(defaultProps.featureName);
- });
-
- it('contains provided link', () => {
- const link = findLink();
-
- expect(link.attributes('href')).toBe(defaultProps.feedbackLink);
- expect(link.attributes('target')).toBe('_blank');
- });
-
it('should have the storage key set', () => {
expect(wrapper.vm.storageKey).toBe(STORAGE_DISMISSAL_KEY);
});
@@ -65,7 +47,7 @@ describe('Dismissible Feedback Alert', () => {
describe('dismissible', () => {
describe('after dismissal', () => {
beforeEach(() => {
- createComponent({ shallow: false });
+ createFullComponent();
findAlert().vm.$emit('dismiss');
});
@@ -81,7 +63,7 @@ describe('Dismissible Feedback Alert', () => {
describe('already dismissed', () => {
it('should not show the alert once dismissed', async () => {
localStorage.setItem(STORAGE_DISMISSAL_KEY, 'true');
- createComponent({ shallow: false });
+ createFullComponent();
await wrapper.vm.$nextTick();
expect(findAlert().exists()).toBe(false);
diff --git a/spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js b/spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js
new file mode 100644
index 00000000000..996df34f2ff
--- /dev/null
+++ b/spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js
@@ -0,0 +1,141 @@
+import { shallowMount } from '@vue/test-utils';
+import DropdownKeyboardNavigation from '~/vue_shared/components/dropdown_keyboard_navigation.vue';
+import { UP_KEY_CODE, DOWN_KEY_CODE, TAB_KEY_CODE } from '~/lib/utils/keycodes';
+
+const MOCK_INDEX = 0;
+const MOCK_MAX = 10;
+const MOCK_MIN = 0;
+const MOCK_DEFAULT_INDEX = 0;
+
+describe('DropdownKeyboardNavigation', () => {
+ let wrapper;
+
+ const defaultProps = {
+ index: MOCK_INDEX,
+ max: MOCK_MAX,
+ min: MOCK_MIN,
+ defaultIndex: MOCK_DEFAULT_INDEX,
+ };
+
+ const createComponent = (props) => {
+ wrapper = shallowMount(DropdownKeyboardNavigation, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ });
+ };
+
+ const helpers = {
+ arrowDown: () => {
+ document.dispatchEvent(new KeyboardEvent('keydown', { keyCode: DOWN_KEY_CODE }));
+ },
+ arrowUp: () => {
+ document.dispatchEvent(new KeyboardEvent('keydown', { keyCode: UP_KEY_CODE }));
+ },
+ tab: () => {
+ document.dispatchEvent(new KeyboardEvent('keydown', { keyCode: TAB_KEY_CODE }));
+ },
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('onInit', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('should $emit @change with the default index', async () => {
+ expect(wrapper.emitted('change')[0]).toStrictEqual([MOCK_DEFAULT_INDEX]);
+ });
+
+ it('should $emit @change with the default index when max changes', async () => {
+ wrapper.setProps({ max: 20 });
+ await wrapper.vm.$nextTick();
+ // The first @change`call happens on created() so we test for the second [1]
+ expect(wrapper.emitted('change')[1]).toStrictEqual([MOCK_DEFAULT_INDEX]);
+ });
+ });
+
+ describe('keydown events', () => {
+ let incrementSpy;
+
+ beforeEach(() => {
+ createComponent();
+ incrementSpy = jest.spyOn(wrapper.vm, 'increment');
+ });
+
+ afterEach(() => {
+ incrementSpy.mockRestore();
+ });
+
+ it('onKeydown-Down calls increment(1)', () => {
+ helpers.arrowDown();
+
+ expect(incrementSpy).toHaveBeenCalledWith(1);
+ });
+
+ it('onKeydown-Up calls increment(-1)', () => {
+ helpers.arrowUp();
+
+ expect(incrementSpy).toHaveBeenCalledWith(-1);
+ });
+
+ it('onKeydown-Tab $emits @tab event', () => {
+ helpers.tab();
+
+ expect(wrapper.emitted('tab')).toHaveLength(1);
+ });
+ });
+
+ describe('increment', () => {
+ describe('when max is 0', () => {
+ beforeEach(() => {
+ createComponent({ max: 0 });
+ });
+
+ it('does not $emit any @change events', () => {
+ helpers.arrowDown();
+
+ // The first @change`call happens on created() so we test that we only have 1 call
+ expect(wrapper.emitted('change')).toHaveLength(1);
+ });
+ });
+
+ describe.each`
+ keyboardAction | direction | index | max | min
+ ${helpers.arrowDown} | ${1} | ${10} | ${10} | ${0}
+ ${helpers.arrowUp} | ${-1} | ${0} | ${10} | ${0}
+ `('moving out of bounds', ({ keyboardAction, direction, index, max, min }) => {
+ beforeEach(() => {
+ createComponent({ index, max, min });
+ keyboardAction();
+ });
+
+ it(`in ${direction} direction does not $emit any @change events`, () => {
+ // The first @change`call happens on created() so we test that we only have 1 call
+ expect(wrapper.emitted('change')).toHaveLength(1);
+ });
+ });
+
+ describe.each`
+ keyboardAction | direction | index | max | min
+ ${helpers.arrowDown} | ${1} | ${0} | ${10} | ${0}
+ ${helpers.arrowUp} | ${-1} | ${10} | ${10} | ${0}
+ `('moving in bounds', ({ keyboardAction, direction, index, max, min }) => {
+ beforeEach(() => {
+ createComponent({ index, max, min });
+ keyboardAction();
+ });
+
+ it(`in ${direction} direction $emits @change event with the correct index ${
+ index + direction
+ }`, () => {
+ // The first @change`call happens on created() so we test for the second [1]
+ expect(wrapper.emitted('change')[1]).toStrictEqual([index + direction]);
+ });
+ });
+ });
+});
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 134c6c8b929..ae02c554e13 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
@@ -141,7 +141,62 @@ export const mockEpicToken = {
token: EpicToken,
operators: OPERATOR_IS_ONLY,
idProperty: 'iid',
- fetchEpics: () => Promise.resolve({ data: mockEpics }),
+ fullPath: 'gitlab-org',
+};
+
+export const mockEpicNode1 = {
+ __typename: 'Epic',
+ parent: null,
+ id: 'gid://gitlab/Epic/40',
+ iid: '2',
+ title: 'Marketing epic',
+ description: 'Mock epic description',
+ state: 'opened',
+ startDate: '2017-12-25',
+ dueDate: '2018-02-15',
+ webUrl: 'http://gdk.test:3000/groups/gitlab-org/marketing/-/epics/1',
+ hasChildren: false,
+ hasParent: false,
+ confidential: false,
+};
+
+export const mockEpicNode2 = {
+ __typename: 'Epic',
+ parent: null,
+ id: 'gid://gitlab/Epic/41',
+ iid: '3',
+ title: 'Another marketing',
+ startDate: '2017-12-26',
+ dueDate: '2018-03-10',
+ state: 'opened',
+ webUrl: 'http://gdk.test:3000/groups/gitlab-org/marketing/-/epics/2',
+};
+
+export const mockGroupEpicsQueryResponse = {
+ data: {
+ group: {
+ id: 'gid://gitlab/Group/1',
+ name: 'Gitlab Org',
+ epics: {
+ edges: [
+ {
+ node: {
+ ...mockEpicNode1,
+ },
+ __typename: 'EpicEdge',
+ },
+ {
+ node: {
+ ...mockEpicNode2,
+ },
+ __typename: 'EpicEdge',
+ },
+ ],
+ __typename: 'EpicConnection',
+ },
+ __typename: 'Group',
+ },
+ },
};
export const mockReactionEmojiToken = {
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 d3e1bfef561..14fcffd3c50 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
@@ -57,7 +57,7 @@ function createComponent(options = {}) {
provide: {
portalName: 'fake target',
alignSuggestions: function fakeAlignSuggestions() {},
- suggestionsListClass: 'custom-class',
+ suggestionsListClass: () => 'custom-class',
},
data() {
return { ...data };
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js
index eb1dbed52cc..f9ce0338d2f 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js
@@ -67,7 +67,7 @@ function createComponent({
provide: {
portalName: 'fake target',
alignSuggestions: jest.fn(),
- suggestionsListClass: 'custom-class',
+ suggestionsListClass: () => 'custom-class',
},
stubs,
slots,
@@ -206,26 +206,50 @@ describe('BaseToken', () => {
describe('events', () => {
let wrapperWithNoStubs;
- beforeEach(() => {
- wrapperWithNoStubs = createComponent({
- stubs: { Portal: true },
- });
- });
-
afterEach(() => {
wrapperWithNoStubs.destroy();
});
- it('emits `fetch-suggestions` event on component after a delay when component emits `input` event', async () => {
- jest.useFakeTimers();
+ describe('when activeToken has been selected', () => {
+ beforeEach(() => {
+ wrapperWithNoStubs = createComponent({
+ props: {
+ ...mockProps,
+ getActiveTokenValue: () => ({ title: '' }),
+ suggestionsLoading: true,
+ },
+ stubs: { Portal: true },
+ });
+ });
+ it('does not emit `fetch-suggestions` event on component after a delay when component emits `input` event', async () => {
+ jest.useFakeTimers();
- wrapperWithNoStubs.find(GlFilteredSearchToken).vm.$emit('input', { data: 'foo' });
- await wrapperWithNoStubs.vm.$nextTick();
+ wrapperWithNoStubs.find(GlFilteredSearchToken).vm.$emit('input', { data: 'foo' });
+ await wrapperWithNoStubs.vm.$nextTick();
- jest.runAllTimers();
+ jest.runAllTimers();
- expect(wrapperWithNoStubs.emitted('fetch-suggestions')).toBeTruthy();
- expect(wrapperWithNoStubs.emitted('fetch-suggestions')[2]).toEqual(['foo']);
+ expect(wrapperWithNoStubs.emitted('fetch-suggestions')).toEqual([['']]);
+ });
+ });
+
+ describe('when activeToken has not been selected', () => {
+ beforeEach(() => {
+ wrapperWithNoStubs = createComponent({
+ stubs: { Portal: true },
+ });
+ });
+ it('emits `fetch-suggestions` event on component after a delay when component emits `input` event', async () => {
+ jest.useFakeTimers();
+
+ wrapperWithNoStubs.find(GlFilteredSearchToken).vm.$emit('input', { data: 'foo' });
+ await wrapperWithNoStubs.vm.$nextTick();
+
+ jest.runAllTimers();
+
+ expect(wrapperWithNoStubs.emitted('fetch-suggestions')).toBeTruthy();
+ expect(wrapperWithNoStubs.emitted('fetch-suggestions')[2]).toEqual(['foo']);
+ });
});
});
});
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 09eac636cae..f3e8b2d0c1b 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
@@ -42,7 +42,7 @@ function createComponent(options = {}) {
provide: {
portalName: 'fake target',
alignSuggestions: function fakeAlignSuggestions() {},
- suggestionsListClass: 'custom-class',
+ suggestionsListClass: () => 'custom-class',
},
stubs,
});
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 c2d61fd9f05..36071c900df 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
@@ -48,7 +48,7 @@ function createComponent(options = {}) {
provide: {
portalName: 'fake target',
alignSuggestions: function fakeAlignSuggestions() {},
- suggestionsListClass: 'custom-class',
+ suggestionsListClass: () => 'custom-class',
},
stubs,
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/epic_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/epic_token_spec.js
index 68ed46fc3a2..6ee5d50d396 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/epic_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/epic_token_spec.js
@@ -1,15 +1,21 @@
-import { GlFilteredSearchToken, GlFilteredSearchTokenSegment } from '@gitlab/ui';
+import { GlFilteredSearchTokenSegment } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+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 axios from '~/lib/utils/axios_utils';
+import searchEpicsQuery from '~/vue_shared/components/filtered_search_bar/queries/search_epics.query.graphql';
import EpicToken from '~/vue_shared/components/filtered_search_bar/tokens/epic_token.vue';
+import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
-import { mockEpicToken, mockEpics } from '../mock_data';
+import { mockEpicToken, mockEpics, mockGroupEpicsQueryResponse } from '../mock_data';
jest.mock('~/flash');
+Vue.use(VueApollo);
const defaultStubs = {
Portal: true,
@@ -21,31 +27,39 @@ const defaultStubs = {
},
};
-function createComponent(options = {}) {
- const {
- config = mockEpicToken,
- value = { data: '' },
- active = false,
- stubs = defaultStubs,
- } = options;
- return mount(EpicToken, {
- propsData: {
- config,
- value,
- active,
- },
- provide: {
- portalName: 'fake target',
- alignSuggestions: function fakeAlignSuggestions() {},
- suggestionsListClass: 'custom-class',
- },
- stubs,
- });
-}
-
describe('EpicToken', () => {
let mock;
let wrapper;
+ let fakeApollo;
+
+ const findBaseToken = () => wrapper.findComponent(BaseToken);
+
+ function createComponent(
+ options = {},
+ epicsQueryHandler = jest.fn().mockResolvedValue(mockGroupEpicsQueryResponse),
+ ) {
+ fakeApollo = createMockApollo([[searchEpicsQuery, epicsQueryHandler]]);
+ const {
+ config = mockEpicToken,
+ value = { data: '' },
+ active = false,
+ stubs = defaultStubs,
+ } = options;
+ return mount(EpicToken, {
+ apolloProvider: fakeApollo,
+ propsData: {
+ config,
+ value,
+ active,
+ },
+ provide: {
+ portalName: 'fake target',
+ alignSuggestions: function fakeAlignSuggestions() {},
+ suggestionsListClass: 'custom-class',
+ },
+ stubs,
+ });
+ }
beforeEach(() => {
mock = new MockAdapter(axios);
@@ -71,23 +85,20 @@ describe('EpicToken', () => {
describe('methods', () => {
describe('fetchEpicsBySearchTerm', () => {
- it('calls `config.fetchEpics` with provided searchTerm param', () => {
- jest.spyOn(wrapper.vm.config, 'fetchEpics');
+ it('calls fetchEpics with provided searchTerm param', () => {
+ jest.spyOn(wrapper.vm, 'fetchEpics');
- wrapper.vm.fetchEpicsBySearchTerm({ search: 'foo' });
+ findBaseToken().vm.$emit('fetch-suggestions', 'foo');
- expect(wrapper.vm.config.fetchEpics).toHaveBeenCalledWith({
- epicPath: '',
- search: 'foo',
- });
+ expect(wrapper.vm.fetchEpics).toHaveBeenCalledWith('foo');
});
it('sets response to `epics` when request is successful', async () => {
- jest.spyOn(wrapper.vm.config, 'fetchEpics').mockResolvedValue({
+ jest.spyOn(wrapper.vm, 'fetchEpics').mockResolvedValue({
data: mockEpics,
});
- wrapper.vm.fetchEpicsBySearchTerm({});
+ findBaseToken().vm.$emit('fetch-suggestions');
await waitForPromises();
@@ -95,9 +106,9 @@ describe('EpicToken', () => {
});
it('calls `createFlash` with flash error message when request fails', async () => {
- jest.spyOn(wrapper.vm.config, 'fetchEpics').mockRejectedValue({});
+ jest.spyOn(wrapper.vm, 'fetchEpics').mockRejectedValue({});
- wrapper.vm.fetchEpicsBySearchTerm({ search: 'foo' });
+ findBaseToken().vm.$emit('fetch-suggestions', 'foo');
await waitForPromises();
@@ -107,9 +118,9 @@ describe('EpicToken', () => {
});
it('sets `loading` to false when request completes', async () => {
- jest.spyOn(wrapper.vm.config, 'fetchEpics').mockRejectedValue({});
+ jest.spyOn(wrapper.vm, 'fetchEpics').mockRejectedValue({});
- wrapper.vm.fetchEpicsBySearchTerm({ search: 'foo' });
+ findBaseToken().vm.$emit('fetch-suggestions', 'foo');
await waitForPromises();
@@ -123,15 +134,15 @@ describe('EpicToken', () => {
beforeEach(async () => {
wrapper = createComponent({
- value: { data: `${mockEpics[0].group_full_path}::&${mockEpics[0].iid}` },
+ value: { data: `${mockEpics[0].title}::&${mockEpics[0].iid}` },
data: { epics: mockEpics },
});
await wrapper.vm.$nextTick();
});
- it('renders gl-filtered-search-token component', () => {
- expect(wrapper.find(GlFilteredSearchToken).exists()).toBe(true);
+ it('renders BaseToken component', () => {
+ expect(findBaseToken().exists()).toBe(true);
});
it('renders token item when value is selected', () => {
@@ -142,9 +153,9 @@ describe('EpicToken', () => {
});
it.each`
- value | valueType | tokenValueString
- ${`${mockEpics[0].group_full_path}::&${mockEpics[0].iid}`} | ${'string'} | ${`${mockEpics[0].title}::&${mockEpics[0].iid}`}
- ${`${mockEpics[1].group_full_path}::&${mockEpics[1].iid}`} | ${'number'} | ${`${mockEpics[1].title}::&${mockEpics[1].iid}`}
+ value | valueType | tokenValueString
+ ${`${mockEpics[0].title}::&${mockEpics[0].iid}`} | ${'string'} | ${`${mockEpics[0].title}::&${mockEpics[0].iid}`}
+ ${`${mockEpics[1].title}::&${mockEpics[1].iid}`} | ${'number'} | ${`${mockEpics[1].title}::&${mockEpics[1].iid}`}
`('renders token item when selection is a $valueType', async ({ value, tokenValueString }) => {
wrapper.setProps({
value: { data: value },
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 a609aaa1c4e..af90ee93543 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
@@ -21,7 +21,7 @@ describe('IterationToken', () => {
provide: {
portalName: 'fake target',
alignSuggestions: function fakeAlignSuggestions() {},
- suggestionsListClass: 'custom-class',
+ suggestionsListClass: () => 'custom-class',
},
});
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 a348344b9dd..f55fb2836e3 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
@@ -48,7 +48,7 @@ function createComponent(options = {}) {
provide: {
portalName: 'fake target',
alignSuggestions: function fakeAlignSuggestions() {},
- suggestionsListClass: 'custom-class',
+ suggestionsListClass: () => 'custom-class',
},
stubs,
listeners,
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 bfb593bf82d..936841651d1 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
@@ -48,7 +48,7 @@ function createComponent(options = {}) {
provide: {
portalName: 'fake target',
alignSuggestions: function fakeAlignSuggestions() {},
- suggestionsListClass: 'custom-class',
+ suggestionsListClass: () => 'custom-class',
},
stubs,
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/weight_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/weight_token_spec.js
index e788c742736..4277899f8db 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/weight_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/weight_token_spec.js
@@ -19,7 +19,7 @@ describe('WeightToken', () => {
provide: {
portalName: 'fake target',
alignSuggestions: function fakeAlignSuggestions() {},
- suggestionsListClass: 'custom-class',
+ suggestionsListClass: () => 'custom-class',
},
});
diff --git a/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js b/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js
index 2658fa4a706..f74b9b37197 100644
--- a/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js
+++ b/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js
@@ -94,10 +94,6 @@ describe('IssueAssigneesComponent', () => {
expect(vm.avatarUrlTitle(mockAssigneesList[0])).toBe('Assigned to Terrell Graham');
});
- it('renders component root element with class `issue-assignees`', () => {
- expect(wrapper.element.classList.contains('issue-assignees')).toBe(true);
- });
-
it('renders assignee', () => {
const data = findAvatars().wrappers.map((x) => ({
...x.props(),
diff --git a/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js b/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js
index ba2450b56c9..9bc2aad1895 100644
--- a/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js
@@ -60,7 +60,7 @@ describe('Suggestion Diff component', () => {
expect(findHelpButton().exists()).toBe(true);
});
- it('renders apply suggestion and add to batch buttons', () => {
+ it('renders add to batch button when more than 1 suggestion', () => {
createComponent({
suggestionsCount: 2,
});
@@ -68,8 +68,7 @@ describe('Suggestion Diff component', () => {
const applyBtn = findApplyButton();
const addToBatchBtn = findAddToBatchButton();
- expect(applyBtn.exists()).toBe(true);
- expect(applyBtn.html().includes('Apply suggestion')).toBe(true);
+ expect(applyBtn.exists()).toBe(false);
expect(addToBatchBtn.exists()).toBe(true);
expect(addToBatchBtn.html().includes('Add suggestion to batch')).toBe(true);
@@ -85,7 +84,7 @@ describe('Suggestion Diff component', () => {
describe('when apply suggestion is clicked', () => {
beforeEach(() => {
- createComponent();
+ createComponent({ batchSuggestionsCount: 0 });
findApplyButton().vm.$emit('apply');
});
@@ -140,11 +139,11 @@ describe('Suggestion Diff component', () => {
describe('apply suggestions is clicked', () => {
it('emits applyBatch', () => {
- createComponent({ isBatched: true });
+ createComponent({ isBatched: true, batchSuggestionsCount: 2 });
- findApplyBatchButton().vm.$emit('click');
+ findApplyButton().vm.$emit('apply');
- expect(wrapper.emitted().applyBatch).toEqual([[]]);
+ expect(wrapper.emitted().applyBatch).toEqual([[undefined]]);
});
});
@@ -155,23 +154,24 @@ describe('Suggestion Diff component', () => {
isBatched: true,
});
- const applyBatchBtn = findApplyBatchButton();
+ const applyBatchBtn = findApplyButton();
const removeFromBatchBtn = findRemoveFromBatchButton();
expect(removeFromBatchBtn.exists()).toBe(true);
expect(removeFromBatchBtn.html().includes('Remove from batch')).toBe(true);
expect(applyBatchBtn.exists()).toBe(true);
- expect(applyBatchBtn.html().includes('Apply suggestions')).toBe(true);
+ expect(applyBatchBtn.html().includes('Apply suggestion')).toBe(true);
expect(applyBatchBtn.html().includes(String('9'))).toBe(true);
});
it('hides add to batch and apply buttons', () => {
createComponent({
isBatched: true,
+ batchSuggestionsCount: 9,
});
- expect(findApplyButton().exists()).toBe(false);
+ expect(findApplyButton().exists()).toBe(true);
expect(findAddToBatchButton().exists()).toBe(false);
});
@@ -215,9 +215,8 @@ describe('Suggestion Diff component', () => {
});
it('disables apply suggestion and hides add to batch button', () => {
- expect(findApplyButton().exists()).toBe(true);
+ expect(findApplyButton().exists()).toBe(false);
expect(findAddToBatchButton().exists()).toBe(false);
- expect(findApplyButton().attributes('disabled')).toBe('true');
});
});
@@ -225,7 +224,7 @@ describe('Suggestion Diff component', () => {
const findTooltip = () => getBinding(findApplyButton().element, 'gl-tooltip');
it('renders correct tooltip message when button is applicable', () => {
- createComponent();
+ createComponent({ batchSuggestionsCount: 0 });
const tooltip = findTooltip();
expect(tooltip.modifiers.viewport).toBe(true);
@@ -234,7 +233,7 @@ describe('Suggestion Diff component', () => {
it('renders the inapplicable reason in the tooltip when button is not applicable', () => {
const inapplicableReason = 'lorem';
- createComponent({ canApply: false, inapplicableReason });
+ createComponent({ canApply: false, inapplicableReason, batchSuggestionsCount: 0 });
const tooltip = findTooltip();
expect(tooltip.modifiers.viewport).toBe(true);
diff --git a/spec/frontend/vue_shared/components/markdown/suggestion_diff_spec.js b/spec/frontend/vue_shared/components/markdown/suggestion_diff_spec.js
index 5bd6bda2d2c..af27e953776 100644
--- a/spec/frontend/vue_shared/components/markdown/suggestion_diff_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/suggestion_diff_spec.js
@@ -77,7 +77,7 @@ describe('Suggestion Diff component', () => {
it.each`
event | childArgs | args
${'apply'} | ${['test-event']} | ${[{ callback: 'test-event', suggestionId }]}
- ${'applyBatch'} | ${[]} | ${[]}
+ ${'applyBatch'} | ${['test-event']} | ${['test-event']}
${'addToBatch'} | ${[]} | ${[suggestionId]}
${'removeFromBatch'} | ${[]} | ${[suggestionId]}
`('emits $event event on sugestion diff header $event', ({ event, childArgs, args }) => {
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 ab028ea52b7..1ed7844b395 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,4 +1,6 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
+// eslint-disable-next-line import/no-deprecated
+import { getJSONFixture } from 'helpers/fixtures';
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';
@@ -11,6 +13,7 @@ describe('ProjectListItem component', () => {
let vm;
let options;
+ // eslint-disable-next-line import/no-deprecated
const project = getJSONFixture('static/projects.json')[0];
beforeEach(() => {
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 06b00a8e196..1f97d3ff3fa 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,6 +2,8 @@ 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 { 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';
@@ -11,6 +13,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 searchResults = allProjects.slice(0, 5);
let selected = [];
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 14e0c8a2278..d9b7cd5afa2 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
@@ -157,9 +157,9 @@ describe('LabelsSelect Mutations', () => {
beforeEach(() => {
labels = [
- { id: 1, title: 'scoped::test', set: true },
- { id: 2, set: false, title: 'scoped::one' },
- { id: 3, title: '' },
+ { id: 1, title: 'scoped' },
+ { id: 2, title: 'scoped::one', set: false },
+ { id: 3, title: 'scoped::test', set: true },
{ id: 4, title: '' },
];
});
@@ -189,9 +189,9 @@ describe('LabelsSelect Mutations', () => {
});
expect(state.labels).toEqual([
- { id: 1, title: 'scoped::test', set: false },
- { id: 2, set: true, title: 'scoped::one', touched: true },
- { id: 3, title: '' },
+ { id: 1, title: 'scoped' },
+ { id: 2, title: 'scoped::one', set: true, touched: true },
+ { id: 3, title: 'scoped::test', set: false },
{ id: 4, title: '' },
]);
});
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 843298a1406..8931584e12c 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,13 +5,14 @@ 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 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 projectLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql';
import {
mockSuggestedColors,
createLabelSuccessfulResponse,
- labelsQueryResponse,
+ workspaceLabelsQueryResponse,
} from './mock_data';
jest.mock('~/flash');
@@ -47,11 +48,14 @@ describe('DropdownContentsCreateView', () => {
findAllColors().at(0).vm.$emit('click', new Event('mouseclick'));
};
- const createComponent = ({ mutationHandler = createLabelSuccessHandler } = {}) => {
+ const createComponent = ({
+ mutationHandler = createLabelSuccessHandler,
+ issuableType = IssuableType.Issue,
+ } = {}) => {
const mockApollo = createMockApollo([[createLabelMutation, mutationHandler]]);
mockApollo.clients.defaultClient.cache.writeQuery({
- query: projectLabelsQuery,
- data: labelsQueryResponse.data,
+ query: labelsQueries[issuableType].workspaceQuery,
+ data: workspaceLabelsQueryResponse.data,
variables: {
fullPath: '',
searchTerm: '',
@@ -61,6 +65,10 @@ describe('DropdownContentsCreateView', () => {
wrapper = shallowMount(DropdownContentsCreateView, {
localVue,
apolloProvider: mockApollo,
+ propsData: {
+ issuableType,
+ fullPath: '',
+ },
});
};
@@ -135,15 +143,6 @@ describe('DropdownContentsCreateView', () => {
expect(findCreateButton().props('disabled')).toBe(false);
});
- it('calls a mutation with correct parameters on Create button click', () => {
- findCreateButton().vm.$emit('click');
- expect(createLabelSuccessHandler).toHaveBeenCalledWith({
- color: '#009966',
- projectPath: '',
- title: 'Test title',
- });
- });
-
it('renders a loader spinner after Create button click', async () => {
findCreateButton().vm.$emit('click');
await nextTick();
@@ -162,6 +161,30 @@ describe('DropdownContentsCreateView', () => {
});
});
+ it('calls a mutation with `projectPath` variable on the issue', () => {
+ createComponent();
+ fillLabelAttributes();
+ findCreateButton().vm.$emit('click');
+
+ expect(createLabelSuccessHandler).toHaveBeenCalledWith({
+ color: '#009966',
+ projectPath: '',
+ title: 'Test title',
+ });
+ });
+
+ it('calls a mutation with `groupPath` variable on the epic', () => {
+ createComponent({ issuableType: IssuableType.Epic });
+ fillLabelAttributes();
+ findCreateButton().vm.$emit('click');
+
+ expect(createLabelSuccessHandler).toHaveBeenCalledWith({
+ color: '#009966',
+ groupPath: '',
+ title: 'Test title',
+ });
+ });
+
it('calls createFlash is mutation has a user-recoverable error', async () => {
createComponent({ mutationHandler: createLabelUserRecoverableErrorHandler });
fillLabelAttributes();
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 537bbc8e71e..fac3331a2b8 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
@@ -1,36 +1,43 @@
-import { GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
+import {
+ GlLoadingIcon,
+ GlSearchBoxByType,
+ GlDropdownItem,
+ GlIntersectionObserver,
+} from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { 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 { 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';
import projectLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql';
import LabelItem from '~/vue_shared/components/sidebar/labels_select_widget/label_item.vue';
-import { mockConfig, labelsQueryResponse } from './mock_data';
+import { mockConfig, workspaceLabelsQueryResponse } from './mock_data';
jest.mock('~/flash');
const localVue = createLocalVue();
localVue.use(VueApollo);
-const selectedLabels = [
+const localSelectedLabels = [
{
- id: 28,
- title: 'Bug',
- description: 'Label for bugs',
- color: '#FF0000',
- textColor: '#FFFFFF',
+ color: '#2f7b2e',
+ description: null,
+ id: 'gid://gitlab/ProjectLabel/2',
+ title: 'Label2',
},
];
describe('DropdownContentsLabelsView', () => {
let wrapper;
- const successfulQueryHandler = jest.fn().mockResolvedValue(labelsQueryResponse);
+ const successfulQueryHandler = jest.fn().mockResolvedValue(workspaceLabelsQueryResponse);
+
+ const findFirstLabel = () => wrapper.findAllComponents(GlDropdownItem).at(0);
const createComponent = ({
initialState = mockConfig,
@@ -43,14 +50,13 @@ describe('DropdownContentsLabelsView', () => {
localVue,
apolloProvider: mockApollo,
provide: {
- projectPath: 'test',
- iid: 1,
variant: DropdownVariant.Sidebar,
...injected,
},
propsData: {
...initialState,
- selectedLabels,
+ localSelectedLabels,
+ issuableType: IssuableType.Issue,
},
stubs: {
GlSearchBoxByType,
@@ -65,23 +71,31 @@ describe('DropdownContentsLabelsView', () => {
const findSearchInput = () => wrapper.findComponent(GlSearchBoxByType);
const findLabels = () => wrapper.findAllComponents(LabelItem);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findObserver = () => wrapper.findComponent(GlIntersectionObserver);
const findLabelsList = () => wrapper.find('[data-testid="labels-list"]');
const findNoResultsMessage = () => wrapper.find('[data-testid="no-results"]');
+ async function makeObserverAppear() {
+ await findObserver().vm.$emit('appear');
+ }
+
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();
expect(findLoadingIcon().exists()).toBe(true);
});
it('does not render labels list', async () => {
createComponent();
+ await makeObserverAppear();
expect(findLabelsList().exists()).toBe(false);
});
});
@@ -89,6 +103,7 @@ describe('DropdownContentsLabelsView', () => {
describe('when labels are loaded', () => {
beforeEach(async () => {
createComponent();
+ await makeObserverAppear();
await waitForPromises();
});
@@ -118,6 +133,7 @@ describe('DropdownContentsLabelsView', () => {
},
}),
});
+ await makeObserverAppear();
findSearchInput().vm.$emit('input', '123');
await waitForPromises();
await nextTick();
@@ -127,8 +143,26 @@ describe('DropdownContentsLabelsView', () => {
it('calls `createFlash` 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();
});
+
+ it('emits an `input` event on label click', async () => {
+ createComponent();
+ await makeObserverAppear();
+ await waitForPromises();
+ findFirstLabel().trigger('click');
+
+ expect(wrapper.emitted('input')[0][0]).toEqual(expect.arrayContaining(localSelectedLabels));
+ });
+
+ it('does not trigger query when component did not appear', () => {
+ createComponent();
+ expect(findLoadingIcon().exists()).toBe(false);
+ expect(findLabelsList().exists()).toBe(false);
+ expect(successfulQueryHandler).not.toHaveBeenCalled();
+ });
});
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 a1b40a891ec..36704ac5ef3 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
@@ -1,6 +1,5 @@
-import { GlDropdown } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-
+import { nextTick } from 'vue';
import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
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';
@@ -8,10 +7,26 @@ import DropdownContentsLabelsView from '~/vue_shared/components/sidebar/labels_s
import { mockLabels } from './mock_data';
+const showDropdown = jest.fn();
+
+const GlDropdownStub = {
+ template: `
+ <div data-testid="dropdown">
+ <slot name="header"></slot>
+ <slot></slot>
+ <slot name="footer"></slot>
+ </div>
+ `,
+ methods: {
+ show: showDropdown,
+ hide: jest.fn(),
+ },
+};
+
describe('DropdownContent', () => {
let wrapper;
- const createComponent = ({ props = {}, injected = {} } = {}) => {
+ const createComponent = ({ props = {}, injected = {}, data = {} } = {}) => {
wrapper = shallowMount(DropdownContents, {
propsData: {
labelsCreateTitle: 'test',
@@ -22,38 +37,112 @@ describe('DropdownContent', () => {
footerManageLabelTitle: 'manage',
dropdownButtonText: 'Labels',
variant: 'sidebar',
+ issuableType: 'issue',
+ fullPath: 'test',
...props,
},
+ data() {
+ return {
+ ...data,
+ };
+ },
provide: {
allowLabelCreate: true,
labelsManagePath: 'foo/bar',
...injected,
},
stubs: {
- GlDropdown,
+ GlDropdown: GlDropdownStub,
},
});
};
- beforeEach(() => {
- createComponent();
- });
-
afterEach(() => {
wrapper.destroy();
});
+ const findCreateView = () => wrapper.findComponent(DropdownContentsCreateView);
+ const findLabelsView = () => wrapper.findComponent(DropdownContentsLabelsView);
+ 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({
+ isVisible: true,
+ });
+
+ expect(findDropdown().emitted('show')).toBeUndefined();
+ });
+
+ it('does not emit `setLabels` event on dropdown hide if labels did not change', () => {
+ createComponent();
+ findDropdown().vm.$emit('hide');
+
+ expect(wrapper.emitted('setLabels')).toBeUndefined();
+ });
+
+ it('emits `setLabels` event on dropdown hide if labels changed on non-sidebar widget', async () => {
+ createComponent({ props: { variant: DropdownVariant.Standalone } });
+ const updatedLabel = {
+ id: 28,
+ title: 'Bug',
+ description: 'Label for bugs',
+ color: '#FF0000',
+ textColor: '#FFFFFF',
+ };
+ findLabelsView().vm.$emit('input', [updatedLabel]);
+ await nextTick();
+ findDropdown().vm.$emit('hide');
+
+ expect(wrapper.emitted('setLabels')).toEqual([[[updatedLabel]]]);
+ });
+
+ it('emits `setLabels` event on visibility change if labels changed on sidebar widget', async () => {
+ createComponent({ props: { variant: DropdownVariant.Standalone, isVisible: true } });
+ const updatedLabel = {
+ id: 28,
+ title: 'Bug',
+ description: 'Label for bugs',
+ color: '#FF0000',
+ textColor: '#FFFFFF',
+ };
+ findLabelsView().vm.$emit('input', [updatedLabel]);
+ wrapper.setProps({ isVisible: false });
+ await nextTick();
+
+ expect(wrapper.emitted('setLabels')).toEqual([[[updatedLabel]]]);
+ });
+
+ it('does not render header on standalone variant', () => {
+ createComponent({ props: { variant: DropdownVariant.Standalone } });
+
+ expect(findDropdownHeader().exists()).toBe(false);
+ });
+
+ it('renders header on embedded variant', () => {
+ createComponent({ props: { variant: DropdownVariant.Embedded } });
+
+ expect(findDropdownHeader().exists()).toBe(true);
+ });
+
+ it('renders header on sidebar variant', () => {
+ createComponent();
+
+ expect(findDropdownHeader().exists()).toBe(true);
+ });
+
describe('Create view', () => {
beforeEach(() => {
- wrapper.vm.toggleDropdownContentsCreateView();
+ createComponent({ data: { showDropdownContentsCreateView: true } });
});
it('renders create view when `showDropdownContentsCreateView` prop is `true`', () => {
- expect(wrapper.findComponent(DropdownContentsCreateView).exists()).toBe(true);
+ expect(findCreateView().exists()).toBe(true);
});
it('does not render footer', () => {
@@ -67,11 +156,31 @@ describe('DropdownContent', () => {
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'));
+ await nextTick();
+
+ expect(findCreateView().exists()).toBe(false);
+ expect(findLabelsView().exists()).toBe(true);
+ });
+
+ it('changes the view to Labels view on `hideCreateView` event', async () => {
+ findCreateView().vm.$emit('hideCreateView');
+ await nextTick();
+
+ expect(findCreateView().exists()).toBe(false);
+ expect(findLabelsView().exists()).toBe(true);
+ });
});
describe('Labels view', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
it('renders labels view when `showDropdownContentsCreateView` when `showDropdownContentsCreateView` prop is `false`', () => {
- expect(wrapper.findComponent(DropdownContentsLabelsView).exists()).toBe(true);
+ expect(findLabelsView().exists()).toBe(true);
});
it('renders footer on sidebar dropdown', () => {
@@ -109,19 +218,12 @@ describe('DropdownContent', () => {
expect(findCreateLabelButton().exists()).toBe(true);
});
- it('triggers `toggleDropdownContent` method on create label button click', () => {
- jest.spyOn(wrapper.vm, 'toggleDropdownContent').mockImplementation(() => {});
+ it('changes the view to Create on create label button click', async () => {
findCreateLabelButton().trigger('click');
- expect(wrapper.vm.toggleDropdownContent).toHaveBeenCalled();
+ await nextTick();
+ expect(findLabelsView().exists()).toBe(false);
});
});
});
-
- describe('template', () => {
- it('renders component container element with classes `gl-w-full gl-mt-2` and no styles', () => {
- expect(wrapper.attributes('class')).toContain('gl-w-full gl-mt-2');
- expect(wrapper.attributes('style')).toBeUndefined();
- });
- });
});
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 a18511fa21d..b5441d711a5 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
@@ -1,28 +1,55 @@
-import { shallowMount } from '@vue/test-utils';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { 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 { IssuableType } from '~/issue_show/constants';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import DropdownContents from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue';
import DropdownValue from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue';
-import DropdownValueCollapsed from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_value_collapsed.vue';
+import issueLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql';
import LabelsSelectRoot from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue';
+import { mockConfig, issuableLabelsQueryResponse } from './mock_data';
-import { mockConfig } from './mock_data';
+jest.mock('~/flash');
+
+const localVue = createLocalVue();
+localVue.use(VueApollo);
+
+const successfulQueryHandler = jest.fn().mockResolvedValue(issuableLabelsQueryResponse);
+const errorQueryHandler = jest.fn().mockRejectedValue('Houston, we have a problem');
describe('LabelsSelectRoot', () => {
let wrapper;
- const createComponent = (config = mockConfig, slots = {}) => {
+ const findSidebarEditableItem = () => wrapper.findComponent(SidebarEditableItem);
+ const findDropdownValue = () => wrapper.findComponent(DropdownValue);
+ const findDropdownContents = () => wrapper.findComponent(DropdownContents);
+
+ const createComponent = ({
+ config = mockConfig,
+ slots = {},
+ queryHandler = successfulQueryHandler,
+ } = {}) => {
+ const mockApollo = createMockApollo([[issueLabelsQuery, queryHandler]]);
+
wrapper = shallowMount(LabelsSelectRoot, {
slots,
- propsData: config,
+ apolloProvider: mockApollo,
+ localVue,
+ propsData: {
+ ...config,
+ issuableType: IssuableType.Issue,
+ },
stubs: {
- DropdownContents,
SidebarEditableItem,
},
provide: {
- iid: '1',
- projectPath: 'test',
canUpdate: true,
allowLabelEdit: true,
+ allowLabelCreate: true,
+ labelsManagePath: 'test',
},
});
};
@@ -42,33 +69,63 @@ describe('LabelsSelectRoot', () => {
${'embedded'} | ${'is-embedded'}
`(
'renders component root element with CSS class `$cssClass` when `state.variant` is "$variant"',
- ({ variant, cssClass }) => {
+ async ({ variant, cssClass }) => {
createComponent({
- ...mockConfig,
- variant,
+ config: { ...mockConfig, variant },
});
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.classes()).toContain(cssClass);
- });
+ await nextTick();
+ expect(wrapper.classes()).toContain(cssClass);
},
);
- it('renders `dropdown-value-collapsed` component when `allowLabelCreate` prop is `true`', async () => {
- createComponent();
- await wrapper.vm.$nextTick;
- expect(wrapper.find(DropdownValueCollapsed).exists()).toBe(true);
- });
+ describe('if dropdown variant is `sidebar`', () => {
+ it('renders sidebar editable item', () => {
+ createComponent();
+ expect(findSidebarEditableItem().exists()).toBe(true);
+ });
+
+ it('passes true `loading` prop to sidebar editable item when loading labels', () => {
+ createComponent();
+ expect(findSidebarEditableItem().props('loading')).toBe(true);
+ });
- it('renders `dropdown-value` component', async () => {
- createComponent(mockConfig, {
- default: 'None',
+ describe('when labels are fetched successfully', () => {
+ beforeEach(async () => {
+ createComponent();
+ await waitForPromises();
+ });
+
+ it('passes true `loading` prop to sidebar editable item', () => {
+ expect(findSidebarEditableItem().props('loading')).toBe(false);
+ });
+
+ it('renders dropdown value component when query labels is resolved', () => {
+ expect(findDropdownValue().exists()).toBe(true);
+ expect(findDropdownValue().props('selectedLabels')).toEqual(
+ issuableLabelsQueryResponse.data.workspace.issuable.labels.nodes,
+ );
+ });
+
+ it('emits `onLabelRemove` event on dropdown value label remove event', () => {
+ const label = { id: 'gid://gitlab/ProjectLabel/1' };
+ findDropdownValue().vm.$emit('onLabelRemove', label);
+ expect(wrapper.emitted('onLabelRemove')).toEqual([[label]]);
+ });
});
- await wrapper.vm.$nextTick;
- const valueComp = wrapper.find(DropdownValue);
+ it('creates flash with error message when query is rejected', async () => {
+ createComponent({ queryHandler: errorQueryHandler });
+ await waitForPromises();
+ expect(createFlash).toHaveBeenCalledWith({ message: 'Error fetching labels.' });
+ });
+ });
+
+ it('emits `updateSelectedLabels` event on dropdown contents `setLabels` event', async () => {
+ const label = { id: 'gid://gitlab/ProjectLabel/1' };
+ createComponent();
- expect(valueComp.exists()).toBe(true);
- expect(valueComp.text()).toBe('None');
+ findDropdownContents().vm.$emit('setLabels', [label]);
+ expect(wrapper.emitted('updateSelectedLabels')).toEqual([[[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 fceaabec2d0..23a457848d9 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
@@ -34,6 +34,8 @@ export const mockLabels = [
];
export const mockConfig = {
+ iid: '1',
+ fullPath: 'test',
allowMultiselect: true,
labelsListTitle: 'Assign labels',
labelsCreateTitle: 'Create label',
@@ -86,7 +88,7 @@ export const createLabelSuccessfulResponse = {
},
};
-export const labelsQueryResponse = {
+export const workspaceLabelsQueryResponse = {
data: {
workspace: {
labels: {
@@ -108,3 +110,23 @@ export const labelsQueryResponse = {
},
},
};
+
+export const issuableLabelsQueryResponse = {
+ data: {
+ workspace: {
+ issuable: {
+ id: '1',
+ labels: {
+ nodes: [
+ {
+ color: '#330066',
+ description: null,
+ id: 'gid://gitlab/ProjectLabel/1',
+ title: 'Label1',
+ },
+ ],
+ },
+ },
+ },
+ },
+};
diff --git a/spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap b/spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap
index af4fa462cbf..0f1e118d44c 100644
--- a/spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap
+++ b/spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap
@@ -45,6 +45,7 @@ exports[`Upload dropzone component correctly overrides description and drop mess
>
<div
class="mw-50 gl-text-center"
+ style="display: none;"
>
<h3
class=""
@@ -61,7 +62,6 @@ exports[`Upload dropzone component correctly overrides description and drop mess
<div
class="mw-50 gl-text-center"
- style="display: none;"
>
<h3
class=""
@@ -146,7 +146,6 @@ exports[`Upload dropzone component when dragging renders correct template when d
<div
class="mw-50 gl-text-center"
- style=""
>
<h3
class=""
@@ -231,7 +230,6 @@ exports[`Upload dropzone component when dragging renders correct template when d
<div
class="mw-50 gl-text-center"
- style=""
>
<h3
class=""
@@ -299,6 +297,7 @@ exports[`Upload dropzone component when dragging renders correct template when d
>
<div
class="mw-50 gl-text-center"
+ style=""
>
<h3
class=""
@@ -383,6 +382,7 @@ exports[`Upload dropzone component when dragging renders correct template when d
>
<div
class="mw-50 gl-text-center"
+ style=""
>
<h3
class=""
@@ -467,6 +467,7 @@ exports[`Upload dropzone component when dragging renders correct template when d
>
<div
class="mw-50 gl-text-center"
+ style=""
>
<h3
class=""
@@ -551,6 +552,7 @@ exports[`Upload dropzone component when no slot provided renders default dropzon
>
<div
class="mw-50 gl-text-center"
+ style="display: none;"
>
<h3
class=""
@@ -567,7 +569,6 @@ exports[`Upload dropzone component when no slot provided renders default dropzon
<div
class="mw-50 gl-text-center"
- style="display: none;"
>
<h3
class=""
@@ -603,6 +604,7 @@ exports[`Upload dropzone component when slot provided renders dropzone with slot
>
<div
class="mw-50 gl-text-center"
+ style="display: none;"
>
<h3
class=""
@@ -619,7 +621,6 @@ exports[`Upload dropzone component when slot provided renders dropzone with slot
<div
class="mw-50 gl-text-center"
- style="display: none;"
>
<h3
class=""
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
new file mode 100644
index 00000000000..a92f058f311
--- /dev/null
+++ b/spec/frontend/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list_spec.js
@@ -0,0 +1,116 @@
+import { GlLink, GlSprintf } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { OBSTACLE_TYPES } from '~/vue_shared/components/user_deletion_obstacles/constants';
+import UserDeletionObstaclesList from '~/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.vue';
+
+const mockSchedules = [
+ {
+ type: OBSTACLE_TYPES.oncallSchedules,
+ name: 'Schedule 1',
+ url: 'http://gitlab.com/gitlab-org/gitlab-shell/-/oncall_schedules',
+ projectName: 'Shell',
+ projectUrl: 'http://gitlab.com/gitlab-org/gitlab-shell/',
+ },
+ {
+ type: OBSTACLE_TYPES.oncallSchedules,
+ name: 'Schedule 2',
+ url: 'http://gitlab.com/gitlab-org/gitlab-ui/-/oncall_schedules',
+ projectName: 'UI',
+ projectUrl: 'http://gitlab.com/gitlab-org/gitlab-ui/',
+ },
+];
+const mockPolicies = [
+ {
+ type: OBSTACLE_TYPES.escalationPolicies,
+ name: 'Policy 1',
+ url: 'http://gitlab.com/gitlab-org/gitlab-ui/-/escalation-policies',
+ projectName: 'UI',
+ projectUrl: 'http://gitlab.com/gitlab-org/gitlab-ui/',
+ },
+];
+const mockObstacles = mockSchedules.concat(mockPolicies);
+
+const userName = "O'User";
+
+describe('User deletion obstacles list', () => {
+ let wrapper;
+
+ function createComponent(props) {
+ wrapper = extendedWrapper(
+ shallowMount(UserDeletionObstaclesList, {
+ propsData: {
+ obstacles: mockObstacles,
+ userName,
+ ...props,
+ },
+ stubs: {
+ GlSprintf,
+ },
+ }),
+ );
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findLinks = () => wrapper.findAllComponents(GlLink);
+ const findTitle = () => wrapper.findByTestId('title');
+ const findFooter = () => wrapper.findByTestId('footer');
+ const findObstacles = () => wrapper.findByTestId('obstacles-list');
+
+ describe.each`
+ isCurrentUser | titleText | footerText
+ ${true} | ${'You are currently a part of:'} | ${'Removing yourself may put your on-call team at risk of missing a notification.'}
+ ${false} | ${`User ${userName} is currently part of:`} | ${'Removing this user may put their on-call team at risk of missing a notification.'}
+ `('when current user', ({ isCurrentUser, titleText, footerText }) => {
+ it(`${isCurrentUser ? 'is' : 'is not'} a part of on-call management`, async () => {
+ createComponent({
+ isCurrentUser,
+ });
+
+ expect(findTitle().text()).toBe(titleText);
+ expect(findFooter().text()).toBe(footerText);
+ });
+ });
+
+ describe.each(mockObstacles)(
+ 'renders all obstacles',
+ ({ type, name, url, projectName, projectUrl }) => {
+ it(`includes the project name and link for ${name}`, () => {
+ createComponent({ obstacles: [{ type, name, url, projectName, projectUrl }] });
+ const msg = findObstacles().text();
+
+ expect(msg).toContain(`in Project ${projectName}`);
+ expect(findLinks().at(1).attributes('href')).toBe(projectUrl);
+ });
+ },
+ );
+
+ describe.each(mockSchedules)(
+ 'renders on-call schedules',
+ ({ type, name, url, projectName, projectUrl }) => {
+ it(`includes the schedule name and link for ${name}`, () => {
+ createComponent({ obstacles: [{ type, name, url, projectName, projectUrl }] });
+ const msg = findObstacles().text();
+
+ expect(msg).toContain(`On-call schedule ${name}`);
+ expect(findLinks().at(0).attributes('href')).toBe(url);
+ });
+ },
+ );
+
+ describe.each(mockPolicies)(
+ 'renders escalation policies',
+ ({ type, name, url, projectName, projectUrl }) => {
+ it(`includes the policy name and link for ${name}`, () => {
+ createComponent({ obstacles: [{ type, name, url, projectName, projectUrl }] });
+ const msg = findObstacles().text();
+
+ expect(msg).toContain(`Escalation policy ${name}`);
+ expect(findLinks().at(0).attributes('href')).toBe(url);
+ });
+ },
+ );
+});
diff --git a/spec/frontend/vue_shared/components/user_deletion_obstacles/utils_spec.js b/spec/frontend/vue_shared/components/user_deletion_obstacles/utils_spec.js
new file mode 100644
index 00000000000..99f739098f7
--- /dev/null
+++ b/spec/frontend/vue_shared/components/user_deletion_obstacles/utils_spec.js
@@ -0,0 +1,43 @@
+import { OBSTACLE_TYPES } from '~/vue_shared/components/user_deletion_obstacles/constants';
+import { parseUserDeletionObstacles } from '~/vue_shared/components/user_deletion_obstacles/utils';
+
+describe('parseUserDeletionObstacles', () => {
+ const mockObstacles = [{ name: 'Obstacle' }];
+ const expectedSchedule = { name: 'Obstacle', type: OBSTACLE_TYPES.oncallSchedules };
+ const expectedPolicy = { name: 'Obstacle', type: OBSTACLE_TYPES.escalationPolicies };
+
+ it('is undefined when user is not available', () => {
+ expect(parseUserDeletionObstacles()).toHaveLength(0);
+ });
+
+ it('is empty when obstacles are not available for user', () => {
+ expect(parseUserDeletionObstacles({})).toHaveLength(0);
+ });
+
+ it('is empty when user has no obstacles to deletion', () => {
+ const input = { oncallSchedules: [], escalationPolicies: [] };
+
+ expect(parseUserDeletionObstacles(input)).toHaveLength(0);
+ });
+
+ it('returns obstacles with type when user is part of on-call schedules', () => {
+ const input = { oncallSchedules: mockObstacles, escalationPolicies: [] };
+ const expectedOutput = [expectedSchedule];
+
+ expect(parseUserDeletionObstacles(input)).toEqual(expectedOutput);
+ });
+
+ it('returns obstacles with type when user is part of escalation policies', () => {
+ const input = { oncallSchedules: [], escalationPolicies: mockObstacles };
+ const expectedOutput = [expectedPolicy];
+
+ expect(parseUserDeletionObstacles(input)).toEqual(expectedOutput);
+ });
+
+ it('returns obstacles with type when user have every obstacle type', () => {
+ const input = { oncallSchedules: mockObstacles, escalationPolicies: mockObstacles };
+ const expectedOutput = [expectedSchedule, expectedPolicy];
+
+ expect(parseUserDeletionObstacles(input)).toEqual(expectedOutput);
+ });
+});
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 926223e0670..09633daf587 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
@@ -9,6 +9,7 @@ const DEFAULT_PROPS = {
username: 'root',
name: 'Administrator',
location: 'Vienna',
+ localTime: '2:30 PM',
bot: false,
bio: null,
workInformation: null,
@@ -31,10 +32,11 @@ describe('User Popover Component', () => {
wrapper.destroy();
});
- const findUserStatus = () => wrapper.find('.js-user-status');
+ const findUserStatus = () => wrapper.findByTestId('user-popover-status');
const findTarget = () => document.querySelector('.js-user-link');
const findUserName = () => wrapper.find(UserNameWithStatus);
const findSecurityBotDocsLink = () => wrapper.findByTestId('user-popover-bot-docs-link');
+ const findUserLocalTime = () => wrapper.findByTestId('user-popover-local-time');
const createWrapper = (props = {}, options = {}) => {
wrapper = mountExtended(UserPopover, {
@@ -71,7 +73,6 @@ describe('User Popover Component', () => {
expect(wrapper.text()).toContain(DEFAULT_PROPS.user.name);
expect(wrapper.text()).toContain(DEFAULT_PROPS.user.username);
- expect(wrapper.text()).toContain(DEFAULT_PROPS.user.location);
});
it('shows icon for location', () => {
@@ -164,6 +165,25 @@ describe('User Popover Component', () => {
});
});
+ describe('local time', () => {
+ it('should show local time when it is available', () => {
+ createWrapper();
+
+ expect(findUserLocalTime().exists()).toBe(true);
+ });
+
+ it('should not show local time when it is not available', () => {
+ const user = {
+ ...DEFAULT_PROPS.user,
+ localTime: null,
+ };
+
+ createWrapper({ user });
+
+ expect(findUserLocalTime().exists()).toBe(false);
+ });
+ });
+
describe('status data', () => {
it('should show only message', () => {
const user = { ...DEFAULT_PROPS.user, status: { message_html: 'Hello World' } };
@@ -256,5 +276,11 @@ describe('User Popover Component', () => {
const securityBotDocsLink = findSecurityBotDocsLink();
expect(securityBotDocsLink.text()).toBe('Learn more about %<>\';"');
});
+
+ it('does not display local time', () => {
+ createWrapper({ user: SECURITY_BOT_USER });
+
+ expect(findUserLocalTime().exists()).toBe(false);
+ });
});
});
diff --git a/spec/frontend/vue_shared/components/web_ide_link_spec.js b/spec/frontend/vue_shared/components/web_ide_link_spec.js
index 5fe4eeb6061..92938b2717f 100644
--- a/spec/frontend/vue_shared/components/web_ide_link_spec.js
+++ b/spec/frontend/vue_shared/components/web_ide_link_spec.js
@@ -160,4 +160,26 @@ describe('Web IDE link component', () => {
expect(findLocalStorageSync().props('value')).toBe(ACTION_GITPOD.key);
});
});
+
+ describe('edit actions', () => {
+ it.each([
+ {
+ props: { showWebIdeButton: true, showEditButton: false },
+ expectedEventPayload: 'ide',
+ },
+ {
+ props: { showWebIdeButton: false, showEditButton: true },
+ expectedEventPayload: 'simple',
+ },
+ ])(
+ 'emits the correct event when an action handler is called',
+ async ({ props, expectedEventPayload }) => {
+ createComponent({ ...props, needsToFork: true });
+
+ findActionsButton().props('actions')[0].handle();
+
+ expect(wrapper.emitted('edit')).toEqual([[expectedEventPayload]]);
+ },
+ );
+ });
});
diff --git a/spec/frontend/vue_shared/directives/validation_spec.js b/spec/frontend/vue_shared/directives/validation_spec.js
index 51ee73cabde..dcd3a44a6fc 100644
--- a/spec/frontend/vue_shared/directives/validation_spec.js
+++ b/spec/frontend/vue_shared/directives/validation_spec.js
@@ -4,11 +4,13 @@ import validation, { initForm } from '~/vue_shared/directives/validation';
describe('validation directive', () => {
let wrapper;
- const createComponentFactory = ({ inputAttributes, template, data }) => {
- const defaultInputAttributes = {
- type: 'text',
- required: true,
- };
+ const createComponentFactory = (options) => {
+ const {
+ inputAttributes = { type: 'text', required: true },
+ template,
+ data,
+ feedbackMap = {},
+ } = options;
const defaultTemplate = `
<form>
@@ -18,11 +20,11 @@ describe('validation directive', () => {
const component = {
directives: {
- validation: validation(),
+ validation: validation(feedbackMap),
},
data() {
return {
- attributes: inputAttributes || defaultInputAttributes,
+ attributes: inputAttributes,
...data,
};
},
@@ -32,8 +34,10 @@ describe('validation directive', () => {
wrapper = shallowMount(component, { attachTo: document.body });
};
- const createComponent = ({ inputAttributes, showValidation, template } = {}) =>
- createComponentFactory({
+ const createComponent = (options = {}) => {
+ const { inputAttributes, showValidation, template, feedbackMap } = options;
+
+ return createComponentFactory({
inputAttributes,
data: {
showValidation,
@@ -48,10 +52,14 @@ describe('validation directive', () => {
},
},
template,
+ feedbackMap,
});
+ };
+
+ const createComponentWithInitForm = (options = {}) => {
+ const { inputAttributes, feedbackMap } = options;
- const createComponentWithInitForm = ({ inputAttributes } = {}) =>
- createComponentFactory({
+ return createComponentFactory({
inputAttributes,
data: {
form: initForm({
@@ -68,7 +76,9 @@ describe('validation directive', () => {
<input v-validation:[form.showValidation] name="exampleField" v-bind="attributes" />
</form>
`,
+ feedbackMap,
});
+ };
afterEach(() => {
wrapper.destroy();
@@ -209,6 +219,111 @@ describe('validation directive', () => {
});
});
+ describe('with custom feedbackMap', () => {
+ const customMessage = 'Please fill out the name field.';
+ const template = `
+ <form>
+ <div v-validation:[showValidation]>
+ <input name="exampleField" v-bind="attributes" />
+ </div>
+ </form>
+ `;
+ beforeEach(() => {
+ const feedbackMap = {
+ valueMissing: {
+ isInvalid: (el) => el.validity?.valueMissing,
+ message: customMessage,
+ },
+ };
+
+ createComponent({
+ template,
+ inputAttributes: {
+ required: true,
+ },
+ feedbackMap,
+ });
+ });
+
+ describe('with invalid value', () => {
+ beforeEach(() => {
+ setValueAndTriggerValidation('');
+ });
+
+ it('should set correct field state', () => {
+ expect(getFormData().fields.exampleField).toEqual({
+ state: false,
+ feedback: customMessage,
+ });
+ });
+ });
+
+ describe('with valid value', () => {
+ beforeEach(() => {
+ setValueAndTriggerValidation('hello');
+ });
+
+ it('set the correct state', () => {
+ expect(getFormData().fields.exampleField).toEqual({
+ state: true,
+ feedback: '',
+ });
+ });
+ });
+ });
+
+ describe('with validation-message present on the element', () => {
+ const customMessage = 'The name field is required.';
+ const template = `
+ <form>
+ <div v-validation:[showValidation]>
+ <input name="exampleField" v-bind="attributes" validation-message="${customMessage}" />
+ </div>
+ </form>
+ `;
+ beforeEach(() => {
+ const feedbackMap = {
+ valueMissing: {
+ isInvalid: (el) => el.validity?.valueMissing,
+ },
+ };
+
+ createComponent({
+ template,
+ inputAttributes: {
+ required: true,
+ },
+ feedbackMap,
+ });
+ });
+
+ describe('with invalid value', () => {
+ beforeEach(() => {
+ setValueAndTriggerValidation('');
+ });
+
+ it('should set correct field state', () => {
+ expect(getFormData().fields.exampleField).toEqual({
+ state: false,
+ feedback: customMessage,
+ });
+ });
+ });
+
+ describe('with valid value', () => {
+ beforeEach(() => {
+ setValueAndTriggerValidation('hello');
+ });
+
+ it('set the correct state', () => {
+ expect(getFormData().fields.exampleField).toEqual({
+ state: true,
+ feedback: '',
+ });
+ });
+ });
+ });
+
describe('component using initForm', () => {
it('sets the form fields correctly', () => {
createComponentWithInitForm();
diff --git a/spec/frontend/vue_shared/oncall_schedules_list_spec.js b/spec/frontend/vue_shared/oncall_schedules_list_spec.js
deleted file mode 100644
index f83a5187b8b..00000000000
--- a/spec/frontend/vue_shared/oncall_schedules_list_spec.js
+++ /dev/null
@@ -1,87 +0,0 @@
-import { GlLink, GlSprintf } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import OncallSchedulesList from '~/vue_shared/components/oncall_schedules_list.vue';
-
-const mockSchedules = [
- {
- name: 'Schedule 1',
- scheduleUrl: 'http://gitlab.com/gitlab-org/gitlab-shell/-/oncall_schedules',
- projectName: 'Shell',
- projectUrl: 'http://gitlab.com/gitlab-org/gitlab-shell/',
- },
- {
- name: 'Schedule 2',
- scheduleUrl: 'http://gitlab.com/gitlab-org/gitlab-ui/-/oncall_schedules',
- projectName: 'UI',
- projectUrl: 'http://gitlab.com/gitlab-org/gitlab-ui/',
- },
-];
-
-const userName = "O'User";
-
-describe('On-call schedules list', () => {
- let wrapper;
-
- function createComponent(props) {
- wrapper = extendedWrapper(
- shallowMount(OncallSchedulesList, {
- propsData: {
- schedules: mockSchedules,
- userName,
- ...props,
- },
- stubs: {
- GlSprintf,
- },
- }),
- );
- }
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- const findLinks = () => wrapper.findAllComponents(GlLink);
- const findTitle = () => wrapper.findByTestId('title');
- const findFooter = () => wrapper.findByTestId('footer');
- const findSchedules = () => wrapper.findByTestId('schedules-list');
-
- describe.each`
- isCurrentUser | titleText | footerText
- ${true} | ${'You are currently a part of:'} | ${'Removing yourself may put your on-call team at risk of missing a notification.'}
- ${false} | ${`User ${userName} is currently part of:`} | ${'Removing this user may put their on-call team at risk of missing a notification.'}
- `('when current user ', ({ isCurrentUser, titleText, footerText }) => {
- it(`${isCurrentUser ? 'is' : 'is not'} a part of on-call schedule`, async () => {
- createComponent({
- isCurrentUser,
- });
-
- expect(findTitle().text()).toBe(titleText);
- expect(findFooter().text()).toBe(footerText);
- });
- });
-
- describe.each(mockSchedules)(
- 'renders each on-call schedule data',
- ({ name, scheduleUrl, projectName, projectUrl }) => {
- beforeEach(() => {
- createComponent({ schedules: [{ name, scheduleUrl, projectName, projectUrl }] });
- });
-
- it(`renders schedule ${name}'s name and link`, () => {
- const msg = findSchedules().text();
-
- expect(msg).toContain(`On-call schedule ${name}`);
- expect(findLinks().at(0).attributes('href')).toBe(scheduleUrl);
- });
-
- it(`renders project ${projectName}'s name and link`, () => {
- const msg = findSchedules().text();
-
- expect(msg).toContain(`in Project ${projectName}`);
- expect(findLinks().at(1).attributes('href')).toBe(projectUrl);
- });
- },
- );
-});
diff --git a/spec/frontend/vue_shared/security_reports/mock_data.js b/spec/frontend/vue_shared/security_reports/mock_data.js
index 06631710509..cdaeec78e47 100644
--- a/spec/frontend/vue_shared/security_reports/mock_data.js
+++ b/spec/frontend/vue_shared/security_reports/mock_data.js
@@ -314,7 +314,7 @@ export const sastDiffSuccessMock = {
head_report_created_at: '2020-01-10T10:00:00.000Z',
};
-export const secretScanningDiffSuccessMock = {
+export const secretDetectionDiffSuccessMock = {
added: [mockFindings[0], mockFindings[1]],
fixed: [mockFindings[2]],
base_report_created_at: '2020-01-01T10:00:00.000Z',
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 4d579fa61df..68a97103d3a 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
@@ -12,7 +12,7 @@ import {
securityReportMergeRequestDownloadPathsQueryNoArtifactsResponse,
securityReportMergeRequestDownloadPathsQueryResponse,
sastDiffSuccessMock,
- secretScanningDiffSuccessMock,
+ secretDetectionDiffSuccessMock,
} from 'jest/vue_shared/security_reports/mock_data';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
@@ -31,7 +31,7 @@ Vue.use(VueApollo);
Vue.use(Vuex);
const SAST_COMPARISON_PATH = '/sast.json';
-const SECRET_SCANNING_COMPARISON_PATH = '/secret_detection.json';
+const SECRET_DETECTION_COMPARISON_PATH = '/secret_detection.json';
describe('Security reports app', () => {
let wrapper;
@@ -175,12 +175,12 @@ describe('Security reports app', () => {
const SAST_SUCCESS_MESSAGE =
'Security scanning detected 1 potential vulnerability 1 Critical 0 High and 0 Others';
- const SECRET_SCANNING_SUCCESS_MESSAGE =
+ const SECRET_DETECTION_SUCCESS_MESSAGE =
'Security scanning detected 2 potential vulnerabilities 1 Critical 1 High and 0 Others';
describe.each`
- reportType | pathProp | path | successResponse | successMessage
- ${REPORT_TYPE_SAST} | ${'sastComparisonPath'} | ${SAST_COMPARISON_PATH} | ${sastDiffSuccessMock} | ${SAST_SUCCESS_MESSAGE}
- ${REPORT_TYPE_SECRET_DETECTION} | ${'secretScanningComparisonPath'} | ${SECRET_SCANNING_COMPARISON_PATH} | ${secretScanningDiffSuccessMock} | ${SECRET_SCANNING_SUCCESS_MESSAGE}
+ reportType | pathProp | path | successResponse | successMessage
+ ${REPORT_TYPE_SAST} | ${'sastComparisonPath'} | ${SAST_COMPARISON_PATH} | ${sastDiffSuccessMock} | ${SAST_SUCCESS_MESSAGE}
+ ${REPORT_TYPE_SECRET_DETECTION} | ${'secretDetectionComparisonPath'} | ${SECRET_DETECTION_COMPARISON_PATH} | ${secretDetectionDiffSuccessMock} | ${SECRET_DETECTION_SUCCESS_MESSAGE}
`(
'given a $pathProp and $reportType artifact',
({ pathProp, path, successResponse, successMessage }) => {
diff --git a/spec/frontend/vue_shared/security_reports/store/getters_spec.js b/spec/frontend/vue_shared/security_reports/store/getters_spec.js
index 97746c7c38b..bcc8955ba02 100644
--- a/spec/frontend/vue_shared/security_reports/store/getters_spec.js
+++ b/spec/frontend/vue_shared/security_reports/store/getters_spec.js
@@ -8,7 +8,7 @@ import {
summaryCounts,
} from '~/vue_shared/security_reports/store/getters';
import createSastState from '~/vue_shared/security_reports/store/modules/sast/state';
-import createSecretScanningState from '~/vue_shared/security_reports/store/modules/secret_detection/state';
+import createSecretDetectionState from '~/vue_shared/security_reports/store/modules/secret_detection/state';
import createState from '~/vue_shared/security_reports/store/state';
import { groupedTextBuilder } from '~/vue_shared/security_reports/store/utils';
import { CRITICAL, HIGH, LOW } from '~/vulnerabilities/constants';
@@ -21,7 +21,7 @@ describe('Security reports getters', () => {
beforeEach(() => {
state = createState();
state.sast = createSastState();
- state.secretDetection = createSecretScanningState();
+ state.secretDetection = createSecretDetectionState();
});
describe('summaryCounts', () => {