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
path: root/spec
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-08-09 18:11:41 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-08-09 18:11:41 +0300
commitd56569ff3e73ae1dbcf93d2530925c4ecb8fd185 (patch)
treef89e6dd59d8d807201a9dd3ca46b5eee0ea5f438 /spec
parent1faea1c6a0464e44dca4477fb31846938c2ad871 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/frontend/custom_emoji/components/delete_item_spec.js89
-rw-r--r--spec/frontend/custom_emoji/components/list_spec.js18
-rw-r--r--spec/frontend/custom_emoji/mock_data.js3
-rw-r--r--spec/frontend/work_items/components/shared/work_item_link_child_contents_spec.js179
-rw-r--r--spec/frontend/work_items/components/shared/work_item_link_child_metadata_spec.js (renamed from spec/frontend/work_items/components/work_item_links/work_item_link_child_metadata_spec.js)2
-rw-r--r--spec/frontend/work_items/components/shared/work_item_links_menu_spec.js (renamed from spec/frontend/work_items/components/work_item_links/work_item_links_menu_spec.js)2
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js169
-rw-r--r--spec/helpers/time_helper_spec.rb2
-rw-r--r--spec/lib/gitlab/blame_spec.rb6
-rw-r--r--spec/lib/gitlab/git/blame_spec.rb8
-rw-r--r--spec/lib/gitlab/gitaly_client/operation_service_spec.rb33
-rw-r--r--spec/services/merge_requests/create_ref_service_spec.rb164
12 files changed, 523 insertions, 152 deletions
diff --git a/spec/frontend/custom_emoji/components/delete_item_spec.js b/spec/frontend/custom_emoji/components/delete_item_spec.js
new file mode 100644
index 00000000000..06c4ca8d54b
--- /dev/null
+++ b/spec/frontend/custom_emoji/components/delete_item_spec.js
@@ -0,0 +1,89 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import * as Sentry from '@sentry/browser';
+import { GlModal } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { createAlert } from '~/alert';
+import DeleteItem from '~/custom_emoji/components/delete_item.vue';
+import deleteCustomEmojiMutation from '~/custom_emoji/queries/delete_custom_emoji.mutation.graphql';
+import { CUSTOM_EMOJI } from '../mock_data';
+
+jest.mock('~/alert');
+jest.mock('@sentry/browser');
+
+let wrapper;
+let deleteMutationSpy;
+
+Vue.use(VueApollo);
+
+function createSuccessSpy() {
+ deleteMutationSpy = jest.fn().mockResolvedValue({
+ data: { destroyCustomEmoji: { customEmoji: { id: CUSTOM_EMOJI[0].id } } },
+ });
+}
+
+function createErrorSpy() {
+ deleteMutationSpy = jest.fn().mockRejectedValue();
+}
+
+function createMockApolloProvider() {
+ const requestHandlers = [[deleteCustomEmojiMutation, deleteMutationSpy]];
+
+ return createMockApollo(requestHandlers);
+}
+
+function createComponent() {
+ const apolloProvider = createMockApolloProvider();
+
+ wrapper = mountExtended(DeleteItem, {
+ apolloProvider,
+ propsData: {
+ emoji: CUSTOM_EMOJI[0],
+ },
+ });
+}
+
+const findDeleteButton = () => wrapper.findByTestId('delete-button');
+const findModal = () => wrapper.findComponent(GlModal);
+
+describe('Custom emoji delete item component', () => {
+ it('opens modal when clicking button', async () => {
+ createSuccessSpy();
+ createComponent();
+
+ await findDeleteButton().trigger('click');
+
+ expect(document.querySelector('.gl-modal')).not.toBe(null);
+ });
+
+ it('calls GraphQL mutation on modals primary action', () => {
+ createSuccessSpy();
+ createComponent();
+
+ findModal().vm.$emit('primary');
+
+ expect(deleteMutationSpy).toHaveBeenCalledWith({ id: CUSTOM_EMOJI[0].id });
+ });
+
+ it('creates alert when mutation fails', async () => {
+ createErrorSpy();
+ createComponent();
+
+ findModal().vm.$emit('primary');
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith('Failed to delete custom emoji. Please try again.');
+ });
+
+ it('calls sentry when mutation fails', async () => {
+ createErrorSpy();
+ createComponent();
+
+ findModal().vm.$emit('primary');
+ await waitForPromises();
+
+ expect(Sentry.captureException).toHaveBeenCalled();
+ });
+});
diff --git a/spec/frontend/custom_emoji/components/list_spec.js b/spec/frontend/custom_emoji/components/list_spec.js
index 2bffe367435..b5729d59464 100644
--- a/spec/frontend/custom_emoji/components/list_spec.js
+++ b/spec/frontend/custom_emoji/components/list_spec.js
@@ -1,6 +1,7 @@
import Vue from 'vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import List from '~/custom_emoji/components/list.vue';
+import DeleteItem from '~/custom_emoji/components/delete_item.vue';
import { CUSTOM_EMOJI } from '../mock_data';
jest.mock('~/lib/utils/datetime/date_format_utility', () => ({
@@ -58,4 +59,21 @@ describe('Custom emoji settings list component', () => {
expect(wrapper.emitted('input')[0]).toEqual([emits]);
});
});
+
+ describe('delete button', () => {
+ it.each`
+ deleteCustomEmoji | rendersText | renders
+ ${true} | ${'renders'} | ${true}
+ ${false} | ${'does not render'} | ${false}
+ `(
+ '$rendersText delete button when deleteCustomEmoji is $deleteCustomEmoji',
+ ({ deleteCustomEmoji, renders }) => {
+ createComponent({
+ customEmojis: [{ ...CUSTOM_EMOJI[0], userPermissions: { deleteCustomEmoji } }],
+ });
+
+ expect(wrapper.findComponent(DeleteItem).exists()).toBe(renders);
+ },
+ );
+ });
});
diff --git a/spec/frontend/custom_emoji/mock_data.js b/spec/frontend/custom_emoji/mock_data.js
index 9936274d71c..f2b32bf1cfb 100644
--- a/spec/frontend/custom_emoji/mock_data.js
+++ b/spec/frontend/custom_emoji/mock_data.js
@@ -4,6 +4,9 @@ export const CUSTOM_EMOJI = [
name: 'confused_husky',
url: 'https://gitlab.com/custom_emoji/custom_emoji/-/raw/main/img/confused_husky.gif',
createdAt: 'created-at',
+ userPermissions: {
+ deleteCustomEmoji: false,
+ },
},
];
diff --git a/spec/frontend/work_items/components/shared/work_item_link_child_contents_spec.js b/spec/frontend/work_items/components/shared/work_item_link_child_contents_spec.js
new file mode 100644
index 00000000000..9a20e2ec98f
--- /dev/null
+++ b/spec/frontend/work_items/components/shared/work_item_link_child_contents_spec.js
@@ -0,0 +1,179 @@
+import { GlLabel, GlIcon } from '@gitlab/ui';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+import WorkItemLinkChildMetadata from 'ee_else_ce/work_items/components/shared/work_item_link_child_metadata.vue';
+
+import { createAlert } from '~/alert';
+import RichTimestampTooltip from '~/vue_shared/components/rich_timestamp_tooltip.vue';
+
+import WorkItemLinkChildContents from '~/work_items/components/shared/work_item_link_child_contents.vue';
+import WorkItemLinksMenu from '~/work_items/components/shared/work_item_links_menu.vue';
+import { TASK_TYPE_NAME, WORK_ITEM_TYPE_VALUE_OBJECTIVE } from '~/work_items/constants';
+
+import {
+ workItemTask,
+ workItemObjectiveWithChild,
+ workItemObjectiveNoMetadata,
+ confidentialWorkItemTask,
+ closedWorkItemTask,
+ workItemObjectiveMetadataWidgets,
+} from '../../mock_data';
+
+jest.mock('~/alert');
+
+describe('WorkItemLinkChildContents', () => {
+ Vue.use(VueApollo);
+
+ const WORK_ITEM_ID = 'gid://gitlab/WorkItem/2';
+ let wrapper;
+ const { LABELS } = workItemObjectiveMetadataWidgets;
+ const mockLabels = LABELS.labels.nodes;
+ const mockFullPath = 'gitlab-org/gitlab-test';
+
+ const findStatusIconComponent = () =>
+ wrapper.findByTestId('item-status-icon').findComponent(GlIcon);
+ const findConfidentialIconComponent = () => wrapper.findByTestId('confidential-icon');
+ const findTitleEl = () => wrapper.findByTestId('item-title');
+ const findStatusTooltipComponent = () => wrapper.findComponent(RichTimestampTooltip);
+ const findMetadataComponent = () => wrapper.findComponent(WorkItemLinkChildMetadata);
+ const findAllLabels = () => wrapper.findAllComponents(GlLabel);
+ const findRegularLabel = () => findAllLabels().at(0);
+ const findScopedLabel = () => findAllLabels().at(1);
+ const findLinksMenuComponent = () => wrapper.findComponent(WorkItemLinksMenu);
+
+ const createComponent = ({
+ canUpdate = true,
+ parentWorkItemId = WORK_ITEM_ID,
+ childItem = workItemTask,
+ workItemType = TASK_TYPE_NAME,
+ } = {}) => {
+ wrapper = shallowMountExtended(WorkItemLinkChildContents, {
+ propsData: {
+ canUpdate,
+ parentWorkItemId,
+ childItem,
+ workItemType,
+ fullPath: mockFullPath,
+ childPath: '/gitlab-org/gitlab-test/-/work_items/4',
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createAlert.mockClear();
+ });
+
+ it.each`
+ status | childItem | statusIconName | statusIconColorClass | rawTimestamp | tooltipContents
+ ${'open'} | ${workItemTask} | ${'issue-open-m'} | ${'gl-text-green-500'} | ${workItemTask.createdAt} | ${'Created'}
+ ${'closed'} | ${closedWorkItemTask} | ${'issue-close'} | ${'gl-text-blue-500'} | ${closedWorkItemTask.closedAt} | ${'Closed'}
+ `(
+ 'renders item status icon and tooltip when item status is `$status`',
+ ({ childItem, statusIconName, statusIconColorClass, rawTimestamp, tooltipContents }) => {
+ createComponent({ childItem });
+
+ expect(findStatusIconComponent().props('name')).toBe(statusIconName);
+ expect(findStatusIconComponent().classes()).toContain(statusIconColorClass);
+ expect(findStatusTooltipComponent().props('rawTimestamp')).toBe(rawTimestamp);
+ expect(findStatusTooltipComponent().props('timestampTypeText')).toContain(tooltipContents);
+ },
+ );
+
+ it('renders confidential icon when item is confidential', () => {
+ createComponent({ childItem: confidentialWorkItemTask });
+
+ expect(findConfidentialIconComponent().props('name')).toBe('eye-slash');
+ expect(findConfidentialIconComponent().attributes('title')).toBe('Confidential');
+ });
+
+ describe('item title', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders item title', () => {
+ expect(findTitleEl().attributes('href')).toBe('/gitlab-org/gitlab-test/-/work_items/4');
+ expect(findTitleEl().text()).toBe(workItemTask.title);
+ });
+
+ it.each`
+ action | event | emittedEvent
+ ${'on mouseover'} | ${'mouseover'} | ${'mouseover'}
+ ${'on mouseout'} | ${'mouseout'} | ${'mouseout'}
+ `('$action item title emit `$emittedEvent` event', ({ event, emittedEvent }) => {
+ findTitleEl().vm.$emit(event);
+
+ expect(wrapper.emitted(emittedEvent)).toEqual([[]]);
+ });
+
+ it('emits click event with correct parameters on clicking title', () => {
+ const eventObj = {
+ preventDefault: jest.fn(),
+ };
+ findTitleEl().vm.$emit('click', eventObj);
+
+ expect(wrapper.emitted('click')).toEqual([[eventObj]]);
+ });
+ });
+
+ describe('item metadata', () => {
+ beforeEach(() => {
+ createComponent({
+ childItem: workItemObjectiveWithChild,
+ workItemType: WORK_ITEM_TYPE_VALUE_OBJECTIVE,
+ });
+ });
+
+ it('renders item metadata component when item has metadata present', () => {
+ expect(findMetadataComponent().props()).toMatchObject({
+ metadataWidgets: workItemObjectiveMetadataWidgets,
+ });
+ });
+
+ it('does not render item metadata component when item has no metadata present', () => {
+ createComponent({
+ childItem: workItemObjectiveNoMetadata,
+ workItemType: WORK_ITEM_TYPE_VALUE_OBJECTIVE,
+ });
+
+ expect(findMetadataComponent().exists()).toBe(false);
+ });
+
+ it('renders labels', () => {
+ const mockLabel = mockLabels[0];
+
+ expect(findAllLabels()).toHaveLength(mockLabels.length);
+ expect(findRegularLabel().props()).toMatchObject({
+ title: mockLabel.title,
+ backgroundColor: mockLabel.color,
+ description: mockLabel.description,
+ scoped: false,
+ });
+ expect(findScopedLabel().props('scoped')).toBe(true); // Second label is scoped
+ });
+ });
+
+ describe('item menu', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders work-item-links-menu', () => {
+ expect(findLinksMenuComponent().exists()).toBe(true);
+ });
+
+ it('does not render work-item-links-menu when canUpdate is false', () => {
+ createComponent({ canUpdate: false });
+
+ expect(findLinksMenuComponent().exists()).toBe(false);
+ });
+
+ it('removeChild event on menu triggers `click-remove-child` event', () => {
+ findLinksMenuComponent().vm.$emit('removeChild');
+
+ expect(wrapper.emitted('removeChild')).toEqual([[workItemTask]]);
+ });
+ });
+});
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_link_child_metadata_spec.js b/spec/frontend/work_items/components/shared/work_item_link_child_metadata_spec.js
index 07efb1c5ac8..25ef0e69a40 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_link_child_metadata_spec.js
+++ b/spec/frontend/work_items/components/shared/work_item_link_child_metadata_spec.js
@@ -3,7 +3,7 @@ import { GlAvatarsInline } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ItemMilestone from '~/issuable/components/issue_milestone.vue';
-import WorkItemLinkChildMetadata from '~/work_items/components/work_item_links/work_item_link_child_metadata.vue';
+import WorkItemLinkChildMetadata from '~/work_items/components/shared/work_item_link_child_metadata.vue';
import { workItemObjectiveMetadataWidgets } from '../../mock_data';
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_menu_spec.js b/spec/frontend/work_items/components/shared/work_item_links_menu_spec.js
index f02a9fbd021..721db6c3315 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_links_menu_spec.js
+++ b/spec/frontend/work_items/components/shared/work_item_links_menu_spec.js
@@ -1,7 +1,7 @@
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import WorkItemLinksMenu from '~/work_items/components/work_item_links/work_item_links_menu.vue';
+import WorkItemLinksMenu from '~/work_items/components/shared/work_item_links_menu.vue';
describe('WorkItemLinksMenu', () => {
let wrapper;
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js
index 71d1a0e253f..803ff950cbe 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js
@@ -1,20 +1,16 @@
-import { GlLabel, GlIcon } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import WorkItemLinkChildMetadata from 'ee_else_ce/work_items/components/work_item_links/work_item_link_child_metadata.vue';
-
import { createAlert } from '~/alert';
-import RichTimestampTooltip from '~/vue_shared/components/rich_timestamp_tooltip.vue';
import getWorkItemTreeQuery from '~/work_items/graphql/work_item_tree.query.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import WorkItemLinkChild from '~/work_items/components/work_item_links/work_item_link_child.vue';
-import WorkItemLinksMenu from '~/work_items/components/work_item_links/work_item_links_menu.vue';
import WorkItemTreeChildren from '~/work_items/components/work_item_links/work_item_tree_children.vue';
+import WorkItemLinkChildContents from '~/work_items/components/shared/work_item_link_child_contents.vue';
import {
WIDGET_TYPE_HIERARCHY,
TASK_TYPE_NAME,
@@ -24,12 +20,8 @@ import {
import {
workItemTask,
workItemObjectiveWithChild,
- workItemObjectiveNoMetadata,
- confidentialWorkItemTask,
- closedWorkItemTask,
workItemHierarchyTreeResponse,
workItemHierarchyTreeFailureResponse,
- workItemObjectiveMetadataWidgets,
changeIndirectWorkItemParentMutationResponse,
workItemUpdateFailureResponse,
} from '../../mock_data';
@@ -41,8 +33,6 @@ describe('WorkItemLinkChild', () => {
let wrapper;
let getWorkItemTreeQueryHandler;
let mutationChangeParentHandler;
- const { LABELS } = workItemObjectiveMetadataWidgets;
- const mockLabels = LABELS.labels.nodes;
const $toast = {
show: jest.fn(),
@@ -51,6 +41,8 @@ describe('WorkItemLinkChild', () => {
Vue.use(VueApollo);
+ const findWorkItemLinkChildContents = () => wrapper.findComponent(WorkItemLinkChildContents);
+
const createComponent = ({
canUpdate = true,
issuableGid = WORK_ITEM_ID,
@@ -89,87 +81,7 @@ describe('WorkItemLinkChild', () => {
createAlert.mockClear();
});
- it.each`
- status | childItem | statusIconName | statusIconColorClass | rawTimestamp | tooltipContents
- ${'open'} | ${workItemTask} | ${'issue-open-m'} | ${'gl-text-green-500'} | ${workItemTask.createdAt} | ${'Created'}
- ${'closed'} | ${closedWorkItemTask} | ${'issue-close'} | ${'gl-text-blue-500'} | ${closedWorkItemTask.closedAt} | ${'Closed'}
- `(
- 'renders item status icon and tooltip when item status is `$status`',
- ({ childItem, statusIconName, statusIconColorClass, rawTimestamp, tooltipContents }) => {
- createComponent({ childItem });
-
- const statusIcon = wrapper.findByTestId('item-status-icon').findComponent(GlIcon);
- const statusTooltip = wrapper.findComponent(RichTimestampTooltip);
-
- expect(statusIcon.props('name')).toBe(statusIconName);
- expect(statusIcon.classes()).toContain(statusIconColorClass);
- expect(statusTooltip.props('rawTimestamp')).toBe(rawTimestamp);
- expect(statusTooltip.props('timestampTypeText')).toContain(tooltipContents);
- },
- );
-
- it('renders confidential icon when item is confidential', () => {
- createComponent({ childItem: confidentialWorkItemTask });
-
- const confidentialIcon = wrapper.findByTestId('confidential-icon');
-
- expect(confidentialIcon.props('name')).toBe('eye-slash');
- expect(confidentialIcon.attributes('title')).toBe('Confidential');
- });
-
- describe('item title', () => {
- let titleEl;
-
- beforeEach(() => {
- createComponent();
-
- titleEl = wrapper.findByTestId('item-title');
- });
-
- it('renders item title', () => {
- expect(titleEl.attributes('href')).toBe('/gitlab-org/gitlab-test/-/work_items/4');
- expect(titleEl.text()).toBe(workItemTask.title);
- });
-
- describe('renders item title correctly for relative instance', () => {
- beforeEach(() => {
- window.gon = { relative_url_root: '/test' };
- createComponent();
- titleEl = wrapper.findByTestId('item-title');
- });
-
- it('renders item title with correct href', () => {
- expect(titleEl.attributes('href')).toBe('/test/gitlab-org/gitlab-test/-/work_items/4');
- });
-
- it('renders item title with correct text', () => {
- expect(titleEl.text()).toBe(workItemTask.title);
- });
- });
-
- it.each`
- action | event | emittedEvent
- ${'doing mouseover on'} | ${'mouseover'} | ${'mouseover'}
- ${'doing mouseout on'} | ${'mouseout'} | ${'mouseout'}
- `('$action item title emit `$emittedEvent` event', ({ event, emittedEvent }) => {
- titleEl.vm.$emit(event);
-
- expect(wrapper.emitted(emittedEvent)).toEqual([[]]);
- });
-
- it('emits click event with correct parameters on clicking title', () => {
- const eventObj = {
- preventDefault: jest.fn(),
- };
- titleEl.vm.$emit('click', eventObj);
-
- expect(wrapper.emitted('click')).toEqual([[eventObj]]);
- });
- });
-
- describe('item metadata', () => {
- const findMetadataComponent = () => wrapper.findComponent(WorkItemLinkChildMetadata);
-
+ describe('renders WorkItemLinkChildContents', () => {
beforeEach(() => {
createComponent({
childItem: workItemObjectiveWithChild,
@@ -177,67 +89,31 @@ describe('WorkItemLinkChild', () => {
});
});
- it('renders item metadata component when item has metadata present', () => {
- const metadataEl = findMetadataComponent();
- expect(metadataEl.exists()).toBe(true);
- expect(metadataEl.props()).toMatchObject({
- metadataWidgets: workItemObjectiveMetadataWidgets,
- });
- });
-
- it('does not render item metadata component when item has no metadata present', () => {
- createComponent({
- childItem: workItemObjectiveNoMetadata,
- workItemType: WORK_ITEM_TYPE_VALUE_OBJECTIVE,
+ it('with default props', () => {
+ expect(findWorkItemLinkChildContents().props()).toEqual({
+ childItem: workItemObjectiveWithChild,
+ canUpdate: true,
+ parentWorkItemId: 'gid://gitlab/WorkItem/2',
+ workItemType: 'Objective',
+ childPath: '/gitlab-org/gitlab-test/-/work_items/12',
});
-
- expect(findMetadataComponent().exists()).toBe(false);
});
- it('renders labels', () => {
- const labels = wrapper.findAllComponents(GlLabel);
- const mockLabel = mockLabels[0];
-
- expect(labels).toHaveLength(mockLabels.length);
- expect(labels.at(0).props()).toMatchObject({
- title: mockLabel.title,
- backgroundColor: mockLabel.color,
- description: mockLabel.description,
- scoped: false,
+ describe('with relative instance', () => {
+ beforeEach(() => {
+ window.gon = { relative_url_root: '/test' };
+ createComponent({
+ childItem: workItemObjectiveWithChild,
+ workItemType: WORK_ITEM_TYPE_VALUE_OBJECTIVE,
+ });
});
- expect(labels.at(1).props('scoped')).toBe(true); // Second label is scoped
- });
- });
-
- describe('item menu', () => {
- let itemMenuEl;
-
- beforeEach(() => {
- createComponent();
-
- itemMenuEl = wrapper.findComponent(WorkItemLinksMenu);
- });
- it('renders work-item-links-menu', () => {
- expect(itemMenuEl.exists()).toBe(true);
-
- expect(itemMenuEl.attributes()).toMatchObject({
- 'work-item-id': workItemTask.id,
- 'parent-work-item-id': WORK_ITEM_ID,
+ it('adds the relative url to child path value', () => {
+ expect(findWorkItemLinkChildContents().props('childPath')).toBe(
+ '/test/gitlab-org/gitlab-test/-/work_items/12',
+ );
});
});
-
- it('does not render work-item-links-menu when canUpdate is false', () => {
- createComponent({ canUpdate: false });
-
- expect(wrapper.findComponent(WorkItemLinksMenu).exists()).toBe(false);
- });
-
- it('removeChild event on menu triggers `click-remove-child` event', () => {
- itemMenuEl.vm.$emit('removeChild');
-
- expect(wrapper.emitted('removeChild')).toEqual([[workItemTask]]);
- });
});
describe('nested children', () => {
@@ -252,7 +128,6 @@ describe('WorkItemLinkChild', () => {
const findFirstItem = () => getChildrenNodes()[0];
beforeEach(() => {
- getWorkItemTreeQueryHandler.mockClear();
createComponent({
childItem: workItemObjectiveWithChild,
workItemType: WORK_ITEM_TYPE_VALUE_OBJECTIVE,
diff --git a/spec/helpers/time_helper_spec.rb b/spec/helpers/time_helper_spec.rb
index 3e406f5e74e..02e28b2ba05 100644
--- a/spec/helpers/time_helper_spec.rb
+++ b/spec/helpers/time_helper_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe TimeHelper do
100.32 => "1 minute and 40 seconds",
120 => "2 minutes",
121 => "2 minutes and 1 second",
- 3721 => "62 minutes and 1 second",
+ 3721 => "1 hour, 2 minutes, and 1 second",
0 => "0 seconds"
}
diff --git a/spec/lib/gitlab/blame_spec.rb b/spec/lib/gitlab/blame_spec.rb
index f636ce283ae..bfe2b7d1360 100644
--- a/spec/lib/gitlab/blame_spec.rb
+++ b/spec/lib/gitlab/blame_spec.rb
@@ -33,12 +33,18 @@ RSpec.describe Gitlab::Blame do
expect(subject.count).to eq(18)
expect(subject[0][:commit].sha).to eq('913c66a37b4a45b9769037c55c2d238bd0942d2e')
expect(subject[0][:lines]).to eq(["require 'fileutils'", "require 'open3'", ""])
+ expect(subject[0][:span]).to eq(3)
+ expect(subject[0][:lineno]).to eq(1)
expect(subject[1][:commit].sha).to eq('874797c3a73b60d2187ed6e2fcabd289ff75171e')
expect(subject[1][:lines]).to eq(["module Popen", " extend self"])
+ expect(subject[1][:span]).to eq(2)
+ expect(subject[1][:lineno]).to eq(4)
expect(subject[-1][:commit].sha).to eq('913c66a37b4a45b9769037c55c2d238bd0942d2e')
expect(subject[-1][:lines]).to eq([" end", "end"])
+ expect(subject[-1][:span]).to eq(2)
+ expect(subject[-1][:lineno]).to eq(36)
end
context 'with a range 1..5' do
diff --git a/spec/lib/gitlab/git/blame_spec.rb b/spec/lib/gitlab/git/blame_spec.rb
index 676ea2663d2..d21ac36bf34 100644
--- a/spec/lib/gitlab/git/blame_spec.rb
+++ b/spec/lib/gitlab/git/blame_spec.rb
@@ -13,13 +13,17 @@ RSpec.describe Gitlab::Git::Blame do
let(:result) do
[].tap do |data|
- blame.each do |commit, line, previous_path|
- data << { commit: commit, line: line, previous_path: previous_path }
+ blame.each do |commit, line, previous_path, span|
+ data << { commit: commit, line: line, previous_path: previous_path, span: span }
end
end
end
describe 'blaming a file' do
+ it 'has the right commit span' do
+ expect(result.first[:span]).to eq(95)
+ end
+
it 'has the right number of lines' do
expect(result.size).to eq(95)
expect(result.first[:commit]).to be_kind_of(Gitlab::Git::Commit)
diff --git a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
index f8b452b157a..9055b284119 100644
--- a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
@@ -731,6 +731,39 @@ RSpec.describe Gitlab::GitalyClient::OperationService, feature_category: :source
end
end
+ describe '#user_rebase_to_ref' do
+ let(:first_parent_ref) { 'refs/heads/my-branch' }
+ let(:source_sha) { 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660' }
+ let(:target_ref) { 'refs/merge-requests/x/merge' }
+ let(:response) { Gitaly::UserRebaseToRefResponse.new(commit_id: 'new-commit-id') }
+
+ let(:payload) do
+ { source_sha: source_sha, target_ref: target_ref, first_parent_ref: first_parent_ref }
+ end
+
+ it 'sends a user_rebase_to_ref message' do
+ freeze_time do
+ expect_any_instance_of(Gitaly::OperationService::Stub).to receive(:user_rebase_to_ref) do |_, request, options|
+ expect(options).to be_kind_of(Hash)
+ expect(request.to_h).to(
+ eq(
+ payload.merge(
+ {
+ expected_old_oid: "",
+ repository: repository.gitaly_repository.to_h,
+ user: Gitlab::Git::User.from_gitlab(user).to_gitaly.to_h,
+ timestamp: { nanos: 0, seconds: Time.current.to_i }
+ }
+ )
+ )
+ )
+ end.and_return(response)
+
+ client.user_rebase_to_ref(user, **payload)
+ end
+ end
+ end
+
describe '#user_squash' do
let(:start_sha) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' }
let(:end_sha) { '54cec5282aa9f21856362fe321c800c236a61615' }
diff --git a/spec/services/merge_requests/create_ref_service_spec.rb b/spec/services/merge_requests/create_ref_service_spec.rb
new file mode 100644
index 00000000000..1d073cd143e
--- /dev/null
+++ b/spec/services/merge_requests/create_ref_service_spec.rb
@@ -0,0 +1,164 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe MergeRequests::CreateRefService, feature_category: :merge_trains do
+ using RSpec::Parameterized::TableSyntax
+
+ describe '#execute' do
+ let_it_be(:project) { create(:project, :empty_repo) }
+ let_it_be(:user) { project.creator }
+ let_it_be(:first_parent_ref) { project.default_branch_or_main }
+ let_it_be(:source_branch) { 'branch' }
+ let(:target_ref) { "refs/merge-requests/#{merge_request.iid}/train" }
+ let(:source_sha) { project.commit(source_branch).sha }
+ let(:squash) { false }
+
+ let(:merge_request) do
+ create(
+ :merge_request,
+ title: 'Merge request ref test',
+ author: user,
+ source_project: project,
+ target_project: project,
+ source_branch: source_branch,
+ target_branch: first_parent_ref,
+ squash: squash
+ )
+ end
+
+ subject(:result) do
+ described_class.new(
+ current_user: user,
+ merge_request: merge_request,
+ target_ref: target_ref,
+ source_sha: source_sha,
+ first_parent_ref: first_parent_ref
+ ).execute
+ end
+
+ context 'when there is a user-caused gitaly error' do
+ let(:source_sha) { '123' }
+
+ it 'returns an error response' do
+ expect(result[:status]).to eq :error
+ end
+ end
+
+ context 'with valid inputs' do
+ before_all do
+ # ensure first_parent_ref is created before source_sha
+ project.repository.create_file(
+ user,
+ 'README.md',
+ '',
+ message: 'Base parent commit 1',
+ branch_name: first_parent_ref
+ )
+ project.repository.create_branch(source_branch, first_parent_ref)
+
+ # create two commits source_branch to test squashing
+ project.repository.create_file(
+ user,
+ '.gitlab-ci.yml',
+ '',
+ message: 'Feature branch commit 1',
+ branch_name: source_branch
+ )
+
+ project.repository.create_file(
+ user,
+ '.gitignore',
+ '',
+ message: 'Feature branch commit 2',
+ branch_name: source_branch
+ )
+
+ # create an extra commit not present on source_branch
+ project.repository.create_file(
+ user,
+ 'EXTRA',
+ '',
+ message: 'Base parent commit 2',
+ branch_name: first_parent_ref
+ )
+ end
+
+ it 'writes the merged result into target_ref', :aggregate_failures do
+ expect(result[:status]).to eq :success
+ expect(project.repository.commits(target_ref, limit: 10, order: 'topo').map(&:message)).to(
+ match(
+ [
+ a_string_matching(/Merge branch '#{source_branch}' into '#{first_parent_ref}'/),
+ 'Feature branch commit 2',
+ 'Feature branch commit 1',
+ 'Base parent commit 2',
+ 'Base parent commit 1'
+ ]
+ )
+ )
+ end
+
+ context 'when squash is requested' do
+ let(:squash) { true }
+
+ it 'writes the squashed result', :aggregate_failures do
+ expect(result[:status]).to eq :success
+ expect(project.repository.commits(target_ref, limit: 10, order: 'topo').map(&:message)).to(
+ match(
+ [
+ a_string_matching(/Merge branch '#{source_branch}' into '#{first_parent_ref}'/),
+ "#{merge_request.title}\n",
+ 'Base parent commit 2',
+ 'Base parent commit 1'
+ ]
+ )
+ )
+ end
+ end
+
+ context 'when semi-linear merges are enabled' do
+ before do
+ project.merge_method = :rebase_merge
+ project.save!
+ end
+
+ it 'writes the semi-linear merged result', :aggregate_failures do
+ expect(result[:status]).to eq :success
+ expect(project.repository.commits(target_ref, limit: 10, order: 'topo').map(&:message)).to(
+ match(
+ [
+ a_string_matching(/Merge branch '#{source_branch}' into '#{first_parent_ref}'/),
+ 'Feature branch commit 2',
+ 'Feature branch commit 1',
+ 'Base parent commit 2',
+ 'Base parent commit 1'
+ ]
+ )
+ )
+ end
+ end
+
+ context 'when fast-forward merges are enabled' do
+ before do
+ project.merge_method = :ff
+ project.save!
+ end
+
+ it 'writes the rebased merged result', :aggregate_failures do
+ expect(result[:status]).to eq :success
+ expect(project.repository.commits(target_ref, limit: 10, order: 'topo').map(&:message)).to(
+ eq(
+ [
+ 'Feature branch commit 2',
+ 'Feature branch commit 1',
+ 'Base parent commit 2',
+ 'Base parent commit 1'
+ ]
+ )
+ )
+ end
+ end
+ end
+ end
+end