diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-26 12:09:24 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-26 12:09:24 +0300 |
commit | 26bba9525deb5e9d05fd29cf5b286e7a65d1c791 (patch) | |
tree | 5c4b2157f5c1fc49f6720339a32fcb69df2e43bb | |
parent | 2d40635435ab225e16494b559ae030da660e91e8 (diff) |
Add latest changes from gitlab-org/gitlab@master
43 files changed, 890 insertions, 122 deletions
diff --git a/app/assets/javascripts/access_tokens/index.js b/app/assets/javascripts/access_tokens/index.js index 43d56295f78..7f5f0403de6 100644 --- a/app/assets/javascripts/access_tokens/index.js +++ b/app/assets/javascripts/access_tokens/index.js @@ -1,20 +1,10 @@ import Vue from 'vue'; import createFlash from '~/flash'; +import { parseRailsFormFields } from '~/lib/utils/forms'; import { __ } from '~/locale'; import ExpiresAtField from './components/expires_at_field.vue'; -const getInputAttrs = (el) => { - const input = el.querySelector('input'); - - return { - id: input.id, - name: input.name, - value: input.value, - placeholder: input.placeholder, - }; -}; - export const initExpiresAtField = () => { const el = document.querySelector('.js-access-tokens-expires-at'); @@ -22,7 +12,7 @@ export const initExpiresAtField = () => { return null; } - const inputAttrs = getInputAttrs(el); + const { expiresAt: inputAttrs } = parseRailsFormFields(el); return new Vue({ el, @@ -43,7 +33,7 @@ export const initProjectsField = () => { return null; } - const inputAttrs = getInputAttrs(el); + const { projects: inputAttrs } = parseRailsFormFields(el); if (window.gon.features.personalAccessTokensScopedToProjects) { return new Promise((resolve) => { diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue index 6ccaec4a633..ca66ad6934a 100644 --- a/app/assets/javascripts/boards/components/board_list_header.vue +++ b/app/assets/javascripts/boards/components/board_list_header.vue @@ -328,6 +328,7 @@ export default { <div class="issue-count-badge gl-display-inline-flex gl-pr-0 no-drag gl-text-gray-500" + data-testid="issue-count-badge" :class="{ 'gl-display-none!': list.collapsed && isSwimlanesHeader, 'gl-p-0': list.collapsed, diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue index a81c28733cd..944eb25bf7e 100644 --- a/app/assets/javascripts/boards/components/board_new_issue.vue +++ b/app/assets/javascripts/boards/components/board_new_issue.vue @@ -2,8 +2,8 @@ import { GlButton } from '@gitlab/ui'; import { mapActions, mapGetters, mapState } from 'vuex'; import { getMilestone } from 'ee_else_ce/boards/boards_util'; +import BoardNewIssueMixin from 'ee_else_ce/boards/mixins/board_new_issue'; import { __ } from '~/locale'; -import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import eventHub from '../eventhub'; import ProjectSelect from './project_select.vue'; @@ -17,8 +17,8 @@ export default { ProjectSelect, GlButton, }, - mixins: [glFeatureFlagMixin()], - inject: ['groupId', 'weightFeatureAvailable', 'boardWeight'], + mixins: [BoardNewIssueMixin], + inject: ['groupId'], props: { list: { type: Object, @@ -53,14 +53,11 @@ export default { submit(e) { e.preventDefault(); + const { title } = this; const labels = this.list.label ? [this.list.label] : []; const assignees = this.list.assignee ? [this.list.assignee] : []; const milestone = getMilestone(this.list); - const weight = this.weightFeatureAvailable ? this.boardWeight : undefined; - - const { title } = this; - eventHub.$emit(`scroll-board-list-${this.list.id}`); return this.addListNewIssue({ @@ -70,7 +67,7 @@ export default { assigneeIds: assignees?.map((a) => a?.id), milestoneId: milestone?.id, projectPath: this.selectedProject.fullPath, - weight: weight >= 0 ? weight : null, + ...this.extraIssueInput(), }, list: this.list, }).then(() => { diff --git a/app/assets/javascripts/boards/mixins/board_new_issue.js b/app/assets/javascripts/boards/mixins/board_new_issue.js new file mode 100644 index 00000000000..d4b74544735 --- /dev/null +++ b/app/assets/javascripts/boards/mixins/board_new_issue.js @@ -0,0 +1,6 @@ +export default { + // EE-only + methods: { + extraIssueInput: () => {}, + }, +}; diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js index d1a6ce9c153..bf67328bbe6 100644 --- a/app/assets/javascripts/boards/stores/actions.js +++ b/app/assets/javascripts/boards/stores/actions.js @@ -327,8 +327,8 @@ export default { commit(types.RESET_ISSUES); }, - moveItem: ({ dispatch }) => { - dispatch('moveIssue'); + moveItem: ({ dispatch }, payload) => { + dispatch('moveIssue', payload); }, moveIssue: ( diff --git a/app/assets/javascripts/ensure_data.js b/app/assets/javascripts/ensure_data.js new file mode 100644 index 00000000000..5b4d1afc9d0 --- /dev/null +++ b/app/assets/javascripts/ensure_data.js @@ -0,0 +1,56 @@ +import emptySvg from '@gitlab/svgs/dist/illustrations/security-dashboard-empty-state.svg'; +import { GlEmptyState } from '@gitlab/ui'; +import * as Sentry from '@sentry/browser'; +import { __ } from '~/locale'; + +const ERROR_FETCHING_DATA_HEADER = __('Could not get the data properly'); +const ERROR_FETCHING_DATA_DESCRIPTION = __( + 'Please try and refresh the page. If the problem persists please contact support.', +); + +/** + * This function takes a Component and extends it with data from the `parseData` function. + * The data will be made available through `props` and `proivde`. + * If the `parseData` throws, the `GlEmptyState` will be returned. + * @param {Component} Component a component to render + * @param {Object} options + * @param {Function} options.parseData a function to parse `data` + * @param {Object} options.data an object to pass to `parseData` + * @param {Boolean} options.shouldLog to tell whether to log any thrown error by `parseData` to Sentry + * @param {Object} options.props to override passed `props` data + * @param {Object} options.provide to override passed `provide` data + * @param {*} ...options the remaining options will be passed as properties to `createElement` + * @return {Component} a Vue component to render, either the GlEmptyState or the extended Component + */ +export default function ensureData(Component, options = {}) { + const { parseData, data, shouldLog = false, props, provide, ...rest } = options; + try { + const parsedData = parseData(data); + return { + provide: { ...parsedData, ...provide }, + render(createElement) { + return createElement(Component, { + props: { ...parsedData, ...props }, + ...rest, + }); + }, + }; + } catch (error) { + if (shouldLog) { + Sentry.captureException(error); + } + + return { + functional: true, + render(createElement) { + return createElement(GlEmptyState, { + props: { + title: ERROR_FETCHING_DATA_HEADER, + description: ERROR_FETCHING_DATA_DESCRIPTION, + svgPath: `data:image/svg+xml;utf8,${encodeURIComponent(emptySvg)}`, + }, + }); + }, + }; + } +} diff --git a/app/assets/javascripts/lib/utils/forms.js b/app/assets/javascripts/lib/utils/forms.js index 52e1323412d..b58aef15dda 100644 --- a/app/assets/javascripts/lib/utils/forms.js +++ b/app/assets/javascripts/lib/utils/forms.js @@ -1,3 +1,5 @@ +import { convertToCamelCase } from '~/lib/utils/text_utility'; + export const serializeFormEntries = (entries) => entries.reduce((acc, { name, value }) => Object.assign(acc, { [name]: value }), {}); @@ -51,3 +53,95 @@ export const serializeFormObject = (form) => return acc; }, []), ); + +/** + * Parse inputs of HTML forms generated by Rails. + * + * This can be helpful when mounting Vue components within Rails forms. + * + * If called with an HTML element like: + * + * ```html + * <input type="text" placeholder="Email" value="foo@bar.com" name="user[contact_info][email]" id="user_contact_info_email" data-js-name="contactInfoEmail"> + * <input type="text" placeholder="Phone" value="(123) 456-7890" name="user[contact_info][phone]" id="user_contact_info_phone" data-js-name="contactInfoPhone"> + * <input type="checkbox" name="user[interests][]" id="user_interests_vue" value="Vue" checked data-js-name="interests"> + * <input type="checkbox" name="user[interests][]" id="user_interests_graphql" value="GraphQL" data-js-name="interests"> + * ``` + * + * It will return an object like: + * + * ```javascript + * { + * contactInfoEmail: { + * name: 'user[contact_info][email]', + * id: 'user_contact_info_email', + * value: 'foo@bar.com', + * placeholder: 'Email', + * }, + * contactInfoPhone: { + * name: 'user[contact_info][phone]', + * id: 'user_contact_info_phone', + * value: '(123) 456-7890', + * placeholder: 'Phone', + * }, + * interests: [ + * { + * name: 'user[interests][]', + * id: 'user_interests_vue', + * value: 'Vue', + * checked: true, + * }, + * { + * name: 'user[interests][]', + * id: 'user_interests_graphql', + * value: 'GraphQL', + * checked: false, + * }, + * ], + * } + * ``` + * + * @param {HTMLInputElement} mountEl + * @returns {Object} object with form fields data. + */ +export const parseRailsFormFields = (mountEl) => { + if (!mountEl) { + throw new TypeError('`mountEl` argument is required'); + } + + const inputs = mountEl.querySelectorAll('[name]'); + + return [...inputs].reduce((accumulator, input) => { + const fieldName = input.dataset.jsName; + + if (!fieldName) { + return accumulator; + } + + const fieldNameCamelCase = convertToCamelCase(fieldName); + const { id, placeholder, name, value, type, checked } = input; + const attributes = { + name, + id, + value, + ...(placeholder && { placeholder }), + }; + + // Store radio buttons and checkboxes as an array so they can be + // looped through and rendered in Vue + if (['radio', 'checkbox'].includes(type)) { + return { + ...accumulator, + [fieldNameCamelCase]: [ + ...(accumulator[fieldNameCamelCase] || []), + { ...attributes, checked }, + ], + }; + } + + return { + ...accumulator, + [fieldNameCamelCase]: attributes, + }; + }, {}); +}; diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb index 69a609b6fd8..5a8090fc4bd 100644 --- a/app/controllers/projects/boards_controller.rb +++ b/app/controllers/projects/boards_controller.rb @@ -10,6 +10,7 @@ class Projects::BoardsController < Projects::ApplicationController before_action do push_frontend_feature_flag(:add_issues_button) push_frontend_feature_flag(:swimlanes_buffered_rendering, project, default_enabled: :yaml) + push_frontend_feature_flag(:graphql_board_lists, project, default_enabled: :yaml) end feature_category :boards diff --git a/app/views/shared/access_tokens/_form.html.haml b/app/views/shared/access_tokens/_form.html.haml index 9709ad8428e..88c24a27497 100644 --- a/app/views/shared/access_tokens/_form.html.haml +++ b/app/views/shared/access_tokens/_form.html.haml @@ -23,7 +23,7 @@ = render_if_exists 'personal_access_tokens/callout_max_personal_access_token_lifetime' .js-access-tokens-expires-at - = f.text_field :expires_at, class: 'datepicker gl-datepicker-input form-control gl-form-input', placeholder: 'YYYY-MM-DD', autocomplete: 'off' + = f.text_field :expires_at, class: 'datepicker gl-datepicker-input form-control gl-form-input', placeholder: 'YYYY-MM-DD', autocomplete: 'off', data: { js_name: 'expiresAt' } .form-group = f.label :scopes, _('Scopes'), class: 'label-bold' @@ -31,7 +31,7 @@ - if prefix == :personal_access_token && Feature.enabled?(:personal_access_tokens_scoped_to_projects, current_user) .js-access-tokens-projects - %input{ type: 'hidden', name: 'temporary-name', id: 'temporary-id' } + %input{ type: 'hidden', name: 'personal_access_token[projects]', id: 'personal_access_token_projects', data: { js_name: 'projects' } } .gl-mt-3 = f.submit _('Create %{type}') % { type: type }, class: 'gl-button btn btn-confirm', data: { qa_selector: 'create_token_button' } diff --git a/app/workers/concerns/worker_attributes.rb b/app/workers/concerns/worker_attributes.rb index 042508d08f2..6f99fd089ac 100644 --- a/app/workers/concerns/worker_attributes.rb +++ b/app/workers/concerns/worker_attributes.rb @@ -11,6 +11,8 @@ module WorkerAttributes # Urgencies that workers can declare through the `urgencies` attribute VALID_URGENCIES = [:high, :low, :throttled].freeze + VALID_DATA_CONSISTENCIES = [:always, :sticky, :delayed].freeze + NAMESPACE_WEIGHTS = { auto_devops: 2, auto_merge: 3, @@ -69,6 +71,35 @@ module WorkerAttributes class_attributes[:urgency] || :low end + def data_consistency(data_consistency, feature_flag: nil) + raise ArgumentError, "Invalid data consistency: #{data_consistency}" unless VALID_DATA_CONSISTENCIES.include?(data_consistency) + raise ArgumentError, 'Data consistency is already set' if class_attributes[:data_consistency] + + class_attributes[:data_consistency_feature_flag] = feature_flag if feature_flag + class_attributes[:data_consistency] = data_consistency + + validate_worker_attributes! + end + + def validate_worker_attributes! + # Since the deduplication should always take into account the latest binary replication pointer into account, + # not the first one, the deduplication will not work with sticky or delayed. + # Follow up issue to improve this: https://gitlab.com/gitlab-org/gitlab/-/issues/325291 + if idempotent? && get_data_consistency != :always + raise ArgumentError, "Class can't be marked as idempotent if data_consistency is not set to :always" + end + end + + def get_data_consistency + class_attributes[:data_consistency] || :always + end + + def get_data_consistency_feature_flag_enabled? + return true unless class_attributes[:data_consistency_feature_flag] + + Feature.enabled?(class_attributes[:data_consistency_feature_flag], default_enabled: :yaml) + end + # Set this attribute on a job when it will call to services outside of the # application, such as 3rd party applications, other k8s clusters etc See # doc/development/sidekiq_style_guide.md#jobs-with-external-dependencies for @@ -96,6 +127,8 @@ module WorkerAttributes def idempotent! class_attributes[:idempotent] = true + + validate_worker_attributes! end def idempotent? diff --git a/changelogs/unreleased/325884-usage-data-count-start-finish-problems.yml b/changelogs/unreleased/325884-usage-data-count-start-finish-problems.yml new file mode 100644 index 00000000000..33e2ffa23f5 --- /dev/null +++ b/changelogs/unreleased/325884-usage-data-count-start-finish-problems.yml @@ -0,0 +1,5 @@ +--- +title: Fix usage data count start/finish export issue +merge_request: 57403 +author: +type: fixed diff --git a/changelogs/unreleased/326057-rspec-feature-flag-failure-for-migrate_delayed_project_removal.yml b/changelogs/unreleased/326057-rspec-feature-flag-failure-for-migrate_delayed_project_removal.yml new file mode 100644 index 00000000000..16e76dbc957 --- /dev/null +++ b/changelogs/unreleased/326057-rspec-feature-flag-failure-for-migrate_delayed_project_removal.yml @@ -0,0 +1,5 @@ +--- +title: Removed migrate_delayed_project_removal feature flag +merge_request: 57541 +author: +type: other diff --git a/config/feature_flags/development/migrate_delayed_project_removal.yml b/config/feature_flags/development/migrate_delayed_project_removal.yml deleted file mode 100644 index 2d4a7ef762e..00000000000 --- a/config/feature_flags/development/migrate_delayed_project_removal.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: migrate_delayed_project_removal -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/53916 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/300207 -milestone: '13.9' -type: development -group: group::access -default_enabled: true diff --git a/doc/development/usage_ping/dictionary.md b/doc/development/usage_ping/dictionary.md index 571f5149d24..87ac8109d3c 100644 --- a/doc/development/usage_ping/dictionary.md +++ b/doc/development/usage_ping/dictionary.md @@ -9914,7 +9914,7 @@ Counts of MAU adding epic notes [YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_28d/20210314215451_g_project_management_users_creating_epic_notes_monthly.yml) -Group: `group:product planning` +Group: `group::product planning` Status: `implemented` @@ -9926,7 +9926,7 @@ Counts of WAU adding epic notes [YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_7d/20210314231518_g_project_management_users_creating_epic_notes_weekly.yml) -Group: `group:product planning` +Group: `group::product planning` Status: `implemented` @@ -9938,7 +9938,7 @@ Counts of MAU destroying epic notes [YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_28d/20210315034808_g_project_management_users_destroying_epic_notes_monthly.yml) -Group: `group:product planning` +Group: `group::product planning` Status: `implemented` @@ -9950,7 +9950,7 @@ Counts of WAU destroying epic notes [YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_7d/20210315034846_g_project_management_users_destroying_epic_notes_weekly.yml) -Group: `group:product planning` +Group: `group::product planning` Status: `implemented` @@ -9962,7 +9962,7 @@ Counts of MAU setting epic start date as fixed [YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_28d/20210315055624_g_project_management_users_setting_epic_start_date_as_fixed_monthly.yml) -Group: `group:product planning` +Group: `group::product planning` Status: `implemented` @@ -9974,7 +9974,7 @@ Counts of WAU setting epic start date as fixed [YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_7d/20210315054905_g_project_management_users_setting_epic_start_date_as_fixed_weekly.yml) -Group: `group:product planning` +Group: `group::product planning` Status: `implemented` @@ -9986,7 +9986,7 @@ Counts of MAU setting epic start date as inherited [YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_28d/20210315055439_g_project_management_users_setting_epic_start_date_as_inherited_monthly.yml) -Group: `group:product planning` +Group: `group::product planning` Status: `implemented` @@ -9998,7 +9998,7 @@ Counts of WAU setting epic start date as inherited [YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_7d/20210315055342_g_project_management_users_setting_epic_start_date_as_inherited_weekly.yml) -Group: `group:product planning` +Group: `group::product planning` Status: `implemented` @@ -10010,7 +10010,7 @@ Counts of MAU changing epic descriptions [YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_28d/20210312102051_g_project_management_users_updating_epic_descriptions_monthly.yml) -Group: `group:product planning` +Group: `group::product planning` Status: `implemented` @@ -10022,7 +10022,7 @@ Counts of WAU changing epic descriptions [YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_7d/20210312101753_g_project_management_users_updating_epic_descriptions_weekly.yml) -Group: `group:product planning` +Group: `group::product planning` Status: `implemented` @@ -10034,7 +10034,7 @@ Counts of MAU updating epic notes [YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_28d/20210314234202_g_project_management_users_updating_epic_notes_monthly.yml) -Group: `group:product planning` +Group: `group::product planning` Status: `implemented` @@ -10046,7 +10046,7 @@ Counts of WAU updating epic notes [YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_7d/20210314234041_g_project_management_users_updating_epic_notes_weekly.yml) -Group: `group:product planning` +Group: `group::product planning` Status: `implemented` @@ -10058,7 +10058,7 @@ Counts of MAU changing epic titles [YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_28d/20210312101935_g_project_management_users_updating_epic_titles_monthly.yml) -Group: `group:product planning` +Group: `group::product planning` Status: `implemented` @@ -10070,7 +10070,7 @@ Counts of WAU changing epic titles [YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_7d/20210312101826_g_project_management_users_updating_epic_titles_weekly.yml) -Group: `group:product planning` +Group: `group::product planning` Status: `implemented` diff --git a/lib/gitlab/instrumentation_helper.rb b/lib/gitlab/instrumentation_helper.rb index 61de6b02453..420803a17e7 100644 --- a/lib/gitlab/instrumentation_helper.rb +++ b/lib/gitlab/instrumentation_helper.rb @@ -16,6 +16,7 @@ module Gitlab :elasticsearch_calls, :elasticsearch_duration_s, :elasticsearch_timed_out_count, + :worker_data_consistency, *::Gitlab::Memory::Instrumentation::KEY_MAPPING.values, *::Gitlab::Instrumentation::Redis.known_payload_keys, *::Gitlab::Metrics::Subscribers::ActiveRecord::DB_COUNTERS, diff --git a/lib/gitlab/sidekiq_middleware.rb b/lib/gitlab/sidekiq_middleware.rb index a2696e17078..563a105484d 100644 --- a/lib/gitlab/sidekiq_middleware.rb +++ b/lib/gitlab/sidekiq_middleware.rb @@ -43,3 +43,5 @@ module Gitlab end end end + +Gitlab::SidekiqMiddleware.singleton_class.prepend_if_ee('EE::Gitlab::SidekiqMiddleware') diff --git a/lib/gitlab/usage_data_queries.rb b/lib/gitlab/usage_data_queries.rb index c00e7a2aa13..dc37e2fef1d 100644 --- a/lib/gitlab/usage_data_queries.rb +++ b/lib/gitlab/usage_data_queries.rb @@ -5,11 +5,11 @@ module Gitlab # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41091 class UsageDataQueries < UsageData class << self - def count(relation, column = nil, *rest) + def count(relation, column = nil, *args, **kwargs) raw_sql(relation, column) end - def distinct_count(relation, column = nil, *rest) + def distinct_count(relation, column = nil, *args, **kwargs) raw_sql(relation, column, :distinct) end @@ -21,14 +21,14 @@ module Gitlab end end - def sum(relation, column, *rest) + def sum(relation, column, *args, **kwargs) relation.select(relation.all.table[column].sum).to_sql end # For estimated distinct count use exact query instead of hll # buckets query, because it can't be used to obtain estimations without # supplementary ruby code present in Gitlab::Database::PostgresHll::BatchDistinctCounter - def estimate_batch_distinct_count(relation, column = nil, *rest) + def estimate_batch_distinct_count(relation, column = nil, *args, **kwargs) raw_sql(relation, column, :distinct) end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index da149087253..18f064a6bb8 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -8707,6 +8707,9 @@ msgstr "" msgid "Could not find iteration" msgstr "" +msgid "Could not get the data properly" +msgstr "" + msgid "Could not load the user chart. Please refresh the page to try again." msgstr "" @@ -23056,6 +23059,9 @@ msgstr "" msgid "Please try again" msgstr "" +msgid "Please try and refresh the page. If the problem persists please contact support." +msgstr "" + msgid "Please type %{phrase_code} to proceed or close this modal to cancel." msgstr "" diff --git a/qa/qa/page/project/pipeline/show.rb b/qa/qa/page/project/pipeline/show.rb index 994b1c02a3d..c5887b84be6 100644 --- a/qa/qa/page/project/pipeline/show.rb +++ b/qa/qa/page/project/pipeline/show.rb @@ -68,20 +68,30 @@ module QA end end - def has_child_pipeline? - has_element? :child_pipeline + def has_child_pipeline?(title: nil) + title ? find_child_pipeline_by_title(title) : has_element?(:child_pipeline) end def has_no_child_pipeline? - has_no_element? :child_pipeline + has_no_element?(:child_pipeline) end def click_job(job_name) click_element(:job_link, Project::Job::Show, text: job_name) end - def expand_child_pipeline - within_element(:child_pipeline) do + def child_pipelines + all_elements(:child_pipeline, minimum: 1) + end + + def find_child_pipeline_by_title(title) + child_pipelines.find { |pipeline| pipeline[:title].include?(title) } + end + + def expand_child_pipeline(title: nil) + child_pipeline = title ? find_child_pipeline_by_title(title) : child_pipelines.first + + within_element_by_index(:child_pipeline, child_pipelines.index(child_pipeline)) do click_element(:expand_pipeline_button) end end diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/trigger_matrix_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/trigger_matrix_spec.rb new file mode 100644 index 00000000000..d87fa0f5127 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/trigger_matrix_spec.rb @@ -0,0 +1,112 @@ +# frozen_string_literal: true + +require 'faker' + +module QA + RSpec.describe 'Verify', :runner do + describe 'Trigger matrix' do + let(:executor) { "qa-runner-#{Faker::Alphanumeric.alphanumeric(8)}" } + + let(:project) do + Resource::Project.fabricate_via_api! do |project| + project.name = 'project-with-pipeline' + end + end + + let!(:runner) do + Resource::Runner.fabricate! do |runner| + runner.project = project + runner.name = executor + runner.tags = [executor] + end + end + + before do + Flow::Login.sign_in + add_ci_files + project.visit! + Flow::Pipeline.visit_latest_pipeline(pipeline_condition: 'succeeded') + end + + after do + runner.remove_via_api! + project.remove_via_api! + end + + it 'creates 2 trigger jobs and passes corresponding matrix variables', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1732' do + Page::Project::Pipeline::Show.perform do |parent_pipeline| + trigger_title1 = 'deploy: [ovh, monitoring]' + trigger_title2 = 'deploy: [ovh, app]' + + aggregate_failures 'Creates two child pipelines' do + expect(parent_pipeline).to have_child_pipeline(title: trigger_title1) + expect(parent_pipeline).to have_child_pipeline(title: trigger_title2) + end + + # Only check output of one of the child pipelines, should be sufficient + parent_pipeline.expand_child_pipeline(title: trigger_title1) + parent_pipeline.click_job('test_vars') + end + + Page::Project::Job::Show.perform do |show| + Support::Waiter.wait_until { show.successful? } + + aggregate_failures 'Job output has the correct variables' do + expect(show.output).to have_content('ovh') + expect(show.output).to have_content('monitoring') + end + end + end + + private + + def add_ci_files + Resource::Repository::Commit.fabricate_via_api! do |commit| + commit.project = project + commit.commit_message = 'Add parent and child pipelines CI files.' + commit.add_files( + [ + child_ci_file, + parent_ci_file + ] + ) + end + end + + def parent_ci_file + { + file_path: '.gitlab-ci.yml', + content: <<~YAML + test: + stage: test + script: echo test + tags: [#{executor}] + + deploy: + stage: deploy + trigger: + include: child.yml + parallel: + matrix: + - PROVIDER: ovh + STACK: [monitoring, app] + + YAML + } + end + + def child_ci_file + { + file_path: 'child.yml', + content: <<~YAML + test_vars: + script: + - echo $PROVIDER + - echo $STACK + tags: [#{executor}] + YAML + } + end + end + end +end diff --git a/scripts/review_apps/base-config.yaml b/scripts/review_apps/base-config.yaml index cf55fca7452..5b4b557c305 100644 --- a/scripts/review_apps/base-config.yaml +++ b/scripts/review_apps/base-config.yaml @@ -70,10 +70,10 @@ gitlab: resources: requests: cpu: 746m - memory: 1873M + memory: 2809M limits: cpu: 1119m - memory: 2809M + memory: 4214M deployment: readinessProbe: initialDelaySeconds: 5 # Default is 0 @@ -83,10 +83,10 @@ gitlab: resources: requests: cpu: 400m - memory: 50M + memory: 75M limits: cpu: 600m - memory: 75M + memory: 113M readinessProbe: initialDelaySeconds: 5 # Default is 0 periodSeconds: 15 # Default is 10 diff --git a/spec/features/boards/add_issues_modal_spec.rb b/spec/features/boards/add_issues_modal_spec.rb index 8d0fa3e023b..ff9e0b9d054 100644 --- a/spec/features/boards/add_issues_modal_spec.rb +++ b/spec/features/boards/add_issues_modal_spec.rb @@ -14,6 +14,7 @@ RSpec.describe 'Issue Boards add issue modal', :js do let!(:issue2) { create(:issue, project: project, title: 'hij', description: 'klm') } before do + stub_feature_flags(graphql_board_lists: false) project.add_maintainer(user) sign_in(user) diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index 2392f9d2f8a..ab544022bff 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'Issue Boards', :js do +RSpec.describe 'Project issue boards', :js do include DragTo include MobileHelpers @@ -23,7 +23,7 @@ RSpec.describe 'Issue Boards', :js do context 'no lists' do before do - visit project_board_path(project, board) + visit_project_board_path_without_query_limit(project, board) end it 'creates default lists' do @@ -52,6 +52,7 @@ RSpec.describe 'Issue Boards', :js do let_it_be(:a_plus) { create(:label, project: project, name: 'A+') } let_it_be(:list1) { create(:list, board: board, label: planning, position: 0) } let_it_be(:list2) { create(:list, board: board, label: development, position: 1) } + let_it_be(:backlog_list) { create(:backlog_list, board: board) } let_it_be(:confidential_issue) { create(:labeled_issue, :confidential, project: project, author: user, labels: [planning], relative_position: 9) } let_it_be(:issue1) { create(:labeled_issue, project: project, title: 'aaa', description: '111', assignees: [user], labels: [planning], relative_position: 8) } @@ -68,7 +69,7 @@ RSpec.describe 'Issue Boards', :js do before do stub_feature_flags(board_new_list: false) - visit project_board_path(project, board) + visit_project_board_path_without_query_limit(project, board) wait_for_requests @@ -121,7 +122,8 @@ RSpec.describe 'Issue Boards', :js do context 'with the NOT queries feature flag disabled' do before do stub_feature_flags(not_issuable_queries: false) - visit project_board_path(project, board) + + visit_project_board_path_without_query_limit(project, board) end it 'does not have the != option' do @@ -141,7 +143,8 @@ RSpec.describe 'Issue Boards', :js do context 'with the NOT queries feature flag enabled' do before do stub_feature_flags(not_issuable_queries: true) - visit project_board_path(project, board) + + visit_project_board_path_without_query_limit(project, board) end it 'does not have the != option' do @@ -171,8 +174,7 @@ RSpec.describe 'Issue Boards', :js do it 'infinite scrolls list' do create_list(:labeled_issue, 50, project: project, labels: [planning]) - visit project_board_path(project, board) - wait_for_requests + visit_project_board_path_without_query_limit(project, board) page.within(find('.board:nth-child(2)')) do expect(page.find('.board-header')).to have_content('58') @@ -180,15 +182,19 @@ RSpec.describe 'Issue Boards', :js do expect(page).to have_content('Showing 20 of 58 issues') find('.board .board-list') - evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight") - wait_for_requests + + inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do + evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight") + end expect(page).to have_selector('.board-card', count: 40) expect(page).to have_content('Showing 40 of 58 issues') find('.board .board-list') - evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight") - wait_for_requests + + inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do + evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight") + end expect(page).to have_selector('.board-card', count: 58) expect(page).to have_content('Showing all issues') @@ -236,13 +242,13 @@ RSpec.describe 'Issue Boards', :js do wait_for_board_cards(4, 1) expect(find('.board:nth-child(2)')).to have_content(development.title) - expect(find('.board:nth-child(2)')).to have_content(planning.title) + expect(find('.board:nth-child(3)')).to have_content(planning.title) # Make sure list positions are preserved after a reload - visit project_board_path(project, board) + visit_project_board_path_without_query_limit(project, board) expect(find('.board:nth-child(2)')).to have_content(development.title) - expect(find('.board:nth-child(2)')).to have_content(planning.title) + expect(find('.board:nth-child(3)')).to have_content(planning.title) end it 'dragging does not duplicate list' do @@ -254,7 +260,8 @@ RSpec.describe 'Issue Boards', :js do expect(page).to have_selector(selector, text: development.title, count: 1) end - it 'issue moves between lists and does not show the "Development" label since the card is in the "Development" list label' do + # TODO https://gitlab.com/gitlab-org/gitlab/-/issues/323551 + xit 'issue moves between lists and does not show the "Development" label since the card is in the "Development" list label' do drag(list_from_index: 1, from_index: 1, list_to_index: 2) wait_for_board_cards(2, 7) @@ -467,14 +474,16 @@ RSpec.describe 'Issue Boards', :js do end it 'removes filtered labels' do - set_filter("label", testing.title) - click_filter_link(testing.title) - submit_filter + inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do + set_filter("label", testing.title) + click_filter_link(testing.title) + submit_filter - wait_for_board_cards(2, 1) + wait_for_board_cards(2, 1) - find('.clear-search').click - submit_filter + find('.clear-search').click + submit_filter + end wait_for_board_cards(2, 8) end @@ -484,7 +493,9 @@ RSpec.describe 'Issue Boards', :js do set_filter("label", testing.title) click_filter_link(testing.title) - submit_filter + inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do + submit_filter + end wait_for_requests @@ -494,13 +505,18 @@ RSpec.describe 'Issue Boards', :js do expect(page).to have_content('Showing 20 of 51 issues') find('.board .board-list') - evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight") + + inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do + evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight") + end expect(page).to have_selector('.board-card', count: 40) expect(page).to have_content('Showing 40 of 51 issues') find('.board .board-list') - evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight") + inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do + evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight") + end expect(page).to have_selector('.board-card', count: 51) expect(page).to have_content('Showing all issues') @@ -569,7 +585,7 @@ RSpec.describe 'Issue Boards', :js do context 'keyboard shortcuts' do before do - visit project_board_path(project, board) + visit_project_board_path_without_query_limit(project, board) wait_for_requests end @@ -617,15 +633,19 @@ RSpec.describe 'Issue Boards', :js do def drag(selector: '.board-list', list_from_index: 0, from_index: 0, to_index: 0, list_to_index: 0, perform_drop: true) # ensure there is enough horizontal space for four boards - resize_window(2000, 800) - - drag_to(selector: selector, - scrollable: '#board-app', - list_from_index: list_from_index, - from_index: from_index, - to_index: to_index, - list_to_index: list_to_index, - perform_drop: perform_drop) + inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do + resize_window(2000, 800) + + drag_to(selector: selector, + scrollable: '#board-app', + list_from_index: list_from_index, + from_index: from_index, + to_index: to_index, + list_to_index: list_to_index, + perform_drop: perform_drop) + end + + wait_for_requests end def wait_for_board_cards(board_number, expected_cards) @@ -666,4 +686,10 @@ RSpec.describe 'Issue Boards', :js do accept_confirm { find('[data-testid="remove-list"]').click } end end + + def visit_project_board_path_without_query_limit(project, board) + inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do + visit project_board_path(project, board) + end + end end diff --git a/spec/features/boards/modal_filter_spec.rb b/spec/features/boards/modal_filter_spec.rb index 5aeb9eb5e50..d2b7686a9e2 100644 --- a/spec/features/boards/modal_filter_spec.rb +++ b/spec/features/boards/modal_filter_spec.rb @@ -12,6 +12,8 @@ RSpec.describe 'Issue Boards add issue modal filtering', :js do let!(:issue1) { create(:issue, project: project) } before do + stub_feature_flags(graphql_board_lists: false) + stub_feature_flags(add_issues_button: true) project.add_maintainer(user) sign_in(user) diff --git a/spec/features/boards/multi_select_spec.rb b/spec/features/boards/multi_select_spec.rb index 162455f75e6..ca322355b8f 100644 --- a/spec/features/boards/multi_select_spec.rb +++ b/spec/features/boards/multi_select_spec.rb @@ -41,6 +41,10 @@ RSpec.describe 'Multi Select Issue', :js do before do project.add_maintainer(user) + # multi-drag disabled with feature flag for now + # https://gitlab.com/gitlab-org/gitlab/-/issues/289797 + stub_feature_flags(graphql_board_lists: false) + sign_in(user) end diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb index f434ea0c66f..4d419f89aa3 100644 --- a/spec/features/boards/new_issue_spec.rb +++ b/spec/features/boards/new_issue_spec.rb @@ -3,10 +3,12 @@ require 'spec_helper' RSpec.describe 'Issue Boards new issue', :js do - let(:project) { create(:project, :public) } - let(:board) { create(:board, project: project) } - let!(:list) { create(:list, board: board, position: 0) } - let(:user) { create(:user) } + let_it_be(:project) { create(:project, :public) } + let_it_be(:board) { create(:board, project: project) } + let_it_be(:backlog_list) { create(:backlog_list, board: board) } + let_it_be(:label) { create(:label, project: project, name: 'Label 1') } + let_it_be(:list) { create(:list, board: board, label: label, position: 0) } + let_it_be(:user) { create(:user) } context 'authorized user' do before do @@ -15,6 +17,7 @@ RSpec.describe 'Issue Boards new issue', :js do sign_in(user) visit project_board_path(project, board) + wait_for_requests expect(page).to have_selector('.board', count: 3) @@ -70,11 +73,12 @@ RSpec.describe 'Issue Boards new issue', :js do issue = project.issues.find_by_title('bug') expect(page).to have_content(issue.to_reference) - expect(page).to have_link(issue.title, href: issue_path(issue)) + expect(page).to have_link(issue.title, href: /#{issue_path(issue)}/) end end - it 'shows sidebar when creating new issue' do + # TODO https://gitlab.com/gitlab-org/gitlab/-/issues/323446 + xit 'shows sidebar when creating new issue' do page.within(first('.board')) do find('.issue-count-badge-add-button').click end @@ -101,12 +105,16 @@ RSpec.describe 'Issue Boards new issue', :js do wait_for_requests + page.within(first('.board')) do + find('.board-card').click + end + page.within(first('.issue-boards-sidebar')) do - find('.labels .edit-link').click + find('.labels [data-testid="edit-button"]').click wait_for_requests - expect(page).to have_selector('.labels .dropdown-content li a') + expect(page).to have_selector('.labels-select-contents-list .dropdown-content li a') end end end diff --git a/spec/features/boards/reload_boards_on_browser_back_spec.rb b/spec/features/boards/reload_boards_on_browser_back_spec.rb index 181cbcc9811..3530be20009 100644 --- a/spec/features/boards/reload_boards_on_browser_back_spec.rb +++ b/spec/features/boards/reload_boards_on_browser_back_spec.rb @@ -43,7 +43,7 @@ RSpec.describe 'Ensure Boards do not show stale data on browser back', :js do issue = project.issues.find_by_title('issue should be shown') expect(page).to have_content(issue.to_reference) - expect(page).to have_link(issue.title, href: issue_path(issue)) + expect(page).to have_link(issue.title, href: /#{issue_path(issue)}/) end end end diff --git a/spec/features/boards/sidebar_assignee_spec.rb b/spec/features/boards/sidebar_assignee_spec.rb index 82383ece2d3..6835721bb4d 100644 --- a/spec/features/boards/sidebar_assignee_spec.rb +++ b/spec/features/boards/sidebar_assignee_spec.rb @@ -17,6 +17,8 @@ RSpec.describe 'Project issue boards sidebar assignee', :js do let(:card) { find('.board:nth-child(2)').first('.board-card') } before do + stub_feature_flags(graphql_board_lists: false) + project.add_maintainer(user) sign_in(user) diff --git a/spec/features/boards/sidebar_due_date_spec.rb b/spec/features/boards/sidebar_due_date_spec.rb index f2d51fb56a7..52ec51c317e 100644 --- a/spec/features/boards/sidebar_due_date_spec.rb +++ b/spec/features/boards/sidebar_due_date_spec.rb @@ -17,6 +17,8 @@ RSpec.describe 'Project issue boards sidebar due date', :js do end before do + stub_feature_flags(graphql_board_lists: false) + project.add_maintainer(user) sign_in(user) diff --git a/spec/features/boards/sidebar_labels_spec.rb b/spec/features/boards/sidebar_labels_spec.rb index d6e908698c6..37de561e689 100644 --- a/spec/features/boards/sidebar_labels_spec.rb +++ b/spec/features/boards/sidebar_labels_spec.rb @@ -18,6 +18,8 @@ RSpec.describe 'Project issue boards sidebar labels', :js do let(:card) { find('.board:nth-child(2)').first('.board-card') } before do + stub_feature_flags(graphql_board_lists: false) + project.add_maintainer(user) sign_in(user) diff --git a/spec/features/boards/sidebar_milestones_spec.rb b/spec/features/boards/sidebar_milestones_spec.rb index d4f130ba3ee..d815d60d5b0 100644 --- a/spec/features/boards/sidebar_milestones_spec.rb +++ b/spec/features/boards/sidebar_milestones_spec.rb @@ -16,6 +16,8 @@ RSpec.describe 'Project issue boards sidebar milestones', :js do let(:card2) { find('.board:nth-child(1) .board-card:nth-of-type(2)') } before do + stub_feature_flags(graphql_board_lists: false) + project.add_maintainer(user) sign_in(user) diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb index 4c93707cc44..45fe5ab8376 100644 --- a/spec/features/boards/sidebar_spec.rb +++ b/spec/features/boards/sidebar_spec.rb @@ -13,6 +13,8 @@ RSpec.describe 'Project issue boards sidebar', :js do let(:card) { find('.board:nth-child(1)').first('.board-card') } before do + stub_feature_flags(graphql_board_lists: false) + project.add_maintainer(user) sign_in(user) diff --git a/spec/features/boards/sidebar_subscription_spec.rb b/spec/features/boards/sidebar_subscription_spec.rb index 77766e909f9..598fec7514e 100644 --- a/spec/features/boards/sidebar_subscription_spec.rb +++ b/spec/features/boards/sidebar_subscription_spec.rb @@ -16,6 +16,8 @@ RSpec.describe 'Project issue boards sidebar subscription', :js do let(:card2) { find('.board:nth-child(1) .board-card:nth-of-type(2)') } before do + stub_feature_flags(graphql_board_lists: false) + project.add_maintainer(user) sign_in(user) diff --git a/spec/features/boards/sidebar_time_tracking_spec.rb b/spec/features/boards/sidebar_time_tracking_spec.rb index 0cdf1e9a787..3ac8b93692a 100644 --- a/spec/features/boards/sidebar_time_tracking_spec.rb +++ b/spec/features/boards/sidebar_time_tracking_spec.rb @@ -15,6 +15,8 @@ RSpec.describe 'Project issue boards sidebar time tracking', :js do let(:application_settings) { {} } before do + stub_feature_flags(graphql_board_lists: false) + project.add_maintainer(user) sign_in(user) diff --git a/spec/features/boards/sub_group_project_spec.rb b/spec/features/boards/sub_group_project_spec.rb index cd3d61726f6..bde5f061a67 100644 --- a/spec/features/boards/sub_group_project_spec.rb +++ b/spec/features/boards/sub_group_project_spec.rb @@ -21,7 +21,8 @@ RSpec.describe 'Sub-group project issue boards', :js do wait_for_requests end - it 'creates new label from sidebar' do + # TODO https://gitlab.com/gitlab-org/gitlab/-/issues/324290 + xit 'creates new label from sidebar' do find('.board-card').click page.within '.labels' do diff --git a/spec/frontend/access_tokens/index_spec.js b/spec/frontend/access_tokens/index_spec.js index e3f17e21739..1d8ac7cec25 100644 --- a/spec/frontend/access_tokens/index_spec.js +++ b/spec/frontend/access_tokens/index_spec.js @@ -25,18 +25,22 @@ describe('access tokens', () => { }); describe.each` - initFunction | mountSelector | expectedComponent - ${initExpiresAtField} | ${'js-access-tokens-expires-at'} | ${ExpiresAtField} - ${initProjectsField} | ${'js-access-tokens-projects'} | ${ProjectsField} - `('$initFunction', ({ initFunction, mountSelector, expectedComponent }) => { + initFunction | mountSelector | fieldName | expectedComponent + ${initExpiresAtField} | ${'js-access-tokens-expires-at'} | ${'expiresAt'} | ${ExpiresAtField} + ${initProjectsField} | ${'js-access-tokens-projects'} | ${'projects'} | ${ProjectsField} + `('$initFunction', ({ initFunction, mountSelector, fieldName, expectedComponent }) => { describe('when mount element exists', () => { + const nameAttribute = `access_tokens[${fieldName}]`; + const idAttribute = `access_tokens_${fieldName}`; + beforeEach(() => { const mountEl = document.createElement('div'); mountEl.classList.add(mountSelector); const input = document.createElement('input'); - input.setAttribute('name', 'foo-bar'); - input.setAttribute('id', 'foo-bar'); + input.setAttribute('name', nameAttribute); + input.setAttribute('data-js-name', fieldName); + input.setAttribute('id', idAttribute); input.setAttribute('placeholder', 'Foo bar'); input.setAttribute('value', '1,2'); @@ -57,8 +61,8 @@ describe('access tokens', () => { expect(component.exists()).toBe(true); expect(component.props('inputAttrs')).toEqual({ - name: 'foo-bar', - id: 'foo-bar', + name: nameAttribute, + id: idAttribute, value: '1,2', placeholder: 'Foo bar', }); diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js index c0f91d3c629..7511a4ad2cb 100644 --- a/spec/frontend/boards/stores/actions_spec.js +++ b/spec/frontend/boards/stores/actions_spec.js @@ -639,10 +639,13 @@ describe('resetIssues', () => { }); describe('moveItem', () => { - it('should dispatch moveIssue action', () => { + it('should dispatch moveIssue action with payload', () => { + const payload = { mock: 'payload' }; + testAction({ action: actions.moveItem, - expectedActions: [{ type: 'moveIssue' }], + payload, + expectedActions: [{ type: 'moveIssue', payload }], }); }); }); diff --git a/spec/frontend/lib/utils/forms_spec.js b/spec/frontend/lib/utils/forms_spec.js index f65bd8ffe0c..123d36ac5d5 100644 --- a/spec/frontend/lib/utils/forms_spec.js +++ b/spec/frontend/lib/utils/forms_spec.js @@ -1,4 +1,9 @@ -import { serializeForm, serializeFormObject, isEmptyValue } from '~/lib/utils/forms'; +import { + serializeForm, + serializeFormObject, + isEmptyValue, + parseRailsFormFields, +} from '~/lib/utils/forms'; describe('lib/utils/forms', () => { const createDummyForm = (inputs) => { @@ -135,4 +140,160 @@ describe('lib/utils/forms', () => { }); }); }); + + describe('parseRailsFormFields', () => { + let mountEl; + + beforeEach(() => { + mountEl = document.createElement('div'); + mountEl.classList.add('js-foo-bar'); + }); + + afterEach(() => { + mountEl = null; + }); + + it('parses fields generated by Rails and returns object with HTML attributes', () => { + mountEl.innerHTML = ` + <input type="text" placeholder="Name" value="Administrator" name="user[name]" id="user_name" data-js-name="name"> + <input type="text" placeholder="Email" value="foo@bar.com" name="user[contact_info][email]" id="user_contact_info_email" data-js-name="contactInfoEmail"> + <input type="text" placeholder="Phone" value="(123) 456-7890" name="user[contact_info][phone]" id="user_contact_info_phone" data-js-name="contact_info_phone"> + <input type="hidden" placeholder="Job title" value="" name="user[job_title]" id="user_job_title" data-js-name="jobTitle"> + <textarea name="user[bio]" id="user_bio" data-js-name="bio">Foo bar</textarea> + <select name="user[timezone]" id="user_timezone" data-js-name="timezone"> + <option value="utc+12">[UTC - 12] International Date Line West</option> + <option value="utc+11" selected>[UTC - 11] American Samoa</option> + </select> + <input type="checkbox" name="user[interests][]" id="user_interests_vue" value="Vue" checked data-js-name="interests"> + <input type="checkbox" name="user[interests][]" id="user_interests_graphql" value="GraphQL" data-js-name="interests"> + <input type="radio" name="user[access_level]" value="regular" id="user_access_level_regular" data-js-name="accessLevel"> + <input type="radio" name="user[access_level]" value="admin" id="user_access_level_admin" checked data-js-name="access_level"> + <input name="user[private_profile]" type="hidden" value="0"> + <input type="radio" name="user[private_profile]" id="user_private_profile" value="1" checked data-js-name="privateProfile"> + <input name="user[email_notifications]" type="hidden" value="0"> + <input type="radio" name="user[email_notifications]" id="user_email_notifications" value="1" data-js-name="emailNotifications"> + `; + + expect(parseRailsFormFields(mountEl)).toEqual({ + name: { + name: 'user[name]', + id: 'user_name', + value: 'Administrator', + placeholder: 'Name', + }, + contactInfoEmail: { + name: 'user[contact_info][email]', + id: 'user_contact_info_email', + value: 'foo@bar.com', + placeholder: 'Email', + }, + contactInfoPhone: { + name: 'user[contact_info][phone]', + id: 'user_contact_info_phone', + value: '(123) 456-7890', + placeholder: 'Phone', + }, + jobTitle: { + name: 'user[job_title]', + id: 'user_job_title', + value: '', + placeholder: 'Job title', + }, + bio: { + name: 'user[bio]', + id: 'user_bio', + value: 'Foo bar', + }, + timezone: { + name: 'user[timezone]', + id: 'user_timezone', + value: 'utc+11', + }, + interests: [ + { + name: 'user[interests][]', + id: 'user_interests_vue', + value: 'Vue', + checked: true, + }, + { + name: 'user[interests][]', + id: 'user_interests_graphql', + value: 'GraphQL', + checked: false, + }, + ], + accessLevel: [ + { + name: 'user[access_level]', + id: 'user_access_level_regular', + value: 'regular', + checked: false, + }, + { + name: 'user[access_level]', + id: 'user_access_level_admin', + value: 'admin', + checked: true, + }, + ], + privateProfile: [ + { + name: 'user[private_profile]', + id: 'user_private_profile', + value: '1', + checked: true, + }, + ], + emailNotifications: [ + { + name: 'user[email_notifications]', + id: 'user_email_notifications', + value: '1', + checked: false, + }, + ], + }); + }); + + it('returns an empty object if there are no inputs', () => { + expect(parseRailsFormFields(mountEl)).toEqual({}); + }); + + it('returns an empty object if inputs do not have `name` attributes', () => { + mountEl.innerHTML = ` + <input type="text" placeholder="Name" value="Administrator" id="user_name"> + <input type="text" placeholder="Email" value="foo@bar.com" id="user_contact_info_email"> + <input type="text" placeholder="Phone" value="(123) 456-7890" id="user_contact_info_phone"> + `; + + expect(parseRailsFormFields(mountEl)).toEqual({}); + }); + + it('does not include field if `data-js-name` attribute is missing', () => { + mountEl.innerHTML = ` + <input type="text" placeholder="Name" value="Administrator" name="user[name]" id="user_name" data-js-name="name"> + <input type="text" placeholder="Email" value="foo@bar.com" name="user[email]" id="email"> + `; + + expect(parseRailsFormFields(mountEl)).toEqual({ + name: { + name: 'user[name]', + id: 'user_name', + value: 'Administrator', + placeholder: 'Name', + }, + }); + }); + + it('throws error if `mountEl` argument is not passed', () => { + expect(() => parseRailsFormFields()).toThrow(new TypeError('`mountEl` argument is required')); + }); + + it('throws error if `mountEl` argument is `null`', () => { + expect(() => parseRailsFormFields(null)).toThrow( + new TypeError('`mountEl` argument is required'), + ); + }); + }); }); diff --git a/spec/frontend/vue_shared/components/ensure_data_spec.js b/spec/frontend/vue_shared/components/ensure_data_spec.js new file mode 100644 index 00000000000..eef8b452f5f --- /dev/null +++ b/spec/frontend/vue_shared/components/ensure_data_spec.js @@ -0,0 +1,145 @@ +import { GlEmptyState } from '@gitlab/ui'; +import * as Sentry from '@sentry/browser'; +import { mount } from '@vue/test-utils'; +import ensureData from '~/ensure_data'; + +const mockData = { message: 'Hello there' }; +const defaultOptions = { + parseData: () => mockData, + data: mockData, +}; + +const MockChildComponent = { + inject: ['message'], + render(createElement) { + return createElement('h1', this.message); + }, +}; + +const MockParentComponent = { + components: { + MockChildComponent, + }, + props: { + message: { + type: String, + required: true, + }, + otherProp: { + type: Boolean, + default: false, + required: false, + }, + }, + render(createElement) { + return createElement('div', [this.message, createElement(MockChildComponent)]); + }, +}; + +describe('EnsureData', () => { + let wrapper; + + function findEmptyState() { + return wrapper.findComponent(GlEmptyState); + } + + function findChild() { + return wrapper.findComponent(MockChildComponent); + } + function findParent() { + return wrapper.findComponent(MockParentComponent); + } + + function createComponent(options = defaultOptions) { + return mount(ensureData(MockParentComponent, options)); + } + + beforeEach(() => { + Sentry.captureException = jest.fn(); + }); + + afterEach(() => { + wrapper.destroy(); + Sentry.captureException.mockClear(); + }); + + describe('when parseData throws', () => { + it('should render GlEmptyState', () => { + wrapper = createComponent({ + parseData: () => { + throw new Error(); + }, + }); + + expect(findParent().exists()).toBe(false); + expect(findChild().exists()).toBe(false); + expect(findEmptyState().exists()).toBe(true); + }); + + it('should not log to Sentry when shouldLog=false (default)', () => { + wrapper = createComponent({ + parseData: () => { + throw new Error(); + }, + }); + + expect(Sentry.captureException).not.toHaveBeenCalled(); + }); + + it('should log to Sentry when shouldLog=true', () => { + const error = new Error('Error!'); + wrapper = createComponent({ + parseData: () => { + throw error; + }, + shouldLog: true, + }); + + expect(Sentry.captureException).toHaveBeenCalledWith(error); + }); + }); + + describe('when parseData succeeds', () => { + it('should render MockParentComponent and MockChildComponent', () => { + wrapper = createComponent(); + + expect(findEmptyState().exists()).toBe(false); + expect(findParent().exists()).toBe(true); + expect(findChild().exists()).toBe(true); + }); + + it('enables user to provide data to child components', () => { + wrapper = createComponent(); + + const childComponent = findChild(); + expect(childComponent.text()).toBe(mockData.message); + }); + + it('enables user to override provide data', () => { + const message = 'Another message'; + wrapper = createComponent({ ...defaultOptions, provide: { message } }); + + const childComponent = findChild(); + expect(childComponent.text()).toBe(message); + }); + + it('enables user to pass props to parent component', () => { + wrapper = createComponent(); + + expect(findParent().props()).toMatchObject(mockData); + }); + + it('enables user to override props data', () => { + const props = { message: 'Another message', otherProp: true }; + wrapper = createComponent({ ...defaultOptions, props }); + + expect(findParent().props()).toMatchObject(props); + }); + + it('should not log to Sentry when shouldLog=true', () => { + wrapper = createComponent({ ...defaultOptions, shouldLog: true }); + + expect(Sentry.captureException).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/lib/gitlab/instrumentation_helper_spec.rb b/spec/lib/gitlab/instrumentation_helper_spec.rb index a5c9cde4c37..bbfdb8f4c2c 100644 --- a/spec/lib/gitlab/instrumentation_helper_spec.rb +++ b/spec/lib/gitlab/instrumentation_helper_spec.rb @@ -17,6 +17,7 @@ RSpec.describe Gitlab::InstrumentationHelper do :elasticsearch_calls, :elasticsearch_duration_s, :elasticsearch_timed_out_count, + :worker_data_consistency, :mem_objects, :mem_bytes, :mem_mallocs, diff --git a/spec/lib/gitlab/usage_data_queries_spec.rb b/spec/lib/gitlab/usage_data_queries_spec.rb index 12eac643383..113df26cdca 100644 --- a/spec/lib/gitlab/usage_data_queries_spec.rb +++ b/spec/lib/gitlab/usage_data_queries_spec.rb @@ -11,12 +11,24 @@ RSpec.describe Gitlab::UsageDataQueries do it 'returns the raw SQL' do expect(described_class.count(User)).to start_with('SELECT COUNT("users"."id") FROM "users"') end + + it 'does not mix a nil column with keyword arguments' do + expect(described_class).to receive(:raw_sql).with(User, nil) + + described_class.count(User, start: 1, finish: 2) + end end describe '.distinct_count' do it 'returns the raw SQL' do expect(described_class.distinct_count(Issue, :author_id)).to eq('SELECT COUNT(DISTINCT "issues"."author_id") FROM "issues"') end + + it 'does not mix a nil column with keyword arguments' do + expect(described_class).to receive(:raw_sql).with(Issue, nil, :distinct) + + described_class.distinct_count(Issue, nil, start: 1, finish: 2) + end end describe '.redis_usage_data' do diff --git a/spec/support/helpers/next_instance_of.rb b/spec/support/helpers/next_instance_of.rb index a8e9ab2bafe..95d8936588c 100644 --- a/spec/support/helpers/next_instance_of.rb +++ b/spec/support/helpers/next_instance_of.rb @@ -2,25 +2,26 @@ module NextInstanceOf def expect_next_instance_of(klass, *new_args, &blk) - stub_new(expect(klass), nil, *new_args, &blk) + stub_new(expect(klass), nil, false, *new_args, &blk) end - def expect_next_instances_of(klass, number, *new_args, &blk) - stub_new(expect(klass), number, *new_args, &blk) + def expect_next_instances_of(klass, number, ordered = false, *new_args, &blk) + stub_new(expect(klass), number, ordered, *new_args, &blk) end def allow_next_instance_of(klass, *new_args, &blk) - stub_new(allow(klass), nil, *new_args, &blk) + stub_new(allow(klass), nil, false, *new_args, &blk) end - def allow_next_instances_of(klass, number, *new_args, &blk) - stub_new(allow(klass), number, *new_args, &blk) + def allow_next_instances_of(klass, number, ordered = false, *new_args, &blk) + stub_new(allow(klass), number, ordered, *new_args, &blk) end private - def stub_new(target, number, *new_args, &blk) + def stub_new(target, number, ordered = false, *new_args, &blk) receive_new = receive(:new) + receive_new.ordered if ordered receive_new.exactly(number).times if number receive_new.with(*new_args) if new_args.any? diff --git a/spec/workers/concerns/worker_attributes_spec.rb b/spec/workers/concerns/worker_attributes_spec.rb new file mode 100644 index 00000000000..a654ecbd3e2 --- /dev/null +++ b/spec/workers/concerns/worker_attributes_spec.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe WorkerAttributes do + let(:worker) do + Class.new do + def self.name + "TestWorker" + end + + include ApplicationWorker + end + end + + describe '.data_consistency' do + context 'with valid data_consistency' do + it 'returns correct data_consistency' do + worker.data_consistency(:sticky) + + expect(worker.get_data_consistency).to eq(:sticky) + end + end + + context 'when data_consistency is not provided' do + it 'defaults to :always' do + expect(worker.get_data_consistency).to eq(:always) + end + end + + context 'with invalid data_consistency' do + it 'raise exception' do + expect { worker.data_consistency(:invalid) } + .to raise_error('Invalid data consistency: invalid') + end + end + + context 'when job is idempotent' do + context 'when data_consistency is not :always' do + it 'raise exception' do + worker.idempotent! + + expect { worker.data_consistency(:sticky) } + .to raise_error("Class can't be marked as idempotent if data_consistency is not set to :always") + end + end + + context 'when feature_flag is provided' do + before do + stub_feature_flags(test_feature_flag: false) + skip_feature_flags_yaml_validation + skip_default_enabled_yaml_check + end + + it 'returns correct feature flag value' do + worker.data_consistency(:sticky, feature_flag: :test_feature_flag) + + expect(worker.get_data_consistency_feature_flag_enabled?).not_to be_truthy + end + end + end + end + + describe '.idempotent!' do + context 'when data consistency is not :always' do + it 'raise exception' do + worker.data_consistency(:sticky) + + expect { worker.idempotent! } + .to raise_error("Class can't be marked as idempotent if data_consistency is not set to :always") + end + end + end +end |