diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-06-22 21:09:45 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-06-22 21:09:45 +0300 |
commit | 06f12476c7962ba59579b3a08d187a22325d9039 (patch) | |
tree | 6ebee8dbf9f31ca8fe2a3fe1b75de9e93a2adb82 /spec | |
parent | 3a8d221b7e3dc909876fe60ac267e63d1ffffdd7 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
52 files changed, 598 insertions, 603 deletions
diff --git a/spec/factories/organizations/organization_users.rb b/spec/factories/organizations/organization_users.rb new file mode 100644 index 00000000000..761f260ccb3 --- /dev/null +++ b/spec/factories/organizations/organization_users.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :organization_user, class: 'Organizations::OrganizationUser' do + user + organization + end +end diff --git a/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb b/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb index 19b5ad0fa84..a4c03dc4e73 100644 --- a/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb +++ b/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb @@ -13,22 +13,6 @@ RSpec.describe 'Merge request > User merges only if pipeline succeeds', :js, fea context 'project does not have CI enabled' do it 'allows MR to be merged' do - stub_feature_flags(auto_merge_labels_mr_widget: false) - - visit project_merge_request_path(project, merge_request) - - wait_for_requests - - page.within('.mr-state-widget') do - expect(page).to have_button 'Merge' - end - end - end - - context 'project does not have CI enabled and auto_merge_labels_mr_widget on' do - it 'allows MR to be merged' do - stub_feature_flags(auto_merge_labels_mr_widget: true) - visit project_merge_request_path(project, merge_request) wait_for_requests @@ -51,79 +35,6 @@ RSpec.describe 'Merge request > User merges only if pipeline succeeds', :js, fea context 'when merge requests can only be merged if the pipeline succeeds' do before do project.update_attribute(:only_allow_merge_if_pipeline_succeeds, true) - - stub_feature_flags(auto_merge_labels_mr_widget: false) - end - - context 'when CI is running' do - let(:status) { :running } - - it 'does not allow to merge immediately' do - visit project_merge_request_path(project, merge_request) - - wait_for_requests - - expect(page).to have_button 'Merge when pipeline succeeds' - expect(page).not_to have_button '.js-merge-moment' - end - end - - context 'when CI failed' do - let(:status) { :failed } - - it 'does not allow MR to be merged' do - visit project_merge_request_path(project, merge_request) - - wait_for_requests - - expect(page).not_to have_button('Merge', exact: true) - expect(page).to have_content('Merge blocked: pipeline must succeed. Push a commit that fixes the failure or learn about other solutions.') - end - end - - context 'when CI canceled' do - let(:status) { :canceled } - - it 'does not allow MR to be merged' do - visit project_merge_request_path(project, merge_request) - - wait_for_requests - - expect(page).not_to have_button('Merge', exact: true) - expect(page).to have_content('Merge blocked: pipeline must succeed. Push a commit that fixes the failure or learn about other solutions.') - end - end - - context 'when CI succeeded' do - let(:status) { :success } - - it 'allows MR to be merged' do - visit project_merge_request_path(project, merge_request) - - wait_for_requests - - expect(page).to have_button('Merge', exact: true) - end - end - - context 'when CI skipped' do - let(:status) { :skipped } - - it 'does not allow MR to be merged' do - visit project_merge_request_path(project, merge_request) - - wait_for_requests - - expect(page).not_to have_button('Merge', exact: true) - end - end - end - - context 'when merge requests can only be merged if the pipeline succeeds with auto_merge_labels_mr_widget on' do - before do - project.update_attribute(:only_allow_merge_if_pipeline_succeeds, true) - - stub_feature_flags(auto_merge_labels_mr_widget: true) end context 'when CI is running' do @@ -193,58 +104,6 @@ RSpec.describe 'Merge request > User merges only if pipeline succeeds', :js, fea context 'when merge requests can be merged when the build failed' do before do project.update_attribute(:only_allow_merge_if_pipeline_succeeds, false) - - stub_feature_flags(auto_merge_labels_mr_widget: false) - end - - context 'when CI is running' do - let(:status) { :running } - - it 'allows MR to be merged immediately' do - visit project_merge_request_path(project, merge_request) - - wait_for_requests - - expect(page).to have_button 'Merge when pipeline succeeds' - - page.find('.js-merge-moment').click - expect(page).to have_content 'Merge immediately' - end - end - - context 'when CI failed' do - let(:status) { :failed } - - it 'allows MR to be merged' do - visit project_merge_request_path(project, merge_request) - - wait_for_requests - page.within('.mr-state-widget') do - expect(page).to have_button 'Merge' - end - end - end - - context 'when CI succeeded' do - let(:status) { :success } - - it 'allows MR to be merged' do - visit project_merge_request_path(project, merge_request) - - wait_for_requests - - page.within('.mr-state-widget') do - expect(page).to have_button 'Merge' - end - end - end - end - - context 'when merge requests can be merged when the build failed with auto_merge_labels_mr_widget on' do - before do - project.update_attribute(:only_allow_merge_if_pipeline_succeeds, false) - - stub_feature_flags(auto_merge_labels_mr_widget: true) end context 'when CI is running' do diff --git a/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb b/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb index e42e4735ee2..9a8384bfc9f 100644 --- a/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb +++ b/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb @@ -26,83 +26,6 @@ RSpec.describe 'Merge request > User merges when pipeline succeeds', :js, featur context 'when there is active pipeline for merge request' do before do create(:ci_build, pipeline: pipeline) - stub_feature_flags(auto_merge_labels_mr_widget: false) - - sign_in(user) - visit project_merge_request_path(project, merge_request) - end - - describe 'enabling Merge when pipeline succeeds' do - shared_examples 'Merge when pipeline succeeds activator' do - it 'activates the Merge when pipeline succeeds feature', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/410105' do - click_button "Merge when pipeline succeeds" - - expect(page).to have_content "Set by #{user.name} to be merged automatically when the pipeline succeeds" - expect(page).to have_content "Source branch will not be deleted" - expect(page).to have_selector ".js-cancel-auto-merge" - visit project_merge_request_path(project, merge_request) # Needed to refresh the page - expect(page).to have_content /enabled an automatic merge when the pipeline for \h{8} succeeds/i - end - end - - context "when enabled immediately" do - it_behaves_like 'Merge when pipeline succeeds activator' - end - - context 'when enabled after pipeline status changed', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/258667' do - before do - pipeline.run! - - # We depend on merge request widget being reloaded - # so we have to wait for asynchronous call to reload it - # and have_content expectation handles that. - # - expect(page).to have_content "Pipeline ##{pipeline.id} running" - end - - it_behaves_like 'Merge when pipeline succeeds activator' - end - - context 'when enabled after it was previously canceled' do - before do - click_button "Merge when pipeline succeeds" - - wait_for_requests - - click_button "Cancel auto-merge" - - wait_for_requests - - expect(page).to have_content 'Merge when pipeline succeeds' - end - - it_behaves_like 'Merge when pipeline succeeds activator' - end - - context 'when it was enabled and then canceled' do - let(:merge_request) do - create(:merge_request_with_diffs, - :merge_when_pipeline_succeeds, - source_project: project, - title: 'Bug NS-04', - author: user, - merge_user: user) - end - - before do - merge_request.merge_params['force_remove_source_branch'] = '0' - merge_request.save! - click_button "Cancel auto-merge" - end - - it_behaves_like 'Merge when pipeline succeeds activator' - end - end - end - - context 'when there is active pipeline for merge request with auto_merge_labels_mr_widget on' do - before do - create(:ci_build, pipeline: pipeline) stub_feature_flags(auto_merge_labels_mr_widget: true) sign_in(user) @@ -166,86 +89,6 @@ RSpec.describe 'Merge request > User merges when pipeline succeeds', :js, featur context 'when merge when pipeline succeeds is enabled' do let(:merge_request) do create(:merge_request_with_diffs, :simple, :merge_when_pipeline_succeeds, - source_project: project, - author: user, - merge_user: user, - title: 'MepMep') - end - - let!(:build) do - create(:ci_build, pipeline: pipeline) - end - - before do - stub_feature_flags(auto_merge_labels_mr_widget: false) - sign_in user - visit project_merge_request_path(project, merge_request) - end - - it 'allows to cancel the automatic merge', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/412416' do - click_button "Cancel auto-merge" - - expect(page).to have_button "Merge when pipeline succeeds" - - refresh - - expect(page).to have_content "canceled the automatic merge" - end - - context 'when pipeline succeeds' do - before do - build.success - refresh - end - - it 'merges merge request', :sidekiq_might_not_need_inline do - expect(page).to have_content 'Changes merged' - expect(merge_request.reload).to be_merged - end - end - - context 'view merge request with MWPS enabled but automatically merge fails' do - before do - merge_request.update!( - merge_user: merge_request.author, - merge_error: 'Something went wrong' - ) - refresh - end - - it 'shows information about the merge error' do - # Wait for the `ci_status` and `merge_check` requests - wait_for_requests - - page.within('.mr-state-widget') do - expect(page).to have_content('Something went wrong. Try again.') - end - end - end - - context 'view merge request with MWPS enabled but automatically merge fails' do - before do - merge_request.update!( - merge_user: merge_request.author, - merge_error: 'Something went wrong.' - ) - refresh - end - - it 'shows information about the merge error' do - # Wait for the `ci_status` and `merge_check` requests - wait_for_requests - - page.within('.mr-state-widget') do - expect(page).to have_content('Something went wrong. Try again.') - end - end - end - end - - context 'when merge when pipeline succeeds is enabled and auto_merge_labels_mr_widget on' do - let(:merge_request) do - create(:merge_request_with_diffs, :simple, :merge_when_pipeline_succeeds, source_project: project, author: user, merge_user: user, diff --git a/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb b/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb index fba25b41b83..4ea36286768 100644 --- a/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb +++ b/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb @@ -58,8 +58,6 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request', end before do - stub_feature_flags(auto_merge_labels_mr_widget: false) - visit project_merge_request_path(project, merge_request) page.within('.merge-request-tabs') do @@ -146,53 +144,8 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request', end end - context 'when a user merges a merge request in the parent project', :sidekiq_might_not_need_inline do - before do - click_link 'Overview' - click_button 'Merge when pipeline succeeds' - - wait_for_requests - end - - context 'when detached merge request pipeline is pending' do - it 'waits the head pipeline' do - expect(page).to have_content('to be merged automatically when the pipeline succeeds') - expect(page).to have_button('Cancel auto-merge') - end - end - - context 'when detached merge request pipeline succeeds' do - before do - detached_merge_request_pipeline.reload.succeed! - - wait_for_requests - end - - it 'merges the merge request' do - expect(page).to have_content('Merged by') - expect(page).to have_button('Revert') - end - end - - context 'when branch pipeline succeeds' do - before do - click_link 'Overview' - push_pipeline.reload.succeed! - - wait_for_requests - end - - it 'waits the head pipeline' do - expect(page).to have_content('to be merged automatically when the pipeline succeeds') - expect(page).to have_button('Cancel auto-merge') - end - end - end - - context 'when a user created a merge request in the parent project with auto_merge_labels_mr_widget on' do + context 'when a user created a merge request in the parent project' do before do - stub_feature_flags(auto_merge_labels_mr_widget: true) - visit project_merge_request_path(project, merge_request) page.within('.merge-request-tabs') do @@ -408,10 +361,10 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request', project.update!(only_allow_merge_if_pipeline_succeeds: true) end - it 'shows MWPS button' do + it 'shows Set to auto-merge button' do visit project_merge_request_path(project, merge_request) - expect(page).to have_button('Merge when pipeline succeeds') + expect(page).to have_button('Set to auto-merge') end end end @@ -421,7 +374,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request', before do click_link("Overview") - click_button 'Merge when pipeline succeeds' + click_button 'Set to auto-merge' wait_for_requests end diff --git a/spec/frontend/admin/topics/components/topic_select_spec.js b/spec/frontend/admin/topics/components/topic_select_spec.js index 113a0e3d404..5b7e6365606 100644 --- a/spec/frontend/admin/topics/components/topic_select_spec.js +++ b/spec/frontend/admin/topics/components/topic_select_spec.js @@ -58,10 +58,6 @@ describe('TopicSelect', () => { }); } - afterEach(() => { - jest.clearAllMocks(); - }); - it('mounts', () => { createComponent(); diff --git a/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js b/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js index 521bbf06b02..de4db23bae2 100644 --- a/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js +++ b/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js @@ -239,8 +239,6 @@ describe('Batch comments store actions', () => { params = { note: { id: 1 }, noteText: 'test' }; }); - afterEach(() => jest.clearAllMocks()); - it('commits RECEIVE_DRAFT_UPDATE_SUCCESS with returned data', () => { return actions.updateDraft(context, { ...params, callback() {} }).then(() => { expect(commit).toHaveBeenCalledWith('RECEIVE_DRAFT_UPDATE_SUCCESS', { id: 1 }); diff --git a/spec/frontend/blob_edit/edit_blob_spec.js b/spec/frontend/blob_edit/edit_blob_spec.js index 9ab20fc2cd7..1bdc54723ce 100644 --- a/spec/frontend/blob_edit/edit_blob_spec.js +++ b/spec/frontend/blob_edit/edit_blob_spec.js @@ -61,7 +61,6 @@ describe('Blob Editing', () => { }); afterEach(() => { mock.restore(); - jest.clearAllMocks(); unuseMock.mockClear(); useMock.mockClear(); resetHTMLFixture(); diff --git a/spec/frontend/boards/board_card_inner_spec.js b/spec/frontend/boards/board_card_inner_spec.js index a925f752f5e..47e6a305447 100644 --- a/spec/frontend/boards/board_card_inner_spec.js +++ b/spec/frontend/boards/board_card_inner_spec.js @@ -111,7 +111,6 @@ describe('Board card component', () => { afterEach(() => { store = null; - jest.clearAllMocks(); }); it('renders issue title', () => { diff --git a/spec/frontend/boards/components/board_settings_sidebar_spec.js b/spec/frontend/boards/components/board_settings_sidebar_spec.js index b1e14be8ceb..affe1260c66 100644 --- a/spec/frontend/boards/components/board_settings_sidebar_spec.js +++ b/spec/frontend/boards/components/board_settings_sidebar_spec.js @@ -90,10 +90,6 @@ describe('BoardSettingsSidebar', () => { const findModal = () => wrapper.findComponent(GlModal); const findRemoveButton = () => wrapper.findComponent(GlButton); - afterEach(() => { - jest.restoreAllMocks(); - }); - it('finds a MountingPortal component', () => { createComponent(); diff --git a/spec/frontend/contribution_events/components/contribution_event/contribution_event_approved_spec.js b/spec/frontend/contribution_events/components/contribution_event/contribution_event_approved_spec.js index fb27e160897..5bce0ca3746 100644 --- a/spec/frontend/contribution_events/components/contribution_event/contribution_event_approved_spec.js +++ b/spec/frontend/contribution_events/components/contribution_event/contribution_event_approved_spec.js @@ -1,20 +1,17 @@ -import events from 'test_fixtures/controller/users/activity.json'; -import { mountExtended } from 'helpers/vue_test_utils_helper'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import ContributionEventApproved from '~/contribution_events/components/contribution_event/contribution_event_approved.vue'; import ContributionEventBase from '~/contribution_events/components/contribution_event/contribution_event_base.vue'; -import TargetLink from '~/contribution_events/components/target_link.vue'; -import ResourceParentLink from '~/contribution_events/components/resource_parent_link.vue'; import { eventApproved } from '../../utils'; const defaultPropsData = { - event: eventApproved(events), + event: eventApproved(), }; describe('ContributionEventApproved', () => { let wrapper; const createComponent = () => { - wrapper = mountExtended(ContributionEventApproved, { + wrapper = shallowMountExtended(ContributionEventApproved, { propsData: defaultPropsData, }); }; @@ -28,22 +25,7 @@ describe('ContributionEventApproved', () => { event: defaultPropsData.event, iconName: 'approval-solid', iconClass: 'gl-text-green-500', + message: ContributionEventApproved.i18n.message, }); }); - - it('renders message', () => { - expect(wrapper.findByTestId('event-body').text()).toBe( - `Approved merge request ${defaultPropsData.event.target.reference_link_text} in ${defaultPropsData.event.resource_parent.full_name}.`, - ); - }); - - it('renders target link', () => { - expect(wrapper.findComponent(TargetLink).props('event')).toEqual(defaultPropsData.event); - }); - - it('renders resource parent link', () => { - expect(wrapper.findComponent(ResourceParentLink).props('event')).toEqual( - defaultPropsData.event, - ); - }); }); diff --git a/spec/frontend/contribution_events/components/contribution_event/contribution_event_base_spec.js b/spec/frontend/contribution_events/components/contribution_event/contribution_event_base_spec.js index 8c951e20bed..310966243d1 100644 --- a/spec/frontend/contribution_events/components/contribution_event/contribution_event_base_spec.js +++ b/spec/frontend/contribution_events/components/contribution_event/contribution_event_base_spec.js @@ -1,23 +1,27 @@ import { GlAvatarLabeled, GlAvatarLink, GlIcon } from '@gitlab/ui'; -import events from 'test_fixtures/controller/users/activity.json'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; import ContributionEventBase from '~/contribution_events/components/contribution_event/contribution_event_base.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; - -const [event] = events; +import TargetLink from '~/contribution_events/components/target_link.vue'; +import ResourceParentLink from '~/contribution_events/components/resource_parent_link.vue'; +import { eventApproved } from '../../utils'; describe('ContributionEventBase', () => { let wrapper; const defaultPropsData = { - event, + event: eventApproved(), iconName: 'approval-solid', iconClass: 'gl-text-green-500', + message: 'Approved merge request %{targetLink} in %{resourceParentLink}.', }; - const createComponent = () => { - wrapper = shallowMountExtended(ContributionEventBase, { - propsData: defaultPropsData, + const createComponent = ({ propsData = {} } = {}) => { + wrapper = mountExtended(ContributionEventBase, { + propsData: { + ...defaultPropsData, + ...propsData, + }, scopedSlots: { default: '<div data-testid="default-slot"></div>', 'additional-info': '<div data-testid="additional-info-slot"></div>', @@ -25,38 +29,75 @@ describe('ContributionEventBase', () => { }); }; - beforeEach(() => { + it('renders avatar', () => { createComponent(); - }); - it('renders avatar', () => { const avatarLink = wrapper.findComponent(GlAvatarLink); + const avatarLabeled = avatarLink.findComponent(GlAvatarLabeled); - expect(avatarLink.attributes('href')).toBe(event.author.web_url); - expect(avatarLink.findComponent(GlAvatarLabeled).attributes()).toMatchObject({ - label: event.author.name, - sublabel: `@${event.author.username}`, - src: event.author.avatar_url, + expect(avatarLink.attributes('href')).toBe(defaultPropsData.event.author.web_url); + expect(avatarLabeled.attributes()).toMatchObject({ + src: defaultPropsData.event.author.avatar_url, size: '32', }); + expect(avatarLabeled.props()).toMatchObject({ + label: defaultPropsData.event.author.name, + subLabel: `@${defaultPropsData.event.author.username}`, + }); }); it('renders time ago tooltip', () => { - expect(wrapper.findComponent(TimeAgoTooltip).props('time')).toBe(event.created_at); + createComponent(); + + expect(wrapper.findComponent(TimeAgoTooltip).props('time')).toBe( + defaultPropsData.event.created_at, + ); }); it('renders icon', () => { + createComponent(); + const icon = wrapper.findComponent(GlIcon); expect(icon.props('name')).toBe(defaultPropsData.iconName); expect(icon.classes()).toContain(defaultPropsData.iconClass); }); - it('renders `default` slot', () => { - expect(wrapper.findByTestId('default-slot').exists()).toBe(true); + describe('when `message` prop is passed', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders message', () => { + expect(wrapper.findByTestId('event-body').text()).toBe( + `Approved merge request ${defaultPropsData.event.target.reference_link_text} in ${defaultPropsData.event.resource_parent.full_name}.`, + ); + }); + + it('renders target link', () => { + expect(wrapper.findComponent(TargetLink).props('event')).toEqual(defaultPropsData.event); + }); + + it('renders resource parent link', () => { + expect(wrapper.findComponent(ResourceParentLink).props('event')).toEqual( + defaultPropsData.event, + ); + }); + }); + + describe('when `message` prop is not passed', () => { + beforeEach(() => { + createComponent({ propsData: { message: '' } }); + }); + + it('renders `default` slot', () => { + expect(wrapper.findByTestId('default-slot').exists()).toBe(true); + }); }); it('renders `additional-info` slot', () => { + createComponent(); + expect(wrapper.findByTestId('additional-info-slot').exists()).toBe(true); }); }); diff --git a/spec/frontend/contribution_events/components/contribution_event/contribution_event_expired_spec.js b/spec/frontend/contribution_events/components/contribution_event/contribution_event_expired_spec.js index 90cb7161952..c58fca1ad12 100644 --- a/spec/frontend/contribution_events/components/contribution_event/contribution_event_expired_spec.js +++ b/spec/frontend/contribution_events/components/contribution_event/contribution_event_expired_spec.js @@ -1,19 +1,17 @@ -import events from 'test_fixtures/controller/users/activity.json'; -import { mountExtended } from 'helpers/vue_test_utils_helper'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import ContributionEventExpired from '~/contribution_events/components/contribution_event/contribution_event_expired.vue'; import ContributionEventBase from '~/contribution_events/components/contribution_event/contribution_event_base.vue'; -import ResourceParentLink from '~/contribution_events/components/resource_parent_link.vue'; import { eventExpired } from '../../utils'; const defaultPropsData = { - event: eventExpired(events), + event: eventExpired(), }; describe('ContributionEventExpired', () => { let wrapper; const createComponent = () => { - wrapper = mountExtended(ContributionEventExpired, { + wrapper = shallowMountExtended(ContributionEventExpired, { propsData: defaultPropsData, }); }; @@ -26,18 +24,7 @@ describe('ContributionEventExpired', () => { expect(wrapper.findComponent(ContributionEventBase).props()).toMatchObject({ event: defaultPropsData.event, iconName: 'expire', + message: ContributionEventExpired.i18n.message, }); }); - - it('renders message', () => { - expect(wrapper.findByTestId('event-body').text()).toBe( - `Removed due to membership expiration from ${defaultPropsData.event.resource_parent.full_name}.`, - ); - }); - - it('renders resource parent link', () => { - expect(wrapper.findComponent(ResourceParentLink).props('event')).toEqual( - defaultPropsData.event, - ); - }); }); diff --git a/spec/frontend/contribution_events/components/contribution_event/contribution_event_joined_spec.js b/spec/frontend/contribution_events/components/contribution_event/contribution_event_joined_spec.js index 511972e35dd..56688e2ef27 100644 --- a/spec/frontend/contribution_events/components/contribution_event/contribution_event_joined_spec.js +++ b/spec/frontend/contribution_events/components/contribution_event/contribution_event_joined_spec.js @@ -1,19 +1,17 @@ -import events from 'test_fixtures/controller/users/activity.json'; -import { mountExtended } from 'helpers/vue_test_utils_helper'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import ContributionEventJoined from '~/contribution_events/components/contribution_event/contribution_event_joined.vue'; import ContributionEventBase from '~/contribution_events/components/contribution_event/contribution_event_base.vue'; -import ResourceParentLink from '~/contribution_events/components/resource_parent_link.vue'; import { eventJoined } from '../../utils'; const defaultPropsData = { - event: eventJoined(events), + event: eventJoined(), }; describe('ContributionEventJoined', () => { let wrapper; const createComponent = () => { - wrapper = mountExtended(ContributionEventJoined, { + wrapper = shallowMountExtended(ContributionEventJoined, { propsData: defaultPropsData, }); }; @@ -26,18 +24,7 @@ describe('ContributionEventJoined', () => { expect(wrapper.findComponent(ContributionEventBase).props()).toMatchObject({ event: defaultPropsData.event, iconName: 'users', + message: ContributionEventJoined.i18n.message, }); }); - - it('renders message', () => { - expect(wrapper.findByTestId('event-body').text()).toBe( - `Joined project ${defaultPropsData.event.resource_parent.full_name}.`, - ); - }); - - it('renders resource parent link', () => { - expect(wrapper.findComponent(ResourceParentLink).props('event')).toEqual( - defaultPropsData.event, - ); - }); }); diff --git a/spec/frontend/contribution_events/components/contribution_event/contribution_event_left_spec.js b/spec/frontend/contribution_events/components/contribution_event/contribution_event_left_spec.js index 2e82addcda2..58cb8714d03 100644 --- a/spec/frontend/contribution_events/components/contribution_event/contribution_event_left_spec.js +++ b/spec/frontend/contribution_events/components/contribution_event/contribution_event_left_spec.js @@ -1,19 +1,17 @@ -import events from 'test_fixtures/controller/users/activity.json'; -import { mountExtended } from 'helpers/vue_test_utils_helper'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import ContributionEventLeft from '~/contribution_events/components/contribution_event/contribution_event_left.vue'; import ContributionEventBase from '~/contribution_events/components/contribution_event/contribution_event_base.vue'; -import ResourceParentLink from '~/contribution_events/components/resource_parent_link.vue'; import { eventLeft } from '../../utils'; const defaultPropsData = { - event: eventLeft(events), + event: eventLeft(), }; describe('ContributionEventLeft', () => { let wrapper; const createComponent = () => { - wrapper = mountExtended(ContributionEventLeft, { + wrapper = shallowMountExtended(ContributionEventLeft, { propsData: defaultPropsData, }); }; @@ -26,18 +24,7 @@ describe('ContributionEventLeft', () => { expect(wrapper.findComponent(ContributionEventBase).props()).toMatchObject({ event: defaultPropsData.event, iconName: 'leave', + message: ContributionEventLeft.i18n.message, }); }); - - it('renders message', () => { - expect(wrapper.findByTestId('event-body').text()).toBe( - `Left project ${defaultPropsData.event.resource_parent.full_name}.`, - ); - }); - - it('renders resource parent link', () => { - expect(wrapper.findComponent(ResourceParentLink).props('event')).toEqual( - defaultPropsData.event, - ); - }); }); diff --git a/spec/frontend/contribution_events/components/contribution_events_spec.js b/spec/frontend/contribution_events/components/contribution_events_spec.js index dcea9bacb78..064799d4a82 100644 --- a/spec/frontend/contribution_events/components/contribution_events_spec.js +++ b/spec/frontend/contribution_events/components/contribution_events_spec.js @@ -20,10 +20,10 @@ describe('ContributionEvents', () => { it.each` expectedComponent | expectedEvent - ${ContributionEventApproved} | ${eventApproved(events)} - ${ContributionEventExpired} | ${eventExpired(events)} - ${ContributionEventJoined} | ${eventJoined(events)} - ${ContributionEventLeft} | ${eventLeft(events)} + ${ContributionEventApproved} | ${eventApproved()} + ${ContributionEventExpired} | ${eventExpired()} + ${ContributionEventJoined} | ${eventJoined()} + ${ContributionEventLeft} | ${eventLeft()} `( 'renders `$expectedComponent.name` component and passes expected event', ({ expectedComponent, expectedEvent }) => { diff --git a/spec/frontend/contribution_events/components/resource_parent_link_spec.js b/spec/frontend/contribution_events/components/resource_parent_link_spec.js index 8d586db2a30..815a1b751cf 100644 --- a/spec/frontend/contribution_events/components/resource_parent_link_spec.js +++ b/spec/frontend/contribution_events/components/resource_parent_link_spec.js @@ -1,30 +1,52 @@ import { GlLink } from '@gitlab/ui'; -import events from 'test_fixtures/controller/users/activity.json'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import { EVENT_TYPE_APPROVED } from '~/contribution_events/constants'; import ResourceParentLink from '~/contribution_events/components/resource_parent_link.vue'; - -const eventApproved = events.find((event) => event.action === EVENT_TYPE_APPROVED); +import { EVENT_TYPE_PRIVATE } from '~/contribution_events/constants'; +import { eventApproved } from '../utils'; describe('ResourceParentLink', () => { let wrapper; - const createComponent = () => { + const defaultPropsData = { + event: eventApproved(), + }; + + const createComponent = ({ propsData = {} } = {}) => { wrapper = shallowMountExtended(ResourceParentLink, { propsData: { - event: eventApproved, + ...defaultPropsData, + ...propsData, }, }); }; - beforeEach(() => { - createComponent(); + describe('when resource parent is defined', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders link', () => { + const link = wrapper.findComponent(GlLink); + const { web_url, full_name } = defaultPropsData.event.resource_parent; + + expect(link.attributes('href')).toBe(web_url); + expect(link.text()).toBe(full_name); + }); }); - it('renders link', () => { - const link = wrapper.findComponent(GlLink); + describe('when resource parent is not defined', () => { + beforeEach(() => { + createComponent({ + propsData: { + event: { + type: EVENT_TYPE_PRIVATE, + }, + }, + }); + }); - expect(link.attributes('href')).toBe(eventApproved.resource_parent.web_url); - expect(link.text()).toBe(eventApproved.resource_parent.full_name); + it('renders nothing', () => { + expect(wrapper.html()).toBe(''); + }); }); }); diff --git a/spec/frontend/contribution_events/components/target_link_spec.js b/spec/frontend/contribution_events/components/target_link_spec.js index 7944375487b..b71d6eff432 100644 --- a/spec/frontend/contribution_events/components/target_link_spec.js +++ b/spec/frontend/contribution_events/components/target_link_spec.js @@ -1,33 +1,48 @@ import { GlLink } from '@gitlab/ui'; -import events from 'test_fixtures/controller/users/activity.json'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import { EVENT_TYPE_APPROVED } from '~/contribution_events/constants'; import TargetLink from '~/contribution_events/components/target_link.vue'; - -const eventApproved = events.find((event) => event.action === EVENT_TYPE_APPROVED); +import { eventApproved, eventJoined } from '../utils'; describe('TargetLink', () => { let wrapper; - const createComponent = () => { + const defaultPropsData = { + event: eventApproved(), + }; + + const createComponent = ({ propsData = {} } = {}) => { wrapper = shallowMountExtended(TargetLink, { propsData: { - event: eventApproved, + ...defaultPropsData, + ...propsData, }, }); }; - beforeEach(() => { - createComponent(); + describe('when target is defined', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders link', () => { + const link = wrapper.findComponent(GlLink); + const { web_url: webUrl, title, reference_link_text } = defaultPropsData.event.target; + + expect(link.attributes()).toMatchObject({ + href: webUrl, + title, + }); + expect(link.text()).toBe(reference_link_text); + }); }); - it('renders link', () => { - const link = wrapper.findComponent(GlLink); + describe('when target is not defined', () => { + beforeEach(() => { + createComponent({ propsData: { event: eventJoined() } }); + }); - expect(link.attributes()).toMatchObject({ - href: eventApproved.target.web_url, - title: eventApproved.target.title, + it('renders nothing', () => { + expect(wrapper.html()).toBe(''); }); - expect(link.text()).toBe(eventApproved.target.reference_link_text); }); }); diff --git a/spec/frontend/contribution_events/utils.js b/spec/frontend/contribution_events/utils.js index 0887a178e5c..736479fce04 100644 --- a/spec/frontend/contribution_events/utils.js +++ b/spec/frontend/contribution_events/utils.js @@ -1,3 +1,4 @@ +import events from 'test_fixtures/controller/users/activity.json'; import { EVENT_TYPE_APPROVED, EVENT_TYPE_EXPIRED, @@ -5,11 +6,12 @@ import { EVENT_TYPE_LEFT, } from '~/contribution_events/constants'; -export const eventApproved = (events) => - events.find((event) => event.action === EVENT_TYPE_APPROVED); +const findEventByAction = (action) => events.find((event) => event.action === action); -export const eventExpired = (events) => events.find((event) => event.action === EVENT_TYPE_EXPIRED); +export const eventApproved = () => findEventByAction(EVENT_TYPE_APPROVED); -export const eventJoined = (events) => events.find((event) => event.action === EVENT_TYPE_JOINED); +export const eventExpired = () => findEventByAction(EVENT_TYPE_EXPIRED); -export const eventLeft = (events) => events.find((event) => event.action === EVENT_TYPE_LEFT); +export const eventJoined = () => findEventByAction(EVENT_TYPE_JOINED); + +export const eventLeft = () => findEventByAction(EVENT_TYPE_LEFT); diff --git a/spec/frontend/design_management/components/design_presentation_spec.js b/spec/frontend/design_management/components/design_presentation_spec.js index fdcea6d88c0..e64dec14461 100644 --- a/spec/frontend/design_management/components/design_presentation_spec.js +++ b/spec/frontend/design_management/components/design_presentation_spec.js @@ -220,10 +220,6 @@ describe('Design management design presentation component', () => { ); }); - afterEach(() => { - jest.clearAllMocks(); - }); - it('sets overlay position correctly when overlay is smaller than viewport', () => { jest.spyOn(wrapper.vm.$refs.presentationViewport, 'offsetWidth', 'get').mockReturnValue(200); jest.spyOn(wrapper.vm.$refs.presentationViewport, 'offsetHeight', 'get').mockReturnValue(200); diff --git a/spec/frontend/design_management/components/design_todo_button_spec.js b/spec/frontend/design_management/components/design_todo_button_spec.js index 698535d8937..2262e5fdd83 100644 --- a/spec/frontend/design_management/components/design_todo_button_spec.js +++ b/spec/frontend/design_management/components/design_todo_button_spec.js @@ -50,10 +50,6 @@ describe('Design management design todo button', () => { createComponent(); }); - afterEach(() => { - jest.clearAllMocks(); - }); - it('renders TodoButton component', () => { expect(wrapper.findComponent(TodoButton).exists()).toBe(true); }); diff --git a/spec/frontend/drawio/drawio_editor_spec.js b/spec/frontend/drawio/drawio_editor_spec.js index 4d93908b757..5a77b9d4689 100644 --- a/spec/frontend/drawio/drawio_editor_spec.js +++ b/spec/frontend/drawio/drawio_editor_spec.js @@ -66,7 +66,6 @@ describe('drawio/drawio_editor', () => { }); afterEach(() => { - jest.clearAllMocks(); findDrawioIframe()?.remove(); }); diff --git a/spec/frontend/editor/source_editor_extension_base_spec.js b/spec/frontend/editor/source_editor_extension_base_spec.js index 70bc1dee0ee..c820d6ac63d 100644 --- a/spec/frontend/editor/source_editor_extension_base_spec.js +++ b/spec/frontend/editor/source_editor_extension_base_spec.js @@ -56,7 +56,6 @@ describe('The basis for an Source Editor extension', () => { }); afterEach(() => { - jest.clearAllMocks(); resetHTMLFixture(); }); diff --git a/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js b/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js index 512b298bbbd..d9e1a22d60d 100644 --- a/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js +++ b/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js @@ -182,10 +182,6 @@ describe('Markdown Live Preview Extension for Source Editor', () => { instance.togglePreview(); }); - afterEach(() => { - jest.clearAllMocks(); - }); - it('does not do anything if there is no model', () => { instance.setModel(null); @@ -199,9 +195,6 @@ describe('Markdown Live Preview Extension for Source Editor', () => { mockAxios.onPost().reply(HTTP_STATUS_OK, { body: responseData }); await togglePreview(); }); - afterEach(() => { - jest.clearAllMocks(); - }); it('removes the registered buttons from the toolbar', () => { expect(instance.toolbar.removeItems).not.toHaveBeenCalled(); diff --git a/spec/frontend/editor/source_editor_yaml_ext_spec.js b/spec/frontend/editor/source_editor_yaml_ext_spec.js index 14ec7f8b93f..4b1ed0fbb42 100644 --- a/spec/frontend/editor/source_editor_yaml_ext_spec.js +++ b/spec/frontend/editor/source_editor_yaml_ext_spec.js @@ -368,10 +368,6 @@ abc: def let highlightLinesSpy; let removeHighlightsSpy; - afterEach(() => { - jest.clearAllMocks(); - }); - it.each` highlightPathOnSetup | path | keepOnNotFound | expectHighlightLinesToBeCalled | withLines | expectRemoveHighlightsToBeCalled | storedHighlightPath ${null} | ${undefined} | ${false} | ${false} | ${undefined} | ${true} | ${null} diff --git a/spec/frontend/header_search/init_spec.js b/spec/frontend/header_search/init_spec.js index baf3c6f08b2..459ca33ee66 100644 --- a/spec/frontend/header_search/init_spec.js +++ b/spec/frontend/header_search/init_spec.js @@ -5,7 +5,6 @@ import initHeaderSearch, { eventHandler, cleanEventListeners } from '~/header_se describe('Header Search EventListener', () => { beforeEach(() => { jest.resetModules(); - jest.restoreAllMocks(); setHTMLFixture(` <div class="js-header-content"> <div class="header-search-form" id="js-header-search" data-autocomplete-path="/search/autocomplete" data-issues-path="/dashboard/issues" data-mr-path="/dashboard/merge_requests" data-search-context="{}" data-search-path="/search"> @@ -16,7 +15,6 @@ describe('Header Search EventListener', () => { afterEach(() => { resetHTMLFixture(); - jest.clearAllMocks(); }); it('attached event listener', () => { diff --git a/spec/frontend/ide/components/repo_editor_spec.js b/spec/frontend/ide/components/repo_editor_spec.js index 6747ec97050..aa99b1cacef 100644 --- a/spec/frontend/ide/components/repo_editor_spec.js +++ b/spec/frontend/ide/components/repo_editor_spec.js @@ -158,7 +158,6 @@ describe('RepoEditor', () => { }); afterEach(() => { - jest.clearAllMocks(); // create a new model each time, otherwise tests conflict with each other // because of same model being used in multiple tests monacoEditor.getModels().forEach((model) => model.dispose()); diff --git a/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js b/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js index 0a887efee4b..f4f4936a134 100644 --- a/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js +++ b/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js @@ -137,7 +137,6 @@ describe('ProjectDropdown', () => { describe('when searching branches', () => { it('triggers a refetch', async () => { createComponent({ mountFn: mount }); - jest.clearAllMocks(); const mockSearchTerm = 'gitl'; await findDropdown().vm.$emit('search', mockSearchTerm); diff --git a/spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js b/spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js index a3bc8e861b2..cf2dacb50d8 100644 --- a/spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js +++ b/spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js @@ -104,7 +104,6 @@ describe('SourceBranchDropdown', () => { it('triggers a refetch', async () => { createComponent({ mountFn: mount, props: { selectedProject: mockSelectedProject } }); await waitForPromises(); - jest.clearAllMocks(); const mockSearchTerm = 'mai'; await findListbox().vm.$emit('search', mockSearchTerm); diff --git a/spec/frontend/lib/utils/downloader_spec.js b/spec/frontend/lib/utils/downloader_spec.js index c14cba3a62b..a95b46d1440 100644 --- a/spec/frontend/lib/utils/downloader_spec.js +++ b/spec/frontend/lib/utils/downloader_spec.js @@ -8,10 +8,6 @@ describe('Downloader', () => { jest.spyOn(document, 'createElement').mockImplementation(() => a); }); - afterEach(() => { - jest.clearAllMocks(); - }); - describe('when inline file content is provided', () => { const fileData = 'inline content'; const fileName = 'test.csv'; diff --git a/spec/frontend/notes/mixins/discussion_navigation_spec.js b/spec/frontend/notes/mixins/discussion_navigation_spec.js index b6a2b318ec3..bef8ed8e659 100644 --- a/spec/frontend/notes/mixins/discussion_navigation_spec.js +++ b/spec/frontend/notes/mixins/discussion_navigation_spec.js @@ -74,7 +74,6 @@ describe('Discussion navigation mixin', () => { }); afterEach(() => { - jest.clearAllMocks(); resetHTMLFixture(); }); diff --git a/spec/frontend/pipeline_wizard/components/commit_spec.js b/spec/frontend/pipeline_wizard/components/commit_spec.js index 7095525e948..bb9a4b85e0e 100644 --- a/spec/frontend/pipeline_wizard/components/commit_spec.js +++ b/spec/frontend/pipeline_wizard/components/commit_spec.js @@ -141,10 +141,6 @@ describe('Pipeline Wizard - Commit Page', () => { it('emits a done event', () => { expect(wrapper.emitted().done.length).toBe(1); }); - - afterEach(() => { - jest.clearAllMocks(); - }); }); describe('failed commit', () => { @@ -167,10 +163,6 @@ describe('Pipeline Wizard - Commit Page', () => { it('will not emit a done event', () => { expect(wrapper.emitted().done?.length).toBeUndefined(); }); - - afterEach(() => { - jest.clearAllMocks(); - }); }); }); diff --git a/spec/frontend/pipelines/graph_shared/links_inner_spec.js b/spec/frontend/pipelines/graph_shared/links_inner_spec.js index 50f754393fe..b4ffd2658fe 100644 --- a/spec/frontend/pipelines/graph_shared/links_inner_spec.js +++ b/spec/frontend/pipelines/graph_shared/links_inner_spec.js @@ -80,7 +80,6 @@ describe('Links Inner component', () => { }; afterEach(() => { - jest.restoreAllMocks(); resetHTMLFixture(); }); diff --git a/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js b/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js index 077995ab6e4..76d45692a63 100644 --- a/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js +++ b/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js @@ -91,7 +91,6 @@ describe('View branch rules', () => { expect(findBranchName().text()).toBe(I18N.allBranches); expect(findBranchTitle().text()).toBe(I18N.targetBranch); - jest.restoreAllMocks(); }); it('renders the correct branch title', () => { diff --git a/spec/frontend/sidebar/components/todo_toggle/todo_button_spec.js b/spec/frontend/sidebar/components/todo_toggle/todo_button_spec.js index 472a89e9b21..4385db43a4a 100644 --- a/spec/frontend/sidebar/components/todo_toggle/todo_button_spec.js +++ b/spec/frontend/sidebar/components/todo_toggle/todo_button_spec.js @@ -23,7 +23,6 @@ describe('Todo Button', () => { afterEach(() => { dispatchEventSpy = null; - jest.clearAllMocks(); }); it('renders GlButton', () => { diff --git a/spec/frontend/sidebar/sidebar_mediator_spec.js b/spec/frontend/sidebar/sidebar_mediator_spec.js index f2003aee96e..9c12088216b 100644 --- a/spec/frontend/sidebar/sidebar_mediator_spec.js +++ b/spec/frontend/sidebar/sidebar_mediator_spec.js @@ -25,8 +25,6 @@ describe('Sidebar mediator', () => { SidebarService.singleton = null; SidebarStore.singleton = null; SidebarMediator.singleton = null; - - jest.clearAllMocks(); }); it('assigns yourself', () => { diff --git a/spec/frontend/super_sidebar/components/sidebar_peek_behavior_spec.js b/spec/frontend/super_sidebar/components/sidebar_peek_behavior_spec.js index 047dc9a6599..abd9c1dc44d 100644 --- a/spec/frontend/super_sidebar/components/sidebar_peek_behavior_spec.js +++ b/spec/frontend/super_sidebar/components/sidebar_peek_behavior_spec.js @@ -9,6 +9,7 @@ import SidebarPeek, { STATE_OPEN, STATE_WILL_CLOSE, } from '~/super_sidebar/components/sidebar_peek_behavior.vue'; +import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; // These are measured at runtime in the browser, but statically defined here // since Jest does not do layout/styling. @@ -32,6 +33,7 @@ jest.mock('~/lib/utils/css_utils', () => ({ describe('SidebarPeek component', () => { let wrapper; + let trackingSpy = null; const createComponent = () => { wrapper = mount(SidebarPeek); @@ -54,6 +56,11 @@ describe('SidebarPeek component', () => { beforeEach(() => { createComponent(); + trackingSpy = mockTracking(undefined, undefined, jest.spyOn); + }); + + afterEach(() => { + unmockTracking(); }); it('begins in the closed state', () => { @@ -87,6 +94,11 @@ describe('SidebarPeek component', () => { jest.advanceTimersByTime(1); expect(lastNChangeEvents(2)).toEqual([STATE_WILL_OPEN, STATE_OPEN]); + + expect(trackingSpy).toHaveBeenCalledWith(undefined, 'nav_peek', { + label: 'nav_hover', + property: 'nav_sidebar', + }); }); it('cancels transition will-open -> open if mouse out of peek region', () => { diff --git a/spec/frontend/super_sidebar/components/super_sidebar_spec.js b/spec/frontend/super_sidebar/components/super_sidebar_spec.js index b76c637caf4..0c785109b5e 100644 --- a/spec/frontend/super_sidebar/components/super_sidebar_spec.js +++ b/spec/frontend/super_sidebar/components/super_sidebar_spec.js @@ -19,6 +19,7 @@ import { isCollapsed, } from '~/super_sidebar/super_sidebar_collapsed_state_manager'; import { stubComponent } from 'helpers/stub_component'; +import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; import { sidebarData as mockSidebarData } from '../mock_data'; const initialSidebarState = { ...sidebarState }; @@ -49,6 +50,7 @@ describe('SuperSidebar component', () => { const findTrialStatusWidget = () => wrapper.findByTestId(trialStatusWidgetStubTestId); const findTrialStatusPopover = () => wrapper.findByTestId(trialStatusPopoverStubTestId); const findSidebarMenu = () => wrapper.findComponent(SidebarMenu); + let trackingSpy = null; const createWrapper = ({ provide = {}, @@ -77,6 +79,11 @@ describe('SuperSidebar component', () => { beforeEach(() => { Object.assign(sidebarState, initialSidebarState); + trackingSpy = mockTracking(undefined, undefined, jest.spyOn); + }); + + afterEach(() => { + unmockTracking(); }); describe('default', () => { @@ -143,12 +150,20 @@ describe('SuperSidebar component', () => { expect(toggleSuperSidebarCollapsed).toHaveBeenCalledTimes(1); expect(toggleSuperSidebarCollapsed).toHaveBeenCalledWith(true, true); + expect(trackingSpy).toHaveBeenCalledWith(undefined, 'nav_hide', { + label: 'nav_toggle_keyboard_shortcut', + property: 'nav_sidebar', + }); isCollapsed.mockReturnValue(true); Mousetrap.trigger('mod+\\'); expect(toggleSuperSidebarCollapsed).toHaveBeenCalledTimes(2); expect(toggleSuperSidebarCollapsed).toHaveBeenCalledWith(false, true); + expect(trackingSpy).toHaveBeenCalledWith(undefined, 'nav_show', { + label: 'nav_toggle_keyboard_shortcut', + property: 'nav_sidebar', + }); jest.spyOn(Mousetrap, 'unbind'); diff --git a/spec/frontend/super_sidebar/components/super_sidebar_toggle_spec.js b/spec/frontend/super_sidebar/components/super_sidebar_toggle_spec.js index 8bb20186e16..23b735c2773 100644 --- a/spec/frontend/super_sidebar/components/super_sidebar_toggle_spec.js +++ b/spec/frontend/super_sidebar/components/super_sidebar_toggle_spec.js @@ -7,6 +7,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { JS_TOGGLE_COLLAPSE_CLASS, JS_TOGGLE_EXPAND_CLASS } from '~/super_sidebar/constants'; import SuperSidebarToggle from '~/super_sidebar/components/super_sidebar_toggle.vue'; import { toggleSuperSidebarCollapsed } from '~/super_sidebar/super_sidebar_collapsed_state_manager'; +import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; jest.mock('~/super_sidebar/super_sidebar_collapsed_state_manager.js', () => ({ toggleSuperSidebarCollapsed: jest.fn(), @@ -61,7 +62,7 @@ describe('SuperSidebarToggle component', () => { }); }); - describe('toolip', () => { + describe('tooltip', () => { it('displays collapse when expanded', () => { createWrapper(); expect(getTooltip().title).toBe(__('Hide sidebar')); @@ -74,15 +75,19 @@ describe('SuperSidebarToggle component', () => { }); describe('toggle', () => { + let trackingSpy = null; + beforeEach(() => { setHTMLFixture(` <button class="${JS_TOGGLE_COLLAPSE_CLASS}">Hide</button> <button class="${JS_TOGGLE_EXPAND_CLASS}">Show</button> `); + trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); }); afterEach(() => { resetHTMLFixture(); + unmockTracking(); }); it('collapses the sidebar and focuses the other toggle', async () => { @@ -93,6 +98,10 @@ describe('SuperSidebarToggle component', () => { expect(document.activeElement).toEqual( document.querySelector(`.${JS_TOGGLE_COLLAPSE_CLASS}`), ); + expect(trackingSpy).toHaveBeenCalledWith(undefined, 'nav_hide', { + label: 'nav_toggle', + property: 'nav_sidebar', + }); }); it('expands the sidebar and focuses the other toggle', async () => { @@ -101,6 +110,10 @@ describe('SuperSidebarToggle component', () => { await nextTick(); expect(toggleSuperSidebarCollapsed).toHaveBeenCalledWith(false, true); expect(document.activeElement).toEqual(document.querySelector(`.${JS_TOGGLE_EXPAND_CLASS}`)); + expect(trackingSpy).toHaveBeenCalledWith(undefined, 'nav_show', { + label: 'nav_toggle', + property: 'nav_sidebar', + }); }); }); }); diff --git a/spec/frontend/super_sidebar/super_sidebar_collapsed_state_manager_spec.js b/spec/frontend/super_sidebar/super_sidebar_collapsed_state_manager_spec.js index 771d1f07fea..9388d837186 100644 --- a/spec/frontend/super_sidebar/super_sidebar_collapsed_state_manager_spec.js +++ b/spec/frontend/super_sidebar/super_sidebar_collapsed_state_manager_spec.js @@ -11,8 +11,10 @@ import { findPage, bindSuperSidebarCollapsedEvents, } from '~/super_sidebar/super_sidebar_collapsed_state_manager'; +import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; const { xl, sm } = breakpoints; +let trackingSpy = null; jest.mock('~/lib/utils/common_utils', () => ({ getCookie: jest.fn(), @@ -27,6 +29,15 @@ const pageHasCollapsedClass = (hasClass) => { } }; +const tracksCollapse = (shouldTrack) => { + if (shouldTrack) { + expect(trackingSpy).toHaveBeenCalledWith(undefined, 'nav_hide', { + label: 'browser_resize', + property: 'nav_sidebar', + }); + } +}; + describe('Super Sidebar Collapsed State Manager', () => { beforeEach(() => { setHTMLFixture(` @@ -34,10 +45,12 @@ describe('Super Sidebar Collapsed State Manager', () => { <aside class="super-sidebar"></aside> </div> `); + trackingSpy = mockTracking(undefined, undefined, jest.spyOn); }); afterEach(() => { resetHTMLFixture(); + unmockTracking(); }); describe('toggleSuperSidebarCollapsed', () => { @@ -109,14 +122,20 @@ describe('Super Sidebar Collapsed State Manager', () => { }); it.each` - initialWindowWidth | updatedWindowWidth | hasClassBeforeResize | hasClassAfterResize - ${xl} | ${sm} | ${false} | ${true} - ${sm} | ${xl} | ${true} | ${false} - ${xl} | ${xl} | ${false} | ${false} - ${sm} | ${sm} | ${true} | ${true} + initialWindowWidth | updatedWindowWidth | hasClassBeforeResize | hasClassAfterResize | sendsTrackingEvent + ${xl} | ${sm} | ${false} | ${true} | ${true} + ${sm} | ${xl} | ${true} | ${false} | ${false} + ${xl} | ${xl} | ${false} | ${false} | ${false} + ${sm} | ${sm} | ${true} | ${true} | ${false} `( 'when changing width from $initialWindowWidth to $updatedWindowWidth expect page to have collapsed class before resize to be $hasClassBeforeResize and after resize to be $hasClassAfterResize', - ({ initialWindowWidth, updatedWindowWidth, hasClassBeforeResize, hasClassAfterResize }) => { + ({ + initialWindowWidth, + updatedWindowWidth, + hasClassBeforeResize, + hasClassAfterResize, + sendsTrackingEvent, + }) => { getCookie.mockReturnValue(undefined); window.innerWidth = initialWindowWidth; initSuperSidebarCollapsedState(); @@ -129,6 +148,7 @@ describe('Super Sidebar Collapsed State Manager', () => { window.dispatchEvent(new Event('resize')); pageHasCollapsedClass(hasClassAfterResize); + tracksCollapse(sendsTrackingEvent); }, ); }); diff --git a/spec/frontend/tracking/tracking_spec.js b/spec/frontend/tracking/tracking_spec.js index c23790bb589..55ce8039399 100644 --- a/spec/frontend/tracking/tracking_spec.js +++ b/spec/frontend/tracking/tracking_spec.js @@ -59,7 +59,6 @@ describe('Tracking', () => { window.doNotTrack = undefined; navigator.doNotTrack = undefined; navigator.msDoNotTrack = undefined; - jest.clearAllMocks(); }); it('tracks to snowplow (our current tracking system)', () => { diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js index 07fc0be9e51..123ff7ded63 100644 --- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js @@ -113,11 +113,6 @@ const createComponent = (customConfig = {}, createState = true) => { GlSprintf, }, apolloProvider: createMockApollo([[readyToMergeQuery, readyToMergeResponseSpy]]), - provide: { - glFeatures: { - autoMergeLabelsMrWidget: false, - }, - }, }); }; @@ -144,6 +139,7 @@ const findDeleteSourceBranchCheckbox = () => const triggerApprovalUpdated = () => eventHub.$emit('ApprovalUpdated'); const triggerEditCommitInput = () => wrapper.find('[data-testid="widget_edit_commit_message"]').vm.$emit('input', true); +const findMergeHelperText = () => wrapper.find('[data-testid="auto-merge-helper-text"]'); describe('ReadyToMerge', () => { beforeEach(() => { @@ -185,26 +181,6 @@ describe('ReadyToMerge', () => { expect(wrapper.vm.status).toEqual('failed'); }); }); - - describe('status icon', () => { - it('defaults to tick icon', () => { - createComponent({ mr: { mergeable: true } }); - - expect(wrapper.vm.iconClass).toEqual('success'); - }); - - it('shows tick for success status', () => { - createComponent({ mr: { pipeline: { status: 'SUCCESS' }, mergeable: true } }); - - expect(wrapper.vm.iconClass).toEqual('success'); - }); - - it('shows tick for pending status', () => { - createComponent({ mr: { pipeline: { active: true }, mergeable: true } }); - - expect(wrapper.vm.iconClass).toEqual('success'); - }); - }); }); describe('merge button text', () => { @@ -214,18 +190,11 @@ describe('ReadyToMerge', () => { expect(findMergeButton().text()).toBe('Merge'); }); - it('should return "Merge when pipeline succeeds" when the MWPS auto merge strategy is available', () => { - createComponent({ - mr: { preferredAutoMergeStrategy: MWPS_MERGE_STRATEGY }, - }); - - expect(findMergeButton().text()).toBe('Merge when pipeline succeeds'); - }); - - it('should return Merge when pipeline succeeds', () => { + it('should return Set to auto-merge in the button and Merge when pipeline succeeds in the helper text', () => { createComponent({ mr: { preferredAutoMergeStrategy: MWPS_MERGE_STRATEGY } }); - expect(findMergeButton().text()).toBe('Merge when pipeline succeeds'); + expect(findMergeButton().text()).toBe('Set to auto-merge'); + expect(findMergeHelperText().text()).toBe('Merge when pipeline succeeds'); }); }); diff --git a/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js b/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js index 6acd1f51a86..1f3029435ee 100644 --- a/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js +++ b/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js @@ -1,3 +1,4 @@ +import { nextTick } from 'vue'; import { shallowMount } from '@vue/test-utils'; import { handleBlobRichViewer } from '~/blob/viewer'; import RichViewer from '~/vue_shared/components/blob_viewers/rich_viewer.vue'; @@ -21,16 +22,24 @@ describe('Blob Rich Viewer component', () => { } beforeEach(() => { + const execImmediately = (callback) => callback(); + jest.spyOn(window, 'requestIdleCallback').mockImplementation(execImmediately); + createComponent(); }); + it('listens to requestIdleCallback', () => { + expect(window.requestIdleCallback).toHaveBeenCalled(); + }); + it('renders the passed content without transformations', () => { expect(wrapper.html()).toContain(content); }); - it('renders the richViewer if one is present', () => { + it('renders the richViewer if one is present', async () => { const richViewer = '<div class="js-pdf-viewer"></div>'; createComponent('pdf', richViewer); + await nextTick(); expect(wrapper.html()).toContain(richViewer); }); diff --git a/spec/frontend/vue_shared/components/source_viewer/components/chunk_new_spec.js b/spec/frontend/vue_shared/components/source_viewer/components/chunk_new_spec.js index 919abc26e05..1154c930e5d 100644 --- a/spec/frontend/vue_shared/components/source_viewer/components/chunk_new_spec.js +++ b/spec/frontend/vue_shared/components/source_viewer/components/chunk_new_spec.js @@ -40,8 +40,6 @@ describe('Chunk component', () => { describe('rendering', () => { it('does not register window.requestIdleCallback for the first chunk, renders content immediately', () => { - jest.clearAllMocks(); - expect(window.requestIdleCallback).not.toHaveBeenCalled(); expect(findContent().text()).toBe(CHUNK_1.highlightedContent); }); diff --git a/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js b/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js index 18d7b2397c9..e4180b2d178 100644 --- a/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js +++ b/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js @@ -5,8 +5,6 @@ import createMockApollo from 'helpers/mock_apollo_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { createMockDirective } from 'helpers/vue_mock_directive'; import { stubComponent } from 'helpers/stub_component'; -import EmojiPicker from '~/emoji/components/picker.vue'; -import waitForPromises from 'helpers/wait_for_promises'; import ReplyButton from '~/notes/components/note_actions/reply_button.vue'; import WorkItemNoteActions from '~/work_items/components/notes/work_item_note_actions.vue'; import addAwardEmojiMutation from '~/work_items/graphql/notes/work_item_note_add_award_emoji.mutation.graphql'; @@ -36,11 +34,6 @@ describe('Work Item Note Actions', () => { }, }); - const EmojiPickerStub = { - props: EmojiPicker.props, - template: '<div></div>', - }; - const createComponent = ({ showReply = true, showEdit = true, @@ -57,6 +50,8 @@ describe('Work Item Note Actions', () => { propsData: { showReply, showEdit, + workItemIid: '1', + note: {}, noteId, showAwardEmoji, showAssignUnassign, @@ -68,12 +63,12 @@ describe('Work Item Note Actions', () => { projectName, }, provide: { + fullPath: 'gitlab-org', glFeatures: { workItemsMvc2: true, }, }, stubs: { - EmojiPicker: EmojiPickerStub, GlDisclosureDropdown: stubComponent(GlDisclosureDropdown, { methods: { close: showSpy }, }), @@ -136,22 +131,6 @@ describe('Work Item Note Actions', () => { expect(findEmojiButton().exists()).toBe(false); }); - - it('commits mutation on click', async () => { - const awardName = 'carrot'; - - createComponent(); - - findEmojiButton().vm.$emit('click', awardName); - - await waitForPromises(); - - expect(findEmojiButton().emitted('errors')).toEqual(undefined); - expect(addEmojiMutationResolver).toHaveBeenCalledWith({ - awardableId: noteId, - name: awardName, - }); - }); }); describe('delete note', () => { diff --git a/spec/frontend/work_items/components/notes/work_item_note_awards_list_spec.js b/spec/frontend/work_items/components/notes/work_item_note_awards_list_spec.js new file mode 100644 index 00000000000..d425f1e50dc --- /dev/null +++ b/spec/frontend/work_items/components/notes/work_item_note_awards_list_spec.js @@ -0,0 +1,147 @@ +import { shallowMount } from '@vue/test-utils'; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import mockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import { __ } from '~/locale'; +import AwardsList from '~/vue_shared/components/awards_list.vue'; +import WorkItemNoteAwardsList from '~/work_items/components/notes/work_item_note_awards_list.vue'; +import addAwardEmojiMutation from '~/work_items/graphql/notes/work_item_note_add_award_emoji.mutation.graphql'; +import removeAwardEmojiMutation from '~/work_items/graphql/notes/work_item_note_remove_award_emoji.mutation.graphql'; +import workItemNotesByIidQuery from '~/work_items/graphql/notes/work_item_notes_by_iid.query.graphql'; +import { + mockWorkItemNotesResponseWithComments, + mockAwardEmojiThumbsUp, +} from 'jest/work_items/mock_data'; +import { EMOJI_THUMBSUP, EMOJI_THUMBSDOWN } from '~/work_items/constants'; + +Vue.use(VueApollo); + +describe('Work Item Note Awards List', () => { + let wrapper; + const workItem = mockWorkItemNotesResponseWithComments.data.workspace.workItems.nodes[0]; + const firstNote = workItem.widgets.find((w) => w.type === 'NOTES').discussions.nodes[0].notes + .nodes[0]; + const fullPath = 'test-project-path'; + const workItemIid = workItem.iid; + const currentUserId = getIdFromGraphQLId(mockAwardEmojiThumbsUp.user.id); + + const addAwardEmojiMutationSuccessHandler = jest.fn().mockResolvedValue({ + data: { + awardEmojiAdd: { + errors: [], + }, + }, + }); + const removeAwardEmojiMutationSuccessHandler = jest.fn().mockResolvedValue({ + data: { + awardEmojiRemove: { + errors: [], + }, + }, + }); + + const findAwardsList = () => wrapper.findComponent(AwardsList); + + const createComponent = ({ + note = firstNote, + addAwardEmojiMutationHandler = addAwardEmojiMutationSuccessHandler, + removeAwardEmojiMutationHandler = removeAwardEmojiMutationSuccessHandler, + } = {}) => { + const apolloProvider = mockApollo([ + [addAwardEmojiMutation, addAwardEmojiMutationHandler], + [removeAwardEmojiMutation, removeAwardEmojiMutationHandler], + ]); + + apolloProvider.clients.defaultClient.writeQuery({ + query: workItemNotesByIidQuery, + variables: { fullPath, iid: workItemIid }, + ...mockWorkItemNotesResponseWithComments, + }); + + wrapper = shallowMount(WorkItemNoteAwardsList, { + provide: { + fullPath, + }, + propsData: { + workItemIid, + note, + isModal: false, + }, + apolloProvider, + }); + }; + + beforeEach(() => { + window.gon.current_user_id = currentUserId; + }); + + describe('when not editing', () => { + it.each([true, false])('passes emoji permission to awards-list', (hasAwardEmojiPermission) => { + const note = { + ...firstNote, + userPermissions: { + ...firstNote.userPermissions, + awardEmoji: hasAwardEmojiPermission, + }, + }; + createComponent({ note }); + + expect(findAwardsList().props('canAwardEmoji')).toBe(hasAwardEmojiPermission); + }); + + it('adds award if not already awarded', async () => { + createComponent(); + await waitForPromises(); + + findAwardsList().vm.$emit('award', EMOJI_THUMBSUP); + + expect(addAwardEmojiMutationSuccessHandler).toHaveBeenCalledWith({ + awardableId: firstNote.id, + name: EMOJI_THUMBSUP, + }); + }); + + it('emits error if awarding emoji fails', async () => { + createComponent({ + addAwardEmojiMutationHandler: jest.fn().mockRejectedValue('oh no'), + }); + await waitForPromises(); + + findAwardsList().vm.$emit('award', EMOJI_THUMBSUP); + + await waitForPromises(); + + expect(wrapper.emitted('error')).toEqual([[__('Failed to add emoji. Please try again')]]); + }); + + it('removes award if already awarded', async () => { + const removeAwardEmojiMutationHandler = removeAwardEmojiMutationSuccessHandler; + + createComponent({ removeAwardEmojiMutationHandler }); + + findAwardsList().vm.$emit('award', EMOJI_THUMBSDOWN); + + await waitForPromises(); + + expect(removeAwardEmojiMutationHandler).toHaveBeenCalledWith({ + awardableId: firstNote.id, + name: EMOJI_THUMBSDOWN, + }); + }); + + it('restores award if remove fails', async () => { + createComponent({ + removeAwardEmojiMutationHandler: jest.fn().mockRejectedValue('oh no'), + }); + await waitForPromises(); + + findAwardsList().vm.$emit('award', EMOJI_THUMBSDOWN); + + await waitForPromises(); + + expect(wrapper.emitted('error')).toEqual([[__('Failed to remove emoji. Please try again')]]); + }); + }); +}); diff --git a/spec/frontend/work_items/components/notes/work_item_note_spec.js b/spec/frontend/work_items/components/notes/work_item_note_spec.js index 8dbd2818fc5..09299f1733c 100644 --- a/spec/frontend/work_items/components/notes/work_item_note_spec.js +++ b/spec/frontend/work_items/components/notes/work_item_note_spec.js @@ -6,6 +6,7 @@ import waitForPromises from 'helpers/wait_for_promises'; import { updateDraft, clearDraft } from '~/lib/utils/autosave'; import EditedAt from '~/issues/show/components/edited.vue'; import WorkItemNote from '~/work_items/components/notes/work_item_note.vue'; +import WorkItemNoteAwardsList from '~/work_items/components/notes/work_item_note_awards_list.vue'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; import NoteBody from '~/work_items/components/notes/work_item_note_body.vue'; import NoteHeader from '~/notes/components/note_header.vue'; @@ -76,6 +77,7 @@ describe('Work Item Note', () => { const errorHandler = jest.fn().mockRejectedValue('Oops'); + const findAwardsList = () => wrapper.findComponent(WorkItemNoteAwardsList); const findTimelineEntryItem = () => wrapper.findComponent(TimelineEntryItem); const findNoteHeader = () => wrapper.findComponent(NoteHeader); const findNoteBody = () => wrapper.findComponent(NoteBody); @@ -92,10 +94,14 @@ describe('Work Item Note', () => { updateWorkItemMutationHandler = updateWorkItemMutationSuccessHandler, assignees = mockAssignees, workItemByIidResponseHandler = workItemResponseHandler, + workItemsMvc2 = false, } = {}) => { wrapper = shallowMount(WorkItemNote, { provide: { fullPath: 'test-project-path', + glFeatures: { + workItemsMvc2, + }, }, propsData: { workItemId, @@ -404,5 +410,18 @@ describe('Work Item Note', () => { }); }); }); + + it('does not show awards when feature flag disabled', () => { + createComponent(); + + expect(findAwardsList().exists()).toBe(false); + }); + + it('passes note props to awards list', () => { + createComponent({ note: mockWorkItemCommentNote, workItemsMvc2: true }); + + expect(findAwardsList().props('note')).toBe(mockWorkItemCommentNote); + expect(findAwardsList().props('workItemIid')).toBe('1'); + }); }); }); diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js index c9c73d29903..41f3adb6703 100644 --- a/spec/frontend/work_items/mock_data.js +++ b/spec/frontend/work_items/mock_data.js @@ -1982,6 +1982,9 @@ export const mockWorkItemNotesResponse = { webUrl: 'http://127.0.0.1:3000/root', __typename: 'UserCore', }, + awardEmoji: { + nodes: [], + }, __typename: 'Note', }, ], @@ -2093,6 +2096,9 @@ export const mockWorkItemNotesByIidResponse = { webUrl: 'http://127.0.0.1:3000/root', __typename: 'UserCore', }, + awardEmoji: { + nodes: [], + }, __typename: 'Note', }, ], @@ -2146,6 +2152,9 @@ export const mockWorkItemNotesByIidResponse = { webUrl: 'http://127.0.0.1:3000/root', __typename: 'UserCore', }, + awardEmoji: { + nodes: [], + }, __typename: 'Note', }, ], @@ -2200,6 +2209,9 @@ export const mockWorkItemNotesByIidResponse = { webUrl: 'http://127.0.0.1:3000/root', __typename: 'UserCore', }, + awardEmoji: { + nodes: [], + }, __typename: 'Note', }, ], @@ -2312,6 +2324,9 @@ export const mockMoreWorkItemNotesResponse = { webUrl: 'http://127.0.0.1:3000/root', __typename: 'UserCore', }, + awardEmoji: { + nodes: [], + }, __typename: 'Note', }, ], @@ -2365,6 +2380,9 @@ export const mockMoreWorkItemNotesResponse = { webUrl: 'http://127.0.0.1:3000/root', __typename: 'UserCore', }, + awardEmoji: { + nodes: [], + }, __typename: 'Note', }, ], @@ -2416,6 +2434,9 @@ export const mockMoreWorkItemNotesResponse = { webUrl: 'http://127.0.0.1:3000/root', __typename: 'UserCore', }, + awardEmoji: { + nodes: [], + }, __typename: 'Note', }, ], @@ -2483,6 +2504,9 @@ export const createWorkItemNoteResponse = { repositionNote: true, __typename: 'NotePermissions', }, + awardEmoji: { + nodes: [], + }, __typename: 'Note', }, ], @@ -2534,6 +2558,9 @@ export const mockWorkItemCommentNote = { webUrl: 'http://127.0.0.1:3000/root', __typename: 'UserCore', }, + awardEmoji: { + nodes: [mockAwardEmojiThumbsDown], + }, }; export const mockWorkItemCommentNoteByContributor = { @@ -2633,6 +2660,9 @@ export const mockWorkItemNotesResponseWithComments = { repositionNote: true, __typename: 'NotePermissions', }, + awardEmoji: { + nodes: [mockAwardEmojiThumbsDown], + }, __typename: 'Note', }, { @@ -2673,6 +2703,9 @@ export const mockWorkItemNotesResponseWithComments = { repositionNote: true, __typename: 'NotePermissions', }, + awardEmoji: { + nodes: [], + }, __typename: 'Note', }, ], @@ -2721,6 +2754,9 @@ export const mockWorkItemNotesResponseWithComments = { webUrl: 'http://127.0.0.1:3000/root', __typename: 'UserCore', }, + awardEmoji: { + nodes: [], + }, __typename: 'Note', }, ], @@ -2797,6 +2833,9 @@ export const workItemNotesCreateSubscriptionResponse = { webUrl: 'http://127.0.0.1:3000/root', __typename: 'UserCore', }, + awardEmoji: { + nodes: [], + }, __typename: 'Note', }, ], @@ -2824,6 +2863,9 @@ export const workItemNotesCreateSubscriptionResponse = { webUrl: 'http://127.0.0.1:3000/root', __typename: 'UserCore', }, + awardEmoji: { + nodes: [], + }, __typename: 'Note', }, }, @@ -2869,6 +2911,9 @@ export const workItemNotesUpdateSubscriptionResponse = { webUrl: 'http://127.0.0.1:3000/root', __typename: 'UserCore', }, + awardEmoji: { + nodes: [], + }, __typename: 'Note', }, }, @@ -3028,6 +3073,9 @@ export const workItemNotesWithSystemNotesWithChangedDescription = { }, __typename: 'SystemNoteMetadata', }, + awardEmoji: { + nodes: [], + }, __typename: 'Note', }, ], @@ -3091,6 +3139,9 @@ export const workItemNotesWithSystemNotesWithChangedDescription = { }, __typename: 'SystemNoteMetadata', }, + awardEmoji: { + nodes: [], + }, __typename: 'Note', }, ], @@ -3154,6 +3205,9 @@ export const workItemNotesWithSystemNotesWithChangedDescription = { }, __typename: 'SystemNoteMetadata', }, + awardEmoji: { + nodes: [], + }, __typename: 'Note', }, ], diff --git a/spec/frontend/work_items/notes/award_utils_spec.js b/spec/frontend/work_items/notes/award_utils_spec.js new file mode 100644 index 00000000000..8ae32ce5f40 --- /dev/null +++ b/spec/frontend/work_items/notes/award_utils_spec.js @@ -0,0 +1,109 @@ +import { getMutation, optimisticAwardUpdate } from '~/work_items/notes/award_utils'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import mockApollo from 'helpers/mock_apollo_helper'; +import { __ } from '~/locale'; +import workItemNotesByIidQuery from '~/work_items/graphql/notes/work_item_notes_by_iid.query.graphql'; +import addAwardEmojiMutation from '~/work_items/graphql/notes/work_item_note_add_award_emoji.mutation.graphql'; +import removeAwardEmojiMutation from '~/work_items/graphql/notes/work_item_note_remove_award_emoji.mutation.graphql'; +import { + mockWorkItemNotesResponseWithComments, + mockAwardEmojiThumbsUp, + mockAwardEmojiThumbsDown, +} from '../mock_data'; + +function getWorkItem(data) { + return data.workspace.workItems.nodes[0]; +} +function getFirstNote(workItem) { + return workItem.widgets.find((w) => w.type === 'NOTES').discussions.nodes[0].notes.nodes[0]; +} + +describe('Work item note award utils', () => { + const workItem = getWorkItem(mockWorkItemNotesResponseWithComments.data); + const firstNote = getFirstNote(workItem); + const fullPath = 'test-project-path'; + const workItemIid = workItem.iid; + const currentUserId = getIdFromGraphQLId(mockAwardEmojiThumbsDown.user.id); + + beforeEach(() => { + window.gon = { current_user_id: currentUserId }; + }); + + describe('getMutation', () => { + it('returns remove mutation when user has already awarded award', () => { + const note = firstNote; + const { name } = mockAwardEmojiThumbsDown; + + expect(getMutation({ note, name })).toEqual({ + mutation: removeAwardEmojiMutation, + mutationName: 'awardEmojiRemove', + errorMessage: __('Failed to remove emoji. Please try again'), + }); + }); + + it('returns remove mutation when user has not already awarded award', () => { + const note = {}; + const { name } = mockAwardEmojiThumbsUp; + + expect(getMutation({ note, name })).toEqual({ + mutation: addAwardEmojiMutation, + mutationName: 'awardEmojiAdd', + errorMessage: __('Failed to add emoji. Please try again'), + }); + }); + }); + + describe('optimisticAwardUpdate', () => { + let apolloProvider; + beforeEach(() => { + apolloProvider = mockApollo(); + + apolloProvider.clients.defaultClient.writeQuery({ + query: workItemNotesByIidQuery, + variables: { fullPath, iid: workItemIid }, + ...mockWorkItemNotesResponseWithComments, + }); + }); + + it('adds new emoji to cache', () => { + const note = firstNote; + const { name } = mockAwardEmojiThumbsUp; + + const updateFn = optimisticAwardUpdate({ note, name, fullPath, workItemIid }); + + updateFn(apolloProvider.clients.defaultClient.cache); + + const updatedResult = apolloProvider.clients.defaultClient.readQuery({ + query: workItemNotesByIidQuery, + variables: { fullPath, iid: workItemIid }, + }); + + const updatedWorkItem = getWorkItem(updatedResult); + const updatedNote = getFirstNote(updatedWorkItem); + + expect(updatedNote.awardEmoji.nodes).toEqual([ + mockAwardEmojiThumbsDown, + mockAwardEmojiThumbsUp, + ]); + }); + + it('removes existing emoji from cache', () => { + const note = firstNote; + const { name } = mockAwardEmojiThumbsDown; + + const updateFn = optimisticAwardUpdate({ note, name, fullPath, workItemIid }); + + updateFn(apolloProvider.clients.defaultClient.cache); + + const updatedResult = apolloProvider.clients.defaultClient.readQuery({ + query: workItemNotesByIidQuery, + variables: { fullPath, iid: workItemIid }, + }); + + const updatedWorkItem = getWorkItem(updatedResult); + const updatedNote = getFirstNote(updatedWorkItem); + + expect(updatedNote.awardEmoji.nodes).toEqual([]); + }); + }); +}); diff --git a/spec/models/organizations/organization_spec.rb b/spec/models/organizations/organization_spec.rb index a3c6b9edf0d..f462fbefccc 100644 --- a/spec/models/organizations/organization_spec.rb +++ b/spec/models/organizations/organization_spec.rb @@ -9,6 +9,8 @@ RSpec.describe Organizations::Organization, type: :model, feature_category: :cel describe 'associations' do it { is_expected.to have_many :namespaces } it { is_expected.to have_many :groups } + it { is_expected.to have_many(:users).through(:organization_users).inverse_of(:organizations) } + it { is_expected.to have_many(:organization_users).inverse_of(:organization) } end describe 'validations' do diff --git a/spec/models/organizations/organization_user_spec.rb b/spec/models/organizations/organization_user_spec.rb new file mode 100644 index 00000000000..392ffa1b5be --- /dev/null +++ b/spec/models/organizations/organization_user_spec.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Organizations::OrganizationUser, type: :model, feature_category: :cell do + describe 'associations' do + it { is_expected.to belong_to(:organization).inverse_of(:organization_users).required } + it { is_expected.to belong_to(:user).inverse_of(:organization_users).required } + end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 9709684f44b..1a7d86328e7 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -186,6 +186,15 @@ RSpec.describe User, feature_category: :user_profile do it { is_expected.to have_many(:merge_request_assignment_events).class_name('ResourceEvents::MergeRequestAssignmentEvent') } it do + is_expected.to have_many(:organization_users).class_name('Organizations::OrganizationUser').inverse_of(:user) + end + + it do + is_expected.to have_many(:organizations) + .through(:organization_users).class_name('Organizations::Organization').inverse_of(:users) + end + + it do is_expected.to have_many(:alert_assignees).class_name('::AlertManagement::AlertAssignee').inverse_of(:assignee) end diff --git a/spec/support/formatters/json_formatter.rb b/spec/support/formatters/json_formatter.rb index 10af5445b7a..e9d65af710a 100644 --- a/spec/support/formatters/json_formatter.rb +++ b/spec/support/formatters/json_formatter.rb @@ -74,7 +74,8 @@ module Support product_group: example.metadata[:product_group], feature_category: example.metadata[:feature_category], ci_job_url: ENV['CI_JOB_URL'], - retry_attempts: example.metadata[:retry_attempts] + retry_attempts: example.metadata[:retry_attempts], + level: example.metadata[:level] } end |