Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-09-29 12:08:49 +00:00
parent 946b1e2fe9
commit 9bf40d9fdc
108 changed files with 1899 additions and 1045 deletions

View File

@ -61,26 +61,6 @@ Layout/FirstArrayElementIndentation:
- 'spec/models/ci/daily_build_group_report_result_spec.rb'
- 'spec/models/ci/pipeline_spec.rb'
- 'spec/models/ci/runner_version_spec.rb'
- 'spec/models/ci/unit_test_spec.rb'
- 'spec/models/clusters/applications/cert_manager_spec.rb'
- 'spec/models/clusters/platforms/kubernetes_spec.rb'
- 'spec/models/commit_collection_spec.rb'
- 'spec/models/compare_spec.rb'
- 'spec/models/concerns/id_in_ordered_spec.rb'
- 'spec/models/concerns/noteable_spec.rb'
- 'spec/models/diff_note_spec.rb'
- 'spec/models/discussion_spec.rb'
- 'spec/models/group_spec.rb'
- 'spec/models/integration_spec.rb'
- 'spec/models/integrations/chat_message/issue_message_spec.rb'
- 'spec/models/integrations/chat_message/wiki_page_message_spec.rb'
- 'spec/models/integrations/jira_spec.rb'
- 'spec/models/label_note_spec.rb'
- 'spec/models/merge_request/cleanup_schedule_spec.rb'
- 'spec/models/merge_request_diff_spec.rb'
- 'spec/models/merge_request_spec.rb'
- 'spec/models/operations/feature_flags/strategy_spec.rb'
- 'spec/models/project_group_link_spec.rb'
- 'spec/models/repository_spec.rb'
- 'spec/models/user_preference_spec.rb'
- 'spec/models/user_spec.rb'

View File

@ -1 +1 @@
2ff0039f15ef06063925ff6c0406f6e092ad2435
5cba52f4acb04ddbe27d8b7cb2e936ea0be45ae1

View File

@ -0,0 +1,146 @@
<script>
import { GlDropdownItem } from '@gitlab/ui';
export default {
components: {
GlDropdownItem,
},
props: {
char: {
type: String,
required: true,
},
referenceProps: {
type: Object,
required: true,
},
items: {
type: Array,
required: true,
},
command: {
type: Function,
required: true,
},
},
data() {
return {
selectedIndex: 0,
};
},
computed: {
isUser() {
return this.referenceProps.referenceType === 'user';
},
isIssue() {
return this.referenceProps.referenceType === 'issue';
},
isMergeRequest() {
return this.referenceProps.referenceType === 'merge_request';
},
isMilestone() {
return this.referenceProps.referenceType === 'milestone';
},
},
watch: {
items() {
this.selectedIndex = 0;
},
},
methods: {
getReferenceText(item) {
switch (this.referenceProps.referenceType) {
case 'user':
return `${this.char}${item.username}`;
case 'issue':
case 'merge_request':
return `${this.char}${item.iid}`;
case 'milestone':
return `${this.char}${item.title}`;
default:
return '';
}
},
onKeyDown({ event }) {
if (event.key === 'ArrowUp') {
this.upHandler();
return true;
}
if (event.key === 'ArrowDown') {
this.downHandler();
return true;
}
if (event.key === 'Enter') {
this.enterHandler();
return true;
}
return false;
},
upHandler() {
this.selectedIndex = (this.selectedIndex + this.items.length - 1) % this.items.length;
},
downHandler() {
this.selectedIndex = (this.selectedIndex + 1) % this.items.length;
},
enterHandler() {
this.selectItem(this.selectedIndex);
},
selectItem(index) {
const item = this.items[index];
if (item) {
this.command({
text: this.getReferenceText(item),
href: '#',
...this.referenceProps,
});
}
},
},
};
</script>
<template>
<ul
:class="{ show: items.length > 0 }"
class="gl-new-dropdown dropdown-men"
data-testid="content-editor-reference-dropdown"
>
<div class="gl-new-dropdown-inner gl-overflow-y-auto">
<gl-dropdown-item
v-for="(item, index) in items"
:key="index"
:class="{ 'gl-bg-gray-50': index === selectedIndex }"
@click="selectItem(index)"
>
<span v-if="isUser">
{{ item.username }}
<small>{{ item.name }}</small>
</span>
<span v-if="isIssue || isMergeRequest || isMilestone">
<small>{{ item.iid }}</small>
{{ item.title }}
</span>
</gl-dropdown-item>
</div>
</ul>
</template>

View File

@ -1 +1,15 @@
export { Heading as default } from '@tiptap/extension-heading';
import { Heading } from '@tiptap/extension-heading';
import { textblockTypeInputRule } from '@tiptap/core';
export default Heading.extend({
addInputRules() {
return this.options.levels.map((level) => {
return textblockTypeInputRule({
// make sure heading regex doesn't conflict with issue references
find: new RegExp(`^(#{1,${level}})[ \t]$`),
type: this.type,
getAttributes: { level },
});
});
},
});

View File

@ -57,7 +57,7 @@ export default Node.create({
'a',
{
class: node.attrs.className,
href: node.attrs.href,
href: '#',
'data-reference-type': node.attrs.referenceType,
'data-original': node.attrs.originalText,
},

View File

@ -0,0 +1,150 @@
import { Node } from '@tiptap/core';
import { VueRenderer } from '@tiptap/vue-2';
import tippy from 'tippy.js';
import Suggestion from '@tiptap/suggestion';
import { PluginKey } from 'prosemirror-state';
import { isFunction } from 'lodash';
import axios from '~/lib/utils/axios_utils';
import Reference from '../components/reference_dropdown.vue';
function createSuggestionPlugin({ editor, char, dataSource, search, referenceProps }) {
return Suggestion({
editor,
char,
pluginKey: new PluginKey(`reference_${referenceProps.referenceType}`),
command: ({ editor: tiptapEditor, range, props }) => {
tiptapEditor
.chain()
.focus()
.insertContentAt(range, [{ type: 'reference', attrs: props }])
.run();
},
async items({ query }) {
if (!dataSource) return [];
try {
const items = await (isFunction(dataSource) ? dataSource() : axios.get(dataSource));
return items.data.filter(search(query));
} catch {
return [];
}
},
render: () => {
let component;
let popup;
return {
onStart: (props) => {
component = new VueRenderer(Reference, {
propsData: {
...props,
char,
referenceProps,
},
editor: props.editor,
});
if (!props.clientRect) {
return;
}
popup = tippy('body', {
getReferenceClientRect: props.clientRect,
appendTo: () => document.body,
content: component.element,
showOnCreate: true,
interactive: true,
trigger: 'manual',
placement: 'bottom-start',
});
},
onUpdate(props) {
component.updateProps(props);
if (!props.clientRect) {
return;
}
popup?.[0].setProps({
getReferenceClientRect: props.clientRect,
});
},
onKeyDown(props) {
if (props.event.key === 'Escape') {
popup?.[0].hide();
return true;
}
return component.ref?.onKeyDown(props);
},
onExit() {
popup?.[0].destroy();
component.destroy();
},
};
},
});
}
export default Node.create({
name: 'suggestions',
addProseMirrorPlugins() {
return [
createSuggestionPlugin({
editor: this.editor,
char: '@',
dataSource: gl.GfmAutoComplete?.dataSources.members,
referenceProps: {
className: 'gfm gfm-project_member',
referenceType: 'user',
},
search: (query) => ({ name, username }) =>
name.toLocaleLowerCase().includes(query.toLocaleLowerCase()) ||
username.toLocaleLowerCase().includes(query.toLocaleLowerCase()),
}),
createSuggestionPlugin({
editor: this.editor,
char: '#',
dataSource: gl.GfmAutoComplete?.dataSources.issues,
referenceProps: {
className: 'gfm gfm-issue',
referenceType: 'issue',
},
search: (query) => ({ iid, title }) =>
String(iid).toLocaleLowerCase().includes(query.toLocaleLowerCase()) ||
title.toLocaleLowerCase().includes(query.toLocaleLowerCase()),
}),
createSuggestionPlugin({
editor: this.editor,
char: '!',
dataSource: gl.GfmAutoComplete?.dataSources.mergeRequests,
referenceProps: {
className: 'gfm gfm-issue',
referenceType: 'merge_request',
},
search: (query) => ({ iid, title }) =>
String(iid).toLocaleLowerCase().includes(query.toLocaleLowerCase()) ||
title.toLocaleLowerCase().includes(query.toLocaleLowerCase()),
}),
createSuggestionPlugin({
editor: this.editor,
char: '%',
dataSource: gl.GfmAutoComplete?.dataSources.milestones,
referenceProps: {
className: 'gfm gfm-milestone',
referenceType: 'milestone',
},
search: (query) => ({ iid, title }) =>
String(iid).toLocaleLowerCase().includes(query.toLocaleLowerCase()) ||
title.toLocaleLowerCase().includes(query.toLocaleLowerCase()),
}),
];
},
});

View File

@ -46,6 +46,7 @@ import ReferenceDefinition from '../extensions/reference_definition';
import Sourcemap from '../extensions/sourcemap';
import Strike from '../extensions/strike';
import Subscript from '../extensions/subscript';
import Suggestions from '../extensions/suggestions';
import Superscript from '../extensions/superscript';
import Table from '../extensions/table';
import TableCell from '../extensions/table_cell';
@ -133,6 +134,7 @@ export const createContentEditor = ({
Sourcemap,
Strike,
Subscript,
Suggestions,
Superscript,
TableCell,
TableHeader,

View File

@ -38,7 +38,12 @@ export default {
},
},
mounted() {
this.$refs.textarea.focus();
this.focus();
},
methods: {
focus() {
this.$refs.textarea?.focus();
},
},
};
</script>

View File

@ -1,7 +1,6 @@
<script>
import { GlAlert } from '@gitlab/ui';
import $ from 'jquery';
import Autosave from '~/autosave';
import { getDraft, updateDraft, getLockVersion, clearDraft } from '~/lib/utils/autosave';
import { IssuableType } from '~/issues/constants';
import eventHub from '../event_hub';
import EditActions from './edit_actions.vue';
@ -76,10 +75,17 @@ export default {
},
},
data() {
const autosaveKey = [document.location.pathname, document.location.search];
const descriptionAutosaveKey = [...autosaveKey, 'description'];
const titleAutosaveKey = [...autosaveKey, 'title'];
return {
titleAutosaveKey,
descriptionAutosaveKey,
autosaveReset: false,
formData: {
title: this.formState.title,
description: this.formState.description,
title: getDraft(titleAutosaveKey) || this.formState.title,
description: getDraft(descriptionAutosaveKey) || this.formState.description,
},
showOutdatedDescriptionWarning: false,
};
@ -118,58 +124,40 @@ export default {
},
methods: {
initAutosave() {
const {
description: {
$refs: { textarea },
},
title: {
$refs: { input },
},
} = this.$refs;
this.autosaveDescription = new Autosave(
$(textarea),
[document.location.pathname, document.location.search, 'description'],
null,
this.formState.lock_version,
);
const savedLockVersion = this.autosaveDescription.getSavedLockVersion();
const savedLockVersion = getLockVersion(this.descriptionAutosaveKey);
this.showOutdatedDescriptionWarning =
savedLockVersion && String(this.formState.lock_version) !== savedLockVersion;
this.autosaveTitle = new Autosave($(input), [
document.location.pathname,
document.location.search,
'title',
]);
},
resetAutosave() {
this.autosaveDescription.reset();
this.autosaveTitle.reset();
this.autosaveReset = true;
clearDraft(this.descriptionAutosaveKey);
clearDraft(this.titleAutosaveKey);
},
keepAutosave() {
const {
description: {
$refs: { textarea },
},
} = this.$refs;
textarea.focus();
this.$refs.description.focus();
this.showOutdatedDescriptionWarning = false;
},
discardAutosave() {
const {
description: {
$refs: { textarea },
},
} = this.$refs;
textarea.value = this.initialDescriptionText;
textarea.focus();
this.formData.description = this.initialDescriptionText;
clearDraft(this.descriptionAutosaveKey);
this.$refs.description.focus();
this.showOutdatedDescriptionWarning = false;
},
updateTitleDraft(title) {
updateDraft(this.titleAutosaveKey, title);
},
updateDescriptionDraft(description) {
/*
* This conditional statement prevents a race-condition
* between clearing the draft and submitting a new draft
* update while the user is typing. It happens when saving
* using the cmd + enter keyboard shortcut.
*/
if (!this.autosaveReset) {
updateDraft(this.descriptionAutosaveKey, description, this.formState.lock_version);
}
},
},
};
</script>
@ -194,7 +182,7 @@ export default {
>
<div class="row gl-mb-3">
<div class="col-12">
<issuable-title-field ref="title" v-model="formData.title" />
<issuable-title-field ref="title" v-model="formData.title" @input="updateTitleDraft" />
</div>
</div>
<div class="row">
@ -220,6 +208,7 @@ export default {
:markdown-docs-path="markdownDocsPath"
:can-attach-file="canAttachFile"
:enable-autocomplete="enableAutocomplete"
@input="updateDescriptionDraft"
/>
<edit-actions :endpoint="endpoint" :form-state="formState" :issuable-type="issuableType" />

View File

@ -1,8 +1,27 @@
import { isString } from 'lodash';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
const normalizeKey = (autosaveKey) => {
let normalizedKey;
if (Array.isArray(autosaveKey) && autosaveKey.every(isString)) {
normalizedKey = autosaveKey.join('/');
} else if (isString(autosaveKey)) {
normalizedKey = autosaveKey;
} else {
// eslint-disable-next-line @gitlab/require-i18n-strings
throw new Error('Invalid autosave key');
}
return `autosave/${normalizedKey}`;
};
const lockVersionKey = (autosaveKey) => `${normalizeKey(autosaveKey)}/lockVersion`;
export const clearDraft = (autosaveKey) => {
try {
window.localStorage.removeItem(`autosave/${autosaveKey}`);
window.localStorage.removeItem(normalizeKey(autosaveKey));
window.localStorage.removeItem(lockVersionKey(autosaveKey));
} catch (e) {
// eslint-disable-next-line no-console
console.error(e);
@ -11,7 +30,7 @@ export const clearDraft = (autosaveKey) => {
export const getDraft = (autosaveKey) => {
try {
return window.localStorage.getItem(`autosave/${autosaveKey}`);
return window.localStorage.getItem(normalizeKey(autosaveKey));
} catch (e) {
// eslint-disable-next-line no-console
console.error(e);
@ -19,9 +38,22 @@ export const getDraft = (autosaveKey) => {
}
};
export const updateDraft = (autosaveKey, text) => {
export const getLockVersion = (autosaveKey) => {
try {
window.localStorage.setItem(`autosave/${autosaveKey}`, text);
return window.localStorage.getItem(lockVersionKey(autosaveKey));
} catch (e) {
// eslint-disable-next-line no-console
console.error(e);
return null;
}
};
export const updateDraft = (autosaveKey, text, lockVersion) => {
try {
window.localStorage.setItem(normalizeKey(autosaveKey), text);
if (lockVersion) {
window.localStorage.setItem(lockVersionKey(autosaveKey), lockVersion);
}
} catch (e) {
// eslint-disable-next-line no-console
console.error(e);

View File

@ -1,6 +1,6 @@
<script>
import { GlEmptyState } from '@gitlab/ui';
import createFlash from '~/flash';
import { createAlert } from '~/flash';
import { n__ } from '~/locale';
import { joinPaths } from '~/lib/utils/url_utility';
import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
@ -69,7 +69,7 @@ export default {
return this.queryVariables;
},
error() {
createFlash({ message: FETCH_IMAGES_LIST_ERROR_MESSAGE });
createAlert({ message: FETCH_IMAGES_LIST_ERROR_MESSAGE });
},
},
},

View File

@ -1,7 +1,7 @@
<script>
import { GlResizeObserverDirective, GlEmptyState } from '@gitlab/ui';
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
import createFlash from '~/flash';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { joinPaths } from '~/lib/utils/url_utility';
import Tracking from '~/tracking';
@ -66,7 +66,7 @@ export default {
this.updateBreadcrumb();
},
error() {
createFlash({ message: FETCH_IMAGES_LIST_ERROR_MESSAGE });
createAlert({ message: FETCH_IMAGES_LIST_ERROR_MESSAGE });
},
},
},

View File

@ -10,7 +10,7 @@ import {
} from '@gitlab/ui';
import { get } from 'lodash';
import getContainerRepositoriesQuery from 'shared_queries/container_registry/get_container_repositories.query.graphql';
import createFlash from '~/flash';
import { createAlert } from '~/flash';
import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
import Tracking from '~/tracking';
import PersistedSearch from '~/packages_and_registries/shared/components/persisted_search.vue';
@ -100,7 +100,7 @@ export default {
this.containerRepositoriesCount = data[this.graphqlResource]?.containerRepositoriesCount;
},
error() {
createFlash({ message: FETCH_IMAGES_LIST_ERROR_MESSAGE });
createAlert({ message: FETCH_IMAGES_LIST_ERROR_MESSAGE });
},
},
additionalDetails: {
@ -115,7 +115,7 @@ export default {
return data[this.graphqlResource]?.containerRepositories.nodes;
},
error() {
createFlash({ message: FETCH_IMAGES_LIST_ERROR_MESSAGE });
createAlert({ message: FETCH_IMAGES_LIST_ERROR_MESSAGE });
},
},
},

View File

@ -1,5 +1,5 @@
import Api from '~/api';
import createFlash from '~/flash';
import { createAlert, VARIANT_SUCCESS, VARIANT_WARNING } from '~/flash';
import {
DELETE_PACKAGE_ERROR_MESSAGE,
DELETE_PACKAGE_FILE_ERROR_MESSAGE,
@ -20,7 +20,7 @@ export const fetchPackageVersions = ({ commit, state }) => {
}
})
.catch(() => {
createFlash({ message: FETCH_PACKAGE_VERSIONS_ERROR, type: 'warning' });
createAlert({ message: FETCH_PACKAGE_VERSIONS_ERROR, variant: VARIANT_WARNING });
})
.finally(() => {
commit(types.SET_LOADING, false);
@ -33,7 +33,7 @@ export const deletePackage = ({
},
}) => {
return Api.deleteProjectPackage(project_id, id).catch(() => {
createFlash({ message: DELETE_PACKAGE_ERROR_MESSAGE, type: 'warning' });
createAlert({ message: DELETE_PACKAGE_ERROR_MESSAGE, variant: VARIANT_WARNING });
});
};
@ -51,9 +51,9 @@ export const deletePackageFile = (
.then(() => {
const filtered = packageFiles.filter((f) => f.id !== fileId);
commit(types.UPDATE_PACKAGE_FILES, filtered);
createFlash({ message: DELETE_PACKAGE_FILE_SUCCESS_MESSAGE, type: 'success' });
createAlert({ message: DELETE_PACKAGE_FILE_SUCCESS_MESSAGE, variant: VARIANT_SUCCESS });
})
.catch(() => {
createFlash({ message: DELETE_PACKAGE_FILE_ERROR_MESSAGE, type: 'warning' });
createAlert({ message: DELETE_PACKAGE_FILE_ERROR_MESSAGE, variant: VARIANT_WARNING });
});
};

View File

@ -1,7 +1,7 @@
<script>
import { GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui';
import { mapActions, mapState } from 'vuex';
import createFlash from '~/flash';
import { createAlert, VARIANT_INFO } from '~/flash';
import { historyReplaceState } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
import {
@ -84,7 +84,7 @@ export default {
const showAlert = urlParams.get(SHOW_DELETE_SUCCESS_ALERT);
if (showAlert) {
// to be refactored to use gl-alert
createFlash({ message: DELETE_PACKAGE_SUCCESS_MESSAGE, type: 'notice' });
createAlert({ message: DELETE_PACKAGE_SUCCESS_MESSAGE, variant: VARIANT_INFO });
const cleanUrl = window.location.href.split('?')[0];
historyReplaceState(cleanUrl);
}

View File

@ -1,5 +1,5 @@
import Api from '~/api';
import createFlash from '~/flash';
import { createAlert, VARIANT_SUCCESS } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { DELETE_PACKAGE_ERROR_MESSAGE } from '~/packages_and_registries/shared/constants';
import {
@ -43,7 +43,7 @@ export const requestPackagesList = ({ dispatch, state }, params = {}) => {
dispatch('receivePackagesListSuccess', { data, headers });
})
.catch(() => {
createFlash({
createAlert({
message: FETCH_PACKAGES_LIST_ERROR_MESSAGE,
});
})
@ -54,7 +54,7 @@ export const requestPackagesList = ({ dispatch, state }, params = {}) => {
export const requestDeletePackage = ({ dispatch, state }, { _links }) => {
if (!_links || !_links.delete_api_path) {
createFlash({
createAlert({
message: DELETE_PACKAGE_ERROR_MESSAGE,
});
const error = new Error(MISSING_DELETE_PATH_ERROR);
@ -69,14 +69,14 @@ export const requestDeletePackage = ({ dispatch, state }, { _links }) => {
const page = getNewPaginationPage(currentPage, perPage, total - 1);
dispatch('requestPackagesList', { page });
createFlash({
createAlert({
message: DELETE_PACKAGE_SUCCESS_MESSAGE,
type: 'success',
variant: VARIANT_SUCCESS,
});
})
.catch(() => {
dispatch('setLoading', false);
createFlash({
createAlert({
message: DELETE_PACKAGE_ERROR_MESSAGE,
});
});

View File

@ -1,6 +1,6 @@
<script>
import destroyPackageMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package.mutation.graphql';
import createFlash from '~/flash';
import { createAlert, VARIANT_SUCCESS, VARIANT_WARNING } from '~/flash';
import { s__ } from '~/locale';
import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '~/packages_and_registries/package_registry/constants';
@ -39,15 +39,15 @@ export default {
throw data.destroyPackage.errors[0];
}
if (this.showSuccessAlert) {
createFlash({
createAlert({
message: this.$options.i18n.successMessage,
type: 'success',
variant: VARIANT_SUCCESS,
});
}
} catch (error) {
createFlash({
createAlert({
message: this.$options.i18n.errorMessage,
type: 'warning',
variant: VARIANT_WARNING,
captureError: true,
error,
});

View File

@ -10,7 +10,7 @@ import {
GlTabs,
GlSprintf,
} from '@gitlab/ui';
import createFlash from '~/flash';
import { createAlert, VARIANT_SUCCESS, VARIANT_WARNING } from '~/flash';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { objectToQuery } from '~/lib/utils/url_utility';
@ -101,7 +101,7 @@ export default {
return data.package || {};
},
error(error) {
createFlash({
createAlert({
message: FETCH_PACKAGE_DETAILS_ERROR_MESSAGE,
captureError: true,
error,
@ -205,20 +205,20 @@ export default {
if (data?.destroyPackageFiles?.errors[0]) {
throw data.destroyPackageFiles.errors[0];
}
createFlash({
createAlert({
message:
ids.length === 1
? DELETE_PACKAGE_FILE_SUCCESS_MESSAGE
: DELETE_PACKAGE_FILES_SUCCESS_MESSAGE,
type: 'success',
variant: VARIANT_SUCCESS,
});
} catch (error) {
createFlash({
createAlert({
message:
ids.length === 1
? DELETE_PACKAGE_FILE_ERROR_MESSAGE
: DELETE_PACKAGE_FILES_ERROR_MESSAGE,
type: 'warning',
variant: VARIANT_WARNING,
captureError: true,
error,
});

View File

@ -1,6 +1,6 @@
<script>
import { GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui';
import createFlash from '~/flash';
import { createAlert, VARIANT_INFO } from '~/flash';
import { historyReplaceState } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages_and_registries/shared/constants';
@ -105,7 +105,7 @@ export default {
const showAlert = urlParams.get(SHOW_DELETE_SUCCESS_ALERT);
if (showAlert) {
// to be refactored to use gl-alert
createFlash({ message: DELETE_PACKAGE_SUCCESS_MESSAGE, type: 'notice' });
createAlert({ message: DELETE_PACKAGE_SUCCESS_MESSAGE, variant: VARIANT_INFO });
const cleanUrl = window.location.href.split('?')[0];
historyReplaceState(cleanUrl);
}

View File

@ -1,5 +1,5 @@
<script>
import { GlButton } from '@gitlab/ui';
import { GlDropdown, GlDropdownItem, GlButton } from '@gitlab/ui';
import csrf from '~/lib/utils/csrf';
import { joinPaths } from '~/lib/utils/url_utility';
import RevisionCard from './revision_card.vue';
@ -9,6 +9,8 @@ export default {
components: {
RevisionCard,
GlButton,
GlDropdown,
GlDropdownItem,
},
props: {
projectCompareIndexPath: {
@ -53,6 +55,10 @@ export default {
type: Array,
required: true,
},
straight: {
type: Boolean,
required: true,
},
},
data() {
return {
@ -67,8 +73,27 @@ export default {
revision: this.paramsTo,
refsProjectPath: this.sourceProjectRefsPath,
},
isStraight: this.straight,
};
},
computed: {
straightModeDropdownItems() {
return [
{
modeType: 'off',
isEnabled: false,
content: '..',
testId: 'disableStraightModeButton',
},
{
modeType: 'on',
isEnabled: true,
content: '...',
testId: 'enableStraightModeButton',
},
];
},
},
methods: {
onSubmit() {
this.$refs.form.submit();
@ -85,6 +110,9 @@ export default {
onSwapRevision() {
[this.from, this.to] = [this.to, this.from]; // swaps 'from' and 'to'
},
setStraightMode(isStraight) {
this.isStraight = isStraight;
},
},
};
</script>
@ -112,10 +140,22 @@ export default {
@selectRevision="onSelectRevision"
/>
<div
class="compare-ellipsis gl-display-flex gl-justify-content-center gl-align-items-center gl-align-self-end gl-my-4 gl-md-my-0"
class="gl-display-flex gl-justify-content-center gl-align-items-center gl-align-self-end gl-my-3 gl-md-my-0 gl-pl-3 gl-pr-3"
data-testid="ellipsis"
>
...
<input :value="isStraight ? 'true' : 'false'" type="hidden" name="straight" />
<gl-dropdown data-testid="modeDropdown" :text="isStraight ? '...' : '..'" size="small">
<gl-dropdown-item
v-for="mode in straightModeDropdownItems"
:key="mode.modeType"
:is-check-item="true"
:is-checked="isStraight == mode.isEnabled"
:data-testid="mode.testId"
@click="setStraightMode(mode.isEnabled)"
>
<span class="dropdown-menu-inner-content"> {{ mode.content }} </span>
</gl-dropdown-item>
</gl-dropdown>
</div>
<revision-card
data-testid="targetRevisionCard"

View File

@ -1,4 +1,5 @@
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import CompareApp from './components/app.vue';
export default function init() {
@ -9,6 +10,7 @@ export default function init() {
targetProjectRefsPath,
paramsFrom,
paramsTo,
straight,
projectCompareIndexPath,
projectMergeRequestPath,
createMrPath,
@ -29,6 +31,7 @@ export default function init() {
targetProjectRefsPath,
paramsFrom,
paramsTo,
straight: parseBoolean(straight),
projectCompareIndexPath,
projectMergeRequestPath,
createMrPath,

View File

@ -13,7 +13,7 @@ import {
GlButton,
} from '@gitlab/ui';
import { kebabCase, snakeCase } from 'lodash';
import createFlash from '~/flash';
import { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { IssuableType } from '~/issues/constants';
import { timeFor } from '~/lib/utils/datetime_utility';
@ -125,7 +125,7 @@ export default {
return data?.workspace?.issuable.attribute;
},
error(error) {
createFlash({
createAlert({
message: this.i18n.currentFetchError,
captureError: true,
error,
@ -179,7 +179,7 @@ export default {
return [];
},
error(error) {
createFlash({ message: this.i18n.listFetchError, captureError: true, error });
createAlert({ message: this.i18n.listFetchError, captureError: true, error });
},
},
},
@ -280,7 +280,7 @@ export default {
})
.then(({ data }) => {
if (data.issuableSetAttribute?.errors?.length) {
createFlash({
createAlert({
message: data.issuableSetAttribute.errors[0],
captureError: true,
error: data.issuableSetAttribute.errors[0],
@ -290,7 +290,7 @@ export default {
}
})
.catch((error) => {
createFlash({ message: this.i18n.updateError, captureError: true, error });
createAlert({ message: this.i18n.updateError, captureError: true, error });
})
.finally(() => {
this.updating = false;

View File

@ -1,6 +1,6 @@
<script>
import { GlDropdownForm, GlIcon, GlLoadingIcon, GlToggle, GlTooltipDirective } from '@gitlab/ui';
import createFlash from '~/flash';
import { createAlert } from '~/flash';
import { IssuableType } from '~/issues/constants';
import { isLoggedIn } from '~/lib/utils/common_utils';
import { __, sprintf } from '~/locale';
@ -74,7 +74,7 @@ export default {
this.$emit('subscribedUpdated', data.workspace?.issuable?.subscribed);
},
error() {
createFlash({
createAlert({
message: sprintf(
__('Something went wrong while setting %{issuableType} notifications.'),
{
@ -138,7 +138,7 @@ export default {
},
}) => {
if (errors.length) {
createFlash({
createAlert({
message: errors[0],
});
}
@ -149,7 +149,7 @@ export default {
},
)
.catch(() => {
createFlash({
createAlert({
message: sprintf(
__('Something went wrong while setting %{issuableType} notifications.'),
{

View File

@ -1,6 +1,6 @@
<script>
import { GlLoadingIcon, GlTableLite, GlButton, GlTooltipDirective } from '@gitlab/ui';
import createFlash from '~/flash';
import { createAlert } from '~/flash';
import { TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { formatDate, parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility';
@ -47,7 +47,7 @@ export default {
return this.extractTimelogs(data);
},
error() {
createFlash({ message: __('Something went wrong. Please try again.') });
createAlert({ message: __('Something went wrong. Please try again.') });
},
},
},
@ -105,7 +105,7 @@ export default {
}
})
.catch((error) => {
createFlash({
createAlert({
message: s__('TimeTracking|An error occurred while removing the timelog.'),
captureError: true,
error,

View File

@ -1,7 +1,7 @@
<script>
import { GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { produce } from 'immer';
import createFlash from '~/flash';
import { createAlert } from '~/flash';
import { __, sprintf } from '~/locale';
import { todoQueries, TodoMutationTypes, todoMutations } from '~/sidebar/constants';
import { todoLabel } from '~/vue_shared/components/sidebar/todo_toggle//utils';
@ -73,7 +73,7 @@ export default {
this.$emit('todoUpdated', currentUserTodos.length > 0);
},
error() {
createFlash({
createAlert({
message: sprintf(__('Something went wrong while setting %{issuableType} to-do item.'), {
issuableType: this.issuableType,
}),
@ -155,7 +155,7 @@ export default {
},
}) => {
if (errors.length) {
createFlash({
createAlert({
message: errors[0],
});
}
@ -166,7 +166,7 @@ export default {
},
)
.catch(() => {
createFlash({
createAlert({
message: sprintf(__('Something went wrong while setting %{issuableType} to-do item.'), {
issuableType: this.issuableType,
}),

View File

@ -1,7 +1,7 @@
import $ from 'jquery';
import { escape } from 'lodash';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import createFlash from '~/flash';
import { createAlert } from '~/flash';
import { __ } from '~/locale';
function isValidProjectId(id) {
@ -44,7 +44,7 @@ class SidebarMoveIssue {
.fetchAutocompleteProjects(searchTerm)
.then(callback)
.catch(() =>
createFlash({
createAlert({
message: __('An error occurred while fetching projects autocomplete.'),
}),
);
@ -79,7 +79,7 @@ class SidebarMoveIssue {
this.$confirmButton.disable().addClass('is-loading');
this.mediator.moveIssue().catch(() => {
createFlash({ message: __('An error occurred while moving the issue.') });
createAlert({ message: __('An error occurred while moving the issue.') });
this.$confirmButton.enable().removeClass('is-loading');
});
}

View File

@ -1,5 +1,5 @@
import Store from '~/sidebar/stores/sidebar_store';
import createFlash from '~/flash';
import { createAlert } from '~/flash';
import { __ } from '~/locale';
import toast from '~/vue_shared/plugins/global_toast';
import { visitUrl } from '../lib/utils/url_utility';
@ -97,7 +97,7 @@ export default class SidebarMediator {
this.processFetchedData(restResponse.data, graphQlResponse.data);
})
.catch(() =>
createFlash({
createAlert({
message: __('Error occurred when fetching sidebar data'),
}),
);

View File

@ -1,10 +1,10 @@
/* eslint-disable consistent-return */
import $ from 'jquery';
import { createAlert } from '~/flash';
import { loadingIconForLegacyJS } from '~/loading_icon_for_legacy_js';
import { spriteIcon } from '~/lib/utils/common_utils';
import FilesCommentButton from './files_comment_button';
import createFlash from './flash';
import initImageDiffHelper from './image_diff/helpers/init_image_diff';
import axios from './lib/utils/axios_utils';
import { __ } from './locale';
@ -96,7 +96,7 @@ export default class SingleFileDiff {
if (cb) cb();
})
.catch(() => {
createFlash({
createAlert({
message: __('An error occurred while retrieving diff'),
});
});

View File

@ -2,7 +2,7 @@
import { GlButton, GlLoadingIcon, GlFormInput, GlFormGroup } from '@gitlab/ui';
import eventHub from '~/blob/components/eventhub';
import createFlash from '~/flash';
import { createAlert } from '~/flash';
import { redirectTo, joinPaths } from '~/lib/utils/url_utility';
import { __, sprintf } from '~/locale';
import {
@ -145,7 +145,7 @@ export default {
const defaultErrorMsg = this.newSnippet
? SNIPPET_CREATE_MUTATION_ERROR
: SNIPPET_UPDATE_MUTATION_ERROR;
createFlash({
createAlert({
message: sprintf(defaultErrorMsg, { err }),
});
this.isUpdating = false;

View File

@ -1,7 +1,7 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import BlobHeaderEdit from '~/blob/components/blob_edit_header.vue';
import createFlash from '~/flash';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { getBaseURL, joinPaths } from '~/lib/utils/url_utility';
import { sprintf } from '~/locale';
@ -63,7 +63,7 @@ export default {
.catch((e) => this.flashAPIFailure(e));
},
flashAPIFailure(err) {
createFlash({ message: sprintf(SNIPPET_BLOB_CONTENT_FETCH_ERROR, { err }) });
createAlert({ message: sprintf(SNIPPET_BLOB_CONTENT_FETCH_ERROR, { err }) });
},
},
};

View File

@ -19,7 +19,7 @@ import axios from '~/lib/utils/axios_utils';
import { joinPaths } from '~/lib/utils/url_utility';
import { __, s__, sprintf } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import createFlash, { FLASH_TYPES } from '~/flash';
import { createAlert, VARIANT_DANGER, VARIANT_SUCCESS } from '~/flash';
import DeleteSnippetMutation from '../mutations/delete_snippet.mutation.graphql';
@ -196,12 +196,12 @@ export default {
try {
this.isSubmittingSpam = true;
await axios.post(this.reportAbusePath);
createFlash({
createAlert({
message: this.$options.i18n.snippetSpamSuccess,
type: FLASH_TYPES.SUCCESS,
variant: VARIANT_SUCCESS,
});
} catch (error) {
createFlash({ message: this.$options.i18n.snippetSpamFailure });
createAlert({ message: this.$options.i18n.snippetSpamFailure, variant: VARIANT_DANGER });
} finally {
this.isSubmittingSpam = false;
}

View File

@ -26,16 +26,6 @@ export default {
required: false,
default: true,
},
divergedCommitsCount: {
type: Number,
required: false,
default: 0,
},
targetBranchPath: {
type: String,
required: false,
default: '',
},
},
computed: {
closesText() {

View File

@ -47,7 +47,8 @@ class Projects::CompareController < Projects::ApplicationController
from_to_vars = {
from: compare_params[:from].presence,
to: compare_params[:to].presence,
from_project_id: compare_params[:from_project_id].presence
from_project_id: compare_params[:from_project_id].presence,
straight: compare_params[:straight].presence
}
if from_to_vars[:from].blank? || from_to_vars[:to].blank?
@ -112,7 +113,11 @@ class Projects::CompareController < Projects::ApplicationController
def compare
return @compare if defined?(@compare)
@compare = CompareService.new(source_project, head_ref).execute(target_project, start_ref)
@compare = CompareService.new(source_project, head_ref).execute(target_project, start_ref, straight: straight)
end
def straight
compare_params[:straight] == "true"
end
def start_ref
@ -160,6 +165,6 @@ class Projects::CompareController < Projects::ApplicationController
# rubocop: enable CodeReuse/ActiveRecord
def compare_params
@compare_params ||= params.permit(:from, :to, :from_project_id)
@compare_params ||= params.permit(:from, :to, :from_project_id, :straight)
end
end

View File

@ -4,7 +4,7 @@ module Types
module BranchProtections
class MergeAccessLevelType < BaseAccessLevelType # rubocop:disable Graphql/AuthorizeTypes
graphql_name 'MergeAccessLevel'
description 'Represents the merge access level of a branch protection.'
description 'Defines which user roles, users, or groups can merge into a protected branch.'
accepts ::ProtectedBranch::MergeAccessLevel
end
end

View File

@ -4,7 +4,7 @@ module Types
module BranchProtections
class PushAccessLevelType < BaseAccessLevelType # rubocop:disable Graphql/AuthorizeTypes
graphql_name 'PushAccessLevel'
description 'Represents the push access level of a branch protection.'
description 'Defines which user roles, users, or groups can push to a protected branch.'
accepts ::ProtectedBranch::PushAccessLevel
end
end

View File

@ -42,7 +42,8 @@ module CompareHelper
source_project_refs_path: refs_project_path(project),
target_project_refs_path: refs_project_path(@target_project),
params_from: params[:from],
params_to: params[:to]
params_to: params[:to],
straight: params[:straight]
}.tap do |data|
data[:projects_from] = target_projects(project).map do |target_project|
{ id: target_project.id, name: target_project.full_path }

View File

@ -1,5 +1,5 @@
- page_description brand_title unless page_description
- site_name = "GitLab"
- site_name = _('GitLab')
%head{ prefix: "og: http://ogp.me/ns#" }
%meta{ charset: "utf-8" }

View File

@ -5,7 +5,7 @@
.js-signature-container{ data: { 'signatures-path' => signatures_namespace_project_compare_index_path } }
#js-compare-selector{ data: project_compare_selector_data(@project, @merge_request, params) }
- if @commits.present?
- if @commits.present? || @diffs.present?
-# Only show commit list in the first page
- hide_commit_list = params[:page].present? && params[:page] != '1'
= render "projects/commits/commit_list" unless hide_commit_list

View File

@ -1,8 +0,0 @@
---
name: usage_data_i_ci_secrets_management_vault_build_created
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/46515
rollout_issue_url:
milestone: '13.6'
type: development
group: group::configure
default_enabled: true

View File

@ -1,7 +1,7 @@
---
name: jira_raise_timeouts
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/86439
rollout_issue_url:
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/375587
milestone: '15.0'
type: ops
group: group::integrations

View File

@ -440,6 +440,7 @@ onboarding
OpenID
OpenShift
Opsgenie
outdent
Overcommit
Packagist
parallelization

View File

@ -9322,6 +9322,29 @@ The edge type for [`TreeEntry`](#treeentry).
| <a id="treeentryedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="treeentryedgenode"></a>`node` | [`TreeEntry`](#treeentry) | The item at the end of the edge. |
#### `UnprotectAccessLevelConnection`
The connection type for [`UnprotectAccessLevel`](#unprotectaccesslevel).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="unprotectaccesslevelconnectionedges"></a>`edges` | [`[UnprotectAccessLevelEdge]`](#unprotectaccessleveledge) | A list of edges. |
| <a id="unprotectaccesslevelconnectionnodes"></a>`nodes` | [`[UnprotectAccessLevel]`](#unprotectaccesslevel) | A list of nodes. |
| <a id="unprotectaccesslevelconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
#### `UnprotectAccessLevelEdge`
The edge type for [`UnprotectAccessLevel`](#unprotectaccesslevel).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="unprotectaccessleveledgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="unprotectaccessleveledgenode"></a>`node` | [`UnprotectAccessLevel`](#unprotectaccesslevel) | The item at the end of the edge. |
#### `UploadRegistryConnection`
The connection type for [`UploadRegistry`](#uploadregistry).
@ -10204,6 +10227,7 @@ Branch protection details for a branch rule.
| <a id="branchprotectioncodeownerapprovalrequired"></a>`codeOwnerApprovalRequired` | [`Boolean!`](#boolean) | Enforce code owner approvals before allowing a merge. |
| <a id="branchprotectionmergeaccesslevels"></a>`mergeAccessLevels` | [`MergeAccessLevelConnection`](#mergeaccesslevelconnection) | Details about who can merge when this branch is the source branch. (see [Connections](#connections)) |
| <a id="branchprotectionpushaccesslevels"></a>`pushAccessLevels` | [`PushAccessLevelConnection`](#pushaccesslevelconnection) | Details about who can push when this branch is the source branch. (see [Connections](#connections)) |
| <a id="branchprotectionunprotectaccesslevels"></a>`unprotectAccessLevels` | [`UnprotectAccessLevelConnection`](#unprotectaccesslevelconnection) | Details about who can unprotect this branch. (see [Connections](#connections)) |
### `BranchRule`
@ -13937,7 +13961,7 @@ Maven metadata.
### `MergeAccessLevel`
Represents the merge access level of a branch protection.
Defines which user roles, users, or groups can merge into a protected branch.
#### Fields
@ -17345,7 +17369,7 @@ Which group, user or role is allowed to execute deployments to the environment.
### `PushAccessLevel`
Represents the push access level of a branch protection.
Defines which user roles, users, or groups can push to a protected branch.
#### Fields
@ -18574,6 +18598,19 @@ Represents a directory.
| <a id="treeentrywebpath"></a>`webPath` | [`String`](#string) | Web path for the tree entry (directory). |
| <a id="treeentryweburl"></a>`webUrl` | [`String`](#string) | Web URL for the tree entry (directory). |
### `UnprotectAccessLevel`
Defines which user roles, users, or groups can unprotect a protected branch.
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="unprotectaccesslevelaccesslevel"></a>`accessLevel` | [`Int!`](#int) | GitLab::Access level. |
| <a id="unprotectaccesslevelaccessleveldescription"></a>`accessLevelDescription` | [`String!`](#string) | Human readable representation for this access level. |
| <a id="unprotectaccesslevelgroup"></a>`group` | [`Group`](#group) | Group associated with this access level. |
| <a id="unprotectaccessleveluser"></a>`user` | [`UserCore`](#usercore) | User associated with this access level. |
### `UploadRegistry`
Represents the Geo replication and verification state of an upload.

View File

@ -321,26 +321,14 @@ class PreparePrimaryKeyForPartitioning < Gitlab::Database::Migration[2.0]
NEW_INDEX_NAME = :new_index_name
def up
with_lock_retries(raise_on_exhaustion: true) do
execute("ALTER TABLE #{TABLE_NAME} DROP CONSTRAINT #{PRIMARY_KEY} CASCADE")
rename_index(TABLE_NAME, NEW_INDEX_NAME, PRIMARY_KEY)
execute("ALTER TABLE #{TABLE_NAME} ADD CONSTRAINT #{PRIMARY_KEY} PRIMARY KEY USING INDEX #{PRIMARY_KEY}")
end
swap_primary_key(TABLE_NAME, PRIMARY_KEY, NEW_INDEX_NAME)
end
def down
add_concurrent_index(TABLE_NAME, :id, unique: true, name: OLD_INDEX_NAME)
add_concurrent_index(TABLE_NAME, [:id, :partition_id], unique: true, name: NEW_INDEX_NAME)
with_lock_retries(raise_on_exhaustion: true) do
execute("ALTER TABLE #{TABLE_NAME} DROP CONSTRAINT #{PRIMARY_KEY} CASCADE")
rename_index(TABLE_NAME, OLD_INDEX_NAME, PRIMARY_KEY)
execute("ALTER TABLE #{TABLE_NAME} ADD CONSTRAINT #{PRIMARY_KEY} PRIMARY KEY USING INDEX #{PRIMARY_KEY}")
end
unswap_primary_key(TABLE_NAME, PRIMARY_KEY, OLD_INDEX_NAME)
end
end
```

View File

@ -982,6 +982,43 @@ NOTE:
`add_sequence` should be avoided for columns with foreign keys.
Adding sequence to these columns is **only allowed** in the down method (restore previous schema state).
## Swapping primary key
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/98645) in GitLab 15.5.
Swapping the primary key is required to partition a table as the **partition key must be included in the primary key**.
You can use the `swap_primary_key` method provided by the database team.
Under the hood, it works like this:
- Drop the primary key constraint.
- Add the primary key using the index defined beforehand.
```ruby
class SwapPrimaryKey < Gitlab::Database::Migration[2.0]
TABLE_NAME = :table_name
PRIMARY_KEY = :table_name_pkey
OLD_INDEX_NAME = :old_index_name
NEW_INDEX_NAME = :new_index_name
def up
swap_primary_key(TABLE_NAME, PRIMARY_KEY, NEW_INDEX_NAME)
end
def down
add_concurrent_index(TABLE_NAME, :id, unique: true, name: OLD_INDEX_NAME)
add_concurrent_index(TABLE_NAME, [:id, :partition_id], unique: true, name: NEW_INDEX_NAME)
unswap_primary_key(TABLE_NAME, PRIMARY_KEY, OLD_INDEX_NAME)
end
end
```
NOTE:
Make sure to introduce the new index beforehand in a separate migration in order
to swap the primary key.
## Integer column type
By default, an integer column can hold up to a 4-byte (32-bit) number. That is

View File

@ -55,6 +55,8 @@ descriptions):
| <kbd>Command</kbd> + <kbd>i</kbd> | <kbd>Control</kbd> + <kbd>i</kbd> | Italicize the selected text (surround it with `_`). |
| <kbd>Command</kbd> + <kbd>Shift</kbd> + <kbd>x</kbd> | <kbd>Control</kbd> + <kbd>Shift</kbd> + <kbd>x</kbd> | Strike through the selected text (surround it with `~~`). |
| <kbd>Command</kbd> + <kbd>k</kbd> | <kbd>Control</kbd> + <kbd>k</kbd> | Add a link (surround the selected text with `[]()`). |
| <kbd>Command</kbd> + <kbd>&#93;</kbd> | <kbd>Control</kbd> + <kbd>&#93;</kbd> | Indent list item. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/351924) in GitLab 15.3. |
| <kbd>Command</kbd> + <kbd>&#91;</kbd> | <kbd>Control</kbd> + <kbd>&#91;</kbd> | Outdent list item. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/351924) in GitLab 15.3. |
The shortcuts for editing in text fields are always enabled, even if other
keyboard shortcuts are disabled.

View File

@ -8,6 +8,11 @@ pre-push:
files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD
glob: '*.{js,vue}'
run: yarn run lint:eslint {files}
jsonlint:
tags: style
files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD
glob: '*.{json}'
run: scripts/lint-json.sh {files}
haml-lint:
tags: view haml style
files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD

View File

@ -1503,6 +1503,26 @@ into similar problems in the future (e.g. when new tables are created).
SQL
end
def drop_constraint(table_name, constraint_name, cascade: false)
execute <<~SQL
ALTER TABLE #{quote_table_name(table_name)} DROP CONSTRAINT #{quote_column_name(constraint_name)} #{cascade_statement(cascade)}
SQL
end
def add_primary_key_using_index(table_name, pk_name, index_to_use)
execute <<~SQL
ALTER TABLE #{quote_table_name(table_name)} ADD CONSTRAINT #{quote_table_name(pk_name)} PRIMARY KEY USING INDEX #{quote_table_name(index_to_use)}
SQL
end
def swap_primary_key(table_name, primary_key_name, index_to_use)
with_lock_retries(raise_on_exhaustion: true) do
drop_constraint(table_name, primary_key_name, cascade: true)
add_primary_key_using_index(table_name, primary_key_name, index_to_use)
end
end
alias_method :unswap_primary_key, :swap_primary_key
def drop_sequence(table_name, column_name, sequence_name)
execute <<~SQL
ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} DROP DEFAULT;
@ -1519,6 +1539,10 @@ into similar problems in the future (e.g. when new tables are created).
private
def cascade_statement(cascade)
cascade ? 'CASCADE' : ''
end
def create_temporary_columns_and_triggers(table, columns, primary_key: :id, data_type: :bigint)
unless table_exists?(table)
raise "Table #{table} does not exist"

View File

@ -261,11 +261,6 @@
redis_slot: project_management
aggregation: daily
# Secrets Management
- name: i_ci_secrets_management_vault_build_created
category: ci_secrets_management
redis_slot: ci_secrets_management
aggregation: weekly
feature_flag: usage_data_i_ci_secrets_management_vault_build_created
- name: i_snippets_show
category: snippets
redis_slot: snippets

View File

@ -39,7 +39,7 @@ namespace :tw do
CodeOwnerRule.new('Documentation Guidelines', '@sselhorn'),
CodeOwnerRule.new('Dynamic Analysis', '@rdickenson'),
CodeOwnerRule.new('Ecosystem', '@kpaizee'),
CodeOwnerRule.new('Editor', '@aqualls'),
CodeOwnerRule.new('Editor', '@ashrafkhamis'),
CodeOwnerRule.new('Foundations', '@rdickenson'),
CodeOwnerRule.new('Fuzz Testing', '@rdickenson'),
CodeOwnerRule.new('Geo', '@axil'),

View File

@ -90,6 +90,7 @@
"@tiptap/extension-task-item": "^2.0.0-beta.37",
"@tiptap/extension-task-list": "^2.0.0-beta.29",
"@tiptap/extension-text": "^2.0.0-beta.17",
"@tiptap/suggestion": "^2.0.0-beta.96",
"@tiptap/vue-2": "^2.0.0-beta.84",
"apollo-upload-client": "15.0.0",
"autosize": "^5.0.1",
@ -171,6 +172,7 @@
"swagger-ui-dist": "4.12.0",
"three": "^0.143.0",
"timeago.js": "^4.0.2",
"tippy.js": "^6.3.7",
"unified": "^10.1.2",
"unist-builder": "^3.0.0",
"unist-util-visit-parents": "^5.1.0",
@ -233,6 +235,7 @@
"jest-raw-loader": "^1.0.1",
"jest-transform-graphql": "^2.1.0",
"jest-util": "^27.5.1",
"jsonlint": "^1.6.3",
"markdownlint-cli": "0.32.2",
"miragejs": "^0.1.40",
"mock-apollo-client": "1.2.0",
@ -262,4 +265,4 @@
"node": ">=12.22.1",
"yarn": "^1.10.0"
}
}
}

View File

@ -11,6 +11,12 @@ module QA
QA_PATTERN = %r{^qa/}.freeze
SPEC_PATTERN = %r{^qa/qa/specs/features/}.freeze
DEPENDENCY_PATTERN = Regexp.union(
/_VERSION/,
/Gemfile\.lock/,
/yarn\.lock/,
/Dockerfile\.assets/
)
def initialize(mr_diff, mr_labels)
@mr_diff = mr_diff
@ -21,7 +27,8 @@ module QA
#
# @return [String]
def qa_tests
return if mr_diff.empty?
return if mr_diff.empty? || dependency_changes
# make paths relative to qa directory
return changed_files&.map { |path| path.delete_prefix("qa/") }&.join(" ") if only_spec_changes?
return qa_spec_directories_for_devops_stage&.join(" ") if non_qa_changes? && mr_labels.any?
@ -104,6 +111,13 @@ module QA
Dir.glob("qa/specs/**/*/").select { |dir| dir =~ %r{\d+_#{devops_stage}/$} }
end
# Changes to gitlab dependencies
#
# @return [Boolean]
def dependency_changes
changed_files.any? { |file| file.match?(DEPENDENCY_PATTERN) }
end
# Change files in merge request
#
# @return [Array<String>]

View File

@ -35,7 +35,7 @@ RSpec.describe QA::Tools::Ci::QaChanges do
context "with framework changes" do
let(:mr_diff) { [{ path: "qa/qa.rb" }] }
it ".qa_tests do not return specifix specs" do
it ".qa_tests do not return specific specs" do
expect(qa_changes.qa_tests).to be_nil
end
@ -84,4 +84,14 @@ RSpec.describe QA::Tools::Ci::QaChanges do
expect(qa_changes.quarantine_changes?).to eq(true)
end
end
%w[GITALY_SERVER_VERSION Gemfile.lock yarn.lock Dockerfile.assets].each do |dependency_file|
context "when #{dependency_file} change" do
let(:mr_diff) { [{ path: dependency_file }] }
it ".qa_tests do not return specific specs" do
expect(qa_changes.qa_tests).to be_nil
end
end
end
end

8
scripts/lint-json.sh Executable file
View File

@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
for file in "$@"
do
yarn run -s jsonlint -p "$file" | perl -pe 'chomp if eof' | diff "$file" -
done

View File

@ -67,11 +67,13 @@ RSpec.describe Projects::CompareController do
from: from_ref,
to: to_ref,
w: whitespace,
page: page
page: page,
straight: straight
}
end
let(:whitespace) { nil }
let(:straight) { nil }
let(:page) { nil }
context 'when the refs exist in the same project' do
@ -142,6 +144,58 @@ RSpec.describe Projects::CompareController do
end
end
context 'when comparing missing commits between source and target' do
let(:from_project_id) { nil }
let(:from_ref) { '5937ac0a7beb003549fc5fd26fc247adbce4a52e' }
let(:to_ref) { '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9' }
let(:page) { 1 }
context 'when comparing them in the other direction' do
let(:straight) { "false" }
let(:from_ref) { '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9' }
let(:to_ref) { '5937ac0a7beb003549fc5fd26fc247adbce4a52e' }
it 'the commits are there' do
show_request
expect(response).to be_successful
expect(assigns(:commits).length).to be >= 2
expect(assigns(:diffs).raw_diff_files.size).to be >= 2
expect(assigns(:diffs).diff_files.first).to be_present
end
end
context 'with straight mode true' do
let(:from_ref) { '5937ac0a7beb003549fc5fd26fc247adbce4a52e' }
let(:to_ref) { '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9' }
let(:straight) { "true" }
it 'the commits are empty, but the removed lines are visible as diffs' do
show_request
expect(response).to be_successful
expect(assigns(:commits).length).to be == 0
expect(assigns(:diffs).diff_files.size).to be >= 4
end
end
context 'with straight mode false' do
let(:from_ref) { '5937ac0a7beb003549fc5fd26fc247adbce4a52e' }
let(:to_ref) { '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9' }
let(:straight) { "false" }
it 'the additional commits are not visible in diffs and commits' do
show_request
expect(response).to be_successful
expect(assigns(:commits).length).to be == 0
expect(assigns(:diffs).diff_files.size).to be == 0
end
end
end
context 'when the refs exist in different projects but the user cannot see' do
let(:from_project_id) { private_fork.id }
let(:from_ref) { 'improve%2Fmore-awesome' }
@ -450,10 +504,13 @@ RSpec.describe Projects::CompareController do
project_id: project,
from: from_ref,
to: to_ref,
straight: straight,
format: :json
}
end
let(:straight) { nil }
context 'when the source and target refs exist' do
let(:from_ref) { 'improve%2Fawesome' }
let(:to_ref) { 'feature' }
@ -464,6 +521,39 @@ RSpec.describe Projects::CompareController do
let(:signature_commit) { project.commit_by(oid: '0b4bc9a49b562e85de7cc9e834518ea6828729b9') }
let(:non_signature_commit) { build(:commit, project: project, safe_message: "message", sha: 'non_signature_commit') }
before do
escaped_from_ref = Addressable::URI.unescape(from_ref)
escaped_to_ref = Addressable::URI.unescape(to_ref)
compare_service = CompareService.new(project, escaped_to_ref)
compare = compare_service.execute(project, escaped_from_ref, straight: false)
expect(CompareService).to receive(:new).with(project, escaped_to_ref).and_return(compare_service)
expect(compare_service).to receive(:execute).with(project, escaped_from_ref, straight: false).and_return(compare)
expect(compare).to receive(:commits).and_return(CommitCollection.new(project, [signature_commit, non_signature_commit]))
expect(non_signature_commit).to receive(:has_signature?).and_return(false)
end
it 'returns only the commit with a signature' do
signatures_request
expect(response).to have_gitlab_http_status(:ok)
signatures = json_response['signatures']
expect(signatures.size).to eq(1)
expect(signatures.first['commit_sha']).to eq(signature_commit.sha)
expect(signatures.first['html']).to be_present
end
end
context 'when the user has access to the project with straight compare' do
render_views
let(:signature_commit) { project.commit_by(oid: '0b4bc9a49b562e85de7cc9e834518ea6828729b9') }
let(:non_signature_commit) { build(:commit, project: project, safe_message: "message", sha: 'non_signature_commit') }
let(:straight) { "true" }
before do
escaped_from_ref = Addressable::URI.unescape(from_ref)
escaped_to_ref = Addressable::URI.unescape(to_ref)
@ -472,7 +562,7 @@ RSpec.describe Projects::CompareController do
compare = compare_service.execute(project, escaped_from_ref)
expect(CompareService).to receive(:new).with(project, escaped_to_ref).and_return(compare_service)
expect(compare_service).to receive(:execute).with(project, escaped_from_ref).and_return(compare)
expect(compare_service).to receive(:execute).with(project, escaped_from_ref, straight: true).and_return(compare)
expect(compare).to receive(:commits).and_return(CommitCollection.new(project, [signature_commit, non_signature_commit]))
expect(non_signature_commit).to receive(:has_signature?).and_return(false)

View File

@ -39,33 +39,13 @@ FactoryBot.define do
end
end
trait :maintainers_can_push do
trait :no_one_can_merge do
transient do
default_push_level { false }
default_merge_level { false }
end
after(:build) do |protected_branch|
protected_branch.push_access_levels.new(access_level: Gitlab::Access::MAINTAINER)
end
end
trait :maintainers_can_merge do
transient do
default_push_level { false }
end
after(:build) do |protected_branch|
protected_branch.push_access_levels.new(access_level: Gitlab::Access::MAINTAINER)
end
end
trait :developers_can_push do
transient do
default_push_level { false }
end
after(:build) do |protected_branch|
protected_branch.push_access_levels.new(access_level: Gitlab::Access::DEVELOPER)
protected_branch.merge_access_levels.new(access_level: Gitlab::Access::NO_ACCESS)
end
end
@ -79,6 +59,16 @@ FactoryBot.define do
end
end
trait :maintainers_can_merge do
transient do
default_merge_level { false }
end
after(:build) do |protected_branch|
protected_branch.merge_access_levels.new(access_level: Gitlab::Access::MAINTAINER)
end
end
trait :no_one_can_push do
transient do
default_push_level { false }
@ -89,13 +79,23 @@ FactoryBot.define do
end
end
trait :no_one_can_merge do
trait :developers_can_push do
transient do
default_merge_level { false }
default_push_level { false }
end
after(:build) do |protected_branch|
protected_branch.merge_access_levels.new(access_level: Gitlab::Access::NO_ACCESS)
protected_branch.push_access_levels.new(access_level: Gitlab::Access::DEVELOPER)
end
end
trait :maintainers_can_push do
transient do
default_push_level { false }
end
after(:build) do |protected_branch|
protected_branch.push_access_levels.new(access_level: Gitlab::Access::MAINTAINER)
end
end
end

View File

@ -0,0 +1,54 @@
import Heading from '~/content_editor/extensions/heading';
import { createTestEditor, createDocBuilder, triggerNodeInputRule } from '../test_utils';
describe('content_editor/extensions/heading', () => {
let tiptapEditor;
let doc;
let p;
let heading;
beforeEach(() => {
tiptapEditor = createTestEditor({ extensions: [Heading] });
({
builders: { doc, p, heading },
} = createDocBuilder({
tiptapEditor,
names: {
heading: { nodeType: Heading.name },
},
}));
});
describe('when typing a valid heading input rule', () => {
it.each`
level | inputRuleText
${1} | ${'# '}
${2} | ${'## '}
${3} | ${'### '}
${4} | ${'#### '}
${5} | ${'##### '}
${6} | ${'###### '}
`('inserts a heading node for $inputRuleText', ({ level, inputRuleText }) => {
const expectedDoc = doc(heading({ level }));
triggerNodeInputRule({ tiptapEditor, inputRuleText });
expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
});
});
describe('when typing a invalid heading input rule', () => {
it.each`
inputRuleText
${'#hi'}
${'#\n'}
`('does not insert a heading node for $inputRuleText', ({ inputRuleText }) => {
const expectedDoc = doc(p());
triggerNodeInputRule({ tiptapEditor, inputRuleText });
// no change to the document
expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
});
});
});

View File

@ -1,7 +1,10 @@
import fs from 'fs';
import jsYaml from 'js-yaml';
import { memoize } from 'lodash';
import MockAdapter from 'axios-mock-adapter';
import axios from 'axios';
import { createContentEditor } from '~/content_editor';
import httpStatus from '~/lib/utils/http_status';
const getFocusedMarkdownExamples = memoize(
() => process.env.FOCUSED_MARKDOWN_EXAMPLES?.split(',') || [],
@ -42,6 +45,11 @@ const loadMarkdownApiExamples = (markdownYamlPath) => {
};
const testSerializesHtmlToMarkdownForElement = async ({ markdown, html }) => {
const mock = new MockAdapter(axios);
// Ignore any API requests from the suggestions plugin
mock.onGet().reply(httpStatus.OK, []);
const contentEditor = createContentEditor({
// Overwrite renderMarkdown to always return this specific html
renderMarkdown: () => html,
@ -55,6 +63,8 @@ const testSerializesHtmlToMarkdownForElement = async ({ markdown, html }) => {
// Assert that the markdown we ended up with after sending it through all the ContentEditor
// plumbing matches the original markdown from the YAML.
expect(serializedContent.trim()).toBe(markdown.trim());
mock.restore();
};
// describeMarkdownProcesssing

View File

@ -44,7 +44,7 @@ describe('content_editor/services/track_input_rules_and_shortcuts', () => {
describe('when creating a heading using an keyboard shortcut', () => {
it('sends a tracking event indicating that a heading was created using an input rule', async () => {
const shortcuts = Heading.config.addKeyboardShortcuts.call(Heading);
const shortcuts = Heading.parent.config.addKeyboardShortcuts.call(Heading);
const [firstShortcut] = Object.keys(shortcuts);
const nodeName = Heading.name;

View File

@ -1,14 +1,16 @@
import { GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import Autosave from '~/autosave';
import { getDraft, updateDraft, clearDraft, getLockVersion } from '~/lib/utils/autosave';
import DescriptionTemplate from '~/issues/show/components/fields/description_template.vue';
import IssuableTitleField from '~/issues/show/components/fields/title.vue';
import DescriptionField from '~/issues/show/components/fields/description.vue';
import IssueTypeField from '~/issues/show/components/fields/type.vue';
import formComponent from '~/issues/show/components/form.vue';
import LockedWarning from '~/issues/show/components/locked_warning.vue';
import eventHub from '~/issues/show/event_hub';
jest.mock('~/autosave');
jest.mock('~/lib/utils/autosave');
describe('Inline edit form component', () => {
let wrapper;
@ -38,9 +40,14 @@ describe('Inline edit form component', () => {
...defaultProps,
...props,
},
stubs: {
DescriptionField,
},
});
};
const findTitleField = () => wrapper.findComponent(IssuableTitleField);
const findDescriptionField = () => wrapper.findComponent(DescriptionField);
const findDescriptionTemplate = () => wrapper.findComponent(DescriptionTemplate);
const findIssuableTypeField = () => wrapper.findComponent(IssueTypeField);
const findLockedWarning = () => wrapper.findComponent(LockedWarning);
@ -108,16 +115,34 @@ describe('Inline edit form component', () => {
});
describe('autosave', () => {
let spy;
beforeEach(() => {
spy = jest.spyOn(Autosave.prototype, 'reset');
getDraft.mockImplementation((autosaveKey) => {
return autosaveKey[autosaveKey.length - 1];
});
});
it('initialized Autosave on mount', () => {
it('initializes title and description fields with saved drafts', () => {
createComponent();
expect(Autosave).toHaveBeenCalledTimes(2);
expect(findTitleField().props().value).toBe('title');
expect(findDescriptionField().props().value).toBe('description');
});
it('updates local storage drafts when title and description change', () => {
const updatedTitle = 'updated title';
const updatedDescription = 'updated description';
createComponent();
findTitleField().vm.$emit('input', updatedTitle);
findDescriptionField().vm.$emit('input', updatedDescription);
expect(updateDraft).toHaveBeenCalledWith(expect.any(Array), updatedTitle);
expect(updateDraft).toHaveBeenCalledWith(
expect.any(Array),
updatedDescription,
defaultProps.formState.lock_version,
);
});
it('calls reset on autosave when eventHub emits appropriate events', () => {
@ -125,33 +150,60 @@ describe('Inline edit form component', () => {
eventHub.$emit('close.form');
expect(spy).toHaveBeenCalledTimes(2);
expect(clearDraft).toHaveBeenCalledTimes(2);
eventHub.$emit('delete.issuable');
expect(spy).toHaveBeenCalledTimes(4);
expect(clearDraft).toHaveBeenCalledTimes(4);
eventHub.$emit('update.issuable');
expect(spy).toHaveBeenCalledTimes(6);
expect(clearDraft).toHaveBeenCalledTimes(6);
});
describe('outdated description', () => {
const clientSideMockVersion = 'lock version from local storage';
const serverSideMockVersion = 'lock version from server';
const mockGetLockVersion = () => getLockVersion.mockResolvedValue(clientSideMockVersion);
it('does not show warning if lock version from server is the same as the local lock version', () => {
createComponent();
expect(findAlert().exists()).toBe(false);
});
it('shows warning if lock version from server differs than the local lock version', async () => {
Autosave.prototype.getSavedLockVersion.mockResolvedValue('lock version from local storage');
mockGetLockVersion();
createComponent({
formState: { ...defaultProps.formState, lock_version: 'lock version from server' },
formState: { ...defaultProps.formState, lock_version: serverSideMockVersion },
});
await nextTick();
expect(findAlert().exists()).toBe(true);
});
describe('when saved draft is discarded', () => {
beforeEach(async () => {
mockGetLockVersion();
createComponent({
formState: { ...defaultProps.formState, lock_version: serverSideMockVersion },
});
await nextTick();
findAlert().vm.$emit('secondaryAction');
});
it('hides the warning alert', () => {
expect(findAlert().exists()).toBe(false);
});
it('clears the description draft', () => {
expect(clearDraft).toHaveBeenCalledWith(expect.any(Array));
});
});
});
});
});

View File

@ -1,32 +1,42 @@
import { clearDraft, getDraft, updateDraft } from '~/lib/utils/autosave';
import { clearDraft, getDraft, updateDraft, getLockVersion } from '~/lib/utils/autosave';
describe('autosave utils', () => {
const autosaveKey = 'dummy-autosave-key';
const text = 'some dummy text';
const lockVersion = '2';
const normalizedAutosaveKey = `autosave/${autosaveKey}`;
const lockVersionKey = `autosave/${autosaveKey}/lockVersion`;
describe('clearDraft', () => {
beforeEach(() => {
localStorage.setItem(`autosave/${autosaveKey}`, text);
localStorage.setItem(normalizedAutosaveKey, text);
localStorage.setItem(lockVersionKey, lockVersion);
});
afterEach(() => {
localStorage.removeItem(`autosave/${autosaveKey}`);
localStorage.removeItem(normalizedAutosaveKey);
});
it('removes the draft from localStorage', () => {
clearDraft(autosaveKey);
expect(localStorage.getItem(`autosave/${autosaveKey}`)).toBe(null);
expect(localStorage.getItem(normalizedAutosaveKey)).toBe(null);
});
it('removes the lockVersion from localStorage', () => {
clearDraft(autosaveKey);
expect(localStorage.getItem(lockVersionKey)).toBe(null);
});
});
describe('getDraft', () => {
beforeEach(() => {
localStorage.setItem(`autosave/${autosaveKey}`, text);
localStorage.setItem(normalizedAutosaveKey, text);
});
afterEach(() => {
localStorage.removeItem(`autosave/${autosaveKey}`);
localStorage.removeItem(normalizedAutosaveKey);
});
it('returns the draft from localStorage', () => {
@ -36,7 +46,7 @@ describe('autosave utils', () => {
});
it('returns null if no entry exists in localStorage', () => {
localStorage.removeItem(`autosave/${autosaveKey}`);
localStorage.removeItem(normalizedAutosaveKey);
const result = getDraft(autosaveKey);
@ -46,19 +56,44 @@ describe('autosave utils', () => {
describe('updateDraft', () => {
beforeEach(() => {
localStorage.setItem(`autosave/${autosaveKey}`, text);
localStorage.setItem(normalizedAutosaveKey, text);
});
afterEach(() => {
localStorage.removeItem(`autosave/${autosaveKey}`);
localStorage.removeItem(normalizedAutosaveKey);
});
it('removes the draft from localStorage', () => {
it('updates the stored draft', () => {
const newText = 'new text';
updateDraft(autosaveKey, newText);
expect(localStorage.getItem(`autosave/${autosaveKey}`)).toBe(newText);
expect(localStorage.getItem(normalizedAutosaveKey)).toBe(newText);
});
describe('when lockVersion is provided', () => {
it('updates the stored lockVersion', () => {
const newText = 'new text';
const newLockVersion = '2';
updateDraft(autosaveKey, newText, lockVersion);
expect(localStorage.getItem(lockVersionKey)).toBe(newLockVersion);
});
});
});
describe('getLockVersion', () => {
beforeEach(() => {
localStorage.setItem(lockVersionKey, lockVersion);
});
afterEach(() => {
localStorage.removeItem(lockVersionKey);
});
it('returns the lockVersion from localStorage', () => {
expect(getLockVersion(autosaveKey)).toBe(lockVersion);
});
});
});

View File

@ -1,6 +1,6 @@
import testAction from 'helpers/vuex_action_helper';
import Api from '~/api';
import createFlash from '~/flash';
import { createAlert, VARIANT_SUCCESS, VARIANT_WARNING } from '~/flash';
import { FETCH_PACKAGE_VERSIONS_ERROR } from '~/packages_and_registries/infrastructure_registry/details/constants';
import {
fetchPackageVersions,
@ -67,9 +67,9 @@ describe('Actions Package details store', () => {
[],
);
expect(Api.projectPackage).toHaveBeenCalledWith(packageEntity.project_id, packageEntity.id);
expect(createFlash).toHaveBeenCalledWith({
expect(createAlert).toHaveBeenCalledWith({
message: FETCH_PACKAGE_VERSIONS_ERROR,
type: 'warning',
variant: VARIANT_WARNING,
});
});
});
@ -87,9 +87,9 @@ describe('Actions Package details store', () => {
Api.deleteProjectPackage = jest.fn().mockRejectedValue();
await testAction(deletePackage, undefined, { packageEntity }, [], []);
expect(createFlash).toHaveBeenCalledWith({
expect(createAlert).toHaveBeenCalledWith({
message: DELETE_PACKAGE_ERROR_MESSAGE,
type: 'warning',
variant: VARIANT_WARNING,
});
});
});
@ -112,18 +112,18 @@ describe('Actions Package details store', () => {
packageEntity.id,
fileId,
);
expect(createFlash).toHaveBeenCalledWith({
expect(createAlert).toHaveBeenCalledWith({
message: DELETE_PACKAGE_FILE_SUCCESS_MESSAGE,
type: 'success',
variant: VARIANT_SUCCESS,
});
});
it('should create flash on API error', async () => {
Api.deleteProjectPackageFile = jest.fn().mockRejectedValue();
await testAction(deletePackageFile, fileId, { packageEntity }, [], []);
expect(createFlash).toHaveBeenCalledWith({
expect(createAlert).toHaveBeenCalledWith({
message: DELETE_PACKAGE_FILE_ERROR_MESSAGE,
type: 'warning',
variant: VARIANT_WARNING,
});
});
});

View File

@ -3,7 +3,7 @@ import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import setWindowLocation from 'helpers/set_window_location_helper';
import createFlash from '~/flash';
import { createAlert, VARIANT_INFO } from '~/flash';
import * as commonUtils from '~/lib/utils/common_utils';
import PackageListApp from '~/packages_and_registries/infrastructure_registry/list/components/packages_list_app.vue';
import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '~/packages_and_registries/infrastructure_registry/list/constants';
@ -222,9 +222,9 @@ describe('packages_list_app', () => {
it(`creates a flash if the query string contains ${SHOW_DELETE_SUCCESS_ALERT}`, () => {
mountComponent();
expect(createFlash).toHaveBeenCalledWith({
expect(createAlert).toHaveBeenCalledWith({
message: DELETE_PACKAGE_SUCCESS_MESSAGE,
type: 'notice',
variant: VARIANT_INFO,
});
});
@ -238,7 +238,7 @@ describe('packages_list_app', () => {
setWindowLocation('?');
mountComponent();
expect(createFlash).not.toHaveBeenCalled();
expect(createAlert).not.toHaveBeenCalled();
expect(commonUtils.historyReplaceState).not.toHaveBeenCalled();
});
});

View File

@ -2,7 +2,7 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import Api from '~/api';
import createFlash from '~/flash';
import { createAlert } from '~/flash';
import { MISSING_DELETE_PATH_ERROR } from '~/packages_and_registries/infrastructure_registry/list/constants';
import * as actions from '~/packages_and_registries/infrastructure_registry/list/stores/actions';
import * as types from '~/packages_and_registries/infrastructure_registry/list/stores/mutation_types';
@ -107,7 +107,7 @@ describe('Actions Package list store', () => {
{ type: 'setLoading', payload: false },
],
);
expect(createFlash).toHaveBeenCalled();
expect(createAlert).toHaveBeenCalled();
});
it('should force the terraform_module type when forceTerraform is true', async () => {
@ -209,17 +209,17 @@ describe('Actions Package list store', () => {
{ type: 'setLoading', payload: false },
],
);
expect(createFlash).toHaveBeenCalled();
expect(createAlert).toHaveBeenCalled();
});
it.each`
property | actionPayload
${'_links'} | ${{}}
${'delete_api_path'} | ${{ _links: {} }}
`('should reject and createFlash when $property is missing', ({ actionPayload }) => {
`('should reject and createAlert when $property is missing', ({ actionPayload }) => {
return testAction(actions.requestDeletePackage, actionPayload, null, [], []).catch((e) => {
expect(e).toEqual(new Error(MISSING_DELETE_PATH_ERROR));
expect(createFlash).toHaveBeenCalledWith({
expect(createAlert).toHaveBeenCalledWith({
message: DELETE_PACKAGE_ERROR_MESSAGE,
});
});

View File

@ -3,7 +3,7 @@ import VueApollo from 'vue-apollo';
import waitForPromises from 'helpers/wait_for_promises';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import createFlash from '~/flash';
import { createAlert, VARIANT_SUCCESS, VARIANT_WARNING } from '~/flash';
import DeletePackage from '~/packages_and_registries/package_registry/components/functional/delete_package.vue';
import destroyPackageMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package.mutation.graphql';
@ -104,22 +104,22 @@ describe('DeletePackage', () => {
expect(wrapper.emitted('end')).toEqual([[]]);
});
it('does not call createFlash', async () => {
it('does not call createAlert', async () => {
createComponent();
await clickOnButtonAndWait(eventPayload);
expect(createFlash).not.toHaveBeenCalled();
expect(createAlert).not.toHaveBeenCalled();
});
it('calls createFlash with the success message when showSuccessAlert is true', async () => {
it('calls createAlert with the success message when showSuccessAlert is true', async () => {
createComponent({ showSuccessAlert: true });
await clickOnButtonAndWait(eventPayload);
expect(createFlash).toHaveBeenCalledWith({
expect(createAlert).toHaveBeenCalledWith({
message: DeletePackage.i18n.successMessage,
type: 'success',
variant: VARIANT_SUCCESS,
});
});
});
@ -141,14 +141,14 @@ describe('DeletePackage', () => {
expect(wrapper.emitted('end')).toEqual([[]]);
});
it('calls createFlash with the error message', async () => {
it('calls createAlert with the error message', async () => {
createComponent({ showSuccessAlert: true });
await clickOnButtonAndWait(eventPayload);
expect(createFlash).toHaveBeenCalledWith({
expect(createAlert).toHaveBeenCalledWith({
message: DeletePackage.i18n.errorMessage,
type: 'warning',
variant: VARIANT_WARNING,
captureError: true,
error: expect.any(Error),
});

View File

@ -6,7 +6,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import { createAlert } from '~/flash';
import AdditionalMetadata from '~/packages_and_registries/package_registry/components/details/additional_metadata.vue';
import PackagesApp from '~/packages_and_registries/package_registry/pages/details.vue';
@ -149,7 +149,7 @@ describe('PackagesApp', () => {
await waitForPromises();
expect(createFlash).toHaveBeenCalledWith(
expect(createAlert).toHaveBeenCalledWith(
expect.objectContaining({
message: FETCH_PACKAGE_DETAILS_ERROR_MESSAGE,
}),
@ -383,7 +383,7 @@ describe('PackagesApp', () => {
await doDeleteFile();
expect(createFlash).toHaveBeenCalledWith(
expect(createAlert).toHaveBeenCalledWith(
expect.objectContaining({
message: DELETE_PACKAGE_FILE_SUCCESS_MESSAGE,
}),
@ -399,7 +399,7 @@ describe('PackagesApp', () => {
await doDeleteFile();
expect(createFlash).toHaveBeenCalledWith(
expect(createAlert).toHaveBeenCalledWith(
expect.objectContaining({
message: DELETE_PACKAGE_FILE_ERROR_MESSAGE,
}),
@ -416,7 +416,7 @@ describe('PackagesApp', () => {
await doDeleteFile();
expect(createFlash).toHaveBeenCalledWith(
expect(createAlert).toHaveBeenCalledWith(
expect.objectContaining({
message: DELETE_PACKAGE_FILE_ERROR_MESSAGE,
}),
@ -468,7 +468,7 @@ describe('PackagesApp', () => {
await doDeleteFiles();
expect(createFlash).toHaveBeenCalledWith(
expect(createAlert).toHaveBeenCalledWith(
expect.objectContaining({
message: DELETE_PACKAGE_FILES_SUCCESS_MESSAGE,
}),
@ -484,7 +484,7 @@ describe('PackagesApp', () => {
await doDeleteFiles();
expect(createFlash).toHaveBeenCalledWith(
expect(createAlert).toHaveBeenCalledWith(
expect.objectContaining({
message: DELETE_PACKAGE_FILES_ERROR_MESSAGE,
}),
@ -501,7 +501,7 @@ describe('PackagesApp', () => {
await doDeleteFiles();
expect(createFlash).toHaveBeenCalledWith(
expect(createAlert).toHaveBeenCalledWith(
expect.objectContaining({
message: DELETE_PACKAGE_FILES_ERROR_MESSAGE,
}),

View File

@ -134,6 +134,40 @@ describe('CompareApp component', () => {
});
});
describe('mode dropdown', () => {
const findModeDropdownButton = () => wrapper.find('[data-testid="modeDropdown"]');
const findEnableStraightModeButton = () =>
wrapper.find('[data-testid="enableStraightModeButton"]');
const findDisableStraightModeButton = () =>
wrapper.find('[data-testid="disableStraightModeButton"]');
it('renders the mode dropdown button', () => {
expect(findModeDropdownButton().exists()).toBe(true);
});
it('has the correct text', () => {
expect(findEnableStraightModeButton().text()).toBe('...');
expect(findDisableStraightModeButton().text()).toBe('..');
});
it('straight mode button when clicked', async () => {
expect(wrapper.props('straight')).toBe(false);
expect(wrapper.find('input[name="straight"]').attributes('value')).toBe('false');
findEnableStraightModeButton().vm.$emit('click');
await nextTick();
expect(wrapper.find('input[name="straight"]').attributes('value')).toBe('true');
findDisableStraightModeButton().vm.$emit('click');
await nextTick();
expect(wrapper.find('input[name="straight"]').attributes('value')).toBe('false');
});
});
describe('merge request buttons', () => {
const findProjectMrButton = () => wrapper.find('[data-testid="projectMrButton"]');
const findCreateMrButton = () => wrapper.find('[data-testid="createMrButton"]');

View File

@ -17,6 +17,7 @@ export const appDefaultProps = {
projects: [sourceProject],
paramsFrom: 'main',
paramsTo: 'target/branch',
straight: false,
createMrPath: '',
sourceProjectRefsPath,
targetProjectRefsPath,

View File

@ -15,7 +15,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { IssuableType } from '~/issues/constants';
import { timeFor } from '~/lib/utils/datetime_utility';
@ -369,9 +369,9 @@ describe('SidebarDropdownWidget', () => {
findDropdownItemWithText('title').vm.$emit('click');
});
it(`calls createFlash with "${expectedMsg}"`, async () => {
it(`calls createAlert with "${expectedMsg}"`, async () => {
await nextTick();
expect(createFlash).toHaveBeenCalledWith({
expect(createAlert).toHaveBeenCalledWith({
message: expectedMsg,
captureError: true,
error: expectedMsg,
@ -455,14 +455,14 @@ describe('SidebarDropdownWidget', () => {
describe('milestones', () => {
let projectMilestonesSpy;
it('should call createFlash if milestones query fails', async () => {
it('should call createAlert if milestones query fails', async () => {
await createComponentWithApollo({
projectMilestonesSpy: jest.fn().mockRejectedValue(error),
});
await clickEdit();
expect(createFlash).toHaveBeenCalledWith({
expect(createAlert).toHaveBeenCalledWith({
message: wrapper.vm.i18n.listFetchError,
captureError: true,
error: expect.any(Error),
@ -514,12 +514,12 @@ describe('SidebarDropdownWidget', () => {
});
describe('currentAttributes', () => {
it('should call createFlash if currentAttributes query fails', async () => {
it('should call createAlert if currentAttributes query fails', async () => {
await createComponentWithApollo({
currentMilestoneSpy: jest.fn().mockRejectedValue(error),
});
expect(createFlash).toHaveBeenCalledWith({
expect(createAlert).toHaveBeenCalledWith({
message: wrapper.vm.i18n.currentFetchError,
captureError: true,
error: expect.any(Error),

View File

@ -4,7 +4,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import { createAlert } from '~/flash';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import SidebarSubscriptionWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
import issueSubscribedQuery from '~/sidebar/queries/issue_subscribed.query.graphql';
@ -144,7 +144,7 @@ describe('Sidebar Subscriptions Widget', () => {
});
await waitForPromises();
expect(createFlash).toHaveBeenCalled();
expect(createAlert).toHaveBeenCalled();
});
describe('merge request', () => {

View File

@ -6,7 +6,7 @@ import VueApollo from 'vue-apollo';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import { createAlert } from '~/flash';
import Report from '~/sidebar/components/time_tracking/report.vue';
import getIssueTimelogsQuery from '~/vue_shared/components/sidebar/queries/get_issue_timelogs.query.graphql';
import getMrTimelogsQuery from '~/vue_shared/components/sidebar/queries/get_mr_timelogs.query.graphql';
@ -65,7 +65,7 @@ describe('Issuable Time Tracking Report', () => {
mountComponent({ queryHandler: jest.fn().mockRejectedValue('ERROR') });
await waitForPromises();
expect(createFlash).toHaveBeenCalled();
expect(createAlert).toHaveBeenCalled();
});
describe('for issue', () => {
@ -153,7 +153,7 @@ describe('Issuable Time Tracking Report', () => {
await findDeleteButton().trigger('click');
await waitForPromises();
expect(createFlash).not.toHaveBeenCalled();
expect(createAlert).not.toHaveBeenCalled();
expect(mutateSpy).toHaveBeenCalledWith({
mutation: deleteTimelogMutation,
variables: {
@ -164,7 +164,7 @@ describe('Issuable Time Tracking Report', () => {
});
});
it('calls `createFlash` with errorMessage and does not remove the row on promise reject', async () => {
it('calls `createAlert` with errorMessage and does not remove the row on promise reject', async () => {
const mutateSpy = jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue({});
await waitForPromises();
@ -180,7 +180,7 @@ describe('Issuable Time Tracking Report', () => {
},
});
expect(createFlash).toHaveBeenCalledWith({
expect(createAlert).toHaveBeenCalledWith({
message: 'An error occurred while removing the timelog.',
captureError: true,
error: expect.any(Object),

View File

@ -4,7 +4,7 @@ import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import { createAlert } from '~/flash';
import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
import epicTodoQuery from '~/sidebar/queries/epic_todo.query.graphql';
import TodoButton from '~/vue_shared/components/sidebar/todo_toggle/todo_button.vue';
@ -83,7 +83,7 @@ describe('Sidebar Todo Widget', () => {
});
await waitForPromises();
expect(createFlash).toHaveBeenCalled();
expect(createAlert).toHaveBeenCalled();
});
describe('collapsed', () => {

View File

@ -1,7 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import SidebarMoveIssue from '~/sidebar/lib/sidebar_move_issue';
import SidebarService from '~/sidebar/services/sidebar_service';
@ -115,7 +115,7 @@ describe('SidebarMoveIssue', () => {
// Wait for the move issue request to fail
await waitForPromises();
expect(createFlash).toHaveBeenCalled();
expect(createAlert).toHaveBeenCalled();
expect(test.$confirmButton.prop('disabled')).toBe(false);
expect(test.$confirmButton.hasClass('is-loading')).toBe(false);
});

View File

@ -9,7 +9,7 @@ import { stubPerformanceWebAPI } from 'helpers/performance';
import waitForPromises from 'helpers/wait_for_promises';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import GetSnippetQuery from 'shared_queries/snippet/snippet.query.graphql';
import createFlash from '~/flash';
import { createAlert } from '~/flash';
import * as urlUtils from '~/lib/utils/url_utility';
import SnippetEditApp from '~/snippets/components/edit.vue';
import SnippetBlobActionsEdit from '~/snippets/components/snippet_blob_actions_edit.vue';
@ -361,7 +361,7 @@ describe('Snippet Edit app', () => {
await waitForPromises();
expect(urlUtils.redirectTo).not.toHaveBeenCalled();
expect(createFlash).toHaveBeenCalledWith({
expect(createAlert).toHaveBeenCalledWith({
message: `Can't create snippet: ${TEST_MUTATION_ERROR}`,
});
});
@ -385,7 +385,7 @@ describe('Snippet Edit app', () => {
});
expect(urlUtils.redirectTo).not.toHaveBeenCalled();
expect(createFlash).toHaveBeenCalledWith({
expect(createAlert).toHaveBeenCalledWith({
message: `Can't update snippet: ${TEST_MUTATION_ERROR}`,
});
},
@ -407,7 +407,7 @@ describe('Snippet Edit app', () => {
it('should flash', () => {
// Apollo automatically wraps the resolver's error in a NetworkError
expect(createFlash).toHaveBeenCalledWith({
expect(createAlert).toHaveBeenCalledWith({
message: `Can't update snippet: ${TEST_API_ERROR.message}`,
});
});

View File

@ -4,7 +4,7 @@ import AxiosMockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import BlobHeaderEdit from '~/blob/components/blob_edit_header.vue';
import createFlash from '~/flash';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { joinPaths } from '~/lib/utils/url_utility';
import SnippetBlobEdit from '~/snippets/components/snippet_blob_edit.vue';
@ -125,7 +125,7 @@ describe('Snippet Blob Edit component', () => {
it('should call flash', async () => {
await waitForPromises();
expect(createFlash).toHaveBeenCalledWith({
expect(createAlert).toHaveBeenCalledWith({
message: "Can't fetch content for the blob: Error: Request failed with status code 500",
});
});

View File

@ -10,7 +10,7 @@ import { differenceInMilliseconds } from '~/lib/utils/datetime_utility';
import SnippetHeader, { i18n } from '~/snippets/components/snippet_header.vue';
import DeleteSnippetMutation from '~/snippets/mutations/delete_snippet.mutation.graphql';
import axios from '~/lib/utils/axios_utils';
import createFlash, { FLASH_TYPES } from '~/flash';
import { createAlert, VARIANT_DANGER, VARIANT_SUCCESS } from '~/flash';
jest.mock('~/flash');
@ -267,9 +267,9 @@ describe('Snippet header component', () => {
});
it.each`
request | variant | text
${200} | ${'SUCCESS'} | ${i18n.snippetSpamSuccess}
${500} | ${'DANGER'} | ${i18n.snippetSpamFailure}
request | variant | text
${200} | ${VARIANT_SUCCESS} | ${i18n.snippetSpamSuccess}
${500} | ${VARIANT_DANGER} | ${i18n.snippetSpamFailure}
`(
'renders a "$variant" flash message with "$text" message for a request with a "$request" response',
async ({ request, variant, text }) => {
@ -278,9 +278,9 @@ describe('Snippet header component', () => {
submitAsSpamBtn.trigger('click');
await waitForPromises();
expect(createFlash).toHaveBeenLastCalledWith({
expect(createAlert).toHaveBeenLastCalledWith({
message: expect.stringContaining(text),
type: FLASH_TYPES[variant],
variant,
});
},
);

View File

@ -1,8 +1,6 @@
import Vue, { nextTick } from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import { mount } from '@vue/test-utils';
import { GREEN_BOX_IMAGE_URL, RED_BOX_IMAGE_URL } from 'spec/test_constants';
import diffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
describe('DiffViewer', () => {
const requiredProps = {
@ -14,37 +12,28 @@ describe('DiffViewer', () => {
oldPath: RED_BOX_IMAGE_URL,
oldSha: 'DEF',
};
let vm;
let wrapper;
function createComponent(props) {
const DiffViewer = Vue.extend(diffViewer);
vm = mountComponent(DiffViewer, props);
function createComponent(propsData) {
wrapper = mount(DiffViewer, { propsData });
}
afterEach(() => {
vm.$destroy();
wrapper.destroy();
});
it('renders image diff', async () => {
it('renders image diff', () => {
window.gon = {
relative_url_root: '',
};
createComponent({ ...requiredProps, projectPath: '' });
await nextTick();
expect(vm.$el.querySelector('.deleted img').getAttribute('src')).toBe(
`//-/raw/DEF/${RED_BOX_IMAGE_URL}`,
);
expect(vm.$el.querySelector('.added img').getAttribute('src')).toBe(
`//-/raw/ABC/${GREEN_BOX_IMAGE_URL}`,
);
expect(wrapper.find('.deleted img').attributes('src')).toBe(`//-/raw/DEF/${RED_BOX_IMAGE_URL}`);
expect(wrapper.find('.added img').attributes('src')).toBe(`//-/raw/ABC/${GREEN_BOX_IMAGE_URL}`);
});
it('renders fallback download diff display', async () => {
it('renders fallback download diff display', () => {
createComponent({
...requiredProps,
diffViewerMode: 'added',
@ -52,18 +41,10 @@ describe('DiffViewer', () => {
oldPath: 'testold.abc',
});
await nextTick();
expect(vm.$el.querySelector('.deleted .file-info').textContent.trim()).toContain('testold.abc');
expect(vm.$el.querySelector('.deleted .btn.btn-default').textContent.trim()).toContain(
'Download',
);
expect(vm.$el.querySelector('.added .file-info').textContent.trim()).toContain('test.abc');
expect(vm.$el.querySelector('.added .btn.btn-default').textContent.trim()).toContain(
'Download',
);
expect(wrapper.find('.deleted .file-info').text()).toContain('testold.abc');
expect(wrapper.find('.deleted .btn.btn-default').text()).toContain('Download');
expect(wrapper.find('.added .file-info').text()).toContain('test.abc');
expect(wrapper.find('.added .btn.btn-default').text()).toContain('Download');
});
describe('renamed file', () => {
@ -85,7 +66,7 @@ describe('DiffViewer', () => {
oldPath: 'testold.abc',
});
expect(vm.$el.textContent).toContain('File renamed with no changes.');
expect(wrapper.text()).toContain('File renamed with no changes.');
});
});
@ -99,6 +80,6 @@ describe('DiffViewer', () => {
bMode: '321',
});
expect(vm.$el.textContent).toContain('File mode changed from 123 to 321');
expect(wrapper.text()).toContain('File mode changed from 123 to 321');
});
});

View File

@ -1,127 +1,119 @@
import Vue, { nextTick } from 'vue';
import createComponent from 'helpers/vue_mount_component_helper';
import { mount } from '@vue/test-utils';
import { file } from 'jest/ide/helpers';
import ItemComponent from '~/vue_shared/components/file_finder/item.vue';
describe('File finder item spec', () => {
const Component = Vue.extend(ItemComponent);
let vm;
let localFile;
let wrapper;
beforeEach(() => {
localFile = {
...file(),
name: 'test file',
path: 'test/file',
};
vm = createComponent(Component, {
file: localFile,
focused: true,
searchText: '',
index: 0,
const createComponent = ({ file: customFileFields = {}, ...otherProps } = {}) => {
wrapper = mount(ItemComponent, {
propsData: {
file: {
...file(),
name: 'test file',
path: 'test/file',
...customFileFields,
},
focused: true,
searchText: '',
index: 0,
...otherProps,
},
});
});
};
afterEach(() => {
vm.$destroy();
wrapper.destroy();
});
it('renders file name & path', () => {
expect(vm.$el.textContent).toContain('test file');
expect(vm.$el.textContent).toContain('test/file');
createComponent();
expect(wrapper.text()).toContain('test file');
expect(wrapper.text()).toContain('test/file');
});
describe('focused', () => {
it('adds is-focused class', () => {
expect(vm.$el.classList).toContain('is-focused');
createComponent();
expect(wrapper.classes()).toContain('is-focused');
});
it('does not have is-focused class when not focused', async () => {
vm.focused = false;
createComponent({ focused: false });
await nextTick();
expect(vm.$el.classList).not.toContain('is-focused');
expect(wrapper.classes()).not.toContain('is-focused');
});
});
describe('changed file icon', () => {
it('does not render when not a changed or temp file', () => {
expect(vm.$el.querySelector('.diff-changed-stats')).toBe(null);
createComponent();
expect(wrapper.find('.diff-changed-stats').exists()).toBe(false);
});
it('renders when a changed file', async () => {
vm.file.changed = true;
createComponent({ file: { changed: true } });
await nextTick();
expect(vm.$el.querySelector('.diff-changed-stats')).not.toBe(null);
expect(wrapper.find('.diff-changed-stats').exists()).toBe(true);
});
it('renders when a temp file', async () => {
vm.file.tempFile = true;
createComponent({ file: { tempFile: true } });
await nextTick();
expect(vm.$el.querySelector('.diff-changed-stats')).not.toBe(null);
expect(wrapper.find('.diff-changed-stats').exists()).toBe(true);
});
});
it('emits event when clicked', () => {
jest.spyOn(vm, '$emit').mockImplementation(() => {});
it('emits event when clicked', async () => {
createComponent();
vm.$el.click();
await wrapper.find('*').trigger('click');
expect(vm.$emit).toHaveBeenCalledWith('click', vm.file);
expect(wrapper.emitted('click')[0]).toStrictEqual([wrapper.props('file')]);
});
describe('path', () => {
let el;
beforeEach(async () => {
vm.searchText = 'file';
el = vm.$el.querySelector('.diff-changed-file-path');
nextTick();
});
const findChangedFilePath = () => wrapper.find('.diff-changed-file-path');
it('highlights text', () => {
expect(el.querySelectorAll('.highlighted').length).toBe(4);
createComponent({ searchText: 'file' });
expect(findChangedFilePath().findAll('.highlighted')).toHaveLength(4);
});
it('adds ellipsis to long text', async () => {
vm.file.path = new Array(70)
const path = new Array(70)
.fill()
.map((_, i) => `${i}-`)
.join('');
await nextTick();
expect(el.textContent).toBe(`...${vm.file.path.substr(vm.file.path.length - 60)}`);
createComponent({ searchText: 'file', file: { path } });
expect(findChangedFilePath().text()).toBe(`...${path.substring(path.length - 60)}`);
});
});
describe('name', () => {
let el;
beforeEach(async () => {
vm.searchText = 'file';
el = vm.$el.querySelector('.diff-changed-file-name');
await nextTick();
});
const findChangedFileName = () => wrapper.find('.diff-changed-file-name');
it('highlights text', () => {
expect(el.querySelectorAll('.highlighted').length).toBe(4);
createComponent({ searchText: 'file' });
expect(findChangedFileName().findAll('.highlighted')).toHaveLength(4);
});
it('does not add ellipsis to long text', async () => {
vm.file.name = new Array(70)
const name = new Array(70)
.fill()
.map((_, i) => `${i}-`)
.join('');
await nextTick();
expect(el.textContent).not.toBe(`...${vm.file.name.substr(vm.file.name.length - 60)}`);
createComponent({ searchText: 'file', file: { name } });
expect(findChangedFileName().text()).not.toBe(`...${name.substring(name.length - 60)}`);
});
});
});

View File

@ -1,10 +1,9 @@
import Vue, { nextTick } from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import { mount } from '@vue/test-utils';
import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
describe('GlCountdown', () => {
const Component = Vue.extend(GlCountdown);
let vm;
let wrapper;
let now = '2000-01-01T00:00:00Z';
beforeEach(() => {
@ -12,21 +11,20 @@ describe('GlCountdown', () => {
});
afterEach(() => {
vm.$destroy();
jest.clearAllTimers();
wrapper.destroy();
});
describe('when there is time remaining', () => {
beforeEach(async () => {
vm = mountComponent(Component, {
endDateString: '2000-01-01T01:02:03Z',
wrapper = mount(GlCountdown, {
propsData: {
endDateString: '2000-01-01T01:02:03Z',
},
});
await nextTick();
});
it('displays remaining time', () => {
expect(vm.$el.textContent).toContain('01:02:03');
expect(wrapper.text()).toContain('01:02:03');
});
it('updates remaining time', async () => {
@ -34,21 +32,21 @@ describe('GlCountdown', () => {
jest.advanceTimersByTime(1000);
await nextTick();
expect(vm.$el.textContent).toContain('01:02:02');
expect(wrapper.text()).toContain('01:02:02');
});
});
describe('when there is no time remaining', () => {
beforeEach(async () => {
vm = mountComponent(Component, {
endDateString: '1900-01-01T00:00:00Z',
wrapper = mount(GlCountdown, {
propsData: {
endDateString: '1900-01-01T00:00:00Z',
},
});
await nextTick();
});
it('displays 00:00:00', () => {
expect(vm.$el.textContent).toContain('00:00:00');
expect(wrapper.text()).toContain('00:00:00');
});
});
@ -62,8 +60,10 @@ describe('GlCountdown', () => {
});
it('throws a validation error', () => {
vm = mountComponent(Component, {
endDateString: 'this is invalid',
wrapper = mount(GlCountdown, {
propsData: {
endDateString: 'this is invalid',
},
});
expect(Vue.config.warnHandler).toHaveBeenCalledTimes(1);

View File

@ -1,12 +1,10 @@
import Vue from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import panelResizer from '~/vue_shared/components/panel_resizer.vue';
import { mount } from '@vue/test-utils';
import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
describe('Panel Resizer component', () => {
let vm;
let PanelResizer;
let wrapper;
const triggerEvent = (eventName, el = vm.$el, clientX = 0) => {
const triggerEvent = (eventName, el = wrapper.element, clientX = 0) => {
const event = document.createEvent('MouseEvents');
event.initMouseEvent(
eventName,
@ -29,57 +27,64 @@ describe('Panel Resizer component', () => {
el.dispatchEvent(event);
};
beforeEach(() => {
PanelResizer = Vue.extend(panelResizer);
});
afterEach(() => {
vm.$destroy();
wrapper.destroy();
});
it('should render a div element with the correct classes and styles', () => {
vm = mountComponent(PanelResizer, {
startSize: 100,
side: 'left',
wrapper = mount(PanelResizer, {
propsData: {
startSize: 100,
side: 'left',
},
});
expect(vm.$el.tagName).toEqual('DIV');
expect(vm.$el.getAttribute('class')).toBe(
'position-absolute position-top-0 position-bottom-0 drag-handle position-left-0',
);
expect(wrapper.element.tagName).toEqual('DIV');
expect(wrapper.classes().sort()).toStrictEqual([
'drag-handle',
'position-absolute',
'position-bottom-0',
'position-left-0',
'position-top-0',
]);
expect(vm.$el.getAttribute('style')).toBe('cursor: ew-resize;');
expect(wrapper.element.getAttribute('style')).toBe('cursor: ew-resize;');
});
it('should render a div element with the correct classes for a right side panel', () => {
vm = mountComponent(PanelResizer, {
startSize: 100,
side: 'right',
wrapper = mount(PanelResizer, {
propsData: {
startSize: 100,
side: 'right',
},
});
expect(vm.$el.tagName).toEqual('DIV');
expect(vm.$el.getAttribute('class')).toBe(
'position-absolute position-top-0 position-bottom-0 drag-handle position-right-0',
);
expect(wrapper.element.tagName).toEqual('DIV');
expect(wrapper.classes().sort()).toStrictEqual([
'drag-handle',
'position-absolute',
'position-bottom-0',
'position-right-0',
'position-top-0',
]);
});
it('drag the resizer', () => {
vm = mountComponent(PanelResizer, {
startSize: 100,
side: 'left',
wrapper = mount(PanelResizer, {
propsData: {
startSize: 100,
side: 'left',
},
});
jest.spyOn(vm, '$emit').mockImplementation(() => {});
triggerEvent('mousedown', vm.$el);
triggerEvent('mousedown');
triggerEvent('mousemove', document);
triggerEvent('mouseup', document);
expect(vm.$emit.mock.calls).toEqual([
['resize-start', 100],
['update:size', 100],
['resize-end', 100],
]);
expect(vm.size).toBe(100);
expect(wrapper.emitted()).toEqual({
'resize-start': [[100]],
'update:size': [[100]],
'resize-end': [[100]],
});
});
});

View File

@ -1,121 +1,109 @@
import Vue from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import stackedProgressBarComponent from '~/vue_shared/components/stacked_progress_bar.vue';
const createComponent = (config) => {
const Component = Vue.extend(stackedProgressBarComponent);
const defaultConfig = {
successLabel: 'Synced',
failureLabel: 'Failed',
neutralLabel: 'Out of sync',
successCount: 25,
failureCount: 10,
totalCount: 5000,
...config,
};
return mountComponent(Component, defaultConfig);
};
import { mount } from '@vue/test-utils';
import StackedProgressBarComponent from '~/vue_shared/components/stacked_progress_bar.vue';
describe('StackedProgressBarComponent', () => {
let vm;
let wrapper;
beforeEach(() => {
vm = createComponent();
});
const createComponent = (config) => {
const defaultConfig = {
successLabel: 'Synced',
failureLabel: 'Failed',
neutralLabel: 'Out of sync',
successCount: 25,
failureCount: 10,
totalCount: 5000,
...config,
};
wrapper = mount(StackedProgressBarComponent, { propsData: defaultConfig });
};
afterEach(() => {
vm.$destroy();
wrapper.destroy();
});
const findSuccessBarText = (wrapper) =>
wrapper.$el.querySelector('.status-green').innerText.trim();
const findNeutralBarText = (wrapper) =>
wrapper.$el.querySelector('.status-neutral').innerText.trim();
const findFailureBarText = (wrapper) => wrapper.$el.querySelector('.status-red').innerText.trim();
const findUnavailableBarText = (wrapper) =>
wrapper.$el.querySelector('.status-unavailable').innerText.trim();
describe('computed', () => {
describe('neutralCount', () => {
it('returns neutralCount based on totalCount, successCount and failureCount', () => {
expect(vm.neutralCount).toBe(4965); // 5000 - 25 - 10
});
});
});
const findSuccessBar = () => wrapper.find('.status-green');
const findNeutralBar = () => wrapper.find('.status-neutral');
const findFailureBar = () => wrapper.find('.status-red');
const findUnavailableBar = () => wrapper.find('.status-unavailable');
describe('template', () => {
it('renders container element', () => {
expect(vm.$el.classList.contains('stacked-progress-bar')).toBe(true);
createComponent();
expect(wrapper.classes()).toContain('stacked-progress-bar');
});
it('renders empty state when count is unavailable', () => {
const vmX = createComponent({ totalCount: 0, successCount: 0, failureCount: 0 });
createComponent({ totalCount: 0, successCount: 0, failureCount: 0 });
expect(findUnavailableBarText(vmX)).not.toBeUndefined();
expect(findUnavailableBar()).not.toBeUndefined();
});
it('renders bar elements when count is available', () => {
expect(findSuccessBarText(vm)).not.toBeUndefined();
expect(findNeutralBarText(vm)).not.toBeUndefined();
expect(findFailureBarText(vm)).not.toBeUndefined();
createComponent();
expect(findSuccessBar().exists()).toBe(true);
expect(findNeutralBar().exists()).toBe(true);
expect(findFailureBar().exists()).toBe(true);
});
describe('getPercent', () => {
it('returns correct percentages from provided count based on `totalCount`', () => {
vm = createComponent({ totalCount: 100, successCount: 25, failureCount: 10 });
createComponent({ totalCount: 100, successCount: 25, failureCount: 10 });
expect(findSuccessBarText(vm)).toBe('25%');
expect(findNeutralBarText(vm)).toBe('65%');
expect(findFailureBarText(vm)).toBe('10%');
expect(findSuccessBar().text()).toBe('25%');
expect(findNeutralBar().text()).toBe('65%');
expect(findFailureBar().text()).toBe('10%');
});
it('returns percentage with decimal place when decimal is greater than 1', () => {
vm = createComponent({ successCount: 67 });
createComponent({ successCount: 67 });
expect(findSuccessBarText(vm)).toBe('1.3%');
expect(findSuccessBar().text()).toBe('1.3%');
});
it('returns percentage as `< 1%` from provided count based on `totalCount` when evaluated value is less than 1', () => {
vm = createComponent({ successCount: 10 });
createComponent({ successCount: 10 });
expect(findSuccessBarText(vm)).toBe('< 1%');
expect(findSuccessBar().text()).toBe('< 1%');
});
it('returns not available if totalCount is falsy', () => {
vm = createComponent({ totalCount: 0 });
createComponent({ totalCount: 0 });
expect(findUnavailableBarText(vm)).toBe('Not available');
expect(findUnavailableBar().text()).toBe('Not available');
});
it('returns 99.9% when numbers are extreme decimals', () => {
vm = createComponent({ totalCount: 1000000 });
createComponent({ totalCount: 1000000 });
expect(findNeutralBarText(vm)).toBe('99.9%');
expect(findNeutralBar().text()).toBe('99.9%');
});
});
describe('barStyle', () => {
it('returns style string based on percentage provided', () => {
expect(vm.barStyle(50)).toBe('width: 50%;');
describe('bar style', () => {
it('renders width based on percentage provided', () => {
createComponent({ totalCount: 100, successCount: 25 });
expect(findSuccessBar().element.style.width).toBe('25%');
});
});
describe('getTooltip', () => {
describe('tooltip', () => {
describe('when hideTooltips is false', () => {
it('returns label string based on label and count provided', () => {
expect(vm.getTooltip('Synced', 10)).toBe('Synced: 10');
createComponent({ successCount: 10, successLabel: 'Synced', hideTooltips: false });
expect(findSuccessBar().attributes('title')).toBe('Synced: 10');
});
});
describe('when hideTooltips is true', () => {
beforeEach(() => {
vm = createComponent({ hideTooltips: true });
});
it('returns an empty string', () => {
expect(vm.getTooltip('Synced', 10)).toBe('');
createComponent({ successCount: 10, successLabel: 'Synced', hideTooltips: true });
expect(findSuccessBar().attributes('title')).toBe('');
});
});
});

View File

@ -3359,6 +3359,73 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
end
end
describe '#drop_constraint' do
it "executes the statement to drop the constraint" do
expect(model).to receive(:execute).with("ALTER TABLE \"test_table\" DROP CONSTRAINT \"constraint_name\" CASCADE\n")
model.drop_constraint(:test_table, :constraint_name, cascade: true)
end
context 'when cascade option is false' do
it "executes the statement to drop the constraint without cascade" do
expect(model).to receive(:execute).with("ALTER TABLE \"test_table\" DROP CONSTRAINT \"constraint_name\" \n")
model.drop_constraint(:test_table, :constraint_name, cascade: false)
end
end
end
describe '#add_primary_key_using_index' do
it "executes the statement to add the primary key" do
expect(model).to receive(:execute).with /ALTER TABLE "test_table" ADD CONSTRAINT "old_name" PRIMARY KEY USING INDEX "new_name"/
model.add_primary_key_using_index(:test_table, :old_name, :new_name)
end
end
context 'when changing the primary key of a given table' do
before do
model.create_table(:test_table, primary_key: :id) do |t|
t.integer :partition_number, default: 1
end
model.add_index(:test_table, :id, unique: true, name: :old_index_name)
model.add_index(:test_table, [:id, :partition_number], unique: true, name: :new_index_name)
end
describe '#swap_primary_key' do
it 'executes statements to swap primary key', :aggregate_failures do
expect(model).to receive(:with_lock_retries).with(raise_on_exhaustion: true).ordered.and_yield
expect(model).to receive(:execute).with(/ALTER TABLE "test_table" DROP CONSTRAINT "test_table_pkey" CASCADE/).and_call_original
expect(model).to receive(:execute).with(/ALTER TABLE "test_table" ADD CONSTRAINT "test_table_pkey" PRIMARY KEY USING INDEX "new_index_name"/).and_call_original
model.swap_primary_key(:test_table, :test_table_pkey, :new_index_name)
end
context 'when new index does not exist' do
before do
model.remove_index(:test_table, column: [:id, :partition_number])
end
it 'raises ActiveRecord::StatementInvalid' do
expect do
model.swap_primary_key(:test_table, :test_table_pkey, :new_index_name)
end.to raise_error(ActiveRecord::StatementInvalid)
end
end
end
describe '#unswap_primary_key' do
it 'executes statements to unswap primary key' do
expect(model).to receive(:with_lock_retries).with(raise_on_exhaustion: true).ordered.and_yield
expect(model).to receive(:execute).with(/ALTER TABLE "test_table" DROP CONSTRAINT "test_table_pkey" CASCADE/).ordered.and_call_original
expect(model).to receive(:execute).with(/ALTER TABLE "test_table" ADD CONSTRAINT "test_table_pkey" PRIMARY KEY USING INDEX "old_index_name"/).ordered.and_call_original
model.unswap_primary_key(:test_table, :test_table_pkey, :old_index_name)
end
end
end
describe '#drop_sequence' do
it "executes the statement to drop the sequence" do
expect(model).to receive(:execute).with /ALTER TABLE "test_table" ALTER COLUMN "test_column" DROP DEFAULT;\nDROP SEQUENCE IF EXISTS "test_table_id_seq"/

View File

@ -99,7 +99,6 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
'incident_management_oncall',
'testing',
'issues_edit',
'ci_secrets_management',
'snippets',
'code_review',
'terraform',

View File

@ -43,18 +43,19 @@ RSpec.describe Ci::UnitTest do
result = described_class.find_or_create_by_batch(project, attrs)
expect(result).to match_array([
have_attributes(
key_hash: existing_test.key_hash,
suite_name: 'rspec',
name: 'Math#sum adds numbers'
),
have_attributes(
key_hash: new_key,
suite_name: 'jest',
name: 'Component works'
)
])
expect(result).to match_array(
[
have_attributes(
key_hash: existing_test.key_hash,
suite_name: 'rspec',
name: 'Math#sum adds numbers'
),
have_attributes(
key_hash: new_key,
suite_name: 'jest',
name: 'Component works'
)
])
expect(result).to all(be_persisted)
end
@ -77,13 +78,14 @@ RSpec.describe Ci::UnitTest do
result = described_class.find_or_create_by_batch(project, attrs)
expect(result).to match_array([
have_attributes(
key_hash: new_key,
suite_name: 'abc...',
name: 'abc...'
)
])
expect(result).to match_array(
[
have_attributes(
key_hash: new_key,
suite_name: 'abc...',
name: 'abc...'
)
])
expect(result).to all(be_persisted)
end

View File

@ -49,13 +49,15 @@ RSpec.describe Clusters::Applications::CertManager do
expect(subject.version).to eq('v0.10.1')
expect(subject).to be_rbac
expect(subject.files).to eq(cert_manager.files.merge(cluster_issuer_file))
expect(subject.preinstall).to eq([
'kubectl apply -f https://raw.githubusercontent.com/jetstack/cert-manager/release-0.10/deploy/manifests/00-crds.yaml',
'kubectl label --overwrite namespace gitlab-managed-apps certmanager.k8s.io/disable-validation=true'
])
expect(subject.postinstall).to eq([
"for i in $(seq 1 90); do kubectl apply -f /data/helm/certmanager/config/cluster_issuer.yaml && s=0 && break || s=$?; sleep 1s; echo \"Retrying ($i)...\"; done; (exit $s)"
])
expect(subject.preinstall).to eq(
[
'kubectl apply -f https://raw.githubusercontent.com/jetstack/cert-manager/release-0.10/deploy/manifests/00-crds.yaml',
'kubectl label --overwrite namespace gitlab-managed-apps certmanager.k8s.io/disable-validation=true'
])
expect(subject.postinstall).to eq(
[
"for i in $(seq 1 90); do kubectl apply -f /data/helm/certmanager/config/cluster_issuer.yaml && s=0 && break || s=$?; sleep 1s; echo \"Retrying ($i)...\"; done; (exit $s)"
])
end
context 'for a specific user' do
@ -99,15 +101,16 @@ RSpec.describe Clusters::Applications::CertManager do
end
it 'specifies a post delete command to remove custom resource definitions' do
expect(subject.postdelete).to eq([
'kubectl delete secret -n gitlab-managed-apps letsencrypt-prod --ignore-not-found',
'kubectl delete crd certificates.certmanager.k8s.io --ignore-not-found',
'kubectl delete crd certificaterequests.certmanager.k8s.io --ignore-not-found',
'kubectl delete crd challenges.certmanager.k8s.io --ignore-not-found',
'kubectl delete crd clusterissuers.certmanager.k8s.io --ignore-not-found',
'kubectl delete crd issuers.certmanager.k8s.io --ignore-not-found',
'kubectl delete crd orders.certmanager.k8s.io --ignore-not-found'
])
expect(subject.postdelete).to eq(
[
'kubectl delete secret -n gitlab-managed-apps letsencrypt-prod --ignore-not-found',
'kubectl delete crd certificates.certmanager.k8s.io --ignore-not-found',
'kubectl delete crd certificaterequests.certmanager.k8s.io --ignore-not-found',
'kubectl delete crd challenges.certmanager.k8s.io --ignore-not-found',
'kubectl delete crd clusterissuers.certmanager.k8s.io --ignore-not-found',
'kubectl delete crd issuers.certmanager.k8s.io --ignore-not-found',
'kubectl delete crd orders.certmanager.k8s.io --ignore-not-found'
])
end
context 'secret key name is not found' do
@ -119,14 +122,15 @@ RSpec.describe Clusters::Applications::CertManager do
end
it 'does not try and delete the secret' do
expect(subject.postdelete).to eq([
'kubectl delete crd certificates.certmanager.k8s.io --ignore-not-found',
'kubectl delete crd certificaterequests.certmanager.k8s.io --ignore-not-found',
'kubectl delete crd challenges.certmanager.k8s.io --ignore-not-found',
'kubectl delete crd clusterissuers.certmanager.k8s.io --ignore-not-found',
'kubectl delete crd issuers.certmanager.k8s.io --ignore-not-found',
'kubectl delete crd orders.certmanager.k8s.io --ignore-not-found'
])
expect(subject.postdelete).to eq(
[
'kubectl delete crd certificates.certmanager.k8s.io --ignore-not-found',
'kubectl delete crd certificaterequests.certmanager.k8s.io --ignore-not-found',
'kubectl delete crd challenges.certmanager.k8s.io --ignore-not-found',
'kubectl delete crd clusterissuers.certmanager.k8s.io --ignore-not-found',
'kubectl delete crd issuers.certmanager.k8s.io --ignore-not-found',
'kubectl delete crd orders.certmanager.k8s.io --ignore-not-found'
])
end
end
end

View File

@ -601,19 +601,27 @@ RSpec.describe Clusters::Platforms::Kubernetes do
it 'creates a matching RolloutStatus' do
expect(rollout_status).to be_kind_of(::Gitlab::Kubernetes::RolloutStatus)
expect(rollout_status.deployments.map(&:annotations)).to eq([
{ 'app.gitlab.com/app' => project.full_path_slug, 'app.gitlab.com/env' => 'env-000000' }
])
expect(rollout_status.instances).to eq([{ pod_name: "kube-pod",
stable: true,
status: "pending",
tooltip: "kube-pod (Pending)",
track: "stable" },
{ pod_name: "Not provided",
stable: true,
status: "pending",
tooltip: "Not provided (Pending)",
track: "stable" }])
expect(rollout_status.deployments.map(&:annotations)).to eq(
[
{ 'app.gitlab.com/app' => project.full_path_slug, 'app.gitlab.com/env' => 'env-000000' }
])
expect(rollout_status.instances).to eq(
[
{
pod_name: "kube-pod",
stable: true,
status: "pending",
tooltip: "kube-pod (Pending)",
track: "stable"
},
{
pod_name: "Not provided",
stable: true,
status: "pending",
tooltip: "Not provided (Pending)",
track: "stable"
}
])
end
context 'with canary ingress' do
@ -720,11 +728,12 @@ RSpec.describe Clusters::Platforms::Kubernetes do
end
it 'returns a pending pod for each missing replica' do
expect(rollout_status.instances.map { |p| p.slice(:pod_name, :status) }).to eq([
{ pod_name: 'pod-a-1', status: 'running' },
{ pod_name: 'Not provided', status: 'pending' },
{ pod_name: 'Not provided', status: 'pending' }
])
expect(rollout_status.instances.map { |p| p.slice(:pod_name, :status) }).to eq(
[
{ pod_name: 'pod-a-1', status: 'running' },
{ pod_name: 'Not provided', status: 'pending' },
{ pod_name: 'Not provided', status: 'pending' }
])
end
end
@ -743,12 +752,13 @@ RSpec.describe Clusters::Platforms::Kubernetes do
end
it 'returns the correct track for the pending pods' do
expect(rollout_status.instances.map { |p| p.slice(:pod_name, :status, :track) }).to eq([
{ pod_name: 'pod-a-1', status: 'running', track: 'canary' },
{ pod_name: 'Not provided', status: 'pending', track: 'canary' },
{ pod_name: 'Not provided', status: 'pending', track: 'stable' },
{ pod_name: 'Not provided', status: 'pending', track: 'stable' }
])
expect(rollout_status.instances.map { |p| p.slice(:pod_name, :status, :track) }).to eq(
[
{ pod_name: 'pod-a-1', status: 'running', track: 'canary' },
{ pod_name: 'Not provided', status: 'pending', track: 'canary' },
{ pod_name: 'Not provided', status: 'pending', track: 'stable' },
{ pod_name: 'Not provided', status: 'pending', track: 'stable' }
])
end
end
@ -765,10 +775,11 @@ RSpec.describe Clusters::Platforms::Kubernetes do
end
it 'returns the correct number of pending pods' do
expect(rollout_status.instances.map { |p| p.slice(:pod_name, :status, :track) }).to eq([
{ pod_name: 'Not provided', status: 'pending', track: 'mytrack' },
{ pod_name: 'Not provided', status: 'pending', track: 'mytrack' }
])
expect(rollout_status.instances.map { |p| p.slice(:pod_name, :status, :track) }).to eq(
[
{ pod_name: 'Not provided', status: 'pending', track: 'mytrack' },
{ pod_name: 'Not provided', status: 'pending', track: 'mytrack' }
])
end
end

View File

@ -42,10 +42,7 @@ RSpec.describe CommitCollection do
merge_commit = project.commit("60ecb67744cb56576c30214ff52294f8ce2def98")
expect(merge_commit).to receive(:merge_commit?).and_return(true)
collection = described_class.new(project, [
commit,
merge_commit
])
collection = described_class.new(project, [commit, merge_commit])
expect(collection.without_merge_commits).to contain_exactly(commit)
end

View File

@ -127,13 +127,14 @@ RSpec.describe Compare do
end
it 'returns affected file paths, without duplication' do
expect(subject.modified_paths).to contain_exactly(*%w{
foo/for_move.txt
foo/bar/for_move.txt
foo/for_create.txt
foo/for_delete.txt
foo/for_edit.txt
})
expect(subject.modified_paths).to contain_exactly(
*%w{
foo/for_move.txt
foo/bar/for_move.txt
foo/for_create.txt
foo/for_delete.txt
foo/for_edit.txt
})
end
end

View File

@ -12,9 +12,10 @@ RSpec.describe IdInOrdered do
issue4 = create(:issue)
issue5 = create(:issue)
expect(Issue.id_in_ordered([issue3.id, issue1.id, issue4.id, issue5.id, issue2.id])).to eq([
issue3, issue1, issue4, issue5, issue2
])
expect(Issue.id_in_ordered([issue3.id, issue1.id, issue4.id, issue5.id, issue2.id])).to eq(
[
issue3, issue1, issue4, issue5, issue2
])
end
context 'when the ids are not an array of integers' do

View File

@ -47,18 +47,19 @@ RSpec.describe Noteable do
let(:discussions) { subject.discussions }
it 'includes discussions for diff notes, commit diff notes, commit notes, and regular notes' do
expect(discussions).to eq([
DiffDiscussion.new([active_diff_note1, active_diff_note2], subject),
DiffDiscussion.new([active_diff_note3], subject),
DiffDiscussion.new([outdated_diff_note1, outdated_diff_note2], subject),
Discussion.new([discussion_note1, discussion_note2], subject),
DiffDiscussion.new([commit_diff_note1, commit_diff_note2], subject),
OutOfContextDiscussion.new([commit_note1, commit_note2], subject),
Discussion.new([commit_discussion_note1, commit_discussion_note2], subject),
Discussion.new([commit_discussion_note3], subject),
IndividualNoteDiscussion.new([note1], subject),
IndividualNoteDiscussion.new([note2], subject)
])
expect(discussions).to eq(
[
DiffDiscussion.new([active_diff_note1, active_diff_note2], subject),
DiffDiscussion.new([active_diff_note3], subject),
DiffDiscussion.new([outdated_diff_note1, outdated_diff_note2], subject),
Discussion.new([discussion_note1, discussion_note2], subject),
DiffDiscussion.new([commit_diff_note1, commit_diff_note2], subject),
OutOfContextDiscussion.new([commit_note1, commit_note2], subject),
Discussion.new([commit_discussion_note1, commit_discussion_note2], subject),
Discussion.new([commit_discussion_note3], subject),
IndividualNoteDiscussion.new([note1], subject),
IndividualNoteDiscussion.new([note2], subject)
])
end
end
@ -88,23 +89,24 @@ RSpec.describe Noteable do
{ table_name: n.table_name, discussion_id: n.discussion_id, id: n.id }
end
expect(discussions).to match([
a_hash_including(table_name: 'notes', discussion_id: active_diff_note1.discussion_id),
a_hash_including(table_name: 'notes', discussion_id: active_diff_note3.discussion_id),
a_hash_including(table_name: 'notes', discussion_id: outdated_diff_note1.discussion_id),
a_hash_including(table_name: 'notes', discussion_id: discussion_note1.discussion_id),
a_hash_including(table_name: 'notes', discussion_id: commit_diff_note1.discussion_id),
a_hash_including(table_name: 'notes', discussion_id: commit_note1.discussion_id),
a_hash_including(table_name: 'notes', discussion_id: commit_note2.discussion_id),
a_hash_including(table_name: 'notes', discussion_id: commit_discussion_note1.discussion_id),
a_hash_including(table_name: 'notes', discussion_id: commit_discussion_note3.discussion_id),
a_hash_including(table_name: 'notes', discussion_id: note1.discussion_id),
a_hash_including(table_name: 'notes', discussion_id: note2.discussion_id),
a_hash_including(table_name: 'resource_label_events', id: label_event.id),
a_hash_including(table_name: 'notes', discussion_id: system_note.discussion_id),
a_hash_including(table_name: 'resource_milestone_events', id: milestone_event.id),
a_hash_including(table_name: 'resource_state_events', id: state_event.id)
])
expect(discussions).to match(
[
a_hash_including(table_name: 'notes', discussion_id: active_diff_note1.discussion_id),
a_hash_including(table_name: 'notes', discussion_id: active_diff_note3.discussion_id),
a_hash_including(table_name: 'notes', discussion_id: outdated_diff_note1.discussion_id),
a_hash_including(table_name: 'notes', discussion_id: discussion_note1.discussion_id),
a_hash_including(table_name: 'notes', discussion_id: commit_diff_note1.discussion_id),
a_hash_including(table_name: 'notes', discussion_id: commit_note1.discussion_id),
a_hash_including(table_name: 'notes', discussion_id: commit_note2.discussion_id),
a_hash_including(table_name: 'notes', discussion_id: commit_discussion_note1.discussion_id),
a_hash_including(table_name: 'notes', discussion_id: commit_discussion_note3.discussion_id),
a_hash_including(table_name: 'notes', discussion_id: note1.discussion_id),
a_hash_including(table_name: 'notes', discussion_id: note2.discussion_id),
a_hash_including(table_name: 'resource_label_events', id: label_event.id),
a_hash_including(table_name: 'notes', discussion_id: system_note.discussion_id),
a_hash_including(table_name: 'resource_milestone_events', id: milestone_event.id),
a_hash_including(table_name: 'resource_state_events', id: state_event.id)
])
end
it 'filters by comments only' do
@ -112,19 +114,20 @@ RSpec.describe Noteable do
{ table_name: n.table_name, discussion_id: n.discussion_id, id: n.id }
end
expect(discussions).to match([
a_hash_including(table_name: 'notes', discussion_id: active_diff_note1.discussion_id),
a_hash_including(table_name: 'notes', discussion_id: active_diff_note3.discussion_id),
a_hash_including(table_name: 'notes', discussion_id: outdated_diff_note1.discussion_id),
a_hash_including(table_name: 'notes', discussion_id: discussion_note1.discussion_id),
a_hash_including(table_name: 'notes', discussion_id: commit_diff_note1.discussion_id),
a_hash_including(table_name: 'notes', discussion_id: commit_note1.discussion_id),
a_hash_including(table_name: 'notes', discussion_id: commit_note2.discussion_id),
a_hash_including(table_name: 'notes', discussion_id: commit_discussion_note1.discussion_id),
a_hash_including(table_name: 'notes', discussion_id: commit_discussion_note3.discussion_id),
a_hash_including(table_name: 'notes', discussion_id: note1.discussion_id),
a_hash_including(table_name: 'notes', discussion_id: note2.discussion_id)
])
expect(discussions).to match(
[
a_hash_including(table_name: 'notes', discussion_id: active_diff_note1.discussion_id),
a_hash_including(table_name: 'notes', discussion_id: active_diff_note3.discussion_id),
a_hash_including(table_name: 'notes', discussion_id: outdated_diff_note1.discussion_id),
a_hash_including(table_name: 'notes', discussion_id: discussion_note1.discussion_id),
a_hash_including(table_name: 'notes', discussion_id: commit_diff_note1.discussion_id),
a_hash_including(table_name: 'notes', discussion_id: commit_note1.discussion_id),
a_hash_including(table_name: 'notes', discussion_id: commit_note2.discussion_id),
a_hash_including(table_name: 'notes', discussion_id: commit_discussion_note1.discussion_id),
a_hash_including(table_name: 'notes', discussion_id: commit_discussion_note3.discussion_id),
a_hash_including(table_name: 'notes', discussion_id: note1.discussion_id),
a_hash_including(table_name: 'notes', discussion_id: note2.discussion_id)
])
end
it 'filters by system notes only' do
@ -132,12 +135,13 @@ RSpec.describe Noteable do
{ table_name: n.table_name, discussion_id: n.discussion_id, id: n.id }
end
expect(discussions).to match([
a_hash_including(table_name: 'resource_label_events', id: label_event.id),
a_hash_including(table_name: 'notes', discussion_id: system_note.discussion_id),
a_hash_including(table_name: 'resource_milestone_events', id: milestone_event.id),
a_hash_including(table_name: 'resource_state_events', id: state_event.id)
])
expect(discussions).to match(
[
a_hash_including(table_name: 'resource_label_events', id: label_event.id),
a_hash_including(table_name: 'notes', discussion_id: system_note.discussion_id),
a_hash_including(table_name: 'resource_milestone_events', id: milestone_event.id),
a_hash_including(table_name: 'resource_state_events', id: state_event.id)
])
end
end

View File

@ -541,11 +541,12 @@ RSpec.describe DiffNote do
describe '#shas' do
it 'returns list of SHAs based on original_position' do
expect(subject.shas).to match_array([
position.base_sha,
position.start_sha,
position.head_sha
])
expect(subject.shas).to match_array(
[
position.base_sha,
position.start_sha,
position.head_sha
])
end
context 'when position changes' do
@ -554,14 +555,15 @@ RSpec.describe DiffNote do
end
it 'includes the new position SHAs' do
expect(subject.shas).to match_array([
position.base_sha,
position.start_sha,
position.head_sha,
new_position.base_sha,
new_position.start_sha,
new_position.head_sha
])
expect(subject.shas).to match_array(
[
position.base_sha,
position.start_sha,
position.head_sha,
new_position.base_sha,
new_position.start_sha,
new_position.head_sha
])
end
end
end

View File

@ -37,10 +37,11 @@ RSpec.describe Discussion do
describe '.build_collection' do
it 'returns an array of discussions of the right type' do
discussions = described_class.build_collection([first_note, second_note, third_note], merge_request)
expect(discussions).to eq([
DiffDiscussion.new([first_note, second_note], merge_request),
DiffDiscussion.new([third_note], merge_request)
])
expect(discussions).to eq(
[
DiffDiscussion.new([first_note, second_note], merge_request),
DiffDiscussion.new([third_note], merge_request)
])
end
end

View File

@ -2293,10 +2293,11 @@ RSpec.describe Group do
it 'clears both self and descendant cache when the parent value is updated' do
expect(Rails.cache).to receive(:delete_multi)
.with(
match_array([
start_with("namespaces:{#{parent.traversal_ids.first}}:first_auto_devops_config:#{parent.id}"),
start_with("namespaces:{#{parent.traversal_ids.first}}:first_auto_devops_config:#{group.id}")
])
match_array(
[
start_with("namespaces:{#{parent.traversal_ids.first}}:first_auto_devops_config:#{parent.id}"),
start_with("namespaces:{#{parent.traversal_ids.first}}:first_auto_devops_config:#{group.id}")
])
)
parent.update!(auto_devops_enabled: true)

View File

@ -971,11 +971,12 @@ RSpec.describe Integration do
describe '#secret_fields' do
it 'returns all fields with type `password`' do
allow(subject).to receive(:fields).and_return([
{ name: 'password', type: 'password' },
{ name: 'secret', type: 'password' },
{ name: 'public', type: 'text' }
])
allow(subject).to receive(:fields).and_return(
[
{ name: 'password', type: 'password' },
{ name: 'secret', type: 'password' },
{ name: 'public', type: 'text' }
])
expect(subject.secret_fields).to match_array(%w[password secret])
end

View File

@ -47,14 +47,15 @@ RSpec.describe Integrations::ChatMessage::IssueMessage do
it 'returns a message regarding opening of issues' do
expect(subject.pretext).to eq(
'[<http://somewhere.com|project_name>] Issue <http://url.com|#100 Issue title> opened by Test User (test.user)')
expect(subject.attachments).to eq([
{
title: "#100 Issue title",
title_link: "http://url.com",
text: "issue description",
color: color
}
])
expect(subject.attachments).to eq(
[
{
title: "#100 Issue title",
title_link: "http://url.com",
text: "issue description",
color: color
}
])
end
end

View File

@ -71,12 +71,13 @@ RSpec.describe Integrations::ChatMessage::WikiPageMessage do
end
it 'returns the commit message for a new wiki page' do
expect(subject.attachments).to eq([
{
text: commit_message,
color: color
}
])
expect(subject.attachments).to eq(
[
{
text: commit_message,
color: color
}
])
end
end
@ -86,12 +87,13 @@ RSpec.describe Integrations::ChatMessage::WikiPageMessage do
end
it 'returns the commit message for an updated wiki page' do
expect(subject.attachments).to eq([
{
text: commit_message,
color: color
}
])
expect(subject.attachments).to eq(
[
{
text: commit_message,
color: color
}
])
end
end
end

View File

@ -81,9 +81,10 @@ RSpec.describe Integrations::Jira do
jira_integration.jira_issue_transition_id = 'foo bar'
expect(jira_integration).not_to be_valid
expect(jira_integration.errors.full_messages).to eq([
'Jira issue transition IDs must be a list of numbers that can be split with , or ;'
])
expect(jira_integration.errors.full_messages).to eq(
[
'Jira issue transition IDs must be a list of numbers that can be split with , or ;'
])
end
end
end

View File

@ -18,9 +18,10 @@ RSpec.describe LabelNote do
it_behaves_like 'label note created from events'
it 'includes a link to the list of issues filtered by the label' do
note = described_class.from_events([
create(:resource_label_event, label: label, issue: resource)
])
note = described_class.from_events(
[
create(:resource_label_event, label: label, issue: resource)
])
expect(note.note_html).to include(project_issues_path(project, label_name: label.title))
end
@ -32,9 +33,10 @@ RSpec.describe LabelNote do
it_behaves_like 'label note created from events'
it 'includes a link to the list of merge requests filtered by the label' do
note = described_class.from_events([
create(:resource_label_event, label: label, merge_request: resource)
])
note = described_class.from_events(
[
create(:resource_label_event, label: label, merge_request: resource)
])
expect(note.note_html).to include(project_merge_requests_path(project, label_name: label.title))
end

View File

@ -111,12 +111,13 @@ RSpec.describe MergeRequest::CleanupSchedule do
let!(:cleanup_schedule_7) { create(:merge_request_cleanup_schedule, :failed, scheduled_at: 5.days.ago) }
it 'returns records that are scheduled before or on current time and unstarted (ordered by scheduled first)' do
expect(described_class.scheduled_and_unstarted).to eq([
cleanup_schedule_2,
cleanup_schedule_1,
cleanup_schedule_5,
cleanup_schedule_4
])
expect(described_class.scheduled_and_unstarted).to eq(
[
cleanup_schedule_2,
cleanup_schedule_1,
cleanup_schedule_5,
cleanup_schedule_4
])
end
end

View File

@ -466,19 +466,20 @@ RSpec.describe MergeRequestDiff do
diff_with_commits.update!(sorted: false) # Mark as unsorted so it'll re-order
# There will be 11 returned, as we have to take into account for new and old paths
expect(diff_with_commits.diffs_in_batch(0, 10, diff_options: diff_options).diff_paths).to eq([
'bar/branch-test.txt',
'custom-highlighting/test.gitlab-custom',
'encoding/iso8859.txt',
'files/images/wm.svg',
'files/js/commit.js.coffee',
'files/js/commit.coffee',
'files/lfs/lfs_object.iso',
'files/ruby/popen.rb',
'files/ruby/regex.rb',
'files/.DS_Store',
'files/whitespace'
])
expect(diff_with_commits.diffs_in_batch(0, 10, diff_options: diff_options).diff_paths).to eq(
[
'bar/branch-test.txt',
'custom-highlighting/test.gitlab-custom',
'encoding/iso8859.txt',
'files/images/wm.svg',
'files/js/commit.js.coffee',
'files/js/commit.coffee',
'files/lfs/lfs_object.iso',
'files/ruby/popen.rb',
'files/ruby/regex.rb',
'files/.DS_Store',
'files/whitespace'
])
end
context 'when diff_options include ignore_whitespace_change' do
@ -555,29 +556,30 @@ RSpec.describe MergeRequestDiff do
it 'sorts diff files directory first' do
diff_with_commits.update!(sorted: false) # Mark as unsorted so it'll re-order
expect(diff_with_commits.diffs(diff_options).diff_paths).to eq([
'bar/branch-test.txt',
'custom-highlighting/test.gitlab-custom',
'encoding/iso8859.txt',
'files/images/wm.svg',
'files/js/commit.js.coffee',
'files/js/commit.coffee',
'files/lfs/lfs_object.iso',
'files/ruby/popen.rb',
'files/ruby/regex.rb',
'files/.DS_Store',
'files/whitespace',
'foo/bar/.gitkeep',
'with space/README.md',
'.DS_Store',
'.gitattributes',
'.gitignore',
'.gitmodules',
'CHANGELOG',
'README',
'gitlab-grack',
'gitlab-shell'
])
expect(diff_with_commits.diffs(diff_options).diff_paths).to eq(
[
'bar/branch-test.txt',
'custom-highlighting/test.gitlab-custom',
'encoding/iso8859.txt',
'files/images/wm.svg',
'files/js/commit.js.coffee',
'files/js/commit.coffee',
'files/lfs/lfs_object.iso',
'files/ruby/popen.rb',
'files/ruby/regex.rb',
'files/.DS_Store',
'files/whitespace',
'foo/bar/.gitkeep',
'with space/README.md',
'.DS_Store',
'.gitattributes',
'.gitignore',
'.gitmodules',
'CHANGELOG',
'README',
'gitlab-grack',
'gitlab-shell'
])
end
end
end
@ -661,28 +663,29 @@ RSpec.describe MergeRequestDiff do
mr_diff = create(:merge_request).merge_request_diff
diff_files_paths = mr_diff.merge_request_diff_files.map { |file| file.new_path.presence || file.old_path }
expect(diff_files_paths).to eq([
'bar/branch-test.txt',
'custom-highlighting/test.gitlab-custom',
'encoding/iso8859.txt',
'files/images/wm.svg',
'files/js/commit.coffee',
'files/lfs/lfs_object.iso',
'files/ruby/popen.rb',
'files/ruby/regex.rb',
'files/.DS_Store',
'files/whitespace',
'foo/bar/.gitkeep',
'with space/README.md',
'.DS_Store',
'.gitattributes',
'.gitignore',
'.gitmodules',
'CHANGELOG',
'README',
'gitlab-grack',
'gitlab-shell'
])
expect(diff_files_paths).to eq(
[
'bar/branch-test.txt',
'custom-highlighting/test.gitlab-custom',
'encoding/iso8859.txt',
'files/images/wm.svg',
'files/js/commit.coffee',
'files/lfs/lfs_object.iso',
'files/ruby/popen.rb',
'files/ruby/regex.rb',
'files/.DS_Store',
'files/whitespace',
'foo/bar/.gitkeep',
'with space/README.md',
'.DS_Store',
'.gitattributes',
'.gitignore',
'.gitmodules',
'CHANGELOG',
'README',
'gitlab-grack',
'gitlab-shell'
])
end
it 'expands collapsed diffs before saving' do

View File

@ -1837,9 +1837,8 @@ RSpec.describe MergeRequest, factory_default: :keep do
context 'persisted merge request' do
context 'with a limit' do
it 'returns a limited number of commit shas' do
expect(subject.commit_shas(limit: 2)).to eq(%w[
b83d6e391c22777fca1ed3012fce84f633d7fed0 498214de67004b1da3d820901307bed2a68a8ef6
])
expect(subject.commit_shas(limit: 2)).to eq(
%w[b83d6e391c22777fca1ed3012fce84f633d7fed0 498214de67004b1da3d820901307bed2a68a8ef6])
end
end
@ -4739,15 +4738,17 @@ RSpec.describe MergeRequest, factory_default: :keep do
context 'persisted merge request' do
context 'with a limit' do
it 'returns a limited number of commits' do
expect(subject.commits(limit: 2).map(&:sha)).to eq(%w[
b83d6e391c22777fca1ed3012fce84f633d7fed0
498214de67004b1da3d820901307bed2a68a8ef6
])
expect(subject.commits(limit: 3).map(&:sha)).to eq(%w[
b83d6e391c22777fca1ed3012fce84f633d7fed0
498214de67004b1da3d820901307bed2a68a8ef6
1b12f15a11fc6e62177bef08f47bc7b5ce50b141
])
expect(subject.commits(limit: 2).map(&:sha)).to eq(
%w[
b83d6e391c22777fca1ed3012fce84f633d7fed0
498214de67004b1da3d820901307bed2a68a8ef6
])
expect(subject.commits(limit: 3).map(&:sha)).to eq(
%w[
b83d6e391c22777fca1ed3012fce84f633d7fed0
498214de67004b1da3d820901307bed2a68a8ef6
1b12f15a11fc6e62177bef08f47bc7b5ce50b141
])
end
end
@ -4792,9 +4793,10 @@ RSpec.describe MergeRequest, factory_default: :keep do
end
it 'returns the safe number of commits' do
expect(subject.recent_commits.map(&:sha)).to eq(%w[
b83d6e391c22777fca1ed3012fce84f633d7fed0 498214de67004b1da3d820901307bed2a68a8ef6
])
expect(subject.recent_commits.map(&:sha)).to eq(
%w[
b83d6e391c22777fca1ed3012fce84f633d7fed0 498214de67004b1da3d820901307bed2a68a8ef6
])
end
end

Some files were not shown because too many files have changed in this diff Show More