diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 690a8c454e7..a0b7d19d05c 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -e4ff30e44b6ac21f33290bbe7a9cbbd42f98d4d1 +e9860f7988a2c87638abf695d8613e3096312857 diff --git a/app/assets/javascripts/admin/dev_ops_score/components/usage_ping_disabled.vue b/app/assets/javascripts/admin/dev_ops_score/components/usage_ping_disabled.vue new file mode 100644 index 00000000000..5429ec403d3 --- /dev/null +++ b/app/assets/javascripts/admin/dev_ops_score/components/usage_ping_disabled.vue @@ -0,0 +1,53 @@ + + diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue index 66d7c2e3530..4ba9c29f9a9 100644 --- a/app/assets/javascripts/diffs/components/app.vue +++ b/app/assets/javascripts/diffs/components/app.vue @@ -468,7 +468,7 @@ export 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; + }, }, }; @@ -41,8 +60,9 @@ export default { diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue index 64c4715267f..d03d450b12d 100644 --- a/app/assets/javascripts/diffs/components/tree_list.vue +++ b/app/assets/javascripts/diffs/components/tree_list.vue @@ -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" diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js index 447136036ee..a79a385b9bc 100644 --- a/app/assets/javascripts/diffs/constants.js +++ b/app/assets/javascripts/diffs/constants.js @@ -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'; diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js index e0470a7d93f..0f275f1cb3e 100644 --- a/app/assets/javascripts/diffs/store/actions.js +++ b/app/assets/javascripts/diffs/store/actions.js @@ -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); }; diff --git a/app/assets/javascripts/diffs/store/modules/diff_state.js b/app/assets/javascripts/diffs/store/modules/diff_state.js index c8f6f6bb0e5..001d9d9f594 100644 --- a/app/assets/javascripts/diffs/store/modules/diff_state.js +++ b/app/assets/javascripts/diffs/store/modules/diff_state.js @@ -34,6 +34,7 @@ export default () => ({ showTreeList: true, currentDiffFileId: '', projectPath: '', + viewedDiffFileIds: {}, commentForms: [], highlightedRow: null, renderTreeList: true, diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js index 4b1dbc34902..5dba2e9d10d 100644 --- a/app/assets/javascripts/diffs/store/mutation_types.js +++ b/app/assets/javascripts/diffs/store/mutation_types.js @@ -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'; diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js index c39532599cb..268326f9246 100644 --- a/app/assets/javascripts/diffs/store/mutations.js +++ b/app/assets/javascripts/diffs/store/mutations.js @@ -1,3 +1,4 @@ +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({ diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js index 320b44bd537..3bdb79d3899 100644 --- a/app/assets/javascripts/diffs/store/utils.js +++ b/app/assets/javascripts/diffs/store/utils.js @@ -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: [], diff --git a/app/assets/javascripts/milestones/project_milestone_combobox.vue b/app/assets/javascripts/milestones/project_milestone_combobox.vue index 4e61e8b4262..b2196bd9ceb 100644 --- a/app/assets/javascripts/milestones/project_milestone_combobox.vue +++ b/app/assets/javascripts/milestones/project_milestone_combobox.vue @@ -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" /> diff --git a/app/assets/javascripts/pages/admin/dev_ops_score/index.js b/app/assets/javascripts/pages/admin/dev_ops_score/index.js index c1056537f90..9d018408b88 100644 --- a/app/assets/javascripts/pages/admin/dev_ops_score/index.js +++ b/app/assets/javascripts/pages/admin/dev_ops_score/index.js @@ -1,3 +1,27 @@ +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); + }, + }); +}); diff --git a/app/assets/javascripts/ref/components/ref_selector.vue b/app/assets/javascripts/ref/components/ref_selector.vue index e388604ed92..dd84246159b 100644 --- a/app/assets/javascripts/ref/components/ref_selector.vue +++ b/app/assets/javascripts/ref/components/ref_selector.vue @@ -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" />
diff --git a/app/assets/javascripts/releases/components/app_edit_new.vue b/app/assets/javascripts/releases/components/app_edit_new.vue index e34be1bd871..e1edf3d689d 100644 --- a/app/assets/javascripts/releases/components/app_edit_new.vue +++ b/app/assets/javascripts/releases/components/app_edit_new.vue @@ -140,7 +140,7 @@ export default { class="form-control" /> - +
{ - 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; diff --git a/app/assets/javascripts/static_site_editor/services/parse_source_file_language_support.js b/app/assets/javascripts/static_site_editor/services/parse_source_file_language_support.js new file mode 100644 index 00000000000..ec0eaca81b8 --- /dev/null +++ b/app/assets/javascripts/static_site_editor/services/parse_source_file_language_support.js @@ -0,0 +1,17 @@ +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; diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue index 0952e37e46e..004bf882dd6 100644 --- a/app/assets/javascripts/vue_shared/components/file_row.vue +++ b/app/assets/javascripts/vue_shared/components/file_row.vue @@ -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" > 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 diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb index 8e42db746dd..cf1ef6a9710 100644 --- a/app/services/issues/base_service.rb +++ b/app/services/issues/base_service.rb @@ -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) diff --git a/app/views/admin/dev_ops_score/_disabled.html.haml b/app/views/admin/dev_ops_score/_disabled.html.haml deleted file mode 100644 index bd808218f75..00000000000 --- a/app/views/admin/dev_ops_score/_disabled.html.haml +++ /dev/null @@ -1,14 +0,0 @@ -.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 = ''.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: ''.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' diff --git a/app/views/admin/dev_ops_score/show.html.haml b/app/views/admin/dev_ops_score/show.html.haml index 520194acc88..3a7fb9d7c8c 100644 --- a/app/views/admin/dev_ops_score/show.html.haml +++ b/app/views/admin/dev_ops_score/show.html.haml @@ -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 diff --git a/app/workers/issue_rebalancing_worker.rb b/app/workers/issue_rebalancing_worker.rb index c31b6cfbd86..032ba5534e6 100644 --- a/app/workers/issue_rebalancing_worker.rb +++ b/app/workers/issue_rebalancing_worker.rb @@ -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 diff --git a/changelogs/unreleased/230835-add-index-of-merge_request_id-on-approval-merge-request-rules-tabl.yml b/changelogs/unreleased/230835-add-index-of-merge_request_id-on-approval-merge-request-rules-tabl.yml new file mode 100644 index 00000000000..a387a47164a --- /dev/null +++ b/changelogs/unreleased/230835-add-index-of-merge_request_id-on-approval-merge-request-rules-tabl.yml @@ -0,0 +1,5 @@ +--- +title: Add index on merge_request_id to approval_merge_request_rules +merge_request: 40556 +author: +type: other diff --git a/changelogs/unreleased/233002-diff-file-input-cutoff.yml b/changelogs/unreleased/233002-diff-file-input-cutoff.yml new file mode 100644 index 00000000000..018c3d0ef0a --- /dev/null +++ b/changelogs/unreleased/233002-diff-file-input-cutoff.yml @@ -0,0 +1,5 @@ +--- +title: Fix file file input top position cutoff +merge_request: 40634 +author: +type: fixed diff --git a/changelogs/unreleased/241000-front-matter-parsing.yml b/changelogs/unreleased/241000-front-matter-parsing.yml new file mode 100644 index 00000000000..a5a1b466ea4 --- /dev/null +++ b/changelogs/unreleased/241000-front-matter-parsing.yml @@ -0,0 +1,5 @@ +--- +title: Add toml and json front matter language support to Static Site Editor's WYSIWYG mode +merge_request: 40718 +author: +type: added diff --git a/changelogs/unreleased/241700-devops-score-migrate-empty-state-into-vue-component.yml b/changelogs/unreleased/241700-devops-score-migrate-empty-state-into-vue-component.yml new file mode 100644 index 00000000000..15128a69829 --- /dev/null +++ b/changelogs/unreleased/241700-devops-score-migrate-empty-state-into-vue-component.yml @@ -0,0 +1,5 @@ +--- +title: Migrate DevOps Score empty state into Vue component +merge_request: 40595 +author: +type: changed diff --git a/changelogs/unreleased/243444-user-cannot-sign-out-of-gitlab-once-admin-resets-their-password.yml b/changelogs/unreleased/243444-user-cannot-sign-out-of-gitlab-once-admin-resets-their-password.yml new file mode 100644 index 00000000000..fa57890b031 --- /dev/null +++ b/changelogs/unreleased/243444-user-cannot-sign-out-of-gitlab-once-admin-resets-their-password.yml @@ -0,0 +1,5 @@ +--- +title: Allow users with expired passwords to sign out +merge_request: 40830 +author: +type: fixed diff --git a/changelogs/unreleased/24629.yml b/changelogs/unreleased/24629.yml new file mode 100644 index 00000000000..16cc6d0db43 --- /dev/null +++ b/changelogs/unreleased/24629.yml @@ -0,0 +1,5 @@ +--- +title: Highlight un-focused/un-viewed file's in file tree +merge_request: 27937 +author: +type: changed diff --git a/changelogs/unreleased/bump-ado-image-to-v1-0-2.yml b/changelogs/unreleased/bump-ado-image-to-v1-0-2.yml new file mode 100644 index 00000000000..d24e9a1e6c3 --- /dev/null +++ b/changelogs/unreleased/bump-ado-image-to-v1-0-2.yml @@ -0,0 +1,5 @@ +--- +title: Fix auto-deploy-image external chart dependencies +merge_request: 40730 +author: +type: fixed diff --git a/changelogs/unreleased/jdb-fix-diffs-viewer-max-lines.yml b/changelogs/unreleased/jdb-fix-diffs-viewer-max-lines.yml new file mode 100644 index 00000000000..e482ce337e2 --- /dev/null +++ b/changelogs/unreleased/jdb-fix-diffs-viewer-max-lines.yml @@ -0,0 +1,5 @@ +--- +title: Fix client usage of max line rendering +merge_request: 40741 +author: +type: fixed diff --git a/changelogs/unreleased/nfriend-prevent-enter-submission.yml b/changelogs/unreleased/nfriend-prevent-enter-submission.yml new file mode 100644 index 00000000000..05db098c95a --- /dev/null +++ b/changelogs/unreleased/nfriend-prevent-enter-submission.yml @@ -0,0 +1,5 @@ +--- +title: Prevent form submission in search boxes on New Release and Edit Release pages +merge_request: 40011 +author: +type: changed diff --git a/db/migrate/20200826212800_add_index_on_merge_request_id_and_rule_type_to_approval_merge_request_rule.rb b/db/migrate/20200826212800_add_index_on_merge_request_id_and_rule_type_to_approval_merge_request_rule.rb new file mode 100644 index 00000000000..a40e09648ef --- /dev/null +++ b/db/migrate/20200826212800_add_index_on_merge_request_id_and_rule_type_to_approval_merge_request_rule.rb @@ -0,0 +1,23 @@ +# 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 diff --git a/db/schema_migrations/20200826212800 b/db/schema_migrations/20200826212800 new file mode 100644 index 00000000000..3eaae5c48c8 --- /dev/null +++ b/db/schema_migrations/20200826212800 @@ -0,0 +1 @@ +360c42f4d34c3b03e7a0375a0ff2776f066888f0a40131180bf301b876ea58db \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 00eae5f4f43..dbf4f29184e 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -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); diff --git a/doc/development/gotchas.md b/doc/development/gotchas.md index 6dff9deb59d..f7b44e74c17 100644 --- a/doc/development/gotchas.md +++ b/doc/development/gotchas.md @@ -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: diff --git a/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml index f234008dad4..e9d77766db3 100644 --- a/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml @@ -1,5 +1,5 @@ .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 diff --git a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml index 76fb2948144..41120750ff4 100644 --- a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml @@ -1,5 +1,5 @@ .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: diff --git a/locale/gitlab.pot b/locale/gitlab.pot index f79b82446e8..04f38ee91fe 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -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 "" diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb index 2eefe6771f1..7754fac2fb2 100644 --- a/spec/controllers/sessions_controller_spec.rb +++ b/spec/controllers/sessions_controller_spec.rb @@ -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 + + 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 - before do - set_devise_mapping(context: @request) - end - 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 diff --git a/spec/features/admin/admin_dev_ops_score_spec.rb b/spec/features/admin/admin_dev_ops_score_spec.rb index 31a2b4bbe72..1d3e7deef65 100644 --- a/spec/features/admin/admin_dev_ops_score_spec.rb +++ b/spec/features/admin/admin_dev_ops_score_spec.rb @@ -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 diff --git a/spec/frontend/__mocks__/lodash/debounce.js b/spec/frontend/__mocks__/lodash/debounce.js index 97fdb39097a..e8b61c80147 100644 --- a/spec/frontend/__mocks__/lodash/debounce.js +++ b/spec/frontend/__mocks__/lodash/debounce.js @@ -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; +}; diff --git a/spec/frontend/diffs/components/diff_file_header_spec.js b/spec/frontend/diffs/components/diff_file_header_spec.js index 5b5edcb2a34..1d8e1740a9e 100644 --- a/spec/frontend/diffs/components/diff_file_header_spec.js +++ b/spec/frontend/diffs/components/diff_file_header_spec.js @@ -44,6 +44,7 @@ describe('DiffFileHeader component', () => { toggleFileDiscussions: jest.fn(), toggleFileDiscussionWrappers: jest.fn(), toggleFullDiff: jest.fn(), + toggleActiveFileByHash: jest.fn(), }, }, }, diff --git a/spec/frontend/diffs/components/diff_file_row_spec.js b/spec/frontend/diffs/components/diff_file_row_spec.js index afdd4bfb335..23adc8f9da4 100644 --- a/spec/frontend/diffs/components/diff_file_row_spec.js +++ b/spec/frontend/diffs/components/diff_file_row_spec.js @@ -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 diff --git a/spec/frontend/diffs/components/tree_list_spec.js b/spec/frontend/diffs/components/tree_list_spec.js index 14cb2a17aec..cc177a81d88 100644 --- a/spec/frontend/diffs/components/tree_list_spec.js +++ b/spec/frontend/diffs/components/tree_list_spec.js @@ -1,16 +1,26 @@ 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, + }; + }); + + 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: [], + }, }; - wrapper = mount(TreeList, { - store, - localVue, - propsData: { hideFileStats: false }, + 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); + }); + }); + }); }); diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js index 858f362d3df..4f647b0cd41 100644 --- a/spec/frontend/diffs/store/actions_spec.js +++ b/spec/frontend/diffs/store/actions_spec.js @@ -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, ); diff --git a/spec/frontend/diffs/store/mutations_spec.js b/spec/frontend/diffs/store/mutations_spec.js index ce98bc58af7..393f042a9f9 100644 --- a/spec/frontend/diffs/store/mutations_spec.js +++ b/spec/frontend/diffs/store/mutations_spec.js @@ -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'); }); diff --git a/spec/frontend/ide/components/pipelines/list_spec.js b/spec/frontend/ide/components/pipelines/list_spec.js index 86cdbafaff9..7f083fa7c25 100644 --- a/spec/frontend/ide/components/pipelines/list_spec.js +++ b/spec/frontend/ide/components/pipelines/list_spec.js @@ -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); diff --git a/spec/frontend/milestones/project_milestone_combobox_spec.js b/spec/frontend/milestones/project_milestone_combobox_spec.js index 2265c9bdc2e..4859561c4e3 100644 --- a/spec/frontend/milestones/project_milestone_combobox_spec.js +++ b/spec/frontend/milestones/project_milestone_combobox_spec.js @@ -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); + }); + }); }); diff --git a/spec/frontend/ref/components/ref_selector_spec.js b/spec/frontend/ref/components/ref_selector_spec.js index 1556f5b19dc..6490bdcf038 100644 --- a/spec/frontend/ref/components/ref_selector_spec.js +++ b/spec/frontend/ref/components/ref_selector_spec.js @@ -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' }]); diff --git a/spec/frontend/static_site_editor/components/edit_area_spec.js b/spec/frontend/static_site_editor/components/edit_area_spec.js index f4be911171e..33c2f641ada 100644 --- a/spec/frontend/static_site_editor/components/edit_area_spec.js +++ b/spec/frontend/static_site_editor/components/edit_area_spec.js @@ -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'; diff --git a/spec/frontend/static_site_editor/graphql/resolvers/file_spec.js b/spec/frontend/static_site_editor/graphql/resolvers/file_spec.js index 8504d09e0f1..24651543650 100644 --- a/spec/frontend/static_site_editor/graphql/resolvers/file_spec.js +++ b/spec/frontend/static_site_editor/graphql/resolvers/file_spec.js @@ -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()); diff --git a/spec/frontend/static_site_editor/graphql/resolvers/submit_content_changes_spec.js b/spec/frontend/static_site_editor/graphql/resolvers/submit_content_changes_spec.js index 515b5394594..750b777cf5d 100644 --- a/spec/frontend/static_site_editor/graphql/resolvers/submit_content_changes_spec.js +++ b/spec/frontend/static_site_editor/graphql/resolvers/submit_content_changes_spec.js @@ -6,7 +6,7 @@ import { projectId as project, sourcePath, username, - sourceContent as content, + sourceContentYAML as content, savedContentMeta, } from '../../mock_data'; diff --git a/spec/frontend/static_site_editor/mock_data.js b/spec/frontend/static_site_editor/mock_data.js index 96de9b73af0..29a6fa23d2f 100644 --- a/spec/frontend/static_site_editor/mock_data.js +++ b/spec/frontend/static_site_editor/mock_data.js @@ -1,8 +1,18 @@ -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'; diff --git a/spec/frontend/static_site_editor/pages/home_spec.js b/spec/frontend/static_site_editor/pages/home_spec.js index c5473596df8..41f8a1075c0 100644 --- a/spec/frontend/static_site_editor/pages/home_spec.js +++ b/spec/frontend/static_site_editor/pages/home_spec.js @@ -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, diff --git a/spec/frontend/static_site_editor/services/load_source_content_spec.js b/spec/frontend/static_site_editor/services/load_source_content_spec.js index 87893bb7a6e..54061b7a503 100644 --- a/spec/frontend/static_site_editor/services/load_source_content_spec.js +++ b/spec/frontend/static_site_editor/services/load_source_content_spec.js @@ -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', () => { diff --git a/spec/frontend/static_site_editor/services/parse_source_file_language_support_spec.js b/spec/frontend/static_site_editor/services/parse_source_file_language_support_spec.js new file mode 100644 index 00000000000..9bc706c31d6 --- /dev/null +++ b/spec/frontend/static_site_editor/services/parse_source_file_language_support_spec.js @@ -0,0 +1,20 @@ +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}`); + } + }); + }); +}); diff --git a/spec/frontend/static_site_editor/services/parse_source_file_spec.js b/spec/frontend/static_site_editor/services/parse_source_file_spec.js index 19d1a21991f..6d55bed6721 100644 --- a/spec/frontend/static_site_editor/services/parse_source_file_spec.js +++ b/spec/frontend/static_site_editor/services/parse_source_file_spec.js @@ -1,12 +1,16 @@ 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); }, ); diff --git a/spec/frontend/static_site_editor/services/submit_content_changes_spec.js b/spec/frontend/static_site_editor/services/submit_content_changes_spec.js index 645ccedf7e7..d464e6b1895 100644 --- a/spec/frontend/static_site_editor/services/submit_content_changes_spec.js +++ b/spec/frontend/static_site_editor/services/submit_content_changes_spec.js @@ -20,7 +20,7 @@ import { commitMultipleResponse, createMergeRequestResponse, sourcePath, - sourceContent as content, + sourceContentYAML as content, trackingCategory, images, } from '../mock_data'; diff --git a/spec/frontend/vue_shared/components/file_row_spec.js b/spec/frontend/vue_shared/components/file_row_spec.js index c5917249f0a..d28c35d26bf 100644 --- a/spec/frontend/vue_shared/components/file_row_spec.js +++ b/spec/frontend/vue_shared/components/file_row_spec.js @@ -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'); + }); }); diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index ff6d448561d..0b9c06d9737 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -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 diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb index 004c37a0a7a..4edbbf4d4d8 100644 --- a/spec/services/issues/create_service_spec.rb +++ b/spec/services/issues/create_service_spec.rb @@ -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 diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index aa9ce4ce1a9..1fed6094314 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -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) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index de4bd178b9d..395930c8cbc 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -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 diff --git a/spec/support/helpers/next_found_instance_of.rb b/spec/support/helpers/next_found_instance_of.rb new file mode 100644 index 00000000000..ff34fcdd1d3 --- /dev/null +++ b/spec/support/helpers/next_found_instance_of.rb @@ -0,0 +1,33 @@ +# 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 diff --git a/spec/workers/issue_rebalancing_worker_spec.rb b/spec/workers/issue_rebalancing_worker_spec.rb index a2e44e74e06..8b0fcd4bc5a 100644 --- a/spec/workers/issue_rebalancing_worker_spec.rb +++ b/spec/workers/issue_rebalancing_worker_spec.rb @@ -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 diff --git a/spec/workers/new_note_worker_spec.rb b/spec/workers/new_note_worker_spec.rb index 76702ee0ffc..86b6d041e5c 100644 --- a/spec/workers/new_note_worker_spec.rb +++ b/spec/workers/new_note_worker_spec.rb @@ -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