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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/vue_shared')
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap1
-rw-r--r--spec/frontend/vue_shared/components/actions_button_spec.js119
-rw-r--r--spec/frontend/vue_shared/components/badges/__snapshots__/beta_badge_spec.js.snap54
-rw-r--r--spec/frontend/vue_shared/components/badges/beta_badge_spec.js32
-rw-r--r--spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/clipboard_button_spec.js9
-rw-r--r--spec/frontend/vue_shared/components/diff_viewer/viewers/renamed_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js101
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/date_token_spec.js49
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js296
-rw-r--r--spec/frontend/vue_shared/components/gl_modal_vuex_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/groups_list/groups_list_item_spec.js182
-rw-r--r--spec/frontend/vue_shared/components/groups_list/groups_list_spec.js34
-rw-r--r--spec/frontend/vue_shared/components/groups_list/mock_data.js35
-rw-r--r--spec/frontend/vue_shared/components/help_popover_spec.js16
-rw-r--r--spec/frontend/vue_shared/components/listbox_input/listbox_input_spec.js9
-rw-r--r--spec/frontend/vue_shared/components/markdown/comment_templates_dropdown_spec.js93
-rw-r--r--spec/frontend/vue_shared/components/markdown/header_spec.js93
-rw-r--r--spec/frontend/vue_shared/components/markdown/toolbar_button_spec.js29
-rw-r--r--spec/frontend/vue_shared/components/markdown/toolbar_spec.js3
-rw-r--r--spec/frontend/vue_shared/components/metric_images/metric_images_tab_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/metric_images/metric_images_table_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/metric_images/store/actions_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/modal_copy_button_spec.js7
-rw-r--r--spec/frontend/vue_shared/components/new_resource_dropdown/new_resource_dropdown_spec.js8
-rw-r--r--spec/frontend/vue_shared/components/notes/placeholder_note_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/page_size_selector_spec.js26
-rw-r--r--spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js84
-rw-r--r--spec/frontend/vue_shared/components/projects_list/projects_list_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/registry/persisted_dropdown_selection_spec.js52
-rw-r--r--spec/frontend/vue_shared/components/registry/registry_search_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/registry/title_area_spec.js63
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/source_viewer_new_spec.js15
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js80
-rw-r--r--spec/frontend/vue_shared/components/web_ide_link_spec.js119
-rw-r--r--spec/frontend/vue_shared/issuable/create/components/issuable_create_root_spec.js6
-rw-r--r--spec/frontend/vue_shared/issuable/create/components/issuable_form_spec.js27
-rw-r--r--spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js37
-rw-r--r--spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js24
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js350
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js3
42 files changed, 1508 insertions, 578 deletions
diff --git a/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap
index 2b89e36344d..62d75fbdc5f 100644
--- a/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap
+++ b/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap
@@ -12,6 +12,7 @@ exports[`SplitButton renders actionItems 1`] = `
menu-class=""
size="medium"
split="true"
+ splithref=""
text="professor"
variant="default"
>
diff --git a/spec/frontend/vue_shared/components/actions_button_spec.js b/spec/frontend/vue_shared/components/actions_button_spec.js
deleted file mode 100644
index 9f9a27c6997..00000000000
--- a/spec/frontend/vue_shared/components/actions_button_spec.js
+++ /dev/null
@@ -1,119 +0,0 @@
-import {
- GlDisclosureDropdown,
- GlDisclosureDropdownGroup,
- GlDisclosureDropdownItem,
-} from '@gitlab/ui';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import ActionsButton from '~/vue_shared/components/actions_button.vue';
-
-const TEST_ACTION = {
- key: 'action1',
- text: 'Sample',
- secondaryText: 'Lorem ipsum.',
- href: '/sample',
- attrs: {
- 'data-test': '123',
- category: 'secondary',
- href: '/sample',
- variant: 'default',
- },
- handle: jest.fn(),
-};
-const TEST_ACTION_2 = {
- key: 'action2',
- text: 'Sample 2',
- secondaryText: 'Dolar sit amit.',
- href: '#',
- attrs: { 'data-test': '456' },
- handle: jest.fn(),
-};
-
-describe('vue_shared/components/actions_button', () => {
- let wrapper;
-
- function createComponent({ props = {}, slots = {} } = {}) {
- wrapper = shallowMountExtended(ActionsButton, {
- propsData: { actions: [TEST_ACTION, TEST_ACTION_2], toggleText: 'Edit', ...props },
- stubs: {
- GlDisclosureDropdownItem,
- },
- slots,
- });
- }
- const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
-
- it('dropdown toggle displays provided toggleLabel', () => {
- createComponent();
-
- expect(findDropdown().props().toggleText).toBe('Edit');
- });
-
- it('dropdown has a fluid width', () => {
- createComponent();
-
- expect(findDropdown().props().fluidWidth).toBe(true);
- });
-
- it('provides a default slot', () => {
- const slotContent = 'default text';
-
- createComponent({
- slots: {
- default: slotContent,
- },
- });
-
- expect(findDropdown().text()).toContain(slotContent);
- });
-
- it('allows customizing variant and category', () => {
- const variant = 'confirm';
- const category = 'secondary';
-
- createComponent({ props: { variant, category } });
-
- expect(findDropdown().props()).toMatchObject({ category, variant });
- });
-
- it('displays a single dropdown group', () => {
- createComponent();
-
- expect(wrapper.findAllComponents(GlDisclosureDropdownGroup)).toHaveLength(1);
- });
-
- it('create dropdown items for every action', () => {
- createComponent();
-
- [TEST_ACTION, TEST_ACTION_2].forEach((action, index) => {
- const dropdownItem = wrapper.findAllComponents(GlDisclosureDropdownItem).at(index);
-
- expect(dropdownItem.props().item).toBe(action);
- expect(dropdownItem.attributes()).toMatchObject(action.attrs);
- expect(dropdownItem.text()).toContain(action.text);
- expect(dropdownItem.text()).toContain(action.secondaryText);
- });
- });
-
- describe('when clicking a dropdown item', () => {
- it("invokes the action's handle method", () => {
- createComponent();
-
- [TEST_ACTION, TEST_ACTION_2].forEach((action, index) => {
- const dropdownItem = wrapper.findAllComponents(GlDisclosureDropdownItem).at(index);
-
- dropdownItem.vm.$emit('action');
-
- expect(action.handle).toHaveBeenCalled();
- });
- });
- });
-
- it.each(['shown', 'hidden'])(
- 'bubbles up %s event from the disclosure dropdown component',
- (event) => {
- createComponent();
- findDropdown().vm.$emit(event);
- expect(wrapper.emitted(event)).toHaveLength(1);
- },
- );
-});
diff --git a/spec/frontend/vue_shared/components/badges/__snapshots__/beta_badge_spec.js.snap b/spec/frontend/vue_shared/components/badges/__snapshots__/beta_badge_spec.js.snap
new file mode 100644
index 00000000000..24b2c54f20b
--- /dev/null
+++ b/spec/frontend/vue_shared/components/badges/__snapshots__/beta_badge_spec.js.snap
@@ -0,0 +1,54 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Beta badge component renders the badge 1`] = `
+<div>
+ <gl-badge-stub
+ class="gl-cursor-pointer"
+ href="#"
+ iconsize="md"
+ size="md"
+ variant="neutral"
+ >
+ Beta
+ </gl-badge-stub>
+
+ <gl-popover-stub
+ cssclasses=""
+ data-testid="beta-badge"
+ showclosebutton="true"
+ target="[Function]"
+ title="What's Beta?"
+ triggers="hover focus click"
+ >
+ <p>
+ A Beta feature is not production-ready, but is unlikely to change drastically before it's released. We encourage users to try Beta features and provide feedback.
+ </p>
+
+ <p
+ class="gl-mb-0"
+ >
+ A Beta feature:
+ </p>
+
+ <ul
+ class="gl-pl-4"
+ >
+ <li>
+ May be unstable.
+ </li>
+
+ <li>
+ Should not cause data loss.
+ </li>
+
+ <li>
+ Is supported by a commercially reasonable effort.
+ </li>
+
+ <li>
+ Is complete or near completion.
+ </li>
+ </ul>
+ </gl-popover-stub>
+</div>
+`;
diff --git a/spec/frontend/vue_shared/components/badges/beta_badge_spec.js b/spec/frontend/vue_shared/components/badges/beta_badge_spec.js
new file mode 100644
index 00000000000..c930c6d5708
--- /dev/null
+++ b/spec/frontend/vue_shared/components/badges/beta_badge_spec.js
@@ -0,0 +1,32 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlBadge } from '@gitlab/ui';
+import BetaBadge from '~/vue_shared/components/badges/beta_badge.vue';
+
+describe('Beta badge component', () => {
+ let wrapper;
+
+ const findBadge = () => wrapper.findComponent(GlBadge);
+ const createWrapper = (props = {}) => {
+ wrapper = shallowMount(BetaBadge, {
+ propsData: { ...props },
+ });
+ };
+
+ it('renders the badge', () => {
+ createWrapper();
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('passes default size to badge', () => {
+ createWrapper();
+
+ expect(findBadge().props('size')).toBe('md');
+ });
+
+ it('passes given size to badge', () => {
+ createWrapper({ size: 'sm' });
+
+ expect(findBadge().props('size')).toBe('sm');
+ });
+});
diff --git a/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js b/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js
index 1f3029435ee..fc8155bd381 100644
--- a/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js
@@ -3,8 +3,10 @@ import { shallowMount } from '@vue/test-utils';
import { handleBlobRichViewer } from '~/blob/viewer';
import RichViewer from '~/vue_shared/components/blob_viewers/rich_viewer.vue';
import MarkdownFieldView from '~/vue_shared/components/markdown/field_view.vue';
+import { handleLocationHash } from '~/lib/utils/common_utils';
jest.mock('~/blob/viewer');
+jest.mock('~/lib/utils/common_utils');
describe('Blob Rich Viewer component', () => {
let wrapper;
@@ -50,4 +52,8 @@ describe('Blob Rich Viewer component', () => {
it('is using Markdown View Field', () => {
expect(wrapper.findComponent(MarkdownFieldView).exists()).toBe(true);
});
+
+ it('scrolls to the hash location', () => {
+ expect(handleLocationHash).toHaveBeenCalled();
+ });
});
diff --git a/spec/frontend/vue_shared/components/clipboard_button_spec.js b/spec/frontend/vue_shared/components/clipboard_button_spec.js
index 08a9c2a42d8..271c99be57a 100644
--- a/spec/frontend/vue_shared/components/clipboard_button_spec.js
+++ b/spec/frontend/vue_shared/components/clipboard_button_spec.js
@@ -1,7 +1,8 @@
import { GlButton } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
+import { mount, createWrapper as makeWrapper } from '@vue/test-utils';
import { nextTick } from 'vue';
+import { BV_HIDE_TOOLTIP, BV_SHOW_TOOLTIP } from '~/lib/utils/constants';
import initCopyToClipboard, {
CLIPBOARD_SUCCESS_EVENT,
CLIPBOARD_ERROR_EVENT,
@@ -31,7 +32,7 @@ describe('clipboard button', () => {
title,
});
- wrapper.vm.$root.$emit = jest.fn();
+ const rootWrapper = makeWrapper(wrapper.vm.$root);
const button = findButton();
@@ -42,7 +43,7 @@ describe('clipboard button', () => {
await button.trigger(event);
- expect(wrapper.vm.$root.$emit).toHaveBeenCalledWith('bv::show::tooltip', 'clipboard-button-1');
+ expect(rootWrapper.emitted(BV_SHOW_TOOLTIP)[0]).toContain('clipboard-button-1');
expect(button.attributes()).toMatchObject({
title: message,
@@ -56,7 +57,7 @@ describe('clipboard button', () => {
title,
'aria-label': title,
});
- expect(wrapper.vm.$root.$emit).toHaveBeenCalledWith('bv::hide::tooltip', 'clipboard-button-1');
+ expect(rootWrapper.emitted(BV_HIDE_TOOLTIP)[0]).toContain('clipboard-button-1');
};
describe('without gfm', () => {
diff --git a/spec/frontend/vue_shared/components/diff_viewer/viewers/renamed_spec.js b/spec/frontend/vue_shared/components/diff_viewer/viewers/renamed_spec.js
index 0d536b23c45..2f165338577 100644
--- a/spec/frontend/vue_shared/components/diff_viewer/viewers/renamed_spec.js
+++ b/spec/frontend/vue_shared/components/diff_viewer/viewers/renamed_spec.js
@@ -1,5 +1,6 @@
import { shallowMount, mount } from '@vue/test-utils';
import Vue from 'vue';
+// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import { GlAlert, GlLink, GlLoadingIcon } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js
index 21a1303ccf3..ce8897027a4 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js
@@ -110,6 +110,8 @@ describe('processFilters', () => {
{ type: 'foo', value: { data: 'foo', operator: '=' } },
{ type: 'bar', value: { data: 'bar1', operator: '=' } },
{ type: 'bar', value: { data: 'bar2', operator: '!=' } },
+ 'just a string',
+ 'and another',
]);
expect(result).toStrictEqual({
@@ -118,6 +120,10 @@ describe('processFilters', () => {
{ value: 'bar1', operator: '=' },
{ value: 'bar2', operator: '!=' },
],
+ 'filtered-search-term': [
+ { value: 'just a string', operator: undefined },
+ { value: 'and another', operator: undefined },
+ ],
});
});
@@ -208,6 +214,67 @@ describe('filterToQueryObject', () => {
expect(res).toEqual(result);
},
);
+
+ describe('with custom operators', () => {
+ it('does not handle filters without custom operators', () => {
+ const res = filterToQueryObject({
+ foo: [
+ { value: '100', operator: '>' },
+ { value: '200', operator: '<' },
+ ],
+ });
+ expect(res).toEqual({ foo: null, 'not[foo]': null });
+ });
+
+ it('handles filters with custom operators', () => {
+ const res = filterToQueryObject(
+ {
+ foo: [
+ { value: '100', operator: '>' },
+ { value: '200', operator: '<' },
+ ],
+ },
+ {
+ customOperators: [
+ {
+ operator: '>',
+ prefix: 'gt',
+ },
+ {
+ operator: '<',
+ prefix: 'lt',
+ },
+ ],
+ },
+ );
+ expect(res).toEqual({ foo: null, 'gt[foo]': ['100'], 'lt[foo]': ['200'], 'not[foo]': null });
+ });
+ });
+
+ it('when applyOnlyToKey is present, it only process custom operators for the given key', () => {
+ const res = filterToQueryObject(
+ {
+ foo: [{ value: '100', operator: '>' }],
+ bar: [{ value: '100', operator: '>' }],
+ },
+ {
+ customOperators: [
+ {
+ operator: '>',
+ prefix: 'gt',
+ applyOnlyToKey: 'foo',
+ },
+ ],
+ },
+ );
+ expect(res).toEqual({
+ bar: null,
+ 'not[bar]': null,
+ foo: null,
+ 'gt[foo]': ['100'],
+ 'not[foo]': null,
+ });
+ });
});
describe('urlQueryToFilter', () => {
@@ -275,28 +342,40 @@ describe('urlQueryToFilter', () => {
[
'search=my terms',
{
- [FILTERED_SEARCH_TERM]: [{ value: 'my' }, { value: 'terms' }],
+ [FILTERED_SEARCH_TERM]: [{ value: 'my terms' }],
},
{ filteredSearchTermKey: 'search' },
],
[
'search[]=my&search[]=terms',
{
- [FILTERED_SEARCH_TERM]: [{ value: 'my' }, { value: 'terms' }],
+ [FILTERED_SEARCH_TERM]: [{ value: 'my terms' }],
},
{ filteredSearchTermKey: 'search' },
],
[
'search=my+terms',
{
- [FILTERED_SEARCH_TERM]: [{ value: 'my' }, { value: 'terms' }],
+ [FILTERED_SEARCH_TERM]: [{ value: 'my terms' }],
},
{ filteredSearchTermKey: 'search' },
],
[
'search=my terms&foo=bar&nop=xxx',
{
- [FILTERED_SEARCH_TERM]: [{ value: 'my' }, { value: 'terms' }],
+ [FILTERED_SEARCH_TERM]: [{ value: 'my terms' }],
+ foo: { value: 'bar', operator: '=' },
+ },
+ { filteredSearchTermKey: 'search', filterNamesAllowList: ['foo'] },
+ ],
+ [
+ {
+ search: 'my terms',
+ foo: 'bar',
+ nop: 'xxx',
+ },
+ {
+ [FILTERED_SEARCH_TERM]: [{ value: 'my terms' }],
foo: { value: 'bar', operator: '=' },
},
{ filteredSearchTermKey: 'search', filterNamesAllowList: ['foo'] },
@@ -308,6 +387,20 @@ describe('urlQueryToFilter', () => {
expect(res).toEqual(result);
},
);
+
+ describe('custom operators', () => {
+ it('handles query param with custom operators', () => {
+ const res = urlQueryToFilter('gt[foo]=bar', {
+ customOperators: [{ operator: '>', prefix: 'gt' }],
+ });
+ expect(res).toEqual({ foo: { operator: '>', value: 'bar' } });
+ });
+
+ it('does not handle query param without custom operators', () => {
+ const res = urlQueryToFilter('gt[foo]=bar');
+ expect(res).toEqual({ 'gt[foo]': { operator: '=', value: 'bar' } });
+ });
+ });
});
describe('getRecentlyUsedSuggestions', () => {
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/date_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/date_token_spec.js
new file mode 100644
index 00000000000..56a59790210
--- /dev/null
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/date_token_spec.js
@@ -0,0 +1,49 @@
+import { GlDatepicker, GlFilteredSearchToken } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import DateToken from '~/vue_shared/components/filtered_search_bar/tokens/date_token.vue';
+
+const propsData = {
+ active: true,
+ config: {},
+ value: { operator: '>', data: null },
+};
+
+function createComponent() {
+ return mount(DateToken, {
+ propsData,
+ provide: {
+ portalName: 'fake target',
+ alignSuggestions: function fakeAlignSuggestions() {},
+ termsAsTokens: () => false,
+ },
+ });
+}
+
+describe('DateToken', () => {
+ let wrapper;
+
+ const findGlFilteredSearchToken = () => wrapper.findComponent(GlFilteredSearchToken);
+ const findDatepicker = () => wrapper.findComponent(GlDatepicker);
+
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ it('renders GlDatepicker', () => {
+ expect(findDatepicker().exists()).toBe(true);
+ });
+
+ it('renders GlFilteredSearchToken', () => {
+ expect(findGlFilteredSearchToken().exists()).toBe(true);
+ });
+
+ it('emits `complete` and `select` with the formatted date when a value is selected', () => {
+ findDatepicker().vm.$emit('input', new Date('October 13, 2014 11:13:00'));
+ findDatepicker().vm.$emit('close');
+
+ expect(findGlFilteredSearchToken().emitted()).toEqual({
+ complete: [[]],
+ select: [['2014-10-13']],
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js
index 5e675c10038..db116a31de7 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js
@@ -122,7 +122,7 @@ describe('EmojiToken', () => {
it('calls `createAlert`', () => {
expect(createAlert).toHaveBeenCalledWith({
- message: 'There was a problem fetching emojis.',
+ message: 'There was a problem fetching emoji.',
});
});
diff --git a/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js b/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js
index 4f1603f93ba..eee85ce4fd3 100644
--- a/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js
+++ b/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js
@@ -1,26 +1,25 @@
-import { merge } from 'lodash';
+import { nextTick } from 'vue';
import { GlFormInputGroup } from '@gitlab/ui';
import InputCopyToggleVisibility from '~/vue_shared/components/form/input_copy_toggle_visibility.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-
import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { MOUSETRAP_COPY_KEYBOARD_SHORTCUT } from '~/lib/mousetrap';
describe('InputCopyToggleVisibility', () => {
let wrapper;
const valueProp = 'hR8x1fuJbzwu5uFKLf9e';
- const createComponent = (options = {}) => {
- wrapper = mountExtended(
- InputCopyToggleVisibility,
- merge({}, options, {
- directives: {
- GlTooltip: createMockDirective('gl-tooltip'),
- },
- }),
- );
+ const createComponent = ({ props, ...options } = {}) => {
+ wrapper = mountExtended(InputCopyToggleVisibility, {
+ propsData: props,
+ directives: {
+ GlTooltip: createMockDirective('gl-tooltip'),
+ },
+ ...options,
+ });
};
const findFormInputGroup = () => wrapper.findComponent(GlFormInputGroup);
@@ -40,6 +39,18 @@ describe('InputCopyToggleVisibility', () => {
return event;
};
+ const triggerCopyShortcut = () => {
+ wrapper.vm.$options.mousetrap.trigger(MOUSETRAP_COPY_KEYBOARD_SHORTCUT);
+ };
+
+ function expectInputToBeMasked() {
+ expect(findFormInput().element.type).toBe('password');
+ }
+
+ function expectInputToBeRevealed() {
+ expect(findFormInput().element.type).toBe('text');
+ expect(findFormInput().element.value).toBe(valueProp);
+ }
const itDoesNotModifyCopyEvent = () => {
it('does not modify copy event', () => {
@@ -55,35 +66,61 @@ describe('InputCopyToggleVisibility', () => {
describe('when `value` prop is passed', () => {
beforeEach(() => {
createComponent({
- propsData: {
+ props: {
value: valueProp,
},
});
});
- it('displays value as hidden', () => {
- expect(findFormInput().element.value).toBe('********************');
+ it('hides the value with a password input', () => {
+ expectInputToBeMasked();
});
- it('saves actual value to clipboard when manually copied', () => {
- const event = createCopyEvent();
- findFormInput().element.dispatchEvent(event);
-
- expect(event.clipboardData.setData).toHaveBeenCalledWith('text/plain', valueProp);
- expect(event.preventDefault).toHaveBeenCalled();
- });
+ it('emits `copy` event and sets clipboard when copying token via keyboard shortcut', async () => {
+ const writeTextSpy = jest.spyOn(global.navigator.clipboard, 'writeText');
- it('emits `copy` event when manually copied the token', () => {
expect(wrapper.emitted('copy')).toBeUndefined();
- findFormInput().element.dispatchEvent(createCopyEvent());
+ triggerCopyShortcut();
+ await nextTick();
- expect(wrapper.emitted()).toHaveProperty('copy');
- expect(wrapper.emitted('copy')).toHaveLength(1);
expect(wrapper.emitted('copy')[0]).toEqual([]);
+ expect(writeTextSpy).toHaveBeenCalledWith(valueProp);
});
+ describe('copy button', () => {
+ it('renders button with correct props passed', () => {
+ expect(findCopyButton().props()).toMatchObject({
+ text: valueProp,
+ title: 'Copy',
+ });
+ });
+
+ describe('when clicked', () => {
+ beforeEach(async () => {
+ await findCopyButton().trigger('click');
+ });
+
+ it('emits `copy` event', () => {
+ expect(wrapper.emitted()).toHaveProperty('copy');
+ expect(wrapper.emitted('copy')).toHaveLength(1);
+ expect(wrapper.emitted('copy')[0]).toEqual([]);
+ });
+ });
+ });
+ });
+
+ describe('when input is readonly', () => {
describe('visibility toggle button', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ value: valueProp,
+ readonly: true,
+ },
+ });
+ });
+
it('renders a reveal button', () => {
const revealButton = findRevealButton();
@@ -103,7 +140,7 @@ describe('InputCopyToggleVisibility', () => {
});
it('displays value', () => {
- expect(findFormInput().element.value).toBe(valueProp);
+ expectInputToBeRevealed();
});
it('renders a hide button', () => {
@@ -127,78 +164,161 @@ describe('InputCopyToggleVisibility', () => {
});
});
- describe('copy button', () => {
- it('renders button with correct props passed', () => {
- expect(findCopyButton().props()).toMatchObject({
- text: valueProp,
- title: 'Copy',
+ describe('when `initialVisibility` prop is `true`', () => {
+ const label = 'My label';
+ beforeEach(() => {
+ createComponent({
+ props: {
+ value: valueProp,
+ initialVisibility: true,
+ readonly: true,
+ label,
+ 'label-for': 'my-input',
+ formInputGroupProps: {
+ id: 'my-input',
+ },
+ },
});
});
- describe('when clicked', () => {
- beforeEach(async () => {
- await findCopyButton().trigger('click');
+ it('displays value', () => {
+ expectInputToBeRevealed();
+ });
+
+ itDoesNotModifyCopyEvent();
+
+ describe('when input is clicked', () => {
+ it('selects input value', async () => {
+ const mockSelect = jest.fn();
+ findFormInput().element.select = mockSelect;
+ await findFormInput().trigger('click');
+
+ expect(mockSelect).toHaveBeenCalled();
});
+ });
- it('emits `copy` event', () => {
- expect(wrapper.emitted()).toHaveProperty('copy');
- expect(wrapper.emitted('copy')).toHaveLength(1);
- expect(wrapper.emitted('copy')[0]).toEqual([]);
+ describe('when label is clicked', () => {
+ it('selects input value', async () => {
+ const mockSelect = jest.fn();
+ findFormInput().element.select = mockSelect;
+ await wrapper.find('label').trigger('click');
+
+ expect(mockSelect).toHaveBeenCalled();
});
});
});
});
- describe('when `value` prop is not passed', () => {
- beforeEach(() => {
- createComponent();
- });
+ describe('when input is editable', () => {
+ describe('and no `value` prop is passed', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ value: '',
+ readonly: false,
+ },
+ });
+ });
- it('displays value as hidden with 20 asterisks', () => {
- expect(findFormInput().element.value).toBe('********************');
- });
- });
+ it('displays value', () => {
+ expect(findRevealButton().exists()).toBe(false);
+ expect(findHideButton().exists()).toBe(true);
- describe('when `initialVisibility` prop is `true`', () => {
- const label = 'My label';
+ const input = findFormInput();
+ input.element.value = valueProp;
+ input.trigger('input');
- beforeEach(() => {
- createComponent({
- propsData: {
- value: valueProp,
- initialVisibility: true,
- label,
- 'label-for': 'my-input',
- formInputGroupProps: {
- id: 'my-input',
- },
- },
+ expectInputToBeRevealed();
});
});
- it('displays value', () => {
- expect(findFormInput().element.value).toBe(valueProp);
- });
+ describe('and `value` prop is passed', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ value: valueProp,
+ readonly: false,
+ },
+ });
+ });
- itDoesNotModifyCopyEvent();
+ it('renders a reveal button', () => {
+ const revealButton = findRevealButton();
+
+ expect(revealButton.exists()).toBe(true);
- describe('when input is clicked', () => {
- it('selects input value', async () => {
- const mockSelect = jest.fn();
- wrapper.vm.$refs.input.$el.select = mockSelect;
- await wrapper.findByLabelText(label).trigger('click');
+ const tooltip = getBinding(revealButton.element, 'gl-tooltip');
- expect(mockSelect).toHaveBeenCalled();
+ expect(tooltip.value).toBe(InputCopyToggleVisibility.i18n.toggleVisibilityLabelReveal);
});
- });
- describe('when label is clicked', () => {
- it('selects input value', async () => {
- const mockSelect = jest.fn();
- wrapper.vm.$refs.input.$el.select = mockSelect;
- await wrapper.find('label').trigger('click');
+ it('renders a hide button once revealed', async () => {
+ const revealButton = findRevealButton();
+ await revealButton.trigger('click');
+ await nextTick();
+
+ const hideButton = findHideButton();
+ expect(hideButton.exists()).toBe(true);
+
+ const tooltip = getBinding(hideButton.element, 'gl-tooltip');
- expect(mockSelect).toHaveBeenCalled();
+ expect(tooltip.value).toBe(InputCopyToggleVisibility.i18n.toggleVisibilityLabelHide);
+ });
+
+ it('emits `input` event when editing', () => {
+ expect(wrapper.emitted('input')).toBeUndefined();
+ const newVal = 'ding!';
+
+ const input = findFormInput();
+ input.element.value = newVal;
+ input.trigger('input');
+
+ expect(wrapper.emitted()).toHaveProperty('input');
+ expect(wrapper.emitted('input')).toHaveLength(1);
+ expect(wrapper.emitted('input')[0][0]).toBe(newVal);
+ });
+
+ it('copies updated value to clipboard after editing', async () => {
+ const writeTextSpy = jest.spyOn(global.navigator.clipboard, 'writeText');
+
+ triggerCopyShortcut();
+ await nextTick();
+
+ expect(wrapper.emitted('copy')).toHaveLength(1);
+ expect(writeTextSpy).toHaveBeenCalledWith(valueProp);
+
+ const updatedValue = 'wow amazing';
+ wrapper.setProps({ value: updatedValue });
+ await nextTick();
+
+ triggerCopyShortcut();
+ await nextTick();
+
+ expect(wrapper.emitted('copy')).toHaveLength(2);
+ expect(writeTextSpy).toHaveBeenCalledWith(updatedValue);
+ });
+
+ describe('when input is clicked', () => {
+ it('shows the actual value', async () => {
+ const input = findFormInput();
+
+ expectInputToBeMasked();
+ await findFormInput().trigger('click');
+
+ expect(input.element.value).toBe(valueProp);
+ });
+
+ it('ensures the selection start/end are in the correct position once the actual value has been revealed', async () => {
+ const input = findFormInput();
+ const selectionStart = 2;
+ const selectionEnd = 4;
+
+ input.element.setSelectionRange(selectionStart, selectionEnd);
+ await input.trigger('click');
+
+ expect(input.element.selectionStart).toBe(selectionStart);
+ expect(input.element.selectionEnd).toBe(selectionEnd);
+ });
});
});
});
@@ -206,7 +326,7 @@ describe('InputCopyToggleVisibility', () => {
describe('when `showToggleVisibilityButton` is `false`', () => {
beforeEach(() => {
createComponent({
- propsData: {
+ props: {
value: valueProp,
showToggleVisibilityButton: false,
},
@@ -219,7 +339,7 @@ describe('InputCopyToggleVisibility', () => {
});
it('displays value', () => {
- expect(findFormInput().element.value).toBe(valueProp);
+ expectInputToBeRevealed();
});
itDoesNotModifyCopyEvent();
@@ -228,7 +348,7 @@ describe('InputCopyToggleVisibility', () => {
describe('when `showCopyButton` is `false`', () => {
beforeEach(() => {
createComponent({
- propsData: {
+ props: {
showCopyButton: false,
},
});
@@ -239,9 +359,23 @@ describe('InputCopyToggleVisibility', () => {
});
});
+ describe('when `size` is used', () => {
+ it('passes no `size` prop', () => {
+ createComponent();
+
+ expect(findFormInput().props('size')).toBe(null);
+ });
+
+ it('passes `size` prop to the input', () => {
+ createComponent({ props: { size: 'md' } });
+
+ expect(findFormInput().props('size')).toBe('md');
+ });
+ });
+
it('passes `formInputGroupProps` prop only to the input', () => {
createComponent({
- propsData: {
+ props: {
formInputGroupProps: {
name: 'Foo bar',
'data-qa-selector': 'Foo bar',
@@ -267,7 +401,7 @@ describe('InputCopyToggleVisibility', () => {
it('passes `copyButtonTitle` prop to `ClipboardButton`', () => {
createComponent({
- propsData: {
+ props: {
copyButtonTitle: 'Copy token',
},
});
diff --git a/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js b/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js
index 6dc018797a6..271214907fc 100644
--- a/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js
+++ b/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js
@@ -1,6 +1,7 @@
import { GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
+// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
import GlModalVuex from '~/vue_shared/components/gl_modal_vuex.vue';
diff --git a/spec/frontend/vue_shared/components/groups_list/groups_list_item_spec.js b/spec/frontend/vue_shared/components/groups_list/groups_list_item_spec.js
new file mode 100644
index 00000000000..877de4f4695
--- /dev/null
+++ b/spec/frontend/vue_shared/components/groups_list/groups_list_item_spec.js
@@ -0,0 +1,182 @@
+import { GlAvatarLabeled, GlIcon } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import GroupsListItem from '~/vue_shared/components/groups_list/groups_list_item.vue';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import {
+ VISIBILITY_TYPE_ICON,
+ VISIBILITY_LEVEL_INTERNAL_STRING,
+ GROUP_VISIBILITY_TYPE,
+} from '~/visibility_level/constants';
+import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.vue';
+import { ACCESS_LEVEL_LABELS } from '~/access_level/constants';
+import { groups } from './mock_data';
+
+describe('GroupsListItem', () => {
+ let wrapper;
+
+ const [group] = groups;
+
+ const defaultPropsData = { group };
+
+ const createComponent = ({ propsData = {} } = {}) => {
+ wrapper = mountExtended(GroupsListItem, {
+ propsData: { ...defaultPropsData, ...propsData },
+ directives: {
+ GlTooltip: createMockDirective('gl-tooltip'),
+ },
+ });
+ };
+
+ const findAvatarLabeled = () => wrapper.findComponent(GlAvatarLabeled);
+ const findGroupDescription = () => wrapper.findByTestId('group-description');
+ const findVisibilityIcon = () => findAvatarLabeled().findComponent(GlIcon);
+
+ it('renders group avatar', () => {
+ createComponent();
+
+ const avatarLabeled = findAvatarLabeled();
+
+ expect(avatarLabeled.props()).toMatchObject({
+ label: group.fullName,
+ labelLink: group.webUrl,
+ });
+
+ expect(avatarLabeled.attributes()).toMatchObject({
+ 'entity-id': group.id.toString(),
+ 'entity-name': group.fullName,
+ shape: 'rect',
+ });
+ });
+
+ it('renders visibility icon with tooltip', () => {
+ createComponent();
+
+ const icon = findAvatarLabeled().findComponent(GlIcon);
+ const tooltip = getBinding(icon.element, 'gl-tooltip');
+
+ expect(icon.props('name')).toBe(VISIBILITY_TYPE_ICON[VISIBILITY_LEVEL_INTERNAL_STRING]);
+ expect(tooltip.value).toBe(GROUP_VISIBILITY_TYPE[VISIBILITY_LEVEL_INTERNAL_STRING]);
+ });
+
+ it('renders subgroup count', () => {
+ createComponent();
+
+ const countWrapper = wrapper.findByTestId('subgroups-count');
+ const tooltip = getBinding(countWrapper.element, 'gl-tooltip');
+
+ expect(tooltip.value).toBe(GroupsListItem.i18n.subgroups);
+ expect(countWrapper.text()).toBe(group.descendantGroupsCount.toString());
+ expect(countWrapper.findComponent(GlIcon).props('name')).toBe('subgroup');
+ });
+
+ it('renders projects count', () => {
+ createComponent();
+
+ const countWrapper = wrapper.findByTestId('projects-count');
+ const tooltip = getBinding(countWrapper.element, 'gl-tooltip');
+
+ expect(tooltip.value).toBe(GroupsListItem.i18n.projects);
+ expect(countWrapper.text()).toBe(group.projectsCount.toString());
+ expect(countWrapper.findComponent(GlIcon).props('name')).toBe('project');
+ });
+
+ it('renders members count', () => {
+ createComponent();
+
+ const countWrapper = wrapper.findByTestId('members-count');
+ const tooltip = getBinding(countWrapper.element, 'gl-tooltip');
+
+ expect(tooltip.value).toBe(GroupsListItem.i18n.directMembers);
+ expect(countWrapper.text()).toBe(group.groupMembersCount.toString());
+ expect(countWrapper.findComponent(GlIcon).props('name')).toBe('users');
+ });
+
+ describe('when visibility is not provided', () => {
+ it('does not render visibility icon', () => {
+ const { visibility, ...groupWithoutVisibility } = group;
+ createComponent({
+ propsData: {
+ group: groupWithoutVisibility,
+ },
+ });
+
+ expect(findVisibilityIcon().exists()).toBe(false);
+ });
+ });
+
+ it('renders access role badge', () => {
+ createComponent();
+
+ expect(findAvatarLabeled().findComponent(UserAccessRoleBadge).text()).toBe(
+ ACCESS_LEVEL_LABELS[group.accessLevel.integerValue],
+ );
+ });
+
+ describe('when group has a description', () => {
+ it('renders description', () => {
+ const descriptionHtml = '<p>Foo bar</p>';
+
+ createComponent({
+ propsData: {
+ group: {
+ ...group,
+ descriptionHtml,
+ },
+ },
+ });
+
+ expect(findGroupDescription().element.innerHTML).toBe(descriptionHtml);
+ });
+ });
+
+ describe('when group does not have a description', () => {
+ it('does not render description', () => {
+ createComponent({
+ propsData: {
+ group: {
+ ...group,
+ descriptionHtml: null,
+ },
+ },
+ });
+
+ expect(findGroupDescription().exists()).toBe(false);
+ });
+ });
+
+ describe('when `showGroupIcon` prop is `true`', () => {
+ describe('when `parent` attribute is `null`', () => {
+ it('shows group icon', () => {
+ createComponent({ propsData: { showGroupIcon: true } });
+
+ expect(wrapper.findByTestId('group-icon').exists()).toBe(true);
+ });
+ });
+
+ describe('when `parent` attribute is set', () => {
+ it('shows subgroup icon', () => {
+ createComponent({
+ propsData: {
+ showGroupIcon: true,
+ group: {
+ ...group,
+ parent: {
+ id: 'gid://gitlab/Group/35',
+ },
+ },
+ },
+ });
+
+ expect(wrapper.findByTestId('subgroup-icon').exists()).toBe(true);
+ });
+ });
+ });
+
+ describe('when `showGroupIcon` prop is `false`', () => {
+ it('does not show group icon', () => {
+ createComponent();
+
+ expect(wrapper.findByTestId('group-icon').exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/groups_list/groups_list_spec.js b/spec/frontend/vue_shared/components/groups_list/groups_list_spec.js
new file mode 100644
index 00000000000..c65aa347bcf
--- /dev/null
+++ b/spec/frontend/vue_shared/components/groups_list/groups_list_spec.js
@@ -0,0 +1,34 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import GroupsList from '~/vue_shared/components/groups_list/groups_list.vue';
+import GroupsListItem from '~/vue_shared/components/groups_list/groups_list_item.vue';
+import { groups } from './mock_data';
+
+describe('GroupsList', () => {
+ let wrapper;
+
+ const defaultPropsData = {
+ groups,
+ };
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(GroupsList, {
+ propsData: defaultPropsData,
+ });
+ };
+
+ it('renders list with `GroupsListItem` component', () => {
+ createComponent();
+
+ const groupsListItemWrappers = wrapper.findAllComponents(GroupsListItem).wrappers;
+ const expectedProps = groupsListItemWrappers.map((groupsListItemWrapper) =>
+ groupsListItemWrapper.props(),
+ );
+
+ expect(expectedProps).toEqual(
+ defaultPropsData.groups.map((group) => ({
+ group,
+ showGroupIcon: false,
+ })),
+ );
+ });
+});
diff --git a/spec/frontend/vue_shared/components/groups_list/mock_data.js b/spec/frontend/vue_shared/components/groups_list/mock_data.js
new file mode 100644
index 00000000000..0dad27f8311
--- /dev/null
+++ b/spec/frontend/vue_shared/components/groups_list/mock_data.js
@@ -0,0 +1,35 @@
+export const groups = [
+ {
+ id: 1,
+ fullName: 'Gitlab Org',
+ parent: null,
+ webUrl: 'http://127.0.0.1:3000/groups/gitlab-org',
+ descriptionHtml:
+ '<p data-sourcepos="1:1-1:64" dir="auto">Dolorem dolorem omnis impedit cupiditate pariatur officia velit. Fusce eget orci a ipsum tempus vehicula. Donec rhoncus ante sed lacus pharetra, vitae imperdiet felis lobortis. Donec maximus dapibus orci, sit amet euismod dolor rhoncus vel. In nec mauris nibh.</p>',
+ avatarUrl: null,
+ descendantGroupsCount: 1,
+ projectsCount: 1,
+ groupMembersCount: 2,
+ visibility: 'internal',
+ accessLevel: {
+ integerValue: 10,
+ },
+ },
+ {
+ id: 2,
+ fullName: 'Gitlab Org / test subgroup',
+ parent: {
+ id: 1,
+ },
+ webUrl: 'http://127.0.0.1:3000/groups/gitlab-org/test-subgroup',
+ descriptionHtml: '',
+ avatarUrl: null,
+ descendantGroupsCount: 4,
+ projectsCount: 4,
+ groupMembersCount: 4,
+ visibility: 'private',
+ accessLevel: {
+ integerValue: 20,
+ },
+ },
+];
diff --git a/spec/frontend/vue_shared/components/help_popover_spec.js b/spec/frontend/vue_shared/components/help_popover_spec.js
index 76e66d07fa0..e39061476b4 100644
--- a/spec/frontend/vue_shared/components/help_popover_spec.js
+++ b/spec/frontend/vue_shared/components/help_popover_spec.js
@@ -74,6 +74,22 @@ describe('HelpPopover', () => {
});
});
+ describe('with trigger classes', () => {
+ it.each`
+ triggerClass
+ ${'class-a class-b'}
+ ${['class-a', 'class-b']}
+ ${{ 'class-a': true, 'class-b': true }}
+ `('renders button with classes given $triggerClass', ({ triggerClass }) => {
+ createComponent({
+ props: { triggerClass },
+ });
+
+ expect(findQuestionButton().classes('class-a')).toBe(true);
+ expect(findQuestionButton().classes('class-b')).toBe(true);
+ });
+ });
+
describe('with other options', () => {
const placement = 'bottom';
diff --git a/spec/frontend/vue_shared/components/listbox_input/listbox_input_spec.js b/spec/frontend/vue_shared/components/listbox_input/listbox_input_spec.js
index b782a2b19da..141c3aa7da6 100644
--- a/spec/frontend/vue_shared/components/listbox_input/listbox_input_spec.js
+++ b/spec/frontend/vue_shared/components/listbox_input/listbox_input_spec.js
@@ -96,6 +96,15 @@ describe('ListboxInput', () => {
expect(findGlListbox().props('fluidWidth')).toBe(fluidWidth);
});
+ it.each(['class-a class-b', ['class-a', 'class-b'], { 'class-a': true, 'class-b': true }])(
+ 'passes %s class to listbox',
+ (toggleClass) => {
+ createComponent({ toggleClass });
+
+ expect(findGlListbox().props('toggleClass')).toBe(toggleClass);
+ },
+ );
+
it.each(['right', 'left'])("passes %s to the listbox's placement prop", (placement) => {
createComponent({ placement });
diff --git a/spec/frontend/vue_shared/components/markdown/comment_templates_dropdown_spec.js b/spec/frontend/vue_shared/components/markdown/comment_templates_dropdown_spec.js
index 2bef6dd15df..cd9f27dccbd 100644
--- a/spec/frontend/vue_shared/components/markdown/comment_templates_dropdown_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/comment_templates_dropdown_spec.js
@@ -1,11 +1,18 @@
+import { GlCollapsibleListbox } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import savedRepliesResponse from 'test_fixtures/graphql/comment_templates/saved_replies.query.graphql.json';
+import { mockTracking } from 'helpers/tracking_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
+import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import waitForPromises from 'helpers/wait_for_promises';
import CommentTemplatesDropdown from '~/vue_shared/components/markdown/comment_templates_dropdown.vue';
import savedRepliesQuery from '~/vue_shared/components/markdown/saved_replies.query.graphql';
+import {
+ TRACKING_SAVED_REPLIES_USE,
+ TRACKING_SAVED_REPLIES_USE_IN_MR,
+} from '~/vue_shared/components/markdown/constants';
let wrapper;
let savedRepliesResp;
@@ -31,19 +38,24 @@ function createComponent(options = {}) {
});
}
-describe('Comment templates dropdown', () => {
- it('fetches data when dropdown gets opened', async () => {
- const mockApollo = createMockApolloProvider(savedRepliesResponse);
- wrapper = createComponent({ mockApollo });
+function findDropdownComponent() {
+ return wrapper.findComponent(GlCollapsibleListbox);
+}
- wrapper.find('.js-comment-template-toggle').trigger('click');
+async function selectSavedReply() {
+ const dropdown = findDropdownComponent();
- await waitForPromises();
+ dropdown.vm.$emit('shown');
- expect(savedRepliesResp).toHaveBeenCalled();
- });
+ await waitForPromises();
+
+ dropdown.vm.$emit('select', savedRepliesResponse.data.currentUser.savedReplies.nodes[0].id);
+}
+
+useMockLocationHelper();
- it('adds emits a select event on selecting a comment', async () => {
+describe('Comment templates dropdown', () => {
+ it('fetches data when dropdown gets opened', async () => {
const mockApollo = createMockApolloProvider(savedRepliesResponse);
wrapper = createComponent({ mockApollo });
@@ -51,8 +63,67 @@ describe('Comment templates dropdown', () => {
await waitForPromises();
- wrapper.find('.gl-new-dropdown-item').trigger('click');
+ expect(savedRepliesResp).toHaveBeenCalled();
+ });
- expect(wrapper.emitted().select[0]).toEqual(['Saved Reply Content']);
+ describe('when selecting a comment', () => {
+ let trackingSpy;
+ let mockApollo;
+
+ beforeEach(() => {
+ trackingSpy = mockTracking(undefined, window.document, jest.spyOn);
+ mockApollo = createMockApolloProvider(savedRepliesResponse);
+ wrapper = createComponent({ mockApollo });
+ });
+
+ it('emits a select event', async () => {
+ wrapper.find('.js-comment-template-toggle').trigger('click');
+
+ await waitForPromises();
+
+ wrapper.find('.gl-new-dropdown-item').trigger('click');
+
+ expect(wrapper.emitted().select[0]).toEqual(['Saved Reply Content']);
+ });
+
+ describe('tracking', () => {
+ it('tracks overall usage', async () => {
+ await selectSavedReply();
+
+ expect(trackingSpy).toHaveBeenCalledWith(
+ expect.any(String),
+ TRACKING_SAVED_REPLIES_USE,
+ expect.any(Object),
+ );
+ });
+
+ describe('MR-specific usage event', () => {
+ it('is sent when in an MR', async () => {
+ window.location.toString.mockReturnValue('this/looks/like/a/-/merge_requests/1');
+
+ await selectSavedReply();
+
+ expect(trackingSpy).toHaveBeenCalledWith(
+ expect.any(String),
+ TRACKING_SAVED_REPLIES_USE_IN_MR,
+ expect.any(Object),
+ );
+ expect(trackingSpy).toHaveBeenCalledTimes(2);
+ });
+
+ it('is not sent when not in an MR', async () => {
+ window.location.toString.mockReturnValue('this/looks/like/a/-/issues/1');
+
+ await selectSavedReply();
+
+ expect(trackingSpy).not.toHaveBeenCalledWith(
+ expect.any(String),
+ TRACKING_SAVED_REPLIES_USE_IN_MR,
+ expect.any(Object),
+ );
+ expect(trackingSpy).toHaveBeenCalledTimes(1);
+ });
+ });
+ });
});
});
diff --git a/spec/frontend/vue_shared/components/markdown/header_spec.js b/spec/frontend/vue_shared/components/markdown/header_spec.js
index eb728879fb7..40875ed5dbc 100644
--- a/spec/frontend/vue_shared/components/markdown/header_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/header_spec.js
@@ -46,84 +46,39 @@ describe('Markdown field header component', () => {
createWrapper();
});
- describe('markdown header buttons', () => {
+ describe.each`
+ i | buttonTitle | nonMacTitle | buttonType
+ ${0} | ${'Insert suggestion'} | ${'Insert suggestion'} | ${'codeSuggestion'}
+ ${1} | ${'Add bold text (⌘B)'} | ${'Add bold text (Ctrl+B)'} | ${'bold'}
+ ${2} | ${'Add italic text (⌘I)'} | ${'Add italic text (Ctrl+I)'} | ${'italic'}
+ ${3} | ${'Add strikethrough text (⌘⇧X)'} | ${'Add strikethrough text (Ctrl+Shift+X)'} | ${'strike'}
+ ${4} | ${'Insert a quote'} | ${'Insert a quote'} | ${'blockquote'}
+ ${5} | ${'Insert code'} | ${'Insert code'} | ${'code'}
+ ${6} | ${'Add a link (⌘K)'} | ${'Add a link (Ctrl+K)'} | ${'link'}
+ ${7} | ${'Add a bullet list'} | ${'Add a bullet list'} | ${'bulletList'}
+ ${8} | ${'Add a numbered list'} | ${'Add a numbered list'} | ${'orderedList'}
+ ${9} | ${'Add a checklist'} | ${'Add a checklist'} | ${'taskList'}
+ ${10} | ${'Indent line (⌘])'} | ${'Indent line (Ctrl+])'} | ${'indent'}
+ ${11} | ${'Outdent line (⌘[)'} | ${'Outdent line (Ctrl+[)'} | ${'outdent'}
+ ${12} | ${'Add a collapsible section'} | ${'Add a collapsible section'} | ${'details'}
+ ${13} | ${'Add a table'} | ${'Add a table'} | ${'table'}
+ ${14} | ${'Attach a file or image'} | ${'Attach a file or image'} | ${'upload'}
+ ${15} | ${'Go full screen'} | ${'Go full screen'} | ${'fullScreen'}
+ `('markdown header buttons', ({ i, buttonTitle, nonMacTitle, buttonType }) => {
it('renders the buttons with the correct title', () => {
- const buttons = [
- 'Insert suggestion',
- 'Add bold text (⌘B)',
- 'Add italic text (⌘I)',
- 'Add strikethrough text (⌘⇧X)',
- 'Insert a quote',
- 'Insert code',
- 'Add a link (⌘K)',
- 'Add a bullet list',
- 'Add a numbered list',
- 'Add a checklist',
- 'Indent line (⌘])',
- 'Outdent line (⌘[)',
- 'Add a collapsible section',
- 'Add a table',
- 'Go full screen',
- ];
- const elements = findToolbarButtons();
-
- elements.wrappers.forEach((buttonEl, index) => {
- expect(buttonEl.props('buttonTitle')).toBe(buttons[index]);
- });
+ expect(findToolbarButtons().wrappers[i].props('buttonTitle')).toBe(buttonTitle);
});
it('renders correct title on non MacOS systems', () => {
- window.gl = {
- client: {
- isMac: false,
- },
- };
+ window.gl = { client: { isMac: false } };
createWrapper();
- const buttons = [
- 'Insert suggestion',
- 'Add bold text (Ctrl+B)',
- 'Add italic text (Ctrl+I)',
- 'Add strikethrough text (Ctrl+Shift+X)',
- 'Insert a quote',
- 'Insert code',
- 'Add a link (Ctrl+K)',
- 'Add a bullet list',
- 'Add a numbered list',
- 'Add a checklist',
- 'Indent line (Ctrl+])',
- 'Outdent line (Ctrl+[)',
- 'Add a collapsible section',
- 'Add a table',
- 'Go full screen',
- ];
- const elements = findToolbarButtons();
-
- elements.wrappers.forEach((buttonEl, index) => {
- expect(buttonEl.props('buttonTitle')).toBe(buttons[index]);
- });
- });
-
- it('renders "Attach a file or image" button using gl-button', () => {
- const button = wrapper.findByTestId('button-attach-file');
-
- expect(button.element.tagName).toBe('GL-BUTTON-STUB');
- expect(button.attributes('title')).toBe('Attach a file or image');
+ expect(findToolbarButtons().wrappers[i].props('buttonTitle')).toBe(nonMacTitle);
});
- describe('when the user is on a non-Mac', () => {
- beforeEach(() => {
- delete window.gl.client.isMac;
-
- createWrapper();
- });
-
- it('renders keyboard shortcuts with Ctrl+ instead of ⌘', () => {
- const boldButton = findToolbarButtonByProp('icon', 'bold');
-
- expect(boldButton.props('buttonTitle')).toBe('Add bold text (Ctrl+B)');
- });
+ it('passes button type to `trackingProperty` prop', () => {
+ expect(findToolbarButtons().wrappers[i].props('trackingProperty')).toBe(buttonType);
});
});
diff --git a/spec/frontend/vue_shared/components/markdown/toolbar_button_spec.js b/spec/frontend/vue_shared/components/markdown/toolbar_button_spec.js
index 33e9d6add99..54510bf043d 100644
--- a/spec/frontend/vue_shared/components/markdown/toolbar_button_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/toolbar_button_spec.js
@@ -1,6 +1,10 @@
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import ToolbarButton from '~/vue_shared/components/markdown/toolbar_button.vue';
+import {
+ TOOLBAR_CONTROL_TRACKING_ACTION,
+ MARKDOWN_EDITOR_TRACKING_LABEL,
+} from '~/vue_shared/components/markdown/tracking';
describe('toolbar_button', () => {
let wrapper;
@@ -20,9 +24,8 @@ describe('toolbar_button', () => {
});
};
- const getButtonShortcutsAttr = () => {
- return wrapper.findComponent(GlButton).attributes('data-md-shortcuts');
- };
+ const findToolbarButton = () => wrapper.findComponent(GlButton);
+ const getButtonShortcutsAttr = () => findToolbarButton().attributes('data-md-shortcuts');
describe('keyboard shortcuts', () => {
it.each`
@@ -40,4 +43,24 @@ describe('toolbar_button', () => {
},
);
});
+
+ it('adds tracking attributes to the button when `trackingProperty` prop is defined', () => {
+ const buttonType = 'bold';
+
+ createComponent({ trackingProperty: buttonType });
+
+ expect(findToolbarButton().attributes('data-track-action')).toBe(
+ TOOLBAR_CONTROL_TRACKING_ACTION,
+ );
+ expect(findToolbarButton().attributes('data-track-label')).toBe(MARKDOWN_EDITOR_TRACKING_LABEL);
+ expect(findToolbarButton().attributes('data-track-property')).toBe(buttonType);
+ });
+
+ it('does not add tracking attributes to the button when `trackingProperty` prop is undefined', () => {
+ createComponent();
+
+ ['data-track-action', 'data-track-label', 'data-track-property'].forEach((dataAttribute) => {
+ expect(findToolbarButton().attributes(dataAttribute)).toBeUndefined();
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/markdown/toolbar_spec.js b/spec/frontend/vue_shared/components/markdown/toolbar_spec.js
index 5bf11ff2b26..90d8ce3b500 100644
--- a/spec/frontend/vue_shared/components/markdown/toolbar_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/toolbar_spec.js
@@ -3,6 +3,7 @@ import Toolbar from '~/vue_shared/components/markdown/toolbar.vue';
import EditorModeSwitcher from '~/vue_shared/components/markdown/editor_mode_switcher.vue';
import { updateText } from '~/lib/utils/text_markdown';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { PROMO_URL } from 'jh_else_ce/lib/utils/url_utility';
jest.mock('~/lib/utils/text_markdown');
@@ -98,7 +99,7 @@ describe('toolbar', () => {
expect.objectContaining({
tag: `### Rich text editor
-Try out **styling** _your_ content right here or read the [direction](https://about.gitlab.com/direction/plan/knowledge/content_editor/).`,
+Try out **styling** _your_ content right here or read the [direction](${PROMO_URL}/direction/plan/knowledge/content_editor/).`,
textArea: document.querySelector('textarea'),
cursorOffset: 0,
wrap: false,
diff --git a/spec/frontend/vue_shared/components/metric_images/metric_images_tab_spec.js b/spec/frontend/vue_shared/components/metric_images/metric_images_tab_spec.js
index 4b0b89fe1e7..36f5517decf 100644
--- a/spec/frontend/vue_shared/components/metric_images/metric_images_tab_spec.js
+++ b/spec/frontend/vue_shared/components/metric_images/metric_images_tab_spec.js
@@ -2,6 +2,7 @@ import { GlFormInput, GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import merge from 'lodash/merge';
+// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import MetricImagesTable from '~/vue_shared/components/metric_images/metric_images_table.vue';
import MetricImagesTab from '~/vue_shared/components/metric_images/metric_images_tab.vue';
diff --git a/spec/frontend/vue_shared/components/metric_images/metric_images_table_spec.js b/spec/frontend/vue_shared/components/metric_images/metric_images_table_spec.js
index 12dca95e9ba..ca141f53bf1 100644
--- a/spec/frontend/vue_shared/components/metric_images/metric_images_table_spec.js
+++ b/spec/frontend/vue_shared/components/metric_images/metric_images_table_spec.js
@@ -2,6 +2,7 @@ import { GlLink, GlModal } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
import Vue from 'vue';
import merge from 'lodash/merge';
+// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import createStore from '~/vue_shared/components/metric_images/store';
import MetricsImageTable from '~/vue_shared/components/metric_images/metric_images_table.vue';
diff --git a/spec/frontend/vue_shared/components/metric_images/store/actions_spec.js b/spec/frontend/vue_shared/components/metric_images/store/actions_spec.js
index 626f6fc735e..544466a22ca 100644
--- a/spec/frontend/vue_shared/components/metric_images/store/actions_spec.js
+++ b/spec/frontend/vue_shared/components/metric_images/store/actions_spec.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import actionsFactory from '~/vue_shared/components/metric_images/store/actions';
import * as types from '~/vue_shared/components/metric_images/store/mutation_types';
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 2f8f97c5b95..7f3cf9820db 100644
--- a/spec/frontend/vue_shared/components/modal_copy_button_spec.js
+++ b/spec/frontend/vue_shared/components/modal_copy_button_spec.js
@@ -27,16 +27,19 @@ describe('modal copy button', () => {
wrapper.trigger('click');
await nextTick();
- expect(wrapper.emitted().success).not.toBeEmpty();
+ expect(wrapper.emitted('error')).toBeUndefined();
+ expect(wrapper.emitted('success')).toHaveLength(1);
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", async () => {
document.execCommand = jest.fn(() => false);
wrapper.trigger('click');
await nextTick();
- expect(wrapper.emitted().error).not.toBeEmpty();
+ expect(wrapper.emitted('success')).toBeUndefined();
+ expect(wrapper.emitted('error')).toHaveLength(1);
expect(document.execCommand).toHaveBeenCalledWith('copy');
});
});
diff --git a/spec/frontend/vue_shared/components/new_resource_dropdown/new_resource_dropdown_spec.js b/spec/frontend/vue_shared/components/new_resource_dropdown/new_resource_dropdown_spec.js
index f04e1976a5f..7efc0e162b8 100644
--- a/spec/frontend/vue_shared/components/new_resource_dropdown/new_resource_dropdown_spec.js
+++ b/spec/frontend/vue_shared/components/new_resource_dropdown/new_resource_dropdown_spec.js
@@ -138,7 +138,7 @@ describe('NewResourceDropdown component', () => {
});
it('dropdown button is not a link', () => {
- expect(findDropdown().attributes('split-href')).toBeUndefined();
+ expect(findDropdown().props('splitHref')).toBe('');
});
it('displays default text on the dropdown button', () => {
@@ -162,7 +162,7 @@ describe('NewResourceDropdown component', () => {
it('dropdown button is a link', () => {
const href = joinPaths(project1.webUrl, DASH_SCOPE, expectedPath);
- expect(findDropdown().attributes('split-href')).toBe(href);
+ expect(findDropdown().props('splitHref')).toBe(href);
});
it('displays project name on the dropdown button', () => {
@@ -199,7 +199,7 @@ describe('NewResourceDropdown component', () => {
await nextTick();
const dropdown = findDropdown();
- expect(dropdown.attributes('split-href')).toBe(
+ expect(dropdown.props('splitHref')).toBe(
joinPaths(project1.webUrl, DASH_SCOPE, 'issues/new'),
);
expect(dropdown.props('text')).toBe(`New issue in ${project1.name}`);
@@ -217,7 +217,7 @@ describe('NewResourceDropdown component', () => {
await nextTick();
const dropdown = findDropdown();
- expect(dropdown.attributes('split-href')).toBe(
+ expect(dropdown.props('splitHref')).toBe(
joinPaths(project1.webUrl, DASH_SCOPE, 'issues/new'),
);
expect(dropdown.props('text')).toBe(`New issue in ${project1.name}`);
diff --git a/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js b/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js
index 7e669fb7c71..6d4745e8e3d 100644
--- a/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js
+++ b/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js
@@ -1,5 +1,6 @@
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
+// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import IssuePlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue';
import { userDataMock } from 'jest/notes/mock_data';
diff --git a/spec/frontend/vue_shared/components/page_size_selector_spec.js b/spec/frontend/vue_shared/components/page_size_selector_spec.js
index fce7ceee2fe..ecb25fa7468 100644
--- a/spec/frontend/vue_shared/components/page_size_selector_spec.js
+++ b/spec/frontend/vue_shared/components/page_size_selector_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { GlCollapsibleListbox } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import PageSizeSelector, { PAGE_SIZES } from '~/vue_shared/components/page_size_selector.vue';
@@ -11,30 +11,30 @@ describe('Page size selector component', () => {
});
};
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
+ const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
- it.each(PAGE_SIZES)('shows expected text in the dropdown button for page size %s', (pageSize) => {
- createWrapper({ pageSize });
+ it.each(PAGE_SIZES)('shows expected text in the listbox button for page size %s', (pageSize) => {
+ createWrapper({ pageSize: pageSize.value });
- expect(findDropdown().props('text')).toBe(`Show ${pageSize} items`);
+ expect(findListbox().props('toggleText')).toBe(`Show ${pageSize.value} items`);
});
- it('shows the expected dropdown items', () => {
+ it('shows the expected listbox items', () => {
createWrapper();
+ const options = findListbox().props('items');
+
PAGE_SIZES.forEach((pageSize, index) => {
- expect(findDropdownItems().at(index).text()).toBe(`Show ${pageSize} items`);
+ expect(options[index].text).toBe(pageSize.text);
});
});
- it('will emit the new page size when a dropdown item is clicked', () => {
+ it('will emit the new page size when a listbox item is clicked', () => {
createWrapper();
- findDropdownItems().wrappers.forEach((itemWrapper, index) => {
- itemWrapper.vm.$emit('click');
-
- expect(wrapper.emitted('input')[index][0]).toBe(PAGE_SIZES[index]);
+ PAGE_SIZES.forEach((pageSize, index) => {
+ findListbox().vm.$emit('select', pageSize.value);
+ expect(wrapper.emitted('input')[index][0]).toBe(PAGE_SIZES[index].value);
});
});
});
diff --git a/spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js b/spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js
index 0e387d1c139..2490422e4e8 100644
--- a/spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js
+++ b/spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js
@@ -1,7 +1,10 @@
-import { GlAvatarLabeled, GlBadge, GlIcon, GlPopover } from '@gitlab/ui';
+import { GlAvatarLabeled, GlBadge, GlIcon, GlPopover, GlDisclosureDropdown } from '@gitlab/ui';
+import uniqueId from 'lodash/uniqueId';
import projects from 'test_fixtures/api/users/projects/get.json';
import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { __ } from '~/locale';
import ProjectsListItem from '~/vue_shared/components/projects_list/projects_list_item.vue';
+import { ACTION_EDIT, ACTION_DELETE } from '~/vue_shared/components/projects_list/constants';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import {
@@ -13,8 +16,9 @@ import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.
import { ACCESS_LEVEL_LABELS } from '~/access_level/constants';
import { FEATURABLE_DISABLED, FEATURABLE_ENABLED } from '~/featurable/constants';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import DeleteModal from '~/projects/components/shared/delete_modal.vue';
-jest.mock('lodash/uniqueId', () => (prefix) => `${prefix}1`);
+jest.mock('lodash/uniqueId');
describe('ProjectsListItem', () => {
let wrapper;
@@ -40,6 +44,10 @@ describe('ProjectsListItem', () => {
const findProjectDescription = () => wrapper.findByTestId('project-description');
const findVisibilityIcon = () => findAvatarLabeled().findComponent(GlIcon);
+ beforeEach(() => {
+ uniqueId.mockImplementation(jest.requireActual('lodash/uniqueId'));
+ });
+
it('renders project avatar', () => {
createComponent();
@@ -207,6 +215,10 @@ describe('ProjectsListItem', () => {
});
describe('if project has topics', () => {
+ beforeEach(() => {
+ uniqueId.mockImplementation((prefix) => `${prefix}1`);
+ });
+
it('renders first three topics', () => {
createComponent();
@@ -306,4 +318,72 @@ describe('ProjectsListItem', () => {
expect(wrapper.findByTestId('project-icon').exists()).toBe(false);
});
});
+
+ describe('when project has actions', () => {
+ const editPath = '/foo/bar/edit';
+
+ beforeEach(() => {
+ createComponent({
+ propsData: {
+ project: {
+ ...project,
+ actions: [ACTION_EDIT, ACTION_DELETE],
+ isForked: true,
+ editPath,
+ },
+ },
+ });
+ });
+
+ it('displays actions dropdown', () => {
+ expect(wrapper.findComponent(GlDisclosureDropdown).props()).toMatchObject({
+ items: [
+ {
+ id: ACTION_EDIT,
+ text: __('Edit'),
+ href: editPath,
+ },
+ {
+ id: ACTION_DELETE,
+ text: __('Delete'),
+ extraAttrs: {
+ class: 'gl-text-red-500!',
+ },
+ action: expect.any(Function),
+ },
+ ],
+ });
+ });
+
+ describe('when delete action is fired', () => {
+ beforeEach(() => {
+ wrapper
+ .findComponent(GlDisclosureDropdown)
+ .props('items')
+ .find((item) => item.id === ACTION_DELETE)
+ .action();
+ });
+
+ it('displays confirmation modal with correct props', () => {
+ expect(wrapper.findComponent(DeleteModal).props()).toMatchObject({
+ visible: true,
+ confirmPhrase: project.name,
+ isFork: true,
+ issuesCount: '0',
+ forksCount: '0',
+ starsCount: '0',
+ });
+ });
+
+ describe('when deletion is confirmed', () => {
+ beforeEach(() => {
+ wrapper.findComponent(DeleteModal).vm.$emit('primary');
+ });
+
+ it('emits `delete` event', () => {
+ expect(wrapper.emitted('delete')).toMatchObject([[project]]);
+ });
+ });
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/projects_list/projects_list_spec.js b/spec/frontend/vue_shared/components/projects_list/projects_list_spec.js
index a0adbb89894..fb195dfe08e 100644
--- a/spec/frontend/vue_shared/components/projects_list/projects_list_spec.js
+++ b/spec/frontend/vue_shared/components/projects_list/projects_list_spec.js
@@ -32,4 +32,18 @@ describe('ProjectsList', () => {
})),
);
});
+
+ describe('when `ProjectListItem` emits `delete` event', () => {
+ const [firstProject] = defaultPropsData.projects;
+
+ beforeEach(() => {
+ createComponent();
+
+ wrapper.findComponent(ProjectsListItem).vm.$emit('delete', firstProject);
+ });
+
+ it('emits `delete` event', () => {
+ expect(wrapper.emitted('delete')).toEqual([[firstProject]]);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/registry/persisted_dropdown_selection_spec.js b/spec/frontend/vue_shared/components/registry/persisted_dropdown_selection_spec.js
index b93fa37546f..400be4ad131 100644
--- a/spec/frontend/vue_shared/components/registry/persisted_dropdown_selection_spec.js
+++ b/spec/frontend/vue_shared/components/registry/persisted_dropdown_selection_spec.js
@@ -1,5 +1,5 @@
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { GlCollapsibleListbox, GlListboxItem } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import component from '~/vue_shared/components/registry/persisted_dropdown_selection.vue';
@@ -16,7 +16,7 @@ describe('Persisted dropdown selection', () => {
};
function createComponent({ props = {}, data = {} } = {}) {
- wrapper = shallowMount(component, {
+ wrapper = mount(component, {
propsData: {
...defaultProps,
...props,
@@ -28,8 +28,10 @@ describe('Persisted dropdown selection', () => {
}
const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
+ const findGlCollapsibleListbox = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findGlListboxItems = () => wrapper.findAllComponents(GlListboxItem);
+ const findGlListboxToggleText = () =>
+ findGlCollapsibleListbox().find('.gl-new-dropdown-button-text');
describe('local storage sync', () => {
it('uses the local storage sync component with the correct props', () => {
@@ -63,20 +65,22 @@ describe('Persisted dropdown selection', () => {
it('has a dropdown component', () => {
createComponent();
- expect(findDropdown().exists()).toBe(true);
+ expect(findGlCollapsibleListbox().exists()).toBe(true);
});
describe('dropdown text', () => {
it('when no selection shows the first', () => {
createComponent();
- expect(findDropdown().props('text')).toBe('Maven');
+ expect(findGlListboxToggleText().text()).toBe('Maven');
});
- it('when an option is selected, shows that option label', () => {
- createComponent({ data: { selected: defaultProps.options[1].value } });
+ it('when an option is selected, shows that option label', async () => {
+ createComponent();
+ findGlCollapsibleListbox().vm.$emit('select', defaultProps.options[1].value);
+ await nextTick();
- expect(findDropdown().props('text')).toBe('Gradle');
+ expect(findGlListboxToggleText().text()).toBe('Gradle');
});
});
@@ -84,34 +88,20 @@ describe('Persisted dropdown selection', () => {
it('has one item for each option', () => {
createComponent();
- expect(findDropdownItems()).toHaveLength(defaultProps.options.length);
- });
-
- it('binds the correct props', () => {
- createComponent({ data: { selected: defaultProps.options[0].value } });
-
- expect(findDropdownItems().at(0).props()).toMatchObject({
- isChecked: true,
- isCheckItem: true,
- });
-
- expect(findDropdownItems().at(1).props()).toMatchObject({
- isChecked: false,
- isCheckItem: true,
- });
+ expect(findGlListboxItems()).toHaveLength(defaultProps.options.length);
});
it('on click updates the data and emits event', async () => {
- createComponent({ data: { selected: defaultProps.options[0].value } });
- expect(findDropdownItems().at(0).props('isChecked')).toBe(true);
+ createComponent();
+ const selectedItem = 'gradle';
- findDropdownItems().at(1).vm.$emit('click');
+ expect(findGlCollapsibleListbox().props('selected')).toBe('maven');
+ findGlCollapsibleListbox().vm.$emit('select', selectedItem);
await nextTick();
- expect(wrapper.emitted('change')).toStrictEqual([['gradle']]);
- expect(findDropdownItems().at(0).props('isChecked')).toBe(false);
- expect(findDropdownItems().at(1).props('isChecked')).toBe(true);
+ expect(wrapper.emitted('change').at(-1)).toStrictEqual([selectedItem]);
+ expect(findGlCollapsibleListbox().props('selected')).toBe(selectedItem);
});
});
});
diff --git a/spec/frontend/vue_shared/components/registry/registry_search_spec.js b/spec/frontend/vue_shared/components/registry/registry_search_spec.js
index 59bb0646350..f86406d05cb 100644
--- a/spec/frontend/vue_shared/components/registry/registry_search_spec.js
+++ b/spec/frontend/vue_shared/components/registry/registry_search_spec.js
@@ -25,6 +25,8 @@ describe('Registry Search', () => {
orderBy: 'name',
search: [],
sort: 'asc',
+ after: null,
+ before: null,
};
const mountComponent = (propsData = defaultProps) => {
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 ec1451de470..138027be0cc 100644
--- a/spec/frontend/vue_shared/components/registry/title_area_spec.js
+++ b/spec/frontend/vue_shared/components/registry/title_area_spec.js
@@ -1,21 +1,16 @@
import { GlAvatar, GlSprintf, GlLink, GlSkeletonLoader } from '@gitlab/ui';
-import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import component from '~/vue_shared/components/registry/title_area.vue';
describe('title area', () => {
let wrapper;
- const DYNAMIC_SLOT = 'metadata-dynamic-slot';
-
const findSubHeaderSlot = () => wrapper.findByTestId('sub-header');
const findRightActionsSlot = () => wrapper.findByTestId('right-actions');
const findMetadataSlot = (name) => wrapper.findByTestId(name);
const findTitle = () => wrapper.findByTestId('title');
const findAvatar = () => wrapper.findComponent(GlAvatar);
const findInfoMessages = () => wrapper.findAllByTestId('info-message');
- const findDynamicSlot = () => wrapper.findByTestId(DYNAMIC_SLOT);
- const findSlotOrderElements = () => wrapper.findAll('[slot-test]');
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
const mountComponent = ({ propsData = { title: 'foo' }, slots } = {}) => {
@@ -93,19 +88,17 @@ describe('title area', () => {
`('$slotNames metadata slots', ({ slotNames }) => {
const slots = generateSlotMocks(slotNames);
- it('exist when the slot is present', async () => {
+ it('exist when the slot is present', () => {
mountComponent({ slots });
- await nextTick();
slotNames.forEach((name) => {
expect(findMetadataSlot(name).exists()).toBe(true);
});
});
- it('is/are hidden when metadata-loading is true', async () => {
+ it('is/are hidden when metadata-loading is true', () => {
mountComponent({ slots, propsData: { title: 'foo', metadataLoading: true } });
- await nextTick();
slotNames.forEach((name) => {
expect(findMetadataSlot(name).exists()).toBe(false);
});
@@ -115,67 +108,19 @@ describe('title area', () => {
describe('metadata skeleton loader', () => {
const slots = generateSlotMocks(['metadata-foo']);
- it('is hidden when metadata loading is false', async () => {
+ it('is hidden when metadata loading is false', () => {
mountComponent({ slots });
- await nextTick();
-
expect(findSkeletonLoader().exists()).toBe(false);
});
- it('is shown when metadata loading is true', async () => {
+ it('is shown when metadata loading is true', () => {
mountComponent({ propsData: { metadataLoading: true }, slots });
- await nextTick();
-
expect(findSkeletonLoader().exists()).toBe(true);
});
});
- 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();
-
- // updating the slots like we do on line 141 does not cause the updated lifecycle-hook to be triggered
- wrapper.vm.$forceUpdate();
- await 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'],
- };
-
- // updating the slots like we do on line 159 does not cause the updated lifecycle-hook to be triggered
- wrapper.vm.$forceUpdate();
- await 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/source_viewer/source_viewer_new_spec.js b/spec/frontend/vue_shared/components/source_viewer/source_viewer_new_spec.js
index 6b711b6b6b2..431ede17954 100644
--- a/spec/frontend/vue_shared/components/source_viewer/source_viewer_new_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/source_viewer_new_spec.js
@@ -7,15 +7,22 @@ import LineHighlighter from '~/blob/line_highlighter';
import addBlobLinksTracking from '~/blob/blob_links_tracking';
import { BLOB_DATA_MOCK, CHUNK_1, CHUNK_2, LANGUAGE_MOCK } from './mock_data';
-jest.mock('~/blob/line_highlighter');
+const lineHighlighter = new LineHighlighter();
+jest.mock('~/blob/line_highlighter', () =>
+ jest.fn().mockReturnValue({
+ highlightHash: jest.fn(),
+ }),
+);
jest.mock('~/blob/blob_links_tracking');
describe('Source Viewer component', () => {
let wrapper;
const CHUNKS_MOCK = [CHUNK_1, CHUNK_2];
+ const hash = '#L142';
const createComponent = () => {
wrapper = shallowMountExtended(SourceViewer, {
+ mocks: { $route: { hash } },
propsData: { blob: BLOB_DATA_MOCK, chunks: CHUNKS_MOCK },
});
};
@@ -48,4 +55,10 @@ describe('Source Viewer component', () => {
expect(findChunks().at(1).props()).toMatchObject(CHUNK_2);
});
});
+
+ describe('hash highlighting', () => {
+ it('calls highlightHash with expected parameter', () => {
+ expect(lineHighlighter.highlightHash).toHaveBeenCalledWith(hash);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js
index 6b1d65c5a6a..a486d13a856 100644
--- a/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js
@@ -1,6 +1,8 @@
import hljs from 'highlight.js/lib/core';
import Vue from 'vue';
import VueRouter from 'vue-router';
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer.vue';
import { registerPlugins } from '~/vue_shared/components/source_viewer/plugins/index';
@@ -14,7 +16,9 @@ import {
LEGACY_FALLBACKS,
CODEOWNERS_FILE_NAME,
CODEOWNERS_LANGUAGE,
+ SVELTE_LANGUAGE,
} from '~/vue_shared/components/source_viewer/constants';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import waitForPromises from 'helpers/wait_for_promises';
import LineHighlighter from '~/blob/line_highlighter';
import eventHub from '~/notes/event_hub';
@@ -25,6 +29,7 @@ jest.mock('highlight.js/lib/core');
jest.mock('~/vue_shared/components/source_viewer/plugins/index');
Vue.use(VueRouter);
const router = new VueRouter();
+const mockAxios = new MockAdapter(axios);
const generateContent = (content, totalLines = 1, delimiter = '\n') => {
let generatedContent = '';
@@ -71,6 +76,42 @@ describe('Source Viewer component', () => {
return createComponent();
});
+ describe('Displaying LFS blob', () => {
+ const rawPath = '/org/project/-/raw/file.xml';
+ const externalStorageUrl = 'http://127.0.0.1:9000/lfs-objects/91/12/1341234';
+ const rawTextBlob = 'This is the external content';
+ const blob = {
+ storedExternally: true,
+ externalStorage: 'lfs',
+ simpleViewer: { fileType: 'text' },
+ rawPath,
+ };
+
+ afterEach(() => {
+ mockAxios.reset();
+ });
+
+ it('Uses externalStorageUrl to fetch content if present', async () => {
+ mockAxios.onGet(externalStorageUrl).replyOnce(HTTP_STATUS_OK, rawTextBlob);
+
+ await createComponent({ ...blob, externalStorageUrl });
+
+ expect(mockAxios.history.get).toHaveLength(1);
+ expect(mockAxios.history.get[0].url).toBe(externalStorageUrl);
+ expect(wrapper.vm.$data.content).toBe(rawTextBlob);
+ });
+
+ it('Falls back to rawPath to fetch content', async () => {
+ mockAxios.onGet(rawPath).replyOnce(HTTP_STATUS_OK, rawTextBlob);
+
+ await createComponent(blob);
+
+ expect(mockAxios.history.get).toHaveLength(1);
+ expect(mockAxios.history.get[0].url).toBe(rawPath);
+ expect(wrapper.vm.$data.content).toBe(rawTextBlob);
+ });
+ });
+
describe('event tracking', () => {
it('fires a tracking event when the component is created', () => {
const eventData = { label: EVENT_LABEL_VIEWER, property: language };
@@ -120,6 +161,33 @@ describe('Source Viewer component', () => {
);
});
+ describe('sub-languages', () => {
+ const languageDefinition = {
+ subLanguage: 'xml',
+ contains: [{ subLanguage: 'javascript' }, { subLanguage: 'typescript' }],
+ };
+
+ beforeEach(async () => {
+ jest.spyOn(hljs, 'getLanguage').mockReturnValue(languageDefinition);
+ createComponent();
+ await waitForPromises();
+ });
+
+ it('registers the primary sub-language', () => {
+ expect(hljs.registerLanguage).toHaveBeenCalledWith(
+ languageDefinition.subLanguage,
+ expect.any(Function),
+ );
+ });
+
+ it.each(languageDefinition.contains)(
+ 'registers the rest of the sub-languages',
+ ({ subLanguage }) => {
+ expect(hljs.registerLanguage).toHaveBeenCalledWith(subLanguage, expect.any(Function));
+ },
+ );
+ });
+
it('registers json language definition if fileType is package_json', async () => {
await createComponent({ language: 'json', fileType: 'package_json' });
const languageDefinition = await import(`highlight.js/lib/languages/json`);
@@ -146,6 +214,18 @@ describe('Source Viewer component', () => {
);
});
+ it('registers svelte language definition if file name ends with .svelte', async () => {
+ await createComponent({ name: `component.${SVELTE_LANGUAGE}` });
+ const languageDefinition = await import(
+ '~/vue_shared/components/source_viewer/languages/svelte'
+ );
+
+ expect(hljs.registerLanguage).toHaveBeenCalledWith(
+ SVELTE_LANGUAGE,
+ languageDefinition.default,
+ );
+ });
+
it('highlights the first chunk', () => {
expect(hljs.highlight).toHaveBeenCalledWith(chunk1.trim(), { language: mappedLanguage });
expect(findChunks().at(0).props('isFirstChunk')).toBe(true);
diff --git a/spec/frontend/vue_shared/components/web_ide_link_spec.js b/spec/frontend/vue_shared/components/web_ide_link_spec.js
index b6c22ceaa23..56d89d428f7 100644
--- a/spec/frontend/vue_shared/components/web_ide_link_spec.js
+++ b/spec/frontend/vue_shared/components/web_ide_link_spec.js
@@ -1,15 +1,20 @@
-import { GlModal } from '@gitlab/ui';
+import { GlModal, GlDisclosureDropdown, GlDisclosureDropdownItem } from '@gitlab/ui';
+import { omit } from 'lodash';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import getWritableForksResponse from 'test_fixtures/graphql/vue_shared/components/web_ide/get_writable_forks.query.graphql_none.json';
-import ActionsButton from '~/vue_shared/components/actions_button.vue';
import WebIdeLink, { i18n } from '~/vue_shared/components/web_ide_link.vue';
import ConfirmForkModal from '~/vue_shared/components/web_ide/confirm_fork_modal.vue';
-import { stubComponent } from 'helpers/stub_component';
-import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
+import { stubComponent } from 'helpers/stub_component';
+import { mockTracking } from 'helpers/tracking_helper';
+import {
+ shallowMountExtended,
+ mountExtended,
+ extendedWrapper,
+} from 'helpers/vue_test_utils_helper';
import { visitUrl } from '~/lib/utils/url_utility';
import getWritableForksQuery from '~/vue_shared/components/web_ide/get_writable_forks.query.graphql';
@@ -26,13 +31,15 @@ const forkPath = '/some/fork/path';
const ACTION_EDIT = {
href: TEST_EDIT_URL,
- key: 'edit',
+ handle: undefined,
text: 'Edit single file',
secondaryText: 'Edit this file only.',
attrs: {
- 'data-qa-selector': 'edit_button',
- 'data-track-action': 'click_consolidated_edit',
- 'data-track-label': 'edit',
+ 'data-qa-selector': 'edit_menu_item',
+ },
+ tracking: {
+ action: 'click_consolidated_edit',
+ label: 'single_file',
},
};
const ACTION_EDIT_CONFIRM_FORK = {
@@ -41,15 +48,17 @@ const ACTION_EDIT_CONFIRM_FORK = {
handle: expect.any(Function),
};
const ACTION_WEB_IDE = {
- key: 'webide',
secondaryText: i18n.webIdeText,
text: 'Web IDE',
attrs: {
- 'data-qa-selector': 'web_ide_button',
- 'data-track-action': 'click_consolidated_edit_ide',
- 'data-track-label': 'web_ide',
+ 'data-qa-selector': 'webide_menu_item',
},
+ href: undefined,
handle: expect.any(Function),
+ tracking: {
+ action: 'click_consolidated_edit',
+ label: 'web_ide',
+ },
};
const ACTION_WEB_IDE_CONFIRM_FORK = {
...ACTION_WEB_IDE,
@@ -58,11 +67,15 @@ const ACTION_WEB_IDE_CONFIRM_FORK = {
const ACTION_WEB_IDE_EDIT_FORK = { ...ACTION_WEB_IDE, text: 'Edit fork in Web IDE' };
const ACTION_GITPOD = {
href: TEST_GITPOD_URL,
- key: 'gitpod',
+ handle: undefined,
secondaryText: 'Launch a ready-to-code development environment for your project.',
text: 'Gitpod',
attrs: {
- 'data-qa-selector': 'gitpod_button',
+ 'data-qa-selector': 'gitpod_menu_item',
+ },
+ tracking: {
+ action: 'click_consolidated_edit',
+ label: 'gitpod',
},
};
const ACTION_GITPOD_ENABLE = {
@@ -72,11 +85,14 @@ const ACTION_GITPOD_ENABLE = {
};
const ACTION_PIPELINE_EDITOR = {
href: TEST_PIPELINE_EDITOR_URL,
- key: 'pipeline_editor',
secondaryText: 'Edit, lint, and visualize your pipeline.',
text: 'Edit in pipeline editor',
attrs: {
- 'data-qa-selector': 'pipeline_editor_button',
+ 'data-qa-selector': 'pipeline_editor_menu_item',
+ },
+ tracking: {
+ action: 'click_consolidated_edit',
+ label: 'pipeline_editor',
},
};
@@ -84,6 +100,7 @@ describe('vue_shared/components/web_ide_link', () => {
Vue.use(VueApollo);
let wrapper;
+ let trackingSpy;
function createComponent(props, { mountFn = shallowMountExtended, slots = {} } = {}) {
const fakeApollo = createMockApollo([
@@ -108,16 +125,37 @@ describe('vue_shared/components/web_ide_link', () => {
<slot name="modal-footer"></slot>
</div>`,
}),
+ GlDisclosureDropdownItem,
},
apolloProvider: fakeApollo,
});
+
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
}
- const findActionsButton = () => wrapper.findComponent(ActionsButton);
+ const findDisclosureDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
+ const findDisclosureDropdownItems = () => wrapper.findAllComponents(GlDisclosureDropdownItem);
const findModal = () => wrapper.findComponent(GlModal);
const findForkConfirmModal = () => wrapper.findComponent(ConfirmForkModal);
+ const getDropdownItemsAsData = () =>
+ findDisclosureDropdownItems().wrappers.map((item) => {
+ const extendedWrapperItem = extendedWrapper(item);
+ const attributes = extendedWrapperItem.attributes();
+ const props = extendedWrapperItem.props();
+
+ return {
+ text: extendedWrapperItem.findByTestId('action-primary-text').text(),
+ secondaryText: extendedWrapperItem.findByTestId('action-secondary-text').text(),
+ href: props.item.href,
+ handle: props.item.handle,
+ attrs: {
+ 'data-qa-selector': attributes['data-qa-selector'],
+ },
+ };
+ });
+ const omitTrackingParams = (actions) => actions.map((action) => omit(action, 'tracking'));
- it.each([
+ describe.each([
{
props: {},
expectedActions: [ACTION_WEB_IDE, ACTION_EDIT],
@@ -207,10 +245,27 @@ describe('vue_shared/components/web_ide_link', () => {
props: { showEditButton: false },
expectedActions: [ACTION_WEB_IDE],
},
- ])('renders actions with appropriately for given props', ({ props, expectedActions }) => {
- createComponent(props);
+ ])('for a set of props', ({ props, expectedActions }) => {
+ beforeEach(() => {
+ createComponent(props);
+ });
+
+ it('renders the appropiate actions', () => {
+ // omit tracking property because it is not included in the dropdown item
+ expect(getDropdownItemsAsData()).toEqual(omitTrackingParams(expectedActions));
+ });
+
+ describe('when an action is clicked', () => {
+ it('tracks event', () => {
+ expectedActions.forEach((action, index) => {
+ findDisclosureDropdownItems().at(index).vm.$emit('action');
- expect(findActionsButton().props('actions')).toEqual(expectedActions);
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, action.tracking.action, {
+ label: action.tracking.label,
+ });
+ });
+ });
+ });
});
it('bubbles up shown and hidden events triggered by actions button component', () => {
@@ -219,17 +274,17 @@ describe('vue_shared/components/web_ide_link', () => {
expect(wrapper.emitted('shown')).toBe(undefined);
expect(wrapper.emitted('hidden')).toBe(undefined);
- findActionsButton().vm.$emit('shown');
- findActionsButton().vm.$emit('hidden');
+ findDisclosureDropdown().vm.$emit('shown');
+ findDisclosureDropdown().vm.$emit('hidden');
expect(wrapper.emitted('shown')).toHaveLength(1);
expect(wrapper.emitted('hidden')).toHaveLength(1);
});
- it('exposes a default slot', () => {
- const slotContent = 'default slot content';
+ it.each(['before-actions', 'after-actions'])('exposes a %s slot', (slot) => {
+ const slotContent = 'slot content';
- createComponent({}, { slots: { default: slotContent } });
+ createComponent({}, { slots: { [slot]: slotContent } });
expect(wrapper.text()).toContain(slotContent);
});
@@ -248,13 +303,13 @@ describe('vue_shared/components/web_ide_link', () => {
});
it('displays Pipeline Editor as the first action', () => {
- expect(findActionsButton().props()).toMatchObject({
- actions: [ACTION_PIPELINE_EDITOR, ACTION_WEB_IDE, ACTION_GITPOD],
- });
+ expect(getDropdownItemsAsData()).toEqual(
+ omitTrackingParams([ACTION_PIPELINE_EDITOR, ACTION_WEB_IDE, ACTION_GITPOD]),
+ );
});
it('when web ide button is clicked it opens in a new tab', async () => {
- findActionsButton().props('actions')[1].handle();
+ findDisclosureDropdownItems().at(1).props().item.handle();
await nextTick();
expect(visitUrl).toHaveBeenCalledWith(TEST_WEB_IDE_URL, true);
});
@@ -289,7 +344,7 @@ describe('vue_shared/components/web_ide_link', () => {
({ props, expectedEventPayload }) => {
createComponent({ ...props, needsToFork: true, disableForkModal: true });
- findActionsButton().props('actions')[0].handle();
+ findDisclosureDropdownItems().at(0).props().item.handle();
expect(wrapper.emitted('edit')).toEqual([[expectedEventPayload]]);
},
@@ -309,7 +364,7 @@ describe('vue_shared/components/web_ide_link', () => {
it.each(testActions)('opens the modal when the button is clicked', async ({ props }) => {
createComponent({ ...props, needsToFork: true }, { mountFn: mountExtended });
- wrapper.findComponent(ActionsButton).props().actions[0].handle();
+ findDisclosureDropdownItems().at(0).props().item.handle();
await nextTick();
await wrapper.findByRole('button', { name: /Web IDE|Edit/im }).trigger('click');
diff --git a/spec/frontend/vue_shared/issuable/create/components/issuable_create_root_spec.js b/spec/frontend/vue_shared/issuable/create/components/issuable_create_root_spec.js
index e983519d9fc..03f509a3fa3 100644
--- a/spec/frontend/vue_shared/issuable/create/components/issuable_create_root_spec.js
+++ b/spec/frontend/vue_shared/issuable/create/components/issuable_create_root_spec.js
@@ -1,8 +1,13 @@
import { mount } from '@vue/test-utils';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
import IssuableCreateRoot from '~/vue_shared/issuable/create/components/issuable_create_root.vue';
import IssuableForm from '~/vue_shared/issuable/create/components/issuable_form.vue';
+Vue.use(VueApollo);
+
const createComponent = ({
descriptionPreviewPath = '/gitlab-org/gitlab-shell/preview_markdown',
descriptionHelpPath = '/help/user/markdown',
@@ -16,6 +21,7 @@ const createComponent = ({
labelsFetchPath,
labelsManagePath,
},
+ apolloProvider: createMockApollo(),
slots: {
title: `
<h1 class="js-create-title">New Issuable</h1>
diff --git a/spec/frontend/vue_shared/issuable/create/components/issuable_form_spec.js b/spec/frontend/vue_shared/issuable/create/components/issuable_form_spec.js
index ae2fd5ebffa..338dc80b43e 100644
--- a/spec/frontend/vue_shared/issuable/create/components/issuable_form_spec.js
+++ b/spec/frontend/vue_shared/issuable/create/components/issuable_form_spec.js
@@ -2,8 +2,9 @@ import { GlFormInput } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import IssuableForm from '~/vue_shared/issuable/create/components/issuable_form.vue';
-import MarkdownField from '~/vue_shared/components/markdown/field.vue';
+import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
import LabelsSelect from '~/sidebar/components/labels/labels_select_vue/labels_select_root.vue';
+import { __ } from '~/locale';
const createComponent = ({
descriptionPreviewPath = '/gitlab-org/gitlab-shell/preview_markdown',
@@ -24,7 +25,7 @@ const createComponent = ({
`,
},
stubs: {
- MarkdownField,
+ MarkdownEditor,
},
});
};
@@ -71,18 +72,20 @@ describe('IssuableForm', () => {
expect(descriptionFieldEl.exists()).toBe(true);
expect(descriptionFieldEl.find('label').text()).toBe('Description');
- expect(descriptionFieldEl.findComponent(MarkdownField).exists()).toBe(true);
- expect(descriptionFieldEl.findComponent(MarkdownField).props()).toMatchObject({
- markdownPreviewPath: wrapper.vm.descriptionPreviewPath,
+ expect(descriptionFieldEl.findComponent(MarkdownEditor).exists()).toBe(true);
+ expect(descriptionFieldEl.findComponent(MarkdownEditor).props()).toMatchObject({
+ renderMarkdownPath: wrapper.vm.descriptionPreviewPath,
markdownDocsPath: wrapper.vm.descriptionHelpPath,
- addSpacingClasses: false,
- showSuggestPopover: true,
- textareaValue: '',
+ value: '',
+ formFieldProps: {
+ ariaLabel: __('Description'),
+ class: 'rspec-issuable-form-description',
+ placeholder: __('Write a comment or drag your files here…'),
+ dataQaSelector: 'issuable_form_description_field',
+ id: 'issuable-description',
+ name: 'issuable-description',
+ },
});
- expect(descriptionFieldEl.find('textarea').exists()).toBe(true);
- expect(descriptionFieldEl.find('textarea').attributes('placeholder')).toBe(
- 'Write a comment or drag your files here…',
- );
});
it('renders labels select field', () => {
diff --git a/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js b/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js
index 502fa609ebc..77333a878d1 100644
--- a/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js
+++ b/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js
@@ -15,6 +15,8 @@ const createComponent = ({
showCheckbox = true,
slots = {},
showWorkItemTypeIcon = false,
+ isActive = false,
+ preventRedirect = false,
} = {}) =>
shallowMount(IssuableItem, {
propsData: {
@@ -24,6 +26,8 @@ const createComponent = ({
showDiscussions: true,
showCheckbox,
showWorkItemTypeIcon,
+ isActive,
+ preventRedirect,
},
slots,
stubs: {
@@ -43,6 +47,8 @@ describe('IssuableItem', () => {
const findTimestampWrapper = () => wrapper.find('[data-testid="issuable-timestamp"]');
const findWorkItemTypeIcon = () => wrapper.findComponent(WorkItemTypeIcon);
+ const findIssuableTitleLink = () => wrapper.findComponentByTestId('issuable-title-link');
+ const findIssuableItemWrapper = () => wrapper.findByTestId('issuable-item-wrapper');
beforeEach(() => {
gon.gitlab_url = MOCK_GITLAB_URL;
@@ -553,4 +559,35 @@ describe('IssuableItem', () => {
});
});
});
+
+ describe('when preventing redirect on clicking the link', () => {
+ it('emits an event on item click', () => {
+ const { iid, webUrl } = mockIssuable;
+
+ wrapper = createComponent({
+ preventRedirect: true,
+ });
+
+ findIssuableTitleLink().vm.$emit('click', new MouseEvent('click'));
+
+ expect(wrapper.emitted('select-issuable')).toEqual([[{ iid, webUrl }]]);
+ });
+
+ it('does not apply highlighted class when item is not active', () => {
+ wrapper = createComponent({
+ preventRedirect: true,
+ });
+
+ expect(findIssuableItemWrapper().classes('gl-bg-blue-50')).toBe(false);
+ });
+
+ it('applies highlghted class when item is active', () => {
+ wrapper = createComponent({
+ isActive: true,
+ preventRedirect: true,
+ });
+
+ expect(findIssuableItemWrapper().classes('gl-bg-blue-50')).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js b/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js
index 68904603f40..51aae9b4512 100644
--- a/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js
+++ b/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js
@@ -530,4 +530,28 @@ describe('IssuableListRoot', () => {
expect(findIssuableGrid().exists()).toBe(true);
});
});
+
+ it('passes `isActive` prop as false if there is no active issuable', () => {
+ wrapper = createComponent({});
+
+ expect(findIssuableItem().props('isActive')).toBe(false);
+ });
+
+ it('passes `isActive` prop as true if active issuable matches issuable item', () => {
+ wrapper = createComponent({
+ props: {
+ activeIssuable: mockIssuableListProps.issuables[0],
+ },
+ });
+
+ expect(findIssuableItem().props('isActive')).toBe(true);
+ });
+
+ it('emits `select-issuable` event on emitting `select-issuable` from issuable item', () => {
+ const mockIssuable = mockIssuableListProps.issuables[0];
+ wrapper = createComponent({});
+ findIssuableItem().vm.$emit('select-issuable', mockIssuable);
+
+ expect(wrapper.emitted('select-issuable')).toEqual([[mockIssuable]]);
+ });
});
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js
index d2b7b2e89c8..4d08ad54e58 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js
@@ -1,195 +1,289 @@
-import { GlButton, GlBadge, GlIcon, GlAvatarLabeled, GlAvatarLink } from '@gitlab/ui';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import { TYPE_ISSUE, WORKSPACE_PROJECT } from '~/issues/constants';
+import { GlBadge, GlButton, GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { resetHTMLFixture, setHTMLFixture } from 'helpers/fixtures';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import {
+ STATUS_CLOSED,
+ STATUS_OPEN,
+ STATUS_REOPENED,
+ TYPE_ISSUE,
+ WORKSPACE_PROJECT,
+} from '~/issues/constants';
+import { __ } from '~/locale';
import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import IssuableHeader from '~/vue_shared/issuable/show/components/issuable_header.vue';
-import { mockIssuableShowProps, mockIssuable } from '../mock_data';
+import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
+import { mockIssuable, mockIssuableShowProps } from '../mock_data';
-const issuableHeaderProps = {
- ...mockIssuable,
- ...mockIssuableShowProps,
- issuableType: TYPE_ISSUE,
- workspaceType: WORKSPACE_PROJECT,
-};
-
-describe('IssuableHeader', () => {
+describe('IssuableHeader component', () => {
let wrapper;
- const findAvatar = () => wrapper.findByTestId('avatar');
- const findTaskStatusEl = () => wrapper.findByTestId('task-status');
- const findButton = () => wrapper.findComponent(GlButton);
- const findGlAvatarLink = () => wrapper.findComponent(GlAvatarLink);
+ const findConfidentialityBadge = () => wrapper.findComponent(ConfidentialityBadge);
+ const findStatusBadge = () => wrapper.findComponent(GlBadge);
+ const findToggleButton = () => wrapper.findComponent(GlButton);
+ const findAuthorLink = () => wrapper.findComponent(GlLink);
+ const findTimeAgoTooltip = () => wrapper.findComponent(TimeAgoTooltip);
+ const findWorkItemTypeIcon = () => wrapper.findComponent(WorkItemTypeIcon);
+ const findGlIconWithName = (name) =>
+ wrapper.findAllComponents(GlIcon).filter((component) => component.props('name') === name);
+ const findIcon = (name) =>
+ findGlIconWithName(name).exists() ? findGlIconWithName(name).at(0) : undefined;
+ const findBlockedIcon = () => findIcon('lock');
+ const findHiddenIcon = () => findIcon('spam');
+ const findExternalLinkIcon = () => findIcon('external-link');
+ const findFirstContributionIcon = () => findIcon('first-contribution');
+ const findComponentTooltip = (component) => getBinding(component.element, 'gl-tooltip');
const createComponent = (props = {}, { stubs } = {}) => {
- wrapper = shallowMountExtended(IssuableHeader, {
+ wrapper = shallowMount(IssuableHeader, {
+ directives: {
+ GlTooltip: createMockDirective('gl-tooltip'),
+ },
propsData: {
- ...issuableHeaderProps,
+ ...mockIssuable,
+ ...mockIssuableShowProps,
+ issuableState: STATUS_OPEN,
+ issuableType: TYPE_ISSUE,
+ workspaceType: WORKSPACE_PROJECT,
...props,
},
slots: {
- 'status-badge': 'Open',
- 'header-actions': `
- <button class="js-close">Close issuable</button>
- <a class="js-new" href="/gitlab-org/gitlab-shell/-/issues/new">New issuable</a>
- `,
+ 'header-actions': `Header actions slot`,
+ },
+ stubs: {
+ GlSprintf,
+ ...stubs,
},
- stubs,
});
};
- afterEach(() => {
- resetHTMLFixture();
- });
+ describe('status badge', () => {
+ describe('variant', () => {
+ it('is `success` when status is open', () => {
+ createComponent({ issuableState: STATUS_OPEN });
- describe('computed', () => {
- describe('authorId', () => {
- it('returns numeric ID from GraphQL ID of `author` prop', () => {
- createComponent();
- expect(findGlAvatarLink().attributes('data-user-id')).toBe('1');
+ expect(findStatusBadge().props('variant')).toBe('success');
+ });
+
+ it('is `success` when status is reopened', () => {
+ createComponent({ issuableState: STATUS_REOPENED });
+
+ expect(findStatusBadge().props('variant')).toBe('success');
+ });
+
+ it('is `info` when status is closed', () => {
+ createComponent({ issuableState: STATUS_CLOSED });
+
+ expect(findStatusBadge().props('variant')).toBe('info');
});
});
- });
- describe('handleRightSidebarToggleClick', () => {
- beforeEach(() => {
- setHTMLFixture('<button class="js-toggle-right-sidebar-button">Collapse sidebar</button>');
+ describe('icon', () => {
+ it('renders when statusIcon prop exists', () => {
+ createComponent({ statusIcon: 'issues' });
+
+ expect(findStatusBadge().findComponent(GlIcon).props('name')).toBe('issues');
+ });
+
+ it('does not render when statusIcon prop does not exist', () => {
+ createComponent({ statusIcon: '' });
+
+ expect(findStatusBadge().findComponent(GlIcon).exists()).toBe(false);
+ });
});
- it('emits a "toggle" event', () => {
+ it('renders status text', () => {
createComponent();
- findButton().vm.$emit('click');
+ expect(findStatusBadge().text()).toBe(__('Open'));
+ });
+ });
+
+ describe('confidential badge', () => {
+ it('renders when issuable is confidential', () => {
+ createComponent({ confidential: true });
+
+ expect(findConfidentialityBadge().props()).toEqual({
+ issuableType: 'issue',
+ workspaceType: 'project',
+ });
+ });
+
+ it('does not render when issuable is not confidential', () => {
+ createComponent({ confidential: false });
- expect(wrapper.emitted('toggle')).toEqual([[]]);
+ expect(findConfidentialityBadge().exists()).toBe(false);
});
+ });
- it('dispatches `click` event on sidebar toggle button', () => {
- createComponent();
- const toggleSidebarButtonEl = document.querySelector('.js-toggle-right-sidebar-button');
- const dispatchEvent = jest
- .spyOn(toggleSidebarButtonEl, 'dispatchEvent')
- .mockImplementation(jest.fn);
+ describe('blocked icon', () => {
+ it('renders when issuable is blocked', () => {
+ createComponent({ blocked: true });
- findButton().vm.$emit('click');
+ expect(findBlockedIcon().props('ariaLabel')).toBe('Blocked');
+ });
- expect(dispatchEvent).toHaveBeenCalledWith(
- expect.objectContaining({
- type: 'click',
- }),
+ it('has tooltip', () => {
+ createComponent({ blocked: true });
+
+ expect(findComponentTooltip(findBlockedIcon())).toBeDefined();
+ expect(findBlockedIcon().attributes('title')).toBe(
+ 'This issue is locked. Only project members can comment.',
);
});
+
+ it('does not render when issuable is not blocked', () => {
+ createComponent({ blocked: false });
+
+ expect(findBlockedIcon()).toBeUndefined();
+ });
});
- describe('template', () => {
- it('renders issuable status icon and text', () => {
- createComponent();
- const statusBoxEl = wrapper.findComponent(GlBadge);
- const statusIconEl = statusBoxEl.findComponent(GlIcon);
+ describe('hidden icon', () => {
+ it('renders when issuable is hidden', () => {
+ createComponent({ isHidden: true });
- expect(statusBoxEl.exists()).toBe(true);
- expect(statusIconEl.props('name')).toBe(mockIssuableShowProps.statusIcon);
- expect(statusIconEl.attributes('class')).toBe(mockIssuableShowProps.statusIconClass);
- expect(statusBoxEl.text()).toContain('Open');
+ expect(findHiddenIcon().props('ariaLabel')).toBe('Hidden');
});
- it('renders blocked icon when issuable is blocked', () => {
- createComponent({
- blocked: true,
- });
+ it('has tooltip', () => {
+ createComponent({ isHidden: true });
- const blockedEl = wrapper.findByTestId('blocked');
+ expect(findComponentTooltip(findHiddenIcon())).toBeDefined();
+ expect(findHiddenIcon().attributes('title')).toBe(
+ 'This issue is hidden because its author has been banned',
+ );
+ });
- expect(blockedEl.exists()).toBe(true);
- expect(blockedEl.findComponent(GlIcon).props('name')).toBe('lock');
+ it('does not render when issuable is not hidden', () => {
+ createComponent({ isHidden: false });
+
+ expect(findHiddenIcon()).toBeUndefined();
});
+ });
- it('renders confidential icon when issuable is confidential', () => {
- createComponent({ confidential: true });
+ describe('work item type icon', () => {
+ it('renders when showWorkItemTypeIcon=true and work item type exists', () => {
+ createComponent({ showWorkItemTypeIcon: true, issuableType: 'issue' });
- expect(wrapper.findComponent(ConfidentialityBadge).props()).toEqual({
- issuableType: 'issue',
- workspaceType: 'project',
+ expect(findWorkItemTypeIcon().props()).toMatchObject({
+ showText: true,
+ workItemType: 'ISSUE',
});
});
- it('renders issuable author avatar', () => {
+ it('does not render when showWorkItemTypeIcon=false', () => {
+ createComponent({ showWorkItemTypeIcon: false });
+
+ expect(findWorkItemTypeIcon().exists()).toBe(false);
+ });
+ });
+
+ describe('timeago tooltip', () => {
+ it('renders', () => {
createComponent();
- const { username, name, webUrl, avatarUrl } = mockIssuable.author;
- const avatarElAttrs = {
+
+ expect(findTimeAgoTooltip().props('time')).toBe('2020-06-29T13:52:56Z');
+ });
+ });
+
+ describe('author', () => {
+ it('renders link', () => {
+ createComponent();
+
+ expect(findAuthorLink().text()).toContain('Administrator');
+ expect(findAuthorLink().attributes()).toMatchObject({
+ href: 'http://0.0.0.0:3000/root',
'data-user-id': '1',
- 'data-username': username,
- 'data-name': name,
- href: webUrl,
- target: '_blank',
- };
- const avatarEl = findAvatar();
- expect(avatarEl.exists()).toBe(true);
- expect(avatarEl.attributes()).toMatchObject(avatarElAttrs);
- expect(avatarEl.findComponent(GlAvatarLabeled).attributes()).toMatchObject({
- size: '24',
- src: avatarUrl,
- label: name,
});
- expect(avatarEl.findComponent(GlAvatarLabeled).findComponent(GlIcon).exists()).toBe(false);
+ expect(findAuthorLink().classes()).toContain('js-user-link');
+ });
+
+ describe('when author exists outside of GitLab', () => {
+ it('renders external link icon', () => {
+ createComponent({ author: { webUrl: 'https://example.com/test-user' } });
+
+ expect(findExternalLinkIcon().props('ariaLabel')).toBe('external link');
+ });
+ });
+ });
+
+ describe('first contribution icon', () => {
+ it('renders when isFirstContribution=true', () => {
+ createComponent({ isFirstContribution: true });
+
+ expect(findFirstContributionIcon().props('ariaLabel')).toBe('1st contribution!');
+ });
+
+ it('has tooltip', () => {
+ createComponent({ isFirstContribution: true });
+
+ expect(findComponentTooltip(findFirstContributionIcon())).toBeDefined();
+ expect(findFirstContributionIcon().attributes('title')).toBe('1st contribution!');
});
+ it('does not render when isFirstContribution=false', () => {
+ createComponent({ isFirstContribution: false });
+
+ expect(findFirstContributionIcon()).toBeUndefined();
+ });
+ });
+
+ describe('task status', () => {
it('renders task status text when `taskCompletionStatus` prop is defined', () => {
createComponent();
- expect(findTaskStatusEl().exists()).toBe(true);
- expect(findTaskStatusEl().text()).toContain('0 of 5 checklist items completed');
+ expect(wrapper.text()).toContain('0 of 5 checklist items completed');
});
it('does not render task status text when tasks count is 0', () => {
- createComponent({
- taskCompletionStatus: {
- count: 0,
- completedCount: 0,
- },
- });
+ createComponent({ taskCompletionStatus: { count: 0, completedCount: 0 } });
- expect(findTaskStatusEl().exists()).toBe(false);
+ expect(wrapper.text()).not.toContain('checklist item');
});
+ });
- it('renders sidebar toggle button', () => {
+ describe('sidebar toggle button', () => {
+ beforeEach(() => {
+ setHTMLFixture('<button class="js-toggle-right-sidebar-button">Collapse sidebar</button>');
createComponent();
- const toggleButtonEl = wrapper.findByTestId('sidebar-toggle');
-
- expect(toggleButtonEl.exists()).toBe(true);
- expect(toggleButtonEl.props('icon')).toBe('chevron-double-lg-left');
});
- it('renders header actions', () => {
- createComponent();
- const actionsEl = wrapper.findByTestId('header-actions');
+ afterEach(() => {
+ resetHTMLFixture();
+ });
- expect(actionsEl.find('button.js-close').exists()).toBe(true);
- expect(actionsEl.find('a.js-new').exists()).toBe(true);
+ it('renders', () => {
+ expect(findToggleButton().props('icon')).toBe('chevron-double-lg-left');
+ expect(findToggleButton().attributes('aria-label')).toBe('Expand sidebar');
});
- describe('when author exists outside of GitLab', () => {
- it("renders 'external-link' icon in avatar label", () => {
- createComponent(
- {
- author: {
- ...issuableHeaderProps.author,
- webUrl: 'https://jira.com/test-user/author.jpg',
- },
- },
- {
- stubs: {
- GlAvatarLabeled,
- },
- },
- );
-
- const avatarEl = wrapper.findComponent(GlAvatarLabeled);
- const icon = avatarEl.findComponent(GlIcon);
-
- expect(icon.exists()).toBe(true);
- expect(icon.props('name')).toBe('external-link');
+ describe('when clicked', () => {
+ it('emits a "toggle" event', () => {
+ findToggleButton().vm.$emit('click');
+
+ expect(wrapper.emitted('toggle')).toEqual([[]]);
+ });
+
+ it('dispatches `click` event on sidebar toggle button', () => {
+ const toggleSidebarButton = document.querySelector('.js-toggle-right-sidebar-button');
+ const dispatchEvent = jest
+ .spyOn(toggleSidebarButton, 'dispatchEvent')
+ .mockImplementation(jest.fn);
+
+ findToggleButton().vm.$emit('click');
+
+ expect(dispatchEvent).toHaveBeenCalledWith(expect.objectContaining({ type: 'click' }));
});
});
});
+
+ describe('header actions', () => {
+ it('renders slot', () => {
+ createComponent();
+
+ expect(wrapper.text()).toContain('Header actions slot');
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js
index f976e0499f0..ad7afefff12 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js
@@ -1,3 +1,4 @@
+import { GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import IssuableBody from '~/vue_shared/issuable/show/components/issuable_body.vue';
@@ -72,7 +73,7 @@ describe('IssuableShowRoot', () => {
author,
taskCompletionStatus,
});
- expect(issuableHeader.find('.issuable-status-badge').text()).toContain('Open');
+ expect(issuableHeader.findComponent(GlBadge).text()).toBe('Open');
expect(issuableHeader.find('.detail-page-header-actions button.js-close').exists()).toBe(
true,
);