Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-07-22 18:08:56 +00:00
parent 632d073475
commit c25c2a508b
23 changed files with 298 additions and 358 deletions

View File

@ -19,6 +19,7 @@ export default {
scope: __('Scope'),
scopeDescription: __('Issues must match this scope to appear in this list.'),
selected: __('Selected'),
requiredFieldFeedback: __('This field is required.'),
},
components: {
GlButton,
@ -55,12 +56,21 @@ export default {
data() {
return {
searchValue: '',
selectedIdValid: true,
};
},
computed: {
toggleClassList() {
return `gl-max-w-full gl-display-flex gl-align-items-center gl-text-trunate ${
this.selectedIdValid ? '' : 'gl-inset-border-1-red-400!'
}`;
},
},
watch: {
selectedId(val) {
if (val) {
this.$refs.dropdown.hide(true);
this.selectedIdValid = true;
}
},
},
@ -74,6 +84,13 @@ export default {
this.$emit('filter-items', '');
this.$emit('hide');
},
onSubmit() {
if (!this.selectedId) {
this.selectedIdValid = false;
} else {
this.$emit('add-list');
}
},
},
};
</script>
@ -103,11 +120,16 @@ export default {
<slot name="select-list-type"></slot>
<gl-form-group class="gl-px-5 lg-mb-3 gl-max-w-full" :label="searchLabel">
<gl-form-group
class="gl-px-5 lg-mb-3 gl-max-w-full"
:label="searchLabel"
:state="selectedIdValid"
:invalid-feedback="$options.i18n.requiredFieldFeedback"
>
<gl-dropdown
ref="dropdown"
class="gl-mb-3 gl-max-w-full"
toggle-class="gl-max-w-full gl-display-flex gl-align-items-center gl-text-trunate"
:toggle-class="toggleClassList"
boundary="viewport"
@shown="setFocus"
@hide="onHide"
@ -147,10 +169,9 @@ export default {
<div class="gl-display-flex gl-mb-4">
<gl-button
data-testid="addNewColumnButton"
:disabled="!selectedId"
variant="confirm"
class="gl-mr-3 gl-ml-4"
@click="$emit('add-list')"
@click="onSubmit"
>{{ $options.i18n.add }}</gl-button
>
<gl-button data-testid="cancelAddNewColumn" @click="setAddColumnFormVisibility(false)">{{

View File

@ -74,7 +74,7 @@ export default {
<div
v-else-if="!isLoading && showTests"
ref="container"
class="position-relative"
class="gl-relative"
data-testid="tests-detail"
>
<transition
@ -82,13 +82,13 @@ export default {
@before-enter="beforeEnterTransition"
@after-leave="afterLeaveTransition"
>
<div v-if="showSuite" key="detail" class="w-100 slide-enter-to-element">
<div v-if="showSuite" key="detail" class="gl-w-full slide-enter-to-element">
<test-summary :report="getSelectedSuite" show-back @on-back-click="summaryBackClick" />
<test-suite-table />
</div>
<div v-else key="summary" class="w-100 slide-enter-from-element">
<div v-else key="summary" class="gl-w-full slide-enter-from-element">
<test-summary :report="testReports" />
<test-summary-table @row-click="summaryTableRowClick" />

View File

@ -80,7 +80,10 @@ export default {
<h4>{{ heading }}</h4>
</div>
</div>
<div role="row" class="gl-responsive-table-row table-row-header font-weight-bold fgray">
<div
role="row"
class="gl-responsive-table-row table-row-header gl-font-weight-bold gl-fill-gray-700"
>
<div role="rowheader" class="table-section section-20">
{{ __('Suite') }}
</div>
@ -104,7 +107,7 @@ export default {
<div
v-for="(testCase, index) in getSuiteTests"
:key="index"
class="gl-responsive-table-row rounded align-items-md-start"
class="gl-responsive-table-row gl-rounded-base gl-align-items-flex-start"
data-testid="test-case-row"
>
<div class="table-section section-20 section-wrap">
@ -142,11 +145,8 @@ export default {
<div class="table-section section-10 section-wrap">
<div role="rowheader" class="table-mobile-header">{{ __('Status') }}</div>
<div class="table-mobile-content text-center">
<div
class="ci-status-icon d-flex align-items-center justify-content-end justify-content-md-center"
:class="`ci-status-icon-${testCase.status}`"
>
<div class="table-mobile-content gl-md-display-flex gl-justify-content-center">
<div class="ci-status-icon" :class="`ci-status-icon-${testCase.status}`">
<gl-icon :size="24" :name="testCase.icon" />
</div>
</div>
@ -156,7 +156,7 @@ export default {
<div role="rowheader" class="table-mobile-header">
{{ __('Duration') }}
</div>
<div class="table-mobile-content pr-sm-1">
<div class="table-mobile-content gl-sm-pr-2">
{{ testCase.formattedTime }}
</div>
</div>

View File

@ -65,58 +65,53 @@ export default {
<template>
<div>
<div class="row">
<div class="col-12 d-flex gl-mt-3 align-items-center">
<gl-button
v-if="showBack"
size="small"
class="gl-mr-3 js-back-button"
icon="chevron-lg-left"
:aria-label="__('Go back')"
@click="onBackClick"
/>
<div class="gl-w-full gl-display-flex gl-mt-3 gl-align-items-center">
<gl-button
v-if="showBack"
size="small"
class="gl-mr-3 js-back-button"
icon="chevron-lg-left"
:aria-label="__('Go back')"
@click="onBackClick"
/>
<h4>{{ heading }}</h4>
</div>
<h4>{{ heading }}</h4>
</div>
<div class="row mt-2">
<div class="col-4 col-md">
<span class="js-total-tests">{{
<div
class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row gl-w-full gl-mt-3"
>
<div class="gl-display-flex gl-justify-content-space-between gl-flex-basis-half">
<span class="js-total-tests gl-flex-grow-1">{{
sprintf(s__('TestReports|%{count} tests'), { count: report.total_count })
}}</span>
</div>
<div class="col-4 col-md text-center text-md-center">
<span class="js-failed-tests">{{
<span class="js-failed-tests gl-flex-grow-1">{{
sprintf(s__('TestReports|%{count} failures'), { count: report.failed_count })
}}</span>
</div>
<div class="col-4 col-md text-right text-md-center">
<span class="js-errored-tests">{{
sprintf(s__('TestReports|%{count} errors'), { count: report.error_count })
}}</span>
</div>
<div class="col-6 mt-3 col-md mt-md-0 text-md-center">
<span class="js-success-rate">{{
<div class="gl-display-flex gl-justify-content-space-between gl-flex-grow-1">
<div class="gl-display-none gl-md-display-block gl-flex-grow-1"></div>
<span class="js-success-rate gl-flex-grow-1">{{
sprintf(s__('TestReports|%{rate}%{sign} success rate'), {
rate: successPercentage,
sign: '%',
})
}}</span>
</div>
<div class="col-6 mt-3 col-md mt-md-0 text-right">
<span class="js-duration">{{ formattedDuration }}</span>
</div>
</div>
<div class="row mt-3">
<div class="col-12">
<gl-progress-bar :value="successPercentage" :variant="progressBarVariant" height="10px" />
</div>
</div>
<gl-progress-bar
class="gl-mt-5"
:value="successPercentage"
:variant="progressBarVariant"
height="10px"
/>
</div>
</template>

View File

@ -34,33 +34,31 @@ export default {
<template>
<div>
<div class="row gl-mt-3">
<div class="col-12">
<h4>{{ heading }}</h4>
</div>
<div class="gl-mt-5">
<h4>{{ heading }}</h4>
</div>
<div v-if="hasSuites" class="test-reports-table gl-mb-3 js-test-suites-table">
<div role="row" class="gl-responsive-table-row table-row-header font-weight-bold">
<div role="rowheader" class="table-section section-25 pl-3">
<div v-if="hasSuites" class="js-test-suites-table">
<div role="row" class="gl-responsive-table-row table-row-header gl-font-weight-bold">
<div role="rowheader" class="table-section section-25 gl-pl-5">
{{ __('Job') }}
</div>
<div role="rowheader" class="table-section section-25">
{{ __('Duration') }}
</div>
<div role="rowheader" class="table-section section-10 text-center">
<div role="rowheader" class="table-section section-10 gl-text-center">
{{ __('Failed') }}
</div>
<div role="rowheader" class="table-section section-10 text-center">
<div role="rowheader" class="table-section section-10 gl-text-center">
{{ __('Errors'), }}
</div>
<div role="rowheader" class="table-section section-10 text-center">
<div role="rowheader" class="table-section section-10 gl-text-center">
{{ __('Skipped'), }}
</div>
<div role="rowheader" class="table-section section-10 text-center">
<div role="rowheader" class="table-section section-10 gl-text-center">
{{ __('Passed'), }}
</div>
<div role="rowheader" class="table-section section-10 pr-3 text-right">
<div role="rowheader" class="table-section section-10 gl-pr-5 gl-text-right">
{{ __('Total') }}
</div>
</div>
@ -69,17 +67,17 @@ export default {
v-for="(testSuite, index) in getTestSuites"
:key="index"
role="row"
class="gl-responsive-table-row test-reports-summary-row rounded js-suite-row"
class="gl-responsive-table-row gl-rounded-base js-suite-row"
:class="{
'gl-responsive-table-row-clickable cursor-pointer': !testSuite.suite_error,
'gl-responsive-table-row-clickable gl-cursor-pointer': !testSuite.suite_error,
}"
@click="tableRowClick(index)"
>
<div class="table-section section-25">
<div role="rowheader" class="table-mobile-header font-weight-bold">
<div role="rowheader" class="table-mobile-header gl-font-weight-bold">
{{ __('Suite') }}
</div>
<div class="table-mobile-content underline cgray pl-3">
<div class="table-mobile-content underline gl-text-gray-900 gl-pl-5">
{{ testSuite.name }}
<gl-icon
v-if="testSuite.suite_error"
@ -93,44 +91,44 @@ export default {
</div>
<div class="table-section section-25">
<div role="rowheader" class="table-mobile-header font-weight-bold">
<div role="rowheader" class="table-mobile-header gl-font-weight-bold">
{{ __('Duration') }}
</div>
<div class="table-mobile-content text-md-left">
<div class="table-mobile-content gl-text-left">
{{ testSuite.formattedTime }}
</div>
</div>
<div class="table-section section-10 text-center">
<div role="rowheader" class="table-mobile-header font-weight-bold">
<div class="table-section section-10 gl-text-center">
<div role="rowheader" class="table-mobile-header gl-font-weight-bold">
{{ __('Failed') }}
</div>
<div class="table-mobile-content">{{ testSuite.failed_count }}</div>
</div>
<div class="table-section section-10 text-center">
<div role="rowheader" class="table-mobile-header font-weight-bold">
<div class="table-section section-10 gl-text-center">
<div role="rowheader" class="table-mobile-header gl-font-weight-bold">
{{ __('Errors') }}
</div>
<div class="table-mobile-content">{{ testSuite.error_count }}</div>
</div>
<div class="table-section section-10 text-center">
<div role="rowheader" class="table-mobile-header font-weight-bold">
<div class="table-section section-10 gl-text-center">
<div role="rowheader" class="table-mobile-header gl-font-weight-bold">
{{ __('Skipped') }}
</div>
<div class="table-mobile-content">{{ testSuite.skipped_count }}</div>
</div>
<div class="table-section section-10 text-center">
<div role="rowheader" class="table-mobile-header font-weight-bold">
<div class="table-section section-10 gl-text-center">
<div role="rowheader" class="table-mobile-header gl-font-weight-bold">
{{ __('Passed') }}
</div>
<div class="table-mobile-content">{{ testSuite.success_count }}</div>
</div>
<div class="table-section section-10 text-right pr-md-3">
<div role="rowheader" class="table-mobile-header font-weight-bold">
<div class="table-section section-10 gl-text-right pr-md-3">
<div role="rowheader" class="table-mobile-header gl-font-weight-bold">
{{ __('Total') }}
</div>
<div class="table-mobile-content">{{ testSuite.total_count }}</div>

View File

@ -5,7 +5,6 @@ fragment ListItemShared on CiRunner {
runnerType
shortSha
version
revision
ipAddress
active
locked

View File

@ -225,12 +225,6 @@
}
}
.test-reports-table {
.build-log {
@include build-log();
}
}
.progress-bar.bg-primary {
background-color: var(--blue-500, $blue-500) !important;
}

View File

@ -57,14 +57,14 @@ class WebHook < ApplicationRecord
!temporarily_disabled? && !permanently_disabled?
end
def temporarily_disabled?(ignore_flag: false)
return false unless ignore_flag || web_hooks_disable_failed?
def temporarily_disabled?
return false unless web_hooks_disable_failed?
disabled_until.present? && disabled_until >= Time.current
end
def permanently_disabled?(ignore_flag: false)
return false unless ignore_flag || web_hooks_disable_failed?
def permanently_disabled?
return false unless web_hooks_disable_failed?
recent_failures > FAILURE_THRESHOLD
end
@ -126,13 +126,6 @@ class WebHook < ApplicationRecord
save(validate: false)
end
def active_state(ignore_flag: false)
return :permanently_disabled if permanently_disabled?(ignore_flag: ignore_flag)
return :temporarily_disabled if temporarily_disabled?(ignore_flag: ignore_flag)
:enabled
end
# @return [Boolean] Whether or not the WebHook is currently throttled.
def rate_limited?
rate_limiter.rate_limited?

View File

@ -14,7 +14,6 @@ module WebHooks
@hook = hook
@log_data = log_data.transform_keys(&:to_sym)
@response_category = response_category
@prev_state = hook.active_state(ignore_flag: true)
end
def execute
@ -43,36 +42,12 @@ module WebHooks
hook.failed!
end
log_state_change
hook.update_last_failure
end
rescue Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError
raise if raise_lock_error?
end
def log_state_change
new_state = hook.active_state(ignore_flag: true)
return if @prev_state == new_state
Gitlab::AuthLogger.info(
message: 'WebHook change active_state',
# identification
hook_id: hook.id,
hook_type: hook.type,
project_id: hook.project_id,
group_id: hook.group_id,
# relevant data
prev_state: @prev_state,
new_state: new_state,
duration: log_data[:execution_duration],
response_status: log_data[:response_status],
recent_hook_failures: hook.recent_failures,
# context
**Gitlab::ApplicationContext.current
)
end
def lock_name
"web_hooks:update_hook_failure_state:#{hook.id}"
end

View File

@ -34,9 +34,10 @@
= display_issuable_type
- unless current_controller?('conflicts')
- if current_user && moved_mr_sidebar_enabled? && !@merge_request.merged?
%li.gl-new-dropdown-divider
%hr.dropdown-divider
- if current_user && moved_mr_sidebar_enabled?
- if !@merge_request.merged?
%li.gl-new-dropdown-divider
%hr.dropdown-divider
%li.gl-new-dropdown-item.js-sidebar-subscriptions-entry-point
- unless issuable_author_is_current_user(@merge_request)
%li.gl-new-dropdown-item

View File

@ -42,7 +42,7 @@
gitlab-com: true
packages: [Free, Premium, Ultimate]
url: https://docs.gitlab.com/ee/ci/runners/configure_runners.html#artifact-attestation
image_url: https://www.youtube.com/embed/MlIdqrDgI8U
image_url: https://img.youtube.com/vi/MlIdqrDgI8U/hqdefault.jpg
published_at: 2022-06-22
release: 15.1
- title: "Link to included CI/CD configuration from the pipeline editor"
@ -54,6 +54,6 @@
gitlab-com: true
packages: [Free, Premium, Ultimate]
url: https://docs.gitlab.com/ee/ci/pipeline_editor/
image_url: https://www.youtube.com/embed/7BNDUYfY_ok
image_url: https://img.youtube.com/vi/7BNDUYfY_ok/hqdefault.jpg
published_at: 2022-06-22
release: 15.1

View File

@ -253,6 +253,10 @@ then `artifacts:reports:dependency_scanning` must be set to `depscan.json`.
Following the POSIX exit code standard, the scanner exits with 0 for success and any number from 1 to 255 for anything else.
Success also includes the case when vulnerabilities are found.
When a CI job fails, security report results are not ingested by GitLab, even if the job
[allows failure](../../ci/yaml/#allow_failure). The report artifacts are still uploaded to GitLab and available
for [download in the pipeline security tab](../../user/application_security/vulnerability_report/pipeline.md#download-security-scan-outputs).
When executing a scanning job using the [Docker-in-Docker privileged mode](../../user/application_security/sast/index.md#requirements),
we reserve the following standard exit codes.

View File

@ -101,7 +101,7 @@ RSpec.describe 'Admin Appearance' do
context 'Profile page with custom profile image guidelines' do
before do
sign_in(create(:admin))
sign_in(admin)
gitlab_enable_admin_mode_sign_in(admin)
visit admin_application_settings_appearances_path
fill_in 'appearance_profile_image_guidelines', with: 'Custom profile image guidelines, please :smile:!'

View File

@ -30,7 +30,7 @@ RSpec.describe 'Profile account page', :js do
it 'deletes user', :js, :sidekiq_might_not_need_inline do
click_button 'Delete account'
fill_in 'password', with: '12345678'
fill_in 'password', with: user.password
page.within '.modal' do
click_button 'Delete account'

View File

@ -13,6 +13,7 @@ RSpec.describe 'Profile > Password' do
end
context 'Password authentication enabled' do
let(:new_password) { User.random_password }
let(:user) { create(:user, password_automatically_set: true) }
before do
@ -23,7 +24,7 @@ RSpec.describe 'Profile > Password' do
context 'User with password automatically set' do
describe 'User puts different passwords in the field and in the confirmation' do
it 'shows an error message' do
fill_passwords('mypassword', 'mypassword2')
fill_passwords(new_password, "#{new_password}2")
page.within('.gl-alert-danger') do
expect(page).to have_content("Password confirmation doesn't match Password")
@ -31,7 +32,7 @@ RSpec.describe 'Profile > Password' do
end
it 'does not contain the current password field after an error' do
fill_passwords('mypassword', 'mypassword2')
fill_passwords(new_password, "#{new_password}2")
expect(page).to have_no_field('user[current_password]')
end
@ -39,7 +40,7 @@ RSpec.describe 'Profile > Password' do
describe 'User puts the same passwords in the field and in the confirmation' do
it 'shows a success message' do
fill_passwords('mypassword', 'mypassword')
fill_passwords(new_password, new_password)
page.within('[data-testid="alert-info"]') do
expect(page).to have_content('Password was successfully updated. Please sign in again.')
@ -79,7 +80,7 @@ RSpec.describe 'Profile > Password' do
end
context 'Change password' do
let(:new_password) { '22233344' }
let(:new_password) { User.random_password }
before do
sign_in(user)
@ -156,6 +157,8 @@ RSpec.describe 'Profile > Password' do
end
context 'when password is expired' do
let(:new_password) { User.random_password }
before do
sign_in(user)
@ -170,8 +173,8 @@ RSpec.describe 'Profile > Password' do
expect(page).to have_current_path new_profile_password_path, ignore_query: true
fill_in :user_password, with: user.password
fill_in :user_new_password, with: '12345678'
fill_in :user_password_confirmation, with: '12345678'
fill_in :user_new_password, with: new_password
fill_in :user_password_confirmation, with: new_password
click_button 'Set new password'
expect(page).to have_current_path new_user_session_path, ignore_query: true

View File

@ -49,15 +49,15 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions do
expect(page).to have_current_path edit_user_password_path, ignore_query: true
expect(page).to have_content('Please create a password for your new account.')
fill_in 'user_password', with: 'password'
fill_in 'user_password_confirmation', with: 'password'
fill_in 'user_password', with: user.password
fill_in 'user_password_confirmation', with: user.password
click_button 'Change your password'
expect(page).to have_current_path new_user_session_path, ignore_query: true
expect(page).to have_content(I18n.t('devise.passwords.updated_not_active'))
fill_in 'user_login', with: user.username
fill_in 'user_password', with: 'password'
fill_in 'user_password', with: user.password
click_button 'Sign in'
expect_single_session_with_authenticated_ttl
@ -231,7 +231,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions do
end
it 'does not allow sign-in if the user password is updated before entering a one-time code' do
user.update!(password: 'new_password')
user.update!(password: User.random_password)
enter_code(user.current_otp)
@ -468,7 +468,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions do
visit new_user_session_path
fill_in 'user_login', with: user.email
fill_in 'user_password', with: '12345678'
fill_in 'user_password', with: user.password
click_button 'Sign in'
expect(page).to have_current_path(new_profile_password_path, ignore_query: true)
@ -788,7 +788,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions do
visit new_user_session_path
fill_in 'user_login', with: user.email
fill_in 'user_password', with: '12345678'
fill_in 'user_password', with: user.password
click_button 'Sign in'
@ -809,7 +809,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions do
visit new_user_session_path
fill_in 'user_login', with: user.email
fill_in 'user_password', with: '12345678'
fill_in 'user_password', with: user.password
click_button 'Sign in'
@ -830,7 +830,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions do
visit new_user_session_path
fill_in 'user_login', with: user.email
fill_in 'user_password', with: '12345678'
fill_in 'user_password', with: user.password
click_button 'Sign in'
@ -873,7 +873,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions do
visit new_user_session_path
fill_in 'user_login', with: user.email
fill_in 'user_password', with: '12345678'
fill_in 'user_password', with: user.password
click_button 'Sign in'
fill_in 'user_otp_attempt', with: user.reload.current_otp
@ -899,7 +899,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions do
visit new_user_session_path
fill_in 'user_login', with: user.email
fill_in 'user_password', with: '12345678'
fill_in 'user_password', with: user.password
click_button 'Sign in'
expect_to_be_on_terms_page
@ -907,9 +907,11 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions do
expect(page).to have_current_path(new_profile_password_path, ignore_query: true)
fill_in 'user_password', with: '12345678'
fill_in 'user_new_password', with: 'new password'
fill_in 'user_password_confirmation', with: 'new password'
new_password = User.random_password
fill_in 'user_password', with: user.password
fill_in 'user_new_password', with: new_password
fill_in 'user_password_confirmation', with: new_password
click_button 'Set new password'
expect(page).to have_content('Password successfully changed')

View File

@ -61,7 +61,7 @@ describe('Board card layout', () => {
const formTitle = () => wrapper.findByTestId('board-add-column-form-title').text();
const findSearchInput = () => wrapper.find(GlSearchBoxByType);
const findSearchLabel = () => wrapper.find(GlFormGroup);
const findSearchLabelFormGroup = () => wrapper.find(GlFormGroup);
const cancelButton = () => wrapper.findByTestId('cancelAddNewColumn');
const submitButton = () => wrapper.findByTestId('addNewColumnButton');
const findDropdown = () => wrapper.findComponent(GlDropdown);
@ -121,10 +121,17 @@ describe('Board card layout', () => {
mountComponent(props);
expect(findSearchLabel().attributes('label')).toEqual(props.searchLabel);
expect(findSearchLabelFormGroup().attributes('label')).toEqual(props.searchLabel);
expect(findSearchInput().attributes('placeholder')).toEqual(props.searchPlaceholder);
});
it('does not show the dropdown as invalid by default', () => {
mountComponent();
expect(findSearchLabelFormGroup().attributes('state')).toBe('true');
expect(findDropdown().props('toggleClass')).not.toContain('gl-inset-border-1-red-400!');
});
it('emits filter event on input', () => {
mountComponent();
@ -137,13 +144,13 @@ describe('Board card layout', () => {
});
describe('Add list button', () => {
it('is disabled if no item is selected', () => {
it('is enabled by default', () => {
mountComponent();
expect(submitButton().props('disabled')).toBe(true);
expect(submitButton().props('disabled')).toBe(false);
});
it('emits add-list event on click', () => {
it('emits add-list event on click when an ID is selected', () => {
mountComponent({
selectedId: mockLabelList.label.id,
});
@ -152,5 +159,16 @@ describe('Board card layout', () => {
expect(wrapper.emitted('add-list')).toEqual([[]]);
});
it('does not emit the add-list event on click and shows the dropdown as invalid when no ID is selected', async () => {
mountComponent();
await submitButton().vm.$emit('click');
expect(findSearchLabelFormGroup().attributes('state')).toBeUndefined();
expect(findDropdown().props('toggleClass')).toContain('gl-inset-border-1-red-400!');
expect(wrapper.emitted('add-list')).toBeUndefined();
});
});
});

View File

@ -13,10 +13,10 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
let_it_be(:project) { create(:project, :repository, :public) }
let_it_be(:project_2) { create(:project, :repository, :public) }
let_it_be(:instance_runner) { create(:ci_runner, :instance, version: '1.0.0', revision: '123', description: 'Instance runner', ip_address: '127.0.0.1') }
let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group], active: false, version: '2.0.0', revision: '456', description: 'Group runner', ip_address: '127.0.0.1') }
let_it_be(:group_runner_2) { create(:ci_runner, :group, groups: [group], active: false, version: '2.0.0', revision: '456', description: 'Group runner 2', ip_address: '127.0.0.1') }
let_it_be(:project_runner) { create(:ci_runner, :project, projects: [project, project_2], active: false, version: '2.0.0', revision: '456', description: 'Project runner', ip_address: '127.0.0.1') }
let_it_be(:instance_runner) { create(:ci_runner, :instance, version: '1.0.0', description: 'Instance runner', ip_address: '127.0.0.1') }
let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group], active: false, version: '2.0.0', description: 'Group runner', ip_address: '127.0.0.1') }
let_it_be(:group_runner_2) { create(:ci_runner, :group, groups: [group], active: false, version: '2.0.0', description: 'Group runner 2', ip_address: '127.0.0.1') }
let_it_be(:project_runner) { create(:ci_runner, :project, projects: [project, project_2], active: false, version: '2.0.0', description: 'Project runner', ip_address: '127.0.0.1') }
let_it_be(:build) { create(:ci_build, runner: instance_runner) }
query_path = 'runner/graphql/'

View File

@ -1,86 +1,82 @@
import Vue, { nextTick } from 'vue';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
import { GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import ActivityBar from '~/ide/components/activity_bar.vue';
import { leftSidebarViews } from '~/ide/constants';
import { createStore } from '~/ide/stores';
describe('IDE activity bar', () => {
const Component = Vue.extend(ActivityBar);
let vm;
describe('IDE ActivityBar component', () => {
let wrapper;
let store;
const findChangesBadge = () => vm.$el.querySelector('.badge');
const findChangesBadge = () => wrapper.findComponent(GlBadge);
beforeEach(() => {
const mountComponent = (state) => {
store = createStore();
Vue.set(store.state.projects, 'abcproject', {
web_url: 'testing',
store.replaceState({
...store.state,
projects: { abcproject: { web_url: 'testing' } },
currentProjectId: 'abcproject',
...state,
});
Vue.set(store.state, 'currentProjectId', 'abcproject');
vm = createComponentWithStore(Component, store);
});
wrapper = shallowMount(ActivityBar, { store });
};
afterEach(() => {
vm.$destroy();
wrapper.destroy();
});
describe('updateActivityBarView', () => {
beforeEach(() => {
jest.spyOn(vm, 'updateActivityBarView').mockImplementation(() => {});
vm.$mount();
mountComponent();
jest.spyOn(wrapper.vm, 'updateActivityBarView').mockImplementation(() => {});
});
it('calls updateActivityBarView with edit value on click', () => {
vm.$el.querySelector('.js-ide-edit-mode').click();
wrapper.find('.js-ide-edit-mode').trigger('click');
expect(vm.updateActivityBarView).toHaveBeenCalledWith(leftSidebarViews.edit.name);
expect(wrapper.vm.updateActivityBarView).toHaveBeenCalledWith(leftSidebarViews.edit.name);
});
it('calls updateActivityBarView with commit value on click', () => {
vm.$el.querySelector('.js-ide-commit-mode').click();
wrapper.find('.js-ide-commit-mode').trigger('click');
expect(vm.updateActivityBarView).toHaveBeenCalledWith(leftSidebarViews.commit.name);
expect(wrapper.vm.updateActivityBarView).toHaveBeenCalledWith(leftSidebarViews.commit.name);
});
it('calls updateActivityBarView with review value on click', () => {
vm.$el.querySelector('.js-ide-review-mode').click();
wrapper.find('.js-ide-review-mode').trigger('click');
expect(vm.updateActivityBarView).toHaveBeenCalledWith(leftSidebarViews.review.name);
expect(wrapper.vm.updateActivityBarView).toHaveBeenCalledWith(leftSidebarViews.review.name);
});
});
describe('active item', () => {
beforeEach(() => {
vm.$mount();
});
it('sets edit item active', () => {
expect(vm.$el.querySelector('.js-ide-edit-mode').classList).toContain('active');
mountComponent();
expect(wrapper.find('.js-ide-edit-mode').classes()).toContain('active');
});
it('sets commit item active', async () => {
vm.$store.state.currentActivityView = leftSidebarViews.commit.name;
it('sets commit item active', () => {
mountComponent({ currentActivityView: leftSidebarViews.commit.name });
await nextTick();
expect(vm.$el.querySelector('.js-ide-commit-mode').classList).toContain('active');
expect(wrapper.find('.js-ide-commit-mode').classes()).toContain('active');
});
});
describe('changes badge', () => {
it('is rendered when files are staged', () => {
store.state.stagedFiles = [{ path: '/path/to/file' }];
vm.$mount();
mountComponent({ stagedFiles: [{ path: '/path/to/file' }] });
expect(findChangesBadge()).toBeTruthy();
expect(findChangesBadge().textContent.trim()).toBe('1');
expect(findChangesBadge().exists()).toBe(true);
expect(findChangesBadge().text()).toBe('1');
});
it('is not rendered when no changes are present', () => {
vm.$mount();
expect(findChangesBadge()).toBeFalsy();
mountComponent();
expect(findChangesBadge().exists()).toBe(false);
});
});
});

View File

@ -1,8 +1,8 @@
import { mount } from '@vue/test-utils';
import _ from 'lodash';
import Vue, { nextTick } from 'vue';
import { TEST_HOST } from 'helpers/test_constants';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
import IdeStatusBar from '~/ide/components/ide_status_bar.vue';
import IdeStatusMR from '~/ide/components/ide_status_mr.vue';
import { rightSidebarViews } from '~/ide/constants';
import { createStore } from '~/ide/stores';
import { projectData } from '../mock_data';
@ -13,42 +13,48 @@ const TEST_MERGE_REQUEST_URL = `${TEST_HOST}merge-requests/${TEST_MERGE_REQUEST_
jest.mock('~/lib/utils/poll');
describe('ideStatusBar', () => {
let store;
let vm;
describe('IdeStatusBar component', () => {
let wrapper;
const createComponent = () => {
vm = createComponentWithStore(Vue.extend(IdeStatusBar), store).$mount();
const findMRStatus = () => wrapper.findComponent(IdeStatusMR);
const mountComponent = (state = {}) => {
const store = createStore();
store.replaceState({
...store.state,
currentBranchId: 'main',
currentProjectId: TEST_PROJECT_ID,
projects: {
...store.state.projects,
[TEST_PROJECT_ID]: _.clone(projectData),
},
...state,
});
wrapper = mount(IdeStatusBar, { store });
};
const findMRStatus = () => vm.$el.querySelector('.js-ide-status-mr');
beforeEach(() => {
store = createStore();
store.state.currentProjectId = TEST_PROJECT_ID;
store.state.projects[TEST_PROJECT_ID] = _.clone(projectData);
store.state.currentBranchId = 'main';
});
afterEach(() => {
vm.$destroy();
wrapper.destroy();
});
describe('default', () => {
beforeEach(() => {
createComponent();
});
it('triggers a setInterval', () => {
expect(vm.intervalId).not.toBe(null);
mountComponent();
expect(wrapper.vm.intervalId).not.toBe(null);
});
it('renders the statusbar', () => {
expect(vm.$el.className).toBe('ide-status-bar');
mountComponent();
expect(wrapper.classes()).toEqual(['ide-status-bar']);
});
describe('commitAgeUpdate', () => {
beforeEach(() => {
jest.spyOn(vm, 'commitAgeUpdate').mockImplementation(() => {});
mountComponent();
jest.spyOn(wrapper.vm, 'commitAgeUpdate').mockImplementation(() => {});
});
afterEach(() => {
@ -56,70 +62,82 @@ describe('ideStatusBar', () => {
});
it('gets called every second', () => {
expect(vm.commitAgeUpdate).not.toHaveBeenCalled();
expect(wrapper.vm.commitAgeUpdate).not.toHaveBeenCalled();
jest.advanceTimersByTime(1000);
expect(vm.commitAgeUpdate.mock.calls.length).toEqual(1);
expect(wrapper.vm.commitAgeUpdate.mock.calls).toHaveLength(1);
jest.advanceTimersByTime(1000);
expect(vm.commitAgeUpdate.mock.calls.length).toEqual(2);
expect(wrapper.vm.commitAgeUpdate.mock.calls).toHaveLength(2);
});
});
describe('getCommitPath', () => {
it('returns the path to the commit details', () => {
expect(vm.getCommitPath('abc123de')).toBe('/commit/abc123de');
mountComponent();
expect(wrapper.vm.getCommitPath('abc123de')).toBe('/commit/abc123de');
});
});
describe('pipeline status', () => {
it('opens right sidebar on clicking icon', async () => {
jest.spyOn(vm, 'openRightPane').mockImplementation(() => {});
Vue.set(vm.$store.state.pipelines, 'latestPipeline', {
details: {
status: {
text: 'success',
details_path: 'test',
icon: 'status_success',
it('opens right sidebar on clicking icon', () => {
const pipelines = {
latestPipeline: {
details: {
status: {
text: 'success',
details_path: 'test',
icon: 'status_success',
},
},
commit: {
author_gravatar_url: 'www',
},
},
commit: {
author_gravatar_url: 'www',
},
});
};
mountComponent({ pipelines });
jest.spyOn(wrapper.vm, 'openRightPane').mockImplementation(() => {});
await nextTick();
vm.$el.querySelector('.ide-status-pipeline button').click();
wrapper.find('button').trigger('click');
expect(vm.openRightPane).toHaveBeenCalledWith(rightSidebarViews.pipelines);
expect(wrapper.vm.openRightPane).toHaveBeenCalledWith(rightSidebarViews.pipelines);
});
});
it('does not show merge request status', () => {
expect(findMRStatus()).toBe(null);
mountComponent();
expect(findMRStatus().exists()).toBe(false);
});
});
describe('with merge request in store', () => {
beforeEach(() => {
store.state.projects[TEST_PROJECT_ID].mergeRequests = {
[TEST_MERGE_REQUEST_ID]: {
web_url: TEST_MERGE_REQUEST_URL,
references: {
short: `!${TEST_MERGE_REQUEST_ID}`,
const state = {
currentMergeRequestId: TEST_MERGE_REQUEST_ID,
projects: {
[TEST_PROJECT_ID]: {
..._.clone(projectData),
mergeRequests: {
[TEST_MERGE_REQUEST_ID]: {
web_url: TEST_MERGE_REQUEST_URL,
references: {
short: `!${TEST_MERGE_REQUEST_ID}`,
},
},
},
},
},
};
store.state.currentMergeRequestId = TEST_MERGE_REQUEST_ID;
createComponent();
mountComponent(state);
});
it('shows merge request status', () => {
expect(findMRStatus().textContent.trim()).toEqual(`Merge request !${TEST_MERGE_REQUEST_ID}`);
expect(findMRStatus().querySelector('a').href).toEqual(TEST_MERGE_REQUEST_URL);
expect(findMRStatus().text()).toBe(`Merge request !${TEST_MERGE_REQUEST_ID}`);
expect(findMRStatus().find('a').attributes('href')).toBe(TEST_MERGE_REQUEST_URL);
});
});
});

View File

@ -1,70 +1,66 @@
import Vue, { nextTick } from 'vue';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
import newDropdown from '~/ide/components/new_dropdown/index.vue';
import { mount } from '@vue/test-utils';
import NewDropdown from '~/ide/components/new_dropdown/index.vue';
import Button from '~/ide/components/new_dropdown/button.vue';
import { createStore } from '~/ide/stores';
describe('new dropdown component', () => {
let store;
let vm;
let wrapper;
const findAllButtons = () => wrapper.findAllComponents(Button);
const mountComponent = () => {
const store = createStore();
store.state.currentProjectId = 'abcproject';
store.state.path = '';
store.state.trees['abcproject/mybranch'] = { tree: [] };
wrapper = mount(NewDropdown, {
store,
propsData: {
branch: 'main',
path: '',
mouseOver: false,
type: 'tree',
},
});
};
beforeEach(() => {
store = createStore();
const component = Vue.extend(newDropdown);
vm = createComponentWithStore(component, store, {
branch: 'main',
path: '',
mouseOver: false,
type: 'tree',
});
vm.$store.state.currentProjectId = 'abcproject';
vm.$store.state.path = '';
vm.$store.state.trees['abcproject/mybranch'] = {
tree: [],
};
vm.$mount();
jest.spyOn(vm.$refs.newModal, 'open').mockImplementation(() => {});
mountComponent();
jest.spyOn(wrapper.vm.$refs.newModal, 'open').mockImplementation(() => {});
});
afterEach(() => {
vm.$destroy();
wrapper.destroy();
});
it('renders new file, upload and new directory links', () => {
const buttons = vm.$el.querySelectorAll('.dropdown-menu button');
expect(buttons[0].textContent.trim()).toBe('New file');
expect(buttons[1].textContent.trim()).toBe('Upload file');
expect(buttons[2].textContent.trim()).toBe('New directory');
expect(findAllButtons().at(0).text()).toBe('New file');
expect(findAllButtons().at(1).text()).toBe('Upload file');
expect(findAllButtons().at(2).text()).toBe('New directory');
});
describe('createNewItem', () => {
it('opens modal for a blob when new file is clicked', () => {
vm.$el.querySelectorAll('.dropdown-menu button')[0].click();
findAllButtons().at(0).trigger('click');
expect(vm.$refs.newModal.open).toHaveBeenCalledWith('blob', '');
expect(wrapper.vm.$refs.newModal.open).toHaveBeenCalledWith('blob', '');
});
it('opens modal for a tree when new directory is clicked', () => {
vm.$el.querySelectorAll('.dropdown-menu button')[2].click();
findAllButtons().at(2).trigger('click');
expect(vm.$refs.newModal.open).toHaveBeenCalledWith('tree', '');
expect(wrapper.vm.$refs.newModal.open).toHaveBeenCalledWith('tree', '');
});
});
describe('isOpen', () => {
it('scrolls dropdown into view', async () => {
jest.spyOn(vm.$refs.dropdownMenu, 'scrollIntoView').mockImplementation(() => {});
jest.spyOn(wrapper.vm.$refs.dropdownMenu, 'scrollIntoView').mockImplementation(() => {});
vm.isOpen = true;
await wrapper.setProps({ isOpen: true });
await nextTick();
expect(vm.$refs.dropdownMenu.scrollIntoView).toHaveBeenCalledWith({
expect(wrapper.vm.$refs.dropdownMenu.scrollIntoView).toHaveBeenCalledWith({
block: 'nearest',
});
});
@ -72,11 +68,11 @@ describe('new dropdown component', () => {
describe('delete entry', () => {
it('calls delete action', () => {
jest.spyOn(vm, 'deleteEntry').mockImplementation(() => {});
jest.spyOn(wrapper.vm, 'deleteEntry').mockImplementation(() => {});
vm.$el.querySelectorAll('.dropdown-menu button')[4].click();
findAllButtons().at(4).trigger('click');
expect(vm.deleteEntry).toHaveBeenCalledWith('');
expect(wrapper.vm.deleteEntry).toHaveBeenCalledWith('');
});
});
});

View File

@ -482,12 +482,6 @@ RSpec.describe WebHook do
expect(hook).not_to be_temporarily_disabled
end
it 'can ignore the feature flag' do
stub_feature_flags(web_hooks_disable_failed: false)
expect(hook).to be_temporarily_disabled(ignore_flag: true)
end
end
end
@ -510,12 +504,6 @@ RSpec.describe WebHook do
expect(hook).not_to be_permanently_disabled
end
it 'can ignore the feature flag' do
stub_feature_flags(web_hooks_disable_failed: false)
expect(hook).to be_permanently_disabled(ignore_flag: true)
end
end
end

View File

@ -101,27 +101,6 @@ RSpec.describe WebHooks::LogExecutionService do
it 'resets the failure count' do
expect { service.execute }.to change(project_hook, :recent_failures).to(0)
end
it 'sends a message to AuthLogger if the hook as not previously enabled' do
project_hook.update!(recent_failures: ::WebHook::FAILURE_THRESHOLD + 1)
expect(Gitlab::AuthLogger).to receive(:info).with include(
message: 'WebHook change active_state',
# identification
hook_id: project_hook.id,
hook_type: project_hook.type,
project_id: project_hook.project_id,
group_id: nil,
# relevant data
prev_state: :permanently_disabled,
new_state: :enabled,
duration: 1.2,
response_status: '200',
recent_hook_failures: 0
)
service.execute
end
end
end
@ -158,27 +137,6 @@ RSpec.describe WebHooks::LogExecutionService do
expect { service.execute }.not_to change(project_hook, :recent_failures)
end
end
it 'sends a message to AuthLogger if the state would change' do
project_hook.update!(recent_failures: ::WebHook::FAILURE_THRESHOLD)
expect(Gitlab::AuthLogger).to receive(:info).with include(
message: 'WebHook change active_state',
# identification
hook_id: project_hook.id,
hook_type: project_hook.type,
project_id: project_hook.project_id,
group_id: nil,
# relevant data
prev_state: :enabled,
new_state: :permanently_disabled,
duration: (be > 0),
response_status: data[:response_status],
recent_hook_failures: ::WebHook::FAILURE_THRESHOLD + 1
)
service.execute
end
end
context 'when response_category is :error' do
@ -200,25 +158,6 @@ RSpec.describe WebHooks::LogExecutionService do
expect { service.execute }.to change(project_hook, :backoff_count).by(1)
end
it 'sends a message to AuthLogger if the state would change' do
expect(Gitlab::AuthLogger).to receive(:info).with include(
message: 'WebHook change active_state',
# identification
hook_id: project_hook.id,
hook_type: project_hook.type,
project_id: project_hook.project_id,
group_id: nil,
# relevant data
prev_state: :enabled,
new_state: :temporarily_disabled,
duration: (be > 0),
response_status: data[:response_status],
recent_hook_failures: 0
)
service.execute
end
context 'when the previous cool-off was near the maximum' do
before do
project_hook.update!(disabled_until: 5.minutes.ago, backoff_count: 8)