diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-12-20 12:10:35 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-12-20 12:10:35 +0300 |
commit | 0b7d3f810f287523fd4ad72c019d78e8d2983f8a (patch) | |
tree | fdb52060e586a5548f026b6a3ce71c5b676d9e5c /spec | |
parent | 51da6793bcbe7295c1f3f881f756b8b60ee06da1 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
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 |