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>2022-07-20 18:40:28 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-07-20 18:40:28 +0300
commitb595cb0c1dec83de5bdee18284abe86614bed33b (patch)
tree8c3d4540f193c5ff98019352f554e921b3a41a72 /spec/frontend/vue_shared/components
parent2f9104a328fc8a4bddeaa4627b595166d24671d0 (diff)
Add latest changes from gitlab-org/gitlab@15-2-stable-eev15.2.0-rc42
Diffstat (limited to 'spec/frontend/vue_shared/components')
-rw-r--r--spec/frontend/vue_shared/components/color_select_dropdown/color_select_root_spec.js18
-rw-r--r--spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_spec.js52
-rw-r--r--spec/frontend/vue_shared/components/color_select_dropdown/dropdown_value_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/deployment_instance/deployment_instance_spec.js50
-rw-r--r--spec/frontend/vue_shared/components/deployment_instance/mock_data.js1
-rw-r--r--spec/frontend/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js143
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js146
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js283
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js282
-rw-r--r--spec/frontend/vue_shared/components/markdown/field_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/notes/__snapshots__/placeholder_note_spec.js.snap23
-rw-r--r--spec/frontend/vue_shared/components/notes/placeholder_note_spec.js12
-rw-r--r--spec/frontend/vue_shared/components/page_size_selector_spec.js44
-rw-r--r--spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/runner_aws_deployments/__snapshots__/runner_aws_deployments_modal_spec.js.snap4
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js10
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/link_dependencies_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/mock_data.js2
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util_spec.js33
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/utils/package_json_linker_spec.js15
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js41
-rw-r--r--spec/frontend/vue_shared/components/user_popover/user_popover_spec.js31
23 files changed, 982 insertions, 235 deletions
diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/color_select_root_spec.js b/spec/frontend/vue_shared/components/color_select_dropdown/color_select_root_spec.js
index 93b59800c27..441e21ee905 100644
--- a/spec/frontend/vue_shared/components/color_select_dropdown/color_select_root_spec.js
+++ b/spec/frontend/vue_shared/components/color_select_dropdown/color_select_root_spec.js
@@ -84,15 +84,15 @@ describe('LabelsSelectRoot', () => {
});
describe('if the variant is `sidebar`', () => {
- beforeEach(() => {
+ it('renders SidebarEditableItem component', () => {
createComponent();
- });
- it('renders SidebarEditableItem component', () => {
expect(findSidebarEditableItem().exists()).toBe(true);
});
it('renders correct props for the SidebarEditableItem component', () => {
+ createComponent();
+
expect(findSidebarEditableItem().props()).toMatchObject({
title: wrapper.vm.$options.i18n.widgetTitle,
canEdit: defaultProps.allowEdit,
@@ -135,7 +135,7 @@ describe('LabelsSelectRoot', () => {
it('handles DropdownContents setColor', () => {
findDropdownContents().vm.$emit('setColor', color);
- expect(wrapper.emitted('updateSelectedColor')).toEqual([[color]]);
+ expect(wrapper.emitted('updateSelectedColor')).toEqual([[{ color }]]);
});
});
@@ -157,20 +157,24 @@ describe('LabelsSelectRoot', () => {
createComponent({ propsData: { iid: undefined } });
findDropdownContents().vm.$emit('setColor', color);
- expect(wrapper.emitted('updateSelectedColor')).toEqual([[color]]);
+ expect(wrapper.emitted('updateSelectedColor')).toEqual([[{ color }]]);
});
describe('when updating color for epic', () => {
- beforeEach(() => {
+ const setup = () => {
createComponent();
findDropdownContents().vm.$emit('setColor', color);
- });
+ };
it('sets the loading state', () => {
+ setup();
+
expect(findSidebarEditableItem().props('loading')).toBe(true);
});
it('updates color correctly after successful mutation', async () => {
+ setup();
+
await waitForPromises();
expect(findDropdownValue().props('selectedColor').color).toEqual(
updateColorMutationResponse.data.updateIssuableColor.issuable.color,
diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_spec.js b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_spec.js
index 74f50b878e2..ee4d3a2630a 100644
--- a/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_spec.js
+++ b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_spec.js
@@ -1,57 +1,30 @@
-import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
+import { GlDropdown } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import { DROPDOWN_VARIANT } from '~/vue_shared/components/color_select_dropdown/constants';
import DropdownContents from '~/vue_shared/components/color_select_dropdown/dropdown_contents.vue';
import DropdownContentsColorView from '~/vue_shared/components/color_select_dropdown/dropdown_contents_color_view.vue';
+import DropdownHeader from '~/vue_shared/components/color_select_dropdown/dropdown_header.vue';
import { color } from './mock_data';
-const showDropdown = jest.fn();
-const focusInput = jest.fn();
-
const defaultProps = {
dropdownTitle: '',
selectedColor: color,
- dropdownButtonText: '',
+ dropdownButtonText: 'Pick a color',
variant: '',
isVisible: false,
};
-const GlDropdownStub = {
- template: `
- <div>
- <slot name="header"></slot>
- <slot></slot>
- </div>
- `,
- methods: {
- show: showDropdown,
- hide: jest.fn(),
- },
-};
-
-const DropdownHeaderStub = {
- template: `
- <div>Hello, I am a header</div>
- `,
- methods: {
- focusInput,
- },
-};
-
describe('DropdownContent', () => {
let wrapper;
const createComponent = ({ propsData = {} } = {}) => {
- wrapper = shallowMount(DropdownContents, {
+ wrapper = mountExtended(DropdownContents, {
propsData: {
...defaultProps,
...propsData,
},
- stubs: {
- GlDropdown: GlDropdownStub,
- DropdownHeader: DropdownHeaderStub,
- },
});
};
@@ -60,16 +33,17 @@ describe('DropdownContent', () => {
});
const findColorView = () => wrapper.findComponent(DropdownContentsColorView);
- const findDropdownHeader = () => wrapper.findComponent(DropdownHeaderStub);
- const findDropdown = () => wrapper.findComponent(GlDropdownStub);
+ const findDropdownHeader = () => wrapper.findComponent(DropdownHeader);
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
it('calls dropdown `show` method on `isVisible` prop change', async () => {
createComponent();
+ const spy = jest.spyOn(wrapper.vm.$refs.dropdown, 'show');
await wrapper.setProps({
isVisible: true,
});
- expect(showDropdown).toHaveBeenCalledTimes(1);
+ expect(spy).toHaveBeenCalledTimes(1);
});
it('does not emit `setColor` event on dropdown hide if color did not change', () => {
@@ -110,4 +84,12 @@ describe('DropdownContent', () => {
expect(findDropdownHeader().exists()).toBe(true);
});
+
+ it('handles no selected color', () => {
+ createComponent({ propsData: { selectedColor: {} } });
+
+ expect(wrapper.findByTestId('fallback-button-text').text()).toEqual(
+ defaultProps.dropdownButtonText,
+ );
+ });
});
diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_value_spec.js b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_value_spec.js
index f22592dd604..5bbdb136353 100644
--- a/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_value_spec.js
+++ b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_value_spec.js
@@ -33,7 +33,7 @@ describe('DropdownValue', () => {
it.each`
index | cssClass
- ${0} | ${['gl-font-base', 'gl-line-height-24']}
+ ${0} | ${[]}
${1} | ${['hide-collapsed']}
`(
'passes correct props to the ColorItem with CSS class `$cssClass`',
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 e3d8bfd22ca..79001b9282f 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
@@ -1,7 +1,6 @@
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import DeployBoardInstance from '~/vue_shared/components/deployment_instance.vue';
-import { folder } from './mock_data';
describe('Deploy Board Instance', () => {
let wrapper;
@@ -13,7 +12,6 @@ describe('Deploy Board Instance', () => {
...props,
},
provide: {
- glFeatures: { monitorLogging: true },
...provide,
},
});
@@ -25,7 +23,6 @@ describe('Deploy Board Instance', () => {
it('should render a div with the correct css status and tooltip data', () => {
wrapper = createComponent({
- logsPath: folder.logs_path,
tooltipText: 'This is a pod',
});
@@ -43,17 +40,6 @@ describe('Deploy Board Instance', () => {
expect(wrapper.classes('deployment-instance-deploying')).toBe(true);
expect(wrapper.attributes('title')).toEqual('');
});
-
- it('should have a log path computed with a pod name as a parameter', () => {
- wrapper = createComponent({
- logsPath: folder.logs_path,
- podName: 'tanuki-1',
- });
-
- expect(wrapper.vm.computedLogPath).toEqual(
- '/root/review-app/-/logs?environment_name=foo&pod_name=tanuki-1',
- );
- });
});
describe('as a canary deployment', () => {
@@ -76,46 +62,10 @@ describe('Deploy Board Instance', () => {
wrapper.destroy();
});
- it('should not be a link without a logsPath prop', async () => {
- wrapper = createComponent({
- stable: false,
- logsPath: '',
- });
-
- await nextTick();
- expect(wrapper.vm.computedLogPath).toBeNull();
- expect(wrapper.vm.isLink).toBeFalsy();
- });
-
- it('should render a link without href if path is not passed', () => {
- wrapper = createComponent();
-
- expect(wrapper.attributes('href')).toBeUndefined();
- });
-
it('should not have a tooltip', () => {
wrapper = createComponent();
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/deployment_instance/mock_data.js b/spec/frontend/vue_shared/components/deployment_instance/mock_data.js
index 6618c57948c..098787cd1b4 100644
--- a/spec/frontend/vue_shared/components/deployment_instance/mock_data.js
+++ b/spec/frontend/vue_shared/components/deployment_instance/mock_data.js
@@ -140,5 +140,4 @@ export const folder = {
created_at: '2017-02-01T19:42:18.400Z',
updated_at: '2017-02-01T19:42:18.400Z',
rollout_status: {},
- logs_path: '/root/review-app/-/logs?environment_name=foo',
};
diff --git a/spec/frontend/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js b/spec/frontend/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js
index d0fa8b8dacb..16f924b44d8 100644
--- a/spec/frontend/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js
@@ -1,11 +1,9 @@
import { mount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
-import { compileToFunctions } from 'vue-template-compiler';
-
+import { nextTick } from 'vue';
import { GREEN_BOX_IMAGE_URL, RED_BOX_IMAGE_URL } from 'spec/test_constants';
-import imageDiffViewer from '~/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue';
+import ImageDiffViewer from '~/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue';
-describe('ImageDiffViewer', () => {
+describe('ImageDiffViewer component', () => {
const requiredProps = {
diffMode: 'replaced',
newPath: GREEN_BOX_IMAGE_URL,
@@ -17,15 +15,12 @@ describe('ImageDiffViewer', () => {
newSize: 1024,
};
let wrapper;
- let vm;
- function createComponent(props) {
- const ImageDiffViewer = Vue.extend(imageDiffViewer);
- wrapper = mount(ImageDiffViewer, { propsData: props });
- vm = wrapper.vm;
- }
+ const createComponent = (props, slots) => {
+ wrapper = mount(ImageDiffViewer, { propsData: props, slots });
+ };
- const triggerEvent = (eventName, el = vm.$el, clientX = 0) => {
+ const triggerEvent = (eventName, el = wrapper.$el, clientX = 0) => {
const event = new MouseEvent(eventName, {
bubbles: true,
cancelable: true,
@@ -51,128 +46,76 @@ describe('ImageDiffViewer', () => {
wrapper.destroy();
});
- it('renders image diff for replaced', async () => {
- createComponent({ ...allProps });
-
- await nextTick();
- const metaInfoElements = vm.$el.querySelectorAll('.image-info');
-
- expect(vm.$el.querySelector('.added img').getAttribute('src')).toBe(GREEN_BOX_IMAGE_URL);
-
- expect(vm.$el.querySelector('.deleted img').getAttribute('src')).toBe(RED_BOX_IMAGE_URL);
-
- expect(vm.$el.querySelector('.view-modes-menu li.active').textContent.trim()).toBe('2-up');
- expect(vm.$el.querySelector('.view-modes-menu li:nth-child(2)').textContent.trim()).toBe(
- 'Swipe',
- );
-
- expect(vm.$el.querySelector('.view-modes-menu li:nth-child(3)').textContent.trim()).toBe(
- 'Onion skin',
- );
-
- expect(metaInfoElements.length).toBe(2);
- expect(metaInfoElements[0]).toHaveText('2.00 KiB');
- expect(metaInfoElements[1]).toHaveText('1.00 KiB');
+ it('renders image diff for replaced', () => {
+ createComponent(allProps);
+ const metaInfoElements = wrapper.findAll('.image-info');
+
+ expect(wrapper.find('.added img').attributes('src')).toBe(GREEN_BOX_IMAGE_URL);
+ expect(wrapper.find('.deleted img').attributes('src')).toBe(RED_BOX_IMAGE_URL);
+ expect(wrapper.find('.view-modes-menu li.active').text()).toBe('2-up');
+ expect(wrapper.find('.view-modes-menu li:nth-child(2)').text()).toBe('Swipe');
+ expect(wrapper.find('.view-modes-menu li:nth-child(3)').text()).toBe('Onion skin');
+ expect(metaInfoElements).toHaveLength(2);
+ expect(metaInfoElements.at(0).text()).toBe('2.00 KiB');
+ expect(metaInfoElements.at(1).text()).toBe('1.00 KiB');
});
- it('renders image diff for new', async () => {
+ it('renders image diff for new', () => {
createComponent({ ...allProps, diffMode: 'new', oldPath: '' });
- await nextTick();
-
- const metaInfoElement = vm.$el.querySelector('.image-info');
-
- expect(vm.$el.querySelector('.added img').getAttribute('src')).toBe(GREEN_BOX_IMAGE_URL);
- expect(metaInfoElement).toHaveText('1.00 KiB');
+ expect(wrapper.find('.added img').attributes('src')).toBe(GREEN_BOX_IMAGE_URL);
+ expect(wrapper.find('.image-info').text()).toBe('1.00 KiB');
});
- it('renders image diff for deleted', async () => {
+ it('renders image diff for deleted', () => {
createComponent({ ...allProps, diffMode: 'deleted', newPath: '' });
- await nextTick();
-
- const metaInfoElement = vm.$el.querySelector('.image-info');
-
- expect(vm.$el.querySelector('.deleted img').getAttribute('src')).toBe(RED_BOX_IMAGE_URL);
- expect(metaInfoElement).toHaveText('2.00 KiB');
+ expect(wrapper.find('.deleted img').attributes('src')).toBe(RED_BOX_IMAGE_URL);
+ expect(wrapper.find('.image-info').text()).toBe('2.00 KiB');
});
- it('renders image diff for renamed', async () => {
- vm = new Vue({
- components: {
- imageDiffViewer,
- },
- data() {
- return {
- ...allProps,
- diffMode: 'renamed',
- };
- },
- ...compileToFunctions(`
- <image-diff-viewer
- :diff-mode="diffMode"
- :new-path="newPath"
- :old-path="oldPath"
- :new-size="newSize"
- :old-size="oldSize"
- >
- <template #image-overlay>
- <span class="overlay">test</span>
- </template>
- </image-diff-viewer>
- `),
- }).$mount();
-
- await nextTick();
-
- const metaInfoElement = vm.$el.querySelector('.image-info');
-
- expect(vm.$el.querySelector('img').getAttribute('src')).toBe(GREEN_BOX_IMAGE_URL);
- expect(vm.$el.querySelector('.overlay')).not.toBe(null);
-
- expect(metaInfoElement).toHaveText('2.00 KiB');
+ it('renders image diff for renamed', () => {
+ createComponent(
+ { ...allProps, diffMode: 'renamed' },
+ { 'image-overlay': '<span class="overlay">test</span>' },
+ );
+
+ expect(wrapper.find('img').attributes('src')).toBe(GREEN_BOX_IMAGE_URL);
+ expect(wrapper.find('.overlay').exists()).toBe(true);
+ expect(wrapper.find('.image-info').text()).toBe('2.00 KiB');
});
describe('swipeMode', () => {
beforeEach(() => {
- createComponent({ ...requiredProps });
-
- return nextTick();
+ createComponent(requiredProps);
});
it('switches to Swipe Mode', async () => {
- vm.$el.querySelector('.view-modes-menu li:nth-child(2)').click();
+ await wrapper.find('.view-modes-menu li:nth-child(2)').trigger('click');
- await nextTick();
- expect(vm.$el.querySelector('.view-modes-menu li.active').textContent.trim()).toBe('Swipe');
+ expect(wrapper.find('.view-modes-menu li.active').text()).toBe('Swipe');
});
});
describe('onionSkin', () => {
beforeEach(() => {
createComponent({ ...requiredProps });
-
- return nextTick();
});
it('switches to Onion Skin Mode', async () => {
- vm.$el.querySelector('.view-modes-menu li:nth-child(3)').click();
+ await wrapper.find('.view-modes-menu li:nth-child(3)').trigger('click');
- await nextTick();
- expect(vm.$el.querySelector('.view-modes-menu li.active').textContent.trim()).toBe(
- 'Onion skin',
- );
+ expect(wrapper.find('.view-modes-menu li.active').text()).toBe('Onion skin');
});
it('has working drag handler', async () => {
- vm.$el.querySelector('.view-modes-menu li:nth-child(3)').click();
+ await wrapper.find('.view-modes-menu li:nth-child(3)').trigger('click');
+ dragSlider(wrapper.find('.dragger').element, document, 20);
await nextTick();
- dragSlider(vm.$el.querySelector('.dragger'), document, 20);
- await nextTick();
- expect(vm.$el.querySelector('.dragger').style.left).toBe('20px');
- expect(vm.$el.querySelector('.added.frame').style.opacity).toBe('0.2');
+ expect(wrapper.find('.dragger').attributes('style')).toBe('left: 20px;');
+ expect(wrapper.find('.added.frame').attributes('style')).toBe('opacity: 0.2;');
});
});
});
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 e3e2ef5610d..86d1f21fd04 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
@@ -8,6 +8,8 @@ import EmojiToken from '~/vue_shared/components/filtered_search_bar/tokens/emoji
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';
import ReleaseToken from '~/vue_shared/components/filtered_search_bar/tokens/release_token.vue';
+import CrmContactToken from '~/vue_shared/components/filtered_search_bar/tokens/crm_contact_token.vue';
+import CrmOrganizationToken from '~/vue_shared/components/filtered_search_bar/tokens/crm_organization_token.vue';
export const mockAuthor1 = {
id: 1,
@@ -62,6 +64,128 @@ export const mockMilestones = [
mockEscapedMilestone,
];
+export const mockCrmContacts = [
+ {
+ id: 'gid://gitlab/CustomerRelations::Contact/1',
+ firstName: 'John',
+ lastName: 'Smith',
+ email: 'john@smith.com',
+ },
+ {
+ id: 'gid://gitlab/CustomerRelations::Contact/2',
+ firstName: 'Andy',
+ lastName: 'Green',
+ email: 'andy@green.net',
+ },
+];
+
+export const mockCrmOrganizations = [
+ {
+ id: 'gid://gitlab/CustomerRelations::Organization/1',
+ name: 'First Org Ltd.',
+ },
+ {
+ id: 'gid://gitlab/CustomerRelations::Organization/2',
+ name: 'Organizer S.p.a.',
+ },
+];
+
+export const mockProjectCrmContactsQueryResponse = {
+ data: {
+ project: {
+ __typename: 'Project',
+ id: 1,
+ group: {
+ __typename: 'Group',
+ id: 1,
+ contacts: {
+ __typename: 'CustomerRelationsContactConnection',
+ nodes: [
+ {
+ __typename: 'CustomerRelationsContact',
+ ...mockCrmContacts[0],
+ },
+ {
+ __typename: 'CustomerRelationsContact',
+ ...mockCrmContacts[1],
+ },
+ ],
+ },
+ },
+ },
+ },
+};
+
+export const mockProjectCrmOrganizationsQueryResponse = {
+ data: {
+ project: {
+ __typename: 'Project',
+ id: 1,
+ group: {
+ __typename: 'Group',
+ id: 1,
+ organizations: {
+ __typename: 'CustomerRelationsOrganizationConnection',
+ nodes: [
+ {
+ __typename: 'CustomerRelationsOrganization',
+ ...mockCrmOrganizations[0],
+ },
+ {
+ __typename: 'CustomerRelationsOrganization',
+ ...mockCrmOrganizations[1],
+ },
+ ],
+ },
+ },
+ },
+ },
+};
+
+export const mockGroupCrmContactsQueryResponse = {
+ data: {
+ group: {
+ __typename: 'Group',
+ id: 1,
+ contacts: {
+ __typename: 'CustomerRelationsContactConnection',
+ nodes: [
+ {
+ __typename: 'CustomerRelationsContact',
+ ...mockCrmContacts[0],
+ },
+ {
+ __typename: 'CustomerRelationsContact',
+ ...mockCrmContacts[1],
+ },
+ ],
+ },
+ },
+ },
+};
+
+export const mockGroupCrmOrganizationsQueryResponse = {
+ data: {
+ group: {
+ __typename: 'Group',
+ id: 1,
+ organizations: {
+ __typename: 'CustomerRelationsOrganizationConnection',
+ nodes: [
+ {
+ __typename: 'CustomerRelationsOrganization',
+ ...mockCrmOrganizations[0],
+ },
+ {
+ __typename: 'CustomerRelationsOrganization',
+ ...mockCrmOrganizations[1],
+ },
+ ],
+ },
+ },
+ },
+};
+
export const mockEmoji1 = {
name: 'thumbsup',
};
@@ -134,6 +258,28 @@ export const mockReactionEmojiToken = {
fetchEmojis: () => Promise.resolve(mockEmojis),
};
+export const mockCrmContactToken = {
+ type: 'crm_contact',
+ title: 'Contact',
+ icon: 'user',
+ token: CrmContactToken,
+ isProject: false,
+ fullPath: 'group',
+ operators: OPERATOR_IS_ONLY,
+ unique: true,
+};
+
+export const mockCrmOrganizationToken = {
+ type: 'crm_contact',
+ title: 'Organization',
+ icon: 'user',
+ token: CrmOrganizationToken,
+ isProject: false,
+ fullPath: 'group',
+ operators: OPERATOR_IS_ONLY,
+ unique: true,
+};
+
export const mockMembershipToken = {
type: 'with_inherited_permissions',
icon: 'group',
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 ca8cd419d87..a0126c2bd63 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
@@ -418,8 +418,6 @@ describe('BaseToken', () => {
});
it('does not emit `fetch-suggestions` event on component after a delay when component emits `input` event', async () => {
- jest.useFakeTimers();
-
findGlFilteredSearchToken().vm.$emit('input', { data: 'foo' });
await nextTick();
@@ -437,8 +435,6 @@ describe('BaseToken', () => {
});
it('emits `fetch-suggestions` event on component after a delay when component emits `input` event', async () => {
- jest.useFakeTimers();
-
findGlFilteredSearchToken().vm.$emit('input', { data: 'foo' });
await nextTick();
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js
new file mode 100644
index 00000000000..157e021fc60
--- /dev/null
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js
@@ -0,0 +1,283 @@
+import {
+ GlFilteredSearchSuggestion,
+ GlFilteredSearchTokenSegment,
+ GlDropdownDivider,
+} from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import createFlash from '~/flash';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { DEFAULT_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants';
+import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
+import CrmContactToken from '~/vue_shared/components/filtered_search_bar/tokens/crm_contact_token.vue';
+import searchCrmContactsQuery from '~/vue_shared/components/filtered_search_bar/queries/search_crm_contacts.query.graphql';
+
+import {
+ mockCrmContacts,
+ mockCrmContactToken,
+ mockGroupCrmContactsQueryResponse,
+ mockProjectCrmContactsQueryResponse,
+} from '../mock_data';
+
+jest.mock('~/flash');
+
+const defaultStubs = {
+ Portal: true,
+ BaseToken,
+ GlFilteredSearchSuggestionList: {
+ template: '<div></div>',
+ methods: {
+ getValue: () => '=',
+ },
+ },
+};
+
+describe('CrmContactToken', () => {
+ Vue.use(VueApollo);
+
+ let wrapper;
+ let fakeApollo;
+
+ const getBaseToken = () => wrapper.findComponent(BaseToken);
+
+ const searchGroupCrmContactsQueryHandler = jest
+ .fn()
+ .mockResolvedValue(mockGroupCrmContactsQueryResponse);
+ const searchProjectCrmContactsQueryHandler = jest
+ .fn()
+ .mockResolvedValue(mockProjectCrmContactsQueryResponse);
+
+ const mountComponent = ({
+ config = mockCrmContactToken,
+ value = { data: '' },
+ active = false,
+ stubs = defaultStubs,
+ listeners = {},
+ queryHandler = searchGroupCrmContactsQueryHandler,
+ } = {}) => {
+ fakeApollo = createMockApollo([[searchCrmContactsQuery, queryHandler]]);
+
+ wrapper = mount(CrmContactToken, {
+ propsData: {
+ config,
+ value,
+ active,
+ cursorPosition: 'start',
+ },
+ provide: {
+ portalName: 'fake target',
+ alignSuggestions: function fakeAlignSuggestions() {},
+ suggestionsListClass: () => 'custom-class',
+ },
+ stubs,
+ listeners,
+ apolloProvider: fakeApollo,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ fakeApollo = null;
+ });
+
+ describe('methods', () => {
+ describe('fetchContacts', () => {
+ describe('for groups', () => {
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it('calls the apollo query providing the searchString when search term is a string', async () => {
+ getBaseToken().vm.$emit('fetch-suggestions', 'foo');
+ await waitForPromises();
+
+ expect(createFlash).not.toHaveBeenCalled();
+ expect(searchGroupCrmContactsQueryHandler).toHaveBeenCalledWith({
+ fullPath: 'group',
+ isProject: false,
+ searchString: 'foo',
+ searchIds: null,
+ });
+ expect(getBaseToken().props('suggestions')).toEqual(mockCrmContacts);
+ });
+
+ it('calls the apollo query providing the searchId when search term is a number', async () => {
+ getBaseToken().vm.$emit('fetch-suggestions', '5');
+ await waitForPromises();
+
+ expect(createFlash).not.toHaveBeenCalled();
+ expect(searchGroupCrmContactsQueryHandler).toHaveBeenCalledWith({
+ fullPath: 'group',
+ isProject: false,
+ searchString: null,
+ searchIds: ['gid://gitlab/CustomerRelations::Contact/5'],
+ });
+ expect(getBaseToken().props('suggestions')).toEqual(mockCrmContacts);
+ });
+ });
+
+ describe('for projects', () => {
+ beforeEach(() => {
+ mountComponent({
+ config: {
+ fullPath: 'project',
+ isProject: true,
+ },
+ queryHandler: searchProjectCrmContactsQueryHandler,
+ });
+ });
+
+ it('calls the apollo query providing the searchString when search term is a string', async () => {
+ getBaseToken().vm.$emit('fetch-suggestions', 'foo');
+ await waitForPromises();
+
+ expect(createFlash).not.toHaveBeenCalled();
+ expect(searchProjectCrmContactsQueryHandler).toHaveBeenCalledWith({
+ fullPath: 'project',
+ isProject: true,
+ searchString: 'foo',
+ searchIds: null,
+ });
+ expect(getBaseToken().props('suggestions')).toEqual(mockCrmContacts);
+ });
+
+ it('calls the apollo query providing the searchId when search term is a number', async () => {
+ getBaseToken().vm.$emit('fetch-suggestions', '5');
+ await waitForPromises();
+
+ expect(createFlash).not.toHaveBeenCalled();
+ expect(searchProjectCrmContactsQueryHandler).toHaveBeenCalledWith({
+ fullPath: 'project',
+ isProject: true,
+ searchString: null,
+ searchIds: ['gid://gitlab/CustomerRelations::Contact/5'],
+ });
+ expect(getBaseToken().props('suggestions')).toEqual(mockCrmContacts);
+ });
+ });
+
+ it('calls `createFlash` with flash error message when request fails', async () => {
+ mountComponent();
+
+ jest.spyOn(wrapper.vm.$apollo, 'query').mockRejectedValue({});
+
+ getBaseToken().vm.$emit('fetch-suggestions');
+ await waitForPromises();
+
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'There was a problem fetching CRM contacts.',
+ });
+ });
+
+ it('sets `loading` to false when request completes', async () => {
+ mountComponent();
+
+ jest.spyOn(wrapper.vm.$apollo, 'query').mockRejectedValue({});
+
+ getBaseToken().vm.$emit('fetch-suggestions');
+
+ await waitForPromises();
+
+ expect(getBaseToken().props('suggestionsLoading')).toBe(false);
+ });
+ });
+ });
+
+ describe('template', () => {
+ const defaultContacts = DEFAULT_NONE_ANY;
+
+ it('renders base-token component', () => {
+ mountComponent({
+ config: { ...mockCrmContactToken, initialContacts: mockCrmContacts },
+ value: { data: '1' },
+ });
+
+ const baseTokenEl = wrapper.find(BaseToken);
+
+ expect(baseTokenEl.exists()).toBe(true);
+ expect(baseTokenEl.props()).toMatchObject({
+ suggestions: mockCrmContacts,
+ getActiveTokenValue: wrapper.vm.getActiveContact,
+ });
+ });
+
+ it.each(mockCrmContacts)('renders token item when value is selected', (contact) => {
+ mountComponent({
+ config: { ...mockCrmContactToken, initialContacts: mockCrmContacts },
+ value: { data: `${getIdFromGraphQLId(contact.id)}` },
+ });
+
+ const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+
+ expect(tokenSegments).toHaveLength(3); // Contact, =, Contact name
+ expect(tokenSegments.at(2).text()).toBe(`${contact.firstName} ${contact.lastName}`); // Contact name
+ });
+
+ it('renders provided defaultContacts as suggestions', async () => {
+ mountComponent({
+ active: true,
+ config: { ...mockCrmContactToken, defaultContacts },
+ stubs: { Portal: true },
+ });
+ const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+ const suggestionsSegment = tokenSegments.at(2);
+ suggestionsSegment.vm.$emit('activate');
+ await nextTick();
+
+ const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
+
+ expect(suggestions).toHaveLength(defaultContacts.length);
+ defaultContacts.forEach((contact, index) => {
+ expect(suggestions.at(index).text()).toBe(contact.text);
+ });
+ });
+
+ it('does not render divider when no defaultContacts', async () => {
+ mountComponent({
+ active: true,
+ config: { ...mockCrmContactToken, defaultContacts: [] },
+ stubs: { Portal: true },
+ });
+ const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+ const suggestionsSegment = tokenSegments.at(2);
+ suggestionsSegment.vm.$emit('activate');
+ await nextTick();
+
+ expect(wrapper.find(GlFilteredSearchSuggestion).exists()).toBe(false);
+ expect(wrapper.find(GlDropdownDivider).exists()).toBe(false);
+ });
+
+ it('renders `DEFAULT_NONE_ANY` as default suggestions', () => {
+ mountComponent({
+ active: true,
+ config: { ...mockCrmContactToken },
+ stubs: { Portal: true },
+ });
+ const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+ const suggestionsSegment = tokenSegments.at(2);
+ suggestionsSegment.vm.$emit('activate');
+
+ const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
+
+ expect(suggestions).toHaveLength(DEFAULT_NONE_ANY.length);
+ DEFAULT_NONE_ANY.forEach((contact, index) => {
+ expect(suggestions.at(index).text()).toBe(contact.text);
+ });
+ });
+
+ it('emits listeners in the base-token', () => {
+ const mockInput = jest.fn();
+ mountComponent({
+ listeners: {
+ input: mockInput,
+ },
+ });
+ wrapper.findComponent(BaseToken).vm.$emit('input', [{ data: 'mockData', operator: '=' }]);
+
+ expect(mockInput).toHaveBeenLastCalledWith([{ data: 'mockData', operator: '=' }]);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js
new file mode 100644
index 00000000000..977f8bbef61
--- /dev/null
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js
@@ -0,0 +1,282 @@
+import {
+ GlFilteredSearchSuggestion,
+ GlFilteredSearchTokenSegment,
+ GlDropdownDivider,
+} from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import createFlash from '~/flash';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { DEFAULT_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants';
+import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
+import CrmOrganizationToken from '~/vue_shared/components/filtered_search_bar/tokens/crm_organization_token.vue';
+import searchCrmOrganizationsQuery from '~/vue_shared/components/filtered_search_bar/queries/search_crm_organizations.query.graphql';
+
+import {
+ mockCrmOrganizations,
+ mockCrmOrganizationToken,
+ mockGroupCrmOrganizationsQueryResponse,
+ mockProjectCrmOrganizationsQueryResponse,
+} from '../mock_data';
+
+jest.mock('~/flash');
+
+const defaultStubs = {
+ Portal: true,
+ BaseToken,
+ GlFilteredSearchSuggestionList: {
+ template: '<div></div>',
+ methods: {
+ getValue: () => '=',
+ },
+ },
+};
+
+describe('CrmOrganizationToken', () => {
+ Vue.use(VueApollo);
+
+ let wrapper;
+ let fakeApollo;
+
+ const getBaseToken = () => wrapper.findComponent(BaseToken);
+
+ const searchGroupCrmOrganizationsQueryHandler = jest
+ .fn()
+ .mockResolvedValue(mockGroupCrmOrganizationsQueryResponse);
+ const searchProjectCrmOrganizationsQueryHandler = jest
+ .fn()
+ .mockResolvedValue(mockProjectCrmOrganizationsQueryResponse);
+
+ const mountComponent = ({
+ config = mockCrmOrganizationToken,
+ value = { data: '' },
+ active = false,
+ stubs = defaultStubs,
+ listeners = {},
+ queryHandler = searchGroupCrmOrganizationsQueryHandler,
+ } = {}) => {
+ fakeApollo = createMockApollo([[searchCrmOrganizationsQuery, queryHandler]]);
+ wrapper = mount(CrmOrganizationToken, {
+ propsData: {
+ config,
+ value,
+ active,
+ cursorPosition: 'start',
+ },
+ provide: {
+ portalName: 'fake target',
+ alignSuggestions: function fakeAlignSuggestions() {},
+ suggestionsListClass: () => 'custom-class',
+ },
+ stubs,
+ listeners,
+ apolloProvider: fakeApollo,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ fakeApollo = null;
+ });
+
+ describe('methods', () => {
+ describe('fetchOrganizations', () => {
+ describe('for groups', () => {
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it('calls the apollo query providing the searchString when search term is a string', async () => {
+ getBaseToken().vm.$emit('fetch-suggestions', 'foo');
+ await waitForPromises();
+
+ expect(createFlash).not.toHaveBeenCalled();
+ expect(searchGroupCrmOrganizationsQueryHandler).toHaveBeenCalledWith({
+ fullPath: 'group',
+ isProject: false,
+ searchString: 'foo',
+ searchIds: null,
+ });
+ expect(getBaseToken().props('suggestions')).toEqual(mockCrmOrganizations);
+ });
+
+ it('calls the apollo query providing the searchId when search term is a number', async () => {
+ getBaseToken().vm.$emit('fetch-suggestions', '5');
+ await waitForPromises();
+
+ expect(createFlash).not.toHaveBeenCalled();
+ expect(searchGroupCrmOrganizationsQueryHandler).toHaveBeenCalledWith({
+ fullPath: 'group',
+ isProject: false,
+ searchString: null,
+ searchIds: ['gid://gitlab/CustomerRelations::Organization/5'],
+ });
+ expect(getBaseToken().props('suggestions')).toEqual(mockCrmOrganizations);
+ });
+ });
+
+ describe('for projects', () => {
+ beforeEach(() => {
+ mountComponent({
+ config: {
+ fullPath: 'project',
+ isProject: true,
+ },
+ queryHandler: searchProjectCrmOrganizationsQueryHandler,
+ });
+ });
+
+ it('calls the apollo query providing the searchString when search term is a string', async () => {
+ getBaseToken().vm.$emit('fetch-suggestions', 'foo');
+ await waitForPromises();
+
+ expect(createFlash).not.toHaveBeenCalled();
+ expect(searchProjectCrmOrganizationsQueryHandler).toHaveBeenCalledWith({
+ fullPath: 'project',
+ isProject: true,
+ searchString: 'foo',
+ searchIds: null,
+ });
+ expect(getBaseToken().props('suggestions')).toEqual(mockCrmOrganizations);
+ });
+
+ it('calls the apollo query providing the searchId when search term is a number', async () => {
+ getBaseToken().vm.$emit('fetch-suggestions', '5');
+ await waitForPromises();
+
+ expect(createFlash).not.toHaveBeenCalled();
+ expect(searchProjectCrmOrganizationsQueryHandler).toHaveBeenCalledWith({
+ fullPath: 'project',
+ isProject: true,
+ searchString: null,
+ searchIds: ['gid://gitlab/CustomerRelations::Organization/5'],
+ });
+ expect(getBaseToken().props('suggestions')).toEqual(mockCrmOrganizations);
+ });
+ });
+
+ it('calls `createFlash` with flash error message when request fails', async () => {
+ mountComponent();
+
+ jest.spyOn(wrapper.vm.$apollo, 'query').mockRejectedValue({});
+
+ getBaseToken().vm.$emit('fetch-suggestions');
+ await waitForPromises();
+
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'There was a problem fetching CRM organizations.',
+ });
+ });
+
+ it('sets `loading` to false when request completes', async () => {
+ mountComponent();
+
+ jest.spyOn(wrapper.vm.$apollo, 'query').mockRejectedValue({});
+
+ getBaseToken().vm.$emit('fetch-suggestions');
+
+ await waitForPromises();
+
+ expect(getBaseToken().props('suggestionsLoading')).toBe(false);
+ });
+ });
+ });
+
+ describe('template', () => {
+ const defaultOrganizations = DEFAULT_NONE_ANY;
+
+ it('renders base-token component', () => {
+ mountComponent({
+ config: { ...mockCrmOrganizationToken, initialOrganizations: mockCrmOrganizations },
+ value: { data: '1' },
+ });
+
+ const baseTokenEl = wrapper.find(BaseToken);
+
+ expect(baseTokenEl.exists()).toBe(true);
+ expect(baseTokenEl.props()).toMatchObject({
+ suggestions: mockCrmOrganizations,
+ getActiveTokenValue: wrapper.vm.getActiveOrganization,
+ });
+ });
+
+ it.each(mockCrmOrganizations)('renders token item when value is selected', (organization) => {
+ mountComponent({
+ config: { ...mockCrmOrganizationToken, initialOrganizations: mockCrmOrganizations },
+ value: { data: `${getIdFromGraphQLId(organization.id)}` },
+ });
+
+ const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+
+ expect(tokenSegments).toHaveLength(3); // Organization, =, Organization name
+ expect(tokenSegments.at(2).text()).toBe(organization.name); // Organization name
+ });
+
+ it('renders provided defaultOrganizations as suggestions', async () => {
+ mountComponent({
+ active: true,
+ config: { ...mockCrmOrganizationToken, defaultOrganizations },
+ stubs: { Portal: true },
+ });
+ const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+ const suggestionsSegment = tokenSegments.at(2);
+ suggestionsSegment.vm.$emit('activate');
+ await nextTick();
+
+ const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
+
+ expect(suggestions).toHaveLength(defaultOrganizations.length);
+ defaultOrganizations.forEach((organization, index) => {
+ expect(suggestions.at(index).text()).toBe(organization.text);
+ });
+ });
+
+ it('does not render divider when no defaultOrganizations', async () => {
+ mountComponent({
+ active: true,
+ config: { ...mockCrmOrganizationToken, defaultOrganizations: [] },
+ stubs: { Portal: true },
+ });
+ const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+ const suggestionsSegment = tokenSegments.at(2);
+ suggestionsSegment.vm.$emit('activate');
+ await nextTick();
+
+ expect(wrapper.find(GlFilteredSearchSuggestion).exists()).toBe(false);
+ expect(wrapper.find(GlDropdownDivider).exists()).toBe(false);
+ });
+
+ it('renders `DEFAULT_NONE_ANY` as default suggestions', () => {
+ mountComponent({
+ active: true,
+ config: { ...mockCrmOrganizationToken },
+ stubs: { Portal: true },
+ });
+ const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+ const suggestionsSegment = tokenSegments.at(2);
+ suggestionsSegment.vm.$emit('activate');
+
+ const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
+
+ expect(suggestions).toHaveLength(DEFAULT_NONE_ANY.length);
+ DEFAULT_NONE_ANY.forEach((organization, index) => {
+ expect(suggestions.at(index).text()).toBe(organization.text);
+ });
+ });
+
+ it('emits listeners in the base-token', () => {
+ const mockInput = jest.fn();
+ mountComponent({
+ listeners: {
+ input: mockInput,
+ },
+ });
+ wrapper.findComponent(BaseToken).vm.$emit('input', [{ data: 'mockData', operator: '=' }]);
+
+ expect(mockInput).toHaveBeenLastCalledWith([{ data: 'mockData', operator: '=' }]);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/markdown/field_spec.js b/spec/frontend/vue_shared/components/markdown/field_spec.js
index b3376f26a25..85a135d2b89 100644
--- a/spec/frontend/vue_shared/components/markdown/field_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/field_spec.js
@@ -67,11 +67,6 @@ describe('Markdown field component', () => {
enablePreview,
restrictedToolBarItems,
},
- provide: {
- glFeatures: {
- contactsAutocomplete: true,
- },
- },
},
);
}
diff --git a/spec/frontend/vue_shared/components/notes/__snapshots__/placeholder_note_spec.js.snap b/spec/frontend/vue_shared/components/notes/__snapshots__/placeholder_note_spec.js.snap
index 5e956d66b6a..bf6c8e8c704 100644
--- a/spec/frontend/vue_shared/components/notes/__snapshots__/placeholder_note_spec.js.snap
+++ b/spec/frontend/vue_shared/components/notes/__snapshots__/placeholder_note_spec.js.snap
@@ -7,16 +7,19 @@ exports[`Issue placeholder note component matches snapshot 1`] = `
<div
class="timeline-icon"
>
- <user-avatar-link-stub
- imgalt=""
- imgcssclasses=""
- imgsize="40"
- imgsrc="mock_path"
- linkhref="/root"
- tooltipplacement="top"
- tooltiptext=""
- username=""
- />
+ <gl-avatar-link-stub
+ class="gl-mr-3"
+ href="/root"
+ >
+ <gl-avatar-stub
+ alt="Root"
+ entityid="0"
+ entityname="root"
+ shape="circle"
+ size="[object Object]"
+ src="mock_path"
+ />
+ </gl-avatar-link-stub>
</div>
<div
diff --git a/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js b/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js
index 6881cb79740..f951cfd5cd9 100644
--- a/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js
+++ b/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js
@@ -1,8 +1,8 @@
import { shallowMount } from '@vue/test-utils';
+import { GlAvatar } from '@gitlab/ui';
import Vue from 'vue';
import Vuex from 'vuex';
import IssuePlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue';
-import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import { userDataMock } from 'jest/notes/mock_data';
Vue.use(Vuex);
@@ -56,14 +56,14 @@ describe('Issue placeholder note component', () => {
describe('avatar size', () => {
it.each`
- size | line | isOverviewTab
- ${40} | ${null} | ${false}
- ${24} | ${{ line_code: '123' }} | ${false}
- ${40} | ${{ line_code: '123' }} | ${true}
+ size | line | isOverviewTab
+ ${{ default: 24, md: 32 }} | ${null} | ${false}
+ ${24} | ${{ line_code: '123' }} | ${false}
+ ${{ default: 24, md: 32 }} | ${{ line_code: '123' }} | ${true}
`('renders avatar $size for $line and $isOverviewTab', ({ size, line, isOverviewTab }) => {
createComponent(false, { line, isOverviewTab });
- expect(wrapper.findComponent(UserAvatarLink).props('imgSize')).toBe(size);
+ expect(wrapper.findComponent(GlAvatar).props('size')).toEqual(size);
});
});
});
diff --git a/spec/frontend/vue_shared/components/page_size_selector_spec.js b/spec/frontend/vue_shared/components/page_size_selector_spec.js
new file mode 100644
index 00000000000..5ec0b863afd
--- /dev/null
+++ b/spec/frontend/vue_shared/components/page_size_selector_spec.js
@@ -0,0 +1,44 @@
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import PageSizeSelector, { PAGE_SIZES } from '~/vue_shared/components/page_size_selector.vue';
+
+describe('Page size selector component', () => {
+ let wrapper;
+
+ const createWrapper = ({ pageSize = 20 } = {}) => {
+ wrapper = shallowMount(PageSizeSelector, {
+ propsData: { value: pageSize },
+ });
+ };
+
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it.each(PAGE_SIZES)('shows expected text in the dropdown button for page size %s', (pageSize) => {
+ createWrapper({ pageSize });
+
+ expect(findDropdown().props('text')).toBe(`Show ${pageSize} items`);
+ });
+
+ it('shows the expected dropdown items', () => {
+ createWrapper();
+
+ PAGE_SIZES.forEach((pageSize, index) => {
+ expect(findDropdownItems().at(index).text()).toBe(`Show ${pageSize} items`);
+ });
+ });
+
+ it('will emit the new page size when a dropdown item is clicked', () => {
+ createWrapper();
+
+ findDropdownItems().wrappers.forEach((itemWrapper, index) => {
+ itemWrapper.vm.$emit('click');
+
+ expect(wrapper.emitted('input')[index][0]).toBe(PAGE_SIZES[index]);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js b/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js
index 8270ff31574..51a936c0509 100644
--- a/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js
+++ b/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js
@@ -195,7 +195,7 @@ describe('AlertManagementEmptyState', () => {
tabs.forEach((tab, i) => {
const status = ITEMS_STATUS_TABS[i].status.toLowerCase();
expect(tab.attributes('data-testid')).toContain(ITEMS_STATUS_TABS[i].status);
- expect(badges.at(i).text()).toContain(itemsCount[status]);
+ expect(badges.at(i).text()).toContain(itemsCount[status].toString());
});
});
});
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 8ff49271eb5..2ea8985b16a 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
@@ -42,6 +42,7 @@ exports[`RunnerAwsDeploymentsModal renders the modal 1`] = `
>
<gl-accordion-item-stub
class="gl-font-weight-normal"
+ headerclass=""
title="More Details"
titlevisible="Less Details"
>
@@ -76,6 +77,7 @@ exports[`RunnerAwsDeploymentsModal renders the modal 1`] = `
>
<gl-accordion-item-stub
class="gl-font-weight-normal"
+ headerclass=""
title="More Details"
titlevisible="Less Details"
>
@@ -110,6 +112,7 @@ exports[`RunnerAwsDeploymentsModal renders the modal 1`] = `
>
<gl-accordion-item-stub
class="gl-font-weight-normal"
+ headerclass=""
title="More Details"
titlevisible="Less Details"
>
@@ -144,6 +147,7 @@ exports[`RunnerAwsDeploymentsModal renders the modal 1`] = `
>
<gl-accordion-item-stub
class="gl-font-weight-normal"
+ headerclass=""
title="More Details"
titlevisible="Less Details"
>
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 7173abe1316..a38dcd626f4 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
@@ -79,7 +79,7 @@ describe('RunnerInstructionsModal component', () => {
}
};
- beforeEach(async () => {
+ beforeEach(() => {
runnerPlatformsHandler = jest.fn().mockResolvedValue(mockGraphqlRunnerPlatforms);
runnerSetupInstructionsHandler = jest.fn().mockResolvedValue(mockGraphqlInstructions);
});
@@ -259,11 +259,11 @@ describe('RunnerInstructionsModal component', () => {
});
describe('when apollo is loading', () => {
- beforeEach(() => {
+ it('should show a skeleton loader', async () => {
createComponent();
- });
+ await nextTick();
+ await nextTick();
- it('should show a skeleton loader', async () => {
expect(findSkeletonLoader().exists()).toBe(true);
expect(findGlLoadingIcon().exists()).toBe(false);
@@ -275,6 +275,8 @@ describe('RunnerInstructionsModal component', () => {
});
it('once loaded, should not show a loading state', async () => {
+ createComponent();
+
await waitForPromises();
expect(findSkeletonLoader().exists()).toBe(false);
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/link_dependencies_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/link_dependencies_spec.js
new file mode 100644
index 00000000000..3036ce43888
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/link_dependencies_spec.js
@@ -0,0 +1,14 @@
+import packageJsonLinker from '~/vue_shared/components/source_viewer/plugins/utils/package_json_linker';
+import linkDependencies from '~/vue_shared/components/source_viewer/plugins/link_dependencies';
+import { PACKAGE_JSON_FILE_TYPE, PACKAGE_JSON_CONTENT } from './mock_data';
+
+jest.mock('~/vue_shared/components/source_viewer/plugins/utils/package_json_linker');
+
+describe('Highlight.js plugin for linking dependencies', () => {
+ const hljsResultMock = { value: 'test' };
+
+ it('calls packageJsonLinker for package_json file types', () => {
+ linkDependencies(hljsResultMock, PACKAGE_JSON_FILE_TYPE, PACKAGE_JSON_CONTENT);
+ expect(packageJsonLinker).toHaveBeenCalled();
+ });
+});
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/mock_data.js b/spec/frontend/vue_shared/components/source_viewer/plugins/mock_data.js
new file mode 100644
index 00000000000..75659770e2c
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/mock_data.js
@@ -0,0 +1,2 @@
+export const PACKAGE_JSON_FILE_TYPE = 'package_json';
+export const PACKAGE_JSON_CONTENT = '{ "dependencies": { "@babel/core": "^7.18.5" } }';
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util_spec.js
new file mode 100644
index 00000000000..ee200747af9
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util_spec.js
@@ -0,0 +1,33 @@
+import {
+ createLink,
+ generateHLJSOpenTag,
+} from '~/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util';
+
+describe('createLink', () => {
+ it('generates a link with the correct attributes', () => {
+ const href = 'http://test.com';
+ const innerText = 'testing';
+ const result = `<a href="${href}" rel="nofollow noreferrer noopener">${innerText}</a>`;
+
+ expect(createLink(href, innerText)).toBe(result);
+ });
+
+ it('escapes the user-controlled content', () => {
+ const unescapedXSS = '<script>XSS</script>';
+ const escapedXSS = '&amp;lt;script&amp;gt;XSS&amp;lt;/script&amp;gt;';
+ const href = `http://test.com/${unescapedXSS}`;
+ const innerText = `testing${unescapedXSS}`;
+ const result = `<a href="http://test.com/${escapedXSS}" rel="nofollow noreferrer noopener">testing${escapedXSS}</a>`;
+
+ expect(createLink(href, innerText)).toBe(result);
+ });
+});
+
+describe('generateHLJSOpenTag', () => {
+ it('generates an open tag with the correct selector', () => {
+ const type = 'string';
+ const result = `<span class="hljs-${type}">&quot;`;
+
+ expect(generateHLJSOpenTag(type)).toBe(result);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/utils/package_json_linker_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/package_json_linker_spec.js
new file mode 100644
index 00000000000..e83c129818c
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/package_json_linker_spec.js
@@ -0,0 +1,15 @@
+import packageJsonLinker from '~/vue_shared/components/source_viewer/plugins/utils/package_json_linker';
+import { PACKAGE_JSON_CONTENT } from '../mock_data';
+
+describe('Highlight.js plugin for linking package.json dependencies', () => {
+ it('mutates the input value by wrapping dependency names and versions in anchors', () => {
+ const inputValue =
+ '<span class="hljs-attr">&quot;@babel/core&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;^7.18.5&quot;</span>';
+ const outputValue =
+ '<span class="hljs-attr">&quot;<a href="https://npmjs.com/package/@babel/core" rel="nofollow noreferrer noopener">@babel/core</a>&quot;</span>: <span class="hljs-attr">&quot;<a href="https://npmjs.com/package/@babel/core" rel="nofollow noreferrer noopener">^7.18.5</a>&quot;</span>';
+ const hljsResultMock = { value: inputValue };
+
+ const output = packageJsonLinker(hljsResultMock, PACKAGE_JSON_CONTENT);
+ expect(output).toBe(outputValue);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js
index bb0945a1f3e..2c03b7aa7d3 100644
--- a/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js
@@ -5,10 +5,16 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer.vue';
import { registerPlugins } from '~/vue_shared/components/source_viewer/plugins/index';
import Chunk from '~/vue_shared/components/source_viewer/components/chunk.vue';
-import { ROUGE_TO_HLJS_LANGUAGE_MAP } from '~/vue_shared/components/source_viewer/constants';
+import {
+ EVENT_ACTION,
+ EVENT_LABEL_VIEWER,
+ EVENT_LABEL_FALLBACK,
+ ROUGE_TO_HLJS_LANGUAGE_MAP,
+} from '~/vue_shared/components/source_viewer/constants';
import waitForPromises from 'helpers/wait_for_promises';
import LineHighlighter from '~/blob/line_highlighter';
import eventHub from '~/notes/event_hub';
+import Tracking from '~/tracking';
jest.mock('~/blob/line_highlighter');
jest.mock('highlight.js/lib/core');
@@ -34,7 +40,8 @@ describe('Source Viewer component', () => {
const chunk2 = generateContent('// Some source code 2', 70);
const content = chunk1 + chunk2;
const path = 'some/path.js';
- const DEFAULT_BLOB_DATA = { language, rawTextBlob: content, path };
+ const fileType = 'javascript';
+ const DEFAULT_BLOB_DATA = { language, rawTextBlob: content, path, fileType };
const highlightedContent = `<span data-testid='test-highlighted' id='LC1'>${content}</span><span id='LC2'></span>`;
const createComponent = async (blob = {}) => {
@@ -52,17 +59,38 @@ describe('Source Viewer component', () => {
hljs.highlightAuto.mockImplementation(() => ({ value: highlightedContent }));
jest.spyOn(window, 'requestIdleCallback').mockImplementation(execImmediately);
jest.spyOn(eventHub, '$emit');
+ jest.spyOn(Tracking, 'event');
return createComponent();
});
afterEach(() => wrapper.destroy());
+ describe('event tracking', () => {
+ it('fires a tracking event when the component is created', () => {
+ const eventData = { label: EVENT_LABEL_VIEWER, property: language };
+ expect(Tracking.event).toHaveBeenCalledWith(undefined, EVENT_ACTION, eventData);
+ });
+
+ it('does not emit an error event when the language is supported', () => {
+ expect(wrapper.emitted('error')).toBeUndefined();
+ });
+
+ it('fires a tracking event and emits an error when the language is not supported', () => {
+ const unsupportedLanguage = 'apex';
+ const eventData = { label: EVENT_LABEL_FALLBACK, property: unsupportedLanguage };
+ createComponent({ language: unsupportedLanguage });
+
+ expect(Tracking.event).toHaveBeenCalledWith(undefined, EVENT_ACTION, eventData);
+ expect(wrapper.emitted('error')).toHaveLength(1);
+ });
+ });
+
describe('highlight.js', () => {
beforeEach(() => createComponent({ language: mappedLanguage }));
it('registers our plugins for Highlight.js', () => {
- expect(registerPlugins).toHaveBeenCalledWith(hljs);
+ expect(registerPlugins).toHaveBeenCalledWith(hljs, fileType, content);
});
it('registers the language definition', async () => {
@@ -74,6 +102,13 @@ describe('Source Viewer component', () => {
);
});
+ it('registers json language definition if fileType is package_json', async () => {
+ await createComponent({ language: 'json', fileType: 'package_json' });
+ const languageDefinition = await import(`highlight.js/lib/languages/json`);
+
+ expect(hljs.registerLanguage).toHaveBeenCalledWith('json', languageDefinition.default);
+ });
+
it('highlights the first chunk', () => {
expect(hljs.highlight).toHaveBeenCalledWith(chunk1.trim(), { language: mappedLanguage });
});
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 a54f3450633..9550368eefc 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
@@ -2,7 +2,6 @@ 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';
@@ -48,7 +47,6 @@ describe('User Popover Component', () => {
const findUserStatus = () => wrapper.findByTestId('user-popover-status');
const findTarget = () => document.querySelector('.js-user-link');
- 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');
@@ -245,9 +243,7 @@ describe('User Popover Component', () => {
createWrapper({ user });
- expect(findUserName().exists()).toBe(true);
- expect(wrapper.text()).toContain(user.name);
- expect(wrapper.text()).toContain('(Busy)');
+ expect(wrapper.findByText('(Busy)').exists()).toBe(true);
});
it('should hide the busy status for any other status', () => {
@@ -258,13 +254,32 @@ describe('User Popover Component', () => {
createWrapper({ user });
- expect(wrapper.text()).not.toContain('(Busy)');
+ expect(wrapper.findByText('(Busy)').exists()).toBe(false);
});
- it('passes `pronouns` prop to `UserNameWithStatus` component', () => {
+ it('shows pronouns when user has them set', () => {
createWrapper();
- expect(findUserName().props('pronouns')).toBe('they/them');
+ expect(wrapper.findByText('(they/them)').exists()).toBe(true);
+ });
+
+ describe.each`
+ pronouns
+ ${undefined}
+ ${null}
+ ${''}
+ ${' '}
+ `('when pronouns are set to $pronouns', ({ pronouns }) => {
+ it('does not render pronouns', () => {
+ const user = {
+ ...DEFAULT_PROPS.user,
+ pronouns,
+ };
+
+ createWrapper({ user });
+
+ expect(wrapper.findByTestId('user-popover-pronouns').exists()).toBe(false);
+ });
});
});