summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-04-11 18:08:31 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2023-04-11 18:08:31 +0000
commit1a2f754734eb189e371e25e685413808f69a7f2c (patch)
tree2c97884971f36d9026600897b74364d2e212a109
parentf1ce71c88c407709987dd4a7b40bdb7596b6baa2 (diff)
downloadgitlab-ce-1a2f754734eb189e371e25e685413808f69a7f2c.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab-ci.yml1
-rw-r--r--.gitlab/ci/build-images.gitlab-ci.yml7
-rw-r--r--.gitlab/ci/frontend.gitlab-ci.yml6
-rw-r--r--.gitlab/ci/package-and-test/main.gitlab-ci.yml30
-rw-r--r--.gitlab/ci/package-and-test/rules.gitlab-ci.yml10
-rw-r--r--.gitlab/ci/qa.gitlab-ci.yml19
-rw-r--r--.gitlab/ci/rails.gitlab-ci.yml152
-rw-r--r--.gitlab/ci/rails/rspec-foss-impact.gitlab-ci.yml.erb2
-rw-r--r--.gitlab/ci/rails/rspec-predictive.gitlab-ci.yml.erb153
-rw-r--r--.gitlab/ci/rails/shared.gitlab-ci.yml5
-rw-r--r--.gitlab/ci/rules.gitlab-ci.yml346
-rw-r--r--.gitlab/ci/setup.gitlab-ci.yml13
-rw-r--r--.rubocop_todo/layout/line_length.yml1
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/boards/components/board_app.vue25
-rw-r--r--app/assets/javascripts/boards/components/board_card.vue39
-rw-r--r--app/assets/javascripts/boards/components/board_content_sidebar.vue138
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_title.vue12
-rw-r--r--app/assets/javascripts/boards/graphql/client/active_board_item.query.graphql7
-rw-r--r--app/assets/javascripts/boards/graphql/client/set_active_board_item.mutation.graphql7
-rw-r--r--app/assets/javascripts/ci/artifacts/components/app.vue (renamed from app/assets/javascripts/artifacts/components/app.vue)0
-rw-r--r--app/assets/javascripts/ci/artifacts/components/artifact_delete_modal.vue (renamed from app/assets/javascripts/artifacts/components/artifact_delete_modal.vue)0
-rw-r--r--app/assets/javascripts/ci/artifacts/components/artifact_row.vue (renamed from app/assets/javascripts/artifacts/components/artifact_row.vue)0
-rw-r--r--app/assets/javascripts/ci/artifacts/components/artifacts_bulk_delete.vue (renamed from app/assets/javascripts/artifacts/components/artifacts_bulk_delete.vue)0
-rw-r--r--app/assets/javascripts/ci/artifacts/components/artifacts_table_row_details.vue (renamed from app/assets/javascripts/artifacts/components/artifacts_table_row_details.vue)0
-rw-r--r--app/assets/javascripts/ci/artifacts/components/feedback_banner.vue (renamed from app/assets/javascripts/artifacts/components/feedback_banner.vue)0
-rw-r--r--app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue (renamed from app/assets/javascripts/artifacts/components/job_artifacts_table.vue)0
-rw-r--r--app/assets/javascripts/ci/artifacts/components/job_checkbox.vue (renamed from app/assets/javascripts/artifacts/components/job_checkbox.vue)0
-rw-r--r--app/assets/javascripts/ci/artifacts/constants.js (renamed from app/assets/javascripts/artifacts/constants.js)0
-rw-r--r--app/assets/javascripts/ci/artifacts/graphql/cache_update.js (renamed from app/assets/javascripts/artifacts/graphql/cache_update.js)0
-rw-r--r--app/assets/javascripts/ci/artifacts/graphql/mutations/bulk_destroy_job_artifacts.mutation.graphql (renamed from app/assets/javascripts/artifacts/graphql/mutations/bulk_destroy_job_artifacts.mutation.graphql)0
-rw-r--r--app/assets/javascripts/ci/artifacts/graphql/mutations/destroy_artifact.mutation.graphql (renamed from app/assets/javascripts/artifacts/graphql/mutations/destroy_artifact.mutation.graphql)0
-rw-r--r--app/assets/javascripts/ci/artifacts/graphql/queries/get_build_artifacts_size.query.graphql (renamed from app/assets/javascripts/artifacts/graphql/queries/get_build_artifacts_size.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/artifacts/graphql/queries/get_job_artifacts.query.graphql (renamed from app/assets/javascripts/artifacts/graphql/queries/get_job_artifacts.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/artifacts/index.js (renamed from app/assets/javascripts/artifacts/index.js)0
-rw-r--r--app/assets/javascripts/ci/artifacts/utils.js (renamed from app/assets/javascripts/artifacts/utils.js)0
-rw-r--r--app/assets/javascripts/graphql_shared/issuable_client.js8
-rw-r--r--app/assets/javascripts/graphql_shared/possible_types.json1
-rw-r--r--app/assets/javascripts/invite_members/components/invite_group_notification.vue20
-rw-r--r--app/assets/javascripts/invite_members/components/invite_groups_modal.vue8
-rw-r--r--app/assets/javascripts/invite_members/constants.js18
-rw-r--r--app/assets/javascripts/issues/constants.js5
-rw-r--r--app/assets/javascripts/issues/show/components/header_actions.vue165
-rw-r--r--app/assets/javascripts/issues/show/components/new_header_actions_popover.vue77
-rw-r--r--app/assets/javascripts/issues/show/constants.js2
-rw-r--r--app/assets/javascripts/issues/show/index.js2
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue13
-rw-r--r--app/assets/javascripts/pages/projects/artifacts/index.js2
-rw-r--r--app/assets/javascripts/right_sidebar.js6
-rw-r--r--app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue27
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue41
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue15
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js17
-rw-r--r--app/assets/stylesheets/page_bundles/issuable.scss10
-rw-r--r--app/assets/stylesheets/pages/detail_page.scss4
-rw-r--r--app/controllers/projects/incidents_controller.rb1
-rw-r--r--app/controllers/projects/issues_controller.rb1
-rw-r--r--app/finders/notes_finder.rb8
-rw-r--r--app/graphql/batch_loaders/award_emoji_votes_batch_loader.rb22
-rw-r--r--app/graphql/resolvers/award_emoji/base_votes_count_resolver.rb21
-rw-r--r--app/graphql/resolvers/down_votes_count_resolver.rb7
-rw-r--r--app/graphql/resolvers/up_votes_count_resolver.rb7
-rw-r--r--app/graphql/resolvers/work_items_resolver.rb3
-rw-r--r--app/graphql/types/work_items/widget_interface.rb5
-rw-r--r--app/graphql/types/work_items/widgets/award_emoji_type.rb41
-rw-r--r--app/helpers/issuables_helper.rb4
-rw-r--r--app/helpers/issues_helper.rb5
-rw-r--r--app/helpers/merge_requests_helper.rb8
-rw-r--r--app/helpers/nav_helper.rb2
-rw-r--r--app/models/note.rb8
-rw-r--r--app/models/work_items/widget_definition.rb3
-rw-r--r--app/models/work_items/widgets/award_emoji.rb9
-rw-r--r--app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml3
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml31
-rw-r--r--app/views/shared/issue_type/_details_header.html.haml2
-rw-r--r--app/views/shared/milestones/_sidebar.html.haml2
-rw-r--r--config/feature_flags/development/hidden_notes.yml8
-rw-r--r--db/migrate/20230323101138_add_award_emoji_work_item_widget.rb57
-rw-r--r--db/post_migrate/20230328100534_truncate_error_tracking_tables.rb20
-rw-r--r--db/schema_migrations/202303231011381
-rw-r--r--db/schema_migrations/202303281005341
-rw-r--r--doc/administration/gitaly/configure_gitaly.md27
-rw-r--r--doc/administration/package_information/supported_os.md2
-rw-r--r--doc/api/graphql/reference/index.md15
-rw-r--r--doc/architecture/blueprints/ci_pipeline_components/index.md1
-rw-r--r--doc/ci/test_cases/index.md6
-rw-r--r--doc/development/documentation/alpha_beta.md22
-rw-r--r--doc/development/pipelines/index.md4
-rw-r--r--doc/install/installation.md13
-rw-r--r--doc/operations/incident_management/manage_incidents.md20
-rw-r--r--doc/update/upgrading_from_source.md15
-rw-r--r--doc/user/group/epics/img/button_close_epic.pngbin13850 -> 0 bytes
-rw-r--r--doc/user/group/epics/manage_epics.md11
-rw-r--r--doc/user/profile/notifications.md2
-rw-r--r--doc/user/project/issues/create_issues.md2
-rw-r--r--doc/user/project/issues/managing_issues.md12
-rw-r--r--doc/user/project/merge_requests/index.md5
-rw-r--r--doc/user/project/repository/code_suggestions.md2
-rw-r--r--doc/user/project/requirements/index.md6
-rw-r--r--doc/user/report_abuse.md4
-rw-r--r--lib/gitlab/ci/pipeline/seed/build.rb1
-rw-r--r--lib/gitlab/ci/yaml_processor.rb4
-rw-r--r--lib/gitlab/database_importers/work_items/base_type_importer.rb24
-rw-r--r--locale/gitlab.pot57
-rw-r--r--package.json2
-rwxr-xr-xscripts/build_assets_image2
-rwxr-xr-xscripts/generate_rspec_pipeline.rb55
-rw-r--r--spec/features/ide/user_opens_merge_request_spec.rb4
-rw-r--r--spec/features/incidents/incident_details_spec.rb2
-rw-r--r--spec/features/issues/discussion_lock_spec.rb1
-rw-r--r--spec/features/issues/gfm_autocomplete_spec.rb3
-rw-r--r--spec/features/issues/issue_detail_spec.rb5
-rw-r--r--spec/features/issues/issue_sidebar_spec.rb4
-rw-r--r--spec/features/issues/user_edits_issue_spec.rb3
-rw-r--r--spec/features/issues/user_toggles_subscription_spec.rb4
-rw-r--r--spec/features/merge_request/user_manages_subscription_spec.rb4
-rw-r--r--spec/features/merge_request/user_opens_checkout_branch_modal_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_check_out_branch_modal_spec.rb3
-rw-r--r--spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb2
-rw-r--r--spec/features/projects/issuable_templates_spec.rb2
-rw-r--r--spec/features/reportable_note/issue_spec.rb4
-rw-r--r--spec/finders/notes_finder_spec.rb45
-rw-r--r--spec/frontend/boards/components/board_app_spec.js38
-rw-r--r--spec/frontend/boards/components/board_card_spec.js39
-rw-r--r--spec/frontend/boards/components/board_content_sidebar_spec.js87
-rw-r--r--spec/frontend/boards/components/board_content_spec.js6
-rw-r--r--spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js10
-rw-r--r--spec/frontend/boards/mock_data.js36
-rw-r--r--spec/frontend/ci/artifacts/components/app_spec.js (renamed from spec/frontend/artifacts/components/app_spec.js)8
-rw-r--r--spec/frontend/ci/artifacts/components/artifact_row_spec.js (renamed from spec/frontend/artifacts/components/artifact_row_spec.js)6
-rw-r--r--spec/frontend/ci/artifacts/components/artifacts_bulk_delete_spec.js (renamed from spec/frontend/artifacts/components/artifacts_bulk_delete_spec.js)6
-rw-r--r--spec/frontend/ci/artifacts/components/artifacts_table_row_details_spec.js (renamed from spec/frontend/artifacts/components/artifacts_table_row_details_spec.js)12
-rw-r--r--spec/frontend/ci/artifacts/components/feedback_banner_spec.js (renamed from spec/frontend/artifacts/components/feedback_banner_spec.js)4
-rw-r--r--spec/frontend/ci/artifacts/components/job_artifacts_table_spec.js (renamed from spec/frontend/artifacts/components/job_artifacts_table_spec.js)18
-rw-r--r--spec/frontend/ci/artifacts/components/job_checkbox_spec.js (renamed from spec/frontend/artifacts/components/job_checkbox_spec.js)4
-rw-r--r--spec/frontend/ci/artifacts/graphql/cache_update_spec.js (renamed from spec/frontend/artifacts/graphql/cache_update_spec.js)4
-rw-r--r--spec/frontend/fixtures/job_artifacts.rb2
-rw-r--r--spec/frontend/ide/components/commit_sidebar/form_spec.js19
-rw-r--r--spec/frontend/invite_members/components/invite_group_notification_spec.js14
-rw-r--r--spec/frontend/invite_members/components/invite_groups_modal_spec.js26
-rw-r--r--spec/frontend/issues/show/components/header_actions_spec.js317
-rw-r--r--spec/frontend/issues/show/components/new_header_actions_popover_spec.js67
-rw-r--r--spec/frontend/notes/components/note_form_spec.js6
-rw-r--r--spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap17
-rw-r--r--spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js21
-rw-r--r--spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js56
-rw-r--r--spec/graphql/types/work_items/widget_interface_spec.rb13
-rw-r--r--spec/graphql/types/work_items/widgets/award_emoji_type_spec.rb12
-rw-r--r--spec/helpers/issues_helper_spec.rb10
-rw-r--r--spec/migrations/20230228142350_add_notifications_work_item_widget_spec.rb21
-rw-r--r--spec/migrations/20230317162059_add_current_user_todos_work_item_widget_spec.rb21
-rw-r--r--spec/migrations/20230323101138_add_award_emoji_work_item_widget_spec.rb8
-rw-r--r--spec/migrations/20230328100534_truncate_error_tracking_tables_spec.rb56
-rw-r--r--spec/models/note_spec.rb26
-rw-r--r--spec/models/work_items/widget_definition_spec.rb3
-rw-r--r--spec/models/work_items/widgets/award_emoji_spec.rb30
-rw-r--r--spec/requests/api/graphql/project/work_items_spec.rb45
-rw-r--r--spec/requests/api/graphql/work_item_spec.rb42
-rw-r--r--spec/scripts/generate_rspec_pipeline_spec.rb69
-rw-r--r--spec/support/shared_examples/migrations/add_work_item_widget_shared_examples.rb33
-rw-r--r--spec/tooling/lib/tooling/find_changes_spec.rb136
-rw-r--r--spec/tooling/lib/tooling/find_tests_spec.rb52
-rw-r--r--spec/tooling/lib/tooling/helpers/file_handler_spec.rb35
-rw-r--r--spec/tooling/lib/tooling/mappings/graphql_base_type_mappings_spec.rb48
-rw-r--r--spec/tooling/lib/tooling/mappings/js_to_system_specs_mappings_spec.rb48
-rw-r--r--spec/tooling/lib/tooling/mappings/partial_to_views_mappings_spec.rb59
-rw-r--r--spec/tooling/lib/tooling/mappings/view_to_js_mappings_spec.rb52
-rw-r--r--spec/tooling/lib/tooling/mappings/view_to_system_specs_mappings_spec.rb45
-rw-r--r--spec/tooling/lib/tooling/predictive_tests_spec.rb109
-rw-r--r--spec/tooling/quality/test_level_spec.rb11
-rwxr-xr-xtooling/bin/find_changes14
-rwxr-xr-xtooling/bin/find_tests9
-rwxr-xr-xtooling/bin/graphql_base_type_mappings9
-rwxr-xr-xtooling/bin/js_to_system_specs_mappings9
-rwxr-xr-xtooling/bin/partial_to_views_mappings9
-rwxr-xr-xtooling/bin/predictive_tests6
-rwxr-xr-xtooling/bin/view_to_js_mappings9
-rwxr-xr-xtooling/bin/view_to_system_specs_mappings9
-rwxr-xr-xtooling/lib/tooling/find_changes.rb45
-rw-r--r--tooling/lib/tooling/find_tests.rb10
-rw-r--r--tooling/lib/tooling/helpers/file_handler.rb12
-rw-r--r--tooling/lib/tooling/mappings/graphql_base_type_mappings.rb10
-rw-r--r--tooling/lib/tooling/mappings/js_to_system_specs_mappings.rb16
-rw-r--r--tooling/lib/tooling/mappings/partial_to_views_mappings.rb12
-rw-r--r--tooling/lib/tooling/mappings/view_to_js_mappings.rb14
-rw-r--r--tooling/lib/tooling/mappings/view_to_system_specs_mappings.rb12
-rw-r--r--tooling/lib/tooling/predictive_tests.rb53
-rw-r--r--tooling/quality/test_level.rb6
-rw-r--r--yarn.lock8
189 files changed, 2993 insertions, 1184 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 6624d31a51d..e8f3ac2813f 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -154,6 +154,7 @@ variables:
KNAPSACK_RSPEC_SUITE_REPORT_PATH: knapsack/report-master.json
RSPEC_CHANGED_FILES_PATH: rspec/changed_files.txt
RSPEC_FOSS_IMPACT_PIPELINE_TEMPLATE_YML: .gitlab/ci/rails/rspec-foss-impact.gitlab-ci.yml.erb
+ RSPEC_PREDICTIVE_PIPELINE_TEMPLATE_YML: .gitlab/ci/rails/rspec-predictive.gitlab-ci.yml.erb
RSPEC_LAST_RUN_RESULTS_FILE: rspec/rspec_last_run_results.txt
RSPEC_MATCHING_JS_FILES_PATH: rspec/js_matching_files.txt
RSPEC_VIEWS_INCLUDING_PARTIALS_PATH: rspec/views_including_partials.txt
diff --git a/.gitlab/ci/build-images.gitlab-ci.yml b/.gitlab/ci/build-images.gitlab-ci.yml
index 4ee15ccb311..c1123b29d67 100644
--- a/.gitlab/ci/build-images.gitlab-ci.yml
+++ b/.gitlab/ci/build-images.gitlab-ci.yml
@@ -71,3 +71,10 @@ build-assets-image:
# The `cached-assets-hash.txt` file is used in `review-build-cng-env` (`.gitlab/ci/review-apps/main.gitlab-ci.yml`)
# to pass the assets image tag to the CNG downstream pipeline.
- cached-assets-hash.txt
+
+build-assets-image as-if-foss:
+ extends:
+ - build-assets-image
+ - .as-if-foss
+ - .build-images:rules:build-assets-image-as-if-foss
+ needs: ["compile-production-assets as-if-foss"]
diff --git a/.gitlab/ci/frontend.gitlab-ci.yml b/.gitlab/ci/frontend.gitlab-ci.yml
index d296604bc68..ad7f4552ae3 100644
--- a/.gitlab/ci/frontend.gitlab-ci.yml
+++ b/.gitlab/ci/frontend.gitlab-ci.yml
@@ -49,6 +49,12 @@ compile-production-assets:
after_script:
- rm -f /etc/apt/sources.list.d/google*.list # We don't need to update Chrome here
+compile-production-assets as-if-foss:
+ extends:
+ - compile-production-assets
+ - .as-if-foss
+ - .frontend:rules:compile-production-assets-as-if-foss
+
compile-test-assets:
extends:
- .compile-assets-base
diff --git a/.gitlab/ci/package-and-test/main.gitlab-ci.yml b/.gitlab/ci/package-and-test/main.gitlab-ci.yml
index 7abcca3a891..97462b9a4dd 100644
--- a/.gitlab/ci/package-and-test/main.gitlab-ci.yml
+++ b/.gitlab/ci/package-and-test/main.gitlab-ci.yml
@@ -4,6 +4,9 @@
default:
interruptible: true
+workflow:
+ name: $PIPELINE_NAME
+
include:
- local: .gitlab/ci/package-and-test/rules.gitlab-ci.yml
- local: .gitlab/ci/package-and-test/variables.gitlab-ci.yml
@@ -54,13 +57,6 @@ stages:
stage: test
tags:
- e2e
- needs:
- - job: trigger-omnibus
- optional: true
- - job: download-knapsack-report
- artifacts: true
- optional: true
- - job: check-release-set
variables:
QA_GENERATE_ALLURE_REPORT: "true"
QA_CAN_TEST_PRAEFECT: "false"
@@ -128,13 +124,21 @@ trigger-omnibus-env:
echo "OMNIBUS_GITLAB_CACHE_EDITION=${OMNIBUS_GITLAB_CACHE_EDITION:-GITLAB}" >> $BUILD_ENV
echo "OMNIBUS_GITLAB_BUILD_ON_ALL_OS=${OMNIBUS_GITLAB_BUILD_ON_ALL_OS:-false}" >> $BUILD_ENV
echo "GITLAB_ASSETS_TAG=$(assets_image_tag)" >> $BUILD_ENV
- echo "EE=$([[ $FOSS_ONLY == 'true' ]] && echo 'false' || echo 'true')" >> $BUILD_ENV
+ echo "EE=$([[ $FOSS_ONLY == '1' ]] && echo 'false' || echo 'true')" >> $BUILD_ENV
echo "Built environment file for omnibus build:"
cat $BUILD_ENV
artifacts:
reports:
dotenv: $BUILD_ENV
+trigger-omnibus-env-ce:
+ extends:
+ - trigger-omnibus-env
+ - .rules:omnibus-build-ce
+ needs:
+ - pipeline: $PARENT_PIPELINE_ID
+ job: build-assets-image as-if-foss
+
trigger-omnibus:
extends: .rules:omnibus-build
stage: .pre
@@ -166,6 +170,16 @@ trigger-omnibus:
project: gitlab-org/build/omnibus-gitlab-mirror
strategy: depend
+trigger-omnibus-ce:
+ extends:
+ - trigger-omnibus
+ - .rules:omnibus-build-ce
+ variables:
+ # Override gitlab repository so that omnibus doesn't use foss repository for CE build
+ GITLAB_ALTERNATIVE_REPO: $CI_PROJECT_URL
+ needs:
+ - trigger-omnibus-env-ce
+
download-knapsack-report:
extends:
- .gitlab-qa-image
diff --git a/.gitlab/ci/package-and-test/rules.gitlab-ci.yml b/.gitlab/ci/package-and-test/rules.gitlab-ci.yml
index 640f5f53bfa..8f63dbc38c3 100644
--- a/.gitlab/ci/package-and-test/rules.gitlab-ci.yml
+++ b/.gitlab/ci/package-and-test/rules.gitlab-ci.yml
@@ -60,7 +60,13 @@
rules:
- if: $SKIP_OMNIBUS_TRIGGER == "true"
when: never
- - when: always
+ - if: $FOSS_ONLY != "1"
+
+.rules:omnibus-build-ce:
+ rules:
+ - if: $SKIP_OMNIBUS_TRIGGER == "true"
+ when: never
+ - if: $FOSS_ONLY == "1"
.rules:update-cache:
rules:
@@ -117,7 +123,7 @@
.rules:test:ee-only:
rules:
- - if: $FOSS_ONLY == "true"
+ - if: $FOSS_ONLY == "1"
when: never
.rules:test:update:
diff --git a/.gitlab/ci/qa.gitlab-ci.yml b/.gitlab/ci/qa.gitlab-ci.yml
index d935fecba01..2956eef6a9e 100644
--- a/.gitlab/ci/qa.gitlab-ci.yml
+++ b/.gitlab/ci/qa.gitlab-ci.yml
@@ -55,7 +55,7 @@ qa:update-qa-cache:
e2e:package-and-test-ee:
extends:
- .production # this makes sure GITLAB_ALLOW_SEPARATE_CI_DATABASE is passed to the child pipeline
- - .qa:rules:package-and-test
+ - .qa:rules:package-and-test-ee
stage: qa
needs:
- build-assets-image
@@ -70,6 +70,7 @@ e2e:package-and-test-ee:
RUN_WITH_BUNDLE: "true" # instructs pipeline to install and run gitlab-qa gem via bundler
QA_PATH: qa # sets the optional path for bundler to run from
QA_RUN_TYPE: e2e-package-and-test
+ PIPELINE_NAME: E2E Omnibus GitLab EE
inherit:
variables:
- CHROME_VERSION
@@ -89,6 +90,22 @@ e2e:package-and-test-ee:
- artifact: package-and-test-pipeline.yml
job: e2e-test-pipeline-generate
+e2e:package-and-test-ce:
+ extends:
+ - e2e:package-and-test-ee
+ - .qa:rules:package-and-test-ce
+ needs:
+ - build-assets-image as-if-foss
+ - build-qa-image as-if-foss
+ - e2e-test-pipeline-generate
+ variables:
+ FOSS_ONLY: "1"
+ QA_RUN_TYPE: e2e-package-and-test-ce
+ ALLURE_JOB_NAME: e2e-package-and-test-ce
+ RELEASE: ${REGISTRY_HOST}/${REGISTRY_GROUP}/build/omnibus-gitlab-mirror/gitlab-ce:${CI_COMMIT_SHA}
+ GITLAB_QA_IMAGE: ${CI_REGISTRY_IMAGE}/gitlab-ce-qa:${CI_COMMIT_SHA}
+ PIPELINE_NAME: E2E Omnibus GitLab CE
+
e2e:test-on-gdk:
extends:
- .qa:rules:e2e:test-on-gdk
diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml
index 89445fb9315..d5480999d87 100644
--- a/.gitlab/ci/rails.gitlab-ci.yml
+++ b/.gitlab/ci/rails.gitlab-ci.yml
@@ -74,12 +74,6 @@ rspec migration pg13:
- .rails:rules:ee-and-foss-migration
- .rspec-migration-parallel
-rspec migration pg13 predictive:
- extends:
- - rspec migration pg13
- - .predictive-rspec-tests
- - .rails:rules:ee-and-foss-migration:predictive
-
rspec background_migration pg13:
extends:
- .rspec-base-pg13
@@ -87,12 +81,6 @@ rspec background_migration pg13:
- .rails:rules:ee-and-foss-background-migration
- .rspec-background-migration-parallel
-rspec background_migration pg13 predictive:
- extends:
- - rspec background_migration pg13
- - .predictive-rspec-tests
- - .rails:rules:ee-and-foss-background-migration:predictive
-
rspec migration pg13 single-db:
extends:
- rspec migration pg13
@@ -135,12 +123,6 @@ rspec unit pg13:
- .rails:rules:ee-and-foss-unit
- .rspec-unit-parallel
-rspec unit pg13 predictive:
- extends:
- - rspec unit pg13
- - .predictive-rspec-tests
- - .rails:rules:ee-and-foss-unit:predictive
-
rspec unit pg13 single-db:
extends:
- rspec unit pg13
@@ -165,12 +147,6 @@ rspec integration pg13:
- .rails:rules:ee-and-foss-integration
- .rspec-integration-parallel
-rspec integration pg13 predictive:
- extends:
- - rspec integration pg13
- - .predictive-rspec-tests
- - .rails:rules:ee-and-foss-integration:predictive
-
rspec integration pg13 single-db:
extends:
- rspec integration pg13
@@ -197,12 +173,6 @@ rspec system pg13:
variables:
DEBUG_GITLAB_TRANSACTION_STACK: "true"
-rspec system pg13 predictive:
- extends:
- - rspec system pg13
- - .predictive-rspec-tests
- - .rails:rules:ee-and-foss-system:predictive
-
rspec system pg13 single-db:
extends:
- rspec system pg13
@@ -305,24 +275,12 @@ rspec:coverage:
- rspec unit pg13
- rspec integration pg13
- rspec system pg13
- # FOSS/EE predictive jobs
- - rspec migration pg13 predictive
- - rspec background_migration pg13 predictive
- - rspec unit pg13 predictive
- - rspec integration pg13 predictive
- - rspec system pg13 predictive
# EE jobs
- rspec-ee migration pg13
- rspec-ee background_migration pg13
- rspec-ee unit pg13
- rspec-ee integration pg13
- rspec-ee system pg13
- # EE predictive jobs
- - rspec-ee migration pg13 predictive
- - rspec-ee background_migration pg13 predictive
- - rspec-ee unit pg13 predictive
- - rspec-ee integration pg13 predictive
- - rspec-ee system pg13 predictive
# Memory jobs
- memory-on-boot
script:
@@ -402,6 +360,56 @@ rspec:flaky-tests-report:
##################################################
# EE: default refs (MRs, default branch, schedules) jobs #
+rspec-predictive:pipeline-generate:
+ extends:
+ - .rails:rules:rspec-predictive
+ stage: prepare
+ needs: ["detect-tests", "retrieve-tests-metadata"]
+ script:
+ - scripts/generate_rspec_pipeline.rb -t "${RSPEC_PREDICTIVE_PIPELINE_TEMPLATE_YML}" -k "${KNAPSACK_RSPEC_SUITE_REPORT_PATH}" -f "${RSPEC_MATCHING_TESTS_FOSS_PATH}" -o "${RSPEC_PREDICTIVE_PIPELINE_TEMPLATE_YML}.yml"
+ - scripts/generate_rspec_pipeline.rb -t "${RSPEC_PREDICTIVE_PIPELINE_TEMPLATE_YML}" -k "${KNAPSACK_RSPEC_SUITE_REPORT_PATH}" -f "${RSPEC_MATCHING_TESTS_EE_PATH}" -o "${RSPEC_PREDICTIVE_PIPELINE_TEMPLATE_YML}-ee.yml" -p "ee/"
+ - echo "Content of ${RSPEC_PREDICTIVE_PIPELINE_TEMPLATE_YML}.yml:"
+ - cat "${RSPEC_PREDICTIVE_PIPELINE_TEMPLATE_YML}.yml"
+ - echo "\n================================================\n"
+ - echo "Content of ${RSPEC_PREDICTIVE_PIPELINE_TEMPLATE_YML}-ee.yml:"
+ - cat "${RSPEC_PREDICTIVE_PIPELINE_TEMPLATE_YML}-ee.yml"
+ artifacts:
+ expire_in: 1 day
+ paths:
+ - "${RSPEC_PREDICTIVE_PIPELINE_TEMPLATE_YML}.yml"
+ - "${RSPEC_PREDICTIVE_PIPELINE_TEMPLATE_YML}-ee.yml"
+
+rspec:predictive:trigger:
+ extends:
+ - .rails:rules:rspec-predictive
+ stage: test
+ needs:
+ - job: "setup-test-env"
+ artifacts: false
+ - job: "retrieve-tests-metadata"
+ artifacts: false
+ - job: "compile-test-assets"
+ artifacts: false
+ - job: "rspec-predictive:pipeline-generate"
+ artifacts: true
+ variables:
+ PARENT_PIPELINE_ID: $CI_PIPELINE_ID
+ trigger:
+ strategy: depend
+ forward:
+ yaml_variables: true
+ pipeline_variables: true
+ include:
+ - artifact: "${RSPEC_PREDICTIVE_PIPELINE_TEMPLATE_YML}.yml"
+ job: rspec-predictive:pipeline-generate
+
+rspec-ee:predictive:trigger:
+ extends: rspec:predictive:trigger
+ trigger:
+ include:
+ - artifact: "${RSPEC_PREDICTIVE_PIPELINE_TEMPLATE_YML}-ee.yml"
+ job: rspec-predictive:pipeline-generate
+
rspec migration pg13-as-if-foss:
extends:
- .rspec-base-pg13-as-if-foss
@@ -409,12 +417,6 @@ rspec migration pg13-as-if-foss:
- .rails:rules:as-if-foss-migration
- .rspec-migration-parallel
-rspec migration pg13-as-if-foss predictive:
- extends:
- - rspec migration pg13-as-if-foss
- - .predictive-rspec-tests
- - .rails:rules:as-if-foss-migration:predictive
-
rspec background_migration pg13-as-if-foss:
extends:
- .rspec-base-pg13-as-if-foss
@@ -422,12 +424,6 @@ rspec background_migration pg13-as-if-foss:
- .rails:rules:as-if-foss-background-migration
- .rspec-background-migration-parallel
-rspec background_migration pg13-as-if-foss predictive:
- extends:
- - rspec background_migration pg13-as-if-foss
- - .predictive-rspec-tests
- - .rails:rules:as-if-foss-background-migration:predictive
-
rspec migration pg13-as-if-foss single-db:
extends:
- rspec migration pg13-as-if-foss
@@ -458,12 +454,6 @@ rspec unit pg13-as-if-foss:
- .rails:rules:as-if-foss-unit
- .rspec-unit-parallel
-rspec unit pg13-as-if-foss predictive:
- extends:
- - rspec unit pg13-as-if-foss
- - .predictive-rspec-tests
- - .rails:rules:as-if-foss-unit:predictive
-
rspec unit pg13-as-if-foss single-db:
extends:
- rspec unit pg13-as-if-foss
@@ -482,12 +472,6 @@ rspec integration pg13-as-if-foss:
- .rails:rules:as-if-foss-integration
- .rspec-integration-parallel
-rspec integration pg13-as-if-foss predictive:
- extends:
- - rspec integration pg13-as-if-foss
- - .predictive-rspec-tests
- - .rails:rules:as-if-foss-integration:predictive
-
rspec integration pg13-as-if-foss single-db:
extends:
- rspec integration pg13-as-if-foss
@@ -506,12 +490,6 @@ rspec system pg13-as-if-foss:
- .rails:rules:as-if-foss-system
- .rspec-system-parallel
-rspec system pg13-as-if-foss predictive:
- extends:
- - rspec system pg13-as-if-foss
- - .predictive-rspec-tests
- - .rails:rules:as-if-foss-system:predictive
-
rspec system pg13-as-if-foss single-db:
extends:
- rspec system pg13-as-if-foss
@@ -531,12 +509,6 @@ rspec-ee migration pg13:
- .rails:rules:ee-only-migration
- .rspec-ee-migration-parallel
-rspec-ee migration pg13 predictive:
- extends:
- - rspec-ee migration pg13
- - .predictive-rspec-tests
- - .rails:rules:ee-only-migration:predictive
-
rspec-ee background_migration pg13:
extends:
- .rspec-ee-base-pg13
@@ -544,12 +516,6 @@ rspec-ee background_migration pg13:
- .rails:rules:ee-only-background-migration
- .rspec-ee-background-migration-parallel
-rspec-ee background_migration pg13 predictive:
- extends:
- - rspec-ee background_migration pg13
- - .predictive-rspec-tests
- - .rails:rules:ee-only-background-migration:predictive
-
rspec-ee migration pg13 single-db:
extends:
- rspec-ee migration pg13
@@ -597,12 +563,6 @@ rspec-ee unit pg13 es8:
- .rspec-ee-base-pg13-es8
- .rspec-ee-unit-parallel
-rspec-ee unit pg13 predictive:
- extends:
- - rspec-ee unit pg13
- - .predictive-rspec-tests
- - .rails:rules:ee-only-unit:predictive
-
rspec-ee unit pg13 single-db:
extends:
- rspec-ee unit pg13
@@ -626,12 +586,6 @@ rspec-ee integration pg13 es8:
- .rspec-ee-base-pg13-es8
- .rspec-ee-integration-parallel
-rspec-ee integration pg13 predictive:
- extends:
- - rspec-ee integration pg13
- - .predictive-rspec-tests
- - .rails:rules:ee-only-integration:predictive
-
rspec-ee integration pg13 single-db:
extends:
- rspec-ee integration pg13
@@ -655,12 +609,6 @@ rspec-ee system pg13 es8:
- .rspec-ee-base-pg13-es8
- .rspec-ee-system-parallel
-rspec-ee system pg13 predictive:
- extends:
- - rspec-ee system pg13
- - .predictive-rspec-tests
- - .rails:rules:ee-only-system:predictive
-
rspec-ee system pg13 single-db:
extends:
- rspec-ee system pg13
diff --git a/.gitlab/ci/rails/rspec-foss-impact.gitlab-ci.yml.erb b/.gitlab/ci/rails/rspec-foss-impact.gitlab-ci.yml.erb
index 38d964af62a..e7a1ee6022f 100644
--- a/.gitlab/ci/rails/rspec-foss-impact.gitlab-ci.yml.erb
+++ b/.gitlab/ci/rails/rspec-foss-impact.gitlab-ci.yml.erb
@@ -1,4 +1,4 @@
-# RSpec FOSS impact pipeline loaded dynamically by script: scripts/generate-rspec-foss-impact-pipeline
+# RSpec FOSS impact pipeline loaded dynamically by script: scripts/generate_rspec_pipeline.rb
include:
- local: .gitlab/ci/rails/shared.gitlab-ci.yml
diff --git a/.gitlab/ci/rails/rspec-predictive.gitlab-ci.yml.erb b/.gitlab/ci/rails/rspec-predictive.gitlab-ci.yml.erb
new file mode 100644
index 00000000000..fcd8754c76a
--- /dev/null
+++ b/.gitlab/ci/rails/rspec-predictive.gitlab-ci.yml.erb
@@ -0,0 +1,153 @@
+# RSpec preditive pipeline loaded dynamically by script: scripts/generate_rspec_pipeline.rb
+
+include:
+ - local: .gitlab/ci/rails/shared.gitlab-ci.yml
+
+default:
+ image: $DEFAULT_CI_IMAGE
+ tags:
+ - gitlab-org
+ # Default job timeout set to 90m https://gitlab.com/gitlab-com/gl-infra/infrastructure/-/issues/10520
+ timeout: 90m
+ interruptible: true
+
+stages:
+ - test
+
+dont-interrupt-me:
+ extends: .rules:dont-interrupt
+ stage: .pre
+ interruptible: false
+ script:
+ - echo "This jobs makes sure this pipeline won't be interrupted! See https://docs.gitlab.com/ee/ci/yaml/#interruptible."
+
+.base-predictive:
+ needs:
+ - pipeline: $PARENT_PIPELINE_ID
+ job: detect-tests
+ - pipeline: $PARENT_PIPELINE_ID
+ job: setup-test-env
+ - pipeline: $PARENT_PIPELINE_ID
+ job: retrieve-tests-metadata
+ - pipeline: $PARENT_PIPELINE_ID
+ job: compile-test-assets
+ rules:
+ - when: always
+ variables:
+ RSPEC_TESTS_MAPPING_ENABLED: "true"
+
+<% if test_suite_prefix.nil? %>
+.base-rspec-predictive:
+ extends:
+ - .rspec-base-pg12
+ - .base-predictive
+ variables:
+ # We're using the FOSS one here because we want to exclude EE-only ones
+ # For EE-only ones, we have EE-only jobs.
+ RSPEC_TESTS_FILTER_FILE: "${RSPEC_MATCHING_TESTS_FOSS_PATH}"
+
+<% if rspec_files_per_test_level.dig(:migration, :files).size > 0 %>
+rspec migration predictive:
+ extends:
+ - .base-rspec-predictive
+ - .rspec-base-migration
+<% if rspec_files_per_test_level.dig(:migration, :parallelization) > 1 %>
+ parallel: <%= rspec_files_per_test_level.dig(:migration, :parallelization) %>
+<% end %>
+<% end %>
+
+<% if rspec_files_per_test_level.dig(:background_migration, :files).size > 0 %>
+rspec background_migration predictive:
+ extends:
+ - .base-rspec-predictive
+ - .rspec-base-migration
+<% if rspec_files_per_test_level.dig(:background_migration, :parallelization) > 1 %>
+ parallel: <%= rspec_files_per_test_level.dig(:background_migration, :parallelization) %>
+<% end %>
+<% end %>
+
+<% if rspec_files_per_test_level.dig(:unit, :files).size > 0 %>
+rspec unit predictive:
+ extends:
+ - .base-rspec-predictive
+<% if rspec_files_per_test_level.dig(:unit, :parallelization) > 1 %>
+ parallel: <%= rspec_files_per_test_level.dig(:unit, :parallelization) %>
+<% end %>
+<% end %>
+
+<% if rspec_files_per_test_level.dig(:integration, :files).size > 0 %>
+rspec integration predictive:
+ extends:
+ - .base-rspec-predictive
+<% if rspec_files_per_test_level.dig(:integration, :parallelization) > 1 %>
+ parallel: <%= rspec_files_per_test_level.dig(:integration, :parallelization) %>
+<% end %>
+<% end %>
+
+<% if rspec_files_per_test_level.dig(:system, :files).size > 0 %>
+rspec system predictive:
+ extends:
+ - .base-rspec-predictive
+<% if rspec_files_per_test_level.dig(:system, :parallelization) > 1 %>
+ parallel: <%= rspec_files_per_test_level.dig(:system, :parallelization) %>
+<% end %>
+<% end %>
+
+<% end %>
+
+<% if test_suite_prefix == 'ee/' %>
+.base-rspec-ee-predictive:
+ extends:
+ - .rspec-ee-base-pg12
+ - .base-predictive
+ variables:
+ RSPEC_TESTS_FILTER_FILE: "${RSPEC_MATCHING_TESTS_EE_PATH}"
+
+<% if rspec_files_per_test_level.dig(:migration, :files).size > 0 %>
+rspec-ee migration predictive:
+ extends:
+ - .base-rspec-ee-predictive
+ - .rspec-base-migration
+<% if rspec_files_per_test_level.dig(:migration, :parallelization) > 1 %>
+ parallel: <%= rspec_files_per_test_level.dig(:migration, :parallelization) %>
+<% end %>
+<% end %>
+
+<% if rspec_files_per_test_level.dig(:background_migration, :files).size > 0 %>
+rspec-ee background_migration predictive:
+ extends:
+ - .base-rspec-ee-predictive
+ - .rspec-base-migration
+<% if rspec_files_per_test_level.dig(:background_migration, :parallelization) > 1 %>
+ parallel: <%= rspec_files_per_test_level.dig(:background_migration, :parallelization) %>
+<% end %>
+<% end %>
+
+<% if rspec_files_per_test_level.dig(:unit, :files).size > 0 %>
+rspec-ee unit predictive:
+ extends:
+ - .base-rspec-ee-predictive
+<% if rspec_files_per_test_level.dig(:unit, :parallelization) > 1 %>
+ parallel: <%= rspec_files_per_test_level.dig(:unit, :parallelization) %>
+<% end %>
+<% end %>
+
+<% if rspec_files_per_test_level.dig(:integration, :files).size > 0 %>
+rspec-ee integration predictive:
+ extends:
+ - .base-rspec-ee-predictive
+<% if rspec_files_per_test_level.dig(:integration, :parallelization) > 1 %>
+ parallel: <%= rspec_files_per_test_level.dig(:integration, :parallelization) %>
+<% end %>
+<% end %>
+
+<% if rspec_files_per_test_level.dig(:system, :files).size > 0 %>
+rspec-ee system predictive:
+ extends:
+ - .base-rspec-ee-predictive
+<% if rspec_files_per_test_level.dig(:system, :parallelization) > 1 %>
+ parallel: <%= rspec_files_per_test_level.dig(:system, :parallelization) %>
+<% end %>
+<% end %>
+
+<% end %>
diff --git a/.gitlab/ci/rails/shared.gitlab-ci.yml b/.gitlab/ci/rails/shared.gitlab-ci.yml
index 62e8547fa5a..adcfcd2010f 100644
--- a/.gitlab/ci/rails/shared.gitlab-ci.yml
+++ b/.gitlab/ci/rails/shared.gitlab-ci.yml
@@ -28,10 +28,6 @@ include:
- run_timed_command "scripts/gitaly-test-spawn" # Do not use 'bundle exec' here
- echo -e "\e[0Ksection_end:`date +%s`:gitaly-test-spawn\r\e[0K"
-.predictive-rspec-tests:
- variables:
- RSPEC_TESTS_MAPPING_ENABLED: "true"
-
.single-db:
variables:
DECOMPOSED_DB: "false"
@@ -61,7 +57,6 @@ include:
RUBY_GC_MALLOC_LIMIT_MAX: 134217728
RECORD_DEPRECATIONS: "true"
GEO_SECONDARY_PROXY: 0
- RSPEC_TESTS_FILTER_FILE: "${RSPEC_MATCHING_TESTS_PATH}"
SUCCESSFULLY_RETRIED_TEST_EXIT_CODE: 137
needs:
- job: "setup-test-env"
diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml
index c61819d0a4c..ab5c56214be 100644
--- a/.gitlab/ci/rules.gitlab-ci.yml
+++ b/.gitlab/ci/rules.gitlab-ci.yml
@@ -593,7 +593,6 @@
##################
# Conditions set #
##################
-
.strict-ee-only-rules:
rules:
- <<: *if-not-ee
@@ -610,15 +609,6 @@
- <<: *if-merge-request-labels-pipeline-expedite
when: never
-.rails:rules:predictive-default-rules:
- rules:
- - <<: *if-merge-request-approved
- when: never
- - <<: *if-automated-merge-request
- when: never
- - <<: *if-security-merge-request
- when: never
-
.rails:rules:run-search-tests:
rules:
- !reference [".rails:rules:default-branch-schedule-nightly--code-backstage-ee-only", rules]
@@ -639,6 +629,40 @@
- <<: *if-merge-request-not-approved
when: never
+.rails:rules:system-default-rules:
+ rules:
+ - <<: *if-merge-request-labels-run-all-rspec
+ - <<: *if-merge-request
+ changes: *core-backend-patterns
+ - <<: *if-merge-request
+ changes: *workhorse-patterns
+ - <<: *if-automated-merge-request
+ changes: *code-backstage-patterns
+ - <<: *if-security-merge-request
+ changes: *code-backstage-patterns
+ - <<: *if-merge-request-not-approved
+ when: never
+
+.rails:rules:previous-failed-tests-default-rules:
+ rules:
+ - <<: *if-security-merge-request
+ when: never
+ - <<: *if-merge-request-labels-run-all-rspec
+ - <<: *if-merge-request
+ changes: *code-backstage-patterns
+
+###########################
+# Conditions set for JiHu #
+###########################
+.rails:rules:predictive-default-rules:
+ rules:
+ - <<: *if-merge-request-approved
+ when: never
+ - <<: *if-automated-merge-request
+ when: never
+ - <<: *if-security-merge-request
+ when: never
+
.rails:rules:as-if-foss-migration-unit-integration:predictive-default-rules:
rules:
- <<: *if-merge-request
@@ -654,43 +678,115 @@
when: never
- !reference [".rails:rules:as-if-foss-migration-unit-integration:predictive-default-rules", rules]
-.rails:rules:system-default-rules:
+.rails:rules:system:predictive-default-rules:
rules:
- <<: *if-merge-request-labels-run-all-rspec
+ when: never
- <<: *if-merge-request
changes: *core-backend-patterns
+ when: never
- <<: *if-merge-request
changes: *workhorse-patterns
- - <<: *if-automated-merge-request
- changes: *code-backstage-patterns
- - <<: *if-security-merge-request
- changes: *code-backstage-patterns
- - <<: *if-merge-request-not-approved
when: never
+ - <<: *if-merge-request
+ changes: *ci-patterns
+ when: never
+ - <<: *if-merge-request
+ changes: *code-backstage-patterns
-.rails:rules:system:predictive-default-rules:
+.rails:rules:ee-and-foss-migration:predictive:
rules:
- - <<: *if-merge-request-labels-run-all-rspec
+ - <<: *if-fork-merge-request
+ changes: *db-patterns
+ - !reference [".rails:rules:predictive-default-rules", rules]
+ - !reference [".rails:rules:unit-integration:predictive-default-rules", rules]
+ # When DB schema changes, many migrations spec may be affected. However, the test mapping from Crystalball does not map db change to a specific migration spec well.
+ # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68840.
+ - <<: *if-merge-request
+ changes: *db-patterns
when: never
+
+.rails:rules:ee-and-foss-background-migration:predictive:
+ rules:
+ - !reference [".rails:rules:ee-and-foss-migration:predictive", rules]
- <<: *if-merge-request
- changes: *core-backend-patterns
+ changes: *backend-patterns
+
+.rails:rules:ee-and-foss-unit:predictive:
+ rules:
+ - <<: *if-fork-merge-request
+ changes: *backend-patterns
+ - !reference [".rails:rules:predictive-default-rules", rules]
+ - !reference [".rails:rules:unit-integration:predictive-default-rules", rules]
+ - <<: *if-merge-request
+ changes: *backend-patterns
+ - <<: *if-merge-request
+ changes: *backstage-patterns
+
+.rails:rules:ee-and-foss-integration:predictive:
+ rules:
+ - <<: *if-fork-merge-request
+ changes: *backend-patterns
+ - !reference [".rails:rules:predictive-default-rules", rules]
+ - !reference [".rails:rules:unit-integration:predictive-default-rules", rules]
+ - <<: *if-merge-request
+ changes: *backend-patterns
+
+.rails:rules:ee-and-foss-system:predictive:
+ rules:
+ - <<: *if-fork-merge-request
+ changes: *code-backstage-patterns
+ - !reference [".rails:rules:predictive-default-rules", rules]
+ - !reference [".rails:rules:system:predictive-default-rules", rules]
+
+.rails:rules:ee-only-migration:predictive:
+ rules:
+ - <<: *if-not-ee
when: never
+ - !reference [".rails:rules:predictive-default-rules", rules]
+ - !reference [".rails:rules:unit-integration:predictive-default-rules", rules]
+ # When DB schema changes, many migrations spec may be affected. However, the test mapping from Crystalball does not map db change to a specific migration spec well.
+ # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68840.
- <<: *if-merge-request
- changes: *workhorse-patterns
+ changes: *db-patterns
when: never
+
+.rails:rules:ee-only-background-migration:predictive:
+ rules:
+ - !reference [".rails:rules:ee-only-migration:predictive", rules]
- <<: *if-merge-request
- changes: *ci-patterns
+ changes: *backend-patterns
+
+.rails:rules:ee-only-unit:predictive:
+ rules:
+ - <<: *if-not-ee
when: never
+ - <<: *if-fork-merge-request
+ changes: *backend-patterns
+ - !reference [".rails:rules:predictive-default-rules", rules]
+ - !reference [".rails:rules:unit-integration:predictive-default-rules", rules]
- <<: *if-merge-request
- changes: *code-backstage-patterns
+ changes: *backend-patterns
-.rails:rules:previous-failed-tests-default-rules:
+.rails:rules:ee-only-integration:predictive:
rules:
- - <<: *if-security-merge-request
+ - <<: *if-not-ee
when: never
- - <<: *if-merge-request-labels-run-all-rspec
+ - <<: *if-fork-merge-request
+ changes: *backend-patterns
+ - !reference [".rails:rules:predictive-default-rules", rules]
+ - !reference [".rails:rules:unit-integration:predictive-default-rules", rules]
- <<: *if-merge-request
+ changes: *backend-patterns
+
+.rails:rules:ee-only-system:predictive:
+ rules:
+ - <<: *if-not-ee
+ when: never
+ - <<: *if-fork-merge-request
changes: *code-backstage-patterns
+ - !reference [".rails:rules:predictive-default-rules", rules]
+ - !reference [".rails:rules:system:predictive-default-rules", rules]
################
# Shared rules #
@@ -773,6 +869,16 @@
- <<: *if-default-refs
changes: *code-qa-patterns
+.build-images:rules:build-assets-image-as-if-foss:
+ rules:
+ - <<: *if-not-canonical-namespace
+ when: never
+ - <<: *if-not-ee
+ when: never
+ - <<: *if-merge-request-labels-pipeline-expedite
+ when: never
+ - !reference [".build-images:rules:build-assets-image", "rules"]
+
#################
# Caching rules #
#################
@@ -941,6 +1047,16 @@
- <<: *if-default-refs
changes: *workhorse-patterns
+.frontend:rules:compile-production-assets-as-if-foss:
+ rules:
+ - <<: *if-not-canonical-namespace
+ when: never
+ - <<: *if-not-ee
+ when: never
+ - <<: *if-merge-request-labels-pipeline-expedite
+ when: never
+ - !reference [.frontend:rules:compile-production-assets, rules]
+
.frontend:rules:compile-test-assets:
rules:
- <<: *if-merge-request-labels-run-all-rspec
@@ -1233,7 +1349,7 @@
when: manual
allow_failure: true
-.qa:rules:package-and-test:
+.qa:rules:package-and-test-ee:
rules:
- !reference [".qa:rules:package-and-test-mrs", rules]
- <<: *if-dot-com-gitlab-org-schedule
@@ -1246,11 +1362,23 @@
QA_SAVE_TEST_METRICS: "true"
QA_EXPORT_TEST_METRICS: "false" # on main runs, metrics are exported to separate bucket via rake task for better consistency
+.qa:rules:package-and-test-ce:
+ rules:
+ - <<: *if-not-canonical-namespace
+ when: never
+ - <<: *if-not-ee
+ when: never
+ - <<: *if-merge-request-labels-pipeline-expedite
+ when: never
+ - <<: *if-dot-com-gitlab-org-and-security-merge-request
+ when: manual
+ allow_failure: true
+
.qa:rules:e2e:test-on-gdk:
rules:
- if: '$QA_RUN_TESTS_ON_GDK !~ /true|yes|1/i'
when: never
- - !reference [".qa:rules:package-and-test", rules]
+ - !reference [".qa:rules:package-and-test-ee", rules]
###############
# Rails rules #
@@ -1335,17 +1463,18 @@
- <<: *if-default-refs
changes: *db-patterns
-.rails:rules:ee-and-foss-migration:predictive:
+.rails:rules:rspec-predictive:
rules:
- - <<: *if-fork-merge-request
- changes: *db-patterns
- - !reference [".rails:rules:predictive-default-rules", rules]
- - !reference [".rails:rules:unit-integration:predictive-default-rules", rules]
- # When DB schema changes, many migrations spec may be affected. However, the test mapping from Crystalball does not map db change to a specific migration spec well.
- # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68840.
- - <<: *if-merge-request
- changes: *db-patterns
+ - <<: *if-merge-request-approved
+ when: never
+ - <<: *if-automated-merge-request
+ when: never
+ - <<: *if-security-merge-request
when: never
+ - <<: *if-merge-request-labels-run-all-rspec
+ when: never
+ - <<: *if-merge-request
+ changes: *code-backstage-patterns
.rails:rules:ee-and-foss-background-migration:
rules:
@@ -1353,12 +1482,6 @@
- <<: *if-default-refs
changes: *backend-patterns
-.rails:rules:ee-and-foss-background-migration:predictive:
- rules:
- - !reference [".rails:rules:ee-and-foss-migration:predictive", rules]
- - <<: *if-merge-request
- changes: *backend-patterns
-
.rails:rules:ee-and-foss-mr-with-migration:
rules:
- <<: *if-merge-request
@@ -1383,17 +1506,6 @@
- <<: *if-default-refs
changes: *backstage-patterns
-.rails:rules:ee-and-foss-unit:predictive:
- rules:
- - <<: *if-fork-merge-request
- changes: *backend-patterns
- - !reference [".rails:rules:predictive-default-rules", rules]
- - !reference [".rails:rules:unit-integration:predictive-default-rules", rules]
- - <<: *if-merge-request
- changes: *backend-patterns
- - <<: *if-merge-request
- changes: *backstage-patterns
-
.rails:rules:ee-and-foss-integration:
rules:
- <<: *if-fork-merge-request
@@ -1402,15 +1514,6 @@
- <<: *if-default-refs
changes: *backend-patterns
-.rails:rules:ee-and-foss-integration:predictive:
- rules:
- - <<: *if-fork-merge-request
- changes: *backend-patterns
- - !reference [".rails:rules:predictive-default-rules", rules]
- - !reference [".rails:rules:unit-integration:predictive-default-rules", rules]
- - <<: *if-merge-request
- changes: *backend-patterns
-
.rails:rules:ee-and-foss-system:
rules:
- <<: *if-fork-merge-request
@@ -1419,13 +1522,6 @@
- <<: *if-default-refs
changes: *code-backstage-patterns
-.rails:rules:ee-and-foss-system:predictive:
- rules:
- - <<: *if-fork-merge-request
- changes: *code-backstage-patterns
- - !reference [".rails:rules:predictive-default-rules", rules]
- - !reference [".rails:rules:system:predictive-default-rules", rules]
-
.rails:rules:ee-and-foss-fast_spec_helper:
rules:
- <<: *if-merge-request-labels-run-all-rspec
@@ -1460,30 +1556,12 @@
- <<: *if-default-refs
changes: *db-patterns
-.rails:rules:ee-only-migration:predictive:
- rules:
- - <<: *if-not-ee
- when: never
- - !reference [".rails:rules:predictive-default-rules", rules]
- - !reference [".rails:rules:unit-integration:predictive-default-rules", rules]
- # When DB schema changes, many migrations spec may be affected. However, the test mapping from Crystalball does not map db change to a specific migration spec well.
- # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68840.
- - <<: *if-merge-request
- changes: *db-patterns
- when: never
-
.rails:rules:ee-only-background-migration:
rules:
- !reference [".rails:rules:ee-only-migration", rules]
- <<: *if-default-refs
changes: *backend-patterns
-.rails:rules:ee-only-background-migration:predictive:
- rules:
- - !reference [".rails:rules:ee-only-migration:predictive", rules]
- - <<: *if-merge-request
- changes: *backend-patterns
-
.rails:rules:ee-only-unit:
rules:
- <<: *if-not-ee
@@ -1494,17 +1572,6 @@
- <<: *if-default-refs
changes: *backend-patterns
-.rails:rules:ee-only-unit:predictive:
- rules:
- - <<: *if-not-ee
- when: never
- - <<: *if-fork-merge-request
- changes: *backend-patterns
- - !reference [".rails:rules:predictive-default-rules", rules]
- - !reference [".rails:rules:unit-integration:predictive-default-rules", rules]
- - <<: *if-merge-request
- changes: *backend-patterns
-
.rails:rules:ee-only-integration:
rules:
- <<: *if-not-ee
@@ -1515,17 +1582,6 @@
- <<: *if-default-refs
changes: *backend-patterns
-.rails:rules:ee-only-integration:predictive:
- rules:
- - <<: *if-not-ee
- when: never
- - <<: *if-fork-merge-request
- changes: *backend-patterns
- - !reference [".rails:rules:predictive-default-rules", rules]
- - !reference [".rails:rules:unit-integration:predictive-default-rules", rules]
- - <<: *if-merge-request
- changes: *backend-patterns
-
.rails:rules:ee-only-system:
rules:
- <<: *if-not-ee
@@ -1536,15 +1592,6 @@
- <<: *if-default-refs
changes: *code-backstage-patterns
-.rails:rules:ee-only-system:predictive:
- rules:
- - <<: *if-not-ee
- when: never
- - <<: *if-fork-merge-request
- changes: *code-backstage-patterns
- - !reference [".rails:rules:predictive-default-rules", rules]
- - !reference [".rails:rules:system:predictive-default-rules", rules]
-
.rails:rules:as-if-foss-migration:
rules:
- <<: *if-not-ee
@@ -1563,30 +1610,12 @@
- <<: *if-merge-request-not-approved
when: never
-.rails:rules:as-if-foss-migration:predictive:
- rules:
- - <<: *if-not-ee
- when: never
- - !reference [".rails:rules:predictive-default-rules", rules]
- - !reference [".rails:rules:as-if-foss-migration-unit-integration:predictive-default-rules", rules]
- # When DB schema changes, many migrations spec may be affected. However, the test mapping from Crystalball does not map db change to a specific migration spec well.
- # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68840.
- - <<: *if-merge-request-labels-as-if-foss
- changes: *db-patterns
- when: never
-
.rails:rules:as-if-foss-background-migration:
rules:
- !reference [".rails:rules:as-if-foss-migration", rules]
- <<: *if-merge-request-labels-as-if-foss
changes: *backend-patterns
-.rails:rules:as-if-foss-background-migration:predictive:
- rules:
- - !reference [".rails:rules:as-if-foss-migration:predictive", rules]
- - <<: *if-merge-request-labels-as-if-foss
- changes: *backend-patterns
-
.rails:rules:as-if-foss-unit:
rules:
- <<: *if-not-ee
@@ -1597,17 +1626,6 @@
- <<: *if-merge-request-labels-as-if-foss
changes: *backend-patterns
-.rails:rules:as-if-foss-unit:predictive:
- rules:
- - <<: *if-not-ee
- when: never
- - <<: *if-fork-merge-request
- when: never
- - !reference [".rails:rules:predictive-default-rules", rules]
- - !reference [".rails:rules:as-if-foss-migration-unit-integration:predictive-default-rules", rules]
- - <<: *if-merge-request-labels-as-if-foss
- changes: *backend-patterns
-
.rails:rules:as-if-foss-integration:
rules:
- <<: *if-not-ee
@@ -1618,17 +1636,6 @@
- <<: *if-merge-request-labels-as-if-foss
changes: *backend-patterns
-.rails:rules:as-if-foss-integration:predictive:
- rules:
- - <<: *if-not-ee
- when: never
- - <<: *if-fork-merge-request
- when: never
- - !reference [".rails:rules:predictive-default-rules", rules]
- - !reference [".rails:rules:as-if-foss-migration-unit-integration:predictive-default-rules", rules]
- - <<: *if-merge-request-labels-as-if-foss
- changes: *backend-patterns
-
.rails:rules:as-if-foss-system:
rules:
- <<: *if-not-ee
@@ -1639,25 +1646,6 @@
- <<: *if-merge-request-labels-as-if-foss
changes: *code-backstage-patterns
-.rails:rules:as-if-foss-system:predictive:
- rules:
- - <<: *if-not-ee
- when: never
- - <<: *if-fork-merge-request
- when: never
- - !reference [".rails:rules:predictive-default-rules", rules]
- - <<: *if-merge-request
- changes: *core-backend-patterns
- when: never
- - <<: *if-merge-request
- changes: *workhorse-patterns
- when: never
- - <<: *if-merge-request
- changes: *ci-patterns
- when: never
- - <<: *if-merge-request-labels-as-if-foss
- changes: *code-backstage-patterns
-
.rails:rules:ee-and-foss-db-library-code:
rules:
- <<: *if-default-refs
@@ -1749,6 +1737,10 @@
when: never
- <<: *if-merge-request-labels-skip-undercoverage
when: never
+ # We cannot get the coverage data from child pipeline so we only run undercoverage on full pipelines for now
+ # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/113410#note_1335422806
+ - <<: *if-merge-request-not-approved
+ when: never
- <<: *if-merge-request-labels-run-all-rspec
- <<: *if-merge-request
changes: *backend-patterns
diff --git a/.gitlab/ci/setup.gitlab-ci.yml b/.gitlab/ci/setup.gitlab-ci.yml
index 089d5a20ceb..0b5860b3ddb 100644
--- a/.gitlab/ci/setup.gitlab-ci.yml
+++ b/.gitlab/ci/setup.gitlab-ci.yml
@@ -118,17 +118,12 @@ detect-tests:
- |
if [ -n "$CI_MERGE_REQUEST_IID" ]; then
mkdir -p $(dirname "$RSPEC_CHANGED_FILES_PATH")
- tooling/bin/find_changes ${RSPEC_CHANGED_FILES_PATH};
- tooling/bin/find_tests ${RSPEC_CHANGED_FILES_PATH} ${RSPEC_MATCHING_TESTS_PATH};
- tooling/bin/partial_to_views_mappings ${RSPEC_CHANGED_FILES_PATH} ${RSPEC_VIEWS_INCLUDING_PARTIALS_PATH};
- tooling/bin/find_tests ${RSPEC_VIEWS_INCLUDING_PARTIALS_PATH} ${RSPEC_MATCHING_TESTS_PATH};
- tooling/bin/js_to_system_specs_mappings ${RSPEC_CHANGED_FILES_PATH} ${RSPEC_MATCHING_TESTS_PATH};
- tooling/bin/graphql_base_type_mappings ${RSPEC_CHANGED_FILES_PATH} ${RSPEC_MATCHING_TESTS_PATH};
- tooling/bin/view_to_system_specs_mappings ${RSPEC_CHANGED_FILES_PATH} ${RSPEC_MATCHING_TESTS_PATH};
- tooling/bin/find_changes ${RSPEC_CHANGED_FILES_PATH} ${RSPEC_MATCHING_TESTS_PATH} ${FRONTEND_FIXTURES_MAPPING_PATH};
+
+ tooling/bin/predictive_tests
+
filter_rspec_matched_foss_tests ${RSPEC_MATCHING_TESTS_PATH} ${RSPEC_MATCHING_TESTS_FOSS_PATH};
filter_rspec_matched_ee_tests ${RSPEC_MATCHING_TESTS_PATH} ${RSPEC_MATCHING_TESTS_EE_PATH};
- tooling/bin/view_to_js_mappings ${RSPEC_CHANGED_FILES_PATH} ${RSPEC_MATCHING_JS_FILES_PATH};
+
echoinfo "Changed files: $(cat $RSPEC_CHANGED_FILES_PATH)";
echoinfo "Related FOSS RSpec tests: $(cat $RSPEC_MATCHING_TESTS_FOSS_PATH)";
echoinfo "Related EE RSpec tests: $(cat $RSPEC_MATCHING_TESTS_EE_PATH)";
diff --git a/.rubocop_todo/layout/line_length.yml b/.rubocop_todo/layout/line_length.yml
index a7d5a7588e5..71c9dccb76a 100644
--- a/.rubocop_todo/layout/line_length.yml
+++ b/.rubocop_todo/layout/line_length.yml
@@ -5587,7 +5587,6 @@ Layout/LineLength:
- 'spec/workers/todos_destroyer/confidential_issue_worker_spec.rb'
- 'spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb'
- 'spec/workers/users/deactivate_dormant_users_worker_spec.rb'
- - 'tooling/bin/find_changes'
- 'tooling/danger/product_intelligence.rb'
- 'tooling/danger/project_helper.rb'
- 'tooling/danger/specs.rb'
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 42a6ec90baf..642880002b1 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-0270b952e5bff22707247670520c695401715e25
+4ad3613cdc0c0ec0970578a6fdb3c17f06ceba6e
diff --git a/app/assets/javascripts/boards/components/board_app.vue b/app/assets/javascripts/boards/components/board_app.vue
index 48dfcf81f1e..c7e6cb38d15 100644
--- a/app/assets/javascripts/boards/components/board_app.vue
+++ b/app/assets/javascripts/boards/components/board_app.vue
@@ -4,6 +4,7 @@ import { refreshCurrentPage, queryToObject } from '~/lib/utils/url_utility';
import BoardContent from '~/boards/components/board_content.vue';
import BoardSettingsSidebar from '~/boards/components/board_settings_sidebar.vue';
import BoardTopBar from '~/boards/components/board_top_bar.vue';
+import activeBoardItemQuery from 'ee_else_ce/boards/graphql/client/active_board_item.query.graphql';
export default {
components: {
@@ -11,7 +12,7 @@ export default {
BoardSettingsSidebar,
BoardTopBar,
},
- inject: ['initialBoardId', 'initialFilterParams'],
+ inject: ['initialBoardId', 'initialFilterParams', 'isIssueBoard', 'isApolloBoard'],
data() {
return {
boardId: this.initialBoardId,
@@ -19,11 +20,31 @@ export default {
isShowingEpicsSwimlanes: Boolean(queryToObject(window.location.search).group_by),
};
},
+ apollo: {
+ activeBoardItem: {
+ query: activeBoardItemQuery,
+ variables() {
+ return {
+ isIssue: this.isIssueBoard,
+ };
+ },
+ skip() {
+ return !this.isApolloBoard;
+ },
+ },
+ },
+
computed: {
...mapGetters(['isSidebarOpen']),
isSwimlanesOn() {
return (gon?.licensed_features?.swimlanes && this.isShowingEpicsSwimlanes) ?? false;
},
+ isAnySidebarOpen() {
+ if (this.isApolloBoard) {
+ return this.activeBoardItem?.id;
+ }
+ return this.isSidebarOpen;
+ },
},
created() {
window.addEventListener('popstate', refreshCurrentPage);
@@ -45,7 +66,7 @@ export default {
</script>
<template>
- <div class="boards-app gl-relative" :class="{ 'is-compact': isSidebarOpen }">
+ <div class="boards-app gl-relative" :class="{ 'is-compact': isAnySidebarOpen }">
<board-top-bar
:board-id="boardId"
:is-swimlanes-on="isSwimlanesOn"
diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue
index 3071c1f334e..18495f285da 100644
--- a/app/assets/javascripts/boards/components/board_card.vue
+++ b/app/assets/javascripts/boards/components/board_card.vue
@@ -1,6 +1,8 @@
<script>
import { mapActions, mapState } from 'vuex';
import Tracking from '~/tracking';
+import setActiveBoardItemMutation from 'ee_else_ce/boards/graphql/client/set_active_board_item.mutation.graphql';
+import activeBoardItemQuery from 'ee_else_ce/boards/graphql/client/active_board_item.query.graphql';
import BoardCardInner from './board_card_inner.vue';
export default {
@@ -9,7 +11,7 @@ export default {
BoardCardInner,
},
mixins: [Tracking.mixin()],
- inject: ['disabled', 'isApolloBoard'],
+ inject: ['disabled', 'isIssueBoard', 'isApolloBoard'],
props: {
list: {
type: Object,
@@ -37,14 +39,30 @@ export default {
default: true,
},
},
+ apollo: {
+ activeBoardItem: {
+ query: activeBoardItemQuery,
+ variables() {
+ return {
+ isIssue: this.isIssueBoard,
+ };
+ },
+ skip() {
+ return !this.isApolloBoard;
+ },
+ },
+ },
computed: {
...mapState(['selectedBoardItems', 'activeId']),
+ activeItemId() {
+ return this.isApolloBoard ? this.activeBoardItem?.id : this.activeId;
+ },
isActive() {
- return this.item.id === this.activeId;
+ return this.item.id === this.activeItemId;
},
multiSelectVisible() {
return (
- !this.activeId &&
+ !this.activeItemId &&
this.selectedBoardItems.findIndex((boardItem) => boardItem.id === this.item.id) > -1
);
},
@@ -83,10 +101,23 @@ export default {
if (isMultiSelect && gon?.features?.boardMultiSelect) {
this.toggleBoardItemMultiSelection(this.item);
} else {
- this.toggleBoardItem({ boardItem: this.item });
+ if (this.isApolloBoard) {
+ this.toggleItem();
+ } else {
+ this.toggleBoardItem({ boardItem: this.item });
+ }
this.track('click_card', { label: 'right_sidebar' });
}
},
+ toggleItem() {
+ this.$apollo.mutate({
+ mutation: setActiveBoardItemMutation,
+ variables: {
+ boardItem: this.item,
+ isIssue: this.isIssueBoard,
+ },
+ });
+ },
},
};
</script>
diff --git a/app/assets/javascripts/boards/components/board_content_sidebar.vue b/app/assets/javascripts/boards/components/board_content_sidebar.vue
index 779fd1be918..1b97214ff8b 100644
--- a/app/assets/javascripts/boards/components/board_content_sidebar.vue
+++ b/app/assets/javascripts/boards/components/board_content_sidebar.vue
@@ -3,10 +3,12 @@ import { GlDrawer } from '@gitlab/ui';
import { MountingPortal } from 'portal-vue';
import { mapState, mapActions, mapGetters } from 'vuex';
import SidebarDropdownWidget from 'ee_else_ce/sidebar/components/sidebar_dropdown_widget.vue';
+import activeBoardItemQuery from 'ee_else_ce/boards/graphql/client/active_board_item.query.graphql';
+import setActiveBoardItemMutation from 'ee_else_ce/boards/graphql/client/set_active_board_item.mutation.graphql';
import { __, sprintf } from '~/locale';
import BoardSidebarTimeTracker from '~/boards/components/sidebar/board_sidebar_time_tracker.vue';
import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
-import { ISSUABLE, INCIDENT } from '~/boards/constants';
+import { INCIDENT } from '~/boards/constants';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { TYPE_ISSUE, WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants';
import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assignees_widget.vue';
@@ -16,7 +18,6 @@ import SidebarSeverityWidget from '~/sidebar/components/severity/sidebar_severit
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
import SidebarLabelsWidget from '~/sidebar/components/labels/labels_select_widget/labels_select_root.vue';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
components: {
@@ -39,7 +40,6 @@ export default {
SidebarWeightWidget: () =>
import('ee_component/sidebar/components/weight/sidebar_weight_widget.vue'),
},
- mixins: [glFeatureFlagMixin()],
inject: {
multipleAssigneesFeatureAvailable: {
default: false,
@@ -71,31 +71,46 @@ export default {
isGroupBoard: {
default: false,
},
+ isApolloBoard: {
+ default: false,
+ },
},
inheritAttrs: false,
+ apollo: {
+ activeBoardCard: {
+ query: activeBoardItemQuery,
+ variables: {
+ isIssue: true,
+ },
+ update(data) {
+ if (!data.activeBoardItem?.id) {
+ return { id: '', iid: '' };
+ }
+ return {
+ ...data.activeBoardItem,
+ assignees: data.activeBoardItem.assignees?.nodes || [],
+ };
+ },
+ skip() {
+ return !this.isApolloBoard;
+ },
+ },
+ },
computed: {
- ...mapGetters([
- 'isSidebarOpen',
- 'activeBoardItem',
- 'groupPathForActiveIssue',
- 'projectPathForActiveIssue',
- ]),
+ ...mapGetters(['activeBoardItem']),
...mapState(['sidebarType']),
- isIssuableSidebar() {
- return this.sidebarType === ISSUABLE;
+ activeBoardIssuable() {
+ return this.isApolloBoard ? this.activeBoardCard : this.activeBoardItem;
},
- isIncidentSidebar() {
- return this.activeBoardItem.type === INCIDENT;
+ isSidebarOpen() {
+ return Boolean(this.activeBoardIssuable?.id);
},
- showSidebar() {
- return this.isIssuableSidebar && this.isSidebarOpen;
+ isIncidentSidebar() {
+ return this.activeBoardIssuable?.type === INCIDENT;
},
sidebarTitle() {
return this.isIncidentSidebar ? __('Incident details') : __('Issue details');
},
- fullPath() {
- return this.activeBoardItem?.referencePath?.split('#')[0] || '';
- },
parentType() {
return this.isGroupBoard ? WORKSPACE_GROUP : WORKSPACE_PROJECT;
},
@@ -120,6 +135,14 @@ export default {
? this.labelsFilterBasePath.replace(':project_path', this.projectPathForActiveIssue)
: this.labelsFilterBasePath;
},
+ groupPathForActiveIssue() {
+ const { referencePath = '' } = this.activeBoardIssuable;
+ return referencePath.slice(0, referencePath.lastIndexOf('/'));
+ },
+ projectPathForActiveIssue() {
+ const { referencePath = '' } = this.activeBoardIssuable;
+ return referencePath.slice(0, referencePath.indexOf('#'));
+ },
},
methods: {
...mapActions([
@@ -131,7 +154,19 @@ export default {
'setActiveItemHealthStatus',
]),
handleClose() {
- this.toggleBoardItem({ boardItem: this.activeBoardItem, sidebarType: this.sidebarType });
+ if (this.isApolloBoard) {
+ this.$apollo.mutate({
+ mutation: setActiveBoardItemMutation,
+ variables: {
+ boardItem: null,
+ },
+ });
+ } else {
+ this.toggleBoardItem({
+ boardItem: this.activeBoardIssuable,
+ sidebarType: this.sidebarType,
+ });
+ }
},
handleUpdateSelectedLabels({ labels, id }) {
this.setActiveBoardItemLabels({
@@ -143,7 +178,7 @@ export default {
},
handleLabelRemove(removeLabelId) {
this.setActiveBoardItemLabels({
- iid: this.activeBoardItem.iid,
+ iid: this.activeBoardIssuable.iid,
projectPath: this.projectPathForActiveIssue,
removeLabelIds: [removeLabelId],
});
@@ -156,7 +191,7 @@ export default {
<mounting-portal mount-to="#js-right-sidebar-portal" name="board-content-sidebar" append>
<gl-drawer
v-bind="$attrs"
- :open="showSidebar"
+ :open="isSidebarOpen"
class="boards-sidebar"
variant="sidebar"
@close="handleClose"
@@ -167,26 +202,27 @@ export default {
<template #header>
<sidebar-todo-widget
class="gl-mt-3"
- :issuable-id="activeBoardItem.id"
- :issuable-iid="activeBoardItem.iid"
- :full-path="fullPath"
+ :issuable-id="activeBoardIssuable.id"
+ :issuable-iid="activeBoardIssuable.iid"
+ :full-path="projectPathForActiveIssue"
:issuable-type="issuableType"
/>
</template>
<template #default>
- <board-sidebar-title data-testid="sidebar-title" />
+ <board-sidebar-title :active-item="activeBoardIssuable" data-testid="sidebar-title" />
<sidebar-assignees-widget
- :iid="activeBoardItem.iid"
- :full-path="fullPath"
- :initial-assignees="activeBoardItem.assignees"
+ v-if="activeBoardItem.assignees"
+ :iid="activeBoardIssuable.iid"
+ :full-path="projectPathForActiveIssue"
+ :initial-assignees="activeBoardIssuable.assignees"
:allow-multiple-assignees="multipleAssigneesFeatureAvailable"
:editable="canUpdate"
- @assignees-updated="setAssignees"
+ @assignees-updated="!isApolloBoard && setAssignees($event)"
/>
<sidebar-dropdown-widget
v-if="epicFeatureAvailable && !isIncidentSidebar"
:key="`epic-${activeBoardItem.iid}`"
- :iid="activeBoardItem.iid"
+ :iid="activeBoardIssuable.iid"
issuable-attribute="epic"
:workspace-path="projectPathForActiveIssue"
:attr-workspace-path="groupPathForActiveIssue"
@@ -196,7 +232,7 @@ export default {
<div>
<sidebar-dropdown-widget
:key="`milestone-${activeBoardItem.iid}`"
- :iid="activeBoardItem.iid"
+ :iid="activeBoardIssuable.iid"
issuable-attribute="milestone"
:workspace-path="projectPathForActiveIssue"
:attr-workspace-path="projectPathForActiveIssue"
@@ -206,7 +242,7 @@ export default {
<sidebar-iteration-widget
v-if="iterationFeatureAvailable && !isIncidentSidebar"
:key="`iteration-${activeBoardItem.iid}`"
- :iid="activeBoardItem.iid"
+ :iid="activeBoardIssuable.iid"
:workspace-path="projectPathForActiveIssue"
:attr-workspace-path="groupPathForActiveIssue"
:issuable-type="issuableType"
@@ -216,14 +252,14 @@ export default {
</div>
<board-sidebar-time-tracker />
<sidebar-date-widget
- :iid="activeBoardItem.iid"
- :full-path="fullPath"
+ :iid="activeBoardIssuable.iid"
+ :full-path="projectPathForActiveIssue"
:issuable-type="issuableType"
data-testid="sidebar-due-date"
/>
<sidebar-labels-widget
class="block labels"
- :iid="activeBoardItem.iid"
+ :iid="activeBoardIssuable.iid"
:full-path="projectPathForActiveIssue"
:allow-label-remove="allowLabelEdit"
:allow-multiselect="true"
@@ -235,40 +271,40 @@ export default {
workspace-type="project"
:issuable-type="issuableType"
:label-create-type="labelType"
- @onLabelRemove="handleLabelRemove"
- @updateSelectedLabels="handleUpdateSelectedLabels"
+ @onLabelRemove="!isApolloBoard && handleLabelRemove($event)"
+ @updateSelectedLabels="!isApolloBoard && handleUpdateSelectedLabels($event)"
>
{{ __('None') }}
</sidebar-labels-widget>
<sidebar-severity-widget
v-if="isIncidentSidebar"
- :iid="activeBoardItem.iid"
- :project-path="fullPath"
- :initial-severity="activeBoardItem.severity"
+ :iid="activeBoardIssuable.iid"
+ :project-path="projectPathForActiveIssue"
+ :initial-severity="activeBoardIssuable.severity"
/>
<sidebar-weight-widget
v-if="weightFeatureAvailable && !isIncidentSidebar"
- :iid="activeBoardItem.iid"
- :full-path="fullPath"
+ :iid="activeBoardIssuable.iid"
+ :full-path="projectPathForActiveIssue"
:issuable-type="issuableType"
- @weightUpdated="setActiveItemWeight($event)"
+ @weightUpdated="!isApolloBoard && setActiveItemWeight($event)"
/>
<sidebar-health-status-widget
v-if="healthStatusFeatureAvailable"
- :iid="activeBoardItem.iid"
- :full-path="fullPath"
+ :iid="activeBoardIssuable.iid"
+ :full-path="projectPathForActiveIssue"
:issuable-type="issuableType"
- @statusUpdated="setActiveItemHealthStatus($event)"
+ @statusUpdated="!isApolloBoard && setActiveItemHealthStatus($event)"
/>
<sidebar-confidentiality-widget
- :iid="activeBoardItem.iid"
- :full-path="fullPath"
+ :iid="activeBoardIssuable.iid"
+ :full-path="projectPathForActiveIssue"
:issuable-type="issuableType"
- @confidentialityUpdated="setActiveItemConfidential($event)"
+ @confidentialityUpdated="!isApolloBoard && setActiveItemConfidential($event)"
/>
<sidebar-subscriptions-widget
- :iid="activeBoardItem.iid"
- :full-path="fullPath"
+ :iid="activeBoardIssuable.iid"
+ :full-path="projectPathForActiveIssue"
:issuable-type="issuableType"
data-testid="sidebar-notifications"
/>
diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_title.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_title.vue
index 43a2b13b81c..f92c06956a8 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_title.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_title.vue
@@ -19,6 +19,13 @@ export default {
directives: {
autofocusonshow,
},
+ inject: ['isApolloBoard'],
+ props: {
+ activeItem: {
+ type: Object,
+ required: true,
+ },
+ },
data() {
return {
title: '',
@@ -27,7 +34,10 @@ export default {
};
},
computed: {
- ...mapGetters({ item: 'activeBoardItem' }),
+ ...mapGetters(['activeBoardItem']),
+ item() {
+ return this.isApolloBoard ? this.activeItem : this.activeBoardItem;
+ },
pendingChangesStorageKey() {
return this.getPendingChangesKey(this.item);
},
diff --git a/app/assets/javascripts/boards/graphql/client/active_board_item.query.graphql b/app/assets/javascripts/boards/graphql/client/active_board_item.query.graphql
new file mode 100644
index 00000000000..81b1b68a038
--- /dev/null
+++ b/app/assets/javascripts/boards/graphql/client/active_board_item.query.graphql
@@ -0,0 +1,7 @@
+#import "ee_else_ce/boards/graphql/issue.fragment.graphql"
+
+query activeBoardItem {
+ activeBoardItem @client {
+ ...Issue
+ }
+}
diff --git a/app/assets/javascripts/boards/graphql/client/set_active_board_item.mutation.graphql b/app/assets/javascripts/boards/graphql/client/set_active_board_item.mutation.graphql
new file mode 100644
index 00000000000..cce558c649e
--- /dev/null
+++ b/app/assets/javascripts/boards/graphql/client/set_active_board_item.mutation.graphql
@@ -0,0 +1,7 @@
+#import "ee_else_ce/boards/graphql/issue.fragment.graphql"
+
+mutation setActiveBoardItem($boardItem: Issue) {
+ setActiveBoardItem(boardItem: $boardItem) @client {
+ ...Issue
+ }
+}
diff --git a/app/assets/javascripts/artifacts/components/app.vue b/app/assets/javascripts/ci/artifacts/components/app.vue
index 3a07be65341..3a07be65341 100644
--- a/app/assets/javascripts/artifacts/components/app.vue
+++ b/app/assets/javascripts/ci/artifacts/components/app.vue
diff --git a/app/assets/javascripts/artifacts/components/artifact_delete_modal.vue b/app/assets/javascripts/ci/artifacts/components/artifact_delete_modal.vue
index 14edd73824e..14edd73824e 100644
--- a/app/assets/javascripts/artifacts/components/artifact_delete_modal.vue
+++ b/app/assets/javascripts/ci/artifacts/components/artifact_delete_modal.vue
diff --git a/app/assets/javascripts/artifacts/components/artifact_row.vue b/app/assets/javascripts/ci/artifacts/components/artifact_row.vue
index f37c4c6f107..f37c4c6f107 100644
--- a/app/assets/javascripts/artifacts/components/artifact_row.vue
+++ b/app/assets/javascripts/ci/artifacts/components/artifact_row.vue
diff --git a/app/assets/javascripts/artifacts/components/artifacts_bulk_delete.vue b/app/assets/javascripts/ci/artifacts/components/artifacts_bulk_delete.vue
index cc08551fdb7..cc08551fdb7 100644
--- a/app/assets/javascripts/artifacts/components/artifacts_bulk_delete.vue
+++ b/app/assets/javascripts/ci/artifacts/components/artifacts_bulk_delete.vue
diff --git a/app/assets/javascripts/artifacts/components/artifacts_table_row_details.vue b/app/assets/javascripts/ci/artifacts/components/artifacts_table_row_details.vue
index 7d675251ffd..7d675251ffd 100644
--- a/app/assets/javascripts/artifacts/components/artifacts_table_row_details.vue
+++ b/app/assets/javascripts/ci/artifacts/components/artifacts_table_row_details.vue
diff --git a/app/assets/javascripts/artifacts/components/feedback_banner.vue b/app/assets/javascripts/ci/artifacts/components/feedback_banner.vue
index d2c96b1a201..d2c96b1a201 100644
--- a/app/assets/javascripts/artifacts/components/feedback_banner.vue
+++ b/app/assets/javascripts/ci/artifacts/components/feedback_banner.vue
diff --git a/app/assets/javascripts/artifacts/components/job_artifacts_table.vue b/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue
index ba4026190a2..ba4026190a2 100644
--- a/app/assets/javascripts/artifacts/components/job_artifacts_table.vue
+++ b/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue
diff --git a/app/assets/javascripts/artifacts/components/job_checkbox.vue b/app/assets/javascripts/ci/artifacts/components/job_checkbox.vue
index ce49b3f8678..ce49b3f8678 100644
--- a/app/assets/javascripts/artifacts/components/job_checkbox.vue
+++ b/app/assets/javascripts/ci/artifacts/components/job_checkbox.vue
diff --git a/app/assets/javascripts/artifacts/constants.js b/app/assets/javascripts/ci/artifacts/constants.js
index 4ac20d963d1..4ac20d963d1 100644
--- a/app/assets/javascripts/artifacts/constants.js
+++ b/app/assets/javascripts/ci/artifacts/constants.js
diff --git a/app/assets/javascripts/artifacts/graphql/cache_update.js b/app/assets/javascripts/ci/artifacts/graphql/cache_update.js
index 9fa6114c7d4..9fa6114c7d4 100644
--- a/app/assets/javascripts/artifacts/graphql/cache_update.js
+++ b/app/assets/javascripts/ci/artifacts/graphql/cache_update.js
diff --git a/app/assets/javascripts/artifacts/graphql/mutations/bulk_destroy_job_artifacts.mutation.graphql b/app/assets/javascripts/ci/artifacts/graphql/mutations/bulk_destroy_job_artifacts.mutation.graphql
index 421b9258ca0..421b9258ca0 100644
--- a/app/assets/javascripts/artifacts/graphql/mutations/bulk_destroy_job_artifacts.mutation.graphql
+++ b/app/assets/javascripts/ci/artifacts/graphql/mutations/bulk_destroy_job_artifacts.mutation.graphql
diff --git a/app/assets/javascripts/artifacts/graphql/mutations/destroy_artifact.mutation.graphql b/app/assets/javascripts/ci/artifacts/graphql/mutations/destroy_artifact.mutation.graphql
index 529224b47e6..529224b47e6 100644
--- a/app/assets/javascripts/artifacts/graphql/mutations/destroy_artifact.mutation.graphql
+++ b/app/assets/javascripts/ci/artifacts/graphql/mutations/destroy_artifact.mutation.graphql
diff --git a/app/assets/javascripts/artifacts/graphql/queries/get_build_artifacts_size.query.graphql b/app/assets/javascripts/ci/artifacts/graphql/queries/get_build_artifacts_size.query.graphql
index 23da65ad0bb..23da65ad0bb 100644
--- a/app/assets/javascripts/artifacts/graphql/queries/get_build_artifacts_size.query.graphql
+++ b/app/assets/javascripts/ci/artifacts/graphql/queries/get_build_artifacts_size.query.graphql
diff --git a/app/assets/javascripts/artifacts/graphql/queries/get_job_artifacts.query.graphql b/app/assets/javascripts/ci/artifacts/graphql/queries/get_job_artifacts.query.graphql
index 5737f9f8e8d..5737f9f8e8d 100644
--- a/app/assets/javascripts/artifacts/graphql/queries/get_job_artifacts.query.graphql
+++ b/app/assets/javascripts/ci/artifacts/graphql/queries/get_job_artifacts.query.graphql
diff --git a/app/assets/javascripts/artifacts/index.js b/app/assets/javascripts/ci/artifacts/index.js
index 6e795fd9bd7..6e795fd9bd7 100644
--- a/app/assets/javascripts/artifacts/index.js
+++ b/app/assets/javascripts/ci/artifacts/index.js
diff --git a/app/assets/javascripts/artifacts/utils.js b/app/assets/javascripts/ci/artifacts/utils.js
index ebcf0af8d2a..ebcf0af8d2a 100644
--- a/app/assets/javascripts/artifacts/utils.js
+++ b/app/assets/javascripts/ci/artifacts/utils.js
diff --git a/app/assets/javascripts/graphql_shared/issuable_client.js b/app/assets/javascripts/graphql_shared/issuable_client.js
index 3e310f941ec..740eb722629 100644
--- a/app/assets/javascripts/graphql_shared/issuable_client.js
+++ b/app/assets/javascripts/graphql_shared/issuable_client.js
@@ -8,6 +8,7 @@ import typeDefs from '~/work_items/graphql/typedefs.graphql';
import { WIDGET_TYPE_NOTES } from '~/work_items/constants';
import getWorkItemLinksQuery from '~/work_items/graphql/work_item_links.query.graphql';
import { findHierarchyWidgetChildren } from '~/work_items/utils';
+import activeBoardItemQuery from 'ee_else_ce/boards/graphql/client/active_board_item.query.graphql';
export const config = {
typeDefs,
@@ -209,6 +210,13 @@ export const resolvers = {
});
cache.writeQuery({ query: getIssueStateQuery, data });
},
+ setActiveBoardItem(_, { boardItem }, { cache }) {
+ cache.writeQuery({
+ query: activeBoardItemQuery,
+ data: { activeBoardItem: boardItem },
+ });
+ return boardItem;
+ },
},
};
diff --git a/app/assets/javascripts/graphql_shared/possible_types.json b/app/assets/javascripts/graphql_shared/possible_types.json
index 105a02a021c..f7d1efc4d1f 100644
--- a/app/assets/javascripts/graphql_shared/possible_types.json
+++ b/app/assets/javascripts/graphql_shared/possible_types.json
@@ -146,6 +146,7 @@
],
"WorkItemWidget": [
"WorkItemWidgetAssignees",
+ "WorkItemWidgetAwardEmoji",
"WorkItemWidgetCurrentUserTodos",
"WorkItemWidgetDescription",
"WorkItemWidgetHealthStatus",
diff --git a/app/assets/javascripts/invite_members/components/invite_group_notification.vue b/app/assets/javascripts/invite_members/components/invite_group_notification.vue
index 767675cc64c..aaa04dc4b43 100644
--- a/app/assets/javascripts/invite_members/components/invite_group_notification.vue
+++ b/app/assets/javascripts/invite_members/components/invite_group_notification.vue
@@ -1,12 +1,7 @@
<script>
import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
-import { GROUP_MODAL_ALERT_BODY } from '../constants';
-
-const SHARE_GROUP_LINK =
- 'https://docs.gitlab.com/ee/user/group/manage.html#share-a-group-with-another-group';
export default {
- SHARE_GROUP_LINK,
name: 'InviteGroupNotification',
components: { GlAlert, GlSprintf, GlLink },
inject: ['freeUsersLimit'],
@@ -15,18 +10,23 @@ export default {
type: String,
required: true,
},
- },
- i18n: {
- body: GROUP_MODAL_ALERT_BODY,
+ notificationText: {
+ type: String,
+ required: true,
+ },
+ notificationLink: {
+ type: String,
+ required: true,
+ },
},
};
</script>
<template>
<gl-alert variant="warning" :dismissible="false">
- <gl-sprintf :message="$options.i18n.body">
+ <gl-sprintf :message="notificationText">
<template #link="{ content }">
- <gl-link :href="$options.SHARE_GROUP_LINK" target="_blank" class="gl-label-link">{{
+ <gl-link :href="notificationLink" target="_blank" class="gl-label-link">{{
content
}}</gl-link>
</template>
diff --git a/app/assets/javascripts/invite_members/components/invite_groups_modal.vue b/app/assets/javascripts/invite_members/components/invite_groups_modal.vue
index 3be3b9df747..51355baef99 100644
--- a/app/assets/javascripts/invite_members/components/invite_groups_modal.vue
+++ b/app/assets/javascripts/invite_members/components/invite_groups_modal.vue
@@ -190,7 +190,13 @@ export default {
@submit="sendInvite"
>
<template #alert>
- <invite-group-notification v-if="freeUserCapEnabled" :name="name" />
+ <invite-group-notification
+ v-if="freeUserCapEnabled"
+ :name="name"
+ :notification-text="$options.labels[inviteTo].notificationText"
+ :notification-link="$options.labels[inviteTo].notificationLink"
+ class="gl-mb-5"
+ />
</template>
<template #select>
diff --git a/app/assets/javascripts/invite_members/constants.js b/app/assets/javascripts/invite_members/constants.js
index 0dbbd00987b..f373ef81e68 100644
--- a/app/assets/javascripts/invite_members/constants.js
+++ b/app/assets/javascripts/invite_members/constants.js
@@ -1,4 +1,5 @@
import { s__ } from '~/locale';
+import { helpPagePath } from '~/helpers/help_page_helper';
export const SEARCH_DELAY = 200;
export const VALID_TOKEN_BACKGROUND = 'gl-bg-green-100';
@@ -59,9 +60,18 @@ export const GROUP_MODAL_TO_PROJECT_DEFAULT_INTRO_TEXT = s__(
"InviteMembersModal|You're inviting a group to the %{strongStart}%{name}%{strongEnd} project.",
);
-export const GROUP_MODAL_ALERT_BODY = s__(
- 'InviteMembersModal| Inviting a group %{linkStart}adds its members to your group%{linkEnd}, including members who join after the invite. This might put your group over the free %{count} user limit.',
+export const GROUP_MODAL_TO_GROUP_ALERT_BODY = s__(
+ 'InviteMembersModal|Inviting a group %{linkStart}adds its members to your group%{linkEnd}, including members who join after the invite. This might put your group over the free %{count} user limit.',
);
+export const GROUP_MODAL_TO_GROUP_ALERT_LINK = helpPagePath('user/group/manage', {
+ anchor: 'share-a-group-with-another-group',
+});
+export const GROUP_MODAL_TO_PROJECT_ALERT_BODY = s__(
+ 'InviteMembersModal|Inviting a group %{linkStart}adds its members to your project%{linkEnd}, including members who join after the invite. This might put your group over the free %{count} user limit.',
+);
+export const GROUP_MODAL_TO_PROJECT_ALERT_LINK = helpPagePath('user/project/members/index', {
+ anchor: 'add-groups-to-a-project',
+});
export const GROUP_SEARCH_FIELD = s__('InviteMembersModal|Select a group to invite');
export const GROUP_PLACEHOLDER = s__('InviteMembersModal|Search for a group to invite');
@@ -127,9 +137,13 @@ export const GROUP_MODAL_LABELS = {
title: GROUP_MODAL_DEFAULT_TITLE,
toGroup: {
introText: GROUP_MODAL_TO_GROUP_DEFAULT_INTRO_TEXT,
+ notificationText: GROUP_MODAL_TO_GROUP_ALERT_BODY,
+ notificationLink: GROUP_MODAL_TO_GROUP_ALERT_LINK,
},
toProject: {
introText: GROUP_MODAL_TO_PROJECT_DEFAULT_INTRO_TEXT,
+ notificationText: GROUP_MODAL_TO_PROJECT_ALERT_BODY,
+ notificationLink: GROUP_MODAL_TO_PROJECT_ALERT_LINK,
},
searchField: GROUP_SEARCH_FIELD,
placeHolder: GROUP_PLACEHOLDER,
diff --git a/app/assets/javascripts/issues/constants.js b/app/assets/javascripts/issues/constants.js
index d35355a8f26..371db4eacc3 100644
--- a/app/assets/javascripts/issues/constants.js
+++ b/app/assets/javascripts/issues/constants.js
@@ -26,3 +26,8 @@ export const IssuableStatusText = {
[STATUS_MERGED]: __('Merged'),
[STATUS_LOCKED]: __('Open'),
};
+
+export const IssuableTypeText = {
+ [TYPE_ISSUE]: __('issue'),
+ [TYPE_MERGE_REQUEST]: __('merge request'),
+};
diff --git a/app/assets/javascripts/issues/show/components/header_actions.vue b/app/assets/javascripts/issues/show/components/header_actions.vue
index 84def374d13..b929c4dbae0 100644
--- a/app/assets/javascripts/issues/show/components/header_actions.vue
+++ b/app/assets/javascripts/issues/show/components/header_actions.vue
@@ -2,23 +2,36 @@
import {
GlButton,
GlDropdown,
+ GlDropdownDivider,
GlDropdownItem,
GlLink,
GlModal,
GlModalDirective,
GlTooltipDirective,
} from '@gitlab/ui';
+import * as Sentry from '@sentry/browser';
import { mapActions, mapGetters, mapState } from 'vuex';
import { createAlert, VARIANT_SUCCESS } from '~/alert';
import { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issuable/constants';
-import { STATUS_CLOSED, TYPE_INCIDENT, TYPE_ISSUE } from '~/issues/constants';
-import { ISSUE_STATE_EVENT_CLOSE, ISSUE_STATE_EVENT_REOPEN } from '~/issues/show/constants';
+import { STATUS_CLOSED, TYPE_INCIDENT, TYPE_ISSUE, IssuableTypeText } from '~/issues/constants';
+import {
+ ISSUE_STATE_EVENT_CLOSE,
+ ISSUE_STATE_EVENT_REOPEN,
+ NEW_ACTIONS_POPOVER_KEY,
+} from '~/issues/show/constants';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+import { getCookie, parseBoolean, setCookie } from '~/lib/utils/common_utils';
import { visitUrl } from '~/lib/utils/url_utility';
import { s__, __, sprintf } from '~/locale';
import eventHub from '~/notes/event_hub';
import Tracking from '~/tracking';
+import toast from '~/vue_shared/plugins/global_toast';
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
+import NewHeaderActionsPopover from '~/issues/show/components/new_header_actions_popover.vue';
+import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
+import IssuableLockForm from '~/sidebar/components/lock/issuable_lock_form.vue';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import issueReferenceQuery from '~/sidebar/queries/issue_reference.query.graphql';
import issuesEventHub from '../event_hub';
import promoteToEpicMutation from '../queries/promote_to_epic.mutation.graphql';
import updateIssueMutation from '../queries/update_issue.mutation.graphql';
@@ -44,21 +57,27 @@ export default {
'The issue was successfully promoted to an epic. Redirecting to epic...',
),
reportAbuse: __('Report abuse to administrator'),
+ referenceFetchError: __('An error occurred while fetching reference'),
+ copyReferenceText: __('Copy reference'),
},
components: {
DeleteIssueModal,
GlButton,
GlDropdown,
+ GlDropdownDivider,
GlDropdownItem,
GlLink,
GlModal,
AbuseCategorySelector,
+ NewHeaderActionsPopover,
+ SidebarSubscriptionsWidget,
+ IssuableLockForm,
},
directives: {
GlModal: GlModalDirective,
GlTooltip: GlTooltipDirective,
},
- mixins: [trackingMixin],
+ mixins: [trackingMixin, glFeatureFlagMixin()],
inject: {
canCreateIssue: {
default: false,
@@ -105,15 +124,46 @@ export default {
reportedFromUrl: {
default: '',
},
+ issuableEmailAddress: {
+ default: '',
+ },
+ fullPath: {
+ default: '',
+ },
},
data() {
return {
isReportAbuseDrawerOpen: false,
};
},
+ apollo: {
+ issuableReference: {
+ query: issueReferenceQuery,
+ variables() {
+ return {
+ fullPath: this.fullPath,
+ iid: this.iid,
+ };
+ },
+ update(data) {
+ return data.workspace?.issuable?.reference || '';
+ },
+ skip() {
+ return !this.isMrSidebarMoved;
+ },
+ error(error) {
+ createAlert({ message: this.$options.i18n.referenceFetchError });
+ Sentry.captureException(error);
+ },
+ },
+ },
computed: {
...mapState(['isToggleStateButtonLoading']),
...mapGetters(['openState', 'getBlockedByIssues']),
+ ...mapGetters(['getNoteableData']),
+ isLocked() {
+ return this.getNoteableData.discussion_locked;
+ },
isClosed() {
return this.openState === STATUS_CLOSED;
},
@@ -157,6 +207,17 @@ export default {
hasMobileDropdown() {
return this.hasDesktopDropdown || this.showToggleIssueStateButton;
},
+ copyMailAddressText() {
+ return sprintf(__('Copy %{issueType} email address'), {
+ issueType: IssuableTypeText[this.issueType],
+ });
+ },
+ isMrSidebarMoved() {
+ return this.glFeatures.movedMrSidebar;
+ },
+ showLockIssueOption() {
+ return this.isMrSidebarMoved && this.issueType === TYPE_ISSUE;
+ },
},
created() {
eventHub.$on('toggle.issuable.state', this.toggleIssueState);
@@ -166,6 +227,7 @@ export default {
},
methods: {
...mapActions(['toggleStateButtonLoading']),
+ ...mapActions(['updateLockedAttribute']),
toggleIssueState() {
if (!this.isClosed && this.getBlockedByIssues?.length) {
this.$refs.blockedByIssuesModal.show();
@@ -244,7 +306,19 @@ export default {
edit() {
issuesEventHub.$emit('open.form');
},
+ dismissPopover() {
+ if (this.isMrSidebarMoved && !parseBoolean(getCookie(`${NEW_ACTIONS_POPOVER_KEY}`))) {
+ setCookie(NEW_ACTIONS_POPOVER_KEY, true);
+ }
+ },
+ copyReference() {
+ toast(__('Reference copied'));
+ },
+ copyEmailAddress() {
+ toast(__('Email address copied'));
+ },
},
+ TYPE_ISSUE,
};
</script>
@@ -259,6 +333,21 @@ export default {
data-testid="mobile-dropdown"
:loading="isToggleStateButtonLoading"
>
+ <template v-if="isMrSidebarMoved">
+ <sidebar-subscriptions-widget
+ :iid="String(iid)"
+ :full-path="fullPath"
+ :issuable-type="$options.TYPE_ISSUE"
+ data-testid="notification-toggle"
+ />
+
+ <gl-dropdown-divider />
+ </template>
+
+ <template v-if="showLockIssueOption">
+ <issuable-lock-form :is-editable="false" data-testid="lock-issue-toggle" />
+ </template>
+
<gl-dropdown-item v-if="canUpdateIssue" @click="edit">
{{ $options.i18n.edit }}
</gl-dropdown-item>
@@ -275,9 +364,21 @@ export default {
<gl-dropdown-item v-if="canPromoteToEpic" @click="promoteToEpic">
{{ __('Promote to epic') }}
</gl-dropdown-item>
- <gl-dropdown-item v-if="!isIssueAuthor" @click="toggleReportAbuseDrawer(true)">
- {{ $options.i18n.reportAbuse }}
- </gl-dropdown-item>
+ <template v-if="isMrSidebarMoved">
+ <gl-dropdown-item
+ :data-clipboard-text="issuableReference"
+ data-testid="copy-reference"
+ @click="copyReference"
+ >{{ $options.i18n.copyReferenceText }}</gl-dropdown-item
+ >
+ <gl-dropdown-item
+ v-if="issuableEmailAddress"
+ :data-clipboard-text="issuableEmailAddress"
+ data-testid="copy-email"
+ @click="copyEmailAddress"
+ >{{ copyMailAddressText }}</gl-dropdown-item
+ >
+ </template>
<gl-dropdown-item
v-if="canReportSpam"
:href="submitAsSpamPath"
@@ -287,6 +388,7 @@ export default {
{{ __('Submit as spam') }}
</gl-dropdown-item>
<template v-if="canDestroyIssue">
+ <gl-dropdown-divider />
<gl-dropdown-item
v-gl-modal="$options.deleteModalId"
variant="danger"
@@ -295,6 +397,13 @@ export default {
{{ deleteButtonText }}
</gl-dropdown-item>
</template>
+ <gl-dropdown-item
+ v-if="!isIssueAuthor"
+ data-testid="report-abuse-item"
+ @click="toggleReportAbuseDrawer(true)"
+ >
+ {{ $options.i18n.reportAbuse }}
+ </gl-dropdown-item>
</gl-dropdown>
<gl-button
@@ -322,6 +431,7 @@ export default {
<gl-dropdown
v-if="hasDesktopDropdown"
+ id="new-actions-header-dropdown"
v-gl-tooltip.hover
class="gl-display-none gl-sm-display-inline-flex! gl-sm-ml-3"
icon="ellipsis_v"
@@ -334,7 +444,19 @@ export default {
data-testid="desktop-dropdown"
no-caret
right
+ @shown="dismissPopover"
>
+ <template v-if="isMrSidebarMoved">
+ <sidebar-subscriptions-widget
+ :iid="String(iid)"
+ :full-path="fullPath"
+ :issuable-type="$options.TYPE_ISSUE"
+ data-testid="notification-toggle"
+ />
+
+ <gl-dropdown-divider />
+ </template>
+
<gl-dropdown-item v-if="canCreateIssue" :href="newIssuePath">
{{ newIssueTypeText }}
</gl-dropdown-item>
@@ -346,9 +468,24 @@ export default {
>
{{ __('Promote to epic') }}
</gl-dropdown-item>
- <gl-dropdown-item v-if="!isIssueAuthor" @click="toggleReportAbuseDrawer(true)">
- {{ $options.i18n.reportAbuse }}
- </gl-dropdown-item>
+ <template v-if="showLockIssueOption">
+ <issuable-lock-form :is-editable="false" data-testid="lock-issue-toggle" />
+ </template>
+ <template v-if="isMrSidebarMoved">
+ <gl-dropdown-item
+ :data-clipboard-text="issuableReference"
+ data-testid="copy-reference"
+ @click="copyReference"
+ >{{ $options.i18n.copyReferenceText }}</gl-dropdown-item
+ >
+ <gl-dropdown-item
+ v-if="issuableEmailAddress"
+ :data-clipboard-text="issuableEmailAddress"
+ data-testid="copy-email"
+ @click="copyEmailAddress"
+ >{{ copyMailAddressText }}</gl-dropdown-item
+ >
+ </template>
<gl-dropdown-item
v-if="canReportSpam"
:href="submitAsSpamPath"
@@ -357,8 +494,8 @@ export default {
>
{{ __('Submit as spam') }}
</gl-dropdown-item>
-
<template v-if="canDestroyIssue">
+ <gl-dropdown-divider />
<gl-dropdown-item
v-gl-modal="$options.deleteModalId"
variant="danger"
@@ -368,8 +505,16 @@ export default {
{{ deleteButtonText }}
</gl-dropdown-item>
</template>
+ <gl-dropdown-item
+ v-if="!isIssueAuthor"
+ data-testid="report-abuse-item"
+ @click="toggleReportAbuseDrawer(true)"
+ >
+ {{ $options.i18n.reportAbuse }}
+ </gl-dropdown-item>
</gl-dropdown>
+ <new-header-actions-popover v-if="isMrSidebarMoved" :issue-type="issueType" />
<gl-modal
ref="blockedByIssuesModal"
modal-id="blocked-by-issues-modal"
diff --git a/app/assets/javascripts/issues/show/components/new_header_actions_popover.vue b/app/assets/javascripts/issues/show/components/new_header_actions_popover.vue
new file mode 100644
index 00000000000..975661c7d32
--- /dev/null
+++ b/app/assets/javascripts/issues/show/components/new_header_actions_popover.vue
@@ -0,0 +1,77 @@
+<script>
+import { GlPopover, GlButton } from '@gitlab/ui';
+import { s__, sprintf } from '~/locale';
+import { getCookie, parseBoolean, setCookie } from '~/lib/utils/common_utils';
+import { NEW_ACTIONS_POPOVER_KEY } from '~/issues/show/constants';
+import { IssuableTypeText } from '~/issues/constants';
+
+export default {
+ name: 'NewHeaderActionsPopover',
+ i18n: {
+ popoverText: s__(
+ 'HeaderAction|Notifications and other %{issueType} actions have moved to this menu.',
+ ),
+ confirmButtonText: s__('HeaderAction|Okay!'),
+ },
+ components: {
+ GlPopover,
+ GlButton,
+ },
+ props: {
+ issueType: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ dismissKey: NEW_ACTIONS_POPOVER_KEY,
+ popoverDismissed: parseBoolean(getCookie(`${NEW_ACTIONS_POPOVER_KEY}`)),
+ };
+ },
+ computed: {
+ popoverText() {
+ return sprintf(this.$options.i18n.popoverText, {
+ issueType: IssuableTypeText[this.issueType],
+ });
+ },
+ showPopover() {
+ return !this.popoverDismissed;
+ },
+ },
+ methods: {
+ dismissPopover() {
+ this.popoverDismissed = true;
+ setCookie(this.dismissKey, this.popoverDismissed);
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-popover
+ v-if="showPopover"
+ target="new-actions-header-dropdown"
+ container="viewport"
+ placement="left"
+ :show="showPopover"
+ triggers="manual"
+ content="text"
+ :css-classes="['gl-p-2 new-header-popover']"
+ >
+ <template #title>
+ <div class="gl-font-base gl-font-weight-normal">
+ {{ popoverText }}
+ </div>
+ </template>
+ <gl-button
+ data-testid="confirm-button"
+ variant="confirm"
+ type="submit"
+ @click="dismissPopover"
+ >{{ $options.i18n.confirmButtonText }}</gl-button
+ >
+ </gl-popover>
+ </div>
+</template>
diff --git a/app/assets/javascripts/issues/show/constants.js b/app/assets/javascripts/issues/show/constants.js
index 4d8c11f9669..6320e4ef266 100644
--- a/app/assets/javascripts/issues/show/constants.js
+++ b/app/assets/javascripts/issues/show/constants.js
@@ -17,3 +17,5 @@ export const issueState = {
issueType: undefined,
isDirty: false,
};
+
+export const NEW_ACTIONS_POPOVER_KEY = 'new-actions-popover-viewed';
diff --git a/app/assets/javascripts/issues/show/index.js b/app/assets/javascripts/issues/show/index.js
index e677328cd2e..100abcbe1e5 100644
--- a/app/assets/javascripts/issues/show/index.js
+++ b/app/assets/javascripts/issues/show/index.js
@@ -174,6 +174,8 @@ export function initHeaderActions(store, type = '') {
reportedUserId: parseInt(el.dataset.reportedUserId, 10),
reportedFromUrl: el.dataset.reportedFromUrl,
submitAsSpamPath: el.dataset.submitAsSpamPath,
+ issuableEmailAddress: el.dataset.issuableEmailAddress,
+ fullPath: el.dataset.projectPath,
},
render: (createElement) => createElement(HeaderActions),
});
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index 42e1577e663..dee97347496 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -1,5 +1,5 @@
<script>
-import { GlButton, GlSprintf, GlLink } from '@gitlab/ui';
+import { GlButton, GlSprintf, GlLink, GlFormCheckbox } from '@gitlab/ui';
import { mapGetters, mapActions, mapState } from 'vuex';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
@@ -20,6 +20,7 @@ export default {
GlButton,
GlSprintf,
GlLink,
+ GlFormCheckbox,
},
mixins: [issuableStateMixin, resolvable, glFeaturesFlagMixin()],
props: {
@@ -372,12 +373,14 @@ export default {
<p v-if="showResolveDiscussionToggle">
<label>
<template v-if="discussionResolved">
- <input v-model="isUnresolving" type="checkbox" class="js-unresolve-checkbox" />
- {{ __('Unresolve thread') }}
+ <gl-form-checkbox v-model="isUnresolving" class="js-unresolve-checkbox">
+ {{ __('Unresolve thread') }}
+ </gl-form-checkbox>
</template>
<template v-else>
- <input v-model="isResolving" type="checkbox" class="js-resolve-checkbox" />
- {{ __('Resolve thread') }}
+ <gl-form-checkbox v-model="isResolving" class="js-resolve-checkbox">
+ {{ __('Resolve thread') }}
+ </gl-form-checkbox>
</template>
</label>
</p>
diff --git a/app/assets/javascripts/pages/projects/artifacts/index.js b/app/assets/javascripts/pages/projects/artifacts/index.js
index 4aa9b225790..df8f110a60d 100644
--- a/app/assets/javascripts/pages/projects/artifacts/index.js
+++ b/app/assets/javascripts/pages/projects/artifacts/index.js
@@ -1,3 +1,3 @@
-import { initArtifactsTable } from '~/artifacts/index';
+import { initArtifactsTable } from '~/ci/artifacts/index';
initArtifactsTable();
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index 297b8ae1fc2..58e4553d00d 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -56,8 +56,10 @@ Sidebar.prototype.addEventListeners = function () {
const layoutPage = document.querySelector('.layout-page');
const rightSidebar = document.querySelector('.js-right-sidebar');
- updateSidebarClasses(layoutPage, rightSidebar);
- window.addEventListener('resize', () => updateSidebarClasses(layoutPage, rightSidebar));
+ if (rightSidebar.classList.contains('right-sidebar-merge-requests')) {
+ updateSidebarClasses(layoutPage, rightSidebar);
+ window.addEventListener('resize', () => updateSidebarClasses(layoutPage, rightSidebar));
+ }
}
};
diff --git a/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue b/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
index 1eff4db3970..06876546fa4 100644
--- a/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
+++ b/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
@@ -1,8 +1,9 @@
<script>
import { GlIcon, GlTooltipDirective, GlOutsideDirective as Outside } from '@gitlab/ui';
import { mapGetters, mapActions } from 'vuex';
-import { TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/issues/constants';
+import { TYPE_ISSUE } from '~/issues/constants';
import { __, sprintf } from '~/locale';
+import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { createAlert } from '~/alert';
import toast from '~/vue_shared/plugins/global_toast';
@@ -45,10 +46,8 @@ export default {
},
computed: {
...mapGetters(['getNoteableData']),
- isMergeRequest() {
- return (
- this.getNoteableData.targetType === TYPE_MERGE_REQUEST && this.glFeatures.movedMrSidebar
- );
+ isMovedMrSidebar() {
+ return this.glFeatures.movedMrSidebar;
},
issuableDisplayName() {
const isInIssuePage = this.getNoteableData.targetType === TYPE_ISSUE;
@@ -60,7 +59,6 @@ export default {
lockStatus() {
return this.isLocked ? this.$options.locked : this.$options.unlocked;
},
-
tooltipLabel() {
return this.isLocked ? __('Locked') : __('Unlocked');
},
@@ -89,8 +87,13 @@ export default {
fullPath: this.fullPath,
})
.then(() => {
- if (this.isMergeRequest) {
- toast(this.isLocked ? __('Merge request locked.') : __('Merge request unlocked.'));
+ if (this.isMovedMrSidebar) {
+ toast(
+ sprintf(__('%{issuableDisplayName} %{lockStatus}.'), {
+ issuableDisplayName: capitalizeFirstCharacter(this.issuableDisplayName),
+ lockStatus: this.isLocked ? __('locked') : __('unlocked'),
+ }),
+ );
}
})
.catch(() => {
@@ -113,14 +116,14 @@ export default {
</script>
<template>
- <li v-if="isMergeRequest" class="gl-dropdown-item">
- <button type="button" class="dropdown-item" @click="toggleLocked">
+ <li v-if="isMovedMrSidebar" class="gl-dropdown-item">
+ <button type="button" class="dropdown-item" data-testid="issuable-lock" @click="toggleLocked">
<span class="gl-dropdown-item-text-wrapper">
<template v-if="isLocked">
- {{ __('Unlock merge request') }}
+ {{ sprintf(__('Unlock %{issuableType}'), { issuableType: issuableDisplayName }) }}
</template>
<template v-else>
- {{ __('Lock merge request') }}
+ {{ sprintf(__('Lock %{issuableType}'), { issuableType: issuableDisplayName }) }}
</template>
</span>
</button>
diff --git a/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue b/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue
index a3710d9534e..23570a374f0 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue
@@ -58,6 +58,9 @@ export default {
approvedByTooltipTitle(user) {
return sprintf(s__('MergeRequest|Approved by @%{username}'), user);
},
+ reviewedButNotApprovedTooltip(user) {
+ return sprintf(s__('MergeRequest|Reviewed by @%{username} but not yet approved'), user);
+ },
toggleShowLess() {
this.showLess = !this.showLess;
},
@@ -105,35 +108,37 @@ export default {
{{ user.name }}
</div>
</reviewer-avatar-link>
- <gl-icon
- v-if="user.mergeRequestInteraction.approved"
- v-gl-tooltip.left
- :size="16"
- :title="approvedByTooltipTitle(user)"
- name="status-success"
- class="float-right gl-my-2 gl-ml-auto gl-text-green-500 gl-flex-shrink-0"
- data-testid="re-approved"
- />
- <gl-icon
- v-if="loadingStates[user.id] === $options.SUCCESS_STATE"
- :size="24"
- name="check"
- class="float-right gl-py-2 gl-mr-2 gl-text-green-500"
- data-testid="re-request-success"
- />
<gl-button
- v-else-if="user.mergeRequestInteraction.canUpdate && user.mergeRequestInteraction.reviewed"
+ v-if="user.mergeRequestInteraction.canUpdate && user.mergeRequestInteraction.reviewed"
v-gl-tooltip.left
:title="$options.i18n.reRequestReview"
:aria-label="$options.i18n.reRequestReview"
:loading="loadingStates[user.id] === $options.LOADING_STATE"
- class="float-right gl-text-gray-500!"
+ class="float-right gl-text-gray-500! gl-mr-2"
size="small"
icon="redo"
variant="link"
data-testid="re-request-button"
@click="reRequestReview(user.id)"
/>
+ <gl-icon
+ v-if="user.mergeRequestInteraction.approved"
+ v-gl-tooltip.left
+ :size="16"
+ :title="approvedByTooltipTitle(user)"
+ name="status-success"
+ class="float-right gl-my-2 gl-ml-auto gl-text-green-500 gl-flex-shrink-0"
+ data-testid="approved"
+ />
+ <gl-icon
+ v-else-if="user.mergeRequestInteraction.reviewed"
+ v-gl-tooltip.left
+ :size="16"
+ :title="reviewedButNotApprovedTooltip(user)"
+ name="dotted-circle"
+ class="float-right gl-my-2 gl-ml-auto gl-text-gray-400 gl-flex-shrink-0"
+ data-testid="reviewed-not-approved"
+ />
</div>
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue
index 344fa880131..f2b960ed02c 100644
--- a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue
+++ b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue
@@ -1,12 +1,7 @@
<script>
import { GlDropdownForm, GlIcon, GlLoadingIcon, GlToggle, GlTooltipDirective } from '@gitlab/ui';
import { createAlert } from '~/alert';
-import {
- TYPE_EPIC,
- TYPE_MERGE_REQUEST,
- WORKSPACE_GROUP,
- WORKSPACE_PROJECT,
-} from '~/issues/constants';
+import { TYPE_EPIC, WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants';
import { isLoggedIn } from '~/lib/utils/common_utils';
import { __, sprintf } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -91,8 +86,8 @@ export default {
},
},
computed: {
- isMergeRequest() {
- return this.issuableType === TYPE_MERGE_REQUEST && this.glFeatures.movedMrSidebar;
+ isMovedMrSidebar() {
+ return this.glFeatures.movedMrSidebar;
},
isLoading() {
return this.$apollo.queries?.subscribed?.loading || this.loading;
@@ -148,7 +143,7 @@ export default {
});
}
- if (this.isMergeRequest) {
+ if (this.isMovedMrSidebar) {
toast(subscribed ? __('Notifications turned on.') : __('Notifications turned off.'));
}
},
@@ -187,7 +182,7 @@ export default {
</script>
<template>
- <gl-dropdown-form v-if="isMergeRequest" class="gl-dropdown-item">
+ <gl-dropdown-form v-if="isMovedMrSidebar" class="gl-dropdown-item">
<div class="gl-px-5 gl-pb-2 gl-pt-1">
<gl-toggle
:value="subscribed"
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index 2c56dc34701..1542c2e441d 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -17,6 +17,7 @@ import { __ } from '~/locale';
import { apolloProvider } from '~/graphql_shared/issuable_client';
import Translate from '~/vue_shared/translate';
import UserSelect from '~/vue_shared/components/user_select/user_select.vue';
+import NewHeaderActionsPopover from '~/issues/show/components/new_header_actions_popover.vue';
import CollapsedAssigneeList from './components/assignees/collapsed_assignee_list.vue';
import SidebarAssignees from './components/assignees/sidebar_assignees.vue';
import SidebarAssigneesWidget from './components/assignees/sidebar_assignees_widget.vue';
@@ -787,6 +788,21 @@ export function mountAssigneesDropdown() {
});
}
+function mountNewIssuePopover() {
+ const el = document.querySelector('.js-sidebar-header-popover');
+
+ if (!el) {
+ return null;
+ }
+
+ return new Vue({
+ el,
+ name: 'NewHeaderActionsPopover',
+ render: (createElement) =>
+ createElement(NewHeaderActionsPopover, { props: { issueType: TYPE_MERGE_REQUEST } }),
+ });
+}
+
const isAssigneesWidgetShown =
(isInIssuePage() || isInDesignPage() || isInMRPage()) && gon.features.issueAssigneesWidget;
@@ -814,6 +830,7 @@ export function mountSidebar(mediator, store) {
mountSidebarSeverityWidget();
mountSidebarEscalationStatus();
mountMoveIssueButton();
+ mountNewIssuePopover();
}
export { getSidebarOptions };
diff --git a/app/assets/stylesheets/page_bundles/issuable.scss b/app/assets/stylesheets/page_bundles/issuable.scss
index e0fb95a1359..1b98fd4df07 100644
--- a/app/assets/stylesheets/page_bundles/issuable.scss
+++ b/app/assets/stylesheets/page_bundles/issuable.scss
@@ -165,3 +165,13 @@
border: 0;
}
}
+
+.merge-request-notification-toggle {
+ .gl-toggle {
+ @include gl-ml-auto;
+ }
+
+ .gl-toggle-label {
+ @include gl-font-weight-normal;
+ }
+}
diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss
index de8142924f9..74f61faa9ae 100644
--- a/app/assets/stylesheets/pages/detail_page.scss
+++ b/app/assets/stylesheets/pages/detail_page.scss
@@ -74,3 +74,7 @@
color: $gl-text-color;
}
}
+
+.new-header-popover {
+ z-index: 999;
+}
diff --git a/app/controllers/projects/incidents_controller.rb b/app/controllers/projects/incidents_controller.rb
index 3842a88d15b..7121096bd77 100644
--- a/app/controllers/projects/incidents_controller.rb
+++ b/app/controllers/projects/incidents_controller.rb
@@ -10,6 +10,7 @@ class Projects::IncidentsController < Projects::ApplicationController
push_force_frontend_feature_flag(:work_items, @project&.work_items_feature_flag_enabled?)
push_force_frontend_feature_flag(:work_items_mvc, @project&.work_items_mvc_feature_flag_enabled?)
push_force_frontend_feature_flag(:work_items_mvc_2, @project&.work_items_mvc_2_feature_flag_enabled?)
+ push_frontend_feature_flag(:moved_mr_sidebar, project)
end
feature_category :incident_management
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index efe88d17cab..d50f681beec 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -67,6 +67,7 @@ class Projects::IssuesController < Projects::ApplicationController
push_force_frontend_feature_flag(:work_items_mvc, project&.work_items_mvc_feature_flag_enabled?)
push_force_frontend_feature_flag(:work_items_mvc_2, project&.work_items_mvc_2_feature_flag_enabled?)
push_frontend_feature_flag(:epic_widget_edit_confirmation, project)
+ push_frontend_feature_flag(:moved_mr_sidebar, project)
end
around_action :allow_gitaly_ref_name_caching, only: [:discussions]
diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb
index 81017290f12..3d764f67990 100644
--- a/app/finders/notes_finder.rb
+++ b/app/finders/notes_finder.rb
@@ -31,6 +31,7 @@ class NotesFinder
notes = since_fetch_at(notes)
notes = notes.with_notes_filter(@params[:notes_filter]) if notes_filter?
notes = redact_internal(notes)
+ notes = notes.without_hidden if without_hidden_notes?
sort(notes)
end
@@ -189,6 +190,13 @@ class NotesFinder
notes.not_internal
end
+
+ def without_hidden_notes?
+ return false unless Feature.enabled?(:hidden_notes)
+ return false if @current_user&.can_admin_all_resources?
+
+ true
+ end
end
NotesFinder.prepend_mod_with('NotesFinder')
diff --git a/app/graphql/batch_loaders/award_emoji_votes_batch_loader.rb b/app/graphql/batch_loaders/award_emoji_votes_batch_loader.rb
index c1b35d3eaf7..4ce9baff8cb 100644
--- a/app/graphql/batch_loaders/award_emoji_votes_batch_loader.rb
+++ b/app/graphql/batch_loaders/award_emoji_votes_batch_loader.rb
@@ -1,21 +1,25 @@
# frozen_string_literal: true
module BatchLoaders
- module AwardEmojiVotesBatchLoader
- private
+ class AwardEmojiVotesBatchLoader
+ def self.load_upvotes(object, awardable_class: nil)
+ load_votes_for(object, AwardEmoji::UPVOTE_NAME, awardable_class: awardable_class)
+ end
+
+ def self.load_downvotes(object, awardable_class: nil)
+ load_votes_for(object, AwardEmoji::DOWNVOTE_NAME, awardable_class: awardable_class)
+ end
- def load_votes(object, vote_type)
- BatchLoader::GraphQL.for(object.id).batch(key: "#{object.issuing_parent_id}-#{vote_type}") do |ids, loader, args|
- counts = AwardEmoji.votes_for_collection(ids, object.class.name).named(vote_type).index_by(&:awardable_id)
+ def self.load_votes_for(object, vote_type, awardable_class: nil)
+ awardable_class ||= object.class.name
+
+ BatchLoader::GraphQL.for(object.id).batch(key: "#{object.issuing_parent_id}-#{vote_type}") do |ids, loader, _args|
+ counts = AwardEmoji.votes_for_collection(ids, awardable_class).named(vote_type).index_by(&:awardable_id)
ids.each do |id|
loader.call(id, counts[id]&.count || 0)
end
end
end
-
- def authorized_resource?(object)
- Ability.allowed?(current_user, "read_#{object.to_ability_name}".to_sym, object)
- end
end
end
diff --git a/app/graphql/resolvers/award_emoji/base_votes_count_resolver.rb b/app/graphql/resolvers/award_emoji/base_votes_count_resolver.rb
new file mode 100644
index 00000000000..406c52eb0d5
--- /dev/null
+++ b/app/graphql/resolvers/award_emoji/base_votes_count_resolver.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module AwardEmoji
+ class BaseVotesCountResolver < BaseResolver
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+
+ type GraphQL::Types::Int, null: true
+
+ private
+
+ def authorized_resource?(object)
+ Ability.allowed?(current_user, "read_#{object.to_ability_name}".to_sym, object)
+ end
+
+ def votes_batch_loader
+ BatchLoaders::AwardEmojiVotesBatchLoader
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/down_votes_count_resolver.rb b/app/graphql/resolvers/down_votes_count_resolver.rb
index 0e7772f988a..5f5340578cd 100644
--- a/app/graphql/resolvers/down_votes_count_resolver.rb
+++ b/app/graphql/resolvers/down_votes_count_resolver.rb
@@ -1,15 +1,12 @@
# frozen_string_literal: true
module Resolvers
- class DownVotesCountResolver < BaseResolver
- include Gitlab::Graphql::Authorize::AuthorizeResource
- include BatchLoaders::AwardEmojiVotesBatchLoader
-
+ class DownVotesCountResolver < Resolvers::AwardEmoji::BaseVotesCountResolver
type GraphQL::Types::Int, null: true
def resolve
authorize!(object)
- load_votes(object, AwardEmoji::DOWNVOTE_NAME)
+ votes_batch_loader.load_downvotes(object)
end
end
end
diff --git a/app/graphql/resolvers/up_votes_count_resolver.rb b/app/graphql/resolvers/up_votes_count_resolver.rb
index 1c78facb694..8b2d705c07a 100644
--- a/app/graphql/resolvers/up_votes_count_resolver.rb
+++ b/app/graphql/resolvers/up_votes_count_resolver.rb
@@ -1,15 +1,12 @@
# frozen_string_literal: true
module Resolvers
- class UpVotesCountResolver < BaseResolver
- include Gitlab::Graphql::Authorize::AuthorizeResource
- include BatchLoaders::AwardEmojiVotesBatchLoader
-
+ class UpVotesCountResolver < Resolvers::AwardEmoji::BaseVotesCountResolver
type GraphQL::Types::Int, null: true
def resolve
authorize!(object)
- load_votes(object, AwardEmoji::UPVOTE_NAME)
+ votes_batch_loader.load_upvotes(object)
end
end
end
diff --git a/app/graphql/resolvers/work_items_resolver.rb b/app/graphql/resolvers/work_items_resolver.rb
index 967788e056e..14eec4f696a 100644
--- a/app/graphql/resolvers/work_items_resolver.rb
+++ b/app/graphql/resolvers/work_items_resolver.rb
@@ -56,7 +56,8 @@ module Resolvers
children: { work_item_children_by_relative_position: [:author, { project: :project_feature }] },
labels: :labels,
milestone: { milestone: [:project, :group] },
- subscribed: [:assignees, :award_emoji, { notes: [:author, :award_emoji] }]
+ subscribed: [:assignees, :award_emoji, { notes: [:author, :award_emoji] }],
+ award_emoji: { award_emoji: :awardable }
}
end
diff --git a/app/graphql/types/work_items/widget_interface.rb b/app/graphql/types/work_items/widget_interface.rb
index e562312d06e..53ea901ea10 100644
--- a/app/graphql/types/work_items/widget_interface.rb
+++ b/app/graphql/types/work_items/widget_interface.rb
@@ -20,7 +20,8 @@ module Types
::Types::WorkItems::Widgets::MilestoneType,
::Types::WorkItems::Widgets::NotesType,
::Types::WorkItems::Widgets::NotificationsType,
- ::Types::WorkItems::Widgets::CurrentUserTodosType
+ ::Types::WorkItems::Widgets::CurrentUserTodosType,
+ ::Types::WorkItems::Widgets::AwardEmojiType
].freeze
def self.ce_orphan_types
@@ -50,6 +51,8 @@ module Types
::Types::WorkItems::Widgets::NotificationsType
when ::WorkItems::Widgets::CurrentUserTodos
::Types::WorkItems::Widgets::CurrentUserTodosType
+ when ::WorkItems::Widgets::AwardEmoji
+ ::Types::WorkItems::Widgets::AwardEmojiType
else
raise "Unknown GraphQL type for widget #{object}"
end
diff --git a/app/graphql/types/work_items/widgets/award_emoji_type.rb b/app/graphql/types/work_items/widgets/award_emoji_type.rb
new file mode 100644
index 00000000000..421bb8f0e98
--- /dev/null
+++ b/app/graphql/types/work_items/widgets/award_emoji_type.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Types
+ module WorkItems
+ module Widgets
+ # Disabling widget level authorization as it might be too granular
+ # and we already authorize the parent work item
+ # rubocop:disable Graphql/AuthorizeTypes
+ class AwardEmojiType < BaseObject
+ graphql_name 'WorkItemWidgetAwardEmoji'
+ description 'Represents the award emoji widget'
+
+ implements Types::WorkItems::WidgetInterface
+
+ field :award_emoji,
+ ::Types::AwardEmojis::AwardEmojiType.connection_type,
+ null: true,
+ description: 'Award emoji on the work item.'
+ field :downvotes,
+ GraphQL::Types::Int,
+ null: false,
+ description: 'Number of downvotes the work item has received.'
+ field :upvotes,
+ GraphQL::Types::Int,
+ null: false,
+ description: 'Number of upvotes the work item has received.'
+
+ def downvotes
+ BatchLoaders::AwardEmojiVotesBatchLoader
+ .load_downvotes(object.work_item, awardable_class: 'Issue')
+ end
+
+ def upvotes
+ BatchLoaders::AwardEmojiVotesBatchLoader
+ .load_upvotes(object.work_item, awardable_class: 'Issue')
+ end
+ end
+ # rubocop:enable Graphql/AuthorizeTypes
+ end
+ end
+end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 179ce01ae44..fc2c927a2b1 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -12,8 +12,8 @@ module IssuablesHelper
end
end
- def sidebar_gutter_collapsed_class
- return "right-sidebar-expanded" if moved_mr_sidebar_enabled?
+ def sidebar_gutter_collapsed_class(is_merge_request_with_flag)
+ return "right-sidebar-expanded" if is_merge_request_with_flag
"right-sidebar-#{sidebar_gutter_collapsed? ? 'collapsed' : 'expanded'}"
end
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index e82f09a0a97..98c378db7d3 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -153,7 +153,7 @@ module IssuesHelper
issue.moved_from.project.service_desk_enabled? && !issue.project.service_desk_enabled?
end
- def issue_header_actions_data(project, issuable, current_user)
+ def issue_header_actions_data(project, issuable, current_user, issuable_sidebar)
new_issuable_params = { issue: {}, add_related_issue: issuable.iid }
if issuable.incident?
new_issuable_params[:issuable_template] = 'incident'
@@ -176,7 +176,8 @@ module IssuesHelper
report_abuse_path: add_category_abuse_reports_path,
reported_user_id: issuable.author.id,
reported_from_url: issue_url(issuable),
- submit_as_spam_path: mark_as_spam_project_issue_path(project, issuable)
+ submit_as_spam_path: mark_as_spam_project_issue_path(project, issuable),
+ issuable_email_address: issuable_sidebar.nil? ? '' : issuable_sidebar[:create_note_email]
}
end
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index a36d61378a0..5cacc8e2199 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -179,6 +179,10 @@ module MergeRequestsHelper
end
end
+ def moved_mr_sidebar_enabled?
+ Feature.enabled?(:moved_mr_sidebar, @project)
+ end
+
def diffs_tab_pane_data(project, merge_request, params)
{
"is-locked": merge_request.discussion_locked?,
@@ -256,10 +260,6 @@ module MergeRequestsHelper
_('%{author} requested to merge %{source_branch} %{copy_button} into %{target_branch} %{created_at}').html_safe % { author: link_to_author.html_safe, source_branch: merge_request_source_branch(merge_request).html_safe, copy_button: copy_button.html_safe, target_branch: target_branch.html_safe, created_at: time_ago_with_tooltip(merge_request.created_at, html_class: 'gl-display-inline-block').html_safe }
end
- def moved_mr_sidebar_enabled?
- Feature.enabled?(:moved_mr_sidebar, @project) && defined?(@merge_request)
- end
-
def sticky_header_data
data = {
iid: @merge_request.iid,
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index 59ffe6a183e..bd8627b9530 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -38,7 +38,7 @@ module NavHelper
end
def page_gutter_class
- moved_sidebar_enabled = current_controller?('merge_requests') && moved_mr_sidebar_enabled?
+ moved_sidebar_enabled = @is_merge_request_with_flag
if (page_has_markdown? || current_path?('projects/merge_requests#diffs')) && !current_controller?('conflicts')
if cookies[:collapsed_gutter] == 'true'
diff --git a/app/models/note.rb b/app/models/note.rb
index b9b884b88c5..13fff9520b7 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -173,6 +173,14 @@ class Note < ApplicationRecord
end
scope :with_metadata, -> { includes(:system_note_metadata) }
+ scope :without_hidden, -> {
+ if Feature.enabled?(:hidden_notes)
+ where_not_exists(Users::BannedUser.where('notes.author_id = banned_users.user_id'))
+ else
+ all
+ end
+ }
+
scope :for_note_or_capitalized_note, ->(text) { where(note: [text, text.capitalize]) }
scope :like_note_or_capitalized_note, ->(text) { where('(note LIKE ? OR note LIKE ?)', text, text.capitalize) }
diff --git a/app/models/work_items/widget_definition.rb b/app/models/work_items/widget_definition.rb
index 9bfdb25ce47..763b1a79069 100644
--- a/app/models/work_items/widget_definition.rb
+++ b/app/models/work_items/widget_definition.rb
@@ -30,7 +30,8 @@ module WorkItems
requirement_legacy: 12, # EE-only
test_reports: 13, # EE-only
notifications: 14,
- current_user_todos: 15
+ current_user_todos: 15,
+ award_emoji: 16
}
def self.available_widgets
diff --git a/app/models/work_items/widgets/award_emoji.rb b/app/models/work_items/widgets/award_emoji.rb
new file mode 100644
index 00000000000..3c862d7c267
--- /dev/null
+++ b/app/models/work_items/widgets/award_emoji.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module WorkItems
+ module Widgets
+ class AwardEmoji < Base
+ delegate :award_emoji, :downvotes, :upvotes, to: :work_item
+ end
+ end
+end
diff --git a/app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml b/app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml
index 92b0a5a0b90..9b6263f2145 100644
--- a/app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml
+++ b/app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml
@@ -1,7 +1,8 @@
- display_issuable_type = issuable_display_type(@merge_request)
.btn-group.gl-md-ml-3.gl-display-flex.dropdown.gl-dropdown.gl-md-w-auto.gl-w-full
- = button_tag type: 'button', class: "btn dropdown-toggle btn-default btn-md gl-button gl-dropdown-toggle btn-default-tertiary dropdown-icon-only dropdown-toggle-no-caret has-tooltip gl-display-none! gl-md-display-inline-flex!", data: { toggle: 'dropdown', title: _('Merge request actions'), testid: 'merge-request-actions', 'aria-label': _('Merge request actions') } do
+ %span.js-sidebar-header-popover
+ = button_tag type: 'button', id: "new-actions-header-dropdown", class: "btn dropdown-toggle btn-default btn-md gl-button gl-dropdown-toggle btn-default-tertiary dropdown-icon-only dropdown-toggle-no-caret has-tooltip gl-display-none! gl-md-display-inline-flex! gl-rounded-base!", data: { toggle: 'dropdown', title: _('Merge request actions'), testid: 'merge-request-actions', 'aria-label': _('Merge request actions') } do
= sprite_icon "ellipsis_v", size: 16, css_class: "dropdown-icon gl-icon"
= button_tag type: 'button', class: "btn dropdown-toggle btn-default btn-md btn-block gl-button gl-dropdown-toggle gl-md-display-none!", data: { 'toggle' => 'dropdown' } do
%span.gl-dropdown-button-text= _('Merge request actions')
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index f54354674e2..82e95a6a8e8 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -9,14 +9,15 @@
- reviewers = local_assigns.fetch(:reviewers, nil)
- in_group_context_with_iterations = @project.group.present? && issuable_sidebar[:supports_iterations]
- is_merge_request = issuable_type === 'merge_request'
-- moved_sidebar_enabled = moved_mr_sidebar_enabled? && is_merge_request
+- moved_sidebar_enabled = moved_mr_sidebar_enabled?
+- is_merge_request_with_flag = is_merge_request && moved_sidebar_enabled
-%aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { signed: { in: signed_in }, issuable_type: issuable_type }, class: "#{sidebar_gutter_collapsed_class} #{'right-sidebar-merge-requests' if moved_sidebar_enabled}", 'aria-live' => 'polite', 'aria-label': issuable_type }
- .issuable-sidebar{ class: "#{'is-merge-request' if moved_sidebar_enabled}" }
- .issuable-sidebar-header{ class: "#{'gl-pb-2! gl-md-display-flex gl-justify-content-end gl-lg-display-none!' if moved_sidebar_enabled}" }
+%aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { signed: { in: signed_in }, issuable_type: issuable_type }, class: "#{sidebar_gutter_collapsed_class(is_merge_request_with_flag)} #{'right-sidebar-merge-requests' if is_merge_request_with_flag}", 'aria-live' => 'polite', 'aria-label': issuable_type }
+ .issuable-sidebar{ class: "#{'is-merge-request' if is_merge_request_with_flag}" }
+ .issuable-sidebar-header{ class: "#{'gl-pb-2! gl-md-display-flex gl-justify-content-end gl-lg-display-none!' if is_merge_request_with_flag}" }
%button.btn.gl-button.gutter-toggle.float-right.js-sidebar-toggle.has-tooltip{ type: "reset", class: "gl-shadow-none! #{'gl-display-block' if moved_sidebar_enabled}", "aria-label" => _('Toggle sidebar'), title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left', boundary: 'viewport' } }
= sidebar_gutter_toggle_icon
- - if signed_in && !moved_sidebar_enabled
+ - if signed_in && !is_merge_request_with_flag
.js-sidebar-todo-widget-root{ data: { project_path: issuable_sidebar[:project_full_path], iid: issuable_sidebar[:iid], id: issuable_sidebar[:id] } }
= form_for issuable_type, url: issuable_sidebar[:issuable_json_path], remote: true, html: { class: 'issuable-context-form inline-update js-issuable-update' } do |f|
@@ -81,17 +82,17 @@
.js-sidebar-participants-widget-root
- .block.with-sub-blocks
- - if !moved_sidebar_enabled
+ - if !moved_sidebar_enabled
+ .block.with-sub-blocks
.js-sidebar-reference-widget-root
- - if issuable_type == 'merge_request' && !moved_sidebar_enabled
- .sub-block.js-sidebar-source-branch
- .sidebar-collapsed-icon.js-dont-change-state
- = clipboard_button(text: source_branch, title: _('Copy branch name'), placement: "left", boundary: 'viewport', class: 'btn-clipboard gl-button btn-default-tertiary btn-icon btn-sm js-source-branch-copy')
- .gl-display-flex.gl-align-items-center.gl-justify-content-space-between.gl-mb-2.hide-collapsed
- %span.gl-overflow-hidden.gl-text-overflow-ellipsis.gl-white-space-nowrap
- = _('Source branch: %{source_branch_open}%{source_branch}%{source_branch_close}').html_safe % { source_branch_open: "<span class='gl-font-monospace' data-testid='ref-name' title='#{html_escape(source_branch)}'>".html_safe, source_branch_close: "</span>".html_safe, source_branch: html_escape(source_branch) }
- = clipboard_button(text: source_branch, title: _('Copy branch name'), placement: "left", boundary: 'viewport', class: 'btn-clipboard gl-button btn-default-tertiary btn-icon btn-sm js-source-branch-copy')
+ - if is_merge_request && !moved_sidebar_enabled
+ .sub-block.js-sidebar-source-branch
+ .sidebar-collapsed-icon.js-dont-change-state
+ = clipboard_button(text: source_branch, title: _('Copy branch name'), placement: "left", boundary: 'viewport', class: 'btn-clipboard gl-button btn-default-tertiary btn-icon btn-sm js-source-branch-copy')
+ .gl-display-flex.gl-align-items-center.gl-justify-content-space-between.gl-mb-2.hide-collapsed
+ %span.gl-overflow-hidden.gl-text-overflow-ellipsis.gl-white-space-nowrap
+ = _('Source branch: %{source_branch_open}%{source_branch}%{source_branch_close}').html_safe % { source_branch_open: "<span class='gl-font-monospace' data-testid='ref-name' title='#{html_escape(source_branch)}'>".html_safe, source_branch_close: "</span>".html_safe, source_branch: html_escape(source_branch) }
+ = clipboard_button(text: source_branch, title: _('Copy branch name'), placement: "left", boundary: 'viewport', class: 'btn-clipboard gl-button btn-default-tertiary btn-icon btn-sm js-source-branch-copy')
- if show_forwarding_email
.block
diff --git a/app/views/shared/issue_type/_details_header.html.haml b/app/views/shared/issue_type/_details_header.html.haml
index ccb501dae11..0326f35a12b 100644
--- a/app/views/shared/issue_type/_details_header.html.haml
+++ b/app/views/shared/issue_type/_details_header.html.haml
@@ -19,4 +19,4 @@
%a.btn.gl-button.btn-default.btn-icon.float-right.gl-display-block.d-sm-none.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ href: "#" }
= sprite_icon('chevron-double-lg-left')
- .js-issue-header-actions{ data: issue_header_actions_data(@project, issuable, current_user) }
+ .js-issue-header-actions{ data: issue_header_actions_data(@project, issuable, current_user, @issuable_sidebar) }
diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml
index cc1965945ac..5477b9395ea 100644
--- a/app/views/shared/milestones/_sidebar.html.haml
+++ b/app/views/shared/milestones/_sidebar.html.haml
@@ -1,7 +1,7 @@
- affix_offset = local_assigns.fetch(:affix_offset, "50")
- project = local_assigns[:project]
-%aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => affix_offset, "spy" => "affix", "always-show-toggle" => true }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite', 'aria-label': _('Milestone') }
+%aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => affix_offset, "spy" => "affix", "always-show-toggle" => true }, class: sidebar_gutter_collapsed_class(false), 'aria-live' => 'polite', 'aria-label': _('Milestone') }
.issuable-sidebar.milestone-sidebar
.block.milestone-progress.issuable-sidebar-header
%a.gutter-toggle.float-right.js-sidebar-toggle.has-tooltip{ role: "button", href: "#", "aria-label" => s_('MilestoneSidebar|Toggle sidebar'), title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left', boundary: 'viewport' } }
diff --git a/config/feature_flags/development/hidden_notes.yml b/config/feature_flags/development/hidden_notes.yml
new file mode 100644
index 00000000000..239234d6960
--- /dev/null
+++ b/config/feature_flags/development/hidden_notes.yml
@@ -0,0 +1,8 @@
+---
+name: hidden_notes
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/112973
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/405148
+milestone: '15.11'
+type: development
+group: group::anti-abuse
+default_enabled: false
diff --git a/db/migrate/20230323101138_add_award_emoji_work_item_widget.rb b/db/migrate/20230323101138_add_award_emoji_work_item_widget.rb
new file mode 100644
index 00000000000..6a6b50c81a5
--- /dev/null
+++ b/db/migrate/20230323101138_add_award_emoji_work_item_widget.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+class AddAwardEmojiWorkItemWidget < Gitlab::Database::Migration[2.1]
+ class WorkItemType < MigrationRecord
+ self.table_name = 'work_item_types'
+ end
+
+ class WidgetDefinition < MigrationRecord
+ self.table_name = 'work_item_widget_definitions'
+ end
+
+ restrict_gitlab_migration gitlab_schema: :gitlab_main
+ disable_ddl_transaction!
+
+ WIDGET_NAME = 'Award emoji'
+ WIDGET_ENUM_VALUE = 16
+ WORK_ITEM_TYPES = [
+ 'Issue',
+ 'Incident',
+ 'Test Case',
+ 'Requirement',
+ 'Task',
+ 'Objective',
+ 'Key Result'
+ ].freeze
+
+ def up
+ widgets = []
+
+ WORK_ITEM_TYPES.each do |type_name|
+ type = WorkItemType.find_by_name_and_namespace_id(type_name, nil)
+
+ unless type
+ Gitlab::AppLogger.warn("type #{type_name} is missing, not adding widget")
+
+ next
+ end
+
+ widgets << {
+ work_item_type_id: type.id,
+ name: WIDGET_NAME,
+ widget_type: WIDGET_ENUM_VALUE
+ }
+ end
+
+ return if widgets.empty?
+
+ WidgetDefinition.upsert_all(
+ widgets,
+ unique_by: :index_work_item_widget_definitions_on_default_witype_and_name
+ )
+ end
+
+ def down
+ WidgetDefinition.where(name: WIDGET_NAME).delete_all
+ end
+end
diff --git a/db/post_migrate/20230328100534_truncate_error_tracking_tables.rb b/db/post_migrate/20230328100534_truncate_error_tracking_tables.rb
new file mode 100644
index 00000000000..3b263303795
--- /dev/null
+++ b/db/post_migrate/20230328100534_truncate_error_tracking_tables.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class TruncateErrorTrackingTables < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ def up
+ # Only truncate tables on Gitlab.com environments.
+ # TRUNCATE is a DDL statement (it drops the table and re-creates it), so we want to run the
+ # migration in DDL mode, but we also don't want to execute it against all schemas because
+ # it's considered a write operation. So, we'll manually check and skip the migration if
+ # it's on not `:gitlab_main`.
+ return unless Gitlab.com? && Gitlab::Database.gitlab_schemas_for_connection(connection).include?(:gitlab_main)
+
+ execute('TRUNCATE table error_tracking_errors CASCADE')
+ end
+
+ def down
+ # noop
+ end
+end
diff --git a/db/schema_migrations/20230323101138 b/db/schema_migrations/20230323101138
new file mode 100644
index 00000000000..2c464eda729
--- /dev/null
+++ b/db/schema_migrations/20230323101138
@@ -0,0 +1 @@
+3d03a0af6421f0b2a0f26b7a7d385dcabb12565cb0b3f6d23454d8588395a59a \ No newline at end of file
diff --git a/db/schema_migrations/20230328100534 b/db/schema_migrations/20230328100534
new file mode 100644
index 00000000000..7bdf4ba65ab
--- /dev/null
+++ b/db/schema_migrations/20230328100534
@@ -0,0 +1 @@
+f74849f237902c24b996b27c552a95b8a3c080f98c03fcefdf8a7c76638396dc \ No newline at end of file
diff --git a/doc/administration/gitaly/configure_gitaly.md b/doc/administration/gitaly/configure_gitaly.md
index b3c36513c0d..af3d714d078 100644
--- a/doc/administration/gitaly/configure_gitaly.md
+++ b/doc/administration/gitaly/configure_gitaly.md
@@ -1203,7 +1203,8 @@ automatically keep parts of the pack-objects cache files in RAM,
making it faster.
Because the pack-objects cache can lead to a significant increase in
-disk write IO, it is off by default.
+disk write IO, it is off by default. In GitLab 15.11 and later,
+the write workload is approximately 50% lower, but the cache is still disabled by default.
### Configure the cache
@@ -1215,6 +1216,7 @@ below.
| `enabled` | `false` | Turns on the cache. When off, Gitaly runs a dedicated `git pack-objects` process for each request. |
| `dir` | `<PATH TO FIRST STORAGE>/+gitaly/PackObjectsCache` | Local directory where cache files get stored. |
| `max_age` | `5m` (5 minutes) | Cache entries older than this get evicted and removed from disk. |
+| `min_occurrences` | 1 | Minimum times a key must occur before a cache entry is created. |
In `/etc/gitlab/gitlab.rb`, set:
@@ -1226,6 +1228,7 @@ gitaly['configuration'] = {
enabled: true,
# dir: '/var/opt/gitlab/git-data/repositories/+gitaly/PackObjectsCache',
# max_age: '5m',
+ # min_occurrences: 1,
},
}
```
@@ -1283,13 +1286,31 @@ a guarantee. Peak size may exceed this average.
The `max_age` configuration setting lets you control the chance of a
cache hit and the average amount of storage used by cache files.
-Entries older than `max_age` get evicted from the in-memory metadata
-store, and deleted from disk.
+Entries older than `max_age` get deleted from the disk.
Eviction does not interfere with ongoing requests. It is OK for `max_age` to be less than the time it takes to do a
fetch over a slow connection because Unix filesystems do not truly delete a file until all processes that are reading
the deleted file have closed it.
+#### Minimum key occurrences `min_occurrences`
+
+> [Introduced](https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/2222) in GitLab 15.11.
+
+The `min_occurrences` setting controls how often an identical request
+must occur before we create a new cache entry. The default value is `1`,
+meaning that unique requests do not get written into the cache.
+
+If you:
+
+- Increase this number, your cache hit rate goes down and the
+cache uses less disk space.
+- Decrease this number, your cache hit
+rate goes up and the cache uses more disk space.
+
+You should set `min_occurrences` to `1`. On GitLab.com,
+going from 0 to 1 saved us 50% cache disk space while barely affecting
+the cache hit rate.
+
### Observe the cache
The cache can be observed [using metrics](monitoring.md#pack-objects-cache) and in the following logged
diff --git a/doc/administration/package_information/supported_os.md b/doc/administration/package_information/supported_os.md
index f92d57c0035..ea87dfb6e28 100644
--- a/doc/administration/package_information/supported_os.md
+++ b/doc/administration/package_information/supported_os.md
@@ -21,7 +21,6 @@ The following lists the currently supported OSs and their possible EOL dates.
| CentOS 7 | GitLab CE / GitLab EE 7.10.0 | x86_64 | [CentOS Install Documentation](https://about.gitlab.com/install/#centos-7) | June 2024 | <https://wiki.centos.org/About/Product> |
| Debian 10 | GitLab CE / GitLab EE 12.2.0 | amd64, arm64 | [Debian Install Documentation](https://about.gitlab.com/install/#debian) | 2024 | <https://wiki.debian.org/LTS> |
| Debian 11 | GitLab CE / GitLab EE 14.6.0 | amd64, arm64 | [Debian Install Documentation](https://about.gitlab.com/install/#debian) | 2026 | <https://wiki.debian.org/LTS> |
-| OpenSUSE 15.3 | GitLab CE / GitLab EE 14.5.0 | x86_64, aarch64 | [OpenSUSE Install Documentation](https://about.gitlab.com/install/#opensuse-leap) | Dec 2022 | <https://en.opensuse.org/Lifetime> |
| OpenSUSE 15.4 | GitLab CE / GitLab EE 15.7.0 | x86_64, aarch64 | [OpenSUSE Install Documentation](https://about.gitlab.com/install/#opensuse-leap) | Nov 2023 | <https://en.opensuse.org/Lifetime> |
| RHEL 8 | GitLab CE / GitLab EE 12.8.1 | x86_64, arm64 | [Use CentOS Install Documentation](https://about.gitlab.com/install/#centos-7) | May 2029 | [RHEL Details](https://access.redhat.com/support/policy/updates/errata/#Life_Cycle_Dates) |
| SLES 12 | GitLab EE 9.0.0 | x86_64 | [Use OpenSUSE Install Documentation](https://about.gitlab.com/install/#opensuse-leap) | Oct 2027 | <https://www.suse.com/lifecycle/> |
@@ -103,6 +102,7 @@ release for them can be found below:
| Ubuntu 16.04 | [April 2021](https://ubuntu.com/info/release-end-of-life) | [GitLab CE](https://packages.gitlab.com/app/gitlab/gitlab-ce/search?q=gitlab-ce_13.12&dist=ubuntu%2Fxenial) / [GitLab EE](https://packages.gitlab.com/app/gitlab/gitlab-ee/search?q=gitlab-ee_13.12&dist=ubuntu%2Fxenial) 13.12 |
| OpenSUSE 15.2 | [December 2021](https://en.opensuse.org/Lifetime#Discontinued_distributions) | [GitLab CE](https://packages.gitlab.com/app/gitlab/gitlab-ce/search?q=gitlab-ce-14.7&dist=opensuse%2F15.2) / [GitLab EE](https://packages.gitlab.com/app/gitlab/gitlab-ee/search?q=gitlab-ee-14.7&dist=opensuse%2F15.2) 14.7 |
| Debian 9 "Stretch" | [June 2022](https://lists.debian.org/debian-lts-announce/2022/07/msg00002.html) | [GitLab CE](https://packages.gitlab.com/app/gitlab/gitlab-ce/search?q=gitlab-ce_15.2&dist=debian%2Fstretch) / [GitLab EE](https://packages.gitlab.com/app/gitlab/gitlab-ee/search?q=gitlab-ee_15.2&dist=debian%2Fstretch) 15.2 |
+| OpenSUSE 15.3 | [December 2022](https://en.opensuse.org/Lifetime#Discontinued_distributions) | [GitLab CE](https://packages.gitlab.com/app/gitlab/gitlab-ce/search?q=gitlab-ce-15.10&dist=opensuse%2F15.3) / [GitLab EE](https://packages.gitlab.com/app/gitlab/gitlab-ee/search?q=gitlab-ee-15.10&dist=opensuse%2F15.3) 15.10 |
NOTE:
An exception to this deprecation policy is when we are unable to provide
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 2e945f5ba66..f030f6169dd 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -22502,6 +22502,19 @@ Represents an assignees widget.
| <a id="workitemwidgetassigneescaninvitemembers"></a>`canInviteMembers` | [`Boolean!`](#boolean) | Indicates whether the current user can invite members to the work item's project. |
| <a id="workitemwidgetassigneestype"></a>`type` | [`WorkItemWidgetType`](#workitemwidgettype) | Widget type. |
+### `WorkItemWidgetAwardEmoji`
+
+Represents the award emoji widget.
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="workitemwidgetawardemojiawardemoji"></a>`awardEmoji` | [`AwardEmojiConnection`](#awardemojiconnection) | Award emoji on the work item. (see [Connections](#connections)) |
+| <a id="workitemwidgetawardemojidownvotes"></a>`downvotes` | [`Int!`](#int) | Number of downvotes the work item has received. |
+| <a id="workitemwidgetawardemojitype"></a>`type` | [`WorkItemWidgetType`](#workitemwidgettype) | Widget type. |
+| <a id="workitemwidgetawardemojiupvotes"></a>`upvotes` | [`Int!`](#int) | Number of upvotes the work item has received. |
+
### `WorkItemWidgetCurrentUserTodos`
Represents a todos widget.
@@ -25008,6 +25021,7 @@ Type of a work item widget.
| Value | Description |
| ----- | ----------- |
| <a id="workitemwidgettypeassignees"></a>`ASSIGNEES` | Assignees widget. |
+| <a id="workitemwidgettypeaward_emoji"></a>`AWARD_EMOJI` | Award Emoji widget. |
| <a id="workitemwidgettypecurrent_user_todos"></a>`CURRENT_USER_TODOS` | Current User Todos widget. |
| <a id="workitemwidgettypedescription"></a>`DESCRIPTION` | Description widget. |
| <a id="workitemwidgettypehealth_status"></a>`HEALTH_STATUS` | Health Status widget. |
@@ -26381,6 +26395,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
Implementations:
- [`WorkItemWidgetAssignees`](#workitemwidgetassignees)
+- [`WorkItemWidgetAwardEmoji`](#workitemwidgetawardemoji)
- [`WorkItemWidgetCurrentUserTodos`](#workitemwidgetcurrentusertodos)
- [`WorkItemWidgetDescription`](#workitemwidgetdescription)
- [`WorkItemWidgetHealthStatus`](#workitemwidgethealthstatus)
diff --git a/doc/architecture/blueprints/ci_pipeline_components/index.md b/doc/architecture/blueprints/ci_pipeline_components/index.md
index f482262fa72..390690c0f35 100644
--- a/doc/architecture/blueprints/ci_pipeline_components/index.md
+++ b/doc/architecture/blueprints/ci_pipeline_components/index.md
@@ -100,6 +100,7 @@ identifying abstract concepts and are subject to changes as we refine the design
- **Catalog resource** is the single item displayed in the catalog. A components repository is a catalog resource.
- **Version** is a specific revision of catalog resource. It maps to the released tag in the project,
which allows components to be pinned to a specific revision.
+- **Steps** is a collection of instructions for how jobs can be executed.
## Definition of pipeline component
diff --git a/doc/ci/test_cases/index.md b/doc/ci/test_cases/index.md
index 8ad9e27de8b..edc962c9921 100644
--- a/doc/ci/test_cases/index.md
+++ b/doc/ci/test_cases/index.md
@@ -16,6 +16,12 @@ Test cases in GitLab can help your teams create testing scenarios in their exist
Now your Implementation and Testing teams can collaborate better, as they no longer have to
use external test planning tools, which require additional overhead, context switching, and expense.
+NOTE:
+[Requirements](../../user/project/requirements/index.md) and test cases are being
+[migrated to work items](https://gitlab.com/groups/gitlab-org/-/epics/5171).
+[Issue 323790](https://gitlab.com/gitlab-org/gitlab/-/issues/323790) proposes to link requirements to test cases.
+For more information, see [Product Stage Direction - Plan](https://about.gitlab.com/direction/plan/).
+
## Create a test case
Prerequisite:
diff --git a/doc/development/documentation/alpha_beta.md b/doc/development/documentation/alpha_beta.md
index 4b5c24d512f..47f9090adbc 100644
--- a/doc/development/documentation/alpha_beta.md
+++ b/doc/development/documentation/alpha_beta.md
@@ -18,26 +18,32 @@ When you document a feature in one of these three statuses:
These features are usually behind a feature flag, which follow [these documentation guidelines](feature_flags.md).
If you add details of how users should enroll, or how to contact the team with issues,
-the `FLAG:` note should be above these details. For example:
+the `FLAG:` note should be above these details.
+
+For example:
```markdown
## Great new feature (Alpha)
+> [Introduced](link) in GitLab 15.10. This feature is in [Alpha](<link_to>/policy/alpha-beta-support.md).
+
FLAG:
On self-managed GitLab, by default this feature is not available.
-To make it available, ask an administrator to enable the feature flag named example_flag.
+To make it available, ask an administrator to enable the feature flag named `example_flag`.
On GitLab.com, this feature is not available. This feature is not ready for production use.
Use this great new feature when you need to do this new thing.
-This feature is in Alpha. To join the list of users testing this feature,
-do this thing. If you find a bug, [open an issue](link).
+This feature is in [Alpha](<link_to>/policy/alpha-beta-support.md). To join
+the list of users testing this feature, do this thing. If you find a bug,
+[open an issue](link).
```
-When the feature is released, remove:
+When the feature is ready for production, remove:
- The text in parentheses.
-- Any language about the feature not being ready for production.
-- The feature flag information.
+- Any language about the feature not being ready for production in the body
+ description.
+- The feature flag information if available.
-Ensure the version history is up-to-date.
+Ensure the version history is up-to-date by adding a note about the production release.
diff --git a/doc/development/pipelines/index.md b/doc/development/pipelines/index.md
index c19580203e6..87c7345491d 100644
--- a/doc/development/pipelines/index.md
+++ b/doc/development/pipelines/index.md
@@ -54,9 +54,9 @@ In summary:
To identify the RSpec tests that are likely to fail in a merge request, we use the [`test_file_finder` gem](https://gitlab.com/gitlab-org/ci-cd/test_file_finder), with two strategies:
- dynamic mapping from test coverage tracing (generated via the [`Crystalball` gem](https://github.com/toptal/crystalball))
- ([see where it's used](https://gitlab.com/gitlab-org/gitlab/-/blob/47d507c93779675d73a05002e2ec9c3c467cd698/tooling/bin/find_tests#L15))
+ ([see where it's used](https://gitlab.com/gitlab-org/gitlab/-/blob/2e19d43ba0d456808916650088c0f70d905e7810/tooling/lib/tooling/find_tests.rb#L20))
- static mapping maintained in the [`tests.yml` file](https://gitlab.com/gitlab-org/gitlab/-/blob/master/tests.yml) for special cases that cannot
- be mapped via coverage tracing ([see where it's used](https://gitlab.com/gitlab-org/gitlab/-/blob/47d507c93779675d73a05002e2ec9c3c467cd698/tooling/bin/find_tests#L12))
+ be mapped via coverage tracing ([see where it's used](https://gitlab.com/gitlab-org/gitlab/-/blob/2e19d43ba0d456808916650088c0f70d905e7810/tooling/lib/tooling/find_tests.rb#L20))
The test mappings contain a map of each source files to a list of test files which is dependent of the source file.
diff --git a/doc/install/installation.md b/doc/install/installation.md
index b14ff89c804..888f28b8318 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -218,18 +218,7 @@ below to use a system Ruby.
Linux distributions generally have older versions of Ruby available, so these
instructions are designed to install Ruby from the official source code.
-Download Ruby and compile it:
-
-```shell
-mkdir /tmp/ruby && cd /tmp/ruby
-curl --remote-name --location --progress-bar "https://cache.ruby-lang.org/pub/ruby/3.0/ruby-3.0.5.tar.gz"
-echo '9afc6380a027a4fe1ae1a3e2eccb6b497b9c5ac0631c12ca56f9b7beb4848776 ruby-3.0.5.tar.gz' | sha256sum -c - && tar xzf ruby-3.0.5.tar.gz
-cd ruby-3.0.5
-
-./configure --disable-install-rdoc --enable-shared
-make
-sudo make install
-```
+[Install Ruby](https://www.ruby-lang.org/en/documentation/installation/).
## 3. Go
diff --git a/doc/operations/incident_management/manage_incidents.md b/doc/operations/incident_management/manage_incidents.md
index 338dacda166..9d0c8075ff9 100644
--- a/doc/operations/incident_management/manage_incidents.md
+++ b/doc/operations/incident_management/manage_incidents.md
@@ -226,6 +226,10 @@ When you close an incident that is linked to an [alert](alerts.md),
the linked alert's status changes to **Resolved**.
You are then credited with the alert's status change.
+<!-- Delete when the `moved_mr_sidebar` feature flag is removed -->
+If you don't see this action at the top of an incident, your project or instance might have
+enabled a feature flag for [moved actions](../../user/project/merge_requests/index.md#move-sidebar-actions)
+
### Automatically close incidents via recovery alerts
> [Introduced for HTTP integrations](https://gitlab.com/gitlab-org/gitlab/-/issues/13402) in GitLab 13.4.
@@ -249,6 +253,22 @@ When GitLab receives a recovery alert, it closes the associated incident.
This action is recorded as a system note on the incident indicating that it
was closed automatically by the GitLab Alert bot.
+## Delete an incident
+
+Prerequisites:
+
+- You must have the Owner role for a project.
+
+To delete an incident:
+
+1. In an incident, select **Incident actions** (**{ellipsis_v}**).
+1. Select **Delete incident**.
+
+Alternatively:
+
+1. In an incident, select **Edit title and description** (**{pencil}**).
+1. Select **Delete incident**.
+
## Other actions
Because incidents in GitLab are built on top of [issues](../../user/project/issues/index.md),
diff --git a/doc/update/upgrading_from_source.md b/doc/update/upgrading_from_source.md
index b4dae3247ac..16bab1eb690 100644
--- a/doc/update/upgrading_from_source.md
+++ b/doc/update/upgrading_from_source.md
@@ -58,21 +58,10 @@ sudo service gitlab stop
### 3. Update Ruby
-From GitLab 15.10, we only support Ruby 3.0 or higher and dropped support for Ruby 2.7. Be sure to upgrade if necessary.
+From GitLab 15.10, we only support Ruby 3.0.x and dropped support for Ruby 2.7. Be sure to upgrade if necessary.
You can check which version you are running with `ruby -v`.
-Download Ruby and compile it:
-
-```shell
-mkdir /tmp/ruby && cd /tmp/ruby
-curl --remote-name --location --progress-bar "https://cache.ruby-lang.org/pub/ruby/3.0/ruby-3.0.5.tar.gz"
-echo '9afc6380a027a4fe1ae1a3e2eccb6b497b9c5ac0631c12ca56f9b7beb4848776 ruby-3.0.5.tar.gz' | sha256sum -c - && tar xzf ruby-3.0.5.tar.gz
-cd ruby-2.7.6
-
-./configure --disable-install-rdoc --enable-shared
-make
-sudo make install
-```
+[Install Ruby](https://www.ruby-lang.org/en/documentation/installation/).
### 4. Update Node.js
diff --git a/doc/user/group/epics/img/button_close_epic.png b/doc/user/group/epics/img/button_close_epic.png
deleted file mode 100644
index aa1a889ea23..00000000000
--- a/doc/user/group/epics/img/button_close_epic.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/group/epics/manage_epics.md b/doc/user/group/epics/manage_epics.md
index 0dc87b7e4e4..1349d28c6a1 100644
--- a/doc/user/group/epics/manage_epics.md
+++ b/doc/user/group/epics/manage_epics.md
@@ -159,14 +159,13 @@ Prerequisites:
- You must have at least the Reporter role for the epic's group.
-Whenever you decide that there is no longer need for that epic,
-close the epic by:
+To close an epic, at the top of an epic, select **Close epic**.
-- Selecting **Close epic**.
+<!-- Delete when the `moved_mr_sidebar` feature flag is removed -->
+If you don't see this action at the top of an epic, your project or instance might have
+enabled a feature flag for [moved actions](../../project/merge_requests/index.md#move-sidebar-actions)
- ![close epic - button](img/button_close_epic.png)
-
-- Using the `/close` [quick action](../../project/quick_actions.md).
+You can also use the `/close` [quick action](../../project/quick_actions.md).
## Reopen a closed epic
diff --git a/doc/user/profile/notifications.md b/doc/user/profile/notifications.md
index c0c967a3f18..89366c73b16 100644
--- a/doc/user/profile/notifications.md
+++ b/doc/user/profile/notifications.md
@@ -239,7 +239,7 @@ Turning this toggle off only unsubscribes you from updates related to this issue
Learn how to [opt out of all emails from GitLab](#opt-out-of-all-gitlab-emails).
<!-- Delete when the `moved_mr_sidebar` feature flag is removed -->
-If you don't see this action on the right sidebar, your project or instance may have
+If you don't see this action on the right sidebar, your project or instance might have
enabled a feature flag for [moved sidebar actions](../project/merge_requests/index.md#move-sidebar-actions).
### Notification events on issues, merge requests, and epics
diff --git a/doc/user/project/issues/create_issues.md b/doc/user/project/issues/create_issues.md
index b6931149ede..4511c89b0ff 100644
--- a/doc/user/project/issues/create_issues.md
+++ b/doc/user/project/issues/create_issues.md
@@ -78,7 +78,7 @@ Prerequisites:
To create an issue from another issue:
-1. In an existing issue, select the vertical ellipsis (**{ellipsis_v}**).
+1. In an existing issue, select **Issue actions** (**{ellipsis_v}**).
1. Select **New related issue**.
1. Complete the [fields](#fields-in-the-new-issue-form).
The new issue form has a **Relate to issue #123** checkbox, where `123` is the ID of the
diff --git a/doc/user/project/issues/managing_issues.md b/doc/user/project/issues/managing_issues.md
index 069bc4582c6..b532fd0c5b8 100644
--- a/doc/user/project/issues/managing_issues.md
+++ b/doc/user/project/issues/managing_issues.md
@@ -209,6 +209,10 @@ To close an issue, you can do the following:
- At the top of the issue, select **Close issue**.
- In an [issue board](../issue_board.md), drag an issue card from its list into the **Closed** list.
+<!-- Delete when the `moved_mr_sidebar` feature flag is removed -->
+If you don't see this action at the top of an issue, your project or instance might have
+enabled a feature flag for [moved actions](../merge_requests/index.md#move-sidebar-actions).
+
### Reopen a closed issue
Prerequisites:
@@ -344,7 +348,7 @@ Prerequisites:
To delete an issue:
-1. In an issue, select the vertical ellipsis (**{ellipsis_v}**).
+1. In an issue, select **Issue actions** (**{ellipsis_v}**).
1. Select **Delete issue**.
Alternatively:
@@ -362,7 +366,7 @@ You can promote an issue to an [epic](../../group/epics/index.md) in the immedia
To promote an issue to an epic:
-1. In an issue, select the vertical ellipsis (**{ellipsis_v}**).
+1. In an issue, select **Issue actions** (**{ellipsis_v}**).
1. Select **Promote to epic**.
Alternatively, you can use the `/promote` [quick action](../quick_actions.md#issues-merge-requests-and-epics).
@@ -472,6 +476,10 @@ You can now paste the reference into another description or comment.
Read more about issue references in [GitLab-Flavored Markdown](../../markdown.md#gitlab-specific-references).
+<!-- Delete when the `moved_mr_sidebar` feature flag is removed -->
+If you don't see this action on the right sidebar, your project or instance might have
+enabled a feature flag for [moved actions](../merge_requests/index.md#move-sidebar-actions).
+
## Copy issue email address
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/18816) in GitLab 13.8.
diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md
index ee7f4e5dfed..6bcf3f15685 100644
--- a/doc/user/project/merge_requests/index.md
+++ b/doc/user/project/merge_requests/index.md
@@ -251,7 +251,8 @@ after merging does not retarget open merge requests. This improvement is
<!-- When the `moved_mr_sidebar` feature flag is removed, delete this topic and update the steps for these actions
like in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/87727/diffs?diff_id=522279685#5d9afba799c4af9920dab533571d7abb8b9e9163 -->
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85584) in GitLab 14.10 [with a flag](../../../administration/feature_flags.md) named `moved_mr_sidebar`. Disabled by default.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85584) in GitLab 14.10 [with a flag](../../../administration/feature_flags.md) named `moved_mr_sidebar`. Disabled by default.
+> - [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/373757) to also move actions on issues, incidents, and epics in GitLab 15.10.
FLAG:
On self-managed GitLab, by default this feature is not available. To make it available per project or for your entire instance, ask an administrator to [enable the feature flag](../../../administration/feature_flags.md) named `moved_mr_sidebar`.
@@ -266,6 +267,8 @@ When this feature flag is enabled, in the upper-right corner,
- [Lock discussion](../../discussions/index.md#prevent-comments-by-locking-the-discussion)
- Copy reference
+In GitLab 15.10 and later, similar action menus are available on issues, incidents, and epics.
+
When this feature flag is disabled, these actions are in the right sidebar.
## Merge request workflows
diff --git a/doc/user/project/repository/code_suggestions.md b/doc/user/project/repository/code_suggestions.md
index f9f6b2f75fd..cfc8f0a1040 100644
--- a/doc/user/project/repository/code_suggestions.md
+++ b/doc/user/project/repository/code_suggestions.md
@@ -23,7 +23,7 @@ To accept a suggestion, press <kbd>Tab</kbd>.
Code Suggestions are supported in Visual Studio Code with the GitLab Workflow extension. GitLab plans to support the [new GitLab WebIDE in an upcoming release](../web_ide_beta/index.md) in the future.
-The best results from Code Suggestions are expected for these six languages:
+Code Suggestions may produce [low-quality or incomplete suggestions](#model-accuracy-and-quality). Beta users should read about the [known limitations](#known-limitations). The best results from Code Suggestions are expected for these six languages:
- C
- C++
diff --git a/doc/user/project/requirements/index.md b/doc/user/project/requirements/index.md
index d385daa4fa1..b623be20b3a 100644
--- a/doc/user/project/requirements/index.md
+++ b/doc/user/project/requirements/index.md
@@ -24,6 +24,12 @@ If an industry standard *requires* that your application has a certain feature o
[create a requirement](#create-a-requirement) to reflect this.
When a feature is no longer necessary, you can [archive the related requirement](#archive-a-requirement).
+NOTE:
+Requirements and [test cases](../../../ci/test_cases/index.md) are being
+[migrated to work items](https://gitlab.com/groups/gitlab-org/-/epics/5171).
+[Issue 323790](https://gitlab.com/gitlab-org/gitlab/-/issues/323790) proposes to link requirements to test cases.
+For more information, see [Product Stage Direction - Plan](https://about.gitlab.com/direction/plan/).
+
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
For an overview, see [GitLab 12.10 Introduces Requirements Management](https://www.youtube.com/watch?v=uSS7oUNSEoU).
diff --git a/doc/user/report_abuse.md b/doc/user/report_abuse.md
index de2b82c28d3..c4b9af28220 100644
--- a/doc/user/report_abuse.md
+++ b/doc/user/report_abuse.md
@@ -50,7 +50,7 @@ A URL to the reported user's comment is pre-filled in the abuse report's
## Report abuse from an issue
-1. On the issue, in the upper-right corner, select the vertical ellipsis (**{ellipsis_v}**).
+1. On the issue, in the upper-right corner, select **Issue actions** (**{ellipsis_v}**).
1. Select **Report abuse to administrator**.
1. Select a reason for reporting the user.
1. Complete an abuse report.
@@ -58,7 +58,7 @@ A URL to the reported user's comment is pre-filled in the abuse report's
## Report abuse from a merge request
-1. On the merge request, in the upper-right corner, select the vertical ellipsis (**{ellipsis_v}**).
+1. On the merge request, in the upper-right corner, select **Merge request actions** (**{ellipsis_v}**).
1. Select **Report abuse to administrator**.
1. Select a reason for reporting this user.
1. Complete an abuse report.
diff --git a/lib/gitlab/ci/pipeline/seed/build.rb b/lib/gitlab/ci/pipeline/seed/build.rb
index 484e18c6979..98f488d0f38 100644
--- a/lib/gitlab/ci/pipeline/seed/build.rb
+++ b/lib/gitlab/ci/pipeline/seed/build.rb
@@ -123,6 +123,7 @@ module Gitlab
end
@needs_attributes.flat_map do |need|
+ # We ignore the optional needed job in case it is excluded from the pipeline due to the job's rules.
next if need[:optional]
result = need_present?(need)
diff --git a/lib/gitlab/ci/yaml_processor.rb b/lib/gitlab/ci/yaml_processor.rb
index 255bb4dc0dc..0f9e7daf4b8 100644
--- a/lib/gitlab/ci/yaml_processor.rb
+++ b/lib/gitlab/ci/yaml_processor.rb
@@ -111,7 +111,9 @@ module Gitlab
def validate_job_dependency!(name, dependency, dependency_type = 'dependency', optional: false)
unless @jobs[dependency.to_sym]
- # Here, we ignore the optional needed job if it is not included in the result YAML.
+ # Here, we ignore the optional needed job if it is not in the result YAML due to the `include`
+ # rules. In `lib/gitlab/ci/pipeline/seed/build.rb`, we use `optional` again to ignore the
+ # optional needed job in case it is excluded from the pipeline due to the job's rules.
return if optional
error!("#{name} job: undefined #{dependency_type}: #{dependency}")
diff --git a/lib/gitlab/database_importers/work_items/base_type_importer.rb b/lib/gitlab/database_importers/work_items/base_type_importer.rb
index c6c45ca4d05..9e5d43f1767 100644
--- a/lib/gitlab/database_importers/work_items/base_type_importer.rb
+++ b/lib/gitlab/database_importers/work_items/base_type_importer.rb
@@ -20,7 +20,8 @@ module Gitlab
requirement_legacy: 'Requirement legacy',
test_reports: 'Test reports',
notifications: 'Notifications',
- current_user_todos: "Current user todos"
+ current_user_todos: "Current user todos",
+ award_emoji: 'Award emoji'
}.freeze
WIDGETS_FOR_TYPE = {
@@ -36,20 +37,23 @@ module Gitlab
:weight,
:health_status,
:notifications,
- :current_user_todos
+ :current_user_todos,
+ :award_emoji
],
incident: [
:description,
:hierarchy,
:notes,
:notifications,
- :current_user_todos
+ :current_user_todos,
+ :award_emoji
],
test_case: [
:description,
:notes,
:notifications,
- :current_user_todos
+ :current_user_todos,
+ :award_emoji
],
requirement: [
:description,
@@ -58,7 +62,8 @@ module Gitlab
:requirement_legacy,
:test_reports,
:notifications,
- :current_user_todos
+ :current_user_todos,
+ :award_emoji
],
task: [
:assignees,
@@ -71,7 +76,8 @@ module Gitlab
:iteration,
:weight,
:notifications,
- :current_user_todos
+ :current_user_todos,
+ :award_emoji
],
objective: [
:assignees,
@@ -83,7 +89,8 @@ module Gitlab
:health_status,
:progress,
:notifications,
- :current_user_todos
+ :current_user_todos,
+ :award_emoji
],
key_result: [
:assignees,
@@ -95,7 +102,8 @@ module Gitlab
:health_status,
:progress,
:notifications,
- :current_user_todos
+ :current_user_todos,
+ :award_emoji
]
}.freeze
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index d0c4d7f101a..deb688382e2 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -751,6 +751,9 @@ msgstr ""
msgid "%{integrations_link_start}Integrations%{link_end} enable you to make third-party applications part of your GitLab workflow. If the available integrations don't meet your needs, consider using a %{webhooks_link_start}webhook%{link_end}."
msgstr ""
+msgid "%{issuableDisplayName} %{lockStatus}."
+msgstr ""
+
msgid "%{issuableType} will be removed! Are you sure?"
msgstr ""
@@ -8867,7 +8870,7 @@ msgstr ""
msgid "Checkout|Name of company or organization using GitLab"
msgstr ""
-msgid "Checkout|Name: %{errors}"
+msgid "Checkout|Name: %{errorMessage}"
msgstr ""
msgid "Checkout|Need more users? Purchase GitLab for your %{company}."
@@ -11774,6 +11777,9 @@ msgstr ""
msgid "Copy %{http_label} clone URL"
msgstr ""
+msgid "Copy %{issueType} email address"
+msgstr ""
+
msgid "Copy %{name}"
msgstr ""
@@ -15853,6 +15859,9 @@ msgstr ""
msgid "Email a new %{name} to this project"
msgstr ""
+msgid "Email address copied"
+msgstr ""
+
msgid "Email address suffix"
msgstr ""
@@ -21268,6 +21277,12 @@ msgstr ""
msgid "Header message"
msgstr ""
+msgid "HeaderAction|Notifications and other %{issueType} actions have moved to this menu."
+msgstr ""
+
+msgid "HeaderAction|Okay!"
+msgstr ""
+
msgid "HeaderAction|incident"
msgstr ""
@@ -23740,9 +23755,6 @@ msgstr ""
msgid "InviteMembersBanner|We noticed that you haven't invited anyone to this group. Invite your colleagues so you can discuss issues, collaborate on merge requests, and share your knowledge."
msgstr ""
-msgid "InviteMembersModal| Inviting a group %{linkStart}adds its members to your group%{linkEnd}, including members who join after the invite. This might put your group over the free %{count} user limit."
-msgstr ""
-
msgid "InviteMembersModal| To get more members, the owner of this top-level group can %{trialLinkStart}start a trial%{trialLinkEnd} or %{upgradeLinkStart}upgrade%{upgradeLinkEnd} to a paid tier."
msgstr ""
@@ -23791,6 +23803,12 @@ msgstr ""
msgid "InviteMembersModal|Invite members"
msgstr ""
+msgid "InviteMembersModal|Inviting a group %{linkStart}adds its members to your group%{linkEnd}, including members who join after the invite. This might put your group over the free %{count} user limit."
+msgstr ""
+
+msgid "InviteMembersModal|Inviting a group %{linkStart}adds its members to your project%{linkEnd}, including members who join after the invite. This might put your group over the free %{count} user limit."
+msgstr ""
+
msgid "InviteMembersModal|Manage members"
msgstr ""
@@ -26211,6 +26229,9 @@ msgstr ""
msgid "Lock %{issuableDisplayName}"
msgstr ""
+msgid "Lock %{issuableType}"
+msgstr ""
+
msgid "Lock File?"
msgstr ""
@@ -26220,9 +26241,6 @@ msgstr ""
msgid "Lock memberships to SAML Group Links synchronization"
msgstr ""
-msgid "Lock merge request"
-msgstr ""
-
msgid "Lock not found"
msgstr ""
@@ -27199,18 +27217,12 @@ msgstr ""
msgid "Merge request events"
msgstr ""
-msgid "Merge request locked."
-msgstr ""
-
msgid "Merge request not merged"
msgstr ""
msgid "Merge request reports"
msgstr ""
-msgid "Merge request unlocked."
-msgstr ""
-
msgid "Merge request was scheduled to merge after pipeline succeeds"
msgstr ""
@@ -27430,6 +27442,9 @@ msgstr ""
msgid "MergeRequest|No files found"
msgstr ""
+msgid "MergeRequest|Reviewed by @%{username} but not yet approved"
+msgstr ""
+
msgid "MergeRequest|Search (e.g. *.vue) (%{MODIFIER_KEY}P)"
msgstr ""
@@ -36212,6 +36227,9 @@ msgstr ""
msgid "Reference"
msgstr ""
+msgid "Reference copied"
+msgstr ""
+
msgid "References"
msgstr ""
@@ -46817,10 +46835,10 @@ msgstr ""
msgid "Unlock"
msgstr ""
-msgid "Unlock account"
+msgid "Unlock %{issuableType}"
msgstr ""
-msgid "Unlock merge request"
+msgid "Unlock account"
msgstr ""
msgid "Unlock more features with GitLab Ultimate"
@@ -48913,6 +48931,9 @@ msgstr ""
msgid "Vulnerability|Unmodified Response"
msgstr ""
+msgid "Vulnerability|Unmodified response:"
+msgstr ""
+
msgid "Vulnerability|View training"
msgstr ""
@@ -52356,6 +52377,9 @@ msgstr ""
msgid "loading"
msgstr ""
+msgid "locked"
+msgstr ""
+
msgid "locked by %{path_lock_user_name} %{created_at}"
msgstr ""
@@ -53254,6 +53278,9 @@ msgstr ""
msgid "unicode domains should use IDNA encoding"
msgstr ""
+msgid "unlocked"
+msgstr ""
+
msgid "updated"
msgstr ""
diff --git a/package.json b/package.json
index d64341e4c85..c676ae7e661 100644
--- a/package.json
+++ b/package.json
@@ -56,7 +56,7 @@
"@gitlab/favicon-overlay": "2.0.0",
"@gitlab/fonts": "^1.2.0",
"@gitlab/svgs": "3.38.0",
- "@gitlab/ui": "59.5.0",
+ "@gitlab/ui": "60.0.0",
"@gitlab/visual-review-tools": "1.7.3",
"@gitlab/web-ide": "0.0.1-dev-20230323132525",
"@mattiasbuelens/web-streams-adapter": "^0.1.0",
diff --git a/scripts/build_assets_image b/scripts/build_assets_image
index ee8623c826e..00f21e5dede 100755
--- a/scripts/build_assets_image
+++ b/scripts/build_assets_image
@@ -29,7 +29,7 @@ fi
ASSETS_IMAGE_NAME="gitlab-assets-ce"
# `dev.gitlab-org` still has gitlab-ee.
-if [ "${CI_PROJECT_NAME}" = "gitlab" ] || [ "${CI_PROJECT_NAME}" = "gitlab-ee" ]; then
+if ([ "${CI_PROJECT_NAME}" = "gitlab" ] && [ "${FOSS_ONLY}" != "1" ]) || ([ "${CI_PROJECT_NAME}" = "gitlab-ee" ] && [ "${FOSS_ONLY}" != "1" ]); then
ASSETS_IMAGE_NAME="gitlab-assets-ee"
fi
diff --git a/scripts/generate_rspec_pipeline.rb b/scripts/generate_rspec_pipeline.rb
index e226acc0430..292b3d85b20 100755
--- a/scripts/generate_rspec_pipeline.rb
+++ b/scripts/generate_rspec_pipeline.rb
@@ -43,12 +43,20 @@ class GenerateRspecPipeline
DEFAULT_AVERAGE_TEST_FILE_DURATION_IN_SECONDS =
DURATION_OF_THE_TEST_SUITE_IN_SECONDS / NUMBER_OF_TESTS_IN_TOTAL_IN_THE_TEST_SUITE
- # rspec_files_path: A file containing RSpec files to run, separated by a space
# pipeline_template_path: A YAML pipeline configuration template to generate the final pipeline config from
- def initialize(pipeline_template_path:, rspec_files_path: nil, knapsack_report_path: nil)
+ # rspec_files_path: A file containing RSpec files to run, separated by a space
+ # knapsack_report_path: A file containing a Knapsack report
+ # test_suite_prefix: An optional test suite folder prefix (e.g. `ee/` or `jh/`)
+ # generated_pipeline_path: An optional filename where to write the pipeline config (defaults to
+ # `"#{pipeline_template_path}.yml"`)
+ def initialize(
+ pipeline_template_path:, rspec_files_path: nil, knapsack_report_path: nil, test_suite_prefix: nil,
+ generated_pipeline_path: nil)
@pipeline_template_path = pipeline_template_path.to_s
@rspec_files_path = rspec_files_path.to_s
@knapsack_report_path = knapsack_report_path.to_s
+ @test_suite_prefix = test_suite_prefix
+ @generated_pipeline_path = generated_pipeline_path || "#{pipeline_template_path}.yml"
raise ArgumentError unless File.exist?(@pipeline_template_path)
end
@@ -56,11 +64,14 @@ class GenerateRspecPipeline
def generate!
if all_rspec_files.empty?
info "Using #{SKIP_PIPELINE_YML_FILE} due to no RSpec files to run"
- FileUtils.cp(SKIP_PIPELINE_YML_FILE, pipeline_filename)
+ FileUtils.cp(SKIP_PIPELINE_YML_FILE, generated_pipeline_path)
return
end
- File.open(pipeline_filename, 'w') do |handle|
+ info "pipeline_template_path: #{pipeline_template_path}"
+ info "generated_pipeline_path: #{generated_pipeline_path}"
+
+ File.open(generated_pipeline_path, 'w') do |handle|
pipeline_yaml = ERB.new(File.read(pipeline_template_path)).result_with_hash(**erb_binding)
handle.write(pipeline_yaml.squeeze("\n").strip)
end
@@ -68,7 +79,8 @@ class GenerateRspecPipeline
private
- attr_reader :pipeline_template_path, :rspec_files_path, :knapsack_report_path
+ attr_reader :pipeline_template_path, :rspec_files_path, :knapsack_report_path, :test_suite_prefix,
+ :generated_pipeline_path
def info(text)
$stdout.puts "[#{self.class.name}] #{text}"
@@ -78,12 +90,11 @@ class GenerateRspecPipeline
@all_rspec_files ||= File.exist?(rspec_files_path) ? File.read(rspec_files_path).split(' ') : []
end
- def pipeline_filename
- @pipeline_filename ||= "#{pipeline_template_path}.yml"
- end
-
def erb_binding
- { rspec_files_per_test_level: rspec_files_per_test_level }
+ {
+ rspec_files_per_test_level: rspec_files_per_test_level,
+ test_suite_prefix: test_suite_prefix
+ }
end
def rspec_files_per_test_level
@@ -91,7 +102,7 @@ class GenerateRspecPipeline
all_remaining_rspec_files = all_rspec_files.dup
TEST_LEVELS.each_with_object(Hash.new { |h, k| h[k] = {} }) do |test_level, memo| # rubocop:disable Rails/IndexWith
memo[test_level][:files] = all_remaining_rspec_files
- .grep(Quality::TestLevel.new.regexp(test_level))
+ .grep(test_level_service.regexp(test_level, true))
.tap { |files| files.each { |file| all_remaining_rspec_files.delete(file) } }
memo[test_level][:parallelization] = optimal_nodes_count(test_level, memo[test_level][:files])
end
@@ -125,10 +136,15 @@ class GenerateRspecPipeline
remaining_knapsack_report = knapsack_report.dup
TEST_LEVELS.each_with_object({}) do |test_level, memo|
matching_data_per_test_level = remaining_knapsack_report
- .select { |test_file, _| test_file.match?(Quality::TestLevel.new.regexp(test_level)) }
+ .select { |test_file, _| test_file.match?(test_level_service.regexp(test_level, true)) }
.tap { |test_data| test_data.each { |file, _| remaining_knapsack_report.delete(file) } }
+
memo[test_level] =
- matching_data_per_test_level.values.sum / matching_data_per_test_level.keys.size
+ if matching_data_per_test_level.empty?
+ DEFAULT_AVERAGE_TEST_FILE_DURATION_IN_SECONDS
+ else
+ matching_data_per_test_level.values.sum / matching_data_per_test_level.keys.size
+ end
end
else
TEST_LEVELS.each_with_object({}) do |test_level, memo| # rubocop:disable Rails/IndexWith
@@ -146,6 +162,10 @@ class GenerateRspecPipeline
{}
end
end
+
+ def test_level_service
+ @test_level_service ||= Quality::TestLevel.new(test_suite_prefix)
+ end
end
if $PROGRAM_NAME == __FILE__
@@ -166,6 +186,15 @@ if $PROGRAM_NAME == __FILE__
options[:knapsack_report_path] = value
end
+ opts.on("-p", "--test-suite-prefix test_suite_prefix", String, "Test suite folder prefix") do |value|
+ options[:test_suite_prefix] = value
+ end
+
+ opts.on("-o", "--generated-pipeline-path generated_pipeline_path", String, "Path where to write the pipeline " \
+ "config") do |value|
+ options[:generated_pipeline_path] = value
+ end
+
opts.on("-h", "--help", "Prints this help") do
puts opts
exit
diff --git a/spec/features/ide/user_opens_merge_request_spec.rb b/spec/features/ide/user_opens_merge_request_spec.rb
index 0074b4b1eb0..dc280133a20 100644
--- a/spec/features/ide/user_opens_merge_request_spec.rb
+++ b/spec/features/ide/user_opens_merge_request_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'IDE merge request', :js, feature_category: :web_ide do
+ include CookieHelper
+
let(:merge_request) { create(:merge_request, :simple, source_project: project) }
let(:project) { create(:project, :public, :repository) }
let(:user) { project.first_owner }
@@ -12,6 +14,8 @@ RSpec.describe 'IDE merge request', :js, feature_category: :web_ide do
sign_in(user)
+ set_cookie('new-actions-popover-viewed', 'true')
+
visit(merge_request_path(merge_request))
end
diff --git a/spec/features/incidents/incident_details_spec.rb b/spec/features/incidents/incident_details_spec.rb
index 709919d0196..a166ff46177 100644
--- a/spec/features/incidents/incident_details_spec.rb
+++ b/spec/features/incidents/incident_details_spec.rb
@@ -94,6 +94,7 @@ RSpec.describe 'Incident details', :js, feature_category: :incident_management d
end
it 'routes the user to the incident details page when the `issue_type` is set to incident' do
+ set_cookie('new-actions-popover-viewed', 'true')
visit project_issue_path(project, issue)
wait_for_requests
@@ -113,6 +114,7 @@ RSpec.describe 'Incident details', :js, feature_category: :incident_management d
end
it 'routes the user to the issue details page when the `issue_type` is set to issue' do
+ set_cookie('new-actions-popover-viewed', 'true')
visit incident_project_issues_path(project, incident)
wait_for_requests
diff --git a/spec/features/issues/discussion_lock_spec.rb b/spec/features/issues/discussion_lock_spec.rb
index 47865d2b6ba..fb9addff1a2 100644
--- a/spec/features/issues/discussion_lock_spec.rb
+++ b/spec/features/issues/discussion_lock_spec.rb
@@ -9,6 +9,7 @@ RSpec.describe 'Discussion Lock', :js, feature_category: :team_planning do
before do
sign_in(user)
+ stub_feature_flags(moved_mr_sidebar: false)
end
context 'when a user is a team member' do
diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb
index 2bd5373b715..665c7307231 100644
--- a/spec/features/issues/gfm_autocomplete_spec.rb
+++ b/spec/features/issues/gfm_autocomplete_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'GFM autocomplete', :js, feature_category: :team_planning do
+ include CookieHelper
+
let_it_be(:user) { create(:user, name: '💃speciąl someone💃', username: 'someone.special') }
let_it_be(:user2) { create(:user, name: 'Marge Simpson', username: 'msimpson') }
@@ -45,6 +47,7 @@ RSpec.describe 'GFM autocomplete', :js, feature_category: :team_planning do
before do
sign_in(user)
+ set_cookie('new-actions-popover-viewed', 'true')
visit project_issue_path(project, issue_to_edit)
wait_for_requests
diff --git a/spec/features/issues/issue_detail_spec.rb b/spec/features/issues/issue_detail_spec.rb
index d5f90bb9260..29a61d584ee 100644
--- a/spec/features/issues/issue_detail_spec.rb
+++ b/spec/features/issues/issue_detail_spec.rb
@@ -98,6 +98,7 @@ RSpec.describe 'Issue Detail', :js, feature_category: :team_planning do
project.add_developer(user_to_be_deleted)
sign_in(user_to_be_deleted)
+ stub_feature_flags(moved_mr_sidebar: false)
visit project_issue_path(project, issue)
wait_for_requests
@@ -129,7 +130,7 @@ RSpec.describe 'Issue Detail', :js, feature_category: :team_planning do
describe 'when an issue `issue_type` is edited' do
before do
sign_in(user)
-
+ set_cookie('new-actions-popover-viewed', 'true')
visit project_issue_path(project, issue)
wait_for_requests
end
@@ -163,7 +164,7 @@ RSpec.describe 'Issue Detail', :js, feature_category: :team_planning do
describe 'when an incident `issue_type` is edited' do
before do
sign_in(user)
-
+ set_cookie('new-actions-popover-viewed', 'true')
visit project_issue_path(project, incident)
wait_for_requests
end
diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb
index 2ae347d4f9e..ee71181fba2 100644
--- a/spec/features/issues/issue_sidebar_spec.rb
+++ b/spec/features/issues/issue_sidebar_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe 'Issue Sidebar', feature_category: :team_planning do
include MobileHelpers
include Features::InviteMembersModalHelpers
+ include CookieHelper
let_it_be(:group) { create(:group, :nested) }
let_it_be(:project) { create(:project, :public, namespace: group) }
@@ -20,6 +21,7 @@ RSpec.describe 'Issue Sidebar', feature_category: :team_planning do
context 'when signed in' do
before do
sign_in(user)
+ set_cookie('new-actions-popover-viewed', 'true')
end
context 'when concerning the assignee', :js do
@@ -205,6 +207,7 @@ RSpec.describe 'Issue Sidebar', feature_category: :team_planning do
context 'as an allowed user' do
before do
+ stub_feature_flags(moved_mr_sidebar: false)
project.add_developer(user)
visit_issue(project, issue)
end
@@ -293,6 +296,7 @@ RSpec.describe 'Issue Sidebar', feature_category: :team_planning do
context 'as a guest' do
before do
+ stub_feature_flags(moved_mr_sidebar: false)
project.add_guest(user)
visit_issue(project, issue)
end
diff --git a/spec/features/issues/user_edits_issue_spec.rb b/spec/features/issues/user_edits_issue_spec.rb
index 3a927e76fd1..4730406c2b2 100644
--- a/spec/features/issues/user_edits_issue_spec.rb
+++ b/spec/features/issues/user_edits_issue_spec.rb
@@ -3,6 +3,8 @@
require "spec_helper"
RSpec.describe "Issues > User edits issue", :js, feature_category: :team_planning do
+ include CookieHelper
+
let_it_be(:project) { create(:project_empty_repo, :public) }
let_it_be(:project_with_milestones) { create(:project_empty_repo, :public) }
let_it_be(:user) { create(:user) }
@@ -18,6 +20,7 @@ RSpec.describe "Issues > User edits issue", :js, feature_category: :team_plannin
project.add_developer(user)
project_with_milestones.add_developer(user)
sign_in(user)
+ set_cookie('new-actions-popover-viewed', 'true')
end
context "from edit page" do
diff --git a/spec/features/issues/user_toggles_subscription_spec.rb b/spec/features/issues/user_toggles_subscription_spec.rb
index 904fafdf56a..00b04c10d33 100644
--- a/spec/features/issues/user_toggles_subscription_spec.rb
+++ b/spec/features/issues/user_toggles_subscription_spec.rb
@@ -10,6 +10,7 @@ RSpec.describe "User toggles subscription", :js, feature_category: :team_plannin
context 'user is not logged in' do
before do
+ stub_feature_flags(moved_mr_sidebar: false)
visit(project_issue_path(project, issue))
end
@@ -20,9 +21,9 @@ RSpec.describe "User toggles subscription", :js, feature_category: :team_plannin
context 'user is logged in' do
before do
+ stub_feature_flags(moved_mr_sidebar: false)
project.add_developer(user)
sign_in(user)
-
visit(project_issue_path(project, issue))
end
@@ -52,6 +53,7 @@ RSpec.describe "User toggles subscription", :js, feature_category: :team_plannin
context 'user is logged in without edit permission' do
before do
+ stub_feature_flags(moved_mr_sidebar: false)
sign_in(user2)
visit(project_issue_path(project, issue))
diff --git a/spec/features/merge_request/user_manages_subscription_spec.rb b/spec/features/merge_request/user_manages_subscription_spec.rb
index d4ccc4a93b5..3bcc8255ab7 100644
--- a/spec/features/merge_request/user_manages_subscription_spec.rb
+++ b/spec/features/merge_request/user_manages_subscription_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'User manages subscription', :js, feature_category: :code_review_workflow do
+ include CookieHelper
+
let(:project) { create(:project, :public, :repository) }
let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let(:user) { create(:user) }
@@ -10,7 +12,7 @@ RSpec.describe 'User manages subscription', :js, feature_category: :code_review_
before do
stub_feature_flags(moved_mr_sidebar: moved_mr_sidebar_enabled)
-
+ set_cookie('new-actions-popover-viewed', 'true')
project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/merge_request/user_opens_checkout_branch_modal_spec.rb b/spec/features/merge_request/user_opens_checkout_branch_modal_spec.rb
index 7cb1c95f6dc..601310cbacf 100644
--- a/spec/features/merge_request/user_opens_checkout_branch_modal_spec.rb
+++ b/spec/features/merge_request/user_opens_checkout_branch_modal_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe 'Merge request > User opens checkout branch modal', :js, feature_category: :code_review_workflow do
include ProjectForksHelper
+ include CookieHelper
let(:project) { create(:project, :public, :repository) }
let(:user) { project.creator }
@@ -11,6 +12,7 @@ RSpec.describe 'Merge request > User opens checkout branch modal', :js, feature_
before do
project.add_maintainer(user)
sign_in(user)
+ set_cookie('new-actions-popover-viewed', 'true')
end
describe 'for fork' do
diff --git a/spec/features/merge_request/user_sees_check_out_branch_modal_spec.rb b/spec/features/merge_request/user_sees_check_out_branch_modal_spec.rb
index ad2ceeb23e2..21c62b0d0d8 100644
--- a/spec/features/merge_request/user_sees_check_out_branch_modal_spec.rb
+++ b/spec/features/merge_request/user_sees_check_out_branch_modal_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'Merge request > User sees check out branch modal', :js, feature_category: :code_review_workflow do
+ include CookieHelper
+
let(:project) { create(:project, :public, :repository) }
let(:user) { project.creator }
let(:merge_request) { create(:merge_request, source_project: project) }
@@ -10,6 +12,7 @@ RSpec.describe 'Merge request > User sees check out branch modal', :js, feature_
before do
sign_in(user)
+ set_cookie('new-actions-popover-viewed', 'true')
visit project_merge_request_path(project, merge_request)
wait_for_requests
diff --git a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
index 0de59ea21c5..dae28cbb05c 100644
--- a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
+++ b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe 'Merge request > User selects branches for new MR', :js, feature_category: :code_review_workflow do
include ListboxHelpers
+ include CookieHelper
let(:project) { create(:project, :public, :repository) }
let(:user) { project.creator }
@@ -17,6 +18,7 @@ RSpec.describe 'Merge request > User selects branches for new MR', :js, feature_
before do
project.add_maintainer(user)
sign_in(user)
+ set_cookie('new-actions-popover-viewed', 'true')
end
it 'selects the source branch sha when a tag with the same name exists' do
diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb
index adf410ce6e8..77f88994bfb 100644
--- a/spec/features/projects/issuable_templates_spec.rb
+++ b/spec/features/projects/issuable_templates_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe 'issuable templates', :js, feature_category: :projects do
include ProjectForksHelper
+ include CookieHelper
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
@@ -12,6 +13,7 @@ RSpec.describe 'issuable templates', :js, feature_category: :projects do
before do
project.add_maintainer(user)
sign_in user
+ set_cookie('new-actions-popover-viewed', 'true')
end
context 'user creates an issue using templates' do
diff --git a/spec/features/reportable_note/issue_spec.rb b/spec/features/reportable_note/issue_spec.rb
index 55e7f5897bc..a18cdf27294 100644
--- a/spec/features/reportable_note/issue_spec.rb
+++ b/spec/features/reportable_note/issue_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'Reportable note on issue', :js, feature_category: :team_planning do
+ include CookieHelper
+
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:issue) { create(:issue, project: project) }
@@ -11,7 +13,7 @@ RSpec.describe 'Reportable note on issue', :js, feature_category: :team_planning
before do
project.add_maintainer(user)
sign_in(user)
-
+ set_cookie('new-actions-popover-viewed', 'true')
visit project_issue_path(project, issue)
end
diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb
index 1df0aa411d6..e93c0c790c2 100644
--- a/spec/finders/notes_finder_spec.rb
+++ b/spec/finders/notes_finder_spec.rb
@@ -128,6 +128,51 @@ RSpec.describe NotesFinder do
end
end
+ context 'for notes from users who have been banned', :enable_admin_mode, feature_category: :instance_resiliency do
+ subject(:finder) { described_class.new(user, project: project).execute }
+
+ let_it_be(:banned_user) { create(:banned_user).user }
+ let!(:banned_note) { create(:note_on_issue, project: project, author: banned_user) }
+
+ context 'when :hidden_notes feature is not enabled' do
+ before do
+ stub_feature_flags(hidden_notes: false)
+ end
+
+ context 'when user is not an admin' do
+ it { is_expected.to include(banned_note) }
+ end
+
+ context 'when @current_user is nil' do
+ let(:user) { nil }
+
+ it { is_expected.to be_empty }
+ end
+ end
+
+ context 'when :hidden_notes feature is enabled' do
+ before do
+ stub_feature_flags(hidden_notes: true)
+ end
+
+ context 'when user is an admin' do
+ let(:user) { create(:admin) }
+
+ it { is_expected.to include(banned_note) }
+ end
+
+ context 'when user is not an admin' do
+ it { is_expected.not_to include(banned_note) }
+ end
+
+ context 'when @current_user is nil' do
+ let(:user) { nil }
+
+ it { is_expected.to be_empty }
+ end
+ end
+ end
+
context 'for target type' do
let(:project) { create(:project, :repository) }
let!(:note1) { create :note_on_issue, project: project }
diff --git a/spec/frontend/boards/components/board_app_spec.js b/spec/frontend/boards/components/board_app_spec.js
index 148e696b57b..77ba6cdc9c0 100644
--- a/spec/frontend/boards/components/board_app_spec.js
+++ b/spec/frontend/boards/components/board_app_spec.js
@@ -1,14 +1,20 @@
import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
import Vuex from 'vuex';
+import createMockApollo from 'helpers/mock_apollo_helper';
import BoardApp from '~/boards/components/board_app.vue';
+import activeBoardItemQuery from 'ee_else_ce/boards/graphql/client/active_board_item.query.graphql';
+import { rawIssue } from '../mock_data';
describe('BoardApp', () => {
let wrapper;
let store;
+ const mockApollo = createMockApollo();
Vue.use(Vuex);
+ Vue.use(VueApollo);
const createStore = ({ mockGetters = {} } = {}) => {
store = new Vuex.Store({
@@ -23,12 +29,22 @@ describe('BoardApp', () => {
});
};
- const createComponent = () => {
+ const createComponent = ({ isApolloBoard = false, issue = rawIssue } = {}) => {
+ mockApollo.clients.defaultClient.cache.writeQuery({
+ query: activeBoardItemQuery,
+ data: {
+ activeBoardItem: issue,
+ },
+ });
+
wrapper = shallowMount(BoardApp, {
+ apolloProvider: mockApollo,
store,
provide: {
initialBoardId: 'gid://gitlab/Board/1',
initialFilterParams: {},
+ isIssueBoard: true,
+ isApolloBoard,
},
});
};
@@ -50,4 +66,22 @@ describe('BoardApp', () => {
expect(wrapper.classes()).not.toContain('is-compact');
});
+
+ describe('Apollo boards', () => {
+ beforeEach(async () => {
+ createComponent({ isApolloBoard: true });
+ await nextTick();
+ });
+
+ it('should have is-compact class when a card is selected', () => {
+ expect(wrapper.classes()).toContain('is-compact');
+ });
+
+ it('should not have is-compact class when no card is selected', async () => {
+ createComponent({ isApolloBoard: true, issue: {} });
+ await nextTick();
+
+ expect(wrapper.classes()).not.toContain('is-compact');
+ });
+ });
});
diff --git a/spec/frontend/boards/components/board_card_spec.js b/spec/frontend/boards/components/board_card_spec.js
index 46116bed4cf..91e9b6f8cfa 100644
--- a/spec/frontend/boards/components/board_card_spec.js
+++ b/spec/frontend/boards/components/board_card_spec.js
@@ -1,8 +1,10 @@
import { GlLabel } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import BoardCard from '~/boards/components/board_card.vue';
import BoardCardInner from '~/boards/components/board_card_inner.vue';
import { inactiveId } from '~/boards/constants';
@@ -14,6 +16,14 @@ describe('Board card', () => {
let mockActions;
Vue.use(Vuex);
+ Vue.use(VueApollo);
+
+ const mockSetActiveBoardItemResolver = jest.fn();
+ const mockApollo = createMockApollo([], {
+ Mutation: {
+ setActiveBoardItem: mockSetActiveBoardItemResolver,
+ },
+ });
const createStore = ({ initialState = {} } = {}) => {
mockActions = {
@@ -36,11 +46,11 @@ describe('Board card', () => {
const mountComponent = ({
propsData = {},
provide = {},
- mountFn = shallowMount,
stubs = { BoardCardInner },
item = mockIssue,
} = {}) => {
- wrapper = mountFn(BoardCard, {
+ wrapper = shallowMountExtended(BoardCard, {
+ apolloProvider: mockApollo,
stubs: {
...stubs,
BoardCardInner,
@@ -56,9 +66,9 @@ describe('Board card', () => {
groupId: null,
rootPath: '/',
scopedLabelsAvailable: false,
+ isIssueBoard: true,
isEpicBoard: false,
issuableType: 'issue',
- isProjectBoard: false,
isGroupBoard: true,
disabled: false,
isApolloBoard: false,
@@ -218,4 +228,25 @@ describe('Board card', () => {
expect(wrapper.attributes('style')).toBeUndefined();
});
});
+
+ describe('Apollo boards', () => {
+ beforeEach(async () => {
+ createStore();
+ mountComponent({ provide: { isApolloBoard: true } });
+ await nextTick();
+ });
+
+ it('set active board item on client when clicking on card', async () => {
+ await selectCard();
+
+ expect(mockSetActiveBoardItemResolver).toHaveBeenCalledWith(
+ {},
+ {
+ boardItem: mockIssue,
+ },
+ expect.anything(),
+ expect.anything(),
+ );
+ });
+ });
});
diff --git a/spec/frontend/boards/components/board_content_sidebar_spec.js b/spec/frontend/boards/components/board_content_sidebar_spec.js
index 90376a4a553..558a0a3b933 100644
--- a/spec/frontend/boards/components/board_content_sidebar_spec.js
+++ b/spec/frontend/boards/components/board_content_sidebar_spec.js
@@ -1,10 +1,15 @@
import { GlDrawer } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
import { MountingPortal } from 'portal-vue';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
import Vuex from 'vuex';
import SidebarDropdownWidget from 'ee_else_ce/sidebar/components/sidebar_dropdown_widget.vue';
+import createMockApollo from 'helpers/mock_apollo_helper';
import { stubComponent } from 'helpers/stub_component';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+
+import activeBoardItemQuery from 'ee_else_ce/boards/graphql/client/active_board_item.query.graphql';
import BoardContentSidebar from '~/boards/components/board_content_sidebar.vue';
import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
import { ISSUABLE } from '~/boards/constants';
@@ -14,13 +19,21 @@ import SidebarSeverityWidget from '~/sidebar/components/severity/sidebar_severit
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
import SidebarLabelsWidget from '~/sidebar/components/labels/labels_select_widget/labels_select_root.vue';
-import { mockActiveIssue, mockIssue, mockIssueGroupPath, mockIssueProjectPath } from '../mock_data';
+import { mockActiveIssue, mockIssue, rawIssue } from '../mock_data';
Vue.use(Vuex);
+Vue.use(VueApollo);
describe('BoardContentSidebar', () => {
let wrapper;
let store;
+ const mockSetActiveBoardItemResolver = jest.fn();
+ const mockApollo = createMockApollo([], {
+ Mutation: {
+ setActiveBoardItem: mockSetActiveBoardItemResolver,
+ },
+ });
+
const createStore = ({ mockGetters = {}, mockActions = {} } = {}) => {
store = new Vuex.Store({
state: {
@@ -32,30 +45,29 @@ describe('BoardContentSidebar', () => {
activeBoardItem: () => {
return { ...mockActiveIssue, epic: null };
},
- groupPathForActiveIssue: () => mockIssueGroupPath,
- projectPathForActiveIssue: () => mockIssueProjectPath,
- isSidebarOpen: () => true,
...mockGetters,
},
actions: mockActions,
});
};
- const createComponent = () => {
- /*
- Dynamically imported components (in our case ee imports)
- aren't stubbed automatically in VTU v1:
- https://github.com/vuejs/vue-test-utils/issues/1279.
+ const createComponent = ({ isApolloBoard = false } = {}) => {
+ mockApollo.clients.defaultClient.cache.writeQuery({
+ query: activeBoardItemQuery,
+ data: {
+ activeBoardItem: rawIssue,
+ },
+ });
- This requires us to additionally mock apollo or vuex stores.
- */
- wrapper = shallowMount(BoardContentSidebar, {
+ wrapper = shallowMountExtended(BoardContentSidebar, {
+ apolloProvider: mockApollo,
provide: {
canUpdate: true,
rootPath: '/',
groupId: 1,
issuableType: TYPE_ISSUE,
isGroupBoard: false,
+ isApolloBoard,
},
store,
stubs: {
@@ -63,24 +75,6 @@ describe('BoardContentSidebar', () => {
template: '<div><slot name="header"></slot><slot></slot></div>',
}),
},
- mocks: {
- $apollo: {
- queries: {
- participants: {
- loading: false,
- },
- currentIteration: {
- loading: false,
- },
- iterations: {
- loading: false,
- },
- attributesList: {
- loading: false,
- },
- },
- },
- },
});
};
@@ -101,10 +95,12 @@ describe('BoardContentSidebar', () => {
});
});
- it('does not render GlDrawer when isSidebarOpen is false', () => {
- createStore({ mockGetters: { isSidebarOpen: () => false } });
+ it('does not render GlDrawer when no active item is set', async () => {
+ createStore({ mockGetters: { activeBoardItem: () => ({ id: '', iid: '' }) } });
createComponent();
+ await nextTick();
+
expect(wrapper.findComponent(GlDrawer).props('open')).toBe(false);
});
@@ -189,4 +185,27 @@ describe('BoardContentSidebar', () => {
expect(wrapper.findComponent(SidebarSeverityWidget).exists()).toBe(true);
});
});
+
+ describe('Apollo boards', () => {
+ beforeEach(async () => {
+ createStore();
+ createComponent({ isApolloBoard: true });
+ await nextTick();
+ });
+
+ it('calls setActiveBoardItemMutation on close', async () => {
+ wrapper.findComponent(GlDrawer).vm.$emit('close');
+
+ await waitForPromises();
+
+ expect(mockSetActiveBoardItemResolver).toHaveBeenCalledWith(
+ {},
+ {
+ boardItem: null,
+ },
+ expect.anything(),
+ expect.anything(),
+ );
+ });
+ });
});
diff --git a/spec/frontend/boards/components/board_content_spec.js b/spec/frontend/boards/components/board_content_spec.js
index 33351bf8efd..ab51f477966 100644
--- a/spec/frontend/boards/components/board_content_spec.js
+++ b/spec/frontend/boards/components/board_content_spec.js
@@ -6,6 +6,7 @@ import Draggable from 'vuedraggable';
import Vuex from 'vuex';
import eventHub from '~/boards/eventhub';
+import { stubComponent } from 'helpers/stub_component';
import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'helpers/mock_apollo_helper';
import EpicsSwimlanes from 'ee_component/boards/components/epics_swimlanes.vue';
@@ -78,6 +79,11 @@ describe('BoardContent', () => {
isApolloBoard,
},
store,
+ stubs: {
+ BoardContentSidebar: stubComponent(BoardContentSidebar, {
+ template: '<div></div>',
+ }),
+ },
});
};
diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js
index a20884baf3b..296f25d96c4 100644
--- a/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js
+++ b/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js
@@ -1,6 +1,7 @@
import { GlAlert, GlFormInput, GlForm, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
+import waitForPromises from 'helpers/wait_for_promises';
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
import { createStore } from '~/boards/stores';
@@ -21,7 +22,7 @@ const TEST_ISSUE_B = {
webUrl: 'webUrl',
};
-describe('~/boards/components/sidebar/board_sidebar_title.vue', () => {
+describe('BoardSidebarTitle', () => {
let wrapper;
let store;
@@ -39,6 +40,10 @@ describe('~/boards/components/sidebar/board_sidebar_title.vue', () => {
store,
provide: {
canUpdate: true,
+ isApolloBoard: false,
+ },
+ propsData: {
+ activeItem: item,
},
stubs: {
'board-editable-item': BoardEditableItem,
@@ -86,7 +91,8 @@ describe('~/boards/components/sidebar/board_sidebar_title.vue', () => {
await nextTick();
});
- it('collapses sidebar and renders new title', () => {
+ it('collapses sidebar and renders new title', async () => {
+ await waitForPromises();
expect(findCollapsed().isVisible()).toBe(true);
expect(findTitle().text()).toContain(TEST_TITLE);
});
diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js
index d5c6871d9c4..cc0945da9f4 100644
--- a/spec/frontend/boards/mock_data.js
+++ b/spec/frontend/boards/mock_data.js
@@ -277,6 +277,9 @@ export const labels = [
},
];
+export const mockIssueFullPath = 'gitlab-org/test-subgroup/gitlab-test';
+export const mockEpicFullPath = 'gitlab-org/test-subgroup';
+
export const rawIssue = {
title: 'Issue 1',
id: 'gid://gitlab/Issue/436',
@@ -302,12 +305,24 @@ export const rawIssue = {
epic: {
id: 'gid://gitlab/Epic/41',
},
+ totalTimeSpent: 0,
+ humanTimeEstimate: null,
+ humanTotalTimeSpent: null,
+ emailsDisabled: false,
+ hidden: false,
+ webUrl: `${mockIssueFullPath}/-/issue/27`,
+ relativePosition: null,
+ severity: null,
+ milestone: null,
+ weight: null,
+ blocked: false,
+ blockedByCount: 0,
+ iteration: null,
+ healthStatus: null,
type: 'ISSUE',
+ __typename: 'Issue',
};
-export const mockIssueFullPath = 'gitlab-org/test-subgroup/gitlab-test';
-export const mockEpicFullPath = 'gitlab-org/test-subgroup';
-
export const mockIssue = {
id: 'gid://gitlab/Issue/436',
iid: '27',
@@ -329,7 +344,22 @@ export const mockIssue = {
epic: {
id: 'gid://gitlab/Epic/41',
},
+ totalTimeSpent: 0,
+ humanTimeEstimate: null,
+ humanTotalTimeSpent: null,
+ emailsDisabled: false,
+ hidden: false,
+ webUrl: `${mockIssueFullPath}/-/issue/27`,
+ relativePosition: null,
+ severity: null,
+ milestone: null,
+ weight: null,
+ blocked: false,
+ blockedByCount: 0,
+ iteration: null,
+ healthStatus: null,
type: 'ISSUE',
+ __typename: 'Issue',
};
export const mockEpic = {
diff --git a/spec/frontend/artifacts/components/app_spec.js b/spec/frontend/ci/artifacts/components/app_spec.js
index 931c4703e95..435b03e82ab 100644
--- a/spec/frontend/artifacts/components/app_spec.js
+++ b/spec/frontend/ci/artifacts/components/app_spec.js
@@ -2,13 +2,13 @@ import { GlSkeletonLoader } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import Vue from 'vue';
import { numberToHumanSize } from '~/lib/utils/number_utils';
-import ArtifactsApp from '~/artifacts/components/app.vue';
-import JobArtifactsTable from '~/artifacts/components/job_artifacts_table.vue';
-import getBuildArtifactsSizeQuery from '~/artifacts/graphql/queries/get_build_artifacts_size.query.graphql';
+import ArtifactsApp from '~/ci/artifacts/components/app.vue';
+import JobArtifactsTable from '~/ci/artifacts/components/job_artifacts_table.vue';
+import getBuildArtifactsSizeQuery from '~/ci/artifacts/graphql/queries/get_build_artifacts_size.query.graphql';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { PAGE_TITLE, TOTAL_ARTIFACTS_SIZE, SIZE_UNKNOWN } from '~/artifacts/constants';
+import { PAGE_TITLE, TOTAL_ARTIFACTS_SIZE, SIZE_UNKNOWN } from '~/ci/artifacts/constants';
const TEST_BUILD_ARTIFACTS_SIZE = 1024;
const TEST_PROJECT_PATH = 'project/path';
diff --git a/spec/frontend/artifacts/components/artifact_row_spec.js b/spec/frontend/ci/artifacts/components/artifact_row_spec.js
index 268772ed4c0..86875cd8566 100644
--- a/spec/frontend/artifacts/components/artifact_row_spec.js
+++ b/spec/frontend/ci/artifacts/components/artifact_row_spec.js
@@ -1,10 +1,10 @@
import { GlBadge, GlButton, GlFriendlyWrap, GlFormCheckbox } from '@gitlab/ui';
-import mockGetJobArtifactsResponse from 'test_fixtures/graphql/artifacts/graphql/queries/get_job_artifacts.query.graphql.json';
+import mockGetJobArtifactsResponse from 'test_fixtures/graphql/ci/artifacts/graphql/queries/get_job_artifacts.query.graphql.json';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import ArtifactRow from '~/artifacts/components/artifact_row.vue';
-import { BULK_DELETE_FEATURE_FLAG } from '~/artifacts/constants';
+import ArtifactRow from '~/ci/artifacts/components/artifact_row.vue';
+import { BULK_DELETE_FEATURE_FLAG } from '~/ci/artifacts/constants';
describe('ArtifactRow component', () => {
let wrapper;
diff --git a/spec/frontend/artifacts/components/artifacts_bulk_delete_spec.js b/spec/frontend/ci/artifacts/components/artifacts_bulk_delete_spec.js
index 876906b2c3c..dd077dcac9f 100644
--- a/spec/frontend/artifacts/components/artifacts_bulk_delete_spec.js
+++ b/spec/frontend/ci/artifacts/components/artifacts_bulk_delete_spec.js
@@ -1,12 +1,12 @@
import { GlSprintf, GlModal } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import mockGetJobArtifactsResponse from 'test_fixtures/graphql/artifacts/graphql/queries/get_job_artifacts.query.graphql.json';
+import mockGetJobArtifactsResponse from 'test_fixtures/graphql/ci/artifacts/graphql/queries/get_job_artifacts.query.graphql.json';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import ArtifactsBulkDelete from '~/artifacts/components/artifacts_bulk_delete.vue';
-import bulkDestroyArtifactsMutation from '~/artifacts/graphql/mutations/bulk_destroy_job_artifacts.mutation.graphql';
+import ArtifactsBulkDelete from '~/ci/artifacts/components/artifacts_bulk_delete.vue';
+import bulkDestroyArtifactsMutation from '~/ci/artifacts/graphql/mutations/bulk_destroy_job_artifacts.mutation.graphql';
Vue.use(VueApollo);
diff --git a/spec/frontend/artifacts/components/artifacts_table_row_details_spec.js b/spec/frontend/ci/artifacts/components/artifacts_table_row_details_spec.js
index 6bf3498f9b0..ebdb7e25c45 100644
--- a/spec/frontend/artifacts/components/artifacts_table_row_details_spec.js
+++ b/spec/frontend/ci/artifacts/components/artifacts_table_row_details_spec.js
@@ -1,15 +1,15 @@
import { GlModal } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import getJobArtifactsResponse from 'test_fixtures/graphql/artifacts/graphql/queries/get_job_artifacts.query.graphql.json';
+import getJobArtifactsResponse from 'test_fixtures/graphql/ci/artifacts/graphql/queries/get_job_artifacts.query.graphql.json';
import waitForPromises from 'helpers/wait_for_promises';
-import ArtifactsTableRowDetails from '~/artifacts/components/artifacts_table_row_details.vue';
-import ArtifactRow from '~/artifacts/components/artifact_row.vue';
-import ArtifactDeleteModal from '~/artifacts/components/artifact_delete_modal.vue';
+import ArtifactsTableRowDetails from '~/ci/artifacts/components/artifacts_table_row_details.vue';
+import ArtifactRow from '~/ci/artifacts/components/artifact_row.vue';
+import ArtifactDeleteModal from '~/ci/artifacts/components/artifact_delete_modal.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
-import destroyArtifactMutation from '~/artifacts/graphql/mutations/destroy_artifact.mutation.graphql';
-import { I18N_DESTROY_ERROR, I18N_MODAL_TITLE } from '~/artifacts/constants';
+import destroyArtifactMutation from '~/ci/artifacts/graphql/mutations/destroy_artifact.mutation.graphql';
+import { I18N_DESTROY_ERROR, I18N_MODAL_TITLE } from '~/ci/artifacts/constants';
import { createAlert } from '~/alert';
jest.mock('~/alert');
diff --git a/spec/frontend/artifacts/components/feedback_banner_spec.js b/spec/frontend/ci/artifacts/components/feedback_banner_spec.js
index af9599daefa..53e0fdac6f6 100644
--- a/spec/frontend/artifacts/components/feedback_banner_spec.js
+++ b/spec/frontend/ci/artifacts/components/feedback_banner_spec.js
@@ -1,12 +1,12 @@
import { GlBanner } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import FeedbackBanner from '~/artifacts/components/feedback_banner.vue';
+import FeedbackBanner from '~/ci/artifacts/components/feedback_banner.vue';
import { makeMockUserCalloutDismisser } from 'helpers/mock_user_callout_dismisser';
import {
I18N_FEEDBACK_BANNER_TITLE,
I18N_FEEDBACK_BANNER_BUTTON,
FEEDBACK_URL,
-} from '~/artifacts/constants';
+} from '~/ci/artifacts/constants';
const mockBannerImagePath = 'banner/image/path';
diff --git a/spec/frontend/artifacts/components/job_artifacts_table_spec.js b/spec/frontend/ci/artifacts/components/job_artifacts_table_spec.js
index 40f3c9633ab..2855b0ecb37 100644
--- a/spec/frontend/artifacts/components/job_artifacts_table_spec.js
+++ b/spec/frontend/ci/artifacts/components/job_artifacts_table_spec.js
@@ -9,17 +9,17 @@ import {
} from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import getJobArtifactsResponse from 'test_fixtures/graphql/artifacts/graphql/queries/get_job_artifacts.query.graphql.json';
+import getJobArtifactsResponse from 'test_fixtures/graphql/ci/artifacts/graphql/queries/get_job_artifacts.query.graphql.json';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import waitForPromises from 'helpers/wait_for_promises';
-import JobArtifactsTable from '~/artifacts/components/job_artifacts_table.vue';
-import FeedbackBanner from '~/artifacts/components/feedback_banner.vue';
-import ArtifactsTableRowDetails from '~/artifacts/components/artifacts_table_row_details.vue';
-import ArtifactDeleteModal from '~/artifacts/components/artifact_delete_modal.vue';
-import ArtifactsBulkDelete from '~/artifacts/components/artifacts_bulk_delete.vue';
+import JobArtifactsTable from '~/ci/artifacts/components/job_artifacts_table.vue';
+import FeedbackBanner from '~/ci/artifacts/components/feedback_banner.vue';
+import ArtifactsTableRowDetails from '~/ci/artifacts/components/artifacts_table_row_details.vue';
+import ArtifactDeleteModal from '~/ci/artifacts/components/artifact_delete_modal.vue';
+import ArtifactsBulkDelete from '~/ci/artifacts/components/artifacts_bulk_delete.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
-import getJobArtifactsQuery from '~/artifacts/graphql/queries/get_job_artifacts.query.graphql';
+import getJobArtifactsQuery from '~/ci/artifacts/graphql/queries/get_job_artifacts.query.graphql';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import {
ARCHIVE_FILE_TYPE,
@@ -27,8 +27,8 @@ import {
I18N_FETCH_ERROR,
INITIAL_CURRENT_PAGE,
BULK_DELETE_FEATURE_FLAG,
-} from '~/artifacts/constants';
-import { totalArtifactsSizeForJob } from '~/artifacts/utils';
+} from '~/ci/artifacts/constants';
+import { totalArtifactsSizeForJob } from '~/ci/artifacts/utils';
import { createAlert } from '~/alert';
jest.mock('~/alert');
diff --git a/spec/frontend/artifacts/components/job_checkbox_spec.js b/spec/frontend/ci/artifacts/components/job_checkbox_spec.js
index 95cc548b8c8..ae70bb4b17b 100644
--- a/spec/frontend/artifacts/components/job_checkbox_spec.js
+++ b/spec/frontend/ci/artifacts/components/job_checkbox_spec.js
@@ -1,7 +1,7 @@
import { GlFormCheckbox } from '@gitlab/ui';
-import mockGetJobArtifactsResponse from 'test_fixtures/graphql/artifacts/graphql/queries/get_job_artifacts.query.graphql.json';
+import mockGetJobArtifactsResponse from 'test_fixtures/graphql/ci/artifacts/graphql/queries/get_job_artifacts.query.graphql.json';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import JobCheckbox from '~/artifacts/components/job_checkbox.vue';
+import JobCheckbox from '~/ci/artifacts/components/job_checkbox.vue';
describe('JobCheckbox component', () => {
let wrapper;
diff --git a/spec/frontend/artifacts/graphql/cache_update_spec.js b/spec/frontend/ci/artifacts/graphql/cache_update_spec.js
index 4d610328298..3c415534c7c 100644
--- a/spec/frontend/artifacts/graphql/cache_update_spec.js
+++ b/spec/frontend/ci/artifacts/graphql/cache_update_spec.js
@@ -1,5 +1,5 @@
-import getJobArtifactsQuery from '~/artifacts/graphql/queries/get_job_artifacts.query.graphql';
-import { removeArtifactFromStore } from '~/artifacts/graphql/cache_update';
+import getJobArtifactsQuery from '~/ci/artifacts/graphql/queries/get_job_artifacts.query.graphql';
+import { removeArtifactFromStore } from '~/ci/artifacts/graphql/cache_update';
describe('Artifact table cache updates', () => {
let store;
diff --git a/spec/frontend/fixtures/job_artifacts.rb b/spec/frontend/fixtures/job_artifacts.rb
index e53cdbbaaa5..6dadd6750f1 100644
--- a/spec/frontend/fixtures/job_artifacts.rb
+++ b/spec/frontend/fixtures/job_artifacts.rb
@@ -12,7 +12,7 @@ RSpec.describe 'Job Artifacts (GraphQL fixtures)' do
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
let_it_be(:user) { create(:user) }
- job_artifacts_query_path = 'artifacts/graphql/queries/get_job_artifacts.query.graphql'
+ job_artifacts_query_path = 'ci/artifacts/graphql/queries/get_job_artifacts.query.graphql'
it "graphql/#{job_artifacts_query_path}.json" do
create(:ci_build, :failed, :artifacts, :trace_artifact, pipeline: pipeline)
diff --git a/spec/frontend/ide/components/commit_sidebar/form_spec.js b/spec/frontend/ide/components/commit_sidebar/form_spec.js
index 0c0998c037a..ba30073dff2 100644
--- a/spec/frontend/ide/components/commit_sidebar/form_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/form_spec.js
@@ -21,6 +21,7 @@ import { COMMIT_TO_NEW_BRANCH } from '~/ide/stores/modules/commit/constants';
describe('IDE commit form', () => {
let wrapper;
let store;
+ const showModalSpy = jest.fn();
const createComponent = () => {
wrapper = shallowMount(CommitForm, {
@@ -29,7 +30,11 @@ describe('IDE commit form', () => {
GlTooltip: createMockDirective('gl-tooltip'),
},
stubs: {
- GlModal: stubComponent(GlModal),
+ GlModal: stubComponent(GlModal, {
+ methods: {
+ show: showModalSpy,
+ },
+ }),
},
});
};
@@ -57,6 +62,7 @@ describe('IDE commit form', () => {
tooltip: getBinding(findCommitButtonTooltip().element, 'gl-tooltip').value.title,
});
const findForm = () => wrapper.find('form');
+ const findModal = () => wrapper.findComponent(GlModal);
const submitForm = () => findForm().trigger('submit');
const findCommitMessageInput = () => wrapper.findComponent(CommitMessageField);
const setCommitMessageInput = (val) => findCommitMessageInput().vm.$emit('input', val);
@@ -298,22 +304,19 @@ describe('IDE commit form', () => {
${() => createCodeownersCommitError('test message')} | ${{ actionPrimary: { text: 'Create new branch' } }}
${createUnexpectedCommitError} | ${{ actionPrimary: null }}
`('opens error modal if commitError with $error', async ({ createError, props }) => {
- const modal = wrapper.findComponent(GlModal);
- modal.vm.show = jest.fn();
-
const error = createError();
store.state.commit.commitError = error;
await nextTick();
- expect(modal.vm.show).toHaveBeenCalled();
- expect(modal.props()).toMatchObject({
+ expect(showModalSpy).toHaveBeenCalled();
+ expect(findModal().props()).toMatchObject({
actionCancel: { text: 'Cancel' },
...props,
});
// Because of the legacy 'mountComponent' approach here, the only way to
// test the text of the modal is by viewing the content of the modal added to the document.
- expect(modal.html()).toContain(error.messageHTML);
+ expect(findModal().html()).toContain(error.messageHTML);
});
});
@@ -339,7 +342,7 @@ describe('IDE commit form', () => {
await nextTick();
- wrapper.findComponent(GlModal).vm.$emit('ok');
+ findModal().vm.$emit('ok');
await waitForPromises();
diff --git a/spec/frontend/invite_members/components/invite_group_notification_spec.js b/spec/frontend/invite_members/components/invite_group_notification_spec.js
index 3e6ba6da9f4..1da2e7b705d 100644
--- a/spec/frontend/invite_members/components/invite_group_notification_spec.js
+++ b/spec/frontend/invite_members/components/invite_group_notification_spec.js
@@ -2,7 +2,7 @@ import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { sprintf } from '~/locale';
import InviteGroupNotification from '~/invite_members/components/invite_group_notification.vue';
-import { GROUP_MODAL_ALERT_BODY } from '~/invite_members/constants';
+import { GROUP_MODAL_TO_GROUP_ALERT_BODY } from '~/invite_members/constants';
describe('InviteGroupNotification', () => {
let wrapper;
@@ -13,7 +13,11 @@ describe('InviteGroupNotification', () => {
const createComponent = () => {
wrapper = shallowMountExtended(InviteGroupNotification, {
provide: { freeUsersLimit: 5 },
- propsData: { name: 'name' },
+ propsData: {
+ name: 'name',
+ notificationLink: '_notification_link_',
+ notificationText: GROUP_MODAL_TO_GROUP_ALERT_BODY,
+ },
stubs: { GlSprintf },
});
};
@@ -28,15 +32,13 @@ describe('InviteGroupNotification', () => {
});
it('shows the correct message', () => {
- const message = sprintf(GROUP_MODAL_ALERT_BODY, { count: 5 });
+ const message = sprintf(GROUP_MODAL_TO_GROUP_ALERT_BODY, { count: 5 });
expect(findAlert().text()).toMatchInterpolatedText(message);
});
it('has a help link', () => {
- expect(findLink().attributes('href')).toEqual(
- 'https://docs.gitlab.com/ee/user/group/manage.html#share-a-group-with-another-group',
- );
+ expect(findLink().attributes('href')).toEqual('_notification_link_');
});
});
});
diff --git a/spec/frontend/invite_members/components/invite_groups_modal_spec.js b/spec/frontend/invite_members/components/invite_groups_modal_spec.js
index 82b4717fbf1..4f082145562 100644
--- a/spec/frontend/invite_members/components/invite_groups_modal_spec.js
+++ b/spec/frontend/invite_members/components/invite_groups_modal_spec.js
@@ -12,6 +12,12 @@ import {
displaySuccessfulInvitationAlert,
reloadOnInvitationSuccess,
} from '~/invite_members/utils/trigger_successful_invite_alert';
+import {
+ GROUP_MODAL_TO_GROUP_ALERT_BODY,
+ GROUP_MODAL_TO_GROUP_ALERT_LINK,
+ GROUP_MODAL_TO_PROJECT_ALERT_BODY,
+ GROUP_MODAL_TO_PROJECT_ALERT_LINK,
+} from '~/invite_members/constants';
import { propsData, sharedGroup } from '../mock_data/group_modal';
jest.mock('~/invite_members/utils/trigger_successful_invite_alert');
@@ -91,6 +97,26 @@ describe('InviteGroupsModal', () => {
expect(findInviteGroupAlert().exists()).toBe(false);
});
+
+ it('shows the user limit notification alert with correct link and text for group', () => {
+ createComponent({ freeUserCapEnabled: true });
+
+ expect(findInviteGroupAlert().props()).toMatchObject({
+ name: propsData.name,
+ notificationText: GROUP_MODAL_TO_GROUP_ALERT_BODY,
+ notificationLink: GROUP_MODAL_TO_GROUP_ALERT_LINK,
+ });
+ });
+
+ it('shows the user limit notification alert with correct link and text for project', () => {
+ createComponent({ freeUserCapEnabled: true, isProject: true });
+
+ expect(findInviteGroupAlert().props()).toMatchObject({
+ name: propsData.name,
+ notificationText: GROUP_MODAL_TO_PROJECT_ALERT_BODY,
+ notificationLink: GROUP_MODAL_TO_PROJECT_ALERT_LINK,
+ });
+ });
});
describe('submitting the invite form', () => {
diff --git a/spec/frontend/issues/show/components/header_actions_spec.js b/spec/frontend/issues/show/components/header_actions_spec.js
index 58ec7387851..bd8e79a90ec 100644
--- a/spec/frontend/issues/show/components/header_actions_spec.js
+++ b/spec/frontend/issues/show/components/header_actions_spec.js
@@ -2,6 +2,8 @@ import Vue, { nextTick } from 'vue';
import { GlDropdownItem, GlLink, GlModal, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
+import VueApollo from 'vue-apollo';
+import waitForPromises from 'helpers/wait_for_promises';
import { mockTracking } from 'helpers/tracking_helper';
import { createAlert, VARIANT_SUCCESS } from '~/alert';
import { STATUS_CLOSED, STATUS_OPEN, TYPE_INCIDENT, TYPE_ISSUE } from '~/issues/constants';
@@ -14,17 +16,22 @@ import promoteToEpicMutation from '~/issues/show/queries/promote_to_epic.mutatio
import * as urlUtility from '~/lib/utils/url_utility';
import eventHub from '~/notes/event_hub';
import createStore from '~/notes/stores';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import issueReferenceQuery from '~/sidebar/queries/issue_reference.query.graphql';
+import updateIssueMutation from '~/issues/show/queries/update_issue.mutation.graphql';
+import toast from '~/vue_shared/plugins/global_toast';
jest.mock('~/alert');
jest.mock('~/issues/show/event_hub', () => ({ $emit: jest.fn() }));
+jest.mock('~/vue_shared/plugins/global_toast');
describe('HeaderActions component', () => {
let dispatchEventSpy;
- let mutateMock;
let wrapper;
let visitUrlSpy;
Vue.use(Vuex);
+ Vue.use(VueApollo);
const store = createStore();
@@ -45,15 +52,28 @@ describe('HeaderActions component', () => {
reportedUserId: 1,
reportedFromUrl: 'http://localhost:/gitlab-org/-/issues/32',
submitAsSpamPath: 'gitlab-org/gitlab-test/-/issues/32/submit_as_spam',
+ issuableEmailAddress: null,
+ fullPath: 'full-path',
};
- const updateIssueMutationResponse = { data: { updateIssue: { errors: [] } } };
+ const updateIssueMutationResponse = {
+ data: {
+ updateIssue: {
+ errors: [],
+ issuable: {
+ id: 'gid://gitlab/Issue/511',
+ state: STATUS_OPEN,
+ },
+ },
+ },
+ };
const promoteToEpicMutationResponse = {
data: {
promoteToEpic: {
errors: [],
epic: {
+ id: 'gid://gitlab/Epic/1',
webPath: '/groups/gitlab-org/-/epics/1',
},
},
@@ -69,6 +89,20 @@ describe('HeaderActions component', () => {
},
};
+ const mockIssueReferenceData = {
+ data: {
+ workspace: {
+ id: 'gid://gitlab/Project/7',
+ issuable: {
+ id: 'gid://gitlab/Issue/511',
+ reference: 'flightjs/Flight#33',
+ __typename: 'Issue',
+ },
+ __typename: 'Project',
+ },
+ },
+ };
+
const findToggleIssueStateButton = () => wrapper.find(`[data-testid="toggle-button"]`);
const findEditButton = () => wrapper.find(`[data-testid="edit-button"]`);
@@ -77,33 +111,54 @@ describe('HeaderActions component', () => {
const findDesktopDropdown = () => findDropdownBy('desktop-dropdown');
const findMobileDropdownItems = () => findMobileDropdown().findAllComponents(GlDropdownItem);
const findDesktopDropdownItems = () => findDesktopDropdown().findAllComponents(GlDropdownItem);
+ const findAbuseCategorySelector = () => wrapper.findComponent(AbuseCategorySelector);
+ const findReportAbuseSelectorItem = () => wrapper.find(`[data-testid="report-abuse-item"]`);
+ const findNotificationWidget = () => wrapper.find(`[data-testid="notification-toggle"]`);
+ const findLockIssueWidget = () => wrapper.find(`[data-testid="lock-issue-toggle"]`);
+ const findCopyRefenceDropdownItem = () => wrapper.find(`[data-testid="copy-reference"]`);
+ const findCopyEmailItem = () => wrapper.find(`[data-testid="copy-email"]`);
const findModal = () => wrapper.findComponent(GlModal);
const findModalLinkAt = (index) => findModal().findAllComponents(GlLink).at(index);
+ const issueReferenceSuccessHandler = jest.fn().mockResolvedValue(mockIssueReferenceData);
+ const updateIssueMutationResponseHandler = jest
+ .fn()
+ .mockResolvedValue(updateIssueMutationResponse);
+ const promoteToEpicMutationSuccessResponseHandler = jest
+ .fn()
+ .mockResolvedValue(promoteToEpicMutationResponse);
+ const promoteToEpicMutationErrorHandler = jest
+ .fn()
+ .mockResolvedValue(promoteToEpicMutationErrorResponse);
+
const mountComponent = ({
props = {},
issueState = STATUS_OPEN,
blockedByIssues = [],
- mutateResponse = {},
+ movedMrSidebarEnabled = false,
+ promoteToEpicHandler = promoteToEpicMutationSuccessResponseHandler,
} = {}) => {
- mutateMock = jest.fn().mockResolvedValue(mutateResponse);
-
store.dispatch('setNoteableData', {
blocked_by_issues: blockedByIssues,
state: issueState,
});
+ const handlers = [
+ [issueReferenceQuery, issueReferenceSuccessHandler],
+ [updateIssueMutation, updateIssueMutationResponseHandler],
+ [promoteToEpicMutation, promoteToEpicHandler],
+ ];
+
return shallowMount(HeaderActions, {
+ apolloProvider: createMockApollo(handlers),
store,
provide: {
...defaultProps,
...props,
- },
- mocks: {
- $apollo: {
- mutate: mutateMock,
+ glFeatures: {
+ movedMrSidebar: movedMrSidebarEnabled,
},
},
stubs: {
@@ -138,7 +193,6 @@ describe('HeaderActions component', () => {
wrapper = mountComponent({
props: { issueType },
issueState,
- mutateResponse: updateIssueMutationResponse,
});
});
@@ -149,23 +203,19 @@ describe('HeaderActions component', () => {
it('calls apollo mutation', () => {
findToggleIssueStateButton().vm.$emit('click');
- expect(mutateMock).toHaveBeenCalledWith(
- expect.objectContaining({
- variables: {
- input: {
- iid: defaultProps.iid,
- projectPath: defaultProps.projectPath,
- stateEvent: newIssueState,
- },
- },
- }),
- );
+ expect(updateIssueMutationResponseHandler).toHaveBeenCalledWith({
+ input: {
+ iid: defaultProps.iid,
+ projectPath: defaultProps.projectPath,
+ stateEvent: newIssueState,
+ },
+ });
});
it('dispatches a custom event to update the issue page', async () => {
findToggleIssueStateButton().vm.$emit('click');
- await nextTick();
+ await waitForPromises();
expect(dispatchEventSpy).toHaveBeenCalledTimes(1);
});
@@ -290,28 +340,25 @@ describe('HeaderActions component', () => {
describe('when "Promote to epic" button is clicked', () => {
describe('when response is successful', () => {
- beforeEach(() => {
+ beforeEach(async () => {
visitUrlSpy = jest.spyOn(urlUtility, 'visitUrl').mockReturnValue({});
wrapper = mountComponent({
- mutateResponse: promoteToEpicMutationResponse,
+ promoteToEpicHandler: promoteToEpicMutationSuccessResponseHandler,
});
wrapper.find('[data-testid="promote-button"]').vm.$emit('click');
+
+ await waitForPromises();
});
it('invokes GraphQL mutation when clicked', () => {
- expect(mutateMock).toHaveBeenCalledWith(
- expect.objectContaining({
- mutation: promoteToEpicMutation,
- variables: {
- input: {
- iid: defaultProps.iid,
- projectPath: defaultProps.projectPath,
- },
- },
- }),
- );
+ expect(promoteToEpicMutationSuccessResponseHandler).toHaveBeenCalledWith({
+ input: {
+ iid: defaultProps.iid,
+ projectPath: defaultProps.projectPath,
+ },
+ });
});
it('shows a success message and tells the user they are being redirected', () => {
@@ -329,14 +376,16 @@ describe('HeaderActions component', () => {
});
describe('when response contains errors', () => {
- beforeEach(() => {
+ beforeEach(async () => {
visitUrlSpy = jest.spyOn(urlUtility, 'visitUrl').mockReturnValue({});
wrapper = mountComponent({
- mutateResponse: promoteToEpicMutationErrorResponse,
+ promoteToEpicHandler: promoteToEpicMutationErrorHandler,
});
wrapper.find('[data-testid="promote-button"]').vm.$emit('click');
+
+ await waitForPromises();
});
it('shows an error message', () => {
@@ -349,21 +398,17 @@ describe('HeaderActions component', () => {
describe('when `toggle.issuable.state` event is emitted', () => {
it('invokes a method to toggle the issue state', () => {
- wrapper = mountComponent({ mutateResponse: updateIssueMutationResponse });
+ wrapper = mountComponent();
eventHub.$emit('toggle.issuable.state');
- expect(mutateMock).toHaveBeenCalledWith(
- expect.objectContaining({
- variables: {
- input: {
- iid: defaultProps.iid,
- projectPath: defaultProps.projectPath,
- stateEvent: ISSUE_STATE_EVENT_CLOSE,
- },
- },
- }),
- );
+ expect(updateIssueMutationResponseHandler).toHaveBeenCalledWith({
+ input: {
+ iid: defaultProps.iid,
+ projectPath: defaultProps.projectPath,
+ stateEvent: ISSUE_STATE_EVENT_CLOSE,
+ },
+ });
});
});
@@ -392,17 +437,13 @@ describe('HeaderActions component', () => {
it('calls apollo mutation when primary button is clicked', () => {
findModal().vm.$emit('primary');
- expect(mutateMock).toHaveBeenCalledWith(
- expect.objectContaining({
- variables: {
- input: {
- iid: defaultProps.iid.toString(),
- projectPath: defaultProps.projectPath,
- stateEvent: ISSUE_STATE_EVENT_CLOSE,
- },
- },
- }),
- );
+ expect(updateIssueMutationResponseHandler).toHaveBeenCalledWith({
+ input: {
+ iid: defaultProps.iid.toString(),
+ projectPath: defaultProps.projectPath,
+ stateEvent: ISSUE_STATE_EVENT_CLOSE,
+ },
+ });
});
describe.each`
@@ -434,8 +475,6 @@ describe('HeaderActions component', () => {
});
describe('abuse category selector', () => {
- const findAbuseCategorySelector = () => wrapper.findComponent(AbuseCategorySelector);
-
beforeEach(() => {
wrapper = mountComponent({ props: { isIssueAuthor: false } });
});
@@ -445,7 +484,7 @@ describe('HeaderActions component', () => {
});
it('opens the drawer', async () => {
- findDesktopDropdownItems().at(2).vm.$emit('click');
+ findReportAbuseSelectorItem().vm.$emit('click');
await nextTick();
@@ -453,10 +492,160 @@ describe('HeaderActions component', () => {
});
it('closes the drawer', async () => {
- await findDesktopDropdownItems().at(2).vm.$emit('click');
+ await findReportAbuseSelectorItem().vm.$emit('click');
await findAbuseCategorySelector().vm.$emit('close-drawer');
expect(findAbuseCategorySelector().exists()).toEqual(false);
});
});
+
+ describe('notification toggle', () => {
+ describe('visibility', () => {
+ describe.each`
+ movedMrSidebarEnabled | issueType | visible
+ ${true} | ${TYPE_ISSUE} | ${true}
+ ${true} | ${TYPE_INCIDENT} | ${true}
+ ${false} | ${TYPE_ISSUE} | ${false}
+ ${false} | ${TYPE_INCIDENT} | ${false}
+ `(
+ `when movedMrSidebarEnabled flag is "$movedMrSidebarEnabled" with issue type "$issueType"`,
+ ({ movedMrSidebarEnabled, issueType, visible }) => {
+ beforeEach(() => {
+ wrapper = mountComponent({
+ props: {
+ issueType,
+ },
+ movedMrSidebarEnabled,
+ });
+ });
+
+ it(`${visible ? 'shows' : 'hides'} Notification toggle`, () => {
+ expect(findNotificationWidget().exists()).toBe(visible);
+ });
+ },
+ );
+ });
+ });
+
+ describe('lock issue option', () => {
+ describe('visibility', () => {
+ describe.each`
+ movedMrSidebarEnabled | issueType | visible
+ ${true} | ${TYPE_ISSUE} | ${true}
+ ${true} | ${TYPE_INCIDENT} | ${false}
+ ${false} | ${TYPE_ISSUE} | ${false}
+ ${false} | ${TYPE_INCIDENT} | ${false}
+ `(
+ `when movedMrSidebarEnabled flag is "$movedMrSidebarEnabled" with issue type "$issueType"`,
+ ({ movedMrSidebarEnabled, issueType, visible }) => {
+ beforeEach(() => {
+ wrapper = mountComponent({
+ props: {
+ issueType,
+ },
+ movedMrSidebarEnabled,
+ });
+ });
+
+ it(`${visible ? 'shows' : 'hides'} Lock issue option`, () => {
+ expect(findLockIssueWidget().exists()).toBe(visible);
+ });
+ },
+ );
+ });
+ });
+
+ describe('copy reference option', () => {
+ describe('visibility', () => {
+ describe.each`
+ movedMrSidebarEnabled | issueType | visible
+ ${true} | ${TYPE_ISSUE} | ${true}
+ ${true} | ${TYPE_INCIDENT} | ${true}
+ ${false} | ${TYPE_ISSUE} | ${false}
+ ${false} | ${TYPE_INCIDENT} | ${false}
+ `(
+ 'when movedMrSidebarFlagEnabled is "$movedMrSidebarEnabled" with issue type "$issueType"',
+ ({ movedMrSidebarEnabled, issueType, visible }) => {
+ beforeEach(() => {
+ wrapper = mountComponent({
+ props: {
+ issueType,
+ },
+ movedMrSidebarEnabled,
+ });
+ });
+
+ it(`${visible ? 'shows' : 'hides'} Copy reference option`, () => {
+ expect(findCopyRefenceDropdownItem().exists()).toBe(visible);
+ });
+ },
+ );
+ });
+
+ describe('clicking when visible', () => {
+ beforeEach(() => {
+ wrapper = mountComponent({
+ props: {
+ issueType: TYPE_ISSUE,
+ },
+ movedMrSidebarEnabled: true,
+ });
+ });
+
+ it('shows toast message', () => {
+ findCopyRefenceDropdownItem().vm.$emit('click');
+
+ expect(toast).toHaveBeenCalledWith('Reference copied');
+ });
+ });
+ });
+
+ describe('copy email option', () => {
+ describe('visibility', () => {
+ describe.each`
+ movedMrSidebarEnabled | issueType | issuableEmailAddress | visible
+ ${true} | ${TYPE_ISSUE} | ${'mock-email-address'} | ${true}
+ ${true} | ${TYPE_ISSUE} | ${''} | ${false}
+ ${true} | ${TYPE_INCIDENT} | ${'mock-email-address'} | ${true}
+ ${true} | ${TYPE_INCIDENT} | ${''} | ${false}
+ ${false} | ${TYPE_ISSUE} | ${'mock-email-address'} | ${false}
+ ${false} | ${TYPE_INCIDENT} | ${'mock-email-address'} | ${false}
+ `(
+ 'when movedMrSidebarEnabled flag is "$movedMrSidebarEnabled" issue type is "$issueType" and issuableEmailAddress="$issuableEmailAddress"',
+ ({ movedMrSidebarEnabled, issueType, issuableEmailAddress, visible }) => {
+ beforeEach(() => {
+ wrapper = mountComponent({
+ props: {
+ issueType,
+ issuableEmailAddress,
+ },
+ movedMrSidebarEnabled,
+ });
+ });
+
+ it(`${visible ? 'shows' : 'hides'} Copy email option`, () => {
+ expect(findCopyEmailItem().exists()).toBe(visible);
+ });
+ },
+ );
+ });
+
+ describe('clicking when visible', () => {
+ beforeEach(() => {
+ wrapper = mountComponent({
+ props: {
+ issueType: TYPE_ISSUE,
+ issuableEmailAddress: 'mock-email-address',
+ },
+ movedMrSidebarEnabled: true,
+ });
+ });
+
+ it('shows toast message', () => {
+ findCopyEmailItem().vm.$emit('click');
+
+ expect(toast).toHaveBeenCalledWith('Email address copied');
+ });
+ });
+ });
});
diff --git a/spec/frontend/issues/show/components/new_header_actions_popover_spec.js b/spec/frontend/issues/show/components/new_header_actions_popover_spec.js
new file mode 100644
index 00000000000..71b7a3da1c3
--- /dev/null
+++ b/spec/frontend/issues/show/components/new_header_actions_popover_spec.js
@@ -0,0 +1,67 @@
+import { GlPopover } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import NewHeaderActionsPopover from '~/issues/show/components/new_header_actions_popover.vue';
+import { NEW_ACTIONS_POPOVER_KEY } from '~/issues/show/constants';
+import { TYPE_ISSUE } from '~/issues/constants';
+import * as utils from '~/lib/utils/common_utils';
+
+describe('NewHeaderActionsPopover', () => {
+ let wrapper;
+
+ const createComponent = ({ issueType = TYPE_ISSUE }) => {
+ wrapper = shallowMountExtended(NewHeaderActionsPopover, {
+ propsData: {
+ issueType,
+ },
+ stubs: {
+ GlPopover,
+ },
+ });
+ };
+
+ const findPopover = () => wrapper.findComponent(GlPopover);
+ const findConfirmButton = () => wrapper.findByTestId('confirm-button');
+
+ describe('without the popover cookie', () => {
+ beforeEach(() => {
+ utils.setCookie = jest.fn();
+
+ createComponent({});
+ });
+
+ it('renders the popover with correct text', () => {
+ expect(findPopover().exists()).toBe(true);
+ expect(findPopover().text()).toContain('issue actions');
+ });
+
+ it('does not call setCookie', () => {
+ expect(utils.setCookie).not.toHaveBeenCalled();
+ });
+
+ describe('when the confirm button is clicked', () => {
+ beforeEach(() => {
+ findConfirmButton().vm.$emit('click');
+ });
+
+ it('sets the popover cookie', () => {
+ expect(utils.setCookie).toHaveBeenCalledWith(NEW_ACTIONS_POPOVER_KEY, true);
+ });
+
+ it('hides the popover', () => {
+ expect(findPopover().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('with the popover cookie', () => {
+ beforeEach(() => {
+ jest.spyOn(utils, 'getCookie').mockReturnValue('true');
+
+ createComponent({});
+ });
+
+ it('does not render the popover', () => {
+ expect(findPopover().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/notes/components/note_form_spec.js b/spec/frontend/notes/components/note_form_spec.js
index 00f75b14e6b..e385c478dd6 100644
--- a/spec/frontend/notes/components/note_form_spec.js
+++ b/spec/frontend/notes/components/note_form_spec.js
@@ -1,4 +1,4 @@
-import { GlLink } from '@gitlab/ui';
+import { GlLink, GlFormCheckbox } from '@gitlab/ui';
import { nextTick } from 'vue';
import batchComments from '~/batch_comments/stores/modules/batch_comments';
import NoteForm from '~/notes/components/note_form.vue';
@@ -234,7 +234,7 @@ describe('issue_note_form component', () => {
});
it('shows resolve checkbox', () => {
- expect(wrapper.find('.js-resolve-checkbox').exists()).toBe(true);
+ expect(wrapper.findComponent(GlFormCheckbox).exists()).toBe(true);
});
it('hides resolve checkbox', async () => {
@@ -253,7 +253,7 @@ describe('issue_note_form component', () => {
},
});
- expect(wrapper.find('.js-resolve-checkbox').exists()).toBe(false);
+ expect(wrapper.findComponent(GlFormCheckbox).exists()).toBe(false);
});
it('hides actions for commits', async () => {
diff --git a/spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap b/spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap
index 213a732d59b..e9ee6ebdb5c 100644
--- a/spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap
+++ b/spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap
@@ -5,7 +5,6 @@ exports[`Registry Breadcrumb when is not rootRoute renders 1`] = `
aria-label="Breadcrumb"
class="gl-breadcrumbs"
>
-
<ol
class="breadcrumb gl-breadcrumb-list"
>
@@ -16,7 +15,10 @@ exports[`Registry Breadcrumb when is not rootRoute renders 1`] = `
class=""
target="_self"
>
-
+ <!---->
+ <span>
+
+ </span>
</a>
</li>
@@ -30,7 +32,10 @@ exports[`Registry Breadcrumb when is not rootRoute renders 1`] = `
href="#"
target="_self"
>
-
+ <!---->
+ <span>
+
+ </span>
</a>
</li>
@@ -44,7 +49,6 @@ exports[`Registry Breadcrumb when is rootRoute renders 1`] = `
aria-label="Breadcrumb"
class="gl-breadcrumbs"
>
-
<ol
class="breadcrumb gl-breadcrumb-list"
>
@@ -56,7 +60,10 @@ exports[`Registry Breadcrumb when is rootRoute renders 1`] = `
class=""
target="_self"
>
-
+ <!---->
+ <span>
+
+ </span>
</a>
</li>
diff --git a/spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js b/spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js
index d26ef7298ce..5e766e9a41c 100644
--- a/spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js
+++ b/spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js
@@ -29,6 +29,7 @@ describe('IssuableLockForm', () => {
const findEditForm = () => wrapper.findComponent(EditForm);
const findSidebarLockStatusTooltip = () =>
getBinding(findSidebarCollapseIcon().element, 'gl-tooltip');
+ const findIssuableLockClickable = () => wrapper.find('[data-testid="issuable-lock"]');
const initStore = (isLocked) => {
if (issuableType === ISSUABLE_TYPE_ISSUE) {
@@ -48,7 +49,7 @@ describe('IssuableLockForm', () => {
store.getters.getNoteableData.discussion_locked = isLocked;
};
- const createComponent = ({ props = {} }, movedMrSidebar = false) => {
+ const createComponent = ({ props = {}, movedMrSidebar = false }) => {
wrapper = shallowMount(IssuableLockForm, {
store,
provide: {
@@ -169,11 +170,27 @@ describe('IssuableLockForm', () => {
`('displays $message when merge request is $locked', async ({ locked, message }) => {
initStore(locked);
- createComponent({}, true);
+ createComponent({ movedMrSidebar: true });
await wrapper.find('.dropdown-item').trigger('click');
expect(toast).toHaveBeenCalledWith(message);
});
});
+
+ describe('moved_mr_sidebar flag', () => {
+ describe('when the flag is off', () => {
+ it('does not show the non editable lock status', () => {
+ createComponent({ movedMrSidebar: false });
+ expect(findIssuableLockClickable().exists()).toBe(false);
+ });
+ });
+
+ describe('when the flag is on', () => {
+ it('does not show the non editable lock status', () => {
+ createComponent({ movedMrSidebar: true });
+ expect(findIssuableLockClickable().exists()).toBe(true);
+ });
+ });
+ });
});
diff --git a/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js b/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js
index 128ef69e5f1..037b56d9904 100644
--- a/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js
+++ b/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js
@@ -1,4 +1,3 @@
-import { nextTick } from 'vue';
import { shallowMount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
import ReviewerAvatarLink from '~/sidebar/components/reviewers/reviewer_avatar_link.vue';
@@ -23,8 +22,9 @@ describe('UncollapsedReviewerList component', () => {
let wrapper;
const findAllRerequestButtons = () => wrapper.findAll('[data-testid="re-request-button"]');
- const findAllRerequestSuccessIcons = () => wrapper.findAll('[data-testid="re-request-success"]');
- const findAllReviewerApprovalIcons = () => wrapper.findAll('[data-testid="re-approved"]');
+ const findAllReviewerApprovalIcons = () => wrapper.findAll('[data-testid="approved"]');
+ const findAllReviewedNotApprovedIcons = () =>
+ wrapper.findAll('[data-testid="reviewed-not-approved"]');
const findAllReviewerAvatarLinks = () => wrapper.findAllComponents(ReviewerAvatarLink);
function createComponent(props = {}, glFeatures = {}) {
@@ -42,13 +42,6 @@ describe('UncollapsedReviewerList component', () => {
});
}
- const callRerequestCallback = async () => {
- const payload = wrapper.emitted('request-review')[0][0];
- // Call payload which is normally called by a parent component
- payload.callback(payload.userId, true);
- await nextTick();
- };
-
describe('single reviewer', () => {
const user = userDataMock();
@@ -71,13 +64,6 @@ describe('UncollapsedReviewerList component', () => {
expect(findAllRerequestButtons().at(0).props('loading')).toBe(true);
});
-
- it('renders re-request success icon', async () => {
- await findAllRerequestButtons().at(0).vm.$emit('click');
- await callRerequestCallback();
-
- expect(findAllRerequestSuccessIcons().at(0).exists()).toBe(true);
- });
});
describe('multiple reviewers', () => {
@@ -92,20 +78,32 @@ describe('UncollapsedReviewerList component', () => {
approved: true,
},
};
+ const user3 = {
+ ...user,
+ id: 3,
+ name: 'lizabeth-wilderman',
+ username: 'lizabeth-wilderman',
+ mergeRequestInteraction: {
+ ...user.mergeRequestInteraction,
+ approved: false,
+ reviewed: true,
+ },
+ };
beforeEach(() => {
createComponent({
- users: [user, user2],
+ users: [user, user2, user3],
});
});
- it('has both users', () => {
- expect(findAllReviewerAvatarLinks()).toHaveLength(2);
+ it('has three users', () => {
+ expect(findAllReviewerAvatarLinks()).toHaveLength(3);
});
- it('shows both users with avatar, and author name', () => {
+ it('shows all users with avatar, and author name', () => {
expect(wrapper.text()).toContain(user.name);
expect(wrapper.text()).toContain(user2.name);
+ expect(wrapper.text()).toContain(user3.name);
});
it('renders approval icon', () => {
@@ -118,21 +116,19 @@ describe('UncollapsedReviewerList component', () => {
expect(icon.attributes('title')).toBe('Approved by @hello-world');
});
+ it('shows that lizabeth-wilderman reviewed but did not approve', () => {
+ const icon = findAllReviewedNotApprovedIcons().at(1);
+
+ expect(icon.attributes('title')).toBe('Reviewed by @lizabeth-wilderman but not yet approved');
+ });
+
it('renders re-request loading icon', async () => {
await findAllRerequestButtons().at(1).vm.$emit('click');
const allRerequestButtons = findAllRerequestButtons();
- expect(allRerequestButtons).toHaveLength(2);
+ expect(allRerequestButtons).toHaveLength(3);
expect(allRerequestButtons.at(1).props('loading')).toBe(true);
});
-
- it('renders re-request success icon', async () => {
- await findAllRerequestButtons().at(1).vm.$emit('click');
- await callRerequestCallback();
-
- expect(findAllRerequestButtons()).toHaveLength(1);
- expect(findAllRerequestSuccessIcons()).toHaveLength(1);
- });
});
});
diff --git a/spec/graphql/types/work_items/widget_interface_spec.rb b/spec/graphql/types/work_items/widget_interface_spec.rb
index 045c1620815..d955ec5023e 100644
--- a/spec/graphql/types/work_items/widget_interface_spec.rb
+++ b/spec/graphql/types/work_items/widget_interface_spec.rb
@@ -15,13 +15,14 @@ RSpec.describe Types::WorkItems::WidgetInterface do
using RSpec::Parameterized::TableSyntax
where(:widget_class, :widget_type_name) do
- WorkItems::Widgets::Description | Types::WorkItems::Widgets::DescriptionType
- WorkItems::Widgets::Hierarchy | Types::WorkItems::Widgets::HierarchyType
- WorkItems::Widgets::Assignees | Types::WorkItems::Widgets::AssigneesType
- WorkItems::Widgets::Labels | Types::WorkItems::Widgets::LabelsType
- WorkItems::Widgets::Notes | Types::WorkItems::Widgets::NotesType
- WorkItems::Widgets::Notifications | Types::WorkItems::Widgets::NotificationsType
+ WorkItems::Widgets::Description | Types::WorkItems::Widgets::DescriptionType
+ WorkItems::Widgets::Hierarchy | Types::WorkItems::Widgets::HierarchyType
+ WorkItems::Widgets::Assignees | Types::WorkItems::Widgets::AssigneesType
+ WorkItems::Widgets::Labels | Types::WorkItems::Widgets::LabelsType
+ WorkItems::Widgets::Notes | Types::WorkItems::Widgets::NotesType
+ WorkItems::Widgets::Notifications | Types::WorkItems::Widgets::NotificationsType
WorkItems::Widgets::CurrentUserTodos | Types::WorkItems::Widgets::CurrentUserTodosType
+ WorkItems::Widgets::AwardEmoji | Types::WorkItems::Widgets::AwardEmojiType
end
with_them do
diff --git a/spec/graphql/types/work_items/widgets/award_emoji_type_spec.rb b/spec/graphql/types/work_items/widgets/award_emoji_type_spec.rb
new file mode 100644
index 00000000000..493e628ac83
--- /dev/null
+++ b/spec/graphql/types/work_items/widgets/award_emoji_type_spec.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::WorkItems::Widgets::AwardEmojiType, feature_category: :team_planning do
+ it 'exposes the expected fields' do
+ expected_fields = %i[award_emoji downvotes upvotes type]
+
+ expect(described_class.graphql_name).to eq('WorkItemWidgetAwardEmoji')
+ expect(described_class).to have_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb
index d940c696fb3..38cbb5a1d66 100644
--- a/spec/helpers/issues_helper_spec.rb
+++ b/spec/helpers/issues_helper_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe IssuesHelper do
+ include Features::MergeRequestHelpers
+
let_it_be(:project) { create(:project) }
let_it_be_with_reload(:issue) { create(:issue, project: project) }
@@ -235,10 +237,13 @@ RSpec.describe IssuesHelper do
describe '#issue_header_actions_data' do
let(:current_user) { create(:user) }
+ let(:merge_request) { create(:merge_request, :opened, source_project: project, author: current_user) }
+ let(:issuable_sidebar_issue) { serialize_issuable_sidebar(current_user, project, merge_request) }
before do
allow(helper).to receive(:current_user).and_return(current_user)
allow(helper).to receive(:can?).and_return(true)
+ allow(helper).to receive(:issuable_sidebar).and_return(issuable_sidebar_issue)
end
it 'returns expected result' do
@@ -257,10 +262,11 @@ RSpec.describe IssuesHelper do
report_abuse_path: add_category_abuse_reports_path,
reported_user_id: issue.author.id,
reported_from_url: issue_url(issue),
- submit_as_spam_path: mark_as_spam_project_issue_path(project, issue)
+ submit_as_spam_path: mark_as_spam_project_issue_path(project, issue),
+ issuable_email_address: issuable_sidebar_issue[:create_note_email]
}
- expect(helper.issue_header_actions_data(project, issue, current_user)).to include(expected)
+ expect(helper.issue_header_actions_data(project, issue, current_user, issuable_sidebar_issue)).to include(expected)
end
end
diff --git a/spec/migrations/20230228142350_add_notifications_work_item_widget_spec.rb b/spec/migrations/20230228142350_add_notifications_work_item_widget_spec.rb
index 065b6d00ddb..7161ca35edd 100644
--- a/spec/migrations/20230228142350_add_notifications_work_item_widget_spec.rb
+++ b/spec/migrations/20230228142350_add_notifications_work_item_widget_spec.rb
@@ -4,24 +4,5 @@ require 'spec_helper'
require_migration!
RSpec.describe AddNotificationsWorkItemWidget, :migration, feature_category: :team_planning do
- let(:migration) { described_class.new }
- let(:work_item_definitions) { table(:work_item_widget_definitions) }
-
- describe '#up' do
- it 'creates notifications widget definition in all types' do
- work_item_definitions.where(name: 'Notifications').delete_all
-
- expect { migrate! }.to change { work_item_definitions.count }.by(7)
- expect(work_item_definitions.all.pluck(:name)).to include('Notifications')
- end
- end
-
- describe '#down' do
- it 'removes definitions for notifications widget' do
- migrate!
-
- expect { migration.down }.to change { work_item_definitions.count }.by(-7)
- expect(work_item_definitions.all.pluck(:name)).not_to include('Notifications')
- end
- end
+ it_behaves_like 'migration that adds widget to work items definitions', widget_name: 'Notifications'
end
diff --git a/spec/migrations/20230317162059_add_current_user_todos_work_item_widget_spec.rb b/spec/migrations/20230317162059_add_current_user_todos_work_item_widget_spec.rb
index f044b15fa6b..1df80a519f2 100644
--- a/spec/migrations/20230317162059_add_current_user_todos_work_item_widget_spec.rb
+++ b/spec/migrations/20230317162059_add_current_user_todos_work_item_widget_spec.rb
@@ -4,24 +4,5 @@ require 'spec_helper'
require_migration!
RSpec.describe AddCurrentUserTodosWorkItemWidget, :migration, feature_category: :team_planning do
- let(:migration) { described_class.new }
- let(:work_item_definitions) { table(:work_item_widget_definitions) }
-
- describe '#up' do
- it 'creates notifications widget definition in all types' do
- work_item_definitions.where(name: 'Current user todos').delete_all
-
- expect { migrate! }.to change { work_item_definitions.count }.by(7)
- expect(work_item_definitions.all.pluck(:name)).to include('Current user todos')
- end
- end
-
- describe '#down' do
- it 'removes definitions for notifications widget' do
- migrate!
-
- expect { migration.down }.to change { work_item_definitions.count }.by(-7)
- expect(work_item_definitions.all.pluck(:name)).not_to include('Current user todos')
- end
- end
+ it_behaves_like 'migration that adds widget to work items definitions', widget_name: 'Current user todos'
end
diff --git a/spec/migrations/20230323101138_add_award_emoji_work_item_widget_spec.rb b/spec/migrations/20230323101138_add_award_emoji_work_item_widget_spec.rb
new file mode 100644
index 00000000000..16a205c5da5
--- /dev/null
+++ b/spec/migrations/20230323101138_add_award_emoji_work_item_widget_spec.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe AddAwardEmojiWorkItemWidget, :migration, feature_category: :team_planning do
+ it_behaves_like 'migration that adds widget to work items definitions', widget_name: 'Award emoji'
+end
diff --git a/spec/migrations/20230328100534_truncate_error_tracking_tables_spec.rb b/spec/migrations/20230328100534_truncate_error_tracking_tables_spec.rb
new file mode 100644
index 00000000000..efbbe22fd1b
--- /dev/null
+++ b/spec/migrations/20230328100534_truncate_error_tracking_tables_spec.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe TruncateErrorTrackingTables, :migration, feature_category: :redis do
+ let(:migration) { described_class.new }
+
+ context 'when on GitLab.com' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(true)
+ end
+
+ context 'when using Main db' do
+ it 'truncates the table' do
+ expect(described_class.connection).to receive(:execute).with('TRUNCATE table error_tracking_errors CASCADE')
+
+ migration.up
+ end
+ end
+
+ context 'when uses CI db', migration: :gitlab_ci do
+ before do
+ skip_if_multiple_databases_not_setup(:ci)
+ end
+
+ it 'does not truncate the table' do
+ expect(described_class.connection).not_to receive(:execute).with('TRUNCATE table error_tracking_errors CASCADE')
+
+ migration.up
+ end
+ end
+ end
+
+ context 'when on self-managed' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(false)
+ end
+
+ context 'when using Main db' do
+ it 'does not truncate the table' do
+ expect(described_class.connection).not_to receive(:execute).with('TRUNCATE table error_tracking_errors CASCADE')
+
+ migration.up
+ end
+ end
+
+ context 'when uses CI db', migration: :gitlab_ci do
+ it 'does not truncate the table' do
+ expect(described_class.connection).not_to receive(:execute).with('TRUNCATE table error_tracking_errors CASCADE')
+
+ migration.up
+ end
+ end
+ end
+end
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index c1de8125c0d..ed2bf26e129 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -1666,6 +1666,32 @@ RSpec.describe Note do
end
end
end
+
+ describe '.without_hidden' do
+ subject { described_class.without_hidden }
+
+ context 'when a note with a banned author exists' do
+ let_it_be(:banned_user) { create(:banned_user).user }
+ let_it_be(:banned_note) { create(:note, author: banned_user) }
+
+ context 'when the :hidden_notes feature is disabled' do
+ before do
+ stub_feature_flags(hidden_notes: false)
+ end
+
+ it { is_expected.to include(banned_note, note1) }
+ end
+
+ context 'when the :hidden_notes feature is enabled' do
+ before do
+ stub_feature_flags(hidden_notes: true)
+ end
+
+ it { is_expected.not_to include(banned_note) }
+ it { is_expected.to include(note1) }
+ end
+ end
+ end
end
describe 'banzai_render_context' do
diff --git a/spec/models/work_items/widget_definition_spec.rb b/spec/models/work_items/widget_definition_spec.rb
index f4d132bec52..a33e08a1bf2 100644
--- a/spec/models/work_items/widget_definition_spec.rb
+++ b/spec/models/work_items/widget_definition_spec.rb
@@ -13,7 +13,8 @@ RSpec.describe WorkItems::WidgetDefinition, feature_category: :team_planning do
::WorkItems::Widgets::Milestone,
::WorkItems::Widgets::Notes,
::WorkItems::Widgets::Notifications,
- ::WorkItems::Widgets::CurrentUserTodos
+ ::WorkItems::Widgets::CurrentUserTodos,
+ ::WorkItems::Widgets::AwardEmoji
]
if Gitlab.ee?
diff --git a/spec/models/work_items/widgets/award_emoji_spec.rb b/spec/models/work_items/widgets/award_emoji_spec.rb
new file mode 100644
index 00000000000..bb61aa41669
--- /dev/null
+++ b/spec/models/work_items/widgets/award_emoji_spec.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe WorkItems::Widgets::AwardEmoji, feature_category: :team_planning do
+ let_it_be(:work_item) { create(:work_item) }
+ let_it_be(:emoji1) { create(:award_emoji, name: 'star', awardable: work_item) }
+ let_it_be(:emoji2) { create(:award_emoji, :upvote, awardable: work_item) }
+ let_it_be(:emoji3) { create(:award_emoji, :downvote, awardable: work_item) }
+
+ describe '.type' do
+ it { expect(described_class.type).to eq(:award_emoji) }
+ end
+
+ describe '#type' do
+ it { expect(described_class.new(work_item).type).to eq(:award_emoji) }
+ end
+
+ describe '#downvotes' do
+ it { expect(described_class.new(work_item).downvotes).to eq(1) }
+ end
+
+ describe '#upvotes' do
+ it { expect(described_class.new(work_item).upvotes).to eq(1) }
+ end
+
+ describe '#award_emoji' do
+ it { expect(described_class.new(work_item).award_emoji).to match_array([emoji1, emoji2, emoji3]) }
+ end
+end
diff --git a/spec/requests/api/graphql/project/work_items_spec.rb b/spec/requests/api/graphql/project/work_items_spec.rb
index d5dd12de63e..b792505374e 100644
--- a/spec/requests/api/graphql/project/work_items_spec.rb
+++ b/spec/requests/api/graphql/project/work_items_spec.rb
@@ -341,6 +341,51 @@ RSpec.describe 'getting a work item list for a project', feature_category: :team
end
end
+ context 'when fetching work item award emoji widget' do
+ let(:fields) do
+ <<~GRAPHQL
+ nodes {
+ widgets {
+ type
+ ... on WorkItemWidgetAwardEmoji {
+ awardEmoji {
+ nodes {
+ name
+ emoji
+ user { id }
+ }
+ }
+ upvotes
+ downvotes
+ }
+ }
+ }
+ GRAPHQL
+ end
+
+ before do
+ create(:award_emoji, name: 'star', user: current_user, awardable: item1)
+ create(:award_emoji, :upvote, awardable: item1)
+ create(:award_emoji, :downvote, awardable: item1)
+ end
+
+ it 'executes limited number of N+1 queries', :use_sql_query_cache do
+ control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ post_graphql(query, current_user: current_user)
+ end
+
+ create_list(:work_item, 2, project: project) do |item|
+ create(:award_emoji, name: 'rocket', awardable: item)
+ create_list(:award_emoji, 2, :upvote, awardable: item)
+ create_list(:award_emoji, 2, :downvote, awardable: item)
+ end
+
+ expect { post_graphql(query, current_user: current_user) }
+ .not_to exceed_all_query_limit(control)
+ expect_graphql_errors_to_be_empty
+ end
+ end
+
def item_ids
graphql_dig_at(items_data, :node, :id)
end
diff --git a/spec/requests/api/graphql/work_item_spec.rb b/spec/requests/api/graphql/work_item_spec.rb
index fe6f75548a5..a89c2268208 100644
--- a/spec/requests/api/graphql/work_item_spec.rb
+++ b/spec/requests/api/graphql/work_item_spec.rb
@@ -486,6 +486,48 @@ RSpec.describe 'Query.work_item(id)', feature_category: :team_planning do
end
end
end
+
+ describe 'award emoji widget' do
+ let_it_be(:emoji) { create(:award_emoji, name: 'star', awardable: work_item) }
+ let_it_be(:upvote) { create(:award_emoji, :upvote, awardable: work_item) }
+ let_it_be(:downvote) { create(:award_emoji, :downvote, awardable: work_item) }
+
+ let(:work_item_fields) do
+ <<~GRAPHQL
+ id
+ widgets {
+ type
+ ... on WorkItemWidgetAwardEmoji {
+ upvotes
+ downvotes
+ awardEmoji {
+ nodes {
+ name
+ }
+ }
+ }
+ }
+ GRAPHQL
+ end
+
+ it 'returns widget information' do
+ expect(work_item_data).to include(
+ 'id' => work_item.to_gid.to_s,
+ 'widgets' => include(
+ hash_including(
+ 'type' => 'AWARD_EMOJI',
+ 'upvotes' => work_item.upvotes,
+ 'downvotes' => work_item.downvotes,
+ 'awardEmoji' => {
+ 'nodes' => match_array(
+ [emoji, upvote, downvote].map { |e| { 'name' => e.name } }
+ )
+ }
+ )
+ )
+ )
+ end
+ end
end
context 'when an Issue Global ID is provided' do
diff --git a/spec/scripts/generate_rspec_pipeline_spec.rb b/spec/scripts/generate_rspec_pipeline_spec.rb
index b3eaf9e9127..91b5739cf63 100644
--- a/spec/scripts/generate_rspec_pipeline_spec.rb
+++ b/spec/scripts/generate_rspec_pipeline_spec.rb
@@ -13,42 +13,49 @@ RSpec.describe GenerateRspecPipeline, :silence_stdout, feature_category: :toolin
"spec/lib/gitlab/background_migration/a_spec.rb spec/lib/gitlab/background_migration/b_spec.rb " \
"spec/models/a_spec.rb spec/models/b_spec.rb " \
"spec/controllers/a_spec.rb spec/controllers/b_spec.rb " \
- "spec/features/a_spec.rb spec/features/b_spec.rb"
+ "spec/features/a_spec.rb spec/features/b_spec.rb " \
+ "ee/spec/features/a_spec.rb"
end
let(:pipeline_template) { Tempfile.new(['pipeline_template', '.yml.erb']) }
let(:pipeline_template_content) do
<<~YAML
- <% if rspec_files_per_test_level[:migration][:files].size > 0 %>
+ <% if test_suite_prefix.nil? && rspec_files_per_test_level[:migration][:files].size > 0 %>
rspec migration:
<% if rspec_files_per_test_level[:migration][:parallelization] > 1 %>
parallel: <%= rspec_files_per_test_level[:migration][:parallelization] %>
<% end %>
<% end %>
- <% if rspec_files_per_test_level[:background_migration][:files].size > 0 %>
+ <% if test_suite_prefix.nil? && rspec_files_per_test_level[:background_migration][:files].size > 0 %>
rspec background_migration:
<% if rspec_files_per_test_level[:background_migration][:parallelization] > 1 %>
parallel: <%= rspec_files_per_test_level[:background_migration][:parallelization] %>
<% end %>
<% end %>
- <% if rspec_files_per_test_level[:unit][:files].size > 0 %>
+ <% if test_suite_prefix.nil? && rspec_files_per_test_level[:unit][:files].size > 0 %>
rspec unit:
<% if rspec_files_per_test_level[:unit][:parallelization] > 1 %>
parallel: <%= rspec_files_per_test_level[:unit][:parallelization] %>
<% end %>
<% end %>
- <% if rspec_files_per_test_level[:integration][:files].size > 0 %>
+ <% if test_suite_prefix.nil? && rspec_files_per_test_level[:integration][:files].size > 0 %>
rspec integration:
<% if rspec_files_per_test_level[:integration][:parallelization] > 1 %>
parallel: <%= rspec_files_per_test_level[:integration][:parallelization] %>
<% end %>
<% end %>
- <% if rspec_files_per_test_level[:system][:files].size > 0 %>
+ <% if test_suite_prefix.nil? && rspec_files_per_test_level[:system][:files].size > 0 %>
rspec system:
<% if rspec_files_per_test_level[:system][:parallelization] > 1 %>
parallel: <%= rspec_files_per_test_level[:system][:parallelization] %>
<% end %>
<% end %>
+ <% if test_suite_prefix == 'ee/' && rspec_files_per_test_level[:system][:files].size > 0 %>
+ rspec-ee system:
+ <% if rspec_files_per_test_level[:system][:parallelization] > 1 %>
+ parallel: <%= rspec_files_per_test_level[:system][:parallelization] %>
+ <% end %>
+ <% end %>
YAML
end
@@ -65,7 +72,8 @@ RSpec.describe GenerateRspecPipeline, :silence_stdout, feature_category: :toolin
"spec/controllers/a_spec.rb": 60.2,
"spec/controllers/ab_spec.rb": 180.4,
"spec/features/a_spec.rb": 360.1,
- "spec/features/b_spec.rb": 180.5
+ "spec/features/b_spec.rb": 180.5,
+ "ee/spec/features/a_spec.rb": 180.5
}
JSON
end
@@ -177,6 +185,53 @@ RSpec.describe GenerateRspecPipeline, :silence_stdout, feature_category: :toolin
end
end
+ context 'when test_suite_prefix is given' do
+ subject do
+ described_class.new(
+ rspec_files_path: rspec_files.path,
+ pipeline_template_path: pipeline_template.path,
+ knapsack_report_path: knapsack_report.path,
+ test_suite_prefix: 'ee/'
+ )
+ end
+
+ it 'generates the pipeline config based on the test_suite_prefix' do
+ subject.generate!
+
+ expect(File.read("#{pipeline_template.path}.yml"))
+ .to eq("rspec-ee system:")
+ end
+ end
+
+ context 'when generated_pipeline_path is given' do
+ let(:custom_pipeline_filename) { Tempfile.new(['custom_pipeline_filename', '.yml']) }
+
+ around do |example|
+ example.run
+ ensure
+ custom_pipeline_filename.close
+ custom_pipeline_filename.unlink
+ end
+
+ subject do
+ described_class.new(
+ rspec_files_path: rspec_files.path,
+ pipeline_template_path: pipeline_template.path,
+ generated_pipeline_path: custom_pipeline_filename.path
+ )
+ end
+
+ it 'writes the pipeline config in the given generated_pipeline_path' do
+ subject.generate!
+
+ expect(File.read(custom_pipeline_filename.path))
+ .to eq(
+ "rspec migration:\nrspec background_migration:\nrspec unit:\n" \
+ "rspec integration:\nrspec system:"
+ )
+ end
+ end
+
context 'when rspec_files does not exist' do
subject { described_class.new(rspec_files_path: nil, pipeline_template_path: pipeline_template.path) }
diff --git a/spec/support/shared_examples/migrations/add_work_item_widget_shared_examples.rb b/spec/support/shared_examples/migrations/add_work_item_widget_shared_examples.rb
new file mode 100644
index 00000000000..28eac52256f
--- /dev/null
+++ b/spec/support/shared_examples/migrations/add_work_item_widget_shared_examples.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'migration that adds widget to work items definitions' do |widget_name:|
+ let(:migration) { described_class.new }
+ let(:work_item_definitions) { table(:work_item_widget_definitions) }
+
+ describe '#up' do
+ it "creates widget definition in all types" do
+ work_item_definitions.where(name: widget_name).delete_all
+
+ expect { migrate! }.to change { work_item_definitions.count }.by(7)
+ expect(work_item_definitions.all.pluck(:name)).to include(widget_name)
+ end
+
+ it 'logs a warning if the type is missing' do
+ allow(described_class::WorkItemType).to receive(:find_by_name_and_namespace_id).and_call_original
+ allow(described_class::WorkItemType).to receive(:find_by_name_and_namespace_id)
+ .with('Issue', nil).and_return(nil)
+
+ expect(Gitlab::AppLogger).to receive(:warn).with('type Issue is missing, not adding widget')
+ migrate!
+ end
+ end
+
+ describe '#down' do
+ it "removes definitions for widget" do
+ migrate!
+
+ expect { migration.down }.to change { work_item_definitions.count }.by(-7)
+ expect(work_item_definitions.all.pluck(:name)).not_to include(widget_name)
+ end
+ end
+end
diff --git a/spec/tooling/lib/tooling/find_changes_spec.rb b/spec/tooling/lib/tooling/find_changes_spec.rb
index 3616732e328..5932eb5e919 100644
--- a/spec/tooling/lib/tooling/find_changes_spec.rb
+++ b/spec/tooling/lib/tooling/find_changes_spec.rb
@@ -3,57 +3,66 @@
require_relative '../../../../tooling/lib/tooling/find_changes'
require_relative '../../../support/helpers/stub_env'
require 'json'
+require 'tempfile'
RSpec.describe Tooling::FindChanges, feature_category: :tooling do
include StubENV
+ attr_accessor :changed_files_file, :predictive_tests_file, :frontend_fixtures_mapping_file
+
let(:instance) do
- described_class.new(
- output_file: output_file,
- matched_tests_file: matched_tests_file,
- frontend_fixtures_mapping_path: frontend_fixtures_mapping_path
- )
+ described_class.new(changed_files_pathname, predictive_tests_pathname, frontend_fixtures_mapping_pathname)
end
- let(:gitlab_client) { double('GitLab') } # rubocop:disable RSpec/VerifiedDoubles
- let(:output_file) { 'output.txt' }
- let(:output_file_content) { 'first_file second_file' }
- let(:matched_tests_file) { 'matched_tests.txt' }
- let(:frontend_fixtures_mapping_path) { 'frontend_fixtures_mapping.json' }
- let(:file_changes) { ['file1.rb', 'file2.rb'] }
+ let(:changed_files_pathname) { changed_files_file.path }
+ let(:predictive_tests_pathname) { predictive_tests_file.path }
+ let(:frontend_fixtures_mapping_pathname) { frontend_fixtures_mapping_file.path }
+ let(:gitlab_client) { double('GitLab') } # rubocop:disable RSpec/VerifiedDoubles
+
+ around do |example|
+ self.changed_files_file = Tempfile.new('changed_files_file')
+ self.predictive_tests_file = Tempfile.new('predictive_tests_file')
+ self.frontend_fixtures_mapping_file = Tempfile.new('frontend_fixtures_mapping_file')
+
+ # See https://ruby-doc.org/stdlib-1.9.3/libdoc/tempfile/rdoc/
+ # Tempfile.html#class-Tempfile-label-Explicit+close
+ begin
+ example.run
+ ensure
+ frontend_fixtures_mapping_file.close
+ frontend_fixtures_mapping_file.unlink
+ predictive_tests_file.close
+ predictive_tests_file.unlink
+ changed_files_file.close
+ changed_files_file.unlink
+ end
+ end
before do
stub_env(
'CI_API_V4_URL' => 'gitlab_api_url',
'CI_MERGE_REQUEST_IID' => '1234',
'CI_MERGE_REQUEST_PROJECT_PATH' => 'dummy-project',
- 'PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE' => 'dummy-token',
- 'RSPEC_TESTS_MAPPING_PATH' => '/tmp/does-not-exist.out'
+ 'PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE' => 'dummy-token'
)
allow(instance).to receive(:gitlab).and_return(gitlab_client)
- allow(File).to receive(:exist?).and_call_original
- allow(File).to receive(:read).and_call_original
- allow(File).to receive(:write)
end
describe '#execute' do
subject { instance.execute }
- context 'when there is no output file' do
- let(:output_file) { nil }
+ context 'when there is no changed files file' do
+ let(:changed_files_pathname) { nil }
it 'raises an ArgumentError' do
- expect { subject }.to raise_error(ArgumentError, "An path to an output file must be given as first argument.")
+ expect { subject }.to raise_error(
+ ArgumentError, "A path to the changed files file must be given as first argument."
+ )
end
end
- context 'when an output file is provided' do
- before do
- allow(File).to receive(:exist?).with(output_file).and_return(true)
- allow(File).to receive(:read).with(output_file).and_return(output_file_content)
- end
-
+ context 'when an changed files file is provided' do
it 'does not call GitLab API to retrieve the MR diff' do
expect(gitlab_client).not_to receive(:merge_request_changes)
@@ -61,64 +70,57 @@ RSpec.describe Tooling::FindChanges, feature_category: :tooling do
end
context 'when there are no file changes' do
- let(:output_file_content) { '' }
-
- it 'writes an empty string to output file' do
- expect(File).to receive(:write).with(output_file, '')
-
- subject
+ it 'writes an empty string to changed files file' do
+ expect { subject }.not_to change { File.read(changed_files_pathname) }
end
end
context 'when there are file changes' do
- let(:output_file_content) { 'first_file_changed second_file_changed' }
+ before do
+ File.write(changed_files_pathname, changed_files_file_content)
+ end
- it 'writes file changes to output file' do
- expect(File).to receive(:write).with(output_file, output_file_content)
+ let(:changed_files_file_content) { 'first_file_changed second_file_changed' }
- subject
+ # This is because we don't have frontend fixture mappings: we will just write the same data that we read.
+ it 'does not change the changed files file' do
+ expect { subject }.not_to change { File.read(changed_files_pathname) }
end
end
context 'when there is no matched tests file' do
- let(:matched_tests_file) { '' }
-
- it 'does not add frontend fixtures mapping to the output file' do
- expect(File).to receive(:write).with(output_file, output_file_content)
+ let(:predictive_tests_pathname) { nil }
- subject
+ it 'does not add frontend fixtures mapping to the changed files file' do
+ expect { subject }.not_to change { File.read(changed_files_pathname) }
end
end
context 'when there is no frontend fixture files' do
- let(:frontend_fixtures_mapping_path) { '' }
+ let(:frontend_fixtures_mapping_pathname) { nil }
- it 'does not add frontend fixtures mapping to the output file' do
- expect(File).to receive(:write).with(output_file, output_file_content)
-
- subject
+ it 'does not add frontend fixtures mapping to the changed files file' do
+ expect { subject }.not_to change { File.read(changed_files_pathname) }
end
end
context 'when the matched tests file and frontend fixture files are provided' do
before do
- allow(File).to receive(:exist?).with(matched_tests_file).and_return(true)
- allow(File).to receive(:exist?).with(frontend_fixtures_mapping_path).and_return(true)
-
- allow(File).to receive(:read).with(matched_tests_file).and_return(matched_tests)
- allow(File).to receive(:read).with(frontend_fixtures_mapping_path).and_return(frontend_fixtures_mapping_json)
+ File.write(predictive_tests_pathname, matched_tests)
+ File.write(frontend_fixtures_mapping_pathname, frontend_fixtures_mapping_json)
+ File.write(changed_files_pathname, changed_files_file_content)
end
+ let(:changed_files_file_content) { '' }
+
context 'when there are no mappings for the matched tests' do
let(:matched_tests) { 'match_spec1 match_spec_2' }
let(:frontend_fixtures_mapping_json) do
{ other_spec: ['other_mapping'] }.to_json
end
- it 'does not add frontend fixtures mapping to the output file' do
- expect(File).to receive(:write).with(output_file, output_file_content)
-
- subject
+ it 'does not change the changed files file' do
+ expect { subject }.not_to change { File.read(changed_files_pathname) }
end
end
@@ -129,10 +131,20 @@ RSpec.describe Tooling::FindChanges, feature_category: :tooling do
{ match_spec1: spec_mappings }.to_json
end
- it 'adds the frontend fixtures mappings to the output file' do
- expect(File).to receive(:write).with(output_file, "#{output_file_content} #{spec_mappings.join(' ')}")
+ context 'when the changed files file is initially empty' do
+ it 'adds the frontend fixtures mappings to the changed files file' do
+ expect { subject }.to change { File.read(changed_files_pathname) }.from('').to(spec_mappings.join(' '))
+ end
+ end
+
+ context 'when the changed files file is initially not empty' do
+ let(:changed_files_file_content) { 'initial_content1 initial_content2' }
- subject
+ it 'adds the frontend fixtures mappings to the changed files file' do
+ expect { subject }.to change { File.read(changed_files_pathname) }
+ .from(changed_files_file_content)
+ .to("#{changed_files_file_content} #{spec_mappings.join(' ')}")
+ end
end
end
end
@@ -155,15 +167,9 @@ RSpec.describe Tooling::FindChanges, feature_category: :tooling do
end
context 'when a file is passed as an argument' do
- let(:output_file) { 'output_file.out' }
-
- it 'does not read the output file' do
- expect(File).not_to receive(:read).with(output_file)
-
- subject
- end
+ let(:changed_files_pathname) { 'does-not-exist.out' }
- it 'calls GitLab API anyways' do
+ it 'calls GitLab API' do
expect(gitlab_client).to receive(:merge_request_changes)
.with('dummy-project', '1234')
diff --git a/spec/tooling/lib/tooling/find_tests_spec.rb b/spec/tooling/lib/tooling/find_tests_spec.rb
index e531fb9548e..905f81c4bbd 100644
--- a/spec/tooling/lib/tooling/find_tests_spec.rb
+++ b/spec/tooling/lib/tooling/find_tests_spec.rb
@@ -7,27 +7,29 @@ require_relative '../../../support/helpers/stub_env'
RSpec.describe Tooling::FindTests, feature_category: :tooling do
include StubENV
- attr_accessor :changes_file, :matching_tests_paths
+ attr_accessor :changed_files_file, :predictive_tests_file
- let(:instance) { described_class.new(changes_file, matching_tests_paths) }
+ let(:instance) { described_class.new(changed_files_pathname, predictive_tests_pathname) }
let(:mock_test_file_finder) { instance_double(TestFileFinder::FileFinder) }
let(:new_matching_tests) { ["new_matching_spec.rb"] }
- let(:changes_file_content) { "changed_file1 changed_file2" }
- let(:matching_tests_paths_content) { "previously_matching_spec.rb" }
+ let(:changed_files_pathname) { changed_files_file.path }
+ let(:predictive_tests_pathname) { predictive_tests_file.path }
+ let(:changed_files_content) { "changed_file1 changed_file2" }
+ let(:predictive_tests_content) { "previously_matching_spec.rb" }
around do |example|
- self.changes_file = Tempfile.new('changes')
- self.matching_tests_paths = Tempfile.new('matching_tests')
+ self.changed_files_file = Tempfile.new('changed_files_file')
+ self.predictive_tests_file = Tempfile.new('predictive_tests_file')
# See https://ruby-doc.org/stdlib-1.9.3/libdoc/tempfile/rdoc/
# Tempfile.html#class-Tempfile-label-Explicit+close
begin
example.run
ensure
- changes_file.close
- matching_tests_paths.close
- changes_file.unlink
- matching_tests_paths.unlink
+ changed_files_file.close
+ predictive_tests_file.close
+ changed_files_file.unlink
+ predictive_tests_file.unlink
end
end
@@ -42,44 +44,44 @@ RSpec.describe Tooling::FindTests, feature_category: :tooling do
)
# We write into the temp files initially, to later check how the code modified those files
- File.write(changes_file, changes_file_content)
- File.write(matching_tests_paths, matching_tests_paths_content)
+ File.write(changed_files_pathname, changed_files_content)
+ File.write(predictive_tests_pathname, predictive_tests_content)
end
describe '#execute' do
subject { instance.execute }
- context 'when the matching_tests_paths file does not exist' do
- let(:instance) { described_class.new(non_existing_output_file, matching_tests_paths) }
- let(:non_existing_output_file) { 'tmp/another_file.out' }
+ context 'when the predictive_tests_pathname file does not exist' do
+ let(:instance) { described_class.new(non_existing_output_pathname, predictive_tests_pathname) }
+ let(:non_existing_output_pathname) { 'tmp/another_file.out' }
around do |example|
example.run
ensure
- FileUtils.rm_rf(non_existing_output_file)
+ FileUtils.rm_rf(non_existing_output_pathname)
end
it 'creates the file' do
- expect { subject }.to change { File.exist?(non_existing_output_file) }.from(false).to(true)
+ expect { subject }.to change { File.exist?(non_existing_output_pathname) }.from(false).to(true)
end
end
- context 'when the matching_tests_paths file already exists' do
+ context 'when the predictive_tests_pathname file already exists' do
it 'does not create an empty file' do
- expect(File).not_to receive(:write).with(matching_tests_paths, '')
+ expect(File).not_to receive(:write).with(predictive_tests_pathname, '')
subject
end
end
it 'does not modify the content of the input file' do
- expect { subject }.not_to change { File.read(changes_file) }
+ expect { subject }.not_to change { File.read(changed_files_pathname) }
end
it 'does not overwrite the output file' do
- expect { subject }.to change { File.read(matching_tests_paths) }
- .from(matching_tests_paths_content)
- .to("#{matching_tests_paths_content} #{new_matching_tests.uniq.join(' ')}")
+ expect { subject }.to change { File.read(predictive_tests_pathname) }
+ .from(predictive_tests_content)
+ .to("#{predictive_tests_content} #{new_matching_tests.uniq.join(' ')}")
end
it 'loads the tests.yml file with a pattern matching mapping' do
@@ -148,8 +150,8 @@ RSpec.describe Tooling::FindTests, feature_category: :tooling do
it 'writes uniquely matching specs to the output' do
subject
- expect(File.read(matching_tests_paths).split(' ')).to match_array(
- matching_tests_paths_content.split(' ') + new_matching_tests.uniq
+ expect(File.read(predictive_tests_pathname).split(' ')).to match_array(
+ predictive_tests_content.split(' ') + new_matching_tests.uniq
)
end
end
diff --git a/spec/tooling/lib/tooling/helpers/file_handler_spec.rb b/spec/tooling/lib/tooling/helpers/file_handler_spec.rb
index 7c8310c4bd9..d6f68baeb90 100644
--- a/spec/tooling/lib/tooling/helpers/file_handler_spec.rb
+++ b/spec/tooling/lib/tooling/helpers/file_handler_spec.rb
@@ -42,18 +42,18 @@ RSpec.describe Tooling::Helpers::FileHandler, feature_category: :tooling do
subject { instance.read_array_from_file(input_file_path) }
context 'when the input file does not exist' do
- let(:non_existing_input_file) { 'tmp/another_file.out' }
+ let(:non_existing_input_pathname) { 'tmp/another_file.out' }
- subject { instance.read_array_from_file(non_existing_input_file) }
+ subject { instance.read_array_from_file(non_existing_input_pathname) }
around do |example|
example.run
ensure
- FileUtils.rm_rf(non_existing_input_file)
+ FileUtils.rm_rf(non_existing_input_pathname)
end
it 'creates the file' do
- expect { subject }.to change { File.exist?(non_existing_input_file) }.from(false).to(true)
+ expect { subject }.to change { File.exist?(non_existing_input_pathname) }.from(false).to(true)
end
end
@@ -67,9 +67,10 @@ RSpec.describe Tooling::Helpers::FileHandler, feature_category: :tooling do
end
describe '#write_array_to_file' do
- let(:content_array) { %w[new_entry] }
+ let(:content_array) { %w[new_entry] }
+ let(:overwrite_flag) { false }
- subject { instance.write_array_to_file(output_file_path, content_array) }
+ subject { instance.write_array_to_file(output_file_path, content_array, overwrite: overwrite_flag) }
context 'when the output file does not exist' do
let(:non_existing_output_file) { 'tmp/another_file.out' }
@@ -93,6 +94,14 @@ RSpec.describe Tooling::Helpers::FileHandler, feature_category: :tooling do
it 'writes the correct content to the file' do
expect { subject }.to change { File.read(output_file_path) }.from('').to(content_array.join(' '))
end
+
+ context 'when the content array is not sorted' do
+ let(:content_array) { %w[new_entry a_new_entry] }
+
+ it 'sorts the array before writing it to file' do
+ expect { subject }.to change { File.read(output_file_path) }.from('').to(content_array.sort.join(' '))
+ end
+ end
end
context 'when the output file is not empty' do
@@ -100,8 +109,18 @@ RSpec.describe Tooling::Helpers::FileHandler, feature_category: :tooling do
it 'appends the correct content to the file' do
expect { subject }.to change { File.read(output_file_path) }
- .from(initial_content)
- .to((initial_content.split(' ') + content_array).join(' '))
+ .from(initial_content)
+ .to((initial_content.split(' ') + content_array).join(' '))
+ end
+
+ context 'when the overwrite flag is set to true' do
+ let(:overwrite_flag) { true }
+
+ it 'overwrites the previous content' do
+ expect { subject }.to change { File.read(output_file_path) }
+ .from(initial_content)
+ .to(content_array.join(' '))
+ end
end
end
end
diff --git a/spec/tooling/lib/tooling/mappings/graphql_base_type_mappings_spec.rb b/spec/tooling/lib/tooling/mappings/graphql_base_type_mappings_spec.rb
index 521958573fd..b6459428214 100644
--- a/spec/tooling/lib/tooling/mappings/graphql_base_type_mappings_spec.rb
+++ b/spec/tooling/lib/tooling/mappings/graphql_base_type_mappings_spec.rb
@@ -6,11 +6,17 @@ require_relative '../../../../../tooling/lib/tooling/mappings/graphql_base_type_
RSpec.describe Tooling::Mappings::GraphqlBaseTypeMappings, feature_category: :tooling do
# We set temporary folders, and those readers give access to those folder paths
attr_accessor :foss_folder, :ee_folder, :jh_folder
- attr_accessor :changes_file, :matching_tests_paths
+ attr_accessor :changed_files_file, :predictive_tests_file
+
+ let(:changed_files_pathname) { changed_files_file.path }
+ let(:predictive_tests_pathname) { predictive_tests_file.path }
+ let(:instance) { described_class.new(changed_files_pathname, predictive_tests_pathname) }
+ let(:changed_files_content) { "changed_file1 changed_file2" }
+ let(:predictive_tests_initial_content) { "previously_matching_spec.rb" }
around do |example|
- self.changes_file = Tempfile.new('changes')
- self.matching_tests_paths = Tempfile.new('matching_tests')
+ self.changed_files_file = Tempfile.new('changed_files_file')
+ self.predictive_tests_file = Tempfile.new('predictive_tests_file')
Dir.mktmpdir('FOSS') do |foss_folder|
Dir.mktmpdir('EE') do |ee_folder|
@@ -24,20 +30,16 @@ RSpec.describe Tooling::Mappings::GraphqlBaseTypeMappings, feature_category: :to
begin
example.run
ensure
- changes_file.close
- matching_tests_paths.close
- changes_file.unlink
- matching_tests_paths.unlink
+ changed_files_file.close
+ predictive_tests_file.close
+ changed_files_file.unlink
+ predictive_tests_file.unlink
end
end
end
end
end
- let(:instance) { described_class.new(changes_file, matching_tests_paths) }
- let(:changes_file_content) { "changed_file1 changed_file2" }
- let(:matching_tests_paths_initial_content) { "previously_matching_spec.rb" }
-
before do
stub_const("Tooling::Mappings::GraphqlBaseTypeMappings::GRAPHQL_TYPES_FOLDERS", {
nil => [foss_folder],
@@ -46,23 +48,23 @@ RSpec.describe Tooling::Mappings::GraphqlBaseTypeMappings, feature_category: :to
})
# We write into the temp files initially, to later check how the code modified those files
- File.write(changes_file, changes_file_content)
- File.write(matching_tests_paths, matching_tests_paths_initial_content)
+ File.write(changed_files_pathname, changed_files_content)
+ File.write(predictive_tests_pathname, predictive_tests_initial_content)
end
describe '#execute' do
subject { instance.execute }
context 'when no GraphQL files were changed' do
- let(:changes_file_content) { '' }
+ let(:changed_files_content) { '' }
it 'does not change the output file' do
- expect { subject }.not_to change { File.read(matching_tests_paths) }
+ expect { subject }.not_to change { File.read(predictive_tests_pathname) }
end
end
context 'when some GraphQL files were changed' do
- let(:changes_file_content) do
+ let(:changed_files_content) do
[
"#{foss_folder}/my_graphql_file.rb",
"#{foss_folder}/my_other_graphql_file.rb"
@@ -76,7 +78,7 @@ RSpec.describe Tooling::Mappings::GraphqlBaseTypeMappings, feature_category: :to
end
it 'does not change the output file' do
- expect { subject }.not_to change { File.read(matching_tests_paths) }
+ expect { subject }.not_to change { File.read(predictive_tests_pathname) }
end
end
@@ -92,9 +94,9 @@ RSpec.describe Tooling::Mappings::GraphqlBaseTypeMappings, feature_category: :to
end
it 'writes the correct specs in the output' do
- expect { subject }.to change { File.read(matching_tests_paths) }
- .from(matching_tests_paths_initial_content)
- .to("#{matching_tests_paths_initial_content} spec/my_graphql_file_spec.rb")
+ expect { subject }.to change { File.read(predictive_tests_pathname) }
+ .from(predictive_tests_initial_content)
+ .to("#{predictive_tests_initial_content} spec/my_graphql_file_spec.rb")
end
end
end
@@ -110,7 +112,7 @@ RSpec.describe Tooling::Mappings::GraphqlBaseTypeMappings, feature_category: :to
end
context 'when no files were changed' do
- let(:changes_file_content) { '' }
+ let(:changed_files_content) { '' }
it 'returns an empty array' do
expect(subject).to match_array([])
@@ -118,7 +120,7 @@ RSpec.describe Tooling::Mappings::GraphqlBaseTypeMappings, feature_category: :to
end
context 'when GraphQL files were changed' do
- let(:changes_file_content) do
+ let(:changed_files_content) do
[
"#{foss_folder}/my_graphql_file.rb",
"#{foss_folder}/my_other_graphql_file.rb",
@@ -135,7 +137,7 @@ RSpec.describe Tooling::Mappings::GraphqlBaseTypeMappings, feature_category: :to
end
context 'when files are deleted' do
- let(:changes_file_content) { "#{foss_folder}/deleted.rb" }
+ let(:changed_files_content) { "#{foss_folder}/deleted.rb" }
it 'returns an empty array' do
expect(subject).to match_array([])
diff --git a/spec/tooling/lib/tooling/mappings/js_to_system_specs_mappings_spec.rb b/spec/tooling/lib/tooling/mappings/js_to_system_specs_mappings_spec.rb
index c36d14cd267..e1f35bedebb 100644
--- a/spec/tooling/lib/tooling/mappings/js_to_system_specs_mappings_spec.rb
+++ b/spec/tooling/lib/tooling/mappings/js_to_system_specs_mappings_spec.rb
@@ -6,23 +6,25 @@ require_relative '../../../../../tooling/lib/tooling/mappings/js_to_system_specs
RSpec.describe Tooling::Mappings::JsToSystemSpecsMappings, feature_category: :tooling do
# We set temporary folders, and those readers give access to those folder paths
attr_accessor :js_base_folder, :system_specs_base_folder
- attr_accessor :changes_file, :matching_tests_paths
+ attr_accessor :changed_files_file, :predictive_tests_file
+
+ let(:changed_files_pathname) { changed_files_file.path }
+ let(:predictive_tests_pathname) { predictive_tests_file.path }
+ let(:changed_files_content) { "changed_file1 changed_file2" }
+ let(:predictive_tests_content) { "previously_matching_spec.rb" }
let(:instance) do
described_class.new(
- changes_file,
- matching_tests_paths,
+ changed_files_pathname,
+ predictive_tests_pathname,
system_specs_base_folder: system_specs_base_folder,
js_base_folder: js_base_folder
)
end
- let(:changes_file_content) { "changed_file1 changed_file2" }
- let(:matching_tests_paths_content) { "previously_matching_spec.rb" }
-
around do |example|
- self.changes_file = Tempfile.new('changes')
- self.matching_tests_paths = Tempfile.new('matching_tests')
+ self.changed_files_file = Tempfile.new('changed_files_file')
+ self.predictive_tests_file = Tempfile.new('predictive_tests_file')
Dir.mktmpdir do |tmp_js_base_folder|
Dir.mktmpdir do |tmp_system_specs_base_folder|
@@ -34,10 +36,10 @@ RSpec.describe Tooling::Mappings::JsToSystemSpecsMappings, feature_category: :to
begin
example.run
ensure
- changes_file.close
- matching_tests_paths.close
- changes_file.unlink
- matching_tests_paths.unlink
+ changed_files_file.close
+ predictive_tests_file.close
+ changed_files_file.unlink
+ predictive_tests_file.unlink
end
end
end
@@ -45,22 +47,22 @@ RSpec.describe Tooling::Mappings::JsToSystemSpecsMappings, feature_category: :to
before do
# We write into the temp files initially, to later check how the code modified those files
- File.write(changes_file, changes_file_content)
- File.write(matching_tests_paths, matching_tests_paths_content)
+ File.write(changed_files_pathname, changed_files_content)
+ File.write(predictive_tests_pathname, predictive_tests_content)
end
describe '#execute' do
subject { instance.execute }
before do
- File.write(changes_file, changed_files.join(' '))
+ File.write(changed_files_pathname, changed_files.join(' '))
end
context 'when no JS files were changed' do
let(:changed_files) { [] }
it 'does not change the output file' do
- expect { subject }.not_to change { File.read(matching_tests_paths) }
+ expect { subject }.not_to change { File.read(predictive_tests_pathname) }
end
end
@@ -69,7 +71,7 @@ RSpec.describe Tooling::Mappings::JsToSystemSpecsMappings, feature_category: :to
context 'when the JS files are not present on disk' do
it 'does not change the output file' do
- expect { subject }.not_to change { File.read(matching_tests_paths) }
+ expect { subject }.not_to change { File.read(predictive_tests_pathname) }
end
end
@@ -81,7 +83,7 @@ RSpec.describe Tooling::Mappings::JsToSystemSpecsMappings, feature_category: :to
context 'when no system specs match the JS keyword' do
it 'does not change the output file' do
- expect { subject }.not_to change { File.read(matching_tests_paths) }
+ expect { subject }.not_to change { File.read(predictive_tests_pathname) }
end
end
@@ -92,9 +94,9 @@ RSpec.describe Tooling::Mappings::JsToSystemSpecsMappings, feature_category: :to
end
it 'adds the new specs to the output file' do
- expect { subject }.to change { File.read(matching_tests_paths) }
- .from(matching_tests_paths_content)
- .to("#{matching_tests_paths_content} #{system_specs_base_folder}/confidential_issues/issues_spec.rb")
+ expect { subject }.to change { File.read(predictive_tests_pathname) }
+ .from(predictive_tests_content)
+ .to("#{predictive_tests_content} #{system_specs_base_folder}/confidential_issues/issues_spec.rb")
end
end
end
@@ -108,7 +110,7 @@ RSpec.describe Tooling::Mappings::JsToSystemSpecsMappings, feature_category: :to
File.write("#{js_base_folder}/index.js", "index.js")
File.write("#{js_base_folder}/index-with-ee-in-it.js", "index-with-ee-in-it.js")
File.write("#{js_base_folder}/index-with-jh-in-it.js", "index-with-jh-in-it.js")
- File.write(changes_file, changed_files.join(' '))
+ File.write(changed_files_pathname, changed_files.join(' '))
end
context 'when no files were changed' do
@@ -148,7 +150,7 @@ RSpec.describe Tooling::Mappings::JsToSystemSpecsMappings, feature_category: :to
end
describe '#construct_js_keywords' do
- subject { described_class.new(changes_file, matching_tests_paths).construct_js_keywords(js_files) }
+ subject { described_class.new(changed_files_file, predictive_tests_file).construct_js_keywords(js_files) }
let(:js_files) do
%w[
diff --git a/spec/tooling/lib/tooling/mappings/partial_to_views_mappings_spec.rb b/spec/tooling/lib/tooling/mappings/partial_to_views_mappings_spec.rb
index 30965ed985d..75ddee18985 100644
--- a/spec/tooling/lib/tooling/mappings/partial_to_views_mappings_spec.rb
+++ b/spec/tooling/lib/tooling/mappings/partial_to_views_mappings_spec.rb
@@ -5,15 +5,20 @@ require 'fileutils'
require_relative '../../../../../tooling/lib/tooling/mappings/partial_to_views_mappings'
RSpec.describe Tooling::Mappings::PartialToViewsMappings, feature_category: :tooling do
- attr_accessor :view_base_folder, :changes_file, :output_file
+ attr_accessor :view_base_folder, :changed_files_file, :views_with_partials_file
- let(:instance) { described_class.new(changes_file, output_file, view_base_folder: view_base_folder) }
- let(:changes_file_content) { "changed_file1 changed_file2" }
- let(:output_file_content) { "previously_added_view.html.haml" }
+ let(:instance) do
+ described_class.new(changed_files_pathname, views_with_partials_pathname, view_base_folder: view_base_folder)
+ end
+
+ let(:changed_files_pathname) { changed_files_file.path }
+ let(:views_with_partials_pathname) { views_with_partials_file.path }
+ let(:changed_files_content) { "changed_file1 changed_file2" }
+ let(:views_with_partials_content) { "previously_added_view.html.haml" }
around do |example|
- self.changes_file = Tempfile.new('changes')
- self.output_file = Tempfile.new('output_file')
+ self.changed_files_file = Tempfile.new('changed_files_file')
+ self.views_with_partials_file = Tempfile.new('views_with_partials_file')
# See https://ruby-doc.org/stdlib-1.9.3/libdoc/tempfile/rdoc/
# Tempfile.html#class-Tempfile-label-Explicit+close
@@ -23,24 +28,24 @@ RSpec.describe Tooling::Mappings::PartialToViewsMappings, feature_category: :too
example.run
end
ensure
- changes_file.close
- output_file.close
- changes_file.unlink
- output_file.unlink
+ changed_files_file.close
+ views_with_partials_file.close
+ changed_files_file.unlink
+ views_with_partials_file.unlink
end
end
before do
# We write into the temp files initially, to check how the code modified those files
- File.write(changes_file, changes_file_content)
- File.write(output_file, output_file_content)
+ File.write(changed_files_pathname, changed_files_content)
+ File.write(views_with_partials_pathname, views_with_partials_content)
end
describe '#execute' do
subject { instance.execute }
- let(:changed_files) { ["#{view_base_folder}/my_view.html.haml"] }
- let(:changes_file_content) { changed_files.join(" ") }
+ let(:changed_files) { ["#{view_base_folder}/my_view.html.haml"] }
+ let(:changed_files_content) { changed_files.join(" ") }
before do
# We create all of the changed_files, so that they are part of the filtered files
@@ -48,12 +53,12 @@ RSpec.describe Tooling::Mappings::PartialToViewsMappings, feature_category: :too
end
it 'does not modify the content of the input file' do
- expect { subject }.not_to change { File.read(changes_file) }
+ expect { subject }.not_to change { File.read(changed_files_pathname) }
end
context 'when no partials were modified' do
it 'does not change the output file' do
- expect { subject }.not_to change { File.read(output_file) }
+ expect { subject }.not_to change { File.read(views_with_partials_pathname) }
end
end
@@ -77,7 +82,7 @@ RSpec.describe Tooling::Mappings::PartialToViewsMappings, feature_category: :too
end
it 'does not change the output file' do
- expect { subject }.not_to change { File.read(output_file) }
+ expect { subject }.not_to change { File.read(views_with_partials_pathname) }
end
end
@@ -87,9 +92,9 @@ RSpec.describe Tooling::Mappings::PartialToViewsMappings, feature_category: :too
end
it 'writes the view including the partial to the output' do
- expect { subject }.to change { File.read(output_file) }
- .from(output_file_content)
- .to(output_file_content + " #{view_base_folder}/my_view.html.haml")
+ expect { subject }.to change { File.read(views_with_partials_pathname) }
+ .from(views_with_partials_content)
+ .to(views_with_partials_content + " #{view_base_folder}/my_view.html.haml")
end
end
end
@@ -98,7 +103,7 @@ RSpec.describe Tooling::Mappings::PartialToViewsMappings, feature_category: :too
describe '#filter_files' do
subject { instance.filter_files }
- let(:changes_file_content) { file_path }
+ let(:changed_files_content) { file_path }
context 'when the file does not exist on disk' do
let(:file_path) { "#{view_base_folder}/_index.html.erb" }
@@ -164,11 +169,11 @@ RSpec.describe Tooling::Mappings::PartialToViewsMappings, feature_category: :too
before do
FileUtils.mkdir_p("#{view_base_folder}/components/subfolder")
- File.write(changes_file_content, "I am a partial!")
+ File.write(changed_files_content, "I am a partial!")
end
context 'when the partial is not part of the changed files' do
- let(:changes_file_content) { "#{view_base_folder}/components/subfolder/_not_the_partial.html.haml" }
+ let(:changed_files_content) { "#{view_base_folder}/components/subfolder/_not_the_partial.html.haml" }
it 'returns false' do
expect(subject).to be_falsey
@@ -176,7 +181,7 @@ RSpec.describe Tooling::Mappings::PartialToViewsMappings, feature_category: :too
end
context 'when the partial is part of the changed files' do
- let(:changes_file_content) { "#{view_base_folder}/components/subfolder/_relative_partial.html.haml" }
+ let(:changed_files_content) { "#{view_base_folder}/components/subfolder/_relative_partial.html.haml" }
it 'returns true' do
expect(subject).to be_truthy
@@ -191,11 +196,11 @@ RSpec.describe Tooling::Mappings::PartialToViewsMappings, feature_category: :too
before do
FileUtils.mkdir_p("#{view_base_folder}/components")
FileUtils.mkdir_p("#{view_base_folder}/shared")
- File.write(changes_file_content, "I am a partial!")
+ File.write(changed_files_content, "I am a partial!")
end
context 'when the partial is not part of the changed files' do
- let(:changes_file_content) { "#{view_base_folder}/shared/not_the_partial" }
+ let(:changed_files_content) { "#{view_base_folder}/shared/not_the_partial" }
it 'returns false' do
expect(subject).to be_falsey
@@ -203,7 +208,7 @@ RSpec.describe Tooling::Mappings::PartialToViewsMappings, feature_category: :too
end
context 'when the partial is part of the changed files' do
- let(:changes_file_content) { "#{view_base_folder}/shared/_absolute_partial.html.haml" }
+ let(:changed_files_content) { "#{view_base_folder}/shared/_absolute_partial.html.haml" }
it 'returns true' do
expect(subject).to be_truthy
diff --git a/spec/tooling/lib/tooling/mappings/view_to_js_mappings_spec.rb b/spec/tooling/lib/tooling/mappings/view_to_js_mappings_spec.rb
index bf07ad1951b..6d007843716 100644
--- a/spec/tooling/lib/tooling/mappings/view_to_js_mappings_spec.rb
+++ b/spec/tooling/lib/tooling/mappings/view_to_js_mappings_spec.rb
@@ -6,23 +6,25 @@ require_relative '../../../../../tooling/lib/tooling/mappings/view_to_js_mapping
RSpec.describe Tooling::Mappings::ViewToJsMappings, feature_category: :tooling do
# We set temporary folders, and those readers give access to those folder paths
attr_accessor :view_base_folder, :js_base_folder
- attr_accessor :changes_file, :matching_tests_paths
+ attr_accessor :changed_files_file, :predictive_tests_file
+
+ let(:changed_files_pathname) { changed_files_file.path }
+ let(:predictive_tests_pathname) { predictive_tests_file.path }
+ let(:changed_files_content) { "changed_file1 changed_file2" }
+ let(:predictive_tests_content) { "previously_matching_spec.rb" }
let(:instance) do
described_class.new(
- changes_file,
- matching_tests_paths,
+ changed_files_pathname,
+ predictive_tests_pathname,
view_base_folder: view_base_folder,
js_base_folder: js_base_folder
)
end
- let(:changes_file_content) { "changed_file1 changed_file2" }
- let(:matching_tests_paths_content) { "previously_matching_spec.rb" }
-
around do |example|
- self.changes_file = Tempfile.new('changes')
- self.matching_tests_paths = Tempfile.new('matching_tests')
+ self.changed_files_file = Tempfile.new('changed_files_file')
+ self.predictive_tests_file = Tempfile.new('matching_tests')
Dir.mktmpdir do |tmp_js_base_folder|
Dir.mktmpdir do |tmp_views_base_folder|
@@ -34,10 +36,10 @@ RSpec.describe Tooling::Mappings::ViewToJsMappings, feature_category: :tooling d
begin
example.run
ensure
- changes_file.close
- matching_tests_paths.close
- changes_file.unlink
- matching_tests_paths.unlink
+ changed_files_file.close
+ predictive_tests_file.close
+ changed_files_file.unlink
+ predictive_tests_file.unlink
end
end
end
@@ -45,8 +47,8 @@ RSpec.describe Tooling::Mappings::ViewToJsMappings, feature_category: :tooling d
before do
# We write into the temp files initially, to later check how the code modified those files
- File.write(changes_file, changes_file_content)
- File.write(matching_tests_paths, matching_tests_paths_content)
+ File.write(changed_files_pathname, changed_files_content)
+ File.write(predictive_tests_pathname, predictive_tests_content)
end
describe '#execute' do
@@ -55,7 +57,7 @@ RSpec.describe Tooling::Mappings::ViewToJsMappings, feature_category: :tooling d
subject { instance.execute }
before do
- File.write(changes_file, changed_files.join(' '))
+ File.write(changed_files_pathname, changed_files.join(' '))
end
context 'when no view files have been changed' do
@@ -64,7 +66,7 @@ RSpec.describe Tooling::Mappings::ViewToJsMappings, feature_category: :tooling d
end
it 'does not change the output file' do
- expect { subject }.not_to change { File.read(matching_tests_paths) }
+ expect { subject }.not_to change { File.read(predictive_tests_pathname) }
end
end
@@ -82,7 +84,7 @@ RSpec.describe Tooling::Mappings::ViewToJsMappings, feature_category: :tooling d
end
it 'does not change the output file' do
- expect { subject }.not_to change { File.read(matching_tests_paths) }
+ expect { subject }.not_to change { File.read(predictive_tests_pathname) }
end
end
@@ -99,7 +101,7 @@ RSpec.describe Tooling::Mappings::ViewToJsMappings, feature_category: :tooling d
context 'when no matching JS files are found' do
it 'does not change the output file' do
- expect { subject }.not_to change { File.read(matching_tests_paths) }
+ expect { subject }.not_to change { File.read(predictive_tests_pathname) }
end
end
@@ -119,9 +121,9 @@ RSpec.describe Tooling::Mappings::ViewToJsMappings, feature_category: :tooling d
end
it 'adds the matching JS files to the output' do
- expect { subject }.to change { File.read(matching_tests_paths) }
- .from(matching_tests_paths_content)
- .to("#{matching_tests_paths_content} #{js_base_folder}/index.js")
+ expect { subject }.to change { File.read(predictive_tests_pathname) }
+ .from(predictive_tests_content)
+ .to("#{predictive_tests_content} #{js_base_folder}/index.js")
end
end
end
@@ -165,9 +167,9 @@ RSpec.describe Tooling::Mappings::ViewToJsMappings, feature_category: :tooling d
end
it 'scans those partials for the HTML attribute value' do
- expect { subject }.to change { File.read(matching_tests_paths) }
- .from(matching_tests_paths_content)
- .to("#{matching_tests_paths_content} #{js_base_folder}/index.js")
+ expect { subject }.to change { File.read(predictive_tests_pathname) }
+ .from(predictive_tests_content)
+ .to("#{predictive_tests_content} #{js_base_folder}/index.js")
end
end
end
@@ -178,7 +180,7 @@ RSpec.describe Tooling::Mappings::ViewToJsMappings, feature_category: :tooling d
before do
File.write("#{js_base_folder}/index.js", "index.js")
File.write("#{view_base_folder}/index.html", "index.html")
- File.write(changes_file, changed_files.join(' '))
+ File.write(changed_files_pathname, changed_files.join(' '))
end
context 'when no files were changed' do
diff --git a/spec/tooling/lib/tooling/mappings/view_to_system_specs_mappings_spec.rb b/spec/tooling/lib/tooling/mappings/view_to_system_specs_mappings_spec.rb
index d4f4bbd7f06..b8a13c50c9b 100644
--- a/spec/tooling/lib/tooling/mappings/view_to_system_specs_mappings_spec.rb
+++ b/spec/tooling/lib/tooling/mappings/view_to_system_specs_mappings_spec.rb
@@ -5,15 +5,20 @@ require 'fileutils'
require_relative '../../../../../tooling/lib/tooling/mappings/view_to_system_specs_mappings'
RSpec.describe Tooling::Mappings::ViewToSystemSpecsMappings, feature_category: :tooling do
- attr_accessor :view_base_folder, :changes_file, :output_file
+ attr_accessor :view_base_folder, :changed_files_file, :predictive_tests_file
- let(:instance) { described_class.new(changes_file, output_file, view_base_folder: view_base_folder) }
- let(:changes_file_content) { "changed_file1 changed_file2" }
- let(:output_file_initial_content) { "previously_added_spec.rb" }
+ let(:instance) do
+ described_class.new(changed_files_pathname, predictive_tests_pathname, view_base_folder: view_base_folder)
+ end
+
+ let(:changed_files_pathname) { changed_files_file.path }
+ let(:predictive_tests_pathname) { predictive_tests_file.path }
+ let(:changed_files_content) { "changed_file1 changed_file2" }
+ let(:predictive_tests_initial_content) { "previously_added_spec.rb" }
around do |example|
- self.changes_file = Tempfile.new('changes')
- self.output_file = Tempfile.new('output_file')
+ self.changed_files_file = Tempfile.new('changed_files_file')
+ self.predictive_tests_file = Tempfile.new('predictive_tests_file')
# See https://ruby-doc.org/stdlib-1.9.3/libdoc/tempfile/rdoc/
# Tempfile.html#class-Tempfile-label-Explicit+close
@@ -23,10 +28,10 @@ RSpec.describe Tooling::Mappings::ViewToSystemSpecsMappings, feature_category: :
example.run
end
ensure
- changes_file.close
- output_file.close
- changes_file.unlink
- output_file.unlink
+ changed_files_file.close
+ predictive_tests_file.close
+ changed_files_file.unlink
+ predictive_tests_file.unlink
end
end
@@ -34,13 +39,13 @@ RSpec.describe Tooling::Mappings::ViewToSystemSpecsMappings, feature_category: :
FileUtils.mkdir_p("#{view_base_folder}/app/views/dashboard")
# We write into the temp files initially, to check how the code modified those files
- File.write(changes_file, changes_file_content)
- File.write(output_file, output_file_initial_content)
+ File.write(changed_files_pathname, changed_files_content)
+ File.write(predictive_tests_pathname, predictive_tests_initial_content)
end
shared_examples 'writes nothing to the output file' do
it 'writes nothing to the output file' do
- expect { subject }.not_to change { File.read(changes_file) }
+ expect { subject }.not_to change { File.read(changed_files_pathname) }
end
end
@@ -48,7 +53,7 @@ RSpec.describe Tooling::Mappings::ViewToSystemSpecsMappings, feature_category: :
subject { instance.execute }
let(:changed_files) { ["#{view_base_folder}/app/views/dashboard/my_view.html.haml"] }
- let(:changes_file_content) { changed_files.join(" ") }
+ let(:changed_files_content) { changed_files.join(" ") }
before do
# We create all of the changed_files, so that they are part of the filtered files
@@ -88,9 +93,9 @@ RSpec.describe Tooling::Mappings::ViewToSystemSpecsMappings, feature_category: :
end
it 'writes that feature spec to the output file' do
- expect { subject }.to change { File.read(output_file) }
- .from(output_file_initial_content)
- .to("#{output_file_initial_content} #{expected_feature_spec}")
+ expect { subject }.to change { File.read(predictive_tests_pathname) }
+ .from(predictive_tests_initial_content)
+ .to("#{predictive_tests_initial_content} #{expected_feature_spec}")
end
end
@@ -111,9 +116,9 @@ RSpec.describe Tooling::Mappings::ViewToSystemSpecsMappings, feature_category: :
end
it 'writes all of the feature specs for the parent folder to the output file' do
- expect { subject }.to change { File.read(output_file) }
- .from(output_file_initial_content)
- .to("#{output_file_initial_content} #{expected_feature_specs.join(' ')}")
+ expect { subject }.to change { File.read(predictive_tests_pathname) }
+ .from(predictive_tests_initial_content)
+ .to("#{predictive_tests_initial_content} #{expected_feature_specs.join(' ')}")
end
end
end
diff --git a/spec/tooling/lib/tooling/predictive_tests_spec.rb b/spec/tooling/lib/tooling/predictive_tests_spec.rb
new file mode 100644
index 00000000000..79554037c48
--- /dev/null
+++ b/spec/tooling/lib/tooling/predictive_tests_spec.rb
@@ -0,0 +1,109 @@
+# frozen_string_literal: true
+
+require 'tempfile'
+require_relative '../../../../tooling/lib/tooling/predictive_tests'
+require_relative '../../../support/helpers/stub_env'
+
+RSpec.describe Tooling::PredictiveTests, feature_category: :tooling do
+ include StubENV
+
+ let(:instance) { described_class.new }
+ let(:matching_tests_initial_content) { 'initial_matching_spec' }
+
+ attr_accessor :changed_files, :fixtures_mapping, :matching_js_files, :matching_tests, :views_with_partials
+
+ around do |example|
+ self.changed_files = Tempfile.new('test-folder/changed_files.txt')
+ self.fixtures_mapping = Tempfile.new('test-folder/fixtures_mapping.txt')
+ self.matching_js_files = Tempfile.new('test-folder/matching_js_files.txt')
+ self.matching_tests = Tempfile.new('test-folder/matching_tests.txt')
+ self.views_with_partials = Tempfile.new('test-folder/views_with_partials.txt')
+
+ # See https://ruby-doc.org/stdlib-1.9.3/libdoc/tempfile/rdoc/
+ # Tempfile.html#class-Tempfile-label-Explicit+close
+ begin
+ example.run
+ ensure
+ changed_files.close
+ changed_files.unlink
+ fixtures_mapping.close
+ fixtures_mapping.unlink
+ matching_js_files.close
+ matching_js_files.unlink
+ matching_tests.close
+ matching_tests.unlink
+ views_with_partials.close
+ views_with_partials.unlink
+ end
+ end
+
+ before do
+ stub_env(
+ 'RSPEC_CHANGED_FILES_PATH' => changed_files.path,
+ 'RSPEC_MATCHING_TESTS_PATH' => matching_tests.path,
+ 'RSPEC_VIEWS_INCLUDING_PARTIALS_PATH' => views_with_partials.path,
+ 'FRONTEND_FIXTURES_MAPPING_PATH' => fixtures_mapping.path,
+ 'RSPEC_MATCHING_JS_FILES_PATH' => matching_js_files.path,
+ 'RSPEC_TESTS_MAPPING_ENABLED' => "false",
+ 'RSPEC_TESTS_MAPPING_PATH' => '/tmp/does-not-exist.out'
+ )
+
+ # We write some data to later on verify that we only append to this file.
+ File.write(matching_tests.path, matching_tests_initial_content)
+ File.write(fixtures_mapping.path, '{}') # We write valid JSON, so that the file can be processed
+ end
+
+ describe '#execute' do
+ subject { instance.execute }
+
+ context 'when ENV variables are missing' do
+ before do
+ stub_env(
+ 'RSPEC_CHANGED_FILES_PATH' => '',
+ 'FRONTEND_FIXTURES_MAPPING_PATH' => ''
+ )
+ end
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(
+ '[predictive tests] Missing ENV variable(s): RSPEC_CHANGED_FILES_PATH,FRONTEND_FIXTURES_MAPPING_PATH.'
+ )
+ end
+ end
+
+ context 'when all ENV variables are provided' do
+ before do
+ File.write(changed_files, changed_files_content)
+ end
+
+ context 'when no files were changed' do
+ let(:changed_files_content) { '' }
+
+ it 'does not change any files' do
+ expect { subject }.not_to change { File.read(changed_files.path) }
+ expect { subject }.not_to change { File.read(matching_tests.path) }
+ expect { subject }.not_to change { File.read(views_with_partials.path) }
+ expect { subject }.not_to change { File.read(fixtures_mapping.path) }
+ expect { subject }.not_to change { File.read(matching_js_files.path) }
+ end
+ end
+
+ context 'when some files were changed' do
+ let(:changed_files_content) { 'tooling/lib/tooling/predictive_tests.rb' }
+
+ it 'appends the spec file to RSPEC_MATCHING_TESTS_PATH' do
+ expect { subject }.to change { File.read(matching_tests.path) }
+ .from(matching_tests_initial_content)
+ .to("#{matching_tests_initial_content} spec/tooling/lib/tooling/predictive_tests_spec.rb")
+ end
+
+ it 'does not change files other than RSPEC_MATCHING_TESTS_PATH' do
+ expect { subject }.not_to change { File.read(changed_files.path) }
+ expect { subject }.not_to change { File.read(views_with_partials.path) }
+ expect { subject }.not_to change { File.read(fixtures_mapping.path) }
+ expect { subject }.not_to change { File.read(matching_js_files.path) }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/tooling/quality/test_level_spec.rb b/spec/tooling/quality/test_level_spec.rb
index aac7d19c079..a7e4e42206a 100644
--- a/spec/tooling/quality/test_level_spec.rb
+++ b/spec/tooling/quality/test_level_spec.rb
@@ -46,7 +46,7 @@ RSpec.describe Quality::TestLevel, feature_category: :tooling do
context 'when level is unit' do
it 'returns a pattern' do
expect(subject.pattern(:unit))
- .to eq("spec/{bin,channels,config,contracts,db,dependencies,elastic,elastic_integration,experiments,factories,finders,frontend,graphql,haml_lint,helpers,initializers,lib,metrics_server,models,policies,presenters,rack_servers,replicators,routing,rubocop,scripts,serializers,services,sidekiq,sidekiq_cluster,spam,support_specs,tasks,uploaders,validators,views,workers,tooling,components}{,/**/}*_spec.rb")
+ .to eq("spec/{bin,channels,components,config,contracts,db,dependencies,elastic,elastic_integration,experiments,factories,finders,frontend,graphql,haml_lint,helpers,initializers,lib,metrics_server,models,policies,presenters,rack_servers,replicators,routing,rubocop,scripts,serializers,services,sidekiq,sidekiq_cluster,spam,support_specs,tasks,uploaders,validators,views,workers,tooling}{,/**/}*_spec.rb")
end
end
@@ -121,7 +121,7 @@ RSpec.describe Quality::TestLevel, feature_category: :tooling do
context 'when level is unit' do
it 'returns a regexp' do
expect(subject.regexp(:unit))
- .to eq(%r{spec/(bin|channels|config|contracts|db|dependencies|elastic|elastic_integration|experiments|factories|finders|frontend|graphql|haml_lint|helpers|initializers|lib|metrics_server|models|policies|presenters|rack_servers|replicators|routing|rubocop|scripts|serializers|services|sidekiq|sidekiq_cluster|spam|support_specs|tasks|uploaders|validators|views|workers|tooling|components)/})
+ .to eq(%r{spec/(bin|channels|components|config|contracts|db|dependencies|elastic|elastic_integration|experiments|factories|finders|frontend|graphql|haml_lint|helpers|initializers|lib|metrics_server|models|policies|presenters|rack_servers|replicators|routing|rubocop|scripts|serializers|services|sidekiq|sidekiq_cluster|spam|support_specs|tasks|uploaders|validators|views|workers|tooling)/})
end
end
@@ -167,6 +167,13 @@ RSpec.describe Quality::TestLevel, feature_category: :tooling do
end
end
+ context 'when start_with == true' do
+ it 'returns a regexp' do
+ expect(described_class.new(['ee/']).regexp(:system, true))
+ .to eq(%r{^(ee/)spec/(features)/})
+ end
+ end
+
describe 'performance' do
it 'memoizes the regexp for a given level' do
expect(subject.regexp(:system).object_id).to eq(subject.regexp(:system).object_id)
diff --git a/tooling/bin/find_changes b/tooling/bin/find_changes
deleted file mode 100755
index 331fae45a53..00000000000
--- a/tooling/bin/find_changes
+++ /dev/null
@@ -1,14 +0,0 @@
-#!/usr/bin/env ruby
-# frozen_string_literal: true
-
-require_relative '../lib/tooling/find_changes'
-
-output_file = ARGV.shift
-matched_tests_file = ARGV.shift
-frontend_fixtures_mapping_path = ARGV.shift
-
-Tooling::FindChanges
- .new(
- output_file: output_file,
- matched_tests_file: matched_tests_file,
- frontend_fixtures_mapping_path: frontend_fixtures_mapping_path).execute
diff --git a/tooling/bin/find_tests b/tooling/bin/find_tests
deleted file mode 100755
index 31afa4d598b..00000000000
--- a/tooling/bin/find_tests
+++ /dev/null
@@ -1,9 +0,0 @@
-#!/usr/bin/env ruby
-# frozen_string_literal: true
-
-require_relative '../lib/tooling/find_tests'
-
-changes_file = ARGV.shift
-matching_tests_paths = ARGV.shift
-
-Tooling::FindTests.new(changes_file, matching_tests_paths).execute
diff --git a/tooling/bin/graphql_base_type_mappings b/tooling/bin/graphql_base_type_mappings
deleted file mode 100755
index 2fc06ab639f..00000000000
--- a/tooling/bin/graphql_base_type_mappings
+++ /dev/null
@@ -1,9 +0,0 @@
-#!/usr/bin/env ruby
-# frozen_string_literal: true
-
-require_relative '../lib/tooling/mappings/graphql_base_type_mappings'
-
-changes_file = ARGV.shift
-matching_tests_paths = ARGV.shift
-
-Tooling::Mappings::GraphqlBaseTypeMappings.new(changes_file, matching_tests_paths).execute
diff --git a/tooling/bin/js_to_system_specs_mappings b/tooling/bin/js_to_system_specs_mappings
deleted file mode 100755
index 22d1dbb6590..00000000000
--- a/tooling/bin/js_to_system_specs_mappings
+++ /dev/null
@@ -1,9 +0,0 @@
-#!/usr/bin/env ruby
-# frozen_string_literal: true
-
-require_relative '../lib/tooling/mappings/js_to_system_specs_mappings'
-
-changes_file = ARGV.shift
-matching_tests_paths = ARGV.shift
-
-Tooling::Mappings::JsToSystemSpecsMappings.new(changes_file, matching_tests_paths).execute
diff --git a/tooling/bin/partial_to_views_mappings b/tooling/bin/partial_to_views_mappings
deleted file mode 100755
index 12c994ee556..00000000000
--- a/tooling/bin/partial_to_views_mappings
+++ /dev/null
@@ -1,9 +0,0 @@
-#!/usr/bin/env ruby
-# frozen_string_literal: true
-
-require_relative '../lib/tooling/mappings/partial_to_views_mappings'
-
-changes_file = ARGV.shift
-output_file = ARGV.shift
-
-Tooling::Mappings::PartialToViewsMappings.new(changes_file, output_file).execute
diff --git a/tooling/bin/predictive_tests b/tooling/bin/predictive_tests
new file mode 100755
index 00000000000..61543494a69
--- /dev/null
+++ b/tooling/bin/predictive_tests
@@ -0,0 +1,6 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require_relative '../lib/tooling/predictive_tests'
+
+Tooling::PredictiveTests.new.execute
diff --git a/tooling/bin/view_to_js_mappings b/tooling/bin/view_to_js_mappings
deleted file mode 100755
index 51d7cc11ed8..00000000000
--- a/tooling/bin/view_to_js_mappings
+++ /dev/null
@@ -1,9 +0,0 @@
-#!/usr/bin/env ruby
-# frozen_string_literal: true
-
-require_relative '../lib/tooling/mappings/view_to_js_mappings'
-
-changes_file = ARGV.shift
-matching_tests_paths = ARGV.shift
-
-Tooling::Mappings::ViewToJsMappings.new(changes_file, matching_tests_paths).execute
diff --git a/tooling/bin/view_to_system_specs_mappings b/tooling/bin/view_to_system_specs_mappings
deleted file mode 100755
index b2e3f2a132a..00000000000
--- a/tooling/bin/view_to_system_specs_mappings
+++ /dev/null
@@ -1,9 +0,0 @@
-#!/usr/bin/env ruby
-# frozen_string_literal: true
-
-require_relative '../lib/tooling/mappings/view_to_system_specs_mappings'
-
-changes_file = ARGV.shift
-output_file = ARGV.shift
-
-Tooling::Mappings::ViewToSystemSpecsMappings.new(changes_file, output_file).execute
diff --git a/tooling/lib/tooling/find_changes.rb b/tooling/lib/tooling/find_changes.rb
index e151734e41c..b4439bd4abe 100755
--- a/tooling/lib/tooling/find_changes.rb
+++ b/tooling/lib/tooling/find_changes.rb
@@ -2,28 +2,35 @@
# frozen_string_literal: true
require 'gitlab'
+require_relative 'helpers/file_handler'
module Tooling
class FindChanges
- def initialize(output_file: nil, matched_tests_file: nil, frontend_fixtures_mapping_path: nil)
- @gitlab_token = ENV['PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE'] || ''
- @gitlab_endpoint = ENV['CI_API_V4_URL']
- @mr_project_path = ENV['CI_MERGE_REQUEST_PROJECT_PATH']
- @mr_iid = ENV['CI_MERGE_REQUEST_IID']
- @output_file = output_file
- @matched_tests_file = matched_tests_file
- @frontend_fixtures_mapping_path = frontend_fixtures_mapping_path
+ include Helpers::FileHandler
+
+ def initialize(
+ changed_files_pathname = nil, predictive_tests_pathname = nil, frontend_fixtures_mapping_pathname = nil
+ )
+ @gitlab_token = ENV['PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE'] || ''
+ @gitlab_endpoint = ENV['CI_API_V4_URL']
+ @mr_project_path = ENV['CI_MERGE_REQUEST_PROJECT_PATH']
+ @mr_iid = ENV['CI_MERGE_REQUEST_IID']
+ @changed_files_pathname = changed_files_pathname
+ @predictive_tests_pathname = predictive_tests_pathname
+ @frontend_fixtures_mapping_pathname = frontend_fixtures_mapping_pathname
end
def execute
- raise ArgumentError, "An path to an output file must be given as first argument." if output_file.nil?
+ if changed_files_pathname.nil?
+ raise ArgumentError, "A path to the changed files file must be given as first argument."
+ end
add_frontend_fixture_files!
- File.write(output_file, file_changes.join(' '))
+ write_array_to_file(changed_files_pathname, file_changes, overwrite: true)
end
def only_js_files_changed
- @output_file = nil # We ensure that we'll get the diff from the MR directly, not from a file.
+ @changed_files_pathname = nil # We ensure that we'll get the diff from the MR directly, not from a file.
file_changes.any? && file_changes.all? { |file| file.end_with?('.js') }
end
@@ -31,7 +38,7 @@ module Tooling
private
attr_reader :gitlab_token, :gitlab_endpoint, :mr_project_path,
- :mr_iid, :output_file, :matched_tests_file, :frontend_fixtures_mapping_path
+ :mr_iid, :changed_files_pathname, :predictive_tests_pathname, :frontend_fixtures_mapping_pathname
def gitlab
@gitlab ||= begin
@@ -45,7 +52,7 @@ module Tooling
end
def add_frontend_fixture_files?
- matched_tests_file && frontend_fixtures_mapping_path
+ predictive_tests_pathname && frontend_fixtures_mapping_pathname
end
def add_frontend_fixture_files!
@@ -61,8 +68,8 @@ module Tooling
def file_changes
@file_changes ||=
- if output_file && File.exist?(output_file)
- File.read(output_file).split(' ')
+ if changed_files_pathname && File.exist?(changed_files_pathname)
+ read_array_from_file(changed_files_pathname)
else
mr_changes.changes.flat_map do |change|
change.to_h.values_at('old_path', 'new_path')
@@ -75,15 +82,15 @@ module Tooling
end
def test_files
- return [] if !matched_tests_file || !File.exist?(matched_tests_file)
+ return [] if !predictive_tests_pathname || !File.exist?(predictive_tests_pathname)
- File.read(matched_tests_file).split(' ')
+ read_array_from_file(predictive_tests_pathname)
end
def frontend_fixtures_mapping
- return {} if !frontend_fixtures_mapping_path || !File.exist?(frontend_fixtures_mapping_path)
+ return {} if !frontend_fixtures_mapping_pathname || !File.exist?(frontend_fixtures_mapping_pathname)
- JSON.parse(File.read(frontend_fixtures_mapping_path)) # rubocop:disable Gitlab/Json
+ JSON.parse(File.read(frontend_fixtures_mapping_pathname)) # rubocop:disable Gitlab/Json
end
end
end
diff --git a/tooling/lib/tooling/find_tests.rb b/tooling/lib/tooling/find_tests.rb
index b63b207c58b..f26c1eacdc7 100644
--- a/tooling/lib/tooling/find_tests.rb
+++ b/tooling/lib/tooling/find_tests.rb
@@ -7,9 +7,9 @@ module Tooling
class FindTests
include Helpers::FileHandler
- def initialize(changes_file, matching_tests_paths)
- @matching_tests_paths = matching_tests_paths
- @changed_files = read_array_from_file(changes_file)
+ def initialize(changed_files_pathname, predictive_tests_pathname)
+ @predictive_tests_pathname = predictive_tests_pathname
+ @changed_files = read_array_from_file(changed_files_pathname)
end
def execute
@@ -21,11 +21,11 @@ module Tooling
end
end
- write_array_to_file(matching_tests_paths, tff.test_files.uniq)
+ write_array_to_file(predictive_tests_pathname, tff.test_files.uniq)
end
private
- attr_reader :changed_files, :matching_tests, :matching_tests_paths
+ attr_reader :changed_files, :matching_tests, :predictive_tests_pathname
end
end
diff --git a/tooling/lib/tooling/helpers/file_handler.rb b/tooling/lib/tooling/helpers/file_handler.rb
index ec4f42ea363..2778bb1ffbc 100644
--- a/tooling/lib/tooling/helpers/file_handler.rb
+++ b/tooling/lib/tooling/helpers/file_handler.rb
@@ -11,10 +11,18 @@ module Tooling
File.read(file).split(' ')
end
- def write_array_to_file(file, content_array)
+ def write_array_to_file(file, content_array, overwrite: false)
FileUtils.touch file
- output_content = (File.read(file).split(' ') + content_array).join(' ')
+ # We sort the array to make it easier to read the output file
+ content_array.sort!
+
+ output_content =
+ if overwrite
+ content_array.join(' ')
+ else
+ (File.read(file).split(' ') + content_array).join(' ')
+ end
File.write(file, output_content)
end
diff --git a/tooling/lib/tooling/mappings/graphql_base_type_mappings.rb b/tooling/lib/tooling/mappings/graphql_base_type_mappings.rb
index cd8b55d5820..569a8278163 100644
--- a/tooling/lib/tooling/mappings/graphql_base_type_mappings.rb
+++ b/tooling/lib/tooling/mappings/graphql_base_type_mappings.rb
@@ -25,9 +25,9 @@ module Tooling
'jh' => GRAPHQL_TYPES_FOLDERS_JH
}.freeze
- def initialize(changes_file, matching_tests_paths)
- @matching_tests_paths = matching_tests_paths
- @changed_files = read_array_from_file(changes_file)
+ def initialize(changed_files_pathname, predictive_tests_pathname)
+ @predictive_tests_pathname = predictive_tests_pathname
+ @changed_files = read_array_from_file(changed_files_pathname)
end
def execute
@@ -46,7 +46,7 @@ module Tooling
end
end.compact.uniq
- write_array_to_file(matching_tests_paths, matching_graphql_tests)
+ write_array_to_file(predictive_tests_pathname, matching_graphql_tests)
end
def filter_files
@@ -113,7 +113,7 @@ module Tooling
private
- attr_reader :changed_files, :matching_tests_paths
+ attr_reader :changed_files, :predictive_tests_pathname
end
end
end
diff --git a/tooling/lib/tooling/mappings/js_to_system_specs_mappings.rb b/tooling/lib/tooling/mappings/js_to_system_specs_mappings.rb
index 0c61b921f83..b2fca3a765a 100644
--- a/tooling/lib/tooling/mappings/js_to_system_specs_mappings.rb
+++ b/tooling/lib/tooling/mappings/js_to_system_specs_mappings.rb
@@ -10,13 +10,13 @@ module Tooling
module Mappings
class JsToSystemSpecsMappings < Base
def initialize(
- changes_file, matching_tests_paths,
+ changed_files_pathname, predictive_tests_pathname,
js_base_folder: 'app/assets/javascripts', system_specs_base_folder: 'spec/features')
- @changed_files = read_array_from_file(changes_file)
- @matching_tests_paths = matching_tests_paths
- @js_base_folder = js_base_folder
- @js_base_folders = folders_for_available_editions(js_base_folder)
- @system_specs_base_folder = system_specs_base_folder
+ @changed_files = read_array_from_file(changed_files_pathname)
+ @predictive_tests_pathname = predictive_tests_pathname
+ @js_base_folder = js_base_folder
+ @js_base_folders = folders_for_available_editions(js_base_folder)
+ @system_specs_base_folder = system_specs_base_folder
# Cannot be extracted to a constant, as it depends on a variable
@first_js_folder_extract_regexp = %r{
@@ -36,7 +36,7 @@ module Tooling
end
end
- write_array_to_file(matching_tests_paths, matching_system_tests)
+ write_array_to_file(predictive_tests_pathname, matching_system_tests)
end
# Keep the files that are in the @js_base_folders folders
@@ -66,7 +66,7 @@ module Tooling
private
- attr_reader :changed_files, :matching_tests_paths
+ attr_reader :changed_files, :predictive_tests_pathname
end
end
end
diff --git a/tooling/lib/tooling/mappings/partial_to_views_mappings.rb b/tooling/lib/tooling/mappings/partial_to_views_mappings.rb
index 3109da685f1..8b0a5ed4ecd 100644
--- a/tooling/lib/tooling/mappings/partial_to_views_mappings.rb
+++ b/tooling/lib/tooling/mappings/partial_to_views_mappings.rb
@@ -7,10 +7,10 @@ require_relative '../../../../lib/gitlab_edition'
module Tooling
module Mappings
class PartialToViewsMappings < Base
- def initialize(changes_file, output_file, view_base_folder: 'app/views')
- @output_file = output_file
- @changed_files = read_array_from_file(changes_file)
- @view_base_folders = folders_for_available_editions(view_base_folder)
+ def initialize(changed_files_pathname, views_with_partials_pathname, view_base_folder: 'app/views')
+ @views_with_partials_pathname = views_with_partials_pathname
+ @changed_files = read_array_from_file(changed_files_pathname)
+ @view_base_folders = folders_for_available_editions(view_base_folder)
end
def execute
@@ -28,7 +28,7 @@ module Tooling
end
end
- write_array_to_file(output_file, views_including_modified_partials)
+ write_array_to_file(views_with_partials_pathname, views_including_modified_partials)
end
def filter_files
@@ -99,7 +99,7 @@ module Tooling
private
- attr_reader :changed_files, :output_file, :view_base_folders
+ attr_reader :changed_files, :views_with_partials_pathname, :view_base_folders
end
end
end
diff --git a/tooling/lib/tooling/mappings/view_to_js_mappings.rb b/tooling/lib/tooling/mappings/view_to_js_mappings.rb
index fd2db6b6d7e..f2098d6acd5 100644
--- a/tooling/lib/tooling/mappings/view_to_js_mappings.rb
+++ b/tooling/lib/tooling/mappings/view_to_js_mappings.rb
@@ -14,12 +14,12 @@ module Tooling
RAILS_PARTIAL_INVOCATION_REGEXP = %r{(?:render|render_if_exists)(?: |\()(?:partial: ?)?['"]([\w/-]+)['"]}.freeze
def initialize(
- changes_file, matching_tests_paths,
+ changed_files_pathname, predictive_tests_pathname,
view_base_folder: 'app/views', js_base_folder: 'app/assets/javascripts')
- @changed_files = read_array_from_file(changes_file)
- @matching_tests_paths = matching_tests_paths
- @view_base_folders = folders_for_available_editions(view_base_folder)
- @js_base_folders = folders_for_available_editions(js_base_folder)
+ @changed_files = read_array_from_file(changed_files_pathname)
+ @predictive_tests_pathname = predictive_tests_pathname
+ @view_base_folders = folders_for_available_editions(view_base_folder)
+ @js_base_folders = folders_for_available_editions(js_base_folder)
end
def execute
@@ -40,7 +40,7 @@ module Tooling
end
end
- write_array_to_file(matching_tests_paths, matching_js_files)
+ write_array_to_file(predictive_tests_pathname, matching_js_files)
end
# Keep the files that are in the @view_base_folders folder
@@ -76,7 +76,7 @@ module Tooling
private
- attr_reader :changed_files, :matching_tests_paths
+ attr_reader :changed_files, :predictive_tests_pathname
end
end
end
diff --git a/tooling/lib/tooling/mappings/view_to_system_specs_mappings.rb b/tooling/lib/tooling/mappings/view_to_system_specs_mappings.rb
index 227f2c6e62b..6d840dcbd71 100644
--- a/tooling/lib/tooling/mappings/view_to_system_specs_mappings.rb
+++ b/tooling/lib/tooling/mappings/view_to_system_specs_mappings.rb
@@ -7,10 +7,10 @@ require_relative '../../../../lib/gitlab_edition'
module Tooling
module Mappings
class ViewToSystemSpecsMappings < Base
- def initialize(changes_file, output_file, view_base_folder: 'app/views')
- @output_file = output_file
- @changed_files = read_array_from_file(changes_file)
- @view_base_folders = folders_for_available_editions(view_base_folder)
+ def initialize(changed_files_pathname, predictive_tests_pathname, view_base_folder: 'app/views')
+ @predictive_tests_pathname = predictive_tests_pathname
+ @changed_files = read_array_from_file(changed_files_pathname)
+ @view_base_folders = folders_for_available_editions(view_base_folder)
end
def execute
@@ -27,12 +27,12 @@ module Tooling
end
end
- write_array_to_file(output_file, found_system_specs.compact.uniq.sort)
+ write_array_to_file(predictive_tests_pathname, found_system_specs.compact.uniq.sort)
end
private
- attr_reader :changed_files, :output_file, :view_base_folders
+ attr_reader :changed_files, :predictive_tests_pathname, :view_base_folders
# Keep the views files that are in the @view_base_folders folder
def filter_files
diff --git a/tooling/lib/tooling/predictive_tests.rb b/tooling/lib/tooling/predictive_tests.rb
new file mode 100644
index 00000000000..2691e1ba56d
--- /dev/null
+++ b/tooling/lib/tooling/predictive_tests.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require_relative 'find_changes'
+require_relative 'find_tests'
+require_relative 'mappings/graphql_base_type_mappings'
+require_relative 'mappings/js_to_system_specs_mappings'
+require_relative 'mappings/partial_to_views_mappings'
+require_relative 'mappings/view_to_js_mappings'
+require_relative 'mappings/view_to_system_specs_mappings'
+
+module Tooling
+ class PredictiveTests
+ REQUIRED_ENV_VARIABLES = %w[
+ RSPEC_CHANGED_FILES_PATH
+ RSPEC_MATCHING_TESTS_PATH
+ RSPEC_VIEWS_INCLUDING_PARTIALS_PATH
+ FRONTEND_FIXTURES_MAPPING_PATH
+ RSPEC_MATCHING_JS_FILES_PATH
+ ].freeze
+
+ def initialize
+ missing_env_variables = REQUIRED_ENV_VARIABLES.select { |key| ENV[key.to_s] == '' }
+ unless missing_env_variables.empty?
+ raise "[predictive tests] Missing ENV variable(s): #{missing_env_variables.join(',')}."
+ end
+
+ @rspec_changed_files_path = ENV['RSPEC_CHANGED_FILES_PATH']
+ @rspec_matching_tests_path = ENV['RSPEC_MATCHING_TESTS_PATH']
+ @rspec_views_including_partials_path = ENV['RSPEC_VIEWS_INCLUDING_PARTIALS_PATH']
+ @frontend_fixtures_mapping_path = ENV['FRONTEND_FIXTURES_MAPPING_PATH']
+ @rspec_matching_js_files_path = ENV['RSPEC_MATCHING_JS_FILES_PATH']
+ end
+
+ def execute
+ Tooling::FindChanges.new(rspec_changed_files_path).execute
+ Tooling::FindTests.new(rspec_changed_files_path, rspec_matching_tests_path).execute
+ Tooling::Mappings::PartialToViewsMappings.new(
+ rspec_changed_files_path, rspec_views_including_partials_path).execute
+ Tooling::FindTests.new(rspec_views_including_partials_path, rspec_matching_tests_path).execute
+ Tooling::Mappings::JsToSystemSpecsMappings.new(rspec_changed_files_path, rspec_matching_tests_path).execute
+ Tooling::Mappings::GraphqlBaseTypeMappings.new(rspec_changed_files_path, rspec_matching_tests_path).execute
+ Tooling::Mappings::ViewToSystemSpecsMappings.new(rspec_changed_files_path, rspec_matching_tests_path).execute
+ Tooling::FindChanges.new(
+ rspec_changed_files_path, rspec_matching_tests_path, frontend_fixtures_mapping_path).execute
+ Tooling::Mappings::ViewToJsMappings.new(rspec_changed_files_path, rspec_matching_js_files_path).execute
+ end
+
+ private
+
+ attr_reader :rspec_changed_files_path, :rspec_matching_tests_path, :rspec_views_including_partials_path,
+ :frontend_fixtures_mapping_path, :rspec_matching_js_files_path
+ end
+end
diff --git a/tooling/quality/test_level.rb b/tooling/quality/test_level.rb
index eeda135f3ee..20e00763f65 100644
--- a/tooling/quality/test_level.rb
+++ b/tooling/quality/test_level.rb
@@ -18,6 +18,7 @@ module Quality
unit: %w[
bin
channels
+ components
config
contracts
db
@@ -54,7 +55,6 @@ module Quality
views
workers
tooling
- components
],
integration: %w[
commands
@@ -77,8 +77,8 @@ module Quality
@patterns[level] ||= "#{prefixes_for_pattern}spec/#{folders_pattern(level)}{,/**/}*#{suffix(level)}".freeze # rubocop:disable Style/RedundantFreeze
end
- def regexp(level)
- @regexps[level] ||= Regexp.new("#{prefixes_for_regex}spec/#{folders_regex(level)}").freeze
+ def regexp(level, start_with = false)
+ @regexps[level] ||= Regexp.new("#{'^' if start_with}#{prefixes_for_regex}spec/#{folders_regex(level)}").freeze
end
def level_for(file_path)
diff --git a/yarn.lock b/yarn.lock
index 11a6c54e03f..dce72892ba9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1127,10 +1127,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.38.0.tgz#5c316bc139b3017eea92bb8d603c7ff26fa9ddb7"
integrity sha512-yDkZnuYDQlBB7Eb7noSHGlNpZE6KgM6kX2zB0bmGdHBY2w4wbnjgCR8INYFblH19QGlIToxoW3rOL9iobu4Puw==
-"@gitlab/ui@59.5.0":
- version "59.5.0"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-59.5.0.tgz#0f0ac51ddad59e0ad3683a6ae505373f66028ca7"
- integrity sha512-a7dbYLugQxrZtZTz1SWFiz7rLxQycQswjiS0uztyW2AtCRr06UumXveOLAnvyBwm076Ckx/VlMQqcfXIT6nW0w==
+"@gitlab/ui@60.0.0":
+ version "60.0.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-60.0.0.tgz#dc5331c9658881487f5fa1e769b4f33572999729"
+ integrity sha512-/2RyNmM/Ji2/hetMbmsbk7p2Ody5dH9/Rp0jryhNeCARxxcFUeUxZbe0Un61k1Tcln6v/IStA05woSUcp257EQ==
dependencies:
"@popperjs/core" "^2.11.2"
bootstrap-vue "2.23.1"