Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
53b1f4eaa2
commit
8a7efa45c3
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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: () =>
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ export default {
|
|||
},
|
||||
},
|
||||
mounted() {
|
||||
if (!this.hasTruncatedDiffLines) {
|
||||
if (this.isTextFile && !this.hasTruncatedDiffLines) {
|
||||
this.fetchDiff();
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Upload a design by copy/pasting the file into the Design Tab
|
||||
merge_request: 27776
|
||||
author:
|
||||
type: added
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Enable container expiration policies by default for new projects
|
||||
merge_request: 28480
|
||||
author:
|
||||
type: changed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Remove duplicate specs in update service spec
|
||||
merge_request: 28650
|
||||
author: Rajendra Kadam
|
||||
type: added
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
\.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -247,7 +247,17 @@ create the actual RDS instance.
|
|||
|
||||

|
||||
|
||||
### 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.
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -75,6 +75,19 @@ you can drag and drop designs onto the dedicated dropzone to upload them.
|
|||
|
||||

|
||||
|
||||
[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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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..."
|
||||
|
|
|
|||
|
|
@ -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:')
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import Sortablejs from 'sortablejs';
|
||||
const Sortablejs = jest.genMockFromModule('sortablejs');
|
||||
|
||||
export default Sortablejs;
|
||||
export const Sortable = Sortablejs;
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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;',
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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();
|
||||
|
||||
|
|
@ -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');
|
||||
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);
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
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');
|
||||
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).toEqual(true);
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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', () => {
|
||||
|
|
@ -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', () => {
|
||||
|
|
@ -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();
|
||||
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1 +0,0 @@
|
|||
export * from '../../frontend/notes/helpers.js';
|
||||
|
|
@ -1 +0,0 @@
|
|||
export * from '../../frontend/notes/mock_data.js';
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue