Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot
2023-04-21 06:09:06 +00:00
parent 22400f4dd0
commit 7e6efee3b3
22 changed files with 285 additions and 226 deletions

View File

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

View File

@ -41,6 +41,7 @@ export default {
item.extraAttrs = {
...USER_MENU_TRACKING_DEFAULTS,
'data-track-label': 'user_profile',
'data-qa-selector': 'user_profile_link',
};
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
e29be6311d828a76c594cf350d5212fac9913362dd3e9b96fda6f74c50edfcdb

View File

@ -0,0 +1 @@
74b9c628c09856c3285452be85a853103e7b7860d1e33df664bdcae927f690d1

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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