summaryrefslogtreecommitdiff
path: root/spec/frontend/vue_shared
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-11-30 11:02:35 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-11-30 11:02:35 +0000
commit434a0ce52d75e13d48eac9ce83774954c7c5d48d (patch)
treede3b7a7cf1ce8b07555f28df592297c76894c90f /spec/frontend/vue_shared
parent0a0d9493ca481c56b739a3df27c31262283150fe (diff)
downloadgitlab-ce-13.7.0-rc2.tar.gz
Add latest changes from gitlab-org/gitlab@13-7-stable-eev13.7.0-rc2
Diffstat (limited to 'spec/frontend/vue_shared')
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap191
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap4
-rw-r--r--spec/frontend/vue_shared/components/awards_list_spec.js36
-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)8
-rw-r--r--spec/frontend/vue_shared/components/gfm_autocomplete/utils_spec.js344
-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/security_reports/__snapshots__/security_summary_spec.js.snap144
-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_vue/labels_select_root_spec.js104
-rw-r--r--spec/frontend/vue_shared/components/tooltip_on_truncate_spec.js239
-rw-r--r--spec/frontend/vue_shared/security_reports/mock_data.js318
-rw-r--r--spec/frontend/vue_shared/security_reports/security_reports_app_spec.js373
-rw-r--r--spec/frontend/vue_shared/security_reports/store/getters_spec.js182
38 files changed, 1810 insertions, 2499 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__/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/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/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..d12faacca75 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 { 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/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/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..0336f4c0325
--- /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[`Severity Summary 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[`Severity Summary 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[`Severity Summary 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[`Severity Summary given the message {"message": ""} interpolates correctly 1`] = `
+<span>
+
+ <!---->
+</span>
+`;
+
+exports[`Severity Summary given the message {"message": "foo"} interpolates correctly 1`] = `
+<span>
+ foo
+ <!---->
+</span>
+`;
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..4217fef9fbf
--- /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('Severity Summary', () => {
+ 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_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/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');
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/security_reports/mock_data.js b/spec/frontend/vue_shared/security_reports/mock_data.js
new file mode 100644
index 00000000000..9061d4586c5
--- /dev/null
+++ b/spec/frontend/vue_shared/security_reports/mock_data.js
@@ -0,0 +1,318 @@
+export const mockFindings = [
+ {
+ id: null,
+ report_type: 'dependency_scanning',
+ name: 'Cross-site Scripting in serialize-javascript',
+ severity: 'critical',
+ scanner: {
+ external_id: 'gemnasium',
+ name: 'Gemnasium',
+ version: '1.1.1',
+ url: 'https://gitlab.com/gitlab-org/security-products/gemnasium',
+ },
+ identifiers: [
+ {
+ external_type: 'gemnasium',
+ external_id: '58caa017-9a9a-46d6-bab2-ec930f46833c',
+ name: 'Gemnasium-58caa017-9a9a-46d6-bab2-ec930f46833c',
+ url:
+ 'https://deps.sec.gitlab.com/packages/npm/serialize-javascript/versions/1.7.0/advisories',
+ },
+ {
+ external_type: 'cve',
+ external_id: 'CVE-2019-16769',
+ name: 'CVE-2019-16769',
+ url: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-16769',
+ },
+ ],
+ project_fingerprint: '09df9f4d11c8deb93d81bdcc39f7667b44143298',
+ create_vulnerability_feedback_issue_path: '/gitlab-org/gitlab-ui/vulnerability_feedback',
+ create_vulnerability_feedback_merge_request_path:
+ '/gitlab-org/gitlab-ui/vulnerability_feedback',
+ create_vulnerability_feedback_dismissal_path: '/gitlab-org/gitlab-ui/vulnerability_feedback',
+ project: {
+ id: 7071551,
+ name: 'gitlab-ui',
+ full_path: '/gitlab-org/gitlab-ui',
+ full_name: 'GitLab.org / gitlab-ui',
+ },
+ dismissal_feedback: null,
+ issue_feedback: null,
+ merge_request_feedback: null,
+ description:
+ 'The serialize-javascript npm package is vulnerable to Cross-site Scripting (XSS). It does not properly mitigate against unsafe characters in serialized regular expressions. If serialized data of regular expression objects are used in an environment other than Node.js, it is affected by this vulnerability.',
+ links: [{ url: 'https://nvd.nist.gov/vuln/detail/CVE-2019-16769' }],
+ location: {
+ file: 'yarn.lock',
+ dependency: { package: { name: 'serialize-javascript' }, version: '1.7.0' },
+ },
+ remediations: [null],
+ solution: 'Upgrade to version 2.1.1 or above.',
+ state: 'opened',
+ blob_path: '/gitlab-org/gitlab-ui/blob/ad137f0a8ac59af961afe47d04e5cc062c6864a9/yarn.lock',
+ evidence: 'Credit Card Detected: Diners Card',
+ },
+ {
+ id: null,
+ report_type: 'dependency_scanning',
+ name: '3rd party CORS request may execute in jquery',
+ severity: 'high',
+ scanner: { external_id: 'retire.js', name: 'Retire.js' },
+ identifiers: [
+ {
+ external_type: 'cve',
+ external_id: 'CVE-2015-9251',
+ name: 'CVE-2015-9251',
+ url: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2015-9251',
+ },
+ ],
+ project_fingerprint: '1ecd3b214cf39c0b9ad23a0a9679778d7cf55876',
+ create_vulnerability_feedback_issue_path: '/gitlab-org/gitlab-ui/vulnerability_feedback',
+ create_vulnerability_feedback_merge_request_path:
+ '/gitlab-org/gitlab-ui/vulnerability_feedback',
+ create_vulnerability_feedback_dismissal_path: '/gitlab-org/gitlab-ui/vulnerability_feedback',
+ project: {
+ id: 7071551,
+ name: 'gitlab-ui',
+ full_path: '/gitlab-org/gitlab-ui',
+ full_name: 'GitLab.org / gitlab-ui',
+ },
+ dismissal_feedback: {
+ id: 2528,
+ created_at: '2019-08-26T12:30:32.349Z',
+ project_id: 7071551,
+ author: {
+ id: 181229,
+ name: "Lukas 'Eipi' Eipert",
+ username: 'leipert',
+ state: 'active',
+ avatar_url:
+ 'https://secure.gravatar.com/avatar/19a1f1260fa70323f35bc508927921a2?s=80\u0026d=identicon',
+ web_url: 'https://gitlab.com/leipert',
+ status_tooltip_html: null,
+ path: '/leipert',
+ },
+ comment_details: {
+ comment: 'This particular jQuery version appears in a test path of tinycolor2.\n',
+ comment_timestamp: '2019-08-26T12:30:37.610Z',
+ comment_author: {
+ id: 181229,
+ name: "Lukas 'Eipi' Eipert",
+ username: 'leipert',
+ state: 'active',
+ avatar_url:
+ 'https://secure.gravatar.com/avatar/19a1f1260fa70323f35bc508927921a2?s=80\u0026d=identicon',
+ web_url: 'https://gitlab.com/leipert',
+ status_tooltip_html: null,
+ path: '/leipert',
+ },
+ },
+ pipeline: { id: 78375355, path: '/gitlab-org/gitlab-ui/pipelines/78375355' },
+ destroy_vulnerability_feedback_dismissal_path:
+ '/gitlab-org/gitlab-ui/vulnerability_feedback/2528',
+ category: 'dependency_scanning',
+ feedback_type: 'dismissal',
+ branch: 'leipert-dogfood-secure',
+ project_fingerprint: '1ecd3b214cf39c0b9ad23a0a9679778d7cf55876',
+ },
+ issue_feedback: null,
+ merge_request_feedback: null,
+ description: null,
+ links: [
+ { url: 'https://github.com/jquery/jquery/issues/2432' },
+ { url: 'http://blog.jquery.com/2016/01/08/jquery-2-2-and-1-12-released/' },
+ { url: 'https://nvd.nist.gov/vuln/detail/CVE-2015-9251' },
+ { url: 'http://research.insecurelabs.org/jquery/test/' },
+ ],
+ location: {
+ file: 'node_modules/tinycolor2/demo/jquery-1.9.1.js',
+ dependency: { package: { name: 'jquery' }, version: '1.9.1' },
+ },
+ remediations: [null],
+ solution: null,
+ state: 'dismissed',
+ blob_path:
+ '/gitlab-org/gitlab-ui/blob/ad137f0a8ac59af961afe47d04e5cc062c6864a9/node_modules/tinycolor2/demo/jquery-1.9.1.js',
+ },
+ {
+ id: null,
+ report_type: 'dependency_scanning',
+ name:
+ 'jQuery before 3.4.0, as used in Drupal, Backdrop CMS, and other products, mishandles jQuery.extend(true, {}, ...) because of Object.prototype pollution in jquery',
+ severity: 'low',
+ scanner: { external_id: 'retire.js', name: 'Retire.js' },
+ identifiers: [
+ {
+ external_type: 'cve',
+ external_id: 'CVE-2019-11358',
+ name: 'CVE-2019-11358',
+ url: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-11358',
+ },
+ ],
+ project_fingerprint: 'aeb4b2442d92d0ccf7023f0c220bda8b4ba910e3',
+ create_vulnerability_feedback_issue_path: '/gitlab-org/gitlab-ui/vulnerability_feedback',
+ create_vulnerability_feedback_merge_request_path:
+ '/gitlab-org/gitlab-ui/vulnerability_feedback',
+ create_vulnerability_feedback_dismissal_path: '/gitlab-org/gitlab-ui/vulnerability_feedback',
+ project: {
+ id: 7071551,
+ name: 'gitlab-ui',
+ full_path: '/gitlab-org/gitlab-ui',
+ full_name: 'GitLab.org / gitlab-ui',
+ },
+ dismissal_feedback: {
+ id: 4197,
+ created_at: '2019-11-14T11:03:18.472Z',
+ project_id: 7071551,
+ author: {
+ id: 181229,
+ name: "Lukas 'Eipi' Eipert",
+ username: 'leipert',
+ state: 'active',
+ avatar_url:
+ 'https://secure.gravatar.com/avatar/19a1f1260fa70323f35bc508927921a2?s=80\u0026d=identicon',
+ web_url: 'https://gitlab.com/leipert',
+ status_tooltip_html: null,
+ path: '/leipert',
+ },
+ comment_details: {
+ comment:
+ 'This is a false positive, as it just part of some documentation assets of sass-true.',
+ comment_timestamp: '2019-11-14T11:03:18.464Z',
+ comment_author: {
+ id: 181229,
+ name: "Lukas 'Eipi' Eipert",
+ username: 'leipert',
+ state: 'active',
+ avatar_url:
+ 'https://secure.gravatar.com/avatar/19a1f1260fa70323f35bc508927921a2?s=80\u0026d=identicon',
+ web_url: 'https://gitlab.com/leipert',
+ status_tooltip_html: null,
+ path: '/leipert',
+ },
+ },
+ destroy_vulnerability_feedback_dismissal_path:
+ '/gitlab-org/gitlab-ui/vulnerability_feedback/4197',
+ category: 'dependency_scanning',
+ feedback_type: 'dismissal',
+ branch: null,
+ project_fingerprint: 'aeb4b2442d92d0ccf7023f0c220bda8b4ba910e3',
+ },
+ issue_feedback: null,
+ merge_request_feedback: null,
+ description: null,
+ links: [
+ { url: 'https://blog.jquery.com/2019/04/10/jquery-3-4-0-released/' },
+ { url: 'https://nvd.nist.gov/vuln/detail/CVE-2019-11358' },
+ { url: 'https://github.com/jquery/jquery/commit/753d591aea698e57d6db58c9f722cd0808619b1b' },
+ ],
+ location: {
+ file: 'node_modules/sass-true/docs/assets/webpack/common.min.js',
+ dependency: { package: { name: 'jquery' }, version: '3.3.1' },
+ },
+ remediations: [null],
+ solution: null,
+ state: 'dismissed',
+ blob_path:
+ '/gitlab-org/gitlab-ui/blob/ad137f0a8ac59af961afe47d04e5cc062c6864a9/node_modules/sass-true/docs/assets/webpack/common.min.js',
+ },
+ {
+ id: null,
+ report_type: 'dependency_scanning',
+ name:
+ 'jQuery before 3.4.0, as used in Drupal, Backdrop CMS, and other products, mishandles jQuery.extend(true, {}, ...) because of Object.prototype pollution in jquery',
+ severity: 'low',
+ scanner: { external_id: 'retire.js', name: 'Retire.js' },
+ identifiers: [
+ {
+ external_type: 'cve',
+ external_id: 'CVE-2019-11358',
+ name: 'CVE-2019-11358',
+ url: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-11358',
+ },
+ ],
+ project_fingerprint: 'eb86aa13eb9d897a083ead6e134aa78aa9cadd52',
+ create_vulnerability_feedback_issue_path: '/gitlab-org/gitlab-ui/vulnerability_feedback',
+ create_vulnerability_feedback_merge_request_path:
+ '/gitlab-org/gitlab-ui/vulnerability_feedback',
+ create_vulnerability_feedback_dismissal_path: '/gitlab-org/gitlab-ui/vulnerability_feedback',
+ project: {
+ id: 7071551,
+ name: 'gitlab-ui',
+ full_path: '/gitlab-org/gitlab-ui',
+ full_name: 'GitLab.org / gitlab-ui',
+ },
+ dismissal_feedback: {
+ id: 2527,
+ created_at: '2019-08-26T12:29:43.624Z',
+ project_id: 7071551,
+ author: {
+ id: 181229,
+ name: "Lukas 'Eipi' Eipert",
+ username: 'leipert',
+ state: 'active',
+ avatar_url:
+ 'https://secure.gravatar.com/avatar/19a1f1260fa70323f35bc508927921a2?s=80\u0026d=identicon',
+ web_url: 'https://gitlab.com/leipert',
+ status_tooltip_html: null,
+ path: '/leipert',
+ },
+ comment_details: {
+ comment: 'This particular jQuery version appears in a test path of tinycolor2.',
+ comment_timestamp: '2019-08-26T12:30:14.840Z',
+ comment_author: {
+ id: 181229,
+ name: "Lukas 'Eipi' Eipert",
+ username: 'leipert',
+ state: 'active',
+ avatar_url:
+ 'https://secure.gravatar.com/avatar/19a1f1260fa70323f35bc508927921a2?s=80\u0026d=identicon',
+ web_url: 'https://gitlab.com/leipert',
+ status_tooltip_html: null,
+ path: '/leipert',
+ },
+ },
+ pipeline: { id: 78375355, path: '/gitlab-org/gitlab-ui/pipelines/78375355' },
+ destroy_vulnerability_feedback_dismissal_path:
+ '/gitlab-org/gitlab-ui/vulnerability_feedback/2527',
+ category: 'dependency_scanning',
+ feedback_type: 'dismissal',
+ branch: 'leipert-dogfood-secure',
+ project_fingerprint: 'eb86aa13eb9d897a083ead6e134aa78aa9cadd52',
+ },
+ issue_feedback: null,
+ merge_request_feedback: null,
+ description: null,
+ links: [
+ { url: 'https://blog.jquery.com/2019/04/10/jquery-3-4-0-released/' },
+ { url: 'https://nvd.nist.gov/vuln/detail/CVE-2019-11358' },
+ { url: 'https://github.com/jquery/jquery/commit/753d591aea698e57d6db58c9f722cd0808619b1b' },
+ ],
+ location: {
+ file: 'node_modules/tinycolor2/demo/jquery-1.9.1.js',
+ dependency: { package: { name: 'jquery' }, version: '1.9.1' },
+ },
+ remediations: [null],
+ solution: null,
+ state: 'dismissed',
+ blob_path:
+ '/gitlab-org/gitlab-ui/blob/ad137f0a8ac59af961afe47d04e5cc062c6864a9/node_modules/tinycolor2/demo/jquery-1.9.1.js',
+ },
+];
+
+export const sastDiffSuccessMock = {
+ added: [mockFindings[0]],
+ fixed: [mockFindings[1], mockFindings[2]],
+ existing: [mockFindings[3]],
+ base_report_created_at: '2020-01-01T10:00:00.000Z',
+ base_report_out_of_date: false,
+ head_report_created_at: '2020-01-10T10:00:00.000Z',
+};
+
+export const secretScanningDiffSuccessMock = {
+ added: [mockFindings[0], mockFindings[1]],
+ fixed: [mockFindings[2]],
+ base_report_created_at: '2020-01-01T10:00:00.000Z',
+ base_report_out_of_date: false,
+ head_report_created_at: '2020-01-10T10:00:00.000Z',
+};
diff --git a/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js b/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js
index ab87d80b291..d11b519e5ad 100644
--- a/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js
+++ b/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js
@@ -1,13 +1,32 @@
-import { mount } from '@vue/test-utils';
+import { mount, createLocalVue } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
+import { merge } from 'lodash';
+import Vuex from 'vuex';
+import { trimText } from 'helpers/text_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import {
+ sastDiffSuccessMock,
+ secretScanningDiffSuccessMock,
+} from 'jest/vue_shared/security_reports/mock_data';
import Api from '~/api';
-import Flash from '~/flash';
+import createFlash from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+import {
+ REPORT_TYPE_SAST,
+ REPORT_TYPE_SECRET_DETECTION,
+} from '~/vue_shared/security_reports/constants';
import SecurityReportsApp from '~/vue_shared/security_reports/security_reports_app.vue';
jest.mock('~/flash');
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+const SAST_COMPARISON_PATH = '/sast.json';
+const SECRET_SCANNING_COMPARISON_PATH = '/secret_detection.json';
+
describe('Security reports app', () => {
let wrapper;
- let mrTabsMock;
const props = {
pipelineId: 123,
@@ -15,148 +34,290 @@ describe('Security reports app', () => {
securityReportsDocsPath: '/docs',
};
- const createComponent = () => {
- wrapper = mount(SecurityReportsApp, {
- propsData: { ...props },
- });
+ const createComponent = options => {
+ wrapper = mount(
+ SecurityReportsApp,
+ merge(
+ {
+ localVue,
+ propsData: { ...props },
+ },
+ options,
+ ),
+ );
};
const anyParams = expect.any(Object);
const findPipelinesTabAnchor = () => wrapper.find('[data-testid="show-pipelines"]');
const findHelpLink = () => wrapper.find('[data-testid="help"]');
- const setupMrTabsMock = () => {
- mrTabsMock = { tabShown: jest.fn() };
- window.mrTabs = mrTabsMock;
- };
const setupMockJobArtifact = reportType => {
jest
.spyOn(Api, 'pipelineJobs')
.mockResolvedValue({ data: [{ artifacts: [{ file_type: reportType }] }] });
};
+ const expectPipelinesTabAnchor = () => {
+ const mrTabsMock = { tabShown: jest.fn() };
+ window.mrTabs = mrTabsMock;
+ findPipelinesTabAnchor().trigger('click');
+ expect(mrTabsMock.tabShown.mock.calls).toEqual([['pipelines']]);
+ };
afterEach(() => {
wrapper.destroy();
delete window.mrTabs;
});
- describe.each(SecurityReportsApp.reportTypes)('given a report type %p', reportType => {
- beforeEach(() => {
- window.mrTabs = { tabShown: jest.fn() };
- setupMockJobArtifact(reportType);
- createComponent();
- return wrapper.vm.$nextTick();
- });
+ describe.each([false, true])(
+ 'given the coreSecurityMrWidgetCounts feature flag is %p',
+ coreSecurityMrWidgetCounts => {
+ const createComponentWithFlag = options =>
+ createComponent(
+ merge(
+ {
+ provide: {
+ glFeatures: {
+ coreSecurityMrWidgetCounts,
+ },
+ },
+ },
+ options,
+ ),
+ );
- it('calls the pipelineJobs API correctly', () => {
- expect(Api.pipelineJobs).toHaveBeenCalledTimes(1);
- expect(Api.pipelineJobs).toHaveBeenCalledWith(props.projectId, props.pipelineId, anyParams);
- });
+ describe.each(SecurityReportsApp.reportTypes)('given a report type %p', reportType => {
+ beforeEach(() => {
+ window.mrTabs = { tabShown: jest.fn() };
+ setupMockJobArtifact(reportType);
+ createComponentWithFlag();
+ return wrapper.vm.$nextTick();
+ });
- it('renders the expected message', () => {
- expect(wrapper.text()).toMatchInterpolatedText(SecurityReportsApp.i18n.scansHaveRun);
- });
+ it('calls the pipelineJobs API correctly', () => {
+ expect(Api.pipelineJobs).toHaveBeenCalledTimes(1);
+ expect(Api.pipelineJobs).toHaveBeenCalledWith(
+ props.projectId,
+ props.pipelineId,
+ anyParams,
+ );
+ });
- describe('clicking the anchor to the pipelines tab', () => {
- beforeEach(() => {
- setupMrTabsMock();
- findPipelinesTabAnchor().trigger('click');
+ it('renders the expected message', () => {
+ expect(wrapper.text()).toMatchInterpolatedText(SecurityReportsApp.i18n.scansHaveRun);
+ });
+
+ describe('clicking the anchor to the pipelines tab', () => {
+ it('calls the mrTabs.tabShown global', () => {
+ expectPipelinesTabAnchor();
+ });
+ });
+
+ it('renders a help link', () => {
+ expect(findHelpLink().attributes()).toMatchObject({
+ href: props.securityReportsDocsPath,
+ });
+ });
});
- it('calls the mrTabs.tabShown global', () => {
- expect(mrTabsMock.tabShown.mock.calls).toEqual([['pipelines']]);
+ describe('given a report type "foo"', () => {
+ beforeEach(() => {
+ setupMockJobArtifact('foo');
+ createComponentWithFlag();
+ return wrapper.vm.$nextTick();
+ });
+
+ it('calls the pipelineJobs API correctly', () => {
+ expect(Api.pipelineJobs).toHaveBeenCalledTimes(1);
+ expect(Api.pipelineJobs).toHaveBeenCalledWith(
+ props.projectId,
+ props.pipelineId,
+ anyParams,
+ );
+ });
+
+ it('renders nothing', () => {
+ expect(wrapper.html()).toBe('');
+ });
});
- });
- it('renders a help link', () => {
- expect(findHelpLink().attributes()).toMatchObject({
- href: props.securityReportsDocsPath,
+ describe('security artifacts on last page of multi-page response', () => {
+ const numPages = 3;
+
+ beforeEach(() => {
+ jest
+ .spyOn(Api, 'pipelineJobs')
+ .mockImplementation(async (projectId, pipelineId, { page }) => {
+ const requestedPage = parseInt(page, 10);
+ if (requestedPage < numPages) {
+ return {
+ // Some jobs with no relevant artifacts
+ data: [{}, {}],
+ headers: { 'x-next-page': String(requestedPage + 1) },
+ };
+ } else if (requestedPage === numPages) {
+ return {
+ data: [{ artifacts: [{ file_type: SecurityReportsApp.reportTypes[0] }] }],
+ };
+ }
+
+ throw new Error('Test failed due to request of non-existent jobs page');
+ });
+
+ createComponentWithFlag();
+ return wrapper.vm.$nextTick();
+ });
+
+ it('fetches all pages', () => {
+ expect(Api.pipelineJobs).toHaveBeenCalledTimes(numPages);
+ });
+
+ it('renders the expected message', () => {
+ expect(wrapper.text()).toMatchInterpolatedText(SecurityReportsApp.i18n.scansHaveRun);
+ });
});
- });
- });
- describe('given a report type "foo"', () => {
- beforeEach(() => {
- setupMockJobArtifact('foo');
- createComponent();
- return wrapper.vm.$nextTick();
- });
+ describe('given an error from the API', () => {
+ let error;
- it('calls the pipelineJobs API correctly', () => {
- expect(Api.pipelineJobs).toHaveBeenCalledTimes(1);
- expect(Api.pipelineJobs).toHaveBeenCalledWith(props.projectId, props.pipelineId, anyParams);
- });
+ beforeEach(() => {
+ error = new Error('an error');
+ jest.spyOn(Api, 'pipelineJobs').mockRejectedValue(error);
+ createComponentWithFlag();
+ return wrapper.vm.$nextTick();
+ });
- it('renders nothing', () => {
- expect(wrapper.html()).toBe('');
- });
- });
+ it('calls the pipelineJobs API correctly', () => {
+ expect(Api.pipelineJobs).toHaveBeenCalledTimes(1);
+ expect(Api.pipelineJobs).toHaveBeenCalledWith(
+ props.projectId,
+ props.pipelineId,
+ anyParams,
+ );
+ });
+
+ it('renders nothing', () => {
+ expect(wrapper.html()).toBe('');
+ });
+
+ it('calls createFlash correctly', () => {
+ expect(createFlash.mock.calls).toEqual([
+ [
+ {
+ message: SecurityReportsApp.i18n.apiError,
+ captureError: true,
+ error,
+ },
+ ],
+ ]);
+ });
+ });
+ },
+ );
- describe('security artifacts on last page of multi-page response', () => {
- const numPages = 3;
+ describe('given the coreSecurityMrWidgetCounts feature flag is enabled', () => {
+ let mock;
+
+ const createComponentWithFlagEnabled = options =>
+ createComponent(
+ merge(options, {
+ provide: {
+ glFeatures: {
+ coreSecurityMrWidgetCounts: true,
+ },
+ },
+ }),
+ );
beforeEach(() => {
- jest
- .spyOn(Api, 'pipelineJobs')
- .mockImplementation(async (projectId, pipelineId, { page }) => {
- const requestedPage = parseInt(page, 10);
- if (requestedPage < numPages) {
- return {
- // Some jobs with no relevant artifacts
- data: [{}, {}],
- headers: { 'x-next-page': String(requestedPage + 1) },
- };
- } else if (requestedPage === numPages) {
- return {
- data: [{ artifacts: [{ file_type: SecurityReportsApp.reportTypes[0] }] }],
- };
- }
-
- throw new Error('Test failed due to request of non-existent jobs page');
- });
-
- createComponent();
- return wrapper.vm.$nextTick();
+ mock = new MockAdapter(axios);
});
- it('fetches all pages', () => {
- expect(Api.pipelineJobs).toHaveBeenCalledTimes(numPages);
+ afterEach(() => {
+ mock.restore();
});
- it('renders the expected message', () => {
- expect(wrapper.text()).toMatchInterpolatedText(SecurityReportsApp.i18n.scansHaveRun);
- });
- });
+ const SAST_SUCCESS_MESSAGE =
+ 'Security scanning detected 1 potential vulnerability 1 Critical 0 High and 0 Others';
+ const SECRET_SCANNING_SUCCESS_MESSAGE =
+ 'Security scanning detected 2 potential vulnerabilities 1 Critical 1 High and 0 Others';
+ describe.each`
+ reportType | pathProp | path | successResponse | successMessage
+ ${REPORT_TYPE_SAST} | ${'sastComparisonPath'} | ${SAST_COMPARISON_PATH} | ${sastDiffSuccessMock} | ${SAST_SUCCESS_MESSAGE}
+ ${REPORT_TYPE_SECRET_DETECTION} | ${'secretScanningComparisonPath'} | ${SECRET_SCANNING_COMPARISON_PATH} | ${secretScanningDiffSuccessMock} | ${SECRET_SCANNING_SUCCESS_MESSAGE}
+ `(
+ 'given a $pathProp and $reportType artifact',
+ ({ reportType, pathProp, path, successResponse, successMessage }) => {
+ beforeEach(() => {
+ setupMockJobArtifact(reportType);
+ });
- describe('given an error from the API', () => {
- let error;
+ describe('when loading', () => {
+ beforeEach(() => {
+ mock = new MockAdapter(axios, { delayResponse: 1 });
+ mock.onGet(path).replyOnce(200, successResponse);
- beforeEach(() => {
- error = new Error('an error');
- jest.spyOn(Api, 'pipelineJobs').mockRejectedValue(error);
- createComponent();
- return wrapper.vm.$nextTick();
- });
+ createComponentWithFlagEnabled({
+ propsData: {
+ [pathProp]: path,
+ },
+ });
- it('calls the pipelineJobs API correctly', () => {
- expect(Api.pipelineJobs).toHaveBeenCalledTimes(1);
- expect(Api.pipelineJobs).toHaveBeenCalledWith(props.projectId, props.pipelineId, anyParams);
- });
+ return waitForPromises();
+ });
- it('renders nothing', () => {
- expect(wrapper.html()).toBe('');
- });
+ it('should have loading message', () => {
+ expect(wrapper.text()).toBe('Security scanning is loading');
+ });
- it('calls Flash correctly', () => {
- expect(Flash.mock.calls).toEqual([
- [
- {
- message: SecurityReportsApp.i18n.apiError,
- captureError: true,
- error,
- },
- ],
- ]);
- });
+ it('should not render the pipeline tab anchor', () => {
+ expect(findPipelinesTabAnchor().exists()).toBe(false);
+ });
+ });
+
+ describe('when successfully loaded', () => {
+ beforeEach(() => {
+ mock.onGet(path).replyOnce(200, successResponse);
+
+ createComponentWithFlagEnabled({
+ propsData: {
+ [pathProp]: path,
+ },
+ });
+
+ return waitForPromises();
+ });
+
+ it('should show counts', () => {
+ expect(trimText(wrapper.text())).toContain(successMessage);
+ });
+
+ it('should render the pipeline tab anchor', () => {
+ expectPipelinesTabAnchor();
+ });
+ });
+
+ describe('when an error occurs', () => {
+ beforeEach(() => {
+ mock.onGet(path).replyOnce(500);
+
+ createComponentWithFlagEnabled({
+ propsData: {
+ [pathProp]: path,
+ },
+ });
+
+ return waitForPromises();
+ });
+
+ it('should show error message', () => {
+ expect(trimText(wrapper.text())).toContain('Loading resulted in an error');
+ });
+
+ it('should render the pipeline tab anchor', () => {
+ expectPipelinesTabAnchor();
+ });
+ });
+ },
+ );
});
});
diff --git a/spec/frontend/vue_shared/security_reports/store/getters_spec.js b/spec/frontend/vue_shared/security_reports/store/getters_spec.js
new file mode 100644
index 00000000000..8de704be455
--- /dev/null
+++ b/spec/frontend/vue_shared/security_reports/store/getters_spec.js
@@ -0,0 +1,182 @@
+import createState from '~/vue_shared/security_reports/store/state';
+import createSastState from '~/vue_shared/security_reports/store/modules/sast/state';
+import createSecretScanningState from '~/vue_shared/security_reports/store/modules/secret_detection/state';
+import { groupedTextBuilder } from '~/vue_shared/security_reports/store/utils';
+import {
+ groupedSummaryText,
+ allReportsHaveError,
+ areReportsLoading,
+ anyReportHasError,
+ areAllReportsLoading,
+ anyReportHasIssues,
+ summaryCounts,
+} from '~/vue_shared/security_reports/store/getters';
+import { CRITICAL, HIGH, LOW } from '~/vulnerabilities/constants';
+
+const generateVuln = severity => ({ severity });
+
+describe('Security reports getters', () => {
+ let state;
+
+ beforeEach(() => {
+ state = createState();
+ state.sast = createSastState();
+ state.secretDetection = createSecretScanningState();
+ });
+
+ describe('summaryCounts', () => {
+ it('returns 0 count for empty state', () => {
+ expect(summaryCounts(state)).toEqual({
+ critical: 0,
+ high: 0,
+ other: 0,
+ });
+ });
+
+ describe('combines all reports', () => {
+ it('of the same severity', () => {
+ state.sast.newIssues = [generateVuln(CRITICAL)];
+ state.secretDetection.newIssues = [generateVuln(CRITICAL)];
+
+ expect(summaryCounts(state)).toEqual({
+ critical: 2,
+ high: 0,
+ other: 0,
+ });
+ });
+
+ it('of different severities', () => {
+ state.sast.newIssues = [generateVuln(CRITICAL)];
+ state.secretDetection.newIssues = [generateVuln(HIGH), generateVuln(LOW)];
+
+ expect(summaryCounts(state)).toEqual({
+ critical: 1,
+ high: 1,
+ other: 1,
+ });
+ });
+ });
+ });
+
+ describe('groupedSummaryText', () => {
+ it('returns failed text', () => {
+ expect(
+ groupedSummaryText(state, {
+ allReportsHaveError: true,
+ areReportsLoading: false,
+ summaryCounts: {},
+ }),
+ ).toEqual({ message: 'Security scanning failed loading any results' });
+ });
+
+ it('returns `is loading` as status text', () => {
+ expect(
+ groupedSummaryText(state, {
+ allReportsHaveError: false,
+ areReportsLoading: true,
+ summaryCounts: {},
+ }),
+ ).toEqual(
+ groupedTextBuilder({
+ reportType: 'Security scanning',
+ critical: 0,
+ high: 0,
+ other: 0,
+ status: 'is loading',
+ }),
+ );
+ });
+
+ it('returns no new status text if there are existing ones', () => {
+ expect(
+ groupedSummaryText(state, {
+ allReportsHaveError: false,
+ areReportsLoading: false,
+ summaryCounts: {},
+ }),
+ ).toEqual(
+ groupedTextBuilder({
+ reportType: 'Security scanning',
+ critical: 0,
+ high: 0,
+ other: 0,
+ status: '',
+ }),
+ );
+ });
+ });
+
+ describe('areReportsLoading', () => {
+ it('returns true when any report is loading', () => {
+ state.sast.isLoading = true;
+
+ expect(areReportsLoading(state)).toEqual(true);
+ });
+
+ it('returns false when none of the reports are loading', () => {
+ expect(areReportsLoading(state)).toEqual(false);
+ });
+ });
+
+ describe('areAllReportsLoading', () => {
+ it('returns true when all reports are loading', () => {
+ state.sast.isLoading = true;
+ state.secretDetection.isLoading = true;
+
+ expect(areAllReportsLoading(state)).toEqual(true);
+ });
+
+ it('returns false when some of the reports are loading', () => {
+ state.sast.isLoading = true;
+
+ expect(areAllReportsLoading(state)).toEqual(false);
+ });
+
+ it('returns false when none of the reports are loading', () => {
+ expect(areAllReportsLoading(state)).toEqual(false);
+ });
+ });
+
+ describe('allReportsHaveError', () => {
+ it('returns true when all reports have error', () => {
+ state.sast.hasError = true;
+ state.secretDetection.hasError = true;
+
+ expect(allReportsHaveError(state)).toEqual(true);
+ });
+
+ it('returns false when none of the reports have error', () => {
+ expect(allReportsHaveError(state)).toEqual(false);
+ });
+
+ it('returns false when one of the reports does not have error', () => {
+ state.secretDetection.hasError = true;
+
+ expect(allReportsHaveError(state)).toEqual(false);
+ });
+ });
+
+ describe('anyReportHasError', () => {
+ it('returns true when any of the reports has error', () => {
+ state.sast.hasError = true;
+
+ expect(anyReportHasError(state)).toEqual(true);
+ });
+
+ it('returns false when none of the reports has error', () => {
+ expect(anyReportHasError(state)).toEqual(false);
+ });
+ });
+
+ describe('anyReportHasIssues', () => {
+ it('returns true when any of the reports has new issues', () => {
+ state.sast.newIssues.push(generateVuln(LOW));
+
+ expect(anyReportHasIssues(state)).toEqual(true);
+ });
+
+ it('returns false when none of the reports has error', () => {
+ expect(anyReportHasIssues(state)).toEqual(false);
+ });
+ });
+});