Commit d91f5211 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent c859c3bf
<!---
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 ...@@ -741,7 +741,7 @@ GEM
parslet (1.8.2) parslet (1.8.2)
peek (1.1.0) peek (1.1.0)
railties (>= 4.0.0) railties (>= 4.0.0)
pg (1.1.4) pg (1.2.2)
png_quantizator (0.2.1) png_quantizator (0.2.1)
po_to_json (1.0.1) po_to_json (1.0.1)
json (>= 1.6.0) json (>= 1.6.0)
......
<script> <script>
import { mapState, mapActions } from 'vuex'; import { mapActions } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui'; import { FETCH_SETTINGS_ERROR_MESSAGE } from '../constants';
import SettingsForm from './settings_form.vue'; import SettingsForm from './settings_form.vue';
export default { export default {
components: { components: {
GlLoadingIcon,
SettingsForm, SettingsForm,
}, },
computed: {
...mapState({
isLoading: 'isLoading',
}),
},
mounted() { mounted() {
this.fetchSettings(); this.fetchSettings().catch(() =>
this.$toast.show(FETCH_SETTINGS_ERROR_MESSAGE, { type: 'error' }),
);
}, },
methods: { methods: {
...mapActions(['fetchSettings']), ...mapActions(['fetchSettings']),
...@@ -37,7 +34,6 @@ export default { ...@@ -37,7 +34,6 @@ export default {
}} }}
</li> </li>
</ul> </ul>
<gl-loading-icon v-if="isLoading" ref="loading-icon" size="xl" /> <settings-form ref="settings-form" />
<settings-form v-else ref="settings-form" />
</div> </div>
</template> </template>
<script> <script>
import { mapActions, mapState } from 'vuex'; 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 { 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'; import { mapComputed } from '~/vuex_shared/bindings';
export default { export default {
...@@ -13,13 +25,14 @@ export default { ...@@ -13,13 +25,14 @@ export default {
GlFormTextarea, GlFormTextarea,
GlButton, GlButton,
GlCard, GlCard,
GlLoadingIcon,
}, },
labelsConfig: { labelsConfig: {
cols: 3, cols: 3,
align: 'right', align: 'right',
}, },
computed: { computed: {
...mapState(['formOptions']), ...mapState(['formOptions', 'isLoading']),
...mapComputed( ...mapComputed(
[ [
'enabled', 'enabled',
...@@ -64,15 +77,26 @@ export default { ...@@ -64,15 +77,26 @@ export default {
formIsInvalid() { formIsInvalid() {
return this.nameRegexState === false; return this.nameRegexState === false;
}, },
isFormElementDisabled() {
return !this.enabled || this.isLoading;
},
isSubmitButtonDisabled() {
return this.formIsInvalid || this.isLoading;
},
}, },
methods: { methods: {
...mapActions(['resetSettings', 'saveSettings']), ...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> </script>
<template> <template>
<form ref="form-element" @submit.prevent="saveSettings" @reset.prevent="resetSettings"> <form ref="form-element" @submit.prevent="submit" @reset.prevent="resetSettings">
<gl-card> <gl-card>
<template #header> <template #header>
{{ s__('ContainerRegistry|Tag expiration policy') }} {{ s__('ContainerRegistry|Tag expiration policy') }}
...@@ -86,7 +110,7 @@ export default { ...@@ -86,7 +110,7 @@ export default {
:label="s__('ContainerRegistry|Expiration policy:')" :label="s__('ContainerRegistry|Expiration policy:')"
> >
<div class="d-flex align-items-start"> <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> <span class="mb-2 ml-1 lh-2" v-html="toggleDescriptionText"></span>
</div> </div>
</gl-form-group> </gl-form-group>
...@@ -98,7 +122,11 @@ export default { ...@@ -98,7 +122,11 @@ export default {
label-for="expiration-policy-interval" label-for="expiration-policy-interval"
:label="s__('ContainerRegistry|Expiration 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 v-for="option in formOptions.olderThan" :key="option.key" :value="option.key">
{{ option.label }} {{ option.label }}
</option> </option>
...@@ -112,7 +140,11 @@ export default { ...@@ -112,7 +140,11 @@ export default {
label-for="expiration-policy-schedule" label-for="expiration-policy-schedule"
:label="s__('ContainerRegistry|Expiration 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 v-for="option in formOptions.cadence" :key="option.key" :value="option.key">
{{ option.label }} {{ option.label }}
</option> </option>
...@@ -126,7 +158,11 @@ export default { ...@@ -126,7 +158,11 @@ export default {
label-for="expiration-policy-latest" label-for="expiration-policy-latest"
:label="s__('ContainerRegistry|Number of tags to retain:')" :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 v-for="option in formOptions.keepN" :key="option.key" :value="option.key">
{{ option.label }} {{ option.label }}
</option> </option>
...@@ -149,7 +185,7 @@ export default { ...@@ -149,7 +185,7 @@ export default {
v-model="name_regex" v-model="name_regex"
:placeholder="nameRegexPlaceholder" :placeholder="nameRegexPlaceholder"
:state="nameRegexState" :state="nameRegexState"
:disabled="!enabled" :disabled="isFormElementDisabled"
trim trim
/> />
<template #description> <template #description>
...@@ -159,17 +195,18 @@ export default { ...@@ -159,17 +195,18 @@ export default {
</template> </template>
<template #footer> <template #footer>
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
<gl-button ref="cancel-button" type="reset" class="mr-2 d-block">{{ <gl-button ref="cancel-button" type="reset" class="mr-2 d-block" :disabled="isLoading">
__('Cancel') {{ __('Cancel') }}
}}</gl-button> </gl-button>
<gl-button <gl-button
ref="save-button" ref="save-button"
type="submit" type="submit"
:disabled="formIsInvalid" :disabled="isSubmitButtonDisabled"
variant="success" variant="success"
class="d-block" class="d-flex justify-content-center align-items-center js-no-auto-disable"
> >
{{ __('Save expiration policy') }} {{ __('Save expiration policy') }}
<gl-loading-icon v-if="isLoading" class="ml-2" />
</gl-button> </gl-button>
</div> </div>
</template> </template>
......
import Vue from 'vue'; import Vue from 'vue';
import { GlToast } from '@gitlab/ui';
import Translate from '~/vue_shared/translate'; import Translate from '~/vue_shared/translate';
import store from './store/'; import store from './store/';
import RegistrySettingsApp from './components/registry_settings_app.vue'; import RegistrySettingsApp from './components/registry_settings_app.vue';
Vue.use(GlToast);
Vue.use(Translate); Vue.use(Translate);
export default () => { export default () => {
......
import Api from '~/api'; 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'; import * as types from './mutation_types';
export const setInitialState = ({ commit }, data) => commit(types.SET_INITIAL_STATE, data); export const setInitialState = ({ commit }, data) => commit(types.SET_INITIAL_STATE, data);
export const updateSettings = ({ commit }, data) => commit(types.UPDATE_SETTINGS, data); export const updateSettings = ({ commit }, data) => commit(types.UPDATE_SETTINGS, data);
export const toggleLoading = ({ commit }) => commit(types.TOGGLE_LOADING); export const toggleLoading = ({ commit }) => commit(types.TOGGLE_LOADING);
export const receiveSettingsSuccess = ({ commit }, data = {}) => commit(types.SET_SETTINGS, data); 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 resetSettings = ({ commit }) => commit(types.RESET_SETTINGS);
export const fetchSettings = ({ dispatch, state }) => { export const fetchSettings = ({ dispatch, state }) => {
...@@ -21,7 +13,6 @@ export const fetchSettings = ({ dispatch, state }) => { ...@@ -21,7 +13,6 @@ export const fetchSettings = ({ dispatch, state }) => {
.then(({ data: { container_expiration_policy } }) => .then(({ data: { container_expiration_policy } }) =>
dispatch('receiveSettingsSuccess', container_expiration_policy), dispatch('receiveSettingsSuccess', container_expiration_policy),
) )
.catch(() => dispatch('receiveSettingsError'))
.finally(() => dispatch('toggleLoading')); .finally(() => dispatch('toggleLoading'));
}; };
...@@ -30,11 +21,9 @@ export const saveSettings = ({ dispatch, state }) => { ...@@ -30,11 +21,9 @@ export const saveSettings = ({ dispatch, state }) => {
return Api.updateProject(state.projectId, { return Api.updateProject(state.projectId, {
container_expiration_policy_attributes: state.settings, container_expiration_policy_attributes: state.settings,
}) })
.then(({ data: { container_expiration_policy } }) => { .then(({ data: { container_expiration_policy } }) =>
dispatch('receiveSettingsSuccess', container_expiration_policy); dispatch('receiveSettingsSuccess', container_expiration_policy),
createFlash(UPDATE_SETTINGS_SUCCESS_MESSAGE, 'success'); )
})
.catch(() => dispatch('updateSettingsError'))
.finally(() => dispatch('toggleLoading')); .finally(() => dispatch('toggleLoading'));
}; };
......
...@@ -124,7 +124,7 @@ class Note < ApplicationRecord ...@@ -124,7 +124,7 @@ class Note < ApplicationRecord
scope :inc_author, -> { includes(:author) } scope :inc_author, -> { includes(:author) }
scope :inc_relations_for_view, -> do scope :inc_relations_for_view, -> do
includes(:project, { author: :status }, :updated_by, :resolved_by, :award_emoji, 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 end
scope :with_notes_filter, -> (notes_filter) do scope :with_notes_filter, -> (notes_filter) do
......
# frozen_string_literal: true # frozen_string_literal: true
class ProjectCiCdSetting < ApplicationRecord 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 belongs_to :project, inverse_of: :ci_cd_settings
# The version of the schema that first introduced this model/table. # The version of the schema that first introduced this model/table.
......
...@@ -192,3 +192,4 @@ ...@@ -192,3 +192,4 @@
- self_monitoring_project_create - self_monitoring_project_create
- self_monitoring_project_delete - self_monitoring_project_delete
- merge_request_mergeability_check - merge_request_mergeability_check
- phabricator_import_import_tasks
...@@ -19,8 +19,7 @@ ...@@ -19,8 +19,7 @@
module Gitlab module Gitlab
module PhabricatorImport module PhabricatorImport
class BaseWorker class BaseWorker
include ApplicationWorker include WorkerAttributes
include ProjectImportOptions # This marks the project as failed after too many tries
include Gitlab::ExclusiveLeaseHelpers include Gitlab::ExclusiveLeaseHelpers
feature_category :importers feature_category :importers
......
...@@ -2,6 +2,9 @@ ...@@ -2,6 +2,9 @@
module Gitlab module Gitlab
module PhabricatorImport module PhabricatorImport
class ImportTasksWorker < BaseWorker class ImportTasksWorker < BaseWorker
include ApplicationWorker
include ProjectImportOptions # This marks the project as failed after too many tries
def importer_class def importer_class
Gitlab::PhabricatorImport::Issues::Importer Gitlab::PhabricatorImport::Issues::Importer
end end
......
---
title: Update pg gem to v1.2.2
merge_request: 23237
author:
type: other
# 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 ...@@ -1410,6 +1410,7 @@ ActiveRecord::Schema.define(version: 2020_01_21_132641) do
t.integer "merge_request_id" t.integer "merge_request_id"
t.integer "epic_id" t.integer "epic_id"
t.text "description" 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 ["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 ["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)" 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] ...@@ -213,7 +213,7 @@ class ChangeUsersUsernameStringToTextCleanup < ActiveRecord::Migration[4.2]
disable_ddl_transaction! disable_ddl_transaction!
def up def up
cleanup_concurrent_column_type_change :users cleanup_concurrent_column_type_change :users, :username
end end
def down def down
......
...@@ -53,14 +53,14 @@ module Gitlab ...@@ -53,14 +53,14 @@ module Gitlab
Experimentation.enabled_for_user?(experiment_key, experimentation_subject_index) || forced_enabled?(experiment_key) Experimentation.enabled_for_user?(experiment_key, experimentation_subject_index) || forced_enabled?(experiment_key)
end end
def track_experiment_event(experiment_key, action) def track_experiment_event(experiment_key, action, value = nil)
track_experiment_event_for(experiment_key, action) do |tracking_data| track_experiment_event_for(experiment_key, action, value) do |tracking_data|
::Gitlab::Tracking.event(tracking_data.delete(:category), tracking_data.delete(:action), tracking_data) ::Gitlab::Tracking.event(tracking_data.delete(:category), tracking_data.delete(:action), tracking_data)
end end
end end
def frontend_experimentation_tracking_data(experiment_key, action) def frontend_experimentation_tracking_data(experiment_key, action, value = nil)
track_experiment_event_for(experiment_key, action) do |tracking_data| track_experiment_event_for(experiment_key, action, value) do |tracking_data|
gon.push(tracking_data: tracking_data) gon.push(tracking_data: tracking_data)
end end
end end
...@@ -77,19 +77,20 @@ module Gitlab ...@@ -77,19 +77,20 @@ module Gitlab
experimentation_subject_id.delete('-').hex % 100 experimentation_subject_id.delete('-').hex % 100
end end
def track_experiment_event_for(experiment_key, action) def track_experiment_event_for(experiment_key, action, value)
return unless Experimentation.enabled?(experiment_key) return unless Experimentation.enabled?(experiment_key)
yield experimentation_tracking_data(experiment_key, action) yield experimentation_tracking_data(experiment_key, action, value)
end end
def experimentation_tracking_data(experiment_key, action) def experimentation_tracking_data(experiment_key, action, value)
{ {
category: tracking_category(experiment_key), category: tracking_category(experiment_key),
action: action, action: action,
property: tracking_group(experiment_key), property: tracking_group(experiment_key),
label: experimentation_subject_id label: experimentation_subject_id,
} value: value
}.compact
end end
def tracking_category(experiment_key) def tracking_category(experiment_key)
......
...@@ -78,6 +78,7 @@ module Gitlab ...@@ -78,6 +78,7 @@ module Gitlab
clusters_applications_runner: count(::Clusters::Applications::Runner.available), clusters_applications_runner: count(::Clusters::Applications::Runner.available),
clusters_applications_knative: count(::Clusters::Applications::Knative.available), clusters_applications_knative: count(::Clusters::Applications::Knative.available),
clusters_applications_elastic_stack: count(::Clusters::Applications::ElasticStack.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), in_review_folder: count(::Environment.in_review_folder),
grafana_integrated_projects: count(GrafanaIntegration.enabled), grafana_integrated_projects: count(GrafanaIntegration.enabled),
groups: count(Group), groups: count(Group),
......
...@@ -47,6 +47,7 @@ module QA ...@@ -47,6 +47,7 @@ module QA
def protect_branch def protect_branch
click_element(:protect_button, wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME) click_element(:protect_button, wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME)
wait_for_requests
end end
private private
......
...@@ -23,6 +23,8 @@ module QA ...@@ -23,6 +23,8 @@ module QA
def perform(options, *args) def perform(options, *args)
extract_address(:gitlab_address, options, args) extract_address(:gitlab_address, options, args)
QA::Runtime::Browser.configure!
Runtime::Feature.enable(options[:enable_feature]) if options.key?(:enable_feature) Runtime::Feature.enable(options[:enable_feature]) if options.key?(:enable_feature)
Specs::Runner.perform do |specs| Specs::Runner.perform do |specs|
......
...@@ -20,6 +20,8 @@ module QA ...@@ -20,6 +20,8 @@ module QA
def self.do_perform(address, *rspec_options) def self.do_perform(address, *rspec_options)
Runtime::Scenario.define(:gitlab_address, address) Runtime::Scenario.define(:gitlab_address, address)
QA::Runtime::Browser.configure!
Specs::Runner.perform do |specs| Specs::Runner.perform do |specs|
specs.tty = true specs.tty = true
specs.options = rspec_options if rspec_options.any? specs.options = rspec_options if rspec_options.any?
......
...@@ -17,10 +17,10 @@ describe 'Project > Settings > CI/CD > Container registry tag expiration policy' ...@@ -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' expect(settings_block).to have_text 'Container Registry tag expiration policy'
end end
it 'Save expiration policy submit the form', :js do it 'Save expiration policy submit the form' do
within '#js-registry-policies' do within '#js-registry-policies' do
within '.card-body' 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('7 days until tags are automatically removed', from: 'expiration-policy-interval')
select('Every day', from: 'expiration-policy-schedule') select('Every day', from: 'expiration-policy-schedule')
select('50 tags per image name', from: 'expiration-policy-latest') select('50 tags per image name', from: 'expiration-policy-latest')
...@@ -30,8 +30,8 @@ describe 'Project > Settings > CI/CD > Container registry tag expiration policy' ...@@ -30,8 +30,8 @@ describe 'Project > Settings > CI/CD > Container registry tag expiration policy'
expect(submit_button).not_to be_disabled expect(submit_button).not_to be_disabled
submit_button.click submit_button.click
end end
flash_text = find('.flash-text') toast = find('.gl-toast')
expect(flash_text).to have_content('Expiration policy successfully saved.') expect(toast).to have_content('Expiration policy successfully saved.')
end end
end end
end end
...@@ -161,17 +161,20 @@ exports[`Settings Form renders 1`] = ` ...@@ -161,17 +161,20 @@ exports[`Settings Form renders 1`] = `
class="mr-2 d-block" class="mr-2 d-block"
type="reset" type="reset"
> >
Cancel Cancel
</glbutton-stub> </glbutton-stub>
<glbutton-stub <glbutton-stub
class="d-block" class="d-flex justify-content-center align-items-center js-no-auto-disable"
type="submit" type="submit"
variant="success" variant="success"
> >
Save expiration policy Save expiration policy
<!---->
</glbutton-stub> </glbutton-stub>
</div> </div>
</div> </div>
......
import Vuex from 'vuex'; import { shallowMount } from '@vue/test-utils';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import component from '~/registry/settings/components/registry_settings_app.vue'; import component from '~/registry/settings/components/registry_settings_app.vue';
import { createStore } from '~/registry/settings/store/'; import { createStore } from '~/registry/settings/store/';
import { FETCH_SETTINGS_ERROR_MESSAGE } from '~/registry/settings/constants';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('Registry Settings App', () => { describe('Registry Settings App', () => {
let wrapper; let wrapper;
let store; let store;
let fetchSpy;
const findSettingsComponent = () => wrapper.find({ ref: 'settings-form' }); const findSettingsComponent = () => wrapper.find({ ref: 'settings-form' });
const findLoadingComponent = () => wrapper.find({ ref: 'loading-icon' });
const mountComponent = (options = {}) => { const mountComponent = ({ dispatchMock } = {}) => {
fetchSpy = jest.fn(); store = createStore();
const dispatchSpy = jest.spyOn(store, 'dispatch');
if (dispatchMock) {
dispatchSpy[dispatchMock]();
}
wrapper = shallowMount(component, { wrapper = shallowMount(component, {
store, mocks: {
methods: { $toast: {
fetchSettings: fetchSpy, show: jest.fn(),
},
}, },
...options, store,
}); });
}; };
beforeEach(() => {
store = createStore();
mountComponent();
});
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
}); });
it('renders', () => { it('renders', () => {
mountComponent({ dispatchMock: 'mockResolvedValue' });
expect(wrapper.element).toMatchSnapshot(); expect(wrapper.element).toMatchSnapshot();
}); });
it('call the store function to load the data on mount', () => { 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', () => { it('show a toast if fetchSettings fails', () => {
store.dispatch('toggleLoading'); mountComponent({ dispatchMock: 'mockRejectedValue' });
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() =>
expect(findLoadingComponent().exists()).toBe(true); expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(FETCH_SETTINGS_ERROR_MESSAGE, {
expect(findSettingsComponent().exists()).toBe(false); type: 'error',
}); }),
);
}); });
it('renders the setting form', () => { it('renders the setting form', () => {
mountComponent({ dispatchMock: 'mockResolvedValue' });
expect(findSettingsComponent().exists()).toBe(true); expect(findSettingsComponent().exists()).toBe(true);
}); });
}); });
import Vuex from 'vuex'; import { mount } from '@vue/test-utils';
import { mount, createLocalVue } from '@vue/test-utils';
import stubChildren from 'helpers/stub_children'; import stubChildren from 'helpers/stub_children';
import component from '~/registry/settings/components/settings_form.vue'; import component from '~/registry/settings/components/settings_form.vue';
import { createStore } from '~/registry/settings/store/'; 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'; import { stringifiedFormOptions } from '../mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('Settings Form', () => { describe('Settings Form', () => {
let wrapper; let wrapper;
let store; let store;
let saveSpy; let dispatchSpy;
let resetSpy;
const FORM_ELEMENTS_ID_PREFIX = '#expiration-policy';
const GlLoadingIcon = { name: 'gl-loading-icon-stub', template: '<svg></svg>' };
const findFormGroup = name => wrapper.find(`#expiration-policy-${name}-group`); const findFormGroup = name => wrapper.find(`${FORM_ELEMENTS_ID_PREFIX}-${name}-group`);
const findFormElements = (name, father = wrapper) => father.find(`#expiration-policy-${name}`); const findFormElements = (name, parent = wrapper) =>
parent.find(`${FORM_ELEMENTS_ID_PREFIX}-${name}`);
const findCancelButton = () => wrapper.find({ ref: 'cancel-button' }); const findCancelButton = () => wrapper.find({ ref: 'cancel-button' });
const findSaveButton = () => wrapper.find({ ref: 'save-button' }); const findSaveButton = () => wrapper.find({ ref: 'save-button' });
const findForm = () => wrapper.find({ ref: 'form-element' }); const findForm = () => wrapper.find({ ref: 'form-element' });
const findLoadingIcon = (parent = wrapper) => parent.find(GlLoadingIcon);
const mountComponent = (options = {}) => { const mountComponent = (options = {}) => {
saveSpy = jest.fn();
resetSpy = jest.fn();
wrapper = mount(component, { wrapper = mount(component, {
stubs: { stubs: {
...stubChildren(component), ...stubChildren(component),
GlCard: false, GlCard: false,
GlLoadingIcon,
}, },
store, mocks: {
methods: { $toast: {
saveSettings: saveSpy, show: jest.fn(),
resetSettings: resetSpy, },
}, },
store,
...options, ...options,
}); });
}; };
...@@ -41,6 +46,7 @@ describe('Settings Form', () => { ...@@ -41,6 +46,7 @@ describe('Settings Form', () => {
beforeEach(() => { beforeEach(() => {
store = createStore(); store = createStore();
store.dispatch('setInitialState', stringifiedFormOptions); store.dispatch('setInitialState', stringifiedFormOptions);
dispatchSpy = jest.spyOn(store, 'dispatch');
mountComponent(); mountComponent();
}); });
...@@ -59,48 +65,53 @@ describe('Settings Form', () => { ...@@ -59,48 +65,53 @@ describe('Settings Form', () => {
${'schedule'} | ${'cadence'} | ${'foo'} | ${'disabled'} ${'schedule'} | ${'cadence'} | ${'foo'} | ${'disabled'}
${'latest'} | ${'keep_n'} | ${'foo'} | ${'disabled'} ${'latest'} | ${'keep_n'} | ${'foo'} | ${'disabled'}
${'name-matching'} | ${'name_regex'} | ${'foo'} | ${'disabled'} ${'name-matching'} | ${'name_regex'} | ${'foo'} | ${'disabled'}
`('$elementName form element', ({ elementName, modelName, value, disabledByToggle }) => { `(
let formGroup; `${FORM_ELEMENTS_ID_PREFIX}-$elementName form element`,
beforeEach(() => { ({ elementName, modelName, value, disabledByToggle }) => {
formGroup = findFormGroup(elementName); let formGroup;
}); beforeEach(() => {
it(`${elementName} form group exist in the dom`, () => { formGroup = findFormGroup(elementName);
expect(formGroup.exists()).toBe(true); });
}); it(`${elementName} form group exist in the dom`, () => {
expect(formGroup.exists()).toBe(true);
});
it(`${elementName} form group has a label-for property`, () => { it(`${elementName} form group has a label-for property`, () => {
expect(formGroup.attributes('label-for')).toBe(`expiration-policy-${elementName}`); expect(formGroup.attributes('label-for')).toBe(`expiration-policy-${elementName}`);
}); });
it(`${elementName} form group has a label-cols property`, () => { it(`${elementName} form group has a label-cols property`, () => {
expect(formGroup.attributes('label-cols')).toBe(`${wrapper.vm.$options.labelsConfig.cols}`); expect(formGroup.attributes('label-cols')).toBe(`${wrapper.vm.$options.labelsConfig.cols}`);
}); });
it(`${elementName} form group has a label-align property`, () => { it(`${elementName} form group has a label-align property`, () => {
expect(formGroup.attributes('label-align')).toBe(`${wrapper.vm.$options.labelsConfig.align}`); expect(formGroup.attributes('label-align')).toBe(
}); `${wrapper.vm.$options.labelsConfig.align}`,
);
});
it(`${elementName} form group contains an input element`, () => { it(`${elementName} form group contains an input element`, () => {
expect(findFormElements(elementName, formGroup).exists()).toBe(true); expect(findFormElements(elementName, formGroup).exists()).toBe(true);
}); });
it(`${elementName} form element change updated ${modelName} with ${value}`, () => { it(`${elementName} form element change updated ${modelName} with ${value}`, () => {
const element = findFormElements(elementName, formGroup); const element = findFormElements(elementName, formGroup);
const modelUpdateEvent = element.vm.$options.model const modelUpdateEvent = element.vm.$options.model
? element.vm.$options.model.event ? element.vm.$options.model.event
: 'input'; : 'input';
element.vm.$emit(modelUpdateEvent, value); element.vm.$emit(modelUpdateEvent, value);
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm[modelName]).toBe(value); expect(wrapper.vm[modelName]).toBe(value);
});
}); });
});
it(`${elementName} is ${disabledByToggle} by enabled set to false`, () => { it(`${elementName} is ${disabledByToggle} by enabled set to false`, () => {
store.dispatch('updateSettings', { enabled: false }); store.dispatch('updateSettings', { enabled: false });
const expectation = disabledByToggle === 'disabled' ? 'true' : undefined; const expectation = disabledByToggle === 'disabled' ? 'true' : undefined;
expect(findFormElements(elementName, formGroup).attributes('disabled')).toBe(expectation); expect(findFormElements(elementName, formGroup).attributes('disabled')).toBe(expectation);
}); });
}); },
);
describe('form actions', () => { describe('form actions', () => {
let form; let form;
...@@ -112,17 +123,79 @@ describe('Settings Form', () => { ...@@ -112,17 +123,79 @@ describe('Settings Form', () => {
}); });
it('form reset event call the appropriate function', () => { it('form reset event call the appropriate function', () => {
dispatchSpy.mockReturnValue();
form.trigger('reset'); 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', () => { it('save has type submit', () => {
expect(findSaveButton().attributes('type')).toBe('submit'); expect(findSaveButton().attributes('type')).toBe('submit');
}); });
it('form submit event call the appropriate function', () => { describe('when isLoading is true', () => {
form.trigger('submit'); beforeEach(() => {
expect(saveSpy).toHaveBeenCalled(); 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', () => { ...@@ -160,7 +233,7 @@ describe('Settings Form', () => {
it('toggleDescriptionText text reflects enabled property', () => { it('toggleDescriptionText text reflects enabled property', () => {
const toggleHelpText = findFormGroup('toggle').find('span'); const toggleHelpText = findFormGroup('toggle').find('span');
expect(toggleHelpText.html()).toContain('disabled'); expect(toggleHelpText.html()).toContain('disabled');
wrapper.vm.enabled = true; wrapper.setData({ enabled: true });
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
expect(toggleHelpText.html()).toContain('enabled'); expect(toggleHelpText.html()).toContain('enabled');
}); });
......
import Api from '~/api'; import Api from '~/api';
import createFlash from '~/flash';
import testAction from 'helpers/vuex_action_helper'; import testAction from 'helpers/vuex_action_helper';
import * as actions from '~/registry/settings/store/actions'; import * as actions from '~/registry/settings/store/actions';
import * as types from '~/registry/settings/store/mutation_types'; 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('Actions Registry Store', () => {
describe.each` describe.each`
...@@ -25,19 +17,6 @@ describe('Actions Registry Store', () => { ...@@ -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', () => { describe('fetchSettings', () => {
const state = { const state = {
projectId: 'bar', projectId: 'bar',
...@@ -64,18 +43,6 @@ describe('Actions Registry Store', () => { ...@@ -64,18 +43,6 @@ describe('Actions Registry Store', () => {
done, 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', () => { describe('saveSettings', () => {
...@@ -102,21 +69,6 @@ describe('Actions Registry Store', () => { ...@@ -102,21 +69,6 @@ describe('Actions Registry Store', () => {
{ type: 'receiveSettingsSuccess', payload: payload.data.container_expiration_policy }, { type: 'receiveSettingsSuccess', payload: payload.data.container_expiration_policy },
{ type: 'toggleLoading' }, { 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, done,
); );
}); });
......
...@@ -96,10 +96,10 @@ describe Gitlab::Experimentation do ...@@ -96,10 +96,10 @@ describe Gitlab::Experimentation do
expect(Gitlab::Tracking).to receive(:event).with( expect(Gitlab::Tracking).to receive(:event).with(
'Team', 'Team',
'start', '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
end end
...@@ -112,10 +112,10 @@ describe Gitlab::Experimentation do ...@@ -112,10 +112,10 @@ describe Gitlab::Experimentation do
expect(Gitlab::Tracking).to receive(:event).with( expect(Gitlab::Tracking).to receive(:event).with(
'Team', 'Team',
'start', '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 end
end end
...@@ -144,13 +144,13 @@ describe Gitlab::Experimentation do ...@@ -144,13 +144,13 @@ describe Gitlab::Experimentation do
end end
it 'pushes the right parameters to gon' do 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( expect(Gon.tracking_data).to eq(
{ {
category: 'Team', category: 'Team',
action: 'start', action: 'start',
label: nil, property: 'experimental_group',
property: 'experimental_group' value: 'team_id'
} }
) )
end end
...@@ -164,12 +164,23 @@ describe Gitlab::Experimentation do ...@@ -164,12 +164,23 @@ describe Gitlab::Experimentation do
end end
it 'pushes the right parameters to gon' do 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') controller.frontend_experimentation_tracking_data(:test_experiment, 'start')
expect(Gon.tracking_data).to eq( expect(Gon.tracking_data).to eq(
{ {
category: 'Team', category: 'Team',
action: 'start', action: 'start',
label: nil,
property: 'control_group' property: 'control_group'
} }
) )
......
...@@ -49,6 +49,7 @@ describe Gitlab::UsageData do ...@@ -49,6 +49,7 @@ describe Gitlab::UsageData do
create(:clusters_applications_runner, :installed, cluster: gcp_cluster) create(:clusters_applications_runner, :installed, cluster: gcp_cluster)
create(:clusters_applications_knative, :installed, cluster: gcp_cluster) create(:clusters_applications_knative, :installed, cluster: gcp_cluster)
create(:clusters_applications_elastic_stack, :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[0], enabled: true)
create(:grafana_integration, project: projects[1], enabled: true) create(:grafana_integration, project: projects[1], enabled: true)
...@@ -149,6 +150,7 @@ describe Gitlab::UsageData do ...@@ -149,6 +150,7 @@ describe Gitlab::UsageData do
clusters_applications_runner clusters_applications_runner
clusters_applications_knative clusters_applications_knative
clusters_applications_elastic_stack clusters_applications_elastic_stack
clusters_applications_jupyter
in_review_folder in_review_folder
grafana_integrated_projects grafana_integrated_projects
groups groups
...@@ -242,6 +244,7 @@ describe Gitlab::UsageData do ...@@ -242,6 +244,7 @@ describe Gitlab::UsageData do
expect(count_data[:clusters_applications_knative]).to eq(1) expect(count_data[:clusters_applications_knative]).to eq(1)
expect(count_data[:clusters_applications_elastic_stack]).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[:grafana_integrated_projects]).to eq(2)
expect(count_data[:clusters_applications_jupyter]).to eq(1)
end end
it 'works when queries time out' do it 'works when queries time out' do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment