summaryrefslogtreecommitdiff
path: root/spec/frontend/vue_shared
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/vue_shared')
-rw-r--r--spec/frontend/vue_shared/components/color_picker/color_picker_spec.js41
-rw-r--r--spec/frontend/vue_shared/components/confidentiality_badge_spec.js52
-rw-r--r--spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js15
-rw-r--r--spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/deployment_instance/deployment_instance_spec.js25
-rw-r--r--spec/frontend/vue_shared/components/dropdown/dropdown_hidden_input_spec.js36
-rw-r--r--spec/frontend/vue_shared/components/dropdown/dropdown_search_input_spec.js51
-rw-r--r--spec/frontend/vue_shared/components/file_finder/index_spec.js27
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js51
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/gitlab_version_check_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/markdown/field_spec.js31
-rw-r--r--spec/frontend/vue_shared/components/markdown/header_spec.js22
-rw-r--r--spec/frontend/vue_shared/components/markdown/toolbar_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/metric_images/__snapshots__/metric_images_table_spec.js.snap2
-rw-r--r--spec/frontend/vue_shared/components/registry/list_item_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/runner_aws_deployments/__snapshots__/runner_aws_deployments_modal_spec.js.snap1
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js34
-rw-r--r--spec/frontend/vue_shared/components/segmented_control_button_group_spec.js104
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/usage_quotas/usage_banner_spec.js62
-rw-r--r--spec/frontend/vue_shared/components/user_popover/user_popover_spec.js140
-rw-r--r--spec/frontend/vue_shared/directives/autofocusonshow_spec.js7
-rw-r--r--spec/frontend/vue_shared/issuable/list/components/issuable_bulk_edit_sidebar_spec.js4
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js1
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js77
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js7
-rw-r--r--spec/frontend/vue_shared/issuable/show/mock_data.js5
-rw-r--r--spec/frontend/vue_shared/issuable/sidebar/components/issuable_sidebar_root_spec.js6
-rw-r--r--spec/frontend/vue_shared/security_configuration/components/section_layout_spec.js58
-rw-r--r--spec/frontend/vue_shared/security_reports/mock_data.js6
37 files changed, 715 insertions, 199 deletions
diff --git a/spec/frontend/vue_shared/components/color_picker/color_picker_spec.js b/spec/frontend/vue_shared/components/color_picker/color_picker_spec.js
index 28b3bf5287a..8cbe0630426 100644
--- a/spec/frontend/vue_shared/components/color_picker/color_picker_spec.js
+++ b/spec/frontend/vue_shared/components/color_picker/color_picker_spec.js
@@ -3,6 +3,8 @@ import { mount, shallowMount } from '@vue/test-utils';
import ColorPicker from '~/vue_shared/components/color_picker/color_picker.vue';
+jest.mock('lodash/uniqueId', () => (prefix) => (prefix ? `${prefix}1` : 1));
+
describe('ColorPicker', () => {
let wrapper;
@@ -14,10 +16,11 @@ describe('ColorPicker', () => {
const setColor = '#000000';
const invalidText = 'Please enter a valid hex (#RRGGBB or #RGB) color value';
- const label = () => wrapper.find(GlFormGroup).attributes('label');
+ const findGlFormGroup = () => wrapper.find(GlFormGroup);
const colorPreview = () => wrapper.find('[data-testid="color-preview"]');
const colorPicker = () => wrapper.find(GlFormInput);
- const colorInput = () => wrapper.find(GlFormInputGroup).find('input[type="text"]');
+ const colorInput = () => wrapper.find('input[type="color"]');
+ const colorTextInput = () => wrapper.find(GlFormInputGroup).find('input[type="text"]');
const invalidFeedback = () => wrapper.find('.invalid-feedback');
const description = () => wrapper.find(GlFormGroup).attributes('description');
const presetColors = () => wrapper.findAll(GlLink);
@@ -39,13 +42,29 @@ describe('ColorPicker', () => {
it('hides the label if the label is not passed', () => {
createComponent(shallowMount);
- expect(label()).toBe('');
+ expect(findGlFormGroup().attributes('label')).toBe('');
});
it('shows the label if the label is passed', () => {
createComponent(shallowMount, { label: 'test' });
- expect(label()).toBe('test');
+ expect(findGlFormGroup().attributes('label')).toBe('test');
+ });
+
+ describe.each`
+ desc | id
+ ${'with prop id'} | ${'test-id'}
+ ${'without prop id'} | ${undefined}
+ `('$desc', ({ id }) => {
+ beforeEach(() => {
+ createComponent(mount, { id, label: 'test' });
+ });
+
+ it('renders the same `ID` for input and `for` for label', () => {
+ expect(findGlFormGroup().find('label').attributes('for')).toBe(
+ colorInput().attributes('id'),
+ );
+ });
});
});
@@ -55,30 +74,30 @@ describe('ColorPicker', () => {
expect(colorPreview().attributes('style')).toBe(undefined);
expect(colorPicker().attributes('value')).toBe(undefined);
- expect(colorInput().props('value')).toBe('');
+ expect(colorTextInput().props('value')).toBe('');
expect(colorPreview().attributes('class')).toContain('gl-inset-border-1-gray-400');
});
it('has a color set on initialization', () => {
createComponent(mount, { value: setColor });
- expect(colorInput().props('value')).toBe(setColor);
+ expect(colorTextInput().props('value')).toBe(setColor);
});
it('emits input event from component when a color is selected', async () => {
createComponent();
- await colorInput().setValue(setColor);
+ await colorTextInput().setValue(setColor);
expect(wrapper.emitted().input[0]).toStrictEqual([setColor]);
});
it('trims spaces from submitted colors', async () => {
createComponent();
- await colorInput().setValue(` ${setColor} `);
+ await colorTextInput().setValue(` ${setColor} `);
expect(wrapper.emitted().input[0]).toStrictEqual([setColor]);
expect(colorPreview().attributes('class')).toContain('gl-inset-border-1-gray-400');
- expect(colorInput().attributes('class')).not.toContain('is-invalid');
+ expect(colorTextInput().attributes('class')).not.toContain('is-invalid');
});
it('shows invalid feedback when the state is marked as invalid', async () => {
@@ -86,14 +105,14 @@ describe('ColorPicker', () => {
expect(invalidFeedback().text()).toBe(invalidText);
expect(colorPreview().attributes('class')).toContain('gl-inset-border-1-red-500');
- expect(colorInput().attributes('class')).toContain('is-invalid');
+ expect(colorTextInput().attributes('class')).toContain('is-invalid');
});
});
describe('inputs', () => {
it('has color input value entered', async () => {
createComponent();
- await colorInput().setValue(setColor);
+ await colorTextInput().setValue(setColor);
expect(wrapper.emitted().input[0]).toStrictEqual([setColor]);
});
diff --git a/spec/frontend/vue_shared/components/confidentiality_badge_spec.js b/spec/frontend/vue_shared/components/confidentiality_badge_spec.js
new file mode 100644
index 00000000000..9d11fbbaf55
--- /dev/null
+++ b/spec/frontend/vue_shared/components/confidentiality_badge_spec.js
@@ -0,0 +1,52 @@
+import { GlBadge } from '@gitlab/ui';
+
+import { shallowMount } from '@vue/test-utils';
+import { WorkspaceType, IssuableType } from '~/issues/constants';
+
+import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
+
+const createComponent = ({
+ workspaceType = WorkspaceType.project,
+ issuableType = IssuableType.Issue,
+} = {}) =>
+ shallowMount(ConfidentialityBadge, {
+ propsData: {
+ workspaceType,
+ issuableType,
+ },
+ });
+
+describe('ConfidentialityBadge', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it.each`
+ workspaceType | issuableType | expectedTooltip
+ ${WorkspaceType.project} | ${IssuableType.Issue} | ${'Only project members with at least Reporter role can view or be notified about this issue.'}
+ ${WorkspaceType.group} | ${IssuableType.Epic} | ${'Only group members with at least Reporter role can view or be notified about this epic.'}
+ `(
+ 'should render gl-badge with correct tooltip when workspaceType is $workspaceType and issuableType is $issuableType',
+ ({ workspaceType, issuableType, expectedTooltip }) => {
+ wrapper = createComponent({
+ workspaceType,
+ issuableType,
+ });
+
+ const badgeEl = wrapper.findComponent(GlBadge);
+
+ expect(badgeEl.props()).toMatchObject({
+ icon: 'eye-slash',
+ variant: 'warning',
+ });
+ expect(badgeEl.attributes('title')).toBe(expectedTooltip);
+ expect(badgeEl.text()).toBe('Confidential');
+ },
+ );
+});
diff --git a/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js b/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js
index f75694bd504..a660643d74f 100644
--- a/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js
+++ b/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js
@@ -3,6 +3,7 @@ import {
CONFIRM_DANGER_WARNING,
CONFIRM_DANGER_MODAL_BUTTON,
CONFIRM_DANGER_MODAL_ID,
+ CONFIRM_DANGER_MODAL_CANCEL,
} from '~/vue_shared/components/confirm_danger/constants';
import ConfirmDangerModal from '~/vue_shared/components/confirm_danger/confirm_danger_modal.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
@@ -10,6 +11,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
describe('Confirm Danger Modal', () => {
const confirmDangerMessage = 'This is a dangerous activity';
const confirmButtonText = 'Confirm button text';
+ const cancelButtonText = 'Cancel button text';
const phrase = 'You must construct additional pylons';
const modalId = CONFIRM_DANGER_MODAL_ID;
@@ -21,6 +23,7 @@ describe('Confirm Danger Modal', () => {
const findDefaultWarning = () => wrapper.findByTestId('confirm-danger-warning');
const findAdditionalMessage = () => wrapper.findByTestId('confirm-danger-message');
const findPrimaryAction = () => findModal().props('actionPrimary');
+ const findCancelAction = () => findModal().props('actionCancel');
const findPrimaryActionAttributes = (attr) => findPrimaryAction().attributes[0][attr];
const createComponent = ({ provide = {} } = {}) =>
@@ -34,7 +37,9 @@ describe('Confirm Danger Modal', () => {
});
beforeEach(() => {
- wrapper = createComponent({ provide: { confirmDangerMessage, confirmButtonText } });
+ wrapper = createComponent({
+ provide: { confirmDangerMessage, confirmButtonText, cancelButtonText },
+ });
});
afterEach(() => {
@@ -54,6 +59,10 @@ describe('Confirm Danger Modal', () => {
expect(findPrimaryActionAttributes('variant')).toBe('danger');
});
+ it('renders the cancel button', () => {
+ expect(findCancelAction().text).toBe(cancelButtonText);
+ });
+
it('renders the correct confirmation phrase', () => {
expect(findConfirmationPhrase().text()).toBe(
`Please type ${phrase} to proceed or close this modal to cancel.`,
@@ -72,6 +81,10 @@ describe('Confirm Danger Modal', () => {
it('renders the default confirm button', () => {
expect(findPrimaryAction().text).toBe(CONFIRM_DANGER_MODAL_BUTTON);
});
+
+ it('renders the default cancel button', () => {
+ expect(findCancelAction().text).toBe(CONFIRM_DANGER_MODAL_CANCEL);
+ });
});
describe('with a valid confirmation phrase', () => {
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 d4b6b987c69..aa41df438d2 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
@@ -15,7 +15,7 @@ describe('DateTimePicker', () => {
const dropdownToggle = () => wrapper.find('.dropdown-toggle');
const dropdownMenu = () => wrapper.find('.dropdown-menu');
const cancelButton = () => wrapper.find('[data-testid="cancelButton"]');
- const applyButtonElement = () => wrapper.find('button.btn-success').element;
+ const applyButtonElement = () => wrapper.find('button.btn-confirm').element;
const findQuickRangeItems = () => wrapper.findAll('.dropdown-item');
const createComponent = (props) => {
diff --git a/spec/frontend/vue_shared/components/deployment_instance/deployment_instance_spec.js b/spec/frontend/vue_shared/components/deployment_instance/deployment_instance_spec.js
index 59653a0ec13..e3d8bfd22ca 100644
--- a/spec/frontend/vue_shared/components/deployment_instance/deployment_instance_spec.js
+++ b/spec/frontend/vue_shared/components/deployment_instance/deployment_instance_spec.js
@@ -6,12 +6,16 @@ import { folder } from './mock_data';
describe('Deploy Board Instance', () => {
let wrapper;
- const createComponent = (props = {}) =>
+ const createComponent = (props = {}, provide) =>
shallowMount(DeployBoardInstance, {
propsData: {
status: 'succeeded',
...props,
},
+ provide: {
+ glFeatures: { monitorLogging: true },
+ ...provide,
+ },
});
describe('as a non-canary deployment', () => {
@@ -95,4 +99,23 @@ describe('Deploy Board Instance', () => {
expect(wrapper.attributes('title')).toEqual('');
});
});
+
+ describe(':monitor_logging feature flag', () => {
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it.each`
+ flagState | logsState | expected
+ ${true} | ${'shows'} | ${'/root/review-app/-/logs?environment_name=foo&pod_name=tanuki-1'}
+ ${false} | ${'hides'} | ${undefined}
+ `('$logsState log link when flag state is $flagState', async ({ flagState, expected }) => {
+ wrapper = createComponent(
+ { logsPath: folder.logs_path, podName: 'tanuki-1' },
+ { glFeatures: { monitorLogging: flagState } },
+ );
+
+ expect(wrapper.attributes('href')).toEqual(expected);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/dropdown/dropdown_hidden_input_spec.js b/spec/frontend/vue_shared/components/dropdown/dropdown_hidden_input_spec.js
deleted file mode 100644
index 30b8e869aab..00000000000
--- a/spec/frontend/vue_shared/components/dropdown/dropdown_hidden_input_spec.js
+++ /dev/null
@@ -1,36 +0,0 @@
-import Vue from 'vue';
-
-import mountComponent from 'helpers/vue_mount_component_helper';
-import dropdownHiddenInputComponent from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
-
-import { mockLabels } from './mock_data';
-
-const createComponent = (name = 'label_id[]', value = mockLabels[0].id) => {
- const Component = Vue.extend(dropdownHiddenInputComponent);
-
- return mountComponent(Component, {
- name,
- value,
- });
-};
-
-describe('DropdownHiddenInputComponent', () => {
- let vm;
-
- beforeEach(() => {
- vm = createComponent();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('template', () => {
- it('renders input element of type `hidden`', () => {
- expect(vm.$el.nodeName).toBe('INPUT');
- expect(vm.$el.getAttribute('type')).toBe('hidden');
- expect(vm.$el.getAttribute('name')).toBe(vm.name);
- expect(vm.$el.getAttribute('value')).toBe(`${vm.value}`);
- });
- });
-});
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
deleted file mode 100644
index b32dbeb8852..00000000000
--- a/spec/frontend/vue_shared/components/dropdown/dropdown_search_input_spec.js
+++ /dev/null
@@ -1,51 +0,0 @@
-import { mount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import DropdownSearchInputComponent from '~/vue_shared/components/dropdown/dropdown_search_input.vue';
-
-describe('DropdownSearchInputComponent', () => {
- let wrapper;
-
- const defaultProps = {
- placeholderText: 'Search something',
- };
- const buildVM = (propsData = defaultProps) => {
- wrapper = mount(DropdownSearchInputComponent, {
- propsData,
- });
- };
- const findInputEl = () => wrapper.find('.dropdown-input-field');
-
- beforeEach(() => {
- buildVM();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('template', () => {
- it('renders input element with type `search`', () => {
- expect(findInputEl().exists()).toBe(true);
- expect(findInputEl().attributes('type')).toBe('search');
- });
-
- it('renders search icon element', () => {
- expect(wrapper.find('.dropdown-input-search[data-testid="search-icon"]').exists()).toBe(true);
- });
-
- it('displays custom placeholder text', () => {
- expect(findInputEl().attributes('placeholder')).toBe(defaultProps.placeholderText);
- });
-
- it('focuses input element when focused property equals true', async () => {
- const inputEl = findInputEl().element;
-
- jest.spyOn(inputEl, 'focus');
-
- wrapper.setProps({ focused: true });
-
- await nextTick();
- expect(inputEl.focus).toHaveBeenCalled();
- });
- });
-});
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 921091c5b84..5cf891a2e52 100644
--- a/spec/frontend/vue_shared/components/file_finder/index_spec.js
+++ b/spec/frontend/vue_shared/components/file_finder/index_spec.js
@@ -1,5 +1,6 @@
import Mousetrap from 'mousetrap';
import Vue, { nextTick } from 'vue';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { file } from 'jest/ide/helpers';
import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
import FindFileComponent from '~/vue_shared/components/file_finder/index.vue';
@@ -22,7 +23,11 @@ describe('File finder item spec', () => {
}
beforeEach(() => {
- setFixtures('<div id="app"></div>');
+ setHTMLFixture('<div id="app"></div>');
+ });
+
+ afterEach(() => {
+ resetHTMLFixture();
});
afterEach(() => {
@@ -105,18 +110,6 @@ describe('File finder item spec', () => {
});
});
- describe('listHeight', () => {
- it('returns 55 when entries exist', () => {
- expect(vm.listHeight).toBe(55);
- });
-
- it('returns 33 when entries dont exist', () => {
- vm.searchText = 'testing 123';
-
- expect(vm.listHeight).toBe(33);
- });
- });
-
describe('filteredBlobsLength', () => {
it('returns length of filtered blobs', () => {
vm.searchText = 'index';
@@ -253,11 +246,9 @@ describe('File finder item spec', () => {
describe('without entries', () => {
it('renders loading text when loading', () => {
- createComponent({
- loading: true,
- });
+ createComponent({ loading: true });
- expect(vm.$el.textContent).toContain('Loading...');
+ expect(vm.$el.querySelector('.gl-spinner')).not.toBe(null);
});
it('renders no files text', () => {
@@ -307,7 +298,7 @@ describe('File finder item spec', () => {
});
it('stops callback in monaco editor', () => {
- setFixtures('<div class="inputarea"></div>');
+ setHTMLFixture('<div class="inputarea"></div>');
expect(
Mousetrap.prototype.stopCallback(null, document.querySelector('.inputarea'), 't'),
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 b6a181e6a0b..e44bc8771f5 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
@@ -11,7 +11,10 @@ import { shallowMount, mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import RecentSearchesService from '~/filtered_search/services/recent_searches_service';
import RecentSearchesStore from '~/filtered_search/stores/recent_searches_store';
-import { SortDirection } from '~/vue_shared/components/filtered_search_bar/constants';
+import {
+ FILTERED_SEARCH_TERM,
+ SortDirection,
+} from '~/vue_shared/components/filtered_search_bar/constants';
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';
@@ -68,6 +71,10 @@ const createComponent = ({
describe('FilteredSearchBarRoot', () => {
let wrapper;
+ const findGlButton = () => wrapper.findComponent(GlButton);
+ const findGlDropdown = () => wrapper.findComponent(GlDropdown);
+ const findGlFilteredSearch = () => wrapper.findComponent(GlFilteredSearch);
+
beforeEach(() => {
wrapper = createComponent({ sortOptions: mockSortOptions });
});
@@ -79,7 +86,7 @@ describe('FilteredSearchBarRoot', () => {
describe('data', () => {
it('initializes `filterValue`, `selectedSortOption` and `selectedSortDirection` data props and displays the sort dropdown', () => {
expect(wrapper.vm.filterValue).toEqual([]);
- expect(wrapper.vm.selectedSortOption).toBe(mockSortOptions[0].sortDirection.descending);
+ expect(wrapper.vm.selectedSortOption).toBe(mockSortOptions[0]);
expect(wrapper.vm.selectedSortDirection).toBe(SortDirection.descending);
expect(wrapper.find(GlButtonGroup).exists()).toBe(true);
expect(wrapper.find(GlButton).exists()).toBe(true);
@@ -225,9 +232,7 @@ describe('FilteredSearchBarRoot', () => {
});
it('initializes `recentSearchesPromise` prop with a promise by using `recentSearchesService.fetch()`', () => {
- jest
- .spyOn(wrapper.vm.recentSearchesService, 'fetch')
- .mockReturnValue(new Promise(() => []));
+ jest.spyOn(wrapper.vm.recentSearchesService, 'fetch').mockResolvedValue([]);
wrapper.vm.setupRecentSearch();
@@ -489,4 +494,40 @@ describe('FilteredSearchBarRoot', () => {
expect(sortButtonEl.props('icon')).toBe('sort-highest');
});
});
+
+ describe('watchers', () => {
+ const tokenValue = {
+ id: 'id-1',
+ type: FILTERED_SEARCH_TERM,
+ value: { data: '' },
+ };
+
+ it('syncs filter value', async () => {
+ await wrapper.setProps({ initialFilterValue: [tokenValue], syncFilterAndSort: true });
+
+ expect(findGlFilteredSearch().props('value')).toEqual([tokenValue]);
+ });
+
+ it('does not sync filter value when syncFilterAndSort=false', async () => {
+ await wrapper.setProps({ initialFilterValue: [tokenValue], syncFilterAndSort: false });
+
+ expect(findGlFilteredSearch().props('value')).toEqual([]);
+ });
+
+ it('syncs sort values', async () => {
+ await wrapper.setProps({ initialSortBy: 'updated_asc', syncFilterAndSort: true });
+
+ expect(findGlDropdown().props('text')).toBe('Last updated');
+ expect(findGlButton().props('icon')).toBe('sort-lowest');
+ expect(findGlButton().attributes('aria-label')).toBe('Sort direction: Ascending');
+ });
+
+ it('does not sync sort values when syncFilterAndSort=false', async () => {
+ await wrapper.setProps({ initialSortBy: 'updated_asc', syncFilterAndSort: false });
+
+ expect(findGlDropdown().props('text')).toBe('Created date');
+ expect(findGlButton().props('icon')).toBe('sort-highest');
+ expect(findGlButton().attributes('aria-label')).toBe('Sort direction: Descending');
+ });
+ });
});
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 87066b70023..3f24d5df858 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
@@ -51,6 +51,7 @@ function createComponent(options = {}) {
config,
value,
active,
+ cursorPosition: 'start',
},
provide: {
portalName: 'fake target',
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js
index af8a2a496ea..ca8cd419d87 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js
@@ -78,6 +78,7 @@ const mockProps = {
suggestionsLoading: false,
defaultSuggestions: DEFAULT_NONE_ANY,
getActiveTokenValue: (labels, data) => labels.find((label) => label.title === data),
+ cursorPosition: 'start',
};
function createComponent({
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js
index 7a7db434052..7b495ec9bee 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js
@@ -39,6 +39,7 @@ function createComponent(options = {}) {
config,
value,
active,
+ cursorPosition: 'start',
},
provide: {
portalName: 'fake target',
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js
index b163563cea4..dcb0d095b1b 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js
@@ -45,6 +45,7 @@ function createComponent(options = {}) {
config,
value,
active,
+ cursorPosition: 'start',
},
provide: {
portalName: 'fake target',
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 52df27c2d00..f03a2e7934f 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
@@ -45,6 +45,7 @@ function createComponent(options = {}) {
config,
value,
active,
+ cursorPosition: 'start',
},
provide: {
portalName: 'fake target',
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 de9ec863dd5..7c545f76c0b 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
@@ -42,6 +42,7 @@ function createComponent(options = {}) {
config,
value,
active,
+ cursorPosition: 'start',
},
provide: {
portalName: 'fake target',
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js
index 8be21b35414..4bbbaab9b7a 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js
@@ -18,6 +18,7 @@ describe('ReleaseToken', () => {
active: false,
config,
value,
+ cursorPosition: 'start',
},
provide: {
portalName: 'fake target',
diff --git a/spec/frontend/vue_shared/components/gitlab_version_check_spec.js b/spec/frontend/vue_shared/components/gitlab_version_check_spec.js
index b673e5407d4..b180e8c12dd 100644
--- a/spec/frontend/vue_shared/components/gitlab_version_check_spec.js
+++ b/spec/frontend/vue_shared/components/gitlab_version_check_spec.js
@@ -1,7 +1,7 @@
import { GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
-import flushPromises from 'helpers/flush_promises';
+import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import GitlabVersionCheck from '~/vue_shared/components/gitlab_version_check.vue';
@@ -43,7 +43,7 @@ describe('GitlabVersionCheck', () => {
describe(`is ${description}`, () => {
beforeEach(async () => {
createComponent(mockResponse);
- await flushPromises(); // Ensure we wrap up the axios call
+ await waitForPromises(); // Ensure we wrap up the axios call
});
it(`does${renders ? '' : ' not'} render GlBadge`, () => {
@@ -61,7 +61,7 @@ describe('GitlabVersionCheck', () => {
describe(`when response is ${mockResponse.res.severity}`, () => {
beforeEach(async () => {
createComponent(mockResponse);
- await flushPromises(); // Ensure we wrap up the axios call
+ await waitForPromises(); // Ensure we wrap up the axios call
});
it(`title is ${expectedUI.title}`, () => {
diff --git a/spec/frontend/vue_shared/components/markdown/field_spec.js b/spec/frontend/vue_shared/components/markdown/field_spec.js
index d1c4d777d44..b3376f26a25 100644
--- a/spec/frontend/vue_shared/components/markdown/field_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/field_spec.js
@@ -5,12 +5,14 @@ import { TEST_HOST, FIXTURES_PATH } from 'spec/test_constants';
import axios from '~/lib/utils/axios_utils';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import MarkdownFieldHeader from '~/vue_shared/components/markdown/header.vue';
+import MarkdownToolbar from '~/vue_shared/components/markdown/toolbar.vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
const markdownPreviewPath = `${TEST_HOST}/preview`;
const markdownDocsPath = `${TEST_HOST}/docs`;
const textareaValue = 'testing\n123';
const uploadsPath = 'test/uploads';
+const restrictedToolBarItems = ['quote'];
function assertMarkdownTabs(isWrite, writeLink, previewLink, wrapper) {
expect(writeLink.element.children[0].classList.contains('active')).toBe(isWrite);
@@ -63,6 +65,7 @@ describe('Markdown field component', () => {
textareaValue,
lines,
enablePreview,
+ restrictedToolBarItems,
},
provide: {
glFeatures: {
@@ -81,6 +84,8 @@ describe('Markdown field component', () => {
const getAttachButton = () => subject.find('.button-attach-file');
const clickAttachButton = () => getAttachButton().trigger('click');
const findDropzone = () => subject.find('.div-dropzone');
+ const findMarkdownHeader = () => subject.findComponent(MarkdownFieldHeader);
+ const findMarkdownToolbar = () => subject.findComponent(MarkdownToolbar);
describe('mounted', () => {
const previewHTML = `
@@ -184,9 +189,23 @@ describe('Markdown field component', () => {
assertMarkdownTabs(false, writeLink, previewLink, subject);
});
+
+ it('passes correct props to MarkdownToolbar', () => {
+ expect(findMarkdownToolbar().props()).toEqual({
+ canAttachFile: true,
+ markdownDocsPath,
+ quickActionsDocsPath: '',
+ showCommentToolBar: true,
+ });
+ });
});
describe('markdown buttons', () => {
+ beforeEach(() => {
+ // needed for the underlying insertText to work
+ document.execCommand = jest.fn(() => false);
+ });
+
it('converts single words', async () => {
const textarea = subject.find('textarea').element;
textarea.setSelectionRange(0, 7);
@@ -309,9 +328,7 @@ describe('Markdown field component', () => {
it('escapes new line characters', () => {
createSubject({ lines: [{ rich_text: 'hello world\\n' }] });
- expect(subject.find('[data-testid="markdownHeader"]').props('lineContent')).toBe(
- 'hello world%br',
- );
+ expect(findMarkdownHeader().props('lineContent')).toBe('hello world%br');
});
});
@@ -325,4 +342,12 @@ describe('Markdown field component', () => {
expect(subject.findComponent(MarkdownFieldHeader).props('enablePreview')).toBe(true);
});
+
+ it('passess restricted tool bar items', () => {
+ createSubject();
+
+ expect(subject.findComponent(MarkdownFieldHeader).props('restrictedToolBarItems')).toBe(
+ restrictedToolBarItems,
+ );
+ });
});
diff --git a/spec/frontend/vue_shared/components/markdown/header_spec.js b/spec/frontend/vue_shared/components/markdown/header_spec.js
index fa4ca63f910..67222cab247 100644
--- a/spec/frontend/vue_shared/components/markdown/header_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/header_spec.js
@@ -166,4 +166,26 @@ describe('Markdown field header component', () => {
expect(wrapper.findByTestId('preview-tab').exists()).toBe(false);
});
+
+ describe('restricted tool bar items', () => {
+ let defaultCount;
+
+ beforeEach(() => {
+ defaultCount = findToolbarButtons().length;
+ });
+
+ it('restricts items as per input', () => {
+ createWrapper({
+ restrictedToolBarItems: ['quote'],
+ });
+
+ expect(findToolbarButtons().length).toBe(defaultCount - 1);
+ });
+
+ it('shows all items by default', () => {
+ createWrapper();
+
+ expect(findToolbarButtons().length).toBe(defaultCount);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/markdown/toolbar_spec.js b/spec/frontend/vue_shared/components/markdown/toolbar_spec.js
index 8bff85b0bda..f698794b951 100644
--- a/spec/frontend/vue_shared/components/markdown/toolbar_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/toolbar_spec.js
@@ -33,4 +33,18 @@ describe('toolbar', () => {
expect(wrapper.vm.$el.querySelector('.uploading-container')).toBeNull();
});
});
+
+ describe('comment tool bar settings', () => {
+ it('does not show comment tool bar div', () => {
+ createMountedWrapper({ showCommentToolBar: false });
+
+ expect(wrapper.find('.comment-toolbar').exists()).toBe(false);
+ });
+
+ it('shows comment tool bar by default', () => {
+ createMountedWrapper();
+
+ expect(wrapper.find('.comment-toolbar').exists()).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/metric_images/__snapshots__/metric_images_table_spec.js.snap b/spec/frontend/vue_shared/components/metric_images/__snapshots__/metric_images_table_spec.js.snap
index 5dd12d9edf5..015049795a1 100644
--- a/spec/frontend/vue_shared/components/metric_images/__snapshots__/metric_images_table_spec.js.snap
+++ b/spec/frontend/vue_shared/components/metric_images/__snapshots__/metric_images_table_spec.js.snap
@@ -10,6 +10,7 @@ exports[`Metrics upload item render the metrics image component 1`] = `
<gl-modal-stub
actioncancel="[object Object]"
actionprimary="[object Object]"
+ arialabel=""
body-class="gl-pb-0! gl-min-h-6!"
dismisslabel="Close"
modalclass=""
@@ -26,6 +27,7 @@ exports[`Metrics upload item render the metrics image component 1`] = `
<gl-modal-stub
actioncancel="[object Object]"
actionprimary="[object Object]"
+ arialabel=""
data-testid="metric-image-edit-modal"
dismisslabel="Close"
modalclass=""
diff --git a/spec/frontend/vue_shared/components/registry/list_item_spec.js b/spec/frontend/vue_shared/components/registry/list_item_spec.js
index 1b93292e37b..6e9abb2bfb3 100644
--- a/spec/frontend/vue_shared/components/registry/list_item_spec.js
+++ b/spec/frontend/vue_shared/components/registry/list_item_spec.js
@@ -101,20 +101,6 @@ describe('list item', () => {
});
});
- describe('disabled prop', () => {
- it('when true applies gl-opacity-5 class', () => {
- mountComponent({ disabled: true });
-
- expect(wrapper.classes('gl-opacity-5')).toBe(true);
- });
-
- it('when false does not apply gl-opacity-5 class', () => {
- mountComponent({ disabled: false });
-
- expect(wrapper.classes('gl-opacity-5')).toBe(false);
- });
- });
-
describe('borders and selection', () => {
it.each`
first | selected | shouldHave | shouldNotHave
diff --git a/spec/frontend/vue_shared/components/runner_aws_deployments/__snapshots__/runner_aws_deployments_modal_spec.js.snap b/spec/frontend/vue_shared/components/runner_aws_deployments/__snapshots__/runner_aws_deployments_modal_spec.js.snap
index ac313e556fc..8ff49271eb5 100644
--- a/spec/frontend/vue_shared/components/runner_aws_deployments/__snapshots__/runner_aws_deployments_modal_spec.js.snap
+++ b/spec/frontend/vue_shared/components/runner_aws_deployments/__snapshots__/runner_aws_deployments_modal_spec.js.snap
@@ -4,6 +4,7 @@ exports[`RunnerAwsDeploymentsModal renders the modal 1`] = `
<gl-modal-stub
actionprimary="[object Object]"
actionsecondary="[object Object]"
+ arialabel=""
dismisslabel="Close"
modalclass=""
modalid="runner-aws-deployments-modal"
diff --git a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js
index 0da9939e97f..001b6ee4a6f 100644
--- a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js
+++ b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js
@@ -45,8 +45,10 @@ describe('RunnerInstructionsModal component', () => {
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
const findGlLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findAlert = () => wrapper.findComponent(GlAlert);
+ const findModal = () => wrapper.findComponent(GlModal);
const findPlatformButtonGroup = () => wrapper.findByTestId('platform-buttons');
const findPlatformButtons = () => findPlatformButtonGroup().findAllComponents(GlButton);
+ const findOsxPlatformButton = () => wrapper.find({ ref: 'osx' });
const findArchitectureDropdownItems = () => wrapper.findAllByTestId('architecture-dropdown-item');
const findBinaryInstructions = () => wrapper.findByTestId('binary-instructions');
const findRegisterCommand = () => wrapper.findByTestId('register-command');
@@ -140,6 +142,38 @@ describe('RunnerInstructionsModal component', () => {
expect(instructions).toBe(registerInstructions);
});
});
+
+ describe('when the modal is shown', () => {
+ it('sets the focus on the selected platform', () => {
+ findPlatformButtons().at(0).element.focus = jest.fn();
+
+ findModal().vm.$emit('shown');
+
+ expect(findPlatformButtons().at(0).element.focus).toHaveBeenCalled();
+ });
+ });
+
+ describe('when providing a defaultPlatformName', () => {
+ beforeEach(async () => {
+ createComponent({ props: { defaultPlatformName: 'osx' } });
+ await waitForPromises();
+ });
+
+ it('runner instructions for the default selected platform are requested', () => {
+ expect(runnerSetupInstructionsHandler).toHaveBeenCalledWith({
+ platform: 'osx',
+ architecture: 'amd64',
+ });
+ });
+
+ it('sets the focus on the default selected platform', () => {
+ findOsxPlatformButton().element.focus = jest.fn();
+
+ findModal().vm.$emit('shown');
+
+ expect(findOsxPlatformButton().element.focus).toHaveBeenCalled();
+ });
+ });
});
describe('after a platform and architecture are selected', () => {
diff --git a/spec/frontend/vue_shared/components/segmented_control_button_group_spec.js b/spec/frontend/vue_shared/components/segmented_control_button_group_spec.js
new file mode 100644
index 00000000000..88445b6684c
--- /dev/null
+++ b/spec/frontend/vue_shared/components/segmented_control_button_group_spec.js
@@ -0,0 +1,104 @@
+import { GlButtonGroup, GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import SegmentedControlButtonGroup from '~/vue_shared/components/segmented_control_button_group.vue';
+
+const DEFAULT_OPTIONS = [
+ { text: 'Lorem', value: 'abc' },
+ { text: 'Ipsum', value: 'def' },
+ { text: 'Foo', value: 'x', disabled: true },
+ { text: 'Dolar', value: 'ghi' },
+];
+
+describe('~/vue_shared/components/segmented_control_button_group.vue', () => {
+ let wrapper;
+
+ const createComponent = (props = {}, scopedSlots = {}) => {
+ wrapper = shallowMount(SegmentedControlButtonGroup, {
+ propsData: {
+ value: DEFAULT_OPTIONS[0].value,
+ options: DEFAULT_OPTIONS,
+ ...props,
+ },
+ scopedSlots,
+ });
+ };
+
+ const findButtonGroup = () => wrapper.findComponent(GlButtonGroup);
+ const findButtons = () => findButtonGroup().findAllComponents(GlButton);
+ const findButtonsData = () =>
+ findButtons().wrappers.map((x) => ({
+ selected: x.props('selected'),
+ text: x.text(),
+ disabled: x.props('disabled'),
+ }));
+ const findButtonWithText = (text) => findButtons().wrappers.find((x) => x.text() === text);
+
+ const optionsAsButtonData = (options) =>
+ options.map(({ text, disabled = false }) => ({
+ selected: false,
+ text,
+ disabled,
+ }));
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('default', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders button group', () => {
+ expect(findButtonGroup().exists()).toBe(true);
+ });
+
+ it('renders buttons', () => {
+ const expectation = optionsAsButtonData(DEFAULT_OPTIONS);
+ expectation[0].selected = true;
+
+ expect(findButtonsData()).toEqual(expectation);
+ });
+
+ describe.each(DEFAULT_OPTIONS.filter((x) => !x.disabled))(
+ 'when button clicked %p',
+ ({ text, value }) => {
+ it('emits input with value', () => {
+ expect(wrapper.emitted('input')).toBeUndefined();
+
+ findButtonWithText(text).vm.$emit('click');
+
+ expect(wrapper.emitted('input')).toEqual([[value]]);
+ });
+ },
+ );
+ });
+
+ const VALUE_TEST_CASES = [0, 1, 3].map((index) => [DEFAULT_OPTIONS[index].value, index]);
+
+ describe.each(VALUE_TEST_CASES)('with value=%s', (value, index) => {
+ it(`renders selected button at ${index}`, () => {
+ createComponent({ value });
+
+ const expectation = optionsAsButtonData(DEFAULT_OPTIONS);
+ expectation[index].selected = true;
+
+ expect(findButtonsData()).toEqual(expectation);
+ });
+ });
+
+ describe('with button-content slot', () => {
+ it('renders button content based on slot', () => {
+ createComponent(
+ {},
+ {
+ 'button-content': `<template #button-content="{ text }">In a slot - {{ text }}</template>`,
+ },
+ );
+
+ expect(findButtonsData().map((x) => x.text)).toEqual(
+ DEFAULT_OPTIONS.map((x) => `In a slot - ${x.text}`),
+ );
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js
index 3ceed670d77..9c29f304c71 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js
@@ -153,7 +153,11 @@ describe('DropdownContentsCreateView', () => {
});
it('enables a Create button', () => {
- expect(findCreateButton().props('disabled')).toBe(false);
+ expect(findCreateButton().props()).toMatchObject({
+ disabled: false,
+ category: 'primary',
+ variant: 'confirm',
+ });
});
it('renders a loader spinner after Create button click', async () => {
diff --git a/spec/frontend/vue_shared/components/usage_quotas/usage_banner_spec.js b/spec/frontend/vue_shared/components/usage_quotas/usage_banner_spec.js
new file mode 100644
index 00000000000..662c09d02bf
--- /dev/null
+++ b/spec/frontend/vue_shared/components/usage_quotas/usage_banner_spec.js
@@ -0,0 +1,62 @@
+import { GlSkeletonLoader } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import component from '~/vue_shared/components/usage_quotas/usage_banner.vue';
+
+describe('usage banner', () => {
+ let wrapper;
+
+ const findLeftPrimaryTextSlot = () => wrapper.findByTestId('left-primary-text');
+ const findLeftSecondaryTextSlot = () => wrapper.findByTestId('left-secondary-text');
+ const findRightPrimaryTextSlot = () => wrapper.findByTestId('right-primary-text');
+ const findRightSecondaryTextSlot = () => wrapper.findByTestId('right-secondary-text');
+ const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
+
+ const mountComponent = (propsData, slots) => {
+ wrapper = shallowMountExtended(component, {
+ propsData,
+ slots: {
+ 'left-primary-text': '<div data-testid="left-primary-text" />',
+ 'left-secondary-text': '<div data-testid="left-secondary-text" />',
+ 'right-primary-text': '<div data-testid="right-primary-text" />',
+ 'right-secondary-text': '<div data-testid="right-secondary-text" />',
+ ...slots,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe.each`
+ slotName | finderFunction
+ ${'left-primary-text'} | ${findLeftPrimaryTextSlot}
+ ${'left-secondary-text'} | ${findLeftSecondaryTextSlot}
+ ${'right-primary-text'} | ${findRightPrimaryTextSlot}
+ ${'right-secondary-text'} | ${findRightSecondaryTextSlot}
+ `('$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);
+ });
+ });
+
+ it('should show a skeleton loader component', () => {
+ mountComponent({ loading: true });
+
+ expect(findSkeletonLoader().exists()).toBe(true);
+ });
+
+ it('should not show a skeleton loader component', () => {
+ mountComponent();
+
+ expect(findSkeletonLoader().exists()).toBe(false);
+ });
+});
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 3329199a46b..a54f3450633 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,11 +1,22 @@
import { GlSkeletonLoader, GlIcon } from '@gitlab/ui';
+import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { AVAILABILITY_STATUS } from '~/set_status_modal/utils';
import UserNameWithStatus from '~/sidebar/components/assignees/user_name_with_status.vue';
import UserPopover from '~/vue_shared/components/user_popover/user_popover.vue';
+import axios from '~/lib/utils/axios_utils';
+import createFlash from '~/flash';
+import { followUser, unfollowUser } from '~/api/user_api';
+
+jest.mock('~/flash');
+jest.mock('~/api/user_api', () => ({
+ followUser: jest.fn(),
+ unfollowUser: jest.fn(),
+}));
const DEFAULT_PROPS = {
user: {
+ id: 1,
username: 'root',
name: 'Administrator',
location: 'Vienna',
@@ -15,6 +26,7 @@ const DEFAULT_PROPS = {
workInformation: null,
status: null,
pronouns: 'they/them',
+ isFollowed: false,
loaded: true,
},
};
@@ -25,11 +37,13 @@ describe('User Popover Component', () => {
let wrapper;
beforeEach(() => {
- loadFixtures(fixtureTemplate);
+ loadHTMLFixture(fixtureTemplate);
+ gon.features = {};
});
afterEach(() => {
wrapper.destroy();
+ resetHTMLFixture();
});
const findUserStatus = () => wrapper.findByTestId('user-popover-status');
@@ -37,15 +51,15 @@ describe('User Popover Component', () => {
const findUserName = () => wrapper.find(UserNameWithStatus);
const findSecurityBotDocsLink = () => wrapper.findByTestId('user-popover-bot-docs-link');
const findUserLocalTime = () => wrapper.findByTestId('user-popover-local-time');
+ const findToggleFollowButton = () => wrapper.findByTestId('toggle-follow-button');
- const createWrapper = (props = {}, options = {}) => {
+ const createWrapper = (props = {}) => {
wrapper = mountExtended(UserPopover, {
propsData: {
...DEFAULT_PROPS,
target: findTarget(),
...props,
},
- ...options,
});
};
@@ -289,4 +303,124 @@ describe('User Popover Component', () => {
expect(findUserLocalTime().exists()).toBe(false);
});
});
+
+ describe("when current user doesn't follow the user", () => {
+ beforeEach(() => createWrapper());
+
+ it('renders the Follow button with the correct variant', () => {
+ expect(findToggleFollowButton().text()).toBe('Follow');
+ expect(findToggleFollowButton().props('variant')).toBe('confirm');
+ });
+
+ describe('when clicking', () => {
+ it('follows the user', async () => {
+ followUser.mockResolvedValue({});
+
+ await findToggleFollowButton().trigger('click');
+
+ expect(findToggleFollowButton().props('loading')).toBe(true);
+
+ await axios.waitForAll();
+
+ expect(wrapper.emitted().follow.length).toBe(1);
+ expect(wrapper.emitted().unfollow).toBeFalsy();
+ });
+
+ describe('when an error occurs', () => {
+ beforeEach(() => {
+ followUser.mockRejectedValue({});
+
+ findToggleFollowButton().trigger('click');
+ });
+
+ it('shows an error message', async () => {
+ await axios.waitForAll();
+
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'An error occurred while trying to follow this user, please try again.',
+ error: {},
+ captureError: true,
+ });
+ });
+
+ it('emits no events', async () => {
+ await axios.waitForAll();
+
+ expect(wrapper.emitted().follow).toBe(undefined);
+ expect(wrapper.emitted().unfollow).toBe(undefined);
+ });
+ });
+ });
+ });
+
+ describe('when current user follows the user', () => {
+ beforeEach(() => createWrapper({ user: { ...DEFAULT_PROPS.user, isFollowed: true } }));
+
+ it('renders the Unfollow button with the correct variant', () => {
+ expect(findToggleFollowButton().text()).toBe('Unfollow');
+ expect(findToggleFollowButton().props('variant')).toBe('default');
+ });
+
+ describe('when clicking', () => {
+ it('unfollows the user', async () => {
+ unfollowUser.mockResolvedValue({});
+
+ findToggleFollowButton().trigger('click');
+
+ await axios.waitForAll();
+
+ expect(wrapper.emitted().follow).toBe(undefined);
+ expect(wrapper.emitted().unfollow.length).toBe(1);
+ });
+
+ describe('when an error occurs', () => {
+ beforeEach(async () => {
+ unfollowUser.mockRejectedValue({});
+
+ findToggleFollowButton().trigger('click');
+
+ await axios.waitForAll();
+ });
+
+ it('shows an error message', () => {
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'An error occurred while trying to unfollow this user, please try again.',
+ error: {},
+ captureError: true,
+ });
+ });
+
+ it('emits no events', () => {
+ expect(wrapper.emitted().follow).toBe(undefined);
+ expect(wrapper.emitted().unfollow).toBe(undefined);
+ });
+ });
+ });
+ });
+
+ describe('when the current user is the user', () => {
+ beforeEach(() => {
+ gon.current_username = DEFAULT_PROPS.user.username;
+ createWrapper();
+ });
+
+ it("doesn't render the toggle follow button", () => {
+ expect(findToggleFollowButton().exists()).toBe(false);
+ });
+ });
+
+ describe('when API does not support `isFollowed`', () => {
+ beforeEach(() => {
+ const user = {
+ ...DEFAULT_PROPS.user,
+ isFollowed: undefined,
+ };
+
+ createWrapper({ user });
+ });
+
+ it('does not render the toggle follow button', () => {
+ expect(findToggleFollowButton().exists()).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/directives/autofocusonshow_spec.js b/spec/frontend/vue_shared/directives/autofocusonshow_spec.js
index 59ce9f086c3..d052c99ec0e 100644
--- a/spec/frontend/vue_shared/directives/autofocusonshow_spec.js
+++ b/spec/frontend/vue_shared/directives/autofocusonshow_spec.js
@@ -1,3 +1,4 @@
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
/**
@@ -10,10 +11,14 @@ describe('AutofocusOnShow directive', () => {
let el;
beforeEach(() => {
- setFixtures('<div id="container" style="display: none;"><input id="inputel"/></div>');
+ setHTMLFixture('<div id="container" style="display: none;"><input id="inputel"/></div>');
el = document.querySelector('#inputel');
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it('should bind IntersectionObserver on input element', () => {
jest.spyOn(el, 'focus').mockImplementation(() => {});
diff --git a/spec/frontend/vue_shared/issuable/list/components/issuable_bulk_edit_sidebar_spec.js b/spec/frontend/vue_shared/issuable/list/components/issuable_bulk_edit_sidebar_spec.js
index 7dfeced571a..a25f92c9cf2 100644
--- a/spec/frontend/vue_shared/issuable/list/components/issuable_bulk_edit_sidebar_spec.js
+++ b/spec/frontend/vue_shared/issuable/list/components/issuable_bulk_edit_sidebar_spec.js
@@ -1,6 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import IssuableBulkEditSidebar from '~/vue_shared/issuable/list/components/issuable_bulk_edit_sidebar.vue';
const createComponent = ({ expanded = true } = {}) =>
@@ -22,12 +23,13 @@ describe('IssuableBulkEditSidebar', () => {
let wrapper;
beforeEach(() => {
- setFixtures('<div class="layout-page right-sidebar-collapsed"></div>');
+ setHTMLFixture('<div class="layout-page right-sidebar-collapsed"></div>');
wrapper = createComponent();
});
afterEach(() => {
wrapper.destroy();
+ resetHTMLFixture();
});
describe('watch', () => {
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js
index b79dc0bf976..d3e484cf913 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js
@@ -36,7 +36,6 @@ describe('IssuableEditForm', () => {
beforeEach(() => {
wrapper = createComponent();
- gon.features = { markdownContinueLists: true };
});
afterEach(() => {
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js
index 1cdd709159f..544db891a13 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js
@@ -1,8 +1,6 @@
import { GlIcon, GlAvatarLabeled } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import IssuableHeader from '~/vue_shared/issuable/show/components/issuable_header.vue';
import { mockIssuableShowProps, mockIssuable } from '../mock_data';
@@ -12,10 +10,17 @@ const issuableHeaderProps = {
...mockIssuableShowProps,
};
-const createComponent = (propsData = issuableHeaderProps, { stubs } = {}) =>
- extendedWrapper(
- shallowMount(IssuableHeader, {
- propsData,
+describe('IssuableHeader', () => {
+ let wrapper;
+
+ const findTaskStatusEl = () => wrapper.findByTestId('task-status');
+
+ const createComponent = (props = {}, { stubs } = {}) => {
+ wrapper = shallowMountExtended(IssuableHeader, {
+ propsData: {
+ ...issuableHeaderProps,
+ ...props,
+ },
slots: {
'status-badge': 'Open',
'header-actions': `
@@ -24,23 +29,18 @@ const createComponent = (propsData = issuableHeaderProps, { stubs } = {}) =>
`,
},
stubs,
- }),
- );
-
-describe('IssuableHeader', () => {
- let wrapper;
-
- beforeEach(() => {
- wrapper = createComponent();
- });
+ });
+ };
afterEach(() => {
wrapper.destroy();
+ resetHTMLFixture();
});
describe('computed', () => {
describe('authorId', () => {
it('returns numeric ID from GraphQL ID of `author` prop', () => {
+ createComponent();
expect(wrapper.vm.authorId).toBe(1);
});
});
@@ -48,10 +48,11 @@ describe('IssuableHeader', () => {
describe('handleRightSidebarToggleClick', () => {
beforeEach(() => {
- setFixtures('<button class="js-toggle-right-sidebar-button">Collapse sidebar</button>');
+ setHTMLFixture('<button class="js-toggle-right-sidebar-button">Collapse sidebar</button>');
});
it('dispatches `click` event on sidebar toggle button', () => {
+ createComponent();
wrapper.vm.toggleSidebarButtonEl = document.querySelector('.js-toggle-right-sidebar-button');
jest.spyOn(wrapper.vm.toggleSidebarButtonEl, 'dispatchEvent').mockImplementation(jest.fn);
@@ -67,20 +68,21 @@ describe('IssuableHeader', () => {
describe('template', () => {
it('renders issuable status icon and text', () => {
+ createComponent();
const statusBoxEl = wrapper.findByTestId('status');
+ const statusIconEl = statusBoxEl.findComponent(GlIcon);
expect(statusBoxEl.exists()).toBe(true);
- expect(statusBoxEl.find(GlIcon).props('name')).toBe(mockIssuableShowProps.statusIcon);
+ expect(statusIconEl.props('name')).toBe(mockIssuableShowProps.statusIcon);
+ expect(statusIconEl.attributes('class')).toBe(mockIssuableShowProps.statusIconClass);
expect(statusBoxEl.text()).toContain('Open');
});
it('renders blocked icon when issuable is blocked', async () => {
- wrapper.setProps({
+ createComponent({
blocked: true,
});
- await nextTick();
-
const blockedEl = wrapper.findByTestId('blocked');
expect(blockedEl.exists()).toBe(true);
@@ -88,12 +90,10 @@ describe('IssuableHeader', () => {
});
it('renders confidential icon when issuable is confidential', async () => {
- wrapper.setProps({
+ createComponent({
confidential: true,
});
- await nextTick();
-
const confidentialEl = wrapper.findByTestId('confidential');
expect(confidentialEl.exists()).toBe(true);
@@ -101,6 +101,7 @@ describe('IssuableHeader', () => {
});
it('renders issuable author avatar', () => {
+ createComponent();
const { username, name, webUrl, avatarUrl } = mockIssuable.author;
const avatarElAttrs = {
'data-user-id': '1',
@@ -120,28 +121,26 @@ describe('IssuableHeader', () => {
expect(avatarEl.find(GlAvatarLabeled).find(GlIcon).exists()).toBe(false);
});
- it('renders tast status text when `taskCompletionStatus` prop is defined', () => {
- let taskStatusEl = wrapper.findByTestId('task-status');
+ it('renders task status text when `taskCompletionStatus` prop is defined', () => {
+ createComponent();
- expect(taskStatusEl.exists()).toBe(true);
- expect(taskStatusEl.text()).toContain('0 of 5 tasks completed');
+ expect(findTaskStatusEl().exists()).toBe(true);
+ expect(findTaskStatusEl().text()).toContain('0 of 5 tasks completed');
+ });
- const wrapperSingleTask = createComponent({
- ...issuableHeaderProps,
+ it('does not render task status text when tasks count is 0', () => {
+ createComponent({
taskCompletionStatus: {
+ count: 0,
completedCount: 0,
- count: 1,
},
});
- taskStatusEl = wrapperSingleTask.findByTestId('task-status');
-
- expect(taskStatusEl.text()).toContain('0 of 1 task completed');
-
- wrapperSingleTask.destroy();
+ expect(findTaskStatusEl().exists()).toBe(false);
});
it('renders sidebar toggle button', () => {
+ createComponent();
const toggleButtonEl = wrapper.findByTestId('sidebar-toggle');
expect(toggleButtonEl.exists()).toBe(true);
@@ -149,6 +148,7 @@ describe('IssuableHeader', () => {
});
it('renders header actions', () => {
+ createComponent();
const actionsEl = wrapper.findByTestId('header-actions');
expect(actionsEl.find('button.js-close').exists()).toBe(true);
@@ -157,9 +157,8 @@ describe('IssuableHeader', () => {
describe('when author exists outside of GitLab', () => {
it("renders 'external-link' icon in avatar label", () => {
- wrapper = createComponent(
+ createComponent(
{
- ...issuableHeaderProps,
author: {
...issuableHeaderProps.author,
webUrl: 'https://jira.com/test-user/author.jpg',
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js
index d1eb1366225..8b027f990a2 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js
@@ -49,6 +49,7 @@ describe('IssuableShowRoot', () => {
const {
statusBadgeClass,
statusIcon,
+ statusIconClass,
enableEdit,
enableAutocomplete,
editFormVisible,
@@ -56,7 +57,7 @@ describe('IssuableShowRoot', () => {
descriptionHelpPath,
taskCompletionStatus,
} = mockIssuableShowProps;
- const { blocked, confidential, createdAt, author } = mockIssuable;
+ const { state, blocked, confidential, createdAt, author } = mockIssuable;
it('renders component container element with class `issuable-show-container`', () => {
expect(wrapper.classes()).toContain('issuable-show-container');
@@ -67,15 +68,17 @@ describe('IssuableShowRoot', () => {
expect(issuableHeader.exists()).toBe(true);
expect(issuableHeader.props()).toMatchObject({
+ issuableState: state,
statusBadgeClass,
statusIcon,
+ statusIconClass,
blocked,
confidential,
createdAt,
author,
taskCompletionStatus,
});
- expect(issuableHeader.find('.issuable-status-box').text()).toContain('Open');
+ expect(issuableHeader.find('.issuable-status-badge').text()).toContain('Open');
expect(issuableHeader.find('.detail-page-header-actions button.js-close').exists()).toBe(
true,
);
diff --git a/spec/frontend/vue_shared/issuable/show/mock_data.js b/spec/frontend/vue_shared/issuable/show/mock_data.js
index f5f3ed58655..32bb9edfe08 100644
--- a/spec/frontend/vue_shared/issuable/show/mock_data.js
+++ b/spec/frontend/vue_shared/issuable/show/mock_data.js
@@ -36,8 +36,9 @@ export const mockIssuableShowProps = {
enableTaskList: true,
enableEdit: true,
showFieldTitle: false,
- statusBadgeClass: 'status-box-open',
- statusIcon: 'issue-open-m',
+ statusBadgeClass: 'issuable-status-badge-open',
+ statusIcon: 'issues',
+ statusIconClass: 'gl-sm-display-none',
taskCompletionStatus: {
completedCount: 0,
count: 5,
diff --git a/spec/frontend/vue_shared/issuable/sidebar/components/issuable_sidebar_root_spec.js b/spec/frontend/vue_shared/issuable/sidebar/components/issuable_sidebar_root_spec.js
index 47bf3c8ed83..6c9e5f85fa0 100644
--- a/spec/frontend/vue_shared/issuable/sidebar/components/issuable_sidebar_root_spec.js
+++ b/spec/frontend/vue_shared/issuable/sidebar/components/issuable_sidebar_root_spec.js
@@ -1,6 +1,7 @@
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
-import Cookies from 'js-cookie';
import { nextTick } from 'vue';
+import Cookies from '~/lib/utils/cookies';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import IssuableSidebarRoot from '~/vue_shared/issuable/sidebar/components/issuable_sidebar_root.vue';
@@ -9,7 +10,7 @@ import { USER_COLLAPSED_GUTTER_COOKIE } from '~/vue_shared/issuable/sidebar/cons
const MOCK_LAYOUT_PAGE_CLASS = 'layout-page';
const createComponent = () => {
- setFixtures(`<div class="${MOCK_LAYOUT_PAGE_CLASS}"></div>`);
+ setHTMLFixture(`<div class="${MOCK_LAYOUT_PAGE_CLASS}"></div>`);
return shallowMountExtended(IssuableSidebarRoot, {
slots: {
@@ -38,6 +39,7 @@ describe('IssuableSidebarRoot', () => {
afterEach(() => {
wrapper.destroy();
+ resetHTMLFixture();
});
describe('when sidebar is expanded', () => {
diff --git a/spec/frontend/vue_shared/security_configuration/components/section_layout_spec.js b/spec/frontend/vue_shared/security_configuration/components/section_layout_spec.js
new file mode 100644
index 00000000000..136fe74b0d6
--- /dev/null
+++ b/spec/frontend/vue_shared/security_configuration/components/section_layout_spec.js
@@ -0,0 +1,58 @@
+import { mount } from '@vue/test-utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import SectionLayout from '~/vue_shared/security_configuration/components/section_layout.vue';
+import SectionLoader from '~/vue_shared/security_configuration/components/section_loader.vue';
+
+describe('Section Layout component', () => {
+ let wrapper;
+
+ const createComponent = (propsData) => {
+ wrapper = extendedWrapper(
+ mount(SectionLayout, {
+ propsData,
+ scopedSlots: {
+ description: '<span>foo</span>',
+ features: '<span>bar</span>',
+ },
+ }),
+ );
+ };
+
+ const findHeading = () => wrapper.find('h2');
+ const findLoader = () => wrapper.findComponent(SectionLoader);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('basic structure', () => {
+ beforeEach(() => {
+ createComponent({ heading: 'testheading' });
+ });
+
+ const slots = {
+ description: 'foo',
+ features: 'bar',
+ };
+
+ it('should render heading when passed in as props', () => {
+ expect(findHeading().exists()).toBe(true);
+ expect(findHeading().text()).toBe('testheading');
+ });
+
+ Object.keys(slots).forEach((slot) => {
+ it('renders the slots', () => {
+ const slotContent = slots[slot];
+ createComponent({ heading: '' });
+ expect(wrapper.text()).toContain(slotContent);
+ });
+ });
+ });
+
+ describe('loading state', () => {
+ it('should show loaders when loading', () => {
+ createComponent({ heading: 'testheading', isLoading: true });
+ expect(findLoader().exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/security_reports/mock_data.js b/spec/frontend/vue_shared/security_reports/mock_data.js
index dac9accbbf5..a9ad675e538 100644
--- a/spec/frontend/vue_shared/security_reports/mock_data.js
+++ b/spec/frontend/vue_shared/security_reports/mock_data.js
@@ -62,7 +62,7 @@ export const mockFindings = [
report_type: 'dependency_scanning',
name: '3rd party CORS request may execute in jquery',
severity: 'high',
- scanner: { external_id: 'retire.js', name: 'Retire.js' },
+ scanner: { external_id: 'gemnasium', name: 'gemnasium' },
identifiers: [
{
external_type: 'cve',
@@ -145,7 +145,7 @@ export const mockFindings = [
name:
'jQuery before 3.4.0, as used in Drupal, Backdrop CMS, and other products, mishandles jQuery.extend(true, {}, ...) because of Object.prototype pollution in jquery',
severity: 'low',
- scanner: { external_id: 'retire.js', name: 'Retire.js' },
+ scanner: { external_id: 'gemnasium', name: 'gemnasium' },
identifiers: [
{
external_type: 'cve',
@@ -227,7 +227,7 @@ export const mockFindings = [
name:
'jQuery before 3.4.0, as used in Drupal, Backdrop CMS, and other products, mishandles jQuery.extend(true, {}, ...) because of Object.prototype pollution in jquery',
severity: 'low',
- scanner: { external_id: 'retire.js', name: 'Retire.js' },
+ scanner: { external_id: 'gemnasium', name: 'gemnasium' },
identifiers: [
{
external_type: 'cve',