提交 a928c517 编写于 作者: G GitLab Bot

Add latest changes from gitlab-org/gitlab@master

上级 0a6b0190
e4ff30e44b6ac21f33290bbe7a9cbbd42f98d4d1
e9860f7988a2c87638abf695d8613e3096312857
<script>
import { GlEmptyState, GlSprintf, GlLink, GlButton } from '@gitlab/ui';
export default {
components: {
GlEmptyState,
GlSprintf,
GlLink,
GlButton,
},
inject: {
isAdmin: {
type: Boolean,
},
svgPath: {
type: String,
},
docsLink: {
type: String,
},
primaryButtonPath: {
type: String,
},
},
};
</script>
<template>
<gl-empty-state class="js-empty-state" :title="__('Usage ping is off')" :svg-path="svgPath">
<template #description>
<gl-sprintf
v-if="!isAdmin"
:message="
__(
'To view instance-level analytics, ask an admin to turn on %{docLinkStart}usage ping%{docLinkEnd}.',
)
"
>
<template #docLink="{content}">
<gl-link :href="docsLink" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
<template v-else
><p>
{{ __('Turn on usage ping to review instance-level analytics.') }}
</p>
<gl-button category="primary" variant="success" :href="primaryButtonPath">
{{ __('Turn on usage ping') }}</gl-button
>
</template>
</template>
</gl-empty-state>
</template>
......@@ -468,7 +468,7 @@ export default {
<div
:data-can-create-note="getNoteableData.current_user.can_create_note"
class="files d-flex"
class="files d-flex gl-mt-2"
>
<div
v-if="showTreeList"
......
......@@ -133,6 +133,7 @@ export default {
'toggleFileDiscussions',
'toggleFileDiscussionWrappers',
'toggleFullDiff',
'toggleActiveFileByHash',
]),
handleToggleFile() {
this.$emit('toggleFile');
......@@ -149,6 +150,9 @@ export default {
const selector = this.diffContentIDSelector;
scrollToElement(document.querySelector(selector));
window.location.hash = selector;
if (!this.viewDiffsFileByFile) {
this.toggleActiveFileByHash(this.diffFile.file_hash);
}
}
},
},
......
......@@ -3,9 +3,10 @@
* This component is an iterative step towards refactoring and simplifying `vue_shared/components/file_row.vue`
* https://gitlab.com/gitlab-org/gitlab/-/merge_requests/23720
*/
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import FileRow from '~/vue_shared/components/file_row.vue';
import FileRowStats from './file_row_stats.vue';
import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue';
import FileRowStats from './file_row_stats.vue';
export default {
name: 'DiffFileRow',
......@@ -14,6 +15,7 @@ export default {
FileRowStats,
ChangedFileIcon,
},
mixins: [glFeatureFlagsMixin()],
props: {
file: {
type: Object,
......@@ -28,11 +30,28 @@ export default {
required: false,
default: null,
},
viewedFiles: {
type: Object,
required: false,
default: () => ({}),
},
},
computed: {
showFileRowStats() {
return !this.hideFileStats && this.file.type === 'blob';
},
fileClasses() {
if (!this.glFeatures.highlightCurrentDiffRow) {
return '';
}
return this.file.type === 'blob' && !this.viewedFiles[this.file.fileHash]
? 'gl-font-weight-bold'
: '';
},
isActive() {
return this.currentDiffFileId === this.file.fileHash;
},
},
};
</script>
......@@ -41,8 +60,9 @@ export default {
<file-row
:file="file"
v-bind="$attrs"
:class="{ 'is-active': currentDiffFileId === file.fileHash }"
:class="{ 'is-active': isActive }"
class="diff-file-row"
:file-classes="fileClasses"
v-on="$listeners"
>
<file-row-stats v-if="showFileRowStats" :file="file" class="mr-1" />
......
......@@ -25,7 +25,7 @@ export default {
};
},
computed: {
...mapState('diffs', ['tree', 'renderTreeList', 'currentDiffFileId']),
...mapState('diffs', ['tree', 'renderTreeList', 'currentDiffFileId', 'viewedDiffFileIds']),
...mapGetters('diffs', ['allBlobs']),
filteredTreeList() {
const search = this.search.toLowerCase().trim();
......@@ -93,6 +93,7 @@ export default {
:key="file.key"
:file="file"
:level="0"
:viewed-files="viewedDiffFileIds"
:hide-file-stats="hideFileStats"
:file-row-component="$options.DiffFileRow"
:current-diff-file-id="currentDiffFileId"
......
......@@ -34,7 +34,6 @@ export const COUNT_OF_AVATARS_IN_GUTTER = 3;
export const LENGTH_OF_AVATAR_TOOLTIP = 17;
export const LINES_TO_BE_RENDERED_DIRECTLY = 100;
export const MAX_LINES_TO_BE_RENDERED = 2000;
export const DIFF_FILE_SYMLINK_MODE = '120000';
export const DIFF_FILE_DELETED_MODE = '0';
......
......@@ -84,7 +84,7 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
commit(types.SET_BATCH_LOADING, false);
if (!isNoteLink && !state.currentDiffFileId) {
commit(types.UPDATE_CURRENT_DIFF_FILE_ID, diff_files[0].file_hash);
commit(types.VIEW_DIFF_FILE, diff_files[0].file_hash);
}
if (isNoteLink) {
......@@ -100,7 +100,7 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
!state.diffFiles.some(f => f.file_hash === state.currentDiffFileId) &&
!isNoteLink
) {
commit(types.UPDATE_CURRENT_DIFF_FILE_ID, state.diffFiles[0].file_hash);
commit(types.VIEW_DIFF_FILE, state.diffFiles[0].file_hash);
}
if (gon.features?.codeNavigation) {
......@@ -183,7 +183,7 @@ export const fetchCoverageFiles = ({ commit, state }) => {
export const setHighlightedRow = ({ commit }, lineCode) => {
const fileHash = lineCode.split('_')[0];
commit(types.SET_HIGHLIGHTED_ROW, lineCode);
commit(types.UPDATE_CURRENT_DIFF_FILE_ID, fileHash);
commit(types.VIEW_DIFF_FILE, fileHash);
};
// This is adding line discussions to the actual lines in the diff tree
......@@ -428,13 +428,17 @@ export const toggleTreeOpen = ({ commit }, path) => {
commit(types.TOGGLE_FOLDER_OPEN, path);
};
export const toggleActiveFileByHash = ({ commit }, hash) => {
commit(types.VIEW_DIFF_FILE, hash);
};
export const scrollToFile = ({ state, commit }, path) => {
if (!state.treeEntries[path]) return;
const { fileHash } = state.treeEntries[path];
document.location.hash = fileHash;
commit(types.UPDATE_CURRENT_DIFF_FILE_ID, fileHash);
commit(types.VIEW_DIFF_FILE, fileHash);
};
export const toggleShowTreeList = ({ commit, state }, saving = true) => {
......@@ -702,7 +706,7 @@ export const setCurrentDiffFileIdFromNote = ({ commit, state, rootGetters }, not
const fileHash = rootGetters.getDiscussion(note.discussion_id).diff_file?.file_hash;
if (fileHash && state.diffFiles.some(f => f.file_hash === fileHash)) {
commit(types.UPDATE_CURRENT_DIFF_FILE_ID, fileHash);
commit(types.VIEW_DIFF_FILE, fileHash);
}
};
......@@ -710,5 +714,5 @@ export const navigateToDiffFileIndex = ({ commit, state }, index) => {
const fileHash = state.diffFiles[index].file_hash;
document.location.hash = fileHash;
commit(types.UPDATE_CURRENT_DIFF_FILE_ID, fileHash);
commit(types.VIEW_DIFF_FILE, fileHash);
};
......@@ -34,6 +34,7 @@ export default () => ({
showTreeList: true,
currentDiffFileId: '',
projectPath: '',
viewedDiffFileIds: {},
commentForms: [],
highlightedRow: null,
renderTreeList: true,
......
......@@ -19,7 +19,7 @@ export const SET_LINE_DISCUSSIONS_FOR_FILE = 'SET_LINE_DISCUSSIONS_FOR_FILE';
export const REMOVE_LINE_DISCUSSIONS_FOR_FILE = 'REMOVE_LINE_DISCUSSIONS_FOR_FILE';
export const TOGGLE_FOLDER_OPEN = 'TOGGLE_FOLDER_OPEN';
export const TOGGLE_SHOW_TREE_LIST = 'TOGGLE_SHOW_TREE_LIST';
export const UPDATE_CURRENT_DIFF_FILE_ID = 'UPDATE_CURRENT_DIFF_FILE_ID';
export const VIEW_DIFF_FILE = 'VIEW_DIFF_FILE';
export const OPEN_DIFF_FILE_COMMENT_FORM = 'OPEN_DIFF_FILE_COMMENT_FORM';
export const UPDATE_DIFF_FILE_COMMENT_FORM = 'UPDATE_DIFF_FILE_COMMENT_FORM';
......
import Vue from 'vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { PARALLEL_DIFF_VIEW_TYPE } from '../constants';
import {
......@@ -291,8 +292,9 @@ export default {
[types.TOGGLE_SHOW_TREE_LIST](state) {
state.showTreeList = !state.showTreeList;
},
[types.UPDATE_CURRENT_DIFF_FILE_ID](state, fileId) {
[types.VIEW_DIFF_FILE](state, fileId) {
state.currentDiffFileId = fileId;
Vue.set(state.viewedDiffFileIds, fileId, true);
},
[types.OPEN_DIFF_FILE_COMMENT_FORM](state, formData) {
state.commentForms.push({
......
......@@ -11,7 +11,6 @@ import {
OLD_LINE_TYPE,
MATCH_LINE_TYPE,
LINES_TO_BE_RENDERED_DIRECTLY,
MAX_LINES_TO_BE_RENDERED,
TREE_TYPE,
INLINE_DIFF_VIEW_TYPE,
PARALLEL_DIFF_VIEW_TYPE,
......@@ -457,12 +456,10 @@ function getVisibleDiffLines(file) {
}
function finalizeDiffFile(file) {
const name = (file.viewer && file.viewer.name) || diffViewerModes.text;
const lines = getVisibleDiffLines(file);
Object.assign(file, {
renderIt: lines < LINES_TO_BE_RENDERED_DIRECTLY,
collapsed: name === diffViewerModes.text && lines > MAX_LINES_TO_BE_RENDERED,
isShowingFullFile: false,
isLoadingFullFile: false,
discussions: [],
......
......@@ -89,6 +89,14 @@ export default {
return this.requestCount !== 0;
},
},
created() {
// This method is defined here instead of in `methods`
// because we need to access the .cancel() method
// lodash attaches to the function, which is
// made inaccessible by Vue. More info:
// https://stackoverflow.com/a/52988020/1063392
this.debouncedSearchMilestones = debounce(this.searchMilestones, 100);
},
mounted() {
this.fetchMilestones();
},
......@@ -108,7 +116,7 @@ export default {
this.requestCount -= 1;
});
},
searchMilestones: debounce(function searchMilestones() {
searchMilestones() {
this.requestCount += 1;
const options = {
search: this.searchQuery,
......@@ -133,7 +141,14 @@ export default {
.finally(() => {
this.requestCount -= 1;
});
}, 100),
},
onSearchBoxInput() {
this.debouncedSearchMilestones();
},
onSearchBoxEnter() {
this.debouncedSearchMilestones.cancel();
this.searchMilestones();
},
toggleMilestoneSelection(clickedMilestone) {
if (!clickedMilestone) return [];
......@@ -186,7 +201,8 @@ export default {
v-model.trim="searchQuery"
class="gl-m-3"
:placeholder="this.$options.translations.searchMilestones"
@input="searchMilestones"
@input="onSearchBoxInput"
@keydown.enter.prevent="onSearchBoxEnter"
/>
<gl-new-dropdown-item @click="onMilestoneClicked(null)">
......
import Vue from 'vue';
import UserCallout from '~/user_callout';
import UsagePingDisabled from '~/admin/dev_ops_score/components/usage_ping_disabled.vue';
document.addEventListener('DOMContentLoaded', () => new UserCallout());
document.addEventListener('DOMContentLoaded', () => {
// eslint-disable-next-line no-new
new UserCallout();
const emptyStateContainer = document.getElementById('js-devops-empty-state');
if (!emptyStateContainer) return false;
const { emptyStateSvgPath, enableUsagePingLink, docsLink, isAdmin } = emptyStateContainer.dataset;
return new Vue({
el: emptyStateContainer,
provide: {
isAdmin: Boolean(isAdmin),
svgPath: emptyStateSvgPath,
primaryButtonPath: enableUsagePingLink,
docsLink,
},
render(h) {
return h(UsagePingDisabled);
},
});
});
......@@ -87,6 +87,15 @@ export default {
},
},
created() {
// This method is defined here instead of in `methods`
// because we need to access the .cancel() method
// lodash attaches to the function, which is
// made inaccessible by Vue. More info:
// https://stackoverflow.com/a/52988020/1063392
this.debouncedSearch = debounce(function search() {
this.search(this.query);
}, SEARCH_DEBOUNCE_MS);
this.setProjectId(this.projectId);
this.search(this.query);
},
......@@ -95,9 +104,13 @@ export default {
focusSearchBox() {
this.$refs.searchBox.$el.querySelector('input').focus();
},
onSearchBoxInput: debounce(function search() {
onSearchBoxEnter() {
this.debouncedSearch.cancel();
this.search(this.query);
}, SEARCH_DEBOUNCE_MS),
},
onSearchBoxInput() {
this.debouncedSearch();
},
selectRef(ref) {
this.setSelectedRef(ref);
this.$emit('input', this.selectedRef);
......@@ -129,6 +142,7 @@ export default {
class="gl-m-3"
:placeholder="i18n.searchPlaceholder"
@input="onSearchBoxInput"
@keydown.enter.prevent="onSearchBoxEnter"
/>
<div class="gl-flex-grow-1 gl-overflow-y-auto">
......
......@@ -140,7 +140,7 @@ export default {
class="form-control"
/>
</gl-form-group>
<gl-form-group class="w-50" data-testid="milestones-field" @keydown.enter.prevent.capture>
<gl-form-group class="w-50" data-testid="milestones-field">
<label>{{ __('Milestones') }}</label>
<div class="d-flex flex-column col-md-6 col-sm-10 pl-0">
<milestone-combobox
......
const parseSourceFile = raw => {
const frontMatterRegex = /(^---$[\s\S]*?^---$)/m;
const preGroupedRegex = /([\s\S]*?)(^---$[\s\S]*?^---$)(\s*)([\s\S]*)/m; // preFrontMatter, frontMatter, spacing, and content
import getFrontMatterLanguageDefinition from './parse_source_file_language_support';
const parseSourceFile = (raw, options = { frontMatterLanguage: 'yaml' }) => {
const { open, close } = getFrontMatterLanguageDefinition(options.frontMatterLanguage);
const anyChar = '[\\s\\S]';
const frontMatterBlock = `^${open}$${anyChar}*?^${close}$`;
const frontMatterRegex = new RegExp(`${frontMatterBlock}`, 'm');
const preGroupedRegex = new RegExp(`(${anyChar}*?)(${frontMatterBlock})(\\s*)(${anyChar}*)`, 'm'); // preFrontMatter, frontMatter, spacing, and content
let initial;
let editable;
......
const frontMatterLanguageDefinitions = [
{ name: 'yaml', open: '---', close: '---' },
{ name: 'toml', open: '\\+\\+\\+', close: '\\+\\+\\+' },
{ name: 'json', open: '{', close: '}' },
];
const getFrontMatterLanguageDefinition = name => {
const languageDefinition = frontMatterLanguageDefinitions.find(def => def.name === name);
if (!languageDefinition) {
throw new Error(`Unsupported front matter language: ${name}`);
}
return languageDefinition;
};
export default getFrontMatterLanguageDefinition;
......@@ -18,6 +18,11 @@ export default {
type: Number,
required: true,
},
fileClasses: {
type: String,
required: false,
default: '',
},
},
computed: {
isTree() {
......@@ -123,6 +128,7 @@ export default {
:style="levelIndentation"
class="file-row-name str-truncated"
data-qa-selector="file_name_content"
:class="fileClasses"
>
<file-icon
class="file-row-icon"
......
......@@ -180,3 +180,33 @@ table {
border-top: 0;
}
}
.vulnerability-list {
@media (min-width: $breakpoint-sm) {
.checkbox {
padding-left: $gl-spacing-scale-4;
padding-right: 0;
+ td,
+ th {
padding-left: $gl-spacing-scale-4;
}
}
.status {
width: 8%;
}
.severity {
width: 9%;
}
.identifier {
width: 12%;
}
.scanner {
width: 15%;
}
}
}
......@@ -1062,7 +1062,7 @@ table.code {
.diff-tree-list {
position: -webkit-sticky;
position: sticky;
$top-pos: $header-height + $mr-tabs-height + $mr-version-controls-height + 11px;
$top-pos: $header-height + $mr-tabs-height + $mr-version-controls-height + 15px;
top: $top-pos;
max-height: calc(100vh - #{$top-pos});
z-index: 202;
......
......@@ -39,6 +39,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:hide_jump_to_next_unresolved_in_threads, default_enabled: true)
push_frontend_feature_flag(:merge_request_widget_graphql, @project)
push_frontend_feature_flag(:unified_diff_lines, @project)
push_frontend_feature_flag(:highlight_current_diff_row, @project)
end
before_action do
......
......@@ -10,6 +10,8 @@ class SessionsController < Devise::SessionsController
include KnownSignIn
skip_before_action :check_two_factor_requirement, only: [:destroy]
skip_before_action :check_password_expiration, only: [:destroy]
# replaced with :require_no_authentication_without_flash
skip_before_action :require_no_authentication, only: [:new, :create]
......
......@@ -445,7 +445,7 @@ class Issue < ApplicationRecord
super
rescue ActiveRecord::QueryCanceled => e
# Symptom of running out of space - schedule rebalancing
IssueRebalancingWorker.perform_async(id)
IssueRebalancingWorker.perform_async(nil, project_id)
raise e
end
......@@ -453,7 +453,7 @@ class Issue < ApplicationRecord
super
rescue ActiveRecord::QueryCanceled => e
# Symptom of running out of space - schedule rebalancing
IssueRebalancingWorker.perform_async(id)
IssueRebalancingWorker.perform_async(nil, project_id)
raise e
end
end
......
......@@ -29,7 +29,7 @@ module Issues
gates = [issue.project, issue.project.group].compact
return unless gates.any? { |gate| Feature.enabled?(:rebalance_issues, gate) }
IssueRebalancingWorker.perform_async(issue.id)
IssueRebalancingWorker.perform_async(nil, issue.project_id)
end
def create_assignee_note(issue, old_assignees)
......
.container.devops-empty
.col-sm-12.justify-content-center.text-center
= custom_icon('dev_ops_score_no_index')
%h4= _('Usage ping is not enabled')
- if !current_user.admin?
%p
- usage_ping_path = help_page_path('development/telemetry/usage_ping')
- usage_ping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: usage_ping_path }
= s_('In order to enable instance-level analytics, please ask an admin to enable %{usage_ping_link_start}usage ping%{usage_ping_link_end}.').html_safe % { usage_ping_link_start: usage_ping_link_start, usage_ping_link_end: '</a>'.html_safe }
- if current_user.admin?
%p
= _('Enable usage ping to get an overview of how you are using GitLab from a feature perspective.')
- if current_user.admin?
= link_to _('Enable usage ping'), metrics_and_profiling_admin_application_settings_path(anchor: 'js-usage-settings'), class: 'btn btn-primary'
......@@ -7,7 +7,7 @@
.gl-mt-3
- if !usage_ping_enabled
= render 'disabled'
#js-devops-empty-state{ data: { is_admin: current_user&.admin.to_s, empty_state_svg_path: image_path('illustrations/convdev/convdev_no_index.svg'), enable_usage_ping_link: metrics_and_profiling_admin_application_settings_path(anchor: 'js-usage-settings'), docs_link: help_page_path('development/telemetry/usage_ping') } }
- elsif @metric.blank?
= render 'no_data'
- else
......
......@@ -7,11 +7,14 @@ class IssueRebalancingWorker
urgency :low
feature_category :issue_tracking
def perform(issue_id)
issue = Issue.find(issue_id)
def perform(ignore = nil, project_id = nil)
return if project_id.nil?
project = Project.find(project_id)
issue = project.issues.first # All issues are equivalent as far as we are concerned
IssueRebalancingService.new(issue).execute
rescue ActiveRecord::RecordNotFound, IssueRebalancingService::TooManyIssues => e
Gitlab::ErrorTracking.log_exception(e, issue_id: issue_id)
Gitlab::ErrorTracking.log_exception(e, project_id: project_id)
end
end
---
title: Add index on merge_request_id to approval_merge_request_rules
merge_request: 40556
author:
type: other
---
title: Fix file file input top position cutoff
merge_request: 40634
author:
type: fixed
---
title: Add toml and json front matter language support to Static Site Editor's WYSIWYG mode
merge_request: 40718
author:
type: added
---
title: Migrate DevOps Score empty state into Vue component
merge_request: 40595
author:
type: changed
---
title: Allow users with expired passwords to sign out
merge_request: 40830
author:
type: fixed
---
title: Highlight un-focused/un-viewed file's in file tree
merge_request: 27937
author:
type: changed
---
title: Fix auto-deploy-image external chart dependencies
merge_request: 40730
author:
type: fixed
---
title: Fix client usage of max line rendering
merge_request: 40741
author:
type: fixed
---
title: Prevent form submission in search boxes on New Release and Edit Release pages
merge_request: 40011
author:
type: changed
# frozen_string_literal: true
class AddIndexOnMergeRequestIdAndRuleTypeToApprovalMergeRequestRule < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
INDEX_NAME = "approval_mr_rule_index_merge_request_id"
def up
add_concurrent_index(
:approval_merge_request_rules,
:merge_request_id,
name: INDEX_NAME
)
end
def down
remove_concurrent_index_by_name :approval_merge_request_rules, INDEX_NAME
end
end
360c42f4d34c3b03e7a0375a0ff2776f066888f0a40131180bf301b876ea58db
\ No newline at end of file
......@@ -18945,6 +18945,8 @@ CREATE UNIQUE INDEX any_approver_merge_request_rule_type_unique_index ON public.
CREATE UNIQUE INDEX any_approver_project_rule_type_unique_index ON public.approval_project_rules USING btree (project_id) WHERE (rule_type = 3);
CREATE INDEX approval_mr_rule_index_merge_request_id ON public.approval_merge_request_rules USING btree (merge_request_id);
CREATE UNIQUE INDEX approval_rule_name_index_for_code_owners ON public.approval_merge_request_rules USING btree (merge_request_id, code_owner, name) WHERE ((code_owner = true) AND (section IS NULL));
CREATE UNIQUE INDEX backup_labels_group_id_project_id_title_idx ON public.backup_labels USING btree (group_id, project_id, title);
......
......@@ -106,7 +106,7 @@ end
Using `any_instance` to stub a method (elasticsearch_indexing) that has been defined on a prepended module (EE::ApplicationSetting) is not supported.
```
### Alternative: `expect_next_instance_of` or `allow_next_instance_of`
### Alternative: `expect_next_instance_of`, `allow_next_instance_of`, `expect_next_found_instance_of` or `allow_next_found_instance_of`
Instead of writing:
......@@ -130,8 +130,21 @@ end
allow_next_instance_of(Project) do |project|
allow(project).to receive(:add_import_job)
end
# Do this:
expect_next_found_instance_of(Project) do |project|
expect(project).to receive(:add_import_job)
end
# Do this:
allow_next_found_instance_of(Project) do |project|
allow(project).to receive(:add_import_job)
end
```
_**Note:** Since Active Record is not calling the `.new` method on model classes to instantiate the objects,
you should use `expect_next_found_instance_of` or `allow_next_found_instance_of` mock helpers to setup mock on objects returned by Active Record query & finder methods._
If we also want to initialize the instance with some particular arguments, we
could also pass it like:
......
.dast-auto-deploy:
image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v1.0.0"
image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v1.0.2"
dast_environment_deploy:
extends: .dast-auto-deploy
......
.auto-deploy:
image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v1.0.0"
image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v1.0.2"
dependencies: []
include:
......
......@@ -7021,6 +7021,9 @@ msgstr ""
msgid "Could not create group"
msgstr ""
msgid "Could not create issue"
msgstr ""
msgid "Could not create project"
msgstr ""
......@@ -9281,9 +9284,6 @@ msgstr ""
msgid "Enable usage ping"
msgstr ""
msgid "Enable usage ping to get an overview of how you are using GitLab from a feature perspective."
msgstr ""
msgid "Enable/disable your service desk. %{link_start}Learn more about service desk%{link_end}."
msgstr ""
......@@ -13034,9 +13034,6 @@ msgstr ""
msgid "In %{time_to_now}"
msgstr ""
msgid "In order to enable instance-level analytics, please ask an admin to enable %{usage_ping_link_start}usage ping%{usage_ping_link_end}."
msgstr ""
msgid "In order to gather accurate feature usage data, it can take 1 to 2 weeks to see your index."
msgstr ""
......@@ -21873,6 +21870,9 @@ msgstr ""
msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
msgid "SecurityReports|Ensure that %{trackingStart}issue tracking%{trackingEnd} is enabled for this project and you have %{permissionsStart}permission to create new issues%{permissionsEnd}."
msgstr ""
msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
......@@ -21924,6 +21924,9 @@ msgstr ""
msgid "SecurityReports|Project"
msgstr ""
msgid "SecurityReports|Project was not found or you do not have permission to add this project to Security Dashboards."
msgstr ""
msgid "SecurityReports|Projects added"
msgstr ""
......@@ -21999,7 +22002,7 @@ msgstr ""
msgid "SecurityReports|To widen your search, change or remove filters above"
msgstr ""
msgid "SecurityReports|Unable to add %{invalidProjectsMessage}"
msgid "SecurityReports|Unable to add %{invalidProjectsMessage}: %{errorMessage}"
msgstr ""
msgid "SecurityReports|Unable to add %{invalidProjects}"
......@@ -25974,6 +25977,9 @@ msgstr ""
msgid "To view all %{scannedResourcesCount} scanned URLs, please download the CSV file"
msgstr ""
msgid "To view instance-level analytics, ask an admin to turn on %{docLinkStart}usage ping%{docLinkEnd}."
msgstr ""
msgid "To view the roadmap, add a start or due date to one of your epics in this group or its subgroups. In the months view, only epics in the past month, current month, and next 5 months are shown."
msgstr ""
......@@ -26283,6 +26289,9 @@ msgstr ""
msgid "Turn on usage ping"
msgstr ""
msgid "Turn on usage ping to review instance-level analytics."
msgstr ""
msgid "Twitter"
msgstr ""
......@@ -26760,7 +26769,7 @@ msgstr ""
msgid "Usage"
msgstr ""
msgid "Usage ping is not enabled"
msgid "Usage ping is off"
msgstr ""
msgid "Usage statistics"
......@@ -27547,9 +27556,6 @@ msgstr ""
msgid "VulnerabilityManagement|Something went wrong while trying to unlink the issue. Please try again later."
msgstr ""
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
msgid "VulnerabilityManagement|Something went wrong, could not get user."
msgstr ""
......@@ -29534,9 +29540,15 @@ msgstr ""
msgid "mrWidget|%{prefixToLinkStart}No pipeline%{prefixToLinkEnd} %{addPipelineLinkStart}Add the .gitlab-ci.yml file%{addPipelineLinkEnd} to create one."
msgstr ""
msgid "mrWidget|A new merge train has started and this merge request is the first of the queue."
msgstr ""
msgid "mrWidget|Added to the merge train by"
msgstr ""
msgid "mrWidget|Added to the merge train. There are %{mergeTrainPosition} merge requests waiting to be merged"
msgstr ""
msgid "mrWidget|Allows commits from members who can merge to the target branch"
msgstr ""
......@@ -29624,9 +29636,6 @@ msgstr ""
msgid "mrWidget|If the %{missingBranchName} branch exists in your local repository, you can merge this merge request manually using the command line"
msgstr ""
msgid "mrWidget|In the merge train at position %{mergeTrainPosition}"
msgstr ""
msgid "mrWidget|Jump to first unresolved thread"
msgstr ""
......
......@@ -6,11 +6,11 @@ RSpec.describe SessionsController do
include DeviseHelpers
include LdapHelpers
describe '#new' do
before do
set_devise_mapping(context: @request)
end
before do
set_devise_mapping(context: @request)
end
describe '#new' do
context 'when auto sign-in is enabled' do
before do
stub_omniauth_setting(auto_sign_in_with_provider: :saml)
......@@ -59,13 +59,19 @@ RSpec.describe SessionsController do
end
end
end
end
describe '#create' do
before do
set_devise_mapping(context: @request)
it "redirects correctly for referer on same host with params" do
host = "test.host"
search_path = "/search?search=seed_project"
request.headers[:HTTP_REFERER] = "http://#{host}#{search_path}"
get(:new, params: { redirect_to_referer: :yes })
expect(controller.stored_location_for(:redirect)).to eq(search_path)
end
end
describe '#create' do
it_behaves_like 'known sign in' do
let(:user) { create(:user) }
let(:post_action) { post(:create, params: { user: { login: user.username, password: user.password } }) }
......@@ -439,25 +445,8 @@ RSpec.describe SessionsController do
end
end
describe "#new" do
before do
set_devise_mapping(context: @request)
end
it "redirects correctly for referer on same host with params" do
host = "test.host"
search_path = "/search?search=seed_project"
request.headers[:HTTP_REFERER] = "http://#{host}#{search_path}"
get(:new, params: { redirect_to_referer: :yes })
expect(controller.stored_location_for(:redirect)).to eq(search_path)
end
end
context 'when login fails' do
before do
set_devise_mapping(context: @request)
@request.env["warden.options"] = { action: 'unauthenticated' }
end
......@@ -471,10 +460,6 @@ RSpec.describe SessionsController do
describe '#set_current_context' do
let_it_be(:user) { create(:user) }
before do
set_devise_mapping(context: @request)
end
context 'when signed in' do
before do
sign_in(user)
......@@ -528,4 +513,21 @@ RSpec.describe SessionsController do
end
end
end
describe '#destroy' do
before do
sign_in(user)
end
context 'for a user whose password has expired' do
let(:user) { create(:user, password_expires_at: 2.days.ago) }
it 'allows to sign out successfully' do
delete :destroy
expect(response).to redirect_to(new_user_session_path)
expect(controller.current_user).to be_nil
end
end
end
end
......@@ -22,10 +22,10 @@ RSpec.describe 'DevOps Report page' do
stub_application_setting(usage_ping_enabled: false)
end
it 'shows empty state' do
it 'shows empty state', :js do
visit admin_dev_ops_score_path
expect(page).to have_content('Usage ping is not enabled')
expect(page).to have_selector(".js-empty-state")
end
it 'hides the intro callout' do
......
......@@ -8,4 +8,15 @@
// [2]: https://gitlab.com/gitlab-org/gitlab/-/issues/213378
// Further reference: https://github.com/facebook/jest/issues/3465
export default fn => fn;
export default fn => {
const debouncedFn = jest.fn().mockImplementation(fn);
debouncedFn.cancel = jest.fn();
debouncedFn.flush = jest.fn().mockImplementation(() => {
const errorMessage =
"The .flush() method returned by lodash.debounce is not yet implemented/mocked by the mock in 'spec/frontend/__mocks__/lodash/debounce.js'.";
throw new Error(errorMessage);
});
return debouncedFn;
};
......@@ -44,6 +44,7 @@ describe('DiffFileHeader component', () => {
toggleFileDiscussions: jest.fn(),
toggleFileDiscussionWrappers: jest.fn(),
toggleFullDiff: jest.fn(),
toggleActiveFileByHash: jest.fn(),
},
},
},
......
......@@ -7,9 +7,12 @@ import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue';
describe('Diff File Row component', () => {
let wrapper;
const createComponent = (props = {}) => {
const createComponent = (props = {}, highlightCurrentDiffRow = false) => {
wrapper = shallowMount(DiffFileRow, {
propsData: { ...props },
provide: {
glFeatures: { highlightCurrentDiffRow },
},
});
};
......@@ -56,6 +59,31 @@ describe('Diff File Row component', () => {
);
});
it.each`
features | fileType | isViewed | expected
${{ highlightCurrentDiffRow: true }} | ${'blob'} | ${false} | ${'gl-font-weight-bold'}
${{}} | ${'blob'} | ${true} | ${''}
${{}} | ${'tree'} | ${false} | ${''}
${{}} | ${'tree'} | ${true} | ${''}
`(
'with (features="$features", fileType="$fileType", isViewed=$isViewed), sets fileClasses="$expected"',
({ features, fileType, isViewed, expected }) => {
createComponent(
{
file: {
type: fileType,
fileHash: '#123456789',
},
level: 0,
hideFileStats: false,
viewedFiles: isViewed ? { '#123456789': true } : {},
},
features.highlightCurrentDiffRow,
);
expect(wrapper.find(FileRow).props('fileClasses')).toBe(expected);
},
);
describe('FileRowStats components', () => {
it.each`
type | hideFileStats | value | desc
......
import Vuex from 'vuex';
import { mount, createLocalVue } from '@vue/test-utils';
import { shallowMount, mount, createLocalVue } from '@vue/test-utils';
import TreeList from '~/diffs/components/tree_list.vue';
import createStore from '~/diffs/store/modules';
import FileTree from '~/vue_shared/components/file_tree.vue';
describe('Diffs tree list component', () => {
let wrapper;
let store;
const getFileRows = () => wrapper.findAll('.file-row');
const localVue = createLocalVue();
localVue.use(Vuex);
const createComponent = state => {
const store = new Vuex.Store({
const createComponent = (mountFn = mount) => {
wrapper = mountFn(TreeList, {
store,
localVue,
propsData: { hideFileStats: false },
});
};
beforeEach(() => {
store = new Vuex.Store({
modules: {
diffs: createStore(),
},
......@@ -23,61 +33,57 @@ describe('Diffs tree list component', () => {
addedLines: 10,
removedLines: 20,
...store.state.diffs,
...state,
};
});
wrapper = mount(TreeList, {
store,
localVue,
propsData: { hideFileStats: false },
const setupFilesInState = () => {
const treeEntries = {
'index.js': {
addedLines: 0,
changed: true,
deleted: false,
fileHash: 'test',
key: 'index.js',
name: 'index.js',
path: 'app/index.js',
removedLines: 0,
tempFile: true,
type: 'blob',
parentPath: 'app',
},
app: {
key: 'app',
path: 'app',
name: 'app',
type: 'tree',
tree: [],
},
};
Object.assign(store.state.diffs, {
treeEntries,
tree: [treeEntries['index.js'], treeEntries.app],
});
};
beforeEach(() => {
localStorage.removeItem('mr_diff_tree_list');
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
it('renders empty text', () => {
expect(wrapper.text()).toContain('No files found');
describe('default', () => {
beforeEach(() => {
createComponent();
});
it('renders empty text', () => {
expect(wrapper.text()).toContain('No files found');
});
});
describe('with files', () => {
beforeEach(() => {
const treeEntries = {
'index.js': {
addedLines: 0,
changed: true,
deleted: false,
fileHash: 'test',
key: 'index.js',
name: 'index.js',
path: 'app/index.js',
removedLines: 0,
tempFile: true,
type: 'blob',
parentPath: 'app',
},
app: {
key: 'app',
path: 'app',
name: 'app',
type: 'tree',
tree: [],
},
};
createComponent({
treeEntries,
tree: [treeEntries['index.js'], treeEntries.app],
});
return wrapper.vm.$nextTick();
setupFilesInState();
createComponent();
});
it('renders tree', () => {
......@@ -136,4 +142,23 @@ describe('Diffs tree list component', () => {
});
});
});
describe('with viewedDiffFileIds', () => {
const viewedDiffFileIds = { fileId: '#12345' };
beforeEach(() => {
setupFilesInState();
store.state.diffs.viewedDiffFileIds = viewedDiffFileIds;
});
it('passes the viewedDiffFileIds to the FileTree', () => {
createComponent(shallowMount);
return wrapper.vm.$nextTick().then(() => {
// Have to use $attrs['viewed-files'] because we are passing down an object
// and attributes('') stringifies values (e.g. [object])...
expect(wrapper.find(FileTree).vm.$attrs['viewed-files']).toBe(viewedDiffFileIds);
});
});
});
});
......@@ -191,10 +191,10 @@ describe('DiffsStoreActions', () => {
{ type: types.SET_RETRIEVING_BATCHES, payload: true },
{ type: types.SET_DIFF_DATA_BATCH, payload: { diff_files: res1.diff_files } },
{ type: types.SET_BATCH_LOADING, payload: false },
{ type: types.UPDATE_CURRENT_DIFF_FILE_ID, payload: 'test' },
{ type: types.VIEW_DIFF_FILE, payload: 'test' },
{ type: types.SET_DIFF_DATA_BATCH, payload: { diff_files: res2.diff_files } },
{ type: types.SET_BATCH_LOADING, payload: false },
{ type: types.UPDATE_CURRENT_DIFF_FILE_ID, payload: 'test2' },
{ type: types.VIEW_DIFF_FILE, payload: 'test2' },
{ type: types.SET_RETRIEVING_BATCHES, payload: false },
],
[],
......@@ -300,7 +300,7 @@ describe('DiffsStoreActions', () => {
it('should mark currently selected diff and set lineHash and fileHash of highlightedRow', () => {
testAction(setHighlightedRow, 'ABC_123', {}, [
{ type: types.SET_HIGHLIGHTED_ROW, payload: 'ABC_123' },
{ type: types.UPDATE_CURRENT_DIFF_FILE_ID, payload: 'ABC' },
{ type: types.VIEW_DIFF_FILE, payload: 'ABC' },
]);
});
});
......@@ -904,7 +904,7 @@ describe('DiffsStoreActions', () => {
expect(document.location.hash).toBe('#test');
});
it('commits UPDATE_CURRENT_DIFF_FILE_ID', () => {
it('commits VIEW_DIFF_FILE', () => {
const state = {
treeEntries: {
path: {
......@@ -915,7 +915,7 @@ describe('DiffsStoreActions', () => {
scrollToFile({ state, commit }, 'path');
expect(commit).toHaveBeenCalledWith(types.UPDATE_CURRENT_DIFF_FILE_ID, 'test');
expect(commit).toHaveBeenCalledWith(types.VIEW_DIFF_FILE, 'test');
});
});
......@@ -1413,7 +1413,7 @@ describe('DiffsStoreActions', () => {
});
describe('setCurrentDiffFileIdFromNote', () => {
it('commits UPDATE_CURRENT_DIFF_FILE_ID', () => {
it('commits VIEW_DIFF_FILE', () => {
const commit = jest.fn();
const state = { diffFiles: [{ file_hash: '123' }] };
const rootGetters = {
......@@ -1423,10 +1423,10 @@ describe('DiffsStoreActions', () => {
setCurrentDiffFileIdFromNote({ commit, state, rootGetters }, '1');
expect(commit).toHaveBeenCalledWith(types.UPDATE_CURRENT_DIFF_FILE_ID, '123');
expect(commit).toHaveBeenCalledWith(types.VIEW_DIFF_FILE, '123');
});
it('does not commit UPDATE_CURRENT_DIFF_FILE_ID when discussion has no diff_file', () => {
it('does not commit VIEW_DIFF_FILE when discussion has no diff_file', () => {
const commit = jest.fn();
const state = { diffFiles: [{ file_hash: '123' }] };
const rootGetters = {
......@@ -1439,7 +1439,7 @@ describe('DiffsStoreActions', () => {
expect(commit).not.toHaveBeenCalled();
});
it('does not commit UPDATE_CURRENT_DIFF_FILE_ID when diff file does not exist', () => {
it('does not commit VIEW_DIFF_FILE when diff file does not exist', () => {
const commit = jest.fn();
const state = { diffFiles: [{ file_hash: '123' }] };
const rootGetters = {
......@@ -1454,12 +1454,12 @@ describe('DiffsStoreActions', () => {
});
describe('navigateToDiffFileIndex', () => {
it('commits UPDATE_CURRENT_DIFF_FILE_ID', done => {
it('commits VIEW_DIFF_FILE', done => {
testAction(
navigateToDiffFileIndex,
0,
{ diffFiles: [{ file_hash: '123' }] },
[{ type: types.UPDATE_CURRENT_DIFF_FILE_ID, payload: '123' }],
[{ type: types.VIEW_DIFF_FILE, payload: '123' }],
[],
done,
);
......
......@@ -737,11 +737,11 @@ describe('DiffsStoreMutations', () => {
});
});
describe('UPDATE_CURRENT_DIFF_FILE_ID', () => {
describe('VIEW_DIFF_FILE', () => {
it('updates currentDiffFileId', () => {
const state = createState();
mutations[types.UPDATE_CURRENT_DIFF_FILE_ID](state, 'somefileid');
mutations[types.VIEW_DIFF_FILE](state, 'somefileid');
expect(state.currentDiffFileId).toBe('somefileid');
});
......
......@@ -22,11 +22,11 @@ describe('IDE pipelines list', () => {
const defaultState = {
links: { ciHelpPagePath: TEST_HOST },
pipelinesEmptyStateSvgPath: TEST_HOST,
pipelines: {
stages: [],
failedStages: [],
isLoadingJobs: false,
},
};
const defaultPipelinesState = {
stages: [],
failedStages: [],
isLoadingJobs: false,
};
const fetchLatestPipelineMock = jest.fn();
......@@ -34,23 +34,20 @@ describe('IDE pipelines list', () => {
const failedStagesGetterMock = jest.fn().mockReturnValue([]);
const fakeProjectPath = 'alpha/beta';
const createComponent = (state = {}) => {
const { pipelines: pipelinesState, ...restOfState } = state;
const { defaultPipelines, ...defaultRestOfState } = defaultState;
const fakeStore = new Vuex.Store({
const createStore = (rootState, pipelinesState) => {
return new Vuex.Store({
getters: {
currentProject: () => ({ web_url: 'some/url ', path_with_namespace: fakeProjectPath }),
},
state: {
...defaultRestOfState,
...restOfState,
...defaultState,
...rootState,
},
modules: {
pipelines: {
namespaced: true,
state: {
...defaultPipelines,
...defaultPipelinesState,
...pipelinesState,
},
actions: {
......@@ -69,10 +66,12 @@ describe('IDE pipelines list', () => {
},
},
});
};
const createComponent = (state = {}, pipelinesState = {}) => {
wrapper = shallowMount(List, {
localVue,
store: fakeStore,
store: createStore(state, pipelinesState),
});
};
......@@ -94,31 +93,33 @@ describe('IDE pipelines list', () => {
describe('when loading', () => {
let defaultPipelinesLoadingState;
beforeAll(() => {
defaultPipelinesLoadingState = {
...defaultState.pipelines,
isLoadingPipeline: true,
};
});
it('does not render when pipeline has loaded before', () => {
createComponent({
pipelines: {
createComponent(
{},
{
...defaultPipelinesLoadingState,
hasLoadedPipeline: true,
},
});
);
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
});
it('renders loading state', () => {
createComponent({
pipelines: {
createComponent(
{},
{
...defaultPipelinesLoadingState,
hasLoadedPipeline: false,
},
});
);
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
......@@ -126,21 +127,22 @@ describe('IDE pipelines list', () => {
describe('when loaded', () => {
let defaultPipelinesLoadedState;
beforeAll(() => {
defaultPipelinesLoadedState = {
...defaultState.pipelines,
isLoadingPipeline: false,
hasLoadedPipeline: true,
};
});
it('renders empty state when no latestPipeline', () => {
createComponent({ pipelines: { ...defaultPipelinesLoadedState, latestPipeline: null } });
createComponent({}, { ...defaultPipelinesLoadedState, latestPipeline: null });
expect(wrapper.element).toMatchSnapshot();
});
describe('with latest pipeline loaded', () => {
let withLatestPipelineState;
beforeAll(() => {
withLatestPipelineState = {
...defaultPipelinesLoadedState,
......@@ -149,12 +151,12 @@ describe('IDE pipelines list', () => {
});
it('renders ci icon', () => {
createComponent({ pipelines: withLatestPipelineState });
createComponent({}, withLatestPipelineState);
expect(wrapper.find(CiIcon).exists()).toBe(true);
});
it('renders pipeline data', () => {
createComponent({ pipelines: withLatestPipelineState });
createComponent({}, withLatestPipelineState);
expect(wrapper.text()).toContain('#1');
});
......@@ -162,7 +164,7 @@ describe('IDE pipelines list', () => {
it('renders list of jobs', () => {
const stages = [];
const isLoadingJobs = true;
createComponent({ pipelines: { ...withLatestPipelineState, stages, isLoadingJobs } });
createComponent({}, { ...withLatestPipelineState, stages, isLoadingJobs });
const jobProps = wrapper
.findAll(Tab)
......@@ -177,7 +179,7 @@ describe('IDE pipelines list', () => {
const failedStages = [];
failedStagesGetterMock.mockReset().mockReturnValue(failedStages);
const isLoadingJobs = true;
createComponent({ pipelines: { ...withLatestPipelineState, isLoadingJobs } });
createComponent({}, { ...withLatestPipelineState, isLoadingJobs });
const jobProps = wrapper
.findAll(Tab)
......@@ -191,12 +193,13 @@ describe('IDE pipelines list', () => {
describe('with YAML error', () => {
it('renders YAML error', () => {
const yamlError = 'test yaml error';
createComponent({
pipelines: {
createComponent(
{},
{
...defaultPipelinesLoadedState,
latestPipeline: { ...pipelines[0], yamlError },
},
});
);
expect(wrapper.text()).toContain('Found errors in your .gitlab-ci.yml:');
expect(wrapper.text()).toContain(yamlError);
......
......@@ -2,10 +2,12 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { shallowMount } from '@vue/test-utils';
import { GlNewDropdown, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
import { ENTER_KEY } from '~/lib/utils/keys';
import MilestoneCombobox from '~/milestones/project_milestone_combobox.vue';
import { milestones as projectMilestones } from './mock_data';
const TEST_SEARCH_ENDPOINT = '/api/v4/projects/8/search';
const TEST_SEARCH = 'TEST_SEARCH';
const extraLinks = [
{ text: 'Create new', url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/-/milestones/new' },
......@@ -21,6 +23,8 @@ describe('Milestone selector', () => {
const findNoResultsMessage = () => wrapper.find({ ref: 'noResults' });
const findSearchBox = () => wrapper.find(GlSearchBoxByType);
const factory = (options = {}) => {
wrapper = shallowMount(MilestoneCombobox, {
...options,
......@@ -63,7 +67,7 @@ describe('Milestone selector', () => {
describe('before results', () => {
it('should show a loading icon', () => {
const request = mock.onGet(TEST_SEARCH_ENDPOINT, {
params: { search: 'TEST_SEARCH', scope: 'milestones' },
params: { search: TEST_SEARCH, scope: 'milestones' },
});
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
......@@ -85,9 +89,9 @@ describe('Milestone selector', () => {
describe('with empty results', () => {
beforeEach(() => {
mock
.onGet(TEST_SEARCH_ENDPOINT, { params: { search: 'TEST_SEARCH', scope: 'milestones' } })
.onGet(TEST_SEARCH_ENDPOINT, { params: { search: TEST_SEARCH, scope: 'milestones' } })
.reply(200, []);
wrapper.find(GlSearchBoxByType).vm.$emit('input', 'TEST_SEARCH');
findSearchBox().vm.$emit('input', TEST_SEARCH);
return axios.waitForAll();
});
......@@ -116,7 +120,7 @@ describe('Milestone selector', () => {
web_url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/-/milestones/6',
},
]);
wrapper.find(GlSearchBoxByType).vm.$emit('input', 'v0.1');
findSearchBox().vm.$emit('input', 'v0.1');
return axios.waitForAll().then(() => {
items = wrapper.findAll('[role="milestone option"]');
});
......@@ -147,4 +151,36 @@ describe('Milestone selector', () => {
expect(findNoResultsMessage().exists()).toBe(false);
});
});
describe('when Enter is pressed', () => {
beforeEach(() => {
factory({
propsData: {
projectId,
preselectedMilestones,
extraLinks,
},
data() {
return {
searchQuery: 'TEST_SEARCH',
};
},
});
mock
.onGet(TEST_SEARCH_ENDPOINT, { params: { search: 'TEST_SEARCH', scope: 'milestones' } })
.reply(200, []);
});
it('should trigger a search', async () => {
mock.resetHistory();
findSearchBox().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY }));
await axios.waitForAll();
expect(mock.history.get.length).toBe(1);
expect(mock.history.get[0].url).toBe(TEST_SEARCH_ENDPOINT);
});
});
});
......@@ -5,6 +5,7 @@ import MockAdapter from 'axios-mock-adapter';
import { GlLoadingIcon, GlSearchBoxByType, GlNewDropdownItem, GlIcon } from '@gitlab/ui';
import { trimText } from 'helpers/text_helper';
import { sprintf } from '~/locale';
import { ENTER_KEY } from '~/lib/utils/keys';
import RefSelector from '~/ref/components/ref_selector.vue';
import { X_TOTAL_HEADER, DEFAULT_I18N } from '~/ref/constants';
import createStore from '~/ref/stores/';
......@@ -83,6 +84,8 @@ describe('Ref selector component', () => {
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findSearchBox = () => wrapper.find(GlSearchBoxByType);
const findBranchesSection = () => wrapper.find('[data-testid="branches-section"]');
const findBranchDropdownItems = () => findBranchesSection().findAll(GlNewDropdownItem);
const findFirstBranchDropdownItem = () => findBranchDropdownItems().at(0);
......@@ -120,7 +123,7 @@ describe('Ref selector component', () => {
// Convenience methods
//
const updateQuery = newQuery => {
wrapper.find(GlSearchBoxByType).vm.$emit('input', newQuery);
findSearchBox().vm.$emit('input', newQuery);
};
const selectFirstBranch = () => {
......@@ -244,6 +247,23 @@ describe('Ref selector component', () => {
});
});
describe('when the Enter is pressed', () => {
beforeEach(() => {
createComponent();
return waitForRequests({ andClearMocks: true });
});
it('requeries the endpoints when Enter is pressed', () => {
findSearchBox().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY }));
return waitForRequests().then(() => {
expect(branchesApiCallSpy).toHaveBeenCalledTimes(1);
expect(tagsApiCallSpy).toHaveBeenCalledTimes(1);
});
});
});
describe('when no results are found', () => {
beforeEach(() => {
branchesApiCallSpy = jest.fn().mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]);
......
......@@ -10,7 +10,7 @@ import UnsavedChangesConfirmDialog from '~/static_site_editor/components/unsaved
import {
sourceContentTitle as title,
sourceContent as content,
sourceContentYAML as content,
sourceContentBody as body,
returnUrl,
} from '../mock_data';
......
......@@ -5,7 +5,7 @@ import {
projectId,
sourcePath,
sourceContentTitle as title,
sourceContent as content,
sourceContentYAML as content,
} from '../../mock_data';
jest.mock('~/static_site_editor/services/load_source_content', () => jest.fn());
......
......@@ -6,7 +6,7 @@ import {
projectId as project,
sourcePath,
username,
sourceContent as content,
sourceContentYAML as content,
savedContentMeta,
} from '../../mock_data';
......
export const sourceContentHeader = `---
export const sourceContentHeaderYAML = `---
layout: handbook-page-toc
title: Handbook
twitter_image: '/images/tweets/handbook-gitlab.png'
---`;
export const sourceContentHeaderTOML = `+++
layout: "handbook-page-toc"
title: "Handbook"
twitter_image: "/images/tweets/handbook-gitlab.png"
+++`;
export const sourceContentHeaderJSON = `{
"layout": "handbook-page-toc",
"title": "Handbook",
"twitter_image": "/images/tweets/handbook-gitlab.png",
}`;
export const sourceContentSpacing = `
`;
export const sourceContentBody = `## On this page
......@@ -13,7 +23,9 @@ export const sourceContentBody = `## On this page
![image](path/to/image1.png)
`;
export const sourceContent = `${sourceContentHeader}${sourceContentSpacing}${sourceContentBody}`;
export const sourceContentYAML = `${sourceContentHeaderYAML}${sourceContentSpacing}${sourceContentBody}`;
export const sourceContentTOML = `${sourceContentHeaderTOML}${sourceContentSpacing}${sourceContentBody}`;
export const sourceContentJSON = `${sourceContentHeaderJSON}${sourceContentSpacing}${sourceContentBody}`;
export const sourceContentTitle = 'Handbook';
export const username = 'gitlabuser';
......
......@@ -13,7 +13,7 @@ import { TRACKING_ACTION_INITIALIZE_EDITOR } from '~/static_site_editor/constant
import {
projectId as project,
returnUrl,
sourceContent as content,
sourceContentYAML as content,
sourceContentTitle as title,
sourcePath,
username,
......
......@@ -2,7 +2,12 @@ import Api from '~/api';
import loadSourceContent from '~/static_site_editor/services/load_source_content';
import { sourceContent, sourceContentTitle, projectId, sourcePath } from '../mock_data';
import {
sourceContentYAML as sourceContent,
sourceContentTitle,
projectId,
sourcePath,
} from '../mock_data';
describe('loadSourceContent', () => {
describe('requesting source content succeeds', () => {
......
import getFrontMatterLanguageDefinition from '~/static_site_editor/services/parse_source_file_language_support';
describe('static_site_editor/services/parse_source_file_language_support', () => {
describe('getFrontMatterLanguageDefinition', () => {
it.each`
languageName
${'yaml'}
${'toml'}
${'json'}
${'abcd'}
`('returns $hasMatch when provided $languageName', ({ languageName }) => {
try {
const definition = getFrontMatterLanguageDefinition(languageName);
expect(definition.name).toBe(languageName);
} catch (error) {
expect(error.message).toBe(`Unsupported front matter language: ${languageName}`);
}
});
});
});
import {
sourceContent as content,
sourceContentHeader as frontMatter,
sourceContentYAML as content,
sourceContentTOML as tomlContent,
sourceContentJSON as jsonContent,
sourceContentHeaderYAML as yamlFrontMatter,
sourceContentHeaderTOML as tomlFrontMatter,
sourceContentHeaderJSON as jsonFrontMatter,
sourceContentBody as body,
} from '../mock_data';
import parseSourceFile from '~/static_site_editor/services/parse_source_file';
describe('parseSourceFile', () => {
describe('static_site_editor/services/parse_source_file', () => {
const contentComplex = [content, content, content].join('');
const complexBody = [body, content, content].join('');
const edit = 'and more';
......@@ -14,13 +18,22 @@ describe('parseSourceFile', () => {
const newContentComplex = `${contentComplex} ${edit}`;
describe('unmodified front matter', () => {
const yamlOptions = { frontMatterLanguage: 'yaml' };
it.each`
parsedSource
${parseSourceFile(content)}
${parseSourceFile(contentComplex)}
`('returns the correct front matter when queried', ({ parsedSource }) => {
expect(parsedSource.frontMatter()).toBe(frontMatter);
});
parsedSource | targetFrontMatter
${parseSourceFile(content)} | ${yamlFrontMatter}
${parseSourceFile(contentComplex)} | ${yamlFrontMatter}
${parseSourceFile(content, yamlOptions)} | ${yamlFrontMatter}
${parseSourceFile(contentComplex, yamlOptions)} | ${yamlFrontMatter}
${parseSourceFile(tomlContent, { frontMatterLanguage: 'toml' })} | ${tomlFrontMatter}
${parseSourceFile(jsonContent, { frontMatterLanguage: 'json' })} | ${jsonFrontMatter}
`(
'returns $targetFrontMatter when frontMatter queried',
({ parsedSource, targetFrontMatter }) => {
expect(parsedSource.frontMatter()).toBe(targetFrontMatter);
},
);
});
describe('unmodified content', () => {
......@@ -49,9 +62,12 @@ describe('parseSourceFile', () => {
});
describe('modified front matter', () => {
const newFrontMatter = '---\nnewKey: newVal\n---';
const contentWithNewFrontMatter = content.replace(frontMatter, newFrontMatter);
const contentComplexWithNewFrontMatter = contentComplex.replace(frontMatter, newFrontMatter);
const newYamlFrontMatter = '---\nnewKey: newVal\n---';
const contentWithNewFrontMatter = content.replace(yamlFrontMatter, newYamlFrontMatter);
const contentComplexWithNewFrontMatter = contentComplex.replace(
yamlFrontMatter,
newYamlFrontMatter,
);
it.each`
parsedSource | targetContent
......@@ -60,11 +76,11 @@ describe('parseSourceFile', () => {
`(
'returns the correct front matter and modified content',
({ parsedSource, targetContent }) => {
expect(parsedSource.frontMatter()).toBe(frontMatter);
expect(parsedSource.frontMatter()).toBe(yamlFrontMatter);
parsedSource.setFrontMatter(newFrontMatter);
parsedSource.setFrontMatter(newYamlFrontMatter);
expect(parsedSource.frontMatter()).toBe(newFrontMatter);
expect(parsedSource.frontMatter()).toBe(newYamlFrontMatter);
expect(parsedSource.content()).toBe(targetContent);
},
);
......
......@@ -20,7 +20,7 @@ import {
commitMultipleResponse,
createMergeRequestResponse,
sourcePath,
sourceContent as content,
sourceContentYAML as content,
trackingCategory,
images,
} from '../mock_data';
......
......@@ -139,4 +139,16 @@ describe('File row component', () => {
expect(wrapper.vm.hasUrlAtCurrentRoute()).toBe(true);
});
it('render with the correct file classes prop', () => {
createComponent({
file: {
...file(),
},
level: 0,
fileClasses: 'font-weight-bold',
});
expect(wrapper.find('.file-row-name').classes()).toContain('font-weight-bold');
});
});
......@@ -1196,7 +1196,7 @@ RSpec.describe Issue do
it 'schedules rebalancing if we time-out when finding a gap' do
lhs = build_stubbed(:issue, relative_position: 99, project: project)
to_move = build(:issue, project: project)
expect(IssueRebalancingWorker).to receive(:perform_async).with(issue.id)
expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, project.id)
expect { to_move.move_between(lhs, issue) }.to raise_error(ActiveRecord::QueryCanceled)
end
......@@ -1205,7 +1205,7 @@ RSpec.describe Issue do
describe '#find_next_gap_after' do
it 'schedules rebalancing if we time-out when finding a gap' do
allow(issue).to receive(:find_next_gap) { raise ActiveRecord::QueryCanceled }
expect(IssueRebalancingWorker).to receive(:perform_async).with(issue.id)
expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, project.id)
expect { issue.move_sequence_after }.to raise_error(ActiveRecord::QueryCanceled)
end
......
......@@ -77,7 +77,7 @@ RSpec.describe Issues::CreateService do
it 'rebalances if needed' do
create(:issue, project: project, relative_position: RelativePositioning::MAX_POSITION)
expect(IssueRebalancingWorker).to receive(:perform_async).with(Integer)
expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, project.id)
expect(issue.relative_position).to eq(project.issues.maximum(:relative_position))
end
......@@ -86,7 +86,7 @@ RSpec.describe Issues::CreateService do
stub_feature_flags(rebalance_issues: false)
create(:issue, project: project, relative_position: RelativePositioning::MAX_POSITION)
expect(IssueRebalancingWorker).not_to receive(:perform_async).with(Integer)
expect(IssueRebalancingWorker).not_to receive(:perform_async)
expect(issue.relative_position).to eq(project.issues.maximum(:relative_position))
end
......@@ -95,7 +95,7 @@ RSpec.describe Issues::CreateService do
stub_feature_flags(rebalance_issues: project)
create(:issue, project: project, relative_position: RelativePositioning::MAX_POSITION)
expect(IssueRebalancingWorker).to receive(:perform_async).with(Integer)
expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, project.id)
expect(issue.relative_position).to eq(project.issues.maximum(:relative_position))
end
......
......@@ -126,7 +126,7 @@ RSpec.describe Issues::UpdateService, :mailer do
opts[:move_between_ids] = [issue1.id, issue2.id]
expect(IssueRebalancingWorker).not_to receive(:perform_async).with(issue.id)
expect(IssueRebalancingWorker).not_to receive(:perform_async)
update_issue(opts)
expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position)
......@@ -142,7 +142,7 @@ RSpec.describe Issues::UpdateService, :mailer do
opts[:move_between_ids] = [issue1.id, issue2.id]
expect(IssueRebalancingWorker).to receive(:perform_async).with(issue.id)
expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, project.id)
update_issue(opts)
expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position)
......@@ -156,7 +156,7 @@ RSpec.describe Issues::UpdateService, :mailer do
opts[:move_between_ids] = [issue1.id, issue2.id]
expect(IssueRebalancingWorker).to receive(:perform_async).with(issue.id)
expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, project.id)
update_issue(opts)
expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position)
......@@ -170,7 +170,7 @@ RSpec.describe Issues::UpdateService, :mailer do
opts[:move_between_ids] = [issue1.id, issue2.id]
expect(IssueRebalancingWorker).to receive(:perform_async).with(issue.id)
expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, project.id)
update_issue(opts)
expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position)
......
......@@ -115,6 +115,7 @@ RSpec.configure do |config|
config.include StubExperiments
config.include StubGitlabCalls
config.include StubGitlabData
config.include NextFoundInstanceOf
config.include NextInstanceOf
config.include TestEnv
config.include Devise::Test::ControllerHelpers, type: :controller
......
# frozen_string_literal: true
module NextFoundInstanceOf
ERROR_MESSAGE = 'NextFoundInstanceOf mock helpers can only be used with ActiveRecord targets'
def expect_next_found_instance_of(klass)
check_if_active_record!(klass)
stub_allocate(expect(klass)) do |expectation|
yield(expectation)
end
end
def allow_next_found_instance_of(klass)
check_if_active_record!(klass)
stub_allocate(allow(klass)) do |allowance|
yield(allowance)
end
end
private
def check_if_active_record!(klass)
raise ArgumentError.new(ERROR_MESSAGE) unless klass < ActiveRecord::Base
end
def stub_allocate(target)
target.to receive(:allocate).and_wrap_original do |method|
method.call.tap { |allocation| yield(allocation) }
end
end
end
......@@ -10,23 +10,30 @@ RSpec.describe IssueRebalancingWorker do
service = double(execute: nil)
expect(IssueRebalancingService).to receive(:new).with(issue).and_return(service)
described_class.new.perform(issue.id)
described_class.new.perform(nil, issue.project_id)
end
it 'anticipates the inability to find the issue' do
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(ActiveRecord::RecordNotFound, include(issue_id: -1))
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(ActiveRecord::RecordNotFound, include(project_id: -1))
expect(IssueRebalancingService).not_to receive(:new)
described_class.new.perform(-1)
described_class.new.perform(nil, -1)
end
it 'anticipates there being too many issues' do
service = double
allow(service).to receive(:execute) { raise IssueRebalancingService::TooManyIssues }
expect(IssueRebalancingService).to receive(:new).with(issue).and_return(service)
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(IssueRebalancingService::TooManyIssues, include(issue_id: issue.id))
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(IssueRebalancingService::TooManyIssues, include(project_id: issue.project_id))
described_class.new.perform(issue.id)
described_class.new.perform(nil, issue.project_id)
end
it 'takes no action if the value is nil' do
expect(IssueRebalancingService).not_to receive(:new)
expect(Gitlab::ErrorTracking).not_to receive(:log_exception)
described_class.new.perform(nil, nil)
end
end
end
......@@ -54,13 +54,9 @@ RSpec.describe NewNoteWorker do
let(:note) { create(:note) }
before do
# TODO: `allow_next_instance_of` helper method is not working
# because ActiveRecord is directly calling `.allocate` on model
# classes and bypasses the `.new` method call.
# Fix the `allow_next_instance_of` helper and change these to mock
# the next instance of `Note` model class.
allow(Note).to receive(:find_by).with(id: note.id).and_return(note)
allow(note).to receive(:skip_notification?).and_return(true)
allow_next_found_instance_of(Note) do |note|
allow(note).to receive(:skip_notification?).and_return(true)
end
end
it 'does not create a new note notification' do
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册