summaryrefslogtreecommitdiff
path: root/spec/frontend/vue_shared/components
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/vue_shared/components')
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap32
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/file_row_header_spec.js.snap24
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/integration_help_text_spec.js.snap27
-rw-r--r--spec/frontend/vue_shared/components/alert_details_table_spec.js23
-rw-r--r--spec/frontend/vue_shared/components/awards_list_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/blob_viewers/__snapshots__/simple_viewer_spec.js.snap2
-rw-r--r--spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/confirm_modal_spec.js20
-rw-r--r--spec/frontend/vue_shared/components/dropdown/dropdown_button_spec.js3
-rw-r--r--spec/frontend/vue_shared/components/file_row_spec.js15
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js33
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/integration_help_text_spec.js57
-rw-r--r--spec/frontend/vue_shared/components/local_storage_sync_spec.js26
-rw-r--r--spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js12
-rw-r--r--spec/frontend/vue_shared/components/members/mock_data.js1
-rw-r--r--spec/frontend/vue_shared/components/members/table/expiration_datepicker_spec.js166
-rw-r--r--spec/frontend/vue_shared/components/members/table/members_table_spec.js101
-rw-r--r--spec/frontend/vue_shared/components/members/table/role_dropdown_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/members/utils_spec.js97
-rw-r--r--spec/frontend/vue_shared/components/modal_copy_button_spec.js11
-rw-r--r--spec/frontend/vue_shared/components/multiselect_dropdown_spec.js31
-rw-r--r--spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/registry/title_area_spec.js57
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/editor_service_spec.js21
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer_spec.js26
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/mock_data.js107
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js119
-rw-r--r--spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js375
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js11
-rw-r--r--spec/frontend/vue_shared/components/stacked_progress_bar_spec.js74
-rw-r--r--spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap608
-rw-r--r--spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js174
-rw-r--r--spec/frontend/vue_shared/components/user_popover/user_popover_spec.js30
40 files changed, 2184 insertions, 131 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 82503e5a025..04ae2a0f34d 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
@@ -6,10 +6,8 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
>
<button
class="btn award-control"
- data-boundary="viewport"
- data-original-title="Ada, Leonardo, and Marie"
data-testid="award-button"
- title=""
+ title="Ada, Leonardo, and Marie"
type="button"
>
<span
@@ -32,10 +30,8 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
</button>
<button
class="btn award-control active"
- data-boundary="viewport"
- data-original-title="You, Ada, and Marie"
data-testid="award-button"
- title=""
+ title="You, Ada, and Marie"
type="button"
>
<span
@@ -58,10 +54,8 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
</button>
<button
class="btn award-control"
- data-boundary="viewport"
- data-original-title="Ada and Jane"
data-testid="award-button"
- title=""
+ title="Ada and Jane"
type="button"
>
<span
@@ -84,10 +78,8 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
</button>
<button
class="btn award-control active"
- data-boundary="viewport"
- data-original-title="You, Ada, Jane, and Leonardo"
data-testid="award-button"
- title=""
+ title="You, Ada, Jane, and Leonardo"
type="button"
>
<span
@@ -110,10 +102,8 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
</button>
<button
class="btn award-control active"
- data-boundary="viewport"
- data-original-title="You"
data-testid="award-button"
- title=""
+ title="You"
type="button"
>
<span
@@ -136,10 +126,8 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
</button>
<button
class="btn award-control"
- data-boundary="viewport"
- data-original-title="Marie"
data-testid="award-button"
- title=""
+ title="Marie"
type="button"
>
<span
@@ -162,10 +150,8 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
</button>
<button
class="btn award-control active"
- data-boundary="viewport"
- data-original-title="You"
data-testid="award-button"
- title=""
+ title="You"
type="button"
>
<span
@@ -193,9 +179,7 @@ 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"
- data-boundary="viewport"
- data-original-title="Add reaction"
- title=""
+ title="Add reaction"
type="button"
>
<span
diff --git a/spec/frontend/vue_shared/components/__snapshots__/file_row_header_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/file_row_header_spec.js.snap
index 5ab159a5a84..ca9d4488870 100644
--- a/spec/frontend/vue_shared/components/__snapshots__/file_row_header_spec.js.snap
+++ b/spec/frontend/vue_shared/components/__snapshots__/file_row_header_spec.js.snap
@@ -5,11 +5,11 @@ exports[`File row header component adds multiple ellipsises after 40 characters
class="file-row-header bg-white sticky-top p-2 js-file-row-header"
title="app/assets/javascripts/merge_requests/widget/diffs/notes"
>
- <span
+ <gl-truncate-stub
class="bold"
- >
- app/assets/javascripts/…/…/diffs/notes
- </span>
+ position="middle"
+ text="app/assets/javascripts/merge_requests/widget/diffs/notes"
+ />
</div>
`;
@@ -18,11 +18,11 @@ exports[`File row header component renders file path 1`] = `
class="file-row-header bg-white sticky-top p-2 js-file-row-header"
title="app/assets"
>
- <span
+ <gl-truncate-stub
class="bold"
- >
- app/assets
- </span>
+ position="middle"
+ text="app/assets"
+ />
</div>
`;
@@ -31,10 +31,10 @@ exports[`File row header component trucates path after 40 characters 1`] = `
class="file-row-header bg-white sticky-top p-2 js-file-row-header"
title="app/assets/javascripts/merge_requests"
>
- <span
+ <gl-truncate-stub
class="bold"
- >
- app/assets/javascripts/merge_requests
- </span>
+ position="middle"
+ text="app/assets/javascripts/merge_requests"
+ />
</div>
`;
diff --git a/spec/frontend/vue_shared/components/__snapshots__/integration_help_text_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/integration_help_text_spec.js.snap
new file mode 100644
index 00000000000..df0fcf5da1c
--- /dev/null
+++ b/spec/frontend/vue_shared/components/__snapshots__/integration_help_text_spec.js.snap
@@ -0,0 +1,27 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`IntegrationHelpText component should not render the link when start and end is not provided 1`] = `
+<span>
+ Click nowhere!
+</span>
+`;
+
+exports[`IntegrationHelpText component should render the help text 1`] = `
+<span>
+ Click
+ <gl-link-stub
+ href="http://bar.com"
+ target="_blank"
+ >
+
+ Bar
+
+ <gl-icon-stub
+ class="gl-vertical-align-middle"
+ name="external-link"
+ size="12"
+ />
+ </gl-link-stub>
+ !
+</span>
+`;
diff --git a/spec/frontend/vue_shared/components/alert_details_table_spec.js b/spec/frontend/vue_shared/components/alert_details_table_spec.js
index dff307e92c2..ef7815f9e9e 100644
--- a/spec/frontend/vue_shared/components/alert_details_table_spec.js
+++ b/spec/frontend/vue_shared/components/alert_details_table_spec.js
@@ -23,14 +23,10 @@ const environmentPath = '/fake/path';
describe('AlertDetails', () => {
let environmentData = { name: environmentName, path: environmentPath };
- let glFeatures = { exposeEnvironmentPathInAlertDetails: false };
let wrapper;
function mountComponent(propsData = {}) {
wrapper = mount(AlertDetailsTable, {
- provide: {
- glFeatures,
- },
propsData: {
alert: {
...mockAlert,
@@ -97,34 +93,19 @@ describe('AlertDetails', () => {
expect(findTableField(fields, 'Severity').exists()).toBe(true);
expect(findTableField(fields, 'Status').exists()).toBe(true);
expect(findTableField(fields, 'Hosts').exists()).toBe(true);
- expect(findTableField(fields, 'Environment').exists()).toBe(false);
+ expect(findTableField(fields, 'Environment').exists()).toBe(true);
});
- it('should not show disallowed and flaggedAllowed alert fields', () => {
+ it('should not show disallowed alert fields', () => {
const fields = findTableKeys();
expect(findTableField(fields, 'Typename').exists()).toBe(false);
expect(findTableField(fields, 'Todos').exists()).toBe(false);
expect(findTableField(fields, 'Notes').exists()).toBe(false);
expect(findTableField(fields, 'Assignees').exists()).toBe(false);
- expect(findTableField(fields, 'Environment').exists()).toBe(false);
- });
- });
-
- describe('when exposeEnvironmentPathInAlertDetails is enabled', () => {
- beforeEach(() => {
- glFeatures = { exposeEnvironmentPathInAlertDetails: true };
- mountComponent();
- });
-
- it('should show flaggedAllowed alert fields', () => {
- const fields = findTableKeys();
-
- expect(findTableField(fields, 'Environment').exists()).toBe(true);
});
it('should display only the name for the environment', () => {
- expect(findTableFieldValueByKey('Iid').text()).toBe('1527542');
expect(findTableFieldValueByKey('Environment').text()).toBe(environmentName);
});
diff --git a/spec/frontend/vue_shared/components/awards_list_spec.js b/spec/frontend/vue_shared/components/awards_list_spec.js
index 0abb72ace2e..63fc8a5749d 100644
--- a/spec/frontend/vue_shared/components/awards_list_spec.js
+++ b/spec/frontend/vue_shared/components/awards_list_spec.js
@@ -62,7 +62,7 @@ describe('vue_shared/components/awards_list', () => {
findAwardButtons().wrappers.map(x => {
return {
classes: x.classes(),
- title: x.attributes('data-original-title'),
+ title: x.attributes('title'),
html: x.find('[data-testid="award-html"]').element.innerHTML,
count: Number(x.find('.js-counter').text()),
};
diff --git a/spec/frontend/vue_shared/components/blob_viewers/__snapshots__/simple_viewer_spec.js.snap b/spec/frontend/vue_shared/components/blob_viewers/__snapshots__/simple_viewer_spec.js.snap
index 4909d2d4226..023895099b1 100644
--- a/spec/frontend/vue_shared/components/blob_viewers/__snapshots__/simple_viewer_spec.js.snap
+++ b/spec/frontend/vue_shared/components/blob_viewers/__snapshots__/simple_viewer_spec.js.snap
@@ -59,7 +59,7 @@ exports[`Blob Simple Viewer component rendering matches the snapshot 1`] = `
class="code highlight"
>
<code
- id="blob-code-content"
+ data-blob-hash="foo-bar"
>
<span
id="LC1"
diff --git a/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js b/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js
index 79195aa1350..8434fdaccde 100644
--- a/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js
@@ -5,9 +5,13 @@ import { HIGHLIGHT_CLASS_NAME } from '~/vue_shared/components/blob_viewers/const
describe('Blob Simple Viewer component', () => {
let wrapper;
const contentMock = `<span id="LC1">First</span>\n<span id="LC2">Second</span>\n<span id="LC3">Third</span>`;
+ const blobHash = 'foo-bar';
function createComponent(content = contentMock) {
wrapper = shallowMount(SimpleViewer, {
+ provide: {
+ blobHash,
+ },
propsData: {
content,
type: 'text',
diff --git a/spec/frontend/vue_shared/components/confirm_modal_spec.js b/spec/frontend/vue_shared/components/confirm_modal_spec.js
index 8456ca9d125..96ccf56cbc6 100644
--- a/spec/frontend/vue_shared/components/confirm_modal_spec.js
+++ b/spec/frontend/vue_shared/components/confirm_modal_spec.js
@@ -62,7 +62,7 @@ describe('vue_shared/components/confirm_modal', () => {
wrapper.vm.modalAttributes = MOCK_MODAL_DATA.modalAttributes;
});
- it('renders GlModal wtih data', () => {
+ it('renders GlModal with data', () => {
expect(findModal().exists()).toBeTruthy();
expect(findModal().attributes()).toEqual(
expect.objectContaining({
@@ -72,6 +72,24 @@ describe('vue_shared/components/confirm_modal', () => {
);
});
});
+
+ describe.each`
+ desc | attrs | expectation
+ ${'when message is simple text'} | ${{}} | ${`<div>${MOCK_MODAL_DATA.modalAttributes.message}</div>`}
+ ${'when message has html'} | ${{ messageHtml: '<p>Header</p><ul onhover="alert(1)"><li>First</li></ul>' }} | ${'<p>Header</p><ul><li>First</li></ul>'}
+ `('$desc', ({ attrs, expectation }) => {
+ beforeEach(() => {
+ createComponent();
+ wrapper.vm.modalAttributes = {
+ ...MOCK_MODAL_DATA.modalAttributes,
+ ...attrs,
+ };
+ });
+
+ it('renders message', () => {
+ expect(findForm().element.innerHTML).toContain(expectation);
+ });
+ });
});
describe('methods', () => {
diff --git a/spec/frontend/vue_shared/components/dropdown/dropdown_button_spec.js b/spec/frontend/vue_shared/components/dropdown/dropdown_button_spec.js
index 892a96b76fd..08e5d828b8f 100644
--- a/spec/frontend/vue_shared/components/dropdown/dropdown_button_spec.js
+++ b/spec/frontend/vue_shared/components/dropdown/dropdown_button_spec.js
@@ -60,10 +60,9 @@ describe('DropdownButtonComponent', () => {
});
it('renders dropdown button icon', () => {
- const dropdownIconEl = vm.$el.querySelector('.dropdown-toggle-icon i.fa');
+ const dropdownIconEl = vm.$el.querySelector('[data-testid="chevron-down-icon"]');
expect(dropdownIconEl).not.toBeNull();
- expect(dropdownIconEl.classList.contains('fa-chevron-down')).toBe(true);
});
it('renders slot, if default slot exists', () => {
diff --git a/spec/frontend/vue_shared/components/file_row_spec.js b/spec/frontend/vue_shared/components/file_row_spec.js
index d28c35d26bf..bd6a18bf704 100644
--- a/spec/frontend/vue_shared/components/file_row_spec.js
+++ b/spec/frontend/vue_shared/components/file_row_spec.js
@@ -3,6 +3,7 @@ import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import FileRow from '~/vue_shared/components/file_row.vue';
import FileHeader from '~/vue_shared/components/file_row_header.vue';
+import FileIcon from '~/vue_shared/components/file_icon.vue';
import { escapeFileUrl } from '~/lib/utils/url_utility';
describe('File row component', () => {
@@ -151,4 +152,18 @@ describe('File row component', () => {
expect(wrapper.find('.file-row-name').classes()).toContain('font-weight-bold');
});
+
+ it('renders submodule icon', () => {
+ const submodule = true;
+
+ createComponent({
+ file: {
+ ...file(),
+ submodule,
+ },
+ level: 0,
+ });
+
+ expect(wrapper.find(FileIcon).props('submodule')).toBe(submodule);
+ });
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
index c79880d4766..64bfff3dfa1 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
@@ -1,5 +1,12 @@
import { shallowMount, mount } from '@vue/test-utils';
-import { GlFilteredSearch, GlButtonGroup, GlButton, GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import {
+ GlFilteredSearch,
+ GlButtonGroup,
+ GlButton,
+ GlDropdown,
+ GlDropdownItem,
+ GlFormCheckbox,
+} from '@gitlab/ui';
import FilteredSearchBarRoot from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import { uniqueTokens } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
@@ -30,6 +37,8 @@ const createComponent = ({
recentSearchesStorageKey = 'requirements',
tokens = mockAvailableTokens,
sortOptions,
+ showCheckbox = false,
+ checkboxChecked = false,
searchInputPlaceholder = 'Filter requirements',
} = {}) => {
const mountMethod = shallow ? shallowMount : mount;
@@ -40,6 +49,8 @@ const createComponent = ({
recentSearchesStorageKey,
tokens,
sortOptions,
+ showCheckbox,
+ checkboxChecked,
searchInputPlaceholder,
},
});
@@ -364,6 +375,26 @@ describe('FilteredSearchBarRoot', () => {
expect(glFilteredSearchEl.props('historyItems')).toEqual(mockHistoryItems);
});
+ it('renders checkbox when `showCheckbox` prop is true', async () => {
+ let wrapperWithCheckbox = createComponent({
+ showCheckbox: true,
+ });
+
+ expect(wrapperWithCheckbox.find(GlFormCheckbox).exists()).toBe(true);
+ expect(wrapperWithCheckbox.find(GlFormCheckbox).attributes('checked')).not.toBeDefined();
+
+ wrapperWithCheckbox.destroy();
+
+ wrapperWithCheckbox = createComponent({
+ showCheckbox: true,
+ checkboxChecked: true,
+ });
+
+ expect(wrapperWithCheckbox.find(GlFormCheckbox).attributes('checked')).toBe('true');
+
+ wrapperWithCheckbox.destroy();
+ });
+
it('renders search history items dropdown with formatting done using token symbols', async () => {
const wrapperFullMount = createComponent({ sortOptions: mockSortOptions, shallow: false });
wrapperFullMount.vm.recentSearchesStore.addRecentSearch(mockHistoryItems[0]);
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js
index 72840ce381f..3fd1d8b7f42 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js
@@ -45,6 +45,7 @@ function createComponent(options = {}) {
provide: {
portalName: 'fake target',
alignSuggestions: function fakeAlignSuggestions() {},
+ suggestionsListClass: 'custom-class',
},
stubs,
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js
index 12b7fd58670..5b7f7d242e9 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js
@@ -45,6 +45,7 @@ function createComponent(options = {}) {
provide: {
portalName: 'fake target',
alignSuggestions: function fakeAlignSuggestions() {},
+ suggestionsListClass: 'custom-class',
},
stubs,
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
index 3feb05bab35..74172db81c2 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
@@ -50,6 +50,7 @@ function createComponent(options = {}) {
provide: {
portalName: 'fake target',
alignSuggestions: function fakeAlignSuggestions() {},
+ suggestionsListClass: 'custom-class',
},
stubs,
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js
index 0ec814e3f15..67f9a9c70cc 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js
@@ -48,6 +48,7 @@ function createComponent(options = {}) {
provide: {
portalName: 'fake target',
alignSuggestions: function fakeAlignSuggestions() {},
+ suggestionsListClass: 'custom-class',
},
stubs,
});
@@ -120,7 +121,9 @@ describe('MilestoneToken', () => {
wrapper.vm.fetchMilestoneBySearchTerm('foo');
return waitForPromises().then(() => {
- expect(createFlash).toHaveBeenCalledWith('There was a problem fetching milestones.');
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'There was a problem fetching milestones.',
+ });
});
});
diff --git a/spec/frontend/vue_shared/components/integration_help_text_spec.js b/spec/frontend/vue_shared/components/integration_help_text_spec.js
new file mode 100644
index 00000000000..4269d36d0e2
--- /dev/null
+++ b/spec/frontend/vue_shared/components/integration_help_text_spec.js
@@ -0,0 +1,57 @@
+import { shallowMount } from '@vue/test-utils';
+
+import { GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
+import IntegrationHelpText from '~/vue_shared/components/integrations_help_text.vue';
+
+describe('IntegrationHelpText component', () => {
+ let wrapper;
+ const defaultProps = {
+ message: 'Click %{linkStart}Bar%{linkEnd}!',
+ messageUrl: 'http://bar.com',
+ };
+
+ function createComponent(props = {}) {
+ return shallowMount(IntegrationHelpText, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ stubs: {
+ GlSprintf,
+ },
+ });
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('should use the gl components', () => {
+ wrapper = createComponent();
+
+ expect(wrapper.find(GlSprintf).exists()).toBe(true);
+ expect(wrapper.find(GlIcon).exists()).toBe(true);
+ expect(wrapper.find(GlLink).exists()).toBe(true);
+ });
+
+ it('should render the help text', () => {
+ wrapper = createComponent();
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('should not use the gl-link and gl-icon components', () => {
+ wrapper = createComponent({ message: 'Click nowhere!' });
+
+ expect(wrapper.find(GlSprintf).exists()).toBe(true);
+ expect(wrapper.find(GlIcon).exists()).toBe(false);
+ expect(wrapper.find(GlLink).exists()).toBe(false);
+ });
+
+ it('should not render the link when start and end is not provided', () => {
+ wrapper = createComponent({ message: 'Click nowhere!' });
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+});
diff --git a/spec/frontend/vue_shared/components/local_storage_sync_spec.js b/spec/frontend/vue_shared/components/local_storage_sync_spec.js
index efa9b5796fb..464fe3411dd 100644
--- a/spec/frontend/vue_shared/components/local_storage_sync_spec.js
+++ b/spec/frontend/vue_shared/components/local_storage_sync_spec.js
@@ -239,4 +239,30 @@ describe('Local Storage Sync', () => {
});
});
});
+
+ it('clears localStorage when clear property is true', async () => {
+ const storageKey = 'key';
+ const value = 'initial';
+
+ createComponent({
+ props: {
+ storageKey,
+ },
+ });
+ wrapper.setProps({
+ value,
+ });
+
+ await wrapper.vm.$nextTick();
+
+ expect(localStorage.getItem(storageKey)).toBe(value);
+
+ wrapper.setProps({
+ clear: true,
+ });
+
+ await wrapper.vm.$nextTick();
+
+ expect(localStorage.getItem(storageKey)).toBe(null);
+ });
});
diff --git a/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js b/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js
index b19e74b5b11..c0a000690f8 100644
--- a/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js
@@ -29,6 +29,10 @@ describe('Suggestion Diff component', () => {
});
};
+ beforeEach(() => {
+ window.gon.current_user_id = 1;
+ });
+
afterEach(() => {
wrapper.destroy();
});
@@ -71,6 +75,14 @@ describe('Suggestion Diff component', () => {
expect(addToBatchBtn.html().includes('Add suggestion to batch')).toBe(true);
});
+ it('does not render apply suggestion button with anonymous user', () => {
+ window.gon.current_user_id = null;
+
+ createComponent();
+
+ expect(findApplyButton().exists()).toBe(false);
+ });
+
describe('when apply suggestion is clicked', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/vue_shared/components/members/mock_data.js b/spec/frontend/vue_shared/components/members/mock_data.js
index d7bb8c0d142..5674929716d 100644
--- a/spec/frontend/vue_shared/components/members/mock_data.js
+++ b/spec/frontend/vue_shared/components/members/mock_data.js
@@ -3,6 +3,7 @@ export const member = {
canUpdate: false,
canRemove: false,
canOverride: false,
+ isOverridden: false,
accessLevel: { integerValue: 50, stringValue: 'Owner' },
source: {
id: 178,
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
new file mode 100644
index 00000000000..a1afdbc2b49
--- /dev/null
+++ b/spec/frontend/vue_shared/components/members/table/expiration_datepicker_spec.js
@@ -0,0 +1,166 @@
+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/members_table_spec.js b/spec/frontend/vue_shared/components/members/table/members_table_spec.js
index 20c1c26d2ee..e593e88438c 100644
--- a/spec/frontend/vue_shared/components/members/table/members_table_spec.js
+++ b/spec/frontend/vue_shared/components/members/table/members_table_spec.js
@@ -3,14 +3,16 @@ import Vuex from 'vuex';
import {
getByText as getByTextHelper,
getByTestId as getByTestIdHelper,
+ within,
} from '@testing-library/dom';
-import { GlBadge } from '@gitlab/ui';
+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';
@@ -26,7 +28,12 @@ describe('MemberList', () => {
state: {
members: [],
tableFields: [],
+ tableAttrs: {
+ table: { 'data-qa-selector': 'members_list' },
+ tr: { 'data-qa-selector': 'member_row' },
+ },
sourceId: 1,
+ currentUserId: 1,
...state,
},
});
@@ -44,6 +51,7 @@ describe('MemberList', () => {
'member-action-buttons',
'role-dropdown',
'remove-group-link-modal',
+ 'expiration-datepicker',
],
});
};
@@ -54,18 +62,24 @@ describe('MemberList', () => {
const getByTestId = (id, options) =>
createWrapper(getByTestIdHelper(wrapper.element, id, options));
+ const findTable = () => wrapper.find(GlTable);
+
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('fields', () => {
- const memberCanUpdate = {
+ const directMember = {
...memberMock,
- canUpdate: true,
source: { ...memberMock.source, id: 1 },
};
+ const memberCanUpdate = {
+ ...directMember,
+ canUpdate: true,
+ };
+
it.each`
field | label | member | expectedComponent
${'account'} | ${'Account'} | ${memberMock} | ${MemberAvatar}
@@ -75,7 +89,7 @@ describe('MemberList', () => {
${'requested'} | ${'Requested'} | ${accessRequest} | ${CreatedAt}
${'expires'} | ${'Access expires'} | ${memberMock} | ${ExpiresAt}
${'maxRole'} | ${'Max role'} | ${memberCanUpdate} | ${RoleDropdown}
- ${'expiration'} | ${'Expiration'} | ${memberMock} | ${null}
+ ${'expiration'} | ${'Expiration'} | ${memberMock} | ${ExpirationDatepicker}
`('renders the $label field', ({ field, label, member, expectedComponent }) => {
createComponent({
members: [member],
@@ -94,19 +108,60 @@ describe('MemberList', () => {
}
});
- it('renders "Actions" field for screen readers', () => {
- createComponent({ members: [memberMock], tableFields: ['actions'] });
+ describe('"Actions" field', () => {
+ it('renders "Actions" field for screen readers', () => {
+ createComponent({ members: [memberCanUpdate], tableFields: ['actions'] });
- const actionField = getByTestId('col-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);
+ 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);
+ });
+ });
});
});
@@ -138,4 +193,20 @@ describe('MemberList', () => {
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
index 1e47953a510..55ec7000693 100644
--- a/spec/frontend/vue_shared/components/members/table/role_dropdown_spec.js
+++ b/spec/frontend/vue_shared/components/members/table/role_dropdown_spec.js
@@ -30,6 +30,7 @@ describe('RoleDropdown', () => {
wrapper = mount(RoleDropdown, {
propsData: {
member,
+ permissions: {},
...propsData,
},
localVue,
@@ -115,11 +116,11 @@ describe('RoleDropdown', () => {
await nextTick();
- expect(findDropdown().attributes('disabled')).toBe('disabled');
+ expect(findDropdown().props('disabled')).toBe(true);
await waitForPromises();
- expect(findDropdown().attributes('disabled')).toBeUndefined();
+ expect(findDropdown().props('disabled')).toBe(false);
});
});
});
diff --git a/spec/frontend/vue_shared/components/members/utils_spec.js b/spec/frontend/vue_shared/components/members/utils_spec.js
index f183abc08d6..3f2b2097133 100644
--- a/spec/frontend/vue_shared/components/members/utils_spec.js
+++ b/spec/frontend/vue_shared/components/members/utils_spec.js
@@ -1,5 +1,19 @@
-import { generateBadges } from '~/vue_shared/components/members/utils';
-import { member as memberMock } from './mock_data';
+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', () => {
@@ -26,4 +40,83 @@ describe('Members Utils', () => {
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/modal_copy_button_spec.js b/spec/frontend/vue_shared/components/modal_copy_button_spec.js
index e5a8860f42e..ca9f8ff54d4 100644
--- a/spec/frontend/vue_shared/components/modal_copy_button_spec.js
+++ b/spec/frontend/vue_shared/components/modal_copy_button_spec.js
@@ -1,9 +1,7 @@
-import Vue from 'vue';
-import { shallowMount } from '@vue/test-utils';
-import modalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
+import { shallowMount, createWrapper } from '@vue/test-utils';
+import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
describe('modal copy button', () => {
- const Component = Vue.extend(modalCopyButton);
let wrapper;
afterEach(() => {
@@ -11,16 +9,18 @@ describe('modal copy button', () => {
});
beforeEach(() => {
- wrapper = shallowMount(Component, {
+ wrapper = shallowMount(ModalCopyButton, {
propsData: {
text: 'copy me',
title: 'Copy this value',
+ id: 'test-id',
},
});
});
describe('clipboard', () => {
it('should fire a `success` event on click', () => {
+ const root = createWrapper(wrapper.vm.$root);
document.execCommand = jest.fn(() => true);
window.getSelection = jest.fn(() => ({
toString: jest.fn(() => 'test'),
@@ -31,6 +31,7 @@ describe('modal copy button', () => {
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.emitted().success).not.toBeEmpty();
expect(document.execCommand).toHaveBeenCalledWith('copy');
+ expect(root.emitted('bv::hide::tooltip')).toEqual([['test-id']]);
});
});
it("should propagate the clipboard error event if execCommand doesn't work", () => {
diff --git a/spec/frontend/vue_shared/components/multiselect_dropdown_spec.js b/spec/frontend/vue_shared/components/multiselect_dropdown_spec.js
new file mode 100644
index 00000000000..233c488b60b
--- /dev/null
+++ b/spec/frontend/vue_shared/components/multiselect_dropdown_spec.js
@@ -0,0 +1,31 @@
+import { shallowMount } from '@vue/test-utils';
+import { getByText } from '@testing-library/dom';
+import MultiSelectDropdown from '~/vue_shared/components/sidebar/multiselect_dropdown.vue';
+
+describe('MultiSelectDropdown Component', () => {
+ it('renders items slot', () => {
+ const wrapper = shallowMount(MultiSelectDropdown, {
+ propsData: {
+ text: '',
+ headerText: '',
+ },
+ slots: {
+ items: '<p>Test</p>',
+ },
+ });
+ expect(getByText(wrapper.element, 'Test')).toBeDefined();
+ });
+
+ it('renders search slot', () => {
+ const wrapper = shallowMount(MultiSelectDropdown, {
+ propsData: {
+ text: '',
+ headerText: '',
+ },
+ slots: {
+ search: '<p>Search</p>',
+ },
+ });
+ expect(getByText(wrapper.element, 'Search')).toBeDefined();
+ });
+});
diff --git a/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js b/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js
index d943aaf3e5f..0f7c8e97635 100644
--- a/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js
+++ b/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js
@@ -70,7 +70,7 @@ describe('AlertManagementEmptyState', () => {
...props,
},
slots: {
- 'emtpy-state': EmptyStateSlot,
+ 'empty-state': EmptyStateSlot,
'header-actions': HeaderActionsSlot,
title: TitleSlot,
table: TableSlot,
diff --git a/spec/frontend/vue_shared/components/registry/title_area_spec.js b/spec/frontend/vue_shared/components/registry/title_area_spec.js
index 5cb606b58d9..b743a663f06 100644
--- a/spec/frontend/vue_shared/components/registry/title_area_spec.js
+++ b/spec/frontend/vue_shared/components/registry/title_area_spec.js
@@ -5,12 +5,16 @@ import component from '~/vue_shared/components/registry/title_area.vue';
describe('title area', () => {
let wrapper;
+ const DYNAMIC_SLOT = 'metadata-dynamic-slot';
+
const findSubHeaderSlot = () => wrapper.find('[data-testid="sub-header"]');
const findRightActionsSlot = () => wrapper.find('[data-testid="right-actions"]');
const findMetadataSlot = name => wrapper.find(`[data-testid="${name}"]`);
const findTitle = () => wrapper.find('[data-testid="title"]');
const findAvatar = () => wrapper.find(GlAvatar);
const findInfoMessages = () => wrapper.findAll('[data-testid="info-message"]');
+ const findDynamicSlot = () => wrapper.find(`[data-testid="${DYNAMIC_SLOT}`);
+ const findSlotOrderElements = () => wrapper.findAll('[slot-test]');
const mountComponent = ({ propsData = { title: 'foo' }, slots } = {}) => {
wrapper = shallowMount(component, {
@@ -98,6 +102,59 @@ describe('title area', () => {
});
});
+ describe('dynamic slots', () => {
+ const createDynamicSlot = () => {
+ return wrapper.vm.$createElement('div', {
+ attrs: {
+ 'data-testid': DYNAMIC_SLOT,
+ 'slot-test': true,
+ },
+ });
+ };
+ it('shows dynamic slots', async () => {
+ mountComponent();
+ // we manually add a new slot to simulate dynamic slots being evaluated after the initial mount
+ wrapper.vm.$slots[DYNAMIC_SLOT] = createDynamicSlot();
+
+ await wrapper.vm.$nextTick();
+ expect(findDynamicSlot().exists()).toBe(false);
+
+ await wrapper.vm.$nextTick();
+ expect(findDynamicSlot().exists()).toBe(true);
+ });
+
+ it('preserve the order of the slots', async () => {
+ mountComponent({
+ slots: {
+ 'metadata-foo': '<div slot-test data-testid="metadata-foo"></div>',
+ },
+ });
+
+ // rewrite slot putting dynamic slot as first
+ wrapper.vm.$slots = {
+ 'metadata-dynamic-slot': createDynamicSlot(),
+ 'metadata-foo': wrapper.vm.$slots['metadata-foo'],
+ };
+
+ await wrapper.vm.$nextTick();
+ expect(findDynamicSlot().exists()).toBe(false);
+ expect(findMetadataSlot('metadata-foo').exists()).toBe(true);
+
+ await wrapper.vm.$nextTick();
+
+ expect(
+ findSlotOrderElements()
+ .at(0)
+ .attributes('data-testid'),
+ ).toBe(DYNAMIC_SLOT);
+ expect(
+ findSlotOrderElements()
+ .at(1)
+ .attributes('data-testid'),
+ ).toBe('metadata-foo');
+ });
+ });
+
describe('info-messages', () => {
it('shows a message when the props contains one', () => {
mountComponent({ propsData: { infoMessages: [{ text: 'foo foo bar bar' }] } });
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/editor_service_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/editor_service_spec.js
index 0f2f263a776..d79df4d0557 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/editor_service_spec.js
+++ b/spec/frontend/vue_shared/components/rich_content_editor/editor_service_spec.js
@@ -91,12 +91,25 @@ describe('Editor Service', () => {
});
describe('addImage', () => {
- it('calls the exec method on the instance', () => {
- const mockImage = { imageUrl: 'some/url.png', description: 'some description' };
+ const file = new File([], 'some-file.jpg');
+ const mockImage = { imageUrl: 'some/url.png', altText: 'some alt text' };
- addImage(mockInstance, mockImage);
+ it('calls the insertElement method on the squire instance when in WYSIWYG mode', () => {
+ jest.spyOn(URL, 'createObjectURL');
+ mockInstance.editor.isWysiwygMode.mockReturnValue(true);
+ mockInstance.editor.getSquire.mockReturnValue({ insertElement: jest.fn() });
- expect(mockInstance.editor.exec).toHaveBeenCalledWith('AddImage', mockImage);
+ addImage(mockInstance, mockImage, file);
+
+ expect(mockInstance.editor.getSquire().insertElement).toHaveBeenCalled();
+ expect(global.URL.createObjectURL).toHaveBeenLastCalledWith(file);
+ });
+
+ it('calls the insertText method on the instance when in Markdown mode', () => {
+ mockInstance.editor.isWysiwygMode.mockReturnValue(false);
+ addImage(mockInstance, mockImage, file);
+
+ expect(mockInstance.editor.insertText).toHaveBeenCalledWith('![some alt text](some/url.png)');
});
});
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal_spec.js
index 0c2ac53aa52..16370a7aaad 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal_spec.js
+++ b/spec/frontend/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal_spec.js
@@ -15,10 +15,7 @@ describe('Add Image Modal', () => {
const findDescriptionInput = () => wrapper.find({ ref: 'descriptionInput' });
beforeEach(() => {
- wrapper = shallowMount(AddImageModal, {
- provide: { glFeatures: { sseImageUploads: true } },
- propsData,
- });
+ wrapper = shallowMount(AddImageModal, { propsData });
});
describe('when content is loaded', () => {
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js
index 8c2c0413819..d50cf2915e8 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js
+++ b/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js
@@ -180,7 +180,7 @@ describe('Rich Content Editor', () => {
wrapper.vm.$refs.editor = mockInstance;
findAddImageModal().vm.$emit('addImage', mockImage);
- expect(addImage).toHaveBeenCalledWith(mockInstance, mockImage);
+ expect(addImage).toHaveBeenCalledWith(mockInstance, mockImage, undefined);
});
});
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer_spec.js
index fd745c21bb6..85516eae4cf 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer_spec.js
+++ b/spec/frontend/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer_spec.js
@@ -189,4 +189,30 @@ describe('rich_content_editor/services/html_to_markdown_renderer', () => {
expect(htmlToMarkdownRenderer['PRE CODE'](node, subContent)).toBe(originalConverterResult);
});
});
+
+ describe('IMG', () => {
+ const originalSrc = 'path/to/image.png';
+ const alt = 'alt text';
+ let node;
+
+ beforeEach(() => {
+ node = document.createElement('img');
+ node.alt = alt;
+ node.src = originalSrc;
+ });
+
+ it('returns an image with its original src of the `original-src` attribute is preset', () => {
+ node.dataset.originalSrc = originalSrc;
+ node.src = 'modified/path/to/image.png';
+
+ htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer);
+
+ expect(htmlToMarkdownRenderer.IMG(node)).toBe(`![${alt}](${originalSrc})`);
+ });
+
+ it('fallback to `src` if no `original-src` is specified on the image', () => {
+ htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer);
+ expect(htmlToMarkdownRenderer.IMG(node)).toBe(`![${alt}](${originalSrc})`);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/runner_instructions/mock_data.js b/spec/frontend/vue_shared/components/runner_instructions/mock_data.js
new file mode 100644
index 00000000000..01f7f3d49c7
--- /dev/null
+++ b/spec/frontend/vue_shared/components/runner_instructions/mock_data.js
@@ -0,0 +1,107 @@
+export const mockGraphqlRunnerPlatforms = {
+ data: {
+ runnerPlatforms: {
+ nodes: [
+ {
+ name: 'linux',
+ humanReadableName: 'Linux',
+ architectures: {
+ nodes: [
+ {
+ name: 'amd64',
+ downloadLocation:
+ 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64',
+ __typename: 'RunnerArchitecture',
+ },
+ {
+ name: '386',
+ downloadLocation:
+ 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-386',
+ __typename: 'RunnerArchitecture',
+ },
+ {
+ name: 'arm',
+ downloadLocation:
+ 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-arm',
+ __typename: 'RunnerArchitecture',
+ },
+ {
+ name: 'arm64',
+ downloadLocation:
+ 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-arm64',
+ __typename: 'RunnerArchitecture',
+ },
+ ],
+ __typename: 'RunnerArchitectureConnection',
+ },
+ __typename: 'RunnerPlatform',
+ },
+ {
+ name: 'osx',
+ humanReadableName: 'macOS',
+ architectures: {
+ nodes: [
+ {
+ name: 'amd64',
+ downloadLocation:
+ 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-darwin-amd64',
+ __typename: 'RunnerArchitecture',
+ },
+ ],
+ __typename: 'RunnerArchitectureConnection',
+ },
+ __typename: 'RunnerPlatform',
+ },
+ {
+ name: 'windows',
+ humanReadableName: 'Windows',
+ architectures: {
+ nodes: [
+ {
+ name: 'amd64',
+ downloadLocation:
+ 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-windows-amd64.exe',
+ __typename: 'RunnerArchitecture',
+ },
+ {
+ name: '386',
+ downloadLocation:
+ 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-windows-386.exe',
+ __typename: 'RunnerArchitecture',
+ },
+ ],
+ __typename: 'RunnerArchitectureConnection',
+ },
+ __typename: 'RunnerPlatform',
+ },
+ {
+ name: 'docker',
+ humanReadableName: 'Docker',
+ architectures: null,
+ __typename: 'RunnerPlatform',
+ },
+ {
+ name: 'kubernetes',
+ humanReadableName: 'Kubernetes',
+ architectures: null,
+ __typename: 'RunnerPlatform',
+ },
+ ],
+ __typename: 'RunnerPlatformConnection',
+ },
+ project: { id: 'gid://gitlab/Project/1', __typename: 'Project' },
+ group: null,
+ },
+};
+
+export const mockGraphqlInstructions = {
+ data: {
+ runnerSetup: {
+ installInstructions:
+ "# Download the binary for your system\nsudo curl -L --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64\n\n# Give it permissions to execute\nsudo chmod +x /usr/local/bin/gitlab-runner\n\n# Create a GitLab CI user\nsudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash\n\n# Install and run as service\nsudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner\nsudo gitlab-runner start\n",
+ registerInstructions:
+ 'sudo gitlab-runner register --url http://192.168.1.81:3000/ --registration-token GE5gsjeep_HAtBf9s3Yz',
+ __typename: 'RunnerSetup',
+ },
+ },
+};
diff --git a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js
new file mode 100644
index 00000000000..afbcee506c7
--- /dev/null
+++ b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js
@@ -0,0 +1,119 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'jest/helpers/mock_apollo_helper';
+import RunnerInstructions from '~/vue_shared/components/runner_instructions/runner_instructions.vue';
+import getRunnerPlatforms from '~/vue_shared/components/runner_instructions/graphql/queries/get_runner_platforms.query.graphql';
+import getRunnerSetupInstructions from '~/vue_shared/components/runner_instructions/graphql/queries/get_runner_setup.query.graphql';
+
+import { mockGraphqlRunnerPlatforms, mockGraphqlInstructions } from './mock_data';
+
+const projectPath = 'gitlab-org/gitlab';
+const localVue = createLocalVue();
+localVue.use(VueApollo);
+
+describe('RunnerInstructions component', () => {
+ let wrapper;
+ let fakeApollo;
+
+ const findModalButton = () => wrapper.find('[data-testid="show-modal-button"]');
+ const findPlatformButtons = () => wrapper.findAll('[data-testid="platform-button"]');
+ const findArchitectureDropdownItems = () =>
+ wrapper.findAll('[data-testid="architecture-dropdown-item"]');
+ const findBinaryInstructionsSection = () => wrapper.find('[data-testid="binary-instructions"]');
+ const findRunnerInstructionsSection = () => wrapper.find('[data-testid="runner-instructions"]');
+
+ beforeEach(() => {
+ const requestHandlers = [
+ [getRunnerPlatforms, jest.fn().mockResolvedValue(mockGraphqlRunnerPlatforms)],
+ [getRunnerSetupInstructions, jest.fn().mockResolvedValue(mockGraphqlInstructions)],
+ ];
+
+ fakeApollo = createMockApollo(requestHandlers);
+
+ wrapper = shallowMount(RunnerInstructions, {
+ provide: {
+ projectPath,
+ },
+ localVue,
+ apolloProvider: fakeApollo,
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('should show the "Show Runner installation instructions" button', () => {
+ const button = findModalButton();
+
+ expect(button.exists()).toBe(true);
+ expect(button.text()).toBe('Show Runner installation instructions');
+ });
+
+ it('should contain a number of platforms buttons', () => {
+ const buttons = findPlatformButtons();
+
+ expect(buttons).toHaveLength(mockGraphqlRunnerPlatforms.data.runnerPlatforms.nodes.length);
+ });
+
+ it('should contain a number of dropdown items for the architecture options', () => {
+ const platformButton = findPlatformButtons().at(0);
+ platformButton.vm.$emit('click');
+
+ return wrapper.vm.$nextTick(() => {
+ const dropdownItems = findArchitectureDropdownItems();
+
+ expect(dropdownItems).toHaveLength(
+ mockGraphqlRunnerPlatforms.data.runnerPlatforms.nodes[0].architectures.nodes.length,
+ );
+ });
+ });
+
+ it('should display the binary installation instructions for a selected architecture', async () => {
+ const platformButton = findPlatformButtons().at(0);
+ platformButton.vm.$emit('click');
+
+ await wrapper.vm.$nextTick();
+
+ const dropdownItem = findArchitectureDropdownItems().at(0);
+ dropdownItem.vm.$emit('click');
+
+ await wrapper.vm.$nextTick();
+
+ const runner = findBinaryInstructionsSection();
+
+ expect(runner.text()).toEqual(
+ expect.stringContaining('sudo chmod +x /usr/local/bin/gitlab-runner'),
+ );
+ expect(runner.text()).toEqual(
+ expect.stringContaining(
+ `sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash`,
+ ),
+ );
+ expect(runner.text()).toEqual(
+ expect.stringContaining(
+ 'sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner',
+ ),
+ );
+ expect(runner.text()).toEqual(expect.stringContaining('sudo gitlab-runner start'));
+ });
+
+ it('should display the runner register instructions for a selected architecture', async () => {
+ const platformButton = findPlatformButtons().at(0);
+ platformButton.vm.$emit('click');
+
+ await wrapper.vm.$nextTick();
+
+ const dropdownItem = findArchitectureDropdownItems().at(0);
+ dropdownItem.vm.$emit('click');
+
+ await wrapper.vm.$nextTick();
+
+ const runner = findRunnerInstructionsSection();
+
+ expect(runner.text()).toEqual(
+ expect.stringContaining(mockGraphqlInstructions.data.runnerSetup.registerInstructions),
+ );
+ });
+});
diff --git a/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js b/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js
new file mode 100644
index 00000000000..a97e26caf53
--- /dev/null
+++ b/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js
@@ -0,0 +1,375 @@
+import { shallowMount } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
+import {
+ GlIcon,
+ GlLoadingIcon,
+ GlDropdown,
+ GlDropdownForm,
+ GlDropdownItem,
+ GlSearchBoxByType,
+ GlButton,
+} from '@gitlab/ui';
+
+import axios from '~/lib/utils/axios_utils';
+import IssuableMoveDropdown from '~/vue_shared/components/sidebar/issuable_move_dropdown.vue';
+
+const mockProjects = [
+ {
+ id: 2,
+ name_with_namespace: 'Gitlab Org / Gitlab Shell',
+ full_path: 'gitlab-org/gitlab-shell',
+ },
+ {
+ id: 3,
+ name_with_namespace: 'Gnuwget / Wget2',
+ full_path: 'gnuwget/wget2',
+ },
+ {
+ id: 4,
+ name_with_namespace: 'Commit451 / Lab Coat',
+ full_path: 'Commit451/lab-coat',
+ },
+];
+
+const mockProps = {
+ projectsFetchPath: '/-/autocomplete/projects?project_id=1',
+ dropdownButtonTitle: 'Move issuable',
+ dropdownHeaderTitle: 'Move issuable',
+ moveInProgress: false,
+};
+
+const mockEvent = {
+ stopPropagation: jest.fn(),
+ preventDefault: jest.fn(),
+};
+
+const createComponent = (propsData = mockProps) =>
+ shallowMount(IssuableMoveDropdown, {
+ propsData,
+ });
+
+describe('IssuableMoveDropdown', () => {
+ let mock;
+ let wrapper;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ wrapper = createComponent();
+ wrapper.vm.$refs.dropdown.hide = jest.fn();
+ wrapper.vm.$refs.searchInput.focusInput = jest.fn();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ mock.restore();
+ });
+
+ describe('watch', () => {
+ describe('searchKey', () => {
+ it('calls `fetchProjects` with value of the prop', async () => {
+ jest.spyOn(wrapper.vm, 'fetchProjects');
+ wrapper.setData({
+ searchKey: 'foo',
+ });
+
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.vm.fetchProjects).toHaveBeenCalledWith('foo');
+ });
+ });
+ });
+
+ describe('methods', () => {
+ describe('fetchProjects', () => {
+ it('sets projectsListLoading to true and projectsListLoadFailed to false', () => {
+ wrapper.vm.fetchProjects();
+
+ expect(wrapper.vm.projectsListLoading).toBe(true);
+ expect(wrapper.vm.projectsListLoadFailed).toBe(false);
+ });
+
+ it('calls `axios.get` with `projectsFetchPath` and query param `search`', () => {
+ jest.spyOn(axios, 'get').mockResolvedValue({
+ data: mockProjects,
+ });
+
+ wrapper.vm.fetchProjects('foo');
+
+ expect(axios.get).toHaveBeenCalledWith(
+ mockProps.projectsFetchPath,
+ expect.objectContaining({
+ params: {
+ search: 'foo',
+ },
+ }),
+ );
+ });
+
+ it('sets response to `projects` and focuses on searchInput when request is successful', async () => {
+ jest.spyOn(axios, 'get').mockResolvedValue({
+ data: mockProjects,
+ });
+
+ await wrapper.vm.fetchProjects('foo');
+
+ expect(wrapper.vm.projects).toBe(mockProjects);
+ expect(wrapper.vm.$refs.searchInput.focusInput).toHaveBeenCalled();
+ });
+
+ it('sets projectsListLoadFailed to true when request fails', async () => {
+ jest.spyOn(axios, 'get').mockRejectedValue({});
+
+ await wrapper.vm.fetchProjects('foo');
+
+ expect(wrapper.vm.projectsListLoadFailed).toBe(true);
+ });
+
+ it('sets projectsListLoading to false when request completes', async () => {
+ jest.spyOn(axios, 'get').mockResolvedValue({
+ data: mockProjects,
+ });
+
+ await wrapper.vm.fetchProjects('foo');
+
+ expect(wrapper.vm.projectsListLoading).toBe(false);
+ });
+ });
+
+ describe('isSelectedProject', () => {
+ it.each`
+ project | selectedProject | title | returnValue
+ ${mockProjects[0]} | ${mockProjects[0]} | ${'are same projects'} | ${true}
+ ${mockProjects[0]} | ${mockProjects[1]} | ${'are different projects'} | ${false}
+ `(
+ 'returns $returnValue when selectedProject and provided project param $title',
+ async ({ project, selectedProject, returnValue }) => {
+ wrapper.setData({
+ selectedProject,
+ });
+
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.vm.isSelectedProject(project)).toBe(returnValue);
+ },
+ );
+
+ it('returns false when selectedProject is null', async () => {
+ wrapper.setData({
+ selectedProject: null,
+ });
+
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.vm.isSelectedProject(mockProjects[0])).toBe(false);
+ });
+ });
+ });
+
+ describe('template', () => {
+ const findDropdownEl = () => wrapper.find(GlDropdown);
+
+ it('renders collapsed state element with icon', () => {
+ const collapsedEl = wrapper.find('[data-testid="move-collapsed"]');
+
+ expect(collapsedEl.exists()).toBe(true);
+ expect(collapsedEl.attributes('title')).toBe(mockProps.dropdownButtonTitle);
+ expect(collapsedEl.find(GlIcon).exists()).toBe(true);
+ expect(collapsedEl.find(GlIcon).props('name')).toBe('arrow-right');
+ });
+
+ describe('gl-dropdown component', () => {
+ it('renders component container element', () => {
+ expect(findDropdownEl().exists()).toBe(true);
+ expect(findDropdownEl().props('block')).toBe(true);
+ });
+
+ it('renders gl-dropdown-form component', () => {
+ expect(
+ findDropdownEl()
+ .find(GlDropdownForm)
+ .exists(),
+ ).toBe(true);
+ });
+
+ it('renders header element', () => {
+ const headerEl = findDropdownEl().find('[data-testid="header"]');
+
+ expect(headerEl.exists()).toBe(true);
+ expect(headerEl.find('span').text()).toBe(mockProps.dropdownHeaderTitle);
+ expect(headerEl.find(GlButton).props('icon')).toBe('close');
+ });
+
+ it('renders gl-search-box-by-type component', () => {
+ const searchEl = findDropdownEl().find(GlSearchBoxByType);
+
+ expect(searchEl.exists()).toBe(true);
+ expect(searchEl.attributes()).toMatchObject({
+ placeholder: 'Search project',
+ debounce: '300',
+ });
+ });
+
+ it('renders gl-loading-icon component when projectsListLoading prop is true', async () => {
+ wrapper.setData({
+ projectsListLoading: true,
+ });
+
+ await wrapper.vm.$nextTick();
+
+ expect(
+ findDropdownEl()
+ .find(GlLoadingIcon)
+ .exists(),
+ ).toBe(true);
+ });
+
+ it('renders gl-dropdown-item components for available projects', async () => {
+ wrapper.setData({
+ projects: mockProjects,
+ selectedProject: mockProjects[0],
+ });
+
+ await wrapper.vm.$nextTick();
+
+ const dropdownItems = wrapper.findAll(GlDropdownItem);
+
+ expect(dropdownItems).toHaveLength(mockProjects.length);
+ expect(dropdownItems.at(0).props()).toMatchObject({
+ isCheckItem: true,
+ isChecked: true,
+ });
+ expect(dropdownItems.at(0).text()).toBe(mockProjects[0].name_with_namespace);
+ });
+
+ it('renders string "No matching results" when search does not yield any matches', async () => {
+ wrapper.setData({
+ searchKey: 'foo',
+ });
+
+ // Wait for `searchKey` watcher to run.
+ await wrapper.vm.$nextTick();
+
+ wrapper.setData({
+ projects: [],
+ projectsListLoading: false,
+ });
+
+ await wrapper.vm.$nextTick();
+
+ const dropdownContentEl = wrapper.find('[data-testid="content"]');
+
+ expect(dropdownContentEl.text()).toContain('No matching results');
+ });
+
+ it('renders string "Failed to load projects" when loading projects list fails', async () => {
+ wrapper.setData({
+ projects: [],
+ projectsListLoading: false,
+ projectsListLoadFailed: true,
+ });
+
+ await wrapper.vm.$nextTick();
+
+ const dropdownContentEl = wrapper.find('[data-testid="content"]');
+
+ expect(dropdownContentEl.text()).toContain('Failed to load projects');
+ });
+
+ it('renders gl-button within footer', async () => {
+ const moveButtonEl = wrapper.find('[data-testid="footer"]').find(GlButton);
+
+ expect(moveButtonEl.text()).toBe('Move');
+ expect(moveButtonEl.attributes('disabled')).toBe('true');
+
+ wrapper.setData({
+ selectedProject: mockProjects[0],
+ });
+
+ await wrapper.vm.$nextTick();
+
+ expect(
+ wrapper
+ .find('[data-testid="footer"]')
+ .find(GlButton)
+ .attributes('disabled'),
+ ).not.toBeDefined();
+ });
+ });
+
+ describe('events', () => {
+ it('collapsed state element emits `toggle-collapse` event on component when clicked', () => {
+ wrapper.find('[data-testid="move-collapsed"]').trigger('click');
+
+ expect(wrapper.emitted('toggle-collapse')).toBeTruthy();
+ });
+
+ it('gl-dropdown component calls `fetchProjects` on `shown` event', () => {
+ jest.spyOn(axios, 'get').mockResolvedValue({
+ data: mockProjects,
+ });
+
+ findDropdownEl().vm.$emit('shown');
+
+ expect(axios.get).toHaveBeenCalled();
+ });
+
+ it('gl-dropdown component prevents dropdown body from closing on `hide` event when `projectItemClick` prop is true', async () => {
+ wrapper.setData({
+ projectItemClick: true,
+ });
+
+ findDropdownEl().vm.$emit('hide', mockEvent);
+
+ expect(mockEvent.preventDefault).toHaveBeenCalled();
+ expect(wrapper.vm.projectItemClick).toBe(false);
+ });
+
+ it('gl-dropdown component emits `dropdown-close` event on component from `hide` event', async () => {
+ findDropdownEl().vm.$emit('hide');
+
+ expect(wrapper.emitted('dropdown-close')).toBeTruthy();
+ });
+
+ it('close icon in dropdown header closes the dropdown when clicked', () => {
+ wrapper
+ .find('[data-testid="header"]')
+ .find(GlButton)
+ .vm.$emit('click', mockEvent);
+
+ expect(wrapper.vm.$refs.dropdown.hide).toHaveBeenCalled();
+ });
+
+ it('sets project for clicked gl-dropdown-item to selectedProject', async () => {
+ wrapper.setData({
+ projects: mockProjects,
+ });
+
+ await wrapper.vm.$nextTick();
+
+ wrapper
+ .findAll(GlDropdownItem)
+ .at(0)
+ .vm.$emit('click', mockEvent);
+
+ expect(wrapper.vm.selectedProject).toBe(mockProjects[0]);
+ });
+
+ it('hides dropdown and emits `move-issuable` event when move button is clicked', async () => {
+ wrapper.setData({
+ selectedProject: mockProjects[0],
+ });
+
+ await wrapper.vm.$nextTick();
+
+ wrapper
+ .find('[data-testid="footer"]')
+ .find(GlButton)
+ .vm.$emit('click');
+
+ expect(wrapper.vm.$refs.dropdown.hide).toHaveBeenCalled();
+ expect(wrapper.emitted('move-issuable')).toBeTruthy();
+ expect(wrapper.emitted('move-issuable')[0]).toEqual([mockProjects[0]]);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
index 7847e0ee71d..71c040c6633 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
@@ -81,9 +81,7 @@ describe('DropdownValueCollapsedComponent', () => {
describe('template', () => {
it('renders component container element with tooltip`', () => {
- expect(vm.$el.dataset.placement).toBe('left');
- expect(vm.$el.dataset.container).toBe('body');
- expect(vm.$el.dataset.originalTitle).toBe(vm.labelsList);
+ expect(vm.$el.title).toBe(vm.labelsList);
});
it('renders tags icon element', () => {
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js
index e8a126d8774..78367b3a5b4 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js
@@ -128,6 +128,16 @@ describe('DropdownContentsLabelsView', () => {
});
});
+ describe('handleComponentAppear', () => {
+ it('calls `focusInput` on searchInput field', async () => {
+ wrapper.vm.$refs.searchInput.focusInput = jest.fn();
+
+ await wrapper.vm.handleComponentAppear();
+
+ expect(wrapper.vm.$refs.searchInput.focusInput).toHaveBeenCalled();
+ });
+ });
+
describe('handleComponentDisappear', () => {
it('calls action `receiveLabelsSuccess` with empty array', () => {
jest.spyOn(wrapper.vm, 'receiveLabelsSuccess');
@@ -301,7 +311,6 @@ describe('DropdownContentsLabelsView', () => {
const searchInputEl = wrapper.find(GlSearchBoxByType);
expect(searchInputEl.exists()).toBe(true);
- expect(searchInputEl.attributes('autofocus')).toBe('true');
});
it('renders label elements for all labels', () => {
diff --git a/spec/frontend/vue_shared/components/stacked_progress_bar_spec.js b/spec/frontend/vue_shared/components/stacked_progress_bar_spec.js
index bc86ee5a0c6..0786882f527 100644
--- a/spec/frontend/vue_shared/components/stacked_progress_bar_spec.js
+++ b/spec/frontend/vue_shared/components/stacked_progress_bar_spec.js
@@ -29,6 +29,13 @@ describe('StackedProgressBarComponent', () => {
vm.$destroy();
});
+ const findSuccessBarText = wrapper => wrapper.$el.querySelector('.status-green').innerText.trim();
+ const findNeutralBarText = wrapper =>
+ wrapper.$el.querySelector('.status-neutral').innerText.trim();
+ const findFailureBarText = wrapper => wrapper.$el.querySelector('.status-red').innerText.trim();
+ const findUnavailableBarText = wrapper =>
+ wrapper.$el.querySelector('.status-unavailable').innerText.trim();
+
describe('computed', () => {
describe('neutralCount', () => {
it('returns neutralCount based on totalCount, successCount and failureCount', () => {
@@ -37,24 +44,54 @@ describe('StackedProgressBarComponent', () => {
});
});
- describe('methods', () => {
+ describe('template', () => {
+ it('renders container element', () => {
+ expect(vm.$el.classList.contains('stacked-progress-bar')).toBeTruthy();
+ });
+
+ it('renders empty state when count is unavailable', () => {
+ const vmX = createComponent({ totalCount: 0, successCount: 0, failureCount: 0 });
+
+ expect(findUnavailableBarText(vmX)).not.toBeUndefined();
+ });
+
+ it('renders bar elements when count is available', () => {
+ expect(findSuccessBarText(vm)).not.toBeUndefined();
+ expect(findNeutralBarText(vm)).not.toBeUndefined();
+ expect(findFailureBarText(vm)).not.toBeUndefined();
+ });
+
describe('getPercent', () => {
- it('returns percentage from provided count based on `totalCount`', () => {
- expect(vm.getPercent(500)).toBe(10);
+ it('returns correct percentages from provided count based on `totalCount`', () => {
+ vm = createComponent({ totalCount: 100, successCount: 25, failureCount: 10 });
+
+ expect(findSuccessBarText(vm)).toBe('25%');
+ expect(findNeutralBarText(vm)).toBe('65%');
+ expect(findFailureBarText(vm)).toBe('10%');
});
- it('returns percentage with decimal place from provided count based on `totalCount`', () => {
- expect(vm.getPercent(67)).toBe(1.3);
+ it('returns percentage with decimal place when decimal is greater than 1', () => {
+ vm = createComponent({ successCount: 67 });
+
+ expect(findSuccessBarText(vm)).toBe('1.3%');
});
- it('returns percentage as `< 1` from provided count based on `totalCount` when evaluated value is less than 1', () => {
- expect(vm.getPercent(10)).toBe('< 1');
+ it('returns percentage as `< 1%` from provided count based on `totalCount` when evaluated value is less than 1', () => {
+ vm = createComponent({ successCount: 10 });
+
+ expect(findSuccessBarText(vm)).toBe('< 1%');
});
- it('returns 0 if totalCount is falsy', () => {
+ it('returns not available if totalCount is falsy', () => {
vm = createComponent({ totalCount: 0 });
- expect(vm.getPercent(100)).toBe(0);
+ expect(findUnavailableBarText(vm)).toBe('Not available');
+ });
+
+ it('returns 99.9% when numbers are extreme decimals', () => {
+ vm = createComponent({ totalCount: 1000000 });
+
+ expect(findNeutralBarText(vm)).toBe('99.9%');
});
});
@@ -82,23 +119,4 @@ describe('StackedProgressBarComponent', () => {
});
});
});
-
- describe('template', () => {
- it('renders container element', () => {
- expect(vm.$el.classList.contains('stacked-progress-bar')).toBeTruthy();
- });
-
- it('renders empty state when count is unavailable', () => {
- const vmX = createComponent({ totalCount: 0, successCount: 0, failureCount: 0 });
-
- expect(vmX.$el.querySelectorAll('.status-unavailable').length).not.toBe(0);
- vmX.$destroy();
- });
-
- it('renders bar elements when count is available', () => {
- expect(vm.$el.querySelectorAll('.status-green').length).not.toBe(0);
- expect(vm.$el.querySelectorAll('.status-neutral').length).not.toBe(0);
- expect(vm.$el.querySelectorAll('.status-red').length).not.toBe(0);
- });
- });
});
diff --git a/spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap b/spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap
new file mode 100644
index 00000000000..d2fe3cd76cb
--- /dev/null
+++ b/spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap
@@ -0,0 +1,608 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Upload dropzone component correctly overrides description and drop messages 1`] = `
+<div
+ class="gl-w-full gl-relative"
+>
+ <button
+ class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3"
+ >
+ <div
+ class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column"
+ data-testid="dropzone-area"
+ >
+ <gl-icon-stub
+ class="gl-mb-2"
+ name="upload"
+ size="24"
+ />
+
+ <p
+ class="gl-mb-0"
+ >
+ <span>
+ Test %{linkStart}description%{linkEnd} message.
+ </span>
+ </p>
+ </div>
+ </button>
+
+ <input
+ accept="image/jpg,image/jpeg"
+ class="hide"
+ multiple="multiple"
+ name="upload_file"
+ type="file"
+ />
+
+ <transition-stub
+ name="upload-dropzone-fade"
+ >
+ <div
+ class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-3 gl-bg-white"
+ style="display: none;"
+ >
+ <div
+ class="mw-50 gl-text-center"
+ >
+ <h3
+ class=""
+ >
+
+ Oh no!
+
+ </h3>
+
+ <span>
+ You are trying to upload something other than an image. Please upload a .png, .jpg, .jpeg, .gif, .bmp, .tiff or .ico.
+ </span>
+ </div>
+
+ <div
+ class="mw-50 gl-text-center"
+ style="display: none;"
+ >
+ <h3
+ class=""
+ >
+
+ Incoming!
+
+ </h3>
+
+ <span>
+ Test drop-to-start message.
+ </span>
+ </div>
+ </div>
+ </transition-stub>
+</div>
+`;
+
+exports[`Upload dropzone component when dragging renders correct template when drag event contains files 1`] = `
+<div
+ class="gl-w-full gl-relative"
+>
+ <button
+ class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3"
+ >
+ <div
+ class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column"
+ data-testid="dropzone-area"
+ >
+ <gl-icon-stub
+ class="gl-mb-2"
+ name="upload"
+ size="24"
+ />
+
+ <p
+ class="gl-mb-0"
+ >
+ <gl-sprintf-stub
+ message="Drop or %{linkStart}upload%{linkEnd} files to attach"
+ />
+ </p>
+ </div>
+ </button>
+
+ <input
+ accept="image/*"
+ class="hide"
+ multiple="multiple"
+ name="upload_file"
+ type="file"
+ />
+
+ <transition-stub
+ name="upload-dropzone-fade"
+ >
+ <div
+ class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-3 gl-bg-white"
+ style=""
+ >
+ <div
+ class="mw-50 gl-text-center"
+ style="display: none;"
+ >
+ <h3
+ class=""
+ >
+
+ Oh no!
+
+ </h3>
+
+ <span>
+ You are trying to upload something other than an image. Please upload a .png, .jpg, .jpeg, .gif, .bmp, .tiff or .ico.
+ </span>
+ </div>
+
+ <div
+ class="mw-50 gl-text-center"
+ style=""
+ >
+ <h3
+ class=""
+ >
+
+ Incoming!
+
+ </h3>
+
+ <span>
+ Drop your files to start your upload.
+ </span>
+ </div>
+ </div>
+ </transition-stub>
+</div>
+`;
+
+exports[`Upload dropzone component when dragging renders correct template when drag event contains files and text 1`] = `
+<div
+ class="gl-w-full gl-relative"
+>
+ <button
+ class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3"
+ >
+ <div
+ class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column"
+ data-testid="dropzone-area"
+ >
+ <gl-icon-stub
+ class="gl-mb-2"
+ name="upload"
+ size="24"
+ />
+
+ <p
+ class="gl-mb-0"
+ >
+ <gl-sprintf-stub
+ message="Drop or %{linkStart}upload%{linkEnd} files to attach"
+ />
+ </p>
+ </div>
+ </button>
+
+ <input
+ accept="image/*"
+ class="hide"
+ multiple="multiple"
+ name="upload_file"
+ type="file"
+ />
+
+ <transition-stub
+ name="upload-dropzone-fade"
+ >
+ <div
+ class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-3 gl-bg-white"
+ style=""
+ >
+ <div
+ class="mw-50 gl-text-center"
+ style="display: none;"
+ >
+ <h3
+ class=""
+ >
+
+ Oh no!
+
+ </h3>
+
+ <span>
+ You are trying to upload something other than an image. Please upload a .png, .jpg, .jpeg, .gif, .bmp, .tiff or .ico.
+ </span>
+ </div>
+
+ <div
+ class="mw-50 gl-text-center"
+ style=""
+ >
+ <h3
+ class=""
+ >
+
+ Incoming!
+
+ </h3>
+
+ <span>
+ Drop your files to start your upload.
+ </span>
+ </div>
+ </div>
+ </transition-stub>
+</div>
+`;
+
+exports[`Upload dropzone component when dragging renders correct template when drag event contains text 1`] = `
+<div
+ class="gl-w-full gl-relative"
+>
+ <button
+ class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3"
+ >
+ <div
+ class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column"
+ data-testid="dropzone-area"
+ >
+ <gl-icon-stub
+ class="gl-mb-2"
+ name="upload"
+ size="24"
+ />
+
+ <p
+ class="gl-mb-0"
+ >
+ <gl-sprintf-stub
+ message="Drop or %{linkStart}upload%{linkEnd} files to attach"
+ />
+ </p>
+ </div>
+ </button>
+
+ <input
+ accept="image/*"
+ class="hide"
+ multiple="multiple"
+ name="upload_file"
+ type="file"
+ />
+
+ <transition-stub
+ name="upload-dropzone-fade"
+ >
+ <div
+ class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-3 gl-bg-white"
+ style=""
+ >
+ <div
+ class="mw-50 gl-text-center"
+ >
+ <h3
+ class=""
+ >
+
+ Oh no!
+
+ </h3>
+
+ <span>
+ You are trying to upload something other than an image. Please upload a .png, .jpg, .jpeg, .gif, .bmp, .tiff or .ico.
+ </span>
+ </div>
+
+ <div
+ class="mw-50 gl-text-center"
+ style="display: none;"
+ >
+ <h3
+ class=""
+ >
+
+ Incoming!
+
+ </h3>
+
+ <span>
+ Drop your files to start your upload.
+ </span>
+ </div>
+ </div>
+ </transition-stub>
+</div>
+`;
+
+exports[`Upload dropzone component when dragging renders correct template when drag event is empty 1`] = `
+<div
+ class="gl-w-full gl-relative"
+>
+ <button
+ class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3"
+ >
+ <div
+ class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column"
+ data-testid="dropzone-area"
+ >
+ <gl-icon-stub
+ class="gl-mb-2"
+ name="upload"
+ size="24"
+ />
+
+ <p
+ class="gl-mb-0"
+ >
+ <gl-sprintf-stub
+ message="Drop or %{linkStart}upload%{linkEnd} files to attach"
+ />
+ </p>
+ </div>
+ </button>
+
+ <input
+ accept="image/*"
+ class="hide"
+ multiple="multiple"
+ name="upload_file"
+ type="file"
+ />
+
+ <transition-stub
+ name="upload-dropzone-fade"
+ >
+ <div
+ class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-3 gl-bg-white"
+ style=""
+ >
+ <div
+ class="mw-50 gl-text-center"
+ >
+ <h3
+ class=""
+ >
+
+ Oh no!
+
+ </h3>
+
+ <span>
+ You are trying to upload something other than an image. Please upload a .png, .jpg, .jpeg, .gif, .bmp, .tiff or .ico.
+ </span>
+ </div>
+
+ <div
+ class="mw-50 gl-text-center"
+ style="display: none;"
+ >
+ <h3
+ class=""
+ >
+
+ Incoming!
+
+ </h3>
+
+ <span>
+ Drop your files to start your upload.
+ </span>
+ </div>
+ </div>
+ </transition-stub>
+</div>
+`;
+
+exports[`Upload dropzone component when dragging renders correct template when dragging stops 1`] = `
+<div
+ class="gl-w-full gl-relative"
+>
+ <button
+ class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3"
+ >
+ <div
+ class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column"
+ data-testid="dropzone-area"
+ >
+ <gl-icon-stub
+ class="gl-mb-2"
+ name="upload"
+ size="24"
+ />
+
+ <p
+ class="gl-mb-0"
+ >
+ <gl-sprintf-stub
+ message="Drop or %{linkStart}upload%{linkEnd} files to attach"
+ />
+ </p>
+ </div>
+ </button>
+
+ <input
+ accept="image/*"
+ class="hide"
+ multiple="multiple"
+ name="upload_file"
+ type="file"
+ />
+
+ <transition-stub
+ name="upload-dropzone-fade"
+ >
+ <div
+ class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-3 gl-bg-white"
+ style="display: none;"
+ >
+ <div
+ class="mw-50 gl-text-center"
+ >
+ <h3
+ class=""
+ >
+
+ Oh no!
+
+ </h3>
+
+ <span>
+ You are trying to upload something other than an image. Please upload a .png, .jpg, .jpeg, .gif, .bmp, .tiff or .ico.
+ </span>
+ </div>
+
+ <div
+ class="mw-50 gl-text-center"
+ style="display: none;"
+ >
+ <h3
+ class=""
+ >
+
+ Incoming!
+
+ </h3>
+
+ <span>
+ Drop your files to start your upload.
+ </span>
+ </div>
+ </div>
+ </transition-stub>
+</div>
+`;
+
+exports[`Upload dropzone component when no slot provided renders default dropzone card 1`] = `
+<div
+ class="gl-w-full gl-relative"
+>
+ <button
+ class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3"
+ >
+ <div
+ class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column"
+ data-testid="dropzone-area"
+ >
+ <gl-icon-stub
+ class="gl-mb-2"
+ name="upload"
+ size="24"
+ />
+
+ <p
+ class="gl-mb-0"
+ >
+ <gl-sprintf-stub
+ message="Drop or %{linkStart}upload%{linkEnd} files to attach"
+ />
+ </p>
+ </div>
+ </button>
+
+ <input
+ accept="image/*"
+ class="hide"
+ multiple="multiple"
+ name="upload_file"
+ type="file"
+ />
+
+ <transition-stub
+ name="upload-dropzone-fade"
+ >
+ <div
+ class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-3 gl-bg-white"
+ style="display: none;"
+ >
+ <div
+ class="mw-50 gl-text-center"
+ >
+ <h3
+ class=""
+ >
+
+ Oh no!
+
+ </h3>
+
+ <span>
+ You are trying to upload something other than an image. Please upload a .png, .jpg, .jpeg, .gif, .bmp, .tiff or .ico.
+ </span>
+ </div>
+
+ <div
+ class="mw-50 gl-text-center"
+ style="display: none;"
+ >
+ <h3
+ class=""
+ >
+
+ Incoming!
+
+ </h3>
+
+ <span>
+ Drop your files to start your upload.
+ </span>
+ </div>
+ </div>
+ </transition-stub>
+</div>
+`;
+
+exports[`Upload dropzone component when slot provided renders dropzone with slot content 1`] = `
+<div
+ class="gl-w-full gl-relative"
+>
+ <div>
+ dropzone slot
+ </div>
+
+ <transition-stub
+ name="upload-dropzone-fade"
+ >
+ <div
+ class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-3 gl-bg-white"
+ style="display: none;"
+ >
+ <div
+ class="mw-50 gl-text-center"
+ >
+ <h3
+ class=""
+ >
+
+ Oh no!
+
+ </h3>
+
+ <span>
+ You are trying to upload something other than an image. Please upload a .png, .jpg, .jpeg, .gif, .bmp, .tiff or .ico.
+ </span>
+ </div>
+
+ <div
+ class="mw-50 gl-text-center"
+ style="display: none;"
+ >
+ <h3
+ class=""
+ >
+
+ Incoming!
+
+ </h3>
+
+ <span>
+ Drop your files to start your upload.
+ </span>
+ </div>
+ </div>
+ </transition-stub>
+</div>
+`;
diff --git a/spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js b/spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js
new file mode 100644
index 00000000000..11982eb513d
--- /dev/null
+++ b/spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js
@@ -0,0 +1,174 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlIcon } from '@gitlab/ui';
+import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
+
+jest.mock('~/flash');
+
+describe('Upload dropzone component', () => {
+ let wrapper;
+
+ const mockDragEvent = ({ types = ['Files'], files = [] }) => {
+ return { dataTransfer: { types, files } };
+ };
+
+ const findDropzoneCard = () => wrapper.find('.upload-dropzone-card');
+ const findDropzoneArea = () => wrapper.find('[data-testid="dropzone-area"]');
+ const findIcon = () => wrapper.find(GlIcon);
+
+ function createComponent({ slots = {}, data = {}, props = {} } = {}) {
+ wrapper = shallowMount(UploadDropzone, {
+ slots,
+ propsData: {
+ displayAsCard: true,
+ ...props,
+ },
+ data() {
+ return data;
+ },
+ });
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when slot provided', () => {
+ it('renders dropzone with slot content', () => {
+ createComponent({
+ slots: {
+ default: ['<div>dropzone slot</div>'],
+ },
+ });
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+
+ describe('when no slot provided', () => {
+ it('renders default dropzone card', () => {
+ createComponent();
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('triggers click event on file input element when clicked', () => {
+ createComponent();
+ const clickSpy = jest.spyOn(wrapper.find('input').element, 'click');
+
+ findDropzoneCard().trigger('click');
+ expect(clickSpy).toHaveBeenCalled();
+ });
+ });
+
+ describe('when dragging', () => {
+ it.each`
+ description | eventPayload
+ ${'is empty'} | ${{}}
+ ${'contains text'} | ${mockDragEvent({ types: ['text'] })}
+ ${'contains files and text'} | ${mockDragEvent({ types: ['Files', 'text'] })}
+ ${'contains files'} | ${mockDragEvent({ types: ['Files'] })}
+ `('renders correct template when drag event $description', ({ eventPayload }) => {
+ createComponent();
+
+ wrapper.trigger('dragenter', eventPayload);
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+
+ it('renders correct template when dragging stops', () => {
+ createComponent();
+
+ wrapper.trigger('dragenter');
+ return wrapper.vm
+ .$nextTick()
+ .then(() => {
+ wrapper.trigger('dragleave');
+ return wrapper.vm.$nextTick();
+ })
+ .then(() => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+ });
+
+ describe('when dropping', () => {
+ it('emits upload event', () => {
+ createComponent();
+ const mockFile = { name: 'test', type: 'image/jpg' };
+ const mockEvent = mockDragEvent({ files: [mockFile] });
+
+ wrapper.trigger('dragenter', mockEvent);
+ return wrapper.vm
+ .$nextTick()
+ .then(() => {
+ wrapper.trigger('drop', mockEvent);
+ return wrapper.vm.$nextTick();
+ })
+ .then(() => {
+ expect(wrapper.emitted().change[0]).toEqual([[mockFile]]);
+ });
+ });
+ });
+
+ describe('ondrop', () => {
+ const mockData = { dragCounter: 1, isDragDataValid: true };
+
+ describe('when drag data is valid', () => {
+ it('emits upload event for valid files', () => {
+ createComponent({ data: mockData });
+
+ const mockFile = { type: 'image/jpg' };
+ const mockEvent = mockDragEvent({ files: [mockFile] });
+
+ wrapper.vm.ondrop(mockEvent);
+ expect(wrapper.emitted().change[0]).toEqual([[mockFile]]);
+ });
+
+ it('emits error event when files are invalid', () => {
+ createComponent({ data: mockData });
+ const mockEvent = mockDragEvent({ files: [{ type: 'audio/midi' }] });
+
+ wrapper.vm.ondrop(mockEvent);
+ expect(wrapper.emitted()).toHaveProperty('error');
+ });
+
+ it('allows validation function to be overwritten', () => {
+ createComponent({ data: mockData, props: { isFileValid: () => true } });
+
+ const mockEvent = mockDragEvent({ files: [{ type: 'audio/midi' }] });
+
+ wrapper.vm.ondrop(mockEvent);
+ expect(wrapper.emitted()).not.toHaveProperty('error');
+ });
+ });
+ });
+
+ it('applies correct classes when displaying as a standalone item', () => {
+ createComponent({ props: { displayAsCard: false } });
+ expect(findDropzoneArea().classes()).not.toContain('gl-flex-direction-column');
+ expect(findIcon().classes()).toEqual(['gl-mr-3', 'gl-text-gray-500']);
+ expect(findIcon().props('size')).toBe(16);
+ });
+
+ it('applies correct classes when displaying in card mode', () => {
+ createComponent({ props: { displayAsCard: true } });
+ expect(findDropzoneArea().classes()).toContain('gl-flex-direction-column');
+ expect(findIcon().classes()).toEqual(['gl-mb-2']);
+ expect(findIcon().props('size')).toBe(24);
+ });
+
+ it('correctly overrides description and drop messages', () => {
+ createComponent({
+ props: {
+ dropToStartMessage: 'Test drop-to-start message.',
+ validFileMimetypes: ['image/jpg', 'image/jpeg'],
+ },
+ slots: {
+ 'upload-text': '<span>Test %{linkStart}description%{linkEnd} message.</span>',
+ },
+ });
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+});
diff --git a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
index c208d7b0226..7d58a865ba3 100644
--- a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
+++ b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
@@ -1,6 +1,8 @@
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlSprintf, GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import UserPopover from '~/vue_shared/components/user_popover/user_popover.vue';
+import UserAvailabilityStatus from '~/set_status_modal/components/user_availability_status.vue';
+import { AVAILABILITY_STATUS } from '~/set_status_modal/utils';
const DEFAULT_PROPS = {
user: {
@@ -34,6 +36,7 @@ describe('User Popover Component', () => {
const findByTestId = testid => wrapper.find(`[data-testid="${testid}"]`);
const findUserStatus = () => wrapper.find('.js-user-status');
const findTarget = () => document.querySelector('.js-user-link');
+ const findAvailabilityStatus = () => wrapper.find(UserAvailabilityStatus);
const createWrapper = (props = {}, options = {}) => {
wrapper = shallowMount(UserPopover, {
@@ -43,7 +46,8 @@ describe('User Popover Component', () => {
...props,
},
stubs: {
- 'gl-sprintf': GlSprintf,
+ GlSprintf,
+ UserAvailabilityStatus,
},
...options,
});
@@ -199,6 +203,30 @@ describe('User Popover Component', () => {
expect(findUserStatus().exists()).toBe(false);
});
+
+ it('should show the busy status if user set to busy', () => {
+ const user = {
+ ...DEFAULT_PROPS.user,
+ status: { availability: AVAILABILITY_STATUS.BUSY },
+ };
+
+ createWrapper({ user });
+
+ expect(findAvailabilityStatus().exists()).toBe(true);
+ expect(wrapper.text()).toContain(user.name);
+ expect(wrapper.text()).toContain('(Busy)');
+ });
+
+ it('should hide the busy status for any other status', () => {
+ const user = {
+ ...DEFAULT_PROPS.user,
+ status: { availability: AVAILABILITY_STATUS.NOT_SET },
+ };
+
+ createWrapper({ user });
+
+ expect(wrapper.text()).not.toContain('(Busy)');
+ });
});
describe('security bot', () => {