Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
c68ee79c33
commit
8bac8f55ba
|
@ -1 +1 @@
|
|||
641e24107a6f4f8593b87ed55c3aa7cd76010963
|
||||
3627c14a64ce48446e8a67299c3161ff7290d1ad
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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);
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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!';
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
= 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'
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
= 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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.')
|
||||
|
|
|
@ -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. |
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
`;
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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=""
|
||||
/>
|
||||
|
||||
<!---->
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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+})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue