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-12-20 12:10:35 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-12-20 12:10:35 +0300
commit0b7d3f810f287523fd4ad72c019d78e8d2983f8a (patch)
treefdb52060e586a5548f026b6a3ce71c5b676d9e5c /spec
parent51da6793bcbe7295c1f3f881f756b8b60ee06da1 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/factories/users/in_product_marketing_email.rb10
-rw-r--r--spec/frontend/lib/utils/secret_detection_spec.js1
-rw-r--r--spec/frontend/organizations/shared/components/groups_view_spec.js10
-rw-r--r--spec/frontend/organizations/shared/components/projects_view_spec.js10
-rw-r--r--spec/frontend/vue_shared/components/groups_list/groups_list_item_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/groups_list/groups_list_spec.js7
-rw-r--r--spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js3
-rw-r--r--spec/frontend/vue_shared/components/projects_list/projects_list_spec.js7
-rw-r--r--spec/frontend/work_items/components/work_item_description_rendered_spec.js15
-rw-r--r--spec/frontend/work_items/components/work_item_description_spec.js36
-rw-r--r--spec/frontend/work_items/components/work_item_detail_spec.js64
-rw-r--r--spec/frontend/work_items/components/work_item_with_title_edit_spec.js59
-rw-r--r--spec/lib/gitlab/hook_data/project_builder_spec.rb26
-rw-r--r--spec/lib/gitlab/usage/service_ping_report_spec.rb5
-rw-r--r--spec/models/user_spec.rb1
-rw-r--r--spec/models/users/in_product_marketing_email_spec.rb137
-rw-r--r--spec/services/work_items/callbacks/assignees_spec.rb (renamed from spec/services/work_items/widgets/assignees_service/update_service_spec.rb)27
-rw-r--r--spec/support/helpers/database/duplicate_indexes.yml3
-rw-r--r--spec/support/shared_examples/features/work_items_shared_examples.rb92
19 files changed, 301 insertions, 217 deletions
diff --git a/spec/factories/users/in_product_marketing_email.rb b/spec/factories/users/in_product_marketing_email.rb
deleted file mode 100644
index c86c469ff31..00000000000
--- a/spec/factories/users/in_product_marketing_email.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# frozen_string_literal: true
-
-FactoryBot.define do
- factory :in_product_marketing_email, class: 'Users::InProductMarketingEmail' do
- user
-
- track { 'create' }
- series { 0 }
- end
-end
diff --git a/spec/frontend/lib/utils/secret_detection_spec.js b/spec/frontend/lib/utils/secret_detection_spec.js
index a8da6e8969f..0d1bf1abbaa 100644
--- a/spec/frontend/lib/utils/secret_detection_spec.js
+++ b/spec/frontend/lib/utils/secret_detection_spec.js
@@ -32,6 +32,7 @@ describe('containsSensitiveToken', () => {
'https://example.com/feed?feed_token=123456789_abcdefghij',
'glpat-1234567890 and feed_token=ABCDEFGHIJKLMNOPQRSTUVWXYZ',
'token: gldt-cgyKc1k_AsnEpmP-5fRL',
+ 'curl "https://gitlab.example.com/api/v4/groups/33/scim/identities" --header "PRIVATE-TOKEN: glsoat-cgyKc1k_AsnEpmP-5fRL',
];
it.each(sensitiveMessages)('returns true for message: %s', (message) => {
diff --git a/spec/frontend/organizations/shared/components/groups_view_spec.js b/spec/frontend/organizations/shared/components/groups_view_spec.js
index 8d6ea60ffd2..e51d6a98743 100644
--- a/spec/frontend/organizations/shared/components/groups_view_spec.js
+++ b/spec/frontend/organizations/shared/components/groups_view_spec.js
@@ -25,13 +25,20 @@ describe('GroupsView', () => {
newGroupPath: '/groups/new',
};
+ const defaultPropsData = {
+ listItemClass: 'gl-px-5',
+ };
+
const createComponent = ({ mockResolvers = resolvers, propsData = {} } = {}) => {
mockApollo = createMockApollo([], mockResolvers);
wrapper = shallowMountExtended(GroupsView, {
apolloProvider: mockApollo,
provide: defaultProvide,
- propsData,
+ propsData: {
+ ...defaultPropsData,
+ ...propsData,
+ },
});
};
@@ -115,6 +122,7 @@ describe('GroupsView', () => {
expect(wrapper.findComponent(GroupsList).props()).toEqual({
groups: formatGroups(organizationGroups.nodes),
showGroupIcon: true,
+ listItemClass: defaultPropsData.listItemClass,
});
});
});
diff --git a/spec/frontend/organizations/shared/components/projects_view_spec.js b/spec/frontend/organizations/shared/components/projects_view_spec.js
index 490b0c89348..3cc71927bfa 100644
--- a/spec/frontend/organizations/shared/components/projects_view_spec.js
+++ b/spec/frontend/organizations/shared/components/projects_view_spec.js
@@ -25,13 +25,20 @@ describe('ProjectsView', () => {
newProjectPath: '/projects/new',
};
+ const defaultPropsData = {
+ listItemClass: 'gl-px-5',
+ };
+
const createComponent = ({ mockResolvers = resolvers, propsData = {} } = {}) => {
mockApollo = createMockApollo([], mockResolvers);
wrapper = shallowMountExtended(ProjectsView, {
apolloProvider: mockApollo,
provide: defaultProvide,
- propsData,
+ propsData: {
+ ...defaultPropsData,
+ ...propsData,
+ },
});
};
@@ -115,6 +122,7 @@ describe('ProjectsView', () => {
expect(wrapper.findComponent(ProjectsList).props()).toEqual({
projects: formatProjects(organizationProjects.nodes),
showProjectIcon: true,
+ listItemClass: defaultPropsData.listItemClass,
});
});
});
diff --git a/spec/frontend/vue_shared/components/groups_list/groups_list_item_spec.js b/spec/frontend/vue_shared/components/groups_list/groups_list_item_spec.js
index cba9f78790d..f0b33284125 100644
--- a/spec/frontend/vue_shared/components/groups_list/groups_list_item_spec.js
+++ b/spec/frontend/vue_shared/components/groups_list/groups_list_item_spec.js
@@ -1,4 +1,4 @@
-import { GlAvatarLabeled, GlIcon } from '@gitlab/ui';
+import { GlAvatarLabeled, GlIcon, GlBadge } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import GroupsListItem from '~/vue_shared/components/groups_list/groups_list_item.vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
@@ -7,7 +7,6 @@ import {
VISIBILITY_LEVEL_INTERNAL_STRING,
GROUP_VISIBILITY_TYPE,
} from '~/visibility_level/constants';
-import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.vue';
import { ACCESS_LEVEL_LABELS } from '~/access_level/constants';
import ListActions from '~/vue_shared/components/list_actions/list_actions.vue';
import { ACTION_EDIT, ACTION_DELETE } from '~/vue_shared/components/list_actions/constants';
@@ -112,7 +111,7 @@ describe('GroupsListItem', () => {
it('renders access role badge', () => {
createComponent();
- expect(findAvatarLabeled().findComponent(UserAccessRoleBadge).text()).toBe(
+ expect(findAvatarLabeled().findComponent(GlBadge).text()).toBe(
ACCESS_LEVEL_LABELS[group.accessLevel.integerValue],
);
});
diff --git a/spec/frontend/vue_shared/components/groups_list/groups_list_spec.js b/spec/frontend/vue_shared/components/groups_list/groups_list_spec.js
index ec6a1dc9576..072b27b4807 100644
--- a/spec/frontend/vue_shared/components/groups_list/groups_list_spec.js
+++ b/spec/frontend/vue_shared/components/groups_list/groups_list_spec.js
@@ -8,6 +8,7 @@ describe('GroupsList', () => {
const defaultPropsData = {
groups,
+ listItemClass: 'gl-px-5',
};
const createComponent = () => {
@@ -23,6 +24,9 @@ describe('GroupsList', () => {
const expectedProps = groupsListItemWrappers.map((groupsListItemWrapper) =>
groupsListItemWrapper.props(),
);
+ const expectedClasses = groupsListItemWrappers.map((groupsListItemWrapper) =>
+ groupsListItemWrapper.classes(),
+ );
expect(expectedProps).toEqual(
defaultPropsData.groups.map((group) => ({
@@ -30,6 +34,9 @@ describe('GroupsList', () => {
showGroupIcon: false,
})),
);
+ expect(expectedClasses).toEqual(
+ defaultPropsData.groups.map(() => [defaultPropsData.listItemClass]),
+ );
});
describe('when `GroupsListItem` emits `delete` event', () => {
diff --git a/spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js b/spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js
index 7cf560745b6..a5a5a43effe 100644
--- a/spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js
+++ b/spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js
@@ -12,7 +12,6 @@ import {
VISIBILITY_LEVEL_PRIVATE_STRING,
PROJECT_VISIBILITY_TYPE,
} from '~/visibility_level/constants';
-import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.vue';
import { ACCESS_LEVEL_LABELS } from '~/access_level/constants';
import { FEATURABLE_DISABLED, FEATURABLE_ENABLED } from '~/featurable/constants';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -92,7 +91,7 @@ describe('ProjectsListItem', () => {
it('renders access role badge', () => {
createComponent();
- expect(findAvatarLabeled().findComponent(UserAccessRoleBadge).text()).toBe(
+ expect(findAvatarLabeled().findComponent(GlBadge).text()).toBe(
ACCESS_LEVEL_LABELS[project.permissions.projectAccess.accessLevel],
);
});
diff --git a/spec/frontend/vue_shared/components/projects_list/projects_list_spec.js b/spec/frontend/vue_shared/components/projects_list/projects_list_spec.js
index fb195dfe08e..6530157811c 100644
--- a/spec/frontend/vue_shared/components/projects_list/projects_list_spec.js
+++ b/spec/frontend/vue_shared/components/projects_list/projects_list_spec.js
@@ -9,6 +9,7 @@ describe('ProjectsList', () => {
const defaultPropsData = {
projects: convertObjectPropsToCamelCase(projects, { deep: true }),
+ listItemClass: 'gl-px-5',
};
const createComponent = () => {
@@ -24,6 +25,9 @@ describe('ProjectsList', () => {
const expectedProps = projectsListItemWrappers.map((projectsListItemWrapper) =>
projectsListItemWrapper.props(),
);
+ const expectedClasses = projectsListItemWrappers.map((projectsListItemWrapper) =>
+ projectsListItemWrapper.classes(),
+ );
expect(expectedProps).toEqual(
defaultPropsData.projects.map((project) => ({
@@ -31,6 +35,9 @@ describe('ProjectsList', () => {
showProjectIcon: false,
})),
);
+ expect(expectedClasses).toEqual(
+ defaultPropsData.projects.map(() => [defaultPropsData.listItemClass]),
+ );
});
describe('when `ProjectListItem` emits `delete` event', () => {
diff --git a/spec/frontend/work_items/components/work_item_description_rendered_spec.js b/spec/frontend/work_items/components/work_item_description_rendered_spec.js
index 4f1d49ee2e5..c4c88c7643f 100644
--- a/spec/frontend/work_items/components/work_item_description_rendered_spec.js
+++ b/spec/frontend/work_items/components/work_item_description_rendered_spec.js
@@ -20,11 +20,13 @@ describe('WorkItemDescription', () => {
const createComponent = ({
workItemDescription = defaultWorkItemDescription,
canEdit = false,
+ disableInlineEditing = false,
} = {}) => {
wrapper = shallowMount(WorkItemDescriptionRendered, {
propsData: {
workItemDescription,
canEdit,
+ disableInlineEditing,
},
});
};
@@ -81,8 +83,8 @@ describe('WorkItemDescription', () => {
});
describe('Edit button', () => {
- it('is not visible when canUpdate = false', async () => {
- await createComponent({
+ it('is not visible when canUpdate = false', () => {
+ createComponent({
canUpdate: false,
});
@@ -100,5 +102,14 @@ describe('WorkItemDescription', () => {
expect(wrapper.emitted('startEditing')).toEqual([[]]);
});
+
+ it('is not visible when `disableInlineEditing` is true and the user can edit', () => {
+ createComponent({
+ disableInlineEditing: true,
+ canEdit: true,
+ });
+
+ expect(findEditButton().exists()).toBe(false);
+ });
});
});
diff --git a/spec/frontend/work_items/components/work_item_description_spec.js b/spec/frontend/work_items/components/work_item_description_spec.js
index 1d25bb74986..3b137008b5b 100644
--- a/spec/frontend/work_items/components/work_item_description_spec.js
+++ b/spec/frontend/work_items/components/work_item_description_spec.js
@@ -56,6 +56,8 @@ describe('WorkItemDescription', () => {
isEditing = false,
isGroup = false,
workItemIid = '1',
+ disableInlineEditing = false,
+ editMode = false,
} = {}) => {
workItemResponseHandler = jest.fn().mockResolvedValue(workItemResponse);
groupWorkItemResponseHandler = jest
@@ -73,6 +75,8 @@ describe('WorkItemDescription', () => {
fullPath: 'test-project-path',
workItemId: id,
workItemIid,
+ disableInlineEditing,
+ editMode,
},
provide: {
isGroup,
@@ -283,4 +287,36 @@ describe('WorkItemDescription', () => {
expect(groupWorkItemResponseHandler).toHaveBeenCalled();
});
});
+
+ describe('when inline editing is disabled', () => {
+ describe('when edit mode is inactive', () => {
+ beforeEach(() => {
+ createComponent({ disableInlineEditing: true });
+ });
+
+ it('passes the correct props for work item rendered description', () => {
+ expect(findRenderedDescription().props('disableInlineEditing')).toBe(true);
+ });
+
+ it('does not show edit mode of markdown editor in default mode', () => {
+ expect(findMarkdownEditor().exists()).toBe(false);
+ });
+ });
+
+ describe('when edit mode is active', () => {
+ beforeEach(() => {
+ createComponent({ disableInlineEditing: true, editMode: true });
+ });
+
+ it('shows markdown editor in edit mode only when the correct props are passed', () => {
+ expect(findMarkdownEditor().exists()).toBe(true);
+ });
+
+ it('emits the `updateDraft` event when clicked on submit button in edit mode', () => {
+ const updatedDesc = 'updated desc with inline editing disabled';
+ findMarkdownEditor().vm.$emit('input', updatedDesc);
+ expect(wrapper.emitted('updateDraft')).toEqual([[updatedDesc]]);
+ });
+ });
+ });
});
diff --git a/spec/frontend/work_items/components/work_item_detail_spec.js b/spec/frontend/work_items/components/work_item_detail_spec.js
index d63bb94c3f0..e43c4d3c74d 100644
--- a/spec/frontend/work_items/components/work_item_detail_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_spec.js
@@ -19,6 +19,7 @@ import WorkItemRelationships from '~/work_items/components/work_item_relationshi
import WorkItemNotes from '~/work_items/components/work_item_notes.vue';
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
import WorkItemStickyHeader from '~/work_items/components/work_item_sticky_header.vue';
+import WorkItemTitleWithEdit from '~/work_items/components/work_item_title_with_edit.vue';
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
import WorkItemTodos from '~/work_items/components/work_item_todos.vue';
import { i18n } from '~/work_items/constants';
@@ -81,6 +82,8 @@ describe('WorkItemDetail component', () => {
const findStickyHeader = () => wrapper.findComponent(WorkItemStickyHeader);
const findWorkItemTwoColumnViewContainer = () => wrapper.findByTestId('work-item-overview');
const findRightSidebar = () => wrapper.findByTestId('work-item-overview-right-sidebar');
+ const findEditButton = () => wrapper.findByTestId('work-item-edit-form-button');
+ const findWorkItemTitleWithEdit = () => wrapper.findComponent(WorkItemTitleWithEdit);
const createComponent = ({
isGroup = false,
@@ -686,4 +689,65 @@ describe('WorkItemDetail component', () => {
});
});
});
+
+ describe('edit button for work item title and description', () => {
+ describe('when `workItemsMvc2Enabled` is false', () => {
+ beforeEach(async () => {
+ createComponent({ workItemsMvc2Enabled: false });
+ await waitForPromises();
+ });
+
+ it('does not show the edit button', () => {
+ expect(findEditButton().exists()).toBe(false);
+ });
+
+ it('renders the work item title inline editable component', () => {
+ expect(findWorkItemTitle().exists()).toBe(true);
+ });
+
+ it('does not render the work item title with edit component', () => {
+ expect(findWorkItemTitleWithEdit().exists()).toBe(false);
+ });
+ });
+
+ describe('when `workItemsMvc2Enabled` is true', () => {
+ beforeEach(async () => {
+ createComponent({ workItemsMvc2Enabled: true });
+ await waitForPromises();
+ });
+
+ it('shows the edit button', () => {
+ expect(findEditButton().exists()).toBe(true);
+ });
+
+ it('does not render the work item title inline editable component', () => {
+ expect(findWorkItemTitle().exists()).toBe(false);
+ });
+
+ it('renders the work item title with edit component', () => {
+ expect(findWorkItemTitleWithEdit().exists()).toBe(true);
+ expect(findWorkItemTitleWithEdit().props('isEditing')).toBe(false);
+ });
+
+ it('work item description is not shown in edit mode by default', () => {
+ expect(findWorkItemDescription().props('editMode')).toBe(false);
+ });
+
+ describe('when edit is clicked', () => {
+ beforeEach(async () => {
+ findEditButton().vm.$emit('click');
+ await nextTick();
+ });
+
+ it('work item title component shows in edit mode', () => {
+ expect(findWorkItemTitleWithEdit().props('isEditing')).toBe(true);
+ });
+
+ it('work item description component shows in edit mode', () => {
+ expect(findWorkItemDescription().props('disableInlineEditing')).toBe(true);
+ expect(findWorkItemDescription().props('editMode')).toBe(true);
+ });
+ });
+ });
+ });
});
diff --git a/spec/frontend/work_items/components/work_item_with_title_edit_spec.js b/spec/frontend/work_items/components/work_item_with_title_edit_spec.js
new file mode 100644
index 00000000000..db9551b6ec3
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_with_title_edit_spec.js
@@ -0,0 +1,59 @@
+import { GlFormGroup, GlFormInput } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import WorkItemTitleWithEdit from '~/work_items/components/work_item_title_with_edit.vue';
+
+describe('Work Item title with edit', () => {
+ let wrapper;
+ const mockTitle = 'Work Item title';
+
+ const createComponent = ({ isEditing = false } = {}) => {
+ wrapper = shallowMountExtended(WorkItemTitleWithEdit, {
+ propsData: {
+ title: mockTitle,
+ isEditing,
+ },
+ });
+ };
+
+ const findTitle = () => wrapper.findByTestId('work-item-title');
+ const findEditableTitleForm = () => wrapper.findComponent(GlFormGroup);
+ const findEditableTitleInput = () => wrapper.findComponent(GlFormInput);
+
+ describe('Default mode', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders title', () => {
+ expect(findTitle().exists()).toBe(true);
+ expect(findTitle().text()).toBe(mockTitle);
+ });
+
+ it('does not render edit mode', () => {
+ expect(findEditableTitleForm().exists()).toBe(false);
+ });
+ });
+
+ describe('Edit mode', () => {
+ beforeEach(() => {
+ createComponent({ isEditing: true });
+ });
+
+ it('does not render read only title', () => {
+ expect(findTitle().exists()).toBe(false);
+ });
+
+ it('renders the editable title with label', () => {
+ expect(findEditableTitleForm().exists()).toBe(true);
+ expect(findEditableTitleForm().attributes('label')).toBe(
+ WorkItemTitleWithEdit.i18n.titleLabel,
+ );
+ });
+
+ it('emits `updateDraft` event on change of the input', () => {
+ findEditableTitleInput().vm.$emit('change', 'updated title');
+
+ expect(wrapper.emitted('updateDraft')).toEqual([['updated title']]);
+ });
+ });
+});
diff --git a/spec/lib/gitlab/hook_data/project_builder_spec.rb b/spec/lib/gitlab/hook_data/project_builder_spec.rb
index 9d5eaf0608c..77ab55f3e26 100644
--- a/spec/lib/gitlab/hook_data/project_builder_spec.rb
+++ b/spec/lib/gitlab/hook_data/project_builder_spec.rb
@@ -12,8 +12,17 @@ RSpec.describe Gitlab::HookData::ProjectBuilder do
let(:event_name) { data[:event_name] }
let(:attributes) do
[
- :event_name, :created_at, :updated_at, :name, :path, :path_with_namespace, :project_id,
- :owners, :owner_name, :owner_email, :project_visibility
+ :created_at,
+ :event_name,
+ :name,
+ :owner_email,
+ :owner_name,
+ :owners,
+ :path,
+ :path_with_namespace,
+ :project_id,
+ :project_visibility,
+ :updated_at
]
end
@@ -118,6 +127,19 @@ RSpec.describe Gitlab::HookData::ProjectBuilder do
it_behaves_like 'includes the required attributes'
it_behaves_like 'does not include `old_path_with_namespace` attribute'
+
+ context 'group has pending owner invitation' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, :internal, name: 'group project', namespace: group) }
+
+ let(:owners_data) { [] }
+
+ before do
+ create(:group_member, :invited, group: group)
+ end
+
+ it { expect(event_name).to eq('project_create') }
+ end
end
context 'on destroy' do
diff --git a/spec/lib/gitlab/usage/service_ping_report_spec.rb b/spec/lib/gitlab/usage/service_ping_report_spec.rb
index a848c286fa9..09866198639 100644
--- a/spec/lib/gitlab/usage/service_ping_report_spec.rb
+++ b/spec/lib/gitlab/usage/service_ping_report_spec.rb
@@ -168,11 +168,6 @@ RSpec.describe Gitlab::Usage::ServicePingReport, :use_clean_rails_memory_store_c
memoized_constatns += Gitlab::UsageData::EE_MEMOIZED_VALUES if defined? Gitlab::UsageData::EE_MEMOIZED_VALUES
memoized_constatns.each { |v| Gitlab::UsageData.clear_memoization(v) }
stub_database_flavor_check('Cloud SQL for PostgreSQL')
-
- # in_product_marketing_email metrics values are extracted from a single group by query
- # to check if the queries for individual metrics return the same value as group by when the value is non-zero
- create(:in_product_marketing_email, track: :create, series: 0, cta_clicked_at: Time.current)
- create(:in_product_marketing_email, track: :verify, series: 0)
end
let(:service_ping_payload) { described_class.for(output: :all_metrics_values) }
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index d098f0b4c4a..ddf5a9a7221 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -187,7 +187,6 @@ RSpec.describe User, feature_category: :user_profile do
it { is_expected.to have_many(:merge_request_assignees).inverse_of(:assignee) }
it { is_expected.to have_many(:merge_request_reviewers).inverse_of(:reviewer) }
it { is_expected.to have_many(:created_custom_emoji).inverse_of(:creator) }
- it { is_expected.to have_many(:in_product_marketing_emails) }
it { is_expected.to have_many(:timelogs) }
it { is_expected.to have_many(:callouts).class_name('Users::Callout') }
it { is_expected.to have_many(:group_callouts).class_name('Users::GroupCallout') }
diff --git a/spec/models/users/in_product_marketing_email_spec.rb b/spec/models/users/in_product_marketing_email_spec.rb
deleted file mode 100644
index b1642383e42..00000000000
--- a/spec/models/users/in_product_marketing_email_spec.rb
+++ /dev/null
@@ -1,137 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Users::InProductMarketingEmail, type: :model, feature_category: :onboarding do
- let(:track) { :create }
- let(:series) { 0 }
-
- describe 'associations' do
- it { is_expected.to belong_to(:user) }
- end
-
- describe 'validations' do
- subject { build(:in_product_marketing_email) }
-
- it { is_expected.to validate_presence_of(:user) }
-
- context 'when track+series email' do
- it { is_expected.to validate_presence_of(:track) }
- it { is_expected.to validate_presence_of(:series) }
-
- it {
- is_expected.to validate_uniqueness_of(:user_id)
- .scoped_to([:track, :series]).with_message('track series email has already been sent')
- }
- end
- end
-
- describe '.without_track_and_series' do
- let_it_be(:user) { create(:user) }
-
- subject(:without_track_and_series) { User.merge(described_class.without_track_and_series(track, series)) }
-
- before do
- create(:in_product_marketing_email, track: :create, series: 0, user: user)
- create(:in_product_marketing_email, track: :create, series: 1, user: user)
- create(:in_product_marketing_email, track: :verify, series: 0, user: user)
- end
-
- context 'when given track and series already exists' do
- it { expect(without_track_and_series).to be_empty }
- end
-
- context 'when track does not exist' do
- let(:track) { :trial }
-
- it { expect(without_track_and_series).to eq [user] }
- end
-
- context 'when series does not exist' do
- let(:series) { 2 }
-
- it { expect(without_track_and_series).to eq [user] }
- end
-
- context 'when no track or series for a user exists' do
- let(:track) { :create }
- let(:series) { 0 }
- let(:other_user) { create(:user) }
-
- it { expect(without_track_and_series).to eq [other_user] }
- end
- end
-
- describe '.for_user_with_track_and_series' do
- let_it_be(:user) { create(:user) }
- let_it_be(:in_product_marketing_email) { create(:in_product_marketing_email, series: 0, track: 0, user: user) }
-
- subject(:for_user_with_track_and_series) do
- described_class.for_user_with_track_and_series(user, track, series).first
- end
-
- context 'when record for user with given track and series exists' do
- it { is_expected.to eq(in_product_marketing_email) }
- end
-
- context 'when user is different' do
- let(:user) { build_stubbed(:user) }
-
- it { is_expected.to be_nil }
- end
-
- context 'when track is different' do
- let(:track) { 1 }
-
- it { is_expected.to be_nil }
- end
-
- context 'when series is different' do
- let(:series) { 1 }
-
- it { is_expected.to be_nil }
- end
- end
-
- describe '.save_cta_click' do
- let(:user) { create(:user) }
-
- subject(:save_cta_click) { described_class.save_cta_click(user, track, series) }
-
- context 'when there is no record' do
- it 'does not error' do
- expect { save_cta_click }.not_to raise_error
- end
- end
-
- context 'when there is no record for the track and series' do
- it 'does not perform an update' do
- other_email = create(:in_product_marketing_email, user: user, track: :verify, series: 2, cta_clicked_at: nil)
-
- expect { save_cta_click }.not_to change { other_email.reload }
- end
- end
-
- context 'when there is a record for the track and series' do
- it 'saves the cta click date' do
- email = create(:in_product_marketing_email, user: user, track: track, series: series, cta_clicked_at: nil)
-
- freeze_time do
- expect { save_cta_click }.to change { email.reload.cta_clicked_at }.from(nil).to(Time.zone.now)
- end
- end
-
- context 'when cta_clicked_at is already set' do
- it 'does not update' do
- create(:in_product_marketing_email, user: user, track: track, series: series, cta_clicked_at: Time.zone.now)
-
- expect_next_found_instance_of(described_class) do |record|
- expect(record).not_to receive(:update)
- end
-
- save_cta_click
- end
- end
- end
- end
-end
diff --git a/spec/services/work_items/widgets/assignees_service/update_service_spec.rb b/spec/services/work_items/callbacks/assignees_spec.rb
index 66e30e2f882..e6f57c54104 100644
--- a/spec/services/work_items/widgets/assignees_service/update_service_spec.rb
+++ b/spec/services/work_items/callbacks/assignees_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe WorkItems::Widgets::AssigneesService::UpdateService, :freeze_time, feature_category: :portfolio_management do
+RSpec.describe WorkItems::Callbacks::Assignees, :freeze_time, feature_category: :portfolio_management do
let_it_be(:reporter) { create(:user) }
let_it_be(:project) { create(:project, :private) }
let_it_be(:new_assignee) { create(:user) }
@@ -11,7 +11,6 @@ RSpec.describe WorkItems::Widgets::AssigneesService::UpdateService, :freeze_time
create(:work_item, project: project, updated_at: 1.day.ago)
end
- let(:widget) { work_item.widgets.find { |widget| widget.is_a?(WorkItems::Widgets::Assignees) } }
let(:current_user) { reporter }
let(:params) { { assignee_ids: [new_assignee.id] } }
@@ -20,13 +19,13 @@ RSpec.describe WorkItems::Widgets::AssigneesService::UpdateService, :freeze_time
project.add_guest(new_assignee)
end
- describe '#before_update_in_transaction' do
- let(:service) { described_class.new(widget: widget, current_user: current_user) }
+ describe '#before_update' do
+ let(:service) { described_class.new(issuable: work_item, current_user: current_user, params: params) }
- subject { service.before_update_in_transaction(params: params) }
+ subject(:before_update_callback) { service.before_update }
it 'updates the assignees and sets updated_at to the current time' do
- subject
+ before_update_callback
expect(work_item.assignee_ids).to contain_exactly(new_assignee.id)
expect(work_item.updated_at).to be_like_time(Time.current)
@@ -40,7 +39,7 @@ RSpec.describe WorkItems::Widgets::AssigneesService::UpdateService, :freeze_time
end
it 'removes existing assignees' do
- subject
+ before_update_callback
expect(work_item.assignee_ids).to be_empty
expect(work_item.updated_at).to be_like_time(Time.current)
@@ -51,7 +50,7 @@ RSpec.describe WorkItems::Widgets::AssigneesService::UpdateService, :freeze_time
let(:current_user) { create(:user) }
it 'does not update the assignees' do
- subject
+ before_update_callback
expect(work_item.assignee_ids).to be_empty
expect(work_item.updated_at).to be_like_time(1.day.ago)
@@ -67,7 +66,7 @@ RSpec.describe WorkItems::Widgets::AssigneesService::UpdateService, :freeze_time
end
it 'sets all the given assignees' do
- subject
+ before_update_callback
expect(work_item.assignee_ids).to contain_exactly(new_assignee.id, reporter.id)
expect(work_item.updated_at).to be_like_time(Time.current)
@@ -80,7 +79,7 @@ RSpec.describe WorkItems::Widgets::AssigneesService::UpdateService, :freeze_time
end
it 'only sets the first assignee' do
- subject
+ before_update_callback
expect(work_item.assignee_ids).to contain_exactly(new_assignee.id)
expect(work_item.updated_at).to be_like_time(Time.current)
@@ -92,7 +91,7 @@ RSpec.describe WorkItems::Widgets::AssigneesService::UpdateService, :freeze_time
let(:params) { { assignee_ids: [create(:user).id] } }
it 'does not set the assignee' do
- subject
+ before_update_callback
expect(work_item.assignee_ids).to be_empty
expect(work_item.updated_at).to be_like_time(1.day.ago)
@@ -105,7 +104,7 @@ RSpec.describe WorkItems::Widgets::AssigneesService::UpdateService, :freeze_time
end
it 'does not touch updated_at' do
- subject
+ before_update_callback
expect(work_item.assignee_ids).to contain_exactly(new_assignee.id)
expect(work_item.updated_at).to be_like_time(1.day.ago)
@@ -116,12 +115,12 @@ RSpec.describe WorkItems::Widgets::AssigneesService::UpdateService, :freeze_time
let(:params) { {} }
before do
- allow(service).to receive(:new_type_excludes_widget?).and_return(true)
+ allow(service).to receive(:excluded_in_new_type?).and_return(true)
work_item.assignee_ids = [new_assignee.id]
end
it "resets the work item's assignees" do
- subject
+ before_update_callback
expect(work_item.assignee_ids).to be_empty
end
diff --git a/spec/support/helpers/database/duplicate_indexes.yml b/spec/support/helpers/database/duplicate_indexes.yml
index cbb0b0be457..87a1e0c2c50 100644
--- a/spec/support/helpers/database/duplicate_indexes.yml
+++ b/spec/support/helpers/database/duplicate_indexes.yml
@@ -69,9 +69,6 @@ error_tracking_errors:
geo_node_namespace_links:
index_geo_node_namespace_links_on_geo_node_id_and_namespace_id:
- index_geo_node_namespace_links_on_geo_node_id
-in_product_marketing_emails:
- index_in_product_marketing_emails_on_user_track_series:
- - index_in_product_marketing_emails_on_user_id
incident_management_oncall_participants:
index_inc_mgmnt_oncall_participants_on_user_id_and_rotation_id:
- index_inc_mgmnt_oncall_participants_on_oncall_user_id
diff --git a/spec/support/shared_examples/features/work_items_shared_examples.rb b/spec/support/shared_examples/features/work_items_shared_examples.rb
index 3dfd7604914..a5b467da45d 100644
--- a/spec/support/shared_examples/features/work_items_shared_examples.rb
+++ b/spec/support/shared_examples/features/work_items_shared_examples.rb
@@ -3,6 +3,13 @@
RSpec.shared_examples 'work items title' do
let(:title_selector) { '[data-testid="work-item-title"]' }
+ before do
+ stub_feature_flags(work_items_mvc_2: false)
+
+ page.refresh
+ wait_for_all_requests
+ end
+
it 'successfully shows and changes the title of the work item' do
expect(work_item.reload.title).to eq work_item.title
@@ -299,54 +306,67 @@ RSpec.shared_examples 'work items labels' do
end
RSpec.shared_examples 'work items description' do
- it 'shows GFM autocomplete', :aggregate_failures do
- click_button "Edit description"
- fill_in _('Description'), with: "@#{user.username}"
+ context 'for work_items_mvc_2 FF' do
+ [true, false].each do |work_items_mvc_2_flag| # rubocop:disable RSpec/UselessDynamicDefinition -- check it for both off and on
+ let(:edit_button) { work_items_mvc_2_flag ? 'Edit' : 'Edit description' }
- page.within('.atwho-container') do
- expect(page).to have_text(user.name)
- end
- end
+ before do
+ stub_feature_flags(work_items_mvc_2: work_items_mvc_2_flag)
+
+ page.refresh
+ wait_for_all_requests
+ end
- it 'autocompletes available quick actions', :aggregate_failures do
- click_button "Edit description"
- fill_in _('Description'), with: '/'
+ it 'shows GFM autocomplete', :aggregate_failures do
+ click_button edit_button
+ fill_in _('Description'), with: "@#{user.username}"
- page.within('#at-view-commands') do
- expect(page).to have_text("title")
- expect(page).to have_text("shrug")
- expect(page).to have_text("tableflip")
- expect(page).to have_text("close")
- expect(page).to have_text("cc")
- end
- end
+ page.within('.atwho-container') do
+ expect(page).to have_text(user.name)
+ end
+ end
- context 'on conflict' do
- let_it_be(:other_user) { create(:user) }
- let(:expected_warning) { 'Someone edited the description at the same time you did.' }
+ it 'autocompletes available quick actions', :aggregate_failures do
+ click_button edit_button
+ fill_in _('Description'), with: '/'
- before do
- project.add_developer(other_user)
- end
+ page.within('#at-view-commands') do
+ expect(page).to have_text("title")
+ expect(page).to have_text("shrug")
+ expect(page).to have_text("tableflip")
+ expect(page).to have_text("close")
+ expect(page).to have_text("cc")
+ end
+ end
- it 'shows conflict message when description changes', :aggregate_failures do
- click_button "Edit description"
+ context 'on conflict' do
+ let_it_be(:other_user) { create(:user) }
+ let(:expected_warning) { 'Someone edited the description at the same time you did.' }
- ::WorkItems::UpdateService.new(
- container: work_item.project,
- current_user: other_user,
- params: { description: "oh no!" }
- ).execute(work_item)
+ before do
+ project.add_developer(other_user)
+ end
- wait_for_requests
+ it 'shows conflict message when description changes', :aggregate_failures do
+ click_button edit_button
+
+ ::WorkItems::UpdateService.new(
+ container: work_item.project,
+ current_user: other_user,
+ params: { description: "oh no!" }
+ ).execute(work_item)
- fill_in _('Description'), with: 'oh yeah!'
+ wait_for_requests
- expect(page).to have_text(expected_warning)
+ fill_in _('Description'), with: 'oh yeah!'
- click_button s_('WorkItem|Save and overwrite')
+ expect(page).to have_text(expected_warning)
- expect(page.find('[data-testid="work-item-description"]')).to have_text("oh yeah!")
+ click_button s_('WorkItem|Save and overwrite')
+
+ expect(page.find('[data-testid="work-item-description"]')).to have_text("oh yeah!")
+ end
+ end
end
end
end