Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
4ca378cac7
commit
8994750e4e
|
@ -308,7 +308,7 @@ rspec db-library-code pg12:
|
|||
- .rails:rules:ee-and-foss-db-library-code
|
||||
script:
|
||||
- *base-script
|
||||
- rspec_simple_job "-- spec/lib/gitlab/database/ spec/support/helpers/database/ ee/spec/lib/gitlab/database/ ee/spec/lib/ee/gitlab/database_spec.rb"
|
||||
- rspec_db_library_code
|
||||
|
||||
rspec fast_spec_helper:
|
||||
extends:
|
||||
|
|
|
@ -31,9 +31,6 @@ FactoryBot/InlineAssociation:
|
|||
|
||||
InternalAffairs/DeprecateCopHelper:
|
||||
Exclude:
|
||||
- 'spec/rubocop/code_reuse_helpers_spec.rb'
|
||||
- 'spec/rubocop/qa_helpers_spec.rb'
|
||||
- 'spec/rubocop/migration_helpers_spec.rb'
|
||||
- 'spec/rubocop/cop/group_public_or_visible_to_user_spec.rb'
|
||||
- 'spec/rubocop/cop/static_translation_definition_spec.rb'
|
||||
- 'spec/rubocop/cop/lint/last_keyword_argument_spec.rb'
|
||||
|
@ -64,25 +61,12 @@ InternalAffairs/DeprecateCopHelper:
|
|||
- 'spec/rubocop/cop/qa/ambiguous_page_object_name_spec.rb'
|
||||
- 'spec/rubocop/cop/qa/element_with_pattern_spec.rb'
|
||||
- 'spec/rubocop/cop/inject_enterprise_edition_module_spec.rb'
|
||||
- 'spec/rubocop/cop/code_reuse/finder_spec.rb'
|
||||
- 'spec/rubocop/cop/code_reuse/worker_spec.rb'
|
||||
- 'spec/rubocop/cop/code_reuse/service_class_spec.rb'
|
||||
- 'spec/rubocop/cop/code_reuse/presenter_spec.rb'
|
||||
- 'spec/rubocop/cop/code_reuse/serializer_spec.rb'
|
||||
- 'spec/rubocop/cop/avoid_keyword_arguments_in_sidekiq_workers_spec.rb'
|
||||
- 'spec/rubocop/cop/default_scope_spec.rb'
|
||||
- 'spec/rubocop/cop/graphql/resolver_type_spec.rb'
|
||||
- 'spec/rubocop/cop/graphql/descriptions_spec.rb'
|
||||
- 'spec/rubocop/cop/graphql/json_type_spec.rb'
|
||||
- 'spec/rubocop/cop/graphql/gid_expected_type_spec.rb'
|
||||
- 'spec/rubocop/cop/graphql/authorize_types_spec.rb'
|
||||
- 'spec/rubocop/cop/graphql/id_type_spec.rb'
|
||||
- 'spec/rubocop/cop/scalability/bulk_perform_with_context_spec.rb'
|
||||
- 'spec/rubocop/cop/scalability/idempotent_worker_spec.rb'
|
||||
- 'spec/rubocop/cop/scalability/cron_worker_context_spec.rb'
|
||||
- 'spec/rubocop/cop/scalability/file_uploads_spec.rb'
|
||||
- 'spec/rubocop/cop/api/grape_array_missing_coerce_spec.rb'
|
||||
- 'spec/rubocop/cop/api/base_spec.rb'
|
||||
- 'spec/rubocop/cop/destroy_all_spec.rb'
|
||||
- 'spec/rubocop/cop/safe_params_spec.rb'
|
||||
- 'spec/rubocop/cop/include_sidekiq_worker_spec.rb'
|
||||
|
|
|
@ -3,7 +3,10 @@ import { escape } from 'lodash';
|
|||
import { mapState, mapGetters, createNamespacedHelpers } from 'vuex';
|
||||
import { GlSprintf } from '@gitlab/ui';
|
||||
import { s__ } from '~/locale';
|
||||
import consts from '../../stores/modules/commit/constants';
|
||||
import {
|
||||
COMMIT_TO_CURRENT_BRANCH,
|
||||
COMMIT_TO_NEW_BRANCH,
|
||||
} from '../../stores/modules/commit/constants';
|
||||
import RadioGroup from './radio_group.vue';
|
||||
import NewMergeRequestOption from './new_merge_request_option.vue';
|
||||
|
||||
|
@ -53,14 +56,14 @@ export default {
|
|||
}
|
||||
|
||||
if (this.shouldDefaultToCurrentBranch) {
|
||||
this.updateCommitAction(consts.COMMIT_TO_CURRENT_BRANCH);
|
||||
this.updateCommitAction(COMMIT_TO_CURRENT_BRANCH);
|
||||
} else {
|
||||
this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH);
|
||||
this.updateCommitAction(COMMIT_TO_NEW_BRANCH);
|
||||
}
|
||||
},
|
||||
},
|
||||
commitToCurrentBranch: consts.COMMIT_TO_CURRENT_BRANCH,
|
||||
commitToNewBranch: consts.COMMIT_TO_NEW_BRANCH,
|
||||
commitToCurrentBranch: COMMIT_TO_CURRENT_BRANCH,
|
||||
commitToNewBranch: COMMIT_TO_NEW_BRANCH,
|
||||
currentBranchPermissionsTooltip: s__(
|
||||
"IDE|This option is disabled because you don't have write permissions for the current branch.",
|
||||
),
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { mapState, mapActions, mapGetters } from 'vuex';
|
||||
import { GlModal, GlSafeHtmlDirective, GlButton } from '@gitlab/ui';
|
||||
import { n__, __ } from '~/locale';
|
||||
import { n__ } from '~/locale';
|
||||
import CommitMessageField from './message_field.vue';
|
||||
import Actions from './actions.vue';
|
||||
import SuccessMessage from './success_message.vue';
|
||||
|
@ -35,10 +35,6 @@ export default {
|
|||
overviewText() {
|
||||
return n__('%d changed file', '%d changed files', this.stagedFiles.length);
|
||||
},
|
||||
commitButtonText() {
|
||||
return this.stagedFiles.length ? __('Commit') : __('Stage & Commit');
|
||||
},
|
||||
|
||||
currentViewIsCommitView() {
|
||||
return this.currentActivityView === leftSidebarViews.commit.name;
|
||||
},
|
||||
|
@ -160,13 +156,19 @@ export default {
|
|||
<gl-button
|
||||
:loading="submitCommitLoading"
|
||||
class="float-left qa-commit-button"
|
||||
data-testid="commit-button"
|
||||
category="primary"
|
||||
variant="success"
|
||||
@click="commit"
|
||||
>
|
||||
{{ __('Commit') }}
|
||||
</gl-button>
|
||||
<gl-button v-if="!discardDraftButtonDisabled" class="float-right" @click="discardDraft">
|
||||
<gl-button
|
||||
v-if="!discardDraftButtonDisabled"
|
||||
class="float-right"
|
||||
data-testid="discard-draft"
|
||||
@click="discardDraft"
|
||||
>
|
||||
{{ __('Discard draft') }}
|
||||
</gl-button>
|
||||
<gl-button
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { escape } from 'lodash';
|
||||
import { __ } from '~/locale';
|
||||
import consts from '../stores/modules/commit/constants';
|
||||
import { COMMIT_TO_NEW_BRANCH } from '../stores/modules/commit/constants';
|
||||
|
||||
const CODEOWNERS_REGEX = /Push.*protected branches.*CODEOWNERS/;
|
||||
const BRANCH_CHANGED_REGEX = /changed.*since.*start.*edit/;
|
||||
|
@ -8,7 +8,7 @@ const BRANCH_ALREADY_EXISTS = /branch.*already.*exists/;
|
|||
|
||||
const createNewBranchAndCommit = (store) =>
|
||||
store
|
||||
.dispatch('commit/updateCommitAction', consts.COMMIT_TO_NEW_BRANCH)
|
||||
.dispatch('commit/updateCommitAction', COMMIT_TO_NEW_BRANCH)
|
||||
.then(() => store.dispatch('commit/commitChanges'));
|
||||
|
||||
export const createUnexpectedCommitError = (message) => ({
|
||||
|
|
|
@ -4,7 +4,7 @@ import * as rootTypes from '../../mutation_types';
|
|||
import { createCommitPayload, createNewMergeRequestUrl } from '../../utils';
|
||||
import service from '../../../services';
|
||||
import * as types from './mutation_types';
|
||||
import consts from './constants';
|
||||
import { COMMIT_TO_CURRENT_BRANCH } from './constants';
|
||||
import { leftSidebarViews } from '../../../constants';
|
||||
import eventHub from '../../../eventhub';
|
||||
import { parseCommitError } from '../../../lib/errors';
|
||||
|
@ -112,7 +112,7 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
|
|||
// Pull commit options out because they could change
|
||||
// During some of the pre and post commit processing
|
||||
const { shouldCreateMR, shouldHideNewMrOption, isCreatingNewBranch, branchName } = getters;
|
||||
const newBranch = state.commitAction !== consts.COMMIT_TO_CURRENT_BRANCH;
|
||||
const newBranch = state.commitAction !== COMMIT_TO_CURRENT_BRANCH;
|
||||
const stageFilesPromise = rootState.stagedFiles.length
|
||||
? Promise.resolve()
|
||||
: dispatch('stageAllChanges', null, { root: true });
|
||||
|
@ -206,7 +206,7 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
|
|||
dispatch('updateViewer', 'editor', { root: true });
|
||||
}
|
||||
})
|
||||
.then(() => dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH))
|
||||
.then(() => dispatch('updateCommitAction', COMMIT_TO_CURRENT_BRANCH))
|
||||
.then(() => {
|
||||
if (newBranch) {
|
||||
const path = rootGetters.activeFile ? rootGetters.activeFile.path : '';
|
||||
|
|
|
@ -1,7 +1,2 @@
|
|||
const COMMIT_TO_CURRENT_BRANCH = '1';
|
||||
const COMMIT_TO_NEW_BRANCH = '2';
|
||||
|
||||
export default {
|
||||
COMMIT_TO_CURRENT_BRANCH,
|
||||
COMMIT_TO_NEW_BRANCH,
|
||||
};
|
||||
export const COMMIT_TO_CURRENT_BRANCH = '1';
|
||||
export const COMMIT_TO_NEW_BRANCH = '2';
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { sprintf, n__, __ } from '../../../../locale';
|
||||
import consts from './constants';
|
||||
import { COMMIT_TO_NEW_BRANCH } from './constants';
|
||||
|
||||
const BRANCH_SUFFIX_COUNT = 5;
|
||||
const createTranslatedTextForFiles = (files, text) => {
|
||||
|
@ -48,7 +48,7 @@ export const preBuiltCommitMessage = (state, _, rootState) => {
|
|||
.join('\n');
|
||||
};
|
||||
|
||||
export const isCreatingNewBranch = (state) => state.commitAction === consts.COMMIT_TO_NEW_BRANCH;
|
||||
export const isCreatingNewBranch = (state) => state.commitAction === COMMIT_TO_NEW_BRANCH;
|
||||
|
||||
export const shouldHideNewMrOption = (_state, getters, _rootState, rootGetters) =>
|
||||
!getters.isCreatingNewBranch &&
|
||||
|
|
|
@ -118,7 +118,7 @@ module Resolvers
|
|||
end
|
||||
|
||||
def offset_pagination(relation)
|
||||
::Gitlab::Graphql::Pagination::OffsetActiveRecordRelationConnection.new(relation)
|
||||
::Gitlab::Graphql::Pagination::OffsetPaginatedRelation.new(relation)
|
||||
end
|
||||
|
||||
override :object
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
.settings-header
|
||||
%h4
|
||||
= _('External authentication')
|
||||
%button.btn.js-settings-toggle{ type: 'button' }
|
||||
%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
|
||||
= expanded ? 'Collapse' : 'Expand'
|
||||
%p
|
||||
= _('External Classification Policy Authorization')
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
.settings-header
|
||||
%h4
|
||||
= _('Email')
|
||||
%button.btn.btn-default.js-settings-toggle{ type: 'button' }
|
||||
%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
|
||||
= expanded_by_default? ? _('Collapse') : _('Expand')
|
||||
%p
|
||||
= _('Various email settings.')
|
||||
|
@ -17,7 +17,7 @@
|
|||
.settings-header
|
||||
%h4
|
||||
= _('Help page')
|
||||
%button.btn.btn-default.js-settings-toggle{ type: 'button' }
|
||||
%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
|
||||
= expanded_by_default? ? _('Collapse') : _('Expand')
|
||||
%p
|
||||
= _('Help page text and support page url.')
|
||||
|
@ -28,7 +28,7 @@
|
|||
.settings-header
|
||||
%h4
|
||||
= _('Pages')
|
||||
%button.btn.btn-default.js-settings-toggle{ type: 'button' }
|
||||
%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
|
||||
= expanded_by_default? ? _('Collapse') : _('Expand')
|
||||
%p
|
||||
= _('Size and domain settings for static websites')
|
||||
|
@ -39,7 +39,7 @@
|
|||
.settings-header
|
||||
%h4
|
||||
= _('Real-time features')
|
||||
%button.btn.btn-default.js-settings-toggle{ type: 'button' }
|
||||
%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
|
||||
= expanded_by_default? ? _('Collapse') : _('Expand')
|
||||
%p
|
||||
= _('Change this value to influence how frequently the GitLab UI polls for updates.')
|
||||
|
@ -50,7 +50,7 @@
|
|||
.settings-header
|
||||
%h4
|
||||
= _('Gitaly')
|
||||
%button.btn.btn-default.js-settings-toggle{ type: 'button' }
|
||||
%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
|
||||
= expanded_by_default? ? _('Collapse') : _('Expand')
|
||||
%p
|
||||
= _('Configure Gitaly timeouts.')
|
||||
|
@ -61,7 +61,7 @@
|
|||
.settings-header
|
||||
%h4
|
||||
= _('Localization')
|
||||
%button.btn.btn-default.js-settings-toggle{ type: 'button' }
|
||||
%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
|
||||
= expanded_by_default? ? _('Collapse') : _('Expand')
|
||||
%p
|
||||
= _('Various localization settings.')
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
.settings-header
|
||||
%h4
|
||||
= _('Spam and Anti-bot Protection')
|
||||
%button.btn.btn-default.js-settings-toggle{ type: 'button' }
|
||||
%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
|
||||
= expanded_by_default? ? _('Collapse') : _('Expand')
|
||||
%p
|
||||
- recaptcha_v2_link_url = 'https://developers.google.com/recaptcha/docs/versions'
|
||||
|
@ -19,7 +19,7 @@
|
|||
.settings-header
|
||||
%h4
|
||||
= _('Abuse reports')
|
||||
%button.btn.btn-default.js-settings-toggle{ type: 'button' }
|
||||
%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
|
||||
= expanded_by_default? ? _('Collapse') : _('Expand')
|
||||
%p
|
||||
= _('Set notification email for abuse reports.')
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
.settings-header
|
||||
%h4
|
||||
= _('Default initial branch name')
|
||||
%button.gl-button.js-settings-toggle{ type: 'button' }
|
||||
%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
|
||||
= expanded_by_default? ? _('Collapse') : _('Expand')
|
||||
%p
|
||||
= _('Set the default name of the initial branch when creating new repositories through the user interface.')
|
||||
|
@ -18,7 +18,7 @@
|
|||
.settings-header
|
||||
%h4
|
||||
= _('Repository mirroring')
|
||||
%button.btn.js-settings-toggle{ type: 'button' }
|
||||
%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
|
||||
= expanded_by_default? ? 'Collapse' : 'Expand'
|
||||
%p
|
||||
= _('Configure repository mirroring.')
|
||||
|
@ -29,7 +29,7 @@
|
|||
.settings-header
|
||||
%h4
|
||||
= _('Repository storage')
|
||||
%button.btn.btn-default.js-settings-toggle{ type: 'button' }
|
||||
%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
|
||||
= expanded_by_default? ? _('Collapse') : _('Expand')
|
||||
%p
|
||||
= _('Configure storage path settings.')
|
||||
|
@ -40,7 +40,7 @@
|
|||
.settings-header
|
||||
%h4
|
||||
= _('Repository maintenance')
|
||||
%button.btn.btn-default.js-settings-toggle{ type: 'button' }
|
||||
%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
|
||||
= expanded_by_default? ? _('Collapse') : _('Expand')
|
||||
%p
|
||||
= _('Configure automatic git checks and housekeeping on repositories.')
|
||||
|
@ -51,7 +51,7 @@
|
|||
.settings-header
|
||||
%h4
|
||||
= _('Repository static objects')
|
||||
%button.btn.btn-default.js-settings-toggle{ type: 'button' }
|
||||
%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
|
||||
= expanded_by_default? ? _('Collapse') : _('Expand')
|
||||
%p
|
||||
= _('Serve repository static objects (e.g. archives, blobs, ...) from an external storage (e.g. a CDN).')
|
||||
|
|
|
@ -20,7 +20,6 @@
|
|||
.form-group.col-sm-12.js-other-role-group{ class: ("hidden") }
|
||||
= f.label :other_role, _('What is your job title? (optional)'), class: 'form-check-label gl-mb-3'
|
||||
= f.text_field :other_role, class: 'form-control'
|
||||
- else
|
||||
.row
|
||||
.form-group.col-sm-12
|
||||
.form-text.gl-text-gray-500.gl-mt-0.gl-line-height-normal.gl-px-1= _('This will help us personalize your onboarding experience.')
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Apply new GitLab UI for buttons in admin settings
|
||||
merge_request: 51789
|
||||
author: Yogi (@yo)
|
||||
type: other
|
|
@ -1,13 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
if Gitlab::Runtime.puma? && ::Puma.cli_config.options[:workers].to_i == 0
|
||||
return if allow_single_mode?
|
||||
|
||||
raise 'Puma is only supported in Cluster-mode: workers > 0'
|
||||
end
|
||||
|
||||
def allow_single_mode?
|
||||
return false if Gitlab.com?
|
||||
|
||||
Gitlab::Utils.to_boolean(ENV['PUMA_SKIP_CLUSTER_VALIDATION'])
|
||||
end
|
||||
|
||||
if Gitlab::Runtime.puma? && ::Puma.cli_config.options[:workers].to_i == 0
|
||||
return if allow_single_mode?
|
||||
|
||||
raise 'Puma is only supported in Cluster-mode: workers > 0'
|
||||
end
|
||||
|
|
|
@ -634,6 +634,20 @@ For each Patroni instance on the secondary site:
|
|||
to `gitlab.rb` where `<slot_name>` is the name of the replication slot for your Geo secondary. This will ensure that Patroni recognizes the replication slot as permanent and will not drop it upon restarting.
|
||||
1. If database replication to the secondary was paused before migration, resume replication once Patroni is confirmed working on the primary.
|
||||
|
||||
## Migrating a single PostgreSQL node to Patroni
|
||||
|
||||
Before the introduction of Patroni, Geo had no Omnibus support for HA setups on the secondary node.
|
||||
|
||||
With Patroni it's now possible to support that. In order to migrate the existing PostgreSQL to Patroni:
|
||||
|
||||
1. Make sure you have a Consul cluster setup on the secondary (similar to how you set it up on the primary).
|
||||
1. [Configure a permanent replication slot](#step-1-configure-patroni-permanent-replication-slot-on-the-primary-site).
|
||||
1. [Configure a Standby Cluster](#step-2-configure-a-standby-cluster-on-the-secondary-site)
|
||||
on that single node machine.
|
||||
|
||||
You will end up with a "Standby Cluster" with a single node. That allows you to later on add additional patroni nodes
|
||||
by following the same instructions above.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
Read the [troubleshooting document](../replication/troubleshooting.md).
|
||||
|
|
|
@ -5,6 +5,10 @@ module Gitlab
|
|||
module Pagination
|
||||
module Connections
|
||||
def self.use(schema)
|
||||
schema.connections.add(
|
||||
::Gitlab::Graphql::Pagination::OffsetPaginatedRelation,
|
||||
::Gitlab::Graphql::Pagination::OffsetActiveRecordRelationConnection)
|
||||
|
||||
schema.connections.add(
|
||||
ActiveRecord::Relation,
|
||||
Gitlab::Graphql::Pagination::Keyset::Connection)
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Marker class to enable us to choose the correct
|
||||
# connection type during resolution
|
||||
module Gitlab
|
||||
module Graphql
|
||||
module Pagination
|
||||
class OffsetPaginatedRelation < SimpleDelegator
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -26815,9 +26815,6 @@ msgstr ""
|
|||
msgid "Stage"
|
||||
msgstr ""
|
||||
|
||||
msgid "Stage & Commit"
|
||||
msgstr ""
|
||||
|
||||
msgid "Stage data updated"
|
||||
msgstr ""
|
||||
|
||||
|
@ -28325,9 +28322,6 @@ msgstr ""
|
|||
msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request."
|
||||
msgstr ""
|
||||
|
||||
msgid "The roadmap shows the progress of your epics along a timeline"
|
||||
msgstr ""
|
||||
|
||||
msgid "The same shared runner executes code from multiple projects, unless you configure autoscaling with %{link} set to 1 (which it is on GitLab.com)."
|
||||
msgstr ""
|
||||
|
||||
|
@ -29757,9 +29751,6 @@ 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 ""
|
||||
|
||||
msgid "To widen your search, change or remove filters above"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -75,6 +75,16 @@ function rspec_simple_job() {
|
|||
bin/rspec -Ispec -rspec_helper --color --format documentation --format RspecJunitFormatter --out junit_rspec.xml ${rspec_opts}
|
||||
}
|
||||
|
||||
function rspec_db_library_code() {
|
||||
local db_files="spec/lib/gitlab/database/ spec/support/helpers/database/"
|
||||
|
||||
if [[ -d "ee/" ]]; then
|
||||
db_files="${db_files} ee/spec/lib/gitlab/database/ ee/spec/lib/ee/gitlab/database_spec.rb"
|
||||
fi
|
||||
|
||||
rspec_simple_job "-- ${db_files}"
|
||||
}
|
||||
|
||||
function rspec_paralellized_job() {
|
||||
read -ra job_name <<< "${CI_JOB_NAME}"
|
||||
local test_tool="${job_name[0]}"
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'User closes/reopens a merge request', :js do
|
||||
RSpec.describe 'User closes/reopens a merge request', :js, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/297500' do
|
||||
let_it_be(:project) { create(:project, :repository) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
|
@ -55,7 +55,7 @@ RSpec.describe 'User closes/reopens a merge request', :js do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'when closed', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/297500' do
|
||||
describe 'when closed' do
|
||||
context 'when clicking the top `Reopen merge request` link', :aggregate_failures do
|
||||
let(:closed_merge_request) { create(:merge_request, source_project: project, target_project: project, state: 'closed') }
|
||||
|
||||
|
|
|
@ -1,7 +1,32 @@
|
|||
/**
|
||||
* Returns a new object with keys pointing to stubbed methods
|
||||
*
|
||||
* This is helpful for stubbing components like GlModal where it's supported
|
||||
* in the API to call `.show()` and `.hide()` ([Bootstrap Vue docs][1]).
|
||||
*
|
||||
* [1]: https://bootstrap-vue.org/docs/components/modal#using-show-hide-and-toggle-component-methods
|
||||
*
|
||||
* @param {Object} methods - Object whose keys will be in the returned object.
|
||||
*/
|
||||
const createStubbedMethods = (methods = {}) => {
|
||||
if (!methods) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return Object.keys(methods).reduce(
|
||||
(acc, key) =>
|
||||
Object.assign(acc, {
|
||||
[key]: () => {},
|
||||
}),
|
||||
{},
|
||||
);
|
||||
};
|
||||
|
||||
export function stubComponent(Component, options = {}) {
|
||||
return {
|
||||
props: Component.props,
|
||||
model: Component.model,
|
||||
methods: createStubbedMethods(Component.methods),
|
||||
// Do not render any slots/scoped slots except default
|
||||
// This differs from VTU behavior which renders all slots
|
||||
template: '<div><slot></slot></div>',
|
||||
|
|
|
@ -3,7 +3,10 @@ import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
|
|||
import { projectData, branches } from 'jest/ide/mock_data';
|
||||
import { createStore } from '~/ide/stores';
|
||||
import commitActions from '~/ide/components/commit_sidebar/actions.vue';
|
||||
import consts from '~/ide/stores/modules/commit/constants';
|
||||
import {
|
||||
COMMIT_TO_NEW_BRANCH,
|
||||
COMMIT_TO_CURRENT_BRANCH,
|
||||
} from '~/ide/stores/modules/commit/constants';
|
||||
|
||||
const ACTION_UPDATE_COMMIT_ACTION = 'commit/updateCommitAction';
|
||||
|
||||
|
@ -126,16 +129,16 @@ describe('IDE commit sidebar actions', () => {
|
|||
|
||||
it.each`
|
||||
input | expectedOption
|
||||
${{ currentBranchId: BRANCH_DEFAULT }} | ${consts.COMMIT_TO_NEW_BRANCH}
|
||||
${{ currentBranchId: BRANCH_DEFAULT, emptyRepo: true }} | ${consts.COMMIT_TO_CURRENT_BRANCH}
|
||||
${{ currentBranchId: BRANCH_PROTECTED, hasMR: true }} | ${consts.COMMIT_TO_CURRENT_BRANCH}
|
||||
${{ currentBranchId: BRANCH_PROTECTED, hasMR: false }} | ${consts.COMMIT_TO_CURRENT_BRANCH}
|
||||
${{ currentBranchId: BRANCH_PROTECTED_NO_ACCESS, hasMR: true }} | ${consts.COMMIT_TO_NEW_BRANCH}
|
||||
${{ currentBranchId: BRANCH_PROTECTED_NO_ACCESS, hasMR: false }} | ${consts.COMMIT_TO_NEW_BRANCH}
|
||||
${{ currentBranchId: BRANCH_REGULAR, hasMR: true }} | ${consts.COMMIT_TO_CURRENT_BRANCH}
|
||||
${{ currentBranchId: BRANCH_REGULAR, hasMR: false }} | ${consts.COMMIT_TO_CURRENT_BRANCH}
|
||||
${{ currentBranchId: BRANCH_REGULAR_NO_ACCESS, hasMR: true }} | ${consts.COMMIT_TO_NEW_BRANCH}
|
||||
${{ currentBranchId: BRANCH_REGULAR_NO_ACCESS, hasMR: false }} | ${consts.COMMIT_TO_NEW_BRANCH}
|
||||
${{ currentBranchId: BRANCH_DEFAULT }} | ${COMMIT_TO_NEW_BRANCH}
|
||||
${{ currentBranchId: BRANCH_DEFAULT, emptyRepo: true }} | ${COMMIT_TO_CURRENT_BRANCH}
|
||||
${{ currentBranchId: BRANCH_PROTECTED, hasMR: true }} | ${COMMIT_TO_CURRENT_BRANCH}
|
||||
${{ currentBranchId: BRANCH_PROTECTED, hasMR: false }} | ${COMMIT_TO_CURRENT_BRANCH}
|
||||
${{ currentBranchId: BRANCH_PROTECTED_NO_ACCESS, hasMR: true }} | ${COMMIT_TO_NEW_BRANCH}
|
||||
${{ currentBranchId: BRANCH_PROTECTED_NO_ACCESS, hasMR: false }} | ${COMMIT_TO_NEW_BRANCH}
|
||||
${{ currentBranchId: BRANCH_REGULAR, hasMR: true }} | ${COMMIT_TO_CURRENT_BRANCH}
|
||||
${{ currentBranchId: BRANCH_REGULAR, hasMR: false }} | ${COMMIT_TO_CURRENT_BRANCH}
|
||||
${{ currentBranchId: BRANCH_REGULAR_NO_ACCESS, hasMR: true }} | ${COMMIT_TO_NEW_BRANCH}
|
||||
${{ currentBranchId: BRANCH_REGULAR_NO_ACCESS, hasMR: false }} | ${COMMIT_TO_NEW_BRANCH}
|
||||
`(
|
||||
'with $input, it dispatches update commit action with $expectedOption',
|
||||
({ input, expectedOption }) => {
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import Vue from 'vue';
|
||||
import { getByText } from '@testing-library/dom';
|
||||
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { GlModal } from '@gitlab/ui';
|
||||
import { projectData } from 'jest/ide/mock_data';
|
||||
import { stubComponent } from 'helpers/stub_component';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import { createStore } from '~/ide/stores';
|
||||
import consts from '~/ide/stores/modules/commit/constants';
|
||||
import { COMMIT_TO_NEW_BRANCH } from '~/ide/stores/modules/commit/constants';
|
||||
import CommitForm from '~/ide/components/commit_sidebar/form.vue';
|
||||
import CommitMessageField from '~/ide/components/commit_sidebar/message_field.vue';
|
||||
import { leftSidebarViews } from '~/ide/constants';
|
||||
import {
|
||||
createCodeownersCommitError,
|
||||
|
@ -15,256 +17,245 @@ import {
|
|||
} from '~/ide/lib/errors';
|
||||
|
||||
describe('IDE commit form', () => {
|
||||
const Component = Vue.extend(CommitForm);
|
||||
let vm;
|
||||
let wrapper;
|
||||
let store;
|
||||
|
||||
const beginCommitButton = () => vm.$el.querySelector('[data-testid="begin-commit-button"]');
|
||||
const createComponent = () => {
|
||||
wrapper = shallowMount(CommitForm, {
|
||||
store,
|
||||
stubs: {
|
||||
GlModal: stubComponent(GlModal),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const setLastCommitMessage = (msg) => {
|
||||
store.state.lastCommitMsg = msg;
|
||||
};
|
||||
const goToCommitView = () => {
|
||||
store.state.currentActivityView = leftSidebarViews.commit.name;
|
||||
};
|
||||
const goToEditView = () => {
|
||||
store.state.currentActivityView = leftSidebarViews.edit.name;
|
||||
};
|
||||
const findBeginCommitButton = () => wrapper.find('[data-testid="begin-commit-button"]');
|
||||
const findCommitButton = () => wrapper.find('[data-testid="commit-button"]');
|
||||
const findForm = () => wrapper.find('form');
|
||||
const findCommitMessageInput = () => wrapper.find(CommitMessageField);
|
||||
const setCommitMessageInput = (val) => findCommitMessageInput().vm.$emit('input', val);
|
||||
const findDiscardDraftButton = () => wrapper.find('[data-testid="discard-draft"]');
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore();
|
||||
store.state.changedFiles.push('test');
|
||||
store.state.stagedFiles.push('test');
|
||||
store.state.currentProjectId = 'abcproject';
|
||||
store.state.currentBranchId = 'master';
|
||||
Vue.set(store.state.projects, 'abcproject', { ...projectData });
|
||||
|
||||
vm = createComponentWithStore(Component, store).$mount();
|
||||
Vue.set(store.state.projects, 'abcproject', {
|
||||
...projectData,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
});
|
||||
|
||||
it('enables begin commit button when there are changes', () => {
|
||||
expect(beginCommitButton()).not.toHaveAttr('disabled');
|
||||
describe.each`
|
||||
desc | stagedFiles | disabled
|
||||
${'when there are changes'} | ${['test']} | ${false}
|
||||
${'when there are no changes'} | ${[]} | ${true}
|
||||
`('$desc', ({ stagedFiles, disabled }) => {
|
||||
beforeEach(async () => {
|
||||
store.state.stagedFiles = stagedFiles;
|
||||
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it(`begin button disabled=${disabled}`, async () => {
|
||||
expect(findBeginCommitButton().props('disabled')).toBe(disabled);
|
||||
});
|
||||
});
|
||||
|
||||
it('disables begin commit button when there are no changes', async () => {
|
||||
store.state.changedFiles = [];
|
||||
await vm.$nextTick();
|
||||
describe('on edit tab', () => {
|
||||
beforeEach(async () => {
|
||||
// Test that we react to switching to compact view.
|
||||
goToCommitView();
|
||||
|
||||
expect(beginCommitButton()).toHaveAttr('disabled');
|
||||
});
|
||||
createComponent();
|
||||
|
||||
describe('compact', () => {
|
||||
beforeEach(() => {
|
||||
vm.isCompact = true;
|
||||
goToEditView();
|
||||
|
||||
return vm.$nextTick();
|
||||
await wrapper.vm.$nextTick();
|
||||
});
|
||||
|
||||
it('renders commit button in compact mode', () => {
|
||||
expect(beginCommitButton()).not.toBeNull();
|
||||
expect(beginCommitButton().textContent).toContain('Commit');
|
||||
expect(findBeginCommitButton().exists()).toBe(true);
|
||||
expect(findBeginCommitButton().text()).toBe('Commit…');
|
||||
});
|
||||
|
||||
it('does not render form', () => {
|
||||
expect(vm.$el.querySelector('form')).toBeNull();
|
||||
expect(findForm().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('renders overview text', () => {
|
||||
vm.$store.state.stagedFiles.push('test');
|
||||
|
||||
return vm.$nextTick(() => {
|
||||
expect(vm.$el.querySelector('p').textContent).toContain('1 changed file');
|
||||
});
|
||||
expect(wrapper.find('p').text()).toBe('1 changed file');
|
||||
});
|
||||
|
||||
it('shows form when clicking commit button', () => {
|
||||
beginCommitButton().click();
|
||||
it('when begin commit button is clicked, shows form', async () => {
|
||||
findBeginCommitButton().vm.$emit('click');
|
||||
|
||||
return vm.$nextTick(() => {
|
||||
expect(vm.$el.querySelector('form')).not.toBeNull();
|
||||
});
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(findForm().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('toggles activity bar view when clicking commit button', () => {
|
||||
beginCommitButton().click();
|
||||
it('when begin commit button is clicked, sets activity view', async () => {
|
||||
findBeginCommitButton().vm.$emit('click');
|
||||
|
||||
return vm.$nextTick(() => {
|
||||
expect(store.state.currentActivityView).toBe(leftSidebarViews.commit.name);
|
||||
});
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(store.state.currentActivityView).toBe(leftSidebarViews.commit.name);
|
||||
});
|
||||
|
||||
it('collapses if lastCommitMsg is set to empty and current view is not commit view', async () => {
|
||||
store.state.lastCommitMsg = 'abc';
|
||||
store.state.currentActivityView = leftSidebarViews.edit.name;
|
||||
await vm.$nextTick();
|
||||
// Test that it expands when lastCommitMsg is set
|
||||
setLastCommitMessage('test');
|
||||
goToEditView();
|
||||
|
||||
// if commit message is set, form is uncollapsed
|
||||
expect(vm.isCompact).toBe(false);
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
store.state.lastCommitMsg = '';
|
||||
await vm.$nextTick();
|
||||
expect(findForm().exists()).toBe(true);
|
||||
|
||||
// collapsed when set to empty
|
||||
expect(vm.isCompact).toBe(true);
|
||||
});
|
||||
// Now test that it collapses when lastCommitMsg is cleared
|
||||
setLastCommitMessage('');
|
||||
|
||||
it('collapses if in commit view but there are no changes and vice versa', async () => {
|
||||
store.state.currentActivityView = leftSidebarViews.commit.name;
|
||||
await vm.$nextTick();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
// expanded by default if there are changes
|
||||
expect(vm.isCompact).toBe(false);
|
||||
|
||||
store.state.changedFiles = [];
|
||||
await vm.$nextTick();
|
||||
|
||||
expect(vm.isCompact).toBe(true);
|
||||
|
||||
store.state.changedFiles.push('test');
|
||||
await vm.$nextTick();
|
||||
|
||||
// uncollapsed once again
|
||||
expect(vm.isCompact).toBe(false);
|
||||
});
|
||||
|
||||
it('collapses if switched from commit view to edit view and vice versa', async () => {
|
||||
store.state.currentActivityView = leftSidebarViews.edit.name;
|
||||
await vm.$nextTick();
|
||||
|
||||
expect(vm.isCompact).toBe(true);
|
||||
|
||||
store.state.currentActivityView = leftSidebarViews.commit.name;
|
||||
await vm.$nextTick();
|
||||
|
||||
expect(vm.isCompact).toBe(false);
|
||||
|
||||
store.state.currentActivityView = leftSidebarViews.edit.name;
|
||||
await vm.$nextTick();
|
||||
|
||||
expect(vm.isCompact).toBe(true);
|
||||
});
|
||||
|
||||
describe('when window height is less than MAX_WINDOW_HEIGHT', () => {
|
||||
let oldHeight;
|
||||
|
||||
beforeEach(() => {
|
||||
oldHeight = window.innerHeight;
|
||||
window.innerHeight = 700;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.innerHeight = oldHeight;
|
||||
});
|
||||
|
||||
it('stays collapsed when switching from edit view to commit view and back', async () => {
|
||||
store.state.currentActivityView = leftSidebarViews.edit.name;
|
||||
await vm.$nextTick();
|
||||
|
||||
expect(vm.isCompact).toBe(true);
|
||||
|
||||
store.state.currentActivityView = leftSidebarViews.commit.name;
|
||||
await vm.$nextTick();
|
||||
|
||||
expect(vm.isCompact).toBe(true);
|
||||
|
||||
store.state.currentActivityView = leftSidebarViews.edit.name;
|
||||
await vm.$nextTick();
|
||||
|
||||
expect(vm.isCompact).toBe(true);
|
||||
});
|
||||
|
||||
it('stays uncollapsed if changes are added or removed', async () => {
|
||||
store.state.currentActivityView = leftSidebarViews.commit.name;
|
||||
await vm.$nextTick();
|
||||
|
||||
expect(vm.isCompact).toBe(true);
|
||||
|
||||
store.state.changedFiles = [];
|
||||
await vm.$nextTick();
|
||||
|
||||
expect(vm.isCompact).toBe(true);
|
||||
|
||||
store.state.changedFiles.push('test');
|
||||
await vm.$nextTick();
|
||||
|
||||
expect(vm.isCompact).toBe(true);
|
||||
});
|
||||
|
||||
it('uncollapses when clicked on Commit button in the edit view', async () => {
|
||||
store.state.currentActivityView = leftSidebarViews.edit.name;
|
||||
beginCommitButton().click();
|
||||
await waitForPromises();
|
||||
|
||||
expect(vm.isCompact).toBe(false);
|
||||
});
|
||||
expect(findForm().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('full', () => {
|
||||
beforeEach(() => {
|
||||
vm.isCompact = false;
|
||||
describe('on commit tab when window height is less than MAX_WINDOW_HEIGHT', () => {
|
||||
let oldHeight;
|
||||
|
||||
return vm.$nextTick();
|
||||
beforeEach(async () => {
|
||||
oldHeight = window.innerHeight;
|
||||
window.innerHeight = 700;
|
||||
|
||||
createComponent();
|
||||
|
||||
goToCommitView();
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
});
|
||||
|
||||
it('updates commitMessage in store on input', () => {
|
||||
const textarea = vm.$el.querySelector('textarea');
|
||||
afterEach(() => {
|
||||
window.innerHeight = oldHeight;
|
||||
});
|
||||
|
||||
textarea.value = 'testing commit message';
|
||||
it('stays collapsed if changes are added or removed', async () => {
|
||||
expect(findForm().exists()).toBe(false);
|
||||
|
||||
textarea.dispatchEvent(new Event('input'));
|
||||
store.state.stagedFiles = [];
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
return vm.$nextTick().then(() => {
|
||||
expect(vm.$store.state.commit.commitMessage).toBe('testing commit message');
|
||||
expect(findForm().exists()).toBe(false);
|
||||
|
||||
store.state.stagedFiles.push('test');
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(findForm().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('on commit tab', () => {
|
||||
beforeEach(async () => {
|
||||
// Test that the component reacts to switching to full view
|
||||
goToEditView();
|
||||
|
||||
createComponent();
|
||||
|
||||
goToCommitView();
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
});
|
||||
|
||||
it('shows form', () => {
|
||||
expect(findForm().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('hides begin commit button', () => {
|
||||
expect(findBeginCommitButton().exists()).toBe(false);
|
||||
});
|
||||
|
||||
describe('when no changed files', () => {
|
||||
beforeEach(async () => {
|
||||
store.state.stagedFiles = [];
|
||||
await wrapper.vm.$nextTick();
|
||||
});
|
||||
|
||||
it('hides form', () => {
|
||||
expect(findForm().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('expands again when staged files are added', async () => {
|
||||
store.state.stagedFiles.push('test');
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(findForm().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('updating currentActivityView not to commit view sets compact mode', () => {
|
||||
store.state.currentActivityView = 'a';
|
||||
it('updates commitMessage in store on input', async () => {
|
||||
setCommitMessageInput('testing commit message');
|
||||
|
||||
return vm.$nextTick(() => {
|
||||
expect(vm.isCompact).toBe(true);
|
||||
});
|
||||
});
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
it('always opens itself in full view current activity view is not commit view when clicking commit button', () => {
|
||||
beginCommitButton().click();
|
||||
|
||||
return vm.$nextTick(() => {
|
||||
expect(store.state.currentActivityView).toBe(leftSidebarViews.commit.name);
|
||||
expect(vm.isCompact).toBe(false);
|
||||
});
|
||||
expect(store.state.commit.commitMessage).toBe('testing commit message');
|
||||
});
|
||||
|
||||
describe('discard draft button', () => {
|
||||
it('hidden when commitMessage is empty', () => {
|
||||
expect(vm.$el.querySelector('.btn-default').textContent).toContain('Collapse');
|
||||
expect(findDiscardDraftButton().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('resets commitMessage when clicking discard button', () => {
|
||||
vm.$store.state.commit.commitMessage = 'testing commit message';
|
||||
it('resets commitMessage when clicking discard button', async () => {
|
||||
setCommitMessageInput('testing commit message');
|
||||
|
||||
return vm
|
||||
.$nextTick()
|
||||
.then(() => {
|
||||
vm.$el.querySelector('.btn-default').click();
|
||||
})
|
||||
.then(() => vm.$nextTick())
|
||||
.then(() => {
|
||||
expect(vm.$store.state.commit.commitMessage).not.toBe('testing commit message');
|
||||
});
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(findCommitMessageInput().props('text')).toBe('testing commit message');
|
||||
|
||||
// Test that commitMessage is cleared on click
|
||||
findDiscardDraftButton().vm.$emit('click');
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(findCommitMessageInput().props('text')).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when submitting', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(vm, 'commitChanges');
|
||||
beforeEach(async () => {
|
||||
goToEditView();
|
||||
|
||||
vm.$store.state.stagedFiles.push('test');
|
||||
vm.$store.state.commit.commitMessage = 'testing commit message';
|
||||
createComponent();
|
||||
|
||||
goToCommitView();
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
setCommitMessageInput('testing commit message');
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
jest.spyOn(store, 'dispatch').mockResolvedValue();
|
||||
});
|
||||
|
||||
it('calls commitChanges', () => {
|
||||
vm.commitChanges.mockResolvedValue({ success: true });
|
||||
findCommitButton().vm.$emit('click');
|
||||
|
||||
return vm.$nextTick().then(() => {
|
||||
vm.$el.querySelector('.btn-success').click();
|
||||
|
||||
expect(vm.commitChanges).toHaveBeenCalled();
|
||||
});
|
||||
expect(store.dispatch).toHaveBeenCalledWith('commit/commitChanges', undefined);
|
||||
});
|
||||
|
||||
it.each`
|
||||
|
@ -272,31 +263,32 @@ describe('IDE commit form', () => {
|
|||
${() => createCodeownersCommitError('test message')} | ${{ actionPrimary: { text: 'Create new branch' } }}
|
||||
${createUnexpectedCommitError} | ${{ actionPrimary: null }}
|
||||
`('opens error modal if commitError with $error', async ({ createError, props }) => {
|
||||
jest.spyOn(vm.$refs.commitErrorModal, 'show');
|
||||
const modal = wrapper.find(GlModal);
|
||||
modal.vm.show = jest.fn();
|
||||
|
||||
const error = createError();
|
||||
store.state.commit.commitError = error;
|
||||
|
||||
await vm.$nextTick();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(vm.$refs.commitErrorModal.show).toHaveBeenCalled();
|
||||
expect(vm.$refs.commitErrorModal).toMatchObject({
|
||||
expect(modal.vm.show).toHaveBeenCalled();
|
||||
expect(modal.props()).toMatchObject({
|
||||
actionCancel: { text: 'Cancel' },
|
||||
...props,
|
||||
});
|
||||
// Because of the legacy 'mountComponent' approach here, the only way to
|
||||
// test the text of the modal is by viewing the content of the modal added to the document.
|
||||
expect(document.body).toHaveText(error.messageHTML);
|
||||
expect(modal.html()).toContain(error.messageHTML);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with error modal with primary', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(vm.$store, 'dispatch').mockReturnValue(Promise.resolve());
|
||||
jest.spyOn(store, 'dispatch').mockResolvedValue();
|
||||
});
|
||||
|
||||
const commitActions = [
|
||||
['commit/updateCommitAction', consts.COMMIT_TO_NEW_BRANCH],
|
||||
['commit/updateCommitAction', COMMIT_TO_NEW_BRANCH],
|
||||
['commit/commitChanges'],
|
||||
];
|
||||
|
||||
|
@ -310,27 +302,15 @@ describe('IDE commit form', () => {
|
|||
async ({ commitError, expectedActions }) => {
|
||||
store.state.commit.commitError = commitError('test message');
|
||||
|
||||
await vm.$nextTick();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
getByText(document.body, 'Create new branch').click();
|
||||
wrapper.find(GlModal).vm.$emit('ok');
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(vm.$store.dispatch.mock.calls).toEqual(expectedActions);
|
||||
expect(store.dispatch.mock.calls).toEqual(expectedActions);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('commitButtonText', () => {
|
||||
it('returns commit text when staged files exist', () => {
|
||||
vm.$store.state.stagedFiles.push('testing');
|
||||
|
||||
expect(vm.commitButtonText).toBe('Commit');
|
||||
});
|
||||
|
||||
it('returns stage & commit text when staged files do not exist', () => {
|
||||
expect(vm.commitButtonText).toBe('Stage & Commit');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,7 +4,10 @@ import { projectData, branches } from 'jest/ide/mock_data';
|
|||
import NewMergeRequestOption from '~/ide/components/commit_sidebar/new_merge_request_option.vue';
|
||||
import { createStore } from '~/ide/stores';
|
||||
import { PERMISSION_CREATE_MR } from '~/ide/constants';
|
||||
import consts from '~/ide/stores/modules/commit/constants';
|
||||
import {
|
||||
COMMIT_TO_CURRENT_BRANCH,
|
||||
COMMIT_TO_NEW_BRANCH,
|
||||
} from '~/ide/stores/modules/commit/constants';
|
||||
|
||||
describe('create new MR checkbox', () => {
|
||||
let store;
|
||||
|
@ -27,8 +30,8 @@ describe('create new MR checkbox', () => {
|
|||
vm = createComponentWithStore(Component, store);
|
||||
|
||||
vm.$store.state.commit.commitAction = createNewBranch
|
||||
? consts.COMMIT_TO_NEW_BRANCH
|
||||
: consts.COMMIT_TO_CURRENT_BRANCH;
|
||||
? COMMIT_TO_NEW_BRANCH
|
||||
: COMMIT_TO_CURRENT_BRANCH;
|
||||
|
||||
vm.$store.state.currentBranchId = currentBranchId;
|
||||
|
||||
|
|
|
@ -7,7 +7,10 @@ import { createStore } from '~/ide/stores';
|
|||
import service from '~/ide/services';
|
||||
import { createRouter } from '~/ide/ide_router';
|
||||
import eventHub from '~/ide/eventhub';
|
||||
import consts from '~/ide/stores/modules/commit/constants';
|
||||
import {
|
||||
COMMIT_TO_CURRENT_BRANCH,
|
||||
COMMIT_TO_NEW_BRANCH,
|
||||
} from '~/ide/stores/modules/commit/constants';
|
||||
import * as mutationTypes from '~/ide/stores/modules/commit/mutation_types';
|
||||
import * as actions from '~/ide/stores/modules/commit/actions';
|
||||
import { createUnexpectedCommitError } from '~/ide/lib/errors';
|
||||
|
@ -425,12 +428,12 @@ describe('IDE commit module actions', () => {
|
|||
});
|
||||
|
||||
it('resets stores commit actions', (done) => {
|
||||
store.state.commit.commitAction = consts.COMMIT_TO_NEW_BRANCH;
|
||||
store.state.commit.commitAction = COMMIT_TO_NEW_BRANCH;
|
||||
|
||||
store
|
||||
.dispatch('commit/commitChanges')
|
||||
.then(() => {
|
||||
expect(store.state.commit.commitAction).not.toBe(consts.COMMIT_TO_NEW_BRANCH);
|
||||
expect(store.state.commit.commitAction).not.toBe(COMMIT_TO_NEW_BRANCH);
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
|
@ -450,7 +453,7 @@ describe('IDE commit module actions', () => {
|
|||
it('redirects to new merge request page', (done) => {
|
||||
jest.spyOn(eventHub, '$on').mockImplementation();
|
||||
|
||||
store.state.commit.commitAction = consts.COMMIT_TO_NEW_BRANCH;
|
||||
store.state.commit.commitAction = COMMIT_TO_NEW_BRANCH;
|
||||
store.state.commit.shouldCreateMR = true;
|
||||
|
||||
store
|
||||
|
@ -468,7 +471,7 @@ describe('IDE commit module actions', () => {
|
|||
it('does not redirect to new merge request page when shouldCreateMR is not checked', (done) => {
|
||||
jest.spyOn(eventHub, '$on').mockImplementation();
|
||||
|
||||
store.state.commit.commitAction = consts.COMMIT_TO_NEW_BRANCH;
|
||||
store.state.commit.commitAction = COMMIT_TO_NEW_BRANCH;
|
||||
store.state.commit.shouldCreateMR = false;
|
||||
|
||||
store
|
||||
|
@ -483,7 +486,7 @@ describe('IDE commit module actions', () => {
|
|||
it('does not redirect to merge request page if shouldCreateMR is checked, but branch is the default branch', async () => {
|
||||
jest.spyOn(eventHub, '$on').mockImplementation();
|
||||
|
||||
store.state.commit.commitAction = consts.COMMIT_TO_CURRENT_BRANCH;
|
||||
store.state.commit.commitAction = COMMIT_TO_CURRENT_BRANCH;
|
||||
store.state.commit.shouldCreateMR = true;
|
||||
|
||||
await store.dispatch('commit/commitChanges');
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import commitState from '~/ide/stores/modules/commit/state';
|
||||
import * as getters from '~/ide/stores/modules/commit/getters';
|
||||
import consts from '~/ide/stores/modules/commit/constants';
|
||||
import {
|
||||
COMMIT_TO_CURRENT_BRANCH,
|
||||
COMMIT_TO_NEW_BRANCH,
|
||||
} from '~/ide/stores/modules/commit/constants';
|
||||
|
||||
describe('IDE commit module getters', () => {
|
||||
let state;
|
||||
|
@ -147,13 +150,13 @@ describe('IDE commit module getters', () => {
|
|||
|
||||
describe('isCreatingNewBranch', () => {
|
||||
it('returns false if NOT creating a new branch', () => {
|
||||
state.commitAction = consts.COMMIT_TO_CURRENT_BRANCH;
|
||||
state.commitAction = COMMIT_TO_CURRENT_BRANCH;
|
||||
|
||||
expect(getters.isCreatingNewBranch(state)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('returns true if creating a new branch', () => {
|
||||
state.commitAction = consts.COMMIT_TO_NEW_BRANCH;
|
||||
state.commitAction = COMMIT_TO_NEW_BRANCH;
|
||||
|
||||
expect(getters.isCreatingNewBranch(state)).toBeTruthy();
|
||||
});
|
||||
|
|
|
@ -277,8 +277,8 @@ RSpec.describe Resolvers::BaseResolver do
|
|||
describe '#offset_pagination' do
|
||||
let(:instance) { resolver_instance(resolver) }
|
||||
|
||||
it 'is sugar for OffsetActiveRecordRelationConnection.new' do
|
||||
expect(instance.offset_pagination(User.none)).to be_a(::Gitlab::Graphql::Pagination::OffsetActiveRecordRelationConnection)
|
||||
it 'is sugar for OffsetPaginatedRelation.new' do
|
||||
expect(instance.offset_pagination(User.none)).to be_a(::Gitlab::Graphql::Pagination::OffsetPaginatedRelation)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -23,19 +23,19 @@ RSpec.describe Resolvers::BoardListIssuesResolver do
|
|||
|
||||
it 'returns the issues in the correct order' do
|
||||
# by relative_position and then ID
|
||||
issues = resolve_board_list_issues.items
|
||||
issues = resolve_board_list_issues
|
||||
|
||||
expect(issues.map(&:id)).to eq [issue3.id, issue1.id, issue2.id]
|
||||
end
|
||||
|
||||
it 'finds only issues matching filters' do
|
||||
result = resolve_board_list_issues(args: { filters: { label_name: [label.title], not: { label_name: [label2.title] } } }).items
|
||||
result = resolve_board_list_issues(args: { filters: { label_name: [label.title], not: { label_name: [label2.title] } } })
|
||||
|
||||
expect(result).to match_array([issue1, issue3])
|
||||
end
|
||||
|
||||
it 'finds only issues matching search param' do
|
||||
result = resolve_board_list_issues(args: { filters: { search: issue1.title } }).items
|
||||
result = resolve_board_list_issues(args: { filters: { search: issue1.title } })
|
||||
|
||||
expect(result).to match_array([issue1])
|
||||
end
|
||||
|
|
|
@ -21,7 +21,7 @@ RSpec.describe Resolvers::BoardListsResolver do
|
|||
end
|
||||
|
||||
it 'does not create the backlog list' do
|
||||
lists = resolve_board_lists.items
|
||||
lists = resolve_board_lists
|
||||
|
||||
expect(lists.count).to eq 1
|
||||
expect(lists[0].list_type).to eq 'closed'
|
||||
|
@ -38,7 +38,7 @@ RSpec.describe Resolvers::BoardListsResolver do
|
|||
let!(:backlog_list) { create(:backlog_list, board: board) }
|
||||
|
||||
it 'returns a list of board lists' do
|
||||
lists = resolve_board_lists.items
|
||||
lists = resolve_board_lists
|
||||
|
||||
expect(lists.count).to eq 3
|
||||
expect(lists.map(&:list_type)).to eq %w(backlog label closed)
|
||||
|
@ -50,7 +50,7 @@ RSpec.describe Resolvers::BoardListsResolver do
|
|||
end
|
||||
|
||||
it 'returns the complete list of board lists for this user' do
|
||||
lists = resolve_board_lists.items
|
||||
lists = resolve_board_lists
|
||||
|
||||
expect(lists.count).to eq 3
|
||||
end
|
||||
|
@ -58,7 +58,7 @@ RSpec.describe Resolvers::BoardListsResolver do
|
|||
|
||||
context 'when querying for a single list' do
|
||||
it 'returns specified list' do
|
||||
list = resolve_board_lists(args: { id: global_id_of(label_list) }).items
|
||||
list = resolve_board_lists(args: { id: global_id_of(label_list) })
|
||||
|
||||
expect(list).to eq [label_list]
|
||||
end
|
||||
|
@ -69,13 +69,13 @@ RSpec.describe Resolvers::BoardListsResolver do
|
|||
external_label = create(:group_label, group: group)
|
||||
external_list = create(:list, board: external_board, label: external_label)
|
||||
|
||||
list = resolve_board_lists(args: { id: global_id_of(external_list) }).items
|
||||
list = resolve_board_lists(args: { id: global_id_of(external_list) })
|
||||
|
||||
expect(list).to eq List.none
|
||||
end
|
||||
|
||||
it 'raises an argument error if list ID is not valid' do
|
||||
expect { resolve_board_lists(args: { id: 'test' }).items }
|
||||
expect { resolve_board_lists(args: { id: 'test' }) }
|
||||
.to raise_error(Gitlab::Graphql::Errors::ArgumentError)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -195,11 +195,11 @@ RSpec.describe Resolvers::IssuesResolver do
|
|||
let_it_be(:priority_issue4) { create(:issue, project: project) }
|
||||
|
||||
it 'sorts issues ascending' do
|
||||
expect(resolve_issues(sort: :priority_asc).items).to eq([priority_issue3, priority_issue1, priority_issue2, priority_issue4])
|
||||
expect(resolve_issues(sort: :priority_asc).to_a).to eq([priority_issue3, priority_issue1, priority_issue2, priority_issue4])
|
||||
end
|
||||
|
||||
it 'sorts issues descending' do
|
||||
expect(resolve_issues(sort: :priority_desc).items).to eq([priority_issue1, priority_issue3, priority_issue2, priority_issue4])
|
||||
expect(resolve_issues(sort: :priority_desc).to_a).to eq([priority_issue1, priority_issue3, priority_issue2, priority_issue4])
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -214,11 +214,11 @@ RSpec.describe Resolvers::IssuesResolver do
|
|||
let_it_be(:label_issue4) { create(:issue, project: project) }
|
||||
|
||||
it 'sorts issues ascending' do
|
||||
expect(resolve_issues(sort: :label_priority_asc).items).to eq([label_issue3, label_issue1, label_issue2, label_issue4])
|
||||
expect(resolve_issues(sort: :label_priority_asc).to_a).to eq([label_issue3, label_issue1, label_issue2, label_issue4])
|
||||
end
|
||||
|
||||
it 'sorts issues descending' do
|
||||
expect(resolve_issues(sort: :label_priority_desc).items).to eq([label_issue2, label_issue3, label_issue1, label_issue4])
|
||||
expect(resolve_issues(sort: :label_priority_desc).to_a).to eq([label_issue2, label_issue3, label_issue1, label_issue4])
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -231,11 +231,11 @@ RSpec.describe Resolvers::IssuesResolver do
|
|||
let_it_be(:milestone_issue3) { create(:issue, project: project, milestone: late_milestone) }
|
||||
|
||||
it 'sorts issues ascending' do
|
||||
expect(resolve_issues(sort: :milestone_due_asc).items).to eq([milestone_issue2, milestone_issue3, milestone_issue1])
|
||||
expect(resolve_issues(sort: :milestone_due_asc).to_a).to eq([milestone_issue2, milestone_issue3, milestone_issue1])
|
||||
end
|
||||
|
||||
it 'sorts issues descending' do
|
||||
expect(resolve_issues(sort: :milestone_due_desc).items).to eq([milestone_issue3, milestone_issue2, milestone_issue1])
|
||||
expect(resolve_issues(sort: :milestone_due_desc).to_a).to eq([milestone_issue3, milestone_issue2, milestone_issue1])
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe Resolvers::MergeRequestsResolver do
|
||||
include GraphqlHelpers
|
||||
include SortingHelper
|
||||
|
||||
let_it_be(:project) { create(:project, :repository) }
|
||||
let_it_be(:milestone) { create(:milestone, project: project) }
|
||||
|
@ -30,6 +31,16 @@ RSpec.describe Resolvers::MergeRequestsResolver do
|
|||
end
|
||||
|
||||
describe '#resolve' do
|
||||
# One for the initial auth, then MRs, and the load of project and project_feature (for further auth):
|
||||
# SELECT MAX("project_authorizations"."access_level") AS maximum_access_level,
|
||||
# "project_authorizations"."user_id" AS project_authorizations_user_id
|
||||
# FROM "project_authorizations"
|
||||
# WHERE "project_authorizations"."project_id" = 2 AND "project_authorizations"."user_id" = 2
|
||||
# GROUP BY "project_authorizations"."user_id"
|
||||
# SELECT "merge_requests".* FROM "merge_requests" WHERE "merge_requests"."target_project_id" = 2
|
||||
# AND "merge_requests"."iid" = 1 ORDER BY "merge_requests"."id" DESC
|
||||
# SELECT "projects".* FROM "projects" WHERE "projects"."id" = 2
|
||||
# SELECT "project_features".* FROM "project_features" WHERE "project_features"."project_id" = 2
|
||||
let(:queries_per_project) { 3 }
|
||||
|
||||
context 'no arguments' do
|
||||
|
@ -72,15 +83,17 @@ RSpec.describe Resolvers::MergeRequestsResolver do
|
|||
expect(result).to contain_exactly(merge_request_1, merge_request_2, merge_request_3)
|
||||
end
|
||||
|
||||
it 'can batch-resolve merge requests from different projects' do
|
||||
it 'can batch-resolve merge requests from different projects', :request_store, :use_clean_rails_memory_store_caching do
|
||||
# 2 queries for project_authorizations, and 2 for merge_requests
|
||||
result = batch_sync(max_queries: queries_per_project * 2) do
|
||||
resolve_mr(project, iids: [iid_1]) +
|
||||
resolve_mr(project, iids: [iid_2]) +
|
||||
resolve_mr(other_project, iids: [other_iid])
|
||||
results = batch_sync(max_queries: queries_per_project * 2) do
|
||||
a = resolve_mr(project, iids: [iid_1])
|
||||
b = resolve_mr(project, iids: [iid_2])
|
||||
c = resolve_mr(other_project, iids: [other_iid])
|
||||
|
||||
[a, b, c].flat_map(&:to_a)
|
||||
end
|
||||
|
||||
expect(result).to contain_exactly(merge_request_1, merge_request_2, other_merge_request)
|
||||
expect(results).to contain_exactly(merge_request_1, merge_request_2, other_merge_request)
|
||||
end
|
||||
|
||||
it 'resolves an unknown iid to be empty' do
|
||||
|
@ -134,9 +147,9 @@ RSpec.describe Resolvers::MergeRequestsResolver do
|
|||
it 'takes more than one argument' do
|
||||
mrs = [merge_request_3, merge_request_4]
|
||||
branches = mrs.map(&:target_branch)
|
||||
result = resolve_mr(project, target_branches: branches )
|
||||
result = resolve_mr(project, target_branches: branches)
|
||||
|
||||
expect(result.compact).to match_array(mrs)
|
||||
expect(result).to match_array(mrs)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -173,7 +186,7 @@ RSpec.describe Resolvers::MergeRequestsResolver do
|
|||
it 'returns merge requests merged between the given period' do
|
||||
result = resolve_mr(project, merged_after: 20.days.ago, merged_before: 5.days.ago)
|
||||
|
||||
expect(result).to eq([merge_request_1])
|
||||
expect(result).to contain_exactly(merge_request_1)
|
||||
end
|
||||
|
||||
it 'does not return anything' do
|
||||
|
@ -187,7 +200,7 @@ RSpec.describe Resolvers::MergeRequestsResolver do
|
|||
it 'filters merge requests by milestone title' do
|
||||
result = resolve_mr(project, milestone_title: milestone.title)
|
||||
|
||||
expect(result).to eq([merge_request_with_milestone])
|
||||
expect(result).to contain_exactly(merge_request_with_milestone)
|
||||
end
|
||||
|
||||
it 'does not find anything' do
|
||||
|
@ -203,18 +216,29 @@ RSpec.describe Resolvers::MergeRequestsResolver do
|
|||
|
||||
result = resolve_mr(project, source_branches: [merge_request_4.source_branch], state: 'locked')
|
||||
|
||||
expect(result.compact).to contain_exactly(merge_request_4)
|
||||
expect(result).to contain_exactly(merge_request_4)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'sorting' do
|
||||
let(:mrs) do
|
||||
[
|
||||
merge_request_with_milestone, merge_request_6, merge_request_5, merge_request_4,
|
||||
merge_request_3, merge_request_2, merge_request_1
|
||||
]
|
||||
end
|
||||
|
||||
context 'when sorting by created' do
|
||||
it 'sorts merge requests ascending' do
|
||||
expect(resolve_mr(project, sort: 'created_asc')).to eq [merge_request_1, merge_request_2, merge_request_3, merge_request_4, merge_request_5, merge_request_6, merge_request_with_milestone]
|
||||
expect(resolve_mr(project, sort: 'created_asc'))
|
||||
.to match_array(mrs)
|
||||
.and be_sorted(:created_at, :asc)
|
||||
end
|
||||
|
||||
it 'sorts merge requests descending' do
|
||||
expect(resolve_mr(project, sort: 'created_desc')).to eq [merge_request_with_milestone, merge_request_6, merge_request_5, merge_request_4, merge_request_3, merge_request_2, merge_request_1]
|
||||
expect(resolve_mr(project, sort: 'created_desc'))
|
||||
.to match_array(mrs)
|
||||
.and be_sorted(:created_at, :desc)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -225,11 +249,19 @@ RSpec.describe Resolvers::MergeRequestsResolver do
|
|||
end
|
||||
|
||||
it 'sorts merge requests ascending' do
|
||||
expect(resolve_mr(project, sort: :merged_at_asc)).to eq [merge_request_1, merge_request_3, merge_request_with_milestone, merge_request_6, merge_request_5, merge_request_4, merge_request_2]
|
||||
expect(resolve_mr(project, sort: :merged_at_asc))
|
||||
.to match_array(mrs)
|
||||
.and be_sorted(->(mr) { [merged_at(mr), -mr.id] })
|
||||
end
|
||||
|
||||
it 'sorts merge requests descending' do
|
||||
expect(resolve_mr(project, sort: :merged_at_desc)).to eq [merge_request_3, merge_request_1, merge_request_with_milestone, merge_request_6, merge_request_5, merge_request_4, merge_request_2]
|
||||
expect(resolve_mr(project, sort: :merged_at_desc))
|
||||
.to match_array(mrs)
|
||||
.and be_sorted(->(mr) { [-merged_at(mr), -mr.id] })
|
||||
end
|
||||
|
||||
def merged_at(mr)
|
||||
nils_last(mr.metrics.merged_at)
|
||||
end
|
||||
|
||||
context 'when label filter is given and the optimized_issuable_label_filter feature flag is off' do
|
||||
|
|
|
@ -12,12 +12,12 @@ RSpec.describe Resolvers::ReleaseMilestonesResolver do
|
|||
end
|
||||
|
||||
describe '#resolve' do
|
||||
it "returns an OffsetActiveRecordRelationConnection" do
|
||||
expect(resolved).to be_a(::Gitlab::Graphql::Pagination::OffsetActiveRecordRelationConnection)
|
||||
it "uses offset-pagination" do
|
||||
expect(resolved).to be_a(::Gitlab::Graphql::Pagination::OffsetPaginatedRelation)
|
||||
end
|
||||
|
||||
it "includes the release's milestones in the returned OffsetActiveRecordRelationConnection" do
|
||||
expect(resolved.items).to eq(release.milestones.order_by_dates_and_title)
|
||||
expect(resolved.to_a).to eq(release.milestones.order_by_dates_and_title)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
# Tests that our connections are correctly mapped.
|
||||
RSpec.describe ::Gitlab::Graphql::Pagination::Connections do
|
||||
include GraphqlHelpers
|
||||
|
||||
before(:all) do
|
||||
ActiveRecord::Schema.define do
|
||||
create_table :testing_pagination_nodes, force: true do |t|
|
||||
t.integer :value, null: false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
after(:all) do
|
||||
ActiveRecord::Schema.define do
|
||||
drop_table :testing_pagination_nodes, force: true
|
||||
end
|
||||
end
|
||||
|
||||
let_it_be(:node_model) do
|
||||
Class.new(ActiveRecord::Base) do
|
||||
self.table_name = 'testing_pagination_nodes'
|
||||
end
|
||||
end
|
||||
|
||||
let(:query_string) { 'query { items(first: 2) { nodes { value } } }' }
|
||||
let(:user) { nil }
|
||||
|
||||
let(:node) { Struct.new(:value) }
|
||||
let(:node_type) do
|
||||
Class.new(::GraphQL::Schema::Object) do
|
||||
graphql_name 'Node'
|
||||
field :value, GraphQL::INT_TYPE, null: false
|
||||
end
|
||||
end
|
||||
|
||||
let(:query_type) do
|
||||
item_values = nodes
|
||||
|
||||
query_factory do |t|
|
||||
t.field :items, node_type.connection_type, null: true
|
||||
|
||||
t.define_method :items do
|
||||
item_values
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'it maps to a specific connection class' do |connection_type|
|
||||
let(:raw_values) { [1, 7, 42] }
|
||||
|
||||
it "maps to #{connection_type.name}" do
|
||||
expect(connection_type).to receive(:new).and_call_original
|
||||
|
||||
results = execute_query(query_type).to_h
|
||||
|
||||
expect(graphql_dig_at(results, :data, :items, :nodes, :value)).to eq [1, 7]
|
||||
end
|
||||
end
|
||||
|
||||
describe 'OffsetPaginatedRelation' do
|
||||
before do
|
||||
# Expect to be ordered by an explicit ordering.
|
||||
raw_values.each_with_index { |value, id| node_model.create!(id: id, value: value) }
|
||||
end
|
||||
|
||||
let(:nodes) { ::Gitlab::Graphql::Pagination::OffsetPaginatedRelation.new(node_model.order(value: :asc)) }
|
||||
|
||||
include_examples 'it maps to a specific connection class', Gitlab::Graphql::Pagination::OffsetActiveRecordRelationConnection
|
||||
end
|
||||
|
||||
describe 'ActiveRecord::Relation' do
|
||||
before do
|
||||
# Expect to be ordered by ID descending
|
||||
[3, 2, 1].zip(raw_values) { |id, value| node_model.create!(id: id, value: value) }
|
||||
end
|
||||
|
||||
let(:nodes) { node_model.all }
|
||||
|
||||
include_examples 'it maps to a specific connection class', Gitlab::Graphql::Pagination::Keyset::Connection
|
||||
end
|
||||
|
||||
describe 'ExternallyPaginatedArray' do
|
||||
let(:nodes) { ::Gitlab::Graphql::ExternallyPaginatedArray.new(nil, nil, node.new(1), node.new(7)) }
|
||||
|
||||
include_examples 'it maps to a specific connection class', Gitlab::Graphql::Pagination::ExternallyPaginatedArrayConnection
|
||||
end
|
||||
|
||||
describe 'Array' do
|
||||
let(:nodes) { raw_values.map { |x| node.new(x) } }
|
||||
|
||||
include_examples 'it maps to a specific connection class', Gitlab::Graphql::Pagination::ArrayConnection
|
||||
end
|
||||
end
|
|
@ -6,7 +6,7 @@ require 'parser/current'
|
|||
require_relative '../../rubocop/code_reuse_helpers'
|
||||
|
||||
RSpec.describe RuboCop::CodeReuseHelpers do
|
||||
def parse_source(source, path = 'foo.rb')
|
||||
def build_and_parse_source(source, path = 'foo.rb')
|
||||
buffer = Parser::Source::Buffer.new(path)
|
||||
buffer.source = source
|
||||
|
||||
|
@ -24,13 +24,13 @@ RSpec.describe RuboCop::CodeReuseHelpers do
|
|||
|
||||
describe '#send_to_constant?' do
|
||||
it 'returns true when sending to a constant' do
|
||||
node = parse_source('Foo.bar')
|
||||
node = build_and_parse_source('Foo.bar')
|
||||
|
||||
expect(cop.send_to_constant?(node)).to eq(true)
|
||||
end
|
||||
|
||||
it 'returns false when sending to something other than a constant' do
|
||||
node = parse_source('10')
|
||||
node = build_and_parse_source('10')
|
||||
|
||||
expect(cop.send_to_constant?(node)).to eq(false)
|
||||
end
|
||||
|
@ -38,13 +38,13 @@ RSpec.describe RuboCop::CodeReuseHelpers do
|
|||
|
||||
describe '#send_receiver_name_ends_with?' do
|
||||
it 'returns true when the receiver ends with a suffix' do
|
||||
node = parse_source('FooFinder.new')
|
||||
node = build_and_parse_source('FooFinder.new')
|
||||
|
||||
expect(cop.send_receiver_name_ends_with?(node, 'Finder')).to eq(true)
|
||||
end
|
||||
|
||||
it 'returns false when the receiver is the same as a suffix' do
|
||||
node = parse_source('Finder.new')
|
||||
node = build_and_parse_source('Finder.new')
|
||||
|
||||
expect(cop.send_receiver_name_ends_with?(node, 'Finder')).to eq(false)
|
||||
end
|
||||
|
@ -52,7 +52,7 @@ RSpec.describe RuboCop::CodeReuseHelpers do
|
|||
|
||||
describe '#file_path_for_node' do
|
||||
it 'returns the file path of a node' do
|
||||
node = parse_source('10')
|
||||
node = build_and_parse_source('10')
|
||||
path = cop.file_path_for_node(node)
|
||||
|
||||
expect(path).to eq('foo.rb')
|
||||
|
@ -61,7 +61,7 @@ RSpec.describe RuboCop::CodeReuseHelpers do
|
|||
|
||||
describe '#name_of_constant' do
|
||||
it 'returns the name of a constant' do
|
||||
node = parse_source('Foo')
|
||||
node = build_and_parse_source('Foo')
|
||||
|
||||
expect(cop.name_of_constant(node)).to eq(:Foo)
|
||||
end
|
||||
|
@ -69,13 +69,13 @@ RSpec.describe RuboCop::CodeReuseHelpers do
|
|||
|
||||
describe '#in_finder?' do
|
||||
it 'returns true for a node in the finders directory' do
|
||||
node = parse_source('10', rails_root_join('app', 'finders', 'foo.rb'))
|
||||
node = build_and_parse_source('10', rails_root_join('app', 'finders', 'foo.rb'))
|
||||
|
||||
expect(cop.in_finder?(node)).to eq(true)
|
||||
end
|
||||
|
||||
it 'returns false for a node outside the finders directory' do
|
||||
node = parse_source('10', rails_root_join('app', 'foo', 'foo.rb'))
|
||||
node = build_and_parse_source('10', rails_root_join('app', 'foo', 'foo.rb'))
|
||||
|
||||
expect(cop.in_finder?(node)).to eq(false)
|
||||
end
|
||||
|
@ -83,13 +83,13 @@ RSpec.describe RuboCop::CodeReuseHelpers do
|
|||
|
||||
describe '#in_model?' do
|
||||
it 'returns true for a node in the models directory' do
|
||||
node = parse_source('10', rails_root_join('app', 'models', 'foo.rb'))
|
||||
node = build_and_parse_source('10', rails_root_join('app', 'models', 'foo.rb'))
|
||||
|
||||
expect(cop.in_model?(node)).to eq(true)
|
||||
end
|
||||
|
||||
it 'returns false for a node outside the models directory' do
|
||||
node = parse_source('10', rails_root_join('app', 'foo', 'foo.rb'))
|
||||
node = build_and_parse_source('10', rails_root_join('app', 'foo', 'foo.rb'))
|
||||
|
||||
expect(cop.in_model?(node)).to eq(false)
|
||||
end
|
||||
|
@ -97,13 +97,13 @@ RSpec.describe RuboCop::CodeReuseHelpers do
|
|||
|
||||
describe '#in_service_class?' do
|
||||
it 'returns true for a node in the services directory' do
|
||||
node = parse_source('10', rails_root_join('app', 'services', 'foo.rb'))
|
||||
node = build_and_parse_source('10', rails_root_join('app', 'services', 'foo.rb'))
|
||||
|
||||
expect(cop.in_service_class?(node)).to eq(true)
|
||||
end
|
||||
|
||||
it 'returns false for a node outside the services directory' do
|
||||
node = parse_source('10', rails_root_join('app', 'foo', 'foo.rb'))
|
||||
node = build_and_parse_source('10', rails_root_join('app', 'foo', 'foo.rb'))
|
||||
|
||||
expect(cop.in_service_class?(node)).to eq(false)
|
||||
end
|
||||
|
@ -111,13 +111,13 @@ RSpec.describe RuboCop::CodeReuseHelpers do
|
|||
|
||||
describe '#in_presenter?' do
|
||||
it 'returns true for a node in the presenters directory' do
|
||||
node = parse_source('10', rails_root_join('app', 'presenters', 'foo.rb'))
|
||||
node = build_and_parse_source('10', rails_root_join('app', 'presenters', 'foo.rb'))
|
||||
|
||||
expect(cop.in_presenter?(node)).to eq(true)
|
||||
end
|
||||
|
||||
it 'returns false for a node outside the presenters directory' do
|
||||
node = parse_source('10', rails_root_join('app', 'foo', 'foo.rb'))
|
||||
node = build_and_parse_source('10', rails_root_join('app', 'foo', 'foo.rb'))
|
||||
|
||||
expect(cop.in_presenter?(node)).to eq(false)
|
||||
end
|
||||
|
@ -125,13 +125,13 @@ RSpec.describe RuboCop::CodeReuseHelpers do
|
|||
|
||||
describe '#in_serializer?' do
|
||||
it 'returns true for a node in the serializers directory' do
|
||||
node = parse_source('10', rails_root_join('app', 'serializers', 'foo.rb'))
|
||||
node = build_and_parse_source('10', rails_root_join('app', 'serializers', 'foo.rb'))
|
||||
|
||||
expect(cop.in_serializer?(node)).to eq(true)
|
||||
end
|
||||
|
||||
it 'returns false for a node outside the serializers directory' do
|
||||
node = parse_source('10', rails_root_join('app', 'foo', 'foo.rb'))
|
||||
node = build_and_parse_source('10', rails_root_join('app', 'foo', 'foo.rb'))
|
||||
|
||||
expect(cop.in_serializer?(node)).to eq(false)
|
||||
end
|
||||
|
@ -139,13 +139,13 @@ RSpec.describe RuboCop::CodeReuseHelpers do
|
|||
|
||||
describe '#in_worker?' do
|
||||
it 'returns true for a node in the workers directory' do
|
||||
node = parse_source('10', rails_root_join('app', 'workers', 'foo.rb'))
|
||||
node = build_and_parse_source('10', rails_root_join('app', 'workers', 'foo.rb'))
|
||||
|
||||
expect(cop.in_worker?(node)).to eq(true)
|
||||
end
|
||||
|
||||
it 'returns false for a node outside the workers directory' do
|
||||
node = parse_source('10', rails_root_join('app', 'foo', 'foo.rb'))
|
||||
node = build_and_parse_source('10', rails_root_join('app', 'foo', 'foo.rb'))
|
||||
|
||||
expect(cop.in_worker?(node)).to eq(false)
|
||||
end
|
||||
|
@ -153,13 +153,13 @@ RSpec.describe RuboCop::CodeReuseHelpers do
|
|||
|
||||
describe '#in_api?' do
|
||||
it 'returns true for a node in the API directory' do
|
||||
node = parse_source('10', rails_root_join('lib', 'api', 'foo.rb'))
|
||||
node = build_and_parse_source('10', rails_root_join('lib', 'api', 'foo.rb'))
|
||||
|
||||
expect(cop.in_api?(node)).to eq(true)
|
||||
end
|
||||
|
||||
it 'returns false for a node outside the API directory' do
|
||||
node = parse_source('10', rails_root_join('lib', 'foo', 'foo.rb'))
|
||||
node = build_and_parse_source('10', rails_root_join('lib', 'foo', 'foo.rb'))
|
||||
|
||||
expect(cop.in_api?(node)).to eq(false)
|
||||
end
|
||||
|
@ -167,21 +167,21 @@ RSpec.describe RuboCop::CodeReuseHelpers do
|
|||
|
||||
describe '#in_directory?' do
|
||||
it 'returns true for a directory in the CE app/ directory' do
|
||||
node = parse_source('10', rails_root_join('app', 'models', 'foo.rb'))
|
||||
node = build_and_parse_source('10', rails_root_join('app', 'models', 'foo.rb'))
|
||||
|
||||
expect(cop.in_directory?(node, 'models')).to eq(true)
|
||||
end
|
||||
|
||||
it 'returns true for a directory in the EE app/ directory' do
|
||||
node =
|
||||
parse_source('10', rails_root_join('ee', 'app', 'models', 'foo.rb'))
|
||||
build_and_parse_source('10', rails_root_join('ee', 'app', 'models', 'foo.rb'))
|
||||
|
||||
expect(cop.in_directory?(node, 'models')).to eq(true)
|
||||
end
|
||||
|
||||
it 'returns false for a directory in the lib/ directory' do
|
||||
node =
|
||||
parse_source('10', rails_root_join('lib', 'models', 'foo.rb'))
|
||||
build_and_parse_source('10', rails_root_join('lib', 'models', 'foo.rb'))
|
||||
|
||||
expect(cop.in_directory?(node, 'models')).to eq(false)
|
||||
end
|
||||
|
@ -189,7 +189,7 @@ RSpec.describe RuboCop::CodeReuseHelpers do
|
|||
|
||||
describe '#name_of_receiver' do
|
||||
it 'returns the name of a send receiver' do
|
||||
node = parse_source('Foo.bar')
|
||||
node = build_and_parse_source('Foo.bar')
|
||||
|
||||
expect(cop.name_of_receiver(node)).to eq('Foo')
|
||||
end
|
||||
|
@ -197,7 +197,7 @@ RSpec.describe RuboCop::CodeReuseHelpers do
|
|||
|
||||
describe '#each_class_method' do
|
||||
it 'yields every class method to the supplied block' do
|
||||
node = parse_source(<<~RUBY)
|
||||
node = build_and_parse_source(<<~RUBY)
|
||||
class Foo
|
||||
class << self
|
||||
def first
|
||||
|
@ -220,7 +220,7 @@ RSpec.describe RuboCop::CodeReuseHelpers do
|
|||
|
||||
describe '#each_send_node' do
|
||||
it 'yields every send node to the supplied block' do
|
||||
node = parse_source("foo\nbar")
|
||||
node = build_and_parse_source("foo\nbar")
|
||||
nodes = cop.each_send_node(node).to_a
|
||||
|
||||
expect(nodes.length).to eq(2)
|
||||
|
@ -231,7 +231,7 @@ RSpec.describe RuboCop::CodeReuseHelpers do
|
|||
|
||||
describe '#disallow_send_to' do
|
||||
it 'disallows sending a message to a constant' do
|
||||
def_node = parse_source(<<~RUBY)
|
||||
def_node = build_and_parse_source(<<~RUBY)
|
||||
def foo
|
||||
FooFinder.new
|
||||
end
|
||||
|
|
|
@ -2,12 +2,9 @@
|
|||
|
||||
require 'fast_spec_helper'
|
||||
require 'rubocop'
|
||||
require 'rubocop/rspec/support'
|
||||
require_relative '../../../../rubocop/cop/api/base'
|
||||
|
||||
RSpec.describe RuboCop::Cop::API::Base do
|
||||
include CopHelper
|
||||
|
||||
subject(:cop) { described_class.new }
|
||||
|
||||
let(:corrected) do
|
||||
|
@ -17,7 +14,7 @@ RSpec.describe RuboCop::Cop::API::Base do
|
|||
CORRECTED
|
||||
end
|
||||
|
||||
['Grape::API', '::Grape::API', 'Grape::API::Instance', '::Grape::API::Instance'].each do |offense|
|
||||
%w[Grape::API ::Grape::API Grape::API::Instance ::Grape::API::Instance].each do |offense|
|
||||
it "adds an offense when inheriting from #{offense}" do
|
||||
expect_offense(<<~CODE)
|
||||
class SomeAPI < #{offense}
|
||||
|
|
|
@ -5,36 +5,38 @@ require 'rubocop'
|
|||
require_relative '../../../../rubocop/cop/api/grape_array_missing_coerce'
|
||||
|
||||
RSpec.describe RuboCop::Cop::API::GrapeArrayMissingCoerce do
|
||||
include CopHelper
|
||||
let(:msg) do
|
||||
"This Grape parameter defines an Array but is missing a coerce_with definition. " \
|
||||
"For more details, see " \
|
||||
"https://github.com/ruby-grape/grape/blob/master/UPGRADING.md#ensure-that-array-types-have-explicit-coercions"
|
||||
end
|
||||
|
||||
subject(:cop) { described_class.new }
|
||||
|
||||
it 'adds an offense with a required parameter' do
|
||||
inspect_source(<<~CODE)
|
||||
expect_offense(<<~TYPE)
|
||||
class SomeAPI < Grape::API::Instance
|
||||
params do
|
||||
requires :values, type: Array[String]
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg}
|
||||
end
|
||||
end
|
||||
CODE
|
||||
|
||||
expect(cop.offenses.size).to eq(1)
|
||||
TYPE
|
||||
end
|
||||
|
||||
it 'adds an offense with an optional parameter' do
|
||||
inspect_source(<<~CODE)
|
||||
expect_offense(<<~TYPE)
|
||||
class SomeAPI < Grape::API::Instance
|
||||
params do
|
||||
optional :values, type: Array[String]
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg}
|
||||
end
|
||||
end
|
||||
CODE
|
||||
|
||||
expect(cop.offenses.size).to eq(1)
|
||||
TYPE
|
||||
end
|
||||
|
||||
it 'does not add an offense' do
|
||||
inspect_source(<<~CODE)
|
||||
expect_no_offenses(<<~CODE)
|
||||
class SomeAPI < Grape::API::Instance
|
||||
params do
|
||||
requires :values, type: Array[String], coerce_with: ->(val) { val.split(',').map(&:strip) }
|
||||
|
@ -44,19 +46,15 @@ RSpec.describe RuboCop::Cop::API::GrapeArrayMissingCoerce do
|
|||
end
|
||||
end
|
||||
CODE
|
||||
|
||||
expect(cop.offenses.size).to be_zero
|
||||
end
|
||||
|
||||
it 'does not add an offense for unrelated classes' do
|
||||
inspect_source(<<~CODE)
|
||||
expect_no_offenses(<<~CODE)
|
||||
class SomeClass
|
||||
params do
|
||||
requires :values, type: Array[String]
|
||||
end
|
||||
end
|
||||
CODE
|
||||
|
||||
expect(cop.offenses.size).to be_zero
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,12 +2,9 @@
|
|||
|
||||
require 'fast_spec_helper'
|
||||
require 'rubocop'
|
||||
require 'rubocop/rspec/support'
|
||||
require_relative '../../../../rubocop/cop/code_reuse/finder'
|
||||
|
||||
RSpec.describe RuboCop::Cop::CodeReuse::Finder do
|
||||
include CopHelper
|
||||
|
||||
subject(:cop) { described_class.new }
|
||||
|
||||
it 'flags the use of a Finder inside another Finder' do
|
||||
|
@ -23,8 +20,6 @@ RSpec.describe RuboCop::Cop::CodeReuse::Finder do
|
|||
end
|
||||
end
|
||||
SOURCE
|
||||
|
||||
expect(cop.offenses.size).to eq(1)
|
||||
end
|
||||
|
||||
it 'flags the use of a Finder inside a model class method' do
|
||||
|
|
|
@ -2,12 +2,9 @@
|
|||
|
||||
require 'fast_spec_helper'
|
||||
require 'rubocop'
|
||||
require 'rubocop/rspec/support'
|
||||
require_relative '../../../../rubocop/cop/code_reuse/presenter'
|
||||
|
||||
RSpec.describe RuboCop::Cop::CodeReuse::Presenter do
|
||||
include CopHelper
|
||||
|
||||
subject(:cop) { described_class.new }
|
||||
|
||||
it 'flags the use of a Presenter in a Service class' do
|
||||
|
|
|
@ -2,12 +2,9 @@
|
|||
|
||||
require 'fast_spec_helper'
|
||||
require 'rubocop'
|
||||
require 'rubocop/rspec/support'
|
||||
require_relative '../../../../rubocop/cop/code_reuse/serializer'
|
||||
|
||||
RSpec.describe RuboCop::Cop::CodeReuse::Serializer do
|
||||
include CopHelper
|
||||
|
||||
subject(:cop) { described_class.new }
|
||||
|
||||
it 'flags the use of a Serializer in a Service class' do
|
||||
|
|
|
@ -2,12 +2,9 @@
|
|||
|
||||
require 'fast_spec_helper'
|
||||
require 'rubocop'
|
||||
require 'rubocop/rspec/support'
|
||||
require_relative '../../../../rubocop/cop/code_reuse/service_class'
|
||||
|
||||
RSpec.describe RuboCop::Cop::CodeReuse::ServiceClass do
|
||||
include CopHelper
|
||||
|
||||
subject(:cop) { described_class.new }
|
||||
|
||||
it 'flags the use of a Service class in a Finder' do
|
||||
|
|
|
@ -2,12 +2,9 @@
|
|||
|
||||
require 'fast_spec_helper'
|
||||
require 'rubocop'
|
||||
require 'rubocop/rspec/support'
|
||||
require_relative '../../../../rubocop/cop/code_reuse/worker'
|
||||
|
||||
RSpec.describe RuboCop::Cop::CodeReuse::Worker do
|
||||
include CopHelper
|
||||
|
||||
subject(:cop) { described_class.new }
|
||||
|
||||
it 'flags the use of a worker in a controller' do
|
||||
|
|
|
@ -6,21 +6,18 @@ require 'rubocop'
|
|||
require_relative '../../../../rubocop/cop/graphql/authorize_types'
|
||||
|
||||
RSpec.describe RuboCop::Cop::Graphql::AuthorizeTypes do
|
||||
include CopHelper
|
||||
|
||||
subject(:cop) { described_class.new }
|
||||
|
||||
it 'adds an offense when there is no authorize call' do
|
||||
inspect_source(<<~TYPE)
|
||||
expect_offense(<<~TYPE)
|
||||
module Types
|
||||
class AType < BaseObject
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^ Add an `authorize :ability` call to the type: https://docs.gitlab.com/ee/development/api_graphql_styleguide.html#type-authorization
|
||||
field :a_thing
|
||||
field :another_thing
|
||||
end
|
||||
end
|
||||
TYPE
|
||||
|
||||
expect(cop.offenses.size).to eq 1
|
||||
end
|
||||
|
||||
it 'does not add an offense for classes that have an authorize call' do
|
||||
|
|
|
@ -5,38 +5,34 @@ require 'rubocop'
|
|||
require_relative '../../../../rubocop/cop/graphql/descriptions'
|
||||
|
||||
RSpec.describe RuboCop::Cop::Graphql::Descriptions do
|
||||
include CopHelper
|
||||
|
||||
subject(:cop) { described_class.new }
|
||||
|
||||
context 'fields' do
|
||||
it 'adds an offense when there is no description' do
|
||||
inspect_source(<<~TYPE)
|
||||
expect_offense(<<~TYPE)
|
||||
module Types
|
||||
class FakeType < BaseObject
|
||||
field :a_thing,
|
||||
^^^^^^^^^^^^^^^ Please add a `description` property.
|
||||
GraphQL::STRING_TYPE,
|
||||
null: false
|
||||
end
|
||||
end
|
||||
TYPE
|
||||
|
||||
expect(cop.offenses.size).to eq 1
|
||||
end
|
||||
|
||||
it 'adds an offense when description does not end in a period' do
|
||||
inspect_source(<<~TYPE)
|
||||
expect_offense(<<~TYPE)
|
||||
module Types
|
||||
class FakeType < BaseObject
|
||||
field :a_thing,
|
||||
^^^^^^^^^^^^^^^ `description` strings must end with a `.`.
|
||||
GraphQL::STRING_TYPE,
|
||||
null: false,
|
||||
description: 'A descriptive description'
|
||||
end
|
||||
end
|
||||
TYPE
|
||||
|
||||
expect(cop.offenses.size).to eq 1
|
||||
end
|
||||
|
||||
it 'does not add an offense when description is correct' do
|
||||
|
@ -55,32 +51,30 @@ RSpec.describe RuboCop::Cop::Graphql::Descriptions do
|
|||
|
||||
context 'arguments' do
|
||||
it 'adds an offense when there is no description' do
|
||||
inspect_source(<<~TYPE)
|
||||
expect_offense(<<~TYPE)
|
||||
module Types
|
||||
class FakeType < BaseObject
|
||||
argument :a_thing,
|
||||
^^^^^^^^^^^^^^^^^^ Please add a `description` property.
|
||||
GraphQL::STRING_TYPE,
|
||||
null: false
|
||||
end
|
||||
end
|
||||
TYPE
|
||||
|
||||
expect(cop.offenses.size).to eq 1
|
||||
end
|
||||
|
||||
it 'adds an offense when description does not end in a period' do
|
||||
inspect_source(<<~TYPE)
|
||||
expect_offense(<<~TYPE)
|
||||
module Types
|
||||
class FakeType < BaseObject
|
||||
argument :a_thing,
|
||||
^^^^^^^^^^^^^^^^^^ `description` strings must end with a `.`.
|
||||
GraphQL::STRING_TYPE,
|
||||
null: false,
|
||||
description: 'Behold! A description'
|
||||
end
|
||||
end
|
||||
TYPE
|
||||
|
||||
expect(cop.offenses.size).to eq 1
|
||||
end
|
||||
|
||||
it 'does not add an offense when description is correct' do
|
||||
|
|
|
@ -6,16 +6,13 @@ require 'rubocop'
|
|||
require_relative '../../../../rubocop/cop/graphql/gid_expected_type'
|
||||
|
||||
RSpec.describe RuboCop::Cop::Graphql::GIDExpectedType do
|
||||
include CopHelper
|
||||
|
||||
subject(:cop) { described_class.new }
|
||||
|
||||
it 'adds an offense when there is no expected_type parameter' do
|
||||
inspect_source(<<~TYPE)
|
||||
expect_offense(<<~TYPE)
|
||||
GitlabSchema.object_from_id(received_id)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Add an expected_type parameter to #object_from_id calls if possible.
|
||||
TYPE
|
||||
|
||||
expect(cop.offenses.size).to eq 1
|
||||
end
|
||||
|
||||
it 'does not add an offense for calls that have an expected_type parameter' do
|
||||
|
|
|
@ -6,16 +6,13 @@ require 'rubocop'
|
|||
require_relative '../../../../rubocop/cop/graphql/id_type'
|
||||
|
||||
RSpec.describe RuboCop::Cop::Graphql::IDType do
|
||||
include CopHelper
|
||||
|
||||
subject(:cop) { described_class.new }
|
||||
|
||||
it 'adds an offense when GraphQL::ID_TYPE is used as a param to #argument' do
|
||||
inspect_source(<<~TYPE)
|
||||
expect_offense(<<~TYPE)
|
||||
argument :some_arg, GraphQL::ID_TYPE, some: other, params: do_not_matter
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Do not use GraphQL::ID_TYPE, use a specific GlobalIDType instead
|
||||
TYPE
|
||||
|
||||
expect(cop.offenses.size).to eq 1
|
||||
end
|
||||
|
||||
context 'whitelisted arguments' do
|
||||
|
|
|
@ -5,29 +5,29 @@ require 'rubocop'
|
|||
require_relative '../../../../rubocop/cop/graphql/json_type'
|
||||
|
||||
RSpec.describe RuboCop::Cop::Graphql::JSONType do
|
||||
include CopHelper
|
||||
let(:msg) do
|
||||
'Avoid using GraphQL::Types::JSON. See: https://docs.gitlab.com/ee/development/api_graphql_styleguide.html#json'
|
||||
end
|
||||
|
||||
subject(:cop) { described_class.new }
|
||||
|
||||
context 'fields' do
|
||||
it 'adds an offense when GraphQL::Types::JSON is used' do
|
||||
inspect_source(<<~RUBY.strip)
|
||||
expect_offense(<<~RUBY)
|
||||
class MyType
|
||||
field :some_field, GraphQL::Types::JSON
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg}
|
||||
end
|
||||
RUBY
|
||||
|
||||
expect(cop.offenses.size).to eq(1)
|
||||
end
|
||||
|
||||
it 'adds an offense when GraphQL::Types::JSON is used with other keywords' do
|
||||
inspect_source(<<~RUBY.strip)
|
||||
expect_offense(<<~RUBY)
|
||||
class MyType
|
||||
field :some_field, GraphQL::Types::JSON, null: true, description: 'My description'
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg}
|
||||
end
|
||||
RUBY
|
||||
|
||||
expect(cop.offenses.size).to eq(1)
|
||||
end
|
||||
|
||||
it 'does not add an offense for other types' do
|
||||
|
@ -41,23 +41,21 @@ RSpec.describe RuboCop::Cop::Graphql::JSONType do
|
|||
|
||||
context 'arguments' do
|
||||
it 'adds an offense when GraphQL::Types::JSON is used' do
|
||||
inspect_source(<<~RUBY.strip)
|
||||
expect_offense(<<~RUBY)
|
||||
class MyType
|
||||
argument :some_arg, GraphQL::Types::JSON
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg}
|
||||
end
|
||||
RUBY
|
||||
|
||||
expect(cop.offenses.size).to eq(1)
|
||||
end
|
||||
|
||||
it 'adds an offense when GraphQL::Types::JSON is used with other keywords' do
|
||||
inspect_source(<<~RUBY.strip)
|
||||
expect_offense(<<~RUBY)
|
||||
class MyType
|
||||
argument :some_arg, GraphQL::Types::JSON, null: true, description: 'My description'
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg}
|
||||
end
|
||||
RUBY
|
||||
|
||||
expect(cop.offenses.size).to eq(1)
|
||||
end
|
||||
|
||||
it 'does not add an offense for other types' do
|
||||
|
|
|
@ -6,24 +6,19 @@ require 'rubocop'
|
|||
require_relative '../../../../rubocop/cop/graphql/resolver_type'
|
||||
|
||||
RSpec.describe RuboCop::Cop::Graphql::ResolverType do
|
||||
include CopHelper
|
||||
|
||||
subject(:cop) { described_class.new }
|
||||
|
||||
it 'adds an offense when there is no type annotaion' do
|
||||
lacks_type = <<-SRC
|
||||
it 'adds an offense when there is no type annotation' do
|
||||
expect_offense(<<~SRC)
|
||||
module Resolvers
|
||||
class FooResolver < BaseResolver
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Missing type annotation: Please add `type` DSL method call. e.g: type UserType.connection_type, null: true
|
||||
def resolve(**args)
|
||||
[:thing]
|
||||
end
|
||||
end
|
||||
end
|
||||
SRC
|
||||
|
||||
inspect_source(lacks_type)
|
||||
|
||||
expect(cop.offenses.size).to eq 1
|
||||
end
|
||||
|
||||
it 'does not add an offense for resolvers that have a type call' do
|
||||
|
@ -41,9 +36,10 @@ RSpec.describe RuboCop::Cop::Graphql::ResolverType do
|
|||
end
|
||||
|
||||
it 'ignores type calls on other objects' do
|
||||
lacks_type = <<-SRC
|
||||
expect_offense(<<~SRC)
|
||||
module Resolvers
|
||||
class FooResolver < BaseResolver
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Missing type annotation: Please add `type` DSL method call. e.g: type UserType.connection_type, null: true
|
||||
class FalsePositive < BaseObject
|
||||
type RedHerringType, null: true
|
||||
end
|
||||
|
@ -54,10 +50,6 @@ RSpec.describe RuboCop::Cop::Graphql::ResolverType do
|
|||
end
|
||||
end
|
||||
SRC
|
||||
|
||||
inspect_source(lacks_type)
|
||||
|
||||
expect(cop.offenses.size).to eq 1
|
||||
end
|
||||
|
||||
it 'does not add an offense unless the class is named using the Resolver convention' do
|
||||
|
|
|
@ -6,7 +6,7 @@ require 'parser/current'
|
|||
require_relative '../../rubocop/qa_helpers'
|
||||
|
||||
RSpec.describe RuboCop::QAHelpers do
|
||||
def parse_source(source, path = 'foo.rb')
|
||||
def build_and_parse_source(source, path = 'foo.rb')
|
||||
buffer = Parser::Source::Buffer.new(path)
|
||||
buffer.source = source
|
||||
|
||||
|
@ -24,13 +24,13 @@ RSpec.describe RuboCop::QAHelpers do
|
|||
|
||||
describe '#in_qa_file?' do
|
||||
it 'returns true for a node in the qa/ directory' do
|
||||
node = parse_source('10', rails_root_join('qa', 'qa', 'page', 'dashboard', 'groups.rb'))
|
||||
node = build_and_parse_source('10', rails_root_join('qa', 'qa', 'page', 'dashboard', 'groups.rb'))
|
||||
|
||||
expect(cop.in_qa_file?(node)).to eq(true)
|
||||
end
|
||||
|
||||
it 'returns false for a node outside the qa/ directory' do
|
||||
node = parse_source('10', rails_root_join('app', 'foo', 'foo.rb'))
|
||||
node = build_and_parse_source('10', rails_root_join('app', 'foo', 'foo.rb'))
|
||||
|
||||
expect(cop.in_qa_file?(node)).to eq(false)
|
||||
end
|
||||
|
|
|
@ -17,4 +17,35 @@ module SortingHelper
|
|||
click_link value
|
||||
end
|
||||
end
|
||||
|
||||
def nils_last(value)
|
||||
NilsLast.new(value)
|
||||
end
|
||||
|
||||
class NilsLast
|
||||
include Comparable
|
||||
|
||||
attr_reader :value
|
||||
delegate :==, :eql?, :hash, to: :value
|
||||
|
||||
def initialize(value)
|
||||
@value = value
|
||||
@reverse = false
|
||||
end
|
||||
|
||||
def <=>(other)
|
||||
return unless other.is_a?(self.class)
|
||||
return 0 if value.nil? && other.value.nil?
|
||||
return 1 if value.nil?
|
||||
return -1 if other.value.nil?
|
||||
|
||||
int = value <=> other.value
|
||||
@reverse ? -int : int
|
||||
end
|
||||
|
||||
def -@
|
||||
@reverse = true
|
||||
self
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue