Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-01-20 09:10:52 +00:00
parent 4ca378cac7
commit 8994750e4e
51 changed files with 609 additions and 460 deletions

View File

@ -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:

View File

@ -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'

View File

@ -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.",
),

View File

@ -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

View File

@ -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) => ({

View File

@ -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 : '';

View File

@ -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';

View File

@ -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 &&

View File

@ -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

View File

@ -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')

View File

@ -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.')

View File

@ -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.')

View File

@ -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).')

View File

@ -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.')

View File

@ -0,0 +1,5 @@
---
title: Apply new GitLab UI for buttons in admin settings
merge_request: 51789
author: Yogi (@yo)
type: other

View File

@ -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

View File

@ -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).

View File

@ -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)

View File

@ -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

View File

@ -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 ""

View File

@ -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]}"

View File

@ -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') }

View File

@ -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>',

View File

@ -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 }) => {

View File

@ -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');
});
});
});

View File

@ -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;

View File

@ -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');

View File

@ -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();
});

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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