diff options
29 files changed, 354 insertions, 201 deletions
diff --git a/.gitlab/issue_templates/QA failure.md b/.gitlab/issue_templates/QA failure.md new file mode 100644 index 00000000000..13b5d7bf92c --- /dev/null +++ b/.gitlab/issue_templates/QA failure.md @@ -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 % diff --git a/Gemfile.lock b/Gemfile.lock index 4949ac8b9a5..500f1729d08 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) diff --git a/app/assets/javascripts/registry/settings/components/registry_settings_app.vue b/app/assets/javascripts/registry/settings/components/registry_settings_app.vue index ca495cd2eca..7530c1dfcaf 100644 --- a/app/assets/javascripts/registry/settings/components/registry_settings_app.vue +++ b/app/assets/javascripts/registry/settings/components/registry_settings_app.vue @@ -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> diff --git a/app/assets/javascripts/registry/settings/components/settings_form.vue b/app/assets/javascripts/registry/settings/components/settings_form.vue index 457bf35daab..b713cfe2e34 100644 --- a/app/assets/javascripts/registry/settings/components/settings_form.vue +++ b/app/assets/javascripts/registry/settings/components/settings_form.vue @@ -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> diff --git a/app/assets/javascripts/registry/settings/registry_settings_bundle.js b/app/assets/javascripts/registry/settings/registry_settings_bundle.js index 927b6059884..6ae1dbb72c4 100644 --- a/app/assets/javascripts/registry/settings/registry_settings_bundle.js +++ b/app/assets/javascripts/registry/settings/registry_settings_bundle.js @@ -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 () => { diff --git a/app/assets/javascripts/registry/settings/store/actions.js b/app/assets/javascripts/registry/settings/store/actions.js index 5e46d564121..21a2008fef6 100644 --- a/app/assets/javascripts/registry/settings/store/actions.js +++ b/app/assets/javascripts/registry/settings/store/actions.js @@ -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')); }; diff --git a/app/models/note.rb b/app/models/note.rb index 7731b477ad0..de9478ce68d 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -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 diff --git a/app/models/project_ci_cd_setting.rb b/app/models/project_ci_cd_setting.rb index 1dd65c76258..a495d34c07c 100644 --- a/app/models/project_ci_cd_setting.rb +++ b/app/models/project_ci_cd_setting.rb @@ -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. diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index f26a2201550..f19dd0e4a48 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -192,3 +192,4 @@ - self_monitoring_project_create - self_monitoring_project_delete - merge_request_mergeability_check +- phabricator_import_import_tasks diff --git a/lib/gitlab/phabricator_import/base_worker.rb b/app/workers/gitlab/phabricator_import/base_worker.rb index d2c2ef8db48..faae71d4627 100644 --- a/lib/gitlab/phabricator_import/base_worker.rb +++ b/app/workers/gitlab/phabricator_import/base_worker.rb @@ -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 diff --git a/lib/gitlab/phabricator_import/import_tasks_worker.rb b/app/workers/gitlab/phabricator_import/import_tasks_worker.rb index c36954a8d41..b5d9e80797b 100644 --- a/lib/gitlab/phabricator_import/import_tasks_worker.rb +++ b/app/workers/gitlab/phabricator_import/import_tasks_worker.rb @@ -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 diff --git a/changelogs/unreleased/sh-bump-pg-gem-1-2-2.yml b/changelogs/unreleased/sh-bump-pg-gem-1-2-2.yml new file mode 100644 index 00000000000..2e2910d17a5 --- /dev/null +++ b/changelogs/unreleased/sh-bump-pg-gem-1-2-2.yml @@ -0,0 +1,5 @@ +--- +title: Update pg gem to v1.2.2 +merge_request: 23237 +author: +type: other diff --git a/db/migrate/20191209143606_add_deleted_at_to_description_versions.rb b/db/migrate/20191209143606_add_deleted_at_to_description_versions.rb new file mode 100644 index 00000000000..02a3d1271c2 --- /dev/null +++ b/db/migrate/20191209143606_add_deleted_at_to_description_versions.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb index 39ada44b5aa..ae1b8533102 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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)" diff --git a/doc/development/what_requires_downtime.md b/doc/development/what_requires_downtime.md index 356feae4eaf..841a05d8e61 100644 --- a/doc/development/what_requires_downtime.md +++ b/doc/development/what_requires_downtime.md @@ -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 diff --git a/lib/gitlab/experimentation.rb b/lib/gitlab/experimentation.rb index 9d14695c098..c689142d79d 100644 --- a/lib/gitlab/experimentation.rb +++ b/lib/gitlab/experimentation.rb @@ -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) diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb index e00b49b9042..f10eb82e03e 100644 --- a/lib/gitlab/usage_data.rb +++ b/lib/gitlab/usage_data.rb @@ -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), diff --git a/qa/qa/page/project/settings/protected_branches.rb b/qa/qa/page/project/settings/protected_branches.rb index f718311fbf2..9d302acb058 100644 --- a/qa/qa/page/project/settings/protected_branches.rb +++ b/qa/qa/page/project/settings/protected_branches.rb @@ -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 diff --git a/qa/qa/scenario/template.rb b/qa/qa/scenario/template.rb index 97373f7a059..9c599ec2d4d 100644 --- a/qa/qa/scenario/template.rb +++ b/qa/qa/scenario/template.rb @@ -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| diff --git a/qa/qa/scenario/test/instance.rb b/qa/qa/scenario/test/instance.rb index 79dad7f4619..11b6a7f7dfa 100644 --- a/qa/qa/scenario/test/instance.rb +++ b/qa/qa/scenario/test/instance.rb @@ -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? diff --git a/spec/features/projects/settings/registry_settings_spec.rb b/spec/features/projects/settings/registry_settings_spec.rb index 86da866a927..89da9d1b996 100644 --- a/spec/features/projects/settings/registry_settings_spec.rb +++ b/spec/features/projects/settings/registry_settings_spec.rb @@ -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 diff --git a/spec/frontend/registry/settings/components/__snapshots__/settings_form_spec.js.snap b/spec/frontend/registry/settings/components/__snapshots__/settings_form_spec.js.snap index d26df308b97..1d8627da181 100644 --- a/spec/frontend/registry/settings/components/__snapshots__/settings_form_spec.js.snap +++ b/spec/frontend/registry/settings/components/__snapshots__/settings_form_spec.js.snap @@ -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> diff --git a/spec/frontend/registry/settings/components/registry_settings_app_spec.js b/spec/frontend/registry/settings/components/registry_settings_app_spec.js index 448ff2b3be9..eceb5bf643c 100644 --- a/spec/frontend/registry/settings/components/registry_settings_app_spec.js +++ b/spec/frontend/registry/settings/components/registry_settings_app_spec.js @@ -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); }); }); diff --git a/spec/frontend/registry/settings/components/settings_form_spec.js b/spec/frontend/registry/settings/components/settings_form_spec.js index bd733e965a4..996804f6d08 100644 --- a/spec/frontend/registry/settings/components/settings_form_spec.js +++ b/spec/frontend/registry/settings/components/settings_form_spec.js @@ -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 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 findFormElements = (name, father = wrapper) => father.find(`#expiration-policy-${name}`); + 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, }, - store, - methods: { - saveSettings: saveSpy, - resetSettings: resetSpy, + mocks: { + $toast: { + show: jest.fn(), + }, }, + store, ...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); - }); + `( + `${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} form group has a label-for property`, () => { - expect(formGroup.attributes('label-for')).toBe(`expiration-policy-${elementName}`); - }); + 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-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 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 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} 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); - }); - }); + 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'); }); diff --git a/spec/frontend/registry/settings/store/actions_spec.js b/spec/frontend/registry/settings/store/actions_spec.js index 80fb800ac3a..65b1fc42bfe 100644 --- a/spec/frontend/registry/settings/store/actions_spec.js +++ b/spec/frontend/registry/settings/store/actions_spec.js @@ -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, ); }); diff --git a/spec/lib/gitlab/experimentation_spec.rb b/spec/lib/gitlab/experimentation_spec.rb index e4624accd58..1506794cbb5 100644 --- a/spec/lib/gitlab/experimentation_spec.rb +++ b/spec/lib/gitlab/experimentation_spec.rb @@ -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' } ) diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index cf1dacd088e..9a49d334f52 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -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 diff --git a/spec/lib/gitlab/phabricator_import/base_worker_spec.rb b/spec/workers/gitlab/phabricator_import/base_worker_spec.rb index d46d908a3e3..d46d908a3e3 100644 --- a/spec/lib/gitlab/phabricator_import/base_worker_spec.rb +++ b/spec/workers/gitlab/phabricator_import/base_worker_spec.rb diff --git a/spec/lib/gitlab/phabricator_import/import_tasks_worker_spec.rb b/spec/workers/gitlab/phabricator_import/import_tasks_worker_spec.rb index 1e38ef8aaa5..1e38ef8aaa5 100644 --- a/spec/lib/gitlab/phabricator_import/import_tasks_worker_spec.rb +++ b/spec/workers/gitlab/phabricator_import/import_tasks_worker_spec.rb |