summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.rubocop.yml1
-rw-r--r--app/assets/javascripts/boards/components/board.js15
-rw-r--r--app/assets/javascripts/boards/components/board_column.vue384
-rw-r--r--app/assets/javascripts/boards/index.js11
-rw-r--r--app/assets/javascripts/dropzone_input.js13
-rw-r--r--app/assets/javascripts/lib/utils/file_upload.js11
-rw-r--r--app/assets/javascripts/notes/components/diff_with_note.vue2
-rw-r--r--app/controllers/groups/boards_controller.rb1
-rw-r--r--app/controllers/projects/boards_controller.rb1
-rw-r--r--app/helpers/container_expiration_policies_helper.rb2
-rw-r--r--app/models/ci/legacy_stage.rb2
-rw-r--r--app/models/ci/pipeline.rb2
-rw-r--r--app/models/ci/stage.rb2
-rw-r--r--app/models/commit_status.rb8
-rw-r--r--app/models/concerns/has_status.rb4
-rw-r--r--app/models/user.rb2
-rw-r--r--app/services/ci/pipeline_processing/legacy_processing_service.rb4
-rw-r--r--app/views/shared/boards/_show.html.haml6
-rw-r--r--app/views/shared/boards/components/_board.html.haml3
-rw-r--r--changelogs/unreleased/202634-upload-a-design-by-copy-pasting-the-file-into-the-design-tab.yml5
-rw-r--r--changelogs/unreleased/208268-enable-container-policies-by-default.yml5
-rw-r--r--changelogs/unreleased/refactor-service-spec.yml5
-rw-r--r--db/migrate/20200331220930_enable_container_expiration_policies_by_default.rb19
-rw-r--r--db/structure.sql3
-rw-r--r--doc/api/README.md18
-rw-r--r--doc/development/README.md4
-rw-r--r--doc/development/api_styleguide.md2
-rw-r--r--doc/development/database_review.md1
-rw-r--r--doc/development/migration_style_guide.md88
-rw-r--r--doc/install/aws/index.md25
-rw-r--r--doc/install/installation.md9
-rw-r--r--doc/user/project/issues/design_management.md13
-rw-r--r--lib/gitlab/sidekiq_middleware/metrics.rb8
-rwxr-xr-xlib/support/init.d/gitlab13
-rw-r--r--spec/features/projects/settings/registry_settings_spec.rb1
-rw-r--r--spec/frontend/__mocks__/sortablejs/index.js2
-rw-r--r--spec/frontend/boards/components/board_column_spec.js172
-rw-r--r--spec/frontend/boards/list_spec.js5
-rw-r--r--spec/frontend/boards/mock_data.js4
-rw-r--r--spec/frontend/lib/utils/file_upload_spec.js14
-rw-r--r--spec/frontend/notes/components/diff_discussion_header_spec.js2
-rw-r--r--spec/frontend/notes/components/diff_with_note_spec.js86
-rw-r--r--spec/frontend/notes/components/discussion_filter_spec.js219
-rw-r--r--spec/frontend/notes/components/discussion_resolve_with_issue_button_spec.js (renamed from spec/javascripts/notes/components/discussion_resolve_with_issue_button_spec.js)0
-rw-r--r--spec/frontend/notes/components/note_actions/reply_button_spec.js (renamed from spec/javascripts/notes/components/note_actions/reply_button_spec.js)0
-rw-r--r--spec/frontend/notes/components/note_actions_spec.js (renamed from spec/javascripts/notes/components/note_actions_spec.js)0
-rw-r--r--spec/frontend/notes/components/note_awards_list_spec.js (renamed from spec/javascripts/notes/components/note_awards_list_spec.js)19
-rw-r--r--spec/frontend/notes/components/note_body_spec.js (renamed from spec/javascripts/notes/components/note_body_spec.js)0
-rw-r--r--spec/frontend/notes/components/note_form_spec.js (renamed from spec/javascripts/notes/components/note_form_spec.js)105
-rw-r--r--spec/frontend/notes/components/note_signed_out_widget_spec.js (renamed from spec/javascripts/notes/components/note_signed_out_widget_spec.js)0
-rw-r--r--spec/frontend/notes/components/noteable_discussion_spec.js (renamed from spec/javascripts/notes/components/noteable_discussion_spec.js)32
-rw-r--r--spec/frontend/notes/components/noteable_note_spec.js (renamed from spec/javascripts/notes/components/noteable_note_spec.js)6
-rw-r--r--spec/frontend/notes/components/toggle_replies_widget_spec.js (renamed from spec/javascripts/notes/components/toggle_replies_widget_spec.js)6
-rw-r--r--spec/frontend/notes/stores/collapse_utils_spec.js (renamed from spec/javascripts/notes/stores/collapse_utils_spec.js)0
-rw-r--r--spec/helpers/container_expiration_policies_helper_spec.rb4
-rw-r--r--spec/javascripts/helpers/init_vue_mr_page_helper.js2
-rw-r--r--spec/javascripts/notes/components/diff_with_note_spec.js89
-rw-r--r--spec/javascripts/notes/components/discussion_filter_spec.js187
-rw-r--r--spec/javascripts/notes/helpers.js1
-rw-r--r--spec/javascripts/notes/mock_data.js1
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/client_metrics_spec.rb9
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb9
-rw-r--r--spec/models/ci/pipeline_spec.rb2
-rw-r--r--spec/models/commit_status_spec.rb6
-rw-r--r--spec/models/concerns/has_status_spec.rb2
-rw-r--r--spec/models/user_spec.rb6
66 files changed, 1240 insertions, 443 deletions
diff --git a/.rubocop.yml b/.rubocop.yml
index 7474c87dafe..1418a72c88a 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -409,5 +409,4 @@ RSpec/RepeatedExample:
- 'spec/rubocop/cop/migration/update_large_table_spec.rb'
- 'spec/services/notification_service_spec.rb'
- 'spec/services/web_hook_service_spec.rb'
- - 'ee/spec/services/boards/lists/update_service_spec.rb'
- 'ee/spec/services/geo/repository_verification_primary_service_spec.rb'
diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js
index b68a6ad0ef5..48f5614cefe 100644
--- a/app/assets/javascripts/boards/components/board.js
+++ b/app/assets/javascripts/boards/components/board.js
@@ -16,6 +16,14 @@ import { getBoardSortableDefaultOptions, sortableEnd } from '../mixins/sortable_
import { ListType } from '../constants';
import { isScopedLabel } from '~/lib/utils/common_utils';
+/**
+ * Please don't edit this file, have a look at:
+ * ./board_column.vue
+ * https://gitlab.com/gitlab-org/gitlab/-/issues/212300
+ *
+ * This file here will be deleted soon
+ * @deprecated
+ */
export default Vue.extend({
components: {
BoardBlankState,
@@ -54,6 +62,13 @@ export default Vue.extend({
type: String,
required: true,
},
+ // Does not do anything but is used
+ // to support the API of the new board_column.vue
+ canAdminList: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
diff --git a/app/assets/javascripts/boards/components/board_column.vue b/app/assets/javascripts/boards/components/board_column.vue
new file mode 100644
index 00000000000..693b1f9d7b1
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_column.vue
@@ -0,0 +1,384 @@
+<script>
+import $ from 'jquery';
+import Sortable from 'sortablejs';
+import { GlButtonGroup, GlButton, GlLabel, GlTooltip, GlIcon } from '@gitlab/ui';
+import isWipLimitsOn from 'ee_else_ce/boards/mixins/is_wip_limits';
+import { s__, __, sprintf } from '~/locale';
+import Tooltip from '~/vue_shared/directives/tooltip';
+import EmptyComponent from '~/vue_shared/components/empty_component';
+import AccessorUtilities from '../../lib/utils/accessor';
+import BoardBlankState from './board_blank_state.vue';
+import BoardDelete from './board_delete';
+import BoardList from './board_list.vue';
+import IssueCount from './issue_count.vue';
+import boardsStore from '../stores/boards_store';
+import { getBoardSortableDefaultOptions, sortableEnd } from '../mixins/sortable_default_options';
+import { ListType } from '../constants';
+import { isScopedLabel } from '~/lib/utils/common_utils';
+
+export default {
+ components: {
+ BoardPromotionState: EmptyComponent,
+ BoardBlankState,
+ BoardDelete,
+ BoardList,
+ GlButtonGroup,
+ IssueCount,
+ GlButton,
+ GlLabel,
+ GlTooltip,
+ GlIcon,
+ },
+ directives: {
+ Tooltip,
+ },
+ mixins: [isWipLimitsOn],
+ props: {
+ list: {
+ type: Object,
+ default: () => ({}),
+ required: false,
+ },
+ disabled: {
+ type: Boolean,
+ required: true,
+ },
+ issueLinkBase: {
+ type: String,
+ required: true,
+ },
+ rootPath: {
+ type: String,
+ required: true,
+ },
+ boardId: {
+ type: String,
+ required: true,
+ },
+ canAdminList: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ groupId: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+ },
+ data() {
+ return {
+ detailIssue: boardsStore.detail,
+ filter: boardsStore.filter,
+ weightFeatureAvailable: false,
+ };
+ },
+ computed: {
+ isLoggedIn() {
+ return Boolean(gon.current_user_id);
+ },
+ showListHeaderButton() {
+ return (
+ !this.disabled &&
+ this.list.type !== ListType.closed &&
+ this.list.type !== ListType.blank &&
+ this.list.type !== ListType.promotion
+ );
+ },
+ issuesTooltip() {
+ const { issuesSize } = this.list;
+
+ return sprintf(__('%{issuesSize} issues'), { issuesSize });
+ },
+ // Only needed to make karma pass.
+ weightCountToolTip() {}, // eslint-disable-line vue/return-in-computed-property
+ caretTooltip() {
+ return this.list.isExpanded ? s__('Boards|Collapse') : s__('Boards|Expand');
+ },
+ isNewIssueShown() {
+ return this.list.type === ListType.backlog || this.showListHeaderButton;
+ },
+ isSettingsShown() {
+ return (
+ this.list.type !== ListType.backlog &&
+ this.showListHeaderButton &&
+ this.list.isExpanded &&
+ this.isWipLimitsOn
+ );
+ },
+ showBoardListAndBoardInfo() {
+ return this.list.type !== ListType.blank && this.list.type !== ListType.promotion;
+ },
+ uniqueKey() {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ return `boards.${this.boardId}.${this.list.type}.${this.list.id}`;
+ },
+ helpLink() {
+ return boardsStore.scopedLabels.helpLink;
+ },
+ },
+ watch: {
+ filter: {
+ handler() {
+ this.list.page = 1;
+ this.list.getIssues(true).catch(() => {
+ // TODO: handle request error
+ });
+ },
+ deep: true,
+ },
+ },
+ mounted() {
+ const instance = this;
+
+ const sortableOptions = getBoardSortableDefaultOptions({
+ disabled: this.disabled,
+ group: 'boards',
+ draggable: '.is-draggable',
+ handle: '.js-board-handle',
+ onEnd(e) {
+ sortableEnd();
+
+ const sortable = this;
+
+ if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) {
+ const order = sortable.toArray();
+ const list = boardsStore.findList('id', parseInt(e.item.dataset.id, 10));
+
+ instance.$nextTick(() => {
+ boardsStore.moveList(list, order);
+ });
+ }
+ },
+ });
+
+ Sortable.create(this.$el.parentNode, sortableOptions);
+ },
+ created() {
+ if (
+ this.list.isExpandable &&
+ AccessorUtilities.isLocalStorageAccessSafe() &&
+ !this.isLoggedIn
+ ) {
+ const isCollapsed = localStorage.getItem(`${this.uniqueKey}.expanded`) === 'false';
+
+ this.list.isExpanded = !isCollapsed;
+ }
+ },
+ methods: {
+ showScopedLabels(label) {
+ return boardsStore.scopedLabels.enabled && isScopedLabel(label);
+ },
+
+ showNewIssueForm() {
+ this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm;
+ },
+ toggleExpanded() {
+ if (this.list.isExpandable) {
+ this.list.isExpanded = !this.list.isExpanded;
+
+ if (AccessorUtilities.isLocalStorageAccessSafe() && !this.isLoggedIn) {
+ localStorage.setItem(`${this.uniqueKey}.expanded`, this.list.isExpanded);
+ }
+
+ if (this.isLoggedIn) {
+ this.list.update();
+ }
+
+ // When expanding/collapsing, the tooltip on the caret button sometimes stays open.
+ // Close all tooltips manually to prevent dangling tooltips.
+ $('.tooltip').tooltip('hide');
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ :class="{
+ 'is-draggable': !list.preset,
+ 'is-expandable': list.isExpandable,
+ 'is-collapsed': !list.isExpanded,
+ 'board-type-assignee': list.type === 'assignee',
+ }"
+ :data-id="list.id"
+ class="board h-100 px-2 align-top ws-normal"
+ data-qa-selector="board_list"
+ >
+ <div class="board-inner d-flex flex-column position-relative h-100 rounded">
+ <header
+ :class="{
+ 'has-border': list.label && list.label.color,
+ 'position-relative': list.isExpanded,
+ 'position-absolute position-top-0 position-left-0 w-100 h-100': !list.isExpanded,
+ }"
+ :style="{ borderTopColor: list.label && list.label.color ? list.label.color : null }"
+ class="board-header"
+ data-qa-selector="board_list_header"
+ >
+ <h3
+ :class="{
+ 'user-can-drag': !disabled && !list.preset,
+ 'border-bottom-0': !list.isExpanded,
+ }"
+ class="board-title m-0 d-flex js-board-handle"
+ >
+ <div
+ v-if="list.isExpandable"
+ v-tooltip=""
+ :aria-label="caretTooltip"
+ :title="caretTooltip"
+ aria-hidden="true"
+ class="board-title-caret no-drag"
+ data-placement="bottom"
+ @click="toggleExpanded"
+ >
+ <i
+ :class="{ 'fa-caret-right': list.isExpanded, 'fa-caret-down': !list.isExpanded }"
+ class="fa fa-fw"
+ ></i>
+ </div>
+ <!-- The following is only true in EE and if it is a milestone -->
+ <span
+ v-if="list.type === 'milestone' && list.milestone"
+ aria-hidden="true"
+ class="append-right-5 milestone-icon"
+ >
+ <gl-icon name="timer" />
+ </span>
+
+ <a
+ v-if="list.type === 'assignee'"
+ :href="list.assignee.path"
+ class="user-avatar-link js-no-trigger"
+ >
+ <img
+ :alt="list.assignee.name"
+ :src="list.assignee.avatar"
+ class="avatar s20 has-tooltip"
+ height="20"
+ width="20"
+ />
+ </a>
+ <div class="board-title-text">
+ <span
+ v-if="list.type !== 'label'"
+ :class="{
+ 'has-tooltip': !['backlog', 'closed'].includes(list.type),
+ 'd-block': list.type === 'milestone',
+ }"
+ :title="(list.label && list.label.description) || list.title || ''"
+ class="board-title-main-text block-truncated"
+ data-container="body"
+ >
+ {{ list.title }}
+ </span>
+ <span
+ v-if="list.type === 'assignee'"
+ :title="(list.assignee && list.assignee.username) || ''"
+ class="board-title-sub-text prepend-left-5 has-tooltip"
+ >
+ @{{ list.assignee.username }}
+ </span>
+ <gl-label
+ v-if="list.type === 'label'"
+ :background-color="list.label.color"
+ :description="list.label.description"
+ :scoped="showScopedLabels(list.label)"
+ :scoped-labels-documentation-link="helpLink"
+ :size="!list.isExpanded ? 'sm' : ''"
+ :title="list.label.title"
+ tooltip-placement="bottom"
+ />
+ </div>
+ <board-delete
+ v-if="canAdminList && !list.preset && list.id"
+ :list="list"
+ inline-template="true"
+ >
+ <button
+ :class="{ 'd-none': !list.isExpanded }"
+ :aria-label="__(`Delete list`)"
+ class="board-delete no-drag p-0 border-0 has-tooltip float-right"
+ data-placement="bottom"
+ title="Delete list"
+ type="button"
+ @click.stop="deleteBoard"
+ >
+ <i aria-hidden="true" data-hidden="true" class="fa fa-trash"></i>
+ </button>
+ </board-delete>
+ <div
+ v-if="showBoardListAndBoardInfo"
+ class="issue-count-badge pr-0 no-drag text-secondary"
+ >
+ <span class="d-inline-flex">
+ <gl-tooltip :target="() => $refs.issueCount" :title="issuesTooltip" />
+ <span ref="issueCount" class="issue-count-badge-count">
+ <gl-icon class="mr-1" name="issues" />
+ <issue-count :issues-size="list.issuesSize" :max-issue-count="list.maxIssueCount" />
+ </span>
+ <!-- The following is only true in EE. -->
+ <template v-if="weightFeatureAvailable">
+ <gl-tooltip :target="() => $refs.weightTooltip" :title="weightCountToolTip" />
+ <span ref="weightTooltip" class="d-inline-flex ml-2">
+ <gl-icon class="mr-1" name="weight" />
+ {{ list.totalWeight }}
+ </span>
+ </template>
+ </span>
+ </div>
+ <gl-button-group
+ v-if="isNewIssueShown || isSettingsShown"
+ class="board-list-button-group pl-2"
+ >
+ <gl-button
+ v-if="isNewIssueShown"
+ ref="newIssueBtn"
+ :class="{
+ 'd-none': !list.isExpanded,
+ 'rounded-right': isNewIssueShown && !isSettingsShown,
+ }"
+ :aria-label="__(`New issue`)"
+ class="issue-count-badge-add-button no-drag"
+ type="button"
+ @click="showNewIssueForm"
+ >
+ <i aria-hidden="true" data-hidden="true" class="fa fa-plus"></i>
+ </gl-button>
+ <gl-tooltip :target="() => $refs.newIssueBtn">{{ __('New Issue') }}</gl-tooltip>
+
+ <gl-button
+ v-if="isSettingsShown"
+ ref="settingsBtn"
+ :aria-label="__(`List settings`)"
+ class="no-drag rounded-right"
+ title="List settings"
+ type="button"
+ @click="openSidebarSettings"
+ >
+ <gl-icon name="settings" />
+ </gl-button>
+ <gl-tooltip :target="() => $refs.settingsBtn">{{ __('List settings') }}</gl-tooltip>
+ </gl-button-group>
+ </h3>
+ </header>
+ <board-list
+ v-if="showBoardListAndBoardInfo"
+ ref="board-list"
+ :disabled="disabled"
+ :group-id="groupId || null"
+ :issue-link-base="issueLinkBase"
+ :issues="list.issues"
+ :list="list"
+ :loading="list.loading"
+ :root-path="rootPath"
+ />
+ <board-blank-state v-if="canAdminList && list.id === 'blank'" />
+
+ <!-- Will be only available in EE -->
+ <board-promotion-state v-if="list.id === 'promotion'" />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index 781cb0c1cc9..a12db7a5f1a 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -3,7 +3,6 @@ import Vue from 'vue';
import 'ee_else_ce/boards/models/issue';
import 'ee_else_ce/boards/models/list';
-import Board from 'ee_else_ce/boards/components/board';
import BoardSidebar from 'ee_else_ce/boards/components/board_sidebar';
import initNewListDropdown from 'ee_else_ce/boards/components/new_list_dropdown';
import boardConfigToggle from 'ee_else_ce/boards/config_toggle';
@@ -65,7 +64,15 @@ export default () => {
issueBoardsApp = new Vue({
el: $boardApp,
components: {
- Board,
+ Board: () =>
+ window?.gon?.features?.sfcIssueBoards
+ ? import('ee_else_ce/boards/components/board_column.vue')
+ : /**
+ * Please have a look at, we are moving to the SFC soon:
+ * https://gitlab.com/gitlab-org/gitlab/-/issues/212300
+ * @deprecated
+ */
+ import('ee_else_ce/boards/components/board'),
BoardSidebar,
BoardAddIssuesModal,
BoardSettingsSidebar: () =>
diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js
index 0e2dd59092a..6c158ad8990 100644
--- a/app/assets/javascripts/dropzone_input.js
+++ b/app/assets/javascripts/dropzone_input.js
@@ -6,6 +6,7 @@ import PasteMarkdownTable from './behaviors/markdown/paste_markdown_table';
import csrf from './lib/utils/csrf';
import axios from './lib/utils/axios_utils';
import { n__, __ } from '~/locale';
+import { getFilename } from '~/lib/utils/file_upload';
Dropzone.autoDiscover = false;
@@ -41,7 +42,6 @@ export default function dropzoneInput(form) {
let addFileToForm;
let updateAttachingMessage;
let isImage;
- let getFilename;
let uploadFile;
formTextarea.wrap('<div class="div-dropzone"></div>');
@@ -235,17 +235,6 @@ export default function dropzoneInput(form) {
$(form).append(`<input type="hidden" name="files[]" value="${_.escape(path)}">`);
};
- getFilename = e => {
- let value;
- if (window.clipboardData && window.clipboardData.getData) {
- value = window.clipboardData.getData('Text');
- } else if (e.clipboardData && e.clipboardData.getData) {
- value = e.clipboardData.getData('text/plain');
- }
- value = value.split('\r');
- return value[0];
- };
-
const showSpinner = () => $uploadingProgressContainer.removeClass('hide');
const closeSpinner = () => $uploadingProgressContainer.addClass('hide');
diff --git a/app/assets/javascripts/lib/utils/file_upload.js b/app/assets/javascripts/lib/utils/file_upload.js
index 82ee83e4348..b8b63bf58d4 100644
--- a/app/assets/javascripts/lib/utils/file_upload.js
+++ b/app/assets/javascripts/lib/utils/file_upload.js
@@ -14,3 +14,14 @@ export default (buttonSelector, fileSelector) => {
form.querySelector('.js-filename').textContent = fileInput.value.replace(/^.*[\\\/]/, ''); // eslint-disable-line no-useless-escape
});
};
+
+export const getFilename = ({ clipboardData }) => {
+ let value;
+ if (window.clipboardData && window.clipboardData.getData) {
+ value = window.clipboardData.getData('Text');
+ } else if (clipboardData && clipboardData.getData) {
+ value = clipboardData.getData('text/plain');
+ }
+ value = value.split('\r');
+ return value[0];
+};
diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue
index c3915ef299b..a58a040fb4e 100644
--- a/app/assets/javascripts/notes/components/diff_with_note.vue
+++ b/app/assets/javascripts/notes/components/diff_with_note.vue
@@ -48,7 +48,7 @@ export default {
},
},
mounted() {
- if (!this.hasTruncatedDiffLines) {
+ if (this.isTextFile && !this.hasTruncatedDiffLines) {
this.fetchDiff();
}
},
diff --git a/app/controllers/groups/boards_controller.rb b/app/controllers/groups/boards_controller.rb
index fab84fb8299..a9bd24890ee 100644
--- a/app/controllers/groups/boards_controller.rb
+++ b/app/controllers/groups/boards_controller.rb
@@ -8,6 +8,7 @@ class Groups::BoardsController < Groups::ApplicationController
before_action :assign_endpoint_vars
before_action do
push_frontend_feature_flag(:multi_select_board, default_enabled: true)
+ push_frontend_feature_flag(:sfc_issue_boards, default_enabled: true)
end
private
diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb
index db05da0bb7f..8fa823e0be1 100644
--- a/app/controllers/projects/boards_controller.rb
+++ b/app/controllers/projects/boards_controller.rb
@@ -9,6 +9,7 @@ class Projects::BoardsController < Projects::ApplicationController
before_action :assign_endpoint_vars
before_action do
push_frontend_feature_flag(:multi_select_board, default_enabled: true)
+ push_frontend_feature_flag(:sfc_issue_boards, default_enabled: true)
end
private
diff --git a/app/helpers/container_expiration_policies_helper.rb b/app/helpers/container_expiration_policies_helper.rb
index 5fb7b5afa6e..cc6d717ce35 100644
--- a/app/helpers/container_expiration_policies_helper.rb
+++ b/app/helpers/container_expiration_policies_helper.rb
@@ -20,7 +20,7 @@ module ContainerExpirationPoliciesHelper
def older_than_options
ContainerExpirationPolicy.older_than_options.map do |key, val|
{ key: key.to_s, label: val }.tap do |base|
- base[:default] = true if key.to_s == '30d'
+ base[:default] = true if key.to_s == '90d'
end
end
end
diff --git a/app/models/ci/legacy_stage.rb b/app/models/ci/legacy_stage.rb
index 0a67a652e22..9ca5cf13907 100644
--- a/app/models/ci/legacy_stage.rb
+++ b/app/models/ci/legacy_stage.rb
@@ -32,7 +32,7 @@ module Ci
end
def status
- @status ||= statuses.latest.slow_composite_status
+ @status ||= statuses.latest.slow_composite_status(project: project)
end
def detailed_status(current_user)
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 4e9be86d3a9..3ce44a066ae 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -968,7 +968,7 @@ module Ci
def latest_builds_status
return 'failed' unless yaml_errors.blank?
- statuses.latest.slow_composite_status || 'skipped'
+ statuses.latest.slow_composite_status(project: project) || 'skipped'
end
def keep_around_commits
diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb
index 75f73429c2a..e6c34f3df03 100644
--- a/app/models/ci/stage.rb
+++ b/app/models/ci/stage.rb
@@ -138,7 +138,7 @@ module Ci
end
def latest_stage_status
- statuses.latest.slow_composite_status || 'skipped'
+ statuses.latest.slow_composite_status(project: project) || 'skipped'
end
end
end
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 03260b28335..046f131b041 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -178,12 +178,12 @@ class CommitStatus < ApplicationRecord
select(:name)
end
- def self.status_for_prior_stages(index)
- before_stage(index).latest.slow_composite_status || 'success'
+ def self.status_for_prior_stages(index, project:)
+ before_stage(index).latest.slow_composite_status(project: project) || 'success'
end
- def self.status_for_names(names)
- where(name: names).latest.slow_composite_status || 'success'
+ def self.status_for_names(names, project:)
+ where(name: names).latest.slow_composite_status(project: project) || 'success'
end
def self.update_as_processed!
diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb
index e06dad38c32..b80f8c2bbb2 100644
--- a/app/models/concerns/has_status.rb
+++ b/app/models/concerns/has_status.rb
@@ -65,8 +65,8 @@ module HasStatus
# This method performs expensive calculation of status:
# 1. By plucking all related objects,
# 2. Or executes expensive SQL query
- def slow_composite_status
- if Feature.enabled?(:ci_composite_status, default_enabled: false)
+ def slow_composite_status(project:)
+ if Feature.enabled?(:ci_composite_status, project, default_enabled: false)
Gitlab::Ci::Status::Composite
.new(all, with_allow_failure: columns_hash.key?('allow_failure'))
.status
diff --git a/app/models/user.rb b/app/models/user.rb
index e18d642a155..4d450f9305f 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -1689,7 +1689,7 @@ class User < ApplicationRecord
def gitlab_employee?
strong_memoize(:gitlab_employee) do
if Gitlab.com?
- Mail::Address.new(email).domain == "gitlab.com"
+ Mail::Address.new(email).domain == "gitlab.com" && confirmed?
else
false
end
diff --git a/app/services/ci/pipeline_processing/legacy_processing_service.rb b/app/services/ci/pipeline_processing/legacy_processing_service.rb
index 278fba20283..8d7b80282fc 100644
--- a/app/services/ci/pipeline_processing/legacy_processing_service.rb
+++ b/app/services/ci/pipeline_processing/legacy_processing_service.rb
@@ -89,11 +89,11 @@ module Ci
end
def status_for_prior_stages(index)
- pipeline.processables.status_for_prior_stages(index)
+ pipeline.processables.status_for_prior_stages(index, project: pipeline.project)
end
def status_for_build_needs(needs)
- pipeline.processables.status_for_names(needs)
+ pipeline.processables.status_for_names(needs, project: pipeline.project)
end
# rubocop: disable CodeReuse/ActiveRecord
diff --git a/app/views/shared/boards/_show.html.haml b/app/views/shared/boards/_show.html.haml
index 7f62b983bfc..cf42ac3dd37 100644
--- a/app/views/shared/boards/_show.html.haml
+++ b/app/views/shared/boards/_show.html.haml
@@ -1,5 +1,9 @@
- board = local_assigns.fetch(:board, nil)
- group = local_assigns.fetch(:group, false)
+-# TODO: Move group_id and can_admin_list to the board store
+ See: https://gitlab.com/gitlab-org/gitlab/-/issues/213082
+- group_id = @group&.id || "null"
+- can_admin_list = can?(current_user, :admin_list, current_board_parent) == true
- @no_breadcrumb_container = true
- @no_container = true
- @content_class = "issue-boards-content js-focus-mode-board"
@@ -22,6 +26,8 @@
%board{ "v-cloak" => "true",
"v-for" => "list in state.lists",
"ref" => "board",
+ ":can-admin-list" => can_admin_list,
+ ":group-id" => group_id,
":list" => "list",
":disabled" => "disabled",
":issue-link-base" => "issueLinkBase",
diff --git a/app/views/shared/boards/components/_board.html.haml b/app/views/shared/boards/components/_board.html.haml
index e42d8650708..f3f2c09ea61 100644
--- a/app/views/shared/boards/components/_board.html.haml
+++ b/app/views/shared/boards/components/_board.html.haml
@@ -1,3 +1,6 @@
+-# Please have a look at app/assets/javascripts/boards/components/board_column.vue
+ This haml file is deprecated and will be deleted soon, please change the Vue app
+ https://gitlab.com/gitlab-org/gitlab/-/issues/212300
.board.h-100.px-2.align-top.ws-normal{ ":class" => '{ "is-draggable": !list.preset, "is-expandable": list.isExpandable, "is-collapsed": !list.isExpanded, "board-type-assignee": list.type === "assignee" }',
":data-id" => "list.id", data: { qa_selector: "board_list" } }
.board-inner.d-flex.flex-column.position-relative.h-100.rounded
diff --git a/changelogs/unreleased/202634-upload-a-design-by-copy-pasting-the-file-into-the-design-tab.yml b/changelogs/unreleased/202634-upload-a-design-by-copy-pasting-the-file-into-the-design-tab.yml
new file mode 100644
index 00000000000..834bb02c284
--- /dev/null
+++ b/changelogs/unreleased/202634-upload-a-design-by-copy-pasting-the-file-into-the-design-tab.yml
@@ -0,0 +1,5 @@
+---
+title: Upload a design by copy/pasting the file into the Design Tab
+merge_request: 27776
+author:
+type: added
diff --git a/changelogs/unreleased/208268-enable-container-policies-by-default.yml b/changelogs/unreleased/208268-enable-container-policies-by-default.yml
new file mode 100644
index 00000000000..eaf796a047b
--- /dev/null
+++ b/changelogs/unreleased/208268-enable-container-policies-by-default.yml
@@ -0,0 +1,5 @@
+---
+title: Enable container expiration policies by default for new projects
+merge_request: 28480
+author:
+type: changed
diff --git a/changelogs/unreleased/refactor-service-spec.yml b/changelogs/unreleased/refactor-service-spec.yml
new file mode 100644
index 00000000000..ded09385fc5
--- /dev/null
+++ b/changelogs/unreleased/refactor-service-spec.yml
@@ -0,0 +1,5 @@
+---
+title: Remove duplicate specs in update service spec
+merge_request: 28650
+author: Rajendra Kadam
+type: added
diff --git a/db/migrate/20200331220930_enable_container_expiration_policies_by_default.rb b/db/migrate/20200331220930_enable_container_expiration_policies_by_default.rb
new file mode 100644
index 00000000000..46393416eb7
--- /dev/null
+++ b/db/migrate/20200331220930_enable_container_expiration_policies_by_default.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class EnableContainerExpirationPoliciesByDefault < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ with_lock_retries do
+ change_column_default :container_expiration_policies, :enabled, true
+ end
+ end
+
+ def down
+ with_lock_retries do
+ change_column_default :container_expiration_policies, :enabled, false
+ end
+ end
+end
diff --git a/db/structure.sql b/db/structure.sql
index bbd6312b08a..0d25712e0b4 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -1842,7 +1842,7 @@ CREATE TABLE public.container_expiration_policies (
cadence character varying(12) DEFAULT '7d'::character varying NOT NULL,
older_than character varying(12),
keep_n integer,
- enabled boolean DEFAULT false NOT NULL
+ enabled boolean DEFAULT true NOT NULL
);
CREATE TABLE public.container_repositories (
@@ -12926,5 +12926,6 @@ COPY "schema_migrations" (version) FROM STDIN;
20200326145443
20200330074719
20200330132913
+20200331220930
\.
diff --git a/doc/api/README.md b/doc/api/README.md
index 319a697b082..24b81852dc5 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -436,6 +436,24 @@ Keyset-based pagination is only supported for selected resources and ordering op
| ------------------------- | -------------------------- |
| [Projects](projects.md) | `order_by=id` only |
+## Path parameters
+
+If an endpoint has path parameters, the documentation shows them with a preceding colon.
+
+For example:
+
+```plaintext
+DELETE /projects/:id/share/:group_id
+```
+
+The `:id` path parameter needs to be replaced with the project id, and the `:group_id` needs to be replaced with the id of the group. The colons `:` should not be included.
+
+The resulting cURL call for a project with id `5` and a group id of `17` is then:
+
+```shell
+curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/share/17
+```
+
## Namespaced path encoding
If using namespaced API calls, make sure that the `NAMESPACE/PROJECT_PATH` is
diff --git a/doc/development/README.md b/doc/development/README.md
index 2089cf794ba..e55989e312a 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -98,6 +98,8 @@ Complementary reads:
- [Application limits](application_limits.md)
- [Redis guidelines](redis.md)
- [Rails initializers](rails_initializers.md)
+- [Code comments](code_comments.md)
+- [Renaming features](renaming_features.md)
## Performance guides
@@ -150,9 +152,7 @@ Complementary reads:
- [Verifying database capabilities](verifying_database_capabilities.md)
- [Database Debugging and Troubleshooting](database_debugging.md)
- [Query Count Limits](query_count_limits.md)
-- [Code comments](code_comments.md)
- [Creating enums](creating_enums.md)
-- [Renaming features](renaming_features.md)
### Case studies
diff --git a/doc/development/api_styleguide.md b/doc/development/api_styleguide.md
index 7c0146017b1..37d8a677389 100644
--- a/doc/development/api_styleguide.md
+++ b/doc/development/api_styleguide.md
@@ -121,7 +121,7 @@ For instance:
The [internal API](./internal_api.md) is documented for internal use. Please keep it up to date so we know what endpoints
different components are making use of.
-[Entity]: https://gitlab.com/gitlab-org/gitlab/blob/master/lib/api/entities.rb
+[Entity]: https://gitlab.com/gitlab-org/gitlab/blob/master/lib/api/entities
[validation, and coercion of the parameters]: https://github.com/ruby-grape/grape#parameter-validation-and-coercion
[installing GitLab under a relative URL]: https://docs.gitlab.com/ee/install/relative_url.html
diff --git a/doc/development/database_review.md b/doc/development/database_review.md
index 0fdf255e266..650c6fc7936 100644
--- a/doc/development/database_review.md
+++ b/doc/development/database_review.md
@@ -79,6 +79,7 @@ the following preparations into account.
- Include either a rollback procedure or describe how to rollback changes.
- Add the output of the migration(s) to the MR description.
- Add tests for the migration in `spec/migrations` if necessary. See [Testing Rails migrations at GitLab](testing_guide/testing_migrations_guide.md) for more details.
+- When [high-traffic](https://gitlab.com/gitlab-org/gitlab/-/blob/master/rubocop/migration_helpers.rb#L12) tables are involved in the migration, use the [`with_lock_retries`](migration_style_guide.md#retry-mechanism-when-acquiring-database-locks) helper method. Review the relevant [examples in our documentation](migration_style_guide.md#examples) for use cases and solutions.
#### Preparation when adding or modifying queries
diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md
index 46ea91fcdf3..3e993243855 100644
--- a/doc/development/migration_style_guide.md
+++ b/doc/development/migration_style_guide.md
@@ -171,7 +171,7 @@ lock allow the database to process other statements.
### Examples
-Removing a column:
+**Removing a column:**
```ruby
include Gitlab::Database::MigrationHelpers
@@ -189,7 +189,7 @@ def down
end
```
-Removing a foreign key:
+**Removing a foreign key:**
```ruby
include Gitlab::Database::MigrationHelpers
@@ -207,7 +207,7 @@ def down
end
```
-Changing default value for a column:
+**Changing default value for a column:**
```ruby
include Gitlab::Database::MigrationHelpers
@@ -225,6 +225,88 @@ def down
end
```
+**Creating a new table with a foreign key:**
+
+We can simply wrap the `create_table` method with `with_lock_retries`:
+
+```ruby
+def up
+ with_lock_retries do
+ create_table :issues do |t|
+ t.references :project, index: true, null: false, foreign_key: { on_delete: :cascade }
+ t.string :title, limit: 255
+ end
+ end
+end
+
+def down
+ drop_table :issues
+end
+```
+
+**Creating a new table when we have two foreign keys:**
+
+For this, we'll need three migrations:
+
+1. Creating the table without foreign keys (with the indices).
+1. Add foreign key to the first table.
+1. Add foreign key to the second table.
+
+Creating the table:
+
+```ruby
+def up
+ create_table :imports do |t|
+ t.bigint :project_id, null: false
+ t.bigint :user_id, null: false
+ t.string :jid, limit: 255
+ end
+
+ add_index :imports, :project_id
+ add_index :imports, :user_id
+end
+
+def down
+ drop_table :imports
+end
+```
+
+Adding foreign key to `projects`:
+
+```ruby
+include Gitlab::Database::MigrationHelpers
+
+def up
+ with_lock_retries do
+ add_foreign_key :imports, :projects, column: :project_id, on_delete: :cascade
+ end
+end
+
+def down
+ with_lock_retries do
+ remove_foreign_key :imports, column: :project_id
+ end
+end
+```
+
+Adding foreign key to `users`:
+
+```ruby
+include Gitlab::Database::MigrationHelpers
+
+def up
+ with_lock_retries do
+ add_foreign_key :imports, :users, column: :user_id, on_delete: :cascade
+ end
+end
+
+def down
+ with_lock_retries do
+ remove_foreign_key :imports, column: :user_id
+ end
+end
+```
+
### When to use the helper method
The `with_lock_retries` helper method can be used when you normally use
diff --git a/doc/install/aws/index.md b/doc/install/aws/index.md
index 6e16433dfee..ee4f759be3a 100644
--- a/doc/install/aws/index.md
+++ b/doc/install/aws/index.md
@@ -247,7 +247,17 @@ create the actual RDS instance.
![RDS Subnet Group](img/rds_subnet_group.png)
-### Creating the database
+### RDS Security Group
+
+We need a security group for our database that will allow inbound traffic from the instances we'll deploy in our `gitlab-loadbalancer-sec-group` later on:
+
+1. From the EC2 dashboard, select **Security Groups** from the left menu bar.
+1. Click **Create security group**.
+1. Give it a name (we'll use `gitlab-rds-sec-group`), a description, and select the `gitlab-vpc` from the **VPC** dropdown.
+1. In the **Inbound rules** section, click **Add rule** and add a **PostgreSQL** rule, and set the "Custom" source as the `gitlab-loadbalancer-sec-group` we created earlier. The default PostgreSQL port is `5432`, which we'll also use when creating our database below.
+1. When done, click **Create security group**.
+
+### Create the database
Now, it's time to create the database:
@@ -266,7 +276,7 @@ Now, it's time to create the database:
1. Select the VPC we created earlier (`gitlab-vpc`) from the **Virtual Private Cloud (VPC)** dropdown menu.
1. Expand the **Additional connectivity configuration** section and select the subnet group (`gitlab-rds-group`) we created earlier.
1. Set public accessibility to **No**.
- 1. Under **VPC security group**, select **Create new** and enter a name. We'll use `gitlab-rds-sec-group`.
+ 1. Under **VPC security group**, select **Choose existing** and select the `gitlab-rds-sec-group` we create above from the dropdown.
1. Leave the database port as the default `5432`.
1. For **Database authentication**, select **Password authentication**.
1. Expand the **Additional configuration** section and complete the following:
@@ -327,17 +337,6 @@ persistence and is used for certain types of the GitLab application.
1. Leave the rest of the settings to their default values or edit to your liking.
1. When done, click **Create**.
-## RDS and Redis Security Group
-
-Let's navigate to our EC2 security groups and add a small change for our EC2
-instances to be able to connect to RDS. First, copy the security group name we
-defined, namely `gitlab-security-group`, select the RDS security group and edit the
-inbound rules. Choose the rule type to be PostgreSQL and paste the name under
-source.
-
-Similar to the above, jump to the `gitlab-security-group` group
-and add a custom TCP rule for port `6379` accessible within itself.
-
## Setting up Bastion Hosts
Since our GitLab instances will be in private subnets, we need a way to connect to these instances via SSH to make configuration changes, perform upgrades, etc. One way of doing this is via a [bastion host](https://en.wikipedia.org/wiki/Bastion_host), sometimes also referred to as a jump box.
diff --git a/doc/install/installation.md b/doc/install/installation.md
index f3574395a5c..f6eeec11539 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -969,6 +969,15 @@ If you want to switch back to Unicorn, follow these steps:
1. Edit the system `init.d` script to set the `USE_UNICORN=1` flag. If you have `/etc/default/gitlab`, then you should edit it instead.
1. Restart GitLab.
+### Using Sidekiq instead of Sidekiq Cluster
+
+As of GitLab 12.10, Source installations are using `bin/sidekiq-cluster` for managing Sidekiq processes.
+Using Sidekiq directly will still be supported until 14.0. So if you're experiencing issues, please:
+
+1. Edit the system `init.d` script to remove the `SIDEKIQ_WORKERS` flag. If you have `/etc/default/gitlab`, then you should edit it instead.
+1. Restart GitLab.
+1. [Create an issue](https://gitlab.com/gitlab-org/gitlab/issues/-/new) describing the problem.
+
## Troubleshooting
### "You appear to have cloned an empty repository."
diff --git a/doc/user/project/issues/design_management.md b/doc/user/project/issues/design_management.md
index dd65c8f0929..40771c14ea4 100644
--- a/doc/user/project/issues/design_management.md
+++ b/doc/user/project/issues/design_management.md
@@ -75,6 +75,19 @@ you can drag and drop designs onto the dedicated dropzone to upload them.
![Drag and drop design uploads](img/design_drag_and_drop_uploads_v12_9.png)
+[Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/202634)
+in GitLab 12.10, you can also copy images from your file system and
+paste them directly on GitLab's Design page as a new design.
+
+On macOS you can also take a screenshot and immediately copy it to
+the clipboard by simultaneously clicking <kbd>Control</kbd> + <kbd>Command</kbd> + <kbd>Shift</kbd> + <kbd>3</kbd>, and then paste it as a design.
+
+Copy-and-pasting has some limitations:
+
+- You can paste only one image at a time. When copy/pasting multiple files, only the first one will be uploaded.
+- All images will be converted to `png` format under the hood, so when you want to copy/paste `gif` file, it will result in broken animation.
+- Copy/pasting designs is not supported on Internet Explorer.
+
Designs with the same filename as an existing uploaded design will create a new version
of the design, and will replace the previous version. [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/34353) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.9, dropping a design on an existing uploaded design will also create a new version,
provided the filenames are the same.
diff --git a/lib/gitlab/sidekiq_middleware/metrics.rb b/lib/gitlab/sidekiq_middleware/metrics.rb
index 693e35f2500..7ae8995c46d 100644
--- a/lib/gitlab/sidekiq_middleware/metrics.rb
+++ b/lib/gitlab/sidekiq_middleware/metrics.rb
@@ -9,7 +9,13 @@ module Gitlab
private
def create_labels(worker_class, queue)
- labels = { queue: queue.to_s, urgency: "", external_dependencies: FALSE_LABEL, feature_category: "", boundary: "" }
+ labels = { queue: queue.to_s,
+ worker: worker_class.to_s,
+ urgency: "",
+ external_dependencies: FALSE_LABEL,
+ feature_category: "",
+ boundary: "" }
+
return labels unless worker_class && worker_class.include?(WorkerAttributes)
labels[:urgency] = worker_class.get_urgency.to_s
diff --git a/lib/support/init.d/gitlab b/lib/support/init.d/gitlab
index 982c1dc8866..5e8e2ab9c25 100755
--- a/lib/support/init.d/gitlab
+++ b/lib/support/init.d/gitlab
@@ -27,6 +27,7 @@
### Environment variables
RAILS_ENV="production"
USE_UNICORN=""
+SIDEKIQ_WORKERS=1
# Script variable names should be lower-case not to conflict with
# internal /bin/sh variables such as PATH, EDITOR or SHELL.
@@ -36,7 +37,6 @@ pid_path="$app_root/tmp/pids"
socket_path="$app_root/tmp/sockets"
rails_socket="$socket_path/gitlab.socket"
web_server_pid_path="$pid_path/unicorn.pid"
-sidekiq_pid_path="$pid_path/sidekiq.pid"
mail_room_enabled=false
mail_room_pid_path="$pid_path/mail_room.pid"
gitlab_workhorse_dir=$(cd $app_root/../gitlab-workhorse 2> /dev/null && pwd)
@@ -74,6 +74,11 @@ else
use_web_server="unicorn"
fi
+if [ -z "$SIDEKIQ_WORKERS" ]; then
+ sidekiq_pid_path="$pid_path/sidekiq.pid"
+else
+ sidekiq_pid_path="$pid_path/sidekiq-cluster.pid"
+fi
### Init Script functions
@@ -295,7 +300,7 @@ start_gitlab() {
if [ "$sidekiq_status" = "0" ]; then
echo "The Sidekiq job dispatcher is already running with pid $spid, not restarting"
else
- RAILS_ENV=$RAILS_ENV bin/background_jobs start &
+ RAILS_ENV=$RAILS_ENV SIDEKIQ_WORKERS=$SIDEKIQ_WORKERS bin/background_jobs start &
fi
if [ "$gitlab_workhorse_status" = "0" ]; then
@@ -354,7 +359,7 @@ stop_gitlab() {
fi
if [ "$sidekiq_status" = "0" ]; then
echo "Shutting down GitLab Sidekiq"
- RAILS_ENV=$RAILS_ENV bin/background_jobs stop
+ RAILS_ENV=$RAILS_ENV SIDEKIQ_WORKERS=$SIDEKIQ_WORKERS bin/background_jobs stop
fi
if [ "$gitlab_workhorse_status" = "0" ]; then
echo "Shutting down GitLab Workhorse"
@@ -458,7 +463,7 @@ reload_gitlab(){
echo "Done."
echo "Restarting GitLab Sidekiq since it isn't capable of reloading its config..."
- RAILS_ENV=$RAILS_ENV bin/background_jobs restart
+ RAILS_ENV=$RAILS_ENV SIDEKIQ_WORKERS=$SIDEKIQ_WORKERS bin/background_jobs restart
if [ "$mail_room_enabled" != true ]; then
echo "Restarting GitLab MailRoom since it isn't capable of reloading its config..."
diff --git a/spec/features/projects/settings/registry_settings_spec.rb b/spec/features/projects/settings/registry_settings_spec.rb
index 0613148172f..74d3544ce92 100644
--- a/spec/features/projects/settings/registry_settings_spec.rb
+++ b/spec/features/projects/settings/registry_settings_spec.rb
@@ -26,7 +26,6 @@ describe 'Project > Settings > CI/CD > Container registry tag expiration policy'
it 'saves expiration policy submit the form' do
within '#js-registry-policies' do
within '.card-body' do
- find('.gl-toggle-wrapper button:not(.is-disabled)').click
select('7 days until tags are automatically removed', from: 'Expiration interval:')
select('Every day', from: 'Expiration schedule:')
select('50 tags per image name', from: 'Number of tags to retain:')
diff --git a/spec/frontend/__mocks__/sortablejs/index.js b/spec/frontend/__mocks__/sortablejs/index.js
index a1166d21561..5039af54542 100644
--- a/spec/frontend/__mocks__/sortablejs/index.js
+++ b/spec/frontend/__mocks__/sortablejs/index.js
@@ -1,4 +1,4 @@
-import Sortablejs from 'sortablejs';
+const Sortablejs = jest.genMockFromModule('sortablejs');
export default Sortablejs;
export const Sortable = Sortablejs;
diff --git a/spec/frontend/boards/components/board_column_spec.js b/spec/frontend/boards/components/board_column_spec.js
new file mode 100644
index 00000000000..7cf6ec913b4
--- /dev/null
+++ b/spec/frontend/boards/components/board_column_spec.js
@@ -0,0 +1,172 @@
+import Vue from 'vue';
+import { shallowMount } from '@vue/test-utils';
+import AxiosMockAdapter from 'axios-mock-adapter';
+
+import Board from '~/boards/components/board_column.vue';
+import List from '~/boards/models/list';
+import { ListType } from '~/boards/constants';
+import axios from '~/lib/utils/axios_utils';
+
+import { TEST_HOST } from 'helpers/test_constants';
+import { listObj } from 'jest/boards/mock_data';
+
+describe('Board Column Component', () => {
+ let wrapper;
+ let axiosMock;
+
+ beforeEach(() => {
+ window.gon = {};
+ axiosMock = new AxiosMockAdapter(axios);
+ axiosMock.onGet(`${TEST_HOST}/lists/1/issues`).reply(200, { issues: [] });
+ });
+
+ afterEach(() => {
+ axiosMock.restore();
+
+ wrapper.destroy();
+
+ localStorage.clear();
+ });
+
+ const createComponent = ({
+ listType = ListType.backlog,
+ collapsed = false,
+ withLocalStorage = true,
+ } = {}) => {
+ const boardId = '1';
+
+ const listMock = {
+ ...listObj,
+ list_type: listType,
+ collapsed,
+ };
+
+ if (listType === ListType.assignee) {
+ delete listMock.label;
+ listMock.user = {};
+ }
+
+ // Making List reactive
+ const list = Vue.observable(new List(listMock));
+
+ if (withLocalStorage) {
+ localStorage.setItem(
+ `boards.${boardId}.${list.type}.${list.id}.expanded`,
+ (!collapsed).toString(),
+ );
+ }
+
+ wrapper = shallowMount(Board, {
+ propsData: {
+ boardId,
+ disabled: false,
+ issueLinkBase: '/',
+ rootPath: '/',
+ list,
+ },
+ });
+ };
+
+ const isExpandable = () => wrapper.classes('is-expandable');
+ const isCollapsed = () => wrapper.classes('is-collapsed');
+
+ const findAddIssueButton = () => wrapper.find({ ref: 'newIssueBtn' });
+
+ describe('Add issue button', () => {
+ const hasNoAddButton = [ListType.promotion, ListType.blank, ListType.closed];
+ const hasAddButton = [ListType.backlog, ListType.label, ListType.milestone, ListType.assignee];
+
+ it.each(hasNoAddButton)('does not render when List Type is `%s`', listType => {
+ createComponent({ listType });
+
+ expect(findAddIssueButton().exists()).toBe(false);
+ });
+
+ it.each(hasAddButton)('does render when List Type is `%s`', listType => {
+ createComponent({ listType });
+
+ expect(findAddIssueButton().exists()).toBe(true);
+ });
+
+ it('has a test for each list type', () => {
+ Object.values(ListType).forEach(value => {
+ expect([...hasAddButton, ...hasNoAddButton]).toContain(value);
+ });
+ });
+
+ it('does render when logged out', () => {
+ createComponent();
+
+ expect(findAddIssueButton().exists()).toBe(true);
+ });
+ });
+
+ describe('Given different list types', () => {
+ it('is expandable when List Type is `backlog`', () => {
+ createComponent({ listType: ListType.backlog });
+
+ expect(isExpandable()).toBe(true);
+ });
+ });
+
+ describe('expanding / collapsing the column', () => {
+ it('does not collapse when clicking the header', () => {
+ createComponent();
+ expect(isCollapsed()).toBe(false);
+ wrapper.find('.board-header').trigger('click');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(isCollapsed()).toBe(false);
+ });
+ });
+
+ it('collapses expanded Column when clicking the collapse icon', () => {
+ createComponent();
+ expect(wrapper.vm.list.isExpanded).toBe(true);
+ wrapper.find('.board-title-caret').trigger('click');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(isCollapsed()).toBe(true);
+ });
+ });
+
+ it('expands collapsed Column when clicking the expand icon', () => {
+ createComponent({ collapsed: true });
+ expect(isCollapsed()).toBe(true);
+ wrapper.find('.board-title-caret').trigger('click');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(isCollapsed()).toBe(false);
+ });
+ });
+
+ it("when logged in it calls list update and doesn't set localStorage", () => {
+ jest.spyOn(List.prototype, 'update');
+ window.gon.current_user_id = 1;
+
+ createComponent({ withLocalStorage: false });
+
+ wrapper.find('.board-title-caret').trigger('click');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.list.update).toHaveBeenCalledTimes(1);
+ expect(localStorage.getItem(`${wrapper.vm.uniqueKey}.expanded`)).toBe(null);
+ });
+ });
+
+ it("when logged out it doesn't call list update and sets localStorage", () => {
+ jest.spyOn(List.prototype, 'update');
+
+ createComponent();
+
+ wrapper.find('.board-title-caret').trigger('click');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.list.update).toHaveBeenCalledTimes(0);
+ expect(localStorage.getItem(`${wrapper.vm.uniqueKey}.expanded`)).toBe(
+ String(wrapper.vm.list.isExpanded),
+ );
+ });
+ });
+ });
+});
diff --git a/spec/frontend/boards/list_spec.js b/spec/frontend/boards/list_spec.js
index c0dd5afe498..b30281f8df5 100644
--- a/spec/frontend/boards/list_spec.js
+++ b/spec/frontend/boards/list_spec.js
@@ -56,7 +56,7 @@ describe('List model', () => {
label: {
id: 1,
title: 'test',
- color: 'red',
+ color: '#ff0000',
text_color: 'white',
},
});
@@ -64,8 +64,7 @@ describe('List model', () => {
expect(list.id).toBe(listObj.id);
expect(list.type).toBe('label');
expect(list.position).toBe(0);
- expect(list.label.color).toBe('red');
- expect(list.label.textColor).toBe('white');
+ expect(list.label).toEqual(listObj.label);
});
});
diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js
index fa4154676a2..97d49de6f2e 100644
--- a/spec/frontend/boards/mock_data.js
+++ b/spec/frontend/boards/mock_data.js
@@ -15,7 +15,7 @@ export const listObj = {
label: {
id: 5000,
title: 'Test',
- color: 'red',
+ color: '#ff0000',
description: 'testing;',
textColor: 'white',
},
@@ -30,7 +30,7 @@ export const listObjDuplicate = {
label: {
id: listObj.label.id,
title: 'Test',
- color: 'red',
+ color: '#ff0000',
description: 'testing;',
},
};
diff --git a/spec/frontend/lib/utils/file_upload_spec.js b/spec/frontend/lib/utils/file_upload_spec.js
index 1255d6fc14f..1dff5d4f925 100644
--- a/spec/frontend/lib/utils/file_upload_spec.js
+++ b/spec/frontend/lib/utils/file_upload_spec.js
@@ -1,4 +1,4 @@
-import fileUpload from '~/lib/utils/file_upload';
+import fileUpload, { getFilename } from '~/lib/utils/file_upload';
describe('File upload', () => {
beforeEach(() => {
@@ -62,3 +62,15 @@ describe('File upload', () => {
expect(input.click).not.toHaveBeenCalled();
});
});
+
+describe('getFilename', () => {
+ it('returns first value correctly', () => {
+ const event = {
+ clipboardData: {
+ getData: () => 'test.png\rtest.txt',
+ },
+ };
+
+ expect(getFilename(event)).toBe('test.png');
+ });
+});
diff --git a/spec/frontend/notes/components/diff_discussion_header_spec.js b/spec/frontend/notes/components/diff_discussion_header_spec.js
index 4c76f9c50fb..9162bee2078 100644
--- a/spec/frontend/notes/components/diff_discussion_header_spec.js
+++ b/spec/frontend/notes/components/diff_discussion_header_spec.js
@@ -3,7 +3,7 @@ import { mount } from '@vue/test-utils';
import createStore from '~/notes/stores';
import diffDiscussionHeader from '~/notes/components/diff_discussion_header.vue';
-import { discussionMock } from '../../../javascripts/notes/mock_data';
+import { discussionMock } from '../mock_data';
import mockDiffFile from '../../diffs/mock_data/diff_discussions';
const discussionWithTwoUnresolvedNotes = 'merge_requests/resolved_diff_discussion.json';
diff --git a/spec/frontend/notes/components/diff_with_note_spec.js b/spec/frontend/notes/components/diff_with_note_spec.js
new file mode 100644
index 00000000000..d6d42e1988d
--- /dev/null
+++ b/spec/frontend/notes/components/diff_with_note_spec.js
@@ -0,0 +1,86 @@
+import { mount } from '@vue/test-utils';
+import DiffWithNote from '~/notes/components/diff_with_note.vue';
+import { createStore } from '~/mr_notes/stores';
+
+const discussionFixture = 'merge_requests/diff_discussion.json';
+const imageDiscussionFixture = 'merge_requests/image_diff_discussion.json';
+
+describe('diff_with_note', () => {
+ let store;
+ let wrapper;
+
+ const selectors = {
+ get diffTable() {
+ return wrapper.find('.diff-content table');
+ },
+ get diffRows() {
+ return wrapper.findAll('.diff-content .line_holder');
+ },
+ get noteRow() {
+ return wrapper.find('.diff-content .notes_holder');
+ },
+ };
+
+ beforeEach(() => {
+ store = createStore();
+ store.replaceState({
+ ...store.state,
+ notes: {
+ noteableData: {
+ current_user: {},
+ },
+ },
+ });
+ });
+
+ describe('text diff', () => {
+ beforeEach(() => {
+ const diffDiscussion = getJSONFixture(discussionFixture)[0];
+
+ wrapper = mount(DiffWithNote, {
+ propsData: {
+ discussion: diffDiscussion,
+ },
+ store,
+ });
+ });
+
+ it('removes trailing "+" char', () => {
+ const richText = wrapper.vm.$el
+ .querySelectorAll('.line_holder')[4]
+ .querySelector('.line_content').textContent[0];
+
+ expect(richText).not.toEqual('+');
+ });
+
+ it('removes trailing "-" char', () => {
+ const richText = wrapper.vm.$el.querySelector('#LC13').parentNode.textContent[0];
+
+ expect(richText).not.toEqual('-');
+ });
+
+ it('shows text diff', () => {
+ expect(wrapper.classes('text-file')).toBe(true);
+ expect(selectors.diffTable.exists()).toBe(true);
+ });
+
+ it('shows diff lines', () => {
+ expect(selectors.diffRows.length).toBe(12);
+ });
+
+ it('shows notes row', () => {
+ expect(selectors.noteRow.exists()).toBe(true);
+ });
+ });
+
+ describe('image diff', () => {
+ beforeEach(() => {
+ const imageDiscussion = getJSONFixture(imageDiscussionFixture)[0];
+ wrapper = mount(DiffWithNote, { propsData: { discussion: imageDiscussion }, store });
+ });
+
+ it('shows image diff', () => {
+ expect(selectors.diffTable.exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/notes/components/discussion_filter_spec.js b/spec/frontend/notes/components/discussion_filter_spec.js
new file mode 100644
index 00000000000..b8d2d721443
--- /dev/null
+++ b/spec/frontend/notes/components/discussion_filter_spec.js
@@ -0,0 +1,219 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+
+import { createLocalVue, mount } from '@vue/test-utils';
+import AxiosMockAdapter from 'axios-mock-adapter';
+
+import axios from '~/lib/utils/axios_utils';
+import notesModule from '~/notes/stores/modules';
+import DiscussionFilter from '~/notes/components/discussion_filter.vue';
+import { DISCUSSION_FILTERS_DEFAULT_VALUE, DISCUSSION_FILTER_TYPES } from '~/notes/constants';
+
+import { discussionFiltersMock, discussionMock } from '../mock_data';
+import { TEST_HOST } from 'jest/helpers/test_constants';
+
+const localVue = createLocalVue();
+
+localVue.use(Vuex);
+
+const DISCUSSION_PATH = `${TEST_HOST}/example`;
+
+describe('DiscussionFilter component', () => {
+ let wrapper;
+ let store;
+ let eventHub;
+ let mock;
+
+ const filterDiscussion = jest.fn();
+
+ const mountComponent = () => {
+ const discussions = [
+ {
+ ...discussionMock,
+ id: discussionMock.id,
+ notes: [{ ...discussionMock.notes[0], resolvable: true, resolved: true }],
+ },
+ ];
+
+ const defaultStore = { ...notesModule() };
+
+ store = new Vuex.Store({
+ ...defaultStore,
+ actions: {
+ ...defaultStore.actions,
+ filterDiscussion,
+ },
+ });
+
+ store.state.notesData.discussionsPath = DISCUSSION_PATH;
+
+ store.state.discussions = discussions;
+
+ return mount(DiscussionFilter, {
+ store,
+ propsData: {
+ filters: discussionFiltersMock,
+ selectedValue: DISCUSSION_FILTERS_DEFAULT_VALUE,
+ },
+ localVue,
+ });
+ };
+
+ beforeEach(() => {
+ mock = new AxiosMockAdapter(axios);
+
+ // We are mocking the discussions retrieval,
+ // as it doesn't matter for our tests here
+ mock.onGet(DISCUSSION_PATH).reply(200, '');
+ window.mrTabs = undefined;
+ wrapper = mountComponent();
+ });
+
+ afterEach(() => {
+ wrapper.vm.$destroy();
+ mock.restore();
+ });
+
+ it('renders the all filters', () => {
+ expect(wrapper.findAll('.dropdown-menu li').length).toBe(discussionFiltersMock.length);
+ });
+
+ it('renders the default selected item', () => {
+ expect(
+ wrapper
+ .find('#discussion-filter-dropdown')
+ .text()
+ .trim(),
+ ).toBe(discussionFiltersMock[0].title);
+ });
+
+ it('updates to the selected item', () => {
+ const filterItem = wrapper.find(
+ `.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.HISTORY}"] button`,
+ );
+
+ filterItem.trigger('click');
+
+ expect(wrapper.vm.currentFilter.title).toBe(filterItem.text().trim());
+ });
+
+ it('only updates when selected filter changes', () => {
+ wrapper
+ .find(`.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.ALL}"] button`)
+ .trigger('click');
+
+ expect(filterDiscussion).not.toHaveBeenCalled();
+ });
+
+ it('disables commenting when "Show history only" filter is applied', () => {
+ const filterItem = wrapper.find(
+ `.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.HISTORY}"] button`,
+ );
+ filterItem.trigger('click');
+
+ expect(wrapper.vm.$store.state.commentsDisabled).toBe(true);
+ });
+
+ it('enables commenting when "Show history only" filter is not applied', () => {
+ const filterItem = wrapper.find(
+ `.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.ALL}"] button`,
+ );
+ filterItem.trigger('click');
+
+ expect(wrapper.vm.$store.state.commentsDisabled).toBe(false);
+ });
+
+ it('renders a dropdown divider for the default filter', () => {
+ const defaultFilter = wrapper.findAll(
+ `.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.ALL}"] > *`,
+ );
+
+ expect(defaultFilter.at(defaultFilter.length - 1).classes('dropdown-divider')).toBe(true);
+ });
+
+ describe('Merge request tabs', () => {
+ eventHub = new Vue();
+
+ beforeEach(() => {
+ window.mrTabs = {
+ eventHub,
+ currentTab: 'show',
+ };
+
+ wrapper = mountComponent();
+ });
+
+ afterEach(() => {
+ window.mrTabs = undefined;
+ });
+
+ it('only renders when discussion tab is active', done => {
+ eventHub.$emit('MergeRequestTabChange', 'commit');
+
+ wrapper.vm.$nextTick(() => {
+ expect(wrapper.isEmpty()).toBe(true);
+ done();
+ });
+ });
+ });
+
+ describe('URL with Links to notes', () => {
+ afterEach(() => {
+ window.location.hash = '';
+ });
+
+ it('updates the filter when the URL links to a note', done => {
+ window.location.hash = `note_${discussionMock.notes[0].id}`;
+ wrapper.vm.currentValue = discussionFiltersMock[2].value;
+ wrapper.vm.handleLocationHash();
+
+ wrapper.vm.$nextTick(() => {
+ expect(wrapper.vm.currentValue).toBe(DISCUSSION_FILTERS_DEFAULT_VALUE);
+ done();
+ });
+ });
+
+ it('does not update the filter when the current filter is "Show all activity"', done => {
+ window.location.hash = `note_${discussionMock.notes[0].id}`;
+ wrapper.vm.handleLocationHash();
+
+ wrapper.vm.$nextTick(() => {
+ expect(wrapper.vm.currentValue).toBe(DISCUSSION_FILTERS_DEFAULT_VALUE);
+ done();
+ });
+ });
+
+ it('only updates filter when the URL links to a note', done => {
+ window.location.hash = `testing123`;
+ wrapper.vm.handleLocationHash();
+
+ wrapper.vm.$nextTick(() => {
+ expect(wrapper.vm.currentValue).toBe(DISCUSSION_FILTERS_DEFAULT_VALUE);
+ done();
+ });
+ });
+
+ it('fetches discussions when there is a hash', done => {
+ window.location.hash = `note_${discussionMock.notes[0].id}`;
+ wrapper.vm.currentValue = discussionFiltersMock[2].value;
+ jest.spyOn(wrapper.vm, 'selectFilter').mockImplementation(() => {});
+ wrapper.vm.handleLocationHash();
+
+ wrapper.vm.$nextTick(() => {
+ expect(wrapper.vm.selectFilter).toHaveBeenCalled();
+ done();
+ });
+ });
+
+ it('does not fetch discussions when there is no hash', done => {
+ window.location.hash = '';
+ jest.spyOn(wrapper.vm, 'selectFilter').mockImplementation(() => {});
+ wrapper.vm.handleLocationHash();
+
+ wrapper.vm.$nextTick(() => {
+ expect(wrapper.vm.selectFilter).not.toHaveBeenCalled();
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/notes/components/discussion_resolve_with_issue_button_spec.js b/spec/frontend/notes/components/discussion_resolve_with_issue_button_spec.js
index 4348445f7ca..4348445f7ca 100644
--- a/spec/javascripts/notes/components/discussion_resolve_with_issue_button_spec.js
+++ b/spec/frontend/notes/components/discussion_resolve_with_issue_button_spec.js
diff --git a/spec/javascripts/notes/components/note_actions/reply_button_spec.js b/spec/frontend/notes/components/note_actions/reply_button_spec.js
index 720ab10b270..720ab10b270 100644
--- a/spec/javascripts/notes/components/note_actions/reply_button_spec.js
+++ b/spec/frontend/notes/components/note_actions/reply_button_spec.js
diff --git a/spec/javascripts/notes/components/note_actions_spec.js b/spec/frontend/notes/components/note_actions_spec.js
index 5d13f587ca7..5d13f587ca7 100644
--- a/spec/javascripts/notes/components/note_actions_spec.js
+++ b/spec/frontend/notes/components/note_actions_spec.js
diff --git a/spec/javascripts/notes/components/note_awards_list_spec.js b/spec/frontend/notes/components/note_awards_list_spec.js
index 90aa1684272..822b1f9efce 100644
--- a/spec/javascripts/notes/components/note_awards_list_spec.js
+++ b/spec/frontend/notes/components/note_awards_list_spec.js
@@ -1,14 +1,24 @@
import Vue from 'vue';
+import AxiosMockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import createStore from '~/notes/stores';
import awardsNote from '~/notes/components/note_awards_list.vue';
import { noteableDataMock, notesDataMock } from '../mock_data';
+import { TEST_HOST } from 'jest/helpers/test_constants';
describe('note_awards_list component', () => {
let store;
let vm;
let awardsMock;
+ let mock;
+
+ const toggleAwardPath = `${TEST_HOST}/gitlab-org/gitlab-foss/notes/545/toggle_award_emoji`;
beforeEach(() => {
+ mock = new AxiosMockAdapter(axios);
+
+ mock.onPost(toggleAwardPath).reply(200, '');
+
const Component = Vue.extend(awardsNote);
store = createStore();
@@ -32,12 +42,13 @@ describe('note_awards_list component', () => {
noteAuthorId: 2,
noteId: '545',
canAwardEmoji: true,
- toggleAwardPath: '/gitlab-org/gitlab-foss/notes/545/toggle_award_emoji',
+ toggleAwardPath,
},
}).$mount();
});
afterEach(() => {
+ mock.restore();
vm.$destroy();
});
@@ -49,8 +60,8 @@ describe('note_awards_list component', () => {
});
it('should be possible to remove awarded emoji', () => {
- spyOn(vm, 'handleAward').and.callThrough();
- spyOn(vm, 'toggleAwardRequest').and.callThrough();
+ jest.spyOn(vm, 'handleAward');
+ jest.spyOn(vm, 'toggleAwardRequest');
vm.$el.querySelector('.js-awards-block button').click();
expect(vm.handleAward).toHaveBeenCalledWith('flag_tz');
@@ -138,7 +149,7 @@ describe('note_awards_list component', () => {
});
it('should not be possible to remove awarded emoji', () => {
- spyOn(vm, 'toggleAwardRequest').and.callThrough();
+ jest.spyOn(vm, 'toggleAwardRequest');
vm.$el.querySelector('.js-awards-block button').click();
diff --git a/spec/javascripts/notes/components/note_body_spec.js b/spec/frontend/notes/components/note_body_spec.js
index efad0785afe..efad0785afe 100644
--- a/spec/javascripts/notes/components/note_body_spec.js
+++ b/spec/frontend/notes/components/note_body_spec.js
diff --git a/spec/javascripts/notes/components/note_form_spec.js b/spec/frontend/notes/components/note_form_spec.js
index 8ab8bce9027..bccac03126c 100644
--- a/spec/javascripts/notes/components/note_form_spec.js
+++ b/spec/frontend/notes/components/note_form_spec.js
@@ -4,6 +4,10 @@ import NoteForm from '~/notes/components/note_form.vue';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import { noteableDataMock, notesDataMock } from '../mock_data';
+import { getDraft, updateDraft } from '~/lib/utils/autosave';
+
+jest.mock('~/lib/utils/autosave');
+
describe('issue_note_form component', () => {
const dummyAutosaveKey = 'some-autosave-key';
const dummyDraft = 'dummy draft content';
@@ -23,7 +27,7 @@ describe('issue_note_form component', () => {
};
beforeEach(() => {
- spyOnDependency(NoteForm, 'getDraft').and.callFake(key => {
+ getDraft.mockImplementation(key => {
if (key === dummyAutosaveKey) {
return dummyDraft;
}
@@ -55,19 +59,15 @@ describe('issue_note_form component', () => {
expect(wrapper.vm.noteHash).toBe(`#note_${props.noteId}`);
});
- it('return note hash as `#` when `noteId` is empty', done => {
+ it('return note hash as `#` when `noteId` is empty', () => {
wrapper.setProps({
...props,
noteId: '',
});
- wrapper.vm
- .$nextTick()
- .then(() => {
- expect(wrapper.vm.noteHash).toBe('#');
- })
- .then(done)
- .catch(done.fail);
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.noteHash).toBe('#');
+ });
});
});
@@ -76,7 +76,7 @@ describe('issue_note_form component', () => {
wrapper = createComponentWrapper();
});
- it('should show conflict message if note changes outside the component', done => {
+ it('should show conflict message if note changes outside the component', () => {
wrapper.setProps({
...props,
isEditing: true,
@@ -86,21 +86,17 @@ describe('issue_note_form component', () => {
const message =
'This comment has changed since you started editing, please review the updated comment to ensure information is not lost.';
- wrapper.vm
- .$nextTick()
- .then(() => {
- const conflictWarning = wrapper.find('.js-conflict-edit-warning');
-
- expect(conflictWarning.exists()).toBe(true);
- expect(
- conflictWarning
- .text()
- .replace(/\s+/g, ' ')
- .trim(),
- ).toBe(message);
- })
- .then(done)
- .catch(done.fail);
+ return wrapper.vm.$nextTick().then(() => {
+ const conflictWarning = wrapper.find('.js-conflict-edit-warning');
+
+ expect(conflictWarning.exists()).toBe(true);
+ expect(
+ conflictWarning
+ .text()
+ .replace(/\s+/g, ' ')
+ .trim(),
+ ).toBe(message);
+ });
});
});
@@ -136,7 +132,7 @@ describe('issue_note_form component', () => {
describe('up', () => {
it('should ender edit mode', () => {
// TODO: do not spy on vm
- spyOn(wrapper.vm, 'editMyLastNote').and.callThrough();
+ jest.spyOn(wrapper.vm, 'editMyLastNote');
textarea.trigger('keydown.up');
@@ -164,61 +160,50 @@ describe('issue_note_form component', () => {
});
describe('actions', () => {
- it('should be possible to cancel', done => {
+ it('should be possible to cancel', () => {
// TODO: do not spy on vm
- spyOn(wrapper.vm, 'cancelHandler').and.callThrough();
+ jest.spyOn(wrapper.vm, 'cancelHandler');
wrapper.setProps({
...props,
isEditing: true,
});
- wrapper.vm
- .$nextTick()
- .then(() => {
- const cancelButton = wrapper.find('.note-edit-cancel');
- cancelButton.trigger('click');
+ return wrapper.vm.$nextTick().then(() => {
+ const cancelButton = wrapper.find('.note-edit-cancel');
+ cancelButton.trigger('click');
- expect(wrapper.vm.cancelHandler).toHaveBeenCalled();
- })
- .then(done)
- .catch(done.fail);
+ expect(wrapper.vm.cancelHandler).toHaveBeenCalled();
+ });
});
- it('should be possible to update the note', done => {
+ it('should be possible to update the note', () => {
wrapper.setProps({
...props,
isEditing: true,
});
- wrapper.vm
- .$nextTick()
- .then(() => {
- const textarea = wrapper.find('textarea');
- textarea.setValue('Foo');
- const saveButton = wrapper.find('.js-vue-issue-save');
- saveButton.trigger('click');
-
- expect(wrapper.vm.isSubmitting).toEqual(true);
- })
- .then(done)
- .catch(done.fail);
+ return wrapper.vm.$nextTick().then(() => {
+ const textarea = wrapper.find('textarea');
+ textarea.setValue('Foo');
+ const saveButton = wrapper.find('.js-vue-issue-save');
+ saveButton.trigger('click');
+
+ expect(wrapper.vm.isSubmitting).toBe(true);
+ });
});
});
});
describe('with autosaveKey', () => {
describe('with draft', () => {
- beforeEach(done => {
+ beforeEach(() => {
Object.assign(props, {
noteBody: '',
autosaveKey: dummyAutosaveKey,
});
wrapper = createComponentWrapper();
- wrapper.vm
- .$nextTick()
- .then(done)
- .catch(done.fail);
+ return wrapper.vm.$nextTick();
});
it('displays the draft in textarea', () => {
@@ -229,17 +214,14 @@ describe('issue_note_form component', () => {
});
describe('without draft', () => {
- beforeEach(done => {
+ beforeEach(() => {
Object.assign(props, {
noteBody: '',
autosaveKey: 'some key without draft',
});
wrapper = createComponentWrapper();
- wrapper.vm
- .$nextTick()
- .then(done)
- .catch(done.fail);
+ return wrapper.vm.$nextTick();
});
it('leaves the textarea empty', () => {
@@ -250,7 +232,6 @@ describe('issue_note_form component', () => {
});
it('updates the draft if textarea content changes', () => {
- const updateDraftSpy = spyOnDependency(NoteForm, 'updateDraft').and.stub();
Object.assign(props, {
noteBody: '',
autosaveKey: dummyAutosaveKey,
@@ -261,7 +242,7 @@ describe('issue_note_form component', () => {
textarea.setValue(dummyContent);
- expect(updateDraftSpy).toHaveBeenCalledWith(dummyAutosaveKey, dummyContent);
+ expect(updateDraft).toHaveBeenCalledWith(dummyAutosaveKey, dummyContent);
});
});
});
diff --git a/spec/javascripts/notes/components/note_signed_out_widget_spec.js b/spec/frontend/notes/components/note_signed_out_widget_spec.js
index e217a2caa73..e217a2caa73 100644
--- a/spec/javascripts/notes/components/note_signed_out_widget_spec.js
+++ b/spec/frontend/notes/components/note_signed_out_widget_spec.js
diff --git a/spec/javascripts/notes/components/noteable_discussion_spec.js b/spec/frontend/notes/components/noteable_discussion_spec.js
index ee84fd2b091..b91f599f158 100644
--- a/spec/javascripts/notes/components/noteable_discussion_spec.js
+++ b/spec/frontend/notes/components/noteable_discussion_spec.js
@@ -12,8 +12,8 @@ import {
loggedOutnoteableData,
userDataMock,
} from '../mock_data';
-import mockDiffFile from '../../diffs/mock_data/diff_file';
-import { trimText } from '../../helpers/text_helper';
+import mockDiffFile from 'jest/diffs/mock_data/diff_file';
+import { trimText } from 'helpers/text_helper';
const discussionWithTwoUnresolvedNotes = 'merge_requests/resolved_diff_discussion.json';
@@ -47,27 +47,24 @@ describe('noteable_discussion component', () => {
expect(wrapper.find('.discussion-header').exists()).toBe(false);
});
- it('should render thread header', done => {
+ it('should render thread header', () => {
const discussion = { ...discussionMock };
discussion.diff_file = mockDiffFile;
discussion.diff_discussion = true;
+ discussion.expanded = false;
wrapper.setProps({ discussion });
- wrapper.vm
- .$nextTick()
- .then(() => {
- expect(wrapper.find('.discussion-header').exists()).toBe(true);
- })
- .then(done)
- .catch(done.fail);
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.find('.discussion-header').exists()).toBe(true);
+ });
});
describe('actions', () => {
- it('should toggle reply form', done => {
+ it('should toggle reply form', () => {
const replyPlaceholder = wrapper.find(ReplyPlaceholder);
- wrapper.vm
+ return wrapper.vm
.$nextTick()
.then(() => {
expect(wrapper.vm.isReplying).toEqual(false);
@@ -89,9 +86,7 @@ describe('noteable_discussion component', () => {
expect(noteFormProps.line).toBe(null);
expect(noteFormProps.saveButtonTitle).toBe('Comment');
expect(noteFormProps.autosaveKey).toBe(`Note/Issue/${discussionMock.id}/Reply`);
- })
- .then(done)
- .catch(done.fail);
+ });
});
it('does not render jump to thread button', () => {
@@ -115,7 +110,7 @@ describe('noteable_discussion component', () => {
});
describe('for unresolved thread', () => {
- beforeEach(done => {
+ beforeEach(() => {
const discussion = {
...getJSONFixture(discussionWithTwoUnresolvedNotes)[0],
expanded: true,
@@ -131,10 +126,7 @@ describe('noteable_discussion component', () => {
wrapper.setProps({ discussion });
- wrapper.vm
- .$nextTick()
- .then(done)
- .catch(done.fail);
+ return wrapper.vm.$nextTick();
});
it('displays a button to resolve with issue', () => {
diff --git a/spec/javascripts/notes/components/noteable_note_spec.js b/spec/frontend/notes/components/noteable_note_spec.js
index 1906dae7800..0d67b1d87a9 100644
--- a/spec/javascripts/notes/components/noteable_note_spec.js
+++ b/spec/frontend/notes/components/noteable_note_spec.js
@@ -86,7 +86,7 @@ describe('issue_note', () => {
it('prevents note preview xss', done => {
const imgSrc = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
const noteBody = `<img src="${imgSrc}" onload="alert(1)" />`;
- const alertSpy = spyOn(window, 'alert');
+ const alertSpy = jest.spyOn(window, 'alert');
store.hotUpdate({
actions: {
updateNote() {},
@@ -96,11 +96,11 @@ describe('issue_note', () => {
noteBodyComponent.vm.$emit('handleFormUpdate', noteBody, null, () => {});
- setTimeout(() => {
+ setImmediate(() => {
expect(alertSpy).not.toHaveBeenCalled();
expect(wrapper.vm.note.note_html).toEqual(escape(noteBody));
done();
- }, 0);
+ });
});
describe('cancel edit', () => {
diff --git a/spec/javascripts/notes/components/toggle_replies_widget_spec.js b/spec/frontend/notes/components/toggle_replies_widget_spec.js
index 8485ec0262f..b4f68b039cf 100644
--- a/spec/javascripts/notes/components/toggle_replies_widget_spec.js
+++ b/spec/frontend/notes/components/toggle_replies_widget_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import mountComponent from 'helpers/vue_mount_component_helper';
import toggleRepliesWidget from '~/notes/components/toggle_replies_widget.vue';
import { note } from '../mock_data';
@@ -44,7 +44,7 @@ describe('toggle replies widget for notes', () => {
});
it('should emit toggle event when the replies text clicked', () => {
- const spy = spyOn(vm, '$emit');
+ const spy = jest.spyOn(vm, '$emit');
vm.$el.querySelector('.js-replies-text').click();
@@ -68,7 +68,7 @@ describe('toggle replies widget for notes', () => {
});
it('should emit toggle event when the collapse replies text called', () => {
- const spy = spyOn(vm, '$emit');
+ const spy = jest.spyOn(vm, '$emit');
vm.$el.querySelector('.js-collapse-replies').click();
diff --git a/spec/javascripts/notes/stores/collapse_utils_spec.js b/spec/frontend/notes/stores/collapse_utils_spec.js
index d3019f4b9a4..d3019f4b9a4 100644
--- a/spec/javascripts/notes/stores/collapse_utils_spec.js
+++ b/spec/frontend/notes/stores/collapse_utils_spec.js
diff --git a/spec/helpers/container_expiration_policies_helper_spec.rb b/spec/helpers/container_expiration_policies_helper_spec.rb
index f7e851fb012..6dcbadd89cb 100644
--- a/spec/helpers/container_expiration_policies_helper_spec.rb
+++ b/spec/helpers/container_expiration_policies_helper_spec.rb
@@ -37,8 +37,8 @@ describe ContainerExpirationPoliciesHelper do
expected_result = [
{ key: '7d', label: '7 days until tags are automatically removed' },
{ key: '14d', label: '14 days until tags are automatically removed' },
- { key: '30d', label: '30 days until tags are automatically removed', default: true },
- { key: '90d', label: '90 days until tags are automatically removed' }
+ { key: '30d', label: '30 days until tags are automatically removed' },
+ { key: '90d', label: '90 days until tags are automatically removed', default: true }
]
expect(helper.older_than_options).to eq(expected_result)
diff --git a/spec/javascripts/helpers/init_vue_mr_page_helper.js b/spec/javascripts/helpers/init_vue_mr_page_helper.js
index 3fa29cb9136..04f969fcd2d 100644
--- a/spec/javascripts/helpers/init_vue_mr_page_helper.js
+++ b/spec/javascripts/helpers/init_vue_mr_page_helper.js
@@ -1,7 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import initMRPage from '~/mr_notes/index';
import axios from '~/lib/utils/axios_utils';
-import { userDataMock, notesDataMock, noteableDataMock } from '../notes/mock_data';
+import { userDataMock, notesDataMock, noteableDataMock } from '../../frontend/notes/mock_data';
import diffFileMockData from '../diffs/mock_data/diff_file';
export default function initVueMRPage() {
diff --git a/spec/javascripts/notes/components/diff_with_note_spec.js b/spec/javascripts/notes/components/diff_with_note_spec.js
deleted file mode 100644
index 573aac2c3e0..00000000000
--- a/spec/javascripts/notes/components/diff_with_note_spec.js
+++ /dev/null
@@ -1,89 +0,0 @@
-import Vue from 'vue';
-import { mountComponentWithStore } from 'spec/helpers';
-import DiffWithNote from '~/notes/components/diff_with_note.vue';
-import { createStore } from '~/mr_notes/stores';
-
-const discussionFixture = 'merge_requests/diff_discussion.json';
-const imageDiscussionFixture = 'merge_requests/image_diff_discussion.json';
-
-describe('diff_with_note', () => {
- let store;
- let vm;
- const diffDiscussionMock = getJSONFixture(discussionFixture)[0];
- const diffDiscussion = diffDiscussionMock;
- const Component = Vue.extend(DiffWithNote);
- const props = {
- discussion: diffDiscussion,
- };
- const selectors = {
- get container() {
- return vm.$el;
- },
- get diffTable() {
- return this.container.querySelector('.diff-content table');
- },
- get diffRows() {
- return this.container.querySelectorAll('.diff-content .line_holder');
- },
- get noteRow() {
- return this.container.querySelector('.diff-content .notes_holder');
- },
- };
-
- beforeEach(() => {
- store = createStore();
- store.replaceState({
- ...store.state,
- notes: {
- noteableData: {
- current_user: {},
- },
- },
- });
- });
-
- describe('text diff', () => {
- beforeEach(() => {
- vm = mountComponentWithStore(Component, { props, store });
- });
-
- it('removes trailing "+" char', () => {
- const richText = vm.$el.querySelectorAll('.line_holder')[4].querySelector('.line_content')
- .textContent[0];
-
- expect(richText).not.toEqual('+');
- });
-
- it('removes trailing "-" char', () => {
- const richText = vm.$el.querySelector('#LC13').parentNode.textContent[0];
-
- expect(richText).not.toEqual('-');
- });
-
- it('shows text diff', () => {
- expect(selectors.container).toHaveClass('text-file');
- expect(selectors.diffTable).toExist();
- });
-
- it('shows diff lines', () => {
- expect(selectors.diffRows.length).toBe(12);
- });
-
- it('shows notes row', () => {
- expect(selectors.noteRow).toExist();
- });
- });
-
- describe('image diff', () => {
- beforeEach(() => {
- const imageDiffDiscussionMock = getJSONFixture(imageDiscussionFixture)[0];
- props.discussion = imageDiffDiscussionMock;
- });
-
- it('shows image diff', () => {
- vm = mountComponentWithStore(Component, { props, store });
-
- expect(selectors.diffTable).not.toExist();
- });
- });
-});
diff --git a/spec/javascripts/notes/components/discussion_filter_spec.js b/spec/javascripts/notes/components/discussion_filter_spec.js
deleted file mode 100644
index 7524de36ac5..00000000000
--- a/spec/javascripts/notes/components/discussion_filter_spec.js
+++ /dev/null
@@ -1,187 +0,0 @@
-import Vue from 'vue';
-import createStore from '~/notes/stores';
-import DiscussionFilter from '~/notes/components/discussion_filter.vue';
-import { DISCUSSION_FILTERS_DEFAULT_VALUE, DISCUSSION_FILTER_TYPES } from '~/notes/constants';
-import { mountComponentWithStore } from '../../helpers/vue_mount_component_helper';
-import { discussionFiltersMock, discussionMock } from '../mock_data';
-
-describe('DiscussionFilter component', () => {
- let vm;
- let store;
- let eventHub;
-
- const mountComponent = () => {
- store = createStore();
-
- const discussions = [
- {
- ...discussionMock,
- id: discussionMock.id,
- notes: [{ ...discussionMock.notes[0], resolvable: true, resolved: true }],
- },
- ];
- const Component = Vue.extend(DiscussionFilter);
- const selectedValue = DISCUSSION_FILTERS_DEFAULT_VALUE;
- const props = { filters: discussionFiltersMock, selectedValue };
-
- store.state.discussions = discussions;
- return mountComponentWithStore(Component, {
- el: null,
- store,
- props,
- });
- };
-
- beforeEach(() => {
- window.mrTabs = undefined;
- vm = mountComponent();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- it('renders the all filters', () => {
- expect(vm.$el.querySelectorAll('.dropdown-menu li').length).toEqual(
- discussionFiltersMock.length,
- );
- });
-
- it('renders the default selected item', () => {
- expect(vm.$el.querySelector('#discussion-filter-dropdown').textContent.trim()).toEqual(
- discussionFiltersMock[0].title,
- );
- });
-
- it('updates to the selected item', () => {
- const filterItem = vm.$el.querySelector(
- `.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.HISTORY}"] button`,
- );
- filterItem.click();
-
- expect(vm.currentFilter.title).toEqual(filterItem.textContent.trim());
- });
-
- it('only updates when selected filter changes', () => {
- const filterItem = vm.$el.querySelector(
- `.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.ALL}"] button`,
- );
-
- spyOn(vm, 'filterDiscussion');
- filterItem.click();
-
- expect(vm.filterDiscussion).not.toHaveBeenCalled();
- });
-
- it('disables commenting when "Show history only" filter is applied', () => {
- const filterItem = vm.$el.querySelector(
- `.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.HISTORY}"] button`,
- );
- filterItem.click();
-
- expect(vm.$store.state.commentsDisabled).toBe(true);
- });
-
- it('enables commenting when "Show history only" filter is not applied', () => {
- const filterItem = vm.$el.querySelector(
- `.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.ALL}"] button`,
- );
- filterItem.click();
-
- expect(vm.$store.state.commentsDisabled).toBe(false);
- });
-
- it('renders a dropdown divider for the default filter', () => {
- const defaultFilter = vm.$el.querySelector(
- `.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.ALL}"]`,
- );
-
- expect(defaultFilter.lastChild.classList).toContain('dropdown-divider');
- });
-
- describe('Merge request tabs', () => {
- eventHub = new Vue();
-
- beforeEach(() => {
- window.mrTabs = {
- eventHub,
- currentTab: 'show',
- };
-
- vm = mountComponent();
- });
-
- afterEach(() => {
- window.mrTabs = undefined;
- });
-
- it('only renders when discussion tab is active', done => {
- eventHub.$emit('MergeRequestTabChange', 'commit');
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector).toBeUndefined();
- done();
- });
- });
- });
-
- describe('URL with Links to notes', () => {
- afterEach(() => {
- window.location.hash = '';
- });
-
- it('updates the filter when the URL links to a note', done => {
- window.location.hash = `note_${discussionMock.notes[0].id}`;
- vm.currentValue = discussionFiltersMock[2].value;
- vm.handleLocationHash();
-
- vm.$nextTick(() => {
- expect(vm.currentValue).toEqual(DISCUSSION_FILTERS_DEFAULT_VALUE);
- done();
- });
- });
-
- it('does not update the filter when the current filter is "Show all activity"', done => {
- window.location.hash = `note_${discussionMock.notes[0].id}`;
- vm.handleLocationHash();
-
- vm.$nextTick(() => {
- expect(vm.currentValue).toEqual(DISCUSSION_FILTERS_DEFAULT_VALUE);
- done();
- });
- });
-
- it('only updates filter when the URL links to a note', done => {
- window.location.hash = `testing123`;
- vm.handleLocationHash();
-
- vm.$nextTick(() => {
- expect(vm.currentValue).toEqual(DISCUSSION_FILTERS_DEFAULT_VALUE);
- done();
- });
- });
-
- it('fetches discussions when there is a hash', done => {
- window.location.hash = `note_${discussionMock.notes[0].id}`;
- vm.currentValue = discussionFiltersMock[2].value;
- spyOn(vm, 'selectFilter');
- vm.handleLocationHash();
-
- vm.$nextTick(() => {
- expect(vm.selectFilter).toHaveBeenCalled();
- done();
- });
- });
-
- it('does not fetch discussions when there is no hash', done => {
- window.location.hash = '';
- spyOn(vm, 'selectFilter');
- vm.handleLocationHash();
-
- vm.$nextTick(() => {
- expect(vm.selectFilter).not.toHaveBeenCalled();
- done();
- });
- });
- });
-});
diff --git a/spec/javascripts/notes/helpers.js b/spec/javascripts/notes/helpers.js
deleted file mode 100644
index 7bcba609311..00000000000
--- a/spec/javascripts/notes/helpers.js
+++ /dev/null
@@ -1 +0,0 @@
-export * from '../../frontend/notes/helpers.js';
diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js
deleted file mode 100644
index 89e4553092a..00000000000
--- a/spec/javascripts/notes/mock_data.js
+++ /dev/null
@@ -1 +0,0 @@
-export * from '../../frontend/notes/mock_data.js';
diff --git a/spec/lib/gitlab/sidekiq_middleware/client_metrics_spec.rb b/spec/lib/gitlab/sidekiq_middleware/client_metrics_spec.rb
index 59639409183..5f80ef9538a 100644
--- a/spec/lib/gitlab/sidekiq_middleware/client_metrics_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/client_metrics_spec.rb
@@ -9,7 +9,14 @@ describe Gitlab::SidekiqMiddleware::ClientMetrics do
let(:queue) { :test }
let(:worker_class) { worker.class }
let(:job) { {} }
- let(:default_labels) { { queue: queue.to_s, boundary: "", external_dependencies: "no", feature_category: "", urgency: "low" } }
+ let(:default_labels) do
+ { queue: queue.to_s,
+ worker: worker_class.to_s,
+ boundary: "",
+ external_dependencies: "no",
+ feature_category: "",
+ urgency: "low" }
+ end
shared_examples "a metrics client middleware" do
context "with mocked prometheus" do
diff --git a/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb
index 3343587beff..3214bd758e7 100644
--- a/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb
@@ -11,7 +11,14 @@ describe Gitlab::SidekiqMiddleware::ServerMetrics do
let(:job) { {} }
let(:job_status) { :done }
let(:labels_with_job_status) { labels.merge(job_status: job_status.to_s) }
- let(:default_labels) { { queue: queue.to_s, boundary: "", external_dependencies: "no", feature_category: "", urgency: "low" } }
+ let(:default_labels) do
+ { queue: queue.to_s,
+ worker: worker_class.to_s,
+ boundary: "",
+ external_dependencies: "no",
+ feature_category: "",
+ urgency: "low" }
+ end
shared_examples "a metrics middleware" do
context "with mocked prometheus" do
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 425194ba0e3..844e50dbb58 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -1924,7 +1924,7 @@ describe Ci::Pipeline, :mailer do
describe '#update_status' do
context 'when pipeline is empty' do
it 'updates does not change pipeline status' do
- expect(pipeline.statuses.latest.slow_composite_status).to be_nil
+ expect(pipeline.statuses.latest.slow_composite_status(project: project)).to be_nil
expect { pipeline.update_legacy_status }
.to change { pipeline.reload.status }
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index 40d9afcdd14..73b81b2225a 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -423,7 +423,7 @@ describe CommitStatus do
end
it 'returns a correct compound status' do
- expect(described_class.all.slow_composite_status).to eq 'running'
+ expect(described_class.all.slow_composite_status(project: project)).to eq 'running'
end
end
@@ -433,7 +433,7 @@ describe CommitStatus do
end
it 'returns status that indicates success' do
- expect(described_class.all.slow_composite_status).to eq 'success'
+ expect(described_class.all.slow_composite_status(project: project)).to eq 'success'
end
end
@@ -444,7 +444,7 @@ describe CommitStatus do
end
it 'returns status according to the scope' do
- expect(described_class.latest.slow_composite_status).to eq 'success'
+ expect(described_class.latest.slow_composite_status(project: project)).to eq 'success'
end
end
end
diff --git a/spec/models/concerns/has_status_spec.rb b/spec/models/concerns/has_status_spec.rb
index 99d09af80d0..68047f24ec3 100644
--- a/spec/models/concerns/has_status_spec.rb
+++ b/spec/models/concerns/has_status_spec.rb
@@ -6,7 +6,7 @@ describe HasStatus do
describe '.slow_composite_status' do
using RSpec::Parameterized::TableSyntax
- subject { CommitStatus.slow_composite_status }
+ subject { CommitStatus.slow_composite_status(project: nil) }
shared_examples 'build status summary' do
context 'all successful' do
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 61c871ead92..291c628bfde 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -4400,6 +4400,12 @@ describe User, :do_not_mock_admin_mode do
it { is_expected.to be expected_result }
end
+
+ context 'when email is of Gitlab and is not confirmed' do
+ let(:user) { build(:user, email: 'test@gitlab.com', confirmed_at: nil) }
+
+ it { is_expected.to be false }
+ end
end
describe '#current_highest_access_level' do