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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/vue_shared/components')
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap191
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap2
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap4
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap4
-rw-r--r--spec/frontend/vue_shared/components/awards_list_spec.js36
-rw-r--r--spec/frontend/vue_shared/components/callout_spec.js63
-rw-r--r--spec/frontend/vue_shared/components/clipboard_button_spec.js42
-rw-r--r--spec/frontend/vue_shared/components/color_picker/color_picker_spec.js140
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js39
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js24
-rw-r--r--spec/frontend/vue_shared/components/gfm_autocomplete/__snapshots__/utils_spec.js.snap47
-rw-r--r--spec/frontend/vue_shared/components/gfm_autocomplete/gfm_autocomplete_spec.js (renamed from spec/frontend/vue_shared/components/gl_mentions_spec.js)10
-rw-r--r--spec/frontend/vue_shared/components/gfm_autocomplete/utils_spec.js344
-rw-r--r--spec/frontend/vue_shared/components/issue/issue_milestone_spec.js29
-rw-r--r--spec/frontend/vue_shared/components/loading_button_spec.js100
-rw-r--r--spec/frontend/vue_shared/components/markdown/apply_suggestion_spec.js72
-rw-r--r--spec/frontend/vue_shared/components/members/action_buttons/access_request_action_buttons_spec.js108
-rw-r--r--spec/frontend/vue_shared/components/members/action_buttons/approve_access_request_button_spec.js74
-rw-r--r--spec/frontend/vue_shared/components/members/action_buttons/invite_action_buttons_spec.js85
-rw-r--r--spec/frontend/vue_shared/components/members/action_buttons/leave_button_spec.js59
-rw-r--r--spec/frontend/vue_shared/components/members/action_buttons/remove_group_link_button_spec.js64
-rw-r--r--spec/frontend/vue_shared/components/members/action_buttons/remove_member_button_spec.js66
-rw-r--r--spec/frontend/vue_shared/components/members/action_buttons/resend_invite_button_spec.js66
-rw-r--r--spec/frontend/vue_shared/components/members/action_buttons/user_action_buttons_spec.js89
-rw-r--r--spec/frontend/vue_shared/components/members/avatars/group_avatar_spec.js46
-rw-r--r--spec/frontend/vue_shared/components/members/avatars/invite_avatar_spec.js38
-rw-r--r--spec/frontend/vue_shared/components/members/avatars/user_avatar_spec.js115
-rw-r--r--spec/frontend/vue_shared/components/members/mock_data.js71
-rw-r--r--spec/frontend/vue_shared/components/members/modals/leave_modal_spec.js91
-rw-r--r--spec/frontend/vue_shared/components/members/modals/remove_group_link_modal_spec.js106
-rw-r--r--spec/frontend/vue_shared/components/members/table/created_at_spec.js61
-rw-r--r--spec/frontend/vue_shared/components/members/table/expiration_datepicker_spec.js166
-rw-r--r--spec/frontend/vue_shared/components/members/table/expires_at_spec.js86
-rw-r--r--spec/frontend/vue_shared/components/members/table/member_action_buttons_spec.js43
-rw-r--r--spec/frontend/vue_shared/components/members/table/member_avatar_spec.js39
-rw-r--r--spec/frontend/vue_shared/components/members/table/member_source_spec.js71
-rw-r--r--spec/frontend/vue_shared/components/members/table/member_table_cell_spec.js251
-rw-r--r--spec/frontend/vue_shared/components/members/table/members_table_spec.js212
-rw-r--r--spec/frontend/vue_shared/components/members/table/role_dropdown_spec.js151
-rw-r--r--spec/frontend/vue_shared/components/members/utils_spec.js122
-rw-r--r--spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap1
-rw-r--r--spec/frontend/vue_shared/components/resizable_chart/__snapshots__/skeleton_loader_spec.js.snap6
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js13
-rw-r--r--spec/frontend/vue_shared/components/security_reports/__snapshots__/security_summary_spec.js.snap144
-rw-r--r--spec/frontend/vue_shared/components/security_reports/help_icon_spec.js68
-rw-r--r--spec/frontend/vue_shared/components/security_reports/security_summary_spec.js38
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js12
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js104
-rw-r--r--spec/frontend/vue_shared/components/toggle_button_spec.js107
-rw-r--r--spec/frontend/vue_shared/components/tooltip_on_truncate_spec.js239
50 files changed, 1524 insertions, 2635 deletions
diff --git a/spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap
index 04ae2a0f34d..20ea897e29c 100644
--- a/spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap
+++ b/spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap
@@ -5,12 +5,17 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
class="awards js-awards-block"
>
<button
- class="btn award-control"
+ class="btn gl-mr-3 btn-default btn-md gl-button"
data-testid="award-button"
title="Ada, Leonardo, and Marie"
type="button"
>
+ <!---->
+
+ <!---->
+
<span
+ class="award-emoji-block"
data-testid="award-html"
>
@@ -23,18 +28,28 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
</span>
<span
- class="award-control-text js-counter"
+ class="gl-button-text"
>
- 3
+
+ <span
+ class="js-counter"
+ >
+ 3
+ </span>
</span>
</button>
<button
- class="btn award-control active"
+ class="btn gl-mr-3 btn-default btn-md gl-button selected"
data-testid="award-button"
title="You, Ada, and Marie"
type="button"
>
+ <!---->
+
+ <!---->
+
<span
+ class="award-emoji-block"
data-testid="award-html"
>
@@ -47,18 +62,28 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
</span>
<span
- class="award-control-text js-counter"
+ class="gl-button-text"
>
- 3
+
+ <span
+ class="js-counter"
+ >
+ 3
+ </span>
</span>
</button>
<button
- class="btn award-control"
+ class="btn gl-mr-3 btn-default btn-md gl-button"
data-testid="award-button"
title="Ada and Jane"
type="button"
>
+ <!---->
+
+ <!---->
+
<span
+ class="award-emoji-block"
data-testid="award-html"
>
@@ -71,18 +96,28 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
</span>
<span
- class="award-control-text js-counter"
+ class="gl-button-text"
>
- 2
+
+ <span
+ class="js-counter"
+ >
+ 2
+ </span>
</span>
</button>
<button
- class="btn award-control active"
+ class="btn gl-mr-3 btn-default btn-md gl-button selected"
data-testid="award-button"
title="You, Ada, Jane, and Leonardo"
type="button"
>
+ <!---->
+
+ <!---->
+
<span
+ class="award-emoji-block"
data-testid="award-html"
>
@@ -95,18 +130,28 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
</span>
<span
- class="award-control-text js-counter"
+ class="gl-button-text"
>
- 4
+
+ <span
+ class="js-counter"
+ >
+ 4
+ </span>
</span>
</button>
<button
- class="btn award-control active"
+ class="btn gl-mr-3 btn-default btn-md gl-button selected"
data-testid="award-button"
title="You"
type="button"
>
+ <!---->
+
+ <!---->
+
<span
+ class="award-emoji-block"
data-testid="award-html"
>
@@ -119,18 +164,28 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
</span>
<span
- class="award-control-text js-counter"
+ class="gl-button-text"
>
- 1
+
+ <span
+ class="js-counter"
+ >
+ 1
+ </span>
</span>
</button>
<button
- class="btn award-control"
+ class="btn gl-mr-3 btn-default btn-md gl-button"
data-testid="award-button"
title="Marie"
type="button"
>
+ <!---->
+
+ <!---->
+
<span
+ class="award-emoji-block"
data-testid="award-html"
>
@@ -143,18 +198,28 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
</span>
<span
- class="award-control-text js-counter"
+ class="gl-button-text"
>
- 1
+
+ <span
+ class="js-counter"
+ >
+ 1
+ </span>
</span>
</button>
<button
- class="btn award-control active"
+ class="btn gl-mr-3 btn-default btn-md gl-button selected"
data-testid="award-button"
title="You"
type="button"
>
+ <!---->
+
+ <!---->
+
<span
+ class="award-emoji-block"
data-testid="award-html"
>
@@ -167,9 +232,14 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
</span>
<span
- class="award-control-text js-counter"
+ class="gl-button-text"
>
- 1
+
+ <span
+ class="js-counter"
+ >
+ 1
+ </span>
</span>
</button>
@@ -178,46 +248,59 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
>
<button
aria-label="Add reaction"
- class="award-control btn js-add-award js-test-add-button-class"
+ class="btn add-reaction-button js-add-award btn-default btn-md gl-button js-test-add-button-class"
title="Add reaction"
type="button"
>
- <span
- class="award-control-icon award-control-icon-neutral"
- >
- <gl-icon-stub
- aria-hidden="true"
- name="slight-smile"
- size="16"
- />
- </span>
+ <!---->
+ <!---->
+
<span
- class="award-control-icon award-control-icon-positive"
+ class="gl-button-text"
>
- <gl-icon-stub
- aria-hidden="true"
- name="smiley"
- size="16"
- />
+ <span
+ class="reaction-control-icon reaction-control-icon-neutral"
+ >
+ <svg
+ aria-hidden="true"
+ class="gl-icon s16"
+ data-testid="slight-smile-icon"
+ >
+ <use
+ href="#slight-smile"
+ />
+ </svg>
+ </span>
+
+ <span
+ class="reaction-control-icon reaction-control-icon-positive"
+ >
+ <svg
+ aria-hidden="true"
+ class="gl-icon s16"
+ data-testid="smiley-icon"
+ >
+ <use
+ href="#smiley"
+ />
+ </svg>
+ </span>
+
+ <span
+ class="reaction-control-icon reaction-control-icon-super-positive"
+ >
+ <svg
+ aria-hidden="true"
+ class="gl-icon s16"
+ data-testid="smile-icon"
+ >
+ <use
+ href="#smile"
+ />
+ </svg>
+ </span>
</span>
-
- <span
- class="award-control-icon award-control-icon-super-positive"
- >
- <gl-icon-stub
- aria-hidden="true"
- name="smiley"
- size="16"
- />
- </span>
-
- <gl-loading-icon-stub
- class="award-control-icon-loading"
- color="dark"
- label="Loading"
- size="md"
- />
</button>
</div>
</div>
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 ec4a81054db..63d38e7587a 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,7 +4,7 @@ exports[`Clone Dropdown Button rendering matches the snapshot 1`] = `
<gl-dropdown-stub
category="primary"
headertext=""
- right=""
+ right="true"
size="medium"
text="Clone"
variant="info"
diff --git a/spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap
index 19a649089e0..adb6c935f96 100644
--- a/spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap
+++ b/spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap
@@ -11,6 +11,7 @@ exports[`Expand button on click when short text is provided renders button after
<!---->
<svg
+ aria-hidden="true"
class="gl-button-icon gl-icon s16"
data-testid="ellipsis_h-icon"
>
@@ -39,6 +40,7 @@ exports[`Expand button on click when short text is provided renders button after
<!---->
<svg
+ aria-hidden="true"
class="gl-button-icon gl-icon s16"
data-testid="ellipsis_h-icon"
>
@@ -62,6 +64,7 @@ exports[`Expand button when short text is provided renders button before text 1`
<!---->
<svg
+ aria-hidden="true"
class="gl-button-icon gl-icon s16"
data-testid="ellipsis_h-icon"
>
@@ -90,6 +93,7 @@ exports[`Expand button when short text is provided renders button before text 1`
<!---->
<svg
+ aria-hidden="true"
class="gl-button-icon gl-icon s16"
data-testid="ellipsis_h-icon"
>
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 8eb0e8f9550..dd88ba9a6fb 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
@@ -2,7 +2,7 @@
exports[`SplitButton renders actionItems 1`] = `
<gl-dropdown-stub
- category="tertiary"
+ category="primary"
headertext=""
menu-class=""
size="medium"
@@ -14,6 +14,7 @@ exports[`SplitButton renders actionItems 1`] = `
avatarurl=""
iconcolor=""
iconname=""
+ iconrightarialabel=""
iconrightname=""
ischecked="true"
ischeckitem="true"
@@ -33,6 +34,7 @@ exports[`SplitButton renders actionItems 1`] = `
avatarurl=""
iconcolor=""
iconname=""
+ iconrightarialabel=""
iconrightname=""
ischeckitem="true"
secondarytext=""
diff --git a/spec/frontend/vue_shared/components/awards_list_spec.js b/spec/frontend/vue_shared/components/awards_list_spec.js
index 63fc8a5749d..d20de81c446 100644
--- a/spec/frontend/vue_shared/components/awards_list_spec.js
+++ b/spec/frontend/vue_shared/components/awards_list_spec.js
@@ -1,4 +1,4 @@
-import { shallowMount } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
import AwardsList from '~/vue_shared/components/awards_list.vue';
const createUser = (id, name) => ({ id, name });
@@ -41,6 +41,8 @@ const TEST_AWARDS = [
];
const TEST_ADD_BUTTON_CLASS = 'js-test-add-button-class';
+const REACTION_CONTROL_CLASSES = ['btn', 'gl-mr-3', 'btn-default', 'btn-md', 'gl-button'];
+
describe('vue_shared/components/awards_list', () => {
let wrapper;
@@ -54,16 +56,16 @@ describe('vue_shared/components/awards_list', () => {
throw new Error('There should only be one wrapper created per test');
}
- wrapper = shallowMount(AwardsList, { propsData: props });
+ wrapper = mount(AwardsList, { propsData: props });
};
const matchingEmojiTag = name => expect.stringMatching(`gl-emoji data-name="${name}"`);
- const findAwardButtons = () => wrapper.findAll('[data-testid="award-button"');
+ const findAwardButtons = () => wrapper.findAll('[data-testid="award-button"]');
const findAwardsData = () =>
findAwardButtons().wrappers.map(x => {
return {
classes: x.classes(),
title: x.attributes('title'),
- html: x.find('[data-testid="award-html"]').element.innerHTML,
+ html: x.find('[data-testid="award-html"]').html(),
count: Number(x.find('.js-counter').text()),
};
});
@@ -86,43 +88,43 @@ describe('vue_shared/components/awards_list', () => {
it('shows awards in correct order', () => {
expect(findAwardsData()).toEqual([
{
- classes: ['btn', 'award-control'],
+ classes: REACTION_CONTROL_CLASSES,
count: 3,
html: matchingEmojiTag(EMOJI_THUMBSUP),
title: 'Ada, Leonardo, and Marie',
},
{
- classes: ['btn', 'award-control', 'active'],
+ classes: [...REACTION_CONTROL_CLASSES, 'selected'],
count: 3,
html: matchingEmojiTag(EMOJI_THUMBSDOWN),
title: 'You, Ada, and Marie',
},
{
- classes: ['btn', 'award-control'],
+ classes: REACTION_CONTROL_CLASSES,
count: 2,
html: matchingEmojiTag(EMOJI_SMILE),
title: 'Ada and Jane',
},
{
- classes: ['btn', 'award-control', 'active'],
+ classes: [...REACTION_CONTROL_CLASSES, 'selected'],
count: 4,
html: matchingEmojiTag(EMOJI_OK),
title: 'You, Ada, Jane, and Leonardo',
},
{
- classes: ['btn', 'award-control', 'active'],
+ classes: [...REACTION_CONTROL_CLASSES, 'selected'],
count: 1,
html: matchingEmojiTag(EMOJI_CACTUS),
title: 'You',
},
{
- classes: ['btn', 'award-control'],
+ classes: REACTION_CONTROL_CLASSES,
count: 1,
html: matchingEmojiTag(EMOJI_A),
title: 'Marie',
},
{
- classes: ['btn', 'award-control', 'active'],
+ classes: [...REACTION_CONTROL_CLASSES, 'selected'],
count: 1,
html: matchingEmojiTag(EMOJI_B),
title: 'You',
@@ -135,7 +137,7 @@ describe('vue_shared/components/awards_list', () => {
findAwardButtons()
.at(2)
- .trigger('click');
+ .vm.$emit('click');
expect(wrapper.emitted().award).toEqual([[EMOJI_SMILE]]);
});
@@ -162,7 +164,7 @@ describe('vue_shared/components/awards_list', () => {
findAwardButtons()
.at(0)
- .trigger('click');
+ .vm.$emit('click');
expect(wrapper.emitted().award).toEqual([[Number(EMOJI_100)]]);
});
@@ -225,26 +227,26 @@ describe('vue_shared/components/awards_list', () => {
it('shows awards in correct order', () => {
expect(findAwardsData()).toEqual([
{
- classes: ['btn', 'award-control'],
+ classes: REACTION_CONTROL_CLASSES,
count: 0,
html: matchingEmojiTag(EMOJI_THUMBSUP),
title: '',
},
{
- classes: ['btn', 'award-control'],
+ classes: REACTION_CONTROL_CLASSES,
count: 0,
html: matchingEmojiTag(EMOJI_THUMBSDOWN),
title: '',
},
// We expect the EMOJI_100 before the EMOJI_SMILE because it was given as a defaultAward
{
- classes: ['btn', 'award-control'],
+ classes: REACTION_CONTROL_CLASSES,
count: 1,
html: matchingEmojiTag(EMOJI_100),
title: 'Marie',
},
{
- classes: ['btn', 'award-control'],
+ classes: REACTION_CONTROL_CLASSES,
count: 1,
html: matchingEmojiTag(EMOJI_SMILE),
title: 'Marie',
diff --git a/spec/frontend/vue_shared/components/callout_spec.js b/spec/frontend/vue_shared/components/callout_spec.js
deleted file mode 100644
index 7c9bb6b4650..00000000000
--- a/spec/frontend/vue_shared/components/callout_spec.js
+++ /dev/null
@@ -1,63 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import Callout from '~/vue_shared/components/callout.vue';
-
-const TEST_MESSAGE = 'This is a callout message!';
-const TEST_SLOT = '<button>This is a callout slot!</button>';
-
-describe('Callout Component', () => {
- let wrapper;
-
- const factory = options => {
- wrapper = shallowMount(Callout, {
- ...options,
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('should render the appropriate variant of callout', () => {
- factory({
- propsData: {
- category: 'info',
- message: TEST_MESSAGE,
- },
- });
-
- expect(wrapper.classes()).toEqual(['bs-callout', 'bs-callout-info']);
-
- expect(wrapper.element.tagName).toEqual('DIV');
- });
-
- it('should render accessibility attributes', () => {
- factory({
- propsData: {
- message: TEST_MESSAGE,
- },
- });
-
- expect(wrapper.attributes('role')).toEqual('alert');
- expect(wrapper.attributes('aria-live')).toEqual('assertive');
- });
-
- it('should render the provided message', () => {
- factory({
- propsData: {
- message: TEST_MESSAGE,
- },
- });
-
- expect(wrapper.element.innerHTML.trim()).toEqual(TEST_MESSAGE);
- });
-
- it('should render the provided slot', () => {
- factory({
- slots: {
- default: TEST_SLOT,
- },
- });
-
- expect(wrapper.element.innerHTML.trim()).toEqual(TEST_SLOT);
- });
-});
diff --git a/spec/frontend/vue_shared/components/clipboard_button_spec.js b/spec/frontend/vue_shared/components/clipboard_button_spec.js
index 51a2653befc..ac0be1537b7 100644
--- a/spec/frontend/vue_shared/components/clipboard_button_spec.js
+++ b/spec/frontend/vue_shared/components/clipboard_button_spec.js
@@ -1,16 +1,19 @@
-import { shallowMount } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
describe('clipboard button', () => {
let wrapper;
- const createWrapper = propsData => {
- wrapper = shallowMount(ClipboardButton, {
+ const createWrapper = (propsData, options = {}) => {
+ wrapper = mount(ClipboardButton, {
propsData,
+ ...options,
});
};
+ const findButton = () => wrapper.find(GlButton);
+
afterEach(() => {
wrapper.destroy();
wrapper = null;
@@ -26,7 +29,7 @@ describe('clipboard button', () => {
});
it('renders a button for clipboard', () => {
- expect(wrapper.find(GlButton).exists()).toBe(true);
+ expect(findButton().exists()).toBe(true);
expect(wrapper.attributes('data-clipboard-text')).toBe('copy me');
});
@@ -53,4 +56,35 @@ describe('clipboard button', () => {
);
});
});
+
+ it('renders default slot as button text', () => {
+ createWrapper(
+ {
+ text: 'copy me',
+ title: 'Copy this value',
+ },
+ {
+ slots: {
+ default: 'Foo bar',
+ },
+ },
+ );
+
+ expect(findButton().text()).toBe('Foo bar');
+ });
+
+ it('re-emits button events', () => {
+ const onClick = jest.fn();
+ createWrapper(
+ {
+ text: 'copy me',
+ title: 'Copy this value',
+ },
+ { listeners: { click: onClick } },
+ );
+
+ findButton().trigger('click');
+
+ expect(onClick).toHaveBeenCalled();
+ });
});
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
new file mode 100644
index 00000000000..a50a4b742b3
--- /dev/null
+++ b/spec/frontend/vue_shared/components/color_picker/color_picker_spec.js
@@ -0,0 +1,140 @@
+import { GlFormGroup, GlFormInput, GlFormInputGroup, GlLink } from '@gitlab/ui';
+import { mount, shallowMount } from '@vue/test-utils';
+
+import ColorPicker from '~/vue_shared/components/color_picker/color_picker.vue';
+
+describe('ColorPicker', () => {
+ let wrapper;
+
+ const createComponent = (fn = mount, propsData = {}) => {
+ wrapper = fn(ColorPicker, {
+ propsData,
+ });
+ };
+
+ const setColor = '#000000';
+ const label = () => wrapper.find(GlFormGroup).attributes('label');
+ const colorPreview = () => wrapper.find('[data-testid="color-preview"]');
+ const colorPicker = () => wrapper.find(GlFormInput);
+ const colorInput = () => wrapper.find(GlFormInputGroup).find('input[type="text"]');
+ const invalidFeedback = () => wrapper.find('.invalid-feedback');
+ const description = () => wrapper.find(GlFormGroup).attributes('description');
+ const presetColors = () => wrapper.findAll(GlLink);
+
+ beforeEach(() => {
+ gon.suggested_label_colors = {
+ [setColor]: 'Black',
+ '#0033CC': 'UA blue',
+ '#428BCA': 'Moderate blue',
+ '#44AD8E': 'Lime green',
+ };
+
+ createComponent(shallowMount);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('label', () => {
+ it('hides the label if the label is not passed', () => {
+ expect(label()).toBe('');
+ });
+
+ it('shows the label if the label is passed', () => {
+ createComponent(shallowMount, { label: 'test' });
+
+ expect(label()).toBe('test');
+ });
+ });
+
+ describe('behavior', () => {
+ it('by default has no values', () => {
+ createComponent();
+
+ expect(colorPreview().attributes('style')).toBe(undefined);
+ expect(colorPicker().attributes('value')).toBe(undefined);
+ expect(colorInput().props('value')).toBe('');
+ });
+
+ it('has a color set on initialization', () => {
+ createComponent(shallowMount, { setColor });
+
+ expect(wrapper.vm.$data.selectedColor).toBe(setColor);
+ });
+
+ it('emits input event from component when a color is selected', async () => {
+ createComponent();
+ await colorInput().setValue(setColor);
+
+ expect(wrapper.emitted().input[0]).toEqual([setColor]);
+ });
+
+ it('trims spaces from submitted colors', async () => {
+ createComponent();
+ await colorInput().setValue(` ${setColor} `);
+
+ expect(wrapper.vm.$data.selectedColor).toBe(setColor);
+ });
+
+ it('shows invalid feedback when an invalid color is used', async () => {
+ createComponent();
+ await colorInput().setValue('abcd');
+
+ expect(invalidFeedback().text()).toBe(
+ 'Please enter a valid hex (#RRGGBB or #RGB) color value',
+ );
+ expect(wrapper.emitted().input).toBe(undefined);
+ });
+
+ it('shows an invalid feedback border on the preview when an invalid color is used', async () => {
+ createComponent();
+ await colorInput().setValue('abcd');
+
+ expect(colorPreview().attributes('class')).toContain('gl-inset-border-1-red-500');
+ });
+ });
+
+ describe('inputs', () => {
+ it('has color input value entered', async () => {
+ createComponent();
+ await colorInput().setValue(setColor);
+
+ expect(wrapper.vm.$data.selectedColor).toBe(setColor);
+ });
+
+ it('has color picker value entered', async () => {
+ createComponent();
+ await colorPicker().setValue(setColor);
+
+ expect(wrapper.vm.$data.selectedColor).toBe(setColor);
+ });
+ });
+
+ describe('preset colors', () => {
+ it('hides the suggested colors if they are empty', () => {
+ gon.suggested_label_colors = {};
+ createComponent(shallowMount);
+
+ expect(description()).toBe('Choose 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(presetColors()).toHaveLength(4);
+ });
+
+ it('has preset color selected', async () => {
+ createComponent();
+ await presetColors()
+ .at(0)
+ .trigger('click');
+
+ expect(wrapper.vm.$data.selectedColor).toBe(setColor);
+ });
+ });
+});
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 64bfff3dfa1..8cc5d6775a7 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
@@ -17,11 +17,14 @@ import RecentSearchesService from '~/filtered_search/services/recent_searches_se
import {
mockAvailableTokens,
+ mockMembershipToken,
+ mockMembershipTokenOptionsWithoutTitles,
mockSortOptions,
mockHistoryItems,
tokenValueAuthor,
tokenValueLabel,
tokenValueMilestone,
+ tokenValueMembership,
} from './mock_data';
jest.mock('~/vue_shared/components/filtered_search_bar/filtered_search_utils', () => ({
@@ -412,6 +415,42 @@ describe('FilteredSearchBarRoot', () => {
wrapperFullMount.destroy();
});
+ describe('when token options have `title` attribute defined', () => {
+ it('renders search history items using the provided `title` attribute', async () => {
+ const wrapperFullMount = createComponent({
+ sortOptions: mockSortOptions,
+ tokens: [mockMembershipToken],
+ shallow: false,
+ });
+
+ wrapperFullMount.vm.recentSearchesStore.addRecentSearch([tokenValueMembership]);
+
+ await wrapperFullMount.vm.$nextTick();
+
+ expect(wrapperFullMount.find(GlDropdownItem).text()).toBe('Membership := Direct');
+
+ wrapperFullMount.destroy();
+ });
+ });
+
+ describe('when token options have do not have `title` attribute defined', () => {
+ it('renders search history items using the provided `value` attribute', async () => {
+ const wrapperFullMount = createComponent({
+ sortOptions: mockSortOptions,
+ tokens: [mockMembershipTokenOptionsWithoutTitles],
+ shallow: false,
+ });
+
+ wrapperFullMount.vm.recentSearchesStore.addRecentSearch([tokenValueMembership]);
+
+ await wrapperFullMount.vm.$nextTick();
+
+ expect(wrapperFullMount.find(GlDropdownItem).text()).toBe('Membership := exclude');
+
+ wrapperFullMount.destroy();
+ });
+ });
+
it('renders sort dropdown component', () => {
expect(wrapper.find(GlButtonGroup).exists()).toBe(true);
expect(wrapper.find(GlDropdown).exists()).toBe(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 e0a3208cac9..64fbe70696d 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js
@@ -1,3 +1,4 @@
+import { GlFilteredSearchToken } from '@gitlab/ui';
import { mockLabels } from 'jest/vue_shared/components/sidebar/labels_select_vue/mock_data';
import Api from '~/api';
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
@@ -102,6 +103,21 @@ export const mockMilestoneToken = {
fetchMilestones: () => Promise.resolve({ data: mockMilestones }),
};
+export const mockMembershipToken = {
+ type: 'with_inherited_permissions',
+ icon: 'group',
+ title: 'Membership',
+ token: GlFilteredSearchToken,
+ unique: true,
+ operators: [{ value: '=', description: 'is' }],
+ options: [{ value: 'exclude', title: 'Direct' }, { value: 'only', title: 'Inherited' }],
+};
+
+export const mockMembershipTokenOptionsWithoutTitles = {
+ ...mockMembershipToken,
+ options: [{ value: 'exclude' }, { value: 'only' }],
+};
+
export const mockAvailableTokens = [mockAuthorToken, mockLabelToken, mockMilestoneToken];
export const tokenValueAuthor = {
@@ -128,6 +144,14 @@ export const tokenValueMilestone = {
},
};
+export const tokenValueMembership = {
+ type: 'with_inherited_permissions',
+ value: {
+ operator: '=',
+ data: 'exclude',
+ },
+};
+
export const tokenValuePlain = {
type: 'filtered-search-term',
value: { data: 'foo' },
diff --git a/spec/frontend/vue_shared/components/gfm_autocomplete/__snapshots__/utils_spec.js.snap b/spec/frontend/vue_shared/components/gfm_autocomplete/__snapshots__/utils_spec.js.snap
new file mode 100644
index 00000000000..d0fa2086fdc
--- /dev/null
+++ b/spec/frontend/vue_shared/components/gfm_autocomplete/__snapshots__/utils_spec.js.snap
@@ -0,0 +1,47 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`gfm_autocomplete/utils issues config shows the iid and title in the menu item within a project context 1`] = `"<small>123456</small> Project context issue title &lt;script&gt;alert(&#39;hi&#39;)&lt;/script&gt;"`;
+
+exports[`gfm_autocomplete/utils issues config shows the reference and title in the menu item within a group context 1`] = `"<small>gitlab#987654</small> Group context issue title &lt;script&gt;alert(&#39;hi&#39;)&lt;/script&gt;"`;
+
+exports[`gfm_autocomplete/utils labels config shows the title in the menu item 1`] = `
+"
+ <span class=\\"dropdown-label-box\\" style=\\"background: #123456;\\"></span>
+ bug &lt;script&gt;alert(&#39;hi&#39;)&lt;/script&gt;"
+`;
+
+exports[`gfm_autocomplete/utils members config shows an avatar character, name, parent name, and count in the menu item for a group 1`] = `
+"
+ <div class=\\"gl-display-flex gl-align-items-center\\">
+ <div class=\\"gl-avatar gl-avatar-s24 gl-flex-shrink-0 gl-rounded-small
+ gl-display-flex gl-align-items-center gl-justify-content-center\\" aria-hidden=\\"true\\">
+ G</div>
+ <div class=\\"gl-font-sm gl-line-height-normal gl-ml-3\\">
+ <div>1-1s &lt;script&gt;alert(&#39;hi&#39;)&lt;/script&gt; (2)</div>
+ <div class=\\"gl-text-gray-700\\">GitLab Support Team</div>
+ </div>
+
+ </div>
+ "
+`;
+
+exports[`gfm_autocomplete/utils members config shows the avatar, name and username in the menu item for a user 1`] = `
+"
+ <div class=\\"gl-display-flex gl-align-items-center\\">
+ <img class=\\"gl-avatar gl-avatar-s24 gl-flex-shrink-0 gl-avatar-circle\\" src=\\"/uploads/-/system/user/avatar/123456/avatar.png\\" alt=\\"\\" />
+ <div class=\\"gl-font-sm gl-line-height-normal gl-ml-3\\">
+ <div>My Name &lt;script&gt;alert(&#39;hi&#39;)&lt;/script&gt;</div>
+ <div class=\\"gl-text-gray-700\\">@myusername</div>
+ </div>
+
+ </div>
+ "
+`;
+
+exports[`gfm_autocomplete/utils merge requests config shows the iid and title in the menu item within a project context 1`] = `"<small>123456</small> Project context merge request title &lt;script&gt;alert(&#39;hi&#39;)&lt;/script&gt;"`;
+
+exports[`gfm_autocomplete/utils merge requests config shows the reference and title in the menu item within a group context 1`] = `"<small>gitlab!456789</small> Group context merge request title &lt;script&gt;alert(&#39;hi&#39;)&lt;/script&gt;"`;
+
+exports[`gfm_autocomplete/utils milestones config shows the title in the menu item 1`] = `"13.2 &lt;script&gt;alert(&#39;hi&#39;)&lt;/script&gt;"`;
+
+exports[`gfm_autocomplete/utils snippets config shows the id and title in the menu item 1`] = `"<small>123456</small> Snippet title &lt;script&gt;alert(&#39;hi&#39;)&lt;/script&gt;"`;
diff --git a/spec/frontend/vue_shared/components/gl_mentions_spec.js b/spec/frontend/vue_shared/components/gfm_autocomplete/gfm_autocomplete_spec.js
index 32fc055a77d..b4002fdf4ec 100644
--- a/spec/frontend/vue_shared/components/gl_mentions_spec.js
+++ b/spec/frontend/vue_shared/components/gfm_autocomplete/gfm_autocomplete_spec.js
@@ -1,15 +1,15 @@
+import Tribute from '@gitlab/tributejs';
import { shallowMount } from '@vue/test-utils';
-import Tribute from 'tributejs';
-import GlMentions from '~/vue_shared/components/gl_mentions.vue';
+import GfmAutocomplete from '~/vue_shared/components/gfm_autocomplete/gfm_autocomplete.vue';
-describe('GlMentions', () => {
+describe('GfmAutocomplete', () => {
let wrapper;
- describe('Tribute', () => {
+ describe('tribute', () => {
const mentions = '/gitlab-org/gitlab-test/-/autocomplete_sources/members?type=Issue&type_id=1';
beforeEach(() => {
- wrapper = shallowMount(GlMentions, {
+ wrapper = shallowMount(GfmAutocomplete, {
propsData: {
dataSources: {
mentions,
diff --git a/spec/frontend/vue_shared/components/gfm_autocomplete/utils_spec.js b/spec/frontend/vue_shared/components/gfm_autocomplete/utils_spec.js
new file mode 100644
index 00000000000..647f8c6e000
--- /dev/null
+++ b/spec/frontend/vue_shared/components/gfm_autocomplete/utils_spec.js
@@ -0,0 +1,344 @@
+import { escape, last } from 'lodash';
+import { GfmAutocompleteType, tributeConfig } from '~/vue_shared/components/gfm_autocomplete/utils';
+
+describe('gfm_autocomplete/utils', () => {
+ describe('issues config', () => {
+ const issuesConfig = tributeConfig[GfmAutocompleteType.Issues].config;
+ const groupContextIssue = {
+ iid: 987654,
+ reference: 'gitlab#987654',
+ title: "Group context issue title <script>alert('hi')</script>",
+ };
+ const projectContextIssue = {
+ id: null,
+ iid: 123456,
+ time_estimate: 0,
+ title: "Project context issue title <script>alert('hi')</script>",
+ };
+
+ it('uses # as the trigger', () => {
+ expect(issuesConfig.trigger).toBe('#');
+ });
+
+ it('searches using both the iid and title', () => {
+ expect(issuesConfig.lookup(projectContextIssue)).toBe(
+ `${projectContextIssue.iid}${projectContextIssue.title}`,
+ );
+ });
+
+ it('shows the reference and title in the menu item within a group context', () => {
+ expect(issuesConfig.menuItemTemplate({ original: groupContextIssue })).toMatchSnapshot();
+ });
+
+ it('shows the iid and title in the menu item within a project context', () => {
+ expect(issuesConfig.menuItemTemplate({ original: projectContextIssue })).toMatchSnapshot();
+ });
+
+ it('inserts the reference on autocomplete selection within a group context', () => {
+ expect(issuesConfig.selectTemplate({ original: groupContextIssue })).toBe(
+ groupContextIssue.reference,
+ );
+ });
+
+ it('inserts the iid on autocomplete selection within a project context', () => {
+ expect(issuesConfig.selectTemplate({ original: projectContextIssue })).toBe(
+ `#${projectContextIssue.iid}`,
+ );
+ });
+ });
+
+ describe('labels config', () => {
+ const labelsConfig = tributeConfig[GfmAutocompleteType.Labels].config;
+ const labelsFilter = tributeConfig[GfmAutocompleteType.Labels].filterValues;
+ const label = {
+ color: '#123456',
+ textColor: '#FFFFFF',
+ title: `bug <script>alert('hi')</script>`,
+ type: 'GroupLabel',
+ };
+ const singleWordLabel = {
+ color: '#456789',
+ textColor: '#DDD',
+ title: `bug`,
+ type: 'GroupLabel',
+ };
+ const numericalLabel = {
+ color: '#abcdef',
+ textColor: '#AAA',
+ title: 123456,
+ type: 'ProjectLabel',
+ };
+
+ it('uses ~ as the trigger', () => {
+ expect(labelsConfig.trigger).toBe('~');
+ });
+
+ it('searches using `title`', () => {
+ expect(labelsConfig.lookup).toBe('title');
+ });
+
+ it('shows the title in the menu item', () => {
+ expect(labelsConfig.menuItemTemplate({ original: label })).toMatchSnapshot();
+ });
+
+ it('inserts the title on autocomplete selection', () => {
+ expect(labelsConfig.selectTemplate({ original: singleWordLabel })).toBe(
+ `~${escape(singleWordLabel.title)}`,
+ );
+ });
+
+ it('inserts the title enclosed with quotes on autocomplete selection when the title is numerical', () => {
+ expect(labelsConfig.selectTemplate({ original: numericalLabel })).toBe(
+ `~"${escape(numericalLabel.title)}"`,
+ );
+ });
+
+ it('inserts the title enclosed with quotes on autocomplete selection when the title contains multiple words', () => {
+ expect(labelsConfig.selectTemplate({ original: label })).toBe(`~"${escape(label.title)}"`);
+ });
+
+ describe('filter', () => {
+ const collection = [label, singleWordLabel, { ...numericalLabel, set: true }];
+
+ describe('/label quick action', () => {
+ describe('when the line starts with `/label`', () => {
+ it('shows labels that are not currently selected', () => {
+ const fullText = '/label ~';
+ const selectionStart = 8;
+
+ expect(labelsFilter({ collection, fullText, selectionStart })).toEqual([
+ collection[0],
+ collection[1],
+ ]);
+ });
+ });
+
+ describe('when the line does not start with `/label`', () => {
+ it('shows all labels', () => {
+ const fullText = '~';
+ const selectionStart = 1;
+
+ expect(labelsFilter({ collection, fullText, selectionStart })).toEqual(collection);
+ });
+ });
+ });
+
+ describe('/unlabel quick action', () => {
+ describe('when the line starts with `/unlabel`', () => {
+ it('shows labels that are currently selected', () => {
+ const fullText = '/unlabel ~';
+ const selectionStart = 10;
+
+ expect(labelsFilter({ collection, fullText, selectionStart })).toEqual([collection[2]]);
+ });
+ });
+
+ describe('when the line does not start with `/unlabel`', () => {
+ it('shows all labels', () => {
+ const fullText = '~';
+ const selectionStart = 1;
+
+ expect(labelsFilter({ collection, fullText, selectionStart })).toEqual(collection);
+ });
+ });
+ });
+ });
+ });
+
+ describe('members config', () => {
+ const membersConfig = tributeConfig[GfmAutocompleteType.Members].config;
+ const membersFilter = tributeConfig[GfmAutocompleteType.Members].filterValues;
+ const userMember = {
+ type: 'User',
+ username: 'myusername',
+ name: "My Name <script>alert('hi')</script>",
+ avatar_url: '/uploads/-/system/user/avatar/123456/avatar.png',
+ availability: null,
+ };
+ const groupMember = {
+ type: 'Group',
+ username: 'gitlab-com/support/1-1s',
+ name: "GitLab.com / GitLab Support Team / 1-1s <script>alert('hi')</script>",
+ avatar_url: null,
+ count: 2,
+ mentionsDisabled: null,
+ };
+
+ it('uses @ as the trigger', () => {
+ expect(membersConfig.trigger).toBe('@');
+ });
+
+ it('inserts the username on autocomplete selection', () => {
+ expect(membersConfig.fillAttr).toBe('username');
+ });
+
+ it('searches using both the name and username for a user', () => {
+ expect(membersConfig.lookup(userMember)).toBe(`${userMember.name}${userMember.username}`);
+ });
+
+ it('searches using only its own name and not its ancestors for a group', () => {
+ expect(membersConfig.lookup(groupMember)).toBe(last(groupMember.name.split(' / ')));
+ });
+
+ it('shows the avatar, name and username in the menu item for a user', () => {
+ expect(membersConfig.menuItemTemplate({ original: userMember })).toMatchSnapshot();
+ });
+
+ it('shows an avatar character, name, parent name, and count in the menu item for a group', () => {
+ expect(membersConfig.menuItemTemplate({ original: groupMember })).toMatchSnapshot();
+ });
+
+ describe('filter', () => {
+ const assignees = [userMember.username];
+ const collection = [userMember, groupMember];
+
+ describe('/assign quick action', () => {
+ describe('when the line starts with `/assign`', () => {
+ it('shows members that are not currently selected', () => {
+ const fullText = '/assign @';
+ const selectionStart = 9;
+
+ expect(membersFilter({ assignees, collection, fullText, selectionStart })).toEqual([
+ collection[1],
+ ]);
+ });
+ });
+
+ describe('when the line does not start with `/assign`', () => {
+ it('shows all labels', () => {
+ const fullText = '@';
+ const selectionStart = 1;
+
+ expect(membersFilter({ assignees, collection, fullText, selectionStart })).toEqual(
+ collection,
+ );
+ });
+ });
+ });
+
+ describe('/unassign quick action', () => {
+ describe('when the line starts with `/unassign`', () => {
+ it('shows members that are currently selected', () => {
+ const fullText = '/unassign @';
+ const selectionStart = 11;
+
+ expect(membersFilter({ assignees, collection, fullText, selectionStart })).toEqual([
+ collection[0],
+ ]);
+ });
+ });
+
+ describe('when the line does not start with `/unassign`', () => {
+ it('shows all members', () => {
+ const fullText = '@';
+ const selectionStart = 1;
+
+ expect(membersFilter({ assignees, collection, fullText, selectionStart })).toEqual(
+ collection,
+ );
+ });
+ });
+ });
+ });
+ });
+
+ describe('merge requests config', () => {
+ const mergeRequestsConfig = tributeConfig[GfmAutocompleteType.MergeRequests].config;
+ const groupContextMergeRequest = {
+ iid: 456789,
+ reference: 'gitlab!456789',
+ title: "Group context merge request title <script>alert('hi')</script>",
+ };
+ const projectContextMergeRequest = {
+ id: null,
+ iid: 123456,
+ time_estimate: 0,
+ title: "Project context merge request title <script>alert('hi')</script>",
+ };
+
+ it('uses ! as the trigger', () => {
+ expect(mergeRequestsConfig.trigger).toBe('!');
+ });
+
+ it('searches using both the iid and title', () => {
+ expect(mergeRequestsConfig.lookup(projectContextMergeRequest)).toBe(
+ `${projectContextMergeRequest.iid}${projectContextMergeRequest.title}`,
+ );
+ });
+
+ it('shows the reference and title in the menu item within a group context', () => {
+ expect(
+ mergeRequestsConfig.menuItemTemplate({ original: groupContextMergeRequest }),
+ ).toMatchSnapshot();
+ });
+
+ it('shows the iid and title in the menu item within a project context', () => {
+ expect(
+ mergeRequestsConfig.menuItemTemplate({ original: projectContextMergeRequest }),
+ ).toMatchSnapshot();
+ });
+
+ it('inserts the reference on autocomplete selection within a group context', () => {
+ expect(mergeRequestsConfig.selectTemplate({ original: groupContextMergeRequest })).toBe(
+ groupContextMergeRequest.reference,
+ );
+ });
+
+ it('inserts the iid on autocomplete selection within a project context', () => {
+ expect(mergeRequestsConfig.selectTemplate({ original: projectContextMergeRequest })).toBe(
+ `!${projectContextMergeRequest.iid}`,
+ );
+ });
+ });
+
+ describe('milestones config', () => {
+ const milestonesConfig = tributeConfig[GfmAutocompleteType.Milestones].config;
+ const milestone = {
+ id: null,
+ iid: 49,
+ title: "13.2 <script>alert('hi')</script>",
+ };
+
+ it('uses % as the trigger', () => {
+ expect(milestonesConfig.trigger).toBe('%');
+ });
+
+ it('searches using the title', () => {
+ expect(milestonesConfig.lookup).toBe('title');
+ });
+
+ it('shows the title in the menu item', () => {
+ expect(milestonesConfig.menuItemTemplate({ original: milestone })).toMatchSnapshot();
+ });
+
+ it('inserts the title on autocomplete selection', () => {
+ expect(milestonesConfig.selectTemplate({ original: milestone })).toBe(
+ `%"${escape(milestone.title)}"`,
+ );
+ });
+ });
+
+ describe('snippets config', () => {
+ const snippetsConfig = tributeConfig[GfmAutocompleteType.Snippets].config;
+ const snippet = {
+ id: 123456,
+ title: "Snippet title <script>alert('hi')</script>",
+ };
+
+ it('uses $ as the trigger', () => {
+ expect(snippetsConfig.trigger).toBe('$');
+ });
+
+ it('inserts the id on autocomplete selection', () => {
+ expect(snippetsConfig.fillAttr).toBe('id');
+ });
+
+ it('searches using both the id and title', () => {
+ expect(snippetsConfig.lookup(snippet)).toBe(`${snippet.id}${snippet.title}`);
+ });
+
+ it('shows the id and title in the menu item', () => {
+ expect(snippetsConfig.menuItemTemplate({ original: snippet })).toMatchSnapshot();
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js b/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js
index c87d19df1f7..d1bfc180082 100644
--- a/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js
+++ b/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js
@@ -33,28 +33,31 @@ describe('IssueMilestoneComponent', () => {
describe('computed', () => {
describe('isMilestoneStarted', () => {
- it('should return `false` when milestoneStart prop is not defined', () => {
+ it('should return `false` when milestoneStart prop is not defined', async () => {
wrapper.setProps({
milestone: { ...mockMilestone, start_date: '' },
});
+ await wrapper.vm.$nextTick();
expect(wrapper.vm.isMilestoneStarted).toBe(false);
});
- it('should return `true` when milestone start date is past current date', () => {
- wrapper.setProps({
+ it('should return `true` when milestone start date is past current date', async () => {
+ await wrapper.setProps({
milestone: { ...mockMilestone, start_date: '1990-07-22' },
});
+ await wrapper.vm.$nextTick();
expect(wrapper.vm.isMilestoneStarted).toBe(true);
});
});
describe('isMilestonePastDue', () => {
- it('should return `false` when milestoneDue prop is not defined', () => {
+ it('should return `false` when milestoneDue prop is not defined', async () => {
wrapper.setProps({
milestone: { ...mockMilestone, due_date: '' },
});
+ await wrapper.vm.$nextTick();
expect(wrapper.vm.isMilestonePastDue).toBe(false);
});
@@ -73,41 +76,45 @@ describe('IssueMilestoneComponent', () => {
expect(vm.milestoneDatesAbsolute).toBe('(December 31, 2019)');
});
- it('returns string containing absolute milestone start date when due date is not present', () => {
+ it('returns string containing absolute milestone start date when due date is not present', async () => {
wrapper.setProps({
milestone: { ...mockMilestone, due_date: '' },
});
+ await wrapper.vm.$nextTick();
expect(wrapper.vm.milestoneDatesAbsolute).toBe('(January 1, 2018)');
});
- it('returns empty string when both milestone start and due dates are not present', () => {
+ it('returns empty string when both milestone start and due dates are not present', async () => {
wrapper.setProps({
milestone: { ...mockMilestone, start_date: '', due_date: '' },
});
+ await wrapper.vm.$nextTick();
expect(wrapper.vm.milestoneDatesAbsolute).toBe('');
});
});
describe('milestoneDatesHuman', () => {
- it('returns string containing milestone due date when date is yet to be due', () => {
+ it('returns string containing milestone due date when date is yet to be due', async () => {
wrapper.setProps({
milestone: { ...mockMilestone, due_date: `${new Date().getFullYear() + 10}-01-01` },
});
+ await wrapper.vm.$nextTick();
expect(wrapper.vm.milestoneDatesHuman).toContain('years remaining');
});
- it('returns string containing milestone start date when date has already started and due date is not present', () => {
+ it('returns string containing milestone start date when date has already started and due date is not present', async () => {
wrapper.setProps({
milestone: { ...mockMilestone, start_date: '1990-07-22', due_date: '' },
});
+ await wrapper.vm.$nextTick();
expect(wrapper.vm.milestoneDatesHuman).toContain('Started');
});
- it('returns string containing milestone start date when date is yet to start and due date is not present', () => {
+ it('returns string containing milestone start date when date is yet to start and due date is not present', async () => {
wrapper.setProps({
milestone: {
...mockMilestone,
@@ -115,14 +122,16 @@ describe('IssueMilestoneComponent', () => {
due_date: '',
},
});
+ await wrapper.vm.$nextTick();
expect(wrapper.vm.milestoneDatesHuman).toContain('Starts');
});
- it('returns empty string when milestone start and due dates are not present', () => {
+ it('returns empty string when milestone start and due dates are not present', async () => {
wrapper.setProps({
milestone: { ...mockMilestone, start_date: '', due_date: '' },
});
+ await wrapper.vm.$nextTick();
expect(wrapper.vm.milestoneDatesHuman).toBe('');
});
diff --git a/spec/frontend/vue_shared/components/loading_button_spec.js b/spec/frontend/vue_shared/components/loading_button_spec.js
deleted file mode 100644
index 8bcb80d140e..00000000000
--- a/spec/frontend/vue_shared/components/loading_button_spec.js
+++ /dev/null
@@ -1,100 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import LoadingButton from '~/vue_shared/components/loading_button.vue';
-
-const LABEL = 'Hello';
-
-describe('LoadingButton', () => {
- let wrapper;
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- const buildWrapper = (propsData = {}) => {
- wrapper = shallowMount(LoadingButton, {
- propsData,
- });
- };
- const findButtonLabel = () => wrapper.find('.js-loading-button-label');
- const findButtonIcon = () => wrapper.find('.js-loading-button-icon');
-
- describe('loading spinner', () => {
- it('shown when loading', () => {
- buildWrapper({ loading: true });
-
- expect(findButtonIcon().exists()).toBe(true);
- });
- });
-
- describe('disabled state', () => {
- it('disabled when loading', () => {
- buildWrapper({ loading: true });
- expect(wrapper.attributes('disabled')).toBe('disabled');
- });
-
- it('not disabled when normal', () => {
- buildWrapper({ loading: false });
-
- expect(wrapper.attributes('disabled')).toBe(undefined);
- });
- });
-
- describe('label', () => {
- it('shown when normal', () => {
- buildWrapper({ loading: false, label: LABEL });
- expect(findButtonLabel().text()).toBe(LABEL);
- });
-
- it('shown when loading', () => {
- buildWrapper({ loading: false, label: LABEL });
- expect(findButtonLabel().text()).toBe(LABEL);
- });
- });
-
- describe('container class', () => {
- it('should default to btn btn-align-content', () => {
- buildWrapper();
-
- expect(wrapper.classes()).toContain('btn');
- expect(wrapper.classes()).toContain('btn-align-content');
- });
-
- it('should be configurable through props', () => {
- const containerClass = 'test-class';
-
- buildWrapper({
- containerClass,
- });
-
- expect(wrapper.classes()).not.toContain('btn');
- expect(wrapper.classes()).not.toContain('btn-align-content');
- expect(wrapper.classes()).toContain(containerClass);
- });
- });
-
- describe('click callback prop', () => {
- it('calls given callback when normal', () => {
- buildWrapper({
- loading: false,
- });
-
- wrapper.trigger('click');
-
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted('click')).toBeTruthy();
- });
- });
-
- it('does not call given callback when disabled because of loading', () => {
- buildWrapper({
- loading: true,
- });
-
- wrapper.trigger('click');
-
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted('click')).toBeFalsy();
- });
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/markdown/apply_suggestion_spec.js b/spec/frontend/vue_shared/components/markdown/apply_suggestion_spec.js
new file mode 100644
index 00000000000..0598506891b
--- /dev/null
+++ b/spec/frontend/vue_shared/components/markdown/apply_suggestion_spec.js
@@ -0,0 +1,72 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlDropdown, GlFormTextarea, GlButton } from '@gitlab/ui';
+import ApplySuggestionComponent from '~/vue_shared/components/markdown/apply_suggestion.vue';
+
+describe('Apply Suggestion component', () => {
+ const propsData = { fileName: 'test.js', disabled: false };
+ let wrapper;
+
+ const createWrapper = props => {
+ wrapper = shallowMount(ApplySuggestionComponent, { propsData: { ...propsData, ...props } });
+ };
+
+ const findDropdown = () => wrapper.find(GlDropdown);
+ const findTextArea = () => wrapper.find(GlFormTextarea);
+ const findApplyButton = () => wrapper.find(GlButton);
+
+ beforeEach(() => createWrapper());
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('initial template', () => {
+ it('renders a dropdown with the correct props', () => {
+ const dropdown = findDropdown();
+
+ expect(dropdown.exists()).toBe(true);
+ expect(dropdown.props('text')).toBe('Apply suggestion');
+ expect(dropdown.props('headerText')).toBe('Apply suggestion commit message');
+ expect(dropdown.props('disabled')).toBe(false);
+ });
+
+ it('renders a textarea with the correct props', () => {
+ const textArea = findTextArea();
+
+ expect(textArea.exists()).toBe(true);
+ expect(textArea.attributes('placeholder')).toBe('Apply suggestion on test.js');
+ });
+
+ it('renders an apply button', () => {
+ const applyButton = findApplyButton();
+
+ expect(applyButton.exists()).toBe(true);
+ expect(applyButton.text()).toBe('Apply');
+ });
+ });
+
+ describe('disabled', () => {
+ it('disables the dropdown', () => {
+ createWrapper({ disabled: true });
+
+ expect(findDropdown().props('disabled')).toBe(true);
+ });
+ });
+
+ describe('apply suggestion', () => {
+ it('emits an apply event with a default message if no message was added', () => {
+ findTextArea().vm.$emit('input', null);
+ findApplyButton().vm.$emit('click');
+
+ expect(wrapper.emitted('apply')).toEqual([['Apply suggestion on test.js']]);
+ });
+
+ it('emits an apply event with a user-defined message', () => {
+ findTextArea().vm.$emit('input', 'some text');
+ findApplyButton().vm.$emit('click');
+
+ expect(wrapper.emitted('apply')).toEqual([['some text']]);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/members/action_buttons/access_request_action_buttons_spec.js b/spec/frontend/vue_shared/components/members/action_buttons/access_request_action_buttons_spec.js
deleted file mode 100644
index 58cb8ef61d1..00000000000
--- a/spec/frontend/vue_shared/components/members/action_buttons/access_request_action_buttons_spec.js
+++ /dev/null
@@ -1,108 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import AccessRequestActionButtons from '~/vue_shared/components/members/action_buttons/access_request_action_buttons.vue';
-import RemoveMemberButton from '~/vue_shared/components/members/action_buttons/remove_member_button.vue';
-import ApproveAccessRequestButton from '~/vue_shared/components/members/action_buttons/approve_access_request_button.vue';
-import { accessRequest as member } from '../mock_data';
-
-describe('AccessRequestActionButtons', () => {
- let wrapper;
-
- const createComponent = (propsData = {}) => {
- wrapper = shallowMount(AccessRequestActionButtons, {
- propsData: {
- member,
- isCurrentUser: true,
- ...propsData,
- },
- });
- };
-
- const findRemoveMemberButton = () => wrapper.find(RemoveMemberButton);
- const findApproveButton = () => wrapper.find(ApproveAccessRequestButton);
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('when user has `canRemove` permissions', () => {
- beforeEach(() => {
- createComponent({
- permissions: {
- canRemove: true,
- },
- });
- });
-
- it('renders remove member button', () => {
- expect(findRemoveMemberButton().exists()).toBe(true);
- });
-
- it('sets props correctly', () => {
- expect(findRemoveMemberButton().props()).toMatchObject({
- memberId: member.id,
- title: 'Deny access',
- isAccessRequest: true,
- icon: 'close',
- });
- });
-
- describe('when member is the current user', () => {
- it('sets `message` prop correctly', () => {
- expect(findRemoveMemberButton().props('message')).toBe(
- `Are you sure you want to withdraw your access request for "${member.source.name}"`,
- );
- });
- });
-
- describe('when member is not the current user', () => {
- it('sets `message` prop correctly', () => {
- createComponent({
- isCurrentUser: false,
- permissions: {
- canRemove: true,
- },
- });
-
- expect(findRemoveMemberButton().props('message')).toBe(
- `Are you sure you want to deny ${member.user.name}'s request to join "${member.source.name}"`,
- );
- });
- });
- });
-
- describe('when user does not have `canRemove` permissions', () => {
- it('does not render remove member button', () => {
- createComponent({
- permissions: {
- canRemove: false,
- },
- });
-
- expect(findRemoveMemberButton().exists()).toBe(false);
- });
- });
-
- describe('when user has `canUpdate` permissions', () => {
- it('renders the approve button', () => {
- createComponent({
- permissions: {
- canUpdate: true,
- },
- });
-
- expect(findApproveButton().exists()).toBe(true);
- });
- });
-
- describe('when user does not have `canUpdate` permissions', () => {
- it('does not render the approve button', () => {
- createComponent({
- permissions: {
- canUpdate: false,
- },
- });
-
- expect(findApproveButton().exists()).toBe(false);
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/members/action_buttons/approve_access_request_button_spec.js b/spec/frontend/vue_shared/components/members/action_buttons/approve_access_request_button_spec.js
deleted file mode 100644
index 93edaaa400d..00000000000
--- a/spec/frontend/vue_shared/components/members/action_buttons/approve_access_request_button_spec.js
+++ /dev/null
@@ -1,74 +0,0 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
-import Vuex from 'vuex';
-import { GlButton, GlForm } from '@gitlab/ui';
-import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import ApproveAccessRequestButton from '~/vue_shared/components/members/action_buttons/approve_access_request_button.vue';
-
-jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
-
-const localVue = createLocalVue();
-localVue.use(Vuex);
-
-describe('ApproveAccessRequestButton', () => {
- let wrapper;
-
- const createStore = (state = {}) => {
- return new Vuex.Store({
- state: {
- memberPath: '/groups/foo-bar/-/group_members/:id',
- ...state,
- },
- });
- };
-
- const createComponent = (propsData = {}, state) => {
- wrapper = shallowMount(ApproveAccessRequestButton, {
- localVue,
- store: createStore(state),
- propsData: {
- memberId: 1,
- ...propsData,
- },
- directives: {
- GlTooltip: createMockDirective(),
- },
- });
- };
-
- const findForm = () => wrapper.find(GlForm);
- const findButton = () => findForm().find(GlButton);
-
- beforeEach(() => {
- createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('displays a tooltip', () => {
- const button = findButton();
-
- expect(getBinding(button.element, 'gl-tooltip')).not.toBeUndefined();
- expect(button.attributes('title')).toBe('Grant access');
- });
-
- it('sets `aria-label` attribute', () => {
- expect(findButton().attributes('aria-label')).toBe('Grant access');
- });
-
- it('submits the form when button is clicked', () => {
- expect(findButton().attributes('type')).toBe('submit');
- });
-
- it('displays form with correct action and inputs', () => {
- const form = findForm();
-
- expect(form.attributes('action')).toBe(
- '/groups/foo-bar/-/group_members/1/approve_access_request',
- );
- expect(form.find('input[name="authenticity_token"]').attributes('value')).toBe(
- 'mock-csrf-token',
- );
- });
-});
diff --git a/spec/frontend/vue_shared/components/members/action_buttons/invite_action_buttons_spec.js b/spec/frontend/vue_shared/components/members/action_buttons/invite_action_buttons_spec.js
deleted file mode 100644
index 1374cdc6aef..00000000000
--- a/spec/frontend/vue_shared/components/members/action_buttons/invite_action_buttons_spec.js
+++ /dev/null
@@ -1,85 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import InviteActionButtons from '~/vue_shared/components/members/action_buttons/invite_action_buttons.vue';
-import RemoveMemberButton from '~/vue_shared/components/members/action_buttons/remove_member_button.vue';
-import ResendInviteButton from '~/vue_shared/components/members/action_buttons/resend_invite_button.vue';
-import { invite as member } from '../mock_data';
-
-describe('InviteActionButtons', () => {
- let wrapper;
-
- const createComponent = (propsData = {}) => {
- wrapper = shallowMount(InviteActionButtons, {
- propsData: {
- member,
- ...propsData,
- },
- });
- };
-
- const findRemoveMemberButton = () => wrapper.find(RemoveMemberButton);
- const findResendInviteButton = () => wrapper.find(ResendInviteButton);
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('when user has `canRemove` permissions', () => {
- beforeEach(() => {
- createComponent({
- permissions: {
- canRemove: true,
- },
- });
- });
-
- it('renders remove member button', () => {
- expect(findRemoveMemberButton().exists()).toBe(true);
- });
-
- it('sets props correctly', () => {
- expect(findRemoveMemberButton().props()).toEqual({
- memberId: member.id,
- message: `Are you sure you want to revoke the invitation for ${member.invite.email} to join "${member.source.name}"`,
- title: 'Revoke invite',
- isAccessRequest: false,
- icon: 'remove',
- });
- });
- });
-
- describe('when user does not have `canRemove` permissions', () => {
- it('does not render remove member button', () => {
- createComponent({
- permissions: {
- canRemove: false,
- },
- });
-
- expect(findRemoveMemberButton().exists()).toBe(false);
- });
- });
-
- describe('when user has `canResend` permissions', () => {
- it('renders resend invite button', () => {
- createComponent({
- permissions: {
- canResend: true,
- },
- });
-
- expect(findResendInviteButton().exists()).toBe(true);
- });
- });
-
- describe('when user does not have `canResend` permissions', () => {
- it('does not render resend invite button', () => {
- createComponent({
- permissions: {
- canResend: false,
- },
- });
-
- expect(findResendInviteButton().exists()).toBe(false);
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/members/action_buttons/leave_button_spec.js b/spec/frontend/vue_shared/components/members/action_buttons/leave_button_spec.js
deleted file mode 100644
index 00896b23b95..00000000000
--- a/spec/frontend/vue_shared/components/members/action_buttons/leave_button_spec.js
+++ /dev/null
@@ -1,59 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { GlButton } from '@gitlab/ui';
-import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import LeaveButton from '~/vue_shared/components/members/action_buttons/leave_button.vue';
-import LeaveModal from '~/vue_shared/components/members/modals/leave_modal.vue';
-import { LEAVE_MODAL_ID } from '~/vue_shared/components/members/constants';
-import { member } from '../mock_data';
-
-describe('LeaveButton', () => {
- let wrapper;
-
- const createComponent = (propsData = {}) => {
- wrapper = shallowMount(LeaveButton, {
- propsData: {
- member,
- ...propsData,
- },
- directives: {
- GlTooltip: createMockDirective(),
- GlModal: createMockDirective(),
- },
- });
- };
-
- const findButton = () => wrapper.find(GlButton);
-
- beforeEach(() => {
- createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('displays a tooltip', () => {
- const button = findButton();
-
- expect(getBinding(button.element, 'gl-tooltip')).not.toBeUndefined();
- expect(button.attributes('title')).toBe('Leave');
- });
-
- it('sets `aria-label` attribute', () => {
- expect(findButton().attributes('aria-label')).toBe('Leave');
- });
-
- it('renders leave modal', () => {
- const leaveModal = wrapper.find(LeaveModal);
-
- expect(leaveModal.exists()).toBe(true);
- expect(leaveModal.props('member')).toEqual(member);
- });
-
- it('triggers leave modal', () => {
- const binding = getBinding(findButton().element, 'gl-modal');
-
- expect(binding).not.toBeUndefined();
- expect(binding.value).toBe(LEAVE_MODAL_ID);
- });
-});
diff --git a/spec/frontend/vue_shared/components/members/action_buttons/remove_group_link_button_spec.js b/spec/frontend/vue_shared/components/members/action_buttons/remove_group_link_button_spec.js
deleted file mode 100644
index 84fe1c51773..00000000000
--- a/spec/frontend/vue_shared/components/members/action_buttons/remove_group_link_button_spec.js
+++ /dev/null
@@ -1,64 +0,0 @@
-import { mount, createLocalVue } from '@vue/test-utils';
-import Vuex from 'vuex';
-import { GlButton } from '@gitlab/ui';
-import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import RemoveGroupLinkButton from '~/vue_shared/components/members/action_buttons/remove_group_link_button.vue';
-import { group } from '../mock_data';
-
-const localVue = createLocalVue();
-localVue.use(Vuex);
-
-describe('RemoveGroupLinkButton', () => {
- let wrapper;
-
- const actions = {
- showRemoveGroupLinkModal: jest.fn(),
- };
-
- const createStore = () => {
- return new Vuex.Store({
- actions,
- });
- };
-
- const createComponent = () => {
- wrapper = mount(RemoveGroupLinkButton, {
- localVue,
- store: createStore(),
- propsData: {
- groupLink: group,
- },
- directives: {
- GlTooltip: createMockDirective(),
- },
- });
- };
-
- const findButton = () => wrapper.find(GlButton);
-
- beforeEach(() => {
- createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- it('displays a tooltip', () => {
- const button = findButton();
-
- expect(getBinding(button.element, 'gl-tooltip')).not.toBeUndefined();
- expect(button.attributes('title')).toBe('Remove group');
- });
-
- it('sets `aria-label` attribute', () => {
- expect(findButton().attributes('aria-label')).toBe('Remove group');
- });
-
- it('calls Vuex action to open remove group link modal when clicked', () => {
- findButton().trigger('click');
-
- expect(actions.showRemoveGroupLinkModal).toHaveBeenCalledWith(expect.any(Object), group);
- });
-});
diff --git a/spec/frontend/vue_shared/components/members/action_buttons/remove_member_button_spec.js b/spec/frontend/vue_shared/components/members/action_buttons/remove_member_button_spec.js
deleted file mode 100644
index 7aa30494234..00000000000
--- a/spec/frontend/vue_shared/components/members/action_buttons/remove_member_button_spec.js
+++ /dev/null
@@ -1,66 +0,0 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
-import Vuex from 'vuex';
-import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import RemoveMemberButton from '~/vue_shared/components/members/action_buttons/remove_member_button.vue';
-
-const localVue = createLocalVue();
-localVue.use(Vuex);
-
-describe('RemoveMemberButton', () => {
- let wrapper;
-
- const createStore = (state = {}) => {
- return new Vuex.Store({
- state: {
- memberPath: '/groups/foo-bar/-/group_members/:id',
- ...state,
- },
- });
- };
-
- const createComponent = (propsData = {}, state) => {
- wrapper = shallowMount(RemoveMemberButton, {
- localVue,
- store: createStore(state),
- propsData: {
- memberId: 1,
- message: 'Are you sure you want to remove John Smith?',
- title: 'Remove member',
- isAccessRequest: true,
- ...propsData,
- },
- directives: {
- GlTooltip: createMockDirective(),
- },
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('sets attributes on button', () => {
- createComponent();
-
- expect(wrapper.attributes()).toMatchObject({
- 'data-member-path': '/groups/foo-bar/-/group_members/1',
- 'data-message': 'Are you sure you want to remove John Smith?',
- 'data-is-access-request': 'true',
- 'aria-label': 'Remove member',
- title: 'Remove member',
- icon: 'remove',
- });
- });
-
- it('displays `title` prop as a tooltip', () => {
- createComponent();
-
- expect(getBinding(wrapper.element, 'gl-tooltip')).not.toBeUndefined();
- });
-
- it('has CSS class used by `remove_member_modal.vue`', () => {
- createComponent();
-
- expect(wrapper.classes()).toContain('js-remove-member-button');
- });
-});
diff --git a/spec/frontend/vue_shared/components/members/action_buttons/resend_invite_button_spec.js b/spec/frontend/vue_shared/components/members/action_buttons/resend_invite_button_spec.js
deleted file mode 100644
index 859fdd01043..00000000000
--- a/spec/frontend/vue_shared/components/members/action_buttons/resend_invite_button_spec.js
+++ /dev/null
@@ -1,66 +0,0 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
-import Vuex from 'vuex';
-import { GlButton } from '@gitlab/ui';
-import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import ResendInviteButton from '~/vue_shared/components/members/action_buttons/resend_invite_button.vue';
-
-jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
-
-const localVue = createLocalVue();
-localVue.use(Vuex);
-
-describe('ResendInviteButton', () => {
- let wrapper;
-
- const createStore = (state = {}) => {
- return new Vuex.Store({
- state: {
- memberPath: '/groups/foo-bar/-/group_members/:id',
- ...state,
- },
- });
- };
-
- const createComponent = (propsData = {}, state) => {
- wrapper = shallowMount(ResendInviteButton, {
- localVue,
- store: createStore(state),
- propsData: {
- memberId: 1,
- ...propsData,
- },
- directives: {
- GlTooltip: createMockDirective(),
- },
- });
- };
-
- const findForm = () => wrapper.find('form');
- const findButton = () => findForm().find(GlButton);
-
- beforeEach(() => {
- createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('displays a tooltip', () => {
- expect(getBinding(findButton().element, 'gl-tooltip')).not.toBeUndefined();
- expect(findButton().attributes('title')).toBe('Resend invite');
- });
-
- it('submits the form when button is clicked', () => {
- expect(findButton().attributes('type')).toBe('submit');
- });
-
- it('displays form with correct action and inputs', () => {
- expect(findForm().attributes('action')).toBe('/groups/foo-bar/-/group_members/1/resend_invite');
- expect(
- findForm()
- .find('input[name="authenticity_token"]')
- .attributes('value'),
- ).toBe('mock-csrf-token');
- });
-});
diff --git a/spec/frontend/vue_shared/components/members/action_buttons/user_action_buttons_spec.js b/spec/frontend/vue_shared/components/members/action_buttons/user_action_buttons_spec.js
deleted file mode 100644
index f766ad5b0d1..00000000000
--- a/spec/frontend/vue_shared/components/members/action_buttons/user_action_buttons_spec.js
+++ /dev/null
@@ -1,89 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import UserActionButtons from '~/vue_shared/components/members/action_buttons/user_action_buttons.vue';
-import RemoveMemberButton from '~/vue_shared/components/members/action_buttons/remove_member_button.vue';
-import LeaveButton from '~/vue_shared/components/members/action_buttons/leave_button.vue';
-import { member, orphanedMember } from '../mock_data';
-
-describe('UserActionButtons', () => {
- let wrapper;
-
- const createComponent = (propsData = {}) => {
- wrapper = shallowMount(UserActionButtons, {
- propsData: {
- member,
- isCurrentUser: false,
- ...propsData,
- },
- });
- };
-
- const findRemoveMemberButton = () => wrapper.find(RemoveMemberButton);
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('when user has `canRemove` permissions', () => {
- beforeEach(() => {
- createComponent({
- permissions: {
- canRemove: true,
- },
- });
- });
-
- it('renders remove member button', () => {
- expect(findRemoveMemberButton().exists()).toBe(true);
- });
-
- it('sets props correctly', () => {
- expect(findRemoveMemberButton().props()).toEqual({
- memberId: member.id,
- message: `Are you sure you want to remove ${member.user.name} from "${member.source.name}"`,
- title: 'Remove member',
- isAccessRequest: false,
- icon: 'remove',
- });
- });
-
- describe('when member is orphaned', () => {
- it('sets `message` prop correctly', () => {
- createComponent({
- member: orphanedMember,
- permissions: {
- canRemove: true,
- },
- });
-
- expect(findRemoveMemberButton().props('message')).toBe(
- `Are you sure you want to remove this orphaned member from "${orphanedMember.source.name}"`,
- );
- });
- });
-
- describe('when member is the current user', () => {
- it('renders leave button', () => {
- createComponent({
- isCurrentUser: true,
- permissions: {
- canRemove: true,
- },
- });
-
- expect(wrapper.find(LeaveButton).exists()).toBe(true);
- });
- });
- });
-
- describe('when user does not have `canRemove` permissions', () => {
- it('does not render remove member button', () => {
- createComponent({
- permissions: {
- canRemove: false,
- },
- });
-
- expect(findRemoveMemberButton().exists()).toBe(false);
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/members/avatars/group_avatar_spec.js b/spec/frontend/vue_shared/components/members/avatars/group_avatar_spec.js
deleted file mode 100644
index d6f5773295c..00000000000
--- a/spec/frontend/vue_shared/components/members/avatars/group_avatar_spec.js
+++ /dev/null
@@ -1,46 +0,0 @@
-import { mount, createWrapper } from '@vue/test-utils';
-import { getByText as getByTextHelper } from '@testing-library/dom';
-import { GlAvatarLink } from '@gitlab/ui';
-import { group as member } from '../mock_data';
-import GroupAvatar from '~/vue_shared/components/members/avatars/group_avatar.vue';
-
-describe('MemberList', () => {
- let wrapper;
-
- const group = member.sharedWithGroup;
-
- const createComponent = (propsData = {}) => {
- wrapper = mount(GroupAvatar, {
- propsData: {
- member,
- ...propsData,
- },
- });
- };
-
- const getByText = (text, options) =>
- createWrapper(getByTextHelper(wrapper.element, text, options));
-
- beforeEach(() => {
- createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('renders link to group', () => {
- const link = wrapper.find(GlAvatarLink);
-
- expect(link.exists()).toBe(true);
- expect(link.attributes('href')).toBe(group.webUrl);
- });
-
- it("renders group's full name", () => {
- expect(getByText(group.fullName).exists()).toBe(true);
- });
-
- it("renders group's avatar", () => {
- expect(wrapper.find('img').attributes('src')).toBe(group.avatarUrl);
- });
-});
diff --git a/spec/frontend/vue_shared/components/members/avatars/invite_avatar_spec.js b/spec/frontend/vue_shared/components/members/avatars/invite_avatar_spec.js
deleted file mode 100644
index 7948da7eb40..00000000000
--- a/spec/frontend/vue_shared/components/members/avatars/invite_avatar_spec.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import { mount, createWrapper } from '@vue/test-utils';
-import { getByText as getByTextHelper } from '@testing-library/dom';
-import { invite as member } from '../mock_data';
-import InviteAvatar from '~/vue_shared/components/members/avatars/invite_avatar.vue';
-
-describe('MemberList', () => {
- let wrapper;
-
- const { invite } = member;
-
- const createComponent = (propsData = {}) => {
- wrapper = mount(InviteAvatar, {
- propsData: {
- member,
- ...propsData,
- },
- });
- };
-
- const getByText = (text, options) =>
- createWrapper(getByTextHelper(wrapper.element, text, options));
-
- beforeEach(() => {
- createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('renders email as name', () => {
- expect(getByText(invite.email).exists()).toBe(true);
- });
-
- it('renders avatar', () => {
- expect(wrapper.find('img').attributes('src')).toBe(invite.avatarUrl);
- });
-});
diff --git a/spec/frontend/vue_shared/components/members/avatars/user_avatar_spec.js b/spec/frontend/vue_shared/components/members/avatars/user_avatar_spec.js
deleted file mode 100644
index 93d8e640968..00000000000
--- a/spec/frontend/vue_shared/components/members/avatars/user_avatar_spec.js
+++ /dev/null
@@ -1,115 +0,0 @@
-import { mount, createWrapper } from '@vue/test-utils';
-import { within } from '@testing-library/dom';
-import { GlAvatarLink, GlBadge } from '@gitlab/ui';
-import { member as memberMock, orphanedMember } from '../mock_data';
-import UserAvatar from '~/vue_shared/components/members/avatars/user_avatar.vue';
-
-describe('UserAvatar', () => {
- let wrapper;
-
- const { user } = memberMock;
-
- const createComponent = (propsData = {}) => {
- wrapper = mount(UserAvatar, {
- propsData: {
- member: memberMock,
- isCurrentUser: false,
- ...propsData,
- },
- });
- };
-
- const getByText = (text, options) =>
- createWrapper(within(wrapper.element).findByText(text, options));
-
- const findStatusEmoji = emoji => wrapper.find(`gl-emoji[data-name="${emoji}"]`);
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it("renders link to user's profile", () => {
- createComponent();
-
- const link = wrapper.find(GlAvatarLink);
-
- expect(link.exists()).toBe(true);
- expect(link.attributes()).toMatchObject({
- href: user.webUrl,
- 'data-user-id': `${user.id}`,
- 'data-username': user.username,
- });
- });
-
- it("renders user's name", () => {
- createComponent();
-
- expect(getByText(user.name).exists()).toBe(true);
- });
-
- it("renders user's username", () => {
- createComponent();
-
- expect(getByText(`@${user.username}`).exists()).toBe(true);
- });
-
- it("renders user's avatar", () => {
- createComponent();
-
- expect(wrapper.find('img').attributes('src')).toBe(user.avatarUrl);
- });
-
- describe('when user property does not exist', () => {
- it('displays an orphaned user', () => {
- createComponent({ member: orphanedMember });
-
- expect(getByText('Orphaned member').exists()).toBe(true);
- });
- });
-
- describe('badges', () => {
- it.each`
- member | badgeText
- ${{ ...memberMock, user: { ...memberMock.user, blocked: true } }} | ${'Blocked'}
- ${{ ...memberMock, user: { ...memberMock.user, twoFactorEnabled: true } }} | ${'2FA'}
- `('renders the "$badgeText" badge', ({ member, badgeText }) => {
- createComponent({ member });
-
- expect(wrapper.find(GlBadge).text()).toBe(badgeText);
- });
-
- it('renders the "It\'s you" badge when member is current user', () => {
- createComponent({ isCurrentUser: true });
-
- expect(getByText("It's you").exists()).toBe(true);
- });
- });
-
- describe('user status', () => {
- const emoji = 'island';
-
- describe('when set', () => {
- it('displays the status emoji', () => {
- createComponent({
- member: {
- ...memberMock,
- user: {
- ...memberMock.user,
- status: { emoji, messageHtml: 'On vacation' },
- },
- },
- });
-
- expect(findStatusEmoji(emoji).exists()).toBe(true);
- });
- });
-
- describe('when not set', () => {
- it('does not display status emoji', () => {
- createComponent();
-
- expect(findStatusEmoji(emoji).exists()).toBe(false);
- });
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/members/mock_data.js b/spec/frontend/vue_shared/components/members/mock_data.js
deleted file mode 100644
index 5674929716d..00000000000
--- a/spec/frontend/vue_shared/components/members/mock_data.js
+++ /dev/null
@@ -1,71 +0,0 @@
-export const member = {
- requestedAt: null,
- canUpdate: false,
- canRemove: false,
- canOverride: false,
- isOverridden: false,
- accessLevel: { integerValue: 50, stringValue: 'Owner' },
- source: {
- id: 178,
- name: 'Foo Bar',
- webUrl: 'https://gitlab.com/groups/foo-bar',
- },
- user: {
- id: 123,
- name: 'Administrator',
- username: 'root',
- webUrl: 'https://gitlab.com/root',
- avatarUrl: 'https://www.gravatar.com/avatar/4816142ef496f956a277bedf1a40607b?s=80&d=identicon',
- blocked: false,
- twoFactorEnabled: false,
- },
- id: 238,
- createdAt: '2020-07-17T16:22:46.923Z',
- expiresAt: null,
- usingLicense: false,
- groupSso: false,
- groupManagedAccount: false,
- validRoles: {
- Guest: 10,
- Reporter: 20,
- Developer: 30,
- Maintainer: 40,
- Owner: 50,
- 'Minimal Access': 5,
- },
-};
-
-export const group = {
- accessLevel: { integerValue: 10, stringValue: 'Guest' },
- sharedWithGroup: {
- id: 24,
- name: 'Commit451',
- avatarUrl: '/uploads/-/system/user/avatar/1/avatar.png?width=40',
- fullPath: 'parent-group/commit451',
- fullName: 'Parent group / Commit451',
- webUrl: 'https://gitlab.com/groups/parent-group/commit451',
- },
- id: 3,
- createdAt: '2020-08-06T15:31:07.662Z',
- expiresAt: null,
- validRoles: { Guest: 10, Reporter: 20, Developer: 30, Maintainer: 40, Owner: 50 },
-};
-
-const { user, ...memberNoUser } = member;
-export const invite = {
- ...memberNoUser,
- invite: {
- email: 'jewel@hudsonwalter.biz',
- avatarUrl: 'https://www.gravatar.com/avatar/cbab7510da7eec2f60f638261b05436d?s=80&d=identicon',
- canResend: true,
- },
-};
-
-export const orphanedMember = memberNoUser;
-
-export const accessRequest = {
- ...member,
- requestedAt: '2020-07-17T16:22:46.923Z',
-};
-
-export const members = [member];
diff --git a/spec/frontend/vue_shared/components/members/modals/leave_modal_spec.js b/spec/frontend/vue_shared/components/members/modals/leave_modal_spec.js
deleted file mode 100644
index 63de355a3c8..00000000000
--- a/spec/frontend/vue_shared/components/members/modals/leave_modal_spec.js
+++ /dev/null
@@ -1,91 +0,0 @@
-import { mount, createLocalVue, createWrapper } from '@vue/test-utils';
-import { GlModal, GlForm } from '@gitlab/ui';
-import { nextTick } from 'vue';
-import { within } from '@testing-library/dom';
-import Vuex from 'vuex';
-import LeaveModal from '~/vue_shared/components/members/modals/leave_modal.vue';
-import { LEAVE_MODAL_ID } from '~/vue_shared/components/members/constants';
-import { member } from '../mock_data';
-
-jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
-
-const localVue = createLocalVue();
-localVue.use(Vuex);
-
-describe('LeaveModal', () => {
- let wrapper;
-
- const createStore = (state = {}) => {
- return new Vuex.Store({
- state: {
- memberPath: '/groups/foo-bar/-/group_members/:id',
- ...state,
- },
- });
- };
-
- const createComponent = (propsData = {}, state) => {
- wrapper = mount(LeaveModal, {
- localVue,
- store: createStore(state),
- propsData: {
- member,
- ...propsData,
- },
- attrs: {
- static: true,
- visible: true,
- },
- });
- };
-
- const findModal = () => wrapper.find(GlModal);
-
- const findForm = () => findModal().find(GlForm);
-
- const getByText = (text, options) =>
- createWrapper(within(findModal().element).getByText(text, options));
-
- beforeEach(async () => {
- createComponent();
- await nextTick();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('sets modal ID', () => {
- expect(findModal().props('modalId')).toBe(LEAVE_MODAL_ID);
- });
-
- it('displays modal title', () => {
- expect(getByText(`Leave "${member.source.name}"`).exists()).toBe(true);
- });
-
- it('displays modal body', () => {
- expect(getByText(`Are you sure you want to leave "${member.source.name}"?`).exists()).toBe(
- true,
- );
- });
-
- it('displays form with correct action and inputs', () => {
- const form = findForm();
-
- expect(form.attributes('action')).toBe('/groups/foo-bar/-/group_members/leave');
- expect(form.find('input[name="_method"]').attributes('value')).toBe('delete');
- expect(form.find('input[name="authenticity_token"]').attributes('value')).toBe(
- 'mock-csrf-token',
- );
- });
-
- it('submits the form when "Leave" button is clicked', () => {
- const submitSpy = jest.spyOn(findForm().element, 'submit');
-
- getByText('Leave').trigger('click');
-
- expect(submitSpy).toHaveBeenCalled();
-
- submitSpy.mockRestore();
- });
-});
diff --git a/spec/frontend/vue_shared/components/members/modals/remove_group_link_modal_spec.js b/spec/frontend/vue_shared/components/members/modals/remove_group_link_modal_spec.js
deleted file mode 100644
index 84da051792d..00000000000
--- a/spec/frontend/vue_shared/components/members/modals/remove_group_link_modal_spec.js
+++ /dev/null
@@ -1,106 +0,0 @@
-import { mount, createLocalVue, createWrapper } from '@vue/test-utils';
-import { GlModal, GlForm } from '@gitlab/ui';
-import { nextTick } from 'vue';
-import { within } from '@testing-library/dom';
-import Vuex from 'vuex';
-import RemoveGroupLinkModal from '~/vue_shared/components/members/modals/remove_group_link_modal.vue';
-import { REMOVE_GROUP_LINK_MODAL_ID } from '~/vue_shared/components/members/constants';
-import { group } from '../mock_data';
-
-jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
-
-const localVue = createLocalVue();
-localVue.use(Vuex);
-
-describe('RemoveGroupLinkModal', () => {
- let wrapper;
-
- const actions = {
- hideRemoveGroupLinkModal: jest.fn(),
- };
-
- const createStore = (state = {}) => {
- return new Vuex.Store({
- state: {
- memberPath: '/groups/foo-bar/-/group_links/:id',
- groupLinkToRemove: group,
- removeGroupLinkModalVisible: true,
- ...state,
- },
- actions,
- });
- };
-
- const createComponent = state => {
- wrapper = mount(RemoveGroupLinkModal, {
- localVue,
- store: createStore(state),
- attrs: {
- static: true,
- },
- });
- };
-
- const findModal = () => wrapper.find(GlModal);
- const findForm = () => findModal().find(GlForm);
- const getByText = (text, options) =>
- createWrapper(within(findModal().element).getByText(text, options));
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- describe('when modal is open', () => {
- beforeEach(async () => {
- createComponent();
- await nextTick();
- });
-
- it('sets modal ID', () => {
- expect(findModal().props('modalId')).toBe(REMOVE_GROUP_LINK_MODAL_ID);
- });
-
- it('displays modal title', () => {
- expect(getByText(`Remove "${group.sharedWithGroup.fullName}"`).exists()).toBe(true);
- });
-
- it('displays modal body', () => {
- expect(
- getByText(`Are you sure you want to remove "${group.sharedWithGroup.fullName}"?`).exists(),
- ).toBe(true);
- });
-
- it('displays form with correct action and inputs', () => {
- const form = findForm();
-
- expect(form.attributes('action')).toBe(`/groups/foo-bar/-/group_links/${group.id}`);
- expect(form.find('input[name="_method"]').attributes('value')).toBe('delete');
- expect(form.find('input[name="authenticity_token"]').attributes('value')).toBe(
- 'mock-csrf-token',
- );
- });
-
- it('submits the form when "Remove group" button is clicked', () => {
- const submitSpy = jest.spyOn(findForm().element, 'submit');
-
- getByText('Remove group').trigger('click');
-
- expect(submitSpy).toHaveBeenCalled();
-
- submitSpy.mockRestore();
- });
-
- it('calls `hideRemoveGroupLinkModal` action when modal is closed', () => {
- getByText('Cancel').trigger('click');
-
- expect(actions.hideRemoveGroupLinkModal).toHaveBeenCalled();
- });
- });
-
- it('modal does not show when `removeGroupLinkModalVisible` is `false`', () => {
- createComponent({ removeGroupLinkModalVisible: false });
-
- expect(findModal().vm.$attrs.visible).toBe(false);
- });
-});
diff --git a/spec/frontend/vue_shared/components/members/table/created_at_spec.js b/spec/frontend/vue_shared/components/members/table/created_at_spec.js
deleted file mode 100644
index cf3821baf44..00000000000
--- a/spec/frontend/vue_shared/components/members/table/created_at_spec.js
+++ /dev/null
@@ -1,61 +0,0 @@
-import { mount, createWrapper } from '@vue/test-utils';
-import { within } from '@testing-library/dom';
-import { useFakeDate } from 'helpers/fake_date';
-import CreatedAt from '~/vue_shared/components/members/table/created_at.vue';
-import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-
-describe('CreatedAt', () => {
- // March 15th, 2020
- useFakeDate(2020, 2, 15);
-
- const date = '2020-03-01T00:00:00.000';
- const dateTimeAgo = '2 weeks ago';
-
- let wrapper;
-
- const createComponent = propsData => {
- wrapper = mount(CreatedAt, {
- propsData: {
- date,
- ...propsData,
- },
- });
- };
-
- const getByText = (text, options) =>
- createWrapper(within(wrapper.element).getByText(text, options));
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('created at text', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('displays created at text', () => {
- expect(getByText(dateTimeAgo).exists()).toBe(true);
- });
-
- it('uses `TimeAgoTooltip` component to display tooltip', () => {
- expect(wrapper.find(TimeAgoTooltip).exists()).toBe(true);
- });
- });
-
- describe('when `createdBy` prop is provided', () => {
- it('displays a link to the user that created the member', () => {
- createComponent({
- createdBy: {
- name: 'Administrator',
- webUrl: 'https://gitlab.com/root',
- },
- });
-
- const link = getByText('Administrator');
-
- expect(link.exists()).toBe(true);
- expect(link.attributes('href')).toBe('https://gitlab.com/root');
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/members/table/expiration_datepicker_spec.js b/spec/frontend/vue_shared/components/members/table/expiration_datepicker_spec.js
deleted file mode 100644
index a1afdbc2b49..00000000000
--- a/spec/frontend/vue_shared/components/members/table/expiration_datepicker_spec.js
+++ /dev/null
@@ -1,166 +0,0 @@
-import { mount, createLocalVue } from '@vue/test-utils';
-import Vuex from 'vuex';
-import { nextTick } from 'vue';
-import { GlDatepicker } from '@gitlab/ui';
-import { useFakeDate } from 'helpers/fake_date';
-import waitForPromises from 'helpers/wait_for_promises';
-import ExpirationDatepicker from '~/vue_shared/components/members/table/expiration_datepicker.vue';
-import { member } from '../mock_data';
-
-const localVue = createLocalVue();
-localVue.use(Vuex);
-
-describe('ExpirationDatepicker', () => {
- // March 15th, 2020 3:00
- useFakeDate(2020, 2, 15, 3);
-
- let wrapper;
- let actions;
- let resolveUpdateMemberExpiration;
- const $toast = {
- show: jest.fn(),
- };
-
- const createStore = () => {
- actions = {
- updateMemberExpiration: jest.fn(
- () =>
- new Promise(resolve => {
- resolveUpdateMemberExpiration = resolve;
- }),
- ),
- };
-
- return new Vuex.Store({ actions });
- };
-
- const createComponent = (propsData = {}) => {
- wrapper = mount(ExpirationDatepicker, {
- propsData: {
- member,
- permissions: { canUpdate: true },
- ...propsData,
- },
- localVue,
- store: createStore(),
- mocks: {
- $toast,
- },
- });
- };
-
- const findInput = () => wrapper.find('input');
- const findDatepicker = () => wrapper.find(GlDatepicker);
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('datepicker input', () => {
- it('sets `member.expiresAt` as initial date', async () => {
- createComponent({ member: { ...member, expiresAt: '2020-03-17T00:00:00Z' } });
-
- await nextTick();
-
- expect(findInput().element.value).toBe('2020-03-17');
- });
- });
-
- describe('props', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('sets `minDate` prop as tomorrow', () => {
- expect(
- findDatepicker()
- .props('minDate')
- .toISOString(),
- ).toBe(new Date('2020-3-16').toISOString());
- });
-
- it('sets `target` prop as `null` so datepicker opens on focus', () => {
- expect(findDatepicker().props('target')).toBe(null);
- });
-
- it("sets `container` prop as `null` so table styles don't affect the datepicker styles", () => {
- expect(findDatepicker().props('container')).toBe(null);
- });
-
- it('shows clear button', () => {
- expect(findDatepicker().props('showClearButton')).toBe(true);
- });
- });
-
- describe('when datepicker is changed', () => {
- beforeEach(async () => {
- createComponent();
-
- findDatepicker().vm.$emit('input', new Date('2020-03-17'));
- });
-
- it('calls `updateMemberExpiration` Vuex action', () => {
- expect(actions.updateMemberExpiration).toHaveBeenCalledWith(expect.any(Object), {
- memberId: member.id,
- expiresAt: new Date('2020-03-17'),
- });
- });
-
- it('displays toast when successful', async () => {
- resolveUpdateMemberExpiration();
- await waitForPromises();
-
- expect($toast.show).toHaveBeenCalledWith('Expiration date updated successfully.');
- });
-
- it('disables dropdown while waiting for `updateMemberExpiration` to resolve', async () => {
- expect(findDatepicker().props('disabled')).toBe(true);
-
- resolveUpdateMemberExpiration();
- await waitForPromises();
-
- expect(findDatepicker().props('disabled')).toBe(false);
- });
- });
-
- describe('when datepicker is cleared', () => {
- beforeEach(async () => {
- createComponent();
-
- findInput().setValue('2020-03-17');
- await nextTick();
- wrapper.find('[data-testid="clear-button"]').trigger('click');
- });
-
- it('calls `updateMemberExpiration` Vuex action', () => {
- expect(actions.updateMemberExpiration).toHaveBeenCalledWith(expect.any(Object), {
- memberId: member.id,
- expiresAt: null,
- });
- });
-
- it('displays toast when successful', async () => {
- resolveUpdateMemberExpiration();
- await waitForPromises();
-
- expect($toast.show).toHaveBeenCalledWith('Expiration date removed successfully.');
- });
-
- it('disables datepicker while waiting for `updateMemberExpiration` to resolve', async () => {
- expect(findDatepicker().props('disabled')).toBe(true);
-
- resolveUpdateMemberExpiration();
- await waitForPromises();
-
- expect(findDatepicker().props('disabled')).toBe(false);
- });
- });
-
- describe('when user does not have `canUpdate` permissions', () => {
- it('disables datepicker', () => {
- createComponent({ permissions: { canUpdate: false } });
-
- expect(findDatepicker().props('disabled')).toBe(true);
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/members/table/expires_at_spec.js b/spec/frontend/vue_shared/components/members/table/expires_at_spec.js
deleted file mode 100644
index 95ae251b0fd..00000000000
--- a/spec/frontend/vue_shared/components/members/table/expires_at_spec.js
+++ /dev/null
@@ -1,86 +0,0 @@
-import { mount, createWrapper } from '@vue/test-utils';
-import { within } from '@testing-library/dom';
-import { useFakeDate } from 'helpers/fake_date';
-import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import ExpiresAt from '~/vue_shared/components/members/table/expires_at.vue';
-
-describe('ExpiresAt', () => {
- // March 15th, 2020
- useFakeDate(2020, 2, 15);
-
- let wrapper;
-
- const createComponent = propsData => {
- wrapper = mount(ExpiresAt, {
- propsData,
- directives: {
- GlTooltip: createMockDirective(),
- },
- });
- };
-
- const getByText = (text, options) =>
- createWrapper(within(wrapper.element).getByText(text, options));
-
- const getTooltipDirective = elementWrapper => getBinding(elementWrapper.element, 'gl-tooltip');
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('when no expiration date is set', () => {
- it('displays "No expiration set"', () => {
- createComponent({ date: null });
-
- expect(getByText('No expiration set').exists()).toBe(true);
- });
- });
-
- describe('when expiration date is in the past', () => {
- let expiredText;
-
- beforeEach(() => {
- createComponent({ date: '2019-03-15T00:00:00.000' });
-
- expiredText = getByText('Expired');
- });
-
- it('displays "Expired"', () => {
- expect(expiredText.exists()).toBe(true);
- expect(expiredText.classes()).toContain('gl-text-red-500');
- });
-
- it('displays tooltip with formatted date', () => {
- const tooltipDirective = getTooltipDirective(expiredText);
-
- expect(tooltipDirective).not.toBeUndefined();
- expect(expiredText.attributes('title')).toBe('Mar 15, 2019 12:00am GMT+0000');
- });
- });
-
- describe('when expiration date is in the future', () => {
- it.each`
- date | expected | warningColor
- ${'2020-03-23T00:00:00.000'} | ${'in 8 days'} | ${false}
- ${'2020-03-20T00:00:00.000'} | ${'in 5 days'} | ${true}
- ${'2020-03-16T00:00:00.000'} | ${'in 1 day'} | ${true}
- ${'2020-03-15T05:00:00.000'} | ${'in about 5 hours'} | ${true}
- ${'2020-03-15T01:00:00.000'} | ${'in about 1 hour'} | ${true}
- ${'2020-03-15T00:30:00.000'} | ${'in 30 minutes'} | ${true}
- ${'2020-03-15T00:01:15.000'} | ${'in 1 minute'} | ${true}
- ${'2020-03-15T00:00:15.000'} | ${'in less than a minute'} | ${true}
- `('displays "$expected"', ({ date, expected, warningColor }) => {
- createComponent({ date });
-
- const expiredText = getByText(expected);
-
- expect(expiredText.exists()).toBe(true);
-
- if (warningColor) {
- expect(expiredText.classes()).toContain('gl-text-orange-500');
- } else {
- expect(expiredText.classes()).not.toContain('gl-text-orange-500');
- }
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/members/table/member_action_buttons_spec.js b/spec/frontend/vue_shared/components/members/table/member_action_buttons_spec.js
deleted file mode 100644
index e55d9b6be2a..00000000000
--- a/spec/frontend/vue_shared/components/members/table/member_action_buttons_spec.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { MEMBER_TYPES } from '~/vue_shared/components/members/constants';
-import { member as memberMock, group, invite, accessRequest } from '../mock_data';
-import MemberActionButtons from '~/vue_shared/components/members/table/member_action_buttons.vue';
-import UserActionButtons from '~/vue_shared/components/members/action_buttons/user_action_buttons.vue';
-import GroupActionButtons from '~/vue_shared/components/members/action_buttons/group_action_buttons.vue';
-import InviteActionButtons from '~/vue_shared/components/members/action_buttons/invite_action_buttons.vue';
-import AccessRequestActionButtons from '~/vue_shared/components/members/action_buttons/access_request_action_buttons.vue';
-
-describe('MemberActionButtons', () => {
- let wrapper;
-
- const createComponent = (propsData = {}) => {
- wrapper = shallowMount(MemberActionButtons, {
- propsData: {
- isCurrentUser: false,
- permissions: {
- canRemove: true,
- },
- ...propsData,
- },
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- test.each`
- memberType | member | expectedComponent | expectedComponentName
- ${MEMBER_TYPES.user} | ${memberMock} | ${UserActionButtons} | ${'UserActionButtons'}
- ${MEMBER_TYPES.group} | ${group} | ${GroupActionButtons} | ${'GroupActionButtons'}
- ${MEMBER_TYPES.invite} | ${invite} | ${InviteActionButtons} | ${'InviteActionButtons'}
- ${MEMBER_TYPES.accessRequest} | ${accessRequest} | ${AccessRequestActionButtons} | ${'AccessRequestActionButtons'}
- `(
- 'renders $expectedComponentName when `memberType` is $memberType',
- ({ memberType, member, expectedComponent }) => {
- createComponent({ memberType, member });
-
- expect(wrapper.find(expectedComponent).exists()).toBe(true);
- },
- );
-});
diff --git a/spec/frontend/vue_shared/components/members/table/member_avatar_spec.js b/spec/frontend/vue_shared/components/members/table/member_avatar_spec.js
deleted file mode 100644
index a171dd830c1..00000000000
--- a/spec/frontend/vue_shared/components/members/table/member_avatar_spec.js
+++ /dev/null
@@ -1,39 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { MEMBER_TYPES } from '~/vue_shared/components/members/constants';
-import { member as memberMock, group, invite, accessRequest } from '../mock_data';
-import MemberAvatar from '~/vue_shared/components/members/table/member_avatar.vue';
-import UserAvatar from '~/vue_shared/components/members/avatars/user_avatar.vue';
-import GroupAvatar from '~/vue_shared/components/members/avatars/group_avatar.vue';
-import InviteAvatar from '~/vue_shared/components/members/avatars/invite_avatar.vue';
-
-describe('MemberList', () => {
- let wrapper;
-
- const createComponent = propsData => {
- wrapper = shallowMount(MemberAvatar, {
- propsData: {
- isCurrentUser: false,
- ...propsData,
- },
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- test.each`
- memberType | member | expectedComponent | expectedComponentName
- ${MEMBER_TYPES.user} | ${memberMock} | ${UserAvatar} | ${'UserAvatar'}
- ${MEMBER_TYPES.group} | ${group} | ${GroupAvatar} | ${'GroupAvatar'}
- ${MEMBER_TYPES.invite} | ${invite} | ${InviteAvatar} | ${'InviteAvatar'}
- ${MEMBER_TYPES.accessRequest} | ${accessRequest} | ${UserAvatar} | ${'UserAvatar'}
- `(
- 'renders $expectedComponentName when `memberType` is $memberType',
- ({ memberType, member, expectedComponent }) => {
- createComponent({ memberType, member });
-
- expect(wrapper.find(expectedComponent).exists()).toBe(true);
- },
- );
-});
diff --git a/spec/frontend/vue_shared/components/members/table/member_source_spec.js b/spec/frontend/vue_shared/components/members/table/member_source_spec.js
deleted file mode 100644
index 8b914d76674..00000000000
--- a/spec/frontend/vue_shared/components/members/table/member_source_spec.js
+++ /dev/null
@@ -1,71 +0,0 @@
-import { mount, createWrapper } from '@vue/test-utils';
-import { getByText as getByTextHelper } from '@testing-library/dom';
-import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import MemberSource from '~/vue_shared/components/members/table/member_source.vue';
-
-describe('MemberSource', () => {
- let wrapper;
-
- const createComponent = propsData => {
- wrapper = mount(MemberSource, {
- propsData: {
- memberSource: {
- id: 102,
- name: 'Foo bar',
- webUrl: 'https://gitlab.com/groups/foo-bar',
- },
- ...propsData,
- },
- directives: {
- GlTooltip: createMockDirective(),
- },
- });
- };
-
- const getByText = (text, options) =>
- createWrapper(getByTextHelper(wrapper.element, text, options));
-
- const getTooltipDirective = elementWrapper => getBinding(elementWrapper.element, 'gl-tooltip');
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('direct member', () => {
- it('displays "Direct member"', () => {
- createComponent({
- isDirectMember: true,
- });
-
- expect(getByText('Direct member').exists()).toBe(true);
- });
- });
-
- describe('inherited member', () => {
- let sourceGroupLink;
-
- beforeEach(() => {
- createComponent({
- isDirectMember: false,
- });
-
- sourceGroupLink = getByText('Foo bar');
- });
-
- it('displays a link to source group', () => {
- createComponent({
- isDirectMember: false,
- });
-
- expect(sourceGroupLink.exists()).toBe(true);
- expect(sourceGroupLink.attributes('href')).toBe('https://gitlab.com/groups/foo-bar');
- });
-
- it('displays tooltip with "Inherited"', () => {
- const tooltipDirective = getTooltipDirective(sourceGroupLink);
-
- expect(tooltipDirective).not.toBeUndefined();
- expect(sourceGroupLink.attributes('title')).toBe('Inherited');
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/members/table/member_table_cell_spec.js b/spec/frontend/vue_shared/components/members/table/member_table_cell_spec.js
deleted file mode 100644
index ba693975a88..00000000000
--- a/spec/frontend/vue_shared/components/members/table/member_table_cell_spec.js
+++ /dev/null
@@ -1,251 +0,0 @@
-import { mount, createLocalVue } from '@vue/test-utils';
-import Vuex from 'vuex';
-import { MEMBER_TYPES } from '~/vue_shared/components/members/constants';
-import { member as memberMock, group, invite, accessRequest } from '../mock_data';
-import MembersTableCell from '~/vue_shared/components/members/table/members_table_cell.vue';
-
-describe('MemberList', () => {
- const WrappedComponent = {
- props: {
- memberType: {
- type: String,
- required: true,
- },
- isDirectMember: {
- type: Boolean,
- required: true,
- },
- isCurrentUser: {
- type: Boolean,
- required: true,
- },
- permissions: {
- type: Object,
- required: true,
- },
- },
- render(createElement) {
- return createElement('div', this.memberType);
- },
- };
-
- const localVue = createLocalVue();
- localVue.use(Vuex);
- localVue.component('wrapped-component', WrappedComponent);
-
- const createStore = (state = {}) => {
- return new Vuex.Store({
- state: {
- sourceId: 1,
- currentUserId: 1,
- ...state,
- },
- });
- };
-
- let wrapper;
-
- const createComponent = (propsData, state = {}) => {
- wrapper = mount(MembersTableCell, {
- localVue,
- propsData,
- store: createStore(state),
- scopedSlots: {
- default: `
- <wrapped-component
- :member-type="props.memberType"
- :is-direct-member="props.isDirectMember"
- :is-current-user="props.isCurrentUser"
- :permissions="props.permissions"
- />
- `,
- },
- });
- };
-
- const findWrappedComponent = () => wrapper.find(WrappedComponent);
-
- const memberCurrentUser = {
- ...memberMock,
- user: {
- ...memberMock.user,
- id: 1,
- },
- };
-
- const createComponentWithDirectMember = (member = {}) => {
- createComponent({
- member: {
- ...memberMock,
- source: {
- ...memberMock.source,
- id: 1,
- },
- ...member,
- },
- });
- };
- const createComponentWithInheritedMember = (member = {}) => {
- createComponent({
- member: { ...memberMock, ...member },
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- test.each`
- member | expectedMemberType
- ${memberMock} | ${MEMBER_TYPES.user}
- ${group} | ${MEMBER_TYPES.group}
- ${invite} | ${MEMBER_TYPES.invite}
- ${accessRequest} | ${MEMBER_TYPES.accessRequest}
- `(
- 'sets scoped slot prop `memberType` to $expectedMemberType',
- ({ member, expectedMemberType }) => {
- createComponent({ member });
-
- expect(findWrappedComponent().props('memberType')).toBe(expectedMemberType);
- },
- );
-
- describe('isDirectMember', () => {
- it('returns `true` when member source has same ID as `sourceId`', () => {
- createComponentWithDirectMember();
-
- expect(findWrappedComponent().props('isDirectMember')).toBe(true);
- });
-
- it('returns `false` when member is inherited', () => {
- createComponentWithInheritedMember();
-
- expect(findWrappedComponent().props('isDirectMember')).toBe(false);
- });
-
- it('returns `true` for linked groups', () => {
- createComponent({
- member: group,
- });
-
- expect(findWrappedComponent().props('isDirectMember')).toBe(true);
- });
- });
-
- describe('isCurrentUser', () => {
- it('returns `true` when `member.user` has the same ID as `currentUserId`', () => {
- createComponent({
- member: memberCurrentUser,
- });
-
- expect(findWrappedComponent().props('isCurrentUser')).toBe(true);
- });
-
- it('returns `false` when `member.user` does not have the same ID as `currentUserId`', () => {
- createComponent({
- member: memberMock,
- });
-
- expect(findWrappedComponent().props('isCurrentUser')).toBe(false);
- });
- });
-
- describe('permissions', () => {
- describe('canRemove', () => {
- describe('for a direct member', () => {
- it('returns `true` when `canRemove` is `true`', () => {
- createComponentWithDirectMember({
- canRemove: true,
- });
-
- expect(findWrappedComponent().props('permissions').canRemove).toBe(true);
- });
-
- it('returns `false` when `canRemove` is `false`', () => {
- createComponentWithDirectMember({
- canRemove: false,
- });
-
- expect(findWrappedComponent().props('permissions').canRemove).toBe(false);
- });
- });
-
- describe('for an inherited member', () => {
- it('returns `false`', () => {
- createComponentWithInheritedMember();
-
- expect(findWrappedComponent().props('permissions').canRemove).toBe(false);
- });
- });
- });
-
- describe('canResend', () => {
- describe('when member type is `invite`', () => {
- it('returns `true` when `canResend` is `true`', () => {
- createComponent({
- member: invite,
- });
-
- expect(findWrappedComponent().props('permissions').canResend).toBe(true);
- });
-
- it('returns `false` when `canResend` is `false`', () => {
- createComponent({
- member: {
- ...invite,
- invite: {
- ...invite,
- canResend: false,
- },
- },
- });
-
- expect(findWrappedComponent().props('permissions').canResend).toBe(false);
- });
- });
-
- describe('when member type is not `invite`', () => {
- it('returns `false`', () => {
- createComponent({ member: memberMock });
-
- expect(findWrappedComponent().props('permissions').canResend).toBe(false);
- });
- });
- });
-
- describe('canUpdate', () => {
- describe('for a direct member', () => {
- it('returns `true` when `canUpdate` is `true`', () => {
- createComponentWithDirectMember({
- canUpdate: true,
- });
-
- expect(findWrappedComponent().props('permissions').canUpdate).toBe(true);
- });
-
- it('returns `false` when `canUpdate` is `false`', () => {
- createComponentWithDirectMember({
- canUpdate: false,
- });
-
- expect(findWrappedComponent().props('permissions').canUpdate).toBe(false);
- });
-
- it('returns `false` for current user', () => {
- createComponentWithDirectMember(memberCurrentUser);
-
- expect(findWrappedComponent().props('permissions').canUpdate).toBe(false);
- });
- });
-
- describe('for an inherited member', () => {
- it('returns `false`', () => {
- createComponentWithInheritedMember();
-
- expect(findWrappedComponent().props('permissions').canUpdate).toBe(false);
- });
- });
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/members/table/members_table_spec.js b/spec/frontend/vue_shared/components/members/table/members_table_spec.js
deleted file mode 100644
index e593e88438c..00000000000
--- a/spec/frontend/vue_shared/components/members/table/members_table_spec.js
+++ /dev/null
@@ -1,212 +0,0 @@
-import { mount, createLocalVue, createWrapper } from '@vue/test-utils';
-import Vuex from 'vuex';
-import {
- getByText as getByTextHelper,
- getByTestId as getByTestIdHelper,
- within,
-} from '@testing-library/dom';
-import { GlBadge, GlTable } from '@gitlab/ui';
-import MembersTable from '~/vue_shared/components/members/table/members_table.vue';
-import MemberAvatar from '~/vue_shared/components/members/table/member_avatar.vue';
-import MemberSource from '~/vue_shared/components/members/table/member_source.vue';
-import ExpiresAt from '~/vue_shared/components/members/table/expires_at.vue';
-import CreatedAt from '~/vue_shared/components/members/table/created_at.vue';
-import RoleDropdown from '~/vue_shared/components/members/table/role_dropdown.vue';
-import ExpirationDatepicker from '~/vue_shared/components/members/table/expiration_datepicker.vue';
-import MemberActionButtons from '~/vue_shared/components/members/table/member_action_buttons.vue';
-import * as initUserPopovers from '~/user_popovers';
-import { member as memberMock, invite, accessRequest } from '../mock_data';
-
-const localVue = createLocalVue();
-localVue.use(Vuex);
-
-describe('MemberList', () => {
- let wrapper;
-
- const createStore = (state = {}) => {
- return new Vuex.Store({
- state: {
- members: [],
- tableFields: [],
- tableAttrs: {
- table: { 'data-qa-selector': 'members_list' },
- tr: { 'data-qa-selector': 'member_row' },
- },
- sourceId: 1,
- currentUserId: 1,
- ...state,
- },
- });
- };
-
- const createComponent = state => {
- wrapper = mount(MembersTable, {
- localVue,
- store: createStore(state),
- stubs: [
- 'member-avatar',
- 'member-source',
- 'expires-at',
- 'created-at',
- 'member-action-buttons',
- 'role-dropdown',
- 'remove-group-link-modal',
- 'expiration-datepicker',
- ],
- });
- };
-
- const getByText = (text, options) =>
- createWrapper(getByTextHelper(wrapper.element, text, options));
-
- const getByTestId = (id, options) =>
- createWrapper(getByTestIdHelper(wrapper.element, id, options));
-
- const findTable = () => wrapper.find(GlTable);
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- describe('fields', () => {
- const directMember = {
- ...memberMock,
- source: { ...memberMock.source, id: 1 },
- };
-
- const memberCanUpdate = {
- ...directMember,
- canUpdate: true,
- };
-
- it.each`
- field | label | member | expectedComponent
- ${'account'} | ${'Account'} | ${memberMock} | ${MemberAvatar}
- ${'source'} | ${'Source'} | ${memberMock} | ${MemberSource}
- ${'granted'} | ${'Access granted'} | ${memberMock} | ${CreatedAt}
- ${'invited'} | ${'Invited'} | ${invite} | ${CreatedAt}
- ${'requested'} | ${'Requested'} | ${accessRequest} | ${CreatedAt}
- ${'expires'} | ${'Access expires'} | ${memberMock} | ${ExpiresAt}
- ${'maxRole'} | ${'Max role'} | ${memberCanUpdate} | ${RoleDropdown}
- ${'expiration'} | ${'Expiration'} | ${memberMock} | ${ExpirationDatepicker}
- `('renders the $label field', ({ field, label, member, expectedComponent }) => {
- createComponent({
- members: [member],
- tableFields: [field],
- });
-
- expect(getByText(label, { selector: '[role="columnheader"]' }).exists()).toBe(true);
-
- if (expectedComponent) {
- expect(
- wrapper
- .find(`[data-label="${label}"][role="cell"]`)
- .find(expectedComponent)
- .exists(),
- ).toBe(true);
- }
- });
-
- describe('"Actions" field', () => {
- it('renders "Actions" field for screen readers', () => {
- createComponent({ members: [memberCanUpdate], tableFields: ['actions'] });
-
- const actionField = getByTestId('col-actions');
-
- expect(actionField.exists()).toBe(true);
- expect(actionField.classes('gl-sr-only')).toBe(true);
- expect(
- wrapper
- .find(`[data-label="Actions"][role="cell"]`)
- .find(MemberActionButtons)
- .exists(),
- ).toBe(true);
- });
-
- describe('when user is not logged in', () => {
- it('does not render the "Actions" field', () => {
- createComponent({ currentUserId: null, tableFields: ['actions'] });
-
- expect(within(wrapper.element).queryByTestId('col-actions')).toBe(null);
- });
- });
-
- const memberCanRemove = {
- ...directMember,
- canRemove: true,
- };
-
- describe.each`
- permission | members
- ${'canUpdate'} | ${[memberCanUpdate]}
- ${'canRemove'} | ${[memberCanRemove]}
- ${'canResend'} | ${[invite]}
- `('when one of the members has $permission permissions', ({ members }) => {
- it('renders the "Actions" field', () => {
- createComponent({ members, tableFields: ['actions'] });
-
- expect(getByTestId('col-actions').exists()).toBe(true);
- });
- });
-
- describe.each`
- permission | members
- ${'canUpdate'} | ${[memberMock]}
- ${'canRemove'} | ${[memberMock]}
- ${'canResend'} | ${[{ ...invite, invite: { ...invite.invite, canResend: false } }]}
- `('when none of the members have $permission permissions', ({ members }) => {
- it('does not render the "Actions" field', () => {
- createComponent({ members, tableFields: ['actions'] });
-
- expect(within(wrapper.element).queryByTestId('col-actions')).toBe(null);
- });
- });
- });
- });
-
- describe('when `members` is an empty array', () => {
- it('displays a "No members found" message', () => {
- createComponent();
-
- expect(getByText('No members found').exists()).toBe(true);
- });
- });
-
- describe('when member can not be updated', () => {
- it('renders badge in "Max role" field', () => {
- createComponent({ members: [memberMock], tableFields: ['maxRole'] });
-
- expect(
- wrapper
- .find(`[data-label="Max role"][role="cell"]`)
- .find(GlBadge)
- .text(),
- ).toBe(memberMock.accessLevel.stringValue);
- });
- });
-
- it('initializes user popovers when mounted', () => {
- const initUserPopoversMock = jest.spyOn(initUserPopovers, 'default');
-
- createComponent();
-
- expect(initUserPopoversMock).toHaveBeenCalled();
- });
-
- it('adds QA selector to table', () => {
- createComponent();
-
- expect(findTable().attributes('data-qa-selector')).toBe('members_list');
- });
-
- it('adds QA selector to table row', () => {
- createComponent();
-
- expect(
- findTable()
- .find('tbody tr')
- .attributes('data-qa-selector'),
- ).toBe('member_row');
- });
-});
diff --git a/spec/frontend/vue_shared/components/members/table/role_dropdown_spec.js b/spec/frontend/vue_shared/components/members/table/role_dropdown_spec.js
deleted file mode 100644
index 55ec7000693..00000000000
--- a/spec/frontend/vue_shared/components/members/table/role_dropdown_spec.js
+++ /dev/null
@@ -1,151 +0,0 @@
-import { mount, createWrapper, createLocalVue } from '@vue/test-utils';
-import Vuex from 'vuex';
-import { nextTick } from 'vue';
-import { within } from '@testing-library/dom';
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
-import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
-import waitForPromises from 'helpers/wait_for_promises';
-import RoleDropdown from '~/vue_shared/components/members/table/role_dropdown.vue';
-import { member } from '../mock_data';
-
-const localVue = createLocalVue();
-localVue.use(Vuex);
-
-describe('RoleDropdown', () => {
- let wrapper;
- let actions;
- const $toast = {
- show: jest.fn(),
- };
-
- const createStore = () => {
- actions = {
- updateMemberRole: jest.fn(() => Promise.resolve()),
- };
-
- return new Vuex.Store({ actions });
- };
-
- const createComponent = (propsData = {}) => {
- wrapper = mount(RoleDropdown, {
- propsData: {
- member,
- permissions: {},
- ...propsData,
- },
- localVue,
- store: createStore(),
- mocks: {
- $toast,
- },
- });
- };
-
- const getDropdownMenu = () => within(wrapper.element).getByRole('menu');
- const getByTextInDropdownMenu = (text, options = {}) =>
- createWrapper(within(getDropdownMenu()).getByText(text, options));
- const getDropdownItemByText = text =>
- createWrapper(
- within(getDropdownMenu())
- .getByText(text, { selector: '[role="menuitem"] p' })
- .closest('[role="menuitem"]'),
- );
- const getCheckedDropdownItem = () =>
- wrapper
- .findAll(GlDropdownItem)
- .wrappers.find(dropdownItemWrapper => dropdownItemWrapper.props('isChecked'));
-
- const findDropdownToggle = () => wrapper.find('button[aria-haspopup="true"]');
- const findDropdown = () => wrapper.find(GlDropdown);
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('when dropdown is open', () => {
- beforeEach(done => {
- createComponent();
-
- findDropdownToggle().trigger('click');
- wrapper.vm.$root.$on('bv::dropdown::shown', () => {
- done();
- });
- });
-
- it('renders all valid roles', () => {
- Object.keys(member.validRoles).forEach(role => {
- expect(getDropdownItemByText(role).exists()).toBe(true);
- });
- });
-
- it('renders dropdown header', () => {
- expect(getByTextInDropdownMenu('Change permissions').exists()).toBe(true);
- });
-
- it('sets dropdown toggle and checks selected role', () => {
- expect(findDropdownToggle().text()).toBe('Owner');
- expect(getCheckedDropdownItem().text()).toBe('Owner');
- });
-
- describe('when dropdown item is selected', () => {
- it('does nothing if the item selected was already selected', () => {
- getDropdownItemByText('Owner').trigger('click');
-
- expect(actions.updateMemberRole).not.toHaveBeenCalled();
- });
-
- it('calls `updateMemberRole` Vuex action', () => {
- getDropdownItemByText('Developer').trigger('click');
-
- expect(actions.updateMemberRole).toHaveBeenCalledWith(expect.any(Object), {
- memberId: member.id,
- accessLevel: { integerValue: 30, stringValue: 'Developer' },
- });
- });
-
- it('displays toast when successful', async () => {
- getDropdownItemByText('Developer').trigger('click');
-
- await waitForPromises();
-
- expect($toast.show).toHaveBeenCalledWith('Role updated successfully.');
- });
-
- it('disables dropdown while waiting for `updateMemberRole` to resolve', async () => {
- getDropdownItemByText('Developer').trigger('click');
-
- await nextTick();
-
- expect(findDropdown().props('disabled')).toBe(true);
-
- await waitForPromises();
-
- expect(findDropdown().props('disabled')).toBe(false);
- });
- });
- });
-
- it("sets initial dropdown toggle value to member's role", () => {
- createComponent();
-
- expect(findDropdownToggle().text()).toBe('Owner');
- });
-
- it('sets the dropdown alignment to right on mobile', async () => {
- jest.spyOn(bp, 'isDesktop').mockReturnValue(false);
- createComponent();
-
- await nextTick();
-
- expect(findDropdown().attributes('right')).toBe('true');
- });
-
- it('sets the dropdown alignment to left on desktop', async () => {
- jest.spyOn(bp, 'isDesktop').mockReturnValue(true);
- createComponent();
-
- await nextTick();
-
- expect(findDropdown().attributes('right')).toBeUndefined();
- });
-});
diff --git a/spec/frontend/vue_shared/components/members/utils_spec.js b/spec/frontend/vue_shared/components/members/utils_spec.js
deleted file mode 100644
index 3f2b2097133..00000000000
--- a/spec/frontend/vue_shared/components/members/utils_spec.js
+++ /dev/null
@@ -1,122 +0,0 @@
-import {
- generateBadges,
- isGroup,
- isDirectMember,
- isCurrentUser,
- canRemove,
- canResend,
- canUpdate,
- canOverride,
-} from '~/vue_shared/components/members/utils';
-import { member as memberMock, group, invite } from './mock_data';
-
-const DIRECT_MEMBER_ID = 178;
-const INHERITED_MEMBER_ID = 179;
-const IS_CURRENT_USER_ID = 123;
-const IS_NOT_CURRENT_USER_ID = 124;
-
-describe('Members Utils', () => {
- describe('generateBadges', () => {
- it('has correct properties for each badge', () => {
- const badges = generateBadges(memberMock, true);
-
- badges.forEach(badge => {
- expect(badge).toEqual(
- expect.objectContaining({
- show: expect.any(Boolean),
- text: expect.any(String),
- variant: expect.stringMatching(/muted|neutral|info|success|danger|warning/),
- }),
- );
- });
- });
-
- it.each`
- member | expected
- ${memberMock} | ${{ show: true, text: "It's you", variant: 'success' }}
- ${{ ...memberMock, user: { ...memberMock.user, blocked: true } }} | ${{ show: true, text: 'Blocked', variant: 'danger' }}
- ${{ ...memberMock, user: { ...memberMock.user, twoFactorEnabled: true } }} | ${{ show: true, text: '2FA', variant: 'info' }}
- `('returns expected output for "$expected.text" badge', ({ member, expected }) => {
- expect(generateBadges(member, true)).toContainEqual(expect.objectContaining(expected));
- });
- });
-
- describe('isGroup', () => {
- test.each`
- member | expected
- ${group} | ${true}
- ${memberMock} | ${false}
- `('returns $expected', ({ member, expected }) => {
- expect(isGroup(member)).toBe(expected);
- });
- });
-
- describe('isDirectMember', () => {
- test.each`
- sourceId | expected
- ${DIRECT_MEMBER_ID} | ${true}
- ${INHERITED_MEMBER_ID} | ${false}
- `('returns $expected', ({ sourceId, expected }) => {
- expect(isDirectMember(memberMock, sourceId)).toBe(expected);
- });
- });
-
- describe('isCurrentUser', () => {
- test.each`
- currentUserId | expected
- ${IS_CURRENT_USER_ID} | ${true}
- ${IS_NOT_CURRENT_USER_ID} | ${false}
- `('returns $expected', ({ currentUserId, expected }) => {
- expect(isCurrentUser(memberMock, currentUserId)).toBe(expected);
- });
- });
-
- describe('canRemove', () => {
- const memberCanRemove = {
- ...memberMock,
- canRemove: true,
- };
-
- test.each`
- member | sourceId | expected
- ${memberCanRemove} | ${DIRECT_MEMBER_ID} | ${true}
- ${memberCanRemove} | ${INHERITED_MEMBER_ID} | ${false}
- ${memberMock} | ${INHERITED_MEMBER_ID} | ${false}
- `('returns $expected', ({ member, sourceId, expected }) => {
- expect(canRemove(member, sourceId)).toBe(expected);
- });
- });
-
- describe('canResend', () => {
- test.each`
- member | expected
- ${invite} | ${true}
- ${{ ...invite, invite: { ...invite.invite, canResend: false } }} | ${false}
- `('returns $expected', ({ member, sourceId, expected }) => {
- expect(canResend(member, sourceId)).toBe(expected);
- });
- });
-
- describe('canUpdate', () => {
- const memberCanUpdate = {
- ...memberMock,
- canUpdate: true,
- };
-
- test.each`
- member | currentUserId | sourceId | expected
- ${memberCanUpdate} | ${IS_NOT_CURRENT_USER_ID} | ${DIRECT_MEMBER_ID} | ${true}
- ${memberCanUpdate} | ${IS_CURRENT_USER_ID} | ${DIRECT_MEMBER_ID} | ${false}
- ${memberCanUpdate} | ${IS_CURRENT_USER_ID} | ${INHERITED_MEMBER_ID} | ${false}
- ${memberMock} | ${IS_NOT_CURRENT_USER_ID} | ${DIRECT_MEMBER_ID} | ${false}
- `('returns $expected', ({ member, currentUserId, sourceId, expected }) => {
- expect(canUpdate(member, currentUserId, sourceId)).toBe(expected);
- });
- });
-
- describe('canOverride', () => {
- it('returns `false`', () => {
- expect(canOverride(memberMock)).toBe(false);
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap b/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap
index ecea151fc8a..da49778f216 100644
--- a/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap
+++ b/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap
@@ -47,6 +47,7 @@ exports[`Package code instruction single line to match the default snapshot 1`]
<!---->
<svg
+ aria-hidden="true"
class="gl-button-icon gl-icon s16"
data-testid="copy-to-clipboard-icon"
>
diff --git a/spec/frontend/vue_shared/components/resizable_chart/__snapshots__/skeleton_loader_spec.js.snap b/spec/frontend/vue_shared/components/resizable_chart/__snapshots__/skeleton_loader_spec.js.snap
index 3990248d021..623f7d083c5 100644
--- a/spec/frontend/vue_shared/components/resizable_chart/__snapshots__/skeleton_loader_spec.js.snap
+++ b/spec/frontend/vue_shared/components/resizable_chart/__snapshots__/skeleton_loader_spec.js.snap
@@ -10,6 +10,9 @@ exports[`Resizable Skeleton Loader default setup renders the bars, labels, and g
version="1.1"
viewBox="0 0 400 130"
>
+ <title>
+ Loading
+ </title>
<rect
clip-path="url(#null-idClip)"
height="130"
@@ -226,6 +229,9 @@ exports[`Resizable Skeleton Loader with custom settings renders the correct posi
version="1.1"
viewBox="0 0 400 130"
>
+ <title>
+ Loading
+ </title>
<rect
clip-path="url(#-idClip)"
height="130"
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js
index d50cf2915e8..cd1157a1c2e 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js
+++ b/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js
@@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
+import { mockEditorApi } from '@toast-ui/vue-editor';
import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue';
import AddImageModal from '~/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal.vue';
import InsertVideoModal from '~/vue_shared/components/rich_content_editor/modals/insert_video_modal.vue';
@@ -114,10 +115,17 @@ describe('Rich Content Editor', () => {
});
describe('when editor is loaded', () => {
+ const formattedMarkdown = 'formatted markdown';
+
beforeEach(() => {
+ mockEditorApi.getMarkdown.mockReturnValueOnce(formattedMarkdown);
buildWrapper();
});
+ afterEach(() => {
+ mockEditorApi.getMarkdown.mockReset();
+ });
+
it('adds the CUSTOM_EVENTS.openAddImageModal custom event listener', () => {
expect(addCustomEventListener).toHaveBeenCalledWith(
wrapper.vm.editorApi,
@@ -137,6 +145,11 @@ describe('Rich Content Editor', () => {
it('registers HTML to markdown renderer', () => {
expect(registerHTMLToMarkdownRenderer).toHaveBeenCalledWith(wrapper.vm.editorApi);
});
+
+ it('emits load event with the markdown formatted by Toast UI', () => {
+ expect(mockEditorApi.getMarkdown).toHaveBeenCalled();
+ expect(wrapper.emitted('load')[0]).toEqual([{ formattedMarkdown }]);
+ });
});
describe('when editor is destroyed', () => {
diff --git a/spec/frontend/vue_shared/components/security_reports/__snapshots__/security_summary_spec.js.snap b/spec/frontend/vue_shared/components/security_reports/__snapshots__/security_summary_spec.js.snap
new file mode 100644
index 00000000000..1e08394dd56
--- /dev/null
+++ b/spec/frontend/vue_shared/components/security_reports/__snapshots__/security_summary_spec.js.snap
@@ -0,0 +1,144 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`SecuritySummary component given the message {"countMessage": "%{criticalStart}0 Critical%{criticalEnd} %{highStart}1 High%{highEnd} and %{otherStart}0 Others%{otherEnd}", "critical": 0, "high": 1, "message": "Security scanning detected %{totalStart}1%{totalEnd} potential vulnerability", "other": 0, "status": "", "total": 1} interpolates correctly 1`] = `
+<span>
+ Security scanning detected
+ <strong>
+ 1
+ </strong>
+ potential vulnerability
+ <span
+ class="gl-font-sm"
+ >
+ <span>
+ <span
+ class="gl-pl-4"
+ >
+
+ 0 Critical
+
+ </span>
+ </span>
+
+ <span>
+ <strong
+ class="text-danger-600 gl-px-2"
+ >
+
+ 1 High
+
+ </strong>
+ </span>
+ and
+ <span>
+ <span
+ class="gl-px-2"
+ >
+
+ 0 Others
+
+ </span>
+ </span>
+ </span>
+</span>
+`;
+
+exports[`SecuritySummary component given the message {"countMessage": "%{criticalStart}1 Critical%{criticalEnd} %{highStart}0 High%{highEnd} and %{otherStart}0 Others%{otherEnd}", "critical": 1, "high": 0, "message": "Security scanning detected %{totalStart}1%{totalEnd} potential vulnerability", "other": 0, "status": "", "total": 1} interpolates correctly 1`] = `
+<span>
+ Security scanning detected
+ <strong>
+ 1
+ </strong>
+ potential vulnerability
+ <span
+ class="gl-font-sm"
+ >
+ <span>
+ <strong
+ class="text-danger-800 gl-pl-4"
+ >
+
+ 1 Critical
+
+ </strong>
+ </span>
+
+ <span>
+ <span
+ class="gl-px-2"
+ >
+
+ 0 High
+
+ </span>
+ </span>
+ and
+ <span>
+ <span
+ class="gl-px-2"
+ >
+
+ 0 Others
+
+ </span>
+ </span>
+ </span>
+</span>
+`;
+
+exports[`SecuritySummary component given the message {"countMessage": "%{criticalStart}1 Critical%{criticalEnd} %{highStart}2 High%{highEnd} and %{otherStart}0 Others%{otherEnd}", "critical": 1, "high": 2, "message": "Security scanning detected %{totalStart}3%{totalEnd} potential vulnerabilities", "other": 0, "status": "", "total": 3} interpolates correctly 1`] = `
+<span>
+ Security scanning detected
+ <strong>
+ 3
+ </strong>
+ potential vulnerabilities
+ <span
+ class="gl-font-sm"
+ >
+ <span>
+ <strong
+ class="text-danger-800 gl-pl-4"
+ >
+
+ 1 Critical
+
+ </strong>
+ </span>
+
+ <span>
+ <strong
+ class="text-danger-600 gl-px-2"
+ >
+
+ 2 High
+
+ </strong>
+ </span>
+ and
+ <span>
+ <span
+ class="gl-px-2"
+ >
+
+ 0 Others
+
+ </span>
+ </span>
+ </span>
+</span>
+`;
+
+exports[`SecuritySummary component given the message {"message": ""} interpolates correctly 1`] = `
+<span>
+
+ <!---->
+</span>
+`;
+
+exports[`SecuritySummary component given the message {"message": "foo"} interpolates correctly 1`] = `
+<span>
+ foo
+ <!---->
+</span>
+`;
diff --git a/spec/frontend/vue_shared/components/security_reports/help_icon_spec.js b/spec/frontend/vue_shared/components/security_reports/help_icon_spec.js
new file mode 100644
index 00000000000..60203493cbd
--- /dev/null
+++ b/spec/frontend/vue_shared/components/security_reports/help_icon_spec.js
@@ -0,0 +1,68 @@
+import { GlLink, GlPopover } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import HelpIcon from '~/vue_shared/security_reports/components/help_icon.vue';
+
+const helpPath = '/docs';
+const discoverProjectSecurityPath = '/discoverProjectSecurityPath';
+
+describe('HelpIcon component', () => {
+ let wrapper;
+
+ const createWrapper = props => {
+ wrapper = shallowMount(HelpIcon, {
+ propsData: {
+ helpPath,
+ ...props,
+ },
+ });
+ };
+
+ const findLink = () => wrapper.find(GlLink);
+ const findPopover = () => wrapper.find(GlPopover);
+ const findPopoverTarget = () => wrapper.find({ ref: 'discoverProjectSecurity' });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('given a help path only', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it('does not render a popover', () => {
+ expect(findPopover().exists()).toBe(false);
+ });
+
+ it('renders a help link', () => {
+ expect(findLink().attributes()).toMatchObject({
+ href: helpPath,
+ target: '_blank',
+ });
+ });
+ });
+
+ describe('given a help path and discover project security path', () => {
+ beforeEach(() => {
+ createWrapper({ discoverProjectSecurityPath });
+ });
+
+ it('renders a popover', () => {
+ const popover = findPopover();
+ expect(popover.props('target')()).toBe(findPopoverTarget().element);
+ expect(popover.attributes()).toMatchObject({
+ title: HelpIcon.i18n.upgradeToManageVulnerabilities,
+ triggers: 'click blur',
+ });
+ expect(popover.text()).toContain(HelpIcon.i18n.upgradeToInteract);
+ });
+
+ it('renders a link to the discover path', () => {
+ expect(findLink().attributes()).toMatchObject({
+ href: discoverProjectSecurityPath,
+ target: '_blank',
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/security_reports/security_summary_spec.js b/spec/frontend/vue_shared/components/security_reports/security_summary_spec.js
new file mode 100644
index 00000000000..e57152c3cbf
--- /dev/null
+++ b/spec/frontend/vue_shared/components/security_reports/security_summary_spec.js
@@ -0,0 +1,38 @@
+import { GlSprintf } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import SecuritySummary from '~/vue_shared/security_reports/components/security_summary.vue';
+import { groupedTextBuilder } from '~/vue_shared/security_reports/store/utils';
+
+describe('SecuritySummary component', () => {
+ let wrapper;
+
+ const createWrapper = message => {
+ wrapper = shallowMount(SecuritySummary, {
+ propsData: { message },
+ stubs: {
+ GlSprintf,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe.each([
+ { message: '' },
+ { message: 'foo' },
+ groupedTextBuilder({ reportType: 'Security scanning', critical: 1, high: 0, total: 1 }),
+ groupedTextBuilder({ reportType: 'Security scanning', critical: 0, high: 1, total: 1 }),
+ groupedTextBuilder({ reportType: 'Security scanning', critical: 1, high: 2, total: 3 }),
+ ])('given the message %p', message => {
+ beforeEach(() => {
+ createWrapper(message);
+ });
+
+ it('interpolates correctly', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js
index 9db86fa775f..596cb22fca5 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js
@@ -33,8 +33,8 @@ describe('BaseComponent', () => {
expect(vm.hiddenInputName).toBe('issue[label_names][]');
});
- it('returns correct string when showCreate prop is `false`', () => {
- wrapper.setProps({ showCreate: false });
+ it('returns correct string when showCreate prop is `false`', async () => {
+ await wrapper.setProps({ showCreate: false });
expect(vm.hiddenInputName).toBe('label_id[]');
});
@@ -45,8 +45,8 @@ describe('BaseComponent', () => {
expect(vm.createLabelTitle).toBe('Create project label');
});
- it('return `Create group label` when `isProject` prop is false', () => {
- wrapper.setProps({ isProject: false });
+ it('return `Create group label` when `isProject` prop is false', async () => {
+ await wrapper.setProps({ isProject: false });
expect(vm.createLabelTitle).toBe('Create group label');
});
@@ -57,8 +57,8 @@ describe('BaseComponent', () => {
expect(vm.manageLabelsTitle).toBe('Manage project labels');
});
- it('return `Manage group labels` when `isProject` prop is false', () => {
- wrapper.setProps({ isProject: false });
+ it('return `Manage group labels` when `isProject` prop is false', async () => {
+ await wrapper.setProps({ isProject: false });
expect(vm.manageLabelsTitle).toBe('Manage group labels');
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js
index 8c17a974b39..1206450bbeb 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js
@@ -20,22 +20,24 @@ jest.mock('~/lib/utils/common_utils', () => ({
const localVue = createLocalVue();
localVue.use(Vuex);
-const createComponent = (config = mockConfig, slots = {}) =>
- shallowMount(LabelsSelectRoot, {
- localVue,
- slots,
- store: new Vuex.Store(labelsSelectModule()),
- propsData: config,
- stubs: {
- 'dropdown-contents': DropdownContents,
- },
- });
-
describe('LabelsSelectRoot', () => {
let wrapper;
+ let store;
+
+ const createComponent = (config = mockConfig, slots = {}) => {
+ wrapper = shallowMount(LabelsSelectRoot, {
+ localVue,
+ slots,
+ store,
+ propsData: config,
+ stubs: {
+ 'dropdown-contents': DropdownContents,
+ },
+ });
+ };
beforeEach(() => {
- wrapper = createComponent();
+ store = new Vuex.Store(labelsSelectModule());
});
afterEach(() => {
@@ -45,6 +47,7 @@ describe('LabelsSelectRoot', () => {
describe('methods', () => {
describe('handleVuexActionDispatch', () => {
it('calls `handleDropdownClose` when params `action.type` is `toggleDropdownContents` and state has `showDropdownButton` & `showDropdownContents` props `false`', () => {
+ createComponent();
jest.spyOn(wrapper.vm, 'handleDropdownClose').mockImplementation();
wrapper.vm.handleVuexActionDispatch(
@@ -67,7 +70,7 @@ describe('LabelsSelectRoot', () => {
});
it('calls `handleDropdownClose` with state.labels filterd using `set` prop when dropdown variant is `embedded`', () => {
- wrapper = createComponent({
+ createComponent({
...mockConfig,
variant: 'embedded',
});
@@ -95,6 +98,10 @@ describe('LabelsSelectRoot', () => {
});
describe('handleDropdownClose', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
it('emits `updateSelectedLabels` & `onDropdownClose` events on component when provided `labels` param is not empty', () => {
wrapper.vm.handleDropdownClose([{ id: 1 }, { id: 2 }]);
@@ -112,6 +119,7 @@ describe('LabelsSelectRoot', () => {
describe('handleCollapsedValueClick', () => {
it('emits `toggleCollapse` event on component', () => {
+ createComponent();
wrapper.vm.handleCollapsedValueClick();
expect(wrapper.emitted().toggleCollapse).toBeTruthy();
@@ -121,6 +129,7 @@ describe('LabelsSelectRoot', () => {
describe('template', () => {
it('renders component with classes `labels-select-wrapper position-relative`', () => {
+ createComponent();
expect(wrapper.attributes('class')).toContain('labels-select-wrapper position-relative');
});
@@ -131,7 +140,7 @@ describe('LabelsSelectRoot', () => {
`(
'renders component root element with CSS class `$cssClass` when `state.variant` is "$variant"',
({ variant, cssClass }) => {
- wrapper = createComponent({
+ createComponent({
...mockConfig,
variant,
});
@@ -142,57 +151,58 @@ describe('LabelsSelectRoot', () => {
},
);
- it('renders `dropdown-value-collapsed` component when `allowLabelCreate` prop is `true`', () => {
+ it('renders `dropdown-value-collapsed` component when `allowLabelCreate` prop is `true`', async () => {
+ createComponent();
+ await wrapper.vm.$nextTick;
expect(wrapper.find(DropdownValueCollapsed).exists()).toBe(true);
});
- it('renders `dropdown-title` component', () => {
+ it('renders `dropdown-title` component', async () => {
+ createComponent();
+ await wrapper.vm.$nextTick;
expect(wrapper.find(DropdownTitle).exists()).toBe(true);
});
- it('renders `dropdown-value` component', () => {
- const wrapperDropdownValue = createComponent(mockConfig, {
+ it('renders `dropdown-value` component', async () => {
+ createComponent(mockConfig, {
default: 'None',
});
+ await wrapper.vm.$nextTick;
- return wrapperDropdownValue.vm.$nextTick(() => {
- const valueComp = wrapperDropdownValue.find(DropdownValue);
+ const valueComp = wrapper.find(DropdownValue);
- expect(valueComp.exists()).toBe(true);
- expect(valueComp.text()).toBe('None');
-
- wrapperDropdownValue.destroy();
- });
+ expect(valueComp.exists()).toBe(true);
+ expect(valueComp.text()).toBe('None');
});
- it('renders `dropdown-button` component when `showDropdownButton` prop is `true`', () => {
+ it('renders `dropdown-button` component when `showDropdownButton` prop is `true`', async () => {
+ createComponent();
wrapper.vm.$store.dispatch('toggleDropdownButton');
-
+ await wrapper.vm.$nextTick;
expect(wrapper.find(DropdownButton).exists()).toBe(true);
});
- it('renders `dropdown-contents` component when `showDropdownButton` & `showDropdownContents` prop is `true`', () => {
+ it('renders `dropdown-contents` component when `showDropdownButton` & `showDropdownContents` prop is `true`', async () => {
+ createComponent();
wrapper.vm.$store.dispatch('toggleDropdownContents');
-
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.find(DropdownContents).exists()).toBe(true);
- });
+ await wrapper.vm.$nextTick;
+ expect(wrapper.find(DropdownContents).exists()).toBe(true);
});
describe('sets content direction based on viewport', () => {
- it('does not set direction when `state.variant` is not "embedded"', () => {
- wrapper.vm.$store.dispatch('toggleDropdownContents');
+ it('does not set direction when `state.variant` is not "embedded"', async () => {
+ createComponent();
+ wrapper.vm.$store.dispatch('toggleDropdownContents');
wrapper.vm.setContentIsOnViewport(wrapper.vm.$store.state);
+ await wrapper.vm.$nextTick;
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(DropdownContents).props('renderOnTop')).toBe(false);
- });
+ expect(wrapper.find(DropdownContents).props('renderOnTop')).toBe(false);
});
describe('when `state.variant` is "embedded"', () => {
beforeEach(() => {
- wrapper = createComponent({ ...mockConfig, variant: 'embedded' });
+ createComponent({ ...mockConfig, variant: 'embedded' });
wrapper.vm.$store.dispatch('toggleDropdownContents');
});
@@ -216,4 +226,22 @@ describe('LabelsSelectRoot', () => {
});
});
});
+
+ it('calls toggleDropdownContents action when isEditing prop is changing to true', async () => {
+ createComponent();
+
+ jest.spyOn(store, 'dispatch').mockResolvedValue();
+ await wrapper.setProps({ isEditing: true });
+
+ expect(store.dispatch).toHaveBeenCalledWith('toggleDropdownContents');
+ });
+
+ it('does not call toggleDropdownContents action when isEditing prop is changing to false', async () => {
+ createComponent();
+
+ jest.spyOn(store, 'dispatch').mockResolvedValue();
+ await wrapper.setProps({ isEditing: false });
+
+ expect(store.dispatch).not.toHaveBeenCalled();
+ });
});
diff --git a/spec/frontend/vue_shared/components/toggle_button_spec.js b/spec/frontend/vue_shared/components/toggle_button_spec.js
index f58647ff12b..2822b1999bc 100644
--- a/spec/frontend/vue_shared/components/toggle_button_spec.js
+++ b/spec/frontend/vue_shared/components/toggle_button_spec.js
@@ -1,101 +1,96 @@
-import Vue from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
-import toggleButton from '~/vue_shared/components/toggle_button.vue';
+import { shallowMount } from '@vue/test-utils';
+import { GlIcon } from '@gitlab/ui';
+import ToggleButton from '~/vue_shared/components/toggle_button.vue';
-describe('Toggle Button', () => {
- let vm;
- let Component;
+describe('Toggle Button component', () => {
+ let wrapper;
- beforeEach(() => {
- Component = Vue.extend(toggleButton);
- });
+ function createComponent(propsData = {}) {
+ wrapper = shallowMount(ToggleButton, {
+ propsData,
+ });
+ }
+
+ const findInput = () => wrapper.find('input');
+ const findButton = () => wrapper.find('button');
+ const findToggleIcon = () => wrapper.find(GlIcon);
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
+ wrapper = null;
});
- describe('render output', () => {
- beforeEach(() => {
- vm = mountComponent(Component, {
- value: true,
- name: 'foo',
- });
- });
-
- it('renders input with provided name', () => {
- expect(vm.$el.querySelector('input').getAttribute('name')).toEqual('foo');
+ it('renders input with provided name', () => {
+ createComponent({
+ name: 'foo',
});
- it('renders input with provided value', () => {
- expect(vm.$el.querySelector('input').getAttribute('value')).toEqual('true');
- });
-
- it('renders input status icon', () => {
- expect(vm.$el.querySelectorAll('span.toggle-icon').length).toEqual(1);
- expect(vm.$el.querySelectorAll('svg.s18').length).toEqual(1);
- });
+ expect(findInput().attributes('name')).toBe('foo');
});
- describe('is-checked', () => {
+ describe.each`
+ value | iconName
+ ${true} | ${'status_success_borderless'}
+ ${false} | ${'status_failed_borderless'}
+ `('when `value` prop is `$value`', ({ value, iconName }) => {
beforeEach(() => {
- vm = mountComponent(Component, {
- value: true,
+ createComponent({
+ value,
+ name: 'foo',
});
-
- jest.spyOn(vm, '$emit').mockImplementation(() => {});
});
- it('renders is checked class', () => {
- expect(vm.$el.querySelector('button').classList.contains('is-checked')).toEqual(true);
+ it('renders input with correct value attribute', () => {
+ expect(findInput().attributes('value')).toBe(`${value}`);
});
- it('sets aria-label representing toggle state', () => {
- vm.value = true;
-
- expect(vm.ariaLabel).toEqual('Toggle Status: ON');
-
- vm.value = false;
-
- expect(vm.ariaLabel).toEqual('Toggle Status: OFF');
+ it('renders correct icon', () => {
+ const icon = findToggleIcon();
+ expect(icon.isVisible()).toBe(true);
+ expect(icon.props('name')).toBe(iconName);
+ expect(findButton().classes('is-checked')).toBe(value);
});
- it('emits change event when clicked', () => {
- vm.$el.querySelector('button').click();
+ describe('when clicked', () => {
+ it('emits `change` event with correct event', async () => {
+ findButton().trigger('click');
+ await wrapper.vm.$nextTick();
- expect(vm.$emit).toHaveBeenCalledWith('change', false);
+ expect(wrapper.emitted('change')).toStrictEqual([[!value]]);
+ });
});
});
- describe('is-disabled', () => {
+ describe('when `disabledInput` prop is `true`', () => {
beforeEach(() => {
- vm = mountComponent(Component, {
+ createComponent({
value: true,
disabledInput: true,
});
- jest.spyOn(vm, '$emit').mockImplementation(() => {});
});
it('renders disabled button', () => {
- expect(vm.$el.querySelector('button').classList.contains('is-disabled')).toEqual(true);
+ expect(findButton().classes()).toContain('is-disabled');
});
- it('does not emit change event when clicked', () => {
- vm.$el.querySelector('button').click();
+ it('does not emit change event when clicked', async () => {
+ findButton().trigger('click');
+ await wrapper.vm.$nextTick();
- expect(vm.$emit).not.toHaveBeenCalled();
+ expect(wrapper.emitted('change')).toBeFalsy();
});
});
- describe('is-loading', () => {
+ describe('when `isLoading` prop is `true`', () => {
beforeEach(() => {
- vm = mountComponent(Component, {
+ createComponent({
value: true,
isLoading: true,
});
});
it('renders loading class', () => {
- expect(vm.$el.querySelector('button').classList.contains('is-loading')).toEqual(true);
+ expect(findButton().classes()).toContain('is-loading');
});
});
});
diff --git a/spec/frontend/vue_shared/components/tooltip_on_truncate_spec.js b/spec/frontend/vue_shared/components/tooltip_on_truncate_spec.js
new file mode 100644
index 00000000000..175abf5aae0
--- /dev/null
+++ b/spec/frontend/vue_shared/components/tooltip_on_truncate_spec.js
@@ -0,0 +1,239 @@
+import { mount, shallowMount } from '@vue/test-utils';
+import { hasHorizontalOverflow } from '~/lib/utils/dom_utils';
+import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
+
+const DUMMY_TEXT = 'lorem-ipsum-dolar-sit-amit-consectur-adipiscing-elit-sed-do';
+
+const createChildElement = () => `<a href="#">${DUMMY_TEXT}</a>`;
+
+jest.mock('~/lib/utils/dom_utils', () => ({
+ hasHorizontalOverflow: jest.fn(() => {
+ throw new Error('this needs to be mocked');
+ }),
+}));
+
+describe('TooltipOnTruncate component', () => {
+ let wrapper;
+ let parent;
+
+ const createComponent = ({ propsData, ...options } = {}) => {
+ wrapper = shallowMount(TooltipOnTruncate, {
+ attachToDocument: true,
+ propsData: {
+ ...propsData,
+ },
+ ...options,
+ });
+ };
+
+ const createWrappedComponent = ({ propsData, ...options }) => {
+ // set a parent around the tested component
+ parent = mount(
+ {
+ props: {
+ title: { default: '' },
+ },
+ template: `
+ <TooltipOnTruncate :title="title" truncate-target="child">
+ <div>{{title}}</div>
+ </TooltipOnTruncate>
+ `,
+ components: {
+ TooltipOnTruncate,
+ },
+ },
+ {
+ propsData: { ...propsData },
+ attachToDocument: true,
+ ...options,
+ },
+ );
+
+ wrapper = parent.find(TooltipOnTruncate);
+ };
+
+ const hasTooltip = () => wrapper.classes('js-show-tooltip');
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('with default target', () => {
+ it('renders tooltip if truncated', () => {
+ hasHorizontalOverflow.mockReturnValueOnce(true);
+ createComponent({
+ propsData: {
+ title: DUMMY_TEXT,
+ },
+ slots: {
+ default: [DUMMY_TEXT],
+ },
+ });
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(hasHorizontalOverflow).toHaveBeenCalledWith(wrapper.element);
+ expect(hasTooltip()).toBe(true);
+ expect(wrapper.attributes('data-original-title')).toEqual(DUMMY_TEXT);
+ expect(wrapper.attributes('data-placement')).toEqual('top');
+ });
+ });
+
+ it('does not render tooltip if normal', () => {
+ hasHorizontalOverflow.mockReturnValueOnce(false);
+ createComponent({
+ propsData: {
+ title: DUMMY_TEXT,
+ },
+ slots: {
+ default: [DUMMY_TEXT],
+ },
+ });
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(hasHorizontalOverflow).toHaveBeenCalledWith(wrapper.element);
+ expect(hasTooltip()).toBe(false);
+ });
+ });
+ });
+
+ describe('with child target', () => {
+ it('renders tooltip if truncated', () => {
+ hasHorizontalOverflow.mockReturnValueOnce(true);
+ createComponent({
+ propsData: {
+ title: DUMMY_TEXT,
+ truncateTarget: 'child',
+ },
+ slots: {
+ default: createChildElement(),
+ },
+ });
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(hasHorizontalOverflow).toHaveBeenCalledWith(wrapper.element.childNodes[0]);
+ expect(hasTooltip()).toBe(true);
+ });
+ });
+
+ it('does not render tooltip if normal', () => {
+ hasHorizontalOverflow.mockReturnValueOnce(false);
+ createComponent({
+ propsData: {
+ truncateTarget: 'child',
+ },
+ slots: {
+ default: createChildElement(),
+ },
+ });
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(hasHorizontalOverflow).toHaveBeenCalledWith(wrapper.element.childNodes[0]);
+ expect(hasTooltip()).toBe(false);
+ });
+ });
+ });
+
+ describe('with fn target', () => {
+ it('renders tooltip if truncated', () => {
+ hasHorizontalOverflow.mockReturnValueOnce(true);
+ createComponent({
+ propsData: {
+ title: DUMMY_TEXT,
+ truncateTarget: el => el.childNodes[1],
+ },
+ slots: {
+ default: [createChildElement(), createChildElement()],
+ },
+ });
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(hasHorizontalOverflow).toHaveBeenCalledWith(wrapper.element.childNodes[1]);
+ expect(hasTooltip()).toBe(true);
+ });
+ });
+ });
+
+ describe('placement', () => {
+ it('sets data-placement when tooltip is rendered', () => {
+ const placement = 'bottom';
+
+ hasHorizontalOverflow.mockReturnValueOnce(true);
+ createComponent({
+ propsData: {
+ placement,
+ },
+ slots: {
+ default: DUMMY_TEXT,
+ },
+ });
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(hasTooltip()).toBe(true);
+ expect(wrapper.attributes('data-placement')).toEqual(placement);
+ });
+ });
+ });
+
+ describe('updates when title and slot content changes', () => {
+ describe('is initialized with a long text', () => {
+ beforeEach(() => {
+ hasHorizontalOverflow.mockReturnValueOnce(true);
+ createWrappedComponent({
+ propsData: { title: DUMMY_TEXT },
+ });
+ return parent.vm.$nextTick();
+ });
+
+ it('renders tooltip', () => {
+ expect(hasTooltip()).toBe(true);
+ expect(wrapper.attributes('data-original-title')).toEqual(DUMMY_TEXT);
+ expect(wrapper.attributes('data-placement')).toEqual('top');
+ });
+
+ it('does not render tooltip after updated to a short text', () => {
+ hasHorizontalOverflow.mockReturnValueOnce(false);
+ parent.setProps({
+ title: 'new-text',
+ });
+
+ return wrapper.vm
+ .$nextTick()
+ .then(() => wrapper.vm.$nextTick()) // wait 2 times to get an updated slot
+ .then(() => {
+ expect(hasTooltip()).toBe(false);
+ });
+ });
+ });
+
+ describe('is initialized with a short text', () => {
+ beforeEach(() => {
+ hasHorizontalOverflow.mockReturnValueOnce(false);
+ createWrappedComponent({
+ propsData: { title: DUMMY_TEXT },
+ });
+ return wrapper.vm.$nextTick();
+ });
+
+ it('does not render tooltip', () => {
+ expect(hasTooltip()).toBe(false);
+ });
+
+ it('renders tooltip after text is updated', () => {
+ hasHorizontalOverflow.mockReturnValueOnce(true);
+ const newText = 'new-text';
+ parent.setProps({
+ title: newText,
+ });
+
+ return wrapper.vm
+ .$nextTick()
+ .then(() => wrapper.vm.$nextTick()) // wait 2 times to get an updated slot
+ .then(() => {
+ expect(hasTooltip()).toBe(true);
+ expect(wrapper.attributes('data-original-title')).toEqual(newText);
+ expect(wrapper.attributes('data-placement')).toEqual('top');
+ });
+ });
+ });
+ });
+});