Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-01-07 18:16:06 +00:00
parent c68ee79c33
commit 8bac8f55ba
71 changed files with 1276 additions and 781 deletions

View File

@ -1 +1 @@
641e24107a6f4f8593b87ed55c3aa7cd76010963
3627c14a64ce48446e8a67299c3161ff7290d1ad

View File

@ -7,6 +7,7 @@ import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vu
import { updateGlobalTodoCount } from '~/vue_shared/components/sidebar/todo_toggle/utils';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import DesignNotePin from '~/vue_shared/components/design_management/design_note_pin.vue';
import { isLoggedIn } from '~/lib/utils/common_utils';
import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../../constants';
import createNoteMutation from '../../graphql/mutations/create_note.mutation.graphql';
import toggleResolveDiscussionMutation from '../../graphql/mutations/toggle_resolve_discussion.mutation.graphql';
@ -17,6 +18,7 @@ import { hasErrors } from '../../utils/cache_update';
import { extractDesign } from '../../utils/design_management_utils';
import { ADD_DISCUSSION_COMMENT_ERROR } from '../../utils/error_messages';
import DesignNote from './design_note.vue';
import DesignNoteSignedOut from './design_note_signed_out.vue';
import DesignReplyForm from './design_reply_form.vue';
import ToggleRepliesWidget from './toggle_replies_widget.vue';
@ -24,6 +26,7 @@ export default {
components: {
ApolloMutation,
DesignNote,
DesignNoteSignedOut,
ReplyPlaceholder,
DesignReplyForm,
GlIcon,
@ -55,6 +58,14 @@ export default {
required: false,
default: '',
},
registerPath: {
type: String,
required: true,
},
signInPath: {
type: String,
required: true,
},
resolvedDiscussionsExpanded: {
type: Boolean,
required: true,
@ -93,6 +104,7 @@ export default {
isResolving: false,
shouldChangeResolvedStatus: false,
areRepliesCollapsed: this.discussion.resolved,
isLoggedIn: isLoggedIn(),
};
},
computed: {
@ -226,7 +238,7 @@ export default {
:class="{ 'gl-bg-blue-50': isDiscussionActive }"
@error="$emit('update-note-error', $event)"
>
<template v-if="discussion.resolvable" #resolve-discussion>
<template v-if="isLoggedIn && discussion.resolvable" #resolve-discussion>
<button
v-gl-tooltip
:class="{ 'is-active': discussion.resolved }"
@ -269,38 +281,47 @@ export default {
:class="{ 'gl-bg-blue-50': isDiscussionActive }"
@error="$emit('update-note-error', $event)"
/>
<li v-show="isReplyPlaceholderVisible" class="reply-wrapper discussion-reply-holder">
<reply-placeholder
v-if="!isFormVisible"
class="qa-discussion-reply"
:placeholder-text="__('Reply…')"
@focus="showForm"
/>
<apollo-mutation
v-else
#default="{ mutate, loading }"
:mutation="$options.createNoteMutation"
:variables="{
input: mutationPayload,
}"
@done="onDone"
@error="onCreateNoteError"
>
<design-reply-form
v-model="discussionComment"
:is-saving="loading"
:markdown-preview-path="markdownPreviewPath"
@submit-form="mutate"
@cancel-form="hideForm"
<li
v-show="isReplyPlaceholderVisible"
class="reply-wrapper discussion-reply-holder"
:class="{ 'gl-bg-gray-10': !isLoggedIn }"
>
<template v-if="!isLoggedIn">
<design-note-signed-out :register-path="registerPath" :sign-in-path="signInPath" />
</template>
<template v-else>
<reply-placeholder
v-if="!isFormVisible"
class="qa-discussion-reply"
:placeholder-text="__('Reply…')"
@focus="showForm"
/>
<apollo-mutation
v-else
#default="{ mutate, loading }"
:mutation="$options.createNoteMutation"
:variables="{
input: mutationPayload,
}"
@done="onDone"
@error="onCreateNoteError"
>
<template v-if="discussion.resolvable" #resolve-checkbox>
<label data-testid="resolve-checkbox">
<input v-model="shouldChangeResolvedStatus" type="checkbox" />
{{ resolveCheckboxText }}
</label>
</template>
</design-reply-form>
</apollo-mutation>
<design-reply-form
v-model="discussionComment"
:is-saving="loading"
:markdown-preview-path="markdownPreviewPath"
@submit-form="mutate"
@cancel-form="hideForm"
>
<template v-if="discussion.resolvable" #resolve-checkbox>
<label data-testid="resolve-checkbox">
<input v-model="shouldChangeResolvedStatus" type="checkbox" />
{{ resolveCheckboxText }}
</label>
</template>
</design-reply-form>
</apollo-mutation>
</template>
</li>
</ul>
</div>

View File

@ -0,0 +1,50 @@
<script>
import { GlSprintf, GlLink } from '@gitlab/ui';
import { __ } from '~/locale';
export default {
components: {
GlSprintf,
GlLink,
},
props: {
registerPath: {
type: String,
required: true,
},
signInPath: {
type: String,
required: true,
},
isAddDiscussion: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
signedOutText() {
return this.isAddDiscussion
? __(
'Please %{registerLinkStart}register%{registerLinkEnd} or %{signInLinkStart}sign in%{signInLinkEnd} to start a new discussion.',
)
: __(
'Please %{registerLinkStart}register%{registerLinkEnd} or %{signInLinkStart}sign in%{signInLinkEnd} to reply.',
);
},
},
};
</script>
<template>
<div class="disabled-comment text-center">
<gl-sprintf :message="signedOutText">
<template #registerLink="{ content }">
<gl-link :href="registerPath">{{ content }}</gl-link>
</template>
<template #signInLink="{ content }">
<gl-link :href="signInPath">{{ content }}</gl-link>
</template>
</gl-sprintf>
</div>
</template>

View File

@ -1,5 +1,6 @@
<script>
import { throttle } from 'lodash';
import { isLoggedIn } from '~/lib/utils/common_utils';
import DesignOverlay from './design_overlay.vue';
import DesignImage from './image.vue';
@ -54,6 +55,7 @@ export default {
initialLoad: true,
lastDragPosition: null,
isDraggingDesign: false,
isLoggedIn: isLoggedIn(),
};
},
computed: {
@ -311,7 +313,7 @@ export default {
:position="overlayPosition"
:notes="discussionStartingNotes"
:current-comment-form="currentCommentForm"
:disable-commenting="isDraggingDesign"
:disable-commenting="!isLoggedIn || isDraggingDesign"
:resolved-discussions-expanded="resolvedDiscussionsExpanded"
@openCommentForm="openCommentForm"
@closeCommentForm="closeCommentForm"

View File

@ -1,7 +1,7 @@
<script>
import { GlCollapse, GlButton, GlPopover } from '@gitlab/ui';
import Cookies from 'js-cookie';
import { parseBoolean } from '~/lib/utils/common_utils';
import { parseBoolean, isLoggedIn } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
import Participants from '~/sidebar/components/participants/participants.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@ -9,11 +9,13 @@ import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../constants';
import updateActiveDiscussionMutation from '../graphql/mutations/update_active_discussion.mutation.graphql';
import { extractDiscussions, extractParticipants } from '../utils/design_management_utils';
import DesignDiscussion from './design_notes/design_discussion.vue';
import DesignNoteSignedOut from './design_notes/design_note_signed_out.vue';
import DesignTodoButton from './design_todo_button.vue';
export default {
components: {
DesignDiscussion,
DesignNoteSignedOut,
Participants,
GlCollapse,
GlButton,
@ -28,6 +30,12 @@ export default {
issueIid: {
default: '',
},
registerPath: {
default: '',
},
signInPath: {
default: '',
},
},
props: {
design: {
@ -47,6 +55,7 @@ export default {
return {
isResolvedCommentsPopoverHidden: parseBoolean(Cookies.get(this.$options.cookieKey)),
discussionWithOpenForm: '',
isLoggedIn: isLoggedIn(),
};
},
computed: {
@ -134,12 +143,19 @@ export default {
class="gl-mb-4"
/>
<h2
v-if="unresolvedDiscussions.length === 0"
v-if="isLoggedIn && unresolvedDiscussions.length === 0"
class="new-discussion-disclaimer gl-font-base gl-m-0 gl-mb-4"
data-testid="new-discussion-disclaimer"
>
{{ s__("DesignManagement|Click the image where you'd like to start a new discussion") }}
</h2>
<design-note-signed-out
v-if="!isLoggedIn"
class="gl-mb-4"
:register-path="registerPath"
:sign-in-path="signInPath"
:is-add-discussion="true"
/>
<design-discussion
v-for="discussion in unresolvedDiscussions"
:key="discussion.id"
@ -147,6 +163,8 @@ export default {
:design-id="$route.params.id"
:noteable-id="design.id"
:markdown-preview-path="markdownPreviewPath"
:register-path="registerPath"
:sign-in-path="signInPath"
:resolved-discussions-expanded="resolvedDiscussionsExpanded"
:discussion-with-open-form="discussionWithOpenForm"
data-testid="unresolved-discussion"
@ -197,6 +215,8 @@ export default {
:design-id="$route.params.id"
:noteable-id="design.id"
:markdown-preview-path="markdownPreviewPath"
:register-path="registerPath"
:sign-in-path="signInPath"
:resolved-discussions-expanded="resolvedDiscussionsExpanded"
:discussion-with-open-form="discussionWithOpenForm"
data-testid="resolved-discussion"

View File

@ -8,7 +8,7 @@ import createRouter from './router';
export default () => {
const el = document.querySelector('.js-design-management');
const { issueIid, projectPath, issuePath } = el.dataset;
const { issueIid, projectPath, issuePath, registerPath, signInPath } = el.dataset;
const router = createRouter(issuePath);
apolloProvider.clients.defaultClient.cache.writeQuery({
@ -29,6 +29,8 @@ export default () => {
provide: {
projectPath,
issueIid,
registerPath,
signInPath,
},
mounted() {
performanceMarkAndMeasure({

View File

@ -0,0 +1,20 @@
import Vue from 'vue';
import GitlabVersionCheck from '~/vue_shared/components/gitlab_version_check.vue';
const mountGitlabVersionCheck = (el) => {
const { size } = el.dataset;
return new Vue({
el,
render(createElement) {
return createElement(GitlabVersionCheck, {
props: {
size,
},
});
},
});
};
export default () =>
[...document.querySelectorAll('.js-gitlab-version-check')].map(mountGitlabVersionCheck);

View File

@ -6,7 +6,7 @@ import { IssuableStatus, IssuableStatusText, IssuableType } from '~/issues/const
import Poll from '~/lib/utils/poll';
import { visitUrl } from '~/lib/utils/url_utility';
import { __, sprintf } from '~/locale';
import { IssueTypePath, IncidentTypePath, IncidentType, POLLING_DELAY } from '../constants';
import { ISSUE_TYPE_PATH, INCIDENT_TYPE_PATH, INCIDENT_TYPE, POLLING_DELAY } from '../constants';
import eventHub from '../event_hub';
import getIssueStateQuery from '../queries/get_issue_state.query.graphql';
import Service from '../services/index';
@ -378,15 +378,15 @@ export default {
.then((data) => {
if (
!window.location.pathname.includes(data.web_url) &&
issueState.issueType !== IncidentType
issueState.issueType !== INCIDENT_TYPE
) {
visitUrl(data.web_url);
}
if (issueState.isDirty) {
const URI =
issueState.issueType === IncidentType
? data.web_url.replace(IssueTypePath, IncidentTypePath)
issueState.issueType === INCIDENT_TYPE
? data.web_url.replace(ISSUE_TYPE_PATH, INCIDENT_TYPE_PATH)
: data.web_url;
visitUrl(URI);
}

View File

@ -2,7 +2,7 @@
import { GlFormGroup, GlDropdown, GlDropdownItem, GlIcon } from '@gitlab/ui';
import { capitalize } from 'lodash';
import { __ } from '~/locale';
import { IssuableTypes, IncidentType } from '../../constants';
import { issuableTypes, INCIDENT_TYPE } from '../../constants';
import getIssueStateQuery from '../../queries/get_issue_state.query.graphql';
import updateIssueStateMutation from '../../queries/update_issue_state.mutation.graphql';
@ -12,7 +12,7 @@ export const i18n = {
export default {
i18n,
IssuableTypes,
issuableTypes,
components: {
GlFormGroup,
GlIcon,
@ -45,7 +45,7 @@ export default {
return capitalize(issueType);
},
shouldShowIncident() {
return this.issueType === IncidentType || this.canCreateIncident;
return this.issueType === INCIDENT_TYPE || this.canCreateIncident;
},
},
methods: {
@ -59,7 +59,7 @@ export default {
});
},
isShown(type) {
return type.value !== IncidentType || this.shouldShowIncident;
return type.value !== INCIDENT_TYPE || this.shouldShowIncident;
},
},
};
@ -81,7 +81,7 @@ export default {
toggle-class="dropdown-menu-toggle"
>
<gl-dropdown-item
v-for="type in $options.IssuableTypes"
v-for="type in $options.issuableTypes"
v-show="isShown(type)"
:key="type.value"
:is-checked="issueState.issueType === type.value"

View File

@ -13,7 +13,7 @@ import createFlash, { FLASH_TYPES } from '~/flash';
import { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issuable/constants';
import { IssuableType } from '~/vue_shared/issuable/show/constants';
import { IssuableStatus } from '~/issues/constants';
import { IssueStateEvent } from '~/issues/show/constants';
import { ISSUE_STATE_EVENT_CLOSE, ISSUE_STATE_EVENT_REOPEN } from '~/issues/show/constants';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import { visitUrl } from '~/lib/utils/url_utility';
import { s__, __, sprintf } from '~/locale';
@ -163,7 +163,7 @@ export default {
input: {
iid: this.iid.toString(),
projectPath: this.projectPath,
stateEvent: this.isClosed ? IssueStateEvent.Reopen : IssueStateEvent.Close,
stateEvent: this.isClosed ? ISSUE_STATE_EVENT_REOPEN : ISSUE_STATE_EVENT_CLOSE,
},
},
})

View File

@ -1,22 +1,20 @@
import { __ } from '~/locale';
export const IssueStateEvent = {
Close: 'CLOSE',
Reopen: 'REOPEN',
};
export const STATUS_PAGE_PUBLISHED = __('Published on status page');
export const INCIDENT_TYPE = 'incident';
export const INCIDENT_TYPE_PATH = 'issues/incident';
export const ISSUE_STATE_EVENT_CLOSE = 'CLOSE';
export const ISSUE_STATE_EVENT_REOPEN = 'REOPEN';
export const ISSUE_TYPE_PATH = 'issues';
export const JOIN_ZOOM_MEETING = __('Join Zoom meeting');
export const POLLING_DELAY = 2000;
export const STATUS_PAGE_PUBLISHED = __('Published on status page');
export const IssuableTypes = [
export const issuableTypes = [
{ value: 'issue', text: __('Issue'), icon: 'issue-type-issue' },
{ value: 'incident', text: __('Incident'), icon: 'issue-type-incident' },
];
export const IssueTypePath = 'issues';
export const IncidentTypePath = 'issues/incident';
export const IncidentType = 'incident';
export const issueState = { issueType: undefined, isDirty: false };
export const POLLING_DELAY = 2000;
export const issueState = {
issueType: undefined,
isDirty: false,
};

View File

@ -6,7 +6,7 @@ import IssueApp from './components/app.vue';
import HeaderActions from './components/header_actions.vue';
import IncidentTabs from './components/incidents/incident_tabs.vue';
import SentryErrorStackTrace from './components/sentry_error_stack_trace.vue';
import { IncidentType, issueState } from './constants';
import { INCIDENT_TYPE, issueState } from './constants';
import apolloProvider from './graphql';
import getIssueStateQuery from './queries/get_issue_state.query.graphql';
@ -45,7 +45,7 @@ export function initIncidentApp(issueData = {}) {
el,
apolloProvider,
provide: {
issueType: IncidentType,
issueType: INCIDENT_TYPE,
canCreateIncident,
canUpdate,
fullPath,
@ -111,7 +111,7 @@ export function initHeaderActions(store, type = '') {
bootstrapApollo({ ...issueState, issueType: el.dataset.issueType });
const canCreate =
type === IncidentType ? el.dataset.canCreateIncident : el.dataset.canCreateIssue;
type === INCIDENT_TYPE ? el.dataset.canCreateIncident : el.dataset.canCreateIssue;
return new Vue({
el,

View File

@ -17,7 +17,6 @@ export const BV_HIDE_MODAL = 'bv::hide::modal';
export const BV_HIDE_TOOLTIP = 'bv::hide::tooltip';
export const BV_DROPDOWN_SHOW = 'bv::dropdown::show';
export const BV_DROPDOWN_HIDE = 'bv::dropdown::hide';
export const BV_COLLAPSE_STATE = 'bv::collapse::state';
export const DEFAULT_TH_CLASSES =
'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-100! gl-p-5! gl-border-b-1!';

View File

@ -1,6 +1,5 @@
import $ from 'jquery';
import docs from '~/docs/docs_bundle';
import VersionCheckImage from '~/version_check_image';
import initGitlabVersionCheck from '~/gitlab_version_check';
docs();
VersionCheckImage.bindErrorEvent($('img.js-version-status-badge'));
initGitlabVersionCheck();

View File

@ -596,7 +596,9 @@ export default {
:disabled="disableSubmitButton"
>{{ submitButtonText }}</gl-button
>
<gl-button :href="cancelFormPath" class="float-right">{{ $options.i18n.cancel }}</gl-button>
<gl-button data-testid="wiki-cancel-button" :href="cancelFormPath" class="float-right">{{
$options.i18n.cancel
}}</gl-button>
</div>
</gl-form>
</template>

View File

@ -1,6 +0,0 @@
export default class VersionCheckImage {
static bindErrorEvent(imageElement) {
// eslint-disable-next-line @gitlab/no-global-event-off
imageElement.off('error').on('error', () => imageElement.hide());
}
}

View File

@ -0,0 +1,67 @@
<script>
import { GlBadge } from '@gitlab/ui';
import { s__ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
const STATUS_TYPES = {
SUCCESS: 'success',
WARNING: 'warning',
DANGER: 'danger',
};
export default {
name: 'GitlabVersionCheck',
components: {
GlBadge,
},
props: {
size: {
type: String,
required: false,
default: 'md',
},
},
data() {
return {
status: null,
};
},
computed: {
title() {
if (this.status === STATUS_TYPES.SUCCESS) {
return s__('VersionCheck|Up to date');
} else if (this.status === STATUS_TYPES.WARNING) {
return s__('VersionCheck|Update available');
} else if (this.status === STATUS_TYPES.DANGER) {
return s__('VersionCheck|Update ASAP');
}
return null;
},
},
created() {
this.checkGitlabVersion();
},
methods: {
checkGitlabVersion() {
axios
.get('/admin/version_check.json')
.then((res) => {
if (res.data) {
this.status = res.data.severity;
}
})
.catch(() => {
// Silently fail
this.status = null;
});
},
},
};
</script>
<template>
<gl-badge v-if="status" class="version-check-badge" :variant="status" :size="size">{{
title
}}</gl-badge>
</template>

View File

@ -3,6 +3,8 @@
module Types
module Packages
class PackageDetailsType < PackageType
include ::PackagesHelper
graphql_name 'PackageDetailsType'
description 'Represents a package details in the Package Registry. Note that this type is in beta and susceptible to changes'
authorize :read_package
@ -21,6 +23,15 @@ module Types
description: 'Pipelines that built the package.',
deprecated: { reason: 'Due to scalability concerns, this field is going to be removed', milestone: '14.6' }
field :composer_config_repository_url, GraphQL::Types::String, null: true, description: 'Url of the Composer setup endpoint.'
field :composer_url, GraphQL::Types::String, null: true, description: 'Url of the Composer endpoint.'
field :conan_url, GraphQL::Types::String, null: true, description: 'Url of the Conan project endpoint.'
field :maven_url, GraphQL::Types::String, null: true, description: 'Url of the Maven project endpoint.'
field :npm_url, GraphQL::Types::String, null: true, description: 'Url of the NPM project endpoint.'
field :nuget_url, GraphQL::Types::String, null: true, description: 'Url of the Nuget project endpoint.'
field :pypi_setup_url, GraphQL::Types::String, null: true, description: 'Url of the PyPi project setup endpoint.'
field :pypi_url, GraphQL::Types::String, null: true, description: 'Url of the PyPi project endpoint.'
def versions
object.versions
end
@ -32,6 +43,38 @@ module Types
object.package_files
end
end
def composer_config_repository_url
composer_config_repository_name(object.project.group&.id)
end
def composer_url
composer_registry_url(object.project.group&.id)
end
def conan_url
package_registry_project_url(object.project.id, :conan)
end
def maven_url
package_registry_project_url(object.project.id, :maven)
end
def npm_url
package_registry_project_url(object.project.id, :npm)
end
def nuget_url
nuget_package_registry_url(object.project.id)
end
def pypi_setup_url
package_registry_project_url(object.project.id, :pypi)
end
def pypi_url
pypi_registry_url(object.project.id)
end
end
end
end

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true
module PackagesHelper
include ::API::Helpers::RelatedResourcesHelpers
def package_sort_path(options = {})
"#{request.path}?#{options.to_param}"
end

View File

@ -1,12 +1,11 @@
# frozen_string_literal: true
module VersionCheckHelper
def version_status_badge
return unless Rails.env.production?
return unless Gitlab::CurrentSettings.version_check_enabled
return if User.single_user&.requires_usage_stats_consent?
def show_version_check?
return false unless Gitlab::CurrentSettings.version_check_enabled
return false if User.single_user&.requires_usage_stats_consent?
image_tag VersionCheck.image_url, class: 'js-version-status-badge'
current_user&.can_read_all_resources?
end
def link_to_version

View File

@ -118,9 +118,9 @@
.gl-card-body
%h4
= s_('AdminArea|Components')
- if Gitlab::CurrentSettings.version_check_enabled
- if show_version_check?
.float-right
= version_status_badge
.js-gitlab-version-check{ data: { "size": "lg" } }
= link_to(sprite_icon('question'), "https://gitlab.com/gitlab-org/gitlab/-/blob/master/CHANGELOG.md", class: 'gl-ml-2', target: '_blank', rel: 'noopener noreferrer')
%p
= link_to _('GitLab'), general_admin_application_settings_path

View File

@ -1,31 +0,0 @@
= form_for [:admin, @label], html: { class: 'label-form js-requires-input' } do |f|
= form_errors(@label)
.form-group.row
.col-sm-2.col-form-label
= f.label :title
.col-sm-10
= f.text_field :title, class: "form-control gl-form-input", required: true
.form-group.row
.col-sm-2.col-form-label
= f.label :description
.col-sm-10
= f.text_field :description, class: "form-control gl-form-input js-quick-submit"
.form-group.row
.col-sm-2.col-form-label
= f.label :color, _("Background color")
.col-sm-10
.input-group
.input-group-prepend
.input-group-text.label-color-preview &nbsp;
= f.text_field :color, class: "form-control gl-form-input"
.form-text.text-muted
= _('Choose any color.')
%br
= _("Or you can choose one of the suggested colors below")
= render_suggested_colors
.form-actions
= f.submit _('Save'), class: 'btn gl-button btn-confirm js-save-button'
= link_to _("Cancel"), admin_labels_path, class: 'btn gl-button btn-default btn-cancel'

View File

@ -4,4 +4,4 @@
%h3.page-title
= _('Edit Label')
%hr
= render 'form'
= render 'shared/labels/form', url: admin_label_path(@label), back_path: admin_labels_path

View File

@ -2,4 +2,4 @@
%h3.page-title
= _('New Label')
%hr
= render 'form'
= render 'shared/labels/form', url: admin_labels_path, back_path: admin_labels_path

View File

@ -4,12 +4,15 @@
= markdown_field(Gitlab::CurrentSettings.current_application_settings, :help_page_text)
%hr
%h1
= default_brand_title
- if user_signed_in?
%span= link_to_version
= version_status_badge
%hr
.gl-display-flex.gl-align-items-flex-end
%h1.gl-mt-5.gl-mb-3
= default_brand_title
- if user_signed_in?
%span= link_to_version
- if show_version_check?
%span.gl-mt-5.gl-mb-3.gl-ml-3
.js-gitlab-version-check{ data: { "size": "lg" } }
%hr
- unless Gitlab::CurrentSettings.help_page_hide_commercial_content?
%p.slead

View File

@ -8,7 +8,11 @@
- if @project.design_management_enabled?
- add_page_startup_graphql_call('design_management/get_design_list', { fullPath: @project.full_path, iid: @issue.iid.to_s, atVersion: nil })
- add_page_startup_graphql_call('design_management/design_permissions', { fullPath: @project.full_path, iid: @issue.iid.to_s })
.js-design-management{ data: { project_path: @project.full_path, issue_iid: @issue.iid, issue_path: project_issue_path(@project, @issue) } }
.js-design-management{ data: { project_path: @project.full_path,
issue_iid: @issue.iid,
issue_path: project_issue_path(@project, @issue),
register_path: new_user_registration_path(redirect_to_referer: 'yes'),
sign_in_path: new_session_path(:user, redirect_to_referer: 'yes') } }
- else
.gl-border-solid.gl-border-1.gl-border-gray-100.gl-rounded-base.gl-mt-5.gl-p-3.gl-text-center
= enable_lfs_message

View File

@ -64,9 +64,9 @@
for this project.
- if issuable.new_record?
= form.submit "#{_('Create')} #{issuable.class.model_name.human.downcase}", class: 'gl-button btn btn-confirm gl-mr-2', data: { qa_selector: 'issuable_create_button' }
= form.submit "#{_('Create')} #{issuable.class.model_name.human.downcase}", class: 'gl-button btn btn-confirm gl-mr-2', data: { qa_selector: 'issuable_create_button', track_experiment: 'promote_mr_approvals_in_free', track_action: 'click_button', track_label: 'submit_mr', track_value: 0 }
- else
= form.submit _('Save changes'), class: 'gl-button btn btn-confirm gl-mr-2'
= form.submit _('Save changes'), class: 'gl-button btn btn-confirm gl-mr-2', data: { track_experiment: 'promote_mr_approvals_in_free', track_action: 'click_button', track_label: 'submit_mr', track_value: 0 }
- if issuable.new_record?
= link_to _('Cancel'), polymorphic_path([@project, issuable.class]), class: 'btn gl-button btn-default'

View File

@ -5,30 +5,30 @@
.col-sm-2.col-form-label
= f.label :title
.col-sm-10
= f.text_field :title, class: "form-control js-label-title qa-label-title", required: true, autofocus: true
= f.text_field :title, class: "gl-form-input form-control js-label-title qa-label-title", required: true, autofocus: true
= render_if_exists 'shared/labels/create_label_help_text'
.form-group.row
.col-sm-2.col-form-label
= f.label :description
.col-sm-10
= f.text_field :description, class: "form-control js-quick-submit qa-label-description"
= f.text_field :description, class: "gl-form-input form-control js-quick-submit qa-label-description"
.form-group.row
.col-sm-2.col-form-label
= f.label :color, "Background color"
= f.label :color, _("Background color")
.col-sm-10
.input-group
.input-group-prepend
.input-group-text.label-color-preview &nbsp;
= f.text_field :color, class: "form-control qa-label-color"
= f.text_field :color, class: "gl-form-input form-control qa-label-color"
.form-text.text-muted
Choose any color.
= _('Choose any color.')
%br
Or you can choose one of the suggested colors below
= _("Or you can choose one of the suggested colors below")
= render_suggested_colors
.form-actions
- if @label.persisted?
= f.submit _('Save changes'), class: 'btn gl-button btn-confirm js-save-button'
- else
= f.submit 'Create label', class: 'btn gl-button btn-confirm js-save-button qa-label-create-button'
= f.submit _('Create label'), class: 'btn gl-button btn-confirm js-save-button qa-label-create-button'
= link_to _('Cancel'), back_path, class: 'btn gl-button btn-default btn-cancel'

View File

@ -4,12 +4,14 @@
= form.label :url, s_('Webhooks|URL'), class: 'label-bold'
= form.text_field :url, class: 'form-control gl-form-input', placeholder: 'http://example.com/trigger-ci.json'
%p.form-text.text-muted
= s_('Webhooks|URL must be percent-encoded if neccessary.')
= s_('Webhooks|URL must be percent-encoded if it contains one or more special characters.')
.form-group
= form.label :token, s_('Webhooks|Secret token'), class: 'label-bold'
= form.text_field :token, class: 'form-control gl-form-input', placeholder: ''
%p.form-text.text-muted
= s_('Webhooks|Use this token to validate received payloads. It is sent with the request in the X-Gitlab-Token HTTP header.')
- code_start = '<code>'.html_safe
- code_end = '</code>'.html_safe
= s_('Webhooks|Used to validate received payloads. Sent with the request in the %{code_start}X-Gitlab-Token HTTP%{code_end} header.').html_safe % { code_start: code_start, code_end: code_end }
.form-group
= form.label :url, s_('Webhooks|Trigger'), class: 'label-bold'
%ul.list-unstyled.gl-ml-6
@ -19,37 +21,37 @@
%strong= s_('Webhooks|Push events')
= form.text_field :push_events_branch_filter, class: 'form-control gl-form-input', placeholder: 'Branch name or wildcard pattern to trigger on (leave blank for all)'
%p.text-muted.gl-ml-1
= s_('Webhooks|URL is triggered by a push to the repository')
= s_('Webhooks|Push to the repository.')
%li
= form.check_box :tag_push_events, class: 'form-check-input'
= form.label :tag_push_events, class: 'list-label form-check-label gl-ml-1' do
%strong= s_('Webhooks|Tag push events')
%p.text-muted.gl-ml-1
= s_('Webhooks|URL is triggered when a new tag is pushed to the repository')
= s_('Webhooks|A new tag is pushed to the repository.')
%li
= form.check_box :note_events, class: 'form-check-input'
= form.label :note_events, class: 'list-label form-check-label gl-ml-1' do
%strong= s_('Webhooks|Comments')
%p.text-muted.gl-ml-1
= s_('Webhooks|URL is triggered when someone adds a comment')
= s_('Webhooks|A comment is added to an issue.')
%li
= form.check_box :confidential_note_events, class: 'form-check-input'
= form.label :confidential_note_events, class: 'list-label form-check-label gl-ml-1' do
%strong= s_('Webhooks|Confidential comments')
%p.text-muted.gl-ml-1
= s_('Webhooks|URL is triggered when someone adds a comment on a confidential issue')
= s_('Webhooks|A comment is added to a confidential issue.')
%li
= form.check_box :issues_events, class: 'form-check-input'
= form.label :issues_events, class: 'list-label form-check-label gl-ml-1' do
%strong= s_('Webhooks|Issues events')
%p.text-muted.gl-ml-1
= s_('Webhooks|URL is triggered when an issue is created, updated, closed, or reopened')
= s_('Webhooks|An issue is created, updated, closed, or reopened.')
%li
= form.check_box :confidential_issues_events, class: 'form-check-input'
= form.label :confidential_issues_events, class: 'list-label form-check-label gl-ml-1' do
%strong= s_('Webhooks|Confidential issues events')
%p.text-muted.gl-ml-1
= s_('Webhooks|URL is triggered when a confidential issue is created, updated, closed, or reopened')
= s_('Webhooks|A confidential issue is created, updated, closed, or reopened.')
- if @group
= render_if_exists 'groups/hooks/member_events', form: form
= render_if_exists 'groups/hooks/subgroup_events', form: form
@ -58,43 +60,43 @@
= form.label :merge_requests_events, class: 'list-label form-check-label gl-ml-1' do
%strong= s_('Webhooks|Merge request events')
%p.text-muted.gl-ml-1
= s_('Webhooks|URL is triggered when a merge request is created, updated, or merged')
= s_('Webhooks|A merge request is created, updated, or merged.')
%li
= form.check_box :job_events, class: 'form-check-input'
= form.label :job_events, class: 'list-label form-check-label gl-ml-1' do
%strong= s_('Webhooks|Job events')
%p.text-muted.gl-ml-1
= s_('Webhooks|URL is triggered when the job status changes')
= s_("Webhooks|A job's status changes.")
%li
= form.check_box :pipeline_events, class: 'form-check-input'
= form.label :pipeline_events, class: 'list-label form-check-label gl-ml-1' do
%strong= s_('Webhooks|Pipeline events')
%p.text-muted.gl-ml-1
= s_('Webhooks|URL is triggered when the pipeline status changes')
= s_("Webhooks|A pipeline's status changes.")
%li
= form.check_box :wiki_page_events, class: 'form-check-input'
= form.label :wiki_page_events, class: 'list-label form-check-label gl-ml-1' do
%strong= s_('Webhooks|Wiki page events')
%p.text-muted.gl-ml-1
= s_('Webhooks|URL is triggered when a wiki page is created or updated')
= s_('Webhooks|A wiki page is created or updated.')
%li
= form.check_box :deployment_events, class: 'form-check-input'
= form.label :deployment_events, class: 'list-label form-check-label gl-ml-1' do
%strong= s_('Webhooks|Deployment events')
%p.text-muted.gl-ml-1
= s_('Webhooks|URL is triggered when a deployment starts, finishes, fails, or is canceled')
= s_('Webhooks|A deployment starts, finishes, fails, or is canceled.')
%li
= form.check_box :feature_flag_events, class: 'form-check-input'
= form.label :feature_flag_events, class: 'list-label form-check-label gl-ml-1' do
%strong= s_('Webhooks|Feature flag events')
%p.text-muted.gl-ml-1
= s_('Webhooks|URL is triggered when a feature flag is turned on or off')
= s_('Webhooks|A feature flag is turned on or off.')
%li
= form.check_box :releases_events, class: 'form-check-input'
= form.label :releases_events, class: 'list-label form-check-label gl-ml-1' do
%strong= s_('Webhooks|Releases events')
%p.text-muted.gl-ml-1
= s_('Webhooks|URL is triggered when a release is created or updated')
= s_('Webhooks|A release is created or updated.')
.form-group
= form.label :enable_ssl_verification, s_('Webhooks|SSL verification'), class: 'label-bold checkbox'
.form-check

View File

@ -11,4 +11,4 @@
= render 'shared/web_hooks/hook', hook: hook
- else
%p.text-center.gl-mt-3.gl-mb-3
= _('No webhooks found, add one in the form above.')
= _('No webhooks enabled. Select trigger events above.')

View File

@ -12701,15 +12701,23 @@ Represents a package details in the Package Registry. Note that this type is in
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="packagedetailstypecandestroy"></a>`canDestroy` | [`Boolean!`](#boolean) | Whether the user can destroy the package. |
| <a id="packagedetailstypecomposerconfigrepositoryurl"></a>`composerConfigRepositoryUrl` | [`String`](#string) | Url of the Composer setup endpoint. |
| <a id="packagedetailstypecomposerurl"></a>`composerUrl` | [`String`](#string) | Url of the Composer endpoint. |
| <a id="packagedetailstypeconanurl"></a>`conanUrl` | [`String`](#string) | Url of the Conan project endpoint. |
| <a id="packagedetailstypecreatedat"></a>`createdAt` | [`Time!`](#time) | Date of creation. |
| <a id="packagedetailstypedependencylinks"></a>`dependencyLinks` | [`PackageDependencyLinkConnection`](#packagedependencylinkconnection) | Dependency link. (see [Connections](#connections)) |
| <a id="packagedetailstypeid"></a>`id` | [`PackagesPackageID!`](#packagespackageid) | ID of the package. |
| <a id="packagedetailstypemavenurl"></a>`mavenUrl` | [`String`](#string) | Url of the Maven project endpoint. |
| <a id="packagedetailstypemetadata"></a>`metadata` | [`PackageMetadata`](#packagemetadata) | Package metadata. |
| <a id="packagedetailstypename"></a>`name` | [`String!`](#string) | Name of the package. |
| <a id="packagedetailstypenpmurl"></a>`npmUrl` | [`String`](#string) | Url of the NPM project endpoint. |
| <a id="packagedetailstypenugeturl"></a>`nugetUrl` | [`String`](#string) | Url of the Nuget project endpoint. |
| <a id="packagedetailstypepackagefiles"></a>`packageFiles` | [`PackageFileConnection`](#packagefileconnection) | Package files. (see [Connections](#connections)) |
| <a id="packagedetailstypepackagetype"></a>`packageType` | [`PackageTypeEnum!`](#packagetypeenum) | Package type. |
| <a id="packagedetailstypepipelines"></a>`pipelines` **{warning-solid}** | [`PipelineConnection`](#pipelineconnection) | **Deprecated** in 14.6. Due to scalability concerns, this field is going to be removed. |
| <a id="packagedetailstypeproject"></a>`project` | [`Project!`](#project) | Project where the package is stored. |
| <a id="packagedetailstypepypisetupurl"></a>`pypiSetupUrl` | [`String`](#string) | Url of the PyPi project setup endpoint. |
| <a id="packagedetailstypepypiurl"></a>`pypiUrl` | [`String`](#string) | Url of the PyPi project endpoint. |
| <a id="packagedetailstypestatus"></a>`status` | [`PackageStatus!`](#packagestatus) | Package status. |
| <a id="packagedetailstypetags"></a>`tags` | [`PackageTagConnection`](#packagetagconnection) | Package tags. (see [Connections](#connections)) |
| <a id="packagedetailstypeupdatedat"></a>`updatedAt` | [`Time!`](#time) | Date of most recent update. |

View File

@ -189,24 +189,6 @@ are loaded dynamically with webpack.
Do not use `innerHTML`, `append()` or `html()` to set content. It opens up too many
vulnerabilities.
## Avoid single-line conditional statements
Indentation is important when scanning code as it gives a quick indication of the existence of branches, loops, and return points.
This can help to quickly understand the control flow.
```javascript
// bad
if (isThingNull) return '';
if (isThingNull)
return '';
// good
if (isThingNull) {
return '';
}
```
## ESLint
ESLint behavior can be found in our [tooling guide](../tooling.md).

View File

@ -21,11 +21,10 @@ you can use webhooks to:
every time an issue is created for a specific project or group in GitLab.
- [Automatically assign labels to merge requests](https://about.gitlab.com/blog/2016/08/19/applying-gitlab-labels-automatically/).
You can configure your GitLab project or [group](#group-webhooks) to trigger
a percent-encoded webhook URL when an event occurs. For example, when new code
is pushed or a new issue is created.
The webhook listens for specific [events](#events) and
GitLab sends a POST request with data to the webhook URL.
You can configure your GitLab project or [group](#group-webhooks) to trigger a
[percent-encoded](https://developer.mozilla.org/en-US/docs/Glossary/percent-encoding) webhook URL
when an event occurs. For example, when new code is pushed or a new issue is created. The webhook
listens for specific [events](#events) and GitLab sends a POST request with data to the webhook URL.
Usually, you set up your own [webhook receiver](#create-an-example-webhook-receiver)
to receive information from GitLab and send it to another app, according to your requirements.
@ -55,7 +54,7 @@ You can configure a webhook for a group or a project.
1. In your project or group, on the left sidebar, select **Settings > Webhooks**.
1. In **URL**, enter the URL of the webhook endpoint.
The URL must be percentage-encoded, if necessary.
The URL must be percent-encoded if it contains one or more special characters.
1. In **Secret token**, enter the [secret token](#validate-payloads-by-using-a-secret-token) to validate payloads.
1. In the **Trigger** section, select the [events](webhook_events.md) to trigger the webhook.
1. Optional. Clear the **Enable SSL verification** checkbox to disable [SSL verification](#verify-an-ssl-certificate).

View File

@ -2,41 +2,6 @@
namespace :gitlab do
namespace :gitaly do
desc 'Installs gitaly for running tests within gitlab-development-kit'
task :test_install, [:dir, :storage_path, :repo] => :gitlab_environment do |t, args|
inside_gdk = Rails.env.test? && File.exist?(Rails.root.join('../GDK_ROOT'))
if ENV['FORCE_GITALY_INSTALL'] || !inside_gdk
Rake::Task["gitlab:gitaly:install"].invoke(*args)
next
end
gdk_gitaly_dir = ENV.fetch('GDK_GITALY', Rails.root.join('../gitaly'))
# Our test setup expects a git repo, so clone rather than copy
clone_repo(gdk_gitaly_dir, args.dir, clone_opts: %w[--depth 1]) unless Dir.exist?(args.dir)
# We assume the GDK gitaly already compiled binaries
build_dir = File.join(gdk_gitaly_dir, '_build')
FileUtils.cp_r(build_dir, args.dir)
# We assume the GDK gitaly already ran bundle install
bundle_dir = File.join(gdk_gitaly_dir, 'ruby', '.bundle')
FileUtils.cp_r(bundle_dir, File.join(args.dir, 'ruby'))
# For completeness we copy this for gitaly's make target
ruby_bundle_file = File.join(gdk_gitaly_dir, '.ruby-bundle')
FileUtils.cp_r(ruby_bundle_file, args.dir)
gitaly_binary = File.join(build_dir, 'bin', 'gitaly')
warn_gitaly_out_of_date!(gitaly_binary, Gitlab::GitalyClient.expected_server_version)
rescue Errno::ENOENT => e
puts "Could not copy files, did you run `gdk update`? Error: #{e.message}"
raise
end
desc 'GitLab | Gitaly | Clone and checkout gitaly'
task :clone, [:dir, :storage_path, :repo] => :gitlab_environment do |t, args|
warn_user_is_not_gitlab
@ -60,9 +25,6 @@ Usage: rake "gitlab:gitaly:install[/installation/dir,/storage/path]")
storage_paths = { 'default' => args.storage_path }
Gitlab::SetupHelper::Gitaly.create_configuration(args.dir, storage_paths)
# In CI we run scripts/gitaly-test-build
next if ENV['CI'].present?
Dir.chdir(args.dir) do
Bundler.with_original_env do
env = { "RUBYOPT" => nil, "BUNDLE_GEMFILE" => nil }

View File

@ -16,14 +16,6 @@ class VersionCheck
{ "REFERER": Gitlab.config.gitlab.url }
end
# This is temporary and will be removed when the new UI is hooked up
# to the version_check.json endpoint.
def self.image_url
encoded_data = Base64.urlsafe_encode64(data.to_json)
"#{host}/check.svg?gitlab_info=#{encoded_data}"
end
def self.url
encoded_data = Base64.urlsafe_encode64(data.to_json)

View File

@ -10039,6 +10039,9 @@ msgstr ""
msgid "Create iteration"
msgstr ""
msgid "Create label"
msgstr ""
msgid "Create list"
msgstr ""
@ -24101,7 +24104,7 @@ msgstr ""
msgid "No webhook events"
msgstr ""
msgid "No webhooks found, add one in the form above."
msgid "No webhooks enabled. Select trigger events above."
msgstr ""
msgid "No worries, you can still use all the %{strong}%{plan_name}%{strong_close} features for now. You have %{remaining_days} day to renew your subscription."
@ -26467,6 +26470,12 @@ msgstr ""
msgid "Please %{link_to_register} or %{link_to_sign_in} to comment"
msgstr ""
msgid "Please %{registerLinkStart}register%{registerLinkEnd} or %{signInLinkStart}sign in%{signInLinkEnd} to reply."
msgstr ""
msgid "Please %{registerLinkStart}register%{registerLinkEnd} or %{signInLinkStart}sign in%{signInLinkEnd} to start a new discussion."
msgstr ""
msgid "Please %{startTagRegister}register%{endRegisterTag} or %{startTagSignIn}sign in%{endSignInTag} to reply"
msgstr ""
@ -39107,6 +39116,15 @@ msgstr ""
msgid "Version %{versionNumber} (latest)"
msgstr ""
msgid "VersionCheck|Up to date"
msgstr ""
msgid "VersionCheck|Update ASAP"
msgstr ""
msgid "VersionCheck|Update available"
msgstr ""
msgid "View Documentation"
msgstr ""
@ -39779,6 +39797,48 @@ msgstr ""
msgid "Webhooks Help"
msgstr ""
msgid "Webhooks|A comment is added to a confidential issue."
msgstr ""
msgid "Webhooks|A comment is added to an issue."
msgstr ""
msgid "Webhooks|A confidential issue is created, updated, closed, or reopened."
msgstr ""
msgid "Webhooks|A deployment starts, finishes, fails, or is canceled."
msgstr ""
msgid "Webhooks|A feature flag is turned on or off."
msgstr ""
msgid "Webhooks|A group member is created, updated, or removed."
msgstr ""
msgid "Webhooks|A job's status changes."
msgstr ""
msgid "Webhooks|A merge request is created, updated, or merged."
msgstr ""
msgid "Webhooks|A new tag is pushed to the repository."
msgstr ""
msgid "Webhooks|A pipeline's status changes."
msgstr ""
msgid "Webhooks|A release is created or updated."
msgstr ""
msgid "Webhooks|A subgroup is created or removed."
msgstr ""
msgid "Webhooks|A wiki page is created or updated."
msgstr ""
msgid "Webhooks|An issue is created, updated, closed, or reopened."
msgstr ""
msgid "Webhooks|Comments"
msgstr ""
@ -39821,6 +39881,9 @@ msgstr ""
msgid "Webhooks|Push events"
msgstr ""
msgid "Webhooks|Push to the repository."
msgstr ""
msgid "Webhooks|Releases events"
msgstr ""
@ -39851,55 +39914,10 @@ msgstr ""
msgid "Webhooks|URL"
msgstr ""
msgid "Webhooks|URL is triggered by a push to the repository"
msgid "Webhooks|URL must be percent-encoded if it contains one or more special characters."
msgstr ""
msgid "Webhooks|URL is triggered when a confidential issue is created, updated, closed, or reopened"
msgstr ""
msgid "Webhooks|URL is triggered when a deployment starts, finishes, fails, or is canceled"
msgstr ""
msgid "Webhooks|URL is triggered when a feature flag is turned on or off"
msgstr ""
msgid "Webhooks|URL is triggered when a group member is created, updated, or removed"
msgstr ""
msgid "Webhooks|URL is triggered when a merge request is created, updated, or merged"
msgstr ""
msgid "Webhooks|URL is triggered when a new tag is pushed to the repository"
msgstr ""
msgid "Webhooks|URL is triggered when a release is created or updated"
msgstr ""
msgid "Webhooks|URL is triggered when a subgroup is created or removed"
msgstr ""
msgid "Webhooks|URL is triggered when a wiki page is created or updated"
msgstr ""
msgid "Webhooks|URL is triggered when an issue is created, updated, closed, or reopened"
msgstr ""
msgid "Webhooks|URL is triggered when someone adds a comment"
msgstr ""
msgid "Webhooks|URL is triggered when someone adds a comment on a confidential issue"
msgstr ""
msgid "Webhooks|URL is triggered when the job status changes"
msgstr ""
msgid "Webhooks|URL is triggered when the pipeline status changes"
msgstr ""
msgid "Webhooks|URL must be percent-encoded if neccessary."
msgstr ""
msgid "Webhooks|Use this token to validate received payloads. It is sent with the request in the X-Gitlab-Token HTTP header."
msgid "Webhooks|Used to validate received payloads. Sent with the request in the %{code_start}X-Gitlab-Token HTTP%{code_end} header."
msgstr ""
msgid "Webhooks|Webhook failed to connect"

View File

@ -9,6 +9,7 @@ module QA
extend self
attr_writer :personal_access_token, :admin_personal_access_token
attr_accessor :dry_run
ENV_VARIABLES = Gitlab::QA::Runtime::Env::ENV_VARIABLES

View File

@ -30,6 +30,13 @@ module QA
Runtime::Scenario.define(opt.name, value)
end
next
elsif opt.name == :count_examples_only
parser.on(opt.arg, opt.desc) do |value|
QA::Runtime::Env.dry_run = true
Runtime::Scenario.define(opt.name, value)
end
next
end

View File

@ -13,6 +13,7 @@ module QA
'Specify FEATURE_FLAGS as comma-separated flag=state pairs, e.g., "flag1=enabled,flag2=disabled"'
attribute :parallel, '--parallel', 'Execute tests in parallel'
attribute :loop, '--loop', 'Execute test repeatedly'
attribute :count_examples_only, '--count-examples-only', 'Return the number of examples without running them'
end
end
end

View File

@ -40,7 +40,8 @@ module QA
##
# Perform before hooks, which are different for CE and EE
#
Runtime::Release.perform_before_hooks
Runtime::Release.perform_before_hooks unless Runtime::Env.dry_run
Runtime::Feature.enable(options[:enable_feature]) if options.key?(:enable_feature)
Runtime::Feature.disable(options[:disable_feature]) if options.key?(:disable_feature) && (@feature_enabled = Runtime::Feature.enabled?(options[:disable_feature]))

View File

@ -9,6 +9,7 @@ module QA
attr_accessor :tty, :tags, :options
DEFAULT_TEST_PATH_ARGS = ['--', File.expand_path('./features', __dir__)].freeze
DEFAULT_STD_ARGS = [$stderr, $stdout].freeze
def initialize
@tty = false
@ -68,8 +69,15 @@ module QA
ParallelRunner.run(args.flatten)
elsif Runtime::Scenario.attributes[:loop]
LoopRunner.run(args.flatten)
elsif Runtime::Scenario.attributes[:count_examples_only]
args.unshift('--dry-run')
out = StringIO.new
RSpec::Core::Runner.run(args.flatten, $stderr, out).tap do |status|
abort if status.nonzero?
end
$stdout.puts out.string.match(/(\d+) examples,/)[1]
else
RSpec::Core::Runner.run(args.flatten, $stderr, $stdout).tap do |status|
RSpec::Core::Runner.run(args.flatten, *DEFAULT_STD_ARGS).tap do |status|
abort if status.nonzero?
end
end

View File

@ -2,7 +2,7 @@
RSpec.describe QA::Scenario::Test::Integration::Github do
describe '#perform' do
let(:env) { spy('Runtime::Env', knapsack?: false) }
let(:env) { spy('Runtime::Env', knapsack?: false, dry_run: false) }
before do
stub_const('QA::Runtime::Env', env)

View File

@ -12,7 +12,7 @@ QaDeprecationToolkitEnv.configure!
Knapsack::Adapters::RSpecAdapter.bind if QA::Runtime::Env.knapsack?
QA::Runtime::Browser.configure!
QA::Runtime::Browser.configure! unless QA::Runtime::Env.dry_run
QA::Runtime::AllureReport.configure!
QA::Runtime::Scenario.from_env(QA::Runtime::Env.runtime_scenario_attributes)

View File

@ -28,6 +28,26 @@ RSpec.describe QA::Specs::Runner do
end
end
context 'when count_examples_only is set as an option' do
let(:out) { StringIO.new }
before do
QA::Runtime::Scenario.define(:count_examples_only, true)
out.string = '22 examples,'
allow(StringIO).to receive(:new).and_return(out)
end
it 'sets the `--dry-run` flag' do
expect_rspec_runner_arguments(['--dry-run', '--tag', '~orchestrated', '--tag', '~transient', '--tag', '~geo', *described_class::DEFAULT_TEST_PATH_ARGS], [$stderr, anything])
subject.perform
end
after do
QA::Runtime::Scenario.attributes.delete(:count_examples_only)
end
end
context 'when tags are set' do
subject { described_class.new.tap { |runner| runner.tags = %i[orchestrated github] } }
@ -158,10 +178,10 @@ RSpec.describe QA::Specs::Runner do
end
end
def expect_rspec_runner_arguments(arguments)
def expect_rspec_runner_arguments(arguments, std_arguments = described_class::DEFAULT_STD_ARGS)
expect(RSpec::Core::Runner).to receive(:run)
.with(arguments, $stderr, $stdout)
.and_return(0)
.with(arguments, *std_arguments)
.and_return(0)
end
end
end

View File

@ -13,8 +13,6 @@ class GitalyTestBuild
include GitalySetup
def run
set_bundler_config
# If we have the binaries from the cache, we can skip building them again
if File.exist?(tmp_tests_gitaly_bin_dir)
GitalySetup::LOGGER.debug "Gitaly binary already built. Skip building...\n"

View File

@ -9,17 +9,8 @@ class GitalyTestSpawn
include GitalySetup
def run
set_bundler_config
install_gitaly_gems if ENV['CI']
check_gitaly_config!
# # Uncomment line below to see all gitaly logs merged into CI trace
# spawn('sleep 1; tail -f log/gitaly-test.log')
# In local development this pid file is used by rspec.
IO.write(File.expand_path('../tmp/tests/gitaly.pid', __dir__), start_gitaly)
IO.write(File.expand_path('../tmp/tests/gitaly2.pid', __dir__), start_gitaly2)
IO.write(File.expand_path('../tmp/tests/praefect.pid', __dir__), start_praefect)
install_gitaly_gems
spawn_gitaly
end
end

View File

@ -59,7 +59,7 @@ RSpec.describe 'admin issues labels' do
it 'creates new label' do
fill_in 'Title', with: 'support'
fill_in 'Background color', with: '#F95610'
click_button 'Save'
click_button 'Create label'
page.within '.manage-labels-list' do
expect(page).to have_content('support')
@ -69,7 +69,7 @@ RSpec.describe 'admin issues labels' do
it 'does not creates label with invalid color' do
fill_in 'Title', with: 'support'
fill_in 'Background color', with: '#12'
click_button 'Save'
click_button 'Create label'
page.within '.label-form' do
expect(page).to have_content('Color must be a valid color code')
@ -79,7 +79,7 @@ RSpec.describe 'admin issues labels' do
it 'does not creates label if label already exists' do
fill_in 'Title', with: 'bug'
fill_in 'Background color', with: '#F95610'
click_button 'Save'
click_button 'Create label'
page.within '.label-form' do
expect(page).to have_content 'Title has already been taken'
@ -93,7 +93,7 @@ RSpec.describe 'admin issues labels' do
fill_in 'Title', with: 'fix'
fill_in 'Background color', with: '#F15610'
click_button 'Save'
click_button 'Save changes'
page.within '.manage-labels-list' do
expect(page).to have_content('fix')

View File

@ -28,21 +28,20 @@ RSpec.describe 'Help Pages' do
end
end
context 'in a production environment with version check enabled' do
describe 'with version check enabled' do
let_it_be(:user) { create(:user) }
before do
stub_application_setting(version_check_enabled: true)
allow(User).to receive(:single_user).and_return(double(user, requires_usage_stats_consent?: false))
allow(user).to receive(:can_read_all_resources?).and_return(true)
stub_rails_env('production')
allow(VersionCheck).to receive(:image_url).and_return('/version-check-url')
sign_in(create(:user))
sign_in(user)
visit help_path
end
it 'has a version check image' do
# Check `data-src` due to lazy image loading
expect(find('.js-version-status-badge', visible: false)['data-src'])
.to end_with('/version-check-url')
it 'renders the version check badge' do
expect(page).to have_selector('.js-gitlab-version-check')
end
end

View File

@ -149,6 +149,30 @@
}
}
}
},
"npmUrl": {
"type": "string"
},
"mavenUrl": {
"type": "string"
},
"conanUrl": {
"type": "string"
},
"nugetUrl": {
"type": "string"
},
"pypiUrl": {
"type": "string"
},
"pypiSetupUrl": {
"type": "string"
},
"composerUrl": {
"type": "string"
},
"composerConfigRepositoryUrl": {
"type": "string"
}
}
}

View File

@ -0,0 +1,41 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DesignNoteSignedOut renders message containing register and sign-in links while user wants to reply to a discussion 1`] = `
<div
class="disabled-comment text-center"
>
Please
<gl-link-stub
href="/users/sign_up?redirect_to_referer=yes"
>
register
</gl-link-stub>
or
<gl-link-stub
href="/users/sign_in?redirect_to_referer=yes"
>
sign in
</gl-link-stub>
to reply.
</div>
`;
exports[`DesignNoteSignedOut renders message containing register and sign-in links while user wants to start a new discussion 1`] = `
<div
class="disabled-comment text-center"
>
Please
<gl-link-stub
href="/users/sign_up?redirect_to_referer=yes"
>
register
</gl-link-stub>
or
<gl-link-stub
href="/users/sign_in?redirect_to_referer=yes"
>
sign in
</gl-link-stub>
to start a new discussion.
</div>
`;

View File

@ -1,7 +1,9 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { ApolloMutation } from 'vue-apollo';
import DesignDiscussion from '~/design_management/components/design_notes/design_discussion.vue';
import DesignNote from '~/design_management/components/design_notes/design_note.vue';
import DesignNoteSignedOut from '~/design_management/components/design_notes/design_note_signed_out.vue';
import DesignReplyForm from '~/design_management/components/design_notes/design_reply_form.vue';
import ToggleRepliesWidget from '~/design_management/components/design_notes/toggle_replies_widget.vue';
import createNoteMutation from '~/design_management/graphql/mutations/create_note.mutation.graphql';
@ -20,6 +22,7 @@ const defaultMockDiscussion = {
const DEFAULT_TODO_COUNT = 2;
describe('Design discussions component', () => {
const originalGon = window.gon;
let wrapper;
const findDesignNotes = () => wrapper.findAll(DesignNote);
@ -31,6 +34,7 @@ describe('Design discussions component', () => {
const findResolvedMessage = () => wrapper.find('[data-testid="resolved-message"]');
const findResolveLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findResolveCheckbox = () => wrapper.find('[data-testid="resolve-checkbox"]');
const findApolloMutation = () => wrapper.findComponent(ApolloMutation);
const mutationVariables = {
mutation: createNoteMutation,
@ -42,6 +46,8 @@ describe('Design discussions component', () => {
},
},
};
const registerPath = '/users/sign_up?redirect_to_referer=yes';
const signInPath = '/users/sign_in?redirect_to_referer=yes';
const mutate = jest.fn().mockResolvedValue({ data: { createNote: { errors: [] } } });
const readQuery = jest.fn().mockReturnValue({
project: {
@ -62,6 +68,8 @@ describe('Design discussions component', () => {
designId: 'design-id',
discussionIndex: 1,
discussionWithOpenForm: '',
registerPath,
signInPath,
...props,
},
data() {
@ -88,8 +96,13 @@ describe('Design discussions component', () => {
});
}
beforeEach(() => {
window.gon = { current_user_id: 1 };
});
afterEach(() => {
wrapper.destroy();
window.gon = originalGon;
});
describe('when discussion is not resolvable', () => {
@ -349,4 +362,41 @@ describe('Design discussions component', () => {
expect(wrapper.emitted('open-form')).toBeTruthy();
});
describe('when user is not logged in', () => {
const findDesignNoteSignedOut = () => wrapper.findComponent(DesignNoteSignedOut);
beforeEach(() => {
window.gon = { current_user_id: null };
createComponent(
{
discussion: {
...defaultMockDiscussion,
},
discussionWithOpenForm: defaultMockDiscussion.id,
},
{ discussionComment: 'test', isFormRendered: true },
);
});
it('does not render resolve discussion button', () => {
expect(findResolveButton().exists()).toBe(false);
});
it('does not render replace-placeholder component', () => {
expect(findReplyPlaceholder().exists()).toBe(false);
});
it('does not render apollo-mutation component', () => {
expect(findApolloMutation().exists()).toBe(false);
});
it('renders design-note-signed-out component', () => {
expect(findDesignNoteSignedOut().exists()).toBe(true);
expect(findDesignNoteSignedOut().props()).toMatchObject({
registerPath,
signInPath,
});
});
});
});

View File

@ -0,0 +1,36 @@
import { GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import DesignNoteSignedOut from '~/design_management/components/design_notes/design_note_signed_out.vue';
function createComponent(isAddDiscussion = false) {
return shallowMount(DesignNoteSignedOut, {
propsData: {
registerPath: '/users/sign_up?redirect_to_referer=yes',
signInPath: '/users/sign_in?redirect_to_referer=yes',
isAddDiscussion,
},
stubs: {
GlSprintf,
},
});
}
describe('DesignNoteSignedOut', () => {
let wrapper;
afterEach(() => {
wrapper.destroy();
});
it('renders message containing register and sign-in links while user wants to reply to a discussion', () => {
wrapper = createComponent();
expect(wrapper.element).toMatchSnapshot();
});
it('renders message containing register and sign-in links while user wants to start a new discussion', () => {
wrapper = createComponent(true);
expect(wrapper.element).toMatchSnapshot();
});
});

View File

@ -15,6 +15,7 @@ const mockOverlayData = {
};
describe('Design management design presentation component', () => {
const originalGon = window.gon;
let wrapper;
function createComponent(
@ -115,8 +116,13 @@ describe('Design management design presentation component', () => {
});
}
beforeEach(() => {
window.gon = { current_user_id: 1 };
});
afterEach(() => {
wrapper.destroy();
window.gon = originalGon;
});
it('renders image and overlay when image provided', () => {
@ -552,4 +558,23 @@ describe('Design management design presentation component', () => {
});
});
});
describe('when user is not logged in', () => {
beforeEach(() => {
window.gon = { current_user_id: null };
createComponent(
{
image: 'test.jpg',
imageName: 'test',
},
mockOverlayData,
);
});
it('disables commenting from design overlay', () => {
expect(wrapper.findComponent(DesignOverlay).props()).toMatchObject({
disableCommenting: true,
});
});
});
});

View File

@ -2,6 +2,7 @@ import { GlCollapse, GlPopover } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Cookies from 'js-cookie';
import DesignDiscussion from '~/design_management/components/design_notes/design_discussion.vue';
import DesignNoteSignedOut from '~/design_management/components/design_notes/design_note_signed_out.vue';
import DesignSidebar from '~/design_management/components/design_sidebar.vue';
import DesignTodoButton from '~/design_management/components/design_todo_button.vue';
import updateActiveDiscussionMutation from '~/design_management/graphql/mutations/update_active_discussion.mutation.graphql';
@ -30,6 +31,7 @@ const cookieKey = 'hide_design_resolved_comments_popover';
const mutate = jest.fn().mockResolvedValue();
describe('Design management design sidebar component', () => {
const originalGon = window.gon;
let wrapper;
const findDiscussions = () => wrapper.findAll(DesignDiscussion);
@ -58,11 +60,20 @@ describe('Design management design sidebar component', () => {
},
},
stubs: { GlPopover },
provide: {
registerPath: '/users/sign_up?redirect_to_referer=yes',
signInPath: '/users/sign_in?redirect_to_referer=yes',
},
});
}
beforeEach(() => {
window.gon = { current_user_id: 1 };
});
afterEach(() => {
wrapper.destroy();
window.gon = originalGon;
});
it('renders participants', () => {
@ -248,4 +259,44 @@ describe('Design management design sidebar component', () => {
expect(Cookies.set).toHaveBeenCalledWith(cookieKey, 'true', { expires: 365 * 10 });
});
});
describe('when user is not logged in', () => {
const findDesignNoteSignedOut = () => wrapper.findComponent(DesignNoteSignedOut);
beforeEach(() => {
window.gon = { current_user_id: null };
});
describe('design has no discussions', () => {
beforeEach(() => {
createComponent({
design: {
...design,
discussions: {
nodes: [],
},
},
});
});
it('does not render a message about possibility to create a new discussion', () => {
expect(findNewDiscussionDisclaimer().exists()).toBe(false);
});
it('renders design-note-signed-out component', () => {
expect(findDesignNoteSignedOut().exists()).toBe(true);
});
});
describe('design has discussions', () => {
beforeEach(() => {
Cookies.set(cookieKey, true);
createComponent();
});
it('renders design-note-signed-out component', () => {
expect(findDesignNoteSignedOut().exists()).toBe(true);
});
});
});
});

View File

@ -70,6 +70,13 @@ exports[`Design management design index page renders design index 1`] = `
<!---->
<design-note-signed-out-stub
class="gl-mb-4"
isadddiscussion="true"
registerpath=""
signinpath=""
/>
<design-discussion-stub
data-testid="unresolved-discussion"
designid="gid::/gitlab/Design/1"
@ -77,6 +84,8 @@ exports[`Design management design index page renders design index 1`] = `
discussionwithopenform=""
markdownpreviewpath="/project-path/preview_markdown?target_type=Issue"
noteableid="gid::/gitlab/Design/1"
registerpath=""
signinpath=""
/>
<gl-button-stub
@ -126,6 +135,8 @@ exports[`Design management design index page renders design index 1`] = `
discussionwithopenform=""
markdownpreviewpath="/project-path/preview_markdown?target_type=Issue"
noteableid="gid::/gitlab/Design/1"
registerpath=""
signinpath=""
/>
</gl-collapse-stub>
@ -231,14 +242,14 @@ exports[`Design management design index page with error GlAlert is rendered in c
participants="[object Object]"
/>
<h2
class="new-discussion-disclaimer gl-font-base gl-m-0 gl-mb-4"
data-testid="new-discussion-disclaimer"
>
Click the image where you'd like to start a new discussion
</h2>
<!---->
<design-note-signed-out-stub
class="gl-mb-4"
isadddiscussion="true"
registerpath=""
signinpath=""
/>
<!---->

View File

@ -91,6 +91,8 @@ const designToMove = {
};
describe('Design management index page', () => {
const registerPath = '/users/sign_up?redirect_to_referer=yes';
const signInPath = '/users/sign_in?redirect_to_referer=yes';
let mutate;
let wrapper;
let fakeApollo;
@ -164,6 +166,8 @@ describe('Design management index page', () => {
provide: {
projectPath: 'project-path',
issueIid: '1',
registerPath,
signInPath,
},
});
}
@ -186,6 +190,10 @@ describe('Design management index page', () => {
apolloProvider: fakeApollo,
router,
stubs: { VueDraggable },
provide: {
registerPath,
signInPath,
},
});
}

View File

@ -4,7 +4,7 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import IssueTypeField, { i18n } from '~/issues/show/components/fields/type.vue';
import { IssuableTypes } from '~/issues/show/constants';
import { issuableTypes } from '~/issues/show/constants';
import {
getIssueStateQueryResponse,
updateIssueStateQueryResponse,
@ -69,8 +69,8 @@ describe('Issue type field component', () => {
it.each`
at | text | icon
${0} | ${IssuableTypes[0].text} | ${IssuableTypes[0].icon}
${1} | ${IssuableTypes[1].text} | ${IssuableTypes[1].icon}
${0} | ${issuableTypes[0].text} | ${issuableTypes[0].icon}
${1} | ${issuableTypes[1].text} | ${issuableTypes[1].icon}
`(`renders the issue type $text with an icon in the dropdown`, ({ at, text, icon }) => {
expect(findTypeFromDropDownItemIconAt(at).attributes('name')).toBe(icon);
expect(findTypeFromDropDownItemAt(at).text()).toBe(text);
@ -81,20 +81,20 @@ describe('Issue type field component', () => {
});
it('renders a form select with the `issue_type` value', () => {
expect(findTypeFromDropDown().attributes('value')).toBe(IssuableTypes.issue);
expect(findTypeFromDropDown().attributes('value')).toBe(issuableTypes.issue);
});
describe('with Apollo cache mock', () => {
it('renders the selected issueType', async () => {
mockIssueStateData.mockResolvedValue(getIssueStateQueryResponse);
await waitForPromises();
expect(findTypeFromDropDown().attributes('value')).toBe(IssuableTypes.issue);
expect(findTypeFromDropDown().attributes('value')).toBe(issuableTypes.issue);
});
it('updates the `issue_type` in the apollo cache when the value is changed', async () => {
findTypeFromDropDownItems().at(1).vm.$emit('click', IssuableTypes.incident);
findTypeFromDropDownItems().at(1).vm.$emit('click', issuableTypes.incident);
await wrapper.vm.$nextTick();
expect(findTypeFromDropDown().attributes('value')).toBe(IssuableTypes.incident);
expect(findTypeFromDropDown().attributes('value')).toBe(issuableTypes.incident);
});
describe('when user is a guest', () => {
@ -104,7 +104,7 @@ describe('Issue type field component', () => {
expect(findTypeFromDropDownItemAt(0).isVisible()).toBe(true);
expect(findTypeFromDropDownItemAt(1).isVisible()).toBe(false);
expect(findTypeFromDropDown().attributes('value')).toBe(IssuableTypes.issue);
expect(findTypeFromDropDown().attributes('value')).toBe(issuableTypes.issue);
});
it('and incident is selected, includes incident in the dropdown', async () => {
@ -113,7 +113,7 @@ describe('Issue type field component', () => {
expect(findTypeFromDropDownItemAt(0).isVisible()).toBe(true);
expect(findTypeFromDropDownItemAt(1).isVisible()).toBe(true);
expect(findTypeFromDropDown().attributes('value')).toBe(IssuableTypes.incident);
expect(findTypeFromDropDown().attributes('value')).toBe(issuableTypes.incident);
});
});
});

View File

@ -8,7 +8,7 @@ import { IssuableType } from '~/vue_shared/issuable/show/constants';
import DeleteIssueModal from '~/issues/show/components/delete_issue_modal.vue';
import HeaderActions from '~/issues/show/components/header_actions.vue';
import { IssuableStatus } from '~/issues/constants';
import { IssueStateEvent } from '~/issues/show/constants';
import { ISSUE_STATE_EVENT_CLOSE, ISSUE_STATE_EVENT_REOPEN } from '~/issues/show/constants';
import promoteToEpicMutation from '~/issues/show/queries/promote_to_epic.mutation.graphql';
import * as urlUtility from '~/lib/utils/url_utility';
import eventHub from '~/notes/event_hub';
@ -118,8 +118,8 @@ describe('HeaderActions component', () => {
describe('close/reopen button', () => {
describe.each`
description | issueState | buttonText | newIssueState
${`when the ${issueType} is open`} | ${IssuableStatus.Open} | ${`Close ${issueType}`} | ${IssueStateEvent.Close}
${`when the ${issueType} is closed`} | ${IssuableStatus.Closed} | ${`Reopen ${issueType}`} | ${IssueStateEvent.Reopen}
${`when the ${issueType} is open`} | ${IssuableStatus.Open} | ${`Close ${issueType}`} | ${ISSUE_STATE_EVENT_CLOSE}
${`when the ${issueType} is closed`} | ${IssuableStatus.Closed} | ${`Reopen ${issueType}`} | ${ISSUE_STATE_EVENT_REOPEN}
`('$description', ({ issueState, buttonText, newIssueState }) => {
beforeEach(() => {
dispatchEventSpy = jest.spyOn(document, 'dispatchEvent');
@ -306,7 +306,7 @@ describe('HeaderActions component', () => {
input: {
iid: defaultProps.iid,
projectPath: defaultProps.projectPath,
stateEvent: IssueStateEvent.Close,
stateEvent: ISSUE_STATE_EVENT_CLOSE,
},
},
}),
@ -345,7 +345,7 @@ describe('HeaderActions component', () => {
input: {
iid: defaultProps.iid.toString(),
projectPath: defaultProps.projectPath,
stateEvent: IssueStateEvent.Close,
stateEvent: ISSUE_STATE_EVENT_CLOSE,
},
},
}),

View File

@ -1,5 +1,5 @@
import { nextTick } from 'vue';
import { GlLoadingIcon, GlModal } from '@gitlab/ui';
import { GlLoadingIcon, GlModal, GlAlert, GlButton } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
@ -31,25 +31,28 @@ describe('WikiForm', () => {
const findContent = () => wrapper.find('#wiki_content');
const findMessage = () => wrapper.find('#wiki_message');
const findSubmitButton = () => wrapper.findByTestId('wiki-submit-button');
const findCancelButton = () => wrapper.findByRole('link', { name: 'Cancel' });
const findUseNewEditorButton = () => wrapper.findByRole('button', { name: 'Use the new editor' });
const findCancelButton = () => wrapper.findByTestId('wiki-cancel-button');
const findUseNewEditorButton = () => wrapper.findByText('Use the new editor');
const findToggleEditingModeButton = () => wrapper.findByTestId('toggle-editing-mode-button');
const findDismissContentEditorAlertButton = () =>
wrapper.findByRole('button', { name: 'Try this later' });
const findDismissContentEditorAlertButton = () => wrapper.findByText('Try this later');
const findSwitchToOldEditorButton = () =>
wrapper.findByRole('button', { name: 'Switch me back to the classic editor.' });
const findTitleHelpLink = () => wrapper.findByRole('link', { name: 'Learn more.' });
const findTitleHelpLink = () => wrapper.findByText('Learn more.');
const findMarkdownHelpLink = () => wrapper.findByTestId('wiki-markdown-help-link');
const findContentEditor = () => wrapper.findComponent(ContentEditor);
const findClassicEditor = () => wrapper.findComponent(MarkdownField);
const setFormat = (value) => {
const format = findFormat();
format.find(`option[value=${value}]`).setSelected();
format.element.dispatchEvent(new Event('change'));
return format.find(`option[value=${value}]`).setSelected();
};
const triggerFormSubmit = () => findForm().element.dispatchEvent(new Event('submit'));
const triggerFormSubmit = () => {
findForm().element.dispatchEvent(new Event('submit'));
return nextTick();
};
const dispatchBeforeUnload = () => {
const e = new Event('beforeunload');
@ -84,34 +87,14 @@ describe('WikiForm', () => {
Org: 'org',
};
function createWrapper(
function createWrapper({
mountFn = shallowMount,
persisted = false,
{ pageInfo, glFeatures = { wikiSwitchBetweenContentEditorRawMarkdown: false } } = {},
) {
pageInfo,
glFeatures = { wikiSwitchBetweenContentEditorRawMarkdown: false },
} = {}) {
wrapper = extendedWrapper(
mount(
WikiForm,
{
provide: {
formatOptions,
glFeatures,
pageInfo: {
...(persisted ? pageInfoPersisted : pageInfoNew),
...pageInfo,
},
},
},
{ attachToDocument: true },
),
);
}
const createShallowWrapper = (
persisted = false,
{ pageInfo, glFeatures = { wikiSwitchBetweenContentEditorRawMarkdown: false } } = {},
) => {
wrapper = extendedWrapper(
shallowMount(WikiForm, {
mountFn(WikiForm, {
provide: {
formatOptions,
glFeatures,
@ -122,10 +105,12 @@ describe('WikiForm', () => {
},
stubs: {
MarkdownField,
GlAlert,
GlButton,
},
}),
);
};
}
beforeEach(() => {
trackingSpy = mockTracking(undefined, null, jest.spyOn);
@ -147,26 +132,24 @@ describe('WikiForm', () => {
`(
'updates the commit message to $message when title is $title and persisted=$persisted',
async ({ title, message, persisted }) => {
createWrapper(persisted);
createWrapper({ persisted });
findTitle().setValue(title);
await wrapper.vm.$nextTick();
await findTitle().setValue(title);
expect(findMessage().element.value).toBe(message);
},
);
it('sets the commit message to "Update My page" when the page first loads when persisted', async () => {
createWrapper(true);
createWrapper({ persisted: true });
await wrapper.vm.$nextTick();
await nextTick();
expect(findMessage().element.value).toBe('Update My page');
});
it('does not trim page content by default', () => {
createWrapper(true);
createWrapper({ persisted: true });
expect(findContent().element.value).toBe(' My page content ');
});
@ -178,20 +161,16 @@ describe('WikiForm', () => {
${'asciidoc'} | ${'link:page-slug[Link title]'}
${'org'} | ${'[[page-slug]]'}
`('updates the link help message when format=$value is selected', async ({ value, text }) => {
createWrapper();
createWrapper({ mountFn: mount });
setFormat(value);
await wrapper.vm.$nextTick();
await setFormat(value);
expect(wrapper.text()).toContain(text);
});
it('starts with no unload warning', async () => {
it('starts with no unload warning', () => {
createWrapper();
await wrapper.vm.$nextTick();
const e = dispatchBeforeUnload();
expect(typeof e.returnValue).not.toBe('string');
expect(e.preventDefault).not.toHaveBeenCalled();
@ -203,20 +182,16 @@ describe('WikiForm', () => {
${false} | ${'You can specify the full path for the new file. We will automatically create any missing directories.'} | ${'/help/user/project/wiki/index#create-a-new-wiki-page'}
`(
'shows appropriate title help text and help link for when persisted=$persisted',
async ({ persisted, titleHelpLink, titleHelpText }) => {
createWrapper(persisted);
await wrapper.vm.$nextTick();
({ persisted, titleHelpLink, titleHelpText }) => {
createWrapper({ persisted });
expect(wrapper.text()).toContain(titleHelpText);
expect(findTitleHelpLink().attributes().href).toBe(titleHelpLink);
},
);
it('shows correct link for wiki specific markdown docs', async () => {
createWrapper();
await wrapper.vm.$nextTick();
it('shows correct link for wiki specific markdown docs', () => {
createWrapper({ mountFn: mount });
expect(findMarkdownHelpLink().attributes().href).toBe(
'/help/user/markdown#wiki-specific-markdown',
@ -225,12 +200,11 @@ describe('WikiForm', () => {
describe('when wiki content is updated', () => {
beforeEach(async () => {
createWrapper(true);
createWrapper({ mountFn: mount, persisted: true });
const input = findContent();
input.setValue(' Lorem ipsum dolar sit! ');
await input.trigger('input');
await input.setValue(' Lorem ipsum dolar sit! ');
});
it('sets before unload warning', () => {
@ -241,17 +215,15 @@ describe('WikiForm', () => {
describe('form submit', () => {
beforeEach(async () => {
triggerFormSubmit();
await wrapper.vm.$nextTick();
await triggerFormSubmit();
});
it('when form submitted, unsets before unload warning', async () => {
it('when form submitted, unsets before unload warning', () => {
const e = dispatchBeforeUnload();
expect(e.preventDefault).not.toHaveBeenCalled();
});
it('triggers wiki format tracking event', async () => {
it('triggers wiki format tracking event', () => {
expect(trackingSpy).toHaveBeenCalledTimes(1);
});
@ -264,22 +236,20 @@ describe('WikiForm', () => {
describe('submit button state', () => {
it.each`
title | content | buttonState | disabledAttr
${'something'} | ${'something'} | ${'enabled'} | ${undefined}
${''} | ${'something'} | ${'disabled'} | ${'disabled'}
${'something'} | ${''} | ${'disabled'} | ${'disabled'}
${''} | ${''} | ${'disabled'} | ${'disabled'}
${' '} | ${' '} | ${'disabled'} | ${'disabled'}
${'something'} | ${'something'} | ${'enabled'} | ${false}
${''} | ${'something'} | ${'disabled'} | ${true}
${'something'} | ${''} | ${'disabled'} | ${true}
${''} | ${''} | ${'disabled'} | ${true}
${' '} | ${' '} | ${'disabled'} | ${true}
`(
"when title='$title', content='$content', then the button is $buttonState'",
async ({ title, content, disabledAttr }) => {
createWrapper();
findTitle().setValue(title);
findContent().setValue(content);
await findTitle().setValue(title);
await findContent().setValue(content);
await wrapper.vm.$nextTick();
expect(findSubmitButton().attributes().disabled).toBe(disabledAttr);
expect(findSubmitButton().props().disabled).toBe(disabledAttr);
},
);
@ -288,7 +258,7 @@ describe('WikiForm', () => {
${true} | ${'Save changes'}
${false} | ${'Create page'}
`('when persisted=$persisted, label is set to $buttonLabel', ({ persisted, buttonLabel }) => {
createWrapper(persisted);
createWrapper({ persisted });
expect(findSubmitButton().text()).toBe(buttonLabel);
});
@ -302,7 +272,7 @@ describe('WikiForm', () => {
`(
'when persisted=$persisted, redirects the user to appropriate path',
({ persisted, redirectLink }) => {
createWrapper(persisted);
createWrapper({ persisted });
expect(findCancelButton().attributes().href).toBe(redirectLink);
},
@ -311,7 +281,7 @@ describe('WikiForm', () => {
describe('when wikiSwitchBetweenContentEditorRawMarkdown feature flag is not enabled', () => {
beforeEach(() => {
createShallowWrapper(true, {
createWrapper({
glFeatures: { wikiSwitchBetweenContentEditorRawMarkdown: false },
});
});
@ -323,7 +293,7 @@ describe('WikiForm', () => {
describe('when wikiSwitchBetweenContentEditorRawMarkdown feature flag is enabled', () => {
beforeEach(() => {
createShallowWrapper(true, {
createWrapper({
glFeatures: { wikiSwitchBetweenContentEditorRawMarkdown: true },
});
});
@ -404,10 +374,6 @@ describe('WikiForm', () => {
});
describe('wiki content editor', () => {
beforeEach(() => {
createWrapper(true);
});
it.each`
format | buttonExists
${'markdown'} | ${true}
@ -415,15 +381,17 @@ describe('WikiForm', () => {
`(
'gl-alert containing "use new editor" button exists: $buttonExists if format is $format',
async ({ format, buttonExists }) => {
setFormat(format);
createWrapper();
await wrapper.vm.$nextTick();
await setFormat(format);
expect(findUseNewEditorButton().exists()).toBe(buttonExists);
},
);
it('gl-alert containing "use new editor" button is dismissed on clicking dismiss button', async () => {
createWrapper();
await findDismissContentEditorAlertButton().trigger('click');
expect(findUseNewEditorButton().exists()).toBe(false);
@ -442,22 +410,24 @@ describe('WikiForm', () => {
);
};
it('shows classic editor by default', assertOldEditorIsVisible);
it('shows classic editor by default', () => {
createWrapper({ persisted: true });
assertOldEditorIsVisible();
});
describe('switch format to rdoc', () => {
beforeEach(async () => {
setFormat('rdoc');
createWrapper({ persisted: true });
await wrapper.vm.$nextTick();
await setFormat('rdoc');
});
it('continues to show the classic editor', assertOldEditorIsVisible);
describe('switch format back to markdown', () => {
beforeEach(async () => {
setFormat('rdoc');
await wrapper.vm.$nextTick();
await setFormat('markdown');
});
it(
@ -469,6 +439,7 @@ describe('WikiForm', () => {
describe('clicking "use new editor": editor fails to load', () => {
beforeEach(async () => {
createWrapper({ mountFn: mount });
mock.onPost(/preview-markdown/).reply(400);
await findUseNewEditorButton().trigger('click');
@ -494,10 +465,12 @@ describe('WikiForm', () => {
});
describe('clicking "use new editor": editor loads successfully', () => {
beforeEach(() => {
beforeEach(async () => {
createWrapper({ persisted: true, mountFn: mount });
mock.onPost(/preview-markdown/).reply(200, { body: '<p>hello <strong>world</strong></p>' });
findUseNewEditorButton().trigger('click');
await findUseNewEditorButton().trigger('click');
});
it('shows a tip to send feedback', () => {
@ -542,48 +515,42 @@ describe('WikiForm', () => {
});
it('unsets before unload warning on form submit', async () => {
triggerFormSubmit();
await nextTick();
await triggerFormSubmit();
const e = dispatchBeforeUnload();
expect(e.preventDefault).not.toHaveBeenCalled();
});
});
it('triggers tracking events on form submit', async () => {
triggerFormSubmit();
it('triggers tracking events on form submit', async () => {
await triggerFormSubmit();
await wrapper.vm.$nextTick();
expect(trackingSpy).toHaveBeenCalledWith(undefined, SAVED_USING_CONTENT_EDITOR_ACTION, {
label: WIKI_CONTENT_EDITOR_TRACKING_LABEL,
});
expect(trackingSpy).toHaveBeenCalledWith(undefined, SAVED_USING_CONTENT_EDITOR_ACTION, {
label: WIKI_CONTENT_EDITOR_TRACKING_LABEL,
expect(trackingSpy).toHaveBeenCalledWith(undefined, WIKI_FORMAT_UPDATED_ACTION, {
label: WIKI_FORMAT_LABEL,
extra: {
value: findFormat().element.value,
old_format: pageInfoPersisted.format,
project_path: pageInfoPersisted.path,
},
});
});
expect(trackingSpy).toHaveBeenCalledWith(undefined, WIKI_FORMAT_UPDATED_ACTION, {
label: WIKI_FORMAT_LABEL,
extra: {
value: findFormat().element.value,
old_format: pageInfoPersisted.format,
project_path: pageInfoPersisted.path,
},
it('updates content from content editor on form submit', async () => {
// old value
expect(findContent().element.value).toBe(' My page content ');
// wait for content editor to load
await waitForPromises();
await triggerFormSubmit();
expect(findContent().element.value).toBe('hello **world**');
});
});
it('updates content from content editor on form submit', async () => {
// old value
expect(findContent().element.value).toBe(' My page content ');
// wait for content editor to load
await waitForPromises();
triggerFormSubmit();
await wrapper.vm.$nextTick();
expect(findContent().element.value).toBe('hello **world**');
});
describe('clicking "switch to classic editor"', () => {
let modal;

View File

@ -1,42 +0,0 @@
import $ from 'jquery';
import ClassSpecHelper from 'helpers/class_spec_helper';
import VersionCheckImage from '~/version_check_image';
describe('VersionCheckImage', () => {
let testContext;
beforeEach(() => {
testContext = {};
});
describe('bindErrorEvent', () => {
ClassSpecHelper.itShouldBeAStaticMethod(VersionCheckImage, 'bindErrorEvent');
beforeEach(() => {
testContext.imageElement = $('<div></div>');
});
it('registers an error event', () => {
jest.spyOn($.prototype, 'on').mockImplementation(() => {});
// eslint-disable-next-line func-names
jest.spyOn($.prototype, 'off').mockImplementation(function () {
return this;
});
VersionCheckImage.bindErrorEvent(testContext.imageElement);
expect($.prototype.off).toHaveBeenCalledWith('error');
expect($.prototype.on).toHaveBeenCalledWith('error', expect.any(Function));
});
it('hides the imageElement on error', () => {
jest.spyOn($.prototype, 'hide').mockImplementation(() => {});
VersionCheckImage.bindErrorEvent(testContext.imageElement);
testContext.imageElement.trigger('error');
expect($.prototype.hide).toHaveBeenCalled();
});
});
});

View File

@ -0,0 +1,77 @@
import { GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import flushPromises from 'helpers/flush_promises';
import axios from '~/lib/utils/axios_utils';
import GitlabVersionCheck from '~/vue_shared/components/gitlab_version_check.vue';
describe('GitlabVersionCheck', () => {
let wrapper;
let mock;
const defaultResponse = {
code: 200,
res: { severity: 'success' },
};
const createComponent = (mockResponse) => {
const response = {
...defaultResponse,
...mockResponse,
};
mock = new MockAdapter(axios);
mock.onGet().replyOnce(response.code, response.res);
wrapper = shallowMount(GitlabVersionCheck);
};
afterEach(() => {
wrapper.destroy();
mock.restore();
});
const findGlBadge = () => wrapper.findComponent(GlBadge);
describe('template', () => {
describe.each`
description | mockResponse | renders
${'successful but null'} | ${{ code: 200, res: null }} | ${false}
${'successful and valid'} | ${{ code: 200, res: { severity: 'success' } }} | ${true}
${'an error'} | ${{ code: 500, res: null }} | ${false}
`('version_check.json response', ({ description, mockResponse, renders }) => {
describe(`is ${description}`, () => {
beforeEach(async () => {
createComponent(mockResponse);
await flushPromises(); // Ensure we wrap up the axios call
});
it(`does${renders ? '' : ' not'} render GlBadge`, () => {
expect(findGlBadge().exists()).toBe(renders);
});
});
});
describe.each`
mockResponse | expectedUI
${{ code: 200, res: { severity: 'success' } }} | ${{ title: 'Up to date', variant: 'success' }}
${{ code: 200, res: { severity: 'warning' } }} | ${{ title: 'Update available', variant: 'warning' }}
${{ code: 200, res: { severity: 'danger' } }} | ${{ title: 'Update ASAP', variant: 'danger' }}
`('badge ui', ({ mockResponse, expectedUI }) => {
describe(`when response is ${mockResponse.res.severity}`, () => {
beforeEach(async () => {
createComponent(mockResponse);
await flushPromises(); // Ensure we wrap up the axios call
});
it(`title is ${expectedUI.title}`, () => {
expect(findGlBadge().text()).toBe(expectedUI.title);
});
it(`variant is ${expectedUI.variant}`, () => {
expect(findGlBadge().attributes('variant')).toBe(expectedUI.variant);
});
});
});
});
});

View File

@ -5,7 +5,10 @@ require 'spec_helper'
RSpec.describe GitlabSchema.types['PackageDetailsType'] do
it 'includes all the package fields' do
expected_fields = %w[
id name version created_at updated_at package_type tags project pipelines versions package_files dependency_links
id name version created_at updated_at package_type tags project
pipelines versions package_files dependency_links
npm_url maven_url conan_url nuget_url pypi_url pypi_setup_url
composer_url composer_config_repository_url
]
expect(described_class).to include_graphql_fields(*expected_fields)

View File

@ -3,33 +3,34 @@
require 'spec_helper'
RSpec.describe VersionCheckHelper do
describe '#version_status_badge' do
it 'returns nil if not dev environment and not enabled' do
stub_rails_env('development')
allow(Gitlab::CurrentSettings.current_application_settings).to receive(:version_check_enabled) { false }
let_it_be(:user) { create(:user) }
expect(helper.version_status_badge).to be(nil)
end
context 'when production and enabled' do
before do
stub_rails_env('production')
allow(Gitlab::CurrentSettings.current_application_settings).to receive(:version_check_enabled) { true }
allow(VersionCheck).to receive(:image_url) { 'https://version.host.com/check.svg?gitlab_info=xxx' }
describe '#show_version_check?' do
describe 'return conditions' do
where(:enabled, :consent, :is_admin, :result) do
[
[false, false, false, false],
[false, false, true, false],
[false, true, false, false],
[false, true, true, false],
[true, false, false, false],
[true, false, true, true],
[true, true, false, false],
[true, true, true, false]
]
end
it 'returns an image tag' do
expect(helper.version_status_badge).to start_with('<img')
end
with_them do
before do
stub_application_setting(version_check_enabled: enabled)
allow(User).to receive(:single_user).and_return(double(user, requires_usage_stats_consent?: consent))
allow(helper).to receive(:current_user).and_return(user)
allow(user).to receive(:can_read_all_resources?).and_return(is_admin)
end
it 'has a js prefixed css class' do
expect(helper.version_status_badge)
.to match(/class="js-version-status-badge lazy"/)
end
it 'has a VersionCheck image_url as the src' do
expect(helper.version_status_badge)
.to include(%{src="https://version.host.com/check.svg?gitlab_info=xxx"})
it 'returns correct results' do
expect(helper.show_version_check?).to eq result
end
end
end
end

View File

@ -3,12 +3,6 @@
require 'spec_helper'
RSpec.describe VersionCheck do
describe '.image_url' do
it 'returns the correct URL' do
expect(described_class.image_url).to match(%r{\A#{Regexp.escape(described_class.host)}/check\.svg\?gitlab_info=\w+})
end
end
describe '.url' do
it 'returns the correct URL' do
expect(described_class.url).to match(%r{\A#{Regexp.escape(described_class.host)}/check\.json\?gitlab_info=\w+})

View File

@ -4,7 +4,9 @@ require 'spec_helper'
RSpec.describe 'package details' do
include GraphqlHelpers
let_it_be_with_reload(:project) { create(:project) }
let_it_be_with_reload(:group) { create(:group) }
let_it_be_with_reload(:project) { create(:project, group: group) }
let_it_be(:user) { create(:user) }
let_it_be(:composer_package) { create(:composer_package, project: project) }
let_it_be(:composer_json) { { name: 'name', type: 'type', license: 'license', version: 1 } }
let_it_be(:composer_metadatum) do
@ -17,7 +19,6 @@ RSpec.describe 'package details' do
let(:excluded) { %w[metadata apiFuzzingCiConfiguration pipeline packageFiles] }
let(:metadata) { query_graphql_fragment('ComposerMetadata') }
let(:package_files) {all_graphql_fields_for('PackageFile')}
let(:user) { project.owner }
let(:package_global_id) { global_id_of(composer_package) }
let(:package_details) { graphql_data_at(:package) }
@ -37,94 +38,7 @@ RSpec.describe 'package details' do
subject { post_graphql(query, current_user: user) }
it_behaves_like 'a working graphql query' do
before do
subject
end
it 'matches the JSON schema' do
expect(package_details).to match_schema('graphql/packages/package_details')
end
end
context 'there are other versions of this package' do
let(:depth) { 3 }
let(:excluded) { %w[metadata project tags pipelines] } # to limit the query complexity
let_it_be(:siblings) { create_list(:composer_package, 2, project: project, name: composer_package.name) }
it 'includes the sibling versions' do
subject
expect(graphql_data_at(:package, :versions, :nodes)).to match_array(
siblings.map { |p| a_hash_including('id' => global_id_of(p)) }
)
end
context 'going deeper' do
let(:depth) { 6 }
it 'does not create a cycle of versions' do
subject
expect(graphql_data_at(:package, :versions, :nodes, :version)).to be_present
expect(graphql_data_at(:package, :versions, :nodes, :versions, :nodes)).to eq [nil, nil]
end
end
end
context 'with package files pending destruction' do
let_it_be(:package_file) { create(:package_file, package: composer_package) }
let_it_be(:package_file_pending_destruction) { create(:package_file, :pending_destruction, package: composer_package) }
let(:package_file_ids) { graphql_data_at(:package, :package_files, :nodes).map { |node| node["id"] } }
it 'does not return them' do
subject
expect(package_file_ids).to contain_exactly(package_file.to_global_id.to_s)
end
context 'with packages_installable_package_files disabled' do
before do
stub_feature_flags(packages_installable_package_files: false)
end
it 'returns them' do
subject
expect(package_file_ids).to contain_exactly(package_file_pending_destruction.to_global_id.to_s, package_file.to_global_id.to_s)
end
end
end
context 'with a batched query' do
let_it_be(:conan_package) { create(:conan_package, project: project) }
let(:batch_query) do
<<~QUERY
{
a: package(id: "#{global_id_of(composer_package)}") { name }
b: package(id: "#{global_id_of(conan_package)}") { name }
}
QUERY
end
let(:a_packages_names) { graphql_data_at(:a, :packages, :nodes, :name) }
it 'returns an error for the second package and data for the first' do
post_graphql(batch_query, current_user: user)
expect(graphql_data_at(:a, :name)).to eq(composer_package.name)
expect_graphql_errors_to_include [/Package details can be requested only for one package at a time/]
expect(graphql_data_at(:b)).to be(nil)
end
end
context 'with unauthorized user' do
let_it_be(:user) { create(:user) }
before do
project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
end
@ -136,71 +50,186 @@ RSpec.describe 'package details' do
end
end
context 'pipelines field', :aggregate_failures do
let(:pipelines) { create_list(:ci_pipeline, 6, project: project) }
let(:pipeline_gids) { pipelines.sort_by(&:id).map(&:to_gid).map(&:to_s).reverse }
context 'with authorized user' do
before do
composer_package.pipelines = pipelines
composer_package.save!
project.add_developer(user)
end
def run_query(args)
pipelines_nodes = <<~QUERY
nodes {
id
}
pageInfo {
startCursor
endCursor
}
QUERY
query = graphql_query_for(:package, { id: package_global_id }, query_graphql_field("pipelines", args, pipelines_nodes))
post_graphql(query, current_user: user)
end
it 'loads the second page with pagination first correctly' do
run_query(first: 2)
pipeline_ids = graphql_data.dig('package', 'pipelines', 'nodes').pluck('id')
expect(pipeline_ids).to eq(pipeline_gids[0..1])
cursor = graphql_data.dig('package', 'pipelines', 'pageInfo', 'endCursor')
run_query(first: 2, after: cursor)
pipeline_ids = graphql_data.dig('package', 'pipelines', 'nodes').pluck('id')
expect(pipeline_ids).to eq(pipeline_gids[2..3])
end
it 'loads the second page with pagination last correctly' do
run_query(last: 2)
pipeline_ids = graphql_data.dig('package', 'pipelines', 'nodes').pluck('id')
expect(pipeline_ids).to eq(pipeline_gids[4..5])
cursor = graphql_data.dig('package', 'pipelines', 'pageInfo', 'startCursor')
run_query(last: 2, before: cursor)
pipeline_ids = graphql_data.dig('package', 'pipelines', 'nodes').pluck('id')
expect(pipeline_ids).to eq(pipeline_gids[2..3])
end
context 'with unauthorized user' do
let_it_be(:user) { create(:user) }
it_behaves_like 'a working graphql query' do
before do
project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
subject
end
it 'returns no packages' do
run_query(first: 2)
it 'matches the JSON schema' do
expect(package_details).to match_schema('graphql/packages/package_details')
end
end
expect(graphql_data_at(:package)).to be_nil
context 'there are other versions of this package' do
let(:depth) { 3 }
let(:excluded) { %w[metadata project tags pipelines] } # to limit the query complexity
let_it_be(:siblings) { create_list(:composer_package, 2, project: project, name: composer_package.name) }
it 'includes the sibling versions' do
subject
expect(graphql_data_at(:package, :versions, :nodes)).to match_array(
siblings.map { |p| a_hash_including('id' => global_id_of(p)) }
)
end
context 'going deeper' do
let(:depth) { 6 }
it 'does not create a cycle of versions' do
subject
expect(graphql_data_at(:package, :versions, :nodes, :version)).to be_present
expect(graphql_data_at(:package, :versions, :nodes, :versions, :nodes)).to match_array [nil, nil]
end
end
end
context 'with package files pending destruction' do
let_it_be(:package_file) { create(:package_file, package: composer_package) }
let_it_be(:package_file_pending_destruction) { create(:package_file, :pending_destruction, package: composer_package) }
let(:package_file_ids) { graphql_data_at(:package, :package_files, :nodes).map { |node| node["id"] } }
it 'does not return them' do
subject
expect(package_file_ids).to contain_exactly(package_file.to_global_id.to_s)
end
context 'with packages_installable_package_files disabled' do
before do
stub_feature_flags(packages_installable_package_files: false)
end
it 'returns them' do
subject
expect(package_file_ids).to contain_exactly(package_file_pending_destruction.to_global_id.to_s, package_file.to_global_id.to_s)
end
end
end
context 'with a batched query' do
let_it_be(:conan_package) { create(:conan_package, project: project) }
let(:batch_query) do
<<~QUERY
{
a: package(id: "#{global_id_of(composer_package)}") { name }
b: package(id: "#{global_id_of(conan_package)}") { name }
}
QUERY
end
let(:a_packages_names) { graphql_data_at(:a, :packages, :nodes, :name) }
it 'returns an error for the second package and data for the first' do
post_graphql(batch_query, current_user: user)
expect(graphql_data_at(:a, :name)).to eq(composer_package.name)
expect_graphql_errors_to_include [/Package details can be requested only for one package at a time/]
expect(graphql_data_at(:b)).to be(nil)
end
end
context 'pipelines field', :aggregate_failures do
let(:pipelines) { create_list(:ci_pipeline, 6, project: project) }
let(:pipeline_gids) { pipelines.sort_by(&:id).map(&:to_gid).map(&:to_s).reverse }
before do
composer_package.pipelines = pipelines
composer_package.save!
end
def run_query(args)
pipelines_nodes = <<~QUERY
nodes {
id
}
pageInfo {
startCursor
endCursor
}
QUERY
query = graphql_query_for(:package, { id: package_global_id }, query_graphql_field("pipelines", args, pipelines_nodes))
post_graphql(query, current_user: user)
end
it 'loads the second page with pagination first correctly' do
run_query(first: 2)
pipeline_ids = graphql_data.dig('package', 'pipelines', 'nodes').pluck('id')
expect(pipeline_ids).to eq(pipeline_gids[0..1])
cursor = graphql_data.dig('package', 'pipelines', 'pageInfo', 'endCursor')
run_query(first: 2, after: cursor)
pipeline_ids = graphql_data.dig('package', 'pipelines', 'nodes').pluck('id')
expect(pipeline_ids).to eq(pipeline_gids[2..3])
end
it 'loads the second page with pagination last correctly' do
run_query(last: 2)
pipeline_ids = graphql_data.dig('package', 'pipelines', 'nodes').pluck('id')
expect(pipeline_ids).to eq(pipeline_gids[4..5])
cursor = graphql_data.dig('package', 'pipelines', 'pageInfo', 'startCursor')
run_query(last: 2, before: cursor)
pipeline_ids = graphql_data.dig('package', 'pipelines', 'nodes').pluck('id')
expect(pipeline_ids).to eq(pipeline_gids[2..3])
end
end
context 'package managers paths' do
before do
subject
end
it 'returns npm_url correctly' do
expect(graphql_data_at(:package, :npm_url)).to eq("http://localhost/api/v4/projects/#{project.id}/packages/npm")
end
it 'returns maven_url correctly' do
expect(graphql_data_at(:package, :maven_url)).to eq("http://localhost/api/v4/projects/#{project.id}/packages/maven")
end
it 'returns conan_url correctly' do
expect(graphql_data_at(:package, :conan_url)).to eq("http://localhost/api/v4/projects/#{project.id}/packages/conan")
end
it 'returns nuget_url correctly' do
expect(graphql_data_at(:package, :nuget_url)).to eq("http://localhost/api/v4/projects/#{project.id}/packages/nuget/index.json")
end
it 'returns pypi_url correctly' do
expect(graphql_data_at(:package, :pypi_url)).to eq("http://__token__:<your_personal_token>@localhost/api/v4/projects/#{project.id}/packages/pypi/simple")
end
it 'returns pypi_setup_url correctly' do
expect(graphql_data_at(:package, :pypi_setup_url)).to eq("http://localhost/api/v4/projects/#{project.id}/packages/pypi")
end
it 'returns composer_url correctly' do
expect(graphql_data_at(:package, :composer_url)).to eq("http://localhost/api/v4/group/#{group.id}/-/packages/composer/packages.json")
end
it 'returns composer_config_repository_url correctly' do
expect(graphql_data_at(:package, :composer_config_repository_url)).to eq("localhost/#{group.id}")
end
end
end

View File

@ -459,6 +459,11 @@ RSpec.configure do |config|
end
end
# Ensures that any Javascript script that tries to make the external VersionCheck API call skips it and returns a response
config.before(:each, :js) do
allow_any_instance_of(VersionCheck).to receive(:response).and_return({ "severity" => "success" })
end
config.after(:each, :silence_stdout) do
$stdout = STDOUT
end

View File

@ -9,8 +9,13 @@
require 'securerandom'
require 'socket'
require 'logger'
require 'bundler'
module GitalySetup
extend self
REPOS_STORAGE = 'default'
LOGGER = begin
default_name = ENV['CI'] ? 'DEBUG' : 'WARN'
level_name = ENV['GITLAB_TESTING_LOG_LEVEL']&.upcase
@ -54,9 +59,12 @@ module GitalySetup
{
'HOME' => expand_path('tmp/tests'),
'GEM_PATH' => Gem.path.join(':'),
'BUNDLE_APP_CONFIG' => File.join(gemfile_dir, '.bundle'),
'BUNDLE_INSTALL_FLAGS' => nil,
'BUNDLE_IGNORE_CONFIG' => '1',
'BUNDLE_PATH' => bundle_path,
'BUNDLE_GEMFILE' => gemfile,
'BUNDLE_JOBS' => '4',
'BUNDLE_RETRY' => '3',
'RUBYOPT' => nil,
# Git hooks can't run during tests as the internal API is not running.
@ -65,17 +73,20 @@ module GitalySetup
}
end
# rubocop:disable GitlabSecurity/SystemCommandInjection
def set_bundler_config
system('bundle config set --local jobs 4', chdir: gemfile_dir)
system('bundle config set --local retry 3', chdir: gemfile_dir)
def bundle_path
# Allow the user to override BUNDLE_PATH if they need to
return ENV['GITALY_TEST_BUNDLE_PATH'] if ENV['GITALY_TEST_BUNDLE_PATH']
if ENV['CI']
bundle_path = expand_path('vendor/gitaly-ruby')
system('bundle', 'config', 'set', '--local', 'path', bundle_path, chdir: gemfile_dir)
expand_path('vendor/gitaly-ruby')
else
explicit_path = Bundler.configured_bundle_path.explicit_path
return unless explicit_path
expand_path(explicit_path)
end
end
# rubocop:enable GitlabSecurity/SystemCommandInjection
def config_path(service)
case service
@ -88,6 +99,10 @@ module GitalySetup
end
end
def repos_path(storage = REPOS_STORAGE)
Gitlab.config.repositories.storages[REPOS_STORAGE].legacy_disk_path
end
def service_binary(service)
case service
when :gitaly, :gitaly2
@ -196,4 +211,104 @@ module GitalySetup
raise "could not connect to #{socket}"
end
def gitaly_socket_path
Gitlab::GitalyClient.address(REPOS_STORAGE).delete_prefix('unix:')
end
def gitaly_dir
socket_path = gitaly_socket_path
socket_path = File.expand_path(gitaly_socket_path) if expand_path_for_socket?
File.dirname(socket_path)
end
# Linux fails with "bind: invalid argument" if a UNIX socket path exceeds 108 characters:
# https://github.com/golang/go/issues/6895. We use absolute paths in CI to ensure
# that changes in the current working directory don't affect GRPC reconnections.
def expand_path_for_socket?
!!ENV['CI']
end
def setup_gitaly
unless ENV['CI']
# In CI Gitaly is built in the setup-test-env job and saved in the
# artifacts. So when tests are started, there's no need to build Gitaly.
build_gitaly
end
Gitlab::SetupHelper::Gitaly.create_configuration(
gitaly_dir,
{ 'default' => repos_path },
force: true,
options: {
prometheus_listen_addr: 'localhost:9236'
}
)
Gitlab::SetupHelper::Gitaly.create_configuration(
gitaly_dir,
{ 'default' => repos_path },
force: true,
options: {
internal_socket_dir: File.join(gitaly_dir, "internal_gitaly2"),
gitaly_socket: "gitaly2.socket",
config_filename: "gitaly2.config.toml"
}
)
Gitlab::SetupHelper::Praefect.create_configuration(gitaly_dir, { 'praefect' => repos_path }, force: true)
end
def socket_path(service)
File.join(tmp_tests_gitaly_dir, "#{service}.socket")
end
def praefect_socket_path
"unix:" + socket_path(:praefect)
end
def wait(service)
sleep_time = 10
sleep_interval = 0.1
socket = socket_path(service)
Integer(sleep_time / sleep_interval).times do
Socket.unix(socket)
return
rescue StandardError
sleep sleep_interval
end
raise "could not connect to #{service} at #{socket.inspect} after #{sleep_time} seconds"
end
def stop(pid)
Process.kill('KILL', pid)
rescue Errno::ESRCH
# The process can already be gone if the test run was INTerrupted.
end
def spawn_gitaly
check_gitaly_config!
gitaly_pid = start_gitaly
gitaly2_pid = start_gitaly2
praefect_pid = start_praefect
Kernel.at_exit do
# In CI this function is called by scripts/gitaly-test-spawn, triggered a
# before_script. Gitaly needs to remain running until the container is
# stopped.
next if ENV['CI']
pids = [gitaly_pid, gitaly2_pid, praefect_pid]
pids.each { |pid| stop(pid) }
end
wait('gitaly')
wait('praefect')
rescue StandardError
message = 'gitaly spawn failed'
message += " (try `rm -rf #{gitaly_dir}` ?)" unless ENV['CI']
raise message
end
end

View File

@ -1,6 +1,7 @@
# frozen_string_literal: true
require 'parallel'
require_relative 'gitaly_setup'
module TestEnv
extend self
@ -93,7 +94,6 @@ module TestEnv
}.freeze
TMP_TEST_PATH = Rails.root.join('tmp', 'tests').freeze
REPOS_STORAGE = 'default'
SECOND_STORAGE_PATH = Rails.root.join('tmp', 'tests', 'second_storage')
SETUP_METHODS = %i[setup_gitaly setup_gitlab_shell setup_workhorse setup_factory_repo setup_forked_repo].freeze
@ -128,7 +128,7 @@ module TestEnv
# Can be overriden
def post_init
start_gitaly(gitaly_dir)
start_gitaly
end
# Clean /tmp/tests
@ -142,7 +142,7 @@ module TestEnv
end
FileUtils.mkdir_p(
Gitlab::GitalyClient::StorageSettings.allow_disk_access { TestEnv.repos_path }
Gitlab::GitalyClient::StorageSettings.allow_disk_access { GitalySetup.repos_path }
)
FileUtils.mkdir_p(SECOND_STORAGE_PATH)
FileUtils.mkdir_p(backup_path)
@ -158,111 +158,28 @@ module TestEnv
def setup_gitaly
component_timed_setup('Gitaly',
install_dir: gitaly_dir,
install_dir: GitalySetup.gitaly_dir,
version: Gitlab::GitalyClient.expected_server_version,
task: "gitlab:gitaly:test_install",
task_args: [gitaly_dir, repos_path, gitaly_url].compact) do
Gitlab::SetupHelper::Gitaly.create_configuration(
gitaly_dir,
{ 'default' => repos_path },
force: true,
options: {
prometheus_listen_addr: 'localhost:9236'
}
)
Gitlab::SetupHelper::Gitaly.create_configuration(
gitaly_dir,
{ 'default' => repos_path },
force: true,
options: {
internal_socket_dir: File.join(gitaly_dir, "internal_gitaly2"),
gitaly_socket: "gitaly2.socket",
config_filename: "gitaly2.config.toml"
}
)
Gitlab::SetupHelper::Praefect.create_configuration(gitaly_dir, { 'praefect' => repos_path }, force: true)
end
task: "gitlab:gitaly:clone",
fresh_install: ENV.key?('FORCE_GITALY_INSTALL'),
task_args: [GitalySetup.gitaly_dir, GitalySetup.repos_path, gitaly_url].compact) do
GitalySetup.setup_gitaly
end
end
def gitaly_socket_path
Gitlab::GitalyClient.address('default').sub(/\Aunix:/, '')
end
def gitaly_dir
socket_path = gitaly_socket_path
socket_path = File.expand_path(gitaly_socket_path) if expand_path?
File.dirname(socket_path)
end
# Linux fails with "bind: invalid argument" if a UNIX socket path exceeds 108 characters:
# https://github.com/golang/go/issues/6895. We use absolute paths in CI to ensure
# that changes in the current working directory don't affect GRPC reconnections.
def expand_path?
!!ENV['CI']
end
def start_gitaly(gitaly_dir)
def start_gitaly
if ci?
# Gitaly has been spawned outside this process already
return
end
spawn_script = Rails.root.join('scripts/gitaly-test-spawn').to_s
Bundler.with_original_env do
unless system(spawn_script)
message = 'gitaly spawn failed'
message += " (try `rm -rf #{gitaly_dir}` ?)" unless ci?
raise message
end
end
gitaly_pid = Integer(File.read(TMP_TEST_PATH.join('gitaly.pid')))
gitaly2_pid = Integer(File.read(TMP_TEST_PATH.join('gitaly2.pid')))
praefect_pid = Integer(File.read(TMP_TEST_PATH.join('praefect.pid')))
Kernel.at_exit do
pids = [gitaly_pid, gitaly2_pid, praefect_pid]
pids.each { |pid| stop(pid) }
end
wait('gitaly')
wait('praefect')
end
def stop(pid)
Process.kill('KILL', pid)
rescue Errno::ESRCH
# The process can already be gone if the test run was INTerrupted.
GitalySetup.spawn_gitaly
end
def gitaly_url
ENV.fetch('GITALY_REPO_URL', nil)
end
def socket_path(service)
TMP_TEST_PATH.join('gitaly', "#{service}.socket").to_s
end
def praefect_socket_path
"unix:" + socket_path(:praefect)
end
def wait(service)
sleep_time = 10
sleep_interval = 0.1
socket = socket_path(service)
Integer(sleep_time / sleep_interval).times do
Socket.unix(socket)
return
rescue StandardError
sleep sleep_interval
end
raise "could not connect to #{service} at #{socket.inspect} after #{sleep_time} seconds"
end
# Feature specs are run through Workhorse
def setup_workhorse
# Always rebuild the config file
@ -378,8 +295,7 @@ module TestEnv
def rm_storage_dir(storage, dir)
Gitlab::GitalyClient::StorageSettings.allow_disk_access do
repos_path = Gitlab.config.repositories.storages[storage].legacy_disk_path
target_repo_refs_path = File.join(repos_path, dir)
target_repo_refs_path = File.join(GitalySetup.repos_path(storage), dir)
FileUtils.remove_dir(target_repo_refs_path)
end
rescue Errno::ENOENT
@ -387,8 +303,7 @@ module TestEnv
def storage_dir_exists?(storage, dir)
Gitlab::GitalyClient::StorageSettings.allow_disk_access do
repos_path = Gitlab.config.repositories.storages[storage].legacy_disk_path
File.exist?(File.join(repos_path, dir))
File.exist?(File.join(GitalySetup.repos_path(storage), dir))
end
end
@ -401,7 +316,7 @@ module TestEnv
end
def repos_path
@repos_path ||= Gitlab.config.repositories.storages[REPOS_STORAGE].legacy_disk_path
@repos_path ||= GitalySetup.repos_path
end
def backup_path
@ -522,7 +437,7 @@ module TestEnv
end
end
def component_timed_setup(component, install_dir:, version:, task:, task_args: [])
def component_timed_setup(component, install_dir:, version:, task:, fresh_install: true, task_args: [])
start = Time.now
ensure_component_dir_name_is_correct!(component, install_dir)
@ -532,7 +447,7 @@ module TestEnv
if component_needs_update?(install_dir, version)
# Cleanup the component entirely to ensure we start fresh
FileUtils.rm_rf(install_dir)
FileUtils.rm_rf(install_dir) if fresh_install
if ENV['SKIP_RAILS_ENV_IN_RAKE']
# When we run `scripts/setup-test-env`, we take care of loading the necessary dependencies

View File

@ -1,11 +1,11 @@
# frozen_string_literal: true
require_relative 'helpers/test_env'
require_relative 'helpers/gitaly_setup'
RSpec.configure do |config|
config.before(:each, :praefect) do
allow(Gitlab.config.repositories.storages['default']).to receive(:[]).and_call_original
allow(Gitlab.config.repositories.storages['default']).to receive(:[]).with('gitaly_address')
.and_return(TestEnv.praefect_socket_path)
.and_return(GitalySetup.praefect_socket_path)
end
end

View File

@ -53,11 +53,14 @@ RSpec.describe 'admin/dashboard/index.html.haml' do
expect(rendered).not_to have_content "Users over License"
end
it 'links to the GitLab Changelog' do
stub_application_setting(version_check_enabled: true)
describe 'when show_version_check? is true' do
before do
allow(view).to receive(:show_version_check?).and_return(true)
render
end
render
expect(rendered).to have_link(href: 'https://gitlab.com/gitlab-org/gitlab/-/blob/master/CHANGELOG.md')
it 'renders the version check badge' do
expect(rendered).to have_selector('.js-gitlab-version-check')
end
end
end

View File

@ -76,7 +76,6 @@ RSpec.describe 'help/index' do
def stub_helpers
allow(view).to receive(:markdown).and_return('')
allow(view).to receive(:version_status_badge).and_return('')
allow(view).to receive(:current_application_settings).and_return(Gitlab::CurrentSettings.current_application_settings)
end
end