Commit 8d7e0081 authored by Alessio Caiazza's avatar Alessio Caiazza

Merge remote-tracking branch 'origin/master' into fix-master-merge-train

Conflicts:
	ee/app/assets/javascripts/vulnerabilities/components/history_comment.vue
	ee/app/assets/javascripts/vulnerabilities/components/history_comment_editor.vue
parents f49eebc5 574f3bce
e4ff30e44b6ac21f33290bbe7a9cbbd42f98d4d1 e9860f7988a2c87638abf695d8613e3096312857
<script>
import { GlEmptyState, GlSprintf, GlLink, GlButton } from '@gitlab/ui';
export default {
components: {
GlEmptyState,
GlSprintf,
GlLink,
GlButton,
},
inject: {
isAdmin: {
type: Boolean,
},
svgPath: {
type: String,
},
docsLink: {
type: String,
},
primaryButtonPath: {
type: String,
},
},
};
</script>
<template>
<gl-empty-state class="js-empty-state" :title="__('Usage ping is off')" :svg-path="svgPath">
<template #description>
<gl-sprintf
v-if="!isAdmin"
:message="
__(
'To view instance-level analytics, ask an admin to turn on %{docLinkStart}usage ping%{docLinkEnd}.',
)
"
>
<template #docLink="{content}">
<gl-link :href="docsLink" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
<template v-else
><p>
{{ __('Turn on usage ping to review instance-level analytics.') }}
</p>
<gl-button category="primary" variant="success" :href="primaryButtonPath">
{{ __('Turn on usage ping') }}</gl-button
>
</template>
</template>
</gl-empty-state>
</template>
...@@ -468,7 +468,7 @@ export default { ...@@ -468,7 +468,7 @@ export default {
<div <div
:data-can-create-note="getNoteableData.current_user.can_create_note" :data-can-create-note="getNoteableData.current_user.can_create_note"
class="files d-flex" class="files d-flex gl-mt-2"
> >
<div <div
v-if="showTreeList" v-if="showTreeList"
......
...@@ -34,7 +34,6 @@ export const COUNT_OF_AVATARS_IN_GUTTER = 3; ...@@ -34,7 +34,6 @@ export const COUNT_OF_AVATARS_IN_GUTTER = 3;
export const LENGTH_OF_AVATAR_TOOLTIP = 17; export const LENGTH_OF_AVATAR_TOOLTIP = 17;
export const LINES_TO_BE_RENDERED_DIRECTLY = 100; export const LINES_TO_BE_RENDERED_DIRECTLY = 100;
export const MAX_LINES_TO_BE_RENDERED = 2000;
export const DIFF_FILE_SYMLINK_MODE = '120000'; export const DIFF_FILE_SYMLINK_MODE = '120000';
export const DIFF_FILE_DELETED_MODE = '0'; export const DIFF_FILE_DELETED_MODE = '0';
......
...@@ -11,7 +11,6 @@ import { ...@@ -11,7 +11,6 @@ import {
OLD_LINE_TYPE, OLD_LINE_TYPE,
MATCH_LINE_TYPE, MATCH_LINE_TYPE,
LINES_TO_BE_RENDERED_DIRECTLY, LINES_TO_BE_RENDERED_DIRECTLY,
MAX_LINES_TO_BE_RENDERED,
TREE_TYPE, TREE_TYPE,
INLINE_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE,
PARALLEL_DIFF_VIEW_TYPE, PARALLEL_DIFF_VIEW_TYPE,
...@@ -457,12 +456,10 @@ function getVisibleDiffLines(file) { ...@@ -457,12 +456,10 @@ function getVisibleDiffLines(file) {
} }
function finalizeDiffFile(file) { function finalizeDiffFile(file) {
const name = (file.viewer && file.viewer.name) || diffViewerModes.text;
const lines = getVisibleDiffLines(file); const lines = getVisibleDiffLines(file);
Object.assign(file, { Object.assign(file, {
renderIt: lines < LINES_TO_BE_RENDERED_DIRECTLY, renderIt: lines < LINES_TO_BE_RENDERED_DIRECTLY,
collapsed: name === diffViewerModes.text && lines > MAX_LINES_TO_BE_RENDERED,
isShowingFullFile: false, isShowingFullFile: false,
isLoadingFullFile: false, isLoadingFullFile: false,
discussions: [], discussions: [],
......
import Vue from 'vue';
import UserCallout from '~/user_callout'; import UserCallout from '~/user_callout';
import UsagePingDisabled from '~/admin/dev_ops_score/components/usage_ping_disabled.vue';
document.addEventListener('DOMContentLoaded', () => new UserCallout()); document.addEventListener('DOMContentLoaded', () => {
// eslint-disable-next-line no-new
new UserCallout();
const emptyStateContainer = document.getElementById('js-devops-empty-state');
if (!emptyStateContainer) return false;
const { emptyStateSvgPath, enableUsagePingLink, docsLink, isAdmin } = emptyStateContainer.dataset;
return new Vue({
el: emptyStateContainer,
provide: {
isAdmin: Boolean(isAdmin),
svgPath: emptyStateSvgPath,
primaryButtonPath: enableUsagePingLink,
docsLink,
},
render(h) {
return h(UsagePingDisabled);
},
});
});
...@@ -1062,7 +1062,7 @@ table.code { ...@@ -1062,7 +1062,7 @@ table.code {
.diff-tree-list { .diff-tree-list {
position: -webkit-sticky; position: -webkit-sticky;
position: sticky; position: sticky;
$top-pos: $header-height + $mr-tabs-height + $mr-version-controls-height + 11px; $top-pos: $header-height + $mr-tabs-height + $mr-version-controls-height + 15px;
top: $top-pos; top: $top-pos;
max-height: calc(100vh - #{$top-pos}); max-height: calc(100vh - #{$top-pos});
z-index: 202; z-index: 202;
......
...@@ -445,7 +445,7 @@ class Issue < ApplicationRecord ...@@ -445,7 +445,7 @@ class Issue < ApplicationRecord
super super
rescue ActiveRecord::QueryCanceled => e rescue ActiveRecord::QueryCanceled => e
# Symptom of running out of space - schedule rebalancing # Symptom of running out of space - schedule rebalancing
IssueRebalancingWorker.perform_async(id) IssueRebalancingWorker.perform_async(nil, project_id)
raise e raise e
end end
...@@ -453,7 +453,7 @@ class Issue < ApplicationRecord ...@@ -453,7 +453,7 @@ class Issue < ApplicationRecord
super super
rescue ActiveRecord::QueryCanceled => e rescue ActiveRecord::QueryCanceled => e
# Symptom of running out of space - schedule rebalancing # Symptom of running out of space - schedule rebalancing
IssueRebalancingWorker.perform_async(id) IssueRebalancingWorker.perform_async(nil, project_id)
raise e raise e
end end
end end
......
...@@ -29,7 +29,7 @@ module Issues ...@@ -29,7 +29,7 @@ module Issues
gates = [issue.project, issue.project.group].compact gates = [issue.project, issue.project.group].compact
return unless gates.any? { |gate| Feature.enabled?(:rebalance_issues, gate) } return unless gates.any? { |gate| Feature.enabled?(:rebalance_issues, gate) }
IssueRebalancingWorker.perform_async(issue.id) IssueRebalancingWorker.perform_async(nil, issue.project_id)
end end
def create_assignee_note(issue, old_assignees) def create_assignee_note(issue, old_assignees)
......
.container.devops-empty
.col-sm-12.justify-content-center.text-center
= custom_icon('dev_ops_score_no_index')
%h4= _('Usage ping is not enabled')
- if !current_user.admin?
%p
- usage_ping_path = help_page_path('development/telemetry/usage_ping')
- usage_ping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: usage_ping_path }
= s_('In order to enable instance-level analytics, please ask an admin to enable %{usage_ping_link_start}usage ping%{usage_ping_link_end}.').html_safe % { usage_ping_link_start: usage_ping_link_start, usage_ping_link_end: '</a>'.html_safe }
- if current_user.admin?
%p
= _('Enable usage ping to get an overview of how you are using GitLab from a feature perspective.')
- if current_user.admin?
= link_to _('Enable usage ping'), metrics_and_profiling_admin_application_settings_path(anchor: 'js-usage-settings'), class: 'btn btn-primary'
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
.gl-mt-3 .gl-mt-3
- if !usage_ping_enabled - if !usage_ping_enabled
= render 'disabled' #js-devops-empty-state{ data: { is_admin: current_user&.admin.to_s, empty_state_svg_path: image_path('illustrations/convdev/convdev_no_index.svg'), enable_usage_ping_link: metrics_and_profiling_admin_application_settings_path(anchor: 'js-usage-settings'), docs_link: help_page_path('development/telemetry/usage_ping') } }
- elsif @metric.blank? - elsif @metric.blank?
= render 'no_data' = render 'no_data'
- else - else
......
...@@ -7,11 +7,14 @@ class IssueRebalancingWorker ...@@ -7,11 +7,14 @@ class IssueRebalancingWorker
urgency :low urgency :low
feature_category :issue_tracking feature_category :issue_tracking
def perform(issue_id) def perform(ignore = nil, project_id = nil)
issue = Issue.find(issue_id) return if project_id.nil?
project = Project.find(project_id)
issue = project.issues.first # All issues are equivalent as far as we are concerned
IssueRebalancingService.new(issue).execute IssueRebalancingService.new(issue).execute
rescue ActiveRecord::RecordNotFound, IssueRebalancingService::TooManyIssues => e rescue ActiveRecord::RecordNotFound, IssueRebalancingService::TooManyIssues => e
Gitlab::ErrorTracking.log_exception(e, issue_id: issue_id) Gitlab::ErrorTracking.log_exception(e, project_id: project_id)
end end
end end
---
title: Add index on merge_request_id to approval_merge_request_rules
merge_request: 40556
author:
type: other
---
title: Fix file file input top position cutoff
merge_request: 40634
author:
type: fixed
---
title: Migrate DevOps Score empty state into Vue component
merge_request: 40595
author:
type: changed
---
title: Fix auto-deploy-image external chart dependencies
merge_request: 40730
author:
type: fixed
---
title: Fix client usage of max line rendering
merge_request: 40741
author:
type: fixed
# frozen_string_literal: true
class AddIndexOnMergeRequestIdAndRuleTypeToApprovalMergeRequestRule < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
INDEX_NAME = "approval_mr_rule_index_merge_request_id"
def up
add_concurrent_index(
:approval_merge_request_rules,
:merge_request_id,
name: INDEX_NAME
)
end
def down
remove_concurrent_index_by_name :approval_merge_request_rules, INDEX_NAME
end
end
360c42f4d34c3b03e7a0375a0ff2776f066888f0a40131180bf301b876ea58db
\ No newline at end of file
...@@ -18945,6 +18945,8 @@ CREATE UNIQUE INDEX any_approver_merge_request_rule_type_unique_index ON public. ...@@ -18945,6 +18945,8 @@ CREATE UNIQUE INDEX any_approver_merge_request_rule_type_unique_index ON public.
CREATE UNIQUE INDEX any_approver_project_rule_type_unique_index ON public.approval_project_rules USING btree (project_id) WHERE (rule_type = 3); CREATE UNIQUE INDEX any_approver_project_rule_type_unique_index ON public.approval_project_rules USING btree (project_id) WHERE (rule_type = 3);
CREATE INDEX approval_mr_rule_index_merge_request_id ON public.approval_merge_request_rules USING btree (merge_request_id);
CREATE UNIQUE INDEX approval_rule_name_index_for_code_owners ON public.approval_merge_request_rules USING btree (merge_request_id, code_owner, name) WHERE ((code_owner = true) AND (section IS NULL)); CREATE UNIQUE INDEX approval_rule_name_index_for_code_owners ON public.approval_merge_request_rules USING btree (merge_request_id, code_owner, name) WHERE ((code_owner = true) AND (section IS NULL));
CREATE UNIQUE INDEX backup_labels_group_id_project_id_title_idx ON public.backup_labels USING btree (group_id, project_id, title); CREATE UNIQUE INDEX backup_labels_group_id_project_id_title_idx ON public.backup_labels USING btree (group_id, project_id, title);
......
...@@ -106,7 +106,7 @@ end ...@@ -106,7 +106,7 @@ end
Using `any_instance` to stub a method (elasticsearch_indexing) that has been defined on a prepended module (EE::ApplicationSetting) is not supported. Using `any_instance` to stub a method (elasticsearch_indexing) that has been defined on a prepended module (EE::ApplicationSetting) is not supported.
``` ```
### Alternative: `expect_next_instance_of` or `allow_next_instance_of` ### Alternative: `expect_next_instance_of`, `allow_next_instance_of`, `expect_next_found_instance_of` or `allow_next_found_instance_of`
Instead of writing: Instead of writing:
...@@ -130,8 +130,21 @@ end ...@@ -130,8 +130,21 @@ end
allow_next_instance_of(Project) do |project| allow_next_instance_of(Project) do |project|
allow(project).to receive(:add_import_job) allow(project).to receive(:add_import_job)
end end
# Do this:
expect_next_found_instance_of(Project) do |project|
expect(project).to receive(:add_import_job)
end
# Do this:
allow_next_found_instance_of(Project) do |project|
allow(project).to receive(:add_import_job)
end
``` ```
_**Note:** Since Active Record is not calling the `.new` method on model classes to instantiate the objects,
you should use `expect_next_found_instance_of` or `allow_next_found_instance_of` mock helpers to setup mock on objects returned by Active Record query & finder methods._
If we also want to initialize the instance with some particular arguments, we If we also want to initialize the instance with some particular arguments, we
could also pass it like: could also pass it like:
......
...@@ -203,15 +203,18 @@ export default { ...@@ -203,15 +203,18 @@ export default {
/> />
</li> </li>
<li <li
v-if="item.labels.nodes.length" class="gl-mr-3 gl-display-flex gl-align-items-center"
class="gl-mr-3" :class="{ 'gl-opacity-5': !item.labels.nodes.length }"
:data-testid="$options.testIds.LABEL_DETAILS" :data-testid="$options.testIds.LABEL_DETAILS"
> >
<span class="gl-display-flex gl-align-items-center" <gl-icon name="label" class="gl-mr-1" /><span>{{ item.labels.nodes.length }}</span>
><gl-icon name="label" class="gl-mr-1" /><span>{{ </li>
item.labels.nodes.length <li
}}</span></span class="gl-mr-3 gl-display-flex gl-align-items-center"
> :class="{ 'gl-opacity-5': !item.userNotesCount }"
:data-testid="$options.testIds.COMMENT_COUNT"
>
<gl-icon name="comments" class="gl-mr-2" /><span>{{ item.userNotesCount }}</span>
</li> </li>
</ul> </ul>
</div> </div>
......
...@@ -41,6 +41,7 @@ export const THROUGHPUT_TABLE_TEST_IDS = { ...@@ -41,6 +41,7 @@ export const THROUGHPUT_TABLE_TEST_IDS = {
LINE_CHANGES: 'lineChangesCol', LINE_CHANGES: 'lineChangesCol',
ASSIGNEES: 'assigneesCol', ASSIGNEES: 'assigneesCol',
COMMITS: 'commitsCol', COMMITS: 'commitsCol',
COMMENT_COUNT: 'commentCount',
}; };
export const PIPELINE_STATUS_ICON_CLASSES = { export const PIPELINE_STATUS_ICON_CLASSES = {
......
...@@ -34,6 +34,7 @@ query($fullPath: ID!, $startDate: Time!, $endDate: Time!, $limit: Int!) { ...@@ -34,6 +34,7 @@ query($fullPath: ID!, $startDate: Time!, $endDate: Time!, $limit: Int!) {
} }
} }
commitCount commitCount
userNotesCount
} }
} }
} }
......
...@@ -13,6 +13,8 @@ function createMainApp() { ...@@ -13,6 +13,8 @@ function createMainApp() {
createIssueUrl: vulnerability.create_issue_url, createIssueUrl: vulnerability.create_issue_url,
projectFingerprint: vulnerability.project_fingerprint, projectFingerprint: vulnerability.project_fingerprint,
vulnerabilityId: vulnerability.id, vulnerabilityId: vulnerability.id,
issueTrackingHelpPath: vulnerability.issueTrackingHelpPath,
permissionsHelpPath: vulnerability.permissionsHelpPath,
}, },
render: h => render: h =>
......
...@@ -14,10 +14,3 @@ export const ONBOARDING_ISSUES_EXPERIMENT_FLOW_STEPS = [ ...@@ -14,10 +14,3 @@ export const ONBOARDING_ISSUES_EXPERIMENT_FLOW_STEPS = [
STEPS.yourGroup, STEPS.yourGroup,
STEPS.yourProject, STEPS.yourProject,
]; ];
export const ONBOARDING_ISSUES_EXPERIMENT_AND_SUBSCRIPTION_FLOW_STEPS = [
STEPS.yourProfile,
STEPS.checkout,
STEPS.yourGroup,
STEPS.yourProject,
];
import Vue from 'vue'; import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils'; import { STEPS, ONBOARDING_ISSUES_EXPERIMENT_FLOW_STEPS } from '../../constants';
import {
STEPS,
ONBOARDING_ISSUES_EXPERIMENT_FLOW_STEPS,
ONBOARDING_ISSUES_EXPERIMENT_AND_SUBSCRIPTION_FLOW_STEPS,
} from '../../constants';
import ProgressBar from '../../components/progress_bar.vue'; import ProgressBar from '../../components/progress_bar.vue';
export default () => { export default () => {
...@@ -12,17 +7,11 @@ export default () => { ...@@ -12,17 +7,11 @@ export default () => {
if (!el) return null; if (!el) return null;
const isInSubscriptionFlow = parseBoolean(el.dataset.isInSubscriptionFlow);
const steps = isInSubscriptionFlow
? ONBOARDING_ISSUES_EXPERIMENT_AND_SUBSCRIPTION_FLOW_STEPS
: ONBOARDING_ISSUES_EXPERIMENT_FLOW_STEPS;
return new Vue({ return new Vue({
el, el,
render(createElement) { render(createElement) {
return createElement(ProgressBar, { return createElement(ProgressBar, {
props: { steps, currentStep: STEPS.yourProject }, props: { steps: ONBOARDING_ISSUES_EXPERIMENT_FLOW_STEPS, currentStep: STEPS.yourProject },
}); });
}, },
}); });
......
...@@ -4,7 +4,6 @@ import { ...@@ -4,7 +4,6 @@ import {
STEPS, STEPS,
SUBSCRIPTON_FLOW_STEPS, SUBSCRIPTON_FLOW_STEPS,
ONBOARDING_ISSUES_EXPERIMENT_FLOW_STEPS, ONBOARDING_ISSUES_EXPERIMENT_FLOW_STEPS,
ONBOARDING_ISSUES_EXPERIMENT_AND_SUBSCRIPTION_FLOW_STEPS,
} from '../constants'; } from '../constants';
import ProgressBar from '../components/progress_bar.vue'; import ProgressBar from '../components/progress_bar.vue';
...@@ -20,9 +19,7 @@ export default () => { ...@@ -20,9 +19,7 @@ export default () => {
let steps; let steps;
if (isInSubscriptionFlow && isOnboardingIssuesExperimentEnabled) { if (isInSubscriptionFlow) {
steps = ONBOARDING_ISSUES_EXPERIMENT_AND_SUBSCRIPTION_FLOW_STEPS;
} else if (isInSubscriptionFlow) {
steps = SUBSCRIPTON_FLOW_STEPS; steps = SUBSCRIPTON_FLOW_STEPS;
} else if (isOnboardingIssuesExperimentEnabled) { } else if (isOnboardingIssuesExperimentEnabled) {
steps = ONBOARDING_ISSUES_EXPERIMENT_FLOW_STEPS; steps = ONBOARDING_ISSUES_EXPERIMENT_FLOW_STEPS;
......
import Vue from 'vue'; import Vue from 'vue';
import { import { STEPS, SUBSCRIPTON_FLOW_STEPS } from 'ee/registrations/constants';
STEPS,
SUBSCRIPTON_FLOW_STEPS,
ONBOARDING_ISSUES_EXPERIMENT_AND_SUBSCRIPTION_FLOW_STEPS,
} from 'ee/registrations/constants';
import ProgressBar from 'ee/registrations/components/progress_bar.vue'; import ProgressBar from 'ee/registrations/components/progress_bar.vue';
import { parseBoolean } from '~/lib/utils/common_utils';
export default () => { export default () => {
const el = document.getElementById('progress-bar'); const el = document.getElementById('progress-bar');
if (!el) return null; if (!el) return null;
const isOnboardingIssuesExperimentEnabled = parseBoolean(
el.dataset.isOnboardingIssuesExperimentEnabled,
);
const steps = isOnboardingIssuesExperimentEnabled
? ONBOARDING_ISSUES_EXPERIMENT_AND_SUBSCRIPTION_FLOW_STEPS
: SUBSCRIPTON_FLOW_STEPS;
return new Vue({ return new Vue({
el, el,
render(createElement) { render(createElement) {
return createElement(ProgressBar, { return createElement(ProgressBar, {
props: { steps, currentStep: STEPS.yourGroup }, props: { steps: SUBSCRIPTON_FLOW_STEPS, currentStep: STEPS.yourGroup },
}); });
}, },
}); });
......
<script> <script>
import { import { STEPS, SUBSCRIPTON_FLOW_STEPS } from 'ee/registrations/constants';
STEPS,
SUBSCRIPTON_FLOW_STEPS,
ONBOARDING_ISSUES_EXPERIMENT_AND_SUBSCRIPTION_FLOW_STEPS,
} from 'ee/registrations/constants';
import { mapState } from 'vuex'; import { mapState } from 'vuex';
import ProgressBar from 'ee/registrations/components/progress_bar.vue'; import ProgressBar from 'ee/registrations/components/progress_bar.vue';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
...@@ -15,13 +11,9 @@ import ConfirmOrder from './checkout/confirm_order.vue'; ...@@ -15,13 +11,9 @@ import ConfirmOrder from './checkout/confirm_order.vue';
export default { export default {
components: { ProgressBar, SubscriptionDetails, BillingAddress, PaymentMethod, ConfirmOrder }, components: { ProgressBar, SubscriptionDetails, BillingAddress, PaymentMethod, ConfirmOrder },
currentStep: STEPS.checkout, currentStep: STEPS.checkout,
steps: SUBSCRIPTON_FLOW_STEPS,
computed: { computed: {
...mapState(['isOnboardingIssuesExperimentEnabled', 'isNewUser']), ...mapState(['isNewUser']),
steps() {
return this.isOnboardingIssuesExperimentEnabled
? ONBOARDING_ISSUES_EXPERIMENT_AND_SUBSCRIPTION_FLOW_STEPS
: SUBSCRIPTON_FLOW_STEPS;
},
}, },
i18n: { i18n: {
checkout: s__('Checkout|Checkout'), checkout: s__('Checkout|Checkout'),
...@@ -31,7 +23,7 @@ export default { ...@@ -31,7 +23,7 @@ export default {
<template> <template>
<div class="checkout d-flex flex-column justify-content-between w-100"> <div class="checkout d-flex flex-column justify-content-between w-100">
<div class="full-width"> <div class="full-width">
<progress-bar v-if="isNewUser" :steps="steps" :current-step="$options.currentStep" /> <progress-bar v-if="isNewUser" :steps="$options.steps" :current-step="$options.currentStep" />
<div class="flash-container"></div> <div class="flash-container"></div>
<h2 class="mt-4 mb-3 mb-lg-5">{{ $options.i18n.checkout }}</h2> <h2 class="mt-4 mb-3 mb-lg-5">{{ $options.i18n.checkout }}</h2>
<subscription-details /> <subscription-details />
......
...@@ -44,7 +44,6 @@ export default ({ ...@@ -44,7 +44,6 @@ export default ({
setupForCompany, setupForCompany,
fullName, fullName,
newUser, newUser,
onboardingIssuesExperimentEnabled,
groupData = '[]', groupData = '[]',
}) => { }) => {
const availablePlans = parsePlanData(planData); const availablePlans = parsePlanData(planData);
...@@ -58,7 +57,6 @@ export default ({ ...@@ -58,7 +57,6 @@ export default ({
availablePlans, availablePlans,
selectedPlan: determineSelectedPlan(planId, availablePlans), selectedPlan: determineSelectedPlan(planId, availablePlans),
isNewUser, isNewUser,
isOnboardingIssuesExperimentEnabled: parseBoolean(onboardingIssuesExperimentEnabled),
fullName, fullName,
groupData: groups, groupData: groups,
selectedGroup: groupId, selectedGroup: groupId,
......
...@@ -11,9 +11,19 @@ export default { ...@@ -11,9 +11,19 @@ export default {
}, },
computed: { computed: {
message() { message() {
return sprintf(s__('mrWidget|In the merge train at position %{mergeTrainPosition}'), { const messageBeginningTrainPosition = s__(
mergeTrainPosition: this.mergeTrainIndex + 1, 'mrWidget|A new merge train has started and this merge request is the first of the queue.',
}); );
const messageAddedTrainPosition = sprintf(
s__(
'mrWidget|Added to the merge train. There are %{mergeTrainPosition} merge requests waiting to be merged',
),
{
mergeTrainPosition: this.mergeTrainIndex + 1,
},
);
return this.mergeTrainIndex === 0 ? messageBeginningTrainPosition : messageAddedTrainPosition;
}, },
}, },
}; };
......
...@@ -136,7 +136,11 @@ export default { ...@@ -136,7 +136,11 @@ export default {
<history-comment-editor <history-comment-editor
v-if="isEditingComment" v-if="isEditingComment"
class="discussion-reply-holder" class="discussion-reply-holder"
<<<<<<< HEAD
:initial-comment="initialComment" :initial-comment="initialComment"
=======
:initial-comment="commentNote"
>>>>>>> origin/master
:is-saving="isSavingComment" :is-saving="isSavingComment"
@onSave="saveComment" @onSave="saveComment"
@onCancel="cancelEditingComment" @onCancel="cancelEditingComment"
......
<script> <script>
<<<<<<< HEAD
import { sanitize } from 'dompurify'; import { sanitize } from 'dompurify';
import { GlFormTextarea, GlButton, GlLoadingIcon } from '@gitlab/ui'; import { GlFormTextarea, GlButton, GlLoadingIcon } from '@gitlab/ui';
=======
import { GlFormTextarea, GlButton } from '@gitlab/ui';
>>>>>>> origin/master
export default { export default {
components: { GlFormTextarea, GlButton, GlLoadingIcon }, components: { GlFormTextarea, GlButton },
props: { props: {
initialComment: { initialComment: {
type: String, type: String,
...@@ -44,9 +48,13 @@ export default { ...@@ -44,9 +48,13 @@ export default {
ref="saveButton" ref="saveButton"
variant="success" variant="success"
:disabled="isSaveButtonDisabled" :disabled="isSaveButtonDisabled"
<<<<<<< HEAD
@click="$emit('onSave', sanitizedComment)" @click="$emit('onSave', sanitizedComment)"
=======
:loading="isSaving"
@click="$emit('onSave', trimmedComment)"
>>>>>>> origin/master
> >
<gl-loading-icon v-if="isSaving" class="mr-1" />
{{ __('Save comment') }} {{ __('Save comment') }}
</gl-button> </gl-button>
<gl-button ref="cancelButton" class="ml-1" :disabled="isSaving" @click="$emit('onCancel')"> <gl-button ref="cancelButton" class="ml-1" :disabled="isSaving" @click="$emit('onCancel')">
......
<script> <script>
import axios from 'axios'; import axios from 'axios';
import { GlButton } from '@gitlab/ui'; import { GlButton, GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
import RelatedIssuesStore from '~/related_issues/stores/related_issues_store'; import RelatedIssuesStore from '~/related_issues/stores/related_issues_store';
import RelatedIssuesBlock from '~/related_issues/components/related_issues_block.vue'; import RelatedIssuesBlock from '~/related_issues/components/related_issues_block.vue';
import { issuableTypesMap, PathIdSeparator } from '~/related_issues/constants'; import { issuableTypesMap, PathIdSeparator } from '~/related_issues/constants';
...@@ -15,6 +15,9 @@ export default { ...@@ -15,6 +15,9 @@ export default {
components: { components: {
RelatedIssuesBlock, RelatedIssuesBlock,
GlButton, GlButton,
GlAlert,
GlSprintf,
GlLink,
}, },
props: { props: {
endpoint: { endpoint: {
...@@ -45,6 +48,7 @@ export default { ...@@ -45,6 +48,7 @@ export default {
isFetching: false, isFetching: false,
isSubmitting: false, isSubmitting: false,
isFormVisible: false, isFormVisible: false,
errorCreatingIssue: false,
inputValue: '', inputValue: '',
}; };
}, },
...@@ -69,6 +73,12 @@ export default { ...@@ -69,6 +73,12 @@ export default {
reportType: { reportType: {
type: String, type: String,
}, },
issueTrackingHelpPath: {
type: String,
},
permissionsHelpPath: {
type: String,
},
}, },
created() { created() {
this.fetchRelatedIssues(); this.fetchRelatedIssues();
...@@ -76,6 +86,7 @@ export default { ...@@ -76,6 +86,7 @@ export default {
methods: { methods: {
createIssue() { createIssue() {
this.isProcessingAction = true; this.isProcessingAction = true;
this.errorCreatingIssue = false;
return axios return axios
.post(this.createIssueUrl, { .post(this.createIssueUrl, {
...@@ -95,9 +106,7 @@ export default { ...@@ -95,9 +106,7 @@ export default {
}) })
.catch(() => { .catch(() => {
this.isProcessingAction = false; this.isProcessingAction = false;
createFlash( this.errorCreatingIssue = true;
s__('VulnerabilityManagement|Something went wrong, could not create an issue.'),
);
}); });
}, },
toggleFormVisibility() { toggleFormVisibility() {
...@@ -204,44 +213,76 @@ export default { ...@@ -204,44 +213,76 @@ export default {
autoCompleteSources: gl?.GfmAutoComplete?.dataSources, autoCompleteSources: gl?.GfmAutoComplete?.dataSources,
issuableType: issuableTypesMap.ISSUE, issuableType: issuableTypesMap.ISSUE,
pathIdSeparator: PathIdSeparator.Issue, pathIdSeparator: PathIdSeparator.Issue,
i18n: {
relatedIssues: __('Related issues'),
createIssue: __('Create issue'),
createIssueErrorTitle: __('Could not create issue'),
createIssueErrorBody: s__(
'SecurityReports|Ensure that %{trackingStart}issue tracking%{trackingEnd} is enabled for this project and you have %{permissionsStart}permission to create new issues%{permissionsEnd}.',
),
},
}; };
</script> </script>
<template> <template>
<related-issues-block <div>
:help-path="helpPath" <gl-alert
:is-fetching="isFetching" v-if="errorCreatingIssue"
:is-submitting="isSubmitting" variant="danger"
:related-issues="state.relatedIssues" class="gl-mt-5"
:can-admin="canModifyRelatedIssues" @dismiss="errorCreatingIssue = false"
:pending-references="state.pendingReferences" >
:is-form-visible="isFormVisible" <p class="gl-font-weight-bold gl-mb-2">{{ $options.i18n.createIssueErrorTitle }}</p>
:input-value="inputValue" <p class="gl-mb-0">
:auto-complete-sources="$options.autoCompleteSources" <gl-sprintf :message="$options.i18n.createIssueErrorBody">
:issuable-type="$options.issuableType" <template #tracking="{ content }">
:path-id-separator="$options.pathIdSeparator" <gl-link class="gl-display-inline-block" :href="issueTrackingHelpPath" target="_blank">
:show-categorized-issues="false" {{ content }}
@toggleAddRelatedIssuesForm="toggleFormVisibility" </gl-link>
@addIssuableFormInput="addPendingReferences" </template>
@addIssuableFormBlur="processAllReferences" <template #permissions="{ content }">
@addIssuableFormSubmit="addRelatedIssue" <gl-link class="gl-display-inline-block" :href="permissionsHelpPath" target="_blank">
@addIssuableFormCancel="resetForm" {{ content }}
@pendingIssuableRemoveRequest="removePendingReference" </gl-link>
@relatedIssueRemoveRequest="removeRelatedIssue" </template>
> </gl-sprintf>
<template #headerText> </p>
{{ __('Related issues') }} </gl-alert>
</template> <related-issues-block
<template v-if="!isIssueAlreadyCreated && !isFetching" #headerActions> :help-path="helpPath"
<gl-button :is-fetching="isFetching"
ref="createIssue" :is-submitting="isSubmitting"
variant="success" :related-issues="state.relatedIssues"
category="secondary" :can-admin="canModifyRelatedIssues"
:loading="isProcessingAction" :pending-references="state.pendingReferences"
@click="createIssue" :is-form-visible="isFormVisible"
> :input-value="inputValue"
{{ __('Create issue') }} :auto-complete-sources="$options.autoCompleteSources"
</gl-button> :issuable-type="$options.issuableType"
</template> :path-id-separator="$options.pathIdSeparator"
</related-issues-block> :show-categorized-issues="false"
@toggleAddRelatedIssuesForm="toggleFormVisibility"
@addIssuableFormInput="addPendingReferences"
@addIssuableFormBlur="processAllReferences"
@addIssuableFormSubmit="addRelatedIssue"
@addIssuableFormCancel="resetForm"
@pendingIssuableRemoveRequest="removePendingReference"
@relatedIssueRemoveRequest="removeRelatedIssue"
>
<template #headerText>
{{ $options.i18n.relatedIssues }}
</template>
<template v-if="!isIssueAlreadyCreated && !isFetching" #headerActions>
<gl-button
ref="createIssue"
variant="success"
category="secondary"
:loading="isProcessingAction"
@click="createIssue"
>
{{ $options.i18n.createIssue }}
</gl-button>
</template>
</related-issues-block>
</div>
</template> </template>
...@@ -11,7 +11,6 @@ module SubscriptionsHelper ...@@ -11,7 +11,6 @@ module SubscriptionsHelper
plan_id: params[:plan_id], plan_id: params[:plan_id],
namespace_id: params[:namespace_id], namespace_id: params[:namespace_id],
new_user: new_user?.to_s, new_user: new_user?.to_s,
onboarding_issues_experiment_enabled: experiment_enabled?(:onboarding_issues).to_s,
group_data: group_data.to_json group_data: group_data.to_json
} }
end end
......
...@@ -18,7 +18,9 @@ module VulnerabilitiesHelper ...@@ -18,7 +18,9 @@ module VulnerabilitiesHelper
vulnerability_feedback_help_path: help_page_path('user/application_security/index', anchor: 'interacting-with-the-vulnerabilities'), vulnerability_feedback_help_path: help_page_path('user/application_security/index', anchor: 'interacting-with-the-vulnerabilities'),
related_issues_help_path: help_page_path('user/application_security/index', anchor: 'managing-related-issues-for-a-vulnerability'), related_issues_help_path: help_page_path('user/application_security/index', anchor: 'managing-related-issues-for-a-vulnerability'),
pipeline: vulnerability_pipeline_data(pipeline), pipeline: vulnerability_pipeline_data(pipeline),
can_modify_related_issues: current_user.can?(:admin_vulnerability_issue_link, vulnerability) can_modify_related_issues: current_user.can?(:admin_vulnerability_issue_link, vulnerability),
issue_tracking_help_path: help_page_path('user/project/settings', anchor: 'sharing-and-permissions'),
permissions_help_path: help_page_path('user/permissions', anchor: 'project-members-permissions')
} }
result.merge(vulnerability_data(vulnerability), vulnerability_finding_data(vulnerability)) result.merge(vulnerability_data(vulnerability), vulnerability_finding_data(vulnerability))
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
.row.flex-grow-1.bg-gray-light .row.flex-grow-1.bg-gray-light
.d-flex.flex-column.align-items-center.w-100.p-3 .d-flex.flex-column.align-items-center.w-100.p-3
.new-project.d-flex.flex-column.align-items-center.pt-5 .new-project.d-flex.flex-column.align-items-center.pt-5
#progress-bar{ data: { is_in_subscription_flow: in_subscription_flow?.to_s } } #progress-bar
%h2.center= _('Create/import your first project') %h2.center= _('Create/import your first project')
%p %p
.center= html_escape(_('This project will live in your group %{strong_open}%{namespace}%{strong_close}. A project is where you house your files (repository), plan your work (issues), publish your documentation (wiki), and so much more.')) % { namespace: html_escape(@project.namespace.name), strong_open: '<strong>'.html_safe, strong_close: '</strong>'.html_safe } .center= html_escape(_('This project will live in your group %{strong_open}%{namespace}%{strong_close}. A project is where you house your files (repository), plan your work (issues), publish your documentation (wiki), and so much more.')) % { namespace: html_escape(@project.namespace.name), strong_open: '<strong>'.html_safe, strong_close: '</strong>'.html_safe }
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
%p= _('You have successfully purchased a %{plan} plan subscription for %{seats}. You’ll receive a receipt via email.') % { plan: plan_title, seats: number_of_users } %p= _('You have successfully purchased a %{plan} plan subscription for %{seats}. You’ll receive a receipt via email.') % { plan: plan_title, seats: number_of_users }
.edit-group.d-flex.flex-column.align-items-center.gl-pt-7 .edit-group.d-flex.flex-column.align-items-center.gl-pt-7
- if params[:new_user] - if params[:new_user]
#progress-bar{ data: { is_onboarding_issues_experiment_enabled: experiment_enabled?(:onboarding_issues).to_s } } #progress-bar
%h2.center= _('Create your group') %h2.center= _('Create your group')
%p %p
%div= _('A group represents your organization in GitLab. Groups allow you to manage users and collaborate across multiple projects.') %div= _('A group represents your organization in GitLab. Groups allow you to manage users and collaborate across multiple projects.')
......
---
title: Simplify progress bar steps logic
merge_request: 40390
author:
type: changed
---
title: Fix vulnerability save button spinner position
merge_request: 40781
author:
type: fixed
---
title: Improve error message when creating issue fails
merge_request: 40525
author:
type: changed
---
title: Change merge train position messaging
merge_request: 40777
author:
type: changed
...@@ -5,12 +5,10 @@ require 'spec_helper' ...@@ -5,12 +5,10 @@ require 'spec_helper'
RSpec.describe 'New project screen', :js do RSpec.describe 'New project screen', :js do
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:namespace) { create(:group) } let_it_be(:namespace) { create(:group) }
let(:in_subscription_flow) { false }
before do before do
gitlab_sign_in(user) gitlab_sign_in(user)
namespace.add_owner(user) namespace.add_owner(user)
allow_any_instance_of(EE::RegistrationsHelper).to receive(:in_subscription_flow?).and_return(in_subscription_flow)
stub_experiment_for_user(onboarding_issues: true) stub_experiment_for_user(onboarding_issues: true)
visit new_users_sign_up_project_path(namespace_id: namespace.id) visit new_users_sign_up_project_path(namespace_id: namespace.id)
end end
...@@ -21,10 +19,4 @@ RSpec.describe 'New project screen', :js do ...@@ -21,10 +19,4 @@ RSpec.describe 'New project screen', :js do
expect(subject).to have_content('Create/import your first project') expect(subject).to have_content('Create/import your first project')
expect(subject).to have_content('Your profile Your GitLab group Your first project') expect(subject).to have_content('Your profile Your GitLab group Your first project')
end end
context 'when in the subscription flow' do
let(:in_subscription_flow) { true }
it { is_expected.to have_content('Your profile Checkout Your GitLab group Your first project') }
end
end end
...@@ -58,14 +58,5 @@ RSpec.describe 'Welcome screen', :js do ...@@ -58,14 +58,5 @@ RSpec.describe 'Welcome screen', :js do
end end
end end
end end
context 'when in the subscription flow and part of the onboarding issues experiment' do
let(:in_subscription_flow) { true }
let(:part_of_onboarding_issues_experiment) { true }
it 'shows the progress bar with the correct steps' do
expect(page).to have_content('Your profile Checkout Your GitLab group Your first project')
end
end
end end
end end
...@@ -7,13 +7,11 @@ RSpec.describe 'Welcome screen', :js do ...@@ -7,13 +7,11 @@ RSpec.describe 'Welcome screen', :js do
let_it_be(:group) { create(:group) } let_it_be(:group) { create(:group) }
let(:params) { {} } let(:params) { {} }
let(:part_of_onboarding_issues_experiment) { false }
describe 'on GitLab.com' do describe 'on GitLab.com' do
before do before do
group.add_owner(user) group.add_owner(user)
gitlab_sign_in(user) gitlab_sign_in(user)
stub_experiment_for_user(onboarding_issues: part_of_onboarding_issues_experiment)
stub_request(:get, 'https://customers.gitlab.com/gitlab_plans?plan=free') stub_request(:get, 'https://customers.gitlab.com/gitlab_plans?plan=free')
.to_return(status: 200, body: '{}', headers: {}) .to_return(status: 200, body: '{}', headers: {})
...@@ -31,14 +29,6 @@ RSpec.describe 'Welcome screen', :js do ...@@ -31,14 +29,6 @@ RSpec.describe 'Welcome screen', :js do
it 'shows the progress bar with the correct steps' do it 'shows the progress bar with the correct steps' do
expect(page).to have_content('Your profile Checkout Your GitLab group') expect(page).to have_content('Your profile Checkout Your GitLab group')
end end
context 'when part of the onboarding issues experiment' do
let(:part_of_onboarding_issues_experiment) { true }
it 'shows the progress bar with the correct steps' do
expect(page).to have_content('Your profile Checkout Your GitLab group Your first project')
end
end
end end
end end
end end
...@@ -140,16 +140,33 @@ describe('ThroughputTable', () => { ...@@ -140,16 +140,33 @@ describe('ThroughputTable', () => {
it('includes the correct title and IID', () => { it('includes the correct title and IID', () => {
const { title, iid } = throughputTableData[0]; const { title, iid } = throughputTableData[0];
expect(findCol(TEST_IDS.MERGE_REQUEST_DETAILS).text()).toBe(`${title} !${iid}`); expect(findCol(TEST_IDS.MERGE_REQUEST_DETAILS).text()).toContain(`${title} !${iid}`);
}); });
it('does not include any icons by default', () => { it('includes an inactive label icon by default', () => {
const icon = findColSubComponent(TEST_IDS.MERGE_REQUEST_DETAILS, GlIcon); const labels = findColSubItem(TEST_IDS.MERGE_REQUEST_DETAILS, TEST_IDS.LABEL_DETAILS);
const icon = labels.find(GlIcon);
expect(labels.text()).toBe('0');
expect(labels.classes()).toContain('gl-opacity-5');
expect(icon.exists()).toBe(true);
expect(icon.props('name')).toBe('label');
});
expect(icon.exists()).toBe(false); it('includes an inactive comment icon by default', () => {
const commentCount = findColSubItem(
TEST_IDS.MERGE_REQUEST_DETAILS,
TEST_IDS.COMMENT_COUNT,
);
const icon = commentCount.find(GlIcon);
expect(commentCount.text()).toBe('0');
expect(commentCount.classes()).toContain('gl-opacity-5');
expect(icon.exists()).toBe(true);
expect(icon.props('name')).toBe('comments');
}); });
it('includes a label icon and count when available', async () => { it('includes an active label icon and count when available', async () => {
additionalData({ additionalData({
labels: { labels: {
nodes: [{ title: 'Brinix' }], nodes: [{ title: 'Brinix' }],
...@@ -165,10 +182,30 @@ describe('ThroughputTable', () => { ...@@ -165,10 +182,30 @@ describe('ThroughputTable', () => {
const icon = labelDetails.find(GlIcon); const icon = labelDetails.find(GlIcon);
expect(labelDetails.text()).toBe('1'); expect(labelDetails.text()).toBe('1');
expect(labelDetails.classes()).not.toContain('gl-opacity-5');
expect(icon.exists()).toBe(true); expect(icon.exists()).toBe(true);
expect(icon.props('name')).toBe('label'); expect(icon.props('name')).toBe('label');
}); });
it('includes an active comment icon and count when available', async () => {
additionalData({
userNotesCount: 2,
});
await wrapper.vm.$nextTick();
const commentCount = findColSubItem(
TEST_IDS.MERGE_REQUEST_DETAILS,
TEST_IDS.COMMENT_COUNT,
);
const icon = commentCount.find(GlIcon);
expect(commentCount.text()).toBe('2');
expect(commentCount.classes()).not.toContain('gl-opacity-5');
expect(icon.exists()).toBe(true);
expect(icon.props('name')).toBe('comments');
});
it('includes a pipeline icon and when available', async () => { it('includes a pipeline icon and when available', async () => {
const iconName = 'status_canceled'; const iconName = 'status_canceled';
......
...@@ -83,5 +83,6 @@ export const throughputTableData = [ ...@@ -83,5 +83,6 @@ export const throughputTableData = [
nodes: [], nodes: [],
}, },
commitCount: 1, commitCount: 1,
userNotesCount: 0,
}, },
]; ];
...@@ -51,21 +51,6 @@ describe('Checkout', () => { ...@@ -51,21 +51,6 @@ describe('Checkout', () => {
]); ]);
}); });
describe('when part of the onboarding issues experiment', () => {
beforeEach(() => {
store.state.isOnboardingIssuesExperimentEnabled = true;
});
it('passes the steps', () => {
expect(findProgressBar().props('steps')).toEqual([
'Your profile',
'Checkout',
'Your GitLab group',
'Your first project',
]);
});
});
it('passes the current step', () => { it('passes the current step', () => {
expect(findProgressBar().props('currentStep')).toEqual('Checkout'); expect(findProgressBar().props('currentStep')).toEqual('Checkout');
}); });
......
...@@ -4,14 +4,11 @@ import MergeTrainPositionIndicator from 'ee/vue_merge_request_widget/components/ ...@@ -4,14 +4,11 @@ import MergeTrainPositionIndicator from 'ee/vue_merge_request_widget/components/
describe('MergeTrainPositionIndicator', () => { describe('MergeTrainPositionIndicator', () => {
let wrapper; let wrapper;
let vm;
const factory = propsData => { const factory = propsData => {
wrapper = shallowMount(MergeTrainPositionIndicator, { wrapper = shallowMount(MergeTrainPositionIndicator, {
propsData, propsData,
}); });
({ vm } = wrapper);
}; };
afterEach(() => { afterEach(() => {
...@@ -19,19 +16,21 @@ describe('MergeTrainPositionIndicator', () => { ...@@ -19,19 +16,21 @@ describe('MergeTrainPositionIndicator', () => {
}); });
describe('computed', () => { describe('computed', () => {
describe('message', () => { describe('template', () => {
it('should return the message with the correct position (i.e., index + 1)', () => { it('should render the correct message', () => {
factory({ mergeTrainIndex: 3 }); factory({ mergeTrainIndex: 3 });
expect(vm.message).toBe('In the merge train at position 4'); expect(trimText(wrapper.text())).toBe(
'Added to the merge train. There are 4 merge requests waiting to be merged',
);
}); });
});
describe('template', () => { it('should change the merge train message when the position is 1', () => {
it('should render the correct message', () => { factory({ mergeTrainIndex: 0 });
factory({ mergeTrainIndex: 3 });
expect(trimText(wrapper.text())).toBe('In the merge train at position 4'); expect(trimText(wrapper.text())).toBe(
'A new merge train has started and this merge request is the first of the queue.',
);
}); });
}); });
}); });
......
...@@ -73,20 +73,19 @@ describe('History Comment Editor', () => { ...@@ -73,20 +73,19 @@ describe('History Comment Editor', () => {
expect(wrapper.emitted().onCancel).toHaveLength(1); expect(wrapper.emitted().onCancel).toHaveLength(1);
}); });
it('disables the save button when there is no text or only whitespace in the textarea', () => { it('disables the save button when there is no text or only whitespace in the textarea', async () => {
createWrapper({ initialComment: 'some comment' }); createWrapper({ initialComment: 'some comment' });
textarea().vm.$emit('input', ' '); textarea().vm.$emit('input', ' ');
await wrapper.vm.$nextTick();
return wrapper.vm.$nextTick().then(() => { expect(saveButton().props('disabled')).toBe(true);
expect(saveButton().attributes('disabled')).toBeTruthy();
});
}); });
it('disables all elements when the isSaving prop is true', () => { it('disables all elements when the comment is being saved', () => {
createWrapper({ isSaving: true }); createWrapper({ isSaving: true });
expect(textarea().attributes('disabled')).toBeTruthy(); expect(textarea().attributes('disabled')).toBeTruthy();
expect(saveButton().attributes('disabled')).toBeTruthy(); expect(saveButton().props('loading')).toBe(true);
expect(cancelButton().attributes('disabled')).toBeTruthy(); expect(cancelButton().props('disabled')).toBe(true);
}); });
}); });
import { GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import RelatedIssues from 'ee/vulnerabilities/components/related_issues.vue'; import RelatedIssues from 'ee/vulnerabilities/components/related_issues.vue';
...@@ -27,6 +28,8 @@ describe('Vulnerability related issues component', () => { ...@@ -27,6 +28,8 @@ describe('Vulnerability related issues component', () => {
const vulnerabilityId = 5131; const vulnerabilityId = 5131;
const createIssueUrl = '/create/issue'; const createIssueUrl = '/create/issue';
const projectFingerprint = 'project-fingerprint'; const projectFingerprint = 'project-fingerprint';
const issueTrackingHelpPath = '/help/issue/tracking';
const permissionsHelpPath = '/help/permissions';
const reportType = 'vulnerability'; const reportType = 'vulnerability';
const issue1 = { id: 3, vulnerabilityLinkId: 987 }; const issue1 = { id: 3, vulnerabilityLinkId: 987 };
const issue2 = { id: 25, vulnerabilityLinkId: 876 }; const issue2 = { id: 25, vulnerabilityLinkId: 876 };
...@@ -40,6 +43,8 @@ describe('Vulnerability related issues component', () => { ...@@ -40,6 +43,8 @@ describe('Vulnerability related issues component', () => {
projectFingerprint, projectFingerprint,
createIssueUrl, createIssueUrl,
reportType, reportType,
issueTrackingHelpPath,
permissionsHelpPath,
}, },
...opts, ...opts,
}); });
...@@ -54,6 +59,7 @@ describe('Vulnerability related issues component', () => { ...@@ -54,6 +59,7 @@ describe('Vulnerability related issues component', () => {
const blockProp = prop => relatedIssuesBlock().props(prop); const blockProp = prop => relatedIssuesBlock().props(prop);
const blockEmit = (eventName, data) => relatedIssuesBlock().vm.$emit(eventName, data); const blockEmit = (eventName, data) => relatedIssuesBlock().vm.$emit(eventName, data);
const findCreateIssueButton = () => wrapper.find({ ref: 'createIssue' }); const findCreateIssueButton = () => wrapper.find({ ref: 'createIssue' });
const findAlert = () => wrapper.find(GlAlert);
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
...@@ -275,6 +281,13 @@ describe('Vulnerability related issues component', () => { ...@@ -275,6 +281,13 @@ describe('Vulnerability related issues component', () => {
}); });
describe('when linked issue is not yet created', () => { describe('when linked issue is not yet created', () => {
const failCreateIssueAction = async () => {
mockAxios.onPost(createIssueUrl).reply(500);
expect(findAlert().exists()).toBe(false);
findCreateIssueButton().vm.$emit('click');
await waitForPromises();
};
beforeEach(async () => { beforeEach(async () => {
mockAxios.onGet(propsData.endpoint).replyOnce(httpStatusCodes.OK, [issue1, issue2]); mockAxios.onGet(propsData.endpoint).replyOnce(httpStatusCodes.OK, [issue1, issue2]);
createWrapper({}, { stubs: { RelatedIssuesBlock } }); createWrapper({}, { stubs: { RelatedIssuesBlock } });
...@@ -313,15 +326,17 @@ describe('Vulnerability related issues component', () => { ...@@ -313,15 +326,17 @@ describe('Vulnerability related issues component', () => {
}); });
}); });
it('shows an error message when issue creation fails', () => { it('shows an error message when issue creation fails', async () => {
mockAxios.onPost(createIssueUrl).reply(500); await failCreateIssueAction();
findCreateIssueButton().vm.$emit('click'); expect(mockAxios.history.post).toHaveLength(1);
return waitForPromises().then(() => { expect(findAlert().exists()).toBe(true);
expect(mockAxios.history.post).toHaveLength(1); });
expect(createFlash).toHaveBeenCalledWith(
'Something went wrong, could not create an issue.', it('dismisses the error message', async () => {
); await failCreateIssueAction();
}); findAlert().vm.$emit('dismiss');
await wrapper.vm.$nextTick();
expect(findAlert().exists()).toBe(false);
}); });
}); });
}); });
...@@ -35,7 +35,6 @@ RSpec.describe SubscriptionsHelper do ...@@ -35,7 +35,6 @@ RSpec.describe SubscriptionsHelper do
before do before do
allow(helper).to receive(:params).and_return(plan_id: 'bronze_id', namespace_id: group.id.to_s) allow(helper).to receive(:params).and_return(plan_id: 'bronze_id', namespace_id: group.id.to_s)
allow(helper).to receive(:current_user).and_return(user) allow(helper).to receive(:current_user).and_return(user)
allow(helper).to receive(:experiment_enabled?).with(:onboarding_issues).and_return(false)
group.add_owner(user) group.add_owner(user)
end end
......
...@@ -11,7 +11,6 @@ RSpec.describe 'subscriptions/groups/edit' do ...@@ -11,7 +11,6 @@ RSpec.describe 'subscriptions/groups/edit' do
allow(view).to receive(:group_path).and_return('') allow(view).to receive(:group_path).and_return('')
allow(view).to receive(:subscriptions_groups_path).and_return('') allow(view).to receive(:subscriptions_groups_path).and_return('')
allow(view).to receive(:current_user).and_return(User.new) allow(view).to receive(:current_user).and_return(User.new)
allow(view).to receive(:experiment_enabled?).with(:onboarding_issues).and_return(false)
end end
let(:quantity) { '1' } let(:quantity) { '1' }
......
.dast-auto-deploy: .dast-auto-deploy:
image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v1.0.0" image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v1.0.2"
dast_environment_deploy: dast_environment_deploy:
extends: .dast-auto-deploy extends: .dast-auto-deploy
......
.auto-deploy: .auto-deploy:
image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v1.0.0" image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v1.0.2"
dependencies: [] dependencies: []
include: include:
......
...@@ -7024,6 +7024,9 @@ msgstr "" ...@@ -7024,6 +7024,9 @@ msgstr ""
msgid "Could not create group" msgid "Could not create group"
msgstr "" msgstr ""
msgid "Could not create issue"
msgstr ""
msgid "Could not create project" msgid "Could not create project"
msgstr "" msgstr ""
...@@ -9284,9 +9287,6 @@ msgstr "" ...@@ -9284,9 +9287,6 @@ msgstr ""
msgid "Enable usage ping" msgid "Enable usage ping"
msgstr "" msgstr ""
msgid "Enable usage ping to get an overview of how you are using GitLab from a feature perspective."
msgstr ""
msgid "Enable/disable your service desk. %{link_start}Learn more about service desk%{link_end}." msgid "Enable/disable your service desk. %{link_start}Learn more about service desk%{link_end}."
msgstr "" msgstr ""
...@@ -13037,9 +13037,6 @@ msgstr "" ...@@ -13037,9 +13037,6 @@ msgstr ""
msgid "In %{time_to_now}" msgid "In %{time_to_now}"
msgstr "" msgstr ""
msgid "In order to enable instance-level analytics, please ask an admin to enable %{usage_ping_link_start}usage ping%{usage_ping_link_end}."
msgstr ""
msgid "In order to gather accurate feature usage data, it can take 1 to 2 weeks to see your index." msgid "In order to gather accurate feature usage data, it can take 1 to 2 weeks to see your index."
msgstr "" msgstr ""
...@@ -21876,6 +21873,9 @@ msgstr "" ...@@ -21876,6 +21873,9 @@ msgstr ""
msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed." msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr "" msgstr ""
msgid "SecurityReports|Ensure that %{trackingStart}issue tracking%{trackingEnd} is enabled for this project and you have %{permissionsStart}permission to create new issues%{permissionsEnd}."
msgstr ""
msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again." msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr "" msgstr ""
...@@ -25977,6 +25977,9 @@ msgstr "" ...@@ -25977,6 +25977,9 @@ msgstr ""
msgid "To view all %{scannedResourcesCount} scanned URLs, please download the CSV file" msgid "To view all %{scannedResourcesCount} scanned URLs, please download the CSV file"
msgstr "" msgstr ""
msgid "To view instance-level analytics, ask an admin to turn on %{docLinkStart}usage ping%{docLinkEnd}."
msgstr ""
msgid "To view the roadmap, add a start or due date to one of your epics in this group or its subgroups. In the months view, only epics in the past month, current month, and next 5 months are shown." msgid "To view the roadmap, add a start or due date to one of your epics in this group or its subgroups. In the months view, only epics in the past month, current month, and next 5 months are shown."
msgstr "" msgstr ""
...@@ -26286,6 +26289,9 @@ msgstr "" ...@@ -26286,6 +26289,9 @@ msgstr ""
msgid "Turn on usage ping" msgid "Turn on usage ping"
msgstr "" msgstr ""
msgid "Turn on usage ping to review instance-level analytics."
msgstr ""
msgid "Twitter" msgid "Twitter"
msgstr "" msgstr ""
...@@ -26763,7 +26769,7 @@ msgstr "" ...@@ -26763,7 +26769,7 @@ msgstr ""
msgid "Usage" msgid "Usage"
msgstr "" msgstr ""
msgid "Usage ping is not enabled" msgid "Usage ping is off"
msgstr "" msgstr ""
msgid "Usage statistics" msgid "Usage statistics"
...@@ -27550,9 +27556,6 @@ msgstr "" ...@@ -27550,9 +27556,6 @@ msgstr ""
msgid "VulnerabilityManagement|Something went wrong while trying to unlink the issue. Please try again later." msgid "VulnerabilityManagement|Something went wrong while trying to unlink the issue. Please try again later."
msgstr "" msgstr ""
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
msgid "VulnerabilityManagement|Something went wrong, could not get user." msgid "VulnerabilityManagement|Something went wrong, could not get user."
msgstr "" msgstr ""
...@@ -29537,9 +29540,15 @@ msgstr "" ...@@ -29537,9 +29540,15 @@ msgstr ""
msgid "mrWidget|%{prefixToLinkStart}No pipeline%{prefixToLinkEnd} %{addPipelineLinkStart}Add the .gitlab-ci.yml file%{addPipelineLinkEnd} to create one." msgid "mrWidget|%{prefixToLinkStart}No pipeline%{prefixToLinkEnd} %{addPipelineLinkStart}Add the .gitlab-ci.yml file%{addPipelineLinkEnd} to create one."
msgstr "" msgstr ""
msgid "mrWidget|A new merge train has started and this merge request is the first of the queue."
msgstr ""
msgid "mrWidget|Added to the merge train by" msgid "mrWidget|Added to the merge train by"
msgstr "" msgstr ""
msgid "mrWidget|Added to the merge train. There are %{mergeTrainPosition} merge requests waiting to be merged"
msgstr ""
msgid "mrWidget|Allows commits from members who can merge to the target branch" msgid "mrWidget|Allows commits from members who can merge to the target branch"
msgstr "" msgstr ""
...@@ -29627,9 +29636,6 @@ msgstr "" ...@@ -29627,9 +29636,6 @@ msgstr ""
msgid "mrWidget|If the %{missingBranchName} branch exists in your local repository, you can merge this merge request manually using the command line" msgid "mrWidget|If the %{missingBranchName} branch exists in your local repository, you can merge this merge request manually using the command line"
msgstr "" msgstr ""
msgid "mrWidget|In the merge train at position %{mergeTrainPosition}"
msgstr ""
msgid "mrWidget|Jump to first unresolved thread" msgid "mrWidget|Jump to first unresolved thread"
msgstr "" msgstr ""
......
...@@ -22,10 +22,10 @@ RSpec.describe 'DevOps Report page' do ...@@ -22,10 +22,10 @@ RSpec.describe 'DevOps Report page' do
stub_application_setting(usage_ping_enabled: false) stub_application_setting(usage_ping_enabled: false)
end end
it 'shows empty state' do it 'shows empty state', :js do
visit admin_dev_ops_score_path visit admin_dev_ops_score_path
expect(page).to have_content('Usage ping is not enabled') expect(page).to have_selector(".js-empty-state")
end end
it 'hides the intro callout' do it 'hides the intro callout' do
......
...@@ -22,11 +22,11 @@ describe('IDE pipelines list', () => { ...@@ -22,11 +22,11 @@ describe('IDE pipelines list', () => {
const defaultState = { const defaultState = {
links: { ciHelpPagePath: TEST_HOST }, links: { ciHelpPagePath: TEST_HOST },
pipelinesEmptyStateSvgPath: TEST_HOST, pipelinesEmptyStateSvgPath: TEST_HOST,
pipelines: { };
stages: [], const defaultPipelinesState = {
failedStages: [], stages: [],
isLoadingJobs: false, failedStages: [],
}, isLoadingJobs: false,
}; };
const fetchLatestPipelineMock = jest.fn(); const fetchLatestPipelineMock = jest.fn();
...@@ -34,23 +34,20 @@ describe('IDE pipelines list', () => { ...@@ -34,23 +34,20 @@ describe('IDE pipelines list', () => {
const failedStagesGetterMock = jest.fn().mockReturnValue([]); const failedStagesGetterMock = jest.fn().mockReturnValue([]);
const fakeProjectPath = 'alpha/beta'; const fakeProjectPath = 'alpha/beta';
const createComponent = (state = {}) => { const createStore = (rootState, pipelinesState) => {
const { pipelines: pipelinesState, ...restOfState } = state; return new Vuex.Store({
const { defaultPipelines, ...defaultRestOfState } = defaultState;
const fakeStore = new Vuex.Store({
getters: { getters: {
currentProject: () => ({ web_url: 'some/url ', path_with_namespace: fakeProjectPath }), currentProject: () => ({ web_url: 'some/url ', path_with_namespace: fakeProjectPath }),
}, },
state: { state: {
...defaultRestOfState, ...defaultState,
...restOfState, ...rootState,
}, },
modules: { modules: {
pipelines: { pipelines: {
namespaced: true, namespaced: true,
state: { state: {
...defaultPipelines, ...defaultPipelinesState,
...pipelinesState, ...pipelinesState,
}, },
actions: { actions: {
...@@ -69,10 +66,12 @@ describe('IDE pipelines list', () => { ...@@ -69,10 +66,12 @@ describe('IDE pipelines list', () => {
}, },
}, },
}); });
};
const createComponent = (state = {}, pipelinesState = {}) => {
wrapper = shallowMount(List, { wrapper = shallowMount(List, {
localVue, localVue,
store: fakeStore, store: createStore(state, pipelinesState),
}); });
}; };
...@@ -94,31 +93,33 @@ describe('IDE pipelines list', () => { ...@@ -94,31 +93,33 @@ describe('IDE pipelines list', () => {
describe('when loading', () => { describe('when loading', () => {
let defaultPipelinesLoadingState; let defaultPipelinesLoadingState;
beforeAll(() => { beforeAll(() => {
defaultPipelinesLoadingState = { defaultPipelinesLoadingState = {
...defaultState.pipelines,
isLoadingPipeline: true, isLoadingPipeline: true,
}; };
}); });
it('does not render when pipeline has loaded before', () => { it('does not render when pipeline has loaded before', () => {
createComponent({ createComponent(
pipelines: { {},
{
...defaultPipelinesLoadingState, ...defaultPipelinesLoadingState,
hasLoadedPipeline: true, hasLoadedPipeline: true,
}, },
}); );
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
}); });
it('renders loading state', () => { it('renders loading state', () => {
createComponent({ createComponent(
pipelines: { {},
{
...defaultPipelinesLoadingState, ...defaultPipelinesLoadingState,
hasLoadedPipeline: false, hasLoadedPipeline: false,
}, },
}); );
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
}); });
...@@ -126,21 +127,22 @@ describe('IDE pipelines list', () => { ...@@ -126,21 +127,22 @@ describe('IDE pipelines list', () => {
describe('when loaded', () => { describe('when loaded', () => {
let defaultPipelinesLoadedState; let defaultPipelinesLoadedState;
beforeAll(() => { beforeAll(() => {
defaultPipelinesLoadedState = { defaultPipelinesLoadedState = {
...defaultState.pipelines,
isLoadingPipeline: false, isLoadingPipeline: false,
hasLoadedPipeline: true, hasLoadedPipeline: true,
}; };
}); });
it('renders empty state when no latestPipeline', () => { it('renders empty state when no latestPipeline', () => {
createComponent({ pipelines: { ...defaultPipelinesLoadedState, latestPipeline: null } }); createComponent({}, { ...defaultPipelinesLoadedState, latestPipeline: null });
expect(wrapper.element).toMatchSnapshot(); expect(wrapper.element).toMatchSnapshot();
}); });
describe('with latest pipeline loaded', () => { describe('with latest pipeline loaded', () => {
let withLatestPipelineState; let withLatestPipelineState;
beforeAll(() => { beforeAll(() => {
withLatestPipelineState = { withLatestPipelineState = {
...defaultPipelinesLoadedState, ...defaultPipelinesLoadedState,
...@@ -149,12 +151,12 @@ describe('IDE pipelines list', () => { ...@@ -149,12 +151,12 @@ describe('IDE pipelines list', () => {
}); });
it('renders ci icon', () => { it('renders ci icon', () => {
createComponent({ pipelines: withLatestPipelineState }); createComponent({}, withLatestPipelineState);
expect(wrapper.find(CiIcon).exists()).toBe(true); expect(wrapper.find(CiIcon).exists()).toBe(true);
}); });
it('renders pipeline data', () => { it('renders pipeline data', () => {
createComponent({ pipelines: withLatestPipelineState }); createComponent({}, withLatestPipelineState);
expect(wrapper.text()).toContain('#1'); expect(wrapper.text()).toContain('#1');
}); });
...@@ -162,7 +164,7 @@ describe('IDE pipelines list', () => { ...@@ -162,7 +164,7 @@ describe('IDE pipelines list', () => {
it('renders list of jobs', () => { it('renders list of jobs', () => {
const stages = []; const stages = [];
const isLoadingJobs = true; const isLoadingJobs = true;
createComponent({ pipelines: { ...withLatestPipelineState, stages, isLoadingJobs } }); createComponent({}, { ...withLatestPipelineState, stages, isLoadingJobs });
const jobProps = wrapper const jobProps = wrapper
.findAll(Tab) .findAll(Tab)
...@@ -177,7 +179,7 @@ describe('IDE pipelines list', () => { ...@@ -177,7 +179,7 @@ describe('IDE pipelines list', () => {
const failedStages = []; const failedStages = [];
failedStagesGetterMock.mockReset().mockReturnValue(failedStages); failedStagesGetterMock.mockReset().mockReturnValue(failedStages);
const isLoadingJobs = true; const isLoadingJobs = true;
createComponent({ pipelines: { ...withLatestPipelineState, isLoadingJobs } }); createComponent({}, { ...withLatestPipelineState, isLoadingJobs });
const jobProps = wrapper const jobProps = wrapper
.findAll(Tab) .findAll(Tab)
...@@ -191,12 +193,13 @@ describe('IDE pipelines list', () => { ...@@ -191,12 +193,13 @@ describe('IDE pipelines list', () => {
describe('with YAML error', () => { describe('with YAML error', () => {
it('renders YAML error', () => { it('renders YAML error', () => {
const yamlError = 'test yaml error'; const yamlError = 'test yaml error';
createComponent({ createComponent(
pipelines: { {},
{
...defaultPipelinesLoadedState, ...defaultPipelinesLoadedState,
latestPipeline: { ...pipelines[0], yamlError }, latestPipeline: { ...pipelines[0], yamlError },
}, },
}); );
expect(wrapper.text()).toContain('Found errors in your .gitlab-ci.yml:'); expect(wrapper.text()).toContain('Found errors in your .gitlab-ci.yml:');
expect(wrapper.text()).toContain(yamlError); expect(wrapper.text()).toContain(yamlError);
......
...@@ -1196,7 +1196,7 @@ RSpec.describe Issue do ...@@ -1196,7 +1196,7 @@ RSpec.describe Issue do
it 'schedules rebalancing if we time-out when finding a gap' do it 'schedules rebalancing if we time-out when finding a gap' do
lhs = build_stubbed(:issue, relative_position: 99, project: project) lhs = build_stubbed(:issue, relative_position: 99, project: project)
to_move = build(:issue, project: project) to_move = build(:issue, project: project)
expect(IssueRebalancingWorker).to receive(:perform_async).with(issue.id) expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, project.id)
expect { to_move.move_between(lhs, issue) }.to raise_error(ActiveRecord::QueryCanceled) expect { to_move.move_between(lhs, issue) }.to raise_error(ActiveRecord::QueryCanceled)
end end
...@@ -1205,7 +1205,7 @@ RSpec.describe Issue do ...@@ -1205,7 +1205,7 @@ RSpec.describe Issue do
describe '#find_next_gap_after' do describe '#find_next_gap_after' do
it 'schedules rebalancing if we time-out when finding a gap' do it 'schedules rebalancing if we time-out when finding a gap' do
allow(issue).to receive(:find_next_gap) { raise ActiveRecord::QueryCanceled } allow(issue).to receive(:find_next_gap) { raise ActiveRecord::QueryCanceled }
expect(IssueRebalancingWorker).to receive(:perform_async).with(issue.id) expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, project.id)
expect { issue.move_sequence_after }.to raise_error(ActiveRecord::QueryCanceled) expect { issue.move_sequence_after }.to raise_error(ActiveRecord::QueryCanceled)
end end
......
...@@ -77,7 +77,7 @@ RSpec.describe Issues::CreateService do ...@@ -77,7 +77,7 @@ RSpec.describe Issues::CreateService do
it 'rebalances if needed' do it 'rebalances if needed' do
create(:issue, project: project, relative_position: RelativePositioning::MAX_POSITION) create(:issue, project: project, relative_position: RelativePositioning::MAX_POSITION)
expect(IssueRebalancingWorker).to receive(:perform_async).with(Integer) expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, project.id)
expect(issue.relative_position).to eq(project.issues.maximum(:relative_position)) expect(issue.relative_position).to eq(project.issues.maximum(:relative_position))
end end
...@@ -86,7 +86,7 @@ RSpec.describe Issues::CreateService do ...@@ -86,7 +86,7 @@ RSpec.describe Issues::CreateService do
stub_feature_flags(rebalance_issues: false) stub_feature_flags(rebalance_issues: false)
create(:issue, project: project, relative_position: RelativePositioning::MAX_POSITION) create(:issue, project: project, relative_position: RelativePositioning::MAX_POSITION)
expect(IssueRebalancingWorker).not_to receive(:perform_async).with(Integer) expect(IssueRebalancingWorker).not_to receive(:perform_async)
expect(issue.relative_position).to eq(project.issues.maximum(:relative_position)) expect(issue.relative_position).to eq(project.issues.maximum(:relative_position))
end end
...@@ -95,7 +95,7 @@ RSpec.describe Issues::CreateService do ...@@ -95,7 +95,7 @@ RSpec.describe Issues::CreateService do
stub_feature_flags(rebalance_issues: project) stub_feature_flags(rebalance_issues: project)
create(:issue, project: project, relative_position: RelativePositioning::MAX_POSITION) create(:issue, project: project, relative_position: RelativePositioning::MAX_POSITION)
expect(IssueRebalancingWorker).to receive(:perform_async).with(Integer) expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, project.id)
expect(issue.relative_position).to eq(project.issues.maximum(:relative_position)) expect(issue.relative_position).to eq(project.issues.maximum(:relative_position))
end end
......
...@@ -126,7 +126,7 @@ RSpec.describe Issues::UpdateService, :mailer do ...@@ -126,7 +126,7 @@ RSpec.describe Issues::UpdateService, :mailer do
opts[:move_between_ids] = [issue1.id, issue2.id] opts[:move_between_ids] = [issue1.id, issue2.id]
expect(IssueRebalancingWorker).not_to receive(:perform_async).with(issue.id) expect(IssueRebalancingWorker).not_to receive(:perform_async)
update_issue(opts) update_issue(opts)
expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position) expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position)
...@@ -142,7 +142,7 @@ RSpec.describe Issues::UpdateService, :mailer do ...@@ -142,7 +142,7 @@ RSpec.describe Issues::UpdateService, :mailer do
opts[:move_between_ids] = [issue1.id, issue2.id] opts[:move_between_ids] = [issue1.id, issue2.id]
expect(IssueRebalancingWorker).to receive(:perform_async).with(issue.id) expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, project.id)
update_issue(opts) update_issue(opts)
expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position) expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position)
...@@ -156,7 +156,7 @@ RSpec.describe Issues::UpdateService, :mailer do ...@@ -156,7 +156,7 @@ RSpec.describe Issues::UpdateService, :mailer do
opts[:move_between_ids] = [issue1.id, issue2.id] opts[:move_between_ids] = [issue1.id, issue2.id]
expect(IssueRebalancingWorker).to receive(:perform_async).with(issue.id) expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, project.id)
update_issue(opts) update_issue(opts)
expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position) expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position)
...@@ -170,7 +170,7 @@ RSpec.describe Issues::UpdateService, :mailer do ...@@ -170,7 +170,7 @@ RSpec.describe Issues::UpdateService, :mailer do
opts[:move_between_ids] = [issue1.id, issue2.id] opts[:move_between_ids] = [issue1.id, issue2.id]
expect(IssueRebalancingWorker).to receive(:perform_async).with(issue.id) expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, project.id)
update_issue(opts) update_issue(opts)
expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position) expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position)
......
...@@ -115,6 +115,7 @@ RSpec.configure do |config| ...@@ -115,6 +115,7 @@ RSpec.configure do |config|
config.include StubExperiments config.include StubExperiments
config.include StubGitlabCalls config.include StubGitlabCalls
config.include StubGitlabData config.include StubGitlabData
config.include NextFoundInstanceOf
config.include NextInstanceOf config.include NextInstanceOf
config.include TestEnv config.include TestEnv
config.include Devise::Test::ControllerHelpers, type: :controller config.include Devise::Test::ControllerHelpers, type: :controller
......
# frozen_string_literal: true
module NextFoundInstanceOf
ERROR_MESSAGE = 'NextFoundInstanceOf mock helpers can only be used with ActiveRecord targets'
def expect_next_found_instance_of(klass)
check_if_active_record!(klass)
stub_allocate(expect(klass)) do |expectation|
yield(expectation)
end
end
def allow_next_found_instance_of(klass)
check_if_active_record!(klass)
stub_allocate(allow(klass)) do |allowance|
yield(allowance)
end
end
private
def check_if_active_record!(klass)
raise ArgumentError.new(ERROR_MESSAGE) unless klass < ActiveRecord::Base
end
def stub_allocate(target)
target.to receive(:allocate).and_wrap_original do |method|
method.call.tap { |allocation| yield(allocation) }
end
end
end
...@@ -10,23 +10,30 @@ RSpec.describe IssueRebalancingWorker do ...@@ -10,23 +10,30 @@ RSpec.describe IssueRebalancingWorker do
service = double(execute: nil) service = double(execute: nil)
expect(IssueRebalancingService).to receive(:new).with(issue).and_return(service) expect(IssueRebalancingService).to receive(:new).with(issue).and_return(service)
described_class.new.perform(issue.id) described_class.new.perform(nil, issue.project_id)
end end
it 'anticipates the inability to find the issue' do it 'anticipates the inability to find the issue' do
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(ActiveRecord::RecordNotFound, include(issue_id: -1)) expect(Gitlab::ErrorTracking).to receive(:log_exception).with(ActiveRecord::RecordNotFound, include(project_id: -1))
expect(IssueRebalancingService).not_to receive(:new) expect(IssueRebalancingService).not_to receive(:new)
described_class.new.perform(-1) described_class.new.perform(nil, -1)
end end
it 'anticipates there being too many issues' do it 'anticipates there being too many issues' do
service = double service = double
allow(service).to receive(:execute) { raise IssueRebalancingService::TooManyIssues } allow(service).to receive(:execute) { raise IssueRebalancingService::TooManyIssues }
expect(IssueRebalancingService).to receive(:new).with(issue).and_return(service) expect(IssueRebalancingService).to receive(:new).with(issue).and_return(service)
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(IssueRebalancingService::TooManyIssues, include(issue_id: issue.id)) expect(Gitlab::ErrorTracking).to receive(:log_exception).with(IssueRebalancingService::TooManyIssues, include(project_id: issue.project_id))
described_class.new.perform(issue.id) described_class.new.perform(nil, issue.project_id)
end
it 'takes no action if the value is nil' do
expect(IssueRebalancingService).not_to receive(:new)
expect(Gitlab::ErrorTracking).not_to receive(:log_exception)
described_class.new.perform(nil, nil)
end end
end end
end end
...@@ -54,13 +54,9 @@ RSpec.describe NewNoteWorker do ...@@ -54,13 +54,9 @@ RSpec.describe NewNoteWorker do
let(:note) { create(:note) } let(:note) { create(:note) }
before do before do
# TODO: `allow_next_instance_of` helper method is not working allow_next_found_instance_of(Note) do |note|
# because ActiveRecord is directly calling `.allocate` on model allow(note).to receive(:skip_notification?).and_return(true)
# classes and bypasses the `.new` method call. end
# Fix the `allow_next_instance_of` helper and change these to mock
# the next instance of `Note` model class.
allow(Note).to receive(:find_by).with(id: note.id).and_return(note)
allow(note).to receive(:skip_notification?).and_return(true)
end end
it 'does not create a new note notification' do it 'does not create a new note notification' do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment