mirror of
https://gitlab.com/gitlab-org/gitlab-foss.git
synced 2025-08-15 21:39:00 +00:00
Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
@ -194,7 +194,11 @@ export default {
|
||||
<div v-if="hasNoSearchResults" class="gl-text-center gl-p-3">
|
||||
{{ __('No matching results') }}
|
||||
</div>
|
||||
<div v-if="failedToLoadResults" class="gl-text-center gl-p-3">
|
||||
<div
|
||||
v-if="failedToLoadResults"
|
||||
data-testid="failed-load-results"
|
||||
class="gl-text-center gl-p-3"
|
||||
>
|
||||
{{ __('Failed to load projects') }}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -41,6 +41,7 @@ export default {
|
||||
item.extraAttrs = {
|
||||
...USER_MENU_TRACKING_DEFAULTS,
|
||||
'data-track-label': 'user_profile',
|
||||
'data-qa-selector': 'user_profile_link',
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -130,7 +130,7 @@ module Ci
|
||||
end
|
||||
|
||||
# pick builds that older than specified age
|
||||
if params.key?(:job_age)
|
||||
if params.key?(:job_age) && Feature.disabled?(:remove_job_age_from_jobs_api)
|
||||
builds = queue.builds_queued_before(builds, params[:job_age].seconds.ago)
|
||||
end
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
- is_project_overview = local_assigns.fetch(:is_project_overview, false)
|
||||
|
||||
.tree-ref-container.gl-display-flex.gl-flex-wrap.gl-gap-2.mb-2.mb-md-0
|
||||
.tree-ref-holder.gl-max-w-26
|
||||
.tree-ref-holder.gl-max-w-26{ data: { qa_selector: 'ref_dropdown_container' } }
|
||||
#js-tree-ref-switcher{ data: { project_id: @project.id, ref_type: @ref_type.to_s, project_root_path: project_path(@project) } }
|
||||
|
||||
#js-repo-breadcrumb{ data: breadcrumb_data_attributes }
|
||||
|
@ -0,0 +1,8 @@
|
||||
---
|
||||
name: remove_job_age_from_jobs_api
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/118045
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/406662
|
||||
milestone: '16.0'
|
||||
type: development
|
||||
group: group::pipeline execution
|
||||
default_enabled: true
|
@ -0,0 +1,25 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# See https://docs.gitlab.com/ee/development/migration_style_guide.html
|
||||
# for more information on how to write migrations for GitLab.
|
||||
|
||||
class FinalizeFixIncoherentPackagesSizeOnProjectStatistics < Gitlab::Database::Migration[2.1]
|
||||
MIGRATION = 'FixIncoherentPackagesSizeOnProjectStatistics'
|
||||
disable_ddl_transaction!
|
||||
|
||||
restrict_gitlab_migration gitlab_schema: :gitlab_main
|
||||
|
||||
def up
|
||||
ensure_batched_background_migration_is_finished(
|
||||
job_class_name: MIGRATION,
|
||||
table_name: :project_statistics,
|
||||
column_name: :id,
|
||||
job_arguments: [],
|
||||
finalize: true
|
||||
)
|
||||
end
|
||||
|
||||
def down
|
||||
# no-op
|
||||
end
|
||||
end
|
@ -0,0 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class DropTmpIdxPackageFilesOnNonZeroSize < Gitlab::Database::Migration[2.1]
|
||||
disable_ddl_transaction!
|
||||
|
||||
INDEX_NAME = 'tmp_idx_package_files_on_non_zero_size'
|
||||
|
||||
def up
|
||||
remove_concurrent_index :packages_package_files, %i[package_id size], name: INDEX_NAME
|
||||
end
|
||||
|
||||
def down
|
||||
add_concurrent_index :packages_package_files, %i[package_id size], where: 'size IS NOT NULL', name: INDEX_NAME
|
||||
end
|
||||
end
|
1
db/schema_migrations/20230420065656
Normal file
1
db/schema_migrations/20230420065656
Normal file
@ -0,0 +1 @@
|
||||
e29be6311d828a76c594cf350d5212fac9913362dd3e9b96fda6f74c50edfcdb
|
1
db/schema_migrations/20230420070009
Normal file
1
db/schema_migrations/20230420070009
Normal file
@ -0,0 +1 @@
|
||||
74b9c628c09856c3285452be85a853103e7b7860d1e33df664bdcae927f690d1
|
@ -32982,8 +32982,6 @@ CREATE INDEX tmp_idx_for_feedback_comment_processing ON vulnerability_feedback U
|
||||
|
||||
CREATE INDEX tmp_idx_for_vulnerability_feedback_migration ON vulnerability_feedback USING btree (id) WHERE ((migrated_to_state_transition = false) AND (feedback_type = 0));
|
||||
|
||||
CREATE INDEX tmp_idx_package_files_on_non_zero_size ON packages_package_files USING btree (package_id, size) WHERE (size IS NOT NULL);
|
||||
|
||||
CREATE INDEX tmp_idx_vulnerability_occurrences_on_id_where_report_type_7_99 ON vulnerability_occurrences USING btree (id) WHERE (report_type = ANY (ARRAY[7, 99]));
|
||||
|
||||
CREATE INDEX tmp_index_ci_job_artifacts_on_expire_at_where_locked_unknown ON ci_job_artifacts USING btree (expire_at, job_id) WHERE ((locked = 2) AND (expire_at IS NOT NULL));
|
||||
|
@ -59,8 +59,7 @@ listed here that also do not work properly in FIPS mode:
|
||||
- [Container Scanning](../user/application_security/container_scanning/index.md) support for scanning images in repositories that require authentication.
|
||||
- [Code Quality](../ci/testing/code_quality.md) does not support operating in FIPS-compliant mode.
|
||||
- [Dependency scanning](../user/application_security/dependency_scanning/index.md) support for Gradle.
|
||||
- [Dynamic Application Security Testing (DAST)](../user/application_security/dast/index.md)
|
||||
does not support operating in FIPS-compliant mode.
|
||||
- [Dynamic Application Security Testing (DAST)](../user/application_security/dast/index.md) supports a reduced set of analyzers. Browser-based and proxy-based analyzers are not available in FIPS mode today, however DAST API and DAST API Fuzzing images are available.
|
||||
- [License compliance](../user/compliance/license_compliance/index.md).
|
||||
- [Solutions for vulnerabilities](../user/application_security/vulnerabilities/index.md#resolve-a-vulnerability)
|
||||
for yarn projects.
|
||||
|
@ -141,7 +141,7 @@ module API
|
||||
optional :certificate, type: String, desc: %q(Session's certificate)
|
||||
optional :authorization, type: String, desc: %q(Session's authorization)
|
||||
end
|
||||
optional :job_age, type: Integer, desc: %q(Job should be older than passed age in seconds to be ran on runner)
|
||||
optional :job_age, type: Integer, desc: %q(DEPRECATED and will be REMOVED in GitLab 16.0. Job should be older than passed age in seconds to be ran on runner)
|
||||
end
|
||||
|
||||
# Since we serialize the build output ourselves to ensure Gitaly
|
||||
@ -167,6 +167,10 @@ module API
|
||||
|
||||
runner_params = declared_params(include_missing: false)
|
||||
|
||||
if Feature.enabled?(:remove_job_age_from_jobs_api)
|
||||
runner_params.delete(:job_age)
|
||||
end
|
||||
|
||||
if current_runner.runner_queue_value_latest?(runner_params[:last_update])
|
||||
header 'X-GitLab-Last-Update', runner_params[:last_update]
|
||||
Gitlab::Metrics.add_event(:build_not_found_cached)
|
||||
|
@ -9,7 +9,7 @@ gem 'capybara', '~> 3.39.0'
|
||||
gem 'capybara-screenshot', '~> 1.0.26'
|
||||
gem 'rake', '~> 13', '>= 13.0.6'
|
||||
gem 'rspec', '~> 3.12'
|
||||
gem 'selenium-webdriver', '~> 4.8', '>= 4.8.6'
|
||||
gem 'selenium-webdriver', '~> 4.9'
|
||||
gem 'airborne', '~> 0.3.7', require: false # airborne is messing with rspec sandboxed mode so not requiring by default
|
||||
gem 'rest-client', '~> 2.1.0'
|
||||
gem 'rspec-retry', '~> 0.6.2', require: 'rspec/retry'
|
||||
|
@ -260,7 +260,7 @@ GEM
|
||||
sawyer (0.9.2)
|
||||
addressable (>= 2.3.5)
|
||||
faraday (>= 0.17.3, < 3)
|
||||
selenium-webdriver (4.8.6)
|
||||
selenium-webdriver (4.9.0)
|
||||
rexml (~> 3.2, >= 3.2.5)
|
||||
rubyzip (>= 1.2.2, < 3.0)
|
||||
websocket (~> 1.0)
|
||||
@ -335,7 +335,7 @@ DEPENDENCIES
|
||||
rspec-retry (~> 0.6.2)
|
||||
rspec_junit_formatter (~> 0.6.0)
|
||||
ruby-debug-ide (~> 0.7.3)
|
||||
selenium-webdriver (~> 4.8, >= 4.8.6)
|
||||
selenium-webdriver (~> 4.9)
|
||||
slack-notifier (~> 2.4)
|
||||
terminal-table (~> 3.0.2)
|
||||
warning (~> 1.3)
|
||||
|
@ -249,7 +249,7 @@ module QA
|
||||
terms.accept_terms if terms.visible?
|
||||
end
|
||||
|
||||
Page::Main::Menu.perform(&:enable_new_navigation) if Runtime::Env.super_sidebar_enabled? && !on_login_page?
|
||||
Page::Main::Menu.perform(&:enable_new_navigation) if Runtime::Env.super_sidebar_enabled?
|
||||
Page::Main::Menu.validate_elements_present! unless skip_page_validation
|
||||
end
|
||||
|
||||
|
@ -25,6 +25,10 @@ module QA
|
||||
element :sign_out_link
|
||||
element :edit_profile_link
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/super_sidebar/components/user_name_group.vue' do
|
||||
element :user_profile_link
|
||||
end
|
||||
else
|
||||
view 'app/views/layouts/header/_default.html.haml' do
|
||||
element :navbar, required: true
|
||||
@ -257,6 +261,7 @@ module QA
|
||||
|
||||
def enable_new_navigation
|
||||
Runtime::Logger.info("Enabling super sidebar!")
|
||||
return Runtime::Logger.info("User is not signed in, skipping") unless has_element?(:navbar, wait: 2)
|
||||
return Runtime::Logger.info("Super sidebar is already enabled") if has_css?('[data-testid="super-sidebar"]')
|
||||
|
||||
within_user_menu { click_element(:new_navigation_toggle) }
|
||||
|
@ -48,11 +48,9 @@ module QA
|
||||
end
|
||||
|
||||
def choose_namespace(namespace)
|
||||
retry_on_exception do
|
||||
click_element :select_namespace_dropdown
|
||||
fill_element :select_namespace_dropdown_search_field, namespace
|
||||
click_button namespace
|
||||
end
|
||||
click_element :select_namespace_dropdown
|
||||
fill_element :select_namespace_dropdown_search_field, namespace
|
||||
within_element(:select_namespace_dropdown) { click_button namespace }
|
||||
end
|
||||
|
||||
def click_import_project
|
||||
|
@ -74,6 +74,10 @@ module QA
|
||||
element :download_source_code_button
|
||||
end
|
||||
|
||||
view 'app/views/projects/tree/_tree_header.html.haml' do
|
||||
element :ref_dropdown_container
|
||||
end
|
||||
|
||||
def wait_for_viewers_to_load
|
||||
has_no_element?(:spinner_placeholder, wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME)
|
||||
end
|
||||
@ -172,8 +176,10 @@ module QA
|
||||
end
|
||||
|
||||
def switch_to_branch(branch_name)
|
||||
expand_select_list
|
||||
select_item(branch_name)
|
||||
within_element(:ref_dropdown_container) do
|
||||
expand_select_list
|
||||
select_item(branch_name)
|
||||
end
|
||||
end
|
||||
|
||||
def wait_for_import
|
||||
|
@ -22,6 +22,8 @@ module QA
|
||||
end
|
||||
|
||||
def click_following_tab
|
||||
return click_element(:nav_item_link, submenu_item: 'Following') if Runtime::Env.super_sidebar_enabled?
|
||||
|
||||
click_element(:following_tab)
|
||||
end
|
||||
|
||||
|
@ -16,7 +16,8 @@ module QA
|
||||
end
|
||||
|
||||
RSpec.describe 'Manage', :skip_signup_disabled, :requires_admin, product_group: :authentication_and_authorization do
|
||||
describe 'while LDAP is enabled', :orchestrated, :ldap_no_tls, testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347934' do
|
||||
describe 'while LDAP is enabled', :orchestrated, :ldap_no_tls,
|
||||
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347934' do
|
||||
before do
|
||||
# When LDAP is enabled, a previous test might have created a token for the LDAP 'tanuki' user who is not an admin
|
||||
# So we need to set it to nil in order to create a new token for admin user so that we are able to set_application_settings
|
||||
@ -29,22 +30,22 @@ module QA
|
||||
ldap_username = Runtime::Env.ldap_username
|
||||
Runtime::Env.ldap_username = nil
|
||||
|
||||
set_require_admin_approval_after_user_signup_via_api(false)
|
||||
set_require_admin_approval_after_user_signup(false)
|
||||
|
||||
Runtime::Env.ldap_username = ldap_username
|
||||
end
|
||||
|
||||
it_behaves_like 'registration and login'
|
||||
|
||||
after do
|
||||
Runtime::Env.personal_access_token = @personal_access_token
|
||||
end
|
||||
|
||||
it_behaves_like 'registration and login'
|
||||
end
|
||||
|
||||
describe 'standard', :reliable, testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347867' do
|
||||
context 'when admin approval is not required' do
|
||||
before(:all) do
|
||||
set_require_admin_approval_after_user_signup_via_api(false)
|
||||
set_require_admin_approval_after_user_signup(false)
|
||||
end
|
||||
|
||||
it_behaves_like 'registration and login'
|
||||
@ -70,7 +71,15 @@ module QA
|
||||
Support::Waiter.wait_until(max_duration: 120, sleep_interval: 3) { !user.exists? }
|
||||
end
|
||||
|
||||
it 'allows recreating with same credentials', :reliable, testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347868' do
|
||||
after do
|
||||
if @recreated_user
|
||||
@recreated_user.api_client = admin_api_client
|
||||
@recreated_user.remove_via_api!
|
||||
end
|
||||
end
|
||||
|
||||
it 'allows recreating with same credentials', :reliable,
|
||||
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347868' do
|
||||
expect(Page::Main::Menu.perform(&:signed_in?)).to be_falsy
|
||||
|
||||
Flow::Login.sign_in(as: user, skip_page_validation: true)
|
||||
@ -86,13 +95,6 @@ module QA
|
||||
expect(Page::Main::Menu.perform(&:signed_in?)).to be_truthy
|
||||
end
|
||||
|
||||
after do
|
||||
if @recreated_user
|
||||
@recreated_user.api_client = admin_api_client
|
||||
@recreated_user.remove_via_api!
|
||||
end
|
||||
end
|
||||
|
||||
def admin_api_client
|
||||
@admin_api_client ||= Runtime::API::Client.as_admin
|
||||
end
|
||||
@ -100,29 +102,42 @@ module QA
|
||||
end
|
||||
|
||||
context 'when admin approval is required' do
|
||||
let(:signed_up_waiting_approval_text) { 'You have signed up successfully. However, we could not sign you in because your account is awaiting approval from your GitLab administrator.' }
|
||||
let(:pending_approval_blocked_text) { 'Your account is pending approval from your GitLab administrator and hence blocked. Please contact your GitLab administrator if you think this is an error.' }
|
||||
let(:signed_up_waiting_approval_text) do
|
||||
'You have signed up successfully. However, we could not sign you in because your account is awaiting approval from your GitLab administrator.'
|
||||
end
|
||||
|
||||
before do
|
||||
enable_require_admin_approval_after_user_signup_via_ui
|
||||
let(:pending_approval_blocked_text) do
|
||||
'Your account is pending approval from your GitLab administrator and hence blocked. Please contact your GitLab administrator if you think this is an error.'
|
||||
end
|
||||
|
||||
Support::Retrier.retry_on_exception do
|
||||
@user = Resource::User.fabricate_via_browser_ui! do |user|
|
||||
user.expect_fabrication_success = false
|
||||
end
|
||||
let(:user) do
|
||||
Resource::User.fabricate_via_browser_ui! do |user|
|
||||
user.expect_fabrication_success = false
|
||||
end
|
||||
end
|
||||
|
||||
it 'allows user login after approval', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347871' do
|
||||
before do
|
||||
set_require_admin_approval_after_user_signup(true)
|
||||
end
|
||||
|
||||
after do
|
||||
set_require_admin_approval_after_user_signup(false)
|
||||
user.remove_via_api! if user
|
||||
end
|
||||
|
||||
it 'allows user login after approval',
|
||||
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347871' do
|
||||
user # sign up user
|
||||
|
||||
expect(page).to have_text(signed_up_waiting_approval_text)
|
||||
|
||||
Flow::Login.sign_in(as: @user, skip_page_validation: true)
|
||||
Flow::Login.sign_in(as: user, skip_page_validation: true)
|
||||
|
||||
expect(page).to have_text(pending_approval_blocked_text)
|
||||
|
||||
approve_user(@user)
|
||||
approve_user(user)
|
||||
|
||||
Flow::Login.sign_in(as: @user, skip_page_validation: true)
|
||||
Flow::Login.sign_in(as: user, skip_page_validation: true)
|
||||
|
||||
Flow::UserOnboarding.onboard_user
|
||||
|
||||
@ -131,11 +146,6 @@ module QA
|
||||
Runtime::Browser.visit(:gitlab, Page::Dashboard::Welcome)
|
||||
Page::Main::Menu.perform(&:has_personal_area?)
|
||||
end
|
||||
|
||||
after do
|
||||
set_require_admin_approval_after_user_signup_via_api(false)
|
||||
@user.remove_via_api! if @user
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -158,36 +168,17 @@ module QA
|
||||
end
|
||||
end
|
||||
|
||||
def set_require_admin_approval_after_user_signup_via_api(enable_or_disable)
|
||||
return if get_require_admin_approval_after_user_signup_via_api == enable_or_disable
|
||||
def set_require_admin_approval_after_user_signup(enable_or_disable)
|
||||
return if get_require_admin_approval_after_user_signup == enable_or_disable
|
||||
|
||||
Runtime::ApplicationSettings.set_application_settings(require_admin_approval_after_user_signup: enable_or_disable)
|
||||
|
||||
sleep 10 # It takes a moment for the setting to come into effect
|
||||
end
|
||||
|
||||
def get_require_admin_approval_after_user_signup_via_api
|
||||
Runtime::ApplicationSettings.get_application_settings[:require_admin_approval_after_user_signup]
|
||||
end
|
||||
|
||||
def enable_require_admin_approval_after_user_signup_via_ui
|
||||
unless get_require_admin_approval_after_user_signup_via_api
|
||||
QA::Support::Retrier.retry_until do
|
||||
Flow::Login.while_signed_in_as_admin do
|
||||
Page::Main::Menu.perform(&:go_to_admin_area)
|
||||
QA::Page::Admin::Menu.perform(&:go_to_general_settings)
|
||||
Page::Admin::Settings::General.perform do |setting|
|
||||
setting.expand_sign_up_restrictions do |settings|
|
||||
settings.require_admin_approval_after_user_signup
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
sleep 15 # It takes a moment for the setting to come into effect
|
||||
|
||||
get_require_admin_approval_after_user_signup_via_api
|
||||
end
|
||||
QA::Support::Retrier.retry_until(max_duration: 10, sleep_interval: 1) do
|
||||
get_require_admin_approval_after_user_signup == enable_or_disable
|
||||
end
|
||||
end
|
||||
|
||||
def get_require_admin_approval_after_user_signup
|
||||
Runtime::ApplicationSettings.get_application_settings[:require_admin_approval_after_user_signup]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { nextTick } from 'vue';
|
||||
import {
|
||||
GlIcon,
|
||||
GlLoadingIcon,
|
||||
@ -7,12 +8,13 @@ import {
|
||||
GlSearchBoxByType,
|
||||
GlButton,
|
||||
} from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
|
||||
import { nextTick } from 'vue';
|
||||
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import IssuableMoveDropdown from '~/sidebar/components/move/issuable_move_dropdown.vue';
|
||||
import { stubComponent } from 'helpers/stub_component';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
|
||||
const mockProjects = [
|
||||
{
|
||||
@ -45,20 +47,35 @@ const mockEvent = {
|
||||
preventDefault: jest.fn(),
|
||||
};
|
||||
|
||||
const focusInputMock = jest.fn();
|
||||
const hideMock = jest.fn();
|
||||
|
||||
describe('IssuableMoveDropdown', () => {
|
||||
let mock;
|
||||
let wrapper;
|
||||
|
||||
const createComponent = (propsData = mockProps) => {
|
||||
wrapper = shallowMount(IssuableMoveDropdown, {
|
||||
wrapper = shallowMountExtended(IssuableMoveDropdown, {
|
||||
propsData,
|
||||
stubs: {
|
||||
GlDropdown: stubComponent(GlDropdown, {
|
||||
methods: {
|
||||
hide: hideMock,
|
||||
},
|
||||
}),
|
||||
GlSearchBoxByType: stubComponent(GlSearchBoxByType, {
|
||||
methods: {
|
||||
focusInput: focusInputMock,
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
wrapper.vm.$refs.dropdown.hide = jest.fn();
|
||||
wrapper.vm.$refs.searchInput.focusInput = jest.fn();
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mock = new MockAdapter(axios);
|
||||
mock.onGet(mockProps.projectsFetchPath).reply(HTTP_STATUS_OK, mockProjects);
|
||||
|
||||
createComponent();
|
||||
});
|
||||
|
||||
@ -66,38 +83,46 @@ describe('IssuableMoveDropdown', () => {
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
const findCollapsedEl = () => wrapper.findByTestId('move-collapsed');
|
||||
const findFooter = () => wrapper.findByTestId('footer');
|
||||
const findHeader = () => wrapper.findByTestId('header');
|
||||
const findFailedLoadResults = () => wrapper.findByTestId('failed-load-results');
|
||||
const findDropdownContent = () => wrapper.findByTestId('content');
|
||||
const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
|
||||
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
|
||||
const findDropdownEl = () => wrapper.findComponent(GlDropdown);
|
||||
const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
|
||||
|
||||
describe('watch', () => {
|
||||
describe('searchKey', () => {
|
||||
it('calls `fetchProjects` with value of the prop', async () => {
|
||||
jest.spyOn(wrapper.vm, 'fetchProjects');
|
||||
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
wrapper.setData({
|
||||
searchKey: 'foo',
|
||||
jest.spyOn(axios, 'get');
|
||||
findSearchBox().vm.$emit('input', 'foo');
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith('/-/autocomplete/projects?project_id=1', {
|
||||
params: { search: 'foo' },
|
||||
});
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.vm.fetchProjects).toHaveBeenCalledWith('foo');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('methods', () => {
|
||||
describe('fetchProjects', () => {
|
||||
it('sets projectsListLoading to true and projectsListLoadFailed to false', () => {
|
||||
wrapper.vm.fetchProjects();
|
||||
it('sets projectsListLoading to true and projectsListLoadFailed to false', async () => {
|
||||
findDropdownEl().vm.$emit('shown');
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.vm.projectsListLoading).toBe(true);
|
||||
expect(wrapper.vm.projectsListLoadFailed).toBe(false);
|
||||
expect(findLoadingIcon().exists()).toBe(true);
|
||||
expect(findFailedLoadResults().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('calls `axios.get` with `projectsFetchPath` and query param `search`', () => {
|
||||
jest.spyOn(axios, 'get').mockResolvedValue({
|
||||
data: mockProjects,
|
||||
});
|
||||
it('calls `axios.get` with `projectsFetchPath` and query param `search`', async () => {
|
||||
jest.spyOn(axios, 'get');
|
||||
|
||||
wrapper.vm.fetchProjects('foo');
|
||||
findSearchBox().vm.$emit('input', 'foo');
|
||||
await waitForPromises();
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(
|
||||
mockProps.projectsFetchPath,
|
||||
@ -110,74 +135,65 @@ describe('IssuableMoveDropdown', () => {
|
||||
});
|
||||
|
||||
it('sets response to `projects` and focuses on searchInput when request is successful', async () => {
|
||||
jest.spyOn(axios, 'get').mockResolvedValue({
|
||||
data: mockProjects,
|
||||
});
|
||||
jest.spyOn(axios, 'get');
|
||||
|
||||
await wrapper.vm.fetchProjects('foo');
|
||||
findSearchBox().vm.$emit('input', 'foo');
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.vm.projects).toBe(mockProjects);
|
||||
expect(wrapper.vm.$refs.searchInput.focusInput).toHaveBeenCalled();
|
||||
expect(findAllDropdownItems()).toHaveLength(mockProjects.length);
|
||||
expect(focusInputMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('sets projectsListLoadFailed to true when request fails', async () => {
|
||||
jest.spyOn(axios, 'get').mockRejectedValue({});
|
||||
|
||||
await wrapper.vm.fetchProjects('foo');
|
||||
findSearchBox().vm.$emit('input', 'foo');
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.vm.projectsListLoadFailed).toBe(true);
|
||||
expect(findFailedLoadResults().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('sets projectsListLoading to false when request completes', async () => {
|
||||
jest.spyOn(axios, 'get').mockResolvedValue({
|
||||
data: mockProjects,
|
||||
});
|
||||
jest.spyOn(axios, 'get');
|
||||
|
||||
await wrapper.vm.fetchProjects('foo');
|
||||
findDropdownEl().vm.$emit('shown');
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.vm.projectsListLoading).toBe(false);
|
||||
expect(findLoadingIcon().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSelectedProject', () => {
|
||||
it.each`
|
||||
project | selectedProject | title | returnValue
|
||||
${mockProjects[0]} | ${mockProjects[0]} | ${'are same projects'} | ${true}
|
||||
${mockProjects[0]} | ${mockProjects[1]} | ${'are different projects'} | ${false}
|
||||
projectIndex | selectedProjectIndex | title | returnValue
|
||||
${0} | ${0} | ${'are same projects'} | ${true}
|
||||
${0} | ${1} | ${'are different projects'} | ${false}
|
||||
`(
|
||||
'returns $returnValue when selectedProject and provided project param $title',
|
||||
async ({ project, selectedProject, returnValue }) => {
|
||||
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
wrapper.setData({
|
||||
selectedProject,
|
||||
});
|
||||
async ({ projectIndex, selectedProjectIndex, returnValue }) => {
|
||||
findDropdownEl().vm.$emit('shown');
|
||||
await waitForPromises();
|
||||
|
||||
findAllDropdownItems().at(selectedProjectIndex).vm.$emit('click', mockEvent);
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.vm.isSelectedProject(project)).toBe(returnValue);
|
||||
expect(findAllDropdownItems().at(projectIndex).props('isChecked')).toBe(returnValue);
|
||||
},
|
||||
);
|
||||
|
||||
it('returns false when selectedProject is null', async () => {
|
||||
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
wrapper.setData({
|
||||
selectedProject: null,
|
||||
});
|
||||
findDropdownEl().vm.$emit('shown');
|
||||
await waitForPromises();
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.vm.isSelectedProject(mockProjects[0])).toBe(false);
|
||||
expect(findAllDropdownItems().at(0).props('isChecked')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('template', () => {
|
||||
const findDropdownEl = () => wrapper.findComponent(GlDropdown);
|
||||
|
||||
it('renders collapsed state element with icon', () => {
|
||||
const collapsedEl = wrapper.find('[data-testid="move-collapsed"]');
|
||||
const collapsedEl = findCollapsedEl();
|
||||
|
||||
expect(collapsedEl.exists()).toBe(true);
|
||||
expect(collapsedEl.attributes('title')).toBe(mockProps.dropdownButtonTitle);
|
||||
@ -197,12 +213,11 @@ describe('IssuableMoveDropdown', () => {
|
||||
|
||||
it('renders disabled dropdown when `disabled` is true', () => {
|
||||
createComponent({ ...mockProps, disabled: true });
|
||||
|
||||
expect(findDropdownEl().attributes('disabled')).toBe('true');
|
||||
expect(findDropdownEl().props('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('renders header element', () => {
|
||||
const headerEl = findDropdownEl().find('[data-testid="header"]');
|
||||
const headerEl = findHeader();
|
||||
|
||||
expect(headerEl.exists()).toBe(true);
|
||||
expect(headerEl.find('span').text()).toBe(mockProps.dropdownHeaderTitle);
|
||||
@ -220,125 +235,87 @@ describe('IssuableMoveDropdown', () => {
|
||||
});
|
||||
|
||||
it('renders gl-loading-icon component when projectsListLoading prop is true', async () => {
|
||||
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
wrapper.setData({
|
||||
projectsListLoading: true,
|
||||
});
|
||||
|
||||
findDropdownEl().vm.$emit('shown');
|
||||
await nextTick();
|
||||
|
||||
expect(findDropdownEl().findComponent(GlLoadingIcon).exists()).toBe(true);
|
||||
expect(findLoadingIcon().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders gl-dropdown-item components for available projects', async () => {
|
||||
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
wrapper.setData({
|
||||
projects: mockProjects,
|
||||
selectedProject: mockProjects[0],
|
||||
});
|
||||
findDropdownEl().vm.$emit('shown');
|
||||
await waitForPromises();
|
||||
|
||||
findAllDropdownItems().at(0).vm.$emit('click', mockEvent);
|
||||
await nextTick();
|
||||
|
||||
const dropdownItems = wrapper.findAllComponents(GlDropdownItem);
|
||||
|
||||
expect(dropdownItems).toHaveLength(mockProjects.length);
|
||||
expect(dropdownItems.at(0).props()).toMatchObject({
|
||||
expect(findAllDropdownItems()).toHaveLength(mockProjects.length);
|
||||
expect(findAllDropdownItems().at(0).props()).toMatchObject({
|
||||
isCheckItem: true,
|
||||
isChecked: true,
|
||||
});
|
||||
expect(dropdownItems.at(0).text()).toBe(mockProjects[0].name_with_namespace);
|
||||
expect(findAllDropdownItems().at(0).text()).toBe(mockProjects[0].name_with_namespace);
|
||||
});
|
||||
|
||||
it('renders string "No matching results" when search does not yield any matches', async () => {
|
||||
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
wrapper.setData({
|
||||
searchKey: 'foo',
|
||||
});
|
||||
mock.onGet(mockProps.projectsFetchPath).reply(HTTP_STATUS_OK, []);
|
||||
|
||||
// Wait for `searchKey` watcher to run.
|
||||
await nextTick();
|
||||
findSearchBox().vm.$emit('input', 'foo');
|
||||
await waitForPromises();
|
||||
|
||||
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
wrapper.setData({
|
||||
projects: [],
|
||||
projectsListLoading: false,
|
||||
});
|
||||
|
||||
await nextTick();
|
||||
|
||||
const dropdownContentEl = wrapper.find('[data-testid="content"]');
|
||||
|
||||
expect(dropdownContentEl.text()).toContain('No matching results');
|
||||
expect(findDropdownContent().text()).toContain('No matching results');
|
||||
});
|
||||
|
||||
it('renders string "Failed to load projects" when loading projects list fails', async () => {
|
||||
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
wrapper.setData({
|
||||
projects: [],
|
||||
projectsListLoading: false,
|
||||
projectsListLoadFailed: true,
|
||||
});
|
||||
mock.onGet(mockProps.projectsFetchPath).reply(HTTP_STATUS_OK, []);
|
||||
jest.spyOn(axios, 'get').mockRejectedValue({});
|
||||
|
||||
await nextTick();
|
||||
findDropdownEl().vm.$emit('shown');
|
||||
await waitForPromises();
|
||||
|
||||
const dropdownContentEl = wrapper.find('[data-testid="content"]');
|
||||
|
||||
expect(dropdownContentEl.text()).toContain('Failed to load projects');
|
||||
expect(findDropdownContent().text()).toContain('Failed to load projects');
|
||||
});
|
||||
|
||||
it('renders gl-button within footer', async () => {
|
||||
const moveButtonEl = wrapper.find('[data-testid="footer"]').findComponent(GlButton);
|
||||
const moveButtonEl = findFooter().findComponent(GlButton);
|
||||
|
||||
expect(moveButtonEl.text()).toBe('Move');
|
||||
expect(moveButtonEl.attributes('disabled')).toBe('true');
|
||||
|
||||
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
wrapper.setData({
|
||||
selectedProject: mockProjects[0],
|
||||
});
|
||||
findDropdownEl().vm.$emit('shown');
|
||||
await waitForPromises();
|
||||
|
||||
findAllDropdownItems().at(0).vm.$emit('click', mockEvent);
|
||||
await nextTick();
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-testid="footer"]').findComponent(GlButton).attributes('disabled'),
|
||||
).not.toBeDefined();
|
||||
expect(findFooter().findComponent(GlButton).attributes('disabled')).not.toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('events', () => {
|
||||
it('collapsed state element emits `toggle-collapse` event on component when clicked', () => {
|
||||
wrapper.find('[data-testid="move-collapsed"]').trigger('click');
|
||||
findCollapsedEl().trigger('click');
|
||||
|
||||
expect(wrapper.emitted('toggle-collapse')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('gl-dropdown component calls `fetchProjects` on `shown` event', () => {
|
||||
jest.spyOn(axios, 'get').mockResolvedValue({
|
||||
data: mockProjects,
|
||||
});
|
||||
jest.spyOn(axios, 'get');
|
||||
|
||||
findDropdownEl().vm.$emit('shown');
|
||||
|
||||
expect(axios.get).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('gl-dropdown component prevents dropdown body from closing on `hide` event when `projectItemClick` prop is true', () => {
|
||||
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
wrapper.setData({
|
||||
projectItemClick: true,
|
||||
});
|
||||
it('gl-dropdown component prevents dropdown body from closing on `hide` event when `projectItemClick` prop is true', async () => {
|
||||
findDropdownEl().vm.$emit('shown');
|
||||
await waitForPromises();
|
||||
|
||||
findAllDropdownItems().at(0).vm.$emit('click', mockEvent);
|
||||
await nextTick();
|
||||
|
||||
findDropdownEl().vm.$emit('hide', mockEvent);
|
||||
|
||||
expect(mockEvent.preventDefault).toHaveBeenCalled();
|
||||
expect(wrapper.vm.projectItemClick).toBe(false);
|
||||
});
|
||||
|
||||
it('gl-dropdown component emits `dropdown-close` event on component from `hide` event', () => {
|
||||
@ -347,38 +324,33 @@ describe('IssuableMoveDropdown', () => {
|
||||
expect(wrapper.emitted('dropdown-close')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('close icon in dropdown header closes the dropdown when clicked', () => {
|
||||
wrapper.find('[data-testid="header"]').findComponent(GlButton).vm.$emit('click', mockEvent);
|
||||
it('close icon in dropdown header closes the dropdown when clicked', async () => {
|
||||
findHeader().findComponent(GlButton).vm.$emit('click', mockEvent);
|
||||
|
||||
expect(wrapper.vm.$refs.dropdown.hide).toHaveBeenCalled();
|
||||
await nextTick();
|
||||
expect(hideMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('sets project for clicked gl-dropdown-item to selectedProject', async () => {
|
||||
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
wrapper.setData({
|
||||
projects: mockProjects,
|
||||
});
|
||||
findDropdownEl().vm.$emit('shown');
|
||||
await waitForPromises();
|
||||
|
||||
findAllDropdownItems().at(0).vm.$emit('click', mockEvent);
|
||||
await nextTick();
|
||||
|
||||
wrapper.findAllComponents(GlDropdownItem).at(0).vm.$emit('click', mockEvent);
|
||||
|
||||
expect(wrapper.vm.selectedProject).toBe(mockProjects[0]);
|
||||
expect(findAllDropdownItems().at(0).props('isChecked')).toBe(true);
|
||||
});
|
||||
|
||||
it('hides dropdown and emits `move-issuable` event when move button is clicked', async () => {
|
||||
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
wrapper.setData({
|
||||
selectedProject: mockProjects[0],
|
||||
});
|
||||
findDropdownEl().vm.$emit('shown');
|
||||
await waitForPromises();
|
||||
|
||||
findAllDropdownItems().at(0).vm.$emit('click', mockEvent);
|
||||
await nextTick();
|
||||
|
||||
wrapper.find('[data-testid="footer"]').findComponent(GlButton).vm.$emit('click');
|
||||
findFooter().findComponent(GlButton).vm.$emit('click');
|
||||
|
||||
expect(wrapper.vm.$refs.dropdown.hide).toHaveBeenCalled();
|
||||
expect(hideMock).toHaveBeenCalled();
|
||||
expect(wrapper.emitted('move-issuable')).toHaveLength(1);
|
||||
expect(wrapper.emitted('move-issuable')[0]).toEqual([mockProjects[0]]);
|
||||
});
|
||||
|
@ -343,6 +343,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: Remove this with https://gitlab.com/gitlab-org/gitlab/-/issues/334253
|
||||
context 'when job filtered by job_age' do
|
||||
let!(:job) do
|
||||
create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0, queued_at: 60.seconds.ago)
|
||||
@ -355,20 +356,48 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
|
||||
context 'job is queued less than job_age parameter' do
|
||||
let(:job_age) { 120 }
|
||||
|
||||
it 'gives 204' do
|
||||
it 'ignores the param and picks the job' do
|
||||
request_job(job_age: job_age)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:no_content)
|
||||
expect(response).to have_gitlab_http_status(:created)
|
||||
expect(json_response['id']).to eq(job.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'job is queued more than job_age parameter' do
|
||||
let(:job_age) { 30 }
|
||||
|
||||
it 'picks a job' do
|
||||
it 'ignores the param and picks the job' do
|
||||
request_job(job_age: job_age)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:created)
|
||||
expect(json_response['id']).to eq(job.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when FF remove_job_age_from_jobs_api is disabled' do
|
||||
before do
|
||||
stub_feature_flags(remove_job_age_from_jobs_api: false)
|
||||
end
|
||||
|
||||
context 'job is queued less than job_age parameter' do
|
||||
let(:job_age) { 120 }
|
||||
|
||||
it 'gives 204' do
|
||||
request_job(job_age: job_age)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:no_content)
|
||||
end
|
||||
end
|
||||
|
||||
context 'job is queued more than job_age parameter' do
|
||||
let(:job_age) { 30 }
|
||||
|
||||
it 'picks a job' do
|
||||
request_job(job_age: job_age)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:created)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
Reference in New Issue
Block a user