Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
c859c3bfd2
commit
d91f521169
29 changed files with 359 additions and 206 deletions
65
.gitlab/issue_templates/QA failure.md
Normal file
65
.gitlab/issue_templates/QA failure.md
Normal file
|
@ -0,0 +1,65 @@
|
|||
<!---
|
||||
Before opening a new QA failure issue, make sure to first search for it in the
|
||||
QA failures board: https://gitlab.com/groups/gitlab-org/-/boards/1385578
|
||||
|
||||
The issue should have the following:
|
||||
|
||||
- The relative path of the failing spec file in the title, e.g. if the login
|
||||
test fails, include `qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb` in the title.
|
||||
This is required so that existing issues can easily be found by searching for the spec file.
|
||||
- If the issue is about multiple test failures, include the path for each failing spec file in the description.
|
||||
- A link to the failing job.
|
||||
- The stack trace from the job's logs in the "Stack trace" section below.
|
||||
- A screenshot (if available), and HTML capture (if available), in the "Screenshot / HTML page" section below.
|
||||
--->
|
||||
|
||||
### Summary
|
||||
|
||||
|
||||
|
||||
### Stack trace
|
||||
|
||||
```
|
||||
PUT STACK TRACE HERE
|
||||
```
|
||||
|
||||
### Screenshot / HTML page
|
||||
|
||||
<!--
|
||||
Attach the screenshot and HTML snapshot of the page from the job's artifacts:
|
||||
1. Download the job's artifacts and unarchive them.
|
||||
1. Open the `gitlab-qa-run-2020-*/gitlab-{ce,ee}-qa-*/{,ee}/{api,browser_ui}/<path to failed test>` folder.
|
||||
1. Select the `.png` and `.html` files that appears in the job logs (look for `HTML screenshot: /path/to/html/page.html` / `Image screenshot: `/path/to/html/page.png`).
|
||||
1. Drag and drop them here.
|
||||
-->
|
||||
|
||||
### Possible fixes
|
||||
|
||||
|
||||
<!-- Default due date. -->
|
||||
/due in 2 weeks
|
||||
|
||||
<!-- Base labels. -->
|
||||
/label ~Quality ~QA ~bug ~S1
|
||||
|
||||
<!--
|
||||
Choose the stage that appears in the test path, e.g. ~"devops::create" for
|
||||
`qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb`.
|
||||
-->
|
||||
/label ~devops::
|
||||
|
||||
<!--
|
||||
Select a label for where the failure was found, e.g. if the failure occurred in
|
||||
a nightly pipeline, select ~"found:nightly".
|
||||
-->
|
||||
/label ~found:
|
||||
|
||||
<!--
|
||||
https://about.gitlab.com/handbook/engineering/quality/guidelines/#priorities:
|
||||
- ~P1: Tests that are needed to verify fundamental GitLab functionality.
|
||||
- ~P2: Tests that deal with external integrations which may take a longer time to debug and fix.
|
||||
-->
|
||||
/label ~P
|
||||
|
||||
<!-- Select the current milestone if ~P1 or the next milestone if ~P2. -->
|
||||
/milestone %
|
|
@ -741,7 +741,7 @@ GEM
|
|||
parslet (1.8.2)
|
||||
peek (1.1.0)
|
||||
railties (>= 4.0.0)
|
||||
pg (1.1.4)
|
||||
pg (1.2.2)
|
||||
png_quantizator (0.2.1)
|
||||
po_to_json (1.0.1)
|
||||
json (>= 1.6.0)
|
||||
|
|
|
@ -1,20 +1,17 @@
|
|||
<script>
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
import { GlLoadingIcon } from '@gitlab/ui';
|
||||
import { mapActions } from 'vuex';
|
||||
import { FETCH_SETTINGS_ERROR_MESSAGE } from '../constants';
|
||||
|
||||
import SettingsForm from './settings_form.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlLoadingIcon,
|
||||
SettingsForm,
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
isLoading: 'isLoading',
|
||||
}),
|
||||
},
|
||||
mounted() {
|
||||
this.fetchSettings();
|
||||
this.fetchSettings().catch(() =>
|
||||
this.$toast.show(FETCH_SETTINGS_ERROR_MESSAGE, { type: 'error' }),
|
||||
);
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['fetchSettings']),
|
||||
|
@ -37,7 +34,6 @@ export default {
|
|||
}}
|
||||
</li>
|
||||
</ul>
|
||||
<gl-loading-icon v-if="isLoading" ref="loading-icon" size="xl" />
|
||||
<settings-form v-else ref="settings-form" />
|
||||
<settings-form ref="settings-form" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,8 +1,20 @@
|
|||
<script>
|
||||
import { mapActions, mapState } from 'vuex';
|
||||
import { GlFormGroup, GlToggle, GlFormSelect, GlFormTextarea, GlButton, GlCard } from '@gitlab/ui';
|
||||
import {
|
||||
GlFormGroup,
|
||||
GlToggle,
|
||||
GlFormSelect,
|
||||
GlFormTextarea,
|
||||
GlButton,
|
||||
GlCard,
|
||||
GlLoadingIcon,
|
||||
} from '@gitlab/ui';
|
||||
import { s__, __, sprintf } from '~/locale';
|
||||
import { NAME_REGEX_LENGTH } from '../constants';
|
||||
import {
|
||||
NAME_REGEX_LENGTH,
|
||||
UPDATE_SETTINGS_ERROR_MESSAGE,
|
||||
UPDATE_SETTINGS_SUCCESS_MESSAGE,
|
||||
} from '../constants';
|
||||
import { mapComputed } from '~/vuex_shared/bindings';
|
||||
|
||||
export default {
|
||||
|
@ -13,13 +25,14 @@ export default {
|
|||
GlFormTextarea,
|
||||
GlButton,
|
||||
GlCard,
|
||||
GlLoadingIcon,
|
||||
},
|
||||
labelsConfig: {
|
||||
cols: 3,
|
||||
align: 'right',
|
||||
},
|
||||
computed: {
|
||||
...mapState(['formOptions']),
|
||||
...mapState(['formOptions', 'isLoading']),
|
||||
...mapComputed(
|
||||
[
|
||||
'enabled',
|
||||
|
@ -64,15 +77,26 @@ export default {
|
|||
formIsInvalid() {
|
||||
return this.nameRegexState === false;
|
||||
},
|
||||
isFormElementDisabled() {
|
||||
return !this.enabled || this.isLoading;
|
||||
},
|
||||
isSubmitButtonDisabled() {
|
||||
return this.formIsInvalid || this.isLoading;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['resetSettings', 'saveSettings']),
|
||||
submit() {
|
||||
this.saveSettings()
|
||||
.then(() => this.$toast.show(UPDATE_SETTINGS_SUCCESS_MESSAGE, { type: 'success' }))
|
||||
.catch(() => this.$toast.show(UPDATE_SETTINGS_ERROR_MESSAGE, { type: 'error' }));
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form ref="form-element" @submit.prevent="saveSettings" @reset.prevent="resetSettings">
|
||||
<form ref="form-element" @submit.prevent="submit" @reset.prevent="resetSettings">
|
||||
<gl-card>
|
||||
<template #header>
|
||||
{{ s__('ContainerRegistry|Tag expiration policy') }}
|
||||
|
@ -86,7 +110,7 @@ export default {
|
|||
:label="s__('ContainerRegistry|Expiration policy:')"
|
||||
>
|
||||
<div class="d-flex align-items-start">
|
||||
<gl-toggle id="expiration-policy-toggle" v-model="enabled" />
|
||||
<gl-toggle id="expiration-policy-toggle" v-model="enabled" :disabled="isLoading" />
|
||||
<span class="mb-2 ml-1 lh-2" v-html="toggleDescriptionText"></span>
|
||||
</div>
|
||||
</gl-form-group>
|
||||
|
@ -98,7 +122,11 @@ export default {
|
|||
label-for="expiration-policy-interval"
|
||||
:label="s__('ContainerRegistry|Expiration interval:')"
|
||||
>
|
||||
<gl-form-select id="expiration-policy-interval" v-model="older_than" :disabled="!enabled">
|
||||
<gl-form-select
|
||||
id="expiration-policy-interval"
|
||||
v-model="older_than"
|
||||
:disabled="isFormElementDisabled"
|
||||
>
|
||||
<option v-for="option in formOptions.olderThan" :key="option.key" :value="option.key">
|
||||
{{ option.label }}
|
||||
</option>
|
||||
|
@ -112,7 +140,11 @@ export default {
|
|||
label-for="expiration-policy-schedule"
|
||||
:label="s__('ContainerRegistry|Expiration schedule:')"
|
||||
>
|
||||
<gl-form-select id="expiration-policy-schedule" v-model="cadence" :disabled="!enabled">
|
||||
<gl-form-select
|
||||
id="expiration-policy-schedule"
|
||||
v-model="cadence"
|
||||
:disabled="isFormElementDisabled"
|
||||
>
|
||||
<option v-for="option in formOptions.cadence" :key="option.key" :value="option.key">
|
||||
{{ option.label }}
|
||||
</option>
|
||||
|
@ -126,7 +158,11 @@ export default {
|
|||
label-for="expiration-policy-latest"
|
||||
:label="s__('ContainerRegistry|Number of tags to retain:')"
|
||||
>
|
||||
<gl-form-select id="expiration-policy-latest" v-model="keep_n" :disabled="!enabled">
|
||||
<gl-form-select
|
||||
id="expiration-policy-latest"
|
||||
v-model="keep_n"
|
||||
:disabled="isFormElementDisabled"
|
||||
>
|
||||
<option v-for="option in formOptions.keepN" :key="option.key" :value="option.key">
|
||||
{{ option.label }}
|
||||
</option>
|
||||
|
@ -149,7 +185,7 @@ export default {
|
|||
v-model="name_regex"
|
||||
:placeholder="nameRegexPlaceholder"
|
||||
:state="nameRegexState"
|
||||
:disabled="!enabled"
|
||||
:disabled="isFormElementDisabled"
|
||||
trim
|
||||
/>
|
||||
<template #description>
|
||||
|
@ -159,17 +195,18 @@ export default {
|
|||
</template>
|
||||
<template #footer>
|
||||
<div class="d-flex justify-content-end">
|
||||
<gl-button ref="cancel-button" type="reset" class="mr-2 d-block">{{
|
||||
__('Cancel')
|
||||
}}</gl-button>
|
||||
<gl-button ref="cancel-button" type="reset" class="mr-2 d-block" :disabled="isLoading">
|
||||
{{ __('Cancel') }}
|
||||
</gl-button>
|
||||
<gl-button
|
||||
ref="save-button"
|
||||
type="submit"
|
||||
:disabled="formIsInvalid"
|
||||
:disabled="isSubmitButtonDisabled"
|
||||
variant="success"
|
||||
class="d-block"
|
||||
class="d-flex justify-content-center align-items-center js-no-auto-disable"
|
||||
>
|
||||
{{ __('Save expiration policy') }}
|
||||
<gl-loading-icon v-if="isLoading" class="ml-2" />
|
||||
</gl-button>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import Vue from 'vue';
|
||||
import { GlToast } from '@gitlab/ui';
|
||||
import Translate from '~/vue_shared/translate';
|
||||
import store from './store/';
|
||||
import RegistrySettingsApp from './components/registry_settings_app.vue';
|
||||
|
||||
Vue.use(GlToast);
|
||||
Vue.use(Translate);
|
||||
|
||||
export default () => {
|
||||
|
|
|
@ -1,18 +1,10 @@
|
|||
import Api from '~/api';
|
||||
import createFlash from '~/flash';
|
||||
import {
|
||||
FETCH_SETTINGS_ERROR_MESSAGE,
|
||||
UPDATE_SETTINGS_ERROR_MESSAGE,
|
||||
UPDATE_SETTINGS_SUCCESS_MESSAGE,
|
||||
} from '../constants';
|
||||
import * as types from './mutation_types';
|
||||
|
||||
export const setInitialState = ({ commit }, data) => commit(types.SET_INITIAL_STATE, data);
|
||||
export const updateSettings = ({ commit }, data) => commit(types.UPDATE_SETTINGS, data);
|
||||
export const toggleLoading = ({ commit }) => commit(types.TOGGLE_LOADING);
|
||||
export const receiveSettingsSuccess = ({ commit }, data = {}) => commit(types.SET_SETTINGS, data);
|
||||
export const receiveSettingsError = () => createFlash(FETCH_SETTINGS_ERROR_MESSAGE);
|
||||
export const updateSettingsError = () => createFlash(UPDATE_SETTINGS_ERROR_MESSAGE);
|
||||
export const resetSettings = ({ commit }) => commit(types.RESET_SETTINGS);
|
||||
|
||||
export const fetchSettings = ({ dispatch, state }) => {
|
||||
|
@ -21,7 +13,6 @@ export const fetchSettings = ({ dispatch, state }) => {
|
|||
.then(({ data: { container_expiration_policy } }) =>
|
||||
dispatch('receiveSettingsSuccess', container_expiration_policy),
|
||||
)
|
||||
.catch(() => dispatch('receiveSettingsError'))
|
||||
.finally(() => dispatch('toggleLoading'));
|
||||
};
|
||||
|
||||
|
@ -30,11 +21,9 @@ export const saveSettings = ({ dispatch, state }) => {
|
|||
return Api.updateProject(state.projectId, {
|
||||
container_expiration_policy_attributes: state.settings,
|
||||
})
|
||||
.then(({ data: { container_expiration_policy } }) => {
|
||||
dispatch('receiveSettingsSuccess', container_expiration_policy);
|
||||
createFlash(UPDATE_SETTINGS_SUCCESS_MESSAGE, 'success');
|
||||
})
|
||||
.catch(() => dispatch('updateSettingsError'))
|
||||
.then(({ data: { container_expiration_policy } }) =>
|
||||
dispatch('receiveSettingsSuccess', container_expiration_policy),
|
||||
)
|
||||
.finally(() => dispatch('toggleLoading'));
|
||||
};
|
||||
|
||||
|
|
|
@ -124,7 +124,7 @@ class Note < ApplicationRecord
|
|||
scope :inc_author, -> { includes(:author) }
|
||||
scope :inc_relations_for_view, -> do
|
||||
includes(:project, { author: :status }, :updated_by, :resolved_by, :award_emoji,
|
||||
:system_note_metadata, :note_diff_file, :suggestions)
|
||||
{ system_note_metadata: :description_version }, :note_diff_file, :suggestions)
|
||||
end
|
||||
|
||||
scope :with_notes_filter, -> (notes_filter) do
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ProjectCiCdSetting < ApplicationRecord
|
||||
include IgnorableColumns
|
||||
# https://gitlab.com/gitlab-org/gitlab/issues/36651
|
||||
ignore_column :merge_trains_enabled, remove_with: '12.7', remove_after: '2019-12-22'
|
||||
belongs_to :project, inverse_of: :ci_cd_settings
|
||||
|
||||
# The version of the schema that first introduced this model/table.
|
||||
|
|
|
@ -192,3 +192,4 @@
|
|||
- self_monitoring_project_create
|
||||
- self_monitoring_project_delete
|
||||
- merge_request_mergeability_check
|
||||
- phabricator_import_import_tasks
|
||||
|
|
|
@ -19,8 +19,7 @@
|
|||
module Gitlab
|
||||
module PhabricatorImport
|
||||
class BaseWorker
|
||||
include ApplicationWorker
|
||||
include ProjectImportOptions # This marks the project as failed after too many tries
|
||||
include WorkerAttributes
|
||||
include Gitlab::ExclusiveLeaseHelpers
|
||||
|
||||
feature_category :importers
|
|
@ -2,6 +2,9 @@
|
|||
module Gitlab
|
||||
module PhabricatorImport
|
||||
class ImportTasksWorker < BaseWorker
|
||||
include ApplicationWorker
|
||||
include ProjectImportOptions # This marks the project as failed after too many tries
|
||||
|
||||
def importer_class
|
||||
Gitlab::PhabricatorImport::Issues::Importer
|
||||
end
|
5
changelogs/unreleased/sh-bump-pg-gem-1-2-2.yml
Normal file
5
changelogs/unreleased/sh-bump-pg-gem-1-2-2.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Update pg gem to v1.2.2
|
||||
merge_request: 23237
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddDeletedAtToDescriptionVersions < ActiveRecord::Migration[5.2]
|
||||
DOWNTIME = false
|
||||
|
||||
def change
|
||||
add_column :description_versions, :deleted_at, :datetime_with_timezone
|
||||
end
|
||||
end
|
|
@ -1410,6 +1410,7 @@ ActiveRecord::Schema.define(version: 2020_01_21_132641) do
|
|||
t.integer "merge_request_id"
|
||||
t.integer "epic_id"
|
||||
t.text "description"
|
||||
t.datetime_with_timezone "deleted_at"
|
||||
t.index ["epic_id"], name: "index_description_versions_on_epic_id", where: "(epic_id IS NOT NULL)"
|
||||
t.index ["issue_id"], name: "index_description_versions_on_issue_id", where: "(issue_id IS NOT NULL)"
|
||||
t.index ["merge_request_id"], name: "index_description_versions_on_merge_request_id", where: "(merge_request_id IS NOT NULL)"
|
||||
|
|
|
@ -213,7 +213,7 @@ class ChangeUsersUsernameStringToTextCleanup < ActiveRecord::Migration[4.2]
|
|||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
cleanup_concurrent_column_type_change :users
|
||||
cleanup_concurrent_column_type_change :users, :username
|
||||
end
|
||||
|
||||
def down
|
||||
|
|
|
@ -53,14 +53,14 @@ module Gitlab
|
|||
Experimentation.enabled_for_user?(experiment_key, experimentation_subject_index) || forced_enabled?(experiment_key)
|
||||
end
|
||||
|
||||
def track_experiment_event(experiment_key, action)
|
||||
track_experiment_event_for(experiment_key, action) do |tracking_data|
|
||||
def track_experiment_event(experiment_key, action, value = nil)
|
||||
track_experiment_event_for(experiment_key, action, value) do |tracking_data|
|
||||
::Gitlab::Tracking.event(tracking_data.delete(:category), tracking_data.delete(:action), tracking_data)
|
||||
end
|
||||
end
|
||||
|
||||
def frontend_experimentation_tracking_data(experiment_key, action)
|
||||
track_experiment_event_for(experiment_key, action) do |tracking_data|
|
||||
def frontend_experimentation_tracking_data(experiment_key, action, value = nil)
|
||||
track_experiment_event_for(experiment_key, action, value) do |tracking_data|
|
||||
gon.push(tracking_data: tracking_data)
|
||||
end
|
||||
end
|
||||
|
@ -77,19 +77,20 @@ module Gitlab
|
|||
experimentation_subject_id.delete('-').hex % 100
|
||||
end
|
||||
|
||||
def track_experiment_event_for(experiment_key, action)
|
||||
def track_experiment_event_for(experiment_key, action, value)
|
||||
return unless Experimentation.enabled?(experiment_key)
|
||||
|
||||
yield experimentation_tracking_data(experiment_key, action)
|
||||
yield experimentation_tracking_data(experiment_key, action, value)
|
||||
end
|
||||
|
||||
def experimentation_tracking_data(experiment_key, action)
|
||||
def experimentation_tracking_data(experiment_key, action, value)
|
||||
{
|
||||
category: tracking_category(experiment_key),
|
||||
action: action,
|
||||
property: tracking_group(experiment_key),
|
||||
label: experimentation_subject_id
|
||||
}
|
||||
label: experimentation_subject_id,
|
||||
value: value
|
||||
}.compact
|
||||
end
|
||||
|
||||
def tracking_category(experiment_key)
|
||||
|
|
|
@ -78,6 +78,7 @@ module Gitlab
|
|||
clusters_applications_runner: count(::Clusters::Applications::Runner.available),
|
||||
clusters_applications_knative: count(::Clusters::Applications::Knative.available),
|
||||
clusters_applications_elastic_stack: count(::Clusters::Applications::ElasticStack.available),
|
||||
clusters_applications_jupyter: count(::Clusters::Applications::Jupyter.available),
|
||||
in_review_folder: count(::Environment.in_review_folder),
|
||||
grafana_integrated_projects: count(GrafanaIntegration.enabled),
|
||||
groups: count(Group),
|
||||
|
|
|
@ -47,6 +47,7 @@ module QA
|
|||
|
||||
def protect_branch
|
||||
click_element(:protect_button, wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME)
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -23,6 +23,8 @@ module QA
|
|||
def perform(options, *args)
|
||||
extract_address(:gitlab_address, options, args)
|
||||
|
||||
QA::Runtime::Browser.configure!
|
||||
|
||||
Runtime::Feature.enable(options[:enable_feature]) if options.key?(:enable_feature)
|
||||
|
||||
Specs::Runner.perform do |specs|
|
||||
|
|
|
@ -20,6 +20,8 @@ module QA
|
|||
def self.do_perform(address, *rspec_options)
|
||||
Runtime::Scenario.define(:gitlab_address, address)
|
||||
|
||||
QA::Runtime::Browser.configure!
|
||||
|
||||
Specs::Runner.perform do |specs|
|
||||
specs.tty = true
|
||||
specs.options = rspec_options if rspec_options.any?
|
||||
|
|
|
@ -17,10 +17,10 @@ describe 'Project > Settings > CI/CD > Container registry tag expiration policy'
|
|||
expect(settings_block).to have_text 'Container Registry tag expiration policy'
|
||||
end
|
||||
|
||||
it 'Save expiration policy submit the form', :js do
|
||||
it 'Save expiration policy submit the form' do
|
||||
within '#js-registry-policies' do
|
||||
within '.card-body' do
|
||||
click_button(class: 'gl-toggle')
|
||||
find('#expiration-policy-toggle button:not(.is-disabled)').click
|
||||
select('7 days until tags are automatically removed', from: 'expiration-policy-interval')
|
||||
select('Every day', from: 'expiration-policy-schedule')
|
||||
select('50 tags per image name', from: 'expiration-policy-latest')
|
||||
|
@ -30,8 +30,8 @@ describe 'Project > Settings > CI/CD > Container registry tag expiration policy'
|
|||
expect(submit_button).not_to be_disabled
|
||||
submit_button.click
|
||||
end
|
||||
flash_text = find('.flash-text')
|
||||
expect(flash_text).to have_content('Expiration policy successfully saved.')
|
||||
toast = find('.gl-toast')
|
||||
expect(toast).to have_content('Expiration policy successfully saved.')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -161,17 +161,20 @@ exports[`Settings Form renders 1`] = `
|
|||
class="mr-2 d-block"
|
||||
type="reset"
|
||||
>
|
||||
|
||||
Cancel
|
||||
|
||||
</glbutton-stub>
|
||||
|
||||
<glbutton-stub
|
||||
class="d-block"
|
||||
class="d-flex justify-content-center align-items-center js-no-auto-disable"
|
||||
type="submit"
|
||||
variant="success"
|
||||
>
|
||||
|
||||
Save expiration policy
|
||||
|
||||
|
||||
<!---->
|
||||
</glbutton-stub>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,55 +1,55 @@
|
|||
import Vuex from 'vuex';
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import component from '~/registry/settings/components/registry_settings_app.vue';
|
||||
import { createStore } from '~/registry/settings/store/';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
import { FETCH_SETTINGS_ERROR_MESSAGE } from '~/registry/settings/constants';
|
||||
|
||||
describe('Registry Settings App', () => {
|
||||
let wrapper;
|
||||
let store;
|
||||
let fetchSpy;
|
||||
|
||||
const findSettingsComponent = () => wrapper.find({ ref: 'settings-form' });
|
||||
const findLoadingComponent = () => wrapper.find({ ref: 'loading-icon' });
|
||||
|
||||
const mountComponent = (options = {}) => {
|
||||
fetchSpy = jest.fn();
|
||||
const mountComponent = ({ dispatchMock } = {}) => {
|
||||
store = createStore();
|
||||
const dispatchSpy = jest.spyOn(store, 'dispatch');
|
||||
if (dispatchMock) {
|
||||
dispatchSpy[dispatchMock]();
|
||||
}
|
||||
wrapper = shallowMount(component, {
|
||||
store,
|
||||
methods: {
|
||||
fetchSettings: fetchSpy,
|
||||
mocks: {
|
||||
$toast: {
|
||||
show: jest.fn(),
|
||||
},
|
||||
},
|
||||
...options,
|
||||
store,
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore();
|
||||
mountComponent();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it('renders', () => {
|
||||
mountComponent({ dispatchMock: 'mockResolvedValue' });
|
||||
expect(wrapper.element).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('call the store function to load the data on mount', () => {
|
||||
expect(fetchSpy).toHaveBeenCalled();
|
||||
mountComponent({ dispatchMock: 'mockResolvedValue' });
|
||||
expect(store.dispatch).toHaveBeenCalledWith('fetchSettings');
|
||||
});
|
||||
|
||||
it('renders a loader if isLoading is true', () => {
|
||||
store.dispatch('toggleLoading');
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(findLoadingComponent().exists()).toBe(true);
|
||||
expect(findSettingsComponent().exists()).toBe(false);
|
||||
});
|
||||
it('show a toast if fetchSettings fails', () => {
|
||||
mountComponent({ dispatchMock: 'mockRejectedValue' });
|
||||
return wrapper.vm.$nextTick().then(() =>
|
||||
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(FETCH_SETTINGS_ERROR_MESSAGE, {
|
||||
type: 'error',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('renders the setting form', () => {
|
||||
mountComponent({ dispatchMock: 'mockResolvedValue' });
|
||||
expect(findSettingsComponent().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,39 +1,44 @@
|
|||
import Vuex from 'vuex';
|
||||
import { mount, createLocalVue } from '@vue/test-utils';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import stubChildren from 'helpers/stub_children';
|
||||
import component from '~/registry/settings/components/settings_form.vue';
|
||||
import { createStore } from '~/registry/settings/store/';
|
||||
import { NAME_REGEX_LENGTH } from '~/registry/settings/constants';
|
||||
import {
|
||||
NAME_REGEX_LENGTH,
|
||||
UPDATE_SETTINGS_ERROR_MESSAGE,
|
||||
UPDATE_SETTINGS_SUCCESS_MESSAGE,
|
||||
} from '~/registry/settings/constants';
|
||||
import { stringifiedFormOptions } from '../mock_data';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
|
||||
describe('Settings Form', () => {
|
||||
let wrapper;
|
||||
let store;
|
||||
let saveSpy;
|
||||
let resetSpy;
|
||||
let dispatchSpy;
|
||||
|
||||
const findFormGroup = name => wrapper.find(`#expiration-policy-${name}-group`);
|
||||
const findFormElements = (name, father = wrapper) => father.find(`#expiration-policy-${name}`);
|
||||
const FORM_ELEMENTS_ID_PREFIX = '#expiration-policy';
|
||||
|
||||
const GlLoadingIcon = { name: 'gl-loading-icon-stub', template: '<svg></svg>' };
|
||||
|
||||
const findFormGroup = name => wrapper.find(`${FORM_ELEMENTS_ID_PREFIX}-${name}-group`);
|
||||
const findFormElements = (name, parent = wrapper) =>
|
||||
parent.find(`${FORM_ELEMENTS_ID_PREFIX}-${name}`);
|
||||
const findCancelButton = () => wrapper.find({ ref: 'cancel-button' });
|
||||
const findSaveButton = () => wrapper.find({ ref: 'save-button' });
|
||||
const findForm = () => wrapper.find({ ref: 'form-element' });
|
||||
const findLoadingIcon = (parent = wrapper) => parent.find(GlLoadingIcon);
|
||||
|
||||
const mountComponent = (options = {}) => {
|
||||
saveSpy = jest.fn();
|
||||
resetSpy = jest.fn();
|
||||
wrapper = mount(component, {
|
||||
stubs: {
|
||||
...stubChildren(component),
|
||||
GlCard: false,
|
||||
GlLoadingIcon,
|
||||
},
|
||||
mocks: {
|
||||
$toast: {
|
||||
show: jest.fn(),
|
||||
},
|
||||
},
|
||||
store,
|
||||
methods: {
|
||||
saveSettings: saveSpy,
|
||||
resetSettings: resetSpy,
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
@ -41,6 +46,7 @@ describe('Settings Form', () => {
|
|||
beforeEach(() => {
|
||||
store = createStore();
|
||||
store.dispatch('setInitialState', stringifiedFormOptions);
|
||||
dispatchSpy = jest.spyOn(store, 'dispatch');
|
||||
mountComponent();
|
||||
});
|
||||
|
||||
|
@ -59,48 +65,53 @@ describe('Settings Form', () => {
|
|||
${'schedule'} | ${'cadence'} | ${'foo'} | ${'disabled'}
|
||||
${'latest'} | ${'keep_n'} | ${'foo'} | ${'disabled'}
|
||||
${'name-matching'} | ${'name_regex'} | ${'foo'} | ${'disabled'}
|
||||
`('$elementName form element', ({ elementName, modelName, value, disabledByToggle }) => {
|
||||
let formGroup;
|
||||
beforeEach(() => {
|
||||
formGroup = findFormGroup(elementName);
|
||||
});
|
||||
it(`${elementName} form group exist in the dom`, () => {
|
||||
expect(formGroup.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it(`${elementName} form group has a label-for property`, () => {
|
||||
expect(formGroup.attributes('label-for')).toBe(`expiration-policy-${elementName}`);
|
||||
});
|
||||
|
||||
it(`${elementName} form group has a label-cols property`, () => {
|
||||
expect(formGroup.attributes('label-cols')).toBe(`${wrapper.vm.$options.labelsConfig.cols}`);
|
||||
});
|
||||
|
||||
it(`${elementName} form group has a label-align property`, () => {
|
||||
expect(formGroup.attributes('label-align')).toBe(`${wrapper.vm.$options.labelsConfig.align}`);
|
||||
});
|
||||
|
||||
it(`${elementName} form group contains an input element`, () => {
|
||||
expect(findFormElements(elementName, formGroup).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it(`${elementName} form element change updated ${modelName} with ${value}`, () => {
|
||||
const element = findFormElements(elementName, formGroup);
|
||||
const modelUpdateEvent = element.vm.$options.model
|
||||
? element.vm.$options.model.event
|
||||
: 'input';
|
||||
element.vm.$emit(modelUpdateEvent, value);
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(wrapper.vm[modelName]).toBe(value);
|
||||
`(
|
||||
`${FORM_ELEMENTS_ID_PREFIX}-$elementName form element`,
|
||||
({ elementName, modelName, value, disabledByToggle }) => {
|
||||
let formGroup;
|
||||
beforeEach(() => {
|
||||
formGroup = findFormGroup(elementName);
|
||||
});
|
||||
it(`${elementName} form group exist in the dom`, () => {
|
||||
expect(formGroup.exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it(`${elementName} is ${disabledByToggle} by enabled set to false`, () => {
|
||||
store.dispatch('updateSettings', { enabled: false });
|
||||
const expectation = disabledByToggle === 'disabled' ? 'true' : undefined;
|
||||
expect(findFormElements(elementName, formGroup).attributes('disabled')).toBe(expectation);
|
||||
});
|
||||
});
|
||||
it(`${elementName} form group has a label-for property`, () => {
|
||||
expect(formGroup.attributes('label-for')).toBe(`expiration-policy-${elementName}`);
|
||||
});
|
||||
|
||||
it(`${elementName} form group has a label-cols property`, () => {
|
||||
expect(formGroup.attributes('label-cols')).toBe(`${wrapper.vm.$options.labelsConfig.cols}`);
|
||||
});
|
||||
|
||||
it(`${elementName} form group has a label-align property`, () => {
|
||||
expect(formGroup.attributes('label-align')).toBe(
|
||||
`${wrapper.vm.$options.labelsConfig.align}`,
|
||||
);
|
||||
});
|
||||
|
||||
it(`${elementName} form group contains an input element`, () => {
|
||||
expect(findFormElements(elementName, formGroup).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it(`${elementName} form element change updated ${modelName} with ${value}`, () => {
|
||||
const element = findFormElements(elementName, formGroup);
|
||||
const modelUpdateEvent = element.vm.$options.model
|
||||
? element.vm.$options.model.event
|
||||
: 'input';
|
||||
element.vm.$emit(modelUpdateEvent, value);
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(wrapper.vm[modelName]).toBe(value);
|
||||
});
|
||||
});
|
||||
|
||||
it(`${elementName} is ${disabledByToggle} by enabled set to false`, () => {
|
||||
store.dispatch('updateSettings', { enabled: false });
|
||||
const expectation = disabledByToggle === 'disabled' ? 'true' : undefined;
|
||||
expect(findFormElements(elementName, formGroup).attributes('disabled')).toBe(expectation);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
describe('form actions', () => {
|
||||
let form;
|
||||
|
@ -112,17 +123,79 @@ describe('Settings Form', () => {
|
|||
});
|
||||
|
||||
it('form reset event call the appropriate function', () => {
|
||||
dispatchSpy.mockReturnValue();
|
||||
form.trigger('reset');
|
||||
expect(resetSpy).toHaveBeenCalled();
|
||||
// expect.any(Object) is necessary because the event payload is passed to the function
|
||||
expect(dispatchSpy).toHaveBeenCalledWith('resetSettings', expect.any(Object));
|
||||
});
|
||||
|
||||
it('save has type submit', () => {
|
||||
expect(findSaveButton().attributes('type')).toBe('submit');
|
||||
});
|
||||
|
||||
it('form submit event call the appropriate function', () => {
|
||||
form.trigger('submit');
|
||||
expect(saveSpy).toHaveBeenCalled();
|
||||
describe('when isLoading is true', () => {
|
||||
beforeEach(() => {
|
||||
store.dispatch('toggleLoading');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
store.dispatch('toggleLoading');
|
||||
});
|
||||
|
||||
it.each`
|
||||
elementName
|
||||
${'toggle'}
|
||||
${'interval'}
|
||||
${'schedule'}
|
||||
${'latest'}
|
||||
${'name-matching'}
|
||||
`(`${FORM_ELEMENTS_ID_PREFIX}-$elementName is disabled`, ({ elementName }) => {
|
||||
expect(findFormElements(elementName).attributes('disabled')).toBe('true');
|
||||
});
|
||||
|
||||
it('submit button is disabled and shows a spinner', () => {
|
||||
const button = findSaveButton();
|
||||
expect(button.attributes('disabled')).toBeTruthy();
|
||||
expect(findLoadingIcon(button)).toExist();
|
||||
});
|
||||
|
||||
it('cancel button is disabled', () => {
|
||||
expect(findCancelButton().attributes('disabled')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('form submit event ', () => {
|
||||
it('calls the appropriate function', () => {
|
||||
dispatchSpy.mockResolvedValue();
|
||||
form.trigger('submit');
|
||||
expect(dispatchSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('dispatches the saveSettings action', () => {
|
||||
dispatchSpy.mockResolvedValue();
|
||||
form.trigger('submit');
|
||||
expect(dispatchSpy).toHaveBeenCalledWith('saveSettings');
|
||||
});
|
||||
|
||||
it('show a success toast when submit succeed', () => {
|
||||
dispatchSpy.mockResolvedValue();
|
||||
form.trigger('submit');
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(UPDATE_SETTINGS_SUCCESS_MESSAGE, {
|
||||
type: 'success',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('show an error toast when submit fails', () => {
|
||||
dispatchSpy.mockRejectedValue();
|
||||
form.trigger('submit');
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(UPDATE_SETTINGS_ERROR_MESSAGE, {
|
||||
type: 'error',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -160,7 +233,7 @@ describe('Settings Form', () => {
|
|||
it('toggleDescriptionText text reflects enabled property', () => {
|
||||
const toggleHelpText = findFormGroup('toggle').find('span');
|
||||
expect(toggleHelpText.html()).toContain('disabled');
|
||||
wrapper.vm.enabled = true;
|
||||
wrapper.setData({ enabled: true });
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(toggleHelpText.html()).toContain('enabled');
|
||||
});
|
||||
|
|
|
@ -1,15 +1,7 @@
|
|||
import Api from '~/api';
|
||||
import createFlash from '~/flash';
|
||||
import testAction from 'helpers/vuex_action_helper';
|
||||
import * as actions from '~/registry/settings/store/actions';
|
||||
import * as types from '~/registry/settings/store/mutation_types';
|
||||
import {
|
||||
UPDATE_SETTINGS_ERROR_MESSAGE,
|
||||
FETCH_SETTINGS_ERROR_MESSAGE,
|
||||
UPDATE_SETTINGS_SUCCESS_MESSAGE,
|
||||
} from '~/registry/settings/constants';
|
||||
|
||||
jest.mock('~/flash');
|
||||
|
||||
describe('Actions Registry Store', () => {
|
||||
describe.each`
|
||||
|
@ -25,19 +17,6 @@ describe('Actions Registry Store', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe.each`
|
||||
actionName | message
|
||||
${'receiveSettingsError'} | ${FETCH_SETTINGS_ERROR_MESSAGE}
|
||||
${'updateSettingsError'} | ${UPDATE_SETTINGS_ERROR_MESSAGE}
|
||||
`('%s action', ({ actionName, message }) => {
|
||||
it(`should call createFlash with ${message}`, done => {
|
||||
testAction(actions[actionName], null, null, [], [], () => {
|
||||
expect(createFlash).toHaveBeenCalledWith(message);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchSettings', () => {
|
||||
const state = {
|
||||
projectId: 'bar',
|
||||
|
@ -64,18 +43,6 @@ describe('Actions Registry Store', () => {
|
|||
done,
|
||||
);
|
||||
});
|
||||
|
||||
it('should call receiveSettingsError on error', done => {
|
||||
Api.project = jest.fn().mockRejectedValue();
|
||||
testAction(
|
||||
actions.fetchSettings,
|
||||
null,
|
||||
state,
|
||||
[],
|
||||
[{ type: 'toggleLoading' }, { type: 'receiveSettingsError' }, { type: 'toggleLoading' }],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('saveSettings', () => {
|
||||
|
@ -102,21 +69,6 @@ describe('Actions Registry Store', () => {
|
|||
{ type: 'receiveSettingsSuccess', payload: payload.data.container_expiration_policy },
|
||||
{ type: 'toggleLoading' },
|
||||
],
|
||||
() => {
|
||||
expect(createFlash).toHaveBeenCalledWith(UPDATE_SETTINGS_SUCCESS_MESSAGE, 'success');
|
||||
done();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should call receiveSettingsError on error', done => {
|
||||
Api.updateProject = jest.fn().mockRejectedValue();
|
||||
testAction(
|
||||
actions.saveSettings,
|
||||
null,
|
||||
state,
|
||||
[],
|
||||
[{ type: 'toggleLoading' }, { type: 'updateSettingsError' }, { type: 'toggleLoading' }],
|
||||
done,
|
||||
);
|
||||
});
|
||||
|
|
|
@ -96,10 +96,10 @@ describe Gitlab::Experimentation do
|
|||
expect(Gitlab::Tracking).to receive(:event).with(
|
||||
'Team',
|
||||
'start',
|
||||
label: nil,
|
||||
property: 'experimental_group'
|
||||
property: 'experimental_group',
|
||||
value: 'team_id'
|
||||
)
|
||||
controller.track_experiment_event(:test_experiment, 'start')
|
||||
controller.track_experiment_event(:test_experiment, 'start', 'team_id')
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -112,10 +112,10 @@ describe Gitlab::Experimentation do
|
|||
expect(Gitlab::Tracking).to receive(:event).with(
|
||||
'Team',
|
||||
'start',
|
||||
label: nil,
|
||||
property: 'control_group'
|
||||
property: 'control_group',
|
||||
value: 'team_id'
|
||||
)
|
||||
controller.track_experiment_event(:test_experiment, 'start')
|
||||
controller.track_experiment_event(:test_experiment, 'start', 'team_id')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -144,13 +144,13 @@ describe Gitlab::Experimentation do
|
|||
end
|
||||
|
||||
it 'pushes the right parameters to gon' do
|
||||
controller.frontend_experimentation_tracking_data(:test_experiment, 'start')
|
||||
controller.frontend_experimentation_tracking_data(:test_experiment, 'start', 'team_id')
|
||||
expect(Gon.tracking_data).to eq(
|
||||
{
|
||||
category: 'Team',
|
||||
action: 'start',
|
||||
label: nil,
|
||||
property: 'experimental_group'
|
||||
property: 'experimental_group',
|
||||
value: 'team_id'
|
||||
}
|
||||
)
|
||||
end
|
||||
|
@ -164,12 +164,23 @@ describe Gitlab::Experimentation do
|
|||
end
|
||||
|
||||
it 'pushes the right parameters to gon' do
|
||||
controller.frontend_experimentation_tracking_data(:test_experiment, 'start', 'team_id')
|
||||
expect(Gon.tracking_data).to eq(
|
||||
{
|
||||
category: 'Team',
|
||||
action: 'start',
|
||||
property: 'control_group',
|
||||
value: 'team_id'
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
it 'does not send nil value to gon' do
|
||||
controller.frontend_experimentation_tracking_data(:test_experiment, 'start')
|
||||
expect(Gon.tracking_data).to eq(
|
||||
{
|
||||
category: 'Team',
|
||||
action: 'start',
|
||||
label: nil,
|
||||
property: 'control_group'
|
||||
}
|
||||
)
|
||||
|
|
|
@ -49,6 +49,7 @@ describe Gitlab::UsageData do
|
|||
create(:clusters_applications_runner, :installed, cluster: gcp_cluster)
|
||||
create(:clusters_applications_knative, :installed, cluster: gcp_cluster)
|
||||
create(:clusters_applications_elastic_stack, :installed, cluster: gcp_cluster)
|
||||
create(:clusters_applications_jupyter, :installed, cluster: gcp_cluster)
|
||||
|
||||
create(:grafana_integration, project: projects[0], enabled: true)
|
||||
create(:grafana_integration, project: projects[1], enabled: true)
|
||||
|
@ -149,6 +150,7 @@ describe Gitlab::UsageData do
|
|||
clusters_applications_runner
|
||||
clusters_applications_knative
|
||||
clusters_applications_elastic_stack
|
||||
clusters_applications_jupyter
|
||||
in_review_folder
|
||||
grafana_integrated_projects
|
||||
groups
|
||||
|
@ -242,6 +244,7 @@ describe Gitlab::UsageData do
|
|||
expect(count_data[:clusters_applications_knative]).to eq(1)
|
||||
expect(count_data[:clusters_applications_elastic_stack]).to eq(1)
|
||||
expect(count_data[:grafana_integrated_projects]).to eq(2)
|
||||
expect(count_data[:clusters_applications_jupyter]).to eq(1)
|
||||
end
|
||||
|
||||
it 'works when queries time out' do
|
||||
|
|
Loading…
Reference in a new issue