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:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-09-19 04:45:44 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-09-19 04:45:44 +0300
commit85dc423f7090da0a52c73eb66faf22ddb20efff9 (patch)
tree9160f299afd8c80c038f08e1545be119f5e3f1e1 /spec/frontend/vue_shared/components
parent15c2c8c66dbe422588e5411eee7e68f1fa440bb8 (diff)
Add latest changes from gitlab-org/gitlab@13-4-stable-ee
Diffstat (limited to 'spec/frontend/vue_shared/components')
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap12
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap8
-rw-r--r--spec/frontend/vue_shared/components/actions_button_spec.js203
-rw-r--r--spec/frontend/vue_shared/components/alert_detail_table_spec.js74
-rw-r--r--spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/changed_file_icon_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/clone_dropdown_spec.js10
-rw-r--r--spec/frontend/vue_shared/components/commit_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/confirm_modal_spec.js22
-rw-r--r--spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js9
-rw-r--r--spec/frontend/vue_shared/components/diff_viewer/viewers/renamed_spec.js16
-rw-r--r--spec/frontend/vue_shared/components/dropdown/dropdown_search_input_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/file_finder/index_spec.js41
-rw-r--r--spec/frontend/vue_shared/components/file_row_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js138
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js203
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js73
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js97
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js207
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js106
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js102
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_dropdown_spec.js190
-rw-r--r--spec/frontend/vue_shared/components/icon_spec.js78
-rw-r--r--spec/frontend/vue_shared/components/issue/issue_milestone_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js38
-rw-r--r--spec/frontend/vue_shared/components/markdown/header_spec.js58
-rw-r--r--spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js20
-rw-r--r--spec/frontend/vue_shared/components/markdown/suggestion_diff_row_spec.js20
-rw-r--r--spec/frontend/vue_shared/components/markdown/toolbar_button_spec.js47
-rw-r--r--spec/frontend/vue_shared/components/notes/noteable_warning_spec.js8
-rw-r--r--spec/frontend/vue_shared/components/notes/timeline_entry_item_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/ordered_layout_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/paginated_list_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap60
-rw-r--r--spec/frontend/vue_shared/components/registry/__snapshots__/history_item_spec.js.snap42
-rw-r--r--spec/frontend/vue_shared/components/registry/code_instruction_spec.js117
-rw-r--r--spec/frontend/vue_shared/components/registry/details_row_spec.js71
-rw-r--r--spec/frontend/vue_shared/components/registry/history_item_spec.js67
-rw-r--r--spec/frontend/vue_shared/components/registry/list_item_spec.js135
-rw-r--r--spec/frontend/vue_shared/components/registry/metadata_item_spec.js101
-rw-r--r--spec/frontend/vue_shared/components/registry/title_area_spec.js98
-rw-r--r--spec/frontend/vue_shared/components/remove_member_modal_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/resizable_chart/__snapshots__/skeleton_loader_spec.js.snap739
-rw-r--r--spec/frontend/vue_shared/components/resizable_chart/skeleton_loader_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/build_custom_renderer_spec.js9
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer_spec.js87
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/renderers/mock_data.js40
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_attribute_definition_spec.js25
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_heading_spec.js12
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph_spec.js48
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list_spec.js38
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text_spec.js24
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_list_item_spec.js12
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_utils_spec.js70
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js18
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/mock_data.js46
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js30
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js15
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js13
-rw-r--r--spec/frontend/vue_shared/components/table_pagination_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/todo_button_spec.js48
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/user_popover/user_popover_spec.js15
-rw-r--r--spec/frontend/vue_shared/components/web_ide_link_spec.js106
68 files changed, 2913 insertions, 1027 deletions
diff --git a/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap
index e84eb7789d3..dfd114a2d1c 100644
--- a/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap
+++ b/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap
@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Clone Dropdown Button rendering matches the snapshot 1`] = `
-<gl-new-dropdown-stub
+<gl-dropdown-stub
category="primary"
headertext=""
right=""
@@ -12,9 +12,9 @@ exports[`Clone Dropdown Button rendering matches the snapshot 1`] = `
<div
class="pb-2 mx-1"
>
- <gl-new-dropdown-header-stub>
+ <gl-dropdown-section-header-stub>
Clone with SSH
- </gl-new-dropdown-header-stub>
+ </gl-dropdown-section-header-stub>
<div
class="mx-3"
@@ -53,9 +53,9 @@ exports[`Clone Dropdown Button rendering matches the snapshot 1`] = `
</div>
</div>
- <gl-new-dropdown-header-stub>
+ <gl-dropdown-section-header-stub>
Clone with HTTP
- </gl-new-dropdown-header-stub>
+ </gl-dropdown-section-header-stub>
<div
class="mx-3"
@@ -94,5 +94,5 @@ exports[`Clone Dropdown Button rendering matches the snapshot 1`] = `
</div>
</div>
</div>
-</gl-new-dropdown-stub>
+</gl-dropdown-stub>
`;
diff --git a/spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap
index cd4728baeaa..c2b97f1e7f9 100644
--- a/spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap
+++ b/spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap
@@ -4,7 +4,7 @@ exports[`Expand button on click when short text is provided renders button after
<span>
<button
aria-label="Click to expand text"
- class="btn js-text-expander-prepend text-expander btn-blank btn-default btn-md btn-icon button-ellipsis-horizontal gl-button"
+ class="btn js-text-expander-prepend text-expander btn-blank btn-default btn-md gl-button btn-icon button-ellipsis-horizontal"
style="display: none;"
type="button"
>
@@ -32,7 +32,7 @@ exports[`Expand button on click when short text is provided renders button after
<button
aria-label="Click to expand text"
- class="btn js-text-expander-append text-expander btn-blank btn-default btn-md btn-icon button-ellipsis-horizontal gl-button"
+ class="btn js-text-expander-append text-expander btn-blank btn-default btn-md gl-button btn-icon button-ellipsis-horizontal"
style=""
type="button"
>
@@ -56,7 +56,7 @@ exports[`Expand button when short text is provided renders button before text 1`
<span>
<button
aria-label="Click to expand text"
- class="btn js-text-expander-prepend text-expander btn-blank btn-default btn-md btn-icon button-ellipsis-horizontal gl-button"
+ class="btn js-text-expander-prepend text-expander btn-blank btn-default btn-md gl-button btn-icon button-ellipsis-horizontal"
type="button"
>
<!---->
@@ -83,7 +83,7 @@ exports[`Expand button when short text is provided renders button before text 1`
<button
aria-label="Click to expand text"
- class="btn js-text-expander-append text-expander btn-blank btn-default btn-md btn-icon button-ellipsis-horizontal gl-button"
+ class="btn js-text-expander-append text-expander btn-blank btn-default btn-md gl-button btn-icon button-ellipsis-horizontal"
style="display: none;"
type="button"
>
diff --git a/spec/frontend/vue_shared/components/actions_button_spec.js b/spec/frontend/vue_shared/components/actions_button_spec.js
new file mode 100644
index 00000000000..4dde9d726d1
--- /dev/null
+++ b/spec/frontend/vue_shared/components/actions_button_spec.js
@@ -0,0 +1,203 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlDropdown, GlLink } from '@gitlab/ui';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import ActionsButton from '~/vue_shared/components/actions_button.vue';
+
+const TEST_ACTION = {
+ key: 'action1',
+ text: 'Sample',
+ secondaryText: 'Lorem ipsum.',
+ tooltip: '',
+ href: '/sample',
+ attrs: { 'data-test': '123' },
+};
+const TEST_ACTION_2 = {
+ key: 'action2',
+ text: 'Sample 2',
+ secondaryText: 'Dolar sit amit.',
+ tooltip: 'Dolar sit amit.',
+ href: '#',
+ attrs: { 'data-test': '456' },
+};
+const TEST_TOOLTIP = 'Lorem ipsum dolar sit';
+
+describe('Actions button component', () => {
+ let wrapper;
+
+ function createComponent(props) {
+ wrapper = shallowMount(ActionsButton, {
+ propsData: { ...props },
+ directives: { GlTooltip: createMockDirective() },
+ });
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const getTooltip = child => {
+ const directiveBinding = getBinding(child.element, 'gl-tooltip');
+
+ return directiveBinding.value;
+ };
+ const findLink = () => wrapper.find(GlLink);
+ const findLinkTooltip = () => getTooltip(findLink());
+ const findDropdown = () => wrapper.find(GlDropdown);
+ const findDropdownTooltip = () => getTooltip(findDropdown());
+ const parseDropdownItems = () =>
+ findDropdown()
+ .findAll('gl-dropdown-item-stub,gl-dropdown-divider-stub')
+ .wrappers.map(x => {
+ if (x.is('gl-dropdown-divider-stub')) {
+ return { type: 'divider' };
+ }
+
+ const { isCheckItem, isChecked, secondaryText } = x.props();
+
+ return {
+ type: 'item',
+ isCheckItem,
+ isChecked,
+ secondaryText,
+ text: x.text(),
+ };
+ });
+ const clickOn = (child, evt = new Event('click')) => child.vm.$emit('click', evt);
+ const clickLink = (...args) => clickOn(findLink(), ...args);
+ const clickDropdown = (...args) => clickOn(findDropdown(), ...args);
+
+ describe('with 1 action', () => {
+ beforeEach(() => {
+ createComponent({ actions: [TEST_ACTION] });
+ });
+
+ it('should not render dropdown', () => {
+ expect(findDropdown().exists()).toBe(false);
+ });
+
+ it('should render single button', () => {
+ const link = findLink();
+
+ expect(link.attributes()).toEqual({
+ class: expect.any(String),
+ href: TEST_ACTION.href,
+ ...TEST_ACTION.attrs,
+ });
+ expect(link.text()).toBe(TEST_ACTION.text);
+ });
+
+ it('should have tooltip', () => {
+ expect(findLinkTooltip()).toBe(TEST_ACTION.tooltip);
+ });
+
+ it('should have attrs', () => {
+ expect(findLink().attributes()).toMatchObject(TEST_ACTION.attrs);
+ });
+
+ it('can click', () => {
+ expect(clickLink).not.toThrow();
+ });
+ });
+
+ describe('with 1 action with tooltip', () => {
+ it('should have tooltip', () => {
+ createComponent({ actions: [{ ...TEST_ACTION, tooltip: TEST_TOOLTIP }] });
+
+ expect(findLinkTooltip()).toBe(TEST_TOOLTIP);
+ });
+ });
+
+ describe('with 1 action with handle', () => {
+ it('can click and trigger handle', () => {
+ const handleClick = jest.fn();
+ createComponent({ actions: [{ ...TEST_ACTION, handle: handleClick }] });
+
+ const event = new Event('click');
+ clickLink(event);
+
+ expect(handleClick).toHaveBeenCalledWith(event);
+ });
+ });
+
+ describe('with multiple actions', () => {
+ let handleAction;
+
+ beforeEach(() => {
+ handleAction = jest.fn();
+
+ createComponent({ actions: [{ ...TEST_ACTION, handle: handleAction }, TEST_ACTION_2] });
+ });
+
+ it('should default to selecting first action', () => {
+ expect(findDropdown().attributes()).toMatchObject({
+ text: TEST_ACTION.text,
+ 'split-href': TEST_ACTION.href,
+ });
+ });
+
+ it('should handle first action click', () => {
+ const event = new Event('click');
+
+ clickDropdown(event);
+
+ expect(handleAction).toHaveBeenCalledWith(event);
+ });
+
+ it('should render dropdown items', () => {
+ expect(parseDropdownItems()).toEqual([
+ {
+ type: 'item',
+ isCheckItem: true,
+ isChecked: true,
+ secondaryText: TEST_ACTION.secondaryText,
+ text: TEST_ACTION.text,
+ },
+ { type: 'divider' },
+ {
+ type: 'item',
+ isCheckItem: true,
+ isChecked: false,
+ secondaryText: TEST_ACTION_2.secondaryText,
+ text: TEST_ACTION_2.text,
+ },
+ ]);
+ });
+
+ it('should select action 2 when clicked', () => {
+ expect(wrapper.emitted('select')).toBeUndefined();
+
+ const action2 = wrapper.find(`[data-testid="action_${TEST_ACTION_2.key}"]`);
+ action2.vm.$emit('click');
+
+ expect(wrapper.emitted('select')).toEqual([[TEST_ACTION_2.key]]);
+ });
+
+ it('should have tooltip value', () => {
+ expect(findDropdownTooltip()).toBe(TEST_ACTION.tooltip);
+ });
+ });
+
+ describe('with multiple actions and selectedKey', () => {
+ beforeEach(() => {
+ createComponent({ actions: [TEST_ACTION, TEST_ACTION_2], selectedKey: TEST_ACTION_2.key });
+ });
+
+ it('should show action 2 as selected', () => {
+ expect(parseDropdownItems()).toEqual([
+ expect.objectContaining({
+ type: 'item',
+ isChecked: false,
+ }),
+ { type: 'divider' },
+ expect.objectContaining({
+ type: 'item',
+ isChecked: true,
+ }),
+ ]);
+ });
+
+ it('should have tooltip value', () => {
+ expect(findDropdownTooltip()).toBe(TEST_ACTION_2.tooltip);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/alert_detail_table_spec.js b/spec/frontend/vue_shared/components/alert_detail_table_spec.js
new file mode 100644
index 00000000000..9c38ccad8a7
--- /dev/null
+++ b/spec/frontend/vue_shared/components/alert_detail_table_spec.js
@@ -0,0 +1,74 @@
+import { mount } from '@vue/test-utils';
+import { GlTable, GlLoadingIcon } from '@gitlab/ui';
+import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
+
+const mockAlert = {
+ iid: '1527542',
+ title: 'SyntaxError: Invalid or unexpected token',
+ severity: 'CRITICAL',
+ eventCount: 7,
+ createdAt: '2020-04-17T23:18:14.996Z',
+ startedAt: '2020-04-17T23:18:14.996Z',
+ endedAt: '2020-04-17T23:18:14.996Z',
+ status: 'TRIGGERED',
+ assignees: { nodes: [] },
+ notes: { nodes: [] },
+ todos: { nodes: [] },
+};
+
+describe('AlertDetails', () => {
+ let wrapper;
+
+ function mountComponent(propsData = {}) {
+ wrapper = mount(AlertDetailsTable, {
+ propsData: {
+ alert: mockAlert,
+ loading: false,
+ ...propsData,
+ },
+ });
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ const findTableComponent = () => wrapper.find(GlTable);
+
+ describe('Alert details', () => {
+ describe('empty state', () => {
+ beforeEach(() => {
+ mountComponent({ alert: null });
+ });
+
+ it('shows an empty state when no alert is provided', () => {
+ expect(wrapper.text()).toContain('No alert data to display.');
+ });
+ });
+
+ describe('loading state', () => {
+ beforeEach(() => {
+ mountComponent({ loading: true });
+ });
+
+ it('displays a loading state when loading', () => {
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ });
+ });
+
+ describe('with table data', () => {
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it('renders a table', () => {
+ expect(findTableComponent().exists()).toBe(true);
+ });
+
+ it('renders a cell based on alert data', () => {
+ expect(findTableComponent().text()).toContain('SyntaxError: Invalid or unexpected token');
+ });
+ });
+ });
+});
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 5cf42ecdc1d..22643a17b2b 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
@@ -36,6 +36,6 @@ describe('Blob Rich Viewer component', () => {
});
it('is using Markdown View Field', () => {
- expect(wrapper.contains(MarkdownFieldView)).toBe(true);
+ expect(wrapper.find(MarkdownFieldView).exists()).toBe(true);
});
});
diff --git a/spec/frontend/vue_shared/components/changed_file_icon_spec.js b/spec/frontend/vue_shared/components/changed_file_icon_spec.js
index 03519a6f803..80918c5e771 100644
--- a/spec/frontend/vue_shared/components/changed_file_icon_spec.js
+++ b/spec/frontend/vue_shared/components/changed_file_icon_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
+import { GlIcon } from '@gitlab/ui';
import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue';
-import Icon from '~/vue_shared/components/icon.vue';
const changedFile = () => ({ changed: true });
const stagedFile = () => ({ changed: true, staged: true });
@@ -25,7 +25,7 @@ describe('Changed file icon', () => {
wrapper.destroy();
});
- const findIcon = () => wrapper.find(Icon);
+ const findIcon = () => wrapper.find(GlIcon);
const findIconName = () => findIcon().props('name');
const findIconClasses = () => findIcon().classes();
const findTooltipText = () => wrapper.attributes('title');
diff --git a/spec/frontend/vue_shared/components/clone_dropdown_spec.js b/spec/frontend/vue_shared/components/clone_dropdown_spec.js
index d9829874b93..5b8576ad761 100644
--- a/spec/frontend/vue_shared/components/clone_dropdown_spec.js
+++ b/spec/frontend/vue_shared/components/clone_dropdown_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import { GlFormInputGroup, GlNewDropdownHeader } from '@gitlab/ui';
+import { GlFormInputGroup, GlDropdownSectionHeader } from '@gitlab/ui';
import CloneDropdown from '~/vue_shared/components/clone_dropdown.vue';
describe('Clone Dropdown Button', () => {
@@ -40,7 +40,7 @@ describe('Clone Dropdown Button', () => {
createComponent();
const group = wrapper.findAll(GlFormInputGroup).at(index);
expect(group.props('value')).toBe(value);
- expect(group.contains(GlFormInputGroup)).toBe(true);
+ expect(group.find(GlFormInputGroup).exists()).toBe(true);
});
it.each`
@@ -51,7 +51,7 @@ describe('Clone Dropdown Button', () => {
createComponent({ [name]: value });
expect(wrapper.find(GlFormInputGroup).props('value')).toBe(value);
- expect(wrapper.findAll(GlNewDropdownHeader).length).toBe(1);
+ expect(wrapper.findAll(GlDropdownSectionHeader).length).toBe(1);
});
});
@@ -63,12 +63,12 @@ describe('Clone Dropdown Button', () => {
`('allows null values for the props', ({ name, value }) => {
createComponent({ ...defaultPropsData, [name]: value });
- expect(wrapper.findAll(GlNewDropdownHeader).length).toBe(1);
+ expect(wrapper.findAll(GlDropdownSectionHeader).length).toBe(1);
});
it('correctly calculates httpLabel for HTTPS protocol', () => {
createComponent({ httpLink: httpsLink });
- expect(wrapper.find(GlNewDropdownHeader).text()).toContain('HTTPS');
+ expect(wrapper.find(GlDropdownSectionHeader).text()).toContain('HTTPS');
});
});
});
diff --git a/spec/frontend/vue_shared/components/commit_spec.js b/spec/frontend/vue_shared/components/commit_spec.js
index 3510c9b699d..9b5c0941a0d 100644
--- a/spec/frontend/vue_shared/components/commit_spec.js
+++ b/spec/frontend/vue_shared/components/commit_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
+import { GlIcon } from '@gitlab/ui';
import CommitComponent from '~/vue_shared/components/commit.vue';
-import Icon from '~/vue_shared/components/icon.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
describe('Commit component', () => {
@@ -8,7 +8,7 @@ describe('Commit component', () => {
let wrapper;
const findIcon = name => {
- const icons = wrapper.findAll(Icon).filter(c => c.attributes('name') === name);
+ const icons = wrapper.findAll(GlIcon).filter(c => c.attributes('name') === name);
return icons.length ? icons.at(0) : icons;
};
@@ -46,7 +46,7 @@ describe('Commit component', () => {
expect(
wrapper
.find('.icon-container')
- .find(Icon)
+ .find(GlIcon)
.exists(),
).toBe(true);
});
diff --git a/spec/frontend/vue_shared/components/confirm_modal_spec.js b/spec/frontend/vue_shared/components/confirm_modal_spec.js
index 7bccd6f1a64..5d92af64de0 100644
--- a/spec/frontend/vue_shared/components/confirm_modal_spec.js
+++ b/spec/frontend/vue_shared/components/confirm_modal_spec.js
@@ -1,5 +1,4 @@
import { shallowMount } from '@vue/test-utils';
-import { GlModal } from '@gitlab/ui';
import { TEST_HOST } from 'helpers/test_constants';
import ConfirmModal from '~/vue_shared/components/confirm_modal.vue';
@@ -21,9 +20,14 @@ describe('vue_shared/components/confirm_modal', () => {
selector: '.test-button',
};
- const actionSpies = {
- openModal: jest.fn(),
- closeModal: jest.fn(),
+ const popupMethods = {
+ hide: jest.fn(),
+ show: jest.fn(),
+ };
+
+ const GlModalStub = {
+ template: '<div><slot></slot></div>',
+ methods: popupMethods,
};
let wrapper;
@@ -34,8 +38,8 @@ describe('vue_shared/components/confirm_modal', () => {
...defaultProps,
...props,
},
- methods: {
- ...actionSpies,
+ stubs: {
+ GlModal: GlModalStub,
},
});
};
@@ -44,7 +48,7 @@ describe('vue_shared/components/confirm_modal', () => {
wrapper.destroy();
});
- const findModal = () => wrapper.find(GlModal);
+ const findModal = () => wrapper.find(GlModalStub);
const findForm = () => wrapper.find('form');
const findFormData = () =>
findForm()
@@ -103,7 +107,7 @@ describe('vue_shared/components/confirm_modal', () => {
});
it('does not close modal', () => {
- expect(actionSpies.closeModal).not.toHaveBeenCalled();
+ expect(popupMethods.hide).not.toHaveBeenCalled();
});
describe('when modal closed', () => {
@@ -112,7 +116,7 @@ describe('vue_shared/components/confirm_modal', () => {
});
it('closes modal', () => {
- expect(actionSpies.closeModal).toHaveBeenCalled();
+ expect(popupMethods.hide).toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js
index 223e22d650b..afd1f1a3123 100644
--- a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js
+++ b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js
@@ -234,7 +234,8 @@ describe('DateTimePicker', () => {
});
it('unchecks quick range when text is input is clicked', () => {
- const findActiveItems = () => findQuickRangeItems().filter(w => w.is('.active'));
+ const findActiveItems = () =>
+ findQuickRangeItems().filter(w => w.classes().includes('active'));
expect(findActiveItems().length).toBe(1);
@@ -332,13 +333,13 @@ describe('DateTimePicker', () => {
expect(items.length).toBe(Object.keys(otherTimeRanges).length);
expect(items.at(0).text()).toBe('1 minute');
- expect(items.at(0).is('.active')).toBe(false);
+ expect(items.at(0).classes()).not.toContain('active');
expect(items.at(1).text()).toBe('2 minutes');
- expect(items.at(1).is('.active')).toBe(true);
+ expect(items.at(1).classes()).toContain('active');
expect(items.at(2).text()).toBe('5 minutes');
- expect(items.at(2).is('.active')).toBe(false);
+ expect(items.at(2).classes()).not.toContain('active');
});
});
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 e0e982f4e11..e91e6577aaf 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
@@ -14,19 +14,13 @@ import {
const localVue = createLocalVue();
localVue.use(Vuex);
-function createRenamedComponent({
- props = {},
- methods = {},
- store = new Vuex.Store({}),
- deep = false,
-}) {
+function createRenamedComponent({ props = {}, store = new Vuex.Store({}), deep = false }) {
const mnt = deep ? mount : shallowMount;
return mnt(Renamed, {
propsData: { ...props },
localVue,
store,
- methods,
});
}
@@ -258,25 +252,17 @@ describe('Renamed Diff Viewer', () => {
'includes a link to the full file for alternate viewer type "$altType"',
({ altType, linkText }) => {
const file = { ...diffFile };
- const clickMock = jest.fn().mockImplementation(() => {});
file.alternate_viewer.name = altType;
wrapper = createRenamedComponent({
deep: true,
props: { diffFile: file },
- methods: {
- clickLink: clickMock,
- },
});
const link = wrapper.find('a');
expect(link.text()).toEqual(linkText);
expect(link.attributes('href')).toEqual(DIFF_FILE_VIEW_PATH);
-
- link.vm.$emit('click');
-
- expect(clickMock).toHaveBeenCalled();
},
);
});
diff --git a/spec/frontend/vue_shared/components/dropdown/dropdown_search_input_spec.js b/spec/frontend/vue_shared/components/dropdown/dropdown_search_input_spec.js
index ffdeb25439c..efa30bf6605 100644
--- a/spec/frontend/vue_shared/components/dropdown/dropdown_search_input_spec.js
+++ b/spec/frontend/vue_shared/components/dropdown/dropdown_search_input_spec.js
@@ -32,12 +32,6 @@ describe('DropdownSearchInputComponent', () => {
expect(wrapper.find('.fa-search.dropdown-input-search').exists()).toBe(true);
});
- it('renders clear search icon element', () => {
- expect(wrapper.find('.fa-times.dropdown-input-clear.js-dropdown-input-clear').exists()).toBe(
- true,
- );
- });
-
it('displays custom placeholder text', () => {
expect(findInputEl().attributes('placeholder')).toBe(defaultProps.placeholderText);
});
diff --git a/spec/frontend/vue_shared/components/file_finder/index_spec.js b/spec/frontend/vue_shared/components/file_finder/index_spec.js
index f9e56774526..40026021777 100644
--- a/spec/frontend/vue_shared/components/file_finder/index_spec.js
+++ b/spec/frontend/vue_shared/components/file_finder/index_spec.js
@@ -84,7 +84,7 @@ describe('File finder item spec', () => {
waitForPromises()
.then(() => {
- vm.$el.querySelector('.dropdown-input-clear').click();
+ vm.clearSearchInput();
})
.then(waitForPromises)
.then(() => {
@@ -94,13 +94,13 @@ describe('File finder item spec', () => {
.catch(done.fail);
});
- it('clear button focues search input', done => {
+ it('clear button focuses search input', done => {
jest.spyOn(vm.$refs.searchInput, 'focus').mockImplementation(() => {});
vm.searchText = 'index';
waitForPromises()
.then(() => {
- vm.$el.querySelector('.dropdown-input-clear').click();
+ vm.clearSearchInput();
})
.then(waitForPromises)
.then(() => {
@@ -319,8 +319,8 @@ describe('File finder item spec', () => {
.catch(done.fail);
});
- it('calls toggle on `command+p` key press', done => {
- Mousetrap.trigger('command+p');
+ it('calls toggle on `mod+p` key press', done => {
+ Mousetrap.trigger('mod+p');
vm.$nextTick()
.then(() => {
@@ -330,39 +330,28 @@ describe('File finder item spec', () => {
.catch(done.fail);
});
- it('calls toggle on `ctrl+p` key press', done => {
- Mousetrap.trigger('ctrl+p');
-
- vm.$nextTick()
- .then(() => {
- expect(vm.toggle).toHaveBeenCalled();
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('always allows `command+p` to trigger toggle', () => {
+ it('always allows `mod+p` to trigger toggle', () => {
expect(
- vm.mousetrapStopCallback(null, vm.$el.querySelector('.dropdown-input-field'), 'command+p'),
- ).toBe(false);
- });
-
- it('always allows `ctrl+p` to trigger toggle', () => {
- expect(
- vm.mousetrapStopCallback(null, vm.$el.querySelector('.dropdown-input-field'), 'ctrl+p'),
+ Mousetrap.prototype.stopCallback(
+ null,
+ vm.$el.querySelector('.dropdown-input-field'),
+ 'mod+p',
+ ),
).toBe(false);
});
it('onlys handles `t` when focused in input-field', () => {
expect(
- vm.mousetrapStopCallback(null, vm.$el.querySelector('.dropdown-input-field'), 't'),
+ Mousetrap.prototype.stopCallback(null, vm.$el.querySelector('.dropdown-input-field'), 't'),
).toBe(true);
});
it('stops callback in monaco editor', () => {
setFixtures('<div class="inputarea"></div>');
- expect(vm.mousetrapStopCallback(null, document.querySelector('.inputarea'), 't')).toBe(true);
+ expect(
+ Mousetrap.prototype.stopCallback(null, document.querySelector('.inputarea'), 't'),
+ ).toBe(true);
});
});
});
diff --git a/spec/frontend/vue_shared/components/file_row_spec.js b/spec/frontend/vue_shared/components/file_row_spec.js
index 1acd2e05464..d28c35d26bf 100644
--- a/spec/frontend/vue_shared/components/file_row_spec.js
+++ b/spec/frontend/vue_shared/components/file_row_spec.js
@@ -118,7 +118,7 @@ describe('File row component', () => {
level: 0,
});
- expect(wrapper.contains(FileHeader)).toBe(true);
+ expect(wrapper.find(FileHeader).exists()).toBe(true);
});
it('matches the current route against encoded file URL', () => {
@@ -139,4 +139,16 @@ describe('File row component', () => {
expect(wrapper.vm.hasUrlAtCurrentRoute()).toBe(true);
});
+
+ it('render with the correct file classes prop', () => {
+ createComponent({
+ file: {
+ ...file(),
+ },
+ level: 0,
+ fileClasses: 'font-weight-bold',
+ });
+
+ expect(wrapper.find('.file-row-name').classes()).toContain('font-weight-bold');
+ });
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
index 73dbecadd89..c79880d4766 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
@@ -1,19 +1,28 @@
import { shallowMount, mount } from '@vue/test-utils';
-import {
- GlFilteredSearch,
- GlButtonGroup,
- GlButton,
- GlNewDropdown as GlDropdown,
- GlNewDropdownItem as GlDropdownItem,
-} from '@gitlab/ui';
+import { GlFilteredSearch, GlButtonGroup, GlButton, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import FilteredSearchBarRoot from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
+import { uniqueTokens } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
import { SortDirection } from '~/vue_shared/components/filtered_search_bar/constants';
import RecentSearchesStore from '~/filtered_search/stores/recent_searches_store';
import RecentSearchesService from '~/filtered_search/services/recent_searches_service';
-import { mockAvailableTokens, mockSortOptions, mockHistoryItems } from './mock_data';
+import {
+ mockAvailableTokens,
+ mockSortOptions,
+ mockHistoryItems,
+ tokenValueAuthor,
+ tokenValueLabel,
+ tokenValueMilestone,
+} from './mock_data';
+
+jest.mock('~/vue_shared/components/filtered_search_bar/filtered_search_utils', () => ({
+ uniqueTokens: jest.fn().mockImplementation(tokens => tokens),
+ stripQuotes: jest.requireActual(
+ '~/vue_shared/components/filtered_search_bar/filtered_search_utils',
+ ).stripQuotes,
+}));
const createComponent = ({
shallow = true,
@@ -52,10 +61,10 @@ describe('FilteredSearchBarRoot', () => {
expect(wrapper.vm.filterValue).toEqual([]);
expect(wrapper.vm.selectedSortOption).toBe(mockSortOptions[0].sortDirection.descending);
expect(wrapper.vm.selectedSortDirection).toBe(SortDirection.descending);
- expect(wrapper.contains(GlButtonGroup)).toBe(true);
- expect(wrapper.contains(GlButton)).toBe(true);
- expect(wrapper.contains(GlDropdown)).toBe(true);
- expect(wrapper.contains(GlDropdownItem)).toBe(true);
+ expect(wrapper.find(GlButtonGroup).exists()).toBe(true);
+ expect(wrapper.find(GlButton).exists()).toBe(true);
+ expect(wrapper.find(GlDropdown).exists()).toBe(true);
+ expect(wrapper.find(GlDropdownItem).exists()).toBe(true);
});
it('does not initialize `selectedSortOption` and `selectedSortDirection` when `sortOptions` is not applied and hides the sort dropdown', () => {
@@ -63,23 +72,31 @@ describe('FilteredSearchBarRoot', () => {
expect(wrapperNoSort.vm.filterValue).toEqual([]);
expect(wrapperNoSort.vm.selectedSortOption).toBe(undefined);
- expect(wrapperNoSort.contains(GlButtonGroup)).toBe(false);
- expect(wrapperNoSort.contains(GlButton)).toBe(false);
- expect(wrapperNoSort.contains(GlDropdown)).toBe(false);
- expect(wrapperNoSort.contains(GlDropdownItem)).toBe(false);
+ expect(wrapperNoSort.find(GlButtonGroup).exists()).toBe(false);
+ expect(wrapperNoSort.find(GlButton).exists()).toBe(false);
+ expect(wrapperNoSort.find(GlDropdown).exists()).toBe(false);
+ expect(wrapperNoSort.find(GlDropdownItem).exists()).toBe(false);
});
});
describe('computed', () => {
describe('tokenSymbols', () => {
it('returns a map containing type and symbols from `tokens` prop', () => {
- expect(wrapper.vm.tokenSymbols).toEqual({ author_username: '@', label_name: '~' });
+ expect(wrapper.vm.tokenSymbols).toEqual({
+ author_username: '@',
+ label_name: '~',
+ milestone_title: '%',
+ });
});
});
describe('tokenTitles', () => {
it('returns a map containing type and title from `tokens` prop', () => {
- expect(wrapper.vm.tokenTitles).toEqual({ author_username: 'Author', label_name: 'Label' });
+ expect(wrapper.vm.tokenTitles).toEqual({
+ author_username: 'Author',
+ label_name: 'Label',
+ milestone_title: 'Milestone',
+ });
});
});
@@ -131,6 +148,20 @@ describe('FilteredSearchBarRoot', () => {
expect(wrapper.vm.filteredRecentSearches[0]).toEqual({ foo: 'bar' });
});
+ it('returns array of recent searches sanitizing any duplicate token values', async () => {
+ wrapper.setData({
+ recentSearches: [
+ [tokenValueAuthor, tokenValueLabel, tokenValueMilestone, tokenValueLabel],
+ [tokenValueAuthor, tokenValueMilestone],
+ ],
+ });
+
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.vm.filteredRecentSearches).toHaveLength(2);
+ expect(uniqueTokens).toHaveBeenCalled();
+ });
+
it('returns undefined when recentSearchesStorageKey prop is not set on component', async () => {
wrapper.setProps({
recentSearchesStorageKey: '',
@@ -182,40 +213,12 @@ describe('FilteredSearchBarRoot', () => {
});
describe('removeQuotesEnclosure', () => {
- const mockFilters = [
- {
- type: 'author_username',
- value: {
- data: 'root',
- operator: '=',
- },
- },
- {
- type: 'label_name',
- value: {
- data: '"Documentation Update"',
- operator: '=',
- },
- },
- 'foo',
- ];
+ const mockFilters = [tokenValueAuthor, tokenValueLabel, 'foo'];
it('returns filter array with unescaped strings for values which have spaces', () => {
expect(wrapper.vm.removeQuotesEnclosure(mockFilters)).toEqual([
- {
- type: 'author_username',
- value: {
- data: 'root',
- operator: '=',
- },
- },
- {
- type: 'label_name',
- value: {
- data: 'Documentation Update',
- operator: '=',
- },
- },
+ tokenValueAuthor,
+ tokenValueLabel,
'foo',
]);
});
@@ -277,21 +280,26 @@ describe('FilteredSearchBarRoot', () => {
});
describe('handleFilterSubmit', () => {
- const mockFilters = [
- {
- type: 'author_username',
- value: {
- data: 'root',
- operator: '=',
- },
- },
- 'foo',
- ];
+ const mockFilters = [tokenValueAuthor, 'foo'];
+
+ beforeEach(async () => {
+ wrapper.setData({
+ filterValue: mockFilters,
+ });
+
+ await wrapper.vm.$nextTick();
+ });
+
+ it('calls `uniqueTokens` on `filterValue` prop to remove duplicates', () => {
+ wrapper.vm.handleFilterSubmit();
+
+ expect(uniqueTokens).toHaveBeenCalledWith(wrapper.vm.filterValue);
+ });
it('calls `recentSearchesStore.addRecentSearch` with serialized value of provided `filters` param', () => {
jest.spyOn(wrapper.vm.recentSearchesStore, 'addRecentSearch');
- wrapper.vm.handleFilterSubmit(mockFilters);
+ wrapper.vm.handleFilterSubmit();
return wrapper.vm.recentSearchesPromise.then(() => {
expect(wrapper.vm.recentSearchesStore.addRecentSearch).toHaveBeenCalledWith(mockFilters);
@@ -301,7 +309,7 @@ describe('FilteredSearchBarRoot', () => {
it('calls `recentSearchesService.save` with array of searches', () => {
jest.spyOn(wrapper.vm.recentSearchesService, 'save');
- wrapper.vm.handleFilterSubmit(mockFilters);
+ wrapper.vm.handleFilterSubmit();
return wrapper.vm.recentSearchesPromise.then(() => {
expect(wrapper.vm.recentSearchesService.save).toHaveBeenCalledWith([mockFilters]);
@@ -311,7 +319,7 @@ describe('FilteredSearchBarRoot', () => {
it('sets `recentSearches` data prop with array of searches', () => {
jest.spyOn(wrapper.vm.recentSearchesService, 'save');
- wrapper.vm.handleFilterSubmit(mockFilters);
+ wrapper.vm.handleFilterSubmit();
return wrapper.vm.recentSearchesPromise.then(() => {
expect(wrapper.vm.recentSearches).toEqual([mockFilters]);
@@ -329,7 +337,7 @@ describe('FilteredSearchBarRoot', () => {
it('emits component event `onFilter` with provided filters param', () => {
jest.spyOn(wrapper.vm, 'removeQuotesEnclosure');
- wrapper.vm.handleFilterSubmit(mockFilters);
+ wrapper.vm.handleFilterSubmit();
expect(wrapper.emitted('onFilter')[0]).toEqual([mockFilters]);
expect(wrapper.vm.removeQuotesEnclosure).toHaveBeenCalledWith(mockFilters);
@@ -366,7 +374,9 @@ describe('FilteredSearchBarRoot', () => {
'.gl-search-box-by-click-menu .gl-search-box-by-click-history-item',
);
- expect(searchHistoryItemsEl.at(0).text()).toBe('Author := @tobyLabel := ~Bug"duo"');
+ expect(searchHistoryItemsEl.at(0).text()).toBe(
+ 'Author := @rootLabel := ~bugMilestone := %v1.0"duo"',
+ );
wrapperFullMount.destroy();
});
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 a857f84adf1..4869e75a2f3 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
@@ -1,4 +1,18 @@
-import * as filteredSearchUtils from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
+import {
+ stripQuotes,
+ uniqueTokens,
+ prepareTokens,
+ processFilters,
+ filterToQueryObject,
+ urlQueryToFilter,
+} from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
+
+import {
+ tokenValueAuthor,
+ tokenValueLabel,
+ tokenValueMilestone,
+ tokenValuePlain,
+} from './mock_data';
describe('Filtered Search Utils', () => {
describe('stripQuotes', () => {
@@ -9,11 +23,196 @@ describe('Filtered Search Utils', () => {
${'FooBar'} | ${'FooBar'}
${"Foo'Bar"} | ${"Foo'Bar"}
${'Foo"Bar'} | ${'Foo"Bar'}
+ ${'Foo Bar'} | ${'Foo Bar'}
`(
'returns string $outputValue when called with string $inputValue',
({ inputValue, outputValue }) => {
- expect(filteredSearchUtils.stripQuotes(inputValue)).toBe(outputValue);
+ expect(stripQuotes(inputValue)).toBe(outputValue);
},
);
});
+
+ describe('uniqueTokens', () => {
+ it('returns tokens array with duplicates removed', () => {
+ expect(
+ uniqueTokens([
+ tokenValueAuthor,
+ tokenValueLabel,
+ tokenValueMilestone,
+ tokenValueLabel,
+ tokenValuePlain,
+ ]),
+ ).toHaveLength(4); // Removes 2nd instance of tokenValueLabel
+ });
+
+ it('returns tokens array as it is if it does not have duplicates', () => {
+ expect(
+ uniqueTokens([tokenValueAuthor, tokenValueLabel, tokenValueMilestone, tokenValuePlain]),
+ ).toHaveLength(4);
+ });
+ });
+});
+
+describe('prepareTokens', () => {
+ describe('with empty data', () => {
+ it('returns an empty array', () => {
+ expect(prepareTokens()).toEqual([]);
+ expect(prepareTokens({})).toEqual([]);
+ expect(prepareTokens({ milestone: null, author: null, assignees: [], labels: [] })).toEqual(
+ [],
+ );
+ });
+ });
+
+ it.each([
+ [
+ 'milestone',
+ { value: 'v1.0', operator: '=' },
+ [{ type: 'milestone', value: { data: 'v1.0', operator: '=' } }],
+ ],
+ [
+ 'author',
+ { value: 'mr.popo', operator: '!=' },
+ [{ type: 'author', value: { data: 'mr.popo', operator: '!=' } }],
+ ],
+ [
+ 'labels',
+ [{ value: 'z-fighters', operator: '=' }],
+ [{ type: 'labels', value: { data: 'z-fighters', operator: '=' } }],
+ ],
+ [
+ 'assignees',
+ [{ value: 'krillin', operator: '=' }, { value: 'piccolo', operator: '!=' }],
+ [
+ { type: 'assignees', value: { data: 'krillin', operator: '=' } },
+ { type: 'assignees', value: { data: 'piccolo', operator: '!=' } },
+ ],
+ ],
+ [
+ 'foo',
+ [{ value: 'bar', operator: '!=' }, { value: 'baz', operator: '!=' }],
+ [
+ { type: 'foo', value: { data: 'bar', operator: '!=' } },
+ { type: 'foo', value: { data: 'baz', operator: '!=' } },
+ ],
+ ],
+ ])('gathers %s=%j into result=%j', (token, value, result) => {
+ const res = prepareTokens({ [token]: value });
+ expect(res).toEqual(result);
+ });
+});
+
+describe('processFilters', () => {
+ it('processes multiple filter values', () => {
+ const result = processFilters([
+ { type: 'foo', value: { data: 'foo', operator: '=' } },
+ { type: 'bar', value: { data: 'bar1', operator: '=' } },
+ { type: 'bar', value: { data: 'bar2', operator: '!=' } },
+ ]);
+
+ expect(result).toStrictEqual({
+ foo: [{ value: 'foo', operator: '=' }],
+ bar: [{ value: 'bar1', operator: '=' }, { value: 'bar2', operator: '!=' }],
+ });
+ });
+
+ it('does not remove wrapping double quotes from the data', () => {
+ const result = processFilters([
+ { type: 'foo', value: { data: '"value with spaces"', operator: '=' } },
+ ]);
+
+ expect(result).toStrictEqual({
+ foo: [{ value: '"value with spaces"', operator: '=' }],
+ });
+ });
+});
+
+describe('filterToQueryObject', () => {
+ describe('with empty data', () => {
+ it('returns an empty object', () => {
+ expect(filterToQueryObject()).toEqual({});
+ expect(filterToQueryObject({})).toEqual({});
+ expect(filterToQueryObject({ author_username: null, label_name: [] })).toEqual({
+ author_username: null,
+ label_name: null,
+ 'not[author_username]': null,
+ 'not[label_name]': null,
+ });
+ });
+ });
+
+ it.each([
+ [
+ 'author_username',
+ { value: 'v1.0', operator: '=' },
+ { author_username: 'v1.0', 'not[author_username]': null },
+ ],
+ [
+ 'author_username',
+ { value: 'v1.0', operator: '!=' },
+ { author_username: null, 'not[author_username]': 'v1.0' },
+ ],
+ [
+ 'label_name',
+ [{ value: 'z-fighters', operator: '=' }],
+ { label_name: ['z-fighters'], 'not[label_name]': null },
+ ],
+ [
+ 'label_name',
+ [{ value: 'z-fighters', operator: '!=' }],
+ { label_name: null, 'not[label_name]': ['z-fighters'] },
+ ],
+ [
+ 'foo',
+ [{ value: 'bar', operator: '=' }, { value: 'baz', operator: '=' }],
+ { foo: ['bar', 'baz'], 'not[foo]': null },
+ ],
+ [
+ 'foo',
+ [{ value: 'bar', operator: '!=' }, { value: 'baz', operator: '!=' }],
+ { foo: null, 'not[foo]': ['bar', 'baz'] },
+ ],
+ [
+ 'foo',
+ [{ value: 'bar', operator: '!=' }, { value: 'baz', operator: '=' }],
+ { foo: ['baz'], 'not[foo]': ['bar'] },
+ ],
+ ])('gathers filter values %s=%j into query object=%j', (token, value, result) => {
+ const res = filterToQueryObject({ [token]: value });
+ expect(res).toEqual(result);
+ });
+});
+
+describe('urlQueryToFilter', () => {
+ describe('with empty data', () => {
+ it('returns an empty object', () => {
+ expect(urlQueryToFilter()).toEqual({});
+ expect(urlQueryToFilter('')).toEqual({});
+ expect(urlQueryToFilter('author_username=&milestone_title=&')).toEqual({});
+ });
+ });
+
+ it.each([
+ ['author_username=v1.0', { author_username: { value: 'v1.0', operator: '=' } }],
+ ['not[author_username]=v1.0', { author_username: { value: 'v1.0', operator: '!=' } }],
+ ['foo=bar&foo=baz', { foo: { value: 'baz', operator: '=' } }],
+ ['foo=bar&foo[]=baz', { foo: [{ value: 'baz', operator: '=' }] }],
+ ['not[foo]=bar&foo=baz', { foo: { value: 'baz', operator: '=' } }],
+ [
+ 'foo[]=bar&foo[]=baz&not[foo]=',
+ { foo: [{ value: 'bar', operator: '=' }, { value: 'baz', operator: '=' }] },
+ ],
+ [
+ 'foo[]=&not[foo][]=bar&not[foo][]=baz',
+ { foo: [{ value: 'bar', operator: '!=' }, { value: 'baz', operator: '!=' }] },
+ ],
+ [
+ 'foo[]=baz&not[foo][]=bar',
+ { foo: [{ value: 'baz', operator: '=' }, { value: 'bar', operator: '!=' }] },
+ ],
+ ['not[foo][]=bar', { foo: [{ value: 'bar', operator: '!=' }] }],
+ ])('gathers filter values %s into query object=%j', (query, result) => {
+ const res = urlQueryToFilter(query);
+ expect(res).toEqual(result);
+ });
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js b/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js
index dcccb1f49b6..e0a3208cac9 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js
@@ -1,6 +1,7 @@
import { mockLabels } from 'jest/vue_shared/components/sidebar/labels_select_vue/mock_data';
import Api from '~/api';
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
+import BranchToken from '~/vue_shared/components/filtered_search_bar/tokens/branch_token.vue';
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
@@ -33,6 +34,8 @@ export const mockAuthor3 = {
export const mockAuthors = [mockAuthor1, mockAuthor2, mockAuthor3];
+export const mockBranches = [{ name: 'Master' }, { name: 'v1.x' }, { name: 'my-Branch' }];
+
export const mockRegularMilestone = {
id: 1,
name: '4.0',
@@ -55,6 +58,16 @@ export const mockMilestones = [
mockEscapedMilestone,
];
+export const mockBranchToken = {
+ type: 'source_branch',
+ icon: 'branch',
+ title: 'Source Branch',
+ unique: true,
+ token: BranchToken,
+ operators: [{ value: '=', description: 'is', default: 'true' }],
+ fetchBranches: Api.branches.bind(Api),
+};
+
export const mockAuthorToken = {
type: 'author_username',
icon: 'user',
@@ -89,36 +102,40 @@ export const mockMilestoneToken = {
fetchMilestones: () => Promise.resolve({ data: mockMilestones }),
};
-export const mockAvailableTokens = [mockAuthorToken, mockLabelToken];
+export const mockAvailableTokens = [mockAuthorToken, mockLabelToken, mockMilestoneToken];
+
+export const tokenValueAuthor = {
+ type: 'author_username',
+ value: {
+ data: 'root',
+ operator: '=',
+ },
+};
+
+export const tokenValueLabel = {
+ type: 'label_name',
+ value: {
+ operator: '=',
+ data: 'bug',
+ },
+};
+
+export const tokenValueMilestone = {
+ type: 'milestone_title',
+ value: {
+ operator: '=',
+ data: 'v1.0',
+ },
+};
+
+export const tokenValuePlain = {
+ type: 'filtered-search-term',
+ value: { data: 'foo' },
+};
export const mockHistoryItems = [
- [
- {
- type: 'author_username',
- value: {
- data: 'toby',
- operator: '=',
- },
- },
- {
- type: 'label_name',
- value: {
- data: 'Bug',
- operator: '=',
- },
- },
- 'duo',
- ],
- [
- {
- type: 'author_username',
- value: {
- data: 'root',
- operator: '=',
- },
- },
- 'si',
- ],
+ [tokenValueAuthor, tokenValueLabel, tokenValueMilestone, 'duo'],
+ [tokenValueAuthor, 'si'],
];
export const mockSortOptions = [
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js
index 160febf9d06..72840ce381f 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js
@@ -1,18 +1,42 @@
import { mount } from '@vue/test-utils';
-import { GlFilteredSearchToken, GlFilteredSearchTokenSegment } from '@gitlab/ui';
+import {
+ GlFilteredSearchToken,
+ GlFilteredSearchTokenSegment,
+ GlFilteredSearchSuggestion,
+ GlDropdownDivider,
+} from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as createFlash } from '~/flash';
+import {
+ DEFAULT_LABEL_NONE,
+ DEFAULT_LABEL_ANY,
+} from '~/vue_shared/components/filtered_search_bar/constants';
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
import { mockAuthorToken, mockAuthors } from '../mock_data';
jest.mock('~/flash');
-
-const createComponent = ({ config = mockAuthorToken, value = { data: '' }, active = false } = {}) =>
- mount(AuthorToken, {
+const defaultStubs = {
+ Portal: true,
+ GlFilteredSearchSuggestionList: {
+ template: '<div></div>',
+ methods: {
+ getValue: () => '=',
+ },
+ },
+};
+
+function createComponent(options = {}) {
+ const {
+ config = mockAuthorToken,
+ value = { data: '' },
+ active = false,
+ stubs = defaultStubs,
+ } = options;
+ return mount(AuthorToken, {
propsData: {
config,
value,
@@ -22,18 +46,9 @@ const createComponent = ({ config = mockAuthorToken, value = { data: '' }, activ
portalName: 'fake target',
alignSuggestions: function fakeAlignSuggestions() {},
},
- stubs: {
- Portal: {
- template: '<div><slot></slot></div>',
- },
- GlFilteredSearchSuggestionList: {
- template: '<div></div>',
- methods: {
- getValue: () => '=',
- },
- },
- },
+ stubs,
});
+}
describe('AuthorToken', () => {
let mock;
@@ -141,5 +156,57 @@ describe('AuthorToken', () => {
expect(tokenSegments.at(2).text()).toBe(mockAuthors[0].name); // "Administrator"
});
});
+
+ it('renders provided defaultAuthors as suggestions', async () => {
+ const defaultAuthors = [DEFAULT_LABEL_NONE, DEFAULT_LABEL_ANY];
+ wrapper = createComponent({
+ active: true,
+ config: { ...mockAuthorToken, defaultAuthors },
+ stubs: { Portal: true },
+ });
+ const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+ const suggestionsSegment = tokenSegments.at(2);
+ suggestionsSegment.vm.$emit('activate');
+ await wrapper.vm.$nextTick();
+
+ const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
+
+ expect(suggestions).toHaveLength(defaultAuthors.length);
+ defaultAuthors.forEach((label, index) => {
+ expect(suggestions.at(index).text()).toBe(label.text);
+ });
+ });
+
+ it('does not render divider when no defaultAuthors', async () => {
+ wrapper = createComponent({
+ active: true,
+ config: { ...mockAuthorToken, defaultAuthors: [] },
+ stubs: { Portal: true },
+ });
+ const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+ const suggestionsSegment = tokenSegments.at(2);
+ suggestionsSegment.vm.$emit('activate');
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.contains(GlFilteredSearchSuggestion)).toBe(false);
+ expect(wrapper.contains(GlDropdownDivider)).toBe(false);
+ });
+
+ it('renders `DEFAULT_LABEL_ANY` as default suggestions', async () => {
+ wrapper = createComponent({
+ active: true,
+ config: { ...mockAuthorToken },
+ stubs: { Portal: true },
+ });
+ const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+ const suggestionsSegment = tokenSegments.at(2);
+ suggestionsSegment.vm.$emit('activate');
+ await wrapper.vm.$nextTick();
+
+ const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
+
+ expect(suggestions).toHaveLength(1);
+ expect(suggestions.at(0).text()).toBe(DEFAULT_LABEL_ANY.text);
+ });
});
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js
new file mode 100644
index 00000000000..12b7fd58670
--- /dev/null
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js
@@ -0,0 +1,207 @@
+import { mount } from '@vue/test-utils';
+import {
+ GlFilteredSearchToken,
+ GlFilteredSearchSuggestion,
+ GlFilteredSearchTokenSegment,
+ GlDropdownDivider,
+} from '@gitlab/ui';
+import MockAdapter from 'axios-mock-adapter';
+import waitForPromises from 'helpers/wait_for_promises';
+
+import axios from '~/lib/utils/axios_utils';
+import createFlash from '~/flash';
+import {
+ DEFAULT_LABEL_NONE,
+ DEFAULT_LABEL_ANY,
+} from '~/vue_shared/components/filtered_search_bar/constants';
+import BranchToken from '~/vue_shared/components/filtered_search_bar/tokens/branch_token.vue';
+
+import { mockBranches, mockBranchToken } from '../mock_data';
+
+jest.mock('~/flash');
+const defaultStubs = {
+ Portal: true,
+ GlFilteredSearchSuggestionList: {
+ template: '<div></div>',
+ methods: {
+ getValue: () => '=',
+ },
+ },
+};
+
+function createComponent(options = {}) {
+ const {
+ config = mockBranchToken,
+ value = { data: '' },
+ active = false,
+ stubs = defaultStubs,
+ } = options;
+ return mount(BranchToken, {
+ propsData: {
+ config,
+ value,
+ active,
+ },
+ provide: {
+ portalName: 'fake target',
+ alignSuggestions: function fakeAlignSuggestions() {},
+ },
+ stubs,
+ });
+}
+
+describe('BranchToken', () => {
+ let mock;
+ let wrapper;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ wrapper.destroy();
+ });
+
+ describe('computed', () => {
+ beforeEach(async () => {
+ wrapper = createComponent({ value: { data: mockBranches[0].name } });
+
+ wrapper.setData({
+ branches: mockBranches,
+ });
+
+ await wrapper.vm.$nextTick();
+ });
+
+ describe('currentValue', () => {
+ it('returns lowercase string for `value.data`', () => {
+ expect(wrapper.vm.currentValue).toBe('master');
+ });
+ });
+
+ describe('activeBranch', () => {
+ it('returns object for currently present `value.data`', () => {
+ expect(wrapper.vm.activeBranch).toEqual(mockBranches[0]);
+ });
+ });
+ });
+
+ describe('methods', () => {
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ describe('fetchBranchBySearchTerm', () => {
+ it('calls `config.fetchBranches` with provided searchTerm param', () => {
+ jest.spyOn(wrapper.vm.config, 'fetchBranches');
+
+ wrapper.vm.fetchBranchBySearchTerm('foo');
+
+ expect(wrapper.vm.config.fetchBranches).toHaveBeenCalledWith('foo');
+ });
+
+ it('sets response to `branches` when request is succesful', () => {
+ jest.spyOn(wrapper.vm.config, 'fetchBranches').mockResolvedValue({ data: mockBranches });
+
+ wrapper.vm.fetchBranchBySearchTerm('foo');
+
+ return waitForPromises().then(() => {
+ expect(wrapper.vm.branches).toEqual(mockBranches);
+ });
+ });
+
+ it('calls `createFlash` with flash error message when request fails', () => {
+ jest.spyOn(wrapper.vm.config, 'fetchBranches').mockRejectedValue({});
+
+ wrapper.vm.fetchBranchBySearchTerm('foo');
+
+ return waitForPromises().then(() => {
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'There was a problem fetching branches.',
+ });
+ });
+ });
+
+ it('sets `loading` to false when request completes', () => {
+ jest.spyOn(wrapper.vm.config, 'fetchBranches').mockRejectedValue({});
+
+ wrapper.vm.fetchBranchBySearchTerm('foo');
+
+ return waitForPromises().then(() => {
+ expect(wrapper.vm.loading).toBe(false);
+ });
+ });
+ });
+ });
+
+ describe('template', () => {
+ const defaultBranches = [DEFAULT_LABEL_NONE, DEFAULT_LABEL_ANY];
+ async function showSuggestions() {
+ const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+ const suggestionsSegment = tokenSegments.at(2);
+ suggestionsSegment.vm.$emit('activate');
+ await wrapper.vm.$nextTick();
+ }
+
+ beforeEach(async () => {
+ wrapper = createComponent({ value: { data: mockBranches[0].name } });
+
+ wrapper.setData({
+ branches: mockBranches,
+ });
+
+ await wrapper.vm.$nextTick();
+ });
+
+ it('renders gl-filtered-search-token component', () => {
+ expect(wrapper.find(GlFilteredSearchToken).exists()).toBe(true);
+ });
+
+ it('renders token item when value is selected', () => {
+ const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+
+ expect(tokenSegments).toHaveLength(3);
+ expect(tokenSegments.at(2).text()).toBe(mockBranches[0].name);
+ });
+
+ it('renders provided defaultBranches as suggestions', async () => {
+ wrapper = createComponent({
+ active: true,
+ config: { ...mockBranchToken, defaultBranches },
+ stubs: { Portal: true },
+ });
+ await showSuggestions();
+ const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
+
+ expect(suggestions).toHaveLength(defaultBranches.length);
+ defaultBranches.forEach((branch, index) => {
+ expect(suggestions.at(index).text()).toBe(branch.text);
+ });
+ });
+
+ it('does not render divider when no defaultBranches', async () => {
+ wrapper = createComponent({
+ active: true,
+ config: { ...mockBranchToken, defaultBranches: [] },
+ stubs: { Portal: true },
+ });
+ await showSuggestions();
+
+ expect(wrapper.contains(GlFilteredSearchSuggestion)).toBe(false);
+ expect(wrapper.contains(GlDropdownDivider)).toBe(false);
+ });
+
+ it('renders no suggestions as default', async () => {
+ wrapper = createComponent({
+ active: true,
+ config: { ...mockBranchToken },
+ stubs: { Portal: true },
+ });
+ await showSuggestions();
+ const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
+
+ expect(suggestions).toHaveLength(0);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
index 0e60ee99327..3feb05bab35 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
@@ -1,5 +1,10 @@
import { mount } from '@vue/test-utils';
-import { GlFilteredSearchToken, GlFilteredSearchTokenSegment } from '@gitlab/ui';
+import {
+ GlFilteredSearchToken,
+ GlFilteredSearchSuggestion,
+ GlFilteredSearchTokenSegment,
+ GlDropdownDivider,
+} from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import {
@@ -9,14 +14,34 @@ import {
import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as createFlash } from '~/flash';
+import {
+ DEFAULT_LABELS,
+ DEFAULT_LABEL_NONE,
+ DEFAULT_LABEL_ANY,
+} from '~/vue_shared/components/filtered_search_bar/constants';
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
import { mockLabelToken } from '../mock_data';
jest.mock('~/flash');
-
-const createComponent = ({ config = mockLabelToken, value = { data: '' }, active = false } = {}) =>
- mount(LabelToken, {
+const defaultStubs = {
+ Portal: true,
+ GlFilteredSearchSuggestionList: {
+ template: '<div></div>',
+ methods: {
+ getValue: () => '=',
+ },
+ },
+};
+
+function createComponent(options = {}) {
+ const {
+ config = mockLabelToken,
+ value = { data: '' },
+ active = false,
+ stubs = defaultStubs,
+ } = options;
+ return mount(LabelToken, {
propsData: {
config,
value,
@@ -26,18 +51,9 @@ const createComponent = ({ config = mockLabelToken, value = { data: '' }, active
portalName: 'fake target',
alignSuggestions: function fakeAlignSuggestions() {},
},
- stubs: {
- Portal: {
- template: '<div><slot></slot></div>',
- },
- GlFilteredSearchSuggestionList: {
- template: '<div></div>',
- methods: {
- getValue: () => '=',
- },
- },
- },
+ stubs,
});
+}
describe('LabelToken', () => {
let mock;
@@ -45,7 +61,6 @@ describe('LabelToken', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- wrapper = createComponent();
});
afterEach(() => {
@@ -98,6 +113,10 @@ describe('LabelToken', () => {
});
describe('methods', () => {
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
describe('fetchLabelBySearchTerm', () => {
it('calls `config.fetchLabels` with provided searchTerm param', () => {
jest.spyOn(wrapper.vm.config, 'fetchLabels');
@@ -140,6 +159,8 @@ describe('LabelToken', () => {
});
describe('template', () => {
+ const defaultLabels = [DEFAULT_LABEL_NONE, DEFAULT_LABEL_ANY];
+
beforeEach(async () => {
wrapper = createComponent({ value: { data: `"${mockRegularLabel.title}"` } });
@@ -166,5 +187,58 @@ describe('LabelToken', () => {
.attributes('style'),
).toBe('background-color: rgb(186, 218, 85); color: rgb(255, 255, 255);');
});
+
+ it('renders provided defaultLabels as suggestions', async () => {
+ wrapper = createComponent({
+ active: true,
+ config: { ...mockLabelToken, defaultLabels },
+ stubs: { Portal: true },
+ });
+ const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+ const suggestionsSegment = tokenSegments.at(2);
+ suggestionsSegment.vm.$emit('activate');
+ await wrapper.vm.$nextTick();
+
+ const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
+
+ expect(suggestions).toHaveLength(defaultLabels.length);
+ defaultLabels.forEach((label, index) => {
+ expect(suggestions.at(index).text()).toBe(label.text);
+ });
+ });
+
+ it('does not render divider when no defaultLabels', async () => {
+ wrapper = createComponent({
+ active: true,
+ config: { ...mockLabelToken, defaultLabels: [] },
+ stubs: { Portal: true },
+ });
+ const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+ const suggestionsSegment = tokenSegments.at(2);
+ suggestionsSegment.vm.$emit('activate');
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.contains(GlFilteredSearchSuggestion)).toBe(false);
+ expect(wrapper.contains(GlDropdownDivider)).toBe(false);
+ });
+
+ it('renders `DEFAULT_LABELS` as default suggestions', async () => {
+ wrapper = createComponent({
+ active: true,
+ config: { ...mockLabelToken },
+ stubs: { Portal: true },
+ });
+ const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+ const suggestionsSegment = tokenSegments.at(2);
+ suggestionsSegment.vm.$emit('activate');
+ await wrapper.vm.$nextTick();
+
+ const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
+
+ expect(suggestions).toHaveLength(DEFAULT_LABELS.length);
+ DEFAULT_LABELS.forEach((label, index) => {
+ expect(suggestions.at(index).text()).toBe(label.text);
+ });
+ });
});
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js
index de893bf44c8..0ec814e3f15 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js
@@ -1,10 +1,16 @@
import { mount } from '@vue/test-utils';
-import { GlFilteredSearchToken, GlFilteredSearchTokenSegment } from '@gitlab/ui';
+import {
+ GlFilteredSearchToken,
+ GlFilteredSearchSuggestion,
+ GlFilteredSearchTokenSegment,
+ GlDropdownDivider,
+} from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
+import { DEFAULT_MILESTONES } from '~/vue_shared/components/filtered_search_bar/constants';
import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
import {
@@ -16,12 +22,24 @@ import {
jest.mock('~/flash');
-const createComponent = ({
- config = mockMilestoneToken,
- value = { data: '' },
- active = false,
-} = {}) =>
- mount(MilestoneToken, {
+const defaultStubs = {
+ Portal: true,
+ GlFilteredSearchSuggestionList: {
+ template: '<div></div>',
+ methods: {
+ getValue: () => '=',
+ },
+ },
+};
+
+function createComponent(options = {}) {
+ const {
+ config = mockMilestoneToken,
+ value = { data: '' },
+ active = false,
+ stubs = defaultStubs,
+ } = options;
+ return mount(MilestoneToken, {
propsData: {
config,
value,
@@ -31,18 +49,9 @@ const createComponent = ({
portalName: 'fake target',
alignSuggestions: function fakeAlignSuggestions() {},
},
- stubs: {
- Portal: {
- template: '<div><slot></slot></div>',
- },
- GlFilteredSearchSuggestionList: {
- template: '<div></div>',
- methods: {
- getValue: () => '=',
- },
- },
- },
+ stubs,
});
+}
describe('MilestoneToken', () => {
let mock;
@@ -128,6 +137,8 @@ describe('MilestoneToken', () => {
});
describe('template', () => {
+ const defaultMilestones = [{ text: 'foo', value: 'foo' }, { text: 'bar', value: 'baz' }];
+
beforeEach(async () => {
wrapper = createComponent({ value: { data: `"${mockRegularMilestone.title}"` } });
@@ -146,7 +157,60 @@ describe('MilestoneToken', () => {
const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
expect(tokenSegments).toHaveLength(3); // Milestone, =, '%"4.0"'
- expect(tokenSegments.at(2).text()).toBe(`%"${mockRegularMilestone.title}"`); // "4.0 RC1"
+ expect(tokenSegments.at(2).text()).toBe(`%${mockRegularMilestone.title}`); // "4.0 RC1"
+ });
+
+ it('renders provided defaultMilestones as suggestions', async () => {
+ wrapper = createComponent({
+ active: true,
+ config: { ...mockMilestoneToken, defaultMilestones },
+ stubs: { Portal: true },
+ });
+ const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+ const suggestionsSegment = tokenSegments.at(2);
+ suggestionsSegment.vm.$emit('activate');
+ await wrapper.vm.$nextTick();
+
+ const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
+
+ expect(suggestions).toHaveLength(defaultMilestones.length);
+ defaultMilestones.forEach((milestone, index) => {
+ expect(suggestions.at(index).text()).toBe(milestone.text);
+ });
+ });
+
+ it('does not render divider when no defaultMilestones', async () => {
+ wrapper = createComponent({
+ active: true,
+ config: { ...mockMilestoneToken, defaultMilestones: [] },
+ stubs: { Portal: true },
+ });
+ const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+ const suggestionsSegment = tokenSegments.at(2);
+ suggestionsSegment.vm.$emit('activate');
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.contains(GlFilteredSearchSuggestion)).toBe(false);
+ expect(wrapper.contains(GlDropdownDivider)).toBe(false);
+ });
+
+ it('renders `DEFAULT_MILESTONES` as default suggestions', async () => {
+ wrapper = createComponent({
+ active: true,
+ config: { ...mockMilestoneToken },
+ stubs: { Portal: true },
+ });
+ const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+ const suggestionsSegment = tokenSegments.at(2);
+ suggestionsSegment.vm.$emit('activate');
+ await wrapper.vm.$nextTick();
+
+ const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
+
+ expect(suggestions).toHaveLength(DEFAULT_MILESTONES.length);
+ DEFAULT_MILESTONES.forEach((milestone, index) => {
+ expect(suggestions.at(index).text()).toBe(milestone.text);
+ });
});
});
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_dropdown_spec.js b/spec/frontend/vue_shared/components/filtered_search_dropdown_spec.js
deleted file mode 100644
index 87cafa0bb8c..00000000000
--- a/spec/frontend/vue_shared/components/filtered_search_dropdown_spec.js
+++ /dev/null
@@ -1,190 +0,0 @@
-import Vue from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
-import component from '~/vue_shared/components/filtered_search_dropdown.vue';
-
-describe('Filtered search dropdown', () => {
- const Component = Vue.extend(component);
- let vm;
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('with an empty array of items', () => {
- beforeEach(() => {
- vm = mountComponent(Component, {
- items: [],
- filterKey: '',
- });
- });
-
- it('renders empty list', () => {
- expect(vm.$el.querySelectorAll('.js-filtered-dropdown-result').length).toEqual(0);
- });
-
- it('renders filter input', () => {
- expect(vm.$el.querySelector('.js-filtered-dropdown-input')).not.toBeNull();
- });
- });
-
- describe('when visible numbers is less than the items length', () => {
- beforeEach(() => {
- vm = mountComponent(Component, {
- items: [{ title: 'One' }, { title: 'Two' }, { title: 'Three' }],
- visibleItems: 2,
- filterKey: 'title',
- });
- });
-
- it('it renders only the maximum number provided', () => {
- expect(vm.$el.querySelectorAll('.js-filtered-dropdown-result').length).toEqual(2);
- });
- });
-
- describe('when visible number is bigger than the items length', () => {
- beforeEach(() => {
- vm = mountComponent(Component, {
- items: [{ title: 'One' }, { title: 'Two' }, { title: 'Three' }],
- filterKey: 'title',
- });
- });
-
- it('it renders the full list of items the maximum number provided', () => {
- expect(vm.$el.querySelectorAll('.js-filtered-dropdown-result').length).toEqual(3);
- });
- });
-
- describe('while filtering', () => {
- beforeEach(() => {
- vm = mountComponent(Component, {
- items: [
- { title: 'One' },
- { title: 'Two/three' },
- { title: 'Three four' },
- { title: 'Five' },
- ],
- filterKey: 'title',
- });
- });
-
- it('updates the results to match the typed value', done => {
- vm.$el.querySelector('.js-filtered-dropdown-input').value = 'three';
- vm.$el.querySelector('.js-filtered-dropdown-input').dispatchEvent(new Event('input'));
- vm.$nextTick(() => {
- expect(vm.$el.querySelectorAll('.js-filtered-dropdown-result').length).toEqual(2);
- done();
- });
- });
-
- describe('when no value matches the typed one', () => {
- it('does not render any result', done => {
- vm.$el.querySelector('.js-filtered-dropdown-input').value = 'six';
- vm.$el.querySelector('.js-filtered-dropdown-input').dispatchEvent(new Event('input'));
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelectorAll('.js-filtered-dropdown-result').length).toEqual(0);
- done();
- });
- });
- });
- });
-
- describe('with create mode enabled', () => {
- describe('when there are no matches', () => {
- beforeEach(() => {
- vm = mountComponent(Component, {
- items: [
- { title: 'One' },
- { title: 'Two/three' },
- { title: 'Three four' },
- { title: 'Five' },
- ],
- filterKey: 'title',
- showCreateMode: true,
- });
-
- vm.$el.querySelector('.js-filtered-dropdown-input').value = 'eleven';
- vm.$el.querySelector('.js-filtered-dropdown-input').dispatchEvent(new Event('input'));
- });
-
- it('renders a create button', done => {
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.js-dropdown-create-button')).not.toBeNull();
- done();
- });
- });
-
- it('renders computed button text', done => {
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.js-dropdown-create-button').textContent.trim()).toEqual(
- 'Create eleven',
- );
- done();
- });
- });
-
- describe('on click create button', () => {
- it('emits createItem event with the filter', done => {
- jest.spyOn(vm, '$emit').mockImplementation(() => {});
- vm.$nextTick(() => {
- vm.$el.querySelector('.js-dropdown-create-button').click();
-
- expect(vm.$emit).toHaveBeenCalledWith('createItem', 'eleven');
- done();
- });
- });
- });
- });
-
- describe('when there are matches', () => {
- beforeEach(() => {
- vm = mountComponent(Component, {
- items: [
- { title: 'One' },
- { title: 'Two/three' },
- { title: 'Three four' },
- { title: 'Five' },
- ],
- filterKey: 'title',
- showCreateMode: true,
- });
-
- vm.$el.querySelector('.js-filtered-dropdown-input').value = 'one';
- vm.$el.querySelector('.js-filtered-dropdown-input').dispatchEvent(new Event('input'));
- });
-
- it('does not render a create button', done => {
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.js-dropdown-create-button')).toBeNull();
- done();
- });
- });
- });
- });
-
- describe('with create mode disabled', () => {
- describe('when there are no matches', () => {
- beforeEach(() => {
- vm = mountComponent(Component, {
- items: [
- { title: 'One' },
- { title: 'Two/three' },
- { title: 'Three four' },
- { title: 'Five' },
- ],
- filterKey: 'title',
- });
-
- vm.$el.querySelector('.js-filtered-dropdown-input').value = 'eleven';
- vm.$el.querySelector('.js-filtered-dropdown-input').dispatchEvent(new Event('input'));
- });
-
- it('does not render a create button', done => {
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.js-dropdown-create-button')).toBeNull();
- done();
- });
- });
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/icon_spec.js b/spec/frontend/vue_shared/components/icon_spec.js
deleted file mode 100644
index 16728e1705a..00000000000
--- a/spec/frontend/vue_shared/components/icon_spec.js
+++ /dev/null
@@ -1,78 +0,0 @@
-import Vue from 'vue';
-import { mount } from '@vue/test-utils';
-import mountComponent from 'helpers/vue_mount_component_helper';
-import iconsPath from '@gitlab/svgs/dist/icons.svg';
-import Icon from '~/vue_shared/components/icon.vue';
-
-jest.mock('@gitlab/svgs/dist/icons.svg', () => 'testing');
-
-describe('Sprite Icon Component', () => {
- describe('Initialization', () => {
- let icon;
-
- beforeEach(() => {
- const IconComponent = Vue.extend(Icon);
-
- icon = mountComponent(IconComponent, {
- name: 'commit',
- size: 32,
- });
- });
-
- afterEach(() => {
- icon.$destroy();
- });
-
- it('should return a defined Vue component', () => {
- expect(icon).toBeDefined();
- });
-
- it('should have <svg> as a child element', () => {
- expect(icon.$el.tagName).toBe('svg');
- });
-
- it('should have <use> as a child element with the correct href', () => {
- expect(icon.$el.firstChild.tagName).toBe('use');
- expect(icon.$el.firstChild.getAttribute('xlink:href')).toBe(`${iconsPath}#commit`);
- });
-
- it('should properly compute iconSizeClass', () => {
- expect(icon.iconSizeClass).toBe('s32');
- });
-
- it('forbids invalid size prop', () => {
- expect(icon.$options.props.size.validator(NaN)).toBeFalsy();
- expect(icon.$options.props.size.validator(0)).toBeFalsy();
- expect(icon.$options.props.size.validator(9001)).toBeFalsy();
- });
-
- it('should properly render img css', () => {
- const { classList } = icon.$el;
- const containsSizeClass = classList.contains('s32');
-
- expect(containsSizeClass).toBe(true);
- });
-
- it('`name` validator should return false for non existing icons', () => {
- jest.spyOn(console, 'warn').mockImplementation();
-
- expect(Icon.props.name.validator('non_existing_icon_sprite')).toBe(false);
- });
-
- it('`name` validator should return true for existing icons', () => {
- expect(Icon.props.name.validator('commit')).toBe(true);
- });
- });
-
- it('should call registered listeners when they are triggered', () => {
- const clickHandler = jest.fn();
- const wrapper = mount(Icon, {
- propsData: { name: 'commit' },
- listeners: { click: clickHandler },
- });
-
- wrapper.find('svg').trigger('click');
-
- expect(clickHandler).toHaveBeenCalled();
- });
-});
diff --git a/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js b/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js
index b72f78c4f60..c87d19df1f7 100644
--- a/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js
+++ b/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js
@@ -2,8 +2,8 @@ import Vue from 'vue';
import { shallowMount } from '@vue/test-utils';
import { mockMilestone } from 'jest/boards/mock_data';
+import { GlIcon } from '@gitlab/ui';
import IssueMilestone from '~/vue_shared/components/issue/issue_milestone.vue';
-import Icon from '~/vue_shared/components/icon.vue';
const createComponent = (milestone = mockMilestone) => {
const Component = Vue.extend(IssueMilestone);
@@ -135,7 +135,7 @@ describe('IssueMilestoneComponent', () => {
});
it('renders milestone icon', () => {
- expect(wrapper.find(Icon).props('name')).toBe('clock');
+ expect(wrapper.find(GlIcon).props('name')).toBe('clock');
});
it('renders milestone title', () => {
diff --git a/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js b/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js
index fb9487d0bf8..2319bf61482 100644
--- a/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js
+++ b/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js
@@ -35,6 +35,9 @@ describe('RelatedIssuableItem', () => {
weight: '<div class="js-weight-slot"></div>',
};
+ const findRemoveButton = () => wrapper.find({ ref: 'removeButton' });
+ const findLockIcon = () => wrapper.find({ ref: 'lockIcon' });
+
beforeEach(() => {
mountComponent({ props, slots });
});
@@ -121,10 +124,10 @@ describe('RelatedIssuableItem', () => {
});
it('renders milestone icon and name', () => {
- const milestoneIcon = tokenMetadata().find('.item-milestone svg use');
+ const milestoneIcon = tokenMetadata().find('.item-milestone svg');
const milestoneTitle = tokenMetadata().find('.item-milestone .milestone-title');
- expect(milestoneIcon.attributes('href')).toContain('clock');
+ expect(milestoneIcon.attributes('data-testid')).toBe('clock-icon');
expect(milestoneTitle.text()).toContain('Milestone title');
});
@@ -143,25 +146,27 @@ describe('RelatedIssuableItem', () => {
});
describe('remove button', () => {
- const removeButton = () => wrapper.find({ ref: 'removeButton' });
-
beforeEach(() => {
wrapper.setProps({ canRemove: true });
});
it('renders if canRemove', () => {
- expect(removeButton().exists()).toBe(true);
+ expect(findRemoveButton().exists()).toBe(true);
+ });
+
+ it('does not render the lock icon', () => {
+ expect(findLockIcon().exists()).toBe(false);
});
it('renders disabled button when removeDisabled', async () => {
wrapper.setData({ removeDisabled: true });
await wrapper.vm.$nextTick();
- expect(removeButton().attributes('disabled')).toEqual('disabled');
+ expect(findRemoveButton().attributes('disabled')).toEqual('disabled');
});
it('triggers onRemoveRequest when clicked', async () => {
- removeButton().trigger('click');
+ findRemoveButton().trigger('click');
await wrapper.vm.$nextTick();
const { relatedIssueRemoveRequest } = wrapper.emitted();
@@ -169,4 +174,23 @@ describe('RelatedIssuableItem', () => {
expect(relatedIssueRemoveRequest[0]).toEqual([props.idKey]);
});
});
+
+ describe('when issue is locked', () => {
+ const lockedMessage = 'Issues created from a vulnerability cannot be removed';
+
+ beforeEach(() => {
+ wrapper.setProps({
+ isLocked: true,
+ lockedMessage,
+ });
+ });
+
+ it('does not render the remove button', () => {
+ expect(findRemoveButton().exists()).toBe(false);
+ });
+
+ it('renders the lock icon with the correct title', () => {
+ expect(findLockIcon().attributes('title')).toBe(lockedMessage);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/markdown/header_spec.js b/spec/frontend/vue_shared/components/markdown/header_spec.js
index 551d781d296..82bc9b9fe08 100644
--- a/spec/frontend/vue_shared/components/markdown/header_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/header_spec.js
@@ -22,6 +22,12 @@ describe('Markdown field header component', () => {
.at(0);
beforeEach(() => {
+ window.gl = {
+ client: {
+ isMac: true,
+ },
+ };
+
createWrapper();
});
@@ -30,24 +36,40 @@ describe('Markdown field header component', () => {
wrapper = null;
});
- it('renders markdown header buttons', () => {
- const buttons = [
- 'Add bold text',
- 'Add italic text',
- 'Insert a quote',
- 'Insert suggestion',
- 'Insert code',
- 'Add a link',
- 'Add a bullet list',
- 'Add a numbered list',
- 'Add a task list',
- 'Add a table',
- 'Go full screen',
- ];
- const elements = findToolbarButtons();
-
- elements.wrappers.forEach((buttonEl, index) => {
- expect(buttonEl.props('buttonTitle')).toBe(buttons[index]);
+ describe('markdown header buttons', () => {
+ it('renders the buttons with the correct title', () => {
+ const buttons = [
+ 'Add bold text (⌘B)',
+ 'Add italic text (⌘I)',
+ 'Insert a quote',
+ 'Insert suggestion',
+ 'Insert code',
+ 'Add a link (⌘K)',
+ 'Add a bullet list',
+ 'Add a numbered list',
+ 'Add a task list',
+ 'Add a table',
+ 'Go full screen',
+ ];
+ const elements = findToolbarButtons();
+
+ elements.wrappers.forEach((buttonEl, index) => {
+ expect(buttonEl.props('buttonTitle')).toBe(buttons[index]);
+ });
+ });
+
+ 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)');
+ });
});
});
diff --git a/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js b/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js
index c6e147899e4..a521668b15c 100644
--- a/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js
@@ -77,10 +77,7 @@ describe('Suggestion Diff component', () => {
});
it('emits apply', () => {
- expect(wrapper.emittedByOrder()).toContainEqual({
- name: 'apply',
- args: [expect.any(Function)],
- });
+ expect(wrapper.emitted().apply).toEqual([[expect.any(Function)]]);
});
it('does not render apply suggestion and add to batch buttons', () => {
@@ -111,10 +108,7 @@ describe('Suggestion Diff component', () => {
findAddToBatchButton().vm.$emit('click');
- expect(wrapper.emittedByOrder()).toContainEqual({
- name: 'addToBatch',
- args: [],
- });
+ expect(wrapper.emitted().addToBatch).toEqual([[]]);
});
});
@@ -124,10 +118,7 @@ describe('Suggestion Diff component', () => {
findRemoveFromBatchButton().vm.$emit('click');
- expect(wrapper.emittedByOrder()).toContainEqual({
- name: 'removeFromBatch',
- args: [],
- });
+ expect(wrapper.emitted().removeFromBatch).toEqual([[]]);
});
});
@@ -137,10 +128,7 @@ describe('Suggestion Diff component', () => {
findApplyBatchButton().vm.$emit('click');
- expect(wrapper.emittedByOrder()).toContainEqual({
- name: 'applyBatch',
- args: [],
- });
+ expect(wrapper.emitted().applyBatch).toEqual([[]]);
});
});
diff --git a/spec/frontend/vue_shared/components/markdown/suggestion_diff_row_spec.js b/spec/frontend/vue_shared/components/markdown/suggestion_diff_row_spec.js
index 6ae405017c9..b67f4cf12bf 100644
--- a/spec/frontend/vue_shared/components/markdown/suggestion_diff_row_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/suggestion_diff_row_spec.js
@@ -34,12 +34,25 @@ describe('SuggestionDiffRow', () => {
const findOldLineWrapper = () => wrapper.find('.old_line');
const findNewLineWrapper = () => wrapper.find('.new_line');
+ const findSuggestionContent = () => wrapper.find('[data-testid="suggestion-diff-content"]');
afterEach(() => {
wrapper.destroy();
});
describe('renders correctly', () => {
+ it('renders the correct base suggestion markup', () => {
+ factory({
+ propsData: {
+ line: oldLine,
+ },
+ });
+
+ expect(findSuggestionContent().html()).toBe(
+ '<td data-testid="suggestion-diff-content" class="line_content old"><span class="line">oldrichtext</span></td>',
+ );
+ });
+
it('has the right classes on the wrapper', () => {
factory({
propsData: {
@@ -47,7 +60,12 @@ describe('SuggestionDiffRow', () => {
},
});
- expect(wrapper.is('.line_holder')).toBe(true);
+ expect(wrapper.classes()).toContain('line_holder');
+ expect(
+ findSuggestionContent()
+ .find('span')
+ .classes(),
+ ).toContain('line');
});
it('renders the rich text when it is available', () => {
diff --git a/spec/frontend/vue_shared/components/markdown/toolbar_button_spec.js b/spec/frontend/vue_shared/components/markdown/toolbar_button_spec.js
new file mode 100644
index 00000000000..8a7946fd7b1
--- /dev/null
+++ b/spec/frontend/vue_shared/components/markdown/toolbar_button_spec.js
@@ -0,0 +1,47 @@
+import { shallowMount } from '@vue/test-utils';
+import ToolbarButton from '~/vue_shared/components/markdown/toolbar_button.vue';
+
+describe('toolbar_button', () => {
+ let wrapper;
+
+ const defaultProps = {
+ buttonTitle: 'test button',
+ icon: 'rocket',
+ tag: 'test tag',
+ };
+
+ const createComponent = propUpdates => {
+ wrapper = shallowMount(ToolbarButton, {
+ propsData: {
+ ...defaultProps,
+ ...propUpdates,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ const getButtonShortcutsAttr = () => {
+ return wrapper.find('button').attributes('data-md-shortcuts');
+ };
+
+ describe('keyboard shortcuts', () => {
+ it.each`
+ shortcutsProp | mdShortcutsAttr
+ ${undefined} | ${JSON.stringify([])}
+ ${[]} | ${JSON.stringify([])}
+ ${'command+b'} | ${JSON.stringify(['command+b'])}
+ ${['command+b', 'ctrl+b']} | ${JSON.stringify(['command+b', 'ctrl+b'])}
+ `(
+ 'adds the attribute data-md-shortcuts="$mdShortcutsAttr" to the button when the shortcuts prop is $shortcutsProp',
+ ({ shortcutsProp, mdShortcutsAttr }) => {
+ createComponent({ shortcuts: shortcutsProp });
+
+ expect(getButtonShortcutsAttr()).toBe(mdShortcutsAttr);
+ },
+ );
+ });
+});
diff --git a/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js b/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js
index ae8c9a0928e..61660f79b71 100644
--- a/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js
+++ b/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js
@@ -1,11 +1,11 @@
import { shallowMount } from '@vue/test-utils';
+import { GlIcon } from '@gitlab/ui';
import NoteableWarning from '~/vue_shared/components/notes/noteable_warning.vue';
-import Icon from '~/vue_shared/components/icon.vue';
describe('Issue Warning Component', () => {
let wrapper;
- const findIcon = (w = wrapper) => w.find(Icon);
+ const findIcon = (w = wrapper) => w.find(GlIcon);
const findLockedBlock = (w = wrapper) => w.find({ ref: 'locked' });
const findConfidentialBlock = (w = wrapper) => w.find({ ref: 'confidential' });
const findLockedAndConfidentialBlock = (w = wrapper) => w.find({ ref: 'lockedAndConfidential' });
@@ -69,7 +69,7 @@ describe('Issue Warning Component', () => {
});
it('renders warning icon', () => {
- expect(wrapper.find(Icon).exists()).toBe(true);
+ expect(wrapper.find(GlIcon).exists()).toBe(true);
});
it('does not render information about locked noteable', () => {
@@ -95,7 +95,7 @@ describe('Issue Warning Component', () => {
});
it('does not render warning icon', () => {
- expect(wrapper.find(Icon).exists()).toBe(false);
+ expect(wrapper.find(GlIcon).exists()).toBe(false);
});
it('does not render information about locked noteable', () => {
diff --git a/spec/frontend/vue_shared/components/notes/timeline_entry_item_spec.js b/spec/frontend/vue_shared/components/notes/timeline_entry_item_spec.js
index f73d3edec5d..bd4b6a463ab 100644
--- a/spec/frontend/vue_shared/components/notes/timeline_entry_item_spec.js
+++ b/spec/frontend/vue_shared/components/notes/timeline_entry_item_spec.js
@@ -17,9 +17,9 @@ describe(`TimelineEntryItem`, () => {
it('renders correctly', () => {
factory();
- expect(wrapper.is('.timeline-entry')).toBe(true);
+ expect(wrapper.classes()).toContain('timeline-entry');
- expect(wrapper.contains('.timeline-entry-inner')).toBe(true);
+ expect(wrapper.find('.timeline-entry-inner').exists()).toBe(true);
});
it('accepts default slot', () => {
diff --git a/spec/frontend/vue_shared/components/ordered_layout_spec.js b/spec/frontend/vue_shared/components/ordered_layout_spec.js
index e8667d9ee4a..eec153c3792 100644
--- a/spec/frontend/vue_shared/components/ordered_layout_spec.js
+++ b/spec/frontend/vue_shared/components/ordered_layout_spec.js
@@ -27,7 +27,9 @@ describe('Ordered Layout', () => {
let wrapper;
const verifyOrder = () =>
- wrapper.findAll('footer,header').wrappers.map(x => (x.is('footer') ? 'footer' : 'header'));
+ wrapper
+ .findAll('footer,header')
+ .wrappers.map(x => (x.element.tagName === 'FOOTER' ? 'footer' : 'header'));
const createComponent = (props = {}) => {
wrapper = mount(TestComponent, {
diff --git a/spec/frontend/vue_shared/components/paginated_list_spec.js b/spec/frontend/vue_shared/components/paginated_list_spec.js
index 46e45296c37..c0ee49f194f 100644
--- a/spec/frontend/vue_shared/components/paginated_list_spec.js
+++ b/spec/frontend/vue_shared/components/paginated_list_spec.js
@@ -48,7 +48,7 @@ describe('Pagination links component', () => {
describe('rendering', () => {
it('it renders the gl-paginated-list', () => {
- expect(wrapper.contains('ul.list-group')).toBe(true);
+ expect(wrapper.find('ul.list-group').exists()).toBe(true);
expect(wrapper.findAll('li.list-group-item').length).toBe(2);
});
});
diff --git a/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js b/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js
index 385134c4a3f..649eb2643f1 100644
--- a/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js
+++ b/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js
@@ -29,7 +29,7 @@ describe('ProjectListItem component', () => {
it('does not render a check mark icon if selected === false', () => {
wrapper = shallowMount(Component, options);
- expect(wrapper.contains('.js-selected-icon.js-unselected')).toBe(true);
+ expect(wrapper.find('.js-selected-icon').exists()).toBe(false);
});
it('renders a check mark icon if selected === true', () => {
@@ -37,7 +37,7 @@ describe('ProjectListItem component', () => {
wrapper = shallowMount(Component, options);
- expect(wrapper.contains('.js-selected-icon.js-selected')).toBe(true);
+ expect(wrapper.find('.js-selected-icon').exists()).toBe(true);
});
it(`emits a "clicked" event when clicked`, () => {
@@ -53,7 +53,7 @@ describe('ProjectListItem component', () => {
it(`renders the project avatar`, () => {
wrapper = shallowMount(Component, options);
- expect(wrapper.contains('.js-project-avatar')).toBe(true);
+ expect(wrapper.find('.js-project-avatar').exists()).toBe(true);
});
it(`renders a simple namespace name with a trailing slash`, () => {
diff --git a/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap b/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap
new file mode 100644
index 00000000000..16094a42668
--- /dev/null
+++ b/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap
@@ -0,0 +1,60 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Package code instruction multiline to match the snapshot 1`] = `
+<div>
+ <pre
+ class="gl-font-monospace"
+ data-testid="multiline-instruction"
+ >
+ this is some
+multiline text
+ </pre>
+</div>
+`;
+
+exports[`Package code instruction single line to match the default snapshot 1`] = `
+<div
+ class="gl-mb-3"
+>
+ <label
+ for="instruction-input_2"
+ >
+ foo_label
+ </label>
+
+ <div
+ class="input-group gl-mb-3"
+ >
+ <input
+ class="form-control gl-font-monospace"
+ data-testid="instruction-input"
+ id="instruction-input_2"
+ readonly="readonly"
+ type="text"
+ />
+
+ <span
+ class="input-group-append"
+ data-testid="instruction-button"
+ >
+ <button
+ class="btn input-group-text btn-secondary btn-md btn-default"
+ data-clipboard-text="npm i @my-package"
+ title="Copy npm install command"
+ type="button"
+ >
+ <!---->
+
+ <svg
+ class="gl-icon s16"
+ data-testid="copy-to-clipboard-icon"
+ >
+ <use
+ href="#copy-to-clipboard"
+ />
+ </svg>
+ </button>
+ </span>
+ </div>
+</div>
+`;
diff --git a/spec/frontend/vue_shared/components/registry/__snapshots__/history_item_spec.js.snap b/spec/frontend/vue_shared/components/registry/__snapshots__/history_item_spec.js.snap
new file mode 100644
index 00000000000..2abae33bc19
--- /dev/null
+++ b/spec/frontend/vue_shared/components/registry/__snapshots__/history_item_spec.js.snap
@@ -0,0 +1,42 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`History Item renders the correct markup 1`] = `
+<li
+ class="timeline-entry system-note note-wrapper gl-mb-6!"
+>
+ <div
+ class="timeline-entry-inner"
+ >
+ <div
+ class="timeline-icon"
+ >
+ <gl-icon-stub
+ name="pencil"
+ size="16"
+ />
+ </div>
+
+ <div
+ class="timeline-content"
+ >
+ <div
+ class="note-header"
+ >
+ <span>
+ <div
+ data-testid="default-slot"
+ />
+ </span>
+ </div>
+
+ <div
+ class="note-body"
+ >
+ <div
+ data-testid="body-slot"
+ />
+ </div>
+ </div>
+ </div>
+</li>
+`;
diff --git a/spec/frontend/vue_shared/components/registry/code_instruction_spec.js b/spec/frontend/vue_shared/components/registry/code_instruction_spec.js
new file mode 100644
index 00000000000..84c738764a3
--- /dev/null
+++ b/spec/frontend/vue_shared/components/registry/code_instruction_spec.js
@@ -0,0 +1,117 @@
+import { mount } from '@vue/test-utils';
+import Tracking from '~/tracking';
+import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+
+describe('Package code instruction', () => {
+ let wrapper;
+
+ const defaultProps = {
+ instruction: 'npm i @my-package',
+ copyText: 'Copy npm install command',
+ };
+
+ function createComponent(props = {}) {
+ wrapper = mount(CodeInstruction, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ });
+ }
+
+ const findCopyButton = () => wrapper.find(ClipboardButton);
+ const findInputElement = () => wrapper.find('[data-testid="instruction-input"]');
+ const findMultilineInstruction = () => wrapper.find('[data-testid="multiline-instruction"]');
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('single line', () => {
+ beforeEach(() =>
+ createComponent({
+ label: 'foo_label',
+ }),
+ );
+
+ it('to match the default snapshot', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+
+ describe('multiline', () => {
+ beforeEach(() =>
+ createComponent({
+ instruction: 'this is some\nmultiline text',
+ copyText: 'Copy the command',
+ label: 'foo_label',
+ multiline: true,
+ }),
+ );
+
+ it('to match the snapshot', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+
+ describe('tracking', () => {
+ let eventSpy;
+ const trackingAction = 'test_action';
+ const trackingLabel = 'foo_label';
+
+ beforeEach(() => {
+ eventSpy = jest.spyOn(Tracking, 'event');
+ });
+
+ it('should not track when no trackingAction is provided', () => {
+ createComponent();
+ findCopyButton().trigger('click');
+
+ expect(eventSpy).toHaveBeenCalledTimes(0);
+ });
+
+ describe('when trackingAction is provided for single line', () => {
+ beforeEach(() =>
+ createComponent({
+ trackingAction,
+ trackingLabel,
+ }),
+ );
+
+ it('should track when copying from the input', () => {
+ findInputElement().trigger('copy');
+
+ expect(eventSpy).toHaveBeenCalledWith(undefined, trackingAction, {
+ label: trackingLabel,
+ });
+ });
+
+ it('should track when the copy button is pressed', () => {
+ findCopyButton().trigger('click');
+
+ expect(eventSpy).toHaveBeenCalledWith(undefined, trackingAction, {
+ label: trackingLabel,
+ });
+ });
+ });
+
+ describe('when trackingAction is provided for multiline', () => {
+ beforeEach(() =>
+ createComponent({
+ trackingAction,
+ trackingLabel,
+ multiline: true,
+ }),
+ );
+
+ it('should track when copying from the multiline pre element', () => {
+ findMultilineInstruction().trigger('copy');
+
+ expect(eventSpy).toHaveBeenCalledWith(undefined, trackingAction, {
+ label: trackingLabel,
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/registry/details_row_spec.js b/spec/frontend/vue_shared/components/registry/details_row_spec.js
new file mode 100644
index 00000000000..16a55b84787
--- /dev/null
+++ b/spec/frontend/vue_shared/components/registry/details_row_spec.js
@@ -0,0 +1,71 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlIcon } from '@gitlab/ui';
+import component from '~/vue_shared/components/registry/details_row.vue';
+
+describe('DetailsRow', () => {
+ let wrapper;
+
+ const findIcon = () => wrapper.find(GlIcon);
+ const findDefaultSlot = () => wrapper.find('[data-testid="default-slot"]');
+
+ const mountComponent = props => {
+ wrapper = shallowMount(component, {
+ propsData: {
+ icon: 'clock',
+ ...props,
+ },
+ slots: {
+ default: '<div data-testid="default-slot"></div>',
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('has a default slot', () => {
+ mountComponent();
+ expect(findDefaultSlot().exists()).toBe(true);
+ });
+
+ describe('icon prop', () => {
+ it('contains an icon', () => {
+ mountComponent();
+ expect(findIcon().exists()).toBe(true);
+ });
+
+ it('icon has the correct props', () => {
+ mountComponent();
+ expect(findIcon().props()).toMatchObject({
+ name: 'clock',
+ });
+ });
+ });
+
+ describe('padding prop', () => {
+ it('padding has a default', () => {
+ mountComponent();
+ expect(wrapper.classes('gl-py-2')).toBe(true);
+ });
+
+ it('is reflected in the template', () => {
+ mountComponent({ padding: 'gl-py-4' });
+ expect(wrapper.classes('gl-py-4')).toBe(true);
+ });
+ });
+
+ describe('dashed prop', () => {
+ const borderClasses = ['gl-border-b-solid', 'gl-border-gray-100', 'gl-border-b-1'];
+ it('by default component has no border', () => {
+ mountComponent();
+ expect(wrapper.classes).not.toEqual(expect.arrayContaining(borderClasses));
+ });
+
+ it('has a border when dashed is true', () => {
+ mountComponent({ dashed: true });
+ expect(wrapper.classes()).toEqual(expect.arrayContaining(borderClasses));
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/registry/history_item_spec.js b/spec/frontend/vue_shared/components/registry/history_item_spec.js
new file mode 100644
index 00000000000..d51ddda2e3e
--- /dev/null
+++ b/spec/frontend/vue_shared/components/registry/history_item_spec.js
@@ -0,0 +1,67 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlIcon } from '@gitlab/ui';
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
+import component from '~/vue_shared/components/registry/history_item.vue';
+
+describe('History Item', () => {
+ let wrapper;
+ const defaultProps = {
+ icon: 'pencil',
+ };
+
+ const mountComponent = () => {
+ wrapper = shallowMount(component, {
+ propsData: { ...defaultProps },
+ stubs: {
+ TimelineEntryItem,
+ },
+ slots: {
+ default: '<div data-testid="default-slot"></div>',
+ body: '<div data-testid="body-slot"></div>',
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ const findTimelineEntry = () => wrapper.find(TimelineEntryItem);
+ const findGlIcon = () => wrapper.find(GlIcon);
+ const findDefaultSlot = () => wrapper.find('[data-testid="default-slot"]');
+ const findBodySlot = () => wrapper.find('[data-testid="body-slot"]');
+
+ it('renders the correct markup', () => {
+ mountComponent();
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('has a default slot', () => {
+ mountComponent();
+
+ expect(findDefaultSlot().exists()).toBe(true);
+ });
+
+ it('has a body slot', () => {
+ mountComponent();
+
+ expect(findBodySlot().exists()).toBe(true);
+ });
+
+ it('has a timeline entry', () => {
+ mountComponent();
+
+ expect(findTimelineEntry().exists()).toBe(true);
+ });
+
+ it('has an icon', () => {
+ mountComponent();
+
+ const icon = findGlIcon();
+
+ expect(icon.exists()).toBe(true);
+ expect(icon.attributes('name')).toBe(defaultProps.icon);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/registry/list_item_spec.js b/spec/frontend/vue_shared/components/registry/list_item_spec.js
new file mode 100644
index 00000000000..e2cfdedb4bf
--- /dev/null
+++ b/spec/frontend/vue_shared/components/registry/list_item_spec.js
@@ -0,0 +1,135 @@
+import { GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import component from '~/vue_shared/components/registry/list_item.vue';
+
+describe('list item', () => {
+ let wrapper;
+
+ const findLeftActionSlot = () => wrapper.find('[data-testid="left-action"]');
+ const findLeftPrimarySlot = () => wrapper.find('[data-testid="left-primary"]');
+ const findLeftSecondarySlot = () => wrapper.find('[data-testid="left-secondary"]');
+ const findRightPrimarySlot = () => wrapper.find('[data-testid="right-primary"]');
+ const findRightSecondarySlot = () => wrapper.find('[data-testid="right-secondary"]');
+ const findRightActionSlot = () => wrapper.find('[data-testid="right-action"]');
+ const findDetailsSlot = name => wrapper.find(`[data-testid="${name}"]`);
+ const findToggleDetailsButton = () => wrapper.find(GlButton);
+
+ const mountComponent = (propsData, slots) => {
+ wrapper = shallowMount(component, {
+ propsData,
+ slots: {
+ 'left-action': '<div data-testid="left-action" />',
+ 'left-primary': '<div data-testid="left-primary" />',
+ 'left-secondary': '<div data-testid="left-secondary" />',
+ 'right-primary': '<div data-testid="right-primary" />',
+ 'right-secondary': '<div data-testid="right-secondary" />',
+ 'right-action': '<div data-testid="right-action" />',
+ ...slots,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe.each`
+ slotName | finderFunction
+ ${'left-primary'} | ${findLeftPrimarySlot}
+ ${'left-secondary'} | ${findLeftSecondarySlot}
+ ${'right-primary'} | ${findRightPrimarySlot}
+ ${'right-secondary'} | ${findRightSecondarySlot}
+ ${'left-action'} | ${findLeftActionSlot}
+ ${'right-action'} | ${findRightActionSlot}
+ `('$slotName slot', ({ finderFunction, slotName }) => {
+ it('exist when the slot is filled', () => {
+ mountComponent();
+
+ expect(finderFunction().exists()).toBe(true);
+ });
+
+ it('does not exist when the slot is empty', () => {
+ mountComponent({}, { [slotName]: '' });
+
+ expect(finderFunction().exists()).toBe(false);
+ });
+ });
+
+ describe.each`
+ slotNames
+ ${['details_foo']}
+ ${['details_foo', 'details_bar']}
+ ${['details_foo', 'details_bar', 'details_baz']}
+ `('$slotNames details slots', ({ slotNames }) => {
+ const slotMocks = slotNames.reduce((acc, current) => {
+ acc[current] = `<div data-testid="${current}" />`;
+ return acc;
+ }, {});
+
+ it('are visible when details is shown', async () => {
+ mountComponent({}, slotMocks);
+
+ await wrapper.vm.$nextTick();
+ findToggleDetailsButton().vm.$emit('click');
+
+ await wrapper.vm.$nextTick();
+ slotNames.forEach(name => {
+ expect(findDetailsSlot(name).exists()).toBe(true);
+ });
+ });
+ it('are not visible when details are not shown', () => {
+ mountComponent({}, slotMocks);
+
+ slotNames.forEach(name => {
+ expect(findDetailsSlot(name).exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('details toggle button', () => {
+ it('is visible when at least one details slot exists', async () => {
+ mountComponent({}, { details_foo: '<span></span>' });
+ await wrapper.vm.$nextTick();
+ expect(findToggleDetailsButton().exists()).toBe(true);
+ });
+
+ it('is hidden without details slot', () => {
+ mountComponent();
+ expect(findToggleDetailsButton().exists()).toBe(false);
+ });
+ });
+
+ describe('disabled prop', () => {
+ it('when true applies disabled-content class', () => {
+ mountComponent({ disabled: true });
+
+ expect(wrapper.classes('disabled-content')).toBe(true);
+ });
+
+ it('when false does not apply disabled-content class', () => {
+ mountComponent({ disabled: false });
+
+ expect(wrapper.classes('disabled-content')).toBe(false);
+ });
+ });
+
+ describe('borders and selection', () => {
+ it.each`
+ first | selected | shouldHave | shouldNotHave
+ ${true} | ${true} | ${['gl-bg-blue-50', 'gl-border-blue-200']} | ${['gl-border-t-transparent', 'gl-border-t-gray-100']}
+ ${false} | ${true} | ${['gl-bg-blue-50', 'gl-border-blue-200']} | ${['gl-border-t-transparent', 'gl-border-t-gray-100']}
+ ${true} | ${false} | ${['gl-border-b-gray-100']} | ${['gl-bg-blue-50', 'gl-border-blue-200']}
+ ${false} | ${false} | ${['gl-border-b-gray-100']} | ${['gl-bg-blue-50', 'gl-border-blue-200']}
+ `(
+ 'when first is $first and selected is $selected',
+ ({ first, selected, shouldHave, shouldNotHave }) => {
+ mountComponent({ first, selected });
+
+ expect(wrapper.classes()).toEqual(expect.arrayContaining(shouldHave));
+
+ expect(wrapper.classes()).toEqual(expect.not.arrayContaining(shouldNotHave));
+ },
+ );
+ });
+});
diff --git a/spec/frontend/vue_shared/components/registry/metadata_item_spec.js b/spec/frontend/vue_shared/components/registry/metadata_item_spec.js
new file mode 100644
index 00000000000..ff968ff1831
--- /dev/null
+++ b/spec/frontend/vue_shared/components/registry/metadata_item_spec.js
@@ -0,0 +1,101 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlIcon, GlLink } from '@gitlab/ui';
+import component from '~/vue_shared/components/registry/metadata_item.vue';
+import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
+
+describe('Metadata Item', () => {
+ let wrapper;
+ const defaultProps = {
+ text: 'foo',
+ };
+
+ const mountComponent = (propsData = defaultProps) => {
+ wrapper = shallowMount(component, {
+ propsData,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ const findIcon = () => wrapper.find(GlIcon);
+ const findLink = (w = wrapper) => w.find(GlLink);
+ const findText = () => wrapper.find('[data-testid="metadata-item-text"]');
+ const findTooltipOnTruncate = (w = wrapper) => w.find(TooltipOnTruncate);
+
+ describe.each(['xs', 's', 'm', 'l', 'xl'])('size class', size => {
+ const className = `mw-${size}`;
+
+ it(`${size} is assigned correctly to text`, () => {
+ mountComponent({ ...defaultProps, size });
+
+ expect(findText().classes()).toContain(className);
+ });
+
+ it(`${size} is assigned correctly to link`, () => {
+ mountComponent({ ...defaultProps, link: 'foo', size });
+
+ expect(findTooltipOnTruncate().classes()).toContain(className);
+ });
+ });
+
+ describe('text', () => {
+ it('display a proper text', () => {
+ mountComponent();
+
+ expect(findText().text()).toBe(defaultProps.text);
+ });
+
+ it('uses tooltip_on_truncate', () => {
+ mountComponent();
+
+ const tooltip = findTooltipOnTruncate(findText());
+ expect(tooltip.exists()).toBe(true);
+ expect(tooltip.attributes('title')).toBe(defaultProps.text);
+ });
+ });
+
+ describe('link', () => {
+ it('if a link prop is passed shows a link and hides the text', () => {
+ mountComponent({ ...defaultProps, link: 'bar' });
+
+ expect(findLink().exists()).toBe(true);
+ expect(findText().exists()).toBe(false);
+
+ expect(findLink().attributes('href')).toBe('bar');
+ });
+
+ it('uses tooltip_on_truncate', () => {
+ mountComponent({ ...defaultProps, link: 'bar' });
+
+ const tooltip = findTooltipOnTruncate();
+ expect(tooltip.exists()).toBe(true);
+ expect(tooltip.attributes('title')).toBe(defaultProps.text);
+ expect(findLink(tooltip).exists()).toBe(true);
+ });
+
+ it('hides the link and shows the test if a link prop is not passed', () => {
+ mountComponent();
+
+ expect(findText().exists()).toBe(true);
+ expect(findLink().exists()).toBe(false);
+ });
+ });
+
+ describe('icon', () => {
+ it('if a icon prop is passed shows a icon', () => {
+ mountComponent({ ...defaultProps, icon: 'pencil' });
+
+ expect(findIcon().exists()).toBe(true);
+ expect(findIcon().props('name')).toBe('pencil');
+ });
+
+ it('if a icon prop is not passed hides the icon', () => {
+ mountComponent();
+
+ expect(findIcon().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/registry/title_area_spec.js b/spec/frontend/vue_shared/components/registry/title_area_spec.js
new file mode 100644
index 00000000000..6740d6097a4
--- /dev/null
+++ b/spec/frontend/vue_shared/components/registry/title_area_spec.js
@@ -0,0 +1,98 @@
+import { GlAvatar } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import component from '~/vue_shared/components/registry/title_area.vue';
+
+describe('title area', () => {
+ let wrapper;
+
+ const findSubHeaderSlot = () => wrapper.find('[data-testid="sub-header"]');
+ const findRightActionsSlot = () => wrapper.find('[data-testid="right-actions"]');
+ const findMetadataSlot = name => wrapper.find(`[data-testid="${name}"]`);
+ const findTitle = () => wrapper.find('[data-testid="title"]');
+ const findAvatar = () => wrapper.find(GlAvatar);
+
+ const mountComponent = ({ propsData = { title: 'foo' }, slots } = {}) => {
+ wrapper = shallowMount(component, {
+ propsData,
+ slots: {
+ 'sub-header': '<div data-testid="sub-header" />',
+ 'right-actions': '<div data-testid="right-actions" />',
+ ...slots,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('title', () => {
+ it('if slot is not present defaults to prop', () => {
+ mountComponent();
+
+ expect(findTitle().text()).toBe('foo');
+ });
+ it('if slot is present uses slot', () => {
+ mountComponent({
+ slots: {
+ title: 'slot_title',
+ },
+ });
+ expect(findTitle().text()).toBe('slot_title');
+ });
+ });
+
+ describe('avatar', () => {
+ it('is shown if avatar props exist', () => {
+ mountComponent({ propsData: { title: 'foo', avatar: 'baz' } });
+
+ expect(findAvatar().props('src')).toBe('baz');
+ });
+
+ it('is hidden if avatar props does not exist', () => {
+ mountComponent();
+
+ expect(findAvatar().exists()).toBe(false);
+ });
+ });
+
+ describe.each`
+ slotName | finderFunction
+ ${'sub-header'} | ${findSubHeaderSlot}
+ ${'right-actions'} | ${findRightActionsSlot}
+ `('$slotName slot', ({ finderFunction, slotName }) => {
+ it('exist when the slot is filled', () => {
+ mountComponent();
+
+ expect(finderFunction().exists()).toBe(true);
+ });
+
+ it('does not exist when the slot is empty', () => {
+ mountComponent({ slots: { [slotName]: '' } });
+
+ expect(finderFunction().exists()).toBe(false);
+ });
+ });
+
+ describe.each`
+ slotNames
+ ${['metadata_foo']}
+ ${['metadata_foo', 'metadata_bar']}
+ ${['metadata_foo', 'metadata_bar', 'metadata_baz']}
+ `('$slotNames metadata slots', ({ slotNames }) => {
+ const slotMocks = slotNames.reduce((acc, current) => {
+ acc[current] = `<div data-testid="${current}" />`;
+ return acc;
+ }, {});
+
+ it('exist when the slot is present', async () => {
+ mountComponent({ slots: slotMocks });
+
+ await wrapper.vm.$nextTick();
+ slotNames.forEach(name => {
+ expect(findMetadataSlot(name).exists()).toBe(true);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/remove_member_modal_spec.js b/spec/frontend/vue_shared/components/remove_member_modal_spec.js
index 2d380b25a0a..78fe6d53eee 100644
--- a/spec/frontend/vue_shared/components/remove_member_modal_spec.js
+++ b/spec/frontend/vue_shared/components/remove_member_modal_spec.js
@@ -48,7 +48,7 @@ describe('RemoveMemberModal', () => {
});
it(`${checkboxTestDescription}`, () => {
- expect(wrapper.contains(GlFormCheckbox)).toBe(checkboxExpected);
+ expect(wrapper.find(GlFormCheckbox).exists()).toBe(checkboxExpected);
});
it('submits the form when the modal is submitted', () => {
diff --git a/spec/frontend/vue_shared/components/resizable_chart/__snapshots__/skeleton_loader_spec.js.snap b/spec/frontend/vue_shared/components/resizable_chart/__snapshots__/skeleton_loader_spec.js.snap
index 103b53cb280..3990248d021 100644
--- a/spec/frontend/vue_shared/components/resizable_chart/__snapshots__/skeleton_loader_spec.js.snap
+++ b/spec/frontend/vue_shared/components/resizable_chart/__snapshots__/skeleton_loader_spec.js.snap
@@ -1,324 +1,433 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Resizable Skeleton Loader default setup renders the bars, labels, and grid with correct position, size, and rx percentages 1`] = `
-<gl-skeleton-loader-stub
- baseurl=""
- height="130"
- preserveaspectratio="xMidYMid meet"
- width="400"
+<div
+ class="gl-px-8"
>
- <rect
- data-testid="skeleton-chart-grid"
- height="1px"
- width="100%"
- x="0"
- y="30%"
- />
- <rect
- data-testid="skeleton-chart-grid"
- height="1px"
- width="100%"
- x="0"
- y="60%"
- />
- <rect
- data-testid="skeleton-chart-grid"
- height="1px"
- width="100%"
- x="0"
- y="90%"
- />
-
- <rect
- data-testid="skeleton-chart-bar"
- height="5%"
- rx="0.4%"
- width="6%"
- x="5.875%"
- y="85%"
- />
- <rect
- data-testid="skeleton-chart-bar"
- height="7%"
- rx="0.4%"
- width="6%"
- x="17.625%"
- y="83%"
- />
- <rect
- data-testid="skeleton-chart-bar"
- height="9%"
- rx="0.4%"
- width="6%"
- x="29.375%"
- y="81%"
- />
- <rect
- data-testid="skeleton-chart-bar"
- height="14%"
- rx="0.4%"
- width="6%"
- x="41.125%"
- y="76%"
- />
- <rect
- data-testid="skeleton-chart-bar"
- height="21%"
- rx="0.4%"
- width="6%"
- x="52.875%"
- y="69%"
- />
- <rect
- data-testid="skeleton-chart-bar"
- height="35%"
- rx="0.4%"
- width="6%"
- x="64.625%"
- y="55%"
- />
- <rect
- data-testid="skeleton-chart-bar"
- height="50%"
- rx="0.4%"
- width="6%"
- x="76.375%"
- y="40%"
- />
- <rect
- data-testid="skeleton-chart-bar"
- height="80%"
- rx="0.4%"
- width="6%"
- x="88.125%"
- y="10%"
- />
-
- <rect
- data-testid="skeleton-chart-label"
- height="5%"
- rx="0.4%"
- width="4%"
- x="6.875%"
- y="95%"
- />
- <rect
- data-testid="skeleton-chart-label"
- height="5%"
- rx="0.4%"
- width="4%"
- x="18.625%"
- y="95%"
- />
- <rect
- data-testid="skeleton-chart-label"
- height="5%"
- rx="0.4%"
- width="4%"
- x="30.375%"
- y="95%"
- />
- <rect
- data-testid="skeleton-chart-label"
- height="5%"
- rx="0.4%"
- width="4%"
- x="42.125%"
- y="95%"
- />
- <rect
- data-testid="skeleton-chart-label"
- height="5%"
- rx="0.4%"
- width="4%"
- x="53.875%"
- y="95%"
- />
- <rect
- data-testid="skeleton-chart-label"
- height="5%"
- rx="0.4%"
- width="4%"
- x="65.625%"
- y="95%"
- />
- <rect
- data-testid="skeleton-chart-label"
- height="5%"
- rx="0.4%"
- width="4%"
- x="77.375%"
- y="95%"
- />
- <rect
- data-testid="skeleton-chart-label"
- height="5%"
- rx="0.4%"
- width="4%"
- x="89.125%"
- y="95%"
- />
-</gl-skeleton-loader-stub>
+ <svg
+ class="gl-skeleton-loader"
+ preserveAspectRatio="xMidYMid meet"
+ version="1.1"
+ viewBox="0 0 400 130"
+ >
+ <rect
+ clip-path="url(#null-idClip)"
+ height="130"
+ style="fill: url(#null-idGradient);"
+ width="400"
+ x="0"
+ y="0"
+ />
+ <defs>
+ <clippath
+ id="null-idClip"
+ >
+ <rect
+ data-testid="skeleton-chart-grid"
+ height="1px"
+ width="100%"
+ x="0"
+ y="30%"
+ />
+ <rect
+ data-testid="skeleton-chart-grid"
+ height="1px"
+ width="100%"
+ x="0"
+ y="60%"
+ />
+ <rect
+ data-testid="skeleton-chart-grid"
+ height="1px"
+ width="100%"
+ x="0"
+ y="90%"
+ />
+
+ <rect
+ data-testid="skeleton-chart-bar"
+ height="5%"
+ rx="0.4%"
+ width="4%"
+ x="6%"
+ y="85%"
+ />
+ <rect
+ data-testid="skeleton-chart-bar"
+ height="7%"
+ rx="0.4%"
+ width="4%"
+ x="18%"
+ y="83%"
+ />
+ <rect
+ data-testid="skeleton-chart-bar"
+ height="9%"
+ rx="0.4%"
+ width="4%"
+ x="30%"
+ y="81%"
+ />
+ <rect
+ data-testid="skeleton-chart-bar"
+ height="14%"
+ rx="0.4%"
+ width="4%"
+ x="42%"
+ y="76%"
+ />
+ <rect
+ data-testid="skeleton-chart-bar"
+ height="21%"
+ rx="0.4%"
+ width="4%"
+ x="54%"
+ y="69%"
+ />
+ <rect
+ data-testid="skeleton-chart-bar"
+ height="35%"
+ rx="0.4%"
+ width="4%"
+ x="66%"
+ y="55%"
+ />
+ <rect
+ data-testid="skeleton-chart-bar"
+ height="50%"
+ rx="0.4%"
+ width="4%"
+ x="78%"
+ y="40%"
+ />
+ <rect
+ data-testid="skeleton-chart-bar"
+ height="80%"
+ rx="0.4%"
+ width="4%"
+ x="90%"
+ y="10%"
+ />
+
+ <rect
+ data-testid="skeleton-chart-label"
+ height="3%"
+ rx="0.4%"
+ width="3%"
+ x="6.5%"
+ y="97%"
+ />
+ <rect
+ data-testid="skeleton-chart-label"
+ height="3%"
+ rx="0.4%"
+ width="3%"
+ x="18.5%"
+ y="97%"
+ />
+ <rect
+ data-testid="skeleton-chart-label"
+ height="3%"
+ rx="0.4%"
+ width="3%"
+ x="30.5%"
+ y="97%"
+ />
+ <rect
+ data-testid="skeleton-chart-label"
+ height="3%"
+ rx="0.4%"
+ width="3%"
+ x="42.5%"
+ y="97%"
+ />
+ <rect
+ data-testid="skeleton-chart-label"
+ height="3%"
+ rx="0.4%"
+ width="3%"
+ x="54.5%"
+ y="97%"
+ />
+ <rect
+ data-testid="skeleton-chart-label"
+ height="3%"
+ rx="0.4%"
+ width="3%"
+ x="66.5%"
+ y="97%"
+ />
+ <rect
+ data-testid="skeleton-chart-label"
+ height="3%"
+ rx="0.4%"
+ width="3%"
+ x="78.5%"
+ y="97%"
+ />
+ <rect
+ data-testid="skeleton-chart-label"
+ height="3%"
+ rx="0.4%"
+ width="3%"
+ x="90.5%"
+ y="97%"
+ />
+ </clippath>
+ <lineargradient
+ id="null-idGradient"
+ >
+ <stop
+ class="primary-stop"
+ offset="0%"
+ >
+ <animate
+ attributeName="offset"
+ dur="1s"
+ repeatCount="indefinite"
+ values="-2; 1"
+ />
+ </stop>
+ <stop
+ class="secondary-stop"
+ offset="50%"
+ >
+ <animate
+ attributeName="offset"
+ dur="1s"
+ repeatCount="indefinite"
+ values="-1.5; 1.5"
+ />
+ </stop>
+ <stop
+ class="primary-stop"
+ offset="100%"
+ >
+ <animate
+ attributeName="offset"
+ dur="1s"
+ repeatCount="indefinite"
+ values="-1; 2"
+ />
+ </stop>
+ </lineargradient>
+ </defs>
+ </svg>
+</div>
`;
exports[`Resizable Skeleton Loader with custom settings renders the correct position, and size percentages for bars and labels with different settings 1`] = `
-<gl-skeleton-loader-stub
- baseurl=""
- height="130"
- preserveaspectratio="xMidYMid meet"
- uniquekey=""
- width="400"
+<div
+ class="gl-px-8"
>
- <rect
- data-testid="skeleton-chart-grid"
- height="1px"
- width="100%"
- x="0"
- y="30%"
- />
- <rect
- data-testid="skeleton-chart-grid"
- height="1px"
- width="100%"
- x="0"
- y="60%"
- />
- <rect
- data-testid="skeleton-chart-grid"
- height="1px"
- width="100%"
- x="0"
- y="90%"
- />
-
- <rect
- data-testid="skeleton-chart-bar"
- height="5%"
- rx="0.6%"
- width="3%"
- x="6.0625%"
- y="85%"
- />
- <rect
- data-testid="skeleton-chart-bar"
- height="7%"
- rx="0.6%"
- width="3%"
- x="18.1875%"
- y="83%"
- />
- <rect
- data-testid="skeleton-chart-bar"
- height="9%"
- rx="0.6%"
- width="3%"
- x="30.3125%"
- y="81%"
- />
- <rect
- data-testid="skeleton-chart-bar"
- height="14%"
- rx="0.6%"
- width="3%"
- x="42.4375%"
- y="76%"
- />
- <rect
- data-testid="skeleton-chart-bar"
- height="21%"
- rx="0.6%"
- width="3%"
- x="54.5625%"
- y="69%"
- />
- <rect
- data-testid="skeleton-chart-bar"
- height="35%"
- rx="0.6%"
- width="3%"
- x="66.6875%"
- y="55%"
- />
- <rect
- data-testid="skeleton-chart-bar"
- height="50%"
- rx="0.6%"
- width="3%"
- x="78.8125%"
- y="40%"
- />
- <rect
- data-testid="skeleton-chart-bar"
- height="80%"
- rx="0.6%"
- width="3%"
- x="90.9375%"
- y="10%"
- />
-
- <rect
- data-testid="skeleton-chart-label"
- height="2%"
- rx="0.6%"
- width="7%"
- x="4.0625%"
- y="98%"
- />
- <rect
- data-testid="skeleton-chart-label"
- height="2%"
- rx="0.6%"
- width="7%"
- x="16.1875%"
- y="98%"
- />
- <rect
- data-testid="skeleton-chart-label"
- height="2%"
- rx="0.6%"
- width="7%"
- x="28.3125%"
- y="98%"
- />
- <rect
- data-testid="skeleton-chart-label"
- height="2%"
- rx="0.6%"
- width="7%"
- x="40.4375%"
- y="98%"
- />
- <rect
- data-testid="skeleton-chart-label"
- height="2%"
- rx="0.6%"
- width="7%"
- x="52.5625%"
- y="98%"
- />
- <rect
- data-testid="skeleton-chart-label"
- height="2%"
- rx="0.6%"
- width="7%"
- x="64.6875%"
- y="98%"
- />
- <rect
- data-testid="skeleton-chart-label"
- height="2%"
- rx="0.6%"
- width="7%"
- x="76.8125%"
- y="98%"
- />
- <rect
- data-testid="skeleton-chart-label"
- height="2%"
- rx="0.6%"
- width="7%"
- x="88.9375%"
- y="98%"
- />
-</gl-skeleton-loader-stub>
+ <svg
+ class="gl-skeleton-loader"
+ preserveAspectRatio="xMidYMid meet"
+ version="1.1"
+ viewBox="0 0 400 130"
+ >
+ <rect
+ clip-path="url(#-idClip)"
+ height="130"
+ style="fill: url(#-idGradient);"
+ width="400"
+ x="0"
+ y="0"
+ />
+ <defs>
+ <clippath
+ id="-idClip"
+ >
+ <rect
+ data-testid="skeleton-chart-grid"
+ height="1px"
+ width="100%"
+ x="0"
+ y="30%"
+ />
+ <rect
+ data-testid="skeleton-chart-grid"
+ height="1px"
+ width="100%"
+ x="0"
+ y="60%"
+ />
+ <rect
+ data-testid="skeleton-chart-grid"
+ height="1px"
+ width="100%"
+ x="0"
+ y="90%"
+ />
+
+ <rect
+ data-testid="skeleton-chart-bar"
+ height="5%"
+ rx="0.6%"
+ width="3%"
+ x="6.0625%"
+ y="85%"
+ />
+ <rect
+ data-testid="skeleton-chart-bar"
+ height="7%"
+ rx="0.6%"
+ width="3%"
+ x="18.1875%"
+ y="83%"
+ />
+ <rect
+ data-testid="skeleton-chart-bar"
+ height="9%"
+ rx="0.6%"
+ width="3%"
+ x="30.3125%"
+ y="81%"
+ />
+ <rect
+ data-testid="skeleton-chart-bar"
+ height="14%"
+ rx="0.6%"
+ width="3%"
+ x="42.4375%"
+ y="76%"
+ />
+ <rect
+ data-testid="skeleton-chart-bar"
+ height="21%"
+ rx="0.6%"
+ width="3%"
+ x="54.5625%"
+ y="69%"
+ />
+ <rect
+ data-testid="skeleton-chart-bar"
+ height="35%"
+ rx="0.6%"
+ width="3%"
+ x="66.6875%"
+ y="55%"
+ />
+ <rect
+ data-testid="skeleton-chart-bar"
+ height="50%"
+ rx="0.6%"
+ width="3%"
+ x="78.8125%"
+ y="40%"
+ />
+ <rect
+ data-testid="skeleton-chart-bar"
+ height="80%"
+ rx="0.6%"
+ width="3%"
+ x="90.9375%"
+ y="10%"
+ />
+
+ <rect
+ data-testid="skeleton-chart-label"
+ height="2%"
+ rx="0.6%"
+ width="7%"
+ x="4.0625%"
+ y="98%"
+ />
+ <rect
+ data-testid="skeleton-chart-label"
+ height="2%"
+ rx="0.6%"
+ width="7%"
+ x="16.1875%"
+ y="98%"
+ />
+ <rect
+ data-testid="skeleton-chart-label"
+ height="2%"
+ rx="0.6%"
+ width="7%"
+ x="28.3125%"
+ y="98%"
+ />
+ <rect
+ data-testid="skeleton-chart-label"
+ height="2%"
+ rx="0.6%"
+ width="7%"
+ x="40.4375%"
+ y="98%"
+ />
+ <rect
+ data-testid="skeleton-chart-label"
+ height="2%"
+ rx="0.6%"
+ width="7%"
+ x="52.5625%"
+ y="98%"
+ />
+ <rect
+ data-testid="skeleton-chart-label"
+ height="2%"
+ rx="0.6%"
+ width="7%"
+ x="64.6875%"
+ y="98%"
+ />
+ <rect
+ data-testid="skeleton-chart-label"
+ height="2%"
+ rx="0.6%"
+ width="7%"
+ x="76.8125%"
+ y="98%"
+ />
+ <rect
+ data-testid="skeleton-chart-label"
+ height="2%"
+ rx="0.6%"
+ width="7%"
+ x="88.9375%"
+ y="98%"
+ />
+ </clippath>
+ <lineargradient
+ id="-idGradient"
+ >
+ <stop
+ class="primary-stop"
+ offset="0%"
+ >
+ <animate
+ attributeName="offset"
+ dur="1s"
+ repeatCount="indefinite"
+ values="-2; 1"
+ />
+ </stop>
+ <stop
+ class="secondary-stop"
+ offset="50%"
+ >
+ <animate
+ attributeName="offset"
+ dur="1s"
+ repeatCount="indefinite"
+ values="-1.5; 1.5"
+ />
+ </stop>
+ <stop
+ class="primary-stop"
+ offset="100%"
+ >
+ <animate
+ attributeName="offset"
+ dur="1s"
+ repeatCount="indefinite"
+ values="-1; 2"
+ />
+ </stop>
+ </lineargradient>
+ </defs>
+ </svg>
+</div>
`;
diff --git a/spec/frontend/vue_shared/components/resizable_chart/skeleton_loader_spec.js b/spec/frontend/vue_shared/components/resizable_chart/skeleton_loader_spec.js
index 7facd02e596..bfc3aeb0303 100644
--- a/spec/frontend/vue_shared/components/resizable_chart/skeleton_loader_spec.js
+++ b/spec/frontend/vue_shared/components/resizable_chart/skeleton_loader_spec.js
@@ -1,11 +1,11 @@
-import { shallowMount } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue';
describe('Resizable Skeleton Loader', () => {
let wrapper;
const createComponent = (propsData = {}) => {
- wrapper = shallowMount(ChartSkeletonLoader, {
+ wrapper = mount(ChartSkeletonLoader, {
propsData,
});
};
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/build_custom_renderer_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/build_custom_renderer_spec.js
index cafe53e6bb2..a823d04024d 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/services/build_custom_renderer_spec.js
+++ b/spec/frontend/vue_shared/components/rich_content_editor/services/build_custom_renderer_spec.js
@@ -5,8 +5,13 @@ describe('Build Custom Renderer Service', () => {
it('should return an object with the default renderer functions when lacking arguments', () => {
expect(buildCustomHTMLRenderer()).toEqual(
expect.objectContaining({
- list: expect.any(Function),
+ htmlBlock: expect.any(Function),
+ htmlInline: expect.any(Function),
+ heading: expect.any(Function),
+ item: expect.any(Function),
+ paragraph: expect.any(Function),
text: expect.any(Function),
+ softbreak: expect.any(Function),
}),
);
});
@@ -20,8 +25,6 @@ describe('Build Custom Renderer Service', () => {
expect(buildCustomHTMLRenderer(customRenderers)).toEqual(
expect.objectContaining({
html: expect.any(Function),
- list: expect.any(Function),
- text: expect.any(Function),
}),
);
});
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer_spec.js
index a90d3528d60..fd745c21bb6 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer_spec.js
+++ b/spec/frontend/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer_spec.js
@@ -1,9 +1,10 @@
import buildHTMLToMarkdownRenderer from '~/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer';
+import { attributeDefinition } from './renderers/mock_data';
-describe('HTMLToMarkdownRenderer', () => {
+describe('rich_content_editor/services/html_to_markdown_renderer', () => {
let baseRenderer;
let htmlToMarkdownRenderer;
- const NODE = { nodeValue: 'mock_node' };
+ let fakeNode;
beforeEach(() => {
baseRenderer = {
@@ -12,14 +13,20 @@ describe('HTMLToMarkdownRenderer', () => {
getSpaceControlled: jest.fn(input => `space controlled ${input}`),
convert: jest.fn(),
};
+
+ fakeNode = { nodeValue: 'mock_node', dataset: {} };
+ });
+
+ afterEach(() => {
+ htmlToMarkdownRenderer = null;
});
describe('TEXT_NODE visitor', () => {
it('composes getSpaceControlled, getSpaceCollapsedText, and trim services', () => {
htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer);
- expect(htmlToMarkdownRenderer.TEXT_NODE(NODE)).toBe(
- `space controlled trimmed space collapsed ${NODE.nodeValue}`,
+ expect(htmlToMarkdownRenderer.TEXT_NODE(fakeNode)).toBe(
+ `space controlled trimmed space collapsed ${fakeNode.nodeValue}`,
);
});
});
@@ -43,8 +50,8 @@ describe('HTMLToMarkdownRenderer', () => {
baseRenderer.convert.mockReturnValueOnce(list);
- expect(htmlToMarkdownRenderer['LI OL, LI UL'](NODE, list)).toBe(result);
- expect(baseRenderer.convert).toHaveBeenCalledWith(NODE, list);
+ expect(htmlToMarkdownRenderer['LI OL, LI UL'](fakeNode, list)).toBe(result);
+ expect(baseRenderer.convert).toHaveBeenCalledWith(fakeNode, list);
});
});
@@ -62,10 +69,21 @@ describe('HTMLToMarkdownRenderer', () => {
});
baseRenderer.convert.mockReturnValueOnce(listItem);
- expect(htmlToMarkdownRenderer['UL LI'](NODE, listItem)).toBe(result);
- expect(baseRenderer.convert).toHaveBeenCalledWith(NODE, listItem);
+ expect(htmlToMarkdownRenderer['UL LI'](fakeNode, listItem)).toBe(result);
+ expect(baseRenderer.convert).toHaveBeenCalledWith(fakeNode, listItem);
},
);
+
+ it('detects attribute definitions and attaches them to the list item', () => {
+ const listItem = '- list item';
+ const result = `${listItem}\n${attributeDefinition}\n`;
+
+ fakeNode.dataset.attributeDefinition = attributeDefinition;
+ htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer);
+ baseRenderer.convert.mockReturnValueOnce(`${listItem}\n`);
+
+ expect(htmlToMarkdownRenderer['UL LI'](fakeNode, listItem)).toBe(result);
+ });
});
describe('OL LI visitor', () => {
@@ -85,8 +103,8 @@ describe('HTMLToMarkdownRenderer', () => {
});
baseRenderer.convert.mockReturnValueOnce(listItem);
- expect(htmlToMarkdownRenderer['OL LI'](NODE, subContent)).toBe(result);
- expect(baseRenderer.convert).toHaveBeenCalledWith(NODE, subContent);
+ expect(htmlToMarkdownRenderer['OL LI'](fakeNode, subContent)).toBe(result);
+ expect(baseRenderer.convert).toHaveBeenCalledWith(fakeNode, subContent);
},
);
});
@@ -105,8 +123,8 @@ describe('HTMLToMarkdownRenderer', () => {
baseRenderer.convert.mockReturnValueOnce(input);
- expect(htmlToMarkdownRenderer['STRONG, B'](NODE, input)).toBe(result);
- expect(baseRenderer.convert).toHaveBeenCalledWith(NODE, input);
+ expect(htmlToMarkdownRenderer['STRONG, B'](fakeNode, input)).toBe(result);
+ expect(baseRenderer.convert).toHaveBeenCalledWith(fakeNode, input);
},
);
});
@@ -125,9 +143,50 @@ describe('HTMLToMarkdownRenderer', () => {
baseRenderer.convert.mockReturnValueOnce(input);
- expect(htmlToMarkdownRenderer['EM, I'](NODE, input)).toBe(result);
- expect(baseRenderer.convert).toHaveBeenCalledWith(NODE, input);
+ expect(htmlToMarkdownRenderer['EM, I'](fakeNode, input)).toBe(result);
+ expect(baseRenderer.convert).toHaveBeenCalledWith(fakeNode, input);
},
);
});
+
+ describe('H1, H2, H3, H4, H5, H6 visitor', () => {
+ it('detects attribute definitions and attaches them to the heading', () => {
+ const heading = 'heading text';
+ const result = `${heading.trimRight()}\n${attributeDefinition}\n\n`;
+
+ fakeNode.dataset.attributeDefinition = attributeDefinition;
+ htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer);
+ baseRenderer.convert.mockReturnValueOnce(`${heading}\n\n`);
+
+ expect(htmlToMarkdownRenderer['H1, H2, H3, H4, H5, H6'](fakeNode, heading)).toBe(result);
+ });
+ });
+
+ describe('PRE CODE', () => {
+ let node;
+ const subContent = 'sub content';
+ const originalConverterResult = 'base result';
+
+ beforeEach(() => {
+ node = document.createElement('PRE');
+
+ node.innerText = 'reference definition content';
+ node.dataset.sseReferenceDefinition = true;
+
+ baseRenderer.convert.mockReturnValueOnce(originalConverterResult);
+ htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer);
+ });
+
+ it('returns raw text when pre node has sse-reference-definitions class', () => {
+ expect(htmlToMarkdownRenderer['PRE CODE'](node, subContent)).toBe(
+ `\n\n${node.innerText}\n\n`,
+ );
+ });
+
+ it('returns base result when pre node does not have sse-reference-definitions class', () => {
+ delete node.dataset.sseReferenceDefinition;
+
+ expect(htmlToMarkdownRenderer['PRE CODE'](node, subContent)).toBe(originalConverterResult);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/mock_data.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/mock_data.js
index 660c21281fd..5cf3961819e 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/mock_data.js
+++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/mock_data.js
@@ -1,12 +1,6 @@
// Node spec helpers
-export const buildMockTextNode = literal => {
- return {
- firstChild: null,
- literal,
- type: 'text',
- };
-};
+export const buildMockTextNode = literal => ({ literal, type: 'text' });
export const normalTextNode = buildMockTextNode('This is just normal text.');
@@ -23,17 +17,20 @@ const buildMockUneditableOpenToken = type => {
};
};
-const buildMockUneditableCloseToken = type => {
- return { type: 'closeTag', tagName: type };
+const buildMockTextToken = content => {
+ return {
+ type: 'text',
+ tagName: null,
+ content,
+ };
};
-export const originToken = {
- type: 'text',
- tagName: null,
- content: '{:.no_toc .hidden-md .hidden-lg}',
-};
+const buildMockUneditableCloseToken = type => ({ type: 'closeTag', tagName: type });
+
+export const originToken = buildMockTextToken('{:.no_toc .hidden-md .hidden-lg}');
+const uneditableOpenToken = buildMockUneditableOpenToken('div');
+export const uneditableOpenTokens = [uneditableOpenToken, originToken];
export const uneditableCloseToken = buildMockUneditableCloseToken('div');
-export const uneditableOpenTokens = [buildMockUneditableOpenToken('div'), originToken];
export const uneditableCloseTokens = [originToken, uneditableCloseToken];
export const uneditableTokens = [...uneditableOpenTokens, uneditableCloseToken];
@@ -41,6 +38,7 @@ export const originInlineToken = {
type: 'text',
content: '<i>Inline</i> content',
};
+
export const uneditableInlineTokens = [
buildMockUneditableOpenToken('a'),
originInlineToken,
@@ -48,11 +46,9 @@ export const uneditableInlineTokens = [
];
export const uneditableBlockTokens = [
- buildMockUneditableOpenToken('div'),
- {
- type: 'text',
- tagName: null,
- content: '<div><h1>Some header</h1><p>Some paragraph</p></div>',
- },
- buildMockUneditableCloseToken('div'),
+ uneditableOpenToken,
+ buildMockTextToken('<div><h1>Some header</h1><p>Some paragraph</p></div>'),
+ uneditableCloseToken,
];
+
+export const attributeDefinition = '{:.no_toc .hidden-md .hidden-lg}';
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_attribute_definition_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_attribute_definition_spec.js
new file mode 100644
index 00000000000..69fd9a67a21
--- /dev/null
+++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_attribute_definition_spec.js
@@ -0,0 +1,25 @@
+import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_attribute_definition';
+import { attributeDefinition } from './mock_data';
+
+describe('rich_content_editor/renderers/render_attribute_definition', () => {
+ describe('canRender', () => {
+ it.each`
+ input | result
+ ${{ literal: attributeDefinition }} | ${true}
+ ${{ literal: `FOO${attributeDefinition}` }} | ${false}
+ ${{ literal: `${attributeDefinition}BAR` }} | ${false}
+ ${{ literal: 'foobar' }} | ${false}
+ `('returns $result when input is $input', ({ input, result }) => {
+ expect(renderer.canRender(input)).toBe(result);
+ });
+ });
+
+ describe('render', () => {
+ it('returns an empty HTML comment', () => {
+ expect(renderer.render()).toEqual({
+ type: 'html',
+ content: '<!-- sse-attribute-definition -->',
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_heading_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_heading_spec.js
new file mode 100644
index 00000000000..76abc1ec3d8
--- /dev/null
+++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_heading_spec.js
@@ -0,0 +1,12 @@
+import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_heading';
+import * as renderUtils from '~/vue_shared/components/rich_content_editor/services/renderers/render_utils';
+
+describe('rich_content_editor/renderers/render_heading', () => {
+ it('canRender delegates to renderUtils.willAlwaysRender', () => {
+ expect(renderer.canRender).toBe(renderUtils.willAlwaysRender);
+ });
+
+ it('render delegates to renderUtils.renderWithAttributeDefinitions', () => {
+ expect(renderer.render).toBe(renderUtils.renderWithAttributeDefinitions);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph_spec.js
index f4a06b91a10..b3d9576f38b 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph_spec.js
+++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph_spec.js
@@ -1,5 +1,4 @@
import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph';
-import { renderUneditableBranch } from '~/vue_shared/components/rich_content_editor/services/renderers/render_utils';
import { buildMockTextNode } from './mock_data';
@@ -17,7 +16,7 @@ const identifierParagraphNode = buildMockParagraphNode(
`[another-identifier]: https://example.com "This example has a title" [identifier]: http://example1.com [this link]: http://example2.com`,
);
-describe('Render Identifier Paragraph renderer', () => {
+describe('rich_content_editor/renderers_render_identifier_paragraph', () => {
describe('canRender', () => {
it.each`
node | paragraph | target
@@ -37,8 +36,49 @@ describe('Render Identifier Paragraph renderer', () => {
});
describe('render', () => {
- it('should delegate rendering to the renderUneditableBranch util', () => {
- expect(renderer.render).toBe(renderUneditableBranch);
+ let context;
+ let result;
+
+ beforeEach(() => {
+ const node = {
+ firstChild: {
+ type: 'text',
+ literal: '[Some text]: https://link.com',
+ next: {
+ type: 'linebreak',
+ next: {
+ type: 'text',
+ literal: '[identifier]: http://example1.com "title"',
+ },
+ },
+ },
+ };
+ context = { skipChildren: jest.fn() };
+ result = renderer.render(node, context);
+ });
+
+ it('renders the reference definitions as a code block', () => {
+ expect(result).toEqual([
+ {
+ type: 'openTag',
+ tagName: 'pre',
+ classNames: ['code-block', 'language-markdown'],
+ attributes: {
+ 'data-sse-reference-definition': true,
+ },
+ },
+ { type: 'openTag', tagName: 'code' },
+ {
+ type: 'text',
+ content: '[Some text]: https://link.com\n[identifier]: http://example1.com "title"',
+ },
+ { type: 'closeTag', tagName: 'code' },
+ { type: 'closeTag', tagName: 'pre' },
+ ]);
+ });
+
+ it('skips the reference definition node children from rendering', () => {
+ expect(context.skipChildren).toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list_spec.js
deleted file mode 100644
index 7d427108ba6..00000000000
--- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list_spec.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list';
-import { renderUneditableBranch } from '~/vue_shared/components/rich_content_editor/services/renderers/render_utils';
-
-import { buildMockTextNode } from './mock_data';
-
-const buildMockListNode = literal => {
- return {
- firstChild: {
- firstChild: {
- firstChild: buildMockTextNode(literal),
- type: 'paragraph',
- },
- type: 'item',
- },
- type: 'list',
- };
-};
-
-const normalListNode = buildMockListNode('Just another bullet point');
-const kramdownListNode = buildMockListNode('TOC');
-
-describe('Render Kramdown List renderer', () => {
- describe('canRender', () => {
- it('should return true when the argument is a special kramdown TOC ordered/unordered list', () => {
- expect(renderer.canRender(kramdownListNode)).toBe(true);
- });
-
- it('should return false when the argument is a normal ordered/unordered list', () => {
- expect(renderer.canRender(normalListNode)).toBe(false);
- });
- });
-
- describe('render', () => {
- it('should delegate rendering to the renderUneditableBranch util', () => {
- expect(renderer.render).toBe(renderUneditableBranch);
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text_spec.js
deleted file mode 100644
index 1d2d152ffc3..00000000000
--- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text_spec.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text';
-import { renderUneditableLeaf } from '~/vue_shared/components/rich_content_editor/services/renderers/render_utils';
-
-import { buildMockTextNode, normalTextNode } from './mock_data';
-
-const kramdownTextNode = buildMockTextNode('{:toc}');
-
-describe('Render Kramdown Text renderer', () => {
- describe('canRender', () => {
- it('should return true when the argument `literal` has kramdown syntax', () => {
- expect(renderer.canRender(kramdownTextNode)).toBe(true);
- });
-
- it('should return false when the argument `literal` lacks kramdown syntax', () => {
- expect(renderer.canRender(normalTextNode)).toBe(false);
- });
- });
-
- describe('render', () => {
- it('should delegate rendering to the renderUneditableLeaf util', () => {
- expect(renderer.render).toBe(renderUneditableLeaf);
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_list_item_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_list_item_spec.js
new file mode 100644
index 00000000000..c1ab700535b
--- /dev/null
+++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_list_item_spec.js
@@ -0,0 +1,12 @@
+import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_list_item';
+import * as renderUtils from '~/vue_shared/components/rich_content_editor/services/renderers/render_utils';
+
+describe('rich_content_editor/renderers/render_list_item', () => {
+ it('canRender delegates to renderUtils.willAlwaysRender', () => {
+ expect(renderer.canRender).toBe(renderUtils.willAlwaysRender);
+ });
+
+ it('render delegates to renderUtils.renderWithAttributeDefinitions', () => {
+ expect(renderer.render).toBe(renderUtils.renderWithAttributeDefinitions);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_utils_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_utils_spec.js
index 92435b3e4e3..774f830f421 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_utils_spec.js
+++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_utils_spec.js
@@ -1,6 +1,8 @@
import {
renderUneditableLeaf,
renderUneditableBranch,
+ renderWithAttributeDefinitions,
+ willAlwaysRender,
} from '~/vue_shared/components/rich_content_editor/services/renderers/render_utils';
import {
@@ -8,9 +10,9 @@ import {
buildUneditableOpenTokens,
} from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token';
-import { originToken, uneditableCloseToken } from './mock_data';
+import { originToken, uneditableCloseToken, attributeDefinition } from './mock_data';
-describe('Render utils', () => {
+describe('rich_content_editor/renderers/render_utils', () => {
describe('renderUneditableLeaf', () => {
it('should return uneditable block tokens around an origin token', () => {
const context = { origin: jest.fn().mockReturnValueOnce(originToken) };
@@ -41,4 +43,68 @@ describe('Render utils', () => {
expect(result).toStrictEqual(uneditableCloseToken);
});
});
+
+ describe('willAlwaysRender', () => {
+ it('always returns true', () => {
+ expect(willAlwaysRender()).toBe(true);
+ });
+ });
+
+ describe('renderWithAttributeDefinitions', () => {
+ let openTagToken;
+ let closeTagToken;
+ let node;
+ const attributes = {
+ 'data-attribute-definition': attributeDefinition,
+ };
+
+ beforeEach(() => {
+ openTagToken = { type: 'openTag' };
+ closeTagToken = { type: 'closeTag' };
+ node = {
+ next: {
+ firstChild: {
+ literal: attributeDefinition,
+ },
+ },
+ };
+ });
+
+ describe('when token type is openTag', () => {
+ it('attaches attributes when attributes exist in the node’s next sibling', () => {
+ const context = { origin: () => openTagToken };
+
+ expect(renderWithAttributeDefinitions(node, context)).toEqual({
+ ...openTagToken,
+ attributes,
+ });
+ });
+
+ it('attaches attributes when attributes exist in the node’s children', () => {
+ const context = { origin: () => openTagToken };
+ node = {
+ firstChild: {
+ firstChild: {
+ next: {
+ next: {
+ literal: attributeDefinition,
+ },
+ },
+ },
+ },
+ };
+
+ expect(renderWithAttributeDefinitions(node, context)).toEqual({
+ ...openTagToken,
+ attributes,
+ });
+ });
+ });
+
+ it('does not attach attributes when token type is "closeTag"', () => {
+ const context = { origin: () => closeTagToken };
+
+ expect(renderWithAttributeDefinitions({}, context)).toBe(closeTagToken);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js
index edec3b138b3..c2091a681f2 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js
@@ -14,6 +14,7 @@ const createComponent = headerTitle => {
};
describe('DropdownCreateLabelComponent', () => {
+ const colorsCount = Object.keys(mockSuggestedColors).length;
let vm;
beforeEach(() => {
@@ -27,7 +28,7 @@ describe('DropdownCreateLabelComponent', () => {
describe('created', () => {
it('initializes `suggestedColors` prop on component from `gon.suggested_color_labels` object', () => {
- expect(vm.suggestedColors.length).toBe(mockSuggestedColors.length);
+ expect(vm.suggestedColors.length).toBe(colorsCount);
});
});
@@ -37,12 +38,10 @@ describe('DropdownCreateLabelComponent', () => {
});
it('renders `Go back` button on component header', () => {
- const backButtonEl = vm.$el.querySelector(
- '.dropdown-title button.dropdown-title-button.dropdown-menu-back',
- );
+ const backButtonEl = vm.$el.querySelector('.dropdown-title .dropdown-menu-back');
expect(backButtonEl).not.toBe(null);
- expect(backButtonEl.querySelector('.fa-arrow-left')).not.toBe(null);
+ expect(backButtonEl.querySelector('[data-testid="arrow-left-icon"]')).not.toBe(null);
});
it('renders component header element as `Create new label` when `headerTitle` prop is not provided', () => {
@@ -61,12 +60,9 @@ describe('DropdownCreateLabelComponent', () => {
});
it('renders `Close` button on component header', () => {
- const closeButtonEl = vm.$el.querySelector(
- '.dropdown-title button.dropdown-title-button.dropdown-menu-close',
- );
+ const closeButtonEl = vm.$el.querySelector('.dropdown-title .dropdown-menu-close');
expect(closeButtonEl).not.toBe(null);
- expect(closeButtonEl.querySelector('.fa-times.dropdown-menu-close-icon')).not.toBe(null);
});
it('renders `Name new label` input element', () => {
@@ -78,11 +74,11 @@ describe('DropdownCreateLabelComponent', () => {
const colorsListContainerEl = vm.$el.querySelector('.suggest-colors.suggest-colors-dropdown');
expect(colorsListContainerEl).not.toBe(null);
- expect(colorsListContainerEl.querySelectorAll('a').length).toBe(mockSuggestedColors.length);
+ expect(colorsListContainerEl.querySelectorAll('a').length).toBe(colorsCount);
const colorItemEl = colorsListContainerEl.querySelectorAll('a')[0];
- expect(colorItemEl.dataset.color).toBe(vm.suggestedColors[0]);
+ expect(colorItemEl.dataset.color).toBe(vm.suggestedColors[0].colorCode);
expect(colorItemEl.getAttribute('style')).toBe('background-color: rgb(0, 51, 204);');
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js
index 2e721d75b40..0b9a7262e41 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js
@@ -33,7 +33,7 @@ describe('DropdownHeaderComponent', () => {
);
expect(closeBtnEl).not.toBeNull();
- expect(closeBtnEl.querySelector('.fa-times.dropdown-menu-close-icon')).not.toBeNull();
+ expect(closeBtnEl.querySelector('.dropdown-menu-close-icon')).not.toBeNull();
});
});
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
index e09f0006359..7847e0ee71d 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
@@ -87,7 +87,7 @@ describe('DropdownValueCollapsedComponent', () => {
});
it('renders tags icon element', () => {
- expect(vm.$el.querySelector('.fa-tags')).not.toBeNull();
+ expect(vm.$el.querySelector('[data-testid="labels-icon"]')).not.toBeNull();
});
it('renders labels count', () => {
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/mock_data.js b/spec/frontend/vue_shared/components/sidebar/labels_select/mock_data.js
index 6564c012e67..648ba84fe8f 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/mock_data.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/mock_data.js
@@ -15,29 +15,29 @@ export const mockLabels = [
},
];
-export const mockSuggestedColors = [
- '#0033CC',
- '#428BCA',
- '#44AD8E',
- '#A8D695',
- '#5CB85C',
- '#69D100',
- '#004E00',
- '#34495E',
- '#7F8C8D',
- '#A295D6',
- '#5843AD',
- '#8E44AD',
- '#FFECDB',
- '#AD4363',
- '#D10069',
- '#CC0033',
- '#FF0000',
- '#D9534F',
- '#D1D100',
- '#F0AD4E',
- '#AD8D43',
-];
+export const mockSuggestedColors = {
+ '#0033CC': 'UA blue',
+ '#428BCA': 'Moderate blue',
+ '#44AD8E': 'Lime green',
+ '#A8D695': 'Feijoa',
+ '#5CB85C': 'Slightly desaturated green',
+ '#69D100': 'Bright green',
+ '#004E00': 'Very dark lime green',
+ '#34495E': 'Very dark desaturated blue',
+ '#7F8C8D': 'Dark grayish cyan',
+ '#A295D6': 'Slightly desaturated blue',
+ '#5843AD': 'Dark moderate blue',
+ '#8E44AD': 'Dark moderate violet',
+ '#FFECDB': 'Very pale orange',
+ '#AD4363': 'Dark moderate pink',
+ '#D10069': 'Strong pink',
+ '#CC0033': 'Strong red',
+ '#FF0000': 'Pure red',
+ '#D9534F': 'Soft red',
+ '#D1D100': 'Strong yellow',
+ '#F0AD4E': 'Soft orange',
+ '#AD8D43': 'Dark moderate orange',
+};
export const mockConfig = {
showCreate: true,
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js
index cb758797c63..951f706421f 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js
@@ -62,7 +62,7 @@ describe('DropdownButton', () => {
describe('template', () => {
it('renders component container element', () => {
- expect(wrapper.is('gl-button-stub')).toBe(true);
+ expect(wrapper.find(GlButton).element).toBe(wrapper.element);
});
it('renders default button text element', () => {
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js
index a1e0db4d29e..8c17a974b39 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js
@@ -65,6 +65,33 @@ describe('LabelsSelectRoot', () => {
]),
);
});
+
+ it('calls `handleDropdownClose` with state.labels filterd using `set` prop when dropdown variant is `embedded`', () => {
+ wrapper = createComponent({
+ ...mockConfig,
+ variant: 'embedded',
+ });
+
+ jest.spyOn(wrapper.vm, 'handleDropdownClose').mockImplementation();
+
+ wrapper.vm.handleVuexActionDispatch(
+ { type: 'toggleDropdownContents' },
+ {
+ showDropdownButton: false,
+ showDropdownContents: false,
+ labels: [{ id: 1 }, { id: 2, set: true }],
+ },
+ );
+
+ expect(wrapper.vm.handleDropdownClose).toHaveBeenCalledWith(
+ expect.arrayContaining([
+ {
+ id: 2,
+ set: true,
+ },
+ ]),
+ );
+ });
});
describe('handleDropdownClose', () => {
@@ -123,11 +150,10 @@ describe('LabelsSelectRoot', () => {
expect(wrapper.find(DropdownTitle).exists()).toBe(true);
});
- it('renders `dropdown-value` component with slot when `showDropdownButton` prop is `false`', () => {
+ it('renders `dropdown-value` component', () => {
const wrapperDropdownValue = createComponent(mockConfig, {
default: 'None',
});
- wrapperDropdownValue.vm.$store.state.showDropdownButton = false;
return wrapperDropdownValue.vm.$nextTick(() => {
const valueComp = wrapperDropdownValue.find(DropdownValue);
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js
index c742220ba8a..bfb8e263d81 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js
@@ -259,6 +259,21 @@ describe('LabelsSelect Actions', () => {
});
});
+ describe('replaceSelectedLabels', () => {
+ it('replaces `state.selectedLabels`', done => {
+ const selectedLabels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
+
+ testAction(
+ actions.replaceSelectedLabels,
+ selectedLabels,
+ state,
+ [{ type: types.REPLACE_SELECTED_LABELS, payload: selectedLabels }],
+ [],
+ done,
+ );
+ });
+ });
+
describe('updateSelectedLabels', () => {
it('updates `state.labels` based on provided `labels` param', done => {
const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js
index 8081806e314..3414eec8a63 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js
@@ -152,6 +152,19 @@ describe('LabelsSelect Mutations', () => {
});
});
+ describe(`${types.REPLACE_SELECTED_LABELS}`, () => {
+ it('replaces `state.selectedLabels`', () => {
+ const state = {
+ selectedLabels: [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }],
+ };
+ const newSelectedLabels = [{ id: 2 }, { id: 5 }];
+
+ mutations[types.REPLACE_SELECTED_LABELS](state, newSelectedLabels);
+
+ expect(state.selectedLabels).toEqual(newSelectedLabels);
+ });
+ });
+
describe(`${types.UPDATE_SELECTED_LABELS}`, () => {
const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
diff --git a/spec/frontend/vue_shared/components/table_pagination_spec.js b/spec/frontend/vue_shared/components/table_pagination_spec.js
index ef3ae088eec..058dfcdbde2 100644
--- a/spec/frontend/vue_shared/components/table_pagination_spec.js
+++ b/spec/frontend/vue_shared/components/table_pagination_spec.js
@@ -34,7 +34,7 @@ describe('Pagination component', () => {
change: spy,
});
- expect(wrapper.isEmpty()).toBe(true);
+ expect(wrapper.html()).toBe('');
});
it('renders if there is a next page', () => {
@@ -50,7 +50,7 @@ describe('Pagination component', () => {
change: spy,
});
- expect(wrapper.isEmpty()).toBe(false);
+ expect(wrapper.find(GlPagination).exists()).toBe(true);
});
it('renders if there is a prev page', () => {
@@ -66,7 +66,7 @@ describe('Pagination component', () => {
change: spy,
});
- expect(wrapper.isEmpty()).toBe(false);
+ expect(wrapper.find(GlPagination).exists()).toBe(true);
});
});
diff --git a/spec/frontend/vue_shared/components/todo_button_spec.js b/spec/frontend/vue_shared/components/todo_button_spec.js
new file mode 100644
index 00000000000..482b5de11f6
--- /dev/null
+++ b/spec/frontend/vue_shared/components/todo_button_spec.js
@@ -0,0 +1,48 @@
+import { shallowMount, mount } from '@vue/test-utils';
+import { GlButton } from '@gitlab/ui';
+import TodoButton from '~/vue_shared/components/todo_button.vue';
+
+describe('Todo Button', () => {
+ let wrapper;
+
+ const createComponent = (props = {}, mountFn = shallowMount) => {
+ wrapper = mountFn(TodoButton, {
+ propsData: {
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders GlButton', () => {
+ createComponent();
+
+ expect(wrapper.find(GlButton).exists()).toBe(true);
+ });
+
+ it('emits click event when clicked', () => {
+ createComponent({}, mount);
+ wrapper.find(GlButton).trigger('click');
+
+ expect(wrapper.emitted().click).toBeTruthy();
+ });
+
+ it.each`
+ label | isTodo
+ ${'Mark as done'} | ${true}
+ ${'Add a To-Do'} | ${false}
+ `('sets correct label when isTodo is $isTodo', ({ label, isTodo }) => {
+ createComponent({ isTodo });
+
+ expect(wrapper.find(GlButton).text()).toBe(label);
+ });
+
+ it('binds additional props to GlButton', () => {
+ createComponent({ loading: true });
+
+ expect(wrapper.find(GlButton).props('loading')).toBe(true);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js
index 902e83da7be..84e7a6a162e 100644
--- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js
+++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js
@@ -38,10 +38,6 @@ describe('User Avatar Link Component', () => {
wrapper = null;
});
- it('should return a defined Vue component', () => {
- expect(wrapper.isVueInstance()).toBe(true);
- });
-
it('should have user-avatar-image registered as child component', () => {
expect(wrapper.vm.$options.components.userAvatarImage).toBeDefined();
});
diff --git a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
index a4ff6ac0c16..b43bb6b10e0 100644
--- a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
+++ b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
@@ -1,7 +1,6 @@
-import { GlSkeletonLoading, GlSprintf } from '@gitlab/ui';
+import { GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlSprintf, GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import UserPopover from '~/vue_shared/components/user_popover/user_popover.vue';
-import Icon from '~/vue_shared/components/icon.vue';
const DEFAULT_PROPS = {
user: {
@@ -74,7 +73,7 @@ describe('User Popover Component', () => {
});
it('shows icon for location', () => {
- const iconEl = wrapper.find(Icon);
+ const iconEl = wrapper.find(GlIcon);
expect(iconEl.props('name')).toEqual('location');
});
@@ -139,9 +138,9 @@ describe('User Popover Component', () => {
createWrapper({ user });
- expect(wrapper.findAll(Icon).filter(icon => icon.props('name') === 'profile').length).toEqual(
- 1,
- );
+ expect(
+ wrapper.findAll(GlIcon).filter(icon => icon.props('name') === 'profile').length,
+ ).toEqual(1);
});
it('shows icon for work information', () => {
@@ -152,7 +151,9 @@ describe('User Popover Component', () => {
createWrapper({ user });
- expect(wrapper.findAll(Icon).filter(icon => icon.props('name') === 'work').length).toEqual(1);
+ expect(wrapper.findAll(GlIcon).filter(icon => icon.props('name') === 'work').length).toEqual(
+ 1,
+ );
});
});
diff --git a/spec/frontend/vue_shared/components/web_ide_link_spec.js b/spec/frontend/vue_shared/components/web_ide_link_spec.js
new file mode 100644
index 00000000000..57f511903d9
--- /dev/null
+++ b/spec/frontend/vue_shared/components/web_ide_link_spec.js
@@ -0,0 +1,106 @@
+import { shallowMount } from '@vue/test-utils';
+import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
+import WebIdeLink from '~/vue_shared/components/web_ide_link.vue';
+import ActionsButton from '~/vue_shared/components/actions_button.vue';
+
+const TEST_WEB_IDE_URL = '/-/ide/project/gitlab-test/test/edit/master/-/';
+const TEST_GITPOD_URL = 'https://gitpod.test/';
+
+const ACTION_WEB_IDE = {
+ href: TEST_WEB_IDE_URL,
+ key: 'webide',
+ secondaryText: 'Quickly and easily edit multiple files in your project.',
+ tooltip: '',
+ text: 'Web IDE',
+ attrs: {
+ 'data-qa-selector': 'web_ide_button',
+ },
+};
+const ACTION_WEB_IDE_FORK = {
+ ...ACTION_WEB_IDE,
+ href: '#modal-confirm-fork',
+ handle: expect.any(Function),
+};
+const ACTION_GITPOD = {
+ href: TEST_GITPOD_URL,
+ key: 'gitpod',
+ secondaryText: 'Launch a ready-to-code development environment for your project.',
+ tooltip: 'Launch a ready-to-code development environment for your project.',
+ text: 'Gitpod',
+ attrs: {
+ 'data-qa-selector': 'gitpod_button',
+ },
+};
+const ACTION_GITPOD_ENABLE = {
+ ...ACTION_GITPOD,
+ href: '#modal-enable-gitpod',
+ handle: expect.any(Function),
+};
+
+describe('Web IDE link component', () => {
+ let wrapper;
+
+ function createComponent(props) {
+ wrapper = shallowMount(WebIdeLink, {
+ propsData: {
+ webIdeUrl: TEST_WEB_IDE_URL,
+ gitpodUrl: TEST_GITPOD_URL,
+ ...props,
+ },
+ });
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findActionsButton = () => wrapper.find(ActionsButton);
+ const findLocalStorageSync = () => wrapper.find(LocalStorageSync);
+
+ it.each`
+ props | expectedActions
+ ${{}} | ${[ACTION_WEB_IDE]}
+ ${{ needsToFork: true }} | ${[ACTION_WEB_IDE_FORK]}
+ ${{ showWebIdeButton: false, showGitpodButton: true, gitpodEnabled: true }} | ${[ACTION_GITPOD]}
+ ${{ showWebIdeButton: false, showGitpodButton: true, gitpodEnabled: false }} | ${[ACTION_GITPOD_ENABLE]}
+ ${{ showGitpodButton: true, gitpodEnabled: false }} | ${[ACTION_WEB_IDE, ACTION_GITPOD_ENABLE]}
+ `('renders actions with props=$props', ({ props, expectedActions }) => {
+ createComponent(props);
+
+ expect(findActionsButton().props('actions')).toEqual(expectedActions);
+ });
+
+ describe('with multiple actions', () => {
+ beforeEach(() => {
+ createComponent({ showWebIdeButton: true, showGitpodButton: true, gitpodEnabled: true });
+ });
+
+ it('selected Web IDE by default', () => {
+ expect(findActionsButton().props()).toMatchObject({
+ actions: [ACTION_WEB_IDE, ACTION_GITPOD],
+ selectedKey: ACTION_WEB_IDE.key,
+ });
+ });
+
+ it('should set selection with local storage value', async () => {
+ expect(findActionsButton().props('selectedKey')).toBe(ACTION_WEB_IDE.key);
+
+ findLocalStorageSync().vm.$emit('input', ACTION_GITPOD.key);
+
+ await wrapper.vm.$nextTick();
+
+ expect(findActionsButton().props('selectedKey')).toBe(ACTION_GITPOD.key);
+ });
+
+ it('should update local storage when selection changes', async () => {
+ expect(findLocalStorageSync().props('value')).toBe(ACTION_WEB_IDE.key);
+
+ findActionsButton().vm.$emit('select', ACTION_GITPOD.key);
+
+ await wrapper.vm.$nextTick();
+
+ expect(findActionsButton().props('selectedKey')).toBe(ACTION_GITPOD.key);
+ expect(findLocalStorageSync().props('value')).toBe(ACTION_GITPOD.key);
+ });
+ });
+});