diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-08-31 12:13:12 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-08-31 12:13:12 +0300 |
commit | 6170bdc060501ecf6f817a530b3dc9f2e39ad4c3 (patch) | |
tree | c0e92981040c3aa0b2b78de816ee51cfb53ce661 /spec | |
parent | 136651d7cb69357d2823adefac430df389e81e17 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
21 files changed, 342 insertions, 286 deletions
diff --git a/spec/controllers/concerns/product_analytics_tracking_spec.rb b/spec/controllers/concerns/product_analytics_tracking_spec.rb index 250cc3cf2cf..2e734d81ea0 100644 --- a/spec/controllers/concerns/product_analytics_tracking_spec.rb +++ b/spec/controllers/concerns/product_analytics_tracking_spec.rb @@ -18,7 +18,7 @@ RSpec.describe ProductAnalyticsTracking, :snowplow do skip_before_action :authenticate_user!, only: :show track_event(:index, :show, name: 'g_analytics_valuestream', destinations: [:redis_hll, :snowplow], - conditions: [:custom_condition_one?, :custom_condition_two?]) { |controller| controller.get_custom_id } + conditions: [:custom_condition_one?, :custom_condition_two?]) { |controller| controller.get_custom_id } def index render html: 'index' diff --git a/spec/controllers/concerns/redis_tracking_spec.rb b/spec/controllers/concerns/redis_tracking_spec.rb index 178684ae2d0..0ad8fa79e5e 100644 --- a/spec/controllers/concerns/redis_tracking_spec.rb +++ b/spec/controllers/concerns/redis_tracking_spec.rb @@ -11,7 +11,8 @@ RSpec.describe RedisTracking do include RedisTracking skip_before_action :authenticate_user!, only: :show - track_redis_hll_event(:index, :show, name: 'g_compliance_approval_rules', + track_redis_hll_event(:index, :show, + name: 'g_compliance_approval_rules', if: [:custom_condition_one?, :custom_condition_two?]) { |controller| controller.get_custom_id } def index diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb index af220e2d515..e73e61b6ec5 100644 --- a/spec/controllers/import/bitbucket_controller_spec.rb +++ b/spec/controllers/import/bitbucket_controller_spec.rb @@ -49,10 +49,10 @@ RSpec.describe Import::BitbucketController do let(:expires_in) { 1.day } let(:access_token) do double(token: token, - secret: secret, - expires_at: expires_at, - expires_in: expires_in, - refresh_token: refresh_token) + secret: secret, + expires_at: expires_at, + expires_in: expires_in, + refresh_token: refresh_token) end before do diff --git a/spec/controllers/oauth/token_info_controller_spec.rb b/spec/controllers/oauth/token_info_controller_spec.rb index b66fff4d4e9..3cd952d4935 100644 --- a/spec/controllers/oauth/token_info_controller_spec.rb +++ b/spec/controllers/oauth/token_info_controller_spec.rb @@ -24,12 +24,12 @@ RSpec.describe Oauth::TokenInfoController do expect(response).to have_gitlab_http_status(:ok) expect(Gitlab::Json.parse(response.body)).to eq( - 'scope' => %w[api], - 'scopes' => %w[api], - 'created_at' => access_token.created_at.to_i, - 'expires_in' => access_token.expires_in, - 'application' => { 'uid' => application.uid }, - 'resource_owner_id' => access_token.resource_owner_id, + 'scope' => %w[api], + 'scopes' => %w[api], + 'created_at' => access_token.created_at.to_i, + 'expires_in' => access_token.expires_in, + 'application' => { 'uid' => application.uid }, + 'resource_owner_id' => access_token.resource_owner_id, 'expires_in_seconds' => access_token.expires_in ) end diff --git a/spec/controllers/omniauth_callbacks_controller_spec.rb b/spec/controllers/omniauth_callbacks_controller_spec.rb index 9ecef8b7450..df5da29495e 100644 --- a/spec/controllers/omniauth_callbacks_controller_spec.rb +++ b/spec/controllers/omniauth_callbacks_controller_spec.rb @@ -406,7 +406,7 @@ RSpec.describe OmniauthCallbacksController, type: :controller do before do stub_last_request_id(last_request_id) stub_omniauth_saml_config(enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'], - providers: [saml_config]) + providers: [saml_config]) mock_auth_hash_with_saml_xml('saml', +'my-uid', user.email, mock_saml_response) request.env['devise.mapping'] = Devise.mappings[:user] request.env['omniauth.auth'] = Rails.application.env_config['omniauth.auth'] diff --git a/spec/controllers/projects/artifacts_controller_spec.rb b/spec/controllers/projects/artifacts_controller_spec.rb index 958fcd4360c..263f488ddbf 100644 --- a/spec/controllers/projects/artifacts_controller_spec.rb +++ b/spec/controllers/projects/artifacts_controller_spec.rb @@ -488,7 +488,7 @@ RSpec.describe Projects::ArtifactsController do context 'with regular branch' do before do pipeline.update!(ref: 'master', - sha: project.commit('master').sha) + sha: project.commit('master').sha) get :latest_succeeded, params: params_from_ref('master') end @@ -499,7 +499,7 @@ RSpec.describe Projects::ArtifactsController do context 'with branch name containing slash' do before do pipeline.update!(ref: 'improve/awesome', - sha: project.commit('improve/awesome').sha) + sha: project.commit('improve/awesome').sha) get :latest_succeeded, params: params_from_ref('improve/awesome') end @@ -510,7 +510,7 @@ RSpec.describe Projects::ArtifactsController do context 'with branch name and path containing slashes' do before do pipeline.update!(ref: 'improve/awesome', - sha: project.commit('improve/awesome').sha) + sha: project.commit('improve/awesome').sha) get :latest_succeeded, params: params_from_ref('improve/awesome', job.name, 'file/README.md') end diff --git a/spec/controllers/projects/feature_flags_controller_spec.rb b/spec/controllers/projects/feature_flags_controller_spec.rb index d7850cc5f33..29ad51d590f 100644 --- a/spec/controllers/projects/feature_flags_controller_spec.rb +++ b/spec/controllers/projects/feature_flags_controller_spec.rb @@ -194,7 +194,7 @@ RSpec.describe Projects::FeatureFlagsController do other_project = create(:project) other_project.add_developer(user) other_feature_flag = create(:operations_feature_flag, project: other_project, - name: 'other_flag') + name: 'other_flag') params = { namespace_id: other_project.namespace, project_id: other_project, @@ -486,7 +486,7 @@ RSpec.describe Projects::FeatureFlagsController do context 'when creating a version 2 feature flag with a gitlabUserList strategy' do let!(:user_list) do create(:operations_feature_flag_user_list, project: project, - name: 'My List', user_xids: 'user1,user2') + name: 'My List', user_xids: 'user1,user2') end let(:params) do diff --git a/spec/controllers/projects/grafana_api_controller_spec.rb b/spec/controllers/projects/grafana_api_controller_spec.rb index baee9705127..2e25b0271ce 100644 --- a/spec/controllers/projects/grafana_api_controller_spec.rb +++ b/spec/controllers/projects/grafana_api_controller_spec.rb @@ -52,8 +52,8 @@ RSpec.describe Projects::GrafanaApiController do .with(project, '1', 'api/v1/query_range', { 'query' => params[:query], 'start' => params[:start_time], - 'end' => params[:end_time], - 'step' => params[:step] }) + 'end' => params[:end_time], + 'step' => params[:step] }) expect(response).to have_gitlab_http_status(:ok) expect(json_response).to eq({}) diff --git a/spec/controllers/projects/pipeline_schedules_controller_spec.rb b/spec/controllers/projects/pipeline_schedules_controller_spec.rb index 77acd5fe13c..fa90a0d9abc 100644 --- a/spec/controllers/projects/pipeline_schedules_controller_spec.rb +++ b/spec/controllers/projects/pipeline_schedules_controller_spec.rb @@ -360,7 +360,7 @@ RSpec.describe Projects::PipelineSchedulesController do id: pipeline_schedule, schedule: schedule }, - as: :html + as: :html end end diff --git a/spec/controllers/projects/registry/tags_controller_spec.rb b/spec/controllers/projects/registry/tags_controller_spec.rb index c03a280d2cd..7b786f4a8af 100644 --- a/spec/controllers/projects/registry/tags_controller_spec.rb +++ b/spec/controllers/projects/registry/tags_controller_spec.rb @@ -167,7 +167,7 @@ RSpec.describe Projects::Registry::TagsController do repository_id: repository, ids: names }, - format: :json + format: :json end end diff --git a/spec/controllers/projects/service_desk_controller_spec.rb b/spec/controllers/projects/service_desk_controller_spec.rb index 1c4d6665414..bc507a033dc 100644 --- a/spec/controllers/projects/service_desk_controller_spec.rb +++ b/spec/controllers/projects/service_desk_controller_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe Projects::ServiceDeskController do let_it_be(:project) do create(:project, :private, :custom_repo, service_desk_enabled: true, - files: { '.gitlab/issue_templates/service_desk.md' => 'template' }) + files: { '.gitlab/issue_templates/service_desk.md' => 'template' }) end let_it_be(:user) { create(:user) } diff --git a/spec/features/projects/settings/registry_settings_cleanup_tags_spec.rb b/spec/features/projects/settings/registry_settings_cleanup_tags_spec.rb index 5a50b3de772..0fea0c1f37f 100644 --- a/spec/features/projects/settings/registry_settings_cleanup_tags_spec.rb +++ b/spec/features/projects/settings/registry_settings_cleanup_tags_spec.rb @@ -24,6 +24,76 @@ RSpec.describe 'Project > Settings > Packages & Registries > Container registry subject expect(find('.breadcrumbs')).to have_content('Clean up image tags') + + section = find('[data-testid="container-expiration-policy-project-settings"]') + expect(section).to have_text 'Clean up image tags' + end + + it 'saves cleanup policy submit the form' do + subject + + within '[data-testid="container-expiration-policy-project-settings"]' do + select('Every day', from: 'Run cleanup') + select('50 tags per image name', from: 'Keep the most recent:') + fill_in('Keep tags matching:', with: 'stable') + select('7 days', from: 'Remove tags older than:') + fill_in('Remove tags matching:', with: '.*-production') + + submit_button = find('[data-testid="save-button"') + expect(submit_button).not_to be_disabled + submit_button.click + end + + expect(find('.gl-toast')).to have_content('Cleanup policy successfully saved.') + end + + it 'does not save cleanup policy submit form with invalid regex' do + subject + + within '[data-testid="container-expiration-policy-project-settings"]' do + fill_in('Remove tags matching:', with: '*-production') + + submit_button = find('[data-testid="save-button"') + expect(submit_button).not_to be_disabled + submit_button.click + end + + expect(find('.gl-toast')).to have_content('Something went wrong while updating the cleanup policy.') + end + end + + context 'with a project without expiration policy', :js do + before do + project.container_expiration_policy.destroy! + end + + context 'with container_expiration_policies_enable_historic_entries enabled' do + before do + stub_application_setting(container_expiration_policies_enable_historic_entries: true) + end + + it 'displays the related section' do + subject + + within '[data-testid="container-expiration-policy-project-settings"]' do + expect(find('[data-testid="enable-toggle"]')) + .to have_content('Disabled - Tags will not be automatically deleted.') + end + end + end + + context 'with container_expiration_policies_enable_historic_entries disabled' do + before do + stub_application_setting(container_expiration_policies_enable_historic_entries: false) + end + + it 'does not display the related section' do + subject + + within '[data-testid="container-expiration-policy-project-settings"]' do + expect(find('.gl-alert-title')).to have_content('Cleanup policy for tags is disabled') + end + end end end diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/cleanup_image_tags_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/cleanup_image_tags_spec.js new file mode 100644 index 00000000000..51fcb1d7300 --- /dev/null +++ b/spec/frontend/packages_and_registries/settings/project/settings/components/cleanup_image_tags_spec.js @@ -0,0 +1,164 @@ +import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui'; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import component from '~/packages_and_registries/settings/project/components/cleanup_image_tags.vue'; +import ContainerExpirationPolicyForm from '~/packages_and_registries/settings/project/components/container_expiration_policy_form.vue'; +import { + CONTAINER_CLEANUP_POLICY_TITLE, + CONTAINER_CLEANUP_POLICY_DESCRIPTION, + FETCH_SETTINGS_ERROR_MESSAGE, + UNAVAILABLE_FEATURE_INTRO_TEXT, + UNAVAILABLE_USER_FEATURE_TEXT, +} from '~/packages_and_registries/settings/project/constants'; +import expirationPolicyQuery from '~/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql'; + +import { + expirationPolicyPayload, + emptyExpirationPolicyPayload, + containerExpirationPolicyData, +} from '../mock_data'; + +describe('Cleanup image tags project settings', () => { + let wrapper; + let fakeApollo; + + const defaultProvidedValues = { + projectPath: 'path', + isAdmin: false, + adminSettingsPath: 'settingsPath', + enableHistoricEntries: false, + helpPagePath: 'helpPagePath', + showCleanupPolicyLink: false, + }; + + const findFormComponent = () => wrapper.findComponent(ContainerExpirationPolicyForm); + const findAlert = () => wrapper.findComponent(GlAlert); + const findTitle = () => wrapper.findByTestId('title'); + const findDescription = () => wrapper.findByTestId('description'); + + const mountComponent = (provide = defaultProvidedValues, config) => { + wrapper = shallowMountExtended(component, { + stubs: { + GlSprintf, + }, + provide, + ...config, + }); + }; + + const mountComponentWithApollo = ({ provide = defaultProvidedValues, resolver } = {}) => { + Vue.use(VueApollo); + + const requestHandlers = [[expirationPolicyQuery, resolver]]; + + fakeApollo = createMockApollo(requestHandlers); + mountComponent(provide, { + apolloProvider: fakeApollo, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('isEdited status', () => { + it.each` + description | apiResponse | workingCopy | result + ${'empty response and no changes from user'} | ${emptyExpirationPolicyPayload()} | ${{}} | ${false} + ${'empty response and changes from user'} | ${emptyExpirationPolicyPayload()} | ${{ enabled: true }} | ${true} + ${'response and no changes'} | ${expirationPolicyPayload()} | ${containerExpirationPolicyData()} | ${false} + ${'response and changes'} | ${expirationPolicyPayload()} | ${{ ...containerExpirationPolicyData(), nameRegex: '12345' }} | ${true} + ${'response and empty'} | ${expirationPolicyPayload()} | ${{}} | ${true} + `('$description', async ({ apiResponse, workingCopy, result }) => { + mountComponentWithApollo({ + provide: { ...defaultProvidedValues, enableHistoricEntries: true }, + resolver: jest.fn().mockResolvedValue(apiResponse), + }); + await waitForPromises(); + + findFormComponent().vm.$emit('input', workingCopy); + + await waitForPromises(); + + expect(findFormComponent().props('isEdited')).toBe(result); + }); + }); + + it('renders the setting form', async () => { + mountComponentWithApollo({ + resolver: jest.fn().mockResolvedValue(expirationPolicyPayload()), + }); + await waitForPromises(); + + expect(findFormComponent().exists()).toBe(true); + expect(findTitle().text()).toMatchInterpolatedText(CONTAINER_CLEANUP_POLICY_TITLE); + expect(findDescription().text()).toMatchInterpolatedText(CONTAINER_CLEANUP_POLICY_DESCRIPTION); + }); + + describe('the form is disabled', () => { + it('hides the form', () => { + mountComponent(); + + expect(findFormComponent().exists()).toBe(false); + }); + + it('shows an alert', () => { + mountComponent(); + + const text = findAlert().text(); + expect(text).toContain(UNAVAILABLE_FEATURE_INTRO_TEXT); + expect(text).toContain(UNAVAILABLE_USER_FEATURE_TEXT); + }); + + describe('an admin is visiting the page', () => { + it('shows the admin part of the alert message', () => { + mountComponent({ ...defaultProvidedValues, isAdmin: true }); + + const sprintf = findAlert().find(GlSprintf); + expect(sprintf.text()).toBe('administration settings'); + expect(sprintf.find(GlLink).attributes('href')).toBe( + defaultProvidedValues.adminSettingsPath, + ); + }); + }); + }); + + describe('fetchSettingsError', () => { + beforeEach(async () => { + mountComponentWithApollo({ + resolver: jest.fn().mockRejectedValue(new Error('GraphQL error')), + }); + await waitForPromises(); + }); + + it('hides the form', () => { + expect(findFormComponent().exists()).toBe(false); + }); + + it('shows an alert', () => { + expect(findAlert().html()).toContain(FETCH_SETTINGS_ERROR_MESSAGE); + }); + }); + + describe('empty API response', () => { + it.each` + enableHistoricEntries | isShown + ${true} | ${true} + ${false} | ${false} + `('is $isShown that the form is shown', async ({ enableHistoricEntries, isShown }) => { + mountComponentWithApollo({ + provide: { + ...defaultProvidedValues, + enableHistoricEntries, + }, + resolver: jest.fn().mockResolvedValue(emptyExpirationPolicyPayload()), + }); + await waitForPromises(); + + expect(findFormComponent().exists()).toBe(isShown); + }); + }); +}); diff --git a/spec/frontend/releases/stores/modules/detail/getters_spec.js b/spec/frontend/releases/stores/modules/detail/getters_spec.js index 4ac6eaebaa2..2982dc5c46c 100644 --- a/spec/frontend/releases/stores/modules/detail/getters_spec.js +++ b/spec/frontend/releases/stores/modules/detail/getters_spec.js @@ -320,7 +320,9 @@ describe('Release edit/new getters', () => { it(description, () => { const expectedVariablesObject = { input: expect.objectContaining(expectedVariables) }; - const actualVariables = getters.releaseUpdateMutatationVariables(state); + const actualVariables = getters.releaseUpdateMutatationVariables(state, { + releasedAtChanged: Object.hasOwn(state.release, 'releasedAt'), + }); expect(actualVariables).toEqual(expectedVariablesObject); }); @@ -409,4 +411,19 @@ describe('Release edit/new getters', () => { }, ); }); + + describe('releasedAtChange', () => { + it('is false if the released at date has not changed', () => { + const date = new Date(); + expect( + getters.releasedAtChanged({ originalReleasedAt: date, release: { releasedAt: date } }), + ).toBe(false); + }); + + it('is true if the date changed', () => { + const originalReleasedAt = new Date(); + const releasedAt = new Date(2022, 5, 30); + expect(getters.releasedAtChanged({ originalReleasedAt, release: { releasedAt } })).toBe(true); + }); + }); }); diff --git a/spec/frontend/releases/stores/modules/detail/mutations_spec.js b/spec/frontend/releases/stores/modules/detail/mutations_spec.js index 60b57c7a7ff..8bbf550b77d 100644 --- a/spec/frontend/releases/stores/modules/detail/mutations_spec.js +++ b/spec/frontend/releases/stores/modules/detail/mutations_spec.js @@ -36,6 +36,12 @@ describe('Release edit/new mutations', () => { }, }); }); + + it('saves the original released at date as well', () => { + mutations[types.INITIALIZE_EMPTY_RELEASE](state); + + expect(state.originalReleasedAt).toEqual(new Date()); + }); }); describe(`${types.REQUEST_RELEASE}`, () => { @@ -57,6 +63,7 @@ describe('Release edit/new mutations', () => { expect(state.release).toEqual(release); expect(state.originalRelease).toEqual(release); + expect(state.originalReleasedAt).toEqual(release.releasedAt); }); }); diff --git a/spec/frontend/work_items/pages/work_item_detail_spec.js b/spec/frontend/work_items/components/work_item_detail_spec.js index 95982db3620..51467656908 100644 --- a/spec/frontend/work_items/pages/work_item_detail_spec.js +++ b/spec/frontend/work_items/components/work_item_detail_spec.js @@ -2,6 +2,7 @@ import { GlAlert, GlBadge, GlLoadingIcon, GlSkeletonLoader, GlButton } from '@gi import { shallowMount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; +import workItemWeightSubscription from 'ee_component/work_items/graphql/work_item_weight.subscription.graphql'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; @@ -12,7 +13,6 @@ import WorkItemState from '~/work_items/components/work_item_state.vue'; import WorkItemTitle from '~/work_items/components/work_item_title.vue'; import WorkItemAssignees from '~/work_items/components/work_item_assignees.vue'; import WorkItemLabels from '~/work_items/components/work_item_labels.vue'; -import WorkItemWeight from '~/work_items/components/work_item_weight.vue'; import WorkItemInformation from '~/work_items/components/work_item_information.vue'; import { i18n } from '~/work_items/constants'; import workItemQuery from '~/work_items/graphql/work_item.query.graphql'; @@ -25,6 +25,7 @@ import { workItemTitleSubscriptionResponse, workItemResponseFactory, mockParent, + workItemWeightSubscriptionResponse, } from '../mock_data'; describe('WorkItemDetail component', () => { @@ -41,6 +42,7 @@ describe('WorkItemDetail component', () => { }); const successHandler = jest.fn().mockResolvedValue(workItemQueryResponse); const initialSubscriptionHandler = jest.fn().mockResolvedValue(workItemTitleSubscriptionResponse); + const weightSubscriptionHandler = jest.fn().mockResolvedValue(workItemWeightSubscriptionResponse); const findAlert = () => wrapper.findComponent(GlAlert); const findSkeleton = () => wrapper.findComponent(GlSkeletonLoader); @@ -51,7 +53,6 @@ describe('WorkItemDetail component', () => { const findWorkItemDescription = () => wrapper.findComponent(WorkItemDescription); const findWorkItemAssignees = () => wrapper.findComponent(WorkItemAssignees); const findWorkItemLabels = () => wrapper.findComponent(WorkItemLabels); - const findWorkItemWeight = () => wrapper.findComponent(WorkItemWeight); const findParent = () => wrapper.find('[data-testid="work-item-parent"]'); const findParentButton = () => findParent().findComponent(GlButton); const findCloseButton = () => wrapper.find('[data-testid="work-item-close"]'); @@ -70,13 +71,19 @@ describe('WorkItemDetail component', () => { includeWidgets = false, error = undefined, } = {}) => { + const handlers = [ + [workItemQuery, handler], + [workItemTitleSubscription, subscriptionHandler], + confidentialityMock, + ]; + + if (IS_EE) { + handlers.push([workItemWeightSubscription, weightSubscriptionHandler]); + } + wrapper = shallowMount(WorkItemDetail, { apolloProvider: createMockApollo( - [ - [workItemQuery, handler], - [workItemTitleSubscription, subscriptionHandler], - confidentialityMock, - ], + handlers, {}, { typePolicies: includeWidgets ? temporaryConfig.cacheConfig.typePolicies : {}, @@ -93,6 +100,7 @@ describe('WorkItemDetail component', () => { glFeatures: { workItemsMvc2: workItemsMvc2Enabled, }, + hasIssueWeightsFeature: true, }, }); }; @@ -438,34 +446,6 @@ describe('WorkItemDetail component', () => { }); }); - describe('weight widget', () => { - describe.each` - description | weightWidgetPresent | exists - ${'when widget is returned from API'} | ${true} | ${true} - ${'when widget is not returned from API'} | ${false} | ${false} - `('$description', ({ weightWidgetPresent, exists }) => { - it(`${weightWidgetPresent ? 'renders' : 'does not render'} weight component`, async () => { - const response = workItemResponseFactory({ weightWidgetPresent }); - const handler = jest.fn().mockResolvedValue(response); - createComponent({ handler }); - await waitForPromises(); - - expect(findWorkItemWeight().exists()).toBe(exists); - }); - }); - - it('shows an error message when it emits an `error` event', async () => { - createComponent({ workItemsMvc2Enabled: true }); - await waitForPromises(); - const updateError = 'Failed to update'; - - findWorkItemWeight().vm.$emit('error', updateError); - await waitForPromises(); - - expect(findAlert().text()).toBe(updateError); - }); - }); - describe('work item information', () => { beforeEach(() => { createComponent(); diff --git a/spec/frontend/work_items/components/work_item_weight_spec.js b/spec/frontend/work_items/components/work_item_weight_spec.js deleted file mode 100644 index 38dc1af0bac..00000000000 --- a/spec/frontend/work_items/components/work_item_weight_spec.js +++ /dev/null @@ -1,218 +0,0 @@ -import { GlForm, GlFormInput } from '@gitlab/ui'; -import Vue, { nextTick } from 'vue'; -import VueApollo from 'vue-apollo'; -import createMockApollo from 'helpers/mock_apollo_helper'; -import { mockTracking } from 'helpers/tracking_helper'; -import { mountExtended } from 'helpers/vue_test_utils_helper'; -import waitForPromises from 'helpers/wait_for_promises'; -import { __ } from '~/locale'; -import WorkItemWeight from '~/work_items/components/work_item_weight.vue'; -import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants'; -import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; -import { updateWorkItemMutationResponse } from 'jest/work_items/mock_data'; - -describe('WorkItemWeight component', () => { - Vue.use(VueApollo); - - let wrapper; - - const workItemId = 'gid://gitlab/WorkItem/1'; - const workItemType = 'Task'; - - const findForm = () => wrapper.findComponent(GlForm); - const findInput = () => wrapper.findComponent(GlFormInput); - - const createComponent = ({ - canUpdate = false, - hasIssueWeightsFeature = true, - isEditing = false, - weight, - mutationHandler = jest.fn().mockResolvedValue(updateWorkItemMutationResponse), - } = {}) => { - wrapper = mountExtended(WorkItemWeight, { - apolloProvider: createMockApollo([[updateWorkItemMutation, mutationHandler]]), - propsData: { - canUpdate, - weight, - workItemId, - workItemType, - }, - provide: { - hasIssueWeightsFeature, - }, - }); - - if (isEditing) { - findInput().vm.$emit('focus'); - } - }; - - describe('`issue_weights` licensed feature', () => { - describe.each` - description | hasIssueWeightsFeature | exists - ${'when available'} | ${true} | ${true} - ${'when not available'} | ${false} | ${false} - `('$description', ({ hasIssueWeightsFeature, exists }) => { - it(hasIssueWeightsFeature ? 'renders component' : 'does not render component', () => { - createComponent({ hasIssueWeightsFeature }); - - expect(findForm().exists()).toBe(exists); - }); - }); - }); - - describe('weight input', () => { - it('has "Weight" label', () => { - createComponent(); - - expect(wrapper.findByLabelText(__('Weight')).exists()).toBe(true); - }); - - describe('placeholder attribute', () => { - describe.each` - description | isEditing | canUpdate | value - ${'when not editing and cannot update'} | ${false} | ${false} | ${__('None')} - ${'when editing and cannot update'} | ${true} | ${false} | ${__('None')} - ${'when not editing and can update'} | ${false} | ${true} | ${__('None')} - ${'when editing and can update'} | ${true} | ${true} | ${__('Enter a number')} - `('$description', ({ isEditing, canUpdate, value }) => { - it(`has a value of "${value}"`, async () => { - createComponent({ canUpdate, isEditing }); - await nextTick(); - - expect(findInput().attributes('placeholder')).toBe(value); - }); - }); - }); - - describe('readonly attribute', () => { - describe.each` - description | canUpdate | value - ${'when cannot update'} | ${false} | ${'readonly'} - ${'when can update'} | ${true} | ${undefined} - `('$description', ({ canUpdate, value }) => { - it(`renders readonly=${value}`, () => { - createComponent({ canUpdate }); - - expect(findInput().attributes('readonly')).toBe(value); - }); - }); - }); - - describe('type attribute', () => { - describe.each` - description | isEditing | canUpdate | type - ${'when not editing and cannot update'} | ${false} | ${false} | ${'text'} - ${'when editing and cannot update'} | ${true} | ${false} | ${'text'} - ${'when not editing and can update'} | ${false} | ${true} | ${'text'} - ${'when editing and can update'} | ${true} | ${true} | ${'number'} - `('$description', ({ isEditing, canUpdate, type }) => { - it(`has a value of "${type}"`, async () => { - createComponent({ canUpdate, isEditing }); - await nextTick(); - - expect(findInput().attributes('type')).toBe(type); - }); - }); - }); - - describe('value attribute', () => { - describe.each` - weight | value - ${1} | ${'1'} - ${0} | ${'0'} - ${null} | ${''} - ${undefined} | ${''} - `('when `weight` prop is "$weight"', ({ weight, value }) => { - it(`value is "${value}"`, () => { - createComponent({ weight }); - - expect(findInput().element.value).toBe(value); - }); - }); - }); - - describe('when blurred', () => { - it('calls a mutation to update the weight when the input value is different', () => { - const mutationSpy = jest.fn().mockResolvedValue(updateWorkItemMutationResponse); - createComponent({ - isEditing: true, - weight: 0, - mutationHandler: mutationSpy, - canUpdate: true, - }); - - findInput().vm.$emit('blur', { target: { value: 1 } }); - - expect(mutationSpy).toHaveBeenCalledWith({ - input: { - id: workItemId, - weightWidget: { - weight: 1, - }, - }, - }); - }); - - it('does not call a mutation to update the weight when the input value is the same', () => { - const mutationSpy = jest.fn().mockResolvedValue(updateWorkItemMutationResponse); - createComponent({ isEditing: true, mutationHandler: mutationSpy, canUpdate: true }); - - findInput().trigger('blur'); - - expect(mutationSpy).not.toHaveBeenCalledWith(); - }); - - it('emits an error when there is a GraphQL error', async () => { - const response = { - data: { - workItemUpdate: { - errors: ['Error!'], - workItem: {}, - }, - }, - }; - createComponent({ - isEditing: true, - mutationHandler: jest.fn().mockResolvedValue(response), - canUpdate: true, - }); - - findInput().trigger('blur'); - await waitForPromises(); - - expect(wrapper.emitted('error')).toEqual([ - ['Something went wrong while updating the task. Please try again.'], - ]); - }); - - it('emits an error when there is a network error', async () => { - createComponent({ - isEditing: true, - mutationHandler: jest.fn().mockRejectedValue(new Error()), - canUpdate: true, - }); - - findInput().trigger('blur'); - await waitForPromises(); - - expect(wrapper.emitted('error')).toEqual([ - ['Something went wrong while updating the task. Please try again.'], - ]); - }); - - it('tracks updating the weight', () => { - const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); - createComponent({ canUpdate: true }); - - findInput().trigger('blur'); - - expect(trackingSpy).toHaveBeenCalledWith(TRACKING_CATEGORY_SHOW, 'updated_weight', { - category: TRACKING_CATEGORY_SHOW, - label: 'item_weight', - property: 'type_Task', - }); - }); - }); - }); -}); diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js index 8deca5c50b7..6a5aa48b610 100644 --- a/spec/frontend/work_items/mock_data.js +++ b/spec/frontend/work_items/mock_data.js @@ -407,6 +407,20 @@ export const workItemTitleSubscriptionResponse = { }, }; +export const workItemWeightSubscriptionResponse = { + data: { + issuableWeightUpdated: { + id: 'gid://gitlab/WorkItem/1', + widgets: [ + { + __typename: 'WorkItemWidgetWeight', + weight: 1, + }, + ], + }, + }, +}; + export const workItemHierarchyEmptyResponse = { data: { workItem: { diff --git a/spec/frontend/work_items/router_spec.js b/spec/frontend/work_items/router_spec.js index 99dcd886f7b..a86663cbdf9 100644 --- a/spec/frontend/work_items/router_spec.js +++ b/spec/frontend/work_items/router_spec.js @@ -1,5 +1,16 @@ import { mount } from '@vue/test-utils'; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import workItemWeightSubscription from 'ee_component/work_items/graphql/work_item_weight.subscription.graphql'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import { + workItemResponseFactory, + workItemTitleSubscriptionResponse, + workItemWeightSubscriptionResponse, +} from 'jest/work_items/mock_data'; import App from '~/work_items/components/app.vue'; +import workItemQuery from '~/work_items/graphql/work_item.query.graphql'; +import workItemTitleSubscription from '~/work_items/graphql/work_item_title.subscription.graphql'; import CreateWorkItem from '~/work_items/pages/create_work_item.vue'; import WorkItemsRoot from '~/work_items/pages/work_item_root.vue'; import { createRouter } from '~/work_items/router'; @@ -7,26 +18,34 @@ import { createRouter } from '~/work_items/router'; describe('Work items router', () => { let wrapper; + Vue.use(VueApollo); + + const workItemQueryHandler = jest.fn().mockResolvedValue(workItemResponseFactory()); + const titleSubscriptionHandler = jest.fn().mockResolvedValue(workItemTitleSubscriptionResponse); + const weightSubscriptionHandler = jest.fn().mockResolvedValue(workItemWeightSubscriptionResponse); + const createComponent = async (routeArg) => { const router = createRouter('/work_item'); if (routeArg !== undefined) { await router.push(routeArg); } + const handlers = [ + [workItemQuery, workItemQueryHandler], + [workItemTitleSubscription, titleSubscriptionHandler], + ]; + + if (IS_EE) { + handlers.push([workItemWeightSubscription, weightSubscriptionHandler]); + } + wrapper = mount(App, { + apolloProvider: createMockApollo(handlers), router, provide: { fullPath: 'full-path', issuesListPath: 'full-path/-/issues', }, - mocks: { - $apollo: { - queries: { - workItem: {}, - workItemTypes: {}, - }, - }, - }, }); }; diff --git a/spec/support/shared_examples/models/synthetic_note_shared_examples.rb b/spec/support/shared_examples/models/synthetic_note_shared_examples.rb index a41ade2950a..12e865b1312 100644 --- a/spec/support/shared_examples/models/synthetic_note_shared_examples.rb +++ b/spec/support/shared_examples/models/synthetic_note_shared_examples.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true RSpec.shared_examples 'a synthetic note' do |action| - it_behaves_like 'a system note', exclude_project: true do + it_behaves_like 'a system note', exclude_project: true, skip_persistence_check: true do let(:action) { action } end diff --git a/spec/support/shared_examples/services/common_system_notes_shared_examples.rb b/spec/support/shared_examples/services/common_system_notes_shared_examples.rb index ce412ef55de..1887b38b50e 100644 --- a/spec/support/shared_examples/services/common_system_notes_shared_examples.rb +++ b/spec/support/shared_examples/services/common_system_notes_shared_examples.rb @@ -42,6 +42,7 @@ RSpec.shared_examples 'a system note' do |params| it 'has the correct attributes', :aggregate_failures do exclude_project = !params.nil? && params[:exclude_project] + skip_persistence_check = !params.nil? && params[:skip_persistence_check] expect(subject).to be_valid expect(subject).to be_system @@ -50,6 +51,7 @@ RSpec.shared_examples 'a system note' do |params| expect(subject.project).to eq project unless exclude_project expect(subject.author).to eq author + expect(subject.system_note_metadata).to be_persisted unless skip_persistence_check expect(subject.system_note_metadata.action).to eq(action) expect(subject.system_note_metadata.commit_count).to eq(commit_count) end |