Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-11-11 12:10:41 +00:00
parent 3e9023894d
commit 1c7411c597
79 changed files with 759 additions and 83 deletions

View File

@ -1 +1 @@
9de3dd28a5c8248903160ea35d9f718899f51c89
4892c8502cc45217903a8a584a7b5edb15edf86e

View File

@ -0,0 +1,47 @@
<script>
import { GlModal } from '@gitlab/ui';
import { __ } from '~/locale';
export default {
cancelAction: { text: __('Cancel') },
components: {
GlModal,
},
props: {
primaryText: {
type: String,
required: false,
default: __('OK'),
},
primaryVariant: {
type: String,
required: false,
default: 'confirm',
},
},
computed: {
primaryAction() {
return { text: this.primaryText, attributes: { variant: this.primaryVariant } };
},
},
mounted() {
this.$refs.modal.show();
},
};
</script>
<template>
<gl-modal
ref="modal"
size="sm"
modal-id="confirmationModal"
body-class="gl-display-flex"
:action-primary="primaryAction"
:action-cancel="$options.cancelAction"
hide-header
@primary="$emit('confirmed')"
@hidden="$emit('closed')"
>
<div class="gl-align-self-center"><slot></slot></div>
</gl-modal>
</template>

View File

@ -0,0 +1,47 @@
import Vue from 'vue';
export function confirmViaGlModal(message, element) {
return new Promise((resolve) => {
let confirmed = false;
const props = {};
const confirmBtnVariant = element.getAttribute('data-confirm-btn-variant');
if (confirmBtnVariant) {
props.primaryVariant = confirmBtnVariant;
}
const screenReaderText =
element.querySelector('.gl-sr-only')?.textContent ||
element.querySelector('.sr-only')?.textContent ||
element.getAttribute('aria-label');
if (screenReaderText) {
props.primaryText = screenReaderText;
}
const component = new Vue({
components: {
ConfirmModal: () => import('./confirm_modal.vue'),
},
render(h) {
return h(
'confirm-modal',
{
props,
on: {
confirmed() {
confirmed = true;
},
closed() {
component.$destroy();
resolve(confirmed);
},
},
},
[message],
);
},
}).$mount();
});
}

View File

@ -1,4 +1,42 @@
import Rails from '@rails/ujs';
import { confirmViaGlModal } from './confirm_via_gl_modal/confirm_via_gl_modal';
function monkeyPatchConfirmModal() {
/**
* This function is used to replace the `Rails.confirm` which uses `window.confirm`
*
* This function opens a confirmation modal which will resolve in a promise.
* Because the `Rails.confirm` API is synchronous, we go with a little hack here:
*
* 1. User clicks on something with `data-confirm`
* 2. We open the modal and return `false`, ending the "Rails" event chain
* 3. If the modal is closed and the user "confirmed" the action
* 1. replace the `Rails.confirm` with a function that always returns `true`
* 2. click the same element programmatically
*
* @param message {String} Message to be shown in the modal
* @param element {HTMLElement} Element that was clicked on
* @returns {boolean}
*/
function confirmViaModal(message, element) {
confirmViaGlModal(message, element)
.then((confirmed) => {
if (confirmed) {
Rails.confirm = () => true;
element.click();
Rails.confirm = confirmViaModal;
}
})
.catch(() => {});
return false;
}
Rails.confirm = confirmViaModal;
}
if (gon?.features?.bootstrapConfirmationModals) {
monkeyPatchConfirmModal();
}
export const initRails = () => {
// eslint-disable-next-line no-underscore-dangle

View File

@ -16,9 +16,11 @@ import { __, s__, sprintf } from '~/locale';
import Tracking from '~/tracking';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import {
WIKI_CONTENT_EDITOR_TRACKING_LABEL,
CONTENT_EDITOR_LOADED_ACTION,
SAVED_USING_CONTENT_EDITOR_ACTION,
WIKI_CONTENT_EDITOR_TRACKING_LABEL,
WIKI_FORMAT_LABEL,
WIKI_FORMAT_UPDATED_ACTION,
} from '../constants';
const trackingMixin = Tracking.mixin({
@ -219,6 +221,8 @@ export default {
this.trackFormSubmit();
}
this.trackWikiFormat();
// Wait until form field values are refreshed
await this.$nextTick();
@ -304,6 +308,14 @@ export default {
}
},
trackWikiFormat() {
this.track(WIKI_FORMAT_UPDATED_ACTION, {
label: WIKI_FORMAT_LABEL,
value: this.format,
extra: { project_path: this.pageInfo.path, old_format: this.pageInfo.format },
});
},
dismissContentEditorAlert() {
this.isContentEditorAlertDismissed = true;
},

View File

@ -1,4 +1,5 @@
export const WIKI_CONTENT_EDITOR_TRACKING_LABEL = 'wiki_content_editor';
export const CONTENT_EDITOR_LOADED_ACTION = 'content_editor_loaded';
export const SAVED_USING_CONTENT_EDITOR_ACTION = 'saved_using_content_editor';
export const WIKI_CONTENT_EDITOR_TRACKING_LABEL = 'wiki_content_editor';
export const WIKI_FORMAT_LABEL = 'wiki_format';
export const WIKI_FORMAT_UPDATED_ACTION = 'wiki_format_updated';

View File

@ -75,6 +75,7 @@ export default {
outgoingName: this.initialOutgoingName || __('GitLab Support Bot'),
projectKey: this.initialProjectKey,
searchTerm: '',
projectKeyError: null,
};
},
computed: {
@ -104,6 +105,14 @@ export default {
this.selectedFileTemplateProjectId = selectedFileTemplateProjectId;
this.selectedTemplate = selectedTemplate;
},
validateProjectKey() {
if (this.projectKey && !new RegExp(/^[a-z0-9_]+$/).test(this.projectKey)) {
this.projectKeyError = __('Only use lowercase letters, numbers, and underscores.');
return;
}
this.projectKeyError = null;
},
},
};
</script>
@ -169,8 +178,17 @@ export default {
v-model.trim="projectKey"
data-testid="project-suffix"
class="form-control"
:state="!projectKeyError"
@blur="validateProjectKey"
/>
<span v-if="hasProjectKeySupport" class="form-text text-muted">
<span v-if="hasProjectKeySupport && projectKeyError" class="form-text text-danger">
{{ projectKeyError }}
</span>
<span
v-if="hasProjectKeySupport"
class="form-text text-muted"
:class="{ 'gl-mt-2!': hasProjectKeySupport && projectKeyError }"
>
{{ __('A string appended to the project path to form the Service Desk email address.') }}
</span>
<span v-else class="form-text text-muted">

View File

@ -1,11 +1,15 @@
<script>
import { GlBadge } from '@gitlab/ui';
import { GlBadge, GlTooltipDirective, GlResizeObserverDirective } from '@gitlab/ui';
import { RUNNER_TAG_BADGE_VARIANT } from '../constants';
export default {
components: {
GlBadge,
},
directives: {
GlTooltip: GlTooltipDirective,
GlResizeObserver: GlResizeObserverDirective,
},
props: {
tag: {
type: String,
@ -14,14 +18,39 @@ export default {
size: {
type: String,
required: false,
default: 'md',
default: 'sm',
},
},
data() {
return {
overflowing: false,
};
},
computed: {
tooltip() {
if (this.overflowing) {
return this.tag;
}
return '';
},
},
methods: {
onResize() {
const { scrollWidth, offsetWidth } = this.$el;
this.overflowing = scrollWidth > offsetWidth;
},
},
RUNNER_TAG_BADGE_VARIANT,
};
</script>
<template>
<gl-badge :size="size" :variant="$options.RUNNER_TAG_BADGE_VARIANT">
<gl-badge
v-gl-tooltip="tooltip"
v-gl-resize-observer="onResize"
class="gl-display-inline-block gl-max-w-full gl-text-truncate"
:size="size"
:variant="$options.RUNNER_TAG_BADGE_VARIANT"
>
{{ tag }}
</gl-badge>
</template>

View File

@ -14,13 +14,19 @@ export default {
size: {
type: String,
required: false,
default: 'md',
default: 'sm',
},
},
};
</script>
<template>
<div>
<runner-tag v-for="tag in tagList" :key="tag" :tag="tag" :size="size" />
<runner-tag
v-for="tag in tagList"
:key="tag"
class="gl-display-inline gl-mr-1"
:tag="tag"
:size="size"
/>
</div>
</template>

View File

@ -27,7 +27,7 @@ export const I18N_NOT_CONNECTED_RUNNER_DESCRIPTION = s__(
export const I18N_LOCKED_RUNNER_DESCRIPTION = s__('Runners|You cannot assign to other projects');
export const I18N_PAUSED_RUNNER_DESCRIPTION = s__('Runners|Not available to run jobs');
export const RUNNER_TAG_BADGE_VARIANT = 'info';
export const RUNNER_TAG_BADGE_VARIANT = 'neutral';
export const RUNNER_TAG_BG_CLASS = 'gl-bg-blue-100';
// Filtered search parameter names

View File

@ -26,6 +26,7 @@ import {
import $ from 'jquery';
import { mapGetters, mapActions, mapState } from 'vuex';
import descriptionVersionHistoryMixin from 'ee_else_ce/notes/mixins/description_version_history';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import initMRPopovers from '~/mr_popover/';
import noteHeader from '~/notes/components/note_header.vue';
@ -61,6 +62,9 @@ export default {
data() {
return {
expanded: false,
lines: [],
showLines: false,
loadingDiff: false,
};
},
computed: {
@ -94,10 +98,25 @@ export default {
},
methods: {
...mapActions(['fetchDescriptionVersion', 'softDeleteDescriptionVersion']),
async toggleDiff() {
this.showLines = !this.showLines;
if (!this.lines.length) {
this.loadingDiff = true;
const { data } = await axios.get(this.note.outdated_line_change_path);
this.lines = data.map((l) => ({
...l,
rich_text: l.rich_text.replace(/^[+ -]/, ''),
}));
this.loadingDiff = false;
}
},
},
safeHtmlConfig: {
ADD_TAGS: ['use'], // to support icon SVGs
},
userColorSchemeClass: window.gon.user_color_scheme,
};
</script>
@ -112,15 +131,28 @@ export default {
<div class="note-header">
<note-header :author="note.author" :created-at="note.created_at" :note-id="note.id">
<span v-safe-html="actionTextHtml"></span>
<template v-if="canSeeDescriptionVersion" #extra-controls>
<template
v-if="canSeeDescriptionVersion || note.outdated_line_change_path"
#extra-controls
>
&middot;
<gl-button
v-if="canSeeDescriptionVersion"
variant="link"
:icon="descriptionVersionToggleIcon"
data-testid="compare-btn"
@click="toggleDescriptionVersion"
>{{ __('Compare with previous version') }}</gl-button
>
<gl-button
v-if="note.outdated_line_change_path"
:icon="showLines ? 'chevron-up' : 'chevron-down'"
variant="link"
data-testid="outdated-lines-change-btn"
@click="toggleDiff"
>
{{ __('Compare changes') }}
</gl-button>
</template>
</note-header>
</div>
@ -154,6 +186,37 @@ export default {
@click="deleteDescriptionVersion"
/>
</div>
<div
v-if="lines.length && showLines"
class="diff-content gl-border-solid gl-border-1 gl-border-gray-200 gl-mt-4 gl-rounded-small gl-overflow-hidden"
>
<table
:class="$options.userColorSchemeClass"
class="code js-syntax-highlight"
data-testid="outdated-lines"
>
<tr v-for="line in lines" v-once :key="line.line_code" class="line_holder">
<td
:class="line.type"
class="diff-line-num old_line gl-border-bottom-0! gl-border-top-0!"
>
{{ line.old_line }}
</td>
<td
:class="line.type"
class="diff-line-num new_line gl-border-bottom-0! gl-border-top-0!"
>
{{ line.new_line }}
</td>
<td
:class="line.type"
class="line_content gl-display-table-cell!"
v-html="line.rich_text /* eslint-disable-line vue/no-v-html */"
></td>
</tr>
</table>
</div>
<gl-skeleton-loading v-else-if="showLines" class="gl-mt-4" />
</div>
</div>
</timeline-entry-item>

View File

@ -55,6 +55,14 @@ class Projects::NotesController < Projects::ApplicationController
end
end
def outdated_line_change
diff_lines = Rails.cache.fetch(['note', note.id, 'oudated_line_change'], expires_in: 7.days) do
::MergeRequests::OutdatedDiscussionDiffLinesService.new(project: @project, note: note).execute.to_json
end
render json: diff_lines
end
private
def render_json_with_notes_serializer

View File

@ -0,0 +1,34 @@
# frozen_string_literal: true
# Mixin for all resolver classes for type `Types::GroupType.connection_type`.
module ResolvesGroups
extend ActiveSupport::Concern
include LooksAhead
def resolve_with_lookahead(**args)
apply_lookahead(resolve_groups(**args))
end
private
# The resolver should implement this method.
def resolve_groups(**args)
raise NotImplementedError
end
def preloads
{
contacts: [:contacts],
container_repositories_count: [:container_repositories],
custom_emoji: [:custom_emoji],
full_path: [:route],
organizations: [:organizations],
path: [:route],
dependency_proxy_blob_count: [:dependency_proxy_blobs],
dependency_proxy_blobs: [:dependency_proxy_blobs],
dependency_proxy_image_count: [:dependency_proxy_manifests],
dependency_proxy_image_ttl_policy: [:dependency_proxy_image_ttl_policy],
dependency_proxy_setting: [:dependency_proxy_setting]
}
end
end

View File

@ -2,6 +2,8 @@
module Resolvers
class GroupsResolver < BaseResolver
include ResolvesGroups
type Types::GroupType, null: true
argument :include_parent_descendants, GraphQL::Types::Boolean,
@ -19,16 +21,12 @@ module Resolvers
alias_method :parent, :object
def resolve(**args)
return [] unless parent.present?
find_groups(args)
end
private
# rubocop: disable CodeReuse/ActiveRecord
def find_groups(args)
def resolve_groups(args)
return Group.none unless parent.present?
GroupsFinder
.new(context[:current_user], args.merge(parent: parent))
.execute

View File

@ -3,8 +3,8 @@
module Resolvers
module Users
class GroupsResolver < BaseResolver
include ResolvesGroups
include Gitlab::Graphql::Authorize::AuthorizeResource
include LooksAhead
type Types::GroupType.connection_type, null: true
@ -23,19 +23,14 @@ module Resolvers
Preloaders::UserMaxAccessLevelInGroupsPreloader.new(nodes, current_user).execute
end
def resolve_with_lookahead(**args)
return unless Feature.enabled?(:paginatable_namespace_drop_down_for_project_creation, current_user, default_enabled: :yaml)
apply_lookahead(Groups::UserGroupsFinder.new(current_user, object, args).execute)
def ready?(**args)
Feature.enabled?(:paginatable_namespace_drop_down_for_project_creation, current_user, default_enabled: :yaml)
end
private
def preloads
{
path: [:route],
full_path: [:route]
}
def resolve_groups(**args)
Groups::UserGroupsFinder.new(current_user, object, args).execute
end
end
end

View File

@ -34,6 +34,7 @@ module Types
null: true,
method: :project_creation_level_str,
description: 'Permission level required to create projects in the group.'
field :subgroup_creation_level,
type: GraphQL::Types::String,
null: true,
@ -44,6 +45,7 @@ module Types
type: GraphQL::Types::Boolean,
null: true,
description: 'Indicates if all users in this group are required to set up two-factor authentication.'
field :two_factor_grace_period,
type: GraphQL::Types::Int,
null: true,
@ -225,11 +227,11 @@ module Types
end
def dependency_proxy_image_count
group.dependency_proxy_manifests.count
group.dependency_proxy_manifests.size
end
def dependency_proxy_blob_count
group.dependency_proxy_blobs.count
group.dependency_proxy_blobs.size
end
def dependency_proxy_total_size

View File

@ -56,6 +56,9 @@ class Group < Namespace
has_many :boards
has_many :badges, class_name: 'GroupBadge'
has_many :organizations, class_name: 'CustomerRelations::Organization', inverse_of: :group
has_many :contacts, class_name: 'CustomerRelations::Contact', inverse_of: :group
has_many :cluster_groups, class_name: 'Clusters::Group'
has_many :clusters, through: :cluster_groups, class_name: 'Clusters::Cluster'
@ -757,14 +760,6 @@ class Group < Namespace
Timelog.in_group(self)
end
def organizations
::CustomerRelations::Organization.where(group_id: self.id)
end
def contacts
::CustomerRelations::Contact.where(group_id: self.id)
end
def dependency_proxy_image_ttl_policy
super || build_dependency_proxy_image_ttl_policy
end

View File

@ -33,7 +33,7 @@ module Ci
end
def runner_variables
variables.sort_and_expand_all(project, keep_undefined: true).to_runner_variables
variables.sort_and_expand_all(keep_undefined: true).to_runner_variables
end
def refspecs

View File

@ -51,6 +51,10 @@ class NoteEntity < API::Entities::Note
SystemNoteHelper.system_note_icon_name(note)
end
expose :outdated_line_change_path, if: -> (note, _) { note.system? && note.change_position&.line_range && Feature.enabled?(:display_outdated_line_diff, note.project, default_enabled: :yaml) } do |note|
outdated_line_change_namespace_project_note_path(namespace_id: note.project.namespace, project_id: note.project, id: note)
end
expose :is_noteable_author do |note|
note.noteable_author?(request.noteable)
end

View File

@ -0,0 +1,55 @@
# frozen_string_literal: true
module MergeRequests
class OutdatedDiscussionDiffLinesService
include Gitlab::Utils::StrongMemoize
attr_reader :project, :note
OVERFLOW_LINES_COUNT = 2
def initialize(project:, note:)
@project = project
@note = note
end
def execute
end_position = position.line_range["end"]
diff_line_index = diff_lines.find_index { |l| l.new_line == end_position["new_line"] || l.old_line == end_position["old_line"] }
initial_line_index = [diff_line_index - OVERFLOW_LINES_COUNT, 0].max
last_line_index = [diff_line_index + OVERFLOW_LINES_COUNT, diff_lines.length].min
prev_lines = []
diff_lines[initial_line_index..last_line_index].each do |line|
if line.meta?
prev_lines.clear
else
prev_lines << line
end
end
prev_lines
end
private
def position
note.change_position
end
def repository
project.repository
end
def diff_file
position.diff_file(repository)
end
def diff_lines
strong_memoize(:diff_lines) do
diff_file.highlighted_diff_lines
end
end
end
end

View File

@ -0,0 +1,8 @@
---
name: bootstrap_confirmation_modals
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/73167
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/344658
milestone: '14.5'
type: development
group: group::foundations
default_enabled: false

View File

@ -0,0 +1,8 @@
---
name: display_outdated_line_diff
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/72597
rollout_issue_url:
milestone: '14.5'
type: development
group: group::code review
default_enabled: false

View File

@ -540,6 +540,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
delete :delete_attachment # rubocop:todo Cop/PutProjectRoutesUnderScope
post :resolve # rubocop:todo Cop/PutProjectRoutesUnderScope
delete :resolve, action: :unresolve # rubocop:todo Cop/PutProjectRoutesUnderScope
get :outdated_line_change # rubocop:todo Cop/PutProjectRoutesUnderScope
end
end

View File

@ -33,6 +33,6 @@ This setup is for when you have installed GitLab using the
[Omnibus GitLab **Enterprise Edition** (EE) package](https://about.gitlab.com/install/?version=ee).
All the tools that are needed like PostgreSQL, PgBouncer, and Patroni are bundled in
the package, so you can it to set up the whole PostgreSQL infrastructure (primary, replica).
the package, so you can use it to set up the whole PostgreSQL infrastructure (primary, replica).
[> Read how to set up PostgreSQL replication and failover using Omnibus GitLab](replication_and_failover.md)

View File

@ -23,7 +23,7 @@ We recommend the GitLab.com for Jira Cloud app, because data is
synchronized in real time. The DVCS connector updates data only once per hour.
The user configuring the GitLab.com for Jira Cloud app must have
at least the [Maintainer](../../user/permissions.md) role the GitLab.com namespace.
at least the [Maintainer](../../user/permissions.md) role in the GitLab.com namespace.
This integration method supports [Smart Commits](dvcs.md#smart-commits).

View File

@ -173,7 +173,7 @@ module Gitlab
end
def variable_expansion_errors
expanded_collection = evaluate_context.variables.sort_and_expand_all(@pipeline.project)
expanded_collection = evaluate_context.variables.sort_and_expand_all
errors = expanded_collection.errors
["#{name}: #{errors}"] if errors
end

View File

@ -89,7 +89,7 @@ module Gitlab
end
end
def sort_and_expand_all(project, keep_undefined: false)
def sort_and_expand_all(keep_undefined: false)
sorted = Sort.new(self)
return self.class.new(self, sorted.errors) unless sorted.valid?

View File

@ -104,7 +104,7 @@ module Gitlab
# the current state on the CD diff, so we treat it as outdated.
ac_diff = ac_diffs.diff_file_with_new_path(c_path, c_mode)
{ position: new_position(ac_diff, nil, c_line), outdated: true }
{ position: new_position(ac_diff, nil, c_line, position.line_range), outdated: true }
end
else
# If the line is still in D and not in C, it is still added.
@ -112,7 +112,7 @@ module Gitlab
end
else
# If the line is no longer in D, it has been removed from the MR.
{ position: new_position(bd_diff, b_line, nil), outdated: true }
{ position: new_position(bd_diff, b_line, nil, position.line_range), outdated: true }
end
end
@ -140,14 +140,14 @@ module Gitlab
# removed line into an unchanged one.
bd_diff = bd_diffs.diff_file_with_new_path(d_path, d_mode)
{ position: new_position(bd_diff, nil, d_line), outdated: true }
{ position: new_position(bd_diff, nil, d_line, position.line_range), outdated: true }
else
# If the line is still in C and not in D, it is still removed.
{ position: new_position(cd_diff, c_line, nil, position.line_range), outdated: false }
end
else
# If the line is no longer in C, it has been removed outside of the MR.
{ position: new_position(ac_diff, a_line, nil), outdated: true }
{ position: new_position(ac_diff, a_line, nil, position.line_range), outdated: true }
end
end

View File

@ -58,6 +58,7 @@ module Gitlab
push_frontend_feature_flag(:new_header_search, default_enabled: :yaml)
push_frontend_feature_flag(:suppress_apollo_errors_during_navigation, current_user, default_enabled: :yaml)
push_frontend_feature_flag(:configure_iac_scanning_via_mr, current_user, default_enabled: :yaml)
push_frontend_feature_flag(:bootstrap_confirmation_modals, default_enabled: :yaml)
end
# Exposes the state of a feature flag to the frontend code.

View File

@ -24184,6 +24184,9 @@ msgstr ""
msgid "Only reCAPTCHA v2 is supported:"
msgstr ""
msgid "Only use lowercase letters, numbers, and underscores."
msgstr ""
msgid "Only users from the specified IP address ranges are able to reach this group, including all subgroups, projects, and Git repositories."
msgstr ""

View File

@ -1007,6 +1007,35 @@ RSpec.describe Projects::NotesController do
end
end
describe 'GET outdated_line_change' do
let(:request_params) do
{
namespace_id: project.namespace,
project_id: project,
id: note,
format: 'json'
}
end
before do
service = double
allow(service).to receive(:execute).and_return([{ line_text: 'Test' }])
allow(MergeRequests::OutdatedDiscussionDiffLinesService).to receive(:new).once.and_return(service)
sign_in(user)
project.add_developer(user)
end
it "successfully renders expected JSON response" do
get :outdated_line_change, params: request_params
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an(Array)
expect(json_response.count).to eq(1)
expect(json_response.first).to include({ "line_text" => "Test" })
end
end
# Convert a time to an integer number of microseconds
def microseconds(time)
(time.to_i * 1_000_000) + time.usec

View File

@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe 'Admin disables 2FA for a user' do
it 'successfully', :js do
stub_feature_flags(bootstrap_confirmation_modals: false)
admin = create(:admin)
sign_in(admin)
gitlab_enable_admin_mode_sign_in(admin)

View File

@ -252,6 +252,7 @@ RSpec.describe 'Admin Groups' do
describe 'admin remove themself from a group', :js, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/222342' do
it 'removes admin from the group' do
stub_feature_flags(bootstrap_confirmation_modals: false)
group.add_user(current_user, Gitlab::Access::DEVELOPER)
visit group_group_members_path(group)

View File

@ -79,6 +79,7 @@ RSpec.describe 'Admin::Hooks' do
let(:hook_url) { generate(:url) }
before do
stub_feature_flags(bootstrap_confirmation_modals: false)
create(:system_hook, url: hook_url)
end

View File

@ -14,6 +14,7 @@ RSpec.describe 'admin issues labels' do
describe 'list' do
before do
stub_feature_flags(bootstrap_confirmation_modals: false)
visit admin_labels_path
end

View File

@ -74,6 +74,7 @@ RSpec.describe 'Admin > Users > Impersonation Tokens', :js do
let!(:impersonation_token) { create(:personal_access_token, :impersonation, user: user) }
it "allows revocation of an active impersonation token" do
stub_feature_flags(bootstrap_confirmation_modals: false)
visit admin_user_impersonation_tokens_path(user_id: user.username)
accept_confirm { click_on "Revoke" }

View File

@ -8,6 +8,7 @@ RSpec.describe 'Admin uses repository checks', :request_store do
let(:admin) { create(:admin) }
before do
stub_feature_flags(bootstrap_confirmation_modals: false)
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
sign_in(admin)
end

View File

@ -9,6 +9,7 @@ RSpec.describe 'Admin::Users::User' do
let_it_be(:current_user) { create(:admin) }
before do
stub_feature_flags(bootstrap_confirmation_modals: false)
sign_in(current_user)
gitlab_enable_admin_mode_sign_in(current_user)
end

View File

@ -9,6 +9,7 @@ RSpec.describe 'Admin::Users' do
let_it_be(:current_user) { create(:admin) }
before do
stub_feature_flags(bootstrap_confirmation_modals: false)
sign_in(current_user)
gitlab_enable_admin_mode_sign_in(current_user)
end

View File

@ -536,6 +536,7 @@ RSpec.describe 'Project issue boards', :js do
let_it_be(:user_guest) { create(:user) }
before do
stub_feature_flags(bootstrap_confirmation_modals: false)
project.add_guest(user_guest)
sign_in(user_guest)
visit project_board_path(project, board)

View File

@ -10,6 +10,7 @@ RSpec.describe 'Groups > Members > Leave group' do
let(:group) { create(:group) }
before do
stub_feature_flags(bootstrap_confirmation_modals: false)
sign_in(user)
end

View File

@ -14,6 +14,7 @@ RSpec.describe 'User comments on a diff', :js do
let(:user) { create(:user) }
before do
stub_feature_flags(bootstrap_confirmation_modals: false)
project.add_maintainer(user)
sign_in(user)

View File

@ -18,6 +18,7 @@ RSpec.describe 'Merge request > User posts diff notes', :js do
project.add_developer(user)
sign_in(user)
stub_feature_flags(bootstrap_confirmation_modals: false)
end
context 'when hovering over a parallel view diff file' do

View File

@ -18,8 +18,10 @@ RSpec.describe 'Merge request > User posts notes', :js do
end
before do
stub_feature_flags(bootstrap_confirmation_modals: false)
project.add_maintainer(user)
sign_in(user)
visit project_merge_request_path(project, merge_request)
end

View File

@ -79,6 +79,7 @@ RSpec.describe 'Merge request > User sees avatars on diff notes', :js do
%w(parallel).each do |view|
context "#{view} view" do
before do
stub_feature_flags(bootstrap_confirmation_modals: false)
visit diffs_project_merge_request_path(project, merge_request, view: view)
wait_for_requests

View File

@ -110,6 +110,7 @@ RSpec.describe 'Merge request > User sees deployment widget', :js do
let(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') }
before do
stub_feature_flags(bootstrap_confirmation_modals: false)
build.success!
deployment.update!(on_stop: manual.name)
visit project_merge_request_path(project, merge_request)

View File

@ -6,6 +6,7 @@ RSpec.describe 'Profile account page', :js do
let(:user) { create(:user) }
before do
stub_feature_flags(bootstrap_confirmation_modals: false)
sign_in(user)
end
@ -80,6 +81,7 @@ RSpec.describe 'Profile account page', :js do
describe 'when I reset incoming email token' do
before do
allow(Gitlab.config.incoming_email).to receive(:enabled).and_return(true)
stub_feature_flags(bootstrap_confirmation_modals: false)
visit profile_personal_access_tokens_path
end

View File

@ -11,6 +11,10 @@ RSpec.describe 'Profile > Active Sessions', :clean_gitlab_redis_shared_state do
let(:admin) { create(:admin) }
before do
stub_feature_flags(bootstrap_confirmation_modals: false)
end
it 'user sees their active sessions' do
travel_to(Time.zone.parse('2018-03-12 09:06')) do
Capybara::Session.new(:session1)

View File

@ -7,6 +7,7 @@ RSpec.describe 'Profile > Applications' do
let(:application) { create(:oauth_application, owner: user) }
before do
stub_feature_flags(bootstrap_confirmation_modals: false)
sign_in(user)
end

View File

@ -34,6 +34,7 @@ RSpec.describe 'Profile > Personal Access Tokens', :js do
end
before do
stub_feature_flags(bootstrap_confirmation_modals: false)
sign_in(user)
end

View File

@ -35,6 +35,7 @@ RSpec.describe "User deletes branch", :js do
context 'when the feature flag :delete_branch_confirmation_modals is disabled' do
before do
stub_feature_flags(bootstrap_confirmation_modals: false)
stub_feature_flags(delete_branch_confirmation_modals: false)
end

View File

@ -179,6 +179,7 @@ RSpec.describe 'Branches' do
context 'when the delete_branch_confirmation_modals feature flag is disabled' do
it 'removes branch after confirmation', :js do
stub_feature_flags(delete_branch_confirmation_modals: false)
stub_feature_flags(bootstrap_confirmation_modals: false)
visit project_branches_filtered_path(project, state: 'all')

View File

@ -11,6 +11,7 @@ RSpec.describe "User deletes comments on a commit", :js do
let(:user) { create(:user) }
before do
stub_feature_flags(bootstrap_confirmation_modals: false)
sign_in(user)
project.add_developer(user)

View File

@ -93,6 +93,8 @@ RSpec.describe "User comments on commit", :js do
context "when deleting comment" do
before do
stub_feature_flags(bootstrap_confirmation_modals: false)
visit(project_commit_path(project, sample_commit.id))
add_note(comment_text)

View File

@ -143,6 +143,8 @@ RSpec.describe 'Environments page', :js do
create(:environment, project: project, state: :available)
end
stub_feature_flags(bootstrap_confirmation_modals: false)
context 'when there are no deployments' do
before do
visit_environments(project)

View File

@ -12,6 +12,7 @@ RSpec.describe 'User browses a job', :js do
before do
project.add_maintainer(user)
project.enable_ci
stub_feature_flags(bootstrap_confirmation_modals: false)
sign_in(user)

View File

@ -9,6 +9,7 @@ RSpec.describe 'Projects > Members > Member leaves project' do
before do
project.add_developer(user)
sign_in(user)
stub_feature_flags(bootstrap_confirmation_modals: false)
end
it 'user leaves project' do

View File

@ -11,6 +11,7 @@ RSpec.describe 'Projects > Members > User requests access', :js do
before do
sign_in(user)
visit project_path(project)
stub_feature_flags(bootstrap_confirmation_modals: false)
end
it 'request access feature is disabled' do

View File

@ -14,6 +14,8 @@ RSpec.describe 'User adds pages domain', :js do
project.add_maintainer(user)
sign_in(user)
stub_feature_flags(bootstrap_confirmation_modals: false)
end
context 'when pages are exposed on external HTTP address', :http_pages_enabled do

View File

@ -14,6 +14,7 @@ RSpec.describe "Pages with Let's Encrypt", :https_pages_enabled do
before do
allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
stub_lets_encrypt_settings
stub_feature_flags(bootstrap_confirmation_modals: false)
project.add_role(user, role)
sign_in(user)

View File

@ -176,6 +176,7 @@ RSpec.describe 'Pages edits pages settings', :js do
describe 'Remove page' do
context 'when pages are deployed' do
before do
stub_feature_flags(bootstrap_confirmation_modals: false)
project.mark_pages_as_deployed
end

View File

@ -11,6 +11,7 @@ RSpec.describe 'Pipeline Schedules', :js do
context 'logged in as maintainer' do
before do
stub_feature_flags(bootstrap_confirmation_modals: false)
project.add_maintainer(user)
gitlab_sign_in(user)
end

View File

@ -317,6 +317,7 @@ RSpec.describe 'Pipelines', :js do
end
before do
stub_feature_flags(bootstrap_confirmation_modals: false)
visit_project_pipelines
end

View File

@ -13,6 +13,7 @@ RSpec.describe 'Project > Settings > Access Tokens', :js do
end
before do
stub_feature_flags(bootstrap_confirmation_modals: false)
sign_in(user)
end

View File

@ -7,6 +7,7 @@ RSpec.describe 'User searches project settings', :js do
let_it_be(:project) { create(:project, :repository, namespace: user.namespace, pages_https_only: false) }
before do
stub_feature_flags(bootstrap_confirmation_modals: false)
sign_in(user)
end

View File

@ -18,6 +18,7 @@ RSpec.describe 'Comments on personal snippets', :js do
end
before do
stub_feature_flags(bootstrap_confirmation_modals: false)
sign_in user
visit snippet_path(snippet)

View File

@ -16,6 +16,7 @@ RSpec.describe 'User creates snippet', :js do
let(:snippet_title_field) { 'snippet-title' }
before do
stub_feature_flags(bootstrap_confirmation_modals: false)
sign_in(user)
visit new_snippet_path

View File

@ -72,6 +72,7 @@ RSpec.describe 'Triggers', :js do
describe 'trigger "Revoke" workflow' do
before do
stub_feature_flags(bootstrap_confirmation_modals: false)
create(:ci_trigger, owner: user2, project: @project, description: trigger_title)
visit project_settings_ci_cd_path(@project)
end

View File

@ -0,0 +1,59 @@
import { GlModal } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import ConfirmModal from '~/lib/utils/confirm_via_gl_modal/confirm_modal.vue';
describe('Confirm Modal', () => {
let wrapper;
let modal;
const createComponent = ({ primaryText, primaryVariant } = {}) => {
wrapper = mount(ConfirmModal, {
propsData: {
primaryText,
primaryVariant,
},
});
};
afterEach(() => {
wrapper.destroy();
});
const findGlModal = () => wrapper.findComponent(GlModal);
describe('Modal events', () => {
beforeEach(() => {
createComponent();
modal = findGlModal();
});
it('should emit `confirmed` event on `primary` modal event', () => {
findGlModal().vm.$emit('primary');
expect(wrapper.emitted('confirmed')).toBeTruthy();
});
it('should emit closed` event on `hidden` modal event', () => {
modal.vm.$emit('hidden');
expect(wrapper.emitted('closed')).toBeTruthy();
});
});
describe('Custom properties', () => {
it('should pass correct custom primary text & button variant to the modal when provided', () => {
const primaryText = "Let's do it!";
const primaryVariant = 'danger';
createComponent({ primaryText, primaryVariant });
const customProps = findGlModal().props('actionPrimary');
expect(customProps.text).toBe(primaryText);
expect(customProps.attributes.variant).toBe(primaryVariant);
});
it('should pass default primary text & button variant to the modal if no custom values provided', () => {
createComponent();
const customProps = findGlModal().props('actionPrimary');
expect(customProps.text).toBe('OK');
expect(customProps.attributes.variant).toBe('confirm');
});
});
});

View File

@ -8,9 +8,11 @@ import waitForPromises from 'helpers/wait_for_promises';
import ContentEditor from '~/content_editor/components/content_editor.vue';
import WikiForm from '~/pages/shared/wikis/components/wiki_form.vue';
import {
WIKI_CONTENT_EDITOR_TRACKING_LABEL,
CONTENT_EDITOR_LOADED_ACTION,
SAVED_USING_CONTENT_EDITOR_ACTION,
WIKI_CONTENT_EDITOR_TRACKING_LABEL,
WIKI_FORMAT_LABEL,
WIKI_FORMAT_UPDATED_ACTION,
} from '~/pages/shared/wikis/constants';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
@ -65,7 +67,6 @@ describe('WikiForm', () => {
const pageInfoPersisted = {
...pageInfoNew,
persisted: true,
title: 'My page',
content: ' My page content ',
format: 'markdown',
@ -177,7 +178,7 @@ describe('WikiForm', () => {
await wrapper.vm.$nextTick();
expect(wrapper.text()).toContain(titleHelpText);
expect(findTitleHelpLink().attributes().href).toEqual(titleHelpLink);
expect(findTitleHelpLink().attributes().href).toBe(titleHelpLink);
},
);
@ -186,7 +187,7 @@ describe('WikiForm', () => {
await wrapper.vm.$nextTick();
expect(findMarkdownHelpLink().attributes().href).toEqual(
expect(findMarkdownHelpLink().attributes().href).toBe(
'/help/user/markdown#wiki-specific-markdown',
);
});
@ -220,8 +221,8 @@ describe('WikiForm', () => {
expect(e.preventDefault).not.toHaveBeenCalled();
});
it('does not trigger tracking event', async () => {
expect(trackingSpy).not.toHaveBeenCalled();
it('triggers wiki format tracking event', async () => {
expect(trackingSpy).toHaveBeenCalledTimes(1);
});
it('does not trim page content', () => {
@ -273,7 +274,7 @@ describe('WikiForm', () => {
({ persisted, redirectLink }) => {
createWrapper(persisted);
expect(findCancelButton().attributes().href).toEqual(redirectLink);
expect(findCancelButton().attributes().href).toBe(redirectLink);
},
);
});
@ -438,7 +439,7 @@ describe('WikiForm', () => {
});
});
it('triggers tracking event on form submit', async () => {
it('triggers tracking events on form submit', async () => {
triggerFormSubmit();
await wrapper.vm.$nextTick();
@ -446,6 +447,15 @@ describe('WikiForm', () => {
expect(trackingSpy).toHaveBeenCalledWith(undefined, SAVED_USING_CONTENT_EDITOR_ACTION, {
label: WIKI_CONTENT_EDITOR_TRACKING_LABEL,
});
expect(trackingSpy).toHaveBeenCalledWith(undefined, WIKI_FORMAT_UPDATED_ACTION, {
label: WIKI_FORMAT_LABEL,
value: findFormat().element.value,
extra: {
old_format: pageInfoPersisted.format,
project_path: pageInfoPersisted.path,
},
});
});
it('updates content from content editor on form submit', async () => {

View File

@ -1,5 +1,5 @@
import { GlButton, GlDropdown, GlLoadingIcon, GlToggle } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { shallowMount, mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import ServiceDeskSetting from '~/projects/settings_service_desk/components/service_desk_setting.vue';
@ -16,9 +16,9 @@ describe('ServiceDeskSetting', () => {
const findTemplateDropdown = () => wrapper.find(GlDropdown);
const findToggle = () => wrapper.find(GlToggle);
const createComponent = ({ props = {} } = {}) =>
const createComponent = ({ props = {}, mountFunction = shallowMount } = {}) =>
extendedWrapper(
shallowMount(ServiceDeskSetting, {
mountFunction(ServiceDeskSetting, {
propsData: {
isEnabled: true,
...props,
@ -128,6 +128,23 @@ describe('ServiceDeskSetting', () => {
expect(input.exists()).toBe(true);
expect(input.attributes('disabled')).toBeUndefined();
});
it('shows error when value contains uppercase or special chars', async () => {
wrapper = createComponent({
props: { customEmailEnabled: true },
mountFunction: mount,
});
const input = wrapper.findByTestId('project-suffix');
input.setValue('abc_A.');
input.trigger('blur');
await wrapper.vm.$nextTick();
const errorText = wrapper.find('.text-danger');
expect(errorText.exists()).toBe(true);
});
});
describe('customEmail is the same as incomingEmail', () => {

View File

@ -1,18 +1,35 @@
import { GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import RunnerTag from '~/runner/components/runner_tag.vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
const mockTag = 'tag1';
describe('RunnerTag', () => {
let wrapper;
const findBadge = () => wrapper.findComponent(GlBadge);
const getTooltipValue = () => getBinding(findBadge().element, 'gl-tooltip').value;
const setDimensions = ({ scrollWidth, offsetWidth }) => {
jest.spyOn(findBadge().element, 'scrollWidth', 'get').mockReturnValue(scrollWidth);
jest.spyOn(findBadge().element, 'offsetWidth', 'get').mockReturnValue(offsetWidth);
// Mock trigger resize
getBinding(findBadge().element, 'gl-resize-observer').value();
};
const createComponent = ({ props = {} } = {}) => {
wrapper = shallowMount(RunnerTag, {
propsData: {
tag: 'tag1',
tag: mockTag,
...props,
},
directives: {
GlTooltip: createMockDirective(),
GlResizeObserver: createMockDirective(),
},
});
};
@ -25,21 +42,36 @@ describe('RunnerTag', () => {
});
it('Displays tag text', () => {
expect(wrapper.text()).toBe('tag1');
expect(wrapper.text()).toBe(mockTag);
});
it('Displays tags with correct style', () => {
expect(findBadge().props()).toMatchObject({
size: 'md',
variant: 'info',
size: 'sm',
variant: 'neutral',
});
});
it('Displays tags with small size', () => {
it('Displays tags with md size', () => {
createComponent({
props: { size: 'sm' },
props: { size: 'md' },
});
expect(findBadge().props('size')).toBe('sm');
expect(findBadge().props('size')).toBe('md');
});
it.each`
case | scrollWidth | offsetWidth | expectedTooltip
${'overflowing'} | ${110} | ${100} | ${mockTag}
${'not overflowing'} | ${90} | ${100} | ${''}
${'almost overflowing'} | ${100} | ${100} | ${''}
`(
'Sets "$expectedTooltip" as tooltip when $case',
async ({ scrollWidth, offsetWidth, expectedTooltip }) => {
setDimensions({ scrollWidth, offsetWidth });
await nextTick();
expect(getTooltipValue()).toBe(expectedTooltip);
},
);
});

View File

@ -33,16 +33,16 @@ describe('RunnerTags', () => {
});
it('Displays tags with correct style', () => {
expect(findBadge().props('size')).toBe('md');
expect(findBadge().props('variant')).toBe('info');
expect(findBadge().props('size')).toBe('sm');
expect(findBadge().props('variant')).toBe('neutral');
});
it('Displays tags with small size', () => {
it('Displays tags with md size', () => {
createComponent({
props: { size: 'sm' },
props: { size: 'md' },
});
expect(findBadge().props('size')).toBe('sm');
expect(findBadge().props('size')).toBe('md');
});
it('Is empty when there are no tags', () => {

View File

@ -1,13 +1,27 @@
import MockAdapter from 'axios-mock-adapter';
import { mount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
import initMRPopovers from '~/mr_popover/index';
import createStore from '~/notes/stores';
import IssueSystemNote from '~/vue_shared/components/notes/system_note.vue';
import axios from '~/lib/utils/axios_utils';
jest.mock('~/mr_popover/index', () => jest.fn());
describe('system note component', () => {
let vm;
let props;
let mock;
function createComponent(propsData = {}) {
const store = createStore();
store.dispatch('setTargetNoteHash', `note_${props.note.id}`);
vm = mount(IssueSystemNote, {
store,
propsData,
});
}
beforeEach(() => {
props = {
@ -27,28 +41,29 @@ describe('system note component', () => {
},
};
const store = createStore();
store.dispatch('setTargetNoteHash', `note_${props.note.id}`);
vm = mount(IssueSystemNote, {
store,
propsData: props,
});
mock = new MockAdapter(axios);
});
afterEach(() => {
vm.destroy();
mock.restore();
});
it('should render a list item with correct id', () => {
createComponent(props);
expect(vm.attributes('id')).toEqual(`note_${props.note.id}`);
});
it('should render target class is note is target note', () => {
createComponent(props);
expect(vm.classes()).toContain('target');
});
it('should render svg icon', () => {
createComponent(props);
expect(vm.find('.timeline-icon svg').exists()).toBe(true);
});
@ -56,10 +71,31 @@ describe('system note component', () => {
// we need to strip them because they break layout of commit lists in system notes:
// https://gitlab.com/gitlab-org/gitlab-foss/uploads/b07a10670919254f0220d3ff5c1aa110/jqzI.png
it('removes wrapping paragraph from note HTML', () => {
createComponent(props);
expect(vm.find('.system-note-message').html()).toContain('<span>closed</span>');
});
it('should initMRPopovers onMount', () => {
createComponent(props);
expect(initMRPopovers).toHaveBeenCalled();
});
it('renders outdated code lines', async () => {
mock
.onGet('/outdated_line_change_path')
.reply(200, [
{ rich_text: 'console.log', type: 'new', line_code: '123', old_line: null, new_line: 1 },
]);
createComponent({
note: { ...props.note, outdated_line_change_path: '/outdated_line_change_path' },
});
await vm.find("[data-testid='outdated-lines-change-btn']").trigger('click');
await waitForPromises();
expect(vm.find("[data-testid='outdated-lines']").exists()).toBe(true);
});
});

View File

@ -0,0 +1,71 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ResolvesGroups do
include GraphqlHelpers
include AfterNextHelpers
let_it_be(:user) { create(:user) }
let_it_be(:groups) { create_pair(:group) }
let_it_be(:resolver) do
Class.new(Resolvers::BaseResolver) do
include ResolvesGroups
type Types::GroupType, null: true
end
end
let_it_be(:query_type) do
query_factory do |query|
query.field :groups,
Types::GroupType.connection_type,
null: true,
resolver: resolver
end
end
let_it_be(:lookahead_fields) do
<<~FIELDS
contacts { nodes { id } }
containerRepositoriesCount
customEmoji { nodes { id } }
fullPath
organizations { nodes { id } }
path
dependencyProxyBlobCount
dependencyProxyBlobs { nodes { fileName } }
dependencyProxyImageCount
dependencyProxyImageTtlPolicy { enabled }
dependencyProxySetting { enabled }
FIELDS
end
it 'avoids N+1 queries on the fields marked with lookahead' do
group_ids = groups.map(&:id)
allow_next(resolver).to receive(:resolve_groups).and_return(Group.id_in(group_ids))
# Prevent authorization queries from affecting the test.
allow(Ability).to receive(:allowed?).and_return(true)
single_group_query = ActiveRecord::QueryRecorder.new do
data = query_groups(limit: 1)
expect(data.size).to eq(1)
end
multi_group_query = -> {
data = query_groups(limit: 2)
expect(data.size).to eq(2)
}
expect { multi_group_query.call }.not_to exceed_query_limit(single_group_query)
end
def query_groups(limit:)
query_string = "{ groups(first: #{limit}) { nodes { id #{lookahead_fields} } } }"
data = execute_query(query_type, graphql: query_string)
graphql_dig_at(data, :data, :groups, :nodes)
end
end

View File

@ -358,8 +358,6 @@ RSpec.describe Gitlab::Ci::Variables::Collection do
end
describe '#sort_and_expand_all' do
let_it_be(:project) { create(:project) }
context 'table tests' do
using RSpec::Parameterized::TableSyntax
@ -550,7 +548,7 @@ RSpec.describe Gitlab::Ci::Variables::Collection do
with_them do
let(:collection) { Gitlab::Ci::Variables::Collection.new(variables) }
subject { collection.sort_and_expand_all(project, keep_undefined: keep_undefined) }
subject { collection.sort_and_expand_all(keep_undefined: keep_undefined) }
it 'returns Collection' do
is_expected.to be_an_instance_of(Gitlab::Ci::Variables::Collection)

View File

@ -581,13 +581,16 @@ RSpec.describe Gitlab::Diff::PositionTracer::LineStrategy, :clean_gitlab_redis_c
)
end
it "returns the new position but drops line_range information" do
it "returns the new position" do
expect_change_position(
old_path: file_name,
new_path: file_name,
old_line: nil,
new_line: 2,
line_range: nil
line_range: {
"start_line_code" => 1,
"end_line_code" => 2
}
)
end
end

View File

@ -37,6 +37,8 @@ RSpec.describe Group do
it { is_expected.to have_many(:daily_build_group_report_results).class_name('Ci::DailyBuildGroupReportResult') }
it { is_expected.to have_many(:group_callouts).class_name('Users::GroupCallout').with_foreign_key(:group_id) }
it { is_expected.to have_many(:bulk_import_exports).class_name('BulkImports::Export') }
it { is_expected.to have_many(:contacts).class_name('CustomerRelations::Contact') }
it { is_expected.to have_many(:organizations).class_name('CustomerRelations::Organization') }
describe '#members & #requesters' do
let(:requester) { create(:user) }

View File

@ -18,6 +18,7 @@ RSpec.shared_examples 'hardware device for 2fa' do |device_type|
let(:user) { create(:user) }
before do
stub_feature_flags(bootstrap_confirmation_modals: false)
gitlab_sign_in(user)
user.update_attribute(:otp_required_for_login, true)
end