Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-02-18 12:45:46 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-02-18 12:45:46 +0300
commita7b3560714b4d9cc4ab32dffcd1f74a284b93580 (patch)
tree7452bd5c3545c2fa67a28aa013835fb4fa071baf /spec
parentee9173579ae56a3dbfe5afe9f9410c65bb327ca7 (diff)
Add latest changes from gitlab-org/gitlab@14-8-stable-eev14.8.0-rc42
Diffstat (limited to 'spec')
-rw-r--r--spec/bin/feature_flag_spec.rb40
-rw-r--r--spec/channels/application_cable/connection_spec.rb8
-rw-r--r--spec/commands/metrics_server/metrics_server_spec.rb81
-rw-r--r--spec/commands/sidekiq_cluster/cli_spec.rb23
-rw-r--r--spec/controllers/admin/instance_review_controller_spec.rb2
-rw-r--r--spec/controllers/admin/runners_controller_spec.rb7
-rw-r--r--spec/controllers/autocomplete_controller_spec.rb2
-rw-r--r--spec/controllers/dashboard/projects_controller_spec.rb24
-rw-r--r--spec/controllers/explore/projects_controller_spec.rb18
-rw-r--r--spec/controllers/graphql_controller_spec.rb20
-rw-r--r--spec/controllers/groups/clusters_controller_spec.rb7
-rw-r--r--spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb29
-rw-r--r--spec/controllers/groups/releases_controller_spec.rb8
-rw-r--r--spec/controllers/groups/runners_controller_spec.rb4
-rw-r--r--spec/controllers/groups_controller_spec.rb23
-rw-r--r--spec/controllers/metrics_controller_spec.rb6
-rw-r--r--spec/controllers/oauth/authorizations_controller_spec.rb94
-rw-r--r--spec/controllers/projects/artifacts_controller_spec.rb2
-rw-r--r--spec/controllers/projects/autocomplete_sources_controller_spec.rb60
-rw-r--r--spec/controllers/projects/avatars_controller_spec.rb4
-rw-r--r--spec/controllers/projects/badges_controller_spec.rb168
-rw-r--r--spec/controllers/projects/branches_controller_spec.rb30
-rw-r--r--spec/controllers/projects/clusters_controller_spec.rb7
-rw-r--r--spec/controllers/projects/commit_controller_spec.rb12
-rw-r--r--spec/controllers/projects/commits_controller_spec.rb34
-rw-r--r--spec/controllers/projects/compare_controller_spec.rb25
-rw-r--r--spec/controllers/projects/forks_controller_spec.rb12
-rw-r--r--spec/controllers/projects/group_links_controller_spec.rb2
-rw-r--r--spec/controllers/projects/hooks_controller_spec.rb2
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb18
-rw-r--r--spec/controllers/projects/merge_requests/conflicts_controller_spec.rb2
-rw-r--r--spec/controllers/projects/merge_requests/creations_controller_spec.rb2
-rw-r--r--spec/controllers/projects/merge_requests/drafts_controller_spec.rb2
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb34
-rw-r--r--spec/controllers/projects/mirrors_controller_spec.rb10
-rw-r--r--spec/controllers/projects/packages/infrastructure_registry_controller_spec.rb12
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb2
-rw-r--r--spec/controllers/projects/refs_controller_spec.rb17
-rw-r--r--spec/controllers/projects/repositories_controller_spec.rb32
-rw-r--r--spec/controllers/projects/runners_controller_spec.rb4
-rw-r--r--spec/controllers/projects/service_ping_controller_spec.rb29
-rw-r--r--spec/controllers/projects/settings/repository_controller_spec.rb14
-rw-r--r--spec/controllers/projects/tags_controller_spec.rb9
-rw-r--r--spec/controllers/projects_controller_spec.rb20
-rw-r--r--spec/controllers/registrations_controller_spec.rb22
-rw-r--r--spec/controllers/repositories/git_http_controller_spec.rb4
-rw-r--r--spec/controllers/search_controller_spec.rb3
-rw-r--r--spec/crystalball_env.rb2
-rw-r--r--spec/db/schema_spec.rb2
-rw-r--r--spec/events/projects/project_deleted_event_spec.rb34
-rw-r--r--spec/experiments/application_experiment_spec.rb103
-rw-r--r--spec/experiments/new_project_readme_content_experiment_spec.rb38
-rw-r--r--spec/experiments/require_verification_for_namespace_creation_experiment_spec.rb20
-rw-r--r--spec/factories/ci/build_metadata.rb7
-rw-r--r--spec/factories/ci/runners.rb5
-rw-r--r--spec/factories/container_repositories.rb46
-rw-r--r--spec/factories/gitlab/database/background_migration/batched_jobs.rb16
-rw-r--r--spec/factories/go_module_commits.rb4
-rw-r--r--spec/factories/group_members.rb12
-rw-r--r--spec/factories/issues.rb9
-rw-r--r--spec/factories/keys.rb25
-rw-r--r--spec/factories/labels.rb7
-rw-r--r--spec/factories/namespace_statistics.rb7
-rw-r--r--spec/factories/project_members.rb12
-rw-r--r--spec/factories/projects.rb2
-rw-r--r--spec/factories/usage_data.rb8
-rw-r--r--spec/factories/work_items.rb13
-rw-r--r--spec/features/admin/admin_groups_spec.rb17
-rw-r--r--spec/features/admin/admin_mode/logout_spec.rb2
-rw-r--r--spec/features/admin/admin_runners_spec.rb52
-rw-r--r--spec/features/admin/admin_sees_background_migrations_spec.rb6
-rw-r--r--spec/features/admin/admin_settings_spec.rb56
-rw-r--r--spec/features/admin/integrations/instance_integrations_spec.rb15
-rw-r--r--spec/features/admin/integrations/user_activates_mattermost_slash_command_spec.rb15
-rw-r--r--spec/features/admin/users/users_spec.rb17
-rw-r--r--spec/features/boards/board_filters_spec.rb13
-rw-r--r--spec/features/boards/boards_spec.rb6
-rw-r--r--spec/features/boards/new_issue_spec.rb19
-rw-r--r--spec/features/breadcrumbs_schema_markup_spec.rb6
-rw-r--r--spec/features/clusters/create_agent_spec.rb1
-rw-r--r--spec/features/contextual_sidebar_spec.rb2
-rw-r--r--spec/features/cycle_analytics_spec.rb4
-rw-r--r--spec/features/dashboard/groups_list_spec.rb65
-rw-r--r--spec/features/dashboard/issuables_counter_spec.rb15
-rw-r--r--spec/features/dashboard/merge_requests_spec.rb4
-rw-r--r--spec/features/dashboard/snippets_spec.rb6
-rw-r--r--spec/features/error_tracking/user_filters_errors_by_status_spec.rb2
-rw-r--r--spec/features/error_tracking/user_searches_sentry_errors_spec.rb2
-rw-r--r--spec/features/error_tracking/user_sees_error_details_spec.rb2
-rw-r--r--spec/features/error_tracking/user_sees_error_index_spec.rb4
-rw-r--r--spec/features/file_uploads/attachment_spec.rb2
-rw-r--r--spec/features/file_uploads/git_lfs_spec.rb2
-rw-r--r--spec/features/file_uploads/maven_package_spec.rb2
-rw-r--r--spec/features/file_uploads/nuget_package_spec.rb2
-rw-r--r--spec/features/file_uploads/rubygem_package_spec.rb2
-rw-r--r--spec/features/gitlab_experiments_spec.rb4
-rw-r--r--spec/features/groups/group_settings_spec.rb45
-rw-r--r--spec/features/groups/integrations/group_integrations_spec.rb15
-rw-r--r--spec/features/groups/members/leave_group_spec.rb2
-rw-r--r--spec/features/groups/members/manage_groups_spec.rb20
-rw-r--r--spec/features/groups_spec.rb6
-rw-r--r--spec/features/ide/user_commits_changes_spec.rb2
-rw-r--r--spec/features/ide/user_opens_merge_request_spec.rb2
-rw-r--r--spec/features/issuables/markdown_references/internal_references_spec.rb4
-rw-r--r--spec/features/issues/filtered_search/dropdown_base_spec.rb17
-rw-r--r--spec/features/issues/filtered_search/recent_searches_spec.rb2
-rw-r--r--spec/features/issues/gfm_autocomplete_spec.rb778
-rw-r--r--spec/features/issues/keyboard_shortcut_spec.rb4
-rw-r--r--spec/features/issues/todo_spec.rb8
-rw-r--r--spec/features/issues/user_comments_on_issue_spec.rb1
-rw-r--r--spec/features/issues/user_creates_branch_and_merge_request_spec.rb25
-rw-r--r--spec/features/issues/user_edits_issue_spec.rb2
-rw-r--r--spec/features/issues/user_interacts_with_awards_spec.rb3
-rw-r--r--spec/features/issues/user_sorts_issues_spec.rb83
-rw-r--r--spec/features/jira_connect/branches_spec.rb4
-rw-r--r--spec/features/markdown/copy_as_gfm_spec.rb2
-rw-r--r--spec/features/merge_request/batch_comments_spec.rb18
-rw-r--r--spec/features/merge_request/user_awards_emoji_spec.rb1
-rw-r--r--spec/features/merge_request/user_comments_on_diff_spec.rb52
-rw-r--r--spec/features/merge_request/user_creates_image_diff_notes_spec.rb4
-rw-r--r--spec/features/merge_request/user_edits_assignees_sidebar_spec.rb2
-rw-r--r--spec/features/merge_request/user_expands_diff_spec.rb4
-rw-r--r--spec/features/merge_request/user_interacts_with_batched_mr_diffs_spec.rb45
-rw-r--r--spec/features/merge_request/user_merges_merge_request_spec.rb2
-rw-r--r--spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb4
-rw-r--r--spec/features/merge_request/user_posts_diff_notes_spec.rb60
-rw-r--r--spec/features/merge_request/user_posts_notes_spec.rb12
-rw-r--r--spec/features/merge_request/user_rebases_merge_request_spec.rb2
-rw-r--r--spec/features/merge_request/user_resolves_wip_mr_spec.rb4
-rw-r--r--spec/features/merge_request/user_reviews_image_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb11
-rw-r--r--spec/features/merge_request/user_sees_deleted_target_branch_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_deployment_widget_spec.rb6
-rw-r--r--spec/features/merge_request/user_sees_diff_spec.rb25
-rw-r--r--spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb22
-rw-r--r--spec/features/merge_request/user_sees_merge_widget_spec.rb26
-rw-r--r--spec/features/merge_request/user_sees_mr_with_deleted_source_branch_spec.rb4
-rw-r--r--spec/features/merge_request/user_sees_pipelines_spec.rb7
-rw-r--r--spec/features/merge_request/user_sees_versions_spec.rb10
-rw-r--r--spec/features/merge_request/user_suggests_changes_on_diff_spec.rb12
-rw-r--r--spec/features/merge_request/user_views_merge_request_from_deleted_fork_spec.rb2
-rw-r--r--spec/features/merge_request/user_views_open_merge_request_spec.rb26
-rw-r--r--spec/features/merge_requests/user_mass_updates_spec.rb14
-rw-r--r--spec/features/monitor_sidebar_link_spec.rb5
-rw-r--r--spec/features/participants_autocomplete_spec.rb23
-rw-r--r--spec/features/profiles/keys_spec.rb7
-rw-r--r--spec/features/profiles/password_spec.rb2
-rw-r--r--spec/features/projects/active_tabs_spec.rb2
-rw-r--r--spec/features/projects/activity/rss_spec.rb4
-rw-r--r--spec/features/projects/blobs/blob_show_spec.rb1583
-rw-r--r--spec/features/projects/blobs/user_follows_pipeline_suggest_nudge_spec.rb2
-rw-r--r--spec/features/projects/branches/user_views_branches_spec.rb2
-rw-r--r--spec/features/projects/ci/editor_spec.rb61
-rw-r--r--spec/features/projects/cluster_agents_spec.rb7
-rw-r--r--spec/features/projects/clusters_spec.rb3
-rw-r--r--spec/features/projects/environments/environment_spec.rb23
-rw-r--r--spec/features/projects/environments_pod_logs_spec.rb2
-rw-r--r--spec/features/projects/files/dockerfile_dropdown_spec.rb2
-rw-r--r--spec/features/projects/files/edit_file_soft_wrap_spec.rb2
-rw-r--r--spec/features/projects/files/editing_a_file_spec.rb2
-rw-r--r--spec/features/projects/files/files_sort_submodules_with_folders_spec.rb2
-rw-r--r--spec/features/projects/files/find_file_keyboard_spec.rb2
-rw-r--r--spec/features/projects/files/gitignore_dropdown_spec.rb2
-rw-r--r--spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb2
-rw-r--r--spec/features/projects/files/project_owner_creates_license_file_spec.rb2
-rw-r--r--spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb2
-rw-r--r--spec/features/projects/files/template_type_dropdown_spec.rb2
-rw-r--r--spec/features/projects/files/undo_template_spec.rb2
-rw-r--r--spec/features/projects/files/user_browses_a_tree_with_a_folder_containing_only_a_folder_spec.rb2
-rw-r--r--spec/features/projects/files/user_browses_files_spec.rb29
-rw-r--r--spec/features/projects/files/user_browses_lfs_files_spec.rb2
-rw-r--r--spec/features/projects/files/user_searches_for_files_spec.rb2
-rw-r--r--spec/features/projects/gfm_autocomplete_load_spec.rb2
-rw-r--r--spec/features/projects/import_export/import_file_spec.rb2
-rw-r--r--spec/features/projects/integrations/disable_triggers_spec.rb (renamed from spec/features/projects/services/disable_triggers_spec.rb)12
-rw-r--r--spec/features/projects/integrations/project_integrations_spec.rb15
-rw-r--r--spec/features/projects/integrations/prometheus_external_alerts_spec.rb (renamed from spec/features/projects/services/prometheus_external_alerts_spec.rb)2
-rw-r--r--spec/features/projects/integrations/user_activates_asana_spec.rb4
-rw-r--r--spec/features/projects/integrations/user_activates_assembla_spec.rb4
-rw-r--r--spec/features/projects/integrations/user_activates_atlassian_bamboo_ci_spec.rb4
-rw-r--r--spec/features/projects/integrations/user_activates_emails_on_push_spec.rb (renamed from spec/features/projects/services/user_activates_emails_on_push_spec.rb)4
-rw-r--r--spec/features/projects/integrations/user_activates_flowdock_spec.rb4
-rw-r--r--spec/features/projects/integrations/user_activates_irker_spec.rb (renamed from spec/features/projects/services/user_activates_irker_spec.rb)4
-rw-r--r--spec/features/projects/integrations/user_activates_issue_tracker_spec.rb (renamed from spec/features/projects/services/user_activates_issue_tracker_spec.rb)12
-rw-r--r--spec/features/projects/integrations/user_activates_jetbrains_teamcity_ci_spec.rb (renamed from spec/features/projects/services/user_activates_jetbrains_teamcity_ci_spec.rb)4
-rw-r--r--spec/features/projects/integrations/user_activates_jira_spec.rb14
-rw-r--r--spec/features/projects/integrations/user_activates_mattermost_slash_command_spec.rb (renamed from spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb)14
-rw-r--r--spec/features/projects/integrations/user_activates_packagist_spec.rb (renamed from spec/features/projects/services/user_activates_packagist_spec.rb)4
-rw-r--r--spec/features/projects/integrations/user_activates_pivotaltracker_spec.rb4
-rw-r--r--spec/features/projects/integrations/user_activates_prometheus_spec.rb (renamed from spec/features/projects/services/user_activates_prometheus_spec.rb)4
-rw-r--r--spec/features/projects/integrations/user_activates_pushover_spec.rb (renamed from spec/features/projects/services/user_activates_pushover_spec.rb)4
-rw-r--r--spec/features/projects/integrations/user_activates_slack_notifications_spec.rb (renamed from spec/features/projects/services/user_activates_slack_notifications_spec.rb)8
-rw-r--r--spec/features/projects/integrations/user_activates_slack_slash_command_spec.rb (renamed from spec/features/projects/services/user_activates_slack_slash_command_spec.rb)2
-rw-r--r--spec/features/projects/integrations/user_uses_inherited_settings_spec.rb2
-rw-r--r--spec/features/projects/integrations/user_views_services_spec.rb (renamed from spec/features/projects/services/user_views_services_spec.rb)6
-rw-r--r--spec/features/projects/issues/design_management/user_uploads_designs_spec.rb2
-rw-r--r--spec/features/projects/jobs_spec.rb2
-rw-r--r--spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb2
-rw-r--r--spec/features/projects/members/invite_group_spec.rb126
-rw-r--r--spec/features/projects/members/member_leaves_project_spec.rb13
-rw-r--r--spec/features/projects/members/owner_cannot_leave_project_spec.rb2
-rw-r--r--spec/features/projects/members/owner_cannot_request_access_to_his_project_spec.rb2
-rw-r--r--spec/features/projects/members/user_requests_access_spec.rb2
-rw-r--r--spec/features/projects/navbar_spec.rb2
-rw-r--r--spec/features/projects/network_graph_spec.rb2
-rw-r--r--spec/features/projects/new_project_spec.rb91
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb2
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb120
-rw-r--r--spec/features/projects/settings/monitor_settings_spec.rb2
-rw-r--r--spec/features/projects/settings/packages_settings_spec.rb2
-rw-r--r--spec/features/projects/settings/project_settings_spec.rb2
-rw-r--r--spec/features/projects/settings/user_interacts_with_deploy_keys_spec.rb2
-rw-r--r--spec/features/projects/show/redirects_spec.rb2
-rw-r--r--spec/features/projects/show/user_manages_notifications_spec.rb2
-rw-r--r--spec/features/projects/show/user_sees_deletion_failure_message_spec.rb2
-rw-r--r--spec/features/projects/show/user_sees_git_instructions_spec.rb4
-rw-r--r--spec/features/projects/tree/tree_show_spec.rb8
-rw-r--r--spec/features/projects/user_changes_project_visibility_spec.rb10
-rw-r--r--spec/features/projects/user_uses_shortcuts_spec.rb2
-rw-r--r--spec/features/projects/view_on_env_spec.rb1
-rw-r--r--spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb2
-rw-r--r--spec/features/projects_spec.rb12
-rw-r--r--spec/features/protected_tags_spec.rb2
-rw-r--r--spec/features/security/project/snippet/internal_access_spec.rb4
-rw-r--r--spec/features/security/project/snippet/private_access_spec.rb2
-rw-r--r--spec/features/security/project/snippet/public_access_spec.rb6
-rw-r--r--spec/features/snippets_spec.rb2
-rw-r--r--spec/features/triggers_spec.rb6
-rw-r--r--spec/features/user_sorts_things_spec.rb12
-rw-r--r--spec/features/users/bizible_csp_spec.rb15
-rw-r--r--spec/features/users/logout_spec.rb2
-rw-r--r--spec/finders/autocomplete/users_finder_spec.rb56
-rw-r--r--spec/finders/ci/daily_build_group_report_results_finder_spec.rb2
-rw-r--r--spec/finders/ci/jobs_finder_spec.rb37
-rw-r--r--spec/finders/ci/runners_finder_spec.rb32
-rw-r--r--spec/finders/clusters/agents_finder_spec.rb6
-rw-r--r--spec/finders/crm/contacts_finder_spec.rb70
-rw-r--r--spec/finders/deployments_finder_spec.rb6
-rw-r--r--spec/finders/environments/environments_by_deployments_finder_spec.rb16
-rw-r--r--spec/finders/group_descendants_finder_spec.rb92
-rw-r--r--spec/finders/group_projects_finder_spec.rb92
-rw-r--r--spec/finders/issues_finder_spec.rb30
-rw-r--r--spec/finders/merge_request_target_project_finder_spec.rb4
-rw-r--r--spec/finders/merge_requests_finder/params_spec.rb23
-rw-r--r--spec/finders/merge_requests_finder_spec.rb16
-rw-r--r--spec/finders/packages/conan/package_file_finder_spec.rb12
-rw-r--r--spec/finders/packages/package_file_finder_spec.rb10
-rw-r--r--spec/finders/releases_finder_spec.rb72
-rw-r--r--spec/finders/tags_finder_spec.rb15
-rw-r--r--spec/finders/template_finder_spec.rb7
-rw-r--r--spec/fixtures/api/schemas/analytics/cycle_analytics/summary.json3
-rw-r--r--spec/fixtures/api/schemas/entities/member_user.json5
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/deployment.json6
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/merge_request.json221
-rw-r--r--spec/frontend/__helpers__/matchers/to_match_interpolated_text.js15
-rw-r--r--spec/frontend/__helpers__/mock_apollo_helper.js19
-rw-r--r--spec/frontend/__helpers__/test_apollo_link.js5
-rw-r--r--spec/frontend/__mocks__/@gitlab/ui.js13
-rw-r--r--spec/frontend/actioncable_link_spec.js2
-rw-r--r--spec/frontend/add_context_commits_modal/components/__snapshots__/add_context_commits_modal_spec.js.snap1
-rw-r--r--spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js50
-rw-r--r--spec/frontend/admin/users/components/modals/delete_user_modal_spec.js19
-rw-r--r--spec/frontend/admin/users/components/modals/user_modal_manager_spec.js22
-rw-r--r--spec/frontend/admin/users/components/users_table_spec.js11
-rw-r--r--spec/frontend/alert_management/components/alert_management_table_spec.js3
-rw-r--r--spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js34
-rw-r--r--spec/frontend/alerts_settings/components/mocks/apollo_mock.js6
-rw-r--r--spec/frontend/analytics/shared/components/daterange_spec.js41
-rw-r--r--spec/frontend/analytics/shared/components/metric_popover_spec.js (renamed from spec/frontend/cycle_analytics/metric_popover_spec.js)2
-rw-r--r--spec/frontend/analytics/shared/components/metric_tile_spec.js81
-rw-r--r--spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js31
-rw-r--r--spec/frontend/analytics/shared/utils_spec.js36
-rw-r--r--spec/frontend/analytics/usage_trends/apollo_mock_data.js1
-rw-r--r--spec/frontend/analytics/usage_trends/components/usage_trends_count_chart_spec.js15
-rw-r--r--spec/frontend/analytics/usage_trends/components/users_chart_spec.js15
-rw-r--r--spec/frontend/analytics/usage_trends/mock_data.js20
-rw-r--r--spec/frontend/artifacts_settings/components/keep_latest_artifact_checkbox_spec.js11
-rw-r--r--spec/frontend/authentication/webauthn/util_spec.js19
-rw-r--r--spec/frontend/badges/components/badge_form_spec.js16
-rw-r--r--spec/frontend/badges/components/badge_list_row_spec.js16
-rw-r--r--spec/frontend/badges/components/badge_list_spec.js38
-rw-r--r--spec/frontend/badges/components/badge_settings_spec.js9
-rw-r--r--spec/frontend/badges/components/badge_spec.js77
-rw-r--r--spec/frontend/batch_comments/components/diff_file_drafts_spec.js19
-rw-r--r--spec/frontend/batch_comments/components/draft_note_spec.js94
-rw-r--r--spec/frontend/batch_comments/components/drafts_count_spec.js11
-rw-r--r--spec/frontend/batch_comments/components/preview_dropdown_spec.js6
-rw-r--r--spec/frontend/batch_comments/components/publish_button_spec.js11
-rw-r--r--spec/frontend/batch_comments/components/publish_dropdown_spec.js6
-rw-r--r--spec/frontend/behaviors/copy_as_gfm_spec.js37
-rw-r--r--spec/frontend/behaviors/shortcuts/shortcuts_issuable_spec.js144
-rw-r--r--spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap1
-rw-r--r--spec/frontend/blob/components/blob_edit_header_spec.js8
-rw-r--r--spec/frontend/blob/components/blob_header_default_actions_spec.js43
-rw-r--r--spec/frontend/blob/components/blob_header_spec.js15
-rw-r--r--spec/frontend/blob/components/blob_header_viewer_switcher_spec.js15
-rw-r--r--spec/frontend/blob/components/mock_data.js3
-rw-r--r--spec/frontend/blob/viewer/index_spec.js48
-rw-r--r--spec/frontend/boards/board_card_inner_spec.js62
-rw-r--r--spec/frontend/boards/board_list_helper.js9
-rw-r--r--spec/frontend/boards/board_list_spec.js25
-rw-r--r--spec/frontend/boards/components/board_add_new_column_trigger_spec.js4
-rw-r--r--spec/frontend/boards/components/board_blocked_icon_spec.js4
-rw-r--r--spec/frontend/boards/components/board_card_spec.js6
-rw-r--r--spec/frontend/boards/components/board_filtered_search_spec.js4
-rw-r--r--spec/frontend/boards/components/board_form_spec.js2
-rw-r--r--spec/frontend/boards/components/board_list_header_spec.js8
-rw-r--r--spec/frontend/boards/components/board_new_issue_spec.js49
-rw-r--r--spec/frontend/boards/components/board_new_item_spec.js37
-rw-r--r--spec/frontend/boards/components/board_settings_sidebar_spec.js28
-rw-r--r--spec/frontend/boards/components/boards_selector_spec.js195
-rw-r--r--spec/frontend/boards/components/sidebar/board_editable_item_spec.js20
-rw-r--r--spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js11
-rw-r--r--spec/frontend/boards/mock_data.js80
-rw-r--r--spec/frontend/boards/project_select_spec.js4
-rw-r--r--spec/frontend/boards/stores/actions_spec.js4
-rw-r--r--spec/frontend/broadcast_notification_spec.js1
-rw-r--r--spec/frontend/captcha/apollo_captcha_link_spec.js2
-rw-r--r--spec/frontend/ci_lint/components/ci_lint_spec.js5
-rw-r--r--spec/frontend/ci_settings_pipeline_triggers/components/triggers_list_spec.js5
-rw-r--r--spec/frontend/ci_variable_list/components/ci_environments_dropdown_spec.js11
-rw-r--r--spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js7
-rw-r--r--spec/frontend/ci_variable_list/components/ci_variable_settings_spec.js7
-rw-r--r--spec/frontend/ci_variable_list/components/ci_variable_table_spec.js46
-rw-r--r--spec/frontend/clusters/agents/components/activity_events_list_spec.js7
-rw-r--r--spec/frontend/clusters/agents/components/show_spec.js51
-rw-r--r--spec/frontend/clusters/components/new_cluster_spec.js5
-rw-r--r--spec/frontend/clusters/components/remove_cluster_confirmation_spec.js5
-rw-r--r--spec/frontend/clusters/forms/components/integration_form_spec.js45
-rw-r--r--spec/frontend/clusters_list/components/agent_table_spec.js155
-rw-r--r--spec/frontend/clusters_list/components/agents_spec.js116
-rw-r--r--spec/frontend/clusters_list/components/ancestor_notice_spec.js13
-rw-r--r--spec/frontend/clusters_list/components/clusters_actions_spec.js35
-rw-r--r--spec/frontend/clusters_list/components/clusters_spec.js5
-rw-r--r--spec/frontend/clusters_list/components/clusters_view_all_spec.js79
-rw-r--r--spec/frontend/clusters_list/components/delete_agent_button_spec.js (renamed from spec/frontend/clusters_list/components/agent_options_spec.js)77
-rw-r--r--spec/frontend/clusters_list/components/install_agent_modal_spec.js60
-rw-r--r--spec/frontend/clusters_list/components/mock_data.js112
-rw-r--r--spec/frontend/clusters_list/components/node_error_help_text_spec.js5
-rw-r--r--spec/frontend/clusters_list/mocks/apollo.js13
-rw-r--r--spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap1
-rw-r--r--spec/frontend/code_navigation/components/app_spec.js8
-rw-r--r--spec/frontend/code_quality_walkthrough/components/step_spec.js2
-rw-r--r--spec/frontend/commit/commit_pipeline_status_component_spec.js12
-rw-r--r--spec/frontend/commit/pipelines/pipelines_table_spec.js66
-rw-r--r--spec/frontend/confirm_modal_spec.js16
-rw-r--r--spec/frontend/content_editor/test_utils.js2
-rw-r--r--spec/frontend/contributors/component/contributors_spec.js20
-rw-r--r--spec/frontend/create_cluster/components/cluster_form_dropdown_spec.js134
-rw-r--r--spec/frontend/create_cluster/eks_cluster/components/create_eks_cluster_spec.js7
-rw-r--r--spec/frontend/create_cluster/eks_cluster/components/eks_cluster_configuration_form_spec.js99
-rw-r--r--spec/frontend/create_cluster/eks_cluster/components/service_credentials_form_spec.js18
-rw-r--r--spec/frontend/create_cluster/gke_cluster/components/gke_machine_type_dropdown_spec.js22
-rw-r--r--spec/frontend/create_cluster/gke_cluster/components/gke_network_dropdown_spec.js8
-rw-r--r--spec/frontend/create_cluster/gke_cluster/components/gke_project_id_dropdown_spec.js42
-rw-r--r--spec/frontend/create_cluster/gke_cluster/components/gke_submit_button_spec.js8
-rw-r--r--spec/frontend/create_cluster/gke_cluster/components/gke_subnetwork_dropdown_spec.js8
-rw-r--r--spec/frontend/create_cluster/gke_cluster/components/gke_zone_dropdown_spec.js24
-rw-r--r--spec/frontend/cycle_analytics/base_spec.js2
-rw-r--r--spec/frontend/cycle_analytics/filter_bar_spec.js9
-rw-r--r--spec/frontend/cycle_analytics/stage_table_spec.js3
-rw-r--r--spec/frontend/cycle_analytics/utils_spec.js31
-rw-r--r--spec/frontend/cycle_analytics/value_stream_metrics_spec.js80
-rw-r--r--spec/frontend/deploy_freeze/components/deploy_freeze_settings_spec.js7
-rw-r--r--spec/frontend/deploy_freeze/components/deploy_freeze_table_spec.js11
-rw-r--r--spec/frontend/deploy_freeze/components/timezone_dropdown_spec.js7
-rw-r--r--spec/frontend/deploy_keys/components/action_btn_spec.js15
-rw-r--r--spec/frontend/deploy_keys/components/app_spec.js114
-rw-r--r--spec/frontend/deploy_keys/components/key_spec.js14
-rw-r--r--spec/frontend/design_management/components/delete_button_spec.js18
-rw-r--r--spec/frontend/design_management/components/design_notes/design_discussion_spec.js64
-rw-r--r--spec/frontend/design_management/components/design_notes/design_note_spec.js33
-rw-r--r--spec/frontend/design_management/components/design_notes/design_reply_form_spec.js65
-rw-r--r--spec/frontend/design_management/components/design_overlay_spec.js149
-rw-r--r--spec/frontend/design_management/components/design_presentation_spec.js117
-rw-r--r--spec/frontend/design_management/components/design_scaler_spec.js9
-rw-r--r--spec/frontend/design_management/components/design_sidebar_spec.js27
-rw-r--r--spec/frontend/design_management/components/design_todo_button_spec.js9
-rw-r--r--spec/frontend/design_management/components/image_spec.js46
-rw-r--r--spec/frontend/design_management/components/list/item_spec.js35
-rw-r--r--spec/frontend/design_management/components/toolbar/design_navigation_spec.js8
-rw-r--r--spec/frontend/design_management/components/toolbar/index_spec.js61
-rw-r--r--spec/frontend/design_management/components/upload/design_version_dropdown_spec.js59
-rw-r--r--spec/frontend/design_management/pages/design/index_spec.js64
-rw-r--r--spec/frontend/design_management/pages/index_spec.js35
-rw-r--r--spec/frontend/design_management/router_spec.js8
-rw-r--r--spec/frontend/design_management/utils/cache_update_spec.js2
-rw-r--r--spec/frontend/diffs/components/app_spec.js23
-rw-r--r--spec/frontend/diffs/components/collapsed_files_warning_spec.js29
-rw-r--r--spec/frontend/diffs/components/compare_versions_spec.js36
-rw-r--r--spec/frontend/diffs/components/diff_content_spec.js7
-rw-r--r--spec/frontend/diffs/components/diff_discussion_reply_spec.js7
-rw-r--r--spec/frontend/diffs/components/diff_discussions_spec.js13
-rw-r--r--spec/frontend/diffs/components/diff_expansion_cell_spec.js8
-rw-r--r--spec/frontend/diffs/components/diff_file_header_spec.js30
-rw-r--r--spec/frontend/diffs/components/diff_file_spec.js40
-rw-r--r--spec/frontend/diffs/components/diff_gutter_avatars_spec.js22
-rw-r--r--spec/frontend/diffs/components/diff_view_spec.js5
-rw-r--r--spec/frontend/diffs/components/image_diff_overlay_spec.js4
-rw-r--r--spec/frontend/diffs/components/merge_conflict_warning_spec.js15
-rw-r--r--spec/frontend/diffs/components/no_changes_spec.js7
-rw-r--r--spec/frontend/diffs/components/settings_dropdown_spec.js7
-rw-r--r--spec/frontend/diffs/components/tree_list_spec.js39
-rw-r--r--spec/frontend/diffs/store/actions_spec.js14
-rw-r--r--spec/frontend/editor/source_editor_yaml_ext_spec.js102
-rw-r--r--spec/frontend/emoji/awards_app/store/actions_spec.js37
-rw-r--r--spec/frontend/emoji/components/utils_spec.js3
-rw-r--r--spec/frontend/environment.js3
-rw-r--r--spec/frontend/environments/canary_ingress_spec.js27
-rw-r--r--spec/frontend/environments/canary_update_modal_spec.js11
-rw-r--r--spec/frontend/environments/commit_spec.js71
-rw-r--r--spec/frontend/environments/deploy_board_component_spec.js75
-rw-r--r--spec/frontend/environments/deploy_board_wrapper_spec.js124
-rw-r--r--spec/frontend/environments/deployment_spec.js243
-rw-r--r--spec/frontend/environments/environment_actions_spec.js4
-rw-r--r--spec/frontend/environments/environment_pin_spec.js74
-rw-r--r--spec/frontend/environments/environment_table_spec.js3
-rw-r--r--spec/frontend/environments/environments_app_spec.js8
-rw-r--r--spec/frontend/environments/graphql/mock_data.js66
-rw-r--r--spec/frontend/environments/graphql/resolvers_spec.js4
-rw-r--r--spec/frontend/environments/new_environment_folder_spec.js1
-rw-r--r--spec/frontend/environments/new_environment_item_spec.js185
-rw-r--r--spec/frontend/environments/new_environments_app_spec.js31
-rw-r--r--spec/frontend/error_tracking/components/error_details_spec.js160
-rw-r--r--spec/frontend/error_tracking/components/error_tracking_actions_spec.js24
-rw-r--r--spec/frontend/error_tracking/components/error_tracking_list_spec.js52
-rw-r--r--spec/frontend/error_tracking_settings/components/app_spec.js15
-rw-r--r--spec/frontend/error_tracking_settings/components/error_tracking_form_spec.js7
-rw-r--r--spec/frontend/error_tracking_settings/components/project_dropdown_spec.js19
-rw-r--r--spec/frontend/feature_flags/components/configure_feature_flags_modal_spec.js7
-rw-r--r--spec/frontend/feature_flags/components/edit_feature_flag_spec.js14
-rw-r--r--spec/frontend/feature_flags/components/empty_state_spec.js3
-rw-r--r--spec/frontend/feature_flags/components/environments_dropdown_spec.js15
-rw-r--r--spec/frontend/feature_flags/components/feature_flags_spec.js29
-rw-r--r--spec/frontend/feature_flags/components/feature_flags_table_spec.js8
-rw-r--r--spec/frontend/feature_flags/components/form_spec.js21
-rw-r--r--spec/frontend/feature_flags/components/new_environments_dropdown_spec.js12
-rw-r--r--spec/frontend/feature_flags/components/new_feature_flag_spec.js18
-rw-r--r--spec/frontend/feature_flags/components/strategies/flexible_rollout_spec.js3
-rw-r--r--spec/frontend/feature_flags/components/strategies/gitlab_user_list_spec.js11
-rw-r--r--spec/frontend/feature_flags/components/strategies/percent_rollout_spec.js3
-rw-r--r--spec/frontend/feature_flags/components/strategy_spec.js140
-rw-r--r--spec/frontend/feature_highlight/feature_highlight_popover_spec.js3
-rw-r--r--spec/frontend/filtered_search/dropdown_user_spec.js2
-rw-r--r--spec/frontend/filtered_search/dropdown_utils_spec.js4
-rw-r--r--spec/frontend/filtered_search/filtered_search_visual_tokens_spec.js9
-rw-r--r--spec/frontend/filtered_search/recent_searches_root_spec.js5
-rw-r--r--spec/frontend/fixtures/application_settings.rb1
-rw-r--r--spec/frontend/fixtures/blob.rb2
-rw-r--r--spec/frontend/fixtures/branches.rb2
-rw-r--r--spec/frontend/fixtures/clusters.rb2
-rw-r--r--spec/frontend/fixtures/commit.rb2
-rw-r--r--spec/frontend/fixtures/freeze_period.rb2
-rw-r--r--spec/frontend/fixtures/issues.rb11
-rw-r--r--spec/frontend/fixtures/jobs.rb2
-rw-r--r--spec/frontend/fixtures/listbox.rb31
-rw-r--r--spec/frontend/fixtures/merge_requests.rb13
-rw-r--r--spec/frontend/fixtures/merge_requests_diffs.rb2
-rw-r--r--spec/frontend/fixtures/pipeline_schedules.rb2
-rw-r--r--spec/frontend/fixtures/projects.rb2
-rw-r--r--spec/frontend/fixtures/prometheus_service.rb2
-rw-r--r--spec/frontend/fixtures/runner.rb44
-rw-r--r--spec/frontend/fixtures/services.rb2
-rw-r--r--spec/frontend/fixtures/snippet.rb2
-rw-r--r--spec/frontend/fixtures/tags.rb2
-rw-r--r--spec/frontend/fixtures/todos.rb2
-rw-r--r--spec/frontend/flash_spec.js8
-rw-r--r--spec/frontend/frequent_items/components/app_spec.js8
-rw-r--r--spec/frontend/frequent_items/components/frequent_items_list_item_spec.js7
-rw-r--r--spec/frontend/frequent_items/components/frequent_items_list_spec.js33
-rw-r--r--spec/frontend/frequent_items/components/frequent_items_search_input_spec.js9
-rw-r--r--spec/frontend/gl_form_spec.js15
-rw-r--r--spec/frontend/google_cloud/components/app_spec.js4
-rw-r--r--spec/frontend/google_cloud/components/deployments_service_table_spec.js7
-rw-r--r--spec/frontend/google_cloud/components/home_spec.js4
-rw-r--r--spec/frontend/google_cloud/components/service_accounts_form_spec.js19
-rw-r--r--spec/frontend/google_cloud/components/service_accounts_list_spec.js19
-rw-r--r--spec/frontend/google_tag_manager/index_spec.js178
-rw-r--r--spec/frontend/grafana_integration/components/grafana_integration_spec.js22
-rw-r--r--spec/frontend/group_settings/components/shared_runners_form_spec.js3
-rw-r--r--spec/frontend/groups/components/app_spec.js66
-rw-r--r--spec/frontend/groups/components/group_folder_spec.js6
-rw-r--r--spec/frontend/groups/components/groups_spec.js26
-rw-r--r--spec/frontend/groups/components/invite_members_banner_spec.js4
-rw-r--r--spec/frontend/groups/components/item_actions_spec.js38
-rw-r--r--spec/frontend/groups/components/transfer_group_form_spec.js131
-rw-r--r--spec/frontend/groups/landing_spec.js5
-rw-r--r--spec/frontend/groups/transfer_edit_spec.js31
-rw-r--r--spec/frontend/header_search/components/app_spec.js14
-rw-r--r--spec/frontend/header_search/components/header_search_autocomplete_items_spec.js4
-rw-r--r--spec/frontend/ide/components/activity_bar_spec.js11
-rw-r--r--spec/frontend/ide/components/branches/search_list_spec.js14
-rw-r--r--spec/frontend/ide/components/commit_sidebar/actions_spec.js29
-rw-r--r--spec/frontend/ide/components/commit_sidebar/editor_header_spec.js7
-rw-r--r--spec/frontend/ide/components/commit_sidebar/form_spec.js47
-rw-r--r--spec/frontend/ide/components/commit_sidebar/list_item_spec.js50
-rw-r--r--spec/frontend/ide/components/commit_sidebar/list_spec.js7
-rw-r--r--spec/frontend/ide/components/commit_sidebar/message_field_spec.js138
-rw-r--r--spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js52
-rw-r--r--spec/frontend/ide/components/commit_sidebar/radio_group_spec.js45
-rw-r--r--spec/frontend/ide/components/commit_sidebar/success_message_spec.js11
-rw-r--r--spec/frontend/ide/components/error_message_spec.js38
-rw-r--r--spec/frontend/ide/components/file_row_extra_spec.js74
-rw-r--r--spec/frontend/ide/components/file_templates/bar_spec.js28
-rw-r--r--spec/frontend/ide/components/file_templates/dropdown_spec.js36
-rw-r--r--spec/frontend/ide/components/ide_file_row_spec.js32
-rw-r--r--spec/frontend/ide/components/ide_review_spec.js16
-rw-r--r--spec/frontend/ide/components/ide_side_bar_spec.js19
-rw-r--r--spec/frontend/ide/components/ide_spec.js7
-rw-r--r--spec/frontend/ide/components/ide_status_bar_spec.js14
-rw-r--r--spec/frontend/ide/components/ide_status_list_spec.js7
-rw-r--r--spec/frontend/ide/components/ide_tree_list_spec.js13
-rw-r--r--spec/frontend/ide/components/ide_tree_spec.js6
-rw-r--r--spec/frontend/ide/components/jobs/detail_spec.js29
-rw-r--r--spec/frontend/ide/components/jobs/item_spec.js11
-rw-r--r--spec/frontend/ide/components/jobs/list_spec.js8
-rw-r--r--spec/frontend/ide/components/jobs/stage_spec.js15
-rw-r--r--spec/frontend/ide/components/merge_requests/item_spec.js7
-rw-r--r--spec/frontend/ide/components/merge_requests/list_spec.js75
-rw-r--r--spec/frontend/ide/components/nav_dropdown_button_spec.js32
-rw-r--r--spec/frontend/ide/components/nav_dropdown_spec.js23
-rw-r--r--spec/frontend/ide/components/new_dropdown/button_spec.js20
-rw-r--r--spec/frontend/ide/components/new_dropdown/index_spec.js12
-rw-r--r--spec/frontend/ide/components/new_dropdown/modal_spec.js17
-rw-r--r--spec/frontend/ide/components/panes/collapsible_sidebar_spec.js9
-rw-r--r--spec/frontend/ide/components/panes/right_spec.js29
-rw-r--r--spec/frontend/ide/components/preview/clientside_spec.js60
-rw-r--r--spec/frontend/ide/components/preview/navigator_spec.js120
-rw-r--r--spec/frontend/ide/components/repo_editor_spec.js28
-rw-r--r--spec/frontend/ide/components/repo_tab_spec.js7
-rw-r--r--spec/frontend/ide/components/repo_tabs_spec.js22
-rw-r--r--spec/frontend/ide/components/resizable_panel_spec.js14
-rw-r--r--spec/frontend/ide/components/shared/tokened_input_spec.js12
-rw-r--r--spec/frontend/ide/components/terminal/session_spec.js25
-rw-r--r--spec/frontend/ide/components/terminal/terminal_controls_spec.js15
-rw-r--r--spec/frontend/ide/components/terminal/view_spec.js8
-rw-r--r--spec/frontend/ide/components/terminal_sync/terminal_sync_status_safe_spec.js7
-rw-r--r--spec/frontend/ide/components/terminal_sync/terminal_sync_status_spec.js7
-rw-r--r--spec/frontend/ide/lib/common/model_spec.js7
-rw-r--r--spec/frontend/ide/stores/actions/file_spec.js6
-rw-r--r--spec/frontend/ide/stores/actions_spec.js17
-rw-r--r--spec/frontend/ide/stores/modules/clientside/actions_spec.js5
-rw-r--r--spec/frontend/ide/stores/plugins/terminal_spec.js5
-rw-r--r--spec/frontend/image_diff/helpers/badge_helper_spec.js9
-rw-r--r--spec/frontend/image_diff/helpers/dom_helper_spec.js12
-rw-r--r--spec/frontend/image_diff/image_diff_spec.js8
-rw-r--r--spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js2
-rw-r--r--spec/frontend/import_entities/import_groups/graphql/fixtures.js1
-rw-r--r--spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js8
-rw-r--r--spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js8
-rw-r--r--spec/frontend/incidents/components/incidents_list_spec.js11
-rw-r--r--spec/frontend/incidents/mocks/incidents.json4
-rw-r--r--spec/frontend/incidents_settings/components/__snapshots__/incidents_settings_tabs_spec.js.snap1
-rw-r--r--spec/frontend/integrations/edit/components/active_checkbox_spec.js3
-rw-r--r--spec/frontend/integrations/edit/components/confirmation_modal_spec.js3
-rw-r--r--spec/frontend/integrations/edit/components/dynamic_field_spec.js58
-rw-r--r--spec/frontend/integrations/edit/components/integration_form_spec.js363
-rw-r--r--spec/frontend/integrations/edit/components/jira_issues_fields_spec.js28
-rw-r--r--spec/frontend/integrations/edit/components/jira_trigger_fields_spec.js19
-rw-r--r--spec/frontend/integrations/edit/mock_data.js9
-rw-r--r--spec/frontend/invite_members/components/group_select_spec.js30
-rw-r--r--spec/frontend/invite_members/components/import_a_project_modal_spec.js5
-rw-r--r--spec/frontend/invite_members/components/invite_group_trigger_spec.js2
-rw-r--r--spec/frontend/invite_members/components/invite_groups_modal_spec.js143
-rw-r--r--spec/frontend/invite_members/components/invite_members_modal_spec.js495
-rw-r--r--spec/frontend/invite_members/components/invite_members_trigger_spec.js1
-rw-r--r--spec/frontend/invite_members/components/invite_modal_base_spec.js103
-rw-r--r--spec/frontend/invite_members/mock_data/group_modal.js11
-rw-r--r--spec/frontend/invite_members/mock_data/member_modal.js36
-rw-r--r--spec/frontend/invite_members/mock_data/modal_base.js11
-rw-r--r--spec/frontend/issuable/components/issue_milestone_spec.js24
-rw-r--r--spec/frontend/issuable/components/related_issuable_item_spec.js9
-rw-r--r--spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js72
-rw-r--r--spec/frontend/issuable/related_issues/components/related_issues_block_spec.js29
-rw-r--r--spec/frontend/issuable/related_issues/components/related_issues_root_spec.js30
-rw-r--r--spec/frontend/issues/create_merge_request_dropdown_spec.js4
-rw-r--r--spec/frontend/issues/list/components/issues_list_app_spec.js161
-rw-r--r--spec/frontend/issues/list/components/jira_issues_import_status_app_spec.js9
-rw-r--r--spec/frontend/issues/list/components/new_issue_dropdown_spec.js18
-rw-r--r--spec/frontend/issues/list/mock_data.js21
-rw-r--r--spec/frontend/issues/list/utils_spec.js13
-rw-r--r--spec/frontend/issues/new/components/title_suggestions_spec.js57
-rw-r--r--spec/frontend/issues/related_merge_requests/components/related_merge_requests_spec.js10
-rw-r--r--spec/frontend/issues/show/components/app_spec.js165
-rw-r--r--spec/frontend/issues/show/components/description_spec.js294
-rw-r--r--spec/frontend/issues/show/components/fields/description_spec.js1
-rw-r--r--spec/frontend/issues/show/components/fields/type_spec.js9
-rw-r--r--spec/frontend/issues/show/components/form_spec.js3
-rw-r--r--spec/frontend/issues/show/components/header_actions_spec.js4
-rw-r--r--spec/frontend/issues/show/components/title_spec.js46
-rw-r--r--spec/frontend/issues/show/mock_data/mock_data.js14
-rw-r--r--spec/frontend/jira_connect/branches/components/new_branch_form_spec.js143
-rw-r--r--spec/frontend/jira_connect/branches/components/project_dropdown_spec.js74
-rw-r--r--spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js19
-rw-r--r--spec/frontend/jira_connect/branches/mock_data.js30
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item_spec.js3
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_spec.js110
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/app_spec.js96
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/compatibility_alert_spec.js56
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/sign_in_button_spec.js12
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js3
-rw-r--r--spec/frontend/jira_connect/subscriptions/pages/sign_in_spec.js62
-rw-r--r--spec/frontend/jira_connect/subscriptions/pages/subscriptions_spec.js56
-rw-r--r--spec/frontend/jira_import/components/jira_import_app_spec.js6
-rw-r--r--spec/frontend/jobs/bridge/app_spec.js18
-rw-r--r--spec/frontend/jobs/components/job_app_spec.js18
-rw-r--r--spec/frontend/jobs/components/job_container_item_spec.js3
-rw-r--r--spec/frontend/jobs/components/job_log_controllers_spec.js9
-rw-r--r--spec/frontend/jobs/components/log/collapsible_section_spec.js8
-rw-r--r--spec/frontend/jobs/components/log/line_header_spec.js8
-rw-r--r--spec/frontend/jobs/components/log/log_spec.js11
-rw-r--r--spec/frontend/jobs/components/manual_variables_form_spec.js7
-rw-r--r--spec/frontend/jobs/components/sidebar_spec.js3
-rw-r--r--spec/frontend/jobs/components/table/cells/actions_cell_spec.js31
-rw-r--r--spec/frontend/jobs/components/table/job_table_app_spec.js53
-rw-r--r--spec/frontend/jobs/mixins/delayed_job_mixin_spec.js11
-rw-r--r--spec/frontend/jobs/mock_data.js97
-rw-r--r--spec/frontend/lib/apollo/suppress_network_errors_during_navigation_link_spec.js2
-rw-r--r--spec/frontend/lib/utils/apollo_startup_js_link_spec.js2
-rw-r--r--spec/frontend/lib/utils/common_utils_spec.js9
-rw-r--r--spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js18
-rw-r--r--spec/frontend/lib/utils/number_utility_spec.js16
-rw-r--r--spec/frontend/lib/utils/table_utility_spec.js31
-rw-r--r--spec/frontend/lib/utils/text_markdown_spec.js74
-rw-r--r--spec/frontend/lib/utils/vuex_module_mappers_spec.js10
-rw-r--r--spec/frontend/lib/utils/yaml_spec.js105
-rw-r--r--spec/frontend/listbox/index_spec.js111
-rw-r--r--spec/frontend/listbox/redirect_behavior_spec.js51
-rw-r--r--spec/frontend/logs/components/environment_logs_spec.js22
-rw-r--r--spec/frontend/logs/components/log_control_buttons_spec.js31
-rw-r--r--spec/frontend/members/components/action_buttons/approve_access_request_button_spec.js7
-rw-r--r--spec/frontend/members/components/action_buttons/remove_group_link_button_spec.js7
-rw-r--r--spec/frontend/members/components/action_buttons/remove_member_button_spec.js7
-rw-r--r--spec/frontend/members/components/action_buttons/resend_invite_button_spec.js7
-rw-r--r--spec/frontend/members/components/app_spec.js8
-rw-r--r--spec/frontend/members/components/avatars/user_avatar_spec.js63
-rw-r--r--spec/frontend/members/components/filter_sort/filter_sort_container_spec.js7
-rw-r--r--spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js7
-rw-r--r--spec/frontend/members/components/filter_sort/sort_dropdown_spec.js7
-rw-r--r--spec/frontend/members/components/modals/leave_modal_spec.js8
-rw-r--r--spec/frontend/members/components/modals/remove_group_link_modal_spec.js8
-rw-r--r--spec/frontend/members/components/table/expiration_datepicker_spec.js8
-rw-r--r--spec/frontend/members/components/table/members_table_cell_spec.js9
-rw-r--r--spec/frontend/members/components/table/members_table_spec.js11
-rw-r--r--spec/frontend/members/components/table/role_dropdown_spec.js8
-rw-r--r--spec/frontend/members/mock_data.js2
-rw-r--r--spec/frontend/merge_conflicts/components/merge_conflict_resolver_app_spec.js10
-rw-r--r--spec/frontend/merge_conflicts/store/actions_spec.js5
-rw-r--r--spec/frontend/merge_request_spec.js67
-rw-r--r--spec/frontend/mocks_spec.js11
-rw-r--r--spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap22
-rw-r--r--spec/frontend/monitoring/components/charts/stacked_column_spec.js25
-rw-r--r--spec/frontend/monitoring/components/charts/time_series_spec.js142
-rw-r--r--spec/frontend/monitoring/components/create_dashboard_modal_spec.js10
-rw-r--r--spec/frontend/monitoring/components/dashboard_actions_menu_spec.js100
-rw-r--r--spec/frontend/monitoring/components/dashboard_header_spec.js143
-rw-r--r--spec/frontend/monitoring/components/dashboard_panel_builder_spec.js67
-rw-r--r--spec/frontend/monitoring/components/dashboard_panel_spec.js137
-rw-r--r--spec/frontend/monitoring/components/dashboard_spec.js341
-rw-r--r--spec/frontend/monitoring/components/dashboard_url_time_spec.js101
-rw-r--r--spec/frontend/monitoring/components/embeds/embed_group_spec.js15
-rw-r--r--spec/frontend/monitoring/components/embeds/metric_embed_spec.js7
-rw-r--r--spec/frontend/monitoring/components/graph_group_spec.js35
-rw-r--r--spec/frontend/monitoring/components/links_section_spec.js20
-rw-r--r--spec/frontend/monitoring/components/refresh_button_spec.js13
-rw-r--r--spec/frontend/monitoring/components/variables/dropdown_field_spec.js8
-rw-r--r--spec/frontend/monitoring/components/variables/text_field_spec.js22
-rw-r--r--spec/frontend/monitoring/components/variables_section_spec.js39
-rw-r--r--spec/frontend/monitoring/router_spec.js7
-rw-r--r--spec/frontend/mr_popover/mr_popover_spec.js29
-rw-r--r--spec/frontend/nav/components/responsive_app_spec.js5
-rw-r--r--spec/frontend/nav/components/top_nav_menu_item_spec.js1
-rw-r--r--spec/frontend/notebook/cells/code_spec.js22
-rw-r--r--spec/frontend/notebook/cells/markdown_spec.js71
-rw-r--r--spec/frontend/notebook/cells/output/index_spec.js36
-rw-r--r--spec/frontend/notebook/cells/prompt_spec.js14
-rw-r--r--spec/frontend/notebook/index_spec.js20
-rw-r--r--spec/frontend/notes/components/comment_form_spec.js13
-rw-r--r--spec/frontend/notes/components/diff_discussion_header_spec.js63
-rw-r--r--spec/frontend/notes/components/discussion_counter_spec.js34
-rw-r--r--spec/frontend/notes/components/discussion_filter_spec.js56
-rw-r--r--spec/frontend/notes/components/discussion_navigator_spec.js10
-rw-r--r--spec/frontend/notes/components/discussion_notes_spec.js29
-rw-r--r--spec/frontend/notes/components/discussion_resolve_button_spec.js17
-rw-r--r--spec/frontend/notes/components/discussion_resolve_with_issue_button_spec.js5
-rw-r--r--spec/frontend/notes/components/note_actions_spec.js40
-rw-r--r--spec/frontend/notes/components/note_body_spec.js6
-rw-r--r--spec/frontend/notes/components/note_form_spec.js2
-rw-r--r--spec/frontend/notes/components/note_header_spec.js8
-rw-r--r--spec/frontend/notes/components/noteable_note_spec.js24
-rw-r--r--spec/frontend/notes/components/notes_app_spec.js20
-rw-r--r--spec/frontend/notes/components/sort_discussion_spec.js7
-rw-r--r--spec/frontend/notes/components/timeline_toggle_spec.js15
-rw-r--r--spec/frontend/notes/deprecated_notes_spec.js81
-rw-r--r--spec/frontend/notes/mixins/discussion_navigation_spec.js57
-rw-r--r--spec/frontend/notifications/components/custom_notifications_modal_spec.js7
-rw-r--r--spec/frontend/notifications/components/notifications_dropdown_spec.js8
-rw-r--r--spec/frontend/operation_settings/components/metrics_settings_spec.js21
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js15
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_loader_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cli_commands_spec.js21
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state_spec.js7
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/project_empty_state_spec.js7
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js14
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js30
-rw-r--r--spec/frontend/packages_and_registries/dependency_proxy/app_spec.js13
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/app_spec.js8
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/details_title_spec.js14
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/terraform_installation_spec.js7
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap5
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_search_spec.js7
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_title_spec.js39
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js72
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_spec.js23
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/shared/package_list_row_spec.js3
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/version_row_spec.js.snap14
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/package_title_spec.js9
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/functional/delete_package_spec.js7
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js8
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js24
-rw-r--r--spec/frontend/packages_and_registries/package_registry/mock_data.js1
-rw-r--r--spec/frontend/packages_and_registries/package_registry/pages/__snapshots__/list_spec.js.snap4
-rw-r--r--spec/frontend/packages_and_registries/package_registry/pages/details_spec.js19
-rw-r--r--spec/frontend/packages_and_registries/package_registry/pages/list_spec.js24
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/dependency_proxy_settings_spec.js7
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js10
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js9
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js31
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/settings_form_spec.js15
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/mock_data.js1
-rw-r--r--spec/frontend/pager_spec.js57
-rw-r--r--spec/frontend/pages/admin/projects/components/namespace_select_spec.js7
-rw-r--r--spec/frontend/pages/dashboard/todos/index/todos_spec.js5
-rw-r--r--spec/frontend/pages/profiles/password_prompt/password_prompt_modal_spec.js7
-rw-r--r--spec/frontend/pages/projects/forks/new/components/fork_form_spec.js11
-rw-r--r--spec/frontend/pages/projects/graphs/code_coverage_spec.js5
-rw-r--r--spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_spec.js.snap27
-rw-r--r--spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_section_link_spec.js38
-rw-r--r--spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_spec.js30
-rw-r--r--spec/frontend/pages/projects/learn_gitlab/components/mock_data.js2
-rw-r--r--spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js13
-rw-r--r--spec/frontend/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js8
-rw-r--r--spec/frontend/pages/projects/shared/permissions/components/project_setting_row_spec.js33
-rw-r--r--spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js2
-rw-r--r--spec/frontend/pages/shared/wikis/components/wiki_form_spec.js6
-rw-r--r--spec/frontend/performance_bar/components/add_request_spec.js25
-rw-r--r--spec/frontend/persistent_user_callout_spec.js8
-rw-r--r--spec/frontend/pipeline_editor/components/commit/commit_section_spec.js53
-rw-r--r--spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js46
-rw-r--r--spec/frontend/pipeline_editor/components/header/pipeline_status_spec.js11
-rw-r--r--spec/frontend/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js11
-rw-r--r--spec/frontend/pipeline_editor/components/header/validation_segment_spec.js7
-rw-r--r--spec/frontend/pipeline_editor/components/ui/pipeline_editor_messages_spec.js13
-rw-r--r--spec/frontend/pipeline_editor/pipeline_editor_app_spec.js36
-rw-r--r--spec/frontend/pipeline_new/components/pipeline_new_form_spec.js13
-rw-r--r--spec/frontend/pipeline_wizard/components/commit_spec.js282
-rw-r--r--spec/frontend/pipeline_wizard/components/editor_spec.js69
-rw-r--r--spec/frontend/pipeline_wizard/components/step_nav_spec.js79
-rw-r--r--spec/frontend/pipeline_wizard/components/widgets/text_spec.js152
-rw-r--r--spec/frontend/pipeline_wizard/mock/query_responses.js62
-rw-r--r--spec/frontend/pipelines/__snapshots__/utils_spec.js.snap11
-rw-r--r--spec/frontend/pipelines/components/dag/dag_annotations_spec.js19
-rw-r--r--spec/frontend/pipelines/components/dag/dag_spec.js9
-rw-r--r--spec/frontend/pipelines/components/jobs/jobs_app_spec.js13
-rw-r--r--spec/frontend/pipelines/components/pipelines_filtered_search_spec.js37
-rw-r--r--spec/frontend/pipelines/graph/action_component_spec.js23
-rw-r--r--spec/frontend/pipelines/graph/graph_component_wrapper_spec.js58
-rw-r--r--spec/frontend/pipelines/graph/job_item_spec.js49
-rw-r--r--spec/frontend/pipelines/graph/linked_pipeline_spec.js113
-rw-r--r--spec/frontend/pipelines/graph/linked_pipelines_column_spec.js30
-rw-r--r--spec/frontend/pipelines/graph/mock_data.js12
-rw-r--r--spec/frontend/pipelines/header_component_spec.js5
-rw-r--r--spec/frontend/pipelines/mock_data.js680
-rw-r--r--spec/frontend/pipelines/notification/deprecated_type_keyword_notification_spec.js146
-rw-r--r--spec/frontend/pipelines/notification/mock_data.js33
-rw-r--r--spec/frontend/pipelines/pipeline_triggerer_spec.js8
-rw-r--r--spec/frontend/pipelines/pipeline_url_spec.js349
-rw-r--r--spec/frontend/pipelines/pipelines_actions_spec.js20
-rw-r--r--spec/frontend/pipelines/pipelines_table_spec.js50
-rw-r--r--spec/frontend/pipelines/test_reports/test_case_details_spec.js5
-rw-r--r--spec/frontend/pipelines/test_reports/test_reports_spec.js7
-rw-r--r--spec/frontend/pipelines/test_reports/test_suite_table_spec.js7
-rw-r--r--spec/frontend/pipelines/test_reports/test_summary_table_spec.js7
-rw-r--r--spec/frontend/popovers/components/popovers_spec.js15
-rw-r--r--spec/frontend/popovers/index_spec.js17
-rw-r--r--spec/frontend/profile/account/components/delete_account_modal_spec.js74
-rw-r--r--spec/frontend/profile/account/components/update_username_spec.js11
-rw-r--r--spec/frontend/projects/commit/components/branches_dropdown_spec.js4
-rw-r--r--spec/frontend/projects/commit/components/form_modal_spec.js7
-rw-r--r--spec/frontend/projects/commits/components/author_select_spec.js60
-rw-r--r--spec/frontend/projects/compare/components/app_spec.js7
-rw-r--r--spec/frontend/projects/compare/components/repo_dropdown_spec.js7
-rw-r--r--spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js3
-rw-r--r--spec/frontend/projects/compare/components/revision_dropdown_spec.js3
-rw-r--r--spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap33
-rw-r--r--spec/frontend/projects/components/project_delete_button_spec.js5
-rw-r--r--spec/frontend/projects/components/shared/__snapshots__/delete_button_spec.js.snap54
-rw-r--r--spec/frontend/projects/components/shared/delete_button_spec.js30
-rw-r--r--spec/frontend/projects/new/components/deployment_target_select_spec.js82
-rw-r--r--spec/frontend/projects/new/components/new_project_url_select_spec.js9
-rw-r--r--spec/frontend/projects/pipelines/charts/components/app_spec.js7
-rw-r--r--spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_charts_spec.js43
-rw-r--r--spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js12
-rw-r--r--spec/frontend/projects/project_find_file_spec.js5
-rw-r--r--spec/frontend/projects/settings/components/shared_runners_toggle_spec.js3
-rw-r--r--spec/frontend/projects/settings/components/transfer_project_form_spec.js10
-rw-r--r--spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js2
-rw-r--r--spec/frontend/prometheus_alerts/components/reset_key_spec.js44
-rw-r--r--spec/frontend/prometheus_metrics/prometheus_metrics_spec.js30
-rw-r--r--spec/frontend/ref/components/ref_selector_spec.js41
-rw-r--r--spec/frontend/related_issues/components/related_issuable_input_spec.js3
-rw-r--r--spec/frontend/releases/components/app_edit_new_spec.js3
-rw-r--r--spec/frontend/releases/components/app_index_apollo_client_spec.js14
-rw-r--r--spec/frontend/releases/components/app_show_spec.js13
-rw-r--r--spec/frontend/releases/components/asset_links_form_spec.js7
-rw-r--r--spec/frontend/releases/components/evidence_block_spec.js8
-rw-r--r--spec/frontend/releases/components/release_block_footer_spec.js5
-rw-r--r--spec/frontend/releases/components/release_block_milestone_info_spec.js30
-rw-r--r--spec/frontend/releases/components/release_block_spec.js5
-rw-r--r--spec/frontend/releases/components/releases_pagination_spec.js7
-rw-r--r--spec/frontend/releases/components/releases_sort_spec.js7
-rw-r--r--spec/frontend/releases/components/tag_field_exsting_spec.js7
-rw-r--r--spec/frontend/releases/components/tag_field_new_spec.js4
-rw-r--r--spec/frontend/reports/accessibility_report/components/accessibility_issue_body_spec.js14
-rw-r--r--spec/frontend/reports/accessibility_report/grouped_accessibility_reports_app_spec.js10
-rw-r--r--spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js7
-rw-r--r--spec/frontend/reports/components/report_section_spec.js92
-rw-r--r--spec/frontend/reports/grouped_test_report/components/test_issue_body_spec.js7
-rw-r--r--spec/frontend/reports/grouped_test_report/grouped_test_reports_app_spec.js7
-rw-r--r--spec/frontend/repository/components/blob_content_viewer_spec.js92
-rw-r--r--spec/frontend/repository/components/blob_controls_spec.js9
-rw-r--r--spec/frontend/repository/components/blob_viewers/download_viewer_spec.js37
-rw-r--r--spec/frontend/repository/components/blob_viewers/image_viewer_spec.js12
-rw-r--r--spec/frontend/repository/components/blob_viewers/lfs_viewer_spec.js41
-rw-r--r--spec/frontend/repository/components/blob_viewers/pdf_viewer_spec.js10
-rw-r--r--spec/frontend/repository/components/blob_viewers/video_viewer_spec.js6
-rw-r--r--spec/frontend/repository/components/breadcrumbs_spec.js9
-rw-r--r--spec/frontend/repository/components/last_commit_spec.js25
-rw-r--r--spec/frontend/repository/components/preview/index_spec.js25
-rw-r--r--spec/frontend/repository/components/table/index_spec.js14
-rw-r--r--spec/frontend/repository/components/table/row_spec.js84
-rw-r--r--spec/frontend/repository/components/tree_content_spec.js13
-rw-r--r--spec/frontend/repository/components/upload_blob_modal_spec.js5
-rw-r--r--spec/frontend/repository/mock_data.js5
-rw-r--r--spec/frontend/runner/admin_runner_edit/admin_runner_edit_app_spec.js14
-rw-r--r--spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js146
-rw-r--r--spec/frontend/runner/admin_runners/admin_runners_app_spec.js51
-rw-r--r--spec/frontend/runner/components/cells/link_cell_spec.js72
-rw-r--r--spec/frontend/runner/components/cells/runner_actions_cell_spec.js201
-rw-r--r--spec/frontend/runner/components/registration/registration_dropdown_spec.js15
-rw-r--r--spec/frontend/runner/components/registration/registration_token_reset_dropdown_item_spec.js15
-rw-r--r--spec/frontend/runner/components/registration/registration_token_spec.js20
-rw-r--r--spec/frontend/runner/components/runner_assigned_item_spec.js53
-rw-r--r--spec/frontend/runner/components/runner_details_spec.js189
-rw-r--r--spec/frontend/runner/components/runner_edit_button_spec.js41
-rw-r--r--spec/frontend/runner/components/runner_filtered_search_bar_spec.js41
-rw-r--r--spec/frontend/runner/components/runner_groups_spec.js67
-rw-r--r--spec/frontend/runner/components/runner_header_spec.js44
-rw-r--r--spec/frontend/runner/components/runner_jobs_spec.js156
-rw-r--r--spec/frontend/runner/components/runner_jobs_table_spec.js119
-rw-r--r--spec/frontend/runner/components/runner_list_spec.js50
-rw-r--r--spec/frontend/runner/components/runner_pagination_spec.js1
-rw-r--r--spec/frontend/runner/components/runner_pause_button_spec.js239
-rw-r--r--spec/frontend/runner/components/runner_projects_spec.js193
-rw-r--r--spec/frontend/runner/components/runner_type_tabs_spec.js22
-rw-r--r--spec/frontend/runner/components/runner_update_form_spec.js46
-rw-r--r--spec/frontend/runner/group_runners/group_runners_app_spec.js79
-rw-r--r--spec/frontend/runner/mock_data.js10
-rw-r--r--spec/frontend/runner/utils_spec.js65
-rw-r--r--spec/frontend/search/sidebar/components/confidentiality_filter_spec.js7
-rw-r--r--spec/frontend/search/sidebar/components/radio_filter_spec.js7
-rw-r--r--spec/frontend/search/sidebar/components/status_filter_spec.js7
-rw-r--r--spec/frontend/search/sort/components/app_spec.js7
-rw-r--r--spec/frontend/security_configuration/components/app_spec.js24
-rw-r--r--spec/frontend/security_configuration/components/feature_card_spec.js1
-rw-r--r--spec/frontend/security_configuration/components/training_provider_list_spec.js200
-rw-r--r--spec/frontend/security_configuration/components/upgrade_banner_spec.js85
-rw-r--r--spec/frontend/security_configuration/mock_data.js57
-rw-r--r--spec/frontend/serverless/components/__snapshots__/empty_state_spec.js.snap2
-rw-r--r--spec/frontend/serverless/components/function_details_spec.js11
-rw-r--r--spec/frontend/serverless/components/functions_spec.js52
-rw-r--r--spec/frontend/serverless/survey_banner_spec.js51
-rw-r--r--spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js19
-rw-r--r--spec/frontend/settings_panels_spec.js14
-rw-r--r--spec/frontend/sidebar/assignees_realtime_spec.js7
-rw-r--r--spec/frontend/sidebar/assignees_spec.js8
-rw-r--r--spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js29
-rw-r--r--spec/frontend/sidebar/components/assignees/sidebar_editable_item_spec.js11
-rw-r--r--spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js5
-rw-r--r--spec/frontend/sidebar/components/confidential/sidebar_confidentiality_widget_spec.js9
-rw-r--r--spec/frontend/sidebar/components/mock_data.js2
-rw-r--r--spec/frontend/sidebar/components/participants/sidebar_participants_widget_spec.js5
-rw-r--r--spec/frontend/sidebar/components/severity/sidebar_severity_spec.js8
-rw-r--r--spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js17
-rw-r--r--spec/frontend/sidebar/components/time_tracking/report_spec.js7
-rw-r--r--spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js15
-rw-r--r--spec/frontend/sidebar/components/todo_toggle/sidebar_todo_widget_spec.js10
-rw-r--r--spec/frontend/sidebar/lock/edit_form_buttons_spec.js25
-rw-r--r--spec/frontend/sidebar/lock/issuable_lock_form_spec.js22
-rw-r--r--spec/frontend/sidebar/mock_data.js10
-rw-r--r--spec/frontend/sidebar/participants_spec.js40
-rw-r--r--spec/frontend/sidebar/sidebar_assignees_spec.js8
-rw-r--r--spec/frontend/sidebar/sidebar_move_issue_spec.js54
-rw-r--r--spec/frontend/sidebar/todo_spec.js8
-rw-r--r--spec/frontend/snippets/components/edit_spec.js14
-rw-r--r--spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js3
-rw-r--r--spec/frontend/snippets/components/snippet_blob_view_spec.js38
-rw-r--r--spec/frontend/snippets/components/snippet_header_spec.js25
-rw-r--r--spec/frontend/static_site_editor/components/edit_meta_controls_spec.js5
-rw-r--r--spec/frontend/static_site_editor/components/edit_meta_modal_spec.js15
-rw-r--r--spec/frontend/static_site_editor/pages/home_spec.js28
-rw-r--r--spec/frontend/terraform/components/states_table_actions_spec.js11
-rw-r--r--spec/frontend/terraform/components/states_table_spec.js9
-rw-r--r--spec/frontend/terraform/components/terraform_list_spec.js8
-rw-r--r--spec/frontend/toggles/index_spec.js149
-rw-r--r--spec/frontend/token_access/token_access_spec.js6
-rw-r--r--spec/frontend/tooltips/components/tooltips_spec.js35
-rw-r--r--spec/frontend/tooltips/index_spec.js15
-rw-r--r--spec/frontend/user_lists/components/add_user_modal_spec.js5
-rw-r--r--spec/frontend/user_lists/components/edit_user_list_spec.js18
-rw-r--r--spec/frontend/user_lists/components/new_user_list_spec.js14
-rw-r--r--spec/frontend/user_lists/components/user_list_spec.js22
-rw-r--r--spec/frontend/user_lists/components/user_lists_spec.js8
-rw-r--r--spec/frontend/user_lists/components/user_lists_table_spec.js28
-rw-r--r--spec/frontend/vue_alerts_spec.js7
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_collapsible_extension_spec.js5
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_author_spec.js3
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_expandable_section_spec.js5
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_memory_usage_spec.js32
-rw-r--r--spec/frontend/vue_mr_widget/components/states/__snapshots__/mr_widget_pipeline_failed_spec.js.snap2
-rw-r--r--spec/frontend/vue_mr_widget/components/states/commit_edit_spec.js10
-rw-r--r--spec/frontend/vue_mr_widget/components/states/merge_failed_pipeline_confirmation_dialog_spec.js78
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js39
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_commit_message_dropdown_spec.js8
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_commits_header_spec.js32
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js5
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js27
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_merging_spec.js25
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js7
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_nothing_to_merge_spec.js4
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js218
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_squash_before_merge_spec.js7
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js26
-rw-r--r--spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js14
-rw-r--r--spec/frontend/vue_mr_widget/deployment/deployment_actions_spec.js64
-rw-r--r--spec/frontend/vue_mr_widget/deployment/deployment_mock_data.js3
-rw-r--r--spec/frontend/vue_mr_widget/extentions/accessibility/index_spec.js125
-rw-r--r--spec/frontend/vue_mr_widget/extentions/accessibility/mock_data.js137
-rw-r--r--spec/frontend/vue_shared/alert_details/alert_details_spec.js10
-rw-r--r--spec/frontend/vue_shared/alert_details/alert_management_sidebar_todo_spec.js9
-rw-r--r--spec/frontend/vue_shared/alert_details/alert_metrics_spec.js3
-rw-r--r--spec/frontend/vue_shared/alert_details/alert_status_spec.js23
-rw-r--r--spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_assignees_spec.js21
-rw-r--r--spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_status_spec.js3
-rw-r--r--spec/frontend/vue_shared/components/blob_viewers/__snapshots__/simple_viewer_spec.js.snap4
-rw-r--r--spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js32
-rw-r--r--spec/frontend/vue_shared/components/chronic_duration_input_spec.js25
-rw-r--r--spec/frontend/vue_shared/components/confirm_fork_modal_spec.js80
-rw-r--r--spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js258
-rw-r--r--spec/frontend/vue_shared/components/deployment_instance/deployment_instance_spec.js29
-rw-r--r--spec/frontend/vue_shared/components/design_management/design_note_pin_spec.js68
-rw-r--r--spec/frontend/vue_shared/components/diff_viewer/diff_viewer_spec.js44
-rw-r--r--spec/frontend/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js120
-rw-r--r--spec/frontend/vue_shared/components/diff_viewer/viewers/renamed_spec.js26
-rw-r--r--spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js3
-rw-r--r--spec/frontend/vue_shared/components/dropdown/dropdown_search_input_spec.js8
-rw-r--r--spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js3
-rw-r--r--spec/frontend/vue_shared/components/expand_button_spec.js39
-rw-r--r--spec/frontend/vue_shared/components/file_finder/index_spec.js152
-rw-r--r--spec/frontend/vue_shared/components/file_finder/item_spec.js53
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js33
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js26
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js141
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js9
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js7
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js35
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js3
-rw-r--r--spec/frontend/vue_shared/components/gfm_autocomplete/__snapshots__/utils_spec.js.snap54
-rw-r--r--spec/frontend/vue_shared/components/gfm_autocomplete/gfm_autocomplete_spec.js34
-rw-r--r--spec/frontend/vue_shared/components/gfm_autocomplete/utils_spec.js427
-rw-r--r--spec/frontend/vue_shared/components/gl_countdown_spec.js20
-rw-r--r--spec/frontend/vue_shared/components/gl_modal_vuex_spec.js24
-rw-r--r--spec/frontend/vue_shared/components/help_popover_spec.js110
-rw-r--r--spec/frontend/vue_shared/components/local_storage_sync_spec.js27
-rw-r--r--spec/frontend/vue_shared/components/markdown/field_spec.js111
-rw-r--r--spec/frontend/vue_shared/components/markdown/header_spec.js49
-rw-r--r--spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js10
-rw-r--r--spec/frontend/vue_shared/components/markdown/suggestions_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/modal_copy_button_spec.js21
-rw-r--r--spec/frontend/vue_shared/components/multiselect_dropdown_spec.js35
-rw-r--r--spec/frontend/vue_shared/components/namespace_select/mock_data.js9
-rw-r--r--spec/frontend/vue_shared/components/namespace_select/namespace_select_spec.js151
-rw-r--r--spec/frontend/vue_shared/components/notes/noteable_warning_spec.js15
-rw-r--r--spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js15
-rw-r--r--spec/frontend/vue_shared/components/pikaday_spec.js41
-rw-r--r--spec/frontend/vue_shared/components/project_avatar/default_spec.js30
-rw-r--r--spec/frontend/vue_shared/components/project_selector/project_selector_spec.js46
-rw-r--r--spec/frontend/vue_shared/components/registry/list_item_spec.js7
-rw-r--r--spec/frontend/vue_shared/components/resizable_chart/resizable_chart_container_spec.js15
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js20
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js68
-rw-r--r--spec/frontend/vue_shared/components/sidebar/date_picker_spec.js125
-rw-r--r--spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js23
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js9
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js65
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js68
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_title_spec.js9
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed_spec.js3
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js33
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js12
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js3
-rw-r--r--spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js3
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js (renamed from spec/frontend/vue_shared/components/source_viewer_spec.js)43
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/utils_spec.js13
-rw-r--r--spec/frontend/vue_shared/components/split_button_spec.js9
-rw-r--r--spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap7
-rw-r--r--spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js99
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js10
-rw-r--r--spec/frontend/vue_shared/components/user_select_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/web_ide_link_spec.js51
-rw-r--r--spec/frontend/vue_shared/directives/track_event_spec.js11
-rw-r--r--spec/frontend/vue_shared/issuable/list/components/issuable_bulk_edit_sidebar_spec.js5
-rw-r--r--spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js33
-rw-r--r--spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js15
-rw-r--r--spec/frontend/vue_shared/issuable/list/components/issuable_tabs_spec.js86
-rw-r--r--spec/frontend/vue_shared/issuable/list/mock_data.js2
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js11
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js6
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js5
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js5
-rw-r--r--spec/frontend/vue_shared/issuable/sidebar/components/issuable_sidebar_root_spec.js8
-rw-r--r--spec/frontend/vue_shared/new_namespace/components/welcome_spec.js5
-rw-r--r--spec/frontend/vue_shared/security_reports/components/manage_via_mr_spec.js6
-rw-r--r--spec/frontend/vue_shared/security_reports/mock_data.js2
-rw-r--r--spec/frontend/whats_new/components/app_spec.js11
-rw-r--r--spec/frontend/work_items/mock_data.js14
-rw-r--r--spec/frontend/work_items/pages/create_work_item_spec.js146
-rw-r--r--spec/frontend/work_items/pages/work_item_root_spec.js6
-rw-r--r--spec/frontend/work_items/router_spec.js10
-rw-r--r--spec/frontend/work_items_hierarchy/components/app_spec.js63
-rw-r--r--spec/frontend/work_items_hierarchy/components/hierarchy_spec.js118
-rw-r--r--spec/frontend/work_items_hierarchy/hierarchy_util_spec.js16
-rw-r--r--spec/frontend/zen_mode_spec.js2
-rw-r--r--spec/frontend_integration/ide/ide_integration_spec.js7
-rw-r--r--spec/graphql/features/authorization_spec.rb2
-rw-r--r--spec/graphql/graphql_triggers_spec.rb14
-rw-r--r--spec/graphql/mutations/alert_management/alerts/todo/create_spec.rb2
-rw-r--r--spec/graphql/mutations/ci/runner/delete_spec.rb15
-rw-r--r--spec/graphql/mutations/issues/create_spec.rb2
-rw-r--r--spec/graphql/resolvers/ci/project_pipeline_counts_resolver_spec.rb63
-rw-r--r--spec/graphql/resolvers/ci/runner_jobs_resolver_spec.rb49
-rw-r--r--spec/graphql/resolvers/ci/runners_resolver_spec.rb111
-rw-r--r--spec/graphql/resolvers/clusters/agent_tokens_resolver_spec.rb8
-rw-r--r--spec/graphql/resolvers/clusters/agents_resolver_spec.rb10
-rw-r--r--spec/graphql/resolvers/merge_requests_resolver_spec.rb60
-rw-r--r--spec/graphql/resolvers/package_details_resolver_spec.rb2
-rw-r--r--spec/graphql/resolvers/package_pipelines_resolver_spec.rb7
-rw-r--r--spec/graphql/resolvers/recent_boards_resolver_spec.rb79
-rw-r--r--spec/graphql/types/ci/pipeline_counts_type_spec.rb87
-rw-r--r--spec/graphql/types/ci/runner_type_spec.rb3
-rw-r--r--spec/graphql/types/clusters/agent_activity_event_type_spec.rb2
-rw-r--r--spec/graphql/types/clusters/agent_token_type_spec.rb2
-rw-r--r--spec/graphql/types/clusters/agent_type_spec.rb2
-rw-r--r--spec/graphql/types/global_id_type_spec.rb2
-rw-r--r--spec/graphql/types/group_type_spec.rb1
-rw-r--r--spec/graphql/types/issuable_type_spec.rb6
-rw-r--r--spec/graphql/types/member_interface_spec.rb13
-rw-r--r--spec/graphql/types/project_type_spec.rb12
-rw-r--r--spec/graphql/types/repository/blob_type_spec.rb5
-rw-r--r--spec/graphql/types/root_storage_statistics_type_spec.rb2
-rw-r--r--spec/graphql/types/subscription_type_spec.rb1
-rw-r--r--spec/graphql/types/user_preferences_type_spec.rb15
-rw-r--r--spec/graphql/types/user_type_spec.rb36
-rw-r--r--spec/helpers/application_helper_spec.rb2
-rw-r--r--spec/helpers/application_settings_helper_spec.rb9
-rw-r--r--spec/helpers/avatars_helper_spec.rb47
-rw-r--r--spec/helpers/bizible_helper_spec.rb47
-rw-r--r--spec/helpers/ci/pipeline_editor_helper_spec.rb11
-rw-r--r--spec/helpers/clusters_helper_spec.rb8
-rw-r--r--spec/helpers/invite_members_helper_spec.rb40
-rw-r--r--spec/helpers/issuables_description_templates_helper_spec.rb31
-rw-r--r--spec/helpers/issuables_helper_spec.rb10
-rw-r--r--spec/helpers/issues_helper_spec.rb12
-rw-r--r--spec/helpers/listbox_helper_spec.rb75
-rw-r--r--spec/helpers/projects/cluster_agents_helper_spec.rb29
-rw-r--r--spec/helpers/projects_helper_spec.rb30
-rw-r--r--spec/helpers/search_helper_spec.rb148
-rw-r--r--spec/helpers/ssh_keys_helper_spec.rb6
-rw-r--r--spec/helpers/storage_helper_spec.rb83
-rw-r--r--spec/helpers/tab_helper_spec.rb4
-rw-r--r--spec/helpers/users_helper_spec.rb38
-rw-r--r--spec/initializers/google_api_client_spec.rb38
-rw-r--r--spec/initializers/net_http_patch_spec.rb3
-rw-r--r--spec/lib/api/entities/basic_project_details_spec.rb2
-rw-r--r--spec/lib/api/entities/deployment_extended_spec.rb15
-rw-r--r--spec/lib/api/helpers_spec.rb8
-rw-r--r--spec/lib/backup/database_spec.rb9
-rw-r--r--spec/lib/backup/gitaly_backup_spec.rb6
-rw-r--r--spec/lib/backup/gitaly_rpc_backup_spec.rb6
-rw-r--r--spec/lib/backup/manager_spec.rb11
-rw-r--r--spec/lib/backup/repositories_spec.rb72
-rw-r--r--spec/lib/banzai/filter/external_link_filter_spec.rb7
-rw-r--r--spec/lib/banzai/filter/references/issue_reference_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/table_of_contents_filter_spec.rb6
-rw-r--r--spec/lib/banzai/object_renderer_spec.rb2
-rw-r--r--spec/lib/bitbucket_server/representation/repo_spec.rb5
-rw-r--r--spec/lib/bulk_imports/common/extractors/graphql_extractor_spec.rb15
-rw-r--r--spec/lib/bulk_imports/common/graphql/get_members_query_spec.rb56
-rw-r--r--spec/lib/bulk_imports/common/pipelines/lfs_objects_pipeline_spec.rb210
-rw-r--r--spec/lib/bulk_imports/common/pipelines/members_pipeline_spec.rb161
-rw-r--r--spec/lib/bulk_imports/groups/graphql/get_group_query_spec.rb27
-rw-r--r--spec/lib/bulk_imports/groups/graphql/get_members_query_spec.rb35
-rw-r--r--spec/lib/bulk_imports/groups/graphql/get_projects_query_spec.rb40
-rw-r--r--spec/lib/bulk_imports/groups/pipelines/members_pipeline_spec.rb119
-rw-r--r--spec/lib/bulk_imports/groups/stage_spec.rb2
-rw-r--r--spec/lib/bulk_imports/groups/transformers/member_attributes_transformer_spec.rb24
-rw-r--r--spec/lib/bulk_imports/projects/graphql/get_project_query_spec.rb27
-rw-r--r--spec/lib/bulk_imports/projects/graphql/get_repository_query_spec.rb32
-rw-r--r--spec/lib/bulk_imports/projects/graphql/get_snippet_repository_query_spec.rb78
-rw-r--r--spec/lib/bulk_imports/projects/stage_spec.rb1
-rw-r--r--spec/lib/container_registry/client_spec.rb110
-rw-r--r--spec/lib/container_registry/gitlab_api_client_spec.rb204
-rw-r--r--spec/lib/container_registry/migration_spec.rb168
-rw-r--r--spec/lib/container_registry/registry_spec.rb10
-rw-r--r--spec/lib/extracts_path_spec.rb21
-rw-r--r--spec/lib/extracts_ref_spec.rb15
-rw-r--r--spec/lib/feature_spec.rb4
-rw-r--r--spec/lib/generators/gitlab/snowplow_event_definition_generator_spec.rb48
-rw-r--r--spec/lib/gitlab/analytics/cycle_analytics/aggregated/records_fetcher_spec.rb15
-rw-r--r--spec/lib/gitlab/analytics/cycle_analytics/records_fetcher_spec.rb14
-rw-r--r--spec/lib/gitlab/application_context_spec.rb11
-rw-r--r--spec/lib/gitlab/audit/ci_runner_token_author_spec.rb83
-rw-r--r--spec/lib/gitlab/audit/null_author_spec.rb40
-rw-r--r--spec/lib/gitlab/auth/ldap/user_spec.rb28
-rw-r--r--spec/lib/gitlab/auth/o_auth/user_spec.rb74
-rw-r--r--spec/lib/gitlab/auth/request_authenticator_spec.rb6
-rw-r--r--spec/lib/gitlab/auth/saml/user_spec.rb46
-rw-r--r--spec/lib/gitlab/auth_spec.rb49
-rw-r--r--spec/lib/gitlab/authorized_keys_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/backfill_ci_queuing_tables_spec.rb244
-rw-r--r--spec/lib/gitlab/background_migration/backfill_legacy_project_repositories_spec.rb7
-rw-r--r--spec/lib/gitlab/background_migration/backfill_namespace_id_for_namespace_route_spec.rb50
-rw-r--r--spec/lib/gitlab/background_migration/backfill_project_updated_at_after_repository_storage_move_spec.rb35
-rw-r--r--spec/lib/gitlab/background_migration/batching_strategies/backfill_project_namespace_per_group_batching_strategy_spec.rb53
-rw-r--r--spec/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy_spec.rb8
-rw-r--r--spec/lib/gitlab/background_migration/copy_column_using_background_migration_job_spec.rb9
-rw-r--r--spec/lib/gitlab/background_migration/populate_finding_uuid_for_vulnerability_feedback_spec.rb134
-rw-r--r--spec/lib/gitlab/background_migration/populate_issue_email_participants_spec.rb20
-rw-r--r--spec/lib/gitlab/background_migration/populate_topics_non_private_projects_count_spec.rb50
-rw-r--r--spec/lib/gitlab/background_migration/populate_vulnerability_reads_spec.rb93
-rw-r--r--spec/lib/gitlab/background_migration/project_namespaces/backfill_project_namespaces_spec.rb28
-rw-r--r--spec/lib/gitlab/background_migration/remove_duplicate_vulnerabilities_findings_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/wrongfully_confirmed_email_unconfirmer_spec.rb130
-rw-r--r--spec/lib/gitlab/bitbucket_server_import/importer_spec.rb4
-rw-r--r--spec/lib/gitlab/buffered_io_spec.rb54
-rw-r--r--spec/lib/gitlab/changelog/config_spec.rb14
-rw-r--r--spec/lib/gitlab/changelog/release_spec.rb10
-rw-r--r--spec/lib/gitlab/checks/branch_check_spec.rb9
-rw-r--r--spec/lib/gitlab/ci/badge/release/latest_release_spec.rb42
-rw-r--r--spec/lib/gitlab/ci/badge/release/metadata_spec.rb40
-rw-r--r--spec/lib/gitlab/ci/badge/release/template_spec.rb90
-rw-r--r--spec/lib/gitlab/ci/build/artifacts/expire_in_parser_spec.rb21
-rw-r--r--spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb19
-rw-r--r--spec/lib/gitlab/ci/config/entry/include/rules/rule_spec.rb1
-rw-r--r--spec/lib/gitlab/ci/config/entry/include/rules_spec.rb1
-rw-r--r--spec/lib/gitlab/ci/config/entry/include_spec.rb1
-rw-r--r--spec/lib/gitlab/ci/config/entry/jobs_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/config/entry/policy_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/config/entry/root_spec.rb18
-rw-r--r--spec/lib/gitlab/ci/config/entry/script_spec.rb109
-rw-r--r--spec/lib/gitlab/ci/config/external/mapper_spec.rb32
-rw-r--r--spec/lib/gitlab/ci/config/external/rules_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/normalizer/matrix_strategy_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/config_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/lint_spec.rb124
-rw-r--r--spec/lib/gitlab/ci/parsers/security/common_spec.rb148
-rw-r--r--spec/lib/gitlab/ci/parsers/test/junit_spec.rb26
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/create_deployments_spec.rb12
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/ensure_environments_spec.rb12
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/ensure_resource_groups_spec.rb12
-rw-r--r--spec/lib/gitlab/ci/pipeline/logger_spec.rb39
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/build_spec.rb165
-rw-r--r--spec/lib/gitlab/ci/reports/codequality_reports_spec.rb28
-rw-r--r--spec/lib/gitlab/ci/reports/security/finding_key_spec.rb61
-rw-r--r--spec/lib/gitlab/ci/templates/5_minute_production_app_ci_yaml_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/templates/AWS/deploy_ecs_gitlab_ci_yaml_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/templates/Jobs/build_gitlab_ci_yaml_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/templates/Jobs/code_quality_gitlab_ci_yaml_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb7
-rw-r--r--spec/lib/gitlab/ci/templates/Jobs/sast_iac_gitlab_ci_yaml_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/templates/Jobs/test_gitlab_ci_yaml_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/templates/Terraform/base_gitlab_ci_yaml_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/templates/Terraform/base_latest_gitlab_ci_yaml_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/templates/Verify/load_performance_testing_gitlab_ci_yaml_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/templates/flutter_gitlab_ci_yaml_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/templates/kaniko_gitlab_ci_yaml_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/templates/npm_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/templates/terraform_gitlab_ci_yaml_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/templates/terraform_latest_gitlab_ci_yaml_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/variables/builder/instance_spec.rb39
-rw-r--r--spec/lib/gitlab/ci/variables/builder/project_spec.rb149
-rw-r--r--spec/lib/gitlab/ci/variables/builder_spec.rb247
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb58
-rw-r--r--spec/lib/gitlab/cluster/lifecycle_events_spec.rb3
-rw-r--r--spec/lib/gitlab/config/entry/factory_spec.rb6
-rw-r--r--spec/lib/gitlab/console_spec.rb51
-rw-r--r--spec/lib/gitlab/current_settings_spec.rb12
-rw-r--r--spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb7
-rw-r--r--spec/lib/gitlab/database/background_migration/batch_metrics_spec.rb2
-rw-r--r--spec/lib/gitlab/database/background_migration/batch_optimizer_spec.rb17
-rw-r--r--spec/lib/gitlab/database/background_migration/batched_job_spec.rb95
-rw-r--r--spec/lib/gitlab/database/background_migration/batched_job_transition_log_spec.rb19
-rw-r--r--spec/lib/gitlab/database/background_migration/batched_migration_runner_spec.rb27
-rw-r--r--spec/lib/gitlab/database/background_migration/batched_migration_spec.rb27
-rw-r--r--spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb49
-rw-r--r--spec/lib/gitlab/database/dynamic_model_helpers_spec.rb17
-rw-r--r--spec/lib/gitlab/database/each_database_spec.rb100
-rw-r--r--spec/lib/gitlab/database/gitlab_schema_spec.rb2
-rw-r--r--spec/lib/gitlab/database/load_balancing/configuration_spec.rb23
-rw-r--r--spec/lib/gitlab/database/load_balancing/setup_spec.rb31
-rw-r--r--spec/lib/gitlab/database/loose_foreign_keys_spec.rb39
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb102
-rw-r--r--spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb14
-rw-r--r--spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb2
-rw-r--r--spec/lib/gitlab/database/migrations/instrumentation_spec.rb58
-rw-r--r--spec/lib/gitlab/database/migrations/lock_retry_mixin_spec.rb18
-rw-r--r--spec/lib/gitlab/database/migrations/observers/query_details_spec.rb2
-rw-r--r--spec/lib/gitlab/database/migrations/observers/query_log_spec.rb2
-rw-r--r--spec/lib/gitlab/database/migrations/observers/transaction_duration_spec.rb2
-rw-r--r--spec/lib/gitlab/database/no_cross_db_foreign_keys_spec.rb57
-rw-r--r--spec/lib/gitlab/database/query_analyzers/prevent_cross_database_modification_spec.rb57
-rw-r--r--spec/lib/gitlab/database/with_lock_retries_outside_transaction_spec.rb29
-rw-r--r--spec/lib/gitlab/database/with_lock_retries_spec.rb2
-rw-r--r--spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb6
-rw-r--r--spec/lib/gitlab/database_spec.rb28
-rw-r--r--spec/lib/gitlab/diff/file_spec.rb6
-rw-r--r--spec/lib/gitlab/diff/position_tracer/image_strategy_spec.rb2
-rw-r--r--spec/lib/gitlab/diff/position_tracer/line_strategy_spec.rb2
-rw-r--r--spec/lib/gitlab/diff/position_tracer_spec.rb2
-rw-r--r--spec/lib/gitlab/email/handler/create_note_handler_spec.rb39
-rw-r--r--spec/lib/gitlab/endpoint_attributes_spec.rb5
-rw-r--r--spec/lib/gitlab/error_tracking/context_payload_generator_spec.rb3
-rw-r--r--spec/lib/gitlab/error_tracking/log_formatter_spec.rb2
-rw-r--r--spec/lib/gitlab/event_store/store_spec.rb27
-rw-r--r--spec/lib/gitlab/experiment/rollout/feature_spec.rb75
-rw-r--r--spec/lib/gitlab/feature_categories_spec.rb2
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb38
-rw-r--r--spec/lib/gitlab/git/wiki_spec.rb2
-rw-r--r--spec/lib/gitlab/git_access_design_spec.rb2
-rw-r--r--spec/lib/gitlab/git_access_spec.rb5
-rw-r--r--spec/lib/gitlab/gitaly_client/operation_service_spec.rb47
-rw-r--r--spec/lib/gitlab/gitaly_client/repository_service_spec.rb10
-rw-r--r--spec/lib/gitlab/github_import/importer/diff_note_importer_spec.rb167
-rw-r--r--spec/lib/gitlab/github_import/importer/releases_importer_spec.rb24
-rw-r--r--spec/lib/gitlab/github_import/importer/repository_importer_spec.rb4
-rw-r--r--spec/lib/gitlab/github_import/representation/diff_note_spec.rb19
-rw-r--r--spec/lib/gitlab/gl_repository/identifier_spec.rb4
-rw-r--r--spec/lib/gitlab/gl_repository/repo_type_spec.rb4
-rw-r--r--spec/lib/gitlab/gon_helper_spec.rb32
-rw-r--r--spec/lib/gitlab/graphql/authorize/object_authorization_spec.rb2
-rw-r--r--spec/lib/gitlab/graphql/batch_key_spec.rb6
-rw-r--r--spec/lib/gitlab/graphql/markdown_field_spec.rb2
-rw-r--r--spec/lib/gitlab/graphql/queries_spec.rb2
-rw-r--r--spec/lib/gitlab/graphql/tracers/application_context_tracer_spec.rb5
-rw-r--r--spec/lib/gitlab/hook_data/project_builder_spec.rb3
-rw-r--r--spec/lib/gitlab/http_connection_adapter_spec.rb36
-rw-r--r--spec/lib/gitlab/http_spec.rb18
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml2
-rw-r--r--spec/lib/gitlab/import_export/command_line_util_spec.rb23
-rw-r--r--spec/lib/gitlab/import_export/config_spec.rb3
-rw-r--r--spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/fork_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/group/legacy_tree_saver_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/group/relation_factory_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/group/tree_saver_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/importer_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb20
-rw-r--r--spec/lib/gitlab/import_export/legacy_relation_tree_saver_spec.rb34
-rw-r--r--spec/lib/gitlab/import_export/lfs_restorer_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/lfs_saver_spec.rb8
-rw-r--r--spec/lib/gitlab/import_export/members_mapper_spec.rb1
-rw-r--r--spec/lib/gitlab/import_export/project/relation_factory_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/project/tree_restorer_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/project/tree_saver_spec.rb16
-rw-r--r--spec/lib/gitlab/import_export/repo_restorer_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/saver_spec.rb48
-rw-r--r--spec/lib/gitlab/import_export/snippet_repo_restorer_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/snippet_repo_saver_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/snippets_repo_restorer_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/snippets_repo_saver_spec.rb6
-rw-r--r--spec/lib/gitlab/import_export/uploads_manager_spec.rb10
-rw-r--r--spec/lib/gitlab/incident_management/pager_duty/incident_issue_description_spec.rb2
-rw-r--r--spec/lib/gitlab/json_spec.rb42
-rw-r--r--spec/lib/gitlab/kubernetes/kubectl_cmd_spec.rb2
-rw-r--r--spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb8
-rw-r--r--spec/lib/gitlab/metrics/boot_time_tracker_spec.rb84
-rw-r--r--spec/lib/gitlab/metrics/exporter/web_exporter_spec.rb53
-rw-r--r--spec/lib/gitlab/metrics/rails_slis_spec.rb10
-rw-r--r--spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb14
-rw-r--r--spec/lib/gitlab/metrics/system_spec.rb34
-rw-r--r--spec/lib/gitlab/middleware/memory_report_spec.rb91
-rw-r--r--spec/lib/gitlab/net_http_adapter_spec.rb22
-rw-r--r--spec/lib/gitlab/omniauth_initializer_spec.rb14
-rw-r--r--spec/lib/gitlab/pagination/keyset/in_operator_optimization/array_scope_columns_spec.rb9
-rw-r--r--spec/lib/gitlab/pipeline_scope_counts_spec.rb48
-rw-r--r--spec/lib/gitlab/popen_spec.rb11
-rw-r--r--spec/lib/gitlab/process_memory_cache/helper_spec.rb9
-rw-r--r--spec/lib/gitlab/project_authorizations_spec.rb74
-rw-r--r--spec/lib/gitlab/rack_attack/request_spec.rb266
-rw-r--r--spec/lib/gitlab/regex_spec.rb2
-rw-r--r--spec/lib/gitlab/request_profiler/profile_spec.rb2
-rw-r--r--spec/lib/gitlab/runtime_spec.rb24
-rw-r--r--spec/lib/gitlab/security/scan_configuration_spec.rb33
-rw-r--r--spec/lib/gitlab/ssh_public_key_spec.rb123
-rw-r--r--spec/lib/gitlab/subscription_portal_spec.rb1
-rw-r--r--spec/lib/gitlab/untrusted_regexp/ruby_syntax_spec.rb5
-rw-r--r--spec/lib/gitlab/usage/service_ping_report_spec.rb69
-rw-r--r--spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb3
-rw-r--r--spec/lib/gitlab/usage_data_counters/jetbrains_plugin_activity_unique_counter_spec.rb15
-rw-r--r--spec/lib/gitlab/usage_data_counters/vscode_extension_activity_unique_counter_spec.rb15
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb6
-rw-r--r--spec/lib/gitlab/utils_spec.rb17
-rw-r--r--spec/lib/gitlab/web_ide/config/entry/global_spec.rb2
-rw-r--r--spec/lib/gitlab/web_ide/config_spec.rb2
-rw-r--r--spec/lib/gitlab/webpack/file_loader_spec.rb4
-rw-r--r--spec/lib/gitlab_edition_spec.rb25
-rw-r--r--spec/lib/gitlab_spec.rb7
-rw-r--r--spec/lib/google_api/cloud_platform/client_spec.rb90
-rw-r--r--spec/lib/learn_gitlab/project_spec.rb7
-rw-r--r--spec/lib/peek/views/detailed_view_spec.rb2
-rw-r--r--spec/lib/security/ci_configuration/container_scanning_build_action_spec.rb191
-rw-r--r--spec/lib/security/ci_configuration/sast_build_action_spec.rb9
-rw-r--r--spec/lib/security/ci_configuration/sast_iac_build_action_spec.rb4
-rw-r--r--spec/lib/security/ci_configuration/secret_detection_build_action_spec.rb4
-rw-r--r--spec/lib/serializers/json_spec.rb1
-rw-r--r--spec/lib/serializers/symbolized_json_spec.rb1
-rw-r--r--spec/lib/sidebars/concerns/work_item_hierarchy_spec.rb21
-rw-r--r--spec/lib/sidebars/projects/menus/analytics_menu_spec.rb2
-rw-r--r--spec/lib/sidebars/projects/menus/ci_cd_menu_spec.rb2
-rw-r--r--spec/lib/sidebars/projects/menus/confluence_menu_spec.rb2
-rw-r--r--spec/lib/sidebars/projects/menus/deployments_menu_spec.rb2
-rw-r--r--spec/lib/sidebars/projects/menus/external_issue_tracker_menu_spec.rb2
-rw-r--r--spec/lib/sidebars/projects/menus/external_wiki_menu_spec.rb2
-rw-r--r--spec/lib/sidebars/projects/menus/hidden_menu_spec.rb2
-rw-r--r--spec/lib/sidebars/projects/menus/infrastructure_menu_spec.rb2
-rw-r--r--spec/lib/sidebars/projects/menus/invite_team_members_menu_spec.rb2
-rw-r--r--spec/lib/sidebars/projects/menus/issues_menu_spec.rb2
-rw-r--r--spec/lib/sidebars/projects/menus/merge_requests_menu_spec.rb2
-rw-r--r--spec/lib/sidebars/projects/menus/monitor_menu_spec.rb2
-rw-r--r--spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb2
-rw-r--r--spec/lib/sidebars/projects/menus/project_information_menu_spec.rb8
-rw-r--r--spec/lib/sidebars/projects/menus/repository_menu_spec.rb2
-rw-r--r--spec/lib/sidebars/projects/menus/scope_menu_spec.rb2
-rw-r--r--spec/lib/sidebars/projects/menus/security_compliance_menu_spec.rb2
-rw-r--r--spec/lib/sidebars/projects/menus/settings_menu_spec.rb2
-rw-r--r--spec/lib/sidebars/projects/menus/shimo_menu_spec.rb2
-rw-r--r--spec/lib/sidebars/projects/menus/snippets_menu_spec.rb2
-rw-r--r--spec/lib/sidebars/projects/menus/wiki_menu_spec.rb2
-rw-r--r--spec/lib/system_check/incoming_email/imap_authentication_check_spec.rb2
-rw-r--r--spec/mailers/notify_spec.rb2
-rw-r--r--spec/metrics_server/metrics_server_spec.rb203
-rw-r--r--spec/migrations/20220106111958_add_insert_or_update_vulnerability_reads_trigger_spec.rb151
-rw-r--r--spec/migrations/20220106112043_add_update_vulnerability_reads_trigger_spec.rb128
-rw-r--r--spec/migrations/20220106112085_add_update_vulnerability_reads_location_trigger_spec.rb136
-rw-r--r--spec/migrations/20220106163326_add_has_issues_on_vulnerability_reads_trigger_spec.rb134
-rw-r--r--spec/migrations/20220107064845_populate_vulnerability_reads_spec.rb107
-rw-r--r--spec/migrations/20220120094340_drop_position_from_security_findings_spec.rb21
-rw-r--r--spec/migrations/20220124130028_dedup_runner_projects_spec.rb65
-rw-r--r--spec/migrations/20220128155251_remove_dangling_running_builds_spec.rb52
-rw-r--r--spec/migrations/20220128155814_fix_approval_rules_code_owners_rule_type_index_spec.rb33
-rw-r--r--spec/migrations/20220202105733_delete_service_template_records_spec.rb42
-rw-r--r--spec/migrations/20220211214605_update_integrations_trigger_type_new_on_insert_null_safe_spec.rb37
-rw-r--r--spec/migrations/backfill_namespace_id_for_namespace_routes_spec.rb29
-rw-r--r--spec/migrations/backfill_project_namespaces_for_group_spec.rb43
-rw-r--r--spec/migrations/populate_audit_event_streaming_verification_token_spec.rb22
-rw-r--r--spec/migrations/schedule_fix_incorrect_max_seats_used2_spec.rb34
-rw-r--r--spec/migrations/schedule_fix_incorrect_max_seats_used_spec.rb26
-rw-r--r--spec/migrations/schedule_recalculate_vulnerability_finding_signatures_for_findings_spec.rb9
-rw-r--r--spec/migrations/start_backfill_ci_queuing_tables_spec.rb48
-rw-r--r--spec/migrations/update_default_scan_method_of_dast_site_profile_spec.rb32
-rw-r--r--spec/models/ability_spec.rb2
-rw-r--r--spec/models/application_setting_spec.rb7
-rw-r--r--spec/models/audit_event_spec.rb33
-rw-r--r--spec/models/blob_spec.rb14
-rw-r--r--spec/models/board_spec.rb4
-rw-r--r--spec/models/ci/build_metadata_spec.rb7
-rw-r--r--spec/models/ci/build_spec.rb168
-rw-r--r--spec/models/ci/build_trace_chunk_spec.rb2
-rw-r--r--spec/models/ci/job_artifact_spec.rb7
-rw-r--r--spec/models/ci/job_token/project_scope_link_spec.rb14
-rw-r--r--spec/models/ci/namespace_mirror_spec.rb4
-rw-r--r--spec/models/ci/pipeline_schedule_spec.rb7
-rw-r--r--spec/models/ci/pipeline_spec.rb120
-rw-r--r--spec/models/ci/ref_spec.rb7
-rw-r--r--spec/models/ci/runner_project_spec.rb7
-rw-r--r--spec/models/ci/runner_spec.rb449
-rw-r--r--spec/models/ci/sources/pipeline_spec.rb14
-rw-r--r--spec/models/ci/stage_spec.rb7
-rw-r--r--spec/models/ci/trigger_spec.rb16
-rw-r--r--spec/models/ci/variable_spec.rb7
-rw-r--r--spec/models/commit_spec.rb18
-rw-r--r--spec/models/commit_status_spec.rb7
-rw-r--r--spec/models/concerns/after_commit_queue_spec.rb4
-rw-r--r--spec/models/concerns/ci/has_variable_spec.rb39
-rw-r--r--spec/models/concerns/cross_database_modification_spec.rb89
-rw-r--r--spec/models/concerns/has_environment_scope_spec.rb32
-rw-r--r--spec/models/concerns/issuable_spec.rb8
-rw-r--r--spec/models/concerns/resolvable_discussion_spec.rb10
-rw-r--r--spec/models/concerns/taskable_spec.rb66
-rw-r--r--spec/models/concerns/token_authenticatable_spec.rb138
-rw-r--r--spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb10
-rw-r--r--spec/models/container_repository_spec.rb765
-rw-r--r--spec/models/customer_relations/contact_spec.rb39
-rw-r--r--spec/models/customer_relations/issue_contact_spec.rb8
-rw-r--r--spec/models/deployment_spec.rb38
-rw-r--r--spec/models/design_management/design_action_spec.rb4
-rw-r--r--spec/models/design_management/design_at_version_spec.rb2
-rw-r--r--spec/models/draft_note_spec.rb22
-rw-r--r--spec/models/environment_spec.rb4
-rw-r--r--spec/models/environment_status_spec.rb2
-rw-r--r--spec/models/event_spec.rb32
-rw-r--r--spec/models/external_pull_request_spec.rb7
-rw-r--r--spec/models/group_spec.rb2
-rw-r--r--spec/models/hooks/service_hook_spec.rb2
-rw-r--r--spec/models/hooks/system_hook_spec.rb8
-rw-r--r--spec/models/hooks/web_hook_spec.rb14
-rw-r--r--spec/models/instance_configuration_spec.rb4
-rw-r--r--spec/models/instance_metadata_spec.rb2
-rw-r--r--spec/models/integration_spec.rb19
-rw-r--r--spec/models/integrations/datadog_spec.rb32
-rw-r--r--spec/models/issue_collection_spec.rb4
-rw-r--r--spec/models/issue_spec.rb22
-rw-r--r--spec/models/key_spec.rb28
-rw-r--r--spec/models/label_note_spec.rb18
-rw-r--r--spec/models/loose_foreign_keys/deleted_record_spec.rb37
-rw-r--r--spec/models/member_spec.rb32
-rw-r--r--spec/models/members/project_member_spec.rb11
-rw-r--r--spec/models/merge_request_spec.rb205
-rw-r--r--spec/models/namespace/root_storage_statistics_spec.rb94
-rw-r--r--spec/models/namespace_spec.rb84
-rw-r--r--spec/models/namespace_statistics_spec.rb207
-rw-r--r--spec/models/namespaces/user_namespace_spec.rb9
-rw-r--r--spec/models/note_spec.rb73
-rw-r--r--spec/models/packages/package_file_spec.rb20
-rw-r--r--spec/models/pages_domain_spec.rb5
-rw-r--r--spec/models/personal_access_token_spec.rb10
-rw-r--r--spec/models/preloaders/users_max_access_level_in_projects_preloader_spec.rb51
-rw-r--r--spec/models/project_import_state_spec.rb23
-rw-r--r--spec/models/project_spec.rb279
-rw-r--r--spec/models/project_team_spec.rb32
-rw-r--r--spec/models/state_note_spec.rb4
-rw-r--r--spec/models/user_spec.rb211
-rw-r--r--spec/models/work_item_spec.rb13
-rw-r--r--spec/policies/ci/pipeline_policy_spec.rb6
-rw-r--r--spec/policies/clusters/agent_token_policy_spec.rb11
-rw-r--r--spec/policies/clusters/agents/activity_event_policy_spec.rb11
-rw-r--r--spec/policies/namespaces/project_namespace_policy_spec.rb2
-rw-r--r--spec/policies/project_member_policy_spec.rb2
-rw-r--r--spec/policies/project_policy_spec.rb4
-rw-r--r--spec/presenters/blob_presenter_spec.rb65
-rw-r--r--spec/presenters/blobs/unfold_presenter_spec.rb8
-rw-r--r--spec/presenters/clusterable_presenter_spec.rb24
-rw-r--r--spec/presenters/packages/conan/package_presenter_spec.rb19
-rw-r--r--spec/presenters/packages/detail/package_presenter_spec.rb8
-rw-r--r--spec/presenters/packages/npm/package_presenter_spec.rb11
-rw-r--r--spec/presenters/packages/nuget/package_metadata_presenter_spec.rb8
-rw-r--r--spec/presenters/packages/pypi/package_presenter_spec.rb8
-rw-r--r--spec/presenters/projects/security/configuration_presenter_spec.rb3
-rw-r--r--spec/requests/abuse_reports_controller_spec.rb (renamed from spec/controllers/abuse_reports_controller_spec.rb)16
-rw-r--r--spec/requests/admin/background_migrations_controller_spec.rb2
-rw-r--r--spec/requests/api/api_spec.rb2
-rw-r--r--spec/requests/api/branches_spec.rb18
-rw-r--r--spec/requests/api/ci/pipelines_spec.rb2
-rw-r--r--spec/requests/api/ci/runner/runners_post_spec.rb85
-rw-r--r--spec/requests/api/ci/runner/runners_verify_post_spec.rb24
-rw-r--r--spec/requests/api/ci/runners_reset_registration_token_spec.rb2
-rw-r--r--spec/requests/api/ci/runners_spec.rb171
-rw-r--r--spec/requests/api/ci/secure_files_spec.rb314
-rw-r--r--spec/requests/api/commits_spec.rb16
-rw-r--r--spec/requests/api/features_spec.rb119
-rw-r--r--spec/requests/api/graphql/ci/ci_cd_setting_spec.rb2
-rw-r--r--spec/requests/api/graphql/ci/config_spec.rb7
-rw-r--r--spec/requests/api/graphql/ci/runner_spec.rb103
-rw-r--r--spec/requests/api/graphql/container_repository/container_repository_details_spec.rb2
-rw-r--r--spec/requests/api/graphql/gitlab_schema_spec.rb4
-rw-r--r--spec/requests/api/graphql/group/recent_issue_boards_query_spec.rb14
-rw-r--r--spec/requests/api/graphql/mutations/ci/ci_cd_settings_update_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/ci/job_token_scope/add_project_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/ci/job_token_scope/remove_project_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/ci/pipeline_destroy_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/ci/runners_registration_token/reset_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/issues/create_spec.rb17
-rw-r--r--spec/requests/api/graphql/mutations/security/ci_configuration/configure_sast_iac_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/security/ci_configuration/configure_secret_detection_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/user_preferences/update_spec.rb49
-rw-r--r--spec/requests/api/graphql/mutations/work_items/create_spec.rb21
-rw-r--r--spec/requests/api/graphql/mutations/work_items/delete_spec.rb49
-rw-r--r--spec/requests/api/graphql/mutations/work_items/update_spec.rb84
-rw-r--r--spec/requests/api/graphql/packages/package_spec.rb17
-rw-r--r--spec/requests/api/graphql/project/container_expiration_policy_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/container_repositories_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/error_tracking/sentry_detailed_error_request_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/grafana_integration_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/issue/designs/designs_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/issue/designs/notes_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/merge_requests_spec.rb37
-rw-r--r--spec/requests/api/graphql/project/project_members_spec.rb96
-rw-r--r--spec/requests/api/graphql/project/recent_issue_boards_query_spec.rb14
-rw-r--r--spec/requests/api/graphql/project/repository/blobs_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/repository_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/tree/tree_spec.rb2
-rw-r--r--spec/requests/api/group_clusters_spec.rb16
-rw-r--r--spec/requests/api/groups_spec.rb34
-rw-r--r--spec/requests/api/internal/base_spec.rb48
-rw-r--r--spec/requests/api/internal/container_registry/migration_spec.rb153
-rw-r--r--spec/requests/api/issues/issues_spec.rb48
-rw-r--r--spec/requests/api/lint_spec.rb19
-rw-r--r--spec/requests/api/markdown_spec.rb4
-rw-r--r--spec/requests/api/members_spec.rb2
-rw-r--r--spec/requests/api/merge_requests_spec.rb42
-rw-r--r--spec/requests/api/package_files_spec.rb24
-rw-r--r--spec/requests/api/project_attributes.yml2
-rw-r--r--spec/requests/api/project_clusters_spec.rb32
-rw-r--r--spec/requests/api/project_export_spec.rb2
-rw-r--r--spec/requests/api/project_snapshots_spec.rb2
-rw-r--r--spec/requests/api/projects_spec.rb5
-rw-r--r--spec/requests/api/repositories_spec.rb11
-rw-r--r--spec/requests/api/rubygem_packages_spec.rb13
-rw-r--r--spec/requests/api/settings_spec.rb45
-rw-r--r--spec/requests/api/tags_spec.rb310
-rw-r--r--spec/requests/api/terraform/modules/v1/packages_spec.rb14
-rw-r--r--spec/requests/api/usage_data_spec.rb26
-rw-r--r--spec/requests/api/users_spec.rb25
-rw-r--r--spec/requests/boards/lists_controller_spec.rb2
-rw-r--r--spec/requests/concerns/planning_hierarchy_spec.rb23
-rw-r--r--spec/requests/git_http_spec.rb26
-rw-r--r--spec/requests/import/gitlab_projects_controller_spec.rb2
-rw-r--r--spec/requests/lfs_http_spec.rb8
-rw-r--r--spec/requests/openid_connect_spec.rb4
-rw-r--r--spec/requests/projects/cluster_agents_controller_spec.rb2
-rw-r--r--spec/requests/projects/clusters/integrations_controller_spec.rb2
-rw-r--r--spec/requests/projects/google_cloud/deployments_controller_spec.rb58
-rw-r--r--spec/requests/projects/google_cloud/service_accounts_controller_spec.rb11
-rw-r--r--spec/requests/projects/merge_requests/creations_spec.rb2
-rw-r--r--spec/requests/projects/merge_requests_discussions_spec.rb2
-rw-r--r--spec/requests/projects/merge_requests_spec.rb2
-rw-r--r--spec/requests/projects/metrics_dashboard_spec.rb2
-rw-r--r--spec/requests/projects/noteable_notes_spec.rb2
-rw-r--r--spec/requests/rack_attack_global_spec.rb32
-rw-r--r--spec/requests/recursive_webhook_detection_spec.rb45
-rw-r--r--spec/requests/users_controller_spec.rb34
-rw-r--r--spec/rubocop/cop/file_decompression_spec.rb48
-rw-r--r--spec/rubocop/cop/gitlab/event_store_subscriber_spec.rb82
-rw-r--r--spec/rubocop/cop/migration/schedule_async_spec.rb51
-rw-r--r--spec/rubocop/cop/scalability/bulk_perform_with_context_spec.rb8
-rw-r--r--spec/rubocop/cop/sidekiq_load_balancing/worker_data_consistency_with_deduplication_spec.rb166
-rw-r--r--spec/serializers/build_details_entity_spec.rb2
-rw-r--r--spec/serializers/ci/lint/result_serializer_spec.rb4
-rw-r--r--spec/serializers/codequality_degradation_entity_spec.rb15
-rw-r--r--spec/serializers/deployment_cluster_entity_spec.rb6
-rw-r--r--spec/serializers/diff_file_base_entity_spec.rb2
-rw-r--r--spec/serializers/environment_serializer_spec.rb15
-rw-r--r--spec/serializers/group_child_entity_spec.rb19
-rw-r--r--spec/serializers/issue_sidebar_basic_entity_spec.rb74
-rw-r--r--spec/serializers/merge_request_poll_cached_widget_entity_spec.rb20
-rw-r--r--spec/serializers/merge_request_poll_widget_entity_spec.rb10
-rw-r--r--spec/serializers/runner_entity_spec.rb2
-rw-r--r--spec/serializers/test_case_entity_spec.rb12
-rw-r--r--spec/serializers/trigger_variable_entity_spec.rb2
-rw-r--r--spec/services/alert_management/alerts/update_service_spec.rb13
-rw-r--r--spec/services/alert_management/create_alert_issue_service_spec.rb4
-rw-r--r--spec/services/application_settings/update_service_spec.rb18
-rw-r--r--spec/services/auth/container_registry_authentication_service_spec.rb24
-rw-r--r--spec/services/branches/create_service_spec.rb2
-rw-r--r--spec/services/ci/copy_cross_database_associations_service_spec.rb18
-rw-r--r--spec/services/ci/create_downstream_pipeline_service_spec.rb101
-rw-r--r--spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb16
-rw-r--r--spec/services/ci/pipeline_schedule_service_spec.rb17
-rw-r--r--spec/services/ci/process_sync_events_service_spec.rb76
-rw-r--r--spec/services/ci/register_job_service_spec.rb4
-rw-r--r--spec/services/ci/register_runner_service_spec.rb330
-rw-r--r--spec/services/ci/retry_build_service_spec.rb20
-rw-r--r--spec/services/ci/unregister_runner_service_spec.rb15
-rw-r--r--spec/services/ci/update_build_queue_service_spec.rb32
-rw-r--r--spec/services/ci/update_runner_service_spec.rb14
-rw-r--r--spec/services/concerns/rate_limited_service_spec.rb20
-rw-r--r--spec/services/draft_notes/create_service_spec.rb4
-rw-r--r--spec/services/environments/stop_service_spec.rb8
-rw-r--r--spec/services/feature_flags/update_service_spec.rb2
-rw-r--r--spec/services/google_cloud/create_service_accounts_service_spec.rb16
-rw-r--r--spec/services/google_cloud/enable_cloud_run_service_spec.rb41
-rw-r--r--spec/services/google_cloud/generate_pipeline_service_spec.rb230
-rw-r--r--spec/services/groups/create_service_spec.rb57
-rw-r--r--spec/services/groups/update_statistics_service_spec.rb55
-rw-r--r--spec/services/incident_management/create_incident_label_service_spec.rb7
-rw-r--r--spec/services/incident_management/incidents/create_service_spec.rb36
-rw-r--r--spec/services/incident_management/issuable_escalation_statuses/after_update_service_spec.rb11
-rw-r--r--spec/services/incident_management/issuable_escalation_statuses/prepare_update_service_spec.rb8
-rw-r--r--spec/services/issues/close_service_spec.rb10
-rw-r--r--spec/services/issues/create_service_spec.rb24
-rw-r--r--spec/services/issues/move_service_spec.rb42
-rw-r--r--spec/services/issues/reorder_service_spec.rb12
-rw-r--r--spec/services/issues/set_crm_contacts_service_spec.rb112
-rw-r--r--spec/services/issues/update_service_spec.rb20
-rw-r--r--spec/services/loose_foreign_keys/batch_cleaner_service_spec.rb78
-rw-r--r--spec/services/members/create_service_spec.rb27
-rw-r--r--spec/services/merge_requests/after_create_service_spec.rb32
-rw-r--r--spec/services/merge_requests/bulk_remove_attention_requested_service_spec.rb4
-rw-r--r--spec/services/merge_requests/create_service_spec.rb10
-rw-r--r--spec/services/merge_requests/merge_service_spec.rb2
-rw-r--r--spec/services/merge_requests/mergeability_check_service_spec.rb18
-rw-r--r--spec/services/merge_requests/rebase_service_spec.rb2
-rw-r--r--spec/services/merge_requests/squash_service_spec.rb6
-rw-r--r--spec/services/merge_requests/update_service_spec.rb6
-rw-r--r--spec/services/notes/create_service_spec.rb4
-rw-r--r--spec/services/packages/maven/metadata/sync_service_spec.rb19
-rw-r--r--spec/services/packages/nuget/metadata_extraction_service_spec.rb4
-rw-r--r--spec/services/pages/zip_directory_service_spec.rb6
-rw-r--r--spec/services/projects/autocomplete_service_spec.rb26
-rw-r--r--spec/services/projects/container_repository/delete_tags_service_spec.rb18
-rw-r--r--spec/services/projects/create_service_spec.rb98
-rw-r--r--spec/services/projects/destroy_service_spec.rb45
-rw-r--r--spec/services/projects/import_export/export_service_spec.rb15
-rw-r--r--spec/services/projects/import_service_spec.rb2
-rw-r--r--spec/services/projects/overwrite_project_service_spec.rb69
-rw-r--r--spec/services/projects/readme_renderer_service_spec.rb75
-rw-r--r--spec/services/projects/transfer_service_spec.rb31
-rw-r--r--spec/services/quick_actions/interpret_service_spec.rb232
-rw-r--r--spec/services/releases/create_service_spec.rb2
-rw-r--r--spec/services/security/ci_configuration/container_scanning_create_service_spec.rb19
-rw-r--r--spec/services/service_ping/submit_service_ping_service_spec.rb39
-rw-r--r--spec/services/system_note_service_spec.rb61
-rw-r--r--spec/services/system_notes/alert_management_service_spec.rb38
-rw-r--r--spec/services/system_notes/incident_service_spec.rb24
-rw-r--r--spec/services/system_notes/issuables_service_spec.rb48
-rw-r--r--spec/services/test_hooks/project_service_spec.rb18
-rw-r--r--spec/services/test_hooks/system_service_spec.rb8
-rw-r--r--spec/services/update_container_registry_info_service_spec.rb15
-rw-r--r--spec/services/web_hook_service_spec.rb65
-rw-r--r--spec/services/work_items/create_service_spec.rb56
-rw-r--r--spec/services/work_items/delete_service_spec.rb50
-rw-r--r--spec/services/work_items/update_service_spec.rb69
-rw-r--r--spec/spec_helper.rb32
-rw-r--r--spec/support/cross_database_modification.rb9
-rw-r--r--spec/support/db_cleaner.rb2
-rw-r--r--spec/support/flaky_tests.rb11
-rw-r--r--spec/support/gitlab_experiment.rb10
-rw-r--r--spec/support/graphql/arguments.rb1
-rw-r--r--spec/support/helpers/fake_blob_helpers.rb5
-rw-r--r--spec/support/helpers/features/invite_members_modal_helper.rb2
-rw-r--r--spec/support/helpers/features/iteration_helpers.rb6
-rw-r--r--spec/support/helpers/gitaly_setup.rb8
-rw-r--r--spec/support/helpers/import_spec_helper.rb2
-rw-r--r--spec/support/helpers/key_generator_helper.rb44
-rw-r--r--spec/support/helpers/login_helpers.rb2
-rw-r--r--spec/support/helpers/memory_usage_helper.rb37
-rw-r--r--spec/support/helpers/merge_request_diff_helpers.rb54
-rw-r--r--spec/support/helpers/note_interaction_helpers.rb4
-rw-r--r--spec/support/helpers/rack_attack_spec_helpers.rb4
-rw-r--r--spec/support/helpers/repo_helpers.rb2
-rw-r--r--spec/support/helpers/session_helpers.rb16
-rw-r--r--spec/support/helpers/stub_gitlab_calls.rb2
-rw-r--r--spec/support/helpers/test_env.rb11
-rw-r--r--spec/support/import_export/import_export.yml2
-rw-r--r--spec/support/matchers/event_store.rb12
-rw-r--r--spec/support/matchers/schema_matcher.rb34
-rw-r--r--spec/support/shared_contexts/container_repositories_shared_context.rb27
-rw-r--r--spec/support/shared_contexts/features/integrations/integrations_shared_context.rb2
-rw-r--r--spec/support/shared_contexts/features/integrations/project_integrations_jira_context.rb2
-rw-r--r--spec/support/shared_contexts/features/integrations/project_integrations_shared_context.rb2
-rw-r--r--spec/support/shared_contexts/finders/group_projects_finder_shared_contexts.rb6
-rw-r--r--spec/support/shared_contexts/graphql/requests/packages_shared_context.rb2
-rw-r--r--spec/support/shared_contexts/lib/container_registry/client_shared_context.rb27
-rw-r--r--spec/support/shared_contexts/navbar_structure_context.rb1
-rw-r--r--spec/support/shared_contexts/policies/group_policy_shared_context.rb3
-rw-r--r--spec/support/shared_contexts/policies/project_policy_shared_context.rb2
-rw-r--r--spec/support/shared_contexts/services/projects/container_repository/delete_tags_service_shared_context.rb2
-rw-r--r--spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb52
-rw-r--r--spec/support/shared_examples/features/comments_on_merge_request_files_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/sidebar/sidebar_labels_shared_examples.rb5
-rw-r--r--spec/support/shared_examples/features/variable_list_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/wiki/user_previews_wiki_changes_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/graphql/boards_shared_examples.rb58
-rw-r--r--spec/support/shared_examples/graphql/mutations/can_mutate_spammable_examples.rb13
-rw-r--r--spec/support/shared_examples/graphql/mutations/security/ci_configuration_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/integrations/integration_settings_form.rb47
-rw-r--r--spec/support/shared_examples/lib/gitlab/experimentation_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/lib/gitlab/usage_data_counters/code_review_extension_request_examples.rb (renamed from spec/lib/gitlab/usage_data_counters/vscode_extenion_activity_unique_counter_spec.rb)30
-rw-r--r--spec/support/shared_examples/lib/sidebars/projects/menus/zentao_menu_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/loose_foreign_keys/have_loose_foreign_key.rb10
-rw-r--r--spec/support/shared_examples/models/concerns/analytics/cycle_analytics/stage_event_model_examples.rb18
-rw-r--r--spec/support/shared_examples/models/member_shared_examples.rb80
-rw-r--r--spec/support/shared_examples/models/note_access_check_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb12
-rw-r--r--spec/support/shared_examples/models/update_project_statistics_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/namespaces/traversal_scope_examples.rb47
-rw-r--r--spec/support/shared_examples/path_extraction_shared_examples.rb5
-rw-r--r--spec/support/shared_examples/policies/clusterable_shared_examples.rb14
-rw-r--r--spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/quick_actions/merge_request/rebase_quick_action_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/requests/api/graphql/mutations/snippets_shared_examples.rb11
-rw-r--r--spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb56
-rw-r--r--spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb12
-rw-r--r--spec/support/shared_examples/requests/api/notes_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/requests/rack_attack_shared_examples.rb85
-rw-r--r--spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb13
-rw-r--r--spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb93
-rw-r--r--spec/support/shared_examples/services/incident_shared_examples.rb13
-rw-r--r--spec/support/shared_examples/views/registration_features_prompt_shared_examples.rb27
-rw-r--r--spec/support/shared_examples/workers/background_migration_worker_shared_examples.rb16
-rw-r--r--spec/support/shared_examples/workers/project_export_shared_examples.rb16
-rw-r--r--spec/support/stub_settings_source.rb11
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb79
-rw-r--r--spec/tasks/gitlab/db_rake_spec.rb131
-rw-r--r--spec/tasks/gitlab/dependency_proxy/migrate_rake_spec.rb58
-rw-r--r--spec/tasks/gitlab/info_rake_spec.rb39
-rw-r--r--spec/tooling/danger/project_helper_spec.rb1
-rw-r--r--spec/tooling/docs/deprecation_handling_spec.rb6
-rw-r--r--spec/tooling/lib/tooling/test_map_generator_spec.rb16
-rw-r--r--spec/tooling/quality/test_level_spec.rb4
-rw-r--r--spec/tooling/rspec_flaky/config_spec.rb33
-rw-r--r--spec/tooling/rspec_flaky/listener_spec.rb6
-rw-r--r--spec/uploaders/import_export_uploader_spec.rb28
-rw-r--r--spec/validators/x509_certificate_credentials_validator_spec.rb10
-rw-r--r--spec/views/admin/application_settings/general.html.haml_spec.rb27
-rw-r--r--spec/views/admin/dashboard/index.html.haml_spec.rb29
-rw-r--r--spec/views/devise/shared/_signup_box.html.haml_spec.rb16
-rw-r--r--spec/views/groups/settings/_transfer.html.haml_spec.rb17
-rw-r--r--spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb18
-rw-r--r--spec/views/profiles/keys/_key.html.haml_spec.rb4
-rw-r--r--spec/views/projects/edit.html.haml_spec.rb34
-rw-r--r--spec/views/projects/services/_form.haml_spec.rb52
-rw-r--r--spec/views/shared/_gl_toggle.haml_spec.rb85
-rw-r--r--spec/views/shared/_global_alert.html.haml_spec.rb10
-rw-r--r--spec/views/shared/issuable/_sidebar.html.haml_spec.rb39
-rw-r--r--spec/workers/auto_devops/disable_worker_spec.rb2
-rw-r--r--spec/workers/background_migration/ci_database_worker_spec.rb7
-rw-r--r--spec/workers/background_migration_worker_spec.rb2
-rw-r--r--spec/workers/ci/delete_objects_worker_spec.rb33
-rw-r--r--spec/workers/ci/external_pull_requests/create_pipeline_worker_spec.rb2
-rw-r--r--spec/workers/cleanup_container_repository_worker_spec.rb2
-rw-r--r--spec/workers/concerns/application_worker_spec.rb39
-rw-r--r--spec/workers/container_expiration_policy_worker_spec.rb5
-rw-r--r--spec/workers/container_registry/migration/enqueuer_worker_spec.rb178
-rw-r--r--spec/workers/container_registry/migration/guard_worker_spec.rb162
-rw-r--r--spec/workers/container_registry/migration/observer_worker_spec.rb57
-rw-r--r--spec/workers/delete_container_repository_worker_spec.rb2
-rw-r--r--spec/workers/delete_merged_branches_worker_spec.rb4
-rw-r--r--spec/workers/every_sidekiq_worker_spec.rb6
-rw-r--r--spec/workers/groups/update_statistics_worker_spec.rb29
-rw-r--r--spec/workers/loose_foreign_keys/cleanup_worker_spec.rb10
-rw-r--r--spec/workers/namespaces/process_sync_events_worker_spec.rb12
-rw-r--r--spec/workers/namespaces/update_root_statistics_worker_spec.rb23
-rw-r--r--spec/workers/pages_update_configuration_worker_spec.rb12
-rw-r--r--spec/workers/pipeline_schedule_worker_spec.rb10
-rw-r--r--spec/workers/post_receive_spec.rb16
-rw-r--r--spec/workers/project_destroy_worker_spec.rb4
-rw-r--r--spec/workers/projects/git_garbage_collect_worker_spec.rb15
-rw-r--r--spec/workers/projects/process_sync_events_worker_spec.rb12
-rw-r--r--spec/workers/run_pipeline_schedule_worker_spec.rb19
-rw-r--r--spec/workers/web_hook_worker_spec.rb11
1770 files changed, 37662 insertions, 18068 deletions
diff --git a/spec/bin/feature_flag_spec.rb b/spec/bin/feature_flag_spec.rb
index a85cafcb4a3..03f5ac135f7 100644
--- a/spec/bin/feature_flag_spec.rb
+++ b/spec/bin/feature_flag_spec.rb
@@ -25,7 +25,7 @@ RSpec.describe 'bin/feature-flag' do
allow(File).to receive(:write).and_return(true)
# ignore stdin
- allow($stdin).to receive(:gets).and_raise('EOF')
+ allow(Readline).to receive(:readline).and_raise('EOF')
end
subject { creator.execute }
@@ -135,8 +135,8 @@ RSpec.describe 'bin/feature-flag' do
let(:type) { 'deprecated' }
it 'shows error message and retries' do
- expect($stdin).to receive(:gets).and_return(type)
- expect($stdin).to receive(:gets).and_raise('EOF')
+ expect(Readline).to receive(:readline).and_return(type)
+ expect(Readline).to receive(:readline).and_raise('EOF')
expect do
expect { described_class.read_type }.to raise_error(/EOF/)
@@ -154,8 +154,8 @@ RSpec.describe 'bin/feature-flag' do
)
end
- it 'reads type from $stdin' do
- expect($stdin).to receive(:gets).and_return(type)
+ it 'reads type from stdin' do
+ expect(Readline).to receive(:readline).and_return(type)
expect do
expect(described_class.read_type).to eq(:development)
end.to output(/Specify the feature flag type/).to_stdout
@@ -165,8 +165,8 @@ RSpec.describe 'bin/feature-flag' do
let(:type) { 'invalid' }
it 'shows error message and retries' do
- expect($stdin).to receive(:gets).and_return(type)
- expect($stdin).to receive(:gets).and_raise('EOF')
+ expect(Readline).to receive(:readline).and_return(type)
+ expect(Readline).to receive(:readline).and_raise('EOF')
expect do
expect { described_class.read_type }.to raise_error(/EOF/)
@@ -180,8 +180,8 @@ RSpec.describe 'bin/feature-flag' do
describe '.read_group' do
let(:group) { 'group::memory' }
- it 'reads type from $stdin' do
- expect($stdin).to receive(:gets).and_return(group)
+ it 'reads type from stdin' do
+ expect(Readline).to receive(:readline).and_return(group)
expect do
expect(described_class.read_group).to eq('group::memory')
end.to output(/Specify the group introducing the feature flag/).to_stdout
@@ -191,8 +191,8 @@ RSpec.describe 'bin/feature-flag' do
let(:type) { 'invalid' }
it 'shows error message and retries' do
- expect($stdin).to receive(:gets).and_return(type)
- expect($stdin).to receive(:gets).and_raise('EOF')
+ expect(Readline).to receive(:readline).and_return(type)
+ expect(Readline).to receive(:readline).and_raise('EOF')
expect do
expect { described_class.read_group }.to raise_error(/EOF/)
@@ -205,8 +205,8 @@ RSpec.describe 'bin/feature-flag' do
describe '.read_introduced_by_url' do
let(:url) { 'https://merge-request' }
- it 'reads type from $stdin' do
- expect($stdin).to receive(:gets).and_return(url)
+ it 'reads type from stdin' do
+ expect(Readline).to receive(:readline).and_return(url)
expect do
expect(described_class.read_introduced_by_url).to eq('https://merge-request')
end.to output(/URL of the MR introducing the feature flag/).to_stdout
@@ -216,7 +216,7 @@ RSpec.describe 'bin/feature-flag' do
let(:url) { '' }
it 'skips entry' do
- expect($stdin).to receive(:gets).and_return(url)
+ expect(Readline).to receive(:readline).and_return(url)
expect do
expect(described_class.read_introduced_by_url).to be_nil
end.to output(/URL of the MR introducing the feature flag/).to_stdout
@@ -227,8 +227,8 @@ RSpec.describe 'bin/feature-flag' do
let(:url) { 'invalid' }
it 'shows error message and retries' do
- expect($stdin).to receive(:gets).and_return(url)
- expect($stdin).to receive(:gets).and_raise('EOF')
+ expect(Readline).to receive(:readline).and_return(url)
+ expect(Readline).to receive(:readline).and_raise('EOF')
expect do
expect { described_class.read_introduced_by_url }.to raise_error(/EOF/)
@@ -242,8 +242,8 @@ RSpec.describe 'bin/feature-flag' do
let(:options) { double('options', name: 'foo', type: :development) }
let(:url) { 'https://issue' }
- it 'reads type from $stdin' do
- expect($stdin).to receive(:gets).and_return(url)
+ it 'reads type from stdin' do
+ expect(Readline).to receive(:readline).and_return(url)
expect do
expect(described_class.read_rollout_issue_url(options)).to eq('https://issue')
end.to output(/URL of the rollout issue/).to_stdout
@@ -253,8 +253,8 @@ RSpec.describe 'bin/feature-flag' do
let(:type) { 'invalid' }
it 'shows error message and retries' do
- expect($stdin).to receive(:gets).and_return(type)
- expect($stdin).to receive(:gets).and_raise('EOF')
+ expect(Readline).to receive(:readline).and_return(type)
+ expect(Readline).to receive(:readline).and_raise('EOF')
expect do
expect { described_class.read_rollout_issue_url(options) }.to raise_error(/EOF/)
diff --git a/spec/channels/application_cable/connection_spec.rb b/spec/channels/application_cable/connection_spec.rb
index c10e0c0cab4..affde0095cf 100644
--- a/spec/channels/application_cable/connection_spec.rb
+++ b/spec/channels/application_cable/connection_spec.rb
@@ -3,15 +3,11 @@
require 'spec_helper'
RSpec.describe ApplicationCable::Connection, :clean_gitlab_redis_sessions do
- let(:session_id) { Rack::Session::SessionId.new('6919a6f1bb119dd7396fadc38fd18d0d') }
+ include SessionHelpers
context 'when session cookie is set' do
before do
- Gitlab::Redis::Sessions.with do |redis|
- redis.set("session:gitlab:#{session_id.private_id}", Marshal.dump(session_hash))
- end
-
- cookies[Gitlab::Application.config.session_options[:key]] = session_id.public_id
+ stub_session(session_hash)
end
context 'when user is logged in' do
diff --git a/spec/commands/metrics_server/metrics_server_spec.rb b/spec/commands/metrics_server/metrics_server_spec.rb
index b755801bb65..217aa185767 100644
--- a/spec/commands/metrics_server/metrics_server_spec.rb
+++ b/spec/commands/metrics_server/metrics_server_spec.rb
@@ -12,6 +12,11 @@ RSpec.describe 'bin/metrics-server', :aggregate_failures do
{
'test' => {
'monitoring' => {
+ 'web_exporter' => {
+ 'address' => 'localhost',
+ 'enabled' => true,
+ 'port' => 3807
+ },
'sidekiq_exporter' => {
'address' => 'localhost',
'enabled' => true,
@@ -22,56 +27,52 @@ RSpec.describe 'bin/metrics-server', :aggregate_failures do
}
end
- context 'with a running server' do
- let(:metrics_dir) { Dir.mktmpdir }
+ %w(puma sidekiq).each do |target|
+ context "with a running server targeting #{target}" do
+ let(:metrics_dir) { Dir.mktmpdir }
- before do
- # We need to send a request to localhost
- WebMock.allow_net_connect!
+ before do
+ # We need to send a request to localhost
+ WebMock.allow_net_connect!
- config_file.write(YAML.dump(config))
- config_file.close
+ config_file.write(YAML.dump(config))
+ config_file.close
- env = {
- 'GITLAB_CONFIG' => config_file.path,
- 'METRICS_SERVER_TARGET' => 'sidekiq',
- 'WIPE_METRICS_DIR' => '1',
- 'prometheus_multiproc_dir' => metrics_dir
- }
- @pid = Process.spawn(env, 'bin/metrics-server', pgroup: true)
- end
+ @pid = MetricsServer.spawn(target, metrics_dir: metrics_dir, gitlab_config: config_file.path, wipe_metrics_dir: true)
+ end
- after do
- webmock_enable!
+ after do
+ webmock_enable!
- if @pid
- pgrp = Process.getpgid(@pid)
+ if @pid
+ pgrp = Process.getpgid(@pid)
- Timeout.timeout(5) do
- Process.kill('TERM', -pgrp)
- Process.waitpid(@pid)
- end
+ Timeout.timeout(10) do
+ Process.kill('TERM', -pgrp)
+ Process.waitpid(@pid)
+ end
- expect(Gitlab::ProcessManagement.process_alive?(@pid)).to be(false)
+ expect(Gitlab::ProcessManagement.process_alive?(@pid)).to be(false)
+ end
+ rescue Errno::ESRCH => _
+ # 'No such process' means the process died before
+ ensure
+ config_file.unlink
+ FileUtils.rm_rf(metrics_dir, secure: true)
end
- rescue Errno::ESRCH => _
- # 'No such process' means the process died before
- ensure
- config_file.unlink
- FileUtils.rm_rf(metrics_dir, secure: true)
- end
- it 'serves /metrics endpoint' do
- expect do
- Timeout.timeout(5) do
- http_ok = false
- until http_ok
- sleep 1
- response = Gitlab::HTTP.try_get("http://localhost:3807/metrics", allow_local_requests: true)
- http_ok = response&.success?
+ it 'serves /metrics endpoint' do
+ expect do
+ Timeout.timeout(10) do
+ http_ok = false
+ until http_ok
+ sleep 1
+ response = Gitlab::HTTP.try_get("http://localhost:3807/metrics", allow_local_requests: true)
+ http_ok = response&.success?
+ end
end
- end
- end.not_to raise_error
+ end.not_to raise_error
+ end
end
end
end
diff --git a/spec/commands/sidekiq_cluster/cli_spec.rb b/spec/commands/sidekiq_cluster/cli_spec.rb
index d7488e8d965..15b738cacd1 100644
--- a/spec/commands/sidekiq_cluster/cli_spec.rb
+++ b/spec/commands/sidekiq_cluster/cli_spec.rb
@@ -3,9 +3,10 @@
require 'fast_spec_helper'
require 'rspec-parameterized'
+require_relative '../../support/stub_settings_source'
require_relative '../../../sidekiq_cluster/cli'
-RSpec.describe Gitlab::SidekiqCluster::CLI, stubbing_settings_source: true do # rubocop:disable RSpec/FilePath
+RSpec.describe Gitlab::SidekiqCluster::CLI, stub_settings_source: true do # rubocop:disable RSpec/FilePath
let(:cli) { described_class.new('/dev/null') }
let(:timeout) { Gitlab::SidekiqCluster::DEFAULT_SOFT_TIMEOUT_SECONDS }
let(:default_options) do
@@ -302,7 +303,7 @@ RSpec.describe Gitlab::SidekiqCluster::CLI, stubbing_settings_source: true do #
end
it 'does not start a sidekiq metrics server' do
- expect(MetricsServer).not_to receive(:spawn)
+ expect(MetricsServer).not_to receive(:fork)
cli.run(%w(foo))
end
@@ -319,7 +320,7 @@ RSpec.describe Gitlab::SidekiqCluster::CLI, stubbing_settings_source: true do #
end
it 'does not start a sidekiq metrics server' do
- expect(MetricsServer).not_to receive(:spawn)
+ expect(MetricsServer).not_to receive(:fork)
cli.run(%w(foo))
end
@@ -349,7 +350,7 @@ RSpec.describe Gitlab::SidekiqCluster::CLI, stubbing_settings_source: true do #
end
it 'does not start a sidekiq metrics server' do
- expect(MetricsServer).not_to receive(:spawn)
+ expect(MetricsServer).not_to receive(:fork)
cli.run(%w(foo))
end
@@ -375,7 +376,7 @@ RSpec.describe Gitlab::SidekiqCluster::CLI, stubbing_settings_source: true do #
end
it 'does not start a sidekiq metrics server' do
- expect(MetricsServer).not_to receive(:spawn)
+ expect(MetricsServer).not_to receive(:fork)
cli.run(%w(foo))
end
@@ -405,9 +406,9 @@ RSpec.describe Gitlab::SidekiqCluster::CLI, stubbing_settings_source: true do #
specify do
if start_metrics_server
- expect(MetricsServer).to receive(:spawn).with('sidekiq', metrics_dir: metrics_dir, wipe_metrics_dir: true, trapped_signals: trapped_signals)
+ expect(MetricsServer).to receive(:fork).with('sidekiq', metrics_dir: metrics_dir, wipe_metrics_dir: true, reset_signals: trapped_signals)
else
- expect(MetricsServer).not_to receive(:spawn)
+ expect(MetricsServer).not_to receive(:fork)
end
cli.run(%w(foo))
@@ -420,7 +421,7 @@ RSpec.describe Gitlab::SidekiqCluster::CLI, stubbing_settings_source: true do #
let(:sidekiq_exporter_enabled) { true }
it 'does not start the server' do
- expect(MetricsServer).not_to receive(:spawn)
+ expect(MetricsServer).not_to receive(:fork)
cli.run(%w(foo --dryrun))
end
@@ -433,7 +434,7 @@ RSpec.describe Gitlab::SidekiqCluster::CLI, stubbing_settings_source: true do #
before do
allow(cli).to receive(:sleep).with(a_kind_of(Numeric))
- allow(MetricsServer).to receive(:spawn).and_return(99)
+ allow(MetricsServer).to receive(:fork).and_return(99)
cli.start_metrics_server
end
@@ -452,8 +453,8 @@ RSpec.describe Gitlab::SidekiqCluster::CLI, stubbing_settings_source: true do #
allow(Gitlab::ProcessManagement).to receive(:all_alive?).with(an_instance_of(Array)).and_return(false)
allow(cli).to receive(:stop_metrics_server)
- expect(MetricsServer).to receive(:spawn).with(
- 'sidekiq', metrics_dir: metrics_dir, wipe_metrics_dir: false, trapped_signals: trapped_signals
+ expect(MetricsServer).to receive(:fork).with(
+ 'sidekiq', metrics_dir: metrics_dir, wipe_metrics_dir: false, reset_signals: trapped_signals
)
cli.start_loop
diff --git a/spec/controllers/admin/instance_review_controller_spec.rb b/spec/controllers/admin/instance_review_controller_spec.rb
index 2169be4e70c..342562618b2 100644
--- a/spec/controllers/admin/instance_review_controller_spec.rb
+++ b/spec/controllers/admin/instance_review_controller_spec.rb
@@ -23,7 +23,7 @@ RSpec.describe Admin::InstanceReviewController do
stub_application_setting(usage_ping_enabled: true)
stub_usage_data_connections
stub_database_flavor_check
- ::Gitlab::UsageData.data(force_refresh: true)
+ ::Gitlab::Usage::ServicePingReport.for(output: :all_metrics_values)
subject
end
diff --git a/spec/controllers/admin/runners_controller_spec.rb b/spec/controllers/admin/runners_controller_spec.rb
index 08fb12c375e..74f352e8ec2 100644
--- a/spec/controllers/admin/runners_controller_spec.rb
+++ b/spec/controllers/admin/runners_controller_spec.rb
@@ -4,9 +4,10 @@ require 'spec_helper'
RSpec.describe Admin::RunnersController do
let_it_be(:runner) { create(:ci_runner) }
+ let_it_be(:user) { create(:admin) }
before do
- sign_in(create(:admin))
+ sign_in(user)
end
describe '#index' do
@@ -104,6 +105,10 @@ RSpec.describe Admin::RunnersController do
describe '#destroy' do
it 'destroys the runner' do
+ expect_next_instance_of(Ci::UnregisterRunnerService, runner) do |service|
+ expect(service).to receive(:execute).once.and_call_original
+ end
+
delete :destroy, params: { id: runner.id }
expect(response).to have_gitlab_http_status(:found)
diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb
index 6ccba866ebb..533d3896ee6 100644
--- a/spec/controllers/autocomplete_controller_spec.rb
+++ b/spec/controllers/autocomplete_controller_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe AutocompleteController do
let(:project) { create(:project) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
context 'GET users' do
let!(:user2) { create(:user) }
diff --git a/spec/controllers/dashboard/projects_controller_spec.rb b/spec/controllers/dashboard/projects_controller_spec.rb
index 743759d5023..b4a4ac56fce 100644
--- a/spec/controllers/dashboard/projects_controller_spec.rb
+++ b/spec/controllers/dashboard/projects_controller_spec.rb
@@ -97,14 +97,18 @@ RSpec.describe Dashboard::ProjectsController, :aggregate_failures do
subject { get :starred, format: :json }
let(:projects) { create_list(:project, 2, creator: user) }
+ let(:aimed_for_deletion_project) { create_list(:project, 2, :archived, creator: user, marked_for_deletion_at: 3.days.ago) }
before do
- allow(Kaminari.config).to receive(:default_per_page).and_return(1)
-
projects.each do |project|
project.add_developer(user)
create(:users_star_project, project_id: project.id, user_id: user.id)
end
+
+ aimed_for_deletion_project.each do |project|
+ project.add_developer(user)
+ create(:users_star_project, project_id: project.id, user_id: user.id)
+ end
end
it 'returns success' do
@@ -113,10 +117,22 @@ RSpec.describe Dashboard::ProjectsController, :aggregate_failures do
expect(response).to have_gitlab_http_status(:ok)
end
- it 'paginates the records' do
+ context "pagination" do
+ before do
+ allow(Kaminari.config).to receive(:default_per_page).and_return(1)
+ end
+
+ it 'paginates the records' do
+ subject
+
+ expect(assigns(:projects).count).to eq(1)
+ end
+ end
+
+ it 'does not include projects aimed for deletion' do
subject
- expect(assigns(:projects).count).to eq(1)
+ expect(assigns(:projects).count).to eq(2)
end
end
end
diff --git a/spec/controllers/explore/projects_controller_spec.rb b/spec/controllers/explore/projects_controller_spec.rb
index f2328303102..c3f6c653376 100644
--- a/spec/controllers/explore/projects_controller_spec.rb
+++ b/spec/controllers/explore/projects_controller_spec.rb
@@ -73,6 +73,24 @@ RSpec.describe Explore::ProjectsController do
expect(assigns(:projects)).to eq [project1, project2]
end
end
+
+ context 'projects aimed for deletion' do
+ let(:project1) { create(:project, :public, updated_at: 3.days.ago) }
+ let(:project2) { create(:project, :public, updated_at: 1.day.ago) }
+ let(:aimed_for_deletion_project) { create(:project, :public, :archived, updated_at: 2.days.ago, marked_for_deletion_at: 2.days.ago) }
+
+ before do
+ create(:trending_project, project: project1)
+ create(:trending_project, project: project2)
+ create(:trending_project, project: aimed_for_deletion_project)
+ end
+
+ it 'does not list projects aimed for deletion' do
+ get :trending
+
+ expect(assigns(:projects)).to eq [project2, project1]
+ end
+ end
end
describe 'GET #topic' do
diff --git a/spec/controllers/graphql_controller_spec.rb b/spec/controllers/graphql_controller_spec.rb
index 578ce04721c..95f60156c40 100644
--- a/spec/controllers/graphql_controller_spec.rb
+++ b/spec/controllers/graphql_controller_spec.rb
@@ -124,6 +124,16 @@ RSpec.describe GraphqlController do
post :execute
end
+
+ it 'calls the track jetbrains api when trackable method' do
+ agent = 'gitlab-jetbrains-plugin/0.0.1 intellij-idea/2021.2.4 java/11.0.13 mac-os-x/aarch64/12.1'
+ request.env['HTTP_USER_AGENT'] = agent
+
+ expect(Gitlab::UsageDataCounters::JetBrainsPluginActivityUniqueCounter)
+ .to receive(:track_api_request_when_trackable).with(user_agent: agent, user: user)
+
+ post :execute
+ end
end
context 'when user uses an API token' do
@@ -151,6 +161,16 @@ RSpec.describe GraphqlController do
subject
end
+
+ it 'calls the track jetbrains api when trackable method' do
+ agent = 'gitlab-jetbrains-plugin/0.0.1 intellij-idea/2021.2.4 java/11.0.13 mac-os-x/aarch64/12.1'
+ request.env['HTTP_USER_AGENT'] = agent
+
+ expect(Gitlab::UsageDataCounters::JetBrainsPluginActivityUniqueCounter)
+ .to receive(:track_api_request_when_trackable).with(user_agent: agent, user: user)
+
+ subject
+ end
end
context 'when user is not logged in' do
diff --git a/spec/controllers/groups/clusters_controller_spec.rb b/spec/controllers/groups/clusters_controller_spec.rb
index 93c560b4753..710e983dfbd 100644
--- a/spec/controllers/groups/clusters_controller_spec.rb
+++ b/spec/controllers/groups/clusters_controller_spec.rb
@@ -103,7 +103,7 @@ RSpec.describe Groups::ClustersController do
it('is denied for admin when admin mode is disabled') { expect { go }.to be_denied_for(:admin) }
it { expect { go }.to be_allowed_for(:owner).of(group) }
it { expect { go }.to be_allowed_for(:maintainer).of(group) }
- it { expect { go }.to be_denied_for(:developer).of(group) }
+ it { expect { go }.to be_allowed_for(:developer).of(group) }
it { expect { go }.to be_denied_for(:reporter).of(group) }
it { expect { go }.to be_denied_for(:guest).of(group) }
it { expect { go }.to be_denied_for(:user) }
@@ -309,7 +309,8 @@ RSpec.describe Groups::ClustersController do
.to receive(:expires_at_in_session).and_return(1.hour.since.to_i.to_s)
allow_any_instance_of(GoogleApi::CloudPlatform::Client)
.to receive(:projects_zones_clusters_create) do
- OpenStruct.new(
+ double(
+ 'instance',
self_link: 'projects/gcp-project-12345/zones/us-central1-a/operations/ope-123',
status: 'RUNNING'
)
@@ -673,7 +674,7 @@ RSpec.describe Groups::ClustersController do
it('is denied for admin when admin mode is disabled') { expect { go }.to be_denied_for(:admin) }
it { expect { go }.to be_allowed_for(:owner).of(group) }
it { expect { go }.to be_allowed_for(:maintainer).of(group) }
- it { expect { go }.to be_denied_for(:developer).of(group) }
+ it { expect { go }.to be_allowed_for(:developer).of(group) }
it { expect { go }.to be_denied_for(:reporter).of(group) }
it { expect { go }.to be_denied_for(:guest).of(group) }
it { expect { go }.to be_denied_for(:user) }
diff --git a/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb b/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb
index f438be534fa..57a83da3425 100644
--- a/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb
+++ b/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb
@@ -47,6 +47,24 @@ RSpec.describe Groups::DependencyProxyForContainersController do
end
end
+ shared_examples 'with invalid path' do
+ context 'with invalid image' do
+ let(:image) { '../path_traversal' }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(Gitlab::Utils::PathTraversalAttackError, 'Invalid path')
+ end
+ end
+
+ context 'with invalid tag' do
+ let(:tag) { 'latest%2f..%2f..%2fpath_traversal' }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(Gitlab::Utils::PathTraversalAttackError, 'Invalid path')
+ end
+ end
+ end
+
shared_examples 'without permission' do
context 'with invalid user' do
before do
@@ -164,8 +182,10 @@ RSpec.describe Groups::DependencyProxyForContainersController do
end
describe 'GET #manifest' do
+ let_it_be(:image) { 'alpine' }
let_it_be(:tag) { 'latest' }
- let_it_be(:manifest) { create(:dependency_proxy_manifest, file_name: "alpine:#{tag}.json", group: group) }
+ let_it_be(:file_name) { "#{image}:#{tag}.json" }
+ let_it_be(:manifest) { create(:dependency_proxy_manifest, file_name: file_name, group: group) }
let(:pull_response) { { status: :success, manifest: manifest, from_cache: false } }
@@ -235,6 +255,8 @@ RSpec.describe Groups::DependencyProxyForContainersController do
context 'with workhorse response' do
let(:pull_response) { { status: :success, manifest: nil, from_cache: false } }
+ it_behaves_like 'with invalid path'
+
it 'returns Workhorse send-dependency instructions', :aggregate_failures do
subject
@@ -246,7 +268,7 @@ RSpec.describe Groups::DependencyProxyForContainersController do
"Authorization" => ["Bearer abcd1234"],
"Accept" => ::ContainerRegistry::Client::ACCEPTED_TYPES
)
- expect(url).to eq(DependencyProxy::Registry.manifest_url('alpine', tag))
+ expect(url).to eq(DependencyProxy::Registry.manifest_url(image, tag))
expect(response.headers['Content-Type']).to eq('application/gzip')
expect(response.headers['Content-Disposition']).to eq(
ActionDispatch::Http::ContentDisposition.format(disposition: 'attachment', filename: manifest.file_name)
@@ -277,7 +299,7 @@ RSpec.describe Groups::DependencyProxyForContainersController do
it_behaves_like 'not found when disabled'
def get_manifest(tag)
- get :manifest, params: { group_id: group.to_param, image: 'alpine', tag: tag }
+ get :manifest, params: { group_id: group.to_param, image: image, tag: tag }
end
end
@@ -440,6 +462,7 @@ RSpec.describe Groups::DependencyProxyForContainersController do
end
it_behaves_like 'a package tracking event', described_class.name, 'pull_manifest'
+ it_behaves_like 'with invalid path'
context 'with no existing manifest' do
it 'creates a manifest' do
diff --git a/spec/controllers/groups/releases_controller_spec.rb b/spec/controllers/groups/releases_controller_spec.rb
index 50701382945..582a77b1c50 100644
--- a/spec/controllers/groups/releases_controller_spec.rb
+++ b/spec/controllers/groups/releases_controller_spec.rb
@@ -6,14 +6,14 @@ RSpec.describe Groups::ReleasesController do
let(:group) { create(:group) }
let!(:project) { create(:project, :repository, :public, namespace: group) }
let!(:private_project) { create(:project, :repository, :private, namespace: group) }
- let(:developer) { create(:user) }
+ let(:guest) { create(:user) }
let!(:release_1) { create(:release, project: project, tag: 'v1', released_at: Time.zone.parse('2020-02-15')) }
let!(:release_2) { create(:release, project: project, tag: 'v2', released_at: Time.zone.parse('2020-02-20')) }
let!(:private_release_1) { create(:release, project: private_project, tag: 'p1', released_at: Time.zone.parse('2020-03-01')) }
let!(:private_release_2) { create(:release, project: private_project, tag: 'p2', released_at: Time.zone.parse('2020-03-05')) }
before do
- private_project.add_developer(developer)
+ group.add_guest(guest)
end
describe 'GET #index' do
@@ -42,7 +42,7 @@ RSpec.describe Groups::ReleasesController do
end
it 'does not return any releases' do
- expect(json_response.map {|r| r['tag'] } ).to match_array(%w(v2 v1))
+ expect(json_response.map {|r| r['tag'] } ).to be_empty
end
it 'returns OK' do
@@ -52,7 +52,7 @@ RSpec.describe Groups::ReleasesController do
context 'the user is authorized' do
it "returns all group's public and private project's releases as JSON, ordered by released_at" do
- sign_in(developer)
+ sign_in(guest)
subject
diff --git a/spec/controllers/groups/runners_controller_spec.rb b/spec/controllers/groups/runners_controller_spec.rb
index a8830efe653..9f0615a96ae 100644
--- a/spec/controllers/groups/runners_controller_spec.rb
+++ b/spec/controllers/groups/runners_controller_spec.rb
@@ -190,6 +190,10 @@ RSpec.describe Groups::RunnersController do
end
it 'destroys the runner and redirects' do
+ expect_next_instance_of(Ci::UnregisterRunnerService, runner) do |service|
+ expect(service).to receive(:execute).once.and_call_original
+ end
+
delete :destroy, params: params
expect(response).to have_gitlab_http_status(:found)
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
index 62171528695..a82c5681911 100644
--- a/spec/controllers/groups_controller_spec.rb
+++ b/spec/controllers/groups_controller_spec.rb
@@ -132,6 +132,29 @@ RSpec.describe GroupsController, factory_default: :keep do
end
end
end
+
+ describe 'require_verification_for_namespace_creation experiment', :experiment do
+ before do
+ sign_in(owner)
+ stub_experiments(require_verification_for_namespace_creation: :candidate)
+ end
+
+ it 'tracks a "start_create_group" event' do
+ expect(experiment(:require_verification_for_namespace_creation)).to track(
+ :start_create_group
+ ).on_next_instance.with_context(user: owner)
+
+ get :new
+ end
+
+ context 'when creating a sub-group' do
+ it 'does not track a "start_create_group" event' do
+ expect(experiment(:require_verification_for_namespace_creation)).not_to track(:start_create_group)
+
+ get :new, params: { parent_id: group.id }
+ end
+ end
+ end
end
describe 'GET #activity' do
diff --git a/spec/controllers/metrics_controller_spec.rb b/spec/controllers/metrics_controller_spec.rb
index 4f74af295c6..9fa90dde997 100644
--- a/spec/controllers/metrics_controller_spec.rb
+++ b/spec/controllers/metrics_controller_spec.rb
@@ -67,12 +67,6 @@ RSpec.describe MetricsController, :request_store do
expect(response.body).to match(/^prometheus_counter 1$/)
end
- it 'initializes the rails request SLIs' do
- expect(Gitlab::Metrics::RailsSlis).to receive(:initialize_request_slis_if_needed!).and_call_original
-
- get :index
- end
-
context 'prometheus metrics are disabled' do
before do
allow(Gitlab::Metrics).to receive(:prometheus_metrics_enabled?).and_return(false)
diff --git a/spec/controllers/oauth/authorizations_controller_spec.rb b/spec/controllers/oauth/authorizations_controller_spec.rb
index 98cc8d83e0c..e6553c027d6 100644
--- a/spec/controllers/oauth/authorizations_controller_spec.rb
+++ b/spec/controllers/oauth/authorizations_controller_spec.rb
@@ -4,7 +4,13 @@ require 'spec_helper'
RSpec.describe Oauth::AuthorizationsController do
let(:user) { create(:user) }
- let!(:application) { create(:oauth_application, scopes: 'api read_user', redirect_uri: 'http://example.com') }
+ let(:application_scopes) { 'api read_user' }
+
+ let!(:application) do
+ create(:oauth_application, scopes: application_scopes,
+ redirect_uri: 'http://example.com')
+ end
+
let(:params) do
{
response_type: "code",
@@ -119,6 +125,92 @@ RSpec.describe Oauth::AuthorizationsController do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template('doorkeeper/authorizations/redirect')
end
+
+ context 'with gl_auth_type=login' do
+ let(:minimal_scope) { Gitlab::Auth::READ_USER_SCOPE.to_s }
+
+ before do
+ params[:gl_auth_type] = 'login'
+ end
+
+ shared_examples 'downgrades scopes' do
+ it 'downgrades the scopes' do
+ subject
+
+ pre_auth = controller.send(:pre_auth)
+
+ expect(pre_auth.scopes).to contain_exactly(minimal_scope)
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template('doorkeeper/authorizations/new')
+ # See: config/locales/doorkeeper.en.yml
+ expect(response.body).to include("Read the authenticated user&#39;s personal information")
+ expect(response.body).not_to include("Access the authenticated user&#39;s API")
+ end
+ end
+
+ shared_examples 'adds read_user scope' do
+ it 'modifies the client.application.scopes' do
+ expect { subject }
+ .to change { application.reload.scopes }.to include(minimal_scope)
+ end
+
+ it 'does not remove pre-existing scopes' do
+ subject
+
+ expect(application.scopes).to include(*application_scopes.split(/ /))
+ end
+ end
+
+ context 'the application has all scopes' do
+ let(:application_scopes) { 'api read_api read_user' }
+
+ include_examples 'downgrades scopes'
+ end
+
+ context 'the application has api and read_user scopes' do
+ let(:application_scopes) { 'api read_user' }
+
+ include_examples 'downgrades scopes'
+ end
+
+ context 'the application has read_api and read_user scopes' do
+ let(:application_scopes) { 'read_api read_user' }
+
+ include_examples 'downgrades scopes'
+ end
+
+ context 'the application has only api scopes' do
+ let(:application_scopes) { 'api' }
+
+ include_examples 'downgrades scopes'
+ include_examples 'adds read_user scope'
+ end
+
+ context 'the application has only read_api scopes' do
+ let(:application_scopes) { 'read_api' }
+
+ include_examples 'downgrades scopes'
+ include_examples 'adds read_user scope'
+ end
+
+ context 'the application has scopes we do not handle' do
+ let(:application_scopes) { Gitlab::Auth::PROFILE_SCOPE.to_s }
+
+ before do
+ params[:scope] = application_scopes
+ end
+
+ it 'does not modify the scopes' do
+ subject
+
+ pre_auth = controller.send(:pre_auth)
+
+ expect(pre_auth.scopes).to contain_exactly(application_scopes)
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template('doorkeeper/authorizations/new')
+ end
+ end
+ end
end
end
end
diff --git a/spec/controllers/projects/artifacts_controller_spec.rb b/spec/controllers/projects/artifacts_controller_spec.rb
index 754b0ddfb94..f410c16b30b 100644
--- a/spec/controllers/projects/artifacts_controller_spec.rb
+++ b/spec/controllers/projects/artifacts_controller_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Projects::ArtifactsController do
include RepoHelpers
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let_it_be(:project) { create(:project, :repository, :public) }
let_it_be(:pipeline, reload: true) do
diff --git a/spec/controllers/projects/autocomplete_sources_controller_spec.rb b/spec/controllers/projects/autocomplete_sources_controller_spec.rb
index 865b31a28d7..79edc261809 100644
--- a/spec/controllers/projects/autocomplete_sources_controller_spec.rb
+++ b/spec/controllers/projects/autocomplete_sources_controller_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Projects::AutocompleteSourcesController do
- let_it_be(:group) { create(:group) }
+ let_it_be(:group, reload: true) { create(:group) }
let_it_be(:project) { create(:project, namespace: group) }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:user) { create(:user) }
@@ -69,4 +69,62 @@ RSpec.describe Projects::AutocompleteSourcesController do
end
end
end
+
+ describe 'GET contacts' do
+ let_it_be(:contact_1) { create(:contact, group: group) }
+ let_it_be(:contact_2) { create(:contact, group: group) }
+
+ before do
+ sign_in(user)
+ end
+
+ context 'when feature flag is enabled' do
+ context 'when a group has contact relations enabled' do
+ before do
+ create(:crm_settings, group: group, enabled: true)
+ end
+
+ context 'when a user can read contacts' do
+ it 'lists contacts' do
+ group.add_developer(user)
+
+ get :contacts, format: :json, params: { namespace_id: group.path, project_id: project.path }
+
+ emails = json_response.map { |contact_data| contact_data["email"] }
+ expect(emails).to match_array([contact_1.email, contact_2.email])
+ end
+ end
+
+ context 'when a user can not read contacts' do
+ it 'renders 404' do
+ get :contacts, format: :json, params: { namespace_id: group.path, project_id: project.path }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ context 'when a group has contact relations disabled' do
+ it 'renders 404' do
+ group.add_developer(user)
+
+ get :contacts, format: :json, params: { namespace_id: group.path, project_id: project.path }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(customer_relations: false)
+ end
+
+ it 'renders 404' do
+ get :contacts, format: :json, params: { namespace_id: group.path, project_id: project.path }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
end
diff --git a/spec/controllers/projects/avatars_controller_spec.rb b/spec/controllers/projects/avatars_controller_spec.rb
index 35878fe4c2d..39a373ed6b6 100644
--- a/spec/controllers/projects/avatars_controller_spec.rb
+++ b/spec/controllers/projects/avatars_controller_spec.rb
@@ -38,7 +38,7 @@ RSpec.describe Projects::AvatarsController do
end
it 'sets appropriate caching headers' do
- sign_in(project.owner)
+ sign_in(project.first_owner)
subject
expect(response.cache_control[:public]).to eq(true)
@@ -63,7 +63,7 @@ RSpec.describe Projects::AvatarsController do
let(:project) { create(:project, :repository, avatar: fixture_file_upload("spec/fixtures/dk.png", "image/png")) }
before do
- sign_in(project.owner)
+ sign_in(project.first_owner)
end
it 'removes avatar from DB by calling destroy' do
diff --git a/spec/controllers/projects/badges_controller_spec.rb b/spec/controllers/projects/badges_controller_spec.rb
index 242b2fd3ec6..d41e8d6169f 100644
--- a/spec/controllers/projects/badges_controller_spec.rb
+++ b/spec/controllers/projects/badges_controller_spec.rb
@@ -7,39 +7,100 @@ RSpec.describe Projects::BadgesController do
let_it_be(:pipeline, reload: true) { create(:ci_empty_pipeline, project: project) }
let_it_be(:user) { create(:user) }
- shared_examples 'a badge resource' do |badge_type|
- context 'when pipelines are public' do
+ shared_context 'renders badge irrespective of project access levels' do |badge_type|
+ context 'when project is public' do
before do
- project.update!(public_builds: true)
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
end
- context 'when project is public' do
- before do
- project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
- end
+ it "returns the #{badge_type} badge to unauthenticated users" do
+ get_badge(badge_type)
- it "returns the #{badge_type} badge to unauthenticated users" do
- get_badge(badge_type)
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
- expect(response).to have_gitlab_http_status(:ok)
- end
+ context 'when project is restricted' do
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
+ project.add_guest(user)
+ sign_in(user)
end
- context 'when project is restricted' do
- before do
- project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
- project.add_guest(user)
- sign_in(user)
- end
+ it "returns the #{badge_type} badge to guest users" do
+ get_badge(badge_type)
- it "returns the #{badge_type} badge to guest users" do
- get_badge(badge_type)
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+ end
- expect(response).to have_gitlab_http_status(:ok)
- end
+ shared_context 'when pipelines are public' do |badge_type|
+ before do
+ project.update!(public_builds: true)
+ end
+
+ it_behaves_like 'renders badge irrespective of project access levels', badge_type
+ end
+
+ shared_context 'when pipelines are not public' do |badge_type|
+ before do
+ project.update!(public_builds: false)
+ end
+
+ context 'when project is public' do
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ end
+
+ it 'returns 404 to unauthenticated users' do
+ get_badge(badge_type)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when project is restricted to the user' do
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
+ project.add_guest(user)
+ sign_in(user)
+ end
+
+ it 'defaults to project permissions' do
+ get_badge(badge_type)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ shared_context 'customization' do |badge_type|
+ render_views
+
+ before do
+ project.add_maintainer(user)
+ sign_in(user)
+ end
+
+ context 'when key_text param is used' do
+ it 'sets custom key text' do
+ get_badge(badge_type, key_text: 'custom key text')
+
+ expect(response.body).to include('custom key text')
+ end
+ end
+
+ context 'when key_width param is used' do
+ it 'sets custom key width' do
+ get_badge(badge_type, key_width: '123')
+
+ expect(response.body).to include('123')
end
end
+ end
+ shared_examples 'a badge resource' do |badge_type|
context 'format' do
before do
project.add_maintainer(user)
@@ -77,61 +138,11 @@ RSpec.describe Projects::BadgesController do
end
end
- context 'when pipelines are not public' do
- before do
- project.update!(public_builds: false)
- end
-
- context 'when project is public' do
- before do
- project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
- end
-
- it 'returns 404 to unauthenticated users' do
- get_badge(badge_type)
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
-
- context 'when project is restricted to the user' do
- before do
- project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
- project.add_guest(user)
- sign_in(user)
- end
-
- it 'defaults to project permissions' do
- get_badge(badge_type)
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
- end
-
- context 'customization' do
- render_views
-
- before do
- project.add_maintainer(user)
- sign_in(user)
- end
-
- context 'when key_text param is used' do
- it 'sets custom key text' do
- get_badge(badge_type, key_text: 'custom key text')
-
- expect(response.body).to include('custom key text')
- end
- end
-
- context 'when key_width param is used' do
- it 'sets custom key width' do
- get_badge(badge_type, key_width: '123')
+ it_behaves_like 'customization', badge_type
- expect(response.body).to include('123')
- end
- end
+ if [:pipeline, :coverage].include?(badge_type)
+ it_behaves_like 'when pipelines are public', badge_type
+ it_behaves_like 'when pipelines are not public', badge_type
end
end
@@ -163,6 +174,13 @@ RSpec.describe Projects::BadgesController do
it_behaves_like 'a badge resource', :coverage
end
+ describe '#release' do
+ action = :release
+
+ it_behaves_like 'a badge resource', action
+ it_behaves_like 'renders badge irrespective of project access levels', action
+ end
+
def get_badge(badge, args = {})
params = {
namespace_id: project.namespace.to_param,
diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb
index d9dedb04b0d..ea22e6b6f10 100644
--- a/spec/controllers/projects/branches_controller_spec.rb
+++ b/spec/controllers/projects/branches_controller_spec.rb
@@ -657,6 +657,36 @@ RSpec.describe Projects::BranchesController do
end
end
+ context 'sorting', :aggregate_failures do
+ let(:sort) { 'name_asc' }
+
+ before do
+ get :index, format: :html, params: {
+ namespace_id: project.namespace, project_id: project, state: 'all', sort: sort
+ }
+ end
+
+ it { expect(assigns[:sort]).to eq('name_asc') }
+
+ context 'when sort is not provided' do
+ let(:sort) { nil }
+
+ it 'uses a default sort without an error message' do
+ expect(assigns[:sort]).to eq('updated_desc')
+ expect(controller).not_to set_flash.now[:alert]
+ end
+ end
+
+ context 'when sort is not supported' do
+ let(:sort) { 'unknown' }
+
+ it 'uses a default sort and shows an error message' do
+ expect(assigns[:sort]).to eq('updated_desc')
+ expect(controller).to set_flash.now[:alert].to(/Unsupported sort/)
+ end
+ end
+ end
+
context 'when gitaly is not available' do
before do
allow_next_instance_of(Gitlab::GitalyClient::RefService) do |ref_service|
diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb
index 2a8feb09780..d0bef810ec8 100644
--- a/spec/controllers/projects/clusters_controller_spec.rb
+++ b/spec/controllers/projects/clusters_controller_spec.rb
@@ -101,7 +101,7 @@ RSpec.describe Projects::ClustersController do
it { expect { go }.to be_allowed_for(:owner).of(project) }
it { expect { go }.to be_allowed_for(:maintainer).of(project) }
- it { expect { go }.to be_denied_for(:developer).of(project) }
+ it { expect { go }.to be_allowed_for(:developer).of(project) }
it { expect { go }.to be_denied_for(:reporter).of(project) }
it { expect { go }.to be_denied_for(:guest).of(project) }
it { expect { go }.to be_denied_for(:user) }
@@ -315,7 +315,8 @@ RSpec.describe Projects::ClustersController do
.to receive(:expires_at_in_session).and_return(1.hour.since.to_i.to_s)
allow_any_instance_of(GoogleApi::CloudPlatform::Client)
.to receive(:projects_zones_clusters_create) do
- OpenStruct.new(
+ double(
+ 'secure',
self_link: 'projects/gcp-project-12345/zones/us-central1-a/operations/ope-123',
status: 'RUNNING'
)
@@ -711,7 +712,7 @@ RSpec.describe Projects::ClustersController do
end
it { expect { go }.to be_allowed_for(:owner).of(project) }
it { expect { go }.to be_allowed_for(:maintainer).of(project) }
- it { expect { go }.to be_denied_for(:developer).of(project) }
+ it { expect { go }.to be_allowed_for(:developer).of(project) }
it { expect { go }.to be_denied_for(:reporter).of(project) }
it { expect { go }.to be_denied_for(:guest).of(project) }
it { expect { go }.to be_denied_for(:user) }
diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb
index 16bb33e95c8..72fee40a6e9 100644
--- a/spec/controllers/projects/commit_controller_spec.rb
+++ b/spec/controllers/projects/commit_controller_spec.rb
@@ -183,6 +183,18 @@ RSpec.describe Projects::CommitController do
expect(assigns(:tags)).to eq([])
expect(assigns(:tags_limit_exceeded)).to be_truthy
end
+
+ context 'when commit is not found' do
+ it 'responds with 404' do
+ get(:branches, params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: '11111111111111111111111111111111111111'
+ })
+
+ expect(response).to be_not_found
+ end
+ end
end
describe 'POST revert' do
diff --git a/spec/controllers/projects/commits_controller_spec.rb b/spec/controllers/projects/commits_controller_spec.rb
index fd840fafa61..c7f98406201 100644
--- a/spec/controllers/projects/commits_controller_spec.rb
+++ b/spec/controllers/projects/commits_controller_spec.rb
@@ -88,6 +88,26 @@ RSpec.describe Projects::CommitsController do
expect(response).to be_successful
end
+
+ context 'when limit is a hash' do
+ it 'uses the default limit' do
+ expect_any_instance_of(Repository).to receive(:commits).with(
+ "master",
+ path: "README.md",
+ limit: described_class::COMMITS_DEFAULT_LIMIT,
+ offset: 0
+ ).and_call_original
+
+ get(:show, params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: id,
+ limit: { 'broken' => 'value' }
+ })
+
+ expect(response).to be_successful
+ end
+ end
end
context "when the ref name ends in .atom" do
@@ -131,6 +151,20 @@ RSpec.describe Projects::CommitsController do
expect(response.media_type).to eq('text/html')
end
end
+
+ context 'when the ref does not exist' do
+ before do
+ get(:show, params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: 'unknown.atom'
+ })
+ end
+
+ it 'returns 404 page' do
+ expect(response).to be_not_found
+ end
+ end
end
end
diff --git a/spec/controllers/projects/compare_controller_spec.rb b/spec/controllers/projects/compare_controller_spec.rb
index 48afd42e8ff..62b93a2728b 100644
--- a/spec/controllers/projects/compare_controller_spec.rb
+++ b/spec/controllers/projects/compare_controller_spec.rb
@@ -25,15 +25,25 @@ RSpec.describe Projects::CompareController do
end
describe 'GET index' do
+ let(:params) { { namespace_id: project.namespace, project_id: project } }
+
render_views
before do
- get :index, params: { namespace_id: project.namespace, project_id: project }
+ get :index, params: params
end
it 'returns successfully' do
expect(response).to be_successful
end
+
+ context 'with incorrect parameters' do
+ let(:params) { super().merge(from: { invalid: :param }, to: { also: :invalid }) }
+
+ it 'returns successfully' do
+ expect(response).to be_successful
+ end
+ end
end
describe 'GET show' do
@@ -340,12 +350,13 @@ RSpec.describe Projects::CompareController do
context 'when sending invalid params' do
where(:from_ref, :to_ref, :from_project_id, :expected_redirect_params) do
- '' | '' | '' | {}
- 'main' | '' | '' | { from: 'main' }
- '' | 'main' | '' | { to: 'main' }
- '' | '' | '1' | { from_project_id: 1 }
- 'main' | '' | '1' | { from: 'main', from_project_id: 1 }
- '' | 'main' | '1' | { to: 'main', from_project_id: 1 }
+ '' | '' | '' | {}
+ 'main' | '' | '' | { from: 'main' }
+ '' | 'main' | '' | { to: 'main' }
+ '' | '' | '1' | { from_project_id: 1 }
+ 'main' | '' | '1' | { from: 'main', from_project_id: 1 }
+ '' | 'main' | '1' | { to: 'main', from_project_id: 1 }
+ ['a'] | ['b'] | ['c'] | {}
end
with_them do
diff --git a/spec/controllers/projects/forks_controller_spec.rb b/spec/controllers/projects/forks_controller_spec.rb
index e53e53980b5..0f8f3b49e02 100644
--- a/spec/controllers/projects/forks_controller_spec.rb
+++ b/spec/controllers/projects/forks_controller_spec.rb
@@ -67,6 +67,18 @@ RSpec.describe Projects::ForksController do
expect(assigns[:private_forks_count]).to eq(0)
end
end
+
+ context 'when unsupported keys are provided' do
+ it 'ignores them' do
+ get :index, params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ user: 'unsupported'
+ }
+
+ expect(assigns[:forks]).to be_present
+ end
+ end
end
context 'when fork is internal' do
diff --git a/spec/controllers/projects/group_links_controller_spec.rb b/spec/controllers/projects/group_links_controller_spec.rb
index d514c486f60..ea15d483c90 100644
--- a/spec/controllers/projects/group_links_controller_spec.rb
+++ b/spec/controllers/projects/group_links_controller_spec.rb
@@ -178,7 +178,7 @@ RSpec.describe Projects::GroupLinksController do
context 'when `expires_at` is set' do
it 'returns correct json response' do
- expect(json_response).to eq({ "expires_in" => "about 1 month", "expires_soon" => false })
+ expect(json_response).to eq({ "expires_in" => controller.helpers.time_ago_with_tooltip(expiry_date), "expires_soon" => false })
end
end
diff --git a/spec/controllers/projects/hooks_controller_spec.rb b/spec/controllers/projects/hooks_controller_spec.rb
index 2ab18ccddbf..ebcf35a7ecd 100644
--- a/spec/controllers/projects/hooks_controller_spec.rb
+++ b/spec/controllers/projects/hooks_controller_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Projects::HooksController do
let_it_be(:project) { create(:project) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
before do
sign_in(user)
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index d91c1b0d29a..bf0b833b311 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -502,10 +502,7 @@ RSpec.describe Projects::IssuesController do
context 'with valid params' do
it 'reorders issues and returns a successful 200 response' do
- reorder_issue(issue1,
- move_after_id: issue2.id,
- move_before_id: issue3.id,
- group_full_path: group.full_path)
+ reorder_issue(issue1, move_after_id: issue2.id, move_before_id: issue3.id)
[issue1, issue2, issue3].map(&:reload)
@@ -531,12 +528,10 @@ RSpec.describe Projects::IssuesController do
end
it 'returns a unprocessable entity 422 response for issues not in group' do
- another_group = create(:group)
+ other_group_project = create(:project, group: create(:group))
+ other_group_issue = create(:issue, project: other_group_project)
- reorder_issue(issue1,
- move_after_id: issue2.id,
- move_before_id: issue3.id,
- group_full_path: another_group.full_path)
+ reorder_issue(issue1, move_after_id: issue2.id, move_before_id: other_group_issue.id)
expect(response).to have_gitlab_http_status(:unprocessable_entity)
end
@@ -555,15 +550,14 @@ RSpec.describe Projects::IssuesController do
end
end
- def reorder_issue(issue, move_after_id: nil, move_before_id: nil, group_full_path: nil)
+ def reorder_issue(issue, move_after_id: nil, move_before_id: nil)
put :reorder,
params: {
namespace_id: project.namespace.to_param,
project_id: project,
id: issue.iid,
move_after_id: move_after_id,
- move_before_id: move_before_id,
- group_full_path: group_full_path
+ move_before_id: move_before_id
},
format: :json
end
diff --git a/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb b/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb
index e07b7e4586a..366a1e587ab 100644
--- a/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Projects::MergeRequests::ConflictsController do
let(:project) { create(:project, :repository) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
let(:merge_request_with_conflicts) do
create(:merge_request, source_branch: 'conflict-resolvable', target_branch: 'conflict-start', source_project: project, merge_status: :unchecked) do |mr|
diff --git a/spec/controllers/projects/merge_requests/creations_controller_spec.rb b/spec/controllers/projects/merge_requests/creations_controller_spec.rb
index df2023b7356..3c650988b4f 100644
--- a/spec/controllers/projects/merge_requests/creations_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/creations_controller_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Projects::MergeRequests::CreationsController do
let(:project) { create(:project, :repository) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:fork_project) { create(:forked_project_with_submodules) }
let(:get_diff_params) do
{
diff --git a/spec/controllers/projects/merge_requests/drafts_controller_spec.rb b/spec/controllers/projects/merge_requests/drafts_controller_spec.rb
index 580211893dc..222bb977beb 100644
--- a/spec/controllers/projects/merge_requests/drafts_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/drafts_controller_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe Projects::MergeRequests::DraftsController do
let(:project) { create(:project, :repository) }
let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:user2) { create(:user) }
let(:params) do
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 36b6df59ef5..2390687c3ea 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe Projects::MergeRequestsController do
let_it_be_with_refind(:project) { create(:project, :repository) }
let_it_be_with_reload(:project_public_with_private_builds) { create(:project, :repository, :public, :builds_private) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: merge_request_source_project, allow_collaboration: false) }
let(:merge_request_source_project) { project }
@@ -57,19 +57,13 @@ RSpec.describe Projects::MergeRequestsController do
merge_request.mark_as_unchecked!
end
- context 'check_mergeability_async_in_widget feature flag is disabled' do
- before do
- stub_feature_flags(check_mergeability_async_in_widget: false)
+ it 'checks mergeability asynchronously' do
+ expect_next_instance_of(MergeRequests::MergeabilityCheckService) do |service|
+ expect(service).not_to receive(:execute)
+ expect(service).to receive(:async_execute)
end
- it 'checks mergeability asynchronously' do
- expect_next_instance_of(MergeRequests::MergeabilityCheckService) do |service|
- expect(service).not_to receive(:execute)
- expect(service).to receive(:async_execute)
- end
-
- go
- end
+ go
end
end
@@ -449,7 +443,7 @@ RSpec.describe Projects::MergeRequestsController do
context 'when the merge request is not mergeable' do
before do
- merge_request.update!(title: "WIP: #{merge_request.title}")
+ merge_request.update!(title: "Draft: #{merge_request.title}")
post :merge, params: base_params
end
@@ -2084,6 +2078,20 @@ RSpec.describe Projects::MergeRequestsController do
end
end
+ context 'when source branch is protected from force push' do
+ before do
+ create(:protected_branch, project: project, name: merge_request.source_branch, allow_force_push: false)
+ end
+
+ it 'returns 404' do
+ expect_rebase_worker_for(user).never
+
+ post_rebase
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
context 'with a forked project' do
let(:forked_project) { fork_project(project, fork_owner, repository: true) }
let(:fork_owner) { create(:user) }
diff --git a/spec/controllers/projects/mirrors_controller_spec.rb b/spec/controllers/projects/mirrors_controller_spec.rb
index 7c5d14d3a22..7bc86d7c583 100644
--- a/spec/controllers/projects/mirrors_controller_spec.rb
+++ b/spec/controllers/projects/mirrors_controller_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Projects::MirrorsController do
shared_examples 'only admin is allowed when mirroring is disabled' do
let(:subject_action) { raise 'subject_action is required' }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:project_settings_path) { project_settings_repository_path(project, anchor: 'js-push-remote-settings') }
context 'when project mirroring is enabled' do
@@ -88,7 +88,7 @@ RSpec.describe Projects::MirrorsController do
context 'when the current project is not a mirror' do
it 'allows to create a remote mirror' do
- sign_in(project.owner)
+ sign_in(project.first_owner)
expect do
do_put(project, remote_mirrors_attributes: { '0' => { 'enabled' => 1, 'url' => 'http://foo.com' } })
@@ -106,7 +106,7 @@ RSpec.describe Projects::MirrorsController do
end
it 'processes a successful update' do
- sign_in(project.owner)
+ sign_in(project.first_owner)
do_put(project, remote_mirrors_attributes: { '0' => ssh_mirror_attributes })
expect(response).to redirect_to(project_settings_repository_path(project, anchor: 'js-push-remote-settings'))
@@ -126,7 +126,7 @@ RSpec.describe Projects::MirrorsController do
let(:project) { create(:project, :repository, :remote_mirror) }
before do
- sign_in(project.owner)
+ sign_in(project.first_owner)
end
context 'With valid URL for a push' do
@@ -169,7 +169,7 @@ RSpec.describe Projects::MirrorsController do
let(:cache) { SshHostKey.new(project: project, url: "ssh://example.com:22") }
before do
- sign_in(project.owner)
+ sign_in(project.first_owner)
end
context 'invalid URLs' do
diff --git a/spec/controllers/projects/packages/infrastructure_registry_controller_spec.rb b/spec/controllers/projects/packages/infrastructure_registry_controller_spec.rb
index 707edeaeee3..a655c742973 100644
--- a/spec/controllers/projects/packages/infrastructure_registry_controller_spec.rb
+++ b/spec/controllers/projects/packages/infrastructure_registry_controller_spec.rb
@@ -52,18 +52,6 @@ RSpec.describe Projects::Packages::InfrastructureRegistryController do
expect(assigns(:package_files)).to contain_exactly(terraform_module_package_file)
end
-
- context 'with packages_installable_package_files disabled' do
- before do
- stub_feature_flags(packages_installable_package_files: false)
- end
-
- it 'returns them' do
- subject
-
- expect(assigns(:package_files)).to contain_exactly(package_file_pending_destruction, terraform_module_package_file)
- end
- end
end
end
end
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
index 3fe709a0d44..4a51e2ed5a0 100644
--- a/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -1169,7 +1169,7 @@ RSpec.describe Projects::PipelinesController do
context 'when user has ability to delete pipeline' do
before do
- sign_in(project.owner)
+ sign_in(project.first_owner)
end
it 'deletes pipeline and redirects' do
diff --git a/spec/controllers/projects/refs_controller_spec.rb b/spec/controllers/projects/refs_controller_spec.rb
index b625ce35d61..56415663109 100644
--- a/spec/controllers/projects/refs_controller_spec.rb
+++ b/spec/controllers/projects/refs_controller_spec.rb
@@ -47,6 +47,23 @@ RSpec.describe Projects::RefsController do
expect(response).to be_not_found
end
+ context 'when ref is incorrect' do
+ it 'returns 404 page' do
+ xhr_get(:json, id: '.')
+
+ expect(response).to be_not_found
+ end
+ end
+
+ context 'when offset has an invalid format' do
+ it 'renders JSON' do
+ xhr_get(:json, offset: { wrong: :format })
+
+ expect(response).to be_successful
+ expect(json_response).to be_kind_of(Array)
+ end
+ end
+
context 'when json is requested' do
it 'renders JSON' do
expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
diff --git a/spec/controllers/projects/repositories_controller_spec.rb b/spec/controllers/projects/repositories_controller_spec.rb
index 1370ec9cc0b..928428b5caf 100644
--- a/spec/controllers/projects/repositories_controller_spec.rb
+++ b/spec/controllers/projects/repositories_controller_spec.rb
@@ -3,7 +3,37 @@
require "spec_helper"
RSpec.describe Projects::RepositoriesController do
- let(:project) { create(:project, :repository) }
+ let_it_be(:project) { create(:project, :repository) }
+
+ describe 'POST create' do
+ let_it_be(:user) { create(:user) }
+
+ let(:request) { post :create, params: { namespace_id: project.namespace, project_id: project } }
+
+ before do
+ project.add_maintainer(user)
+ sign_in(user)
+ end
+
+ context 'when repository does not exist' do
+ let!(:project) { create(:project) }
+
+ it 'creates the repository' do
+ expect { request }.to change { project.repository.raw_repository.exists? }.from(false).to(true)
+
+ expect(response).to be_redirect
+ end
+ end
+
+ context 'when repository already exists' do
+ it 'does not raise an exception' do
+ expect(Gitlab::ErrorTracking).not_to receive(:track_exception)
+ request
+
+ expect(response).to be_redirect
+ end
+ end
+ end
describe "GET archive" do
before do
diff --git a/spec/controllers/projects/runners_controller_spec.rb b/spec/controllers/projects/runners_controller_spec.rb
index 70ff77d7ff0..246a37129d7 100644
--- a/spec/controllers/projects/runners_controller_spec.rb
+++ b/spec/controllers/projects/runners_controller_spec.rb
@@ -37,6 +37,10 @@ RSpec.describe Projects::RunnersController do
describe '#destroy' do
it 'destroys the runner' do
+ expect_next_instance_of(Ci::UnregisterRunnerService, runner) do |service|
+ expect(service).to receive(:execute).once.and_call_original
+ end
+
delete :destroy, params: params
expect(response).to have_gitlab_http_status(:found)
diff --git a/spec/controllers/projects/service_ping_controller_spec.rb b/spec/controllers/projects/service_ping_controller_spec.rb
index e6afaadc75f..13b34290962 100644
--- a/spec/controllers/projects/service_ping_controller_spec.rb
+++ b/spec/controllers/projects/service_ping_controller_spec.rb
@@ -32,7 +32,7 @@ RSpec.describe Projects::ServicePingController do
shared_examples 'counter is increased' do |counter|
context 'when the authenticated user has access to the project' do
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
it 'increments the usage counter' do
expect do
@@ -55,6 +55,33 @@ RSpec.describe Projects::ServicePingController do
end
context 'when web ide clientside preview is not enabled' do
+ let(:user) { project.first_owner }
+
+ before do
+ stub_application_setting(web_ide_clientside_preview_enabled: false)
+ end
+
+ it 'returns 404' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ describe 'POST #web_ide_clientside_preview_success' do
+ subject { post :web_ide_clientside_preview_success, params: { namespace_id: project.namespace, project_id: project } }
+
+ context 'when web ide clientside preview is enabled' do
+ before do
+ stub_application_setting(web_ide_clientside_preview_enabled: true)
+ end
+
+ it_behaves_like 'counter is not increased'
+ it_behaves_like 'counter is increased', 'WEB_IDE_PREVIEWS_SUCCESS_COUNT'
+ end
+
+ context 'when web ide clientside preview is not enabled' do
let(:user) { project.owner }
before do
diff --git a/spec/controllers/projects/settings/repository_controller_spec.rb b/spec/controllers/projects/settings/repository_controller_spec.rb
index 2bb93990c58..22287fea82c 100644
--- a/spec/controllers/projects/settings/repository_controller_spec.rb
+++ b/spec/controllers/projects/settings/repository_controller_spec.rb
@@ -33,6 +33,20 @@ RSpec.describe Projects::Settings::RepositoryController do
expect(response).to redirect_to project_settings_repository_path(project)
end
+
+ context 'when project cleanup returns an error', :aggregate_failures do
+ it 'shows an error' do
+ expect(Projects::CleanupService)
+ .to receive(:enqueue)
+ .with(project, user, anything)
+ .and_return(status: :error, message: 'error message')
+
+ put :cleanup, params: { namespace_id: project.namespace, project_id: project, project: { bfg_object_map: object_map } }
+
+ expect(controller).to set_flash[:alert].to('error message')
+ expect(response).to redirect_to project_settings_repository_path(project)
+ end
+ end
end
describe 'POST create_deploy_token' do
diff --git a/spec/controllers/projects/tags_controller_spec.rb b/spec/controllers/projects/tags_controller_spec.rb
index 9823c36cb86..f955f9d0248 100644
--- a/spec/controllers/projects/tags_controller_spec.rb
+++ b/spec/controllers/projects/tags_controller_spec.rb
@@ -17,6 +17,14 @@ RSpec.describe Projects::TagsController do
expect(assigns(:tags).map(&:name)).to include('v1.1.0', 'v1.0.0')
end
+ context 'default sort for tags' do
+ it 'sorts tags by recently updated' do
+ subject
+
+ expect(assigns(:sort)).to eq('updated_desc')
+ end
+ end
+
context 'when Gitaly is unavailable' do
where(:format) do
[:html, :atom]
@@ -31,6 +39,7 @@ RSpec.describe Projects::TagsController do
get :index, params: { namespace_id: project.namespace.to_param, project_id: project }, format: format
expect(assigns(:tags)).to eq([])
+ expect(assigns(:releases)).to eq([])
expect(response).to have_gitlab_http_status(:service_unavailable)
end
end
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index 7ebd86640ad..08d1d88fcda 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -1202,6 +1202,26 @@ RSpec.describe ProjectsController do
end
end
end
+
+ context 'when input params are invalid' do
+ let(:request) { get :refs, params: { namespace_id: project.namespace, id: project, ref: { invalid: :format } } }
+
+ it 'does not break' do
+ request
+
+ expect(response).to have_gitlab_http_status(:success)
+ end
+
+ context 'when "strong_parameters_for_project_controller" FF is disabled' do
+ before do
+ stub_feature_flags(strong_parameters_for_project_controller: false)
+ end
+
+ it 'raises an exception' do
+ expect { request }.to raise_error(TypeError)
+ end
+ end
+ end
end
describe 'POST #preview_markdown' do
diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb
index d5fe32ac094..af34ae2f69b 100644
--- a/spec/controllers/registrations_controller_spec.rb
+++ b/spec/controllers/registrations_controller_spec.rb
@@ -456,6 +456,28 @@ RSpec.describe RegistrationsController do
subject
end
+
+ describe 'logged_out_marketing_header experiment', :experiment do
+ before do
+ stub_experiments(logged_out_marketing_header: :candidate)
+ end
+
+ it 'tracks signed_up event' do
+ expect(experiment(:logged_out_marketing_header)).to track(:signed_up).on_next_instance
+
+ subject
+ end
+
+ context 'when registration fails' do
+ let_it_be(:user_params) { { user: base_user_params.merge({ username: '' }) } }
+
+ it 'does not track signed_up event' do
+ expect(experiment(:logged_out_marketing_header)).not_to track(:signed_up)
+
+ subject
+ end
+ end
+ end
end
describe '#destroy' do
diff --git a/spec/controllers/repositories/git_http_controller_spec.rb b/spec/controllers/repositories/git_http_controller_spec.rb
index 4a6e745cd63..fb2637238ec 100644
--- a/spec/controllers/repositories/git_http_controller_spec.rb
+++ b/spec/controllers/repositories/git_http_controller_spec.rb
@@ -29,7 +29,7 @@ RSpec.describe Repositories::GitHttpController do
context 'when repository container is a project' do
it_behaves_like Repositories::GitHttpController do
let(:container) { project }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:access_checker_class) { Gitlab::GitAccess }
it_behaves_like 'handles unavailable Gitaly'
@@ -103,7 +103,7 @@ RSpec.describe Repositories::GitHttpController do
context 'when repository container is a project wiki' do
it_behaves_like Repositories::GitHttpController do
let(:container) { create(:project_wiki, :empty_repo, project: project) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:access_checker_class) { Gitlab::GitAccessWiki }
end
end
diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb
index 58d34a5e5c1..0f1501d4c3c 100644
--- a/spec/controllers/search_controller_spec.rb
+++ b/spec/controllers/search_controller_spec.rb
@@ -397,9 +397,10 @@ RSpec.describe SearchController do
expect(payload[:metadata]['meta.search.filters.confidential']).to eq('true')
expect(payload[:metadata]['meta.search.filters.state']).to eq('true')
expect(payload[:metadata]['meta.search.project_ids']).to eq(%w(456 789))
+ expect(payload[:metadata]['meta.search.search_level']).to eq('multi-project')
end
- get :show, params: { scope: 'issues', search: 'hello world', group_id: '123', project_id: '456', project_ids: %w(456 789), confidential: true, state: true, force_search_results: true }
+ get :show, params: { scope: 'issues', search: 'hello world', group_id: '123', project_id: '456', project_ids: %w(456 789), search_level: 'multi-project', confidential: true, state: true, force_search_results: true }
end
it 'appends the default scope in meta.search.scope' do
diff --git a/spec/crystalball_env.rb b/spec/crystalball_env.rb
index d606fe69cdf..cd609dfb3f9 100644
--- a/spec/crystalball_env.rb
+++ b/spec/crystalball_env.rb
@@ -6,7 +6,7 @@ module CrystalballEnv
extend self
def start!
- return unless ENV['CRYSTALBALL']
+ return unless ENV['CRYSTALBALL'] == 'true'
require 'crystalball'
require_relative '../tooling/lib/tooling/crystalball/coverage_lines_execution_detector'
diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb
index 9bd6691bdb2..2608a13a399 100644
--- a/spec/db/schema_spec.rb
+++ b/spec/db/schema_spec.rb
@@ -26,7 +26,7 @@ RSpec.describe 'Database schema' do
boards: %w[milestone_id iteration_id],
chat_names: %w[chat_id team_id user_id],
chat_teams: %w[team_id],
- ci_builds: %w[erased_by_id runner_id trigger_request_id],
+ ci_builds: %w[erased_by_id trigger_request_id],
ci_namespace_monthly_usages: %w[namespace_id],
ci_runner_projects: %w[runner_id],
ci_trigger_requests: %w[commit_id],
diff --git a/spec/events/projects/project_deleted_event_spec.rb b/spec/events/projects/project_deleted_event_spec.rb
new file mode 100644
index 00000000000..fd8cec7271b
--- /dev/null
+++ b/spec/events/projects/project_deleted_event_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::ProjectDeletedEvent do
+ where(:data, :valid) do
+ [
+ [{ project_id: 1, namespace_id: 2 }, true],
+ [{ project_id: 1 }, false],
+ [{ namespace_id: 1 }, false],
+ [{ project_id: 'foo', namespace_id: 2 }, false],
+ [{ project_id: 1, namespace_id: 'foo' }, false],
+ [{ project_id: [], namespace_id: 2 }, false],
+ [{ project_id: 1, namespace_id: [] }, false],
+ [{ project_id: {}, namespace_id: 2 }, false],
+ [{ project_id: 1, namespace_id: {} }, false],
+ ['foo', false],
+ [123, false],
+ [[], false]
+ ]
+ end
+
+ with_them do
+ it 'validates data' do
+ constructor = -> { described_class.new(data: data) }
+
+ if valid
+ expect { constructor.call }.not_to raise_error
+ else
+ expect { constructor.call }.to raise_error(Gitlab::EventStore::InvalidEvent)
+ end
+ end
+ end
+end
diff --git a/spec/experiments/application_experiment_spec.rb b/spec/experiments/application_experiment_spec.rb
index 5146fe3e752..70ee4bd3c5a 100644
--- a/spec/experiments/application_experiment_spec.rb
+++ b/spec/experiments/application_experiment_spec.rb
@@ -24,38 +24,6 @@ RSpec.describe ApplicationExperiment, :experiment do
expect { experiment('namespaced/stub') { } }.not_to raise_error
end
- describe "#enabled?" do
- before do
- allow(application_experiment).to receive(:enabled?).and_call_original
-
- allow(Feature::Definition).to receive(:get).and_return('_instance_')
- allow(Gitlab).to receive(:dev_env_or_com?).and_return(true)
- allow(Feature).to receive(:get).and_return(double(state: :on))
- end
-
- it "is enabled when all criteria are met" do
- expect(application_experiment).to be_enabled
- end
-
- it "isn't enabled if the feature definition doesn't exist" do
- expect(Feature::Definition).to receive(:get).with('namespaced_stub').and_return(nil)
-
- expect(application_experiment).not_to be_enabled
- end
-
- it "isn't enabled if we're not in dev or dotcom environments" do
- expect(Gitlab).to receive(:dev_env_or_com?).and_return(false)
-
- expect(application_experiment).not_to be_enabled
- end
-
- it "isn't enabled if the feature flag state is :off" do
- expect(Feature).to receive(:get).with('namespaced_stub').and_return(double(state: :off))
-
- expect(application_experiment).not_to be_enabled
- end
- end
-
describe "#publish" do
let(:should_track) { true }
@@ -117,7 +85,7 @@ RSpec.describe ApplicationExperiment, :experiment do
describe '#publish_to_database' do
using RSpec::Parameterized::TableSyntax
- let(:publish_to_database) { application_experiment.publish_to_database }
+ let(:publish_to_database) { ActiveSupport::Deprecation.silence { application_experiment.publish_to_database } }
shared_examples 'does not record to the database' do
it 'does not create an experiment record' do
@@ -214,26 +182,6 @@ RSpec.describe ApplicationExperiment, :experiment do
)
end
- it "tracks the event correctly even when using the base class" do
- subject = Gitlab::Experiment.new(:unnamed)
- subject.track(:action, context: [fake_context])
-
- expect_snowplow_event(
- category: 'unnamed',
- action: 'action',
- context: [
- {
- schema: 'iglu:com.gitlab/fake/jsonschema/0-0-0',
- data: { data: '_data_' }
- },
- {
- schema: 'iglu:com.gitlab/gitlab_experiment/jsonschema/1-0-0',
- data: { experiment: 'unnamed', key: subject.context.key, variant: 'control' }
- }
- ]
- )
- end
-
context "when using known context resources" do
let(:user) { build(:user, id: non_existing_record_id) }
let(:project) { build(:project, id: non_existing_record_id) }
@@ -347,23 +295,15 @@ RSpec.describe ApplicationExperiment, :experiment do
end
context "when resolving variants" do
- it "uses the default value as specified in the yaml" do
- expect(Feature).to receive(:enabled?).with('namespaced_stub', application_experiment, type: :experiment, default_enabled: :yaml)
-
- expect(application_experiment.variant.name).to eq('control')
+ before do
+ stub_feature_flags(namespaced_stub: true)
end
- context "when rolled out to 100%" do
- before do
- stub_feature_flags(namespaced_stub: true)
- end
-
- it "returns the first variant name" do
- application_experiment.try(:variant1) {}
- application_experiment.try(:variant2) {}
+ it "returns an assigned name" do
+ application_experiment.variant(:variant1) {}
+ application_experiment.variant(:variant2) {}
- expect(application_experiment.variant.name).to eq('variant1')
- end
+ expect(application_experiment.assigned.name).to eq('variant2')
end
end
@@ -395,8 +335,8 @@ RSpec.describe ApplicationExperiment, :experiment do
cache.clear(key: application_experiment.name)
- application_experiment.use { } # setup the control
- application_experiment.try { } # setup the candidate
+ application_experiment.control { }
+ application_experiment.candidate { }
end
it "caches the variant determined by the variant resolver" do
@@ -451,4 +391,29 @@ RSpec.describe ApplicationExperiment, :experiment do
end
end
end
+
+ context "with deprecation warnings" do
+ before do
+ Gitlab::Experiment::Configuration.instance_variable_set(:@__dep_versions, nil) # clear the internal memoization
+
+ allow(ActiveSupport::Deprecation).to receive(:new).and_call_original
+ end
+
+ it "doesn't warn on non dev/test environments" do
+ allow(Gitlab).to receive(:dev_or_test_env?).and_return(false)
+
+ expect { experiment(:example) { |e| e.use { } } }.not_to raise_error
+ expect(ActiveSupport::Deprecation).not_to have_received(:new).with(anything, 'Gitlab::Experiment')
+ end
+
+ it "warns on dev and test environments" do
+ allow(Gitlab).to receive(:dev_or_test_env?).and_return(true)
+
+ # This will eventually raise an ActiveSupport::Deprecation exception,
+ # it's ok to change it when that happens.
+ expect { experiment(:example) { |e| e.use { } } }.not_to raise_error
+
+ expect(ActiveSupport::Deprecation).to have_received(:new).with(anything, 'Gitlab::Experiment')
+ end
+ end
end
diff --git a/spec/experiments/new_project_readme_content_experiment_spec.rb b/spec/experiments/new_project_readme_content_experiment_spec.rb
deleted file mode 100644
index a6a81580a29..00000000000
--- a/spec/experiments/new_project_readme_content_experiment_spec.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe NewProjectReadmeContentExperiment, :experiment do
- subject { described_class.new(namespace: project.namespace) }
-
- let(:project) { create(:project, name: 'Experimental', description: 'An experiment project') }
-
- it "renders the basic README" do
- expect(subject.run_with(project)).to eq(<<~MARKDOWN.strip)
- # Experimental
-
- An experiment project
- MARKDOWN
- end
-
- describe "the advanced variant" do
- let(:markdown) { subject.run_with(project, variant: :advanced) }
- let(:initial_url) { 'https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file' }
-
- it "renders the project details" do
- expect(markdown).to include(<<~MARKDOWN.strip)
- # Experimental
-
- An experiment project
-
- ## Getting started
- MARKDOWN
- end
-
- it "renders redirect URLs" do
- url = Rails.application.routes.url_helpers.experiment_redirect_url(subject, url: initial_url)
- expect(url).to include("/-/experiment/#{subject.to_param}?")
- expect(markdown).to include(url)
- end
- end
-end
diff --git a/spec/experiments/require_verification_for_namespace_creation_experiment_spec.rb b/spec/experiments/require_verification_for_namespace_creation_experiment_spec.rb
index 87417fe1637..269b6222020 100644
--- a/spec/experiments/require_verification_for_namespace_creation_experiment_spec.rb
+++ b/spec/experiments/require_verification_for_namespace_creation_experiment_spec.rb
@@ -5,7 +5,8 @@ require 'spec_helper'
RSpec.describe RequireVerificationForNamespaceCreationExperiment, :experiment do
subject(:experiment) { described_class.new(user: user) }
- let_it_be(:user) { create(:user) }
+ let(:user_created_at) { RequireVerificationForNamespaceCreationExperiment::EXPERIMENT_START_DATE + 1.hour }
+ let(:user) { create(:user, created_at: user_created_at) }
describe '#candidate?' do
context 'when experiment subject is candidate' do
@@ -56,4 +57,21 @@ RSpec.describe RequireVerificationForNamespaceCreationExperiment, :experiment do
end
end
end
+
+ describe 'exclusions' do
+ context 'when user is new' do
+ it 'is not excluded' do
+ expect(subject).not_to exclude(user: user)
+ end
+ end
+
+ context 'when user is NOT new' do
+ let(:user_created_at) { RequireVerificationForNamespaceCreationExperiment::EXPERIMENT_START_DATE - 1.day }
+ let(:user) { create(:user, created_at: user_created_at) }
+
+ it 'is excluded' do
+ expect(subject).to exclude(user: user)
+ end
+ end
+ end
end
diff --git a/spec/factories/ci/build_metadata.rb b/spec/factories/ci/build_metadata.rb
new file mode 100644
index 00000000000..cfc86c4ef4b
--- /dev/null
+++ b/spec/factories/ci/build_metadata.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :ci_build_metadata, class: 'Ci::BuildMetadata' do
+ build { association(:ci_build, strategy: :build, metadata: instance) }
+ end
+end
diff --git a/spec/factories/ci/runners.rb b/spec/factories/ci/runners.rb
index 6665b7b76a0..18026412261 100644
--- a/spec/factories/ci/runners.rb
+++ b/spec/factories/ci/runners.rb
@@ -13,6 +13,7 @@ FactoryBot.define do
transient do
groups { [] }
projects { [] }
+ token_expires_at { nil }
end
after(:build) do |runner, evaluator|
@@ -25,6 +26,10 @@ FactoryBot.define do
end
end
+ after(:create) do |runner, evaluator|
+ runner.update!(token_expires_at: evaluator.token_expires_at) if evaluator.token_expires_at
+ end
+
trait :online do
contacted_at { Time.now }
end
diff --git a/spec/factories/container_repositories.rb b/spec/factories/container_repositories.rb
index 86bb129067f..ce83e9e8006 100644
--- a/spec/factories/container_repositories.rb
+++ b/spec/factories/container_repositories.rb
@@ -33,6 +33,52 @@ FactoryBot.define do
expiration_policy_cleanup_status { :cleanup_ongoing }
end
+ trait :default do
+ migration_state { 'default' }
+ end
+
+ trait :pre_importing do
+ migration_state { 'pre_importing' }
+ migration_pre_import_started_at { Time.zone.now }
+ end
+
+ trait :pre_import_done do
+ migration_state { 'pre_import_done' }
+ migration_pre_import_started_at { Time.zone.now }
+ migration_pre_import_done_at { Time.zone.now }
+ end
+
+ trait :importing do
+ migration_state { 'importing' }
+ migration_pre_import_started_at { Time.zone.now }
+ migration_pre_import_done_at { Time.zone.now }
+ migration_import_started_at { Time.zone.now }
+ end
+
+ trait :import_done do
+ migration_state { 'import_done' }
+ migration_pre_import_started_at { Time.zone.now }
+ migration_pre_import_done_at { Time.zone.now }
+ migration_import_started_at { Time.zone.now }
+ migration_import_done_at { Time.zone.now }
+ end
+
+ trait :import_aborted do
+ migration_state { 'import_aborted' }
+ migration_pre_import_started_at { Time.zone.now }
+ migration_pre_import_done_at { Time.zone.now }
+ migration_import_started_at { Time.zone.now }
+ migration_aborted_at { Time.zone.now }
+ migration_aborted_in_state { 'importing' }
+ migration_retries_count { 1 }
+ end
+
+ trait :import_skipped do
+ migration_state { 'import_skipped' }
+ migration_skipped_at { Time.zone.now }
+ migration_skipped_reason { :too_many_tags }
+ end
+
after(:build) do |repository, evaluator|
next if evaluator.tags.to_a.none?
diff --git a/spec/factories/gitlab/database/background_migration/batched_jobs.rb b/spec/factories/gitlab/database/background_migration/batched_jobs.rb
index cec20616f7f..3c7dcd03701 100644
--- a/spec/factories/gitlab/database/background_migration/batched_jobs.rb
+++ b/spec/factories/gitlab/database/background_migration/batched_jobs.rb
@@ -9,5 +9,21 @@ FactoryBot.define do
batch_size { 5 }
sub_batch_size { 1 }
pause_ms { 100 }
+
+ trait(:pending) do
+ status { 0 }
+ end
+
+ trait(:running) do
+ status { 1 }
+ end
+
+ trait(:failed) do
+ status { 2 }
+ end
+
+ trait(:succeeded) do
+ status { 3 }
+ end
end
end
diff --git a/spec/factories/go_module_commits.rb b/spec/factories/go_module_commits.rb
index 514a5559344..4f86d38954c 100644
--- a/spec/factories/go_module_commits.rb
+++ b/spec/factories/go_module_commits.rb
@@ -17,7 +17,7 @@ FactoryBot.define do
service do
Files::MultiService.new(
project,
- project.owner,
+ project.first_owner,
commit_message: message,
start_branch: project.repository.root_ref || 'master',
branch_name: project.repository.root_ref || 'master',
@@ -38,7 +38,7 @@ FactoryBot.define do
commit = project.repository.commit_by(oid: r[:result])
if tag
- r = Tags::CreateService.new(project, project.owner).execute(tag, commit.sha, tag_message)
+ r = Tags::CreateService.new(project, project.first_owner).execute(tag, commit.sha, tag_message)
raise "operation failed: #{r}" unless r[:status] == :success
end
diff --git a/spec/factories/group_members.rb b/spec/factories/group_members.rb
index ab2321c81c4..4b1bf9a7d11 100644
--- a/spec/factories/group_members.rb
+++ b/spec/factories/group_members.rb
@@ -35,6 +35,18 @@ FactoryBot.define do
access_level { GroupMember::MINIMAL_ACCESS }
end
+ trait :awaiting do
+ after(:create) do |member|
+ member.update!(state: ::Member::STATE_AWAITING)
+ end
+ end
+
+ trait :active do
+ after(:create) do |member|
+ member.update!(state: ::Member::STATE_ACTIVE)
+ end
+ end
+
transient do
tasks_to_be_done { [] }
end
diff --git a/spec/factories/issues.rb b/spec/factories/issues.rb
index 8b53732a3c1..26c858665a8 100644
--- a/spec/factories/issues.rb
+++ b/spec/factories/issues.rb
@@ -61,6 +61,15 @@ FactoryBot.define do
factory :incident do
issue_type { :incident }
association :work_item_type, :default, :incident
+
+ # An escalation status record is created for all incidents
+ # in app code. This is a trait to avoid creating escalation
+ # status records in specs which do not need them.
+ trait :with_escalation_status do
+ after(:create) do |incident|
+ create(:incident_management_issuable_escalation_status, issue: incident)
+ end
+ end
end
end
end
diff --git a/spec/factories/keys.rb b/spec/factories/keys.rb
index cf52e772ae0..2af1c6cc62d 100644
--- a/spec/factories/keys.rb
+++ b/spec/factories/keys.rb
@@ -1,14 +1,12 @@
# frozen_string_literal: true
-require_relative '../support/helpers/key_generator_helper'
-
FactoryBot.define do
factory :key do
title
- key { Spec::Support::Helpers::KeyGeneratorHelper.new(1024).generate + ' dummy@gitlab.com' }
+ key { SSHData::PrivateKey::RSA.generate(1024, unsafe_allow_small_key: true).public_key.openssh(comment: 'dummy@gitlab.com') }
factory :key_without_comment do
- key { Spec::Support::Helpers::KeyGeneratorHelper.new(1024).generate }
+ key { SSHData::PrivateKey::RSA.generate(1024, unsafe_allow_small_key: true).public_key.openssh }
end
factory :deploy_key, class: 'DeployKey'
@@ -148,5 +146,24 @@ FactoryBot.define do
KEY
end
end
+
+ factory :ecdsa_sk_key_256 do
+ key do
+ <<~KEY.delete("\n")
+ sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyN
+ TZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBDZ+f5tSRhlB7EN39f93SscTN5PUv
+ bD3UQsNrlE1ZdbwPMMRul2zlPiUvwAvnJitW0jlD/vwZOW2YN+q+iZ5c0MAAAAEc3NoOg== dummy@gitlab.com
+ KEY
+ end
+ end
+
+ factory :ed25519_sk_key_256 do
+ key do
+ <<~KEY.delete("\n")
+ sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tA
+ AAAIEX/dQ0v4127bEo8eeG1EV0ApO2lWbSnN6RWusn/NjqIAAAABHNzaDo= dummy@gitlab.com
+ KEY
+ end
+ end
end
end
diff --git a/spec/factories/labels.rb b/spec/factories/labels.rb
index f0cef41db69..250c92c0038 100644
--- a/spec/factories/labels.rb
+++ b/spec/factories/labels.rb
@@ -18,13 +18,6 @@ FactoryBot.define do
title { "#{prefix}::#{generate(:label_title)}" }
end
- trait :incident do
- properties = IncidentManagement::CreateIncidentLabelService::LABEL_PROPERTIES
- title { properties.fetch(:title) }
- description { properties.fetch(:description) }
- color { properties.fetch(:color) }
- end
-
factory :label, traits: [:base_label], class: 'ProjectLabel' do
project
diff --git a/spec/factories/namespace_statistics.rb b/spec/factories/namespace_statistics.rb
new file mode 100644
index 00000000000..49e2c8957c5
--- /dev/null
+++ b/spec/factories/namespace_statistics.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :namespace_statistics do
+ namespace factory: :namespace
+ end
+end
diff --git a/spec/factories/project_members.rb b/spec/factories/project_members.rb
index f2dedc178c7..c38257b06b6 100644
--- a/spec/factories/project_members.rb
+++ b/spec/factories/project_members.rb
@@ -24,6 +24,18 @@ FactoryBot.define do
after(:build) { |project_member, _| project_member.user.block! }
end
+ trait :awaiting do
+ after(:create) do |member|
+ member.update!(state: ::Member::STATE_AWAITING)
+ end
+ end
+
+ trait :active do
+ after(:create) do |member|
+ member.update!(state: ::Member::STATE_ACTIVE)
+ end
+ end
+
transient do
tasks_to_be_done { [] }
end
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index c345fa0c8b4..8a406f95f58 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -82,7 +82,7 @@ FactoryBot.define do
# user have access to the project. Our specs don't use said service class,
# thus we must manually refresh things here.
unless project.group || project.pending_delete
- project.add_maintainer(project.owner)
+ project.add_maintainer(project.first_owner)
end
project.group&.refresh_members_authorized_projects
diff --git a/spec/factories/usage_data.rb b/spec/factories/usage_data.rb
index f00d1f8b808..86799af1719 100644
--- a/spec/factories/usage_data.rb
+++ b/spec/factories/usage_data.rb
@@ -58,14 +58,6 @@ FactoryBot.define do
# Tracing
create(:project_tracing_setting, project: projects[0])
- # Incident Labeled Issues
- incident_label = create(:label, :incident, project: projects[0])
- create(:labeled_issue, project: projects[0], labels: [incident_label])
- incident_label_scoped_to_project = create(:label, :incident, project: projects[1])
- incident_label_scoped_to_group = create(:group_label, :incident, group: group)
- create(:labeled_issue, project: projects[1], labels: [incident_label_scoped_to_project])
- create(:labeled_issue, project: projects[1], labels: [incident_label_scoped_to_group])
-
# Alert Issues
create(:alert_management_alert, issue: issues[0], project: projects[0])
create(:alert_management_alert, issue: alert_bot_issues[0], project: projects[0])
diff --git a/spec/factories/work_items.rb b/spec/factories/work_items.rb
new file mode 100644
index 00000000000..6d9dcac6165
--- /dev/null
+++ b/spec/factories/work_items.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :work_item, traits: [:has_internal_id] do
+ title { generate(:title) }
+ project
+ author { project.creator }
+ updated_by { author }
+ relative_position { RelativePositioning::START_POSITION }
+ issue_type { :issue }
+ association :work_item_type, :default
+ end
+end
diff --git a/spec/features/admin/admin_groups_spec.rb b/spec/features/admin/admin_groups_spec.rb
index 8d4e7a7442c..a0a41061d64 100644
--- a/spec/features/admin/admin_groups_spec.rb
+++ b/spec/features/admin/admin_groups_spec.rb
@@ -6,6 +6,7 @@ RSpec.describe 'Admin Groups' do
include Select2Helper
include Spec::Support::Helpers::Features::MembersHelpers
include Spec::Support::Helpers::Features::InviteMembersModalHelper
+ include Spec::Support::Helpers::ModalHelpers
let(:internal) { Gitlab::VisibilityLevel::INTERNAL }
@@ -250,26 +251,26 @@ RSpec.describe 'Admin Groups' do
end
end
- describe 'admin remove themself from a group', :js, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/222342' do
+ describe 'admin removes themself from a group', :js do
it 'removes admin from the group' do
- stub_feature_flags(bootstrap_confirmation_modals: false)
group.add_user(current_user, Gitlab::Access::DEVELOPER)
visit group_group_members_path(group)
- page.within '[data-qa-selector="members_list"]' do # rubocop:disable QA/SelectorUsage
+ page.within members_table do
expect(page).to have_content(current_user.name)
expect(page).to have_content('Developer')
end
- accept_confirm { find(:css, 'li', text: current_user.name).find(:css, 'a.btn-danger').click }
+ find_member_row(current_user).click_button(title: 'Leave')
+
+ accept_gl_confirm(button_text: 'Leave')
+
+ wait_for_all_requests
visit group_group_members_path(group)
- page.within '[data-qa-selector="members_list"]' do # rubocop:disable QA/SelectorUsage
- expect(page).not_to have_content(current_user.name)
- expect(page).not_to have_content('Developer')
- end
+ expect(members_table).not_to have_content(current_user.name)
end
end
diff --git a/spec/features/admin/admin_mode/logout_spec.rb b/spec/features/admin/admin_mode/logout_spec.rb
index 58bea5c4b5f..f2f6e26fbee 100644
--- a/spec/features/admin/admin_mode/logout_spec.rb
+++ b/spec/features/admin/admin_mode/logout_spec.rb
@@ -32,7 +32,7 @@ RSpec.describe 'Admin Mode Logout', :js do
it 'disable shows flash notice' do
gitlab_disable_admin_mode
- expect(page).to have_selector('.flash-notice')
+ expect(page).to have_selector('[data-testid="alert-info"]')
end
context 'on a read-only instance' do
diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb
index ceb91b86876..25ff4022454 100644
--- a/spec/features/admin/admin_runners_spec.rb
+++ b/spec/features/admin/admin_runners_spec.rb
@@ -254,6 +254,18 @@ RSpec.describe "Admin Runners" do
expect(page).not_to have_content 'runner-group'
end
+ it 'show the same counts after selecting another tab' do
+ visit admin_runners_path
+
+ page.within('[data-testid="runner-type-tabs"]') do
+ click_on('Project')
+
+ expect(page).to have_link('All 2')
+ expect(page).to have_link('Group 1')
+ expect(page).to have_link('Project 1')
+ end
+ end
+
it 'shows no runner when type does not match' do
visit admin_runners_path
@@ -460,7 +472,7 @@ RSpec.describe "Admin Runners" do
click_on 'Reset registration token'
within_modal do
- click_button('OK', match: :first)
+ click_button('Reset token', match: :first)
end
wait_for_requests
@@ -476,6 +488,42 @@ RSpec.describe "Admin Runners" do
end
end
+ describe "Runner show page", :js do
+ let(:runner) do
+ create(
+ :ci_runner,
+ description: 'runner-foo',
+ version: '14.0',
+ ip_address: '127.0.0.1',
+ tag_list: ['tag1']
+ )
+ end
+
+ before do
+ visit admin_runner_path(runner)
+ end
+
+ describe 'runner show page breadcrumbs' do
+ it 'contains the current runner id and token' do
+ page.within '[data-testid="breadcrumb-links"]' do
+ expect(page.find('h2')).to have_link("##{runner.id} (#{runner.short_sha})")
+ end
+ end
+ end
+
+ it 'shows runner details' do
+ aggregate_failures do
+ expect(page).to have_content 'Description runner-foo'
+ expect(page).to have_content 'Last contact Never contacted'
+ expect(page).to have_content 'Version 14.0'
+ expect(page).to have_content 'IP Address 127.0.0.1'
+ expect(page).to have_content 'Configuration Runs untagged jobs'
+ expect(page).to have_content 'Maximum job timeout None'
+ expect(page).to have_content 'Tags tag1'
+ end
+ end
+ end
+
describe "Runner edit page" do
let(:runner) { create(:ci_runner) }
@@ -487,7 +535,7 @@ RSpec.describe "Admin Runners" do
wait_for_requests
end
- describe 'runner page breadcrumbs' do
+ describe 'runner edit page breadcrumbs' do
it 'contains the current runner id and token' do
page.within '[data-testid="breadcrumb-links"]' do
expect(page).to have_link("##{runner.id} (#{runner.short_sha})")
diff --git a/spec/features/admin/admin_sees_background_migrations_spec.rb b/spec/features/admin/admin_sees_background_migrations_spec.rb
index 94fb3a0314f..a3d0c7bdd4d 100644
--- a/spec/features/admin/admin_sees_background_migrations_spec.rb
+++ b/spec/features/admin/admin_sees_background_migrations_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe "Admin > Admin sees background migrations" do
let_it_be(:finished_migration) { create(:batched_background_migration, table_name: 'finished', status: :finished) }
before_all do
- create(:batched_background_migration_job, batched_migration: failed_migration, batch_size: 10, min_value: 6, max_value: 15, status: :failed, attempts: 3)
+ create(:batched_background_migration_job, :failed, batched_migration: failed_migration, batch_size: 10, min_value: 6, max_value: 15, attempts: 3)
end
before do
@@ -68,7 +68,7 @@ RSpec.describe "Admin > Admin sees background migrations" do
tab.click
expect(page).to have_current_path(admin_background_migrations_path(tab: 'failed'))
- expect(tab[:class]).to include('gl-tab-nav-item-active', 'gl-tab-nav-item-active-indigo')
+ expect(tab[:class]).to include('gl-tab-nav-item-active')
expect(page).to have_selector('tbody tr', count: 1)
@@ -93,7 +93,7 @@ RSpec.describe "Admin > Admin sees background migrations" do
tab.click
expect(page).to have_current_path(admin_background_migrations_path(tab: 'finished'))
- expect(tab[:class]).to include('gl-tab-nav-item-active', 'gl-tab-nav-item-active-indigo')
+ expect(tab[:class]).to include('gl-tab-nav-item-active')
expect(page).to have_selector('tbody tr', count: 1)
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index e136ab41966..ca452264c02 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -85,6 +85,8 @@ RSpec.describe 'Admin updates settings' do
select 'Are allowed', from: 'DSA SSH keys'
select 'Must be at least 384 bits', from: 'ECDSA SSH keys'
select 'Are forbidden', from: 'ED25519 SSH keys'
+ select 'Are forbidden', from: 'ECDSA_SK SSH keys'
+ select 'Are forbidden', from: 'ED25519_SK SSH keys'
click_on 'Save changes'
end
@@ -95,6 +97,8 @@ RSpec.describe 'Admin updates settings' do
expect(find_field('DSA SSH keys').value).to eq('0')
expect(find_field('ECDSA SSH keys').value).to eq('384')
expect(find_field('ED25519 SSH keys').value).to eq(forbidden)
+ expect(find_field('ECDSA_SK SSH keys').value).to eq(forbidden)
+ expect(find_field('ED25519_SK SSH keys').value).to eq(forbidden)
end
it 'change Account and Limit Settings' do
@@ -528,7 +532,7 @@ RSpec.describe 'Admin updates settings' do
expect(find_field('Allow access to members of the following group').value).to be_nil
end
- it 'loads usage ping payload on click', :js do
+ it 'loads togglable usage ping payload on click', :js do
stub_usage_data_connections
stub_database_flavor_check
@@ -544,6 +548,10 @@ RSpec.describe 'Admin updates settings' do
expect(page).to have_selector '.js-service-ping-payload'
expect(page).to have_button 'Hide payload'
expect(page).to have_content expected_payload_content
+
+ click_button('Hide payload')
+
+ expect(page).not_to have_content expected_payload_content
end
end
end
@@ -623,6 +631,20 @@ RSpec.describe 'Admin updates settings' do
expect(current_settings.issues_create_limit).to eq(0)
end
+ it 'changes Users API rate limits settings' do
+ visit network_admin_application_settings_path
+
+ page.within('.as-users-api-limits') do
+ fill_in 'Maximum requests per 10 minutes per user', with: 0
+ fill_in 'Users to exclude from the rate limit', with: 'someone, someone_else'
+ click_button 'Save changes'
+ end
+
+ expect(page).to have_content "Application settings saved successfully"
+ expect(current_settings.users_get_by_id_limit).to eq(0)
+ expect(current_settings.users_get_by_id_limit_allowlist).to eq(%w[someone someone_else])
+ end
+
shared_examples 'regular throttle rate limit settings' do
it 'changes rate limit settings' do
visit network_admin_application_settings_path
@@ -771,6 +793,38 @@ RSpec.describe 'Admin updates settings' do
end
end
end
+
+ context 'Service usage data page' do
+ before do
+ stub_usage_data_connections
+ stub_database_flavor_check
+
+ visit service_usage_data_admin_application_settings_path
+ end
+
+ it 'loads usage ping payload on click', :js do
+ expected_payload_content = /(?=.*"uuid")(?=.*"hostname")/m
+
+ expect(page).not_to have_content expected_payload_content
+
+ click_button('Preview payload')
+
+ wait_for_requests
+
+ expect(page).to have_button 'Hide payload'
+ expect(page).to have_content expected_payload_content
+ end
+
+ it 'generates usage ping payload on button click', :js do
+ expect_next_instance_of(Admin::ApplicationSettingsController) do |instance|
+ expect(instance).to receive(:usage_data).and_call_original
+ end
+
+ click_button('Download payload')
+
+ wait_for_requests
+ end
+ end
end
context 'application setting :admin_mode is disabled' do
diff --git a/spec/features/admin/integrations/instance_integrations_spec.rb b/spec/features/admin/integrations/instance_integrations_spec.rb
new file mode 100644
index 00000000000..7b326ec161c
--- /dev/null
+++ b/spec/features/admin/integrations/instance_integrations_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Instance integrations', :js do
+ include_context 'instance integration activation'
+
+ it_behaves_like 'integration settings form' do
+ let(:integrations) { Integration.find_or_initialize_all_non_project_specific(Integration.for_instance) }
+
+ def navigate_to_integration(integration)
+ visit_instance_integration(integration.title)
+ end
+ end
+end
diff --git a/spec/features/admin/integrations/user_activates_mattermost_slash_command_spec.rb b/spec/features/admin/integrations/user_activates_mattermost_slash_command_spec.rb
index 793a5bced00..22a27b33671 100644
--- a/spec/features/admin/integrations/user_activates_mattermost_slash_command_spec.rb
+++ b/spec/features/admin/integrations/user_activates_mattermost_slash_command_spec.rb
@@ -19,19 +19,4 @@ RSpec.describe 'User activates the instance-level Mattermost Slash Command integ
expect(page).to have_link('Settings', href: edit_path)
expect(page).to have_link('Projects using custom settings', href: overrides_path)
end
-
- it 'does not render integration form element' do
- expect(page).not_to have_selector('[data-testid="integration-form"]')
- end
-
- context 'when `vue_integration_form` feature flag is disabled' do
- before do
- stub_feature_flags(vue_integration_form: false)
- visit_instance_integration('Mattermost slash commands')
- end
-
- it 'renders integration form element' do
- expect(page).to have_selector('[data-testid="integration-form"]')
- end
- end
end
diff --git a/spec/features/admin/users/users_spec.rb b/spec/features/admin/users/users_spec.rb
index 473f51370b3..5b0b6e085c9 100644
--- a/spec/features/admin/users/users_spec.rb
+++ b/spec/features/admin/users/users_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe 'Admin::Users' do
include Spec::Support::Helpers::Features::AdminUsersHelpers
+ include Spec::Support::Helpers::ModalHelpers
let_it_be(:user, reload: true) { create(:omniauth_user, provider: 'twitter', extern_uid: '123456') }
let_it_be(:current_user) { create(:admin) }
@@ -294,6 +295,22 @@ RSpec.describe 'Admin::Users' do
end
end
+ context 'when a user is locked', time_travel_to: '2020-02-25 10:30:45 -0700' do
+ let_it_be(:locked_user) { create(:user, locked_at: DateTime.parse('2020-02-25 10:30:00 -0700')) }
+
+ it "displays `Locked` badge next to user" do
+ expect(page).to have_content("#{locked_user.name} Locked")
+ end
+
+ it 'allows a user to be unlocked from the `User administration dropdown', :js do
+ accept_gl_confirm("Unlock user #{locked_user.name}?", button_text: 'Unlock') do
+ click_action_in_user_dropdown(locked_user.id, 'Unlock')
+ end
+
+ expect(page).not_to have_content("#{locked_user.name} (Locked)")
+ end
+ end
+
describe 'internal users' do
context 'when showing a `Ghost User`' do
let_it_be(:ghost_user) { create(:user, :ghost) }
diff --git a/spec/features/boards/board_filters_spec.rb b/spec/features/boards/board_filters_spec.rb
index 49375e4b37b..e37bf515088 100644
--- a/spec/features/boards/board_filters_spec.rb
+++ b/spec/features/boards/board_filters_spec.rb
@@ -7,15 +7,15 @@ RSpec.describe 'Issue board filters', :js do
let_it_be(:user) { create(:user) }
let_it_be(:board) { create(:board, project: project) }
let_it_be(:project_label) { create(:label, project: project, title: 'Label') }
- let_it_be(:milestone_1) { create(:milestone, project: project) }
- let_it_be(:milestone_2) { create(:milestone, project: project) }
+ let_it_be(:milestone_1) { create(:milestone, project: project, due_date: 3.days.from_now ) }
+ let_it_be(:milestone_2) { create(:milestone, project: project, due_date: Date.tomorrow ) }
let_it_be(:release) { create(:release, tag: 'v1.0', project: project, milestones: [milestone_1]) }
let_it_be(:release_2) { create(:release, tag: 'v2.0', project: project, milestones: [milestone_2]) }
let_it_be(:issue_1) { create(:issue, project: project, milestone: milestone_1, author: user) }
let_it_be(:issue_2) { create(:labeled_issue, project: project, milestone: milestone_2, assignees: [user], labels: [project_label], confidential: true) }
let_it_be(:award_emoji1) { create(:award_emoji, name: 'thumbsup', user: user, awardable: issue_1) }
- let(:filtered_search) { find('[data-testid="issue_1-board-filtered-search"]') }
+ let(:filtered_search) { find('[data-testid="issue-board-filtered-search"]') }
let(:filter_input) { find('.gl-filtered-search-term-input')}
let(:filter_dropdown) { find('.gl-filtered-search-suggestion-list') }
let(:filter_first_suggestion) { find('.gl-filtered-search-suggestion-list').first('.gl-filtered-search-suggestion') }
@@ -134,8 +134,11 @@ RSpec.describe 'Issue board filters', :js do
expect(filter_dropdown).to have_content('Any')
expect(filter_dropdown).to have_content('Started')
expect(filter_dropdown).to have_content('Upcoming')
- expect(filter_dropdown).to have_content(milestone_1.title)
- expect(filter_dropdown).to have_content(milestone_2.title)
+
+ dropdown_nodes = page.find_all('.gl-filtered-search-suggestion-list > .gl-filtered-search-suggestion')
+
+ expect(dropdown_nodes[4]).to have_content(milestone_2.title)
+ expect(dropdown_nodes.last).to have_content(milestone_1.title)
click_on milestone_1.title
filter_submit.click
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index d25cddea902..2ca4ff94911 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -583,7 +583,11 @@ RSpec.describe 'Project issue boards', :js do
end
page.within(find('.js-board-settings-sidebar')) do
- accept_confirm { find('[data-testid="remove-list"]').click }
+ click_button 'Remove list'
+ end
+
+ page.within('.modal') do
+ click_button 'Remove list'
end
end
diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb
index f88d31bda88..5f4517d47ee 100644
--- a/spec/features/boards/new_issue_spec.rb
+++ b/spec/features/boards/new_issue_spec.rb
@@ -3,12 +3,13 @@
require 'spec_helper'
RSpec.describe 'Issue Boards new issue', :js do
- let_it_be(:project) { create(:project, :public) }
- let_it_be(:board) { create(:board, project: project) }
- let_it_be(:backlog_list) { create(:backlog_list, board: board) }
- let_it_be(:label) { create(:label, project: project, name: 'Label 1') }
- let_it_be(:list) { create(:list, board: board, label: label, position: 0) }
- let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:board) { create(:board, project: project) }
+ let_it_be(:backlog_list) { create(:backlog_list, board: board) }
+ let_it_be(:label) { create(:label, project: project, name: 'Label 1') }
+ let_it_be(:list) { create(:list, board: board, label: label, position: 0) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:existing_issue) { create(:issue, project: project, title: 'other issue', relative_position: 50) }
let(:board_list_header) { first('[data-testid="board-list-header"]') }
let(:project_select_dropdown) { find('[data-testid="project-select-dropdown"]') }
@@ -56,7 +57,7 @@ RSpec.describe 'Issue Boards new issue', :js do
end
end
- it 'creates new issue and opens sidebar' do
+ it 'creates new issue, places it on top of the list, and opens sidebar' do
page.within(first('.board')) do
click_button 'New issue'
end
@@ -69,12 +70,14 @@ RSpec.describe 'Issue Boards new issue', :js do
wait_for_requests
page.within(first('.board [data-testid="issue-count-badge"]')) do
- expect(page).to have_content('1')
+ expect(page).to have_content('2')
end
page.within(first('.board-card')) do
issue = project.issues.find_by_title('bug')
+ expect(issue.relative_position).to be < existing_issue.relative_position
+
expect(page).to have_content(issue.to_reference)
expect(page).to have_link(issue.title, href: /#{issue_path(issue)}/)
end
diff --git a/spec/features/breadcrumbs_schema_markup_spec.rb b/spec/features/breadcrumbs_schema_markup_spec.rb
index a87a3d284de..f86ad5cd2ae 100644
--- a/spec/features/breadcrumbs_schema_markup_spec.rb
+++ b/spec/features/breadcrumbs_schema_markup_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe 'Breadcrumbs schema markup', :aggregate_failures do
expect(item_list.size).to eq 2
expect(item_list[0]['name']).to eq project.namespace.name
- expect(item_list[0]['item']).to eq user_url(project.owner)
+ expect(item_list[0]['item']).to eq user_url(project.first_owner)
expect(item_list[1]['name']).to eq project.name
expect(item_list[1]['item']).to eq project_url(project)
@@ -59,7 +59,7 @@ RSpec.describe 'Breadcrumbs schema markup', :aggregate_failures do
expect(item_list.size).to eq 3
expect(item_list[0]['name']).to eq project.namespace.name
- expect(item_list[0]['item']).to eq user_url(project.owner)
+ expect(item_list[0]['item']).to eq user_url(project.first_owner)
expect(item_list[1]['name']).to eq project.name
expect(item_list[1]['item']).to eq project_url(project)
@@ -75,7 +75,7 @@ RSpec.describe 'Breadcrumbs schema markup', :aggregate_failures do
expect(item_list.size).to eq 4
expect(item_list[0]['name']).to eq project.namespace.name
- expect(item_list[0]['item']).to eq user_url(project.owner)
+ expect(item_list[0]['item']).to eq user_url(project.first_owner)
expect(item_list[1]['name']).to eq project.name
expect(item_list[1]['item']).to eq project_url(project)
diff --git a/spec/features/clusters/create_agent_spec.rb b/spec/features/clusters/create_agent_spec.rb
index 7ed31a8c549..e03126d344e 100644
--- a/spec/features/clusters/create_agent_spec.rb
+++ b/spec/features/clusters/create_agent_spec.rb
@@ -15,6 +15,7 @@ RSpec.describe 'Cluster agent registration', :js do
double(agent_name: 'example-agent-1', path: '.gitlab/agents/example-agent-1/config.yaml'),
double(agent_name: 'example-agent-2', path: '.gitlab/agents/example-agent-2/config.yaml')
])
+ allow(client).to receive(:get_connected_agents).and_return([])
end
allow(Devise).to receive(:friendly_token).and_return('example-agent-token')
diff --git a/spec/features/contextual_sidebar_spec.rb b/spec/features/contextual_sidebar_spec.rb
index 29c7e0ddd21..cc4a0471d4e 100644
--- a/spec/features/contextual_sidebar_spec.rb
+++ b/spec/features/contextual_sidebar_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'Contextual sidebar', :js do
context 'when context is a project' do
let_it_be(:project) { create(:project) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
before do
sign_in(user)
diff --git a/spec/features/cycle_analytics_spec.rb b/spec/features/cycle_analytics_spec.rb
index 69361f66a71..03d61020ff0 100644
--- a/spec/features/cycle_analytics_spec.rb
+++ b/spec/features/cycle_analytics_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe 'Value Stream Analytics', :js do
let_it_be(:stage_table_event_title_selector) { '[data-testid="vsa-stage-event-title"]' }
let_it_be(:stage_table_pagination_selector) { '[data-testid="vsa-stage-pagination"]' }
let_it_be(:stage_table_duration_column_header_selector) { '[data-testid="vsa-stage-header-duration"]' }
- let_it_be(:metrics_selector) { "[data-testid='vsa-time-metrics']" }
+ let_it_be(:metrics_selector) { "[data-testid='vsa-metrics']" }
let_it_be(:metric_value_selector) { "[data-testid='displayValue']" }
let(:stage_table) { find(stage_table_selector) }
@@ -134,7 +134,7 @@ RSpec.describe 'Value Stream Analytics', :js do
end
it 'can filter the metrics by date' do
- expect(metrics_values).to match_array(["21.0", "2.0", "1.0", "0.0"])
+ expect(metrics_values).to match_array(%w[21 2 1 0])
set_daterange(from, to)
diff --git a/spec/features/dashboard/groups_list_spec.rb b/spec/features/dashboard/groups_list_spec.rb
index 8c941b27cd2..3a4296836bd 100644
--- a/spec/features/dashboard/groups_list_spec.rb
+++ b/spec/features/dashboard/groups_list_spec.rb
@@ -15,6 +15,10 @@ RSpec.describe 'Dashboard Groups page', :js do
wait_for_requests
end
+ def click_options_menu(group)
+ page.find("[data-testid='group-#{group.id}-dropdown-button'").click
+ end
+
it 'shows groups user is member of' do
group.add_owner(user)
nested_group.add_owner(user)
@@ -112,6 +116,67 @@ RSpec.describe 'Dashboard Groups page', :js do
end
end
+ context 'group actions dropdown' do
+ let!(:subgroup) { create(:group, :public, parent: group) }
+
+ context 'user with subgroup ownership' do
+ before do
+ subgroup.add_owner(user)
+ sign_in(user)
+
+ visit dashboard_groups_path
+ end
+
+ it 'cannot remove parent group' do
+ expect(page).not_to have_selector("[data-testid='group-#{group.id}-dropdown-button'")
+ end
+ end
+
+ context 'user with parent group ownership' do
+ before do
+ group.add_owner(user)
+ sign_in(user)
+
+ visit dashboard_groups_path
+ end
+
+ it 'can remove parent group' do
+ click_options_menu(group)
+
+ expect(page).to have_selector("[data-testid='remove-group-#{group.id}-btn']")
+ end
+
+ it 'can remove subgroups' do
+ click_group_caret(group)
+ click_options_menu(subgroup)
+
+ expect(page).to have_selector("[data-testid='remove-group-#{subgroup.id}-btn']")
+ end
+ end
+
+ context 'user is a maintainer' do
+ before do
+ group.add_maintainer(user)
+ sign_in(user)
+
+ visit dashboard_groups_path
+ click_options_menu(group)
+ end
+
+ it 'cannot remove the group' do
+ expect(page).not_to have_selector("[data-testid='remove-group-#{group.id}-btn']")
+ end
+
+ it 'cannot edit the group' do
+ expect(page).not_to have_selector("[data-testid='edit-group-#{group.id}-btn']")
+ end
+
+ it 'can leave the group' do
+ expect(page).to have_selector("[data-testid='leave-group-#{group.id}-btn']")
+ end
+ end
+ end
+
context 'when using pagination' do
let(:group) { create(:group, created_at: 5.days.ago) }
let(:group2) { create(:group, created_at: 2.days.ago) }
diff --git a/spec/features/dashboard/issuables_counter_spec.rb b/spec/features/dashboard/issuables_counter_spec.rb
index 8e938fef155..6700ec07765 100644
--- a/spec/features/dashboard/issuables_counter_spec.rb
+++ b/spec/features/dashboard/issuables_counter_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe 'Navigation bar counter', :use_clean_rails_memory_store_caching d
it 'reflects dashboard issues count' do
visit issues_path
- expect_counters('issues', '1')
+ expect_counters('issues', '1', n_("%d assigned issue", "%d assigned issues", 1) % 1)
issue.assignees = []
@@ -26,14 +26,14 @@ RSpec.describe 'Navigation bar counter', :use_clean_rails_memory_store_caching d
travel_to(3.minutes.from_now) do
visit issues_path
- expect_counters('issues', '0')
+ expect_counters('issues', '0', n_("%d assigned issue", "%d assigned issues", 0) % 0)
end
end
it 'reflects dashboard merge requests count' do
visit merge_requests_path
- expect_counters('merge_requests', '1')
+ expect_counters('merge_requests', '1', n_("%d merge request", "%d merge requests", 1) % 1)
merge_request.update!(assignees: [])
@@ -42,7 +42,7 @@ RSpec.describe 'Navigation bar counter', :use_clean_rails_memory_store_caching d
travel_to(3.minutes.from_now) do
visit merge_requests_path
- expect_counters('merge_requests', '0')
+ expect_counters('merge_requests', '0', n_("%d merge request", "%d merge requests", 0) % 0)
end
end
@@ -54,13 +54,14 @@ RSpec.describe 'Navigation bar counter', :use_clean_rails_memory_store_caching d
merge_requests_dashboard_path(assignee_username: user.username)
end
- def expect_counters(issuable_type, count)
+ def expect_counters(issuable_type, count, badge_label)
dashboard_count = find('.gl-tabs-nav li a.active')
nav_count = find(".dashboard-shortcuts-#{issuable_type}")
- header_count = find(".header-content .#{issuable_type.tr('_', '-')}-count")
expect(dashboard_count).to have_content(count)
expect(nav_count).to have_content(count)
- expect(header_count).to have_content(count)
+ within("span[aria-label='#{badge_label}']") do
+ expect(page).to have_content(count)
+ end
end
end
diff --git a/spec/features/dashboard/merge_requests_spec.rb b/spec/features/dashboard/merge_requests_spec.rb
index 6239702edde..7507ef4e453 100644
--- a/spec/features/dashboard/merge_requests_spec.rb
+++ b/spec/features/dashboard/merge_requests_spec.rb
@@ -112,7 +112,9 @@ RSpec.describe 'Dashboard Merge Requests' do
end
it 'includes assigned and reviewers in badge' do
- expect(find('.merge-requests-count')).to have_content('3')
+ within("span[aria-label='#{n_("%d merge request", "%d merge requests", 3) % 3}']") do
+ expect(page).to have_content('3')
+ end
expect(find('.js-assigned-mr-count')).to have_content('2')
expect(find('.js-reviewer-mr-count')).to have_content('1')
end
diff --git a/spec/features/dashboard/snippets_spec.rb b/spec/features/dashboard/snippets_spec.rb
index 224f2111014..f891950eeb8 100644
--- a/spec/features/dashboard/snippets_spec.rb
+++ b/spec/features/dashboard/snippets_spec.rb
@@ -7,11 +7,11 @@ RSpec.describe 'Dashboard snippets' do
context 'when the project has snippets' do
let(:project) { create(:project, :public, creator: user) }
- let!(:snippets) { create_list(:project_snippet, 2, :public, author: project.owner, project: project) }
+ let!(:snippets) { create_list(:project_snippet, 2, :public, author: project.first_owner, project: project) }
before do
allow(Snippet).to receive(:default_per_page).and_return(1)
- sign_in(project.owner)
+ sign_in(project.first_owner)
visit dashboard_snippets_path
end
@@ -27,7 +27,7 @@ RSpec.describe 'Dashboard snippets' do
let(:project) { create(:project, :public, creator: user) }
before do
- sign_in(project.owner)
+ sign_in(project.first_owner)
visit dashboard_snippets_path
end
diff --git a/spec/features/error_tracking/user_filters_errors_by_status_spec.rb b/spec/features/error_tracking/user_filters_errors_by_status_spec.rb
index 6846d8f6ade..d5dbe259159 100644
--- a/spec/features/error_tracking/user_filters_errors_by_status_spec.rb
+++ b/spec/features/error_tracking/user_filters_errors_by_status_spec.rb
@@ -22,7 +22,7 @@ RSpec.describe 'When a user filters Sentry errors by status', :js, :use_clean_ra
end
it 'displays the results' do
- sign_in(project.owner)
+ sign_in(project.first_owner)
visit project_error_tracking_index_path(project)
page.within(find('.gl-table')) do
results = page.all('.table-row')
diff --git a/spec/features/error_tracking/user_searches_sentry_errors_spec.rb b/spec/features/error_tracking/user_searches_sentry_errors_spec.rb
index c16c9d3fb1f..89bf79ebb81 100644
--- a/spec/features/error_tracking/user_searches_sentry_errors_spec.rb
+++ b/spec/features/error_tracking/user_searches_sentry_errors_spec.rb
@@ -22,7 +22,7 @@ RSpec.describe 'When a user searches for Sentry errors', :js, :use_clean_rails_m
end
it 'displays the results' do
- sign_in(project.owner)
+ sign_in(project.first_owner)
visit project_error_tracking_index_path(project)
page.within(find('.gl-table')) do
diff --git a/spec/features/error_tracking/user_sees_error_details_spec.rb b/spec/features/error_tracking/user_sees_error_details_spec.rb
index e4a09d04ca1..ecbb3fe0412 100644
--- a/spec/features/error_tracking/user_sees_error_details_spec.rb
+++ b/spec/features/error_tracking/user_sees_error_details_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe 'View error details page', :js, :use_clean_rails_memory_store_cac
context 'with current user as project owner' do
before do
- sign_in(project.owner)
+ sign_in(project.first_owner)
visit details_project_error_tracking_index_path(project, issue_id: issue_id)
end
diff --git a/spec/features/error_tracking/user_sees_error_index_spec.rb b/spec/features/error_tracking/user_sees_error_index_spec.rb
index bc6709c659d..21f9e688e3f 100644
--- a/spec/features/error_tracking/user_sees_error_index_spec.rb
+++ b/spec/features/error_tracking/user_sees_error_index_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe 'View error index page', :js, :use_clean_rails_memory_store_cachi
context 'with current user as project owner' do
before do
- sign_in(project.owner)
+ sign_in(project.first_owner)
visit project_error_tracking_index_path(project)
end
@@ -43,7 +43,7 @@ RSpec.describe 'View error index page', :js, :use_clean_rails_memory_store_cachi
context 'with error tracking settings disabled' do
before do
project_error_tracking_settings.update!(enabled: false)
- sign_in(project.owner)
+ sign_in(project.first_owner)
visit project_error_tracking_index_path(project)
end
diff --git a/spec/features/file_uploads/attachment_spec.rb b/spec/features/file_uploads/attachment_spec.rb
index 9ad404ce869..41da0e9fbe0 100644
--- a/spec/features/file_uploads/attachment_spec.rb
+++ b/spec/features/file_uploads/attachment_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'Upload an attachment', :api, :js do
include_context 'file upload requests helpers'
let_it_be(:project) { create(:project) }
- let_it_be(:user) { project.owner }
+ let_it_be(:user) { project.first_owner }
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
let(:api_path) { "/projects/#{project_id}/uploads" }
diff --git a/spec/features/file_uploads/git_lfs_spec.rb b/spec/features/file_uploads/git_lfs_spec.rb
index 239afb1a1bb..8d15c5c33f7 100644
--- a/spec/features/file_uploads/git_lfs_spec.rb
+++ b/spec/features/file_uploads/git_lfs_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'Upload a git lfs object', :js do
include_context 'file upload requests helpers'
let_it_be(:project) { create(:project) }
- let_it_be(:user) { project.owner }
+ let_it_be(:user) { project.first_owner }
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
let(:file) { fixture_file_upload('spec/fixtures/banana_sample.gif') }
diff --git a/spec/features/file_uploads/maven_package_spec.rb b/spec/features/file_uploads/maven_package_spec.rb
index ab9f023bd8f..70302142fa2 100644
--- a/spec/features/file_uploads/maven_package_spec.rb
+++ b/spec/features/file_uploads/maven_package_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'Upload a maven package', :api, :js do
include_context 'file upload requests helpers'
let_it_be(:project) { create(:project) }
- let_it_be(:user) { project.owner }
+ let_it_be(:user) { project.first_owner }
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
let(:project_id) { project.id }
diff --git a/spec/features/file_uploads/nuget_package_spec.rb b/spec/features/file_uploads/nuget_package_spec.rb
index 871c0274445..cbffd34d4ab 100644
--- a/spec/features/file_uploads/nuget_package_spec.rb
+++ b/spec/features/file_uploads/nuget_package_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'Upload a nuget package', :api, :js do
include_context 'file upload requests helpers'
let_it_be(:project) { create(:project) }
- let_it_be(:user) { project.owner }
+ let_it_be(:user) { project.first_owner }
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
let(:api_path) { "/projects/#{project.id}/packages/nuget/" }
diff --git a/spec/features/file_uploads/rubygem_package_spec.rb b/spec/features/file_uploads/rubygem_package_spec.rb
index 4a5891fdfed..f91fb407b28 100644
--- a/spec/features/file_uploads/rubygem_package_spec.rb
+++ b/spec/features/file_uploads/rubygem_package_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'Upload a RubyGems package', :api, :js do
include_context 'file upload requests helpers'
let_it_be(:project) { create(:project) }
- let_it_be(:user) { project.owner }
+ let_it_be(:user) { project.first_owner }
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
let(:api_path) { "/projects/#{project_id}/packages/rubygems/api/v1/gems" }
diff --git a/spec/features/gitlab_experiments_spec.rb b/spec/features/gitlab_experiments_spec.rb
index ca772680ff6..af14b6e2e95 100644
--- a/spec/features/gitlab_experiments_spec.rb
+++ b/spec/features/gitlab_experiments_spec.rb
@@ -21,8 +21,8 @@ RSpec.describe "Gitlab::Experiment", :js do
allow_next_instance_of(Admin::AbuseReportsController) do |instance|
allow(instance).to receive(:index).and_wrap_original do |original|
instance.experiment(:null_hypothesis, user: instance.current_user) do |e|
- e.use { original.call }
- e.try { original.call }
+ e.control { original.call }
+ e.candidate { original.call }
end
end
end
diff --git a/spec/features/groups/group_settings_spec.rb b/spec/features/groups/group_settings_spec.rb
index 161a8a7a203..30a81333547 100644
--- a/spec/features/groups/group_settings_spec.rb
+++ b/spec/features/groups/group_settings_spec.rb
@@ -138,6 +138,51 @@ RSpec.describe 'Edit group settings' do
end
end
+ describe 'transfer group', :js do
+ let(:namespace_select) { page.find('[data-testid="transfer-group-namespace-select"]') }
+ let(:confirm_modal) { page.find('[data-testid="confirm-danger-modal"]') }
+
+ shared_examples 'can transfer the group' do
+ before do
+ selected_group.add_owner(user)
+ end
+
+ it 'can successfully transfer the group' do
+ visit edit_group_path(selected_group)
+
+ page.within('.js-group-transfer-form') do
+ namespace_select.find('button').click
+ namespace_select.find('.dropdown-menu p', text: target_group_name, match: :first).click
+
+ click_button "Transfer group"
+ end
+
+ page.within(confirm_modal) do
+ expect(page).to have_text "You are going to transfer #{selected_group.name} to another namespace. Are you ABSOLUTELY sure? "
+
+ fill_in "confirm_name_input", with: selected_group.name
+ click_button "Confirm"
+ end
+
+ expect(page).to have_text "Group '#{selected_group.name}' was successfully transferred."
+ end
+ end
+
+ context 'with a sub group' do
+ let(:selected_group) { create(:group, path: 'foo-subgroup', parent: group) }
+ let(:target_group_name) { "No parent group" }
+
+ it_behaves_like 'can transfer the group'
+ end
+
+ context 'with a root group' do
+ let(:selected_group) { create(:group, path: 'foo-rootgroup') }
+ let(:target_group_name) { group.name }
+
+ it_behaves_like 'can transfer the group'
+ end
+ end
+
context 'disable email notifications' do
it 'is visible' do
visit edit_group_path(group)
diff --git a/spec/features/groups/integrations/group_integrations_spec.rb b/spec/features/groups/integrations/group_integrations_spec.rb
new file mode 100644
index 00000000000..0d65fa5964b
--- /dev/null
+++ b/spec/features/groups/integrations/group_integrations_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Group integrations', :js do
+ include_context 'group integration activation'
+
+ it_behaves_like 'integration settings form' do
+ let(:integrations) { Integration.find_or_initialize_all_non_project_specific(Integration.for_group(group)) }
+
+ def navigate_to_integration(integration)
+ visit_group_integration(integration.title)
+ end
+ end
+end
diff --git a/spec/features/groups/members/leave_group_spec.rb b/spec/features/groups/members/leave_group_spec.rb
index e6bf1ffc2f7..9612c6625f6 100644
--- a/spec/features/groups/members/leave_group_spec.rb
+++ b/spec/features/groups/members/leave_group_spec.rb
@@ -79,7 +79,7 @@ RSpec.describe 'Groups > Members > Leave group' do
visit group_path(group, leave: 1)
- expect(find('.flash-alert')).to have_content 'You do not have permission to leave this group'
+ expect(find('[data-testid="alert-danger"]')).to have_content 'You do not have permission to leave this group'
end
def left_group_message(group)
diff --git a/spec/features/groups/members/manage_groups_spec.rb b/spec/features/groups/members/manage_groups_spec.rb
index 2beecda23b5..61c6709f9cc 100644
--- a/spec/features/groups/members/manage_groups_spec.rb
+++ b/spec/features/groups/members/manage_groups_spec.rb
@@ -156,6 +156,26 @@ RSpec.describe 'Groups > Members > Manage groups', :js do
group_outside_hierarchy.add_owner(user)
end
+ context 'when the invite members group modal is enabled' do
+ it 'does not show self or ancestors', :aggregate_failures do
+ group_sibbling = create(:group, parent: group)
+ group_sibbling.add_owner(user)
+
+ visit group_group_members_path(group_within_hierarchy)
+
+ click_on 'Invite a group'
+ click_on 'Select a group'
+ wait_for_requests
+
+ page.within('[data-testid="group-select-dropdown"]') do
+ expect(page).to have_selector("[entity-id='#{group_outside_hierarchy.id}']")
+ expect(page).to have_selector("[entity-id='#{group_sibbling.id}']")
+ expect(page).not_to have_selector("[entity-id='#{group.id}']")
+ expect(page).not_to have_selector("[entity-id='#{group_within_hierarchy.id}']")
+ end
+ end
+ end
+
context 'when sharing with groups outside the hierarchy is enabled' do
context 'when the invite members group modal is disabled' do
before do
diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb
index 19f60ce55d3..925bbc47cf6 100644
--- a/spec/features/groups_spec.rb
+++ b/spec/features/groups_spec.rb
@@ -52,7 +52,7 @@ RSpec.describe 'Group' do
click_button 'Create group'
expect(current_path).to eq(new_group_path)
- expect(page).to have_text('Please choose a group URL with no special characters or spaces.')
+ expect(page).to have_text('Choose a group path that does not start with a dash or end with a period. It can also contain alphanumeric characters and underscores.')
end
end
@@ -90,7 +90,7 @@ RSpec.describe 'Group' do
fill_in 'group_path', with: user.username
wait_for_requests
- expect(page).to have_content("Group path is already taken. We've suggested one that is available.")
+ expect(page).to have_content("Group path is unavailable. Path has been replaced with a suggested available path.")
end
it 'does not break after an invalid form submit' do
@@ -279,7 +279,7 @@ RSpec.describe 'Group' do
fill_in 'Group URL', with: subgroup.path
wait_for_requests
- expect(page).to have_content("Group path is already taken. We've suggested one that is available.")
+ expect(page).to have_content("Group path is unavailable. Path has been replaced with a suggested available path.")
end
end
end
diff --git a/spec/features/ide/user_commits_changes_spec.rb b/spec/features/ide/user_commits_changes_spec.rb
index 1b1e71e2862..e1e586a4f18 100644
--- a/spec/features/ide/user_commits_changes_spec.rb
+++ b/spec/features/ide/user_commits_changes_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'IDE user commits changes', :js do
include WebIdeSpecHelpers
let(:project) { create(:project, :public, :repository) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
before do
sign_in(user)
diff --git a/spec/features/ide/user_opens_merge_request_spec.rb b/spec/features/ide/user_opens_merge_request_spec.rb
index 7ae43f35901..72fe6eb6ca8 100644
--- a/spec/features/ide/user_opens_merge_request_spec.rb
+++ b/spec/features/ide/user_opens_merge_request_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'IDE merge request', :js do
let(:merge_request) { create(:merge_request, :simple, source_project: project) }
let(:project) { create(:project, :public, :repository) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
before do
sign_in(user)
diff --git a/spec/features/issuables/markdown_references/internal_references_spec.rb b/spec/features/issuables/markdown_references/internal_references_spec.rb
index 2dcabb38b8f..ab7c0ce2891 100644
--- a/spec/features/issuables/markdown_references/internal_references_spec.rb
+++ b/spec/features/issuables/markdown_references/internal_references_spec.rb
@@ -5,11 +5,11 @@ require 'spec_helper'
RSpec.describe "Internal references", :js do
include Spec::Support::Helpers::Features::NotesHelpers
- let(:private_project_user) { private_project.owner }
+ let(:private_project_user) { private_project.first_owner }
let(:private_project) { create(:project, :private, :repository) }
let(:private_project_issue) { create(:issue, project: private_project) }
let(:private_project_merge_request) { create(:merge_request, source_project: private_project) }
- let(:public_project_user) { public_project.owner }
+ let(:public_project_user) { public_project.first_owner }
let(:public_project) { create(:project, :public, :repository) }
let(:public_project_issue) { create(:issue, project: public_project) }
let(:public_project_merge_request) { create(:merge_request, source_project: public_project) }
diff --git a/spec/features/issues/filtered_search/dropdown_base_spec.rb b/spec/features/issues/filtered_search/dropdown_base_spec.rb
index 3a304515cab..b8fb807dd78 100644
--- a/spec/features/issues/filtered_search/dropdown_base_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_base_spec.rb
@@ -24,23 +24,6 @@ RSpec.describe 'Dropdown base', :js do
visit project_issues_path(project)
end
- describe 'behavior' do
- it 'shows loading indicator when opened' do
- slow_requests do
- # We aren't using `input_filtered_search` because we want to see the loading indicator
- filtered_search.set('assignee:=')
-
- expect(page).to have_css("#{js_dropdown_assignee} .filter-dropdown-loading", visible: true)
- end
- end
-
- it 'hides loading indicator when loaded' do
- input_filtered_search('assignee:=', submit: false, extra_space: false)
-
- expect(find(js_dropdown_assignee)).not_to have_css('.filter-dropdown-loading')
- end
- end
-
describe 'caching requests' do
it 'caches requests after the first load' do
input_filtered_search('assignee:=', submit: false, extra_space: false)
diff --git a/spec/features/issues/filtered_search/recent_searches_spec.rb b/spec/features/issues/filtered_search/recent_searches_spec.rb
index 3ddcbf1bd01..3929d3694ff 100644
--- a/spec/features/issues/filtered_search/recent_searches_spec.rb
+++ b/spec/features/issues/filtered_search/recent_searches_spec.rb
@@ -104,7 +104,7 @@ RSpec.describe 'Recent searches', :js do
set_recent_searches(project_1_local_storage_key, 'fail')
visit project_issues_path(project_1)
- expect(find('.flash-alert')).to have_text('An error occurred while parsing recent searches')
+ expect(find('[data-testid="alert-danger"]')).to have_text('An error occurred while parsing recent searches')
end
context 'on tablet/mobile screen' do
diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb
index b0e4729db8b..b4d1b0aeab9 100644
--- a/spec/features/issues/gfm_autocomplete_spec.rb
+++ b/spec/features/issues/gfm_autocomplete_spec.rb
@@ -6,10 +6,8 @@ RSpec.describe 'GFM autocomplete', :js do
let_it_be(:user) { create(:user, name: '💃speciąl someone💃', username: 'someone.special') }
let_it_be(:user2) { create(:user, name: 'Marge Simpson', username: 'msimpson') }
- let_it_be(:group) { create(:group, name: 'Ancestor') }
- let_it_be(:child_group) { create(:group, parent: group, name: 'My group') }
- let_it_be(:project) { create(:project, group: child_group) }
-
+ let_it_be(:group) { create(:group, :crm_enabled) }
+ let_it_be(:project) { create(:project, group: group) }
let_it_be(:issue) { create(:issue, project: project, assignees: [user]) }
let_it_be(:label) { create(:label, project: project, title: 'special+') }
let_it_be(:label_scoped) { create(:label, project: project, title: 'scoped::label') }
@@ -22,675 +20,390 @@ RSpec.describe 'GFM autocomplete', :js do
let_it_be(:label_xss) { create(:label, project: project, title: label_xss_title) }
before_all do
- project.add_maintainer(user)
- project.add_maintainer(user_xss)
- project.add_maintainer(user2)
+ group.add_maintainer(user)
+ group.add_maintainer(user_xss)
+ group.add_maintainer(user2)
end
- describe 'when tribute_autocomplete feature flag is off' do
- describe 'new issue page' do
- before do
- stub_feature_flags(tribute_autocomplete: false)
-
- sign_in(user)
- visit new_project_issue_path(project)
+ describe 'new issue page' do
+ before do
+ sign_in(user)
+ visit new_project_issue_path(project)
- wait_for_requests
- end
+ wait_for_requests
+ end
- it 'allows quick actions' do
- fill_in 'Description', with: '/'
+ it 'allows quick actions' do
+ fill_in 'Description', with: '/'
- expect(find_autocomplete_menu).to be_visible
- end
+ expect(find_autocomplete_menu).to be_visible
end
+ end
- describe 'issue description' do
- let(:issue_to_edit) { create(:issue, project: project) }
-
- before do
- stub_feature_flags(tribute_autocomplete: false)
+ describe 'issue description' do
+ let(:issue_to_edit) { create(:issue, project: project) }
- sign_in(user)
- visit project_issue_path(project, issue_to_edit)
+ before do
+ sign_in(user)
+ visit project_issue_path(project, issue_to_edit)
- wait_for_requests
- end
+ wait_for_requests
+ end
- it 'updates with GFM reference' do
- click_button 'Edit title and description'
+ it 'updates with GFM reference' do
+ click_button 'Edit title and description'
- wait_for_requests
+ wait_for_requests
- fill_in 'Description', with: "@#{user.name[0...3]}"
+ fill_in 'Description', with: "@#{user.name[0...3]}"
- wait_for_requests
+ wait_for_requests
- find_highlighted_autocomplete_item.click
+ find_highlighted_autocomplete_item.click
- click_button 'Save changes'
+ click_button 'Save changes'
- wait_for_requests
+ wait_for_requests
- expect(find('.description')).to have_text(user.to_reference)
- end
+ expect(find('.description')).to have_text(user.to_reference)
+ end
- it 'allows quick actions' do
- click_button 'Edit title and description'
+ it 'allows quick actions' do
+ click_button 'Edit title and description'
- fill_in 'Description', with: '/'
+ fill_in 'Description', with: '/'
- expect(find_autocomplete_menu).to be_visible
- end
+ expect(find_autocomplete_menu).to be_visible
end
+ end
- describe 'issue comment' do
- before do
- stub_feature_flags(tribute_autocomplete: false)
-
- sign_in(user)
- visit project_issue_path(project, issue)
+ describe 'issue comment' do
+ before do
+ sign_in(user)
+ visit project_issue_path(project, issue)
- wait_for_requests
- end
+ wait_for_requests
+ end
- describe 'triggering autocomplete' do
- it 'only opens autocomplete menu when trigger character is after whitespace', :aggregate_failures do
- fill_in 'Comment', with: 'testing@'
- expect(page).not_to have_css('.atwho-view')
+ describe 'triggering autocomplete' do
+ it 'only opens autocomplete menu when trigger character is after whitespace', :aggregate_failures do
+ fill_in 'Comment', with: 'testing@'
+ expect(page).not_to have_css('.atwho-view')
- fill_in 'Comment', with: '@@'
- expect(page).not_to have_css('.atwho-view')
+ fill_in 'Comment', with: '@@'
+ expect(page).not_to have_css('.atwho-view')
- fill_in 'Comment', with: "@#{user.username[0..2]}!"
- expect(page).not_to have_css('.atwho-view')
+ fill_in 'Comment', with: "@#{user.username[0..2]}!"
+ expect(page).not_to have_css('.atwho-view')
- fill_in 'Comment', with: "hello:#{user.username[0..2]}"
- expect(page).not_to have_css('.atwho-view')
+ fill_in 'Comment', with: "hello:#{user.username[0..2]}"
+ expect(page).not_to have_css('.atwho-view')
- fill_in 'Comment', with: '7:'
- expect(page).not_to have_css('.atwho-view')
+ fill_in 'Comment', with: '7:'
+ expect(page).not_to have_css('.atwho-view')
- fill_in 'Comment', with: 'w:'
- expect(page).not_to have_css('.atwho-view')
+ fill_in 'Comment', with: 'w:'
+ expect(page).not_to have_css('.atwho-view')
- fill_in 'Comment', with: 'Ё:'
- expect(page).not_to have_css('.atwho-view')
+ fill_in 'Comment', with: 'Ё:'
+ expect(page).not_to have_css('.atwho-view')
- fill_in 'Comment', with: "test\n\n@"
- expect(find_autocomplete_menu).to be_visible
- end
+ fill_in 'Comment', with: "test\n\n@"
+ expect(find_autocomplete_menu).to be_visible
end
+ end
- context 'xss checks' do
- it 'opens autocomplete menu for Issues when field starts with text with item escaping HTML characters' do
- issue_xss_title = 'This will execute alert<img src=x onerror=alert(2)&lt;img src=x onerror=alert(1)&gt;'
- create(:issue, project: project, title: issue_xss_title)
-
- fill_in 'Comment', with: '#'
-
- wait_for_requests
-
- expect(find_autocomplete_menu).to have_text(issue_xss_title)
- end
-
- it 'opens autocomplete menu for Username when field starts with text with item escaping HTML characters' do
- fill_in 'Comment', with: '@ev'
-
- wait_for_requests
-
- expect(find_highlighted_autocomplete_item).to have_text(user_xss.username)
- end
-
- it 'opens autocomplete menu for Milestone when field starts with text with item escaping HTML characters' do
- milestone_xss_title = 'alert milestone &lt;img src=x onerror="alert(\'Hello xss\');" a'
- create(:milestone, project: project, title: milestone_xss_title)
-
- fill_in 'Comment', with: '%'
-
- wait_for_requests
-
- expect(find_autocomplete_menu).to have_text('alert milestone')
- end
+ context 'xss checks' do
+ it 'opens autocomplete menu for Issues when field starts with text with item escaping HTML characters' do
+ issue_xss_title = 'This will execute alert<img src=x onerror=alert(2)&lt;img src=x onerror=alert(1)&gt;'
+ create(:issue, project: project, title: issue_xss_title)
- it 'opens autocomplete menu for Labels when field starts with text with item escaping HTML characters' do
- fill_in 'Comment', with: '~'
+ fill_in 'Comment', with: '#'
- wait_for_requests
+ wait_for_requests
- expect(find_autocomplete_menu).to have_text('alert label')
- end
+ expect(find_autocomplete_menu).to have_text(issue_xss_title)
end
- describe 'autocomplete highlighting' do
- it 'auto-selects the first item when there is a query, and only for assignees with no query', :aggregate_failures do
- fill_in 'Comment', with: ':'
- wait_for_requests
- expect(find_autocomplete_menu).not_to have_css('.cur')
+ it 'opens autocomplete menu for Username when field starts with text with item escaping HTML characters' do
+ fill_in 'Comment', with: '@ev'
- fill_in 'Comment', with: ':1'
- wait_for_requests
- expect(find_autocomplete_menu).to have_css('.cur:first-of-type')
+ wait_for_requests
- fill_in 'Comment', with: '@'
- wait_for_requests
- expect(find_autocomplete_menu).to have_css('.cur:first-of-type')
- end
+ expect(find_highlighted_autocomplete_item).to have_text(user_xss.username)
end
- describe 'assignees' do
- it 'does not wrap with quotes for assignee values' do
- fill_in 'Comment', with: "@#{user.username}"
-
- find_highlighted_autocomplete_item.click
-
- expect(find_field('Comment').value).to have_text("@#{user.username}")
- end
-
- it 'includes items for assignee dropdowns with non-ASCII characters in name' do
- fill_in 'Comment', with: "@#{user.name[0...8]}"
-
- wait_for_requests
-
- expect(find_autocomplete_menu).to have_text(user.name)
- end
-
- it 'searches across full name for assignees' do
- fill_in 'Comment', with: '@speciąlsome'
-
- wait_for_requests
-
- expect(find_highlighted_autocomplete_item).to have_text(user.name)
- end
-
- it 'shows names that start with the query as the top result' do
- fill_in 'Comment', with: '@mar'
-
- wait_for_requests
-
- expect(find_highlighted_autocomplete_item).to have_text(user2.name)
- end
-
- it 'shows usernames that start with the query as the top result' do
- fill_in 'Comment', with: '@msi'
-
- wait_for_requests
-
- expect(find_highlighted_autocomplete_item).to have_text(user2.name)
- end
-
- # Regression test for https://gitlab.com/gitlab-org/gitlab/-/issues/321925
- it 'shows username when pasting then pressing Enter' do
- fill_in 'Comment', with: "@#{user.username}\n"
-
- expect(find_field('Comment').value).to have_text "@#{user.username}"
- end
-
- it 'does not show `@undefined` when pressing `@` then Enter' do
- fill_in 'Comment', with: "@\n"
-
- expect(find_field('Comment').value).to have_text '@'
- expect(find_field('Comment').value).not_to have_text '@undefined'
- end
+ it 'opens autocomplete menu for Milestone when field starts with text with item escaping HTML characters' do
+ milestone_xss_title = 'alert milestone &lt;img src=x onerror="alert(\'Hello xss\');" a'
+ create(:milestone, project: project, title: milestone_xss_title)
- context 'when /assign quick action is selected' do
- it 'triggers user autocomplete and lists users who are currently not assigned to the issue' do
- fill_in 'Comment', with: '/as'
+ fill_in 'Comment', with: '%'
- find_highlighted_autocomplete_item.click
+ wait_for_requests
- expect(find_autocomplete_menu).not_to have_text(user.username)
- expect(find_autocomplete_menu).to have_text(user2.username)
- end
- end
+ expect(find_autocomplete_menu).to have_text('alert milestone')
end
- context 'if a selected value has special characters' do
- it 'wraps the result in double quotes' do
- fill_in 'Comment', with: "~#{label.title[0..2]}"
-
- find_highlighted_autocomplete_item.click
-
- expect(find_field('Comment').value).to have_text("~\"#{label.title}\"")
- end
-
- it 'doesn\'t wrap for emoji values' do
- fill_in 'Comment', with: ':cartwheel_'
-
- find_highlighted_autocomplete_item.click
-
- expect(find_field('Comment').value).to have_text('cartwheel_tone1')
- end
- end
+ it 'opens autocomplete menu for Labels when field starts with text with item escaping HTML characters' do
+ fill_in 'Comment', with: '~'
- context 'quick actions' do
- it 'does not limit quick actions autocomplete list to 5' do
- fill_in 'Comment', with: '/'
+ wait_for_requests
- expect(find_autocomplete_menu).to have_css('li', minimum: 6)
- end
+ expect(find_autocomplete_menu).to have_text('alert label')
end
+ end
- context 'labels' do
- it 'allows colons when autocompleting scoped labels' do
- fill_in 'Comment', with: '~scoped:'
-
- wait_for_requests
-
- expect(find_autocomplete_menu).to have_text('scoped::label')
- end
-
- it 'allows spaces when autocompleting multi-word labels' do
- fill_in 'Comment', with: '~Accepting merge'
-
- wait_for_requests
-
- expect(find_autocomplete_menu).to have_text('Accepting merge requests')
- end
-
- it 'only autocompletes the last label' do
- fill_in 'Comment', with: '~scoped:: foo bar ~Accepting merge'
+ describe 'autocomplete highlighting' do
+ it 'auto-selects the first item when there is a query, and only for assignees with no query', :aggregate_failures do
+ fill_in 'Comment', with: ':'
+ wait_for_requests
+ expect(find_autocomplete_menu).not_to have_css('.cur')
- wait_for_requests
+ fill_in 'Comment', with: ':1'
+ wait_for_requests
+ expect(find_autocomplete_menu).to have_css('.cur:first-of-type')
- expect(find_autocomplete_menu).to have_text('Accepting merge requests')
- end
+ fill_in 'Comment', with: '@'
+ wait_for_requests
+ expect(find_autocomplete_menu).to have_css('.cur:first-of-type')
+ end
+ end
- it 'does not autocomplete labels if no tilde is typed' do
- fill_in 'Comment', with: 'Accepting merge'
+ describe 'assignees' do
+ it 'does not wrap with quotes for assignee values' do
+ fill_in 'Comment', with: "@#{user.username}"
- wait_for_requests
+ find_highlighted_autocomplete_item.click
- expect(page).not_to have_css('.atwho-view')
- end
+ expect(find_field('Comment').value).to have_text("@#{user.username}")
end
- context 'when other notes are destroyed' do
- let!(:discussion) { create(:discussion_note_on_issue, noteable: issue, project: issue.project) }
+ it 'includes items for assignee dropdowns with non-ASCII characters in name' do
+ fill_in 'Comment', with: "@#{user.name[0...8]}"
- # This is meant to protect against this issue https://gitlab.com/gitlab-org/gitlab/-/issues/228729
- it 'keeps autocomplete key listeners' do
- note = find_field('Comment')
+ wait_for_requests
- start_comment_with_emoji(note, '.atwho-view li')
+ expect(find_autocomplete_menu).to have_text(user.name)
+ end
- start_and_cancel_discussion
+ it 'searches across full name for assignees' do
+ fill_in 'Comment', with: '@speciąlsome'
- note.fill_in(with: '')
- start_comment_with_emoji(note, '.atwho-view li')
- note.native.send_keys(:enter)
+ wait_for_requests
- expect(note.value).to eql('Hello :100: ')
- end
+ expect(find_highlighted_autocomplete_item).to have_text(user.name)
end
- shared_examples 'autocomplete suggestions' do
- it 'suggests objects correctly' do
- fill_in 'Comment', with: object.class.reference_prefix
+ it 'shows names that start with the query as the top result' do
+ fill_in 'Comment', with: '@mar'
- find_autocomplete_menu.find('li').click
+ wait_for_requests
- expect(find_field('Comment').value).to have_text(expected_body)
- end
+ expect(find_highlighted_autocomplete_item).to have_text(user2.name)
end
- context 'issues' do
- let(:object) { issue }
- let(:expected_body) { object.to_reference }
+ it 'shows usernames that start with the query as the top result' do
+ fill_in 'Comment', with: '@msi'
- it_behaves_like 'autocomplete suggestions'
- end
-
- context 'merge requests' do
- let(:object) { create(:merge_request, source_project: project) }
- let(:expected_body) { object.to_reference }
+ wait_for_requests
- it_behaves_like 'autocomplete suggestions'
+ expect(find_highlighted_autocomplete_item).to have_text(user2.name)
end
- context 'project snippets' do
- let!(:object) { snippet }
- let(:expected_body) { object.to_reference }
+ # Regression test for https://gitlab.com/gitlab-org/gitlab/-/issues/321925
+ it 'shows username when pasting then pressing Enter' do
+ fill_in 'Comment', with: "@#{user.username}\n"
- it_behaves_like 'autocomplete suggestions'
+ expect(find_field('Comment').value).to have_text "@#{user.username}"
end
- context 'milestone' do
- let_it_be(:milestone_expired) { create(:milestone, project: project, due_date: 5.days.ago) }
- let_it_be(:milestone_no_duedate) { create(:milestone, project: project, title: 'Foo - No due date') }
- let_it_be(:milestone1) { create(:milestone, project: project, title: 'Milestone-1', due_date: 20.days.from_now) }
- let_it_be(:milestone2) { create(:milestone, project: project, title: 'Milestone-2', due_date: 15.days.from_now) }
- let_it_be(:milestone3) { create(:milestone, project: project, title: 'Milestone-3', due_date: 10.days.from_now) }
-
- before do
- fill_in 'Comment', with: '/milestone %'
+ it 'does not show `@undefined` when pressing `@` then Enter' do
+ fill_in 'Comment', with: "@\n"
- wait_for_requests
- end
+ expect(find_field('Comment').value).to have_text '@'
+ expect(find_field('Comment').value).not_to have_text '@undefined'
+ end
- it 'shows milestons list in the autocomplete menu' do
- page.within(find_autocomplete_menu) do
- expect(page).to have_selector('li', count: 5)
- end
- end
+ context 'when /assign quick action is selected' do
+ it 'triggers user autocomplete and lists users who are currently not assigned to the issue' do
+ fill_in 'Comment', with: '/as'
- it 'shows expired milestone at the bottom of the list' do
- page.within(find_autocomplete_menu) do
- expect(page.find('li:last-child')).to have_content milestone_expired.title
- end
- end
+ find_highlighted_autocomplete_item.click
- it 'shows milestone due earliest at the top of the list' do
- page.within(find_autocomplete_menu) do
- aggregate_failures do
- expect(page.all('li')[0]).to have_content milestone3.title
- expect(page.all('li')[1]).to have_content milestone2.title
- expect(page.all('li')[2]).to have_content milestone1.title
- expect(page.all('li')[3]).to have_content milestone_no_duedate.title
- end
- end
+ expect(find_autocomplete_menu).not_to have_text(user.username)
+ expect(find_autocomplete_menu).to have_text(user2.username)
end
end
end
- end
- describe 'when tribute_autocomplete feature flag is on' do
- describe 'issue description' do
- let(:issue_to_edit) { create(:issue, project: project) }
+ context 'if a selected value has special characters' do
+ it 'wraps the result in double quotes' do
+ fill_in 'Comment', with: "~#{label.title[0..2]}"
- before do
- stub_feature_flags(tribute_autocomplete: true)
-
- sign_in(user)
- visit project_issue_path(project, issue_to_edit)
+ find_highlighted_autocomplete_item.click
- wait_for_requests
+ expect(find_field('Comment').value).to have_text("~\"#{label.title}\"")
end
- it 'updates with GFM reference' do
- click_button 'Edit title and description'
-
- wait_for_requests
-
- fill_in 'Description', with: "@#{user.name[0...3]}"
+ it 'doesn\'t wrap for emoji values' do
+ fill_in 'Comment', with: ':cartwheel_'
- wait_for_requests
-
- find_highlighted_tribute_autocomplete_menu.click
-
- click_button 'Save changes'
-
- wait_for_requests
+ find_highlighted_autocomplete_item.click
- expect(find('.description')).to have_text(user.to_reference)
+ expect(find_field('Comment').value).to have_text('cartwheel_tone1')
end
end
- describe 'issue comment' do
- before do
- stub_feature_flags(tribute_autocomplete: true)
-
- sign_in(user)
- visit project_issue_path(project, issue)
+ context 'quick actions' do
+ it 'does not limit quick actions autocomplete list to 5' do
+ fill_in 'Comment', with: '/'
- wait_for_requests
+ expect(find_autocomplete_menu).to have_css('li', minimum: 6)
end
+ end
- describe 'triggering autocomplete' do
- it 'only opens autocomplete menu when trigger character is after whitespace', :aggregate_failures do
- fill_in 'Comment', with: 'testing@'
- expect(page).not_to have_css('.tribute-container')
-
- fill_in 'Comment', with: "hello:#{user.username[0..2]}"
- expect(page).not_to have_css('.tribute-container')
-
- fill_in 'Comment', with: '7:'
- expect(page).not_to have_css('.tribute-container')
-
- fill_in 'Comment', with: 'w:'
- expect(page).not_to have_css('.tribute-container')
+ context 'labels' do
+ it 'allows colons when autocompleting scoped labels' do
+ fill_in 'Comment', with: '~scoped:'
- fill_in 'Comment', with: 'Ё:'
- expect(page).not_to have_css('.tribute-container')
+ wait_for_requests
- fill_in 'Comment', with: "test\n\n@"
- expect(find_tribute_autocomplete_menu).to be_visible
- end
+ expect(find_autocomplete_menu).to have_text('scoped::label')
end
- context 'xss checks' do
- it 'opens autocomplete menu for Issues when field starts with text with item escaping HTML characters' do
- issue_xss_title = 'This will execute alert<img src=x onerror=alert(2)&lt;img src=x onerror=alert(1)&gt;'
- create(:issue, project: project, title: issue_xss_title)
-
- fill_in 'Comment', with: '#'
-
- wait_for_requests
-
- expect(find_tribute_autocomplete_menu).to have_text(issue_xss_title)
- end
-
- it 'opens autocomplete menu for Username when field starts with text with item escaping HTML characters' do
- fill_in 'Comment', with: '@ev'
-
- wait_for_requests
+ it 'allows spaces when autocompleting multi-word labels' do
+ fill_in 'Comment', with: '~Accepting merge'
- expect(find_tribute_autocomplete_menu).to have_text(user_xss.username)
- end
-
- it 'opens autocomplete menu for Milestone when field starts with text with item escaping HTML characters' do
- milestone_xss_title = 'alert milestone &lt;img src=x onerror="alert(\'Hello xss\');" a'
- create(:milestone, project: project, title: milestone_xss_title)
-
- fill_in 'Comment', with: '%'
-
- wait_for_requests
-
- expect(find_tribute_autocomplete_menu).to have_text('alert milestone')
- end
-
- it 'opens autocomplete menu for Labels when field starts with text with item escaping HTML characters' do
- fill_in 'Comment', with: '~'
-
- wait_for_requests
-
- expect(find_tribute_autocomplete_menu).to have_text('alert label')
- end
- end
-
- describe 'autocomplete highlighting' do
- it 'auto-selects the first item with query', :aggregate_failures do
- fill_in 'Comment', with: ':1'
- wait_for_requests
- expect(find_tribute_autocomplete_menu).to have_css('.highlight:first-of-type')
+ wait_for_requests
- fill_in 'Comment', with: '@'
- wait_for_requests
- expect(find_tribute_autocomplete_menu).to have_css('.highlight:first-of-type')
- end
+ expect(find_autocomplete_menu).to have_text('Accepting merge requests')
end
- describe 'assignees' do
- it 'does not wrap with quotes for assignee values' do
- fill_in 'Comment', with: "@#{user.username[0..2]}"
+ it 'only autocompletes the last label' do
+ fill_in 'Comment', with: '~scoped:: foo bar ~Accepting merge'
- find_highlighted_tribute_autocomplete_menu.click
-
- expect(find_field('Comment').value).to have_text("@#{user.username}")
- end
-
- it 'includes items for assignee dropdowns with non-ASCII characters in name' do
- fill_in 'Comment', with: "@#{user.name[0...8]}"
-
- wait_for_requests
-
- expect(find_tribute_autocomplete_menu).to have_text(user.name)
- end
-
- context 'when autocompleting for groups' do
- it 'shows the group when searching for the name of the group' do
- fill_in 'Comment', with: '@mygroup'
+ wait_for_requests
- expect(find_tribute_autocomplete_menu).to have_text('My group')
- end
+ expect(find_autocomplete_menu).to have_text('Accepting merge requests')
+ end
- it 'does not show the group when searching for the name of the parent of the group' do
- fill_in 'Comment', with: '@ancestor'
+ it 'does not autocomplete labels if no tilde is typed' do
+ fill_in 'Comment', with: 'Accepting merge'
- expect(find_tribute_autocomplete_menu).not_to have_text('My group')
- end
- end
-
- context 'when /assign quick action is selected' do
- it 'lists users who are currently not assigned to the issue' do
- note = find_field('Comment')
- note.native.send_keys('/assign ')
- # The `/assign` ajax response might replace the one by `@` below causing a failed test
- # so we need to wait for the `/assign` ajax request to finish first
- wait_for_requests
- note.native.send_keys('@')
- wait_for_requests
-
- expect(find_tribute_autocomplete_menu).not_to have_text(user.username)
- expect(find_tribute_autocomplete_menu).to have_text(user2.username)
- end
+ wait_for_requests
- it 'lists users who are currently not assigned to the issue when using /assign on the second line' do
- note = find_field('Comment')
- note.native.send_keys('/assign @user2')
- note.native.send_keys(:enter)
- note.native.send_keys('/assign ')
- # The `/assign` ajax response might replace the one by `@` below causing a failed test
- # so we need to wait for the `/assign` ajax request to finish first
- wait_for_requests
- note.native.send_keys('@')
- wait_for_requests
-
- expect(find_tribute_autocomplete_menu).not_to have_text(user.username)
- expect(find_tribute_autocomplete_menu).to have_text(user2.username)
- end
- end
+ expect(page).not_to have_css('.atwho-view')
end
+ end
- context 'if a selected value has special characters' do
- it 'wraps the result in double quotes' do
- fill_in 'Comment', with: "~#{label.title[0..2]}"
+ context 'when other notes are destroyed' do
+ let!(:discussion) { create(:discussion_note_on_issue, noteable: issue, project: issue.project) }
- find_highlighted_tribute_autocomplete_menu.click
+ # This is meant to protect against this issue https://gitlab.com/gitlab-org/gitlab/-/issues/228729
+ it 'keeps autocomplete key listeners' do
+ note = find_field('Comment')
- expect(find_field('Comment').value).to have_text("~\"#{label.title}\"")
- end
+ start_comment_with_emoji(note, '.atwho-view li')
- it 'does not wrap for emoji values' do
- fill_in 'Comment', with: ':cartwheel_'
+ start_and_cancel_discussion
- find_highlighted_tribute_autocomplete_menu.click
+ note.fill_in(with: '')
+ start_comment_with_emoji(note, '.atwho-view li')
+ note.native.send_keys(:enter)
- expect(find_field('Comment').value).to have_text('cartwheel_tone1')
- end
+ expect(note.value).to eql('Hello :100: ')
end
+ end
- context 'quick actions' do
- it 'autocompletes for quick actions' do
- fill_in 'Comment', with: '/as'
+ shared_examples 'autocomplete suggestions' do
+ it 'suggests objects correctly' do
+ fill_in 'Comment', with: object.class.reference_prefix
- find_highlighted_tribute_autocomplete_menu.click
+ find_autocomplete_menu.find('li').click
- expect(find_field('Comment').value).to have_text('/assign')
- end
+ expect(find_field('Comment').value).to have_text(expected_body)
end
+ end
- context 'labels' do
- it 'allows colons when autocompleting scoped labels' do
- fill_in 'Comment', with: '~scoped:'
-
- wait_for_requests
-
- expect(find_tribute_autocomplete_menu).to have_text('scoped::label')
- end
+ context 'issues' do
+ let(:object) { issue }
+ let(:expected_body) { object.to_reference }
- it 'autocompletes multi-word labels' do
- fill_in 'Comment', with: '~Acceptingmerge'
+ it_behaves_like 'autocomplete suggestions'
+ end
- wait_for_requests
+ context 'merge requests' do
+ let(:object) { create(:merge_request, source_project: project) }
+ let(:expected_body) { object.to_reference }
- expect(find_tribute_autocomplete_menu).to have_text('Accepting merge requests')
- end
+ it_behaves_like 'autocomplete suggestions'
+ end
- it 'only autocompletes the last label' do
- fill_in 'Comment', with: '~scoped:: foo bar ~Acceptingmerge'
- # Invoke autocompletion
- find_field('Comment').native.send_keys(:right)
+ context 'project snippets' do
+ let!(:object) { snippet }
+ let(:expected_body) { object.to_reference }
- wait_for_requests
+ it_behaves_like 'autocomplete suggestions'
+ end
- expect(find_tribute_autocomplete_menu).to have_text('Accepting merge requests')
- end
+ context 'milestone' do
+ let_it_be(:milestone_expired) { create(:milestone, project: project, due_date: 5.days.ago) }
+ let_it_be(:milestone_no_duedate) { create(:milestone, project: project, title: 'Foo - No due date') }
+ let_it_be(:milestone1) { create(:milestone, project: project, title: 'Milestone-1', due_date: 20.days.from_now) }
+ let_it_be(:milestone2) { create(:milestone, project: project, title: 'Milestone-2', due_date: 15.days.from_now) }
+ let_it_be(:milestone3) { create(:milestone, project: project, title: 'Milestone-3', due_date: 10.days.from_now) }
- it 'does not autocomplete labels if no tilde is typed' do
- fill_in 'Comment', with: 'Accepting'
+ before do
+ fill_in 'Comment', with: '/milestone %'
- wait_for_requests
+ wait_for_requests
+ end
- expect(page).not_to have_css('.tribute-container')
+ it 'shows milestons list in the autocomplete menu' do
+ page.within(find_autocomplete_menu) do
+ expect(page).to have_selector('li', count: 5)
end
end
- context 'when other notes are destroyed' do
- let!(:discussion) { create(:discussion_note_on_issue, noteable: issue, project: issue.project) }
-
- # This is meant to protect against this issue https://gitlab.com/gitlab-org/gitlab/-/issues/228729
- it 'keeps autocomplete key listeners' do
- note = find_field('Comment')
-
- start_comment_with_emoji(note, '.tribute-container li')
-
- start_and_cancel_discussion
-
- note.fill_in(with: '')
- start_comment_with_emoji(note, '.tribute-container li')
- note.native.send_keys(:enter)
-
- expect(note.value).to eql('Hello :100: ')
+ it 'shows expired milestone at the bottom of the list' do
+ page.within(find_autocomplete_menu) do
+ expect(page.find('li:last-child')).to have_content milestone_expired.title
end
end
- shared_examples 'autocomplete suggestions' do
- it 'suggests objects correctly' do
- fill_in 'Comment', with: object.class.reference_prefix
-
- find_tribute_autocomplete_menu.find('li').click
-
- expect(find_field('Comment').value).to have_text(expected_body)
+ it 'shows milestone due earliest at the top of the list' do
+ page.within(find_autocomplete_menu) do
+ aggregate_failures do
+ expect(page.all('li')[0]).to have_content milestone3.title
+ expect(page.all('li')[1]).to have_content milestone2.title
+ expect(page.all('li')[2]).to have_content milestone1.title
+ expect(page.all('li')[3]).to have_content milestone_no_duedate.title
+ end
end
end
+ end
- context 'issues' do
- let(:object) { issue }
- let(:expected_body) { object.to_reference }
-
- it_behaves_like 'autocomplete suggestions'
- end
+ context 'contact' do
+ let_it_be(:contacts) { create_list(:contact, 2, group: group) }
- context 'merge requests' do
- let(:object) { create(:merge_request, source_project: project) }
- let(:expected_body) { object.to_reference }
+ before do
+ fill_in 'Comment', with: '/add_contacts [contact:'
- it_behaves_like 'autocomplete suggestions'
+ wait_for_requests
end
- context 'project snippets' do
- let!(:object) { snippet }
- let(:expected_body) { object.to_reference }
-
- it_behaves_like 'autocomplete suggestions'
+ it 'shows contacts list in the autocomplete menu' do
+ page.within(find_autocomplete_menu) do
+ expect(page).to have_selector('li', count: 2)
+ end
end
- context 'milestone' do
- let!(:object) { create(:milestone, project: project) }
- let(:expected_body) { object.to_reference }
+ it 'shows all contacts' do
+ page.within(find_autocomplete_menu) do
+ expected_data = contacts.map { |c| "#{c.first_name} #{c.last_name} #{c.email}"}
- it_behaves_like 'autocomplete suggestions'
+ expect(page.all('li').map(&:text)).to match_array(expected_data)
+ end
end
end
end
@@ -707,9 +420,10 @@ RSpec.describe 'GFM autocomplete', :js do
def start_and_cancel_discussion
fill_in('Reply to comment', with: 'Whoops!')
+ click_button('Cancel')
- page.accept_alert 'Are you sure you want to cancel creating this comment?' do
- click_button('Cancel')
+ page.within('.modal') do
+ click_button('OK', match: :first)
end
wait_for_requests
@@ -722,12 +436,4 @@ RSpec.describe 'GFM autocomplete', :js do
def find_highlighted_autocomplete_item
find('.atwho-view li.cur', visible: true)
end
-
- def find_tribute_autocomplete_menu
- find('.tribute-container ul', visible: true)
- end
-
- def find_highlighted_tribute_autocomplete_menu
- find('.tribute-container li.highlight', visible: true)
- end
end
diff --git a/spec/features/issues/keyboard_shortcut_spec.rb b/spec/features/issues/keyboard_shortcut_spec.rb
index 502412bab5d..4dbc5d8e01c 100644
--- a/spec/features/issues/keyboard_shortcut_spec.rb
+++ b/spec/features/issues/keyboard_shortcut_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe 'Issues shortcut', :js do
let(:project) { create(:project) }
before do
- sign_in(project.owner)
+ sign_in(project.first_owner)
visit project_path(project)
end
@@ -23,7 +23,7 @@ RSpec.describe 'Issues shortcut', :js do
let(:project) { create(:project, :issues_disabled) }
before do
- sign_in(project.owner)
+ sign_in(project.first_owner)
visit project_path(project)
end
diff --git a/spec/features/issues/todo_spec.rb b/spec/features/issues/todo_spec.rb
index 315d1c911a2..d63d21353e5 100644
--- a/spec/features/issues/todo_spec.rb
+++ b/spec/features/issues/todo_spec.rb
@@ -19,13 +19,13 @@ RSpec.describe 'Manually create a todo item from issue', :js do
expect(page).to have_content 'Mark as done'
end
- page.within '.header-content .todos-count' do
+ page.within ".header-content span[aria-label='#{_('Todos count')}']" do
expect(page).to have_content '1'
end
visit project_issue_path(project, issue)
- page.within '.header-content .todos-count' do
+ page.within ".header-content span[aria-label='#{_('Todos count')}']" do
expect(page).to have_content '1'
end
end
@@ -36,10 +36,10 @@ RSpec.describe 'Manually create a todo item from issue', :js do
click_button 'Mark as done'
end
- expect(page).to have_selector('.todos-count', visible: false)
+ expect(page).to have_selector(".header-content span[aria-label='#{_('Todos count')}']", visible: false)
visit project_issue_path(project, issue)
- expect(page).to have_selector('.todos-count', visible: false)
+ expect(page).to have_selector(".header-content span[aria-label='#{_('Todos count')}']", visible: false)
end
end
diff --git a/spec/features/issues/user_comments_on_issue_spec.rb b/spec/features/issues/user_comments_on_issue_spec.rb
index 5d03aa1fc2b..a719263f092 100644
--- a/spec/features/issues/user_comments_on_issue_spec.rb
+++ b/spec/features/issues/user_comments_on_issue_spec.rb
@@ -10,7 +10,6 @@ RSpec.describe "User comments on issue", :js do
let(:user) { create(:user) }
before do
- stub_feature_flags(tribute_autocomplete: false)
stub_feature_flags(sandboxed_mermaid: false)
project.add_guest(user)
sign_in(user)
diff --git a/spec/features/issues/user_creates_branch_and_merge_request_spec.rb b/spec/features/issues/user_creates_branch_and_merge_request_spec.rb
index 875b0a60634..167521134b1 100644
--- a/spec/features/issues/user_creates_branch_and_merge_request_spec.rb
+++ b/spec/features/issues/user_creates_branch_and_merge_request_spec.rb
@@ -71,16 +71,10 @@ RSpec.describe 'User creates branch and merge request on issue page', :js do
perform_enqueued_jobs do
select_dropdown_option('create-mr')
- expect(page).to have_content('Draft: Resolve "Cherry-Coloured Funk"')
- expect(current_path).to eq(project_merge_request_path(project, MergeRequest.first))
-
- wait_for_requests
+ expect(page).to have_content('New merge request')
+ expect(page).to have_content("From #{issue.to_branch_name} into #{project.default_branch}")
+ expect(page).to have_current_path(project_new_merge_request_path(project, merge_request: { source_branch: issue.to_branch_name, target_branch: project.default_branch }))
end
-
- visit project_issue_path(project, issue)
-
- expect(page).to have_content("created merge request !1 to address this issue")
- expect(page).to have_content('mentioned in merge request !1')
end
it 'creates a branch' do
@@ -100,17 +94,10 @@ RSpec.describe 'User creates branch and merge request on issue page', :js do
perform_enqueued_jobs do
select_dropdown_option('create-mr', branch_name)
- expect(page).to have_content('Draft: Resolve "Cherry-Coloured Funk"')
- expect(page).to have_content('Request to merge custom-branch-name into')
- expect(current_path).to eq(project_merge_request_path(project, MergeRequest.first))
-
- wait_for_requests
+ expect(page).to have_content('New merge request')
+ expect(page).to have_content("From #{branch_name} into #{project.default_branch}")
+ expect(page).to have_current_path(project_new_merge_request_path(project, merge_request: { source_branch: branch_name, target_branch: project.default_branch }))
end
-
- visit project_issue_path(project, issue)
-
- expect(page).to have_content("created merge request !1 to address this issue")
- expect(page).to have_content('mentioned in merge request !1')
end
it 'creates a branch' do
diff --git a/spec/features/issues/user_edits_issue_spec.rb b/spec/features/issues/user_edits_issue_spec.rb
index a036a9a5bbc..8c906e6a27c 100644
--- a/spec/features/issues/user_edits_issue_spec.rb
+++ b/spec/features/issues/user_edits_issue_spec.rb
@@ -145,7 +145,7 @@ RSpec.describe "Issues > User edits issue", :js do
fill_in 'Comment', with: '/label ~syzygy'
click_button 'Comment'
- expect(page).to have_text('added syzygy label just now', wait: 300)
+ expect(page).to have_text('added syzygy label just now')
page.within '.block.labels' do
# Remove `verisimilitude` label
diff --git a/spec/features/issues/user_interacts_with_awards_spec.rb b/spec/features/issues/user_interacts_with_awards_spec.rb
index 2e52a8d862e..892b57bac5c 100644
--- a/spec/features/issues/user_interacts_with_awards_spec.rb
+++ b/spec/features/issues/user_interacts_with_awards_spec.rb
@@ -65,9 +65,10 @@ RSpec.describe 'User interacts with awards' do
expect(page.find('[data-testid="award-button"].selected .js-counter')).to have_content('1')
expect(page).to have_css('[data-testid="award-button"].selected[title="You reacted with :8ball:"]')
+ wait_for_requests
+
expect do
page.find('[data-testid="award-button"].selected').click
- wait_for_requests
end.to change { page.all('[data-testid="award-button"]').size }.from(3).to(2)
end
end
diff --git a/spec/features/issues/user_sorts_issues_spec.rb b/spec/features/issues/user_sorts_issues_spec.rb
index 48297e9049e..f3eaff379a1 100644
--- a/spec/features/issues/user_sorts_issues_spec.rb
+++ b/spec/features/issues/user_sorts_issues_spec.rb
@@ -119,89 +119,6 @@ RSpec.describe "User sorts issues" do
end
end
- describe 'filtering by due date', :js do
- before do
- issue1.update!(due_date: 1.day.from_now)
- issue2.update!(due_date: 6.days.from_now)
- end
-
- it 'filters by none' do
- visit project_issues_path(project, due_date: Issue::NoDueDate.name)
-
- page.within '.issues-list' do
- expect(page).not_to have_content('foo')
- expect(page).not_to have_content('bar')
- expect(page).to have_content('baz')
- end
- end
-
- it 'filters by any' do
- visit project_issues_path(project, due_date: Issue::AnyDueDate.name)
-
- page.within '.issues-list' do
- expect(page).to have_content('foo')
- expect(page).to have_content('bar')
- expect(page).to have_content('baz')
- end
- end
-
- it 'filters by due this week' do
- issue1.update!(due_date: Date.today.beginning_of_week + 2.days)
- issue2.update!(due_date: Date.today.end_of_week)
- issue3.update!(due_date: Date.today - 8.days)
-
- visit project_issues_path(project, due_date: Issue::DueThisWeek.name)
-
- page.within '.issues-list' do
- expect(page).to have_content('foo')
- expect(page).to have_content('bar')
- expect(page).not_to have_content('baz')
- end
- end
-
- it 'filters by due this month' do
- issue1.update!(due_date: Date.today.beginning_of_month + 2.days)
- issue2.update!(due_date: Date.today.end_of_month)
- issue3.update!(due_date: Date.today - 50.days)
-
- visit project_issues_path(project, due_date: Issue::DueThisMonth.name)
-
- page.within '.issues-list' do
- expect(page).to have_content('foo')
- expect(page).to have_content('bar')
- expect(page).not_to have_content('baz')
- end
- end
-
- it 'filters by overdue' do
- issue1.update!(due_date: Date.today + 2.days)
- issue2.update!(due_date: Date.today + 20.days)
- issue3.update!(due_date: Date.yesterday)
-
- visit project_issues_path(project, due_date: Issue::Overdue.name)
-
- page.within '.issues-list' do
- expect(page).not_to have_content('foo')
- expect(page).not_to have_content('bar')
- expect(page).to have_content('baz')
- end
- end
-
- it 'filters by due next month and previous two weeks' do
- issue1.update!(due_date: Date.today - 4.weeks)
- issue2.update!(due_date: (Date.today + 2.months).beginning_of_month)
- issue3.update!(due_date: Date.yesterday)
-
- visit project_issues_path(project, due_date: Issue::DueNextMonthAndPreviousTwoWeeks.name)
-
- page.within '.issues-list' do
- expect(page).not_to have_content('foo')
- expect(page).not_to have_content('bar')
- expect(page).to have_content('baz')
- end
- end
- end
-
describe 'sorting by milestone', :js do
before do
issue1.milestone = newer_due_milestone
diff --git a/spec/features/jira_connect/branches_spec.rb b/spec/features/jira_connect/branches_spec.rb
index 6fa600c6906..c334a425849 100644
--- a/spec/features/jira_connect/branches_spec.rb
+++ b/spec/features/jira_connect/branches_spec.rb
@@ -25,8 +25,9 @@ RSpec.describe 'Create GitLab branches from Jira', :js do
it 'select project and branch and submit the form' do
visit new_jira_connect_branch_path(issue_key: 'ACME-123', issue_summary: 'My issue !@#$% title')
- expect(page).to have_field('Branch name', with: 'ACME-123-my-issue-title')
expect(page).to have_button('Create branch', disabled: true)
+ # initially, branch field should be hidden.
+ expect(page).not_to have_field('Branch name')
# Select project1
@@ -44,6 +45,7 @@ RSpec.describe 'Create GitLab branches from Jira', :js do
click_on 'Alice / foo'
end
+ expect(page).to have_field('Branch name', with: 'ACME-123-my-issue-title')
expect(page).to have_button('Create branch', disabled: false)
click_on 'master'
diff --git a/spec/features/markdown/copy_as_gfm_spec.rb b/spec/features/markdown/copy_as_gfm_spec.rb
index 0e5a20fe24a..6951d8298e5 100644
--- a/spec/features/markdown/copy_as_gfm_spec.rb
+++ b/spec/features/markdown/copy_as_gfm_spec.rb
@@ -663,7 +663,7 @@ RSpec.describe 'Copy as GFM', :js do
let(:project) { create(:project, :repository) }
before do
- sign_in(project.owner)
+ sign_in(project.first_owner)
end
context 'from a diff' do
diff --git a/spec/features/merge_request/batch_comments_spec.rb b/spec/features/merge_request/batch_comments_spec.rb
index f695b225915..eb98c7d5061 100644
--- a/spec/features/merge_request/batch_comments_spec.rb
+++ b/spec/features/merge_request/batch_comments_spec.rb
@@ -32,7 +32,7 @@ RSpec.describe 'Merge request > Batch comments', :js do
expect(page).to have_selector('[data-testid="review_bar_component"]')
- expect(find('.review-bar-content .btn-confirm')).to have_content('1')
+ expect(find('[data-testid="review_bar_component"] .btn-confirm')).to have_content('1')
end
it 'publishes review' do
@@ -64,7 +64,11 @@ RSpec.describe 'Merge request > Batch comments', :js do
it 'deletes draft note' do
write_diff_comment
- accept_alert { find('.js-note-delete').click }
+ find('.js-note-delete').click
+
+ page.within('.modal') do
+ click_button('Delete Comment', match: :first)
+ end
wait_for_requests
@@ -146,10 +150,6 @@ RSpec.describe 'Merge request > Batch comments', :js do
it 'adds draft comments to both sides' do
write_parallel_comment('2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9')
-
- # make sure line 9 is in the view
- execute_script("window.scrollBy(0, -200)")
-
write_parallel_comment('2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9', button_text: 'Add to review', text: 'Another wrong line')
expect(find('.new .draft-note-component')).to have_content('Line is wrong')
@@ -251,13 +251,15 @@ RSpec.describe 'Merge request > Batch comments', :js do
end
def write_diff_comment(**params)
- click_diff_line(find("[id='#{sample_compare.changes[0][:line_code]}']"))
+ click_diff_line(find_by_scrolling("[id='#{sample_compare.changes[0][:line_code]}']"))
write_comment(**params)
end
def write_parallel_comment(line, **params)
- find("div[id='#{line}']").hover
+ line_element = find_by_scrolling("[id='#{line}']")
+ scroll_to_elements_bottom(line_element)
+ line_element.hover
find(".js-add-diff-note-button").click
write_comment(selector: "form[data-line-code='#{line}']", **params)
diff --git a/spec/features/merge_request/user_awards_emoji_spec.rb b/spec/features/merge_request/user_awards_emoji_spec.rb
index 240b8f996c8..35eadb34799 100644
--- a/spec/features/merge_request/user_awards_emoji_spec.rb
+++ b/spec/features/merge_request/user_awards_emoji_spec.rb
@@ -27,6 +27,7 @@ RSpec.describe 'Merge request > User awards emoji', :js do
it 'removes award from merge request' do
first('[data-testid="award-button"]').click
+ expect(first('[data-testid="award-button"]')).to have_content '1'
find('[data-testid="award-button"].selected').click
expect(first('[data-testid="award-button"]')).to have_content '0'
diff --git a/spec/features/merge_request/user_comments_on_diff_spec.rb b/spec/features/merge_request/user_comments_on_diff_spec.rb
index f9b554c5ed2..c06019f6b77 100644
--- a/spec/features/merge_request/user_comments_on_diff_spec.rb
+++ b/spec/features/merge_request/user_comments_on_diff_spec.rb
@@ -25,14 +25,15 @@ RSpec.describe 'User comments on a diff', :js do
context 'when toggling inline comments' do
context 'in a single file' do
it 'hides a comment' do
- click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']"))
+ line_element = find_by_scrolling("[id='#{sample_compare.changes[1][:line_code]}']").find(:xpath, "..")
+ click_diff_line(line_element)
page.within('.js-discussion-note-form') do
fill_in('note_note', with: 'Line is wrong')
click_button('Add comment now')
end
- page.within('.diff-files-holder > div:nth-child(6)') do
+ page.within(line_element.ancestor('[data-path]')) do
expect(page).to have_content('Line is wrong')
find('.js-diff-more-actions').click
@@ -45,7 +46,9 @@ RSpec.describe 'User comments on a diff', :js do
context 'in multiple files' do
it 'toggles comments' do
- click_diff_line(find("[id='#{sample_compare.changes[0][:line_code]}']"))
+ first_line_element = find_by_scrolling("[id='#{sample_compare.changes[0][:line_code]}']").find(:xpath, "..")
+ first_root_element = first_line_element.ancestor('[data-path]')
+ click_diff_line(first_line_element)
page.within('.js-discussion-note-form') do
fill_in('note_note', with: 'Line is correct')
@@ -54,11 +57,14 @@ RSpec.describe 'User comments on a diff', :js do
wait_for_requests
- page.within('.diff-files-holder > div:nth-child(5) .note-body > .note-text') do
+ page.within(first_root_element) do
expect(page).to have_content('Line is correct')
end
- click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']"))
+ second_line_element = find_by_scrolling("[id='#{sample_compare.changes[1][:line_code]}']")
+ second_root_element = second_line_element.ancestor('[data-path]')
+
+ click_diff_line(second_line_element)
page.within('.js-discussion-note-form') do
fill_in('note_note', with: 'Line is wrong')
@@ -68,7 +74,7 @@ RSpec.describe 'User comments on a diff', :js do
wait_for_requests
# Hide the comment.
- page.within('.diff-files-holder > div:nth-child(6)') do
+ page.within(second_root_element) do
find('.js-diff-more-actions').click
click_button 'Hide comments on this file'
@@ -77,37 +83,36 @@ RSpec.describe 'User comments on a diff', :js do
# At this moment a user should see only one comment.
# The other one should be hidden.
- page.within('.diff-files-holder > div:nth-child(5) .note-body > .note-text') do
+ page.within(first_root_element) do
expect(page).to have_content('Line is correct')
end
# Show the comment.
- page.within('.diff-files-holder > div:nth-child(6)') do
+ page.within(second_root_element) do
find('.js-diff-more-actions').click
click_button 'Show comments on this file'
end
# Now both the comments should be shown.
- page.within('.diff-files-holder > div:nth-child(6) .note-body > .note-text') do
+ page.within(second_root_element) do
expect(page).to have_content('Line is wrong')
end
- page.within('.diff-files-holder > div:nth-child(5) .note-body > .note-text') do
+ page.within(first_root_element) do
expect(page).to have_content('Line is correct')
end
# Check the same comments in the side-by-side view.
- execute_script("window.scrollTo(0,0);")
find('.js-show-diff-settings').click
click_button 'Side-by-side'
wait_for_requests
- page.within('.diff-files-holder > div:nth-child(6) .parallel .note-body > .note-text') do
+ page.within(second_root_element) do
expect(page).to have_content('Line is wrong')
end
- page.within('.diff-files-holder > div:nth-child(5) .parallel .note-body > .note-text') do
+ page.within(first_root_element) do
expect(page).to have_content('Line is correct')
end
end
@@ -121,7 +126,7 @@ RSpec.describe 'User comments on a diff', :js do
context 'when adding multiline comments' do
it 'saves a multiline comment' do
- click_diff_line(find("[id='#{sample_commit.line_code}']"))
+ click_diff_line(find_by_scrolling("[id='#{sample_commit.line_code}']").find(:xpath, '..'))
add_comment('-13', '+14')
end
@@ -133,13 +138,13 @@ RSpec.describe 'User comments on a diff', :js do
# In `files/ruby/popen.rb`
it 'allows comments for changes involving both sides' do
# click +15, select -13 add and verify comment
- click_diff_line(find('div[data-path="files/ruby/popen.rb"] .right-side a[data-linenumber="15"]').find(:xpath, '../../..'), 'right')
+ click_diff_line(find_by_scrolling('div[data-path="files/ruby/popen.rb"] .right-side a[data-linenumber="15"]').find(:xpath, '../../..'), 'right')
add_comment('-13', '+15')
end
it 'allows comments on previously hidden lines at the top of a file', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/285294' do
# Click -9, expand up, select 1 add and verify comment
- page.within('[data-path="files/ruby/popen.rb"]') do
+ page.within find_by_scrolling('[data-path="files/ruby/popen.rb"]') do
all('.js-unfold-all')[0].click
end
click_diff_line(find('div[data-path="files/ruby/popen.rb"] .left-side a[data-linenumber="9"]').find(:xpath, '../..'), 'left')
@@ -148,7 +153,7 @@ RSpec.describe 'User comments on a diff', :js do
it 'allows comments on previously hidden lines the middle of a file' do
# Click 27, expand up, select 18, add and verify comment
- page.within('[data-path="files/ruby/popen.rb"]') do
+ page.within find_by_scrolling('[data-path="files/ruby/popen.rb"]') do
all('.js-unfold-all')[1].click
end
click_diff_line(find('div[data-path="files/ruby/popen.rb"] .left-side a[data-linenumber="21"]').find(:xpath, '../..'), 'left')
@@ -157,8 +162,8 @@ RSpec.describe 'User comments on a diff', :js do
it 'allows comments on previously hidden lines at the bottom of a file' do
# Click +28, expand down, select 37 add and verify comment
- page.within('[data-path="files/ruby/popen.rb"]') do
- all('.js-unfold-down')[1].click
+ page.within find_by_scrolling('[data-path="files/ruby/popen.rb"]') do
+ all('.js-unfold-down:not([disabled])')[1].click
end
click_diff_line(find('div[data-path="files/ruby/popen.rb"] .left-side a[data-linenumber="30"]').find(:xpath, '../..'), 'left')
add_comment('+28', '37')
@@ -198,7 +203,7 @@ RSpec.describe 'User comments on a diff', :js do
context 'when editing comments' do
it 'edits a comment' do
- click_diff_line(find("[id='#{sample_commit.line_code}']"))
+ click_diff_line(find_by_scrolling("[id='#{sample_commit.line_code}']"))
page.within('.js-discussion-note-form') do
fill_in(:note_note, with: 'Line is wrong')
@@ -224,7 +229,7 @@ RSpec.describe 'User comments on a diff', :js do
context 'when deleting comments' do
it 'deletes a comment' do
- click_diff_line(find("[id='#{sample_commit.line_code}']"))
+ click_diff_line(find_by_scrolling("[id='#{sample_commit.line_code}']"))
page.within('.js-discussion-note-form') do
fill_in(:note_note, with: 'Line is wrong')
@@ -238,8 +243,11 @@ RSpec.describe 'User comments on a diff', :js do
page.within('.diff-file:nth-of-type(1) .discussion .note') do
find('.more-actions').click
find('.more-actions .dropdown-menu li', match: :first)
+ find('.js-note-delete').click
+ end
- accept_confirm { find('.js-note-delete').click }
+ page.within('.modal') do
+ click_button('Delete Comment', match: :first)
end
page.within('.merge-request-tabs') do
diff --git a/spec/features/merge_request/user_creates_image_diff_notes_spec.rb b/spec/features/merge_request/user_creates_image_diff_notes_spec.rb
index cc0d7a279dd..15f186b649a 100644
--- a/spec/features/merge_request/user_creates_image_diff_notes_spec.rb
+++ b/spec/features/merge_request/user_creates_image_diff_notes_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe 'Merge request > User creates image diff notes', :js do
it 'shows indicator and avatar badges, and allows collapsing/expanding the discussion notes' do
indicator = find('.js-image-badge')
- badge = find('.image-diff-avatar-link .badge')
+ badge = find('.image-diff-avatar-link .design-note-pin')
expect(indicator).to have_content('1')
expect(badge).to have_content('1')
@@ -127,7 +127,7 @@ RSpec.describe 'Merge request > User creates image diff notes', :js do
visit diffs_project_merge_request_path(project, merge_request, view: view)
wait_for_requests
- expect(page.all('.diff-file span.label-lfs', visible: :all)).not_to be_empty
+ expect(page.all('[data-testid="label-lfs"]', visible: :all)).not_to be_empty
end
it_behaves_like 'creates image diff note'
diff --git a/spec/features/merge_request/user_edits_assignees_sidebar_spec.rb b/spec/features/merge_request/user_edits_assignees_sidebar_spec.rb
index 1087be3d8c6..5894ec923c2 100644
--- a/spec/features/merge_request/user_edits_assignees_sidebar_spec.rb
+++ b/spec/features/merge_request/user_edits_assignees_sidebar_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe 'Merge request > User edits assignees sidebar', :js do
before do
stub_const('Autocomplete::UsersFinder::LIMIT', users_find_limit)
- sign_in(project.owner)
+ sign_in(project.first_owner)
merge_request.assignees << assignee
diff --git a/spec/features/merge_request/user_expands_diff_spec.rb b/spec/features/merge_request/user_expands_diff_spec.rb
index 52554f11d28..25c9584350d 100644
--- a/spec/features/merge_request/user_expands_diff_spec.rb
+++ b/spec/features/merge_request/user_expands_diff_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe 'User expands diff', :js do
let(:merge_request) { create(:merge_request, source_branch: 'expand-collapse-files', source_project: project, target_project: project) }
before do
- allow(Gitlab::CurrentSettings).to receive(:diff_max_patch_bytes).and_return(100.kilobytes)
+ allow(Gitlab::CurrentSettings).to receive(:diff_max_patch_bytes).and_return(100.bytes)
visit(diffs_project_merge_request_path(project, merge_request))
@@ -15,7 +15,7 @@ RSpec.describe 'User expands diff', :js do
end
it 'allows user to expand diff' do
- page.within find('[id="19763941ab80e8c09871c0a425f0560d9053bcb3"]') do
+ page.within find("[id='4c76a1271e41072d7da9fe40bf0f79f7384d472a']") do
find('[data-testid="expand-button"]').click
wait_for_requests
diff --git a/spec/features/merge_request/user_interacts_with_batched_mr_diffs_spec.rb b/spec/features/merge_request/user_interacts_with_batched_mr_diffs_spec.rb
index 3665ad91dd6..7d67cde4bbb 100644
--- a/spec/features/merge_request/user_interacts_with_batched_mr_diffs_spec.rb
+++ b/spec/features/merge_request/user_interacts_with_batched_mr_diffs_spec.rb
@@ -10,21 +10,19 @@ RSpec.describe 'Batch diffs', :js do
let(:merge_request) { create(:merge_request, source_project: project, source_branch: 'master', target_branch: 'empty-branch') }
before do
- sign_in(project.owner)
+ sign_in(project.first_owner)
visit diffs_project_merge_request_path(merge_request.project, merge_request)
wait_for_requests
- # Add discussion to first line of first file
- click_diff_line(find('.diff-file.file-holder:first-of-type .line_holder .left-side:first-of-type'))
- page.within('.js-discussion-note-form') do
+ click_diff_line(get_first_diff.find('[data-testid="left-side"]', match: :first))
+ page.within get_first_diff.find('.js-discussion-note-form') do
fill_in('note_note', with: 'First Line Comment')
click_button('Add comment now')
end
- # Add discussion to first line of last file
- click_diff_line(find('.diff-file.file-holder:last-of-type .line_holder .left-side:first-of-type'))
- page.within('.js-discussion-note-form') do
+ click_diff_line(get_second_diff.find('[data-testid="left-side"]', match: :first))
+ page.within get_second_diff.find('.js-discussion-note-form') do
fill_in('note_note', with: 'Last Line Comment')
click_button('Add comment now')
end
@@ -36,17 +34,14 @@ RSpec.describe 'Batch diffs', :js do
# Reload so we know the discussions are persisting across batch loads
visit page.current_url
- # Wait for JS to settle
wait_for_requests
- expect(page).to have_selector('.diff-files-holder .file-holder', count: 39)
-
# Confirm discussions are applied to appropriate files (should be contained in multiple diff pages)
- page.within('.diff-file.file-holder:first-of-type .notes .timeline-entry .note .note-text') do
+ page.within get_first_diff.find('.notes .timeline-entry .note .note-text') do
expect(page).to have_content('First Line Comment')
end
- page.within('.diff-file.file-holder:last-of-type .notes .timeline-entry .note .note-text') do
+ page.within get_second_diff.find('.notes .timeline-entry .note .note-text') do
expect(page).to have_content('Last Line Comment')
end
end
@@ -54,7 +49,7 @@ RSpec.describe 'Batch diffs', :js do
context 'when user visits a URL with a link directly to to a discussion' do
context 'which is in the first batched page of diffs' do
it 'scrolls to the correct discussion' do
- page.within('.diff-file.file-holder:first-of-type') do
+ page.within get_first_diff do
click_link('just now')
end
@@ -63,15 +58,15 @@ RSpec.describe 'Batch diffs', :js do
wait_for_requests
# Confirm scrolled to correct UI element
- expect(page.find('.diff-file.file-holder:first-of-type .discussion-notes .timeline-entry li.note[id]').obscured?).to be_falsey
- expect(page.find('.diff-file.file-holder:last-of-type .discussion-notes .timeline-entry li.note[id]').obscured?).to be_truthy
+ expect(get_first_diff.find('.discussion-notes .timeline-entry li.note[id]').obscured?).to be_falsey
+ expect(get_second_diff.find('.discussion-notes .timeline-entry li.note[id]').obscured?).to be_truthy
end
end
context 'which is in at least page 2 of the batched pages of diffs' do
it 'scrolls to the correct discussion',
quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/293814' } do
- page.within('.diff-file.file-holder:last-of-type') do
+ page.within get_first_diff do
click_link('just now')
end
@@ -80,8 +75,8 @@ RSpec.describe 'Batch diffs', :js do
wait_for_requests
# Confirm scrolled to correct UI element
- expect(page.find('.diff-file.file-holder:first-of-type .discussion-notes .timeline-entry li.note[id]').obscured?).to be_truthy
- expect(page.find('.diff-file.file-holder:last-of-type .discussion-notes .timeline-entry li.note[id]').obscured?).to be_falsey
+ expect(get_first_diff.find('.discussion-notes .timeline-entry li.note[id]').obscured?).to be_truthy
+ expect(get_second_diff.find('.discussion-notes .timeline-entry li.note[id]').obscured?).to be_falsey
end
end
end
@@ -95,15 +90,21 @@ RSpec.describe 'Batch diffs', :js do
end
it 'has the correct discussions applied to files across batched pages' do
- expect(page).to have_selector('.diff-files-holder .file-holder', count: 39)
-
- page.within('.diff-file.file-holder:first-of-type .notes .timeline-entry .note .note-text') do
+ page.within get_first_diff.find('.notes .timeline-entry .note .note-text') do
expect(page).to have_content('First Line Comment')
end
- page.within('.diff-file.file-holder:last-of-type .notes .timeline-entry .note .note-text') do
+ page.within get_second_diff.find('.notes .timeline-entry .note .note-text') do
expect(page).to have_content('Last Line Comment')
end
end
end
+
+ def get_first_diff
+ find('#a9b6f940524f646951cc28d954aa41f814f95d4f')
+ end
+
+ def get_second_diff
+ find('#b285a86891571c7fdbf1f82e840816079de1cc8b')
+ end
end
diff --git a/spec/features/merge_request/user_merges_merge_request_spec.rb b/spec/features/merge_request/user_merges_merge_request_spec.rb
index 7758fa8e666..d1be93cae02 100644
--- a/spec/features/merge_request/user_merges_merge_request_spec.rb
+++ b/spec/features/merge_request/user_merges_merge_request_spec.rb
@@ -3,7 +3,7 @@
require "spec_helper"
RSpec.describe "User merges a merge request", :js do
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
before do
sign_in(user)
diff --git a/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb b/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb
index 8438c0af553..4d7ee11e366 100644
--- a/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb
+++ b/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb
@@ -57,7 +57,7 @@ RSpec.describe 'Merge request > User merges only if pipeline succeeds', :js do
wait_for_requests
expect(page).to have_css('button[disabled="disabled"]', text: 'Merge')
- expect(page).to have_content('The pipeline for this merge request did not complete. Push a new commit to fix the failure, or check the troubleshooting documentation to see other possible actions.')
+ expect(page).to have_content('Merge blocked: pipeline must succeed. Push a commit that fixes the failure, or learn about other solutions.')
end
end
@@ -70,7 +70,7 @@ RSpec.describe 'Merge request > User merges only if pipeline succeeds', :js do
wait_for_requests
expect(page).not_to have_button 'Merge'
- expect(page).to have_content('The pipeline for this merge request did not complete. Push a new commit to fix the failure, or check the troubleshooting documentation to see other possible actions.')
+ expect(page).to have_content('Merge blocked: pipeline must succeed. Push a commit that fixes the failure, or learn about other solutions.')
end
end
diff --git a/spec/features/merge_request/user_posts_diff_notes_spec.rb b/spec/features/merge_request/user_posts_diff_notes_spec.rb
index 9e314e18563..d803aec5895 100644
--- a/spec/features/merge_request/user_posts_diff_notes_spec.rb
+++ b/spec/features/merge_request/user_posts_diff_notes_spec.rb
@@ -29,54 +29,54 @@ RSpec.describe 'Merge request > User posts diff notes', :js do
context 'with an old line on the left and no line on the right' do
it 'allows commenting on the left side' do
- should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_23_22"]'), 'left')
+ should_allow_commenting(find_by_scrolling('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_23_22"]'), 'left')
end
it 'does not allow commenting on the right side' do
- should_not_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_23_22"]').find(:xpath, '..'), 'right')
+ should_not_allow_commenting(find_by_scrolling('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_23_22"]').find(:xpath, '..'), 'right')
end
end
context 'with no line on the left and a new line on the right' do
it 'does not allow commenting on the left side' do
- should_not_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15"]').find(:xpath, '..'), 'left')
+ should_not_allow_commenting(find_by_scrolling('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15"]').find(:xpath, '..'), 'left')
end
it 'allows commenting on the right side' do
- should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15"]').find(:xpath, '..'), 'right')
+ should_allow_commenting(find_by_scrolling('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15"]').find(:xpath, '..'), 'right')
end
end
context 'with an old line on the left and a new line on the right' do
it 'allows commenting on the left side', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/199050' do
- should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9"]').find(:xpath, '..'), 'left')
+ should_allow_commenting(find_by_scrolling('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9"]').find(:xpath, '..'), 'left')
end
it 'allows commenting on the right side' do
- should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9"]').find(:xpath, '..'), 'right')
+ should_allow_commenting(find_by_scrolling('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9"]').find(:xpath, '..'), 'right')
end
end
context 'with an unchanged line on the left and an unchanged line on the right' do
it 'allows commenting on the left side', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/196826' do
- should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]', match: :first).find(:xpath, '..'), 'left')
+ should_allow_commenting(find_by_scrolling('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]', match: :first).find(:xpath, '..'), 'left')
end
it 'allows commenting on the right side' do
- should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]', match: :first).find(:xpath, '..'), 'right')
+ should_allow_commenting(find_by_scrolling('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]', match: :first).find(:xpath, '..'), 'right')
end
end
context 'with a match line' do
it 'does not allow commenting' do
- line_holder = find('.match', match: :first)
+ line_holder = find_by_scrolling('.match', match: :first)
match_should_not_allow_commenting(line_holder)
end
end
context 'with an unfolded line' do
before do
- page.within('.file-holder[id="a5cc2925ca8258af241be7e5b0381edf30266302"]') do
+ page.within find_by_scrolling('.file-holder[id="a5cc2925ca8258af241be7e5b0381edf30266302"]') do
find('.js-unfold', match: :first).click
end
@@ -84,12 +84,12 @@ RSpec.describe 'Merge request > User posts diff notes', :js do
end
it 'allows commenting on the left side' do
- should_allow_commenting(first('#a5cc2925ca8258af241be7e5b0381edf30266302 .line_holder [data-testid="left-side"]'))
+ should_allow_commenting(find_by_scrolling('#a5cc2925ca8258af241be7e5b0381edf30266302').first('.line_holder [data-testid="left-side"]'))
end
it 'allows commenting on the right side' do
# Automatically shifts comment box to left side.
- should_allow_commenting(first('#a5cc2925ca8258af241be7e5b0381edf30266302 .line_holder [data-testid="right-side"]'))
+ should_allow_commenting(find_by_scrolling('#a5cc2925ca8258af241be7e5b0381edf30266302').first('.line_holder [data-testid="right-side"]'))
end
end
end
@@ -101,44 +101,44 @@ RSpec.describe 'Merge request > User posts diff notes', :js do
context 'after deleteing a note' do
it 'allows commenting' do
- should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
+ should_allow_commenting(find_by_scrolling('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
- accept_confirm do
+ accept_gl_confirm(button_text: 'Delete Comment') do
first('button.more-actions-toggle').click
first('.js-note-delete').click
end
- should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
+ should_allow_commenting(find_by_scrolling('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
end
end
context 'with a new line' do
it 'allows commenting' do
- should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
+ should_allow_commenting(find_by_scrolling('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
end
end
context 'with an old line' do
it 'allows commenting' do
- should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]'))
+ should_allow_commenting(find_by_scrolling('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]'))
end
end
context 'with an unchanged line' do
it 'allows commenting' do
- should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]'))
+ should_allow_commenting(find_by_scrolling('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]'))
end
end
context 'with a match line' do
it 'does not allow commenting' do
- match_should_not_allow_commenting(find('.match', match: :first))
+ match_should_not_allow_commenting(find_by_scrolling('.match', match: :first))
end
end
context 'with an unfolded line' do
before do
- page.within('.file-holder[id="a5cc2925ca8258af241be7e5b0381edf30266302"]') do
+ page.within find_by_scrolling('.file-holder[id="a5cc2925ca8258af241be7e5b0381edf30266302"]') do
find('.js-unfold', match: :first).click
end
@@ -147,7 +147,7 @@ RSpec.describe 'Merge request > User posts diff notes', :js do
# The first `.js-unfold` unfolds upwards, therefore the first
# `.line_holder` will be an unfolded line.
- let(:line_holder) { first('[id="a5cc2925ca8258af241be7e5b0381edf30266302_1_1"]') }
+ let(:line_holder) { find_by_scrolling('[id="a5cc2925ca8258af241be7e5b0381edf30266302_1_1"]') }
it 'allows commenting' do
should_allow_commenting line_holder
@@ -157,7 +157,7 @@ RSpec.describe 'Merge request > User posts diff notes', :js do
context 'when hovering over a diff discussion' do
before do
visit diffs_project_merge_request_path(project, merge_request, view: 'inline')
- should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]'))
+ should_allow_commenting(find_by_scrolling('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]'))
visit project_merge_request_path(project, merge_request)
end
@@ -174,7 +174,7 @@ RSpec.describe 'Merge request > User posts diff notes', :js do
context 'with a new line' do
it 'allows dismissing a comment' do
- should_allow_dismissing_a_comment(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
+ should_allow_dismissing_a_comment(find_by_scrolling('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
end
end
end
@@ -182,13 +182,13 @@ RSpec.describe 'Merge request > User posts diff notes', :js do
describe 'with multiple note forms' do
before do
visit diffs_project_merge_request_path(project, merge_request, view: 'inline')
- click_diff_line(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
- click_diff_line(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]'))
+ click_diff_line(find_by_scrolling('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
+ click_diff_line(find_by_scrolling('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]'))
end
describe 'posting a note' do
it 'adds as discussion' do
- should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]'), asset_form_reset: false)
+ should_allow_commenting(find_by_scrolling('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]'), asset_form_reset: false)
expect(page).to have_css('.notes_holder .note.note-discussion', count: 1)
expect(page).to have_field('Reply…')
end
@@ -203,25 +203,25 @@ RSpec.describe 'Merge request > User posts diff notes', :js do
context 'with a new line' do
it 'allows commenting' do
- should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
+ should_allow_commenting(find_by_scrolling('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
end
end
context 'with an old line' do
it 'allows commenting' do
- should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]'))
+ should_allow_commenting(find_by_scrolling('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]'))
end
end
context 'with an unchanged line' do
it 'allows commenting' do
- should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]'))
+ should_allow_commenting(find_by_scrolling('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]'))
end
end
context 'with a match line' do
it 'does not allow commenting' do
- match_should_not_allow_commenting(find('.match', match: :first))
+ match_should_not_allow_commenting(find_by_scrolling('.match', match: :first))
end
end
end
diff --git a/spec/features/merge_request/user_posts_notes_spec.rb b/spec/features/merge_request/user_posts_notes_spec.rb
index 0416474218f..1779567624c 100644
--- a/spec/features/merge_request/user_posts_notes_spec.rb
+++ b/spec/features/merge_request/user_posts_notes_spec.rb
@@ -133,7 +133,7 @@ RSpec.describe 'Merge request > User posts notes', :js do
describe 'when previewing a note' do
it 'shows the toolbar buttons when editing a note' do
page.within('.js-main-target-form') do
- expect(page).to have_css('.md-header-toolbar.active')
+ expect(page).to have_css('.md-header-toolbar')
end
end
@@ -141,7 +141,7 @@ RSpec.describe 'Merge request > User posts notes', :js do
wait_for_requests
find('.js-md-preview-button').click
page.within('.js-main-target-form') do
- expect(page).not_to have_css('.md-header-toolbar.active')
+ expect(page).not_to have_css('.md-header-toolbar')
end
end
end
@@ -165,11 +165,13 @@ RSpec.describe 'Merge request > User posts notes', :js do
it 'resets the edit note form textarea with the original content of the note if cancelled' do
within('.current-note-edit-form') do
fill_in 'note[note]', with: 'Some new content'
+ find('[data-testid="cancel"]').click
+ end
- accept_confirm do
- find('[data-testid="cancel"]').click
- end
+ page.within('.modal') do
+ click_button('OK', match: :first)
end
+
expect(find('.js-note-text').text).to eq ''
end
diff --git a/spec/features/merge_request/user_rebases_merge_request_spec.rb b/spec/features/merge_request/user_rebases_merge_request_spec.rb
index a3f72a6266b..d42864200ec 100644
--- a/spec/features/merge_request/user_rebases_merge_request_spec.rb
+++ b/spec/features/merge_request/user_rebases_merge_request_spec.rb
@@ -4,7 +4,7 @@ require "spec_helper"
RSpec.describe "User rebases a merge request", :js do
let(:merge_request) { create(:merge_request, :simple, source_project: project) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
before do
sign_in(user)
diff --git a/spec/features/merge_request/user_resolves_wip_mr_spec.rb b/spec/features/merge_request/user_resolves_wip_mr_spec.rb
index fd405855cf8..92927b713f1 100644
--- a/spec/features/merge_request/user_resolves_wip_mr_spec.rb
+++ b/spec/features/merge_request/user_resolves_wip_mr_spec.rb
@@ -2,13 +2,13 @@
require 'spec_helper'
-RSpec.describe 'Merge request > User resolves Work in Progress', :js do
+RSpec.describe 'Merge request > User resolves Draft', :js do
let(:project) { create(:project, :public, :repository) }
let(:user) { project.creator }
let(:merge_request) do
create(:merge_request_with_diffs, source_project: project,
author: user,
- title: 'WIP: Bug NS-04',
+ title: 'Draft: Bug NS-04',
merge_params: { force_remove_source_branch: '1' })
end
diff --git a/spec/features/merge_request/user_reviews_image_spec.rb b/spec/features/merge_request/user_reviews_image_spec.rb
index 533f3c9c91a..bd490294829 100644
--- a/spec/features/merge_request/user_reviews_image_spec.rb
+++ b/spec/features/merge_request/user_reviews_image_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'Merge request > image review', :js do
include MergeRequestDiffHelpers
include RepoHelpers
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:project) { create(:project, :repository) }
let(:merge_request) { create(:merge_request_with_diffs, :with_image_diffs, source_project: project, author: user) }
diff --git a/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb b/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb
index 64cd5aa2bb1..33c5a936b8d 100644
--- a/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb
+++ b/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb
@@ -1,9 +1,12 @@
# frozen_string_literal: true
require 'spec_helper'
+include Spec::Support::Helpers::ModalHelpers # rubocop:disable Style/MixinUsage
RSpec.describe 'Merge request > User sees avatars on diff notes', :js do
include NoteInteractionHelpers
+ include Spec::Support::Helpers::ModalHelpers
+ include MergeRequestDiffHelpers
let(:project) { create(:project, :public, :repository) }
let(:user) { project.creator }
@@ -121,8 +124,8 @@ RSpec.describe 'Merge request > User sees avatars on diff notes', :js do
it 'removes avatar when note is deleted' do
open_more_actions_dropdown(note)
- page.within find(".note-row-#{note.id}") do
- accept_confirm { find('.js-note-delete').click }
+ accept_gl_confirm(button_text: 'Delete Comment') do
+ find(".note-row-#{note.id} .js-note-delete").click
end
wait_for_requests
@@ -133,6 +136,7 @@ RSpec.describe 'Merge request > User sees avatars on diff notes', :js do
end
it 'adds avatar when commenting' do
+ find_by_scrolling('[data-discussion-id]', match: :first)
find_field('Reply…', match: :first).click
page.within '.js-discussion-note-form' do
@@ -152,6 +156,7 @@ RSpec.describe 'Merge request > User sees avatars on diff notes', :js do
it 'adds multiple comments' do
3.times do
+ find_by_scrolling('[data-discussion-id]', match: :first)
find_field('Reply…', match: :first).click
page.within '.js-discussion-note-form' do
@@ -190,7 +195,7 @@ RSpec.describe 'Merge request > User sees avatars on diff notes', :js do
end
def find_line(line_code)
- line = find("[id='#{line_code}']")
+ line = find_by_scrolling("[id='#{line_code}']")
line = line.find(:xpath, 'preceding-sibling::*[1][self::td]/preceding-sibling::*[1][self::td]') if line.tag_name == 'td'
line
end
diff --git a/spec/features/merge_request/user_sees_deleted_target_branch_spec.rb b/spec/features/merge_request/user_sees_deleted_target_branch_spec.rb
index 7c93952ee99..dc50c3bc8db 100644
--- a/spec/features/merge_request/user_sees_deleted_target_branch_spec.rb
+++ b/spec/features/merge_request/user_sees_deleted_target_branch_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe 'Merge request > User sees deleted target branch', :js do
end
it 'shows a message about missing target branch' do
- expect(page).to have_content('Target branch does not exist')
+ expect(page).to have_content('The target branch feature does not exist')
end
it 'does not show link to target branch' do
diff --git a/spec/features/merge_request/user_sees_deployment_widget_spec.rb b/spec/features/merge_request/user_sees_deployment_widget_spec.rb
index 345404cc28f..01cc58777ba 100644
--- a/spec/features/merge_request/user_sees_deployment_widget_spec.rb
+++ b/spec/features/merge_request/user_sees_deployment_widget_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'Merge request > User sees deployment widget', :js do
+ include Spec::Support::Helpers::ModalHelpers
+
describe 'when merge request has associated environments' do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
@@ -118,7 +120,9 @@ RSpec.describe 'Merge request > User sees deployment widget', :js do
end
it 'does start build when stop button clicked' do
- accept_confirm { find('.js-stop-env').click }
+ accept_gl_confirm(button_text: 'Stop environment') do
+ find('.js-stop-env').click
+ end
expect(page).to have_content('close_app')
end
diff --git a/spec/features/merge_request/user_sees_diff_spec.rb b/spec/features/merge_request/user_sees_diff_spec.rb
index a7713ed9964..7cd9ef80874 100644
--- a/spec/features/merge_request/user_sees_diff_spec.rb
+++ b/spec/features/merge_request/user_sees_diff_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe 'Merge request > User sees diff', :js do
include ProjectForksHelper
include RepoHelpers
+ include MergeRequestDiffHelpers
let(:project) { create(:project, :public, :repository) }
let(:merge_request) { create(:merge_request, source_project: project) }
@@ -58,12 +59,12 @@ RSpec.describe 'Merge request > User sees diff', :js do
let(:changelog_id) { Digest::SHA1.hexdigest("CHANGELOG") }
context 'as author' do
- it 'shows direct edit link', :sidekiq_might_not_need_inline do
+ it 'contains direct edit link', :sidekiq_might_not_need_inline do
sign_in(author_user)
visit diffs_project_merge_request_path(project, merge_request)
# Throws `Capybara::Poltergeist::InvalidSelector` if we try to use `#hash` syntax
- expect(page).to have_selector("[id=\"#{changelog_id}\"] .js-edit-blob", visible: false)
+ expect(page).to have_selector(".js-edit-blob", visible: false)
end
end
@@ -72,6 +73,8 @@ RSpec.describe 'Merge request > User sees diff', :js do
sign_in(user)
visit diffs_project_merge_request_path(project, merge_request)
+ find_by_scrolling("[id=\"#{changelog_id}\"]")
+
# Throws `Capybara::Poltergeist::InvalidSelector` if we try to use `#hash` syntax
find("[id=\"#{changelog_id}\"] .js-diff-more-actions").click
find("[id=\"#{changelog_id}\"] .js-edit-blob").click
@@ -82,7 +85,7 @@ RSpec.describe 'Merge request > User sees diff', :js do
end
context 'when file contains html' do
- let(:current_user) { project.owner }
+ let(:current_user) { project.first_owner }
let(:branch_name) {"test_branch"}
it 'escapes any HTML special characters in the diff chunk header' do
@@ -107,6 +110,7 @@ RSpec.describe 'Merge request > User sees diff', :js do
CONTENT
file_name = 'xss_file.rs'
+ file_hash = Digest::SHA1.hexdigest(file_name)
create_file('master', file_name, file_content)
merge_request = create(:merge_request, source_project: project)
@@ -116,6 +120,8 @@ RSpec.describe 'Merge request > User sees diff', :js do
visit diffs_project_merge_request_path(project, merge_request)
+ find_by_scrolling("[id='#{file_hash}']")
+
expect(page).to have_text("function foo<input> {")
expect(page).to have_css(".line[lang='rust'] .k")
end
@@ -123,7 +129,7 @@ RSpec.describe 'Merge request > User sees diff', :js do
context 'when file is stored in LFS' do
let(:merge_request) { create(:merge_request, source_project: project) }
- let(:current_user) { project.owner }
+ let(:current_user) { project.first_owner }
context 'when LFS is enabled on the project' do
before do
@@ -136,7 +142,7 @@ RSpec.describe 'Merge request > User sees diff', :js do
end
context 'when file is an image', :js do
- let(:file_name) { 'files/lfs/image.png' }
+ let(:file_name) { 'a/image.png' }
it 'shows an error message' do
expect(page).not_to have_content('could not be displayed because it is stored in LFS')
@@ -144,7 +150,7 @@ RSpec.describe 'Merge request > User sees diff', :js do
end
context 'when file is not an image' do
- let(:file_name) { 'files/lfs/ruby.rb' }
+ let(:file_name) { 'a/ruby.rb' }
it 'shows an error message' do
expect(page).to have_content('This source diff could not be displayed because it is stored in LFS')
@@ -153,7 +159,14 @@ RSpec.describe 'Merge request > User sees diff', :js do
end
context 'when LFS is not enabled' do
+ let(:file_name) { 'a/lfs_object.iso' }
+
before do
+ allow(Gitlab.config.lfs).to receive(:disabled).and_return(true)
+ project.update_attribute(:lfs_enabled, false)
+
+ create_file('master', file_name, project.repository.blob_at('master', 'files/lfs/lfs_object.iso').data)
+
visit diffs_project_merge_request_path(project, merge_request)
end
diff --git a/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb b/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb
index 2a49109d360..b77b3d69fc1 100644
--- a/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb
@@ -64,7 +64,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
it 'sees branch pipelines and detached merge request pipelines in correct order' do
page.within('.ci-table') do
expect(page).to have_selector('.ci-created', count: 2)
- expect(first('[data-testid="pipeline-url-link"]')).to have_content("##{detached_merge_request_pipeline.id}")
+ expect(first('[data-testid="pipeline-identifier"]')).to have_content("##{detached_merge_request_pipeline.id}")
end
end
@@ -101,16 +101,16 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
page.within('.ci-table') do
expect(page).to have_selector('.ci-pending', count: 4)
- expect(all('[data-testid="pipeline-url-link"]')[0])
+ expect(all('[data-testid="pipeline-identifier"]')[0])
.to have_content("##{detached_merge_request_pipeline_2.id}")
- expect(all('[data-testid="pipeline-url-link"]')[1])
+ expect(all('[data-testid="pipeline-identifier"]')[1])
.to have_content("##{detached_merge_request_pipeline.id}")
- expect(all('[data-testid="pipeline-url-link"]')[2])
+ expect(all('[data-testid="pipeline-identifier"]')[2])
.to have_content("##{push_pipeline_2.id}")
- expect(all('[data-testid="pipeline-url-link"]')[3])
+ expect(all('[data-testid="pipeline-identifier"]')[3])
.to have_content("##{push_pipeline.id}")
end
end
@@ -201,7 +201,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
it 'sees a branch pipeline in pipeline tab' do
page.within('.ci-table') do
expect(page).to have_selector('.ci-created', count: 1)
- expect(first('[data-testid="pipeline-url-link"]')).to have_content("##{push_pipeline.id}")
+ expect(first('[data-testid="pipeline-identifier"]')).to have_content("##{push_pipeline.id}")
end
end
@@ -252,7 +252,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
it 'sees branch pipelines and detached merge request pipelines in correct order' do
page.within('.ci-table') do
expect(page).to have_selector('.ci-pending', count: 2)
- expect(first('[data-testid="pipeline-url-link"]')).to have_content("##{detached_merge_request_pipeline.id}")
+ expect(first('[data-testid="pipeline-identifier"]')).to have_content("##{detached_merge_request_pipeline.id}")
end
end
@@ -295,16 +295,16 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
page.within('.ci-table') do
expect(page).to have_selector('.ci-pending', count: 4)
- expect(all('[data-testid="pipeline-url-link"]')[0])
+ expect(all('[data-testid="pipeline-identifier"]')[0])
.to have_content("##{detached_merge_request_pipeline_2.id}")
- expect(all('[data-testid="pipeline-url-link"]')[1])
+ expect(all('[data-testid="pipeline-identifier"]')[1])
.to have_content("##{detached_merge_request_pipeline.id}")
- expect(all('[data-testid="pipeline-url-link"]')[2])
+ expect(all('[data-testid="pipeline-identifier"]')[2])
.to have_content("##{push_pipeline_2.id}")
- expect(all('[data-testid="pipeline-url-link"]')[3])
+ expect(all('[data-testid="pipeline-identifier"]')[3])
.to have_content("##{push_pipeline.id}")
end
end
diff --git a/spec/features/merge_request/user_sees_merge_widget_spec.rb b/spec/features/merge_request/user_sees_merge_widget_spec.rb
index 8761ee89463..872507c3b7a 100644
--- a/spec/features/merge_request/user_sees_merge_widget_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb
@@ -104,10 +104,11 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
visit project_merge_request_path(project, merge_request)
end
- it 'has danger button while waiting for external CI status' do
+ it 'has merge button with confirm variant while waiting for external CI status' do
# Wait for the `ci_status` and `merge_check` requests
wait_for_requests
- expect(page).to have_selector('.accept-merge-request.btn-danger')
+
+ expect(page).to have_selector('.accept-merge-request.btn-confirm')
end
end
@@ -125,10 +126,27 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
visit project_merge_request_path(project, merge_request)
end
- it 'has danger button when not succeeded' do
+ it 'has merge button that shows modal when pipeline does not succeeded' do
# Wait for the `ci_status` and `merge_check` requests
wait_for_requests
- expect(page).to have_selector('.accept-merge-request.btn-danger')
+
+ click_button 'Merge...'
+
+ expect(page).to have_selector('[data-testid="merge-failed-pipeline-confirmation-dialog"]', visible: true)
+ end
+
+ it 'allows me to merge with a failed pipeline' do
+ modal_selector = '[data-testid="merge-failed-pipeline-confirmation-dialog"]'
+
+ wait_for_requests
+
+ click_button 'Merge...'
+
+ page.within(modal_selector) do
+ click_button 'Merge unverified changes'
+ end
+
+ expect(find('.media-body h4')).to have_content('Merging!')
end
end
diff --git a/spec/features/merge_request/user_sees_mr_with_deleted_source_branch_spec.rb b/spec/features/merge_request/user_sees_mr_with_deleted_source_branch_spec.rb
index e997fb3e853..39bba3f2f73 100644
--- a/spec/features/merge_request/user_sees_mr_with_deleted_source_branch_spec.rb
+++ b/spec/features/merge_request/user_sees_mr_with_deleted_source_branch_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe 'Merge request > User sees MR with deleted source branch', :js do
end
it 'shows a message about missing source branch' do
- expect(page).to have_content('Source branch does not exist.')
+ expect(page).to have_content('The source branch this-branch-does-not-exist does not exist.')
end
it 'still contains Discussion, Commits and Changes tabs' do
@@ -27,7 +27,7 @@ RSpec.describe 'Merge request > User sees MR with deleted source branch', :js do
expect(page).to have_content('Changes')
end
- expect(page).to have_content('Source branch does not exist.')
+ expect(page).to have_content('The source branch this-branch-does-not-exist does not exist.')
click_on 'Changes'
wait_for_requests
diff --git a/spec/features/merge_request/user_sees_pipelines_spec.rb b/spec/features/merge_request/user_sees_pipelines_spec.rb
index 4967f58528e..a356dd50898 100644
--- a/spec/features/merge_request/user_sees_pipelines_spec.rb
+++ b/spec/features/merge_request/user_sees_pipelines_spec.rb
@@ -125,6 +125,7 @@ RSpec.describe 'Merge request > User sees pipelines', :js do
before do
stub_feature_flags(ci_disallow_to_create_merge_request_pipelines_in_target_project: false)
+ stub_feature_flags(rearrange_pipelines_table: false)
end
it 'creates a pipeline in the parent project when user proceeds with the warning' do
@@ -133,7 +134,7 @@ RSpec.describe 'Merge request > User sees pipelines', :js do
create_merge_request_pipeline
act_on_security_warning(action: 'Run pipeline')
- check_pipeline(expected_project: parent_project)
+ check_pipeline(expected_project: parent_project, link_selector: 'pipeline-url-link')
check_head_pipeline(expected_project: parent_project)
end
@@ -178,13 +179,13 @@ RSpec.describe 'Merge request > User sees pipelines', :js do
click_button('Run pipeline')
end
- def check_pipeline(expected_project:)
+ def check_pipeline(expected_project:, link_selector: 'commit-title')
page.within('.ci-table') do
expect(page).to have_selector('.commit', count: 2)
page.within(first('.commit')) do
page.within('.pipeline-tags') do
- expect(page.find('[data-testid="pipeline-url-link"]')[:href]).to include(expected_project.full_path)
+ expect(page.find("[data-testid=#{link_selector}]")[:href]).to include(expected_project.full_path)
expect(page).to have_content('detached')
end
page.within('.pipeline-triggerer') do
diff --git a/spec/features/merge_request/user_sees_versions_spec.rb b/spec/features/merge_request/user_sees_versions_spec.rb
index 5abf4e2f5ad..2b856811e02 100644
--- a/spec/features/merge_request/user_sees_versions_spec.rb
+++ b/spec/features/merge_request/user_sees_versions_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'Merge request > User sees versions', :js do
+ include MergeRequestDiffHelpers
+
let(:merge_request) do
create(:merge_request).tap do |mr|
mr.merge_request_diff.destroy!
@@ -27,8 +29,12 @@ RSpec.describe 'Merge request > User sees versions', :js do
diff_file_selector = ".diff-file[id='#{file_id}']"
line_code = "#{file_id}_#{line_code}"
- page.within(diff_file_selector) do
- first("[id='#{line_code}']").hover
+ page.within find_by_scrolling(diff_file_selector) do
+ line_code_element = first("[id='#{line_code}']")
+ # scrolling to element's bottom is required in order for .hover action to work
+ # otherwise, the element could be hidden underneath a sticky header
+ scroll_to_elements_bottom(line_code_element)
+ line_code_element.hover
first("[id='#{line_code}'] [role='button']").click
page.within("form[data-line-code='#{line_code}']") do
diff --git a/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb b/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb
index 690a292937a..beb658bb7a0 100644
--- a/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb
+++ b/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb
@@ -34,7 +34,7 @@ RSpec.describe 'User comments on a diff', :js do
context 'single suggestion note' do
it 'hides suggestion popover' do
- click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']"))
+ click_diff_line(find_by_scrolling("[id='#{sample_compare.changes[1][:line_code]}']"))
expect(page).to have_selector('.diff-suggest-popover')
@@ -46,7 +46,7 @@ RSpec.describe 'User comments on a diff', :js do
end
it 'suggestion is presented' do
- click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']"))
+ click_diff_line(find_by_scrolling("[id='#{sample_compare.changes[1][:line_code]}']"))
page.within('.js-discussion-note-form') do
fill_in('note_note', with: "```suggestion\n# change to a comment\n```")
@@ -74,7 +74,7 @@ RSpec.describe 'User comments on a diff', :js do
end
it 'allows suggestions in replies' do
- click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']"))
+ click_diff_line(find_by_scrolling("[id='#{sample_compare.changes[1][:line_code]}']"))
page.within('.js-discussion-note-form') do
fill_in('note_note', with: "```suggestion\n# change to a comment\n```")
@@ -91,7 +91,7 @@ RSpec.describe 'User comments on a diff', :js do
end
it 'suggestion is appliable' do
- click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']"))
+ click_diff_line(find_by_scrolling("[id='#{sample_compare.changes[1][:line_code]}']"))
page.within('.js-discussion-note-form') do
fill_in('note_note', with: "```suggestion\n# change to a comment\n```")
@@ -273,7 +273,7 @@ RSpec.describe 'User comments on a diff', :js do
context 'multiple suggestions in a single note' do
it 'suggestions are presented', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/258989' do
- click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']"))
+ click_diff_line(find_by_scrolling("[id='#{sample_compare.changes[1][:line_code]}']"))
page.within('.js-discussion-note-form') do
fill_in('note_note', with: "```suggestion\n# change to a comment\n```\n```suggestion:-2\n# or that\n# heh\n```")
@@ -316,7 +316,7 @@ RSpec.describe 'User comments on a diff', :js do
context 'multi-line suggestions' do
before do
- click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']"))
+ click_diff_line(find_by_scrolling("[id='#{sample_compare.changes[1][:line_code]}']"))
page.within('.js-discussion-note-form') do
fill_in('note_note', with: "```suggestion:-3+5\n# change to a\n# comment\n# with\n# broken\n# lines\n```")
diff --git a/spec/features/merge_request/user_views_merge_request_from_deleted_fork_spec.rb b/spec/features/merge_request/user_views_merge_request_from_deleted_fork_spec.rb
index 370341a43f9..e3272a6e280 100644
--- a/spec/features/merge_request/user_views_merge_request_from_deleted_fork_spec.rb
+++ b/spec/features/merge_request/user_views_merge_request_from_deleted_fork_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe 'User views merged merge request from deleted fork' do
let(:project) { create(:project, :repository) }
let(:source_project) { fork_project(project, nil, repository: true) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let!(:merge_request) { create(:merge_request, :merged, source_project: source_project, target_project: project) }
before do
diff --git a/spec/features/merge_request/user_views_open_merge_request_spec.rb b/spec/features/merge_request/user_views_open_merge_request_spec.rb
index b5a973a53c0..a145bcb976b 100644
--- a/spec/features/merge_request/user_views_open_merge_request_spec.rb
+++ b/spec/features/merge_request/user_views_open_merge_request_spec.rb
@@ -128,4 +128,30 @@ RSpec.describe 'User views an open merge request' do
expect(find("[data-testid='ref-name']")[:title]).to eq(source_branch)
end
end
+
+ context 'when user preferred language has changed', :use_clean_rails_memory_store_fragment_caching do
+ let(:project) { create(:project, :public, :repository) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_maintainer(user)
+ sign_in(user)
+ end
+
+ it 'renders edit button in preferred language' do
+ visit(merge_request_path(merge_request))
+
+ page.within('.detail-page-header-actions') do
+ expect(page).to have_link('Edit')
+ end
+
+ user.update!(preferred_language: 'de')
+
+ visit(merge_request_path(merge_request))
+
+ page.within('.detail-page-header-actions') do
+ expect(page).to have_link('Bearbeiten')
+ end
+ end
+ end
end
diff --git a/spec/features/merge_requests/user_mass_updates_spec.rb b/spec/features/merge_requests/user_mass_updates_spec.rb
index 46c12784ea8..f781ba0827c 100644
--- a/spec/features/merge_requests/user_mass_updates_spec.rb
+++ b/spec/features/merge_requests/user_mass_updates_spec.rb
@@ -8,6 +8,8 @@ RSpec.describe 'Merge requests > User mass updates', :js do
let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
before do
+ stub_feature_flags(mr_attention_requests: false)
+
project.add_maintainer(user)
sign_in(user)
end
@@ -59,6 +61,18 @@ RSpec.describe 'Merge requests > User mass updates', :js do
expect(find('.merge-request')).to have_link "Assigned to #{user.name}"
end
+
+ describe 'with attention requests feature flag on' do
+ before do
+ stub_feature_flags(mr_attention_requests: true)
+ end
+
+ it 'updates merge request with assignee' do
+ change_assignee(user.name)
+
+ expect(find('.issuable-meta a.author-link')[:title]).to eq "Attention requested from assignee #{user.name}, go to their profile."
+ end
+ end
end
describe 'remove assignee' do
diff --git a/spec/features/monitor_sidebar_link_spec.rb b/spec/features/monitor_sidebar_link_spec.rb
index bb5e581a034..fcef0fa0eff 100644
--- a/spec/features/monitor_sidebar_link_spec.rb
+++ b/spec/features/monitor_sidebar_link_spec.rb
@@ -117,9 +117,8 @@ RSpec.describe 'Monitor dropdown sidebar', :aggregate_failures do
expect(page).to have_link('Error Tracking', href: project_error_tracking_index_path(project))
expect(page).to have_link('Product Analytics', href: project_product_analytics_path(project))
expect(page).to have_link('Logs', href: project_logs_path(project))
-
- expect(page).not_to have_link('Serverless', href: project_serverless_functions_path(project))
- expect(page).not_to have_link('Kubernetes', href: project_clusters_path(project))
+ expect(page).to have_link('Serverless', href: project_serverless_functions_path(project))
+ expect(page).to have_link('Kubernetes', href: project_clusters_path(project))
end
it_behaves_like 'shows Monitor menu based on the access level'
diff --git a/spec/features/participants_autocomplete_spec.rb b/spec/features/participants_autocomplete_spec.rb
index cc805e7d369..b2739454b52 100644
--- a/spec/features/participants_autocomplete_spec.rb
+++ b/spec/features/participants_autocomplete_spec.rb
@@ -33,31 +33,12 @@ RSpec.describe 'Member autocomplete', :js do
let(:noteable) { create(:issue, author: author, project: project) }
before do
- stub_feature_flags(tribute_autocomplete: false)
visit project_issue_path(project, noteable)
end
include_examples "open suggestions when typing @", 'issue'
end
- describe 'when tribute_autocomplete feature flag is on' do
- context 'adding a new note on a Issue' do
- let(:noteable) { create(:issue, author: author, project: project) }
-
- before do
- stub_feature_flags(tribute_autocomplete: true)
- visit project_issue_path(project, noteable)
-
- fill_in 'Comment', with: '@'
- end
-
- it 'suggests noteable author and note author' do
- expect(find_tribute_autocomplete_menu).to have_content(author.username)
- expect(find_tribute_autocomplete_menu).to have_content(note.author.username)
- end
- end
- end
-
context 'adding a new note on a Merge Request' do
let(:noteable) do
create(:merge_request, source_project: project,
@@ -91,8 +72,4 @@ RSpec.describe 'Member autocomplete', :js do
def find_autocomplete_menu
find('.atwho-view ul', visible: true)
end
-
- def find_tribute_autocomplete_menu
- find('.tribute-container ul', visible: true)
- end
end
diff --git a/spec/features/profiles/keys_spec.rb b/spec/features/profiles/keys_spec.rb
index b9e59a0239b..fde85a731a1 100644
--- a/spec/features/profiles/keys_spec.rb
+++ b/spec/features/profiles/keys_spec.rb
@@ -49,7 +49,12 @@ RSpec.describe 'Profile > SSH Keys' do
context 'when only DSA and ECDSA keys are allowed' do
before do
forbidden = ApplicationSetting::FORBIDDEN_KEY_VALUE
- stub_application_setting(rsa_key_restriction: forbidden, ed25519_key_restriction: forbidden)
+ stub_application_setting(
+ rsa_key_restriction: forbidden,
+ ed25519_key_restriction: forbidden,
+ ecdsa_sk_key_restriction: forbidden,
+ ed25519_sk_key_restriction: forbidden
+ )
end
it 'shows a validation error' do
diff --git a/spec/features/profiles/password_spec.rb b/spec/features/profiles/password_spec.rb
index 25fe43617fd..898e2c2aa59 100644
--- a/spec/features/profiles/password_spec.rb
+++ b/spec/features/profiles/password_spec.rb
@@ -41,7 +41,7 @@ RSpec.describe 'Profile > Password' do
it 'shows a success message' do
fill_passwords(Gitlab::Password.test_default, Gitlab::Password.test_default)
- page.within('.flash-notice') do
+ page.within('[data-testid="alert-info"]') do
expect(page).to have_content('Password was successfully updated. Please sign in again.')
end
end
diff --git a/spec/features/projects/active_tabs_spec.rb b/spec/features/projects/active_tabs_spec.rb
index b8c928004ed..2601dcf55c9 100644
--- a/spec/features/projects/active_tabs_spec.rb
+++ b/spec/features/projects/active_tabs_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'Project active tab' do
let_it_be(:project) { create(:project, :repository) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
before do
sign_in(user)
diff --git a/spec/features/projects/activity/rss_spec.rb b/spec/features/projects/activity/rss_spec.rb
index 9012b335bf4..a3e511b5c22 100644
--- a/spec/features/projects/activity/rss_spec.rb
+++ b/spec/features/projects/activity/rss_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe 'Project Activity RSS' do
let(:project) { create(:project, :public) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:path) { activity_project_path(project) }
before do
@@ -13,7 +13,7 @@ RSpec.describe 'Project Activity RSS' do
context 'when signed in' do
before do
- sign_in(project.owner)
+ sign_in(project.first_owner)
visit path
end
diff --git a/spec/features/projects/blobs/blob_show_spec.rb b/spec/features/projects/blobs/blob_show_spec.rb
index 62994d19fc0..77194fd6ca1 100644
--- a/spec/features/projects/blobs/blob_show_spec.rb
+++ b/spec/features/projects/blobs/blob_show_spec.rb
@@ -29,443 +29,421 @@ RSpec.describe 'File blob', :js do
).execute
end
- before do
- stub_feature_flags(refactor_blob_viewer: false) # This stub will be removed in https://gitlab.com/gitlab-org/gitlab/-/issues/350455
- end
-
- context 'Ruby file' do
- before do
- visit_blob('files/ruby/popen.rb')
-
- wait_for_requests
- end
-
- it 'displays the blob' do
- aggregate_failures do
- # shows highlighted Ruby code
- expect(page).to have_css(".js-syntax-highlight")
- expect(page).to have_content("require 'fileutils'")
-
- # does not show a viewer switcher
- expect(page).not_to have_selector('.js-blob-viewer-switcher')
-
- # shows an enabled copy button
- expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
-
- # shows a raw button
- expect(page).to have_link('Open raw')
- end
- end
-
- it 'displays file actions on all screen sizes' do
- file_actions_selector = '.file-actions'
-
- resize_screen_sm
- expect(page).to have_selector(file_actions_selector, visible: true)
-
- resize_screen_xs
- expect(page).to have_selector(file_actions_selector, visible: true)
- end
- end
-
- context 'Markdown file' do
- context 'visiting directly' do
+ context 'with refactor_blob_viewer feature flag enabled' do
+ context 'Ruby file' do
before do
- visit_blob('files/markdown/ruby-style-guide.md')
+ visit_blob('files/ruby/popen.rb')
wait_for_requests
end
- it 'displays the blob using the rich viewer' do
+ it 'displays the blob' do
aggregate_failures do
- # hides the simple viewer
- expect(page).to have_selector('.blob-viewer[data-type="simple"]', visible: false)
- expect(page).to have_selector('.blob-viewer[data-type="rich"]')
-
- # shows rendered Markdown
- expect(page).to have_link("PEP-8")
+ # shows highlighted Ruby code
+ expect(page).to have_css(".js-syntax-highlight")
+ expect(page).to have_content("require 'fileutils'")
- # shows a viewer switcher
- expect(page).to have_selector('.js-blob-viewer-switcher')
+ # does not show a viewer switcher
+ expect(page).not_to have_selector('.js-blob-viewer-switcher')
- # shows a disabled copy button
- expect(page).to have_selector('.js-copy-blob-source-btn.disabled')
+ # shows an enabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
# shows a raw button
expect(page).to have_link('Open raw')
end
end
- context 'switching to the simple viewer' do
+ it 'displays file actions on all screen sizes' do
+ file_actions_selector = '.file-actions'
+
+ resize_screen_sm
+ expect(page).to have_selector(file_actions_selector, visible: true)
+
+ resize_screen_xs
+ expect(page).to have_selector(file_actions_selector, visible: true)
+ end
+ end
+
+ context 'Markdown file' do
+ context 'visiting directly' do
before do
- find('.js-blob-viewer-switch-btn[data-viewer=simple]').click
+ visit_blob('files/markdown/ruby-style-guide.md')
wait_for_requests
end
- it 'displays the blob using the simple viewer' do
+ it 'displays the blob using the rich viewer' do
aggregate_failures do
- # hides the rich viewer
- expect(page).to have_selector('.blob-viewer[data-type="simple"]')
- expect(page).to have_selector('.blob-viewer[data-type="rich"]', visible: false)
+ # hides the simple viewer
+ expect(page).not_to have_selector('.blob-viewer[data-type="simple"]')
+ expect(page).to have_selector('.blob-viewer[data-type="rich"]')
- # shows highlighted Markdown code
- expect(page).to have_css(".js-syntax-highlight")
- expect(page).to have_content("[PEP-8](http://www.python.org/dev/peps/pep-0008/)")
+ # shows rendered Markdown
+ expect(page).to have_link("PEP-8")
- # shows an enabled copy button
- expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+ # shows a viewer switcher
+ expect(page).to have_selector('.js-blob-viewer-switcher')
+
+ # shows a disabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn.disabled')
+
+ # shows a raw button
+ expect(page).to have_link('Open raw')
end
end
- context 'switching to the rich viewer again' do
+ context 'switching to the simple viewer' do
before do
- find('.js-blob-viewer-switch-btn[data-viewer=rich]').click
+ find('.js-blob-viewer-switch-btn[data-viewer=simple]').click
wait_for_requests
end
- it 'displays the blob using the rich viewer' do
+ it 'displays the blob using the simple viewer' do
aggregate_failures do
- # hides the simple viewer
- expect(page).to have_selector('.blob-viewer[data-type="simple"]', visible: false)
- expect(page).to have_selector('.blob-viewer[data-type="rich"]')
+ # hides the rich viewer
+ expect(page).to have_selector('.blob-viewer[data-type="simple"]')
+ expect(page).not_to have_selector('.blob-viewer[data-type="rich"]')
+
+ # shows highlighted Markdown code
+ expect(page).to have_css(".js-syntax-highlight")
+ expect(page).to have_content("[PEP-8](http://www.python.org/dev/peps/pep-0008/)")
# shows an enabled copy button
expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
end
end
- end
- end
- end
- context 'when ref switch' do
- def switch_ref_to(ref_name)
- first('.qa-branches-select').click # rubocop:disable QA/SelectorUsage
-
- page.within '.project-refs-form' do
- click_link ref_name
- wait_for_requests
- end
- end
+ context 'switching to the rich viewer again' do
+ before do
+ find('.js-blob-viewer-switch-btn[data-viewer=rich]').click
- it 'displays single highlighted line number of different ref' do
- visit_blob('files/js/application.js', anchor: 'L1')
+ wait_for_requests
+ end
- switch_ref_to('feature')
+ it 'displays the blob using the rich viewer' do
+ aggregate_failures do
+ # hides the simple viewer
+ expect(page).not_to have_selector('.blob-viewer[data-type="simple"]')
+ expect(page).to have_selector('.blob-viewer[data-type="rich"]')
- page.within '.blob-content' do
- expect(find_by_id('LC1')[:class]).to include("hll")
+ # shows a disabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn.disabled')
+ end
+ end
+ end
end
end
- it 'displays multiple highlighted line numbers of different ref' do
- visit_blob('files/js/application.js', anchor: 'L1-3')
+ context 'when ref switch' do
+ def switch_ref_to(ref_name)
+ first('.qa-branches-select').click # rubocop:disable QA/SelectorUsage
- switch_ref_to('feature')
-
- page.within '.blob-content' do
- expect(find_by_id('LC1')[:class]).to include("hll")
- expect(find_by_id('LC2')[:class]).to include("hll")
- expect(find_by_id('LC3')[:class]).to include("hll")
+ page.within '.project-refs-form' do
+ click_link ref_name
+ wait_for_requests
+ end
end
- end
- it 'displays no highlighted number of different ref' do
- Files::UpdateService.new(
- project,
- project.owner,
- commit_message: 'Update',
- start_branch: 'feature',
- branch_name: 'feature',
- file_path: 'files/js/application.js',
- file_content: 'new content'
- ).execute
+ it 'displays no highlighted number of different ref' do
+ Files::UpdateService.new(
+ project,
+ project.first_owner,
+ commit_message: 'Update',
+ start_branch: 'feature',
+ branch_name: 'feature',
+ file_path: 'files/js/application.js',
+ file_content: 'new content'
+ ).execute
- project.commit('feature').diffs.diff_files.first
+ project.commit('feature').diffs.diff_files.first
- visit_blob('files/js/application.js', anchor: 'L3')
- switch_ref_to('feature')
+ visit_blob('files/js/application.js', anchor: 'L3')
+ switch_ref_to('feature')
- page.within '.blob-content' do
- expect(page).not_to have_css('.hll')
+ page.within '.blob-content' do
+ expect(page).not_to have_css('.hll')
+ end
end
- end
- context 'successfully change ref of similar name' do
- before do
- project.repository.create_branch('dev')
- project.repository.create_branch('development')
- end
+ context 'successfully change ref of similar name' do
+ before do
+ project.repository.create_branch('dev')
+ project.repository.create_branch('development')
+ end
- it 'switch ref from longer to shorter ref name' do
- visit_blob('files/js/application.js', ref: 'development')
- switch_ref_to('dev')
+ it 'switch ref from longer to shorter ref name' do
+ visit_blob('files/js/application.js', ref: 'development')
+ switch_ref_to('dev')
- aggregate_failures do
- expect(page.find('.file-title-name').text).to eq('application.js')
- expect(page).not_to have_css('flash-container')
+ aggregate_failures do
+ expect(page.find('.file-title-name').text).to eq('application.js')
+ expect(page).not_to have_css('flash-container')
+ end
end
- end
- it 'switch ref from shorter to longer ref name' do
- visit_blob('files/js/application.js', ref: 'dev')
- switch_ref_to('development')
+ it 'switch ref from shorter to longer ref name' do
+ visit_blob('files/js/application.js', ref: 'dev')
+ switch_ref_to('development')
- aggregate_failures do
- expect(page.find('.file-title-name').text).to eq('application.js')
- expect(page).not_to have_css('flash-container')
+ aggregate_failures do
+ expect(page.find('.file-title-name').text).to eq('application.js')
+ expect(page).not_to have_css('flash-container')
+ end
end
end
- end
- it 'successfully changes ref when the ref name matches the project name' do
- project.repository.create_branch(project.name)
+ it 'successfully changes ref when the ref name matches the project name' do
+ project.repository.create_branch(project.name)
- visit_blob('files/js/application.js', ref: project.name)
- switch_ref_to('master')
+ visit_blob('files/js/application.js', ref: project.name)
+ switch_ref_to('master')
- aggregate_failures do
- expect(page.find('.file-title-name').text).to eq('application.js')
- expect(page).not_to have_css('flash-container')
+ aggregate_failures do
+ expect(page.find('.file-title-name').text).to eq('application.js')
+ expect(page).not_to have_css('flash-container')
+ end
end
end
end
- context 'visiting with a line number anchor' do
+ context 'Markdown rendering' do
before do
- visit_blob('files/markdown/ruby-style-guide.md', anchor: 'L1')
- end
+ project.add_maintainer(project.creator)
- it 'displays the blob using the simple viewer' do
- aggregate_failures do
- # hides the rich viewer
- expect(page).to have_selector('.blob-viewer[data-type="simple"]')
- expect(page).to have_selector('.blob-viewer[data-type="rich"]', visible: false)
+ Files::CreateService.new(
+ project,
+ project.creator,
+ start_branch: 'master',
+ branch_name: 'master',
+ commit_message: "Add RedCarpet and CommonMark Markdown ",
+ file_path: 'files/commonmark/file.md',
+ file_content: "1. one\n - sublist\n"
+ ).execute
+ end
- # highlights the line in question
- expect(page).to have_selector('#LC1.hll')
+ context 'when rendering default markdown' do
+ before do
+ visit_blob('files/commonmark/file.md')
- # shows highlighted Markdown code
- expect(page).to have_css(".js-syntax-highlight")
- expect(page).to have_content("[PEP-8](http://www.python.org/dev/peps/pep-0008/)")
+ wait_for_requests
+ end
- # shows an enabled copy button
- expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+ it 'renders using CommonMark' do
+ aggregate_failures do
+ expect(page).to have_content("sublist")
+ expect(page).not_to have_xpath("//ol//li//ul")
+ end
end
end
end
- end
-
- context 'Markdown rendering' do
- before do
- project.add_maintainer(project.creator)
-
- Files::CreateService.new(
- project,
- project.creator,
- start_branch: 'master',
- branch_name: 'master',
- commit_message: "Add RedCarpet and CommonMark Markdown ",
- file_path: 'files/commonmark/file.md',
- file_content: "1. one\n - sublist\n"
- ).execute
- end
- context 'when rendering default markdown' do
+ context 'Markdown file (stored in LFS)' do
before do
- visit_blob('files/commonmark/file.md')
-
- wait_for_requests
- end
+ project.add_maintainer(project.creator)
- it 'renders using CommonMark' do
- aggregate_failures do
- expect(page).to have_content("sublist")
- expect(page).not_to have_xpath("//ol//li//ul")
- end
+ Files::CreateService.new(
+ project,
+ project.creator,
+ start_branch: 'master',
+ branch_name: 'master',
+ commit_message: "Add Markdown in LFS",
+ file_path: 'files/lfs/file.md',
+ file_content: project.repository.blob_at('master', 'files/lfs/lfs_object.iso').data
+ ).execute
end
- end
- end
- context 'Markdown file (stored in LFS)' do
- before do
- project.add_maintainer(project.creator)
-
- Files::CreateService.new(
- project,
- project.creator,
- start_branch: 'master',
- branch_name: 'master',
- commit_message: "Add Markdown in LFS",
- file_path: 'files/lfs/file.md',
- file_content: project.repository.blob_at('master', 'files/lfs/lfs_object.iso').data
- ).execute
- end
-
- context 'when LFS is enabled on the project' do
- before do
- allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
- project.update_attribute(:lfs_enabled, true)
+ context 'when LFS is enabled on the project' do
+ before do
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+ project.update_attribute(:lfs_enabled, true)
- visit_blob('files/lfs/file.md')
+ visit_blob('files/lfs/file.md')
- wait_for_requests
- end
+ wait_for_requests
+ end
- it 'displays an error' do
- aggregate_failures do
- # hides the simple viewer
- expect(page).to have_selector('.blob-viewer[data-type="simple"]', visible: false)
- expect(page).to have_selector('.blob-viewer[data-type="rich"]')
+ it 'displays an error' do
+ aggregate_failures do
+ # hides the simple viewer
+ expect(page).not_to have_selector('.blob-viewer[data-type="simple"]')
+ expect(page).not_to have_selector('.blob-viewer[data-type="rich"]')
- # shows an error message
- expect(page).to have_content('The rendered file could not be displayed because it is stored in LFS. You can download it instead.')
+ # shows an error message
+ expect(page).to have_content('This content could not be displayed because it is stored in LFS. You can download it instead.')
- # shows a viewer switcher
- expect(page).to have_selector('.js-blob-viewer-switcher')
+ # does not show a viewer switcher
+ expect(page).not_to have_selector('.js-blob-viewer-switcher')
- # does not show a copy button
- expect(page).not_to have_selector('.js-copy-blob-source-btn')
+ # does not show a copy button
+ expect(page).not_to have_selector('.js-copy-blob-source-btn')
- # shows a download button
- expect(page).to have_link('Download')
+ # shows a download button
+ expect(page).to have_link('Download')
+ end
end
end
- context 'switching to the simple viewer' do
+ context 'when LFS is disabled on the project' do
before do
- find('.js-blob-viewer-switcher .js-blob-viewer-switch-btn[data-viewer=simple]').click
+ visit_blob('files/lfs/file.md')
wait_for_requests
end
- it 'displays an error' do
+ it 'displays the blob' do
aggregate_failures do
- # hides the rich viewer
- expect(page).to have_selector('.blob-viewer[data-type="simple"]')
- expect(page).to have_selector('.blob-viewer[data-type="rich"]', visible: false)
+ # shows text
+ expect(page).to have_content('size 1575078')
- # shows an error message
- expect(page).to have_content('The source could not be displayed because it is stored in LFS. You can download it instead.')
+ # does not show a viewer switcher
+ expect(page).not_to have_selector('.js-blob-viewer-switcher')
- # does not show a copy button
- expect(page).not_to have_selector('.js-copy-blob-source-btn')
+ # shows an enabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+
+ # shows a raw button
+ expect(page).to have_link('Open raw')
end
end
end
end
- context 'when LFS is disabled on the project' do
+ context 'PDF file' do
before do
- visit_blob('files/lfs/file.md')
+ project.add_maintainer(project.creator)
+
+ Files::CreateService.new(
+ project,
+ project.creator,
+ start_branch: 'master',
+ branch_name: 'master',
+ commit_message: "Add PDF",
+ file_path: 'files/test.pdf',
+ file_content: project.repository.blob_at('add-pdf-file', 'files/pdf/test.pdf').data
+ ).execute
+
+ visit_blob('files/test.pdf')
wait_for_requests
end
it 'displays the blob' do
aggregate_failures do
- # shows text
- expect(page).to have_content('size 1575078')
+ # shows rendered PDF
+ expect(page).to have_selector('.js-pdf-viewer')
# does not show a viewer switcher
expect(page).not_to have_selector('.js-blob-viewer-switcher')
- # shows an enabled copy button
- expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+ # does not show a copy button
+ expect(page).not_to have_selector('.js-copy-blob-source-btn')
- # shows a raw button
- expect(page).to have_link('Open raw')
+ # shows a download button
+ expect(page).to have_link('Download')
end
end
end
- end
- context 'PDF file' do
- before do
- project.add_maintainer(project.creator)
+ context 'Jupiter Notebook file' do
+ before do
+ project.add_maintainer(project.creator)
- Files::CreateService.new(
- project,
- project.creator,
- start_branch: 'master',
- branch_name: 'master',
- commit_message: "Add PDF",
- file_path: 'files/test.pdf',
- file_content: project.repository.blob_at('add-pdf-file', 'files/pdf/test.pdf').data
- ).execute
+ Files::CreateService.new(
+ project,
+ project.creator,
+ start_branch: 'master',
+ branch_name: 'master',
+ commit_message: "Add Jupiter Notebook",
+ file_path: 'files/basic.ipynb',
+ file_content: project.repository.blob_at('add-ipython-files', 'files/ipython/basic.ipynb').data
+ ).execute
- visit_blob('files/test.pdf')
+ visit_blob('files/basic.ipynb')
- wait_for_requests
- end
+ wait_for_requests
+ end
- it 'displays the blob' do
- aggregate_failures do
- # shows rendered PDF
- expect(page).to have_selector('.js-pdf-viewer')
+ it 'displays the blob' do
+ aggregate_failures do
+ # shows rendered notebook
+ expect(page).to have_selector('.js-notebook-viewer-mounted')
+
+ # does show a viewer switcher
+ expect(page).to have_selector('.js-blob-viewer-switcher')
+
+ # show a disabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn.disabled')
- # does not show a viewer switcher
- expect(page).not_to have_selector('.js-blob-viewer-switcher')
+ # shows a raw button
+ expect(page).to have_link('Open raw')
- # does not show a copy button
- expect(page).not_to have_selector('.js-copy-blob-source-btn')
+ # shows a download button
+ expect(page).to have_link('Download')
- # shows a download button
- expect(page).to have_link('Download')
+ # shows the rendered notebook
+ expect(page).to have_content('test')
+ end
end
end
- end
- context 'Jupiter Notebook file' do
- before do
- project.add_maintainer(project.creator)
+ context 'ISO file (stored in LFS)' do
+ context 'when LFS is enabled on the project' do
+ before do
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+ project.update_attribute(:lfs_enabled, true)
- Files::CreateService.new(
- project,
- project.creator,
- start_branch: 'master',
- branch_name: 'master',
- commit_message: "Add Jupiter Notebook",
- file_path: 'files/basic.ipynb',
- file_content: project.repository.blob_at('add-ipython-files', 'files/ipython/basic.ipynb').data
- ).execute
+ visit_blob('files/lfs/lfs_object.iso')
- visit_blob('files/basic.ipynb')
+ wait_for_requests
+ end
- wait_for_requests
- end
+ it 'displays the blob' do
+ aggregate_failures do
+ # shows a download link
+ expect(page).to have_link('Download (1.50 MiB)')
+
+ # does not show a viewer switcher
+ expect(page).not_to have_selector('.js-blob-viewer-switcher')
+
+ # does not show a copy button
+ expect(page).not_to have_selector('.js-copy-blob-source-btn')
+
+ # shows a download button
+ expect(page).to have_link('Download')
+ end
+ end
+ end
- it 'displays the blob' do
- aggregate_failures do
- # shows rendered notebook
- expect(page).to have_selector('.js-notebook-viewer-mounted')
+ context 'when LFS is disabled on the project' do
+ before do
+ visit_blob('files/lfs/lfs_object.iso')
- # does show a viewer switcher
- expect(page).to have_selector('.js-blob-viewer-switcher')
+ wait_for_requests
+ end
- # show a disabled copy button
- expect(page).to have_selector('.js-copy-blob-source-btn.disabled')
+ it 'displays the blob' do
+ aggregate_failures do
+ # shows text
+ expect(page).to have_content('size 1575078')
- # shows a raw button
- expect(page).to have_link('Open raw')
+ # does not show a viewer switcher
+ expect(page).not_to have_selector('.js-blob-viewer-switcher')
- # shows a download button
- expect(page).to have_link('Download')
+ # shows an enabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
- # shows the rendered notebook
- expect(page).to have_content('test')
+ # shows a raw button
+ expect(page).to have_link('Open raw')
+ end
+ end
end
end
- end
- context 'ISO file (stored in LFS)' do
- context 'when LFS is enabled on the project' do
+ context 'ZIP file' do
before do
- allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
- project.update_attribute(:lfs_enabled, true)
-
- visit_blob('files/lfs/lfs_object.iso')
+ visit_blob('Gemfile.zip')
wait_for_requests
end
@@ -473,7 +451,7 @@ RSpec.describe 'File blob', :js do
it 'displays the blob' do
aggregate_failures do
# shows a download link
- expect(page).to have_link('Download (1.5 MB)')
+ expect(page).to have_link('Download (2.11 KiB)')
# does not show a viewer switcher
expect(page).not_to have_selector('.js-blob-viewer-switcher')
@@ -487,578 +465,703 @@ RSpec.describe 'File blob', :js do
end
end
- context 'when LFS is disabled on the project' do
+ context 'empty file' do
before do
- visit_blob('files/lfs/lfs_object.iso')
+ project.add_maintainer(project.creator)
+
+ Files::CreateService.new(
+ project,
+ project.creator,
+ start_branch: 'master',
+ branch_name: 'master',
+ commit_message: "Add empty file",
+ file_path: 'files/empty.md',
+ file_content: ''
+ ).execute
+
+ visit_blob('files/empty.md')
wait_for_requests
end
- it 'displays the blob' do
+ it 'displays an error' do
aggregate_failures do
- # shows text
- expect(page).to have_content('size 1575078')
+ # shows an error message
+ expect(page).to have_content('Empty file')
# does not show a viewer switcher
expect(page).not_to have_selector('.js-blob-viewer-switcher')
- # shows an enabled copy button
- expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+ # does not show a copy button
+ expect(page).not_to have_selector('.js-copy-blob-source-btn')
- # shows a raw button
- expect(page).to have_link('Open raw')
+ # does not show a download or raw button
+ expect(page).not_to have_link('Download')
+ expect(page).not_to have_link('Open raw')
end
end
end
- end
-
- context 'ZIP file' do
- before do
- visit_blob('Gemfile.zip')
- wait_for_requests
- end
+ context 'files with auxiliary viewers' do
+ describe '.gitlab-ci.yml' do
+ before do
+ project.add_maintainer(project.creator)
- it 'displays the blob' do
- aggregate_failures do
- # shows a download link
- expect(page).to have_link('Download (2.11 KB)')
+ Files::CreateService.new(
+ project,
+ project.creator,
+ start_branch: 'master',
+ branch_name: 'master',
+ commit_message: "Add .gitlab-ci.yml",
+ file_path: '.gitlab-ci.yml',
+ file_content: File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
+ ).execute
- # does not show a viewer switcher
- expect(page).not_to have_selector('.js-blob-viewer-switcher')
+ visit_blob('.gitlab-ci.yml')
+ end
- # does not show a copy button
- expect(page).not_to have_selector('.js-copy-blob-source-btn')
+ it 'displays an auxiliary viewer' do
+ aggregate_failures do
+ # shows that configuration is valid
+ expect(page).to have_content('This GitLab CI configuration is valid.')
- # shows a download button
- expect(page).to have_link('Download')
+ # shows a learn more link
+ expect(page).to have_link('Learn more')
+ end
+ end
end
- end
- end
- context 'empty file' do
- before do
- project.add_maintainer(project.creator)
+ describe '.gitlab/route-map.yml' do
+ before do
+ project.add_maintainer(project.creator)
+
+ Files::CreateService.new(
+ project,
+ project.creator,
+ start_branch: 'master',
+ branch_name: 'master',
+ commit_message: "Add .gitlab/route-map.yml",
+ file_path: '.gitlab/route-map.yml',
+ file_content: <<-MAP.strip_heredoc
+ # Team data
+ - source: 'data/team.yml'
+ public: 'team/'
+ MAP
+ ).execute
- Files::CreateService.new(
- project,
- project.creator,
- start_branch: 'master',
- branch_name: 'master',
- commit_message: "Add empty file",
- file_path: 'files/empty.md',
- file_content: ''
- ).execute
+ visit_blob('.gitlab/route-map.yml')
+ end
- visit_blob('files/empty.md')
+ it 'displays an auxiliary viewer' do
+ aggregate_failures do
+ # shows that map is valid
+ expect(page).to have_content('This Route Map is valid.')
- wait_for_requests
- end
+ # shows a learn more link
+ expect(page).to have_link('Learn more')
+ end
+ end
+ end
- it 'displays an error' do
- aggregate_failures do
- # shows an error message
- expect(page).to have_content('Empty file')
+ describe '.gitlab/dashboards/custom-dashboard.yml' do
+ before do
+ project.add_maintainer(project.creator)
- # does not show a viewer switcher
- expect(page).not_to have_selector('.js-blob-viewer-switcher')
+ Files::CreateService.new(
+ project,
+ project.creator,
+ start_branch: 'master',
+ branch_name: 'master',
+ commit_message: "Add .gitlab/dashboards/custom-dashboard.yml",
+ file_path: '.gitlab/dashboards/custom-dashboard.yml',
+ file_content: file_content
+ ).execute
+ end
- # does not show a copy button
- expect(page).not_to have_selector('.js-copy-blob-source-btn')
+ context 'with metrics_dashboard_exhaustive_validations feature flag off' do
+ before do
+ stub_feature_flags(metrics_dashboard_exhaustive_validations: false)
+ visit_blob('.gitlab/dashboards/custom-dashboard.yml')
+ end
- # does not show a download or raw button
- expect(page).not_to have_link('Download')
- expect(page).not_to have_link('Open raw')
- end
- end
- end
+ context 'valid dashboard file' do
+ let(:file_content) { File.read(Rails.root.join('config/prometheus/common_metrics.yml')) }
- context 'binary file that appears to be text in the first 1024 bytes' do
- before do
- visit_blob('encoding/binary-1.bin', ref: 'binary-encoding')
- end
+ it 'displays an auxiliary viewer' do
+ aggregate_failures do
+ # shows that dashboard yaml is valid
+ expect(page).to have_content('Metrics Dashboard YAML definition is valid.')
- it 'displays the blob' do
- aggregate_failures do
- # shows a download link
- expect(page).to have_link('Download (23.8 KB)')
+ # shows a learn more link
+ expect(page).to have_link('Learn more')
+ end
+ end
+ end
- # does not show a viewer switcher
- expect(page).not_to have_selector('.js-blob-viewer-switcher')
+ context 'invalid dashboard file' do
+ let(:file_content) { "dashboard: 'invalid'" }
- # The specs below verify an arguably incorrect result, but since we only
- # learn that the file is not actually text once the text viewer content
- # is loaded asynchronously, there is no straightforward way to get these
- # synchronously loaded elements to display correctly.
- #
- # Clicking the copy button will result in nothing being copied.
- # Clicking the raw button will result in the binary file being downloaded,
- # as expected.
+ it 'displays an auxiliary viewer' do
+ aggregate_failures do
+ # shows that dashboard yaml is invalid
+ expect(page).to have_content('Metrics Dashboard YAML definition is invalid:')
+ expect(page).to have_content("panel_groups: should be an array of panel_groups objects")
- # shows an enabled copy button, incorrectly
- expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+ # shows a learn more link
+ expect(page).to have_link('Learn more')
+ end
+ end
+ end
+ end
- # shows a raw button, incorrectly
- expect(page).to have_link('Open raw')
- end
- end
- end
+ context 'with metrics_dashboard_exhaustive_validations feature flag on' do
+ before do
+ stub_feature_flags(metrics_dashboard_exhaustive_validations: true)
+ visit_blob('.gitlab/dashboards/custom-dashboard.yml')
+ end
- context 'files with auxiliary viewers' do
- before do
- stub_feature_flags(refactor_blob_viewer: true)
- end
+ context 'valid dashboard file' do
+ let(:file_content) { File.read(Rails.root.join('config/prometheus/common_metrics.yml')) }
- describe '.gitlab-ci.yml' do
- before do
- project.add_maintainer(project.creator)
+ it 'displays an auxiliary viewer' do
+ aggregate_failures do
+ # shows that dashboard yaml is valid
+ expect(page).to have_content('Metrics Dashboard YAML definition is valid.')
- Files::CreateService.new(
- project,
- project.creator,
- start_branch: 'master',
- branch_name: 'master',
- commit_message: "Add .gitlab-ci.yml",
- file_path: '.gitlab-ci.yml',
- file_content: File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
- ).execute
+ # shows a learn more link
+ expect(page).to have_link('Learn more')
+ end
+ end
+ end
- visit_blob('.gitlab-ci.yml')
- end
+ context 'invalid dashboard file' do
+ let(:file_content) { "dashboard: 'invalid'" }
- it 'displays an auxiliary viewer' do
- aggregate_failures do
- # shows that configuration is valid
- expect(page).to have_content('This GitLab CI configuration is valid.')
+ it 'displays an auxiliary viewer' do
+ aggregate_failures do
+ # shows that dashboard yaml is invalid
+ expect(page).to have_content('Metrics Dashboard YAML definition is invalid:')
+ expect(page).to have_content("root is missing required keys: panel_groups")
- # shows a learn more link
- expect(page).to have_link('Learn more')
+ # shows a learn more link
+ expect(page).to have_link('Learn more')
+ end
+ end
+ end
end
end
- end
- describe '.gitlab/route-map.yml' do
- before do
- project.add_maintainer(project.creator)
+ context 'LICENSE' do
+ before do
+ visit_blob('LICENSE')
+ end
- Files::CreateService.new(
- project,
- project.creator,
- start_branch: 'master',
- branch_name: 'master',
- commit_message: "Add .gitlab/route-map.yml",
- file_path: '.gitlab/route-map.yml',
- file_content: <<-MAP.strip_heredoc
- # Team data
- - source: 'data/team.yml'
- public: 'team/'
- MAP
- ).execute
+ it 'displays an auxiliary viewer' do
+ aggregate_failures do
+ # shows license
+ expect(page).to have_content('This project is licensed under the MIT License.')
- visit_blob('.gitlab/route-map.yml')
+ # shows a learn more link
+ expect(page).to have_link('Learn more', href: 'http://choosealicense.com/licenses/mit/')
+ end
+ end
end
- it 'displays an auxiliary viewer' do
- aggregate_failures do
- # shows that map is valid
- expect(page).to have_content('This Route Map is valid.')
+ context '*.gemspec' do
+ before do
+ project.add_maintainer(project.creator)
- # shows a learn more link
- expect(page).to have_link('Learn more')
+ Files::CreateService.new(
+ project,
+ project.creator,
+ start_branch: 'master',
+ branch_name: 'master',
+ commit_message: "Add activerecord.gemspec",
+ file_path: 'activerecord.gemspec',
+ file_content: <<-SPEC.strip_heredoc
+ Gem::Specification.new do |s|
+ s.platform = Gem::Platform::RUBY
+ s.name = "activerecord"
+ end
+ SPEC
+ ).execute
+
+ visit_blob('activerecord.gemspec')
end
- end
- end
- describe '.gitlab/dashboards/custom-dashboard.yml' do
- before do
- project.add_maintainer(project.creator)
+ it 'displays an auxiliary viewer' do
+ aggregate_failures do
+ # shows names of dependency manager and package
+ expect(page).to have_content('This project manages its dependencies using RubyGems.')
- Files::CreateService.new(
- project,
- project.creator,
- start_branch: 'master',
- branch_name: 'master',
- commit_message: "Add .gitlab/dashboards/custom-dashboard.yml",
- file_path: '.gitlab/dashboards/custom-dashboard.yml',
- file_content: file_content
- ).execute
+ # shows a learn more link
+ expect(page).to have_link('Learn more', href: 'https://rubygems.org/')
+ end
+ end
end
- context 'with metrics_dashboard_exhaustive_validations feature flag off' do
+ context 'CONTRIBUTING.md' do
before do
- stub_feature_flags(metrics_dashboard_exhaustive_validations: false)
- visit_blob('.gitlab/dashboards/custom-dashboard.yml')
+ file_name = 'CONTRIBUTING.md'
+
+ create_file(file_name, '## Contribution guidelines')
+ visit_blob(file_name)
end
- context 'valid dashboard file' do
- let(:file_content) { File.read(Rails.root.join('config/prometheus/common_metrics.yml')) }
+ it 'displays an auxiliary viewer' do
+ aggregate_failures do
+ expect(page).to have_content("After you've reviewed these contribution guidelines, you'll be all set to contribute to this project.")
+ end
+ end
+ end
- it 'displays an auxiliary viewer' do
- aggregate_failures do
- # shows that dashboard yaml is valid
- expect(page).to have_content('Metrics Dashboard YAML definition is valid.')
+ context 'CHANGELOG.md' do
+ before do
+ file_name = 'CHANGELOG.md'
- # shows a learn more link
- expect(page).to have_link('Learn more')
- end
+ create_file(file_name, '## Changelog for v1.0.0')
+ visit_blob(file_name)
+ end
+
+ it 'displays an auxiliary viewer' do
+ aggregate_failures do
+ expect(page).to have_content("To find the state of this project's repository at the time of any of these versions, check out the tags.")
end
end
+ end
- context 'invalid dashboard file' do
- let(:file_content) { "dashboard: 'invalid'" }
+ context 'Cargo.toml' do
+ before do
+ file_name = 'Cargo.toml'
- it 'displays an auxiliary viewer' do
- aggregate_failures do
- # shows that dashboard yaml is invalid
- expect(page).to have_content('Metrics Dashboard YAML definition is invalid:')
- expect(page).to have_content("panel_groups: should be an array of panel_groups objects")
+ create_file(file_name, '
+ [package]
+ name = "hello_world" # the name of the package
+ version = "0.1.0" # the current version, obeying semver
+ authors = ["Alice <a@example.com>", "Bob <b@example.com>"]
+ ')
+ visit_blob(file_name)
+ end
- # shows a learn more link
- expect(page).to have_link('Learn more')
- end
+ it 'displays an auxiliary viewer' do
+ aggregate_failures do
+ expect(page).to have_content("This project manages its dependencies using Cargo.")
end
end
end
- context 'with metrics_dashboard_exhaustive_validations feature flag on' do
+ context 'Cartfile' do
before do
- stub_feature_flags(metrics_dashboard_exhaustive_validations: true)
- visit_blob('.gitlab/dashboards/custom-dashboard.yml')
+ file_name = 'Cartfile'
+
+ create_file(file_name, '
+ gitlab "Alamofire/Alamofire" == 4.9.0
+ gitlab "Alamofire/AlamofireImage" ~> 3.4
+ ')
+ visit_blob(file_name)
+ end
+
+ it 'displays an auxiliary viewer' do
+ aggregate_failures do
+ expect(page).to have_content("This project manages its dependencies using Carthage.")
+ end
end
+ end
- context 'valid dashboard file' do
- let(:file_content) { File.read(Rails.root.join('config/prometheus/common_metrics.yml')) }
+ context 'composer.json' do
+ before do
+ file_name = 'composer.json'
- it 'displays an auxiliary viewer' do
- aggregate_failures do
- # shows that dashboard yaml is valid
- expect(page).to have_content('Metrics Dashboard YAML definition is valid.')
+ create_file(file_name, '
+ {
+ "license": "MIT"
+ }
+ ')
+ visit_blob(file_name)
+ end
- # shows a learn more link
- expect(page).to have_link('Learn more')
- end
+ it 'displays an auxiliary viewer' do
+ aggregate_failures do
+ expect(page).to have_content("This project manages its dependencies using Composer.")
end
end
+ end
- context 'invalid dashboard file' do
- let(:file_content) { "dashboard: 'invalid'" }
+ context 'Gemfile' do
+ before do
+ file_name = 'Gemfile'
- it 'displays an auxiliary viewer' do
- aggregate_failures do
- # shows that dashboard yaml is invalid
- expect(page).to have_content('Metrics Dashboard YAML definition is invalid:')
- expect(page).to have_content("root is missing required keys: panel_groups")
+ create_file(file_name, '
+ source "https://rubygems.org"
- # shows a learn more link
- expect(page).to have_link('Learn more')
- end
+ # Gems here
+ ')
+ visit_blob(file_name)
+ end
+
+ it 'displays an auxiliary viewer' do
+ aggregate_failures do
+ expect(page).to have_content("This project manages its dependencies using Bundler.")
end
end
end
- end
- context 'LICENSE' do
- before do
- visit_blob('LICENSE')
- end
+ context 'Godeps.json' do
+ before do
+ file_name = 'Godeps.json'
- it 'displays an auxiliary viewer' do
- aggregate_failures do
- # shows license
- expect(page).to have_content('This project is licensed under the MIT License.')
+ create_file(file_name, '
+ {
+ "GoVersion": "go1.6"
+ }
+ ')
+ visit_blob(file_name)
+ end
- # shows a learn more link
- expect(page).to have_link('Learn more', href: 'http://choosealicense.com/licenses/mit/')
+ it 'displays an auxiliary viewer' do
+ aggregate_failures do
+ expect(page).to have_content("This project manages its dependencies using godep.")
+ end
end
end
- end
- context '*.gemspec' do
- before do
- project.add_maintainer(project.creator)
+ context 'go.mod' do
+ before do
+ file_name = 'go.mod'
- Files::CreateService.new(
- project,
- project.creator,
- start_branch: 'master',
- branch_name: 'master',
- commit_message: "Add activerecord.gemspec",
- file_path: 'activerecord.gemspec',
- file_content: <<-SPEC.strip_heredoc
- Gem::Specification.new do |s|
- s.platform = Gem::Platform::RUBY
- s.name = "activerecord"
- end
- SPEC
- ).execute
+ create_file(file_name, '
+ module example.com/mymodule
+
+ go 1.14
+ ')
+ visit_blob(file_name)
+ end
- visit_blob('activerecord.gemspec')
+ it 'displays an auxiliary viewer' do
+ aggregate_failures do
+ expect(page).to have_content("This project manages its dependencies using Go Modules.")
+ end
+ end
end
- it 'displays an auxiliary viewer' do
- aggregate_failures do
- # shows names of dependency manager and package
- expect(page).to have_content('This project manages its dependencies using RubyGems.')
+ context 'package.json' do
+ before do
+ file_name = 'package.json'
- # shows a learn more link
- expect(page).to have_link('Learn more', href: 'https://rubygems.org/')
+ create_file(file_name, '
+ {
+ "name": "my-awesome-package",
+ "version": "1.0.0"
+ }
+ ')
+ visit_blob(file_name)
+ end
+
+ it 'displays an auxiliary viewer' do
+ aggregate_failures do
+ expect(page).to have_content("This project manages its dependencies using npm.")
+ end
end
end
- end
- context 'CONTRIBUTING.md' do
- before do
- file_name = 'CONTRIBUTING.md'
+ context 'podfile' do
+ before do
+ file_name = 'podfile'
- create_file(file_name, '## Contribution guidelines')
- visit_blob(file_name)
- end
+ create_file(file_name, 'platform :ios, "8.0"')
+ visit_blob(file_name)
+ end
- it 'displays an auxiliary viewer' do
- aggregate_failures do
- expect(page).to have_content("After you've reviewed these contribution guidelines, you'll be all set to contribute to this project.")
+ it 'displays an auxiliary viewer' do
+ aggregate_failures do
+ expect(page).to have_content("This project manages its dependencies using CocoaPods.")
+ end
end
end
- end
- context 'CHANGELOG.md' do
- before do
- file_name = 'CHANGELOG.md'
+ context 'test.podspec' do
+ before do
+ file_name = 'test.podspec'
- create_file(file_name, '## Changelog for v1.0.0')
- visit_blob(file_name)
- end
+ create_file(file_name, '
+ Pod::Spec.new do |s|
+ s.name = "TensorFlowLiteC"
+ ')
+ visit_blob(file_name)
+ end
- it 'displays an auxiliary viewer' do
- aggregate_failures do
- expect(page).to have_content("To find the state of this project's repository at the time of any of these versions, check out the tags.")
+ it 'displays an auxiliary viewer' do
+ aggregate_failures do
+ expect(page).to have_content("This project manages its dependencies using CocoaPods.")
+ end
end
end
- end
- context 'Cargo.toml' do
- before do
- file_name = 'Cargo.toml'
+ context 'JSON.podspec.json' do
+ before do
+ file_name = 'JSON.podspec.json'
- create_file(file_name, '
- [package]
- name = "hello_world" # the name of the package
- version = "0.1.0" # the current version, obeying semver
- authors = ["Alice <a@example.com>", "Bob <b@example.com>"]
- ')
- visit_blob(file_name)
- end
+ create_file(file_name, '
+ {
+ "name": "JSON"
+ }
+ ')
+ visit_blob(file_name)
+ end
- it 'displays an auxiliary viewer' do
- aggregate_failures do
- expect(page).to have_content("This project manages its dependencies using Cargo.")
+ it 'displays an auxiliary viewer' do
+ aggregate_failures do
+ expect(page).to have_content("This project manages its dependencies using CocoaPods.")
+ end
end
end
- end
- context 'Cartfile' do
- before do
- file_name = 'Cartfile'
+ context 'requirements.txt' do
+ before do
+ file_name = 'requirements.txt'
- create_file(file_name, '
- gitlab "Alamofire/Alamofire" == 4.9.0
- gitlab "Alamofire/AlamofireImage" ~> 3.4
- ')
- visit_blob(file_name)
+ create_file(file_name, 'Project requirements')
+ visit_blob(file_name)
+ end
+
+ it 'displays an auxiliary viewer' do
+ aggregate_failures do
+ expect(page).to have_content("This project manages its dependencies using pip.")
+ end
+ end
end
- it 'displays an auxiliary viewer' do
- aggregate_failures do
- expect(page).to have_content("This project manages its dependencies using Carthage.")
+ context 'yarn.lock' do
+ before do
+ file_name = 'yarn.lock'
+
+ create_file(file_name, '
+ # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+ # yarn lockfile v1
+ ')
+ visit_blob(file_name)
+ end
+
+ it 'displays an auxiliary viewer' do
+ aggregate_failures do
+ expect(page).to have_content("This project manages its dependencies using Yarn.")
+ end
end
end
end
- context 'composer.json' do
+ context 'realtime pipelines' do
before do
- file_name = 'composer.json'
+ Files::CreateService.new(
+ project,
+ project.creator,
+ start_branch: 'feature',
+ branch_name: 'feature',
+ commit_message: "Add ruby file",
+ file_path: 'files/ruby/test.rb',
+ file_content: "# Awesome content"
+ ).execute
- create_file(file_name, '
- {
- "license": "MIT"
- }
- ')
- visit_blob(file_name)
+ create(:ci_pipeline, status: 'running', project: project, ref: 'feature', sha: project.commit('feature').sha)
+ visit_blob('files/ruby/test.rb', ref: 'feature')
end
- it 'displays an auxiliary viewer' do
- aggregate_failures do
- expect(page).to have_content("This project manages its dependencies using Composer.")
+ it 'shows the realtime pipeline status' do
+ page.within('.commit-actions') do
+ expect(page).to have_css('.ci-status-icon')
+ expect(page).to have_css('.ci-status-icon-running')
+ expect(page).to have_css('.js-ci-status-icon-running')
end
end
end
- context 'Gemfile' do
- before do
- file_name = 'Gemfile'
+ context 'for subgroups' do
+ let(:group) { create(:group) }
+ let(:subgroup) { create(:group, parent: group) }
+ let(:project) { create(:project, :public, :repository, group: subgroup) }
- create_file(file_name, '
- source "https://rubygems.org"
+ it 'renders tree table without errors' do
+ visit_blob('README.md')
- # Gems here
- ')
- visit_blob(file_name)
+ expect(page).to have_selector('.file-content')
+ expect(page).not_to have_selector('[data-testid="alert-danger"]')
end
- it 'displays an auxiliary viewer' do
- aggregate_failures do
- expect(page).to have_content("This project manages its dependencies using Bundler.")
- end
+ it 'displays a GPG badge' do
+ visit_blob('CONTRIBUTING.md', ref: '33f3729a45c02fc67d00adb1b8bca394b0e761d9')
+
+ expect(page).not_to have_selector '.gpg-status-box.js-loading-gpg-badge'
+ expect(page).to have_selector '.gpg-status-box.invalid'
end
end
- context 'Godeps.json' do
- before do
- file_name = 'Godeps.json'
+ context 'on signed merge commit' do
+ it 'displays a GPG badge' do
+ visit_blob('conflicting-file.md', ref: '6101e87e575de14b38b4e1ce180519a813671e10')
- create_file(file_name, '
- {
- "GoVersion": "go1.6"
- }
- ')
- visit_blob(file_name)
- end
-
- it 'displays an auxiliary viewer' do
- aggregate_failures do
- expect(page).to have_content("This project manages its dependencies using godep.")
- end
+ expect(page).not_to have_selector '.gpg-status-box.js-loading-gpg-badge'
+ expect(page).to have_selector '.gpg-status-box.invalid'
end
end
- context 'go.mod' do
+ context 'when static objects external storage is enabled' do
before do
- file_name = 'go.mod'
+ stub_application_setting(static_objects_external_storage_url: 'https://cdn.gitlab.com')
+ end
- create_file(file_name, '
- module example.com/mymodule
+ context 'public project' do
+ before do
+ visit_blob('README.md')
+ end
- go 1.14
- ')
- visit_blob(file_name)
- end
+ it 'shows open raw and download buttons with external storage URL prepended to their href' do
+ path = project_raw_path(project, 'master/README.md')
+ raw_uri = "https://cdn.gitlab.com#{path}"
+ download_uri = "https://cdn.gitlab.com#{path}?inline=false"
- it 'displays an auxiliary viewer' do
- aggregate_failures do
- expect(page).to have_content("This project manages its dependencies using Go Modules.")
+ aggregate_failures do
+ expect(page).to have_link 'Open raw', href: raw_uri
+ expect(page).to have_link 'Download', href: download_uri
+ end
end
end
end
+ end
- context 'package.json' do
- before do
- file_name = 'package.json'
+ context 'with refactor_blob_viewer feature flag disabled' do
+ before do
+ stub_feature_flags(refactor_blob_viewer: false)
+ end
- create_file(file_name, '
- {
- "name": "my-awesome-package",
- "version": "1.0.0"
- }
- ')
- visit_blob(file_name)
- end
+ context 'when ref switch' do
+ # We need to unsre that this test runs with the refactor_blob_viewer feature flag enabled
+ # This will be addressed in https://gitlab.com/gitlab-org/gitlab/-/issues/351558
- it 'displays an auxiliary viewer' do
- aggregate_failures do
- expect(page).to have_content("This project manages its dependencies using npm.")
+ def switch_ref_to(ref_name)
+ first('.qa-branches-select').click # rubocop:disable QA/SelectorUsage
+
+ page.within '.project-refs-form' do
+ click_link ref_name
+ wait_for_requests
end
end
- end
- context 'podfile' do
- before do
- file_name = 'podfile'
+ context 'when highlighting lines' do
+ it 'displays single highlighted line number of different ref' do
+ visit_blob('files/js/application.js', anchor: 'L1')
- create_file(file_name, 'platform :ios, "8.0"')
- visit_blob(file_name)
- end
+ switch_ref_to('feature')
- it 'displays an auxiliary viewer' do
- aggregate_failures do
- expect(page).to have_content("This project manages its dependencies using CocoaPods.")
+ page.within '.blob-content' do
+ expect(find_by_id('LC1')[:class]).to include("hll")
+ end
end
- end
- end
- context 'test.podspec' do
- before do
- file_name = 'test.podspec'
+ it 'displays multiple highlighted line numbers of different ref' do
+ visit_blob('files/js/application.js', anchor: 'L1-3')
- create_file(file_name, '
- Pod::Spec.new do |s|
- s.name = "TensorFlowLiteC"
- ')
- visit_blob(file_name)
- end
+ switch_ref_to('feature')
- it 'displays an auxiliary viewer' do
- aggregate_failures do
- expect(page).to have_content("This project manages its dependencies using CocoaPods.")
+ page.within '.blob-content' do
+ expect(find_by_id('LC1')[:class]).to include("hll")
+ expect(find_by_id('LC2')[:class]).to include("hll")
+ expect(find_by_id('LC3')[:class]).to include("hll")
+ end
end
end
end
- context 'JSON.podspec.json' do
- before do
- file_name = 'JSON.podspec.json'
+ context 'visiting with a line number anchor' do
+ # We need to unsre that this test runs with the refactor_blob_viewer feature flag enabled
+ # This will be addressed in https://gitlab.com/gitlab-org/gitlab/-/issues/351558
- create_file(file_name, '
- {
- "name": "JSON"
- }
- ')
- visit_blob(file_name)
+ before do
+ visit_blob('files/markdown/ruby-style-guide.md', anchor: 'L1')
end
- it 'displays an auxiliary viewer' do
+ it 'displays the blob using the simple viewer' do
aggregate_failures do
- expect(page).to have_content("This project manages its dependencies using CocoaPods.")
+ # hides the rich viewer
+ expect(page).to have_selector('.blob-viewer[data-type="simple"]')
+ expect(page).not_to have_selector('.blob-viewer[data-type="rich"]')
+
+ # highlights the line in question
+ expect(page).to have_selector('#LC1.hll')
+
+ # shows highlighted Markdown code
+ expect(page).to have_css(".js-syntax-highlight")
+ expect(page).to have_content("[PEP-8](http://www.python.org/dev/peps/pep-0008/)")
+
+ # shows an enabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
end
end
end
- context 'requirements.txt' do
- before do
- file_name = 'requirements.txt'
+ context 'binary file that appears to be text in the first 1024 bytes' do
+ # We need to unsre that this test runs with the refactor_blob_viewer feature flag enabled
+ # This will be addressed in https://gitlab.com/gitlab-org/gitlab/-/issues/351559
- create_file(file_name, 'Project requirements')
- visit_blob(file_name)
+ before do
+ visit_blob('encoding/binary-1.bin', ref: 'binary-encoding')
end
-
- it 'displays an auxiliary viewer' do
+ it 'displays the blob' do
aggregate_failures do
- expect(page).to have_content("This project manages its dependencies using pip.")
+ # shows a download link
+ expect(page).to have_link('Download (23.8 KB)')
+ # does not show a viewer switcher
+ expect(page).not_to have_selector('.js-blob-viewer-switcher')
+ # The specs below verify an arguably incorrect result, but since we only
+ # learn that the file is not actually text once the text viewer content
+ # is loaded asynchronously, there is no straightforward way to get these
+ # synchronously loaded elements to display correctly.
+ #
+ # Clicking the copy button will result in nothing being copied.
+ # Clicking the raw button will result in the binary file being downloaded,
+ # as expected.
+ # shows an enabled copy button, incorrectly
+ expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+ # shows a raw button, incorrectly
+ expect(page).to have_link('Open raw')
end
end
end
- context 'yarn.lock' do
- before do
- file_name = 'yarn.lock'
+ context 'when static objects external storage is enabled' do
+ # We need to unsre that this test runs with the refactor_blob_viewer feature flag enabled
+ # This will be addressed in https://gitlab.com/gitlab-org/gitlab/-/issues/351555
- create_file(file_name, '
- # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
- # yarn lockfile v1
- ')
- visit_blob(file_name)
+ before do
+ stub_application_setting(static_objects_external_storage_url: 'https://cdn.gitlab.com')
end
- it 'displays an auxiliary viewer' do
- aggregate_failures do
- expect(page).to have_content("This project manages its dependencies using Yarn.")
+ context 'private project' do
+ let_it_be(:project) { create(:project, :repository, :private) }
+ let_it_be(:user) { create(:user) }
+
+ before do
+ project.add_developer(user)
+
+ sign_in(user)
+ visit_blob('README.md')
+ end
+
+ it 'shows open raw and download buttons with external storage URL prepended and user token appended to their href' do
+ path = project_raw_path(project, 'master/README.md')
+ raw_uri = "https://cdn.gitlab.com#{path}?token=#{user.static_object_token}"
+ download_uri = "https://cdn.gitlab.com#{path}?inline=false&token=#{user.static_object_token}"
+
+ aggregate_failures do
+ expect(page).to have_link 'Open raw', href: raw_uri
+ expect(page).to have_link 'Download', href: download_uri
+ end
end
end
end
- context 'when refactor_blob_viewer is disabled' do
- before do
- stub_feature_flags(refactor_blob_viewer: false)
- end
+ context 'files with auxiliary viewers' do
+ # This context is the same as the other 'files with auxiliary viewers' in this file, we just ensure that the auxiliary viewers still work this the refactor_blob_viewer disabled
+ # It should be safe to remove once we rollout the refactored blob viewer
describe '.gitlab-ci.yml' do
before do
@@ -1554,104 +1657,4 @@ RSpec.describe 'File blob', :js do
end
end
end
-
- context 'realtime pipelines' do
- before do
- Files::CreateService.new(
- project,
- project.creator,
- start_branch: 'feature',
- branch_name: 'feature',
- commit_message: "Add ruby file",
- file_path: 'files/ruby/test.rb',
- file_content: "# Awesome content"
- ).execute
-
- create(:ci_pipeline, status: 'running', project: project, ref: 'feature', sha: project.commit('feature').sha)
- visit_blob('files/ruby/test.rb', ref: 'feature')
- end
-
- it 'shows the realtime pipeline status' do
- page.within('.commit-actions') do
- expect(page).to have_css('.ci-status-icon')
- expect(page).to have_css('.ci-status-icon-running')
- expect(page).to have_css('.js-ci-status-icon-running')
- end
- end
- end
-
- context 'for subgroups' do
- let(:group) { create(:group) }
- let(:subgroup) { create(:group, parent: group) }
- let(:project) { create(:project, :public, :repository, group: subgroup) }
-
- it 'renders tree table without errors' do
- visit_blob('README.md')
-
- expect(page).to have_selector('.file-content')
- expect(page).not_to have_selector('.flash-alert')
- end
-
- it 'displays a GPG badge' do
- visit_blob('CONTRIBUTING.md', ref: '33f3729a45c02fc67d00adb1b8bca394b0e761d9')
-
- expect(page).not_to have_selector '.gpg-status-box.js-loading-gpg-badge'
- expect(page).to have_selector '.gpg-status-box.invalid'
- end
- end
-
- context 'on signed merge commit' do
- it 'displays a GPG badge' do
- visit_blob('conflicting-file.md', ref: '6101e87e575de14b38b4e1ce180519a813671e10')
-
- expect(page).not_to have_selector '.gpg-status-box.js-loading-gpg-badge'
- expect(page).to have_selector '.gpg-status-box.invalid'
- end
- end
-
- context 'when static objects external storage is enabled' do
- before do
- stub_application_setting(static_objects_external_storage_url: 'https://cdn.gitlab.com')
- end
-
- context 'private project' do
- let_it_be(:project) { create(:project, :repository, :private) }
- let_it_be(:user) { create(:user) }
-
- before do
- project.add_developer(user)
-
- sign_in(user)
- visit_blob('README.md')
- end
-
- it 'shows open raw and download buttons with external storage URL prepended and user token appended to their href' do
- path = project_raw_path(project, 'master/README.md')
- raw_uri = "https://cdn.gitlab.com#{path}?token=#{user.static_object_token}"
- download_uri = "https://cdn.gitlab.com#{path}?inline=false&token=#{user.static_object_token}"
-
- aggregate_failures do
- expect(page).to have_link 'Open raw', href: raw_uri
- expect(page).to have_link 'Download', href: download_uri
- end
- end
- end
-
- context 'public project' do
- before do
- visit_blob('README.md')
- end
-
- it 'shows open raw and download buttons with external storage URL prepended to their href' do
- path = project_raw_path(project, 'master/README.md')
- raw_uri = "https://cdn.gitlab.com#{path}"
- download_uri = "https://cdn.gitlab.com#{path}?inline=false"
-
- aggregate_failures do
- expect(page).to have_link 'Open raw', href: raw_uri
- expect(page).to have_link 'Download', href: download_uri
- end
- end
- end
- end
end
diff --git a/spec/features/projects/blobs/user_follows_pipeline_suggest_nudge_spec.rb b/spec/features/projects/blobs/user_follows_pipeline_suggest_nudge_spec.rb
index 1c79b2ddc38..a2db5e11c7c 100644
--- a/spec/features/projects/blobs/user_follows_pipeline_suggest_nudge_spec.rb
+++ b/spec/features/projects/blobs/user_follows_pipeline_suggest_nudge_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'User follows pipeline suggest nudge spec when feature is enabled
include CookieHelper
let(:project) { create(:project, :empty_repo) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
describe 'viewing the new blob page' do
before do
diff --git a/spec/features/projects/branches/user_views_branches_spec.rb b/spec/features/projects/branches/user_views_branches_spec.rb
index d0c0a0860d9..b6b6dcb5cf1 100644
--- a/spec/features/projects/branches/user_views_branches_spec.rb
+++ b/spec/features/projects/branches/user_views_branches_spec.rb
@@ -4,7 +4,7 @@ require "spec_helper"
RSpec.describe "User views branches", :js do
let_it_be(:project) { create(:project, :repository) }
- let_it_be(:user) { project.owner }
+ let_it_be(:user) { project.first_owner }
before do
sign_in(user)
diff --git a/spec/features/projects/ci/editor_spec.rb b/spec/features/projects/ci/editor_spec.rb
index 16cfa9f5f84..daf5ac61d73 100644
--- a/spec/features/projects/ci/editor_spec.rb
+++ b/spec/features/projects/ci/editor_spec.rb
@@ -26,7 +26,7 @@ RSpec.describe 'Pipeline Editor', :js do
expect(page).to have_content('Pipeline Editor')
end
- context 'branch switcher' do
+ describe 'Branch switcher' do
def switch_to_branch(branch)
find('[data-testid="branch-selector"]').click
@@ -64,7 +64,64 @@ RSpec.describe 'Pipeline Editor', :js do
end
end
- context 'Editor content' do
+ describe 'Editor navigation' do
+ context 'when no change is made' do
+ it 'user can navigate away without a browser alert' do
+ expect(page).to have_content('Pipeline Editor')
+
+ click_link 'Pipelines'
+
+ expect(page).not_to have_content('Pipeline Editor')
+ end
+ end
+
+ context 'when a change is made' do
+ before do
+ click_button 'Collapse'
+
+ page.within('#source-editor-') do
+ find('textarea').send_keys '123'
+ # It takes some time after sending keys for the vue
+ # component to update
+ sleep 1
+ end
+ end
+
+ it 'user who tries to navigate away can cancel the action and keep their changes' do
+ click_link 'Pipelines'
+
+ page.driver.browser.switch_to.alert.dismiss
+
+ expect(page).to have_content('Pipeline Editor')
+
+ page.within('#source-editor-') do
+ expect(page).to have_content('Default Content123')
+ end
+ end
+
+ it 'user who tries to navigate away can confirm the action and discard their change' do
+ click_link 'Pipelines'
+
+ page.driver.browser.switch_to.alert.accept
+
+ expect(page).not_to have_content('Pipeline Editor')
+ end
+
+ it 'user who creates a MR is taken to the merge request page without warnings' do
+ expect(page).not_to have_content('New merge request')
+
+ find_field('Target Branch').set 'new_branch'
+ find_field('Start a new merge request with these changes').click
+
+ click_button 'Commit changes'
+
+ expect(page).not_to have_content('Pipeline Editor')
+ expect(page).to have_content('New merge request')
+ end
+ end
+ end
+
+ describe 'Editor content' do
it 'user can reset their CI configuration' do
click_button 'Collapse'
diff --git a/spec/features/projects/cluster_agents_spec.rb b/spec/features/projects/cluster_agents_spec.rb
index 4018ef2abc9..d2b07bbc1de 100644
--- a/spec/features/projects/cluster_agents_spec.rb
+++ b/spec/features/projects/cluster_agents_spec.rb
@@ -10,6 +10,11 @@ RSpec.describe 'ClusterAgents', :js do
let(:user) { project.creator }
before do
+ allow(Gitlab::Kas).to receive(:enabled?).and_return(true)
+ allow_next_instance_of(Gitlab::Kas::Client) do |client|
+ allow(client).to receive(:get_connected_agents).and_return([])
+ end
+
gitlab_sign_in(user)
end
@@ -22,7 +27,7 @@ RSpec.describe 'ClusterAgents', :js do
end
it 'displays empty state', :aggregate_failures do
- expect(page).to have_content('Install a new agent')
+ expect(page).to have_content('Install new Agent')
expect(page).to have_selector('.empty-state')
end
end
diff --git a/spec/features/projects/clusters_spec.rb b/spec/features/projects/clusters_spec.rb
index 6e45529c659..b0406e1f3c4 100644
--- a/spec/features/projects/clusters_spec.rb
+++ b/spec/features/projects/clusters_spec.rb
@@ -99,7 +99,8 @@ RSpec.describe 'Clusters', :js do
allow_any_instance_of(GoogleApi::CloudPlatform::Client)
.to receive(:projects_zones_clusters_create) do
- OpenStruct.new(
+ double(
+ 'cluster_control',
self_link: 'projects/gcp-project-12345/zones/us-central1-a/operations/ope-123',
status: 'RUNNING'
)
diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb
index d88ff5c1aa5..bfd54b9c6da 100644
--- a/spec/features/projects/environments/environment_spec.rb
+++ b/spec/features/projects/environments/environment_spec.rb
@@ -132,6 +132,29 @@ RSpec.describe 'Environment' do
end
end
+ context 'with upcoming deployments' do
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+
+ let!(:runnind_deployment_1) { create(:deployment, environment: environment, deployable: build, status: :running) }
+ let!(:runnind_deployment_2) { create(:deployment, environment: environment, deployable: build, status: :running) }
+ # Success deployments must have present `finished_at`. We'll backfill in the future.
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/350618 for more information.
+ let!(:success_without_finished_at) { create(:deployment, environment: environment, deployable: build, status: :success, finished_at: nil) }
+
+ before do
+ visit_environment(environment)
+ end
+
+ # This ordering is unexpected and to be fixed.
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/350618 for more information.
+ it 'shows upcoming deployments in unordered way' do
+ displayed_ids = find_all('[data-testid="deployment-id"]').map { |e| e.text }
+ internal_ids = [runnind_deployment_1, runnind_deployment_2, success_without_finished_at].map { |d| "##{d.iid}" }
+ expect(displayed_ids).to match_array(internal_ids)
+ end
+ end
+
context 'with related deployable present' do
let(:pipeline) { create(:ci_pipeline, project: project) }
let(:build) { create(:ci_build, pipeline: pipeline) }
diff --git a/spec/features/projects/environments_pod_logs_spec.rb b/spec/features/projects/environments_pod_logs_spec.rb
index 7d31de2b418..531eae1d638 100644
--- a/spec/features/projects/environments_pod_logs_spec.rb
+++ b/spec/features/projects/environments_pod_logs_spec.rb
@@ -21,7 +21,7 @@ RSpec.describe 'Environment > Pod Logs', :js, :kubeclient do
stub_kubeclient_ingresses(environment.deployment_namespace)
stub_kubeclient_nodes_and_nodes_metrics(cluster.platform.api_url)
- sign_in(project.owner)
+ sign_in(project.first_owner)
end
it "shows environments in dropdown" do
diff --git a/spec/features/projects/files/dockerfile_dropdown_spec.rb b/spec/features/projects/files/dockerfile_dropdown_spec.rb
index 11663158b33..3a0cc61d9c6 100644
--- a/spec/features/projects/files/dockerfile_dropdown_spec.rb
+++ b/spec/features/projects/files/dockerfile_dropdown_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe 'Projects > Files > User wants to add a Dockerfile file', :js do
before do
project = create(:project, :repository)
- sign_in project.owner
+ sign_in project.first_owner
visit project_new_blob_path(project, 'master', file_name: 'Dockerfile')
end
diff --git a/spec/features/projects/files/edit_file_soft_wrap_spec.rb b/spec/features/projects/files/edit_file_soft_wrap_spec.rb
index fda024e893d..e08c53a67dd 100644
--- a/spec/features/projects/files/edit_file_soft_wrap_spec.rb
+++ b/spec/features/projects/files/edit_file_soft_wrap_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'Projects > Files > User uses soft wrap while editing file', :js do
before do
project = create(:project, :repository)
- user = project.owner
+ user = project.first_owner
sign_in user
visit project_new_blob_path(project, 'master', file_name: 'test_file-name')
diff --git a/spec/features/projects/files/editing_a_file_spec.rb b/spec/features/projects/files/editing_a_file_spec.rb
index 819864b3def..e256bec2a1c 100644
--- a/spec/features/projects/files/editing_a_file_spec.rb
+++ b/spec/features/projects/files/editing_a_file_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe 'Projects > Files > User wants to edit a file' do
let(:project) { create(:project, :repository) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:commit_params) do
{
start_branch: project.default_branch,
diff --git a/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb b/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb
index 94190889ace..a283f7d128c 100644
--- a/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb
+++ b/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe 'Projects > Files > User views files page' do
let(:project) { create(:forked_project_with_submodules) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
before do
sign_in user
diff --git a/spec/features/projects/files/find_file_keyboard_spec.rb b/spec/features/projects/files/find_file_keyboard_spec.rb
index 4293183fd9a..9ae3be4993b 100644
--- a/spec/features/projects/files/find_file_keyboard_spec.rb
+++ b/spec/features/projects/files/find_file_keyboard_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe 'Projects > Files > Find file keyboard shortcuts', :js do
let(:project) { create(:project, :repository) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
before do
sign_in user
diff --git a/spec/features/projects/files/gitignore_dropdown_spec.rb b/spec/features/projects/files/gitignore_dropdown_spec.rb
index d47eaee2e79..4a92216f46c 100644
--- a/spec/features/projects/files/gitignore_dropdown_spec.rb
+++ b/spec/features/projects/files/gitignore_dropdown_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe 'Projects > Files > User wants to add a .gitignore file', :js do
before do
project = create(:project, :repository)
- sign_in project.owner
+ sign_in project.first_owner
visit project_new_blob_path(project, 'master', file_name: '.gitignore')
end
diff --git a/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb b/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb
index fc199f66490..cdf6c219ea5 100644
--- a/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb
+++ b/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe 'Projects > Files > User wants to add a .gitlab-ci.yml file', :js
let_it_be(:project) { create(:project, :repository) }
before do
- sign_in project.owner
+ sign_in project.first_owner
visit project_new_blob_path(project, 'master', file_name: filename, **params)
end
diff --git a/spec/features/projects/files/project_owner_creates_license_file_spec.rb b/spec/features/projects/files/project_owner_creates_license_file_spec.rb
index ab62e8aabc0..4a0b1f4c548 100644
--- a/spec/features/projects/files/project_owner_creates_license_file_spec.rb
+++ b/spec/features/projects/files/project_owner_creates_license_file_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe 'Projects > Files > Project owner creates a license file', :js do
let(:project) { create(:project, :repository) }
- let(:project_maintainer) { project.owner }
+ let(:project_maintainer) { project.first_owner }
before do
project.repository.delete_file(project_maintainer, 'LICENSE',
diff --git a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
index 37583870cfd..ca384291c12 100644
--- a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
+++ b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'Projects > Files > Project owner sees a link to create a license
include WebIdeSpecHelpers
let(:project) { create(:project_empty_repo) }
- let(:project_maintainer) { project.owner }
+ let(:project_maintainer) { project.first_owner }
before do
sign_in(project_maintainer)
diff --git a/spec/features/projects/files/template_type_dropdown_spec.rb b/spec/features/projects/files/template_type_dropdown_spec.rb
index ca9ce841a92..9cdb5eeb076 100644
--- a/spec/features/projects/files/template_type_dropdown_spec.rb
+++ b/spec/features/projects/files/template_type_dropdown_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe 'Projects > Files > Template type dropdown selector', :js do
let(:project) { create(:project, :repository) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
before do
sign_in user
diff --git a/spec/features/projects/files/undo_template_spec.rb b/spec/features/projects/files/undo_template_spec.rb
index 560cb53ead2..0b2daf12063 100644
--- a/spec/features/projects/files/undo_template_spec.rb
+++ b/spec/features/projects/files/undo_template_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe 'Projects > Files > Template Undo Button', :js do
let(:project) { create(:project, :repository) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
before do
sign_in user
diff --git a/spec/features/projects/files/user_browses_a_tree_with_a_folder_containing_only_a_folder_spec.rb b/spec/features/projects/files/user_browses_a_tree_with_a_folder_containing_only_a_folder_spec.rb
index 4d9da783f98..220572c6a6d 100644
--- a/spec/features/projects/files/user_browses_a_tree_with_a_folder_containing_only_a_folder_spec.rb
+++ b/spec/features/projects/files/user_browses_a_tree_with_a_folder_containing_only_a_folder_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
# This is a regression test for https://gitlab.com/gitlab-org/gitlab-foss/issues/37569
RSpec.describe 'Projects > Files > User browses a tree with a folder containing only a folder', :js do
let(:project) { create(:project, :empty_repo) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
before do
project.repository.create_dir(user, 'foo/bar', branch_name: 'master', message: 'Add the foo/bar folder')
diff --git a/spec/features/projects/files/user_browses_files_spec.rb b/spec/features/projects/files/user_browses_files_spec.rb
index 508dec70db6..9b4d1502bc8 100644
--- a/spec/features/projects/files/user_browses_files_spec.rb
+++ b/spec/features/projects/files/user_browses_files_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe "User browses files" do
+RSpec.describe "User browses files", :js do
include RepoHelpers
let(:fork_message) do
@@ -13,7 +13,7 @@ RSpec.describe "User browses files" do
let(:project) { create(:project, :repository, name: "Shop") }
let(:project2) { create(:project, :repository, name: "Another Project", path: "another-project") }
let(:tree_path_root_ref) { project_tree_path(project, project.repository.root_ref) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
before do
sign_in(user)
@@ -340,32 +340,37 @@ RSpec.describe "User browses files" do
let(:newrev) { project.repository.commit('master').sha }
before do
- stub_feature_flags(refactor_blob_viewer: false) # This stub will be removed in https://gitlab.com/gitlab-org/gitlab/-/issues/350456
create_file_in_repo(project, 'master', 'master', filename, 'Test file')
path = File.join('master', filename)
visit(project_blob_path(project, path))
+ wait_for_requests
end
- it "shows a raw file content" do
- click_link("Open raw")
+ it "shows raw file content in a new tab" do
+ new_tab = window_opened_by {click_link 'Open raw'}
- expect(source).to eq("") # Body is filled in by gitlab-workhorse
+ within_window new_tab do
+ expect(page).to have_content("Test file")
+ end
end
end
context "when browsing a raw file" do
before do
- stub_feature_flags(refactor_blob_viewer: false) # This stub will be removed in https://gitlab.com/gitlab-org/gitlab/-/issues/350456
- path = File.join(RepoHelpers.sample_commit.id, RepoHelpers.sample_blob.path)
+ visit(tree_path_root_ref)
+ wait_for_requests
- visit(project_blob_path(project, path))
+ click_link(".gitignore")
+ wait_for_requests
end
- it "shows a raw file content" do
- click_link("Open raw")
+ it "shows raw file content in a new tab" do
+ new_tab = window_opened_by {click_link 'Open raw'}
- expect(source).to eq("") # Body is filled in by gitlab-workhorse
+ within_window new_tab do
+ expect(page).to have_content("*.rbc")
+ end
end
end
end
diff --git a/spec/features/projects/files/user_browses_lfs_files_spec.rb b/spec/features/projects/files/user_browses_lfs_files_spec.rb
index 17699847704..3976df849fa 100644
--- a/spec/features/projects/files/user_browses_lfs_files_spec.rb
+++ b/spec/features/projects/files/user_browses_lfs_files_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe 'Projects > Files > User browses LFS files' do
let(:project) { create(:project, :repository) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
before do
sign_in(user)
diff --git a/spec/features/projects/files/user_searches_for_files_spec.rb b/spec/features/projects/files/user_searches_for_files_spec.rb
index 7fd7dfff279..cce73d06f94 100644
--- a/spec/features/projects/files/user_searches_for_files_spec.rb
+++ b/spec/features/projects/files/user_searches_for_files_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'Projects > Files > User searches for files' do
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
before do
sign_in(user)
diff --git a/spec/features/projects/gfm_autocomplete_load_spec.rb b/spec/features/projects/gfm_autocomplete_load_spec.rb
index f4cd65bcba1..a7d68b07dd3 100644
--- a/spec/features/projects/gfm_autocomplete_load_spec.rb
+++ b/spec/features/projects/gfm_autocomplete_load_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'GFM autocomplete loading', :js do
let(:project) { create(:project) }
before do
- sign_in(project.owner)
+ sign_in(project.first_owner)
visit project_path(project)
end
diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb
index 2fbec4e22f4..1e5c5d33ad9 100644
--- a/spec/features/projects/import_export/import_file_spec.rb
+++ b/spec/features/projects/import_export/import_file_spec.rb
@@ -41,7 +41,7 @@ RSpec.describe 'Import/Export - project import integration test', :js do
project = Project.last
expect(project).not_to be_nil
- expect(page).to have_content("Project 'test-project-path' is being imported")
+ expect(page).to have_content("Project 'Test Project Name' is being imported")
end
it 'invalid project' do
diff --git a/spec/features/projects/services/disable_triggers_spec.rb b/spec/features/projects/integrations/disable_triggers_spec.rb
index c6413685f38..b039d610ecb 100644
--- a/spec/features/projects/services/disable_triggers_spec.rb
+++ b/spec/features/projects/integrations/disable_triggers_spec.rb
@@ -3,16 +3,16 @@
require 'spec_helper'
RSpec.describe 'Disable individual triggers', :js do
- include_context 'project service activation'
+ include_context 'project integration activation'
let(:checkbox_selector) { 'input[name$="_events]"]' }
before do
- visit_project_integration(service_name)
+ visit_project_integration(integration_name)
end
- context 'service has multiple supported events' do
- let(:service_name) { 'Jenkins' }
+ context 'integration has multiple supported events' do
+ let(:integration_name) { 'Jenkins' }
it 'shows trigger checkboxes' do
event_count = Integrations::Jenkins.supported_events.count
@@ -22,8 +22,8 @@ RSpec.describe 'Disable individual triggers', :js do
end
end
- context 'services only has one supported event' do
- let(:service_name) { 'Asana' }
+ context 'integrations only has one supported event' do
+ let(:integration_name) { 'Asana' }
it "doesn't show unnecessary Trigger checkboxes" do
expect(page).not_to have_content "Trigger"
diff --git a/spec/features/projects/integrations/project_integrations_spec.rb b/spec/features/projects/integrations/project_integrations_spec.rb
new file mode 100644
index 00000000000..708a5bca8c1
--- /dev/null
+++ b/spec/features/projects/integrations/project_integrations_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Project integrations', :js do
+ include_context 'project integration activation'
+
+ it_behaves_like 'integration settings form' do
+ let(:integrations) { project.find_or_initialize_integrations }
+
+ def navigate_to_integration(integration)
+ visit_project_integration(integration.title)
+ end
+ end
+end
diff --git a/spec/features/projects/services/prometheus_external_alerts_spec.rb b/spec/features/projects/integrations/prometheus_external_alerts_spec.rb
index c2ae72ddb5e..7e56ca13e23 100644
--- a/spec/features/projects/services/prometheus_external_alerts_spec.rb
+++ b/spec/features/projects/integrations/prometheus_external_alerts_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'Prometheus external alerts', :js do
- include_context 'project service activation'
+ include_context 'project integration activation'
let(:alerts_section_selector) { '.js-prometheus-alerts' }
let(:alerts_section) { page.find(alerts_section_selector) }
diff --git a/spec/features/projects/integrations/user_activates_asana_spec.rb b/spec/features/projects/integrations/user_activates_asana_spec.rb
index cf2290383e8..9ec9f00529a 100644
--- a/spec/features/projects/integrations/user_activates_asana_spec.rb
+++ b/spec/features/projects/integrations/user_activates_asana_spec.rb
@@ -3,9 +3,9 @@
require 'spec_helper'
RSpec.describe 'User activates Asana' do
- include_context 'project service activation'
+ include_context 'project integration activation'
- it 'activates service', :js do
+ it 'activates integration', :js do
visit_project_integration('Asana')
fill_in('API key', with: 'verySecret')
fill_in('Restrict to branch', with: 'verySecret')
diff --git a/spec/features/projects/integrations/user_activates_assembla_spec.rb b/spec/features/projects/integrations/user_activates_assembla_spec.rb
index 63cc424a641..be9034ec5ba 100644
--- a/spec/features/projects/integrations/user_activates_assembla_spec.rb
+++ b/spec/features/projects/integrations/user_activates_assembla_spec.rb
@@ -3,13 +3,13 @@
require 'spec_helper'
RSpec.describe 'User activates Assembla' do
- include_context 'project service activation'
+ include_context 'project integration activation'
before do
stub_request(:post, /.*atlas.assembla.com.*/)
end
- it 'activates service', :js do
+ it 'activates integration', :js do
visit_project_integration('Assembla')
fill_in('Token', with: 'verySecret')
diff --git a/spec/features/projects/integrations/user_activates_atlassian_bamboo_ci_spec.rb b/spec/features/projects/integrations/user_activates_atlassian_bamboo_ci_spec.rb
index 91db375be3a..49f62a34bd2 100644
--- a/spec/features/projects/integrations/user_activates_atlassian_bamboo_ci_spec.rb
+++ b/spec/features/projects/integrations/user_activates_atlassian_bamboo_ci_spec.rb
@@ -3,13 +3,13 @@
require 'spec_helper'
RSpec.describe 'User activates Atlassian Bamboo CI' do
- include_context 'project service activation'
+ include_context 'project integration activation'
before do
stub_request(:get, /.*bamboo.example.com.*/)
end
- it 'activates service', :js do
+ it 'activates integration', :js do
visit_project_integration('Atlassian Bamboo')
fill_in('Bamboo URL', with: 'http://bamboo.example.com')
fill_in('Build key', with: 'KEY')
diff --git a/spec/features/projects/services/user_activates_emails_on_push_spec.rb b/spec/features/projects/integrations/user_activates_emails_on_push_spec.rb
index 5a075fc61e8..168779aad07 100644
--- a/spec/features/projects/services/user_activates_emails_on_push_spec.rb
+++ b/spec/features/projects/integrations/user_activates_emails_on_push_spec.rb
@@ -3,9 +3,9 @@
require 'spec_helper'
RSpec.describe 'User activates Emails on push' do
- include_context 'project service activation'
+ include_context 'project integration activation'
- it 'activates service', :js do
+ it 'activates integration', :js do
visit_project_integration('Emails on push')
fill_in('Recipients', with: 'qa@company.name')
diff --git a/spec/features/projects/integrations/user_activates_flowdock_spec.rb b/spec/features/projects/integrations/user_activates_flowdock_spec.rb
index 4a4d7bbecfd..df1a4feddfb 100644
--- a/spec/features/projects/integrations/user_activates_flowdock_spec.rb
+++ b/spec/features/projects/integrations/user_activates_flowdock_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'User activates Flowdock' do
- include_context 'project service activation' do
+ include_context 'project integration activation' do
let(:project) { create(:project, :repository) }
end
@@ -11,7 +11,7 @@ RSpec.describe 'User activates Flowdock' do
stub_request(:post, /.*api.flowdock.com.*/)
end
- it 'activates service', :js do
+ it 'activates integration', :js do
visit_project_integration('Flowdock')
fill_in('Token', with: 'verySecret')
diff --git a/spec/features/projects/services/user_activates_irker_spec.rb b/spec/features/projects/integrations/user_activates_irker_spec.rb
index 004aa116bb3..23b5f2a5c47 100644
--- a/spec/features/projects/services/user_activates_irker_spec.rb
+++ b/spec/features/projects/integrations/user_activates_irker_spec.rb
@@ -3,9 +3,9 @@
require 'spec_helper'
RSpec.describe 'User activates irker (IRC gateway)' do
- include_context 'project service activation'
+ include_context 'project integration activation'
- it 'activates service', :js do
+ it 'activates integration', :js do
visit_project_integration('irker (IRC gateway)')
check('Colorize messages')
fill_in('Recipients', with: 'irc://chat.freenode.net/#commits')
diff --git a/spec/features/projects/services/user_activates_issue_tracker_spec.rb b/spec/features/projects/integrations/user_activates_issue_tracker_spec.rb
index 27c23e7beb5..b9c2c539899 100644
--- a/spec/features/projects/services/user_activates_issue_tracker_spec.rb
+++ b/spec/features/projects/integrations/user_activates_issue_tracker_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'User activates issue tracker', :js do
- include_context 'project service activation'
+ include_context 'project integration activation'
let(:url) { 'http://tracker.example.com' }
@@ -17,7 +17,7 @@ RSpec.describe 'User activates issue tracker', :js do
end
shared_examples 'external issue tracker activation' do |tracker:, skip_new_issue_url: false, skip_test: false|
- describe 'user sets and activates the Service' do
+ describe 'user sets and activates the integration' do
context 'when the connection test succeeds' do
before do
stub_request(:head, url).to_return(headers: { 'Content-Type' => 'application/json' })
@@ -32,7 +32,7 @@ RSpec.describe 'User activates issue tracker', :js do
end
end
- it 'activates the service' do
+ it 'activates the integration' do
expect(page).to have_content("#{tracker} settings saved and active.")
expect(current_path).to eq(edit_project_integration_path(project, tracker.parameterize(separator: '_')))
end
@@ -45,7 +45,7 @@ RSpec.describe 'User activates issue tracker', :js do
end
context 'when the connection test fails' do
- it 'activates the service' do
+ it 'activates the integration' do
stub_request(:head, url).to_raise(Gitlab::HTTP::Error)
visit_project_integration(tracker)
@@ -63,7 +63,7 @@ RSpec.describe 'User activates issue tracker', :js do
end
end
- describe 'user disables the service' do
+ describe 'user disables the integration' do
before do
visit_project_integration(tracker)
fill_form(disable: true, skip_new_issue_url: skip_new_issue_url)
@@ -71,7 +71,7 @@ RSpec.describe 'User activates issue tracker', :js do
click_button('Save changes')
end
- it 'saves but does not activate the service' do
+ it 'saves but does not activate the integration' do
expect(page).to have_content("#{tracker} settings saved, but not active.")
expect(current_path).to eq(edit_project_integration_path(project, tracker.parameterize(separator: '_')))
end
diff --git a/spec/features/projects/services/user_activates_jetbrains_teamcity_ci_spec.rb b/spec/features/projects/integrations/user_activates_jetbrains_teamcity_ci_spec.rb
index 17bfe8fc1e2..e6f2e462b8c 100644
--- a/spec/features/projects/services/user_activates_jetbrains_teamcity_ci_spec.rb
+++ b/spec/features/projects/integrations/user_activates_jetbrains_teamcity_ci_spec.rb
@@ -3,13 +3,13 @@
require 'spec_helper'
RSpec.describe 'User activates JetBrains TeamCity CI' do
- include_context 'project service activation'
+ include_context 'project integration activation'
before do
stub_request(:post, /.*teamcity.example.com.*/)
end
- it 'activates service', :js do
+ it 'activates integration', :js do
visit_project_integration('JetBrains TeamCity')
check('Push')
check('Merge Request')
diff --git a/spec/features/projects/integrations/user_activates_jira_spec.rb b/spec/features/projects/integrations/user_activates_jira_spec.rb
index 50010950f0e..7562dc00092 100644
--- a/spec/features/projects/integrations/user_activates_jira_spec.rb
+++ b/spec/features/projects/integrations/user_activates_jira_spec.rb
@@ -3,14 +3,14 @@
require 'spec_helper'
RSpec.describe 'User activates Jira', :js do
- include_context 'project service activation'
- include_context 'project service Jira context'
+ include_context 'project integration activation'
+ include_context 'project integration Jira context'
before do
stub_request(:get, test_url).to_return(body: { key: 'value' }.to_json)
end
- describe 'user tests Jira Service' do
+ describe 'user tests Jira integration' do
context 'when Jira connection test succeeds' do
before do
visit_project_integration('Jira')
@@ -18,7 +18,7 @@ RSpec.describe 'User activates Jira', :js do
click_test_then_save_integration(expect_test_to_fail: false)
end
- it 'activates the Jira service' do
+ it 'activates the Jira integration' do
expect(page).to have_content('Jira settings saved and active.')
expect(current_path).to eq(edit_project_integration_path(project, :jira))
end
@@ -46,7 +46,7 @@ RSpec.describe 'User activates Jira', :js do
end
end
- it 'activates the Jira service' do
+ it 'activates the Jira integration' do
stub_request(:get, test_url).with(basic_auth: %w(username password))
.to_raise(JIRA::HTTPError.new(double(message: 'message')))
@@ -60,7 +60,7 @@ RSpec.describe 'User activates Jira', :js do
end
end
- describe 'user disables the Jira Service' do
+ describe 'user disables the Jira integration' do
include JiraServiceHelper
before do
@@ -70,7 +70,7 @@ RSpec.describe 'User activates Jira', :js do
click_save_integration
end
- it 'saves but does not activate the Jira service' do
+ it 'saves but does not activate the Jira integration' do
expect(page).to have_content('Jira settings saved, but not active.')
expect(current_path).to eq(edit_project_integration_path(project, :jira))
end
diff --git a/spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb b/spec/features/projects/integrations/user_activates_mattermost_slash_command_spec.rb
index 74919a99f04..ed0877ab0e9 100644
--- a/spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb
+++ b/spec/features/projects/integrations/user_activates_mattermost_slash_command_spec.rb
@@ -4,14 +4,14 @@ require 'spec_helper'
RSpec.describe 'Set up Mattermost slash commands', :js do
describe 'user visits the mattermost slash command config page' do
- include_context 'project service activation'
+ include_context 'project integration activation'
before do
stub_mattermost_setting(enabled: mattermost_enabled)
visit_project_integration('Mattermost slash commands')
end
- context 'mattermost service is enabled' do
+ context 'mattermost integration is enabled' do
let(:mattermost_enabled) { true }
describe 'activation' do
@@ -84,7 +84,9 @@ RSpec.describe 'Set up Mattermost slash commands', :js do
end
it 'shows an error alert with the error message if there is an error requesting teams' do
- allow_any_instance_of(Integrations::MattermostSlashCommands).to receive(:list_teams) { [[], 'test mattermost error message'] }
+ allow_next_instance_of(Integrations::MattermostSlashCommands) do |integration|
+ allow(integration).to receive(:list_teams).and_return([[], 'test mattermost error message'])
+ end
click_link 'Add to Mattermost'
@@ -113,7 +115,9 @@ RSpec.describe 'Set up Mattermost slash commands', :js do
def stub_teams(count: 0)
teams = create_teams(count)
- allow_any_instance_of(Integrations::MattermostSlashCommands).to receive(:list_teams) { [teams, nil] }
+ allow_next_instance_of(Integrations::MattermostSlashCommands) do |integration|
+ allow(integration).to receive(:list_teams).and_return([teams, nil])
+ end
teams
end
@@ -129,7 +133,7 @@ RSpec.describe 'Set up Mattermost slash commands', :js do
end
end
- context 'mattermost service is not enabled' do
+ context 'mattermost integration is not enabled' do
let(:mattermost_enabled) { false }
it 'shows the correct trigger url' do
diff --git a/spec/features/projects/services/user_activates_packagist_spec.rb b/spec/features/projects/integrations/user_activates_packagist_spec.rb
index 87303cf8fb4..0892843e840 100644
--- a/spec/features/projects/services/user_activates_packagist_spec.rb
+++ b/spec/features/projects/integrations/user_activates_packagist_spec.rb
@@ -3,13 +3,13 @@
require 'spec_helper'
RSpec.describe 'User activates Packagist' do
- include_context 'project service activation'
+ include_context 'project integration activation'
before do
stub_request(:post, /.*packagist.org.*/)
end
- it 'activates service', :js do
+ it 'activates integration', :js do
visit_project_integration('Packagist')
fill_in('Username', with: 'theUser')
fill_in('Token', with: 'verySecret')
diff --git a/spec/features/projects/integrations/user_activates_pivotaltracker_spec.rb b/spec/features/projects/integrations/user_activates_pivotaltracker_spec.rb
index ea34a766719..fe6ed786ace 100644
--- a/spec/features/projects/integrations/user_activates_pivotaltracker_spec.rb
+++ b/spec/features/projects/integrations/user_activates_pivotaltracker_spec.rb
@@ -3,13 +3,13 @@
require 'spec_helper'
RSpec.describe 'User activates PivotalTracker' do
- include_context 'project service activation'
+ include_context 'project integration activation'
before do
stub_request(:post, /.*www.pivotaltracker.com.*/)
end
- it 'activates service', :js do
+ it 'activates integration', :js do
visit_project_integration('Pivotal Tracker')
fill_in('Token', with: 'verySecret')
diff --git a/spec/features/projects/services/user_activates_prometheus_spec.rb b/spec/features/projects/integrations/user_activates_prometheus_spec.rb
index 73ad8088be2..80629af6fce 100644
--- a/spec/features/projects/services/user_activates_prometheus_spec.rb
+++ b/spec/features/projects/integrations/user_activates_prometheus_spec.rb
@@ -3,13 +3,13 @@
require 'spec_helper'
RSpec.describe 'User activates Prometheus' do
- include_context 'project service activation'
+ include_context 'project integration activation'
before do
stub_request(:get, /.*prometheus.example.com.*/)
end
- it 'does not activate service and informs about deprecation', :js do
+ it 'does not activate integration and informs about deprecation', :js do
visit_project_integration('Prometheus')
check('Active')
fill_in('API URL', with: 'http://prometheus.example.com')
diff --git a/spec/features/projects/services/user_activates_pushover_spec.rb b/spec/features/projects/integrations/user_activates_pushover_spec.rb
index d92f69e700a..616efdc836f 100644
--- a/spec/features/projects/services/user_activates_pushover_spec.rb
+++ b/spec/features/projects/integrations/user_activates_pushover_spec.rb
@@ -3,13 +3,13 @@
require 'spec_helper'
RSpec.describe 'User activates Pushover' do
- include_context 'project service activation'
+ include_context 'project integration activation'
before do
stub_request(:post, /.*api.pushover.net.*/)
end
- it 'activates service', :js do
+ it 'activates integration', :js do
visit_project_integration('Pushover')
fill_in('API key', with: 'verySecret')
fill_in('User key', with: 'verySecret')
diff --git a/spec/features/projects/services/user_activates_slack_notifications_spec.rb b/spec/features/projects/integrations/user_activates_slack_notifications_spec.rb
index 38b6ad84c77..616469c5df8 100644
--- a/spec/features/projects/services/user_activates_slack_notifications_spec.rb
+++ b/spec/features/projects/integrations/user_activates_slack_notifications_spec.rb
@@ -3,14 +3,14 @@
require 'spec_helper'
RSpec.describe 'User activates Slack notifications', :js do
- include_context 'project service activation'
+ include_context 'project integration activation'
- context 'when service is not configured yet' do
+ context 'when integration is not configured yet' do
before do
visit_project_integration('Slack notifications')
end
- it 'activates service' do
+ it 'activates integration' do
fill_in('Webhook', with: 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685')
click_test_then_save_integration
@@ -19,7 +19,7 @@ RSpec.describe 'User activates Slack notifications', :js do
end
end
- context 'when service is already configured' do
+ context 'when integration is already configured' do
let(:integration) { Integrations::Slack.new }
let(:project) { create(:project, slack_integration: integration) }
diff --git a/spec/features/projects/services/user_activates_slack_slash_command_spec.rb b/spec/features/projects/integrations/user_activates_slack_slash_command_spec.rb
index d46d1f739b7..7ec469070ea 100644
--- a/spec/features/projects/services/user_activates_slack_slash_command_spec.rb
+++ b/spec/features/projects/integrations/user_activates_slack_slash_command_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'Slack slash commands', :js do
- include_context 'project service activation'
+ include_context 'project integration activation'
before do
visit_project_integration('Slack slash commands')
diff --git a/spec/features/projects/integrations/user_uses_inherited_settings_spec.rb b/spec/features/projects/integrations/user_uses_inherited_settings_spec.rb
index d2c4418f0d6..fcb04c338a9 100644
--- a/spec/features/projects/integrations/user_uses_inherited_settings_spec.rb
+++ b/spec/features/projects/integrations/user_uses_inherited_settings_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'User uses inherited settings', :js do
include JiraServiceHelper
- include_context 'project service activation'
+ include_context 'project integration activation'
before do
stub_jira_integration_test
diff --git a/spec/features/projects/services/user_views_services_spec.rb b/spec/features/projects/integrations/user_views_services_spec.rb
index 201a58ba379..559461f911f 100644
--- a/spec/features/projects/services/user_views_services_spec.rb
+++ b/spec/features/projects/integrations/user_views_services_spec.rb
@@ -2,10 +2,10 @@
require 'spec_helper'
-RSpec.describe 'User views services', :js do
- include_context 'project service activation'
+RSpec.describe 'User views integrations', :js do
+ include_context 'project integration activation'
- it 'shows the list of available services' do
+ it 'shows the list of available integrations' do
visit_project_integrations
expect(page).to have_content('Integrations')
diff --git a/spec/features/projects/issues/design_management/user_uploads_designs_spec.rb b/spec/features/projects/issues/design_management/user_uploads_designs_spec.rb
index 211576a93f3..762f9c33510 100644
--- a/spec/features/projects/issues/design_management/user_uploads_designs_spec.rb
+++ b/spec/features/projects/issues/design_management/user_uploads_designs_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'User uploads new design', :js do
include DesignManagementTestHelpers
let(:project) { create(:project_empty_repo, :public) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:issue) { create(:issue, project: project) }
before do
diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb
index 7ccd5c51493..a65d2d15c12 100644
--- a/spec/features/projects/jobs_spec.rb
+++ b/spec/features/projects/jobs_spec.rb
@@ -615,7 +615,7 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state do
end
context 'when the user is not able to view the cluster' do
- let(:user_access_level) { :developer }
+ let(:user_access_level) { :reporter }
it 'includes only the name of the cluster without a link' do
expect(page).to have_content 'using cluster the-cluster'
diff --git a/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb b/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb
index c8a9f959188..c9fee9bee7a 100644
--- a/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb
+++ b/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb
@@ -21,6 +21,6 @@ RSpec.describe 'Projects > Members > Group member cannot leave group project' do
it 'renders a flash message if attempting to leave by url', :js do
visit project_path(project, leave: 1)
- expect(find('.flash-alert')).to have_content 'You do not have permission to leave this project'
+ expect(find('[data-testid="alert-danger"]')).to have_content 'You do not have permission to leave this project'
end
end
diff --git a/spec/features/projects/members/invite_group_spec.rb b/spec/features/projects/members/invite_group_spec.rb
index b674cad0312..066e0b0d20f 100644
--- a/spec/features/projects/members/invite_group_spec.rb
+++ b/spec/features/projects/members/invite_group_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe 'Project > Members > Invite group', :js do
include Spec::Support::Helpers::Features::MembersHelpers
include Spec::Support::Helpers::Features::InviteMembersModalHelper
- let(:maintainer) { create(:user) }
+ let_it_be(:maintainer) { create(:user) }
using RSpec::Parameterized::TableSyntax
@@ -190,17 +190,26 @@ RSpec.describe 'Project > Members > Invite group', :js do
end
describe 'the groups dropdown' do
- context 'with multiple groups to choose from' do
- let(:project) { create(:project) }
+ let_it_be(:parent_group) { create(:group, :public) }
+ let_it_be(:project_group) { create(:group, :public, parent: parent_group) }
+ let_it_be(:public_sub_subgroup) { create(:group, :public, parent: project_group) }
+ let_it_be(:public_sibbling_group) { create(:group, :public, parent: parent_group) }
+ let_it_be(:private_sibbling_group) { create(:group, :private, parent: parent_group) }
+ let_it_be(:private_membership_group) { create(:group, :private) }
+ let_it_be(:public_membership_group) { create(:group, :public) }
+ let_it_be(:project) { create(:project, group: project_group) }
- it 'includes multiple groups' do
- project.add_maintainer(maintainer)
- sign_in(maintainer)
+ before do
+ private_membership_group.add_guest(maintainer)
+ public_membership_group.add_maintainer(maintainer)
+
+ sign_in(maintainer)
+ end
- group1 = create(:group)
- group1.add_owner(maintainer)
- group2 = create(:group)
- group2.add_owner(maintainer)
+ context 'for a project in a nested group' do
+ it 'does not show the groups inherited from projects' do
+ project.add_maintainer(maintainer)
+ public_sibbling_group.add_maintainer(maintainer)
visit project_project_members_path(project)
@@ -208,37 +217,90 @@ RSpec.describe 'Project > Members > Invite group', :js do
click_on 'Select a group'
wait_for_requests
- expect(page).to have_button(group1.name)
- expect(page).to have_button(group2.name)
- end
- end
-
- context 'for a project in a nested group' do
- let(:group) { create(:group) }
- let!(:nested_group) { create(:group, parent: group) }
- let!(:group_to_share_with) { create(:group) }
- let!(:project) { create(:project, namespace: nested_group) }
+ page.within('[data-testid="group-select-dropdown"]') do
+ expect_to_have_group(public_membership_group)
+ expect_to_have_group(public_sibbling_group)
+ expect_to_have_group(private_membership_group)
- before do
- project.add_maintainer(maintainer)
- sign_in(maintainer)
- group.add_maintainer(maintainer)
- group_to_share_with.add_maintainer(maintainer)
+ expect_not_to_have_group(public_sub_subgroup)
+ expect_not_to_have_group(private_sibbling_group)
+ expect_not_to_have_group(parent_group)
+ expect_not_to_have_group(project_group)
+ end
end
- # This behavior should be changed to exclude the ancestor and project
- # group from the options once issue is fixed for the modal:
- # https://gitlab.com/gitlab-org/gitlab/-/issues/329835
- it 'the groups dropdown does show ancestors and the project group' do
+ it 'does not show the ancestors or project group', :aggregate_failures do
+ parent_group.add_maintainer(maintainer)
+
visit project_project_members_path(project)
click_on 'Invite a group'
click_on 'Select a group'
wait_for_requests
- expect(page).to have_button(group_to_share_with.name)
- expect(page).to have_button(group.name)
- expect(page).to have_button(nested_group.name)
+ page.within('[data-testid="group-select-dropdown"]') do
+ expect_to_have_group(public_membership_group)
+ expect_to_have_group(public_sibbling_group)
+ expect_to_have_group(private_membership_group)
+ expect_to_have_group(public_sub_subgroup)
+ expect_to_have_group(private_sibbling_group)
+
+ expect_not_to_have_group(parent_group)
+ expect_not_to_have_group(project_group)
+ end
+ end
+
+ context 'when invite_members_group_modal feature disabled' do
+ let(:group_invite_dropdown) { find('#select2-results-2') }
+
+ before do
+ stub_feature_flags(invite_members_group_modal: false)
+ end
+
+ it 'does not show the groups inherited from projects', :aggregate_failures do
+ project.add_maintainer(maintainer)
+ public_sibbling_group.add_maintainer(maintainer)
+
+ visit project_project_members_path(project)
+
+ click_on 'Invite group'
+ click_on 'Search for a group'
+ wait_for_requests
+
+ expect(group_invite_dropdown).to have_text(public_membership_group.full_path)
+ expect(group_invite_dropdown).to have_text(public_sibbling_group.full_path)
+ expect(group_invite_dropdown).to have_text(private_membership_group.full_path)
+ expect(group_invite_dropdown).not_to have_text(public_sub_subgroup.full_path)
+ expect(group_invite_dropdown).not_to have_text(private_sibbling_group.full_path)
+ expect(group_invite_dropdown).not_to have_text(parent_group.full_path, exact: true)
+ expect(group_invite_dropdown).not_to have_text(project_group.full_path, exact: true)
+ end
+
+ it 'does not show the ancestors or project group', :aggregate_failures do
+ parent_group.add_maintainer(maintainer)
+
+ visit project_project_members_path(project)
+
+ click_on 'Invite group'
+ click_on 'Search for a group'
+ wait_for_requests
+
+ expect(group_invite_dropdown).to have_text(public_membership_group.full_path)
+ expect(group_invite_dropdown).to have_text(public_sub_subgroup.full_path)
+ expect(group_invite_dropdown).to have_text(public_sibbling_group.full_path)
+ expect(group_invite_dropdown).to have_text(private_sibbling_group.full_path)
+ expect(group_invite_dropdown).to have_text(private_membership_group.full_path)
+ expect(group_invite_dropdown).not_to have_text(parent_group.full_path, exact: true)
+ expect(group_invite_dropdown).not_to have_text(project_group.full_path, exact: true)
+ end
+ end
+
+ def expect_to_have_group(group)
+ expect(page).to have_selector("[entity-id='#{group.id}']")
+ end
+
+ def expect_not_to_have_group(group)
+ expect(page).not_to have_selector("[entity-id='#{group.id}']")
end
end
end
diff --git a/spec/features/projects/members/member_leaves_project_spec.rb b/spec/features/projects/members/member_leaves_project_spec.rb
index 4881a7bdf1a..c38292f81bf 100644
--- a/spec/features/projects/members/member_leaves_project_spec.rb
+++ b/spec/features/projects/members/member_leaves_project_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'Projects > Members > Member leaves project' do
+ include Spec::Support::Helpers::Features::MembersHelpers
+
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
@@ -21,13 +23,18 @@ RSpec.describe 'Projects > Members > Member leaves project' do
expect(project.users.exists?(user.id)).to be_falsey
end
- it 'user leaves project by url param', :js, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/35925' do
+ it 'user leaves project by url param', :js do
visit project_path(project, leave: 1)
page.accept_confirm
+ wait_for_all_requests
- expect(find('.flash-notice')).to have_content "You left the \"#{project.full_name}\" project"
expect(current_path).to eq(dashboard_projects_path)
- expect(project.users.exists?(user.id)).to be_falsey
+
+ sign_in(project.first_owner)
+
+ visit project_project_members_path(project)
+
+ expect(members_table).not_to have_content(user.name)
end
end
diff --git a/spec/features/projects/members/owner_cannot_leave_project_spec.rb b/spec/features/projects/members/owner_cannot_leave_project_spec.rb
index fbe8583b236..45a8f979b87 100644
--- a/spec/features/projects/members/owner_cannot_leave_project_spec.rb
+++ b/spec/features/projects/members/owner_cannot_leave_project_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'Projects > Members > Owner cannot leave project' do
let(:project) { create(:project) }
before do
- sign_in(project.owner)
+ sign_in(project.first_owner)
visit project_path(project)
end
diff --git a/spec/features/projects/members/owner_cannot_request_access_to_his_project_spec.rb b/spec/features/projects/members/owner_cannot_request_access_to_his_project_spec.rb
index 5e6e3d4d7f2..fad5d831c19 100644
--- a/spec/features/projects/members/owner_cannot_request_access_to_his_project_spec.rb
+++ b/spec/features/projects/members/owner_cannot_request_access_to_his_project_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'Projects > Members > Owner cannot request access to their own pr
let(:project) { create(:project) }
before do
- sign_in(project.owner)
+ sign_in(project.first_owner)
visit project_path(project)
end
diff --git a/spec/features/projects/members/user_requests_access_spec.rb b/spec/features/projects/members/user_requests_access_spec.rb
index dcaef5f4ef0..0b00656f87b 100644
--- a/spec/features/projects/members/user_requests_access_spec.rb
+++ b/spec/features/projects/members/user_requests_access_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'Projects > Members > User requests access', :js do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public, :repository) }
- let(:maintainer) { project.owner }
+ let(:maintainer) { project.first_owner }
before do
sign_in(user)
diff --git a/spec/features/projects/navbar_spec.rb b/spec/features/projects/navbar_spec.rb
index f61eaccf5b9..91e643ff258 100644
--- a/spec/features/projects/navbar_spec.rb
+++ b/spec/features/projects/navbar_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe 'Project navbar' do
let_it_be(:project) { create(:project, :repository) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
before do
sign_in(user)
diff --git a/spec/features/projects/network_graph_spec.rb b/spec/features/projects/network_graph_spec.rb
index 4ae809399b6..1ee0ea51e53 100644
--- a/spec/features/projects/network_graph_spec.rb
+++ b/spec/features/projects/network_graph_spec.rb
@@ -96,7 +96,7 @@ RSpec.describe 'Project Network Graph', :js do
find('button').click
end
- expect(page).to have_selector '.flash-alert', text: "Git revision ';' does not exist."
+ expect(page).to have_selector '[data-testid="alert-danger"]', text: "Git revision ';' does not exist."
end
end
diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb
index f1786c1be40..b3fbf5d356e 100644
--- a/spec/features/projects/new_project_spec.rb
+++ b/spec/features/projects/new_project_spec.rb
@@ -306,10 +306,24 @@ RSpec.describe 'New project', :js do
expect(page).to have_text('There is not a valid Git repository at this URL')
end
+ it 'reports error if repo URL is not a valid Git repository and submit button is clicked immediately' do
+ stub_request(:get, "http://foo/bar/info/refs?service=git-upload-pack").to_return(status: 200, body: "not-a-git-repo")
+
+ fill_in 'project_import_url', with: 'http://foo/bar'
+ click_on 'Create project'
+
+ wait_for_requests
+
+ expect(page).to have_text('There is not a valid Git repository at this URL')
+ end
+
it 'keeps "Import project" tab open after form validation error' do
collision_project = create(:project, name: 'test-name-collision', namespace: user.namespace)
+ stub_request(:get, "http://foo/bar/info/refs?service=git-upload-pack").to_return({ status: 200,
+ body: '001e# service=git-upload-pack',
+ headers: { 'Content-Type': 'application/x-git-upload-pack-advertisement' } })
- fill_in 'project_import_url', with: collision_project.http_url_to_repo
+ fill_in 'project_import_url', with: 'http://foo/bar'
fill_in 'project_name', with: collision_project.name
click_on 'Create project'
@@ -319,6 +333,38 @@ RSpec.describe 'New project', :js do
end
end
+ context 'when import is initiated from project page' do
+ before do
+ project_without_repo = create(:project, name: 'project-without-repo', namespace: user.namespace)
+ visit project_path(project_without_repo)
+ click_on 'Import repository'
+ end
+
+ it 'reports error when invalid url is provided' do
+ stub_request(:get, "http://foo/bar/info/refs?service=git-upload-pack").to_return(status: 200, body: "not-a-git-repo")
+
+ fill_in 'project_import_url', with: 'http://foo/bar'
+
+ click_on 'Start import'
+ wait_for_requests
+
+ expect(page).to have_text('There is not a valid Git repository at this URL')
+ end
+
+ it 'initiates import when valid repo url is provided' do
+ stub_request(:get, "http://foo/bar/info/refs?service=git-upload-pack").to_return({ status: 200,
+ body: '001e# service=git-upload-pack',
+ headers: { 'Content-Type': 'application/x-git-upload-pack-advertisement' } })
+
+ fill_in 'project_import_url', with: 'http://foo/bar'
+
+ click_on 'Start import'
+ wait_for_requests
+
+ expect(page).to have_text('Import in progress')
+ end
+ end
+
context 'from GitHub' do
before do
first('.js-import-github').click
@@ -358,4 +404,47 @@ RSpec.describe 'New project', :js do
end
end
end
+
+ context 'from Bitbucket', :js do
+ shared_examples 'has a link to bitbucket cloud' do
+ context 'when bitbucket is not configured' do
+ before do
+ allow(Gitlab::Auth::OAuth::Provider).to receive(:enabled?).and_call_original
+ allow(Gitlab::Auth::OAuth::Provider)
+ .to receive(:enabled?).with(:bitbucket)
+ .and_return(false)
+
+ visit new_project_path
+ click_link 'Import project'
+ click_link 'Bitbucket Cloud'
+ end
+
+ it 'shows import instructions' do
+ expect(find('.modal-body')).to have_content(bitbucket_link_content)
+ end
+ end
+ end
+
+ context 'as a user' do
+ let(:user) { create(:user) }
+ let(:bitbucket_link_content) { 'To enable importing projects from Bitbucket, ask your GitLab administrator to configure OAuth integration' }
+
+ before do
+ sign_in(user)
+ end
+
+ it_behaves_like 'has a link to bitbucket cloud'
+ end
+
+ context 'as an admin' do
+ let(:user) { create(:admin) }
+ let(:bitbucket_link_content) { 'To enable importing projects from Bitbucket, as administrator you need to configure OAuth integration' }
+
+ before do
+ sign_in(user)
+ end
+
+ it_behaves_like 'has a link to bitbucket cloud'
+ end
+ end
end
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index 5176a7ec5a1..01c942aec4c 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -715,7 +715,7 @@ RSpec.describe 'Pipeline', :js do
let(:schedule) do
create(:ci_pipeline_schedule,
project: project,
- owner: project.owner,
+ owner: project.first_owner,
description: 'blocked user schedule'
).tap do |schedule|
schedule.update_column(:next_run_at, 1.minute.ago)
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index fb45db213d0..d5b470194a2 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe 'Pipelines', :js do
include ProjectForksHelper
+ include Spec::Support::Helpers::ModalHelpers
let(:project) { create(:project) }
@@ -159,7 +160,7 @@ RSpec.describe 'Pipelines', :js do
end
end
- context 'when pipeline is detached merge request pipeline' do
+ context 'when pipeline is detached merge request pipeline, with rearrange_pipelines_table feature flag turned off' do
let(:merge_request) do
create(:merge_request,
:with_detached_merge_request_pipeline,
@@ -172,6 +173,8 @@ RSpec.describe 'Pipelines', :js do
let(:target_project) { project }
before do
+ stub_feature_flags(rearrange_pipelines_table: false)
+
visit project_pipelines_path(source_project)
end
@@ -201,7 +204,47 @@ RSpec.describe 'Pipelines', :js do
end
end
- context 'when pipeline is merge request pipeline' do
+ context 'when pipeline is detached merge request pipeline, with rearrange_pipelines_table feature flag turned on' do
+ let(:merge_request) do
+ create(:merge_request,
+ :with_detached_merge_request_pipeline,
+ source_project: source_project,
+ target_project: target_project)
+ end
+
+ let!(:pipeline) { merge_request.all_pipelines.first }
+ let(:source_project) { project }
+ let(:target_project) { project }
+
+ before do
+ stub_feature_flags(rearrange_pipelines_table: true)
+
+ visit project_pipelines_path(source_project)
+ end
+
+ shared_examples_for 'detached merge request pipeline' do
+ it 'shows pipeline information without pipeline ref', :sidekiq_might_not_need_inline do
+ within '.pipeline-tags' do
+ expect(page).to have_content('detached')
+
+ expect(page).to have_link(merge_request.iid,
+ href: project_merge_request_path(project, merge_request))
+
+ expect(page).not_to have_link(pipeline.ref)
+ end
+ end
+ end
+
+ it_behaves_like 'detached merge request pipeline'
+
+ context 'when source project is a forked project' do
+ let(:source_project) { fork_project(project, user, repository: true) }
+
+ it_behaves_like 'detached merge request pipeline'
+ end
+ end
+
+ context 'when pipeline is merge request pipeline, with rearrange_pipelines_table feature flag turned off' do
let(:merge_request) do
create(:merge_request,
:with_merge_request_pipeline,
@@ -215,6 +258,8 @@ RSpec.describe 'Pipelines', :js do
let(:target_project) { project }
before do
+ stub_feature_flags(rearrange_pipelines_table: false)
+
visit project_pipelines_path(source_project)
end
@@ -244,6 +289,47 @@ RSpec.describe 'Pipelines', :js do
end
end
+ context 'when pipeline is merge request pipeline, with rearrange_pipelines_table feature flag turned on' do
+ let(:merge_request) do
+ create(:merge_request,
+ :with_merge_request_pipeline,
+ source_project: source_project,
+ target_project: target_project,
+ merge_sha: target_project.commit.sha)
+ end
+
+ let!(:pipeline) { merge_request.all_pipelines.first }
+ let(:source_project) { project }
+ let(:target_project) { project }
+
+ before do
+ stub_feature_flags(rearrange_pipelines_table: true)
+
+ visit project_pipelines_path(source_project)
+ end
+
+ shared_examples_for 'Correct merge request pipeline information' do
+ it 'does not show detached tag for the pipeline, and shows the link of the merge request, and does not show the ref of the pipeline', :sidekiq_might_not_need_inline do
+ within '.pipeline-tags' do
+ expect(page).not_to have_content('detached')
+
+ expect(page).to have_link(merge_request.iid,
+ href: project_merge_request_path(project, merge_request))
+
+ expect(page).not_to have_link(pipeline.ref)
+ end
+ end
+ end
+
+ it_behaves_like 'Correct merge request pipeline information'
+
+ context 'when source project is a forked project' do
+ let(:source_project) { fork_project(project, user, repository: true) }
+
+ it_behaves_like 'Correct merge request pipeline information'
+ end
+ end
+
context 'when pipeline has configuration errors' do
let(:pipeline) do
create(:ci_pipeline, :invalid, project: project)
@@ -351,7 +437,9 @@ RSpec.describe 'Pipelines', :js do
context 'when user played a delayed job immediately' do
before do
find('[data-testid="pipelines-manual-actions-dropdown"]').click
- page.accept_confirm { click_button('delayed job 1') }
+ accept_gl_confirm do
+ click_button 'delayed job 1'
+ end
wait_for_requests
end
@@ -587,6 +675,7 @@ RSpec.describe 'Pipelines', :js do
context 'with pipeline key selection' do
before do
+ stub_feature_flags(rearrange_pipelines_table: false)
visit project_pipelines_path(project)
wait_for_requests
end
@@ -604,6 +693,27 @@ RSpec.describe 'Pipelines', :js do
expect(page.find('[data-testid="pipeline-url-link"]')).to have_content "##{pipeline.iid}"
end
end
+
+ context 'with pipeline key selection and rearrange_pipelines_table ff on' do
+ before do
+ stub_feature_flags(rearrange_pipelines_table: true)
+ visit project_pipelines_path(project)
+ wait_for_requests
+ end
+
+ it 'changes the Pipeline ID column for Pipeline IID' do
+ page.find('[data-testid="pipeline-key-dropdown"]').click
+
+ within '.gl-new-dropdown-contents' do
+ dropdown_options = page.find_all '.gl-new-dropdown-item'
+
+ dropdown_options[1].click
+ end
+
+ expect(page.find('[data-testid="pipeline-th"]')).to have_content 'Pipeline'
+ expect(page.find('[data-testid="pipeline-identifier"]')).to have_content "##{pipeline.iid}"
+ end
+ end
end
describe 'GET /:project/-/pipelines/show' do
@@ -750,7 +860,7 @@ RSpec.describe 'Pipelines', :js do
it 'increments jobs_cache_index' do
click_button 'Clear runner caches'
wait_for_requests
- expect(page.find('.flash-notice')).to have_content 'Project cache successfully reset.'
+ expect(page.find('[data-testid="alert-info"]')).to have_content 'Project cache successfully reset.'
end
end
@@ -758,7 +868,7 @@ RSpec.describe 'Pipelines', :js do
it 'sets jobs_cache_index to 1' do
click_button 'Clear runner caches'
wait_for_requests
- expect(page.find('.flash-notice')).to have_content 'Project cache successfully reset.'
+ expect(page.find('[data-testid="alert-info"]')).to have_content 'Project cache successfully reset.'
end
end
end
diff --git a/spec/features/projects/settings/monitor_settings_spec.rb b/spec/features/projects/settings/monitor_settings_spec.rb
index 3f6c4646f00..871391fbe9c 100644
--- a/spec/features/projects/settings/monitor_settings_spec.rb
+++ b/spec/features/projects/settings/monitor_settings_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'Projects > Settings > For a forked project', :js do
let_it_be(:project) { create(:project, :repository, create_templates: :issue) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
before do
sign_in(user)
diff --git a/spec/features/projects/settings/packages_settings_spec.rb b/spec/features/projects/settings/packages_settings_spec.rb
index e70839e9720..057e6b635fe 100644
--- a/spec/features/projects/settings/packages_settings_spec.rb
+++ b/spec/features/projects/settings/packages_settings_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'Projects > Settings > Packages', :js do
let_it_be(:project) { create(:project) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
before do
sign_in(user)
diff --git a/spec/features/projects/settings/project_settings_spec.rb b/spec/features/projects/settings/project_settings_spec.rb
index b67caa5a5f9..a0d44b579a8 100644
--- a/spec/features/projects/settings/project_settings_spec.rb
+++ b/spec/features/projects/settings/project_settings_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'Projects settings' do
let_it_be(:project) { create(:project) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:panel) { find('.general-settings', match: :first) }
let(:button) { panel.find('.btn.gl-button.js-settings-toggle') }
let(:title) { panel.find('.settings-title') }
diff --git a/spec/features/projects/settings/user_interacts_with_deploy_keys_spec.rb b/spec/features/projects/settings/user_interacts_with_deploy_keys_spec.rb
index 91a7753fe6d..d16295aedbe 100644
--- a/spec/features/projects/settings/user_interacts_with_deploy_keys_spec.rb
+++ b/spec/features/projects/settings/user_interacts_with_deploy_keys_spec.rb
@@ -4,7 +4,7 @@ require "spec_helper"
RSpec.describe "User interacts with deploy keys", :js do
let(:project) { create(:project, :repository) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
before do
sign_in(user)
diff --git a/spec/features/projects/show/redirects_spec.rb b/spec/features/projects/show/redirects_spec.rb
index 659edda5672..3ac82244ded 100644
--- a/spec/features/projects/show/redirects_spec.rb
+++ b/spec/features/projects/show/redirects_spec.rb
@@ -47,7 +47,7 @@ RSpec.describe 'Projects > Show > Redirects' do
it 'redirects to private project page after sign in' do
visit project_path(private_project)
- owner = private_project.owner
+ owner = private_project.first_owner
fill_in 'user_login', with: owner.email
fill_in 'user_password', with: owner.password
click_button 'Sign in'
diff --git a/spec/features/projects/show/user_manages_notifications_spec.rb b/spec/features/projects/show/user_manages_notifications_spec.rb
index 80dae4ec386..1df37eef6a9 100644
--- a/spec/features/projects/show/user_manages_notifications_spec.rb
+++ b/spec/features/projects/show/user_manages_notifications_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'Projects > Show > User manages notifications', :js do
let(:project) { create(:project, :public, :repository) }
before do
- sign_in(project.owner)
+ sign_in(project.first_owner)
end
def click_notifications_button
diff --git a/spec/features/projects/show/user_sees_deletion_failure_message_spec.rb b/spec/features/projects/show/user_sees_deletion_failure_message_spec.rb
index b7af0c29b33..47e010dcf89 100644
--- a/spec/features/projects/show/user_sees_deletion_failure_message_spec.rb
+++ b/spec/features/projects/show/user_sees_deletion_failure_message_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'Projects > Show > User sees a deletion failure message' do
let(:project) { create(:project, :empty_repo, pending_delete: true) }
before do
- sign_in(project.owner)
+ sign_in(project.first_owner)
end
it 'shows error message if deletion for project fails' do
diff --git a/spec/features/projects/show/user_sees_git_instructions_spec.rb b/spec/features/projects/show/user_sees_git_instructions_spec.rb
index e6157887c12..5270939f681 100644
--- a/spec/features/projects/show/user_sees_git_instructions_spec.rb
+++ b/spec/features/projects/show/user_sees_git_instructions_spec.rb
@@ -61,7 +61,7 @@ RSpec.describe 'Projects > Show > User sees Git instructions' do
let_it_be(:project) { create(:project, :public) }
before do
- sign_in(project.owner)
+ sign_in(project.first_owner)
visit project_path(project)
end
@@ -77,7 +77,7 @@ RSpec.describe 'Projects > Show > User sees Git instructions' do
.at_least(:once)
.and_return('example_branch')
- sign_in(project.owner)
+ sign_in(project.first_owner)
visit project_path(project)
end
diff --git a/spec/features/projects/tree/tree_show_spec.rb b/spec/features/projects/tree/tree_show_spec.rb
index f8bbaa9535b..cd94e6da018 100644
--- a/spec/features/projects/tree/tree_show_spec.rb
+++ b/spec/features/projects/tree/tree_show_spec.rb
@@ -25,7 +25,7 @@ RSpec.describe 'Projects tree', :js do
expect(page).to have_selector('.tree-item')
expect(page).to have_content('add tests for .gitattributes custom highlighting')
- expect(page).not_to have_selector('.flash-alert')
+ expect(page).not_to have_selector('[data-testid="alert-danger"]')
expect(page).not_to have_selector('[data-qa-selector="label-lfs"]', text: 'LFS') # rubocop:disable QA/SelectorUsage
end
@@ -36,7 +36,7 @@ RSpec.describe 'Projects tree', :js do
expect(page).to have_selector('.tree-item')
expect(page).to have_content('add spaces in whitespace file')
expect(page).not_to have_selector('[data-qa-selector="label-lfs"]', text: 'LFS') # rubocop:disable QA/SelectorUsage
- expect(page).not_to have_selector('.flash-alert')
+ expect(page).not_to have_selector('[data-testid="alert-danger"]')
end
it 'renders tree table with non-ASCII filenames without errors' do
@@ -46,7 +46,7 @@ RSpec.describe 'Projects tree', :js do
expect(page).to have_selector('.tree-item')
expect(page).to have_content('Files, encoding and much more')
expect(page).to have_content('テスト.txt')
- expect(page).not_to have_selector('.flash-alert')
+ expect(page).not_to have_selector('[data-testid="alert-danger"]')
end
context "with a tree that contains pathspec characters" do
@@ -139,7 +139,7 @@ RSpec.describe 'Projects tree', :js do
wait_for_requests
expect(page).to have_selector('.tree-item')
- expect(page).not_to have_selector('.flash-alert')
+ expect(page).not_to have_selector('[data-testid="alert-danger"]')
end
context 'for signed commit' do
diff --git a/spec/features/projects/user_changes_project_visibility_spec.rb b/spec/features/projects/user_changes_project_visibility_spec.rb
index 68fed9b8a74..d2a7596aec0 100644
--- a/spec/features/projects/user_changes_project_visibility_spec.rb
+++ b/spec/features/projects/user_changes_project_visibility_spec.rb
@@ -43,9 +43,9 @@ RSpec.describe 'User changes public project visibility', :js do
context 'when the project has forks' do
before do
- fork_project(project, project.owner)
+ fork_project(project, project.first_owner)
- sign_in(project.owner)
+ sign_in(project.first_owner)
visit edit_project_path(project)
end
@@ -84,7 +84,7 @@ RSpec.describe 'User changes public project visibility', :js do
let(:project) { create(:project, :empty_repo, :public) }
before do
- sign_in(project.owner)
+ sign_in(project.first_owner)
visit edit_project_path(project)
end
@@ -98,9 +98,9 @@ RSpec.describe 'User changes public project visibility', :js do
before do
stub_feature_flags(unlink_fork_network_upon_visibility_decrease: false)
- fork_project(project, project.owner)
+ fork_project(project, project.first_owner)
- sign_in(project.owner)
+ sign_in(project.first_owner)
visit edit_project_path(project)
end
diff --git a/spec/features/projects/user_uses_shortcuts_spec.rb b/spec/features/projects/user_uses_shortcuts_spec.rb
index 7bb15451538..cd6f09ce275 100644
--- a/spec/features/projects/user_uses_shortcuts_spec.rb
+++ b/spec/features/projects/user_uses_shortcuts_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'User uses shortcuts', :js do
let_it_be(:project) { create(:project, :repository) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
before do
sign_in(user)
diff --git a/spec/features/projects/view_on_env_spec.rb b/spec/features/projects/view_on_env_spec.rb
index 94085b075aa..5dd30f59e3d 100644
--- a/spec/features/projects/view_on_env_spec.rb
+++ b/spec/features/projects/view_on_env_spec.rb
@@ -9,7 +9,6 @@ RSpec.describe 'View on environment', :js do
let(:user) { project.creator }
before do
- stub_feature_flags(refactor_blob_viewer: false) # This stub will be removed in https://gitlab.com/gitlab-org/gitlab/-/issues/350457
project.add_maintainer(user)
end
diff --git a/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb b/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb
index 63ee89bd11f..fbb5c24f6e1 100644
--- a/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb
+++ b/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe 'Projects > Wiki > User views wiki in project page' do
before do
- sign_in(project.owner)
+ sign_in(project.first_owner)
end
context 'when repository is disabled for project' do
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index 26deca9c8f1..1049f8bc18f 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -59,7 +59,7 @@ RSpec.describe 'Project' do
let(:path) { project_path(project) }
before do
- sign_in(project.owner)
+ sign_in(project.first_owner)
end
it 'parses Markdown' do
@@ -123,7 +123,7 @@ RSpec.describe 'Project' do
let(:path) { project_path(project) }
before do
- sign_in(project.owner)
+ sign_in(project.first_owner)
visit path
end
@@ -154,7 +154,7 @@ RSpec.describe 'Project' do
let(:path) { project_path(project) }
before do
- sign_in(project.owner)
+ sign_in(project.first_owner)
visit path
end
@@ -201,7 +201,7 @@ RSpec.describe 'Project' do
it 'does not show the name of the deleted project when the source was deleted', :sidekiq_might_not_need_inline do
forked_project
- Projects::DestroyService.new(base_project, base_project.owner).execute
+ Projects::DestroyService.new(base_project, base_project.first_owner).execute
visit project_path(forked_project)
@@ -236,7 +236,7 @@ RSpec.describe 'Project' do
it 'does not show an error' do
wait_for_requests
- expect(page).not_to have_selector('.flash-alert')
+ expect(page).not_to have_selector('[data-testid="alert-danger"]')
end
end
@@ -316,7 +316,7 @@ RSpec.describe 'Project' do
wait_for_requests
expect(page).to have_selector('.tree-item')
- expect(page).not_to have_selector('.flash-alert')
+ expect(page).not_to have_selector('[data-testid="alert-danger"]')
end
context 'for signed commit' do
diff --git a/spec/features/protected_tags_spec.rb b/spec/features/protected_tags_spec.rb
index 376f990f054..c89dd4d1f90 100644
--- a/spec/features/protected_tags_spec.rb
+++ b/spec/features/protected_tags_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'Protected Tags', :js do
include ProtectedTagHelpers
let(:project) { create(:project, :repository) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
before do
sign_in(user)
diff --git a/spec/features/security/project/snippet/internal_access_spec.rb b/spec/features/security/project/snippet/internal_access_spec.rb
index 12237863188..ab080f0a460 100644
--- a/spec/features/security/project/snippet/internal_access_spec.rb
+++ b/spec/features/security/project/snippet/internal_access_spec.rb
@@ -6,8 +6,8 @@ RSpec.describe "Internal Project Snippets Access" do
include AccessMatchers
let_it_be(:project) { create(:project, :internal) }
- let_it_be(:internal_snippet) { create(:project_snippet, :internal, project: project, author: project.owner) }
- let_it_be(:private_snippet) { create(:project_snippet, :private, project: project, author: project.owner) }
+ let_it_be(:internal_snippet) { create(:project_snippet, :internal, project: project, author: project.first_owner) }
+ let_it_be(:private_snippet) { create(:project_snippet, :private, project: project, author: project.first_owner) }
describe "GET /:project_path/snippets" do
subject { project_snippets_path(project) }
diff --git a/spec/features/security/project/snippet/private_access_spec.rb b/spec/features/security/project/snippet/private_access_spec.rb
index 0c06841399d..1e0afc09b74 100644
--- a/spec/features/security/project/snippet/private_access_spec.rb
+++ b/spec/features/security/project/snippet/private_access_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe "Private Project Snippets Access" do
include AccessMatchers
let_it_be(:project) { create(:project, :private) }
- let_it_be(:private_snippet) { create(:project_snippet, :private, project: project, author: project.owner) }
+ let_it_be(:private_snippet) { create(:project_snippet, :private, project: project, author: project.first_owner) }
describe "GET /:project_path/snippets" do
subject { project_snippets_path(project) }
diff --git a/spec/features/security/project/snippet/public_access_spec.rb b/spec/features/security/project/snippet/public_access_spec.rb
index 2ae08205602..f734f7ba9e2 100644
--- a/spec/features/security/project/snippet/public_access_spec.rb
+++ b/spec/features/security/project/snippet/public_access_spec.rb
@@ -6,9 +6,9 @@ RSpec.describe "Public Project Snippets Access" do
include AccessMatchers
let_it_be(:project) { create(:project, :public) }
- let_it_be(:public_snippet) { create(:project_snippet, :public, project: project, author: project.owner) }
- let_it_be(:internal_snippet) { create(:project_snippet, :internal, project: project, author: project.owner) }
- let_it_be(:private_snippet) { create(:project_snippet, :private, project: project, author: project.owner) }
+ let_it_be(:public_snippet) { create(:project_snippet, :public, project: project, author: project.first_owner) }
+ let_it_be(:internal_snippet) { create(:project_snippet, :internal, project: project, author: project.first_owner) }
+ let_it_be(:private_snippet) { create(:project_snippet, :private, project: project, author: project.first_owner) }
describe "GET /:project_path/snippets" do
subject { project_snippets_path(project) }
diff --git a/spec/features/snippets_spec.rb b/spec/features/snippets_spec.rb
index 8cdb4bc3344..35eb5c2e193 100644
--- a/spec/features/snippets_spec.rb
+++ b/spec/features/snippets_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'Snippets' do
context 'when the project has snippets' do
let(:project) { create(:project, :public) }
- let!(:snippets) { create_list(:project_snippet, 2, :public, author: project.owner, project: project) }
+ let!(:snippets) { create_list(:project_snippet, 2, :public, author: project.first_owner, project: project) }
before do
allow(Snippet).to receive(:default_per_page).and_return(1)
diff --git a/spec/features/triggers_spec.rb b/spec/features/triggers_spec.rb
index 2ddd86dd807..1f1824c897e 100644
--- a/spec/features/triggers_spec.rb
+++ b/spec/features/triggers_spec.rb
@@ -34,7 +34,7 @@ RSpec.describe 'Triggers', :js do
click_button 'Add trigger'
aggregate_failures 'display creation notice and trigger is created' do
- expect(page.find('.flash-notice')).to have_content 'Trigger was created successfully.'
+ expect(page.find('[data-testid="alert-info"]')).to have_content 'Trigger was created successfully.'
expect(page.find('.triggers-list')).to have_content 'trigger desc'
expect(page.find('.triggers-list .trigger-owner')).to have_content user.name
end
@@ -63,7 +63,7 @@ RSpec.describe 'Triggers', :js do
click_button 'Save trigger'
aggregate_failures 'display update notice and trigger is updated' do
- expect(page.find('.flash-notice')).to have_content 'Trigger was successfully updated.'
+ expect(page.find('[data-testid="alert-info"]')).to have_content 'Trigger was successfully updated.'
expect(page.find('.triggers-list')).to have_content new_trigger_title
expect(page.find('.triggers-list .trigger-owner')).to have_content user.name
end
@@ -89,7 +89,7 @@ RSpec.describe 'Triggers', :js do
end
aggregate_failures 'trigger is removed' do
- expect(page.find('.flash-notice')).to have_content 'Trigger removed'
+ expect(page.find('[data-testid="alert-info"]')).to have_content 'Trigger removed'
expect(page).to have_css('[data-testid="no_triggers_content"]')
end
end
diff --git a/spec/features/user_sorts_things_spec.rb b/spec/features/user_sorts_things_spec.rb
index 8e6f6a96bd2..fa37d692225 100644
--- a/spec/features/user_sorts_things_spec.rb
+++ b/spec/features/user_sorts_things_spec.rb
@@ -33,18 +33,6 @@ RSpec.describe "User sorts things" do
expect(find(".issues-filters")).to have_content(sort_option)
end
- it "issues -> merge requests" do
- sort_option = 'Updated date'
-
- visit(project_issues_path(project))
-
- sort_by(sort_option)
-
- visit(project_merge_requests_path(project))
-
- expect(find(".issues-filters")).to have_content(sort_option)
- end
-
it "merge requests -> dashboard merge requests" do
sort_option = 'Updated date'
diff --git a/spec/features/users/bizible_csp_spec.rb b/spec/features/users/bizible_csp_spec.rb
new file mode 100644
index 00000000000..af0b42050b3
--- /dev/null
+++ b/spec/features/users/bizible_csp_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Bizible content security policy' do
+ before do
+ stub_config(extra: { one_trust_id: SecureRandom.uuid })
+ end
+
+ it 'has proper Content Security Policy headers' do
+ visit root_path
+
+ expect(response_headers['Content-Security-Policy']).to include('https://cdn.bizible.com/scripts/bizible.js')
+ end
+end
diff --git a/spec/features/users/logout_spec.rb b/spec/features/users/logout_spec.rb
index ffb8785b277..3129eb5e6f3 100644
--- a/spec/features/users/logout_spec.rb
+++ b/spec/features/users/logout_spec.rb
@@ -19,7 +19,7 @@ RSpec.describe 'Logout/Sign out', :js do
it 'sign out does not show signed out flash notice' do
gitlab_sign_out
- expect(page).not_to have_selector('.flash-notice')
+ expect(page).not_to have_selector('[data-testid="alert-info"]')
end
context 'on a read-only instance' do
diff --git a/spec/finders/autocomplete/users_finder_spec.rb b/spec/finders/autocomplete/users_finder_spec.rb
index 28bd7e12916..9b3421d1b4f 100644
--- a/spec/finders/autocomplete/users_finder_spec.rb
+++ b/spec/finders/autocomplete/users_finder_spec.rb
@@ -3,16 +3,20 @@
require 'spec_helper'
RSpec.describe Autocomplete::UsersFinder do
+ # TODO update when multiple owners are possible in projects
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/21432
+
describe '#execute' do
- let!(:user1) { create(:user, username: 'johndoe') }
- let!(:user2) { create(:user, :blocked, username: 'notsorandom') }
- let!(:external_user) { create(:user, :external) }
- let!(:omniauth_user) { create(:omniauth_user, provider: 'twitter', extern_uid: '123456') }
+ let_it_be(:user1) { create(:user, name: 'zzzzzname', username: 'johndoe') }
+ let_it_be(:user2) { create(:user, :blocked, username: 'notsorandom') }
+ let_it_be(:external_user) { create(:user, :external) }
+ let_it_be(:omniauth_user) { create(:omniauth_user, provider: 'twitter', extern_uid: '123456') }
+
let(:current_user) { create(:user) }
let(:params) { {} }
- let(:project) { nil }
- let(:group) { nil }
+ let_it_be(:project) { nil }
+ let_it_be(:group) { nil }
subject { described_class.new(params: params, current_user: current_user, project: project, group: group).execute.to_a }
@@ -23,33 +27,53 @@ RSpec.describe Autocomplete::UsersFinder do
end
context 'when project passed' do
- let(:project) { create(:project) }
+ let_it_be(:project) { create(:project) }
- it { is_expected.to match_array([project.owner]) }
+ it { is_expected.to match_array([project.first_owner]) }
context 'when author_id passed' do
context 'and author is active' do
let(:params) { { author_id: user1.id } }
- it { is_expected.to match_array([project.owner, user1]) }
+ it { is_expected.to match_array([project.first_owner, user1]) }
end
context 'and author is blocked' do
let(:params) { { author_id: user2.id } }
- it { is_expected.to match_array([project.owner]) }
+ it { is_expected.to match_array([project.first_owner]) }
+ end
+ end
+
+ context 'searching with less than 3 characters' do
+ let(:params) { { search: 'zz' } }
+
+ before do
+ project.add_guest(user1)
+ end
+
+ it 'allows partial matches' do
+ expect(subject).to contain_exactly(user1)
end
end
end
context 'when group passed and project not passed' do
- let(:group) { create(:group, :public) }
+ let_it_be(:group) { create(:group, :public) }
- before do
+ before_all do
group.add_users([user1], GroupMember::DEVELOPER)
end
it { is_expected.to match_array([user1]) }
+
+ context 'searching with less than 3 characters' do
+ let(:params) { { search: 'zz' } }
+
+ it 'allows partial matches' do
+ expect(subject).to contain_exactly(user1)
+ end
+ end
end
context 'when passed a subgroup' do
@@ -73,6 +97,14 @@ RSpec.describe Autocomplete::UsersFinder do
let(:params) { { search: 'johndoe' } }
it { is_expected.to match_array([user1]) }
+
+ context 'searching with less than 3 characters' do
+ let(:params) { { search: 'zz' } }
+
+ it 'does not allow partial matches' do
+ expect(subject).to be_empty
+ end
+ end
end
context 'when filtered by skip_users' do
diff --git a/spec/finders/ci/daily_build_group_report_results_finder_spec.rb b/spec/finders/ci/daily_build_group_report_results_finder_spec.rb
index cf15a00323b..5352cfe5238 100644
--- a/spec/finders/ci/daily_build_group_report_results_finder_spec.rb
+++ b/spec/finders/ci/daily_build_group_report_results_finder_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe Ci::DailyBuildGroupReportResultsFinder do
describe '#execute' do
let_it_be(:project) { create(:project, :private) }
let(:user_without_permission) { create(:user) }
- let_it_be(:user_with_permission) { project.owner }
+ let_it_be(:user_with_permission) { project.first_owner }
let_it_be(:ref_path) { 'refs/heads/master' }
let(:limit) { nil }
let_it_be(:default_branch) { false }
diff --git a/spec/finders/ci/jobs_finder_spec.rb b/spec/finders/ci/jobs_finder_spec.rb
index ab056dd26e8..959716b1fd3 100644
--- a/spec/finders/ci/jobs_finder_spec.rb
+++ b/spec/finders/ci/jobs_finder_spec.rb
@@ -126,4 +126,41 @@ RSpec.describe Ci::JobsFinder, '#execute' do
end
end
end
+
+ context 'a runner is present' do
+ let_it_be(:runner) { create(:ci_runner, :project, projects: [project]) }
+ let_it_be(:job_4) { create(:ci_build, :success, runner: runner) }
+
+ subject { described_class.new(current_user: user, runner: runner, params: params).execute }
+
+ context 'user has access to the runner', :enable_admin_mode do
+ let(:user) { admin }
+
+ it 'returns jobs for the specified project' do
+ expect(subject).to match_array([job_4])
+ end
+ end
+
+ context 'user has no access to project builds' do
+ let_it_be(:guest) { create(:user) }
+
+ let(:user) { guest }
+
+ before do
+ project.add_guest(guest)
+ end
+
+ it 'returns no jobs' do
+ expect(subject).to be_empty
+ end
+ end
+
+ context 'without user' do
+ let(:user) { nil }
+
+ it 'returns no jobs' do
+ expect(subject).to be_empty
+ end
+ end
+ end
end
diff --git a/spec/finders/ci/runners_finder_spec.rb b/spec/finders/ci/runners_finder_spec.rb
index dac244e4300..e7ec4f01995 100644
--- a/spec/finders/ci/runners_finder_spec.rb
+++ b/spec/finders/ci/runners_finder_spec.rb
@@ -91,8 +91,8 @@ RSpec.describe Ci::RunnersFinder do
end
context 'sorting' do
- let_it_be(:runner1) { create :ci_runner, created_at: '2018-07-12 07:00', contacted_at: 1.minute.ago }
- let_it_be(:runner2) { create :ci_runner, created_at: '2018-07-12 08:00', contacted_at: 3.minutes.ago }
+ let_it_be(:runner1) { create :ci_runner, created_at: '2018-07-12 07:00', contacted_at: 1.minute.ago, token_expires_at: '2022-02-15 07:00' }
+ let_it_be(:runner2) { create :ci_runner, created_at: '2018-07-12 08:00', contacted_at: 3.minutes.ago, token_expires_at: '2022-02-15 06:00' }
let_it_be(:runner3) { create :ci_runner, created_at: '2018-07-12 09:00', contacted_at: 2.minutes.ago }
subject do
@@ -142,6 +142,22 @@ RSpec.describe Ci::RunnersFinder do
is_expected.to eq [runner1, runner3, runner2]
end
end
+
+ context 'with sort param equal to token_expires_at_asc' do
+ let(:params) { { sort: 'token_expires_at_asc' } }
+
+ it 'sorts by contacted_at ascending' do
+ is_expected.to eq [runner2, runner1, runner3]
+ end
+ end
+
+ context 'with sort param equal to token_expires_at_desc' do
+ let(:params) { { sort: 'token_expires_at_desc' } }
+
+ it 'sorts by contacted_at descending' do
+ is_expected.to eq [runner3, runner1, runner2]
+ end
+ end
end
context 'by non admin user' do
@@ -206,7 +222,7 @@ RSpec.describe Ci::RunnersFinder do
sub_group_4.runners << runner_sub_group_4
end
- shared_examples '#execute' do
+ describe '#execute' do
subject { described_class.new(current_user: user, params: params).execute }
shared_examples 'membership equal to :descendants' do
@@ -349,16 +365,6 @@ RSpec.describe Ci::RunnersFinder do
end
end
- it_behaves_like '#execute'
-
- context 'when the FF ci_find_runners_by_ci_mirrors is disabled' do
- before do
- stub_feature_flags(ci_find_runners_by_ci_mirrors: false)
- end
-
- it_behaves_like '#execute'
- end
-
describe '#sort_key' do
subject { described_class.new(current_user: user, params: params.merge(group: group)).sort_key }
diff --git a/spec/finders/clusters/agents_finder_spec.rb b/spec/finders/clusters/agents_finder_spec.rb
index 0996d76b723..4ec798daa99 100644
--- a/spec/finders/clusters/agents_finder_spec.rb
+++ b/spec/finders/clusters/agents_finder_spec.rb
@@ -15,7 +15,11 @@ RSpec.describe Clusters::AgentsFinder do
it { is_expected.to contain_exactly(matching_agent) }
context 'user does not have permission' do
- let(:user) { create(:user, developer_projects: [project]) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_reporter(user)
+ end
it { is_expected.to be_empty }
end
diff --git a/spec/finders/crm/contacts_finder_spec.rb b/spec/finders/crm/contacts_finder_spec.rb
new file mode 100644
index 00000000000..151af1ad825
--- /dev/null
+++ b/spec/finders/crm/contacts_finder_spec.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Crm::ContactsFinder do
+ let_it_be(:user) { create(:user) }
+
+ describe '#execute' do
+ subject { described_class.new(user, group: group).execute }
+
+ context 'when customer relations feature is enabled for the group' do
+ let_it_be(:root_group) { create(:group, :crm_enabled) }
+ let_it_be(:group) { create(:group, parent: root_group) }
+
+ let_it_be(:contact_1) { create(:contact, group: root_group) }
+ let_it_be(:contact_2) { create(:contact, group: root_group) }
+
+ context 'when user does not have permissions to see contacts in the group' do
+ it 'returns an empty array' do
+ expect(subject).to be_empty
+ end
+ end
+
+ context 'when user is member of the root group' do
+ before do
+ root_group.add_developer(user)
+ end
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(customer_relations: false)
+ end
+
+ it 'returns an empty array' do
+ expect(subject).to be_empty
+ end
+ end
+
+ context 'when feature flag is enabled' do
+ it 'returns all group contacts' do
+ expect(subject).to match_array([contact_1, contact_2])
+ end
+ end
+ end
+
+ context 'when user is member of the sub group' do
+ before do
+ group.add_developer(user)
+ end
+
+ it 'returns an empty array' do
+ expect(subject).to be_empty
+ end
+ end
+ end
+
+ context 'when customer relations feature is disabled for the group' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:contact) { create(:contact, group: group) }
+
+ before do
+ group.add_developer(user)
+ end
+
+ it 'returns an empty array' do
+ expect(subject).to be_empty
+ end
+ end
+ end
+end
diff --git a/spec/finders/deployments_finder_spec.rb b/spec/finders/deployments_finder_spec.rb
index 6d9d0c33de3..51c293bcfd1 100644
--- a/spec/finders/deployments_finder_spec.rb
+++ b/spec/finders/deployments_finder_spec.rb
@@ -222,11 +222,7 @@ RSpec.describe DeploymentsFinder do
end
describe 'enforce sorting to `updated_at` sorting' do
- let(:params) { { **base_params, updated_before: 1.day.ago, order_by: 'id', sort: 'asc' } }
-
- before do
- allow(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception)
- end
+ let(:params) { { **base_params, updated_before: 1.day.ago, order_by: 'id', sort: 'asc', raise_for_inefficient_updated_at_query: false } }
it 'sorts by only one column' do
expect(subject.order_values.size).to eq(2)
diff --git a/spec/finders/environments/environments_by_deployments_finder_spec.rb b/spec/finders/environments/environments_by_deployments_finder_spec.rb
index 8349092c79e..c8e6b038634 100644
--- a/spec/finders/environments/environments_by_deployments_finder_spec.rb
+++ b/spec/finders/environments/environments_by_deployments_finder_spec.rb
@@ -64,6 +64,22 @@ RSpec.describe Environments::EnvironmentsByDeploymentsFinder do
end
end
+ context 'sha deployment' do
+ before do
+ create(:deployment, :success, environment: environment, sha: project.commit.id)
+ end
+
+ it 'returns environment' do
+ expect(described_class.new(project, user, sha: project.commit.id).execute)
+ .to contain_exactly(environment)
+ end
+
+ it 'does not return environment when sha is different' do
+ expect(described_class.new(project, user, sha: '1234').execute)
+ .to be_empty
+ end
+ end
+
context 'commit deployment' do
before do
create(:deployment, :success, environment: environment, ref: 'master', sha: project.commit.id)
diff --git a/spec/finders/group_descendants_finder_spec.rb b/spec/finders/group_descendants_finder_spec.rb
index 59eeb078e9e..5c5db874e85 100644
--- a/spec/finders/group_descendants_finder_spec.rb
+++ b/spec/finders/group_descendants_finder_spec.rb
@@ -68,6 +68,12 @@ RSpec.describe GroupDescendantsFinder do
expect(finder.execute).to be_empty
end
+ it 'does not include projects aimed for deletion' do
+ _project_aimed_for_deletion = create(:project, :archived, marked_for_deletion_at: 2.days.ago, pending_delete: false)
+
+ expect(finder.execute).to be_empty
+ end
+
context 'with a filter' do
let(:params) { { filter: 'test' } }
@@ -165,8 +171,8 @@ RSpec.describe GroupDescendantsFinder do
end
context 'with nested groups' do
- let!(:project) { create(:project, namespace: group) }
- let!(:subgroup) { create(:group, :private, parent: group) }
+ let_it_be(:project) { create(:project, namespace: group) }
+ let_it_be_with_reload(:subgroup) { create(:group, :private, parent: group) }
describe '#execute' do
it 'contains projects and subgroups' do
@@ -208,57 +214,69 @@ RSpec.describe GroupDescendantsFinder do
context 'with a filter' do
let(:params) { { filter: 'test' } }
- it 'contains only matching projects and subgroups' do
- matching_project = create(:project, namespace: group, name: 'Testproject')
- matching_subgroup = create(:group, name: 'testgroup', parent: group)
+ shared_examples 'filter examples' do
+ it 'contains only matching projects and subgroups' do
+ matching_project = create(:project, namespace: group, name: 'Testproject')
+ matching_subgroup = create(:group, name: 'testgroup', parent: group)
- expect(finder.execute).to contain_exactly(matching_subgroup, matching_project)
- end
+ expect(finder.execute).to contain_exactly(matching_subgroup, matching_project)
+ end
- it 'does not include subgroups the user does not have access to' do
- _invisible_subgroup = create(:group, :private, parent: group, name: 'test1')
- other_subgroup = create(:group, :private, parent: group, name: 'test2')
- public_subgroup = create(:group, :public, parent: group, name: 'test3')
- other_subsubgroup = create(:group, :private, parent: other_subgroup, name: 'test4')
- other_user = create(:user)
- other_subgroup.add_developer(other_user)
+ it 'does not include subgroups the user does not have access to' do
+ _invisible_subgroup = create(:group, :private, parent: group, name: 'test1')
+ other_subgroup = create(:group, :private, parent: group, name: 'test2')
+ public_subgroup = create(:group, :public, parent: group, name: 'test3')
+ other_subsubgroup = create(:group, :private, parent: other_subgroup, name: 'test4')
+ other_user = create(:user)
+ other_subgroup.add_developer(other_user)
- finder = described_class.new(current_user: other_user,
- parent_group: group,
- params: params)
+ finder = described_class.new(current_user: other_user,
+ parent_group: group,
+ params: params)
- expect(finder.execute).to contain_exactly(other_subgroup, public_subgroup, other_subsubgroup)
- end
+ expect(finder.execute).to contain_exactly(other_subgroup, public_subgroup, other_subsubgroup)
+ end
- context 'with matching children' do
- it 'includes a group that has a subgroup matching the query and its parent' do
- matching_subgroup = create(:group, :private, name: 'testgroup', parent: subgroup)
+ context 'with matching children' do
+ it 'includes a group that has a subgroup matching the query and its parent' do
+ matching_subgroup = create(:group, :private, name: 'testgroup', parent: subgroup)
- expect(finder.execute).to contain_exactly(subgroup, matching_subgroup)
- end
+ expect(finder.execute).to contain_exactly(subgroup, matching_subgroup)
+ end
- it 'includes the parent of a matching project' do
- matching_project = create(:project, namespace: subgroup, name: 'Testproject')
+ it 'includes the parent of a matching project' do
+ matching_project = create(:project, namespace: subgroup, name: 'Testproject')
- expect(finder.execute).to contain_exactly(subgroup, matching_project)
- end
+ expect(finder.execute).to contain_exactly(subgroup, matching_project)
+ end
+
+ context 'with a small page size' do
+ let(:params) { { filter: 'test', per_page: 1 } }
- context 'with a small page size' do
- let(:params) { { filter: 'test', per_page: 1 } }
+ it 'contains all the ancestors of a matching subgroup regardless the page size' do
+ subgroup = create(:group, :private, parent: group)
+ matching = create(:group, :private, name: 'testgroup', parent: subgroup)
- it 'contains all the ancestors of a matching subgroup regardless the page size' do
- subgroup = create(:group, :private, parent: group)
- matching = create(:group, :private, name: 'testgroup', parent: subgroup)
+ expect(finder.execute).to contain_exactly(subgroup, matching)
+ end
+ end
+
+ it 'does not include the parent itself' do
+ group.update!(name: 'test')
- expect(finder.execute).to contain_exactly(subgroup, matching)
+ expect(finder.execute).not_to include(group)
end
end
+ end
- it 'does not include the parent itself' do
- group.update!(name: 'test')
+ it_behaves_like 'filter examples'
- expect(finder.execute).not_to include(group)
+ context 'when feature flag :linear_group_descendants_finder_upto is disabled' do
+ before do
+ stub_feature_flags(linear_group_descendants_finder_upto: false)
end
+
+ it_behaves_like 'filter examples'
end
end
end
diff --git a/spec/finders/group_projects_finder_spec.rb b/spec/finders/group_projects_finder_spec.rb
index 3fc4393df5d..4189be94cc1 100644
--- a/spec/finders/group_projects_finder_spec.rb
+++ b/spec/finders/group_projects_finder_spec.rb
@@ -9,13 +9,29 @@ RSpec.describe GroupProjectsFinder do
describe 'with a group member current user' do
before do
- group.add_maintainer(current_user)
+ root_group.add_maintainer(current_user)
end
context "only shared" do
let(:options) { { only_shared: true } }
it { is_expected.to match_array([shared_project_3, shared_project_2, shared_project_1]) }
+
+ context 'with ancestor groups projects' do
+ before do
+ options[:include_ancestor_groups] = true
+ end
+
+ it { is_expected.to match_array([shared_project_3, shared_project_2, shared_project_1]) }
+ end
+
+ context 'with subgroups projects' do
+ before do
+ options[:include_subgroups] = true
+ end
+
+ it { is_expected.to match_array([shared_project_3, shared_project_2, shared_project_1]) }
+ end
end
context "only owned" do
@@ -29,9 +45,46 @@ RSpec.describe GroupProjectsFinder do
it { is_expected.to match_array([private_project, public_project, subgroup_project, subgroup_private_project]) }
end
- context 'without subgroups projects' do
+ context 'with ancestor group projects' do
+ before do
+ options[:include_ancestor_groups] = true
+ end
+
+ it { is_expected.to match_array([private_project, public_project, root_group_public_project, root_group_private_project, root_group_private_project_2]) }
+ end
+
+ context 'with ancestor groups and subgroups projects' do
+ before do
+ options[:include_ancestor_groups] = true
+ options[:include_subgroups] = true
+ end
+
+ it { is_expected.to match_array([private_project, public_project, root_group_public_project, root_group_private_project, root_group_private_project_2, subgroup_private_project, subgroup_project]) }
+ end
+
+ context 'without subgroups and ancestor group projects' do
it { is_expected.to match_array([private_project, public_project]) }
end
+
+ context 'when user is member only of a subgroup' do
+ let(:subgroup_member) { create(:user) }
+
+ context 'with ancestor groups and subgroups projects' do
+ before do
+ group.add_maintainer(subgroup_member)
+ options[:include_ancestor_groups] = true
+ options[:include_subgroups] = true
+ end
+
+ it 'does not return parent group projects' do
+ finder = described_class.new(group: group, current_user: subgroup_member, params: params, options: options)
+
+ projects = finder.execute
+
+ expect(projects).to match_array([private_project, public_project, subgroup_project, subgroup_private_project, root_group_public_project])
+ end
+ end
+ end
end
context "all" do
@@ -90,6 +143,7 @@ RSpec.describe GroupProjectsFinder do
before do
private_project.add_maintainer(current_user)
subgroup_private_project.add_maintainer(current_user)
+ root_group_private_project.add_maintainer(current_user)
end
context 'with subgroups projects' do
@@ -100,6 +154,23 @@ RSpec.describe GroupProjectsFinder do
it { is_expected.to match_array([private_project, public_project, subgroup_project, subgroup_private_project]) }
end
+ context 'with ancestor groups projects' do
+ before do
+ options[:include_ancestor_groups] = true
+ end
+
+ it { is_expected.to match_array([private_project, public_project, root_group_public_project, root_group_private_project]) }
+ end
+
+ context 'with ancestor groups and subgroups projects' do
+ before do
+ options[:include_ancestor_groups] = true
+ options[:include_subgroups] = true
+ end
+
+ it { is_expected.to match_array([private_project, public_project, root_group_private_project, root_group_public_project, subgroup_private_project, subgroup_project]) }
+ end
+
context 'without subgroups projects' do
it { is_expected.to match_array([private_project, public_project]) }
end
@@ -118,6 +189,23 @@ RSpec.describe GroupProjectsFinder do
it { is_expected.to match_array([public_project, subgroup_project]) }
end
+ context 'with ancestor groups projects' do
+ before do
+ options[:include_ancestor_groups] = true
+ end
+
+ it { is_expected.to match_array([public_project, root_group_public_project]) }
+ end
+
+ context 'with ancestor groups and subgroups projects' do
+ before do
+ options[:include_subgroups] = true
+ options[:include_ancestor_groups] = true
+ end
+
+ it { is_expected.to match_array([public_project, root_group_public_project, subgroup_project]) }
+ end
+
context 'without subgroups projects' do
it { is_expected.to eq([public_project]) }
end
diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index 31563a6326d..c22e56c3b9e 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -1018,6 +1018,8 @@ RSpec.describe IssuesFinder do
end
context 'filtering by due date' do
+ let_it_be(:issue_due_today) { create(:issue, project: project1, due_date: Date.current) }
+ let_it_be(:issue_due_tomorrow) { create(:issue, project: project1, due_date: 1.day.from_now) }
let_it_be(:issue_overdue) { create(:issue, project: project1, due_date: 2.days.ago) }
let_it_be(:issue_due_soon) { create(:issue, project: project1, due_date: 2.days.from_now) }
@@ -1032,6 +1034,30 @@ RSpec.describe IssuesFinder do
end
end
+ context 'with param set to any due date' do
+ let(:params) { base_params.merge(due_date: Issue::AnyDueDate.name) }
+
+ it 'returns issues with any due date' do
+ expect(issues).to contain_exactly(issue_due_today, issue_due_tomorrow, issue_overdue, issue_due_soon)
+ end
+ end
+
+ context 'with param set to due today' do
+ let(:params) { base_params.merge(due_date: Issue::DueToday.name) }
+
+ it 'returns issues due today' do
+ expect(issues).to contain_exactly(issue_due_today)
+ end
+ end
+
+ context 'with param set to due tomorrow' do
+ let(:params) { base_params.merge(due_date: Issue::DueTomorrow.name) }
+
+ it 'returns issues due today' do
+ expect(issues).to contain_exactly(issue_due_tomorrow)
+ end
+ end
+
context 'with param set to overdue' do
let(:params) { base_params.merge(due_date: Issue::Overdue.name) }
@@ -1043,8 +1069,8 @@ RSpec.describe IssuesFinder do
context 'with param set to next month and previous two weeks' do
let(:params) { base_params.merge(due_date: Issue::DueNextMonthAndPreviousTwoWeeks.name) }
- it 'returns issues from the previous two weeks and next month' do
- expect(issues).to contain_exactly(issue_overdue, issue_due_soon)
+ it 'returns issues due in the previous two weeks and next month' do
+ expect(issues).to contain_exactly(issue_due_today, issue_due_tomorrow, issue_overdue, issue_due_soon)
end
end
diff --git a/spec/finders/merge_request_target_project_finder_spec.rb b/spec/finders/merge_request_target_project_finder_spec.rb
index 08fbfd7229a..bf735152d99 100644
--- a/spec/finders/merge_request_target_project_finder_spec.rb
+++ b/spec/finders/merge_request_target_project_finder_spec.rb
@@ -65,8 +65,8 @@ RSpec.describe MergeRequestTargetProjectFinder do
context 'private projects' do
let(:base_project) { create(:project, :private, path: 'base') }
- let(:forked_project) { fork_project(base_project, base_project.owner) }
- let(:other_fork) { fork_project(base_project, base_project.owner) }
+ let(:forked_project) { fork_project(base_project, base_project.first_owner) }
+ let(:other_fork) { fork_project(base_project, base_project.first_owner) }
context 'when the user is a member of all projects' do
before do
diff --git a/spec/finders/merge_requests_finder/params_spec.rb b/spec/finders/merge_requests_finder/params_spec.rb
new file mode 100644
index 00000000000..8c285972f48
--- /dev/null
+++ b/spec/finders/merge_requests_finder/params_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe MergeRequestsFinder::Params do
+ let(:user) { create(:user) }
+
+ subject { described_class.new(params, user, MergeRequest) }
+
+ describe 'attention' do
+ context 'attention param exists' do
+ let(:params) { { attention: user.username } }
+
+ it { expect(subject.attention).to eq(user) }
+ end
+
+ context 'attention param does not exist' do
+ let(:params) { { attention: nil } }
+
+ it { expect(subject.attention).to eq(nil) }
+ end
+ end
+end
diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb
index 0b6c438fd02..1f63f69a411 100644
--- a/spec/finders/merge_requests_finder_spec.rb
+++ b/spec/finders/merge_requests_finder_spec.rb
@@ -408,6 +408,22 @@ RSpec.describe MergeRequestsFinder do
end
end
+ context 'attention' do
+ subject { described_class.new(user, params).execute }
+
+ before do
+ reviewer = merge_request1.find_reviewer(user2)
+ reviewer.update!(state: :reviewed)
+ end
+
+ context 'by username' do
+ let(:params) { { attention: user2.username } }
+ let(:expected_mr) { [merge_request2, merge_request3] }
+
+ it { is_expected.to contain_exactly(*expected_mr) }
+ end
+ end
+
context 'reviewer filtering' do
subject { described_class.new(user, params).execute }
diff --git a/spec/finders/packages/conan/package_file_finder_spec.rb b/spec/finders/packages/conan/package_file_finder_spec.rb
index 3da7da456c2..62906a7b0a9 100644
--- a/spec/finders/packages/conan/package_file_finder_spec.rb
+++ b/spec/finders/packages/conan/package_file_finder_spec.rb
@@ -49,18 +49,6 @@ RSpec.describe ::Packages::Conan::PackageFileFinder do
expect(subject).to eq(package_file)
end
-
- context 'with packages_installable_package_files disabled' do
- before do
- stub_feature_flags(packages_installable_package_files: false)
- end
-
- it 'returns the correct package file' do
- expect(package.package_files.last).to eq(recent_package_file_pending_destruction)
-
- expect(subject).to eq(recent_package_file_pending_destruction)
- end
- end
end
describe '#execute' do
diff --git a/spec/finders/packages/package_file_finder_spec.rb b/spec/finders/packages/package_file_finder_spec.rb
index 8b21c9cd3ec..711e1396126 100644
--- a/spec/finders/packages/package_file_finder_spec.rb
+++ b/spec/finders/packages/package_file_finder_spec.rb
@@ -29,16 +29,6 @@ RSpec.describe Packages::PackageFileFinder do
expect(subject).to eq(package_file)
end
-
- context 'with packages_installable_package_files disabled' do
- before do
- stub_feature_flags(packages_installable_package_files: false)
- end
-
- it 'returns them' do
- expect(subject).to eq(recent_package_file_pending_destruction)
- end
- end
end
describe '#execute' do
diff --git a/spec/finders/releases_finder_spec.rb b/spec/finders/releases_finder_spec.rb
index 94b6fe53daa..5ddb5c33fad 100644
--- a/spec/finders/releases_finder_spec.rb
+++ b/spec/finders/releases_finder_spec.rb
@@ -23,6 +23,16 @@ RSpec.describe ReleasesFinder do
end
end
+ shared_examples_for 'when the user is not part of the group' do
+ before do
+ allow(Ability).to receive(:allowed?).with(user, :read_release, group).and_return(false)
+ end
+
+ it 'returns no releases' do
+ is_expected.to be_empty
+ end
+ end
+
# See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/27716
shared_examples_for 'when tag is nil' do
before do
@@ -66,9 +76,9 @@ RSpec.describe ReleasesFinder do
it_behaves_like 'when the user is not part of the project'
- context 'when the user is a project developer' do
+ context 'when the user is a project guest' do
before do
- project.add_developer(user)
+ project.add_guest(user)
end
it 'sorts by release date' do
@@ -118,25 +128,24 @@ RSpec.describe ReleasesFinder do
subject { described_class.new(group, user, params).execute(**args) }
- it_behaves_like 'when the user is not part of the project'
+ it_behaves_like 'when the user is not part of the group'
- context 'when the user is a project developer on one sibling project' do
+ context 'when the user is a project guest on one sibling project' do
before do
- project.add_developer(user)
+ project.add_guest(user)
v1_0_0.update_attribute(:released_at, 3.days.ago)
v1_1_0.update_attribute(:released_at, 1.day.ago)
end
- it 'sorts by release date' do
- expect(subject.size).to eq(2)
- expect(subject).to eq([v1_1_0, v1_0_0])
+ it 'does not return any releases' do
+ expect(subject.size).to eq(0)
+ expect(subject).to eq([])
end
end
- context 'when the user is a project developer on all projects' do
+ context 'when the user is a guest on the group' do
before do
- project.add_developer(user)
- project2.add_developer(user)
+ group.add_guest(user)
v1_0_0.update_attribute(:released_at, 3.days.ago)
v6.update_attribute(:released_at, 2.days.ago)
v1_1_0.update_attribute(:released_at, 1.day.ago)
@@ -161,22 +170,21 @@ RSpec.describe ReleasesFinder do
let(:project2) { create(:project, :repository, namespace: subgroup) }
let!(:v6) { create(:release, project: project2, tag: 'v6') }
- it_behaves_like 'when the user is not part of the project'
+ it_behaves_like 'when the user is not part of the group'
- context 'when the user a project developer in the subgroup project' do
+ context 'when the user a project guest in the subgroup project' do
before do
- project2.add_developer(user)
+ project2.add_guest(user)
end
- it 'returns only the subgroup releases' do
- expect(subject).to match_array([v6])
+ it 'does not return any releases' do
+ expect(subject).to match_array([])
end
end
- context 'when the user a project developer in both projects' do
+ context 'when the user is a guest on the group' do
before do
- project.add_developer(user)
- project2.add_developer(user)
+ group.add_guest(user)
v6.update_attribute(:released_at, 2.days.ago)
end
@@ -201,34 +209,32 @@ RSpec.describe ReleasesFinder do
p3.update_attribute(:released_at, 3.days.ago)
end
- it_behaves_like 'when the user is not part of the project'
+ it_behaves_like 'when the user is not part of the group'
- context 'when the user a project developer in the subgroup and subsubgroup project' do
+ context 'when the user a project guest in the subgroup and subsubgroup project' do
before do
- project2.add_developer(user)
- project3.add_developer(user)
+ project2.add_guest(user)
+ project3.add_guest(user)
end
- it 'returns only the subgroup and subsubgroup releases' do
- expect(subject).to match_array([v6, p3])
+ it 'does not return any releases' do
+ expect(subject).to match_array([])
end
end
- context 'when the user a project developer in the subsubgroup project' do
+ context 'when the user a project guest in the subsubgroup project' do
before do
- project3.add_developer(user)
+ project3.add_guest(user)
end
- it 'returns only the subsubgroup releases' do
- expect(subject).to match_array([p3])
+ it 'does not return any releases' do
+ expect(subject).to match_array([])
end
end
- context 'when the user a project developer in all projects' do
+ context 'when the user a guest on the group' do
before do
- project.add_developer(user)
- project2.add_developer(user)
- project3.add_developer(user)
+ group.add_guest(user)
end
it 'returns all releases' do
diff --git a/spec/finders/tags_finder_spec.rb b/spec/finders/tags_finder_spec.rb
index acc86547271..70d79ced81d 100644
--- a/spec/finders/tags_finder_spec.rb
+++ b/spec/finders/tags_finder_spec.rb
@@ -32,6 +32,14 @@ RSpec.describe TagsFinder do
expect(load_tags(params).first.name).to eq('v1.0.0')
end
+
+ context 'when sort is not a string' do
+ it 'ignores sort parameter' do
+ params = { sort: { 'invalid' => 'string' } }
+
+ expect(load_tags(params).first.name).to eq('v1.0.0')
+ end
+ end
end
context 'filter only' do
@@ -70,6 +78,13 @@ RSpec.describe TagsFinder do
result = load_tags({ search: 'nope$' })
expect(result.count).to eq(0)
end
+
+ context 'when search is not a string' do
+ it 'returns no matches' do
+ result = load_tags({ search: { 'a' => 'b' } })
+ expect(result.count).to eq(0)
+ end
+ end
end
context 'filter and sort' do
diff --git a/spec/finders/template_finder_spec.rb b/spec/finders/template_finder_spec.rb
index 97eecf8a89d..8e2426e697b 100644
--- a/spec/finders/template_finder_spec.rb
+++ b/spec/finders/template_finder_spec.rb
@@ -153,7 +153,12 @@ RSpec.describe TemplateFinder do
let(:params) { {} }
- subject(:result) { described_class.new(type, project, params).template_names.values.flatten.map { |el| OpenStruct.new(el) } }
+ let(:template_name_struct) { Struct.new(:name, :id, :key, :project_id, keyword_init: true) }
+
+ subject(:result) do
+ described_class.new(type, project, params).template_names.values.flatten
+ .map { |el| template_name_struct.new(el) }
+ end
where(:type, :vendored_name) do
:dockerfiles | 'Binary'
diff --git a/spec/fixtures/api/schemas/analytics/cycle_analytics/summary.json b/spec/fixtures/api/schemas/analytics/cycle_analytics/summary.json
index 296e18fca47..85acebb99ba 100644
--- a/spec/fixtures/api/schemas/analytics/cycle_analytics/summary.json
+++ b/spec/fixtures/api/schemas/analytics/cycle_analytics/summary.json
@@ -6,6 +6,9 @@
"type": "object",
"required": ["value", "title"],
"properties": {
+ "identifier": {
+ "type": "string"
+ },
"value": {
"type": "string"
},
diff --git a/spec/fixtures/api/schemas/entities/member_user.json b/spec/fixtures/api/schemas/entities/member_user.json
index 41a1e510de5..d42c686bb65 100644
--- a/spec/fixtures/api/schemas/entities/member_user.json
+++ b/spec/fixtures/api/schemas/entities/member_user.json
@@ -1,6 +1,6 @@
{
"type": "object",
- "required": ["id", "name", "username", "avatar_url", "web_url", "blocked", "two_factor_enabled"],
+ "required": ["id", "name", "username", "avatar_url", "web_url", "blocked", "two_factor_enabled", "show_status"],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
@@ -17,6 +17,7 @@
"emoji": { "type": "string" }
},
"additionalProperties": false
- }
+ },
+ "show_status": { "type": "boolean" }
}
}
diff --git a/spec/fixtures/api/schemas/public_api/v4/deployment.json b/spec/fixtures/api/schemas/public_api/v4/deployment.json
index 2371509edd6..5dab1456e7d 100644
--- a/spec/fixtures/api/schemas/public_api/v4/deployment.json
+++ b/spec/fixtures/api/schemas/public_api/v4/deployment.json
@@ -8,7 +8,8 @@
"created_at",
"updated_at",
"user",
- "deployable"
+ "deployable",
+ "status"
],
"properties": {
"id": { "type": "integer" },
@@ -30,6 +31,5 @@
]
},
"status": { "type": "string" }
- },
- "additionalProperties": false
+ }
}
diff --git a/spec/fixtures/api/schemas/public_api/v4/merge_request.json b/spec/fixtures/api/schemas/public_api/v4/merge_request.json
index a55c4b8974b..1ef2f9f9534 100644
--- a/spec/fixtures/api/schemas/public_api/v4/merge_request.json
+++ b/spec/fixtures/api/schemas/public_api/v4/merge_request.json
@@ -1,152 +1,91 @@
{
"type": "object",
"properties" : {
- "properties" : {
- "id": { "type": "integer" },
- "iid": { "type": "integer" },
- "project_id": { "type": "integer" },
- "title": { "type": "string" },
- "description": { "type": ["string", "null"] },
- "state": { "type": "string" },
- "merged_by": {
- "type": ["object", "null"],
- "properties": {
- "name": { "type": "string" },
- "username": { "type": "string" },
- "id": { "type": "integer" },
- "state": { "type": "string" },
- "avatar_url": { "type": "uri" },
- "web_url": { "type": "uri" }
- },
- "additionalProperties": false
- },
- "merge_user": {
- "type": ["object", "null"],
- "properties": {
- "name": { "type": "string" },
- "username": { "type": "string" },
- "id": { "type": "integer" },
- "state": { "type": "string" },
- "avatar_url": { "type": "uri" },
- "web_url": { "type": "uri" }
- },
- "additionalProperties": false
- },
- "merged_at": { "type": ["string", "null"] },
- "closed_by": {
- "type": ["object", "null"],
- "properties": {
- "name": { "type": "string" },
- "username": { "type": "string" },
- "id": { "type": "integer" },
- "state": { "type": "string" },
- "avatar_url": { "type": "uri" },
- "web_url": { "type": "uri" }
- },
- "additionalProperties": false
- },
- "closed_at": { "type": ["string", "null"], "format": "date-time" },
- "created_at": { "type": "string", "format": "date-time" },
- "updated_at": { "type": "string", "format": "date-time" },
- "target_branch": { "type": "string" },
- "source_branch": { "type": "string" },
- "upvotes": { "type": "integer" },
- "downvotes": { "type": "integer" },
- "author": {
- "type": "object",
- "properties": {
- "name": { "type": "string" },
- "username": { "type": "string" },
- "id": { "type": "integer" },
- "state": { "type": "string" },
- "avatar_url": { "type": "uri" },
- "web_url": { "type": "uri" }
- },
- "additionalProperties": false
- },
- "assignee": {
- "type": ["object", "null"],
- "properties": {
- "name": { "type": "string" },
- "username": { "type": "string" },
- "id": { "type": "integer" },
- "state": { "type": "string" },
- "avatar_url": { "type": "uri" },
- "web_url": { "type": "uri" }
- },
- "additionalProperties": false
- },
- "assignees": {
- "items": {
- "$ref": "./merge_request.json"
- }
- },
- "source_project_id": { "type": "integer" },
- "target_project_id": { "type": "integer" },
- "labels": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "work_in_progress": { "type": "boolean" },
- "milestone": {
- "type": ["object", "null"],
- "properties": {
- "id": { "type": "integer" },
- "iid": { "type": "integer" },
- "project_id": { "type": ["integer", "null"] },
- "group_id": { "type": ["integer", "null"] },
- "title": { "type": "string" },
- "description": { "type": ["string", "null"] },
- "state": { "type": "string" },
- "created_at": { "type": "string", "format": "date-time" },
- "updated_at": { "type": "string", "format": "date-time" },
- "due_date": { "type": "string", "format": "date-time" },
- "start_date": { "type": "string", "format": "date-time" }
- },
- "additionalProperties": false
- },
- "merge_when_pipeline_succeeds": { "type": "boolean" },
- "merge_status": { "type": "string" },
- "sha": { "type": "string" },
- "merge_commit_sha": { "type": ["string", "null"] },
- "user_notes_count": { "type": "integer" },
- "changes_count": { "type": "string" },
- "should_remove_source_branch": { "type": ["boolean", "null"] },
- "force_remove_source_branch": { "type": ["boolean", "null"] },
- "discussion_locked": { "type": ["boolean", "null"] },
- "web_url": { "type": "uri" },
- "squash": { "type": "boolean" },
- "time_stats": {
- "time_estimate": { "type": "integer" },
- "total_time_spent": { "type": "integer" },
- "human_time_estimate": { "type": ["string", "null"] },
- "human_total_time_spent": { "type": ["string", "null"] }
- },
- "allow_collaboration": { "type": ["boolean", "null"] },
- "allow_maintainer_to_push": { "type": ["boolean", "null"] },
- "references": {
- "short": {"type": "string"},
- "relative": {"type": "string"},
- "full": {"type": "string"}
+ "id": { "type": "integer" },
+ "iid": { "type": "integer" },
+ "project_id": { "type": "integer" },
+ "title": { "type": "string" },
+ "description": { "type": ["string", "null"] },
+ "state": { "type": "string" },
+ "merged_by": { "$ref": "user/basic.json" },
+ "merge_user": { "$ref": "user/basic.json" },
+ "merged_at": { "type": ["string", "null"] },
+ "closed_by": { "$ref": "user/basic.json" },
+ "closed_at": { "type": ["string", "null"], "format": "date-time" },
+ "created_at": { "type": "string", "format": "date-time" },
+ "updated_at": { "type": "string", "format": "date-time" },
+ "target_branch": { "type": "string" },
+ "source_branch": { "type": "string" },
+ "upvotes": { "type": "integer" },
+ "downvotes": { "type": "integer" },
+ "author": { "$ref": "user/basic.json" },
+ "assignee": { "$ref": "user/basic.json" },
+ "assignees": {
+ "type": "array",
+ "items": {
+ "$ref": "user/basic.json"
}
},
- "required": [
- "id", "iid", "project_id", "title", "description",
- "state", "created_at", "updated_at", "target_branch",
- "source_branch", "upvotes", "downvotes", "author",
- "assignee", "source_project_id", "target_project_id",
- "labels", "work_in_progress", "milestone", "merge_when_pipeline_succeeds",
- "merge_status", "sha", "merge_commit_sha", "user_notes_count",
- "should_remove_source_branch", "force_remove_source_branch",
- "web_url", "squash"
- ],
- "head_pipeline": {
+ "reviewers": {
+ "type": "array",
+ "items": {
+ "$ref": "user/basic.json"
+ }
+ },
+ "source_project_id": { "type": "integer" },
+ "target_project_id": { "type": "integer" },
+ "labels": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "work_in_progress": { "type": "boolean" },
+ "milestone": {
"oneOf": [
{ "type": "null" },
- { "$ref": "pipeline/detail.json" }
+ { "$ref": "milestone.json" }
]
+ },
+ "merge_when_pipeline_succeeds": { "type": "boolean" },
+ "merge_status": { "type": "string" },
+ "sha": { "type": "string" },
+ "merge_commit_sha": { "type": ["string", "null"] },
+ "user_notes_count": { "type": "integer" },
+ "changes_count": { "type": "string" },
+ "should_remove_source_branch": { "type": ["boolean", "null"] },
+ "force_remove_source_branch": { "type": ["boolean", "null"] },
+ "discussion_locked": { "type": ["boolean", "null"] },
+ "web_url": { "type": "uri" },
+ "squash": { "type": "boolean" },
+ "time_stats": {
+ "time_estimate": { "type": "integer" },
+ "total_time_spent": { "type": "integer" },
+ "human_time_estimate": { "type": ["string", "null"] },
+ "human_total_time_spent": { "type": ["string", "null"] }
+ },
+ "allow_collaboration": { "type": ["boolean", "null"] },
+ "allow_maintainer_to_push": { "type": ["boolean", "null"] },
+ "references": {
+ "short": {"type": "string"},
+ "relative": {"type": "string"},
+ "full": {"type": "string"}
}
+ },
+ "required": [
+ "id", "iid", "project_id", "title", "description",
+ "state", "created_at", "updated_at", "target_branch",
+ "source_branch", "upvotes", "downvotes", "author",
+ "assignee", "source_project_id", "target_project_id",
+ "labels", "work_in_progress", "milestone", "merge_when_pipeline_succeeds",
+ "merge_status", "sha", "merge_commit_sha", "user_notes_count",
+ "should_remove_source_branch", "force_remove_source_branch",
+ "web_url", "squash"
+ ],
+ "head_pipeline": {
+ "oneOf": [
+ { "type": "null" },
+ { "$ref": "pipeline/detail.json" }
+ ]
}
}
diff --git a/spec/frontend/__helpers__/matchers/to_match_interpolated_text.js b/spec/frontend/__helpers__/matchers/to_match_interpolated_text.js
index 4ce814a01b4..41e69bffd88 100644
--- a/spec/frontend/__helpers__/matchers/to_match_interpolated_text.js
+++ b/spec/frontend/__helpers__/matchers/to_match_interpolated_text.js
@@ -1,4 +1,5 @@
-export const toMatchInterpolatedText = (received, match) => {
+// Custom matchers are object methods and should be traditional functions to be able to access `utils` on `this`
+export function toMatchInterpolatedText(received, match) {
let clearReceived;
let clearMatch;
@@ -15,16 +16,14 @@ export const toMatchInterpolatedText = (received, match) => {
const pass = clearReceived === clearMatch;
const message = pass
? () => `
- \n\n
- Expected: ${this.utils.printExpected(clearReceived)}
- To not equal: ${this.utils.printReceived(clearMatch)}
+ Expected to not be: ${this.utils.printExpected(clearMatch)}
+ Received: ${this.utils.printReceived(clearReceived)}
`
: () =>
`
- \n\n
- Expected: ${this.utils.printExpected(clearReceived)}
- To equal: ${this.utils.printReceived(clearMatch)}
+ Expected to be: ${this.utils.printExpected(clearMatch)}
+ Received: ${this.utils.printReceived(clearReceived)}
`;
return { actual: received, message, pass };
-};
+}
diff --git a/spec/frontend/__helpers__/mock_apollo_helper.js b/spec/frontend/__helpers__/mock_apollo_helper.js
index ee4bbd42b1e..c07a6d8ef85 100644
--- a/spec/frontend/__helpers__/mock_apollo_helper.js
+++ b/spec/frontend/__helpers__/mock_apollo_helper.js
@@ -1,22 +1,25 @@
-import { InMemoryCache } from 'apollo-cache-inmemory';
+import { InMemoryCache } from '@apollo/client/core';
import { createMockClient as createMockApolloClient } from 'mock-apollo-client';
import VueApollo from 'vue-apollo';
-
-const defaultCacheOptions = {
- fragmentMatcher: { match: () => true },
- addTypename: false,
-};
+import possibleTypes from '~/graphql_shared/possibleTypes.json';
+import { typePolicies } from '~/lib/graphql';
export function createMockClient(handlers = [], resolvers = {}, cacheOptions = {}) {
const cache = new InMemoryCache({
- ...defaultCacheOptions,
+ possibleTypes,
+ typePolicies,
+ addTypename: false,
...cacheOptions,
});
const mockClient = createMockApolloClient({ cache, resolvers });
if (Array.isArray(handlers)) {
- handlers.forEach(([query, value]) => mockClient.setRequestHandler(query, value));
+ handlers.forEach(([query, value]) =>
+ mockClient.setRequestHandler(query, (...args) =>
+ Promise.resolve(value(...args)).then((r) => ({ ...r })),
+ ),
+ );
} else {
throw new Error('You should pass an array of handlers to mock Apollo client');
}
diff --git a/spec/frontend/__helpers__/test_apollo_link.js b/spec/frontend/__helpers__/test_apollo_link.js
index dde3a4e99bb..eab0c2de212 100644
--- a/spec/frontend/__helpers__/test_apollo_link.js
+++ b/spec/frontend/__helpers__/test_apollo_link.js
@@ -1,7 +1,4 @@
-import { InMemoryCache } from 'apollo-cache-inmemory';
-import { ApolloClient } from 'apollo-client';
-import { ApolloLink } from 'apollo-link';
-import gql from 'graphql-tag';
+import { InMemoryCache, ApolloClient, ApolloLink, gql } from '@apollo/client/core';
const FOO_QUERY = gql`
query {
diff --git a/spec/frontend/__mocks__/@gitlab/ui.js b/spec/frontend/__mocks__/@gitlab/ui.js
index 6b3f1f01e6a..6f2888e5c42 100644
--- a/spec/frontend/__mocks__/@gitlab/ui.js
+++ b/spec/frontend/__mocks__/@gitlab/ui.js
@@ -41,10 +41,15 @@ jest.mock('@gitlab/ui/dist/components/base/popover/popover.js', () => ({
default: () => [],
},
...Object.fromEntries(
- ['title', 'target', 'triggers', 'placement', 'boundary', 'container'].map((prop) => [
- prop,
- {},
- ]),
+ [
+ 'title',
+ 'target',
+ 'triggers',
+ 'placement',
+ 'boundary',
+ 'container',
+ 'showCloseButton',
+ ].map((prop) => [prop, {}]),
),
},
render(h) {
diff --git a/spec/frontend/actioncable_link_spec.js b/spec/frontend/actioncable_link_spec.js
index c785151f8fd..b15b93cb11d 100644
--- a/spec/frontend/actioncable_link_spec.js
+++ b/spec/frontend/actioncable_link_spec.js
@@ -1,5 +1,5 @@
import { print } from 'graphql';
-import gql from 'graphql-tag';
+import { gql } from '@apollo/client/core';
import cable from '~/actioncable_consumer';
import ActionCableLink from '~/actioncable_link';
diff --git a/spec/frontend/add_context_commits_modal/components/__snapshots__/add_context_commits_modal_spec.js.snap b/spec/frontend/add_context_commits_modal/components/__snapshots__/add_context_commits_modal_spec.js.snap
index 10437c48f88..82114077455 100644
--- a/spec/frontend/add_context_commits_modal/components/__snapshots__/add_context_commits_modal_spec.js.snap
+++ b/spec/frontend/add_context_commits_modal/components/__snapshots__/add_context_commits_modal_spec.js.snap
@@ -17,7 +17,6 @@ exports[`AddContextCommitsModal renders modal with 2 tabs 1`] = `
<gl-tabs-stub
contentclass="pt-0"
queryparamname="tab"
- theme="indigo"
value="0"
>
<gl-tab-stub
diff --git a/spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js b/spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js
index e7a20ae114c..9b93fd26fa0 100644
--- a/spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js
+++ b/spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js
@@ -1,5 +1,6 @@
import { GlModal, GlSearchBoxByType } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import getDiffWithCommit from 'test_fixtures/merge_request_diffs/with_commit.json';
import AddReviewItemsModal from '~/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue';
@@ -8,8 +9,7 @@ import * as actions from '~/add_context_commits_modal/store/actions';
import mutations from '~/add_context_commits_modal/store/mutations';
import defaultState from '~/add_context_commits_modal/store/state';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('AddContextCommitsModal', () => {
let wrapper;
@@ -36,7 +36,6 @@ describe('AddContextCommitsModal', () => {
});
wrapper = shallowMount(AddReviewItemsModal, {
- localVue,
store,
propsData: {
contextCommitsPath: '',
@@ -85,11 +84,10 @@ describe('AddContextCommitsModal', () => {
expect(findModal().attributes('ok-disabled')).toBe('true');
});
- it('enabled ok button when atleast one row is selected', () => {
+ it('enabled ok button when atleast one row is selected', async () => {
wrapper.vm.$store.state.selectedCommits = [{ ...commit, isSelected: true }];
- return wrapper.vm.$nextTick().then(() => {
- expect(findModal().attributes('ok-disabled')).toBeFalsy();
- });
+ await nextTick();
+ expect(findModal().attributes('ok-disabled')).toBeFalsy();
});
});
@@ -101,11 +99,10 @@ describe('AddContextCommitsModal', () => {
expect(findModal().attributes('ok-disabled')).toBe('true');
});
- it('an enabled ok button when atleast one row is selected', () => {
+ it('an enabled ok button when atleast one row is selected', async () => {
wrapper.vm.$store.state.selectedCommits = [{ ...commit, isSelected: true }];
- return wrapper.vm.$nextTick().then(() => {
- expect(findModal().attributes('ok-disabled')).toBeFalsy();
- });
+ await nextTick();
+ expect(findModal().attributes('ok-disabled')).toBeFalsy();
});
it('a disabled ok button in first tab, when row is selected in second tab', () => {
@@ -115,33 +112,30 @@ describe('AddContextCommitsModal', () => {
});
describe('has an ok button when clicked calls action', () => {
- it('"createContextCommits" when only new commits to be added ', () => {
+ it('"createContextCommits" when only new commits to be added ', async () => {
wrapper.vm.$store.state.selectedCommits = [{ ...commit, isSelected: true }];
findModal().vm.$emit('ok');
- return wrapper.vm.$nextTick().then(() => {
- expect(createContextCommits).toHaveBeenCalledWith(expect.anything(), {
- commits: [{ ...commit, isSelected: true }],
- forceReload: true,
- });
+ await nextTick();
+ expect(createContextCommits).toHaveBeenCalledWith(expect.anything(), {
+ commits: [{ ...commit, isSelected: true }],
+ forceReload: true,
});
});
- it('"removeContextCommits" when only added commits are to be removed ', () => {
+ it('"removeContextCommits" when only added commits are to be removed ', async () => {
wrapper.vm.$store.state.toRemoveCommits = [commit.short_id];
findModal().vm.$emit('ok');
- return wrapper.vm.$nextTick().then(() => {
- expect(removeContextCommits).toHaveBeenCalledWith(expect.anything(), true);
- });
+ await nextTick();
+ expect(removeContextCommits).toHaveBeenCalledWith(expect.anything(), true);
});
- it('"createContextCommits" and "removeContextCommits" when new commits are to be added and old commits are to be removed', () => {
+ it('"createContextCommits" and "removeContextCommits" when new commits are to be added and old commits are to be removed', async () => {
wrapper.vm.$store.state.selectedCommits = [{ ...commit, isSelected: true }];
wrapper.vm.$store.state.toRemoveCommits = [commit.short_id];
findModal().vm.$emit('ok');
- return wrapper.vm.$nextTick().then(() => {
- expect(createContextCommits).toHaveBeenCalledWith(expect.anything(), {
- commits: [{ ...commit, isSelected: true }],
- });
- expect(removeContextCommits).toHaveBeenCalledWith(expect.anything(), undefined);
+ await nextTick();
+ expect(createContextCommits).toHaveBeenCalledWith(expect.anything(), {
+ commits: [{ ...commit, isSelected: true }],
});
+ expect(removeContextCommits).toHaveBeenCalledWith(expect.anything(), undefined);
});
});
diff --git a/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js b/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js
index 025ae825e0d..f875cd24ee1 100644
--- a/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js
+++ b/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js
@@ -1,5 +1,6 @@
import { GlButton, GlFormInput, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import DeleteUserModal from '~/admin/users/components/modals/delete_user_modal.vue';
import UserDeletionObstaclesList from '~/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.vue';
import ModalStub from './stubs/modal_stub';
@@ -82,11 +83,11 @@ describe('User Operation confirmation modal', () => {
});
describe('with incorrect username', () => {
- beforeEach(() => {
+ beforeEach(async () => {
createComponent();
setUsername(badUsername);
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('shows incorrect username', () => {
@@ -100,11 +101,11 @@ describe('User Operation confirmation modal', () => {
});
describe('with correct username', () => {
- beforeEach(() => {
+ beforeEach(async () => {
createComponent();
setUsername(username);
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('shows correct username', () => {
@@ -117,10 +118,10 @@ describe('User Operation confirmation modal', () => {
});
describe('when primary action is submitted', () => {
- beforeEach(() => {
+ beforeEach(async () => {
findPrimaryButton().vm.$emit('click');
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('clears the input', () => {
@@ -136,10 +137,10 @@ describe('User Operation confirmation modal', () => {
});
describe('when secondary action is submitted', () => {
- beforeEach(() => {
+ beforeEach(async () => {
findSecondaryButton().vm.$emit('click');
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('has correct form attributes and calls submit', () => {
@@ -168,7 +169,7 @@ describe('User Operation confirmation modal', () => {
it("shows enabled buttons when user's name is entered without whitespace", async () => {
setUsername('John Smith');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findPrimaryButton().attributes('disabled')).toBeUndefined();
expect(findSecondaryButton().attributes('disabled')).toBeUndefined();
diff --git a/spec/frontend/admin/users/components/modals/user_modal_manager_spec.js b/spec/frontend/admin/users/components/modals/user_modal_manager_spec.js
index 65ce242662b..4786357faa1 100644
--- a/spec/frontend/admin/users/components/modals/user_modal_manager_spec.js
+++ b/spec/frontend/admin/users/components/modals/user_modal_manager_spec.js
@@ -1,4 +1,5 @@
import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import UserModalManager from '~/admin/users/components/modals/user_modal_manager.vue';
import ModalStub from './stubs/modal_stub';
@@ -50,20 +51,19 @@ describe('Users admin page Modal Manager', () => {
expect(() => wrapper.vm.show({ glModalAction: 'action1' })).toThrow();
});
- it('renders modal with expected props when valid configuration is passed', () => {
+ it('renders modal with expected props when valid configuration is passed', async () => {
createComponent();
wrapper.vm.show({
glModalAction: 'action1',
extraProp: 'extraPropValue',
});
- return wrapper.vm.$nextTick().then(() => {
- const modal = findModal();
- expect(modal.exists()).toBeTruthy();
- expect(modal.vm.$attrs.csrfToken).toEqual('dummyCSRF');
- expect(modal.vm.$attrs.extraProp).toEqual('extraPropValue');
- expect(modal.vm.showWasCalled).toBeTruthy();
- });
+ await nextTick();
+ const modal = findModal();
+ expect(modal.exists()).toBeTruthy();
+ expect(modal.vm.$attrs.csrfToken).toEqual('dummyCSRF');
+ expect(modal.vm.$attrs.extraProp).toEqual('extraPropValue');
+ expect(modal.vm.showWasCalled).toBeTruthy();
});
});
@@ -101,7 +101,7 @@ describe('Users admin page Modal Manager', () => {
it('renders the modal when the button is clicked', async () => {
button.click();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findModal().exists()).toBe(true);
});
@@ -110,7 +110,7 @@ describe('Users admin page Modal Manager', () => {
button.removeAttribute('data-gl-modal-action');
button.click();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findModal().exists()).toBe(false);
});
@@ -118,7 +118,7 @@ describe('Users admin page Modal Manager', () => {
it('does not render the modal when a button without the selector class is clicked', async () => {
button2.click();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findModal().exists()).toBe(false);
});
diff --git a/spec/frontend/admin/users/components/users_table_spec.js b/spec/frontend/admin/users/components/users_table_spec.js
index 9ff5961c7ec..ad1c45495b5 100644
--- a/spec/frontend/admin/users/components/users_table_spec.js
+++ b/spec/frontend/admin/users/components/users_table_spec.js
@@ -3,6 +3,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import AdminUserActions from '~/admin/users/components/user_actions.vue';
@@ -106,8 +107,9 @@ describe('AdminUsersTable component', () => {
});
describe('when the data has been fetched', () => {
- beforeEach(() => {
+ beforeEach(async () => {
initComponent();
+ await waitForPromises();
});
it("renders the user's group count", () => {
@@ -115,8 +117,9 @@ describe('AdminUsersTable component', () => {
});
describe("and a user's group count is null", () => {
- beforeEach(() => {
+ beforeEach(async () => {
initComponent({}, createFetchGroupCount([{ id: user.id, groupCount: null }]));
+ await waitForPromises();
});
it("renders the user's group count as 0", () => {
@@ -126,12 +129,12 @@ describe('AdminUsersTable component', () => {
});
describe('when there is an error while fetching the data', () => {
- beforeEach(() => {
+ beforeEach(async () => {
initComponent({}, fetchGroupCountsError);
+ await waitForPromises();
});
it('creates a flash message and captures the error', () => {
- expect(createFlash).toHaveBeenCalledTimes(1);
expect(createFlash).toHaveBeenCalledWith({
message: 'Could not load user group counts. Please refresh the page to try again.',
captureError: true,
diff --git a/spec/frontend/alert_management/components/alert_management_table_spec.js b/spec/frontend/alert_management/components/alert_management_table_spec.js
index 39aab8dc1f8..5b823694b99 100644
--- a/spec/frontend/alert_management/components/alert_management_table_spec.js
+++ b/spec/frontend/alert_management/components/alert_management_table_spec.js
@@ -2,6 +2,7 @@ import { GlTable, GlAlert, GlLoadingIcon, GlDropdown, GlIcon, GlAvatar } from '@
import { mount } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
+import { nextTick } from 'vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import mockAlerts from 'jest/vue_shared/alert_details/mocks/alerts.json';
@@ -169,7 +170,7 @@ describe('AlertManagementTable', () => {
loading: false,
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.find(GlTable).exists()).toBe(true);
expect(findAlertsTable().find(GlIcon).classes('icon-critical')).toBe(true);
diff --git a/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js b/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js
index e6a6e01c41c..6193233881d 100644
--- a/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js
+++ b/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js
@@ -1,7 +1,7 @@
import { GlLoadingIcon, GlAlert } from '@gitlab/ui';
-import { mount, createLocalVue } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
-import { nextTick } from 'vue';
+import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createHttpIntegrationMutation from 'ee_else_ce/alerts_settings/graphql/mutations/create_http_integration.mutation.graphql';
import updateHttpIntegrationMutation from 'ee_else_ce/alerts_settings/graphql/mutations/update_http_integration.mutation.graphql';
@@ -50,8 +50,6 @@ import mockIntegrations from './mocks/integrations.json';
jest.mock('~/flash');
-const localVue = createLocalVue();
-
describe('AlertsSettingsWrapper', () => {
let wrapper;
let fakeApollo;
@@ -70,21 +68,12 @@ describe('AlertsSettingsWrapper', () => {
const findAlertsSettingsForm = () => wrapper.findComponent(AlertsSettingsForm);
const findAlert = () => wrapper.findComponent(GlAlert);
- async function destroyHttpIntegration(localWrapper) {
- await jest.runOnlyPendingTimers();
- await localWrapper.vm.$nextTick();
-
+ function destroyHttpIntegration(localWrapper) {
localWrapper
.find(IntegrationsList)
.vm.$emit('delete-integration', { id: integrationToDestroy.id });
}
- async function awaitApolloDomMock() {
- await nextTick(); // kick off the DOM update
- await jest.runOnlyPendingTimers(); // kick off the mocked GQL stuff (promises)
- await nextTick(); // kick off the DOM update for flash
- }
-
const createComponent = ({ data = {}, provide = {}, loading = false } = {}) => {
wrapper = extendedWrapper(
mount(AlertsSettingsWrapper, {
@@ -118,7 +107,7 @@ describe('AlertsSettingsWrapper', () => {
function createComponentWithApollo({
destroyHandler = jest.fn().mockResolvedValue(destroyIntegrationResponse),
} = {}) {
- localVue.use(VueApollo);
+ Vue.use(VueApollo);
destroyIntegrationHandler = destroyHandler;
const requestHandlers = [
@@ -129,7 +118,6 @@ describe('AlertsSettingsWrapper', () => {
fakeApollo = createMockApollo(requestHandlers);
wrapper = mount(AlertsSettingsWrapper, {
- localVue,
apolloProvider: fakeApollo,
provide: {
alertSettings: {
@@ -476,21 +464,19 @@ describe('AlertsSettingsWrapper', () => {
describe('with mocked Apollo client', () => {
it('has a selection of integrations loaded via the getIntegrationsQuery', async () => {
createComponentWithApollo();
-
- await jest.runOnlyPendingTimers();
- await nextTick();
+ await waitForPromises();
expect(findIntegrations()).toHaveLength(4);
});
it('calls a mutation with correct parameters and destroys a integration', async () => {
createComponentWithApollo();
+ await waitForPromises();
- await destroyHttpIntegration(wrapper);
+ destroyHttpIntegration(wrapper);
expect(destroyIntegrationHandler).toHaveBeenCalled();
-
- await nextTick();
+ await waitForPromises();
expect(findIntegrations()).toHaveLength(3);
});
@@ -501,7 +487,7 @@ describe('AlertsSettingsWrapper', () => {
});
await destroyHttpIntegration(wrapper);
- await awaitApolloDomMock();
+ await waitForPromises();
expect(createFlash).toHaveBeenCalledWith({ message: 'Houston, we have a problem' });
});
@@ -512,7 +498,7 @@ describe('AlertsSettingsWrapper', () => {
});
await destroyHttpIntegration(wrapper);
- await awaitApolloDomMock();
+ await waitForPromises();
expect(createFlash).toHaveBeenCalledWith({
message: DELETE_INTEGRATION_ERROR,
diff --git a/spec/frontend/alerts_settings/components/mocks/apollo_mock.js b/spec/frontend/alerts_settings/components/mocks/apollo_mock.js
index e7ad2cd1d2a..694dff56632 100644
--- a/spec/frontend/alerts_settings/components/mocks/apollo_mock.js
+++ b/spec/frontend/alerts_settings/components/mocks/apollo_mock.js
@@ -38,6 +38,7 @@ export const getIntegrationsQueryResponse = {
alertManagementIntegrations: {
nodes: [
{
+ __typename: 'AlertManagementIntegration',
id: '37',
type: 'HTTP',
active: true,
@@ -48,6 +49,7 @@ export const getIntegrationsQueryResponse = {
apiUrl: null,
},
{
+ __typename: 'AlertManagementIntegration',
id: '41',
type: 'HTTP',
active: true,
@@ -58,6 +60,7 @@ export const getIntegrationsQueryResponse = {
apiUrl: null,
},
{
+ __typename: 'AlertManagementIntegration',
id: '40',
type: 'HTTP',
active: true,
@@ -68,6 +71,7 @@ export const getIntegrationsQueryResponse = {
apiUrl: null,
},
{
+ __typename: 'AlertManagementIntegration',
id: '12',
type: 'PROMETHEUS',
active: false,
@@ -83,6 +87,7 @@ export const getIntegrationsQueryResponse = {
};
export const integrationToDestroy = {
+ __typename: 'AlertManagementIntegration',
id: '37',
type: 'HTTP',
active: true,
@@ -97,6 +102,7 @@ export const destroyIntegrationResponse = {
httpIntegrationDestroy: {
errors: [],
integration: {
+ __typename: 'AlertManagementIntegration',
id: '37',
type: 'HTTP',
active: true,
diff --git a/spec/frontend/analytics/shared/components/daterange_spec.js b/spec/frontend/analytics/shared/components/daterange_spec.js
index 854582abb82..a38df274243 100644
--- a/spec/frontend/analytics/shared/components/daterange_spec.js
+++ b/spec/frontend/analytics/shared/components/daterange_spec.js
@@ -1,7 +1,6 @@
-import { GlDaterangePicker } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
+import { GlDaterangePicker, GlSprintf } from '@gitlab/ui';
+import { shallowMount, mount } from '@vue/test-utils';
import { useFakeDate } from 'helpers/fake_date';
-import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import Daterange from '~/analytics/shared/components/daterange.vue';
const defaultProps = {
@@ -14,13 +13,13 @@ describe('Daterange component', () => {
let wrapper;
- const factory = (props = defaultProps) => {
- wrapper = mount(Daterange, {
+ const factory = (props = defaultProps, mountFn = shallowMount) => {
+ wrapper = mountFn(Daterange, {
propsData: {
...defaultProps,
...props,
},
- directives: { GlTooltip: createMockDirective() },
+ stubs: { GlSprintf },
});
};
@@ -28,9 +27,8 @@ describe('Daterange component', () => {
wrapper.destroy();
});
- const findDaterangePicker = () => wrapper.find(GlDaterangePicker);
-
- const findDateRangeIndicator = () => wrapper.find('.daterange-indicator');
+ const findDaterangePicker = () => wrapper.findComponent(GlDaterangePicker);
+ const findDateRangeIndicator = () => wrapper.findComponent(GlSprintf);
describe('template', () => {
describe('when show is false', () => {
@@ -43,26 +41,24 @@ describe('Daterange component', () => {
describe('when show is true', () => {
it('renders the daterange picker', () => {
factory({ show: true });
+
expect(findDaterangePicker().exists()).toBe(true);
});
});
describe('with a minDate being set', () => {
- it('emits the change event with the minDate when the user enters a start date before the minDate', () => {
+ it('emits the change event with the minDate when the user enters a start date before the minDate', async () => {
const startDate = new Date('2019-09-01');
const endDate = new Date('2019-09-30');
const minDate = new Date('2019-06-01');
- factory({ show: true, startDate, endDate, minDate });
-
+ factory({ show: true, startDate, endDate, minDate }, mount);
const input = findDaterangePicker().find('input');
input.setValue('2019-01-01');
- input.trigger('change');
+ await input.trigger('change');
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted().change).toEqual([[{ startDate: minDate, endDate }]]);
- });
+ expect(wrapper.emitted().change).toEqual([[{ startDate: minDate, endDate }]]);
});
});
@@ -76,16 +72,13 @@ describe('Daterange component', () => {
});
it('displays the correct number of selected days in the indicator', () => {
- expect(findDateRangeIndicator().find('span').text()).toBe('10 days selected');
+ expect(findDateRangeIndicator().text()).toMatchInterpolatedText('10 days selected');
});
- it('displays a tooltip', () => {
- const icon = wrapper.find('[data-testid="helper-icon"]');
- const tooltip = getBinding(icon.element, 'gl-tooltip');
-
- expect(tooltip).toBeDefined();
- expect(icon.attributes('title')).toBe(
- 'Showing data for workflow items created in this date range. Date range cannot exceed 30 days.',
+ it('sets the tooltip', () => {
+ const tooltip = findDaterangePicker().props('tooltip');
+ expect(tooltip).toBe(
+ 'Showing data for workflow items created in this date range. Date range limited to 30 days.',
);
});
});
diff --git a/spec/frontend/cycle_analytics/metric_popover_spec.js b/spec/frontend/analytics/shared/components/metric_popover_spec.js
index 5a622fcacd5..b799c911488 100644
--- a/spec/frontend/cycle_analytics/metric_popover_spec.js
+++ b/spec/frontend/analytics/shared/components/metric_popover_spec.js
@@ -1,6 +1,6 @@
import { GlLink, GlIcon } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import MetricPopover from '~/cycle_analytics/components/metric_popover.vue';
+import MetricPopover from '~/analytics/shared/components/metric_popover.vue';
const MOCK_METRIC = {
key: 'deployment-frequency',
diff --git a/spec/frontend/analytics/shared/components/metric_tile_spec.js b/spec/frontend/analytics/shared/components/metric_tile_spec.js
new file mode 100644
index 00000000000..980dfad9eb0
--- /dev/null
+++ b/spec/frontend/analytics/shared/components/metric_tile_spec.js
@@ -0,0 +1,81 @@
+import { GlSingleStat } from '@gitlab/ui/dist/charts';
+import { shallowMount } from '@vue/test-utils';
+import MetricTile from '~/analytics/shared/components/metric_tile.vue';
+import MetricPopover from '~/analytics/shared/components/metric_popover.vue';
+import { redirectTo } from '~/lib/utils/url_utility';
+
+jest.mock('~/lib/utils/url_utility');
+
+describe('MetricTile', () => {
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ return shallowMount(MetricTile, {
+ propsData: {
+ metric: {},
+ ...props,
+ },
+ });
+ };
+
+ const findSingleStat = () => wrapper.findComponent(GlSingleStat);
+ const findPopover = () => wrapper.findComponent(MetricPopover);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('template', () => {
+ describe('links', () => {
+ it('when the metric has links, it redirects the user on click', () => {
+ const metric = {
+ identifier: 'deploys',
+ value: '10',
+ label: 'Deploys',
+ links: [{ url: 'foo/bar' }],
+ };
+ wrapper = createComponent({ metric });
+
+ const singleStat = findSingleStat();
+ singleStat.vm.$emit('click');
+ expect(redirectTo).toHaveBeenCalledWith('foo/bar');
+ });
+
+ it("when the metric doesn't have links, it won't the user on click", () => {
+ const metric = { identifier: 'deploys', value: '10', label: 'Deploys' };
+ wrapper = createComponent({ metric });
+
+ const singleStat = findSingleStat();
+ singleStat.vm.$emit('click');
+ expect(redirectTo).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('decimal places', () => {
+ it(`will render 0 decimal places for an integer value`, () => {
+ const metric = { identifier: 'deploys', value: '10', label: 'Deploys' };
+ wrapper = createComponent({ metric });
+
+ const singleStat = findSingleStat();
+ expect(singleStat.props('animationDecimalPlaces')).toBe(0);
+ });
+
+ it(`will render 1 decimal place for a float value`, () => {
+ const metric = { identifier: 'deploys', value: '10.5', label: 'Deploys' };
+ wrapper = createComponent({ metric });
+
+ const singleStat = findSingleStat();
+ expect(singleStat.props('animationDecimalPlaces')).toBe(1);
+ });
+ });
+
+ it('renders a metric popover', () => {
+ const metric = { identifier: 'deploys', value: '10', label: 'Deploys' };
+ wrapper = createComponent({ metric });
+
+ const popover = findPopover();
+ expect(popover.exists()).toBe(true);
+ expect(popover.props()).toMatchObject({ metric, target: metric.identifier });
+ });
+ });
+});
diff --git a/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js b/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js
index 28d7ebe28df..386fb4eb616 100644
--- a/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js
+++ b/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js
@@ -1,4 +1,5 @@
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { nextTick } from 'vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { stubComponent } from 'helpers/stub_component';
import { TEST_HOST } from 'helpers/test_constants';
@@ -99,9 +100,9 @@ describe('ProjectsDropdownFilter component', () => {
const findDropdownFullPathAtIndex = (index) =>
findDropdownAtIndex(index).find('[data-testid="project-full-path"]');
- const selectDropdownItemAtIndex = (index) => {
+ const selectDropdownItemAtIndex = async (index) => {
findDropdownAtIndex(index).find('button').trigger('click');
- return wrapper.vm.$nextTick();
+ await nextTick();
};
// NOTE: Selected items are now visually separated from unselected items
@@ -132,16 +133,15 @@ describe('ProjectsDropdownFilter component', () => {
expect(spyQuery).toHaveBeenCalledTimes(1);
- await wrapper.vm.$nextTick(() => {
- expect(spyQuery).toHaveBeenCalledWith({
- query: getProjects,
- variables: {
- search: 'gitlab',
- groupFullPath: wrapper.vm.groupNamespace,
- first: 50,
- includeSubgroups: true,
- },
- });
+ await nextTick();
+ expect(spyQuery).toHaveBeenCalledWith({
+ query: getProjects,
+ variables: {
+ search: 'gitlab',
+ groupFullPath: wrapper.vm.groupNamespace,
+ first: 50,
+ includeSubgroups: true,
+ },
});
});
});
@@ -193,7 +193,7 @@ describe('ProjectsDropdownFilter component', () => {
expect(wrapper.text()).toContain('2 projects selected');
findClearAllButton().trigger('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.text()).not.toContain('2 projects selected');
expect(wrapper.text()).toContain('Select projects');
@@ -366,9 +366,8 @@ describe('ProjectsDropdownFilter component', () => {
selectDropdownItemAtIndex(0);
selectDropdownItemAtIndex(1);
- await wrapper.vm.$nextTick().then(() => {
- expect(findDropdownButton().text()).toBe('2 projects selected');
- });
+ await nextTick();
+ expect(findDropdownButton().text()).toBe('2 projects selected');
});
});
});
diff --git a/spec/frontend/analytics/shared/utils_spec.js b/spec/frontend/analytics/shared/utils_spec.js
index 0513ccb2890..b48e2d971b5 100644
--- a/spec/frontend/analytics/shared/utils_spec.js
+++ b/spec/frontend/analytics/shared/utils_spec.js
@@ -1,9 +1,12 @@
+import metricsData from 'test_fixtures/projects/analytics/value_stream_analytics/summary.json';
import {
filterBySearchTerm,
extractFilterQueryParameters,
extractPaginationQueryParameters,
getDataZoomOption,
+ prepareTimeMetricsData,
} from '~/analytics/shared/utils';
+import { slugify } from '~/lib/utils/text_utility';
import { objectToQuery } from '~/lib/utils/url_utility';
describe('filterBySearchTerm', () => {
@@ -176,3 +179,36 @@ describe('getDataZoomOption', () => {
});
});
});
+
+describe('prepareTimeMetricsData', () => {
+ let prepared;
+ const [first, second] = metricsData;
+ delete second.identifier; // testing the case when identifier is missing
+
+ const firstIdentifier = first.identifier;
+ const secondIdentifier = slugify(second.title);
+
+ beforeEach(() => {
+ prepared = prepareTimeMetricsData([first, second], {
+ [firstIdentifier]: { description: 'Is a value that is good' },
+ });
+ });
+
+ it('will add a `identifier` based on the title', () => {
+ expect(prepared).toMatchObject([
+ { identifier: firstIdentifier },
+ { identifier: secondIdentifier },
+ ]);
+ });
+
+ it('will add a `label` key', () => {
+ expect(prepared).toMatchObject([{ label: 'New Issues' }, { label: 'Commits' }]);
+ });
+
+ it('will add a popover description using the key if it is provided', () => {
+ expect(prepared).toMatchObject([
+ { description: 'Is a value that is good' },
+ { description: '' },
+ ]);
+ });
+});
diff --git a/spec/frontend/analytics/usage_trends/apollo_mock_data.js b/spec/frontend/analytics/usage_trends/apollo_mock_data.js
index 98eabd577ee..934bbc63689 100644
--- a/spec/frontend/analytics/usage_trends/apollo_mock_data.js
+++ b/spec/frontend/analytics/usage_trends/apollo_mock_data.js
@@ -1,4 +1,5 @@
const defaultPageInfo = {
+ __typename: 'PageInfo',
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
diff --git a/spec/frontend/analytics/usage_trends/components/usage_trends_count_chart_spec.js b/spec/frontend/analytics/usage_trends/components/usage_trends_count_chart_spec.js
index 1a331100bb8..02cf7f42a0b 100644
--- a/spec/frontend/analytics/usage_trends/components/usage_trends_count_chart_spec.js
+++ b/spec/frontend/analytics/usage_trends/components/usage_trends_count_chart_spec.js
@@ -1,9 +1,10 @@
import { GlAlert } from '@gitlab/ui';
import { GlLineChart } from '@gitlab/ui/dist/charts';
import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import UsageTrendsCountChart from '~/analytics/usage_trends/components/usage_trends_count_chart.vue';
import statsQuery from '~/analytics/usage_trends/graphql/queries/usage_count.query.graphql';
import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue';
@@ -77,9 +78,10 @@ describe('UsageTrendsCountChart', () => {
});
describe('without data', () => {
- beforeEach(() => {
+ beforeEach(async () => {
queryHandler = mockQueryResponse({ key: queryResponseDataKey, data: [] });
wrapper = createComponent({ responseHandler: queryHandler });
+ await waitForPromises();
});
it('renders an no data message', () => {
@@ -96,9 +98,10 @@ describe('UsageTrendsCountChart', () => {
});
describe('with data', () => {
- beforeEach(() => {
+ beforeEach(async () => {
queryHandler = mockQueryResponse({ key: queryResponseDataKey, data: mockCountsData1 });
wrapper = createComponent({ responseHandler: queryHandler });
+ await waitForPromises();
});
it('requests data', () => {
@@ -126,7 +129,7 @@ describe('UsageTrendsCountChart', () => {
const recordedAt = '2020-08-01';
describe('when the fetchMore query returns data', () => {
beforeEach(async () => {
- const newData = [{ recordedAt, count: 5 }];
+ const newData = [{ __typename: 'UsageTrendsMeasurement', recordedAt, count: 5 }];
queryHandler = mockQueryResponse({
key: queryResponseDataKey,
data: mockCountsData1,
@@ -134,7 +137,7 @@ describe('UsageTrendsCountChart', () => {
});
wrapper = createComponent({ responseHandler: queryHandler });
- await wrapper.vm.$nextTick();
+ await waitForPromises();
});
it('requests data twice', () => {
@@ -161,7 +164,7 @@ describe('UsageTrendsCountChart', () => {
.spyOn(wrapper.vm.$apollo.queries[identifier], 'fetchMore')
.mockImplementation(jest.fn().mockRejectedValue());
- await wrapper.vm.$nextTick();
+ await nextTick();
});
it('calls fetchMore', () => {
diff --git a/spec/frontend/analytics/usage_trends/components/users_chart_spec.js b/spec/frontend/analytics/usage_trends/components/users_chart_spec.js
index 04ea25a02d5..32a664a5026 100644
--- a/spec/frontend/analytics/usage_trends/components/users_chart_spec.js
+++ b/spec/frontend/analytics/usage_trends/components/users_chart_spec.js
@@ -1,9 +1,10 @@
import { GlAlert } from '@gitlab/ui';
import { GlAreaChart } from '@gitlab/ui/dist/charts';
import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import UsersChart from '~/analytics/usage_trends/components/users_chart.vue';
import usersQuery from '~/analytics/usage_trends/graphql/queries/users.query.graphql';
import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue';
@@ -67,7 +68,7 @@ describe('UsersChart', () => {
describe('without data', () => {
beforeEach(async () => {
wrapper = createComponent({ users: [] });
- await wrapper.vm.$nextTick();
+ await nextTick();
});
it('renders an no data message', () => {
@@ -86,7 +87,7 @@ describe('UsersChart', () => {
describe('with data', () => {
beforeEach(async () => {
wrapper = createComponent({ users: mockCountsData2 });
- await wrapper.vm.$nextTick();
+ await waitForPromises();
});
it('hides the skeleton loader', () => {
@@ -107,7 +108,7 @@ describe('UsersChart', () => {
describe('with errors', () => {
beforeEach(async () => {
wrapper = createComponent({ loadingError: true });
- await wrapper.vm.$nextTick();
+ await nextTick();
});
it('renders an error message', () => {
@@ -134,7 +135,7 @@ describe('UsersChart', () => {
});
jest.spyOn(wrapper.vm.$apollo.queries.users, 'fetchMore');
- await wrapper.vm.$nextTick();
+ await nextTick();
});
it('requests data twice', () => {
@@ -147,7 +148,7 @@ describe('UsersChart', () => {
});
describe('when the fetchMore query throws an error', () => {
- beforeEach(() => {
+ beforeEach(async () => {
wrapper = createComponent({
users: mockCountsData2,
additionalData: mockCountsData1,
@@ -156,7 +157,7 @@ describe('UsersChart', () => {
jest
.spyOn(wrapper.vm.$apollo.queries.users, 'fetchMore')
.mockImplementation(jest.fn().mockRejectedValue());
- return wrapper.vm.$nextTick();
+ await waitForPromises();
});
it('calls fetchMore', () => {
diff --git a/spec/frontend/analytics/usage_trends/mock_data.js b/spec/frontend/analytics/usage_trends/mock_data.js
index d96dfa26209..77bd44d17f5 100644
--- a/spec/frontend/analytics/usage_trends/mock_data.js
+++ b/spec/frontend/analytics/usage_trends/mock_data.js
@@ -4,11 +4,11 @@ export const mockUsageCounts = [
];
export const mockCountsData1 = [
- { recordedAt: '2020-07-23', count: 52 },
- { recordedAt: '2020-07-22', count: 40 },
- { recordedAt: '2020-07-21', count: 31 },
- { recordedAt: '2020-06-14', count: 23 },
- { recordedAt: '2020-06-12', count: 20 },
+ { __typename: 'UsageTrendsMeasurement', recordedAt: '2020-07-23', count: 52 },
+ { __typename: 'UsageTrendsMeasurement', recordedAt: '2020-07-22', count: 40 },
+ { __typename: 'UsageTrendsMeasurement', recordedAt: '2020-07-21', count: 31 },
+ { __typename: 'UsageTrendsMeasurement', recordedAt: '2020-06-14', count: 23 },
+ { __typename: 'UsageTrendsMeasurement', recordedAt: '2020-06-12', count: 20 },
];
export const countsMonthlyChartData1 = [
@@ -17,11 +17,11 @@ export const countsMonthlyChartData1 = [
];
export const mockCountsData2 = [
- { recordedAt: '2020-07-28', count: 10 },
- { recordedAt: '2020-07-27', count: 9 },
- { recordedAt: '2020-06-26', count: 14 },
- { recordedAt: '2020-06-25', count: 23 },
- { recordedAt: '2020-06-24', count: 25 },
+ { __typename: 'UsageTrendsMeasurement', recordedAt: '2020-07-28', count: 10 },
+ { __typename: 'UsageTrendsMeasurement', recordedAt: '2020-07-27', count: 9 },
+ { __typename: 'UsageTrendsMeasurement', recordedAt: '2020-06-26', count: 14 },
+ { __typename: 'UsageTrendsMeasurement', recordedAt: '2020-06-25', count: 23 },
+ { __typename: 'UsageTrendsMeasurement', recordedAt: '2020-06-24', count: 25 },
];
export const countsMonthlyChartData2 = [
diff --git a/spec/frontend/artifacts_settings/components/keep_latest_artifact_checkbox_spec.js b/spec/frontend/artifacts_settings/components/keep_latest_artifact_checkbox_spec.js
index bfa8274f0eb..2f3ff2b22f2 100644
--- a/spec/frontend/artifacts_settings/components/keep_latest_artifact_checkbox_spec.js
+++ b/spec/frontend/artifacts_settings/components/keep_latest_artifact_checkbox_spec.js
@@ -1,5 +1,6 @@
import { GlFormCheckbox, GlLink } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import UpdateKeepLatestArtifactProjectSetting from '~/artifacts_settings/graphql/mutations/update_keep_latest_artifact_project_setting.mutation.graphql';
@@ -7,8 +8,7 @@ import GetKeepLatestArtifactApplicationSetting from '~/artifacts_settings/graphq
import GetKeepLatestArtifactProjectSetting from '~/artifacts_settings/graphql/queries/get_keep_latest_artifact_project_setting.query.graphql';
import KeepLatestArtifactCheckbox from '~/artifacts_settings/keep_latest_artifact_checkbox.vue';
-const localVue = createLocalVue();
-localVue.use(VueApollo);
+Vue.use(VueApollo);
const keepLatestArtifactProjectMock = {
data: {
@@ -73,7 +73,6 @@ describe('Keep latest artifact checkbox', () => {
stubs: {
GlFormCheckbox,
},
- localVue,
apolloProvider,
});
};
@@ -110,13 +109,13 @@ describe('Keep latest artifact checkbox', () => {
});
it('sets correct setting value in checkbox with query result', async () => {
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.element).toMatchSnapshot();
});
it('checkbox is enabled when application setting is enabled', async () => {
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findCheckbox().attributes('disabled')).toBeUndefined();
});
diff --git a/spec/frontend/authentication/webauthn/util_spec.js b/spec/frontend/authentication/webauthn/util_spec.js
new file mode 100644
index 00000000000..c9b8bfd8679
--- /dev/null
+++ b/spec/frontend/authentication/webauthn/util_spec.js
@@ -0,0 +1,19 @@
+import { base64ToBuffer, bufferToBase64 } from '~/authentication/webauthn/util';
+
+const encodedString = 'SGVsbG8gd29ybGQh';
+const stringBytes = [72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33];
+
+describe('Webauthn utils', () => {
+ it('base64ToBuffer', () => {
+ const toArray = (val) => new Uint8Array(val);
+
+ expect(base64ToBuffer(encodedString)).toBeInstanceOf(ArrayBuffer);
+
+ expect(toArray(base64ToBuffer(encodedString))).toEqual(toArray(stringBytes));
+ });
+
+ it('bufferToBase64', () => {
+ const buffer = base64ToBuffer(encodedString);
+ expect(bufferToBase64(buffer)).toBe(encodedString);
+ });
+});
diff --git a/spec/frontend/badges/components/badge_form_spec.js b/spec/frontend/badges/components/badge_form_spec.js
index e375fcb4705..ba2ec775b61 100644
--- a/spec/frontend/badges/components/badge_form_spec.js
+++ b/spec/frontend/badges/components/badge_form_spec.js
@@ -1,5 +1,5 @@
import MockAdapter from 'axios-mock-adapter';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import { DUMMY_IMAGE_URL, TEST_HOST } from 'helpers/test_constants';
import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
import BadgeForm from '~/badges/components/badge_form.vue';
@@ -74,7 +74,7 @@ describe('BadgeForm component', () => {
expect(feedbackElement).toBeVisible();
};
- beforeEach((done) => {
+ beforeEach(async () => {
jest.spyOn(vm, submitAction).mockReturnValue(Promise.resolve());
store.replaceState({
...store.state,
@@ -83,14 +83,10 @@ describe('BadgeForm component', () => {
isSaving: false,
});
- Vue.nextTick()
- .then(() => {
- setValue(nameSelector, 'TestBadge');
- setValue(linkUrlSelector, `${TEST_HOST}/link/url`);
- setValue(imageUrlSelector, `${window.location.origin}${DUMMY_IMAGE_URL}`);
- })
- .then(done)
- .catch(done.fail);
+ await nextTick();
+ setValue(nameSelector, 'TestBadge');
+ setValue(linkUrlSelector, `${TEST_HOST}/link/url`);
+ setValue(imageUrlSelector, `${window.location.origin}${DUMMY_IMAGE_URL}`);
});
it('returns immediately if imageUrl is empty', () => {
diff --git a/spec/frontend/badges/components/badge_list_row_spec.js b/spec/frontend/badges/components/badge_list_row_spec.js
index 372663017e2..0fb0fa86a02 100644
--- a/spec/frontend/badges/components/badge_list_row_spec.js
+++ b/spec/frontend/badges/components/badge_list_row_spec.js
@@ -1,4 +1,4 @@
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
import BadgeListRow from '~/badges/components/badge_list_row.vue';
import { GROUP_BADGE, PROJECT_BADGE } from '~/badges/constants';
@@ -73,25 +73,21 @@ describe('BadgeListRow component', () => {
expect(vm.editBadge).toHaveBeenCalled();
});
- it('calls updateBadgeInModal and shows modal when clicking then delete button', (done) => {
+ it('calls updateBadgeInModal and shows modal when clicking then delete button', async () => {
jest.spyOn(vm, 'updateBadgeInModal').mockImplementation(() => {});
const deleteButton = vm.$el.querySelector('.table-button-footer button:last-of-type');
deleteButton.click();
- Vue.nextTick()
- .then(() => {
- expect(vm.updateBadgeInModal).toHaveBeenCalled();
- })
- .then(done)
- .catch(done.fail);
+ await nextTick();
+ expect(vm.updateBadgeInModal).toHaveBeenCalled();
});
describe('for a group badge', () => {
- beforeEach((done) => {
+ beforeEach(async () => {
badge.kind = GROUP_BADGE;
- Vue.nextTick().then(done).catch(done.fail);
+ await nextTick();
});
it('renders the badge kind', () => {
diff --git a/spec/frontend/badges/components/badge_list_spec.js b/spec/frontend/badges/components/badge_list_spec.js
index 6cc90c6de46..39fa502b207 100644
--- a/spec/frontend/badges/components/badge_list_spec.js
+++ b/spec/frontend/badges/components/badge_list_spec.js
@@ -1,4 +1,4 @@
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
import BadgeList from '~/badges/components/badge_list.vue';
import { GROUP_BADGE, PROJECT_BADGE } from '~/badges/constants';
@@ -48,46 +48,34 @@ describe('BadgeList component', () => {
expect(rows).toHaveLength(numberOfDummyBadges);
});
- it('renders a message if no badges exist', (done) => {
+ it('renders a message if no badges exist', async () => {
store.state.badges = [];
- Vue.nextTick()
- .then(() => {
- expect(vm.$el.innerText).toMatch('This project has no badges');
- })
- .then(done)
- .catch(done.fail);
+ await nextTick();
+ expect(vm.$el.innerText).toMatch('This project has no badges');
});
- it('shows a loading icon when loading', (done) => {
+ it('shows a loading icon when loading', async () => {
store.state.isLoading = true;
- Vue.nextTick()
- .then(() => {
- const loadingIcon = vm.$el.querySelector('.gl-spinner');
+ await nextTick();
+ const loadingIcon = vm.$el.querySelector('.gl-spinner');
- expect(loadingIcon).toBeVisible();
- })
- .then(done)
- .catch(done.fail);
+ expect(loadingIcon).toBeVisible();
});
describe('for group badges', () => {
- beforeEach((done) => {
+ beforeEach(async () => {
store.state.kind = GROUP_BADGE;
- Vue.nextTick().then(done).catch(done.fail);
+ await nextTick();
});
- it('renders a message if no badges exist', (done) => {
+ it('renders a message if no badges exist', async () => {
store.state.badges = [];
- Vue.nextTick()
- .then(() => {
- expect(vm.$el.innerText).toMatch('This group has no badges');
- })
- .then(done)
- .catch(done.fail);
+ await nextTick();
+ expect(vm.$el.innerText).toMatch('This group has no badges');
});
});
});
diff --git a/spec/frontend/badges/components/badge_settings_spec.js b/spec/frontend/badges/components/badge_settings_spec.js
index 0c29379763e..79cf5f3e4ff 100644
--- a/spec/frontend/badges/components/badge_settings_spec.js
+++ b/spec/frontend/badges/components/badge_settings_spec.js
@@ -1,5 +1,6 @@
import { GlModal } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import BadgeList from '~/badges/components/badge_list.vue';
import BadgeListRow from '~/badges/components/badge_list_row.vue';
@@ -7,8 +8,7 @@ import BadgeSettings from '~/badges/components/badge_settings.vue';
import store from '~/badges/store';
import { createDummyBadge } from '../dummy_badge';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('BadgeSettings component', () => {
let wrapper;
@@ -21,7 +21,6 @@ describe('BadgeSettings component', () => {
wrapper = shallowMount(BadgeSettings, {
store,
- localVue,
stubs: {
'badge-list': BadgeList,
'badge-list-row': BadgeListRow,
@@ -41,7 +40,7 @@ describe('BadgeSettings component', () => {
const button = wrapper.find('[data-testid="delete-badge"]');
button.vm.$emit('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
const modal = wrapper.find(GlModal);
expect(modal.isVisible()).toBe(true);
diff --git a/spec/frontend/badges/components/badge_spec.js b/spec/frontend/badges/components/badge_spec.js
index 990bc094d59..2310fb8bd8e 100644
--- a/spec/frontend/badges/components/badge_spec.js
+++ b/spec/frontend/badges/components/badge_spec.js
@@ -1,4 +1,4 @@
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import { DUMMY_IMAGE_URL, TEST_HOST } from 'spec/test_constants';
import Badge from '~/badges/components/badge.vue';
@@ -27,7 +27,7 @@ describe('Badge component', () => {
badgeImage.addEventListener('load', resolve);
// Manually dispatch load event as it is not triggered
badgeImage.dispatchEvent(new Event('load'));
- }).then(() => Vue.nextTick());
+ }).then(() => nextTick());
};
afterEach(() => {
@@ -36,34 +36,25 @@ describe('Badge component', () => {
describe('watchers', () => {
describe('imageUrl', () => {
- it('sets isLoading and resets numRetries and hasError', (done) => {
+ it('sets isLoading and resets numRetries and hasError', async () => {
const props = { ...dummyProps };
- createComponent(props)
- .then(() => {
- expect(vm.isLoading).toBe(false);
- vm.hasError = true;
- vm.numRetries = 42;
-
- vm.imageUrl = `${props.imageUrl}#something/else`;
-
- return Vue.nextTick();
- })
- .then(() => {
- expect(vm.isLoading).toBe(true);
- expect(vm.numRetries).toBe(0);
- expect(vm.hasError).toBe(false);
- })
- .then(done)
- .catch(done.fail);
+ await createComponent(props);
+ expect(vm.isLoading).toBe(false);
+ vm.hasError = true;
+ vm.numRetries = 42;
+
+ vm.imageUrl = `${props.imageUrl}#something/else`;
+ await nextTick();
+ expect(vm.isLoading).toBe(true);
+ expect(vm.numRetries).toBe(0);
+ expect(vm.hasError).toBe(false);
});
});
});
describe('methods', () => {
- beforeEach((done) => {
- createComponent({ ...dummyProps })
- .then(done)
- .catch(done.fail);
+ beforeEach(async () => {
+ await createComponent({ ...dummyProps });
});
it('onError resets isLoading and sets hasError', () => {
@@ -116,37 +107,29 @@ describe('Badge component', () => {
expect(vm.$el.querySelector('.btn-group')).toBeHidden();
});
- it('shows a loading icon when loading', (done) => {
+ it('shows a loading icon when loading', async () => {
vm.isLoading = true;
- Vue.nextTick()
- .then(() => {
- const { badgeImage, loadingIcon, reloadButton } = findElements();
+ await nextTick();
+ const { badgeImage, loadingIcon, reloadButton } = findElements();
- expect(badgeImage).toBeHidden();
- expect(loadingIcon).toBeVisible();
- expect(reloadButton).toBeHidden();
- expect(vm.$el.querySelector('.btn-group')).toBeHidden();
- })
- .then(done)
- .catch(done.fail);
+ expect(badgeImage).toBeHidden();
+ expect(loadingIcon).toBeVisible();
+ expect(reloadButton).toBeHidden();
+ expect(vm.$el.querySelector('.btn-group')).toBeHidden();
});
- it('shows an error and reload button if loading failed', (done) => {
+ it('shows an error and reload button if loading failed', async () => {
vm.hasError = true;
- Vue.nextTick()
- .then(() => {
- const { badgeImage, loadingIcon, reloadButton } = findElements();
+ await nextTick();
+ const { badgeImage, loadingIcon, reloadButton } = findElements();
- expect(badgeImage).toBeHidden();
- expect(loadingIcon).toBeHidden();
- expect(reloadButton).toBeVisible();
- expect(reloadButton).toHaveSpriteIcon('retry');
- expect(vm.$el.innerText.trim()).toBe('No badge image');
- })
- .then(done)
- .catch(done.fail);
+ expect(badgeImage).toBeHidden();
+ expect(loadingIcon).toBeHidden();
+ expect(reloadButton).toBeVisible();
+ expect(reloadButton).toHaveSpriteIcon('retry');
+ expect(vm.$el.innerText.trim()).toBe('No badge image');
});
});
});
diff --git a/spec/frontend/batch_comments/components/diff_file_drafts_spec.js b/spec/frontend/batch_comments/components/diff_file_drafts_spec.js
index dcb68b1804f..6a5ff1af7c9 100644
--- a/spec/frontend/batch_comments/components/diff_file_drafts_spec.js
+++ b/spec/frontend/batch_comments/components/diff_file_drafts_spec.js
@@ -1,11 +1,11 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import DiffFileDrafts from '~/batch_comments/components/diff_file_drafts.vue';
import DraftNote from '~/batch_comments/components/draft_note.vue';
+import DesignNotePin from '~/vue_shared/components/design_management/design_note_pin.vue';
-const localVue = createLocalVue();
-
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('Batch comments diff file drafts component', () => {
let vm;
@@ -22,9 +22,8 @@ describe('Batch comments diff file drafts component', () => {
},
});
- vm = shallowMount(localVue.extend(DiffFileDrafts), {
+ vm = shallowMount(DiffFileDrafts, {
store,
- localVue,
propsData: { fileHash: 'filehash' },
});
}
@@ -42,10 +41,12 @@ describe('Batch comments diff file drafts component', () => {
it('renders index of draft note', () => {
factory();
- expect(vm.findAll('.js-diff-notes-index').length).toEqual(2);
+ const elements = vm.findAll(DesignNotePin);
+
+ expect(elements.length).toEqual(2);
- expect(vm.findAll('.js-diff-notes-index').at(0).text()).toEqual('1');
+ expect(elements.at(0).props('label')).toEqual(1);
- expect(vm.findAll('.js-diff-notes-index').at(1).text()).toEqual('2');
+ expect(elements.at(1).props('label')).toEqual(2);
});
});
diff --git a/spec/frontend/batch_comments/components/draft_note_spec.js b/spec/frontend/batch_comments/components/draft_note_spec.js
index 5d22823e974..6a997ebaaa8 100644
--- a/spec/frontend/batch_comments/components/draft_note_spec.js
+++ b/spec/frontend/batch_comments/components/draft_note_spec.js
@@ -1,14 +1,15 @@
+import { nextTick } from 'vue';
+import { GlButton, GlBadge } from '@gitlab/ui';
import { getByRole } from '@testing-library/dom';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
import { stubComponent } from 'helpers/stub_component';
import DraftNote from '~/batch_comments/components/draft_note.vue';
+import PublishButton from '~/batch_comments/components/publish_button.vue';
import { createStore } from '~/batch_comments/stores';
import NoteableNote from '~/notes/components/noteable_note.vue';
import '~/behaviors/markdown/render_gfm';
import { createDraft } from '../mock_data';
-const localVue = createLocalVue();
-
const NoteableNoteStub = stubComponent(NoteableNote, {
template: `
<div>
@@ -29,12 +30,13 @@ describe('Batch comments draft note component', () => {
};
const getList = () => getByRole(wrapper.element, 'list');
+ const findSubmitReviewButton = () => wrapper.findComponent(PublishButton);
+ const findAddCommentButton = () => wrapper.findComponent(GlButton);
const createComponent = (propsData = { draft }) => {
- wrapper = shallowMount(localVue.extend(DraftNote), {
+ wrapper = shallowMount(DraftNote, {
store,
propsData,
- localVue,
stubs: {
NoteableNote: NoteableNoteStub,
},
@@ -54,7 +56,7 @@ describe('Batch comments draft note component', () => {
it('renders template', () => {
createComponent();
- expect(wrapper.find('.draft-pending-label').exists()).toBe(true);
+ expect(wrapper.findComponent(GlBadge).exists()).toBe(true);
const note = wrapper.find(NoteableNote);
@@ -65,7 +67,7 @@ describe('Batch comments draft note component', () => {
describe('add comment now', () => {
it('dispatches publishSingleDraft when clicking', () => {
createComponent();
- const publishNowButton = wrapper.find({ ref: 'publishNowButton' });
+ const publishNowButton = findAddCommentButton();
publishNowButton.vm.$emit('click');
expect(wrapper.vm.$store.dispatch).toHaveBeenCalledWith(
@@ -74,45 +76,60 @@ describe('Batch comments draft note component', () => {
);
});
- it('sets as loading when draft is publishing', (done) => {
+ it('sets as loading when draft is publishing', async () => {
createComponent();
wrapper.vm.$store.state.batchComments.currentlyPublishingDrafts.push(1);
- wrapper.vm.$nextTick(() => {
- const publishNowButton = wrapper.find({ ref: 'publishNowButton' });
+ await nextTick();
+ const publishNowButton = findAddCommentButton();
- expect(publishNowButton.props().loading).toBe(true);
+ expect(publishNowButton.props().loading).toBe(true);
+ });
- done();
- });
+ it('sets as disabled when review is publishing', async () => {
+ createComponent();
+ wrapper.vm.$store.state.batchComments.isPublishing = true;
+
+ await nextTick();
+ const publishNowButton = findAddCommentButton();
+
+ expect(publishNowButton.props().disabled).toBe(true);
+ expect(publishNowButton.props().loading).toBe(false);
+ });
+ });
+
+ describe('submit review', () => {
+ it('sets as disabled when draft is publishing', async () => {
+ createComponent();
+ wrapper.vm.$store.state.batchComments.currentlyPublishingDrafts.push(1);
+
+ await nextTick();
+ const publishNowButton = findSubmitReviewButton();
+
+ expect(publishNowButton.attributes().disabled).toBeTruthy();
});
});
describe('update', () => {
- it('dispatches updateDraft', (done) => {
+ it('dispatches updateDraft', async () => {
createComponent();
const note = wrapper.find(NoteableNote);
note.vm.$emit('handleEdit');
- wrapper.vm
- .$nextTick()
- .then(() => {
- const formData = {
- note: draft,
- noteText: 'a',
- resolveDiscussion: false,
- };
-
- note.vm.$emit('handleUpdateNote', formData);
-
- expect(wrapper.vm.$store.dispatch).toHaveBeenCalledWith(
- 'batchComments/updateDraft',
- formData,
- );
- })
- .then(done)
- .catch(done.fail);
+ await nextTick();
+ const formData = {
+ note: draft,
+ noteText: 'a',
+ resolveDiscussion: false,
+ };
+
+ note.vm.$emit('handleUpdateNote', formData);
+
+ expect(wrapper.vm.$store.dispatch).toHaveBeenCalledWith(
+ 'batchComments/updateDraft',
+ formData,
+ );
});
});
@@ -130,7 +147,7 @@ describe('Batch comments draft note component', () => {
});
describe('quick actions', () => {
- it('renders referenced commands', (done) => {
+ it('renders referenced commands', async () => {
createComponent();
wrapper.setProps({
draft: {
@@ -141,14 +158,11 @@ describe('Batch comments draft note component', () => {
},
});
- wrapper.vm.$nextTick(() => {
- const referencedCommands = wrapper.find('.referenced-commands');
+ await nextTick();
+ const referencedCommands = wrapper.find('.referenced-commands');
- expect(referencedCommands.exists()).toBe(true);
- expect(referencedCommands.text()).toContain('test command');
-
- done();
- });
+ expect(referencedCommands.exists()).toBe(true);
+ expect(referencedCommands.text()).toContain('test command');
});
});
diff --git a/spec/frontend/batch_comments/components/drafts_count_spec.js b/spec/frontend/batch_comments/components/drafts_count_spec.js
index 5f74de9c014..390ef21929c 100644
--- a/spec/frontend/batch_comments/components/drafts_count_spec.js
+++ b/spec/frontend/batch_comments/components/drafts_count_spec.js
@@ -1,4 +1,4 @@
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
import DraftsCount from '~/batch_comments/components/drafts_count.vue';
import { createStore } from '~/batch_comments/stores';
@@ -27,17 +27,14 @@ describe('Batch comments drafts count component', () => {
expect(vm.$el.textContent).toContain('1');
});
- it('renders screen reader text', (done) => {
+ it('renders screen reader text', async () => {
const el = vm.$el.querySelector('.sr-only');
expect(el.textContent).toContain('draft');
vm.$store.state.batchComments.drafts.push('comment 2');
- vm.$nextTick(() => {
- expect(el.textContent).toContain('drafts');
-
- done();
- });
+ await nextTick();
+ expect(el.textContent).toContain('drafts');
});
});
diff --git a/spec/frontend/batch_comments/components/preview_dropdown_spec.js b/spec/frontend/batch_comments/components/preview_dropdown_spec.js
index 5327879f003..bf3bbf4de26 100644
--- a/spec/frontend/batch_comments/components/preview_dropdown_spec.js
+++ b/spec/frontend/batch_comments/components/preview_dropdown_spec.js
@@ -1,4 +1,4 @@
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import PreviewDropdown from '~/batch_comments/components/preview_dropdown.vue';
@@ -49,7 +49,7 @@ describe('Batch comments preview dropdown', () => {
wrapper.findByTestId('preview-item').vm.$emit('click');
- await Vue.nextTick();
+ await nextTick();
expect(setCurrentFileHash).toHaveBeenCalledWith(expect.anything(), 'hash');
expect(scrollToDraft).toHaveBeenCalledWith(expect.anything(), { id: 1, file_hash: 'hash' });
@@ -63,7 +63,7 @@ describe('Batch comments preview dropdown', () => {
wrapper.findByTestId('preview-item').vm.$emit('click');
- await Vue.nextTick();
+ await nextTick();
expect(scrollToDraft).toHaveBeenCalledWith(expect.anything(), { id: 1 });
});
diff --git a/spec/frontend/batch_comments/components/publish_button_spec.js b/spec/frontend/batch_comments/components/publish_button_spec.js
index eca424814b4..9a782ec09b6 100644
--- a/spec/frontend/batch_comments/components/publish_button_spec.js
+++ b/spec/frontend/batch_comments/components/publish_button_spec.js
@@ -1,4 +1,4 @@
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
import PublishButton from '~/batch_comments/components/publish_button.vue';
import { createStore } from '~/batch_comments/stores';
@@ -29,13 +29,10 @@ describe('Batch comments publish button component', () => {
expect(vm.$store.dispatch).toHaveBeenCalledWith('batchComments/publishReview', undefined);
});
- it('sets loading when isPublishing is true', (done) => {
+ it('sets loading when isPublishing is true', async () => {
vm.$store.state.batchComments.isPublishing = true;
- vm.$nextTick(() => {
- expect(vm.$el.getAttribute('disabled')).toBe('disabled');
-
- done();
- });
+ await nextTick();
+ expect(vm.$el.getAttribute('disabled')).toBe('disabled');
});
});
diff --git a/spec/frontend/batch_comments/components/publish_dropdown_spec.js b/spec/frontend/batch_comments/components/publish_dropdown_spec.js
index bd8091c20e0..a3168931f1f 100644
--- a/spec/frontend/batch_comments/components/publish_dropdown_spec.js
+++ b/spec/frontend/batch_comments/components/publish_dropdown_spec.js
@@ -1,13 +1,13 @@
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import PreviewDropdown from '~/batch_comments/components/preview_dropdown.vue';
import { createStore } from '~/mr_notes/stores';
import '~/behaviors/markdown/render_gfm';
import { createDraft } from '../mock_data';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('Batch comments publish dropdown component', () => {
let wrapper;
diff --git a/spec/frontend/behaviors/copy_as_gfm_spec.js b/spec/frontend/behaviors/copy_as_gfm_spec.js
index 557b609f5f9..c96db09cc76 100644
--- a/spec/frontend/behaviors/copy_as_gfm_spec.js
+++ b/spec/frontend/behaviors/copy_as_gfm_spec.js
@@ -1,3 +1,4 @@
+import waitForPromises from 'helpers/wait_for_promises';
import initCopyAsGFM, { CopyAsGFM } from '~/behaviors/markdown/copy_as_gfm';
describe('CopyAsGFM', () => {
@@ -81,49 +82,40 @@ describe('CopyAsGFM', () => {
stopPropagation() {},
};
CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformGFMSelection);
- return clipboardData;
+
+ return waitForPromises();
};
- beforeAll((done) => {
+ beforeAll(() => {
initCopyAsGFM();
// Fake call to nodeToGfm so the import of lazy bundle happened
- CopyAsGFM.nodeToGFM(document.createElement('div'))
- .then(() => {
- done();
- })
- .catch(done.fail);
+ return CopyAsGFM.nodeToGFM(document.createElement('div'));
});
beforeEach(() => jest.spyOn(clipboardData, 'setData'));
describe('list handling', () => {
- it('uses correct gfm for unordered lists', (done) => {
+ it('uses correct gfm for unordered lists', async () => {
const selection = stubSelection('<li>List Item1</li><li>List Item2</li>\n', 'UL');
window.getSelection = jest.fn(() => selection);
- simulateCopy();
+ await simulateCopy();
- setImmediate(() => {
- const expectedGFM = '* List Item1\n* List Item2';
+ const expectedGFM = '* List Item1\n* List Item2';
- expect(clipboardData.setData).toHaveBeenCalledWith('text/x-gfm', expectedGFM);
- done();
- });
+ expect(clipboardData.setData).toHaveBeenCalledWith('text/x-gfm', expectedGFM);
});
- it('uses correct gfm for ordered lists', (done) => {
+ it('uses correct gfm for ordered lists', async () => {
const selection = stubSelection('<li>List Item1</li><li>List Item2</li>\n', 'OL');
window.getSelection = jest.fn(() => selection);
- simulateCopy();
+ await simulateCopy();
- setImmediate(() => {
- const expectedGFM = '1. List Item1\n1. List Item2';
+ const expectedGFM = '1. List Item1\n1. List Item2';
- expect(clipboardData.setData).toHaveBeenCalledWith('text/x-gfm', expectedGFM);
- done();
- });
+ expect(clipboardData.setData).toHaveBeenCalledWith('text/x-gfm', expectedGFM);
});
});
});
@@ -131,10 +123,9 @@ describe('CopyAsGFM', () => {
describe('CopyAsGFM.quoted', () => {
const sampleGFM = '* List 1\n* List 2\n\n`Some code`';
- it('adds quote char `> ` to each line', (done) => {
+ it('adds quote char `> ` to each line', () => {
const expectedQuotedGFM = '> * List 1\n> * List 2\n> \n> `Some code`';
expect(CopyAsGFM.quoted(sampleGFM)).toEqual(expectedQuotedGFM);
- done();
});
});
});
diff --git a/spec/frontend/behaviors/shortcuts/shortcuts_issuable_spec.js b/spec/frontend/behaviors/shortcuts/shortcuts_issuable_spec.js
index bb3b16b4c7a..e1811247124 100644
--- a/spec/frontend/behaviors/shortcuts/shortcuts_issuable_spec.js
+++ b/spec/frontend/behaviors/shortcuts/shortcuts_issuable_spec.js
@@ -1,5 +1,6 @@
import $ from 'jquery';
import Mousetrap from 'mousetrap';
+import waitForPromises from 'helpers/wait_for_promises';
import initCopyAsGFM, { CopyAsGFM } from '~/behaviors/markdown/copy_as_gfm';
import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import { getSelectedFragment } from '~/lib/utils/common_utils';
@@ -13,15 +14,11 @@ describe('ShortcutsIssuable', () => {
const snippetShowFixtureName = 'snippets/show.html';
const mrShowFixtureName = 'merge_requests/merge_request_of_current_user.html';
- beforeAll((done) => {
+ beforeAll(() => {
initCopyAsGFM();
// Fake call to nodeToGfm so the import of lazy bundle happened
- CopyAsGFM.nodeToGFM(document.createElement('div'))
- .then(() => {
- done();
- })
- .catch(done.fail);
+ return CopyAsGFM.nodeToGFM(document.createElement('div'));
});
describe('replyWithSelectedText', () => {
@@ -79,22 +76,18 @@ describe('ShortcutsIssuable', () => {
stubSelection('<p>Selected text.</p>');
});
- it('leaves existing input intact', (done) => {
+ it('leaves existing input intact', async () => {
$(FORM_SELECTOR).val('This text was already here.');
expect($(FORM_SELECTOR).val()).toBe('This text was already here.');
ShortcutsIssuable.replyWithSelectedText(true);
- setImmediate(() => {
- expect($(FORM_SELECTOR).val()).toBe(
- 'This text was already here.\n\n> Selected text.\n\n',
- );
- done();
- });
+ await waitForPromises();
+ expect($(FORM_SELECTOR).val()).toBe('This text was already here.\n\n> Selected text.\n\n');
});
- it('triggers `input`', (done) => {
+ it('triggers `input`', async () => {
let triggered = false;
$(FORM_SELECTOR).on('input', () => {
triggered = true;
@@ -102,48 +95,40 @@ describe('ShortcutsIssuable', () => {
ShortcutsIssuable.replyWithSelectedText(true);
- setImmediate(() => {
- expect(triggered).toBe(true);
- done();
- });
+ await waitForPromises();
+ expect(triggered).toBe(true);
});
- it('triggers `focus`', (done) => {
+ it('triggers `focus`', async () => {
const spy = jest.spyOn(document.querySelector(FORM_SELECTOR), 'focus');
ShortcutsIssuable.replyWithSelectedText(true);
- setImmediate(() => {
- expect(spy).toHaveBeenCalled();
- done();
- });
+ await waitForPromises();
+ expect(spy).toHaveBeenCalled();
});
});
describe('with a one-line selection', () => {
- it('quotes the selection', (done) => {
+ it('quotes the selection', async () => {
stubSelection('<p>This text has been selected.</p>');
ShortcutsIssuable.replyWithSelectedText(true);
- setImmediate(() => {
- expect($(FORM_SELECTOR).val()).toBe('> This text has been selected.\n\n');
- done();
- });
+ await waitForPromises();
+ expect($(FORM_SELECTOR).val()).toBe('> This text has been selected.\n\n');
});
});
describe('with a multi-line selection', () => {
- it('quotes the selected lines as a group', (done) => {
+ it('quotes the selected lines as a group', async () => {
stubSelection(
'<p>Selected line one.</p>\n<p>Selected line two.</p>\n<p>Selected line three.</p>',
);
ShortcutsIssuable.replyWithSelectedText(true);
- setImmediate(() => {
- expect($(FORM_SELECTOR).val()).toBe(
- '> Selected line one.\n>\n> Selected line two.\n>\n> Selected line three.\n\n',
- );
- done();
- });
+ await waitForPromises();
+ expect($(FORM_SELECTOR).val()).toBe(
+ '> Selected line one.\n>\n> Selected line two.\n>\n> Selected line three.\n\n',
+ );
});
});
@@ -152,23 +137,19 @@ describe('ShortcutsIssuable', () => {
stubSelection('<p>Selected text.</p>', true);
});
- it('does not add anything to the input', (done) => {
+ it('does not add anything to the input', async () => {
ShortcutsIssuable.replyWithSelectedText(true);
- setImmediate(() => {
- expect($(FORM_SELECTOR).val()).toBe('');
- done();
- });
+ await waitForPromises();
+ expect($(FORM_SELECTOR).val()).toBe('');
});
- it('triggers `focus`', (done) => {
+ it('triggers `focus`', async () => {
const spy = jest.spyOn(document.querySelector(FORM_SELECTOR), 'focus');
ShortcutsIssuable.replyWithSelectedText(true);
- setImmediate(() => {
- expect(spy).toHaveBeenCalled();
- done();
- });
+ await waitForPromises();
+ expect(spy).toHaveBeenCalled();
});
});
@@ -177,26 +158,22 @@ describe('ShortcutsIssuable', () => {
stubSelection('<div class="md">Selected text.</div><p>Invalid selected text.</p>', true);
});
- it('only adds the valid part to the input', (done) => {
+ it('only adds the valid part to the input', async () => {
ShortcutsIssuable.replyWithSelectedText(true);
- setImmediate(() => {
- expect($(FORM_SELECTOR).val()).toBe('> Selected text.\n\n');
- done();
- });
+ await waitForPromises();
+ expect($(FORM_SELECTOR).val()).toBe('> Selected text.\n\n');
});
- it('triggers `focus`', (done) => {
+ it('triggers `focus`', async () => {
const spy = jest.spyOn(document.querySelector(FORM_SELECTOR), 'focus');
ShortcutsIssuable.replyWithSelectedText(true);
- setImmediate(() => {
- expect(spy).toHaveBeenCalled();
- done();
- });
+ await waitForPromises();
+ expect(spy).toHaveBeenCalled();
});
- it('triggers `input`', (done) => {
+ it('triggers `input`', async () => {
let triggered = false;
$(FORM_SELECTOR).on('input', () => {
triggered = true;
@@ -204,10 +181,8 @@ describe('ShortcutsIssuable', () => {
ShortcutsIssuable.replyWithSelectedText(true);
- setImmediate(() => {
- expect(triggered).toBe(true);
- done();
- });
+ await waitForPromises();
+ expect(triggered).toBe(true);
});
});
@@ -231,26 +206,22 @@ describe('ShortcutsIssuable', () => {
});
});
- it('adds the quoted selection to the input', (done) => {
+ it('adds the quoted selection to the input', async () => {
ShortcutsIssuable.replyWithSelectedText(true);
- setImmediate(() => {
- expect($(FORM_SELECTOR).val()).toBe('> *Selected text.*\n\n');
- done();
- });
+ await waitForPromises();
+ expect($(FORM_SELECTOR).val()).toBe('> *Selected text.*\n\n');
});
- it('triggers `focus`', (done) => {
+ it('triggers `focus`', async () => {
const spy = jest.spyOn(document.querySelector(FORM_SELECTOR), 'focus');
ShortcutsIssuable.replyWithSelectedText(true);
- setImmediate(() => {
- expect(spy).toHaveBeenCalled();
- done();
- });
+ await waitForPromises();
+ expect(spy).toHaveBeenCalled();
});
- it('triggers `input`', (done) => {
+ it('triggers `input`', async () => {
let triggered = false;
$(FORM_SELECTOR).on('input', () => {
triggered = true;
@@ -258,10 +229,8 @@ describe('ShortcutsIssuable', () => {
ShortcutsIssuable.replyWithSelectedText(true);
- setImmediate(() => {
- expect(triggered).toBe(true);
- done();
- });
+ await waitForPromises();
+ expect(triggered).toBe(true);
});
});
@@ -285,36 +254,29 @@ describe('ShortcutsIssuable', () => {
});
});
- it('does not add anything to the input', (done) => {
+ it('does not add anything to the input', async () => {
ShortcutsIssuable.replyWithSelectedText(true);
- setImmediate(() => {
- expect($(FORM_SELECTOR).val()).toBe('');
- done();
- });
+ await waitForPromises();
+ expect($(FORM_SELECTOR).val()).toBe('');
});
- it('triggers `focus`', (done) => {
+ it('triggers `focus`', async () => {
const spy = jest.spyOn(document.querySelector(FORM_SELECTOR), 'focus');
ShortcutsIssuable.replyWithSelectedText(true);
- setImmediate(() => {
- expect(spy).toHaveBeenCalled();
- done();
- });
+ await waitForPromises();
+ expect(spy).toHaveBeenCalled();
});
});
describe('with a valid selection with no text content', () => {
- it('returns the proper markdown', (done) => {
+ it('returns the proper markdown', async () => {
stubSelection('<img src="https://gitlab.com/logo.png" alt="logo" />');
ShortcutsIssuable.replyWithSelectedText(true);
- setImmediate(() => {
- expect($(FORM_SELECTOR).val()).toBe('> ![logo](https://gitlab.com/logo.png)\n\n');
-
- done();
- });
+ await waitForPromises();
+ expect($(FORM_SELECTOR).val()).toBe('> ![logo](https://gitlab.com/logo.png)\n\n');
});
});
});
diff --git a/spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap b/spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap
index 22bec77276b..b3d93906445 100644
--- a/spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap
+++ b/spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap
@@ -13,6 +13,7 @@ exports[`Blob Header Default Actions rendering matches the snapshot 1`] = `
<blob-filepath-stub
blob="[object Object]"
+ showpath="true"
/>
</div>
diff --git a/spec/frontend/blob/components/blob_edit_header_spec.js b/spec/frontend/blob/components/blob_edit_header_spec.js
index 910fc5c946d..b1ce0e9a4c5 100644
--- a/spec/frontend/blob/components/blob_edit_header_spec.js
+++ b/spec/frontend/blob/components/blob_edit_header_spec.js
@@ -1,5 +1,6 @@
import { GlFormInput, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import BlobEditHeader from '~/blob/components/blob_edit_header.vue';
describe('Blob Header Editing', () => {
@@ -40,7 +41,7 @@ describe('Blob Header Editing', () => {
});
describe('functionality', () => {
- it('emits input event when the blob name is changed', () => {
+ it('emits input event when the blob name is changed', async () => {
const inputComponent = wrapper.find(GlFormInput);
const newValue = 'bar.txt';
@@ -51,9 +52,8 @@ describe('Blob Header Editing', () => {
});
inputComponent.vm.$emit('change');
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted().input[0]).toEqual([newValue]);
- });
+ await nextTick();
+ expect(wrapper.emitted().input[0]).toEqual([newValue]);
});
});
diff --git a/spec/frontend/blob/components/blob_header_default_actions_spec.js b/spec/frontend/blob/components/blob_header_default_actions_spec.js
index e321bb41774..af605b257de 100644
--- a/spec/frontend/blob/components/blob_header_default_actions_spec.js
+++ b/spec/frontend/blob/components/blob_header_default_actions_spec.js
@@ -1,13 +1,13 @@
import { GlButtonGroup, GlButton } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
import BlobHeaderActions from '~/blob/components/blob_header_default_actions.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import {
BTN_COPY_CONTENTS_TITLE,
BTN_DOWNLOAD_TITLE,
BTN_RAW_TITLE,
RICH_BLOB_VIEWER,
} from '~/blob/components/constants';
-import { Blob } from './mock_data';
+import { Blob, mockEnvironmentName, mockEnvironmentPath } from './mock_data';
describe('Blob Header Default Actions', () => {
let wrapper;
@@ -17,7 +17,7 @@ describe('Blob Header Default Actions', () => {
const blobHash = 'foo-bar';
function createComponent(propsData = {}) {
- wrapper = mount(BlobHeaderActions, {
+ wrapper = shallowMountExtended(BlobHeaderActions, {
provide: {
blobHash,
},
@@ -39,8 +39,8 @@ describe('Blob Header Default Actions', () => {
});
describe('renders', () => {
- const findCopyButton = () => wrapper.find('[data-testid="copyContentsButton"]');
- const findViewRawButton = () => wrapper.find('[data-testid="viewRawButton"]');
+ const findCopyButton = () => wrapper.findByTestId('copyContentsButton');
+ const findViewRawButton = () => wrapper.findByTestId('viewRawButton');
it('gl-button-group component', () => {
expect(btnGroup.exists()).toBe(true);
@@ -89,4 +89,37 @@ describe('Blob Header Default Actions', () => {
expect(findViewRawButton().exists()).toBe(false);
});
});
+
+ describe('view on environment button', () => {
+ const findEnvironmentButton = () => wrapper.findByTestId('environment');
+
+ it.each`
+ environmentName | environmentPath | isVisible
+ ${null} | ${null} | ${false}
+ ${null} | ${mockEnvironmentPath} | ${false}
+ ${mockEnvironmentName} | ${null} | ${false}
+ ${mockEnvironmentName} | ${mockEnvironmentPath} | ${true}
+ `(
+ 'when environmentName is $environmentName and environmentPath is $environmentPath',
+ ({ environmentName, environmentPath, isVisible }) => {
+ createComponent({ environmentName, environmentPath });
+
+ expect(findEnvironmentButton().exists()).toBe(isVisible);
+ },
+ );
+
+ it('renders the correct attributes', () => {
+ createComponent({
+ environmentName: mockEnvironmentName,
+ environmentPath: mockEnvironmentPath,
+ });
+
+ expect(findEnvironmentButton().attributes()).toMatchObject({
+ title: `View on ${mockEnvironmentName}`,
+ href: mockEnvironmentPath,
+ });
+
+ expect(findEnvironmentButton().props('icon')).toBe('external-link');
+ });
+ });
});
diff --git a/spec/frontend/blob/components/blob_header_spec.js b/spec/frontend/blob/components/blob_header_spec.js
index bd81b1594bf..8e1b03c6126 100644
--- a/spec/frontend/blob/components/blob_header_spec.js
+++ b/spec/frontend/blob/components/blob_header_spec.js
@@ -1,4 +1,5 @@
import { shallowMount, mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import BlobHeader from '~/blob/components/blob_header.vue';
import DefaultActions from '~/blob/components/blob_header_default_actions.vue';
import BlobFilepath from '~/blob/components/blob_header_filepath.vue';
@@ -139,26 +140,24 @@ describe('Blob Header Default Actions', () => {
expect(wrapper.vm.viewer).toBe(null);
});
- it('watches the changes in viewer data and emits event when the change is registered', () => {
+ it('watches the changes in viewer data and emits event when the change is registered', async () => {
factory();
jest.spyOn(wrapper.vm, '$emit');
wrapper.vm.viewer = newViewer;
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('viewer-changed', newViewer);
- });
+ await nextTick();
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('viewer-changed', newViewer);
});
- it('does not emit event if the switcher is not rendered', () => {
+ it('does not emit event if the switcher is not rendered', async () => {
factory(true);
expect(wrapper.vm.showViewerSwitcher).toBe(false);
jest.spyOn(wrapper.vm, '$emit');
wrapper.vm.viewer = newViewer;
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.vm.$emit).not.toHaveBeenCalled();
- });
+ await nextTick();
+ expect(wrapper.vm.$emit).not.toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js b/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js
index 9a560ec11f7..91baaf3ea69 100644
--- a/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js
+++ b/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js
@@ -1,5 +1,6 @@
import { GlButtonGroup, GlButton } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import BlobHeaderViewerSwitcher from '~/blob/components/blob_header_viewer_switcher.vue';
import {
RICH_BLOB_VIEWER,
@@ -72,26 +73,24 @@ describe('Blob Header Viewer Switcher', () => {
expect(wrapper.vm.$emit).not.toHaveBeenCalled();
});
- it('emits an event when a Rich Viewer button is clicked', () => {
+ it('emits an event when a Rich Viewer button is clicked', async () => {
factory();
expect(wrapper.vm.value).toBe(SIMPLE_BLOB_VIEWER);
richBtn.vm.$emit('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('input', RICH_BLOB_VIEWER);
- });
+ await nextTick();
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('input', RICH_BLOB_VIEWER);
});
- it('emits an event when a Simple Viewer button is clicked', () => {
+ it('emits an event when a Simple Viewer button is clicked', async () => {
factory({
value: RICH_BLOB_VIEWER,
});
simpleBtn.vm.$emit('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('input', SIMPLE_BLOB_VIEWER);
- });
+ await nextTick();
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('input', SIMPLE_BLOB_VIEWER);
});
});
});
diff --git a/spec/frontend/blob/components/mock_data.js b/spec/frontend/blob/components/mock_data.js
index 95789ca13cb..9a345921f16 100644
--- a/spec/frontend/blob/components/mock_data.js
+++ b/spec/frontend/blob/components/mock_data.js
@@ -55,3 +55,6 @@ export const SimpleBlobContentMock = {
path: 'foo.js',
plainData: 'Plain',
};
+
+export const mockEnvironmentName = 'my.testing.environment';
+export const mockEnvironmentPath = 'https://my.testing.environment';
diff --git a/spec/frontend/blob/viewer/index_spec.js b/spec/frontend/blob/viewer/index_spec.js
index 9e9f866d40c..fe55a537b89 100644
--- a/spec/frontend/blob/viewer/index_spec.js
+++ b/spec/frontend/blob/viewer/index_spec.js
@@ -41,34 +41,30 @@ describe('Blob viewer', () => {
window.location.hash = '';
});
- it('loads source file after switching views', (done) => {
+ it('loads source file after switching views', async () => {
document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]').click();
- setImmediate(() => {
- expect(
- document
- .querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]')
- .classList.contains('hidden'),
- ).toBeFalsy();
+ await axios.waitForAll();
- done();
- });
+ expect(
+ document
+ .querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]')
+ .classList.contains('hidden'),
+ ).toBeFalsy();
});
- it('loads source file when line number is in hash', (done) => {
+ it('loads source file when line number is in hash', async () => {
window.location.hash = '#L1';
new BlobViewer();
- setImmediate(() => {
- expect(
- document
- .querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]')
- .classList.contains('hidden'),
- ).toBeFalsy();
+ await axios.waitForAll();
- done();
- });
+ expect(
+ document
+ .querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]')
+ .classList.contains('hidden'),
+ ).toBeFalsy();
});
it('doesnt reload file if already loaded', () => {
@@ -123,24 +119,20 @@ describe('Blob viewer', () => {
expect(copyButton.blur).not.toHaveBeenCalled();
});
- it('enables after switching to simple view', (done) => {
+ it('enables after switching to simple view', async () => {
document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]').click();
- setImmediate(() => {
- expect(copyButton.classList.contains('disabled')).toBeFalsy();
+ await axios.waitForAll();
- done();
- });
+ expect(copyButton.classList.contains('disabled')).toBeFalsy();
});
- it('updates tooltip after switching to simple view', (done) => {
+ it('updates tooltip after switching to simple view', async () => {
document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]').click();
- setImmediate(() => {
- expect(copyButtonTooltip.getAttribute('title')).toBe('Copy file contents');
+ await axios.waitForAll();
- done();
- });
+ expect(copyButtonTooltip.getAttribute('title')).toBe('Copy file contents');
});
});
diff --git a/spec/frontend/boards/board_card_inner_spec.js b/spec/frontend/boards/board_card_inner_spec.js
index e0446811f64..677978d31ca 100644
--- a/spec/frontend/boards/board_card_inner_spec.js
+++ b/spec/frontend/boards/board_card_inner_spec.js
@@ -1,15 +1,13 @@
import { GlLabel, GlLoadingIcon, GlTooltip } from '@gitlab/ui';
import { range } from 'lodash';
import Vuex from 'vuex';
-import setWindowLocation from 'helpers/set_window_location_helper';
+import { nextTick } from 'vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import BoardBlockedIcon from '~/boards/components/board_blocked_icon.vue';
import BoardCardInner from '~/boards/components/board_card_inner.vue';
import { issuableTypes } from '~/boards/constants';
-import eventHub from '~/boards/eventhub';
import defaultStore from '~/boards/stores';
-import { updateHistory } from '~/lib/utils/url_utility';
import { mockLabelList, mockIssue, mockIssueFullPath } from './mock_data';
jest.mock('~/lib/utils/url_utility');
@@ -53,6 +51,7 @@ describe('Board card component', () => {
state: {
...defaultStore.state,
issuableType: issuableTypes.issue,
+ isShowingLabels: true,
},
getters: {
isGroupBoard: () => true,
@@ -261,7 +260,7 @@ describe('Board card component', () => {
],
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.find('.board-card-assignee img').attributes('src')).toBe(
'test_image_from_avatar_url?width=24',
@@ -376,7 +375,7 @@ describe('Board card component', () => {
},
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.find('.board-card-assignee .avatar-counter').text().trim()).toEqual('99+');
});
@@ -399,58 +398,17 @@ describe('Board card component', () => {
it('does not render label if label does not have an ID', async () => {
wrapper.setProps({ item: { ...issue, labels: [label1, { title: 'closed' }] } });
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.findAll(GlLabel).length).toBe(1);
expect(wrapper.text()).not.toContain('closed');
});
- });
-
- describe('filterByLabel method', () => {
- beforeEach(() => {
- wrapper.setProps({
- updateFilters: true,
- });
- });
-
- describe('when selected label is not in the filter', () => {
- beforeEach(() => {
- jest.spyOn(wrapper.vm, 'performSearch').mockImplementation(() => {});
- setWindowLocation('?');
- wrapper.vm.filterByLabel(label1);
- });
-
- it('calls updateHistory', () => {
- expect(updateHistory).toHaveBeenCalledTimes(1);
- });
-
- it('dispatches performSearch vuex action', () => {
- expect(wrapper.vm.performSearch).toHaveBeenCalledTimes(1);
- });
-
- it('emits updateTokens event', () => {
- expect(eventHub.$emit).toHaveBeenCalledTimes(1);
- expect(eventHub.$emit).toHaveBeenCalledWith('updateTokens');
- });
- });
-
- describe('when selected label is already in the filter', () => {
- beforeEach(() => {
- jest.spyOn(wrapper.vm, 'performSearch').mockImplementation(() => {});
- setWindowLocation('?label_name[]=testing%20123');
- wrapper.vm.filterByLabel(label1);
- });
-
- it('does not call updateHistory', () => {
- expect(updateHistory).not.toHaveBeenCalled();
- });
- it('does not dispatch performSearch vuex action', () => {
- expect(wrapper.vm.performSearch).not.toHaveBeenCalled();
- });
-
- it('does not emit updateTokens event', () => {
- expect(eventHub.$emit).not.toHaveBeenCalled();
+ describe('when label params arent set', () => {
+ it('passes the right target to GlLabel', () => {
+ expect(wrapper.findAll(GlLabel).at(0).props('target')).toEqual(
+ '?label_name[]=testing%20123',
+ );
});
});
});
diff --git a/spec/frontend/boards/board_list_helper.js b/spec/frontend/boards/board_list_helper.js
index d0f14bd37c1..04192489817 100644
--- a/spec/frontend/boards/board_list_helper.js
+++ b/spec/frontend/boards/board_list_helper.js
@@ -1,4 +1,5 @@
-import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import VueApollo from 'vue-apollo';
import Vuex from 'vuex';
@@ -33,9 +34,8 @@ export default function createComponent({
},
issuesCount,
} = {}) {
- const localVue = createLocalVue();
- localVue.use(VueApollo);
- localVue.use(Vuex);
+ Vue.use(VueApollo);
+ Vue.use(Vuex);
const fakeApollo = createMockApollo([
[listQuery, jest.fn().mockResolvedValue(boardListQueryResponse(issuesCount))],
@@ -85,7 +85,6 @@ export default function createComponent({
const component = shallowMount(BoardList, {
apolloProvider: fakeApollo,
- localVue,
store,
propsData: {
disabled: false,
diff --git a/spec/frontend/boards/board_list_spec.js b/spec/frontend/boards/board_list_spec.js
index 1981ed5ab7f..fd9d2b6823d 100644
--- a/spec/frontend/boards/board_list_spec.js
+++ b/spec/frontend/boards/board_list_spec.js
@@ -1,6 +1,8 @@
import Draggable from 'vuedraggable';
+import { nextTick } from 'vue';
import { DraggableItemTypes } from 'ee_else_ce/boards/constants';
import { useFakeRequestAnimationFrame } from 'helpers/fake_request_animation_frame';
+import waitForPromises from 'helpers/wait_for_promises';
import createComponent from 'jest/boards/board_list_helper';
import BoardCard from '~/boards/components/board_card.vue';
import eventHub from '~/boards/eventhub';
@@ -64,14 +66,14 @@ describe('Board list component', () => {
it('shows new issue form', async () => {
wrapper.vm.toggleForm();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.find('.board-new-issue-form').exists()).toBe(true);
});
it('shows new issue form after eventhub event', async () => {
eventHub.$emit(`toggle-issue-form-${wrapper.vm.list.id}`);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.find('.board-new-issue-form').exists()).toBe(true);
});
@@ -85,7 +87,7 @@ describe('Board list component', () => {
it('shows count list item', async () => {
wrapper.vm.showCount = true;
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.find('.board-list-count').exists()).toBe(true);
expect(wrapper.find('.board-list-count').text()).toBe('Showing all issues');
@@ -94,7 +96,7 @@ describe('Board list component', () => {
it('sets data attribute with invalid id', async () => {
wrapper.vm.showCount = true;
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.find('.board-list-count').attributes('data-issue-id')).toBe('-1');
});
});
@@ -104,10 +106,6 @@ describe('Board list component', () => {
fetchItemsForList: jest.fn(),
};
- beforeEach(() => {
- wrapper = createComponent();
- });
-
it('does not load issues if already loading', () => {
wrapper = createComponent({
actions,
@@ -126,20 +124,23 @@ describe('Board list component', () => {
},
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findIssueCountLoadingIcon().exists()).toBe(true);
});
it('shows how many more issues to load', async () => {
- // wrapper.vm.showCount = true;
wrapper = createComponent({
data: {
showCount: true,
},
});
- await wrapper.vm.$nextTick();
+ await nextTick();
+ await waitForPromises();
+ await nextTick();
+ await nextTick();
+
expect(wrapper.find('.board-list-count').text()).toBe('Showing 1 of 20 issues');
});
});
@@ -155,7 +156,7 @@ describe('Board list component', () => {
it('sets background to bg-danger-100', async () => {
wrapper.setProps({ list: { issuesCount: 4, maxIssueCount: 3 } });
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.find('.bg-danger-100').exists()).toBe(true);
});
});
diff --git a/spec/frontend/boards/components/board_add_new_column_trigger_spec.js b/spec/frontend/boards/components/board_add_new_column_trigger_spec.js
index c35f2463f69..7dd02bf1d35 100644
--- a/spec/frontend/boards/components/board_add_new_column_trigger_spec.js
+++ b/spec/frontend/boards/components/board_add_new_column_trigger_spec.js
@@ -1,5 +1,5 @@
import { GlButton } from '@gitlab/ui';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import BoardAddNewColumnTrigger from '~/boards/components/board_add_new_column_trigger.vue';
@@ -49,7 +49,7 @@ describe('BoardAddNewColumnTrigger', () => {
it('shows the tooltip', async () => {
wrapper.find(GlButton).vm.$emit('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
const tooltip = findTooltipText();
diff --git a/spec/frontend/boards/components/board_blocked_icon_spec.js b/spec/frontend/boards/components/board_blocked_icon_spec.js
index 7b04942f056..7a5c49bd488 100644
--- a/spec/frontend/boards/components/board_blocked_icon_spec.js
+++ b/spec/frontend/boards/components/board_blocked_icon_spec.js
@@ -1,6 +1,6 @@
import { GlIcon, GlLink, GlPopover, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
@@ -39,7 +39,7 @@ describe('BoardBlockedIcon', () => {
const mouseenter = async () => {
findGlIcon().vm.$emit('mouseenter');
- await wrapper.vm.$nextTick();
+ await nextTick();
await waitForApollo();
};
diff --git a/spec/frontend/boards/components/board_card_spec.js b/spec/frontend/boards/components/board_card_spec.js
index 3af173aa18c..aad89cf8261 100644
--- a/spec/frontend/boards/components/board_card_spec.js
+++ b/spec/frontend/boards/components/board_card_spec.js
@@ -1,6 +1,6 @@
import { GlLabel } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import BoardCard from '~/boards/components/board_card.vue';
@@ -65,12 +65,12 @@ describe('Board card', () => {
const selectCard = async () => {
wrapper.trigger('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
};
const multiSelectCard = async () => {
wrapper.trigger('click', { ctrlKey: true });
- await wrapper.vm.$nextTick();
+ await nextTick();
};
beforeEach(() => {
diff --git a/spec/frontend/boards/components/board_filtered_search_spec.js b/spec/frontend/boards/components/board_filtered_search_spec.js
index a8398a138ba..85ba703a6ee 100644
--- a/spec/frontend/boards/components/board_filtered_search_spec.js
+++ b/spec/frontend/boards/components/board_filtered_search_spec.js
@@ -120,7 +120,7 @@ describe('BoardFilteredSearch', () => {
{ type: 'author', value: { data: 'root', operator: '=' } },
{ type: 'assignee', value: { data: 'root', operator: '=' } },
{ type: 'label', value: { data: 'label', operator: '=' } },
- { type: 'label', value: { data: 'label2', operator: '=' } },
+ { type: 'label', value: { data: 'label&2', operator: '=' } },
{ type: 'milestone', value: { data: 'New Milestone', operator: '=' } },
{ type: 'type', value: { data: 'INCIDENT', operator: '=' } },
{ type: 'weight', value: { data: '2', operator: '=' } },
@@ -134,7 +134,7 @@ describe('BoardFilteredSearch', () => {
title: '',
replace: true,
url:
- 'http://test.host/?author_username=root&label_name[]=label&label_name[]=label2&assignee_username=root&milestone_title=New+Milestone&iteration_id=3341&types=INCIDENT&weight=2&release_tag=v1.0.0',
+ 'http://test.host/?author_username=root&label_name[]=label&label_name[]=label%262&assignee_username=root&milestone_title=New%20Milestone&iteration_id=3341&types=INCIDENT&weight=2&release_tag=v1.0.0',
});
});
diff --git a/spec/frontend/boards/components/board_form_spec.js b/spec/frontend/boards/components/board_form_spec.js
index 692fd3ec555..5678da2a246 100644
--- a/spec/frontend/boards/components/board_form_spec.js
+++ b/spec/frontend/boards/components/board_form_spec.js
@@ -130,7 +130,7 @@ describe('BoardForm', () => {
it('passes correct primary action text and variant', () => {
expect(findModalActionPrimary().text).toBe('Create board');
- expect(findModalActionPrimary().attributes[0].variant).toBe('success');
+ expect(findModalActionPrimary().attributes[0].variant).toBe('confirm');
});
it('does not render delete confirmation message', () => {
diff --git a/spec/frontend/boards/components/board_list_header_spec.js b/spec/frontend/boards/components/board_list_header_spec.js
index 8cc0ad5f30c..14870ec76a2 100644
--- a/spec/frontend/boards/components/board_list_header_spec.js
+++ b/spec/frontend/boards/components/board_list_header_spec.js
@@ -1,5 +1,5 @@
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';
@@ -148,7 +148,7 @@ describe('Board List Header Component', () => {
findCaret().vm.$emit('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(toggleListCollapsedSpy).toHaveBeenCalledTimes(1);
});
@@ -156,7 +156,7 @@ describe('Board List Header Component', () => {
createComponent({ withLocalStorage: false, currentUserId: 1 });
findCaret().vm.$emit('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(updateListSpy).toHaveBeenCalledTimes(1);
expect(localStorage.getItem(`${wrapper.vm.uniqueKey}.collapsed`)).toBe(null);
@@ -168,7 +168,7 @@ describe('Board List Header Component', () => {
});
findCaret().vm.$emit('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(updateListSpy).not.toHaveBeenCalled();
expect(localStorage.getItem(`${wrapper.vm.uniqueKey}.collapsed`)).toBe(String(isCollapsed()));
diff --git a/spec/frontend/boards/components/board_new_issue_spec.js b/spec/frontend/boards/components/board_new_issue_spec.js
index 57ccebf3676..8b0100d069a 100644
--- a/spec/frontend/boards/components/board_new_issue_spec.js
+++ b/spec/frontend/boards/components/board_new_issue_spec.js
@@ -1,15 +1,14 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import BoardNewIssue from '~/boards/components/board_new_issue.vue';
import BoardNewItem from '~/boards/components/board_new_item.vue';
import ProjectSelect from '~/boards/components/project_select.vue';
import eventHub from '~/boards/eventhub';
-import { mockList, mockGroupProjects } from '../mock_data';
+import { mockList, mockGroupProjects, mockIssue, mockIssue2 } from '../mock_data';
-const localVue = createLocalVue();
-
-localVue.use(Vuex);
+Vue.use(Vuex);
const addListNewIssuesSpy = jest.fn().mockResolvedValue();
const mockActions = { addListNewIssue: addListNewIssuesSpy };
@@ -17,10 +16,9 @@ const mockActions = { addListNewIssue: addListNewIssuesSpy };
const createComponent = ({
state = { selectedProject: mockGroupProjects[0], fullPath: mockGroupProjects[0].fullPath },
actions = mockActions,
- getters = { isGroupBoard: () => true, isProjectBoard: () => false },
+ getters = { isGroupBoard: () => true, getBoardItemsByList: () => () => [] },
} = {}) =>
shallowMount(BoardNewIssue, {
- localVue,
store: new Vuex.Store({
state,
actions,
@@ -47,7 +45,7 @@ describe('Issue boards new issue form', () => {
beforeEach(async () => {
wrapper = createComponent();
- await wrapper.vm.$nextTick();
+ await nextTick();
});
afterEach(() => {
@@ -68,7 +66,7 @@ describe('Issue boards new issue form', () => {
it('calls addListNewIssue action when `board-new-item` emits form-submit event', async () => {
findBoardNewItem().vm.$emit('form-submit', { title: 'Foo' });
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(addListNewIssuesSpy).toHaveBeenCalledWith(expect.any(Object), {
list: mockList,
issueInput: {
@@ -77,15 +75,44 @@ describe('Issue boards new issue form', () => {
assigneeIds: [],
milestoneId: undefined,
projectPath: mockGroupProjects[0].fullPath,
+ moveAfterId: undefined,
},
});
});
+ describe('when list has an existing issues', () => {
+ beforeEach(() => {
+ wrapper = createComponent({
+ getters: {
+ isGroupBoard: () => true,
+ getBoardItemsByList: () => () => [mockIssue, mockIssue2],
+ },
+ });
+ });
+
+ it('it uses the first issue ID as moveAfterId', async () => {
+ findBoardNewItem().vm.$emit('form-submit', { title: 'Foo' });
+
+ await nextTick();
+ expect(addListNewIssuesSpy).toHaveBeenCalledWith(expect.any(Object), {
+ list: mockList,
+ issueInput: {
+ title: 'Foo',
+ labelIds: [],
+ assigneeIds: [],
+ milestoneId: undefined,
+ projectPath: mockGroupProjects[0].fullPath,
+ moveAfterId: mockIssue.id,
+ },
+ });
+ });
+ });
+
it('emits event `toggle-issue-form` with current list Id suffix on eventHub when `board-new-item` emits form-cancel event', async () => {
jest.spyOn(eventHub, '$emit').mockImplementation();
findBoardNewItem().vm.$emit('form-cancel');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(eventHub.$emit).toHaveBeenCalledWith(`toggle-issue-form-${mockList.id}`);
});
@@ -101,7 +128,7 @@ describe('Issue boards new issue form', () => {
describe('when in project issue board', () => {
beforeEach(() => {
wrapper = createComponent({
- getters: { isGroupBoard: () => false, isProjectBoard: () => true },
+ getters: { isGroupBoard: () => false },
});
});
diff --git a/spec/frontend/boards/components/board_new_item_spec.js b/spec/frontend/boards/components/board_new_item_spec.js
index 0151d9c1c14..86cebc8a719 100644
--- a/spec/frontend/boards/components/board_new_item_spec.js
+++ b/spec/frontend/boards/components/board_new_item_spec.js
@@ -1,4 +1,5 @@
import { GlForm, GlFormInput, GlButton } from '@gitlab/ui';
+import { nextTick } from 'vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import BoardNewItem from '~/boards/components/board_new_item.vue';
@@ -39,6 +40,27 @@ describe('BoardNewItem', () => {
});
describe('template', () => {
+ describe('when the user provides a valid input', () => {
+ it('finds an enabled create button', async () => {
+ expect(wrapper.findByTestId('create-button').props('disabled')).toBe(true);
+
+ wrapper.find(GlFormInput).vm.$emit('input', 'hello');
+ await nextTick();
+
+ expect(wrapper.findByTestId('create-button').props('disabled')).toBe(false);
+ });
+ });
+
+ describe('when the user types in a string with only spaces', () => {
+ it('disables the Create Issue button', async () => {
+ wrapper.find(GlFormInput).vm.$emit('input', ' ');
+
+ await nextTick();
+
+ expect(wrapper.findByTestId('create-button').props('disabled')).toBe(true);
+ });
+ });
+
it('renders gl-form component', () => {
expect(wrapper.findComponent(GlForm).exists()).toBe(true);
});
@@ -80,6 +102,19 @@ describe('BoardNewItem', () => {
]);
});
+ it('emits `form-submit` event with trimmed title', async () => {
+ titleInput().setValue(' Foo ');
+
+ await glForm().trigger('submit');
+
+ expect(wrapper.emitted('form-submit')[0]).toEqual([
+ {
+ title: 'Foo',
+ list: mockList,
+ },
+ ]);
+ });
+
it('emits `scroll-board-list-` event with list.id on eventHub when `submit` is triggered on gl-form', async () => {
jest.spyOn(eventHub, '$emit').mockImplementation();
await glForm().trigger('submit');
@@ -90,7 +125,7 @@ describe('BoardNewItem', () => {
it('emits `form-cancel` event and clears title value when `reset` is triggered on gl-form', async () => {
titleInput().setValue('Foo');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(titleInput().element.value).toBe('Foo');
await glForm().trigger('reset');
diff --git a/spec/frontend/boards/components/board_settings_sidebar_spec.js b/spec/frontend/boards/components/board_settings_sidebar_spec.js
index 46dd109ffb1..7f40c426b30 100644
--- a/spec/frontend/boards/components/board_settings_sidebar_spec.js
+++ b/spec/frontend/boards/components/board_settings_sidebar_spec.js
@@ -1,8 +1,9 @@
-import { GlDrawer, GlLabel } from '@gitlab/ui';
+import { GlDrawer, GlLabel, GlModal, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { MountingPortal } from 'portal-vue';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { stubComponent } from 'helpers/stub_component';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import BoardSettingsSidebar from '~/boards/components/board_settings_sidebar.vue';
@@ -20,8 +21,7 @@ describe('BoardSettingsSidebar', () => {
const labelTitle = mockLabelList.label.title;
const labelColor = mockLabelList.label.color;
const listId = mockLabelList.id;
-
- const findRemoveButton = () => wrapper.findByTestId('remove-list');
+ const modalID = 'board-settings-sidebar-modal';
const createComponent = ({
canAdminList = false,
@@ -46,6 +46,9 @@ describe('BoardSettingsSidebar', () => {
canAdminList,
scopedLabelsAvailable: false,
},
+ directives: {
+ GlModal: createMockDirective(),
+ },
stubs: {
GlDrawer: stubComponent(GlDrawer, {
template: '<div><slot name="header"></slot><slot></slot></div>',
@@ -56,6 +59,8 @@ describe('BoardSettingsSidebar', () => {
};
const findLabel = () => wrapper.find(GlLabel);
const findDrawer = () => wrapper.find(GlDrawer);
+ const findModal = () => wrapper.find(GlModal);
+ const findRemoveButton = () => wrapper.find(GlButton);
afterEach(() => {
jest.restoreAllMocks();
@@ -86,7 +91,7 @@ describe('BoardSettingsSidebar', () => {
findDrawer().vm.$emit('close');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.find(GlDrawer).exists()).toBe(false);
});
@@ -96,7 +101,7 @@ describe('BoardSettingsSidebar', () => {
sidebarEventHub.$emit('sidebar.closeAll');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.find(GlDrawer).exists()).toBe(false);
});
@@ -161,5 +166,16 @@ describe('BoardSettingsSidebar', () => {
expect(findRemoveButton().exists()).toBe(true);
});
+
+ it('has the correct ID on the button', () => {
+ createComponent({ canAdminList: true, activeId: listId, list: mockLabelList });
+ const binding = getBinding(findRemoveButton().element, 'gl-modal');
+ expect(binding.value).toBe(modalID);
+ });
+
+ it('has the correct ID on the modal', () => {
+ createComponent({ canAdminList: true, activeId: listId, list: mockLabelList });
+ expect(findModal().props('modalId')).toBe(modalID);
+ });
});
});
diff --git a/spec/frontend/boards/components/boards_selector_spec.js b/spec/frontend/boards/components/boards_selector_spec.js
index 9cf7c5774bf..26a5bf34595 100644
--- a/spec/frontend/boards/components/boards_selector_spec.js
+++ b/spec/frontend/boards/components/boards_selector_spec.js
@@ -1,43 +1,40 @@
import { GlDropdown, GlLoadingIcon, GlDropdownSectionHeader } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
-import MockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import Vuex from 'vuex';
import { TEST_HOST } from 'spec/test_constants';
import BoardsSelector from '~/boards/components/boards_selector.vue';
+import { BoardType } from '~/boards/constants';
import groupBoardQuery from '~/boards/graphql/group_board.query.graphql';
import projectBoardQuery from '~/boards/graphql/project_board.query.graphql';
+import groupBoardsQuery from '~/boards/graphql/group_boards.query.graphql';
+import projectBoardsQuery from '~/boards/graphql/project_boards.query.graphql';
+import groupRecentBoardsQuery from '~/boards/graphql/group_recent_boards.query.graphql';
+import projectRecentBoardsQuery from '~/boards/graphql/project_recent_boards.query.graphql';
import defaultStore from '~/boards/stores';
-import axios from '~/lib/utils/axios_utils';
import createMockApollo from 'helpers/mock_apollo_helper';
-import { mockGroupBoardResponse, mockProjectBoardResponse } from '../mock_data';
+import {
+ mockGroupBoardResponse,
+ mockProjectBoardResponse,
+ mockGroupAllBoardsResponse,
+ mockProjectAllBoardsResponse,
+ mockGroupRecentBoardsResponse,
+ mockProjectRecentBoardsResponse,
+ mockSmallProjectAllBoardsResponse,
+ mockEmptyProjectRecentBoardsResponse,
+ boards,
+ recentIssueBoards,
+} from '../mock_data';
const throttleDuration = 1;
Vue.use(VueApollo);
-function boardGenerator(n) {
- return new Array(n).fill().map((board, index) => {
- const id = `${index}`;
- const name = `board${id}`;
-
- return {
- id,
- name,
- };
- });
-}
-
describe('BoardsSelector', () => {
let wrapper;
- let allBoardsResponse;
- let recentBoardsResponse;
- let mock;
let fakeApollo;
let store;
- const boards = boardGenerator(20);
- const recentBoards = boardGenerator(5);
const createStore = ({ isGroupBoard = false, isProjectBoard = false } = {}) => {
store = new Vuex.Store({
@@ -63,17 +60,43 @@ describe('BoardsSelector', () => {
};
const getDropdownItems = () => wrapper.findAll('.js-dropdown-item');
- const getDropdownHeaders = () => wrapper.findAll(GlDropdownSectionHeader);
- const getLoadingIcon = () => wrapper.find(GlLoadingIcon);
- const findDropdown = () => wrapper.find(GlDropdown);
+ const getDropdownHeaders = () => wrapper.findAllComponents(GlDropdownSectionHeader);
+ const getLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
const projectBoardQueryHandlerSuccess = jest.fn().mockResolvedValue(mockProjectBoardResponse);
const groupBoardQueryHandlerSuccess = jest.fn().mockResolvedValue(mockGroupBoardResponse);
- const createComponent = () => {
+ const projectBoardsQueryHandlerSuccess = jest
+ .fn()
+ .mockResolvedValue(mockProjectAllBoardsResponse);
+ const groupBoardsQueryHandlerSuccess = jest.fn().mockResolvedValue(mockGroupAllBoardsResponse);
+
+ const projectRecentBoardsQueryHandlerSuccess = jest
+ .fn()
+ .mockResolvedValue(mockProjectRecentBoardsResponse);
+ const groupRecentBoardsQueryHandlerSuccess = jest
+ .fn()
+ .mockResolvedValue(mockGroupRecentBoardsResponse);
+
+ const smallBoardsQueryHandlerSuccess = jest
+ .fn()
+ .mockResolvedValue(mockSmallProjectAllBoardsResponse);
+ const emptyRecentBoardsQueryHandlerSuccess = jest
+ .fn()
+ .mockResolvedValue(mockEmptyProjectRecentBoardsResponse);
+
+ const createComponent = ({
+ projectBoardsQueryHandler = projectBoardsQueryHandlerSuccess,
+ projectRecentBoardsQueryHandler = projectRecentBoardsQueryHandlerSuccess,
+ } = {}) => {
fakeApollo = createMockApollo([
[projectBoardQuery, projectBoardQueryHandlerSuccess],
[groupBoardQuery, groupBoardQueryHandlerSuccess],
+ [projectBoardsQuery, projectBoardsQueryHandler],
+ [groupBoardsQuery, groupBoardsQueryHandlerSuccess],
+ [projectRecentBoardsQuery, projectRecentBoardsQueryHandler],
+ [groupRecentBoardsQuery, groupRecentBoardsQueryHandlerSuccess],
]);
wrapper = mount(BoardsSelector, {
@@ -91,67 +114,34 @@ describe('BoardsSelector', () => {
attachTo: document.body,
provide: {
fullPath: '',
- recentBoardsEndpoint: `${TEST_HOST}/recent`,
},
});
-
- wrapper.vm.$apollo.addSmartQuery = jest.fn((_, options) => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- [options.loadingKey]: true,
- });
- });
};
afterEach(() => {
wrapper.destroy();
- wrapper = null;
- mock.restore();
+ fakeApollo = null;
});
- describe('fetching all boards', () => {
+ describe('template', () => {
beforeEach(() => {
- mock = new MockAdapter(axios);
-
- allBoardsResponse = Promise.resolve({
- data: {
- group: {
- boards: {
- edges: boards.map((board) => ({ node: board })),
- },
- },
- },
- });
- recentBoardsResponse = Promise.resolve({
- data: recentBoards,
- });
-
- createStore();
+ createStore({ isProjectBoard: true });
createComponent();
-
- mock.onGet(`${TEST_HOST}/recent`).replyOnce(200, recentBoards);
});
describe('loading', () => {
- beforeEach(async () => {
- // Wait for current board to be loaded
- await nextTick();
-
- // Emits gl-dropdown show event to simulate the dropdown is opened at initialization time
- findDropdown().vm.$emit('show');
- });
-
// we are testing loading state, so don't resolve responses until after the tests
afterEach(async () => {
- await Promise.all([allBoardsResponse, recentBoardsResponse]);
await nextTick();
});
it('shows loading spinner', () => {
+ // Emits gl-dropdown show event to simulate the dropdown is opened at initialization time
+ findDropdown().vm.$emit('show');
+
+ expect(getLoadingIcon().exists()).toBe(true);
expect(getDropdownHeaders()).toHaveLength(0);
expect(getDropdownItems()).toHaveLength(0);
- expect(getLoadingIcon().exists()).toBe(true);
});
});
@@ -163,16 +153,13 @@ describe('BoardsSelector', () => {
// Emits gl-dropdown show event to simulate the dropdown is opened at initialization time
findDropdown().vm.$emit('show');
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- await wrapper.setData({
- loadingBoards: false,
- loadingRecentBoards: false,
- });
- await Promise.all([allBoardsResponse, recentBoardsResponse]);
await nextTick();
});
+ it('fetches all issue boards', () => {
+ expect(projectBoardsQueryHandlerSuccess).toHaveBeenCalled();
+ });
+
it('hides loading spinner', async () => {
await nextTick();
expect(getLoadingIcon().exists()).toBe(false);
@@ -180,22 +167,17 @@ describe('BoardsSelector', () => {
describe('filtering', () => {
beforeEach(async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- boards,
- });
-
await nextTick();
});
it('shows all boards without filtering', () => {
- expect(getDropdownItems()).toHaveLength(boards.length + recentBoards.length);
+ expect(getDropdownItems()).toHaveLength(boards.length + recentIssueBoards.length);
});
it('shows only matching boards when filtering', async () => {
const filterTerm = 'board1';
- const expectedCount = boards.filter((board) => board.name.includes(filterTerm)).length;
+ const expectedCount = boards.filter((board) => board.node.name.includes(filterTerm))
+ .length;
fillSearchBox(filterTerm);
@@ -214,32 +196,21 @@ describe('BoardsSelector', () => {
describe('recent boards section', () => {
it('shows only when boards are greater than 10', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- boards,
- });
-
await nextTick();
+ expect(projectRecentBoardsQueryHandlerSuccess).toHaveBeenCalled();
expect(getDropdownHeaders()).toHaveLength(2);
});
it('does not show when boards are less than 10', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- boards: boards.slice(0, 5),
- });
+ createComponent({ projectBoardsQueryHandler: smallBoardsQueryHandlerSuccess });
await nextTick();
expect(getDropdownHeaders()).toHaveLength(0);
});
- it('does not show when recentBoards api returns empty array', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- recentBoards: [],
+ it('does not show when recentIssueBoards api returns empty array', async () => {
+ createComponent({
+ projectRecentBoardsQueryHandler: emptyRecentBoardsQueryHandlerSuccess,
});
await nextTick();
@@ -256,15 +227,39 @@ describe('BoardsSelector', () => {
});
});
+ describe('fetching all boards', () => {
+ it.each`
+ boardType | queryHandler | notCalledHandler
+ ${BoardType.group} | ${groupBoardsQueryHandlerSuccess} | ${projectBoardsQueryHandlerSuccess}
+ ${BoardType.project} | ${projectBoardsQueryHandlerSuccess} | ${groupBoardsQueryHandlerSuccess}
+ `('fetches $boardType boards', async ({ boardType, queryHandler, notCalledHandler }) => {
+ createStore({
+ isProjectBoard: boardType === BoardType.project,
+ isGroupBoard: boardType === BoardType.group,
+ });
+ createComponent();
+
+ await nextTick();
+
+ // Emits gl-dropdown show event to simulate the dropdown is opened at initialization time
+ findDropdown().vm.$emit('show');
+
+ await nextTick();
+
+ expect(queryHandler).toHaveBeenCalled();
+ expect(notCalledHandler).not.toHaveBeenCalled();
+ });
+ });
+
describe('fetching current board', () => {
it.each`
- boardType | queryHandler | notCalledHandler
- ${'group'} | ${groupBoardQueryHandlerSuccess} | ${projectBoardQueryHandlerSuccess}
- ${'project'} | ${projectBoardQueryHandlerSuccess} | ${groupBoardQueryHandlerSuccess}
+ boardType | queryHandler | notCalledHandler
+ ${BoardType.group} | ${groupBoardQueryHandlerSuccess} | ${projectBoardQueryHandlerSuccess}
+ ${BoardType.project} | ${projectBoardQueryHandlerSuccess} | ${groupBoardQueryHandlerSuccess}
`('fetches $boardType board', async ({ boardType, queryHandler, notCalledHandler }) => {
createStore({
- isProjectBoard: boardType === 'project',
- isGroupBoard: boardType === 'group',
+ isProjectBoard: boardType === BoardType.project,
+ isGroupBoard: boardType === BoardType.group,
});
createComponent();
diff --git a/spec/frontend/boards/components/sidebar/board_editable_item_spec.js b/spec/frontend/boards/components/sidebar/board_editable_item_spec.js
index 12e9a9ba365..0c76c711b3a 100644
--- a/spec/frontend/boards/components/sidebar/board_editable_item_spec.js
+++ b/spec/frontend/boards/components/sidebar/board_editable_item_spec.js
@@ -1,5 +1,6 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import BoardSidebarItem from '~/boards/components/sidebar/board_editable_item.vue';
describe('boards sidebar remove issue', () => {
@@ -79,17 +80,16 @@ describe('boards sidebar remove issue', () => {
createComponent({ canUpdate: true, slots });
findEditButton().vm.$emit('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(findCollapsed().isVisible()).toBe(false);
- expect(findExpanded().isVisible()).toBe(true);
- });
+ await nextTick();
+ expect(findCollapsed().isVisible()).toBe(false);
+ expect(findExpanded().isVisible()).toBe(true);
});
it('hides the header while editing if `toggleHeader` is true', async () => {
createComponent({ canUpdate: true, props: { toggleHeader: true } });
findEditButton().vm.$emit('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findEditButton().isVisible()).toBe(false);
expect(findTitle().isVisible()).toBe(false);
@@ -101,14 +101,14 @@ describe('boards sidebar remove issue', () => {
beforeEach(async () => {
createComponent({ canUpdate: true });
findEditButton().vm.$emit('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
});
it('hides expanded section and displays collapsed section', async () => {
expect(findExpanded().isVisible()).toBe(true);
document.body.click();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findCollapsed().isVisible()).toBe(true);
expect(findExpanded().isVisible()).toBe(false);
@@ -117,7 +117,7 @@ describe('boards sidebar remove issue', () => {
it('emits events', async () => {
document.body.click();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.emitted().close).toHaveLength(1);
expect(wrapper.emitted()['off-click']).toHaveLength(1);
@@ -129,7 +129,7 @@ describe('boards sidebar remove issue', () => {
findEditButton().vm.$emit('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.emitted().open.length).toBe(1);
});
@@ -139,7 +139,7 @@ describe('boards sidebar remove issue', () => {
findEditButton().vm.$emit('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
wrapper.vm.collapse({ emitEvent: false });
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 4a8eda298f2..5364d929c38 100644
--- a/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js
+++ b/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js
@@ -1,5 +1,6 @@
import { GlAlert, GlFormInput, GlForm } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
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';
@@ -75,7 +76,7 @@ describe('~/boards/components/sidebar/board_sidebar_title.vue', () => {
});
findFormInput().vm.$emit('input', TEST_TITLE);
findForm().vm.$emit('submit', { preventDefault: () => {} });
- await wrapper.vm.$nextTick();
+ await nextTick();
});
it('collapses sidebar and renders new title', () => {
@@ -98,7 +99,7 @@ describe('~/boards/components/sidebar/board_sidebar_title.vue', () => {
jest.spyOn(wrapper.vm, 'setActiveItemTitle').mockImplementation(() => {});
findFormInput().vm.$emit('input', '');
findForm().vm.$emit('submit', { preventDefault: () => {} });
- await wrapper.vm.$nextTick();
+ await nextTick();
});
it('commits change to the server', () => {
@@ -113,7 +114,7 @@ describe('~/boards/components/sidebar/board_sidebar_title.vue', () => {
wrapper.vm.$refs.sidebarItem.expand();
findFormInput().vm.$emit('input', TEST_TITLE);
findEditableItem().vm.$emit('off-click');
- await wrapper.vm.$nextTick();
+ await nextTick();
});
it('does not collapses sidebar and shows alert', () => {
@@ -148,7 +149,7 @@ describe('~/boards/components/sidebar/board_sidebar_title.vue', () => {
});
findFormInput().vm.$emit('input', TEST_TITLE);
findCancelButton().vm.$emit('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
});
it('collapses sidebar and render former title', () => {
@@ -168,7 +169,7 @@ describe('~/boards/components/sidebar/board_sidebar_title.vue', () => {
jest.spyOn(wrapper.vm, 'setError').mockImplementation(() => {});
findFormInput().vm.$emit('input', 'Invalid title');
findForm().vm.$emit('submit', { preventDefault: () => {} });
- await wrapper.vm.$nextTick();
+ await nextTick();
});
it('collapses sidebar and renders former item title', () => {
diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js
index a081a60166b..24096fddea6 100644
--- a/spec/frontend/boards/mock_data.js
+++ b/spec/frontend/boards/mock_data.js
@@ -29,6 +29,85 @@ export const listObj = {
},
};
+function boardGenerator(n) {
+ return new Array(n).fill().map((board, index) => {
+ const id = `${index}`;
+ const name = `board${id}`;
+
+ return {
+ node: {
+ id,
+ name,
+ weight: 0,
+ __typename: 'Board',
+ },
+ };
+ });
+}
+
+export const boards = boardGenerator(20);
+export const recentIssueBoards = boardGenerator(5);
+
+export const mockSmallProjectAllBoardsResponse = {
+ data: {
+ project: {
+ id: 'gid://gitlab/Project/114',
+ boards: { edges: boardGenerator(3) },
+ __typename: 'Project',
+ },
+ },
+};
+
+export const mockEmptyProjectRecentBoardsResponse = {
+ data: {
+ project: {
+ id: 'gid://gitlab/Project/114',
+ recentIssueBoards: { edges: [] },
+ __typename: 'Project',
+ },
+ },
+};
+
+export const mockGroupAllBoardsResponse = {
+ data: {
+ group: {
+ id: 'gid://gitlab/Group/114',
+ boards: { edges: boards },
+ __typename: 'Group',
+ },
+ },
+};
+
+export const mockProjectAllBoardsResponse = {
+ data: {
+ project: {
+ id: 'gid://gitlab/Project/1',
+ boards: { edges: boards },
+ __typename: 'Project',
+ },
+ },
+};
+
+export const mockGroupRecentBoardsResponse = {
+ data: {
+ group: {
+ id: 'gid://gitlab/Group/114',
+ recentIssueBoards: { edges: recentIssueBoards },
+ __typename: 'Group',
+ },
+ },
+};
+
+export const mockProjectRecentBoardsResponse = {
+ data: {
+ project: {
+ id: 'gid://gitlab/Project/1',
+ recentIssueBoards: { edges: recentIssueBoards },
+ __typename: 'Project',
+ },
+ },
+};
+
export const mockGroupBoardResponse = {
data: {
workspace: {
@@ -612,6 +691,7 @@ export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones, isSignedI
title: __('Milestone'),
symbol: '%',
type: 'milestone',
+ shouldSkipSort: true,
token: MilestoneToken,
unique: true,
fetchMilestones,
diff --git a/spec/frontend/boards/project_select_spec.js b/spec/frontend/boards/project_select_spec.js
index de823094630..05dc7d28eaa 100644
--- a/spec/frontend/boards/project_select_spec.js
+++ b/spec/frontend/boards/project_select_spec.js
@@ -1,6 +1,6 @@
import { GlDropdown, GlDropdownItem, GlSearchBoxByType, GlLoadingIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import ProjectSelect from '~/boards/components/project_select.vue';
import defaultState from '~/boards/stores/state';
@@ -88,7 +88,7 @@ describe('ProjectSelect component', () => {
expect(findGlDropdownLoadingIcon().exists()).toBe(true);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findGlDropdownLoadingIcon().exists()).toBe(false);
});
diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js
index 7c842d71688..0eca0cb3ee5 100644
--- a/spec/frontend/boards/stores/actions_spec.js
+++ b/spec/frontend/boards/stores/actions_spec.js
@@ -315,14 +315,14 @@ describe('fetchMilestones', () => {
'project',
{
query: projectBoardMilestones,
- variables: { fullPath: 'gitlab-org/gitlab', state: 'active' },
+ variables: { fullPath: 'gitlab-org/gitlab' },
},
],
[
'group',
{
query: groupBoardMilestones,
- variables: { fullPath: 'gitlab-org/gitlab', state: 'active' },
+ variables: { fullPath: 'gitlab-org/gitlab' },
},
],
])(
diff --git a/spec/frontend/broadcast_notification_spec.js b/spec/frontend/broadcast_notification_spec.js
index 8d433946632..cd947cd417a 100644
--- a/spec/frontend/broadcast_notification_spec.js
+++ b/spec/frontend/broadcast_notification_spec.js
@@ -30,6 +30,7 @@ describe('broadcast message on dismiss', () => {
expect(Cookies.set).toHaveBeenCalledWith('hide_broadcast_message_1', true, {
expires: new Date(endsAt),
+ secure: false,
});
});
});
diff --git a/spec/frontend/captcha/apollo_captcha_link_spec.js b/spec/frontend/captcha/apollo_captcha_link_spec.js
index e7ff4812ee7..eab52344d1f 100644
--- a/spec/frontend/captcha/apollo_captcha_link_spec.js
+++ b/spec/frontend/captcha/apollo_captcha_link_spec.js
@@ -1,4 +1,4 @@
-import { ApolloLink, Observable } from 'apollo-link';
+import { ApolloLink, Observable } from '@apollo/client/core';
import { apolloCaptchaLink } from '~/captcha/apollo_captcha_link';
import UnsolvedCaptchaError from '~/captcha/unsolved_captcha_error';
diff --git a/spec/frontend/ci_lint/components/ci_lint_spec.js b/spec/frontend/ci_lint/components/ci_lint_spec.js
index c4b2927764e..0ad6ed56b0e 100644
--- a/spec/frontend/ci_lint/components/ci_lint_spec.js
+++ b/spec/frontend/ci_lint/components/ci_lint_spec.js
@@ -1,5 +1,6 @@
import { GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
import CiLint from '~/ci_lint/components/ci_lint.vue';
import CiLintResults from '~/pipeline_editor/components/lint/ci_lint_results.vue';
@@ -81,7 +82,7 @@ describe('CI Lint', () => {
it('validation displays results', async () => {
findValidateBtn().vm.$emit('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findValidateBtn().props('loading')).toBe(true);
@@ -96,7 +97,7 @@ describe('CI Lint', () => {
findValidateBtn().vm.$emit('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findValidateBtn().props('loading')).toBe(true);
diff --git a/spec/frontend/ci_settings_pipeline_triggers/components/triggers_list_spec.js b/spec/frontend/ci_settings_pipeline_triggers/components/triggers_list_spec.js
index 41af257ad89..6bf28a67300 100644
--- a/spec/frontend/ci_settings_pipeline_triggers/components/triggers_list_spec.js
+++ b/spec/frontend/ci_settings_pipeline_triggers/components/triggers_list_spec.js
@@ -1,5 +1,6 @@
import { GlTable, GlBadge } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import TriggersList from '~/ci_settings_pipeline_triggers/components/triggers_list.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -25,10 +26,10 @@ describe('TriggersList', () => {
const findEditBtn = (i) => findRowAt(i).find('[data-testid="edit-btn"]');
const findRevokeBtn = (i) => findRowAt(i).find('[data-testid="trigger_revoke_button"]');
- beforeEach(() => {
+ beforeEach(async () => {
createComponent();
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('displays a table with expected headers', () => {
diff --git a/spec/frontend/ci_variable_list/components/ci_environments_dropdown_spec.js b/spec/frontend/ci_variable_list/components/ci_environments_dropdown_spec.js
index 5c5ea102f12..e7e4897abfa 100644
--- a/spec/frontend/ci_variable_list/components/ci_environments_dropdown_spec.js
+++ b/spec/frontend/ci_variable_list/components/ci_environments_dropdown_spec.js
@@ -1,10 +1,10 @@
import { GlDropdown, GlDropdownItem, GlIcon } from '@gitlab/ui';
-import { mount, createLocalVue } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import CiEnvironmentsDropdown from '~/ci_variable_list/components/ci_environments_dropdown.vue';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('Ci environments dropdown', () => {
let wrapper;
@@ -22,7 +22,6 @@ describe('Ci environments dropdown', () => {
wrapper = mount(CiEnvironmentsDropdown, {
store,
- localVue,
propsData: {
value: term,
},
@@ -74,7 +73,7 @@ describe('Ci environments dropdown', () => {
describe('Environments found', () => {
beforeEach(async () => {
createComponent('prod');
- await wrapper.vm.$nextTick();
+ await nextTick();
});
it('renders only the environment searched for', () => {
@@ -111,7 +110,7 @@ describe('Ci environments dropdown', () => {
it('should emit createClicked if an environment is clicked', async () => {
createComponent('newscope');
- await wrapper.vm.$nextTick();
+ await nextTick();
findDropdownItemByIndex(1).vm.$emit('click');
expect(wrapper.emitted('createClicked')).toEqual([['newscope']]);
});
diff --git a/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js b/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js
index 7c4ff67feb3..085ab1c0c30 100644
--- a/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js
+++ b/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js
@@ -1,5 +1,6 @@
import { GlButton, GlFormInput } from '@gitlab/ui';
-import { createLocalVue, shallowMount, mount } from '@vue/test-utils';
+import { shallowMount, mount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import { mockTracking } from 'helpers/tracking_helper';
import CiEnvironmentsDropdown from '~/ci_variable_list/components/ci_environments_dropdown.vue';
@@ -9,8 +10,7 @@ import createStore from '~/ci_variable_list/store';
import mockData from '../services/mock_data';
import ModalStub from '../stubs';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('Ci variable modal', () => {
let wrapper;
@@ -26,7 +26,6 @@ describe('Ci variable modal', () => {
stubs: {
GlModal: ModalStub,
},
- localVue,
store,
...options,
});
diff --git a/spec/frontend/ci_variable_list/components/ci_variable_settings_spec.js b/spec/frontend/ci_variable_list/components/ci_variable_settings_spec.js
index 03f90f72d87..13e417940a8 100644
--- a/spec/frontend/ci_variable_list/components/ci_variable_settings_spec.js
+++ b/spec/frontend/ci_variable_list/components/ci_variable_settings_spec.js
@@ -1,10 +1,10 @@
-import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import CiVariableSettings from '~/ci_variable_list/components/ci_variable_settings.vue';
import createStore from '~/ci_variable_list/store';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('Ci variable table', () => {
let wrapper;
@@ -16,7 +16,6 @@ describe('Ci variable table', () => {
store.state.isGroup = groupState;
jest.spyOn(store, 'dispatch').mockImplementation();
wrapper = shallowMount(CiVariableSettings, {
- localVue,
store,
});
};
diff --git a/spec/frontend/ci_variable_list/components/ci_variable_table_spec.js b/spec/frontend/ci_variable_list/components/ci_variable_table_spec.js
index 8367c3f6bb8..62f9ae4eb4e 100644
--- a/spec/frontend/ci_variable_list/components/ci_variable_table_spec.js
+++ b/spec/frontend/ci_variable_list/components/ci_variable_table_spec.js
@@ -1,11 +1,11 @@
-import { createLocalVue, mount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import CiVariableTable from '~/ci_variable_list/components/ci_variable_table.vue';
import createStore from '~/ci_variable_list/store';
import mockData from '../services/mock_data';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('Ci variable table', () => {
let wrapper;
@@ -14,16 +14,15 @@ describe('Ci variable table', () => {
const createComponent = () => {
store = createStore();
jest.spyOn(store, 'dispatch').mockImplementation();
- wrapper = mount(CiVariableTable, {
+ wrapper = mountExtended(CiVariableTable, {
attachTo: document.body,
- localVue,
store,
});
};
- const findRevealButton = () => wrapper.find({ ref: 'secret-value-reveal-button' });
- const findEditButton = () => wrapper.find({ ref: 'edit-ci-variable' });
- const findEmptyVariablesPlaceholder = () => wrapper.find({ ref: 'empty-variables' });
+ const findRevealButton = () => wrapper.findByText('Reveal values');
+ const findEditButton = () => wrapper.findByLabelText('Edit');
+ const findEmptyVariablesPlaceholder = () => wrapper.findByText('There are no variables yet.');
beforeEach(() => {
createComponent();
@@ -37,18 +36,35 @@ describe('Ci variable table', () => {
expect(store.dispatch).toHaveBeenCalledWith('fetchVariables');
});
- describe('Renders correct data', () => {
- it('displays empty message when variables are not present', () => {
+ describe('When table is empty', () => {
+ beforeEach(() => {
+ store.state.variables = [];
+ });
+
+ it('displays empty message', () => {
expect(findEmptyVariablesPlaceholder().exists()).toBe(true);
});
- it('displays correct amount of variables present and no empty message', () => {
+ it('hides the reveal button', () => {
+ expect(findRevealButton().exists()).toBe(false);
+ });
+ });
+
+ describe('When table has variables', () => {
+ beforeEach(() => {
store.state.variables = mockData.mockVariables;
+ });
+
+ it('does not display the empty message', () => {
+ expect(findEmptyVariablesPlaceholder().exists()).toBe(false);
+ });
+
+ it('displays the reveal button', () => {
+ expect(findRevealButton().exists()).toBe(true);
+ });
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.findAll('.js-ci-variable-row').length).toBe(1);
- expect(findEmptyVariablesPlaceholder().exists()).toBe(false);
- });
+ it('displays the correct amount of variables', async () => {
+ expect(wrapper.findAll('.js-ci-variable-row')).toHaveLength(1);
});
});
diff --git a/spec/frontend/clusters/agents/components/activity_events_list_spec.js b/spec/frontend/clusters/agents/components/activity_events_list_spec.js
index 4abbd77dfb7..6b374b6620d 100644
--- a/spec/frontend/clusters/agents/components/activity_events_list_spec.js
+++ b/spec/frontend/clusters/agents/components/activity_events_list_spec.js
@@ -70,8 +70,9 @@ describe('ActivityEvents', () => {
});
describe('when there are no agentEvents', () => {
- beforeEach(() => {
+ beforeEach(async () => {
createWrapper({ queryResponse: jest.fn().mockResolvedValue(mockEmptyResponse) });
+ await waitForPromises();
});
it('displays an empty state with the correct illustration', () => {
@@ -83,9 +84,11 @@ describe('ActivityEvents', () => {
describe('when the agentEvents are present', () => {
const length = mockResponse.data?.project?.clusterAgent?.activityEvents?.nodes?.length;
- beforeEach(() => {
+ beforeEach(async () => {
createWrapper();
+ await waitForPromises();
});
+
it('renders an activity-history-item components for every event', () => {
expect(findAllActivityHistoryItems()).toHaveLength(length);
});
diff --git a/spec/frontend/clusters/agents/components/show_spec.js b/spec/frontend/clusters/agents/components/show_spec.js
index 2a3c11f4b47..f2f073544e3 100644
--- a/spec/frontend/clusters/agents/components/show_spec.js
+++ b/spec/frontend/clusters/agents/components/show_spec.js
@@ -11,12 +11,14 @@ import { useFakeDate } from 'helpers/fake_date';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import { MAX_LIST_COUNT, TOKEN_STATUS_ACTIVE } from '~/clusters/agents/constants';
const localVue = createLocalVue();
localVue.use(VueApollo);
describe('ClusterAgentShow', () => {
let wrapper;
+ let agentQueryResponse;
useFakeDate([2021, 2, 15]);
const provide = {
@@ -40,7 +42,7 @@ describe('ClusterAgentShow', () => {
};
const createWrapper = ({ clusterAgent, queryResponse = null }) => {
- const agentQueryResponse =
+ agentQueryResponse =
queryResponse ||
jest.fn().mockResolvedValue({ data: { project: { id: 'project-1', clusterAgent } } });
const apolloProvider = createMockApollo([[getAgentQuery, agentQueryResponse]]);
@@ -80,8 +82,21 @@ describe('ClusterAgentShow', () => {
});
describe('default behaviour', () => {
- beforeEach(() => {
- return createWrapper({ clusterAgent: defaultClusterAgent });
+ beforeEach(async () => {
+ createWrapper({ clusterAgent: defaultClusterAgent });
+ await waitForPromises();
+ });
+
+ it('sends expected params', () => {
+ const variables = {
+ agentName: provide.agentName,
+ projectPath: provide.projectPath,
+ tokenStatus: TOKEN_STATUS_ACTIVE,
+ first: MAX_LIST_COUNT,
+ last: null,
+ };
+
+ expect(agentQueryResponse).toHaveBeenCalledWith(variables);
});
it('displays the agent name', () => {
@@ -117,11 +132,13 @@ describe('ClusterAgentShow', () => {
createdByUser: null,
};
- beforeEach(() => {
- return createWrapper({ clusterAgent: missingUser });
+ beforeEach(async () => {
+ createWrapper({ clusterAgent: missingUser });
+ await waitForPromises();
});
- it('displays agent create information with unknown user', () => {
+ it('displays agent create information with unknown user', async () => {
+ await waitForPromises();
expect(findCreatedText()).toMatchInterpolatedText('Created by Unknown user 2 days ago');
});
});
@@ -132,23 +149,30 @@ describe('ClusterAgentShow', () => {
tokens: null,
};
- beforeEach(() => {
- return createWrapper({ clusterAgent: missingTokens });
+ beforeEach(async () => {
+ createWrapper({ clusterAgent: missingTokens });
+ await waitForPromises();
});
- it('displays token header with no count', () => {
+ it('displays token header with no count', async () => {
+ await waitForPromises();
expect(findTokenCount()).toMatchInterpolatedText(`${ClusterAgentShow.i18n.tokens}`);
});
});
describe('when the token list has additional pages', () => {
- const pageInfo = {
+ const pageInfoResponse = {
hasNextPage: true,
hasPreviousPage: false,
startCursor: 'prev',
endCursor: 'next',
};
+ const pageInfo = {
+ ...pageInfoResponse,
+ __typename: 'PageInfo',
+ };
+
const tokenPagination = {
...defaultClusterAgent,
tokens: {
@@ -157,8 +181,9 @@ describe('ClusterAgentShow', () => {
},
};
- beforeEach(() => {
- return createWrapper({ clusterAgent: tokenPagination });
+ beforeEach(async () => {
+ createWrapper({ clusterAgent: tokenPagination });
+ await waitForPromises();
});
it('should render pagination buttons', () => {
@@ -166,7 +191,7 @@ describe('ClusterAgentShow', () => {
});
it('should pass pageInfo to the pagination component', () => {
- expect(findPaginationButtons().props()).toMatchObject(pageInfo);
+ expect(findPaginationButtons().props()).toMatchObject(pageInfoResponse);
});
});
diff --git a/spec/frontend/clusters/components/new_cluster_spec.js b/spec/frontend/clusters/components/new_cluster_spec.js
index e4bca5eaaa5..b73442f6ec3 100644
--- a/spec/frontend/clusters/components/new_cluster_spec.js
+++ b/spec/frontend/clusters/components/new_cluster_spec.js
@@ -1,5 +1,6 @@
import { GlLink, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import NewCluster from '~/clusters/components/new_cluster.vue';
import createClusterStore from '~/clusters/stores/new_cluster';
@@ -7,10 +8,10 @@ describe('NewCluster', () => {
let store;
let wrapper;
- const createWrapper = () => {
+ const createWrapper = async () => {
store = createClusterStore({ clusterConnectHelpPath: '/some/help/path' });
wrapper = shallowMount(NewCluster, { store, stubs: { GlLink, GlSprintf } });
- return wrapper.vm.$nextTick();
+ await nextTick();
};
const findDescription = () => wrapper.find(GlSprintf);
diff --git a/spec/frontend/clusters/components/remove_cluster_confirmation_spec.js b/spec/frontend/clusters/components/remove_cluster_confirmation_spec.js
index 41bd492148e..173fefe6167 100644
--- a/spec/frontend/clusters/components/remove_cluster_confirmation_spec.js
+++ b/spec/frontend/clusters/components/remove_cluster_confirmation_spec.js
@@ -1,5 +1,6 @@
import { GlModal, GlSprintf } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import { stubComponent } from 'helpers/stub_component';
import RemoveClusterConfirmation from '~/clusters/components/remove_cluster_confirmation.vue';
import SplitButton from '~/vue_shared/components/split_button.vue';
@@ -43,7 +44,7 @@ describe('Remove cluster confirmation modal', () => {
it('opens modal with "cleanup" option', async () => {
findSplitButton().vm.$emit('remove-cluster-and-cleanup');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findModal().vm.show).toHaveBeenCalled();
expect(wrapper.vm.confirmCleanup).toEqual(true);
@@ -55,7 +56,7 @@ describe('Remove cluster confirmation modal', () => {
it('opens modal without "cleanup" option', async () => {
findSplitButton().vm.$emit('remove-cluster');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findModal().vm.show).toHaveBeenCalled();
expect(wrapper.vm.confirmCleanup).toEqual(false);
diff --git a/spec/frontend/clusters/forms/components/integration_form_spec.js b/spec/frontend/clusters/forms/components/integration_form_spec.js
index d041cd1e164..dd278bcd2ce 100644
--- a/spec/frontend/clusters/forms/components/integration_form_spec.js
+++ b/spec/frontend/clusters/forms/components/integration_form_spec.js
@@ -1,11 +1,11 @@
import { GlToggle, GlButton } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import IntegrationForm from '~/clusters/forms/components/integration_form.vue';
import { createStore } from '~/clusters/forms/stores/index';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('ClusterIntegrationForm', () => {
let wrapper;
@@ -19,7 +19,6 @@ describe('ClusterIntegrationForm', () => {
const createWrapper = (storeValues = defaultStoreValues) => {
wrapper = shallowMount(IntegrationForm, {
- localVue,
store: createStore(storeValues),
provide: {
autoDevopsHelpPath: 'topics/autodevops/index',
@@ -76,32 +75,22 @@ describe('ClusterIntegrationForm', () => {
describe('reactivity', () => {
beforeEach(() => createWrapper());
- it('enables the submit button on changing toggle to different value', () => {
- return wrapper.vm
- .$nextTick()
- .then(() => {
- // setData is a bad approach because it changes the internal implementation which we should not touch
- // but our GlFormInput lacks the ability to set a new value.
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ toggleEnabled: !defaultStoreValues.enabled });
- })
- .then(() => {
- expect(findSubmitButton().props('disabled')).toBe(false);
- });
+ it('enables the submit button on changing toggle to different value', async () => {
+ await nextTick();
+ // setData is a bad approach because it changes the internal implementation which we should not touch
+ // but our GlFormInput lacks the ability to set a new value.
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
+ await wrapper.setData({ toggleEnabled: !defaultStoreValues.enabled });
+ expect(findSubmitButton().props('disabled')).toBe(false);
});
- it('enables the submit button on changing input values', () => {
- return wrapper.vm
- .$nextTick()
- .then(() => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ envScope: `${defaultStoreValues.environmentScope}1` });
- })
- .then(() => {
- expect(findSubmitButton().props('disabled')).toBe(false);
- });
+ it('enables the submit button on changing input values', async () => {
+ await nextTick();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
+ await wrapper.setData({ envScope: `${defaultStoreValues.environmentScope}1` });
+ expect(findSubmitButton().props('disabled')).toBe(false);
});
});
});
diff --git a/spec/frontend/clusters_list/components/agent_table_spec.js b/spec/frontend/clusters_list/components/agent_table_spec.js
index 887c17bb4ad..dc7f0ebae74 100644
--- a/spec/frontend/clusters_list/components/agent_table_spec.js
+++ b/spec/frontend/clusters_list/components/agent_table_spec.js
@@ -1,65 +1,32 @@
import { GlLink, GlIcon } from '@gitlab/ui';
+import { sprintf } from '~/locale';
import AgentTable from '~/clusters_list/components/agent_table.vue';
-import AgentOptions from '~/clusters_list/components/agent_options.vue';
-import { ACTIVE_CONNECTION_TIME } from '~/clusters_list/constants';
+import DeleteAgentButton from '~/clusters_list/components/delete_agent_button.vue';
+import { I18N_AGENT_TABLE } from '~/clusters_list/constants';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { stubComponent } from 'helpers/stub_component';
import timeagoMixin from '~/vue_shared/mixins/timeago';
-
-const connectedTimeNow = new Date();
-const connectedTimeInactive = new Date(connectedTimeNow.getTime() - ACTIVE_CONNECTION_TIME);
+import { clusterAgents, connectedTimeNow, connectedTimeInactive } from './mock_data';
const provideData = {
- projectPath: 'path/to/project',
+ gitlabVersion: '14.8',
};
const propsData = {
- agents: [
- {
- name: 'agent-1',
- id: 'agent-1-id',
- configFolder: {
- webPath: '/agent/full/path',
- },
- webPath: '/agent-1',
- status: 'unused',
- lastContact: null,
- tokens: null,
- },
- {
- name: 'agent-2',
- id: 'agent-2-id',
- webPath: '/agent-2',
- status: 'active',
- lastContact: connectedTimeNow.getTime(),
- tokens: {
- nodes: [
- {
- lastUsedAt: connectedTimeNow,
- },
- ],
- },
- },
- {
- name: 'agent-3',
- id: 'agent-3-id',
- webPath: '/agent-3',
- status: 'inactive',
- lastContact: connectedTimeInactive.getTime(),
- tokens: {
- nodes: [
- {
- lastUsedAt: connectedTimeInactive,
- },
- ],
- },
- },
- ],
+ agents: clusterAgents,
};
-const AgentOptionsStub = stubComponent(AgentOptions, {
+const DeleteAgentButtonStub = stubComponent(DeleteAgentButton, {
template: `<div></div>`,
});
+const outdatedTitle = I18N_AGENT_TABLE.versionOutdatedTitle;
+const mismatchTitle = I18N_AGENT_TABLE.versionMismatchTitle;
+const mismatchOutdatedTitle = I18N_AGENT_TABLE.versionMismatchOutdatedTitle;
+const outdatedText = sprintf(I18N_AGENT_TABLE.versionOutdatedText, {
+ version: provideData.gitlabVersion,
+});
+const mismatchText = I18N_AGENT_TABLE.versionMismatchText;
+
describe('AgentTable', () => {
let wrapper;
@@ -67,16 +34,17 @@ describe('AgentTable', () => {
const findStatusIcon = (at) => wrapper.findAllComponents(GlIcon).at(at);
const findStatusText = (at) => wrapper.findAllByTestId('cluster-agent-connection-status').at(at);
const findLastContactText = (at) => wrapper.findAllByTestId('cluster-agent-last-contact').at(at);
+ const findVersionText = (at) => wrapper.findAllByTestId('cluster-agent-version').at(at);
const findConfiguration = (at) =>
wrapper.findAllByTestId('cluster-agent-configuration-link').at(at);
- const findAgentOptions = () => wrapper.findAllComponents(AgentOptions);
+ const findDeleteAgentButton = () => wrapper.findAllComponents(DeleteAgentButton);
beforeEach(() => {
wrapper = mountExtended(AgentTable, {
propsData,
provide: provideData,
stubs: {
- AgentOptions: AgentOptionsStub,
+ DeleteAgentButton: DeleteAgentButtonStub,
},
});
});
@@ -92,7 +60,7 @@ describe('AgentTable', () => {
agentName | link | lineNumber
${'agent-1'} | ${'/agent-1'} | ${0}
${'agent-2'} | ${'/agent-2'} | ${1}
- `('displays agent link', ({ agentName, link, lineNumber }) => {
+ `('displays agent link for $agentName', ({ agentName, link, lineNumber }) => {
expect(findAgentLink(lineNumber).text()).toBe(agentName);
expect(findAgentLink(lineNumber).attributes('href')).toBe(link);
});
@@ -102,33 +70,92 @@ describe('AgentTable', () => {
${'Never connected'} | ${'status-neutral'} | ${0}
${'Connected'} | ${'status-success'} | ${1}
${'Not connected'} | ${'severity-critical'} | ${2}
- `('displays agent connection status', ({ status, iconName, lineNumber }) => {
- expect(findStatusText(lineNumber).text()).toBe(status);
- expect(findStatusIcon(lineNumber).props('name')).toBe(iconName);
- });
+ `(
+ 'displays agent connection status as "$status" at line $lineNumber',
+ ({ status, iconName, lineNumber }) => {
+ expect(findStatusText(lineNumber).text()).toBe(status);
+ expect(findStatusIcon(lineNumber).props('name')).toBe(iconName);
+ },
+ );
it.each`
lastContact | lineNumber
${'Never'} | ${0}
${timeagoMixin.methods.timeFormatted(connectedTimeNow)} | ${1}
${timeagoMixin.methods.timeFormatted(connectedTimeInactive)} | ${2}
- `('displays agent last contact time', ({ lastContact, lineNumber }) => {
- expect(findLastContactText(lineNumber).text()).toBe(lastContact);
- });
+ `(
+ 'displays agent last contact time as "$lastContact" at line $lineNumber',
+ ({ lastContact, lineNumber }) => {
+ expect(findLastContactText(lineNumber).text()).toBe(lastContact);
+ },
+ );
+
+ describe.each`
+ agent | version | podsNumber | versionMismatch | versionOutdated | title | texts | lineNumber
+ ${'agent-1'} | ${''} | ${1} | ${false} | ${false} | ${''} | ${''} | ${0}
+ ${'agent-2'} | ${'14.8'} | ${2} | ${false} | ${false} | ${''} | ${''} | ${1}
+ ${'agent-3'} | ${'14.5'} | ${1} | ${false} | ${true} | ${outdatedTitle} | ${[outdatedText]} | ${2}
+ ${'agent-4'} | ${'14.7'} | ${2} | ${true} | ${false} | ${mismatchTitle} | ${[mismatchText]} | ${3}
+ ${'agent-5'} | ${'14.3'} | ${2} | ${true} | ${true} | ${mismatchOutdatedTitle} | ${[mismatchText, outdatedText]} | ${4}
+ `(
+ 'agent version column at line $lineNumber',
+ ({
+ agent,
+ version,
+ podsNumber,
+ versionMismatch,
+ versionOutdated,
+ title,
+ texts,
+ lineNumber,
+ }) => {
+ const findIcon = () => findVersionText(lineNumber).find(GlIcon);
+ const findPopover = () => wrapper.findByTestId(`popover-${agent}`);
+ const versionWarning = versionMismatch || versionOutdated;
+
+ it('shows the correct agent version', () => {
+ expect(findVersionText(lineNumber).text()).toBe(version);
+ });
+
+ if (versionWarning) {
+ it(`shows a warning icon when agent versions mismatch is ${versionMismatch} and outdated is ${versionOutdated} and the number of pods is ${podsNumber}`, () => {
+ expect(findIcon().props('name')).toBe('warning');
+ });
+
+ it(`renders correct title for the popover when agent versions mismatch is ${versionMismatch} and outdated is ${versionOutdated}`, () => {
+ expect(findPopover().props('title')).toBe(title);
+ });
+
+ it(`renders correct text for the popover when agent versions mismatch is ${versionMismatch} and outdated is ${versionOutdated}`, () => {
+ texts.forEach((text) => {
+ expect(findPopover().text()).toContain(text);
+ });
+ });
+ } else {
+ it(`doesn't show a warning icon with a popover when agent versions mismatch is ${versionMismatch} and outdated is ${versionOutdated} and the number of pods is ${podsNumber}`, () => {
+ expect(findIcon().exists()).toBe(false);
+ expect(findPopover().exists()).toBe(false);
+ });
+ }
+ },
+ );
it.each`
agentPath | hasLink | lineNumber
${'.gitlab/agents/agent-1'} | ${true} | ${0}
${'.gitlab/agents/agent-2'} | ${false} | ${1}
- `('displays config file path', ({ agentPath, hasLink, lineNumber }) => {
- const findLink = findConfiguration(lineNumber).find(GlLink);
+ `(
+ 'displays config file path as "$agentPath" at line $lineNumber',
+ ({ agentPath, hasLink, lineNumber }) => {
+ const findLink = findConfiguration(lineNumber).find(GlLink);
- expect(findLink.exists()).toBe(hasLink);
- expect(findConfiguration(lineNumber).text()).toBe(agentPath);
- });
+ expect(findLink.exists()).toBe(hasLink);
+ expect(findConfiguration(lineNumber).text()).toBe(agentPath);
+ },
+ );
it('displays actions menu for each agent', () => {
- expect(findAgentOptions()).toHaveLength(3);
+ expect(findDeleteAgentButton()).toHaveLength(5);
});
});
});
diff --git a/spec/frontend/clusters_list/components/agents_spec.js b/spec/frontend/clusters_list/components/agents_spec.js
index c9ca10f6bf7..3cfa4b92bc0 100644
--- a/spec/frontend/clusters_list/components/agents_spec.js
+++ b/spec/frontend/clusters_list/components/agents_spec.js
@@ -1,12 +1,18 @@
-import { GlAlert, GlKeysetPagination, GlLoadingIcon } from '@gitlab/ui';
+import { GlAlert, GlKeysetPagination, GlLoadingIcon, GlBanner } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
+import { nextTick } from 'vue';
import AgentEmptyState from '~/clusters_list/components/agent_empty_state.vue';
import AgentTable from '~/clusters_list/components/agent_table.vue';
import Agents from '~/clusters_list/components/agents.vue';
-import { ACTIVE_CONNECTION_TIME } from '~/clusters_list/constants';
+import {
+ ACTIVE_CONNECTION_TIME,
+ AGENT_FEEDBACK_KEY,
+ AGENT_FEEDBACK_ISSUE,
+} from '~/clusters_list/constants';
import getAgentsQuery from '~/clusters_list/graphql/queries/get_agents.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
+import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
const localVue = createLocalVue();
localVue.use(VueApollo);
@@ -21,13 +27,26 @@ describe('Agents', () => {
projectPath: 'path/to/project',
};
- const createWrapper = ({ props = {}, agents = [], pageInfo = null, trees = [], count = 0 }) => {
+ const createWrapper = async ({
+ props = {},
+ glFeatures = {},
+ agents = [],
+ pageInfo = null,
+ trees = [],
+ count = 0,
+ }) => {
const provide = provideData;
const apolloQueryResponse = {
data: {
project: {
id: '1',
- clusterAgents: { nodes: agents, pageInfo, tokens: { nodes: [] }, count },
+ clusterAgents: {
+ nodes: agents,
+ pageInfo,
+ connections: { nodes: [] },
+ tokens: { nodes: [] },
+ count,
+ },
repository: { tree: { trees: { nodes: trees, pageInfo } } },
},
},
@@ -44,35 +63,48 @@ describe('Agents', () => {
...defaultProps,
...props,
},
- provide: provideData,
+ provide: {
+ ...provideData,
+ glFeatures,
+ },
+ stubs: {
+ GlBanner,
+ LocalStorageSync,
+ },
});
- return wrapper.vm.$nextTick();
+ await nextTick();
};
- const findAgentTable = () => wrapper.find(AgentTable);
- const findEmptyState = () => wrapper.find(AgentEmptyState);
- const findPaginationButtons = () => wrapper.find(GlKeysetPagination);
+ const findAgentTable = () => wrapper.findComponent(AgentTable);
+ const findEmptyState = () => wrapper.findComponent(AgentEmptyState);
+ const findPaginationButtons = () => wrapper.findComponent(GlKeysetPagination);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findBanner = () => wrapper.findComponent(GlBanner);
afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- }
+ wrapper.destroy();
+
+ localStorage.removeItem(AGENT_FEEDBACK_KEY);
});
describe('when there is a list of agents', () => {
let testDate = new Date();
const agents = [
{
+ __typename: 'ClusterAgent',
id: '1',
name: 'agent-1',
webPath: '/agent-1',
+ connections: null,
tokens: null,
},
{
+ __typename: 'ClusterAgent',
id: '2',
name: 'agent-2',
webPath: '/agent-2',
+ connections: null,
tokens: {
nodes: [
{
@@ -103,6 +135,7 @@ describe('Agents', () => {
configFolder: undefined,
status: 'unused',
lastContact: null,
+ connections: null,
tokens: null,
},
{
@@ -116,6 +149,7 @@ describe('Agents', () => {
webPath: '/agent-2',
status: 'active',
lastContact: new Date(testDate).getTime(),
+ connections: null,
tokens: {
nodes: [
{
@@ -143,6 +177,49 @@ describe('Agents', () => {
expect(wrapper.emitted().onAgentsLoad).toEqual([[count]]);
});
+ describe.each`
+ featureFlagEnabled | localStorageItemExists | bannerShown
+ ${true} | ${false} | ${true}
+ ${true} | ${true} | ${false}
+ ${false} | ${true} | ${false}
+ ${false} | ${false} | ${false}
+ `(
+ 'when the feature flag enabled is $featureFlagEnabled and dismissed localStorage item exists is $localStorageItemExists',
+ ({ featureFlagEnabled, localStorageItemExists, bannerShown }) => {
+ const glFeatures = {
+ showGitlabAgentFeedback: featureFlagEnabled,
+ };
+ beforeEach(() => {
+ if (localStorageItemExists) {
+ localStorage.setItem(AGENT_FEEDBACK_KEY, true);
+ }
+
+ return createWrapper({ glFeatures, agents, count, trees });
+ });
+
+ it(`should ${bannerShown ? 'show' : 'hide'} the feedback banner`, () => {
+ expect(findBanner().exists()).toBe(bannerShown);
+ });
+ },
+ );
+
+ describe('when the agent feedback banner is present', () => {
+ const glFeatures = {
+ showGitlabAgentFeedback: true,
+ };
+ beforeEach(() => {
+ return createWrapper({ glFeatures, agents, count, trees });
+ });
+
+ it('should render the correct title', () => {
+ expect(findBanner().props('title')).toBe('Tell us what you think');
+ });
+
+ it('should render the correct issue link', () => {
+ expect(findBanner().props('buttonLink')).toBe(AGENT_FEEDBACK_ISSUE);
+ });
+ });
+
describe('when the agent has recently connected tokens', () => {
it('should set agent status to active', () => {
expect(findAgentTable().props('agents')).toMatchObject(expectedAgentsList);
@@ -179,7 +256,10 @@ describe('Agents', () => {
beforeEach(() => {
return createWrapper({
agents,
- pageInfo,
+ pageInfo: {
+ ...pageInfo,
+ __typename: 'PageInfo',
+ },
});
});
@@ -216,6 +296,10 @@ describe('Agents', () => {
expect(findAgentTable().exists()).toBe(false);
expect(findEmptyState().exists()).toBe(true);
});
+
+ it('should not show agent feedback alert', () => {
+ expect(findAlert().exists()).toBe(false);
+ });
});
describe('when agents query has errored', () => {
@@ -224,7 +308,7 @@ describe('Agents', () => {
});
it('displays an alert message', () => {
- expect(wrapper.find(GlAlert).exists()).toBe(true);
+ expect(findAlert().text()).toBe('An error occurred while loading your Agents');
});
});
@@ -239,14 +323,14 @@ describe('Agents', () => {
},
};
- beforeEach(() => {
+ beforeEach(async () => {
wrapper = shallowMount(Agents, {
mocks,
propsData: defaultProps,
provide: provideData,
});
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('displays a loading icon', () => {
diff --git a/spec/frontend/clusters_list/components/ancestor_notice_spec.js b/spec/frontend/clusters_list/components/ancestor_notice_spec.js
index c7ee2a00f5b..a9f11e6fdf8 100644
--- a/spec/frontend/clusters_list/components/ancestor_notice_spec.js
+++ b/spec/frontend/clusters_list/components/ancestor_notice_spec.js
@@ -1,5 +1,6 @@
import { GlLink, GlSprintf, GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import AncestorNotice from '~/clusters_list/components/ancestor_notice.vue';
import ClusterStore from '~/clusters_list/store';
@@ -7,10 +8,10 @@ describe('ClustersAncestorNotice', () => {
let store;
let wrapper;
- const createWrapper = () => {
+ const createWrapper = async () => {
store = ClusterStore({ ancestorHelperPath: '/some/ancestor/path' });
wrapper = shallowMount(AncestorNotice, { store, stubs: { GlSprintf, GlAlert } });
- return wrapper.vm.$nextTick();
+ await nextTick();
};
beforeEach(() => {
@@ -22,9 +23,9 @@ describe('ClustersAncestorNotice', () => {
});
describe('when cluster does not have ancestors', () => {
- beforeEach(() => {
+ beforeEach(async () => {
store.state.hasAncestorClusters = false;
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('displays no notice', () => {
@@ -33,9 +34,9 @@ describe('ClustersAncestorNotice', () => {
});
describe('when cluster has ancestors', () => {
- beforeEach(() => {
+ beforeEach(async () => {
store.state.hasAncestorClusters = true;
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('displays notice text', () => {
diff --git a/spec/frontend/clusters_list/components/clusters_actions_spec.js b/spec/frontend/clusters_list/components/clusters_actions_spec.js
index cb8303ca4b2..331690fc642 100644
--- a/spec/frontend/clusters_list/components/clusters_actions_spec.js
+++ b/spec/frontend/clusters_list/components/clusters_actions_spec.js
@@ -10,9 +10,10 @@ describe('ClustersActionsComponent', () => {
const newClusterPath = 'path/to/create/cluster';
const addClusterPath = 'path/to/connect/existing/cluster';
- const provideData = {
+ const defaultProvide = {
newClusterPath,
addClusterPath,
+ canAddCluster: true,
};
const findDropdown = () => wrapper.findComponent(GlDropdown);
@@ -21,13 +22,21 @@ describe('ClustersActionsComponent', () => {
const findConnectClusterLink = () => wrapper.findByTestId('connect-cluster-link');
const findConnectNewAgentLink = () => wrapper.findByTestId('connect-new-agent-link');
- beforeEach(() => {
+ const createWrapper = (provideData = {}) => {
wrapper = shallowMountExtended(ClustersActions, {
- provide: provideData,
+ provide: {
+ ...defaultProvide,
+ ...provideData,
+ },
directives: {
GlModalDirective: createMockDirective(),
+ GlTooltip: createMockDirective(),
},
});
+ };
+
+ beforeEach(() => {
+ createWrapper();
});
afterEach(() => {
@@ -52,4 +61,24 @@ describe('ClustersActionsComponent', () => {
expect(binding.value).toBe(INSTALL_AGENT_MODAL_ID);
});
+
+ it('shows tooltip', () => {
+ const tooltip = getBinding(findDropdown().element, 'gl-tooltip');
+ expect(tooltip.value).toBe(CLUSTERS_ACTIONS.connectWithAgent);
+ });
+
+ describe('when user cannot add clusters', () => {
+ beforeEach(() => {
+ createWrapper({ canAddCluster: false });
+ });
+
+ it('disables dropdown', () => {
+ expect(findDropdown().props('disabled')).toBe(true);
+ });
+
+ it('shows tooltip explaining why dropdown is disabled', () => {
+ const tooltip = getBinding(findDropdown().element, 'gl-tooltip');
+ expect(tooltip.value).toBe(CLUSTERS_ACTIONS.dropdownDisabledHint);
+ });
+ });
});
diff --git a/spec/frontend/clusters_list/components/clusters_spec.js b/spec/frontend/clusters_list/components/clusters_spec.js
index 9af25a534d8..82e667093aa 100644
--- a/spec/frontend/clusters_list/components/clusters_spec.js
+++ b/spec/frontend/clusters_list/components/clusters_spec.js
@@ -7,6 +7,7 @@ import {
import * as Sentry from '@sentry/browser';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+import { nextTick } from 'vue';
import Clusters from '~/clusters_list/components/clusters.vue';
import ClustersEmptyState from '~/clusters_list/components/clusters_empty_state.vue';
import ClusterStore from '~/clusters_list/store';
@@ -176,9 +177,9 @@ describe('Clusters', () => {
});
describe('nodes finish loading', () => {
- beforeEach(() => {
+ beforeEach(async () => {
wrapper.vm.$store.state.loadingNodes = false;
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it.each`
diff --git a/spec/frontend/clusters_list/components/clusters_view_all_spec.js b/spec/frontend/clusters_list/components/clusters_view_all_spec.js
index 6ef56beddee..2c1e3d909cc 100644
--- a/spec/frontend/clusters_list/components/clusters_view_all_spec.js
+++ b/spec/frontend/clusters_list/components/clusters_view_all_spec.js
@@ -1,5 +1,5 @@
import { GlCard, GlLoadingIcon, GlButton, GlSprintf, GlBadge } from '@gitlab/ui';
-import { createLocalVue } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ClustersViewAll from '~/clusters_list/components/clusters_view_all.vue';
@@ -16,8 +16,7 @@ import {
} from '~/clusters_list/constants';
import { sprintf } from '~/locale';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
const addClusterPath = '/path/to/add/cluster';
const defaultBranchName = 'default-branch';
@@ -33,8 +32,9 @@ describe('ClustersViewAllComponent', () => {
defaultBranchName,
};
- const provideData = {
+ const defaultProvide = {
addClusterPath,
+ canAddCluster: true,
};
const entryData = {
@@ -46,32 +46,43 @@ describe('ClustersViewAllComponent', () => {
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findAgentsComponent = () => wrapper.findComponent(Agents);
const findClustersComponent = () => wrapper.findComponent(Clusters);
+ const findInstallAgentButtonTooltip = () => wrapper.findByTestId('install-agent-button-tooltip');
+ const findConnectExistingClusterButtonTooltip = () =>
+ wrapper.findByTestId('connect-existing-cluster-button-tooltip');
const findCardsContainer = () => wrapper.findByTestId('clusters-cards-container');
const findAgentCardTitle = () => wrapper.findByTestId('agent-card-title');
const findRecommendedBadge = () => wrapper.findComponent(GlBadge);
const findClustersCardTitle = () => wrapper.findByTestId('clusters-card-title');
const findFooterButton = (line) => findCards().at(line).findComponent(GlButton);
+ const getTooltipText = (el) => {
+ const binding = getBinding(el, 'gl-tooltip');
+
+ return binding.value;
+ };
const createStore = (initialState) =>
new Vuex.Store({
state: initialState,
});
- const createWrapper = ({ initialState }) => {
+ const createWrapper = ({ initialState = entryData, provideData } = {}) => {
wrapper = shallowMountExtended(ClustersViewAll, {
- localVue,
store: createStore(initialState),
propsData,
- provide: provideData,
+ provide: {
+ ...defaultProvide,
+ ...provideData,
+ },
directives: {
GlModalDirective: createMockDirective(),
+ GlTooltip: createMockDirective(),
},
stubs: { GlCard, GlSprintf },
});
};
beforeEach(() => {
- createWrapper({ initialState: entryData });
+ createWrapper();
});
afterEach(() => {
@@ -127,15 +138,20 @@ describe('ClustersViewAllComponent', () => {
expect(findAgentsComponent().props('defaultBranchName')).toBe(defaultBranchName);
});
+ it('should show install new Agent button in the footer', () => {
+ expect(findFooterButton(0).exists()).toBe(true);
+ expect(findFooterButton(0).props('disabled')).toBe(false);
+ });
+
+ it('does not show tooltip for install new Agent button', () => {
+ expect(getTooltipText(findInstallAgentButtonTooltip().element)).toBe('');
+ });
+
describe('when there are no agents', () => {
it('should show the empty title', () => {
expect(findAgentCardTitle().text()).toBe(AGENT_CARD_INFO.emptyTitle);
});
- it('should show install new Agent button in the footer', () => {
- expect(findFooterButton(0).exists()).toBe(true);
- });
-
it('should render correct modal id for the agent link', () => {
const binding = getBinding(findFooterButton(0).element, 'gl-modal-directive');
@@ -175,6 +191,22 @@ describe('ClustersViewAllComponent', () => {
});
});
});
+
+ describe('when the user cannot add clusters', () => {
+ beforeEach(() => {
+ createWrapper({ provideData: { canAddCluster: false } });
+ });
+
+ it('should disable the button', () => {
+ expect(findFooterButton(0).props('disabled')).toBe(true);
+ });
+
+ it('should show a tooltip explaining why the button is disabled', () => {
+ expect(getTooltipText(findInstallAgentButtonTooltip().element)).toBe(
+ AGENT_CARD_INFO.installAgentDisabledHint,
+ );
+ });
+ });
});
describe('clusters tab', () => {
@@ -191,13 +223,34 @@ describe('ClustersViewAllComponent', () => {
expect(findClustersCardTitle().text()).toBe(CERTIFICATE_BASED_CARD_INFO.emptyTitle);
});
- it('should show install new Agent button in the footer', () => {
+ it('should show install new cluster button in the footer', () => {
expect(findFooterButton(1).exists()).toBe(true);
+ expect(findFooterButton(1).props('disabled')).toBe(false);
});
it('should render correct href for the button in the footer', () => {
expect(findFooterButton(1).attributes('href')).toBe(addClusterPath);
});
+
+ it('does not show tooltip for install new cluster button', () => {
+ expect(getTooltipText(findConnectExistingClusterButtonTooltip().element)).toBe('');
+ });
+ });
+
+ describe('when the user cannot add clusters', () => {
+ beforeEach(() => {
+ createWrapper({ provideData: { canAddCluster: false } });
+ });
+
+ it('should disable the button', () => {
+ expect(findFooterButton(1).props('disabled')).toBe(true);
+ });
+
+ it('should show a tooltip explaining why the button is disabled', () => {
+ expect(getTooltipText(findConnectExistingClusterButtonTooltip().element)).toBe(
+ CERTIFICATE_BASED_CARD_INFO.connectExistingClusterDisabledHint,
+ );
+ });
});
describe('when the clusters are present', () => {
diff --git a/spec/frontend/clusters_list/components/agent_options_spec.js b/spec/frontend/clusters_list/components/delete_agent_button_spec.js
index 05bab247816..82850b9dea4 100644
--- a/spec/frontend/clusters_list/components/agent_options_spec.js
+++ b/spec/frontend/clusters_list/components/delete_agent_button_spec.js
@@ -1,13 +1,15 @@
-import { GlDropdown, GlDropdownItem, GlModal, GlFormInput } from '@gitlab/ui';
-import Vue from 'vue';
+import { GlButton, GlModal, GlFormInput } from '@gitlab/ui';
+import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { ENTER_KEY } from '~/lib/utils/keys';
import getAgentsQuery from '~/clusters_list/graphql/queries/get_agents.query.graphql';
import deleteAgentMutation from '~/clusters_list/graphql/mutations/delete_agent.mutation.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
-import AgentOptions from '~/clusters_list/components/agent_options.vue';
-import { MAX_LIST_COUNT } from '~/clusters_list/constants';
+import waitForPromises from 'helpers/wait_for_promises';
+import DeleteAgentButton from '~/clusters_list/components/delete_agent_button.vue';
+import { MAX_LIST_COUNT, DELETE_AGENT_BUTTON } from '~/clusters_list/constants';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { getAgentResponse, mockDeleteResponse, mockErrorDeleteResponse } from '../mocks/apollo';
Vue.use(VueApollo);
@@ -21,18 +23,23 @@ const agent = {
webPath: 'agent-webPath',
};
-describe('AgentOptions', () => {
+describe('DeleteAgentButton', () => {
let wrapper;
let toast;
let apolloProvider;
let deleteResponse;
const findModal = () => wrapper.findComponent(GlModal);
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findDeleteBtn = () => wrapper.findComponent(GlDropdownItem);
+ const findDeleteBtn = () => wrapper.findComponent(GlButton);
const findInput = () => wrapper.findComponent(GlFormInput);
const findPrimaryAction = () => findModal().props('actionPrimary');
const findPrimaryActionAttributes = (attr) => findPrimaryAction().attributes[0][attr];
+ const findDeleteAgentButtonTooltip = () => wrapper.findByTestId('delete-agent-button-tooltip');
+ const getTooltipText = (el) => {
+ const binding = getBinding(el, 'gl-tooltip');
+
+ return binding.value;
+ };
const createMockApolloProvider = ({ mutationResponse }) => {
deleteResponse = jest.fn().mockResolvedValue(mutationResponse);
@@ -53,10 +60,14 @@ describe('AgentOptions', () => {
});
};
- const createWrapper = ({ mutationResponse = mockDeleteResponse } = {}) => {
+ const createWrapper = async ({
+ mutationResponse = mockDeleteResponse,
+ provideData = {},
+ } = {}) => {
apolloProvider = createMockApolloProvider({ mutationResponse });
- const provide = {
+ const defaultProvide = {
projectPath,
+ canAdminCluster: true,
};
const propsData = {
defaultBranchName,
@@ -66,9 +77,15 @@ describe('AgentOptions', () => {
toast = jest.fn();
- wrapper = shallowMountExtended(AgentOptions, {
+ wrapper = shallowMountExtended(DeleteAgentButton, {
apolloProvider,
- provide,
+ provide: {
+ ...defaultProvide,
+ ...provideData,
+ },
+ directives: {
+ GlTooltip: createMockDirective(),
+ },
propsData,
mocks: { $toast: { show: toast } },
stubs: { GlModal },
@@ -76,13 +93,14 @@ describe('AgentOptions', () => {
wrapper.vm.$refs.modal.hide = jest.fn();
writeQuery();
- return wrapper.vm.$nextTick();
+ await nextTick();
};
const submitAgentToDelete = async () => {
findDeleteBtn().vm.$emit('click');
findInput().vm.$emit('input', agent.name);
await findModal().vm.$emit('primary');
+ await waitForPromises();
};
beforeEach(() => {
@@ -98,7 +116,13 @@ describe('AgentOptions', () => {
describe('delete agent action', () => {
it('displays a delete button', () => {
- expect(findDeleteBtn().text()).toBe('Delete agent');
+ expect(findDeleteBtn().attributes('aria-label')).toBe(DELETE_AGENT_BUTTON.deleteButton);
+ });
+
+ it('shows a tooltip for the button', () => {
+ expect(getTooltipText(findDeleteAgentButtonTooltip().element)).toBe(
+ DELETE_AGENT_BUTTON.deleteButton,
+ );
});
describe('when clicking the delete button', () => {
@@ -111,6 +135,22 @@ describe('AgentOptions', () => {
});
});
+ describe('when user cannot delete clusters', () => {
+ beforeEach(() => {
+ createWrapper({ provideData: { canAdminCluster: false } });
+ });
+
+ it('disables the button', () => {
+ expect(findDeleteBtn().attributes('disabled')).toBe('true');
+ });
+
+ it('shows a disabled tooltip', () => {
+ expect(getTooltipText(findDeleteAgentButtonTooltip().element)).toBe(
+ DELETE_AGENT_BUTTON.disabledHint,
+ );
+ });
+ });
+
describe.each`
condition | agentName | isDisabled | mutationCalled
${'the input with agent name is missing'} | ${''} | ${true} | ${false}
@@ -173,8 +213,7 @@ describe('AgentOptions', () => {
describe('when getting an error deleting agent', () => {
beforeEach(async () => {
await createWrapper({ mutationResponse: mockErrorDeleteResponse });
-
- submitAgentToDelete();
+ await submitAgentToDelete();
});
it('displays the error message', () => {
@@ -187,17 +226,17 @@ describe('AgentOptions', () => {
const loadingResponse = new Promise(() => {});
await createWrapper({ mutationResponse: loadingResponse });
- submitAgentToDelete();
+ await submitAgentToDelete();
});
- it('reenables the options dropdown', async () => {
+ it('reenables the button', async () => {
expect(findPrimaryActionAttributes('loading')).toBe(true);
- expect(findDropdown().attributes('disabled')).toBe('true');
+ expect(findDeleteBtn().attributes('disabled')).toBe('true');
await findModal().vm.$emit('hide');
expect(findPrimaryActionAttributes('loading')).toBe(false);
- expect(findDropdown().attributes('disabled')).toBeUndefined();
+ expect(findDeleteBtn().attributes('disabled')).toBeUndefined();
});
it('clears the agent name input', async () => {
diff --git a/spec/frontend/clusters_list/components/install_agent_modal_spec.js b/spec/frontend/clusters_list/components/install_agent_modal_spec.js
index 4d1429c9e50..37432ed0193 100644
--- a/spec/frontend/clusters_list/components/install_agent_modal_spec.js
+++ b/spec/frontend/clusters_list/components/install_agent_modal_spec.js
@@ -1,6 +1,7 @@
-import { GlAlert, GlButton, GlFormInputGroup } from '@gitlab/ui';
-import { createLocalVue } from '@vue/test-utils';
+import { GlAlert, GlButton, GlFormInputGroup, GlSprintf } from '@gitlab/ui';
+import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import { sprintf } from '~/locale';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { mockTracking } from 'helpers/tracking_helper';
import AvailableAgentsDropdown from '~/clusters_list/components/available_agents_dropdown.vue';
@@ -27,15 +28,14 @@ import {
createAgentTokenResponse,
createAgentTokenErrorResponse,
getAgentResponse,
+ kasDisabledErrorResponse,
} from '../mocks/apollo';
import ModalStub from '../stubs';
-const localVue = createLocalVue();
-localVue.use(VueApollo);
+Vue.use(VueApollo);
const projectPath = 'path/to/project';
const kasAddress = 'kas.example.com';
-const kasEnabled = true;
const emptyStateImage = 'path/to/image';
const defaultBranchName = 'default';
const maxAgents = MAX_LIST_COUNT;
@@ -49,7 +49,8 @@ describe('InstallAgentModal', () => {
const apolloQueryResponse = {
data: {
project: {
- id: '1',
+ __typename: 'Project',
+ id: 'project-1',
clusterAgents: { nodes: [] },
agentConfigurations: { nodes: configurations },
},
@@ -65,7 +66,7 @@ describe('InstallAgentModal', () => {
.wrappers.find((button) => button.props('variant') === variant);
const findActionButton = () => findButtonByVariant('confirm');
const findCancelButton = () => findButtonByVariant('default');
- const findSecondaryButton = () => wrapper.findByTestId('agent-secondary-button');
+ const findPrimaryButton = () => wrapper.findByTestId('agent-primary-button');
const findImage = () => wrapper.findByRole('img', { alt: I18N_AGENT_MODAL.empty_state.altText });
const expectDisabledAttribute = (element, disabled) => {
@@ -80,7 +81,6 @@ describe('InstallAgentModal', () => {
const provide = {
projectPath,
kasAddress,
- kasEnabled,
emptyStateImage,
};
@@ -92,9 +92,9 @@ describe('InstallAgentModal', () => {
wrapper = shallowMountExtended(InstallAgentModal, {
attachTo: document.body,
stubs: {
+ GlSprintf,
GlModal: ModalStub,
},
- localVue,
apolloProvider,
provide,
propsData,
@@ -118,7 +118,7 @@ describe('InstallAgentModal', () => {
createWrapper();
writeQuery();
- await wrapper.vm.$nextTick();
+ await waitForPromises();
wrapper.vm.setAgentName('agent-name');
findActionButton().vm.$emit('click');
@@ -126,11 +126,12 @@ describe('InstallAgentModal', () => {
return waitForPromises();
};
- beforeEach(() => {
+ beforeEach(async () => {
apolloProvider = createMockApollo([
[getAgentConfigurations, jest.fn().mockResolvedValue(apolloQueryResponse)],
]);
createWrapper();
+ await waitForPromises();
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
});
@@ -293,9 +294,9 @@ describe('InstallAgentModal', () => {
expect(findImage().attributes('src')).toBe(emptyStateImage);
});
- it('renders a secondary button', () => {
- expect(findSecondaryButton().isVisible()).toBe(true);
- expect(findSecondaryButton().text()).toBe(i18n.secondaryButton);
+ it('renders a primary button', () => {
+ expect(findPrimaryButton().isVisible()).toBe(true);
+ expect(findPrimaryButton().text()).toBe(i18n.primaryButton);
});
it('sends the event with the modalType', () => {
@@ -306,4 +307,35 @@ describe('InstallAgentModal', () => {
});
});
});
+
+ describe('when KAS is disabled', () => {
+ const i18n = I18N_AGENT_MODAL.empty_state;
+ beforeEach(async () => {
+ apolloProvider = createMockApollo([
+ [getAgentConfigurations, jest.fn().mockResolvedValue(kasDisabledErrorResponse)],
+ ]);
+
+ createWrapper();
+ await waitForPromises();
+ });
+
+ it('renders empty state image', () => {
+ expect(findImage().attributes('src')).toBe(emptyStateImage);
+ });
+
+ it('renders an instruction to enable the KAS', () => {
+ expect(findModal().text()).toContain(
+ sprintf(i18n.enableKasText, { linkStart: '', linkEnd: '' }),
+ );
+ });
+
+ it('renders a cancel button', () => {
+ expect(findCancelButton().isVisible()).toBe(true);
+ expect(findCancelButton().text()).toBe(i18n.done);
+ });
+
+ it("doesn't render a secondary button", () => {
+ expect(findPrimaryButton().exists()).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/clusters_list/components/mock_data.js b/spec/frontend/clusters_list/components/mock_data.js
index e388d791b89..3d18b22d727 100644
--- a/spec/frontend/clusters_list/components/mock_data.js
+++ b/spec/frontend/clusters_list/components/mock_data.js
@@ -1,3 +1,5 @@
+import { ACTIVE_CONNECTION_TIME } from '~/clusters_list/constants';
+
export const agentConfigurationsResponse = {
data: {
project: {
@@ -10,3 +12,113 @@ export const agentConfigurationsResponse = {
},
},
};
+
+export const connectedTimeNow = new Date();
+export const connectedTimeInactive = new Date(connectedTimeNow.getTime() - ACTIVE_CONNECTION_TIME);
+
+export const clusterAgents = [
+ {
+ name: 'agent-1',
+ id: 'agent-1-id',
+ configFolder: {
+ webPath: '/agent/full/path',
+ },
+ webPath: '/agent-1',
+ status: 'unused',
+ lastContact: null,
+ tokens: null,
+ },
+ {
+ name: 'agent-2',
+ id: 'agent-2-id',
+ webPath: '/agent-2',
+ status: 'active',
+ lastContact: connectedTimeNow.getTime(),
+ connections: {
+ nodes: [
+ {
+ metadata: { version: 'v14.8' },
+ },
+ {
+ metadata: { version: 'v14.8' },
+ },
+ ],
+ },
+ tokens: {
+ nodes: [
+ {
+ lastUsedAt: connectedTimeNow,
+ },
+ ],
+ },
+ },
+ {
+ name: 'agent-3',
+ id: 'agent-3-id',
+ webPath: '/agent-3',
+ status: 'inactive',
+ lastContact: connectedTimeInactive.getTime(),
+ connections: {
+ nodes: [
+ {
+ metadata: { version: 'v14.5' },
+ },
+ ],
+ },
+ tokens: {
+ nodes: [
+ {
+ lastUsedAt: connectedTimeInactive,
+ },
+ ],
+ },
+ },
+ {
+ name: 'agent-4',
+ id: 'agent-4-id',
+ webPath: '/agent-4',
+ status: 'inactive',
+ lastContact: connectedTimeInactive.getTime(),
+ connections: {
+ nodes: [
+ {
+ metadata: { version: 'v14.7' },
+ },
+ {
+ metadata: { version: 'v14.8' },
+ },
+ ],
+ },
+ tokens: {
+ nodes: [
+ {
+ lastUsedAt: connectedTimeInactive,
+ },
+ ],
+ },
+ },
+ {
+ name: 'agent-5',
+ id: 'agent-5-id',
+ webPath: '/agent-5',
+ status: 'inactive',
+ lastContact: connectedTimeInactive.getTime(),
+ connections: {
+ nodes: [
+ {
+ metadata: { version: 'v14.5' },
+ },
+ {
+ metadata: { version: 'v14.3' },
+ },
+ ],
+ },
+ tokens: {
+ nodes: [
+ {
+ lastUsedAt: connectedTimeInactive,
+ },
+ ],
+ },
+ },
+];
diff --git a/spec/frontend/clusters_list/components/node_error_help_text_spec.js b/spec/frontend/clusters_list/components/node_error_help_text_spec.js
index 18d27f3fd80..8187ab75c58 100644
--- a/spec/frontend/clusters_list/components/node_error_help_text_spec.js
+++ b/spec/frontend/clusters_list/components/node_error_help_text_spec.js
@@ -1,13 +1,14 @@
import { GlPopover } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import NodeErrorHelpText from '~/clusters_list/components/node_error_help_text.vue';
describe('NodeErrorHelpText', () => {
let wrapper;
- const createWrapper = (propsData) => {
+ const createWrapper = async (propsData) => {
wrapper = shallowMount(NodeErrorHelpText, { propsData, stubs: { GlPopover } });
- return wrapper.vm.$nextTick();
+ await nextTick();
};
const findPopover = () => wrapper.find(GlPopover);
diff --git a/spec/frontend/clusters_list/mocks/apollo.js b/spec/frontend/clusters_list/mocks/apollo.js
index c4a31ed4394..b0f2978a230 100644
--- a/spec/frontend/clusters_list/mocks/apollo.js
+++ b/spec/frontend/clusters_list/mocks/apollo.js
@@ -10,6 +10,9 @@ const token = {
const tokens = {
nodes: [token],
};
+const connections = {
+ nodes: [],
+};
const pageInfo = {
endCursor: '',
hasNextPage: false,
@@ -23,6 +26,7 @@ export const createAgentResponse = {
createClusterAgent: {
clusterAgent: {
...agent,
+ connections,
tokens,
},
errors: [],
@@ -35,6 +39,7 @@ export const createAgentErrorResponse = {
createClusterAgent: {
clusterAgent: {
...agent,
+ connections,
tokens,
},
errors: ['could not create agent'],
@@ -65,8 +70,9 @@ export const createAgentTokenErrorResponse = {
export const getAgentResponse = {
data: {
project: {
+ __typename: 'Project',
id: 'project-1',
- clusterAgents: { nodes: [{ ...agent, tokens }], pageInfo, count },
+ clusterAgents: { nodes: [{ ...agent, connections, tokens }], pageInfo, count },
repository: {
tree: {
trees: { nodes: [{ ...agent, path: null }], pageInfo },
@@ -76,6 +82,11 @@ export const getAgentResponse = {
},
};
+export const kasDisabledErrorResponse = {
+ data: {},
+ errors: [{ message: 'Gitlab::Kas::Client::ConfigurationError' }],
+};
+
export const mockDeleteResponse = {
data: { clusterAgentDelete: { errors: [] } },
};
diff --git a/spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap b/spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap
index 97d9be110c8..36d2c2cabc5 100644
--- a/spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap
+++ b/spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap
@@ -14,7 +14,6 @@ exports[`Code navigation popover component renders popover 1`] = `
contentclass="gl-py-0"
navclass="gl-hidden"
queryparamname="tab"
- theme="indigo"
value="0"
>
<gl-tab-stub
diff --git a/spec/frontend/code_navigation/components/app_spec.js b/spec/frontend/code_navigation/components/app_spec.js
index 798f3bc0ee2..9306c15e676 100644
--- a/spec/frontend/code_navigation/components/app_spec.js
+++ b/spec/frontend/code_navigation/components/app_spec.js
@@ -1,15 +1,15 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import App from '~/code_navigation/components/app.vue';
import Popover from '~/code_navigation/components/popover.vue';
import createState from '~/code_navigation/store/state';
-const localVue = createLocalVue();
const fetchData = jest.fn();
const showDefinition = jest.fn();
let wrapper;
-localVue.use(Vuex);
+Vue.use(Vuex);
function factory(initialState = {}) {
const store = new Vuex.Store({
@@ -24,7 +24,7 @@ function factory(initialState = {}) {
},
});
- wrapper = shallowMount(App, { store, localVue });
+ wrapper = shallowMount(App, { store });
}
describe('Code navigation app component', () => {
diff --git a/spec/frontend/code_quality_walkthrough/components/step_spec.js b/spec/frontend/code_quality_walkthrough/components/step_spec.js
index bdbcda5f902..b43629c2f96 100644
--- a/spec/frontend/code_quality_walkthrough/components/step_spec.js
+++ b/spec/frontend/code_quality_walkthrough/components/step_spec.js
@@ -118,7 +118,7 @@ describe('When the code_quality_walkthrough URL parameter is present', () => {
expect(Cookies.set).toHaveBeenCalledWith(
EXPERIMENT_NAME,
{ commit_ci_file: true, data: dummyContext },
- { expires: 365 },
+ { expires: 365, secure: false },
);
});
diff --git a/spec/frontend/commit/commit_pipeline_status_component_spec.js b/spec/frontend/commit/commit_pipeline_status_component_spec.js
index 3a549e66eb7..43db6db00c1 100644
--- a/spec/frontend/commit/commit_pipeline_status_component_spec.js
+++ b/spec/frontend/commit/commit_pipeline_status_component_spec.js
@@ -1,6 +1,7 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Visibility from 'visibilityjs';
+import { nextTick } from 'vue';
import fixture from 'test_fixtures/pipelines/pipelines.json';
import createFlash from '~/flash';
import Poll from '~/lib/utils/poll';
@@ -112,7 +113,7 @@ describe('Commit pipeline status component', () => {
createComponent();
});
- it('shows the loading icon at start', () => {
+ it('shows the loading icon at start', async () => {
createComponent();
expect(findLoader().exists()).toBe(true);
@@ -120,17 +121,16 @@ describe('Commit pipeline status component', () => {
data: { pipelines: [] },
});
- return wrapper.vm.$nextTick().then(() => {
- expect(findLoader().exists()).toBe(false);
- });
+ await nextTick();
+ expect(findLoader().exists()).toBe(false);
});
describe('is successful', () => {
- beforeEach(() => {
+ beforeEach(async () => {
pollConfig.successCallback({
data: { pipelines: [{ details: { status: mockCiStatus } }] },
});
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('does not render loader', () => {
diff --git a/spec/frontend/commit/pipelines/pipelines_table_spec.js b/spec/frontend/commit/pipelines/pipelines_table_spec.js
index e209f628aa2..203a4d23160 100644
--- a/spec/frontend/commit/pipelines/pipelines_table_spec.js
+++ b/spec/frontend/commit/pipelines/pipelines_table_spec.js
@@ -1,11 +1,14 @@
import { GlEmptyState, GlLoadingIcon, GlModal, GlTableLite } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+import { nextTick } from 'vue';
import fixture from 'test_fixtures/pipelines/pipelines.json';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api';
import PipelinesTable from '~/commit/pipelines/pipelines_table.vue';
+import httpStatusCodes from '~/lib/utils/http_status';
+import createFlash from '~/flash';
import { TOAST_MESSAGE } from '~/pipelines/constants';
import axios from '~/lib/utils/axios_utils';
@@ -13,6 +16,8 @@ const $toast = {
show: jest.fn(),
};
+jest.mock('~/flash');
+
describe('Pipelines table in Commits and Merge requests', () => {
let wrapper;
let pipeline;
@@ -183,36 +188,61 @@ describe('Pipelines table in Commits and Merge requests', () => {
mergeRequestId: 3,
});
- jest.spyOn(Api, 'postMergeRequestPipeline').mockReturnValue(Promise.resolve());
-
await waitForPromises();
});
+ describe('success', () => {
+ beforeEach(() => {
+ jest.spyOn(Api, 'postMergeRequestPipeline').mockReturnValue(Promise.resolve());
+ });
+ it('displays a toast message during pipeline creation', async () => {
+ await findRunPipelineBtn().trigger('click');
- it('displays a toast message during pipeline creation', async () => {
- await findRunPipelineBtn().trigger('click');
+ expect($toast.show).toHaveBeenCalledWith(TOAST_MESSAGE);
+ });
- expect($toast.show).toHaveBeenCalledWith(TOAST_MESSAGE);
- });
+ it('on desktop, shows a loading button', async () => {
+ await findRunPipelineBtn().trigger('click');
- it('on desktop, shows a loading button', async () => {
- await findRunPipelineBtn().trigger('click');
+ expect(findRunPipelineBtn().props('loading')).toBe(true);
- expect(findRunPipelineBtn().props('loading')).toBe(true);
+ await waitForPromises();
- await waitForPromises();
+ expect(findRunPipelineBtn().props('loading')).toBe(false);
+ });
- expect(findRunPipelineBtn().props('loading')).toBe(false);
+ it('on mobile, shows a loading button', async () => {
+ await findRunPipelineBtnMobile().trigger('click');
+
+ expect(findRunPipelineBtn().props('loading')).toBe(true);
+
+ await waitForPromises();
+
+ expect(findRunPipelineBtn().props('disabled')).toBe(false);
+ expect(findRunPipelineBtn().props('loading')).toBe(false);
+ });
});
- it('on mobile, shows a loading button', async () => {
- await findRunPipelineBtnMobile().trigger('click');
+ describe('failure', () => {
+ const permissionsMsg = 'You do not have permission to run a pipeline on this branch.';
- expect(findRunPipelineBtn().props('loading')).toBe(true);
+ it.each`
+ status | message
+ ${httpStatusCodes.BAD_REQUEST} | ${permissionsMsg}
+ ${httpStatusCodes.UNAUTHORIZED} | ${permissionsMsg}
+ ${httpStatusCodes.INTERNAL_SERVER_ERROR} | ${'An error occurred while trying to run a new pipeline for this merge request.'}
+ `('displays permissions error message', async ({ status, message }) => {
+ const response = { response: { status } };
- await waitForPromises();
+ jest
+ .spyOn(Api, 'postMergeRequestPipeline')
+ .mockImplementation(() => Promise.reject(response));
+
+ await findRunPipelineBtn().trigger('click');
- expect(findRunPipelineBtn().props('disabled')).toBe(false);
- expect(findRunPipelineBtn().props('loading')).toBe(false);
+ await waitForPromises();
+
+ expect(createFlash).toHaveBeenCalledWith({ message });
+ });
});
});
@@ -238,7 +268,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
it('on desktop, shows a security warning modal', async () => {
await findRunPipelineBtn().trigger('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findModal()).not.toBeNull();
});
diff --git a/spec/frontend/confirm_modal_spec.js b/spec/frontend/confirm_modal_spec.js
index 5e5345cbd2b..53991349ee5 100644
--- a/spec/frontend/confirm_modal_spec.js
+++ b/spec/frontend/confirm_modal_spec.js
@@ -1,4 +1,3 @@
-import Vue from 'vue';
import { TEST_HOST } from 'helpers/test_constants';
import initConfirmModal from '~/confirm_modal';
@@ -50,7 +49,6 @@ describe('ConfirmModal', () => {
const findModal = () => document.querySelector('.gl-modal');
const findModalOkButton = (modal, variant) =>
modal.querySelector(`.modal-footer .btn-${variant}`);
- const findModalCancelButton = (modal) => modal.querySelector('.modal-footer .btn-secondary');
const modalIsHidden = () => findModal() === null;
const serializeModal = (modal, buttonIndex) => {
@@ -90,20 +88,6 @@ describe('ConfirmModal', () => {
expect(findModal()).not.toBe(null);
expect(modalIsHidden()).toBe(false);
});
-
- describe('Cancel Button', () => {
- beforeEach(() => {
- findModalCancelButton(findModal()).click();
-
- return Vue.nextTick();
- });
-
- it('closes the modal', () => {
- setImmediate(() => {
- expect(modalIsHidden()).toBe(true);
- });
- });
- });
});
});
diff --git a/spec/frontend/content_editor/test_utils.js b/spec/frontend/content_editor/test_utils.js
index b236c630e13..84eaa3c5f44 100644
--- a/spec/frontend/content_editor/test_utils.js
+++ b/spec/frontend/content_editor/test_utils.js
@@ -132,7 +132,7 @@ export const triggerNodeInputRule = ({ tiptapEditor, inputRuleText }) => {
export const triggerMarkInputRule = ({ tiptapEditor, inputRuleText }) => {
const { view } = tiptapEditor;
- tiptapEditor.chain().setContent(inputRuleText).setTextSelection(0).run();
+ tiptapEditor.chain().setContent(inputRuleText).setTextSelection(1).run();
const { state } = tiptapEditor;
const { selection } = state;
diff --git a/spec/frontend/contributors/component/contributors_spec.js b/spec/frontend/contributors/component/contributors_spec.js
index cb7e13b9fed..bdf3b3636ed 100644
--- a/spec/frontend/contributors/component/contributors_spec.js
+++ b/spec/frontend/contributors/component/contributors_spec.js
@@ -1,6 +1,6 @@
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import ContributorsCharts from '~/contributors/components/contributors.vue';
import { createStore } from '~/contributors/stores';
import axios from '~/lib/utils/axios_utils';
@@ -49,20 +49,18 @@ describe('Contributors charts', () => {
expect(axios.get).toHaveBeenCalledWith(endpoint);
});
- it('should display loader whiled loading data', () => {
+ it('should display loader whiled loading data', async () => {
wrapper.vm.$store.state.loading = true;
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.find('.contributors-loader').exists()).toBe(true);
- });
+ await nextTick();
+ expect(wrapper.find('.contributors-loader').exists()).toBe(true);
});
- it('should render charts when loading completed and there is chart data', () => {
+ it('should render charts when loading completed and there is chart data', async () => {
wrapper.vm.$store.state.loading = false;
wrapper.vm.$store.state.chartData = chartData;
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.find('.contributors-loader').exists()).toBe(false);
- expect(wrapper.find('.contributors-charts').exists()).toBe(true);
- expect(wrapper.element).toMatchSnapshot();
- });
+ await nextTick();
+ expect(wrapper.find('.contributors-loader').exists()).toBe(false);
+ expect(wrapper.find('.contributors-charts').exists()).toBe(true);
+ expect(wrapper.element).toMatchSnapshot();
});
});
diff --git a/spec/frontend/create_cluster/components/cluster_form_dropdown_spec.js b/spec/frontend/create_cluster/components/cluster_form_dropdown_spec.js
index 4e92fa1df16..2f835867f5f 100644
--- a/spec/frontend/create_cluster/components/cluster_form_dropdown_spec.js
+++ b/spec/frontend/create_cluster/components/cluster_form_dropdown_spec.js
@@ -2,6 +2,7 @@ import { GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import $ from 'jquery';
+import { nextTick } from 'vue';
import ClusterFormDropdown from '~/create_cluster/components/cluster_form_dropdown.vue';
import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
import DropdownSearchInput from '~/vue_shared/components/dropdown/dropdown_search_input.vue';
@@ -18,35 +19,31 @@ describe('ClusterFormDropdown', () => {
afterEach(() => wrapper.destroy());
describe('when initial value is provided', () => {
- it('sets selectedItem to initial value', () => {
+ it('sets selectedItem to initial value', async () => {
wrapper.setProps({ items, value: secondItem.value });
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(DropdownButton).props('toggleText')).toEqual(secondItem.name);
- });
+ await nextTick();
+ expect(wrapper.find(DropdownButton).props('toggleText')).toEqual(secondItem.name);
});
});
describe('when no item is selected', () => {
- it('displays placeholder text', () => {
+ it('displays placeholder text', async () => {
const placeholder = 'placeholder';
wrapper.setProps({ placeholder });
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(DropdownButton).props('toggleText')).toEqual(placeholder);
- });
+ await nextTick();
+ expect(wrapper.find(DropdownButton).props('toggleText')).toEqual(placeholder);
});
});
describe('when an item is selected', () => {
- beforeEach(() => {
+ beforeEach(async () => {
wrapper.setProps({ items });
-
- return wrapper.vm.$nextTick().then(() => {
- wrapper.findAll('.js-dropdown-item').at(1).trigger('click');
- return wrapper.vm.$nextTick();
- });
+ await nextTick();
+ wrapper.findAll('.js-dropdown-item').at(1).trigger('click');
+ await nextTick();
});
it('emits input event with selected item', () => {
@@ -57,18 +54,16 @@ describe('ClusterFormDropdown', () => {
describe('when multiple items are selected', () => {
const value = ['1'];
- beforeEach(() => {
+ beforeEach(async () => {
wrapper.setProps({ items, multiple: true, value });
- return wrapper.vm
- .$nextTick()
- .then(() => {
- wrapper.findAll('.js-dropdown-item').at(0).trigger('click');
- return wrapper.vm.$nextTick();
- })
- .then(() => {
- wrapper.findAll('.js-dropdown-item').at(1).trigger('click');
- return wrapper.vm.$nextTick();
- });
+
+ await nextTick();
+ wrapper.findAll('.js-dropdown-item').at(0).trigger('click');
+
+ await nextTick();
+ wrapper.findAll('.js-dropdown-item').at(1).trigger('click');
+
+ await nextTick();
});
it('emits input event with an array of selected items', () => {
@@ -77,9 +72,9 @@ describe('ClusterFormDropdown', () => {
});
describe('when multiple items can be selected', () => {
- beforeEach(() => {
+ beforeEach(async () => {
wrapper.setProps({ items, multiple: true, value: firstItem.value });
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('displays a checked GlIcon next to the item', () => {
@@ -89,19 +84,18 @@ describe('ClusterFormDropdown', () => {
});
describe('when multiple values can be selected and initial value is null', () => {
- it('emits input event with an array of a single selected item', () => {
+ it('emits input event with an array of a single selected item', async () => {
wrapper.setProps({ items, multiple: true, value: null });
- return wrapper.vm.$nextTick().then(() => {
- wrapper.findAll('.js-dropdown-item').at(0).trigger('click');
+ await nextTick();
+ wrapper.findAll('.js-dropdown-item').at(0).trigger('click');
- expect(wrapper.emitted('input')[0]).toEqual([[firstItem.value]]);
- });
+ expect(wrapper.emitted('input')[0]).toEqual([[firstItem.value]]);
});
});
describe('when an item is selected and has a custom label property', () => {
- it('displays selected item custom label', () => {
+ it('displays selected item custom label', async () => {
const labelProperty = 'customLabel';
const label = 'Name';
const currentValue = '1';
@@ -109,9 +103,8 @@ describe('ClusterFormDropdown', () => {
wrapper.setProps({ labelProperty, items: customLabelItems, value: currentValue });
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(DropdownButton).props('toggleText')).toEqual(label);
- });
+ await nextTick();
+ expect(wrapper.find(DropdownButton).props('toggleText')).toEqual(label);
});
});
@@ -123,86 +116,79 @@ describe('ClusterFormDropdown', () => {
});
describe('when loading and loadingText is provided', () => {
- it('uses loading text as toggle button text', () => {
+ it('uses loading text as toggle button text', async () => {
const loadingText = 'loading text';
wrapper.setProps({ loading: true, loadingText });
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(DropdownButton).props('toggleText')).toEqual(loadingText);
- });
+ await nextTick();
+ expect(wrapper.find(DropdownButton).props('toggleText')).toEqual(loadingText);
});
});
describe('when disabled', () => {
- it('dropdown button isDisabled', () => {
+ it('dropdown button isDisabled', async () => {
wrapper.setProps({ disabled: true });
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(DropdownButton).props('isDisabled')).toBe(true);
- });
+ await nextTick();
+ expect(wrapper.find(DropdownButton).props('isDisabled')).toBe(true);
});
});
describe('when disabled and disabledText is provided', () => {
- it('uses disabled text as toggle button text', () => {
+ it('uses disabled text as toggle button text', async () => {
const disabledText = 'disabled text';
wrapper.setProps({ disabled: true, disabledText });
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(DropdownButton).props('toggleText')).toBe(disabledText);
- });
+ await nextTick();
+ expect(wrapper.find(DropdownButton).props('toggleText')).toBe(disabledText);
});
});
describe('when has errors', () => {
- it('sets border-danger class selector to dropdown toggle', () => {
+ it('sets border-danger class selector to dropdown toggle', async () => {
wrapper.setProps({ hasErrors: true });
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(DropdownButton).classes('border-danger')).toBe(true);
- });
+ await nextTick();
+ expect(wrapper.find(DropdownButton).classes('border-danger')).toBe(true);
});
});
describe('when has errors and an error message', () => {
- it('displays error message', () => {
+ it('displays error message', async () => {
const errorMessage = 'error message';
wrapper.setProps({ hasErrors: true, errorMessage });
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find('.js-eks-dropdown-error-message').text()).toEqual(errorMessage);
- });
+ await nextTick();
+ expect(wrapper.find('.js-eks-dropdown-error-message').text()).toEqual(errorMessage);
});
});
describe('when no results are available', () => {
- it('displays empty text', () => {
+ it('displays empty text', async () => {
const emptyText = 'error message';
wrapper.setProps({ items: [], emptyText });
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find('.js-empty-text').text()).toEqual(emptyText);
- });
+ await nextTick();
+ expect(wrapper.find('.js-empty-text').text()).toEqual(emptyText);
});
});
- it('displays search field placeholder', () => {
+ it('displays search field placeholder', async () => {
const searchFieldPlaceholder = 'Placeholder';
wrapper.setProps({ searchFieldPlaceholder });
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(DropdownSearchInput).props('placeholderText')).toEqual(
- searchFieldPlaceholder,
- );
- });
+ await nextTick();
+ expect(wrapper.find(DropdownSearchInput).props('placeholderText')).toEqual(
+ searchFieldPlaceholder,
+ );
});
- it('it filters results by search query', () => {
+ it('it filters results by search query', async () => {
const searchQuery = secondItem.name;
wrapper.setProps({ items });
@@ -210,21 +196,19 @@ describe('ClusterFormDropdown', () => {
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ searchQuery });
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.findAll('.js-dropdown-item').length).toEqual(1);
- expect(wrapper.find('.js-dropdown-item').text()).toEqual(secondItem.name);
- });
+ await nextTick();
+ expect(wrapper.findAll('.js-dropdown-item').length).toEqual(1);
+ expect(wrapper.find('.js-dropdown-item').text()).toEqual(secondItem.name);
});
- it('focuses dropdown search input when dropdown is displayed', () => {
+ it('focuses dropdown search input when dropdown is displayed', async () => {
const dropdownEl = wrapper.find('.dropdown').element;
expect(wrapper.find(DropdownSearchInput).props('focused')).toBe(false);
$(dropdownEl).trigger('shown.bs.dropdown');
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.find(DropdownSearchInput).props('focused')).toBe(true);
- });
+ await nextTick();
+ expect(wrapper.find(DropdownSearchInput).props('focused')).toBe(true);
});
});
diff --git a/spec/frontend/create_cluster/eks_cluster/components/create_eks_cluster_spec.js b/spec/frontend/create_cluster/eks_cluster/components/create_eks_cluster_spec.js
index 95810e882a1..c8020cf8308 100644
--- a/spec/frontend/create_cluster/eks_cluster/components/create_eks_cluster_spec.js
+++ b/spec/frontend/create_cluster/eks_cluster/components/create_eks_cluster_spec.js
@@ -1,12 +1,12 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import CreateEksCluster from '~/create_cluster/eks_cluster/components/create_eks_cluster.vue';
import EksClusterConfigurationForm from '~/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue';
import ServiceCredentialsForm from '~/create_cluster/eks_cluster/components/service_credentials_form.vue';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('CreateEksCluster', () => {
let vm;
@@ -33,7 +33,6 @@ describe('CreateEksCluster', () => {
externalLinkIcon,
kubernetesIntegrationHelpPath,
},
- localVue,
store,
});
});
diff --git a/spec/frontend/create_cluster/eks_cluster/components/eks_cluster_configuration_form_spec.js b/spec/frontend/create_cluster/eks_cluster/components/eks_cluster_configuration_form_spec.js
index 53a6f12c381..1509d26c99d 100644
--- a/spec/frontend/create_cluster/eks_cluster/components/eks_cluster_configuration_form_spec.js
+++ b/spec/frontend/create_cluster/eks_cluster/components/eks_cluster_configuration_form_spec.js
@@ -1,14 +1,13 @@
import { GlFormCheckbox } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
-import Vue from 'vue';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import EksClusterConfigurationForm from '~/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue';
import eksClusterFormState from '~/create_cluster/eks_cluster/store/state';
import clusterDropdownStoreState from '~/create_cluster/store/cluster_dropdown/state';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('EksClusterConfigurationForm', () => {
let store;
@@ -151,7 +150,6 @@ describe('EksClusterConfigurationForm', () => {
const buildWrapper = () => {
vm = shallowMount(EksClusterConfigurationForm, {
- localVue,
store,
propsData: {
gitlabManagedClusterHelpPath: '',
@@ -225,108 +223,98 @@ describe('EksClusterConfigurationForm', () => {
});
});
- it('sets isLoadingRoles to RoleDropdown loading property', () => {
+ it('sets isLoadingRoles to RoleDropdown loading property', async () => {
rolesState.isLoadingItems = true;
- return Vue.nextTick().then(() => {
- expect(findRoleDropdown().props('loading')).toBe(rolesState.isLoadingItems);
- });
+ await nextTick();
+ expect(findRoleDropdown().props('loading')).toBe(rolesState.isLoadingItems);
});
it('sets roles to RoleDropdown items property', () => {
expect(findRoleDropdown().props('items')).toBe(rolesState.items);
});
- it('sets RoleDropdown hasErrors to true when loading roles failed', () => {
+ it('sets RoleDropdown hasErrors to true when loading roles failed', async () => {
rolesState.loadingItemsError = new Error();
- return Vue.nextTick().then(() => {
- expect(findRoleDropdown().props('hasErrors')).toEqual(true);
- });
+ await nextTick();
+ expect(findRoleDropdown().props('hasErrors')).toEqual(true);
});
it('disables KeyPairDropdown when no region is selected', () => {
expect(findKeyPairDropdown().props('disabled')).toBe(true);
});
- it('enables KeyPairDropdown when no region is selected', () => {
+ it('enables KeyPairDropdown when no region is selected', async () => {
state.selectedRegion = { name: 'west-1 ' };
- return Vue.nextTick().then(() => {
- expect(findKeyPairDropdown().props('disabled')).toBe(false);
- });
+ await nextTick();
+ expect(findKeyPairDropdown().props('disabled')).toBe(false);
});
- it('sets isLoadingKeyPairs to KeyPairDropdown loading property', () => {
+ it('sets isLoadingKeyPairs to KeyPairDropdown loading property', async () => {
keyPairsState.isLoadingItems = true;
- return Vue.nextTick().then(() => {
- expect(findKeyPairDropdown().props('loading')).toBe(keyPairsState.isLoadingItems);
- });
+ await nextTick();
+ expect(findKeyPairDropdown().props('loading')).toBe(keyPairsState.isLoadingItems);
});
it('sets keyPairs to KeyPairDropdown items property', () => {
expect(findKeyPairDropdown().props('items')).toBe(keyPairsState.items);
});
- it('sets KeyPairDropdown hasErrors to true when loading key pairs fails', () => {
+ it('sets KeyPairDropdown hasErrors to true when loading key pairs fails', async () => {
keyPairsState.loadingItemsError = new Error();
- return Vue.nextTick().then(() => {
- expect(findKeyPairDropdown().props('hasErrors')).toEqual(true);
- });
+ await nextTick();
+ expect(findKeyPairDropdown().props('hasErrors')).toEqual(true);
});
it('disables VpcDropdown when no region is selected', () => {
expect(findVpcDropdown().props('disabled')).toBe(true);
});
- it('enables VpcDropdown when no region is selected', () => {
+ it('enables VpcDropdown when no region is selected', async () => {
state.selectedRegion = { name: 'west-1 ' };
- return Vue.nextTick().then(() => {
- expect(findVpcDropdown().props('disabled')).toBe(false);
- });
+ await nextTick();
+ expect(findVpcDropdown().props('disabled')).toBe(false);
});
- it('sets isLoadingVpcs to VpcDropdown loading property', () => {
+ it('sets isLoadingVpcs to VpcDropdown loading property', async () => {
vpcsState.isLoadingItems = true;
- return Vue.nextTick().then(() => {
- expect(findVpcDropdown().props('loading')).toBe(vpcsState.isLoadingItems);
- });
+ await nextTick();
+ expect(findVpcDropdown().props('loading')).toBe(vpcsState.isLoadingItems);
});
it('sets vpcs to VpcDropdown items property', () => {
expect(findVpcDropdown().props('items')).toBe(vpcsState.items);
});
- it('sets VpcDropdown hasErrors to true when loading vpcs fails', () => {
+ it('sets VpcDropdown hasErrors to true when loading vpcs fails', async () => {
vpcsState.loadingItemsError = new Error();
- return Vue.nextTick().then(() => {
- expect(findVpcDropdown().props('hasErrors')).toEqual(true);
- });
+ await nextTick();
+ expect(findVpcDropdown().props('hasErrors')).toEqual(true);
});
it('disables SubnetDropdown when no vpc is selected', () => {
expect(findSubnetDropdown().props('disabled')).toBe(true);
});
- it('enables SubnetDropdown when a vpc is selected', () => {
+ it('enables SubnetDropdown when a vpc is selected', async () => {
state.selectedVpc = { name: 'vpc-1 ' };
- return Vue.nextTick().then(() => {
- expect(findSubnetDropdown().props('disabled')).toBe(false);
- });
+ await nextTick();
+ expect(findSubnetDropdown().props('disabled')).toBe(false);
});
- it('sets isLoadingSubnets to SubnetDropdown loading property', () => {
+ it('sets isLoadingSubnets to SubnetDropdown loading property', async () => {
subnetsState.isLoadingItems = true;
- return Vue.nextTick().then(() => {
- expect(findSubnetDropdown().props('loading')).toBe(subnetsState.isLoadingItems);
- });
+ await nextTick();
+ expect(findSubnetDropdown().props('loading')).toBe(subnetsState.isLoadingItems);
});
it('sets subnets to SubnetDropdown items property', () => {
@@ -362,32 +350,29 @@ describe('EksClusterConfigurationForm', () => {
expect(findSecurityGroupDropdown().props('disabled')).toBe(true);
});
- it('enables SecurityGroupDropdown when a vpc is selected', () => {
+ it('enables SecurityGroupDropdown when a vpc is selected', async () => {
state.selectedVpc = { name: 'vpc-1 ' };
- return Vue.nextTick().then(() => {
- expect(findSecurityGroupDropdown().props('disabled')).toBe(false);
- });
+ await nextTick();
+ expect(findSecurityGroupDropdown().props('disabled')).toBe(false);
});
- it('sets isLoadingSecurityGroups to SecurityGroupDropdown loading property', () => {
+ it('sets isLoadingSecurityGroups to SecurityGroupDropdown loading property', async () => {
securityGroupsState.isLoadingItems = true;
- return Vue.nextTick().then(() => {
- expect(findSecurityGroupDropdown().props('loading')).toBe(securityGroupsState.isLoadingItems);
- });
+ await nextTick();
+ expect(findSecurityGroupDropdown().props('loading')).toBe(securityGroupsState.isLoadingItems);
});
it('sets securityGroups to SecurityGroupDropdown items property', () => {
expect(findSecurityGroupDropdown().props('items')).toBe(securityGroupsState.items);
});
- it('sets SecurityGroupDropdown hasErrors to true when loading security groups fails', () => {
+ it('sets SecurityGroupDropdown hasErrors to true when loading security groups fails', async () => {
securityGroupsState.loadingItemsError = new Error();
- return Vue.nextTick().then(() => {
- expect(findSecurityGroupDropdown().props('hasErrors')).toEqual(true);
- });
+ await nextTick();
+ expect(findSecurityGroupDropdown().props('hasErrors')).toEqual(true);
});
it('dispatches setClusterName when cluster name input changes', () => {
diff --git a/spec/frontend/create_cluster/eks_cluster/components/service_credentials_form_spec.js b/spec/frontend/create_cluster/eks_cluster/components/service_credentials_form_spec.js
index a0510d46794..0d823a18012 100644
--- a/spec/frontend/create_cluster/eks_cluster/components/service_credentials_form_spec.js
+++ b/spec/frontend/create_cluster/eks_cluster/components/service_credentials_form_spec.js
@@ -1,11 +1,11 @@
import { GlButton } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import ServiceCredentialsForm from '~/create_cluster/eks_cluster/components/service_credentials_form.vue';
import eksClusterState from '~/create_cluster/eks_cluster/store/state';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('ServiceCredentialsForm', () => {
let vm;
@@ -33,7 +33,6 @@ describe('ServiceCredentialsForm', () => {
createRoleArnHelpPath: '',
externalLinkIcon: '',
},
- localVue,
store,
});
});
@@ -66,14 +65,13 @@ describe('ServiceCredentialsForm', () => {
expect(findSubmitButton().attributes('disabled')).toBeTruthy();
});
- it('enables submit button when role ARN is not provided', () => {
+ it('enables submit button when role ARN is not provided', async () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
vm.setData({ roleArn: '123' });
- return vm.vm.$nextTick().then(() => {
- expect(findSubmitButton().attributes('disabled')).toBeFalsy();
- });
+ await nextTick();
+ expect(findSubmitButton().attributes('disabled')).toBeFalsy();
});
it('dispatches createRole action when submit button is clicked', () => {
@@ -87,14 +85,14 @@ describe('ServiceCredentialsForm', () => {
});
describe('when is creating role', () => {
- beforeEach(() => {
+ beforeEach(async () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
vm.setData({ roleArn: '123' }); // set role ARN to enable button
state.isCreatingRole = true;
- return vm.vm.$nextTick();
+ await nextTick();
});
it('disables submit button', () => {
diff --git a/spec/frontend/create_cluster/gke_cluster/components/gke_machine_type_dropdown_spec.js b/spec/frontend/create_cluster/gke_cluster/components/gke_machine_type_dropdown_spec.js
index 2b6f2134553..f46b84da939 100644
--- a/spec/frontend/create_cluster/gke_cluster/components/gke_machine_type_dropdown_spec.js
+++ b/spec/frontend/create_cluster/gke_cluster/components/gke_machine_type_dropdown_spec.js
@@ -1,4 +1,5 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import GkeMachineTypeDropdown from '~/create_cluster/gke_cluster/components/gke_machine_type_dropdown.vue';
import createState from '~/create_cluster/gke_cluster/store/state';
@@ -19,15 +20,12 @@ const LABELS = {
DEFAULT: 'Select machine type',
};
-const localVue = createLocalVue();
-
-localVue.use(Vuex);
+Vue.use(Vuex);
const createComponent = (store, propsData = componentConfig) =>
shallowMount(GkeMachineTypeDropdown, {
propsData,
store,
- localVue,
});
const createStore = (initialState = {}, getters = {}) =>
@@ -75,7 +73,7 @@ describe('GkeMachineTypeDropdown', () => {
expect(dropdownButtonLabel()).toBe(LABELS.DISABLED_NO_ZONE);
});
- it('returns loading toggle text', () => {
+ it('returns loading toggle text', async () => {
store = createStore();
wrapper = createComponent(store);
@@ -83,9 +81,8 @@ describe('GkeMachineTypeDropdown', () => {
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ isLoading: true });
- return wrapper.vm.$nextTick().then(() => {
- expect(dropdownButtonLabel()).toBe(LABELS.LOADING);
- });
+ await nextTick();
+ expect(dropdownButtonLabel()).toBe(LABELS.LOADING);
});
it('returns default toggle text', () => {
@@ -115,7 +112,7 @@ describe('GkeMachineTypeDropdown', () => {
});
describe('form input', () => {
- it('reflects new value when dropdown item is clicked', () => {
+ it('reflects new value when dropdown item is clicked', async () => {
store = createStore({
machineTypes: gapiMachineTypesResponseMock.items,
});
@@ -125,9 +122,8 @@ describe('GkeMachineTypeDropdown', () => {
wrapper.find('.dropdown-content button').trigger('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(setMachineType).toHaveBeenCalledWith(expect.anything(), selectedMachineTypeMock);
- });
+ await nextTick();
+ expect(setMachineType).toHaveBeenCalledWith(expect.anything(), selectedMachineTypeMock);
});
});
});
diff --git a/spec/frontend/create_cluster/gke_cluster/components/gke_network_dropdown_spec.js b/spec/frontend/create_cluster/gke_cluster/components/gke_network_dropdown_spec.js
index 23a56766037..addb0ef72a0 100644
--- a/spec/frontend/create_cluster/gke_cluster/components/gke_network_dropdown_spec.js
+++ b/spec/frontend/create_cluster/gke_cluster/components/gke_network_dropdown_spec.js
@@ -1,12 +1,11 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import ClusterFormDropdown from '~/create_cluster/components/cluster_form_dropdown.vue';
import GkeNetworkDropdown from '~/create_cluster/gke_cluster/components/gke_network_dropdown.vue';
import createClusterDropdownState from '~/create_cluster/store/cluster_dropdown/state';
-const localVue = createLocalVue();
-
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('GkeNetworkDropdown', () => {
let wrapper;
@@ -54,7 +53,6 @@ describe('GkeNetworkDropdown', () => {
shallowMount(GkeNetworkDropdown, {
propsData,
store,
- localVue,
});
afterEach(() => {
diff --git a/spec/frontend/create_cluster/gke_cluster/components/gke_project_id_dropdown_spec.js b/spec/frontend/create_cluster/gke_cluster/components/gke_project_id_dropdown_spec.js
index 2b0acc8cf5d..36f8d4bd1e8 100644
--- a/spec/frontend/create_cluster/gke_cluster/components/gke_project_id_dropdown_spec.js
+++ b/spec/frontend/create_cluster/gke_cluster/components/gke_project_id_dropdown_spec.js
@@ -1,4 +1,5 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import GkeProjectIdDropdown from '~/create_cluster/gke_cluster/components/gke_project_id_dropdown.vue';
import createState from '~/create_cluster/gke_cluster/store/state';
@@ -19,9 +20,7 @@ const LABELS = {
EMPTY: 'No projects found',
};
-const localVue = createLocalVue();
-
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('GkeProjectIdDropdown', () => {
let wrapper;
@@ -52,7 +51,6 @@ describe('GkeProjectIdDropdown', () => {
shallowMount(GkeProjectIdDropdown, {
propsData,
store,
- localVue,
});
const bootstrap = (initialState, getters) => {
@@ -80,19 +78,18 @@ describe('GkeProjectIdDropdown', () => {
expect(dropdownButtonLabel()).toBe(LABELS.VALIDATING_PROJECT_BILLING);
});
- it('returns default toggle text', () => {
+ it('returns default toggle text', async () => {
bootstrap();
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ isLoading: false });
- return wrapper.vm.$nextTick().then(() => {
- expect(dropdownButtonLabel()).toBe(LABELS.DEFAULT);
- });
+ await nextTick();
+ expect(dropdownButtonLabel()).toBe(LABELS.DEFAULT);
});
- it('returns project name if project selected', () => {
+ it('returns project name if project selected', async () => {
bootstrap(
{
selectedProject: selectedProjectMock,
@@ -105,12 +102,11 @@ describe('GkeProjectIdDropdown', () => {
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ isLoading: false });
- return wrapper.vm.$nextTick().then(() => {
- expect(dropdownButtonLabel()).toBe(selectedProjectMock.name);
- });
+ await nextTick();
+ expect(dropdownButtonLabel()).toBe(selectedProjectMock.name);
});
- it('returns empty toggle text', () => {
+ it('returns empty toggle text', async () => {
bootstrap({
projects: null,
});
@@ -118,26 +114,24 @@ describe('GkeProjectIdDropdown', () => {
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ isLoading: false });
- return wrapper.vm.$nextTick().then(() => {
- expect(dropdownButtonLabel()).toBe(LABELS.EMPTY);
- });
+ await nextTick();
+ expect(dropdownButtonLabel()).toBe(LABELS.EMPTY);
});
});
describe('selectItem', () => {
- it('reflects new value when dropdown item is clicked', () => {
+ it('reflects new value when dropdown item is clicked', async () => {
bootstrap({ projects: gapiProjectsResponseMock.projects });
expect(dropdownHiddenInputValue()).toBe('');
wrapper.find('.dropdown-content button').trigger('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(setProject).toHaveBeenCalledWith(
- expect.anything(),
- gapiProjectsResponseMock.projects[0],
- );
- });
+ await nextTick();
+ expect(setProject).toHaveBeenCalledWith(
+ expect.anything(),
+ gapiProjectsResponseMock.projects[0],
+ );
});
});
});
diff --git a/spec/frontend/create_cluster/gke_cluster/components/gke_submit_button_spec.js b/spec/frontend/create_cluster/gke_cluster/components/gke_submit_button_spec.js
index 014ed6013bd..2bf9158628c 100644
--- a/spec/frontend/create_cluster/gke_cluster/components/gke_submit_button_spec.js
+++ b/spec/frontend/create_cluster/gke_cluster/components/gke_submit_button_spec.js
@@ -1,10 +1,9 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import GkeSubmitButton from '~/create_cluster/gke_cluster/components/gke_submit_button.vue';
-const localVue = createLocalVue();
-
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('GkeSubmitButton', () => {
let wrapper;
@@ -21,7 +20,6 @@ describe('GkeSubmitButton', () => {
const buildWrapper = () =>
shallowMount(GkeSubmitButton, {
store,
- localVue,
});
const bootstrap = () => {
diff --git a/spec/frontend/create_cluster/gke_cluster/components/gke_subnetwork_dropdown_spec.js b/spec/frontend/create_cluster/gke_cluster/components/gke_subnetwork_dropdown_spec.js
index cfa8a678a9b..9df680d94b5 100644
--- a/spec/frontend/create_cluster/gke_cluster/components/gke_subnetwork_dropdown_spec.js
+++ b/spec/frontend/create_cluster/gke_cluster/components/gke_subnetwork_dropdown_spec.js
@@ -1,12 +1,11 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import ClusterFormDropdown from '~/create_cluster/components/cluster_form_dropdown.vue';
import GkeSubnetworkDropdown from '~/create_cluster/gke_cluster/components/gke_subnetwork_dropdown.vue';
import createClusterDropdownState from '~/create_cluster/store/cluster_dropdown/state';
-const localVue = createLocalVue();
-
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('GkeSubnetworkDropdown', () => {
let wrapper;
@@ -41,7 +40,6 @@ describe('GkeSubnetworkDropdown', () => {
shallowMount(GkeSubnetworkDropdown, {
propsData,
store,
- localVue,
});
afterEach(() => {
diff --git a/spec/frontend/create_cluster/gke_cluster/components/gke_zone_dropdown_spec.js b/spec/frontend/create_cluster/gke_cluster/components/gke_zone_dropdown_spec.js
index 22fc681f863..7b4c228b879 100644
--- a/spec/frontend/create_cluster/gke_cluster/components/gke_zone_dropdown_spec.js
+++ b/spec/frontend/create_cluster/gke_cluster/components/gke_zone_dropdown_spec.js
@@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import GkeZoneDropdown from '~/create_cluster/gke_cluster/components/gke_zone_dropdown.vue';
import { createStore } from '~/create_cluster/gke_cluster/store';
import {
@@ -46,11 +47,11 @@ describe('GkeZoneDropdown', () => {
});
describe('isLoading', () => {
- beforeEach(() => {
+ beforeEach(async () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ isLoading: true });
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('returns loading toggle text', () => {
@@ -59,10 +60,10 @@ describe('GkeZoneDropdown', () => {
});
describe('project is set', () => {
- beforeEach(() => {
+ beforeEach(async () => {
wrapper.vm.$store.commit(SET_PROJECT, selectedProjectMock);
wrapper.vm.$store.commit(SET_PROJECT_BILLING_STATUS, true);
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('returns default toggle text', () => {
@@ -71,9 +72,9 @@ describe('GkeZoneDropdown', () => {
});
describe('project is selected', () => {
- beforeEach(() => {
+ beforeEach(async () => {
wrapper.vm.setItem(selectedZoneMock);
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('returns project name if project selected', () => {
@@ -83,21 +84,20 @@ describe('GkeZoneDropdown', () => {
});
describe('selectItem', () => {
- beforeEach(() => {
+ beforeEach(async () => {
wrapper.vm.$store.commit(SET_ZONES, gapiZonesResponseMock.items);
- return wrapper.vm.$nextTick();
+ await nextTick();
});
- it('reflects new value when dropdown item is clicked', () => {
+ it('reflects new value when dropdown item is clicked', async () => {
const dropdown = wrapper.find(DropdownHiddenInput);
expect(dropdown.attributes('value')).toBe('');
wrapper.find('.dropdown-content button').trigger('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(dropdown.attributes('value')).toBe(selectedZoneMock);
- });
+ await nextTick();
+ expect(dropdown.attributes('value')).toBe(selectedZoneMock);
});
});
});
diff --git a/spec/frontend/cycle_analytics/base_spec.js b/spec/frontend/cycle_analytics/base_spec.js
index 9a9415cc12a..7b1ef71da63 100644
--- a/spec/frontend/cycle_analytics/base_spec.js
+++ b/spec/frontend/cycle_analytics/base_spec.js
@@ -3,11 +3,11 @@ import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import ValueStreamMetrics from '~/analytics/shared/components/value_stream_metrics.vue';
import BaseComponent from '~/cycle_analytics/components/base.vue';
import PathNavigation from '~/cycle_analytics/components/path_navigation.vue';
import StageTable from '~/cycle_analytics/components/stage_table.vue';
import ValueStreamFilters from '~/cycle_analytics/components/value_stream_filters.vue';
-import ValueStreamMetrics from '~/cycle_analytics/components/value_stream_metrics.vue';
import { NOT_ENOUGH_DATA_ERROR } from '~/cycle_analytics/constants';
import initState from '~/cycle_analytics/store/state';
import {
diff --git a/spec/frontend/cycle_analytics/filter_bar_spec.js b/spec/frontend/cycle_analytics/filter_bar_spec.js
index 407f21bd956..36933790cf7 100644
--- a/spec/frontend/cycle_analytics/filter_bar_spec.js
+++ b/spec/frontend/cycle_analytics/filter_bar_spec.js
@@ -1,4 +1,5 @@
-import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import Vuex from 'vuex';
@@ -15,8 +16,7 @@ import * as utils from '~/vue_shared/components/filtered_search_bar/filtered_sea
import initialFiltersState from '~/vue_shared/components/filtered_search_bar/store/modules/filters/state';
import UrlSync from '~/vue_shared/components/url_sync.vue';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
const milestoneTokenType = 'milestone';
const labelsTokenType = 'labels';
@@ -42,7 +42,7 @@ const defaultParams = {
};
async function shouldMergeUrlParams(wrapper, result) {
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(urlUtils.mergeUrlParams).toHaveBeenCalledWith(result, window.location.href, {
spreadArrays: true,
});
@@ -77,7 +77,6 @@ describe('Filter bar', () => {
const createComponent = (initialStore) => {
return shallowMount(FilterBar, {
- localVue,
store: initialStore,
propsData: {
groupPath: 'foo',
diff --git a/spec/frontend/cycle_analytics/stage_table_spec.js b/spec/frontend/cycle_analytics/stage_table_spec.js
index 9605dce2668..107fe5fc865 100644
--- a/spec/frontend/cycle_analytics/stage_table_spec.js
+++ b/spec/frontend/cycle_analytics/stage_table_spec.js
@@ -1,5 +1,6 @@
import { GlEmptyState, GlLoadingIcon, GlTable } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import StageTable from '~/cycle_analytics/components/stage_table.vue';
@@ -263,7 +264,7 @@ describe('StageTable', () => {
expect(wrapper.emitted('handleUpdatePagination')).toBeUndefined();
findPagination().vm.$emit('input', 2);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.emitted('handleUpdatePagination')[0]).toEqual([{ page: 2 }]);
});
diff --git a/spec/frontend/cycle_analytics/utils_spec.js b/spec/frontend/cycle_analytics/utils_spec.js
index a6d6d022781..51405a1ba4d 100644
--- a/spec/frontend/cycle_analytics/utils_spec.js
+++ b/spec/frontend/cycle_analytics/utils_spec.js
@@ -1,13 +1,10 @@
-import metricsData from 'test_fixtures/projects/analytics/value_stream_analytics/summary.json';
import {
transformStagesForPathNavigation,
medianTimeToParsedSeconds,
formatMedianValues,
filterStagesByHiddenStatus,
- prepareTimeMetricsData,
buildCycleAnalyticsInitialData,
} from '~/cycle_analytics/utils';
-import { slugify } from '~/lib/utils/text_utility';
import {
selectedStage,
allowedStages,
@@ -89,34 +86,6 @@ describe('Value stream analytics utils', () => {
});
});
- describe('prepareTimeMetricsData', () => {
- let prepared;
- const [first, second] = metricsData;
- const firstKey = slugify(first.title);
- const secondKey = slugify(second.title);
-
- beforeEach(() => {
- prepared = prepareTimeMetricsData([first, second], {
- [firstKey]: { description: 'Is a value that is good' },
- });
- });
-
- it('will add a `key` based on the title', () => {
- expect(prepared).toMatchObject([{ key: firstKey }, { key: secondKey }]);
- });
-
- it('will add a `label` key', () => {
- expect(prepared).toMatchObject([{ label: 'New Issues' }, { label: 'Commits' }]);
- });
-
- it('will add a popover description using the key if it is provided', () => {
- expect(prepared).toMatchObject([
- { description: 'Is a value that is good' },
- { description: '' },
- ]);
- });
- });
-
describe('buildCycleAnalyticsInitialData', () => {
let res = null;
const projectId = '5';
diff --git a/spec/frontend/cycle_analytics/value_stream_metrics_spec.js b/spec/frontend/cycle_analytics/value_stream_metrics_spec.js
index 082db2cc312..7a539b262fc 100644
--- a/spec/frontend/cycle_analytics/value_stream_metrics_spec.js
+++ b/spec/frontend/cycle_analytics/value_stream_metrics_spec.js
@@ -1,20 +1,22 @@
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
-import { GlSingleStat } from '@gitlab/ui/dist/charts';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import metricsData from 'test_fixtures/projects/analytics/value_stream_analytics/summary.json';
import waitForPromises from 'helpers/wait_for_promises';
+import ValueStreamMetrics from '~/analytics/shared/components/value_stream_metrics.vue';
import { METRIC_TYPE_SUMMARY } from '~/api/analytics_api';
-import ValueStreamMetrics from '~/cycle_analytics/components/value_stream_metrics.vue';
+import { METRICS_POPOVER_CONTENT } from '~/analytics/shared/constants';
+import { prepareTimeMetricsData } from '~/analytics/shared/utils';
+import MetricTile from '~/analytics/shared/components/metric_tile.vue';
import createFlash from '~/flash';
-import { redirectTo } from '~/lib/utils/url_utility';
import { group } from './mock_data';
jest.mock('~/flash');
-jest.mock('~/lib/utils/url_utility');
describe('ValueStreamMetrics', () => {
let wrapper;
let mockGetValueStreamSummaryMetrics;
+ let mockFilterFn;
const { full_path: requestPath } = group;
const fakeReqName = 'Mock metrics';
@@ -24,17 +26,18 @@ describe('ValueStreamMetrics', () => {
name: fakeReqName,
});
- const createComponent = ({ requestParams = {} } = {}) => {
+ const createComponent = (props = {}) => {
return shallowMount(ValueStreamMetrics, {
propsData: {
requestPath,
- requestParams,
+ requestParams: {},
requests: [metricsRequestFactory()],
+ ...props,
},
});
};
- const findMetrics = () => wrapper.findAllComponents(GlSingleStat);
+ const findMetrics = () => wrapper.findAllComponents(MetricTile);
const expectToHaveRequest = (fields) => {
expect(mockGetValueStreamSummaryMetrics).toHaveBeenCalledWith({
@@ -55,19 +58,19 @@ describe('ValueStreamMetrics', () => {
});
it('will display a loader with pending requests', async () => {
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.findComponent(GlSkeletonLoading).exists()).toBe(true);
});
- it('renders hidden GlSingleStat components for each metric', async () => {
+ it('renders hidden MetricTile components for each metric', async () => {
await waitForPromises();
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ isLoading: true });
- await wrapper.vm.$nextTick();
+ await nextTick();
const components = findMetrics();
@@ -88,35 +91,52 @@ describe('ValueStreamMetrics', () => {
});
describe.each`
- index | value | title | unit | clickable
- ${0} | ${metricsData[0].value} | ${metricsData[0].title} | ${metricsData[0].unit} | ${false}
- ${1} | ${metricsData[1].value} | ${metricsData[1].title} | ${metricsData[1].unit} | ${false}
- ${2} | ${metricsData[2].value} | ${metricsData[2].title} | ${metricsData[2].unit} | ${false}
- ${3} | ${metricsData[3].value} | ${metricsData[3].title} | ${metricsData[3].unit} | ${true}
- `('metric tiles', ({ index, value, title, unit, clickable }) => {
- it(`renders a single stat component for "${title}" with value and unit`, () => {
+ index | identifier | value | label
+ ${0} | ${metricsData[0].identifier} | ${metricsData[0].value} | ${metricsData[0].title}
+ ${1} | ${metricsData[1].identifier} | ${metricsData[1].value} | ${metricsData[1].title}
+ ${2} | ${metricsData[2].identifier} | ${metricsData[2].value} | ${metricsData[2].title}
+ ${3} | ${metricsData[3].identifier} | ${metricsData[3].value} | ${metricsData[3].title}
+ `('metric tiles', ({ identifier, index, value, label }) => {
+ it(`renders a metric tile component for "${label}"`, () => {
const metric = findMetrics().at(index);
- expect(metric.props()).toMatchObject({ value, title, unit: unit ?? '' });
+ expect(metric.props('metric')).toMatchObject({ identifier, value, label });
expect(metric.isVisible()).toBe(true);
});
-
- it(`${
- clickable ? 'redirects' : "doesn't redirect"
- } when the user clicks the "${title}" metric`, () => {
- const metric = findMetrics().at(index);
- metric.vm.$emit('click');
- if (clickable) {
- expect(redirectTo).toHaveBeenCalledWith(metricsData[index].links[0].url);
- } else {
- expect(redirectTo).not.toHaveBeenCalled();
- }
- });
});
it('will not display a loading icon', () => {
expect(wrapper.find(GlSkeletonLoading).exists()).toBe(false);
});
+ describe('filterFn', () => {
+ const transferedMetricsData = prepareTimeMetricsData(metricsData, METRICS_POPOVER_CONTENT);
+
+ it('with a filter function, will call the function with the metrics data', async () => {
+ const filteredData = [
+ { identifier: 'issues', value: '3', title: 'New Issues', description: 'foo' },
+ ];
+ mockFilterFn = jest.fn(() => filteredData);
+
+ wrapper = createComponent({
+ filterFn: mockFilterFn,
+ });
+
+ await waitForPromises();
+
+ expect(mockFilterFn).toHaveBeenCalledWith(transferedMetricsData);
+ expect(wrapper.vm.metrics).toEqual(filteredData);
+ });
+
+ it('without a filter function, it will only update the metrics', async () => {
+ wrapper = createComponent();
+
+ await waitForPromises();
+
+ expect(mockFilterFn).not.toHaveBeenCalled();
+ expect(wrapper.vm.metrics).toEqual(transferedMetricsData);
+ });
+ });
+
describe('with additional params', () => {
beforeEach(async () => {
wrapper = createComponent({
diff --git a/spec/frontend/deploy_freeze/components/deploy_freeze_settings_spec.js b/spec/frontend/deploy_freeze/components/deploy_freeze_settings_spec.js
index 392652292cf..cc044800e5e 100644
--- a/spec/frontend/deploy_freeze/components/deploy_freeze_settings_spec.js
+++ b/spec/frontend/deploy_freeze/components/deploy_freeze_settings_spec.js
@@ -1,4 +1,5 @@
-import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import DeployFreezeModal from '~/deploy_freeze/components/deploy_freeze_modal.vue';
import DeployFreezeSettings from '~/deploy_freeze/components/deploy_freeze_settings.vue';
@@ -6,8 +7,7 @@ import DeployFreezeTable from '~/deploy_freeze/components/deploy_freeze_table.vu
import createStore from '~/deploy_freeze/store';
import { timezoneDataFixture } from '../helpers';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('Deploy freeze settings', () => {
let wrapper;
@@ -20,7 +20,6 @@ describe('Deploy freeze settings', () => {
});
jest.spyOn(store, 'dispatch').mockImplementation();
wrapper = shallowMount(DeployFreezeSettings, {
- localVue,
store,
});
});
diff --git a/spec/frontend/deploy_freeze/components/deploy_freeze_table_spec.js b/spec/frontend/deploy_freeze/components/deploy_freeze_table_spec.js
index 403d0dce3fc..137776edfab 100644
--- a/spec/frontend/deploy_freeze/components/deploy_freeze_table_spec.js
+++ b/spec/frontend/deploy_freeze/components/deploy_freeze_table_spec.js
@@ -1,13 +1,13 @@
import { GlModal } from '@gitlab/ui';
-import { createLocalVue, mount } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import DeployFreezeTable from '~/deploy_freeze/components/deploy_freeze_table.vue';
import createStore from '~/deploy_freeze/store';
import { RECEIVE_FREEZE_PERIODS_SUCCESS } from '~/deploy_freeze/store/mutation_types';
import { freezePeriodsFixture, timezoneDataFixture } from '../helpers';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('Deploy freeze table', () => {
let wrapper;
@@ -21,7 +21,6 @@ describe('Deploy freeze table', () => {
jest.spyOn(store, 'dispatch').mockImplementation();
wrapper = mount(DeployFreezeTable, {
attachTo: document.body,
- localVue,
store,
});
};
@@ -57,7 +56,7 @@ describe('Deploy freeze table', () => {
describe('with data', () => {
beforeEach(async () => {
store.commit(RECEIVE_FREEZE_PERIODS_SUCCESS, freezePeriodsFixture);
- await wrapper.vm.$nextTick();
+ await nextTick();
});
it('displays data', () => {
@@ -69,7 +68,7 @@ describe('Deploy freeze table', () => {
it('allows user to edit deploy freeze', async () => {
findEditDeployFreezeButton().trigger('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(store.dispatch).toHaveBeenCalledWith(
'setFreezePeriod',
diff --git a/spec/frontend/deploy_freeze/components/timezone_dropdown_spec.js b/spec/frontend/deploy_freeze/components/timezone_dropdown_spec.js
index 5f4d4071f29..aea81daecef 100644
--- a/spec/frontend/deploy_freeze/components/timezone_dropdown_spec.js
+++ b/spec/frontend/deploy_freeze/components/timezone_dropdown_spec.js
@@ -1,12 +1,12 @@
import { GlDropdownItem, GlDropdown } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import createStore from '~/deploy_freeze/store';
import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown.vue';
import { findTzByName, formatTz, timezoneDataFixture } from '../helpers';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('Deploy freeze timezone dropdown', () => {
let wrapper;
@@ -19,7 +19,6 @@ describe('Deploy freeze timezone dropdown', () => {
});
wrapper = shallowMount(TimezoneDropdown, {
store,
- localVue,
propsData: {
value: selectedTimezone,
timezoneData: timezoneDataFixture,
diff --git a/spec/frontend/deploy_keys/components/action_btn_spec.js b/spec/frontend/deploy_keys/components/action_btn_spec.js
index 6ac68061518..c4c7a9aea2d 100644
--- a/spec/frontend/deploy_keys/components/action_btn_spec.js
+++ b/spec/frontend/deploy_keys/components/action_btn_spec.js
@@ -1,5 +1,6 @@
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import data from 'test_fixtures/deploy_keys/keys.json';
import actionBtn from '~/deploy_keys/components/action_btn.vue';
import eventHub from '~/deploy_keys/eventhub';
@@ -37,21 +38,19 @@ describe('Deploy keys action btn', () => {
});
});
- it('sends eventHub event with btn type', () => {
+ it('sends eventHub event with btn type', async () => {
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
findButton().vm.$emit('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(eventHub.$emit).toHaveBeenCalledWith('enable.key', deployKey, expect.anything());
- });
+ await nextTick();
+ expect(eventHub.$emit).toHaveBeenCalledWith('enable.key', deployKey, expect.anything());
});
- it('shows loading spinner after click', () => {
+ it('shows loading spinner after click', async () => {
findButton().vm.$emit('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(findButton().props('loading')).toBe(true);
- });
+ await nextTick();
+ expect(findButton().props('loading')).toBe(true);
});
});
diff --git a/spec/frontend/deploy_keys/components/app_spec.js b/spec/frontend/deploy_keys/components/app_spec.js
index 598b7a0f173..79a9aaa9184 100644
--- a/spec/frontend/deploy_keys/components/app_spec.js
+++ b/spec/frontend/deploy_keys/components/app_spec.js
@@ -1,5 +1,6 @@
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+import { nextTick } from 'vue';
import data from 'test_fixtures/deploy_keys/keys.json';
import waitForPromises from 'helpers/wait_for_promises';
import { TEST_HOST } from 'spec/test_constants';
@@ -39,20 +40,18 @@ describe('Deploy keys app component', () => {
const findKeyPanels = () => wrapper.findAll('.deploy-keys .gl-tabs-nav li');
const findModal = () => wrapper.findComponent(ConfirmModal);
- it('renders loading icon while waiting for request', () => {
+ it('renders loading icon while waiting for request', async () => {
mock.onGet(TEST_ENDPOINT).reply(() => new Promise());
mountComponent();
- return wrapper.vm.$nextTick().then(() => {
- expect(findLoadingIcon().exists()).toBe(true);
- });
+ await nextTick();
+ expect(findLoadingIcon().exists()).toBe(true);
});
- it('renders keys panels', () => {
- return mountComponent().then(() => {
- expect(findKeyPanels().length).toBe(3);
- });
+ it('renders keys panels', async () => {
+ await mountComponent();
+ expect(findKeyPanels().length).toBe(3);
});
it.each`
@@ -75,72 +74,55 @@ describe('Deploy keys app component', () => {
});
});
- it('re-fetches deploy keys when enabling a key', () => {
+ it('re-fetches deploy keys when enabling a key', async () => {
const key = data.public_keys[0];
- return mountComponent()
- .then(() => {
- jest.spyOn(wrapper.vm.service, 'getKeys').mockImplementation(() => {});
- jest.spyOn(wrapper.vm.service, 'enableKey').mockImplementation(() => Promise.resolve());
-
- eventHub.$emit('enable.key', key);
-
- return wrapper.vm.$nextTick();
- })
- .then(() => {
- expect(wrapper.vm.service.enableKey).toHaveBeenCalledWith(key.id);
- expect(wrapper.vm.service.getKeys).toHaveBeenCalled();
- });
+ await mountComponent();
+ jest.spyOn(wrapper.vm.service, 'getKeys').mockImplementation(() => {});
+ jest.spyOn(wrapper.vm.service, 'enableKey').mockImplementation(() => Promise.resolve());
+
+ eventHub.$emit('enable.key', key);
+
+ await nextTick();
+ expect(wrapper.vm.service.enableKey).toHaveBeenCalledWith(key.id);
+ expect(wrapper.vm.service.getKeys).toHaveBeenCalled();
});
- it('re-fetches deploy keys when disabling a key', () => {
+ it('re-fetches deploy keys when disabling a key', async () => {
const key = data.public_keys[0];
- return mountComponent()
- .then(() => {
- jest.spyOn(wrapper.vm.service, 'getKeys').mockImplementation(() => {});
- jest.spyOn(wrapper.vm.service, 'disableKey').mockImplementation(() => Promise.resolve());
-
- eventHub.$emit('disable.key', key, () => {});
-
- return wrapper.vm.$nextTick();
- })
- .then(() => {
- expect(findModal().props('visible')).toBe(true);
- findModal().vm.$emit('remove');
-
- return wrapper.vm.$nextTick();
- })
- .then(() => {
- expect(wrapper.vm.service.disableKey).toHaveBeenCalledWith(key.id);
- expect(wrapper.vm.service.getKeys).toHaveBeenCalled();
- });
+ await mountComponent();
+ jest.spyOn(wrapper.vm.service, 'getKeys').mockImplementation(() => {});
+ jest.spyOn(wrapper.vm.service, 'disableKey').mockImplementation(() => Promise.resolve());
+
+ eventHub.$emit('disable.key', key, () => {});
+
+ await nextTick();
+ expect(findModal().props('visible')).toBe(true);
+ findModal().vm.$emit('remove');
+
+ await nextTick();
+ expect(wrapper.vm.service.disableKey).toHaveBeenCalledWith(key.id);
+ expect(wrapper.vm.service.getKeys).toHaveBeenCalled();
});
- it('calls disableKey when removing a key', () => {
+ it('calls disableKey when removing a key', async () => {
const key = data.public_keys[0];
- return mountComponent()
- .then(() => {
- jest.spyOn(wrapper.vm.service, 'getKeys').mockImplementation(() => {});
- jest.spyOn(wrapper.vm.service, 'disableKey').mockImplementation(() => Promise.resolve());
-
- eventHub.$emit('remove.key', key, () => {});
-
- return wrapper.vm.$nextTick();
- })
- .then(() => {
- expect(findModal().props('visible')).toBe(true);
- findModal().vm.$emit('remove');
-
- return wrapper.vm.$nextTick();
- })
- .then(() => {
- expect(wrapper.vm.service.disableKey).toHaveBeenCalledWith(key.id);
- expect(wrapper.vm.service.getKeys).toHaveBeenCalled();
- });
+ await mountComponent();
+ jest.spyOn(wrapper.vm.service, 'getKeys').mockImplementation(() => {});
+ jest.spyOn(wrapper.vm.service, 'disableKey').mockImplementation(() => Promise.resolve());
+
+ eventHub.$emit('remove.key', key, () => {});
+
+ await nextTick();
+ expect(findModal().props('visible')).toBe(true);
+ findModal().vm.$emit('remove');
+
+ await nextTick();
+ expect(wrapper.vm.service.disableKey).toHaveBeenCalledWith(key.id);
+ expect(wrapper.vm.service.getKeys).toHaveBeenCalled();
});
- it('hasKeys returns true when there are keys', () => {
- return mountComponent().then(() => {
- expect(wrapper.vm.hasKeys).toEqual(3);
- });
+ it('hasKeys returns true when there are keys', async () => {
+ await mountComponent();
+ expect(wrapper.vm.hasKeys).toEqual(3);
});
});
diff --git a/spec/frontend/deploy_keys/components/key_spec.js b/spec/frontend/deploy_keys/components/key_spec.js
index 51c120d8213..8599c55c908 100644
--- a/spec/frontend/deploy_keys/components/key_spec.js
+++ b/spec/frontend/deploy_keys/components/key_spec.js
@@ -1,4 +1,5 @@
import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import data from 'test_fixtures/deploy_keys/keys.json';
import key from '~/deploy_keys/components/key.vue';
import DeployKeysStore from '~/deploy_keys/store';
@@ -95,18 +96,17 @@ describe('Deploy keys key', () => {
expect(labels.at(1).attributes('title')).toContain('Expand');
});
- it('expands all project labels after click', () => {
+ it('expands all project labels after click', async () => {
createComponent({ deployKey });
const { length } = deployKey.deploy_keys_projects;
wrapper.findAll('.deploy-project-label').at(1).trigger('click');
- return wrapper.vm.$nextTick().then(() => {
- const labels = wrapper.findAll('.deploy-project-label');
+ await nextTick();
+ const labels = wrapper.findAll('.deploy-project-label');
- expect(labels.length).toBe(length);
- expect(labels.at(1).text()).not.toContain(`+${length} others`);
- expect(labels.at(1).attributes('title')).not.toContain('Expand');
- });
+ expect(labels.length).toBe(length);
+ expect(labels.at(1).text()).not.toContain(`+${length} others`);
+ expect(labels.at(1).attributes('title')).not.toContain('Expand');
});
it('shows two projects', () => {
diff --git a/spec/frontend/design_management/components/delete_button_spec.js b/spec/frontend/design_management/components/delete_button_spec.js
index f5a841d35b8..e3907fdbe15 100644
--- a/spec/frontend/design_management/components/delete_button_spec.js
+++ b/spec/frontend/design_management/components/delete_button_spec.js
@@ -1,5 +1,6 @@
import { GlButton, GlModal, GlModalDirective } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import BatchDeleteButton from '~/design_management/components/delete_button.vue';
describe('Batch delete button component', () => {
@@ -36,18 +37,15 @@ describe('Batch delete button component', () => {
expect(findButton().attributes('disabled')).toBeTruthy();
});
- it('emits `delete-selected-designs` event on modal ok click', () => {
+ it('emits `delete-selected-designs` event on modal ok click', async () => {
createComponent();
findButton().vm.$emit('click');
- return wrapper.vm
- .$nextTick()
- .then(() => {
- findModal().vm.$emit('ok');
- return wrapper.vm.$nextTick();
- })
- .then(() => {
- expect(wrapper.emitted('delete-selected-designs')).toBeTruthy();
- });
+
+ await nextTick();
+ findModal().vm.$emit('ok');
+
+ await nextTick();
+ expect(wrapper.emitted('delete-selected-designs')).toBeTruthy();
});
it('renders slot content', () => {
diff --git a/spec/frontend/design_management/components/design_notes/design_discussion_spec.js b/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
index e816a05ba53..bbf2190ad47 100644
--- a/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
+++ b/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
@@ -1,6 +1,7 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { ApolloMutation } from 'vue-apollo';
+import { nextTick } from 'vue';
import DesignDiscussion from '~/design_management/components/design_notes/design_discussion.vue';
import DesignNote from '~/design_management/components/design_notes/design_note.vue';
import DesignNoteSignedOut from '~/design_management/components/design_notes/design_note_signed_out.vue';
@@ -119,12 +120,11 @@ describe('Design discussions component', () => {
expect(findResolveIcon().exists()).toBe(false);
});
- it('does not render a checkbox in reply form', () => {
+ it('does not render a checkbox in reply form', async () => {
findReplyPlaceholder().vm.$emit('focus');
- return wrapper.vm.$nextTick().then(() => {
- expect(findResolveCheckbox().exists()).toBe(false);
- });
+ await nextTick();
+ expect(findResolveCheckbox().exists()).toBe(false);
});
});
@@ -150,13 +150,12 @@ describe('Design discussions component', () => {
expect(findResolveIcon().props('name')).toBe('check-circle');
});
- it('renders a checkbox with Resolve thread text in reply form', () => {
+ it('renders a checkbox with Resolve thread text in reply form', async () => {
findReplyPlaceholder().vm.$emit('focus');
wrapper.setProps({ discussionWithOpenForm: defaultMockDiscussion.id });
- return wrapper.vm.$nextTick().then(() => {
- expect(findResolveCheckbox().text()).toBe('Resolve thread');
- });
+ await nextTick();
+ expect(findResolveCheckbox().text()).toBe('Resolve thread');
});
it('does not render resolved message', () => {
@@ -216,7 +215,7 @@ describe('Design discussions component', () => {
findReplyForm().vm.$emit('submitForm');
await mutate();
- await wrapper.vm.$nextTick();
+ await nextTick();
const dispatchedEvent = dispatchEventSpy.mock.calls[0][0];
@@ -226,9 +225,9 @@ describe('Design discussions component', () => {
});
describe('when replies are expanded', () => {
- beforeEach(() => {
+ beforeEach(async () => {
findRepliesWidget().vm.$emit('toggle');
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('renders replies widget with collapsed prop equal to false', () => {
@@ -243,26 +242,24 @@ describe('Design discussions component', () => {
expect(findReplyPlaceholder().isVisible()).toBe(true);
});
- it('renders a checkbox with Unresolve thread text in reply form', () => {
+ it('renders a checkbox with Unresolve thread text in reply form', async () => {
findReplyPlaceholder().vm.$emit('focus');
wrapper.setProps({ discussionWithOpenForm: defaultMockDiscussion.id });
- return wrapper.vm.$nextTick().then(() => {
- expect(findResolveCheckbox().text()).toBe('Unresolve thread');
- });
+ await nextTick();
+ expect(findResolveCheckbox().text()).toBe('Unresolve thread');
});
});
});
- it('hides reply placeholder and opens form on placeholder click', () => {
+ it('hides reply placeholder and opens form on placeholder click', async () => {
createComponent();
findReplyPlaceholder().vm.$emit('focus');
wrapper.setProps({ discussionWithOpenForm: defaultMockDiscussion.id });
- return wrapper.vm.$nextTick().then(() => {
- expect(findReplyPlaceholder().exists()).toBe(false);
- expect(findReplyForm().exists()).toBe(true);
- });
+ await nextTick();
+ expect(findReplyPlaceholder().exists()).toBe(false);
+ expect(findReplyForm().exists()).toBe(true);
});
it('calls mutation on submitting form and closes the form', async () => {
@@ -275,28 +272,24 @@ describe('Design discussions component', () => {
expect(mutate).toHaveBeenCalledWith(mutationVariables);
await mutate();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findReplyForm().exists()).toBe(false);
});
- it('clears the discussion comment on closing comment form', () => {
+ it('clears the discussion comment on closing comment form', async () => {
createComponent(
{ discussionWithOpenForm: defaultMockDiscussion.id },
{ discussionComment: 'test', isFormRendered: true },
);
- return wrapper.vm
- .$nextTick()
- .then(() => {
- findReplyForm().vm.$emit('cancel-form');
+ await nextTick();
+ findReplyForm().vm.$emit('cancel-form');
- expect(wrapper.vm.discussionComment).toBe('');
- return wrapper.vm.$nextTick();
- })
- .then(() => {
- expect(findReplyForm().exists()).toBe(false);
- });
+ expect(wrapper.vm.discussionComment).toBe('');
+
+ await nextTick();
+ expect(findReplyForm().exists()).toBe(false);
});
describe('when any note from a discussion is active', () => {
@@ -322,7 +315,7 @@ describe('Design discussions component', () => {
);
});
- it('calls toggleResolveDiscussion mutation on resolve thread button click', () => {
+ it('calls toggleResolveDiscussion mutation on resolve thread button click', async () => {
createComponent();
findResolveButton().trigger('click');
expect(mutate).toHaveBeenCalledWith({
@@ -332,9 +325,8 @@ describe('Design discussions component', () => {
resolve: true,
},
});
- return wrapper.vm.$nextTick(() => {
- expect(findResolveLoadingIcon().exists()).toBe(true);
- });
+ await nextTick();
+ expect(findResolveLoadingIcon().exists()).toBe(true);
});
it('calls toggleResolveDiscussion mutation after adding a note if checkbox was checked', () => {
diff --git a/spec/frontend/design_management/components/design_notes/design_note_spec.js b/spec/frontend/design_management/components/design_notes/design_note_spec.js
index 3f5f5bcdfa7..35fd1273270 100644
--- a/spec/frontend/design_management/components/design_notes/design_note_spec.js
+++ b/spec/frontend/design_management/components/design_notes/design_note_spec.js
@@ -1,5 +1,6 @@
import { shallowMount } from '@vue/test-utils';
import { ApolloMutation } from 'vue-apollo';
+import { nextTick } from 'vue';
import DesignNote from '~/design_management/components/design_notes/design_note.vue';
import DesignReplyForm from '~/design_management/components/design_notes/design_reply_form.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -96,7 +97,7 @@ describe('Design note component', () => {
});
describe('when user has a permission to edit note', () => {
- it('should open an edit form on edit button click', () => {
+ it('should open an edit form on edit button click', async () => {
createComponent({
note: {
...note,
@@ -108,10 +109,9 @@ describe('Design note component', () => {
findEditButton().trigger('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(findReplyForm().exists()).toBe(true);
- expect(findNoteContent().exists()).toBe(false);
- });
+ await nextTick();
+ expect(findReplyForm().exists()).toBe(true);
+ expect(findNoteContent().exists()).toBe(false);
});
describe('when edit form is rendered', () => {
@@ -134,27 +134,22 @@ describe('Design note component', () => {
expect(findReplyForm().exists()).toBe(true);
});
- it('hides the form on cancel-form event', () => {
+ it('hides the form on cancel-form event', async () => {
findReplyForm().vm.$emit('cancel-form');
- return wrapper.vm.$nextTick().then(() => {
- expect(findReplyForm().exists()).toBe(false);
- expect(findNoteContent().exists()).toBe(true);
- });
+ await nextTick();
+ expect(findReplyForm().exists()).toBe(false);
+ expect(findNoteContent().exists()).toBe(true);
});
- it('calls a mutation on submit-form event and hides a form', () => {
+ it('calls a mutation on submit-form event and hides a form', async () => {
findReplyForm().vm.$emit('submit-form');
expect(mutate).toHaveBeenCalled();
- return mutate()
- .then(() => {
- return wrapper.vm.$nextTick();
- })
- .then(() => {
- expect(findReplyForm().exists()).toBe(false);
- expect(findNoteContent().exists()).toBe(true);
- });
+ await mutate();
+ await nextTick();
+ expect(findReplyForm().exists()).toBe(false);
+ expect(findNoteContent().exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js b/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js
index a338a5ef200..0cef18c60de 100644
--- a/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js
+++ b/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js
@@ -1,4 +1,5 @@
import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import DesignReplyForm from '~/design_management/components/design_notes/design_reply_form.vue';
const showModal = jest.fn();
@@ -30,6 +31,10 @@ describe('Design reply form component', () => {
});
}
+ beforeEach(() => {
+ gon.features = { markdownContinueLists: true };
+ });
+
afterEach(() => {
wrapper.destroy();
});
@@ -64,24 +69,22 @@ describe('Design reply form component', () => {
expect(findSubmitButton().attributes().disabled).toBeTruthy();
});
- it('does not emit submitForm event on textarea ctrl+enter keydown', () => {
+ it('does not emit submitForm event on textarea ctrl+enter keydown', async () => {
findTextarea().trigger('keydown.enter', {
ctrlKey: true,
});
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted('submit-form')).toBeFalsy();
- });
+ await nextTick();
+ expect(wrapper.emitted('submit-form')).toBeFalsy();
});
- it('does not emit submitForm event on textarea meta+enter keydown', () => {
+ it('does not emit submitForm event on textarea meta+enter keydown', async () => {
findTextarea().trigger('keydown.enter', {
metaKey: true,
});
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted('submit-form')).toBeFalsy();
- });
+ await nextTick();
+ expect(wrapper.emitted('submit-form')).toBeFalsy();
});
it('emits cancelForm event on pressing escape button on textarea', () => {
@@ -108,40 +111,36 @@ describe('Design reply form component', () => {
expect(findSubmitButton().attributes().disabled).toBeFalsy();
});
- it('emits submitForm event on Comment button click', () => {
+ it('emits submitForm event on Comment button click', async () => {
findSubmitButton().vm.$emit('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted('submit-form')).toBeTruthy();
- });
+ await nextTick();
+ expect(wrapper.emitted('submit-form')).toBeTruthy();
});
- it('emits submitForm event on textarea ctrl+enter keydown', () => {
+ it('emits submitForm event on textarea ctrl+enter keydown', async () => {
findTextarea().trigger('keydown.enter', {
ctrlKey: true,
});
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted('submit-form')).toBeTruthy();
- });
+ await nextTick();
+ expect(wrapper.emitted('submit-form')).toBeTruthy();
});
- it('emits submitForm event on textarea meta+enter keydown', () => {
+ it('emits submitForm event on textarea meta+enter keydown', async () => {
findTextarea().trigger('keydown.enter', {
metaKey: true,
});
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted('submit-form')).toBeTruthy();
- });
+ await nextTick();
+ expect(wrapper.emitted('submit-form')).toBeTruthy();
});
- it('emits input event on changing textarea content', () => {
+ it('emits input event on changing textarea content', async () => {
findTextarea().setValue('test2');
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted('input')).toBeTruthy();
- });
+ await nextTick();
+ expect(wrapper.emitted('input')).toBeTruthy();
});
it('emits cancelForm event on Escape key if text was not changed', () => {
@@ -150,13 +149,12 @@ describe('Design reply form component', () => {
expect(wrapper.emitted('cancel-form')).toBeTruthy();
});
- it('opens confirmation modal on Escape key when text has changed', () => {
+ it('opens confirmation modal on Escape key when text has changed', async () => {
wrapper.setProps({ value: 'test2' });
- return wrapper.vm.$nextTick().then(() => {
- findTextarea().trigger('keyup.esc');
- expect(showModal).toHaveBeenCalled();
- });
+ await nextTick();
+ findTextarea().trigger('keyup.esc');
+ expect(showModal).toHaveBeenCalled();
});
it('emits cancelForm event on Cancel button click if text was not changed', () => {
@@ -165,13 +163,12 @@ describe('Design reply form component', () => {
expect(wrapper.emitted('cancel-form')).toBeTruthy();
});
- it('opens confirmation modal on Cancel button click when text has changed', () => {
+ it('opens confirmation modal on Cancel button click when text has changed', async () => {
wrapper.setProps({ value: 'test2' });
- return wrapper.vm.$nextTick().then(() => {
- findCancelButton().trigger('click');
- expect(showModal).toHaveBeenCalled();
- });
+ await nextTick();
+ findCancelButton().trigger('click');
+ expect(showModal).toHaveBeenCalled();
});
it('emits cancelForm event on modal Ok button click', () => {
diff --git a/spec/frontend/design_management/components/design_overlay_spec.js b/spec/frontend/design_management/components/design_overlay_spec.js
index 4bda5054090..056959425a6 100644
--- a/spec/frontend/design_management/components/design_overlay_spec.js
+++ b/spec/frontend/design_management/components/design_overlay_spec.js
@@ -1,4 +1,5 @@
import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import DesignOverlay from '~/design_management/components/design_overlay.vue';
import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '~/design_management/constants';
import updateActiveDiscussion from '~/design_management/graphql/mutations/update_active_discussion.mutation.graphql';
@@ -17,12 +18,11 @@ describe('Design overlay component', () => {
const findFirstBadge = () => findBadgeAtIndex(0);
const findSecondBadge = () => findBadgeAtIndex(1);
- const clickAndDragBadge = (elem, fromPoint, toPoint) => {
+ const clickAndDragBadge = async (elem, fromPoint, toPoint) => {
elem.trigger('mousedown', { clientX: fromPoint.x, clientY: fromPoint.y });
- return wrapper.vm.$nextTick().then(() => {
- elem.trigger('mousemove', { clientX: toPoint.x, clientY: toPoint.y });
- return wrapper.vm.$nextTick();
- });
+ await nextTick();
+ elem.trigger('mousemove', { clientX: toPoint.x, clientY: toPoint.y });
+ await nextTick();
};
function createComponent(props = {}, data = {}) {
@@ -59,7 +59,7 @@ describe('Design overlay component', () => {
expect(wrapper.attributes().style).toBe('width: 100px; height: 100px; top: 0px; left: 0px;');
});
- it('should emit `openCommentForm` when clicking on overlay', () => {
+ it('should emit `openCommentForm` when clicking on overlay', async () => {
createComponent();
const newCoordinates = {
x: 10,
@@ -69,11 +69,10 @@ describe('Design overlay component', () => {
wrapper
.find('[data-qa-selector="design_image_button"]')
.trigger('mouseup', { offsetX: newCoordinates.x, offsetY: newCoordinates.y });
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted('openCommentForm')).toEqual([
- [{ x: newCoordinates.x, y: newCoordinates.y }],
- ]);
- });
+ await nextTick();
+ expect(wrapper.emitted('openCommentForm')).toEqual([
+ [{ x: newCoordinates.x, y: newCoordinates.y }],
+ ]);
});
describe('with notes', () => {
@@ -116,7 +115,7 @@ describe('Design overlay component', () => {
describe('when a discussion is active', () => {
it.each([notes[0].discussion.notes.nodes[1], notes[0].discussion.notes.nodes[0]])(
'should not apply inactive class to the pin for the active discussion',
- (note) => {
+ async (note) => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({
@@ -126,13 +125,12 @@ describe('Design overlay component', () => {
},
});
- return wrapper.vm.$nextTick().then(() => {
- expect(findBadgeAtIndex(0).classes()).not.toContain('inactive');
- });
+ await nextTick();
+ expect(findBadgeAtIndex(0).classes()).not.toContain('inactive');
},
);
- it('should apply inactive class to all pins besides the active one', () => {
+ it('should apply inactive class to all pins besides the active one', async () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({
@@ -142,15 +140,14 @@ describe('Design overlay component', () => {
},
});
- return wrapper.vm.$nextTick().then(() => {
- expect(findSecondBadge().classes()).toContain('inactive');
- expect(findFirstBadge().classes()).not.toContain('inactive');
- });
+ await nextTick();
+ expect(findSecondBadge().classes()).toContain('inactive');
+ expect(findFirstBadge().classes()).not.toContain('inactive');
});
});
});
- it('should recalculate badges positions on window resize', () => {
+ it('should recalculate badges positions on window resize', async () => {
createComponent({
notes,
dimensions: {
@@ -168,12 +165,11 @@ describe('Design overlay component', () => {
},
});
- return wrapper.vm.$nextTick().then(() => {
- expect(findFirstBadge().attributes().style).toBe('left: 20px; top: 30px;');
- });
+ await nextTick();
+ expect(findFirstBadge().attributes().style).toBe('left: 20px; top: 30px;');
});
- it('should call an update active discussion mutation when clicking a note without moving it', () => {
+ it('should call an update active discussion mutation when clicking a note without moving it', async () => {
const note = notes[0];
const { position } = note;
const mutationVariables = {
@@ -186,31 +182,25 @@ describe('Design overlay component', () => {
findFirstBadge().trigger('mousedown', { clientX: position.x, clientY: position.y });
- return wrapper.vm.$nextTick().then(() => {
- findFirstBadge().trigger('mouseup', { clientX: position.x, clientY: position.y });
- expect(mutate).toHaveBeenCalledWith(mutationVariables);
- });
+ await nextTick();
+ findFirstBadge().trigger('mouseup', { clientX: position.x, clientY: position.y });
+ expect(mutate).toHaveBeenCalledWith(mutationVariables);
});
});
describe('when moving notes', () => {
- it('should update badge style when note is being moved', () => {
+ it('should update badge style when note is being moved', async () => {
createComponent({
notes,
});
const { position } = notes[0];
- return clickAndDragBadge(
- findFirstBadge(),
- { x: position.x, y: position.y },
- { x: 20, y: 20 },
- ).then(() => {
- expect(findFirstBadge().attributes().style).toBe('left: 20px; top: 20px;');
- });
+ await clickAndDragBadge(findFirstBadge(), { x: position.x, y: position.y }, { x: 20, y: 20 });
+ expect(findFirstBadge().attributes().style).toBe('left: 20px; top: 20px;');
});
- it('should emit `moveNote` event when note-moving action ends', () => {
+ it('should emit `moveNote` event when note-moving action ends', async () => {
createComponent({ notes });
const note = notes[0];
const { position } = note;
@@ -231,22 +221,19 @@ describe('Design overlay component', () => {
});
const badge = findFirstBadge();
- return clickAndDragBadge(badge, { x: position.x, y: position.y }, newCoordinates)
- .then(() => {
- badge.trigger('mouseup');
- return wrapper.vm.$nextTick();
- })
- .then(() => {
- expect(wrapper.emitted('moveNote')).toEqual([
- [
- {
- noteId: notes[0].id,
- discussionId: notes[0].discussion.id,
- coordinates: newCoordinates,
- },
- ],
- ]);
- });
+ await clickAndDragBadge(badge, { x: position.x, y: position.y }, newCoordinates);
+ badge.trigger('mouseup');
+
+ await nextTick();
+ expect(wrapper.emitted('moveNote')).toEqual([
+ [
+ {
+ noteId: notes[0].id,
+ discussionId: notes[0].discussion.id,
+ coordinates: newCoordinates,
+ },
+ ],
+ ]);
});
describe('without [repositionNote] permission', () => {
@@ -262,19 +249,18 @@ describe('Design overlay component', () => {
y: mockNoteNotAuthorised.position.y,
};
- it('should be unable to move a note', () => {
+ it('should be unable to move a note', async () => {
createComponent({
dimensions: mockDimensions,
notes: [mockNoteNotAuthorised],
});
const badge = findAllNotes().at(0);
- return clickAndDragBadge(badge, { ...mockNoteCoordinates }, { x: 20, y: 20 }).then(() => {
- // note position should not change after a click-and-drag attempt
- expect(findFirstBadge().attributes().style).toContain(
- `left: ${mockNoteCoordinates.x}px; top: ${mockNoteCoordinates.y}px;`,
- );
- });
+ await clickAndDragBadge(badge, { ...mockNoteCoordinates }, { x: 20, y: 20 });
+ // note position should not change after a click-and-drag attempt
+ expect(findFirstBadge().attributes().style).toContain(
+ `left: ${mockNoteCoordinates.x}px; top: ${mockNoteCoordinates.y}px;`,
+ );
});
});
});
@@ -292,7 +278,7 @@ describe('Design overlay component', () => {
});
describe('when moving the comment badge', () => {
- it('should update badge style to reflect new position', () => {
+ it('should update badge style to reflect new position', async () => {
const { position } = notes[0];
createComponent({
@@ -301,16 +287,15 @@ describe('Design overlay component', () => {
},
});
- return clickAndDragBadge(
+ await clickAndDragBadge(
findCommentBadge(),
{ x: position.x, y: position.y },
{ x: 20, y: 20 },
- ).then(() => {
- expect(findCommentBadge().attributes().style).toBe('left: 20px; top: 20px;');
- });
+ );
+ expect(findCommentBadge().attributes().style).toBe('left: 20px; top: 20px;');
});
- it('should update badge style when note-moving action ends', () => {
+ it('should update badge style when note-moving action ends', async () => {
const { position } = notes[0];
createComponent({
currentCommentForm: {
@@ -321,19 +306,16 @@ describe('Design overlay component', () => {
const commentBadge = findCommentBadge();
const toPoint = { x: 20, y: 20 };
- return clickAndDragBadge(commentBadge, { x: position.x, y: position.y }, toPoint)
- .then(() => {
- commentBadge.trigger('mouseup');
- // simulates the currentCommentForm being updated in index.vue component, and
- // propagated back down to this prop
- wrapper.setProps({
- currentCommentForm: { height: position.height, width: position.width, ...toPoint },
- });
- return wrapper.vm.$nextTick();
- })
- .then(() => {
- expect(commentBadge.attributes().style).toBe('left: 20px; top: 20px;');
- });
+ await clickAndDragBadge(commentBadge, { x: position.x, y: position.y }, toPoint);
+ commentBadge.trigger('mouseup');
+ // simulates the currentCommentForm being updated in index.vue component, and
+ // propagated back down to this prop
+ wrapper.setProps({
+ currentCommentForm: { height: position.height, width: position.width, ...toPoint },
+ });
+
+ await nextTick();
+ expect(commentBadge.attributes().style).toBe('left: 20px; top: 20px;');
});
it.each`
@@ -342,7 +324,7 @@ describe('Design overlay component', () => {
${'comment badge'} | ${findCommentBadge} | ${'mouseup'}
`(
'should emit `openCommentForm` event when $event fired on $element element',
- ({ getElementFunc, event }) => {
+ async ({ getElementFunc, event }) => {
createComponent({
notes,
currentCommentForm: {
@@ -364,9 +346,8 @@ describe('Design overlay component', () => {
});
getElementFunc().trigger(event);
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted('openCommentForm')).toEqual([[newCoordinates]]);
- });
+ await nextTick();
+ expect(wrapper.emitted('openCommentForm')).toEqual([[newCoordinates]]);
},
);
});
diff --git a/spec/frontend/design_management/components/design_presentation_spec.js b/spec/frontend/design_management/components/design_presentation_spec.js
index adec9ef469d..d79dde84d46 100644
--- a/spec/frontend/design_management/components/design_presentation_spec.js
+++ b/spec/frontend/design_management/components/design_presentation_spec.js
@@ -74,7 +74,7 @@ describe('Design management design presentation component', () => {
.mockReturnValue((childDimensions.height - viewportDimensions.height) * scrollTopPerc);
}
- function clickDragExplore(startCoords, endCoords, { useTouchEvents, mouseup } = {}) {
+ async function clickDragExplore(startCoords, endCoords, { useTouchEvents, mouseup } = {}) {
const event = useTouchEvents
? {
mousedown: 'touchstart',
@@ -96,24 +96,17 @@ describe('Design management design presentation component', () => {
clientX: startCoords.clientX,
clientY: startCoords.clientY,
});
- return wrapper.vm
- .$nextTick()
- .then(() => {
- addCommentOverlay.trigger(event.mousemove, {
- clientX: endCoords.clientX,
- clientY: endCoords.clientY,
- });
-
- return nextTick();
- })
- .then(() => {
- if (mouseup) {
- addCommentOverlay.trigger(event.mouseup);
- return nextTick();
- }
+ await nextTick();
+ addCommentOverlay.trigger(event.mousemove, {
+ clientX: endCoords.clientX,
+ clientY: endCoords.clientY,
+ });
- return undefined;
- });
+ await nextTick();
+ if (mouseup) {
+ addCommentOverlay.trigger(event.mouseup);
+ await nextTick();
+ }
}
beforeEach(() => {
@@ -125,7 +118,7 @@ describe('Design management design presentation component', () => {
window.gon = originalGon;
});
- it('renders image and overlay when image provided', () => {
+ it('renders image and overlay when image provided', async () => {
createComponent(
{
image: 'test.jpg',
@@ -134,20 +127,18 @@ describe('Design management design presentation component', () => {
mockOverlayData,
);
- return nextTick().then(() => {
- expect(wrapper.element).toMatchSnapshot();
- });
+ await nextTick();
+ expect(wrapper.element).toMatchSnapshot();
});
- it('renders empty state when no image provided', () => {
+ it('renders empty state when no image provided', async () => {
createComponent();
- return nextTick().then(() => {
- expect(wrapper.element).toMatchSnapshot();
- });
+ await nextTick();
+ expect(wrapper.element).toMatchSnapshot();
});
- it('openCommentForm event emits correct data', () => {
+ it('openCommentForm event emits correct data', async () => {
createComponent(
{
image: 'test.jpg',
@@ -158,15 +149,14 @@ describe('Design management design presentation component', () => {
wrapper.vm.openCommentForm({ x: 1, y: 1 });
- return nextTick().then(() => {
- expect(wrapper.emitted('openCommentForm')).toEqual([
- [{ ...mockOverlayData.overlayDimensions, x: 1, y: 1 }],
- ]);
- });
+ await nextTick();
+ expect(wrapper.emitted('openCommentForm')).toEqual([
+ [{ ...mockOverlayData.overlayDimensions, x: 1, y: 1 }],
+ ]);
});
describe('currentCommentForm', () => {
- it('is null when isAnnotating is false', () => {
+ it('is null when isAnnotating is false', async () => {
createComponent(
{
image: 'test.jpg',
@@ -175,13 +165,12 @@ describe('Design management design presentation component', () => {
mockOverlayData,
);
- return nextTick().then(() => {
- expect(wrapper.vm.currentCommentForm).toBeNull();
- expect(wrapper.element).toMatchSnapshot();
- });
+ await nextTick();
+ expect(wrapper.vm.currentCommentForm).toBeNull();
+ expect(wrapper.element).toMatchSnapshot();
});
- it('is null when isAnnotating is true but annotation position is falsey', () => {
+ it('is null when isAnnotating is true but annotation position is falsey', async () => {
createComponent(
{
image: 'test.jpg',
@@ -191,13 +180,12 @@ describe('Design management design presentation component', () => {
mockOverlayData,
);
- return nextTick().then(() => {
- expect(wrapper.vm.currentCommentForm).toBeNull();
- expect(wrapper.element).toMatchSnapshot();
- });
+ await nextTick();
+ expect(wrapper.vm.currentCommentForm).toBeNull();
+ expect(wrapper.element).toMatchSnapshot();
});
- it('is equal to current annotation position when isAnnotating is true', () => {
+ it('is equal to current annotation position when isAnnotating is true', async () => {
createComponent(
{
image: 'test.jpg',
@@ -215,15 +203,14 @@ describe('Design management design presentation component', () => {
},
);
- return nextTick().then(() => {
- expect(wrapper.vm.currentCommentForm).toEqual({
- x: 1,
- y: 1,
- width: 100,
- height: 100,
- });
- expect(wrapper.element).toMatchSnapshot();
+ await nextTick();
+ expect(wrapper.vm.currentCommentForm).toEqual({
+ x: 1,
+ y: 1,
+ width: 100,
+ height: 100,
});
+ expect(wrapper.element).toMatchSnapshot();
});
});
@@ -388,7 +375,7 @@ describe('Design management design presentation component', () => {
});
describe('onImageResize', () => {
- beforeEach(() => {
+ beforeEach(async () => {
createComponent(
{
image: 'test.jpg',
@@ -401,7 +388,7 @@ describe('Design management design presentation component', () => {
jest.spyOn(wrapper.vm, 'scaleZoomFocalPoint');
jest.spyOn(wrapper.vm, 'scrollToFocalPoint');
wrapper.vm.onImageResize({ width: 10, height: 10 });
- return nextTick();
+ await nextTick();
});
it('sets zoom focal point on initial load', () => {
@@ -409,12 +396,11 @@ describe('Design management design presentation component', () => {
expect(wrapper.vm.initialLoad).toBe(false);
});
- it('calls scaleZoomFocalPoint and scrollToFocalPoint after initial load', () => {
+ it('calls scaleZoomFocalPoint and scrollToFocalPoint after initial load', async () => {
wrapper.vm.onImageResize({ width: 10, height: 10 });
- return nextTick().then(() => {
- expect(wrapper.vm.scaleZoomFocalPoint).toHaveBeenCalled();
- expect(wrapper.vm.scrollToFocalPoint).toHaveBeenCalled();
- });
+ await nextTick();
+ expect(wrapper.vm.scaleZoomFocalPoint).toHaveBeenCalled();
+ expect(wrapper.vm.scrollToFocalPoint).toHaveBeenCalled();
});
});
@@ -498,7 +484,7 @@ describe('Design management design presentation component', () => {
);
});
- it('opens a comment form if design was not dragged', () => {
+ it('opens a comment form if design was not dragged', async () => {
const addCommentOverlay = findOverlayCommentButton();
const startCoords = {
clientX: 1,
@@ -510,15 +496,10 @@ describe('Design management design presentation component', () => {
clientY: startCoords.clientY,
});
- return wrapper.vm
- .$nextTick()
- .then(() => {
- addCommentOverlay.trigger('mouseup');
- return nextTick();
- })
- .then(() => {
- expect(wrapper.emitted('openCommentForm')).toBeDefined();
- });
+ await nextTick();
+ addCommentOverlay.trigger('mouseup');
+ await nextTick();
+ expect(wrapper.emitted('openCommentForm')).toBeDefined();
});
describe('when clicking and dragging', () => {
diff --git a/spec/frontend/design_management/components/design_scaler_spec.js b/spec/frontend/design_management/components/design_scaler_spec.js
index 095c070e5e8..a04e2ebda5b 100644
--- a/spec/frontend/design_management/components/design_scaler_spec.js
+++ b/spec/frontend/design_management/components/design_scaler_spec.js
@@ -1,5 +1,6 @@
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import DesignScaler from '~/design_management/components/design_scaler.vue';
describe('Design management design scaler component', () => {
@@ -32,7 +33,7 @@ describe('Design management design scaler component', () => {
describe('when `scale` value is greater than 1', () => {
beforeEach(async () => {
setScale(1.6);
- await wrapper.vm.$nextTick();
+ await nextTick();
});
it('emits @scale event when "reset" button clicked', () => {
@@ -68,11 +69,11 @@ describe('Design management design scaler component', () => {
it('computes & increments correct stepSize based on maxScale', async () => {
wrapper.setProps({ maxScale: 11 });
- await wrapper.vm.$nextTick();
+ await nextTick();
getIncreaseScaleButton().vm.$emit('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.emitted().scale[0][0]).toBe(3);
});
@@ -96,7 +97,7 @@ describe('Design management design scaler component', () => {
describe('when `scale` value is maximum', () => {
beforeEach(async () => {
setScale(2);
- await wrapper.vm.$nextTick();
+ await nextTick();
});
it('disables the "increment" button', () => {
diff --git a/spec/frontend/design_management/components/design_sidebar_spec.js b/spec/frontend/design_management/components/design_sidebar_spec.js
index 4cd71bdb7f3..a818a86bef6 100644
--- a/spec/frontend/design_management/components/design_sidebar_spec.js
+++ b/spec/frontend/design_management/components/design_sidebar_spec.js
@@ -1,6 +1,7 @@
import { GlCollapse, GlPopover } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Cookies from 'js-cookie';
+import { nextTick } from 'vue';
import DesignDiscussion from '~/design_management/components/design_notes/design_discussion.vue';
import DesignNoteSignedOut from '~/design_management/components/design_notes/design_note_signed_out.vue';
import DesignSidebar from '~/design_management/components/design_sidebar.vue';
@@ -138,14 +139,13 @@ describe('Design management design sidebar component', () => {
expect(wrapper.emitted('toggleResolvedComments')).toHaveLength(1);
});
- it('opens a collapsible when resolvedDiscussionsExpanded prop changes to true', () => {
+ it('opens a collapsible when resolvedDiscussionsExpanded prop changes to true', async () => {
expect(findCollapsible().attributes('visible')).toBeUndefined();
wrapper.setProps({
resolvedDiscussionsExpanded: true,
});
- return wrapper.vm.$nextTick().then(() => {
- expect(findCollapsible().attributes('visible')).toBe('true');
- });
+ await nextTick();
+ expect(findCollapsible().attributes('visible')).toBe('true');
});
it('does not popover about resolved comments', () => {
@@ -182,12 +182,11 @@ describe('Design management design sidebar component', () => {
expect(wrapper.emitted('resolveDiscussionError')).toEqual([['payload']]);
});
- it('changes prop correctly on opening discussion form', () => {
+ it('changes prop correctly on opening discussion form', async () => {
findFirstDiscussion().vm.$emit('open-form', 'some-id');
- return wrapper.vm.$nextTick().then(() => {
- expect(findFirstDiscussion().props('discussionWithOpenForm')).toBe('some-id');
- });
+ await nextTick();
+ expect(findFirstDiscussion().props('discussionWithOpenForm')).toBe('some-id');
});
});
@@ -246,17 +245,19 @@ describe('Design management design sidebar component', () => {
expect(scrollIntoViewMock).toHaveBeenCalled();
});
- it('dismisses a popover on the outside click', () => {
+ it('dismisses a popover on the outside click', async () => {
wrapper.trigger('click');
- return wrapper.vm.$nextTick(() => {
- expect(findPopover().exists()).toBe(false);
- });
+ await nextTick();
+ expect(findPopover().exists()).toBe(false);
});
it(`sets a ${cookieKey} cookie on clicking outside the popover`, () => {
jest.spyOn(Cookies, 'set');
wrapper.trigger('click');
- expect(Cookies.set).toHaveBeenCalledWith(cookieKey, 'true', { expires: 365 * 10 });
+ expect(Cookies.set).toHaveBeenCalledWith(cookieKey, 'true', {
+ expires: 365 * 10,
+ secure: false,
+ });
});
});
diff --git a/spec/frontend/design_management/components/design_todo_button_spec.js b/spec/frontend/design_management/components/design_todo_button_spec.js
index 757bf50c527..73661c9fcb0 100644
--- a/spec/frontend/design_management/components/design_todo_button_spec.js
+++ b/spec/frontend/design_management/components/design_todo_button_spec.js
@@ -1,4 +1,5 @@
import { shallowMount, mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import DesignTodoButton from '~/design_management/components/design_todo_button.vue';
import createDesignTodoMutation from '~/design_management/graphql/mutations/create_design_todo.mutation.graphql';
import todoMarkDoneMutation from '~/graphql_shared/mutations/todo_mark_done.mutation.graphql';
@@ -71,7 +72,7 @@ describe('Design management design todo button', () => {
describe('when clicked', () => {
let dispatchEventSpy;
- beforeEach(() => {
+ beforeEach(async () => {
dispatchEventSpy = jest.spyOn(document, 'dispatchEvent');
jest.spyOn(document, 'querySelector').mockReturnValue({
innerText: 2,
@@ -79,7 +80,7 @@ describe('Design management design todo button', () => {
createComponent({ design: mockDesignWithPendingTodos }, { mountFn: mount });
wrapper.trigger('click');
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('calls `$apollo.mutate` with the `todoMarkDone` mutation and variables containing `id`', async () => {
@@ -117,7 +118,7 @@ describe('Design management design todo button', () => {
describe('when clicked', () => {
let dispatchEventSpy;
- beforeEach(() => {
+ beforeEach(async () => {
dispatchEventSpy = jest.spyOn(document, 'dispatchEvent');
jest.spyOn(document, 'querySelector').mockReturnValue({
innerText: 2,
@@ -125,7 +126,7 @@ describe('Design management design todo button', () => {
createComponent({}, { mountFn: mount });
wrapper.trigger('click');
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('calls `$apollo.mutate` with the `createDesignTodoMutation` mutation and variables containing `issuable_id`, `issue_id`, & `projectPath`', async () => {
diff --git a/spec/frontend/design_management/components/image_spec.js b/spec/frontend/design_management/components/image_spec.js
index ac3afc73c86..e27b2bc9fa5 100644
--- a/spec/frontend/design_management/components/image_spec.js
+++ b/spec/frontend/design_management/components/image_spec.js
@@ -1,5 +1,6 @@
import { GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import DesignImage from '~/design_management/components/image.vue';
describe('Design management large image component', () => {
@@ -36,7 +37,7 @@ describe('Design management large image component', () => {
expect(wrapper.element).toMatchSnapshot();
});
- it('sets correct classes and styles if imageStyle is set', () => {
+ it('sets correct classes and styles if imageStyle is set', async () => {
createComponent(
{
isLoading: false,
@@ -50,12 +51,11 @@ describe('Design management large image component', () => {
},
},
);
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.element).toMatchSnapshot();
- });
+ await nextTick();
+ expect(wrapper.element).toMatchSnapshot();
});
- it('renders media broken icon on error', () => {
+ it('renders media broken icon on error', async () => {
createComponent({
isLoading: false,
image: 'test.jpg',
@@ -64,10 +64,9 @@ describe('Design management large image component', () => {
const image = wrapper.find('img');
image.trigger('error');
- return wrapper.vm.$nextTick().then(() => {
- expect(image.isVisible()).toBe(false);
- expect(wrapper.find(GlIcon).element).toMatchSnapshot();
- });
+ await nextTick();
+ expect(image.isVisible()).toBe(false);
+ expect(wrapper.find(GlIcon).element).toMatchSnapshot();
});
describe('zoom', () => {
@@ -99,37 +98,34 @@ describe('Design management large image component', () => {
.mockReturnValue(baseImageHeight);
});
- it('emits @resize event on zoom', () => {
+ it('emits @resize event on zoom', async () => {
const zoomAmount = 2;
wrapper.vm.zoom(zoomAmount);
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted('resize')).toEqual([
- [{ width: baseImageWidth * zoomAmount, height: baseImageHeight * zoomAmount }],
- ]);
- });
+ await nextTick();
+ expect(wrapper.emitted('resize')).toEqual([
+ [{ width: baseImageWidth * zoomAmount, height: baseImageHeight * zoomAmount }],
+ ]);
});
- it('emits @resize event with base image size when scale=1', () => {
+ it('emits @resize event with base image size when scale=1', async () => {
wrapper.vm.zoom(1);
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted('resize')).toEqual([
- [{ width: baseImageWidth, height: baseImageHeight }],
- ]);
- });
+ await nextTick();
+ expect(wrapper.emitted('resize')).toEqual([
+ [{ width: baseImageWidth, height: baseImageHeight }],
+ ]);
});
- it('sets image style when zoomed', () => {
+ it('sets image style when zoomed', async () => {
const zoomAmount = 2;
wrapper.vm.zoom(zoomAmount);
expect(wrapper.vm.imageStyle).toEqual({
width: `${baseImageWidth * zoomAmount}px`,
height: `${baseImageHeight * zoomAmount}px`,
});
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.element).toMatchSnapshot();
- });
+ await nextTick();
+ expect(wrapper.element).toMatchSnapshot();
});
});
});
diff --git a/spec/frontend/design_management/components/list/item_spec.js b/spec/frontend/design_management/components/list/item_spec.js
index ed105b112be..e00dda2015e 100644
--- a/spec/frontend/design_management/components/list/item_spec.js
+++ b/spec/frontend/design_management/components/list/item_spec.js
@@ -1,11 +1,11 @@
import { GlIcon, GlLoadingIcon, GlIntersectionObserver } from '@gitlab/ui';
-import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import VueRouter from 'vue-router';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import Item from '~/design_management/components/list/item.vue';
-const localVue = createLocalVue();
-localVue.use(VueRouter);
+Vue.use(VueRouter);
const router = new VueRouter();
// Referenced from: gitlab_schema.graphql:DesignVersionEvent
@@ -34,7 +34,6 @@ describe('Design management list item component', () => {
} = {}) {
wrapper = extendedWrapper(
shallowMount(Item, {
- localVue,
router,
propsData: {
id: imgId,
@@ -72,13 +71,13 @@ describe('Design management list item component', () => {
let image;
let glIntersectionObserver;
- beforeEach(() => {
+ beforeEach(async () => {
createComponent();
image = wrapper.find('img');
glIntersectionObserver = wrapper.find(GlIntersectionObserver);
glIntersectionObserver.vm.$emit('appear');
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('renders a tooltip', () => {
@@ -92,9 +91,9 @@ describe('Design management list item component', () => {
});
describe('after image is loaded', () => {
- beforeEach(() => {
+ beforeEach(async () => {
image.trigger('load');
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('renders an image', () => {
@@ -102,29 +101,27 @@ describe('Design management list item component', () => {
expect(image.isVisible()).toBe(true);
});
- it('renders media broken icon when image onerror triggered', () => {
+ it('renders media broken icon when image onerror triggered', async () => {
image.trigger('error');
- return wrapper.vm.$nextTick().then(() => {
- expect(image.isVisible()).toBe(false);
- expect(wrapper.find(GlIcon).element).toMatchSnapshot();
- });
+ await nextTick();
+ expect(image.isVisible()).toBe(false);
+ expect(wrapper.find(GlIcon).element).toMatchSnapshot();
});
describe('when imageV432x230 and image provided', () => {
- it('renders imageV432x230 image', () => {
+ it('renders imageV432x230 image', async () => {
const mockSrc = 'mock-imageV432x230-url';
wrapper.setProps({ imageV432x230: mockSrc });
- return wrapper.vm.$nextTick().then(() => {
- expect(image.attributes('src')).toBe(mockSrc);
- });
+ await nextTick();
+ expect(image.attributes('src')).toBe(mockSrc);
});
});
describe('when image disappears from view and then reappears', () => {
- beforeEach(() => {
+ beforeEach(async () => {
glIntersectionObserver.vm.$emit('appear');
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('renders an image', () => {
diff --git a/spec/frontend/design_management/components/toolbar/design_navigation_spec.js b/spec/frontend/design_management/components/toolbar/design_navigation_spec.js
index 6e0592984a2..38a7fadee79 100644
--- a/spec/frontend/design_management/components/toolbar/design_navigation_spec.js
+++ b/spec/frontend/design_management/components/toolbar/design_navigation_spec.js
@@ -1,6 +1,7 @@
/* global Mousetrap */
import 'mousetrap';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import DesignNavigation from '~/design_management/components/toolbar/design_navigation.vue';
import { DESIGN_ROUTE_NAME } from '~/design_management/router/constants';
@@ -41,16 +42,15 @@ describe('Design management pagination component', () => {
expect(wrapper.element).toMatchSnapshot();
});
- it('renders navigation buttons', () => {
+ it('renders navigation buttons', async () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({
designCollection: { designs: [{ id: '1' }, { id: '2' }] },
});
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.element).toMatchSnapshot();
- });
+ await nextTick();
+ expect(wrapper.element).toMatchSnapshot();
});
describe('keyboard buttons navigation', () => {
diff --git a/spec/frontend/design_management/components/toolbar/index_spec.js b/spec/frontend/design_management/components/toolbar/index_spec.js
index cf872046f53..412f3de911e 100644
--- a/spec/frontend/design_management/components/toolbar/index_spec.js
+++ b/spec/frontend/design_management/components/toolbar/index_spec.js
@@ -1,12 +1,12 @@
import { GlButton } from '@gitlab/ui';
-import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import VueRouter from 'vue-router';
import DeleteButton from '~/design_management/components/delete_button.vue';
import Toolbar from '~/design_management/components/toolbar/index.vue';
import { DESIGNS_ROUTE_NAME } from '~/design_management/router/constants';
-const localVue = createLocalVue();
-localVue.use(VueRouter);
+Vue.use(VueRouter);
const router = new VueRouter();
const RouterLinkStub = {
@@ -28,7 +28,6 @@ describe('Design management toolbar component', () => {
updatedAt.setHours(updatedAt.getHours() - 1);
wrapper = shallowMount(Toolbar, {
- localVue,
router,
propsData: {
id: '1',
@@ -61,60 +60,54 @@ describe('Design management toolbar component', () => {
wrapper.destroy();
});
- it('renders design and updated data', () => {
+ it('renders design and updated data', async () => {
createComponent();
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.element).toMatchSnapshot();
- });
+ await nextTick();
+ expect(wrapper.element).toMatchSnapshot();
});
- it('links back to designs list', () => {
+ it('links back to designs list', async () => {
createComponent();
- return wrapper.vm.$nextTick().then(() => {
- const link = wrapper.find('a');
+ await nextTick();
+ const link = wrapper.find('a');
- expect(link.props('to')).toEqual({
- name: DESIGNS_ROUTE_NAME,
- query: {
- version: undefined,
- },
- });
+ expect(link.props('to')).toEqual({
+ name: DESIGNS_ROUTE_NAME,
+ query: {
+ version: undefined,
+ },
});
});
- it('renders delete button on latest designs version with logged in user', () => {
+ it('renders delete button on latest designs version with logged in user', async () => {
createComponent();
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(DeleteButton).exists()).toBe(true);
- });
+ await nextTick();
+ expect(wrapper.find(DeleteButton).exists()).toBe(true);
});
- it('does not render delete button on non-latest version', () => {
+ it('does not render delete button on non-latest version', async () => {
createComponent(false, true, { isLatestVersion: false });
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(DeleteButton).exists()).toBe(false);
- });
+ await nextTick();
+ expect(wrapper.find(DeleteButton).exists()).toBe(false);
});
- it('does not render delete button when user is not logged in', () => {
+ it('does not render delete button when user is not logged in', async () => {
createComponent(false, false);
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(DeleteButton).exists()).toBe(false);
- });
+ await nextTick();
+ expect(wrapper.find(DeleteButton).exists()).toBe(false);
});
- it('emits `delete` event on deleteButton `delete-selected-designs` event', () => {
+ it('emits `delete` event on deleteButton `delete-selected-designs` event', async () => {
createComponent();
- return wrapper.vm.$nextTick().then(() => {
- wrapper.find(DeleteButton).vm.$emit('delete-selected-designs');
- expect(wrapper.emitted().delete).toBeTruthy();
- });
+ await nextTick();
+ wrapper.find(DeleteButton).vm.$emit('delete-selected-designs');
+ expect(wrapper.emitted().delete).toBeTruthy();
});
it('renders download button with correct link', () => {
diff --git a/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js b/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js
index a4fb671ae13..ec5db04bb80 100644
--- a/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js
+++ b/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js
@@ -1,5 +1,6 @@
import { GlDropdown, GlDropdownItem, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import DesignVersionDropdown from '~/design_management/components/upload/design_version_dropdown.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import mockAllVersions from './mock_data/all_versions';
@@ -47,77 +48,69 @@ describe('Design management design version dropdown component', () => {
const findVersionLink = (index) => wrapper.findAll(GlDropdownItem).at(index);
- it('renders design version dropdown button', () => {
+ it('renders design version dropdown button', async () => {
createComponent();
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.element).toMatchSnapshot();
- });
+ await nextTick();
+ expect(wrapper.element).toMatchSnapshot();
});
- it('renders design version list', () => {
+ it('renders design version list', async () => {
createComponent();
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.element).toMatchSnapshot();
- });
+ await nextTick();
+ expect(wrapper.element).toMatchSnapshot();
});
describe('selected version name', () => {
- it('has "latest" on most recent version item', () => {
+ it('has "latest" on most recent version item', async () => {
createComponent();
- return wrapper.vm.$nextTick().then(() => {
- expect(findVersionLink(0).text()).toContain('latest');
- });
+ await nextTick();
+ expect(findVersionLink(0).text()).toContain('latest');
});
});
describe('versions list', () => {
- it('displays latest version text by default', () => {
+ it('displays latest version text by default', async () => {
createComponent();
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(GlDropdown).attributes('text')).toBe('Showing latest version');
- });
+ await nextTick();
+ expect(wrapper.find(GlDropdown).attributes('text')).toBe('Showing latest version');
});
- it('displays latest version text when only 1 version is present', () => {
+ it('displays latest version text when only 1 version is present', async () => {
createComponent({ maxVersions: 1 });
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(GlDropdown).attributes('text')).toBe('Showing latest version');
- });
+ await nextTick();
+ expect(wrapper.find(GlDropdown).attributes('text')).toBe('Showing latest version');
});
- it('displays version text when the current version is not the latest', () => {
+ it('displays version text when the current version is not the latest', async () => {
createComponent({ $route: designRouteFactory(PREVIOUS_VERSION_ID) });
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(GlDropdown).attributes('text')).toBe(`Showing version #1`);
- });
+ await nextTick();
+ expect(wrapper.find(GlDropdown).attributes('text')).toBe(`Showing version #1`);
});
- it('displays latest version text when the current version is the latest', () => {
+ it('displays latest version text when the current version is the latest', async () => {
createComponent({ $route: designRouteFactory(LATEST_VERSION_ID) });
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(GlDropdown).attributes('text')).toBe('Showing latest version');
- });
+ await nextTick();
+ expect(wrapper.find(GlDropdown).attributes('text')).toBe('Showing latest version');
});
- it('should have the same length as apollo query', () => {
+ it('should have the same length as apollo query', async () => {
createComponent();
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.findAll(GlDropdownItem)).toHaveLength(wrapper.vm.allVersions.length);
- });
+ await nextTick();
+ expect(wrapper.findAll(GlDropdownItem)).toHaveLength(wrapper.vm.allVersions.length);
});
it('should render TimeAgo', async () => {
createComponent();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.findAllComponents(TimeAgo)).toHaveLength(wrapper.vm.allVersions.length);
});
diff --git a/spec/frontend/design_management/pages/design/index_spec.js b/spec/frontend/design_management/pages/design/index_spec.js
index 98e2313e9f2..55d0fabe402 100644
--- a/spec/frontend/design_management/pages/design/index_spec.js
+++ b/spec/frontend/design_management/pages/design/index_spec.js
@@ -1,5 +1,6 @@
import { GlAlert } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import { ApolloMutation } from 'vue-apollo';
import VueRouter from 'vue-router';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
@@ -78,8 +79,7 @@ const createDiscussionMutationVariables = {
},
};
-const localVue = createLocalVue();
-localVue.use(VueRouter);
+Vue.use(VueRouter);
describe('Design management design index page', () => {
let wrapper;
@@ -128,7 +128,6 @@ describe('Design management design index page', () => {
...data,
};
},
- localVue,
router,
});
}
@@ -154,11 +153,11 @@ describe('Design management design index page', () => {
jest.spyOn(utils, 'getPageLayoutElement').mockReturnValue(mockPageLayoutElement);
createComponent({ loading: false }, { data: { design, scale: 2 } });
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findDesignPresentation().props('scale')).toBe(2);
DesignIndex.beforeRouteUpdate.call(wrapper.vm, {}, {}, jest.fn());
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findDesignPresentation().props('scale')).toBe(1);
});
@@ -201,7 +200,7 @@ describe('Design management design index page', () => {
});
});
- it('opens a new discussion form', () => {
+ it('opens a new discussion form', async () => {
createComponent(
{ loading: false },
{
@@ -213,9 +212,8 @@ describe('Design management design index page', () => {
findDesignPresentation().vm.$emit('openCommentForm', { x: 0, y: 0 });
- return wrapper.vm.$nextTick().then(() => {
- expect(findDiscussionForm().exists()).toBe(true);
- });
+ await nextTick();
+ expect(findDiscussionForm().exists()).toBe(true);
});
it('keeps new discussion form focused', () => {
@@ -234,7 +232,7 @@ describe('Design management design index page', () => {
expect(focusInput).toHaveBeenCalled();
});
- it('sends a mutation on submitting form and closes form', () => {
+ it('sends a mutation on submitting form and closes form', async () => {
createComponent(
{ loading: false },
{
@@ -249,17 +247,12 @@ describe('Design management design index page', () => {
findDiscussionForm().vm.$emit('submit-form');
expect(mutate).toHaveBeenCalledWith(createDiscussionMutationVariables);
- return wrapper.vm
- .$nextTick()
- .then(() => {
- return mutate({ variables: createDiscussionMutationVariables });
- })
- .then(() => {
- expect(findDiscussionForm().exists()).toBe(false);
- });
+ await nextTick();
+ await mutate({ variables: createDiscussionMutationVariables });
+ expect(findDiscussionForm().exists()).toBe(false);
});
- it('closes the form and clears the comment on canceling form', () => {
+ it('closes the form and clears the comment on canceling form', async () => {
createComponent(
{ loading: false },
{
@@ -275,9 +268,8 @@ describe('Design management design index page', () => {
expect(wrapper.vm.comment).toBe('');
- return wrapper.vm.$nextTick().then(() => {
- expect(findDiscussionForm().exists()).toBe(false);
- });
+ await nextTick();
+ expect(findDiscussionForm().exists()).toBe(false);
});
describe('with error', () => {
@@ -300,22 +292,21 @@ describe('Design management design index page', () => {
describe('onDesignQueryResult', () => {
describe('with no designs', () => {
- it('redirects to /designs', () => {
+ it('redirects to /designs', async () => {
createComponent({ loading: true });
router.push = jest.fn();
wrapper.vm.onDesignQueryResult({ data: mockResponseNoDesigns, loading: false });
- return wrapper.vm.$nextTick().then(() => {
- expect(createFlash).toHaveBeenCalledTimes(1);
- expect(createFlash).toHaveBeenCalledWith({ message: DESIGN_NOT_FOUND_ERROR });
- expect(router.push).toHaveBeenCalledTimes(1);
- expect(router.push).toHaveBeenCalledWith({ name: DESIGNS_ROUTE_NAME });
- });
+ await nextTick();
+ expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createFlash).toHaveBeenCalledWith({ message: DESIGN_NOT_FOUND_ERROR });
+ expect(router.push).toHaveBeenCalledTimes(1);
+ expect(router.push).toHaveBeenCalledWith({ name: DESIGNS_ROUTE_NAME });
});
});
describe('when no design exists for given version', () => {
- it('redirects to /designs', () => {
+ it('redirects to /designs', async () => {
createComponent({ loading: true });
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
@@ -328,12 +319,11 @@ describe('Design management design index page', () => {
router.push = jest.fn();
wrapper.vm.onDesignQueryResult({ data: mockResponseWithDesigns, loading: false });
- return wrapper.vm.$nextTick().then(() => {
- expect(createFlash).toHaveBeenCalledTimes(1);
- expect(createFlash).toHaveBeenCalledWith({ message: DESIGN_VERSION_NOT_EXIST_ERROR });
- expect(router.push).toHaveBeenCalledTimes(1);
- expect(router.push).toHaveBeenCalledWith({ name: DESIGNS_ROUTE_NAME });
- });
+ await nextTick();
+ expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createFlash).toHaveBeenCalledWith({ message: DESIGN_VERSION_NOT_EXIST_ERROR });
+ expect(router.push).toHaveBeenCalledTimes(1);
+ expect(router.push).toHaveBeenCalledWith({ name: DESIGNS_ROUTE_NAME });
});
});
});
diff --git a/spec/frontend/design_management/pages/index_spec.js b/spec/frontend/design_management/pages/index_spec.js
index dd0f7972553..a240a41959f 100644
--- a/spec/frontend/design_management/pages/index_spec.js
+++ b/spec/frontend/design_management/pages/index_spec.js
@@ -1,10 +1,12 @@
import { GlEmptyState } from '@gitlab/ui';
-import { createLocalVue, shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
+
import VueApollo, { ApolloMutation } from 'vue-apollo';
import VueRouter from 'vue-router';
import VueDraggable from 'vuedraggable';
import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import permissionsQuery from 'shared_queries/design_management/design_permissions.query.graphql';
import getDesignListQuery from 'shared_queries/design_management/get_design_list.query.graphql';
@@ -48,9 +50,8 @@ jest.spyOn(utils, 'getPageLayoutElement').mockReturnValue(mockPageEl);
const scrollIntoViewMock = jest.fn();
HTMLElement.prototype.scrollIntoView = scrollIntoViewMock;
-const localVue = createLocalVue();
const router = createRouter();
-localVue.use(VueRouter);
+Vue.use(VueRouter);
const mockDesigns = [
{
@@ -115,8 +116,7 @@ describe('Design management index page', () => {
const findDesignToolbarWrapper = () => wrapper.find('[data-testid="design-toolbar-wrapper"]');
async function moveDesigns(localWrapper) {
- await jest.runOnlyPendingTimers();
- await nextTick();
+ await waitForPromises();
localWrapper.find(VueDraggable).vm.$emit('input', reorderedDesigns);
localWrapper.find(VueDraggable).vm.$emit('change', {
@@ -159,7 +159,6 @@ describe('Design management index page', () => {
};
},
mocks: { $apollo },
- localVue,
router,
stubs: { DesignDestroyer, ApolloMutation, VueDraggable, ...stubs },
attachTo: document.body,
@@ -175,7 +174,7 @@ describe('Design management index page', () => {
function createComponentWithApollo({
moveHandler = jest.fn().mockResolvedValue(moveDesignMutationResponse),
}) {
- localVue.use(VueApollo);
+ Vue.use(VueApollo);
moveDesignHandler = moveHandler;
const requestHandlers = [
@@ -186,7 +185,6 @@ describe('Design management index page', () => {
fakeApollo = createMockApollo(requestHandlers);
wrapper = shallowMount(Index, {
- localVue,
apolloProvider: fakeApollo,
router,
stubs: { VueDraggable },
@@ -746,9 +744,7 @@ describe('Design management index page', () => {
describe('with mocked Apollo client', () => {
it('has a design with id 1 as a first one', async () => {
createComponentWithApollo({});
-
- await jest.runOnlyPendingTimers();
- await nextTick();
+ await waitForPromises();
expect(findDesigns()).toHaveLength(3);
expect(findDesigns().at(0).props('id')).toBe('1');
@@ -761,21 +757,18 @@ describe('Design management index page', () => {
expect(moveDesignHandler).toHaveBeenCalled();
- await nextTick();
+ await waitForPromises();
expect(findDesigns().at(0).props('id')).toBe('2');
});
it('prevents reordering when reorderDesigns mutation is in progress', async () => {
createComponentWithApollo({});
-
await moveDesigns(wrapper);
expect(draggableAttributes().disabled).toBe(true);
- await jest.runOnlyPendingTimers(); // kick off the mocked GQL stuff (promises)
- await nextTick(); // kick off the DOM update
- await nextTick(); // kick off the DOM update for finally block
+ await waitForPromises();
expect(draggableAttributes().disabled).toBe(false);
});
@@ -786,8 +779,7 @@ describe('Design management index page', () => {
});
await moveDesigns(wrapper);
-
- await nextTick();
+ await waitForPromises();
expect(createFlash).toHaveBeenCalledWith({ message: 'Houston, we have a problem' });
});
@@ -798,10 +790,7 @@ describe('Design management index page', () => {
});
await moveDesigns(wrapper);
-
- await nextTick(); // kick off the DOM update
- await jest.runOnlyPendingTimers(); // kick off the mocked GQL stuff (promises)
- await nextTick(); // kick off the DOM update for flash
+ await waitForPromises();
expect(createFlash).toHaveBeenCalledWith({
message: 'Something went wrong when reordering designs. Please try again',
diff --git a/spec/frontend/design_management/router_spec.js b/spec/frontend/design_management/router_spec.js
index ac5e6895408..03ab79712a4 100644
--- a/spec/frontend/design_management/router_spec.js
+++ b/spec/frontend/design_management/router_spec.js
@@ -1,5 +1,5 @@
-import { mount, createLocalVue } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import { mount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import VueRouter from 'vue-router';
import App from '~/design_management/components/app.vue';
import DesignDetail from '~/design_management/pages/design/index.vue';
@@ -9,8 +9,7 @@ import { DESIGNS_ROUTE_NAME, DESIGN_ROUTE_NAME } from '~/design_management/route
import '~/commons/bootstrap';
function factory(routeArg) {
- const localVue = createLocalVue();
- localVue.use(VueRouter);
+ Vue.use(VueRouter);
window.gon = { sprite_icons: '' };
@@ -20,7 +19,6 @@ function factory(routeArg) {
}
return mount(App, {
- localVue,
router,
mocks: {
$apollo: {
diff --git a/spec/frontend/design_management/utils/cache_update_spec.js b/spec/frontend/design_management/utils/cache_update_spec.js
index fa6a666bb37..5e2c37e24a1 100644
--- a/spec/frontend/design_management/utils/cache_update_spec.js
+++ b/spec/frontend/design_management/utils/cache_update_spec.js
@@ -1,4 +1,4 @@
-import { InMemoryCache } from 'apollo-cache-inmemory';
+import { InMemoryCache } from '@apollo/client/core';
import {
updateStoreAfterDesignsDelete,
updateStoreAfterAddImageDiffNote,
diff --git a/spec/frontend/diffs/components/app_spec.js b/spec/frontend/diffs/components/app_spec.js
index d50ac0529d6..76e4a944d87 100644
--- a/spec/frontend/diffs/components/app_spec.js
+++ b/spec/frontend/diffs/components/app_spec.js
@@ -69,6 +69,12 @@ describe('diffs/components/app', () => {
},
provide,
store,
+ stubs: {
+ DynamicScroller: {
+ template: `<div><slot :item="$store.state.diffs.diffFiles[0]"></slot></div>`,
+ },
+ DynamicScrollerItem: true,
+ },
});
}
@@ -154,22 +160,6 @@ describe('diffs/components/app', () => {
});
});
- it.each`
- props | state | expected
- ${{ isFluidLayout: true }} | ${{ isParallelView: false }} | ${false}
- ${{}} | ${{ isParallelView: false }} | ${true}
- ${{}} | ${{ showTreeList: true, diffFiles: [{}], isParallelView: false }} | ${false}
- ${{}} | ${{ showTreeList: false, diffFiles: [{}], isParallelView: false }} | ${true}
- ${{}} | ${{ showTreeList: false, diffFiles: [], isParallelView: false }} | ${true}
- `(
- 'uses container-limiting classes ($expected) with state ($state) and props ($props)',
- ({ props, state, expected }) => {
- createComponent(props, ({ state: origState }) => Object.assign(origState.diffs, state));
-
- expect(wrapper.find('.container-limited.limit-container-width').exists()).toBe(expected);
- },
- );
-
it('displays loading icon on loading', () => {
createComponent({}, ({ state }) => {
state.diffs.isLoading = true;
@@ -498,7 +488,6 @@ describe('diffs/components/app', () => {
expect(wrapper.find(CompareVersions).exists()).toBe(true);
expect(wrapper.find(CompareVersions).props()).toEqual(
expect.objectContaining({
- isLimitedContainer: false,
diffFilesCountText: null,
}),
);
diff --git a/spec/frontend/diffs/components/collapsed_files_warning_spec.js b/spec/frontend/diffs/components/collapsed_files_warning_spec.js
index 46caeb01132..8cc342e45a7 100644
--- a/spec/frontend/diffs/components/collapsed_files_warning_spec.js
+++ b/spec/frontend/diffs/components/collapsed_files_warning_spec.js
@@ -1,8 +1,8 @@
-import { shallowMount, mount, createLocalVue } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import { shallowMount, mount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import CollapsedFilesWarning from '~/diffs/components/collapsed_files_warning.vue';
-import { CENTERED_LIMITED_CONTAINER_CLASSES, EVT_EXPAND_ALL_FILES } from '~/diffs/constants';
+import { EVT_EXPAND_ALL_FILES } from '~/diffs/constants';
import eventHub from '~/diffs/event_hub';
import createStore from '~/diffs/store/modules';
@@ -13,21 +13,19 @@ const propsData = {
mergeable: true,
resolutionPath: 'a-path',
};
-const limitedClasses = CENTERED_LIMITED_CONTAINER_CLASSES.split(' ');
async function files(store, count) {
const copies = Array(count).fill(file);
store.state.diffs.diffFiles.push(...copies);
- return nextTick();
+ await nextTick();
}
describe('CollapsedFilesWarning', () => {
- const localVue = createLocalVue();
let store;
let wrapper;
- localVue.use(Vuex);
+ Vue.use(Vuex);
const getAlertActionButton = () =>
wrapper.find(CollapsedFilesWarning).find('button.gl-alert-action:first-child');
@@ -43,7 +41,6 @@ describe('CollapsedFilesWarning', () => {
wrapper = mounter(CollapsedFilesWarning, {
propsData: { ...propsData, ...props },
- localVue,
store,
});
};
@@ -54,20 +51,6 @@ describe('CollapsedFilesWarning', () => {
describe('when there is more than one file', () => {
it.each`
- limited | containerClasses
- ${true} | ${limitedClasses}
- ${false} | ${[]}
- `(
- 'has the correct container classes when limited is $limited',
- async ({ limited, containerClasses }) => {
- createComponent({ limited });
- await files(store, 2);
-
- expect(wrapper.classes()).toEqual(['col-12'].concat(containerClasses));
- },
- );
-
- it.each`
present | dismissed
${false} | ${true}
${true} | ${false}
@@ -86,7 +69,7 @@ describe('CollapsedFilesWarning', () => {
getAlertCloseButton().element.click();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.find('[data-testid="root"]').exists()).toBe(false);
});
diff --git a/spec/frontend/diffs/components/compare_versions_spec.js b/spec/frontend/diffs/components/compare_versions_spec.js
index c48935bc4f0..21f3ee26bf8 100644
--- a/spec/frontend/diffs/components/compare_versions_spec.js
+++ b/spec/frontend/diffs/components/compare_versions_spec.js
@@ -1,4 +1,5 @@
-import { mount, createLocalVue } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import getDiffWithCommit from 'test_fixtures/merge_request_diffs/with_commit.json';
import setWindowLocation from 'helpers/set_window_location_helper';
@@ -8,8 +9,7 @@ import CompareVersionsComponent from '~/diffs/components/compare_versions.vue';
import { createStore } from '~/mr_notes/stores';
import diffsMockData from '../mock_data/merge_request_diffs';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
const NEXT_COMMIT_URL = `${TEST_HOST}/?commit_id=next`;
const PREV_COMMIT_URL = `${TEST_HOST}/?commit_id=prev`;
@@ -30,7 +30,6 @@ describe('CompareVersions', () => {
}
wrapper = mount(CompareVersionsComponent, {
- localVue,
store,
propsData: {
mergeRequestDiffs: diffsMockData,
@@ -39,7 +38,6 @@ describe('CompareVersions', () => {
},
});
};
- const findLimitedContainer = () => wrapper.find('.container-limited.limit-container-width');
const findCompareSourceDropdown = () => wrapper.find('.mr-version-dropdown');
const findCompareTargetDropdown = () => wrapper.find('.mr-version-compare-dropdown');
const getCommitNavButtonsElement = () => wrapper.find('.commit-nav-buttons');
@@ -99,18 +97,6 @@ describe('CompareVersions', () => {
expect(inlineBtn.html()).toContain('Inline');
expect(parallelBtn.html()).toContain('Side-by-side');
});
-
- it('adds container-limiting classes when showFileTree is false with inline diffs', () => {
- createWrapper({ isLimitedContainer: true });
-
- expect(findLimitedContainer().exists()).toBe(true);
- });
-
- it('does not add container-limiting classes when showFileTree is false with inline diffs', () => {
- createWrapper({ isLimitedContainer: false });
-
- expect(findLimitedContainer().exists()).toBe(false);
- });
});
describe('noChangedFiles', () => {
@@ -233,14 +219,13 @@ describe('CompareVersions', () => {
expect(link.element.getAttribute('href')).toEqual(PREV_COMMIT_URL);
});
- it('triggers the correct Vuex action on click', () => {
+ it('triggers the correct Vuex action on click', async () => {
const link = getPrevCommitNavElement();
link.trigger('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.vm.moveToNeighboringCommit).toHaveBeenCalledWith({
- direction: 'previous',
- });
+ await nextTick();
+ expect(wrapper.vm.moveToNeighboringCommit).toHaveBeenCalledWith({
+ direction: 'previous',
});
});
@@ -268,13 +253,12 @@ describe('CompareVersions', () => {
expect(link.element.getAttribute('href')).toEqual(NEXT_COMMIT_URL);
});
- it('triggers the correct Vuex action on click', () => {
+ it('triggers the correct Vuex action on click', async () => {
const link = getNextCommitNavElement();
link.trigger('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.vm.moveToNeighboringCommit).toHaveBeenCalledWith({ direction: 'next' });
- });
+ await nextTick();
+ expect(wrapper.vm.moveToNeighboringCommit).toHaveBeenCalledWith({ direction: 'next' });
});
it('renders a disabled button when there is no next commit', () => {
diff --git a/spec/frontend/diffs/components/diff_content_spec.js b/spec/frontend/diffs/components/diff_content_spec.js
index 0a7dfc02c65..7d2afe105a5 100644
--- a/spec/frontend/diffs/components/diff_content_spec.js
+++ b/spec/frontend/diffs/components/diff_content_spec.js
@@ -1,5 +1,6 @@
import { GlLoadingIcon } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import DiffContentComponent from '~/diffs/components/diff_content.vue';
import DiffDiscussions from '~/diffs/components/diff_discussions.vue';
@@ -11,8 +12,7 @@ import NoPreviewViewer from '~/vue_shared/components/diff_viewer/viewers/no_prev
import NotDiffableViewer from '~/vue_shared/components/diff_viewer/viewers/not_diffable.vue';
import diffFileMockData from '../mock_data/diff_file';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('DiffContent', () => {
let wrapper;
@@ -88,7 +88,6 @@ describe('DiffContent', () => {
...defaultProps,
...props,
},
- localVue,
store: fakeStore,
provide: { glFeatures },
});
diff --git a/spec/frontend/diffs/components/diff_discussion_reply_spec.js b/spec/frontend/diffs/components/diff_discussion_reply_spec.js
index 9443a441ec2..f03c0357a0e 100644
--- a/spec/frontend/diffs/components/diff_discussion_reply_spec.js
+++ b/spec/frontend/diffs/components/diff_discussion_reply_spec.js
@@ -1,11 +1,11 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import DiffDiscussionReply from '~/diffs/components/diff_discussion_reply.vue';
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
import NoteSignedOutWidget from '~/notes/components/note_signed_out_widget.vue';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('DiffDiscussionReply', () => {
let wrapper;
@@ -15,7 +15,6 @@ describe('DiffDiscussionReply', () => {
const createComponent = (props = {}, slots = {}) => {
wrapper = shallowMount(DiffDiscussionReply, {
store,
- localVue,
propsData: {
...props,
},
diff --git a/spec/frontend/diffs/components/diff_discussions_spec.js b/spec/frontend/diffs/components/diff_discussions_spec.js
index bd6f4cd2545..2da68adddf6 100644
--- a/spec/frontend/diffs/components/diff_discussions_spec.js
+++ b/spec/frontend/diffs/components/diff_discussions_spec.js
@@ -1,5 +1,5 @@
import { GlIcon } from '@gitlab/ui';
-import { mount, createLocalVue } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
import DiffDiscussions from '~/diffs/components/diff_discussions.vue';
import { createStore } from '~/mr_notes/stores';
import DiscussionNotes from '~/notes/components/discussion_notes.vue';
@@ -8,8 +8,6 @@ import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item
import '~/behaviors/markdown/render_gfm';
import discussionsMockData from '../mock_data/diff_discussions';
-const localVue = createLocalVue();
-
describe('DiffDiscussions', () => {
let store;
let wrapper;
@@ -17,13 +15,12 @@ describe('DiffDiscussions', () => {
const createComponent = (props) => {
store = createStore();
- wrapper = mount(localVue.extend(DiffDiscussions), {
+ wrapper = mount(DiffDiscussions, {
store,
propsData: {
discussions: getDiscussionsMockData(),
...props,
},
- localVue,
});
};
@@ -74,7 +71,7 @@ describe('DiffDiscussions', () => {
expect(diffNotesToggle.text().trim()).toBe('1');
expect(diffNotesToggle.classes()).toEqual(
- expect.arrayContaining(['btn-transparent', 'badge', 'badge-pill']),
+ expect.arrayContaining(['js-diff-notes-toggle', 'gl-translate-x-n50', 'design-note-pin']),
);
});
@@ -90,8 +87,8 @@ describe('DiffDiscussions', () => {
createComponent({ renderAvatarBadge: true });
const noteableDiscussion = wrapper.find(NoteableDiscussion);
- expect(noteableDiscussion.find('.badge-pill').exists()).toBe(true);
- expect(noteableDiscussion.find('.badge-pill').text().trim()).toBe('1');
+ expect(noteableDiscussion.find('.design-note-pin').exists()).toBe(true);
+ expect(noteableDiscussion.find('.design-note-pin').text().trim()).toBe('1');
});
});
});
diff --git a/spec/frontend/diffs/components/diff_expansion_cell_spec.js b/spec/frontend/diffs/components/diff_expansion_cell_spec.js
index f53f10d955d..cd472920bb9 100644
--- a/spec/frontend/diffs/components/diff_expansion_cell_spec.js
+++ b/spec/frontend/diffs/components/diff_expansion_cell_spec.js
@@ -91,7 +91,9 @@ describe('DiffExpansionCell', () => {
});
expect(findExpandUp(wrapper).exists()).toBe(true);
- expect(findExpandDown(wrapper).exists()).toBe(false);
+ expect(findExpandDown(wrapper).exists()).toBe(true);
+ expect(findExpandUp(wrapper).attributes('disabled')).not.toBeDefined();
+ expect(findExpandDown(wrapper).attributes('disabled')).toBeDefined();
expect(findExpandAll(wrapper)).not.toBe(null);
});
});
@@ -112,8 +114,10 @@ describe('DiffExpansionCell', () => {
isBottom: true,
});
- expect(findExpandUp(wrapper).exists()).toBe(false);
expect(findExpandDown(wrapper).exists()).toBe(true);
+ expect(findExpandUp(wrapper).exists()).toBe(true);
+ expect(findExpandDown(wrapper).attributes('disabled')).not.toBeDefined();
+ expect(findExpandUp(wrapper).attributes('disabled')).toBeDefined();
expect(findExpandAll(wrapper)).not.toBe(null);
});
});
diff --git a/spec/frontend/diffs/components/diff_file_header_spec.js b/spec/frontend/diffs/components/diff_file_header_spec.js
index 342b4bfcc50..f22bd312a6d 100644
--- a/spec/frontend/diffs/components/diff_file_header_spec.js
+++ b/spec/frontend/diffs/components/diff_file_header_spec.js
@@ -1,4 +1,5 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import { cloneDeep } from 'lodash';
import Vuex from 'vuex';
@@ -37,8 +38,7 @@ const diffFile = Object.freeze(
}),
);
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('DiffFileHeader component', () => {
let wrapper;
@@ -82,7 +82,7 @@ describe('DiffFileHeader component', () => {
const findExpandButton = () => wrapper.find({ ref: 'expandDiffToFullFileButton' });
const findFileActions = () => wrapper.find('.file-actions');
const findModeChangedLine = () => wrapper.find({ ref: 'fileMode' });
- const findLfsLabel = () => wrapper.find('.label-lfs');
+ const findLfsLabel = () => wrapper.find('[data-testid="label-lfs"]');
const findToggleDiscussionsButton = () => wrapper.find({ ref: 'toggleDiscussionsButton' });
const findExternalLink = () => wrapper.find({ ref: 'externalLink' });
const findReplacedFileButton = () => wrapper.find({ ref: 'replacedFileButton' });
@@ -103,7 +103,6 @@ describe('DiffFileHeader component', () => {
...props,
},
...options,
- localVue,
store,
});
};
@@ -126,30 +125,27 @@ describe('DiffFileHeader component', () => {
expect(findCollapseIcon().props('name')).toBe(icon);
});
- it('when header is clicked emits toggleFile', () => {
+ it('when header is clicked emits toggleFile', async () => {
createComponent();
findHeader().trigger('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted().toggleFile).toBeDefined();
- });
+ await nextTick();
+ expect(wrapper.emitted().toggleFile).toBeDefined();
});
- it('when collapseIcon is clicked emits toggleFile', () => {
+ it('when collapseIcon is clicked emits toggleFile', async () => {
createComponent({ props: { collapsible: true } });
findCollapseIcon().vm.$emit('click', new Event('click'));
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted().toggleFile).toBeDefined();
- });
+ await nextTick();
+ expect(wrapper.emitted().toggleFile).toBeDefined();
});
- it('when other element in header is clicked does not emits toggleFile', () => {
+ it('when other element in header is clicked does not emits toggleFile', async () => {
createComponent({ props: { collapsible: true } });
findTitleLink().trigger('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted().toggleFile).not.toBeDefined();
- });
+ await nextTick();
+ expect(wrapper.emitted().toggleFile).not.toBeDefined();
});
describe('copy to clipboard', () => {
diff --git a/spec/frontend/diffs/components/diff_file_spec.js b/spec/frontend/diffs/components/diff_file_spec.js
index dc0ed621a64..a0aa4c784bf 100644
--- a/spec/frontend/diffs/components/diff_file_spec.js
+++ b/spec/frontend/diffs/components/diff_file_spec.js
@@ -1,6 +1,6 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
-import { nextTick } from 'vue';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import DiffContentComponent from 'jh_else_ce/diffs/components/diff_content.vue';
@@ -70,9 +70,7 @@ function markFileToBeRendered(store, index = 0) {
}
function createComponent({ file, first = false, last = false, options = {}, props = {} }) {
- const localVue = createLocalVue();
-
- localVue.use(Vuex);
+ Vue.use(Vuex);
const store = new Vuex.Store({
...createNotesStore(),
@@ -85,7 +83,6 @@ function createComponent({ file, first = false, last = false, options = {}, prop
const wrapper = shallowMount(DiffFileComponent, {
store,
- localVue,
propsData: {
file,
canCurrentUserFork: false,
@@ -98,7 +95,6 @@ function createComponent({ file, first = false, last = false, options = {}, prop
});
return {
- localVue,
wrapper,
store,
};
@@ -164,7 +160,7 @@ describe('DiffFile', () => {
last,
}));
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(eventHub.$emit).toHaveBeenCalledTimes(events.length);
events.forEach((event) => {
@@ -188,13 +184,13 @@ describe('DiffFile', () => {
makeFileAutomaticallyCollapsed(store);
- await wrapper.vm.$nextTick(); // Wait for store updates to flow into the component
+ await nextTick(); // Wait for store updates to flow into the component
toggleFile(wrapper);
- await wrapper.vm.$nextTick(); // Wait for the load to resolve
- await wrapper.vm.$nextTick(); // Wait for the idleCallback
- await wrapper.vm.$nextTick(); // Wait for nextTick inside postRender
+ await nextTick(); // Wait for the load to resolve
+ await nextTick(); // Wait for the idleCallback
+ await nextTick(); // Wait for nextTick inside postRender
expect(eventHub.$emit).toHaveBeenCalledTimes(2);
expect(eventHub.$emit).toHaveBeenCalledWith(EVT_PERF_MARK_FIRST_DIFF_FILE_SHOWN);
@@ -218,7 +214,7 @@ describe('DiffFile', () => {
markFileToBeRendered(store);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.find(DiffContentComponent).exists()).toBe(true);
});
@@ -268,7 +264,7 @@ describe('DiffFile', () => {
it('performs the normal file toggle when the file is collapsed', async () => {
makeFileAutomaticallyCollapsed(store);
- await wrapper.vm.$nextTick();
+ await nextTick();
eventHub.$emit(EVT_EXPAND_ALL_FILES);
@@ -278,7 +274,7 @@ describe('DiffFile', () => {
it('does nothing when the file is not collapsed', async () => {
eventHub.$emit(EVT_EXPAND_ALL_FILES);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.vm.handleToggle).not.toHaveBeenCalled();
});
@@ -290,7 +286,7 @@ describe('DiffFile', () => {
});
it('should not have any content at all', async () => {
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findDiffContentArea(wrapper).element.children.length).toBe(0);
});
@@ -396,7 +392,7 @@ describe('DiffFile', () => {
readableText,
});
- await wrapper.vm.$nextTick();
+ await nextTick();
toggleFile(wrapper);
};
@@ -444,7 +440,7 @@ describe('DiffFile', () => {
makeFileAutomaticallyCollapsed(store);
wrapper.vm.requestDiff();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findLoader(wrapper).exists()).toBe(true);
});
@@ -455,7 +451,7 @@ describe('DiffFile', () => {
({ wrapper, store } = createComponent({ file: getUnreadableFile() }));
makeFileAutomaticallyCollapsed(store);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findDiffContentArea(wrapper).html()).toContain(
'Files with large changes are collapsed by default.',
@@ -474,7 +470,7 @@ describe('DiffFile', () => {
markFileToBeRendered(store);
changeViewerType(store, mode);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.classes('has-body')).toBe(true);
expect(wrapper.find(DiffContentComponent).exists()).toBe(true);
@@ -500,7 +496,7 @@ describe('DiffFile', () => {
},
});
- await wrapper.vm.$nextTick();
+ await nextTick();
const button = wrapper.find('[data-testid="blob-button"]');
@@ -525,7 +521,7 @@ describe('DiffFile', () => {
({ wrapper, store } = createComponent({ file, props: { viewDiffsFileByFile: true } }));
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findLoader(wrapper).exists()).toBe(true);
});
diff --git a/spec/frontend/diffs/components/diff_gutter_avatars_spec.js b/spec/frontend/diffs/components/diff_gutter_avatars_spec.js
index 5884a9ebd3a..c18f0b721da 100644
--- a/spec/frontend/diffs/components/diff_gutter_avatars_spec.js
+++ b/spec/frontend/diffs/components/diff_gutter_avatars_spec.js
@@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import DiffGutterAvatars from '~/diffs/components/diff_gutter_avatars.vue';
import discussionsMockData from '../mock_data/diff_discussions';
@@ -35,12 +36,11 @@ describe('DiffGutterAvatars', () => {
expect(findCollapseButton().exists()).toBe(true);
});
- it('should emit toggleDiscussions event on button click', () => {
+ it('should emit toggleDiscussions event on button click', async () => {
findCollapseButton().trigger('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted().toggleLineDiscussions).toBeTruthy();
- });
+ await nextTick();
+ expect(wrapper.emitted().toggleLineDiscussions).toBeTruthy();
});
});
@@ -65,20 +65,18 @@ describe('DiffGutterAvatars', () => {
expect(findMoreCount().text()).toBe('+2');
});
- it('should emit toggleDiscussions event on avatars click', () => {
+ it('should emit toggleDiscussions event on avatars click', async () => {
findUserAvatars().at(0).trigger('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted().toggleLineDiscussions).toBeTruthy();
- });
+ await nextTick();
+ expect(wrapper.emitted().toggleLineDiscussions).toBeTruthy();
});
- it('should emit toggleDiscussions event on more count text click', () => {
+ it('should emit toggleDiscussions event on more count text click', async () => {
findMoreCount().trigger('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted().toggleLineDiscussions).toBeTruthy();
- });
+ await nextTick();
+ expect(wrapper.emitted().toggleLineDiscussions).toBeTruthy();
});
});
diff --git a/spec/frontend/diffs/components/diff_view_spec.js b/spec/frontend/diffs/components/diff_view_spec.js
index 3af66526050..9b8f0421b7c 100644
--- a/spec/frontend/diffs/components/diff_view_spec.js
+++ b/spec/frontend/diffs/components/diff_view_spec.js
@@ -50,8 +50,11 @@ describe('DiffView', () => {
};
it('renders a match line', () => {
- const wrapper = createWrapper({ diffLines: [{ isMatchLineLeft: true }] });
+ const wrapper = createWrapper({
+ diffLines: [{ isMatchLineLeft: true, left: { rich_text: 'matched text', lineDraft: {} } }],
+ });
expect(wrapper.find(DiffExpansionCell).exists()).toBe(true);
+ expect(wrapper.text()).toContain('matched text');
});
it.each`
diff --git a/spec/frontend/diffs/components/image_diff_overlay_spec.js b/spec/frontend/diffs/components/image_diff_overlay_spec.js
index 8c1a8041f6c..70191620eb6 100644
--- a/spec/frontend/diffs/components/image_diff_overlay_spec.js
+++ b/spec/frontend/diffs/components/image_diff_overlay_spec.js
@@ -1,5 +1,5 @@
import { GlIcon } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
import ImageDiffOverlay from '~/diffs/components/image_diff_overlay.vue';
import { createStore } from '~/mr_notes/stores';
import { imageDiffDiscussions } from '../mock_data/diff_discussions';
@@ -19,7 +19,7 @@ describe('Diffs image diff overlay component', () => {
extendStore(store);
dispatch = jest.spyOn(store, 'dispatch').mockImplementation();
- wrapper = shallowMount(ImageDiffOverlay, {
+ wrapper = mount(ImageDiffOverlay, {
store,
parentComponent: {
data() {
diff --git a/spec/frontend/diffs/components/merge_conflict_warning_spec.js b/spec/frontend/diffs/components/merge_conflict_warning_spec.js
index 2f303f25f66..4e47249f5b4 100644
--- a/spec/frontend/diffs/components/merge_conflict_warning_spec.js
+++ b/spec/frontend/diffs/components/merge_conflict_warning_spec.js
@@ -1,13 +1,11 @@
import { shallowMount, mount } from '@vue/test-utils';
import MergeConflictWarning from '~/diffs/components/merge_conflict_warning.vue';
-import { CENTERED_LIMITED_CONTAINER_CLASSES } from '~/diffs/constants';
const propsData = {
limited: true,
mergeable: true,
resolutionPath: 'a-path',
};
-const limitedClasses = CENTERED_LIMITED_CONTAINER_CLASSES.split(' ');
function findResolveButton(wrapper) {
return wrapper.find('.gl-alert-actions a.gl-button:first-child');
@@ -32,19 +30,6 @@ describe('MergeConflictWarning', () => {
});
it.each`
- limited | containerClasses
- ${true} | ${limitedClasses}
- ${false} | ${[]}
- `(
- 'has the correct container classes when limited is $limited',
- ({ limited, containerClasses }) => {
- createComponent({ limited });
-
- expect(wrapper.classes()).toEqual(containerClasses);
- },
- );
-
- it.each`
present | resolutionPath
${false} | ${''}
${true} | ${'some-path'}
diff --git a/spec/frontend/diffs/components/no_changes_spec.js b/spec/frontend/diffs/components/no_changes_spec.js
index 164c58dc8e4..6903b844e5e 100644
--- a/spec/frontend/diffs/components/no_changes_spec.js
+++ b/spec/frontend/diffs/components/no_changes_spec.js
@@ -1,12 +1,12 @@
import { GlButton } from '@gitlab/ui';
-import { createLocalVue, shallowMount, mount } from '@vue/test-utils';
+import { shallowMount, mount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import NoChanges from '~/diffs/components/no_changes.vue';
import { createStore } from '~/mr_notes/stores';
import diffsMockData from '../mock_data/merge_request_diffs';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
const TEST_TARGET_BRANCH = 'foo';
const TEST_SOURCE_BRANCH = 'dev/update';
@@ -17,7 +17,6 @@ describe('Diff no changes empty state', () => {
function createComponent(mountFn = shallowMount) {
wrapper = mountFn(NoChanges, {
- localVue,
store,
propsData: {
changesEmptyStateIllustration: '',
diff --git a/spec/frontend/diffs/components/settings_dropdown_spec.js b/spec/frontend/diffs/components/settings_dropdown_spec.js
index 2dd35519464..693fc5bfd8f 100644
--- a/spec/frontend/diffs/components/settings_dropdown_spec.js
+++ b/spec/frontend/diffs/components/settings_dropdown_spec.js
@@ -1,5 +1,6 @@
import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import SettingsDropdown from '~/diffs/components/settings_dropdown.vue';
@@ -10,7 +11,6 @@ import createDiffsStore from '../create_diffs_store';
describe('Diff settings dropdown component', () => {
let wrapper;
- let vm;
let store;
function createComponent(extendStore = () => {}) {
@@ -23,7 +23,6 @@ describe('Diff settings dropdown component', () => {
store,
}),
);
- vm = wrapper.vm;
}
function getFileByFileCheckbox(vueWrapper) {
@@ -142,7 +141,7 @@ describe('Diff settings dropdown component', () => {
checkbox.trigger('click');
- await vm.$nextTick();
+ await nextTick();
expect(store.dispatch).toHaveBeenCalledWith('diffs/setShowWhitespace', {
showWhitespace: !checked,
@@ -185,7 +184,7 @@ describe('Diff settings dropdown component', () => {
getFileByFileCheckbox(wrapper).trigger('click');
- await vm.$nextTick();
+ await nextTick();
expect(store.dispatch).toHaveBeenCalledWith('diffs/setFileByFile', {
fileByFile: setting,
diff --git a/spec/frontend/diffs/components/tree_list_spec.js b/spec/frontend/diffs/components/tree_list_spec.js
index 31044b0818c..963805f4792 100644
--- a/spec/frontend/diffs/components/tree_list_spec.js
+++ b/spec/frontend/diffs/components/tree_list_spec.js
@@ -1,4 +1,5 @@
-import { shallowMount, mount, createLocalVue } from '@vue/test-utils';
+import { shallowMount, mount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import TreeList from '~/diffs/components/tree_list.vue';
import createStore from '~/diffs/store/modules';
@@ -8,13 +9,11 @@ describe('Diffs tree list component', () => {
let wrapper;
let store;
const getFileRows = () => wrapper.findAll('.file-row');
- const localVue = createLocalVue();
- localVue.use(Vuex);
+ Vue.use(Vuex);
const createComponent = (mountFn = mount) => {
wrapper = mountFn(TreeList, {
store,
- localVue,
propsData: { hideFileStats: false },
});
};
@@ -92,12 +91,11 @@ describe('Diffs tree list component', () => {
expect(getFileRows().at(1).html()).toContain('app');
});
- it('hides file stats', () => {
+ it('hides file stats', async () => {
wrapper.setProps({ hideFileStats: true });
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find('.file-row-stats').exists()).toBe(false);
- });
+ await nextTick();
+ expect(wrapper.find('.file-row-stats').exists()).toBe(false);
});
it('calls toggleTreeOpen when clicking folder', () => {
@@ -118,20 +116,18 @@ describe('Diffs tree list component', () => {
});
});
- it('renders as file list when renderTreeList is false', () => {
+ it('renders as file list when renderTreeList is false', async () => {
wrapper.vm.$store.state.diffs.renderTreeList = false;
- return wrapper.vm.$nextTick().then(() => {
- expect(getFileRows()).toHaveLength(1);
- });
+ await nextTick();
+ expect(getFileRows()).toHaveLength(1);
});
- it('renders file paths when renderTreeList is false', () => {
+ it('renders file paths when renderTreeList is false', async () => {
wrapper.vm.$store.state.diffs.renderTreeList = false;
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find('.file-row').html()).toContain('index.js');
- });
+ await nextTick();
+ expect(wrapper.find('.file-row').html()).toContain('index.js');
});
});
@@ -143,14 +139,13 @@ describe('Diffs tree list component', () => {
store.state.diffs.viewedDiffFileIds = viewedDiffFileIds;
});
- it('passes the viewedDiffFileIds to the FileTree', () => {
+ it('passes the viewedDiffFileIds to the FileTree', async () => {
createComponent(shallowMount);
- return wrapper.vm.$nextTick().then(() => {
- // Have to use $attrs['viewed-files'] because we are passing down an object
- // and attributes('') stringifies values (e.g. [object])...
- expect(wrapper.find(FileTree).vm.$attrs['viewed-files']).toBe(viewedDiffFileIds);
- });
+ await nextTick();
+ // Have to use $attrs['viewed-files'] because we are passing down an object
+ // and attributes('') stringifies values (e.g. [object])...
+ expect(wrapper.find(FileTree).vm.$attrs['viewed-files']).toBe(viewedDiffFileIds);
});
});
});
diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js
index b5003a54917..d6a2aa104cd 100644
--- a/spec/frontend/diffs/store/actions_spec.js
+++ b/spec/frontend/diffs/store/actions_spec.js
@@ -202,7 +202,7 @@ describe('DiffsStoreActions', () => {
testAction(
fetchDiffFilesBatch,
{},
- { endpointBatch, diffViewType: 'inline' },
+ { endpointBatch, diffViewType: 'inline', diffFiles: [] },
[
{ type: types.SET_BATCH_LOADING_STATE, payload: 'loading' },
{ type: types.SET_RETRIEVING_BATCHES, payload: true },
@@ -544,10 +544,8 @@ describe('DiffsStoreActions', () => {
[{ type: types.SET_DIFF_VIEW_TYPE, payload: INLINE_DIFF_VIEW_TYPE }],
[],
() => {
- setImmediate(() => {
- expect(Cookies.get('diff_view')).toEqual(INLINE_DIFF_VIEW_TYPE);
- done();
- });
+ expect(Cookies.get('diff_view')).toEqual(INLINE_DIFF_VIEW_TYPE);
+ done();
},
);
});
@@ -562,10 +560,8 @@ describe('DiffsStoreActions', () => {
[{ type: types.SET_DIFF_VIEW_TYPE, payload: PARALLEL_DIFF_VIEW_TYPE }],
[],
() => {
- setImmediate(() => {
- expect(Cookies.get(DIFF_VIEW_COOKIE_NAME)).toEqual(PARALLEL_DIFF_VIEW_TYPE);
- done();
- });
+ expect(Cookies.get(DIFF_VIEW_COOKIE_NAME)).toEqual(PARALLEL_DIFF_VIEW_TYPE);
+ done();
},
);
});
diff --git a/spec/frontend/editor/source_editor_yaml_ext_spec.js b/spec/frontend/editor/source_editor_yaml_ext_spec.js
index a861d9c7a45..b603b0e3a98 100644
--- a/spec/frontend/editor/source_editor_yaml_ext_spec.js
+++ b/spec/frontend/editor/source_editor_yaml_ext_spec.js
@@ -353,58 +353,78 @@ foo:
});
describe('highlight', () => {
- const highlightPathOnSetup = 'abc';
const value = `foo:
bar:
- baz
- boo
- abc: def
+abc: def
`;
let instance;
let highlightLinesSpy;
let removeHighlightsSpy;
- beforeEach(() => {
- instance = getEditorInstanceWithExtension({ highlightPath: highlightPathOnSetup }, { value });
- highlightLinesSpy = jest.fn();
- removeHighlightsSpy = jest.fn();
- spyOnApi(baseExtension, {
- highlightLines: highlightLinesSpy,
- removeHighlights: removeHighlightsSpy,
- });
- });
-
afterEach(() => {
jest.clearAllMocks();
});
- it('saves the highlighted path in highlightPath', () => {
- const path = 'foo.bar';
- instance.highlight(path);
- expect(yamlExtension.obj.highlightPath).toEqual(path);
- });
-
- it('calls highlightLines with a number of lines', () => {
- const path = 'foo.bar';
- instance.highlight(path);
- expect(highlightLinesSpy).toHaveBeenCalledWith(instance, [2, 4]);
- });
-
- it('calls removeHighlights if path is null', () => {
- instance.highlight(null);
- expect(removeHighlightsSpy).toHaveBeenCalledWith(instance);
- expect(highlightLinesSpy).not.toHaveBeenCalled();
- expect(yamlExtension.obj.highlightPath).toBeNull();
- });
-
- it('throws an error if path is invalid and does not change the highlighted path', () => {
- expect(() => instance.highlight('invalidPath[0]')).toThrow(
- 'The node invalidPath[0] could not be found inside the document.',
- );
- expect(yamlExtension.obj.highlightPath).toEqual(highlightPathOnSetup);
- expect(highlightLinesSpy).not.toHaveBeenCalled();
- expect(removeHighlightsSpy).not.toHaveBeenCalled();
- });
+ it.each`
+ highlightPathOnSetup | path | keepOnNotFound | expectHighlightLinesToBeCalled | withLines | expectRemoveHighlightsToBeCalled | storedHighlightPath
+ ${null} | ${undefined} | ${false} | ${false} | ${undefined} | ${true} | ${null}
+ ${'abc'} | ${'abc'} | ${undefined} | ${false} | ${undefined} | ${false} | ${'abc'}
+ ${null} | ${null} | ${false} | ${false} | ${undefined} | ${false} | ${null}
+ ${null} | ${''} | ${false} | ${false} | ${undefined} | ${true} | ${null}
+ ${null} | ${''} | ${true} | ${false} | ${undefined} | ${true} | ${null}
+ ${'abc'} | ${''} | ${false} | ${false} | ${undefined} | ${true} | ${null}
+ ${'abc'} | ${'foo.bar'} | ${false} | ${true} | ${[2, 4]} | ${false} | ${'foo.bar'}
+ ${'abc'} | ${['foo', 'bar']} | ${false} | ${true} | ${[2, 4]} | ${false} | ${['foo', 'bar']}
+ ${'abc'} | ${'invalid'} | ${true} | ${false} | ${undefined} | ${false} | ${'abc'}
+ ${'abc'} | ${'invalid'} | ${false} | ${false} | ${undefined} | ${true} | ${null}
+ ${'abc'} | ${'invalid'} | ${undefined} | ${false} | ${undefined} | ${true} | ${null}
+ ${'abc'} | ${['invalid']} | ${undefined} | ${false} | ${undefined} | ${true} | ${null}
+ ${'abc'} | ${['invalid']} | ${true} | ${false} | ${undefined} | ${false} | ${'abc'}
+ ${'abc'} | ${[]} | ${true} | ${false} | ${undefined} | ${true} | ${null}
+ ${'abc'} | ${[]} | ${false} | ${false} | ${undefined} | ${true} | ${null}
+ `(
+ 'returns correct result for highlightPathOnSetup=$highlightPathOnSetup, path=$path' +
+ ' and keepOnNotFound=$keepOnNotFound',
+ ({
+ highlightPathOnSetup,
+ path,
+ keepOnNotFound,
+ expectHighlightLinesToBeCalled,
+ withLines,
+ expectRemoveHighlightsToBeCalled,
+ storedHighlightPath,
+ }) => {
+ instance = getEditorInstanceWithExtension(
+ { highlightPath: highlightPathOnSetup },
+ { value },
+ );
+
+ highlightLinesSpy = jest.fn();
+ removeHighlightsSpy = jest.fn();
+ spyOnApi(baseExtension, {
+ highlightLines: highlightLinesSpy,
+ removeHighlights: removeHighlightsSpy,
+ });
+
+ instance.highlight(path, keepOnNotFound);
+
+ if (expectHighlightLinesToBeCalled) {
+ expect(highlightLinesSpy).toHaveBeenCalledWith(instance, withLines);
+ } else {
+ expect(highlightLinesSpy).not.toHaveBeenCalled();
+ }
+
+ if (expectRemoveHighlightsToBeCalled) {
+ expect(removeHighlightsSpy).toHaveBeenCalled();
+ } else {
+ expect(removeHighlightsSpy).not.toHaveBeenCalled();
+ }
+
+ expect(yamlExtension.obj.highlightPath).toEqual(storedHighlightPath);
+ },
+ );
});
describe('locate', () => {
@@ -446,10 +466,10 @@ foo:
expect(instance.locate(path)).toEqual(expected);
});
- it('throws an error if a path cannot be found inside the yaml', () => {
+ it('returns [null, null] if a path cannot be found inside the yaml', () => {
const path = 'baz[8]';
const instance = getEditorInstanceWithExtension(options);
- expect(() => instance.locate(path)).toThrow();
+ expect(instance.locate(path)).toEqual([null, null]);
});
it('returns the expected line numbers for a path to an array entry inside the yaml', () => {
diff --git a/spec/frontend/emoji/awards_app/store/actions_spec.js b/spec/frontend/emoji/awards_app/store/actions_spec.js
index 02b643244d2..0761256ed23 100644
--- a/spec/frontend/emoji/awards_app/store/actions_spec.js
+++ b/spec/frontend/emoji/awards_app/store/actions_spec.js
@@ -87,6 +87,26 @@ describe('Awards app actions', () => {
describe('toggleAward', () => {
let mock;
+ const optimisticAwardId = Number.MAX_SAFE_INTEGER - 1;
+ const makeOptimisticAddMutation = (
+ id = optimisticAwardId,
+ name = null,
+ userId = window.gon.current_user_id,
+ ) => ({
+ type: 'ADD_NEW_AWARD',
+ payload: {
+ id,
+ name,
+ user: {
+ id: userId,
+ },
+ },
+ });
+ const makeOptimisticRemoveMutation = (id = optimisticAwardId) => ({
+ type: 'REMOVE_AWARD',
+ payload: id,
+ });
+
beforeEach(() => {
mock = new MockAdapter(axios);
});
@@ -110,8 +130,10 @@ describe('Awards app actions', () => {
mock.onPost(`${relativeRootUrl || ''}/awards`).reply(200, { id: 1 });
});
- it('commits ADD_NEW_AWARD', async () => {
+ it('adds an optimistic award, removes it, and then commits ADD_NEW_AWARD', async () => {
testAction(actions.toggleAward, null, { path: '/awards', awards: [] }, [
+ makeOptimisticAddMutation(),
+ makeOptimisticRemoveMutation(),
{ type: 'ADD_NEW_AWARD', payload: { id: 1 } },
]);
});
@@ -127,7 +149,7 @@ describe('Awards app actions', () => {
actions.toggleAward,
null,
{ path: '/awards', awards: [] },
- [],
+ [makeOptimisticAddMutation(), makeOptimisticRemoveMutation()],
[],
() => {
expect(Sentry.captureException).toHaveBeenCalled();
@@ -137,7 +159,7 @@ describe('Awards app actions', () => {
});
});
- describe('removing a award', () => {
+ describe('removing an award', () => {
const mockData = { id: 1, name: 'thumbsup', user: { id: 1 } };
describe('success', () => {
@@ -160,6 +182,9 @@ describe('Awards app actions', () => {
});
describe('error', () => {
+ const currentUserId = 1;
+ const name = 'thumbsup';
+
beforeEach(() => {
mock.onDelete(`${relativeRootUrl || ''}/awards/1`).reply(500);
});
@@ -167,13 +192,13 @@ describe('Awards app actions', () => {
it('calls Sentry.captureException', async () => {
await testAction(
actions.toggleAward,
- 'thumbsup',
+ name,
{
path: '/awards',
- currentUserId: 1,
+ currentUserId,
awards: [mockData],
},
- [],
+ [makeOptimisticRemoveMutation(1), makeOptimisticAddMutation(1, name, currentUserId)],
[],
() => {
expect(Sentry.captureException).toHaveBeenCalled();
diff --git a/spec/frontend/emoji/components/utils_spec.js b/spec/frontend/emoji/components/utils_spec.js
index 36521eb1051..03eeb6b6bf7 100644
--- a/spec/frontend/emoji/components/utils_spec.js
+++ b/spec/frontend/emoji/components/utils_spec.js
@@ -31,6 +31,7 @@ describe('addToFrequentlyUsed', () => {
expect(Cookies.set).toHaveBeenCalledWith('frequently_used_emojis', 'thumbsup', {
expires: 365,
+ secure: false,
});
});
@@ -41,6 +42,7 @@ describe('addToFrequentlyUsed', () => {
expect(Cookies.set).toHaveBeenCalledWith('frequently_used_emojis', 'thumbsdown,thumbsup', {
expires: 365,
+ secure: false,
});
});
@@ -51,6 +53,7 @@ describe('addToFrequentlyUsed', () => {
expect(Cookies.set).toHaveBeenCalledWith('frequently_used_emojis', 'thumbsup', {
expires: 365,
+ secure: false,
});
});
});
diff --git a/spec/frontend/environment.js b/spec/frontend/environment.js
index cf47a1cd7bb..d7acf75fc95 100644
--- a/spec/frontend/environment.js
+++ b/spec/frontend/environment.js
@@ -29,6 +29,9 @@ class CustomEnvironment extends JSDOMEnvironment {
},
warn(...args) {
+ if (args[0].includes('The updateQuery callback for fetchMore is deprecated')) {
+ return;
+ }
throw new ErrorWithStack(
`Unexpected call of console.warn() with:\n\n${args.join(', ')}`,
this.warn,
diff --git a/spec/frontend/environments/canary_ingress_spec.js b/spec/frontend/environments/canary_ingress_spec.js
index 6c7a786e652..d58f9f9b8a2 100644
--- a/spec/frontend/environments/canary_ingress_spec.js
+++ b/spec/frontend/environments/canary_ingress_spec.js
@@ -3,6 +3,7 @@ import { mount } from '@vue/test-utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import CanaryIngress from '~/environments/components/canary_ingress.vue';
import { CANARY_UPDATE_MODAL } from '~/environments/constants';
+import { rolloutStatus } from './graphql/mock_data';
describe('/environments/components/canary_ingress.vue', () => {
let wrapper;
@@ -13,16 +14,18 @@ describe('/environments/components/canary_ingress.vue', () => {
.at(x / 5)
.vm.$emit('click');
- const createComponent = () => {
+ const createComponent = (props = {}, options = {}) => {
wrapper = mount(CanaryIngress, {
propsData: {
canaryIngress: {
canary_weight: 60,
},
+ ...props,
},
directives: {
GlModal: createMockDirective(),
},
+ ...options,
});
};
@@ -94,9 +97,25 @@ describe('/environments/components/canary_ingress.vue', () => {
});
it('is set to open the change modal', () => {
- const options = canaryWeightDropdown.findAll(GlDropdownItem);
- expect(options).toHaveLength(21);
- options.wrappers.forEach((w, i) => expect(w.text()).toBe((i * 5).toString()));
+ canaryWeightDropdown
+ .findAll(GlDropdownItem)
+ .wrappers.forEach((w) =>
+ expect(getBinding(w.element, 'gl-modal')).toMatchObject({ value: CANARY_UPDATE_MODAL }),
+ );
+ });
+ });
+
+ describe('graphql', () => {
+ beforeEach(() => {
+ createComponent({
+ graphql: true,
+ canaryIngress: rolloutStatus.canaryIngress,
+ });
+ });
+
+ it('shows the correct weight', () => {
+ const canaryWeightDropdown = wrapper.find('[data-testid="canary-weight"]');
+ expect(canaryWeightDropdown.props('text')).toBe('50');
});
});
});
diff --git a/spec/frontend/environments/canary_update_modal_spec.js b/spec/frontend/environments/canary_update_modal_spec.js
index c7129ee1320..22d13558a84 100644
--- a/spec/frontend/environments/canary_update_modal_spec.js
+++ b/spec/frontend/environments/canary_update_modal_spec.js
@@ -1,5 +1,6 @@
import { GlAlert, GlModal } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
import CanaryUpdateModal from '~/environments/components/canary_update_modal.vue';
import updateCanaryIngress from '~/environments/graphql/mutations/update_canary_ingress.mutation.graphql';
@@ -86,7 +87,7 @@ describe('/environments/components/canary_update_modal.vue', () => {
mutate.mockResolvedValue({ data: { environmentsCanaryIngressUpdate: { errors: [] } } });
modal.vm.$emit('primary');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findAlert().exists()).toBe(false);
});
@@ -95,7 +96,7 @@ describe('/environments/components/canary_update_modal.vue', () => {
mutate.mockResolvedValue({ data: { environmentsCanaryIngressUpdate: { errors: ['error'] } } });
modal.vm.$emit('primary');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findAlert().text()).toBe('error');
});
@@ -105,7 +106,7 @@ describe('/environments/components/canary_update_modal.vue', () => {
modal.vm.$emit('primary');
await waitForPromises();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findAlert().text()).toBe('Something went wrong. Please try again later');
});
@@ -114,12 +115,12 @@ describe('/environments/components/canary_update_modal.vue', () => {
mutate.mockResolvedValue({ data: { environmentsCanaryIngressUpdate: { errors: ['error'] } } });
modal.vm.$emit('primary');
- await wrapper.vm.$nextTick();
+ await nextTick();
const alert = findAlert();
alert.vm.$emit('dismiss');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(alert.exists()).toBe(false);
});
diff --git a/spec/frontend/environments/commit_spec.js b/spec/frontend/environments/commit_spec.js
new file mode 100644
index 00000000000..32eb4b77528
--- /dev/null
+++ b/spec/frontend/environments/commit_spec.js
@@ -0,0 +1,71 @@
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import Commit from '~/environments/components/commit.vue';
+import { resolvedEnvironment } from './graphql/mock_data';
+
+describe('~/environments/components/commit.vue', () => {
+ let commit;
+ let wrapper;
+
+ beforeEach(() => {
+ commit = resolvedEnvironment.lastDeployment.commit;
+ });
+
+ const createWrapper = ({ propsData = {} } = {}) =>
+ mountExtended(Commit, {
+ propsData: {
+ commit,
+ ...propsData,
+ },
+ });
+
+ afterEach(() => {
+ wrapper?.destroy();
+ });
+
+ describe('with gitlab user', () => {
+ beforeEach(() => {
+ wrapper = createWrapper();
+ });
+
+ it('links to the user profile', () => {
+ const link = wrapper.findByRole('link', { name: commit.author.name });
+ expect(link.attributes('href')).toBe(commit.author.path);
+ });
+
+ it('displays the user avatar', () => {
+ const avatar = wrapper.findByRole('img', { name: 'avatar' });
+ expect(avatar.attributes('src')).toBe(commit.author.avatarUrl);
+ });
+
+ it('links the commit message to the commit', () => {
+ const message = wrapper.findByRole('link', { name: commit.message });
+
+ expect(message.attributes('href')).toBe(commit.commitPath);
+ });
+ });
+ describe('without gitlab user', () => {
+ beforeEach(() => {
+ commit = {
+ ...commit,
+ author: null,
+ };
+ wrapper = createWrapper();
+ });
+
+ it('links to the user profile', () => {
+ const link = wrapper.findByRole('link', { name: commit.authorName });
+ expect(link.attributes('href')).toBe(`mailto:${commit.authorEmail}`);
+ });
+
+ it('displays the user avatar', () => {
+ const avatar = wrapper.findByRole('img', { name: 'avatar' });
+ expect(avatar.attributes('src')).toBe(commit.authorGravatarUrl);
+ });
+
+ it('displays the commit message', () => {
+ const message = wrapper.findByRole('link', { name: commit.message });
+
+ expect(message.attributes('href')).toBe(commit.commitPath);
+ });
+ });
+});
diff --git a/spec/frontend/environments/deploy_board_component_spec.js b/spec/frontend/environments/deploy_board_component_spec.js
index 24e94867afd..f0fb4d1027c 100644
--- a/spec/frontend/environments/deploy_board_component_spec.js
+++ b/spec/frontend/environments/deploy_board_component_spec.js
@@ -1,9 +1,10 @@
import { GlTooltip, GlIcon, GlLoadingIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import CanaryIngress from '~/environments/components/canary_ingress.vue';
import DeployBoard from '~/environments/components/deploy_board.vue';
import { deployBoardMockData, environment } from './mock_data';
+import { rolloutStatus } from './graphql/mock_data';
const logsPath = `gitlab-org/gitlab-test/-/logs?environment_name=${environment.name}`;
@@ -24,11 +25,11 @@ describe('Deploy Board', () => {
describe('with valid data', () => {
beforeEach((done) => {
wrapper = createComponent();
- wrapper.vm.$nextTick(done);
+ nextTick(done);
});
it('should render percentage with completion value provided', () => {
- expect(wrapper.vm.$refs.percentage.innerText).toEqual(`${deployBoardMockData.completion}%`);
+ expect(wrapper.find({ ref: 'percentage' }).text()).toBe(`${deployBoardMockData.completion}%`);
});
it('should render total instance count', () => {
@@ -57,20 +58,74 @@ describe('Deploy Board', () => {
it('sets up a tooltip for the legend', () => {
const iconSpan = wrapper.find('[data-testid="legend-tooltip-target"]');
- const tooltip = wrapper.find(GlTooltip);
- const icon = iconSpan.find(GlIcon);
+ const tooltip = wrapper.findComponent(GlTooltip);
+ const icon = iconSpan.findComponent(GlIcon);
expect(tooltip.props('target')()).toBe(iconSpan.element);
expect(icon.props('name')).toBe('question');
});
it('renders the canary weight selector', () => {
- const canary = wrapper.find(CanaryIngress);
+ const canary = wrapper.findComponent(CanaryIngress);
expect(canary.exists()).toBe(true);
expect(canary.props('canaryIngress')).toEqual({ canary_weight: 50 });
});
});
+ describe('with new valid data', () => {
+ beforeEach(async () => {
+ wrapper = createComponent({
+ graphql: true,
+ deployBoardData: rolloutStatus,
+ });
+ await nextTick();
+ });
+
+ it('should render percentage with completion value provided', () => {
+ expect(wrapper.find({ ref: 'percentage' }).text()).toBe(`${rolloutStatus.completion}%`);
+ });
+
+ it('should render total instance count', () => {
+ const renderedTotal = wrapper.find('.deploy-board-instances-text');
+ const actualTotal = rolloutStatus.instances.length;
+ const output = `${actualTotal > 1 ? 'Instances' : 'Instance'} (${actualTotal})`;
+
+ expect(renderedTotal.text()).toEqual(output);
+ });
+
+ it('should render all instances', () => {
+ const instances = wrapper.findAll('.deploy-board-instances-container a');
+
+ expect(instances).toHaveLength(rolloutStatus.instances.length);
+ expect(
+ instances.at(1).classes(`deployment-instance-${rolloutStatus.instances[2].status}`),
+ ).toBe(true);
+ });
+
+ it('should render an abort and a rollback button with the provided url', () => {
+ const buttons = wrapper.findAll('.deploy-board-actions a');
+
+ expect(buttons.at(0).attributes('href')).toEqual(rolloutStatus.rollbackUrl);
+ expect(buttons.at(1).attributes('href')).toEqual(rolloutStatus.abortUrl);
+ });
+
+ it('sets up a tooltip for the legend', () => {
+ const iconSpan = wrapper.find('[data-testid="legend-tooltip-target"]');
+ const tooltip = wrapper.findComponent(GlTooltip);
+ const icon = iconSpan.findComponent(GlIcon);
+
+ expect(tooltip.props('target')()).toBe(iconSpan.element);
+ expect(icon.props('name')).toBe('question');
+ });
+
+ it('renders the canary weight selector', () => {
+ const canary = wrapper.findComponent(CanaryIngress);
+ expect(canary.exists()).toBe(true);
+ expect(canary.props('canaryIngress')).toEqual({ canaryWeight: 50 });
+ expect(canary.props('graphql')).toBe(true);
+ });
+ });
+
describe('with empty state', () => {
beforeEach((done) => {
wrapper = createComponent({
@@ -79,7 +134,7 @@ describe('Deploy Board', () => {
isEmpty: true,
logsPath,
});
- wrapper.vm.$nextTick(done);
+ nextTick(done);
});
it('should render the empty state', () => {
@@ -98,11 +153,11 @@ describe('Deploy Board', () => {
isEmpty: false,
logsPath,
});
- wrapper.vm.$nextTick(done);
+ nextTick(done);
});
it('should render loading spinner', () => {
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
});
@@ -116,7 +171,7 @@ describe('Deploy Board', () => {
deployBoardData: deployBoardMockData,
});
({ statuses } = wrapper.vm);
- wrapper.vm.$nextTick(done);
+ nextTick(done);
});
it('with all the possible statuses', () => {
diff --git a/spec/frontend/environments/deploy_board_wrapper_spec.js b/spec/frontend/environments/deploy_board_wrapper_spec.js
new file mode 100644
index 00000000000..c8e6df4d324
--- /dev/null
+++ b/spec/frontend/environments/deploy_board_wrapper_spec.js
@@ -0,0 +1,124 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlCollapse, GlIcon } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { stubTransition } from 'helpers/stub_transition';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { __, s__ } from '~/locale';
+import DeployBoardWrapper from '~/environments/components/deploy_board_wrapper.vue';
+import DeployBoard from '~/environments/components/deploy_board.vue';
+import setEnvironmentToChangeCanaryMutation from '~/environments/graphql/mutations/set_environment_to_change_canary.mutation.graphql';
+import { resolvedEnvironment, rolloutStatus } from './graphql/mock_data';
+
+Vue.use(VueApollo);
+
+describe('~/environments/components/deploy_board_wrapper.vue', () => {
+ let wrapper;
+ let mockApollo;
+
+ const findDeployBoard = () => wrapper.findComponent(DeployBoard);
+
+ const createWrapper = ({ propsData = {} } = {}) => {
+ mockApollo = createMockApollo();
+ return mountExtended(DeployBoardWrapper, {
+ propsData: { environment: resolvedEnvironment, rolloutStatus, ...propsData },
+ provide: { helpPagePath: '/help' },
+ stubs: { transition: stubTransition() },
+ apolloProvider: mockApollo,
+ });
+ };
+
+ const expandCollapsedSection = async () => {
+ const button = wrapper.findByRole('button', { name: __('Expand') });
+ await button.trigger('click');
+
+ return button;
+ };
+
+ afterEach(() => {
+ wrapper?.destroy();
+ });
+
+ it('is labeled Kubernetes Pods', () => {
+ wrapper = createWrapper();
+
+ expect(wrapper.findByText(s__('DeployBoard|Kubernetes Pods')).exists()).toBe(true);
+ });
+
+ describe('collapse', () => {
+ let icon;
+ let collapse;
+
+ beforeEach(() => {
+ wrapper = createWrapper();
+ collapse = wrapper.findComponent(GlCollapse);
+ icon = wrapper.findComponent(GlIcon);
+ });
+
+ it('is collapsed by default', () => {
+ expect(collapse.attributes('visible')).toBeUndefined();
+ expect(icon.props('name')).toBe('angle-right');
+ });
+
+ it('opens on click', async () => {
+ const button = await expandCollapsedSection();
+
+ expect(button.attributes('aria-label')).toBe(__('Collapse'));
+ expect(collapse.attributes('visible')).toBe('visible');
+ expect(icon.props('name')).toBe('angle-down');
+
+ const deployBoard = findDeployBoard();
+ expect(deployBoard.exists()).toBe(true);
+ });
+ });
+
+ describe('deploy board', () => {
+ it('passes the rollout status on and sets graphql to true', async () => {
+ wrapper = createWrapper();
+ await expandCollapsedSection();
+
+ const deployBoard = findDeployBoard();
+ expect(deployBoard.props('deployBoardData')).toEqual(rolloutStatus);
+ expect(deployBoard.props('graphql')).toBe(true);
+ });
+
+ it('sets the update to the canary via graphql', () => {
+ wrapper = createWrapper();
+ jest.spyOn(mockApollo.defaultClient, 'mutate');
+ const deployBoard = findDeployBoard();
+ deployBoard.vm.$emit('changeCanaryWeight', 15);
+ expect(mockApollo.defaultClient.mutate).toHaveBeenCalledWith({
+ mutation: setEnvironmentToChangeCanaryMutation,
+ variables: { environment: resolvedEnvironment, weight: 15 },
+ });
+ });
+
+ describe('is loading', () => {
+ it('should set the loading prop', async () => {
+ wrapper = createWrapper({
+ propsData: { rolloutStatus: { ...rolloutStatus, status: 'loading' } },
+ });
+
+ await expandCollapsedSection();
+
+ const deployBoard = findDeployBoard();
+
+ expect(deployBoard.props('isLoading')).toBe(true);
+ });
+ });
+
+ describe('is empty', () => {
+ it('should set the empty prop', async () => {
+ wrapper = createWrapper({
+ propsData: { rolloutStatus: { ...rolloutStatus, status: 'not_found' } },
+ });
+
+ await expandCollapsedSection();
+
+ const deployBoard = findDeployBoard();
+
+ expect(deployBoard.props('isEmpty')).toBe(true);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/environments/deployment_spec.js b/spec/frontend/environments/deployment_spec.js
index 37209bdc86c..6cc363e000b 100644
--- a/spec/frontend/environments/deployment_spec.js
+++ b/spec/frontend/environments/deployment_spec.js
@@ -1,17 +1,32 @@
+import { GlCollapse } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { useFakeDate } from 'helpers/fake_date';
+import { stubTransition } from 'helpers/stub_transition';
+import { formatDate } from '~/lib/utils/datetime_utility';
+import { __, s__ } from '~/locale';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import Deployment from '~/environments/components/deployment.vue';
+import Commit from '~/environments/components/commit.vue';
import DeploymentStatusBadge from '~/environments/components/deployment_status_badge.vue';
import { resolvedEnvironment } from './graphql/mock_data';
describe('~/environments/components/deployment.vue', () => {
+ useFakeDate(2022, 0, 8, 16);
+
+ let deployment;
let wrapper;
+ beforeEach(() => {
+ deployment = resolvedEnvironment.lastDeployment;
+ });
+
const createWrapper = ({ propsData = {} } = {}) =>
mountExtended(Deployment, {
propsData: {
- deployment: resolvedEnvironment.lastDeployment,
+ deployment,
...propsData,
},
+ stubs: { transition: stubTransition() },
});
afterEach(() => {
@@ -21,9 +36,229 @@ describe('~/environments/components/deployment.vue', () => {
describe('status', () => {
it('should pass the deployable status to the badge', () => {
wrapper = createWrapper();
- expect(wrapper.findComponent(DeploymentStatusBadge).props('status')).toBe(
- resolvedEnvironment.lastDeployment.status,
- );
+ expect(wrapper.findComponent(DeploymentStatusBadge).props('status')).toBe(deployment.status);
+ });
+ });
+
+ describe('latest', () => {
+ it('should show a badge if the deployment is latest', () => {
+ wrapper = createWrapper({ propsData: { latest: true } });
+
+ const badge = wrapper.findByText(s__('Deployment|Latest Deployed'));
+
+ expect(badge.exists()).toBe(true);
+ });
+
+ it('should not show a badge if the deployment is not latest', () => {
+ wrapper = createWrapper();
+
+ const badge = wrapper.findByText(s__('Deployment|Latest Deployed'));
+
+ expect(badge.exists()).toBe(false);
+ });
+ });
+
+ describe('iid', () => {
+ const findIid = () => wrapper.findByTitle(s__('Deployment|Deployment ID'));
+ const findDeploymentIcon = () => wrapper.findComponent({ ref: 'deployment-iid-icon' });
+
+ describe('is present', () => {
+ beforeEach(() => {
+ wrapper = createWrapper();
+ });
+
+ it('should show the iid', () => {
+ const iid = findIid();
+ expect(iid.exists()).toBe(true);
+ });
+
+ it('should show an icon for the iid', () => {
+ const deploymentIcon = findDeploymentIcon();
+ expect(deploymentIcon.props('name')).toBe('deployments');
+ });
+ });
+
+ describe('is not present', () => {
+ beforeEach(() => {
+ wrapper = createWrapper({ propsData: { deployment: { ...deployment, iid: '' } } });
+ });
+
+ it('should not show the iid', () => {
+ const iid = findIid();
+ expect(iid.exists()).toBe(false);
+ });
+
+ it('should not show an icon for the iid', () => {
+ const deploymentIcon = findDeploymentIcon();
+ expect(deploymentIcon.exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('shortSha', () => {
+ describe('is present', () => {
+ beforeEach(() => {
+ wrapper = createWrapper();
+ });
+
+ it('shows the short SHA for the commit of the deployment', () => {
+ const sha = wrapper.findByTitle(__('Commit SHA'));
+
+ expect(sha.exists()).toBe(true);
+ expect(sha.text()).toBe(deployment.commit.shortId);
+ });
+
+ it('shows the commit icon', () => {
+ const icon = wrapper.findComponent({ ref: 'deployment-commit-icon' });
+ expect(icon.props('name')).toBe('commit');
+ });
+
+ it('shows a copy button for the sha', () => {
+ const button = wrapper.findComponent(ClipboardButton);
+ expect(button.props()).toMatchObject({
+ text: deployment.commit.shortId,
+ title: __('Copy commit SHA'),
+ });
+ });
+ });
+
+ describe('is not present', () => {
+ it('does not show the short SHA for the commit of the deployment', () => {
+ wrapper = createWrapper({
+ propsData: {
+ deployment: {
+ ...deployment,
+ commit: null,
+ },
+ },
+ });
+ const sha = wrapper.findByTestId('deployment-commit-sha');
+ expect(sha.exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('created at time', () => {
+ describe('is present', () => {
+ it('shows the timestamp the deployment was deployed at', () => {
+ wrapper = createWrapper();
+ const date = wrapper.findByTitle(formatDate(deployment.createdAt));
+
+ expect(date.text()).toBe('1 day ago');
+ });
+ });
+ describe('is not present', () => {
+ it('does not show the timestamp', () => {
+ wrapper = createWrapper({ propsData: { deployment: { ...deployment, createdAt: null } } });
+ const date = wrapper.findByTitle(formatDate(deployment.createdAt));
+
+ expect(date.exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('commit message', () => {
+ describe('with commit', () => {
+ beforeEach(() => {
+ wrapper = createWrapper();
+ });
+
+ it('shows the commit component', () => {
+ const commit = wrapper.findComponent(Commit);
+ expect(commit.props('commit')).toBe(deployment.commit);
+ });
+ });
+
+ describe('without a commit', () => {
+ it('displays nothing', () => {
+ const noCommit = {
+ ...deployment,
+ commit: null,
+ };
+ wrapper = createWrapper({ propsData: { deployment: noCommit } });
+
+ const commit = wrapper.findComponent(Commit);
+ expect(commit.exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('collapse', () => {
+ let collapse;
+ let button;
+
+ beforeEach(() => {
+ wrapper = createWrapper();
+ collapse = wrapper.findComponent(GlCollapse);
+ button = wrapper.findComponent({ ref: 'details-toggle' });
+ });
+
+ it('is collapsed by default', () => {
+ expect(collapse.attributes('visible')).toBeUndefined();
+ expect(button.props('icon')).toBe('expand-down');
+ expect(button.text()).toBe(__('Show details'));
+ });
+
+ it('opens on click', async () => {
+ await button.trigger('click');
+
+ expect(button.text()).toBe(__('Hide details'));
+ expect(button.props('icon')).toBe('expand-up');
+ expect(collapse.attributes('visible')).toBe('visible');
+
+ const username = wrapper.findByRole('link', { name: `@${deployment.user.username}` });
+
+ expect(username.attributes('href')).toBe(deployment.user.path);
+ const job = wrapper.findByRole('link', { name: deployment.deployable.name });
+ expect(job.attributes('href')).toBe(deployment.deployable.buildPath);
+ const apiBadge = wrapper.findByText(__('API'));
+ expect(apiBadge.exists()).toBe(false);
+
+ const branchLabel = wrapper.findByText(__('Branch'));
+ expect(branchLabel.exists()).toBe(true);
+ const tagLabel = wrapper.findByText(__('Tag'));
+ expect(tagLabel.exists()).toBe(false);
+ const ref = wrapper.findByRole('link', { name: deployment.ref.name });
+ expect(ref.attributes('href')).toBe(deployment.ref.refPath);
+ });
+ });
+
+ describe('with tagged deployment', () => {
+ beforeEach(async () => {
+ wrapper = createWrapper({ propsData: { deployment: { ...deployment, tag: true } } });
+ await wrapper.findComponent({ ref: 'details-toggle' }).trigger('click');
+ });
+
+ it('shows tag instead of branch', () => {
+ const refLabel = wrapper.findByText(__('Tag'));
+ expect(refLabel.exists()).toBe(true);
+ });
+ });
+
+ describe('with API deployment', () => {
+ beforeEach(async () => {
+ wrapper = createWrapper({ propsData: { deployment: { ...deployment, deployable: null } } });
+ await wrapper.findComponent({ ref: 'details-toggle' }).trigger('click');
+ });
+
+ it('shows API instead of a job name', () => {
+ const apiBadge = wrapper.findByText(__('API'));
+ expect(apiBadge.exists()).toBe(true);
+ });
+ });
+ describe('without a job path', () => {
+ beforeEach(async () => {
+ wrapper = createWrapper({
+ propsData: {
+ deployment: { ...deployment, deployable: { name: deployment.deployable.name } },
+ },
+ });
+ await wrapper.findComponent({ ref: 'details-toggle' }).trigger('click');
+ });
+
+ it('shows a span instead of a link', () => {
+ const job = wrapper.findByTitle(deployment.deployable.name);
+ expect(job.attributes('href')).toBeUndefined();
});
});
});
diff --git a/spec/frontend/environments/environment_actions_spec.js b/spec/frontend/environments/environment_actions_spec.js
index 1b68a692db8..336c207428e 100644
--- a/spec/frontend/environments/environment_actions_spec.js
+++ b/spec/frontend/environments/environment_actions_spec.js
@@ -1,6 +1,6 @@
import { GlDropdown, GlDropdownItem, GlLoadingIcon, GlIcon } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { TEST_HOST } from 'helpers/test_constants';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
@@ -108,7 +108,7 @@ describe('EnvironmentActions Component', () => {
jest.spyOn(window, 'confirm').mockImplementation(() => confirm);
findDropdownItem(scheduledJobAction).vm.$emit('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
};
beforeEach(() => {
diff --git a/spec/frontend/environments/environment_pin_spec.js b/spec/frontend/environments/environment_pin_spec.js
index a9a58071e12..669c974ea4f 100644
--- a/spec/frontend/environments/environment_pin_spec.js
+++ b/spec/frontend/environments/environment_pin_spec.js
@@ -1,5 +1,9 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
import { GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import cancelAutoStopMutation from '~/environments/graphql/mutations/cancel_auto_stop.mutation.graphql';
+import createMockApollo from 'helpers/mock_apollo_helper';
import PinComponent from '~/environments/components/environment_pin.vue';
import eventHub from '~/environments/event_hub';
@@ -18,28 +22,66 @@ describe('Pin Component', () => {
const autoStopUrl = '/root/auto-stop-env-test/-/environments/38/cancel_auto_stop';
- beforeEach(() => {
- factory({
- propsData: {
- autoStopUrl,
- },
+ describe('without graphql', () => {
+ beforeEach(() => {
+ factory({
+ propsData: {
+ autoStopUrl,
+ },
+ });
});
- });
- afterEach(() => {
- wrapper.destroy();
- });
+ afterEach(() => {
+ wrapper.destroy();
+ });
- it('should render the component with descriptive text', () => {
- expect(wrapper.text()).toBe('Prevent auto-stopping');
+ it('should render the component with descriptive text', () => {
+ expect(wrapper.text()).toBe('Prevent auto-stopping');
+ });
+
+ it('should emit onPinClick when clicked', () => {
+ const eventHubSpy = jest.spyOn(eventHub, '$emit');
+ const item = wrapper.find(GlDropdownItem);
+
+ item.vm.$emit('click');
+
+ expect(eventHubSpy).toHaveBeenCalledWith('cancelAutoStop', autoStopUrl);
+ });
});
- it('should emit onPinClick when clicked', () => {
- const eventHubSpy = jest.spyOn(eventHub, '$emit');
- const item = wrapper.find(GlDropdownItem);
+ describe('with graphql', () => {
+ Vue.use(VueApollo);
+ let mockApollo;
- item.vm.$emit('click');
+ beforeEach(() => {
+ mockApollo = createMockApollo();
+ factory({
+ propsData: {
+ autoStopUrl,
+ graphql: true,
+ },
+ apolloProvider: mockApollo,
+ });
+ });
- expect(eventHubSpy).toHaveBeenCalledWith('cancelAutoStop', autoStopUrl);
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('should render the component with descriptive text', () => {
+ expect(wrapper.text()).toBe('Prevent auto-stopping');
+ });
+
+ it('should emit onPinClick when clicked', () => {
+ jest.spyOn(mockApollo.defaultClient, 'mutate');
+ const item = wrapper.find(GlDropdownItem);
+
+ item.vm.$emit('click');
+
+ expect(mockApollo.defaultClient.mutate).toHaveBeenCalledWith({
+ mutation: cancelAutoStopMutation,
+ variables: { autoStopUrl },
+ });
+ });
});
});
diff --git a/spec/frontend/environments/environment_table_spec.js b/spec/frontend/environments/environment_table_spec.js
index 1851163ac68..c7582e4b06d 100644
--- a/spec/frontend/environments/environment_table_spec.js
+++ b/spec/frontend/environments/environment_table_spec.js
@@ -1,4 +1,5 @@
import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import CanaryUpdateModal from '~/environments/components/canary_update_modal.vue';
import DeployBoard from '~/environments/components/deploy_board.vue';
import EnvironmentTable from '~/environments/components/environments_table.vue';
@@ -181,7 +182,7 @@ describe('Environment table', () => {
});
wrapper.find(DeployBoard).vm.$emit('changeCanaryWeight', 40);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.find(CanaryUpdateModal).props()).toMatchObject({
weight: 40,
diff --git a/spec/frontend/environments/environments_app_spec.js b/spec/frontend/environments/environments_app_spec.js
index cd05ecbfb53..92d1820681c 100644
--- a/spec/frontend/environments/environments_app_spec.js
+++ b/spec/frontend/environments/environments_app_spec.js
@@ -1,6 +1,7 @@
import { GlTabs } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+import { nextTick } from 'vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import Container from '~/environments/components/container.vue';
import DeployBoard from '~/environments/components/deploy_board.vue';
@@ -186,14 +187,13 @@ describe('Environment', () => {
expect(wrapper.find('.folder-icon[data-testid="chevron-right-icon"]').exists()).toBe(false);
});
- it('should close an opened folder', () => {
+ it('should close an opened folder', async () => {
expect(wrapper.find('.folder-icon[data-testid="chevron-down-icon"]').exists()).toBe(true);
// close folder
wrapper.find('.folder-name').trigger('click');
- wrapper.vm.$nextTick(() => {
- expect(wrapper.find('.folder-icon[data-testid="chevron-down-icon"]').exists()).toBe(false);
- });
+ await nextTick();
+ expect(wrapper.find('.folder-icon[data-testid="chevron-down-icon"]').exists()).toBe(false);
});
it('should show children environments', () => {
diff --git a/spec/frontend/environments/graphql/mock_data.js b/spec/frontend/environments/graphql/mock_data.js
index fce30973547..1b7b35702de 100644
--- a/spec/frontend/environments/graphql/mock_data.js
+++ b/spec/frontend/environments/graphql/mock_data.js
@@ -1,3 +1,69 @@
+export const rolloutStatus = {
+ instances: [
+ {
+ status: 'succeeded',
+ tooltip: 'tanuki-2334 Finished',
+ podName: 'production-tanuki-1',
+ stable: false,
+ },
+ {
+ status: 'succeeded',
+ tooltip: 'tanuki-2335 Finished',
+ podName: 'production-tanuki-1',
+ stable: false,
+ },
+ {
+ status: 'succeeded',
+ tooltip: 'tanuki-2336 Finished',
+ podName: 'production-tanuki-1',
+ stable: false,
+ },
+ {
+ status: 'succeeded',
+ tooltip: 'tanuki-2337 Finished',
+ podName: 'production-tanuki-1',
+ stable: false,
+ },
+ {
+ status: 'succeeded',
+ tooltip: 'tanuki-2338 Finished',
+ podName: 'production-tanuki-1',
+ stable: false,
+ },
+ {
+ status: 'succeeded',
+ tooltip: 'tanuki-2339 Finished',
+ podName: 'production-tanuki-1',
+ stable: false,
+ },
+ { status: 'succeeded', tooltip: 'tanuki-2340 Finished', podName: 'production-tanuki-1' },
+ { status: 'succeeded', tooltip: 'tanuki-2334 Finished', podName: 'production-tanuki-1' },
+ { status: 'succeeded', tooltip: 'tanuki-2335 Finished', podName: 'production-tanuki-1' },
+ { status: 'succeeded', tooltip: 'tanuki-2336 Finished', podName: 'production-tanuki-1' },
+ { status: 'succeeded', tooltip: 'tanuki-2337 Finished', podName: 'production-tanuki-1' },
+ { status: 'succeeded', tooltip: 'tanuki-2338 Finished', podName: 'production-tanuki-1' },
+ { status: 'succeeded', tooltip: 'tanuki-2339 Finished', podName: 'production-tanuki-1' },
+ { status: 'succeeded', tooltip: 'tanuki-2340 Finished', podName: 'production-tanuki-1' },
+ { status: 'running', tooltip: 'tanuki-2341 Deploying', podName: 'production-tanuki-1' },
+ { status: 'running', tooltip: 'tanuki-2342 Deploying', podName: 'production-tanuki-1' },
+ { status: 'running', tooltip: 'tanuki-2343 Deploying', podName: 'production-tanuki-1' },
+ { status: 'failed', tooltip: 'tanuki-2344 Failed', podName: 'production-tanuki-1' },
+ { status: 'unknown', tooltip: 'tanuki-2345 Ready', podName: 'production-tanuki-1' },
+ { status: 'unknown', tooltip: 'tanuki-2346 Ready', podName: 'production-tanuki-1' },
+ { status: 'pending', tooltip: 'tanuki-2348 Preparing', podName: 'production-tanuki-1' },
+ { status: 'pending', tooltip: 'tanuki-2349 Preparing', podName: 'production-tanuki-1' },
+ { status: 'pending', tooltip: 'tanuki-2350 Preparing', podName: 'production-tanuki-1' },
+ { status: 'pending', tooltip: 'tanuki-2353 Preparing', podName: 'production-tanuki-1' },
+ { status: 'pending', tooltip: 'tanuki-2354 waiting', podName: 'production-tanuki-1' },
+ { status: 'pending', tooltip: 'tanuki-2355 waiting', podName: 'production-tanuki-1' },
+ { status: 'pending', tooltip: 'tanuki-2356 waiting', podName: 'production-tanuki-1' },
+ ],
+ abortUrl: 'url',
+ rollbackUrl: 'url',
+ completion: 100,
+ status: 'found',
+ canaryIngress: { canaryWeight: 50 },
+};
export const environmentsApp = {
environments: [
{
diff --git a/spec/frontend/environments/graphql/resolvers_spec.js b/spec/frontend/environments/graphql/resolvers_spec.js
index 6b53dc24f0f..21d7e09bad5 100644
--- a/spec/frontend/environments/graphql/resolvers_spec.js
+++ b/spec/frontend/environments/graphql/resolvers_spec.js
@@ -173,9 +173,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
it('should post to the auto stop path', async () => {
mock.onPost(ENDPOINT).reply(200);
- await mockResolvers.Mutation.cancelAutoStop(null, {
- environment: { autoStopPath: ENDPOINT },
- });
+ await mockResolvers.Mutation.cancelAutoStop(null, { autoStopUrl: ENDPOINT });
expect(mock.history.post).toContainEqual(
expect.objectContaining({ url: ENDPOINT, method: 'post' }),
diff --git a/spec/frontend/environments/new_environment_folder_spec.js b/spec/frontend/environments/new_environment_folder_spec.js
index 6823c88a5a1..460263587be 100644
--- a/spec/frontend/environments/new_environment_folder_spec.js
+++ b/spec/frontend/environments/new_environment_folder_spec.js
@@ -32,6 +32,7 @@ describe('~/environments/components/new_environments_folder.vue', () => {
apolloProvider,
propsData,
stubs: { transition: stubTransition() },
+ provide: { helpPagePath: '/help' },
});
beforeEach(async () => {
diff --git a/spec/frontend/environments/new_environment_item_spec.js b/spec/frontend/environments/new_environment_item_spec.js
index 244aef5c43b..db596688dad 100644
--- a/spec/frontend/environments/new_environment_item_spec.js
+++ b/spec/frontend/environments/new_environment_item_spec.js
@@ -2,12 +2,14 @@ import VueApollo from 'vue-apollo';
import Vue from 'vue';
import { GlCollapse, GlIcon } from '@gitlab/ui';
import createMockApollo from 'helpers/mock_apollo_helper';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper';
import { stubTransition } from 'helpers/stub_transition';
-import { __, s__ } from '~/locale';
+import { formatDate, getTimeago } from '~/lib/utils/datetime_utility';
+import { __, s__, sprintf } from '~/locale';
import EnvironmentItem from '~/environments/components/new_environment_item.vue';
import Deployment from '~/environments/components/deployment.vue';
-import { resolvedEnvironment } from './graphql/mock_data';
+import DeployBoardWrapper from '~/environments/components/deploy_board_wrapper.vue';
+import { resolvedEnvironment, rolloutStatus } from './graphql/mock_data';
Vue.use(VueApollo);
@@ -22,11 +24,19 @@ describe('~/environments/components/new_environment_item.vue', () => {
mountExtended(EnvironmentItem, {
apolloProvider,
propsData: { environment: resolvedEnvironment, ...propsData },
+ provide: { helpPagePath: '/help' },
stubs: { transition: stubTransition() },
});
const findDeployment = () => wrapper.findComponent(Deployment);
+ const expandCollapsedSection = async () => {
+ const button = wrapper.findByRole('button', { name: __('Expand') });
+ await button.trigger('click');
+
+ return button;
+ };
+
afterEach(() => {
wrapper?.destroy();
});
@@ -165,25 +175,92 @@ describe('~/environments/components/new_environment_item.vue', () => {
});
describe('pin', () => {
- it('shows the option to pin the environment if there is an autostop date', () => {
- wrapper = createWrapper({
- propsData: {
- environment: { ...resolvedEnvironment, autoStopAt: new Date(Date.now() + 100000) },
- },
- apolloProvider: createApolloProvider(),
+ describe('with autostop', () => {
+ let environment;
+
+ beforeEach(() => {
+ environment = {
+ ...resolvedEnvironment,
+ autoStopAt: new Date(Date.now() + 100000).toString(),
+ };
+ wrapper = createWrapper({
+ propsData: {
+ environment,
+ },
+ apolloProvider: createApolloProvider(),
+ });
});
- const rollback = wrapper.findByRole('menuitem', { name: __('Prevent auto-stopping') });
+ it('shows the option to pin the environment if there is an autostop date', () => {
+ const pin = wrapper.findByRole('menuitem', { name: __('Prevent auto-stopping') });
- expect(rollback.exists()).toBe(true);
+ expect(pin.exists()).toBe(true);
+ });
+
+ it('shows when the environment auto stops', () => {
+ const autoStop = wrapper.findByTitle(formatDate(environment.autoStopAt));
+
+ expect(autoStop.text()).toBe('in 1 minute');
+ });
});
- it('does not show the option to pin the environment if there is no autostop date', () => {
- wrapper = createWrapper({ apolloProvider: createApolloProvider() });
+ describe('without autostop', () => {
+ beforeEach(() => {
+ wrapper = createWrapper({ apolloProvider: createApolloProvider() });
+ });
- const rollback = wrapper.findByRole('menuitem', { name: __('Prevent auto-stopping') });
+ it('does not show the option to pin the environment if there is no autostop date', () => {
+ wrapper = createWrapper({ apolloProvider: createApolloProvider() });
- expect(rollback.exists()).toBe(false);
+ const pin = wrapper.findByRole('menuitem', { name: __('Prevent auto-stopping') });
+
+ expect(pin.exists()).toBe(false);
+ });
+
+ it('does not show when the environment auto stops', () => {
+ const autoStop = wrapper.findByText(
+ sprintf(s__('Environment|Auto stop %{time}'), {
+ time: getTimeago().format(resolvedEnvironment.autoStopAt),
+ }),
+ );
+
+ expect(autoStop.exists()).toBe(false);
+ });
+ });
+
+ describe('with past autostop', () => {
+ let environment;
+
+ beforeEach(() => {
+ environment = {
+ ...resolvedEnvironment,
+ autoStopAt: new Date(Date.now() - 100000).toString(),
+ };
+ wrapper = createWrapper({
+ propsData: {
+ environment,
+ },
+ apolloProvider: createApolloProvider(),
+ });
+ });
+
+ it('does not show the option to pin the environment if there is no autostop date', () => {
+ wrapper = createWrapper({ apolloProvider: createApolloProvider() });
+
+ const pin = wrapper.findByRole('menuitem', { name: __('Prevent auto-stopping') });
+
+ expect(pin.exists()).toBe(false);
+ });
+
+ it('does not show when the environment auto stops', () => {
+ const autoStop = wrapper.findByText(
+ sprintf(s__('Environment|Auto stop %{time}'), {
+ time: getTimeago().format(environment.autoStopAt),
+ }),
+ );
+
+ expect(autoStop.exists()).toBe(false);
+ });
});
});
@@ -258,14 +335,12 @@ describe('~/environments/components/new_environment_item.vue', () => {
describe('collapse', () => {
let icon;
let collapse;
- let button;
let environmentName;
beforeEach(() => {
wrapper = createWrapper({ apolloProvider: createApolloProvider() });
collapse = wrapper.findComponent(GlCollapse);
icon = wrapper.findComponent(GlIcon);
- button = wrapper.findByRole('button', { name: __('Expand') });
environmentName = wrapper.findByText(resolvedEnvironment.name);
});
@@ -278,7 +353,7 @@ describe('~/environments/components/new_environment_item.vue', () => {
it('opens on click', async () => {
expect(findDeployment().isVisible()).toBe(false);
- await button.trigger('click');
+ const button = await expandCollapsedSection();
expect(button.attributes('aria-label')).toBe(__('Collapse'));
expect(collapse.attributes('visible')).toBe('visible');
@@ -338,4 +413,78 @@ describe('~/environments/components/new_environment_item.vue', () => {
expect(deployment.exists()).toBe(false);
});
});
+
+ describe('empty state', () => {
+ it('should link to documentation', async () => {
+ const environment = {
+ ...resolvedEnvironment,
+ lastDeployment: null,
+ upcomingDeployment: null,
+ };
+
+ wrapper = createWrapper({
+ propsData: { environment },
+ apolloProvider: createApolloProvider(),
+ });
+
+ await expandCollapsedSection();
+
+ const text = s__(
+ 'Environments|There are no deployments for this environment yet. Learn more about setting up deployments.',
+ );
+
+ const emptyState = wrapper.findByText((_content, element) => element.textContent === text);
+
+ const link = extendedWrapper(emptyState).findByRole('link');
+
+ expect(link.attributes('href')).toBe('/help');
+ });
+
+ it('should not link to the documentation when there are deployments', async () => {
+ wrapper = createWrapper({
+ apolloProvider: createApolloProvider(),
+ });
+
+ await expandCollapsedSection();
+
+ const text = s__(
+ 'Environments|There are no deployments for this environment yet. Learn more about setting up deployments.',
+ );
+
+ const emptyState = wrapper.findByText((_content, element) => element.textContent === text);
+
+ expect(emptyState.exists()).toBe(false);
+ });
+ });
+
+ describe('deploy boards', () => {
+ it('should show a deploy board if the environment has a rollout status', async () => {
+ const environment = {
+ ...resolvedEnvironment,
+ rolloutStatus,
+ };
+
+ wrapper = createWrapper({
+ propsData: { environment },
+ apolloProvider: createApolloProvider(),
+ });
+
+ await expandCollapsedSection();
+
+ const deployBoard = wrapper.findComponent(DeployBoardWrapper);
+ expect(deployBoard.exists()).toBe(true);
+ expect(deployBoard.props('rolloutStatus')).toBe(rolloutStatus);
+ });
+
+ it('should not show a deploy board if the environment has no rollout status', async () => {
+ wrapper = createWrapper({
+ apolloProvider: createApolloProvider(),
+ });
+
+ await expandCollapsedSection();
+
+ const deployBoard = wrapper.findComponent(DeployBoardWrapper);
+ expect(deployBoard.exists()).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/environments/new_environments_app_spec.js b/spec/frontend/environments/new_environments_app_spec.js
index c9eccc26694..42e3608109b 100644
--- a/spec/frontend/environments/new_environments_app_spec.js
+++ b/spec/frontend/environments/new_environments_app_spec.js
@@ -10,6 +10,7 @@ import EnvironmentsApp from '~/environments/components/new_environments_app.vue'
import EnvironmentsFolder from '~/environments/components/new_environment_folder.vue';
import EnvironmentsItem from '~/environments/components/new_environment_item.vue';
import StopEnvironmentModal from '~/environments/components/stop_environment_modal.vue';
+import CanaryUpdateModal from '~/environments/components/canary_update_modal.vue';
import { resolvedEnvironmentsApp, resolvedFolder, resolvedEnvironment } from './graphql/mock_data';
Vue.use(VueApollo);
@@ -20,6 +21,8 @@ describe('~/environments/components/new_environments_app.vue', () => {
let environmentFolderMock;
let paginationMock;
let environmentToStopMock;
+ let environmentToChangeCanaryMock;
+ let weightMock;
const createApolloProvider = () => {
const mockResolvers = {
@@ -28,6 +31,10 @@ describe('~/environments/components/new_environments_app.vue', () => {
folder: environmentFolderMock,
pageInfo: paginationMock,
environmentToStop: environmentToStopMock,
+ environmentToDelete: jest.fn().mockResolvedValue(resolvedEnvironment),
+ environmentToRollback: jest.fn().mockResolvedValue(resolvedEnvironment),
+ environmentToChangeCanary: environmentToChangeCanaryMock,
+ weight: weightMock,
},
};
@@ -40,6 +47,7 @@ describe('~/environments/components/new_environments_app.vue', () => {
newEnvironmentPath: '/environments/new',
canCreateEnvironment: true,
defaultBranchName: 'main',
+ helpPagePath: '/help',
...provide,
},
apolloProvider,
@@ -50,6 +58,8 @@ describe('~/environments/components/new_environments_app.vue', () => {
environmentsApp,
folder,
environmentToStop = {},
+ environmentToChangeCanary = {},
+ weight = 0,
pageInfo = {
total: 20,
perPage: 5,
@@ -64,6 +74,8 @@ describe('~/environments/components/new_environments_app.vue', () => {
environmentFolderMock.mockReturnValue(folder);
paginationMock.mockReturnValue(pageInfo);
environmentToStopMock.mockReturnValue(environmentToStop);
+ environmentToChangeCanaryMock.mockReturnValue(environmentToChangeCanary);
+ weightMock.mockReturnValue(weight);
const apolloProvider = createApolloProvider();
wrapper = createWrapper({ apolloProvider, provide });
@@ -75,11 +87,13 @@ describe('~/environments/components/new_environments_app.vue', () => {
environmentAppMock = jest.fn();
environmentFolderMock = jest.fn();
environmentToStopMock = jest.fn();
+ environmentToChangeCanaryMock = jest.fn();
+ weightMock = jest.fn();
paginationMock = jest.fn();
});
afterEach(() => {
- wrapper?.destroy();
+ wrapper.destroy();
});
it('should show all the folders that are fetched', async () => {
@@ -149,8 +163,6 @@ describe('~/environments/components/new_environments_app.vue', () => {
},
folder: resolvedFolder,
});
- await waitForPromises();
- await nextTick();
const button = wrapper.findByRole('button', { name: s__('Environments|Enable review app') });
expect(button.exists()).toBe(false);
@@ -206,6 +218,19 @@ describe('~/environments/components/new_environments_app.vue', () => {
expect(modal.props('environment')).toMatchObject(resolvedEnvironment);
});
+
+ it('should pass the environment to change canary to the canary update modal', async () => {
+ await createWrapperWithMocked({
+ environmentsApp: resolvedEnvironmentsApp,
+ folder: resolvedFolder,
+ environmentToChangeCanary: resolvedEnvironment,
+ weight: 10,
+ });
+
+ const modal = wrapper.findComponent(CanaryUpdateModal);
+
+ expect(modal.props('environment')).toMatchObject(resolvedEnvironment);
+ });
});
describe('pagination', () => {
diff --git a/spec/frontend/error_tracking/components/error_details_spec.js b/spec/frontend/error_tracking/components/error_details_spec.js
index 77f51193258..03ae437a89e 100644
--- a/spec/frontend/error_tracking/components/error_details_spec.js
+++ b/spec/frontend/error_tracking/components/error_details_spec.js
@@ -7,7 +7,8 @@ import {
GlAlert,
GlSprintf,
} from '@gitlab/ui';
-import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import {
severityLevel,
@@ -27,8 +28,7 @@ import Tracking from '~/tracking';
jest.mock('~/flash');
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('ErrorDetails', () => {
let store;
@@ -53,7 +53,6 @@ describe('ErrorDetails', () => {
function mountComponent() {
wrapper = shallowMount(ErrorDetails, {
stubs: { GlButton, GlSprintf },
- localVue,
store,
mocks,
propsData: {
@@ -140,32 +139,30 @@ describe('ErrorDetails', () => {
mountComponent();
});
- it('when before timeout, still shows loading', () => {
+ it('when before timeout, still shows loading', async () => {
Date.now.mockReturnValue(endTime - 1);
wrapper.vm.onNoApolloResult();
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
- expect(createFlash).not.toHaveBeenCalled();
- expect(mocks.$apollo.queries.error.stopPolling).not.toHaveBeenCalled();
- });
+ await nextTick();
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(createFlash).not.toHaveBeenCalled();
+ expect(mocks.$apollo.queries.error.stopPolling).not.toHaveBeenCalled();
});
- it('when timeout is hit and no apollo result, stops loading and shows flash', () => {
+ it('when timeout is hit and no apollo result, stops loading and shows flash', async () => {
Date.now.mockReturnValue(endTime + 1);
wrapper.vm.onNoApolloResult();
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
- expect(wrapper.find(GlLink).exists()).toBe(false);
- expect(createFlash).toHaveBeenCalledWith({
- message: 'Could not connect to Sentry. Refresh the page to try again.',
- type: 'warning',
- });
- expect(mocks.$apollo.queries.error.stopPolling).toHaveBeenCalled();
+ await nextTick();
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.find(GlLink).exists()).toBe(false);
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'Could not connect to Sentry. Refresh the page to try again.',
+ type: 'warning',
});
+ expect(mocks.$apollo.queries.error.stopPolling).toHaveBeenCalled();
});
});
@@ -225,7 +222,7 @@ describe('ErrorDetails', () => {
});
describe('Badges', () => {
- it('should show language and error level badges', () => {
+ it('should show language and error level badges', async () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({
@@ -233,12 +230,11 @@ describe('ErrorDetails', () => {
tags: { level: 'error', logger: 'ruby' },
},
});
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.findAll(GlBadge).length).toBe(2);
- });
+ await nextTick();
+ expect(wrapper.findAll(GlBadge).length).toBe(2);
});
- it('should NOT show the badge if the tag is not present', () => {
+ it('should NOT show the badge if the tag is not present', async () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({
@@ -246,14 +242,13 @@ describe('ErrorDetails', () => {
tags: { level: 'error' },
},
});
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.findAll(GlBadge).length).toBe(1);
- });
+ await nextTick();
+ expect(wrapper.findAll(GlBadge).length).toBe(1);
});
it.each(Object.keys(severityLevel))(
'should set correct severity level variant for %s badge',
- (level) => {
+ async (level) => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({
@@ -261,15 +256,14 @@ describe('ErrorDetails', () => {
tags: { level: severityLevel[level] },
},
});
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(GlBadge).props('variant')).toEqual(
- severityLevelVariant[severityLevel[level]],
- );
- });
+ await nextTick();
+ expect(wrapper.find(GlBadge).props('variant')).toEqual(
+ severityLevelVariant[severityLevel[level]],
+ );
},
);
- it('should fallback for ERROR severityLevelVariant when severityLevel is unknown', () => {
+ it('should fallback for ERROR severityLevelVariant when severityLevel is unknown', async () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({
@@ -277,32 +271,29 @@ describe('ErrorDetails', () => {
tags: { level: 'someNewErrorLevel' },
},
});
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(GlBadge).props('variant')).toEqual(
- severityLevelVariant[severityLevel.ERROR],
- );
- });
+ await nextTick();
+ expect(wrapper.find(GlBadge).props('variant')).toEqual(
+ severityLevelVariant[severityLevel.ERROR],
+ );
});
});
describe('Stacktrace', () => {
- it('should show stacktrace', () => {
+ it('should show stacktrace', async () => {
store.state.details.loadingStacktrace = false;
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
- expect(wrapper.find(Stacktrace).exists()).toBe(true);
- expect(findAlert().exists()).toBe(false);
- });
+ await nextTick();
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.find(Stacktrace).exists()).toBe(true);
+ expect(findAlert().exists()).toBe(false);
});
- it('should NOT show stacktrace if no entries and show Alert message', () => {
+ it('should NOT show stacktrace if no entries and show Alert message', async () => {
store.state.details.loadingStacktrace = false;
store.getters = { 'details/sentryUrl': () => 'sentry.io', 'details/stacktrace': () => [] };
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
- expect(wrapper.find(Stacktrace).exists()).toBe(false);
- expect(findAlert().text()).toBe('No stack trace for this error');
- });
+ await nextTick();
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.find(Stacktrace).exists()).toBe(false);
+ expect(findAlert().text()).toBe('No stack trace for this error');
});
});
@@ -339,10 +330,10 @@ describe('ErrorDetails', () => {
});
describe('when error is unresolved', () => {
- beforeEach(() => {
+ beforeEach(async () => {
store.state.details.errorStatus = errorStatus.UNRESOLVED;
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('displays Ignore and Resolve buttons', () => {
@@ -366,10 +357,10 @@ describe('ErrorDetails', () => {
});
describe('when error is ignored', () => {
- beforeEach(() => {
+ beforeEach(async () => {
store.state.details.errorStatus = errorStatus.IGNORED;
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('displays Undo Ignore and Resolve buttons', () => {
@@ -393,10 +384,10 @@ describe('ErrorDetails', () => {
});
describe('when error is resolved', () => {
- beforeEach(() => {
+ beforeEach(async () => {
store.state.details.errorStatus = errorStatus.RESOLVED;
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('displays Ignore and Unresolve buttons', () => {
@@ -418,7 +409,7 @@ describe('ErrorDetails', () => {
);
});
- it('should show alert with closed issueId', () => {
+ it('should show alert with closed issueId', async () => {
const closedIssueId = 123;
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
@@ -427,10 +418,9 @@ describe('ErrorDetails', () => {
closedIssueId,
});
- return wrapper.vm.$nextTick().then(() => {
- expect(findAlert().exists()).toBe(true);
- expect(findAlert().text()).toContain(`#${closedIssueId}`);
- });
+ await nextTick();
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().text()).toContain(`#${closedIssueId}`);
});
});
});
@@ -496,7 +486,7 @@ describe('ErrorDetails', () => {
'/gitlab-org/gitlab-test/commit/7975be0116940bf2ad4321f79d02a55c5f7779aa';
const findGitLabCommitLink = () => wrapper.find(`[href$="${gitlabCommitPath}"]`);
- it('should display a link', () => {
+ it('should display a link', async () => {
mocks.$apollo.queries.error.loading = false;
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
@@ -506,12 +496,11 @@ describe('ErrorDetails', () => {
gitlabCommitPath,
},
});
- return wrapper.vm.$nextTick().then(() => {
- expect(findGitLabCommitLink().exists()).toBe(true);
- });
+ await nextTick();
+ expect(findGitLabCommitLink().exists()).toBe(true);
});
- it('should not display a link', () => {
+ it('should not display a link', async () => {
mocks.$apollo.queries.error.loading = false;
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
@@ -520,9 +509,8 @@ describe('ErrorDetails', () => {
gitlabCommit: null,
},
});
- return wrapper.vm.$nextTick().then(() => {
- expect(findGitLabCommitLink().exists()).toBe(false);
- });
+ await nextTick();
+ expect(findGitLabCommitLink().exists()).toBe(false);
});
});
@@ -595,33 +583,25 @@ describe('ErrorDetails', () => {
expect(Tracking.event).toHaveBeenCalledWith(category, action);
});
- it('should track IGNORE status update', () => {
+ it('should track IGNORE status update', async () => {
Tracking.event.mockClear();
- findUpdateIgnoreStatusButton().vm.$emit('click');
- setImmediate(() => {
- const { category, action } = trackErrorStatusUpdateOptions('ignored');
- expect(Tracking.event).toHaveBeenCalledWith(category, action);
- });
+ await findUpdateIgnoreStatusButton().trigger('click');
+ const { category, action } = trackErrorStatusUpdateOptions('ignored');
+ expect(Tracking.event).toHaveBeenCalledWith(category, action);
});
- it('should track RESOLVE status update', () => {
+ it('should track RESOLVE status update', async () => {
Tracking.event.mockClear();
- findUpdateResolveStatusButton().vm.$emit('click');
- setImmediate(() => {
- const { category, action } = trackErrorStatusUpdateOptions('resolved');
- expect(Tracking.event).toHaveBeenCalledWith(category, action);
- });
+ await findUpdateResolveStatusButton().trigger('click');
+ const { category, action } = trackErrorStatusUpdateOptions('resolved');
+ expect(Tracking.event).toHaveBeenCalledWith(category, action);
});
- it('should track external Sentry link views', () => {
+ it('should track external Sentry link views', async () => {
Tracking.event.mockClear();
- findExternalUrl().trigger('click');
- setImmediate(() => {
- const { category, action, label, property } = trackClickErrorLinkToSentryOptions(
- externalUrl,
- );
- expect(Tracking.event).toHaveBeenCalledWith(category, action, { label, property });
- });
+ await findExternalUrl().trigger('click');
+ const { category, action, label, property } = trackClickErrorLinkToSentryOptions(externalUrl);
+ expect(Tracking.event).toHaveBeenCalledWith(category, action, { label, property });
});
});
});
diff --git a/spec/frontend/error_tracking/components/error_tracking_actions_spec.js b/spec/frontend/error_tracking/components/error_tracking_actions_spec.js
index e21c40423c3..7ed4e5f6b05 100644
--- a/spec/frontend/error_tracking/components/error_tracking_actions_spec.js
+++ b/spec/frontend/error_tracking/components/error_tracking_actions_spec.js
@@ -1,5 +1,6 @@
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import ErrorTrackingActions from '~/error_tracking/components/error_tracking_actions.vue';
describe('Error Tracking Actions', () => {
@@ -37,13 +38,12 @@ describe('Error Tracking Actions', () => {
const findButtons = () => wrapper.findAll(GlButton);
describe('when error status is unresolved', () => {
- it('renders the correct actions buttons to allow ignore and resolve', () => {
+ it('renders the correct actions buttons to allow ignore and resolve', async () => {
expect(findButtons().exists()).toBe(true);
- return wrapper.vm.$nextTick().then(() => {
- expect(findButtons().at(0).attributes('title')).toBe('Ignore');
- expect(findButtons().at(1).attributes('title')).toBe('Resolve');
- });
+ await nextTick();
+ expect(findButtons().at(0).attributes('title')).toBe('Ignore');
+ expect(findButtons().at(1).attributes('title')).toBe('Resolve');
});
});
@@ -52,12 +52,11 @@ describe('Error Tracking Actions', () => {
mountComponent({ error: { status: 'ignored' } });
});
- it('renders the correct action button to undo ignore', () => {
+ it('renders the correct action button to undo ignore', async () => {
expect(findButtons().exists()).toBe(true);
- return wrapper.vm.$nextTick().then(() => {
- expect(findButtons().at(0).attributes('title')).toBe('Undo Ignore');
- });
+ await nextTick();
+ expect(findButtons().at(0).attributes('title')).toBe('Undo Ignore');
});
});
@@ -66,12 +65,11 @@ describe('Error Tracking Actions', () => {
mountComponent({ error: { status: 'resolved' } });
});
- it('renders the correct action button to undo unresolve', () => {
+ it('renders the correct action button to undo unresolve', async () => {
expect(findButtons().exists()).toBe(true);
- return wrapper.vm.$nextTick().then(() => {
- expect(findButtons().at(1).attributes('title')).toBe('Unresolve');
- });
+ await nextTick();
+ expect(findButtons().at(1).attributes('title')).toBe('Unresolve');
});
});
});
diff --git a/spec/frontend/error_tracking/components/error_tracking_list_spec.js b/spec/frontend/error_tracking/components/error_tracking_list_spec.js
index 74d5731bbea..59671c175e7 100644
--- a/spec/frontend/error_tracking/components/error_tracking_list_spec.js
+++ b/spec/frontend/error_tracking/components/error_tracking_list_spec.js
@@ -1,5 +1,6 @@
import { GlEmptyState, GlLoadingIcon, GlFormInput, GlPagination, GlDropdown } from '@gitlab/ui';
-import { createLocalVue, mount } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import stubChildren from 'helpers/stub_children';
import ErrorTrackingActions from '~/error_tracking/components/error_tracking_actions.vue';
@@ -8,8 +9,7 @@ import { trackErrorListViewsOptions, trackErrorStatusUpdateOptions } from '~/err
import Tracking from '~/tracking';
import errorsList from './list_mock.json';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('ErrorTrackingList', () => {
let store;
@@ -32,7 +32,6 @@ describe('ErrorTrackingList', () => {
stubs = {},
} = {}) {
wrapper = mount(ErrorTrackingList, {
- localVue,
store,
propsData: {
indexPath: '/path',
@@ -317,15 +316,14 @@ describe('ErrorTrackingList', () => {
expect(findRecentSearchesDropdown().text()).toContain("You don't have any recent searches");
});
- it('shows items', () => {
+ it('shows items', async () => {
store.state.list.recentSearches = ['great', 'search'];
- return wrapper.vm.$nextTick().then(() => {
- const dropdownItems = wrapper.findAll('.filtered-search-box li');
- expect(dropdownItems.length).toBe(3);
- expect(dropdownItems.at(0).text()).toBe('great');
- expect(dropdownItems.at(1).text()).toBe('search');
- });
+ await nextTick();
+ const dropdownItems = wrapper.findAll('.filtered-search-box li');
+ expect(dropdownItems.length).toBe(3);
+ expect(dropdownItems.at(0).text()).toBe('great');
+ expect(dropdownItems.at(1).text()).toBe('search');
});
describe('clear', () => {
@@ -337,23 +335,21 @@ describe('ErrorTrackingList', () => {
expect(clearRecentButton().exists()).toBe(false);
});
- it('is visible when list has items', () => {
+ it('is visible when list has items', async () => {
store.state.list.recentSearches = ['some', 'searches'];
- return wrapper.vm.$nextTick().then(() => {
- expect(clearRecentButton().exists()).toBe(true);
- expect(clearRecentButton().text()).toBe('Clear recent searches');
- });
+ await nextTick();
+ expect(clearRecentButton().exists()).toBe(true);
+ expect(clearRecentButton().text()).toBe('Clear recent searches');
});
- it('clears items on click', () => {
+ it('clears items on click', async () => {
store.state.list.recentSearches = ['some', 'searches'];
- return wrapper.vm.$nextTick().then(() => {
- clearRecentButton().vm.$emit('click');
+ await nextTick();
+ clearRecentButton().vm.$emit('click');
- expect(actions.clearRecentSearches).toHaveBeenCalledTimes(1);
- });
+ expect(actions.clearRecentSearches).toHaveBeenCalledTimes(1);
});
});
});
@@ -388,7 +384,7 @@ describe('ErrorTrackingList', () => {
describe('and the user is not on the first page', () => {
describe('and the previous button is clicked', () => {
- beforeEach(() => {
+ beforeEach(async () => {
store.state.list.loading = false;
mountComponent({
stubs: {
@@ -399,7 +395,7 @@ describe('ErrorTrackingList', () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ pageValue: 2 });
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('fetches the previous page of results', () => {
@@ -451,7 +447,7 @@ describe('ErrorTrackingList', () => {
expect(Tracking.event).toHaveBeenCalledWith(category, action);
});
- it('should track status updates', () => {
+ it('should track status updates', async () => {
Tracking.event.mockClear();
const status = 'ignored';
findErrorActions().vm.$emit('update-issue-status', {
@@ -459,10 +455,10 @@ describe('ErrorTrackingList', () => {
status,
});
- setImmediate(() => {
- const { category, action } = trackErrorStatusUpdateOptions(status);
- expect(Tracking.event).toHaveBeenCalledWith(category, action);
- });
+ await nextTick();
+
+ const { category, action } = trackErrorStatusUpdateOptions(status);
+ expect(Tracking.event).toHaveBeenCalledWith(category, action);
});
});
});
diff --git a/spec/frontend/error_tracking_settings/components/app_spec.js b/spec/frontend/error_tracking_settings/components/app_spec.js
index 844faff64a1..4d19ec047ef 100644
--- a/spec/frontend/error_tracking_settings/components/app_spec.js
+++ b/spec/frontend/error_tracking_settings/components/app_spec.js
@@ -1,6 +1,6 @@
import { GlFormRadioGroup, GlFormRadio, GlFormInputGroup } from '@gitlab/ui';
-import { createLocalVue, shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { TEST_HOST } from 'helpers/test_constants';
@@ -10,8 +10,7 @@ import ErrorTrackingForm from '~/error_tracking_settings/components/error_tracki
import ProjectDropdown from '~/error_tracking_settings/components/project_dropdown.vue';
import createStore from '~/error_tracking_settings/store';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
const TEST_GITLAB_DSN = 'https://gitlab.example.com/123456';
@@ -22,7 +21,6 @@ describe('error tracking settings app', () => {
function mountComponent() {
wrapper = extendedWrapper(
shallowMount(ErrorTrackingSettings, {
- localVue,
store, // Override the imported store
propsData: {
initialEnabled: 'true',
@@ -81,12 +79,11 @@ describe('error tracking settings app', () => {
expect(wrapper.find('.js-error-tracking-button').attributes('disabled')).toBeFalsy();
});
- it('disables the button when saving', () => {
+ it('disables the button when saving', async () => {
store.state.settingsLoading = true;
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.find('.js-error-tracking-button').attributes('disabled')).toBeTruthy();
- });
+ await nextTick();
+ expect(wrapper.find('.js-error-tracking-button').attributes('disabled')).toBeTruthy();
});
});
diff --git a/spec/frontend/error_tracking_settings/components/error_tracking_form_spec.js b/spec/frontend/error_tracking_settings/components/error_tracking_form_spec.js
index 2e8a42dbfe6..69d684faec2 100644
--- a/spec/frontend/error_tracking_settings/components/error_tracking_form_spec.js
+++ b/spec/frontend/error_tracking_settings/components/error_tracking_form_spec.js
@@ -1,12 +1,12 @@
import { GlFormInput, GlButton } from '@gitlab/ui';
-import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import ErrorTrackingForm from '~/error_tracking_settings/components/error_tracking_form.vue';
import createStore from '~/error_tracking_settings/store';
import { defaultProps } from '../mock';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('error tracking settings form', () => {
let wrapper;
@@ -14,7 +14,6 @@ describe('error tracking settings form', () => {
function mountComponent() {
wrapper = shallowMount(ErrorTrackingForm, {
- localVue,
store,
propsData: defaultProps,
});
diff --git a/spec/frontend/error_tracking_settings/components/project_dropdown_spec.js b/spec/frontend/error_tracking_settings/components/project_dropdown_spec.js
index 79518a487d4..1ba5a505f57 100644
--- a/spec/frontend/error_tracking_settings/components/project_dropdown_spec.js
+++ b/spec/frontend/error_tracking_settings/components/project_dropdown_spec.js
@@ -1,19 +1,18 @@
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
-import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import { pick, clone } from 'lodash';
import Vuex from 'vuex';
import ProjectDropdown from '~/error_tracking_settings/components/project_dropdown.vue';
import { defaultProps, projectList, staleProject } from '../mock';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('error tracking settings project dropdown', () => {
let wrapper;
function mountComponent() {
wrapper = shallowMount(ProjectDropdown, {
- localVue,
propsData: {
...pick(
defaultProps,
@@ -64,10 +63,10 @@ describe('error tracking settings project dropdown', () => {
});
describe('populated project list', () => {
- beforeEach(() => {
+ beforeEach(async () => {
wrapper.setProps({ projects: clone(projectList), hasProjects: true });
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('renders the dropdown', () => {
@@ -84,9 +83,9 @@ describe('error tracking settings project dropdown', () => {
describe('selected project', () => {
const selectedProject = clone(projectList[0]);
- beforeEach(() => {
+ beforeEach(async () => {
wrapper.setProps({ projects: clone(projectList), selectedProject, hasProjects: true });
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('does not show helper text', () => {
@@ -96,13 +95,13 @@ describe('error tracking settings project dropdown', () => {
});
describe('invalid project selected', () => {
- beforeEach(() => {
+ beforeEach(async () => {
wrapper.setProps({
projects: clone(projectList),
selectedProject: staleProject,
isProjectInvalid: true,
});
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('displays a error', () => {
diff --git a/spec/frontend/feature_flags/components/configure_feature_flags_modal_spec.js b/spec/frontend/feature_flags/components/configure_feature_flags_modal_spec.js
index f244da228b3..4a0242b4a46 100644
--- a/spec/frontend/feature_flags/components/configure_feature_flags_modal_spec.js
+++ b/spec/frontend/feature_flags/components/configure_feature_flags_modal_spec.js
@@ -1,5 +1,6 @@
import { GlModal, GlSprintf, GlAlert } from '@gitlab/ui';
+import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import Component from '~/feature_flags/components/configure_feature_flags_modal.vue';
@@ -56,7 +57,7 @@ describe('Configure Feature Flags Modal', () => {
it('should emit a `token` event when clicking on the Primary action', async () => {
findGlModal().vm.$emit('secondary', mockEvent);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.emitted('token')).toEqual([[]]);
expect(mockEvent.preventDefault).toHaveBeenCalled();
});
@@ -64,7 +65,7 @@ describe('Configure Feature Flags Modal', () => {
it('should clear the project name input after generating the token', async () => {
findProjectNameInput().vm.$emit('input', provide.projectName);
findGlModal().vm.$emit('primary', mockEvent);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findProjectNameInput().attributes('value')).toBe('');
});
@@ -116,7 +117,7 @@ describe('Configure Feature Flags Modal', () => {
it('should enable the secondary action', async () => {
findProjectNameInput().vm.$emit('input', provide.projectName);
- await wrapper.vm.$nextTick();
+ await nextTick();
const [{ disabled }] = findSecondaryAction().attributes;
expect(disabled).toBe(false);
});
diff --git a/spec/frontend/feature_flags/components/edit_feature_flag_spec.js b/spec/frontend/feature_flags/components/edit_feature_flag_spec.js
index 721b7249abc..05709cd05e6 100644
--- a/spec/frontend/feature_flags/components/edit_feature_flag_spec.js
+++ b/spec/frontend/feature_flags/components/edit_feature_flag_spec.js
@@ -1,7 +1,7 @@
import { GlToggle, GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { mockTracking } from 'helpers/tracking_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -12,6 +12,7 @@ import createStore from '~/feature_flags/store/edit';
import axios from '~/lib/utils/axios_utils';
Vue.use(Vuex);
+
describe('Edit feature flag form', () => {
let wrapper;
let mock;
@@ -66,13 +67,12 @@ describe('Edit feature flag form', () => {
});
describe('with error', () => {
- it('should render the error', () => {
+ it('should render the error', async () => {
store.dispatch('receiveUpdateFeatureFlagError', { message: ['The name is required'] });
- return wrapper.vm.$nextTick(() => {
- const warningGlAlert = findWarningGlAlert();
- expect(warningGlAlert.exists()).toEqual(true);
- expect(warningGlAlert.text()).toContain('The name is required');
- });
+ await nextTick();
+ const warningGlAlert = findWarningGlAlert();
+ expect(warningGlAlert.exists()).toEqual(true);
+ expect(warningGlAlert.text()).toContain('The name is required');
});
});
diff --git a/spec/frontend/feature_flags/components/empty_state_spec.js b/spec/frontend/feature_flags/components/empty_state_spec.js
index 86d0c1a05fd..4ac82ae44a6 100644
--- a/spec/frontend/feature_flags/components/empty_state_spec.js
+++ b/spec/frontend/feature_flags/components/empty_state_spec.js
@@ -1,5 +1,6 @@
import { GlAlert, GlEmptyState, GlLink, GlLoadingIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import EmptyState from '~/feature_flags/components/empty_state.vue';
const DEFAULT_PROPS = {
@@ -123,7 +124,7 @@ describe('feature_flags/components/feature_flags_tab.vue', () => {
beforeEach(async () => {
wrapper = factory();
- await wrapper.vm.$nextTick();
+ await nextTick();
slot = wrapper.find('[data-testid="test-slot"]');
});
diff --git a/spec/frontend/feature_flags/components/environments_dropdown_spec.js b/spec/frontend/feature_flags/components/environments_dropdown_spec.js
index 9194db3a182..cca472012e9 100644
--- a/spec/frontend/feature_flags/components/environments_dropdown_spec.js
+++ b/spec/frontend/feature_flags/components/environments_dropdown_spec.js
@@ -1,6 +1,7 @@
import { GlLoadingIcon, GlButton, GlSearchBoxByType } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
import { TEST_HOST } from 'spec/test_constants';
import EnvironmentsDropdown from '~/feature_flags/components/environments_dropdown.vue';
@@ -54,7 +55,7 @@ describe('Feature flags > Environments dropdown ', () => {
factory();
findEnvironmentSearchInput().vm.$emit('focus');
await waitForPromises();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.find('.dropdown-content > ul').exists()).toBe(true);
expect(wrapper.findAll('.dropdown-content > ul > li').exists()).toBe(true);
});
@@ -66,7 +67,7 @@ describe('Feature flags > Environments dropdown ', () => {
factory();
findEnvironmentSearchInput().vm.$emit('keyup');
await waitForPromises();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.find('.dropdown-content > ul').exists()).toBe(true);
expect(wrapper.findAll('.dropdown-content > ul > li').exists()).toBe(true);
});
@@ -80,7 +81,7 @@ describe('Feature flags > Environments dropdown ', () => {
findEnvironmentSearchInput().vm.$emit('focus');
findEnvironmentSearchInput().vm.$emit('input', 'production');
await waitForPromises();
- await wrapper.vm.$nextTick();
+ await nextTick();
});
it('sets filter value', () => {
@@ -103,7 +104,7 @@ describe('Feature flags > Environments dropdown ', () => {
.filter((b) => b.text() === 'production')
.at(0);
button.vm.$emit('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.emitted('selectEnvironment')).toEqual([['production']]);
});
});
@@ -111,7 +112,7 @@ describe('Feature flags > Environments dropdown ', () => {
describe('on click clear button', () => {
beforeEach(async () => {
wrapper.find(GlButton).vm.$emit('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
});
it('resets filter value', () => {
@@ -132,12 +133,12 @@ describe('Feature flags > Environments dropdown ', () => {
findEnvironmentSearchInput().vm.$emit('focus');
findEnvironmentSearchInput().vm.$emit('input', 'production');
await waitForPromises();
- await wrapper.vm.$nextTick();
+ await nextTick();
});
it('emits create event', async () => {
wrapper.findAll(GlButton).at(0).vm.$emit('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.emitted('createClicked')).toEqual([['production']]);
});
});
diff --git a/spec/frontend/feature_flags/components/feature_flags_spec.js b/spec/frontend/feature_flags/components/feature_flags_spec.js
index db4bdc736de..d27b23c5cd1 100644
--- a/spec/frontend/feature_flags/components/feature_flags_spec.js
+++ b/spec/frontend/feature_flags/components/feature_flags_spec.js
@@ -1,5 +1,6 @@
import { GlAlert, GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
-import { mount, createLocalVue } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import MockAdapter from 'axios-mock-adapter';
import Vuex from 'vuex';
import waitForPromises from 'helpers/wait_for_promises';
@@ -13,8 +14,7 @@ import axios from '~/lib/utils/axios_utils';
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
import { getRequestData } from '../mock_data';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('Feature flags', () => {
const mockData = {
@@ -45,7 +45,6 @@ describe('Feature flags', () => {
const factory = (provide = mockData, fn = mount) => {
store = createStore(mockState);
wrapper = fn(FeatureFlagsComponent, {
- localVue,
store,
provide,
stubs: {
@@ -72,12 +71,12 @@ describe('Feature flags', () => {
describe('when limit exceeded', () => {
const provideData = { ...mockData, featureFlagsLimitExceeded: true };
- beforeEach((done) => {
+ beforeEach(() => {
mock
.onGet(`${TEST_HOST}/endpoint.json`, { params: { page: '1' } })
.reply(200, getRequestData, {});
factory(provideData);
- setImmediate(done);
+ return waitForPromises();
});
it('makes the new feature flag button do nothing if clicked', () => {
@@ -117,12 +116,12 @@ describe('Feature flags', () => {
userListPath: null,
};
- beforeEach((done) => {
+ beforeEach(() => {
mock
.onGet(`${TEST_HOST}/endpoint.json`, { params: { page: '1' } })
.reply(200, getRequestData, {});
factory(provideData);
- setImmediate(done);
+ return waitForPromises();
});
it('does not render configure button', () => {
@@ -173,7 +172,7 @@ describe('Feature flags', () => {
factory();
await waitForPromises();
- await wrapper.vm.$nextTick();
+ await nextTick();
emptyState = wrapper.findComponent(GlEmptyState);
});
@@ -203,7 +202,7 @@ describe('Feature flags', () => {
});
describe('with paginated feature flags', () => {
- beforeEach((done) => {
+ beforeEach(() => {
mock.onGet(mockState.endpoint, { params: { page: '1' } }).replyOnce(200, getRequestData, {
'x-next-page': '2',
'x-page': '1',
@@ -215,7 +214,7 @@ describe('Feature flags', () => {
factory();
jest.spyOn(store, 'dispatch');
- setImmediate(done);
+ return waitForPromises();
});
it('should render a table with feature flags', () => {
@@ -271,11 +270,11 @@ describe('Feature flags', () => {
});
describe('unsuccessful request', () => {
- beforeEach((done) => {
+ beforeEach(() => {
mock.onGet(mockState.endpoint, { params: { page: '1' } }).replyOnce(500, {});
factory();
- setImmediate(done);
+ return waitForPromises();
});
it('should render error state', () => {
@@ -301,12 +300,12 @@ describe('Feature flags', () => {
});
describe('rotate instance id', () => {
- beforeEach((done) => {
+ beforeEach(() => {
mock
.onGet(`${TEST_HOST}/endpoint.json`, { params: { page: '1' } })
.reply(200, getRequestData, {});
factory();
- setImmediate(done);
+ return waitForPromises();
});
it('should fire the rotate action when a `token` event is received', () => {
diff --git a/spec/frontend/feature_flags/components/feature_flags_table_spec.js b/spec/frontend/feature_flags/components/feature_flags_table_spec.js
index d06d60ae310..99864a95f59 100644
--- a/spec/frontend/feature_flags/components/feature_flags_table_spec.js
+++ b/spec/frontend/feature_flags/components/feature_flags_table_spec.js
@@ -1,5 +1,6 @@
import { GlToggle, GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import { trimText } from 'helpers/text_helper';
import { mockTracking } from 'helpers/tracking_helper';
import FeatureFlagsTable from '~/feature_flags/components/feature_flags_table.vue';
@@ -148,13 +149,12 @@ describe('Feature flag table', () => {
});
});
- it('should trigger a toggle event', () => {
+ it('should trigger a toggle event', async () => {
toggle.vm.$emit('change');
const flag = { ...props.featureFlags[0], active: !props.featureFlags[0].active };
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted('toggle-flag')).toEqual([[flag]]);
- });
+ await nextTick();
+ expect(wrapper.emitted('toggle-flag')).toEqual([[flag]]);
});
it('tracks a click', () => {
diff --git a/spec/frontend/feature_flags/components/form_spec.js b/spec/frontend/feature_flags/components/form_spec.js
index c0f9638390a..3ad1225906b 100644
--- a/spec/frontend/feature_flags/components/form_spec.js
+++ b/spec/frontend/feature_flags/components/form_spec.js
@@ -1,5 +1,6 @@
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import Api from '~/api';
import Form from '~/feature_flags/components/form.vue';
@@ -126,28 +127,26 @@ describe('feature flag form', () => {
expect(wrapper.findAll(Strategy)).toHaveLength(2);
});
- it('adds an all users strategy when clicking the Add button', () => {
+ it('adds an all users strategy when clicking the Add button', async () => {
wrapper.find(GlButton).vm.$emit('click');
- return wrapper.vm.$nextTick().then(() => {
- const strategies = wrapper.findAll(Strategy);
+ await nextTick();
+ const strategies = wrapper.findAll(Strategy);
- expect(strategies).toHaveLength(3);
- expect(strategies.at(2).props('strategy')).toEqual(allUsersStrategy);
- });
+ expect(strategies).toHaveLength(3);
+ expect(strategies.at(2).props('strategy')).toEqual(allUsersStrategy);
});
- it('should remove a strategy on delete', () => {
+ it('should remove a strategy on delete', async () => {
const strategy = {
type: ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
parameters: { percentage: '30' },
scopes: [],
};
wrapper.find(Strategy).vm.$emit('delete');
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.findAll(Strategy)).toHaveLength(1);
- expect(wrapper.find(Strategy).props('strategy')).not.toEqual(strategy);
- });
+ await nextTick();
+ expect(wrapper.findAll(Strategy)).toHaveLength(1);
+ expect(wrapper.find(Strategy).props('strategy')).not.toEqual(strategy);
});
});
});
diff --git a/spec/frontend/feature_flags/components/new_environments_dropdown_spec.js b/spec/frontend/feature_flags/components/new_environments_dropdown_spec.js
index 6342ac0bda7..63fa5d19982 100644
--- a/spec/frontend/feature_flags/components/new_environments_dropdown_spec.js
+++ b/spec/frontend/feature_flags/components/new_environments_dropdown_spec.js
@@ -1,6 +1,7 @@
import { GlLoadingIcon, GlSearchBoxByType, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+import { nextTick } from 'vue';
import NewEnvironmentsDropdown from '~/feature_flags/components/new_environments_dropdown.vue';
import axios from '~/lib/utils/axios_utils';
import httpStatusCodes from '~/lib/utils/http_status';
@@ -47,16 +48,13 @@ describe('New Environments Dropdown', () => {
describe('with empty results', () => {
let item;
- beforeEach(() => {
+ beforeEach(async () => {
axiosMock.onGet(TEST_HOST).reply(200, []);
wrapper.find(GlSearchBoxByType).vm.$emit('focus');
wrapper.find(GlSearchBoxByType).vm.$emit('input', TEST_SEARCH);
- return axios
- .waitForAll()
- .then(() => wrapper.vm.$nextTick())
- .then(() => {
- item = wrapper.find(GlDropdownItem);
- });
+ await axios.waitForAll();
+ await nextTick();
+ item = wrapper.find(GlDropdownItem);
});
it('should display a Create item label', () => {
diff --git a/spec/frontend/feature_flags/components/new_feature_flag_spec.js b/spec/frontend/feature_flags/components/new_feature_flag_spec.js
index fe98b6421d4..9c1657bc0d2 100644
--- a/spec/frontend/feature_flags/components/new_feature_flag_spec.js
+++ b/spec/frontend/feature_flags/components/new_feature_flag_spec.js
@@ -1,5 +1,6 @@
import { GlAlert } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { TEST_HOST } from 'spec/test_constants';
import Form from '~/feature_flags/components/form.vue';
@@ -10,8 +11,7 @@ import { allUsersStrategy } from '../mock_data';
const userCalloutId = 'feature_flags_new_version';
const userCalloutsPath = `${TEST_HOST}/user_callouts`;
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('New feature flag form', () => {
let wrapper;
@@ -27,7 +27,6 @@ describe('New feature flag form', () => {
wrapper = null;
}
wrapper = shallowMount(NewFeatureFlag, {
- localVue,
store,
provide: {
showUserCallout: true,
@@ -52,13 +51,12 @@ describe('New feature flag form', () => {
});
describe('with error', () => {
- it('should render the error', () => {
+ it('should render the error', async () => {
store.dispatch('receiveCreateFeatureFlagError', { message: ['The name is required'] });
- return wrapper.vm.$nextTick(() => {
- const warningGlAlert = findWarningGlAlert();
- expect(warningGlAlert.at(0).exists()).toBe(true);
- expect(warningGlAlert.at(0).text()).toContain('The name is required');
- });
+ await nextTick();
+ const warningGlAlert = findWarningGlAlert();
+ expect(warningGlAlert.at(0).exists()).toBe(true);
+ expect(warningGlAlert.at(0).text()).toContain('The name is required');
});
});
diff --git a/spec/frontend/feature_flags/components/strategies/flexible_rollout_spec.js b/spec/frontend/feature_flags/components/strategies/flexible_rollout_spec.js
index 07aa456e69e..56b14d80ab3 100644
--- a/spec/frontend/feature_flags/components/strategies/flexible_rollout_spec.js
+++ b/spec/frontend/feature_flags/components/strategies/flexible_rollout_spec.js
@@ -1,5 +1,6 @@
import { GlFormInput, GlFormSelect } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import FlexibleRollout from '~/feature_flags/components/strategies/flexible_rollout.vue';
import ParameterFormGroup from '~/feature_flags/components/strategies/parameter_form_group.vue';
import { PERCENT_ROLLOUT_GROUP_ID } from '~/feature_flags/constants';
@@ -51,7 +52,7 @@ describe('feature_flags/components/strategies/flexible_rollout.vue', () => {
it('emits a change when the percentage value changes', async () => {
percentageInput.setValue('75');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.emitted('change')).toEqual([
[
{
diff --git a/spec/frontend/feature_flags/components/strategies/gitlab_user_list_spec.js b/spec/frontend/feature_flags/components/strategies/gitlab_user_list_spec.js
index 6188672b23b..3b69194494f 100644
--- a/spec/frontend/feature_flags/components/strategies/gitlab_user_list_spec.js
+++ b/spec/frontend/feature_flags/components/strategies/gitlab_user_list_spec.js
@@ -1,5 +1,6 @@
import { GlDropdown, GlDropdownItem, GlSearchBoxByType, GlLoadingIcon } from '@gitlab/ui';
-import { mount, createLocalVue } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import Api from '~/api';
import GitlabUserList from '~/feature_flags/components/strategies/gitlab_user_list.vue';
@@ -12,15 +13,13 @@ const DEFAULT_PROPS = {
strategy: userListStrategy,
};
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('~/feature_flags/components/strategies/gitlab_user_list.vue', () => {
let wrapper;
const factory = (props = {}) =>
mount(GitlabUserList, {
- localVue,
store: createStore({ projectId: '1' }),
propsData: { ...DEFAULT_PROPS, ...props },
});
@@ -72,7 +71,7 @@ describe('~/feature_flags/components/strategies/gitlab_user_list.vue', () => {
);
const searchWrapper = wrapper.find(GlSearchBoxByType);
searchWrapper.vm.$emit('input', 'new');
- await wrapper.vm.$nextTick();
+ await nextTick();
const loadingIcon = wrapper.find(GlLoadingIcon);
expect(loadingIcon.exists()).toBe(true);
@@ -80,7 +79,7 @@ describe('~/feature_flags/components/strategies/gitlab_user_list.vue', () => {
r({ data: [userList] });
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(loadingIcon.exists()).toBe(false);
});
diff --git a/spec/frontend/feature_flags/components/strategies/percent_rollout_spec.js b/spec/frontend/feature_flags/components/strategies/percent_rollout_spec.js
index 442f7faf161..180697e93e4 100644
--- a/spec/frontend/feature_flags/components/strategies/percent_rollout_spec.js
+++ b/spec/frontend/feature_flags/components/strategies/percent_rollout_spec.js
@@ -1,5 +1,6 @@
import { GlFormInput } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import ParameterFormGroup from '~/feature_flags/components/strategies/parameter_form_group.vue';
import PercentRollout from '~/feature_flags/components/strategies/percent_rollout.vue';
import { PERCENT_ROLLOUT_GROUP_ID } from '~/feature_flags/constants';
@@ -39,7 +40,7 @@ describe('~/feature_flags/components/strategies/percent_rollout.vue', () => {
it('emits a change when the value changes', async () => {
input.setValue('75');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.emitted('change')).toEqual([
[{ parameters: { percentage: '75', groupId: PERCENT_ROLLOUT_GROUP_ID } }],
]);
diff --git a/spec/frontend/feature_flags/components/strategy_spec.js b/spec/frontend/feature_flags/components/strategy_spec.js
index 4fdf436bfc4..aee3873721c 100644
--- a/spec/frontend/feature_flags/components/strategy_spec.js
+++ b/spec/frontend/feature_flags/components/strategy_spec.js
@@ -1,5 +1,6 @@
import { GlAlert, GlFormSelect, GlLink, GlToken, GlButton } from '@gitlab/ui';
-import { mount, createLocalVue } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import { last } from 'lodash';
import Vuex from 'vuex';
import Api from '~/api';
@@ -26,8 +27,7 @@ const provide = {
environmentsEndpoint: '',
};
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('Feature flags strategy', () => {
let wrapper;
@@ -48,7 +48,7 @@ describe('Feature flags strategy', () => {
wrapper.destroy();
wrapper = null;
}
- wrapper = mount(Strategy, { localVue, store: createStore({ projectId: '1' }), ...opts });
+ wrapper = mount(Strategy, { store: createStore({ projectId: '1' }), ...opts });
};
beforeEach(() => {
@@ -85,11 +85,11 @@ describe('Feature flags strategy', () => {
let propsData;
let strategy;
- beforeEach(() => {
+ beforeEach(async () => {
strategy = { name, parameters: {}, scopes: [] };
propsData = { strategy, index: 0 };
factory({ propsData, provide });
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('should set the select to match the strategy name', () => {
@@ -138,19 +138,18 @@ describe('Feature flags strategy', () => {
factory({ propsData, provide });
});
- it('should revert to all-environments scope when last scope is removed', () => {
+ it('should revert to all-environments scope when last scope is removed', async () => {
const token = wrapper.find(GlToken);
token.vm.$emit('close');
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.findAll(GlToken)).toHaveLength(0);
- expect(last(wrapper.emitted('change'))).toEqual([
- {
- name: ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
- parameters: { percentage: '50', groupId: PERCENT_ROLLOUT_GROUP_ID },
- scopes: [{ environmentScope: '*' }],
- },
- ]);
- });
+ await nextTick();
+ expect(wrapper.findAll(GlToken)).toHaveLength(0);
+ expect(last(wrapper.emitted('change'))).toEqual([
+ {
+ name: ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
+ parameters: { percentage: '50', groupId: PERCENT_ROLLOUT_GROUP_ID },
+ scopes: [{ environmentScope: '*' }],
+ },
+ ]);
});
});
@@ -167,56 +166,52 @@ describe('Feature flags strategy', () => {
factory({ propsData, provide });
});
- it('should change the parameters if a different strategy is chosen', () => {
+ it('should change the parameters if a different strategy is chosen', async () => {
const select = wrapper.find(GlFormSelect);
select.setValue(ROLLOUT_STRATEGY_ALL_USERS);
- return wrapper.vm.$nextTick().then(() => {
- expect(last(wrapper.emitted('change'))).toEqual([
- {
- name: ROLLOUT_STRATEGY_ALL_USERS,
- parameters: {},
- scopes: [{ environmentScope: '*' }],
- },
- ]);
- });
+ await nextTick();
+ expect(last(wrapper.emitted('change'))).toEqual([
+ {
+ name: ROLLOUT_STRATEGY_ALL_USERS,
+ parameters: {},
+ scopes: [{ environmentScope: '*' }],
+ },
+ ]);
});
- it('should display selected scopes', () => {
+ it('should display selected scopes', async () => {
const dropdown = wrapper.find(NewEnvironmentsDropdown);
dropdown.vm.$emit('add', 'production');
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.findAll(GlToken)).toHaveLength(1);
- expect(wrapper.find(GlToken).text()).toBe('production');
- });
+ await nextTick();
+ expect(wrapper.findAll(GlToken)).toHaveLength(1);
+ expect(wrapper.find(GlToken).text()).toBe('production');
});
- it('should display all selected scopes', () => {
+ it('should display all selected scopes', async () => {
const dropdown = wrapper.find(NewEnvironmentsDropdown);
dropdown.vm.$emit('add', 'production');
dropdown.vm.$emit('add', 'staging');
- return wrapper.vm.$nextTick().then(() => {
- const tokens = wrapper.findAll(GlToken);
- expect(tokens).toHaveLength(2);
- expect(tokens.at(0).text()).toBe('production');
- expect(tokens.at(1).text()).toBe('staging');
- });
+ await nextTick();
+ const tokens = wrapper.findAll(GlToken);
+ expect(tokens).toHaveLength(2);
+ expect(tokens.at(0).text()).toBe('production');
+ expect(tokens.at(1).text()).toBe('staging');
});
- it('should emit selected scopes', () => {
+ it('should emit selected scopes', async () => {
const dropdown = wrapper.find(NewEnvironmentsDropdown);
dropdown.vm.$emit('add', 'production');
- return wrapper.vm.$nextTick().then(() => {
- expect(last(wrapper.emitted('change'))).toEqual([
- {
- name: ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
- parameters: { percentage: '50', groupId: PERCENT_ROLLOUT_GROUP_ID },
- scopes: [
- { environmentScope: '*', shouldBeDestroyed: true },
- { environmentScope: 'production' },
- ],
- },
- ]);
- });
+ await nextTick();
+ expect(last(wrapper.emitted('change'))).toEqual([
+ {
+ name: ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
+ parameters: { percentage: '50', groupId: PERCENT_ROLLOUT_GROUP_ID },
+ scopes: [
+ { environmentScope: '*', shouldBeDestroyed: true },
+ { environmentScope: 'production' },
+ ],
+ },
+ ]);
});
it('should emit a delete if the delete button is clicked', () => {
@@ -236,39 +231,36 @@ describe('Feature flags strategy', () => {
factory({ propsData, provide });
});
- it('should display selected scopes', () => {
+ it('should display selected scopes', async () => {
const dropdown = wrapper.find(NewEnvironmentsDropdown);
dropdown.vm.$emit('add', 'production');
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.findAll(GlToken)).toHaveLength(1);
- expect(wrapper.find(GlToken).text()).toBe('production');
- });
+ await nextTick();
+ expect(wrapper.findAll(GlToken)).toHaveLength(1);
+ expect(wrapper.find(GlToken).text()).toBe('production');
});
- it('should display all selected scopes', () => {
+ it('should display all selected scopes', async () => {
const dropdown = wrapper.find(NewEnvironmentsDropdown);
dropdown.vm.$emit('add', 'production');
dropdown.vm.$emit('add', 'staging');
- return wrapper.vm.$nextTick().then(() => {
- const tokens = wrapper.findAll(GlToken);
- expect(tokens).toHaveLength(2);
- expect(tokens.at(0).text()).toBe('production');
- expect(tokens.at(1).text()).toBe('staging');
- });
+ await nextTick();
+ const tokens = wrapper.findAll(GlToken);
+ expect(tokens).toHaveLength(2);
+ expect(tokens.at(0).text()).toBe('production');
+ expect(tokens.at(1).text()).toBe('staging');
});
- it('should emit selected scopes', () => {
+ it('should emit selected scopes', async () => {
const dropdown = wrapper.find(NewEnvironmentsDropdown);
dropdown.vm.$emit('add', 'production');
- return wrapper.vm.$nextTick().then(() => {
- expect(last(wrapper.emitted('change'))).toEqual([
- {
- name: ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
- parameters: { percentage: '50', groupId: PERCENT_ROLLOUT_GROUP_ID },
- scopes: [{ environmentScope: 'production' }],
- },
- ]);
- });
+ await nextTick();
+ expect(last(wrapper.emitted('change'))).toEqual([
+ {
+ name: ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
+ parameters: { percentage: '50', groupId: PERCENT_ROLLOUT_GROUP_ID },
+ scopes: [{ environmentScope: 'production' }],
+ },
+ ]);
});
});
});
diff --git a/spec/frontend/feature_highlight/feature_highlight_popover_spec.js b/spec/frontend/feature_highlight/feature_highlight_popover_spec.js
index e5e3974e103..650f9eb1bbc 100644
--- a/spec/frontend/feature_highlight/feature_highlight_popover_spec.js
+++ b/spec/frontend/feature_highlight/feature_highlight_popover_spec.js
@@ -1,5 +1,6 @@
import { GlPopover, GlLink, GlButton } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import { POPOVER_TARGET_ID } from '~/feature_highlight/constants';
import { dismiss } from '~/feature_highlight/feature_highlight_helper';
import FeatureHighlightPopover from '~/feature_highlight/feature_highlight_popover.vue';
@@ -71,7 +72,7 @@ describe('feature_highlight/feature_highlight_popover', () => {
it('hides the popover target', async () => {
await findDismissButton().trigger('click');
findPopover().vm.$emit('hidden');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findPopoverTarget().exists()).toBe(false);
});
diff --git a/spec/frontend/filtered_search/dropdown_user_spec.js b/spec/frontend/filtered_search/dropdown_user_spec.js
index 9a20fb1bae6..ee0eef6a1b6 100644
--- a/spec/frontend/filtered_search/dropdown_user_spec.js
+++ b/spec/frontend/filtered_search/dropdown_user_spec.js
@@ -74,7 +74,7 @@ describe('Dropdown User', () => {
});
describe('hideCurrentUser', () => {
- const fixtureTemplate = 'issues/issue_list.html';
+ const fixtureTemplate = 'merge_requests/merge_request_list.html';
let dropdown;
let authorFilterDropdownElement;
diff --git a/spec/frontend/filtered_search/dropdown_utils_spec.js b/spec/frontend/filtered_search/dropdown_utils_spec.js
index 49e14f58630..4c1e79eba42 100644
--- a/spec/frontend/filtered_search/dropdown_utils_spec.js
+++ b/spec/frontend/filtered_search/dropdown_utils_spec.js
@@ -4,7 +4,7 @@ import FilteredSearchDropdownManager from '~/filtered_search/filtered_search_dro
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
describe('Dropdown Utils', () => {
- const issueListFixture = 'issues/issue_list.html';
+ const issuableListFixture = 'merge_requests/merge_request_list.html';
describe('getEscapedText', () => {
it('should return same word when it has no space', () => {
@@ -350,7 +350,7 @@ describe('Dropdown Utils', () => {
let authorToken;
beforeEach(() => {
- loadFixtures(issueListFixture);
+ loadFixtures(issuableListFixture);
authorToken = FilteredSearchSpecHelper.createFilterVisualToken('author', '=', '@user');
const searchTermToken = FilteredSearchSpecHelper.createSearchVisualToken('search term');
diff --git a/spec/frontend/filtered_search/filtered_search_visual_tokens_spec.js b/spec/frontend/filtered_search/filtered_search_visual_tokens_spec.js
index 44f67f269a2..c4e125e96da 100644
--- a/spec/frontend/filtered_search/filtered_search_visual_tokens_spec.js
+++ b/spec/frontend/filtered_search/filtered_search_visual_tokens_spec.js
@@ -1,6 +1,7 @@
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import FilteredSearchSpecHelper from 'helpers/filtered_search_spec_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import FilteredSearchVisualTokens from '~/filtered_search/filtered_search_visual_tokens';
describe('Filtered Search Visual Tokens', () => {
@@ -715,18 +716,16 @@ describe('Filtered Search Visual Tokens', () => {
`);
});
- it('renders a author token value element', () => {
+ it('renders a author token value element', async () => {
const { tokenNameElement, tokenValueElement } = findElements(authorToken);
const tokenName = tokenNameElement.textContent;
const tokenValue = 'new value';
subject.renderVisualTokenValue(authorToken, tokenName, tokenValue);
- jest.runOnlyPendingTimers();
+ await waitForPromises();
- setImmediate(() => {
- expect(tokenValueElement.textContent).toBe(tokenValue);
- });
+ expect(tokenValueElement.textContent).toBe(tokenValue);
});
});
});
diff --git a/spec/frontend/filtered_search/recent_searches_root_spec.js b/spec/frontend/filtered_search/recent_searches_root_spec.js
index fa3267c98a1..59c428fb3fa 100644
--- a/spec/frontend/filtered_search/recent_searches_root_spec.js
+++ b/spec/frontend/filtered_search/recent_searches_root_spec.js
@@ -1,3 +1,4 @@
+import { nextTick } from 'vue';
import { setHTMLFixture } from 'helpers/fixtures';
import RecentSearchesRoot from '~/filtered_search/recent_searches_root';
@@ -10,7 +11,7 @@ describe('RecentSearchesRoot', () => {
let vm;
let containerEl;
- beforeEach(() => {
+ beforeEach(async () => {
setHTMLFixture(`
<div id="${containerId}">
<div id="${dropdownElementId}"></div>
@@ -33,7 +34,7 @@ describe('RecentSearchesRoot', () => {
RecentSearchesRoot.prototype.render.call(recentSearchesRootMockInstance);
vm = recentSearchesRootMockInstance.vm;
- return vm.$nextTick();
+ await nextTick();
});
afterEach(() => {
diff --git a/spec/frontend/fixtures/application_settings.rb b/spec/frontend/fixtures/application_settings.rb
index 9fa8d68e695..a7a989f31ec 100644
--- a/spec/frontend/fixtures/application_settings.rb
+++ b/spec/frontend/fixtures/application_settings.rb
@@ -13,6 +13,7 @@ RSpec.describe Admin::ApplicationSettingsController, '(JavaScript fixtures)', ty
before do
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
+ allow(Gitlab::Metrics).to receive(:metrics_folder_present?).and_return(true)
sign_in(admin)
enable_admin_mode!(admin)
end
diff --git a/spec/frontend/fixtures/blob.rb b/spec/frontend/fixtures/blob.rb
index 35a7ff4eb07..af548823886 100644
--- a/spec/frontend/fixtures/blob.rb
+++ b/spec/frontend/fixtures/blob.rb
@@ -7,7 +7,7 @@ RSpec.describe Projects::BlobController, '(JavaScript fixtures)', type: :control
let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
let(:project) { create(:project, :repository, namespace: namespace, path: 'branches-project') }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
render_views
diff --git a/spec/frontend/fixtures/branches.rb b/spec/frontend/fixtures/branches.rb
index 828564977e0..b3bb4b8873a 100644
--- a/spec/frontend/fixtures/branches.rb
+++ b/spec/frontend/fixtures/branches.rb
@@ -7,7 +7,7 @@ RSpec.describe 'Branches (JavaScript fixtures)' do
let_it_be(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
let_it_be(:project) { create(:project, :repository, namespace: namespace, path: 'branches-project') }
- let_it_be(:user) { project.owner }
+ let_it_be(:user) { project.first_owner }
after(:all) do
remove_repository(project)
diff --git a/spec/frontend/fixtures/clusters.rb b/spec/frontend/fixtures/clusters.rb
index ea883555255..49596d98774 100644
--- a/spec/frontend/fixtures/clusters.rb
+++ b/spec/frontend/fixtures/clusters.rb
@@ -8,7 +8,7 @@ RSpec.describe Projects::ClustersController, '(JavaScript fixtures)', type: :con
let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
let(:project) { create(:project, :repository, namespace: namespace) }
let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
render_views
diff --git a/spec/frontend/fixtures/commit.rb b/spec/frontend/fixtures/commit.rb
index f9e0f604b52..815c140950d 100644
--- a/spec/frontend/fixtures/commit.rb
+++ b/spec/frontend/fixtures/commit.rb
@@ -6,7 +6,7 @@ RSpec.describe 'Commit (JavaScript fixtures)' do
include JavaScriptFixturesHelpers
let_it_be(:project) { create(:project, :repository) }
- let_it_be(:user) { project.owner }
+ let_it_be(:user) { project.first_owner }
let_it_be(:commit) { project.commit("master") }
before do
diff --git a/spec/frontend/fixtures/freeze_period.rb b/spec/frontend/fixtures/freeze_period.rb
index d9573c8000d..dd16bd81b51 100644
--- a/spec/frontend/fixtures/freeze_period.rb
+++ b/spec/frontend/fixtures/freeze_period.rb
@@ -7,7 +7,7 @@ RSpec.describe 'Freeze Periods (JavaScript fixtures)' do
include TimeZoneHelper
let_it_be(:project) { create(:project, :repository, path: 'freeze-periods-project') }
- let_it_be(:user) { project.owner }
+ let_it_be(:user) { project.first_owner }
after(:all) do
remove_repository(project)
diff --git a/spec/frontend/fixtures/issues.rb b/spec/frontend/fixtures/issues.rb
index 6519416cb9e..8bedb802242 100644
--- a/spec/frontend/fixtures/issues.rb
+++ b/spec/frontend/fixtures/issues.rb
@@ -37,17 +37,6 @@ RSpec.describe Projects::IssuesController, '(JavaScript fixtures)', type: :contr
render_issue(create(:closed_issue, project: project))
end
- it 'issues/issue_list.html' do
- create(:issue, project: project)
-
- get :index, params: {
- namespace_id: project.namespace.to_param,
- project_id: project
- }
-
- expect(response).to be_successful
- end
-
private
def render_issue(issue)
diff --git a/spec/frontend/fixtures/jobs.rb b/spec/frontend/fixtures/jobs.rb
index 12584f38629..3cc87432655 100644
--- a/spec/frontend/fixtures/jobs.rb
+++ b/spec/frontend/fixtures/jobs.rb
@@ -7,7 +7,7 @@ RSpec.describe Projects::JobsController, '(JavaScript fixtures)', type: :control
let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
let(:project) { create(:project, :repository, namespace: namespace, path: 'builds-project') }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.id) }
let!(:build_with_artifacts) { create(:ci_build, :success, :artifacts, :trace_artifact, pipeline: pipeline, stage: 'test', artifacts_expire_at: Time.now + 18.months) }
let!(:failed_build) { create(:ci_build, :failed, pipeline: pipeline, stage: 'build') }
diff --git a/spec/frontend/fixtures/listbox.rb b/spec/frontend/fixtures/listbox.rb
new file mode 100644
index 00000000000..8f8489a2827
--- /dev/null
+++ b/spec/frontend/fixtures/listbox.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'initRedirectListboxBehavior', '(JavaScript fixtures)', type: :helper do
+ include JavaScriptFixturesHelpers
+ include ListboxHelper
+
+ let(:response) { @tag }
+
+ it 'listbox/redirect_listbox.html' do
+ items = [{
+ value: 'foo',
+ text: 'Foo',
+ href: '/foo',
+ arbitrary_key: 'foo xyz'
+ }, {
+ value: 'bar',
+ text: 'Bar',
+ href: '/bar',
+ arbitrary_key: 'bar xyz'
+ }, {
+ value: 'qux',
+ text: 'Qux',
+ href: '/qux',
+ arbitrary_key: 'qux xyz'
+ }]
+
+ @tag = helper.gl_redirect_listbox_tag(items, 'bar', class: %w[test-class-1 test-class-2], data: { right: true })
+ end
+end
diff --git a/spec/frontend/fixtures/merge_requests.rb b/spec/frontend/fixtures/merge_requests.rb
index 68ed2ca2359..1eb48c0ce2c 100644
--- a/spec/frontend/fixtures/merge_requests.rb
+++ b/spec/frontend/fixtures/merge_requests.rb
@@ -7,7 +7,7 @@ RSpec.describe Projects::MergeRequestsController, '(JavaScript fixtures)', type:
let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
let(:project) { create(:project, :repository, namespace: namespace, path: 'merge-requests-project') }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
# rubocop: disable Layout/TrailingWhitespace
let(:description) do
@@ -119,6 +119,17 @@ RSpec.describe Projects::MergeRequestsController, '(JavaScript fixtures)', type:
end
end
+ it 'merge_requests/merge_request_list.html' do
+ create(:merge_request, source_project: project, target_project: project)
+
+ get :index, params: {
+ namespace_id: project.namespace.to_param,
+ project_id: project
+ }
+
+ expect(response).to be_successful
+ end
+
private
def render_discussions_json(merge_request)
diff --git a/spec/frontend/fixtures/merge_requests_diffs.rb b/spec/frontend/fixtures/merge_requests_diffs.rb
index e733764f248..7f0d650b710 100644
--- a/spec/frontend/fixtures/merge_requests_diffs.rb
+++ b/spec/frontend/fixtures/merge_requests_diffs.rb
@@ -7,7 +7,7 @@ RSpec.describe Projects::MergeRequests::DiffsController, '(JavaScript fixtures)'
let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
let(:project) { create(:project, :repository, namespace: namespace, path: 'merge-requests-project') }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:merge_request) { create(:merge_request, source_project: project, target_project: project, description: '- [ ] Task List Item') }
let(:path) { "files/ruby/popen.rb" }
let(:position) do
diff --git a/spec/frontend/fixtures/pipeline_schedules.rb b/spec/frontend/fixtures/pipeline_schedules.rb
index 6389f59aa0a..e155d27920d 100644
--- a/spec/frontend/fixtures/pipeline_schedules.rb
+++ b/spec/frontend/fixtures/pipeline_schedules.rb
@@ -7,7 +7,7 @@ RSpec.describe Projects::PipelineSchedulesController, '(JavaScript fixtures)', t
let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
let(:project) { create(:project, :public, :repository) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let!(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: user) }
let!(:pipeline_schedule_populated) { create(:ci_pipeline_schedule, project: project, owner: user) }
let!(:pipeline_schedule_variable1) { create(:ci_pipeline_schedule_variable, key: 'foo', value: 'foovalue', pipeline_schedule: pipeline_schedule_populated) }
diff --git a/spec/frontend/fixtures/projects.rb b/spec/frontend/fixtures/projects.rb
index 3c8964d398a..fa7d61df3e8 100644
--- a/spec/frontend/fixtures/projects.rb
+++ b/spec/frontend/fixtures/projects.rb
@@ -12,7 +12,7 @@ RSpec.describe 'Projects (JavaScript fixtures)', type: :controller do
let(:project) { create(:project, namespace: namespace, path: 'builds-project', runners_token: runners_token, avatar: fixture_file_upload('spec/fixtures/dk.png', 'image/png')) }
let(:project_with_repo) { create(:project, :repository, description: 'Code and stuff', avatar: fixture_file_upload('spec/fixtures/dk.png', 'image/png')) }
let(:project_variable_populated) { create(:project, namespace: namespace, path: 'builds-project2', runners_token: runners_token) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
render_views
diff --git a/spec/frontend/fixtures/prometheus_service.rb b/spec/frontend/fixtures/prometheus_service.rb
index bbd938c66f6..aed73dc1096 100644
--- a/spec/frontend/fixtures/prometheus_service.rb
+++ b/spec/frontend/fixtures/prometheus_service.rb
@@ -8,7 +8,7 @@ RSpec.describe Projects::ServicesController, '(JavaScript fixtures)', type: :con
let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
let(:project) { create(:project_empty_repo, namespace: namespace, path: 'services-project') }
let!(:integration) { create(:prometheus_integration, project: project) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
render_views
diff --git a/spec/frontend/fixtures/runner.rb b/spec/frontend/fixtures/runner.rb
index 36e6cf72750..cdb4c3fd8ba 100644
--- a/spec/frontend/fixtures/runner.rb
+++ b/spec/frontend/fixtures/runner.rb
@@ -11,11 +11,13 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
let_it_be(:admin) { create(:admin) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :repository, :public) }
+ let_it_be(:project_2) { create(:project, :repository, :public) }
let_it_be(:instance_runner) { create(:ci_runner, :instance, version: '1.0.0', revision: '123', description: 'Instance runner', ip_address: '127.0.0.1') }
let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group], active: false, version: '2.0.0', revision: '456', description: 'Group runner', ip_address: '127.0.0.1') }
let_it_be(:group_runner_2) { create(:ci_runner, :group, groups: [group], active: false, version: '2.0.0', revision: '456', description: 'Group runner 2', ip_address: '127.0.0.1') }
- let_it_be(:project_runner) { create(:ci_runner, :project, projects: [project], active: false, version: '2.0.0', revision: '456', description: 'Project runner', ip_address: '127.0.0.1') }
+ let_it_be(:project_runner) { create(:ci_runner, :project, projects: [project, project_2], active: false, version: '2.0.0', revision: '456', description: 'Project runner', ip_address: '127.0.0.1') }
+ let_it_be(:build) { create(:ci_build, runner: instance_runner) }
query_path = 'runner/graphql/'
fixtures_path = 'graphql/runner/'
@@ -78,6 +80,46 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
expect_graphql_errors_to_be_empty
end
+
+ it "#{fixtures_path}#{get_runner_query_name}.with_group.json" do
+ post_graphql(query, current_user: admin, variables: {
+ id: group_runner.to_global_id.to_s
+ })
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
+
+ describe GraphQL::Query, type: :request do
+ get_runner_projects_query_name = 'get_runner_projects.query.graphql'
+
+ let_it_be(:query) do
+ get_graphql_query_as_string("#{query_path}#{get_runner_projects_query_name}")
+ end
+
+ it "#{fixtures_path}#{get_runner_projects_query_name}.json" do
+ post_graphql(query, current_user: admin, variables: {
+ id: project_runner.to_global_id.to_s
+ })
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
+
+ describe GraphQL::Query, type: :request do
+ get_runner_jobs_query_name = 'get_runner_jobs.query.graphql'
+
+ let_it_be(:query) do
+ get_graphql_query_as_string("#{query_path}#{get_runner_jobs_query_name}")
+ end
+
+ it "#{fixtures_path}#{get_runner_jobs_query_name}.json" do
+ post_graphql(query, current_user: admin, variables: {
+ id: instance_runner.to_global_id.to_s
+ })
+
+ expect_graphql_errors_to_be_empty
+ end
end
end
diff --git a/spec/frontend/fixtures/services.rb b/spec/frontend/fixtures/services.rb
index a8293a080a9..f0bb8fb962f 100644
--- a/spec/frontend/fixtures/services.rb
+++ b/spec/frontend/fixtures/services.rb
@@ -8,7 +8,7 @@ RSpec.describe Projects::ServicesController, '(JavaScript fixtures)', type: :con
let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
let(:project) { create(:project_empty_repo, namespace: namespace, path: 'services-project') }
let!(:service) { create(:custom_issue_tracker_integration, project: project) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
render_views
diff --git a/spec/frontend/fixtures/snippet.rb b/spec/frontend/fixtures/snippet.rb
index 397fb3e7124..f05ff3ee269 100644
--- a/spec/frontend/fixtures/snippet.rb
+++ b/spec/frontend/fixtures/snippet.rb
@@ -7,7 +7,7 @@ RSpec.describe SnippetsController, '(JavaScript fixtures)', type: :controller do
let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
let(:project) { create(:project, :repository, namespace: namespace, path: 'branches-project') }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:snippet) { create(:personal_snippet, :public, title: 'snippet.md', content: '# snippet', file_name: 'snippet.md', author: user) }
render_views
diff --git a/spec/frontend/fixtures/tags.rb b/spec/frontend/fixtures/tags.rb
index 6cfa5f82efe..fdf85844ee2 100644
--- a/spec/frontend/fixtures/tags.rb
+++ b/spec/frontend/fixtures/tags.rb
@@ -6,7 +6,7 @@ RSpec.describe 'Tags (JavaScript fixtures)' do
include JavaScriptFixturesHelpers
let_it_be(:project) { create(:project, :repository, path: 'tags-project') }
- let_it_be(:user) { project.owner }
+ let_it_be(:user) { project.first_owner }
after(:all) do
remove_repository(project)
diff --git a/spec/frontend/fixtures/todos.rb b/spec/frontend/fixtures/todos.rb
index a0573b0b658..7dce09e8f49 100644
--- a/spec/frontend/fixtures/todos.rb
+++ b/spec/frontend/fixtures/todos.rb
@@ -7,7 +7,7 @@ RSpec.describe 'Todos (JavaScript fixtures)' do
let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
let(:project) { create(:project_empty_repo, namespace: namespace, path: 'todos-project') }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:issue_1) { create(:issue, title: 'issue_1', project: project) }
let!(:todo_1) { create(:todo, user: user, project: project, target: issue_1, created_at: 5.hours.ago) }
let(:issue_2) { create(:issue, title: 'issue_2', project: project) }
diff --git a/spec/frontend/flash_spec.js b/spec/frontend/flash_spec.js
index d5451ec2064..942e2c330fa 100644
--- a/spec/frontend/flash_spec.js
+++ b/spec/frontend/flash_spec.js
@@ -517,16 +517,12 @@ describe('Flash', () => {
`;
});
- it('removes global flash on click', (done) => {
+ it('removes global flash on click', () => {
addDismissFlashClickListener(el, false);
el.querySelector('.js-close-icon').click();
- setImmediate(() => {
- expect(document.querySelector('.flash')).toBeNull();
-
- done();
- });
+ expect(document.querySelector('.flash')).toBeNull();
});
});
diff --git a/spec/frontend/frequent_items/components/app_spec.js b/spec/frontend/frequent_items/components/app_spec.js
index a94cb3e2fcc..ba989bf53ab 100644
--- a/spec/frontend/frequent_items/components/app_spec.js
+++ b/spec/frontend/frequent_items/components/app_spec.js
@@ -1,5 +1,5 @@
import MockAdapter from 'axios-mock-adapter';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
@@ -97,7 +97,7 @@ describe('Frequent Items App Component', () => {
triggerDropdownOpen();
store.state[TEST_VUEX_MODULE].isLoadingItems = true;
- await wrapper.vm.$nextTick();
+ await nextTick();
const loading = findLoading();
@@ -119,7 +119,7 @@ describe('Frequent Items App Component', () => {
expect(findFrequentItems().length).toBe(1);
triggerDropdownOpen();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findFrequentItems().length).toBe(expectedResult.length);
expect(findFrequentItemsList().props()).toEqual({
@@ -135,7 +135,7 @@ describe('Frequent Items App Component', () => {
mock.onGet(/\/api\/v4\/projects.json(.*)$/).replyOnce(200, mockSearchedProjects.data);
setSearch('gitlab');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findLoading().exists()).toBe(true);
diff --git a/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js b/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js
index 5a05265afdc..8220ea16342 100644
--- a/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js
+++ b/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js
@@ -1,5 +1,6 @@
import { GlButton } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import { trimText } from 'helpers/text_helper';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
@@ -8,8 +9,7 @@ import { createStore } from '~/frequent_items/store';
import ProjectAvatar from '~/vue_shared/components/project_avatar.vue';
import { mockProject } from '../mock_data';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('FrequentItemsListItemComponent', () => {
let wrapper;
@@ -40,7 +40,6 @@ describe('FrequentItemsListItemComponent', () => {
provide: {
vuexModule: 'frequentProjects',
},
- localVue,
});
};
diff --git a/spec/frontend/frequent_items/components/frequent_items_list_spec.js b/spec/frontend/frequent_items/components/frequent_items_list_spec.js
index c015914c991..beaab1913d0 100644
--- a/spec/frontend/frequent_items/components/frequent_items_list_spec.js
+++ b/spec/frontend/frequent_items/components/frequent_items_list_spec.js
@@ -1,12 +1,12 @@
-import { mount, createLocalVue } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import frequentItemsListComponent from '~/frequent_items/components/frequent_items_list.vue';
import frequentItemsListItemComponent from '~/frequent_items/components/frequent_items_list_item.vue';
import { createStore } from '~/frequent_items/store';
import { mockFrequentProjects } from '../mock_data';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('FrequentItemsListComponent', () => {
let wrapper;
@@ -22,7 +22,6 @@ describe('FrequentItemsListComponent', () => {
matcher: 'lab',
...props,
},
- localVue,
provide: {
vuexModule: 'frequentProjects',
},
@@ -45,7 +44,7 @@ describe('FrequentItemsListComponent', () => {
wrapper.setProps({
items: mockFrequentProjects,
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.vm.isListEmpty).toBe(false);
});
@@ -64,7 +63,7 @@ describe('FrequentItemsListComponent', () => {
wrapper.setProps({
isFetchFailed: false,
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.vm.listEmptyMessage).toBe('Projects you visit often will appear here');
});
@@ -82,7 +81,7 @@ describe('FrequentItemsListComponent', () => {
wrapper.setProps({
isFetchFailed: false,
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.vm.listEmptyMessage).toBe('Sorry, no projects matched your search');
});
@@ -90,25 +89,23 @@ describe('FrequentItemsListComponent', () => {
});
describe('template', () => {
- it('should render component element with list of projects', () => {
+ it('should render component element with list of projects', async () => {
createComponent();
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.classes('frequent-items-list-container')).toBe(true);
- expect(wrapper.findAll({ ref: 'frequentItemsList' })).toHaveLength(1);
- expect(wrapper.findAll(frequentItemsListItemComponent)).toHaveLength(5);
- });
+ await nextTick();
+ expect(wrapper.classes('frequent-items-list-container')).toBe(true);
+ expect(wrapper.findAll({ ref: 'frequentItemsList' })).toHaveLength(1);
+ expect(wrapper.findAll(frequentItemsListItemComponent)).toHaveLength(5);
});
- it('should render component element with empty message', () => {
+ it('should render component element with empty message', async () => {
createComponent({
items: [],
});
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.vm.$el.querySelectorAll('li.section-empty')).toHaveLength(1);
- expect(wrapper.findAll(frequentItemsListItemComponent)).toHaveLength(0);
- });
+ await nextTick();
+ expect(wrapper.vm.$el.querySelectorAll('li.section-empty')).toHaveLength(1);
+ expect(wrapper.findAll(frequentItemsListItemComponent)).toHaveLength(0);
});
});
});
diff --git a/spec/frontend/frequent_items/components/frequent_items_search_input_spec.js b/spec/frontend/frequent_items/components/frequent_items_search_input_spec.js
index c9b7e0f3d13..d0a4cf70f5f 100644
--- a/spec/frontend/frequent_items/components/frequent_items_search_input_spec.js
+++ b/spec/frontend/frequent_items/components/frequent_items_search_input_spec.js
@@ -1,12 +1,12 @@
import { GlSearchBoxByType } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import searchComponent from '~/frequent_items/components/frequent_items_search_input.vue';
import { createStore } from '~/frequent_items/store';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('FrequentItemsSearchInputComponent', () => {
let wrapper;
@@ -18,7 +18,6 @@ describe('FrequentItemsSearchInputComponent', () => {
shallowMount(searchComponent, {
store,
propsData: { namespace },
- localVue,
provide: {
vuexModule: 'frequentProjects',
},
@@ -62,7 +61,7 @@ describe('FrequentItemsSearchInputComponent', () => {
findSearchBoxByType().vm.$emit('input', value);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'type_search_query', {
label: 'projects_dropdown_frequent_items_search_input',
diff --git a/spec/frontend/gl_form_spec.js b/spec/frontend/gl_form_spec.js
index 07487fbb60e..ab5627ce216 100644
--- a/spec/frontend/gl_form_spec.js
+++ b/spec/frontend/gl_form_spec.js
@@ -8,7 +8,7 @@ describe('GLForm', () => {
const testContext = {};
describe('when instantiated', () => {
- beforeEach((done) => {
+ beforeEach(() => {
window.gl = window.gl || {};
testContext.form = $('<form class="gfm-form"><textarea class="js-gfm-input"></form>');
@@ -18,22 +18,11 @@ describe('GLForm', () => {
jest.spyOn($.prototype, 'css').mockImplementation(() => {});
testContext.glForm = new GLForm(testContext.form, false);
-
- setImmediate(() => {
- $.prototype.off.mockClear();
- $.prototype.on.mockClear();
- $.prototype.css.mockClear();
- done();
- });
});
describe('setupAutosize', () => {
- beforeEach((done) => {
+ beforeEach(() => {
testContext.glForm.setupAutosize();
-
- setImmediate(() => {
- done();
- });
});
it('should register an autosize event handler on the textarea', () => {
diff --git a/spec/frontend/google_cloud/components/app_spec.js b/spec/frontend/google_cloud/components/app_spec.js
index 92bc7596f7d..5ddc0ffa50f 100644
--- a/spec/frontend/google_cloud/components/app_spec.js
+++ b/spec/frontend/google_cloud/components/app_spec.js
@@ -24,8 +24,8 @@ const HOME_PROPS = {
serviceAccounts: [{}, {}],
createServiceAccountUrl: '#url-create-service-account',
emptyIllustrationUrl: '#url-empty-illustration',
- deploymentsCloudRunUrl: '#url-deployments-cloud-run',
- deploymentsCloudStorageUrl: '#deploymentsCloudStorageUrl',
+ enableCloudRunUrl: '#url-enable-cloud-run',
+ enableCloudStorageUrl: '#enableCloudStorageUrl',
};
describe('google_cloud App component', () => {
diff --git a/spec/frontend/google_cloud/components/deployments_service_table_spec.js b/spec/frontend/google_cloud/components/deployments_service_table_spec.js
index 76c3bfd00a8..882376547c4 100644
--- a/spec/frontend/google_cloud/components/deployments_service_table_spec.js
+++ b/spec/frontend/google_cloud/components/deployments_service_table_spec.js
@@ -12,8 +12,8 @@ describe('google_cloud DeploymentsServiceTable component', () => {
beforeEach(() => {
const propsData = {
- cloudRunUrl: '#url-deployments-cloud-run',
- cloudStorageUrl: '#url-deployments-cloud-storage',
+ cloudRunUrl: '#url-enable-cloud-run',
+ cloudStorageUrl: '#url-enable-cloud-storage',
};
wrapper = mount(DeploymentsServiceTable, { propsData });
});
@@ -29,12 +29,13 @@ describe('google_cloud DeploymentsServiceTable component', () => {
it('should contain configure cloud run button', () => {
const cloudRunButton = findCloudRunButton();
expect(cloudRunButton.exists()).toBe(true);
- expect(cloudRunButton.props().disabled).toBe(true);
+ expect(cloudRunButton.attributes('href')).toBe('#url-enable-cloud-run');
});
it('should contain configure cloud storage button', () => {
const cloudStorageButton = findCloudStorageButton();
expect(cloudStorageButton.exists()).toBe(true);
expect(cloudStorageButton.props().disabled).toBe(true);
+ expect(cloudStorageButton.attributes('href')).toBe('#url-enable-cloud-storage');
});
});
diff --git a/spec/frontend/google_cloud/components/home_spec.js b/spec/frontend/google_cloud/components/home_spec.js
index 3a009fc88ce..57cf831b19b 100644
--- a/spec/frontend/google_cloud/components/home_spec.js
+++ b/spec/frontend/google_cloud/components/home_spec.js
@@ -20,8 +20,8 @@ describe('google_cloud Home component', () => {
serviceAccounts: [{}, {}],
createServiceAccountUrl: '#url-create-service-account',
emptyIllustrationUrl: '#url-empty-illustration',
- deploymentsCloudRunUrl: '#url-deployments-cloud-run',
- deploymentsCloudStorageUrl: '#deploymentsCloudStorageUrl',
+ enableCloudRunUrl: '#url-enable-cloud-run',
+ enableCloudStorageUrl: '#enableCloudStorageUrl',
};
beforeEach(() => {
diff --git a/spec/frontend/google_cloud/components/service_accounts_form_spec.js b/spec/frontend/google_cloud/components/service_accounts_form_spec.js
index 5394d0cdaef..7262e12c84d 100644
--- a/spec/frontend/google_cloud/components/service_accounts_form_spec.js
+++ b/spec/frontend/google_cloud/components/service_accounts_form_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import { GlButton, GlFormGroup, GlFormSelect } from '@gitlab/ui';
+import { GlButton, GlFormGroup, GlFormSelect, GlFormCheckbox } from '@gitlab/ui';
import ServiceAccountsForm from '~/google_cloud/components/service_accounts_form.vue';
describe('ServiceAccountsForm component', () => {
@@ -9,11 +9,12 @@ describe('ServiceAccountsForm component', () => {
const findAllFormGroups = () => wrapper.findAllComponents(GlFormGroup);
const findAllFormSelects = () => wrapper.findAllComponents(GlFormSelect);
const findAllButtons = () => wrapper.findAllComponents(GlButton);
+ const findCheckbox = () => wrapper.findComponent(GlFormCheckbox);
const propsData = { gcpProjects: [], environments: [], cancelPath: '#cancel-url' };
beforeEach(() => {
- wrapper = shallowMount(ServiceAccountsForm, { propsData });
+ wrapper = shallowMount(ServiceAccountsForm, { propsData, stubs: { GlFormCheckbox } });
});
afterEach(() => {
@@ -35,8 +36,8 @@ describe('ServiceAccountsForm component', () => {
});
it('contains Environments form group', () => {
- const formGorup = findAllFormGroups().at(1);
- expect(formGorup.exists()).toBe(true);
+ const formGroup = findAllFormGroups().at(1);
+ expect(formGroup.exists()).toBe(true);
});
it('contains Environments dropdown', () => {
@@ -56,4 +57,14 @@ describe('ServiceAccountsForm component', () => {
expect(button.text()).toBe(ServiceAccountsForm.i18n.cancelLabel);
expect(button.attributes('href')).toBe('#cancel-url');
});
+
+ it('contains Confirmation checkbox', () => {
+ const checkbox = findCheckbox();
+ expect(checkbox.text()).toBe(ServiceAccountsForm.i18n.checkboxLabel);
+ });
+
+ it('checkbox must be required', () => {
+ const checkbox = findCheckbox();
+ expect(checkbox.attributes('required')).toBe('true');
+ });
});
diff --git a/spec/frontend/google_cloud/components/service_accounts_list_spec.js b/spec/frontend/google_cloud/components/service_accounts_list_spec.js
index cdb3f74051c..f7051c8a53d 100644
--- a/spec/frontend/google_cloud/components/service_accounts_list_spec.js
+++ b/spec/frontend/google_cloud/components/service_accounts_list_spec.js
@@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils';
-import { GlButton, GlEmptyState, GlTable } from '@gitlab/ui';
+import { GlAlert, GlButton, GlEmptyState, GlTable } from '@gitlab/ui';
import ServiceAccountsList from '~/google_cloud/components/service_accounts_list.vue';
describe('ServiceAccounts component', () => {
@@ -28,7 +28,7 @@ describe('ServiceAccounts component', () => {
it('shows the link to create new service accounts', () => {
const button = findButtonInEmptyState();
expect(button.exists()).toBe(true);
- expect(button.text()).toBe('Create service account');
+ expect(button.text()).toBe(ServiceAccountsList.i18n.createServiceAccount);
expect(button.attributes('href')).toBe('#create-url');
});
});
@@ -41,6 +41,7 @@ describe('ServiceAccounts component', () => {
const findTable = () => wrapper.findComponent(GlTable);
const findRows = () => findTable().findAll('tr');
const findButton = () => wrapper.findComponent(GlButton);
+ const findSecretManagerTip = () => wrapper.findComponent(GlAlert);
beforeEach(() => {
const propsData = {
@@ -52,13 +53,11 @@ describe('ServiceAccounts component', () => {
});
it('shows the title', () => {
- expect(findTitle().text()).toBe('Service Accounts');
+ expect(findTitle().text()).toBe(ServiceAccountsList.i18n.serviceAccountsTitle);
});
it('shows the description', () => {
- expect(findDescription().text()).toBe(
- 'Service Accounts keys authorize GitLab to deploy your Google Cloud project',
- );
+ expect(findDescription().text()).toBe(ServiceAccountsList.i18n.serviceAccountsDescription);
});
it('shows the table', () => {
@@ -72,8 +71,14 @@ describe('ServiceAccounts component', () => {
it('shows the link to create new service accounts', () => {
const button = findButton();
expect(button.exists()).toBe(true);
- expect(button.text()).toBe('Create service account');
+ expect(button.text()).toBe(ServiceAccountsList.i18n.createServiceAccount);
expect(button.attributes('href')).toBe('#create-url');
});
+
+ it('must contain secret managers tip', () => {
+ const tip = findSecretManagerTip();
+ const expectedText = ServiceAccountsList.i18n.secretManagersDescription.substr(0, 48);
+ expect(tip.text()).toContain(expectedText);
+ });
});
});
diff --git a/spec/frontend/google_tag_manager/index_spec.js b/spec/frontend/google_tag_manager/index_spec.js
index ff38de28da6..9112b0e17e7 100644
--- a/spec/frontend/google_tag_manager/index_spec.js
+++ b/spec/frontend/google_tag_manager/index_spec.js
@@ -1,4 +1,5 @@
import { merge } from 'lodash';
+import { v4 as uuidv4 } from 'uuid';
import {
trackFreeTrialAccountSubmissions,
trackNewRegistrations,
@@ -8,11 +9,14 @@ import {
trackSaasTrialProject,
trackSaasTrialProjectImport,
trackSaasTrialGetStarted,
+ trackCheckout,
+ trackTransaction,
} from '~/google_tag_manager';
import { setHTMLFixture } from 'helpers/fixtures';
import { logError } from '~/lib/logger';
jest.mock('~/lib/logger');
+jest.mock('uuid');
describe('~/google_tag_manager/index', () => {
let spy;
@@ -209,6 +213,180 @@ describe('~/google_tag_manager/index', () => {
expect(spy).toHaveBeenCalledWith({ event: 'saasTrialSubmit' });
expect(logError).not.toHaveBeenCalled();
});
+
+ describe('when trackCheckout is invoked', () => {
+ it('with selectedPlan: 2c92a00d76f0d5060176f2fb0a5029ff', () => {
+ expect(spy).not.toHaveBeenCalled();
+
+ trackCheckout('2c92a00d76f0d5060176f2fb0a5029ff', 1);
+
+ expect(spy.mock.calls.flatMap((x) => x)).toEqual([
+ { ecommerce: null },
+ {
+ event: 'EECCheckout',
+ ecommerce: {
+ currencyCode: 'USD',
+ checkout: {
+ actionField: { step: 1 },
+ products: [
+ {
+ brand: 'GitLab',
+ category: 'DevOps',
+ id: '0002',
+ name: 'Premium',
+ price: '228',
+ quantity: 1,
+ variant: 'SaaS',
+ },
+ ],
+ },
+ },
+ },
+ ]);
+ });
+
+ it('with selectedPlan: 2c92a0ff76f0d5250176f2f8c86f305a', () => {
+ expect(spy).not.toHaveBeenCalled();
+
+ trackCheckout('2c92a0ff76f0d5250176f2f8c86f305a', 1);
+
+ expect(spy).toHaveBeenCalledTimes(2);
+ expect(spy).toHaveBeenCalledWith({ ecommerce: null });
+ expect(spy).toHaveBeenCalledWith({
+ event: 'EECCheckout',
+ ecommerce: {
+ currencyCode: 'USD',
+ checkout: {
+ actionField: { step: 1 },
+ products: [
+ {
+ brand: 'GitLab',
+ category: 'DevOps',
+ id: '0001',
+ name: 'Ultimate',
+ price: '1188',
+ quantity: 1,
+ variant: 'SaaS',
+ },
+ ],
+ },
+ },
+ });
+ });
+
+ it('with selectedPlan: Something else', () => {
+ expect(spy).not.toHaveBeenCalled();
+
+ trackCheckout('Something else', 1);
+
+ expect(spy).not.toHaveBeenCalled();
+ });
+
+ it('with a different number of users', () => {
+ expect(spy).not.toHaveBeenCalled();
+
+ trackCheckout('2c92a0ff76f0d5250176f2f8c86f305a', 5);
+
+ expect(spy).toHaveBeenCalledTimes(2);
+ expect(spy).toHaveBeenCalledWith({ ecommerce: null });
+ expect(spy).toHaveBeenCalledWith({
+ event: 'EECCheckout',
+ ecommerce: {
+ currencyCode: 'USD',
+ checkout: {
+ actionField: { step: 1 },
+ products: [
+ {
+ brand: 'GitLab',
+ category: 'DevOps',
+ id: '0001',
+ name: 'Ultimate',
+ price: '1188',
+ quantity: 5,
+ variant: 'SaaS',
+ },
+ ],
+ },
+ },
+ });
+ });
+ });
+
+ describe('when trackTransactions is invoked', () => {
+ describe.each([
+ {
+ selectedPlan: '2c92a00d76f0d5060176f2fb0a5029ff',
+ revenue: 228,
+ name: 'Premium',
+ id: '0002',
+ },
+ {
+ selectedPlan: '2c92a0ff76f0d5250176f2f8c86f305a',
+ revenue: 1188,
+ name: 'Ultimate',
+ id: '0001',
+ },
+ ])('with %o', (planObject) => {
+ it('invokes pushes a new event that references the selected plan', () => {
+ const { selectedPlan, revenue, name, id } = planObject;
+
+ expect(spy).not.toHaveBeenCalled();
+ uuidv4.mockImplementationOnce(() => '123');
+
+ const transactionDetails = {
+ paymentOption: 'visa',
+ revenue,
+ tax: 10,
+ selectedPlan,
+ quantity: 1,
+ };
+
+ trackTransaction(transactionDetails);
+
+ expect(spy.mock.calls.flatMap((x) => x)).toEqual([
+ { ecommerce: null },
+ {
+ event: 'EECtransactionSuccess',
+ ecommerce: {
+ currencyCode: 'USD',
+ purchase: {
+ actionField: {
+ id: '123',
+ affiliation: 'GitLab',
+ option: 'visa',
+ revenue: revenue.toString(),
+ tax: '10',
+ },
+ products: [
+ {
+ brand: 'GitLab',
+ category: 'DevOps',
+ id,
+ name,
+ price: revenue.toString(),
+ quantity: 1,
+ variant: 'SaaS',
+ },
+ ],
+ },
+ },
+ },
+ ]);
+ });
+ });
+ });
+
+ describe('when trackTransaction is invoked', () => {
+ describe('with an invalid plan object', () => {
+ it('does not get called', () => {
+ expect(spy).not.toHaveBeenCalled();
+
+ trackTransaction({ selectedPlan: 'notAplan' });
+
+ expect(spy).not.toHaveBeenCalled();
+ });
+ });
+ });
});
describe.each([
diff --git a/spec/frontend/grafana_integration/components/grafana_integration_spec.js b/spec/frontend/grafana_integration/components/grafana_integration_spec.js
index d5338430054..d2111194097 100644
--- a/spec/frontend/grafana_integration/components/grafana_integration_spec.js
+++ b/spec/frontend/grafana_integration/components/grafana_integration_spec.js
@@ -1,5 +1,6 @@
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import { TEST_HOST } from 'helpers/test_constants';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import createFlash from '~/flash';
@@ -93,29 +94,28 @@ describe('grafana integration component', () => {
},
];
- it('submits form on click', () => {
+ it('submits form on click', async () => {
axios.patch.mockResolvedValue();
findSubmitButton(wrapper).trigger('click');
expect(axios.patch).toHaveBeenCalledWith(...endpointRequest);
- return wrapper.vm.$nextTick().then(() => expect(refreshCurrentPage).toHaveBeenCalled());
+ await nextTick();
+ expect(refreshCurrentPage).toHaveBeenCalled();
});
- it('creates flash banner on error', () => {
+ it('creates flash banner on error', async () => {
const message = 'mockErrorMessage';
axios.patch.mockRejectedValue({ response: { data: { message } } });
findSubmitButton().trigger('click');
expect(axios.patch).toHaveBeenCalledWith(...endpointRequest);
- return wrapper.vm
- .$nextTick()
- .then(jest.runAllTicks)
- .then(() =>
- expect(createFlash).toHaveBeenCalledWith({
- message: `There was an error saving your changes. ${message}`,
- }),
- );
+
+ await nextTick();
+ await jest.runAllTicks();
+ expect(createFlash).toHaveBeenCalledWith({
+ message: `There was an error saving your changes. ${message}`,
+ });
});
});
});
diff --git a/spec/frontend/group_settings/components/shared_runners_form_spec.js b/spec/frontend/group_settings/components/shared_runners_form_spec.js
index 617d91178e4..26e9cd39cfd 100644
--- a/spec/frontend/group_settings/components/shared_runners_form_spec.js
+++ b/spec/frontend/group_settings/components/shared_runners_form_spec.js
@@ -1,6 +1,7 @@
import { GlLoadingIcon, GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import MockAxiosAdapter from 'axios-mock-adapter';
+import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
import SharedRunnersForm from '~/group_settings/components/shared_runners_form.vue';
import axios from '~/lib/utils/axios_utils';
@@ -76,7 +77,7 @@ describe('group_settings/components/shared_runners_form', () => {
findEnabledToggle().vm.$emit('change', true);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(isLoadingIconVisible()).toBe(true);
diff --git a/spec/frontend/groups/components/app_spec.js b/spec/frontend/groups/components/app_spec.js
index bc8c6460cf4..848e50c86ba 100644
--- a/spec/frontend/groups/components/app_spec.js
+++ b/spec/frontend/groups/components/app_spec.js
@@ -1,7 +1,7 @@
import { GlModal, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import appComponent from '~/groups/components/app.vue';
@@ -58,7 +58,7 @@ describe('AppComponent', () => {
wrapper = null;
});
- beforeEach(() => {
+ beforeEach(async () => {
mock = new AxiosMockAdapter(axios);
mock.onGet('/dashboard/groups.json').reply(200, mockGroups);
Vue.component('GroupFolder', groupFolderComponent);
@@ -66,7 +66,7 @@ describe('AppComponent', () => {
createShallowComponent();
getGroupsSpy = jest.spyOn(vm.service, 'getGroups');
- return vm.$nextTick();
+ await nextTick();
});
describe('computed', () => {
@@ -280,6 +280,7 @@ describe('AppComponent', () => {
expect(vm.targetParentGroup).toBe(null);
vm.showLeaveGroupModal(group, mockParentGroupItem);
+ expect(vm.isModalVisible).toBe(true);
expect(vm.targetGroup).not.toBe(null);
expect(vm.targetParentGroup).not.toBe(null);
});
@@ -290,6 +291,7 @@ describe('AppComponent', () => {
expect(vm.groupLeaveConfirmationMessage).toBe('');
vm.showLeaveGroupModal(group, mockParentGroupItem);
+ expect(vm.isModalVisible).toBe(true);
expect(vm.groupLeaveConfirmationMessage).toBe(
`Are you sure you want to leave the "${group.fullName}" group?`,
);
@@ -397,66 +399,60 @@ describe('AppComponent', () => {
});
describe('created', () => {
- it('should bind event listeners on eventHub', () => {
+ it('should bind event listeners on eventHub', async () => {
jest.spyOn(eventHub, '$on').mockImplementation(() => {});
createShallowComponent();
- return vm.$nextTick().then(() => {
- expect(eventHub.$on).toHaveBeenCalledWith('fetchPage', expect.any(Function));
- expect(eventHub.$on).toHaveBeenCalledWith('toggleChildren', expect.any(Function));
- expect(eventHub.$on).toHaveBeenCalledWith('showLeaveGroupModal', expect.any(Function));
- expect(eventHub.$on).toHaveBeenCalledWith('updatePagination', expect.any(Function));
- expect(eventHub.$on).toHaveBeenCalledWith('updateGroups', expect.any(Function));
- });
+ await nextTick();
+ expect(eventHub.$on).toHaveBeenCalledWith('fetchPage', expect.any(Function));
+ expect(eventHub.$on).toHaveBeenCalledWith('toggleChildren', expect.any(Function));
+ expect(eventHub.$on).toHaveBeenCalledWith('showLeaveGroupModal', expect.any(Function));
+ expect(eventHub.$on).toHaveBeenCalledWith('updatePagination', expect.any(Function));
+ expect(eventHub.$on).toHaveBeenCalledWith('updateGroups', expect.any(Function));
});
- it('should initialize `searchEmptyMessage` prop with correct string when `hideProjects` is `false`', () => {
+ it('should initialize `searchEmptyMessage` prop with correct string when `hideProjects` is `false`', async () => {
createShallowComponent();
- return vm.$nextTick().then(() => {
- expect(vm.searchEmptyMessage).toBe('No groups or projects matched your search');
- });
+ await nextTick();
+ expect(vm.searchEmptyMessage).toBe('No groups or projects matched your search');
});
- it('should initialize `searchEmptyMessage` prop with correct string when `hideProjects` is `true`', () => {
+ it('should initialize `searchEmptyMessage` prop with correct string when `hideProjects` is `true`', async () => {
createShallowComponent(true);
- return vm.$nextTick().then(() => {
- expect(vm.searchEmptyMessage).toBe('No groups matched your search');
- });
+ await nextTick();
+ expect(vm.searchEmptyMessage).toBe('No groups matched your search');
});
});
describe('beforeDestroy', () => {
- it('should unbind event listeners on eventHub', () => {
+ it('should unbind event listeners on eventHub', async () => {
jest.spyOn(eventHub, '$off').mockImplementation(() => {});
createShallowComponent();
wrapper.destroy();
- return vm.$nextTick().then(() => {
- expect(eventHub.$off).toHaveBeenCalledWith('fetchPage', expect.any(Function));
- expect(eventHub.$off).toHaveBeenCalledWith('toggleChildren', expect.any(Function));
- expect(eventHub.$off).toHaveBeenCalledWith('showLeaveGroupModal', expect.any(Function));
- expect(eventHub.$off).toHaveBeenCalledWith('updatePagination', expect.any(Function));
- expect(eventHub.$off).toHaveBeenCalledWith('updateGroups', expect.any(Function));
- });
+ await nextTick();
+ expect(eventHub.$off).toHaveBeenCalledWith('fetchPage', expect.any(Function));
+ expect(eventHub.$off).toHaveBeenCalledWith('toggleChildren', expect.any(Function));
+ expect(eventHub.$off).toHaveBeenCalledWith('showLeaveGroupModal', expect.any(Function));
+ expect(eventHub.$off).toHaveBeenCalledWith('updatePagination', expect.any(Function));
+ expect(eventHub.$off).toHaveBeenCalledWith('updateGroups', expect.any(Function));
});
});
describe('template', () => {
- it('should render loading icon', () => {
+ it('should render loading icon', async () => {
vm.isLoading = true;
- return vm.$nextTick().then(() => {
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
- });
+ await nextTick();
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
- it('should render groups tree', () => {
+ it('should render groups tree', async () => {
vm.store.state.groups = [mockParentGroupItem];
vm.isLoading = false;
- return vm.$nextTick().then(() => {
- expect(vm.$el.querySelector('.groups-list-tree-container')).toBeDefined();
- });
+ await nextTick();
+ expect(vm.$el.querySelector('.groups-list-tree-container')).toBeDefined();
});
it('renders modal confirmation dialog', () => {
diff --git a/spec/frontend/groups/components/group_folder_spec.js b/spec/frontend/groups/components/group_folder_spec.js
index 1d8e10479b6..98b7c2dd6c6 100644
--- a/spec/frontend/groups/components/group_folder_spec.js
+++ b/spec/frontend/groups/components/group_folder_spec.js
@@ -1,4 +1,4 @@
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import groupFolderComponent from '~/groups/components/group_folder.vue';
import groupItemComponent from '~/groups/components/group_item.vue';
@@ -18,13 +18,13 @@ const createComponent = (groups = mockGroups, parentGroup = mockParentGroupItem)
describe('GroupFolderComponent', () => {
let vm;
- beforeEach(() => {
+ beforeEach(async () => {
Vue.component('GroupItem', groupItemComponent);
vm = createComponent();
vm.$mount();
- return Vue.nextTick();
+ await nextTick();
});
afterEach(() => {
diff --git a/spec/frontend/groups/components/groups_spec.js b/spec/frontend/groups/components/groups_spec.js
index 0ec1ef5a49e..590b4fb3d57 100644
--- a/spec/frontend/groups/components/groups_spec.js
+++ b/spec/frontend/groups/components/groups_spec.js
@@ -1,4 +1,4 @@
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import groupFolderComponent from '~/groups/components/group_folder.vue';
@@ -21,13 +21,13 @@ const createComponent = (searchEmpty = false) => {
describe('GroupsComponent', () => {
let vm;
- beforeEach(() => {
+ beforeEach(async () => {
Vue.component('GroupFolder', groupFolderComponent);
Vue.component('GroupItem', groupItemComponent);
vm = createComponent();
- return vm.$nextTick();
+ await nextTick();
});
afterEach(() => {
@@ -52,20 +52,18 @@ describe('GroupsComponent', () => {
});
describe('template', () => {
- it('should render component template correctly', () => {
- return vm.$nextTick().then(() => {
- expect(vm.$el.querySelector('.groups-list-tree-container')).toBeDefined();
- expect(vm.$el.querySelector('.group-list-tree')).toBeDefined();
- expect(vm.$el.querySelector('.gl-pagination')).toBeDefined();
- expect(vm.$el.querySelectorAll('.has-no-search-results').length).toBe(0);
- });
+ it('should render component template correctly', async () => {
+ await nextTick();
+ expect(vm.$el.querySelector('.groups-list-tree-container')).toBeDefined();
+ expect(vm.$el.querySelector('.group-list-tree')).toBeDefined();
+ expect(vm.$el.querySelector('.gl-pagination')).toBeDefined();
+ expect(vm.$el.querySelectorAll('.has-no-search-results').length).toBe(0);
});
- it('should render empty search message when `searchEmpty` is `true`', () => {
+ it('should render empty search message when `searchEmpty` is `true`', async () => {
vm.searchEmpty = true;
- return vm.$nextTick().then(() => {
- expect(vm.$el.querySelector('.has-no-search-results')).toBeDefined();
- });
+ await nextTick();
+ expect(vm.$el.querySelector('.has-no-search-results')).toBeDefined();
});
});
});
diff --git a/spec/frontend/groups/components/invite_members_banner_spec.js b/spec/frontend/groups/components/invite_members_banner_spec.js
index c81edad499c..1924f400861 100644
--- a/spec/frontend/groups/components/invite_members_banner_spec.js
+++ b/spec/frontend/groups/components/invite_members_banner_spec.js
@@ -1,6 +1,7 @@
import { GlBanner } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+import { nextTick } from 'vue';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import InviteMembersBanner from '~/groups/components/invite_members_banner.vue';
import eventHub from '~/invite_members/event_hub';
@@ -75,7 +76,6 @@ describe('InviteMembersBanner', () => {
it('calls openModal through the eventHub', () => {
expect(eventHub.$emit).toHaveBeenCalledWith('openModal', {
- inviteeType: 'members',
source: 'invite_members_banner',
});
});
@@ -140,7 +140,7 @@ describe('InviteMembersBanner', () => {
expect(wrapper.find(GlBanner).exists()).toBe(true);
wrapper.find(GlBanner).vm.$emit('close');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.find(GlBanner).exists()).toBe(false);
});
});
diff --git a/spec/frontend/groups/components/item_actions_spec.js b/spec/frontend/groups/components/item_actions_spec.js
index ffbdf9b1aa6..3ceb038dd3c 100644
--- a/spec/frontend/groups/components/item_actions_spec.js
+++ b/spec/frontend/groups/components/item_actions_spec.js
@@ -1,4 +1,4 @@
-import { shallowMount } from '@vue/test-utils';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ItemActions from '~/groups/components/item_actions.vue';
import eventHub from '~/groups/event_hub';
import { mockParentGroupItem, mockChildren } from '../mock_data';
@@ -13,7 +13,7 @@ describe('ItemActions', () => {
};
const createComponent = (props = {}) => {
- wrapper = shallowMount(ItemActions, {
+ wrapper = shallowMountExtended(ItemActions, {
propsData: { ...defaultProps, ...props },
});
};
@@ -23,8 +23,10 @@ describe('ItemActions', () => {
wrapper = null;
});
- const findEditGroupBtn = () => wrapper.find('[data-testid="edit-group-btn"]');
- const findLeaveGroupBtn = () => wrapper.find('[data-testid="leave-group-btn"]');
+ const findEditGroupBtn = () => wrapper.findByTestId(`edit-group-${mockParentGroupItem.id}-btn`);
+ const findLeaveGroupBtn = () => wrapper.findByTestId(`leave-group-${mockParentGroupItem.id}-btn`);
+ const findRemoveGroupBtn = () =>
+ wrapper.findByTestId(`remove-group-${mockParentGroupItem.id}-btn`);
describe('template', () => {
let group;
@@ -34,6 +36,7 @@ describe('ItemActions', () => {
...mockParentGroupItem,
canEdit: true,
canLeave: true,
+ canRemove: true,
};
createComponent({ group });
});
@@ -41,21 +44,21 @@ describe('ItemActions', () => {
it('renders component template correctly', () => {
createComponent();
- expect(wrapper.classes()).toContain('controls');
+ expect(wrapper.classes()).toContain('gl-display-flex', 'gl-justify-content-end', 'gl-ml-5');
});
- it('renders "Edit group" button with correct attribute values', () => {
+ it('renders "Edit" group button with correct attribute values', () => {
const button = findEditGroupBtn();
expect(button.exists()).toBe(true);
- expect(button.props('icon')).toBe('pencil');
- expect(button.attributes('aria-label')).toBe('Edit group');
+ expect(button.attributes('href')).toBe(mockParentGroupItem.editPath);
});
- it('renders "Leave this group" button with correct attribute values', () => {
- const button = findLeaveGroupBtn();
+ it('renders "Delete" group button with correct attribute values', () => {
+ const button = findRemoveGroupBtn();
expect(button.exists()).toBe(true);
- expect(button.props('icon')).toBe('leave');
- expect(button.attributes('aria-label')).toBe('Leave this group');
+ expect(button.attributes('href')).toBe(
+ `${mockParentGroupItem.editPath}#js-remove-group-form`,
+ );
});
it('emits `showLeaveGroupModal` event in the event hub', () => {
@@ -103,4 +106,15 @@ describe('ItemActions', () => {
expect(findEditGroupBtn().exists()).toBe(false);
});
+
+ it('does not render delete button if group can not be edited', () => {
+ createComponent({
+ group: {
+ ...mockParentGroupItem,
+ canRemove: false,
+ },
+ });
+
+ expect(findRemoveGroupBtn().exists()).toBe(false);
+ });
});
diff --git a/spec/frontend/groups/components/transfer_group_form_spec.js b/spec/frontend/groups/components/transfer_group_form_spec.js
new file mode 100644
index 00000000000..6dc760f4f7c
--- /dev/null
+++ b/spec/frontend/groups/components/transfer_group_form_spec.js
@@ -0,0 +1,131 @@
+import { GlAlert, GlSprintf } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import Component from '~/groups/components/transfer_group_form.vue';
+import ConfirmDanger from '~/vue_shared/components/confirm_danger/confirm_danger.vue';
+import NamespaceSelect from '~/vue_shared/components/namespace_select/namespace_select.vue';
+
+describe('Transfer group form', () => {
+ let wrapper;
+
+ const confirmButtonText = 'confirm';
+ const confirmationPhrase = 'confirmation-phrase';
+ const paidGroupHelpLink = 'some/fake/link';
+ const groupNamespaces = [
+ {
+ id: 1,
+ humanName: 'Group 1',
+ },
+ {
+ id: 2,
+ humanName: 'Group 2',
+ },
+ ];
+
+ const defaultProps = {
+ groupNamespaces,
+ paidGroupHelpLink,
+ isPaidGroup: false,
+ confirmationPhrase,
+ confirmButtonText,
+ };
+
+ const createComponent = (propsData = {}) =>
+ shallowMountExtended(Component, {
+ propsData: {
+ ...defaultProps,
+ ...propsData,
+ },
+ stubs: { GlSprintf },
+ });
+
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findConfirmDanger = () => wrapper.findComponent(ConfirmDanger);
+ const findNamespaceSelect = () => wrapper.findComponent(NamespaceSelect);
+ const findHiddenInput = () => wrapper.find('[name="new_parent_group_id"]');
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('default', () => {
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ it('renders the namespace select component', () => {
+ expect(findNamespaceSelect().exists()).toBe(true);
+ });
+
+ it('sets the namespace select properties', () => {
+ expect(findNamespaceSelect().props()).toMatchObject({
+ defaultText: 'Select parent group',
+ fullWidth: false,
+ includeHeaders: false,
+ emptyNamespaceTitle: 'No parent group',
+ includeEmptyNamespace: true,
+ groupNamespaces,
+ });
+ });
+
+ it('renders the hidden input field', () => {
+ expect(findHiddenInput().exists()).toBe(true);
+ expect(findHiddenInput().attributes('value')).toBeUndefined();
+ });
+
+ it('does not render the alert message', () => {
+ expect(findAlert().exists()).toBe(false);
+ });
+
+ it('renders the confirm danger component', () => {
+ expect(findConfirmDanger().exists()).toBe(true);
+ });
+
+ it('sets the confirm danger properties', () => {
+ expect(findConfirmDanger().props()).toMatchObject({
+ buttonClass: 'qa-transfer-button',
+ disabled: true,
+ buttonText: confirmButtonText,
+ phrase: confirmationPhrase,
+ });
+ });
+ });
+
+ describe('with a selected project', () => {
+ const [firstGroup] = groupNamespaces;
+ beforeEach(() => {
+ wrapper = createComponent();
+ findNamespaceSelect().vm.$emit('select', firstGroup);
+ });
+
+ it('sets the confirm danger disabled property to false', () => {
+ expect(findConfirmDanger().props()).toMatchObject({ disabled: false });
+ });
+
+ it('sets the hidden input field', () => {
+ expect(findHiddenInput().exists()).toBe(true);
+ expect(parseInt(findHiddenInput().attributes('value'), 10)).toBe(firstGroup.id);
+ });
+
+ it('emits "confirm" event when the danger modal is confirmed', () => {
+ expect(wrapper.emitted('confirm')).toBeUndefined();
+
+ findConfirmDanger().vm.$emit('confirm');
+
+ expect(wrapper.emitted('confirm')).toHaveLength(1);
+ });
+ });
+
+ describe('isPaidGroup = true', () => {
+ beforeEach(() => {
+ wrapper = createComponent({ isPaidGroup: true });
+ });
+
+ it('disables the transfer button', () => {
+ expect(findConfirmDanger().props()).toMatchObject({ disabled: true });
+ });
+
+ it('hides the namespace selector button', () => {
+ expect(findNamespaceSelect().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/groups/landing_spec.js b/spec/frontend/groups/landing_spec.js
index f90f541eb96..d60adea202b 100644
--- a/spec/frontend/groups/landing_spec.js
+++ b/spec/frontend/groups/landing_spec.js
@@ -159,7 +159,10 @@ describe('Landing', () => {
});
it('should call Cookies.set', () => {
- expect(Cookies.set).toHaveBeenCalledWith(test.cookieName, 'true', { expires: 365 });
+ expect(Cookies.set).toHaveBeenCalledWith(test.cookieName, 'true', {
+ expires: 365,
+ secure: false,
+ });
});
});
diff --git a/spec/frontend/groups/transfer_edit_spec.js b/spec/frontend/groups/transfer_edit_spec.js
deleted file mode 100644
index bc070920d02..00000000000
--- a/spec/frontend/groups/transfer_edit_spec.js
+++ /dev/null
@@ -1,31 +0,0 @@
-import $ from 'jquery';
-
-import { loadHTMLFixture } from 'helpers/fixtures';
-import setupTransferEdit from '~/groups/transfer_edit';
-
-describe('setupTransferEdit', () => {
- const formSelector = '.js-group-transfer-form';
- const targetSelector = '#new_parent_group_id';
-
- beforeEach(() => {
- loadHTMLFixture('groups/edit.html');
- setupTransferEdit(formSelector, targetSelector);
- });
-
- it('disables submit button on load', () => {
- expect($(formSelector).find(':submit').prop('disabled')).toBe(true);
- });
-
- it('enables submit button when selection changes to non-empty value', () => {
- const lastValue = $(formSelector).find(targetSelector).find('.dropdown-content li').last();
- $(formSelector).find(targetSelector).val(lastValue).trigger('change');
-
- expect($(formSelector).find(':submit').prop('disabled')).toBeFalsy();
- });
-
- it('disables submit button when selection changes to empty value', () => {
- $(formSelector).find(targetSelector).val('').trigger('change');
-
- expect($(formSelector).find(':submit').prop('disabled')).toBe(true);
- });
-});
diff --git a/spec/frontend/header_search/components/app_spec.js b/spec/frontend/header_search/components/app_spec.js
index 3200c6614f1..dcbeeeffb2d 100644
--- a/spec/frontend/header_search/components/app_spec.js
+++ b/spec/frontend/header_search/components/app_spec.js
@@ -1,5 +1,5 @@
import { GlSearchBoxByType } from '@gitlab/ui';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import HeaderSearchApp from '~/header_search/components/app.vue';
@@ -202,7 +202,7 @@ describe('HeaderSearchApp', () => {
expect(findHeaderSearchDropdown().exists()).toBe(false);
findHeaderSearchInput().vm.$emit('focus');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findHeaderSearchDropdown().exists()).toBe(true);
});
@@ -211,7 +211,7 @@ describe('HeaderSearchApp', () => {
expect(findHeaderSearchDropdown().exists()).toBe(false);
findHeaderSearchInput().vm.$emit('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findHeaderSearchDropdown().exists()).toBe(true);
});
@@ -265,7 +265,7 @@ describe('HeaderSearchApp', () => {
expect(findHeaderSearchDropdown().exists()).toBe(true);
findDropdownKeyboardNavigation().vm.$emit('tab');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findHeaderSearchDropdown().exists()).toBe(false);
});
@@ -284,7 +284,7 @@ describe('HeaderSearchApp', () => {
it(`when currentFocusIndex changes to ${MOCK_INDEX} updates the data to searchOptions[${MOCK_INDEX}]`, async () => {
findDropdownKeyboardNavigation().vm.$emit('change', MOCK_INDEX);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.vm.currentFocusedOption).toBe(MOCK_DEFAULT_SEARCH_OPTIONS[MOCK_INDEX]);
});
});
@@ -299,7 +299,7 @@ describe('HeaderSearchApp', () => {
it('onKey-enter submits a search', async () => {
findHeaderSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY }));
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(visitUrl).toHaveBeenCalledWith(MOCK_SEARCH_QUERY);
});
@@ -316,7 +316,7 @@ describe('HeaderSearchApp', () => {
it('onKey-enter clicks the selected dropdown item rather than submitting a search', async () => {
findDropdownKeyboardNavigation().vm.$emit('change', MOCK_INDEX);
- await wrapper.vm.$nextTick();
+ await nextTick();
findHeaderSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY }));
expect(visitUrl).toHaveBeenCalledWith(MOCK_DEFAULT_SEARCH_OPTIONS[MOCK_INDEX].url);
});
diff --git a/spec/frontend/header_search/components/header_search_autocomplete_items_spec.js b/spec/frontend/header_search/components/header_search_autocomplete_items_spec.js
index bec0cbc8a5c..502f10ff771 100644
--- a/spec/frontend/header_search/components/header_search_autocomplete_items_spec.js
+++ b/spec/frontend/header_search/components/header_search_autocomplete_items_spec.js
@@ -1,6 +1,6 @@
import { GlDropdownItem, GlLoadingIcon, GlAvatar } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import HeaderSearchAutocompleteItems from '~/header_search/components/header_search_autocomplete_items.vue';
import {
@@ -143,7 +143,7 @@ describe('HeaderSearchAutocompleteItems', () => {
wrapper.setProps({ currentFocusedOption: MOCK_SORTED_AUTOCOMPLETE_OPTIONS[0] });
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(scrollSpy).toHaveBeenCalledWith(false);
scrollSpy.mockRestore();
diff --git a/spec/frontend/ide/components/activity_bar_spec.js b/spec/frontend/ide/components/activity_bar_spec.js
index 657817eb3d8..39fe2c7e723 100644
--- a/spec/frontend/ide/components/activity_bar_spec.js
+++ b/spec/frontend/ide/components/activity_bar_spec.js
@@ -1,4 +1,4 @@
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
import ActivityBar from '~/ide/components/activity_bar.vue';
import { leftSidebarViews } from '~/ide/constants';
@@ -61,14 +61,11 @@ describe('IDE activity bar', () => {
expect(vm.$el.querySelector('.js-ide-edit-mode').classList).toContain('active');
});
- it('sets commit item active', (done) => {
+ it('sets commit item active', async () => {
vm.$store.state.currentActivityView = leftSidebarViews.commit.name;
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.js-ide-commit-mode').classList).toContain('active');
-
- done();
- });
+ await nextTick();
+ expect(vm.$el.querySelector('.js-ide-commit-mode').classList).toContain('active');
});
});
diff --git a/spec/frontend/ide/components/branches/search_list_spec.js b/spec/frontend/ide/components/branches/search_list_spec.js
index 0efa7af2c6c..b6e3274153a 100644
--- a/spec/frontend/ide/components/branches/search_list_spec.js
+++ b/spec/frontend/ide/components/branches/search_list_spec.js
@@ -1,13 +1,13 @@
import { GlLoadingIcon } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import Item from '~/ide/components/branches/item.vue';
import List from '~/ide/components/branches/search_list.vue';
import { __ } from '~/locale';
import { branches } from '../../mock_data';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('IDE branches search list', () => {
let wrapper;
@@ -31,7 +31,6 @@ describe('IDE branches search list', () => {
});
wrapper = shallowMount(List, {
- localVue,
store: fakeStore,
});
};
@@ -51,13 +50,12 @@ describe('IDE branches search list', () => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
- it('renders branches not found when search is not empty and branches list is empty', () => {
+ it('renders branches not found when search is not empty and branches list is empty', async () => {
createComponent({ branches: [] });
wrapper.find('input[type="search"]').setValue('something');
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.text()).toContain(__('No branches found'));
- });
+ await nextTick();
+ expect(wrapper.text()).toContain(__('No branches found'));
});
describe('with branches', () => {
diff --git a/spec/frontend/ide/components/commit_sidebar/actions_spec.js b/spec/frontend/ide/components/commit_sidebar/actions_spec.js
index ed9d11246ae..c9425f6c9cd 100644
--- a/spec/frontend/ide/components/commit_sidebar/actions_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/actions_spec.js
@@ -1,4 +1,4 @@
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
import { projectData, branches } from 'jest/ide/mock_data';
import commitActions from '~/ide/components/commit_sidebar/actions.vue';
@@ -71,15 +71,12 @@ describe('IDE commit sidebar actions', () => {
expect(findText()).toContain('Commit to main branch');
});
- it('hides merge request option when project merge requests are disabled', (done) => {
+ it('hides merge request option when project merge requests are disabled', async () => {
createComponent({ hasMR: false });
- vm.$nextTick(() => {
- expect(findRadios().length).toBe(2);
- expect(findText()).not.toContain('Create a new branch and merge request');
-
- done();
- });
+ await nextTick();
+ expect(findRadios().length).toBe(2);
+ expect(findText()).not.toContain('Create a new branch and merge request');
});
describe('currentBranchText', () => {
@@ -105,22 +102,18 @@ describe('IDE commit sidebar actions', () => {
expect(vm.$store.dispatch).not.toHaveBeenCalled();
});
- it('calls again after staged changes', (done) => {
+ it('calls again after staged changes', async () => {
createComponent({ currentBranchId: null });
vm.$store.state.currentBranchId = 'main';
vm.$store.state.changedFiles.push({});
vm.$store.state.stagedFiles.push({});
- vm.$nextTick()
- .then(() => {
- expect(vm.$store.dispatch).toHaveBeenCalledWith(
- ACTION_UPDATE_COMMIT_ACTION,
- expect.anything(),
- );
- })
- .then(done)
- .catch(done.fail);
+ await nextTick();
+ expect(vm.$store.dispatch).toHaveBeenCalledWith(
+ ACTION_UPDATE_COMMIT_ACTION,
+ expect.anything(),
+ );
});
it.each`
diff --git a/spec/frontend/ide/components/commit_sidebar/editor_header_spec.js b/spec/frontend/ide/components/commit_sidebar/editor_header_spec.js
index 50635ffe894..6e4c66cb780 100644
--- a/spec/frontend/ide/components/commit_sidebar/editor_header_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/editor_header_spec.js
@@ -1,11 +1,11 @@
-import { mount, createLocalVue } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import EditorHeader from '~/ide/components/commit_sidebar/editor_header.vue';
import { createStore } from '~/ide/stores';
import { file } from '../../helpers';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
const TEST_FILE_PATH = 'test/file/path';
@@ -16,7 +16,6 @@ describe('IDE commit editor header', () => {
const createComponent = (fileProps = {}) => {
wrapper = mount(EditorHeader, {
store,
- localVue,
propsData: {
activeFile: {
...file(TEST_FILE_PATH),
diff --git a/spec/frontend/ide/components/commit_sidebar/form_spec.js b/spec/frontend/ide/components/commit_sidebar/form_spec.js
index 83d1bbb842e..d3b2923ac6c 100644
--- a/spec/frontend/ide/components/commit_sidebar/form_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/form_spec.js
@@ -1,6 +1,6 @@
import { GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import { stubComponent } from 'helpers/stub_component';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import waitForPromises from 'helpers/wait_for_promises';
@@ -56,7 +56,6 @@ describe('IDE commit form', () => {
disabled: findCommitButton().props('disabled'),
tooltip: getBinding(findCommitButtonTooltip().element, 'gl-tooltip').value.title,
});
- const clickCommitButton = () => findCommitButton().vm.$emit('click');
const findForm = () => wrapper.find('form');
const submitForm = () => findForm().trigger('submit');
const findCommitMessageInput = () => wrapper.find(CommitMessageField);
@@ -98,7 +97,7 @@ describe('IDE commit form', () => {
it(`at view=${viewFn.name}, ${buttonFn.name} has disabled=${disabled} tooltip=${tooltip}`, async () => {
viewFn();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(buttonFn()).toEqual({
disabled,
@@ -116,7 +115,7 @@ describe('IDE commit form', () => {
goToEditView();
- await wrapper.vm.$nextTick();
+ await nextTick();
});
it('renders commit button in compact mode', () => {
@@ -135,7 +134,7 @@ describe('IDE commit form', () => {
it('when begin commit button is clicked, shows form', async () => {
findBeginCommitButton().vm.$emit('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findForm().exists()).toBe(true);
});
@@ -143,7 +142,7 @@ describe('IDE commit form', () => {
it('when begin commit button is clicked, sets activity view', async () => {
findBeginCommitButton().vm.$emit('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(store.state.currentActivityView).toBe(leftSidebarViews.commit.name);
});
@@ -153,14 +152,14 @@ describe('IDE commit form', () => {
setLastCommitMessage('test');
goToEditView();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findForm().exists()).toBe(true);
// Now test that it collapses when lastCommitMsg is cleared
setLastCommitMessage('');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findForm().exists()).toBe(false);
});
@@ -177,7 +176,7 @@ describe('IDE commit form', () => {
goToCommitView();
- await wrapper.vm.$nextTick();
+ await nextTick();
});
afterEach(() => {
@@ -188,12 +187,12 @@ describe('IDE commit form', () => {
expect(findForm().exists()).toBe(false);
store.state.stagedFiles = [];
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findForm().exists()).toBe(false);
store.state.stagedFiles.push('test');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findForm().exists()).toBe(false);
});
@@ -208,7 +207,7 @@ describe('IDE commit form', () => {
goToCommitView();
- await wrapper.vm.$nextTick();
+ await nextTick();
});
it('shows form', () => {
@@ -222,7 +221,7 @@ describe('IDE commit form', () => {
describe('when no changed files', () => {
beforeEach(async () => {
store.state.stagedFiles = [];
- await wrapper.vm.$nextTick();
+ await nextTick();
});
it('hides form', () => {
@@ -231,7 +230,7 @@ describe('IDE commit form', () => {
it('expands again when staged files are added', async () => {
store.state.stagedFiles.push('test');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findForm().exists()).toBe(true);
});
@@ -240,7 +239,7 @@ describe('IDE commit form', () => {
it('updates commitMessage in store on input', async () => {
setCommitMessageInput('testing commit message');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(store.state.commit.commitMessage).toBe('testing commit message');
});
@@ -253,14 +252,14 @@ describe('IDE commit form', () => {
it('resets commitMessage when clicking discard button', async () => {
setCommitMessageInput('testing commit message');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findCommitMessageInput().props('text')).toBe('testing commit message');
// Test that commitMessage is cleared on click
findDiscardDraftButton().vm.$emit('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findCommitMessageInput().props('text')).toBe('');
});
@@ -274,24 +273,24 @@ describe('IDE commit form', () => {
goToCommitView();
- await wrapper.vm.$nextTick();
+ await nextTick();
setCommitMessageInput('testing commit message');
- await wrapper.vm.$nextTick();
+ await nextTick();
jest.spyOn(store, 'dispatch').mockResolvedValue();
});
- it.each([clickCommitButton, submitForm])('when %p, commits changes', (fn) => {
- fn();
+ it('when submitting form, commits changes', () => {
+ submitForm();
expect(store.dispatch).toHaveBeenCalledWith('commit/commitChanges', undefined);
});
it('when cannot push code, submitting does nothing', async () => {
store.state.projects.abcproject.userPermissions.pushCode = false;
- await wrapper.vm.$nextTick();
+ await nextTick();
submitForm();
@@ -309,7 +308,7 @@ describe('IDE commit form', () => {
const error = createError();
store.state.commit.commitError = error;
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(modal.vm.show).toHaveBeenCalled();
expect(modal.props()).toMatchObject({
@@ -342,7 +341,7 @@ describe('IDE commit form', () => {
async ({ commitError, expectedActions }) => {
store.state.commit.commitError = commitError('test message');
- await wrapper.vm.$nextTick();
+ await nextTick();
wrapper.find(GlModal).vm.$emit('ok');
diff --git a/spec/frontend/ide/components/commit_sidebar/list_item_spec.js b/spec/frontend/ide/components/commit_sidebar/list_item_spec.js
index b91ee88e0d6..dea920ecb5e 100644
--- a/spec/frontend/ide/components/commit_sidebar/list_item_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/list_item_spec.js
@@ -1,5 +1,6 @@
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import { trimText } from 'helpers/text_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
import listItem from '~/ide/components/commit_sidebar/list_item.vue';
import { createRouter } from '~/ide/ide_router';
@@ -41,54 +42,42 @@ describe('Multi-file editor commit sidebar list item', () => {
expect(findPathText()).toContain(f.path);
});
- it('correctly renders renamed entries', (done) => {
+ it('correctly renders renamed entries', async () => {
Vue.set(vm.file, 'prevName', 'Old name');
- vm.$nextTick()
- .then(() => {
- expect(findPathText()).toEqual(`Old name → ${f.name}`);
- })
- .then(done)
- .catch(done.fail);
+ await nextTick();
+ expect(findPathText()).toEqual(`Old name → ${f.name}`);
});
- it('correctly renders entry, the name of which did not change after rename (as within a folder)', (done) => {
+ it('correctly renders entry, the name of which did not change after rename (as within a folder)', async () => {
Vue.set(vm.file, 'prevName', f.name);
- vm.$nextTick()
- .then(() => {
- expect(findPathText()).toEqual(f.name);
- })
- .then(done)
- .catch(done.fail);
+ await nextTick();
+ expect(findPathText()).toEqual(f.name);
});
- it('opens a closed file in the editor when clicking the file path', (done) => {
+ it('opens a closed file in the editor when clicking the file path', async () => {
jest.spyOn(vm, 'openPendingTab');
jest.spyOn(router, 'push').mockImplementation(() => {});
findPathEl.click();
- setImmediate(() => {
- expect(vm.openPendingTab).toHaveBeenCalled();
- expect(router.push).toHaveBeenCalled();
+ await nextTick();
- done();
- });
+ expect(vm.openPendingTab).toHaveBeenCalled();
+ expect(router.push).toHaveBeenCalled();
});
- it('calls updateViewer with diff when clicking file', (done) => {
+ it('calls updateViewer with diff when clicking file', async () => {
jest.spyOn(vm, 'openFileInEditor');
jest.spyOn(vm, 'updateViewer');
jest.spyOn(router, 'push').mockImplementation(() => {});
findPathEl.click();
- setImmediate(() => {
- expect(vm.updateViewer).toHaveBeenCalledWith('diff');
+ await waitForPromises();
- done();
- });
+ expect(vm.updateViewer).toHaveBeenCalledWith('diff');
});
describe('computed', () => {
@@ -134,14 +123,11 @@ describe('Multi-file editor commit sidebar list item', () => {
expect(vm.$el.querySelector('.is-active')).toBe(null);
});
- it('adds active class when keys match', (done) => {
+ it('adds active class when keys match', async () => {
vm.keyPrefix = 'staged';
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.is-active')).not.toBe(null);
-
- done();
- });
+ await nextTick();
+ expect(vm.$el.querySelector('.is-active')).not.toBe(null);
});
});
});
diff --git a/spec/frontend/ide/components/commit_sidebar/list_spec.js b/spec/frontend/ide/components/commit_sidebar/list_spec.js
index eb12fc994a5..1d42512c9ee 100644
--- a/spec/frontend/ide/components/commit_sidebar/list_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/list_spec.js
@@ -1,4 +1,4 @@
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
import commitSidebarList from '~/ide/components/commit_sidebar/list.vue';
import { createStore } from '~/ide/stores';
@@ -31,12 +31,11 @@ describe('Multi-file editor commit sidebar list', () => {
});
describe('with a list of files', () => {
- beforeEach((done) => {
+ beforeEach(async () => {
const f = file('file name');
f.changed = true;
vm.fileList.push(f);
-
- Vue.nextTick(done);
+ await nextTick();
});
it('renders list', () => {
diff --git a/spec/frontend/ide/components/commit_sidebar/message_field_spec.js b/spec/frontend/ide/components/commit_sidebar/message_field_spec.js
index 1514fbc2c3b..e66de6bb0b0 100644
--- a/spec/frontend/ide/components/commit_sidebar/message_field_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/message_field_spec.js
@@ -1,4 +1,4 @@
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import createComponent from 'helpers/vue_mount_component_helper';
import CommitMessageField from '~/ide/components/commit_sidebar/message_field.vue';
@@ -23,34 +23,23 @@ describe('IDE commit message field', () => {
vm.$destroy();
});
- it('adds is-focused class on focus', (done) => {
+ it('adds is-focused class on focus', async () => {
vm.$el.querySelector('textarea').focus();
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.is-focused')).not.toBeNull();
-
- done();
- });
+ await nextTick();
+ expect(vm.$el.querySelector('.is-focused')).not.toBeNull();
});
- it('removed is-focused class on blur', (done) => {
+ it('removed is-focused class on blur', async () => {
vm.$el.querySelector('textarea').focus();
- vm.$nextTick()
- .then(() => {
- expect(vm.$el.querySelector('.is-focused')).not.toBeNull();
-
- vm.$el.querySelector('textarea').blur();
+ await nextTick();
+ expect(vm.$el.querySelector('.is-focused')).not.toBeNull();
- return vm.$nextTick();
- })
- .then(() => {
- expect(vm.$el.querySelector('.is-focused')).toBeNull();
+ vm.$el.querySelector('textarea').blur();
- done();
- })
- .then(done)
- .catch(done.fail);
+ await nextTick();
+ expect(vm.$el.querySelector('.is-focused')).toBeNull();
});
it('emits input event on input', () => {
@@ -66,105 +55,78 @@ describe('IDE commit message field', () => {
describe('highlights', () => {
describe('subject line', () => {
- it('does not highlight less than 50 characters', (done) => {
+ it('does not highlight less than 50 characters', async () => {
vm.text = 'text less than 50 chars';
- vm.$nextTick()
- .then(() => {
- expect(vm.$el.querySelector('.highlights span').textContent).toContain(
- 'text less than 50 chars',
- );
+ await nextTick();
+ expect(vm.$el.querySelector('.highlights span').textContent).toContain(
+ 'text less than 50 chars',
+ );
- expect(vm.$el.querySelector('mark').style.display).toBe('none');
- })
- .then(done)
- .catch(done.fail);
+ expect(vm.$el.querySelector('mark').style.display).toBe('none');
});
- it('highlights characters over 50 length', (done) => {
+ it('highlights characters over 50 length', async () => {
vm.text =
'text less than 50 chars that should not highlighted. text more than 50 should be highlighted';
- vm.$nextTick()
- .then(() => {
- expect(vm.$el.querySelector('.highlights span').textContent).toContain(
- 'text less than 50 chars that should not highlighte',
- );
-
- expect(vm.$el.querySelector('mark').style.display).not.toBe('none');
- expect(vm.$el.querySelector('mark').textContent).toBe(
- 'd. text more than 50 should be highlighted',
- );
- })
- .then(done)
- .catch(done.fail);
+ await nextTick();
+ expect(vm.$el.querySelector('.highlights span').textContent).toContain(
+ 'text less than 50 chars that should not highlighte',
+ );
+
+ expect(vm.$el.querySelector('mark').style.display).not.toBe('none');
+ expect(vm.$el.querySelector('mark').textContent).toBe(
+ 'd. text more than 50 should be highlighted',
+ );
});
});
describe('body text', () => {
- it('does not highlight body text less tan 72 characters', (done) => {
+ it('does not highlight body text less tan 72 characters', async () => {
vm.text = 'subject line\nbody content';
- vm.$nextTick()
- .then(() => {
- expect(vm.$el.querySelectorAll('.highlights span').length).toBe(2);
- expect(vm.$el.querySelectorAll('mark')[1].style.display).toBe('none');
- })
- .then(done)
- .catch(done.fail);
+ await nextTick();
+ expect(vm.$el.querySelectorAll('.highlights span').length).toBe(2);
+ expect(vm.$el.querySelectorAll('mark')[1].style.display).toBe('none');
});
- it('highlights body text more than 72 characters', (done) => {
+ it('highlights body text more than 72 characters', async () => {
vm.text =
'subject line\nbody content that will be highlighted when it is more than 72 characters in length';
- vm.$nextTick()
- .then(() => {
- expect(vm.$el.querySelectorAll('.highlights span').length).toBe(2);
- expect(vm.$el.querySelectorAll('mark')[1].style.display).not.toBe('none');
- expect(vm.$el.querySelectorAll('mark')[1].textContent).toBe(' in length');
- })
- .then(done)
- .catch(done.fail);
+ await nextTick();
+ expect(vm.$el.querySelectorAll('.highlights span').length).toBe(2);
+ expect(vm.$el.querySelectorAll('mark')[1].style.display).not.toBe('none');
+ expect(vm.$el.querySelectorAll('mark')[1].textContent).toBe(' in length');
});
- it('highlights body text & subject line', (done) => {
+ it('highlights body text & subject line', async () => {
vm.text =
'text less than 50 chars that should not highlighted\nbody content that will be highlighted when it is more than 72 characters in length';
- vm.$nextTick()
- .then(() => {
- expect(vm.$el.querySelectorAll('.highlights span').length).toBe(2);
- expect(vm.$el.querySelectorAll('mark').length).toBe(2);
+ await nextTick();
+ expect(vm.$el.querySelectorAll('.highlights span').length).toBe(2);
+ expect(vm.$el.querySelectorAll('mark').length).toBe(2);
- expect(vm.$el.querySelectorAll('mark')[0].textContent).toContain('d');
- expect(vm.$el.querySelectorAll('mark')[1].textContent).toBe(' in length');
- })
- .then(done)
- .catch(done.fail);
+ expect(vm.$el.querySelectorAll('mark')[0].textContent).toContain('d');
+ expect(vm.$el.querySelectorAll('mark')[1].textContent).toBe(' in length');
});
});
});
describe('scrolling textarea', () => {
- it('updates transform of highlights', (done) => {
+ it('updates transform of highlights', async () => {
vm.text = 'subject line\n\n\n\n\n\n\n\n\n\n\nbody content';
- vm.$nextTick()
- .then(() => {
- vm.$el.querySelector('textarea').scrollTo(0, 50);
-
- vm.handleScroll();
- })
- .then(vm.$nextTick)
- .then(() => {
- expect(vm.scrollTop).toBe(50);
- expect(vm.$el.querySelector('.highlights').style.transform).toBe(
- 'translate3d(0, -50px, 0)',
- );
- })
- .then(done)
- .catch(done.fail);
+ await nextTick();
+ vm.$el.querySelector('textarea').scrollTo(0, 50);
+
+ vm.handleScroll();
+
+ await nextTick();
+ expect(vm.scrollTop).toBe(50);
+ expect(vm.$el.querySelector('.highlights').style.transform).toBe('translate3d(0, -50px, 0)');
});
});
});
diff --git a/spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js b/spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js
index 4474647552d..64b53264b4d 100644
--- a/spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js
@@ -1,4 +1,4 @@
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
import { projectData, branches } from 'jest/ide/mock_data';
import NewMergeRequestOption from '~/ide/components/commit_sidebar/new_merge_request_option.vue';
@@ -72,15 +72,11 @@ describe('create new MR checkbox', () => {
expect(vm.$el.textContent).not.toBe('');
});
- it('has new MR', (done) => {
+ it('has new MR', async () => {
setMR();
- vm.$nextTick()
- .then(() => {
- expect(vm.$el.textContent).not.toBe('');
- })
- .then(done)
- .catch(done.fail);
+ await nextTick();
+ expect(vm.$el.textContent).not.toBe('');
});
});
@@ -96,15 +92,11 @@ describe('create new MR checkbox', () => {
expect(vm.$el.textContent).toBe('');
});
- it('has new MR', (done) => {
+ it('has new MR', async () => {
setMR();
- vm.$nextTick()
- .then(() => {
- expect(vm.$el.textContent).toBe('');
- })
- .then(done)
- .catch(done.fail);
+ await nextTick();
+ expect(vm.$el.textContent).toBe('');
});
});
});
@@ -121,15 +113,11 @@ describe('create new MR checkbox', () => {
expect(vm.$el.textContent).not.toBe('');
});
- it('is rendered if MR exists', (done) => {
+ it('is rendered if MR exists', async () => {
setMR();
- vm.$nextTick()
- .then(() => {
- expect(vm.$el.textContent).not.toBe('');
- })
- .then(done)
- .catch(done.fail);
+ await nextTick();
+ expect(vm.$el.textContent).not.toBe('');
});
});
@@ -144,15 +132,11 @@ describe('create new MR checkbox', () => {
expect(vm.$el.textContent).not.toBe('');
});
- it('is hidden if MR exists', (done) => {
+ it('is hidden if MR exists', async () => {
setMR();
- vm.$nextTick()
- .then(() => {
- expect(vm.$el.textContent).toBe('');
- })
- .then(done)
- .catch(done.fail);
+ await nextTick();
+ expect(vm.$el.textContent).toBe('');
});
});
});
@@ -168,15 +152,11 @@ describe('create new MR checkbox', () => {
expect(vm.$el.textContent).not.toBe('');
});
- it('is hidden if MR exists', (done) => {
+ it('is hidden if MR exists', async () => {
setMR();
- vm.$nextTick()
- .then(() => {
- expect(vm.$el.textContent).toBe('');
- })
- .then(done)
- .catch(done.fail);
+ await nextTick();
+ expect(vm.$el.textContent).toBe('');
});
it('shows enablded checkbox', () => {
diff --git a/spec/frontend/ide/components/commit_sidebar/radio_group_spec.js b/spec/frontend/ide/components/commit_sidebar/radio_group_spec.js
index a6f3253321b..d899bc4f7d8 100644
--- a/spec/frontend/ide/components/commit_sidebar/radio_group_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/radio_group_spec.js
@@ -1,4 +1,4 @@
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
import radioGroup from '~/ide/components/commit_sidebar/radio_group.vue';
import { createStore } from '~/ide/stores';
@@ -7,7 +7,7 @@ describe('IDE commit sidebar radio group', () => {
let vm;
let store;
- beforeEach((done) => {
+ beforeEach(async () => {
store = createStore();
const Component = Vue.extend(radioGroup);
@@ -22,7 +22,7 @@ describe('IDE commit sidebar radio group', () => {
vm.$mount();
- Vue.nextTick(done);
+ await nextTick();
});
afterEach(() => {
@@ -33,7 +33,7 @@ describe('IDE commit sidebar radio group', () => {
expect(vm.$el.textContent).toContain('test');
});
- it('uses slot if label is not present', (done) => {
+ it('uses slot if label is not present', async () => {
vm.$destroy();
vm = new Vue({
@@ -47,25 +47,19 @@ describe('IDE commit sidebar radio group', () => {
vm.$mount();
- Vue.nextTick(() => {
- expect(vm.$el.textContent).toContain('Testing slot');
-
- done();
- });
+ await nextTick();
+ expect(vm.$el.textContent).toContain('Testing slot');
});
- it('updates store when changing radio button', (done) => {
+ it('updates store when changing radio button', async () => {
vm.$el.querySelector('input').dispatchEvent(new Event('change'));
- Vue.nextTick(() => {
- expect(store.state.commit.commitAction).toBe('1');
-
- done();
- });
+ await nextTick();
+ expect(store.state.commit.commitAction).toBe('1');
});
describe('with input', () => {
- beforeEach((done) => {
+ beforeEach(async () => {
vm.$destroy();
const Component = Vue.extend(radioGroup);
@@ -82,32 +76,27 @@ describe('IDE commit sidebar radio group', () => {
vm.$mount();
- Vue.nextTick(done);
+ await nextTick();
});
it('renders input box when commitAction matches value', () => {
expect(vm.$el.querySelector('.form-control')).not.toBeNull();
});
- it('hides input when commitAction doesnt match value', (done) => {
+ it('hides input when commitAction doesnt match value', async () => {
store.state.commit.commitAction = '2';
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.form-control')).toBeNull();
- done();
- });
+ await nextTick();
+ expect(vm.$el.querySelector('.form-control')).toBeNull();
});
- it('updates branch name in store on input', (done) => {
+ it('updates branch name in store on input', async () => {
const input = vm.$el.querySelector('.form-control');
input.value = 'testing-123';
input.dispatchEvent(new Event('input'));
- Vue.nextTick(() => {
- expect(store.state.commit.newBranchName).toBe('testing-123');
-
- done();
- });
+ await nextTick();
+ expect(store.state.commit.newBranchName).toBe('testing-123');
});
it('renders newBranchName if present', () => {
diff --git a/spec/frontend/ide/components/commit_sidebar/success_message_spec.js b/spec/frontend/ide/components/commit_sidebar/success_message_spec.js
index 7bbe47d37af..52e35bdbb73 100644
--- a/spec/frontend/ide/components/commit_sidebar/success_message_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/success_message_spec.js
@@ -1,4 +1,4 @@
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
import successMessage from '~/ide/components/commit_sidebar/success_message.vue';
import { createStore } from '~/ide/stores';
@@ -23,13 +23,10 @@ describe('IDE commit panel successful commit state', () => {
vm.$destroy();
});
- it('renders last commit message when it exists', (done) => {
+ it('renders last commit message when it exists', async () => {
vm.$store.state.lastCommitMsg = 'testing commit message';
- Vue.nextTick(() => {
- expect(vm.$el.textContent).toContain('testing commit message');
-
- done();
- });
+ await nextTick();
+ expect(vm.$el.textContent).toContain('testing commit message');
});
});
diff --git a/spec/frontend/ide/components/error_message_spec.js b/spec/frontend/ide/components/error_message_spec.js
index 2de3fa863a8..17568158131 100644
--- a/spec/frontend/ide/components/error_message_spec.js
+++ b/spec/frontend/ide/components/error_message_spec.js
@@ -1,10 +1,10 @@
import { GlLoadingIcon } from '@gitlab/ui';
-import { mount, createLocalVue } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import ErrorMessage from '~/ide/components/error_message.vue';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('IDE error message component', () => {
let wrapper;
@@ -25,7 +25,6 @@ describe('IDE error message component', () => {
},
},
store: fakeStore,
- localVue,
});
};
@@ -87,19 +86,15 @@ describe('IDE error message component', () => {
expect(actionMock).toHaveBeenCalledWith(message.actionPayload);
});
- it('does not dispatch action when already loading', () => {
+ it('does not dispatch action when already loading', async () => {
findActionButton().trigger('click');
actionMock.mockReset();
- return wrapper.vm.$nextTick(() => {
- findActionButton().trigger('click');
-
- return wrapper.vm.$nextTick().then(() => {
- expect(actionMock).not.toHaveBeenCalled();
- });
- });
+ findActionButton().trigger('click');
+ await nextTick();
+ expect(actionMock).not.toHaveBeenCalled();
});
- it('shows loading icon when loading', () => {
+ it('shows loading icon when loading', async () => {
let resolveAction;
actionMock.mockImplementation(
() =>
@@ -109,19 +104,16 @@ describe('IDE error message component', () => {
);
findActionButton().trigger('click');
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.find(GlLoadingIcon).isVisible()).toBe(true);
- resolveAction();
- });
+ await nextTick();
+ expect(wrapper.find(GlLoadingIcon).isVisible()).toBe(true);
+ resolveAction();
});
- it('hides loading icon when operation finishes', () => {
+ it('hides loading icon when operation finishes', async () => {
findActionButton().trigger('click');
- return actionMock()
- .then(() => wrapper.vm.$nextTick())
- .then(() => {
- expect(wrapper.find(GlLoadingIcon).isVisible()).toBe(false);
- });
+ await actionMock();
+ await nextTick();
+ expect(wrapper.find(GlLoadingIcon).isVisible()).toBe(false);
});
});
});
diff --git a/spec/frontend/ide/components/file_row_extra_spec.js b/spec/frontend/ide/components/file_row_extra_spec.js
index 641407c7b77..5a7a1fe7db0 100644
--- a/spec/frontend/ide/components/file_row_extra_spec.js
+++ b/spec/frontend/ide/components/file_row_extra_spec.js
@@ -1,4 +1,4 @@
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
import FileRowExtra from '~/ide/components/file_row_extra.vue';
import { createStore } from '~/ide/stores';
@@ -70,28 +70,22 @@ describe('IDE extra file row component', () => {
expect(vm.$el.querySelector('.ide-tree-changes')).toBe(null);
});
- it('does not show when tree is open', (done) => {
+ it('does not show when tree is open', async () => {
vm.file.type = 'tree';
vm.file.opened = true;
changesCount = 1;
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.ide-tree-changes')).toBe(null);
-
- done();
- });
+ await nextTick();
+ expect(vm.$el.querySelector('.ide-tree-changes')).toBe(null);
});
- it('shows for trees with changes', (done) => {
+ it('shows for trees with changes', async () => {
vm.file.type = 'tree';
vm.file.opened = false;
changesCount = 1;
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.ide-tree-changes')).not.toBe(null);
-
- done();
- });
+ await nextTick();
+ expect(vm.$el.querySelector('.ide-tree-changes')).not.toBe(null);
});
});
@@ -100,55 +94,40 @@ describe('IDE extra file row component', () => {
expect(vm.$el.querySelector('.file-changed-icon')).toBe(null);
});
- it('shows when file is changed', (done) => {
+ it('shows when file is changed', async () => {
vm.file.changed = true;
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.file-changed-icon')).not.toBe(null);
-
- done();
- });
+ await nextTick();
+ expect(vm.$el.querySelector('.file-changed-icon')).not.toBe(null);
});
- it('shows when file is staged', (done) => {
+ it('shows when file is staged', async () => {
vm.file.staged = true;
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.file-changed-icon')).not.toBe(null);
-
- done();
- });
+ await nextTick();
+ expect(vm.$el.querySelector('.file-changed-icon')).not.toBe(null);
});
- it('shows when file is a tempFile', (done) => {
+ it('shows when file is a tempFile', async () => {
vm.file.tempFile = true;
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.file-changed-icon')).not.toBe(null);
-
- done();
- });
+ await nextTick();
+ expect(vm.$el.querySelector('.file-changed-icon')).not.toBe(null);
});
- it('shows when file is renamed', (done) => {
+ it('shows when file is renamed', async () => {
vm.file.prevPath = 'original-file';
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.file-changed-icon')).not.toBe(null);
-
- done();
- });
+ await nextTick();
+ expect(vm.$el.querySelector('.file-changed-icon')).not.toBe(null);
});
- it('hides when file is renamed', (done) => {
+ it('hides when file is renamed', async () => {
vm.file.prevPath = 'original-file';
vm.file.type = 'tree';
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.file-changed-icon')).toBe(null);
-
- done();
- });
+ await nextTick();
+ expect(vm.$el.querySelector('.file-changed-icon')).toBe(null);
});
});
@@ -157,14 +136,11 @@ describe('IDE extra file row component', () => {
expect(vm.$el.querySelector('[data-testid="git-merge-icon"]')).toBe(null);
});
- it('shows when a merge request change', (done) => {
+ it('shows when a merge request change', async () => {
vm.file.mrChange = true;
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('[data-testid="git-merge-icon"]')).not.toBe(null);
-
- done();
- });
+ await nextTick();
+ expect(vm.$el.querySelector('[data-testid="git-merge-icon"]')).not.toBe(null);
});
});
});
diff --git a/spec/frontend/ide/components/file_templates/bar_spec.js b/spec/frontend/ide/components/file_templates/bar_spec.js
index 4ca99f8d055..e8ebfa78fe9 100644
--- a/spec/frontend/ide/components/file_templates/bar_spec.js
+++ b/spec/frontend/ide/components/file_templates/bar_spec.js
@@ -1,4 +1,4 @@
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
import Bar from '~/ide/components/file_templates/bar.vue';
import { createStore } from '~/ide/stores';
@@ -46,7 +46,7 @@ describe('IDE file templates bar component', () => {
});
describe('template dropdown', () => {
- beforeEach((done) => {
+ beforeEach(async () => {
vm.$store.state.fileTemplates.templates = [
{
name: 'test',
@@ -57,7 +57,7 @@ describe('IDE file templates bar component', () => {
key: 'gitlab_ci_ymls',
};
- vm.$nextTick(done);
+ await nextTick();
});
it('renders dropdown component', () => {
@@ -75,14 +75,11 @@ describe('IDE file templates bar component', () => {
});
});
- it('shows undo button if updateSuccess is true', (done) => {
+ it('shows undo button if updateSuccess is true', async () => {
vm.$store.state.fileTemplates.updateSuccess = true;
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.btn-default').style.display).not.toBe('none');
-
- done();
- });
+ await nextTick();
+ expect(vm.$el.querySelector('.btn-default').style.display).not.toBe('none');
});
it('calls undoFileTemplate when clicking undo button', () => {
@@ -93,7 +90,7 @@ describe('IDE file templates bar component', () => {
expect(vm.undoFileTemplate).toHaveBeenCalled();
});
- it('calls setSelectedTemplateType if activeFile name matches a template', (done) => {
+ it('calls setSelectedTemplateType if activeFile name matches a template', async () => {
const fileName = '.gitlab-ci.yml';
jest.spyOn(vm, 'setSelectedTemplateType').mockImplementation(() => {});
@@ -101,13 +98,10 @@ describe('IDE file templates bar component', () => {
vm.setInitialType();
- vm.$nextTick(() => {
- expect(vm.setSelectedTemplateType).toHaveBeenCalledWith({
- name: fileName,
- key: 'gitlab_ci_ymls',
- });
-
- done();
+ await nextTick();
+ expect(vm.setSelectedTemplateType).toHaveBeenCalledWith({
+ name: fileName,
+ key: 'gitlab_ci_ymls',
});
});
});
diff --git a/spec/frontend/ide/components/file_templates/dropdown_spec.js b/spec/frontend/ide/components/file_templates/dropdown_spec.js
index 44ac9aa954d..e54b322b9db 100644
--- a/spec/frontend/ide/components/file_templates/dropdown_spec.js
+++ b/spec/frontend/ide/components/file_templates/dropdown_spec.js
@@ -1,11 +1,11 @@
import { GlLoadingIcon } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import $ from 'jquery';
import Vuex from 'vuex';
import Dropdown from '~/ide/components/file_templates/dropdown.vue';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('IDE file templates dropdown component', () => {
let wrapper;
@@ -44,7 +44,6 @@ describe('IDE file templates dropdown component', () => {
...props,
},
store: fakeStore,
- localVue,
});
({ element } = wrapper);
@@ -55,15 +54,14 @@ describe('IDE file templates dropdown component', () => {
wrapper = null;
});
- it('calls clickItem on click', () => {
+ it('calls clickItem on click', async () => {
const itemData = { name: 'test.yml ' };
createComponent({ props: { data: [itemData] } });
const item = findItemButtons().at(0);
item.trigger('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted().click[0][0]).toBe(itemData);
- });
+ await nextTick();
+ expect(wrapper.emitted().click[0][0]).toBe(itemData);
});
it('renders dropdown title', () => {
@@ -112,7 +110,7 @@ describe('IDE file templates dropdown component', () => {
expect(items.wrappers.map((x) => x.text())).toEqual(templates.map((x) => x.name));
});
- it('searches template data', () => {
+ it('searches template data', async () => {
const templates = [{ name: 'match 1' }, { name: 'other' }, { name: 'match 2' }];
const matches = ['match 1', 'match 2'];
createComponent({
@@ -120,12 +118,11 @@ describe('IDE file templates dropdown component', () => {
state: { templates },
});
findSearch().setValue('match');
- return wrapper.vm.$nextTick().then(() => {
- const items = findItemButtons();
+ await nextTick();
+ const items = findItemButtons();
- expect(items.length).toBe(matches.length);
- expect(items.wrappers.map((x) => x.text())).toEqual(matches);
- });
+ expect(items.length).toBe(matches.length);
+ expect(items.wrappers.map((x) => x.text())).toEqual(matches);
});
it('does not render input when `searchable` is true & `showLoading` is true', () => {
@@ -160,17 +157,16 @@ describe('IDE file templates dropdown component', () => {
expect(findSearch().exists()).toBe(true);
});
- it('searches data', () => {
+ it('searches data', async () => {
const data = [{ name: 'match 1' }, { name: 'other' }, { name: 'match 2' }];
const matches = ['match 1', 'match 2'];
createComponent({ props: { searchable: true, data } });
findSearch().setValue('match');
- return wrapper.vm.$nextTick().then(() => {
- const items = findItemButtons();
+ await nextTick();
+ const items = findItemButtons();
- expect(items.length).toBe(matches.length);
- expect(items.wrappers.map((x) => x.text())).toEqual(matches);
- });
+ expect(items.length).toBe(matches.length);
+ expect(items.wrappers.map((x) => x.text())).toEqual(matches);
});
});
});
diff --git a/spec/frontend/ide/components/ide_file_row_spec.js b/spec/frontend/ide/components/ide_file_row_spec.js
index 20c105460f2..baf3d7cca9d 100644
--- a/spec/frontend/ide/components/ide_file_row_spec.js
+++ b/spec/frontend/ide/components/ide_file_row_spec.js
@@ -1,12 +1,12 @@
-import { createLocalVue, mount } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import FileRowExtra from '~/ide/components/file_row_extra.vue';
import IdeFileRow from '~/ide/components/ide_file_row.vue';
import { createStore } from '~/ide/stores';
import FileRow from '~/vue_shared/components/file_row.vue';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
const TEST_EXTRA_PROPS = {
testattribute: 'abc',
@@ -30,7 +30,6 @@ describe('Ide File Row component', () => {
...props,
},
store: createStore(),
- localVue,
...options,
});
};
@@ -44,7 +43,7 @@ describe('Ide File Row component', () => {
const findFileRow = () => wrapper.find(FileRow);
const hasDropdownOpen = () => findFileRowExtra().props('dropdownOpen');
- it('fileRow component has listeners', () => {
+ it('fileRow component has listeners', async () => {
const toggleTreeOpen = jest.fn();
createComponent(
{},
@@ -57,9 +56,8 @@ describe('Ide File Row component', () => {
findFileRow().vm.$emit('toggleTreeOpen');
- return wrapper.vm.$nextTick().then(() => {
- expect(toggleTreeOpen).toHaveBeenCalled();
- });
+ await nextTick();
+ expect(toggleTreeOpen).toHaveBeenCalled();
});
describe('default', () => {
@@ -86,32 +84,30 @@ describe('Ide File Row component', () => {
});
describe('with open dropdown', () => {
- beforeEach(() => {
+ beforeEach(async () => {
createComponent();
findFileRowExtra().vm.$emit('toggle', true);
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('shows open dropdown', () => {
expect(hasDropdownOpen()).toBe(true);
});
- it('hides dropdown when mouseleave', () => {
+ it('hides dropdown when mouseleave', async () => {
findFileRow().vm.$emit('mouseleave');
- return wrapper.vm.$nextTick().then(() => {
- expect(hasDropdownOpen()).toEqual(false);
- });
+ await nextTick();
+ expect(hasDropdownOpen()).toEqual(false);
});
- it('hides dropdown on toggle', () => {
+ it('hides dropdown on toggle', async () => {
findFileRowExtra().vm.$emit('toggle', false);
- return wrapper.vm.$nextTick().then(() => {
- expect(hasDropdownOpen()).toEqual(false);
- });
+ await nextTick();
+ expect(hasDropdownOpen()).toEqual(false);
});
});
});
diff --git a/spec/frontend/ide/components/ide_review_spec.js b/spec/frontend/ide/components/ide_review_spec.js
index 7a92f59641f..13d20761263 100644
--- a/spec/frontend/ide/components/ide_review_spec.js
+++ b/spec/frontend/ide/components/ide_review_spec.js
@@ -1,5 +1,5 @@
-import { createLocalVue, mount } from '@vue/test-utils';
-import Vue from 'vue';
+import { mount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { keepAlive } from 'helpers/keep_alive_component_helper';
import { trimText } from 'helpers/text_helper';
@@ -9,8 +9,7 @@ import { createStore } from '~/ide/stores';
import { file } from '../helpers';
import { projectData } from '../mock_data';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('IDE review mode', () => {
let wrapper;
@@ -28,7 +27,6 @@ describe('IDE review mode', () => {
wrapper = mount(keepAlive(IdeReview), {
store,
- localVue,
});
});
@@ -76,14 +74,14 @@ describe('IDE review mode', () => {
});
describe('merge request', () => {
- beforeEach(() => {
+ beforeEach(async () => {
store.state.currentMergeRequestId = '1';
store.state.projects.abcproject.mergeRequests['1'] = {
iid: 123,
web_url: 'testing123',
};
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('renders edit dropdown', () => {
@@ -93,7 +91,7 @@ describe('IDE review mode', () => {
it('renders merge request link & IID', async () => {
store.state.viewer = 'mrdiff';
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(trimText(wrapper.text())).toContain('Merge request (!123)');
});
@@ -101,7 +99,7 @@ describe('IDE review mode', () => {
it('changes text to latest changes when viewer is not mrdiff', async () => {
store.state.viewer = 'diff';
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.text()).toContain('Latest changes');
});
diff --git a/spec/frontend/ide/components/ide_side_bar_spec.js b/spec/frontend/ide/components/ide_side_bar_spec.js
index c683612b142..34f14ef23a4 100644
--- a/spec/frontend/ide/components/ide_side_bar_spec.js
+++ b/spec/frontend/ide/components/ide_side_bar_spec.js
@@ -1,5 +1,6 @@
import { GlSkeletonLoading } from '@gitlab/ui';
-import { mount, createLocalVue } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import waitForPromises from 'helpers/wait_for_promises';
import IdeReview from '~/ide/components/ide_review.vue';
@@ -10,8 +11,7 @@ import { leftSidebarViews } from '~/ide/constants';
import { createStore } from '~/ide/stores';
import { projectData } from '../mock_data';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('IdeSidebar', () => {
let wrapper;
@@ -26,7 +26,6 @@ describe('IdeSidebar', () => {
return mount(IdeSidebar, {
store,
- localVue,
});
}
@@ -46,7 +45,7 @@ describe('IdeSidebar', () => {
store.state.loading = true;
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.findAll(GlSkeletonLoading)).toHaveLength(3);
});
@@ -61,7 +60,7 @@ describe('IdeSidebar', () => {
store.state.currentActivityView = leftSidebarViews.review.name;
await waitForPromises();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.find(IdeTree).exists()).toBe(false);
expect(wrapper.find(IdeReview).exists()).toBe(true);
@@ -69,7 +68,7 @@ describe('IdeSidebar', () => {
store.state.currentActivityView = leftSidebarViews.commit.name;
await waitForPromises();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.find(IdeTree).exists()).toBe(false);
expect(wrapper.find(IdeReview).exists()).toBe(false);
@@ -85,7 +84,7 @@ describe('IdeSidebar', () => {
view,
});
await waitForPromises();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.find(IdeTree).exists()).toBe(tree);
expect(wrapper.find(IdeReview).exists()).toBe(review);
@@ -100,7 +99,7 @@ describe('IdeSidebar', () => {
store.state.currentActivityView = leftSidebarViews.commit.name;
await waitForPromises();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.find(IdeTree).exists()).toBe(false);
expect(wrapper.find(RepoCommitSection).exists()).toBe(true);
@@ -108,7 +107,7 @@ describe('IdeSidebar', () => {
store.state.currentActivityView = leftSidebarViews.edit.name;
await waitForPromises();
- await wrapper.vm.$nextTick();
+ await nextTick();
// reference to the elements remains the same, meaning the components were kept alive
expect(wrapper.find(IdeTree).element).toEqual(ideTreeComponent);
diff --git a/spec/frontend/ide/components/ide_spec.js b/spec/frontend/ide/components/ide_spec.js
index f8d29fc7b47..37b42001a80 100644
--- a/spec/frontend/ide/components/ide_spec.js
+++ b/spec/frontend/ide/components/ide_spec.js
@@ -1,4 +1,5 @@
-import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import waitForPromises from 'helpers/wait_for_promises';
import CannotPushCodeAlert from '~/ide/components/cannot_push_code_alert.vue';
@@ -9,8 +10,7 @@ import { createStore } from '~/ide/stores';
import { file } from '../helpers';
import { projectData } from '../mock_data';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
const TEST_FORK_IDE_PATH = '/test/ide/path';
@@ -34,7 +34,6 @@ describe('WebIDE', () => {
wrapper = shallowMount(Ide, {
store,
- localVue,
});
};
diff --git a/spec/frontend/ide/components/ide_status_bar_spec.js b/spec/frontend/ide/components/ide_status_bar_spec.js
index f1a0b64caf2..00ef75fcf3a 100644
--- a/spec/frontend/ide/components/ide_status_bar_spec.js
+++ b/spec/frontend/ide/components/ide_status_bar_spec.js
@@ -1,5 +1,5 @@
import _ from 'lodash';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import { TEST_HOST } from 'helpers/test_constants';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
import IdeStatusBar from '~/ide/components/ide_status_bar.vue';
@@ -73,7 +73,7 @@ describe('ideStatusBar', () => {
});
describe('pipeline status', () => {
- it('opens right sidebar on clicking icon', (done) => {
+ it('opens right sidebar on clicking icon', async () => {
jest.spyOn(vm, 'openRightPane').mockImplementation(() => {});
Vue.set(vm.$store.state.pipelines, 'latestPipeline', {
details: {
@@ -88,14 +88,10 @@ describe('ideStatusBar', () => {
},
});
- vm.$nextTick()
- .then(() => {
- vm.$el.querySelector('.ide-status-pipeline button').click();
+ await nextTick();
+ vm.$el.querySelector('.ide-status-pipeline button').click();
- expect(vm.openRightPane).toHaveBeenCalledWith(rightSidebarViews.pipelines);
- })
- .then(done)
- .catch(done.fail);
+ expect(vm.openRightPane).toHaveBeenCalledWith(rightSidebarViews.pipelines);
});
});
diff --git a/spec/frontend/ide/components/ide_status_list_spec.js b/spec/frontend/ide/components/ide_status_list_spec.js
index 036edfb3ec1..371fbc6becd 100644
--- a/spec/frontend/ide/components/ide_status_list_spec.js
+++ b/spec/frontend/ide/components/ide_status_list_spec.js
@@ -1,5 +1,6 @@
import { GlLink } from '@gitlab/ui';
-import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import IdeStatusList from '~/ide/components/ide_status_list.vue';
import TerminalSyncStatusSafe from '~/ide/components/terminal_sync/terminal_sync_status_safe.vue';
@@ -16,8 +17,7 @@ const TEST_FILE_EDITOR = {
};
const TEST_EDITOR_POSITION = `${TEST_FILE_EDITOR.editorRow}:${TEST_FILE_EDITOR.editorColumn}`;
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('ide/components/ide_status_list', () => {
let activeFileEditor;
@@ -42,7 +42,6 @@ describe('ide/components/ide_status_list', () => {
});
wrapper = shallowMount(IdeStatusList, {
- localVue,
store,
...options,
});
diff --git a/spec/frontend/ide/components/ide_tree_list_spec.js b/spec/frontend/ide/components/ide_tree_list_spec.js
index ace51204374..a85c52f5e86 100644
--- a/spec/frontend/ide/components/ide_tree_list_spec.js
+++ b/spec/frontend/ide/components/ide_tree_list_spec.js
@@ -1,4 +1,4 @@
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
import IdeTreeList from '~/ide/components/ide_tree_list.vue';
import { createStore } from '~/ide/stores';
@@ -48,15 +48,12 @@ describe('IDE tree list', () => {
expect(vm.$emit).toHaveBeenCalledWith('tree-ready');
});
- it('renders loading indicator', (done) => {
+ it('renders loading indicator', async () => {
store.state.trees['abcproject/main'].loading = true;
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.multi-file-loading-container')).not.toBeNull();
- expect(vm.$el.querySelectorAll('.multi-file-loading-container').length).toBe(3);
-
- done();
- });
+ await nextTick();
+ expect(vm.$el.querySelector('.multi-file-loading-container')).not.toBeNull();
+ expect(vm.$el.querySelectorAll('.multi-file-loading-container').length).toBe(3);
});
it('renders list of files', () => {
diff --git a/spec/frontend/ide/components/ide_tree_spec.js b/spec/frontend/ide/components/ide_tree_spec.js
index 0792b88aeb6..8465ef9f5f3 100644
--- a/spec/frontend/ide/components/ide_tree_spec.js
+++ b/spec/frontend/ide/components/ide_tree_spec.js
@@ -1,4 +1,4 @@
-import { mount, createLocalVue } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import { keepAlive } from 'helpers/keep_alive_component_helper';
@@ -7,8 +7,7 @@ import { createStore } from '~/ide/stores';
import { file } from '../helpers';
import { projectData } from '../mock_data';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('IdeTree', () => {
let store;
@@ -27,7 +26,6 @@ describe('IdeTree', () => {
wrapper = mount(keepAlive(IdeTree), {
store,
- localVue,
});
});
diff --git a/spec/frontend/ide/components/jobs/detail_spec.js b/spec/frontend/ide/components/jobs/detail_spec.js
index 3634599f328..9122471d421 100644
--- a/spec/frontend/ide/components/jobs/detail_spec.js
+++ b/spec/frontend/ide/components/jobs/detail_spec.js
@@ -1,4 +1,4 @@
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import { TEST_HOST } from 'helpers/test_constants';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
import JobDetail from '~/ide/components/jobs/detail.vue';
@@ -48,14 +48,11 @@ describe('IDE jobs detail view', () => {
expect(vm.$el.querySelector('.bash').textContent).toContain('testing');
});
- it('renders empty message output', (done) => {
+ it('renders empty message output', async () => {
vm.$store.state.pipelines.detailJob.output = '';
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.bash').textContent).toContain('No messages were logged');
-
- done();
- });
+ await nextTick();
+ expect(vm.$el.querySelector('.bash').textContent).toContain('No messages were logged');
});
it('renders loading icon', () => {
@@ -68,14 +65,11 @@ describe('IDE jobs detail view', () => {
expect(vm.$el.querySelector('.bash').style.display).toBe('none');
});
- it('hide loading icon when isLoading is false', (done) => {
+ it('hide loading icon when isLoading is false', async () => {
vm.$store.state.pipelines.detailJob.isLoading = false;
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.build-loader-animation').style.display).toBe('none');
-
- done();
- });
+ await nextTick();
+ expect(vm.$el.querySelector('.build-loader-animation').style.display).toBe('none');
});
it('resets detailJob when clicking header button', () => {
@@ -107,17 +101,16 @@ describe('IDE jobs detail view', () => {
fnName | btnName | scrollPos
${'scrollDown'} | ${'down'} | ${0}
${'scrollUp'} | ${'up'} | ${1}
- `('triggers $fnName when clicking $btnName button', ({ fnName, scrollPos }) => {
+ `('triggers $fnName when clicking $btnName button', async ({ fnName, scrollPos }) => {
jest.spyOn(vm, fnName).mockImplementation();
vm = vm.$mount();
vm.scrollPos = scrollPos;
- return vm.$nextTick().then(() => {
- vm.$el.querySelector('.btn-scroll:not([disabled])').click();
- expect(vm[fnName]).toHaveBeenCalled();
- });
+ await nextTick();
+ vm.$el.querySelector('.btn-scroll:not([disabled])').click();
+ expect(vm[fnName]).toHaveBeenCalled();
});
});
diff --git a/spec/frontend/ide/components/jobs/item_spec.js b/spec/frontend/ide/components/jobs/item_spec.js
index 7343fc80a03..c76760a5522 100644
--- a/spec/frontend/ide/components/jobs/item_spec.js
+++ b/spec/frontend/ide/components/jobs/item_spec.js
@@ -1,4 +1,4 @@
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import JobItem from '~/ide/components/jobs/item.vue';
import { jobs } from '../../mock_data';
@@ -27,13 +27,10 @@ describe('IDE jobs item', () => {
expect(vm.$el.querySelector('[data-testid="status_success_borderless-icon"]')).not.toBe(null);
});
- it('does not render view logs button if not started', (done) => {
+ it('does not render view logs button if not started', async () => {
vm.job.started = false;
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.btn')).toBe(null);
-
- done();
- });
+ await nextTick();
+ expect(vm.$el.querySelector('.btn')).toBe(null);
});
});
diff --git a/spec/frontend/ide/components/jobs/list_spec.js b/spec/frontend/ide/components/jobs/list_spec.js
index 8797e07aef1..cb2c9f8f04f 100644
--- a/spec/frontend/ide/components/jobs/list_spec.js
+++ b/spec/frontend/ide/components/jobs/list_spec.js
@@ -1,11 +1,11 @@
import { GlLoadingIcon } from '@gitlab/ui';
-import { shallowMount, mount, createLocalVue } from '@vue/test-utils';
+import { shallowMount, mount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import StageList from '~/ide/components/jobs/list.vue';
import Stage from '~/ide/components/jobs/stage.vue';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
const storeActions = {
fetchJobs: jest.fn(),
toggleStageCollapsed: jest.fn(),
@@ -42,7 +42,6 @@ describe('IDE stages list', () => {
...defaultProps,
...props,
},
- localVue,
store,
});
};
@@ -92,7 +91,6 @@ describe('IDE stages list', () => {
wrapper = mount(StageList, {
propsData: { ...defaultProps, stages },
store,
- localVue,
});
});
diff --git a/spec/frontend/ide/components/jobs/stage_spec.js b/spec/frontend/ide/components/jobs/stage_spec.js
index 9accd81a2ba..f158c59cd32 100644
--- a/spec/frontend/ide/components/jobs/stage_spec.js
+++ b/spec/frontend/ide/components/jobs/stage_spec.js
@@ -1,5 +1,6 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import Item from '~/ide/components/jobs/item.vue';
import Stage from '~/ide/components/jobs/stage.vue';
import { stages, jobs } from '../../mock_data';
@@ -47,23 +48,21 @@ describe('IDE pipeline stage', () => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
- it('emits toggleCollaped event with stage id when clicking header', () => {
+ it('emits toggleCollaped event with stage id when clicking header', async () => {
const id = 5;
createComponent({ stage: { ...defaultProps.stage, id } });
findHeader().trigger('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted().toggleCollapsed[0][0]).toBe(id);
- });
+ await nextTick();
+ expect(wrapper.emitted().toggleCollapsed[0][0]).toBe(id);
});
- it('emits clickViewLog entity with job', () => {
+ it('emits clickViewLog entity with job', async () => {
const [job] = defaultProps.stage.jobs;
createComponent();
wrapper.findAll(Item).at(0).vm.$emit('clickViewLog', job);
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted().clickViewLog[0][0]).toBe(job);
- });
+ await nextTick();
+ expect(wrapper.emitted().clickViewLog[0][0]).toBe(job);
});
it('renders stage details & icon', () => {
diff --git a/spec/frontend/ide/components/merge_requests/item_spec.js b/spec/frontend/ide/components/merge_requests/item_spec.js
index f0a97a0b10a..d6cf8127b53 100644
--- a/spec/frontend/ide/components/merge_requests/item_spec.js
+++ b/spec/frontend/ide/components/merge_requests/item_spec.js
@@ -1,4 +1,5 @@
-import { mount, createLocalVue } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import Item from '~/ide/components/merge_requests/item.vue';
import { createRouter } from '~/ide/ide_router';
@@ -11,8 +12,7 @@ const TEST_ITEM = {
};
describe('IDE merge request item', () => {
- const localVue = createLocalVue();
- localVue.use(Vuex);
+ Vue.use(Vuex);
let wrapper;
let store;
@@ -28,7 +28,6 @@ describe('IDE merge request item', () => {
currentProjectId: TEST_ITEM.projectPathWithNamespace,
...props,
},
- localVue,
router,
store,
});
diff --git a/spec/frontend/ide/components/merge_requests/list_spec.js b/spec/frontend/ide/components/merge_requests/list_spec.js
index 610e20d5868..583671a0af6 100644
--- a/spec/frontend/ide/components/merge_requests/list_spec.js
+++ b/spec/frontend/ide/components/merge_requests/list_spec.js
@@ -1,13 +1,13 @@
import { GlLoadingIcon } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import Item from '~/ide/components/merge_requests/item.vue';
import List from '~/ide/components/merge_requests/list.vue';
import TokenedInput from '~/ide/components/shared/tokened_input.vue';
import { mergeRequests as mergeRequestsMock } from '../../mock_data';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('IDE merge requests list', () => {
let wrapper;
@@ -41,7 +41,6 @@ describe('IDE merge requests list', () => {
wrapper = shallowMount(List, {
store: fakeStore,
- localVue,
});
};
@@ -67,33 +66,28 @@ describe('IDE merge requests list', () => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
- it('renders no search results text when search is not empty', () => {
+ it('renders no search results text when search is not empty', async () => {
createComponent();
findTokenedInput().vm.$emit('input', 'something');
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.text()).toContain('No merge requests found');
- });
+ await nextTick();
+ expect(wrapper.text()).toContain('No merge requests found');
});
- it('clicking on search type, sets currentSearchType and loads merge requests', () => {
+ it('clicking on search type, sets currentSearchType and loads merge requests', async () => {
createComponent();
findTokenedInput().vm.$emit('focus');
- return wrapper.vm
- .$nextTick()
- .then(() => {
- findSearchTypeButtons().at(0).trigger('click');
- return wrapper.vm.$nextTick();
- })
- .then(() => {
- const searchType = wrapper.vm.$options.searchTypes[0];
+ await nextTick();
+ findSearchTypeButtons().at(0).trigger('click');
- expect(findTokenedInput().props('tokens')).toEqual([searchType]);
- expect(fetchMergeRequestsMock).toHaveBeenCalledWith(expect.any(Object), {
- type: searchType.type,
- search: '',
- });
- });
+ await nextTick();
+ const searchType = wrapper.vm.$options.searchTypes[0];
+
+ expect(findTokenedInput().props('tokens')).toEqual([searchType]);
+ expect(fetchMergeRequestsMock).toHaveBeenCalledWith(expect.any(Object), {
+ type: searchType.type,
+ search: '',
+ });
});
describe('with merge requests', () => {
@@ -120,16 +114,15 @@ describe('IDE merge requests list', () => {
});
describe('when searching merge requests', () => {
- it('calls `loadMergeRequests` on input in search field', () => {
+ it('calls `loadMergeRequests` on input in search field', async () => {
createComponent(defaultStateWithMergeRequests);
const input = findTokenedInput();
input.vm.$emit('input', 'something');
- return wrapper.vm.$nextTick().then(() => {
- expect(fetchMergeRequestsMock).toHaveBeenCalledWith(expect.any(Object), {
- search: 'something',
- type: '',
- });
+ await nextTick();
+ expect(fetchMergeRequestsMock).toHaveBeenCalledWith(expect.any(Object), {
+ search: 'something',
+ type: '',
});
});
});
@@ -144,9 +137,9 @@ describe('IDE merge requests list', () => {
});
describe('without search value', () => {
- beforeEach(() => {
+ beforeEach(async () => {
input.vm.$emit('focus');
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('shows search types', () => {
@@ -156,22 +149,20 @@ describe('IDE merge requests list', () => {
);
});
- it('hides search types when search changes', () => {
+ it('hides search types when search changes', async () => {
input.vm.$emit('input', 'something');
- return wrapper.vm.$nextTick().then(() => {
- expect(findSearchTypeButtons().exists()).toBe(false);
- });
+ await nextTick();
+ expect(findSearchTypeButtons().exists()).toBe(false);
});
describe('with search type', () => {
- beforeEach(() => {
+ beforeEach(async () => {
findSearchTypeButtons().at(0).trigger('click');
- return wrapper.vm
- .$nextTick()
- .then(() => input.vm.$emit('focus'))
- .then(() => wrapper.vm.$nextTick());
+ await nextTick();
+ await input.vm.$emit('focus');
+ await nextTick();
});
it('does not show search types', () => {
@@ -181,10 +172,10 @@ describe('IDE merge requests list', () => {
});
describe('with search value', () => {
- beforeEach(() => {
+ beforeEach(async () => {
input.vm.$emit('input', 'something');
input.vm.$emit('focus');
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('does not show search types', () => {
diff --git a/spec/frontend/ide/components/nav_dropdown_button_spec.js b/spec/frontend/ide/components/nav_dropdown_button_spec.js
index a02bfa5c391..1c14685df68 100644
--- a/spec/frontend/ide/components/nav_dropdown_button_spec.js
+++ b/spec/frontend/ide/components/nav_dropdown_button_spec.js
@@ -1,4 +1,4 @@
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import { trimText } from 'helpers/text_helper';
import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
import NavDropdownButton from '~/ide/components/nav_dropdown_button.vue';
@@ -36,38 +36,26 @@ describe('NavDropdown', () => {
expect(trimText(vm.$el.textContent)).toEqual('- -');
});
- it('renders branch name, if state has currentBranchId', (done) => {
+ it('renders branch name, if state has currentBranchId', async () => {
vm.$store.state.currentBranchId = TEST_BRANCH_ID;
- vm.$nextTick()
- .then(() => {
- expect(trimText(vm.$el.textContent)).toEqual(`${TEST_BRANCH_ID} -`);
- })
- .then(done)
- .catch(done.fail);
+ await nextTick();
+ expect(trimText(vm.$el.textContent)).toEqual(`${TEST_BRANCH_ID} -`);
});
- it('renders mr id, if state has currentMergeRequestId', (done) => {
+ it('renders mr id, if state has currentMergeRequestId', async () => {
vm.$store.state.currentMergeRequestId = TEST_MR_ID;
- vm.$nextTick()
- .then(() => {
- expect(trimText(vm.$el.textContent)).toEqual(`- !${TEST_MR_ID}`);
- })
- .then(done)
- .catch(done.fail);
+ await nextTick();
+ expect(trimText(vm.$el.textContent)).toEqual(`- !${TEST_MR_ID}`);
});
- it('renders branch and mr, if state has both', (done) => {
+ it('renders branch and mr, if state has both', async () => {
vm.$store.state.currentBranchId = TEST_BRANCH_ID;
vm.$store.state.currentMergeRequestId = TEST_MR_ID;
- vm.$nextTick()
- .then(() => {
- expect(trimText(vm.$el.textContent)).toEqual(`${TEST_BRANCH_ID} !${TEST_MR_ID}`);
- })
- .then(done)
- .catch(done.fail);
+ await nextTick();
+ expect(trimText(vm.$el.textContent)).toEqual(`${TEST_BRANCH_ID} !${TEST_MR_ID}`);
});
it('shows icons', () => {
diff --git a/spec/frontend/ide/components/nav_dropdown_spec.js b/spec/frontend/ide/components/nav_dropdown_spec.js
index 6a1be7ee964..33e638843f5 100644
--- a/spec/frontend/ide/components/nav_dropdown_spec.js
+++ b/spec/frontend/ide/components/nav_dropdown_spec.js
@@ -1,5 +1,6 @@
import { mount } from '@vue/test-utils';
import $ from 'jquery';
+import { nextTick } from 'vue';
import NavDropdown from '~/ide/components/nav_dropdown.vue';
import { PERMISSION_READ_MR } from '~/ide/constants';
import { createStore } from '~/ide/stores';
@@ -58,29 +59,19 @@ describe('IDE NavDropdown', () => {
expect(findNavForm().exists()).toBe(false);
});
- it('renders nav form when show.bs.dropdown', (done) => {
+ it('renders nav form when show.bs.dropdown', async () => {
showDropdown();
- wrapper.vm
- .$nextTick()
- .then(() => {
- expect(findNavForm().exists()).toBe(true);
- })
- .then(done)
- .catch(done.fail);
+ await nextTick();
+ expect(findNavForm().exists()).toBe(true);
});
- it('destroys nav form when closed', (done) => {
+ it('destroys nav form when closed', async () => {
showDropdown();
hideDropdown();
- wrapper.vm
- .$nextTick()
- .then(() => {
- expect(findNavForm().exists()).toBe(false);
- })
- .then(done)
- .catch(done.fail);
+ await nextTick();
+ expect(findNavForm().exists()).toBe(false);
});
it('renders merge request icon', () => {
diff --git a/spec/frontend/ide/components/new_dropdown/button_spec.js b/spec/frontend/ide/components/new_dropdown/button_spec.js
index 32fa2babcdb..298d7b810e1 100644
--- a/spec/frontend/ide/components/new_dropdown/button_spec.js
+++ b/spec/frontend/ide/components/new_dropdown/button_spec.js
@@ -1,4 +1,4 @@
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import Button from '~/ide/components/new_dropdown/button.vue';
@@ -37,14 +37,11 @@ describe('IDE new entry dropdown button component', () => {
expect(vm.$emit).toHaveBeenCalledWith('click');
});
- it('hides label if showLabel is false', (done) => {
+ it('hides label if showLabel is false', async () => {
vm.showLabel = false;
- vm.$nextTick(() => {
- expect(vm.$el.textContent).not.toContain('Testing');
-
- done();
- });
+ await nextTick();
+ expect(vm.$el.textContent).not.toContain('Testing');
});
describe('tooltipTitle', () => {
@@ -52,14 +49,11 @@ describe('IDE new entry dropdown button component', () => {
expect(vm.tooltipTitle).toBe('');
});
- it('returns label', (done) => {
+ it('returns label', async () => {
vm.showLabel = false;
- vm.$nextTick(() => {
- expect(vm.tooltipTitle).toBe('Testing');
-
- done();
- });
+ await nextTick();
+ expect(vm.tooltipTitle).toBe('Testing');
});
});
});
diff --git a/spec/frontend/ide/components/new_dropdown/index_spec.js b/spec/frontend/ide/components/new_dropdown/index_spec.js
index fa34d1b257f..19dcd9569b3 100644
--- a/spec/frontend/ide/components/new_dropdown/index_spec.js
+++ b/spec/frontend/ide/components/new_dropdown/index_spec.js
@@ -1,4 +1,4 @@
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
import newDropdown from '~/ide/components/new_dropdown/index.vue';
import { createStore } from '~/ide/stores';
@@ -57,17 +57,15 @@ describe('new dropdown component', () => {
});
describe('isOpen', () => {
- it('scrolls dropdown into view', (done) => {
+ it('scrolls dropdown into view', async () => {
jest.spyOn(vm.$refs.dropdownMenu, 'scrollIntoView').mockImplementation(() => {});
vm.isOpen = true;
- setImmediate(() => {
- expect(vm.$refs.dropdownMenu.scrollIntoView).toHaveBeenCalledWith({
- block: 'nearest',
- });
+ await nextTick();
- done();
+ expect(vm.$refs.dropdownMenu.scrollIntoView).toHaveBeenCalledWith({
+ block: 'nearest',
});
});
});
diff --git a/spec/frontend/ide/components/new_dropdown/modal_spec.js b/spec/frontend/ide/components/new_dropdown/modal_spec.js
index 41111f5dbb4..8134248bbf4 100644
--- a/spec/frontend/ide/components/new_dropdown/modal_spec.js
+++ b/spec/frontend/ide/components/new_dropdown/modal_spec.js
@@ -1,4 +1,4 @@
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
import createFlash from '~/flash';
import modal from '~/ide/components/new_dropdown/modal.vue';
@@ -19,14 +19,14 @@ describe('new file modal component', () => {
${'tree'} | ${'Create new directory'} | ${'Create directory'} | ${false}
${'blob'} | ${'Create new file'} | ${'Create file'} | ${true}
`('$entryType', ({ entryType, modalTitle, btnTitle, showsFileTemplates }) => {
- beforeEach((done) => {
+ beforeEach(async () => {
const store = createStore();
vm = createComponentWithStore(Component, store).$mount();
vm.open(entryType);
vm.name = 'testing';
- vm.$nextTick(done);
+ await nextTick();
});
afterEach(() => {
@@ -71,16 +71,13 @@ describe('new file modal component', () => {
${'blob'} | ${'Rename file'} | ${'Rename file'}
`(
'renders title and button for renaming $entryType',
- ({ entryType, modalTitle, btnTitle }, done) => {
+ async ({ entryType, modalTitle, btnTitle }) => {
vm.$store.state.entries['test-path'].type = entryType;
vm.open('rename', 'test-path');
- vm.$nextTick(() => {
- expect(document.querySelector('.modal-title').textContent.trim()).toBe(modalTitle);
- expect(document.querySelector('.btn-success').textContent.trim()).toBe(btnTitle);
-
- done();
- });
+ await nextTick();
+ expect(document.querySelector('.modal-title').textContent.trim()).toBe(modalTitle);
+ expect(document.querySelector('.btn-success').textContent.trim()).toBe(btnTitle);
},
);
diff --git a/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js b/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js
index 7216f50b05c..7f2ee0fe7d9 100644
--- a/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js
+++ b/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js
@@ -1,12 +1,12 @@
-import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import IdeSidebarNav from '~/ide/components/ide_sidebar_nav.vue';
import CollapsibleSidebar from '~/ide/components/panes/collapsible_sidebar.vue';
import { createStore } from '~/ide/stores';
import paneModule from '~/ide/stores/modules/pane';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('ide/components/panes/collapsible_sidebar.vue', () => {
let wrapper;
@@ -17,7 +17,6 @@ describe('ide/components/panes/collapsible_sidebar.vue', () => {
const createComponent = (props) => {
wrapper = shallowMount(CollapsibleSidebar, {
- localVue,
store,
propsData: {
extensionTabs: [],
@@ -46,7 +45,7 @@ describe('ide/components/panes/collapsible_sidebar.vue', () => {
let extensionTabs;
beforeEach(() => {
- const FakeComponent = localVue.component(fakeComponentName, {
+ const FakeComponent = Vue.component(fakeComponentName, {
render: () => null,
});
diff --git a/spec/frontend/ide/components/panes/right_spec.js b/spec/frontend/ide/components/panes/right_spec.js
index c6231d129ff..d12acd6dc4c 100644
--- a/spec/frontend/ide/components/panes/right_spec.js
+++ b/spec/frontend/ide/components/panes/right_spec.js
@@ -1,5 +1,5 @@
-import { createLocalVue, shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import CollapsibleSidebar from '~/ide/components/panes/collapsible_sidebar.vue';
import RightPane from '~/ide/components/panes/right.vue';
@@ -7,8 +7,7 @@ import { rightSidebarViews } from '~/ide/constants';
import { createStore } from '~/ide/stores';
import extendStore from '~/ide/stores/extend';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('ide/components/panes/right.vue', () => {
let wrapper;
@@ -18,7 +17,6 @@ describe('ide/components/panes/right.vue', () => {
extendStore(store, document.createElement('div'));
wrapper = shallowMount(RightPane, {
- localVue,
store,
propsData: {
...props,
@@ -88,19 +86,18 @@ describe('ide/components/panes/right.vue', () => {
createComponent();
});
- it('adds terminal tab', () => {
+ it('adds terminal tab', async () => {
store.state.terminal.isVisible = true;
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(CollapsibleSidebar).props('extensionTabs')).toEqual(
- expect.arrayContaining([
- expect.objectContaining({
- show: true,
- title: 'Terminal',
- }),
- ]),
- );
- });
+ await nextTick();
+ expect(wrapper.find(CollapsibleSidebar).props('extensionTabs')).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ show: true,
+ title: 'Terminal',
+ }),
+ ]),
+ );
});
it('hides terminal tab when not visible', () => {
diff --git a/spec/frontend/ide/components/preview/clientside_spec.js b/spec/frontend/ide/components/preview/clientside_spec.js
index b168eec0f16..426fbd5c04c 100644
--- a/spec/frontend/ide/components/preview/clientside_spec.js
+++ b/spec/frontend/ide/components/preview/clientside_spec.js
@@ -1,16 +1,19 @@
import { GlLoadingIcon } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
+import { dispatch } from 'codesandbox-api';
import smooshpack from 'smooshpack';
import Vuex from 'vuex';
+import waitForPromises from 'helpers/wait_for_promises';
import Clientside from '~/ide/components/preview/clientside.vue';
+import { PING_USAGE_PREVIEW_KEY, PING_USAGE_PREVIEW_SUCCESS_KEY } from '~/ide/constants';
import eventHub from '~/ide/eventhub';
jest.mock('smooshpack', () => ({
Manager: jest.fn(),
}));
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
const dummyPackageJson = () => ({
raw: JSON.stringify({
@@ -39,8 +42,7 @@ describe('IDE clientside preview', () => {
const storeClientsideActions = {
pingUsage: jest.fn().mockReturnValue(Promise.resolve({})),
};
-
- const waitForCalls = () => new Promise(setImmediate);
+ const dispatchCodesandboxReady = () => dispatch({ type: 'done' });
const createComponent = ({ state, getters } = {}) => {
store = new Vuex.Store({
@@ -67,7 +69,6 @@ describe('IDE clientside preview', () => {
wrapper = shallowMount(Clientside, {
store,
- localVue,
});
};
@@ -98,7 +99,7 @@ describe('IDE clientside preview', () => {
beforeEach(() => {
createComponent({ getters: { packageJson: dummyPackageJson } });
- return waitForCalls();
+ return waitForPromises();
});
it('creates sandpack manager', () => {
@@ -111,6 +112,20 @@ describe('IDE clientside preview', () => {
it('pings usage', () => {
expect(storeClientsideActions.pingUsage).toHaveBeenCalledTimes(1);
+ expect(storeClientsideActions.pingUsage).toHaveBeenCalledWith(
+ expect.anything(),
+ PING_USAGE_PREVIEW_KEY,
+ );
+ });
+
+ it('pings usage success', async () => {
+ dispatchCodesandboxReady();
+ await nextTick();
+ expect(storeClientsideActions.pingUsage).toHaveBeenCalledTimes(2);
+ expect(storeClientsideActions.pingUsage).toHaveBeenCalledWith(
+ expect.anything(),
+ PING_USAGE_PREVIEW_SUCCESS_KEY,
+ );
});
});
@@ -123,7 +138,7 @@ describe('IDE clientside preview', () => {
state: { codesandboxBundlerUrl: TEST_BUNDLER_URL },
});
- return waitForCalls();
+ return waitForPromises();
});
it('creates sandpack manager with bundlerURL', () => {
@@ -138,7 +153,7 @@ describe('IDE clientside preview', () => {
beforeEach(() => {
createComponent({ getters: { packageJson: dummyPackageJson } });
- return waitForCalls();
+ return waitForPromises();
});
it('creates sandpack manager', () => {
@@ -324,7 +339,7 @@ describe('IDE clientside preview', () => {
wrapper.setData({ sandpackReady: true });
wrapper.vm.update();
- return waitForCalls().then(() => {
+ return waitForPromises().then(() => {
expect(smooshpack.Manager).toHaveBeenCalled();
});
});
@@ -352,39 +367,36 @@ describe('IDE clientside preview', () => {
});
describe('template', () => {
- it('renders ide-preview element when showPreview is true', () => {
+ it('renders ide-preview element when showPreview is true', async () => {
createComponent({ getters: { packageJson: dummyPackageJson } });
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ loading: false });
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.find('#ide-preview').exists()).toBe(true);
- });
+ await nextTick();
+ expect(wrapper.find('#ide-preview').exists()).toBe(true);
});
- it('renders empty state', () => {
+ it('renders empty state', async () => {
createComponent();
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ loading: false });
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.text()).toContain(
- 'Preview your web application using Web IDE client-side evaluation.',
- );
- });
+ await nextTick();
+ expect(wrapper.text()).toContain(
+ 'Preview your web application using Web IDE client-side evaluation.',
+ );
});
- it('renders loading icon', () => {
+ it('renders loading icon', async () => {
createComponent();
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ loading: true });
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
- });
+ await nextTick();
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
});
diff --git a/spec/frontend/ide/components/preview/navigator_spec.js b/spec/frontend/ide/components/preview/navigator_spec.js
index ee760364c7e..a199f4704f7 100644
--- a/spec/frontend/ide/components/preview/navigator_spec.js
+++ b/spec/frontend/ide/components/preview/navigator_spec.js
@@ -1,6 +1,7 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { listen } from 'codesandbox-api';
+import { nextTick } from 'vue';
import { TEST_HOST } from 'helpers/test_constants';
import ClientsideNavigator from '~/ide/components/preview/navigator.vue';
@@ -29,31 +30,28 @@ describe('IDE clientside preview navigator', () => {
wrapper.destroy();
});
- it('renders readonly URL bar', () => {
+ it('renders readonly URL bar', async () => {
listenHandler({ type: 'urlchange', url: manager.bundlerURL });
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.find('input[readonly]').element.value).toBe('/');
- });
+ await nextTick();
+ expect(wrapper.find('input[readonly]').element.value).toBe('/');
});
it('renders loading icon by default', () => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
- it('removes loading icon when done event is fired', () => {
+ it('removes loading icon when done event is fired', async () => {
listenHandler({ type: 'done' });
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
- });
+ await nextTick();
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
});
- it('does not count visiting same url multiple times', () => {
+ it('does not count visiting same url multiple times', async () => {
listenHandler({ type: 'done' });
listenHandler({ type: 'done', url: `${TEST_HOST}/url1` });
listenHandler({ type: 'done', url: `${TEST_HOST}/url1` });
- return wrapper.vm.$nextTick().then(() => {
- expect(findBackButton().attributes('disabled')).toBe('disabled');
- });
+ await nextTick();
+ expect(findBackButton().attributes('disabled')).toBe('disabled');
});
it('unsubscribes from listen on destroy', () => {
@@ -64,107 +62,93 @@ describe('IDE clientside preview navigator', () => {
});
describe('back button', () => {
- beforeEach(() => {
+ beforeEach(async () => {
listenHandler({ type: 'done' });
listenHandler({ type: 'urlchange', url: TEST_HOST });
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('is disabled by default', () => {
expect(findBackButton().attributes('disabled')).toBe('disabled');
});
- it('is enabled when there is previous entry', () => {
+ it('is enabled when there is previous entry', async () => {
listenHandler({ type: 'urlchange', url: `${TEST_HOST}/url1` });
- return wrapper.vm.$nextTick().then(() => {
- findBackButton().trigger('click');
- expect(findBackButton().attributes('disabled')).toBeFalsy();
- });
+ await nextTick();
+ findBackButton().trigger('click');
+ expect(findBackButton().attributes('disabled')).toBeFalsy();
});
- it('is disabled when there is no previous entry', () => {
+ it('is disabled when there is no previous entry', async () => {
listenHandler({ type: 'urlchange', url: `${TEST_HOST}/url1` });
- return wrapper.vm
- .$nextTick()
- .then(() => {
- findBackButton().trigger('click');
-
- return wrapper.vm.$nextTick();
- })
- .then(() => {
- expect(findBackButton().attributes('disabled')).toBe('disabled');
- });
+
+ await nextTick();
+ findBackButton().trigger('click');
+
+ await nextTick();
+ expect(findBackButton().attributes('disabled')).toBe('disabled');
});
- it('updates manager iframe src', () => {
+ it('updates manager iframe src', async () => {
listenHandler({ type: 'urlchange', url: `${TEST_HOST}/url1` });
listenHandler({ type: 'urlchange', url: `${TEST_HOST}/url2` });
- return wrapper.vm.$nextTick().then(() => {
- findBackButton().trigger('click');
+ await nextTick();
+ findBackButton().trigger('click');
- expect(manager.iframe.src).toBe(`${TEST_HOST}/url1`);
- });
+ expect(manager.iframe.src).toBe(`${TEST_HOST}/url1`);
});
});
describe('forward button', () => {
- beforeEach(() => {
+ beforeEach(async () => {
listenHandler({ type: 'done' });
listenHandler({ type: 'urlchange', url: TEST_HOST });
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('is disabled by default', () => {
expect(findForwardButton().attributes('disabled')).toBe('disabled');
});
- it('is enabled when there is next entry', () => {
+ it('is enabled when there is next entry', async () => {
listenHandler({ type: 'urlchange', url: `${TEST_HOST}/url1` });
- return wrapper.vm
- .$nextTick()
- .then(() => {
- findBackButton().trigger('click');
- return wrapper.vm.$nextTick();
- })
- .then(() => {
- expect(findForwardButton().attributes('disabled')).toBeFalsy();
- });
+
+ await nextTick();
+ findBackButton().trigger('click');
+
+ await nextTick();
+ expect(findForwardButton().attributes('disabled')).toBeFalsy();
});
- it('is disabled when there is no next entry', () => {
+ it('is disabled when there is no next entry', async () => {
listenHandler({ type: 'urlchange', url: `${TEST_HOST}/url1` });
- return wrapper.vm
- .$nextTick()
- .then(() => {
- findBackButton().trigger('click');
- return wrapper.vm.$nextTick();
- })
- .then(() => {
- findForwardButton().trigger('click');
- return wrapper.vm.$nextTick();
- })
- .then(() => {
- expect(findForwardButton().attributes('disabled')).toBe('disabled');
- });
+
+ await nextTick();
+ findBackButton().trigger('click');
+
+ await nextTick();
+ findForwardButton().trigger('click');
+
+ await nextTick();
+ expect(findForwardButton().attributes('disabled')).toBe('disabled');
});
- it('updates manager iframe src', () => {
+ it('updates manager iframe src', async () => {
listenHandler({ type: 'urlchange', url: `${TEST_HOST}/url1` });
listenHandler({ type: 'urlchange', url: `${TEST_HOST}/url2` });
- return wrapper.vm.$nextTick().then(() => {
- findBackButton().trigger('click');
+ await nextTick();
+ findBackButton().trigger('click');
- expect(manager.iframe.src).toBe(`${TEST_HOST}/url1`);
- });
+ expect(manager.iframe.src).toBe(`${TEST_HOST}/url1`);
});
});
describe('refresh button', () => {
const url = `${TEST_HOST}/some_url`;
- beforeEach(() => {
+ beforeEach(async () => {
listenHandler({ type: 'done' });
listenHandler({ type: 'urlchange', url });
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('calls refresh with current path', () => {
diff --git a/spec/frontend/ide/components/repo_editor_spec.js b/spec/frontend/ide/components/repo_editor_spec.js
index 15af2d03704..96c9baeb328 100644
--- a/spec/frontend/ide/components/repo_editor_spec.js
+++ b/spec/frontend/ide/components/repo_editor_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { editor as monacoEditor, Range } from 'monaco-editor';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import '~/behaviors/markdown/render_gfm';
import waitForPromises from 'helpers/wait_for_promises';
@@ -367,17 +367,17 @@ describe('RepoEditor', () => {
expect(vm.$store.state.panelResizing).toBe(false); // default value
vm.$store.state.panelResizing = true;
- await vm.$nextTick();
+ await nextTick();
expect(updateDimensionsSpy).not.toHaveBeenCalled();
vm.$store.state.panelResizing = false;
- await vm.$nextTick();
+ await nextTick();
expect(updateDimensionsSpy).toHaveBeenCalledTimes(1);
vm.$store.state.panelResizing = true;
- await vm.$nextTick();
+ await nextTick();
expect(updateDimensionsSpy).toHaveBeenCalledTimes(1);
});
@@ -387,12 +387,12 @@ describe('RepoEditor', () => {
expect(vm.$store.state.rightPane.isOpen).toBe(false); // default value
vm.$store.state.rightPane.isOpen = true;
- await vm.$nextTick();
+ await nextTick();
expect(updateDimensionsSpy).toHaveBeenCalledTimes(1);
vm.$store.state.rightPane.isOpen = false;
- await vm.$nextTick();
+ await nextTick();
expect(updateDimensionsSpy).toHaveBeenCalledTimes(2);
});
@@ -411,7 +411,7 @@ describe('RepoEditor', () => {
`('tabs in $mode are $isVisible', async ({ mode, isVisible } = {}) => {
vm.$store.state.currentActivityView = leftSidebarViews[mode].name;
- await vm.$nextTick();
+ await nextTick();
expect(wrapper.find('.nav-links').exists()).toBe(isVisible);
});
});
@@ -436,7 +436,7 @@ describe('RepoEditor', () => {
});
changeViewMode(FILE_VIEW_MODE_PREVIEW);
- await vm.$nextTick();
+ await nextTick();
});
it('do not show the editor', () => {
@@ -448,7 +448,7 @@ describe('RepoEditor', () => {
expect(updateDimensionsSpy).not.toHaveBeenCalled();
changeViewMode(FILE_VIEW_MODE_EDITOR);
- await vm.$nextTick();
+ await nextTick();
expect(updateDimensionsSpy).toHaveBeenCalled();
});
@@ -460,7 +460,7 @@ describe('RepoEditor', () => {
jest.spyOn(vm, 'shouldHideEditor', 'get').mockReturnValue(true);
vm.initEditor();
- await vm.$nextTick();
+ await nextTick();
};
it('does not fetch file information for temp entries', async () => {
@@ -511,20 +511,20 @@ describe('RepoEditor', () => {
const origFile = vm.file;
vm.file.pending = true;
- await vm.$nextTick();
+ await nextTick();
wrapper.setProps({
file: file('testing'),
});
vm.file.content = 'foo'; // need to prevent full cycle of initEditor
- await vm.$nextTick();
+ await nextTick();
expect(vm.removePendingTab).toHaveBeenCalledWith(origFile);
});
it('does not call initEditor if the file did not change', async () => {
Vue.set(vm, 'file', vm.file);
- await vm.$nextTick();
+ await nextTick();
expect(vm.initEditor).not.toHaveBeenCalled();
});
@@ -538,7 +538,7 @@ describe('RepoEditor', () => {
key: 'new',
},
});
- await vm.$nextTick();
+ await nextTick();
expect(vm.initEditor).toHaveBeenCalled();
});
diff --git a/spec/frontend/ide/components/repo_tab_spec.js b/spec/frontend/ide/components/repo_tab_spec.js
index 95d52e8f7a9..b16fd8f80ba 100644
--- a/spec/frontend/ide/components/repo_tab_spec.js
+++ b/spec/frontend/ide/components/repo_tab_spec.js
@@ -1,5 +1,6 @@
import { GlTab } from '@gitlab/ui';
-import { mount, createLocalVue } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import { stubComponent } from 'helpers/stub_component';
import RepoTab from '~/ide/components/repo_tab.vue';
@@ -7,8 +8,7 @@ import { createRouter } from '~/ide/ide_router';
import { createStore } from '~/ide/stores';
import { file } from '../helpers';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
const GlTabStub = stubComponent(GlTab, {
template: '<li><slot name="title" /></li>',
@@ -23,7 +23,6 @@ describe('RepoTab', () => {
function createComponent(propsData) {
wrapper = mount(RepoTab, {
- localVue,
store,
propsData,
stubs: {
diff --git a/spec/frontend/ide/components/repo_tabs_spec.js b/spec/frontend/ide/components/repo_tabs_spec.js
index 6ee73b0a437..1cfc1f12745 100644
--- a/spec/frontend/ide/components/repo_tabs_spec.js
+++ b/spec/frontend/ide/components/repo_tabs_spec.js
@@ -1,11 +1,11 @@
-import { mount, createLocalVue } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import RepoTabs from '~/ide/components/repo_tabs.vue';
import { createStore } from '~/ide/stores';
import { file } from '../helpers';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('RepoTabs', () => {
let wrapper;
@@ -22,7 +22,6 @@ describe('RepoTabs', () => {
activeFile: file('activeFile'),
},
store,
- localVue,
});
});
@@ -30,17 +29,14 @@ describe('RepoTabs', () => {
wrapper.destroy();
});
- it('renders a list of tabs', (done) => {
+ it('renders a list of tabs', async () => {
store.state.openFiles[0].active = true;
- wrapper.vm.$nextTick(() => {
- const tabs = [...wrapper.vm.$el.querySelectorAll('.multi-file-tab')];
+ await nextTick();
+ const tabs = [...wrapper.vm.$el.querySelectorAll('.multi-file-tab')];
- expect(tabs.length).toEqual(2);
- expect(tabs[0].parentNode.classList.contains('active')).toEqual(true);
- expect(tabs[1].parentNode.classList.contains('active')).toEqual(false);
-
- done();
- });
+ expect(tabs.length).toEqual(2);
+ expect(tabs[0].parentNode.classList.contains('active')).toEqual(true);
+ expect(tabs[1].parentNode.classList.contains('active')).toEqual(false);
});
});
diff --git a/spec/frontend/ide/components/resizable_panel_spec.js b/spec/frontend/ide/components/resizable_panel_spec.js
index 6a5af52ea35..55b9423aba8 100644
--- a/spec/frontend/ide/components/resizable_panel_spec.js
+++ b/spec/frontend/ide/components/resizable_panel_spec.js
@@ -1,4 +1,5 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import ResizablePanel from '~/ide/components/resizable_panel.vue';
import { SIDE_LEFT, SIDE_RIGHT } from '~/ide/constants';
@@ -8,8 +9,7 @@ const TEST_WIDTH = 500;
const TEST_MIN_WIDTH = 400;
describe('~/ide/components/resizable_panel', () => {
- const localVue = createLocalVue();
- localVue.use(Vuex);
+ Vue.use(Vuex);
let wrapper;
let store;
@@ -33,7 +33,6 @@ describe('~/ide/components/resizable_panel', () => {
...props,
},
store,
- localVue,
});
};
const findResizer = () => wrapper.find(PanelResizer);
@@ -100,15 +99,14 @@ describe('~/ide/components/resizable_panel', () => {
});
});
- it('when resizer emits update:size, changes inline width', () => {
+ it('when resizer emits update:size, changes inline width', async () => {
const newSize = TEST_WIDTH - 100;
const resizer = findResizer();
resizer.vm.$emit('update:size', newSize);
- return wrapper.vm.$nextTick().then(() => {
- expect(findInlineStyle()).toBe(createInlineStyle(newSize));
- });
+ await nextTick();
+ expect(findInlineStyle()).toBe(createInlineStyle(newSize));
});
});
});
diff --git a/spec/frontend/ide/components/shared/tokened_input_spec.js b/spec/frontend/ide/components/shared/tokened_input_spec.js
index 837bfe6b574..a37c08af0a1 100644
--- a/spec/frontend/ide/components/shared/tokened_input_spec.js
+++ b/spec/frontend/ide/components/shared/tokened_input_spec.js
@@ -1,4 +1,4 @@
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import TokenedInput from '~/ide/components/shared/tokened_input.vue';
@@ -54,15 +54,11 @@ describe('IDE shared/TokenedInput', () => {
expect(vm.$refs.input).toHaveValue(TEST_VALUE);
});
- it('renders placeholder, when tokens are empty', (done) => {
+ it('renders placeholder, when tokens are empty', async () => {
vm.tokens = [];
- vm.$nextTick()
- .then(() => {
- expect(vm.$refs.input).toHaveAttr('placeholder', TEST_PLACEHOLDER);
- })
- .then(done)
- .catch(done.fail);
+ await nextTick();
+ expect(vm.$refs.input).toHaveAttr('placeholder', TEST_PLACEHOLDER);
});
it('triggers "removeToken" on token click', () => {
diff --git a/spec/frontend/ide/components/terminal/session_spec.js b/spec/frontend/ide/components/terminal/session_spec.js
index 5659a7d15da..6a70ddb46a8 100644
--- a/spec/frontend/ide/components/terminal/session_spec.js
+++ b/spec/frontend/ide/components/terminal/session_spec.js
@@ -1,5 +1,6 @@
import { GlButton } from '@gitlab/ui';
-import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import TerminalSession from '~/ide/components/terminal/session.vue';
import Terminal from '~/ide/components/terminal/terminal.vue';
@@ -13,8 +14,7 @@ import {
const TEST_TERMINAL_PATH = 'terminal/path';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('IDE TerminalSession', () => {
let wrapper;
@@ -33,7 +33,6 @@ describe('IDE TerminalSession', () => {
});
wrapper = shallowMount(TerminalSession, {
- localVue,
store,
...options,
});
@@ -68,32 +67,30 @@ describe('IDE TerminalSession', () => {
});
[STARTING, PENDING, RUNNING].forEach((status) => {
- it(`show stop button when status is ${status}`, () => {
+ it(`show stop button when status is ${status}`, async () => {
state.session = { status };
factory();
const button = findButton();
button.vm.$emit('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(button.text()).toEqual('Stop Terminal');
- expect(actions.stopSession).toHaveBeenCalled();
- });
+ await nextTick();
+ expect(button.text()).toEqual('Stop Terminal');
+ expect(actions.stopSession).toHaveBeenCalled();
});
});
[STOPPING, STOPPED].forEach((status) => {
- it(`show stop button when status is ${status}`, () => {
+ it(`show stop button when status is ${status}`, async () => {
state.session = { status };
factory();
const button = findButton();
button.vm.$emit('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(button.text()).toEqual('Restart Terminal');
- expect(actions.restartSession).toHaveBeenCalled();
- });
+ await nextTick();
+ expect(button.text()).toEqual('Restart Terminal');
+ expect(actions.restartSession).toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/ide/components/terminal/terminal_controls_spec.js b/spec/frontend/ide/components/terminal/terminal_controls_spec.js
index 416096083f0..71ec0dca89d 100644
--- a/spec/frontend/ide/components/terminal/terminal_controls_spec.js
+++ b/spec/frontend/ide/components/terminal/terminal_controls_spec.js
@@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import ScrollButton from '~/ide/components/jobs/detail/scroll_button.vue';
import TerminalControls from '~/ide/components/terminal/terminal_controls.vue';
@@ -39,27 +40,25 @@ describe('IDE TerminalControls', () => {
);
});
- it('emits "scroll-up" when click up button', () => {
+ it('emits "scroll-up" when click up button', async () => {
factory({ propsData: { canScrollUp: true } });
expect(wrapper.emitted()).toEqual({});
buttons.at(0).vm.$emit('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted('scroll-up')).toEqual([[]]);
- });
+ await nextTick();
+ expect(wrapper.emitted('scroll-up')).toEqual([[]]);
});
- it('emits "scroll-down" when click down button', () => {
+ it('emits "scroll-down" when click down button', async () => {
factory({ propsData: { canScrollDown: true } });
expect(wrapper.emitted()).toEqual({});
buttons.at(1).vm.$emit('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted('scroll-down')).toEqual([[]]);
- });
+ await nextTick();
+ expect(wrapper.emitted('scroll-down')).toEqual([[]]);
});
});
diff --git a/spec/frontend/ide/components/terminal/view_spec.js b/spec/frontend/ide/components/terminal/view_spec.js
index e97d4d8a73b..49f9513d2ac 100644
--- a/spec/frontend/ide/components/terminal/view_spec.js
+++ b/spec/frontend/ide/components/terminal/view_spec.js
@@ -1,4 +1,5 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import waitForPromises from 'helpers/wait_for_promises';
import { TEST_HOST } from 'spec/test_constants';
@@ -9,8 +10,7 @@ import TerminalView from '~/ide/components/terminal/view.vue';
const TEST_HELP_PATH = `${TEST_HOST}/help`;
const TEST_SVG_PATH = `${TEST_HOST}/illustration.svg`;
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('IDE TerminalView', () => {
let state;
@@ -30,7 +30,7 @@ describe('IDE TerminalView', () => {
},
});
- wrapper = shallowMount(TerminalView, { localVue, store });
+ wrapper = shallowMount(TerminalView, { store });
// Uses deferred components, so wait for those to load...
await waitForPromises();
diff --git a/spec/frontend/ide/components/terminal_sync/terminal_sync_status_safe_spec.js b/spec/frontend/ide/components/terminal_sync/terminal_sync_status_safe_spec.js
index 69077ef2c68..f921037d744 100644
--- a/spec/frontend/ide/components/terminal_sync/terminal_sync_status_safe_spec.js
+++ b/spec/frontend/ide/components/terminal_sync/terminal_sync_status_safe_spec.js
@@ -1,10 +1,10 @@
-import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import TerminalSyncStatus from '~/ide/components/terminal_sync/terminal_sync_status.vue';
import TerminalSyncStatusSafe from '~/ide/components/terminal_sync/terminal_sync_status_safe.vue';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('ide/components/terminal_sync/terminal_sync_status_safe', () => {
let store;
@@ -16,7 +16,6 @@ describe('ide/components/terminal_sync/terminal_sync_status_safe', () => {
});
wrapper = shallowMount(TerminalSyncStatusSafe, {
- localVue,
store,
});
};
diff --git a/spec/frontend/ide/components/terminal_sync/terminal_sync_status_spec.js b/spec/frontend/ide/components/terminal_sync/terminal_sync_status_spec.js
index c916c43d1e2..3a326b08fff 100644
--- a/spec/frontend/ide/components/terminal_sync/terminal_sync_status_spec.js
+++ b/spec/frontend/ide/components/terminal_sync/terminal_sync_status_spec.js
@@ -1,5 +1,6 @@
import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
-import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import TerminalSyncStatus from '~/ide/components/terminal_sync/terminal_sync_status.vue';
import {
@@ -11,8 +12,7 @@ import {
const TEST_MESSAGE = 'lorem ipsum dolar sit';
const START_LOADING = 'START_LOADING';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('ide/components/terminal_sync/terminal_sync_status', () => {
let moduleState;
@@ -35,7 +35,6 @@ describe('ide/components/terminal_sync/terminal_sync_status', () => {
});
wrapper = shallowMount(TerminalSyncStatus, {
- localVue,
store,
});
};
diff --git a/spec/frontend/ide/lib/common/model_spec.js b/spec/frontend/ide/lib/common/model_spec.js
index 51df1e2e42f..5d1623429c0 100644
--- a/spec/frontend/ide/lib/common/model_spec.js
+++ b/spec/frontend/ide/lib/common/model_spec.js
@@ -81,16 +81,13 @@ describe('Multi-file editor library model', () => {
});
describe('onChange', () => {
- it('calls callback on change', (done) => {
+ it('calls callback on change', () => {
const spy = jest.fn();
model.onChange(spy);
model.getModel().setValue('123');
- setImmediate(() => {
- expect(spy).toHaveBeenCalledWith(model, expect.anything());
- done();
- });
+ expect(spy).toHaveBeenCalledWith(model, expect.anything());
});
});
diff --git a/spec/frontend/ide/stores/actions/file_spec.js b/spec/frontend/ide/stores/actions/file_spec.js
index 6b94d7cf6f1..45d1beea3f8 100644
--- a/spec/frontend/ide/stores/actions/file_spec.js
+++ b/spec/frontend/ide/stores/actions/file_spec.js
@@ -1,5 +1,5 @@
import MockAdapter from 'axios-mock-adapter';
-import Vue from 'vue';
+import { nextTick } from 'vue';
import eventHub from '~/ide/eventhub';
import { createRouter } from '~/ide/ide_router';
import service from '~/ide/services';
@@ -68,7 +68,7 @@ describe('IDE store file actions', () => {
return store
.dispatch('closeFile', localFile)
- .then(Vue.nextTick)
+ .then(nextTick)
.then(() => {
expect(store.state.openFiles.length).toBe(0);
expect(store.state.changedFiles.length).toBe(1);
@@ -83,7 +83,7 @@ describe('IDE store file actions', () => {
return store
.dispatch('closeFile', localFile)
- .then(Vue.nextTick)
+ .then(nextTick)
.then(() => {
expect(router.push).toHaveBeenCalledWith('/project/test/test/tree/main/-/newOpenFile/');
});
diff --git a/spec/frontend/ide/stores/actions_spec.js b/spec/frontend/ide/stores/actions_spec.js
index e575667b8c6..be43c618095 100644
--- a/spec/frontend/ide/stores/actions_spec.js
+++ b/spec/frontend/ide/stores/actions_spec.js
@@ -274,24 +274,17 @@ describe('Multi-file store actions', () => {
});
describe('scrollToTab', () => {
- it('focuses the current active element', (done) => {
+ it('focuses the current active element', () => {
document.body.innerHTML +=
'<div id="tabs"><div class="active"><div class="repo-tab"></div></div></div>';
const el = document.querySelector('.repo-tab');
jest.spyOn(el, 'focus').mockImplementation();
- store
- .dispatch('scrollToTab')
- .then(() => {
- setImmediate(() => {
- expect(el.focus).toHaveBeenCalled();
+ return store.dispatch('scrollToTab').then(() => {
+ expect(el.focus).toHaveBeenCalled();
- document.getElementById('tabs').remove();
-
- done();
- });
- })
- .catch(done.fail);
+ document.getElementById('tabs').remove();
+ });
});
});
diff --git a/spec/frontend/ide/stores/modules/clientside/actions_spec.js b/spec/frontend/ide/stores/modules/clientside/actions_spec.js
index 88d7a630a90..d2777623b0d 100644
--- a/spec/frontend/ide/stores/modules/clientside/actions_spec.js
+++ b/spec/frontend/ide/stores/modules/clientside/actions_spec.js
@@ -1,11 +1,12 @@
import MockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
import testAction from 'helpers/vuex_action_helper';
+import { PING_USAGE_PREVIEW_KEY } from '~/ide/constants';
import * as actions from '~/ide/stores/modules/clientside/actions';
import axios from '~/lib/utils/axios_utils';
const TEST_PROJECT_URL = `${TEST_HOST}/lorem/ipsum`;
-const TEST_USAGE_URL = `${TEST_PROJECT_URL}/service_ping/web_ide_clientside_preview`;
+const TEST_USAGE_URL = `${TEST_PROJECT_URL}/service_ping/${PING_USAGE_PREVIEW_KEY}`;
describe('IDE store module clientside actions', () => {
let rootGetters;
@@ -30,7 +31,7 @@ describe('IDE store module clientside actions', () => {
mock.onPost(TEST_USAGE_URL).reply(() => usageSpy());
- testAction(actions.pingUsage, null, rootGetters, [], [], () => {
+ testAction(actions.pingUsage, PING_USAGE_PREVIEW_KEY, rootGetters, [], [], () => {
expect(usageSpy).toHaveBeenCalled();
done();
});
diff --git a/spec/frontend/ide/stores/plugins/terminal_spec.js b/spec/frontend/ide/stores/plugins/terminal_spec.js
index d4cdad16ecb..912de88cb39 100644
--- a/spec/frontend/ide/stores/plugins/terminal_spec.js
+++ b/spec/frontend/ide/stores/plugins/terminal_spec.js
@@ -1,4 +1,4 @@
-import { createLocalVue } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import { TEST_HOST } from 'helpers/test_constants';
import terminalModule from '~/ide/stores/modules/terminal';
@@ -11,8 +11,7 @@ const TEST_DATASET = {
eeWebTerminalConfigHelpPath: `${TEST_HOST}/web/terminal/config/help`,
eeWebTerminalRunnersHelpPath: `${TEST_HOST}/web/terminal/runners/help`,
};
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('ide/stores/extend', () => {
let store;
diff --git a/spec/frontend/image_diff/helpers/badge_helper_spec.js b/spec/frontend/image_diff/helpers/badge_helper_spec.js
index c970ccc535d..9ac6ebf6fdb 100644
--- a/spec/frontend/image_diff/helpers/badge_helper_spec.js
+++ b/spec/frontend/image_diff/helpers/badge_helper_spec.js
@@ -62,7 +62,10 @@ describe('badge helper', () => {
});
it('should add badge classes', () => {
- expect(buttonEl.className).toContain('badge badge-pill');
+ const classes = buttonEl.className.split(' ');
+ expect(classes).toEqual(
+ expect.arrayContaining(['design-note-pin', 'on-image', 'gl-absolute']),
+ );
});
it('should set the badge text', () => {
@@ -105,7 +108,7 @@ describe('badge helper', () => {
beforeEach(() => {
containerEl.innerHTML = `
<div id="${noteId}">
- <div class="badge hidden">
+ <div class="design-note-pin hidden">
</div>
</div>
`;
@@ -116,7 +119,7 @@ describe('badge helper', () => {
badgeNumber,
},
});
- avatarBadgeEl = containerEl.querySelector(`#${noteId} .badge`);
+ avatarBadgeEl = containerEl.querySelector(`#${noteId} .design-note-pin`);
});
it('should update badge number', () => {
diff --git a/spec/frontend/image_diff/helpers/dom_helper_spec.js b/spec/frontend/image_diff/helpers/dom_helper_spec.js
index 9357d626bbe..1c5f1cbe3da 100644
--- a/spec/frontend/image_diff/helpers/dom_helper_spec.js
+++ b/spec/frontend/image_diff/helpers/dom_helper_spec.js
@@ -37,14 +37,16 @@ describe('domHelper', () => {
discussionEl = document.createElement('div');
discussionEl.innerHTML = `
<a href="#" class="image-diff-avatar-link">
- <div class="badge"></div>
+ <div class="design-note-pin"></div>
</a>
`;
domHelper.updateDiscussionAvatarBadgeNumber(discussionEl, badgeNumber);
});
it('should update avatar badge number', () => {
- expect(discussionEl.querySelector('.badge').textContent).toEqual(badgeNumber.toString());
+ expect(discussionEl.querySelector('.design-note-pin').textContent).toEqual(
+ badgeNumber.toString(),
+ );
});
});
@@ -54,13 +56,15 @@ describe('domHelper', () => {
beforeEach(() => {
discussionEl = document.createElement('div');
discussionEl.innerHTML = `
- <div class="badge"></div>
+ <div class="design-note-pin"></div>
`;
domHelper.updateDiscussionBadgeNumber(discussionEl, badgeNumber);
});
it('should update discussion badge number', () => {
- expect(discussionEl.querySelector('.badge').textContent).toEqual(badgeNumber.toString());
+ expect(discussionEl.querySelector('.design-note-pin').textContent).toEqual(
+ badgeNumber.toString(),
+ );
});
});
diff --git a/spec/frontend/image_diff/image_diff_spec.js b/spec/frontend/image_diff/image_diff_spec.js
index 16d19f45496..710aa7108a8 100644
--- a/spec/frontend/image_diff/image_diff_spec.js
+++ b/spec/frontend/image_diff/image_diff_spec.js
@@ -15,9 +15,9 @@ describe('ImageDiff', () => {
<div class="js-image-frame">
<img src="${TEST_HOST}/image.png">
<div class="comment-indicator"></div>
- <div id="badge-1" class="badge">1</div>
- <div id="badge-2" class="badge">2</div>
- <div id="badge-3" class="badge">3</div>
+ <div id="badge-1" class="design-note-pin">1</div>
+ <div id="badge-2" class="design-note-pin">2</div>
+ <div id="badge-3" class="design-note-pin">3</div>
</div>
<div class="note-container">
<div class="discussion-notes">
@@ -335,7 +335,7 @@ describe('ImageDiff', () => {
describe('cascade badge count', () => {
it('should update next imageBadgeEl value', () => {
- const imageBadgeEls = imageDiff.imageFrameEl.querySelectorAll('.badge');
+ const imageBadgeEls = imageDiff.imageFrameEl.querySelectorAll('.design-note-pin');
expect(imageBadgeEls[0].textContent).toEqual('1');
expect(imageBadgeEls[1].textContent).toEqual('2');
diff --git a/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js b/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js
index c6ddce17fe4..52c868e5356 100644
--- a/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js
+++ b/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js
@@ -1,4 +1,4 @@
-import { InMemoryCache } from 'apollo-cache-inmemory';
+import { InMemoryCache } from '@apollo/client/core';
import MockAdapter from 'axios-mock-adapter';
import { createMockClient } from 'mock-apollo-client';
import waitForPromises from 'helpers/wait_for_promises';
diff --git a/spec/frontend/import_entities/import_groups/graphql/fixtures.js b/spec/frontend/import_entities/import_groups/graphql/fixtures.js
index ed4e343f331..938020e03f0 100644
--- a/spec/frontend/import_entities/import_groups/graphql/fixtures.js
+++ b/spec/frontend/import_entities/import_groups/graphql/fixtures.js
@@ -16,6 +16,7 @@ export const generateFakeEntry = ({ id, status, message, ...rest }) => ({
status === STATUSES.NONE || status === STATUSES.PENDING
? null
: {
+ __typename: clientTypenames.BulkImportProgress,
id,
status,
message: message || '',
diff --git a/spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js b/spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js
index 0e748baa313..0b12df83cd1 100644
--- a/spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js
+++ b/spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js
@@ -1,6 +1,6 @@
import { GlLoadingIcon, GlButton, GlIntersectionObserver, GlFormInput } from '@gitlab/ui';
-import { createLocalVue, shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { STATUSES } from '~/import_entities/constants';
import ImportProjectsTable from '~/import_entities/import_projects/components/import_projects_table.vue';
@@ -46,8 +46,7 @@ describe('ImportProjectsTable', () => {
filterable,
paginatable,
} = {}) {
- const localVue = createLocalVue();
- localVue.use(Vuex);
+ Vue.use(Vuex);
const store = new Vuex.Store({
state: { ...state(), defaultTargetNamespace: USER_NAMESPACE, ...initialState },
@@ -67,7 +66,6 @@ describe('ImportProjectsTable', () => {
});
wrapper = shallowMount(ImportProjectsTable, {
- localVue,
store,
propsData: {
providerTitle,
diff --git a/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js b/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js
index 72640f3d601..c8afa9ea57d 100644
--- a/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js
+++ b/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js
@@ -1,6 +1,6 @@
import { GlBadge, GlButton, GlDropdown } from '@gitlab/ui';
-import { createLocalVue, shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { STATUSES } from '~/import_entities//constants';
import ImportGroupDropdown from '~/import_entities/components/group_dropdown.vue';
@@ -38,13 +38,11 @@ describe('ProviderRepoTableRow', () => {
};
function mountComponent(props) {
- const localVue = createLocalVue();
- localVue.use(Vuex);
+ Vue.use(Vuex);
const store = initStore();
wrapper = shallowMount(ProviderRepoTableRow, {
- localVue,
store,
propsData: { availableNamespaces, userNamespace, ...props },
});
diff --git a/spec/frontend/incidents/components/incidents_list_spec.js b/spec/frontend/incidents/components/incidents_list_spec.js
index 48545ffd2d6..1be6007d844 100644
--- a/spec/frontend/incidents/components/incidents_list_spec.js
+++ b/spec/frontend/incidents/components/incidents_list_spec.js
@@ -1,5 +1,6 @@
import { GlAlert, GlLoadingIcon, GlTable, GlAvatar, GlEmptyState } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import IncidentsList from '~/incidents/components/incidents_list.vue';
import {
I18N,
@@ -210,7 +211,7 @@ describe('Incidents List', () => {
it('sets button loading on click', async () => {
findCreateIncidentBtn().vm.$emit('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findCreateIncidentBtn().attributes('loading')).toBe('true');
});
@@ -233,7 +234,7 @@ describe('Incidents List', () => {
it('should track create new incident button', async () => {
findCreateIncidentBtn().vm.$emit('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(Tracking.event).toHaveBeenCalled();
});
});
@@ -263,10 +264,10 @@ describe('Incidents List', () => {
const columnHeader = () => wrapper.find(`[${attr}="${value}"]`);
expect(columnHeader().attributes('aria-sort')).toBe(initialSort);
columnHeader().trigger('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(columnHeader().attributes('aria-sort')).toBe(firstSort);
columnHeader().trigger('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(columnHeader().attributes('aria-sort')).toBe(nextSort);
},
);
@@ -287,7 +288,7 @@ describe('Incidents List', () => {
it('should track incident creation events', async () => {
findCreateIncidentBtn().vm.$emit('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
const { category, action } = trackIncidentCreateNewOptions;
expect(Tracking.event).toHaveBeenCalledWith(category, action);
});
diff --git a/spec/frontend/incidents/mocks/incidents.json b/spec/frontend/incidents/mocks/incidents.json
index 78783a0dce5..357b94e5b6c 100644
--- a/spec/frontend/incidents/mocks/incidents.json
+++ b/spec/frontend/incidents/mocks/incidents.json
@@ -1,5 +1,6 @@
[
{
+ "id": 1,
"iid": "15",
"title": "New: Alert",
"createdAt": "2020-06-03T15:46:08Z",
@@ -9,6 +10,7 @@
"slaDueAt": "2020-06-04T12:46:08Z"
},
{
+ "id": 2,
"iid": "14",
"title": "Create issue4",
"createdAt": "2020-05-19T09:26:07Z",
@@ -27,6 +29,7 @@
"slaDueAt": null
},
{
+ "id": 3,
"iid": "13",
"title": "Create issue3",
"createdAt": "2020-05-19T08:53:55Z",
@@ -35,6 +38,7 @@
"severity": "LOW"
},
{
+ "id": 4,
"iid": "12",
"title": "Create issue2",
"createdAt": "2020-05-18T17:13:35Z",
diff --git a/spec/frontend/incidents_settings/components/__snapshots__/incidents_settings_tabs_spec.js.snap b/spec/frontend/incidents_settings/components/__snapshots__/incidents_settings_tabs_spec.js.snap
index 1e3c344ce65..0f042dfaa4c 100644
--- a/spec/frontend/incidents_settings/components/__snapshots__/incidents_settings_tabs_spec.js.snap
+++ b/spec/frontend/incidents_settings/components/__snapshots__/incidents_settings_tabs_spec.js.snap
@@ -40,7 +40,6 @@ exports[`IncidentsSettingTabs should render the component 1`] = `
>
<gl-tabs-stub
queryparamname="tab"
- theme="indigo"
value="0"
>
<!---->
diff --git a/spec/frontend/integrations/edit/components/active_checkbox_spec.js b/spec/frontend/integrations/edit/components/active_checkbox_spec.js
index 0dc31616166..c335b593f7d 100644
--- a/spec/frontend/integrations/edit/components/active_checkbox_spec.js
+++ b/spec/frontend/integrations/edit/components/active_checkbox_spec.js
@@ -1,6 +1,7 @@
import { GlFormCheckbox } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import ActiveCheckbox from '~/integrations/edit/components/active_checkbox.vue';
import { createStore } from '~/integrations/edit/store';
@@ -68,7 +69,7 @@ describe('ActiveCheckbox', () => {
it('switches the form value', async () => {
findInputInCheckbox().trigger('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findGlFormCheckbox().vm.$attrs.checked).toBe(false);
});
});
diff --git a/spec/frontend/integrations/edit/components/confirmation_modal_spec.js b/spec/frontend/integrations/edit/components/confirmation_modal_spec.js
index 805d3971994..cbe3402727a 100644
--- a/spec/frontend/integrations/edit/components/confirmation_modal_spec.js
+++ b/spec/frontend/integrations/edit/components/confirmation_modal_spec.js
@@ -1,6 +1,7 @@
import { GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import ConfirmationModal from '~/integrations/edit/components/confirmation_modal.vue';
import { createStore } from '~/integrations/edit/store';
@@ -40,7 +41,7 @@ describe('ConfirmationModal', () => {
findGlModal().vm.$emit('primary');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.emitted().submit).toHaveLength(1);
});
diff --git a/spec/frontend/integrations/edit/components/dynamic_field_spec.js b/spec/frontend/integrations/edit/components/dynamic_field_spec.js
index b0fb94d2b29..ee2f6541b03 100644
--- a/spec/frontend/integrations/edit/components/dynamic_field_spec.js
+++ b/spec/frontend/integrations/edit/components/dynamic_field_spec.js
@@ -2,22 +2,14 @@ import { GlFormGroup, GlFormCheckbox, GlFormInput, GlFormSelect, GlFormTextarea
import { mount } from '@vue/test-utils';
import DynamicField from '~/integrations/edit/components/dynamic_field.vue';
+import { mockField } from '../mock_data';
describe('DynamicField', () => {
let wrapper;
- const defaultProps = {
- help: 'The URL of the project',
- name: 'project_url',
- placeholder: 'https://jira.example.com',
- title: 'Project URL',
- type: 'text',
- value: '1',
- };
-
const createComponent = (props, isInheriting = false) => {
wrapper = mount(DynamicField, {
- propsData: { ...defaultProps, ...props },
+ propsData: { ...mockField, ...props },
computed: {
isInheriting: () => isInheriting,
},
@@ -61,7 +53,7 @@ describe('DynamicField', () => {
});
it(`renders GlFormCheckbox with correct text content when checkboxLabel is ${checkboxLabel}`, () => {
- expect(findGlFormCheckbox().text()).toContain(checkboxLabel ?? defaultProps.title);
+ expect(findGlFormCheckbox().text()).toContain(checkboxLabel ?? mockField.title);
});
it('does not render other types of input', () => {
@@ -160,7 +152,7 @@ describe('DynamicField', () => {
type: 'text',
id: 'service_project_url',
name: 'service[project_url]',
- placeholder: defaultProps.placeholder,
+ placeholder: mockField.placeholder,
required: 'required',
});
expect(findGlFormInput().attributes('readonly')).toBe(readonly);
@@ -179,7 +171,7 @@ describe('DynamicField', () => {
it('renders description with help text', () => {
createComponent();
- expect(findGlFormGroup().find('small').text()).toBe(defaultProps.help);
+ expect(findGlFormGroup().find('small').text()).toBe(mockField.help);
});
describe('when type is checkbox', () => {
@@ -189,7 +181,7 @@ describe('DynamicField', () => {
});
expect(findGlFormGroup().find('small').exists()).toBe(false);
- expect(findGlFormCheckbox().text()).toContain(defaultProps.help);
+ expect(findGlFormCheckbox().text()).toContain(mockField.help);
});
});
@@ -221,40 +213,36 @@ describe('DynamicField', () => {
it('renders label with title', () => {
createComponent();
- expect(findGlFormGroup().find('label').text()).toBe(defaultProps.title);
+ expect(findGlFormGroup().find('label').text()).toBe(mockField.title);
});
});
- describe('validations', () => {
- describe('password field', () => {
- beforeEach(() => {
+ describe('password field validations', () => {
+ describe('without value', () => {
+ it('requires validation', () => {
createComponent({
type: 'password',
required: true,
value: null,
+ isValidated: true,
});
- wrapper.vm.validated = true;
- });
-
- describe('without value', () => {
- it('requires validation', () => {
- expect(wrapper.vm.valid).toBe(false);
- expect(findGlFormGroup().classes('is-invalid')).toBe(true);
- expect(findGlFormInput().classes('is-invalid')).toBe(true);
- });
+ expect(findGlFormGroup().classes('is-invalid')).toBe(true);
+ expect(findGlFormInput().classes('is-invalid')).toBe(true);
});
+ });
- describe('with value', () => {
- beforeEach(() => {
- wrapper.setProps({ value: 'true' });
+ describe('with value', () => {
+ it('does not require validation', () => {
+ createComponent({
+ type: 'password',
+ required: true,
+ value: 'test value',
+ isValidated: true,
});
- it('does not require validation', () => {
- expect(wrapper.vm.valid).toBe(true);
- expect(findGlFormGroup().classes('is-valid')).toBe(true);
- expect(findGlFormInput().classes('is-valid')).toBe(true);
- });
+ expect(findGlFormGroup().classes('is-valid')).toBe(true);
+ expect(findGlFormInput().classes('is-valid')).toBe(true);
});
});
});
diff --git a/spec/frontend/integrations/edit/components/integration_form_spec.js b/spec/frontend/integrations/edit/components/integration_form_spec.js
index 8cf8a403e5d..7e01b79383a 100644
--- a/spec/frontend/integrations/edit/components/integration_form_spec.js
+++ b/spec/frontend/integrations/edit/components/integration_form_spec.js
@@ -17,16 +17,13 @@ import TriggerFields from '~/integrations/edit/components/trigger_fields.vue';
import {
integrationLevels,
I18N_SUCCESSFUL_CONNECTION_MESSAGE,
- VALIDATE_INTEGRATION_FORM_EVENT,
I18N_DEFAULT_ERROR_MESSAGE,
} from '~/integrations/constants';
import { createStore } from '~/integrations/edit/store';
-import eventHub from '~/integrations/edit/event_hub';
import httpStatus from '~/lib/utils/http_status';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
-import { mockIntegrationProps } from '../mock_data';
+import { mockIntegrationProps, mockField } from '../mock_data';
-jest.mock('~/integrations/edit/event_hub');
jest.mock('@sentry/browser');
jest.mock('~/lib/utils/url_utility');
@@ -36,13 +33,6 @@ describe('IntegrationForm', () => {
let wrapper;
let dispatch;
let mockAxios;
- let mockForm;
- let vueIntegrationFormFeatureFlag;
-
- const createForm = () => {
- mockForm = document.createElement('form');
- jest.spyOn(document, 'querySelector').mockReturnValue(mockForm);
- };
const createComponent = ({
customStateProps = {},
@@ -56,10 +46,6 @@ describe('IntegrationForm', () => {
});
dispatch = jest.spyOn(store, 'dispatch').mockImplementation();
- if (!vueIntegrationFormFeatureFlag) {
- createForm();
- }
-
wrapper = mountFn(IntegrationForm, {
propsData: { ...props },
store,
@@ -75,11 +61,6 @@ describe('IntegrationForm', () => {
show: mockToastShow,
},
},
- provide: {
- glFeatures: {
- vueIntegrationForm: vueIntegrationFormFeatureFlag,
- },
- },
});
};
@@ -96,12 +77,7 @@ describe('IntegrationForm', () => {
const findTriggerFields = () => wrapper.findComponent(TriggerFields);
const findGlForm = () => wrapper.findComponent(GlForm);
const findRedirectToField = () => wrapper.findByTestId('redirect-to-field');
- const findFormElement = () => (vueIntegrationFormFeatureFlag ? findGlForm().element : mockForm);
-
- const mockFormFunctions = ({ checkValidityReturn }) => {
- jest.spyOn(findFormElement(), 'checkValidity').mockReturnValue(checkValidityReturn);
- jest.spyOn(findFormElement(), 'submit');
- };
+ const findDynamicField = () => wrapper.findComponent(DynamicField);
beforeEach(() => {
mockAxios = new MockAdapter(axios);
@@ -357,17 +333,14 @@ describe('IntegrationForm', () => {
});
});
- describe('when `vueIntegrationForm` feature flag is $vueIntegrationFormEnabled', () => {
- it('renders hidden fields', () => {
- vueIntegrationFormFeatureFlag = true;
- createComponent({
- customStateProps: {
- redirectTo: '/services',
- },
- });
-
- expect(findRedirectToField().attributes('value')).toBe('/services');
+ it('renders hidden fields', () => {
+ createComponent({
+ customStateProps: {
+ redirectTo: '/services',
+ },
});
+
+ expect(findRedirectToField().attributes('value')).toBe('/services');
});
});
@@ -389,216 +362,200 @@ describe('IntegrationForm', () => {
});
describe.each`
- formActive | vueIntegrationFormEnabled | novalidate
- ${true} | ${true} | ${null}
- ${false} | ${true} | ${'novalidate'}
- ${true} | ${false} | ${null}
- ${false} | ${false} | ${'true'}
+ formActive | novalidate
+ ${true} | ${undefined}
+ ${false} | ${'true'}
`(
- 'when `vueIntegrationForm` feature flag is $vueIntegrationFormEnabled and `toggle-integration-active` is emitted with $formActive',
- ({ formActive, vueIntegrationFormEnabled, novalidate }) => {
+ 'when `toggle-integration-active` is emitted with $formActive',
+ ({ formActive, novalidate }) => {
beforeEach(async () => {
- vueIntegrationFormFeatureFlag = vueIntegrationFormEnabled;
-
createComponent({
customStateProps: {
showActive: true,
initialActivated: false,
},
- mountFn: mountExtended,
});
- mockFormFunctions({ checkValidityReturn: false });
await findActiveCheckbox().vm.$emit('toggle-integration-active', formActive);
});
it(`sets noValidate to ${novalidate}`, () => {
- expect(findFormElement().getAttribute('novalidate')).toBe(novalidate);
+ expect(findGlForm().attributes('novalidate')).toBe(novalidate);
});
},
);
});
- describe.each`
- vueIntegrationFormEnabled
- ${true}
- ${false}
- `(
- 'when `vueIntegrationForm` feature flag is $vueIntegrationFormEnabled',
- ({ vueIntegrationFormEnabled }) => {
- beforeEach(() => {
- vueIntegrationFormFeatureFlag = vueIntegrationFormEnabled;
- });
-
- describe('when `save` button is clicked', () => {
- describe('buttons', () => {
- beforeEach(async () => {
- createComponent({
- customStateProps: {
- showActive: true,
- canTest: true,
- initialActivated: true,
- },
- mountFn: mountExtended,
- });
-
- await findProjectSaveButton().vm.$emit('click', new Event('click'));
- });
+ describe('when `save` button is clicked', () => {
+ describe('buttons', () => {
+ beforeEach(async () => {
+ createComponent({
+ customStateProps: {
+ showActive: true,
+ canTest: true,
+ initialActivated: true,
+ },
+ mountFn: mountExtended,
+ });
- it('sets save button `loading` prop to `true`', () => {
- expect(findProjectSaveButton().props('loading')).toBe(true);
- });
+ await findProjectSaveButton().vm.$emit('click', new Event('click'));
+ });
- it('sets test button `disabled` prop to `true`', () => {
- expect(findTestButton().props('disabled')).toBe(true);
+ it('sets save button `loading` prop to `true`', () => {
+ expect(findProjectSaveButton().props('loading')).toBe(true);
+ });
+
+ it('sets test button `disabled` prop to `true`', () => {
+ expect(findTestButton().props('disabled')).toBe(true);
+ });
+ });
+
+ describe.each`
+ checkValidityReturn | integrationActive
+ ${true} | ${false}
+ ${true} | ${true}
+ ${false} | ${false}
+ `(
+ 'when form is valid (checkValidity returns $checkValidityReturn and integrationActive is $integrationActive)',
+ ({ integrationActive, checkValidityReturn }) => {
+ beforeEach(async () => {
+ createComponent({
+ customStateProps: {
+ showActive: true,
+ canTest: true,
+ initialActivated: integrationActive,
+ },
+ mountFn: mountExtended,
});
+ jest.spyOn(findGlForm().element, 'submit');
+ jest.spyOn(findGlForm().element, 'checkValidity').mockReturnValue(checkValidityReturn);
+
+ await findProjectSaveButton().vm.$emit('click', new Event('click'));
+ });
+
+ it('submit form', () => {
+ expect(findGlForm().element.submit).toHaveBeenCalledTimes(1);
});
+ },
+ );
- describe.each`
- checkValidityReturn | integrationActive
- ${true} | ${false}
- ${true} | ${true}
- ${false} | ${false}
- `(
- 'when form is valid (checkValidity returns $checkValidityReturn and integrationActive is $integrationActive)',
- ({ integrationActive, checkValidityReturn }) => {
- beforeEach(async () => {
- createComponent({
- customStateProps: {
- showActive: true,
- canTest: true,
- initialActivated: integrationActive,
- },
- mountFn: mountExtended,
- });
-
- mockFormFunctions({ checkValidityReturn });
-
- await findProjectSaveButton().vm.$emit('click', new Event('click'));
- });
-
- it('submits form', () => {
- expect(findFormElement().submit).toHaveBeenCalledTimes(1);
- });
+ describe('when form is invalid (checkValidity returns false and integrationActive is true)', () => {
+ beforeEach(async () => {
+ createComponent({
+ customStateProps: {
+ showActive: true,
+ canTest: true,
+ initialActivated: true,
+ fields: [mockField],
},
- );
-
- describe('when form is invalid (checkValidity returns false and integrationActive is true)', () => {
- beforeEach(async () => {
- createComponent({
- customStateProps: {
- showActive: true,
- canTest: true,
- initialActivated: true,
- },
- mountFn: mountExtended,
- });
- mockFormFunctions({ checkValidityReturn: false });
-
- await findProjectSaveButton().vm.$emit('click', new Event('click'));
- });
+ mountFn: mountExtended,
+ });
+ jest.spyOn(findGlForm().element, 'submit');
+ jest.spyOn(findGlForm().element, 'checkValidity').mockReturnValue(false);
- it('does not submit form', () => {
- expect(findFormElement().submit).not.toHaveBeenCalled();
- });
+ await findProjectSaveButton().vm.$emit('click', new Event('click'));
+ });
- it('sets save button `loading` prop to `false`', () => {
- expect(findProjectSaveButton().props('loading')).toBe(false);
- });
+ it('does not submit form', () => {
+ expect(findGlForm().element.submit).not.toHaveBeenCalled();
+ });
- it('sets test button `disabled` prop to `false`', () => {
- expect(findTestButton().props('disabled')).toBe(false);
- });
+ it('sets save button `loading` prop to `false`', () => {
+ expect(findProjectSaveButton().props('loading')).toBe(false);
+ });
- it('emits `VALIDATE_INTEGRATION_FORM_EVENT`', () => {
- expect(eventHub.$emit).toHaveBeenCalledWith(VALIDATE_INTEGRATION_FORM_EVENT);
- });
+ it('sets test button `disabled` prop to `false`', () => {
+ expect(findTestButton().props('disabled')).toBe(false);
+ });
+
+ it('sets `isValidated` props on form fields', () => {
+ expect(findDynamicField().props('isValidated')).toBe(true);
+ });
+ });
+ });
+
+ describe('when `test` button is clicked', () => {
+ describe('when form is invalid', () => {
+ it('sets `isValidated` props on form fields', async () => {
+ createComponent({
+ customStateProps: {
+ showActive: true,
+ canTest: true,
+ fields: [mockField],
+ },
+ mountFn: mountExtended,
});
+ jest.spyOn(findGlForm().element, 'checkValidity').mockReturnValue(false);
+
+ await findTestButton().vm.$emit('click', new Event('click'));
+
+ expect(findDynamicField().props('isValidated')).toBe(true);
});
+ });
- describe('when `test` button is clicked', () => {
- describe('when form is invalid', () => {
- it('emits `VALIDATE_INTEGRATION_FORM_EVENT` event to the event hub', () => {
- createComponent({
- customStateProps: {
- showActive: true,
- canTest: true,
- },
- mountFn: mountExtended,
- });
- mockFormFunctions({ checkValidityReturn: false });
+ describe('when form is valid', () => {
+ const mockTestPath = '/test';
- findTestButton().vm.$emit('click', new Event('click'));
+ beforeEach(() => {
+ createComponent({
+ customStateProps: {
+ showActive: true,
+ canTest: true,
+ testPath: mockTestPath,
+ },
+ mountFn: mountExtended,
+ });
+ jest.spyOn(findGlForm().element, 'checkValidity').mockReturnValue(true);
+ });
- expect(eventHub.$emit).toHaveBeenCalledWith(VALIDATE_INTEGRATION_FORM_EVENT);
- });
+ describe('buttons', () => {
+ beforeEach(async () => {
+ await findTestButton().vm.$emit('click', new Event('click'));
});
- describe('when form is valid', () => {
- const mockTestPath = '/test';
+ it('sets test button `loading` prop to `true`', () => {
+ expect(findTestButton().props('loading')).toBe(true);
+ });
- beforeEach(() => {
- createComponent({
- customStateProps: {
- showActive: true,
- canTest: true,
- testPath: mockTestPath,
- },
- mountFn: mountExtended,
- });
- mockFormFunctions({ checkValidityReturn: true });
+ it('sets save button `disabled` prop to `true`', () => {
+ expect(findProjectSaveButton().props('disabled')).toBe(true);
+ });
+ });
+
+ describe.each`
+ scenario | replyStatus | errorMessage | expectToast | expectSentry
+ ${'when "test settings" request fails'} | ${httpStatus.INTERNAL_SERVER_ERROR} | ${undefined} | ${I18N_DEFAULT_ERROR_MESSAGE} | ${true}
+ ${'when "test settings" returns an error'} | ${httpStatus.OK} | ${'an error'} | ${'an error'} | ${false}
+ ${'when "test settings" succeeds'} | ${httpStatus.OK} | ${undefined} | ${I18N_SUCCESSFUL_CONNECTION_MESSAGE} | ${false}
+ `('$scenario', ({ replyStatus, errorMessage, expectToast, expectSentry }) => {
+ beforeEach(async () => {
+ mockAxios.onPut(mockTestPath).replyOnce(replyStatus, {
+ error: Boolean(errorMessage),
+ message: errorMessage,
});
- describe('buttons', () => {
- beforeEach(async () => {
- await findTestButton().vm.$emit('click', new Event('click'));
- });
+ await findTestButton().vm.$emit('click', new Event('click'));
+ await waitForPromises();
+ });
- it('sets test button `loading` prop to `true`', () => {
- expect(findTestButton().props('loading')).toBe(true);
- });
+ it(`calls toast with '${expectToast}'`, () => {
+ expect(mockToastShow).toHaveBeenCalledWith(expectToast);
+ });
- it('sets save button `disabled` prop to `true`', () => {
- expect(findProjectSaveButton().props('disabled')).toBe(true);
- });
- });
+ it('sets `loading` prop of test button to `false`', () => {
+ expect(findTestButton().props('loading')).toBe(false);
+ });
- describe.each`
- scenario | replyStatus | errorMessage | expectToast | expectSentry
- ${'when "test settings" request fails'} | ${httpStatus.INTERNAL_SERVER_ERROR} | ${undefined} | ${I18N_DEFAULT_ERROR_MESSAGE} | ${true}
- ${'when "test settings" returns an error'} | ${httpStatus.OK} | ${'an error'} | ${'an error'} | ${false}
- ${'when "test settings" succeeds'} | ${httpStatus.OK} | ${undefined} | ${I18N_SUCCESSFUL_CONNECTION_MESSAGE} | ${false}
- `('$scenario', ({ replyStatus, errorMessage, expectToast, expectSentry }) => {
- beforeEach(async () => {
- mockAxios.onPut(mockTestPath).replyOnce(replyStatus, {
- error: Boolean(errorMessage),
- message: errorMessage,
- });
-
- await findTestButton().vm.$emit('click', new Event('click'));
- await waitForPromises();
- });
-
- it(`calls toast with '${expectToast}'`, () => {
- expect(mockToastShow).toHaveBeenCalledWith(expectToast);
- });
-
- it('sets `loading` prop of test button to `false`', () => {
- expect(findTestButton().props('loading')).toBe(false);
- });
-
- it('sets save button `disabled` prop to `false`', () => {
- expect(findProjectSaveButton().props('disabled')).toBe(false);
- });
-
- it(`${expectSentry ? 'does' : 'does not'} capture exception in Sentry`, () => {
- expect(Sentry.captureException).toHaveBeenCalledTimes(expectSentry ? 1 : 0);
- });
- });
+ it('sets save button `disabled` prop to `false`', () => {
+ expect(findProjectSaveButton().props('disabled')).toBe(false);
+ });
+
+ it(`${expectSentry ? 'does' : 'does not'} capture exception in Sentry`, () => {
+ expect(Sentry.captureException).toHaveBeenCalledTimes(expectSentry ? 1 : 0);
});
});
- },
- );
+ });
+ });
describe('when `reset-confirmation-modal` emits `reset` event', () => {
const mockResetPath = '/reset';
diff --git a/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js b/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js
index b5a8eed3598..33fd08a5959 100644
--- a/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js
+++ b/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js
@@ -1,9 +1,8 @@
import { GlFormCheckbox, GlFormInput } from '@gitlab/ui';
+import { nextTick } from 'vue';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { VALIDATE_INTEGRATION_FORM_EVENT } from '~/integrations/constants';
import JiraIssuesFields from '~/integrations/edit/components/jira_issues_fields.vue';
-import eventHub from '~/integrations/edit/event_hub';
import { createStore } from '~/integrations/edit/store';
describe('JiraIssuesFields', () => {
@@ -195,7 +194,7 @@ describe('JiraIssuesFields', () => {
await setEnableCheckbox(true);
expect(findJiraForVulnerabilities().attributes('show-full-feature')).toBe('true');
wrapper.setProps({ showJiraVulnerabilitiesIntegration: false });
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findJiraForVulnerabilities().attributes('show-full-feature')).toBeUndefined();
});
@@ -222,7 +221,7 @@ describe('JiraIssuesFields', () => {
});
describe('Project key input field', () => {
- beforeEach(() => {
+ it('sets Project Key `state` attribute to `true` by default', () => {
createComponent({
props: {
initialProjectKey: '',
@@ -230,29 +229,32 @@ describe('JiraIssuesFields', () => {
},
mountFn: shallowMountExtended,
});
- });
- it('sets Project Key `state` attribute to `true` by default', () => {
assertProjectKeyState('true');
});
- describe('when event hub recieves `VALIDATE_INTEGRATION_FORM_EVENT` event', () => {
+ describe('when `isValidated` prop is true', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ initialProjectKey: '',
+ initialEnableJiraIssues: true,
+ isValidated: true,
+ },
+ mountFn: shallowMountExtended,
+ });
+ });
+
describe('with no project key', () => {
it('sets Project Key `state` attribute to `undefined`', async () => {
- eventHub.$emit(VALIDATE_INTEGRATION_FORM_EVENT);
- await wrapper.vm.$nextTick();
-
assertProjectKeyState(undefined);
});
});
describe('when project key is set', () => {
it('sets Project Key `state` attribute to `true`', async () => {
- eventHub.$emit(VALIDATE_INTEGRATION_FORM_EVENT);
-
// set the project key
await findProjectKey().vm.$emit('input', 'AB');
- await wrapper.vm.$nextTick();
assertProjectKeyState('true');
});
diff --git a/spec/frontend/integrations/edit/components/jira_trigger_fields_spec.js b/spec/frontend/integrations/edit/components/jira_trigger_fields_spec.js
index 9e01371f542..49fbebb9396 100644
--- a/spec/frontend/integrations/edit/components/jira_trigger_fields_spec.js
+++ b/spec/frontend/integrations/edit/components/jira_trigger_fields_spec.js
@@ -1,4 +1,5 @@
import { GlFormCheckbox } from '@gitlab/ui';
+import { nextTick } from 'vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import JiraTriggerFields from '~/integrations/edit/components/jira_trigger_fields.vue';
@@ -71,12 +72,11 @@ describe('JiraTriggerFields', () => {
});
describe('on enable comments', () => {
- it('shows comment detail', () => {
+ it('shows comment detail', async () => {
findCommentSettingsCheckbox().vm.$emit('input', true);
- return wrapper.vm.$nextTick().then(() => {
- expect(findCommentDetail().isVisible()).toBe(true);
- });
+ await nextTick();
+ expect(findCommentDetail().isVisible()).toBe(true);
});
});
});
@@ -107,7 +107,7 @@ describe('JiraTriggerFields', () => {
});
describe('initialJiraIssueTransitionAutomatic is false, initialJiraIssueTransitionId is not set', () => {
- it('selects automatic transitions when enabling transitions', () => {
+ it('selects automatic transitions when enabling transitions', async () => {
createComponent({
initialTriggerCommit: true,
initialEnableComments: true,
@@ -117,11 +117,10 @@ describe('JiraTriggerFields', () => {
expect(checkbox.element.checked).toBe(false);
checkbox.trigger('click');
- return wrapper.vm.$nextTick().then(() => {
- const [radio1, radio2] = findIssueTransitionModeRadios().wrappers;
- expect(radio1.element.checked).toBe(true);
- expect(radio2.element.checked).toBe(false);
- });
+ await nextTick();
+ const [radio1, radio2] = findIssueTransitionModeRadios().wrappers;
+ expect(radio1.element.checked).toBe(true);
+ expect(radio2.element.checked).toBe(false);
});
});
diff --git a/spec/frontend/integrations/edit/mock_data.js b/spec/frontend/integrations/edit/mock_data.js
index 3c45ed0fb1b..39e5f8521e8 100644
--- a/spec/frontend/integrations/edit/mock_data.js
+++ b/spec/frontend/integrations/edit/mock_data.js
@@ -20,3 +20,12 @@ export const mockJiraIssueTypes = [
{ id: '2', name: 'bug', description: 'bug' },
{ id: '3', name: 'epic', description: 'epic' },
];
+
+export const mockField = {
+ help: 'The URL of the project',
+ name: 'project_url',
+ placeholder: 'https://jira.example.com',
+ title: 'Project URL',
+ type: 'text',
+ value: '1',
+};
diff --git a/spec/frontend/invite_members/components/group_select_spec.js b/spec/frontend/invite_members/components/group_select_spec.js
index 2ef8fe07650..192f3fdd381 100644
--- a/spec/frontend/invite_members/components/group_select_spec.js
+++ b/spec/frontend/invite_members/components/group_select_spec.js
@@ -4,14 +4,21 @@ import waitForPromises from 'helpers/wait_for_promises';
import * as groupsApi from '~/api/groups_api';
import GroupSelect from '~/invite_members/components/group_select.vue';
-const createComponent = () => {
- return mount(GroupSelect, {});
-};
-
+const accessLevels = { Guest: 10, Reporter: 20, Developer: 30, Maintainer: 40, Owner: 50 };
const group1 = { id: 1, full_name: 'Group One', avatar_url: 'test' };
const group2 = { id: 2, full_name: 'Group Two', avatar_url: 'test' };
const allGroups = [group1, group2];
+const createComponent = (props = {}) => {
+ return mount(GroupSelect, {
+ propsData: {
+ invalidGroups: [],
+ accessLevels,
+ ...props,
+ },
+ });
+};
+
describe('GroupSelect', () => {
let wrapper;
@@ -61,6 +68,7 @@ describe('GroupSelect', () => {
expect(groupsApi.getGroups).toHaveBeenCalledWith(group1.name, {
active: true,
exclude_internal: true,
+ min_access_level: accessLevels.Guest,
});
});
@@ -83,6 +91,20 @@ describe('GroupSelect', () => {
size: '32',
});
});
+
+ describe('when filtering out the group from results', () => {
+ beforeEach(() => {
+ wrapper = createComponent({ invalidGroups: [group1.id] });
+ });
+
+ it('does not find an invalid group', () => {
+ expect(findAvatarByLabel(group1.full_name)).toBe(undefined);
+ });
+
+ it('finds a group that is valid', () => {
+ expect(findAvatarByLabel(group2.full_name).exists()).toBe(true);
+ });
+ });
});
describe('when group is selected from the dropdown', () => {
diff --git a/spec/frontend/invite_members/components/import_a_project_modal_spec.js b/spec/frontend/invite_members/components/import_a_project_modal_spec.js
index fecbf84fb57..6db881d5c75 100644
--- a/spec/frontend/invite_members/components/import_a_project_modal_spec.js
+++ b/spec/frontend/invite_members/components/import_a_project_modal_spec.js
@@ -1,5 +1,6 @@
import { GlFormGroup, GlSprintf, GlModal } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
+import { nextTick } from 'vue';
import { stubComponent } from 'helpers/stub_component';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -91,7 +92,7 @@ describe('ImportAProjectModal', () => {
it('sets isLoading to true when the Invite button is clicked', async () => {
clickImportButton();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findImportButton().props('loading')).toBe(true);
});
@@ -157,7 +158,7 @@ describe('ImportAProjectModal', () => {
clickCancelButton();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(formGroupInvalidFeedback()).toBe('');
expect(formGroupErrorState()).not.toBe(false);
diff --git a/spec/frontend/invite_members/components/invite_group_trigger_spec.js b/spec/frontend/invite_members/components/invite_group_trigger_spec.js
index cb9967ebe8c..84ddb779a9e 100644
--- a/spec/frontend/invite_members/components/invite_group_trigger_spec.js
+++ b/spec/frontend/invite_members/components/invite_group_trigger_spec.js
@@ -44,7 +44,7 @@ describe('InviteGroupTrigger', () => {
});
it('emits event that triggers opening the modal', () => {
- expect(eventHub.$emit).toHaveBeenLastCalledWith('openModal', { inviteeType: 'group' });
+ expect(eventHub.$emit).toHaveBeenLastCalledWith('openGroupModal');
});
});
});
diff --git a/spec/frontend/invite_members/components/invite_groups_modal_spec.js b/spec/frontend/invite_members/components/invite_groups_modal_spec.js
new file mode 100644
index 00000000000..49c55d56080
--- /dev/null
+++ b/spec/frontend/invite_members/components/invite_groups_modal_spec.js
@@ -0,0 +1,143 @@
+import { GlModal, GlSprintf } from '@gitlab/ui';
+import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import Api from '~/api';
+import InviteGroupsModal from '~/invite_members/components/invite_groups_modal.vue';
+import InviteModalBase from '~/invite_members/components/invite_modal_base.vue';
+import GroupSelect from '~/invite_members/components/group_select.vue';
+import { stubComponent } from 'helpers/stub_component';
+import { propsData, sharedGroup } from '../mock_data/group_modal';
+
+describe('InviteGroupsModal', () => {
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMountExtended(InviteGroupsModal, {
+ propsData: {
+ ...propsData,
+ ...props,
+ },
+ stubs: {
+ InviteModalBase,
+ GlSprintf,
+ GlModal: stubComponent(GlModal, {
+ template: '<div><slot></slot><slot name="modal-footer"></slot></div>',
+ }),
+ },
+ });
+ };
+
+ const createInviteGroupToProjectWrapper = () => {
+ createComponent({ isProject: true });
+ };
+
+ const createInviteGroupToGroupWrapper = () => {
+ createComponent({ isProject: false });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ const findGroupSelect = () => wrapper.findComponent(GroupSelect);
+ const findIntroText = () => wrapper.findByTestId('modal-base-intro-text').text();
+ const findCancelButton = () => wrapper.findByTestId('cancel-button');
+ const findInviteButton = () => wrapper.findByTestId('invite-button');
+ const findMembersFormGroup = () => wrapper.findByTestId('members-form-group');
+ const membersFormGroupInvalidFeedback = () =>
+ findMembersFormGroup().attributes('invalid-feedback');
+ const clickInviteButton = () => findInviteButton().vm.$emit('click');
+ const clickCancelButton = () => findCancelButton().vm.$emit('click');
+ const triggerGroupSelect = (val) => findGroupSelect().vm.$emit('input', val);
+
+ describe('displaying the correct introText and form group description', () => {
+ describe('when inviting to a project', () => {
+ it('includes the correct type, and formatted intro text', () => {
+ createInviteGroupToProjectWrapper();
+
+ expect(findIntroText()).toBe("You're inviting a group to the test name project.");
+ });
+ });
+
+ describe('when inviting to a group', () => {
+ it('includes the correct type, and formatted intro text', () => {
+ createInviteGroupToGroupWrapper();
+
+ expect(findIntroText()).toBe("You're inviting a group to the test name group.");
+ });
+ });
+ });
+
+ describe('submitting the invite form', () => {
+ describe('when sharing the group is successful', () => {
+ const groupPostData = {
+ group_id: sharedGroup.id,
+ group_access: propsData.defaultAccessLevel,
+ expires_at: undefined,
+ format: 'json',
+ };
+
+ beforeEach(() => {
+ createComponent();
+ triggerGroupSelect(sharedGroup);
+
+ wrapper.vm.$toast = { show: jest.fn() };
+ jest.spyOn(Api, 'groupShareWithGroup').mockResolvedValue({ data: groupPostData });
+
+ clickInviteButton();
+ });
+
+ it('calls Api groupShareWithGroup with the correct params', () => {
+ expect(Api.groupShareWithGroup).toHaveBeenCalledWith(propsData.id, groupPostData);
+ });
+
+ it('displays the successful toastMessage', () => {
+ expect(wrapper.vm.$toast.show).toHaveBeenCalledWith('Members were successfully added', {
+ onComplete: expect.any(Function),
+ });
+ });
+ });
+
+ describe('when sharing the group fails', () => {
+ beforeEach(() => {
+ createInviteGroupToGroupWrapper();
+ triggerGroupSelect(sharedGroup);
+
+ wrapper.vm.$toast = { show: jest.fn() };
+
+ jest
+ .spyOn(Api, 'groupShareWithGroup')
+ .mockRejectedValue({ response: { data: { success: false } } });
+
+ clickInviteButton();
+ });
+
+ it('does not show the toast message on failure', () => {
+ expect(wrapper.vm.$toast.show).not.toHaveBeenCalled();
+ });
+
+ it('displays the generic error for http server error', () => {
+ expect(membersFormGroupInvalidFeedback()).toBe('Something went wrong');
+ });
+
+ describe('clearing the invalid state and message', () => {
+ it('clears the error when the cancel button is clicked', async () => {
+ clickCancelButton();
+
+ await nextTick();
+
+ expect(membersFormGroupInvalidFeedback()).toBe('');
+ });
+
+ it('clears the error when the modal is hidden', async () => {
+ wrapper.findComponent(GlModal).vm.$emit('hide');
+
+ await nextTick();
+
+ expect(membersFormGroupInvalidFeedback()).toBe('');
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/invite_members/components/invite_members_modal_spec.js b/spec/frontend/invite_members/components/invite_members_modal_spec.js
index 3ab89b3dff2..15a366474e4 100644
--- a/spec/frontend/invite_members/components/invite_members_modal_spec.js
+++ b/spec/frontend/invite_members/components/invite_members_modal_spec.js
@@ -1,28 +1,19 @@
-import {
- GlDropdown,
- GlDropdownItem,
- GlDatepicker,
- GlFormGroup,
- GlSprintf,
- GlLink,
- GlModal,
-} from '@gitlab/ui';
+import { GlLink, GlModal, GlSprintf } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
+import { nextTick } from 'vue';
import { stubComponent } from 'helpers/stub_component';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api';
import ExperimentTracking from '~/experimentation/experiment_tracking';
import InviteMembersModal from '~/invite_members/components/invite_members_modal.vue';
+import InviteModalBase from '~/invite_members/components/invite_modal_base.vue';
import ModalConfetti from '~/invite_members/components/confetti.vue';
import MembersTokenSelect from '~/invite_members/components/members_token_select.vue';
import {
INVITE_MEMBERS_FOR_TASK,
- CANCEL_BUTTON_TEXT,
- INVITE_BUTTON_TEXT,
MEMBERS_MODAL_CELEBRATE_INTRO,
MEMBERS_MODAL_CELEBRATE_TITLE,
- MEMBERS_MODAL_DEFAULT_TITLE,
MEMBERS_PLACEHOLDER,
MEMBERS_TO_PROJECT_CELEBRATE_INTRO_TEXT,
LEARN_GITLAB,
@@ -32,9 +23,16 @@ import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status';
import { getParameterValues } from '~/lib/utils/url_utility';
import { apiPaths, membersApiResponse, invitationsApiResponse } from '../mock_data/api_responses';
-
-let wrapper;
-let mock;
+import {
+ propsData,
+ inviteSource,
+ newProjectPath,
+ user1,
+ user2,
+ user3,
+ user4,
+ GlEmoji,
+} from '../mock_data/member_modal';
jest.mock('~/experimentation/experiment_tracking');
jest.mock('~/lib/utils/url_utility', () => ({
@@ -42,211 +40,125 @@ jest.mock('~/lib/utils/url_utility', () => ({
getParameterValues: jest.fn(() => []),
}));
-const id = '1';
-const name = 'test name';
-const isProject = false;
-const inviteeType = 'members';
-const accessLevels = { Guest: 10, Reporter: 20, Developer: 30, Maintainer: 40, Owner: 50 };
-const defaultAccessLevel = 10;
-const inviteSource = 'unknown';
-const helpLink = 'https://example.com';
-const tasksToBeDoneOptions = [
- { text: 'First task', value: 'first' },
- { text: 'Second task', value: 'second' },
-];
-const newProjectPath = 'projects/new';
-const projects = [
- { text: 'First project', value: '1' },
- { text: 'Second project', value: '2' },
-];
-
-const user1 = { id: 1, name: 'Name One', username: 'one_1', avatar_url: '' };
-const user2 = { id: 2, name: 'Name Two', username: 'one_2', avatar_url: '' };
-const user3 = {
- id: 'user-defined-token',
- name: 'email@example.com',
- username: 'one_2',
- avatar_url: '',
-};
-const user4 = {
- id: 'user-defined-token',
- name: 'email4@example.com',
- username: 'one_4',
- avatar_url: '',
-};
-const sharedGroup = { id: '981' };
-const GlEmoji = { template: '<img/>' };
-
-const createComponent = (data = {}, props = {}) => {
- wrapper = shallowMountExtended(InviteMembersModal, {
- provide: {
- newProjectPath,
- },
- propsData: {
- id,
- name,
- isProject,
- inviteeType,
- accessLevels,
- defaultAccessLevel,
- tasksToBeDoneOptions,
- projects,
- helpLink,
- ...props,
- },
- data() {
- return data;
- },
- stubs: {
- GlModal: stubComponent(GlModal, {
- template:
- '<div><slot name="modal-title"></slot><slot></slot><slot name="modal-footer"></slot></div>',
- }),
- GlDropdown: true,
- GlDropdownItem: true,
- GlEmoji,
- GlSprintf,
- GlFormGroup: stubComponent(GlFormGroup, {
- props: ['state', 'invalidFeedback', 'description'],
- }),
- },
- });
-};
-
-const createInviteMembersToProjectWrapper = () => {
- createComponent({ inviteeType: 'members' }, { isProject: true });
-};
-
-const createInviteMembersToGroupWrapper = () => {
- createComponent({ inviteeType: 'members' }, { isProject: false });
-};
+describe('InviteMembersModal', () => {
+ let wrapper;
+ let mock;
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMountExtended(InviteMembersModal, {
+ provide: {
+ newProjectPath,
+ },
+ propsData: {
+ ...propsData,
+ ...props,
+ },
+ stubs: {
+ InviteModalBase,
+ GlSprintf,
+ GlModal: stubComponent(GlModal, {
+ template: '<div><slot></slot><slot name="modal-footer"></slot></div>',
+ }),
+ GlEmoji,
+ },
+ });
+ };
-const createInviteGroupToProjectWrapper = () => {
- createComponent({ inviteeType: 'group' }, { isProject: true });
-};
+ const createInviteMembersToProjectWrapper = () => {
+ createComponent({ isProject: true });
+ };
-const createInviteGroupToGroupWrapper = () => {
- createComponent({ inviteeType: 'group' }, { isProject: false });
-};
+ const createInviteMembersToGroupWrapper = () => {
+ createComponent({ isProject: false });
+ };
-beforeEach(() => {
- gon.api_version = 'v4';
- mock = new MockAdapter(axios);
-});
+ beforeEach(() => {
+ gon.api_version = 'v4';
+ mock = new MockAdapter(axios);
+ });
-afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- mock.restore();
-});
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ mock.restore();
+ });
-describe('InviteMembersModal', () => {
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findDropdownItems = () => findDropdown().findAllComponents(GlDropdownItem);
- const findDatepicker = () => wrapper.findComponent(GlDatepicker);
- const findLink = () => wrapper.findComponent(GlLink);
- const findIntroText = () => wrapper.find({ ref: 'introText' }).text();
+ const findBase = () => wrapper.findComponent(InviteModalBase);
+ const findIntroText = () => wrapper.findByTestId('modal-base-intro-text').text();
const findCancelButton = () => wrapper.findByTestId('cancel-button');
const findInviteButton = () => wrapper.findByTestId('invite-button');
const clickInviteButton = () => findInviteButton().vm.$emit('click');
const clickCancelButton = () => findCancelButton().vm.$emit('click');
const findMembersFormGroup = () => wrapper.findByTestId('members-form-group');
- const membersFormGroupInvalidFeedback = () => findMembersFormGroup().props('invalidFeedback');
- const membersFormGroupDescription = () => findMembersFormGroup().props('description');
+ const membersFormGroupInvalidFeedback = () =>
+ findMembersFormGroup().attributes('invalid-feedback');
+ const membersFormGroupDescription = () => findMembersFormGroup().attributes('description');
const findMembersSelect = () => wrapper.findComponent(MembersTokenSelect);
const findTasksToBeDone = () => wrapper.findByTestId('invite-members-modal-tasks-to-be-done');
const findTasks = () => wrapper.findByTestId('invite-members-modal-tasks');
const findProjectSelect = () => wrapper.findByTestId('invite-members-modal-project-select');
const findNoProjectsAlert = () => wrapper.findByTestId('invite-members-modal-no-projects-alert');
- const findCelebrationEmoji = () => wrapper.findComponent(GlModal).find(GlEmoji);
-
- describe('rendering the modal', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('renders the modal with the correct title', () => {
- expect(wrapper.findComponent(GlModal).props('title')).toBe(MEMBERS_MODAL_DEFAULT_TITLE);
- });
-
- it('renders the Cancel button text correctly', () => {
- expect(findCancelButton().text()).toBe(CANCEL_BUTTON_TEXT);
- });
-
- it('renders the Invite button text correctly', () => {
- expect(findInviteButton().text()).toBe(INVITE_BUTTON_TEXT);
- });
-
- it('renders the Invite button modal without isLoading', () => {
- expect(findInviteButton().props('loading')).toBe(false);
- });
-
- describe('rendering the access levels dropdown', () => {
- it('sets the default dropdown text to the default access level name', () => {
- expect(findDropdown().attributes('text')).toBe('Guest');
- });
-
- it('renders dropdown items for each accessLevel', () => {
- expect(findDropdownItems()).toHaveLength(5);
- });
- });
-
- describe('rendering the help link', () => {
- it('renders the correct link', () => {
- expect(findLink().attributes('href')).toBe(helpLink);
- });
- });
-
- describe('rendering the access expiration date field', () => {
- it('renders the datepicker', () => {
- expect(findDatepicker().exists()).toBe(true);
- });
- });
- });
+ const findCelebrationEmoji = () => wrapper.findComponent(GlEmoji);
+ const triggerOpenModal = async ({ mode = 'default', source }) => {
+ eventHub.$emit('openModal', { mode, source });
+ await nextTick();
+ };
+ const triggerMembersTokenSelect = async (val) => {
+ findMembersSelect().vm.$emit('input', val);
+ await nextTick();
+ };
+ const triggerTasks = async (val) => {
+ findTasks().vm.$emit('input', val);
+ await nextTick();
+ };
+ const triggerAccessLevel = async (val) => {
+ findBase().vm.$emit('access-level', val);
+ await nextTick();
+ };
describe('rendering the tasks to be done', () => {
- const setupComponent = (
- extraData = {},
- props = {},
- urlParameter = ['invite_members_for_task'],
- ) => {
- const data = {
- selectedAccessLevel: 30,
- selectedTasksToBeDone: ['ci', 'code'],
- ...extraData,
- };
+ const setupComponent = async (props = {}, urlParameter = ['invite_members_for_task']) => {
getParameterValues.mockImplementation(() => urlParameter);
- createComponent(data, props);
+ createComponent(props);
+
+ await triggerAccessLevel(30);
+ };
+
+ const setupComponentWithTasks = async (...args) => {
+ await setupComponent(...args);
+ await triggerTasks(['ci', 'code']);
};
afterAll(() => {
getParameterValues.mockImplementation(() => []);
});
- it('renders the tasks to be done', () => {
- setupComponent();
+ it('renders the tasks to be done', async () => {
+ await setupComponent();
expect(findTasksToBeDone().exists()).toBe(true);
});
describe('when the selected access level is lower than 30', () => {
- it('does not render the tasks to be done', () => {
- setupComponent({ selectedAccessLevel: 20 });
+ it('does not render the tasks to be done', async () => {
+ await setupComponent();
+ await triggerAccessLevel(20);
expect(findTasksToBeDone().exists()).toBe(false);
});
});
describe('when the url does not contain the parameter `open_modal=invite_members_for_task`', () => {
- it('does not render the tasks to be done', () => {
- setupComponent({}, {}, []);
+ it('does not render the tasks to be done', async () => {
+ await setupComponent({}, []);
expect(findTasksToBeDone().exists()).toBe(false);
});
describe('when opened from the Learn GitLab page', () => {
- it('does render the tasks to be done', () => {
- setupComponent({ source: LEARN_GITLAB }, {}, []);
+ it('does render the tasks to be done', async () => {
+ await setupComponent({}, []);
+ await triggerOpenModal({ source: LEARN_GITLAB });
expect(findTasksToBeDone().exists()).toBe(true);
});
@@ -254,27 +166,27 @@ describe('InviteMembersModal', () => {
});
describe('rendering the tasks', () => {
- it('renders the tasks', () => {
- setupComponent();
+ it('renders the tasks', async () => {
+ await setupComponent();
expect(findTasks().exists()).toBe(true);
});
- it('does not render an alert', () => {
- setupComponent();
+ it('does not render an alert', async () => {
+ await setupComponent();
expect(findNoProjectsAlert().exists()).toBe(false);
});
describe('when there are no projects passed in the data', () => {
- it('does not render the tasks', () => {
- setupComponent({}, { projects: [] });
+ it('does not render the tasks', async () => {
+ await setupComponent({ projects: [] });
expect(findTasks().exists()).toBe(false);
});
- it('renders an alert with a link to the new projects path', () => {
- setupComponent({}, { projects: [] });
+ it('renders an alert with a link to the new projects path', async () => {
+ await setupComponent({ projects: [] });
expect(findNoProjectsAlert().exists()).toBe(true);
expect(findNoProjectsAlert().findComponent(GlLink).attributes('href')).toBe(
@@ -285,23 +197,23 @@ describe('InviteMembersModal', () => {
});
describe('rendering the project dropdown', () => {
- it('renders the project select', () => {
- setupComponent();
+ it('renders the project select', async () => {
+ await setupComponentWithTasks();
expect(findProjectSelect().exists()).toBe(true);
});
describe('when the modal is shown for a project', () => {
- it('does not render the project select', () => {
- setupComponent({}, { isProject: true });
+ it('does not render the project select', async () => {
+ await setupComponentWithTasks({ isProject: true });
expect(findProjectSelect().exists()).toBe(false);
});
});
describe('when no tasks are selected', () => {
- it('does not render the project select', () => {
- setupComponent({ selectedTasksToBeDone: [] });
+ it('does not render the project select', async () => {
+ await setupComponent();
expect(findProjectSelect().exists()).toBe(false);
});
@@ -309,8 +221,8 @@ describe('InviteMembersModal', () => {
});
describe('tracking events', () => {
- it('tracks the view for invite_members_for_task', () => {
- setupComponent();
+ it('tracks the view for invite_members_for_task', async () => {
+ await setupComponentWithTasks();
expect(ExperimentTracking).toHaveBeenCalledWith(INVITE_MEMBERS_FOR_TASK.name);
expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith(
@@ -318,8 +230,8 @@ describe('InviteMembersModal', () => {
);
});
- it('tracks the submit for invite_members_for_task', () => {
- setupComponent();
+ it('tracks the submit for invite_members_for_task', async () => {
+ await setupComponentWithTasks();
clickInviteButton();
expect(ExperimentTracking).toHaveBeenCalledWith(INVITE_MEMBERS_FOR_TASK.name, {
@@ -352,8 +264,9 @@ describe('InviteMembersModal', () => {
});
describe('when inviting members with celebration', () => {
- beforeEach(() => {
- createComponent({ mode: 'celebrate', inviteeType: 'members' }, { isProject: true });
+ beforeEach(async () => {
+ createComponent({ isProject: true });
+ await triggerOpenModal({ mode: 'celebrate' });
});
it('renders the modal with confetti', () => {
@@ -372,34 +285,14 @@ describe('InviteMembersModal', () => {
expect(membersFormGroupDescription()).toBe(MEMBERS_PLACEHOLDER);
});
});
-
- describe('when sharing with a group', () => {
- it('includes the correct invitee, type, and formatted name', () => {
- createInviteGroupToProjectWrapper();
-
- expect(findIntroText()).toBe("You're inviting a group to the test name project.");
- expect(membersFormGroupDescription()).toBe('');
- });
- });
});
describe('when inviting to a group', () => {
- describe('when inviting members', () => {
- it('includes the correct invitee, type, and formatted name', () => {
- createInviteMembersToGroupWrapper();
-
- expect(findIntroText()).toBe("You're inviting members to the test name group.");
- expect(membersFormGroupDescription()).toBe(MEMBERS_PLACEHOLDER);
- });
- });
-
- describe('when sharing with a group', () => {
- it('includes the correct invitee, type, and formatted name', () => {
- createInviteGroupToGroupWrapper();
+ it('includes the correct invitee, type, and formatted name', () => {
+ createInviteMembersToGroupWrapper();
- expect(findIntroText()).toBe("You're inviting a group to the test name group.");
- expect(membersFormGroupDescription()).toBe('');
- });
+ expect(findIntroText()).toBe("You're inviting members to the test name group.");
+ expect(membersFormGroupDescription()).toBe(MEMBERS_PLACEHOLDER);
});
});
});
@@ -419,7 +312,7 @@ describe('InviteMembersModal', () => {
describe('when inviting an existing user to group by user ID', () => {
const postData = {
user_id: '1,2',
- access_level: defaultAccessLevel,
+ access_level: propsData.defaultAccessLevel,
expires_at: undefined,
invite_source: inviteSource,
format: 'json',
@@ -428,8 +321,9 @@ describe('InviteMembersModal', () => {
};
describe('when member is added successfully', () => {
- beforeEach(() => {
- createComponent({ newUsersToInvite: [user1, user2] });
+ beforeEach(async () => {
+ createComponent();
+ await triggerMembersTokenSelect([user1, user2]);
wrapper.vm.$toast = { show: jest.fn() };
jest.spyOn(Api, 'addGroupMembersByUserId').mockResolvedValue({ data: postData });
@@ -445,19 +339,17 @@ describe('InviteMembersModal', () => {
});
it('calls Api addGroupMembersByUserId with the correct params', () => {
- expect(Api.addGroupMembersByUserId).toHaveBeenCalledWith(id, postData);
+ expect(Api.addGroupMembersByUserId).toHaveBeenCalledWith(propsData.id, postData);
});
it('displays the successful toastMessage', () => {
- expect(wrapper.vm.$toast.show).toHaveBeenCalledWith('Members were successfully added', {
- onComplete: expect.any(Function),
- });
+ expect(wrapper.vm.$toast.show).toHaveBeenCalledWith('Members were successfully added');
});
});
describe('when opened from a Learn GitLab page', () => {
it('emits the `showSuccessfulInvitationsAlert` event', async () => {
- eventHub.$emit('openModal', { inviteeType: 'members', source: LEARN_GITLAB });
+ await triggerOpenModal({ source: LEARN_GITLAB });
jest.spyOn(eventHub, '$emit').mockImplementation();
@@ -471,12 +363,10 @@ describe('InviteMembersModal', () => {
});
describe('when member is not added successfully', () => {
- beforeEach(() => {
+ beforeEach(async () => {
createInviteMembersToGroupWrapper();
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ newUsersToInvite: [user1] });
+ await triggerMembersTokenSelect([user1]);
});
it('displays "Member already exists" api message for http status conflict', async () => {
@@ -487,7 +377,6 @@ describe('InviteMembersModal', () => {
await waitForPromises();
expect(membersFormGroupInvalidFeedback()).toBe('Member already exists');
- expect(findMembersFormGroup().props('state')).toBe(false);
expect(findMembersSelect().props('validationState')).toBe(false);
expect(findInviteButton().props('loading')).toBe(false);
});
@@ -503,35 +392,31 @@ describe('InviteMembersModal', () => {
it('clears the error when the list of members to invite is cleared', async () => {
expect(membersFormGroupInvalidFeedback()).toBe('Member already exists');
- expect(findMembersFormGroup().props('state')).toBe(false);
expect(findMembersSelect().props('validationState')).toBe(false);
findMembersSelect().vm.$emit('clear');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(membersFormGroupInvalidFeedback()).toBe('');
- expect(findMembersFormGroup().props('state')).not.toBe(false);
expect(findMembersSelect().props('validationState')).not.toBe(false);
});
it('clears the error when the cancel button is clicked', async () => {
clickCancelButton();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(membersFormGroupInvalidFeedback()).toBe('');
- expect(findMembersFormGroup().props('state')).not.toBe(false);
expect(findMembersSelect().props('validationState')).not.toBe(false);
});
it('clears the error when the modal is hidden', async () => {
wrapper.findComponent(GlModal).vm.$emit('hide');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(membersFormGroupInvalidFeedback()).toBe('');
- expect(findMembersFormGroup().props('state')).not.toBe(false);
expect(findMembersSelect().props('validationState')).not.toBe(false);
});
});
@@ -544,7 +429,6 @@ describe('InviteMembersModal', () => {
await waitForPromises();
expect(membersFormGroupInvalidFeedback()).toBe('Member already exists');
- expect(findMembersFormGroup().props('state')).toBe(false);
expect(findMembersSelect().props('validationState')).toBe(false);
expect(findInviteButton().props('loading')).toBe(false);
@@ -553,8 +437,7 @@ describe('InviteMembersModal', () => {
await waitForPromises();
expect(membersFormGroupInvalidFeedback()).toBe('');
- expect(findMembersFormGroup().props('state')).not.toBe(false);
- expect(findMembersSelect().props('validationState')).not.toBe(false);
+ expect(findMembersSelect().props('validationState')).toBe(null);
expect(findInviteButton().props('loading')).toBe(false);
});
@@ -608,7 +491,7 @@ describe('InviteMembersModal', () => {
describe('when inviting a new user by email address', () => {
const postData = {
- access_level: defaultAccessLevel,
+ access_level: propsData.defaultAccessLevel,
expires_at: undefined,
email: 'email@example.com',
invite_source: inviteSource,
@@ -618,8 +501,9 @@ describe('InviteMembersModal', () => {
};
describe('when invites are sent successfully', () => {
- beforeEach(() => {
- createComponent({ newUsersToInvite: [user3] });
+ beforeEach(async () => {
+ createComponent();
+ await triggerMembersTokenSelect([user3]);
wrapper.vm.$toast = { show: jest.fn() };
jest.spyOn(Api, 'inviteGroupMembersByEmail').mockResolvedValue({ data: postData });
@@ -631,24 +515,20 @@ describe('InviteMembersModal', () => {
});
it('calls Api inviteGroupMembersByEmail with the correct params', () => {
- expect(Api.inviteGroupMembersByEmail).toHaveBeenCalledWith(id, postData);
+ expect(Api.inviteGroupMembersByEmail).toHaveBeenCalledWith(propsData.id, postData);
});
it('displays the successful toastMessage', () => {
- expect(wrapper.vm.$toast.show).toHaveBeenCalledWith('Members were successfully added', {
- onComplete: expect.any(Function),
- });
+ expect(wrapper.vm.$toast.show).toHaveBeenCalledWith('Members were successfully added');
});
});
});
describe('when invites are not sent successfully', () => {
- beforeEach(() => {
+ beforeEach(async () => {
createInviteMembersToGroupWrapper();
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ newUsersToInvite: [user3] });
+ await triggerMembersTokenSelect([user3]);
});
it('displays the api error for invalid email syntax', async () => {
@@ -683,9 +563,7 @@ describe('InviteMembersModal', () => {
await waitForPromises();
- expect(wrapper.vm.$toast.show).toHaveBeenCalledWith('Members were successfully added', {
- onComplete: expect.any(Function),
- });
+ expect(wrapper.vm.$toast.show).toHaveBeenCalledWith('Members were successfully added');
expect(findMembersSelect().props('validationState')).toBe(null);
});
@@ -716,9 +594,7 @@ describe('InviteMembersModal', () => {
it('displays the invalid syntax error if one of the emails is invalid', async () => {
createInviteMembersToGroupWrapper();
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ newUsersToInvite: [user3, user4] });
+ await triggerMembersTokenSelect([user3, user4]);
mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.ERROR_EMAIL_INVALID);
clickInviteButton();
@@ -733,7 +609,7 @@ describe('InviteMembersModal', () => {
describe('when inviting members and non-members in same click', () => {
const postData = {
- access_level: defaultAccessLevel,
+ access_level: propsData.defaultAccessLevel,
expires_at: undefined,
invite_source: inviteSource,
format: 'json',
@@ -745,8 +621,9 @@ describe('InviteMembersModal', () => {
const idPostData = { ...postData, user_id: '1' };
describe('when invites are sent successfully', () => {
- beforeEach(() => {
- createComponent({ newUsersToInvite: [user1, user3] });
+ beforeEach(async () => {
+ createComponent();
+ await triggerMembersTokenSelect([user1, user3]);
wrapper.vm.$toast = { show: jest.fn() };
jest.spyOn(Api, 'inviteGroupMembersByEmail').mockResolvedValue({ data: postData });
@@ -759,30 +636,28 @@ describe('InviteMembersModal', () => {
});
it('calls Api inviteGroupMembersByEmail with the correct params', () => {
- expect(Api.inviteGroupMembersByEmail).toHaveBeenCalledWith(id, emailPostData);
+ expect(Api.inviteGroupMembersByEmail).toHaveBeenCalledWith(propsData.id, emailPostData);
});
it('calls Api addGroupMembersByUserId with the correct params', () => {
- expect(Api.addGroupMembersByUserId).toHaveBeenCalledWith(id, idPostData);
+ expect(Api.addGroupMembersByUserId).toHaveBeenCalledWith(propsData.id, idPostData);
});
it('displays the successful toastMessage', () => {
- expect(wrapper.vm.$toast.show).toHaveBeenCalledWith('Members were successfully added', {
- onComplete: expect.any(Function),
- });
+ expect(wrapper.vm.$toast.show).toHaveBeenCalledWith('Members were successfully added');
});
});
- it('calls Apis with the invite source passed through to openModal', () => {
- eventHub.$emit('openModal', { inviteeType: 'members', source: '_invite_source_' });
+ it('calls Apis with the invite source passed through to openModal', async () => {
+ await triggerOpenModal({ source: '_invite_source_' });
clickInviteButton();
- expect(Api.inviteGroupMembersByEmail).toHaveBeenCalledWith(id, {
+ expect(Api.inviteGroupMembersByEmail).toHaveBeenCalledWith(propsData.id, {
...emailPostData,
invite_source: '_invite_source_',
});
- expect(Api.addGroupMembersByUserId).toHaveBeenCalledWith(id, {
+ expect(Api.addGroupMembersByUserId).toHaveBeenCalledWith(propsData.id, {
...idPostData,
invite_source: '_invite_source_',
});
@@ -790,12 +665,10 @@ describe('InviteMembersModal', () => {
});
describe('when any invite failed for any reason', () => {
- beforeEach(() => {
+ beforeEach(async () => {
createInviteMembersToGroupWrapper();
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ newUsersToInvite: [user1, user3] });
+ await triggerMembersTokenSelect([user1, user3]);
mockInvitationsApi(httpStatus.BAD_REQUEST, invitationsApiResponse.EMAIL_INVALID);
mockMembersApi(httpStatus.OK, '200 OK');
@@ -811,71 +684,17 @@ describe('InviteMembersModal', () => {
});
});
- describe('when inviting a group to share', () => {
- describe('when sharing the group is successful', () => {
- const groupPostData = {
- group_id: sharedGroup.id,
- group_access: defaultAccessLevel,
- expires_at: undefined,
- format: 'json',
- };
-
- beforeEach(() => {
- createComponent({ groupToBeSharedWith: sharedGroup });
-
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ inviteeType: 'group' });
- wrapper.vm.$toast = { show: jest.fn() };
- jest.spyOn(Api, 'groupShareWithGroup').mockResolvedValue({ data: groupPostData });
-
- clickInviteButton();
- });
-
- it('calls Api groupShareWithGroup with the correct params', () => {
- expect(Api.groupShareWithGroup).toHaveBeenCalledWith(id, groupPostData);
- });
-
- it('displays the successful toastMessage', () => {
- expect(wrapper.vm.$toast.show).toHaveBeenCalledWith('Members were successfully added', {
- onComplete: expect.any(Function),
- });
- });
- });
-
- describe('when sharing the group fails', () => {
- beforeEach(() => {
- createInviteGroupToGroupWrapper();
-
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ groupToBeSharedWith: sharedGroup });
- wrapper.vm.$toast = { show: jest.fn() };
-
- jest
- .spyOn(Api, 'groupShareWithGroup')
- .mockRejectedValue({ response: { data: { success: false } } });
-
- clickInviteButton();
- });
-
- it('displays the generic error message', () => {
- expect(membersFormGroupInvalidFeedback()).toBe('Something went wrong');
- expect(membersFormGroupDescription()).toBe('');
- });
- });
- });
-
describe('tracking', () => {
- beforeEach(() => {
- createComponent({ newUsersToInvite: [user3] });
+ beforeEach(async () => {
+ createComponent();
+ await triggerMembersTokenSelect([user3]);
wrapper.vm.$toast = { show: jest.fn() };
jest.spyOn(Api, 'inviteGroupMembersByEmail').mockResolvedValue({});
});
it('tracks the view for learn_gitlab source', () => {
- eventHub.$emit('openModal', { inviteeType: 'members', source: LEARN_GITLAB });
+ eventHub.$emit('openModal', { source: LEARN_GITLAB });
expect(ExperimentTracking).toHaveBeenCalledWith(INVITE_MEMBERS_FOR_TASK.name);
expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith(LEARN_GITLAB);
diff --git a/spec/frontend/invite_members/components/invite_members_trigger_spec.js b/spec/frontend/invite_members/components/invite_members_trigger_spec.js
index 429b6fad24a..28402c8331c 100644
--- a/spec/frontend/invite_members/components/invite_members_trigger_spec.js
+++ b/spec/frontend/invite_members/components/invite_members_trigger_spec.js
@@ -71,7 +71,6 @@ describe.each(triggerItems)('with triggerElement as %s', (triggerItem) => {
findButton().vm.$emit('click');
expect(spy).toHaveBeenCalledWith('openModal', {
- inviteeType: 'members',
source: triggerSource,
});
});
diff --git a/spec/frontend/invite_members/components/invite_modal_base_spec.js b/spec/frontend/invite_members/components/invite_modal_base_spec.js
new file mode 100644
index 00000000000..4b183bfd670
--- /dev/null
+++ b/spec/frontend/invite_members/components/invite_modal_base_spec.js
@@ -0,0 +1,103 @@
+import {
+ GlDropdown,
+ GlDropdownItem,
+ GlDatepicker,
+ GlFormGroup,
+ GlSprintf,
+ GlLink,
+ GlModal,
+} from '@gitlab/ui';
+import { stubComponent } from 'helpers/stub_component';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import InviteModalBase from '~/invite_members/components/invite_modal_base.vue';
+import { CANCEL_BUTTON_TEXT, INVITE_BUTTON_TEXT } from '~/invite_members/constants';
+import { propsData } from '../mock_data/modal_base';
+
+describe('InviteModalBase', () => {
+ let wrapper;
+
+ const createComponent = (data = {}, props = {}) => {
+ wrapper = shallowMountExtended(InviteModalBase, {
+ propsData: {
+ ...propsData,
+ ...props,
+ },
+ data() {
+ return data;
+ },
+ stubs: {
+ GlModal: stubComponent(GlModal, {
+ template:
+ '<div><slot name="modal-title"></slot><slot></slot><slot name="modal-footer"></slot></div>',
+ }),
+ GlDropdown: true,
+ GlDropdownItem: true,
+ GlSprintf,
+ GlFormGroup: stubComponent(GlFormGroup, {
+ props: ['state', 'invalidFeedback', 'description'],
+ }),
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findDropdownItems = () => findDropdown().findAllComponents(GlDropdownItem);
+ const findDatepicker = () => wrapper.findComponent(GlDatepicker);
+ const findLink = () => wrapper.findComponent(GlLink);
+ const findIntroText = () => wrapper.findByTestId('modal-base-intro-text').text();
+ const findCancelButton = () => wrapper.findByTestId('cancel-button');
+ const findInviteButton = () => wrapper.findByTestId('invite-button');
+
+ describe('rendering the modal', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders the modal with the correct title', () => {
+ expect(wrapper.findComponent(GlModal).props('title')).toBe(propsData.modalTitle);
+ });
+
+ it('displays the introText', () => {
+ expect(findIntroText()).toBe(propsData.labelIntroText);
+ });
+
+ it('renders the Cancel button text correctly', () => {
+ expect(findCancelButton().text()).toBe(CANCEL_BUTTON_TEXT);
+ });
+
+ it('renders the Invite button text correctly', () => {
+ expect(findInviteButton().text()).toBe(INVITE_BUTTON_TEXT);
+ });
+
+ it('renders the Invite button modal without isLoading', () => {
+ expect(findInviteButton().props('loading')).toBe(false);
+ });
+
+ describe('rendering the access levels dropdown', () => {
+ it('sets the default dropdown text to the default access level name', () => {
+ expect(findDropdown().attributes('text')).toBe('Guest');
+ });
+
+ it('renders dropdown items for each accessLevel', () => {
+ expect(findDropdownItems()).toHaveLength(5);
+ });
+ });
+
+ describe('rendering the help link', () => {
+ it('renders the correct link', () => {
+ expect(findLink().attributes('href')).toBe(propsData.helpLink);
+ });
+ });
+
+ describe('rendering the access expiration date field', () => {
+ it('renders the datepicker', () => {
+ expect(findDatepicker().exists()).toBe(true);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/invite_members/mock_data/group_modal.js b/spec/frontend/invite_members/mock_data/group_modal.js
new file mode 100644
index 00000000000..c05c4edb7d0
--- /dev/null
+++ b/spec/frontend/invite_members/mock_data/group_modal.js
@@ -0,0 +1,11 @@
+export const propsData = {
+ id: '1',
+ name: 'test name',
+ isProject: false,
+ invalidGroups: [],
+ accessLevels: { Guest: 10, Reporter: 20, Developer: 30, Maintainer: 40, Owner: 50 },
+ defaultAccessLevel: 10,
+ helpLink: 'https://example.com',
+};
+
+export const sharedGroup = { id: '981' };
diff --git a/spec/frontend/invite_members/mock_data/member_modal.js b/spec/frontend/invite_members/mock_data/member_modal.js
new file mode 100644
index 00000000000..590502909b2
--- /dev/null
+++ b/spec/frontend/invite_members/mock_data/member_modal.js
@@ -0,0 +1,36 @@
+export const propsData = {
+ id: '1',
+ name: 'test name',
+ isProject: false,
+ accessLevels: { Guest: 10, Reporter: 20, Developer: 30, Maintainer: 40, Owner: 50 },
+ defaultAccessLevel: 30,
+ helpLink: 'https://example.com',
+ tasksToBeDoneOptions: [
+ { text: 'First task', value: 'first' },
+ { text: 'Second task', value: 'second' },
+ ],
+ projects: [
+ { text: 'First project', value: '1' },
+ { text: 'Second project', value: '2' },
+ ],
+};
+
+export const inviteSource = 'unknown';
+export const newProjectPath = 'projects/new';
+
+export const user1 = { id: 1, name: 'Name One', username: 'one_1', avatar_url: '' };
+export const user2 = { id: 2, name: 'Name Two', username: 'one_2', avatar_url: '' };
+export const user3 = {
+ id: 'user-defined-token',
+ name: 'email@example.com',
+ username: 'one_2',
+ avatar_url: '',
+};
+export const user4 = {
+ id: 'user-defined-token',
+ name: 'email4@example.com',
+ username: 'one_4',
+ avatar_url: '',
+};
+
+export const GlEmoji = { template: '<img/>' };
diff --git a/spec/frontend/invite_members/mock_data/modal_base.js b/spec/frontend/invite_members/mock_data/modal_base.js
new file mode 100644
index 00000000000..ea5a8d2b00d
--- /dev/null
+++ b/spec/frontend/invite_members/mock_data/modal_base.js
@@ -0,0 +1,11 @@
+export const propsData = {
+ modalTitle: '_modal_title_',
+ modalId: '_modal_id_',
+ name: '_name_',
+ accessLevels: { Guest: 10, Reporter: 20, Developer: 30, Maintainer: 40, Owner: 50 },
+ defaultAccessLevel: 10,
+ helpLink: 'https://example.com',
+ labelIntroText: '_label_intro_text_',
+ labelSearchField: '_label_search_field_',
+ formGroupDescription: '_form_group_description_',
+};
diff --git a/spec/frontend/issuable/components/issue_milestone_spec.js b/spec/frontend/issuable/components/issue_milestone_spec.js
index 44416676180..9d67f602136 100644
--- a/spec/frontend/issuable/components/issue_milestone_spec.js
+++ b/spec/frontend/issuable/components/issue_milestone_spec.js
@@ -1,6 +1,6 @@
import { GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import { mockMilestone } from 'jest/boards/mock_data';
import IssueMilestone from '~/issuable/components/issue_milestone.vue';
@@ -19,12 +19,12 @@ describe('IssueMilestoneComponent', () => {
let wrapper;
let vm;
- beforeEach((done) => {
+ beforeEach(async () => {
wrapper = createComponent();
({ vm } = wrapper);
- Vue.nextTick(done);
+ await nextTick();
});
afterEach(() => {
@@ -37,7 +37,7 @@ describe('IssueMilestoneComponent', () => {
wrapper.setProps({
milestone: { ...mockMilestone, start_date: '' },
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.vm.isMilestoneStarted).toBe(false);
});
@@ -46,7 +46,7 @@ describe('IssueMilestoneComponent', () => {
await wrapper.setProps({
milestone: { ...mockMilestone, start_date: '1990-07-22' },
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.vm.isMilestoneStarted).toBe(true);
});
@@ -57,7 +57,7 @@ describe('IssueMilestoneComponent', () => {
wrapper.setProps({
milestone: { ...mockMilestone, due_date: '' },
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.vm.isMilestonePastDue).toBe(false);
});
@@ -80,7 +80,7 @@ describe('IssueMilestoneComponent', () => {
wrapper.setProps({
milestone: { ...mockMilestone, due_date: '' },
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.vm.milestoneDatesAbsolute).toBe('(January 1, 2018)');
});
@@ -89,7 +89,7 @@ describe('IssueMilestoneComponent', () => {
wrapper.setProps({
milestone: { ...mockMilestone, start_date: '', due_date: '' },
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.vm.milestoneDatesAbsolute).toBe('');
});
@@ -100,7 +100,7 @@ describe('IssueMilestoneComponent', () => {
wrapper.setProps({
milestone: { ...mockMilestone, due_date: `${new Date().getFullYear() + 10}-01-01` },
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.vm.milestoneDatesHuman).toContain('years remaining');
});
@@ -109,7 +109,7 @@ describe('IssueMilestoneComponent', () => {
wrapper.setProps({
milestone: { ...mockMilestone, start_date: '1990-07-22', due_date: '' },
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.vm.milestoneDatesHuman).toContain('Started');
});
@@ -122,7 +122,7 @@ describe('IssueMilestoneComponent', () => {
due_date: '',
},
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.vm.milestoneDatesHuman).toContain('Starts');
});
@@ -131,7 +131,7 @@ describe('IssueMilestoneComponent', () => {
wrapper.setProps({
milestone: { ...mockMilestone, start_date: '', due_date: '' },
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.vm.milestoneDatesHuman).toBe('');
});
diff --git a/spec/frontend/issuable/components/related_issuable_item_spec.js b/spec/frontend/issuable/components/related_issuable_item_spec.js
index 6a896ccd21a..6b48f83041a 100644
--- a/spec/frontend/issuable/components/related_issuable_item_spec.js
+++ b/spec/frontend/issuable/components/related_issuable_item_spec.js
@@ -1,4 +1,5 @@
import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import { TEST_HOST } from 'helpers/test_constants';
import IssueDueDate from '~/boards/components/issue_due_date.vue';
import { formatDate } from '~/lib/utils/datetime_utility';
@@ -105,7 +106,7 @@ describe('RelatedIssuableItem', () => {
state: 'closed',
closedAt: '2018-12-01T00:00:00.00Z',
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(tokenState().classes('issue-token-state-icon-closed')).toBe(true);
});
@@ -140,7 +141,7 @@ describe('RelatedIssuableItem', () => {
closedAt: '2018-12-01T00:00:00.00Z',
},
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.find(IssueDueDate).props('closed')).toBe(true);
});
@@ -172,14 +173,14 @@ describe('RelatedIssuableItem', () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ removeDisabled: true });
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findRemoveButton().attributes('disabled')).toEqual('disabled');
});
it('triggers onRemoveRequest when clicked', async () => {
findRemoveButton().trigger('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
const { relatedIssueRemoveRequest } = wrapper.emitted();
expect(relatedIssueRemoveRequest.length).toBe(1);
diff --git a/spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js b/spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js
index ff6922989cb..ce98a16dbb7 100644
--- a/spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js
+++ b/spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js
@@ -1,4 +1,6 @@
+import { GlFormGroup } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import AddIssuableForm from '~/related_issues/components/add_issuable_form.vue';
import IssueToken from '~/related_issues/components/issue_token.vue';
import { issuableTypesMap, linkedIssueTypesMap, PathIdSeparator } from '~/related_issues/constants';
@@ -152,6 +154,30 @@ describe('AddIssuableForm', () => {
});
});
+ describe('categorized issuables', () => {
+ it.each`
+ issuableType | pathIdSeparator | contextHeader | contextFooter
+ ${issuableTypesMap.ISSUE} | ${PathIdSeparator.Issue} | ${'The current issue'} | ${'the following issue(s)'}
+ ${issuableTypesMap.EPIC} | ${PathIdSeparator.Epic} | ${'The current epic'} | ${'the following epic(s)'}
+ `(
+ 'show header text as "$contextHeader" and footer text as "$contextFooter" issuableType is set to $issuableType',
+ ({ issuableType, contextHeader, contextFooter }) => {
+ wrapper = shallowMount(AddIssuableForm, {
+ propsData: {
+ issuableType,
+ inputValue: '',
+ showCategorizedIssues: true,
+ pathIdSeparator,
+ pendingReferences: [],
+ },
+ });
+
+ expect(wrapper.findComponent(GlFormGroup).attributes('label')).toBe(contextHeader);
+ expect(wrapper.find('p.bold').text()).toContain(contextFooter);
+ },
+ );
+ });
+
describe('when it is a Linked Issues form', () => {
beforeEach(() => {
wrapper = mount(AddIssuableForm, {
@@ -194,63 +220,55 @@ describe('AddIssuableForm', () => {
});
describe('when the form is submitted', () => {
- it('emits an event with a "relates_to" link type when the "relates to" radio input selected', (done) => {
+ it('emits an event with a "relates_to" link type when the "relates to" radio input selected', async () => {
jest.spyOn(wrapper.vm, '$emit').mockImplementation(() => {});
wrapper.vm.linkedIssueType = linkedIssueTypesMap.RELATES_TO;
wrapper.vm.onFormSubmit();
- wrapper.vm.$nextTick(() => {
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('addIssuableFormSubmit', {
- pendingReferences: '',
- linkedIssueType: linkedIssueTypesMap.RELATES_TO,
- });
- done();
+ await nextTick();
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('addIssuableFormSubmit', {
+ pendingReferences: '',
+ linkedIssueType: linkedIssueTypesMap.RELATES_TO,
});
});
- it('emits an event with a "blocks" link type when the "blocks" radio input selected', (done) => {
+ it('emits an event with a "blocks" link type when the "blocks" radio input selected', async () => {
jest.spyOn(wrapper.vm, '$emit').mockImplementation(() => {});
wrapper.vm.linkedIssueType = linkedIssueTypesMap.BLOCKS;
wrapper.vm.onFormSubmit();
- wrapper.vm.$nextTick(() => {
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('addIssuableFormSubmit', {
- pendingReferences: '',
- linkedIssueType: linkedIssueTypesMap.BLOCKS,
- });
- done();
+ await nextTick();
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('addIssuableFormSubmit', {
+ pendingReferences: '',
+ linkedIssueType: linkedIssueTypesMap.BLOCKS,
});
});
- it('emits an event with a "is_blocked_by" link type when the "is blocked by" radio input selected', (done) => {
+ it('emits an event with a "is_blocked_by" link type when the "is blocked by" radio input selected', async () => {
jest.spyOn(wrapper.vm, '$emit').mockImplementation(() => {});
wrapper.vm.linkedIssueType = linkedIssueTypesMap.IS_BLOCKED_BY;
wrapper.vm.onFormSubmit();
- wrapper.vm.$nextTick(() => {
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('addIssuableFormSubmit', {
- pendingReferences: '',
- linkedIssueType: linkedIssueTypesMap.IS_BLOCKED_BY,
- });
- done();
+ await nextTick();
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('addIssuableFormSubmit', {
+ pendingReferences: '',
+ linkedIssueType: linkedIssueTypesMap.IS_BLOCKED_BY,
});
});
- it('shows error message when error is present', (done) => {
+ it('shows error message when error is present', async () => {
const itemAddFailureMessage = 'Something went wrong while submitting.';
wrapper.setProps({
hasError: true,
itemAddFailureMessage,
});
- wrapper.vm.$nextTick(() => {
- expect(wrapper.find('.gl-field-error').exists()).toBe(true);
- expect(wrapper.find('.gl-field-error').text()).toContain(itemAddFailureMessage);
- done();
- });
+ await nextTick();
+ expect(wrapper.find('.gl-field-error').exists()).toBe(true);
+ expect(wrapper.find('.gl-field-error').text()).toContain(itemAddFailureMessage);
});
});
});
diff --git a/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js b/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js
index 608fec45bbd..c7925034eb0 100644
--- a/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js
+++ b/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js
@@ -7,6 +7,7 @@ import {
} from 'jest/issuable/components/related_issuable_mock_data';
import RelatedIssuesBlock from '~/related_issues/components/related_issues_block.vue';
import {
+ issuableTypesMap,
linkedIssueTypesMap,
linkedIssueTypesTextMap,
PathIdSeparator,
@@ -29,14 +30,34 @@ describe('RelatedIssuesBlock', () => {
wrapper = mount(RelatedIssuesBlock, {
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
- issuableType: 'issue',
+ issuableType: issuableTypesMap.ISSUE,
},
});
});
- it('displays "Linked issues" in the header', () => {
- expect(wrapper.find('.card-title').text()).toContain('Linked issues');
- });
+ it.each`
+ issuableType | pathIdSeparator | titleText | helpLinkText | addButtonText
+ ${'issue'} | ${PathIdSeparator.Issue} | ${'Linked issues'} | ${'Read more about related issues'} | ${'Add a related issue'}
+ ${'epic'} | ${PathIdSeparator.Epic} | ${'Linked epics'} | ${'Read more about related epics'} | ${'Add a related epic'}
+ `(
+ 'displays "$titleText" in the header, "$helpLinkText" aria-label for help link, and "$addButtonText" aria-label for add button when issuableType is set to "$issuableType"',
+ ({ issuableType, pathIdSeparator, titleText, helpLinkText, addButtonText }) => {
+ wrapper = mount(RelatedIssuesBlock, {
+ propsData: {
+ pathIdSeparator,
+ issuableType,
+ canAdmin: true,
+ helpPath: '/help/user/project/issues/related_issues',
+ },
+ });
+
+ expect(wrapper.find('.card-title').text()).toContain(titleText);
+ expect(wrapper.find('[data-testid="help-link"]').attributes('aria-label')).toBe(
+ helpLinkText,
+ );
+ expect(findIssueCountBadgeAddButton().attributes('aria-label')).toBe(addButtonText);
+ },
+ );
it('unable to add new related issues', () => {
expect(findIssueCountBadgeAddButton().exists()).toBe(false);
diff --git a/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js b/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js
index 01de4da7900..b59717a1f60 100644
--- a/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js
+++ b/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js
@@ -1,5 +1,6 @@
import { mount, shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
import {
defaultProps,
@@ -210,40 +211,37 @@ describe('RelatedIssuesRoot', () => {
}),
);
- it('when canceling and hiding add issuable form', () => {
+ it('when canceling and hiding add issuable form', async () => {
wrapper.vm.onPendingFormCancel();
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.vm.isFormVisible).toEqual(false);
- expect(wrapper.vm.inputValue).toEqual('');
- expect(wrapper.vm.state.pendingReferences).toHaveLength(0);
- });
+ await nextTick();
+ expect(wrapper.vm.isFormVisible).toEqual(false);
+ expect(wrapper.vm.inputValue).toEqual('');
+ expect(wrapper.vm.state.pendingReferences).toHaveLength(0);
});
});
describe('fetchRelatedIssues', () => {
beforeEach(() => createComponent());
- it('sets isFetching while fetching', () => {
+ it('sets isFetching while fetching', async () => {
wrapper.vm.fetchRelatedIssues();
expect(wrapper.vm.isFetching).toEqual(true);
- return waitForPromises().then(() => {
- expect(wrapper.vm.isFetching).toEqual(false);
- });
+ await waitForPromises();
+ expect(wrapper.vm.isFetching).toEqual(false);
});
- it('should fetch related issues', () => {
+ it('should fetch related issues', async () => {
mock.onGet(defaultProps.endpoint).reply(200, [issuable1, issuable2]);
wrapper.vm.fetchRelatedIssues();
- return waitForPromises().then(() => {
- expect(wrapper.vm.state.relatedIssues).toHaveLength(2);
- expect(wrapper.vm.state.relatedIssues[0].id).toEqual(issuable1.id);
- expect(wrapper.vm.state.relatedIssues[1].id).toEqual(issuable2.id);
- });
+ await waitForPromises();
+ expect(wrapper.vm.state.relatedIssues).toHaveLength(2);
+ expect(wrapper.vm.state.relatedIssues[0].id).toEqual(issuable1.id);
+ expect(wrapper.vm.state.relatedIssues[1].id).toEqual(issuable2.id);
});
});
diff --git a/spec/frontend/issues/create_merge_request_dropdown_spec.js b/spec/frontend/issues/create_merge_request_dropdown_spec.js
index fdc0bd7d72e..637b4d31999 100644
--- a/spec/frontend/issues/create_merge_request_dropdown_spec.js
+++ b/spec/frontend/issues/create_merge_request_dropdown_spec.js
@@ -59,7 +59,7 @@ describe('CreateMergeRequestDropdown', () => {
describe('updateCreatePaths', () => {
it('escapes branch names correctly', () => {
dropdown.createBranchPath = `${TEST_HOST}/branches?branch_name=some-branch&issue=42`;
- dropdown.createMrPath = `${TEST_HOST}/create_merge_request?branch_name=some-branch&ref=main`;
+ dropdown.createMrPath = `${TEST_HOST}/create_merge_request?merge_request%5Bsource_branch%5D=test&merge_request%5Btarget_branch%5D=master`;
dropdown.updateCreatePaths('branch', 'contains#hash');
@@ -68,7 +68,7 @@ describe('CreateMergeRequestDropdown', () => {
);
expect(dropdown.createMrPath).toBe(
- `${TEST_HOST}/create_merge_request?branch_name=contains%23hash&ref=main`,
+ `${TEST_HOST}/create_merge_request?merge_request%5Bsource_branch%5D=contains%23hash&merge_request%5Btarget_branch%5D=master`,
);
});
});
diff --git a/spec/frontend/issues/list/components/issues_list_app_spec.js b/spec/frontend/issues/list/components/issues_list_app_spec.js
index 66428ee0492..88652ddc3cc 100644
--- a/spec/frontend/issues/list/components/issues_list_app_spec.js
+++ b/spec/frontend/issues/list/components/issues_list_app_spec.js
@@ -16,6 +16,8 @@ import {
getIssuesQueryResponse,
filteredTokens,
locationSearch,
+ setSortPreferenceMutationResponse,
+ setSortPreferenceMutationResponseWithErrors,
urlParams,
} from 'jest/issues/list/mock_data';
import createFlash, { FLASH_TYPES } from '~/flash';
@@ -28,8 +30,6 @@ import IssuesListApp from '~/issues/list/components/issues_list_app.vue';
import NewIssueDropdown from '~/issues/list/components/new_issue_dropdown.vue';
import {
CREATED_DESC,
- DUE_DATE_OVERDUE,
- PARAM_DUE_DATE,
RELATIVE_POSITION,
RELATIVE_POSITION_ASC,
TOKEN_TYPE_ASSIGNEE,
@@ -43,16 +43,15 @@ import {
urlSortParams,
} from '~/issues/list/constants';
import eventHub from '~/issues/list/eventhub';
-import { getSortOptions } from '~/issues/list/utils';
+import setSortPreferenceMutation from '~/issues/list/queries/set_sort_preference.mutation.graphql';
+import { getSortKey, getSortOptions } from '~/issues/list/utils';
import axios from '~/lib/utils/axios_utils';
import { scrollUp } from '~/lib/utils/scroll_utils';
import { joinPaths } from '~/lib/utils/url_utility';
jest.mock('@sentry/browser');
jest.mock('~/flash');
-jest.mock('~/lib/utils/scroll_utils', () => ({
- scrollUp: jest.fn().mockName('scrollUpMock'),
-}));
+jest.mock('~/lib/utils/scroll_utils', () => ({ scrollUp: jest.fn() }));
describe('CE IssuesListApp component', () => {
let axiosMock;
@@ -61,6 +60,7 @@ describe('CE IssuesListApp component', () => {
Vue.use(VueApollo);
const defaultProvide = {
+ autocompleteAwardEmojisPath: 'autocomplete/award/emojis/path',
calendarPath: 'calendar/path',
canBulkUpdate: false,
emptyStateSvgPath: 'empty-state.svg',
@@ -72,10 +72,16 @@ describe('CE IssuesListApp component', () => {
hasIssuableHealthStatusFeature: true,
hasIssueWeightsFeature: true,
hasIterationsFeature: true,
+ hasMultipleIssueAssigneesFeature: true,
+ initialEmail: 'email@example.com',
+ initialSort: CREATED_DESC,
+ isAnonymousSearchDisabled: false,
+ isIssueRepositioningDisabled: false,
isProject: true,
isSignedIn: true,
jiraIntegrationPath: 'jira/integration/path',
newIssuePath: 'new/issue/path',
+ releasesPath: 'releases/path',
rssPath: 'rss/path',
showNewIssueLink: true,
signInPath: 'sign/in/path',
@@ -103,11 +109,13 @@ describe('CE IssuesListApp component', () => {
provide = {},
issuesQueryResponse = jest.fn().mockResolvedValue(defaultQueryResponse),
issuesCountsQueryResponse = jest.fn().mockResolvedValue(getIssuesCountsQueryResponse),
+ sortPreferenceMutationResponse = jest.fn().mockResolvedValue(setSortPreferenceMutationResponse),
mountFn = shallowMount,
} = {}) => {
const requestHandlers = [
[getIssuesQuery, issuesQueryResponse],
[getIssuesCountsQuery, issuesCountsQueryResponse],
+ [setSortPreferenceMutation, sortPreferenceMutationResponse],
];
const apolloProvider = createMockApollo(requestHandlers);
@@ -131,9 +139,10 @@ describe('CE IssuesListApp component', () => {
});
describe('IssuableList', () => {
- beforeEach(() => {
+ beforeEach(async () => {
wrapper = mountComponent();
jest.runOnlyPendingTimers();
+ await waitForPromises();
});
it('renders', () => {
@@ -167,8 +176,9 @@ describe('CE IssuesListApp component', () => {
});
describe('header action buttons', () => {
- it('renders rss button', () => {
+ it('renders rss button', async () => {
wrapper = mountComponent({ mountFn: mount });
+ await waitForPromises();
expect(findGlButtonAt(0).props('icon')).toBe('rss');
expect(findGlButtonAt(0).attributes()).toMatchObject({
@@ -177,8 +187,9 @@ describe('CE IssuesListApp component', () => {
});
});
- it('renders calendar button', () => {
+ it('renders calendar button', async () => {
wrapper = mountComponent({ mountFn: mount });
+ await waitForPromises();
expect(findGlButtonAt(1).props('icon')).toBe('calendar');
expect(findGlButtonAt(1).attributes()).toMatchObject({
@@ -189,19 +200,21 @@ describe('CE IssuesListApp component', () => {
describe('csv import/export component', () => {
describe('when user is signed in', () => {
- const search = '?search=refactor&sort=created_date&state=opened';
+ beforeEach(async () => {
+ setWindowLocation('?search=refactor&state=opened');
- beforeEach(() => {
- setWindowLocation(search);
-
- wrapper = mountComponent({ provide: { isSignedIn: true }, mountFn: mount });
+ wrapper = mountComponent({
+ provide: { initialSortBy: CREATED_DESC, isSignedIn: true },
+ mountFn: mount,
+ });
jest.runOnlyPendingTimers();
+ await waitForPromises();
});
it('renders', () => {
expect(findCsvImportExportButtons().props()).toMatchObject({
- exportCsvPath: `${defaultProvide.exportCsvPath}${search}`,
+ exportCsvPath: `${defaultProvide.exportCsvPath}?search=refactor&sort=created_date&state=opened`,
issuableCount: 1,
});
});
@@ -281,16 +294,6 @@ describe('CE IssuesListApp component', () => {
});
describe('initial url params', () => {
- describe('due_date', () => {
- it('is set from the url params', () => {
- setWindowLocation(`?${PARAM_DUE_DATE}=${DUE_DATE_OVERDUE}`);
-
- wrapper = mountComponent();
-
- expect(findIssuableList().props('urlParams')).toMatchObject({ due_date: DUE_DATE_OVERDUE });
- });
- });
-
describe('search', () => {
it('is set from the url params', () => {
setWindowLocation(locationSearch);
@@ -302,31 +305,57 @@ describe('CE IssuesListApp component', () => {
});
describe('sort', () => {
- it.each(Object.keys(urlSortParams))('is set as %s from the url params', (sortKey) => {
- setWindowLocation(`?sort=${urlSortParams[sortKey]}`);
+ describe('when initial sort value uses old enum values', () => {
+ const oldEnumSortValues = Object.values(urlSortParams);
- wrapper = mountComponent();
+ it.each(oldEnumSortValues)('initial sort is set with value %s', (sort) => {
+ wrapper = mountComponent({ provide: { initialSort: sort } });
- expect(findIssuableList().props()).toMatchObject({
- initialSortBy: sortKey,
- urlParams: {
- sort: urlSortParams[sortKey],
- },
+ expect(findIssuableList().props()).toMatchObject({
+ initialSortBy: getSortKey(sort),
+ urlParams: { sort },
+ });
+ });
+ });
+
+ describe('when initial sort value uses new GraphQL enum values', () => {
+ const graphQLEnumSortValues = Object.keys(urlSortParams);
+
+ it.each(graphQLEnumSortValues)('initial sort is set with value %s', (sort) => {
+ wrapper = mountComponent({ provide: { initialSort: sort.toLowerCase() } });
+
+ expect(findIssuableList().props()).toMatchObject({
+ initialSortBy: sort,
+ urlParams: { sort: urlSortParams[sort] },
+ });
});
});
- describe('when issue repositioning is disabled and the sort is manual', () => {
+ describe('when initial sort value is invalid', () => {
+ it.each(['', 'asdf', null, undefined])(
+ 'initial sort is set to value CREATED_DESC',
+ (sort) => {
+ wrapper = mountComponent({ provide: { initialSort: sort } });
+
+ expect(findIssuableList().props()).toMatchObject({
+ initialSortBy: CREATED_DESC,
+ urlParams: { sort: urlSortParams[CREATED_DESC] },
+ });
+ },
+ );
+ });
+
+ describe('when sort is manual and issue repositioning is disabled', () => {
beforeEach(() => {
- setWindowLocation(`?sort=${RELATIVE_POSITION}`);
- wrapper = mountComponent({ provide: { isIssueRepositioningDisabled: true } });
+ wrapper = mountComponent({
+ provide: { initialSort: RELATIVE_POSITION, isIssueRepositioningDisabled: true },
+ });
});
it('changes the sort to the default of created descending', () => {
expect(findIssuableList().props()).toMatchObject({
initialSortBy: CREATED_DESC,
- urlParams: {
- sort: urlSortParams[CREATED_DESC],
- },
+ urlParams: { sort: urlSortParams[CREATED_DESC] },
});
});
@@ -585,16 +614,17 @@ describe('CE IssuesListApp component', () => {
${'fetching issues'} | ${'issuesQueryResponse'} | ${IssuesListApp.i18n.errorFetchingIssues}
${'fetching issue counts'} | ${'issuesCountsQueryResponse'} | ${IssuesListApp.i18n.errorFetchingCounts}
`('when there is an error $error', ({ mountOption, message }) => {
- beforeEach(() => {
+ beforeEach(async () => {
wrapper = mountComponent({
[mountOption]: jest.fn().mockRejectedValue(new Error('ERROR')),
});
jest.runOnlyPendingTimers();
+ await waitForPromises();
});
it('shows an error message', () => {
expect(findIssuableList().props('error')).toBe(message);
- expect(Sentry.captureException).toHaveBeenCalledWith(new Error('Network error: ERROR'));
+ expect(Sentry.captureException).toHaveBeenCalledWith(new Error('ERROR'));
});
});
@@ -687,12 +717,13 @@ describe('CE IssuesListApp component', () => {
`(
'when moving issue $description',
({ issueToMove, oldIndex, newIndex, moveBeforeId, moveAfterId }) => {
- beforeEach(() => {
+ beforeEach(async () => {
wrapper = mountComponent({
provide: { isProject },
issuesQueryResponse: jest.fn().mockResolvedValue(response(isProject)),
});
jest.runOnlyPendingTimers();
+ await waitForPromises();
});
it('makes API call to reorder the issue', async () => {
@@ -705,7 +736,6 @@ describe('CE IssuesListApp component', () => {
data: JSON.stringify({
move_before_id: getIdFromGraphQLId(moveBeforeId),
move_after_id: getIdFromGraphQLId(moveAfterId),
- group_full_path: isProject ? undefined : defaultProvide.fullPath,
}),
});
});
@@ -715,11 +745,12 @@ describe('CE IssuesListApp component', () => {
});
describe('when unsuccessful', () => {
- beforeEach(() => {
+ beforeEach(async () => {
wrapper = mountComponent({
issuesQueryResponse: jest.fn().mockResolvedValue(response()),
});
jest.runOnlyPendingTimers();
+ await waitForPromises();
});
it('displays an error message', async () => {
@@ -758,8 +789,9 @@ describe('CE IssuesListApp component', () => {
const initialSort = CREATED_DESC;
beforeEach(() => {
- setWindowLocation(`?sort=${initialSort}`);
- wrapper = mountComponent({ provide: { isIssueRepositioningDisabled: true } });
+ wrapper = mountComponent({
+ provide: { initialSort, isIssueRepositioningDisabled: true },
+ });
findIssuableList().vm.$emit('sort', RELATIVE_POSITION_ASC);
});
@@ -777,6 +809,43 @@ describe('CE IssuesListApp component', () => {
});
});
});
+
+ describe('when user is signed in', () => {
+ it('calls mutation to save sort preference', () => {
+ const mutationMock = jest.fn().mockResolvedValue(setSortPreferenceMutationResponse);
+ wrapper = mountComponent({ sortPreferenceMutationResponse: mutationMock });
+
+ findIssuableList().vm.$emit('sort', CREATED_DESC);
+
+ expect(mutationMock).toHaveBeenCalledWith({ input: { issuesSort: CREATED_DESC } });
+ });
+
+ it('captures error when mutation response has errors', async () => {
+ const mutationMock = jest
+ .fn()
+ .mockResolvedValue(setSortPreferenceMutationResponseWithErrors);
+ wrapper = mountComponent({ sortPreferenceMutationResponse: mutationMock });
+
+ findIssuableList().vm.$emit('sort', CREATED_DESC);
+ await waitForPromises();
+
+ expect(Sentry.captureException).toHaveBeenCalledWith(new Error('oh no!'));
+ });
+ });
+
+ describe('when user is signed out', () => {
+ it('does not call mutation to save sort preference', () => {
+ const mutationMock = jest.fn().mockResolvedValue(setSortPreferenceMutationResponse);
+ wrapper = mountComponent({
+ provide: { isSignedIn: false },
+ sortPreferenceMutationResponse: mutationMock,
+ });
+
+ findIssuableList().vm.$emit('sort', CREATED_DESC);
+
+ expect(mutationMock).not.toHaveBeenCalled();
+ });
+ });
});
describe('when "update-legacy-bulk-edit" event is emitted by IssuableList', () => {
diff --git a/spec/frontend/issues/list/components/jira_issues_import_status_app_spec.js b/spec/frontend/issues/list/components/jira_issues_import_status_app_spec.js
index d6d6bb14e9d..2d773e8bf56 100644
--- a/spec/frontend/issues/list/components/jira_issues_import_status_app_spec.js
+++ b/spec/frontend/issues/list/components/jira_issues_import_status_app_spec.js
@@ -1,6 +1,6 @@
import { GlAlert, GlLabel } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
+import { nextTick } from 'vue';
import JiraIssuesImportStatus from '~/issues/list/components/jira_issues_import_status_app.vue';
describe('JiraIssuesImportStatus', () => {
@@ -100,7 +100,7 @@ describe('JiraIssuesImportStatus', () => {
});
describe('alert message', () => {
- it('is hidden when dismissed', () => {
+ it('is hidden when dismissed', async () => {
wrapper = mountComponent({
shouldShowInProgressAlert: true,
});
@@ -109,9 +109,8 @@ describe('JiraIssuesImportStatus', () => {
findAlert().vm.$emit('dismiss');
- return Vue.nextTick(() => {
- expect(wrapper.find(GlAlert).exists()).toBe(false);
- });
+ await nextTick();
+ expect(wrapper.find(GlAlert).exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/issues/list/components/new_issue_dropdown_spec.js b/spec/frontend/issues/list/components/new_issue_dropdown_spec.js
index 0c52e66ff14..2c8cf9caf5d 100644
--- a/spec/frontend/issues/list/components/new_issue_dropdown_spec.js
+++ b/spec/frontend/issues/list/components/new_issue_dropdown_spec.js
@@ -1,10 +1,13 @@
import { GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
-import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
+import { mount, shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import NewIssueDropdown from '~/issues/list/components/new_issue_dropdown.vue';
import searchProjectsQuery from '~/issues/list/queries/search_projects.query.graphql';
import { DASH_SCOPE, joinPaths } from '~/lib/utils/url_utility';
+import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
import {
emptySearchProjectsQueryResponse,
project1,
@@ -15,8 +18,7 @@ import {
describe('NewIssueDropdown component', () => {
let wrapper;
- const localVue = createLocalVue();
- localVue.use(VueApollo);
+ Vue.use(VueApollo);
const mountComponent = ({
search = '',
@@ -27,7 +29,6 @@ describe('NewIssueDropdown component', () => {
const apolloProvider = createMockApollo(requestHandlers);
return mountFn(NewIssueDropdown, {
- localVue,
apolloProvider,
provide: {
fullPath: 'mushroom-kingdom',
@@ -42,8 +43,9 @@ describe('NewIssueDropdown component', () => {
const findInput = () => wrapper.findComponent(GlSearchBoxByType);
const showDropdown = async () => {
findDropdown().vm.$emit('shown');
- await wrapper.vm.$apollo.queries.projects.refetch();
- jest.runOnlyPendingTimers();
+ await waitForPromises();
+ jest.advanceTimersByTime(DEBOUNCE_DELAY);
+ await waitForPromises();
};
afterEach(() => {
@@ -74,7 +76,6 @@ describe('NewIssueDropdown component', () => {
it('renders projects with issues enabled', async () => {
wrapper = mountComponent({ mountFn: mount });
-
await showDropdown();
const listItems = wrapper.findAll('li');
@@ -112,10 +113,11 @@ describe('NewIssueDropdown component', () => {
describe('when a project is selected', () => {
beforeEach(async () => {
wrapper = mountComponent({ mountFn: mount });
-
+ await waitForPromises();
await showDropdown();
wrapper.findComponent(GlDropdownItem).vm.$emit('click', project1);
+ await waitForPromises();
});
it('dropdown button is a link', () => {
diff --git a/spec/frontend/issues/list/mock_data.js b/spec/frontend/issues/list/mock_data.js
index 948699876ce..c883b20682e 100644
--- a/spec/frontend/issues/list/mock_data.js
+++ b/spec/frontend/issues/list/mock_data.js
@@ -7,8 +7,10 @@ export const getIssuesQueryResponse = {
data: {
project: {
id: '1',
+ __typename: 'Project',
issues: {
pageInfo: {
+ __typename: 'PageInfo',
hasNextPage: true,
hasPreviousPage: false,
startCursor: 'startcursor',
@@ -16,6 +18,7 @@ export const getIssuesQueryResponse = {
},
nodes: [
{
+ __typename: 'Issue',
id: 'gid://gitlab/Issue/123456',
iid: '789',
closedAt: null,
@@ -36,6 +39,7 @@ export const getIssuesQueryResponse = {
assignees: {
nodes: [
{
+ __typename: 'UserCore',
id: 'gid://gitlab/User/234',
avatarUrl: 'avatar/url',
name: 'Marge Simpson',
@@ -45,6 +49,7 @@ export const getIssuesQueryResponse = {
],
},
author: {
+ __typename: 'UserCore',
id: 'gid://gitlab/User/456',
avatarUrl: 'avatar/url',
name: 'Homer Simpson',
@@ -90,6 +95,22 @@ export const getIssuesCountsQueryResponse = {
},
};
+export const setSortPreferenceMutationResponse = {
+ data: {
+ userPreferencesUpdate: {
+ errors: [],
+ },
+ },
+};
+
+export const setSortPreferenceMutationResponseWithErrors = {
+ data: {
+ userPreferencesUpdate: {
+ errors: ['oh no!'],
+ },
+ },
+};
+
export const locationSearch = [
'?search=find+issues',
'author_username=homer',
diff --git a/spec/frontend/issues/list/utils_spec.js b/spec/frontend/issues/list/utils_spec.js
index 0e4979fd7b4..1d3e94df897 100644
--- a/spec/frontend/issues/list/utils_spec.js
+++ b/spec/frontend/issues/list/utils_spec.js
@@ -10,7 +10,6 @@ import {
} from 'jest/issues/list/mock_data';
import {
defaultPageSizeParams,
- DUE_DATE_VALUES,
largePageSizeParams,
RELATIVE_POSITION_ASC,
urlSortParams,
@@ -19,11 +18,11 @@ import {
convertToApiParams,
convertToSearchQuery,
convertToUrlParams,
- getDueDateValue,
getFilterTokens,
getInitialPageParams,
getSortKey,
getSortOptions,
+ isSortKey,
} from '~/issues/list/utils';
describe('getInitialPageParams', () => {
@@ -45,13 +44,13 @@ describe('getSortKey', () => {
});
});
-describe('getDueDateValue', () => {
- it.each(DUE_DATE_VALUES)('returns the argument when it is `%s`', (value) => {
- expect(getDueDateValue(value)).toBe(value);
+describe('isSortKey', () => {
+ it.each(Object.keys(urlSortParams))('returns true given %s', (sort) => {
+ expect(isSortKey(sort)).toBe(true);
});
- it('returns undefined when the argument is invalid', () => {
- expect(getDueDateValue('invalid value')).toBeUndefined();
+ it.each(['', 'asdf', null, undefined])('returns false given %s', (sort) => {
+ expect(isSortKey(sort)).toBe(false);
});
});
diff --git a/spec/frontend/issues/new/components/title_suggestions_spec.js b/spec/frontend/issues/new/components/title_suggestions_spec.js
index f6b93cc5a62..0a64890e4ca 100644
--- a/spec/frontend/issues/new/components/title_suggestions_spec.js
+++ b/spec/frontend/issues/new/components/title_suggestions_spec.js
@@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import TitleSuggestions from '~/issues/new/components/title_suggestions.vue';
import TitleSuggestionsItem from '~/issues/new/components/title_suggestions_item.vue';
@@ -22,12 +23,11 @@ describe('Issue title suggestions component', () => {
wrapper.destroy();
});
- it('does not render with empty search', () => {
+ it('does not render with empty search', async () => {
wrapper.setProps({ search: '' });
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.isVisible()).toBe(false);
- });
+ await nextTick();
+ expect(wrapper.isVisible()).toBe(false);
});
describe('with data', () => {
@@ -37,28 +37,26 @@ describe('Issue title suggestions component', () => {
data = { issues: [{ id: 1 }, { id: 2 }] };
});
- it('renders component', () => {
+ it('renders component', async () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData(data);
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.findAll('li').length).toBe(data.issues.length);
- });
+ await nextTick();
+ expect(wrapper.findAll('li').length).toBe(data.issues.length);
});
- it('does not render with empty search', () => {
+ it('does not render with empty search', async () => {
wrapper.setProps({ search: '' });
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData(data);
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.isVisible()).toBe(false);
- });
+ await nextTick();
+ expect(wrapper.isVisible()).toBe(false);
});
- it('does not render when loading', () => {
+ it('does not render when loading', async () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({
@@ -66,49 +64,44 @@ describe('Issue title suggestions component', () => {
loading: 1,
});
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.isVisible()).toBe(false);
- });
+ await nextTick();
+ expect(wrapper.isVisible()).toBe(false);
});
- it('does not render with empty issues data', () => {
+ it('does not render with empty issues data', async () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ issues: [] });
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.isVisible()).toBe(false);
- });
+ await nextTick();
+ expect(wrapper.isVisible()).toBe(false);
});
- it('renders list of issues', () => {
+ it('renders list of issues', async () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData(data);
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.findAll(TitleSuggestionsItem).length).toBe(2);
- });
+ await nextTick();
+ expect(wrapper.findAll(TitleSuggestionsItem).length).toBe(2);
});
- it('adds margin class to first item', () => {
+ it('adds margin class to first item', async () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData(data);
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.findAll('li').at(0).classes()).toContain('gl-mb-3');
- });
+ await nextTick();
+ expect(wrapper.findAll('li').at(0).classes()).toContain('gl-mb-3');
});
- it('does not add margin class to last item', () => {
+ it('does not add margin class to last item', async () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData(data);
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.findAll('li').at(1).classes()).not.toContain('gl-mb-3');
- });
+ await nextTick();
+ expect(wrapper.findAll('li').at(1).classes()).not.toContain('gl-mb-3');
});
});
});
diff --git a/spec/frontend/issues/related_merge_requests/components/related_merge_requests_spec.js b/spec/frontend/issues/related_merge_requests/components/related_merge_requests_spec.js
index 4d780a674be..4df04cd5257 100644
--- a/spec/frontend/issues/related_merge_requests/components/related_merge_requests_spec.js
+++ b/spec/frontend/issues/related_merge_requests/components/related_merge_requests_spec.js
@@ -1,4 +1,4 @@
-import { mount, createLocalVue } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import mockData from 'test_fixtures/issues/related_merge_requests.json';
import axios from '~/lib/utils/axios_utils';
@@ -7,13 +7,12 @@ import createStore from '~/issues/related_merge_requests/store/index';
import RelatedIssuableItem from '~/issuable/components/related_issuable_item.vue';
const API_ENDPOINT = '/api/v4/projects/2/issues/33/related_merge_requests';
-const localVue = createLocalVue();
describe('RelatedMergeRequests', () => {
let wrapper;
let mock;
- beforeEach((done) => {
+ beforeEach(() => {
// put the fixture in DOM as the component expects
document.body.innerHTML = `<div id="js-issuable-app"></div>`;
document.getElementById('js-issuable-app').dataset.initial = JSON.stringify(mockData);
@@ -21,8 +20,7 @@ describe('RelatedMergeRequests', () => {
mock = new MockAdapter(axios);
mock.onGet(`${API_ENDPOINT}?per_page=100`).reply(200, mockData, { 'x-total': 2 });
- wrapper = mount(localVue.extend(RelatedMergeRequests), {
- localVue,
+ wrapper = mount(RelatedMergeRequests, {
store: createStore(),
propsData: {
endpoint: API_ENDPOINT,
@@ -31,7 +29,7 @@ describe('RelatedMergeRequests', () => {
},
});
- setImmediate(done);
+ return axios.waitForAll();
});
afterEach(() => {
diff --git a/spec/frontend/issues/show/components/app_spec.js b/spec/frontend/issues/show/components/app_spec.js
index 02db82b84dc..ac2717a5028 100644
--- a/spec/frontend/issues/show/components/app_spec.js
+++ b/spec/frontend/issues/show/components/app_spec.js
@@ -145,33 +145,30 @@ describe('Issuable output', () => {
});
});
- it('shows actions if permissions are correct', () => {
+ it('shows actions if permissions are correct', async () => {
wrapper.vm.showForm = true;
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find('.markdown-selector').exists()).toBe(true);
- });
+ await nextTick();
+ expect(wrapper.find('.markdown-selector').exists()).toBe(true);
});
- it('does not show actions if permissions are incorrect', () => {
+ it('does not show actions if permissions are incorrect', async () => {
wrapper.vm.showForm = true;
wrapper.setProps({ canUpdate: false });
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find('.markdown-selector').exists()).toBe(false);
- });
+ await nextTick();
+ expect(wrapper.find('.markdown-selector').exists()).toBe(false);
});
- it('does not update formState if form is already open', () => {
+ it('does not update formState if form is already open', async () => {
wrapper.vm.updateAndShowForm();
wrapper.vm.state.titleText = 'testing 123';
wrapper.vm.updateAndShowForm();
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.vm.store.formState.title).not.toBe('testing 123');
- });
+ await nextTick();
+ expect(wrapper.vm.store.formState.title).not.toBe('testing 123');
});
describe('Pinned links propagated', () => {
@@ -186,31 +183,29 @@ describe('Issuable output', () => {
});
describe('updateIssuable', () => {
- it('fetches new data after update', () => {
+ it('fetches new data after update', async () => {
const updateStoreSpy = jest.spyOn(wrapper.vm, 'updateStoreState');
const getDataSpy = jest.spyOn(wrapper.vm.service, 'getData');
jest.spyOn(wrapper.vm.service, 'updateIssuable').mockResolvedValue({
data: { web_url: window.location.pathname },
});
- return wrapper.vm.updateIssuable().then(() => {
- expect(updateStoreSpy).toHaveBeenCalled();
- expect(getDataSpy).toHaveBeenCalled();
- });
+ await wrapper.vm.updateIssuable();
+ expect(updateStoreSpy).toHaveBeenCalled();
+ expect(getDataSpy).toHaveBeenCalled();
});
- it('correctly updates issuable data', () => {
+ it('correctly updates issuable data', async () => {
const spy = jest.spyOn(wrapper.vm.service, 'updateIssuable').mockResolvedValue({
data: { web_url: window.location.pathname },
});
- return wrapper.vm.updateIssuable().then(() => {
- expect(spy).toHaveBeenCalledWith(wrapper.vm.formState);
- expect(eventHub.$emit).toHaveBeenCalledWith('close.form');
- });
+ await wrapper.vm.updateIssuable();
+ expect(spy).toHaveBeenCalledWith(wrapper.vm.formState);
+ expect(eventHub.$emit).toHaveBeenCalledWith('close.form');
});
- it('does not redirect if issue has not moved', () => {
+ it('does not redirect if issue has not moved', async () => {
jest.spyOn(wrapper.vm.service, 'updateIssuable').mockResolvedValue({
data: {
web_url: window.location.pathname,
@@ -218,12 +213,11 @@ describe('Issuable output', () => {
},
});
- return wrapper.vm.updateIssuable().then(() => {
- expect(visitUrl).not.toHaveBeenCalled();
- });
+ await wrapper.vm.updateIssuable();
+ expect(visitUrl).not.toHaveBeenCalled();
});
- it('does not redirect if issue has not moved and user has switched tabs', () => {
+ it('does not redirect if issue has not moved and user has switched tabs', async () => {
jest.spyOn(wrapper.vm.service, 'updateIssuable').mockResolvedValue({
data: {
web_url: '',
@@ -231,12 +225,11 @@ describe('Issuable output', () => {
},
});
- return wrapper.vm.updateIssuable().then(() => {
- expect(visitUrl).not.toHaveBeenCalled();
- });
+ await wrapper.vm.updateIssuable();
+ expect(visitUrl).not.toHaveBeenCalled();
});
- it('redirects if returned web_url has changed', () => {
+ it('redirects if returned web_url has changed', async () => {
jest.spyOn(wrapper.vm.service, 'updateIssuable').mockResolvedValue({
data: {
web_url: '/testing-issue-move',
@@ -246,108 +239,95 @@ describe('Issuable output', () => {
wrapper.vm.updateIssuable();
- return wrapper.vm.updateIssuable().then(() => {
- expect(visitUrl).toHaveBeenCalledWith('/testing-issue-move');
- });
+ await wrapper.vm.updateIssuable();
+ expect(visitUrl).toHaveBeenCalledWith('/testing-issue-move');
});
describe('shows dialog when issue has unsaved changed', () => {
- it('confirms on title change', () => {
+ it('confirms on title change', async () => {
wrapper.vm.showForm = true;
wrapper.vm.state.titleText = 'title has changed';
const e = { returnValue: null };
wrapper.vm.handleBeforeUnloadEvent(e);
- return wrapper.vm.$nextTick().then(() => {
- expect(e.returnValue).not.toBeNull();
- });
+ await nextTick();
+ expect(e.returnValue).not.toBeNull();
});
- it('confirms on description change', () => {
+ it('confirms on description change', async () => {
wrapper.vm.showForm = true;
wrapper.vm.state.descriptionText = 'description has changed';
const e = { returnValue: null };
wrapper.vm.handleBeforeUnloadEvent(e);
- return wrapper.vm.$nextTick().then(() => {
- expect(e.returnValue).not.toBeNull();
- });
+ await nextTick();
+ expect(e.returnValue).not.toBeNull();
});
- it('does nothing when nothing has changed', () => {
+ it('does nothing when nothing has changed', async () => {
const e = { returnValue: null };
wrapper.vm.handleBeforeUnloadEvent(e);
- return wrapper.vm.$nextTick().then(() => {
- expect(e.returnValue).toBeNull();
- });
+ await nextTick();
+ expect(e.returnValue).toBeNull();
});
});
describe('error when updating', () => {
- it('closes form on error', () => {
+ it('closes form on error', async () => {
jest.spyOn(wrapper.vm.service, 'updateIssuable').mockRejectedValue();
- return wrapper.vm.updateIssuable().then(() => {
- expect(eventHub.$emit).not.toHaveBeenCalledWith('close.form');
- expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
- `Error updating issue`,
- );
- });
+ await wrapper.vm.updateIssuable();
+ expect(eventHub.$emit).not.toHaveBeenCalledWith('close.form');
+ expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
+ `Error updating issue`,
+ );
});
- it('returns the correct error message for issuableType', () => {
+ it('returns the correct error message for issuableType', async () => {
jest.spyOn(wrapper.vm.service, 'updateIssuable').mockRejectedValue();
wrapper.setProps({ issuableType: 'merge request' });
- return wrapper.vm
- .$nextTick()
- .then(wrapper.vm.updateIssuable)
- .then(() => {
- expect(eventHub.$emit).not.toHaveBeenCalledWith('close.form');
- expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
- `Error updating merge request`,
- );
- });
+ await nextTick();
+ await wrapper.vm.updateIssuable();
+ expect(eventHub.$emit).not.toHaveBeenCalledWith('close.form');
+ expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
+ `Error updating merge request`,
+ );
});
- it('shows error message from backend if exists', () => {
+ it('shows error message from backend if exists', async () => {
const msg = 'Custom error message from backend';
jest
.spyOn(wrapper.vm.service, 'updateIssuable')
.mockRejectedValue({ response: { data: { errors: [msg] } } });
- return wrapper.vm.updateIssuable().then(() => {
- expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
- `${wrapper.vm.defaultErrorMessage}. ${msg}`,
- );
- });
+ await wrapper.vm.updateIssuable();
+ expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
+ `${wrapper.vm.defaultErrorMessage}. ${msg}`,
+ );
});
});
});
describe('updateAndShowForm', () => {
- it('shows locked warning if form is open & data is different', () => {
- return wrapper.vm
- .$nextTick()
- .then(() => {
- wrapper.vm.updateAndShowForm();
-
- wrapper.vm.poll.makeRequest();
-
- return new Promise((resolve) => {
- wrapper.vm.$watch('formState.lockedWarningVisible', (value) => {
- if (value) {
- resolve();
- }
- });
- });
- })
- .then(() => {
- expect(wrapper.vm.formState.lockedWarningVisible).toBe(true);
- expect(wrapper.vm.formState.lock_version).toBe(1);
- expect(findAlert().exists()).toBe(true);
+ it('shows locked warning if form is open & data is different', async () => {
+ await nextTick();
+ wrapper.vm.updateAndShowForm();
+
+ wrapper.vm.poll.makeRequest();
+
+ await new Promise((resolve) => {
+ wrapper.vm.$watch('formState.lockedWarningVisible', (value) => {
+ if (value) {
+ resolve();
+ }
});
+ });
+
+ expect(wrapper.vm.formState.lockedWarningVisible).toBe(true);
+ expect(wrapper.vm.formState.lock_version).toBe(1);
+ expect(findAlert().exists()).toBe(true);
});
});
@@ -398,12 +378,11 @@ describe('Issuable output', () => {
expect(wrapper.find('.btn-edit').exists()).toBe(true);
});
- it('should render if showInlineEditButton', () => {
+ it('should render if showInlineEditButton', async () => {
wrapper.setProps({ showInlineEditButton: true });
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.find('.btn-edit').exists()).toBe(true);
- });
+ await nextTick();
+ expect(wrapper.find('.btn-edit').exists()).toBe(true);
});
});
diff --git a/spec/frontend/issues/show/components/description_spec.js b/spec/frontend/issues/show/components/description_spec.js
index d39e00b9c9e..3890fc7a353 100644
--- a/spec/frontend/issues/show/components/description_spec.js
+++ b/spec/frontend/issues/show/components/description_spec.js
@@ -1,21 +1,56 @@
import $ from 'jquery';
-import Vue from 'vue';
+import { nextTick } from 'vue';
import '~/behaviors/markdown/render_gfm';
+import { GlPopover, GlModal } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { stubComponent } from 'helpers/stub_component';
import { TEST_HOST } from 'helpers/test_constants';
-import mountComponent from 'helpers/vue_mount_component_helper';
import Description from '~/issues/show/components/description.vue';
import TaskList from '~/task_list';
-import { descriptionProps as props } from '../mock_data/mock_data';
+import CreateWorkItem from '~/work_items/pages/create_work_item.vue';
+import {
+ descriptionProps as initialProps,
+ descriptionHtmlWithCheckboxes,
+} from '../mock_data/mock_data';
jest.mock('~/task_list');
+const showModal = jest.fn();
+const hideModal = jest.fn();
+
describe('Description component', () => {
- let vm;
- let DescriptionComponent;
+ let wrapper;
+
+ const findGfmContent = () => wrapper.find('[data-testid="gfm-content"]');
+ const findTextarea = () => wrapper.find('[data-testid="textarea"]');
+ const findTaskActionButtons = () => wrapper.findAll('.js-add-task');
+ const findConvertToTaskButton = () => wrapper.find('[data-testid="convert-to-task"]');
+ const findTaskSvg = () => wrapper.find('[data-testid="issue-open-m-icon"]');
+
+ const findPopovers = () => wrapper.findAllComponents(GlPopover);
+ const findModal = () => wrapper.findComponent(GlModal);
+ const findCreateWorkItem = () => wrapper.findComponent(CreateWorkItem);
+
+ function createComponent({ props = {}, provide = {} } = {}) {
+ wrapper = shallowMount(Description, {
+ propsData: {
+ ...initialProps,
+ ...props,
+ },
+ provide,
+ stubs: {
+ GlModal: stubComponent(GlModal, {
+ methods: {
+ show: showModal,
+ hide: hideModal,
+ },
+ }),
+ GlPopover,
+ },
+ });
+ }
beforeEach(() => {
- DescriptionComponent = Vue.extend(Description);
-
if (!document.querySelector('.issuable-meta')) {
const metaData = document.createElement('div');
metaData.classList.add('issuable-meta');
@@ -24,91 +59,102 @@ describe('Description component', () => {
document.body.appendChild(metaData);
}
-
- vm = mountComponent(DescriptionComponent, props);
});
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
afterAll(() => {
$('.issuable-meta .flash-container').remove();
});
- it('doesnt animate first description changes', () => {
- vm.descriptionHtml = 'changed';
-
- return vm.$nextTick().then(() => {
- expect(
- vm.$el.querySelector('.md').classList.contains('issue-realtime-pre-pulse'),
- ).toBeFalsy();
- jest.runAllTimers();
- return vm.$nextTick();
+ it('doesnt animate first description changes', async () => {
+ createComponent();
+ await wrapper.setProps({
+ descriptionHtml: 'changed',
});
+
+ expect(findGfmContent().classes()).not.toContain('issue-realtime-pre-pulse');
});
- it('animates description changes on live update', () => {
- vm.descriptionHtml = 'changed';
- return vm
- .$nextTick()
- .then(() => {
- vm.descriptionHtml = 'changed second time';
- return vm.$nextTick();
- })
- .then(() => {
- expect(
- vm.$el.querySelector('.md').classList.contains('issue-realtime-pre-pulse'),
- ).toBeTruthy();
- jest.runAllTimers();
- return vm.$nextTick();
- })
- .then(() => {
- expect(
- vm.$el.querySelector('.md').classList.contains('issue-realtime-trigger-pulse'),
- ).toBeTruthy();
- });
+ it('animates description changes on live update', async () => {
+ createComponent();
+ await wrapper.setProps({
+ descriptionHtml: 'changed',
+ });
+
+ expect(findGfmContent().classes()).not.toContain('issue-realtime-pre-pulse');
+
+ await wrapper.setProps({
+ descriptionHtml: 'changed second time',
+ });
+
+ expect(findGfmContent().classes()).toContain('issue-realtime-pre-pulse');
+
+ await jest.runOnlyPendingTimers();
+
+ expect(findGfmContent().classes()).toContain('issue-realtime-trigger-pulse');
});
- it('applies syntax highlighting and math when description changed', () => {
- const vmSpy = jest.spyOn(vm, 'renderGFM');
+ it('applies syntax highlighting and math when description changed', async () => {
const prototypeSpy = jest.spyOn($.prototype, 'renderGFM');
- vm.descriptionHtml = 'changed';
+ createComponent();
- return vm.$nextTick().then(() => {
- expect(vm.$refs['gfm-content']).toBeDefined();
- expect(vmSpy).toHaveBeenCalled();
- expect(prototypeSpy).toHaveBeenCalled();
- expect($.prototype.renderGFM).toHaveBeenCalled();
+ await wrapper.setProps({
+ descriptionHtml: 'changed',
});
+
+ expect(findGfmContent().exists()).toBe(true);
+ expect(prototypeSpy).toHaveBeenCalled();
});
it('sets data-update-url', () => {
- expect(vm.$el.querySelector('textarea').dataset.updateUrl).toEqual(TEST_HOST);
+ createComponent();
+ expect(findTextarea().attributes('data-update-url')).toBe(TEST_HOST);
});
describe('TaskList', () => {
beforeEach(() => {
- vm.$destroy();
TaskList.mockClear();
- vm = mountComponent(DescriptionComponent, { ...props, issuableType: 'issuableType' });
});
it('re-inits the TaskList when description changed', () => {
- vm.descriptionHtml = 'changed';
+ createComponent({
+ props: {
+ issuableType: 'issuableType',
+ },
+ });
+ wrapper.setProps({
+ descriptionHtml: 'changed',
+ });
expect(TaskList).toHaveBeenCalled();
});
- it('does not re-init the TaskList when canUpdate is false', () => {
- vm.canUpdate = false;
- vm.descriptionHtml = 'changed';
+ it('does not re-init the TaskList when canUpdate is false', async () => {
+ createComponent({
+ props: {
+ issuableType: 'issuableType',
+ canUpdate: false,
+ },
+ });
+ wrapper.setProps({
+ descriptionHtml: 'changed',
+ });
- expect(TaskList).toHaveBeenCalledTimes(1);
+ expect(TaskList).not.toHaveBeenCalled();
});
it('calls with issuableType dataType', () => {
- vm.descriptionHtml = 'changed';
+ createComponent({
+ props: {
+ issuableType: 'issuableType',
+ },
+ });
+ wrapper.setProps({
+ descriptionHtml: 'changed',
+ });
expect(TaskList).toHaveBeenCalledWith({
dataType: 'issuableType',
@@ -123,65 +169,119 @@ describe('Description component', () => {
});
describe('taskStatus', () => {
- it('adds full taskStatus', () => {
- vm.taskStatus = '1 of 1';
-
- return vm.$nextTick().then(() => {
- expect(document.querySelector('.issuable-meta #task_status').textContent.trim()).toBe(
- '1 of 1',
- );
+ it('adds full taskStatus', async () => {
+ createComponent({
+ props: {
+ taskStatus: '1 of 1',
+ },
});
- });
+ await nextTick();
- it('adds short taskStatus', () => {
- vm.taskStatus = '1 of 1';
+ expect(document.querySelector('.issuable-meta #task_status').textContent.trim()).toBe(
+ '1 of 1',
+ );
+ });
- return vm.$nextTick().then(() => {
- expect(document.querySelector('.issuable-meta #task_status_short').textContent.trim()).toBe(
- '1/1 task',
- );
+ it('adds short taskStatus', async () => {
+ createComponent({
+ props: {
+ taskStatus: '1 of 1',
+ },
});
- });
+ await nextTick();
- it('clears task status text when no tasks are present', () => {
- vm.taskStatus = '0 of 0';
+ expect(document.querySelector('.issuable-meta #task_status_short').textContent.trim()).toBe(
+ '1/1 task',
+ );
+ });
- return vm.$nextTick().then(() => {
- expect(document.querySelector('.issuable-meta #task_status').textContent.trim()).toBe('');
+ it('clears task status text when no tasks are present', async () => {
+ createComponent({
+ props: {
+ taskStatus: '0 of 0',
+ },
});
+
+ await nextTick();
+
+ expect(document.querySelector('.issuable-meta #task_status').textContent.trim()).toBe('');
});
});
- describe('taskListUpdateStarted', () => {
- it('emits event to parent', () => {
- const spy = jest.spyOn(vm, '$emit');
-
- vm.taskListUpdateStarted();
+ describe('with work items feature flag is enabled', () => {
+ describe('empty description', () => {
+ beforeEach(async () => {
+ createComponent({
+ props: {
+ descriptionHtml: '',
+ },
+ provide: {
+ glFeatures: {
+ workItems: true,
+ },
+ },
+ });
+ await nextTick();
+ });
- expect(spy).toHaveBeenCalledWith('taskListUpdateStarted');
+ it('renders without error', () => {
+ expect(findTaskActionButtons()).toHaveLength(0);
+ });
});
- });
- describe('taskListUpdateSuccess', () => {
- it('emits event to parent', () => {
- const spy = jest.spyOn(vm, '$emit');
+ describe('description with checkboxes', () => {
+ beforeEach(async () => {
+ createComponent({
+ props: {
+ descriptionHtml: descriptionHtmlWithCheckboxes,
+ },
+ provide: {
+ glFeatures: {
+ workItems: true,
+ },
+ },
+ });
+ await nextTick();
+ });
- vm.taskListUpdateSuccess();
+ it('renders a list of hidden buttons corresponding to checkboxes in description HTML', () => {
+ expect(findTaskActionButtons()).toHaveLength(3);
+ });
- expect(spy).toHaveBeenCalledWith('taskListUpdateSucceeded');
- });
- });
+ it('renders a list of popovers corresponding to checkboxes in description HTML', () => {
+ expect(findPopovers()).toHaveLength(3);
+ expect(findPopovers().at(0).props('target')).toBe(
+ findTaskActionButtons().at(0).attributes('id'),
+ );
+ });
- describe('taskListUpdateError', () => {
- it('should create flash notification and emit an event to parent', () => {
- const msg =
- 'Someone edited this issue at the same time you did. The description has been updated and you will need to make your changes again.';
- const spy = jest.spyOn(vm, '$emit');
+ it('does not show a modal by default', () => {
+ expect(findModal().props('visible')).toBe(false);
+ });
- vm.taskListUpdateError();
+ it('opens a modal when a button on popover is clicked and displays correct title', async () => {
+ findConvertToTaskButton().vm.$emit('click');
+ expect(showModal).toHaveBeenCalled();
+ await nextTick();
+ expect(findCreateWorkItem().props('initialTitle').trim()).toBe('todo 1');
+ });
+
+ it('closes the modal on `closeCreateTaskModal` event', () => {
+ findConvertToTaskButton().vm.$emit('click');
+ findCreateWorkItem().vm.$emit('closeModal');
+ expect(hideModal).toHaveBeenCalled();
+ });
- expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(msg);
- expect(spy).toHaveBeenCalledWith('taskListUpdateFailed');
+ it('updates description HTML on `onCreate` event', async () => {
+ const newTitle = 'New title';
+ findConvertToTaskButton().vm.$emit('click');
+ findCreateWorkItem().vm.$emit('onCreate', newTitle);
+ expect(hideModal).toHaveBeenCalled();
+ await nextTick();
+
+ expect(findTaskSvg().exists()).toBe(true);
+ expect(wrapper.text()).toContain(newTitle);
+ });
});
});
});
diff --git a/spec/frontend/issues/show/components/fields/description_spec.js b/spec/frontend/issues/show/components/fields/description_spec.js
index 3043c4c3673..dd511c3945c 100644
--- a/spec/frontend/issues/show/components/fields/description_spec.js
+++ b/spec/frontend/issues/show/components/fields/description_spec.js
@@ -25,6 +25,7 @@ describe('Description field component', () => {
beforeEach(() => {
jest.spyOn(eventHub, '$emit');
+ gon.features = { markdownContinueLists: true };
});
afterEach(() => {
diff --git a/spec/frontend/issues/show/components/fields/type_spec.js b/spec/frontend/issues/show/components/fields/type_spec.js
index 7f7b16583e6..3333ceffca9 100644
--- a/spec/frontend/issues/show/components/fields/type_spec.js
+++ b/spec/frontend/issues/show/components/fields/type_spec.js
@@ -1,5 +1,6 @@
import { GlFormGroup, GlDropdown, GlDropdownItem, GlIcon } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -10,8 +11,7 @@ import {
updateIssueStateQueryResponse,
} from '../../mock_data/apollo_mock';
-const localVue = createLocalVue();
-localVue.use(VueApollo);
+Vue.use(VueApollo);
describe('Issue type field component', () => {
let wrapper;
@@ -43,7 +43,6 @@ describe('Issue type field component', () => {
fakeApollo = createMockApollo([], mockResolvers);
wrapper = shallowMount(IssueTypeField, {
- localVue,
apolloProvider: fakeApollo,
data() {
return {
@@ -93,7 +92,7 @@ describe('Issue type field component', () => {
it('updates the `issue_type` in the apollo cache when the value is changed', async () => {
findTypeFromDropDownItems().at(1).vm.$emit('click', issuableTypes.incident);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findTypeFromDropDown().attributes('value')).toBe(issuableTypes.incident);
});
diff --git a/spec/frontend/issues/show/components/form_spec.js b/spec/frontend/issues/show/components/form_spec.js
index db49d2635ba..5c0fe991b22 100644
--- a/spec/frontend/issues/show/components/form_spec.js
+++ b/spec/frontend/issues/show/components/form_spec.js
@@ -1,5 +1,6 @@
import { GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import Autosave from '~/autosave';
import DescriptionTemplate from '~/issues/show/components/fields/description_template.vue';
import IssueTypeField from '~/issues/show/components/fields/type.vue';
@@ -148,7 +149,7 @@ describe('Inline edit form component', () => {
formState: { ...defaultProps.formState, lock_version: 'lock version from server' },
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findAlert().exists()).toBe(true);
});
});
diff --git a/spec/frontend/issues/show/components/header_actions_spec.js b/spec/frontend/issues/show/components/header_actions_spec.js
index d09bf6faa13..4a557a60b94 100644
--- a/spec/frontend/issues/show/components/header_actions_spec.js
+++ b/spec/frontend/issues/show/components/header_actions_spec.js
@@ -1,5 +1,5 @@
import { GlButton, GlDropdown, GlDropdownItem, GlLink, GlModal } from '@gitlab/ui';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import { shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import { mockTracking } from 'helpers/tracking_helper';
@@ -153,7 +153,7 @@ describe('HeaderActions component', () => {
it('dispatches a custom event to update the issue page', async () => {
findToggleIssueStateButton().vm.$emit('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(dispatchEventSpy).toHaveBeenCalledTimes(1);
});
diff --git a/spec/frontend/issues/show/components/title_spec.js b/spec/frontend/issues/show/components/title_spec.js
index f9026557be2..29b5353ef1c 100644
--- a/spec/frontend/issues/show/components/title_spec.js
+++ b/spec/frontend/issues/show/components/title_spec.js
@@ -1,4 +1,4 @@
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import titleComponent from '~/issues/show/components/title.vue';
import eventHub from '~/issues/show/event_hub';
import Store from '~/issues/show/stores';
@@ -29,36 +29,33 @@ describe('Title component', () => {
expect(vm.$el.querySelector('.title').innerHTML.trim()).toBe('Testing <img>');
});
- it('updates page title when changing titleHtml', () => {
+ it('updates page title when changing titleHtml', async () => {
const spy = jest.spyOn(vm, 'setPageTitle');
vm.titleHtml = 'test';
- return vm.$nextTick().then(() => {
- expect(spy).toHaveBeenCalled();
- });
+ await nextTick();
+ expect(spy).toHaveBeenCalled();
});
- it('animates title changes', () => {
+ it('animates title changes', async () => {
vm.titleHtml = 'test';
- return vm
- .$nextTick()
- .then(() => {
- expect(vm.$el.querySelector('.title').classList).toContain('issue-realtime-pre-pulse');
- jest.runAllTimers();
- return vm.$nextTick();
- })
- .then(() => {
- expect(vm.$el.querySelector('.title').classList).toContain('issue-realtime-trigger-pulse');
- });
+
+ await nextTick();
+
+ expect(vm.$el.querySelector('.title').classList).toContain('issue-realtime-pre-pulse');
+ jest.runAllTimers();
+
+ await nextTick();
+
+ expect(vm.$el.querySelector('.title').classList).toContain('issue-realtime-trigger-pulse');
});
- it('updates page title after changing title', () => {
+ it('updates page title after changing title', async () => {
vm.titleHtml = 'changed';
vm.titleText = 'changed';
- return vm.$nextTick().then(() => {
- expect(document.querySelector('title').textContent.trim()).toContain('changed');
- });
+ await nextTick();
+ expect(document.querySelector('title').textContent.trim()).toContain('changed');
});
describe('inline edit button', () => {
@@ -80,16 +77,15 @@ describe('Title component', () => {
expect(vm.$el.querySelector('.btn-edit')).toBeDefined();
});
- it('should trigger open.form event when clicked', () => {
+ it('should trigger open.form event when clicked', async () => {
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
vm.showInlineEditButton = true;
vm.canUpdate = true;
- Vue.nextTick(() => {
- vm.$el.querySelector('.btn-edit').click();
+ await nextTick();
+ vm.$el.querySelector('.btn-edit').click();
- expect(eventHub.$emit).toHaveBeenCalledWith('open.form');
- });
+ expect(eventHub.$emit).toHaveBeenCalledWith('open.form');
});
});
});
diff --git a/spec/frontend/issues/show/mock_data/mock_data.js b/spec/frontend/issues/show/mock_data/mock_data.js
index a73826954c3..89653ff82b2 100644
--- a/spec/frontend/issues/show/mock_data/mock_data.js
+++ b/spec/frontend/issues/show/mock_data/mock_data.js
@@ -58,3 +58,17 @@ export const appProps = {
zoomMeetingUrl,
publishedIncidentUrl,
};
+
+export const descriptionHtmlWithCheckboxes = `
+ <ul dir="auto" class="task-list" data-sourcepos"3:1-5:12">
+ <li class="task-list-item" data-sourcepos="3:1-3:11">
+ <input class="task-list-item-checkbox" type="checkbox"> todo 1
+ </li>
+ <li class="task-list-item" data-sourcepos="4:1-4:12">
+ <input class="task-list-item-checkbox" type="checkbox"> todo 2
+ </li>
+ <li class="task-list-item" data-sourcepos="5:1-5:12">
+ <input class="task-list-item-checkbox" type="checkbox"> todo 3
+ </li>
+ </ul>
+`;
diff --git a/spec/frontend/jira_connect/branches/components/new_branch_form_spec.js b/spec/frontend/jira_connect/branches/components/new_branch_form_spec.js
index 7326b84ad54..b9fed5f34f1 100644
--- a/spec/frontend/jira_connect/branches/components/new_branch_form_spec.js
+++ b/spec/frontend/jira_connect/branches/components/new_branch_form_spec.js
@@ -1,5 +1,6 @@
-import { GlAlert, GlForm, GlFormInput, GlButton } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { GlAlert, GlForm, GlFormInput, GlButton, GlSprintf } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -9,17 +10,12 @@ import SourceBranchDropdown from '~/jira_connect/branches/components/source_bran
import {
CREATE_BRANCH_ERROR_GENERIC,
CREATE_BRANCH_ERROR_WITH_CONTEXT,
+ I18N_NEW_BRANCH_PERMISSION_ALERT,
} from '~/jira_connect/branches/constants';
import createBranchMutation from '~/jira_connect/branches/graphql/mutations/create_branch.mutation.graphql';
+import { mockProjects } from '../mock_data';
-const mockProject = {
- id: 'test',
- fullPath: 'test-path',
- repository: {
- branchNames: ['main', 'f-test', 'release'],
- rootRef: 'main',
- },
-};
+const mockProject = mockProjects[0];
const mockCreateBranchMutationResponse = {
data: {
createBranch: {
@@ -45,28 +41,27 @@ const mockCreateBranchMutationWithErrors = jest
const mockCreateBranchMutationFailed = jest.fn().mockRejectedValue(new Error('GraphQL error'));
const mockMutationLoading = jest.fn().mockReturnValue(new Promise(() => {}));
-const localVue = createLocalVue();
-
describe('NewBranchForm', () => {
let wrapper;
const findSourceBranchDropdown = () => wrapper.findComponent(SourceBranchDropdown);
const findProjectDropdown = () => wrapper.findComponent(ProjectDropdown);
const findAlert = () => wrapper.findComponent(GlAlert);
+ const findAlertSprintf = () => findAlert().findComponent(GlSprintf);
const findForm = () => wrapper.findComponent(GlForm);
const findInput = () => wrapper.findComponent(GlFormInput);
const findButton = () => wrapper.findComponent(GlButton);
const completeForm = async () => {
- await findInput().vm.$emit('input', 'cool-branch-name');
await findProjectDropdown().vm.$emit('change', mockProject);
await findSourceBranchDropdown().vm.$emit('change', 'source-branch');
+ await findInput().vm.$emit('input', 'cool-branch-name');
};
function createMockApolloProvider({
mockCreateBranchMutation = mockCreateBranchMutationSuccess,
} = {}) {
- localVue.use(VueApollo);
+ Vue.use(VueApollo);
const mockApollo = createMockApollo([[createBranchMutation, mockCreateBranchMutation]]);
@@ -75,7 +70,6 @@ describe('NewBranchForm', () => {
function createComponent({ mockApollo, provide } = {}) {
wrapper = shallowMount(NewBranchForm, {
- localVue,
apolloProvider: mockApollo || createMockApolloProvider(),
provide: {
initialBranchName: '',
@@ -89,27 +83,107 @@ describe('NewBranchForm', () => {
});
describe('when selecting items from dropdowns', () => {
- describe('when a project is selected', () => {
- it('sets the `selectedProject` prop for ProjectDropdown and SourceBranchDropdown', async () => {
+ describe('when no project selected', () => {
+ beforeEach(() => {
createComponent();
+ });
- const projectDropdown = findProjectDropdown();
- await projectDropdown.vm.$emit('change', mockProject);
+ it('hides source branch selection and branch name input', () => {
+ expect(findSourceBranchDropdown().exists()).toBe(false);
+ expect(findInput().exists()).toBe(false);
+ });
- expect(projectDropdown.props('selectedProject')).toEqual(mockProject);
- expect(findSourceBranchDropdown().props('selectedProject')).toEqual(mockProject);
+ it('disables the submit button', () => {
+ expect(findButton().props('disabled')).toBe(true);
});
});
- describe('when a source branch is selected', () => {
- it('sets the `selectedBranchName` prop for SourceBranchDropdown', async () => {
+ describe('when a valid project is selected', () => {
+ describe("when a source branch isn't selected", () => {
+ beforeEach(async () => {
+ createComponent();
+ await findProjectDropdown().vm.$emit('change', mockProject);
+ });
+
+ it('sets the `selectedProject` prop for ProjectDropdown and SourceBranchDropdown', () => {
+ expect(findProjectDropdown().props('selectedProject')).toEqual(mockProject);
+ expect(findSourceBranchDropdown().exists()).toBe(true);
+ expect(findSourceBranchDropdown().props('selectedProject')).toEqual(mockProject);
+ });
+
+ it('disables the submit button', () => {
+ expect(findButton().props('disabled')).toBe(true);
+ });
+
+ it('renders branch input field', () => {
+ expect(findInput().exists()).toBe(true);
+ });
+ });
+
+ describe('when `initialBranchName` is provided', () => {
+ it('sets value of branch name input to `initialBranchName` by default', async () => {
+ const mockInitialBranchName = 'ap1-test-branch-name';
+
+ createComponent({ provide: { initialBranchName: mockInitialBranchName } });
+ await findProjectDropdown().vm.$emit('change', mockProject);
+
+ expect(findInput().attributes('value')).toBe(mockInitialBranchName);
+ });
+ });
+
+ describe('when a source branch is selected', () => {
+ it('sets the `selectedBranchName` prop for SourceBranchDropdown', async () => {
+ createComponent();
+ await completeForm();
+
+ const mockBranchName = 'main';
+ const sourceBranchDropdown = findSourceBranchDropdown();
+ await sourceBranchDropdown.vm.$emit('change', mockBranchName);
+
+ expect(sourceBranchDropdown.props('selectedBranchName')).toBe(mockBranchName);
+ });
+
+ describe.each`
+ branchName | submitButtonDisabled
+ ${undefined} | ${true}
+ ${''} | ${true}
+ ${' '} | ${true}
+ ${'test-branch'} | ${false}
+ `('when branch name is $branchName', ({ branchName, submitButtonDisabled }) => {
+ it(`sets submit button 'disabled' prop to ${submitButtonDisabled}`, async () => {
+ createComponent();
+ await completeForm();
+ await findInput().vm.$emit('input', branchName);
+
+ expect(findButton().props('disabled')).toBe(submitButtonDisabled);
+ });
+ });
+ });
+ });
+
+ describe("when user doesn't have push permissions for the selected project", () => {
+ beforeEach(async () => {
createComponent();
- const mockBranchName = 'main';
- const sourceBranchDropdown = findSourceBranchDropdown();
- await sourceBranchDropdown.vm.$emit('change', mockBranchName);
+ const projectDropdown = findProjectDropdown();
+ await projectDropdown.vm.$emit('change', {
+ ...mockProject,
+ userPermissions: { pushCode: false },
+ });
+ });
+
+ it('displays an alert', () => {
+ const alert = findAlert();
+
+ expect(alert.exists()).toBe(true);
+ expect(findAlertSprintf().attributes('message')).toBe(I18N_NEW_BRANCH_PERMISSION_ALERT);
+ expect(alert.props('variant')).toBe('warning');
+ expect(alert.props('dismissible')).toBe(false);
+ });
- expect(sourceBranchDropdown.props('selectedBranchName')).toBe(mockBranchName);
+ it('hides source branch selection and branch name input', () => {
+ expect(findSourceBranchDropdown().exists()).toBe(false);
+ expect(findInput().exists()).toBe(false);
});
});
});
@@ -181,7 +255,7 @@ describe('NewBranchForm', () => {
it('displays an alert', () => {
const alert = findAlert();
expect(alert.exists()).toBe(true);
- expect(alert.text()).toBe(alertText);
+ expect(findAlertSprintf().attributes('message')).toBe(alertText);
expect(alert.props()).toMatchObject({ title: alertTitle, variant: 'danger' });
});
@@ -192,15 +266,6 @@ describe('NewBranchForm', () => {
});
});
- describe('when `initialBranchName` is specified', () => {
- it('sets value of branch name input to `initialBranchName` by default', () => {
- const mockInitialBranchName = 'ap1-test-branch-name';
-
- createComponent({ provide: { initialBranchName: mockInitialBranchName } });
- expect(findInput().attributes('value')).toBe(mockInitialBranchName);
- });
- });
-
describe('error handling', () => {
describe.each`
component | componentName
@@ -211,13 +276,15 @@ describe('NewBranchForm', () => {
beforeEach(async () => {
createComponent();
+ await completeForm();
await wrapper.findComponent(component).vm.$emit('error', { message: mockErrorMessage });
});
it('displays an alert', () => {
const alert = findAlert();
+
expect(alert.exists()).toBe(true);
- expect(alert.text()).toBe(mockErrorMessage);
+ expect(findAlertSprintf().attributes('message')).toBe(mockErrorMessage);
expect(alert.props('variant')).toBe('danger');
});
diff --git a/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js b/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js
index ec4cb2739f8..136a5967ee4 100644
--- a/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js
+++ b/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js
@@ -1,5 +1,12 @@
-import { GlDropdown, GlDropdownItem, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
-import { mount, shallowMount, createLocalVue } from '@vue/test-utils';
+import {
+ GlAvatarLabeled,
+ GlDropdown,
+ GlDropdownItem,
+ GlLoadingIcon,
+ GlSearchBoxByType,
+} from '@gitlab/ui';
+import { mount, shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -7,32 +14,7 @@ import ProjectDropdown from '~/jira_connect/branches/components/project_dropdown
import { PROJECTS_PER_PAGE } from '~/jira_connect/branches/constants';
import getProjectsQuery from '~/jira_connect/branches/graphql/queries/get_projects.query.graphql';
-const localVue = createLocalVue();
-
-const mockProjects = [
- {
- id: 'test',
- name: 'test',
- nameWithNamespace: 'test',
- avatarUrl: 'https://gitlab.com',
- path: 'test-path',
- fullPath: 'test-path',
- repository: {
- empty: false,
- },
- },
- {
- id: 'gitlab',
- name: 'GitLab',
- nameWithNamespace: 'gitlab-org/gitlab',
- avatarUrl: 'https://gitlab.com',
- path: 'gitlab',
- fullPath: 'gitlab-org/gitlab',
- repository: {
- empty: false,
- },
- },
-];
+import { mockProjects } from '../mock_data';
const mockProjectsQueryResponse = {
data: {
@@ -57,12 +39,12 @@ describe('ProjectDropdown', () => {
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findDropdownItemByText = (text) =>
- findAllDropdownItems().wrappers.find((item) => item.text() === text);
+ const findDropdownItemByProjectId = (projectId) =>
+ wrapper.find(`[data-testid="test-project-${projectId}"]`);
const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
function createMockApolloProvider({ mockGetProjectsQuery = mockGetProjectsQuerySuccess } = {}) {
- localVue.use(VueApollo);
+ Vue.use(VueApollo);
const mockApollo = createMockApollo([[getProjectsQuery, mockGetProjectsQuery]]);
@@ -71,7 +53,6 @@ describe('ProjectDropdown', () => {
function createComponent({ mockApollo, props, mountFn = shallowMount } = {}) {
wrapper = mountFn(ProjectDropdown, {
- localVue,
apolloProvider: mockApollo || createMockApolloProvider(),
propsData: props,
});
@@ -101,25 +82,38 @@ describe('ProjectDropdown', () => {
beforeEach(async () => {
createComponent();
await waitForPromises();
- await wrapper.vm.$nextTick();
+ await nextTick();
});
it('sets dropdown `loading` prop to `false`', () => {
expect(findDropdown().props('loading')).toBe(false);
});
- it('renders dropdown items', () => {
+ it('renders dropdown items with correct props', () => {
const dropdownItems = findAllDropdownItems();
+ const avatars = dropdownItems.wrappers.map((item) => item.findComponent(GlAvatarLabeled));
+ const avatarAttributes = avatars.map((avatar) => avatar.attributes());
+ const avatarProps = avatars.map((avatar) => avatar.props());
+
expect(dropdownItems.wrappers).toHaveLength(mockProjects.length);
- expect(dropdownItems.wrappers.map((item) => item.text())).toEqual(
- mockProjects.map((project) => project.nameWithNamespace),
+ expect(avatarProps).toMatchObject(
+ mockProjects.map((project) => ({
+ label: project.name,
+ subLabel: project.nameWithNamespace,
+ })),
+ );
+ expect(avatarAttributes).toMatchObject(
+ mockProjects.map((project) => ({
+ src: project.avatarUrl,
+ 'entity-name': project.name,
+ })),
);
});
describe('when selecting a dropdown item', () => {
- it('emits `change` event with the selected project name', async () => {
+ it('emits `change` event with the selected project', async () => {
const mockProject = mockProjects[0];
- const itemToSelect = findDropdownItemByText(mockProject.nameWithNamespace);
+ const itemToSelect = findDropdownItemByProjectId(mockProject.id);
await itemToSelect.vm.$emit('click');
expect(wrapper.emitted('change')[0]).toEqual([mockProject]);
@@ -129,14 +123,14 @@ describe('ProjectDropdown', () => {
describe('when `selectedProject` prop is specified', () => {
const mockProject = mockProjects[0];
- beforeEach(async () => {
+ beforeEach(() => {
wrapper.setProps({
selectedProject: mockProject,
});
});
it('sets `isChecked` prop of the corresponding dropdown item to `true`', () => {
- expect(findDropdownItemByText(mockProject.nameWithNamespace).props('isChecked')).toBe(true);
+ expect(findDropdownItemByProjectId(mockProject.id).props('isChecked')).toBe(true);
});
it('sets dropdown text to `selectedBranchName` value', () => {
diff --git a/spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js b/spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js
index 9dd11dd6345..56eb6d75def 100644
--- a/spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js
+++ b/spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js
@@ -1,22 +1,22 @@
import { GlDropdown, GlDropdownItem, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
-import { mount, shallowMount, createLocalVue } from '@vue/test-utils';
+import { mount, shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import SourceBranchDropdown from '~/jira_connect/branches/components/source_branch_dropdown.vue';
import { BRANCHES_PER_PAGE } from '~/jira_connect/branches/constants';
import getProjectQuery from '~/jira_connect/branches/graphql/queries/get_project.query.graphql';
-
-const localVue = createLocalVue();
+import { mockProjects } from '../mock_data';
const mockProject = {
id: 'test',
- fullPath: 'test-path',
repository: {
branchNames: ['main', 'f-test', 'release'],
rootRef: 'main',
},
};
+const mockSelectedProject = mockProjects[0];
const mockProjectQueryResponse = {
data: {
@@ -45,7 +45,7 @@ describe('SourceBranchDropdown', () => {
};
function createMockApolloProvider({ getProjectQueryLoading = false } = {}) {
- localVue.use(VueApollo);
+ Vue.use(VueApollo);
const mockApollo = createMockApollo([
[getProjectQuery, getProjectQueryLoading ? mockQueryLoading : mockGetProjectQuery],
@@ -56,7 +56,6 @@ describe('SourceBranchDropdown', () => {
function createComponent({ mockApollo, props, mountFn = shallowMount } = {}) {
wrapper = mountFn(SourceBranchDropdown, {
- localVue,
apolloProvider: mockApollo || createMockApolloProvider(),
propsData: props,
});
@@ -78,7 +77,7 @@ describe('SourceBranchDropdown', () => {
describe('when `selectedProject` becomes specified', () => {
beforeEach(async () => {
wrapper.setProps({
- selectedProject: mockProject,
+ selectedProject: mockSelectedProject,
});
await waitForPromises();
@@ -103,7 +102,7 @@ describe('SourceBranchDropdown', () => {
it('renders loading icon in dropdown', () => {
createComponent({
mockApollo: createMockApolloProvider({ getProjectQueryLoading: true }),
- props: { selectedProject: mockProject },
+ props: { selectedProject: mockSelectedProject },
});
expect(findLoadingIcon().isVisible()).toBe(true);
@@ -113,7 +112,7 @@ describe('SourceBranchDropdown', () => {
describe('when branches have loaded', () => {
describe('when searching branches', () => {
it('triggers a refetch', async () => {
- createComponent({ mountFn: mount, props: { selectedProject: mockProject } });
+ createComponent({ mountFn: mount, props: { selectedProject: mockSelectedProject } });
await waitForPromises();
jest.clearAllMocks();
@@ -131,7 +130,7 @@ describe('SourceBranchDropdown', () => {
describe('template', () => {
beforeEach(async () => {
- createComponent({ props: { selectedProject: mockProject } });
+ createComponent({ props: { selectedProject: mockSelectedProject } });
await waitForPromises();
});
diff --git a/spec/frontend/jira_connect/branches/mock_data.js b/spec/frontend/jira_connect/branches/mock_data.js
new file mode 100644
index 00000000000..742ab5392c8
--- /dev/null
+++ b/spec/frontend/jira_connect/branches/mock_data.js
@@ -0,0 +1,30 @@
+export const mockProjects = [
+ {
+ id: 'test',
+ name: 'test',
+ nameWithNamespace: 'test',
+ avatarUrl: 'https://gitlab.com',
+ path: 'test-path',
+ fullPath: 'test-path',
+ repository: {
+ empty: false,
+ },
+ userPermissions: {
+ pushCode: true,
+ },
+ },
+ {
+ id: 'gitlab',
+ name: 'GitLab',
+ nameWithNamespace: 'gitlab-org/gitlab',
+ avatarUrl: 'https://gitlab.com',
+ path: 'gitlab',
+ fullPath: 'gitlab-org/gitlab',
+ repository: {
+ empty: false,
+ },
+ userPermissions: {
+ pushCode: true,
+ },
+ },
+];
diff --git a/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item_spec.js b/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item_spec.js
index 15e9a740c83..b0d5859cd31 100644
--- a/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item_spec.js
@@ -1,5 +1,6 @@
import { GlButton } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
import * as JiraConnectApi from '~/jira_connect/subscriptions/api';
@@ -63,7 +64,7 @@ describe('GroupsListItem', () => {
clickLinkButton();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findLinkButton().props('loading')).toBe(true);
diff --git a/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_spec.js b/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_spec.js
index 04aba8bda23..d871b1e1dcc 100644
--- a/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_spec.js
@@ -1,5 +1,6 @@
import { GlAlert, GlLoadingIcon, GlSearchBoxByType, GlPagination } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { fetchGroups } from '~/jira_connect/subscriptions/api';
@@ -61,7 +62,7 @@ describe('GroupsList', () => {
fetchGroups.mockReturnValue(new Promise(() => {}));
createComponent();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findGlLoadingIcon().exists()).toBe(true);
});
@@ -100,6 +101,8 @@ describe('GroupsList', () => {
});
createComponent();
+ // wait for the initial loadGroups
+ // to finish.
await waitForPromises();
});
@@ -124,7 +127,7 @@ describe('GroupsList', () => {
findFirstItem().vm.$emit('error', errorMessage);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findGlAlert().exists()).toBe(true);
expect(findGlAlert().text()).toContain(errorMessage);
@@ -136,10 +139,12 @@ describe('GroupsList', () => {
describe('while groups are loading', () => {
beforeEach(async () => {
fetchGroups.mockClear();
+ // return a never-ending promise to make test
+ // deterministic.
fetchGroups.mockReturnValue(new Promise(() => {}));
findSearchBox().vm.$emit('input', mockSearchTeam);
- await wrapper.vm.$nextTick();
+ await nextTick();
});
it('calls `fetchGroups` with search term', () => {
@@ -172,7 +177,7 @@ describe('GroupsList', () => {
describe('when group search finishes loading', () => {
beforeEach(async () => {
fetchGroups.mockResolvedValue({ data: [mockGroup1] });
- findSearchBox().vm.$emit('input');
+ findSearchBox().vm.$emit('input', mockSearchTeam);
await waitForPromises();
});
@@ -183,32 +188,48 @@ describe('GroupsList', () => {
});
});
- it.each`
- userSearchTerm | finalSearchTerm
- ${'gitl'} | ${'gitl'}
- ${'git'} | ${'git'}
- ${'gi'} | ${''}
- ${'g'} | ${''}
- ${''} | ${''}
- ${undefined} | ${undefined}
+ describe.each`
+ previousSearch | newSearch | shouldSearch | expectedSearchValue
+ ${''} | ${'git'} | ${true} | ${'git'}
+ ${'g'} | ${'git'} | ${true} | ${'git'}
+ ${'git'} | ${'gitl'} | ${true} | ${'gitl'}
+ ${'git'} | ${'gi'} | ${true} | ${''}
+ ${'gi'} | ${'g'} | ${false} | ${undefined}
+ ${'g'} | ${''} | ${false} | ${undefined}
+ ${''} | ${'g'} | ${false} | ${undefined}
`(
- 'searches for "$finalSearchTerm" when user enters "$userSearchTerm"',
- async ({ userSearchTerm, finalSearchTerm }) => {
- fetchGroups.mockResolvedValue({
- data: [mockGroup1],
- headers: { 'X-PAGE': 1, 'X-TOTAL': 1 },
+ 'when previous search was "$previousSearch" and user enters "$newSearch"',
+ ({ previousSearch, newSearch, shouldSearch, expectedSearchValue }) => {
+ beforeEach(async () => {
+ fetchGroups.mockResolvedValue({
+ data: [mockGroup1],
+ headers: { 'X-PAGE': 1, 'X-TOTAL': 1 },
+ });
+
+ // wait for initial load
+ createComponent();
+ await waitForPromises();
+
+ // set up the "previous search"
+ findSearchBox().vm.$emit('input', previousSearch);
+ await waitForPromises();
+
+ fetchGroups.mockClear();
});
- createComponent();
- await waitForPromises();
-
- const searchBox = findSearchBox();
- searchBox.vm.$emit('input', userSearchTerm);
-
- expect(fetchGroups).toHaveBeenLastCalledWith(mockGroupsPath, {
- page: 1,
- perPage: DEFAULT_GROUPS_PER_PAGE,
- search: finalSearchTerm,
+ it(`${shouldSearch ? 'should' : 'should not'} execute fetch new results`, () => {
+ // enter the new search
+ findSearchBox().vm.$emit('input', newSearch);
+
+ if (shouldSearch) {
+ expect(fetchGroups).toHaveBeenCalledWith(mockGroupsPath, {
+ page: 1,
+ perPage: DEFAULT_GROUPS_PER_PAGE,
+ search: expectedSearchValue,
+ });
+ } else {
+ expect(fetchGroups).not.toHaveBeenCalled();
+ }
});
},
);
@@ -226,7 +247,13 @@ describe('GroupsList', () => {
await waitForPromises();
const paginationEl = findPagination();
- paginationEl.vm.$emit('input', 2);
+
+ // mock the response from page 2
+ fetchGroups.mockResolvedValue({
+ headers: { 'X-TOTAL': totalItems, 'X-PAGE': 2 },
+ data: mockGroups,
+ });
+ await paginationEl.vm.$emit('input', 2);
});
it('should load results for page 2', () => {
@@ -237,18 +264,23 @@ describe('GroupsList', () => {
});
});
- it('resets page to 1 on search `input` event', () => {
- const mockSearchTerm = 'gitlab';
- const searchBox = findSearchBox();
-
- searchBox.vm.$emit('input', mockSearchTerm);
+ it.each`
+ scenario | searchTerm | expectedPage | expectedSearchTerm
+ ${'preserves current page'} | ${'gi'} | ${2} | ${''}
+ ${'resets page to 1'} | ${'gitlab'} | ${1} | ${'gitlab'}
+ `(
+ '$scenario when search term is $searchTerm',
+ ({ searchTerm, expectedPage, expectedSearchTerm }) => {
+ const searchBox = findSearchBox();
+ searchBox.vm.$emit('input', searchTerm);
- expect(fetchGroups).toHaveBeenLastCalledWith(mockGroupsPath, {
- page: 1,
- perPage: DEFAULT_GROUPS_PER_PAGE,
- search: mockSearchTerm,
- });
- });
+ expect(fetchGroups).toHaveBeenLastCalledWith(mockGroupsPath, {
+ page: expectedPage,
+ perPage: DEFAULT_GROUPS_PER_PAGE,
+ search: expectedSearchTerm,
+ });
+ },
+ );
});
});
diff --git a/spec/frontend/jira_connect/subscriptions/components/app_spec.js b/spec/frontend/jira_connect/subscriptions/components/app_spec.js
index 47fe96262ee..aa0f1440b20 100644
--- a/spec/frontend/jira_connect/subscriptions/components/app_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/app_spec.js
@@ -1,10 +1,10 @@
-import { GlAlert, GlLink, GlEmptyState } from '@gitlab/ui';
-import { mount, shallowMount } from '@vue/test-utils';
+import { GlLink } from '@gitlab/ui';
+import { nextTick } from 'vue';
+import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import JiraConnectApp from '~/jira_connect/subscriptions/components/app.vue';
-import AddNamespaceButton from '~/jira_connect/subscriptions/components/add_namespace_button.vue';
-import SignInButton from '~/jira_connect/subscriptions/components/sign_in_button.vue';
-import SubscriptionsList from '~/jira_connect/subscriptions/components/subscriptions_list.vue';
+import SignInPage from '~/jira_connect/subscriptions/pages/sign_in.vue';
+import SubscriptionsPage from '~/jira_connect/subscriptions/pages/subscriptions.vue';
import UserLink from '~/jira_connect/subscriptions/components/user_link.vue';
import createStore from '~/jira_connect/subscriptions/store';
import { SET_ALERT } from '~/jira_connect/subscriptions/store/mutation_types';
@@ -20,14 +20,12 @@ describe('JiraConnectApp', () => {
let wrapper;
let store;
- const findAlert = () => wrapper.findComponent(GlAlert);
+ const findAlert = () => wrapper.findByTestId('jira-connect-persisted-alert');
const findAlertLink = () => findAlert().findComponent(GlLink);
- const findSignInButton = () => wrapper.findComponent(SignInButton);
- const findAddNamespaceButton = () => wrapper.findComponent(AddNamespaceButton);
- const findSubscriptionsList = () => wrapper.findComponent(SubscriptionsList);
- const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+ const findSignInPage = () => wrapper.findComponent(SignInPage);
+ const findSubscriptionsPage = () => wrapper.findComponent(SubscriptionsPage);
- const createComponent = ({ provide, mountFn = shallowMount } = {}) => {
+ const createComponent = ({ provide, mountFn = shallowMountExtended } = {}) => {
store = createStore();
wrapper = mountFn(JiraConnectApp, {
@@ -42,49 +40,35 @@ describe('JiraConnectApp', () => {
describe('template', () => {
describe.each`
- scenario | usersPath | subscriptions | expectSignInButton | expectEmptyState | expectNamespaceButton | expectSubscriptionsList
- ${'user is not signed in with subscriptions'} | ${'/users'} | ${[mockSubscription]} | ${true} | ${false} | ${false} | ${true}
- ${'user is not signed in without subscriptions'} | ${'/users'} | ${undefined} | ${true} | ${false} | ${false} | ${false}
- ${'user is signed in with subscriptions'} | ${undefined} | ${[mockSubscription]} | ${false} | ${false} | ${true} | ${true}
- ${'user is signed in without subscriptions'} | ${undefined} | ${undefined} | ${false} | ${true} | ${false} | ${false}
- `(
- 'when $scenario',
- ({
- usersPath,
- expectSignInButton,
- subscriptions,
- expectEmptyState,
- expectNamespaceButton,
- expectSubscriptionsList,
- }) => {
- beforeEach(() => {
- createComponent({
- provide: {
- usersPath,
- subscriptions,
- },
- });
- });
-
- it(`${expectSignInButton ? 'renders' : 'does not render'} sign in button`, () => {
- expect(findSignInButton().exists()).toBe(expectSignInButton);
- });
-
- it(`${expectEmptyState ? 'renders' : 'does not render'} empty state`, () => {
- expect(findEmptyState().exists()).toBe(expectEmptyState);
+ scenario | usersPath | shouldRenderSignInPage | shouldRenderSubscriptionsPage
+ ${'user is not signed in'} | ${'/users'} | ${true} | ${false}
+ ${'user is signed in'} | ${undefined} | ${false} | ${true}
+ `('when $scenario', ({ usersPath, shouldRenderSignInPage, shouldRenderSubscriptionsPage }) => {
+ beforeEach(() => {
+ createComponent({
+ provide: {
+ usersPath,
+ subscriptions: [mockSubscription],
+ },
});
+ });
- it(`${
- expectNamespaceButton ? 'renders' : 'does not render'
- } button to add namespace`, () => {
- expect(findAddNamespaceButton().exists()).toBe(expectNamespaceButton);
- });
+ it(`${shouldRenderSignInPage ? 'renders' : 'does not render'} sign in page`, () => {
+ expect(findSignInPage().exists()).toBe(shouldRenderSignInPage);
+ if (shouldRenderSignInPage) {
+ expect(findSignInPage().props('hasSubscriptions')).toBe(true);
+ }
+ });
- it(`${expectSubscriptionsList ? 'renders' : 'does not render'} subscriptions list`, () => {
- expect(findSubscriptionsList().exists()).toBe(expectSubscriptionsList);
- });
- },
- );
+ it(`${
+ shouldRenderSubscriptionsPage ? 'renders' : 'does not render'
+ } subscriptions page`, () => {
+ expect(findSubscriptionsPage().exists()).toBe(shouldRenderSubscriptionsPage);
+ if (shouldRenderSubscriptionsPage) {
+ expect(findSubscriptionsPage().props('hasSubscriptions')).toBe(true);
+ }
+ });
+ });
it('renders UserLink component', () => {
createComponent({
@@ -116,7 +100,7 @@ describe('JiraConnectApp', () => {
createComponent();
store.commit(SET_ALERT, { message, variant });
- await wrapper.vm.$nextTick();
+ await nextTick();
const alert = findAlert();
@@ -134,22 +118,22 @@ describe('JiraConnectApp', () => {
createComponent();
store.commit(SET_ALERT, { message: 'test message' });
- await wrapper.vm.$nextTick();
+ await nextTick();
findAlert().vm.$emit('dismiss');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findAlert().exists()).toBe(false);
});
it('renders link when `linkUrl` is set', async () => {
- createComponent({ mountFn: mount });
+ createComponent({ mountFn: mountExtended });
store.commit(SET_ALERT, {
message: __('test message %{linkStart}test link%{linkEnd}'),
linkUrl: 'https://gitlab.com',
});
- await wrapper.vm.$nextTick();
+ await nextTick();
const alertLink = findAlertLink();
diff --git a/spec/frontend/jira_connect/subscriptions/components/compatibility_alert_spec.js b/spec/frontend/jira_connect/subscriptions/components/compatibility_alert_spec.js
new file mode 100644
index 00000000000..f8ee8c2c664
--- /dev/null
+++ b/spec/frontend/jira_connect/subscriptions/components/compatibility_alert_spec.js
@@ -0,0 +1,56 @@
+import { GlAlert, GlLink } from '@gitlab/ui';
+import { shallowMount, mount } from '@vue/test-utils';
+import CompatibilityAlert from '~/jira_connect/subscriptions/components/compatibility_alert.vue';
+
+import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
+
+describe('CompatibilityAlert', () => {
+ let wrapper;
+
+ const createComponent = ({ mountFn = shallowMount } = {}) => {
+ wrapper = mountFn(CompatibilityAlert);
+ };
+
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findLink = () => wrapper.findComponent(GlLink);
+ const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('displays an alert', () => {
+ createComponent();
+
+ expect(findAlert().exists()).toBe(true);
+ });
+
+ it('renders help link with target="_blank" and rel="noopener noreferrer"', () => {
+ createComponent({ mountFn: mount });
+ expect(findLink().attributes()).toMatchObject({
+ target: '_blank',
+ rel: 'noopener noreferrer',
+ });
+ });
+
+ it('`local-storage-sync` value prop is initially false', () => {
+ createComponent();
+
+ expect(findLocalStorageSync().props('value')).toBe(false);
+ });
+
+ describe('when dismissed', () => {
+ beforeEach(async () => {
+ createComponent();
+ await findAlert().vm.$emit('dismiss');
+ });
+
+ it('hides alert', () => {
+ expect(findAlert().exists()).toBe(false);
+ });
+
+ it('updates value prop of `local-storage-sync`', () => {
+ expect(findLocalStorageSync().props('value')).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/jira_connect/subscriptions/components/sign_in_button_spec.js b/spec/frontend/jira_connect/subscriptions/components/sign_in_button_spec.js
index cb5ae877c47..94dcf9decec 100644
--- a/spec/frontend/jira_connect/subscriptions/components/sign_in_button_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/sign_in_button_spec.js
@@ -11,11 +11,12 @@ jest.mock('~/jira_connect/subscriptions/utils');
describe('SignInButton', () => {
let wrapper;
- const createComponent = () => {
+ const createComponent = ({ slots } = {}) => {
wrapper = shallowMount(SignInButton, {
propsData: {
usersPath: MOCK_USERS_PATH,
},
+ slots,
});
};
@@ -29,6 +30,7 @@ describe('SignInButton', () => {
createComponent();
expect(findButton().exists()).toBe(true);
+ expect(findButton().text()).toBe(SignInButton.i18n.defaultButtonText);
});
describe.each`
@@ -45,4 +47,12 @@ describe('SignInButton', () => {
expect(findButton().attributes('href')).toBe(expectedHref);
});
});
+
+ describe('with slot', () => {
+ const mockSlotContent = 'custom button content!';
+ it('renders slot content in button', () => {
+ createComponent({ slots: { default: mockSlotContent } });
+ expect(wrapper.text()).toMatchInterpolatedText(mockSlotContent);
+ });
+ });
});
diff --git a/spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js b/spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js
index 4e4a2b58600..2aad533f677 100644
--- a/spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js
@@ -1,5 +1,6 @@
import { GlButton } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
import * as JiraConnectApi from '~/jira_connect/subscriptions/api';
@@ -71,7 +72,7 @@ describe('SubscriptionsList', () => {
clickUnlinkButton();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findUnlinkButton().props('loading')).toBe(true);
diff --git a/spec/frontend/jira_connect/subscriptions/pages/sign_in_spec.js b/spec/frontend/jira_connect/subscriptions/pages/sign_in_spec.js
new file mode 100644
index 00000000000..4e3297506f1
--- /dev/null
+++ b/spec/frontend/jira_connect/subscriptions/pages/sign_in_spec.js
@@ -0,0 +1,62 @@
+import { mount } from '@vue/test-utils';
+
+import SignInPage from '~/jira_connect/subscriptions/pages/sign_in.vue';
+import SignInButton from '~/jira_connect/subscriptions/components/sign_in_button.vue';
+import SubscriptionsList from '~/jira_connect/subscriptions/components/subscriptions_list.vue';
+import createStore from '~/jira_connect/subscriptions/store';
+
+jest.mock('~/jira_connect/subscriptions/utils');
+
+describe('SignInPage', () => {
+ let wrapper;
+ let store;
+
+ const findSignInButton = () => wrapper.findComponent(SignInButton);
+ const findSubscriptionsList = () => wrapper.findComponent(SubscriptionsList);
+
+ const createComponent = ({ provide, props } = {}) => {
+ store = createStore();
+
+ wrapper = mount(SignInPage, {
+ store,
+ provide,
+ propsData: props,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('template', () => {
+ const mockUsersPath = '/test';
+ describe.each`
+ scenario | expectSubscriptionsList | signInButtonText
+ ${'with subscriptions'} | ${true} | ${SignInPage.i18n.signinButtonTextWithSubscriptions}
+ ${'without subscriptions'} | ${false} | ${SignInButton.i18n.defaultButtonText}
+ `('$scenario', ({ expectSubscriptionsList, signInButtonText }) => {
+ beforeEach(() => {
+ createComponent({
+ provide: {
+ usersPath: mockUsersPath,
+ },
+ props: {
+ hasSubscriptions: expectSubscriptionsList,
+ },
+ });
+ });
+
+ it(`renders sign in button with text ${signInButtonText}`, () => {
+ expect(findSignInButton().text()).toMatchInterpolatedText(signInButtonText);
+ });
+
+ it('renders sign in button with `usersPath` prop', () => {
+ expect(findSignInButton().props('usersPath')).toBe(mockUsersPath);
+ });
+
+ it(`${expectSubscriptionsList ? 'renders' : 'does not render'} subscriptions list`, () => {
+ expect(findSubscriptionsList().exists()).toBe(expectSubscriptionsList);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/jira_connect/subscriptions/pages/subscriptions_spec.js b/spec/frontend/jira_connect/subscriptions/pages/subscriptions_spec.js
new file mode 100644
index 00000000000..198278efc1f
--- /dev/null
+++ b/spec/frontend/jira_connect/subscriptions/pages/subscriptions_spec.js
@@ -0,0 +1,56 @@
+import { GlEmptyState } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import SubscriptionsPage from '~/jira_connect/subscriptions/pages/subscriptions.vue';
+import AddNamespaceButton from '~/jira_connect/subscriptions/components/add_namespace_button.vue';
+import SubscriptionsList from '~/jira_connect/subscriptions/components/subscriptions_list.vue';
+import createStore from '~/jira_connect/subscriptions/store';
+
+describe('SubscriptionsPage', () => {
+ let wrapper;
+ let store;
+
+ const findAddNamespaceButton = () => wrapper.findComponent(AddNamespaceButton);
+ const findSubscriptionsList = () => wrapper.findComponent(SubscriptionsList);
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+
+ const createComponent = ({ props } = {}) => {
+ store = createStore();
+
+ wrapper = shallowMount(SubscriptionsPage, {
+ store,
+ propsData: props,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('template', () => {
+ describe.each`
+ scenario | expectSubscriptionsList | expectEmptyState
+ ${'with subscriptions'} | ${true} | ${false}
+ ${'without subscriptions'} | ${false} | ${true}
+ `('$scenario', ({ expectEmptyState, expectSubscriptionsList }) => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ hasSubscriptions: expectSubscriptionsList,
+ },
+ });
+ });
+
+ it('renders button to add namespace', () => {
+ expect(findAddNamespaceButton().exists()).toBe(true);
+ });
+
+ it(`${expectEmptyState ? 'renders' : 'does not render'} empty state`, () => {
+ expect(findEmptyState().exists()).toBe(expectEmptyState);
+ });
+
+ it(`${expectSubscriptionsList ? 'renders' : 'does not render'} subscriptions list`, () => {
+ expect(findSubscriptionsList().exists()).toBe(expectSubscriptionsList);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/jira_import/components/jira_import_app_spec.js b/spec/frontend/jira_import/components/jira_import_app_spec.js
index 27314a0eb6e..cd8024d4962 100644
--- a/spec/frontend/jira_import/components/jira_import_app_spec.js
+++ b/spec/frontend/jira_import/components/jira_import_app_spec.js
@@ -1,6 +1,6 @@
import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
+import { nextTick } from 'vue';
import JiraImportApp from '~/jira_import/components/jira_import_app.vue';
import JiraImportForm from '~/jira_import/components/jira_import_form.vue';
import JiraImportProgress from '~/jira_import/components/jira_import_progress.vue';
@@ -230,7 +230,7 @@ describe('JiraImportApp', () => {
getFormComponent().vm.$emit('error', 'There was an error');
- await Vue.nextTick();
+ await nextTick();
expect(getAlert().exists()).toBe(true);
});
@@ -248,7 +248,7 @@ describe('JiraImportApp', () => {
getAlert().vm.$emit('dismiss');
- await Vue.nextTick();
+ await nextTick();
expect(getAlert().exists()).toBe(false);
});
diff --git a/spec/frontend/jobs/bridge/app_spec.js b/spec/frontend/jobs/bridge/app_spec.js
index c0faab90552..210dcfa364b 100644
--- a/spec/frontend/jobs/bridge/app_spec.js
+++ b/spec/frontend/jobs/bridge/app_spec.js
@@ -1,5 +1,6 @@
-import { nextTick } from 'vue';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
+import { shallowMount } from '@vue/test-utils';
+
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
import { GlLoadingIcon } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
@@ -17,9 +18,6 @@ import {
mockPipelineQueryResponse,
} from './mock_data';
-const localVue = createLocalVue();
-localVue.use(VueApollo);
-
describe('Bridge Show Page', () => {
let wrapper;
let mockApollo;
@@ -47,10 +45,10 @@ describe('Bridge Show Page', () => {
const createComponentWithApollo = () => {
const handlers = [[getPipelineQuery, mockPipelineQuery]];
+ Vue.use(VueApollo);
mockApollo = createMockApollo(handlers);
createComponent({
- localVue,
apolloProvider: mockApollo,
mocks: {},
});
@@ -81,10 +79,10 @@ describe('Bridge Show Page', () => {
});
describe('after pipeline query is loaded', () => {
- beforeEach(() => {
+ beforeEach(async () => {
mockPipelineQuery.mockResolvedValue(mockPipelineQueryResponse);
createComponentWithApollo();
- waitForPromises();
+ await waitForPromises();
});
it('query is called with correct variables', async () => {
@@ -109,10 +107,10 @@ describe('Bridge Show Page', () => {
});
describe('sidebar expansion', () => {
- beforeEach(() => {
+ beforeEach(async () => {
mockPipelineQuery.mockResolvedValue(mockPipelineQueryResponse);
createComponentWithApollo();
- waitForPromises();
+ await waitForPromises();
});
describe('on resize', () => {
diff --git a/spec/frontend/jobs/components/job_app_spec.js b/spec/frontend/jobs/components/job_app_spec.js
index 07e6ee46c41..d4e1e711777 100644
--- a/spec/frontend/jobs/components/job_app_spec.js
+++ b/spec/frontend/jobs/components/job_app_spec.js
@@ -1,5 +1,6 @@
import { GlLoadingIcon } from '@gitlab/ui';
-import { mount, createLocalVue } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import MockAdapter from 'axios-mock-adapter';
import Vuex from 'vuex';
import delayedJobFixture from 'test_fixtures/jobs/delayed.json';
@@ -16,8 +17,7 @@ import axios from '~/lib/utils/axios_utils';
import job from '../mock_data';
describe('Job App', () => {
- const localVue = createLocalVue();
- localVue.use(Vuex);
+ Vue.use(Vuex);
let store;
let wrapper;
@@ -45,7 +45,7 @@ describe('Job App', () => {
wrapper = mount(JobApp, { propsData: { ...props }, store });
};
- const setupAndMount = ({ jobData = {}, jobLogData = {} } = {}) => {
+ const setupAndMount = async ({ jobData = {}, jobLogData = {} } = {}) => {
mock.onGet(initSettings.endpoint).replyOnce(200, { ...job, ...jobData });
mock.onGet(`${initSettings.pagePath}/trace.json`).reply(200, jobLogData);
@@ -53,12 +53,10 @@ describe('Job App', () => {
createComponent();
- return asyncInit
- .then(() => {
- jest.runOnlyPendingTimers();
- })
- .then(() => axios.waitForAll())
- .then(() => wrapper.vm.$nextTick());
+ await asyncInit;
+ jest.runOnlyPendingTimers();
+ await axios.waitForAll();
+ await nextTick();
};
const findLoadingComponent = () => wrapper.find(GlLoadingIcon);
diff --git a/spec/frontend/jobs/components/job_container_item_spec.js b/spec/frontend/jobs/components/job_container_item_spec.js
index 6b488821bc1..eb2b0184e5f 100644
--- a/spec/frontend/jobs/components/job_container_item_spec.js
+++ b/spec/frontend/jobs/components/job_container_item_spec.js
@@ -1,5 +1,6 @@
import { GlIcon, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import delayedJobFixture from 'test_fixtures/jobs/delayed.json';
import JobContainerItem from '~/jobs/components/job_container_item.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
@@ -87,7 +88,7 @@ describe('JobContainerItem', () => {
});
it('displays remaining time in tooltip', async () => {
- await wrapper.vm.$nextTick();
+ await nextTick();
const link = wrapper.findComponent(GlLink);
diff --git a/spec/frontend/jobs/components/job_log_controllers_spec.js b/spec/frontend/jobs/components/job_log_controllers_spec.js
index 0ba07522243..226322a2951 100644
--- a/spec/frontend/jobs/components/job_log_controllers_spec.js
+++ b/spec/frontend/jobs/components/job_log_controllers_spec.js
@@ -1,4 +1,5 @@
import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import JobLogControllers from '~/jobs/components/job_log_controllers.vue';
describe('Job log controllers', () => {
@@ -111,7 +112,7 @@ describe('Job log controllers', () => {
it('emits scrollJobLogTop event on click', async () => {
findScrollTop().trigger('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.emitted().scrollJobLogTop).toHaveLength(1);
});
@@ -133,7 +134,7 @@ describe('Job log controllers', () => {
it('does not emit scrollJobLogTop event on click', async () => {
findScrollTop().trigger('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.emitted().scrollJobLogTop).toBeUndefined();
});
@@ -149,7 +150,7 @@ describe('Job log controllers', () => {
it('emits scrollJobLogBottom event on click', async () => {
findScrollBottom().trigger('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.emitted().scrollJobLogBottom).toHaveLength(1);
});
@@ -171,7 +172,7 @@ describe('Job log controllers', () => {
it('does not emit scrollJobLogBottom event on click', async () => {
findScrollBottom().trigger('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.emitted().scrollJobLogBottom).toBeUndefined();
});
diff --git a/spec/frontend/jobs/components/log/collapsible_section_spec.js b/spec/frontend/jobs/components/log/collapsible_section_spec.js
index 96bdf03796b..22ddc8b1c2d 100644
--- a/spec/frontend/jobs/components/log/collapsible_section_spec.js
+++ b/spec/frontend/jobs/components/log/collapsible_section_spec.js
@@ -1,4 +1,5 @@
import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import CollapsibleSection from '~/jobs/components/log/collapsible_section.vue';
import { collapsibleSectionClosed, collapsibleSectionOpened } from './mock_data';
@@ -69,7 +70,7 @@ describe('Job Log Collapsible Section', () => {
});
});
- it('emits onClickCollapsibleLine on click', () => {
+ it('emits onClickCollapsibleLine on click', async () => {
createComponent({
section: collapsibleSectionOpened,
jobLogEndpoint,
@@ -77,8 +78,7 @@ describe('Job Log Collapsible Section', () => {
findCollapsibleLine().trigger('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted('onClickCollapsibleLine').length).toBe(1);
- });
+ await nextTick();
+ expect(wrapper.emitted('onClickCollapsibleLine').length).toBe(1);
});
});
diff --git a/spec/frontend/jobs/components/log/line_header_spec.js b/spec/frontend/jobs/components/log/line_header_spec.js
index 9763e2f437b..8055fe64d95 100644
--- a/spec/frontend/jobs/components/log/line_header_spec.js
+++ b/spec/frontend/jobs/components/log/line_header_spec.js
@@ -1,4 +1,5 @@
import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import DurationBadge from '~/jobs/components/log/duration_badge.vue';
import LineHeader from '~/jobs/components/log/line_header.vue';
import LineNumber from '~/jobs/components/log/line_number.vue';
@@ -75,12 +76,11 @@ describe('Job Log Header Line', () => {
createComponent(data);
});
- it('emits toggleLine event', () => {
+ it('emits toggleLine event', async () => {
wrapper.trigger('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted().toggleLine.length).toBe(1);
- });
+ await nextTick();
+ expect(wrapper.emitted().toggleLine.length).toBe(1);
});
});
diff --git a/spec/frontend/jobs/components/log/log_spec.js b/spec/frontend/jobs/components/log/log_spec.js
index 9a5522ab4cd..7e11738f82e 100644
--- a/spec/frontend/jobs/components/log/log_spec.js
+++ b/spec/frontend/jobs/components/log/log_spec.js
@@ -1,4 +1,5 @@
-import { mount, createLocalVue } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import Log from '~/jobs/components/log/log.vue';
import { logLinesParserLegacy, logLinesParser } from '~/jobs/store/utils';
@@ -11,12 +12,10 @@ describe('Job Log', () => {
let store;
let origGon;
- const localVue = createLocalVue();
- localVue.use(Vuex);
+ Vue.use(Vuex);
const createComponent = () => {
wrapper = mount(Log, {
- localVue,
store,
});
};
@@ -91,12 +90,10 @@ describe('Job Log, infinitelyCollapsibleSections feature flag enabled', () => {
let store;
let origGon;
- const localVue = createLocalVue();
- localVue.use(Vuex);
+ Vue.use(Vuex);
const createComponent = () => {
wrapper = mount(Log, {
- localVue,
store,
});
};
diff --git a/spec/frontend/jobs/components/manual_variables_form_spec.js b/spec/frontend/jobs/components/manual_variables_form_spec.js
index a5278af8e33..6faab3ddf31 100644
--- a/spec/frontend/jobs/components/manual_variables_form_spec.js
+++ b/spec/frontend/jobs/components/manual_variables_form_spec.js
@@ -1,12 +1,10 @@
import { GlSprintf, GlLink } from '@gitlab/ui';
-import { createLocalVue, mount } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import ManualVariablesForm from '~/jobs/components/manual_variables_form.vue';
-const localVue = createLocalVue();
-
Vue.use(Vuex);
describe('Manual Variables Form', () => {
@@ -29,9 +27,8 @@ describe('Manual Variables Form', () => {
});
wrapper = extendedWrapper(
- mount(localVue.extend(ManualVariablesForm), {
+ mount(ManualVariablesForm, {
propsData: { ...requiredProps, ...props },
- localVue,
store,
stubs: {
GlSprintf,
diff --git a/spec/frontend/jobs/components/sidebar_spec.js b/spec/frontend/jobs/components/sidebar_spec.js
index 500a1b48950..6e327725627 100644
--- a/spec/frontend/jobs/components/sidebar_spec.js
+++ b/spec/frontend/jobs/components/sidebar_spec.js
@@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import ArtifactsBlock from '~/jobs/components/artifacts_block.vue';
import JobRetryForwardDeploymentModal from '~/jobs/components/job_retry_forward_deployment_modal.vue';
@@ -189,7 +190,7 @@ describe('Sidebar details block', () => {
locked: false,
};
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findArtifactsBlock().exists()).toBe(true);
});
diff --git a/spec/frontend/jobs/components/table/cells/actions_cell_spec.js b/spec/frontend/jobs/components/table/cells/actions_cell_spec.js
index 6caf36e1461..263698e94e1 100644
--- a/spec/frontend/jobs/components/table/cells/actions_cell_spec.js
+++ b/spec/frontend/jobs/components/table/cells/actions_cell_spec.js
@@ -1,13 +1,16 @@
import { GlModal } from '@gitlab/ui';
import { nextTick } from 'vue';
+import waitForPromises from 'helpers/wait_for_promises';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ActionsCell from '~/jobs/components/table/cells/actions_cell.vue';
import JobPlayMutation from '~/jobs/components/table/graphql/mutations/job_play.mutation.graphql';
import JobRetryMutation from '~/jobs/components/table/graphql/mutations/job_retry.mutation.graphql';
import JobUnscheduleMutation from '~/jobs/components/table/graphql/mutations/job_unschedule.mutation.graphql';
+import JobCancelMutation from '~/jobs/components/table/graphql/mutations/job_cancel.mutation.graphql';
import {
playableJob,
retryableJob,
+ cancelableJob,
scheduledJob,
cannotRetryJob,
cannotPlayJob,
@@ -20,6 +23,7 @@ describe('Job actions cell', () => {
const findRetryButton = () => wrapper.findByTestId('retry');
const findPlayButton = () => wrapper.findByTestId('play');
+ const findCancelButton = () => wrapper.findByTestId('cancel-button');
const findDownloadArtifactsButton = () => wrapper.findByTestId('download-artifacts');
const findCountdownButton = () => wrapper.findByTestId('countdown');
const findPlayScheduledJobButton = () => wrapper.findByTestId('play-scheduled');
@@ -32,6 +36,7 @@ describe('Job actions cell', () => {
data: { JobUnscheduleMutation: { jobId: scheduledJob.id } },
};
const MUTATION_SUCCESS_PLAY = { data: { JobPlayMutation: { jobId: playableJob.id } } };
+ const MUTATION_SUCCESS_CANCEL = { data: { JobCancelMutation: { jobId: cancelableJob.id } } };
const $toast = {
show: jest.fn(),
@@ -88,6 +93,7 @@ describe('Job actions cell', () => {
${findPlayButton} | ${'play'} | ${playableJob}
${findRetryButton} | ${'retry'} | ${retryableJob}
${findDownloadArtifactsButton} | ${'download artifacts'} | ${playableJob}
+ ${findCancelButton} | ${'cancel'} | ${cancelableJob}
`('displays the $action button', ({ button, jobType }) => {
createComponent(jobType);
@@ -95,9 +101,10 @@ describe('Job actions cell', () => {
});
it.each`
- button | mutationResult | action | jobType | mutationFile
- ${findPlayButton} | ${MUTATION_SUCCESS_PLAY} | ${'play'} | ${playableJob} | ${JobPlayMutation}
- ${findRetryButton} | ${MUTATION_SUCCESS} | ${'retry'} | ${retryableJob} | ${JobRetryMutation}
+ button | mutationResult | action | jobType | mutationFile
+ ${findPlayButton} | ${MUTATION_SUCCESS_PLAY} | ${'play'} | ${playableJob} | ${JobPlayMutation}
+ ${findRetryButton} | ${MUTATION_SUCCESS} | ${'retry'} | ${retryableJob} | ${JobRetryMutation}
+ ${findCancelButton} | ${MUTATION_SUCCESS_CANCEL} | ${'cancel'} | ${cancelableJob} | ${JobCancelMutation}
`('performs the $action mutation', ({ button, mutationResult, jobType, mutationFile }) => {
createComponent(jobType, mutationResult);
@@ -111,6 +118,24 @@ describe('Job actions cell', () => {
});
});
+ it.each`
+ button | action | jobType
+ ${findPlayButton} | ${'play'} | ${playableJob}
+ ${findRetryButton} | ${'retry'} | ${retryableJob}
+ ${findCancelButton} | ${'cancel'} | ${cancelableJob}
+ ${findUnscheduleButton} | ${'unschedule'} | ${scheduledJob}
+ `('disables the $action button after first request', async ({ button, jobType }) => {
+ createComponent(jobType);
+
+ expect(button().props('disabled')).toBe(false);
+
+ button().vm.$emit('click');
+
+ await waitForPromises();
+
+ expect(button().props('disabled')).toBe(true);
+ });
+
describe('Scheduled Jobs', () => {
const today = () => new Date('2021-08-31');
diff --git a/spec/frontend/jobs/components/table/job_table_app_spec.js b/spec/frontend/jobs/components/table/job_table_app_spec.js
index 05988eecb10..5ccd38af735 100644
--- a/spec/frontend/jobs/components/table/job_table_app_spec.js
+++ b/spec/frontend/jobs/components/table/job_table_app_spec.js
@@ -1,5 +1,6 @@
import { GlSkeletonLoader, GlAlert, GlEmptyState, GlPagination } from '@gitlab/ui';
-import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
+import { mount, shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -7,11 +8,15 @@ import getJobsQuery from '~/jobs/components/table/graphql/queries/get_jobs.query
import JobsTable from '~/jobs/components/table/jobs_table.vue';
import JobsTableApp from '~/jobs/components/table/jobs_table_app.vue';
import JobsTableTabs from '~/jobs/components/table/jobs_table_tabs.vue';
-import { mockJobsQueryResponse, mockJobsQueryEmptyResponse } from '../../mock_data';
+import {
+ mockJobsQueryResponse,
+ mockJobsQueryEmptyResponse,
+ mockJobsQueryResponseLastPage,
+ mockJobsQueryResponseFirstPage,
+} from '../../mock_data';
const projectPath = 'gitlab-org/gitlab';
-const localVue = createLocalVue();
-localVue.use(VueApollo);
+Vue.use(VueApollo);
describe('Job table app', () => {
let wrapper;
@@ -50,7 +55,6 @@ describe('Job table app', () => {
provide: {
projectPath,
},
- localVue,
apolloProvider: createMockApolloProvider(handler),
});
};
@@ -96,35 +100,14 @@ describe('Job table app', () => {
describe('pagination', () => {
it('should disable the next page button on the last page', async () => {
createComponent({
- handler: successHandler,
+ handler: jest.fn().mockResolvedValue(mockJobsQueryResponseLastPage),
mountFn: mount,
data: {
- pagination: {
- currentPage: 3,
- },
- jobs: {
- pageInfo: {
- hasPreviousPage: true,
- startCursor: 'abc',
- endCursor: 'bcd',
- },
- },
+ pagination: { currentPage: 3 },
},
});
- await wrapper.vm.$nextTick();
-
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- jobs: {
- pageInfo: {
- hasNextPage: false,
- },
- },
- });
-
- await wrapper.vm.$nextTick();
+ await waitForPromises();
expect(findPrevious().exists()).toBe(true);
expect(findNext().exists()).toBe(true);
@@ -133,24 +116,16 @@ describe('Job table app', () => {
it('should disable the previous page button on the first page', async () => {
createComponent({
- handler: successHandler,
+ handler: jest.fn().mockResolvedValue(mockJobsQueryResponseFirstPage),
mountFn: mount,
data: {
pagination: {
currentPage: 1,
},
- jobs: {
- pageInfo: {
- hasNextPage: true,
- hasPreviousPage: false,
- startCursor: 'abc',
- endCursor: 'bcd',
- },
- },
},
});
- await wrapper.vm.$nextTick();
+ await waitForPromises();
expect(findPrevious().exists()).toBe(true);
expect(findPrevious().classes('disabled')).toBe(true);
diff --git a/spec/frontend/jobs/mixins/delayed_job_mixin_spec.js b/spec/frontend/jobs/mixins/delayed_job_mixin_spec.js
index 63dcd72f967..1d3845b19bb 100644
--- a/spec/frontend/jobs/mixins/delayed_job_mixin_spec.js
+++ b/spec/frontend/jobs/mixins/delayed_job_mixin_spec.js
@@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import delayedJobFixture from 'test_fixtures/jobs/delayed.json';
import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin';
@@ -34,7 +35,7 @@ describe('DelayedJobMixin', () => {
});
it('does not update remaining time after mounting', async () => {
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.text()).toBe('00:00:00');
});
@@ -57,7 +58,7 @@ describe('DelayedJobMixin', () => {
},
});
- await wrapper.vm.$nextTick();
+ await nextTick();
});
it('sets remaining time', () => {
@@ -68,7 +69,7 @@ describe('DelayedJobMixin', () => {
remainingTimeInMilliseconds = 41000;
jest.advanceTimersByTime(1000);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.text()).toBe('00:00:41');
});
});
@@ -104,7 +105,7 @@ describe('DelayedJobMixin', () => {
},
});
- await wrapper.vm.$nextTick();
+ await nextTick();
});
it('sets remaining time', () => {
@@ -115,7 +116,7 @@ describe('DelayedJobMixin', () => {
remainingTimeInMilliseconds = 41000;
jest.advanceTimersByTime(1000);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.text()).toBe('00:00:41');
});
});
diff --git a/spec/frontend/jobs/mock_data.js b/spec/frontend/jobs/mock_data.js
index 45d297ba364..2be78bac8a9 100644
--- a/spec/frontend/jobs/mock_data.js
+++ b/spec/frontend/jobs/mock_data.js
@@ -1579,6 +1579,44 @@ export const mockJobsQueryResponse = {
},
};
+export const mockJobsQueryResponseLastPage = {
+ data: {
+ project: {
+ id: '1',
+ jobs: {
+ ...mockJobsQueryResponse.data.project.jobs,
+ pageInfo: {
+ endCursor: 'eyJpZCI6IjIzMTcifQ',
+ hasNextPage: false,
+ hasPreviousPage: true,
+ startCursor: 'eyJpZCI6IjIzMzYifQ',
+ __typename: 'PageInfo',
+ },
+ },
+ __typename: 'Project',
+ },
+ },
+};
+
+export const mockJobsQueryResponseFirstPage = {
+ data: {
+ project: {
+ id: '1',
+ jobs: {
+ ...mockJobsQueryResponse.data.project.jobs,
+ pageInfo: {
+ endCursor: 'eyJpZCI6IjIzMTcifQ',
+ hasNextPage: true,
+ hasPreviousPage: false,
+ startCursor: 'eyJpZCI6IjIzMzYifQ',
+ __typename: 'PageInfo',
+ },
+ },
+ __typename: 'Project',
+ },
+ },
+};
+
export const mockJobsQueryEmptyResponse = {
data: {
project: {
@@ -1653,6 +1691,65 @@ export const retryableJob = {
__typename: 'CiJob',
};
+export const cancelableJob = {
+ artifacts: {
+ nodes: [],
+ __typename: 'CiJobArtifactConnection',
+ },
+ allowFailure: false,
+ status: 'PENDING',
+ scheduledAt: null,
+ manualJob: false,
+ triggered: null,
+ createdByTag: false,
+ detailedStatus: {
+ id: 'pending-1305-1305',
+ detailsPath: '/root/lots-of-jobs-project/-/jobs/1305',
+ group: 'pending',
+ icon: 'status_pending',
+ label: 'pending',
+ text: 'pending',
+ tooltip: 'pending',
+ action: {
+ id: 'Ci::Build-pending-1305',
+ buttonTitle: 'Cancel this job',
+ icon: 'cancel',
+ method: 'post',
+ path: '/root/lots-of-jobs-project/-/jobs/1305/cancel',
+ title: 'Cancel',
+ __typename: 'StatusAction',
+ },
+ __typename: 'DetailedStatus',
+ },
+ id: 'gid://gitlab/Ci::Build/1305',
+ refName: 'main',
+ refPath: '/root/lots-of-jobs-project/-/commits/main',
+ tags: [],
+ shortSha: '750605f2',
+ commitPath: '/root/lots-of-jobs-project/-/commit/750605f29530778cf0912779eba6d073128962a5',
+ stage: {
+ id: 'gid://gitlab/Ci::Stage/181',
+ name: 'deploy',
+ __typename: 'CiStage',
+ },
+ name: 'job_212',
+ duration: null,
+ finishedAt: null,
+ coverage: null,
+ retryable: false,
+ playable: false,
+ cancelable: true,
+ active: true,
+ stuck: false,
+ userPermissions: {
+ readBuild: true,
+ readJobArtifacts: true,
+ updateBuild: true,
+ __typename: 'JobPermissions',
+ },
+ __typename: 'CiJob',
+};
+
export const cannotRetryJob = {
...retryableJob,
userPermissions: { readBuild: true, updateBuild: false, __typename: 'JobPermissions' },
diff --git a/spec/frontend/lib/apollo/suppress_network_errors_during_navigation_link_spec.js b/spec/frontend/lib/apollo/suppress_network_errors_during_navigation_link_spec.js
index 7b604724977..971ba8b583c 100644
--- a/spec/frontend/lib/apollo/suppress_network_errors_during_navigation_link_spec.js
+++ b/spec/frontend/lib/apollo/suppress_network_errors_during_navigation_link_spec.js
@@ -1,4 +1,4 @@
-import { ApolloLink, Observable } from 'apollo-link';
+import { ApolloLink, Observable } from '@apollo/client/core';
import waitForPromises from 'helpers/wait_for_promises';
import { getSuppressNetworkErrorsDuringNavigationLink } from '~/lib/apollo/suppress_network_errors_during_navigation_link';
import { isNavigatingAway } from '~/lib/utils/is_navigating_away';
diff --git a/spec/frontend/lib/utils/apollo_startup_js_link_spec.js b/spec/frontend/lib/utils/apollo_startup_js_link_spec.js
index c0e5b06651f..e58bc063004 100644
--- a/spec/frontend/lib/utils/apollo_startup_js_link_spec.js
+++ b/spec/frontend/lib/utils/apollo_startup_js_link_spec.js
@@ -1,4 +1,4 @@
-import { ApolloLink, Observable } from 'apollo-link';
+import { ApolloLink, Observable } from '@apollo/client/core';
import { StartupJSLink } from '~/lib/utils/apollo_startup_js_link';
describe('StartupJSLink', () => {
diff --git a/spec/frontend/lib/utils/common_utils_spec.js b/spec/frontend/lib/utils/common_utils_spec.js
index 3e2ba918d9b..3fea08d5512 100644
--- a/spec/frontend/lib/utils/common_utils_spec.js
+++ b/spec/frontend/lib/utils/common_utils_spec.js
@@ -394,8 +394,7 @@ describe('common_utils', () => {
describe('backOff', () => {
beforeEach(() => {
- // shortcut our timeouts otherwise these tests will take a long time to finish
- jest.spyOn(window, 'setTimeout').mockImplementation((cb) => setImmediate(cb, 0));
+ jest.spyOn(window, 'setTimeout');
});
it('solves the promise from the callback', (done) => {
@@ -446,6 +445,7 @@ describe('common_utils', () => {
if (numberOfCalls < 3) {
numberOfCalls += 1;
next();
+ jest.runOnlyPendingTimers();
} else {
stop(resp);
}
@@ -464,7 +464,10 @@ describe('common_utils', () => {
it('rejects the backOff promise after timing out', (done) => {
commonUtils
- .backOff((next) => next(), 64000)
+ .backOff((next) => {
+ next();
+ jest.runOnlyPendingTimers();
+ }, 64000)
.catch((errBackoffResp) => {
const timeouts = window.setTimeout.mock.calls.map(([, timeout]) => timeout);
diff --git a/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js b/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js
index d19f9352bbc..e06d1384610 100644
--- a/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js
+++ b/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js
@@ -6,11 +6,13 @@ describe('Confirm Modal', () => {
let wrapper;
let modal;
- const createComponent = ({ primaryText, primaryVariant } = {}) => {
+ const createComponent = ({ primaryText, primaryVariant, title, hideCancel = false } = {}) => {
wrapper = mount(ConfirmModal, {
propsData: {
primaryText,
primaryVariant,
+ hideCancel,
+ title,
},
});
};
@@ -55,5 +57,19 @@ describe('Confirm Modal', () => {
expect(customProps.text).toBe('OK');
expect(customProps.attributes.variant).toBe('confirm');
});
+
+ it('should hide the cancel button if `hideCancel` is set', () => {
+ createComponent({ hideCancel: true });
+ const props = findGlModal().props();
+
+ expect(props.actionCancel).toBeNull();
+ });
+
+ it('should set the modal title when the `title` prop is set', () => {
+ const title = 'Modal title';
+ createComponent({ title });
+
+ expect(findGlModal().props().title).toBe(title);
+ });
});
});
diff --git a/spec/frontend/lib/utils/number_utility_spec.js b/spec/frontend/lib/utils/number_utility_spec.js
index e743678ea90..dc4aa0ea5ed 100644
--- a/spec/frontend/lib/utils/number_utility_spec.js
+++ b/spec/frontend/lib/utils/number_utility_spec.js
@@ -4,6 +4,7 @@ import {
bytesToMiB,
bytesToGiB,
numberToHumanSize,
+ numberToMetricPrefix,
sum,
isOdd,
median,
@@ -99,6 +100,21 @@ describe('Number Utils', () => {
});
});
+ describe('numberToMetricPrefix', () => {
+ it.each`
+ number | expected
+ ${123} | ${'123'}
+ ${1234} | ${'1.2k'}
+ ${12345} | ${'12.3k'}
+ ${123456} | ${'123.5k'}
+ ${1234567} | ${'1.2m'}
+ ${12345678} | ${'12.3m'}
+ ${123456789} | ${'123.5m'}
+ `('returns $expected given $number', ({ number, expected }) => {
+ expect(numberToMetricPrefix(number)).toBe(expected);
+ });
+ });
+
describe('sum', () => {
it('should add up two values', () => {
expect(sum(1, 2)).toEqual(3);
diff --git a/spec/frontend/lib/utils/table_utility_spec.js b/spec/frontend/lib/utils/table_utility_spec.js
index 75b9252aa40..0ceccbe4c74 100644
--- a/spec/frontend/lib/utils/table_utility_spec.js
+++ b/spec/frontend/lib/utils/table_utility_spec.js
@@ -8,4 +8,35 @@ describe('table_utility', () => {
expect(tableUtils.thWidthClass(width)).toBe(`gl-w-${width}p ${DEFAULT_TH_CLASSES}`);
});
});
+
+ describe('sortObjectToString', () => {
+ it('returns the expected sorting string ending in "DESC" when sortDesc is true', () => {
+ expect(tableUtils.sortObjectToString({ sortBy: 'mergedAt', sortDesc: true })).toBe(
+ 'MERGED_AT_DESC',
+ );
+ });
+
+ it('returns the expected sorting string ending in "ASC" when sortDesc is false', () => {
+ expect(tableUtils.sortObjectToString({ sortBy: 'mergedAt', sortDesc: false })).toBe(
+ 'MERGED_AT_ASC',
+ );
+ });
+ });
+
+ describe('sortStringToObject', () => {
+ it.each`
+ sortBy | sortDesc | sortString
+ ${'mergedAt'} | ${true} | ${'MERGED_AT_DESC'}
+ ${'mergedAt'} | ${false} | ${'MERGED_AT_ASC'}
+ ${'severity'} | ${true} | ${'SEVERITY_DESC'}
+ ${'severity'} | ${false} | ${'SEVERITY_ASC'}
+ ${null} | ${null} | ${'SEVERITY'}
+ ${null} | ${null} | ${''}
+ `(
+ 'returns the expected sort object when the sort string is "$sortString"',
+ ({ sortBy, sortDesc, sortString }) => {
+ expect(tableUtils.sortStringToObject(sortString)).toStrictEqual({ sortBy, sortDesc });
+ },
+ );
+ });
});
diff --git a/spec/frontend/lib/utils/text_markdown_spec.js b/spec/frontend/lib/utils/text_markdown_spec.js
index ab81ec47b64..dded32cc890 100644
--- a/spec/frontend/lib/utils/text_markdown_spec.js
+++ b/spec/frontend/lib/utils/text_markdown_spec.js
@@ -165,6 +165,80 @@ describe('init markdown', () => {
// cursor placement should be between tags
expect(textArea.selectionStart).toBe(start.length + tag.length);
});
+
+ describe('Continuing markdown lists', () => {
+ const enterEvent = new KeyboardEvent('keydown', { key: 'Enter' });
+
+ beforeEach(() => {
+ gon.features = { markdownContinueLists: true };
+ });
+
+ it.each`
+ text | expected
+ ${'- item'} | ${'- item\n- '}
+ ${'- [ ] item'} | ${'- [ ] item\n- [ ] '}
+ ${'- [x] item'} | ${'- [x] item\n- [x] '}
+ ${'- item\n - second'} | ${'- item\n - second\n - '}
+ ${'1. item'} | ${'1. item\n1. '}
+ ${'1. [ ] item'} | ${'1. [ ] item\n1. [ ] '}
+ ${'1. [x] item'} | ${'1. [x] item\n1. [x] '}
+ ${'108. item'} | ${'108. item\n108. '}
+ ${'108. item\n - second'} | ${'108. item\n - second\n - '}
+ ${'108. item\n 1. second'} | ${'108. item\n 1. second\n 1. '}
+ `('adds correct list continuation characters', ({ text, expected }) => {
+ textArea.value = text;
+ textArea.setSelectionRange(text.length, text.length);
+
+ textArea.addEventListener('keydown', keypressNoteText);
+ textArea.dispatchEvent(enterEvent);
+
+ expect(textArea.value).toEqual(expected);
+ expect(textArea.selectionStart).toBe(expected.length);
+ });
+
+ // test that when pressing Enter on an empty list item, the empty
+ // list item text is selected, so that when the Enter propagates,
+ // it's removed
+ it.each`
+ text | expected
+ ${'- item\n- '} | ${'- item\n'}
+ ${'- [ ] item\n- [ ] '} | ${'- [ ] item\n'}
+ ${'- [x] item\n- [x] '} | ${'- [x] item\n'}
+ ${'- item\n - second\n - '} | ${'- item\n - second\n'}
+ ${'1. item\n1. '} | ${'1. item\n'}
+ ${'1. [ ] item\n1. [ ] '} | ${'1. [ ] item\n'}
+ ${'1. [x] item\n1. [x] '} | ${'1. [x] item\n'}
+ ${'108. item\n108. '} | ${'108. item\n'}
+ ${'108. item\n - second\n - '} | ${'108. item\n - second\n'}
+ ${'108. item\n 1. second\n 1. '} | ${'108. item\n 1. second\n'}
+ `('adds correct list continuation characters', ({ text, expected }) => {
+ textArea.value = text;
+ textArea.setSelectionRange(text.length, text.length);
+
+ textArea.addEventListener('keydown', keypressNoteText);
+ textArea.dispatchEvent(enterEvent);
+
+ expect(textArea.value.substr(0, textArea.selectionStart)).toEqual(expected);
+ expect(textArea.selectionStart).toBe(expected.length);
+ expect(textArea.selectionEnd).toBe(text.length);
+ });
+
+ it('does nothing if feature flag disabled', () => {
+ gon.features = { markdownContinueLists: false };
+
+ const text = '- item';
+ const expected = '- item';
+
+ textArea.value = text;
+ textArea.setSelectionRange(text.length, text.length);
+
+ textArea.addEventListener('keydown', keypressNoteText);
+ textArea.dispatchEvent(enterEvent);
+
+ expect(textArea.value).toEqual(expected);
+ expect(textArea.selectionStart).toBe(expected.length);
+ });
+ });
});
describe('with selection', () => {
diff --git a/spec/frontend/lib/utils/vuex_module_mappers_spec.js b/spec/frontend/lib/utils/vuex_module_mappers_spec.js
index d7e51e4daca..1821a15f677 100644
--- a/spec/frontend/lib/utils/vuex_module_mappers_spec.js
+++ b/spec/frontend/lib/utils/vuex_module_mappers_spec.js
@@ -1,4 +1,4 @@
-import { mount, createLocalVue } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import {
@@ -10,13 +10,12 @@ import {
const TEST_MODULE_NAME = 'testModuleName';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
// setup test component and store ----------------------------------------------
//
// These are used to indirectly test `vuex_module_mappers`.
-const TestComponent = Vue.extend({
+const TestComponent = {
props: {
vuexModule: {
type: String,
@@ -47,7 +46,7 @@ const TestComponent = Vue.extend({
<pre data-testid="state">{{ stateJson }}</pre>
<pre data-testid="getters">{{ gettersJson }}</pre>
</div>`,
-});
+};
const createTestStore = () => {
return new Vuex.Store({
@@ -94,7 +93,6 @@ describe('~/lib/utils/vuex_module_mappers', () => {
vuexModule: TEST_MODULE_NAME,
},
store,
- localVue,
});
});
diff --git a/spec/frontend/lib/utils/yaml_spec.js b/spec/frontend/lib/utils/yaml_spec.js
new file mode 100644
index 00000000000..d1ce00130e2
--- /dev/null
+++ b/spec/frontend/lib/utils/yaml_spec.js
@@ -0,0 +1,105 @@
+import { Document, parseDocument } from 'yaml';
+import { merge } from '~/lib/utils/yaml';
+
+// Mock data for Comments on pairs
+const COMMENTS_ON_PAIRS_SOURCE = `foo:
+ # barbaz
+ bar: baz
+
+ # bazboo
+ baz: boo
+`;
+
+const COMMENTS_ON_PAIRS_TARGET = `foo:
+ # abcdef
+ abc: def
+ # boobaz
+ boo: baz
+`;
+
+const COMMENTS_ON_PAIRS_EXPECTED = `foo:
+ # abcdef
+ abc: def
+ # boobaz
+ boo: baz
+ # barbaz
+ bar: baz
+
+ # bazboo
+ baz: boo
+`;
+
+// Mock data for Comments on seqs
+const COMMENTS_ON_SEQS_SOURCE = `foo:
+ # barbaz
+ - barbaz
+ # bazboo
+ - baz: boo
+`;
+
+const COMMENTS_ON_SEQS_TARGET = `foo:
+ # abcdef
+ - abcdef
+
+ # boobaz
+ - boobaz
+`;
+
+const COMMENTS_ON_SEQS_EXPECTED = `foo:
+ # abcdef
+ - abcdef
+
+ # boobaz
+ - boobaz
+ # barbaz
+ - barbaz
+ # bazboo
+ - baz: boo
+`;
+
+describe('Yaml utility functions', () => {
+ describe('merge', () => {
+ const getAsNode = (yamlStr) => {
+ return parseDocument(yamlStr).contents;
+ };
+
+ describe('Merge two Nodes', () => {
+ it.each`
+ scenario | source | target | options | expected
+ ${'merge a map'} | ${getAsNode('foo:\n bar: baz\n')} | ${'foo:\n abc: def\n'} | ${undefined} | ${'foo:\n abc: def\n bar: baz\n'}
+ ${'merge a seq'} | ${getAsNode('foo:\n - bar\n')} | ${'foo:\n - abc\n'} | ${undefined} | ${'foo:\n - bar\n'}
+ ${'merge-append seqs'} | ${getAsNode('foo:\n - bar\n')} | ${'foo:\n - abc\n'} | ${{ onSequence: 'append' }} | ${'foo:\n - abc\n - bar\n'}
+ ${'merge-replace a seq'} | ${getAsNode('foo:\n - bar\n')} | ${'foo:\n - abc\n'} | ${{ onSequence: 'replace' }} | ${'foo:\n - bar\n'}
+ ${'override existing paths'} | ${getAsNode('foo:\n bar: baz\n')} | ${'foo:\n bar: boo\n'} | ${undefined} | ${'foo:\n bar: baz\n'}
+ ${'deep maps'} | ${getAsNode('foo:\n bar:\n abc: def\n')} | ${'foo:\n bar:\n baz: boo\n jkl: mno\n'} | ${undefined} | ${'foo:\n bar:\n baz: boo\n abc: def\n jkl: mno\n'}
+ ${'append maps inside seqs'} | ${getAsNode('foo:\n - abc: def\n')} | ${'foo:\n - bar: baz\n'} | ${{ onSequence: 'append' }} | ${'foo:\n - bar: baz\n - abc: def\n'}
+ ${'inexistent paths create new nodes'} | ${getAsNode('foo:\n bar: baz\n')} | ${'abc: def\n'} | ${undefined} | ${'abc: def\nfoo:\n bar: baz\n'}
+ ${'document as source'} | ${parseDocument('foo:\n bar: baz\n')} | ${'foo:\n abc: def\n'} | ${undefined} | ${'foo:\n abc: def\n bar: baz\n'}
+ ${'object as source'} | ${{ foo: { bar: 'baz' } }} | ${'foo:\n abc: def\n'} | ${undefined} | ${'foo:\n abc: def\n bar: baz\n'}
+ ${'comments on pairs'} | ${parseDocument(COMMENTS_ON_PAIRS_SOURCE)} | ${COMMENTS_ON_PAIRS_TARGET} | ${undefined} | ${COMMENTS_ON_PAIRS_EXPECTED}
+ ${'comments on seqs'} | ${parseDocument(COMMENTS_ON_SEQS_SOURCE)} | ${COMMENTS_ON_SEQS_TARGET} | ${{ onSequence: 'append' }} | ${COMMENTS_ON_SEQS_EXPECTED}
+ `('$scenario', ({ source, target, expected, options }) => {
+ const targetDoc = parseDocument(target);
+ merge(targetDoc, source, options);
+ const expectedDoc = parseDocument(expected);
+ expect(targetDoc.toString()).toEqual(expectedDoc.toString());
+ });
+
+ it('type conflict will throw an Error', () => {
+ const sourceDoc = parseDocument('foo:\n bar:\n - baz\n');
+ const targetDoc = parseDocument('foo:\n bar: def\n');
+ expect(() => merge(targetDoc, sourceDoc)).toThrow(
+ 'Type conflict at "foo.bar": Destination node is of type Scalar, the node' +
+ ' to be merged is of type YAMLSeq',
+ );
+ });
+
+ it('merging a collection into an empty doc', () => {
+ const targetDoc = new Document();
+ merge(targetDoc, { foo: { bar: 'baz' } });
+ const expected = parseDocument('foo:\n bar: baz\n');
+ expect(targetDoc.toString()).toEqual(expected.toString());
+ });
+ });
+ });
+});
diff --git a/spec/frontend/listbox/index_spec.js b/spec/frontend/listbox/index_spec.js
new file mode 100644
index 00000000000..45659a0e523
--- /dev/null
+++ b/spec/frontend/listbox/index_spec.js
@@ -0,0 +1,111 @@
+import { nextTick } from 'vue';
+import { getAllByRole, getByRole } from '@testing-library/dom';
+import { GlDropdown } from '@gitlab/ui';
+import { createWrapper } from '@vue/test-utils';
+import { initListbox, parseAttributes } from '~/listbox';
+import { getFixture, setHTMLFixture } from 'helpers/fixtures';
+
+jest.mock('~/lib/utils/url_utility');
+
+const fixture = getFixture('listbox/redirect_listbox.html');
+
+const parsedAttributes = (() => {
+ const div = document.createElement('div');
+ div.innerHTML = fixture;
+ return parseAttributes(div.firstChild);
+})();
+
+describe('initListbox', () => {
+ let instance;
+
+ afterEach(() => {
+ if (instance) {
+ instance.$destroy();
+ }
+ });
+
+ const setup = (...args) => {
+ instance = initListbox(...args);
+ };
+
+ // TODO: Rewrite these finders to use better semantics once the
+ // implementation is switched to GlListbox
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/348738
+ const findToggleButton = () => document.body.querySelector('.gl-dropdown-toggle');
+ const findItem = (text) => getByRole(document.body, 'menuitem', { name: text });
+ const findItems = () => getAllByRole(document.body, 'menuitem');
+ const findSelectedItems = () =>
+ findItems().filter(
+ (menuitem) =>
+ !menuitem
+ .querySelector('.gl-new-dropdown-item-check-icon')
+ .classList.contains('gl-visibility-hidden'),
+ );
+
+ it('returns null given no element', () => {
+ setup();
+
+ expect(instance).toBe(null);
+ });
+
+ it('throws given an invalid element', () => {
+ expect(() => setup(document.body)).toThrow();
+ });
+
+ describe('given a valid element', () => {
+ let onChangeSpy;
+
+ beforeEach(async () => {
+ setHTMLFixture(fixture);
+ onChangeSpy = jest.fn();
+ setup(document.querySelector('.js-redirect-listbox'), { onChange: onChangeSpy });
+
+ await nextTick();
+ });
+
+ it('returns an instance', () => {
+ expect(instance).not.toBe(null);
+ });
+
+ it('renders button with selected item text', () => {
+ expect(findToggleButton().textContent.trim()).toBe('Bar');
+ });
+
+ it('has the correct item selected', () => {
+ const selectedItems = findSelectedItems();
+ expect(selectedItems).toHaveLength(1);
+ expect(selectedItems[0].textContent.trim()).toBe('Bar');
+ });
+
+ it('applies additional classes from the original element', () => {
+ expect(instance.$el.classList).toContain('test-class-1', 'test-class-2');
+ });
+
+ describe.each(parsedAttributes.items)('clicking on an item', (item) => {
+ beforeEach(async () => {
+ findItem(item.text).click();
+
+ await nextTick();
+ });
+
+ it('calls the onChange callback with the item', () => {
+ expect(onChangeSpy).toHaveBeenCalledWith(item);
+ });
+
+ it('updates the toggle button text', () => {
+ expect(findToggleButton().textContent.trim()).toBe(item.text);
+ });
+
+ it('marks the item as selected', () => {
+ const selectedItems = findSelectedItems();
+ expect(selectedItems).toHaveLength(1);
+ expect(selectedItems[0].textContent.trim()).toBe(item.text);
+ });
+ });
+
+ it('passes the "right" prop through to the underlying component', () => {
+ const wrapper = createWrapper(instance).findComponent(GlDropdown);
+ expect(wrapper.props('right')).toBe(parsedAttributes.right);
+ });
+ });
+});
diff --git a/spec/frontend/listbox/redirect_behavior_spec.js b/spec/frontend/listbox/redirect_behavior_spec.js
new file mode 100644
index 00000000000..7b2a40b65ce
--- /dev/null
+++ b/spec/frontend/listbox/redirect_behavior_spec.js
@@ -0,0 +1,51 @@
+import { initListbox } from '~/listbox';
+import { initRedirectListboxBehavior } from '~/listbox/redirect_behavior';
+import { redirectTo } from '~/lib/utils/url_utility';
+import { getFixture, setHTMLFixture } from 'helpers/fixtures';
+
+jest.mock('~/lib/utils/url_utility');
+jest.mock('~/listbox', () => ({
+ initListbox: jest.fn().mockReturnValue({ foo: true }),
+}));
+
+const fixture = getFixture('listbox/redirect_listbox.html');
+
+describe('initRedirectListboxBehavior', () => {
+ let instances;
+
+ beforeEach(() => {
+ setHTMLFixture(`
+ ${fixture}
+ ${fixture}
+ `);
+
+ instances = initRedirectListboxBehavior();
+ });
+
+ it('calls initListbox for each .js-redirect-listbox', () => {
+ expect(instances).toEqual([{ foo: true }, { foo: true }]);
+
+ expect(initListbox).toHaveBeenCalledTimes(2);
+
+ initListbox.mock.calls.forEach((callArgs, i) => {
+ const elements = document.querySelectorAll('.js-redirect-listbox');
+
+ expect(callArgs[0]).toBe(elements[i]);
+ expect(callArgs[1]).toEqual({
+ onChange: expect.any(Function),
+ });
+ });
+ });
+
+ it('passes onChange handler to initListbox that calls redirectTo', () => {
+ const [firstCallArgs] = initListbox.mock.calls;
+ const { onChange } = firstCallArgs[1];
+ const mockItem = { href: '/foo' };
+
+ expect(redirectTo).not.toHaveBeenCalled();
+
+ onChange(mockItem);
+
+ expect(redirectTo).toHaveBeenCalledWith(mockItem.href);
+ });
+});
diff --git a/spec/frontend/logs/components/environment_logs_spec.js b/spec/frontend/logs/components/environment_logs_spec.js
index b107708ac2c..84dc0bdf6cd 100644
--- a/spec/frontend/logs/components/environment_logs_spec.js
+++ b/spec/frontend/logs/components/environment_logs_spec.js
@@ -1,5 +1,6 @@
import { GlSprintf, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import { scrollDown } from '~/lib/utils/scroll_utils';
import EnvironmentLogs from '~/logs/components/environment_logs.vue';
@@ -338,35 +339,32 @@ describe('EnvironmentLogs', () => {
expect(store.dispatch).not.toHaveBeenCalledWith(`${module}/fetchMoreLogsPrepend`, undefined);
});
- it('`scroll` on a scrollable target results in enabled scroll buttons', () => {
+ it('`scroll` on a scrollable target results in enabled scroll buttons', async () => {
const target = { scrollTop: 10, clientHeight: 10, scrollHeight: 21 };
state.logs.isLoading = true;
findInfiniteScroll().vm.$emit('scroll', { target });
- return wrapper.vm.$nextTick(() => {
- expect(findLogControlButtons().props('scrollDownButtonDisabled')).toEqual(false);
- });
+ await nextTick();
+ expect(findLogControlButtons().props('scrollDownButtonDisabled')).toEqual(false);
});
- it('`scroll` on a non-scrollable target in disabled scroll buttons', () => {
+ it('`scroll` on a non-scrollable target in disabled scroll buttons', async () => {
const target = { scrollTop: 10, clientHeight: 10, scrollHeight: 20 };
state.logs.isLoading = true;
findInfiniteScroll().vm.$emit('scroll', { target });
- return wrapper.vm.$nextTick(() => {
- expect(findLogControlButtons().props('scrollDownButtonDisabled')).toEqual(true);
- });
+ await nextTick();
+ expect(findLogControlButtons().props('scrollDownButtonDisabled')).toEqual(true);
});
- it('`scroll` on no target results in disabled scroll buttons', () => {
+ it('`scroll` on no target results in disabled scroll buttons', async () => {
state.logs.isLoading = true;
findInfiniteScroll().vm.$emit('scroll', { target: undefined });
- return wrapper.vm.$nextTick(() => {
- expect(findLogControlButtons().props('scrollDownButtonDisabled')).toEqual(true);
- });
+ await nextTick();
+ expect(findLogControlButtons().props('scrollDownButtonDisabled')).toEqual(true);
});
});
});
diff --git a/spec/frontend/logs/components/log_control_buttons_spec.js b/spec/frontend/logs/components/log_control_buttons_spec.js
index 9c1617e4daa..e249272b87d 100644
--- a/spec/frontend/logs/components/log_control_buttons_spec.js
+++ b/spec/frontend/logs/components/log_control_buttons_spec.js
@@ -1,5 +1,6 @@
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import LogControlButtons from '~/logs/components/log_control_buttons.vue';
describe('LogControlButtons', () => {
@@ -33,7 +34,7 @@ describe('LogControlButtons', () => {
expect(findRefreshBtn().is(GlButton)).toBe(true);
});
- it('emits a `refresh` event on click on `refresh` button', () => {
+ it('emits a `refresh` event on click on `refresh` button', async () => {
initWrapper();
// An `undefined` value means no event was emitted
@@ -41,16 +42,15 @@ describe('LogControlButtons', () => {
findRefreshBtn().vm.$emit('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted('refresh')).toHaveLength(1);
- });
+ await nextTick();
+ expect(wrapper.emitted('refresh')).toHaveLength(1);
});
describe('when scrolling actions are enabled', () => {
- beforeEach(() => {
+ beforeEach(async () => {
// mock scrolled to the middle of a long page
initWrapper();
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('click on "scroll to top" scrolls up', () => {
@@ -71,19 +71,18 @@ describe('LogControlButtons', () => {
});
describe('when scrolling actions are disabled', () => {
- beforeEach(() => {
+ beforeEach(async () => {
initWrapper({ listeners: {} });
- return wrapper.vm.$nextTick();
+ await nextTick();
});
- it('buttons are disabled', () => {
- return wrapper.vm.$nextTick(() => {
- expect(findScrollToTop().exists()).toBe(false);
- expect(findScrollToBottom().exists()).toBe(false);
- // This should be enabled when gitlab-ui contains:
- // https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/1149
- // expect(findScrollToBottom().is('[disabled]')).toBe(true);
- });
+ it('buttons are disabled', async () => {
+ await nextTick();
+ expect(findScrollToTop().exists()).toBe(false);
+ expect(findScrollToBottom().exists()).toBe(false);
+ // This should be enabled when gitlab-ui contains:
+ // https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/1149
+ // expect(findScrollToBottom().is('[disabled]')).toBe(true);
});
});
});
diff --git a/spec/frontend/members/components/action_buttons/approve_access_request_button_spec.js b/spec/frontend/members/components/action_buttons/approve_access_request_button_spec.js
index 936715e7723..08d7cf3c932 100644
--- a/spec/frontend/members/components/action_buttons/approve_access_request_button_spec.js
+++ b/spec/frontend/members/components/action_buttons/approve_access_request_button_spec.js
@@ -1,5 +1,6 @@
import { GlButton, GlForm } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import ApproveAccessRequestButton from '~/members/components/action_buttons/approve_access_request_button.vue';
@@ -7,8 +8,7 @@ import { MEMBER_TYPES } from '~/members/constants';
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('ApproveAccessRequestButton', () => {
let wrapper;
@@ -29,7 +29,6 @@ describe('ApproveAccessRequestButton', () => {
const createComponent = (propsData = {}, state) => {
wrapper = shallowMount(ApproveAccessRequestButton, {
- localVue,
store: createStore(state),
provide: {
namespace: MEMBER_TYPES.accessRequest,
diff --git a/spec/frontend/members/components/action_buttons/remove_group_link_button_spec.js b/spec/frontend/members/components/action_buttons/remove_group_link_button_spec.js
index f91aef131a1..ca655e36c42 100644
--- a/spec/frontend/members/components/action_buttons/remove_group_link_button_spec.js
+++ b/spec/frontend/members/components/action_buttons/remove_group_link_button_spec.js
@@ -1,13 +1,13 @@
import { GlButton } from '@gitlab/ui';
-import { mount, createLocalVue } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import RemoveGroupLinkButton from '~/members/components/action_buttons/remove_group_link_button.vue';
import { MEMBER_TYPES } from '~/members/constants';
import { group } from '../../mock_data';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('RemoveGroupLinkButton', () => {
let wrapper;
@@ -29,7 +29,6 @@ describe('RemoveGroupLinkButton', () => {
const createComponent = () => {
wrapper = mount(RemoveGroupLinkButton, {
- localVue,
store: createStore(),
provide: {
namespace: MEMBER_TYPES.group,
diff --git a/spec/frontend/members/components/action_buttons/remove_member_button_spec.js b/spec/frontend/members/components/action_buttons/remove_member_button_spec.js
index 1a031cc56d6..0e5b667eb9b 100644
--- a/spec/frontend/members/components/action_buttons/remove_member_button_spec.js
+++ b/spec/frontend/members/components/action_buttons/remove_member_button_spec.js
@@ -1,13 +1,13 @@
import { GlButton } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { modalData } from 'jest/members/mock_data';
import RemoveMemberButton from '~/members/components/action_buttons/remove_member_button.vue';
import { MEMBER_TYPES } from '~/members/constants';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('RemoveMemberButton', () => {
let wrapper;
@@ -33,7 +33,6 @@ describe('RemoveMemberButton', () => {
const createComponent = (propsData = {}, state) => {
wrapper = shallowMount(RemoveMemberButton, {
- localVue,
store: createStore(state),
provide: {
namespace: MEMBER_TYPES.user,
diff --git a/spec/frontend/members/components/action_buttons/resend_invite_button_spec.js b/spec/frontend/members/components/action_buttons/resend_invite_button_spec.js
index 547e067450c..8e933d16463 100644
--- a/spec/frontend/members/components/action_buttons/resend_invite_button_spec.js
+++ b/spec/frontend/members/components/action_buttons/resend_invite_button_spec.js
@@ -1,5 +1,6 @@
import { GlButton } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import ResendInviteButton from '~/members/components/action_buttons/resend_invite_button.vue';
@@ -7,8 +8,7 @@ import { MEMBER_TYPES } from '~/members/constants';
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('ResendInviteButton', () => {
let wrapper;
@@ -29,7 +29,6 @@ describe('ResendInviteButton', () => {
const createComponent = (propsData = {}, state) => {
wrapper = shallowMount(ResendInviteButton, {
- localVue,
store: createStore(state),
provide: {
namespace: MEMBER_TYPES.invite,
diff --git a/spec/frontend/members/components/app_spec.js b/spec/frontend/members/components/app_spec.js
index 9590cd9d8d4..4124a1870a6 100644
--- a/spec/frontend/members/components/app_spec.js
+++ b/spec/frontend/members/components/app_spec.js
@@ -1,6 +1,6 @@
import { GlAlert } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import * as commonUtils from '~/lib/utils/common_utils';
import MembersApp from '~/members/components/app.vue';
@@ -11,8 +11,7 @@ import { RECEIVE_MEMBER_ROLE_ERROR, HIDE_ERROR } from '~/members/store/mutation_
import mutations from '~/members/store/mutations';
describe('MembersApp', () => {
- const localVue = createLocalVue();
- localVue.use(Vuex);
+ Vue.use(Vuex);
let wrapper;
let store;
@@ -33,7 +32,6 @@ describe('MembersApp', () => {
});
wrapper = shallowMount(MembersApp, {
- localVue,
propsData: {
namespace: MEMBER_TYPES.group,
tabQueryParamValue: TAB_QUERY_PARAM_VALUES.group,
diff --git a/spec/frontend/members/components/avatars/user_avatar_spec.js b/spec/frontend/members/components/avatars/user_avatar_spec.js
index 5cf3a4cdc13..7bcf4a11413 100644
--- a/spec/frontend/members/components/avatars/user_avatar_spec.js
+++ b/spec/frontend/members/components/avatars/user_avatar_spec.js
@@ -1,7 +1,8 @@
import { GlAvatarLink, GlBadge } from '@gitlab/ui';
-import { within } from '@testing-library/dom';
-import { mount, createWrapper } from '@vue/test-utils';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import UserAvatar from '~/members/components/avatars/user_avatar.vue';
+import { AVAILABILITY_STATUS } from '~/set_status_modal/utils';
+
import { member as memberMock, member2faEnabled, orphanedMember } from '../../mock_data';
describe('UserAvatar', () => {
@@ -10,7 +11,7 @@ describe('UserAvatar', () => {
const { user } = memberMock;
const createComponent = (propsData = {}, provide = {}) => {
- wrapper = mount(UserAvatar, {
+ wrapper = mountExtended(UserAvatar, {
propsData: {
member: memberMock,
isCurrentUser: false,
@@ -23,9 +24,6 @@ describe('UserAvatar', () => {
});
};
- const getByText = (text, options) =>
- createWrapper(within(wrapper.element).findByText(text, options));
-
const findStatusEmoji = (emoji) => wrapper.find(`gl-emoji[data-name="${emoji}"]`);
afterEach(() => {
@@ -48,13 +46,13 @@ describe('UserAvatar', () => {
it("renders user's name", () => {
createComponent();
- expect(getByText(user.name).exists()).toBe(true);
+ expect(wrapper.findByText(user.name).exists()).toBe(true);
});
it("renders user's username", () => {
createComponent();
- expect(getByText(`@${user.username}`).exists()).toBe(true);
+ expect(wrapper.findByText(`@${user.username}`).exists()).toBe(true);
});
it("renders user's avatar", () => {
@@ -67,7 +65,7 @@ describe('UserAvatar', () => {
it('displays an orphaned user', () => {
createComponent({ member: orphanedMember });
- expect(getByText('Orphaned member').exists()).toBe(true);
+ expect(wrapper.findByText('Orphaned member').exists()).toBe(true);
});
});
@@ -85,13 +83,13 @@ describe('UserAvatar', () => {
it('renders the "It\'s you" badge when member is current user', () => {
createComponent({ isCurrentUser: true });
- expect(getByText("It's you").exists()).toBe(true);
+ expect(wrapper.findByText("It's you").exists()).toBe(true);
});
it('does not render 2FA badge when `canManageMembers` is `false`', () => {
createComponent({ member: member2faEnabled }, { canManageMembers: false });
- expect(within(wrapper.element).queryByText('2FA')).toBe(null);
+ expect(wrapper.findByText('2FA').exists()).toBe(false);
});
});
@@ -112,6 +110,23 @@ describe('UserAvatar', () => {
expect(findStatusEmoji(emoji).exists()).toBe(true);
});
+
+ describe('when `user.showStatus` is `false', () => {
+ it('does not display status emoji', () => {
+ createComponent({
+ member: {
+ ...memberMock,
+ user: {
+ ...memberMock.user,
+ showStatus: false,
+ status: { emoji, messageHtml: 'On vacation' },
+ },
+ },
+ });
+
+ expect(findStatusEmoji(emoji).exists()).toBe(false);
+ });
+ });
});
describe('when not set', () => {
@@ -122,4 +137,30 @@ describe('UserAvatar', () => {
});
});
});
+
+ describe('user availability', () => {
+ describe('when `user.availability` is `null`', () => {
+ it("does not show `(Busy)` next to user's name", () => {
+ createComponent();
+
+ expect(wrapper.findByText('(Busy)').exists()).toBe(false);
+ });
+ });
+
+ describe(`when user.availability is ${AVAILABILITY_STATUS.BUSY}`, () => {
+ it("shows `(Busy)` next to user's name", () => {
+ createComponent({
+ member: {
+ ...memberMock,
+ user: {
+ ...memberMock.user,
+ availability: AVAILABILITY_STATUS.BUSY,
+ },
+ },
+ });
+
+ expect(wrapper.findByText('(Busy)').exists()).toBe(true);
+ });
+ });
+ });
});
diff --git a/spec/frontend/members/components/filter_sort/filter_sort_container_spec.js b/spec/frontend/members/components/filter_sort/filter_sort_container_spec.js
index 16ac52737bc..4ca8a3bdc36 100644
--- a/spec/frontend/members/components/filter_sort/filter_sort_container_spec.js
+++ b/spec/frontend/members/components/filter_sort/filter_sort_container_spec.js
@@ -1,12 +1,12 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import FilterSortContainer from '~/members/components/filter_sort/filter_sort_container.vue';
import MembersFilteredSearchBar from '~/members/components/filter_sort/members_filtered_search_bar.vue';
import SortDropdown from '~/members/components/filter_sort/sort_dropdown.vue';
import { MEMBER_TYPES } from '~/members/constants';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('FilterSortContainer', () => {
let wrapper;
@@ -32,7 +32,6 @@ describe('FilterSortContainer', () => {
});
wrapper = shallowMount(FilterSortContainer, {
- localVue,
store,
provide: {
namespace: MEMBER_TYPES.user,
diff --git a/spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js b/spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js
index 3f47fa024bc..ee2fbbe57b9 100644
--- a/spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js
+++ b/spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js
@@ -1,5 +1,6 @@
import { GlFilteredSearchToken } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import setWindowLocation from 'helpers/set_window_location_helper';
import { redirectTo } from '~/lib/utils/url_utility';
@@ -18,8 +19,7 @@ jest.mock('~/lib/utils/url_utility', () => {
};
});
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('MembersFilteredSearchBar', () => {
let wrapper;
@@ -44,7 +44,6 @@ describe('MembersFilteredSearchBar', () => {
});
wrapper = shallowMount(MembersFilteredSearchBar, {
- localVue,
provide: {
sourceId: 1,
canManageMembers: true,
diff --git a/spec/frontend/members/components/filter_sort/sort_dropdown_spec.js b/spec/frontend/members/components/filter_sort/sort_dropdown_spec.js
index d0684acd487..709ad907a38 100644
--- a/spec/frontend/members/components/filter_sort/sort_dropdown_spec.js
+++ b/spec/frontend/members/components/filter_sort/sort_dropdown_spec.js
@@ -1,13 +1,13 @@
import { GlSorting, GlSortingItem } from '@gitlab/ui';
-import { mount, createLocalVue } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import setWindowLocation from 'helpers/set_window_location_helper';
import * as urlUtilities from '~/lib/utils/url_utility';
import SortDropdown from '~/members/components/filter_sort/sort_dropdown.vue';
import { MEMBER_TYPES } from '~/members/constants';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('SortDropdown', () => {
let wrapper;
@@ -35,7 +35,6 @@ describe('SortDropdown', () => {
});
wrapper = mount(SortDropdown, {
- localVue,
provide: {
sourceId: 1,
namespace: MEMBER_TYPES.user,
diff --git a/spec/frontend/members/components/modals/leave_modal_spec.js b/spec/frontend/members/components/modals/leave_modal_spec.js
index f755f08dbf2..cdbabb2f646 100644
--- a/spec/frontend/members/components/modals/leave_modal_spec.js
+++ b/spec/frontend/members/components/modals/leave_modal_spec.js
@@ -1,8 +1,8 @@
import { GlModal, GlForm } from '@gitlab/ui';
import { within } from '@testing-library/dom';
-import { mount, createLocalVue, createWrapper } from '@vue/test-utils';
+import { mount, createWrapper } from '@vue/test-utils';
import { cloneDeep } from 'lodash';
-import { nextTick } from 'vue';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import LeaveModal from '~/members/components/modals/leave_modal.vue';
import { LEAVE_MODAL_ID, MEMBER_TYPES } from '~/members/constants';
@@ -12,8 +12,7 @@ import { member } from '../../mock_data';
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('LeaveModal', () => {
let wrapper;
@@ -34,7 +33,6 @@ describe('LeaveModal', () => {
const createComponent = (propsData = {}, state) => {
wrapper = mount(LeaveModal, {
- localVue,
store: createStore(state),
provide: {
namespace: MEMBER_TYPES.user,
diff --git a/spec/frontend/members/components/modals/remove_group_link_modal_spec.js b/spec/frontend/members/components/modals/remove_group_link_modal_spec.js
index 313c237f51c..447496910b8 100644
--- a/spec/frontend/members/components/modals/remove_group_link_modal_spec.js
+++ b/spec/frontend/members/components/modals/remove_group_link_modal_spec.js
@@ -1,7 +1,7 @@
import { GlModal, GlForm } from '@gitlab/ui';
import { within } from '@testing-library/dom';
-import { mount, createLocalVue, createWrapper } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import { mount, createWrapper } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import RemoveGroupLinkModal from '~/members/components/modals/remove_group_link_modal.vue';
import { REMOVE_GROUP_LINK_MODAL_ID, MEMBER_TYPES } from '~/members/constants';
@@ -9,8 +9,7 @@ import { group } from '../../mock_data';
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('RemoveGroupLinkModal', () => {
let wrapper;
@@ -38,7 +37,6 @@ describe('RemoveGroupLinkModal', () => {
const createComponent = (state) => {
wrapper = mount(RemoveGroupLinkModal, {
- localVue,
store: createStore(state),
provide: {
namespace: MEMBER_TYPES.group,
diff --git a/spec/frontend/members/components/table/expiration_datepicker_spec.js b/spec/frontend/members/components/table/expiration_datepicker_spec.js
index 3c4a9ba37ff..4fb43fbd888 100644
--- a/spec/frontend/members/components/table/expiration_datepicker_spec.js
+++ b/spec/frontend/members/components/table/expiration_datepicker_spec.js
@@ -1,6 +1,6 @@
import { GlDatepicker } from '@gitlab/ui';
-import { mount, createLocalVue } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import { mount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { useFakeDate } from 'helpers/fake_date';
import waitForPromises from 'helpers/wait_for_promises';
@@ -8,8 +8,7 @@ import ExpirationDatepicker from '~/members/components/table/expiration_datepick
import { MEMBER_TYPES } from '~/members/constants';
import { member } from '../../mock_data';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('ExpirationDatepicker', () => {
// March 15th, 2020 3:00
@@ -49,7 +48,6 @@ describe('ExpirationDatepicker', () => {
provide: {
namespace: MEMBER_TYPES.user,
},
- localVue,
store: createStore(),
mocks: {
$toast,
diff --git a/spec/frontend/members/components/table/members_table_cell_spec.js b/spec/frontend/members/components/table/members_table_cell_spec.js
index 5375ee11736..6575a7c7126 100644
--- a/spec/frontend/members/components/table/members_table_cell_spec.js
+++ b/spec/frontend/members/components/table/members_table_cell_spec.js
@@ -1,4 +1,5 @@
-import { mount, createLocalVue } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import MembersTableCell from '~/members/components/table/members_table_cell.vue';
import { MEMBER_TYPES } from '~/members/constants';
@@ -36,9 +37,8 @@ describe('MembersTableCell', () => {
},
};
- const localVue = createLocalVue();
- localVue.use(Vuex);
- localVue.component('WrappedComponent', WrappedComponent);
+ Vue.use(Vuex);
+ Vue.component('WrappedComponent', WrappedComponent);
const createStore = (state = {}) => {
return new Vuex.Store({
@@ -50,7 +50,6 @@ describe('MembersTableCell', () => {
const createComponent = (propsData, state) => {
wrapper = mount(MembersTableCell, {
- localVue,
propsData,
store: createStore(state),
provide: {
diff --git a/spec/frontend/members/components/table/members_table_spec.js b/spec/frontend/members/components/table/members_table_spec.js
index 580e5edd652..b2756e506eb 100644
--- a/spec/frontend/members/components/table/members_table_spec.js
+++ b/spec/frontend/members/components/table/members_table_spec.js
@@ -1,5 +1,5 @@
import { GlBadge, GlPagination, GlTable } from '@gitlab/ui';
-import { createLocalVue } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import setWindowLocation from 'helpers/set_window_location_helper';
import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper';
@@ -16,6 +16,7 @@ import {
MEMBER_STATE_AWAITING,
MEMBER_STATE_ACTIVE,
USER_STATE_BLOCKED_PENDING_APPROVAL,
+ BADGE_LABELS_AWAITING_USER_SIGNUP,
BADGE_LABELS_PENDING_OWNER_APPROVAL,
TAB_QUERY_PARAM_VALUES,
} from '~/members/constants';
@@ -28,8 +29,7 @@ import {
pagination,
} from '../../mock_data';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('MembersTable', () => {
let wrapper;
@@ -56,7 +56,6 @@ describe('MembersTable', () => {
const createComponent = (state, provide = {}) => {
wrapper = mountExtended(MembersTable, {
- localVue,
propsData: {
tabQueryParamValue: TAB_QUERY_PARAM_VALUES.invite,
},
@@ -133,9 +132,9 @@ describe('MembersTable', () => {
describe('Invited column', () => {
describe.each`
state | userState | expectedBadgeLabel
- ${MEMBER_STATE_CREATED} | ${null} | ${''}
+ ${MEMBER_STATE_CREATED} | ${null} | ${BADGE_LABELS_AWAITING_USER_SIGNUP}
${MEMBER_STATE_CREATED} | ${USER_STATE_BLOCKED_PENDING_APPROVAL} | ${BADGE_LABELS_PENDING_OWNER_APPROVAL}
- ${MEMBER_STATE_AWAITING} | ${''} | ${''}
+ ${MEMBER_STATE_AWAITING} | ${''} | ${BADGE_LABELS_AWAITING_USER_SIGNUP}
${MEMBER_STATE_AWAITING} | ${USER_STATE_BLOCKED_PENDING_APPROVAL} | ${BADGE_LABELS_PENDING_OWNER_APPROVAL}
${MEMBER_STATE_AWAITING} | ${'something_else'} | ${BADGE_LABELS_PENDING_OWNER_APPROVAL}
${MEMBER_STATE_ACTIVE} | ${null} | ${''}
diff --git a/spec/frontend/members/components/table/role_dropdown_spec.js b/spec/frontend/members/components/table/role_dropdown_spec.js
index a4a4c620921..d4d950e99ba 100644
--- a/spec/frontend/members/components/table/role_dropdown_spec.js
+++ b/spec/frontend/members/components/table/role_dropdown_spec.js
@@ -1,8 +1,8 @@
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { within } from '@testing-library/dom';
-import { mount, createWrapper, createLocalVue } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import { mount, createWrapper } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import waitForPromises from 'helpers/wait_for_promises';
import { BV_DROPDOWN_SHOW } from '~/lib/utils/constants';
@@ -10,8 +10,7 @@ import RoleDropdown from '~/members/components/table/role_dropdown.vue';
import { MEMBER_TYPES } from '~/members/constants';
import { member } from '../../mock_data';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('RoleDropdown', () => {
let wrapper;
@@ -42,7 +41,6 @@ describe('RoleDropdown', () => {
permissions: {},
...propsData,
},
- localVue,
store: createStore(),
mocks: {
$toast,
diff --git a/spec/frontend/members/mock_data.js b/spec/frontend/members/mock_data.js
index 218db0b587a..83856a00a15 100644
--- a/spec/frontend/members/mock_data.js
+++ b/spec/frontend/members/mock_data.js
@@ -25,6 +25,8 @@ export const member = {
twoFactorEnabled: false,
oncallSchedules: [{ name: 'schedule 1' }],
escalationPolicies: [{ name: 'policy 1' }],
+ availability: null,
+ showStatus: true,
},
id: 238,
createdAt: '2020-07-17T16:22:46.923Z',
diff --git a/spec/frontend/merge_conflicts/components/merge_conflict_resolver_app_spec.js b/spec/frontend/merge_conflicts/components/merge_conflict_resolver_app_spec.js
index a09edb50f20..750fff9b0aa 100644
--- a/spec/frontend/merge_conflicts/components/merge_conflict_resolver_app_spec.js
+++ b/spec/frontend/merge_conflicts/components/merge_conflict_resolver_app_spec.js
@@ -1,5 +1,6 @@
import { GlSprintf } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import InlineConflictLines from '~/merge_conflicts/components/inline_conflict_lines.vue';
import ParallelConflictLines from '~/merge_conflicts/components/parallel_conflict_lines.vue';
@@ -8,8 +9,7 @@ import { createStore } from '~/merge_conflicts/store';
import { decorateFiles } from '~/merge_conflicts/utils';
import { conflictsMock } from '../mock_data';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('Merge Conflict Resolver App', () => {
let wrapper;
@@ -93,7 +93,7 @@ describe('Merge Conflict Resolver App', () => {
expect(inlineButton.props('selected')).toBe(false);
inlineButton.vm.$emit('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(inlineButton.props('selected')).toBe(true);
});
@@ -111,7 +111,7 @@ describe('Merge Conflict Resolver App', () => {
mountComponent();
findSideBySideButton().vm.$emit('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
const parallelConflictLinesComponent = findParallelConflictLines(findFiles().at(0));
diff --git a/spec/frontend/merge_conflicts/store/actions_spec.js b/spec/frontend/merge_conflicts/store/actions_spec.js
index 8fa8765a9f9..3e1774a6d56 100644
--- a/spec/frontend/merge_conflicts/store/actions_spec.js
+++ b/spec/frontend/merge_conflicts/store/actions_spec.js
@@ -207,7 +207,10 @@ describe('merge conflicts actions', () => {
],
[],
() => {
- expect(Cookies.set).toHaveBeenCalledWith('diff_view', payload);
+ expect(Cookies.set).toHaveBeenCalledWith('diff_view', payload, {
+ expires: 365,
+ secure: false,
+ });
done();
},
);
diff --git a/spec/frontend/merge_request_spec.js b/spec/frontend/merge_request_spec.js
index 0b7ed349507..9229b353685 100644
--- a/spec/frontend/merge_request_spec.js
+++ b/spec/frontend/merge_request_spec.js
@@ -1,6 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import { TEST_HOST } from 'spec/test_constants';
+import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import MergeRequest from '~/merge_request';
@@ -27,31 +28,31 @@ describe('MergeRequest', () => {
mock.restore();
});
- it('modifies the Markdown field', (done) => {
+ it('modifies the Markdown field', async () => {
jest.spyOn($, 'ajax').mockImplementation();
const changeEvent = document.createEvent('HTMLEvents');
changeEvent.initEvent('change', true, true);
$('input[type=checkbox]').first().attr('checked', true)[0].dispatchEvent(changeEvent);
- setImmediate(() => {
- expect($('.js-task-list-field').val()).toBe(
- '- [x] Task List Item\n- [ ]\n- [ ] Task List Item 2\n',
- );
- done();
- });
+
+ await waitForPromises();
+
+ expect($('.js-task-list-field').val()).toBe(
+ '- [x] Task List Item\n- [ ]\n- [ ] Task List Item 2\n',
+ );
});
- it('ensure that task with only spaces does not get checked incorrectly', (done) => {
+ it('ensure that task with only spaces does not get checked incorrectly', async () => {
// fixed in 'deckar01-task_list', '2.2.1' gem
jest.spyOn($, 'ajax').mockImplementation();
const changeEvent = document.createEvent('HTMLEvents');
changeEvent.initEvent('change', true, true);
$('input[type=checkbox]').last().attr('checked', true)[0].dispatchEvent(changeEvent);
- setImmediate(() => {
- expect($('.js-task-list-field').val()).toBe(
- '- [ ] Task List Item\n- [ ]\n- [x] Task List Item 2\n',
- );
- done();
- });
+
+ await waitForPromises();
+
+ expect($('.js-task-list-field').val()).toBe(
+ '- [ ] Task List Item\n- [ ]\n- [x] Task List Item 2\n',
+ );
});
describe('tasklist', () => {
@@ -60,29 +61,27 @@ describe('MergeRequest', () => {
const index = 3;
const checked = true;
- it('submits an ajax request on tasklist:changed', (done) => {
+ it('submits an ajax request on tasklist:changed', async () => {
$('.js-task-list-field').trigger({
type: 'tasklist:changed',
detail: { lineNumber, lineSource, index, checked },
});
- setImmediate(() => {
- expect(axios.patch).toHaveBeenCalledWith(
- `${TEST_HOST}/frontend-fixtures/merge-requests-project/-/merge_requests/1.json`,
- {
- merge_request: {
- description: '- [ ] Task List Item\n- [ ]\n- [ ] Task List Item 2\n',
- lock_version: 0,
- update_task: { line_number: lineNumber, line_source: lineSource, index, checked },
- },
- },
- );
+ await waitForPromises();
- done();
- });
+ expect(axios.patch).toHaveBeenCalledWith(
+ `${TEST_HOST}/frontend-fixtures/merge-requests-project/-/merge_requests/1.json`,
+ {
+ merge_request: {
+ description: '- [ ] Task List Item\n- [ ]\n- [ ] Task List Item 2\n',
+ lock_version: 0,
+ update_task: { line_number: lineNumber, line_source: lineSource, index, checked },
+ },
+ },
+ );
});
- it('shows an error notification when tasklist update failed', (done) => {
+ it('shows an error notification when tasklist update failed', async () => {
mock
.onPatch(`${TEST_HOST}/frontend-fixtures/merge-requests-project/-/merge_requests/1.json`)
.reply(409, {});
@@ -92,13 +91,11 @@ describe('MergeRequest', () => {
detail: { lineNumber, lineSource, index, checked },
});
- setImmediate(() => {
- expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
- 'Someone edited this merge request at the same time you did. Please refresh the page to see changes.',
- );
+ await waitForPromises();
- done();
- });
+ expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
+ 'Someone edited this merge request at the same time you did. Please refresh the page to see changes.',
+ );
});
});
});
diff --git a/spec/frontend/mocks_spec.js b/spec/frontend/mocks_spec.js
index 110c418e579..0813d2073b9 100644
--- a/spec/frontend/mocks_spec.js
+++ b/spec/frontend/mocks_spec.js
@@ -8,13 +8,10 @@ describe('Mock auto-injection', () => {
failMock = jest.spyOn(global, 'fail').mockImplementation();
});
- it('~/lib/utils/axios_utils', () => {
- return Promise.all([
- expect(axios.get('http://gitlab.com')).rejects.toThrow('Unexpected unmocked request'),
- setImmediate(() => {
- expect(failMock).toHaveBeenCalledTimes(1);
- }),
- ]);
+ it('~/lib/utils/axios_utils', async () => {
+ await expect(axios.get('http://gitlab.com')).rejects.toThrow('Unexpected unmocked request');
+
+ expect(failMock).toHaveBeenCalledTimes(1);
});
it('jQuery.ajax()', () => {
diff --git a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
index 681fb05a6c4..bd2e818df4f 100644
--- a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
+++ b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
@@ -8,6 +8,28 @@ exports[`Dashboard template matches the default snapshot 1`] = `
metricsdashboardbasepath="/monitoring/monitor-project/-/environments/1/metrics"
metricsendpoint="/monitoring/monitor-project/-/environments/1/additional_metrics.json"
>
+ <div>
+ <gl-alert-stub
+ class="mb-3"
+ dismissible="true"
+ dismisslabel="Dismiss"
+ primarybuttonlink=""
+ primarybuttontext=""
+ secondarybuttonlink=""
+ secondarybuttontext=""
+ title="Feature deprecation and removal"
+ variant="danger"
+ >
+ <gl-sprintf-stub
+ message="The metrics, logs and tracing features were deprecated in GitLab 14.7 and are %{epicStart} scheduled for removal %{epicEnd} in GitLab 15.0."
+ />
+
+ <gl-sprintf-stub
+ message="For information on a possible replacement %{epicStart} learn more about Opstrace %{epicEnd}."
+ />
+ </gl-alert-stub>
+ </div>
+
<div
class="prometheus-graphs-header d-sm-flex flex-sm-wrap pt-2 pr-1 pb-0 pl-2 border-bottom bg-gray-light"
>
diff --git a/spec/frontend/monitoring/components/charts/stacked_column_spec.js b/spec/frontend/monitoring/components/charts/stacked_column_spec.js
index f47728313c6..9cab3650f28 100644
--- a/spec/frontend/monitoring/components/charts/stacked_column_spec.js
+++ b/spec/frontend/monitoring/components/charts/stacked_column_spec.js
@@ -2,6 +2,7 @@ import { GlStackedColumnChart, GlChartLegend } from '@gitlab/ui/dist/charts';
import { shallowMount, mount } from '@vue/test-utils';
import { cloneDeep } from 'lodash';
import timezoneMock from 'timezone-mock';
+import { nextTick } from 'vue';
import StackedColumnChart from '~/monitoring/components/charts/stacked_column.vue';
import { stackedColumnGraphData } from '../../graph_data';
@@ -34,9 +35,9 @@ describe('Stacked column chart component', () => {
});
describe('when graphData is present', () => {
- beforeEach(() => {
+ beforeEach(async () => {
createWrapper();
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('chart is rendered', () => {
@@ -116,25 +117,24 @@ describe('Stacked column chart component', () => {
expect(xAxis.axisLabel.formatter('2020-01-30T12:01:00.000Z')).toBe('4:01 AM');
});
- it('date is shown in UTC', () => {
+ it('date is shown in UTC', async () => {
wrapper.setProps({ timezone: 'UTC' });
- return wrapper.vm.$nextTick().then(() => {
- const { xAxis } = findChart().props('option');
- expect(xAxis.axisLabel.formatter('2020-01-30T12:01:00.000Z')).toBe('12:01 PM');
- });
+ await nextTick();
+ const { xAxis } = findChart().props('option');
+ expect(xAxis.axisLabel.formatter('2020-01-30T12:01:00.000Z')).toBe('12:01 PM');
});
});
});
describe('when graphData has results missing', () => {
- beforeEach(() => {
+ beforeEach(async () => {
const graphData = cloneDeep(stackedColumnMockedData);
graphData.metrics[0].result = null;
createWrapper({ graphData });
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('chart is rendered', () => {
@@ -147,7 +147,7 @@ describe('Stacked column chart component', () => {
wrapper = createWrapper({}, mount);
});
- it('allows user to override legend label texts using props', () => {
+ it('allows user to override legend label texts using props', async () => {
const legendRelatedProps = {
legendMinText: 'legendMinText',
legendMaxText: 'legendMaxText',
@@ -158,9 +158,8 @@ describe('Stacked column chart component', () => {
...legendRelatedProps,
});
- return wrapper.vm.$nextTick().then(() => {
- expect(findChart().props()).toMatchObject(legendRelatedProps);
- });
+ await nextTick();
+ expect(findChart().props()).toMatchObject(legendRelatedProps);
});
it('should render a tabular legend layout by default', () => {
diff --git a/spec/frontend/monitoring/components/charts/time_series_spec.js b/spec/frontend/monitoring/components/charts/time_series_spec.js
index ff6f0b9b0c7..73abd81d889 100644
--- a/spec/frontend/monitoring/components/charts/time_series_spec.js
+++ b/spec/frontend/monitoring/components/charts/time_series_spec.js
@@ -7,6 +7,7 @@ import {
} from '@gitlab/ui/dist/charts';
import { mount, shallowMount } from '@vue/test-utils';
import timezoneMock from 'timezone-mock';
+import { nextTick } from 'vue';
import { TEST_HOST } from 'helpers/test_constants';
import { setTestTimeout } from 'helpers/timeout';
import { shallowWrapperContainsSlotText } from 'helpers/vue_test_utils_helper';
@@ -70,12 +71,12 @@ describe('Time series component', () => {
describe('general functions', () => {
const findChart = () => wrapper.find({ ref: 'chart' });
- beforeEach(() => {
+ beforeEach(async () => {
createWrapper({}, mount);
- return wrapper.vm.$nextTick();
+ await nextTick();
});
- it('allows user to override legend label texts using props', () => {
+ it('allows user to override legend label texts using props', async () => {
const legendRelatedProps = {
legendMinText: 'legendMinText',
legendMaxText: 'legendMaxText',
@@ -86,9 +87,8 @@ describe('Time series component', () => {
...legendRelatedProps,
});
- return wrapper.vm.$nextTick().then(() => {
- expect(findChart().props()).toMatchObject(legendRelatedProps);
- });
+ await nextTick();
+ expect(findChart().props()).toMatchObject(legendRelatedProps);
});
it('chart sets a default height', () => {
@@ -96,14 +96,13 @@ describe('Time series component', () => {
expect(wrapper.props('height')).toBe(chartHeight);
});
- it('chart has a configurable height', () => {
+ it('chart has a configurable height', async () => {
const mockHeight = 599;
createWrapper();
wrapper.setProps({ height: mockHeight });
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.props('height')).toBe(mockHeight);
- });
+ await nextTick();
+ expect(wrapper.props('height')).toBe(mockHeight);
});
describe('events', () => {
@@ -112,7 +111,7 @@ describe('Time series component', () => {
let startValue;
let endValue;
- beforeEach(() => {
+ beforeEach(async () => {
eChartMock = {
handlers: {},
getOption: () => ({
@@ -132,9 +131,8 @@ describe('Time series component', () => {
};
createWrapper({}, mount);
- return wrapper.vm.$nextTick(() => {
- findChart().vm.$emit('created', eChartMock);
- });
+ await nextTick();
+ findChart().vm.$emit('created', eChartMock);
});
it('handles datazoom event from chart', () => {
@@ -203,10 +201,10 @@ describe('Time series component', () => {
});
describe('when series is of line type', () => {
- beforeEach(() => {
+ beforeEach(async () => {
createWrapper({}, mount);
wrapper.vm.formatTooltipText(mockLineSeriesData());
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('formats tooltip title', () => {
@@ -241,34 +239,31 @@ describe('Time series component', () => {
timezoneMock.unregister();
});
- it('formats tooltip title in local timezone by default', () => {
+ it('formats tooltip title in local timezone by default', async () => {
createWrapper();
wrapper.vm.formatTooltipText(mockLineSeriesData());
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.vm.tooltip.title).toBe('16 Jul 2019, 3:14AM (GMT-0700)');
- });
+ await nextTick();
+ expect(wrapper.vm.tooltip.title).toBe('16 Jul 2019, 3:14AM (GMT-0700)');
});
- it('formats tooltip title in local timezone', () => {
+ it('formats tooltip title in local timezone', async () => {
createWrapper({ timezone: 'LOCAL' });
wrapper.vm.formatTooltipText(mockLineSeriesData());
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.vm.tooltip.title).toBe('16 Jul 2019, 3:14AM (GMT-0700)');
- });
+ await nextTick();
+ expect(wrapper.vm.tooltip.title).toBe('16 Jul 2019, 3:14AM (GMT-0700)');
});
- it('formats tooltip title in UTC format', () => {
+ it('formats tooltip title in UTC format', async () => {
createWrapper({ timezone: 'UTC' });
wrapper.vm.formatTooltipText(mockLineSeriesData());
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.vm.tooltip.title).toBe('16 Jul 2019, 10:14AM (UTC)');
- });
+ await nextTick();
+ expect(wrapper.vm.tooltip.title).toBe('16 Jul 2019, 10:14AM (UTC)');
});
});
});
describe('when series is of scatter type, for deployments', () => {
- beforeEach(() => {
+ beforeEach(async () => {
wrapper.vm.formatTooltipText({
...mockAnnotationsSeriesData,
seriesData: mockAnnotationsSeriesData.seriesData.map((data) => ({
@@ -276,7 +271,7 @@ describe('Time series component', () => {
data: annotationsMetadata,
})),
});
- return wrapper.vm.$nextTick;
+ await nextTick();
});
it('set tooltip type to deployments', () => {
@@ -297,9 +292,9 @@ describe('Time series component', () => {
});
describe('when series is of scatter type and deployments data is missing', () => {
- beforeEach(() => {
+ beforeEach(async () => {
wrapper.vm.formatTooltipText(mockAnnotationsSeriesData);
- return wrapper.vm.$nextTick;
+ await nextTick();
});
it('formats tooltip title', () => {
@@ -397,14 +392,13 @@ describe('Time series component', () => {
});
});
- it('is not set if time range is not set or incorrectly set', () => {
+ it('is not set if time range is not set or incorrectly set', async () => {
wrapper.setProps({
timeRange: {},
});
- return wrapper.vm.$nextTick(() => {
- expect(getChartOptions().xAxis).not.toHaveProperty('min');
- expect(getChartOptions().xAxis).not.toHaveProperty('max');
- });
+ await nextTick();
+ expect(getChartOptions().xAxis).not.toHaveProperty('min');
+ expect(getChartOptions().xAxis).not.toHaveProperty('max');
});
});
@@ -430,17 +424,16 @@ describe('Time series component', () => {
option2: 'option2',
};
- it('arbitrary options', () => {
+ it('arbitrary options', async () => {
wrapper.setProps({
option: mockOption,
});
- return wrapper.vm.$nextTick().then(() => {
- expect(getChartOptions()).toEqual(expect.objectContaining(mockOption));
- });
+ await nextTick();
+ expect(getChartOptions()).toEqual(expect.objectContaining(mockOption));
});
- it('additional series', () => {
+ it('additional series', async () => {
wrapper.setProps({
option: {
series: [
@@ -453,15 +446,14 @@ describe('Time series component', () => {
},
});
- return wrapper.vm.$nextTick().then(() => {
- const optionSeries = getChartOptions().series;
+ await nextTick();
+ const optionSeries = getChartOptions().series;
- expect(optionSeries.length).toEqual(2);
- expect(optionSeries[0].name).toEqual(mockSeriesName);
- });
+ expect(optionSeries.length).toEqual(2);
+ expect(optionSeries[0].name).toEqual(mockSeriesName);
});
- it('additional y-axis data', () => {
+ it('additional y-axis data', async () => {
const mockCustomYAxisOption = {
name: 'Custom y-axis label',
axisLabel: {
@@ -475,14 +467,13 @@ describe('Time series component', () => {
},
});
- return wrapper.vm.$nextTick().then(() => {
- const { yAxis } = getChartOptions();
+ await nextTick();
+ const { yAxis } = getChartOptions();
- expect(yAxis[0]).toMatchObject(mockCustomYAxisOption);
- });
+ expect(yAxis[0]).toMatchObject(mockCustomYAxisOption);
});
- it('additional x axis data', () => {
+ it('additional x axis data', async () => {
const mockCustomXAxisOption = {
name: 'Custom x axis label',
};
@@ -493,11 +484,10 @@ describe('Time series component', () => {
},
});
- return wrapper.vm.$nextTick().then(() => {
- const { xAxis } = getChartOptions();
+ await nextTick();
+ const { xAxis } = getChartOptions();
- expect(xAxis).toMatchObject(mockCustomXAxisOption);
- });
+ expect(xAxis).toMatchObject(mockCustomXAxisOption);
});
});
@@ -625,12 +615,12 @@ describe('Time series component', () => {
describe(`GitLab UI: ${dynamicComponent.chartType}`, () => {
const findChartComponent = () => wrapper.find(dynamicComponent.component);
- beforeEach(() => {
+ beforeEach(async () => {
createWrapper(
{ graphData: timeSeriesGraphData({ type: dynamicComponent.chartType }) },
mount,
);
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('exists', () => {
@@ -645,22 +635,21 @@ describe('Time series component', () => {
expect(props.formatTooltipText).toBe(wrapper.vm.formatTooltipText);
});
- it('receives a tooltip title', () => {
+ it('receives a tooltip title', async () => {
const mockTitle = 'mockTitle';
wrapper.vm.tooltip.title = mockTitle;
- return wrapper.vm.$nextTick(() => {
- expect(
- shallowWrapperContainsSlotText(findChartComponent(), 'tooltip-title', mockTitle),
- ).toBe(true);
- });
+ await nextTick();
+ expect(
+ shallowWrapperContainsSlotText(findChartComponent(), 'tooltip-title', mockTitle),
+ ).toBe(true);
});
describe('when tooltip is showing deployment data', () => {
const mockSha = 'mockSha';
const commitUrl = `${mockProjectDir}/-/commit/${mockSha}`;
- beforeEach(() => {
+ beforeEach(async () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({
@@ -668,7 +657,7 @@ describe('Time series component', () => {
type: 'deployments',
},
});
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('uses deployment title', () => {
@@ -677,16 +666,15 @@ describe('Time series component', () => {
).toBe(true);
});
- it('renders clickable commit sha in tooltip content', () => {
+ it('renders clickable commit sha in tooltip content', async () => {
wrapper.vm.tooltip.sha = mockSha;
wrapper.vm.tooltip.commitUrl = commitUrl;
- return wrapper.vm.$nextTick(() => {
- const commitLink = wrapper.find(GlLink);
+ await nextTick();
+ const commitLink = wrapper.find(GlLink);
- expect(shallowWrapperContainsSlotText(commitLink, 'default', mockSha)).toBe(true);
- expect(commitLink.attributes('href')).toEqual(commitUrl);
- });
+ expect(shallowWrapperContainsSlotText(commitLink, 'default', mockSha)).toBe(true);
+ expect(commitLink.attributes('href')).toEqual(commitUrl);
});
});
});
@@ -696,11 +684,11 @@ describe('Time series component', () => {
describe('with multiple time series', () => {
describe('General functions', () => {
- beforeEach(() => {
+ beforeEach(async () => {
const graphData = timeSeriesGraphData({ type: panelTypes.AREA_CHART, multiMetric: true });
createWrapper({ graphData }, mount);
- return wrapper.vm.$nextTick();
+ await nextTick();
});
describe('Color match', () => {
@@ -742,9 +730,9 @@ describe('Time series component', () => {
describe('legend layout', () => {
const findLegend = () => wrapper.find(GlChartLegend);
- beforeEach(() => {
+ beforeEach(async () => {
createWrapper({}, mount);
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('should render a tabular legend layout by default', () => {
diff --git a/spec/frontend/monitoring/components/create_dashboard_modal_spec.js b/spec/frontend/monitoring/components/create_dashboard_modal_spec.js
index 8202d423ff3..88de3467580 100644
--- a/spec/frontend/monitoring/components/create_dashboard_modal_spec.js
+++ b/spec/frontend/monitoring/components/create_dashboard_modal_spec.js
@@ -1,5 +1,6 @@
import { GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import CreateDashboardModal from '~/monitoring/components/create_dashboard_modal.vue';
describe('Create dashboard modal', () => {
@@ -32,13 +33,12 @@ describe('Create dashboard modal', () => {
wrapper.destroy();
});
- it('has button that links to the project url', () => {
+ it('has button that links to the project url', async () => {
findRepoButton().trigger('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(findRepoButton().exists()).toBe(true);
- expect(findRepoButton().attributes('href')).toBe(defaultProps.projectPath);
- });
+ await nextTick();
+ expect(findRepoButton().exists()).toBe(true);
+ expect(findRepoButton().attributes('href')).toBe(defaultProps.projectPath);
});
it('has button that links to the docs', () => {
diff --git a/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js b/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js
index f2116c1f478..d0d0c3071d5 100644
--- a/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js
@@ -1,5 +1,6 @@
import { GlDropdownItem, GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import CustomMetricsFormFields from '~/custom_metrics/components/custom_metrics_form_fields.vue';
import { redirectTo } from '~/lib/utils/url_utility';
import ActionsMenu from '~/monitoring/components/dashboard_actions_menu.vue';
@@ -60,22 +61,20 @@ describe('Actions menu', () => {
});
describe('add metric item', () => {
- it('is rendered when custom metrics are available', () => {
+ it('is rendered when custom metrics are available', async () => {
createShallowWrapper();
- return wrapper.vm.$nextTick(() => {
- expect(findAddMetricItem().exists()).toBe(true);
- });
+ await nextTick();
+ expect(findAddMetricItem().exists()).toBe(true);
});
- it('is not rendered when custom metrics are not available', () => {
+ it('is not rendered when custom metrics are not available', async () => {
createShallowWrapper({
addingMetricsAvailable: false,
});
- return wrapper.vm.$nextTick(() => {
- expect(findAddMetricItem().exists()).toBe(false);
- });
+ await nextTick();
+ expect(findAddMetricItem().exists()).toBe(false);
});
describe('when available', () => {
@@ -119,30 +118,23 @@ describe('Actions menu', () => {
origPage = document.body.dataset.page;
document.body.dataset.page = 'projects:environments:metrics';
- wrapper.vm.$nextTick(done);
+ nextTick(done);
});
afterEach(() => {
document.body.dataset.page = origPage;
});
- it('is tracked', (done) => {
+ it('is tracked', async () => {
const submitButton = findAddMetricModalSubmitButton().vm;
- wrapper.vm.$nextTick(() => {
- submitButton.$el.click();
- wrapper.vm.$nextTick(() => {
- expect(Tracking.event).toHaveBeenCalledWith(
- document.body.dataset.page,
- 'click_button',
- {
- label: 'add_new_metric',
- property: 'modal',
- value: undefined,
- },
- );
- done();
- });
+ await nextTick();
+ submitButton.$el.click();
+ await nextTick();
+ expect(Tracking.event).toHaveBeenCalledWith(document.body.dataset.page, 'click_button', {
+ label: 'add_new_metric',
+ property: 'modal',
+ value: undefined,
});
});
});
@@ -172,14 +164,13 @@ describe('Actions menu', () => {
);
});
- it('is disabled for ootb dashboards', () => {
+ it('is disabled for ootb dashboards', async () => {
createShallowWrapper({
isOotbDashboard: true,
});
- return wrapper.vm.$nextTick(() => {
- expect(findAddPanelItemDisabled().exists()).toBe(true);
- });
+ await nextTick();
+ expect(findAddPanelItemDisabled().exists()).toBe(true);
});
it('is visible for custom dashboards', () => {
@@ -256,16 +247,15 @@ describe('Actions menu', () => {
expect(findDuplicateDashboardModal().exists()).toBe(true);
});
- it('clicking on item opens up the duplicate dashboard modal', () => {
+ it('clicking on item opens up the duplicate dashboard modal', async () => {
const modalId = 'duplicateDashboard';
const modalTrigger = findDuplicateDashboardItem();
const rootEmit = jest.spyOn(wrapper.vm.$root, '$emit');
modalTrigger.trigger('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(rootEmit.mock.calls[0]).toContainEqual(modalId);
- });
+ await nextTick();
+ expect(rootEmit.mock.calls[0]).toContainEqual(modalId);
});
});
@@ -300,16 +290,15 @@ describe('Actions menu', () => {
setupAllDashboards(store, dashboardGitResponse[0].path);
});
- it('redirects to the newly created dashboard', () => {
+ it('redirects to the newly created dashboard', async () => {
const newDashboard = dashboardGitResponse[1];
const newDashboardUrl = 'root/sandbox/-/metrics/dashboard.yml';
findDuplicateDashboardModal().vm.$emit('dashboardDuplicated', newDashboard);
- return wrapper.vm.$nextTick().then(() => {
- expect(redirectTo).toHaveBeenCalled();
- expect(redirectTo).toHaveBeenCalledWith(newDashboardUrl);
- });
+ await nextTick();
+ expect(redirectTo).toHaveBeenCalled();
+ expect(redirectTo).toHaveBeenCalledWith(newDashboardUrl);
});
});
});
@@ -330,32 +319,30 @@ describe('Actions menu', () => {
expect(findStarDashboardItem().attributes('disabled')).toBeFalsy();
});
- it('is disabled when starring is taking place', () => {
+ it('is disabled when starring is taking place', async () => {
store.commit(`monitoringDashboard/${types.REQUEST_DASHBOARD_STARRING}`);
- return wrapper.vm.$nextTick(() => {
- expect(findStarDashboardItem().exists()).toBe(true);
- expect(findStarDashboardItem().attributes('disabled')).toBe('true');
- });
+ await nextTick();
+ expect(findStarDashboardItem().exists()).toBe(true);
+ expect(findStarDashboardItem().attributes('disabled')).toBe('true');
});
- it('on click it dispatches a toggle star action', () => {
+ it('on click it dispatches a toggle star action', async () => {
findStarDashboardItem().vm.$emit('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(store.dispatch).toHaveBeenCalledWith(
- 'monitoringDashboard/toggleStarredValue',
- undefined,
- );
- });
+ await nextTick();
+ expect(store.dispatch).toHaveBeenCalledWith(
+ 'monitoringDashboard/toggleStarredValue',
+ undefined,
+ );
});
describe('when dashboard is not starred', () => {
- beforeEach(() => {
+ beforeEach(async () => {
store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
currentDashboard: dashboardGitResponse[0].path,
});
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('item text shows "Star dashboard"', () => {
@@ -364,11 +351,11 @@ describe('Actions menu', () => {
});
describe('when dashboard is starred', () => {
- beforeEach(() => {
+ beforeEach(async () => {
store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
currentDashboard: dashboardGitResponse[1].path,
});
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('item text shows "Unstar dashboard"', () => {
@@ -403,16 +390,15 @@ describe('Actions menu', () => {
expect(findCreateDashboardModal().exists()).toBe(true);
});
- it('clicking opens up the modal', () => {
+ it('clicking opens up the modal', async () => {
const modalId = 'createDashboard';
const modalTrigger = findCreateDashboardItem();
const rootEmit = jest.spyOn(wrapper.vm.$root, '$emit');
modalTrigger.trigger('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(rootEmit.mock.calls[0]).toContainEqual(modalId);
- });
+ await nextTick();
+ expect(rootEmit.mock.calls[0]).toContainEqual(modalId);
});
it('modal gets passed correct props', () => {
diff --git a/spec/frontend/monitoring/components/dashboard_header_spec.js b/spec/frontend/monitoring/components/dashboard_header_spec.js
index 8be7d641953..e28c2913949 100644
--- a/spec/frontend/monitoring/components/dashboard_header_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_header_spec.js
@@ -1,5 +1,6 @@
import { GlDropdownItem, GlSearchBoxByType, GlLoadingIcon, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import { redirectTo } from '~/lib/utils/url_utility';
import ActionsMenu from '~/monitoring/components/dashboard_actions_menu.vue';
import DashboardHeader from '~/monitoring/components/dashboard_header.vue';
@@ -110,9 +111,9 @@ describe('Dashboard header', () => {
});
describe('when environments data is not loaded', () => {
- beforeEach(() => {
+ beforeEach(async () => {
setupStoreWithDashboard(store);
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('there are no environments listed', () => {
@@ -124,13 +125,13 @@ describe('Dashboard header', () => {
const currentDashboard = dashboardGitResponse[0].path;
const currentEnvironmentName = environmentData[0].name;
- beforeEach(() => {
+ beforeEach(async () => {
setupStoreWithData(store);
store.state.monitoringDashboard.projectPath = mockProjectPath;
store.state.monitoringDashboard.currentDashboard = currentDashboard;
store.state.monitoringDashboard.currentEnvironmentName = currentEnvironmentName;
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('renders dropdown items with the environment name', () => {
@@ -159,51 +160,41 @@ describe('Dashboard header', () => {
expect(selectedItems.at(0).text()).toBe(currentEnvironmentName);
});
- it('filters rendered dropdown items', () => {
+ it('filters rendered dropdown items', async () => {
const searchTerm = 'production';
const resultEnvs = environmentData.filter(({ name }) => name.indexOf(searchTerm) !== -1);
setSearchTerm(searchTerm);
- return wrapper.vm.$nextTick().then(() => {
- expect(findEnvsDropdownItems()).toHaveLength(resultEnvs.length);
- });
+ await nextTick();
+ expect(findEnvsDropdownItems()).toHaveLength(resultEnvs.length);
});
- it('does not filter dropdown items if search term is empty string', () => {
+ it('does not filter dropdown items if search term is empty string', async () => {
const searchTerm = '';
setSearchTerm(searchTerm);
- return wrapper.vm.$nextTick(() => {
- expect(findEnvsDropdownItems()).toHaveLength(environmentData.length);
- });
+ await nextTick();
+ expect(findEnvsDropdownItems()).toHaveLength(environmentData.length);
});
- it("shows error message if search term doesn't match", () => {
+ it("shows error message if search term doesn't match", async () => {
const searchTerm = 'does-not-exist';
setSearchTerm(searchTerm);
- return wrapper.vm.$nextTick(() => {
- expect(findEnvsDropdownSearchMsg().isVisible()).toBe(true);
- });
+ await nextTick();
+ expect(findEnvsDropdownSearchMsg().isVisible()).toBe(true);
});
- it('shows loading element when environments fetch is still loading', () => {
+ it('shows loading element when environments fetch is still loading', async () => {
store.commit(`monitoringDashboard/${types.REQUEST_ENVIRONMENTS_DATA}`);
- return wrapper.vm
- .$nextTick()
- .then(() => {
- expect(findEnvsDropdownLoadingIcon().exists()).toBe(true);
- })
- .then(() => {
- store.commit(
- `monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`,
- environmentData,
- );
- })
- .then(() => {
- expect(findEnvsDropdownLoadingIcon().exists()).toBe(false);
- });
+ await nextTick();
+ expect(findEnvsDropdownLoadingIcon().exists()).toBe(true);
+ await store.commit(
+ `monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`,
+ environmentData,
+ );
+ expect(findEnvsDropdownLoadingIcon().exists()).toBe(false);
});
});
});
@@ -262,11 +253,11 @@ describe('Dashboard header', () => {
});
describe('external dashboard link', () => {
- beforeEach(() => {
+ beforeEach(async () => {
store.state.monitoringDashboard.externalDashboardUrl = '/mockUrl';
createShallowWrapper();
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('shows the link', () => {
@@ -295,82 +286,78 @@ describe('Dashboard header', () => {
});
describe('adding metrics prop', () => {
- it.each(ootbDashboards)('gets passed true if current dashboard is OOTB', (dashboardPath) => {
- createShallowWrapper({ customMetricsAvailable: true });
+ it.each(ootbDashboards)(
+ 'gets passed true if current dashboard is OOTB',
+ async (dashboardPath) => {
+ createShallowWrapper({ customMetricsAvailable: true });
- store.state.monitoringDashboard.emptyState = false;
- setupAllDashboards(store, dashboardPath);
+ store.state.monitoringDashboard.emptyState = false;
+ setupAllDashboards(store, dashboardPath);
- return wrapper.vm.$nextTick().then(() => {
+ await nextTick();
expect(findActionsMenu().props('addingMetricsAvailable')).toBe(true);
- });
- });
+ },
+ );
it.each(customDashboards)(
'gets passed false if current dashboard is custom',
- (dashboardPath) => {
+ async (dashboardPath) => {
createShallowWrapper({ customMetricsAvailable: true });
store.state.monitoringDashboard.emptyState = false;
setupAllDashboards(store, dashboardPath);
- return wrapper.vm.$nextTick().then(() => {
- expect(findActionsMenu().props('addingMetricsAvailable')).toBe(false);
- });
+ await nextTick();
+ expect(findActionsMenu().props('addingMetricsAvailable')).toBe(false);
},
);
- it('gets passed false if empty state is shown', () => {
+ it('gets passed false if empty state is shown', async () => {
createShallowWrapper({ customMetricsAvailable: true });
store.state.monitoringDashboard.emptyState = true;
setupAllDashboards(store, ootbDashboards[0]);
- return wrapper.vm.$nextTick().then(() => {
- expect(findActionsMenu().props('addingMetricsAvailable')).toBe(false);
- });
+ await nextTick();
+ expect(findActionsMenu().props('addingMetricsAvailable')).toBe(false);
});
- it('gets passed false if custom metrics are not available', () => {
+ it('gets passed false if custom metrics are not available', async () => {
createShallowWrapper({ customMetricsAvailable: false });
store.state.monitoringDashboard.emptyState = false;
setupAllDashboards(store, ootbDashboards[0]);
- return wrapper.vm.$nextTick().then(() => {
- expect(findActionsMenu().props('addingMetricsAvailable')).toBe(false);
- });
+ await nextTick();
+ expect(findActionsMenu().props('addingMetricsAvailable')).toBe(false);
});
});
- it('custom metrics path gets passed', () => {
+ it('custom metrics path gets passed', async () => {
const path = 'https://path/to/customMetrics';
createShallowWrapper({ customMetricsPath: path });
- return wrapper.vm.$nextTick().then(() => {
- expect(findActionsMenu().props('customMetricsPath')).toBe(path);
- });
+ await nextTick();
+ expect(findActionsMenu().props('customMetricsPath')).toBe(path);
});
- it('validate query path gets passed', () => {
+ it('validate query path gets passed', async () => {
const path = 'https://path/to/validateQuery';
createShallowWrapper({ validateQueryPath: path });
- return wrapper.vm.$nextTick().then(() => {
- expect(findActionsMenu().props('validateQueryPath')).toBe(path);
- });
+ await nextTick();
+ expect(findActionsMenu().props('validateQueryPath')).toBe(path);
});
- it('default branch gets passed', () => {
+ it('default branch gets passed', async () => {
const branch = 'branchName';
createShallowWrapper({ defaultBranch: branch });
- return wrapper.vm.$nextTick().then(() => {
- expect(findActionsMenu().props('defaultBranch')).toBe(branch);
- });
+ await nextTick();
+ expect(findActionsMenu().props('defaultBranch')).toBe(branch);
});
});
@@ -385,40 +372,36 @@ describe('Dashboard header', () => {
store.state.monitoringDashboard.operationsSettingsPath = '';
});
- it('is rendered when the user can access the project settings and path to settings is available', () => {
+ it('is rendered when the user can access the project settings and path to settings is available', async () => {
store.state.monitoringDashboard.canAccessOperationsSettings = true;
store.state.monitoringDashboard.operationsSettingsPath = url;
- return wrapper.vm.$nextTick(() => {
- expect(findSettingsButton().exists()).toBe(true);
- });
+ await nextTick();
+ expect(findSettingsButton().exists()).toBe(true);
});
- it('is not rendered when the user can not access the project settings', () => {
+ it('is not rendered when the user can not access the project settings', async () => {
store.state.monitoringDashboard.canAccessOperationsSettings = false;
store.state.monitoringDashboard.operationsSettingsPath = url;
- return wrapper.vm.$nextTick(() => {
- expect(findSettingsButton().exists()).toBe(false);
- });
+ await nextTick();
+ expect(findSettingsButton().exists()).toBe(false);
});
- it('is not rendered when the path to settings is unavailable', () => {
+ it('is not rendered when the path to settings is unavailable', async () => {
store.state.monitoringDashboard.canAccessOperationsSettings = false;
store.state.monitoringDashboard.operationsSettingsPath = '';
- return wrapper.vm.$nextTick(() => {
- expect(findSettingsButton().exists()).toBe(false);
- });
+ await nextTick();
+ expect(findSettingsButton().exists()).toBe(false);
});
- it('leads to the project settings page', () => {
+ it('leads to the project settings page', async () => {
store.state.monitoringDashboard.canAccessOperationsSettings = true;
store.state.monitoringDashboard.operationsSettingsPath = url;
- return wrapper.vm.$nextTick(() => {
- expect(findSettingsButton().attributes('href')).toBe(url);
- });
+ await nextTick();
+ expect(findSettingsButton().attributes('href')).toBe(url);
});
});
});
diff --git a/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js b/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js
index 400ac2e8f85..f19ef6c6fb7 100644
--- a/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js
@@ -1,5 +1,6 @@
import { GlCard, GlForm, GlFormTextarea, GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import DashboardPanel from '~/monitoring/components/dashboard_panel.vue';
import DashboardPanelBuilder from '~/monitoring/components/dashboard_panel_builder.vue';
import { createStore } from '~/monitoring/stores';
@@ -90,21 +91,20 @@ describe('dashboard invalid url parameters', () => {
expect(mockShowToast).toHaveBeenCalledTimes(1);
});
- it('on submit fetches a panel preview', () => {
+ it('on submit fetches a panel preview', async () => {
findForm().vm.$emit('submit', new Event('submit'));
- return wrapper.vm.$nextTick().then(() => {
- expect(store.dispatch).toHaveBeenCalledWith(
- 'monitoringDashboard/fetchPanelPreview',
- expect.stringContaining('title:'),
- );
- });
+ await nextTick();
+ expect(store.dispatch).toHaveBeenCalledWith(
+ 'monitoringDashboard/fetchPanelPreview',
+ expect.stringContaining('title:'),
+ );
});
describe('when form is submitted', () => {
- beforeEach(() => {
+ beforeEach(async () => {
store.commit(`monitoringDashboard/${types.REQUEST_PANEL_PREVIEW}`, 'mock yml content');
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('submit button is disabled', () => {
@@ -118,23 +118,21 @@ describe('dashboard invalid url parameters', () => {
expect(findTimeRangePicker().exists()).toBe(true);
});
- it('when changed does not trigger data fetch unless preview panel button is clicked', () => {
+ it('when changed does not trigger data fetch unless preview panel button is clicked', async () => {
// mimic initial state where SET_PANEL_PREVIEW_IS_SHOWN is set to false
store.commit(`monitoringDashboard/${types.SET_PANEL_PREVIEW_IS_SHOWN}`, false);
- return wrapper.vm.$nextTick(() => {
- expect(store.dispatch).not.toHaveBeenCalled();
- });
+ await nextTick();
+ expect(store.dispatch).not.toHaveBeenCalled();
});
- it('when changed triggers data fetch if preview panel button is clicked', () => {
+ it('when changed triggers data fetch if preview panel button is clicked', async () => {
findForm().vm.$emit('submit', new Event('submit'));
store.commit(`monitoringDashboard/${types.SET_PANEL_PREVIEW_TIME_RANGE}`, mockTimeRange);
- return wrapper.vm.$nextTick(() => {
- expect(store.dispatch).toHaveBeenCalled();
- });
+ await nextTick();
+ expect(store.dispatch).toHaveBeenCalled();
});
});
@@ -143,27 +141,25 @@ describe('dashboard invalid url parameters', () => {
expect(findRefreshButton().exists()).toBe(true);
});
- it('when clicked does not trigger data fetch unless preview panel button is clicked', () => {
+ it('when clicked does not trigger data fetch unless preview panel button is clicked', async () => {
// mimic initial state where SET_PANEL_PREVIEW_IS_SHOWN is set to false
store.commit(`monitoringDashboard/${types.SET_PANEL_PREVIEW_IS_SHOWN}`, false);
- return wrapper.vm.$nextTick(() => {
- expect(store.dispatch).not.toHaveBeenCalled();
- });
+ await nextTick();
+ expect(store.dispatch).not.toHaveBeenCalled();
});
- it('when clicked triggers data fetch if preview panel button is clicked', () => {
+ it('when clicked triggers data fetch if preview panel button is clicked', async () => {
// mimic state where preview is visible. SET_PANEL_PREVIEW_IS_SHOWN is set to true
store.commit(`monitoringDashboard/${types.SET_PANEL_PREVIEW_IS_SHOWN}`, true);
findRefreshButton().vm.$emit('click');
- return wrapper.vm.$nextTick(() => {
- expect(store.dispatch).toHaveBeenCalledWith(
- 'monitoringDashboard/fetchPanelPreviewMetrics',
- undefined,
- );
- });
+ await nextTick();
+ expect(store.dispatch).toHaveBeenCalledWith(
+ 'monitoringDashboard/fetchPanelPreviewMetrics',
+ undefined,
+ );
});
});
@@ -190,9 +186,9 @@ describe('dashboard invalid url parameters', () => {
describe('when there is an error', () => {
const mockError = 'an error occurred!';
- beforeEach(() => {
+ beforeEach(async () => {
store.commit(`monitoringDashboard/${types.RECEIVE_PANEL_PREVIEW_FAILURE}`, mockError);
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('displays an alert', () => {
@@ -204,19 +200,18 @@ describe('dashboard invalid url parameters', () => {
expect(findPanel().props('graphData')).toBe(null);
});
- it('changing time range should not refetch data', () => {
+ it('changing time range should not refetch data', async () => {
store.commit(`monitoringDashboard/${types.SET_PANEL_PREVIEW_TIME_RANGE}`, mockTimeRange);
- return wrapper.vm.$nextTick(() => {
- expect(store.dispatch).not.toHaveBeenCalled();
- });
+ await nextTick();
+ expect(store.dispatch).not.toHaveBeenCalled();
});
});
describe('when panel data is available', () => {
- beforeEach(() => {
+ beforeEach(async () => {
store.commit(`monitoringDashboard/${types.RECEIVE_PANEL_PREVIEW_SUCCESS}`, mockPanel);
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('displays no alert', () => {
diff --git a/spec/frontend/monitoring/components/dashboard_panel_spec.js b/spec/frontend/monitoring/components/dashboard_panel_spec.js
index 9a73dc820af..7bd062b81f1 100644
--- a/spec/frontend/monitoring/components/dashboard_panel_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_panel_spec.js
@@ -2,6 +2,7 @@ import { GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import Vuex from 'vuex';
+import { nextTick } from 'vue';
import { setTestTimeout } from 'helpers/timeout';
import axios from '~/lib/utils/axios_utils';
import invalidUrl from '~/lib/utils/invalid_url';
@@ -186,7 +187,7 @@ describe('Dashboard Panel', () => {
expect(findCopyLink().exists()).toBe(false);
});
- it('should emit `timerange` event when a zooming in/out in a chart occcurs', () => {
+ it('should emit `timerange` event when a zooming in/out in a chart occcurs', async () => {
const timeRange = {
start: '2020-01-01T00:00:00.000Z',
end: '2020-01-01T01:00:00.000Z',
@@ -196,9 +197,8 @@ describe('Dashboard Panel', () => {
findTimeChart().vm.$emit('datazoom', timeRange);
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('timerangezoom', timeRange);
- });
+ await nextTick();
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('timerangezoom', timeRange);
});
it('includes a default group id', () => {
@@ -253,16 +253,15 @@ describe('Dashboard Panel', () => {
describe('computed', () => {
describe('fixedCurrentTimeRange', () => {
- it('returns fixed time for valid time range', () => {
+ it('returns fixed time for valid time range', async () => {
state.timeRange = mockTimeRange;
- return wrapper.vm.$nextTick(() => {
- expect(findTimeChart().props('timeRange')).toEqual(
- expect.objectContaining({
- start: expect.any(String),
- end: expect.any(String),
- }),
- );
- });
+ await nextTick();
+ expect(findTimeChart().props('timeRange')).toEqual(
+ expect.objectContaining({
+ start: expect.any(String),
+ end: expect.any(String),
+ }),
+ );
});
it.each`
@@ -271,11 +270,10 @@ describe('Dashboard Panel', () => {
${undefined} | ${{}}
${null} | ${{}}
${'2020-12-03'} | ${{}}
- `('returns $output for invalid input like $input', ({ input, output }) => {
+ `('returns $output for invalid input like $input', async ({ input, output }) => {
state.timeRange = input;
- return wrapper.vm.$nextTick(() => {
- expect(findTimeChart().props('timeRange')).toEqual(output);
- });
+ await nextTick();
+ expect(findTimeChart().props('timeRange')).toEqual(output);
});
});
});
@@ -285,17 +283,16 @@ describe('Dashboard Panel', () => {
const findEditCustomMetricLink = () => wrapper.find({ ref: 'editMetricLink' });
const mockEditPath = '/root/kubernetes-gke-project/prometheus/metrics/23/edit';
- beforeEach(() => {
+ beforeEach(async () => {
createWrapper();
-
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('is not present if the panel is not a custom metric', () => {
expect(findEditCustomMetricLink().exists()).toBe(false);
});
- it('is present when the panel contains an edit_path property', () => {
+ it('is present when the panel contains an edit_path property', async () => {
wrapper.setProps({
graphData: {
...graphData,
@@ -308,14 +305,13 @@ describe('Dashboard Panel', () => {
},
});
- return wrapper.vm.$nextTick(() => {
- expect(findEditCustomMetricLink().exists()).toBe(true);
- expect(findEditCustomMetricLink().text()).toBe('Edit metric');
- expect(findEditCustomMetricLink().attributes('href')).toBe(mockEditPath);
- });
+ await nextTick();
+ expect(findEditCustomMetricLink().exists()).toBe(true);
+ expect(findEditCustomMetricLink().text()).toBe('Edit metric');
+ expect(findEditCustomMetricLink().attributes('href')).toBe(mockEditPath);
});
- it('shows an "Edit metrics" link pointing to settingsPath for a panel with multiple metrics', () => {
+ it('shows an "Edit metrics" link pointing to settingsPath for a panel with multiple metrics', async () => {
wrapper.setProps({
graphData: {
...graphData,
@@ -332,63 +328,58 @@ describe('Dashboard Panel', () => {
},
});
- return wrapper.vm.$nextTick(() => {
- expect(findEditCustomMetricLink().text()).toBe('Edit metrics');
- expect(findEditCustomMetricLink().attributes('href')).toBe(dashboardProps.settingsPath);
- });
+ await nextTick();
+ expect(findEditCustomMetricLink().text()).toBe('Edit metrics');
+ expect(findEditCustomMetricLink().attributes('href')).toBe(dashboardProps.settingsPath);
});
});
describe('View Logs dropdown item', () => {
const findViewLogsLink = () => wrapper.find({ ref: 'viewLogsLink' });
- beforeEach(() => {
+ beforeEach(async () => {
createWrapper();
- return wrapper.vm.$nextTick();
+ await nextTick();
});
- it('is not present by default', () =>
- wrapper.vm.$nextTick(() => {
- expect(findViewLogsLink().exists()).toBe(false);
- }));
+ it('is not present by default', async () => {
+ await nextTick();
+ expect(findViewLogsLink().exists()).toBe(false);
+ });
- it('is not present if a time range is not set', () => {
+ it('is not present if a time range is not set', async () => {
state.logsPath = mockLogsPath;
state.timeRange = null;
- return wrapper.vm.$nextTick(() => {
- expect(findViewLogsLink().exists()).toBe(false);
- });
+ await nextTick();
+ expect(findViewLogsLink().exists()).toBe(false);
});
- it('is not present if the logs path is default', () => {
+ it('is not present if the logs path is default', async () => {
state.logsPath = invalidUrl;
state.timeRange = mockTimeRange;
- return wrapper.vm.$nextTick(() => {
- expect(findViewLogsLink().exists()).toBe(false);
- });
+ await nextTick();
+ expect(findViewLogsLink().exists()).toBe(false);
});
- it('is not present if the logs path is not set', () => {
+ it('is not present if the logs path is not set', async () => {
state.logsPath = null;
state.timeRange = mockTimeRange;
- return wrapper.vm.$nextTick(() => {
- expect(findViewLogsLink().exists()).toBe(false);
- });
+ await nextTick();
+ expect(findViewLogsLink().exists()).toBe(false);
});
- it('is present when logs path and time a range is present', () => {
+ it('is present when logs path and time a range is present', async () => {
state.logsPath = mockLogsPath;
state.timeRange = mockTimeRange;
- return wrapper.vm.$nextTick(() => {
- expect(findViewLogsLink().attributes('href')).toMatch(mockLogsHref);
- });
+ await nextTick();
+ expect(findViewLogsLink().attributes('href')).toMatch(mockLogsHref);
});
- it('it is overridden when a datazoom event is received', () => {
+ it('it is overridden when a datazoom event is received', async () => {
state.logsPath = mockLogsPath;
state.timeRange = mockTimeRange;
@@ -399,13 +390,12 @@ describe('Dashboard Panel', () => {
findTimeChart().vm.$emit('datazoom', zoomedTimeRange);
- return wrapper.vm.$nextTick(() => {
- const start = encodeURIComponent(zoomedTimeRange.start);
- const end = encodeURIComponent(zoomedTimeRange.end);
- expect(findViewLogsLink().attributes('href')).toMatch(
- `${mockLogsPath}?start=${start}&end=${end}`,
- );
- });
+ await nextTick();
+ const start = encodeURIComponent(zoomedTimeRange.start);
+ const end = encodeURIComponent(zoomedTimeRange.end);
+ expect(findViewLogsLink().attributes('href')).toMatch(
+ `${mockLogsPath}?start=${start}&end=${end}`,
+ );
});
});
@@ -447,7 +437,7 @@ describe('Dashboard Panel', () => {
});
describe('when downloading metrics data as CSV', () => {
- beforeEach(() => {
+ beforeEach(async () => {
wrapper = shallowMount(DashboardPanel, {
propsData: {
clipboardText: exampleText,
@@ -459,7 +449,7 @@ describe('Dashboard Panel', () => {
},
store,
});
- return wrapper.vm.$nextTick();
+ await nextTick();
});
afterEach(() => {
@@ -509,29 +499,26 @@ describe('Dashboard Panel', () => {
});
});
- it('handles namespaced time range and logs path state', () => {
+ it('handles namespaced time range and logs path state', async () => {
store.state[mockNamespace].timeRange = mockTimeRange;
store.state[mockNamespace].logsPath = mockLogsPath;
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find({ ref: 'viewLogsLink' }).attributes().href).toBe(mockLogsHref);
- });
+ await nextTick();
+ expect(wrapper.find({ ref: 'viewLogsLink' }).attributes().href).toBe(mockLogsHref);
});
- it('handles namespaced deployment data state', () => {
+ it('handles namespaced deployment data state', async () => {
store.state[mockNamespace].deploymentData = mockDeploymentData;
- return wrapper.vm.$nextTick().then(() => {
- expect(findTimeChart().props().deploymentData).toEqual(mockDeploymentData);
- });
+ await nextTick();
+ expect(findTimeChart().props().deploymentData).toEqual(mockDeploymentData);
});
- it('handles namespaced project path state', () => {
+ it('handles namespaced project path state', async () => {
store.state[mockNamespace].projectPath = mockProjectPath;
- return wrapper.vm.$nextTick().then(() => {
- expect(findTimeChart().props().projectPath).toBe(mockProjectPath);
- });
+ await nextTick();
+ expect(findTimeChart().props().projectPath).toBe(mockProjectPath);
});
it('it renders a time series chart with no errors', () => {
diff --git a/spec/frontend/monitoring/components/dashboard_spec.js b/spec/frontend/monitoring/components/dashboard_spec.js
index 7730e7f347f..6c5972e1140 100644
--- a/spec/frontend/monitoring/components/dashboard_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_spec.js
@@ -1,5 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import VueDraggable from 'vuedraggable';
+import { nextTick } from 'vue';
import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
@@ -77,51 +78,47 @@ describe('Dashboard', () => {
});
describe('request information to the server', () => {
- it('calls to set time range and fetch data', () => {
+ it('calls to set time range and fetch data', async () => {
createShallowWrapper({ hasMetrics: true });
- return wrapper.vm.$nextTick().then(() => {
- expect(store.dispatch).toHaveBeenCalledWith(
- 'monitoringDashboard/setTimeRange',
- expect.any(Object),
- );
+ await nextTick();
+ expect(store.dispatch).toHaveBeenCalledWith(
+ 'monitoringDashboard/setTimeRange',
+ expect.any(Object),
+ );
- expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/fetchData', undefined);
- });
+ expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/fetchData', undefined);
});
- it('shows up a loading state', () => {
+ it('shows up a loading state', async () => {
store.state.monitoringDashboard.emptyState = dashboardEmptyStates.LOADING;
createShallowWrapper({ hasMetrics: true });
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(EmptyState).exists()).toBe(true);
- expect(wrapper.find(EmptyState).props('selectedState')).toBe(dashboardEmptyStates.LOADING);
- });
+ await nextTick();
+ expect(wrapper.find(EmptyState).exists()).toBe(true);
+ expect(wrapper.find(EmptyState).props('selectedState')).toBe(dashboardEmptyStates.LOADING);
});
- it('hides the group panels when showPanels is false', () => {
+ it('hides the group panels when showPanels is false', async () => {
createMountedWrapper({ hasMetrics: true, showPanels: false });
setupStoreWithData(store);
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.vm.emptyState).toBeNull();
- expect(wrapper.findAll('.prometheus-panel')).toHaveLength(0);
- });
+ await nextTick();
+ expect(wrapper.vm.emptyState).toBeNull();
+ expect(wrapper.findAll('.prometheus-panel')).toHaveLength(0);
});
- it('fetches the metrics data with proper time window', () => {
+ it('fetches the metrics data with proper time window', async () => {
createMountedWrapper({ hasMetrics: true });
- return wrapper.vm.$nextTick().then(() => {
- expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/fetchData', undefined);
- expect(store.dispatch).toHaveBeenCalledWith(
- 'monitoringDashboard/setTimeRange',
- expect.objectContaining({ duration: { seconds: 28800 } }),
- );
- });
+ await nextTick();
+ expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/fetchData', undefined);
+ expect(store.dispatch).toHaveBeenCalledWith(
+ 'monitoringDashboard/setTimeRange',
+ expect.objectContaining({ duration: { seconds: 28800 } }),
+ );
});
});
@@ -133,69 +130,63 @@ describe('Dashboard', () => {
.at(index);
};
- beforeEach(() => {
+ beforeEach(async () => {
createMountedWrapper({ hasMetrics: true });
-
- return wrapper.vm.$nextTick();
+ await nextTick();
});
describe('when the graph group has an even number of panels', () => {
- it('2 panels - all panel wrappers take half width of their parent', () => {
+ it('2 panels - all panel wrappers take half width of their parent', async () => {
setupStoreWithDataForPanelCount(store, 2);
- wrapper.vm.$nextTick(() => {
- expect(findPanelLayoutWrapperAt(0).classes('col-lg-6')).toBe(true);
- expect(findPanelLayoutWrapperAt(1).classes('col-lg-6')).toBe(true);
- });
+ await nextTick();
+ expect(findPanelLayoutWrapperAt(0).classes('col-lg-6')).toBe(true);
+ expect(findPanelLayoutWrapperAt(1).classes('col-lg-6')).toBe(true);
});
- it('4 panels - all panel wrappers take half width of their parent', () => {
+ it('4 panels - all panel wrappers take half width of their parent', async () => {
setupStoreWithDataForPanelCount(store, 4);
- wrapper.vm.$nextTick(() => {
- expect(findPanelLayoutWrapperAt(0).classes('col-lg-6')).toBe(true);
- expect(findPanelLayoutWrapperAt(1).classes('col-lg-6')).toBe(true);
- expect(findPanelLayoutWrapperAt(2).classes('col-lg-6')).toBe(true);
- expect(findPanelLayoutWrapperAt(3).classes('col-lg-6')).toBe(true);
- });
+ await nextTick();
+ expect(findPanelLayoutWrapperAt(0).classes('col-lg-6')).toBe(true);
+ expect(findPanelLayoutWrapperAt(1).classes('col-lg-6')).toBe(true);
+ expect(findPanelLayoutWrapperAt(2).classes('col-lg-6')).toBe(true);
+ expect(findPanelLayoutWrapperAt(3).classes('col-lg-6')).toBe(true);
});
});
describe('when the graph group has an odd number of panels', () => {
- it('1 panel - panel wrapper does not take half width of its parent', () => {
+ it('1 panel - panel wrapper does not take half width of its parent', async () => {
setupStoreWithDataForPanelCount(store, 1);
- wrapper.vm.$nextTick(() => {
- expect(findPanelLayoutWrapperAt(0).classes('col-lg-6')).toBe(false);
- });
+ await nextTick();
+ expect(findPanelLayoutWrapperAt(0).classes('col-lg-6')).toBe(false);
});
- it('3 panels - all panels but last take half width of their parents', () => {
+ it('3 panels - all panels but last take half width of their parents', async () => {
setupStoreWithDataForPanelCount(store, 3);
- wrapper.vm.$nextTick(() => {
- expect(findPanelLayoutWrapperAt(0).classes('col-lg-6')).toBe(true);
- expect(findPanelLayoutWrapperAt(1).classes('col-lg-6')).toBe(true);
- expect(findPanelLayoutWrapperAt(2).classes('col-lg-6')).toBe(false);
- });
+ await nextTick();
+ expect(findPanelLayoutWrapperAt(0).classes('col-lg-6')).toBe(true);
+ expect(findPanelLayoutWrapperAt(1).classes('col-lg-6')).toBe(true);
+ expect(findPanelLayoutWrapperAt(2).classes('col-lg-6')).toBe(false);
});
- it('5 panels - all panels but last take half width of their parents', () => {
+ it('5 panels - all panels but last take half width of their parents', async () => {
setupStoreWithDataForPanelCount(store, 5);
- wrapper.vm.$nextTick(() => {
- expect(findPanelLayoutWrapperAt(0).classes('col-lg-6')).toBe(true);
- expect(findPanelLayoutWrapperAt(1).classes('col-lg-6')).toBe(true);
- expect(findPanelLayoutWrapperAt(2).classes('col-lg-6')).toBe(true);
- expect(findPanelLayoutWrapperAt(3).classes('col-lg-6')).toBe(true);
- expect(findPanelLayoutWrapperAt(4).classes('col-lg-6')).toBe(false);
- });
+ await nextTick();
+ expect(findPanelLayoutWrapperAt(0).classes('col-lg-6')).toBe(true);
+ expect(findPanelLayoutWrapperAt(1).classes('col-lg-6')).toBe(true);
+ expect(findPanelLayoutWrapperAt(2).classes('col-lg-6')).toBe(true);
+ expect(findPanelLayoutWrapperAt(3).classes('col-lg-6')).toBe(true);
+ expect(findPanelLayoutWrapperAt(4).classes('col-lg-6')).toBe(false);
});
});
});
describe('dashboard validation warning', () => {
- it('displays a warning if there are validation warnings', () => {
+ it('displays a warning if there are validation warnings', async () => {
createMountedWrapper({ hasMetrics: true });
store.commit(
@@ -203,12 +194,11 @@ describe('Dashboard', () => {
true,
);
- return wrapper.vm.$nextTick().then(() => {
- expect(createFlash).toHaveBeenCalled();
- });
+ await nextTick();
+ expect(createFlash).toHaveBeenCalled();
});
- it('does not display a warning if there are no validation warnings', () => {
+ it('does not display a warning if there are no validation warnings', async () => {
createMountedWrapper({ hasMetrics: true });
store.commit(
@@ -216,9 +206,8 @@ describe('Dashboard', () => {
false,
);
- return wrapper.vm.$nextTick().then(() => {
- expect(createFlash).not.toHaveBeenCalled();
- });
+ await nextTick();
+ expect(createFlash).not.toHaveBeenCalled();
});
});
@@ -233,7 +222,7 @@ describe('Dashboard', () => {
setWindowLocation(location);
});
- it('when the URL points to a panel it expands', () => {
+ it('when the URL points to a panel it expands', async () => {
const panelGroup = metricsDashboardViewModel.panelGroups[0];
const panel = panelGroup.panels[0];
@@ -246,32 +235,30 @@ describe('Dashboard', () => {
createMountedWrapper({ hasMetrics: true });
setupStoreWithData(store);
- return wrapper.vm.$nextTick().then(() => {
- expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/setExpandedPanel', {
- group: panelGroup.group,
- panel: expect.objectContaining({
- title: panel.title,
- y_label: panel.y_label,
- }),
- });
+ await nextTick();
+ expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/setExpandedPanel', {
+ group: panelGroup.group,
+ panel: expect.objectContaining({
+ title: panel.title,
+ y_label: panel.y_label,
+ }),
});
});
- it('when the URL does not link to any panel, no panel is expanded', () => {
+ it('when the URL does not link to any panel, no panel is expanded', async () => {
setSearch();
createMountedWrapper({ hasMetrics: true });
setupStoreWithData(store);
- return wrapper.vm.$nextTick().then(() => {
- expect(store.dispatch).not.toHaveBeenCalledWith(
- 'monitoringDashboard/setExpandedPanel',
- expect.anything(),
- );
- });
+ await nextTick();
+ expect(store.dispatch).not.toHaveBeenCalledWith(
+ 'monitoringDashboard/setExpandedPanel',
+ expect.anything(),
+ );
});
- it('when the URL points to an incorrect panel it shows an error', () => {
+ it('when the URL points to an incorrect panel it shows an error', async () => {
const panelGroup = metricsDashboardViewModel.panelGroups[0];
const panel = panelGroup.panels[0];
@@ -284,13 +271,12 @@ describe('Dashboard', () => {
createMountedWrapper({ hasMetrics: true });
setupStoreWithData(store);
- return wrapper.vm.$nextTick().then(() => {
- expect(createFlash).toHaveBeenCalled();
- expect(store.dispatch).not.toHaveBeenCalledWith(
- 'monitoringDashboard/setExpandedPanel',
- expect.anything(),
- );
- });
+ await nextTick();
+ expect(createFlash).toHaveBeenCalled();
+ expect(store.dispatch).not.toHaveBeenCalledWith(
+ 'monitoringDashboard/setExpandedPanel',
+ expect.anything(),
+ );
});
});
@@ -319,7 +305,7 @@ describe('Dashboard', () => {
window.history.pushState.mockRestore();
});
- it('URL is updated with panel parameters', () => {
+ it('URL is updated with panel parameters', async () => {
createMountedWrapper({ hasMetrics: true });
expandPanel(group, panel);
@@ -329,17 +315,16 @@ describe('Dashboard', () => {
y_label: panel.y_label,
});
- return wrapper.vm.$nextTick(() => {
- expect(window.history.pushState).toHaveBeenCalledTimes(1);
- expect(window.history.pushState).toHaveBeenCalledWith(
- expect.anything(), // state
- expect.any(String), // document title
- expect.stringContaining(`${expectedSearch}`),
- );
- });
+ await nextTick();
+ expect(window.history.pushState).toHaveBeenCalledTimes(1);
+ expect(window.history.pushState).toHaveBeenCalledWith(
+ expect.anything(), // state
+ expect.any(String), // document title
+ expect.stringContaining(`${expectedSearch}`),
+ );
});
- it('URL is updated with panel parameters and custom dashboard', () => {
+ it('URL is updated with panel parameters and custom dashboard', async () => {
const dashboard = 'dashboard.yml';
store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
@@ -355,36 +340,34 @@ describe('Dashboard', () => {
y_label: panel.y_label,
});
- return wrapper.vm.$nextTick(() => {
- expect(window.history.pushState).toHaveBeenCalledTimes(1);
- expect(window.history.pushState).toHaveBeenCalledWith(
- expect.anything(), // state
- expect.any(String), // document title
- expect.stringContaining(`${expectedSearch}`),
- );
- });
+ await nextTick();
+ expect(window.history.pushState).toHaveBeenCalledTimes(1);
+ expect(window.history.pushState).toHaveBeenCalledWith(
+ expect.anything(), // state
+ expect.any(String), // document title
+ expect.stringContaining(`${expectedSearch}`),
+ );
});
- it('URL is updated with no parameters', () => {
+ it('URL is updated with no parameters', async () => {
expandPanel(group, panel);
createMountedWrapper({ hasMetrics: true });
expandPanel(null, null);
- return wrapper.vm.$nextTick(() => {
- expect(window.history.pushState).toHaveBeenCalledTimes(1);
- expect(window.history.pushState).toHaveBeenCalledWith(
- expect.anything(), // state
- expect.any(String), // document title
- expect.not.stringMatching(/group|title|y_label/), // no panel params
- );
- });
+ await nextTick();
+ expect(window.history.pushState).toHaveBeenCalledTimes(1);
+ expect(window.history.pushState).toHaveBeenCalledWith(
+ expect.anything(), // state
+ expect.any(String), // document title
+ expect.not.stringMatching(/group|title|y_label/), // no panel params
+ );
});
});
describe('when all panels in the first group are loading', () => {
const findGroupAt = (i) => wrapper.findAll(GraphGroup).at(i);
- beforeEach(() => {
+ beforeEach(async () => {
setupStoreWithDashboard(store);
const { panels } = store.state.monitoringDashboard.dashboard.panelGroups[0];
@@ -396,7 +379,7 @@ describe('Dashboard', () => {
createShallowWrapper();
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('a loading icon appears in the first group', () => {
@@ -409,7 +392,7 @@ describe('Dashboard', () => {
});
describe('when all requests have been committed by the store', () => {
- beforeEach(() => {
+ beforeEach(async () => {
store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
currentEnvironmentName: 'production',
currentDashboard: dashboardGitResponse[0].path,
@@ -418,26 +401,25 @@ describe('Dashboard', () => {
createMountedWrapper({ hasMetrics: true });
setupStoreWithData(store);
- return wrapper.vm.$nextTick();
+ await nextTick();
});
- it('it does not show loading icons in any group', () => {
+ it('it does not show loading icons in any group', async () => {
setupStoreWithData(store);
- wrapper.vm.$nextTick(() => {
- wrapper.findAll(GraphGroup).wrappers.forEach((groupWrapper) => {
- expect(groupWrapper.props('isLoading')).toBe(false);
- });
+ await nextTick();
+ wrapper.findAll(GraphGroup).wrappers.forEach((groupWrapper) => {
+ expect(groupWrapper.props('isLoading')).toBe(false);
});
});
});
describe('variables section', () => {
- beforeEach(() => {
+ beforeEach(async () => {
createShallowWrapper({ hasMetrics: true });
setupStoreWithData(store);
store.state.monitoringDashboard.variables = storeVariables;
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('shows the variables section', () => {
@@ -446,12 +428,11 @@ describe('Dashboard', () => {
});
describe('links section', () => {
- beforeEach(() => {
+ beforeEach(async () => {
createShallowWrapper({ hasMetrics: true });
setupStoreWithData(store);
setupStoreWithLinks(store);
-
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('shows the links section', () => {
@@ -464,10 +445,10 @@ describe('Dashboard', () => {
const findExpandedPanel = () => wrapper.find({ ref: 'expandedPanel' });
describe('when the panel is not expanded', () => {
- beforeEach(() => {
+ beforeEach(async () => {
createShallowWrapper({ hasMetrics: true });
setupStoreWithData(store);
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('expanded panel is not visible', () => {
@@ -502,7 +483,7 @@ describe('Dashboard', () => {
template: `<div><slot name="top-left"/></div>`,
};
- beforeEach(() => {
+ beforeEach(async () => {
createShallowWrapper({ hasMetrics: true }, { stubs: { DashboardPanel: MockPanel } });
setupStoreWithData(store);
@@ -517,8 +498,7 @@ describe('Dashboard', () => {
});
jest.spyOn(store, 'dispatch');
-
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('displays a single panel and others are hidden', () => {
@@ -561,13 +541,12 @@ describe('Dashboard', () => {
});
describe('when one of the metrics is missing', () => {
- beforeEach(() => {
+ beforeEach(async () => {
createShallowWrapper({ hasMetrics: true });
setupStoreWithDashboard(store);
setMetricResult({ store, result: [], panel: 2 });
-
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('shows a group empty area', () => {
@@ -590,14 +569,13 @@ describe('Dashboard', () => {
const findDraggablePanels = () => wrapper.findAll('.js-draggable-panel');
const findRearrangeButton = () => wrapper.find('.js-rearrange-button');
- beforeEach(() => {
+ beforeEach(async () => {
// call original dispatch
store.dispatch.mockRestore();
createShallowWrapper({ hasMetrics: true });
setupStoreWithData(store);
-
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('wraps vuedraggable', () => {
@@ -611,9 +589,9 @@ describe('Dashboard', () => {
});
describe('when rearrange is enabled', () => {
- beforeEach(() => {
+ beforeEach(async () => {
wrapper.setProps({ rearrangePanelsAvailable: true });
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('displays rearrange button', () => {
@@ -624,9 +602,9 @@ describe('Dashboard', () => {
const findFirstDraggableRemoveButton = () =>
findDraggablePanels().at(0).find('.js-draggable-remove');
- beforeEach(() => {
+ beforeEach(async () => {
findRearrangeButton().vm.$emit('click');
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('it enables draggables', () => {
@@ -634,7 +612,7 @@ describe('Dashboard', () => {
expect(findEnabledDraggables().wrappers).toEqual(findDraggables().wrappers);
});
- it('metrics can be swapped', () => {
+ it('metrics can be swapped', async () => {
const firstDraggable = findDraggables().at(0);
const mockMetrics = [...metricsDashboardViewModel.panelGroups[0].panels];
@@ -645,43 +623,40 @@ describe('Dashboard', () => {
[mockMetrics[0], mockMetrics[1]] = [mockMetrics[1], mockMetrics[0]];
firstDraggable.vm.$emit('input', mockMetrics);
- return wrapper.vm.$nextTick(() => {
- const { panels } = wrapper.vm.dashboard.panelGroups[0];
+ await nextTick();
+ const { panels } = wrapper.vm.dashboard.panelGroups[0];
- expect(panels[1].title).toEqual(firstTitle);
- expect(panels[0].title).toEqual(secondTitle);
- });
+ expect(panels[1].title).toEqual(firstTitle);
+ expect(panels[0].title).toEqual(secondTitle);
});
- it('shows a remove button, which removes a panel', () => {
+ it('shows a remove button, which removes a panel', async () => {
expect(findFirstDraggableRemoveButton().find('a').exists()).toBe(true);
expect(findDraggablePanels().length).toEqual(metricsDashboardPanelCount);
findFirstDraggableRemoveButton().trigger('click');
- return wrapper.vm.$nextTick(() => {
- expect(findDraggablePanels().length).toEqual(metricsDashboardPanelCount - 1);
- });
+ await nextTick();
+ expect(findDraggablePanels().length).toEqual(metricsDashboardPanelCount - 1);
});
- it('it disables draggables when clicked again', () => {
+ it('it disables draggables when clicked again', async () => {
findRearrangeButton().vm.$emit('click');
- return wrapper.vm.$nextTick(() => {
- expect(findRearrangeButton().attributes('pressed')).toBeFalsy();
- expect(findEnabledDraggables().length).toBe(0);
- });
+ await nextTick();
+ expect(findRearrangeButton().attributes('pressed')).toBeFalsy();
+ expect(findEnabledDraggables().length).toBe(0);
});
});
});
});
describe('cluster health', () => {
- beforeEach(() => {
+ beforeEach(async () => {
createShallowWrapper({ hasMetrics: true, showHeader: false });
// all_dashboards is not defined in health dashboards
store.commit(`monitoringDashboard/${types.SET_ALL_DASHBOARDS}`, undefined);
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('hides dashboard header by default', () => {
@@ -706,34 +681,31 @@ describe('Dashboard', () => {
document.title = '';
});
- it('is prepended with the overview dashboard name by default', () => {
+ it('is prepended with the overview dashboard name by default', async () => {
setupAllDashboards(store);
- return wrapper.vm.$nextTick().then(() => {
- expect(document.title.startsWith(`${overviewDashboardName} · `)).toBe(true);
- });
+ await nextTick();
+ expect(document.title.startsWith(`${overviewDashboardName} · `)).toBe(true);
});
- it('is prepended with dashboard name if path is known', () => {
+ it('is prepended with dashboard name if path is known', async () => {
const dashboard = dashboardGitResponse[1];
const currentDashboard = dashboard.path;
setupAllDashboards(store, currentDashboard);
- return wrapper.vm.$nextTick().then(() => {
- expect(document.title.startsWith(`${dashboard.display_name} · `)).toBe(true);
- });
+ await nextTick();
+ expect(document.title.startsWith(`${dashboard.display_name} · `)).toBe(true);
});
- it('is prepended with the overview dashboard name if path is not known', () => {
+ it('is prepended with the overview dashboard name if path is not known', async () => {
setupAllDashboards(store, 'unknown/path');
- return wrapper.vm.$nextTick().then(() => {
- expect(document.title.startsWith(`${overviewDashboardName} · `)).toBe(true);
- });
+ await nextTick();
+ expect(document.title.startsWith(`${overviewDashboardName} · `)).toBe(true);
});
- it('is not modified when dashboard name is not provided', () => {
+ it('is not modified when dashboard name is not provided', async () => {
const dashboard = { ...dashboardGitResponse[1], display_name: null };
const currentDashboard = dashboard.path;
@@ -743,9 +715,8 @@ describe('Dashboard', () => {
currentDashboard,
});
- return wrapper.vm.$nextTick().then(() => {
- expect(document.title).toBe(originalTitle);
- });
+ await nextTick();
+ expect(document.title).toBe(originalTitle);
});
});
@@ -756,14 +727,13 @@ describe('Dashboard', () => {
const getClipboardTextFirstPanel = () =>
wrapper.findAll(DashboardPanel).at(panelIndex).props('clipboardText');
- beforeEach(() => {
+ beforeEach(async () => {
setupStoreWithData(store);
store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
currentDashboard,
});
createShallowWrapper({ hasMetrics: true });
-
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('contains a link to the dashboard', () => {
@@ -785,7 +755,7 @@ describe('Dashboard', () => {
// the dashboard panels have a ref attribute set.
const getDashboardPanel = () => wrapper.find({ ref: panelRef });
- beforeEach(() => {
+ beforeEach(async () => {
setupStoreWithData(store);
store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
currentDashboard,
@@ -795,8 +765,7 @@ describe('Dashboard', () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ hoveredPanel: panelRef });
-
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('contains a ref attribute inside a DashboardPanel component', () => {
diff --git a/spec/frontend/monitoring/components/dashboard_url_time_spec.js b/spec/frontend/monitoring/components/dashboard_url_time_spec.js
index e6785f34597..246dd598d19 100644
--- a/spec/frontend/monitoring/components/dashboard_url_time_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_url_time_spec.js
@@ -1,5 +1,6 @@
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+import { nextTick } from 'vue';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import {
@@ -51,23 +52,22 @@ describe('dashboard invalid url parameters', () => {
queryToObject.mockReset();
});
- it('passes default url parameters to the time range picker', () => {
+ it('passes default url parameters to the time range picker', async () => {
queryToObject.mockReturnValue({});
createMountedWrapper();
- return wrapper.vm.$nextTick().then(() => {
- expect(findDateTimePicker().props('value')).toEqual(defaultTimeRange);
+ await nextTick();
+ expect(findDateTimePicker().props('value')).toEqual(defaultTimeRange);
- expect(store.dispatch).toHaveBeenCalledWith(
- 'monitoringDashboard/setTimeRange',
- expect.any(Object),
- );
- expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/fetchData', undefined);
- });
+ expect(store.dispatch).toHaveBeenCalledWith(
+ 'monitoringDashboard/setTimeRange',
+ expect.any(Object),
+ );
+ expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/fetchData', undefined);
});
- it('passes a fixed time range in the URL to the time range picker', () => {
+ it('passes a fixed time range in the URL to the time range picker', async () => {
const params = {
start: '2019-01-01T00:00:00.000Z',
end: '2019-01-10T00:00:00.000Z',
@@ -77,37 +77,35 @@ describe('dashboard invalid url parameters', () => {
createMountedWrapper();
- return wrapper.vm.$nextTick().then(() => {
- expect(findDateTimePicker().props('value')).toEqual(params);
+ await nextTick();
+ expect(findDateTimePicker().props('value')).toEqual(params);
- expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/setTimeRange', params);
- expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/fetchData', undefined);
- });
+ expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/setTimeRange', params);
+ expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/fetchData', undefined);
});
- it('passes a rolling time range in the URL to the time range picker', () => {
+ it('passes a rolling time range in the URL to the time range picker', async () => {
queryToObject.mockReturnValue({
duration_seconds: '120',
});
createMountedWrapper();
- return wrapper.vm.$nextTick().then(() => {
- const expectedTimeRange = {
- duration: { seconds: 60 * 2 },
- };
+ await nextTick();
+ const expectedTimeRange = {
+ duration: { seconds: 60 * 2 },
+ };
- expect(findDateTimePicker().props('value')).toMatchObject(expectedTimeRange);
+ expect(findDateTimePicker().props('value')).toMatchObject(expectedTimeRange);
- expect(store.dispatch).toHaveBeenCalledWith(
- 'monitoringDashboard/setTimeRange',
- expectedTimeRange,
- );
- expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/fetchData', undefined);
- });
+ expect(store.dispatch).toHaveBeenCalledWith(
+ 'monitoringDashboard/setTimeRange',
+ expectedTimeRange,
+ );
+ expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/fetchData', undefined);
});
- it('shows an error message and loads a default time range if invalid url parameters are passed', () => {
+ it('shows an error message and loads a default time range if invalid url parameters are passed', async () => {
queryToObject.mockReturnValue({
start: '<script>alert("XSS")</script>',
end: '<script>alert("XSS")</script>',
@@ -115,37 +113,35 @@ describe('dashboard invalid url parameters', () => {
createMountedWrapper();
- return wrapper.vm.$nextTick().then(() => {
- expect(createFlash).toHaveBeenCalled();
+ await nextTick();
+ expect(createFlash).toHaveBeenCalled();
- expect(findDateTimePicker().props('value')).toEqual(defaultTimeRange);
+ expect(findDateTimePicker().props('value')).toEqual(defaultTimeRange);
- expect(store.dispatch).toHaveBeenCalledWith(
- 'monitoringDashboard/setTimeRange',
- defaultTimeRange,
- );
- expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/fetchData', undefined);
- });
+ expect(store.dispatch).toHaveBeenCalledWith(
+ 'monitoringDashboard/setTimeRange',
+ defaultTimeRange,
+ );
+ expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/fetchData', undefined);
});
- it('redirects to different time range', () => {
+ it('redirects to different time range', async () => {
const toUrl = `${mockProjectDir}/-/environments/1/metrics`;
removeParams.mockReturnValueOnce(toUrl);
createMountedWrapper();
- return wrapper.vm.$nextTick().then(() => {
- findDateTimePicker().vm.$emit('input', {
- duration: { seconds: 120 },
- });
-
- // redirect to with new parameters
- expect(mergeUrlParams).toHaveBeenCalledWith({ duration_seconds: '120' }, toUrl);
- expect(redirectTo).toHaveBeenCalledTimes(1);
+ await nextTick();
+ findDateTimePicker().vm.$emit('input', {
+ duration: { seconds: 120 },
});
+
+ // redirect to with new parameters
+ expect(mergeUrlParams).toHaveBeenCalledWith({ duration_seconds: '120' }, toUrl);
+ expect(redirectTo).toHaveBeenCalledTimes(1);
});
- it('changes the url when a panel moves the time slider', () => {
+ it('changes the url when a panel moves the time slider', async () => {
const timeRange = {
start: '2020-01-01T00:00:00.000Z',
end: '2020-01-01T01:00:00.000Z',
@@ -155,12 +151,11 @@ describe('dashboard invalid url parameters', () => {
createMountedWrapper();
- return wrapper.vm.$nextTick().then(() => {
- wrapper.vm.onTimeRangeZoom(timeRange);
+ await nextTick();
+ wrapper.vm.onTimeRangeZoom(timeRange);
- expect(updateHistory).toHaveBeenCalled();
- expect(wrapper.vm.selectedTimeRange.start.toString()).toBe(timeRange.start);
- expect(wrapper.vm.selectedTimeRange.end.toString()).toBe(timeRange.end);
- });
+ expect(updateHistory).toHaveBeenCalled();
+ expect(wrapper.vm.selectedTimeRange.start.toString()).toBe(timeRange.start);
+ expect(wrapper.vm.selectedTimeRange.end.toString()).toBe(timeRange.end);
});
});
diff --git a/spec/frontend/monitoring/components/embeds/embed_group_spec.js b/spec/frontend/monitoring/components/embeds/embed_group_spec.js
index 79b223d96e4..47366b345a8 100644
--- a/spec/frontend/monitoring/components/embeds/embed_group_spec.js
+++ b/spec/frontend/monitoring/components/embeds/embed_group_spec.js
@@ -1,5 +1,6 @@
import { GlButton, GlCard } from '@gitlab/ui';
-import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
+import { mount, shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { TEST_HOST } from 'helpers/test_constants';
import EmbedGroup from '~/monitoring/components/embeds/embed_group.vue';
@@ -12,8 +13,7 @@ import {
multipleEmbedProps,
} from './mock_data';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('Embed Group', () => {
let wrapper;
@@ -23,7 +23,6 @@ describe('Embed Group', () => {
function mountComponent({ urls = [TEST_HOST], shallow = true, stubs } = {}) {
const mountMethod = shallow ? shallowMount : mount;
wrapper = mountMethod(EmbedGroup, {
- localVue,
store,
propsData: {
urls,
@@ -76,16 +75,14 @@ describe('Embed Group', () => {
expect(wrapper.find('.gl-card-body').classes()).not.toContain('d-none');
});
- it('collapses when clicked', (done) => {
+ it('collapses when clicked', async () => {
metricsWithDataGetter.mockReturnValue([1]);
mountComponent({ shallow: false, stubs: { MetricEmbed: true } });
wrapper.find(GlButton).trigger('click');
- wrapper.vm.$nextTick(() => {
- expect(wrapper.find('.gl-card-body').classes()).toContain('d-none');
- done();
- });
+ await nextTick();
+ expect(wrapper.find('.gl-card-body').classes()).toContain('d-none');
});
});
diff --git a/spec/frontend/monitoring/components/embeds/metric_embed_spec.js b/spec/frontend/monitoring/components/embeds/metric_embed_spec.js
index 90647f50b14..f9f1be4f277 100644
--- a/spec/frontend/monitoring/components/embeds/metric_embed_spec.js
+++ b/spec/frontend/monitoring/components/embeds/metric_embed_spec.js
@@ -1,4 +1,5 @@
-import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import { setHTMLFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'helpers/test_constants';
@@ -6,8 +7,7 @@ import DashboardPanel from '~/monitoring/components/dashboard_panel.vue';
import MetricEmbed from '~/monitoring/components/embeds/metric_embed.vue';
import { groups, initialState, metricsData, metricsWithData } from './mock_data';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('MetricEmbed', () => {
let wrapper;
@@ -17,7 +17,6 @@ describe('MetricEmbed', () => {
function mountComponent() {
wrapper = shallowMount(MetricEmbed, {
- localVue,
store,
propsData: {
dashboardUrl: TEST_HOST,
diff --git a/spec/frontend/monitoring/components/graph_group_spec.js b/spec/frontend/monitoring/components/graph_group_spec.js
index 625dd3f0b33..c5b45564089 100644
--- a/spec/frontend/monitoring/components/graph_group_spec.js
+++ b/spec/frontend/monitoring/components/graph_group_spec.js
@@ -1,5 +1,6 @@
import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import GraphGroup from '~/monitoring/components/graph_group.vue';
describe('Graph group component', () => {
@@ -38,13 +39,12 @@ describe('Graph group component', () => {
expect(findCaretIcon().props('name')).toBe('angle-down');
});
- it('should show the angle-right caret icon when the user collapses the group', () => {
+ it('should show the angle-right caret icon when the user collapses the group', async () => {
findToggleButton().trigger('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(findContent().isVisible()).toBe(false);
- expect(findCaretIcon().props('name')).toBe('angle-right');
- });
+ await nextTick();
+ expect(findContent().isVisible()).toBe(false);
+ expect(findCaretIcon().props('name')).toBe('angle-right');
});
it('should contain a tab index for the collapse button', () => {
@@ -53,15 +53,14 @@ describe('Graph group component', () => {
expect(groupToggle.attributes('tabindex')).toBeDefined();
});
- it('should show the open the group when collapseGroup is set to true', () => {
+ it('should show the open the group when collapseGroup is set to true', async () => {
wrapper.setProps({
collapseGroup: true,
});
- return wrapper.vm.$nextTick().then(() => {
- expect(findContent().isVisible()).toBe(true);
- expect(findCaretIcon().props('name')).toBe('angle-down');
- });
+ await nextTick();
+ expect(findContent().isVisible()).toBe(true);
+ expect(findCaretIcon().props('name')).toBe('angle-down');
});
});
@@ -77,12 +76,11 @@ describe('Graph group component', () => {
expect(findCaretIcon().props('name')).toBe('angle-right');
});
- it('should show the angle-right caret icon when collapseGroup is false', () => {
+ it('should show the angle-right caret icon when collapseGroup is false', async () => {
findToggleButton().trigger('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(findCaretIcon().props('name')).toBe('angle-down');
- });
+ await nextTick();
+ expect(findCaretIcon().props('name')).toBe('angle-down');
});
it('should call collapse the graph group content when enter is pressed on the caret icon', () => {
@@ -137,15 +135,14 @@ describe('Graph group component', () => {
expect(findCaretIcon().exists()).toBe(false);
});
- it('should show the panel content when collapse is set to false', () => {
+ it('should show the panel content when collapse is set to false', async () => {
wrapper.setProps({
collapseGroup: false,
});
- return wrapper.vm.$nextTick().then(() => {
- expect(findContent().isVisible()).toBe(true);
- expect(findCaretIcon().exists()).toBe(false);
- });
+ await nextTick();
+ expect(findContent().isVisible()).toBe(true);
+ expect(findCaretIcon().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/monitoring/components/links_section_spec.js b/spec/frontend/monitoring/components/links_section_spec.js
index e37abf6722a..c9b5aeeecb8 100644
--- a/spec/frontend/monitoring/components/links_section_spec.js
+++ b/spec/frontend/monitoring/components/links_section_spec.js
@@ -36,7 +36,7 @@ describe('Links Section component', () => {
expect(findLinks().length).toBe(0);
});
- it('renders a link inside a section', () => {
+ it('renders a link inside a section', async () => {
setState([
{
title: 'GitLab Website',
@@ -44,23 +44,21 @@ describe('Links Section component', () => {
},
]);
- return wrapper.vm.$nextTick(() => {
- expect(findLinks()).toHaveLength(1);
- const firstLink = findLinks().at(0);
+ await nextTick();
+ expect(findLinks()).toHaveLength(1);
+ const firstLink = findLinks().at(0);
- expect(firstLink.attributes('href')).toBe('https://gitlab.com');
- expect(firstLink.text()).toBe('GitLab Website');
- });
+ expect(firstLink.attributes('href')).toBe('https://gitlab.com');
+ expect(firstLink.text()).toBe('GitLab Website');
});
- it('renders multiple links inside a section', () => {
+ it('renders multiple links inside a section', async () => {
const links = new Array(10)
.fill(null)
.map((_, i) => ({ title: `Title ${i}`, url: `https://gitlab.com/projects/${i}` }));
setState(links);
- return wrapper.vm.$nextTick(() => {
- expect(findLinks()).toHaveLength(10);
- });
+ await nextTick();
+ expect(findLinks()).toHaveLength(10);
});
});
diff --git a/spec/frontend/monitoring/components/refresh_button_spec.js b/spec/frontend/monitoring/components/refresh_button_spec.js
index 248cf32d54b..0e45cc021c5 100644
--- a/spec/frontend/monitoring/components/refresh_button_spec.js
+++ b/spec/frontend/monitoring/components/refresh_button_spec.js
@@ -1,6 +1,7 @@
import { GlDropdown, GlDropdownItem, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Visibility from 'visibilityjs';
+import { nextTick } from 'vue';
import RefreshButton from '~/monitoring/components/refresh_button.vue';
import { createStore } from '~/monitoring/stores';
@@ -79,9 +80,9 @@ describe('RefreshButton', () => {
describe('when a refresh rate is chosen', () => {
const optIndex = 2; // Other option than "Off"
- beforeEach(() => {
+ beforeEach(async () => {
findOptionAt(optIndex).vm.$emit('click');
- return wrapper.vm.$nextTick;
+ await nextTick();
});
it('refresh rate appears in the dropdown', () => {
@@ -101,7 +102,7 @@ describe('RefreshButton', () => {
jest.runOnlyPendingTimers();
expectFetchDataToHaveBeenCalledTimes(2);
- await wrapper.vm.$nextTick();
+ await nextTick();
jest.runOnlyPendingTimers();
expectFetchDataToHaveBeenCalledTimes(3);
@@ -113,7 +114,7 @@ describe('RefreshButton', () => {
jest.runOnlyPendingTimers();
expectFetchDataToHaveBeenCalledTimes(1);
- await wrapper.vm.$nextTick();
+ await nextTick();
jest.runOnlyPendingTimers();
expectFetchDataToHaveBeenCalledTimes(1);
@@ -128,9 +129,9 @@ describe('RefreshButton', () => {
});
describe('when "Off" refresh rate is chosen', () => {
- beforeEach(() => {
+ beforeEach(async () => {
findOptionAt(0).vm.$emit('click');
- return wrapper.vm.$nextTick;
+ await nextTick();
});
it('refresh rate is "Off" in the dropdown', () => {
diff --git a/spec/frontend/monitoring/components/variables/dropdown_field_spec.js b/spec/frontend/monitoring/components/variables/dropdown_field_spec.js
index f5ee32e78e6..643bbb39f04 100644
--- a/spec/frontend/monitoring/components/variables/dropdown_field_spec.js
+++ b/spec/frontend/monitoring/components/variables/dropdown_field_spec.js
@@ -1,5 +1,6 @@
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import DropdownField from '~/monitoring/components/variables/dropdown_field.vue';
describe('Custom variable component', () => {
@@ -53,14 +54,13 @@ describe('Custom variable component', () => {
expect(findDropdown().exists()).toBe(true);
});
- it('changing dropdown items triggers update', () => {
+ it('changing dropdown items triggers update', async () => {
createShallowWrapper();
jest.spyOn(wrapper.vm, '$emit');
findDropdownItems().at(1).vm.$emit('click');
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('input', 'canary');
- });
+ await nextTick();
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('input', 'canary');
});
});
diff --git a/spec/frontend/monitoring/components/variables/text_field_spec.js b/spec/frontend/monitoring/components/variables/text_field_spec.js
index c879803fddd..3073b3968aa 100644
--- a/spec/frontend/monitoring/components/variables/text_field_spec.js
+++ b/spec/frontend/monitoring/components/variables/text_field_spec.js
@@ -1,5 +1,6 @@
import { GlFormInput } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import TextField from '~/monitoring/components/variables/text_field.vue';
describe('Text variable component', () => {
@@ -23,15 +24,14 @@ describe('Text variable component', () => {
expect(findInput().exists()).toBe(true);
});
- it('always has a default value', () => {
+ it('always has a default value', async () => {
createShallowWrapper();
- return wrapper.vm.$nextTick(() => {
- expect(findInput().attributes('value')).toBe(propsData.value);
- });
+ await nextTick();
+ expect(findInput().attributes('value')).toBe(propsData.value);
});
- it('triggers keyup enter', () => {
+ it('triggers keyup enter', async () => {
createShallowWrapper();
jest.spyOn(wrapper.vm, '$emit');
@@ -39,12 +39,11 @@ describe('Text variable component', () => {
findInput().trigger('input');
findInput().trigger('keyup.enter');
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('input', 'prod-pod');
- });
+ await nextTick();
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('input', 'prod-pod');
});
- it('triggers blur enter', () => {
+ it('triggers blur enter', async () => {
createShallowWrapper();
jest.spyOn(wrapper.vm, '$emit');
@@ -52,8 +51,7 @@ describe('Text variable component', () => {
findInput().trigger('input');
findInput().trigger('blur');
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('input', 'canary-pod');
- });
+ await nextTick();
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('input', 'canary-pod');
});
});
diff --git a/spec/frontend/monitoring/components/variables_section_spec.js b/spec/frontend/monitoring/components/variables_section_spec.js
index 6157de0dafe..64b93bd3027 100644
--- a/spec/frontend/monitoring/components/variables_section_spec.js
+++ b/spec/frontend/monitoring/components/variables_section_spec.js
@@ -1,5 +1,6 @@
import { shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
+import { nextTick } from 'vue';
import { updateHistory, mergeUrlParams } from '~/lib/utils/url_utility';
import DropdownField from '~/monitoring/components/variables/dropdown_field.vue';
import TextField from '~/monitoring/components/variables/text_field.vue';
@@ -40,11 +41,11 @@ describe('Metrics dashboard/variables section component', () => {
});
describe('when variables are set', () => {
- beforeEach(() => {
+ beforeEach(async () => {
store.state.monitoringDashboard.variables = storeVariables;
createShallowWrapper();
- return wrapper.vm.$nextTick;
+ await nextTick();
});
it('shows the variables section', () => {
@@ -83,34 +84,32 @@ describe('Metrics dashboard/variables section component', () => {
createShallowWrapper();
});
- it('merges the url params and refreshes the dashboard when a text-based variables inputs are updated', () => {
+ it('merges the url params and refreshes the dashboard when a text-based variables inputs are updated', async () => {
const firstInput = findTextInputs().at(0);
firstInput.vm.$emit('input', 'test');
- return wrapper.vm.$nextTick(() => {
- expect(updateVariablesAndFetchData).toHaveBeenCalled();
- expect(mergeUrlParams).toHaveBeenCalledWith(
- convertVariablesForURL(storeVariables),
- window.location.href,
- );
- expect(updateHistory).toHaveBeenCalled();
- });
+ await nextTick();
+ expect(updateVariablesAndFetchData).toHaveBeenCalled();
+ expect(mergeUrlParams).toHaveBeenCalledWith(
+ convertVariablesForURL(storeVariables),
+ window.location.href,
+ );
+ expect(updateHistory).toHaveBeenCalled();
});
- it('merges the url params and refreshes the dashboard when a custom-based variables inputs are updated', () => {
+ it('merges the url params and refreshes the dashboard when a custom-based variables inputs are updated', async () => {
const firstInput = findCustomInputs().at(0);
firstInput.vm.$emit('input', 'test');
- return wrapper.vm.$nextTick(() => {
- expect(updateVariablesAndFetchData).toHaveBeenCalled();
- expect(mergeUrlParams).toHaveBeenCalledWith(
- convertVariablesForURL(storeVariables),
- window.location.href,
- );
- expect(updateHistory).toHaveBeenCalled();
- });
+ await nextTick();
+ expect(updateVariablesAndFetchData).toHaveBeenCalled();
+ expect(mergeUrlParams).toHaveBeenCalledWith(
+ convertVariablesForURL(storeVariables),
+ window.location.href,
+ );
+ expect(updateHistory).toHaveBeenCalled();
});
it('does not merge the url params and refreshes the dashboard if the value entered is not different that is what currently stored', () => {
diff --git a/spec/frontend/monitoring/router_spec.js b/spec/frontend/monitoring/router_spec.js
index b027d60f61e..7758dd351b7 100644
--- a/spec/frontend/monitoring/router_spec.js
+++ b/spec/frontend/monitoring/router_spec.js
@@ -1,4 +1,5 @@
-import { mount, createLocalVue } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
+import Vue from 'vue';
import VueRouter from 'vue-router';
import Dashboard from '~/monitoring/components/dashboard.vue';
import DashboardPage from '~/monitoring/pages/dashboard_page.vue';
@@ -25,8 +26,7 @@ describe('Monitoring router', () => {
let store;
const createWrapper = (basePath, routeArg) => {
- const localVue = createLocalVue();
- localVue.use(VueRouter);
+ Vue.use(VueRouter);
router = createRouter(basePath);
if (routeArg !== undefined) {
@@ -34,7 +34,6 @@ describe('Monitoring router', () => {
}
return mount(MockApp, {
- localVue,
store,
router,
});
diff --git a/spec/frontend/mr_popover/mr_popover_spec.js b/spec/frontend/mr_popover/mr_popover_spec.js
index 36ad82e93a5..23f97073e9e 100644
--- a/spec/frontend/mr_popover/mr_popover_spec.js
+++ b/spec/frontend/mr_popover/mr_popover_spec.js
@@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import MRPopover from '~/mr_popover/components/mr_popover.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
@@ -25,16 +26,15 @@ describe('MR Popover', () => {
});
});
- it('shows skeleton-loader while apollo is loading', () => {
+ it('shows skeleton-loader while apollo is loading', async () => {
wrapper.vm.$apollo.queries.mergeRequest.loading = true;
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.element).toMatchSnapshot();
- });
+ await nextTick();
+ expect(wrapper.element).toMatchSnapshot();
});
describe('loaded state', () => {
- it('matches the snapshot', () => {
+ it('matches the snapshot', async () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({
@@ -51,12 +51,11 @@ describe('MR Popover', () => {
},
});
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.element).toMatchSnapshot();
- });
+ await nextTick();
+ expect(wrapper.element).toMatchSnapshot();
});
- it('does not show CI Icon if there is no pipeline data', () => {
+ it('does not show CI Icon if there is no pipeline data', async () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({
@@ -69,15 +68,13 @@ describe('MR Popover', () => {
},
});
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(CiIcon).exists()).toBe(false);
- });
+ await nextTick();
+ expect(wrapper.find(CiIcon).exists()).toBe(false);
});
- it('falls back to cached MR title when request fails', () => {
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.text()).toContain('MR Title');
- });
+ it('falls back to cached MR title when request fails', async () => {
+ await nextTick();
+ expect(wrapper.text()).toContain('MR Title');
});
});
});
diff --git a/spec/frontend/nav/components/responsive_app_spec.js b/spec/frontend/nav/components/responsive_app_spec.js
index 4af8c6020bc..76b8ebdc92f 100644
--- a/spec/frontend/nav/components/responsive_app_spec.js
+++ b/spec/frontend/nav/components/responsive_app_spec.js
@@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import ResponsiveApp from '~/nav/components/responsive_app.vue';
import ResponsiveHeader from '~/nav/components/responsive_header.vue';
import ResponsiveHome from '~/nav/components/responsive_home.vue';
@@ -62,7 +63,7 @@ describe('~/nav/components/responsive_app.vue', () => {
wrapper.vm.$root.$emit(evt);
- await wrapper.vm.$nextTick();
+ await nextTick();
}, Promise.resolve());
expect(hasMobileOverlayVisible()).toBe(expectation);
@@ -97,7 +98,7 @@ describe('~/nav/components/responsive_app.vue', () => {
findHome().vm.$emit('menu-item-click', { view });
- await wrapper.vm.$nextTick();
+ await nextTick();
});
it('shows header', () => {
diff --git a/spec/frontend/nav/components/top_nav_menu_item_spec.js b/spec/frontend/nav/components/top_nav_menu_item_spec.js
index 71154e18915..a7430d8c73f 100644
--- a/spec/frontend/nav/components/top_nav_menu_item_spec.js
+++ b/spec/frontend/nav/components/top_nav_menu_item_spec.js
@@ -137,6 +137,7 @@ describe('~/nav/components/top_nav_menu_item.vue', () => {
expect(wrapper.classes()).toStrictEqual([
'top-nav-menu-item',
'gl-display-block',
+ 'gl-pr-3!',
...expectedClasses,
]);
});
diff --git a/spec/frontend/notebook/cells/code_spec.js b/spec/frontend/notebook/cells/code_spec.js
index 669bdc2f89a..9a2db061278 100644
--- a/spec/frontend/notebook/cells/code_spec.js
+++ b/spec/frontend/notebook/cells/code_spec.js
@@ -1,4 +1,4 @@
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import fixture from 'test_fixtures/blob/notebook/basic.json';
import CodeComponent from '~/notebook/cells/code.vue';
@@ -25,12 +25,10 @@ describe('Code component', () => {
};
describe('without output', () => {
- beforeEach((done) => {
+ beforeEach(() => {
vm = setupComponent(json.cells[0]);
- setImmediate(() => {
- done();
- });
+ return nextTick();
});
it('does not render output prompt', () => {
@@ -39,12 +37,10 @@ describe('Code component', () => {
});
describe('with output', () => {
- beforeEach((done) => {
+ beforeEach(() => {
vm = setupComponent(json.cells[2]);
- setImmediate(() => {
- done();
- });
+ return nextTick();
});
it('does not render output prompt', () => {
@@ -58,12 +54,12 @@ describe('Code component', () => {
describe('with string for output', () => {
// NBFormat Version 4.1 allows outputs.text to be a string
- beforeEach(() => {
+ beforeEach(async () => {
const cell = json.cells[2];
cell.outputs[0].text = cell.outputs[0].text.join('');
vm = setupComponent(cell);
- return vm.$nextTick();
+ await nextTick();
});
it('does not render output prompt', () => {
@@ -76,12 +72,12 @@ describe('Code component', () => {
});
describe('with string for cell.source', () => {
- beforeEach(() => {
+ beforeEach(async () => {
const cell = json.cells[0];
cell.source = cell.source.join('');
vm = setupComponent(cell);
- return vm.$nextTick();
+ await nextTick();
});
it('renders the same input as when cell.source is an array', () => {
diff --git a/spec/frontend/notebook/cells/markdown_spec.js b/spec/frontend/notebook/cells/markdown_spec.js
index 36b1e91f15f..7dc6f90d202 100644
--- a/spec/frontend/notebook/cells/markdown_spec.js
+++ b/spec/frontend/notebook/cells/markdown_spec.js
@@ -1,6 +1,6 @@
import { mount } from '@vue/test-utils';
import katex from 'katex';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import markdownTableJson from 'test_fixtures/blob/notebook/markdown-table.json';
import basicJson from 'test_fixtures/blob/notebook/basic.json';
import mathJson from 'test_fixtures/blob/notebook/math.json';
@@ -37,7 +37,7 @@ describe('Markdown component', () => {
let cell;
let json;
- beforeEach(() => {
+ beforeEach(async () => {
json = basicJson;
// eslint-disable-next-line prefer-destructuring
@@ -45,7 +45,7 @@ describe('Markdown component', () => {
vm = buildCellComponent(cell);
- return vm.$nextTick();
+ await nextTick();
});
it('does not render prompt', () => {
@@ -67,7 +67,7 @@ describe('Markdown component', () => {
],
});
- await vm.$nextTick();
+ await nextTick();
expect(vm.$el.querySelector('a').getAttribute('href')).toBeNull();
});
@@ -77,7 +77,7 @@ describe('Markdown component', () => {
source: ['<a href="test.js" data-remote=true data-type="script" class="xss-link">XSS</a>\n'],
});
- await vm.$nextTick();
+ await nextTick();
expect(findLink().getAttribute('data-remote')).toBe(null);
expect(findLink().getAttribute('data-type')).toBe(null);
});
@@ -99,7 +99,7 @@ describe('Markdown component', () => {
])('%s', async ([testMd, mustContain]) => {
vm = buildMarkdownComponent([testMd], '/raw/');
- await vm.$nextTick();
+ await nextTick();
expect(vm.$el.innerHTML).toContain(mustContain);
});
@@ -110,29 +110,28 @@ describe('Markdown component', () => {
json = markdownTableJson;
});
- it('renders images and text', () => {
+ it('renders images and text', async () => {
vm = buildCellComponent(json.cells[0]);
- return vm.$nextTick().then(() => {
- const images = vm.$el.querySelectorAll('img');
- expect(images.length).toBe(5);
-
- const columns = vm.$el.querySelectorAll('td');
- expect(columns.length).toBe(6);
-
- expect(columns[0].textContent).toEqual('Hello ');
- expect(columns[1].textContent).toEqual('Test ');
- expect(columns[2].textContent).toEqual('World ');
- expect(columns[3].textContent).toEqual('Fake ');
- expect(columns[4].textContent).toEqual('External image: ');
- expect(columns[5].textContent).toEqual('Empty');
-
- expect(columns[0].innerHTML).toContain('<img src="data:image/jpeg;base64');
- expect(columns[1].innerHTML).toContain('<img src="data:image/png;base64');
- expect(columns[2].innerHTML).toContain('<img src="data:image/jpeg;base64');
- expect(columns[3].innerHTML).toContain('<img>');
- expect(columns[4].innerHTML).toContain('<img src="https://www.google.com/');
- });
+ await nextTick();
+ const images = vm.$el.querySelectorAll('img');
+ expect(images.length).toBe(5);
+
+ const columns = vm.$el.querySelectorAll('td');
+ expect(columns.length).toBe(6);
+
+ expect(columns[0].textContent).toEqual('Hello ');
+ expect(columns[1].textContent).toEqual('Test ');
+ expect(columns[2].textContent).toEqual('World ');
+ expect(columns[3].textContent).toEqual('Fake ');
+ expect(columns[4].textContent).toEqual('External image: ');
+ expect(columns[5].textContent).toEqual('Empty');
+
+ expect(columns[0].innerHTML).toContain('<img src="data:image/jpeg;base64');
+ expect(columns[1].innerHTML).toContain('<img src="data:image/png;base64');
+ expect(columns[2].innerHTML).toContain('<img src="data:image/jpeg;base64');
+ expect(columns[3].innerHTML).toContain('<img>');
+ expect(columns[4].innerHTML).toContain('<img src="https://www.google.com/');
});
});
@@ -144,28 +143,28 @@ describe('Markdown component', () => {
it('renders multi-line katex', async () => {
vm = buildCellComponent(json.cells[0]);
- await vm.$nextTick();
+ await nextTick();
expect(vm.$el.querySelector('.katex')).not.toBeNull();
});
it('renders inline katex', async () => {
vm = buildCellComponent(json.cells[1]);
- await vm.$nextTick();
+ await nextTick();
expect(vm.$el.querySelector('p:first-child .katex')).not.toBeNull();
});
it('renders multiple inline katex', async () => {
vm = buildCellComponent(json.cells[1]);
- await vm.$nextTick();
+ await nextTick();
expect(vm.$el.querySelectorAll('p:nth-child(2) .katex')).toHaveLength(4);
});
it('output cell in case of katex error', async () => {
vm = buildMarkdownComponent(['Some invalid $a & b$ inline formula $b & c$\n', '\n']);
- await vm.$nextTick();
+ await nextTick();
// expect one paragraph with no katex formula in it
expect(vm.$el.querySelectorAll('p')).toHaveLength(1);
expect(vm.$el.querySelectorAll('p .katex')).toHaveLength(0);
@@ -177,7 +176,7 @@ describe('Markdown component', () => {
'\n',
]);
- await vm.$nextTick();
+ await nextTick();
// expect one paragraph with no katex formula in it
expect(vm.$el.querySelectorAll('p')).toHaveLength(1);
expect(vm.$el.querySelectorAll('p .katex')).toHaveLength(1);
@@ -186,7 +185,7 @@ describe('Markdown component', () => {
it('renders math formula in list object', async () => {
vm = buildMarkdownComponent(["- list with inline $a=2$ inline formula $a' + b = c$\n", '\n']);
- await vm.$nextTick();
+ await nextTick();
// expect one list with a katex formula in it
expect(vm.$el.querySelectorAll('li')).toHaveLength(1);
expect(vm.$el.querySelectorAll('li .katex')).toHaveLength(2);
@@ -195,7 +194,7 @@ describe('Markdown component', () => {
it("renders math formula with tick ' in it", async () => {
vm = buildMarkdownComponent(["- list with inline $a=2$ inline formula $a' + b = c$\n", '\n']);
- await vm.$nextTick();
+ await nextTick();
// expect one list with a katex formula in it
expect(vm.$el.querySelectorAll('li')).toHaveLength(1);
expect(vm.$el.querySelectorAll('li .katex')).toHaveLength(2);
@@ -204,7 +203,7 @@ describe('Markdown component', () => {
it('renders math formula with less-than-operator < in it', async () => {
vm = buildMarkdownComponent(['- list with inline $a=2$ inline formula $a + b < c$\n', '\n']);
- await vm.$nextTick();
+ await nextTick();
// expect one list with a katex formula in it
expect(vm.$el.querySelectorAll('li')).toHaveLength(1);
expect(vm.$el.querySelectorAll('li .katex')).toHaveLength(2);
@@ -213,7 +212,7 @@ describe('Markdown component', () => {
it('renders math formula with greater-than-operator > in it', async () => {
vm = buildMarkdownComponent(['- list with inline $a=2$ inline formula $a + b > c$\n', '\n']);
- await vm.$nextTick();
+ await nextTick();
// expect one list with a katex formula in it
expect(vm.$el.querySelectorAll('li')).toHaveLength(1);
expect(vm.$el.querySelectorAll('li .katex')).toHaveLength(2);
diff --git a/spec/frontend/notebook/cells/output/index_spec.js b/spec/frontend/notebook/cells/output/index_spec.js
index 7ece73d375c..8e04e4c146c 100644
--- a/spec/frontend/notebook/cells/output/index_spec.js
+++ b/spec/frontend/notebook/cells/output/index_spec.js
@@ -1,4 +1,4 @@
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import json from 'test_fixtures/blob/notebook/basic.json';
import CodeComponent from '~/notebook/cells/output/index.vue';
@@ -18,13 +18,11 @@ describe('Output component', () => {
};
describe('text output', () => {
- beforeEach((done) => {
+ beforeEach(() => {
const textType = json.cells[2];
createComponent(textType.outputs[0]);
- setImmediate(() => {
- done();
- });
+ return nextTick();
});
it('renders as plain text', () => {
@@ -37,13 +35,11 @@ describe('Output component', () => {
});
describe('image output', () => {
- beforeEach((done) => {
+ beforeEach(() => {
const imageType = json.cells[3];
createComponent(imageType.outputs[0]);
- setImmediate(() => {
- done();
- });
+ return nextTick();
});
it('renders as an image', () => {
@@ -86,13 +82,11 @@ describe('Output component', () => {
});
describe('svg output', () => {
- beforeEach((done) => {
+ beforeEach(() => {
const svgType = json.cells[5];
createComponent(svgType.outputs[0]);
- setImmediate(() => {
- done();
- });
+ return nextTick();
});
it('renders as an svg', () => {
@@ -101,13 +95,11 @@ describe('Output component', () => {
});
describe('default to plain text', () => {
- beforeEach((done) => {
+ beforeEach(() => {
const unknownType = json.cells[6];
createComponent(unknownType.outputs[0]);
- setImmediate(() => {
- done();
- });
+ return nextTick();
});
it('renders as plain text', () => {
@@ -119,16 +111,14 @@ describe('Output component', () => {
expect(vm.$el.querySelector('.prompt span')).not.toBeNull();
});
- it("renders as plain text when doesn't recognise other types", (done) => {
+ it("renders as plain text when doesn't recognise other types", async () => {
const unknownType = json.cells[7];
createComponent(unknownType.outputs[0]);
- setImmediate(() => {
- expect(vm.$el.querySelector('pre')).not.toBeNull();
- expect(vm.$el.textContent.trim()).toContain('testing');
+ await nextTick();
- done();
- });
+ expect(vm.$el.querySelector('pre')).not.toBeNull();
+ expect(vm.$el.textContent.trim()).toContain('testing');
});
});
});
diff --git a/spec/frontend/notebook/cells/prompt_spec.js b/spec/frontend/notebook/cells/prompt_spec.js
index 8cdcd1f84de..89b2d7b2b90 100644
--- a/spec/frontend/notebook/cells/prompt_spec.js
+++ b/spec/frontend/notebook/cells/prompt_spec.js
@@ -1,4 +1,4 @@
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import PromptComponent from '~/notebook/cells/prompt.vue';
const Component = Vue.extend(PromptComponent);
@@ -7,7 +7,7 @@ describe('Prompt component', () => {
let vm;
describe('input', () => {
- beforeEach((done) => {
+ beforeEach(() => {
vm = new Component({
propsData: {
type: 'In',
@@ -16,9 +16,7 @@ describe('Prompt component', () => {
});
vm.$mount();
- setImmediate(() => {
- done();
- });
+ return nextTick();
});
it('renders in label', () => {
@@ -31,7 +29,7 @@ describe('Prompt component', () => {
});
describe('output', () => {
- beforeEach((done) => {
+ beforeEach(() => {
vm = new Component({
propsData: {
type: 'Out',
@@ -40,9 +38,7 @@ describe('Prompt component', () => {
});
vm.$mount();
- setImmediate(() => {
- done();
- });
+ return nextTick();
});
it('renders in label', () => {
diff --git a/spec/frontend/notebook/index_spec.js b/spec/frontend/notebook/index_spec.js
index cd531d628b3..475c41a72f6 100644
--- a/spec/frontend/notebook/index_spec.js
+++ b/spec/frontend/notebook/index_spec.js
@@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import json from 'test_fixtures/blob/notebook/basic.json';
import jsonWithWorksheet from 'test_fixtures/blob/notebook/worksheets.json';
import Notebook from '~/notebook/index.vue';
@@ -17,12 +17,10 @@ describe('Notebook component', () => {
}
describe('without JSON', () => {
- beforeEach((done) => {
+ beforeEach(() => {
vm = buildComponent({});
- setImmediate(() => {
- done();
- });
+ return nextTick();
});
it('does not render', () => {
@@ -31,12 +29,10 @@ describe('Notebook component', () => {
});
describe('with JSON', () => {
- beforeEach((done) => {
+ beforeEach(() => {
vm = buildComponent(json);
- setImmediate(() => {
- done();
- });
+ return nextTick();
});
it('renders cells', () => {
@@ -57,12 +53,10 @@ describe('Notebook component', () => {
});
describe('with worksheets', () => {
- beforeEach((done) => {
+ beforeEach(() => {
vm = buildComponent(jsonWithWorksheet);
- setImmediate(() => {
- done();
- });
+ return nextTick();
});
it('renders cells', () => {
diff --git a/spec/frontend/notes/components/comment_form_spec.js b/spec/frontend/notes/components/comment_form_spec.js
index 16dbf60cef4..a605edc4357 100644
--- a/spec/frontend/notes/components/comment_form_spec.js
+++ b/spec/frontend/notes/components/comment_form_spec.js
@@ -20,7 +20,6 @@ import { loggedOutnoteableData, notesDataMock, userDataMock, noteableDataMock }
jest.mock('autosize');
jest.mock('~/commons/nav/user_merge_requests');
jest.mock('~/flash');
-jest.mock('~/gl_form');
Vue.use(Vuex);
@@ -466,8 +465,8 @@ describe('issue_comment_form component', () => {
await findCloseReopenButton().trigger('click');
- await wrapper.vm.$nextTick;
- await wrapper.vm.$nextTick;
+ await nextTick;
+ await nextTick;
expect(createFlash).toHaveBeenCalledWith({
message: `Something went wrong while closing the ${type}. Please try again later.`,
@@ -502,8 +501,8 @@ describe('issue_comment_form component', () => {
await findCloseReopenButton().trigger('click');
- await wrapper.vm.$nextTick;
- await wrapper.vm.$nextTick;
+ await nextTick;
+ await nextTick;
expect(createFlash).toHaveBeenCalledWith({
message: `Something went wrong while reopening the ${type}. Please try again later.`,
@@ -521,7 +520,7 @@ describe('issue_comment_form component', () => {
await findCloseReopenButton().trigger('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(refreshUserMergeRequestCounts).toHaveBeenCalled();
});
@@ -581,7 +580,7 @@ describe('issue_comment_form component', () => {
// check checkbox
checkbox.element.checked = shouldCheckboxBeChecked;
checkbox.trigger('change');
- await wrapper.vm.$nextTick();
+ await nextTick();
// submit comment
findCommentButton().trigger('click');
diff --git a/spec/frontend/notes/components/diff_discussion_header_spec.js b/spec/frontend/notes/components/diff_discussion_header_spec.js
index fa34a5e8d39..9f94dd693cb 100644
--- a/spec/frontend/notes/components/diff_discussion_header_spec.js
+++ b/spec/frontend/notes/components/diff_discussion_header_spec.js
@@ -1,5 +1,6 @@
import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import diffDiscussionHeader from '~/notes/components/diff_discussion_header.vue';
import createStore from '~/notes/stores';
@@ -24,16 +25,15 @@ describe('diff_discussion_header component', () => {
wrapper.destroy();
});
- it('should render user avatar', () => {
+ it('should render user avatar', async () => {
const discussion = { ...discussionMock };
discussion.diff_file = mockDiffFile;
discussion.diff_discussion = true;
wrapper.setProps({ discussion });
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find('.user-avatar-link').exists()).toBe(true);
- });
+ await nextTick();
+ expect(wrapper.find('.user-avatar-link').exists()).toBe(true);
});
describe('action text', () => {
@@ -41,7 +41,7 @@ describe('diff_discussion_header component', () => {
const truncatedCommitId = commitId.substr(0, 8);
let commitElement;
- beforeEach((done) => {
+ beforeEach(async () => {
store.state.diffs = {
projectPath: 'something',
};
@@ -58,41 +58,30 @@ describe('diff_discussion_header component', () => {
},
});
- wrapper.vm
- .$nextTick()
- .then(() => {
- commitElement = wrapper.find('.commit-sha');
- })
- .then(done)
- .catch(done.fail);
+ await nextTick();
+ commitElement = wrapper.find('.commit-sha');
});
describe('for diff threads without a commit id', () => {
- it('should show started a thread on the diff text', (done) => {
+ it('should show started a thread on the diff text', async () => {
Object.assign(wrapper.vm.discussion, {
for_commit: false,
commit_id: null,
});
- wrapper.vm.$nextTick(() => {
- expect(wrapper.text()).toContain('started a thread on the diff');
-
- done();
- });
+ await nextTick();
+ expect(wrapper.text()).toContain('started a thread on the diff');
});
- it('should show thread on older version text', (done) => {
+ it('should show thread on older version text', async () => {
Object.assign(wrapper.vm.discussion, {
for_commit: false,
commit_id: null,
active: false,
});
- wrapper.vm.$nextTick(() => {
- expect(wrapper.text()).toContain('started a thread on an old version of the diff');
-
- done();
- });
+ await nextTick();
+ expect(wrapper.text()).toContain('started a thread on an old version of the diff');
});
});
@@ -105,31 +94,25 @@ describe('diff_discussion_header component', () => {
});
describe('for diff thread with a commit id', () => {
- it('should display started thread on commit header', (done) => {
+ it('should display started thread on commit header', async () => {
wrapper.vm.discussion.for_commit = false;
- wrapper.vm.$nextTick(() => {
- expect(wrapper.text()).toContain(`started a thread on commit ${truncatedCommitId}`);
-
- expect(commitElement).not.toBe(null);
+ await nextTick();
+ expect(wrapper.text()).toContain(`started a thread on commit ${truncatedCommitId}`);
- done();
- });
+ expect(commitElement).not.toBe(null);
});
- it('should display outdated change on commit header', (done) => {
+ it('should display outdated change on commit header', async () => {
wrapper.vm.discussion.for_commit = false;
wrapper.vm.discussion.active = false;
- wrapper.vm.$nextTick(() => {
- expect(wrapper.text()).toContain(
- `started a thread on an outdated change in commit ${truncatedCommitId}`,
- );
+ await nextTick();
+ expect(wrapper.text()).toContain(
+ `started a thread on an outdated change in commit ${truncatedCommitId}`,
+ );
- expect(commitElement).not.toBe(null);
-
- done();
- });
+ expect(commitElement).not.toBe(null);
});
});
});
diff --git a/spec/frontend/notes/components/discussion_counter_spec.js b/spec/frontend/notes/components/discussion_counter_spec.js
index c454d502beb..a856d002d2e 100644
--- a/spec/frontend/notes/components/discussion_counter_spec.js
+++ b/spec/frontend/notes/components/discussion_counter_spec.js
@@ -1,5 +1,6 @@
import { GlButton } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import DiscussionCounter from '~/notes/components/discussion_counter.vue';
import notesModule from '~/notes/stores/modules';
@@ -10,9 +11,8 @@ describe('DiscussionCounter component', () => {
let store;
let wrapper;
let setExpandDiscussionsFn;
- const localVue = createLocalVue();
- localVue.use(Vuex);
+ Vue.use(Vuex);
beforeEach(() => {
window.mrTabs = {};
@@ -45,7 +45,7 @@ describe('DiscussionCounter component', () => {
describe('has no discussions', () => {
it('does not render', () => {
- wrapper = shallowMount(DiscussionCounter, { store, localVue });
+ wrapper = shallowMount(DiscussionCounter, { store });
expect(wrapper.find({ ref: 'discussionCounter' }).exists()).toBe(false);
});
@@ -55,7 +55,7 @@ describe('DiscussionCounter component', () => {
it('does not render', () => {
store.commit(types.ADD_OR_UPDATE_DISCUSSIONS, [{ ...discussionMock, resolvable: false }]);
store.dispatch('updateResolvableDiscussionsCounts');
- wrapper = shallowMount(DiscussionCounter, { store, localVue });
+ wrapper = shallowMount(DiscussionCounter, { store });
expect(wrapper.find({ ref: 'discussionCounter' }).exists()).toBe(false);
});
@@ -75,7 +75,7 @@ describe('DiscussionCounter component', () => {
it('renders', () => {
updateStore();
- wrapper = shallowMount(DiscussionCounter, { store, localVue });
+ wrapper = shallowMount(DiscussionCounter, { store });
expect(wrapper.find({ ref: 'discussionCounter' }).exists()).toBe(true);
});
@@ -86,7 +86,7 @@ describe('DiscussionCounter component', () => {
${'allResolved'} | ${true} | ${true} | ${1}
`('renders correctly if $title', ({ resolved, isActive, groupLength }) => {
updateStore({ resolvable: true, resolved });
- wrapper = shallowMount(DiscussionCounter, { store, localVue });
+ wrapper = shallowMount(DiscussionCounter, { store });
expect(wrapper.find(`.is-active`).exists()).toBe(isActive);
expect(wrapper.findAll(GlButton)).toHaveLength(groupLength);
@@ -99,7 +99,7 @@ describe('DiscussionCounter component', () => {
const discussion = { ...discussionMock, expanded };
store.commit(types.ADD_OR_UPDATE_DISCUSSIONS, [discussion]);
store.dispatch('updateResolvableDiscussionsCounts');
- wrapper = shallowMount(DiscussionCounter, { store, localVue });
+ wrapper = shallowMount(DiscussionCounter, { store });
toggleAllButton = wrapper.find('.toggle-all-discussions-btn');
};
@@ -113,7 +113,7 @@ describe('DiscussionCounter component', () => {
expect(setExpandDiscussionsFn).toHaveBeenCalledTimes(1);
});
- it('collapses all discussions if expanded', () => {
+ it('collapses all discussions if expanded', async () => {
updateStoreWithExpanded(true);
expect(wrapper.vm.allExpanded).toBe(true);
@@ -121,13 +121,12 @@ describe('DiscussionCounter component', () => {
toggleAllButton.vm.$emit('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.vm.allExpanded).toBe(false);
- expect(toggleAllButton.props('icon')).toBe('angle-down');
- });
+ await nextTick();
+ expect(wrapper.vm.allExpanded).toBe(false);
+ expect(toggleAllButton.props('icon')).toBe('angle-down');
});
- it('expands all discussions if collapsed', () => {
+ it('expands all discussions if collapsed', async () => {
updateStoreWithExpanded(false);
expect(wrapper.vm.allExpanded).toBe(false);
@@ -135,10 +134,9 @@ describe('DiscussionCounter component', () => {
toggleAllButton.vm.$emit('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.vm.allExpanded).toBe(true);
- expect(toggleAllButton.props('icon')).toBe('angle-up');
- });
+ await nextTick();
+ expect(wrapper.vm.allExpanded).toBe(true);
+ expect(toggleAllButton.props('icon')).toBe('angle-up');
});
});
});
diff --git a/spec/frontend/notes/components/discussion_filter_spec.js b/spec/frontend/notes/components/discussion_filter_spec.js
index 17998dfc9d5..27206bddbfc 100644
--- a/spec/frontend/notes/components/discussion_filter_spec.js
+++ b/spec/frontend/notes/components/discussion_filter_spec.js
@@ -1,5 +1,6 @@
import { GlDropdown } from '@gitlab/ui';
-import { createLocalVue, mount } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import AxiosMockAdapter from 'axios-mock-adapter';
import Vuex from 'vuex';
import { TEST_HOST } from 'helpers/test_constants';
@@ -12,9 +13,7 @@ import notesModule from '~/notes/stores/modules';
import { discussionFiltersMock, discussionMock } from '../mock_data';
-const localVue = createLocalVue();
-
-localVue.use(Vuex);
+Vue.use(Vuex);
const DISCUSSION_PATH = `${TEST_HOST}/example`;
@@ -58,7 +57,6 @@ describe('DiscussionFilter component', () => {
filters: discussionFiltersMock,
selectedValue: DISCUSSION_FILTERS_DEFAULT_VALUE,
},
- localVue,
});
};
@@ -153,13 +151,11 @@ describe('DiscussionFilter component', () => {
window.mrTabs = undefined;
});
- it('only renders when discussion tab is active', (done) => {
+ it('only renders when discussion tab is active', async () => {
eventHub.$emit('MergeRequestTabChange', 'commit');
- wrapper.vm.$nextTick(() => {
- expect(wrapper.html()).toBe('');
- done();
- });
+ await nextTick();
+ expect(wrapper.html()).toBe('');
});
});
@@ -168,58 +164,48 @@ describe('DiscussionFilter component', () => {
window.location.hash = '';
});
- it('updates the filter when the URL links to a note', (done) => {
+ it('updates the filter when the URL links to a note', async () => {
window.location.hash = `note_${discussionMock.notes[0].id}`;
wrapper.vm.currentValue = discussionFiltersMock[2].value;
wrapper.vm.handleLocationHash();
- wrapper.vm.$nextTick(() => {
- expect(wrapper.vm.currentValue).toBe(DISCUSSION_FILTERS_DEFAULT_VALUE);
- done();
- });
+ await nextTick();
+ expect(wrapper.vm.currentValue).toBe(DISCUSSION_FILTERS_DEFAULT_VALUE);
});
- it('does not update the filter when the current filter is "Show all activity"', (done) => {
+ it('does not update the filter when the current filter is "Show all activity"', async () => {
window.location.hash = `note_${discussionMock.notes[0].id}`;
wrapper.vm.handleLocationHash();
- wrapper.vm.$nextTick(() => {
- expect(wrapper.vm.currentValue).toBe(DISCUSSION_FILTERS_DEFAULT_VALUE);
- done();
- });
+ await nextTick();
+ expect(wrapper.vm.currentValue).toBe(DISCUSSION_FILTERS_DEFAULT_VALUE);
});
- it('only updates filter when the URL links to a note', (done) => {
+ it('only updates filter when the URL links to a note', async () => {
window.location.hash = `testing123`;
wrapper.vm.handleLocationHash();
- wrapper.vm.$nextTick(() => {
- expect(wrapper.vm.currentValue).toBe(DISCUSSION_FILTERS_DEFAULT_VALUE);
- done();
- });
+ await nextTick();
+ expect(wrapper.vm.currentValue).toBe(DISCUSSION_FILTERS_DEFAULT_VALUE);
});
- it('fetches discussions when there is a hash', (done) => {
+ it('fetches discussions when there is a hash', async () => {
window.location.hash = `note_${discussionMock.notes[0].id}`;
wrapper.vm.currentValue = discussionFiltersMock[2].value;
jest.spyOn(wrapper.vm, 'selectFilter').mockImplementation(() => {});
wrapper.vm.handleLocationHash();
- wrapper.vm.$nextTick(() => {
- expect(wrapper.vm.selectFilter).toHaveBeenCalled();
- done();
- });
+ await nextTick();
+ expect(wrapper.vm.selectFilter).toHaveBeenCalled();
});
- it('does not fetch discussions when there is no hash', (done) => {
+ it('does not fetch discussions when there is no hash', async () => {
window.location.hash = '';
jest.spyOn(wrapper.vm, 'selectFilter').mockImplementation(() => {});
wrapper.vm.handleLocationHash();
- wrapper.vm.$nextTick(() => {
- expect(wrapper.vm.selectFilter).not.toHaveBeenCalled();
- done();
- });
+ await nextTick();
+ expect(wrapper.vm.selectFilter).not.toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/notes/components/discussion_navigator_spec.js b/spec/frontend/notes/components/discussion_navigator_spec.js
index e430e18b76a..77ae7b2c3b5 100644
--- a/spec/frontend/notes/components/discussion_navigator_spec.js
+++ b/spec/frontend/notes/components/discussion_navigator_spec.js
@@ -1,6 +1,6 @@
/* global Mousetrap */
import 'mousetrap';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import {
keysFor,
@@ -11,8 +11,6 @@ import DiscussionNavigator from '~/notes/components/discussion_navigator.vue';
import eventHub from '~/notes/event_hub';
describe('notes/components/discussion_navigator', () => {
- const localVue = createLocalVue();
-
let wrapper;
let jumpToNextDiscussion;
let jumpToPreviousDiscussion;
@@ -20,12 +18,12 @@ describe('notes/components/discussion_navigator', () => {
const createComponent = () => {
wrapper = shallowMount(DiscussionNavigator, {
mixins: [
- localVue.extend({
+ {
methods: {
jumpToNextDiscussion,
jumpToPreviousDiscussion,
},
- }),
+ },
],
});
};
@@ -48,7 +46,7 @@ describe('notes/components/discussion_navigator', () => {
beforeEach(() => {
onSpy = jest.spyOn(eventHub, '$on');
- vm = new (Vue.extend(DiscussionNavigator))();
+ vm = new Vue(DiscussionNavigator);
});
it('listens for jumpToFirstUnresolvedDiscussion events', () => {
diff --git a/spec/frontend/notes/components/discussion_notes_spec.js b/spec/frontend/notes/components/discussion_notes_spec.js
index 59ac75f00e6..3506b6ac9f3 100644
--- a/spec/frontend/notes/components/discussion_notes_spec.js
+++ b/spec/frontend/notes/components/discussion_notes_spec.js
@@ -1,6 +1,7 @@
import { getByRole } from '@testing-library/dom';
import { shallowMount, mount } from '@vue/test-utils';
import '~/behaviors/markdown/render_gfm';
+import { nextTick } from 'vue';
import DiscussionNotes from '~/notes/components/discussion_notes.vue';
import NoteableNote from '~/notes/components/noteable_note.vue';
import { SYSTEM_NOTE } from '~/notes/constants';
@@ -135,28 +136,25 @@ describe('DiscussionNotes', () => {
createComponent({ shouldGroupReplies: true, isExpanded: true });
});
- it('emits deleteNote when first note emits handleDeleteNote', () => {
+ it('emits deleteNote when first note emits handleDeleteNote', async () => {
findNoteAtIndex(0).vm.$emit('handleDeleteNote');
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted().deleteNote).toBeTruthy();
- });
+ await nextTick();
+ expect(wrapper.emitted().deleteNote).toBeTruthy();
});
- it('emits startReplying when first note emits startReplying', () => {
+ it('emits startReplying when first note emits startReplying', async () => {
findNoteAtIndex(0).vm.$emit('startReplying');
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted().startReplying).toBeTruthy();
- });
+ await nextTick();
+ expect(wrapper.emitted().startReplying).toBeTruthy();
});
- it('emits deleteNote when second note emits handleDeleteNote', () => {
+ it('emits deleteNote when second note emits handleDeleteNote', async () => {
findNoteAtIndex(1).vm.$emit('handleDeleteNote');
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted().deleteNote).toBeTruthy();
- });
+ await nextTick();
+ expect(wrapper.emitted().deleteNote).toBeTruthy();
});
});
@@ -167,12 +165,11 @@ describe('DiscussionNotes', () => {
note = wrapper.find('.notes > *');
});
- it('emits deleteNote when first note emits handleDeleteNote', () => {
+ it('emits deleteNote when first note emits handleDeleteNote', async () => {
note.vm.$emit('handleDeleteNote');
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted().deleteNote).toBeTruthy();
- });
+ await nextTick();
+ expect(wrapper.emitted().deleteNote).toBeTruthy();
});
});
});
diff --git a/spec/frontend/notes/components/discussion_resolve_button_spec.js b/spec/frontend/notes/components/discussion_resolve_button_spec.js
index 64e061830b9..ca0c0ca6de8 100644
--- a/spec/frontend/notes/components/discussion_resolve_button_spec.js
+++ b/spec/frontend/notes/components/discussion_resolve_button_spec.js
@@ -1,5 +1,6 @@
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import resolveDiscussionButton from '~/notes/components/discussion_resolve_button.vue';
const buttonTitle = 'Resolve discussion';
@@ -26,15 +27,14 @@ describe('resolveDiscussionButton', () => {
wrapper.destroy();
});
- it('should emit a onClick event on button click', () => {
+ it('should emit a onClick event on button click', async () => {
const button = wrapper.find(GlButton);
button.vm.$emit('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted()).toEqual({
- onClick: [[]],
- });
+ await nextTick();
+ expect(wrapper.emitted()).toEqual({
+ onClick: [[]],
});
});
@@ -57,7 +57,7 @@ describe('resolveDiscussionButton', () => {
expect(button.props('loading')).toEqual(true);
});
- it('should only show a loading spinner while resolving', () => {
+ it('should only show a loading spinner while resolving', async () => {
factory({
propsData: {
isResolving: false,
@@ -67,8 +67,7 @@ describe('resolveDiscussionButton', () => {
const button = wrapper.find(GlButton);
- wrapper.vm.$nextTick(() => {
- expect(button.props('loading')).toEqual(false);
- });
+ await nextTick();
+ expect(button.props('loading')).toEqual(false);
});
});
diff --git a/spec/frontend/notes/components/discussion_resolve_with_issue_button_spec.js b/spec/frontend/notes/components/discussion_resolve_with_issue_button_spec.js
index 4348445f7ca..5bc6282db03 100644
--- a/spec/frontend/notes/components/discussion_resolve_with_issue_button_spec.js
+++ b/spec/frontend/notes/components/discussion_resolve_with_issue_button_spec.js
@@ -1,17 +1,14 @@
import { GlButton } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
import { TEST_HOST } from 'spec/test_constants';
import ResolveWithIssueButton from '~/notes/components/discussion_resolve_with_issue_button.vue';
-const localVue = createLocalVue();
-
describe('ResolveWithIssueButton', () => {
let wrapper;
const url = `${TEST_HOST}/hello-world/`;
beforeEach(() => {
wrapper = shallowMount(ResolveWithIssueButton, {
- localVue,
propsData: {
url,
},
diff --git a/spec/frontend/notes/components/note_actions_spec.js b/spec/frontend/notes/components/note_actions_spec.js
index ecce854b00a..780f24b3aa8 100644
--- a/spec/frontend/notes/components/note_actions_spec.js
+++ b/spec/frontend/notes/components/note_actions_spec.js
@@ -1,6 +1,6 @@
-import { mount, createLocalVue, createWrapper } from '@vue/test-utils';
+import { mount, createWrapper } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
-import Vue from 'vue';
+import { nextTick } from 'vue';
import { TEST_HOST } from 'spec/test_constants';
import axios from '~/lib/utils/axios_utils';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
@@ -20,11 +20,9 @@ describe('noteActions', () => {
const findUserAccessRoleBadgeText = (idx) => findUserAccessRoleBadge(idx).text().trim();
const mountNoteActions = (propsData, computed) => {
- const localVue = createLocalVue();
- return mount(localVue.extend(noteActions), {
+ return mount(noteActions, {
store,
propsData,
- localVue,
computed,
});
};
@@ -78,15 +76,14 @@ describe('noteActions', () => {
expect(findUserAccessRoleBadgeText(1)).toBe(props.accessLevel);
});
- it('should render contributor badge', () => {
+ it('should render contributor badge', async () => {
wrapper.setProps({
accessLevel: null,
isContributor: true,
});
- return wrapper.vm.$nextTick().then(() => {
- expect(findUserAccessRoleBadgeText(1)).toBe('Contributor');
- });
+ await nextTick();
+ expect(findUserAccessRoleBadgeText(1)).toBe('Contributor');
});
it('should render emoji link', () => {
@@ -107,7 +104,7 @@ describe('noteActions', () => {
expect(wrapper.find('.js-btn-copy-note-link').exists()).toBe(true);
});
- it('should not show copy link action when `noteUrl` prop is empty', (done) => {
+ it('should not show copy link action when `noteUrl` prop is empty', async () => {
wrapper.setProps({
...props,
author: {
@@ -121,30 +118,23 @@ describe('noteActions', () => {
noteUrl: '',
});
- Vue.nextTick()
- .then(() => {
- expect(wrapper.find('.js-btn-copy-note-link').exists()).toBe(false);
- })
- .then(done)
- .catch(done.fail);
+ await nextTick();
+ expect(wrapper.find('.js-btn-copy-note-link').exists()).toBe(false);
});
it('should be possible to delete comment', () => {
expect(wrapper.find('.js-note-delete').exists()).toBe(true);
});
- it('closes tooltip when dropdown opens', (done) => {
+ it('closes tooltip when dropdown opens', async () => {
wrapper.find('.more-actions-toggle').trigger('click');
const rootWrapper = createWrapper(wrapper.vm.$root);
- Vue.nextTick()
- .then(() => {
- const emitted = Object.keys(rootWrapper.emitted());
-
- expect(emitted).toEqual([BV_HIDE_TOOLTIP]);
- done();
- })
- .catch(done.fail);
+
+ await nextTick();
+ const emitted = Object.keys(rootWrapper.emitted());
+
+ expect(emitted).toEqual([BV_HIDE_TOOLTIP]);
});
it('should not be possible to assign or unassign the comment author in a merge request', () => {
diff --git a/spec/frontend/notes/components/note_body_spec.js b/spec/frontend/notes/components/note_body_spec.js
index 4e345c9ac8d..63f3cd865d5 100644
--- a/spec/frontend/notes/components/note_body_spec.js
+++ b/spec/frontend/notes/components/note_body_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { suggestionCommitMessage } from '~/diffs/store/getters';
@@ -46,9 +46,9 @@ describe('issue_note_body component', () => {
});
describe('isEditing', () => {
- beforeEach((done) => {
+ beforeEach(async () => {
vm.isEditing = true;
- Vue.nextTick(done);
+ await nextTick();
});
it('renders edit form', () => {
diff --git a/spec/frontend/notes/components/note_form_spec.js b/spec/frontend/notes/components/note_form_spec.js
index d3b5ab02f24..3e80b24f128 100644
--- a/spec/frontend/notes/components/note_form_spec.js
+++ b/spec/frontend/notes/components/note_form_spec.js
@@ -45,6 +45,8 @@ describe('issue_note_form component', () => {
noteBody: 'Magni suscipit eius consectetur enim et ex et commodi.',
noteId: '545',
};
+
+ gon.features = { markdownContinueLists: true };
});
afterEach(() => {
diff --git a/spec/frontend/notes/components/note_header_spec.js b/spec/frontend/notes/components/note_header_spec.js
index 774d5aaa7d3..8d82cf3d2c7 100644
--- a/spec/frontend/notes/components/note_header_spec.js
+++ b/spec/frontend/notes/components/note_header_spec.js
@@ -1,13 +1,12 @@
import { GlSprintf } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import NoteHeader from '~/notes/components/note_header.vue';
import { AVAILABILITY_STATUS } from '~/set_status_modal/utils';
import UserNameWithStatus from '~/sidebar/components/assignees/user_name_with_status.vue';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
const actions = {
setTargetNoteHash: jest.fn(),
@@ -42,7 +41,6 @@ describe('NoteHeader component', () => {
const createComponent = (props) => {
wrapper = shallowMount(NoteHeader, {
- localVue,
store: new Vuex.Store({
actions,
}),
diff --git a/spec/frontend/notes/components/noteable_note_spec.js b/spec/frontend/notes/components/noteable_note_spec.js
index 038aff3be04..c7115a5911b 100644
--- a/spec/frontend/notes/components/noteable_note_spec.js
+++ b/spec/frontend/notes/components/noteable_note_spec.js
@@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import waitForPromises from 'helpers/wait_for_promises';
@@ -107,11 +107,11 @@ describe('issue_note', () => {
line,
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findMultilineComment().text()).toBe('Comment on lines 1 to 2');
});
- it('should only render if it has everything it needs', () => {
+ it('should only render if it has everything it needs', async () => {
const position = {
line_range: {
start: {
@@ -140,12 +140,11 @@ describe('issue_note', () => {
line,
});
- return wrapper.vm.$nextTick().then(() => {
- expect(findMultilineComment().exists()).toBe(false);
- });
+ await nextTick();
+ expect(findMultilineComment().exists()).toBe(false);
});
- it('should not render if has single line comment', () => {
+ it('should not render if has single line comment', async () => {
const position = {
line_range: {
start: {
@@ -174,9 +173,8 @@ describe('issue_note', () => {
line,
});
- return wrapper.vm.$nextTick().then(() => {
- expect(findMultilineComment().exists()).toBe(false);
- });
+ await nextTick();
+ expect(findMultilineComment().exists()).toBe(false);
});
it('should not render if `line_range` is unavailable', () => {
@@ -204,7 +202,7 @@ describe('issue_note', () => {
line,
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.findComponent(UserAvatarLink).props('imgSize')).toBe(24);
});
@@ -318,13 +316,13 @@ describe('issue_note', () => {
callback: () => {},
});
- await wrapper.vm.$nextTick();
+ await nextTick();
let noteBodyProps = noteBody.props();
expect(noteBodyProps.note.note_html).toBe(`<p>${updatedText}</p>\n`);
noteBody.vm.$emit('cancelForm', {});
- await wrapper.vm.$nextTick();
+ await nextTick();
noteBodyProps = noteBody.props();
diff --git a/spec/frontend/notes/components/notes_app_spec.js b/spec/frontend/notes/components/notes_app_spec.js
index 84d94857fe5..bf36d6cb7a2 100644
--- a/spec/frontend/notes/components/notes_app_spec.js
+++ b/spec/frontend/notes/components/notes_app_spec.js
@@ -1,7 +1,7 @@
import { mount, shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
-import Vue from 'vue';
+import { nextTick } from 'vue';
import setWindowLocation from 'helpers/set_window_location_helper';
import { setTestTimeout } from 'helpers/timeout';
import waitForPromises from 'helpers/wait_for_promises';
@@ -294,24 +294,22 @@ describe('note_app', () => {
return waitForDiscussionsRequest();
});
- it('should render markdown docs url', () => {
+ it('should render markdown docs url', async () => {
wrapper.find('.js-note-edit').trigger('click');
const { markdownDocsPath } = mockData.notesDataMock;
- return Vue.nextTick().then(() => {
- expect(wrapper.find(`.edit-note a[href="${markdownDocsPath}"]`).text().trim()).toEqual(
- 'Markdown is supported',
- );
- });
+ await nextTick();
+ expect(wrapper.find(`.edit-note a[href="${markdownDocsPath}"]`).text().trim()).toEqual(
+ 'Markdown is supported',
+ );
});
- it('should not render quick actions docs url', () => {
+ it('should not render quick actions docs url', async () => {
wrapper.find('.js-note-edit').trigger('click');
const { quickActionsDocsPath } = mockData.notesDataMock;
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(`.edit-note a[href="${quickActionsDocsPath}"]`).exists()).toBe(false);
- });
+ await nextTick();
+ expect(wrapper.find(`.edit-note a[href="${quickActionsDocsPath}"]`).exists()).toBe(false);
});
});
diff --git a/spec/frontend/notes/components/sort_discussion_spec.js b/spec/frontend/notes/components/sort_discussion_spec.js
index 60f03a0f5b5..a279dfd1ef3 100644
--- a/spec/frontend/notes/components/sort_discussion_spec.js
+++ b/spec/frontend/notes/components/sort_discussion_spec.js
@@ -1,4 +1,5 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import SortDiscussion from '~/notes/components/sort_discussion.vue';
import { ASC, DESC } from '~/notes/constants';
@@ -6,8 +7,7 @@ import createStore from '~/notes/stores';
import Tracking from '~/tracking';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('Sort Discussion component', () => {
let wrapper;
@@ -17,7 +17,6 @@ describe('Sort Discussion component', () => {
jest.spyOn(store, 'dispatch').mockImplementation();
wrapper = shallowMount(SortDiscussion, {
- localVue,
store,
});
};
diff --git a/spec/frontend/notes/components/timeline_toggle_spec.js b/spec/frontend/notes/components/timeline_toggle_spec.js
index 73fb2079e31..84fa3008835 100644
--- a/spec/frontend/notes/components/timeline_toggle_spec.js
+++ b/spec/frontend/notes/components/timeline_toggle_spec.js
@@ -1,5 +1,6 @@
import { GlButton } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import TimelineToggle, {
timelineEnabledTooltip,
@@ -10,8 +11,7 @@ import createStore from '~/notes/stores';
import { trackToggleTimelineView } from '~/notes/utils';
import Tracking from '~/tracking';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('Timeline toggle', () => {
let wrapper;
@@ -23,7 +23,6 @@ describe('Timeline toggle', () => {
jest.spyOn(Tracking, 'event').mockImplementation();
wrapper = shallowMount(TimelineToggle, {
- localVue,
store,
});
};
@@ -65,7 +64,7 @@ describe('Timeline toggle', () => {
it('should set correct UI state', async () => {
store.state.isTimelineEnabled = true;
findGlButton().vm.$emit('click', mockEvent);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findGlButton().attributes('title')).toBe(timelineEnabledTooltip);
expect(findGlButton().attributes('selected')).toBe('true');
expect(mockEvent.currentTarget.blur).toHaveBeenCalled();
@@ -73,7 +72,7 @@ describe('Timeline toggle', () => {
it('should track Snowplow event', async () => {
store.state.isTimelineEnabled = true;
- await wrapper.vm.$nextTick();
+ await nextTick();
findGlButton().trigger('click');
@@ -98,7 +97,7 @@ describe('Timeline toggle', () => {
it('should set correct UI state', async () => {
store.state.isTimelineEnabled = false;
findGlButton().vm.$emit('click', mockEvent);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findGlButton().attributes('title')).toBe(timelineDisabledTooltip);
expect(findGlButton().attributes('selected')).toBe(undefined);
expect(mockEvent.currentTarget.blur).toHaveBeenCalled();
@@ -106,7 +105,7 @@ describe('Timeline toggle', () => {
it('should track Snowplow event', async () => {
store.state.isTimelineEnabled = false;
- await wrapper.vm.$nextTick();
+ await nextTick();
findGlButton().trigger('click');
diff --git a/spec/frontend/notes/deprecated_notes_spec.js b/spec/frontend/notes/deprecated_notes_spec.js
index 34623f8aa13..7c52920da90 100644
--- a/spec/frontend/notes/deprecated_notes_spec.js
+++ b/spec/frontend/notes/deprecated_notes_spec.js
@@ -5,6 +5,7 @@ import $ from 'jquery';
import '~/behaviors/markdown/render_gfm';
import { createSpyObj } from 'helpers/jest_helpers';
import { TEST_HOST } from 'helpers/test_constants';
+import waitForPromises from 'helpers/wait_for_promises';
import { setTestTimeoutOnce } from 'helpers/timeout';
import axios from '~/lib/utils/axios_utils';
import * as urlUtility from '~/lib/utils/url_utility';
@@ -549,15 +550,14 @@ describe.skip('Old Notes (~/deprecated_notes.js)', () => {
expect($notesContainer.find('.note.being-posted').length).toBeGreaterThan(0);
});
- it('should remove placeholder note when new comment is done posting', (done) => {
+ it('should remove placeholder note when new comment is done posting', async () => {
mockNotesPost();
$('.js-comment-button').click();
- setImmediate(() => {
- expect($notesContainer.find('.note.being-posted').length).toEqual(0);
- done();
- });
+ await waitForPromises();
+
+ expect($notesContainer.find('.note.being-posted').length).toEqual(0);
});
describe('postComment', () => {
@@ -584,40 +584,37 @@ describe.skip('Old Notes (~/deprecated_notes.js)', () => {
});
});
- it('should show actual note element when new comment is done posting', (done) => {
+ it('should show actual note element when new comment is done posting', async () => {
mockNotesPost();
$('.js-comment-button').click();
- setImmediate(() => {
- expect($notesContainer.find(`#note_${note.id}`).length).toBeGreaterThan(0);
- done();
- });
+ await waitForPromises();
+
+ expect($notesContainer.find(`#note_${note.id}`).length).toBeGreaterThan(0);
});
- it('should reset Form when new comment is done posting', (done) => {
+ it('should reset Form when new comment is done posting', async () => {
mockNotesPost();
$('.js-comment-button').click();
- setImmediate(() => {
- expect($form.find('textarea.js-note-text').val()).toEqual('');
- done();
- });
+ await waitForPromises();
+
+ expect($form.find('textarea.js-note-text').val()).toEqual('');
});
- it('should show flash error message when new comment failed to be posted', (done) => {
+ it('should show flash error message when new comment failed to be posted', async () => {
mockNotesPostError();
jest.spyOn(notes, 'addFlash');
$('.js-comment-button').click();
- setImmediate(() => {
- expect(notes.addFlash).toHaveBeenCalled();
- // JSDom doesn't support the :visible selector yet
- expect(notes.flashContainer.style.display).not.toBe('none');
- done();
- });
+ await waitForPromises();
+
+ expect(notes.addFlash).toHaveBeenCalled();
+ // JSDom doesn't support the :visible selector yet
+ expect(notes.flashContainer.style.display).not.toBe('none');
});
});
@@ -657,16 +654,15 @@ describe.skip('Old Notes (~/deprecated_notes.js)', () => {
$form.find('textarea.js-note-text').val(sampleComment);
});
- it('should remove quick action placeholder when comment with quick actions is done posting', (done) => {
+ it('should remove quick action placeholder when comment with quick actions is done posting', async () => {
jest.spyOn(gl.awardsHandler, 'addAwardToEmojiBar');
$('.js-comment-button').click();
expect($notesContainer.find('.note.being-posted').length).toEqual(1); // Placeholder shown
- setImmediate(() => {
- expect($notesContainer.find('.note.being-posted').length).toEqual(0); // Placeholder removed
- done();
- });
+ await waitForPromises();
+
+ expect($notesContainer.find('.note.being-posted').length).toEqual(0); // Placeholder removed
});
});
@@ -692,16 +688,15 @@ describe.skip('Old Notes (~/deprecated_notes.js)', () => {
$form.find('textarea.js-note-text').val(sampleComment);
});
- it('should show message placeholder including lines starting with slash', (done) => {
+ it('should show message placeholder including lines starting with slash', async () => {
$('.js-comment-button').click();
expect($notesContainer.find('.note.being-posted').length).toEqual(1); // Placeholder shown
expect($notesContainer.find('.note-body p').text()).toEqual(sampleComment); // No quick action processing
- setImmediate(() => {
- expect($notesContainer.find('.note.being-posted').length).toEqual(0); // Placeholder removed
- done();
- });
+ await waitForPromises();
+
+ expect($notesContainer.find('.note.being-posted').length).toEqual(0); // Placeholder removed
});
});
@@ -730,23 +725,21 @@ describe.skip('Old Notes (~/deprecated_notes.js)', () => {
$form.find('textarea.js-note-text').html(sampleComment);
});
- it('should not render a script tag', (done) => {
+ it('should not render a script tag', async () => {
$('.js-comment-button').click();
- setImmediate(() => {
- const $noteEl = $notesContainer.find(`#note_${note.id}`);
- $noteEl.find('.js-note-edit').click();
- $noteEl.find('textarea.js-note-text').html(updatedComment);
- $noteEl.find('.js-comment-save-button').click();
+ await waitForPromises();
- const $updatedNoteEl = $notesContainer
- .find(`#note_${note.id}`)
- .find('.js-task-list-container');
+ const $noteEl = $notesContainer.find(`#note_${note.id}`);
+ $noteEl.find('.js-note-edit').click();
+ $noteEl.find('textarea.js-note-text').html(updatedComment);
+ $noteEl.find('.js-comment-save-button').click();
- expect($updatedNoteEl.find('.note-text').text().trim()).toEqual('');
+ const $updatedNoteEl = $notesContainer
+ .find(`#note_${note.id}`)
+ .find('.js-task-list-container');
- done();
- });
+ expect($updatedNoteEl.find('.note-text').text().trim()).toEqual('');
});
});
diff --git a/spec/frontend/notes/mixins/discussion_navigation_spec.js b/spec/frontend/notes/mixins/discussion_navigation_spec.js
index 26a072b82f8..aba80789a01 100644
--- a/spec/frontend/notes/mixins/discussion_navigation_spec.js
+++ b/spec/frontend/notes/mixins/discussion_navigation_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { setHTMLFixture } from 'helpers/fixtures';
import createEventHub from '~/helpers/event_hub_factory';
@@ -27,8 +27,7 @@ const createComponent = () => ({
});
describe('Discussion navigation mixin', () => {
- const localVue = createLocalVue();
- localVue.use(Vuex);
+ Vue.use(Vuex);
let wrapper;
let store;
@@ -65,7 +64,7 @@ describe('Discussion navigation mixin', () => {
});
store.state.notes.discussions = createDiscussions();
- wrapper = shallowMount(createComponent(), { store, localVue });
+ wrapper = shallowMount(createComponent(), { store });
});
afterEach(() => {
@@ -94,14 +93,13 @@ describe('Discussion navigation mixin', () => {
expect(store.dispatch).toHaveBeenCalledWith('setCurrentDiscussionId', null);
});
- it('triggers the jumpToNextDiscussion action when the previous store action succeeds', () => {
+ it('triggers the jumpToNextDiscussion action when the previous store action succeeds', async () => {
store.dispatch.mockResolvedValue();
vm.jumpToFirstUnresolvedDiscussion();
- return vm.$nextTick().then(() => {
- expect(vm.jumpToNextDiscussion).toHaveBeenCalled();
- });
+ await nextTick();
+ expect(vm.jumpToNextDiscussion).toHaveBeenCalled();
});
});
@@ -127,11 +125,11 @@ describe('Discussion navigation mixin', () => {
});
describe('on `show` active tab', () => {
- beforeEach(() => {
+ beforeEach(async () => {
window.mrTabs.currentAction = 'show';
wrapper.vm[fn](...args);
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('sets current discussion', () => {
@@ -145,17 +143,17 @@ describe('Discussion navigation mixin', () => {
it('scrolls to element', () => {
expect(utils.scrollToElement).toHaveBeenCalledWith(
findDiscussion('div.discussion', expected),
- { behavior: 'smooth' },
+ { behavior: 'auto' },
);
});
});
describe('on `diffs` active tab', () => {
- beforeEach(() => {
+ beforeEach(async () => {
window.mrTabs.currentAction = 'diffs';
wrapper.vm[fn](...args);
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('sets current discussion', () => {
@@ -173,17 +171,17 @@ describe('Discussion navigation mixin', () => {
expect(utils.scrollToElementWithContext).toHaveBeenCalledWith(
findDiscussion('ul.notes', expected),
- { behavior: 'smooth' },
+ { behavior: 'auto' },
);
});
});
describe('on `other` active tab', () => {
- beforeEach(() => {
+ beforeEach(async () => {
window.mrTabs.currentAction = 'other';
wrapper.vm[fn](...args);
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('sets current discussion', () => {
@@ -214,21 +212,15 @@ describe('Discussion navigation mixin', () => {
it('scrolls to discussion', () => {
expect(utils.scrollToElement).toHaveBeenCalledWith(
findDiscussion('div.discussion', expected),
- { behavior: 'smooth' },
+ { behavior: 'auto' },
);
});
});
});
});
- describe.each`
- diffsVirtualScrolling
- ${false}
- ${true}
- `('virtual scrolling feature is $diffsVirtualScrolling', ({ diffsVirtualScrolling }) => {
+ describe('virtual scrolling feature', () => {
beforeEach(() => {
- window.gon = { features: { diffsVirtualScrolling } };
-
jest.spyOn(store, 'dispatch');
store.state.notes.currentDiscussionId = 'a';
@@ -240,22 +232,22 @@ describe('Discussion navigation mixin', () => {
window.location.hash = '';
});
- it('resets location hash if diffsVirtualScrolling flag is true', async () => {
+ it('resets location hash', async () => {
wrapper.vm.jumpToNextDiscussion();
await nextTick();
- expect(window.location.hash).toBe(diffsVirtualScrolling ? '' : '#test');
+ expect(window.location.hash).toBe('');
});
it.each`
- tabValue | hashValue
- ${'diffs'} | ${false}
- ${'show'} | ${!diffsVirtualScrolling}
- ${'other'} | ${!diffsVirtualScrolling}
+ tabValue
+ ${'diffs'}
+ ${'show'}
+ ${'other'}
`(
'calls scrollToFile with setHash as $hashValue when the tab is $tabValue',
- async ({ hashValue, tabValue }) => {
+ async ({ tabValue }) => {
window.mrTabs.currentAction = tabValue;
wrapper.vm.jumpToNextDiscussion();
@@ -264,7 +256,6 @@ describe('Discussion navigation mixin', () => {
expect(store.dispatch).toHaveBeenCalledWith('diffs/scrollToFile', {
path: 'test.js',
- setHash: hashValue,
});
},
);
diff --git a/spec/frontend/notifications/components/custom_notifications_modal_spec.js b/spec/frontend/notifications/components/custom_notifications_modal_spec.js
index 7a036d25559..c5d201c3aec 100644
--- a/spec/frontend/notifications/components/custom_notifications_modal_spec.js
+++ b/spec/frontend/notifications/components/custom_notifications_modal_spec.js
@@ -2,6 +2,7 @@ import { GlSprintf, GlModal, GlFormGroup, GlFormCheckbox, GlLoadingIcon } from '
import { shallowMount } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
+import { nextTick } from 'vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import httpStatus from '~/lib/utils/http_status';
@@ -97,7 +98,7 @@ describe('CustomNotificationsModal', () => {
],
});
- await wrapper.vm.$nextTick();
+ await nextTick();
});
it.each`
@@ -222,7 +223,7 @@ describe('CustomNotificationsModal', () => {
],
});
- await wrapper.vm.$nextTick();
+ await nextTick();
findCheckboxAt(1).vm.$emit('change', true);
@@ -252,7 +253,7 @@ describe('CustomNotificationsModal', () => {
],
});
- await wrapper.vm.$nextTick();
+ await nextTick();
findCheckboxAt(1).vm.$emit('change', true);
diff --git a/spec/frontend/notifications/components/notifications_dropdown_spec.js b/spec/frontend/notifications/components/notifications_dropdown_spec.js
index e12251ce6d9..7ca6c2052ae 100644
--- a/spec/frontend/notifications/components/notifications_dropdown_spec.js
+++ b/spec/frontend/notifications/components/notifications_dropdown_spec.js
@@ -195,6 +195,14 @@ describe('NotificationsDropdown', () => {
);
});
});
+
+ it('passes provided `noFlip` value to `GlDropdown`', () => {
+ wrapper = createComponent({
+ noFlip: true,
+ });
+
+ expect(findDropdown().attributes('no-flip')).toBe('true');
+ });
});
describe('when selecting an item', () => {
diff --git a/spec/frontend/operation_settings/components/metrics_settings_spec.js b/spec/frontend/operation_settings/components/metrics_settings_spec.js
index 258c6eae692..c1fa1d24a82 100644
--- a/spec/frontend/operation_settings/components/metrics_settings_spec.js
+++ b/spec/frontend/operation_settings/components/metrics_settings_spec.js
@@ -1,5 +1,6 @@
import { GlButton, GlLink, GlFormGroup, GlFormInput, GlFormSelect } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import { TEST_HOST } from 'helpers/test_constants';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
@@ -181,17 +182,18 @@ describe('operation settings external dashboard component', () => {
expect(submit.text()).toBe('Save Changes');
});
- it('submits form on click', () => {
+ it('submits form on click', async () => {
mountComponent(false);
axios.patch.mockResolvedValue();
findSubmitButton().trigger('click');
expect(axios.patch).toHaveBeenCalledWith(...endpointRequest);
- return wrapper.vm.$nextTick().then(() => expect(refreshCurrentPage).toHaveBeenCalled());
+ await nextTick();
+ expect(refreshCurrentPage).toHaveBeenCalled();
});
- it('creates flash banner on error', () => {
+ it('creates flash banner on error', async () => {
mountComponent(false);
const message = 'mockErrorMessage';
axios.patch.mockRejectedValue({ response: { data: { message } } });
@@ -199,14 +201,11 @@ describe('operation settings external dashboard component', () => {
expect(axios.patch).toHaveBeenCalledWith(...endpointRequest);
- return wrapper.vm
- .$nextTick()
- .then(jest.runAllTicks)
- .then(() =>
- expect(createFlash).toHaveBeenCalledWith({
- message: `There was an error saving your changes. ${message}`,
- }),
- );
+ await nextTick();
+ await jest.runAllTicks();
+ expect(createFlash).toHaveBeenCalledWith({
+ message: `There was an error saving your changes. ${message}`,
+ });
});
});
});
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js
index 5278e730ec9..f4c22d9bfa7 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js
@@ -1,6 +1,7 @@
import { GlDropdownItem, GlIcon, GlDropdown } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
+import { nextTick } from 'vue';
import { useFakeDate } from 'helpers/fake_date';
import createMockApollo from 'helpers/mock_apollo_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
@@ -54,8 +55,8 @@ describe('Details Header', () => {
const waitForMetadataItems = async () => {
// Metadata items are printed by a loop in the title-area and it takes two ticks for them to be available
- await wrapper.vm.$nextTick();
- await wrapper.vm.$nextTick();
+ await nextTick();
+ await nextTick();
};
const mountComponent = ({
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js
index 0dcf988c814..ef6c4a1fa32 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import { GlEmptyState } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -8,7 +8,7 @@ import { stripTypenames } from 'helpers/graphql_helpers';
import component from '~/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue';
import TagsListRow from '~/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row.vue';
-import TagsLoader from '~/packages_and_registries/container_registry/explorer/components/details_page/tags_loader.vue';
+import TagsLoader from '~/packages_and_registries/shared/components/tags_loader.vue';
import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
import PersistedSearch from '~/packages_and_registries/shared/components/persisted_search.vue';
import getContainerRepositoryTagsQuery from '~/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags.query.graphql';
@@ -22,8 +22,6 @@ import {
import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
import { tagsMock, imageTagsMock, tagsPageInfo } from '../../mock_data';
-const localVue = createLocalVue();
-
describe('Tags List', () => {
let wrapper;
let apolloProvider;
@@ -50,13 +48,12 @@ describe('Tags List', () => {
};
const mountComponent = ({ propsData = { isMobile: false, id: 1 } } = {}) => {
- localVue.use(VueApollo);
+ Vue.use(VueApollo);
const requestHandlers = [[getContainerRepositoryTagsQuery, resolver]];
apolloProvider = createMockApollo(requestHandlers);
wrapper = shallowMount(component, {
- localVue,
apolloProvider,
propsData,
stubs: { RegistryList },
@@ -108,6 +105,7 @@ describe('Tags List', () => {
describe('events', () => {
it('prev-page fetch the previous page', async () => {
findRegistryList().vm.$emit('prev-page');
+ await waitForPromises();
expect(resolver).toHaveBeenCalledWith({
first: null,
@@ -119,8 +117,9 @@ describe('Tags List', () => {
});
});
- it('next-page fetch the previous page', () => {
+ it('next-page fetch the previous page', async () => {
findRegistryList().vm.$emit('next-page');
+ await waitForPromises();
expect(resolver).toHaveBeenCalledWith({
after: tagsPageInfo.endCursor,
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_loader_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_loader_spec.js
index 060dc9dc5f3..e5df260a260 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_loader_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_loader_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import component from '~/packages_and_registries/container_registry/explorer/components/details_page/tags_loader.vue';
+import component from '~/packages_and_registries/shared/components/tags_loader.vue';
import { GlSkeletonLoader } from '../../stubs';
describe('TagsLoader component', () => {
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cli_commands_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cli_commands_spec.js
index 4039fba869b..7727bf167fe 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cli_commands_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cli_commands_spec.js
@@ -1,7 +1,8 @@
import { GlDropdown } from '@gitlab/ui';
-import { mount, createLocalVue } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
-import QuickstartDropdown from '~/packages_and_registries/container_registry/explorer/components/list_page/cli_commands.vue';
+import QuickstartDropdown from '~/packages_and_registries/shared/components/cli_commands.vue';
import {
QUICK_START,
LOGIN_COMMAND_LABEL,
@@ -16,28 +17,18 @@ import CodeInstruction from '~/vue_shared/components/registry/code_instruction.v
import { dockerCommands } from '../../mock_data';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('cli_commands', () => {
let wrapper;
- const config = {
- repositoryUrl: 'foo',
- registryHostUrlWithPort: 'bar',
- };
-
const findDropdownButton = () => wrapper.find(GlDropdown);
const findCodeInstruction = () => wrapper.findAll(CodeInstruction);
const mountComponent = () => {
wrapper = mount(QuickstartDropdown, {
- localVue,
- provide() {
- return {
- config,
- ...dockerCommands,
- };
+ propsData: {
+ ...dockerCommands,
},
});
};
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state_spec.js
index 027cdf732bc..d2086943e4f 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state_spec.js
@@ -1,11 +1,11 @@
import { GlSprintf } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import groupEmptyState from '~/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state.vue';
import { GlEmptyState } from '../../stubs';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('Registry Group Empty state', () => {
let wrapper;
@@ -16,7 +16,6 @@ describe('Registry Group Empty state', () => {
beforeEach(() => {
wrapper = shallowMount(groupEmptyState, {
- localVue,
stubs: {
GlEmptyState,
GlSprintf,
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/project_empty_state_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/project_empty_state_spec.js
index 21748ae2813..8cfa8128021 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/project_empty_state_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/project_empty_state_spec.js
@@ -1,12 +1,12 @@
import { GlSprintf } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import projectEmptyState from '~/packages_and_registries/container_registry/explorer/components/list_page/project_empty_state.vue';
import { dockerCommands } from '../../mock_data';
import { GlEmptyState } from '../../stubs';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('Registry Project Empty state', () => {
let wrapper;
@@ -21,7 +21,6 @@ describe('Registry Project Empty state', () => {
beforeEach(() => {
wrapper = shallowMount(projectEmptyState, {
- localVue,
stubs: {
GlEmptyState,
GlSprintf,
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js
index 92cfeb7633e..c91a9c0f0fb 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js
@@ -1,5 +1,6 @@
import { GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import Component from '~/packages_and_registries/container_registry/explorer/components/list_page/registry_header.vue';
import {
CONTAINER_REGISTRY_TITLE,
@@ -21,7 +22,7 @@ describe('registry_header', () => {
const findImagesCountSubHeader = () => wrapper.find('[data-testid="images-count"]');
const findExpirationPolicySubHeader = () => wrapper.find('[data-testid="expiration-policy"]');
- const mountComponent = (propsData, slots) => {
+ const mountComponent = async (propsData, slots) => {
wrapper = shallowMount(Component, {
stubs: {
GlSprintf,
@@ -30,7 +31,7 @@ describe('registry_header', () => {
propsData,
slots,
});
- return wrapper.vm.$nextTick();
+ await nextTick();
};
afterEach(() => {
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js
index 7992bead60a..c602b37c3b5 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js
@@ -1,7 +1,8 @@
import { GlKeysetPagination, GlEmptyState } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
-import { nextTick } from 'vue';
+
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
@@ -11,7 +12,7 @@ import DetailsHeader from '~/packages_and_registries/container_registry/explorer
import PartialCleanupAlert from '~/packages_and_registries/container_registry/explorer/components/details_page/partial_cleanup_alert.vue';
import StatusAlert from '~/packages_and_registries/container_registry/explorer/components/details_page/status_alert.vue';
import TagsList from '~/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue';
-import TagsLoader from '~/packages_and_registries/container_registry/explorer/components/details_page/tags_loader.vue';
+import TagsLoader from '~/packages_and_registries/shared/components/tags_loader.vue';
import {
UNFINISHED_STATUS,
@@ -39,8 +40,6 @@ import {
} from '../mock_data';
import { DeleteModal } from '../stubs';
-const localVue = createLocalVue();
-
describe('Details Page', () => {
let wrapper;
let apolloProvider;
@@ -85,7 +84,7 @@ describe('Details Page', () => {
options,
config = defaultConfig,
} = {}) => {
- localVue.use(VueApollo);
+ Vue.use(VueApollo);
const requestHandlers = [
[getContainerRepositoryDetailsQuery, resolver],
@@ -96,7 +95,6 @@ describe('Details Page', () => {
apolloProvider = createMockApollo(requestHandlers);
wrapper = shallowMount(component, {
- localVue,
apolloProvider,
stubs: {
DeleteModal,
@@ -522,7 +520,7 @@ describe('Details Page', () => {
findDeleteImage().vm.$emit('start');
- await nextTick();
+ await waitForPromises();
expect(findTagsLoader().exists()).toBe(true);
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js
index 051d1e2a169..bd126fe532d 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js
@@ -1,6 +1,7 @@
import { GlSkeletonLoader, GlSprintf, GlAlert } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
+
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -8,7 +9,7 @@ import getContainerRepositoriesQuery from 'shared_queries/container_registry/get
import CleanupPolicyEnabledAlert from '~/packages_and_registries/shared/components/cleanup_policy_enabled_alert.vue';
import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
import DeleteImage from '~/packages_and_registries/container_registry/explorer/components/delete_image.vue';
-import CliCommands from '~/packages_and_registries/container_registry/explorer/components/list_page/cli_commands.vue';
+import CliCommands from '~/packages_and_registries/shared/components/cli_commands.vue';
import GroupEmptyState from '~/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state.vue';
import ImageList from '~/packages_and_registries/container_registry/explorer/components/list_page/image_list.vue';
import ProjectEmptyState from '~/packages_and_registries/container_registry/explorer/components/list_page/project_empty_state.vue';
@@ -38,8 +39,6 @@ import {
} from '../mock_data';
import { GlModal, GlEmptyState } from '../stubs';
-const localVue = createLocalVue();
-
describe('List Page', () => {
let wrapper;
let apolloProvider;
@@ -75,7 +74,7 @@ describe('List Page', () => {
config = { isGroupPage: false },
query = {},
} = {}) => {
- localVue.use(VueApollo);
+ Vue.use(VueApollo);
const requestHandlers = [
[getContainerRepositoriesQuery, resolver],
@@ -86,7 +85,6 @@ describe('List Page', () => {
apolloProvider = createMockApollo(requestHandlers);
wrapper = shallowMount(component, {
- localVue,
apolloProvider,
stubs: {
GlModal,
@@ -307,15 +305,8 @@ describe('List Page', () => {
await selectImageForDeletion();
findDeleteModal().vm.$emit('primary');
- await waitForApolloRequestRender();
-
- expect(wrapper.vm.itemToDelete).toEqual(deletedContainerRepository);
-
- const updatedImage = findImageList()
- .props('images')
- .find((i) => i.id === deletedContainerRepository.id);
- expect(updatedImage.status).toBe(deletedContainerRepository.status);
+ expect(mutationResolver).toHaveBeenCalledWith({ id: deletedContainerRepository.id });
});
it('should show a success alert when delete request is successful', async () => {
@@ -361,7 +352,7 @@ describe('List Page', () => {
findRegistrySearch().vm.$emit('filter:submit');
- await nextTick();
+ await waitForPromises();
};
it('has a search box element', async () => {
@@ -429,7 +420,7 @@ describe('List Page', () => {
await waitForApolloRequestRender();
findImageList().vm.$emit('prev-page');
- await nextTick();
+ await waitForPromises();
expect(resolver).toHaveBeenCalledWith(
expect.objectContaining({ before: pageInfo.startCursor }),
@@ -449,7 +440,7 @@ describe('List Page', () => {
await waitForApolloRequestRender();
findImageList().vm.$emit('next-page');
- await nextTick();
+ await waitForPromises();
expect(resolver).toHaveBeenCalledWith(
expect.objectContaining({ after: pageInfo.endCursor }),
@@ -471,8 +462,9 @@ describe('List Page', () => {
});
it('contains a description with the path of the item to delete', async () => {
+ await waitForPromises();
findImageList().vm.$emit('delete', { path: 'foo' });
- await nextTick();
+ await waitForPromises();
expect(findDeleteModal().html()).toContain('foo');
});
});
diff --git a/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js b/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js
index 44a7186904d..79894e25889 100644
--- a/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js
+++ b/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js
@@ -5,7 +5,7 @@ import {
GlSprintf,
GlEmptyState,
} from '@gitlab/ui';
-import { createLocalVue } from '@vue/test-utils';
+import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
@@ -21,8 +21,6 @@ import getDependencyProxyDetailsQuery from '~/packages_and_registries/dependency
import { proxyDetailsQuery, proxyData, pagination, proxyManifests } from './mock_data';
-const localVue = createLocalVue();
-
describe('DependencyProxyApp', () => {
let wrapper;
let apolloProvider;
@@ -35,14 +33,13 @@ describe('DependencyProxyApp', () => {
};
function createComponent({ provide = provideDefaults } = {}) {
- localVue.use(VueApollo);
+ Vue.use(VueApollo);
const requestHandlers = [[getDependencyProxyDetailsQuery, resolver]];
apolloProvider = createMockApollo(requestHandlers);
wrapper = shallowMountExtended(DependencyProxyApp, {
- localVue,
apolloProvider,
provide,
stubs: {
@@ -195,8 +192,9 @@ describe('DependencyProxyApp', () => {
});
});
- it('prev-page event on list fetches the previous page', () => {
+ it('prev-page event on list fetches the previous page', async () => {
findManifestList().vm.$emit('prev-page');
+ await waitForPromises();
expect(resolver).toHaveBeenCalledWith({
before: pagination().startCursor,
@@ -206,8 +204,9 @@ describe('DependencyProxyApp', () => {
});
});
- it('next-page event on list fetches the next page', () => {
+ it('next-page event on list fetches the next page', async () => {
findManifestList().vm.$emit('next-page');
+ await waitForPromises();
expect(resolver).toHaveBeenCalledWith({
after: pagination().endCursor,
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/app_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/app_spec.js
index 2868af84181..69c78e64e22 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/app_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/app_spec.js
@@ -1,6 +1,6 @@
import { GlEmptyState } from '@gitlab/ui';
-import { mount, createLocalVue } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import { mount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import stubChildren from 'helpers/stub_children';
@@ -19,8 +19,7 @@ import Tracking from '~/tracking';
import { mavenPackage, mavenFiles, npmPackage } from '../../mock_data';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
useMockLocationHelper();
@@ -60,7 +59,6 @@ describe('PackagesApp', () => {
});
wrapper = mount(PackagesApp, {
- localVue,
store,
stubs: {
...stubChildren(PackagesApp),
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/details_title_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/details_title_spec.js
index 24bd80ba80c..b504f7489ab 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/details_title_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/details_title_spec.js
@@ -1,17 +1,20 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import component from '~/packages_and_registries/infrastructure_registry/details/components/details_title.vue';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import { terraformModule, mavenFiles, npmPackage } from '../../mock_data';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('PackageTitle', () => {
let wrapper;
let store;
- function createComponent({ packageFiles = mavenFiles, packageEntity = terraformModule } = {}) {
+ async function createComponent({
+ packageFiles = mavenFiles,
+ packageEntity = terraformModule,
+ } = {}) {
store = new Vuex.Store({
state: {
packageEntity,
@@ -23,13 +26,12 @@ describe('PackageTitle', () => {
});
wrapper = shallowMount(component, {
- localVue,
store,
stubs: {
TitleArea,
},
});
- return wrapper.vm.$nextTick();
+ await nextTick();
}
const findTitleArea = () => wrapper.findComponent(TitleArea);
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/terraform_installation_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/terraform_installation_spec.js
index 6ff4a4c51ef..78c1b840dbc 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/terraform_installation_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/terraform_installation_spec.js
@@ -1,11 +1,11 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import TerraformInstallation from '~/packages_and_registries/infrastructure_registry/details/components/terraform_installation.vue';
import CodeInstructions from '~/vue_shared/components/registry/code_instruction.vue';
import { terraformModule as packageEntity } from '../../mock_data';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('TerraformInstallation', () => {
let wrapper;
@@ -22,7 +22,6 @@ describe('TerraformInstallation', () => {
function createComponent() {
wrapper = shallowMount(TerraformInstallation, {
- localVue,
store,
});
}
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap
index 7cdf21dde46..d82af8f9e63 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap
@@ -3,6 +3,7 @@
exports[`packages_list_app renders 1`] = `
<div>
<infrastructure-title-stub
+ count="1"
helpurl="foo"
/>
@@ -37,8 +38,8 @@ exports[`packages_list_app renders 1`] = `
class="gl-font-size-h-display gl-line-height-36 h4"
>
- There are no packages yet
-
+ There are no packages yet
+
</h1>
<p
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_search_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_search_spec.js
index b519ab00d06..e5230417c78 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_search_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_search_spec.js
@@ -1,11 +1,11 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import component from '~/packages_and_registries/infrastructure_registry/list/components/infrastructure_search.vue';
import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
import UrlSync from '~/vue_shared/components/url_sync.vue';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('Infrastructure Search', () => {
let wrapper;
@@ -48,7 +48,6 @@ describe('Infrastructure Search', () => {
createStore(isGroupPage);
wrapper = shallowMount(component, {
- localVue,
store,
stubs: {
UrlSync,
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_title_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_title_spec.js
index b0e586f189a..72d08d5683b 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_title_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_title_spec.js
@@ -10,7 +10,9 @@ describe('Infrastructure Title', () => {
const findTitleArea = () => wrapper.find(TitleArea);
const findMetadataItem = () => wrapper.find(MetadataItem);
- const mountComponent = (propsData = { helpUrl: 'foo' }) => {
+ const exampleProps = { helpUrl: 'http://example.gitlab.com/help' };
+
+ const mountComponent = (propsData = exampleProps) => {
wrapper = shallowMount(component, {
store,
propsData,
@@ -26,23 +28,36 @@ describe('Infrastructure Title', () => {
});
describe('title area', () => {
- it('exists', () => {
+ beforeEach(() => {
mountComponent();
+ });
+ it('exists', () => {
expect(findTitleArea().exists()).toBe(true);
});
- it('has the correct props', () => {
- mountComponent();
+ it('has the correct title', () => {
+ expect(findTitleArea().props('title')).toBe('Infrastructure Registry');
+ });
+
+ describe('with no modules', () => {
+ it('has no info message', () => {
+ expect(findTitleArea().props('infoMessages')).toStrictEqual([]);
+ });
+ });
+
+ describe('with at least one module', () => {
+ beforeEach(() => {
+ mountComponent({ ...exampleProps, count: 1 });
+ });
- expect(findTitleArea().props()).toMatchObject({
- title: 'Infrastructure Registry',
- infoMessages: [
+ it('has an info message', () => {
+ expect(findTitleArea().props('infoMessages')).toStrictEqual([
{
text: 'Publish and share your modules. %{docLinkStart}More information%{docLinkEnd}',
- link: 'foo',
+ link: exampleProps.helpUrl,
},
- ],
+ ]);
});
});
});
@@ -51,15 +66,15 @@ describe('Infrastructure Title', () => {
count | exist | text
${null} | ${false} | ${''}
${undefined} | ${false} | ${''}
- ${0} | ${true} | ${'0 Modules'}
+ ${0} | ${false} | ${''}
${1} | ${true} | ${'1 Module'}
${2} | ${true} | ${'2 Modules'}
`('when count is $count metadata item', ({ count, exist, text }) => {
beforeEach(() => {
- mountComponent({ count, helpUrl: 'foo' });
+ mountComponent({ ...exampleProps, count });
});
- it(`is ${exist} that it exists`, () => {
+ it(exist ? 'exists' : 'does not exist', () => {
expect(findMetadataItem().exists()).toBe(exist);
});
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js
index cad75d2a858..31616e0b2f5 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js
@@ -1,5 +1,6 @@
import { GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import setWindowLocation from 'helpers/set_window_location_helper';
import createFlash from '~/flash';
@@ -17,8 +18,7 @@ import InfrastructureSearch from '~/packages_and_registries/infrastructure_regis
jest.mock('~/lib/utils/common_utils');
jest.mock('~/flash');
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('packages_list_app', () => {
let wrapper;
@@ -35,7 +35,7 @@ describe('packages_list_app', () => {
const findListComponent = () => wrapper.find(PackageList);
const findInfrastructureSearch = () => wrapper.find(InfrastructureSearch);
- const createStore = (filter = []) => {
+ const createStore = ({ filter = [], packageCount = 0 } = {}) => {
store = new Vuex.Store({
state: {
isLoading: false,
@@ -46,6 +46,9 @@ describe('packages_list_app', () => {
packageHelpUrl: 'foo',
},
filter,
+ pagination: {
+ total: packageCount,
+ },
},
});
store.dispatch = jest.fn();
@@ -53,7 +56,6 @@ describe('packages_list_app', () => {
const mountComponent = (provide) => {
wrapper = shallowMount(PackageListApp, {
- localVue,
store,
stubs: {
GlEmptyState,
@@ -69,6 +71,7 @@ describe('packages_list_app', () => {
beforeEach(() => {
createStore();
jest.spyOn(packageUtils, 'getQueryParams').mockReturnValue({});
+ mountComponent();
});
afterEach(() => {
@@ -76,30 +79,26 @@ describe('packages_list_app', () => {
});
it('renders', () => {
+ createStore({ packageCount: 1 });
mountComponent();
+
expect(wrapper.element).toMatchSnapshot();
});
- it('call requestPackagesList on page:changed', () => {
- mountComponent();
- store.dispatch.mockClear();
-
+ it('calls requestPackagesList on page:changed', () => {
const list = findListComponent();
list.vm.$emit('page:changed', 1);
expect(store.dispatch).toHaveBeenCalledWith('requestPackagesList', { page: 1 });
});
- it('call requestDeletePackage on package:delete', () => {
- mountComponent();
-
+ it('calls requestDeletePackage on package:delete', () => {
const list = findListComponent();
list.vm.$emit('package:delete', 'foo');
+
expect(store.dispatch).toHaveBeenCalledWith('requestDeletePackage', 'foo');
});
- it('does call requestPackagesList only one time on render', () => {
- mountComponent();
-
+ it('calls requestPackagesList only once on render', () => {
expect(store.dispatch).toHaveBeenCalledTimes(3);
expect(store.dispatch).toHaveBeenNthCalledWith(1, 'setSorting', expect.any(Object));
expect(store.dispatch).toHaveBeenNthCalledWith(2, 'setFilter', expect.any(Array));
@@ -114,9 +113,12 @@ describe('packages_list_app', () => {
orderBy: 'created',
};
- it('calls setSorting with the query string based sorting', () => {
+ beforeEach(() => {
+ createStore();
jest.spyOn(packageUtils, 'getQueryParams').mockReturnValue(defaultQueryParamsMock);
+ });
+ it('calls setSorting with the query string based sorting', () => {
mountComponent();
expect(store.dispatch).toHaveBeenNthCalledWith(1, 'setSorting', {
@@ -126,8 +128,6 @@ describe('packages_list_app', () => {
});
it('calls setFilter with the query string based filters', () => {
- jest.spyOn(packageUtils, 'getQueryParams').mockReturnValue(defaultQueryParamsMock);
-
mountComponent();
expect(store.dispatch).toHaveBeenNthCalledWith(2, 'setFilter', [
@@ -151,8 +151,6 @@ describe('packages_list_app', () => {
describe('empty state', () => {
it('generate the correct empty list link', () => {
- mountComponent();
-
const link = findListComponent().find(GlLink);
expect(link.attributes('href')).toBe(emptyListHelpUrl);
@@ -160,8 +158,6 @@ describe('packages_list_app', () => {
});
it('includes the right content on the default tab', () => {
- mountComponent();
-
const heading = findEmptyState().find('h1');
expect(heading.text()).toBe('There are no packages yet');
@@ -170,7 +166,7 @@ describe('packages_list_app', () => {
describe('filter without results', () => {
beforeEach(() => {
- createStore([{ type: 'something' }]);
+ createStore({ filter: [{ type: 'something' }] });
mountComponent();
});
@@ -182,20 +178,30 @@ describe('packages_list_app', () => {
});
});
- describe('Search', () => {
- it('exists', () => {
- mountComponent();
-
- expect(findInfrastructureSearch().exists()).toBe(true);
+ describe('search', () => {
+ describe('with no packages', () => {
+ it('does not exist', () => {
+ expect(findInfrastructureSearch().exists()).toBe(false);
+ });
});
- it('on update fetches data from the store', () => {
- mountComponent();
- store.dispatch.mockClear();
+ describe('with packages', () => {
+ beforeEach(() => {
+ createStore({ packageCount: 1 });
+ mountComponent();
+ });
- findInfrastructureSearch().vm.$emit('update');
+ it('exists', () => {
+ expect(findInfrastructureSearch().exists()).toBe(true);
+ });
- expect(store.dispatch).toHaveBeenCalledWith('requestPackagesList');
+ it('on update fetches data from the store', () => {
+ store.dispatch.mockClear();
+
+ findInfrastructureSearch().vm.$emit('update');
+
+ expect(store.dispatch).toHaveBeenCalledWith('requestPackagesList');
+ });
});
});
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_spec.js
index 26569f20e94..fed82653016 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_spec.js
@@ -1,5 +1,6 @@
import { GlTable, GlPagination, GlModal } from '@gitlab/ui';
-import { mount, createLocalVue } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import { last } from 'lodash';
import Vuex from 'vuex';
import stubChildren from 'helpers/stub_children';
@@ -11,8 +12,7 @@ import { TRACK_CATEGORY } from '~/packages_and_registries/infrastructure_registr
import Tracking from '~/tracking';
import { packageList } from '../../mock_data';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('packages_list', () => {
let wrapper;
@@ -61,7 +61,6 @@ describe('packages_list', () => {
createStore(isGroupPage, packages, isLoading);
wrapper = mount(PackagesList, {
- localVue,
store,
stubs: {
...stubChildren(PackagesList),
@@ -121,16 +120,15 @@ describe('packages_list', () => {
mountComponent();
});
- it('setItemToBeDeleted sets itemToBeDeleted and open the modal', () => {
+ it('setItemToBeDeleted sets itemToBeDeleted and open the modal', async () => {
const mockModalShow = jest.spyOn(wrapper.vm.$refs.packageListDeleteModal, 'show');
const item = last(wrapper.vm.list);
findPackagesListRow().vm.$emit('packageToDelete', item);
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.vm.itemToBeDeleted).toEqual(item);
- expect(mockModalShow).toHaveBeenCalled();
- });
+ await nextTick();
+ expect(wrapper.vm.itemToBeDeleted).toEqual(item);
+ expect(mockModalShow).toHaveBeenCalled();
});
it('deleteItemConfirmation resets itemToBeDeleted', () => {
@@ -141,15 +139,14 @@ describe('packages_list', () => {
expect(wrapper.vm.itemToBeDeleted).toEqual(null);
});
- it('deleteItemConfirmation emit package:delete', () => {
+ it('deleteItemConfirmation emit package:delete', async () => {
const itemToBeDeleted = { id: 2 };
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ itemToBeDeleted });
wrapper.vm.deleteItemConfirmation();
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.emitted('package:delete')[0]).toEqual([itemToBeDeleted]);
- });
+ await nextTick();
+ expect(wrapper.emitted('package:delete')[0]).toEqual([itemToBeDeleted]);
});
it('deleteItemCanceled resets itemToBeDeleted', () => {
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/package_list_row_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/package_list_row_spec.js
index 1052fdd1dda..79c1b18c9f9 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/package_list_row_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/package_list_row_spec.js
@@ -1,4 +1,5 @@
import { GlLink } from '@gitlab/ui';
+import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
@@ -126,7 +127,7 @@ describe('packages_list_row', () => {
findDeleteButton().vm.$emit('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.emitted('packageToDelete')).toBeTruthy();
expect(wrapper.emitted('packageToDelete')[0]).toEqual([packageWithoutTags]);
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/version_row_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/version_row_spec.js.snap
index 7aa42a1f1e5..bdd0fe3ad9e 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/version_row_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/version_row_spec.js.snap
@@ -72,12 +72,14 @@ exports[`VersionRow renders 1`] = `
<div
class="gl-display-flex gl-align-items-center gl-min-h-6"
>
- Created
- <time-ago-tooltip-stub
- cssclass=""
- time="2021-08-10T09:33:54Z"
- tooltipplacement="top"
- />
+ <span>
+ Created
+ <time-ago-tooltip-stub
+ cssclass=""
+ time="2021-08-10T09:33:54Z"
+ tooltipplacement="top"
+ />
+ </span>
</div>
</div>
</div>
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_title_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_title_spec.js
index 6ad6007c9da..5da9cfffaae 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/package_title_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_title_spec.js
@@ -1,5 +1,6 @@
import { GlIcon, GlSprintf } from '@gitlab/ui';
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
+import { nextTick } from 'vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import PackageTags from '~/packages_and_registries/shared/components/package_tags.vue';
@@ -24,7 +25,7 @@ const packageWithTags = {
describe('PackageTitle', () => {
let wrapper;
- function createComponent(packageEntity = packageWithTags) {
+ async function createComponent(packageEntity = packageWithTags) {
wrapper = shallowMountExtended(PackageTitle, {
propsData: { packageEntity },
stubs: {
@@ -35,7 +36,7 @@ describe('PackageTitle', () => {
GlResizeObserver: createMockDirective(),
},
});
- return wrapper.vm.$nextTick();
+ await nextTick();
}
const findTitleArea = () => wrapper.findComponent(TitleArea);
@@ -71,7 +72,7 @@ describe('PackageTitle', () => {
await createComponent();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findPackageBadges()).toHaveLength(packageTags().length);
});
@@ -85,7 +86,7 @@ describe('PackageTitle', () => {
const { value } = getBinding(wrapper.element, 'gl-resize-observer');
value();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findPackageBadges()).toHaveLength(packageTags().length);
});
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/functional/delete_package_spec.js b/spec/frontend/packages_and_registries/package_registry/components/functional/delete_package_spec.js
index 5de30829fa5..14a70def7d0 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/functional/delete_package_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/functional/delete_package_spec.js
@@ -1,4 +1,4 @@
-import { createLocalVue } from '@vue/test-utils';
+import Vue from 'vue';
import VueApollo from 'vue-apollo';
import waitForPromises from 'helpers/wait_for_promises';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
@@ -16,8 +16,6 @@ import {
jest.mock('~/flash');
-const localVue = createLocalVue();
-
describe('DeletePackage', () => {
let wrapper;
let apolloProvider;
@@ -27,7 +25,7 @@ describe('DeletePackage', () => {
const eventPayload = { id: '1' };
function createComponent(propsData = {}) {
- localVue.use(VueApollo);
+ Vue.use(VueApollo);
const requestHandlers = [
[getPackagesQuery, resolver],
@@ -37,7 +35,6 @@ describe('DeletePackage', () => {
wrapper = shallowMountExtended(DeletePackage, {
propsData,
- localVue,
apolloProvider,
scopedSlots: {
default(props) {
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
index 9467a613b2a..12a3eaa3873 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
@@ -1,5 +1,5 @@
import { GlSprintf } from '@gitlab/ui';
-import { createLocalVue } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import VueRouter from 'vue-router';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
@@ -17,8 +17,7 @@ import { PACKAGE_ERROR_STATUS } from '~/packages_and_registries/package_registry
import ListItem from '~/vue_shared/components/registry/list_item.vue';
import { packageData, packagePipelines, packageProject, packageTags } from '../../mock_data';
-const localVue = createLocalVue();
-localVue.use(VueRouter);
+Vue.use(VueRouter);
describe('packages_list_row', () => {
let wrapper;
@@ -47,7 +46,6 @@ describe('packages_list_row', () => {
provide = defaultProvide,
} = {}) => {
wrapper = shallowMountExtended(PackagesListRow, {
- localVue,
provide,
stubs: {
ListItem,
@@ -121,7 +119,7 @@ describe('packages_list_row', () => {
findDeleteButton().vm.$emit('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.emitted('packageToDelete')).toBeTruthy();
expect(wrapper.emitted('packageToDelete')[0]).toEqual([packageWithoutTags]);
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js
index bed7a07ff36..9e91b15bc6e 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js
@@ -5,7 +5,10 @@ import component from '~/packages_and_registries/package_registry/components/lis
import PackageTypeToken from '~/packages_and_registries/package_registry/components/list/tokens/package_type_token.vue';
import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
import UrlSync from '~/vue_shared/components/url_sync.vue';
+import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
+import { LIST_KEY_CREATED_AT } from '~/packages_and_registries/package_registry/constants';
+
import { getQueryParams, extractFilterAndSorting } from '~/packages_and_registries/shared/utils';
jest.mock('~/packages_and_registries/shared/utils');
@@ -22,6 +25,7 @@ describe('Package Search', () => {
const findRegistrySearch = () => wrapper.findComponent(RegistrySearch);
const findUrlSync = () => wrapper.findComponent(UrlSync);
+ const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
const mountComponent = (isGroupPage = false) => {
wrapper = shallowMountExtended(component, {
@@ -32,6 +36,7 @@ describe('Package Search', () => {
},
stubs: {
UrlSync,
+ LocalStorageSync,
},
});
};
@@ -64,6 +69,19 @@ describe('Package Search', () => {
expect(findUrlSync().exists()).toBe(true);
});
+ it('has a LocalStorageSync component', () => {
+ mountComponent();
+
+ expect(findLocalStorageSync().props()).toMatchObject({
+ asJson: true,
+ storageKey: 'package_registry_list_sorting',
+ value: {
+ orderBy: LIST_KEY_CREATED_AT,
+ sort: 'desc',
+ },
+ });
+ });
+
it.each`
isGroupPage | page
${false} | ${'project'}
@@ -92,7 +110,7 @@ describe('Package Search', () => {
await nextTick();
- expect(findRegistrySearch().props('sorting')).toEqual({ sort: 'foo', orderBy: 'name' });
+ expect(findRegistrySearch().props('sorting')).toEqual({ sort: 'foo', orderBy: 'created_at' });
// there is always a first call on mounted that emits up default values
expect(wrapper.emitted('update')[1]).toEqual([
@@ -101,7 +119,7 @@ describe('Package Search', () => {
packageName: '',
packageType: undefined,
},
- sort: 'NAME_FOO',
+ sort: 'CREATED_FOO',
},
]);
});
@@ -133,7 +151,7 @@ describe('Package Search', () => {
packageName: '',
packageType: undefined,
},
- sort: 'NAME_DESC',
+ sort: 'CREATED_DESC',
},
]);
});
diff --git a/spec/frontend/packages_and_registries/package_registry/mock_data.js b/spec/frontend/packages_and_registries/package_registry/mock_data.js
index c6a59f20998..0a4747fc9ec 100644
--- a/spec/frontend/packages_and_registries/package_registry/mock_data.js
+++ b/spec/frontend/packages_and_registries/package_registry/mock_data.js
@@ -119,6 +119,7 @@ export const packageVersions = () => [
];
export const packageData = (extend) => ({
+ __typename: 'Package',
id: 'gid://gitlab/Packages::Package/111',
canDestroy: true,
name: '@gitlab-org/package-15',
diff --git a/spec/frontend/packages_and_registries/package_registry/pages/__snapshots__/list_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/pages/__snapshots__/list_spec.js.snap
index ed96abe24b1..0154486e224 100644
--- a/spec/frontend/packages_and_registries/package_registry/pages/__snapshots__/list_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/pages/__snapshots__/list_spec.js.snap
@@ -38,8 +38,8 @@ exports[`PackagesListApp renders 1`] = `
class="gl-font-size-h-display gl-line-height-36 h4"
>
- There are no packages yet
-
+ There are no packages yet
+
</h1>
<p
diff --git a/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js b/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js
index 637e2edf3be..a7e31d42c9e 100644
--- a/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js
@@ -1,6 +1,6 @@
import { GlEmptyState, GlBadge, GlTabs, GlTab } from '@gitlab/ui';
-import { createLocalVue } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import Vue, { nextTick } from 'vue';
+
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
@@ -41,8 +41,6 @@ import {
jest.mock('~/flash');
useMockLocationHelper();
-const localVue = createLocalVue();
-
describe('PackagesApp', () => {
let wrapper;
let apolloProvider;
@@ -59,12 +57,14 @@ describe('PackagesApp', () => {
breadCrumbState,
};
+ const { __typename, ...packageWithoutTypename } = packageData();
+
function createComponent({
resolver = jest.fn().mockResolvedValue(packageDetailsQuery()),
fileDeleteMutationResolver = jest.fn().mockResolvedValue(packageDestroyFileMutation()),
routeId = '1',
} = {}) {
- localVue.use(VueApollo);
+ Vue.use(VueApollo);
const requestHandlers = [
[getPackageDetails, resolver],
@@ -73,7 +73,6 @@ describe('PackagesApp', () => {
apolloProvider = createMockApollo(requestHandlers);
wrapper = shallowMountExtended(PackagesApp, {
- localVue,
apolloProvider,
provide,
stubs: {
@@ -133,7 +132,7 @@ describe('PackagesApp', () => {
expect(findPackageTitle().exists()).toBe(true);
expect(findPackageTitle().props()).toMatchObject({
- packageEntity: expect.objectContaining(packageData()),
+ packageEntity: expect.objectContaining(packageWithoutTypename),
});
});
@@ -156,7 +155,7 @@ describe('PackagesApp', () => {
expect(findPackageHistory().exists()).toBe(true);
expect(findPackageHistory().props()).toMatchObject({
- packageEntity: expect.objectContaining(packageData()),
+ packageEntity: expect.objectContaining(packageWithoutTypename),
projectName: packageDetailsQuery().data.package.project.name,
});
});
@@ -168,7 +167,7 @@ describe('PackagesApp', () => {
expect(findAdditionalMetadata().exists()).toBe(true);
expect(findAdditionalMetadata().props()).toMatchObject({
- packageEntity: expect.objectContaining(packageData()),
+ packageEntity: expect.objectContaining(packageWithoutTypename),
});
});
@@ -179,7 +178,7 @@ describe('PackagesApp', () => {
expect(findInstallationCommands().exists()).toBe(true);
expect(findInstallationCommands().props()).toMatchObject({
- packageEntity: expect.objectContaining(packageData()),
+ packageEntity: expect.objectContaining(packageWithoutTypename),
});
});
diff --git a/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js b/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js
index 2ac2a6455ef..0e74fbbc6d9 100644
--- a/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js
@@ -1,8 +1,8 @@
import { GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui';
-import { createLocalVue } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
-import { nextTick } from 'vue';
+
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -27,8 +27,6 @@ import { packagesListQuery, packageData, pagination } from '../mock_data';
jest.mock('~/lib/utils/common_utils');
jest.mock('~/flash');
-const localVue = createLocalVue();
-
describe('PackagesListApp', () => {
let wrapper;
let apolloProvider;
@@ -61,13 +59,12 @@ describe('PackagesListApp', () => {
resolver = jest.fn().mockResolvedValue(packagesListQuery()),
provide = defaultProvide,
} = {}) => {
- localVue.use(VueApollo);
+ Vue.use(VueApollo);
const requestHandlers = [[getPackagesQuery, resolver]];
apolloProvider = createMockApollo(requestHandlers);
wrapper = shallowMountExtended(ListPage, {
- localVue,
apolloProvider,
provide,
stubs: {
@@ -85,7 +82,7 @@ describe('PackagesListApp', () => {
wrapper.destroy();
});
- const waitForFirstRequest = () => {
+ const waitForFirstRequest = async () => {
// emit a search update so the query is executed
findSearch().vm.$emit('update', { sort: 'NAME_DESC', filters: [] });
return waitForPromises();
@@ -149,11 +146,10 @@ describe('PackagesListApp', () => {
beforeEach(() => {
resolver = jest.fn().mockResolvedValue(packagesListQuery());
mountComponent({ resolver });
-
- return waitForFirstRequest();
});
- it('exists and has the right props', () => {
+ it('exists and has the right props', async () => {
+ await waitForFirstRequest();
expect(findListComponent().props()).toMatchObject({
list: expect.arrayContaining([expect.objectContaining({ id: packageData().id })]),
isLoading: false,
@@ -161,16 +157,20 @@ describe('PackagesListApp', () => {
});
});
- it('when list emits next-page fetches the next set of records', () => {
+ it('when list emits next-page fetches the next set of records', async () => {
+ await waitForFirstRequest();
findListComponent().vm.$emit('next-page');
+ await waitForPromises();
expect(resolver).toHaveBeenCalledWith(
expect.objectContaining({ after: pagination().endCursor, first: GRAPHQL_PAGE_SIZE }),
);
});
- it('when list emits prev-page fetches the prev set of records', () => {
+ it('when list emits prev-page fetches the prev set of records', async () => {
+ await waitForFirstRequest();
findListComponent().vm.$emit('prev-page');
+ await waitForPromises();
expect(resolver).toHaveBeenCalledWith(
expect.objectContaining({ before: pagination().startCursor, last: GRAPHQL_PAGE_SIZE }),
diff --git a/spec/frontend/packages_and_registries/settings/group/components/dependency_proxy_settings_spec.js b/spec/frontend/packages_and_registries/settings/group/components/dependency_proxy_settings_spec.js
index f6c1d212b51..94f56e5c979 100644
--- a/spec/frontend/packages_and_registries/settings/group/components/dependency_proxy_settings_spec.js
+++ b/spec/frontend/packages_and_registries/settings/group/components/dependency_proxy_settings_spec.js
@@ -1,5 +1,5 @@
import { GlSprintf, GlToggle } from '@gitlab/ui';
-import { createLocalVue } from '@vue/test-utils';
+import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -33,8 +33,6 @@ import {
jest.mock('~/flash');
jest.mock('~/packages_and_registries/settings/group/graphql/utils/optimistic_responses');
-const localVue = createLocalVue();
-
describe('DependencyProxySettings', () => {
let wrapper;
let apolloProvider;
@@ -47,7 +45,7 @@ describe('DependencyProxySettings', () => {
groupDependencyProxyPath: 'group_dependency_proxy_path',
};
- localVue.use(VueApollo);
+ Vue.use(VueApollo);
const mountComponent = ({
provide = defaultProvide,
@@ -63,7 +61,6 @@ describe('DependencyProxySettings', () => {
apolloProvider = createMockApollo(requestHandlers);
wrapper = shallowMountExtended(component, {
- localVue,
apolloProvider,
provide,
propsData: {
diff --git a/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js b/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js
index 933dac7f5a8..5c30074a6af 100644
--- a/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js
+++ b/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js
@@ -1,7 +1,8 @@
import { GlAlert } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
-import { nextTick } from 'vue';
+
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import PackagesSettings from '~/packages_and_registries/settings/group/components/packages_settings.vue';
@@ -19,8 +20,6 @@ import {
jest.mock('~/flash');
-const localVue = createLocalVue();
-
describe('Group Settings App', () => {
let wrapper;
let apolloProvider;
@@ -36,14 +35,13 @@ describe('Group Settings App', () => {
resolver = jest.fn().mockResolvedValue(groupPackageSettingsMock),
provide = defaultProvide,
} = {}) => {
- localVue.use(VueApollo);
+ Vue.use(VueApollo);
const requestHandlers = [[getGroupPackagesSettingsQuery, resolver]];
apolloProvider = createMockApollo(requestHandlers);
wrapper = shallowMount(component, {
- localVue,
apolloProvider,
provide,
mocks: {
diff --git a/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js b/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js
index 693af21e24a..d92d42e7834 100644
--- a/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js
+++ b/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js
@@ -1,5 +1,5 @@
import { GlSprintf, GlLink } from '@gitlab/ui';
-import { createLocalVue } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -28,8 +28,6 @@ import {
jest.mock('~/flash');
jest.mock('~/packages_and_registries/settings/group/graphql/utils/optimistic_responses');
-const localVue = createLocalVue();
-
describe('Packages Settings', () => {
let wrapper;
let apolloProvider;
@@ -42,14 +40,13 @@ describe('Packages Settings', () => {
const mountComponent = ({
mutationResolver = jest.fn().mockResolvedValue(groupPackageSettingsMutationMock()),
} = {}) => {
- localVue.use(VueApollo);
+ Vue.use(VueApollo);
const requestHandlers = [[updateNamespacePackageSettings, mutationResolver]];
apolloProvider = createMockApollo(requestHandlers);
wrapper = shallowMountExtended(component, {
- localVue,
apolloProvider,
provide: defaultProvide,
propsData: {
@@ -252,7 +249,7 @@ describe('Packages Settings', () => {
emitMavenSettingsUpdate();
- await wrapper.vm.$nextTick();
+ await nextTick();
// errors are reset on mutation call
expect(findMavenDuplicatedSettings().props('duplicateExceptionRegexError')).toBe('');
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js
index 8266f9bee89..a6c929844b1 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js
@@ -1,7 +1,9 @@
import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import component from '~/packages_and_registries/settings/project/components/registry_settings_app.vue';
import SettingsForm from '~/packages_and_registries/settings/project/components/settings_form.vue';
import {
@@ -19,8 +21,6 @@ import {
containerExpirationPolicyData,
} from '../mock_data';
-const localVue = createLocalVue();
-
describe('Registry Settings App', () => {
let wrapper;
let fakeApollo;
@@ -55,17 +55,14 @@ describe('Registry Settings App', () => {
};
const mountComponentWithApollo = ({ provide = defaultProvidedValues, resolver } = {}) => {
- localVue.use(VueApollo);
+ Vue.use(VueApollo);
const requestHandlers = [[expirationPolicyQuery, resolver]];
fakeApollo = createMockApollo(requestHandlers);
mountComponent(provide, {
- localVue,
apolloProvider: fakeApollo,
});
-
- return requestHandlers.map((request) => request[1]);
};
afterEach(() => {
@@ -101,25 +98,25 @@ describe('Registry Settings App', () => {
${'response and changes'} | ${expirationPolicyPayload()} | ${{ ...containerExpirationPolicyData(), nameRegex: '12345' }} | ${true}
${'response and empty'} | ${expirationPolicyPayload()} | ${{}} | ${true}
`('$description', async ({ apiResponse, workingCopy, result }) => {
- const requests = mountComponentWithApollo({
+ mountComponentWithApollo({
provide: { ...defaultProvidedValues, enableHistoricEntries: true },
resolver: jest.fn().mockResolvedValue(apiResponse),
});
- await Promise.all(requests);
+ await waitForPromises();
findSettingsComponent().vm.$emit('input', workingCopy);
- await wrapper.vm.$nextTick();
+ await waitForPromises();
expect(findSettingsComponent().props('isEdited')).toBe(result);
});
});
it('renders the setting form', async () => {
- const requests = mountComponentWithApollo({
+ mountComponentWithApollo({
resolver: jest.fn().mockResolvedValue(expirationPolicyPayload()),
});
- await Promise.all(requests);
+ await waitForPromises();
expect(findSettingsComponent().exists()).toBe(true);
});
@@ -153,11 +150,11 @@ describe('Registry Settings App', () => {
});
describe('fetchSettingsError', () => {
- beforeEach(() => {
- const requests = mountComponentWithApollo({
+ beforeEach(async () => {
+ mountComponentWithApollo({
resolver: jest.fn().mockRejectedValue(new Error('GraphQL error')),
});
- return Promise.all(requests);
+ await waitForPromises();
});
it('the form is hidden', () => {
@@ -175,14 +172,14 @@ describe('Registry Settings App', () => {
${true} | ${true}
${false} | ${false}
`('is $isShown that the form is shown', async ({ enableHistoricEntries, isShown }) => {
- const requests = mountComponentWithApollo({
+ mountComponentWithApollo({
provide: {
...defaultProvidedValues,
enableHistoricEntries,
},
resolver: jest.fn().mockResolvedValue(emptyExpirationPolicyPayload()),
});
- await Promise.all(requests);
+ await waitForPromises();
expect(findSettingsComponent().exists()).toBe(isShown);
});
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/settings_form_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/settings_form_spec.js
index bc104a25ef9..625aa37fc0f 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/settings_form_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/settings_form_spec.js
@@ -1,5 +1,6 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
+import { nextTick } from 'vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { GlCard, GlLoadingIcon } from 'jest/packages_and_registries/shared/stubs';
@@ -201,7 +202,7 @@ describe('Settings Form', () => {
finder().vm.$emit('input', 'foo');
expect(finder().props('error')).toEqual('bar');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(finder().props('error')).toEqual('');
});
@@ -213,7 +214,7 @@ describe('Settings Form', () => {
finder().vm.$emit('validation', false);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findSaveButton().props('disabled')).toBe(true);
});
@@ -252,7 +253,7 @@ describe('Settings Form', () => {
findForm().trigger('reset');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findKeepRegexInput().props('error')).toBe('');
expect(findRemoveRegexInput().props('error')).toBe('');
@@ -319,7 +320,7 @@ describe('Settings Form', () => {
findForm().trigger('submit');
await waitForPromises();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(UPDATE_SETTINGS_SUCCESS_MESSAGE);
});
@@ -335,7 +336,7 @@ describe('Settings Form', () => {
findForm().trigger('submit');
await waitForPromises();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith('foo');
});
@@ -349,7 +350,7 @@ describe('Settings Form', () => {
findForm().trigger('submit');
await waitForPromises();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(UPDATE_SETTINGS_ERROR_MESSAGE);
});
@@ -368,7 +369,7 @@ describe('Settings Form', () => {
findForm().trigger('submit');
await waitForPromises();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findKeepRegexInput().props('error')).toEqual('baz');
});
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/mock_data.js b/spec/frontend/packages_and_registries/settings/project/settings/mock_data.js
index a56bb75f8ed..33406c98f4b 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/mock_data.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/mock_data.js
@@ -13,6 +13,7 @@ export const expirationPolicyPayload = (override) => ({
project: {
id: '1',
containerExpirationPolicy: {
+ __typename: 'ContainerExpirationPolicy',
...containerExpirationPolicyData(),
...override,
},
diff --git a/spec/frontend/pager_spec.js b/spec/frontend/pager_spec.js
index ff352303143..043ea470436 100644
--- a/spec/frontend/pager_spec.js
+++ b/spec/frontend/pager_spec.js
@@ -1,6 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import { TEST_HOST } from 'helpers/test_constants';
+import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import { removeParams } from '~/lib/utils/url_utility';
import Pager from '~/pager';
@@ -64,67 +65,59 @@ describe('pager', () => {
Pager.init();
});
- it('shows loader while loading next page', (done) => {
+ it('shows loader while loading next page', async () => {
mockSuccess();
jest.spyOn(Pager.loading, 'show').mockImplementation(() => {});
Pager.getOld();
- setImmediate(() => {
- expect(Pager.loading.show).toHaveBeenCalled();
+ await waitForPromises();
- done();
- });
+ expect(Pager.loading.show).toHaveBeenCalled();
});
- it('hides loader on success', (done) => {
+ it('hides loader on success', async () => {
mockSuccess();
jest.spyOn(Pager.loading, 'hide').mockImplementation(() => {});
Pager.getOld();
- setImmediate(() => {
- expect(Pager.loading.hide).toHaveBeenCalled();
+ await waitForPromises();
- done();
- });
+ expect(Pager.loading.hide).toHaveBeenCalled();
});
- it('hides loader on error', (done) => {
+ it('hides loader on error', async () => {
mockError();
jest.spyOn(Pager.loading, 'hide').mockImplementation(() => {});
Pager.getOld();
- setImmediate(() => {
- expect(Pager.loading.hide).toHaveBeenCalled();
+ await waitForPromises();
- done();
- });
+ expect(Pager.loading.hide).toHaveBeenCalled();
});
- it('sends request to url with offset and limit params', (done) => {
+ it('sends request to url with offset and limit params', async () => {
Pager.offset = 100;
Pager.limit = 20;
Pager.getOld();
- setImmediate(() => {
- const [url, params] = axios.get.mock.calls[0];
+ await waitForPromises();
- expect(params).toEqual({
- params: {
- limit: 20,
- offset: 100,
- },
- });
+ const [url, params] = axios.get.mock.calls[0];
- expect(url).toBe('/some_list');
-
- done();
+ expect(params).toEqual({
+ params: {
+ limit: 20,
+ offset: 100,
+ },
});
+
+ expect(url).toBe('/some_list');
});
- it('disables if return count is less than limit', (done) => {
+ it('disables if return count is less than limit', async () => {
Pager.offset = 0;
Pager.limit = 20;
@@ -132,12 +125,10 @@ describe('pager', () => {
jest.spyOn(Pager.loading, 'hide').mockImplementation(() => {});
Pager.getOld();
- setImmediate(() => {
- expect(Pager.loading.hide).toHaveBeenCalled();
- expect(Pager.disable).toBe(true);
+ await waitForPromises();
- done();
- });
+ expect(Pager.loading.hide).toHaveBeenCalled();
+ expect(Pager.disable).toBe(true);
});
describe('has data-href attribute from list element', () => {
diff --git a/spec/frontend/pages/admin/projects/components/namespace_select_spec.js b/spec/frontend/pages/admin/projects/components/namespace_select_spec.js
index 1fcc00489e3..f10b202f4d7 100644
--- a/spec/frontend/pages/admin/projects/components/namespace_select_spec.js
+++ b/spec/frontend/pages/admin/projects/components/namespace_select_spec.js
@@ -1,4 +1,5 @@
import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import Api from '~/api';
import NamespaceSelect from '~/pages/admin/projects/components/namespace_select.vue';
@@ -55,14 +56,14 @@ describe('Dropdown select component', () => {
mountDropdown({ fieldName: 'namespace-input' });
// wait for dropdown options to populate
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findDropdownOption('user: Administrator').exists()).toBe(true);
expect(findDropdownOption('group: GitLab Org').exists()).toBe(true);
expect(findDropdownOption('group: Foobar').exists()).toBe(false);
findDropdownOption('user: Administrator').trigger('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findNamespaceInput().attributes('value')).toBe('10');
expect(findDropdownToggle().text()).toBe('user: Administrator');
@@ -72,7 +73,7 @@ describe('Dropdown select component', () => {
mountDropdown();
// wait for dropdown options to populate
- await wrapper.vm.$nextTick();
+ await nextTick();
findDropdownOption('group: GitLab Org').trigger('click');
diff --git a/spec/frontend/pages/dashboard/todos/index/todos_spec.js b/spec/frontend/pages/dashboard/todos/index/todos_spec.js
index 6a7ce80ec5a..ef295e7d1ba 100644
--- a/spec/frontend/pages/dashboard/todos/index/todos_spec.js
+++ b/spec/frontend/pages/dashboard/todos/index/todos_spec.js
@@ -1,5 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
+import waitForPromises from 'helpers/wait_for_promises';
import '~/lib/utils/common_utils';
import axios from '~/lib/utils/axios_utils';
import { addDelimiter } from '~/lib/utils/text_utility';
@@ -71,7 +72,7 @@ describe('Todos', () => {
describe('on done todo click', () => {
let onToggleSpy;
- beforeEach((done) => {
+ beforeEach(() => {
const el = document.querySelector('.js-done-todo');
const path = el.dataset.href;
@@ -86,7 +87,7 @@ describe('Todos', () => {
el.click();
// Wait for axios and HTML to udpate
- setImmediate(done);
+ return waitForPromises();
});
it('dispatches todo:toggle', () => {
diff --git a/spec/frontend/pages/profiles/password_prompt/password_prompt_modal_spec.js b/spec/frontend/pages/profiles/password_prompt/password_prompt_modal_spec.js
index b722ac1e97b..c30b996437d 100644
--- a/spec/frontend/pages/profiles/password_prompt/password_prompt_modal_spec.js
+++ b/spec/frontend/pages/profiles/password_prompt/password_prompt_modal_spec.js
@@ -1,4 +1,5 @@
import { GlModal } from '@gitlab/ui';
+import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import {
I18N_PASSWORD_PROMPT_CANCEL_BUTTON,
@@ -62,7 +63,7 @@ describe('Password prompt modal', () => {
setPassword(mockPassword);
submitModal();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(handleConfirmPasswordSpy).toHaveBeenCalledTimes(1);
expect(handleConfirmPasswordSpy).toHaveBeenCalledWith(mockPassword);
@@ -73,7 +74,7 @@ describe('Password prompt modal', () => {
expect(findConfirmBtnDisabledState()).toBe(true);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findConfirmBtnDisabledState()).toBe(false);
});
@@ -84,7 +85,7 @@ describe('Password prompt modal', () => {
expect(findConfirmBtnDisabledState()).toBe(true);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findConfirmBtnDisabledState()).toBe(true);
});
diff --git a/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js b/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js
index dd617b1ffc2..dc5f1cb9e61 100644
--- a/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js
+++ b/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js
@@ -4,6 +4,7 @@ import { mount, shallowMount } from '@vue/test-utils';
import axios from 'axios';
import AxiosMockAdapter from 'axios-mock-adapter';
import { kebabCase } from 'lodash';
+import { nextTick } from 'vue';
import createFlash from '~/flash';
import httpStatus from '~/lib/utils/http_status';
import * as urlUtility from '~/lib/utils/url_utility';
@@ -217,7 +218,7 @@ describe('ForkForm component', () => {
it('changes to kebab case when project name changes', async () => {
const newInput = `${projectPath}1`;
findForkNameInput().vm.$emit('input', newInput);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findForkSlugInput().attributes('value')).toBe(kebabCase(newInput));
});
@@ -225,7 +226,7 @@ describe('ForkForm component', () => {
it('does not change to kebab case when project slug is changed manually', async () => {
const newInput = `${projectPath}1`;
findForkSlugInput().vm.$emit('input', newInput);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findForkSlugInput().attributes('value')).toBe(newInput);
});
@@ -273,7 +274,7 @@ describe('ForkForm component', () => {
expect(wrapper.vm.form.fields.visibility.value).toBe('public');
await findFormSelectOptions().at(1).setSelected();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(getByRole(wrapper.element, 'radio', { name: /private/i }).checked).toBe(true);
});
@@ -283,7 +284,7 @@ describe('ForkForm component', () => {
await findFormSelectOptions().at(1).setSelected();
- await wrapper.vm.$nextTick();
+ await nextTick();
const container = getByRole(wrapper.element, 'radiogroup', { name: /visibility/i });
const visibilityRadios = getAllByRole(container, 'radio');
@@ -419,7 +420,7 @@ describe('ForkForm component', () => {
const form = wrapper.find(GlForm);
await form.trigger('submit');
- await wrapper.vm.$nextTick();
+ await nextTick();
};
describe('with invalid form', () => {
diff --git a/spec/frontend/pages/projects/graphs/code_coverage_spec.js b/spec/frontend/pages/projects/graphs/code_coverage_spec.js
index 1f9029b40c7..0f763e3220a 100644
--- a/spec/frontend/pages/projects/graphs/code_coverage_spec.js
+++ b/spec/frontend/pages/projects/graphs/code_coverage_spec.js
@@ -3,6 +3,7 @@ import { GlAreaChart } from '@gitlab/ui/dist/charts';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import httpStatusCodes from '~/lib/utils/http_status';
@@ -143,7 +144,7 @@ describe('Code Coverage', () => {
it('updates the selected dropdown option with an icon', async () => {
findSecondDropdownItem().vm.$emit('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findFirstDropdownItem().attributes('ischecked')).toBeFalsy();
expect(findSecondDropdownItem().attributes('ischecked')).toBeTruthy();
@@ -155,7 +156,7 @@ describe('Code Coverage', () => {
findSecondDropdownItem().vm.$emit('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.vm.selectedDailyCoverage).not.toBe(originalSelectedData);
expect(wrapper.vm.selectedDailyCoverage).toBe(expectedData);
diff --git a/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_spec.js.snap b/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_spec.js.snap
index 1586aded6e6..86ccaa43786 100644
--- a/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_spec.js.snap
+++ b/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_spec.js.snap
@@ -135,13 +135,13 @@ exports[`Learn GitLab renders correctly 1`] = `
>
<a
class="gl-link"
+ data-testid="uncompleted-learn-gitlab-link"
data-track-action="click_link"
data-track-experiment="change_continuous_onboarding_link_urls"
data-track-label="Set up CI/CD"
data-track-property="Growth::Conversion::Experiment::LearnGitLab"
href="http://example.com/"
- rel="noopener noreferrer"
- target="_blank"
+ target="_self"
>
Set up CI/CD
@@ -155,13 +155,13 @@ exports[`Learn GitLab renders correctly 1`] = `
>
<a
class="gl-link"
+ data-testid="uncompleted-learn-gitlab-link"
data-track-action="click_link"
data-track-experiment="change_continuous_onboarding_link_urls"
data-track-label="Start a free Ultimate trial"
data-track-property="Growth::Conversion::Experiment::LearnGitLab"
href="http://example.com/"
- rel="noopener noreferrer"
- target="_blank"
+ target="_self"
>
Start a free Ultimate trial
@@ -175,13 +175,13 @@ exports[`Learn GitLab renders correctly 1`] = `
>
<a
class="gl-link"
+ data-testid="uncompleted-learn-gitlab-link"
data-track-action="click_link"
data-track-experiment="change_continuous_onboarding_link_urls"
data-track-label="Add code owners"
data-track-property="Growth::Conversion::Experiment::LearnGitLab"
href="http://example.com/"
- rel="noopener noreferrer"
- target="_blank"
+ target="_self"
>
Add code owners
@@ -202,13 +202,13 @@ exports[`Learn GitLab renders correctly 1`] = `
>
<a
class="gl-link"
+ data-testid="uncompleted-learn-gitlab-link"
data-track-action="click_link"
data-track-experiment="change_continuous_onboarding_link_urls"
data-track-label="Add merge request approval"
data-track-property="Growth::Conversion::Experiment::LearnGitLab"
href="http://example.com/"
- rel="noopener noreferrer"
- target="_blank"
+ target="_self"
>
Add merge request approval
@@ -265,13 +265,13 @@ exports[`Learn GitLab renders correctly 1`] = `
>
<a
class="gl-link"
+ data-testid="uncompleted-learn-gitlab-link"
data-track-action="click_link"
data-track-experiment="change_continuous_onboarding_link_urls"
data-track-label="Create an issue"
data-track-property="Growth::Conversion::Experiment::LearnGitLab"
href="http://example.com/"
- rel="noopener noreferrer"
- target="_blank"
+ target="_self"
>
Create an issue
@@ -285,13 +285,13 @@ exports[`Learn GitLab renders correctly 1`] = `
>
<a
class="gl-link"
+ data-testid="uncompleted-learn-gitlab-link"
data-track-action="click_link"
data-track-experiment="change_continuous_onboarding_link_urls"
data-track-label="Submit a merge request"
data-track-property="Growth::Conversion::Experiment::LearnGitLab"
href="http://example.com/"
- rel="noopener noreferrer"
- target="_blank"
+ target="_self"
>
Submit a merge request
@@ -341,11 +341,12 @@ exports[`Learn GitLab renders correctly 1`] = `
>
<a
class="gl-link"
+ data-testid="uncompleted-learn-gitlab-link"
data-track-action="click_link"
data-track-experiment="change_continuous_onboarding_link_urls"
data-track-label="Run a Security scan using CI/CD"
data-track-property="Growth::Conversion::Experiment::LearnGitLab"
- href="http://example.com/"
+ href="https://docs.gitlab.com/ee/foobar/"
rel="noopener noreferrer"
target="_blank"
>
diff --git a/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_section_link_spec.js b/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_section_link_spec.js
index f7b2154a935..3b113f4dcd7 100644
--- a/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_section_link_spec.js
+++ b/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_section_link_spec.js
@@ -12,6 +12,10 @@ const defaultProps = {
completed: false,
};
+const docLinkProps = {
+ url: 'https://docs.gitlab.com/ee/user/application_security/security_dashboard/',
+};
+
describe('Learn GitLab Section Link', () => {
let wrapper;
@@ -29,6 +33,8 @@ describe('Learn GitLab Section Link', () => {
const openInviteMembesrModalLink = () =>
wrapper.find('[data-testid="invite-for-help-continuous-onboarding-experiment-link"]');
+ const findUncompletedLink = () => wrapper.find('[data-testid="uncompleted-learn-gitlab-link"]');
+
it('renders no icon when not completed', () => {
createWrapper(undefined, { completed: false });
@@ -53,6 +59,32 @@ describe('Learn GitLab Section Link', () => {
expect(wrapper.find('[data-testid="trial-only"]').exists()).toBe(true);
});
+ describe('doc links', () => {
+ beforeEach(() => {
+ createWrapper('securityScanEnabled', docLinkProps);
+ });
+
+ it('renders links with blank target', () => {
+ const linkElement = findUncompletedLink();
+
+ expect(linkElement.exists()).toBe(true);
+ expect(linkElement.attributes('target')).toEqual('_blank');
+ });
+
+ it('tracks the click', () => {
+ const trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn);
+
+ findUncompletedLink().trigger('click');
+
+ expect(trackingSpy).toHaveBeenCalledWith('_category_', 'click_link', {
+ label: 'Run a Security scan using CI/CD',
+ property: 'Growth::Conversion::Experiment::LearnGitLab',
+ });
+
+ unmockTracking();
+ });
+ });
+
describe('rendering a link to open the invite_members modal instead of a regular link', () => {
it.each`
action | experimentVariant | showModal
@@ -82,11 +114,7 @@ describe('Learn GitLab Section Link', () => {
it('calls the eventHub', () => {
openInviteMembesrModalLink().vm.$emit('click');
- expect(eventHub.$emit).toHaveBeenCalledWith('openModal', {
- inviteeType: 'members',
- source: 'learn_gitlab',
- tasksToBeDoneEnabled: true,
- });
+ expect(eventHub.$emit).toHaveBeenCalledWith('openModal', { source: 'learn_gitlab' });
});
it('tracks the click', async () => {
diff --git a/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_spec.js b/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_spec.js
index 7e71622770f..ee682b18af3 100644
--- a/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_spec.js
+++ b/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_spec.js
@@ -1,13 +1,15 @@
import { GlProgressBar, GlAlert } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
+import Cookies from 'js-cookie';
import LearnGitlab from '~/pages/projects/learn_gitlab/components/learn_gitlab.vue';
import eventHub from '~/invite_members/event_hub';
+import { INVITE_MODAL_OPEN_COOKIE } from '~/pages/projects/learn_gitlab/constants';
import { testActions, testSections, testProject } from './mock_data';
describe('Learn GitLab', () => {
let wrapper;
let sidebar;
- let inviteMembersOpen = false;
+ let inviteMembers = false;
const createWrapper = () => {
wrapper = mount(LearnGitlab, {
@@ -15,7 +17,7 @@ describe('Learn GitLab', () => {
actions: testActions,
sections: testSections,
project: testProject,
- inviteMembersOpen,
+ inviteMembers,
},
});
};
@@ -36,7 +38,7 @@ describe('Learn GitLab', () => {
afterEach(() => {
wrapper.destroy();
wrapper = null;
- inviteMembersOpen = false;
+ inviteMembers = false;
sidebar.remove();
});
@@ -59,24 +61,40 @@ describe('Learn GitLab', () => {
describe('Invite Members Modal', () => {
let spy;
+ let cookieSpy;
beforeEach(() => {
spy = jest.spyOn(eventHub, '$emit');
+ cookieSpy = jest.spyOn(Cookies, 'remove');
+ });
+
+ afterEach(() => {
+ Cookies.remove(INVITE_MODAL_OPEN_COOKIE);
});
it('emits openModal', () => {
- inviteMembersOpen = true;
+ inviteMembers = true;
+ Cookies.set(INVITE_MODAL_OPEN_COOKIE, true);
createWrapper();
expect(spy).toHaveBeenCalledWith('openModal', {
mode: 'celebrate',
- inviteeType: 'members',
source: 'learn-gitlab',
});
+ expect(cookieSpy).toHaveBeenCalledWith(INVITE_MODAL_OPEN_COOKIE);
+ });
+
+ it('does not emit openModal when cookie is not set', () => {
+ inviteMembers = true;
+
+ createWrapper();
+
+ expect(spy).not.toHaveBeenCalled();
+ expect(cookieSpy).toHaveBeenCalledWith(INVITE_MODAL_OPEN_COOKIE);
});
- it('does not emit openModal', () => {
+ it('does not emit openModal when inviteMembers is false', () => {
createWrapper();
expect(spy).not.toHaveBeenCalled();
diff --git a/spec/frontend/pages/projects/learn_gitlab/components/mock_data.js b/spec/frontend/pages/projects/learn_gitlab/components/mock_data.js
index 1e633cb7cf5..b21965e8f48 100644
--- a/spec/frontend/pages/projects/learn_gitlab/components/mock_data.js
+++ b/spec/frontend/pages/projects/learn_gitlab/components/mock_data.js
@@ -35,7 +35,7 @@ export const testActions = {
svg: 'http://example.com/images/illustration.svg',
},
securityScanEnabled: {
- url: 'http://example.com/',
+ url: 'https://docs.gitlab.com/ee/foobar/',
completed: false,
svg: 'http://example.com/images/illustration.svg',
},
diff --git a/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js b/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js
index f3d76ca2c1b..ae5404f2d13 100644
--- a/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js
+++ b/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js
@@ -1,5 +1,6 @@
import { GlIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import { trimText } from 'helpers/text_helper';
import IntervalPatternInput from '~/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue';
@@ -98,7 +99,7 @@ describe('Interval Pattern Input Component', () => {
it('when a default option is selected', async () => {
selectEveryDayRadio();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findCustomInput().attributes('disabled')).toBeUndefined();
});
@@ -106,7 +107,7 @@ describe('Interval Pattern Input Component', () => {
it('when the custom option is selected', async () => {
selectCustomRadio();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findCustomInput().attributes('disabled')).toBeUndefined();
});
@@ -150,11 +151,11 @@ describe('Interval Pattern Input Component', () => {
it('when everyday is selected, update value', async () => {
selectEveryWeekRadio();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findCustomInput().element.value).toBe(cronIntervalPresets.everyWeek);
selectEveryDayRadio();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findCustomInput().element.value).toBe(cronIntervalPresets.everyDay);
});
});
@@ -170,7 +171,7 @@ describe('Interval Pattern Input Component', () => {
act();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findCustomInput().element.value).toBe(expectedValue);
});
@@ -189,7 +190,7 @@ describe('Interval Pattern Input Component', () => {
findCustomInput().setValue(newValue);
- await wrapper.vm.$nextTick;
+ await nextTick;
expect(findSelectedRadioKey()).toBe(customKey);
});
diff --git a/spec/frontend/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js b/spec/frontend/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js
index 5fed9fcaad2..c28a03b35d7 100644
--- a/spec/frontend/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js
+++ b/spec/frontend/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js
@@ -1,6 +1,7 @@
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Cookies from 'js-cookie';
+import { nextTick } from 'vue';
import PipelineSchedulesCallout from '~/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue';
const cookieKey = 'pipeline_schedules_callout_dismissed';
@@ -27,7 +28,7 @@ describe('Pipeline Schedule Callout', () => {
Cookies.set(cookieKey, true);
createComponent();
- await wrapper.vm.$nextTick();
+ await nextTick();
});
afterEach(() => {
@@ -71,7 +72,7 @@ describe('Pipeline Schedule Callout', () => {
findDismissCalloutBtn().vm.$emit('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findInnerContentOfCallout().exists()).toBe(false);
});
@@ -83,6 +84,7 @@ describe('Pipeline Schedule Callout', () => {
expect(setCookiesSpy).toHaveBeenCalledWith('pipeline_schedules_callout_dismissed', true, {
expires: 365,
+ secure: false,
});
});
});
@@ -90,7 +92,7 @@ describe('Pipeline Schedule Callout', () => {
it('is hidden when close button is clicked', async () => {
findDismissCalloutBtn().vm.$emit('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findInnerContentOfCallout().exists()).toBe(false);
});
diff --git a/spec/frontend/pages/projects/shared/permissions/components/project_setting_row_spec.js b/spec/frontend/pages/projects/shared/permissions/components/project_setting_row_spec.js
index 7cbcbdcdd1f..6230809a6aa 100644
--- a/spec/frontend/pages/projects/shared/permissions/components/project_setting_row_spec.js
+++ b/spec/frontend/pages/projects/shared/permissions/components/project_setting_row_spec.js
@@ -1,5 +1,6 @@
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import projectSettingRow from '~/pages/projects/shared/permissions/components/project_setting_row.vue';
describe('Project Setting Row', () => {
@@ -18,43 +19,39 @@ describe('Project Setting Row', () => {
wrapper.destroy();
});
- it('should show the label if it is set', () => {
+ it('should show the label if it is set', async () => {
wrapper.setProps({ label: 'Test label' });
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.find('label').text()).toEqual('Test label');
- });
+ await nextTick();
+ expect(wrapper.find('label').text()).toEqual('Test label');
});
it('should hide the label if it is not set', () => {
expect(wrapper.find('label').exists()).toBe(false);
});
- it('should show the help icon with the correct help path if it is set', () => {
+ it('should show the help icon with the correct help path if it is set', async () => {
wrapper.setProps({ label: 'Test label', helpPath: '/123' });
- return wrapper.vm.$nextTick(() => {
- const link = wrapper.find('a');
+ await nextTick();
+ const link = wrapper.find('a');
- expect(link.exists()).toBe(true);
- expect(link.attributes().href).toEqual('/123');
- });
+ expect(link.exists()).toBe(true);
+ expect(link.attributes().href).toEqual('/123');
});
- it('should hide the help icon if no help path is set', () => {
+ it('should hide the help icon if no help path is set', async () => {
wrapper.setProps({ label: 'Test label' });
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.find('a').exists()).toBe(false);
- });
+ await nextTick();
+ expect(wrapper.find('a').exists()).toBe(false);
});
- it('should show the help text if it is set', () => {
+ it('should show the help text if it is set', async () => {
wrapper.setProps({ helpText: 'Test text' });
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.find('span').text()).toEqual('Test text');
- });
+ await nextTick();
+ expect(wrapper.find('span').text()).toEqual('Test text');
});
it('should hide the help text if it is set', () => {
diff --git a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js
index 8a9bb025d55..305dce51971 100644
--- a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js
+++ b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js
@@ -244,7 +244,7 @@ describe('Settings Panel', () => {
wrapper = mountComponent({ currentSettings: { visibilityLevel: visibilityOptions.PUBLIC } });
expect(findRepositoryFeatureProjectRow().props('helpText')).toBe(
- 'View and edit files in this project. Non-project members will only have read access.',
+ 'View and edit files in this project. Non-project members have only read access.',
);
});
});
diff --git a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
index fd581eebd1e..1f964e8bae2 100644
--- a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
+++ b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
@@ -48,10 +48,10 @@ describe('WikiForm', () => {
return format.find(`option[value=${value}]`).setSelected();
};
- const triggerFormSubmit = () => {
+ const triggerFormSubmit = async () => {
findForm().element.dispatchEvent(new Event('submit'));
- return nextTick();
+ await nextTick();
};
const dispatchBeforeUnload = () => {
@@ -574,7 +574,7 @@ describe('WikiForm', () => {
wrapper.findComponent(GlModal).vm.$emit('primary');
- await wrapper.vm.$nextTick();
+ await nextTick();
});
it('switches to classic editor', () => {
diff --git a/spec/frontend/performance_bar/components/add_request_spec.js b/spec/frontend/performance_bar/components/add_request_spec.js
index c5247a43f27..5422481439e 100644
--- a/spec/frontend/performance_bar/components/add_request_spec.js
+++ b/spec/frontend/performance_bar/components/add_request_spec.js
@@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import AddRequest from '~/performance_bar/components/add_request.vue';
describe('add request form', () => {
@@ -17,9 +18,9 @@ describe('add request form', () => {
});
describe('when clicking the button', () => {
- beforeEach(() => {
+ beforeEach(async () => {
wrapper.find('button').trigger('click');
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('shows the form', () => {
@@ -27,9 +28,9 @@ describe('add request form', () => {
});
describe('when pressing escape', () => {
- beforeEach(() => {
+ beforeEach(async () => {
wrapper.find('input').trigger('keyup.esc');
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('hides the input', () => {
@@ -38,12 +39,11 @@ describe('add request form', () => {
});
describe('when submitting the form', () => {
- beforeEach(() => {
+ beforeEach(async () => {
wrapper.find('input').setValue('http://gitlab.example.com/users/root/calendar.json');
- return wrapper.vm.$nextTick().then(() => {
- wrapper.find('input').trigger('keyup.enter');
- return wrapper.vm.$nextTick();
- });
+ await nextTick();
+ wrapper.find('input').trigger('keyup.enter');
+ await nextTick();
});
it('emits an event to add the request', () => {
@@ -57,11 +57,10 @@ describe('add request form', () => {
expect(wrapper.find('input').exists()).toBe(false);
});
- it('clears the value for next time', () => {
+ it('clears the value for next time', async () => {
wrapper.find('button').trigger('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find('input').text()).toEqual('');
- });
+ await nextTick();
+ expect(wrapper.find('input').text()).toEqual('');
});
});
});
diff --git a/spec/frontend/persistent_user_callout_spec.js b/spec/frontend/persistent_user_callout_spec.js
index 1db255106ed..4633602de26 100644
--- a/spec/frontend/persistent_user_callout_spec.js
+++ b/spec/frontend/persistent_user_callout_spec.js
@@ -10,6 +10,7 @@ jest.mock('~/flash');
describe('PersistentUserCallout', () => {
const dismissEndpoint = '/dismiss';
const featureName = 'feature';
+ const groupId = '5';
function createFixture() {
const fixture = document.createElement('div');
@@ -18,6 +19,7 @@ describe('PersistentUserCallout', () => {
class="container"
data-dismiss-endpoint="${dismissEndpoint}"
data-feature-id="${featureName}"
+ data-group-id="${groupId}"
>
<button type="button" class="js-close"></button>
</div>
@@ -86,7 +88,9 @@ describe('PersistentUserCallout', () => {
return waitForPromises().then(() => {
expect(persistentUserCallout.container.remove).toHaveBeenCalled();
- expect(mockAxios.history.post[0].data).toBe(JSON.stringify({ feature_name: featureName }));
+ expect(mockAxios.history.post[0].data).toBe(
+ JSON.stringify({ feature_name: featureName, group_id: groupId }),
+ );
});
});
@@ -191,8 +195,8 @@ describe('PersistentUserCallout', () => {
return waitForPromises().then(() => {
expect(window.location.assign).toBeCalledWith(href);
- expect(mockAxios.history.post[0].data).toBe(JSON.stringify({ feature_name: featureName }));
expect(persistentUserCallout.container.remove).not.toHaveBeenCalled();
+ expect(mockAxios.history.post[0].data).toBe(JSON.stringify({ feature_name: featureName }));
});
});
diff --git a/spec/frontend/pipeline_editor/components/commit/commit_section_spec.js b/spec/frontend/pipeline_editor/components/commit/commit_section_spec.js
index bc77b7045eb..b54feea6ff7 100644
--- a/spec/frontend/pipeline_editor/components/commit/commit_section_spec.js
+++ b/spec/frontend/pipeline_editor/components/commit/commit_section_spec.js
@@ -1,17 +1,20 @@
import VueApollo from 'vue-apollo';
import { GlFormTextarea, GlFormInput, GlLoadingIcon } from '@gitlab/ui';
-import { createLocalVue, mount } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
+import Vue from 'vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { objectToQuery, redirectTo } from '~/lib/utils/url_utility';
import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue';
import CommitSection from '~/pipeline_editor/components/commit/commit_section.vue';
import {
COMMIT_ACTION_CREATE,
COMMIT_ACTION_UPDATE,
COMMIT_SUCCESS,
+ COMMIT_SUCCESS_WITH_REDIRECT,
} from '~/pipeline_editor/constants';
+import { resolvers } from '~/pipeline_editor/graphql/resolvers';
import commitCreate from '~/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql';
+import getCurrentBranch from '~/pipeline_editor/graphql/queries/client/current_branch.query.graphql';
import updatePipelineEtag from '~/pipeline_editor/graphql/mutations/client/update_pipeline_etag.mutation.graphql';
import {
@@ -23,18 +26,8 @@ import {
mockCommitMessage,
mockDefaultBranch,
mockProjectFullPath,
- mockNewMergeRequestPath,
} from '../../mock_data';
-const localVue = createLocalVue();
-
-jest.mock('~/lib/utils/url_utility', () => ({
- redirectTo: jest.fn(),
- refreshCurrentPage: jest.fn(),
- objectToQuery: jest.requireActual('~/lib/utils/url_utility').objectToQuery,
- mergeUrlParams: jest.requireActual('~/lib/utils/url_utility').mergeUrlParams,
-}));
-
const mockVariables = {
action: COMMIT_ACTION_UPDATE,
projectPath: mockProjectFullPath,
@@ -48,7 +41,6 @@ const mockVariables = {
const mockProvide = {
ciConfigPath: mockCiConfigPath,
projectFullPath: mockProjectFullPath,
- newMergeRequestPath: mockNewMergeRequestPath,
};
describe('Pipeline Editor | Commit section', () => {
@@ -79,11 +71,23 @@ describe('Pipeline Editor | Commit section', () => {
const createComponentWithApollo = (options) => {
const handlers = [[commitCreate, mockMutateCommitData]];
- localVue.use(VueApollo);
- mockApollo = createMockApollo(handlers);
+ Vue.use(VueApollo);
+ mockApollo = createMockApollo(handlers, resolvers);
+
+ mockApollo.clients.defaultClient.cache.writeQuery({
+ query: getCurrentBranch,
+ data: {
+ workBranches: {
+ __typename: 'BranchList',
+ current: {
+ __typename: 'WorkBranch',
+ name: mockDefaultBranch,
+ },
+ },
+ },
+ });
const apolloConfig = {
- localVue,
apolloProvider: mockApollo,
};
@@ -209,20 +213,23 @@ describe('Pipeline Editor | Commit section', () => {
const newBranch = 'new-branch';
beforeEach(async () => {
+ mockMutateCommitData.mockResolvedValue(mockCommitCreateResponse);
createComponentWithApollo();
+ mockMutateCommitData.mockResolvedValue(mockCommitCreateResponse);
await submitCommit({
branch: newBranch,
openMergeRequest: true,
});
});
- it('redirects to the merge request page with source and target branches', () => {
- const branchesQuery = objectToQuery({
- 'merge_request[source_branch]': newBranch,
- 'merge_request[target_branch]': mockDefaultBranch,
- });
-
- expect(redirectTo).toHaveBeenCalledWith(`${mockNewMergeRequestPath}?${branchesQuery}`);
+ it('emits a commit event with the right type, sourceBranch and targetBranch', () => {
+ expect(wrapper.emitted('commit')).toBeTruthy();
+ expect(wrapper.emitted('commit')[0]).toMatchObject([
+ {
+ type: COMMIT_SUCCESS_WITH_REDIRECT,
+ params: { sourceBranch: newBranch, targetBranch: mockDefaultBranch },
+ },
+ ]);
});
});
diff --git a/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js b/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js
index ab9027a56a4..7dbacad34bf 100644
--- a/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js
+++ b/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js
@@ -12,6 +12,10 @@ import waitForPromises from 'helpers/wait_for_promises';
import BranchSwitcher from '~/pipeline_editor/components/file_nav/branch_switcher.vue';
import { DEFAULT_FAILURE } from '~/pipeline_editor/constants';
import getAvailableBranchesQuery from '~/pipeline_editor/graphql/queries/available_branches.query.graphql';
+import getCurrentBranch from '~/pipeline_editor/graphql/queries/client/current_branch.query.graphql';
+import getLastCommitBranch from '~/pipeline_editor/graphql/queries/client/last_commit_branch.query.graphql';
+import { resolvers } from '~/pipeline_editor/graphql/resolvers';
+
import {
mockBranchPaginationLimit,
mockDefaultBranch,
@@ -34,6 +38,7 @@ describe('Pipeline editor branch switcher', () => {
const createComponent = ({
currentBranch = mockDefaultBranch,
+ availableBranches = ['main'],
isQueryLoading = false,
mountFn = shallowMount,
options = {},
@@ -59,7 +64,7 @@ describe('Pipeline editor branch switcher', () => {
},
data() {
return {
- availableBranches: ['main'],
+ availableBranches,
currentBranch,
};
},
@@ -67,13 +72,44 @@ describe('Pipeline editor branch switcher', () => {
});
};
- const createComponentWithApollo = ({ mountFn = shallowMount, props = {} } = {}) => {
+ const createComponentWithApollo = ({
+ mountFn = shallowMount,
+ props = {},
+ availableBranches = ['main'],
+ } = {}) => {
const handlers = [[getAvailableBranchesQuery, mockAvailableBranchQuery]];
- mockApollo = createMockApollo(handlers);
+ mockApollo = createMockApollo(handlers, resolvers);
+
+ mockApollo.clients.defaultClient.cache.writeQuery({
+ query: getCurrentBranch,
+ data: {
+ workBranches: {
+ __typename: 'BranchList',
+ current: {
+ __typename: 'WorkBranch',
+ name: mockDefaultBranch,
+ },
+ },
+ },
+ });
+
+ mockApollo.clients.defaultClient.cache.writeQuery({
+ query: getLastCommitBranch,
+ data: {
+ workBranches: {
+ __typename: 'BranchList',
+ lastCommit: {
+ __typename: 'WorkBranch',
+ name: '',
+ },
+ },
+ },
+ });
createComponent({
mountFn,
props,
+ availableBranches,
options: {
localVue,
apolloProvider: mockApollo,
@@ -113,7 +149,7 @@ describe('Pipeline editor branch switcher', () => {
describe('when querying for the first time', () => {
beforeEach(() => {
- createComponentWithApollo();
+ createComponentWithApollo({ availableBranches: [] });
});
it('disables the dropdown', () => {
@@ -153,7 +189,7 @@ describe('Pipeline editor branch switcher', () => {
describe('on fetch error', () => {
beforeEach(async () => {
setAvailableBranchesMock(new Error());
- createComponentWithApollo();
+ createComponentWithApollo({ availableBranches: [] });
await waitForPromises();
});
diff --git a/spec/frontend/pipeline_editor/components/header/pipeline_status_spec.js b/spec/frontend/pipeline_editor/components/header/pipeline_status_spec.js
index c101b1d21c7..35315db39f8 100644
--- a/spec/frontend/pipeline_editor/components/header/pipeline_status_spec.js
+++ b/spec/frontend/pipeline_editor/components/header/pipeline_status_spec.js
@@ -1,5 +1,6 @@
import { GlIcon, GlLink, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -8,8 +9,7 @@ import getPipelineQuery from '~/pipeline_editor/graphql/queries/pipeline.query.g
import PipelineEditorMiniGraph from '~/pipeline_editor/components/header/pipeline_editor_mini_graph.vue';
import { mockCommitSha, mockProjectPipeline, mockProjectFullPath } from '../../mock_data';
-const localVue = createLocalVue();
-localVue.use(VueApollo);
+Vue.use(VueApollo);
describe('Pipeline Status', () => {
let wrapper;
@@ -21,7 +21,6 @@ describe('Pipeline Status', () => {
mockApollo = createMockApollo(handlers);
wrapper = shallowMount(PipelineStatus, {
- localVue,
apolloProvider: mockApollo,
propsData: {
commitSha: mockCommitSha,
@@ -70,13 +69,13 @@ describe('Pipeline Status', () => {
describe('when querying data', () => {
describe('when data is set', () => {
- beforeEach(() => {
+ beforeEach(async () => {
mockPipelineQuery.mockResolvedValue({
data: { project: mockProjectPipeline() },
});
createComponentWithApollo();
- waitForPromises();
+ await waitForPromises();
});
it('query is called with correct variables', async () => {
diff --git a/spec/frontend/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js b/spec/frontend/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js
index 6b9f576917f..93eb18c90cf 100644
--- a/spec/frontend/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js
+++ b/spec/frontend/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js
@@ -1,14 +1,15 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import PipelineEditorMiniGraph from '~/pipeline_editor/components/header/pipeline_editor_mini_graph.vue';
import PipelineMiniGraph from '~/pipelines/components/pipelines_list/pipeline_mini_graph.vue';
import getLinkedPipelinesQuery from '~/projects/commit_box/info/graphql/queries/get_linked_pipelines.query.graphql';
import { PIPELINE_FAILURE } from '~/pipeline_editor/constants';
import { mockLinkedPipelines, mockProjectFullPath, mockProjectPipeline } from '../../mock_data';
-const localVue = createLocalVue();
-localVue.use(VueApollo);
+Vue.use(VueApollo);
describe('Pipeline Status', () => {
let wrapper;
@@ -35,7 +36,6 @@ describe('Pipeline Status', () => {
createComponent({
hasStages,
options: {
- localVue,
apolloProvider: mockApollo,
},
});
@@ -89,9 +89,10 @@ describe('Pipeline Status', () => {
});
describe('when query fails', () => {
- beforeEach(() => {
+ beforeEach(async () => {
mockLinkedPipelinesQuery.mockRejectedValue(new Error());
createComponentWithApollo();
+ await waitForPromises();
});
it('should emit an error event when query fails', async () => {
diff --git a/spec/frontend/pipeline_editor/components/header/validation_segment_spec.js b/spec/frontend/pipeline_editor/components/header/validation_segment_spec.js
index 570323826d1..1ad621e6f45 100644
--- a/spec/frontend/pipeline_editor/components/header/validation_segment_spec.js
+++ b/spec/frontend/pipeline_editor/components/header/validation_segment_spec.js
@@ -1,6 +1,7 @@
import VueApollo from 'vue-apollo';
import { GlIcon } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import { escape } from 'lodash';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -24,8 +25,7 @@ import {
mockYmlHelpPagePath,
} from '../../mock_data';
-const localVue = createLocalVue();
-localVue.use(VueApollo);
+Vue.use(VueApollo);
describe('Validation segment component', () => {
let wrapper;
@@ -45,7 +45,6 @@ describe('Validation segment component', () => {
wrapper = extendedWrapper(
shallowMount(ValidationSegment, {
- localVue,
apolloProvider: mockApollo,
provide: {
ymlHelpPagePath: mockYmlHelpPagePath,
diff --git a/spec/frontend/pipeline_editor/components/ui/pipeline_editor_messages_spec.js b/spec/frontend/pipeline_editor/components/ui/pipeline_editor_messages_spec.js
index a55176ccd79..d9ecee31e83 100644
--- a/spec/frontend/pipeline_editor/components/ui/pipeline_editor_messages_spec.js
+++ b/spec/frontend/pipeline_editor/components/ui/pipeline_editor_messages_spec.js
@@ -8,6 +8,7 @@ import PipelineEditorMessages from '~/pipeline_editor/components/ui/pipeline_edi
import {
COMMIT_FAILURE,
COMMIT_SUCCESS,
+ COMMIT_SUCCESS_WITH_REDIRECT,
DEFAULT_FAILURE,
DEFAULT_SUCCESS,
LOAD_FAILURE_UNKNOWN,
@@ -34,7 +35,13 @@ describe('Pipeline Editor messages', () => {
it('shows a message for successful commit type', () => {
createComponent({ successType: COMMIT_SUCCESS, showSuccess: true });
- expect(findAlert().text()).toBe(wrapper.vm.$options.successTexts[COMMIT_SUCCESS]);
+ expect(findAlert().text()).toBe(wrapper.vm.$options.success[COMMIT_SUCCESS]);
+ });
+
+ it('shows a message for successful commit with redirect type', () => {
+ createComponent({ successType: COMMIT_SUCCESS_WITH_REDIRECT, showSuccess: true });
+
+ expect(findAlert().text()).toBe(wrapper.vm.$options.success[COMMIT_SUCCESS_WITH_REDIRECT]);
});
it('does not show alert when there is a successType but visibility is off', () => {
@@ -46,7 +53,7 @@ describe('Pipeline Editor messages', () => {
it('shows a success alert with default copy if `showSuccess` is true and the `successType` is not valid,', () => {
createComponent({ successType: 'random', showSuccess: true });
- expect(findAlert().text()).toBe(wrapper.vm.$options.successTexts[DEFAULT_SUCCESS]);
+ expect(findAlert().text()).toBe(wrapper.vm.$options.success[DEFAULT_SUCCESS]);
});
it('emit `hide-success` event when clicking on the dismiss button', async () => {
@@ -71,7 +78,7 @@ describe('Pipeline Editor messages', () => {
`('shows a message for $message', ({ failureType, expectedFailureType }) => {
createComponent({ failureType, showFailure: true });
- expect(findAlert().text()).toBe(wrapper.vm.$options.errorTexts[expectedFailureType]);
+ expect(findAlert().text()).toBe(wrapper.vm.$options.errors[expectedFailureType]);
});
it('show failure reasons when there are some', () => {
diff --git a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
index 63eca253c48..0a2c03b7850 100644
--- a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
+++ b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
@@ -5,6 +5,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import { objectToQuery, redirectTo } from '~/lib/utils/url_utility';
import { resolvers } from '~/pipeline_editor/graphql/resolvers';
import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue';
import PipelineEditorEmptyState from '~/pipeline_editor/components/ui/pipeline_editor_empty_state.vue';
@@ -13,7 +14,11 @@ import PipelineEditorHeader from '~/pipeline_editor/components/header/pipeline_e
import ValidationSegment, {
i18n as validationSegmenti18n,
} from '~/pipeline_editor/components/header/validation_segment.vue';
-import { COMMIT_SUCCESS, COMMIT_FAILURE } from '~/pipeline_editor/constants';
+import {
+ COMMIT_SUCCESS,
+ COMMIT_SUCCESS_WITH_REDIRECT,
+ COMMIT_FAILURE,
+} from '~/pipeline_editor/constants';
import getBlobContent from '~/pipeline_editor/graphql/queries/blob_content.query.graphql';
import getCiConfigData from '~/pipeline_editor/graphql/queries/ci_config.query.graphql';
import getTemplate from '~/pipeline_editor/graphql/queries/get_starter_template.query.graphql';
@@ -35,15 +40,22 @@ import {
mockDefaultBranch,
mockEmptyCommitShaResults,
mockNewCommitShaResults,
+ mockNewMergeRequestPath,
mockProjectFullPath,
} from './mock_data';
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
+ redirectTo: jest.fn(),
+}));
+
const localVue = createLocalVue();
localVue.use(VueApollo);
const mockProvide = {
ciConfigPath: mockCiConfigPath,
defaultBranch: mockDefaultBranch,
+ newMergeRequestPath: mockNewMergeRequestPath,
projectFullPath: mockProjectFullPath,
};
@@ -311,6 +323,28 @@ describe('Pipeline editor app component', () => {
});
});
+ describe('when the commit succeeds with a redirect', () => {
+ const newBranch = 'new-branch';
+
+ beforeEach(async () => {
+ await createComponentWithApollo({ stubs: { PipelineEditorMessages } });
+
+ findEditorHome().vm.$emit('commit', {
+ type: COMMIT_SUCCESS_WITH_REDIRECT,
+ params: { sourceBranch: newBranch, targetBranch: mockDefaultBranch },
+ });
+ });
+
+ it('redirects to the merge request page with source and target branches', () => {
+ const branchesQuery = objectToQuery({
+ 'merge_request[source_branch]': newBranch,
+ 'merge_request[target_branch]': mockDefaultBranch,
+ });
+
+ expect(redirectTo).toHaveBeenCalledWith(`${mockNewMergeRequestPath}?${branchesQuery}`);
+ });
+ });
+
describe('and the commit mutation fails', () => {
const commitFailedReasons = ['Commit failed'];
diff --git a/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js b/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js
index 9e2bf1bd367..eec55091efa 100644
--- a/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js
+++ b/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js
@@ -1,6 +1,7 @@
import { GlForm, GlSprintf, GlLoadingIcon } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+import { nextTick } from 'vue';
import CreditCardValidationRequiredAlert from 'ee_component/billings/components/cc_validation_required_alert.vue';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
@@ -122,7 +123,7 @@ describe('Pipeline New Form', () => {
it('removes ci variable row on remove icon button click', async () => {
findRemoveIcons().at(1).trigger('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findVariableRows()).toHaveLength(2);
});
@@ -132,7 +133,7 @@ describe('Pipeline New Form', () => {
input.element.value = 'test_var_2';
input.trigger('change');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findVariableRows()).toHaveLength(4);
expect(findKeyInputs().at(3).element.value).toBe('');
@@ -205,7 +206,7 @@ describe('Pipeline New Form', () => {
mainInput.element.value = 'build_var';
mainInput.trigger('change');
- await wrapper.vm.$nextTick();
+ await nextTick();
selectBranch('branch-1');
@@ -215,7 +216,7 @@ describe('Pipeline New Form', () => {
branchOneInput.element.value = 'deploy_var';
branchOneInput.trigger('change');
- await wrapper.vm.$nextTick();
+ await nextTick();
selectBranch('main');
@@ -309,7 +310,7 @@ describe('Pipeline New Form', () => {
findKeyInputs().at(0).element.value = 'yml_var_modified';
findKeyInputs().at(0).trigger('change');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findVariableRows().at(0).text()).not.toContain(mockYmlDesc);
});
@@ -418,7 +419,7 @@ describe('Pipeline New Form', () => {
findCCAlert().vm.$emit('dismiss');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findCCAlert().exists()).toBe(false);
expect(wrapper.vm.$data.error).toBe(null);
diff --git a/spec/frontend/pipeline_wizard/components/commit_spec.js b/spec/frontend/pipeline_wizard/components/commit_spec.js
new file mode 100644
index 00000000000..6496850b028
--- /dev/null
+++ b/spec/frontend/pipeline_wizard/components/commit_spec.js
@@ -0,0 +1,282 @@
+import { GlButton, GlFormGroup } from '@gitlab/ui';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { __, s__, sprintf } from '~/locale';
+import { mountExtended } from 'jest/__helpers__/vue_test_utils_helper';
+import CommitStep, { i18n } from '~/pipeline_wizard/components/commit.vue';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import createCommitMutation from '~/pipeline_wizard/queries/create_commit.graphql';
+import getFileMetadataQuery from '~/pipeline_wizard/queries/get_file_meta.graphql';
+import RefSelector from '~/ref/components/ref_selector.vue';
+import flushPromises from 'helpers/flush_promises';
+import {
+ createCommitMutationErrorResult,
+ createCommitMutationResult,
+ fileQueryErrorResult,
+ fileQueryResult,
+ fileQueryEmptyResult,
+} from '../mock/query_responses';
+
+Vue.use(VueApollo);
+
+const COMMIT_MESSAGE_ADD_FILE = s__('PipelineWizardDefaultCommitMessage|Add %{filename}');
+const COMMIT_MESSAGE_UPDATE_FILE = s__('PipelineWizardDefaultCommitMessage|Update %{filename}');
+
+describe('Pipeline Wizard - Commit Page', () => {
+ const createCommitMutationHandler = jest.fn();
+ const $toast = {
+ show: jest.fn(),
+ };
+
+ let wrapper;
+
+ const getMockApollo = (scenario = {}) => {
+ return createMockApollo([
+ [
+ createCommitMutation,
+ createCommitMutationHandler.mockResolvedValue(
+ scenario.commitHasError ? createCommitMutationErrorResult : createCommitMutationResult,
+ ),
+ ],
+ [
+ getFileMetadataQuery,
+ (vars) => {
+ if (scenario.fileResultByRef) return scenario.fileResultByRef[vars.ref];
+ if (scenario.hasError) return fileQueryErrorResult;
+ return scenario.fileExists ? fileQueryResult : fileQueryEmptyResult;
+ },
+ ],
+ ]);
+ };
+ const createComponent = (props = {}, mockApollo = getMockApollo()) => {
+ wrapper = mountExtended(CommitStep, {
+ apolloProvider: mockApollo,
+ propsData: {
+ projectPath: 'some/path',
+ defaultBranch: 'main',
+ filename: 'newFile.yml',
+ ...props,
+ },
+ mocks: { $toast },
+ stubs: {
+ RefSelector: true,
+ GlFormGroup,
+ },
+ });
+ };
+
+ function getButtonWithLabel(label) {
+ return wrapper.findAllComponents(GlButton).filter((n) => n.text().match(label));
+ }
+
+ describe('ui setup', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('shows a commit message input with the correct label', () => {
+ expect(wrapper.findByTestId('commit_message').exists()).toBe(true);
+ expect(wrapper.find('label[for="commit_message"]').text()).toBe(i18n.commitMessageLabel);
+ });
+
+ it('shows a branch selector with the correct label', () => {
+ expect(wrapper.findByTestId('branch').exists()).toBe(true);
+ expect(wrapper.find('label[for="branch"]').text()).toBe(i18n.branchSelectorLabel);
+ });
+
+ it('shows a commit button', () => {
+ expect(getButtonWithLabel(i18n.commitButtonLabel).exists()).toBe(true);
+ });
+
+ it('shows a back button', () => {
+ expect(getButtonWithLabel(__('Back')).exists()).toBe(true);
+ });
+
+ it('does not show a next button', () => {
+ expect(getButtonWithLabel(__('Next')).exists()).toBe(false);
+ });
+ });
+
+ describe('loading the remote file', () => {
+ const projectPath = 'foo/bar';
+ const filename = 'foo.yml';
+
+ it('does not show a load error if call is successful', async () => {
+ createComponent({ projectPath, filename });
+ await flushPromises();
+ expect(wrapper.findByTestId('load-error').exists()).not.toBe(true);
+ });
+
+ it('shows a load error if call returns an unexpected error', async () => {
+ const branch = 'foo';
+ createComponent(
+ { defaultBranch: branch, projectPath, filename },
+ createMockApollo([[getFileMetadataQuery, () => fileQueryErrorResult]]),
+ );
+ await flushPromises();
+ expect(wrapper.findByTestId('load-error').exists()).toBe(true);
+ expect(wrapper.findByTestId('load-error').text()).toBe(i18n.errors.loadError);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+ });
+
+ describe('commit result handling', () => {
+ describe('successful commit', () => {
+ beforeEach(async () => {
+ createComponent();
+ await flushPromises();
+ await getButtonWithLabel(__('Commit')).trigger('click');
+ await flushPromises();
+ });
+
+ it('will not show an error', async () => {
+ expect(wrapper.findByTestId('commit-error').exists()).not.toBe(true);
+ });
+
+ it('will show a toast message', () => {
+ expect($toast.show).toHaveBeenCalledWith(
+ s__('PipelineWizard|The file has been committed.'),
+ );
+ });
+
+ it('emits a done event', () => {
+ expect(wrapper.emitted().done.length).toBe(1);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ jest.clearAllMocks();
+ });
+ });
+
+ describe('failed commit', () => {
+ beforeEach(async () => {
+ createComponent({}, getMockApollo({ commitHasError: true }));
+ await flushPromises();
+ await getButtonWithLabel(__('Commit')).trigger('click');
+ await flushPromises();
+ });
+
+ it('will show an error', async () => {
+ expect(wrapper.findByTestId('commit-error').exists()).toBe(true);
+ expect(wrapper.findByTestId('commit-error').text()).toBe(i18n.errors.commitError);
+ });
+
+ it('will not show a toast message', () => {
+ expect($toast.show).not.toHaveBeenCalledWith(i18n.commitSuccessMessage);
+ });
+
+ it('will not emit a done event', () => {
+ expect(wrapper.emitted().done?.length).toBeFalsy();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ jest.clearAllMocks();
+ });
+ });
+ });
+
+ describe('modelling different input combinations', () => {
+ const projectPath = 'some/path';
+ const defaultBranch = 'foo';
+ const fileContent = 'foo: bar';
+
+ describe.each`
+ filename | fileExistsOnDefaultBranch | fileExistsOnInputtedBranch | fileLoadError | commitMessageInputValue | branchInputValue | expectedCommitBranch | expectedCommitMessage | expectedAction
+ ${'foo.yml'} | ${false} | ${undefined} | ${false} | ${'foo'} | ${undefined} | ${defaultBranch} | ${'foo'} | ${'CREATE'}
+ ${'foo.yml'} | ${true} | ${undefined} | ${false} | ${'foo'} | ${undefined} | ${defaultBranch} | ${'foo'} | ${'UPDATE'}
+ ${'foo.yml'} | ${false} | ${true} | ${false} | ${'foo'} | ${'dev'} | ${'dev'} | ${'foo'} | ${'UPDATE'}
+ ${'foo.yml'} | ${false} | ${undefined} | ${false} | ${null} | ${undefined} | ${defaultBranch} | ${COMMIT_MESSAGE_ADD_FILE} | ${'CREATE'}
+ ${'foo.yml'} | ${true} | ${undefined} | ${false} | ${null} | ${undefined} | ${defaultBranch} | ${COMMIT_MESSAGE_UPDATE_FILE} | ${'UPDATE'}
+ ${'foo.yml'} | ${false} | ${true} | ${false} | ${null} | ${'dev'} | ${'dev'} | ${COMMIT_MESSAGE_UPDATE_FILE} | ${'UPDATE'}
+ `(
+ 'Test with fileExistsOnDefaultBranch=$fileExistsOnDefaultBranch, fileExistsOnInputtedBranch=$fileExistsOnInputtedBranch, commitMessageInputValue=$commitMessageInputValue, branchInputValue=$branchInputValue, commitReturnsError=$commitReturnsError',
+ ({
+ filename,
+ fileExistsOnDefaultBranch,
+ fileExistsOnInputtedBranch,
+ commitMessageInputValue,
+ branchInputValue,
+ expectedCommitBranch,
+ expectedCommitMessage,
+ expectedAction,
+ }) => {
+ let consoleSpy;
+
+ beforeAll(async () => {
+ createComponent(
+ {
+ filename,
+ defaultBranch,
+ projectPath,
+ fileContent,
+ },
+ getMockApollo({
+ fileResultByRef: {
+ [defaultBranch]: fileExistsOnDefaultBranch ? fileQueryResult : fileQueryEmptyResult,
+ [branchInputValue]: fileExistsOnInputtedBranch
+ ? fileQueryResult
+ : fileQueryEmptyResult,
+ },
+ }),
+ );
+
+ await flushPromises();
+
+ consoleSpy = jest.spyOn(console, 'error');
+
+ await wrapper
+ .findByTestId('commit_message')
+ .get('textarea')
+ .setValue(commitMessageInputValue);
+
+ if (branchInputValue) {
+ await wrapper.getComponent(RefSelector).vm.$emit('input', branchInputValue);
+ }
+ await Vue.nextTick();
+
+ await flushPromises();
+ });
+
+ afterAll(() => {
+ wrapper.destroy();
+ });
+
+ it('sets up without error', async () => {
+ expect(consoleSpy).not.toHaveBeenCalled();
+ });
+
+ it('does not show a load error', async () => {
+ expect(wrapper.findByTestId('load-error').exists()).not.toBe(true);
+ });
+
+ it('sends the expected commit mutation', async () => {
+ await getButtonWithLabel(__('Commit')).trigger('click');
+
+ expect(createCommitMutationHandler).toHaveBeenCalledWith({
+ input: {
+ actions: [
+ {
+ action: expectedAction,
+ content: fileContent,
+ filePath: `/${filename}`,
+ },
+ ],
+ branch: expectedCommitBranch,
+ message: sprintf(expectedCommitMessage, { filename }),
+ projectPath,
+ },
+ });
+ });
+ },
+ );
+ });
+});
diff --git a/spec/frontend/pipeline_wizard/components/editor_spec.js b/spec/frontend/pipeline_wizard/components/editor_spec.js
new file mode 100644
index 00000000000..446412a4f02
--- /dev/null
+++ b/spec/frontend/pipeline_wizard/components/editor_spec.js
@@ -0,0 +1,69 @@
+import { mount } from '@vue/test-utils';
+import { Document } from 'yaml';
+import YamlEditor from '~/pipeline_wizard/components/editor.vue';
+
+describe('Pages Yaml Editor wrapper', () => {
+ const defaultOptions = {
+ propsData: { doc: new Document({ foo: 'bar' }), filename: 'foo.yml' },
+ };
+
+ describe('mount hook', () => {
+ const wrapper = mount(YamlEditor, defaultOptions);
+
+ it('editor is mounted', () => {
+ expect(wrapper.vm.editor).not.toBeFalsy();
+ expect(wrapper.find('.gl-source-editor').exists()).toBe(true);
+ });
+ });
+
+ describe('watchers', () => {
+ describe('doc', () => {
+ const doc = new Document({ baz: ['bar'] });
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = mount(YamlEditor, defaultOptions);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it("causes the editor's value to be set to the stringified document", async () => {
+ await wrapper.setProps({ doc });
+ expect(wrapper.vm.editor.getValue()).toEqual(doc.toString());
+ });
+
+ it('emits an update:yaml event with the yaml representation of doc', async () => {
+ await wrapper.setProps({ doc });
+ const changeEvents = wrapper.emitted('update:yaml');
+ expect(changeEvents[2]).toEqual([doc.toString()]);
+ });
+
+ it('does not cause the touch event to be emitted', () => {
+ wrapper.setProps({ doc });
+ expect(wrapper.emitted('touch')).not.toBeTruthy();
+ });
+ });
+
+ describe('highlight', () => {
+ const highlight = 'foo';
+ const wrapper = mount(YamlEditor, defaultOptions);
+
+ it('calls editor.highlight(path, keep=true)', async () => {
+ const highlightSpy = jest.spyOn(wrapper.vm.yamlEditorExtension.obj, 'highlight');
+ await wrapper.setProps({ highlight });
+ expect(highlightSpy).toHaveBeenCalledWith(expect.anything(), highlight, true);
+ });
+ });
+ });
+
+ describe('events', () => {
+ const wrapper = mount(YamlEditor, defaultOptions);
+
+ it('emits touch if content is changed in editor', async () => {
+ await wrapper.vm.editor.setValue('foo: boo');
+ expect(wrapper.emitted('touch')).toBeTruthy();
+ });
+ });
+});
diff --git a/spec/frontend/pipeline_wizard/components/step_nav_spec.js b/spec/frontend/pipeline_wizard/components/step_nav_spec.js
new file mode 100644
index 00000000000..c6eac1386fa
--- /dev/null
+++ b/spec/frontend/pipeline_wizard/components/step_nav_spec.js
@@ -0,0 +1,79 @@
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import StepNav from '~/pipeline_wizard/components/step_nav.vue';
+
+describe('Pipeline Wizard - Step Navigation Component', () => {
+ const defaultProps = { showBackButton: true, showNextButton: true };
+
+ let wrapper;
+ let prevButton;
+ let nextButton;
+
+ const createComponent = (props = {}) => {
+ wrapper = mountExtended(StepNav, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ });
+ prevButton = wrapper.findByTestId('back-button');
+ nextButton = wrapper.findByTestId('next-button');
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it.each`
+ scenario | showBackButton | showNextButton
+ ${'does not show prev button'} | ${false} | ${false}
+ ${'has prev, but not next'} | ${true} | ${false}
+ ${'has next, but not prev'} | ${false} | ${true}
+ ${'has both next and prev'} | ${true} | ${true}
+ `('$scenario', async ({ showBackButton, showNextButton }) => {
+ createComponent({ showBackButton, showNextButton });
+
+ expect(prevButton.exists()).toBe(showBackButton);
+ expect(nextButton.exists()).toBe(showNextButton);
+ });
+
+ it('shows the expected button text', () => {
+ createComponent();
+
+ expect(prevButton.text()).toBe('Back');
+ expect(nextButton.text()).toBe('Next');
+ });
+
+ it('emits "back" events when clicking prev button', async () => {
+ createComponent();
+
+ await prevButton.trigger('click');
+ expect(wrapper.emitted().back.length).toBe(1);
+ });
+
+ it('emits "next" events when clicking next button', async () => {
+ createComponent();
+
+ await nextButton.trigger('click');
+ expect(wrapper.emitted().next.length).toBe(1);
+ });
+
+ it('enables the next button if nextButtonEnabled ist set to true', async () => {
+ createComponent({ nextButtonEnabled: true });
+
+ expect(nextButton.attributes('disabled')).not.toBe('disabled');
+ });
+
+ it('disables the next button if nextButtonEnabled ist set to false', async () => {
+ createComponent({ nextButtonEnabled: false });
+
+ expect(nextButton.attributes('disabled')).toBe('disabled');
+ });
+
+ it('does not emit "next" event when clicking next button while nextButtonEnabled ist set to false', async () => {
+ createComponent({ nextButtonEnabled: false });
+
+ await nextButton.trigger('click');
+
+ expect(wrapper.emitted().next).toBe(undefined);
+ });
+});
diff --git a/spec/frontend/pipeline_wizard/components/widgets/text_spec.js b/spec/frontend/pipeline_wizard/components/widgets/text_spec.js
new file mode 100644
index 00000000000..a11c0214d15
--- /dev/null
+++ b/spec/frontend/pipeline_wizard/components/widgets/text_spec.js
@@ -0,0 +1,152 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlFormGroup, GlFormInput } from '@gitlab/ui';
+import TextWidget from '~/pipeline_wizard/components/widgets/text.vue';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+
+describe('Pipeline Wizard - Text Widget', () => {
+ const defaultProps = {
+ label: 'This label',
+ description: 'some description',
+ placeholder: 'some placeholder',
+ pattern: '^[a-z]+$',
+ invalidFeedback: 'some feedback',
+ };
+
+ let wrapper;
+
+ const findGlFormGroup = () => wrapper.findComponent(GlFormGroup);
+ const findGlFormGroupInvalidFeedback = () => findGlFormGroup().find('.invalid-feedback');
+ const findGlFormInput = () => wrapper.findComponent(GlFormInput);
+
+ const createComponent = (props = {}, mountFn = mountExtended) => {
+ wrapper = mountFn(TextWidget, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ if (wrapper) {
+ wrapper.destroy();
+ }
+ });
+
+ it('creates an input element with the correct label', () => {
+ createComponent();
+
+ expect(wrapper.findByLabelText(defaultProps.label).exists()).toBe(true);
+ });
+
+ it('passes the description', () => {
+ createComponent({}, shallowMount);
+
+ expect(findGlFormGroup().attributes('description')).toBe(defaultProps.description);
+ });
+
+ it('sets the "text" type on the input component', () => {
+ createComponent();
+
+ expect(findGlFormInput().attributes('type')).toBe('text');
+ });
+
+ it('passes the placeholder', () => {
+ createComponent();
+
+ expect(findGlFormInput().attributes('placeholder')).toBe(defaultProps.placeholder);
+ });
+
+ it('emits an update event on input', async () => {
+ createComponent();
+
+ const localValue = 'somevalue';
+ await findGlFormInput().setValue(localValue);
+
+ expect(wrapper.emitted('input')).toEqual([[localValue]]);
+ });
+
+ it('passes invalid feedback message', () => {
+ createComponent();
+
+ expect(findGlFormGroupInvalidFeedback().text()).toBe(defaultProps.invalidFeedback);
+ });
+
+ it('provides invalid feedback', async () => {
+ createComponent({ validate: true });
+
+ await findGlFormInput().setValue('invalid%99');
+
+ expect(findGlFormGroup().classes()).toContain('is-invalid');
+ expect(findGlFormInput().classes()).toContain('is-invalid');
+ });
+
+ it('provides valid feedback', async () => {
+ createComponent({ validate: true });
+
+ await findGlFormInput().setValue('valid');
+
+ expect(findGlFormGroup().classes()).toContain('is-valid');
+ expect(findGlFormInput().classes()).toContain('is-valid');
+ });
+
+ it('does not show validation state when untouched', () => {
+ createComponent({ value: 'invalid99' });
+
+ expect(findGlFormGroup().classes()).not.toContain('is-valid');
+ expect(findGlFormGroup().classes()).not.toContain('is-invalid');
+ });
+
+ it('shows invalid state on blur', async () => {
+ createComponent();
+
+ await findGlFormInput().setValue('invalid%99');
+
+ expect(findGlFormGroup().classes()).not.toContain('is-invalid');
+
+ await findGlFormInput().trigger('blur');
+
+ expect(findGlFormInput().classes()).toContain('is-invalid');
+ expect(findGlFormGroup().classes()).toContain('is-invalid');
+ });
+
+ it('shows invalid state when toggling `validate` prop', async () => {
+ createComponent({
+ required: true,
+ validate: false,
+ });
+
+ expect(findGlFormGroup().classes()).not.toContain('is-invalid');
+
+ await wrapper.setProps({ validate: true });
+
+ expect(findGlFormGroup().classes()).toContain('is-invalid');
+ });
+
+ it('does not update validation if not required', async () => {
+ createComponent({
+ pattern: null,
+ validate: true,
+ });
+
+ expect(findGlFormGroup().classes()).not.toContain('is-invalid');
+ });
+
+ it('sets default value', () => {
+ const defaultValue = 'foo';
+ createComponent({
+ default: defaultValue,
+ });
+
+ expect(wrapper.findByLabelText(defaultProps.label).element.value).toBe(defaultValue);
+ });
+
+ it('emits default value on setup', () => {
+ const defaultValue = 'foo';
+ createComponent({
+ default: defaultValue,
+ });
+
+ expect(wrapper.emitted('input')).toEqual([[defaultValue]]);
+ });
+});
diff --git a/spec/frontend/pipeline_wizard/mock/query_responses.js b/spec/frontend/pipeline_wizard/mock/query_responses.js
new file mode 100644
index 00000000000..95dcb881a04
--- /dev/null
+++ b/spec/frontend/pipeline_wizard/mock/query_responses.js
@@ -0,0 +1,62 @@
+export const createCommitMutationResult = {
+ data: {
+ commitCreate: {
+ commit: {
+ id: '82a9df1',
+ },
+ content: 'foo: bar',
+ errors: null,
+ },
+ },
+};
+
+export const createCommitMutationErrorResult = {
+ data: {
+ commitCreate: {
+ commit: null,
+ content: null,
+ errors: ['Some Error Message'],
+ },
+ },
+};
+
+export const fileQueryResult = {
+ data: {
+ project: {
+ id: 'gid://gitlab/Project/1',
+ repository: {
+ blobs: {
+ nodes: [
+ {
+ id: 'gid://gitlab/Blob/9ff96777b315cd37188f7194d8382c718cb2933c',
+ },
+ ],
+ },
+ },
+ },
+ },
+};
+
+export const fileQueryEmptyResult = {
+ data: {
+ project: {
+ id: 'gid://gitlab/Project/2',
+ repository: {
+ blobs: {
+ nodes: [],
+ },
+ },
+ },
+ },
+};
+
+export const fileQueryErrorResult = {
+ data: {
+ foo: 'bar',
+ project: {
+ id: null,
+ repository: null,
+ },
+ },
+ errors: [{ message: 'GraphQL Error' }],
+};
diff --git a/spec/frontend/pipelines/__snapshots__/utils_spec.js.snap b/spec/frontend/pipelines/__snapshots__/utils_spec.js.snap
index 52461885342..2d2e5db598a 100644
--- a/spec/frontend/pipelines/__snapshots__/utils_spec.js.snap
+++ b/spec/frontend/pipelines/__snapshots__/utils_spec.js.snap
@@ -30,6 +30,7 @@ Array [
"hasDetails": true,
"icon": "status_success",
"id": "7",
+ "label": "passed",
"tooltip": "passed",
},
},
@@ -71,6 +72,7 @@ Array [
"hasDetails": true,
"icon": "status_success",
"id": "12",
+ "label": "passed",
"tooltip": "passed",
},
},
@@ -112,6 +114,7 @@ Array [
"hasDetails": true,
"icon": "status_success",
"id": "17",
+ "label": "passed",
"tooltip": "passed",
},
},
@@ -153,6 +156,7 @@ Array [
"hasDetails": true,
"icon": "status_success",
"id": "22",
+ "label": "passed",
"tooltip": "passed",
},
},
@@ -178,6 +182,7 @@ Array [
"hasDetails": true,
"icon": "status_success",
"id": "25",
+ "label": "passed",
"tooltip": "passed",
},
},
@@ -203,6 +208,7 @@ Array [
"hasDetails": true,
"icon": "status_success",
"id": "28",
+ "label": "passed",
"tooltip": "passed",
},
},
@@ -237,6 +243,7 @@ Array [
"hasDetails": true,
"icon": "status_success",
"id": "60",
+ "label": null,
"tooltip": null,
},
},
@@ -295,6 +302,7 @@ Array [
"hasDetails": true,
"icon": "status_success",
"id": "35",
+ "label": "passed",
"tooltip": "passed",
},
},
@@ -348,6 +356,7 @@ Array [
"hasDetails": true,
"icon": "status_success",
"id": "43",
+ "label": "passed",
"tooltip": "passed",
},
},
@@ -385,6 +394,7 @@ Array [
"hasDetails": true,
"icon": "status_success",
"id": "50",
+ "label": "passed",
"tooltip": "passed",
},
},
@@ -423,6 +433,7 @@ Array [
"hasDetails": true,
"icon": "status_success",
"id": "64",
+ "label": null,
"tooltip": null,
},
},
diff --git a/spec/frontend/pipelines/components/dag/dag_annotations_spec.js b/spec/frontend/pipelines/components/dag/dag_annotations_spec.js
index 1941a7f2777..212f8e19a6d 100644
--- a/spec/frontend/pipelines/components/dag/dag_annotations_spec.js
+++ b/spec/frontend/pipelines/components/dag/dag_annotations_spec.js
@@ -1,5 +1,6 @@
import { GlButton } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import DagAnnotations from '~/pipelines/components/dag/dag_annotations.vue';
import { singleNote, multiNote } from './mock_data';
@@ -82,26 +83,24 @@ describe('The DAG annotations', () => {
});
describe('clicking hide', () => {
- it('hides listed items and changes text to show', () => {
+ it('hides listed items and changes text to show', async () => {
expect(getAllTextBlocks().length).toBe(Object.keys(multiNote).length);
expect(getToggleButton().text()).toBe('Hide list');
getToggleButton().trigger('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(getAllTextBlocks().length).toBe(0);
- expect(getToggleButton().text()).toBe('Show list');
- });
+ await nextTick();
+ expect(getAllTextBlocks().length).toBe(0);
+ expect(getToggleButton().text()).toBe('Show list');
});
});
describe('clicking show', () => {
- it('shows listed items and changes text to hide', () => {
+ it('shows listed items and changes text to hide', async () => {
getToggleButton().trigger('click');
getToggleButton().trigger('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(getAllTextBlocks().length).toBe(Object.keys(multiNote).length);
- expect(getToggleButton().text()).toBe('Hide list');
- });
+ await nextTick();
+ expect(getAllTextBlocks().length).toBe(Object.keys(multiNote).length);
+ expect(getToggleButton().text()).toBe('Hide list');
});
});
});
diff --git a/spec/frontend/pipelines/components/dag/dag_spec.js b/spec/frontend/pipelines/components/dag/dag_spec.js
index 14030930657..d78df3eb35e 100644
--- a/spec/frontend/pipelines/components/dag/dag_spec.js
+++ b/spec/frontend/pipelines/components/dag/dag_spec.js
@@ -1,5 +1,6 @@
import { GlAlert, GlEmptyState } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import { ADD_NOTE, REMOVE_NOTE, REPLACE_NOTES } from '~/pipelines/components/dag/constants';
import Dag from '~/pipelines/components/dag/dag.vue';
import DagAnnotations from '~/pipelines/components/dag/dag_annotations.vue';
@@ -153,11 +154,11 @@ describe('Pipeline DAG graph wrapper', () => {
expect(getNotes().exists()).toBe(false);
getGraph().vm.$emit('update-annotation', { type: ADD_NOTE, data: currentNote });
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(getNotes().exists()).toBe(true);
getGraph().vm.$emit('update-annotation', { type: REMOVE_NOTE, data: currentNote });
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(getNotes().exists()).toBe(false);
});
@@ -165,11 +166,11 @@ describe('Pipeline DAG graph wrapper', () => {
expect(getNotes().exists()).toBe(false);
getGraph().vm.$emit('update-annotation', { type: REPLACE_NOTES, data: multiNote });
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(getNotes().exists()).toBe(true);
getGraph().vm.$emit('update-annotation', { type: REPLACE_NOTES, data: {} });
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(getNotes().exists()).toBe(false);
});
});
diff --git a/spec/frontend/pipelines/components/jobs/jobs_app_spec.js b/spec/frontend/pipelines/components/jobs/jobs_app_spec.js
index 1ea6096c922..65814ad9a7f 100644
--- a/spec/frontend/pipelines/components/jobs/jobs_app_spec.js
+++ b/spec/frontend/pipelines/components/jobs/jobs_app_spec.js
@@ -1,5 +1,6 @@
import { GlIntersectionObserver, GlSkeletonLoader } from '@gitlab/ui';
-import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -9,8 +10,7 @@ import JobsTable from '~/jobs/components/table/jobs_table.vue';
import getPipelineJobsQuery from '~/pipelines/graphql/queries/get_pipeline_jobs.query.graphql';
import { mockPipelineJobsQueryResponse } from '../../mock_data';
-const localVue = createLocalVue();
-localVue.use(VueApollo);
+Vue.use(VueApollo);
jest.mock('~/flash');
@@ -36,7 +36,6 @@ describe('Jobs app', () => {
fullPath: 'root/ci-project',
pipelineIid: 1,
},
- localVue,
apolloProvider: createMockApolloProvider(resolver),
});
};
@@ -74,16 +73,16 @@ describe('Jobs app', () => {
await waitForPromises();
expect(createFlash).toHaveBeenCalledWith({
- message: 'An error occured while fetching the pipelines jobs.',
+ message: 'An error occurred while fetching the pipelines jobs.',
});
});
it('handles infinite scrolling by calling fetchMore', async () => {
createComponent(resolverSpy);
-
await waitForPromises();
triggerInfiniteScroll();
+ await waitForPromises();
expect(resolverSpy).toHaveBeenCalledWith({
after: 'eyJpZCI6Ijg0NyJ9',
@@ -96,10 +95,10 @@ describe('Jobs app', () => {
createComponent(resolverSpy);
expect(findSkeletonLoader().exists()).toBe(true);
-
await waitForPromises();
triggerInfiniteScroll();
+ await waitForPromises();
expect(findSkeletonLoader().exists()).toBe(false);
});
diff --git a/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js b/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
index 661c8d99477..97b59a09518 100644
--- a/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
+++ b/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
@@ -1,6 +1,7 @@
import { GlFilteredSearch } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+import { nextTick } from 'vue';
import Api from '~/api';
import axios from '~/lib/utils/axios_utils';
import PipelinesFilteredSearch from '~/pipelines/components/pipelines_list/pipelines_filtered_search.vue';
@@ -103,46 +104,42 @@ describe('Pipelines filtered search', () => {
expect(wrapper.emitted('filterPipelines')[0]).toEqual([mockSearch]);
});
- it('disables tag name token when branch name token is active', () => {
+ it('disables tag name token when branch name token is active', async () => {
findFilteredSearch().vm.$emit('input', [
{ type: 'ref', value: { data: 'branch-1', operator: '=' } },
{ type: 'filtered-search-term', value: { data: '' } },
]);
- return wrapper.vm.$nextTick().then(() => {
- expect(findBranchToken().disabled).toBe(false);
- expect(findTagToken().disabled).toBe(true);
- });
+ await nextTick();
+ expect(findBranchToken().disabled).toBe(false);
+ expect(findTagToken().disabled).toBe(true);
});
- it('disables branch name token when tag name token is active', () => {
+ it('disables branch name token when tag name token is active', async () => {
findFilteredSearch().vm.$emit('input', [
{ type: 'tag', value: { data: 'tag-1', operator: '=' } },
{ type: 'filtered-search-term', value: { data: '' } },
]);
- return wrapper.vm.$nextTick().then(() => {
- expect(findBranchToken().disabled).toBe(true);
- expect(findTagToken().disabled).toBe(false);
- });
+ await nextTick();
+ expect(findBranchToken().disabled).toBe(true);
+ expect(findTagToken().disabled).toBe(false);
});
- it('resets tokens disabled state on clear', () => {
+ it('resets tokens disabled state on clear', async () => {
findFilteredSearch().vm.$emit('clearInput');
- return wrapper.vm.$nextTick().then(() => {
- expect(findBranchToken().disabled).toBe(false);
- expect(findTagToken().disabled).toBe(false);
- });
+ await nextTick();
+ expect(findBranchToken().disabled).toBe(false);
+ expect(findTagToken().disabled).toBe(false);
});
- it('resets tokens disabled state when clearing tokens by backspace', () => {
+ it('resets tokens disabled state when clearing tokens by backspace', async () => {
findFilteredSearch().vm.$emit('input', [{ type: 'filtered-search-term', value: { data: '' } }]);
- return wrapper.vm.$nextTick().then(() => {
- expect(findBranchToken().disabled).toBe(false);
- expect(findTagToken().disabled).toBe(false);
- });
+ await nextTick();
+ expect(findBranchToken().disabled).toBe(false);
+ expect(findTagToken().disabled).toBe(false);
});
describe('Url query params', () => {
diff --git a/spec/frontend/pipelines/graph/action_component_spec.js b/spec/frontend/pipelines/graph/action_component_spec.js
index 177b026491c..fab6e6887b7 100644
--- a/spec/frontend/pipelines/graph/action_component_spec.js
+++ b/spec/frontend/pipelines/graph/action_component_spec.js
@@ -1,6 +1,7 @@
import { GlButton } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import ActionComponent from '~/pipelines/components/jobs_shared/action_component.vue';
@@ -9,6 +10,7 @@ describe('pipeline graph action component', () => {
let wrapper;
let mock;
const findButton = () => wrapper.find(GlButton);
+ const findTooltipWrapper = () => wrapper.find('[data-testid="ci-action-icon-tooltip-wrapper"]');
beforeEach(() => {
mock = new MockAdapter(axios);
@@ -30,19 +32,14 @@ describe('pipeline graph action component', () => {
});
it('should render the provided title as a bootstrap tooltip', () => {
- expect(wrapper.attributes('title')).toBe('bar');
+ expect(findTooltipWrapper().attributes('title')).toBe('bar');
});
- it('should update bootstrap tooltip when title changes', (done) => {
+ it('should update bootstrap tooltip when title changes', async () => {
wrapper.setProps({ tooltipText: 'changed' });
- wrapper.vm
- .$nextTick()
- .then(() => {
- expect(wrapper.attributes('title')).toBe('changed');
- })
- .then(done)
- .catch(done.fail);
+ await nextTick();
+ expect(findTooltipWrapper().attributes('title')).toBe('changed');
});
it('should render an svg', () => {
@@ -64,13 +61,11 @@ describe('pipeline graph action component', () => {
.catch(done.fail);
});
- it('renders a loading icon while waiting for request', (done) => {
+ it('renders a loading icon while waiting for request', async () => {
findButton().trigger('click');
- wrapper.vm.$nextTick(() => {
- expect(wrapper.find('.js-action-icon-loading').exists()).toBe(true);
- done();
- });
+ await nextTick();
+ expect(wrapper.find('.js-action-icon-loading').exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
index 04e004dc6c1..8bc6c086b9d 100644
--- a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
+++ b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
@@ -1,10 +1,11 @@
import { GlAlert, GlButton, GlButtonGroup, GlLoadingIcon } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
-import Vue, { nextTick } from 'vue';
+import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql';
import getUserCallouts from '~/graphql_shared/queries/get_user_callouts.query.graphql';
import axios from '~/lib/utils/axios_utils';
@@ -100,15 +101,6 @@ describe('Pipeline graph wrapper', () => {
wrapper.destroy();
});
- beforeAll(() => {
- jest.useFakeTimers();
- });
-
- afterAll(() => {
- jest.runOnlyPendingTimers();
- jest.useRealTimers();
- });
-
describe('when data is loading', () => {
it('displays the loading icon', () => {
createComponentWithApollo();
@@ -134,8 +126,7 @@ describe('Pipeline graph wrapper', () => {
describe('when data has loaded', () => {
beforeEach(async () => {
createComponentWithApollo();
- jest.runOnlyPendingTimers();
- await nextTick();
+ await waitForPromises();
});
it('does not display the loading icon', () => {
@@ -163,8 +154,7 @@ describe('Pipeline graph wrapper', () => {
createComponentWithApollo({
getPipelineDetailsHandler: jest.fn().mockRejectedValue(new Error('GraphQL error')),
});
- jest.runOnlyPendingTimers();
- await nextTick();
+ await waitForPromises();
});
it('does not display the loading icon', () => {
@@ -187,8 +177,7 @@ describe('Pipeline graph wrapper', () => {
pipelineIid: '',
},
});
- jest.runOnlyPendingTimers();
- await nextTick();
+ await waitForPromises();
});
it('does not display the loading icon', () => {
@@ -210,7 +199,7 @@ describe('Pipeline graph wrapper', () => {
createComponentWithApollo();
jest.spyOn(wrapper.vm.$apollo.queries.headerPipeline, 'refetch');
jest.spyOn(wrapper.vm.$apollo.queries.pipeline, 'refetch');
- await nextTick();
+ await waitForPromises();
getGraph().vm.$emit('refreshPipelineGraph');
});
@@ -224,8 +213,7 @@ describe('Pipeline graph wrapper', () => {
describe('when query times out', () => {
const advanceApolloTimers = async () => {
jest.runOnlyPendingTimers();
- await nextTick();
- await nextTick();
+ await waitForPromises();
};
beforeEach(async () => {
@@ -245,7 +233,7 @@ describe('Pipeline graph wrapper', () => {
.mockResolvedValueOnce(errorData);
createComponentWithApollo({ getPipelineDetailsHandler: failSucceedFail });
- await nextTick();
+ await waitForPromises();
});
it('shows correct errors and does not overwrite populated data when data is empty', async () => {
@@ -274,8 +262,7 @@ describe('Pipeline graph wrapper', () => {
mountFn: mount,
});
- jest.runOnlyPendingTimers();
- await nextTick();
+ await waitForPromises();
});
it('appears when pipeline uses needs', () => {
@@ -318,7 +305,7 @@ describe('Pipeline graph wrapper', () => {
});
jest.runOnlyPendingTimers();
- await nextTick();
+ await waitForPromises();
});
it('sets showLinks to true', async () => {
@@ -327,8 +314,9 @@ describe('Pipeline graph wrapper', () => {
expect(getLinksLayer().props('showLinks')).toBe(false);
expect(getViewSelector().props('type')).toBe(LAYER_VIEW);
await getDependenciesToggle().vm.$emit('change', true);
+
jest.runOnlyPendingTimers();
- await nextTick();
+ await waitForPromises();
expect(wrapper.findComponent(LinksLayer).props('showLinks')).toBe(true);
});
});
@@ -343,8 +331,7 @@ describe('Pipeline graph wrapper', () => {
mountFn: mount,
});
- jest.runOnlyPendingTimers();
- await nextTick();
+ await waitForPromises();
});
it('shows the hover tip in the view selector', async () => {
@@ -365,7 +352,7 @@ describe('Pipeline graph wrapper', () => {
});
jest.runOnlyPendingTimers();
- await nextTick();
+ await waitForPromises();
});
it('does not show the hover tip', async () => {
@@ -382,8 +369,7 @@ describe('Pipeline graph wrapper', () => {
mountFn: mount,
});
- jest.runOnlyPendingTimers();
- await nextTick();
+ await waitForPromises();
});
afterEach(() => {
@@ -411,8 +397,7 @@ describe('Pipeline graph wrapper', () => {
getPipelineDetailsHandler: jest.fn().mockResolvedValue(nonNeedsResponse),
});
- jest.runOnlyPendingTimers();
- await nextTick();
+ await waitForPromises();
});
afterEach(() => {
@@ -435,7 +420,7 @@ describe('Pipeline graph wrapper', () => {
});
jest.runOnlyPendingTimers();
- await nextTick();
+ await waitForPromises();
});
it('does not appear when pipeline does not use needs', () => {
@@ -461,8 +446,7 @@ describe('Pipeline graph wrapper', () => {
describe('with no metrics path', () => {
beforeEach(async () => {
createComponentWithApollo();
- jest.runOnlyPendingTimers();
- await nextTick();
+ await waitForPromises();
});
it('is not called', () => {
@@ -505,8 +489,7 @@ describe('Pipeline graph wrapper', () => {
},
});
- jest.runOnlyPendingTimers();
- await nextTick();
+ await waitForPromises();
});
it('attempts to collect metrics', () => {
@@ -517,7 +500,7 @@ describe('Pipeline graph wrapper', () => {
});
describe('with duration and no error', () => {
- beforeEach(() => {
+ beforeEach(async () => {
mock = new MockAdapter(axios);
mock.onPost(metricsPath).reply(200, {});
@@ -536,6 +519,7 @@ describe('Pipeline graph wrapper', () => {
currentViewType: LAYER_VIEW,
},
});
+ await waitForPromises();
});
afterEach(() => {
diff --git a/spec/frontend/pipelines/graph/job_item_spec.js b/spec/frontend/pipelines/graph/job_item_spec.js
index 06f1fa4c827..23e7ed7ebb4 100644
--- a/spec/frontend/pipelines/graph/job_item_spec.js
+++ b/spec/frontend/pipelines/graph/job_item_spec.js
@@ -1,4 +1,5 @@
import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import JobItem from '~/pipelines/components/graph/job_item.vue';
describe('pipeline graph job item', () => {
@@ -6,6 +7,7 @@ describe('pipeline graph job item', () => {
const findJobWithoutLink = () => wrapper.find('[data-testid="job-without-link"]');
const findJobWithLink = () => wrapper.find('[data-testid="job-with-link"]');
+ const findActionComponent = () => wrapper.find('[data-testid="ci-action-component"]');
const createWrapper = (propsData) => {
wrapper = mount(JobItem, {
@@ -68,28 +70,38 @@ describe('pipeline graph job item', () => {
hasDetails: false,
},
};
+ const mockJobWithUnauthorizedAction = {
+ id: 4258,
+ name: 'stop-environment',
+ status: {
+ icon: 'status_manual',
+ label: 'manual stop action (not allowed)',
+ tooltip: 'manual action',
+ group: 'manual',
+ detailsPath: '/root/ci-mock/builds/4258',
+ hasDetails: true,
+ action: null,
+ },
+ };
afterEach(() => {
wrapper.destroy();
});
describe('name with link', () => {
- it('should render the job name and status with a link', (done) => {
+ it('should render the job name and status with a link', async () => {
createWrapper({ job: mockJob });
- wrapper.vm.$nextTick(() => {
- const link = wrapper.find('a');
-
- expect(link.attributes('href')).toBe(mockJob.status.detailsPath);
+ await nextTick();
+ const link = wrapper.find('a');
- expect(link.attributes('title')).toBe(`${mockJob.name} - ${mockJob.status.label}`);
+ expect(link.attributes('href')).toBe(mockJob.status.detailsPath);
- expect(wrapper.find('.ci-status-icon-success').exists()).toBe(true);
+ expect(link.attributes('title')).toBe(`${mockJob.name} - ${mockJob.status.label}`);
- expect(wrapper.text()).toBe(mockJob.name);
+ expect(wrapper.find('.ci-status-icon-success').exists()).toBe(true);
- done();
- });
+ expect(wrapper.text()).toBe(mockJob.name);
});
});
@@ -118,8 +130,21 @@ describe('pipeline graph job item', () => {
it('it should render the action icon', () => {
createWrapper({ job: mockJob });
- expect(wrapper.find('.ci-action-icon-container').exists()).toBe(true);
- expect(wrapper.find('.ci-action-icon-wrapper').exists()).toBe(true);
+ const actionComponent = findActionComponent();
+
+ expect(actionComponent.exists()).toBe(true);
+ expect(actionComponent.props('actionIcon')).toBe('retry');
+ expect(actionComponent.attributes('disabled')).not.toBe('disabled');
+ });
+
+ it('it should render disabled action icon when user cannot run the action', () => {
+ createWrapper({ job: mockJobWithUnauthorizedAction });
+
+ const actionComponent = findActionComponent();
+
+ expect(actionComponent.exists()).toBe(true);
+ expect(actionComponent.props('actionIcon')).toBe('stop');
+ expect(actionComponent.attributes('disabled')).toBe('disabled');
});
});
diff --git a/spec/frontend/pipelines/graph/linked_pipeline_spec.js b/spec/frontend/pipelines/graph/linked_pipeline_spec.js
index af5cd907dd8..d800a8c341e 100644
--- a/spec/frontend/pipelines/graph/linked_pipeline_spec.js
+++ b/spec/frontend/pipelines/graph/linked_pipeline_spec.js
@@ -9,6 +9,23 @@ import mockPipeline from './linked_pipelines_mock_data';
describe('Linked pipeline', () => {
let wrapper;
+ const downstreamProps = {
+ pipeline: {
+ ...mockPipeline,
+ multiproject: false,
+ },
+ columnTitle: 'Downstream',
+ type: DOWNSTREAM,
+ expanded: false,
+ isLoading: false,
+ };
+
+ const upstreamProps = {
+ ...downstreamProps,
+ columnTitle: 'Upstream',
+ type: UPSTREAM,
+ };
+
const findButton = () => wrapper.find(GlButton);
const findDownstreamPipelineTitle = () => wrapper.find('[data-testid="downstream-title"]');
const findPipelineLabel = () => wrapper.find('[data-testid="downstream-pipeline-label"]');
@@ -86,91 +103,65 @@ describe('Linked pipeline', () => {
});
});
- describe('parent/child', () => {
- const downstreamProps = {
- pipeline: {
- ...mockPipeline,
- multiproject: false,
- },
- columnTitle: 'Downstream',
- type: DOWNSTREAM,
- expanded: false,
- isLoading: false,
- };
+ describe('upstream pipelines', () => {
+ beforeEach(() => {
+ createWrapper(upstreamProps);
+ });
- const upstreamProps = {
- ...downstreamProps,
- columnTitle: 'Upstream',
- type: UPSTREAM,
- };
+ it('should display parent label when pipeline project id is the same as triggered_by pipeline project id', () => {
+ expect(findPipelineLabel().exists()).toBe(true);
+ });
- it('parent/child label container should exist', () => {
+ it('upstream pipeline should contain the correct link', () => {
+ expect(findPipelineLink().attributes('href')).toBe(upstreamProps.pipeline.path);
+ });
+
+ it('applies the reverse-row css class to the card', () => {
+ expect(findLinkedPipeline().classes()).toContain('gl-flex-direction-row-reverse');
+ expect(findLinkedPipeline().classes()).not.toContain('gl-flex-direction-row');
+ });
+ });
+
+ describe('downstream pipelines', () => {
+ beforeEach(() => {
createWrapper(downstreamProps);
+ });
+
+ it('parent/child label container should exist', () => {
expect(findPipelineLabel().exists()).toBe(true);
});
it('should display child label when pipeline project id is the same as triggered pipeline project id', () => {
- createWrapper(downstreamProps);
expect(findPipelineLabel().exists()).toBe(true);
});
it('should have the name of the trigger job on the card when it is a child pipeline', () => {
- createWrapper(downstreamProps);
expect(findDownstreamPipelineTitle().text()).toBe(mockPipeline.sourceJob.name);
});
- it('should display parent label when pipeline project id is the same as triggered_by pipeline project id', () => {
- createWrapper(upstreamProps);
- expect(findPipelineLabel().exists()).toBe(true);
- });
-
it('downstream pipeline should contain the correct link', () => {
- createWrapper(downstreamProps);
expect(findPipelineLink().attributes('href')).toBe(downstreamProps.pipeline.path);
});
- it('upstream pipeline should contain the correct link', () => {
- createWrapper(upstreamProps);
- expect(findPipelineLink().attributes('href')).toBe(upstreamProps.pipeline.path);
+ it('applies the flex-row css class to the card', () => {
+ expect(findLinkedPipeline().classes()).toContain('gl-flex-direction-row');
+ expect(findLinkedPipeline().classes()).not.toContain('gl-flex-direction-row-reverse');
});
+ });
+ describe('expand button', () => {
it.each`
- presentClass | missingClass
- ${'gl-right-0'} | ${'gl-left-0'}
- ${'gl-border-l-1!'} | ${'gl-border-r-1!'}
- `(
- 'pipeline expand button should be postioned right when child pipeline',
- ({ presentClass, missingClass }) => {
- createWrapper(downstreamProps);
- expect(findExpandButton().classes()).toContain(presentClass);
- expect(findExpandButton().classes()).not.toContain(missingClass);
- },
- );
-
- it.each`
- presentClass | missingClass
- ${'gl-left-0'} | ${'gl-right-0'}
- ${'gl-border-r-1!'} | ${'gl-border-l-1!'}
- `(
- 'pipeline expand button should be postioned left when parent pipeline',
- ({ presentClass, missingClass }) => {
- createWrapper(upstreamProps);
- expect(findExpandButton().classes()).toContain(presentClass);
- expect(findExpandButton().classes()).not.toContain(missingClass);
- },
- );
-
- it.each`
- pipelineType | anglePosition | expanded
- ${downstreamProps} | ${'angle-right'} | ${false}
- ${downstreamProps} | ${'angle-left'} | ${true}
- ${upstreamProps} | ${'angle-left'} | ${false}
- ${upstreamProps} | ${'angle-right'} | ${true}
+ pipelineType | anglePosition | borderClass | expanded
+ ${downstreamProps} | ${'angle-right'} | ${'gl-border-l-1!'} | ${false}
+ ${downstreamProps} | ${'angle-left'} | ${'gl-border-l-1!'} | ${true}
+ ${upstreamProps} | ${'angle-left'} | ${'gl-border-r-1!'} | ${false}
+ ${upstreamProps} | ${'angle-right'} | ${'gl-border-r-1!'} | ${true}
`(
- '$pipelineType.columnTitle pipeline button icon should be $anglePosition if expanded state is $expanded',
- ({ pipelineType, anglePosition, expanded }) => {
+ '$pipelineType.columnTitle pipeline button icon should be $anglePosition with $borderClass if expanded state is $expanded',
+ ({ pipelineType, anglePosition, borderClass, expanded }) => {
createWrapper({ ...pipelineType, expanded });
expect(findExpandButton().props('icon')).toBe(anglePosition);
+ expect(findExpandButton().classes()).toContain(borderClass);
},
);
});
diff --git a/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js b/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js
index 2f03b846525..1673065e09c 100644
--- a/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js
+++ b/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js
@@ -1,6 +1,8 @@
-import { mount, shallowMount, createLocalVue } from '@vue/test-utils';
+import { mount, shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql';
import {
DOWNSTREAM,
@@ -40,13 +42,11 @@ describe('Linked Pipelines Column', () => {
const findPipelineGraph = () => wrapper.find(PipelineGraph);
const findExpandButton = () => wrapper.find('[data-testid="expand-pipeline-button"]');
- const localVue = createLocalVue();
- localVue.use(VueApollo);
+ Vue.use(VueApollo);
const createComponent = ({ apolloProvider, mountFn = shallowMount, props = {} } = {}) => {
wrapper = mountFn(LinkedPipelinesColumn, {
apolloProvider,
- localVue,
propsData: {
...defaultProps,
...props,
@@ -87,13 +87,7 @@ describe('Linked Pipelines Column', () => {
describe('click action', () => {
const clickExpandButton = async () => {
await findExpandButton().trigger('click');
- await wrapper.vm.$nextTick();
- };
-
- const clickExpandButtonAndAwaitTimers = async () => {
- await clickExpandButton();
- jest.runOnlyPendingTimers();
- await wrapper.vm.$nextTick();
+ await waitForPromises();
};
describe('layer type rendering', () => {
@@ -106,9 +100,9 @@ describe('Linked Pipelines Column', () => {
it('calls listByLayers only once no matter how many times view is switched', async () => {
expect(layersFn).not.toHaveBeenCalled();
- await clickExpandButtonAndAwaitTimers();
+ await clickExpandButton();
await wrapper.setProps({ viewType: LAYER_VIEW });
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(layersFn).toHaveBeenCalledTimes(1);
await wrapper.setProps({ viewType: STAGE_VIEW });
await wrapper.setProps({ viewType: LAYER_VIEW });
@@ -132,7 +126,7 @@ describe('Linked Pipelines Column', () => {
});
it('shows the stage view, even when the main graph view type is layers', async () => {
- await clickExpandButtonAndAwaitTimers();
+ await clickExpandButton();
expect(findPipelineGraph().props('viewType')).toBe(STAGE_VIEW);
});
});
@@ -145,7 +139,7 @@ describe('Linked Pipelines Column', () => {
it('toggles the pipeline visibility', async () => {
expect(findPipelineGraph().exists()).toBe(false);
- await clickExpandButtonAndAwaitTimers();
+ await clickExpandButton();
expect(findPipelineGraph().exists()).toBe(true);
await clickExpandButton();
expect(findPipelineGraph().exists()).toBe(false);
@@ -167,7 +161,7 @@ describe('Linked Pipelines Column', () => {
it('does not show the pipeline', async () => {
expect(findPipelineGraph().exists()).toBe(false);
- await clickExpandButtonAndAwaitTimers();
+ await clickExpandButton();
expect(findPipelineGraph().exists()).toBe(false);
});
});
@@ -195,7 +189,7 @@ describe('Linked Pipelines Column', () => {
it('toggles the pipeline visibility', async () => {
expect(findPipelineGraph().exists()).toBe(false);
- await clickExpandButtonAndAwaitTimers();
+ await clickExpandButton();
expect(findPipelineGraph().exists()).toBe(true);
await clickExpandButton();
expect(findPipelineGraph().exists()).toBe(false);
@@ -218,7 +212,7 @@ describe('Linked Pipelines Column', () => {
it('does not show the pipeline', async () => {
expect(findPipelineGraph().exists()).toBe(false);
- await clickExpandButtonAndAwaitTimers();
+ await clickExpandButton();
expect(findPipelineGraph().exists()).toBe(false);
});
});
diff --git a/spec/frontend/pipelines/graph/mock_data.js b/spec/frontend/pipelines/graph/mock_data.js
index 41823bfdb9f..0cf7dc507f4 100644
--- a/spec/frontend/pipelines/graph/mock_data.js
+++ b/spec/frontend/pipelines/graph/mock_data.js
@@ -57,6 +57,7 @@ export const mockPipelineResponse = {
id: '7',
icon: 'status_success',
tooltip: 'passed',
+ label: 'passed',
hasDetails: true,
detailsPath: '/root/abcd-dag/-/jobs/1482',
group: 'success',
@@ -106,6 +107,7 @@ export const mockPipelineResponse = {
id: '12',
icon: 'status_success',
tooltip: 'passed',
+ label: 'passed',
hasDetails: true,
detailsPath: '/root/abcd-dag/-/jobs/1515',
group: 'success',
@@ -155,6 +157,7 @@ export const mockPipelineResponse = {
id: '17',
icon: 'status_success',
tooltip: 'passed',
+ label: 'passed',
hasDetails: true,
detailsPath: '/root/abcd-dag/-/jobs/1484',
group: 'success',
@@ -204,6 +207,7 @@ export const mockPipelineResponse = {
id: '22',
icon: 'status_success',
tooltip: 'passed',
+ label: 'passed',
hasDetails: true,
detailsPath: '/root/abcd-dag/-/jobs/1485',
group: 'success',
@@ -235,6 +239,7 @@ export const mockPipelineResponse = {
id: '25',
icon: 'status_success',
tooltip: 'passed',
+ label: 'passed',
hasDetails: true,
detailsPath: '/root/abcd-dag/-/jobs/1486',
group: 'success',
@@ -266,6 +271,7 @@ export const mockPipelineResponse = {
id: '28',
icon: 'status_success',
tooltip: 'passed',
+ label: 'passed',
hasDetails: true,
detailsPath: '/root/abcd-dag/-/jobs/1487',
group: 'success',
@@ -330,6 +336,7 @@ export const mockPipelineResponse = {
id: '35',
icon: 'status_success',
tooltip: 'passed',
+ label: 'passed',
hasDetails: true,
detailsPath: '/root/abcd-dag/-/jobs/1514',
group: 'success',
@@ -413,6 +420,7 @@ export const mockPipelineResponse = {
id: '43',
icon: 'status_success',
tooltip: 'passed',
+ label: 'passed',
hasDetails: true,
detailsPath: '/root/abcd-dag/-/jobs/1489',
group: 'success',
@@ -498,6 +506,7 @@ export const mockPipelineResponse = {
id: '50',
icon: 'status_success',
tooltip: 'passed',
+ label: 'passed',
hasDetails: true,
detailsPath: '/root/abcd-dag/-/jobs/1490',
group: 'success',
@@ -601,6 +610,7 @@ export const mockPipelineResponse = {
id: '60',
icon: 'status_success',
tooltip: null,
+ label: null,
hasDetails: true,
detailsPath: '/root/kinder-pipe/-/pipelines/154',
group: 'success',
@@ -643,6 +653,7 @@ export const mockPipelineResponse = {
id: '64',
icon: 'status_success',
tooltip: null,
+ label: null,
hasDetails: true,
detailsPath: '/root/abcd-dag/-/pipelines/153',
group: 'success',
@@ -850,6 +861,7 @@ export const wrappedPipelineReturn = {
id: '84',
icon: 'status_success',
tooltip: 'passed',
+ label: 'passed',
hasDetails: true,
detailsPath: '/root/elemenohpee/-/jobs/1662',
group: 'success',
diff --git a/spec/frontend/pipelines/header_component_spec.js b/spec/frontend/pipelines/header_component_spec.js
index 9e51003da66..1d89f949564 100644
--- a/spec/frontend/pipelines/header_component_spec.js
+++ b/spec/frontend/pipelines/header_component_spec.js
@@ -4,6 +4,7 @@ import HeaderComponent from '~/pipelines/components/header_component.vue';
import cancelPipelineMutation from '~/pipelines/graphql/mutations/cancel_pipeline.mutation.graphql';
import deletePipelineMutation from '~/pipelines/graphql/mutations/delete_pipeline.mutation.graphql';
import retryPipelineMutation from '~/pipelines/graphql/mutations/retry_pipeline.mutation.graphql';
+import { BUTTON_TOOLTIP_RETRY } from '~/pipelines/constants';
import {
mockCancelledPipelineHeader,
mockFailedPipelineHeader,
@@ -113,6 +114,10 @@ describe('Pipeline details header', () => {
variables: { id: mockCancelledPipelineHeader.id },
});
});
+
+ it('should render retry action tooltip', () => {
+ expect(findRetryButton().attributes('title')).toBe(BUTTON_TOOLTIP_RETRY);
+ });
});
describe('Cancel action', () => {
diff --git a/spec/frontend/pipelines/mock_data.js b/spec/frontend/pipelines/mock_data.js
index b9d20eb7ca5..8cb6cf3bed6 100644
--- a/spec/frontend/pipelines/mock_data.js
+++ b/spec/frontend/pipelines/mock_data.js
@@ -634,3 +634,683 @@ export const mockPipelineJobsQueryResponse = {
},
},
};
+
+export const mockPipeline = (projectPath) => {
+ return {
+ pipeline: {
+ id: 1,
+ user: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url: '',
+ web_url: 'http://0.0.0.0:3000/root',
+ show_status: false,
+ path: '/root',
+ },
+ active: false,
+ source: 'merge_request_event',
+ created_at: '2021-10-19T21:17:38.698Z',
+ updated_at: '2021-10-21T18:00:42.758Z',
+ path: 'foo',
+ flags: {},
+ merge_request: {
+ iid: 1,
+ path: `/${projectPath}/1`,
+ title: 'commit',
+ source_branch: 'test-commit-name',
+ source_branch_path: `/${projectPath}`,
+ target_branch: 'main',
+ target_branch_path: `/${projectPath}/-/commit/main`,
+ },
+ ref: {
+ name: 'refs/merge-requests/1/head',
+ path: `/${projectPath}/-/commits/refs/merge-requests/1/head`,
+ tag: false,
+ branch: false,
+ merge_request: true,
+ },
+ commit: {
+ id: 'fd6df5b3229e213c97d308844a6f3e7fd71e8f8c',
+ short_id: 'fd6df5b3',
+ created_at: '2021-10-19T21:17:12.000+00:00',
+ parent_ids: ['7147906b84306e83cb3fec6582a25390b75713c6'],
+ title: 'Commit Title',
+ message: 'Commit',
+ author_name: 'Administrator',
+ author_email: 'admin@example.com',
+ authored_date: '2021-10-19T21:17:12.000+00:00',
+ committer_name: 'Administrator',
+ committer_email: 'admin@example.com',
+ committed_date: '2021-10-19T21:17:12.000+00:00',
+ trailers: {},
+ web_url: '',
+ author: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url: '',
+ web_url: '',
+ show_status: false,
+ path: '/root',
+ },
+ author_gravatar_url: '',
+ commit_url: `/${projectPath}/fd6df5b3229e213c97d308844a6f3e7fd71e8f8c`,
+ commit_path: `/${projectPath}/commit/fd6df5b3229e213c97d308844a6f3e7fd71e8f8c`,
+ },
+ project: {
+ full_path: `/${projectPath}`,
+ },
+ triggered_by: null,
+ triggered: [],
+ },
+ pipelineScheduleUrl: 'foo',
+ pipelineKey: 'id',
+ viewType: 'root',
+ };
+};
+
+export const mockPipelineTag = () => {
+ return {
+ pipeline: {
+ id: 311,
+ iid: 37,
+ user: {
+ id: 1,
+ username: 'root',
+ name: 'Administrator',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ web_url: 'http://gdk.test:3000/root',
+ show_status: false,
+ path: '/root',
+ },
+ active: false,
+ source: 'push',
+ created_at: '2022-02-02T15:39:04.012Z',
+ updated_at: '2022-02-02T15:40:59.573Z',
+ path: '/root/mr-widgets/-/pipelines/311',
+ flags: {
+ stuck: false,
+ auto_devops: false,
+ merge_request: false,
+ yaml_errors: false,
+ retryable: true,
+ cancelable: false,
+ failure_reason: false,
+ detached_merge_request_pipeline: false,
+ merge_request_pipeline: false,
+ merge_train_pipeline: false,
+ latest: true,
+ },
+ details: {
+ status: {
+ icon: 'status_warning',
+ text: 'passed',
+ label: 'passed with warnings',
+ group: 'success-with-warnings',
+ tooltip: 'passed',
+ has_details: true,
+ details_path: '/root/mr-widgets/-/pipelines/311',
+ illustration: null,
+ favicon:
+ '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
+ },
+ stages: [
+ {
+ name: 'accessibility',
+ title: 'accessibility: passed',
+ status: {
+ icon: 'status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ tooltip: 'passed',
+ has_details: true,
+ details_path: '/root/mr-widgets/-/pipelines/311#accessibility',
+ illustration: null,
+ favicon:
+ '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
+ },
+ path: '/root/mr-widgets/-/pipelines/311#accessibility',
+ dropdown_path: '/root/mr-widgets/-/pipelines/311/stage.json?stage=accessibility',
+ },
+ {
+ name: 'validate',
+ title: 'validate: passed with warnings',
+ status: {
+ icon: 'status_warning',
+ text: 'passed',
+ label: 'passed with warnings',
+ group: 'success-with-warnings',
+ tooltip: 'passed',
+ has_details: true,
+ details_path: '/root/mr-widgets/-/pipelines/311#validate',
+ illustration: null,
+ favicon:
+ '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
+ },
+ path: '/root/mr-widgets/-/pipelines/311#validate',
+ dropdown_path: '/root/mr-widgets/-/pipelines/311/stage.json?stage=validate',
+ },
+ {
+ name: 'test',
+ title: 'test: passed',
+ status: {
+ icon: 'status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ tooltip: 'passed',
+ has_details: true,
+ details_path: '/root/mr-widgets/-/pipelines/311#test',
+ illustration: null,
+ favicon:
+ '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
+ },
+ path: '/root/mr-widgets/-/pipelines/311#test',
+ dropdown_path: '/root/mr-widgets/-/pipelines/311/stage.json?stage=test',
+ },
+ {
+ name: 'build',
+ title: 'build: passed',
+ status: {
+ icon: 'status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ tooltip: 'passed',
+ has_details: true,
+ details_path: '/root/mr-widgets/-/pipelines/311#build',
+ illustration: null,
+ favicon:
+ '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
+ },
+ path: '/root/mr-widgets/-/pipelines/311#build',
+ dropdown_path: '/root/mr-widgets/-/pipelines/311/stage.json?stage=build',
+ },
+ ],
+ duration: 93,
+ finished_at: '2022-02-02T15:40:59.384Z',
+ name: 'Pipeline',
+ manual_actions: [],
+ scheduled_actions: [],
+ },
+ ref: {
+ name: 'test',
+ path: '/root/mr-widgets/-/commits/test',
+ tag: true,
+ branch: false,
+ merge_request: false,
+ },
+ commit: {
+ id: '9b92b4f730d1611bd9a086ca221ae206d5da1e59',
+ short_id: '9b92b4f7',
+ created_at: '2022-01-13T13:59:03.000+00:00',
+ parent_ids: ['0ba763634114e207dc72c65c8e9459556b1204fb'],
+ title: 'Update hello_world.js',
+ message: 'Update hello_world.js',
+ author_name: 'Administrator',
+ author_email: 'admin@example.com',
+ authored_date: '2022-01-13T13:59:03.000+00:00',
+ committer_name: 'Administrator',
+ committer_email: 'admin@example.com',
+ committed_date: '2022-01-13T13:59:03.000+00:00',
+ trailers: {},
+ web_url:
+ 'http://gdk.test:3000/root/mr-widgets/-/commit/9b92b4f730d1611bd9a086ca221ae206d5da1e59',
+ author: {
+ id: 1,
+ username: 'root',
+ name: 'Administrator',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ web_url: 'http://gdk.test:3000/root',
+ show_status: false,
+ path: '/root',
+ },
+ author_gravatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ commit_url:
+ 'http://gdk.test:3000/root/mr-widgets/-/commit/9b92b4f730d1611bd9a086ca221ae206d5da1e59',
+ commit_path: '/root/mr-widgets/-/commit/9b92b4f730d1611bd9a086ca221ae206d5da1e59',
+ },
+ retry_path: '/root/mr-widgets/-/pipelines/311/retry',
+ delete_path: '/root/mr-widgets/-/pipelines/311',
+ failed_builds: [
+ {
+ id: 1696,
+ name: 'fmt',
+ started: '2022-02-02T15:39:45.192Z',
+ complete: true,
+ archived: false,
+ build_path: '/root/mr-widgets/-/jobs/1696',
+ retry_path: '/root/mr-widgets/-/jobs/1696/retry',
+ playable: false,
+ scheduled: false,
+ created_at: '2022-02-02T15:39:04.136Z',
+ updated_at: '2022-02-02T15:39:57.969Z',
+ status: {
+ icon: 'status_warning',
+ text: 'failed',
+ label: 'failed (allowed to fail)',
+ group: 'failed-with-warnings',
+ tooltip: 'failed - (script failure) (allowed to fail)',
+ has_details: true,
+ details_path: '/root/mr-widgets/-/jobs/1696',
+ illustration: {
+ image:
+ '/assets/illustrations/skipped-job_empty-29a8a37d8a61d1b6f68cf3484f9024e53cd6eb95e28eae3554f8011a1146bf27.svg',
+ size: 'svg-430',
+ title: 'This job does not have a trace.',
+ },
+ favicon:
+ '/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png',
+ action: {
+ icon: 'retry',
+ title: 'Retry',
+ path: '/root/mr-widgets/-/jobs/1696/retry',
+ method: 'post',
+ button_title: 'Retry this job',
+ },
+ },
+ recoverable: false,
+ },
+ ],
+ project: {
+ id: 23,
+ name: 'mr-widgets',
+ full_path: '/root/mr-widgets',
+ full_name: 'Administrator / mr-widgets',
+ },
+ triggered_by: null,
+ triggered: [],
+ },
+ pipelineScheduleUrl: 'foo',
+ pipelineKey: 'id',
+ viewType: 'root',
+ };
+};
+
+export const mockPipelineBranch = () => {
+ return {
+ pipeline: {
+ id: 268,
+ iid: 34,
+ user: {
+ id: 1,
+ username: 'root',
+ name: 'Administrator',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ web_url: 'http://gdk.test:3000/root',
+ show_status: false,
+ path: '/root',
+ },
+ active: false,
+ source: 'push',
+ created_at: '2022-01-14T17:40:27.866Z',
+ updated_at: '2022-01-14T18:02:35.850Z',
+ path: '/root/mr-widgets/-/pipelines/268',
+ flags: {
+ stuck: false,
+ auto_devops: false,
+ merge_request: false,
+ yaml_errors: false,
+ retryable: true,
+ cancelable: false,
+ failure_reason: false,
+ detached_merge_request_pipeline: false,
+ merge_request_pipeline: false,
+ merge_train_pipeline: false,
+ latest: true,
+ },
+ details: {
+ status: {
+ icon: 'status_warning',
+ text: 'passed',
+ label: 'passed with warnings',
+ group: 'success-with-warnings',
+ tooltip: 'passed',
+ has_details: true,
+ details_path: '/root/mr-widgets/-/pipelines/268',
+ illustration: null,
+ favicon:
+ '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
+ },
+ stages: [
+ {
+ name: 'validate',
+ title: 'validate: passed with warnings',
+ status: {
+ icon: 'status_warning',
+ text: 'passed',
+ label: 'passed with warnings',
+ group: 'success-with-warnings',
+ tooltip: 'passed',
+ has_details: true,
+ details_path: '/root/mr-widgets/-/pipelines/268#validate',
+ illustration: null,
+ favicon:
+ '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
+ },
+ path: '/root/mr-widgets/-/pipelines/268#validate',
+ dropdown_path: '/root/mr-widgets/-/pipelines/268/stage.json?stage=validate',
+ },
+ {
+ name: 'test',
+ title: 'test: passed',
+ status: {
+ icon: 'status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ tooltip: 'passed',
+ has_details: true,
+ details_path: '/root/mr-widgets/-/pipelines/268#test',
+ illustration: null,
+ favicon:
+ '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
+ },
+ path: '/root/mr-widgets/-/pipelines/268#test',
+ dropdown_path: '/root/mr-widgets/-/pipelines/268/stage.json?stage=test',
+ },
+ {
+ name: 'build',
+ title: 'build: passed',
+ status: {
+ icon: 'status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ tooltip: 'passed',
+ has_details: true,
+ details_path: '/root/mr-widgets/-/pipelines/268#build',
+ illustration: null,
+ favicon:
+ '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
+ },
+ path: '/root/mr-widgets/-/pipelines/268#build',
+ dropdown_path: '/root/mr-widgets/-/pipelines/268/stage.json?stage=build',
+ },
+ ],
+ duration: 75,
+ finished_at: '2022-01-14T18:02:35.842Z',
+ name: 'Pipeline',
+ manual_actions: [],
+ scheduled_actions: [],
+ },
+ ref: {
+ name: 'update-ci',
+ path: '/root/mr-widgets/-/commits/update-ci',
+ tag: false,
+ branch: true,
+ merge_request: false,
+ },
+ commit: {
+ id: '96aef9ecec5752c09371c1ade5fc77860aafc863',
+ short_id: '96aef9ec',
+ created_at: '2022-01-14T17:40:26.000+00:00',
+ parent_ids: ['06860257572d4cf84b73806250b78169050aed83'],
+ title: 'Update main.tf',
+ message: 'Update main.tf',
+ author_name: 'Administrator',
+ author_email: 'admin@example.com',
+ authored_date: '2022-01-14T17:40:26.000+00:00',
+ committer_name: 'Administrator',
+ committer_email: 'admin@example.com',
+ committed_date: '2022-01-14T17:40:26.000+00:00',
+ trailers: {},
+ web_url:
+ 'http://gdk.test:3000/root/mr-widgets/-/commit/96aef9ecec5752c09371c1ade5fc77860aafc863',
+ author: {
+ id: 1,
+ username: 'root',
+ name: 'Administrator',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ web_url: 'http://gdk.test:3000/root',
+ show_status: false,
+ path: '/root',
+ },
+ author_gravatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ commit_url:
+ 'http://gdk.test:3000/root/mr-widgets/-/commit/96aef9ecec5752c09371c1ade5fc77860aafc863',
+ commit_path: '/root/mr-widgets/-/commit/96aef9ecec5752c09371c1ade5fc77860aafc863',
+ },
+ retry_path: '/root/mr-widgets/-/pipelines/268/retry',
+ delete_path: '/root/mr-widgets/-/pipelines/268',
+ failed_builds: [
+ {
+ id: 1260,
+ name: 'fmt',
+ started: '2022-01-14T17:40:36.435Z',
+ complete: true,
+ archived: false,
+ build_path: '/root/mr-widgets/-/jobs/1260',
+ retry_path: '/root/mr-widgets/-/jobs/1260/retry',
+ playable: false,
+ scheduled: false,
+ created_at: '2022-01-14T17:40:27.879Z',
+ updated_at: '2022-01-14T17:40:42.129Z',
+ status: {
+ icon: 'status_warning',
+ text: 'failed',
+ label: 'failed (allowed to fail)',
+ group: 'failed-with-warnings',
+ tooltip: 'failed - (script failure) (allowed to fail)',
+ has_details: true,
+ details_path: '/root/mr-widgets/-/jobs/1260',
+ illustration: {
+ image:
+ '/assets/illustrations/skipped-job_empty-29a8a37d8a61d1b6f68cf3484f9024e53cd6eb95e28eae3554f8011a1146bf27.svg',
+ size: 'svg-430',
+ title: 'This job does not have a trace.',
+ },
+ favicon:
+ '/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png',
+ action: {
+ icon: 'retry',
+ title: 'Retry',
+ path: '/root/mr-widgets/-/jobs/1260/retry',
+ method: 'post',
+ button_title: 'Retry this job',
+ },
+ },
+ recoverable: false,
+ },
+ ],
+ project: {
+ id: 23,
+ name: 'mr-widgets',
+ full_path: '/root/mr-widgets',
+ full_name: 'Administrator / mr-widgets',
+ },
+ triggered_by: null,
+ triggered: [],
+ },
+ pipelineScheduleUrl: 'foo',
+ pipelineKey: 'id',
+ viewType: 'root',
+ };
+};
+
+export const mockPipelineNoCommit = () => {
+ return {
+ pipeline: {
+ id: 268,
+ iid: 34,
+ user: {
+ id: 1,
+ username: 'root',
+ name: 'Administrator',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ web_url: 'http://gdk.test:3000/root',
+ show_status: false,
+ path: '/root',
+ },
+ active: false,
+ source: 'push',
+ created_at: '2022-01-14T17:40:27.866Z',
+ updated_at: '2022-01-14T18:02:35.850Z',
+ path: '/root/mr-widgets/-/pipelines/268',
+ flags: {
+ stuck: false,
+ auto_devops: false,
+ merge_request: false,
+ yaml_errors: false,
+ retryable: true,
+ cancelable: false,
+ failure_reason: false,
+ detached_merge_request_pipeline: false,
+ merge_request_pipeline: false,
+ merge_train_pipeline: false,
+ latest: true,
+ },
+ details: {
+ status: {
+ icon: 'status_warning',
+ text: 'passed',
+ label: 'passed with warnings',
+ group: 'success-with-warnings',
+ tooltip: 'passed',
+ has_details: true,
+ details_path: '/root/mr-widgets/-/pipelines/268',
+ illustration: null,
+ favicon:
+ '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
+ },
+ stages: [
+ {
+ name: 'validate',
+ title: 'validate: passed with warnings',
+ status: {
+ icon: 'status_warning',
+ text: 'passed',
+ label: 'passed with warnings',
+ group: 'success-with-warnings',
+ tooltip: 'passed',
+ has_details: true,
+ details_path: '/root/mr-widgets/-/pipelines/268#validate',
+ illustration: null,
+ favicon:
+ '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
+ },
+ path: '/root/mr-widgets/-/pipelines/268#validate',
+ dropdown_path: '/root/mr-widgets/-/pipelines/268/stage.json?stage=validate',
+ },
+ {
+ name: 'test',
+ title: 'test: passed',
+ status: {
+ icon: 'status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ tooltip: 'passed',
+ has_details: true,
+ details_path: '/root/mr-widgets/-/pipelines/268#test',
+ illustration: null,
+ favicon:
+ '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
+ },
+ path: '/root/mr-widgets/-/pipelines/268#test',
+ dropdown_path: '/root/mr-widgets/-/pipelines/268/stage.json?stage=test',
+ },
+ {
+ name: 'build',
+ title: 'build: passed',
+ status: {
+ icon: 'status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ tooltip: 'passed',
+ has_details: true,
+ details_path: '/root/mr-widgets/-/pipelines/268#build',
+ illustration: null,
+ favicon:
+ '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
+ },
+ path: '/root/mr-widgets/-/pipelines/268#build',
+ dropdown_path: '/root/mr-widgets/-/pipelines/268/stage.json?stage=build',
+ },
+ ],
+ duration: 75,
+ finished_at: '2022-01-14T18:02:35.842Z',
+ name: 'Pipeline',
+ manual_actions: [],
+ scheduled_actions: [],
+ },
+ ref: {
+ name: 'update-ci',
+ path: '/root/mr-widgets/-/commits/update-ci',
+ tag: false,
+ branch: true,
+ merge_request: false,
+ },
+ retry_path: '/root/mr-widgets/-/pipelines/268/retry',
+ delete_path: '/root/mr-widgets/-/pipelines/268',
+ failed_builds: [
+ {
+ id: 1260,
+ name: 'fmt',
+ started: '2022-01-14T17:40:36.435Z',
+ complete: true,
+ archived: false,
+ build_path: '/root/mr-widgets/-/jobs/1260',
+ retry_path: '/root/mr-widgets/-/jobs/1260/retry',
+ playable: false,
+ scheduled: false,
+ created_at: '2022-01-14T17:40:27.879Z',
+ updated_at: '2022-01-14T17:40:42.129Z',
+ status: {
+ icon: 'status_warning',
+ text: 'failed',
+ label: 'failed (allowed to fail)',
+ group: 'failed-with-warnings',
+ tooltip: 'failed - (script failure) (allowed to fail)',
+ has_details: true,
+ details_path: '/root/mr-widgets/-/jobs/1260',
+ illustration: {
+ image:
+ '/assets/illustrations/skipped-job_empty-29a8a37d8a61d1b6f68cf3484f9024e53cd6eb95e28eae3554f8011a1146bf27.svg',
+ size: 'svg-430',
+ title: 'This job does not have a trace.',
+ },
+ favicon:
+ '/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png',
+ action: {
+ icon: 'retry',
+ title: 'Retry',
+ path: '/root/mr-widgets/-/jobs/1260/retry',
+ method: 'post',
+ button_title: 'Retry this job',
+ },
+ },
+ recoverable: false,
+ },
+ ],
+ project: {
+ id: 23,
+ name: 'mr-widgets',
+ full_path: '/root/mr-widgets',
+ full_name: 'Administrator / mr-widgets',
+ },
+ triggered_by: null,
+ triggered: [],
+ },
+ pipelineScheduleUrl: 'foo',
+ pipelineKey: 'id',
+ viewType: 'root',
+ };
+};
diff --git a/spec/frontend/pipelines/notification/deprecated_type_keyword_notification_spec.js b/spec/frontend/pipelines/notification/deprecated_type_keyword_notification_spec.js
new file mode 100644
index 00000000000..f626652a944
--- /dev/null
+++ b/spec/frontend/pipelines/notification/deprecated_type_keyword_notification_spec.js
@@ -0,0 +1,146 @@
+import VueApollo from 'vue-apollo';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { GlAlert, GlSprintf } from '@gitlab/ui';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import DeprecatedTypeKeywordNotification from '~/pipelines/components/notification/deprecated_type_keyword_notification.vue';
+import getPipelineWarnings from '~/pipelines/graphql/queries/get_pipeline_warnings.query.graphql';
+import {
+ mockWarningsWithoutDeprecation,
+ mockWarningsRootType,
+ mockWarningsType,
+ mockWarningsTypesAll,
+} from './mock_data';
+
+const defaultProvide = {
+ deprecatedKeywordsDocPath: '/help/ci/yaml/index.md#deprecated-keywords',
+ fullPath: '/namespace/my-project',
+ pipelineIid: 4,
+};
+
+let wrapper;
+
+const mockWarnings = jest.fn();
+
+const createComponent = ({ isLoading = false, options = {} } = {}) => {
+ return shallowMount(DeprecatedTypeKeywordNotification, {
+ stubs: {
+ GlSprintf,
+ },
+ provide: {
+ ...defaultProvide,
+ },
+ mocks: {
+ $apollo: {
+ queries: {
+ warnings: {
+ loading: isLoading,
+ },
+ },
+ },
+ },
+ ...options,
+ });
+};
+
+const createComponentWithApollo = () => {
+ const localVue = createLocalVue();
+ localVue.use(VueApollo);
+
+ const handlers = [[getPipelineWarnings, mockWarnings]];
+ const mockApollo = createMockApollo(handlers);
+
+ return createComponent({
+ options: {
+ localVue,
+ apolloProvider: mockApollo,
+ mocks: {},
+ },
+ });
+};
+
+const findAlert = () => wrapper.findComponent(GlAlert);
+const findAlertItems = () => findAlert().findAll('li');
+
+afterEach(() => {
+ wrapper.destroy();
+});
+
+describe('Deprecated keyword notification', () => {
+ describe('while loading the pipeline warnings', () => {
+ beforeEach(() => {
+ wrapper = createComponent({ isLoading: true });
+ });
+
+ it('does not display the notification', () => {
+ expect(findAlert().exists()).toBe(false);
+ });
+ });
+
+ describe('if there is an error in the query', () => {
+ beforeEach(async () => {
+ mockWarnings.mockResolvedValue({ errors: ['It didnt work'] });
+ wrapper = createComponentWithApollo();
+ await waitForPromises();
+ });
+
+ it('does not display the notification', () => {
+ expect(findAlert().exists()).toBe(false);
+ });
+ });
+
+ describe('with a valid query result', () => {
+ describe('if there are no deprecation warnings', () => {
+ beforeEach(async () => {
+ mockWarnings.mockResolvedValue(mockWarningsWithoutDeprecation);
+ wrapper = createComponentWithApollo();
+ await waitForPromises();
+ });
+ it('does not show the notification', () => {
+ expect(findAlert().exists()).toBe(false);
+ });
+ });
+
+ describe('with a root type deprecation message', () => {
+ beforeEach(async () => {
+ mockWarnings.mockResolvedValue(mockWarningsRootType);
+ wrapper = createComponentWithApollo();
+ await waitForPromises();
+ });
+ it('shows the notification with one item', () => {
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlertItems()).toHaveLength(1);
+ expect(findAlertItems().at(0).text()).toContain('types');
+ });
+ });
+
+ describe('with a job type deprecation message', () => {
+ beforeEach(async () => {
+ mockWarnings.mockResolvedValue(mockWarningsType);
+ wrapper = createComponentWithApollo();
+ await waitForPromises();
+ });
+ it('shows the notification with one item', () => {
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlertItems()).toHaveLength(1);
+ expect(findAlertItems().at(0).text()).toContain('type');
+ expect(findAlertItems().at(0).text()).not.toContain('types');
+ });
+ });
+
+ describe('with both the root types and job type deprecation message', () => {
+ beforeEach(async () => {
+ mockWarnings.mockResolvedValue(mockWarningsTypesAll);
+ wrapper = createComponentWithApollo();
+ await waitForPromises();
+ });
+ it('shows the notification with two items', () => {
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlertItems()).toHaveLength(2);
+ expect(findAlertItems().at(0).text()).toContain('types');
+ expect(findAlertItems().at(1).text()).toContain('type');
+ expect(findAlertItems().at(1).text()).not.toContain('types');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/notification/mock_data.js b/spec/frontend/pipelines/notification/mock_data.js
new file mode 100644
index 00000000000..e36f391a854
--- /dev/null
+++ b/spec/frontend/pipelines/notification/mock_data.js
@@ -0,0 +1,33 @@
+const randomWarning = {
+ content: 'another random warning',
+ id: 'gid://gitlab/Ci::PipelineMessage/272',
+};
+
+const rootTypeWarning = {
+ content: 'root `types` will be removed in 15.0.',
+ id: 'gid://gitlab/Ci::PipelineMessage/273',
+};
+
+const typeWarning = {
+ content: '`type` will be removed in 15.0.',
+ id: 'gid://gitlab/Ci::PipelineMessage/274',
+};
+
+function createWarningMock(warnings) {
+ return {
+ data: {
+ project: {
+ id: 'gid://gitlab/Project/28"',
+ pipeline: {
+ id: 'gid://gitlab/Ci::Pipeline/183',
+ warningMessages: warnings,
+ },
+ },
+ },
+ };
+}
+
+export const mockWarningsWithoutDeprecation = createWarningMock([randomWarning]);
+export const mockWarningsRootType = createWarningMock([rootTypeWarning]);
+export const mockWarningsType = createWarningMock([typeWarning]);
+export const mockWarningsTypesAll = createWarningMock([rootTypeWarning, typeWarning]);
diff --git a/spec/frontend/pipelines/pipeline_triggerer_spec.js b/spec/frontend/pipelines/pipeline_triggerer_spec.js
index ffb2721f159..701b1691c7b 100644
--- a/spec/frontend/pipelines/pipeline_triggerer_spec.js
+++ b/spec/frontend/pipelines/pipeline_triggerer_spec.js
@@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import pipelineTriggerer from '~/pipelines/components/pipelines_list/pipeline_triggerer.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
@@ -47,15 +48,14 @@ describe('Pipelines Triggerer', () => {
});
});
- it('should render "API" when no triggerer is provided', () => {
+ it('should render "API" when no triggerer is provided', async () => {
wrapper.setProps({
pipeline: {
user: null,
},
});
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.find('.js-pipeline-url-api').text()).toEqual('API');
- });
+ await nextTick();
+ expect(wrapper.find('.js-pipeline-url-api').text()).toEqual('API');
});
});
diff --git a/spec/frontend/pipelines/pipeline_url_spec.js b/spec/frontend/pipelines/pipeline_url_spec.js
index 912b5afe0e1..b24e2e09ea8 100644
--- a/spec/frontend/pipelines/pipeline_url_spec.js
+++ b/spec/frontend/pipelines/pipeline_url_spec.js
@@ -1,41 +1,48 @@
-import { shallowMount } from '@vue/test-utils';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { trimText } from 'helpers/text_helper';
import PipelineUrlComponent from '~/pipelines/components/pipelines_list/pipeline_url.vue';
+import {
+ mockPipeline,
+ mockPipelineBranch,
+ mockPipelineTag,
+ mockPipelineNoCommit,
+} from './mock_data';
const projectPath = 'test/test';
describe('Pipeline Url Component', () => {
let wrapper;
- const findTableCell = () => wrapper.find('[data-testid="pipeline-url-table-cell"]');
- const findPipelineUrlLink = () => wrapper.find('[data-testid="pipeline-url-link"]');
- const findScheduledTag = () => wrapper.find('[data-testid="pipeline-url-scheduled"]');
- const findLatestTag = () => wrapper.find('[data-testid="pipeline-url-latest"]');
- const findYamlTag = () => wrapper.find('[data-testid="pipeline-url-yaml"]');
- const findFailureTag = () => wrapper.find('[data-testid="pipeline-url-failure"]');
- const findAutoDevopsTag = () => wrapper.find('[data-testid="pipeline-url-autodevops"]');
- const findAutoDevopsTagLink = () => wrapper.find('[data-testid="pipeline-url-autodevops-link"]');
- const findStuckTag = () => wrapper.find('[data-testid="pipeline-url-stuck"]');
- const findDetachedTag = () => wrapper.find('[data-testid="pipeline-url-detached"]');
- const findForkTag = () => wrapper.find('[data-testid="pipeline-url-fork"]');
- const findTrainTag = () => wrapper.find('[data-testid="pipeline-url-train"]');
-
- const defaultProps = {
- pipeline: {
- id: 1,
- path: 'foo',
- project: { full_path: `/${projectPath}` },
- flags: {},
- },
- pipelineScheduleUrl: 'foo',
- pipelineKey: 'id',
- };
-
- const createComponent = (props) => {
- wrapper = shallowMount(PipelineUrlComponent, {
+ const findTableCell = () => wrapper.findByTestId('pipeline-url-table-cell');
+ const findPipelineUrlLink = () => wrapper.findByTestId('pipeline-url-link');
+ const findScheduledTag = () => wrapper.findByTestId('pipeline-url-scheduled');
+ const findLatestTag = () => wrapper.findByTestId('pipeline-url-latest');
+ const findYamlTag = () => wrapper.findByTestId('pipeline-url-yaml');
+ const findFailureTag = () => wrapper.findByTestId('pipeline-url-failure');
+ const findAutoDevopsTag = () => wrapper.findByTestId('pipeline-url-autodevops');
+ const findAutoDevopsTagLink = () => wrapper.findByTestId('pipeline-url-autodevops-link');
+ const findStuckTag = () => wrapper.findByTestId('pipeline-url-stuck');
+ const findDetachedTag = () => wrapper.findByTestId('pipeline-url-detached');
+ const findForkTag = () => wrapper.findByTestId('pipeline-url-fork');
+ const findTrainTag = () => wrapper.findByTestId('pipeline-url-train');
+ const findRefName = () => wrapper.findByTestId('merge-request-ref');
+ const findCommitShortSha = () => wrapper.findByTestId('commit-short-sha');
+ const findCommitIcon = () => wrapper.findByTestId('commit-icon');
+ const findCommitIconType = () => wrapper.findByTestId('commit-icon-type');
+
+ const findCommitTitleContainer = () => wrapper.findByTestId('commit-title-container');
+ const findCommitTitle = () => wrapper.findByTestId('commit-title');
+
+ const defaultProps = mockPipeline(projectPath);
+
+ const createComponent = (props, rearrangePipelinesTable = false) => {
+ wrapper = shallowMountExtended(PipelineUrlComponent, {
propsData: { ...defaultProps, ...props },
provide: {
targetProjectFullPath: projectPath,
+ glFeatures: {
+ rearrangePipelinesTable,
+ },
},
});
};
@@ -45,158 +52,218 @@ describe('Pipeline Url Component', () => {
wrapper = null;
});
- it('should render pipeline url table cell', () => {
- createComponent();
+ describe('with the rearrangePipelinesTable feature flag turned off', () => {
+ it('should render pipeline url table cell', () => {
+ createComponent();
- expect(findTableCell().exists()).toBe(true);
- });
+ expect(findTableCell().exists()).toBe(true);
+ });
- it('should render a link the provided path and id', () => {
- createComponent();
+ it('should render a link the provided path and id', () => {
+ createComponent();
- expect(findPipelineUrlLink().attributes('href')).toBe('foo');
+ expect(findPipelineUrlLink().attributes('href')).toBe('foo');
- expect(findPipelineUrlLink().text()).toBe('#1');
- });
+ expect(findPipelineUrlLink().text()).toBe('#1');
+ });
- it('should not render tags when flags are not set', () => {
- createComponent();
-
- expect(findStuckTag().exists()).toBe(false);
- expect(findLatestTag().exists()).toBe(false);
- expect(findYamlTag().exists()).toBe(false);
- expect(findAutoDevopsTag().exists()).toBe(false);
- expect(findFailureTag().exists()).toBe(false);
- expect(findScheduledTag().exists()).toBe(false);
- expect(findForkTag().exists()).toBe(false);
- expect(findTrainTag().exists()).toBe(false);
- });
+ it('should not render tags when flags are not set', () => {
+ createComponent();
+
+ expect(findStuckTag().exists()).toBe(false);
+ expect(findLatestTag().exists()).toBe(false);
+ expect(findYamlTag().exists()).toBe(false);
+ expect(findAutoDevopsTag().exists()).toBe(false);
+ expect(findFailureTag().exists()).toBe(false);
+ expect(findScheduledTag().exists()).toBe(false);
+ expect(findForkTag().exists()).toBe(false);
+ expect(findTrainTag().exists()).toBe(false);
+ });
- it('should render the stuck tag when flag is provided', () => {
- createComponent({
- pipeline: {
- flags: {
- stuck: true,
- },
- },
+ it('should render the stuck tag when flag is provided', () => {
+ const stuckPipeline = defaultProps.pipeline;
+ stuckPipeline.flags.stuck = true;
+
+ createComponent({
+ ...stuckPipeline.pipeline,
+ });
+
+ expect(findStuckTag().text()).toContain('stuck');
});
- expect(findStuckTag().text()).toContain('stuck');
- });
+ it('should render latest tag when flag is provided', () => {
+ const latestPipeline = defaultProps.pipeline;
+ latestPipeline.flags.latest = true;
- it('should render latest tag when flag is provided', () => {
- createComponent({
- pipeline: {
- flags: {
- latest: true,
- },
- },
+ createComponent({
+ ...latestPipeline,
+ });
+
+ expect(findLatestTag().text()).toContain('latest');
});
- expect(findLatestTag().text()).toContain('latest');
- });
+ it('should render a yaml badge when it is invalid', () => {
+ const yamlPipeline = defaultProps.pipeline;
+ yamlPipeline.flags.yaml_errors = true;
- it('should render a yaml badge when it is invalid', () => {
- createComponent({
- pipeline: {
- flags: {
- yaml_errors: true,
- },
- },
+ createComponent({
+ ...yamlPipeline,
+ });
+
+ expect(findYamlTag().text()).toContain('yaml invalid');
});
- expect(findYamlTag().text()).toContain('yaml invalid');
- });
+ it('should render an autodevops badge when flag is provided', () => {
+ const autoDevopsPipeline = defaultProps.pipeline;
+ autoDevopsPipeline.flags.auto_devops = true;
- it('should render an autodevops badge when flag is provided', () => {
- createComponent({
- pipeline: {
- ...defaultProps.pipeline,
- flags: {
- auto_devops: true,
- },
- },
+ createComponent({
+ ...autoDevopsPipeline,
+ });
+
+ expect(trimText(findAutoDevopsTag().text())).toBe('Auto DevOps');
+
+ expect(findAutoDevopsTagLink().attributes()).toMatchObject({
+ href: '/help/topics/autodevops/index.md',
+ target: '_blank',
+ });
});
- expect(trimText(findAutoDevopsTag().text())).toBe('Auto DevOps');
+ it('should render a detached badge when flag is provided', () => {
+ const detachedMRPipeline = defaultProps.pipeline;
+ detachedMRPipeline.flags.detached_merge_request_pipeline = true;
- expect(findAutoDevopsTagLink().attributes()).toMatchObject({
- href: '/help/topics/autodevops/index.md',
- target: '_blank',
+ createComponent({
+ ...detachedMRPipeline,
+ });
+
+ expect(findDetachedTag().text()).toContain('detached');
});
- });
- it('should render a detached badge when flag is provided', () => {
- createComponent({
- pipeline: {
- flags: {
- detached_merge_request_pipeline: true,
- },
- },
+ it('should render error badge when pipeline has a failure reason set', () => {
+ const failedPipeline = defaultProps.pipeline;
+ failedPipeline.flags.failure_reason = true;
+ failedPipeline.failure_reason = 'some reason';
+
+ createComponent({
+ ...failedPipeline,
+ });
+
+ expect(findFailureTag().text()).toContain('error');
+ expect(findFailureTag().attributes('title')).toContain('some reason');
});
- expect(findDetachedTag().text()).toContain('detached');
- });
+ it('should render scheduled badge when pipeline was triggered by a schedule', () => {
+ const scheduledPipeline = defaultProps.pipeline;
+ scheduledPipeline.source = 'schedule';
- it('should render error badge when pipeline has a failure reason set', () => {
- createComponent({
- pipeline: {
- flags: {
- failure_reason: true,
- },
- failure_reason: 'some reason',
- },
+ createComponent({
+ ...scheduledPipeline,
+ });
+
+ expect(findScheduledTag().exists()).toBe(true);
+ expect(findScheduledTag().text()).toContain('Scheduled');
});
- expect(findFailureTag().text()).toContain('error');
- expect(findFailureTag().attributes('title')).toContain('some reason');
- });
+ it('should render the fork badge when the pipeline was run in a fork', () => {
+ const forkedPipeline = defaultProps.pipeline;
+ forkedPipeline.project.full_path = '/test/forked';
- it('should render scheduled badge when pipeline was triggered by a schedule', () => {
- createComponent({
- pipeline: {
- flags: {},
- source: 'schedule',
- },
+ createComponent({
+ ...forkedPipeline,
+ });
+
+ expect(findForkTag().exists()).toBe(true);
+ expect(findForkTag().text()).toBe('fork');
});
- expect(findScheduledTag().exists()).toBe(true);
- expect(findScheduledTag().text()).toContain('Scheduled');
- });
+ it('should render the train badge when the pipeline is a merge train pipeline', () => {
+ const mergeTrainPipeline = defaultProps.pipeline;
+ mergeTrainPipeline.flags.merge_train_pipeline = true;
- it('should render the fork badge when the pipeline was run in a fork', () => {
- createComponent({
- pipeline: {
- flags: {},
- project: { fullPath: '/test/forked' },
- },
+ createComponent({
+ ...mergeTrainPipeline,
+ });
+
+ expect(findTrainTag().text()).toContain('train');
});
- expect(findForkTag().exists()).toBe(true);
- expect(findForkTag().text()).toBe('fork');
- });
+ it('should not render the train badge when the pipeline is not a merge train pipeline', () => {
+ const mergeTrainPipeline = defaultProps.pipeline;
+ mergeTrainPipeline.flags.merge_train_pipeline = false;
- it('should render the train badge when the pipeline is a merge train pipeline', () => {
- createComponent({
- pipeline: {
- flags: {
- merge_train_pipeline: true,
- },
- },
+ createComponent({
+ ...mergeTrainPipeline,
+ });
+
+ expect(findTrainTag().exists()).toBe(false);
});
- expect(findTrainTag().text()).toContain('train');
+ it('should not render the commit wrapper and commit-short-sha', () => {
+ createComponent();
+
+ expect(findCommitTitleContainer().exists()).toBe(false);
+ expect(findCommitShortSha().exists()).toBe(false);
+ });
});
- it('should not render the train badge when the pipeline is not a merge train pipeline', () => {
- createComponent({
- pipeline: {
- flags: {
- merge_train_pipeline: false,
- },
+ describe('with the rearrangePipelinesTable feature flag turned on', () => {
+ it('should render the commit title, commit reference and commit-short-sha', () => {
+ createComponent({}, true);
+
+ const commitWrapper = findCommitTitleContainer();
+
+ expect(findCommitTitle(commitWrapper).exists()).toBe(true);
+ expect(findRefName().exists()).toBe(true);
+ expect(findCommitShortSha().exists()).toBe(true);
+ });
+
+ it('should render commit icon tooltip', () => {
+ createComponent({}, true);
+
+ expect(findCommitIcon().attributes('title')).toBe('Commit');
+ });
+
+ it.each`
+ pipeline | expectedTitle
+ ${mockPipelineTag()} | ${'Tag'}
+ ${mockPipelineBranch()} | ${'Branch'}
+ ${mockPipeline()} | ${'Merge Request'}
+ `(
+ 'should render tooltip $expectedTitle for commit icon type',
+ ({ pipeline, expectedTitle }) => {
+ createComponent(pipeline, true);
+
+ expect(findCommitIconType().attributes('title')).toBe(expectedTitle);
},
+ );
+
+ describe('with commit', () => {
+ beforeEach(() => {
+ createComponent({}, true);
+ });
+
+ it('displays commit title with link to pipeline', () => {
+ expect(findCommitTitle().attributes('href')).toBe(defaultProps.pipeline.path);
+ });
+
+ it('displays commit title text', () => {
+ expect(findCommitTitle().text()).toBe(defaultProps.pipeline.commit.title);
+ });
});
- expect(findTrainTag().exists()).toBe(false);
+ describe('without commit', () => {
+ beforeEach(() => {
+ createComponent(mockPipelineNoCommit(), true);
+ });
+
+ it('displays cant find head commit text', () => {
+ expect(findCommitTitle().text()).toBe("Can't find HEAD commit for this branch");
+ });
+
+ it('displays link to pipeline', () => {
+ expect(findCommitTitle().attributes('href')).toBe(mockPipelineNoCommit().pipeline.path);
+ });
+ });
});
});
diff --git a/spec/frontend/pipelines/pipelines_actions_spec.js b/spec/frontend/pipelines/pipelines_actions_spec.js
index c4bfec8ae14..9b2ee6b8278 100644
--- a/spec/frontend/pipelines/pipelines_actions_spec.js
+++ b/spec/frontend/pipelines/pipelines_actions_spec.js
@@ -1,14 +1,21 @@
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
import { TEST_HOST } from 'spec/test_constants';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
+import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import PipelinesManualActions from '~/pipelines/components/pipelines_list/pipelines_manual_actions.vue';
import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
jest.mock('~/flash');
+jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal', () => {
+ return {
+ confirmAction: jest.fn(),
+ };
+});
describe('Pipelines Actions dropdown', () => {
let wrapper;
@@ -35,6 +42,7 @@ describe('Pipelines Actions dropdown', () => {
wrapper = null;
mock.restore();
+ confirmAction.mockReset();
});
describe('manual actions', () => {
@@ -68,7 +76,7 @@ describe('Pipelines Actions dropdown', () => {
findAllDropdownItems().at(0).vm.$emit('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findDropdown().props('loading')).toBe(true);
await waitForPromises();
@@ -80,7 +88,7 @@ describe('Pipelines Actions dropdown', () => {
findAllDropdownItems().at(0).vm.$emit('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findDropdown().props('loading')).toBe(true);
await waitForPromises();
@@ -111,11 +119,11 @@ describe('Pipelines Actions dropdown', () => {
it('makes post request after confirming', async () => {
mock.onPost(scheduledJobAction.path).reply(200);
- jest.spyOn(window, 'confirm').mockReturnValue(true);
+ confirmAction.mockResolvedValueOnce(true);
findAllDropdownItems().at(0).vm.$emit('click');
- expect(window.confirm).toHaveBeenCalled();
+ expect(confirmAction).toHaveBeenCalled();
await waitForPromises();
@@ -124,11 +132,11 @@ describe('Pipelines Actions dropdown', () => {
it('does not make post request if confirmation is cancelled', async () => {
mock.onPost(scheduledJobAction.path).reply(200);
- jest.spyOn(window, 'confirm').mockReturnValue(false);
+ confirmAction.mockResolvedValueOnce(false);
findAllDropdownItems().at(0).vm.$emit('click');
- expect(window.confirm).toHaveBeenCalled();
+ expect(confirmAction).toHaveBeenCalled();
await waitForPromises();
diff --git a/spec/frontend/pipelines/pipelines_table_spec.js b/spec/frontend/pipelines/pipelines_table_spec.js
index 6fdbe907aed..f200d683a7a 100644
--- a/spec/frontend/pipelines/pipelines_table_spec.js
+++ b/spec/frontend/pipelines/pipelines_table_spec.js
@@ -9,7 +9,11 @@ import PipelineTriggerer from '~/pipelines/components/pipelines_list/pipeline_tr
import PipelineUrl from '~/pipelines/components/pipelines_list/pipeline_url.vue';
import PipelinesTable from '~/pipelines/components/pipelines_list/pipelines_table.vue';
import PipelinesTimeago from '~/pipelines/components/pipelines_list/time_ago.vue';
-import { PipelineKeyOptions } from '~/pipelines/constants';
+import {
+ PipelineKeyOptions,
+ BUTTON_TOOLTIP_RETRY,
+ BUTTON_TOOLTIP_CANCEL,
+} from '~/pipelines/constants';
import eventHub from '~/pipelines/event_hub';
import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
@@ -33,13 +37,18 @@ describe('Pipelines Table', () => {
return pipelines.find((p) => p.user !== null && p.commit !== null);
};
- const createComponent = (props = {}) => {
+ const createComponent = (props = {}, rearrangePipelinesTable = false) => {
wrapper = extendedWrapper(
mount(PipelinesTable, {
propsData: {
...defaultProps,
...props,
},
+ provide: {
+ glFeatures: {
+ rearrangePipelinesTable,
+ },
+ },
}),
);
};
@@ -61,6 +70,8 @@ describe('Pipelines Table', () => {
const findStagesTh = () => wrapper.findByTestId('stages-th');
const findTimeAgoTh = () => wrapper.findByTestId('timeago-th');
const findActionsTh = () => wrapper.findByTestId('actions-th');
+ const findRetryBtn = () => wrapper.findByTestId('pipelines-retry-button');
+ const findCancelBtn = () => wrapper.findByTestId('pipelines-cancel-button');
beforeEach(() => {
pipeline = createMockPipeline();
@@ -71,7 +82,7 @@ describe('Pipelines Table', () => {
wrapper = null;
});
- describe('Pipelines Table', () => {
+ describe('Pipelines Table with rearrangePipelinesTable feature flag turned off', () => {
beforeEach(() => {
createComponent({ pipelines: [pipeline], viewType: 'root' });
});
@@ -187,6 +198,39 @@ describe('Pipelines Table', () => {
it('should render pipeline operations', () => {
expect(findActions().exists()).toBe(true);
});
+
+ it('should render retry action tooltip', () => {
+ expect(findRetryBtn().attributes('title')).toBe(BUTTON_TOOLTIP_RETRY);
+ });
+
+ it('should render cancel action tooltip', () => {
+ expect(findCancelBtn().attributes('title')).toBe(BUTTON_TOOLTIP_CANCEL);
+ });
+ });
+ });
+
+ describe('Pipelines Table with rearrangePipelinesTable feature flag turned on', () => {
+ beforeEach(() => {
+ createComponent({ pipelines: [pipeline], viewType: 'root' }, true);
+ });
+
+ it('should render table head with correct columns', () => {
+ expect(findStatusTh().text()).toBe('Status');
+ expect(findPipelineTh().text()).toBe('Pipeline');
+ expect(findStagesTh().text()).toBe('Stages');
+ expect(findActionsTh().text()).toBe('Actions');
+ });
+
+ describe('triggerer cell', () => {
+ it('should render the pipeline triggerer', () => {
+ expect(findTriggerer().exists()).toBe(true);
+ });
+ });
+
+ describe('commit cell', () => {
+ it('should not render commit information', () => {
+ expect(findCommit().exists()).toBe(false);
+ });
});
});
});
diff --git a/spec/frontend/pipelines/test_reports/test_case_details_spec.js b/spec/frontend/pipelines/test_reports/test_case_details_spec.js
index c995eb864d1..4b33c1522a5 100644
--- a/spec/frontend/pipelines/test_reports/test_case_details_spec.js
+++ b/spec/frontend/pipelines/test_reports/test_case_details_spec.js
@@ -1,11 +1,9 @@
import { GlModal } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import TestCaseDetails from '~/pipelines/components/test_reports/test_case_details.vue';
import CodeBlock from '~/vue_shared/components/code_block.vue';
-const localVue = createLocalVue();
-
describe('Test case details', () => {
let wrapper;
const defaultTestCase = {
@@ -29,7 +27,6 @@ describe('Test case details', () => {
const createComponent = (testCase = {}) => {
wrapper = extendedWrapper(
shallowMount(TestCaseDetails, {
- localVue,
propsData: {
modalId: 'my-modal',
testCase: {
diff --git a/spec/frontend/pipelines/test_reports/test_reports_spec.js b/spec/frontend/pipelines/test_reports/test_reports_spec.js
index 384b7cf6930..e0daf8cb4b5 100644
--- a/spec/frontend/pipelines/test_reports/test_reports_spec.js
+++ b/spec/frontend/pipelines/test_reports/test_reports_spec.js
@@ -1,5 +1,6 @@
import { GlLoadingIcon } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import testReports from 'test_fixtures/pipelines/test_report.json';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
@@ -9,8 +10,7 @@ import TestSummary from '~/pipelines/components/test_reports/test_summary.vue';
import TestSummaryTable from '~/pipelines/components/test_reports/test_summary_table.vue';
import * as getters from '~/pipelines/stores/test_reports/getters';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('Test reports app', () => {
let wrapper;
@@ -44,7 +44,6 @@ describe('Test reports app', () => {
wrapper = extendedWrapper(
shallowMount(TestReports, {
store,
- localVue,
}),
);
};
diff --git a/spec/frontend/pipelines/test_reports/test_suite_table_spec.js b/spec/frontend/pipelines/test_reports/test_suite_table_spec.js
index 793bad6b82a..97241e14129 100644
--- a/spec/frontend/pipelines/test_reports/test_suite_table_spec.js
+++ b/spec/frontend/pipelines/test_reports/test_suite_table_spec.js
@@ -1,5 +1,6 @@
import { GlButton, GlFriendlyWrap, GlLink, GlPagination } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import testReports from 'test_fixtures/pipelines/test_report.json';
import SuiteTable from '~/pipelines/components/test_reports/test_suite_table.vue';
@@ -8,8 +9,7 @@ import * as getters from '~/pipelines/stores/test_reports/getters';
import { formatFilePath } from '~/pipelines/stores/test_reports/utils';
import skippedTestCases from './mock_data';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('Test reports suite table', () => {
let wrapper;
@@ -47,7 +47,6 @@ describe('Test reports suite table', () => {
wrapper = shallowMount(SuiteTable, {
store,
- localVue,
stubs: { GlFriendlyWrap },
});
};
diff --git a/spec/frontend/pipelines/test_reports/test_summary_table_spec.js b/spec/frontend/pipelines/test_reports/test_summary_table_spec.js
index 0813739d72f..1598d5c337f 100644
--- a/spec/frontend/pipelines/test_reports/test_summary_table_spec.js
+++ b/spec/frontend/pipelines/test_reports/test_summary_table_spec.js
@@ -1,11 +1,11 @@
-import { mount, createLocalVue } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import testReports from 'test_fixtures/pipelines/test_report.json';
import SummaryTable from '~/pipelines/components/test_reports/test_summary_table.vue';
import * as getters from '~/pipelines/stores/test_reports/getters';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('Test reports summary table', () => {
let wrapper;
@@ -29,7 +29,6 @@ describe('Test reports summary table', () => {
wrapper = mount(SummaryTable, {
propsData: defaultProps,
store,
- localVue,
});
};
diff --git a/spec/frontend/popovers/components/popovers_spec.js b/spec/frontend/popovers/components/popovers_spec.js
index 2751a878e51..6fdcd34ae83 100644
--- a/spec/frontend/popovers/components/popovers_spec.js
+++ b/spec/frontend/popovers/components/popovers_spec.js
@@ -1,5 +1,6 @@
import { GlPopover } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import { useMockMutationObserver } from 'helpers/mock_dom_observer';
import Popovers from '~/popovers/components/popovers.vue';
@@ -7,10 +8,10 @@ describe('popovers/components/popovers.vue', () => {
const { trigger: triggerMutate } = useMockMutationObserver();
let wrapper;
- const buildWrapper = (...targets) => {
+ const buildWrapper = async (...targets) => {
wrapper = shallowMount(Popovers);
wrapper.vm.addPopovers(targets);
- return wrapper.vm.$nextTick();
+ await nextTick();
};
const createPopoverTarget = (options = {}) => {
@@ -49,7 +50,7 @@ describe('popovers/components/popovers.vue', () => {
buildWrapper(target);
wrapper.vm.addPopovers([target]);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.findAll(GlPopover)).toHaveLength(1);
});
@@ -86,7 +87,7 @@ describe('popovers/components/popovers.vue', () => {
await buildWrapper(createPopoverTarget(), createPopoverTarget());
wrapper.vm.dispose();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(allPopovers()).toHaveLength(0);
});
@@ -97,7 +98,7 @@ describe('popovers/components/popovers.vue', () => {
await buildWrapper(target, createPopoverTarget());
wrapper.vm.dispose(target);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(allPopovers()).toHaveLength(1);
});
@@ -109,13 +110,13 @@ describe('popovers/components/popovers.vue', () => {
await buildWrapper(target);
wrapper.vm.addPopovers([target, createPopoverTarget()]);
- await wrapper.vm.$nextTick();
+ await nextTick();
triggerMutate(document.body, {
entry: { removedNodes: [target] },
options: { childList: true },
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(allPopovers()).toHaveLength(1);
});
diff --git a/spec/frontend/popovers/index_spec.js b/spec/frontend/popovers/index_spec.js
index ea3b78332d7..c82fe7b47d9 100644
--- a/spec/frontend/popovers/index_spec.js
+++ b/spec/frontend/popovers/index_spec.js
@@ -1,8 +1,7 @@
+import { nextTick } from 'vue';
import { initPopovers, dispose, destroy } from '~/popovers';
describe('popovers/index.js', () => {
- let popoversApp;
-
const createPopoverTarget = (trigger = 'hover') => {
const target = document.createElement('button');
const dataset = {
@@ -22,7 +21,7 @@ describe('popovers/index.js', () => {
};
const buildPopoversApp = () => {
- popoversApp = initPopovers('[data-toggle="popover"]');
+ initPopovers('[data-toggle="popover"]');
};
const triggerEvent = (target, eventName = 'mouseenter') => {
@@ -44,7 +43,7 @@ describe('popovers/index.js', () => {
triggerEvent(target);
- await popoversApp.$nextTick();
+ await nextTick();
const html = document.querySelector('.gl-popover').innerHTML;
expect(document.querySelector('.gl-popover')).not.toBe(null);
@@ -59,7 +58,7 @@ describe('popovers/index.js', () => {
buildPopoversApp();
triggerEvent(target, trigger);
- await popoversApp.$nextTick();
+ await nextTick();
expect(document.querySelector('.gl-popover')).not.toBe(null);
expect(document.querySelector('.gl-popover').innerHTML).toContain('default title');
@@ -73,7 +72,7 @@ describe('popovers/index.js', () => {
const trigger = 'click';
const target = createPopoverTarget(trigger);
triggerEvent(target, trigger);
- await popoversApp.$nextTick();
+ await nextTick();
expect(document.querySelector('.gl-popover')).not.toBe(null);
});
@@ -86,17 +85,17 @@ describe('popovers/index.js', () => {
buildPopoversApp();
triggerEvent(target);
triggerEvent(createPopoverTarget());
- await popoversApp.$nextTick();
+ await nextTick();
expect(document.querySelectorAll('.gl-popover')).toHaveLength(2);
dispose([fakeTarget]);
- await popoversApp.$nextTick();
+ await nextTick();
expect(document.querySelectorAll('.gl-popover')).toHaveLength(2);
dispose([target]);
- await popoversApp.$nextTick();
+ await nextTick();
expect(document.querySelectorAll('.gl-popover')).toHaveLength(1);
});
diff --git a/spec/frontend/profile/account/components/delete_account_modal_spec.js b/spec/frontend/profile/account/components/delete_account_modal_spec.js
index f1784500baf..ad62d84c43c 100644
--- a/spec/frontend/profile/account/components/delete_account_modal_spec.js
+++ b/spec/frontend/profile/account/components/delete_account_modal_spec.js
@@ -1,6 +1,6 @@
import { mount } from '@vue/test-utils';
import { merge } from 'lodash';
-import Vue from 'vue';
+import { nextTick } from 'vue';
import { TEST_HOST } from 'helpers/test_constants';
import deleteAccountModal from '~/profile/account/components/delete_account_modal.vue';
@@ -56,7 +56,7 @@ describe('DeleteAccountModal component', () => {
const findModal = () => wrapper.find(GlModalStub);
describe('with password confirmation', () => {
- beforeEach((done) => {
+ beforeEach(async () => {
createWrapper({
propsData: {
confirmWithPassword: true,
@@ -65,48 +65,40 @@ describe('DeleteAccountModal component', () => {
vm.isOpen = true;
- Vue.nextTick().then(done).catch(done.fail);
+ await nextTick();
});
- it('does not accept empty password', (done) => {
+ it('does not accept empty password', async () => {
const { form, input } = findElements();
jest.spyOn(form, 'submit').mockImplementation(() => {});
input.value = '';
input.dispatchEvent(new Event('input'));
- Vue.nextTick()
- .then(() => {
- expect(vm.enteredPassword).toBe(input.value);
- expect(findModal().attributes('ok-disabled')).toBe('true');
- findModal().vm.$emit('primary');
+ await nextTick();
+ expect(vm.enteredPassword).toBe(input.value);
+ expect(findModal().attributes('ok-disabled')).toBe('true');
+ findModal().vm.$emit('primary');
- expect(form.submit).not.toHaveBeenCalled();
- })
- .then(done)
- .catch(done.fail);
+ expect(form.submit).not.toHaveBeenCalled();
});
- it('submits form with password', (done) => {
+ it('submits form with password', async () => {
const { form, input } = findElements();
jest.spyOn(form, 'submit').mockImplementation(() => {});
input.value = 'anything';
input.dispatchEvent(new Event('input'));
- Vue.nextTick()
- .then(() => {
- expect(vm.enteredPassword).toBe(input.value);
- expect(findModal().attributes('ok-disabled')).toBeUndefined();
- findModal().vm.$emit('primary');
+ await nextTick();
+ expect(vm.enteredPassword).toBe(input.value);
+ expect(findModal().attributes('ok-disabled')).toBeUndefined();
+ findModal().vm.$emit('primary');
- expect(form.submit).toHaveBeenCalled();
- })
- .then(done)
- .catch(done.fail);
+ expect(form.submit).toHaveBeenCalled();
});
});
describe('with username confirmation', () => {
- beforeEach((done) => {
+ beforeEach(async () => {
createWrapper({
propsData: {
confirmWithPassword: false,
@@ -115,43 +107,35 @@ describe('DeleteAccountModal component', () => {
vm.isOpen = true;
- Vue.nextTick().then(done).catch(done.fail);
+ await nextTick();
});
- it('does not accept wrong username', (done) => {
+ it('does not accept wrong username', async () => {
const { form, input } = findElements();
jest.spyOn(form, 'submit').mockImplementation(() => {});
input.value = 'this is wrong';
input.dispatchEvent(new Event('input'));
- Vue.nextTick()
- .then(() => {
- expect(vm.enteredUsername).toBe(input.value);
- expect(findModal().attributes('ok-disabled')).toBe('true');
- findModal().vm.$emit('primary');
+ await nextTick();
+ expect(vm.enteredUsername).toBe(input.value);
+ expect(findModal().attributes('ok-disabled')).toBe('true');
+ findModal().vm.$emit('primary');
- expect(form.submit).not.toHaveBeenCalled();
- })
- .then(done)
- .catch(done.fail);
+ expect(form.submit).not.toHaveBeenCalled();
});
- it('submits form with correct username', (done) => {
+ it('submits form with correct username', async () => {
const { form, input } = findElements();
jest.spyOn(form, 'submit').mockImplementation(() => {});
input.value = username;
input.dispatchEvent(new Event('input'));
- Vue.nextTick()
- .then(() => {
- expect(vm.enteredUsername).toBe(input.value);
- expect(findModal().attributes('ok-disabled')).toBeUndefined();
- findModal().vm.$emit('primary');
+ await nextTick();
+ expect(vm.enteredUsername).toBe(input.value);
+ expect(findModal().attributes('ok-disabled')).toBeUndefined();
+ findModal().vm.$emit('primary');
- expect(form.submit).toHaveBeenCalled();
- })
- .then(done)
- .catch(done.fail);
+ expect(form.submit).toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/profile/account/components/update_username_spec.js b/spec/frontend/profile/account/components/update_username_spec.js
index bda07af4feb..e342b7c4ba1 100644
--- a/spec/frontend/profile/account/components/update_username_spec.js
+++ b/spec/frontend/profile/account/components/update_username_spec.js
@@ -1,6 +1,7 @@
import { GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+import { nextTick } from 'vue';
import { TEST_HOST } from 'helpers/test_constants';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
@@ -58,7 +59,7 @@ describe('UpdateUsername component', () => {
it('has a disabled button if the username was not changed', async () => {
const { openModalBtn } = findElements();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(openModalBtn.props('disabled')).toBe(true);
});
@@ -69,7 +70,7 @@ describe('UpdateUsername component', () => {
input.element.value = 'newUsername';
input.trigger('input');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(openModalBtn.props('disabled')).toBe(false);
});
@@ -83,7 +84,7 @@ describe('UpdateUsername component', () => {
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ newUsername });
- await wrapper.vm.$nextTick();
+ await nextTick();
});
it('confirmation modal contains proper header and body', async () => {
@@ -100,7 +101,7 @@ describe('UpdateUsername component', () => {
jest.spyOn(axios, 'put');
await wrapper.vm.onConfirm();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(axios.put).toHaveBeenCalledWith(actionUrl, { user: { username: newUsername } });
});
@@ -117,7 +118,7 @@ describe('UpdateUsername component', () => {
});
await wrapper.vm.onConfirm();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(input.attributes('disabled')).toBe(undefined);
expect(openModalBtn.props('disabled')).toBe(true);
diff --git a/spec/frontend/projects/commit/components/branches_dropdown_spec.js b/spec/frontend/projects/commit/components/branches_dropdown_spec.js
index 30556cdeae1..e2848e615c3 100644
--- a/spec/frontend/projects/commit/components/branches_dropdown_spec.js
+++ b/spec/frontend/projects/commit/components/branches_dropdown_spec.js
@@ -1,6 +1,6 @@
import { GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
@@ -115,7 +115,7 @@ describe('BranchesDropdown', () => {
findSearchBoxByType().vm.$emit('input', '_anything_');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(spy).toHaveBeenCalledWith('_anything_');
expect(wrapper.vm.searchTerm).toBe('_anything_');
diff --git a/spec/frontend/projects/commit/components/form_modal_spec.js b/spec/frontend/projects/commit/components/form_modal_spec.js
index 93e2ae13628..79e9dab935d 100644
--- a/spec/frontend/projects/commit/components/form_modal_spec.js
+++ b/spec/frontend/projects/commit/components/form_modal_spec.js
@@ -2,6 +2,7 @@ import { GlModal, GlForm, GlFormCheckbox, GlSprintf } from '@gitlab/ui';
import { within } from '@testing-library/dom';
import { shallowMount, mount, createWrapper } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+import { nextTick } from 'vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import api from '~/api';
import axios from '~/lib/utils/axios_utils';
@@ -156,7 +157,7 @@ describe('CommitFormModal', () => {
it('Changes the start_branch input value', async () => {
findBranchesDropdown().vm.$emit('selectBranch', '_changed_branch_value_');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findStartBranch().attributes('value')).toBe('_changed_branch_value_');
});
@@ -165,7 +166,7 @@ describe('CommitFormModal', () => {
createComponent(shallowMount, {}, {}, { isCherryPick: true });
findProjectsDropdown().vm.$emit('selectProject', '_changed_project_value_');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findTargetProject().attributes('value')).toBe('_changed_project_value_');
});
@@ -174,7 +175,7 @@ describe('CommitFormModal', () => {
it('action primary button triggers Redis HLL tracking api call', async () => {
createComponent(mount, {}, {}, { primaryActionEventName: 'test_event' });
- await wrapper.vm.$nextTick();
+ await nextTick();
jest.spyOn(findForm().element, 'submit');
diff --git a/spec/frontend/projects/commits/components/author_select_spec.js b/spec/frontend/projects/commits/components/author_select_spec.js
index 23b4cccd92c..4e567ab030e 100644
--- a/spec/frontend/projects/commits/components/author_select_spec.js
+++ b/spec/frontend/projects/commits/components/author_select_spec.js
@@ -1,12 +1,12 @@
import { GlDropdown, GlDropdownSectionHeader, GlSearchBoxByType, GlDropdownItem } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import * as urlUtility from '~/lib/utils/url_utility';
import AuthorSelect from '~/projects/commits/components/author_select.vue';
import { createStore } from '~/projects/commits/store';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
const commitsPath = 'author/search/url';
const currentAuthor = 'lorem';
@@ -38,7 +38,6 @@ describe('Author Select', () => {
`);
wrapper = shallowMount(AuthorSelect, {
- localVue,
store: new Vuex.Store(store),
propsData: {
projectCommitsEl: document.querySelector('.js-project-commits-show'),
@@ -64,36 +63,33 @@ describe('Author Select', () => {
const findDropdownItems = () => wrapper.findAll(GlDropdownItem);
describe('user is searching via "filter by commit message"', () => {
- it('disables dropdown container', () => {
+ it('disables dropdown container', async () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ hasSearchParam: true });
- return wrapper.vm.$nextTick().then(() => {
- expect(findDropdownContainer().attributes('disabled')).toBeFalsy();
- });
+ await nextTick();
+ expect(findDropdownContainer().attributes('disabled')).toBeFalsy();
});
- it('has correct tooltip message', () => {
+ it('has correct tooltip message', async () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ hasSearchParam: true });
- return wrapper.vm.$nextTick().then(() => {
- expect(findDropdownContainer().attributes('title')).toBe(
- 'Searching by both author and message is currently not supported.',
- );
- });
+ await nextTick();
+ expect(findDropdownContainer().attributes('title')).toBe(
+ 'Searching by both author and message is currently not supported.',
+ );
});
- it('disables dropdown', () => {
+ it('disables dropdown', async () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ hasSearchParam: false });
- return wrapper.vm.$nextTick().then(() => {
- expect(findDropdown().attributes('disabled')).toBeFalsy();
- });
+ await nextTick();
+ expect(findDropdown().attributes('disabled')).toBeFalsy();
});
it('hasSearchParam if user types a truthy string', () => {
@@ -108,14 +104,13 @@ describe('Author Select', () => {
expect(findDropdown().attributes('text')).toBe('Author');
});
- it('displays the current selected author', () => {
+ it('displays the current selected author', async () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ currentAuthor });
- return wrapper.vm.$nextTick().then(() => {
- expect(findDropdown().attributes('text')).toBe(currentAuthor);
- });
+ await nextTick();
+ expect(findDropdown().attributes('text')).toBe(currentAuthor);
});
it('displays correct header text', () => {
@@ -150,13 +145,12 @@ describe('Author Select', () => {
expect(findDropdownItems().at(0).text()).toBe('Any Author');
});
- it('displays the project authors', () => {
- return wrapper.vm.$nextTick().then(() => {
- expect(findDropdownItems()).toHaveLength(authors.length + 1);
- });
+ it('displays the project authors', async () => {
+ await nextTick();
+ expect(findDropdownItems()).toHaveLength(authors.length + 1);
});
- it('has the correct props', () => {
+ it('has the correct props', async () => {
const [{ avatar_url, username }] = authors;
const result = {
avatarUrl: avatar_url,
@@ -168,15 +162,13 @@ describe('Author Select', () => {
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ currentAuthor });
- return wrapper.vm.$nextTick().then(() => {
- expect(findDropdownItems().at(1).props()).toEqual(expect.objectContaining(result));
- });
+ await nextTick();
+ expect(findDropdownItems().at(1).props()).toEqual(expect.objectContaining(result));
});
- it("display the author's name", () => {
- return wrapper.vm.$nextTick().then(() => {
- expect(findDropdownItems().at(1).text()).toBe(currentAuthor);
- });
+ it("display the author's name", async () => {
+ await nextTick();
+ expect(findDropdownItems().at(1).text()).toBe(currentAuthor);
});
it('passes selected author to redirectPath', () => {
diff --git a/spec/frontend/projects/compare/components/app_spec.js b/spec/frontend/projects/compare/components/app_spec.js
index 7989a6f3d74..18e7f2e0f6e 100644
--- a/spec/frontend/projects/compare/components/app_spec.js
+++ b/spec/frontend/projects/compare/components/app_spec.js
@@ -1,5 +1,6 @@
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import CompareApp from '~/projects/compare/components/app.vue';
import RevisionCard from '~/projects/compare/components/revision_card.vue';
import { appDefaultProps as defaultProps } from './mock_data';
@@ -91,7 +92,7 @@ describe('CompareApp component', () => {
project,
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findTargetRevisionCard().props('selectedProject')).toEqual(
expect.objectContaining(project),
@@ -106,7 +107,7 @@ describe('CompareApp component', () => {
revision,
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findSourceRevisionCard().props('paramsBranch')).toBe(revision);
});
@@ -125,7 +126,7 @@ describe('CompareApp component', () => {
it('swaps revisions when clicked', async () => {
findSwapRevisionsButton().vm.$emit('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findTargetRevisionCard().props('paramsBranch')).toBe(defaultProps.paramsTo);
expect(findSourceRevisionCard().props('paramsBranch')).toBe(defaultProps.paramsFrom);
diff --git a/spec/frontend/projects/compare/components/repo_dropdown_spec.js b/spec/frontend/projects/compare/components/repo_dropdown_spec.js
index 27a7a32ebca..98aec347e4b 100644
--- a/spec/frontend/projects/compare/components/repo_dropdown_spec.js
+++ b/spec/frontend/projects/compare/components/repo_dropdown_spec.js
@@ -1,5 +1,6 @@
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import RepoDropdown from '~/projects/compare/components/repo_dropdown.vue';
import { revisionCardDefaultProps as defaultProps } from './mock_data';
@@ -39,7 +40,7 @@ describe('RepoDropdown component', () => {
it('does not emit `changeTargetProject` event', async () => {
wrapper.vm.emitTargetProject('foo');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.emitted('changeTargetProject')).toBeUndefined();
});
});
@@ -67,13 +68,13 @@ describe('RepoDropdown component', () => {
it('updates the hidden input value when onClick method is triggered', async () => {
const repoId = '1';
wrapper.vm.onClick({ id: repoId });
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findHiddenInput().attributes('value')).toBe(repoId);
});
it('emits `selectProject` event when another target project is selected', async () => {
findGlDropdown().findAll(GlDropdownItem).at(0).vm.$emit('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.emitted('selectProject')[0][0]).toEqual({
direction: 'from',
diff --git a/spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js b/spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js
index eb80d57fb3c..102f95f65da 100644
--- a/spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js
+++ b/spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js
@@ -1,6 +1,7 @@
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
+import { nextTick } from 'vue';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import RevisionDropdown from '~/projects/compare/components/revision_dropdown_legacy.vue';
@@ -105,7 +106,7 @@ describe('RevisionDropdown component', () => {
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ branches: ['some-branch'] });
- await wrapper.vm.$nextTick();
+ await nextTick();
findFirstGlDropdownItem().vm.$emit('click');
diff --git a/spec/frontend/projects/compare/components/revision_dropdown_spec.js b/spec/frontend/projects/compare/components/revision_dropdown_spec.js
index 118bb68585e..c8a90848492 100644
--- a/spec/frontend/projects/compare/components/revision_dropdown_spec.js
+++ b/spec/frontend/projects/compare/components/revision_dropdown_spec.js
@@ -1,6 +1,7 @@
import { GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
+import { nextTick } from 'vue';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import RevisionDropdown from '~/projects/compare/components/revision_dropdown.vue';
@@ -141,7 +142,7 @@ describe('RevisionDropdown component', () => {
it('emits `selectRevision` event when another revision is selected', async () => {
createComponent();
wrapper.vm.branches = ['some-branch'];
- await wrapper.vm.$nextTick();
+ await nextTick();
findGlDropdown().findAll(GlDropdownItem).at(0).vm.$emit('click');
diff --git a/spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap b/spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap
index e1e1aac09aa..b8f9951bbfc 100644
--- a/spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap
+++ b/spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap
@@ -49,16 +49,17 @@ exports[`Project remove modal initialized matches the snapshot 1`] = `
primarybuttontext=""
secondarybuttonlink=""
secondarybuttontext=""
- title="You are about to permanently delete this project"
+ title=""
variant="danger"
>
- <p>
- This project is
- <strong>
- NOT
- </strong>
- a fork, and has the following:
- </p>
+ <h4
+ class="gl-alert-title"
+ data-testid="delete-alert-title"
+ >
+
+ You are about to delete this project containing:
+
+ </h4>
<ul>
<li>
@@ -77,25 +78,17 @@ exports[`Project remove modal initialized matches the snapshot 1`] = `
4 stars
</li>
</ul>
- After a project is permanently deleted, it
- <strong>
- cannot be recovered
- </strong>
- . Permanently deleting this project will
- <strong>
- immediately delete
- </strong>
- its repositories and
+ This project is
<strong>
- all related resources
+ NOT
</strong>
- , including issues, merge requests etc.
+ a fork. This process deletes the project repository and all related resources.
</gl-alert-stub>
<p
class="gl-mb-1"
>
- Please type the following to confirm:
+ Enter the following to confirm:
</p>
<p>
diff --git a/spec/frontend/projects/components/project_delete_button_spec.js b/spec/frontend/projects/components/project_delete_button_spec.js
index bb6021fadda..a3bc4931eb3 100644
--- a/spec/frontend/projects/components/project_delete_button_spec.js
+++ b/spec/frontend/projects/components/project_delete_button_spec.js
@@ -50,7 +50,12 @@ describe('Project remove modal', () => {
it('passes confirmPhrase and formPath props to the shared delete button', () => {
expect(findSharedDeleteButton().props()).toEqual({
confirmPhrase: defaultProps.confirmPhrase,
+ forksCount: defaultProps.forksCount,
formPath: defaultProps.formPath,
+ isFork: defaultProps.isFork,
+ issuesCount: defaultProps.issuesCount,
+ mergeRequestsCount: defaultProps.mergeRequestsCount,
+ starsCount: defaultProps.starsCount,
});
});
});
diff --git a/spec/frontend/projects/components/shared/__snapshots__/delete_button_spec.js.snap b/spec/frontend/projects/components/shared/__snapshots__/delete_button_spec.js.snap
index dd54db7dc0a..2d1039a8743 100644
--- a/spec/frontend/projects/components/shared/__snapshots__/delete_button_spec.js.snap
+++ b/spec/frontend/projects/components/shared/__snapshots__/delete_button_spec.js.snap
@@ -34,13 +34,63 @@ exports[`Project remove modal intialized matches the snapshot 1`] = `
ok-variant="danger"
title-class="gl-text-red-500"
>
- Delete project. Are you ABSOLUTELY SURE?
+ Are you absolutely sure?
<div>
+ <gl-alert-stub
+ class="gl-mb-5"
+ dismisslabel="Dismiss"
+ primarybuttonlink=""
+ primarybuttontext=""
+ secondarybuttonlink=""
+ secondarybuttontext=""
+ title=""
+ variant="danger"
+ >
+ <h4
+ class="gl-alert-title"
+ data-testid="delete-alert-title"
+ >
+
+ You are about to delete this project containing:
+
+ </h4>
+
+ <ul>
+ <li>
+ <gl-sprintf-stub
+ message="1 issue"
+ />
+ </li>
+
+ <li>
+ <gl-sprintf-stub
+ message="2 merge requests"
+ />
+ </li>
+
+ <li>
+ <gl-sprintf-stub
+ message="3 forks"
+ />
+ </li>
+
+ <li>
+ <gl-sprintf-stub
+ message="4 stars"
+ />
+ </li>
+ </ul>
+
+ <gl-sprintf-stub
+ data-testid="delete-alert-body"
+ message="This project is %{strongStart}NOT%{strongEnd} a fork. This process deletes the project repository and all related resources."
+ />
+ </gl-alert-stub>
<p
class="gl-mb-1"
>
- Please type the following to confirm:
+ Enter the following to confirm:
</p>
<p>
diff --git a/spec/frontend/projects/components/shared/delete_button_spec.js b/spec/frontend/projects/components/shared/delete_button_spec.js
index 3e491584670..45c39ee91d8 100644
--- a/spec/frontend/projects/components/shared/delete_button_spec.js
+++ b/spec/frontend/projects/components/shared/delete_button_spec.js
@@ -12,15 +12,25 @@ describe('Project remove modal', () => {
const findConfirmButton = () => wrapper.find('.js-modal-action-primary');
const findAuthenticityTokenInput = () => findFormElement().find('input[name=authenticity_token]');
const findModal = () => wrapper.find(GlModal);
+ const findTitle = () => wrapper.find('[data-testid="delete-alert-title"]');
+ const findAlertBody = () => wrapper.find('[data-testid="delete-alert-body"]');
const defaultProps = {
confirmPhrase: 'foo',
formPath: 'some/path',
+ isFork: false,
+ issuesCount: 1,
+ mergeRequestsCount: 2,
+ forksCount: 3,
+ starsCount: 4,
};
- const createComponent = (data = {}, stubs = {}) => {
+ const createComponent = (data = {}, stubs = {}, props = {}) => {
wrapper = shallowMount(SharedDeleteButton, {
- propsData: defaultProps,
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
data: () => data,
stubs: {
GlModal: stubComponent(GlModal, {
@@ -88,4 +98,20 @@ describe('Project remove modal', () => {
expect(findFormElement().element.submit).toHaveBeenCalled();
});
});
+
+ describe('when project is a fork', () => {
+ beforeEach(() => {
+ createComponent({}, {}, { isFork: true });
+ });
+
+ it('matches the fork title', () => {
+ expect(findTitle().text()).toEqual('You are about to delete this forked project containing:');
+ });
+
+ it('matches the fork body', () => {
+ expect(findAlertBody().attributes().message).toEqual(
+ 'This process deletes the project repository and all related resources.',
+ );
+ });
+ });
});
diff --git a/spec/frontend/projects/new/components/deployment_target_select_spec.js b/spec/frontend/projects/new/components/deployment_target_select_spec.js
new file mode 100644
index 00000000000..8fe4c5f1230
--- /dev/null
+++ b/spec/frontend/projects/new/components/deployment_target_select_spec.js
@@ -0,0 +1,82 @@
+import { GlFormGroup, GlFormSelect } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { mockTracking } from 'helpers/tracking_helper';
+import DeploymentTargetSelect from '~/projects/new/components/deployment_target_select.vue';
+import {
+ DEPLOYMENT_TARGET_SELECTIONS,
+ DEPLOYMENT_TARGET_LABEL,
+ DEPLOYMENT_TARGET_EVENT,
+ NEW_PROJECT_FORM,
+} from '~/projects/new/constants';
+
+describe('Deployment target select', () => {
+ let wrapper;
+ let trackingSpy;
+
+ const findFormGroup = () => wrapper.findComponent(GlFormGroup);
+ const findSelect = () => wrapper.findComponent(GlFormSelect);
+
+ const createdWrapper = () => {
+ wrapper = shallowMount(DeploymentTargetSelect, {
+ stubs: {
+ GlFormSelect,
+ },
+ });
+ };
+
+ const createForm = () => {
+ setFixtures(`
+ <form id="${NEW_PROJECT_FORM}">
+ </form>
+ `);
+ };
+
+ beforeEach(() => {
+ createForm();
+ createdWrapper();
+
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders the correct label', () => {
+ expect(findFormGroup().attributes('label')).toBe('Project deployment target (optional)');
+ });
+
+ it('renders a select with the disabled default option', () => {
+ expect(findSelect().find('option').text()).toBe('Select the deployment target');
+ expect(findSelect().find('option').attributes('disabled')).toBe('disabled');
+ });
+
+ describe.each`
+ selectedTarget | formSubmitted | eventSent
+ ${null} | ${true} | ${false}
+ ${DEPLOYMENT_TARGET_SELECTIONS[0]} | ${false} | ${false}
+ ${DEPLOYMENT_TARGET_SELECTIONS[0]} | ${true} | ${true}
+ `('Snowplow tracking event', ({ selectedTarget, formSubmitted, eventSent }) => {
+ beforeEach(() => {
+ findSelect().vm.$emit('input', selectedTarget);
+
+ if (formSubmitted) {
+ const form = document.getElementById(NEW_PROJECT_FORM);
+ form.dispatchEvent(new Event('submit'));
+ }
+ });
+
+ if (eventSent) {
+ it(`is sent, when the the selectedTarget is ${selectedTarget} and the formSubmitted is ${formSubmitted} `, () => {
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, DEPLOYMENT_TARGET_EVENT, {
+ label: DEPLOYMENT_TARGET_LABEL,
+ property: selectedTarget,
+ });
+ });
+ } else {
+ it(`is not sent, when the the selectedTarget is ${selectedTarget} and the formSubmitted is ${formSubmitted} `, () => {
+ expect(trackingSpy).toHaveBeenCalledTimes(0);
+ });
+ }
+ });
+});
diff --git a/spec/frontend/projects/new/components/new_project_url_select_spec.js b/spec/frontend/projects/new/components/new_project_url_select_spec.js
index 258fa7636d4..921f5b74278 100644
--- a/spec/frontend/projects/new/components/new_project_url_select_spec.js
+++ b/spec/frontend/projects/new/components/new_project_url_select_spec.js
@@ -6,9 +6,10 @@ import {
GlSearchBoxByType,
} from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import eventHub from '~/projects/new/event_hub';
@@ -94,13 +95,14 @@ describe('NewProjectUrlSelect component', () => {
const clickDropdownItem = async () => {
wrapper.findComponent(GlDropdownItem).vm.$emit('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
};
const showDropdown = async () => {
findDropdown().vm.$emit('shown');
await wrapper.vm.$apollo.queries.currentUser.refetch();
jest.runOnlyPendingTimers();
+ await waitForPromises();
};
afterEach(() => {
@@ -235,8 +237,7 @@ describe('NewProjectUrlSelect component', () => {
};
wrapper = mountComponent({ search: 'no matches', queryResponse, mountFn: mount });
- jest.runOnlyPendingTimers();
- await wrapper.vm.$nextTick();
+ await waitForPromises();
expect(wrapper.find('li').text()).toBe('No matches found');
});
diff --git a/spec/frontend/projects/pipelines/charts/components/app_spec.js b/spec/frontend/projects/pipelines/charts/components/app_spec.js
index 574756322c7..9c94925c817 100644
--- a/spec/frontend/projects/pipelines/charts/components/app_spec.js
+++ b/spec/frontend/projects/pipelines/charts/components/app_spec.js
@@ -1,5 +1,6 @@
import { GlTabs, GlTab } from '@gitlab/ui';
import { merge } from 'lodash';
+import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants';
@@ -99,7 +100,7 @@ describe('ProjectsPipelinesChartsApp', () => {
tabs.vm.$emit('input', 1);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(tabs.attributes('value')).toBe('1');
});
@@ -115,7 +116,7 @@ describe('ProjectsPipelinesChartsApp', () => {
tabs.vm.$emit('input', 0);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(updateHistory).not.toHaveBeenCalled();
});
@@ -183,7 +184,7 @@ describe('ProjectsPipelinesChartsApp', () => {
popstateHandler();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findGlTabs().attributes('value')).toBe('1');
});
diff --git a/spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_charts_spec.js b/spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_charts_spec.js
index 9adc6dba51e..cafb3f231bd 100644
--- a/spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_charts_spec.js
+++ b/spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_charts_spec.js
@@ -1,6 +1,6 @@
-import { GlSegmentedControl } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
+import { GlSegmentedControl } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import CiCdAnalyticsAreaChart from '~/vue_shared/components/ci_cd_analytics/ci_cd_analytics_area_chart.vue';
import CiCdAnalyticsCharts from '~/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue';
import { transformedAreaChartData, chartOptions } from '../mock_data';
@@ -29,12 +29,15 @@ const DEFAULT_PROPS = {
describe('~/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue', () => {
let wrapper;
- const createWrapper = (props = {}) =>
- shallowMount(CiCdAnalyticsCharts, {
+ const createWrapper = (props = {}, slots = {}) =>
+ shallowMountExtended(CiCdAnalyticsCharts, {
propsData: {
...DEFAULT_PROPS,
...props,
},
+ scopedSlots: {
+ ...slots,
+ },
});
afterEach(() => {
@@ -44,20 +47,20 @@ describe('~/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue', (
}
});
- describe('segmented control', () => {
- let segmentedControl;
+ const findMetricsSlot = () => wrapper.findByTestId('metrics-slot');
+ const findSegmentedControl = () => wrapper.findComponent(GlSegmentedControl);
+ describe('segmented control', () => {
beforeEach(() => {
wrapper = createWrapper();
- segmentedControl = wrapper.find(GlSegmentedControl);
});
it('should default to the first chart', () => {
- expect(segmentedControl.props('checked')).toBe(0);
+ expect(findSegmentedControl().props('checked')).toBe(0);
});
it('should use the title and index as values', () => {
- const options = segmentedControl.props('options');
+ const options = findSegmentedControl().props('options');
expect(options).toHaveLength(3);
expect(options).toEqual([
{
@@ -76,7 +79,7 @@ describe('~/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue', (
});
it('should select a different chart on change', async () => {
- segmentedControl.vm.$emit('input', 1);
+ findSegmentedControl().vm.$emit('input', 1);
const chart = wrapper.find(CiCdAnalyticsAreaChart);
@@ -91,4 +94,24 @@ describe('~/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue', (
wrapper = createWrapper({ charts: [] });
expect(wrapper.find(CiCdAnalyticsAreaChart).exists()).toBe(false);
});
+
+ describe('slots', () => {
+ beforeEach(() => {
+ wrapper = createWrapper(
+ {},
+ {
+ metrics: '<div data-testid="metrics-slot">selected chart: {{props.selectedChart}}</div>',
+ },
+ );
+ });
+
+ it('renders a metrics slot', async () => {
+ const selectedChart = 1;
+ findSegmentedControl().vm.$emit('input', selectedChart);
+
+ await nextTick();
+
+ expect(findMetricsSlot().text()).toBe(`selected chart: ${selectedChart}`);
+ });
+ });
});
diff --git a/spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js b/spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js
index 6ef49390c47..3c91b913e67 100644
--- a/spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js
+++ b/spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js
@@ -1,7 +1,9 @@
import { GlColumnChart } from '@gitlab/ui/dist/charts';
-import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import PipelineCharts from '~/projects/pipelines/charts/components/pipeline_charts.vue';
import StatisticsList from '~/projects/pipelines/charts/components/statistics_list.vue';
import getPipelineCountByStatus from '~/projects/pipelines/charts/graphql/queries/get_pipeline_count_by_status.query.graphql';
@@ -10,8 +12,7 @@ import CiCdAnalyticsCharts from '~/vue_shared/components/ci_cd_analytics/ci_cd_a
import { mockPipelineCount, mockPipelineStatistics } from '../mock_data';
const projectPath = 'gitlab-org/gitlab';
-const localVue = createLocalVue();
-localVue.use(VueApollo);
+Vue.use(VueApollo);
describe('~/projects/pipelines/charts/components/pipeline_charts.vue', () => {
let wrapper;
@@ -25,14 +26,15 @@ describe('~/projects/pipelines/charts/components/pipeline_charts.vue', () => {
return createMockApollo(requestHandlers);
}
- beforeEach(() => {
+ beforeEach(async () => {
wrapper = shallowMount(PipelineCharts, {
provide: {
projectPath,
},
- localVue,
apolloProvider: createMockApolloProvider(),
});
+
+ await waitForPromises();
});
afterEach(() => {
diff --git a/spec/frontend/projects/project_find_file_spec.js b/spec/frontend/projects/project_find_file_spec.js
index 9c1000039b1..eec54dd04bc 100644
--- a/spec/frontend/projects/project_find_file_spec.js
+++ b/spec/frontend/projects/project_find_file_spec.js
@@ -1,6 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import { TEST_HOST } from 'helpers/test_constants';
+import waitForPromises from 'helpers/wait_for_promises';
import { sanitize } from '~/lib/dompurify';
import axios from '~/lib/utils/axios_utils';
import ProjectFindFile from '~/projects/project_find_file';
@@ -53,7 +54,7 @@ describe('ProjectFindFile', () => {
{ path: 'folde?rC/fil#F.txt', escaped: 'folde%3FrC/fil%23F.txt' },
];
- beforeEach((done) => {
+ beforeEach(() => {
// Create a mock adapter for stubbing axios API requests
mock = new MockAdapter(axios);
@@ -64,7 +65,7 @@ describe('ProjectFindFile', () => {
);
getProjectFindFileInstance(); // This triggers a load / axios call + subsequent render in the constructor
- setImmediate(done);
+ return waitForPromises();
});
afterEach(() => {
diff --git a/spec/frontend/projects/settings/components/shared_runners_toggle_spec.js b/spec/frontend/projects/settings/components/shared_runners_toggle_spec.js
index 0c5bbe2a115..0a05832ceb6 100644
--- a/spec/frontend/projects/settings/components/shared_runners_toggle_spec.js
+++ b/spec/frontend/projects/settings/components/shared_runners_toggle_spec.js
@@ -1,6 +1,7 @@
import { GlAlert, GlToggle, GlTooltip } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import MockAxiosAdapter from 'axios-mock-adapter';
+import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import SharedRunnersToggleComponent from '~/projects/settings/components/shared_runners_toggle.vue';
@@ -121,7 +122,7 @@ describe('projects/settings/components/shared_runners', () => {
expect(isToggleLoading()).toBe(false);
findSharedRunnersToggle().vm.$emit('change', true);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(isToggleLoading()).toBe(true);
await waitForPromises();
diff --git a/spec/frontend/projects/settings/components/transfer_project_form_spec.js b/spec/frontend/projects/settings/components/transfer_project_form_spec.js
index f7ce7c6f840..85b09ced024 100644
--- a/spec/frontend/projects/settings/components/transfer_project_form_spec.js
+++ b/spec/frontend/projects/settings/components/transfer_project_form_spec.js
@@ -1,4 +1,7 @@
-import { namespaces } from 'jest/vue_shared/components/namespace_select/mock_data';
+import {
+ groupNamespaces,
+ userNamespaces,
+} from 'jest/vue_shared/components/namespace_select/mock_data';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import TransferProjectForm from '~/projects/settings/components/transfer_project_form.vue';
import NamespaceSelect from '~/vue_shared/components/namespace_select/namespace_select.vue';
@@ -13,7 +16,8 @@ describe('Transfer project form', () => {
const createComponent = () =>
shallowMountExtended(TransferProjectForm, {
propsData: {
- namespaces,
+ userNamespaces,
+ groupNamespaces,
confirmButtonText,
confirmationPhrase,
},
@@ -43,7 +47,7 @@ describe('Transfer project form', () => {
});
describe('with a selected namespace', () => {
- const [selectedItem] = namespaces.group;
+ const [selectedItem] = groupNamespaces;
beforeEach(() => {
findNamespaceSelect().vm.$emit('select', selectedItem);
diff --git a/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js b/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js
index 875c58583df..57e515723e5 100644
--- a/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js
+++ b/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js
@@ -139,7 +139,7 @@ describe('ServiceDeskSetting', () => {
input.setValue('abc_A.');
input.trigger('blur');
- await wrapper.vm.$nextTick();
+ await nextTick();
const errorText = wrapper.find('.invalid-feedback');
expect(errorText.exists()).toBe(true);
diff --git a/spec/frontend/prometheus_alerts/components/reset_key_spec.js b/spec/frontend/prometheus_alerts/components/reset_key_spec.js
index edf5297cc6a..dc5fdb1dffc 100644
--- a/spec/frontend/prometheus_alerts/components/reset_key_spec.js
+++ b/spec/frontend/prometheus_alerts/components/reset_key_spec.js
@@ -1,6 +1,7 @@
import { GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import ResetKey from '~/prometheus_alerts/components/reset_key.vue';
@@ -45,37 +46,31 @@ describe('ResetKey', () => {
expect(vm.find('.js-reset-auth-key').text()).toEqual('Reset key');
});
- it('reset updates key', () => {
+ it('reset updates key', async () => {
mock.onPost(propsData.changeKeyUrl).replyOnce(200, { token: 'newToken' });
vm.find(GlModal).vm.$emit('ok');
- return vm.vm
- .$nextTick()
- .then(waitForPromises)
- .then(() => {
- expect(vm.vm.authorizationKey).toEqual('newToken');
- expect(vm.find('#authorization-key').attributes('value')).toEqual('newToken');
- });
+ await nextTick();
+ await waitForPromises();
+ expect(vm.vm.authorizationKey).toEqual('newToken');
+ expect(vm.find('#authorization-key').attributes('value')).toEqual('newToken');
});
- it('reset key failure shows error', () => {
+ it('reset key failure shows error', async () => {
mock.onPost(propsData.changeKeyUrl).replyOnce(500);
vm.find(GlModal).vm.$emit('ok');
- return vm.vm
- .$nextTick()
- .then(waitForPromises)
- .then(() => {
- expect(vm.find('#authorization-key').attributes('value')).toEqual(
- propsData.initialAuthorizationKey,
- );
-
- expect(document.querySelector('.flash-container').innerText.trim()).toEqual(
- 'Failed to reset key. Please try again.',
- );
- });
+ await nextTick();
+ await waitForPromises();
+ expect(vm.find('#authorization-key').attributes('value')).toEqual(
+ propsData.initialAuthorizationKey,
+ );
+
+ expect(document.querySelector('.flash-container').innerText.trim()).toEqual(
+ 'Failed to reset key. Please try again.',
+ );
});
});
@@ -92,14 +87,13 @@ describe('ResetKey', () => {
expect(vm.find('#authorization-key').attributes('value')).toEqual('');
});
- it('Generate key button triggers key change', () => {
+ it('Generate key button triggers key change', async () => {
mock.onPost(propsData.changeKeyUrl).replyOnce(200, { token: 'newToken' });
vm.find('.js-reset-auth-key').vm.$emit('click');
- return waitForPromises().then(() => {
- expect(vm.find('#authorization-key').attributes('value')).toEqual('newToken');
- });
+ await waitForPromises();
+ expect(vm.find('#authorization-key').attributes('value')).toEqual('newToken');
});
});
});
diff --git a/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js b/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js
index a703dc0a66f..ee74e28ba23 100644
--- a/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js
+++ b/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js
@@ -1,4 +1,5 @@
import MockAdapter from 'axios-mock-adapter';
+import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import PANEL_STATE from '~/prometheus_metrics/constants';
import PrometheusMetrics from '~/prometheus_metrics/prometheus_metrics';
@@ -132,7 +133,7 @@ describe('PrometheusMetrics', () => {
mock.restore();
});
- it('should show loader animation while response is being loaded and hide it when request is complete', (done) => {
+ it('should show loader animation while response is being loaded and hide it when request is complete', async () => {
mockSuccess();
prometheusMetrics.loadActiveMetrics();
@@ -140,34 +141,31 @@ describe('PrometheusMetrics', () => {
expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeFalsy();
expect(axios.get).toHaveBeenCalledWith(prometheusMetrics.activeMetricsEndpoint);
- setImmediate(() => {
- expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy();
- done();
- });
+ await waitForPromises();
+
+ expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy();
});
- it('should show empty state if response failed to load', (done) => {
+ it('should show empty state if response failed to load', async () => {
mockError();
prometheusMetrics.loadActiveMetrics();
- setImmediate(() => {
- expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy();
- expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBeFalsy();
- done();
- });
+ await waitForPromises();
+
+ expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy();
+ expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBeFalsy();
});
- it('should populate metrics list once response is loaded', (done) => {
+ it('should populate metrics list once response is loaded', async () => {
jest.spyOn(prometheusMetrics, 'populateActiveMetrics').mockImplementation();
mockSuccess();
prometheusMetrics.loadActiveMetrics();
- setImmediate(() => {
- expect(prometheusMetrics.populateActiveMetrics).toHaveBeenCalledWith(metrics);
- done();
- });
+ await waitForPromises();
+
+ expect(prometheusMetrics.populateActiveMetrics).toHaveBeenCalledWith(metrics);
});
});
});
diff --git a/spec/frontend/ref/components/ref_selector_spec.js b/spec/frontend/ref/components/ref_selector_spec.js
index b486992ac4b..e1fc60f0d92 100644
--- a/spec/frontend/ref/components/ref_selector_spec.js
+++ b/spec/frontend/ref/components/ref_selector_spec.js
@@ -1,5 +1,6 @@
import { GlLoadingIcon, GlSearchBoxByType, GlDropdownItem, GlDropdown, GlIcon } from '@gitlab/ui';
-import { mount, createLocalVue } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { merge, last } from 'lodash';
@@ -20,8 +21,7 @@ import {
} from '~/ref/constants';
import createStore from '~/ref/stores/';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('Ref selector component', () => {
const fixtures = { branches, tags, commit };
@@ -52,7 +52,6 @@ describe('Ref selector component', () => {
stubs: {
GlSearchBoxByType: true,
},
- localVue,
store: createStore(),
},
mountOverrides,
@@ -138,19 +137,19 @@ describe('Ref selector component', () => {
findSearchBox().vm.$emit('input', newQuery);
};
- const selectFirstBranch = () => {
+ const selectFirstBranch = async () => {
findFirstBranchDropdownItem().vm.$emit('click');
- return wrapper.vm.$nextTick();
+ await nextTick();
};
- const selectFirstTag = () => {
+ const selectFirstTag = async () => {
findFirstTagDropdownItem().vm.$emit('click');
- return wrapper.vm.$nextTick();
+ await nextTick();
};
- const selectFirstCommit = () => {
+ const selectFirstCommit = async () => {
findFirstCommitDropdownItem().vm.$emit('click');
- return wrapper.vm.$nextTick();
+ await nextTick();
};
const waitForRequests = ({ andClearMocks } = { andClearMocks: false }) =>
@@ -220,12 +219,11 @@ describe('Ref selector component', () => {
return waitForRequests();
});
- it('renders the updated ref name', () => {
+ it('renders the updated ref name', async () => {
wrapper.setProps({ value: updatedRef });
- return localVue.nextTick().then(() => {
- expect(findButtonContent().text()).toBe(updatedRef);
- });
+ await nextTick();
+ expect(findButtonContent().text()).toBe(updatedRef);
});
});
@@ -547,9 +545,8 @@ describe('Ref selector component', () => {
await selectFirstBranch();
- return localVue.nextTick().then(() => {
- expect(findButtonContent().text()).toBe(fixtures.branches[0].name);
- });
+ await nextTick();
+ expect(findButtonContent().text()).toBe(fixtures.branches[0].name);
});
it("updates the v-model binding with the branch's name", async () => {
@@ -567,9 +564,8 @@ describe('Ref selector component', () => {
await selectFirstTag();
- return localVue.nextTick().then(() => {
- expect(findButtonContent().text()).toBe(fixtures.tags[0].name);
- });
+ await nextTick();
+ expect(findButtonContent().text()).toBe(fixtures.tags[0].name);
});
it("updates the v-model binding with the tag's name", async () => {
@@ -587,9 +583,8 @@ describe('Ref selector component', () => {
await selectFirstCommit();
- return localVue.nextTick().then(() => {
- expect(findButtonContent().text()).toBe(fixtures.commit.id);
- });
+ await nextTick();
+ expect(findButtonContent().text()).toBe(fixtures.commit.id);
});
it("updates the v-model binding with the commit's full SHA", async () => {
diff --git a/spec/frontend/related_issues/components/related_issuable_input_spec.js b/spec/frontend/related_issues/components/related_issuable_input_spec.js
index 79b228454f4..7d11e3cffb0 100644
--- a/spec/frontend/related_issues/components/related_issuable_input_spec.js
+++ b/spec/frontend/related_issues/components/related_issuable_input_spec.js
@@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import { TEST_HOST } from 'helpers/test_constants';
import RelatedIssuableInput from '~/related_issues/components/related_issuable_input.vue';
import { issuableTypesMap, PathIdSeparator } from '~/related_issues/constants';
@@ -82,7 +83,7 @@ describe('RelatedIssuableInput', () => {
wrapper.find('li').trigger('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(document.activeElement).toBe(wrapper.find({ ref: 'input' }).element);
});
diff --git a/spec/frontend/releases/components/app_edit_new_spec.js b/spec/frontend/releases/components/app_edit_new_spec.js
index 029d720f7b9..0a0a683b56d 100644
--- a/spec/frontend/releases/components/app_edit_new_spec.js
+++ b/spec/frontend/releases/components/app_edit_new_spec.js
@@ -3,6 +3,7 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { merge } from 'lodash';
import Vuex from 'vuex';
+import { nextTick } from 'vue';
import originalRelease from 'test_fixtures/api/releases/release.json';
import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants';
@@ -71,7 +72,7 @@ describe('Release edit/new component', () => {
},
});
- await wrapper.vm.$nextTick();
+ await nextTick();
wrapper.element.querySelectorAll('input').forEach((input) => jest.spyOn(input, 'focus'));
};
diff --git a/spec/frontend/releases/components/app_index_apollo_client_spec.js b/spec/frontend/releases/components/app_index_apollo_client_spec.js
index 32bbfd386f5..9881ef9bc9f 100644
--- a/spec/frontend/releases/components/app_index_apollo_client_spec.js
+++ b/spec/frontend/releases/components/app_index_apollo_client_spec.js
@@ -1,9 +1,10 @@
import { cloneDeep } from 'lodash';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import originalAllReleasesQueryResponse from 'test_fixtures/graphql/releases/graphql/queries/all_releases.query.graphql.json';
import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import allReleasesQuery from 'shared_queries/releases/all_releases.query.graphql';
import createFlash from '~/flash';
import { historyPushState } from '~/lib/utils/common_utils';
@@ -141,7 +142,8 @@ describe('app_index_apollo_client.vue', () => {
});
});
- it(`${toDescription(loadingIndicator)} render a loading indicator`, () => {
+ it(`${toDescription(loadingIndicator)} render a loading indicator`, async () => {
+ await waitForPromises();
expect(findLoadingIndicator().exists()).toBe(loadingIndicator);
});
@@ -294,7 +296,7 @@ describe('app_index_apollo_client.vue', () => {
mockQueryParams = { after };
findPagination().vm.$emit('next', after);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(queryMock.mock.calls).toEqual([
[expect.objectContaining({ before })],
@@ -319,7 +321,7 @@ describe('app_index_apollo_client.vue', () => {
it('requeries the GraphQL endpoint and updates the URL when the sort is changed', async () => {
findSort().vm.$emit('input', CREATED_ASC);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(queryMock.mock.calls).toEqual([
[expect.objectContaining({ sort: DEFAULT_SORT })],
@@ -335,7 +337,7 @@ describe('app_index_apollo_client.vue', () => {
it('does not requery the GraphQL endpoint or update the URL if the sort is updated to the same value', async () => {
findSort().vm.$emit('input', DEFAULT_SORT);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(queryMock.mock.calls).toEqual([
[expect.objectContaining({ sort: DEFAULT_SORT })],
@@ -368,7 +370,7 @@ describe('app_index_apollo_client.vue', () => {
findSort().vm.$emit('input', CREATED_ASC);
- await wrapper.vm.$nextTick();
+ await nextTick();
});
it(`resets the page's "${paramName}" pagination cursor when the sort is changed`, () => {
diff --git a/spec/frontend/releases/components/app_show_spec.js b/spec/frontend/releases/components/app_show_spec.js
index a60b9bda66a..41c9746a363 100644
--- a/spec/frontend/releases/components/app_show_spec.js
+++ b/spec/frontend/releases/components/app_show_spec.js
@@ -3,6 +3,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import oneReleaseQueryResponse from 'test_fixtures/graphql/releases/graphql/queries/one_release.query.graphql.json';
import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import ReleaseShowApp from '~/releases/components/app_show.vue';
import ReleaseBlock from '~/releases/components/release_block.vue';
@@ -111,12 +112,13 @@ describe('Release show component', () => {
});
describe('when the component has successfully loaded the release', () => {
- beforeEach(() => {
+ beforeEach(async () => {
const apolloProvider = createMockApollo([
[oneReleaseQuery, jest.fn().mockResolvedValueOnce(oneReleaseQueryResponse)],
]);
createComponent({ apolloProvider });
+ await waitForPromises();
});
expectNoLoadingIndicator();
@@ -125,12 +127,13 @@ describe('Release show component', () => {
});
describe('when the request succeeded, but the returned "project" key was null', () => {
- beforeEach(() => {
+ beforeEach(async () => {
const apolloProvider = createMockApollo([
[oneReleaseQuery, jest.fn().mockResolvedValueOnce({ data: { project: null } })],
]);
createComponent({ apolloProvider });
+ await waitForPromises();
});
expectNoLoadingIndicator();
@@ -139,7 +142,7 @@ describe('Release show component', () => {
});
describe('when the request succeeded, but the returned "project.release" key was null', () => {
- beforeEach(() => {
+ beforeEach(async () => {
const apolloProvider = createMockApollo([
[
oneReleaseQuery,
@@ -148,6 +151,7 @@ describe('Release show component', () => {
]);
createComponent({ apolloProvider });
+ await waitForPromises();
});
expectNoLoadingIndicator();
@@ -156,12 +160,13 @@ describe('Release show component', () => {
});
describe('when an error occurs while loading the release', () => {
- beforeEach(() => {
+ beforeEach(async () => {
const apolloProvider = createMockApollo([
[oneReleaseQuery, jest.fn().mockRejectedValueOnce('An error occurred!')],
]);
createComponent({ apolloProvider });
+ await waitForPromises();
});
expectNoLoadingIndicator();
diff --git a/spec/frontend/releases/components/asset_links_form_spec.js b/spec/frontend/releases/components/asset_links_form_spec.js
index 839d127e00f..c0f7738bec5 100644
--- a/spec/frontend/releases/components/asset_links_form_spec.js
+++ b/spec/frontend/releases/components/asset_links_form_spec.js
@@ -1,4 +1,5 @@
-import { mount, createLocalVue } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import originalRelease from 'test_fixtures/api/releases/release.json';
import * as commonUtils from '~/lib/utils/common_utils';
@@ -6,8 +7,7 @@ import { ENTER_KEY } from '~/lib/utils/keys';
import AssetLinksForm from '~/releases/components/asset_links_form.vue';
import { ASSET_LINK_TYPE, DEFAULT_ASSET_LINK_TYPE } from '~/releases/constants';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('Release edit component', () => {
let wrapper;
@@ -52,7 +52,6 @@ describe('Release edit component', () => {
});
wrapper = mount(AssetLinksForm, {
- localVue,
store,
});
};
diff --git a/spec/frontend/releases/components/evidence_block_spec.js b/spec/frontend/releases/components/evidence_block_spec.js
index 973428257b7..f0d02884305 100644
--- a/spec/frontend/releases/components/evidence_block_spec.js
+++ b/spec/frontend/releases/components/evidence_block_spec.js
@@ -1,5 +1,6 @@
import { GlLink, GlIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import originalRelease from 'test_fixtures/api/releases/release.json';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { truncateSha } from '~/lib/utils/text_utility';
@@ -51,12 +52,11 @@ describe('Evidence Block', () => {
expect(wrapper.find('.js-short').text()).toBe(truncateSha(release.evidences[0].sha));
});
- it('renders the long sha after expansion', () => {
+ it('renders the long sha after expansion', async () => {
wrapper.find('.js-text-expander-prepend').trigger('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find('.js-expanded').text()).toBe(release.evidences[0].sha);
- });
+ await nextTick();
+ expect(wrapper.find('.js-expanded').text()).toBe(release.evidences[0].sha);
});
});
diff --git a/spec/frontend/releases/components/release_block_footer_spec.js b/spec/frontend/releases/components/release_block_footer_spec.js
index f645dc309d7..b095e9e1d78 100644
--- a/spec/frontend/releases/components/release_block_footer_spec.js
+++ b/spec/frontend/releases/components/release_block_footer_spec.js
@@ -1,6 +1,7 @@
import { GlLink, GlIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { cloneDeep } from 'lodash';
+import { nextTick } from 'vue';
import originalRelease from 'test_fixtures/api/releases/release.json';
import { trimText } from 'helpers/text_helper';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
@@ -14,7 +15,7 @@ describe('Release block footer', () => {
let wrapper;
let release;
- const factory = (props = {}) => {
+ const factory = async (props = {}) => {
wrapper = mount(ReleaseBlockFooter, {
propsData: {
...convertObjectPropsToCamelCase(release, { deep: true }),
@@ -22,7 +23,7 @@ describe('Release block footer', () => {
},
});
- return wrapper.vm.$nextTick();
+ await nextTick();
};
beforeEach(() => {
diff --git a/spec/frontend/releases/components/release_block_milestone_info_spec.js b/spec/frontend/releases/components/release_block_milestone_info_spec.js
index 146b2cc7490..84a0080965b 100644
--- a/spec/frontend/releases/components/release_block_milestone_info_spec.js
+++ b/spec/frontend/releases/components/release_block_milestone_info_spec.js
@@ -1,5 +1,6 @@
import { GlProgressBar, GlLink, GlBadge, GlButton } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import originalRelease from 'test_fixtures/api/releases/release.json';
import { trimText } from 'helpers/text_helper';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
@@ -12,12 +13,12 @@ describe('Release block milestone info', () => {
let wrapper;
let milestones;
- const factory = (props) => {
+ const factory = async (props) => {
wrapper = mount(ReleaseBlockMilestoneInfo, {
propsData: props,
});
- return wrapper.vm.$nextTick();
+ await nextTick();
};
beforeEach(() => {
@@ -105,10 +106,10 @@ describe('Release block milestone info', () => {
return factory({ milestones: lotsOfMilestones });
});
- const clickShowMoreFewerButton = () => {
+ const clickShowMoreFewerButton = async () => {
milestoneListContainer().find(GlButton).trigger('click');
- return wrapper.vm.$nextTick();
+ await nextTick();
};
const milestoneListText = () => trimText(milestoneListContainer().text());
@@ -117,19 +118,16 @@ describe('Release block milestone info', () => {
expect(milestoneListText()).toContain(`Milestones ${abbreviatedListString} • show 10 more`);
});
- it('renders all milestones when "show more" is clicked', () =>
- clickShowMoreFewerButton().then(() => {
- expect(milestoneListText()).toContain(`Milestones ${fullListString} • show fewer`);
- }));
+ it('renders all milestones when "show more" is clicked', async () => {
+ await clickShowMoreFewerButton();
+ expect(milestoneListText()).toContain(`Milestones ${fullListString} • show fewer`);
+ });
- it('returns to the original view when "show fewer" is clicked', () =>
- clickShowMoreFewerButton()
- .then(clickShowMoreFewerButton)
- .then(() => {
- expect(milestoneListText()).toContain(
- `Milestones ${abbreviatedListString} • show 10 more`,
- );
- }));
+ it('returns to the original view when "show fewer" is clicked', async () => {
+ await clickShowMoreFewerButton();
+ await clickShowMoreFewerButton();
+ expect(milestoneListText()).toContain(`Milestones ${abbreviatedListString} • show 10 more`);
+ });
});
const expectAllZeros = () => {
diff --git a/spec/frontend/releases/components/release_block_spec.js b/spec/frontend/releases/components/release_block_spec.js
index a847c32b8f1..c4910ae9b2f 100644
--- a/spec/frontend/releases/components/release_block_spec.js
+++ b/spec/frontend/releases/components/release_block_spec.js
@@ -1,5 +1,6 @@
import { mount } from '@vue/test-utils';
import $ from 'jquery';
+import { nextTick } from 'vue';
import originalRelease from 'test_fixtures/api/releases/release.json';
import * as commonUtils from '~/lib/utils/common_utils';
import * as urlUtility from '~/lib/utils/url_utility';
@@ -13,7 +14,7 @@ describe('Release block', () => {
let wrapper;
let release;
- const factory = (releaseProp, featureFlags = {}) => {
+ const factory = async (releaseProp, featureFlags = {}) => {
wrapper = mount(ReleaseBlock, {
propsData: {
release: releaseProp,
@@ -25,7 +26,7 @@ describe('Release block', () => {
},
});
- return wrapper.vm.$nextTick();
+ await nextTick();
};
const milestoneListLabel = () => wrapper.find('.js-milestone-list-label');
diff --git a/spec/frontend/releases/components/releases_pagination_spec.js b/spec/frontend/releases/components/releases_pagination_spec.js
index 2d08f72ad8b..b8c69b0ea70 100644
--- a/spec/frontend/releases/components/releases_pagination_spec.js
+++ b/spec/frontend/releases/components/releases_pagination_spec.js
@@ -1,5 +1,6 @@
import { GlKeysetPagination } from '@gitlab/ui';
-import { mount, createLocalVue } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import { historyPushState } from '~/lib/utils/common_utils';
import ReleasesPagination from '~/releases/components/releases_pagination.vue';
@@ -11,8 +12,7 @@ jest.mock('~/lib/utils/common_utils', () => ({
historyPushState: jest.fn(),
}));
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('~/releases/components/releases_pagination.vue', () => {
let wrapper;
@@ -39,7 +39,6 @@ describe('~/releases/components/releases_pagination.vue', () => {
},
featureFlags: {},
}),
- localVue,
});
};
diff --git a/spec/frontend/releases/components/releases_sort_spec.js b/spec/frontend/releases/components/releases_sort_spec.js
index b16f80b9c73..7774532bc12 100644
--- a/spec/frontend/releases/components/releases_sort_spec.js
+++ b/spec/frontend/releases/components/releases_sort_spec.js
@@ -1,12 +1,12 @@
import { GlSorting, GlSortingItem } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import ReleasesSort from '~/releases/components/releases_sort.vue';
import createStore from '~/releases/stores';
import createIndexModule from '~/releases/stores/modules/index';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('~/releases/components/releases_sort.vue', () => {
let wrapper;
@@ -30,7 +30,6 @@ describe('~/releases/components/releases_sort.vue', () => {
stubs: {
GlSortingItem,
},
- localVue,
});
};
diff --git a/spec/frontend/releases/components/tag_field_exsting_spec.js b/spec/frontend/releases/components/tag_field_exsting_spec.js
index 294538086b4..f45a28392b7 100644
--- a/spec/frontend/releases/components/tag_field_exsting_spec.js
+++ b/spec/frontend/releases/components/tag_field_exsting_spec.js
@@ -1,5 +1,6 @@
import { GlFormInput } from '@gitlab/ui';
-import { shallowMount, mount, createLocalVue } from '@vue/test-utils';
+import { shallowMount, mount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import TagFieldExisting from '~/releases/components/tag_field_existing.vue';
import createStore from '~/releases/stores';
@@ -7,8 +8,7 @@ import createEditNewModule from '~/releases/stores/modules/edit_new';
const TEST_TAG_NAME = 'test-tag-name';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('releases/components/tag_field_existing', () => {
let store;
@@ -17,7 +17,6 @@ describe('releases/components/tag_field_existing', () => {
const createComponent = (mountFn = shallowMount) => {
wrapper = mountFn(TagFieldExisting, {
store,
- localVue,
});
};
diff --git a/spec/frontend/releases/components/tag_field_new_spec.js b/spec/frontend/releases/components/tag_field_new_spec.js
index 0f416e46dba..c13b513f87e 100644
--- a/spec/frontend/releases/components/tag_field_new_spec.js
+++ b/spec/frontend/releases/components/tag_field_new_spec.js
@@ -1,6 +1,6 @@
import { GlDropdownItem } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import { __ } from '~/locale';
import TagFieldNew from '~/releases/components/tag_field_new.vue';
import createStore from '~/releases/stores';
@@ -153,7 +153,7 @@ describe('releases/components/tag_field_new', () => {
* Should be passed either 'shown' or 'hidden'
*/
const expectValidationMessageToBe = async (state) => {
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findTagNameFormGroup().element).toHaveClass(
state === 'shown' ? 'is-invalid' : 'is-valid',
diff --git a/spec/frontend/reports/accessibility_report/components/accessibility_issue_body_spec.js b/spec/frontend/reports/accessibility_report/components/accessibility_issue_body_spec.js
index 794deca42ac..ddabb7194cb 100644
--- a/spec/frontend/reports/accessibility_report/components/accessibility_issue_body_spec.js
+++ b/spec/frontend/reports/accessibility_report/components/accessibility_issue_body_spec.js
@@ -1,3 +1,4 @@
+import { GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import AccessibilityIssueBody from '~/reports/accessibility_report/components/accessibility_issue_body.vue';
@@ -29,7 +30,7 @@ describe('CustomMetricsForm', () => {
});
};
- const findIsNewBadge = () => wrapper.find({ ref: 'accessibility-issue-is-new-badge' });
+ const findIsNewBadge = () => wrapper.findComponent(GlBadge);
beforeEach(() => {
mountComponent(issue);
@@ -37,7 +38,6 @@ describe('CustomMetricsForm', () => {
afterEach(() => {
wrapper.destroy();
- wrapper = null;
});
it('Displays the issue message', () => {
@@ -52,7 +52,7 @@ describe('CustomMetricsForm', () => {
.find({ ref: 'accessibility-issue-learn-more' })
.attributes('href');
- expect(learnMoreUrl).toEqual(issue.learnMoreUrl);
+ expect(learnMoreUrl).toBe(issue.learnMoreUrl);
});
});
@@ -69,7 +69,7 @@ describe('CustomMetricsForm', () => {
.find({ ref: 'accessibility-issue-learn-more' })
.attributes('href');
- expect(learnMoreUrl).toEqual('https://www.w3.org/TR/WCAG20-TECHS/Overview.html');
+ expect(learnMoreUrl).toBe('https://www.w3.org/TR/WCAG20-TECHS/Overview.html');
});
});
@@ -86,7 +86,7 @@ describe('CustomMetricsForm', () => {
.find({ ref: 'accessibility-issue-learn-more' })
.attributes('href');
- expect(learnMoreUrl).toEqual('https://www.w3.org/TR/WCAG20-TECHS/Overview.html');
+ expect(learnMoreUrl).toBe('https://www.w3.org/TR/WCAG20-TECHS/Overview.html');
});
});
@@ -96,7 +96,7 @@ describe('CustomMetricsForm', () => {
});
it('Renders the new badge', () => {
- expect(findIsNewBadge().exists()).toEqual(true);
+ expect(findIsNewBadge().exists()).toBe(true);
});
});
@@ -106,7 +106,7 @@ describe('CustomMetricsForm', () => {
});
it('Does not render the new badge', () => {
- expect(findIsNewBadge().exists()).toEqual(false);
+ expect(findIsNewBadge().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/reports/accessibility_report/grouped_accessibility_reports_app_spec.js b/spec/frontend/reports/accessibility_report/grouped_accessibility_reports_app_spec.js
index b716d54c9fc..34b1cdd92bc 100644
--- a/spec/frontend/reports/accessibility_report/grouped_accessibility_reports_app_spec.js
+++ b/spec/frontend/reports/accessibility_report/grouped_accessibility_reports_app_spec.js
@@ -1,22 +1,20 @@
-import { mount, createLocalVue } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import AccessibilityIssueBody from '~/reports/accessibility_report/components/accessibility_issue_body.vue';
import GroupedAccessibilityReportsApp from '~/reports/accessibility_report/grouped_accessibility_reports_app.vue';
import { getStoreConfig } from '~/reports/accessibility_report/store';
import { mockReport } from './mock_data';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('Grouped accessibility reports app', () => {
- const Component = localVue.extend(GroupedAccessibilityReportsApp);
let wrapper;
let mockStore;
const mountComponent = () => {
- wrapper = mount(Component, {
+ wrapper = mount(GroupedAccessibilityReportsApp, {
store: mockStore,
- localVue,
propsData: {
endpoint: 'endpoint.json',
},
diff --git a/spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js b/spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js
index 685a1c50a46..1f923f41274 100644
--- a/spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js
+++ b/spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js
@@ -1,4 +1,5 @@
-import { mount, createLocalVue } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import CodequalityIssueBody from '~/reports/codequality_report/components/codequality_issue_body.vue';
import GroupedCodequalityReportsApp from '~/reports/codequality_report/grouped_codequality_reports_app.vue';
@@ -6,8 +7,7 @@ import { getStoreConfig } from '~/reports/codequality_report/store';
import { STATUS_NOT_FOUND } from '~/reports/constants';
import { parsedReportIssues } from './mock_data';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('Grouped code quality reports app', () => {
let wrapper;
@@ -22,7 +22,6 @@ describe('Grouped code quality reports app', () => {
const mountComponent = (props = {}) => {
wrapper = mount(GroupedCodequalityReportsApp, {
store: mockStore,
- localVue,
propsData: {
...PATHS,
...props,
diff --git a/spec/frontend/reports/components/report_section_spec.js b/spec/frontend/reports/components/report_section_spec.js
index 39932b62dbb..f9eb6dd05f3 100644
--- a/spec/frontend/reports/components/report_section_spec.js
+++ b/spec/frontend/reports/components/report_section_spec.js
@@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import mountComponent, { mountComponentWithSlots } from 'helpers/vue_mount_component_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import reportSection from '~/reports/components/report_section.vue';
@@ -71,16 +71,12 @@ describe('Report section', () => {
const issues = hasIssues ? 'has issues' : 'has no issues';
const open = alwaysOpen ? 'is always open' : 'is not always open';
- it(`is ${isCollapsible}, if the report ${issues} and ${open}`, (done) => {
+ it(`is ${isCollapsible}, if the report ${issues} and ${open}`, async () => {
vm.hasIssues = hasIssues;
vm.alwaysOpen = alwaysOpen;
- Vue.nextTick()
- .then(() => {
- expect(vm.isCollapsible).toBe(isCollapsible);
- })
- .then(done)
- .catch(done.fail);
+ await nextTick();
+ expect(vm.isCollapsible).toBe(isCollapsible);
});
});
});
@@ -97,16 +93,12 @@ describe('Report section', () => {
const issues = isCollapsed ? 'is collapsed' : 'is not collapsed';
const open = alwaysOpen ? 'is always open' : 'is not always open';
- it(`is ${isExpanded}, if the report ${issues} and ${open}`, (done) => {
+ it(`is ${isExpanded}, if the report ${issues} and ${open}`, async () => {
vm.isCollapsed = isCollapsed;
vm.alwaysOpen = alwaysOpen;
- Vue.nextTick()
- .then(() => {
- expect(vm.isExpanded).toBe(isExpanded);
- })
- .then(done)
- .catch(done.fail);
+ await nextTick();
+ expect(vm.isExpanded).toBe(isExpanded);
});
});
});
@@ -148,79 +140,55 @@ describe('Report section', () => {
describe('toggleCollapsed', () => {
const hiddenCss = { display: 'none' };
- it('toggles issues', (done) => {
+ it('toggles issues', async () => {
vm.$el.querySelector('button').click();
- Vue.nextTick()
- .then(() => {
- expect(vm.$el.querySelector('.js-report-section-container')).not.toHaveCss(hiddenCss);
- expect(vm.$el.querySelector('button').textContent.trim()).toEqual('Collapse');
-
- vm.$el.querySelector('button').click();
- })
- .then(Vue.nextTick)
- .then(() => {
- expect(vm.$el.querySelector('.js-report-section-container')).toHaveCss(hiddenCss);
- expect(vm.$el.querySelector('button').textContent.trim()).toEqual('Expand');
- })
- .then(done)
- .catch(done.fail);
+ await nextTick();
+ expect(vm.$el.querySelector('.js-report-section-container')).not.toHaveCss(hiddenCss);
+ expect(vm.$el.querySelector('button').textContent.trim()).toEqual('Collapse');
+
+ vm.$el.querySelector('button').click();
+
+ await nextTick();
+ expect(vm.$el.querySelector('.js-report-section-container')).toHaveCss(hiddenCss);
+ expect(vm.$el.querySelector('button').textContent.trim()).toEqual('Expand');
});
- it('is always expanded, if always-open is set to true', (done) => {
+ it('is always expanded, if always-open is set to true', async () => {
vm.alwaysOpen = true;
- Vue.nextTick()
- .then(() => {
- expect(vm.$el.querySelector('.js-report-section-container')).not.toHaveCss(hiddenCss);
- expect(vm.$el.querySelector('button')).toBeNull();
- })
- .then(done)
- .catch(done.fail);
+ await nextTick();
+ expect(vm.$el.querySelector('.js-report-section-container')).not.toHaveCss(hiddenCss);
+ expect(vm.$el.querySelector('button')).toBeNull();
});
});
});
describe('snowplow events', () => {
- it('does emit an event on issue toggle if the shouldEmitToggleEvent prop does exist', (done) => {
+ it('does emit an event on issue toggle if the shouldEmitToggleEvent prop does exist', async () => {
createComponent({ hasIssues: true, shouldEmitToggleEvent: true });
expect(wrapper.emitted().toggleEvent).toBeUndefined();
findCollapseButton().trigger('click');
- return wrapper.vm
- .$nextTick()
- .then(() => {
- expect(wrapper.emitted().toggleEvent).toHaveLength(1);
- })
- .then(done)
- .catch(done.fail);
+ await nextTick();
+ expect(wrapper.emitted().toggleEvent).toHaveLength(1);
});
- it('does not emit an event on issue toggle if the shouldEmitToggleEvent prop does not exist', (done) => {
+ it('does not emit an event on issue toggle if the shouldEmitToggleEvent prop does not exist', async () => {
createComponent({ hasIssues: true });
expect(wrapper.emitted().toggleEvent).toBeUndefined();
findCollapseButton().trigger('click');
- return wrapper.vm
- .$nextTick()
- .then(() => {
- expect(wrapper.emitted().toggleEvent).toBeUndefined();
- })
- .then(done)
- .catch(done.fail);
+ await nextTick();
+ expect(wrapper.emitted().toggleEvent).toBeUndefined();
});
- it('does not emit an event if always-open is set to true', (done) => {
+ it('does not emit an event if always-open is set to true', async () => {
createComponent({ alwaysOpen: true, hasIssues: true, shouldEmitToggleEvent: true });
- wrapper.vm
- .$nextTick()
- .then(() => {
- expect(wrapper.emitted().toggleEvent).toBeUndefined();
- })
- .then(done)
- .catch(done.fail);
+ await nextTick();
+ expect(wrapper.emitted().toggleEvent).toBeUndefined();
});
});
diff --git a/spec/frontend/reports/grouped_test_report/components/test_issue_body_spec.js b/spec/frontend/reports/grouped_test_report/components/test_issue_body_spec.js
index 2f6f62ca1d3..8a854a92ad7 100644
--- a/spec/frontend/reports/grouped_test_report/components/test_issue_body_spec.js
+++ b/spec/frontend/reports/grouped_test_report/components/test_issue_body_spec.js
@@ -1,13 +1,13 @@
import { GlBadge, GlButton } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import IssueStatusIcon from '~/reports/components/issue_status_icon.vue';
import TestIssueBody from '~/reports/grouped_test_report/components/test_issue_body.vue';
import { failedIssue, successIssue } from '../../mock_data/mock_data';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('Test issue body', () => {
let wrapper;
@@ -29,7 +29,6 @@ describe('Test issue body', () => {
wrapper = extendedWrapper(
shallowMount(TestIssueBody, {
store,
- localVue,
propsData: {
issue,
},
diff --git a/spec/frontend/reports/grouped_test_report/grouped_test_reports_app_spec.js b/spec/frontend/reports/grouped_test_report/grouped_test_reports_app_spec.js
index c60c1f7b63c..90edb27d1d6 100644
--- a/spec/frontend/reports/grouped_test_report/grouped_test_reports_app_spec.js
+++ b/spec/frontend/reports/grouped_test_report/grouped_test_reports_app_spec.js
@@ -1,4 +1,5 @@
-import { mount, createLocalVue } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import Api from '~/api';
import GroupedTestReportsApp from '~/reports/grouped_test_report/grouped_test_reports_app.vue';
@@ -14,8 +15,7 @@ import resolvedFailures from '../mock_data/resolved_failures.json';
jest.mock('~/api.js');
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('Grouped test reports app', () => {
const endpoint = 'endpoint.json';
@@ -27,7 +27,6 @@ describe('Grouped test reports app', () => {
const mountComponent = ({ props = { pipelinePath } } = {}) => {
wrapper = mount(GroupedTestReportsApp, {
store: mockStore,
- localVue,
propsData: {
endpoint,
headBlobPath,
diff --git a/spec/frontend/repository/components/blob_content_viewer_spec.js b/spec/frontend/repository/components/blob_content_viewer_spec.js
index d3b60ec3768..109e5cef49b 100644
--- a/spec/frontend/repository/components/blob_content_viewer_spec.js
+++ b/spec/frontend/repository/components/blob_content_viewer_spec.js
@@ -1,8 +1,8 @@
import { GlLoadingIcon } from '@gitlab/ui';
-import { mount, shallowMount, createLocalVue } from '@vue/test-utils';
+import { mount, shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
-import { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -12,10 +12,10 @@ import BlobButtonGroup from '~/repository/components/blob_button_group.vue';
import BlobContentViewer from '~/repository/components/blob_content_viewer.vue';
import BlobEdit from '~/repository/components/blob_edit.vue';
import ForkSuggestion from '~/repository/components/fork_suggestion.vue';
-import { loadViewer, viewerProps } from '~/repository/components/blob_viewers';
+import { loadViewer } from '~/repository/components/blob_viewers';
import DownloadViewer from '~/repository/components/blob_viewers/download_viewer.vue';
import EmptyViewer from '~/repository/components/blob_viewers/empty_viewer.vue';
-import SourceViewer from '~/vue_shared/components/source_viewer.vue';
+import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer.vue';
import blobInfoQuery from '~/repository/queries/blob_info.query.graphql';
import { redirectTo } from '~/lib/utils/url_utility';
import { isLoggedIn } from '~/lib/utils/common_utils';
@@ -36,11 +36,10 @@ jest.mock('~/lib/utils/common_utils');
let wrapper;
let mockResolver;
-const localVue = createLocalVue();
const mockAxios = new MockAdapter(axios);
const createComponent = async (mockData = {}, mountFn = shallowMount) => {
- localVue.use(VueApollo);
+ Vue.use(VueApollo);
const {
blob = simpleViewerMock,
@@ -51,6 +50,7 @@ const createComponent = async (mockData = {}, mountFn = shallowMount) => {
createMergeRequestIn = userPermissionsMock.createMergeRequestIn,
isBinary,
inject = {},
+ highlightJs = true,
} = mockData;
const project = {
@@ -75,11 +75,17 @@ const createComponent = async (mockData = {}, mountFn = shallowMount) => {
wrapper = extendedWrapper(
mountFn(BlobContentViewer, {
- localVue,
apolloProvider: fakeApollo,
propsData: propsMock,
mixins: [{ data: () => ({ ref: refMock }) }],
- provide: { ...inject },
+ provide: {
+ targetBranch: 'test',
+ originalBranch: 'default-ref',
+ ...inject,
+ glFeatures: {
+ highlightJs,
+ },
+ },
}),
);
@@ -100,7 +106,6 @@ describe('Blob content viewer component', () => {
const findForkSuggestion = () => wrapper.findComponent(ForkSuggestion);
beforeEach(() => {
- gon.features = { highlightJs: true };
isLoggedIn.mockReturnValue(true);
});
@@ -138,6 +143,15 @@ describe('Blob content viewer component', () => {
});
describe('legacy viewers', () => {
+ it('loads a legacy viewer when a the fileType is text and the highlightJs feature is turned off', async () => {
+ await createComponent({
+ blob: { ...simpleViewerMock, fileType: 'text', highlightJs: false },
+ });
+
+ expect(mockAxios.history.get).toHaveLength(1);
+ expect(mockAxios.history.get[0].url).toEqual('some_file.js?format=json&viewer=simple');
+ });
+
it('loads a legacy viewer when a viewer component is not available', async () => {
await createComponent({ blob: { ...simpleViewerMock, fileType: 'unknown' } });
@@ -203,44 +217,39 @@ describe('Blob content viewer component', () => {
describe('Blob viewer', () => {
afterEach(() => {
loadViewer.mockRestore();
- viewerProps.mockRestore();
});
it('does not render a BlobContent component if a Blob viewer is available', async () => {
loadViewer.mockReturnValue(() => true);
await createComponent({ blob: richViewerMock });
-
+ await waitForPromises();
expect(findBlobContent().exists()).toBe(false);
});
it.each`
- viewer | loadViewerReturnValue | viewerPropsReturnValue
- ${'empty'} | ${EmptyViewer} | ${{}}
- ${'download'} | ${DownloadViewer} | ${{ filePath: '/some/file/path', fileName: 'test.js', fileSize: 100 }}
- ${'text'} | ${SourceViewer} | ${{ content: 'test', autoDetect: true }}
- `(
- 'renders viewer component for $viewer files',
- async ({ viewer, loadViewerReturnValue, viewerPropsReturnValue }) => {
- loadViewer.mockReturnValue(loadViewerReturnValue);
- viewerProps.mockReturnValue(viewerPropsReturnValue);
-
- createComponent({
- blob: {
- ...simpleViewerMock,
- fileType: 'null',
- simpleViewer: {
- ...simpleViewerMock.simpleViewer,
- fileType: viewer,
- },
+ viewer | loadViewerReturnValue
+ ${'empty'} | ${EmptyViewer}
+ ${'download'} | ${DownloadViewer}
+ ${'text'} | ${SourceViewer}
+ `('renders viewer component for $viewer files', async ({ viewer, loadViewerReturnValue }) => {
+ loadViewer.mockReturnValue(loadViewerReturnValue);
+
+ createComponent({
+ blob: {
+ ...simpleViewerMock,
+ fileType: 'null',
+ simpleViewer: {
+ ...simpleViewerMock.simpleViewer,
+ fileType: viewer,
},
- });
+ },
+ });
- await nextTick();
+ await waitForPromises();
- expect(loadViewer).toHaveBeenCalledWith(viewer);
- expect(wrapper.findComponent(loadViewerReturnValue).exists()).toBe(true);
- },
- );
+ expect(loadViewer).toHaveBeenCalledWith(viewer, false);
+ expect(wrapper.findComponent(loadViewerReturnValue).exists()).toBe(true);
+ });
});
describe('BlobHeader action slot', () => {
@@ -354,6 +363,19 @@ describe('Blob content viewer component', () => {
});
describe('blob info query', () => {
+ it.each`
+ highlightJs | shouldFetchRawText
+ ${true} | ${true}
+ ${false} | ${false}
+ `(
+ 'calls blob info query with shouldFetchRawText: $shouldFetchRawText when highlightJs (feature flag): $highlightJs',
+ async ({ highlightJs, shouldFetchRawText }) => {
+ await createComponent({ highlightJs });
+
+ expect(mockResolver).toHaveBeenCalledWith(expect.objectContaining({ shouldFetchRawText }));
+ },
+ );
+
it('is called with originalBranch value if the prop has a value', async () => {
await createComponent({ inject: { originalBranch: 'some-branch' } });
diff --git a/spec/frontend/repository/components/blob_controls_spec.js b/spec/frontend/repository/components/blob_controls_spec.js
index 03e389ea5cb..6da1861ea7c 100644
--- a/spec/frontend/repository/components/blob_controls_spec.js
+++ b/spec/frontend/repository/components/blob_controls_spec.js
@@ -1,6 +1,6 @@
-import { createLocalVue } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
-import { nextTick } from 'vue';
+
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import BlobControls from '~/repository/components/blob_controls.vue';
@@ -16,10 +16,8 @@ let router;
let wrapper;
let mockResolver;
-const localVue = createLocalVue();
-
const createComponent = async () => {
- localVue.use(VueApollo);
+ Vue.use(VueApollo);
const project = { ...blobControlsDataMock };
const projectPath = 'some/project';
@@ -31,7 +29,6 @@ const createComponent = async () => {
mockResolver = jest.fn().mockResolvedValue({ data: { project } });
wrapper = shallowMountExtended(BlobControls, {
- localVue,
router,
apolloProvider: createMockApollo([[blobControlsQuery, mockResolver]]),
propsData: { projectPath },
diff --git a/spec/frontend/repository/components/blob_viewers/download_viewer_spec.js b/spec/frontend/repository/components/blob_viewers/download_viewer_spec.js
index c71b2b3c55c..5fe25ced302 100644
--- a/spec/frontend/repository/components/blob_viewers/download_viewer_spec.js
+++ b/spec/frontend/repository/components/blob_viewers/download_viewer_spec.js
@@ -6,42 +6,33 @@ import DownloadViewer from '~/repository/components/blob_viewers/download_viewer
describe('Text Viewer', () => {
let wrapper;
- const DEFAULT_PROPS = {
- fileName: 'file_name.js',
- filePath: '/some/file/path',
- fileSize: 2269674,
+ const DEFAULT_BLOB_DATA = {
+ name: 'file_name.js',
+ rawPath: '/some/file/path',
+ rawSize: 2269674,
};
- const createComponent = (props = {}) => {
+ const createComponent = (blobData = {}) => {
wrapper = shallowMount(DownloadViewer, {
propsData: {
- ...DEFAULT_PROPS,
- ...props,
+ blob: {
+ ...DEFAULT_BLOB_DATA,
+ ...blobData,
+ },
},
});
};
- it('renders component', () => {
- createComponent();
-
- const { fileName, filePath, fileSize } = DEFAULT_PROPS;
- expect(wrapper.props()).toMatchObject({
- fileName,
- filePath,
- fileSize,
- });
- });
-
it('renders download human readable file size text', () => {
createComponent();
- const downloadText = `Download (${numberToHumanSize(DEFAULT_PROPS.fileSize)})`;
+ const downloadText = `Download (${numberToHumanSize(DEFAULT_BLOB_DATA.rawSize)})`;
expect(wrapper.text()).toBe(downloadText);
});
it('renders download text', () => {
createComponent({
- fileSize: 0,
+ rawSize: 0,
});
expect(wrapper.text()).toBe('Download');
@@ -49,13 +40,13 @@ describe('Text Viewer', () => {
it('renders download link', () => {
createComponent();
- const { filePath, fileName } = DEFAULT_PROPS;
+ const { rawPath, name } = DEFAULT_BLOB_DATA;
expect(wrapper.findComponent(GlLink).attributes()).toMatchObject({
rel: 'nofollow',
target: '_blank',
- href: filePath,
- download: fileName,
+ href: rawPath,
+ download: name,
});
});
diff --git a/spec/frontend/repository/components/blob_viewers/image_viewer_spec.js b/spec/frontend/repository/components/blob_viewers/image_viewer_spec.js
index 6735dddf51e..c23de0efdfd 100644
--- a/spec/frontend/repository/components/blob_viewers/image_viewer_spec.js
+++ b/spec/frontend/repository/components/blob_viewers/image_viewer_spec.js
@@ -4,13 +4,13 @@ import ImageViewer from '~/repository/components/blob_viewers/image_viewer.vue';
describe('Image Viewer', () => {
let wrapper;
- const propsData = {
- url: 'some/image.png',
- alt: 'image.png',
+ const DEFAULT_BLOB_DATA = {
+ rawPath: 'some/image.png',
+ name: 'image.png',
};
const createComponent = () => {
- wrapper = shallowMount(ImageViewer, { propsData });
+ wrapper = shallowMount(ImageViewer, { propsData: { blob: DEFAULT_BLOB_DATA } });
};
const findImage = () => wrapper.find('[data-testid="image"]');
@@ -19,7 +19,7 @@ describe('Image Viewer', () => {
createComponent();
expect(findImage().exists()).toBe(true);
- expect(findImage().attributes('src')).toBe(propsData.url);
- expect(findImage().attributes('alt')).toBe(propsData.alt);
+ expect(findImage().attributes('src')).toBe(DEFAULT_BLOB_DATA.rawPath);
+ expect(findImage().attributes('alt')).toBe(DEFAULT_BLOB_DATA.name);
});
});
diff --git a/spec/frontend/repository/components/blob_viewers/lfs_viewer_spec.js b/spec/frontend/repository/components/blob_viewers/lfs_viewer_spec.js
new file mode 100644
index 00000000000..5caeb85834d
--- /dev/null
+++ b/spec/frontend/repository/components/blob_viewers/lfs_viewer_spec.js
@@ -0,0 +1,41 @@
+import { GlLink, GlSprintf } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import LfsViewer from '~/repository/components/blob_viewers/lfs_viewer.vue';
+
+describe('LFS Viewer', () => {
+ let wrapper;
+
+ const DEFAULT_BLOB_DATA = {
+ name: 'file_name.js',
+ rawPath: '/some/file/path',
+ };
+
+ const createComponent = () => {
+ wrapper = shallowMount(LfsViewer, {
+ propsData: { blob: { ...DEFAULT_BLOB_DATA } },
+ stubs: { GlSprintf },
+ });
+ };
+
+ const findLink = () => wrapper.findComponent(GlLink);
+
+ beforeEach(() => createComponent());
+
+ afterEach(() => wrapper.destroy());
+
+ it('renders the correct text', () => {
+ expect(wrapper.text()).toBe(
+ 'This content could not be displayed because it is stored in LFS. You can download it instead.',
+ );
+ });
+
+ it('renders download link', () => {
+ const { rawPath, name } = DEFAULT_BLOB_DATA;
+
+ expect(findLink().attributes()).toMatchObject({
+ target: '_blank',
+ href: rawPath,
+ download: name,
+ });
+ });
+});
diff --git a/spec/frontend/repository/components/blob_viewers/pdf_viewer_spec.js b/spec/frontend/repository/components/blob_viewers/pdf_viewer_spec.js
index fd910002529..10eea691335 100644
--- a/spec/frontend/repository/components/blob_viewers/pdf_viewer_spec.js
+++ b/spec/frontend/repository/components/blob_viewers/pdf_viewer_spec.js
@@ -6,10 +6,12 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
describe('PDF Viewer', () => {
let wrapper;
- const defaultPropsData = { url: 'some/pdf_blob.pdf' };
+ const DEFAULT_BLOB_DATA = { rawPath: 'some/pdf_blob.pdf' };
- const createComponent = (fileSize = 999) => {
- wrapper = shallowMountExtended(Component, { propsData: { ...defaultPropsData, fileSize } });
+ const createComponent = (rawSize = 999) => {
+ wrapper = shallowMountExtended(Component, {
+ propsData: { blob: { ...DEFAULT_BLOB_DATA, rawSize } },
+ });
};
const findPDFViewer = () => wrapper.findComponent(PdfViewer);
@@ -20,7 +22,7 @@ describe('PDF Viewer', () => {
createComponent();
expect(findPDFViewer().exists()).toBe(true);
- expect(findPDFViewer().props('pdf')).toBe(defaultPropsData.url);
+ expect(findPDFViewer().props('pdf')).toBe(DEFAULT_BLOB_DATA.rawPath);
});
describe('Too large', () => {
diff --git a/spec/frontend/repository/components/blob_viewers/video_viewer_spec.js b/spec/frontend/repository/components/blob_viewers/video_viewer_spec.js
index 34448c03b31..2e79a1496ce 100644
--- a/spec/frontend/repository/components/blob_viewers/video_viewer_spec.js
+++ b/spec/frontend/repository/components/blob_viewers/video_viewer_spec.js
@@ -4,10 +4,10 @@ import VideoViewer from '~/repository/components/blob_viewers/video_viewer.vue';
describe('Video Viewer', () => {
let wrapper;
- const propsData = { url: 'some/video.mp4' };
+ const DEFAULT_BLOB_DATA = { rawPath: 'some/video.mp4' };
const createComponent = () => {
- wrapper = shallowMountExtended(VideoViewer, { propsData });
+ wrapper = shallowMountExtended(VideoViewer, { propsData: { blob: { ...DEFAULT_BLOB_DATA } } });
};
const findVideo = () => wrapper.findByTestId('video');
@@ -16,7 +16,7 @@ describe('Video Viewer', () => {
createComponent();
expect(findVideo().exists()).toBe(true);
- expect(findVideo().attributes('src')).toBe(propsData.url);
+ expect(findVideo().attributes('src')).toBe(DEFAULT_BLOB_DATA.rawPath);
expect(findVideo().attributes('controls')).not.toBeUndefined();
});
});
diff --git a/spec/frontend/repository/components/breadcrumbs_spec.js b/spec/frontend/repository/components/breadcrumbs_spec.js
index ad2cbd70187..0e300291d05 100644
--- a/spec/frontend/repository/components/breadcrumbs_spec.js
+++ b/spec/frontend/repository/components/breadcrumbs_spec.js
@@ -1,5 +1,6 @@
import { GlDropdown } from '@gitlab/ui';
import { shallowMount, RouterLinkStub } from '@vue/test-utils';
+import { nextTick } from 'vue';
import Breadcrumbs from '~/repository/components/breadcrumbs.vue';
import UploadBlobModal from '~/repository/components/upload_blob_modal.vue';
import NewDirectoryModal from '~/repository/components/new_directory_modal.vue';
@@ -79,7 +80,7 @@ describe('Repository breadcrumbs component', () => {
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ userPermissions: { forkProject: false, createMergeRequestIn: false } });
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.find(GlDropdown).exists()).toBe(false);
});
@@ -106,7 +107,7 @@ describe('Repository breadcrumbs component', () => {
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ userPermissions: { forkProject: true, createMergeRequestIn: true } });
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.find(GlDropdown).exists()).toBe(true);
});
@@ -125,7 +126,7 @@ describe('Repository breadcrumbs component', () => {
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ $apollo: { queries: { userPermissions: { loading: false } } } });
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findUploadBlobModal().exists()).toBe(true);
});
@@ -149,7 +150,7 @@ describe('Repository breadcrumbs component', () => {
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ $apollo: { queries: { userPermissions: { loading: false } } } });
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findNewDirectoryModal().exists()).toBe(true);
});
diff --git a/spec/frontend/repository/components/last_commit_spec.js b/spec/frontend/repository/components/last_commit_spec.js
index fe05a981845..bb710c3a96c 100644
--- a/spec/frontend/repository/components/last_commit_spec.js
+++ b/spec/frontend/repository/components/last_commit_spec.js
@@ -1,5 +1,6 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import LastCommit from '~/repository/components/last_commit.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
@@ -63,7 +64,7 @@ describe('Repository last commit component', () => {
`('$label when loading icon $loading is true', async ({ loading }) => {
factory(createCommitData(), loading);
- await vm.vm.$nextTick();
+ await nextTick();
expect(vm.find(GlLoadingIcon).exists()).toBe(loading);
});
@@ -71,7 +72,7 @@ describe('Repository last commit component', () => {
it('renders commit widget', async () => {
factory();
- await vm.vm.$nextTick();
+ await nextTick();
expect(vm.element).toMatchSnapshot();
});
@@ -79,7 +80,7 @@ describe('Repository last commit component', () => {
it('renders short commit ID', async () => {
factory();
- await vm.vm.$nextTick();
+ await nextTick();
expect(vm.find('[data-testid="last-commit-id-label"]').text()).toEqual('12345678');
});
@@ -87,7 +88,7 @@ describe('Repository last commit component', () => {
it('hides pipeline components when pipeline does not exist', async () => {
factory(createCommitData({ pipeline: null }));
- await vm.vm.$nextTick();
+ await nextTick();
expect(vm.find('.js-commit-pipeline').exists()).toBe(false);
});
@@ -95,7 +96,7 @@ describe('Repository last commit component', () => {
it('renders pipeline components', async () => {
factory();
- await vm.vm.$nextTick();
+ await nextTick();
expect(vm.find('.js-commit-pipeline').exists()).toBe(true);
});
@@ -103,7 +104,7 @@ describe('Repository last commit component', () => {
it('hides author component when author does not exist', async () => {
factory(createCommitData({ author: null }));
- await vm.vm.$nextTick();
+ await nextTick();
expect(vm.find('.js-user-link').exists()).toBe(false);
expect(vm.find(UserAvatarLink).exists()).toBe(false);
@@ -112,7 +113,7 @@ describe('Repository last commit component', () => {
it('does not render description expander when description is null', async () => {
factory(createCommitData({ descriptionHtml: null }));
- await vm.vm.$nextTick();
+ await nextTick();
expect(vm.find('.text-expander').exists()).toBe(false);
expect(vm.find('.commit-row-description').exists()).toBe(false);
@@ -121,11 +122,11 @@ describe('Repository last commit component', () => {
it('expands commit description when clicking expander', async () => {
factory(createCommitData({ descriptionHtml: 'Test description' }));
- await vm.vm.$nextTick();
+ await nextTick();
vm.find('.text-expander').vm.$emit('click');
- await vm.vm.$nextTick();
+ await nextTick();
expect(vm.find('.commit-row-description').isVisible()).toBe(true);
expect(vm.find('.text-expander').classes('open')).toBe(true);
@@ -134,7 +135,7 @@ describe('Repository last commit component', () => {
it('strips the first newline of the description', async () => {
factory(createCommitData({ descriptionHtml: '&#x000A;Update ADOPTERS.md' }));
- await vm.vm.$nextTick();
+ await nextTick();
expect(vm.find('.commit-row-description').html()).toBe(
'<pre class="commit-row-description gl-mb-3">Update ADOPTERS.md</pre>',
@@ -144,7 +145,7 @@ describe('Repository last commit component', () => {
it('renders the signature HTML as returned by the backend', async () => {
factory(createCommitData({ signatureHtml: '<button>Verified</button>' }));
- await vm.vm.$nextTick();
+ await nextTick();
expect(vm.element).toMatchSnapshot();
});
@@ -152,7 +153,7 @@ describe('Repository last commit component', () => {
it('sets correct CSS class if the commit message is empty', async () => {
factory(createCommitData({ message: '' }));
- await vm.vm.$nextTick();
+ await nextTick();
expect(vm.find('.item-title').classes()).toContain(emptyMessageClass);
});
diff --git a/spec/frontend/repository/components/preview/index_spec.js b/spec/frontend/repository/components/preview/index_spec.js
index 2490258a048..0d9bfc62ed5 100644
--- a/spec/frontend/repository/components/preview/index_spec.js
+++ b/spec/frontend/repository/components/preview/index_spec.js
@@ -1,5 +1,6 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import { handleLocationHash } from '~/lib/utils/common_utils';
import Preview from '~/repository/components/preview/index.vue';
@@ -28,7 +29,7 @@ describe('Repository file preview component', () => {
vm.destroy();
});
- it('renders file HTML', () => {
+ it('renders file HTML', async () => {
factory({
webPath: 'http://test.com',
name: 'README.md',
@@ -38,12 +39,11 @@ describe('Repository file preview component', () => {
// eslint-disable-next-line no-restricted-syntax
vm.setData({ readme: { html: '<div class="blob">test</div>' } });
- return vm.vm.$nextTick(() => {
- expect(vm.element).toMatchSnapshot();
- });
+ await nextTick();
+ expect(vm.element).toMatchSnapshot();
});
- it('handles hash after render', () => {
+ it('handles hash after render', async () => {
factory({
webPath: 'http://test.com',
name: 'README.md',
@@ -53,15 +53,11 @@ describe('Repository file preview component', () => {
// eslint-disable-next-line no-restricted-syntax
vm.setData({ readme: { html: '<div class="blob">test</div>' } });
- return vm.vm
- .$nextTick()
- .then(vm.vm.$nextTick())
- .then(() => {
- expect(handleLocationHash).toHaveBeenCalled();
- });
+ await nextTick();
+ expect(handleLocationHash).toHaveBeenCalled();
});
- it('renders loading icon', () => {
+ it('renders loading icon', async () => {
factory({
webPath: 'http://test.com',
name: 'README.md',
@@ -71,8 +67,7 @@ describe('Repository file preview component', () => {
// eslint-disable-next-line no-restricted-syntax
vm.setData({ loading: 1 });
- return vm.vm.$nextTick(() => {
- expect(vm.find(GlLoadingIcon).exists()).toBe(true);
- });
+ await nextTick();
+ expect(vm.find(GlLoadingIcon).exists()).toBe(true);
});
});
diff --git a/spec/frontend/repository/components/table/index_spec.js b/spec/frontend/repository/components/table/index_spec.js
index 2cd88944f81..07c151ad935 100644
--- a/spec/frontend/repository/components/table/index_spec.js
+++ b/spec/frontend/repository/components/table/index_spec.js
@@ -1,5 +1,6 @@
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import Table from '~/repository/components/table/index.vue';
import TableRow from '~/repository/components/table/row.vue';
@@ -86,18 +87,17 @@ describe('Repository table component', () => {
${'/'} | ${'main'}
${'app/assets'} | ${'main'}
${'/'} | ${'test'}
- `('renders table caption for $ref in $path', ({ path, ref }) => {
+ `('renders table caption for $ref in $path', async ({ path, ref }) => {
factory({ path });
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
vm.setData({ ref });
- return vm.vm.$nextTick(() => {
- expect(vm.find('.table').attributes('aria-label')).toEqual(
- `Files, directories, and submodules in the path ${path} for commit reference ${ref}`,
- );
- });
+ await nextTick();
+ expect(vm.find('.table').attributes('aria-label')).toEqual(
+ `Files, directories, and submodules in the path ${path} for commit reference ${ref}`,
+ );
});
it('shows loading icon', () => {
@@ -140,7 +140,7 @@ describe('Repository table component', () => {
showMoreButton().vm.$emit('click');
- await vm.vm.$nextTick();
+ await nextTick();
expect(vm.emitted('showMore')).toHaveLength(1);
});
diff --git a/spec/frontend/repository/components/table/row_spec.js b/spec/frontend/repository/components/table/row_spec.js
index 440baa72a3c..22570b2d6ed 100644
--- a/spec/frontend/repository/components/table/row_spec.js
+++ b/spec/frontend/repository/components/table/row_spec.js
@@ -1,5 +1,6 @@
import { GlBadge, GlLink, GlIcon, GlIntersectionObserver } from '@gitlab/ui';
import { shallowMount, RouterLinkStub } from '@vue/test-utils';
+import { nextTick } from 'vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import TableRow from '~/repository/components/table/row.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
@@ -53,7 +54,7 @@ describe('Repository table row component', () => {
vm.destroy();
});
- it('renders table row', () => {
+ it('renders table row', async () => {
factory({
id: '1',
sha: '123',
@@ -62,12 +63,11 @@ describe('Repository table row component', () => {
currentPath: '/',
});
- return vm.vm.$nextTick().then(() => {
- expect(vm.element).toMatchSnapshot();
- });
+ await nextTick();
+ expect(vm.element).toMatchSnapshot();
});
- it('renders a symlink table row', () => {
+ it('renders a symlink table row', async () => {
factory({
id: '1',
sha: '123',
@@ -77,12 +77,11 @@ describe('Repository table row component', () => {
mode: FILE_SYMLINK_MODE,
});
- return vm.vm.$nextTick().then(() => {
- expect(vm.element).toMatchSnapshot();
- });
+ await nextTick();
+ expect(vm.element).toMatchSnapshot();
});
- it('renders table row for path with special character', () => {
+ it('renders table row for path with special character', async () => {
factory({
id: '1',
sha: '123',
@@ -91,9 +90,8 @@ describe('Repository table row component', () => {
currentPath: 'test$',
});
- return vm.vm.$nextTick().then(() => {
- expect(vm.element).toMatchSnapshot();
- });
+ await nextTick();
+ expect(vm.element).toMatchSnapshot();
});
it('renders a gl-hover-load directive', () => {
@@ -116,7 +114,7 @@ describe('Repository table row component', () => {
${'tree'} | ${RouterLinkStub} | ${'RouterLink'}
${'blob'} | ${RouterLinkStub} | ${'RouterLink'}
${'commit'} | ${'a'} | ${'hyperlink'}
- `('renders a $componentName for type $type', ({ type, component }) => {
+ `('renders a $componentName for type $type', async ({ type, component }) => {
factory({
id: '1',
sha: '123',
@@ -125,16 +123,15 @@ describe('Repository table row component', () => {
currentPath: '/',
});
- return vm.vm.$nextTick().then(() => {
- expect(vm.find(component).exists()).toBe(true);
- });
+ await nextTick();
+ expect(vm.find(component).exists()).toBe(true);
});
it.each`
path
${'test#'}
${'Änderungen'}
- `('renders link for $path', ({ path }) => {
+ `('renders link for $path', async ({ path }) => {
factory({
id: '1',
sha: '123',
@@ -143,14 +140,13 @@ describe('Repository table row component', () => {
currentPath: '/',
});
- return vm.vm.$nextTick().then(() => {
- expect(vm.find({ ref: 'link' }).props('to')).toEqual({
- path: `/-/tree/main/${encodeURIComponent(path)}`,
- });
+ await nextTick();
+ expect(vm.find({ ref: 'link' }).props('to')).toEqual({
+ path: `/-/tree/main/${encodeURIComponent(path)}`,
});
});
- it('renders link for directory with hash', () => {
+ it('renders link for directory with hash', async () => {
factory({
id: '1',
sha: '123',
@@ -159,12 +155,11 @@ describe('Repository table row component', () => {
currentPath: '/',
});
- return vm.vm.$nextTick().then(() => {
- expect(vm.find('.tree-item-link').props('to')).toEqual({ path: '/-/tree/main/test%23' });
- });
+ await nextTick();
+ expect(vm.find('.tree-item-link').props('to')).toEqual({ path: '/-/tree/main/test%23' });
});
- it('renders commit ID for submodule', () => {
+ it('renders commit ID for submodule', async () => {
factory({
id: '1',
sha: '123',
@@ -173,12 +168,11 @@ describe('Repository table row component', () => {
currentPath: '/',
});
- return vm.vm.$nextTick().then(() => {
- expect(vm.find('.commit-sha').text()).toContain('1');
- });
+ await nextTick();
+ expect(vm.find('.commit-sha').text()).toContain('1');
});
- it('renders link with href', () => {
+ it('renders link with href', async () => {
factory({
id: '1',
sha: '123',
@@ -188,12 +182,11 @@ describe('Repository table row component', () => {
currentPath: '/',
});
- return vm.vm.$nextTick().then(() => {
- expect(vm.find('a').attributes('href')).toEqual('https://test.com');
- });
+ await nextTick();
+ expect(vm.find('a').attributes('href')).toEqual('https://test.com');
});
- it('renders LFS badge', () => {
+ it('renders LFS badge', async () => {
factory({
id: '1',
sha: '123',
@@ -203,12 +196,11 @@ describe('Repository table row component', () => {
lfsOid: '1',
});
- return vm.vm.$nextTick().then(() => {
- expect(vm.find(GlBadge).exists()).toBe(true);
- });
+ await nextTick();
+ expect(vm.find(GlBadge).exists()).toBe(true);
});
- it('renders commit and web links with href for submodule', () => {
+ it('renders commit and web links with href for submodule', async () => {
factory({
id: '1',
sha: '123',
@@ -219,13 +211,12 @@ describe('Repository table row component', () => {
currentPath: '/',
});
- return vm.vm.$nextTick().then(() => {
- expect(vm.find('a').attributes('href')).toEqual('https://test.com');
- expect(vm.find(GlLink).attributes('href')).toEqual('https://test.com/commit');
- });
+ await nextTick();
+ expect(vm.find('a').attributes('href')).toEqual('https://test.com');
+ expect(vm.find(GlLink).attributes('href')).toEqual('https://test.com/commit');
});
- it('renders lock icon', () => {
+ it('renders lock icon', async () => {
factory({
id: '1',
sha: '123',
@@ -234,10 +225,9 @@ describe('Repository table row component', () => {
currentPath: '/',
});
- return vm.vm.$nextTick().then(() => {
- expect(vm.find(GlIcon).exists()).toBe(true);
- expect(vm.find(GlIcon).props('name')).toBe('lock');
- });
+ await nextTick();
+ expect(vm.find(GlIcon).exists()).toBe(true);
+ expect(vm.find(GlIcon).props('name')).toBe('lock');
});
it('renders loading icon when path is loading', () => {
diff --git a/spec/frontend/repository/components/tree_content_spec.js b/spec/frontend/repository/components/tree_content_spec.js
index 00ad1fc05f6..9d3a5394df8 100644
--- a/spec/frontend/repository/components/tree_content_spec.js
+++ b/spec/frontend/repository/components/tree_content_spec.js
@@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import paginatedTreeQuery from 'shared_queries/repository/paginated_tree.query.graphql';
import FilePreview from '~/repository/components/preview/index.vue';
import FileTable from '~/repository/components/table/index.vue';
@@ -50,7 +51,7 @@ describe('Repository table component', () => {
// eslint-disable-next-line no-restricted-syntax
vm.setData({ entries: { blobs: [{ name: 'README.md' }] } });
- await vm.vm.$nextTick();
+ await nextTick();
expect(vm.find(FilePreview).exists()).toBe(true);
});
@@ -60,7 +61,7 @@ describe('Repository table component', () => {
jest.spyOn(vm.vm, 'fetchFiles').mockImplementation(() => {});
- await vm.vm.$nextTick();
+ await nextTick();
expect(vm.vm.fetchFiles).toHaveBeenCalled();
expect(resetRequestedCommits).toHaveBeenCalled();
@@ -111,7 +112,7 @@ describe('Repository table component', () => {
it('is changes hasShowMore to false when "showMore" event is emitted', async () => {
findFileTable().vm.$emit('showMore');
- await vm.vm.$nextTick();
+ await nextTick();
expect(vm.vm.hasShowMore).toBe(false);
});
@@ -119,7 +120,7 @@ describe('Repository table component', () => {
it('changes clickedShowMore when "showMore" event is emitted', async () => {
findFileTable().vm.$emit('showMore');
- await vm.vm.$nextTick();
+ await nextTick();
expect(vm.vm.clickedShowMore).toBe(true);
});
@@ -140,7 +141,7 @@ describe('Repository table component', () => {
// eslint-disable-next-line no-restricted-syntax
vm.setData({ fetchCounter: 5, clickedShowMore: false });
- await vm.vm.$nextTick();
+ await nextTick();
expect(vm.vm.hasShowMore).toBe(false);
});
@@ -161,7 +162,7 @@ describe('Repository table component', () => {
// eslint-disable-next-line no-restricted-syntax
vm.setData({ entries: { blobs }, pagesLoaded });
- await vm.vm.$nextTick();
+ await nextTick();
expect(findFileTable().props('hasMore')).toBe(limitReached);
});
diff --git a/spec/frontend/repository/components/upload_blob_modal_spec.js b/spec/frontend/repository/components/upload_blob_modal_spec.js
index 6b8b0752485..bf024baa627 100644
--- a/spec/frontend/repository/components/upload_blob_modal_spec.js
+++ b/spec/frontend/repository/components/upload_blob_modal_spec.js
@@ -2,6 +2,7 @@ import { GlModal, GlFormInput, GlFormTextarea, GlToggle, GlAlert } from '@gitlab
import { shallowMount } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
+import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import httpStatusCodes from '~/lib/utils/http_status';
@@ -113,7 +114,7 @@ describe('UploadBlobModal', () => {
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ target: 'Not main' });
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findMrToggle().exists()).toBe(true);
});
@@ -202,7 +203,7 @@ describe('UploadBlobModal', () => {
wrapper.vm.uploadFile = jest.fn();
wrapper.vm.replaceFile = jest.fn();
wrapper.vm.submitForm();
- await wrapper.vm.$nextTick();
+ await nextTick();
};
const submitRequest = async () => {
diff --git a/spec/frontend/repository/mock_data.js b/spec/frontend/repository/mock_data.js
index a5ee17ba672..5a6551cb94a 100644
--- a/spec/frontend/repository/mock_data.js
+++ b/spec/frontend/repository/mock_data.js
@@ -5,16 +5,20 @@ export const simpleViewerMock = {
rawSize: 123,
rawTextBlob: 'raw content',
fileType: 'text',
+ language: 'javascript',
path: 'some_file.js',
webPath: 'some_file.js',
editBlobPath: 'some_file.js/edit',
ideEditPath: 'some_file.js/ide/edit',
forkAndEditPath: 'some_file.js/fork/edit',
ideForkAndEditPath: 'some_file.js/fork/ide',
+ environmentFormattedExternalUrl: '',
+ environmentExternalUrlForRouteMap: '',
canModifyBlob: true,
canCurrentUserPushToBranch: true,
archived: false,
storedExternally: false,
+ externalStorageUrl: '',
externalStorage: 'lfs',
rawPath: 'some_file.js',
replacePath: 'some_file.js/replace',
@@ -46,6 +50,7 @@ export const userPermissionsMock = {
};
export const projectMock = {
+ __typename: 'Project',
id: '1234',
userPermissions: userPermissionsMock,
pathLocks: {
diff --git a/spec/frontend/runner/admin_runner_edit/admin_runner_edit_app_spec.js b/spec/frontend/runner/admin_runner_edit/admin_runner_edit_app_spec.js
index ad0bce5c9af..ff6a632a4f8 100644
--- a/spec/frontend/runner/admin_runner_edit/admin_runner_edit_app_spec.js
+++ b/spec/frontend/runner/admin_runner_edit/admin_runner_edit_app_spec.js
@@ -1,4 +1,5 @@
-import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
+import { mount, shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -18,8 +19,7 @@ jest.mock('~/runner/sentry_utils');
const mockRunnerGraphqlId = runnerData.data.runner.id;
const mockRunnerId = `${getIdFromGraphQLId(mockRunnerGraphqlId)}`;
-const localVue = createLocalVue();
-localVue.use(VueApollo);
+Vue.use(VueApollo);
describe('AdminRunnerEditApp', () => {
let wrapper;
@@ -29,7 +29,6 @@ describe('AdminRunnerEditApp', () => {
const createComponentWithApollo = ({ props = {}, mountFn = shallowMount } = {}) => {
wrapper = mountFn(AdminRunnerEditApp, {
- localVue,
apolloProvider: createMockApollo([[getRunnerQuery, mockRunnerQuery]]),
propsData: {
runnerId: mockRunnerId,
@@ -55,10 +54,11 @@ describe('AdminRunnerEditApp', () => {
expect(mockRunnerQuery).toHaveBeenCalledWith({ id: mockRunnerGraphqlId });
});
- it('displays the runner id', async () => {
+ it('displays the runner id and creation date', async () => {
await createComponentWithApollo({ mountFn: mount });
- expect(findRunnerHeader().text()).toContain(`Runner #${mockRunnerId} created`);
+ expect(findRunnerHeader().text()).toContain(`Runner #${mockRunnerId}`);
+ expect(findRunnerHeader().text()).toContain('created');
});
it('displays the runner type and status', async () => {
@@ -76,7 +76,7 @@ describe('AdminRunnerEditApp', () => {
it('error is reported to sentry', () => {
expect(captureException).toHaveBeenCalledWith({
- error: new Error('Network error: Error!'),
+ error: new Error('Error!'),
component: 'AdminRunnerEditApp',
});
});
diff --git a/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js b/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js
new file mode 100644
index 00000000000..4b651961112
--- /dev/null
+++ b/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js
@@ -0,0 +1,146 @@
+import Vue from 'vue';
+import { mount, shallowMount } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { createAlert } from '~/flash';
+
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import RunnerHeader from '~/runner/components/runner_header.vue';
+import RunnerPauseButton from '~/runner/components/runner_pause_button.vue';
+import RunnerEditButton from '~/runner/components/runner_edit_button.vue';
+import getRunnerQuery from '~/runner/graphql/get_runner.query.graphql';
+import AdminRunnerShowApp from '~/runner/admin_runner_show/admin_runner_show_app.vue';
+import { captureException } from '~/runner/sentry_utils';
+
+import { runnerData } from '../mock_data';
+
+jest.mock('~/flash');
+jest.mock('~/runner/sentry_utils');
+
+const mockRunner = runnerData.data.runner;
+const mockRunnerGraphqlId = mockRunner.id;
+const mockRunnerId = `${getIdFromGraphQLId(mockRunnerGraphqlId)}`;
+
+Vue.use(VueApollo);
+
+describe('AdminRunnerShowApp', () => {
+ let wrapper;
+ let mockRunnerQuery;
+
+ const findRunnerHeader = () => wrapper.findComponent(RunnerHeader);
+ const findRunnerEditButton = () => wrapper.findComponent(RunnerEditButton);
+ const findRunnerPauseButton = () => wrapper.findComponent(RunnerPauseButton);
+
+ const mockRunnerQueryResult = (runner = {}) => {
+ mockRunnerQuery = jest.fn().mockResolvedValue({
+ data: {
+ runner: { ...mockRunner, ...runner },
+ },
+ });
+ };
+
+ const createComponent = ({ props = {}, mountFn = shallowMount } = {}) => {
+ wrapper = mountFn(AdminRunnerShowApp, {
+ apolloProvider: createMockApollo([[getRunnerQuery, mockRunnerQuery]]),
+ propsData: {
+ runnerId: mockRunnerId,
+ ...props,
+ },
+ });
+
+ return waitForPromises();
+ };
+
+ afterEach(() => {
+ mockRunnerQuery.mockReset();
+ wrapper.destroy();
+ });
+
+ describe('When showing runner details', () => {
+ beforeEach(async () => {
+ mockRunnerQueryResult();
+
+ await createComponent({ mountFn: mount });
+ });
+
+ it('expect GraphQL ID to be requested', async () => {
+ expect(mockRunnerQuery).toHaveBeenCalledWith({ id: mockRunnerGraphqlId });
+ });
+
+ it('displays the runner header', async () => {
+ expect(findRunnerHeader().text()).toContain(`Runner #${mockRunnerId}`);
+ });
+
+ it('displays the runner edit and pause buttons', async () => {
+ expect(findRunnerEditButton().exists()).toBe(true);
+ expect(findRunnerPauseButton().exists()).toBe(true);
+ });
+
+ it('shows basic runner details', async () => {
+ const expected = `Description Instance runner
+ Last contact Never contacted
+ Version 1.0.0
+ IP Address 127.0.0.1
+ Configuration Runs untagged jobs
+ Maximum job timeout None
+ Tags None`.replace(/\s+/g, ' ');
+
+ expect(wrapper.text().replace(/\s+/g, ' ')).toContain(expected);
+ });
+
+ describe('when runner cannot be updated', () => {
+ beforeEach(async () => {
+ mockRunnerQueryResult({
+ userPermissions: {
+ updateRunner: false,
+ },
+ });
+
+ await createComponent({
+ mountFn: mount,
+ });
+ });
+
+ it('does not display the runner edit and pause buttons', () => {
+ expect(findRunnerEditButton().exists()).toBe(false);
+ expect(findRunnerPauseButton().exists()).toBe(false);
+ });
+ });
+
+ describe('when runner does not have an edit url ', () => {
+ beforeEach(async () => {
+ mockRunnerQueryResult({
+ editAdminUrl: null,
+ });
+
+ await createComponent({
+ mountFn: mount,
+ });
+ });
+
+ it('does not display the runner edit button', () => {
+ expect(findRunnerEditButton().exists()).toBe(false);
+ expect(findRunnerPauseButton().exists()).toBe(true);
+ });
+ });
+ });
+
+ describe('When there is an error', () => {
+ beforeEach(async () => {
+ mockRunnerQuery = jest.fn().mockRejectedValueOnce(new Error('Error!'));
+ await createComponent();
+ });
+
+ it('error is reported to sentry', () => {
+ expect(captureException).toHaveBeenCalledWith({
+ error: new Error('Error!'),
+ component: 'AdminRunnerShowApp',
+ });
+ });
+
+ it('error is shown to the user', () => {
+ expect(createAlert).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
index 42be691ba4c..995f0cf7ba1 100644
--- a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
+++ b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
@@ -1,9 +1,13 @@
+import Vue from 'vue';
import { GlLink } from '@gitlab/ui';
-import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import {
+ extendedWrapper,
+ shallowMountExtended,
+ mountExtended,
+} from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
@@ -46,8 +50,7 @@ jest.mock('~/lib/utils/url_utility', () => ({
updateHistory: jest.fn(),
}));
-const localVue = createLocalVue();
-localVue.use(VueApollo);
+Vue.use(VueApollo);
describe('AdminRunnersApp', () => {
let wrapper;
@@ -65,22 +68,19 @@ describe('AdminRunnersApp', () => {
const findRunnerFilteredSearchBar = () => wrapper.findComponent(RunnerFilteredSearchBar);
const findFilteredSearch = () => wrapper.findComponent(FilteredSearch);
- const createComponent = ({ props = {}, mountFn = shallowMount } = {}) => {
+ const createComponent = ({ props = {}, mountFn = shallowMountExtended } = {}) => {
const handlers = [
[getRunnersQuery, mockRunnersQuery],
[getRunnersCountQuery, mockRunnersCountQuery],
];
- wrapper = extendedWrapper(
- mountFn(AdminRunnersApp, {
- localVue,
- apolloProvider: createMockApollo(handlers),
- propsData: {
- registrationToken: mockRegistrationToken,
- ...props,
- },
- }),
- );
+ wrapper = mountFn(AdminRunnersApp, {
+ apolloProvider: createMockApollo(handlers),
+ propsData: {
+ registrationToken: mockRegistrationToken,
+ ...props,
+ },
+ });
};
beforeEach(async () => {
@@ -98,7 +98,7 @@ describe('AdminRunnersApp', () => {
});
it('shows total runner counts', async () => {
- createComponent({ mountFn: mount });
+ createComponent({ mountFn: mountExtended });
await waitForPromises();
@@ -129,7 +129,7 @@ describe('AdminRunnersApp', () => {
return Promise.resolve({ data: { runners: { count } } });
});
- createComponent({ mountFn: mount });
+ createComponent({ mountFn: mountExtended });
await waitForPromises();
expect(findRunnerTypeTabs().text()).toMatchInterpolatedText(
@@ -157,7 +157,7 @@ describe('AdminRunnersApp', () => {
return Promise.resolve({ data: { runners: { count } } });
});
- createComponent({ mountFn: mount });
+ createComponent({ mountFn: mountExtended });
await waitForPromises();
expect(findRunnerTypeTabs().text()).toMatchInterpolatedText(
@@ -175,7 +175,7 @@ describe('AdminRunnersApp', () => {
});
it('runner item links to the runner admin page', async () => {
- createComponent({ mountFn: mount });
+ createComponent({ mountFn: mountExtended });
await waitForPromises();
@@ -198,7 +198,7 @@ describe('AdminRunnersApp', () => {
});
it('sets tokens in the filtered search', () => {
- createComponent({ mountFn: mount });
+ createComponent({ mountFn: mountExtended });
expect(findFilteredSearch().props('tokens')).toEqual([
expect.objectContaining({
@@ -281,6 +281,7 @@ describe('AdminRunnersApp', () => {
},
});
createComponent();
+ await waitForPromises();
});
it('shows a message for no results', async () => {
@@ -289,9 +290,10 @@ describe('AdminRunnersApp', () => {
});
describe('when runners query fails', () => {
- beforeEach(() => {
+ beforeEach(async () => {
mockRunnersQuery = jest.fn().mockRejectedValue(new Error('Error!'));
createComponent();
+ await waitForPromises();
});
it('error is shown to the user', async () => {
@@ -300,17 +302,18 @@ describe('AdminRunnersApp', () => {
it('error is reported to sentry', async () => {
expect(captureException).toHaveBeenCalledWith({
- error: new Error('Network error: Error!'),
+ error: new Error('Error!'),
component: 'AdminRunnersApp',
});
});
});
describe('Pagination', () => {
- beforeEach(() => {
+ beforeEach(async () => {
mockRunnersQuery = jest.fn().mockResolvedValue(runnersDataPaginated);
- createComponent({ mountFn: mount });
+ createComponent({ mountFn: mountExtended });
+ await waitForPromises();
});
it('more pages can be selected', () => {
diff --git a/spec/frontend/runner/components/cells/link_cell_spec.js b/spec/frontend/runner/components/cells/link_cell_spec.js
new file mode 100644
index 00000000000..a59a0eaa5d8
--- /dev/null
+++ b/spec/frontend/runner/components/cells/link_cell_spec.js
@@ -0,0 +1,72 @@
+import { GlLink } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import LinkCell from '~/runner/components/cells/link_cell.vue';
+
+describe('LinkCell', () => {
+ let wrapper;
+
+ const findGlLink = () => wrapper.find(GlLink);
+ const findSpan = () => wrapper.find('span');
+
+ const createComponent = ({ props = {}, ...options } = {}) => {
+ wrapper = shallowMountExtended(LinkCell, {
+ propsData: {
+ ...props,
+ },
+ ...options,
+ });
+ };
+
+ it('when an href is provided, renders a link', () => {
+ createComponent({ props: { href: '/url' } });
+ expect(findGlLink().exists()).toBe(true);
+ });
+
+ it('when an href is not provided, renders no link', () => {
+ createComponent();
+ expect(findGlLink().exists()).toBe(false);
+ });
+
+ describe.each`
+ href | findContent
+ ${null} | ${findSpan}
+ ${'/url'} | ${findGlLink}
+ `('When href is $href', ({ href, findContent }) => {
+ const content = 'My Text';
+ const attrs = { foo: 'bar' };
+ const listeners = {
+ click: jest.fn(),
+ };
+
+ beforeEach(() => {
+ createComponent({
+ props: { href },
+ slots: {
+ default: content,
+ },
+ attrs,
+ listeners,
+ });
+ });
+
+ afterAll(() => {
+ listeners.click.mockReset();
+ });
+
+ it('Renders content', () => {
+ expect(findContent().text()).toBe(content);
+ });
+
+ it('Passes attributes', () => {
+ expect(findContent().attributes()).toMatchObject(attrs);
+ });
+
+ it('Passes event listeners', () => {
+ expect(listeners.click).toHaveBeenCalledTimes(0);
+
+ findContent().vm.$emit('click');
+
+ expect(listeners.click).toHaveBeenCalledTimes(1);
+ });
+ });
+});
diff --git a/spec/frontend/runner/components/cells/runner_actions_cell_spec.js b/spec/frontend/runner/components/cells/runner_actions_cell_spec.js
index 4233d86c24c..dcb0af67784 100644
--- a/spec/frontend/runner/components/cells/runner_actions_cell_spec.js
+++ b/spec/frontend/runner/components/cells/runner_actions_cell_spec.js
@@ -1,7 +1,7 @@
-import { createLocalVue, shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { createAlert } from '~/flash';
@@ -9,11 +9,12 @@ import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { captureException } from '~/runner/sentry_utils';
import RunnerActionCell from '~/runner/components/cells/runner_actions_cell.vue';
+import RunnerPauseButton from '~/runner/components/runner_pause_button.vue';
+import RunnerEditButton from '~/runner/components/runner_edit_button.vue';
import RunnerDeleteModal from '~/runner/components/runner_delete_modal.vue';
import getGroupRunnersQuery from '~/runner/graphql/get_group_runners.query.graphql';
import getRunnersQuery from '~/runner/graphql/get_runners.query.graphql';
import runnerDeleteMutation from '~/runner/graphql/runner_delete.mutation.graphql';
-import runnerActionsUpdateMutation from '~/runner/graphql/runner_actions_update.mutation.graphql';
import { runnersData } from '../../mock_data';
const mockRunner = runnersData.data.runners.nodes[0];
@@ -21,8 +22,7 @@ const mockRunner = runnersData.data.runners.nodes[0];
const getRunnersQueryName = getRunnersQuery.definitions[0].name.value;
const getGroupRunnersQueryName = getGroupRunnersQuery.definitions[0].name.value;
-const localVue = createLocalVue();
-localVue.use(VueApollo);
+Vue.use(VueApollo);
jest.mock('~/flash');
jest.mock('~/runner/sentry_utils');
@@ -32,44 +32,37 @@ describe('RunnerTypeCell', () => {
const mockToastShow = jest.fn();
const runnerDeleteMutationHandler = jest.fn();
- const runnerActionsUpdateMutationHandler = jest.fn();
- const findEditBtn = () => wrapper.findByTestId('edit-runner');
- const findToggleActiveBtn = () => wrapper.findByTestId('toggle-active-runner');
+ const findEditBtn = () => wrapper.findComponent(RunnerEditButton);
+ const findRunnerPauseBtn = () => wrapper.findComponent(RunnerPauseButton);
const findRunnerDeleteModal = () => wrapper.findComponent(RunnerDeleteModal);
const findDeleteBtn = () => wrapper.findByTestId('delete-runner');
const getTooltip = (w) => getBinding(w.element, 'gl-tooltip')?.value;
const createComponent = (runner = {}, options) => {
- wrapper = extendedWrapper(
- shallowMount(RunnerActionCell, {
- propsData: {
- runner: {
- id: mockRunner.id,
- shortSha: mockRunner.shortSha,
- editAdminUrl: mockRunner.editAdminUrl,
- userPermissions: mockRunner.userPermissions,
- active: mockRunner.active,
- ...runner,
- },
+ wrapper = shallowMountExtended(RunnerActionCell, {
+ propsData: {
+ runner: {
+ id: mockRunner.id,
+ shortSha: mockRunner.shortSha,
+ editAdminUrl: mockRunner.editAdminUrl,
+ userPermissions: mockRunner.userPermissions,
+ active: mockRunner.active,
+ ...runner,
},
- localVue,
- apolloProvider: createMockApollo([
- [runnerDeleteMutation, runnerDeleteMutationHandler],
- [runnerActionsUpdateMutation, runnerActionsUpdateMutationHandler],
- ]),
- directives: {
- GlTooltip: createMockDirective(),
- GlModal: createMockDirective(),
- },
- mocks: {
- $toast: {
- show: mockToastShow,
- },
+ },
+ apolloProvider: createMockApollo([[runnerDeleteMutation, runnerDeleteMutationHandler]]),
+ directives: {
+ GlTooltip: createMockDirective(),
+ GlModal: createMockDirective(),
+ },
+ mocks: {
+ $toast: {
+ show: mockToastShow,
},
- ...options,
- }),
- );
+ },
+ ...options,
+ });
};
beforeEach(() => {
@@ -80,21 +73,11 @@ describe('RunnerTypeCell', () => {
},
},
});
-
- runnerActionsUpdateMutationHandler.mockResolvedValue({
- data: {
- runnerUpdate: {
- runner: mockRunner,
- errors: [],
- },
- },
- });
});
afterEach(() => {
mockToastShow.mockReset();
runnerDeleteMutationHandler.mockReset();
- runnerActionsUpdateMutationHandler.mockReset();
wrapper.destroy();
});
@@ -126,116 +109,14 @@ describe('RunnerTypeCell', () => {
});
});
- describe('Toggle active action', () => {
- describe.each`
- state | label | icon | isActive | newActiveValue
- ${'active'} | ${'Pause'} | ${'pause'} | ${true} | ${false}
- ${'paused'} | ${'Resume'} | ${'play'} | ${false} | ${true}
- `('When the runner is $state', ({ label, icon, isActive, newActiveValue }) => {
- beforeEach(() => {
- createComponent({ active: isActive });
- });
-
- it(`Displays a ${icon} button`, () => {
- expect(findToggleActiveBtn().props('loading')).toBe(false);
- expect(findToggleActiveBtn().props('icon')).toBe(icon);
- expect(getTooltip(findToggleActiveBtn())).toBe(label);
- expect(findToggleActiveBtn().attributes('aria-label')).toBe(label);
- });
-
- it(`After clicking the ${icon} button, the button has a loading state`, async () => {
- await findToggleActiveBtn().vm.$emit('click');
-
- expect(findToggleActiveBtn().props('loading')).toBe(true);
- });
-
- it(`After the ${icon} button is clicked, stale tooltip is removed`, async () => {
- await findToggleActiveBtn().vm.$emit('click');
-
- expect(getTooltip(findToggleActiveBtn())).toBe('');
- expect(findToggleActiveBtn().attributes('aria-label')).toBe('');
- });
-
- describe(`When clicking on the ${icon} button`, () => {
- it(`The apollo mutation to set active to ${newActiveValue} is called`, async () => {
- expect(runnerActionsUpdateMutationHandler).toHaveBeenCalledTimes(0);
-
- await findToggleActiveBtn().vm.$emit('click');
-
- expect(runnerActionsUpdateMutationHandler).toHaveBeenCalledTimes(1);
- expect(runnerActionsUpdateMutationHandler).toHaveBeenCalledWith({
- input: {
- id: mockRunner.id,
- active: newActiveValue,
- },
- });
- });
-
- it('The button does not have a loading state after the mutation occurs', async () => {
- await findToggleActiveBtn().vm.$emit('click');
-
- expect(findToggleActiveBtn().props('loading')).toBe(true);
-
- await waitForPromises();
-
- expect(findToggleActiveBtn().props('loading')).toBe(false);
- });
- });
-
- describe('When update fails', () => {
- describe('On a network error', () => {
- const mockErrorMsg = 'Update error!';
-
- beforeEach(async () => {
- runnerActionsUpdateMutationHandler.mockRejectedValueOnce(new Error(mockErrorMsg));
-
- await findToggleActiveBtn().vm.$emit('click');
- });
-
- it('error is reported to sentry', () => {
- expect(captureException).toHaveBeenCalledWith({
- error: new Error(`Network error: ${mockErrorMsg}`),
- component: 'RunnerActionsCell',
- });
- });
-
- it('error is shown to the user', () => {
- expect(createAlert).toHaveBeenCalledTimes(1);
- });
- });
-
- describe('On a validation error', () => {
- const mockErrorMsg = 'Runner not found!';
- const mockErrorMsg2 = 'User not allowed!';
-
- beforeEach(async () => {
- runnerActionsUpdateMutationHandler.mockResolvedValue({
- data: {
- runnerUpdate: {
- runner: mockRunner,
- errors: [mockErrorMsg, mockErrorMsg2],
- },
- },
- });
-
- await findToggleActiveBtn().vm.$emit('click');
- });
-
- it('error is reported to sentry', () => {
- expect(captureException).toHaveBeenCalledWith({
- error: new Error(`${mockErrorMsg} ${mockErrorMsg2}`),
- component: 'RunnerActionsCell',
- });
- });
+ describe('Pause action', () => {
+ it('Renders a compact pause button', () => {
+ createComponent();
- it('error is shown to the user', () => {
- expect(createAlert).toHaveBeenCalledTimes(1);
- });
- });
- });
+ expect(findRunnerPauseBtn().props('compact')).toBe(true);
});
- it('Does not render the runner toggle active button when user cannot update', () => {
+ it('Does not render the runner pause button when user cannot update', () => {
createComponent({
userPermissions: {
...mockRunner.userPermissions,
@@ -243,7 +124,7 @@ describe('RunnerTypeCell', () => {
},
});
- expect(findToggleActiveBtn().exists()).toBe(false);
+ expect(findRunnerPauseBtn().exists()).toBe(false);
});
});
@@ -308,8 +189,9 @@ describe('RunnerTypeCell', () => {
});
describe('When delete is clicked', () => {
- beforeEach(() => {
+ beforeEach(async () => {
findRunnerDeleteModal().vm.$emit('primary');
+ await waitForPromises();
});
it('The delete mutation is called correctly', () => {
@@ -324,7 +206,8 @@ describe('RunnerTypeCell', () => {
expect(getTooltip(findDeleteBtn())).toBe('');
});
- it('The toast notification is shown', () => {
+ it('The toast notification is shown', async () => {
+ await waitForPromises();
expect(mockToastShow).toHaveBeenCalledTimes(1);
expect(mockToastShow).toHaveBeenCalledWith(
expect.stringContaining(`#${getIdFromGraphQLId(mockRunner.id)} (${mockRunner.shortSha})`),
@@ -336,15 +219,16 @@ describe('RunnerTypeCell', () => {
describe('On a network error', () => {
const mockErrorMsg = 'Delete error!';
- beforeEach(() => {
+ beforeEach(async () => {
runnerDeleteMutationHandler.mockRejectedValueOnce(new Error(mockErrorMsg));
findRunnerDeleteModal().vm.$emit('primary');
+ await waitForPromises();
});
it('error is reported to sentry', () => {
expect(captureException).toHaveBeenCalledWith({
- error: new Error(`Network error: ${mockErrorMsg}`),
+ error: new Error(mockErrorMsg),
component: 'RunnerActionsCell',
});
});
@@ -362,7 +246,7 @@ describe('RunnerTypeCell', () => {
const mockErrorMsg = 'Runner not found!';
const mockErrorMsg2 = 'User not allowed!';
- beforeEach(() => {
+ beforeEach(async () => {
runnerDeleteMutationHandler.mockResolvedValue({
data: {
runnerDelete: {
@@ -372,6 +256,7 @@ describe('RunnerTypeCell', () => {
});
findRunnerDeleteModal().vm.$emit('primary');
+ await waitForPromises();
});
it('error is reported to sentry', () => {
diff --git a/spec/frontend/runner/components/registration/registration_dropdown_spec.js b/spec/frontend/runner/components/registration/registration_dropdown_spec.js
index d18d2bec18e..da8ef7c3af0 100644
--- a/spec/frontend/runner/components/registration/registration_dropdown_spec.js
+++ b/spec/frontend/runner/components/registration/registration_dropdown_spec.js
@@ -1,9 +1,11 @@
import { GlDropdown, GlDropdownItem, GlDropdownForm } from '@gitlab/ui';
-import { createLocalVue, mount, shallowMount, createWrapper } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import { mount, shallowMount, createWrapper } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
+
import VueApollo from 'vue-apollo';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue';
import RegistrationTokenResetDropdownItem from '~/runner/components/registration/registration_token_reset_dropdown_item.vue';
@@ -73,8 +75,7 @@ describe('RegistrationDropdown', () => {
});
describe('When the dropdown item is clicked', () => {
- const localVue = createLocalVue();
- localVue.use(VueApollo);
+ Vue.use(VueApollo);
const requestHandlers = [
[getRunnerPlatformsQuery, jest.fn().mockResolvedValue(mockGraphqlRunnerPlatforms)],
@@ -84,10 +85,9 @@ describe('RegistrationDropdown', () => {
const findModalInBody = () =>
createWrapper(document.body).find('[data-testid="runner-instructions-modal"]');
- beforeEach(() => {
+ beforeEach(async () => {
createComponent(
{
- localVue,
// Mock load modal contents from API
apolloProvider: createMockApollo(requestHandlers),
// Use `attachTo` to find the modal
@@ -96,7 +96,8 @@ describe('RegistrationDropdown', () => {
mount,
);
- findRegistrationInstructionsDropdownItem().trigger('click');
+ await findRegistrationInstructionsDropdownItem().trigger('click');
+ await waitForPromises();
});
afterEach(() => {
diff --git a/spec/frontend/runner/components/registration/registration_token_reset_dropdown_item_spec.js b/spec/frontend/runner/components/registration/registration_token_reset_dropdown_item_spec.js
index e75decddf70..d2deb49a5f7 100644
--- a/spec/frontend/runner/components/registration/registration_token_reset_dropdown_item_spec.js
+++ b/spec/frontend/runner/components/registration/registration_token_reset_dropdown_item_spec.js
@@ -1,6 +1,7 @@
import { GlDropdownItem, GlLoadingIcon, GlToast, GlModal } from '@gitlab/ui';
-import { createLocalVue, shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
+
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -14,9 +15,8 @@ import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
jest.mock('~/flash');
jest.mock('~/runner/sentry_utils');
-const localVue = createLocalVue();
-localVue.use(VueApollo);
-localVue.use(GlToast);
+Vue.use(VueApollo);
+Vue.use(GlToast);
const mockNewToken = 'NEW_TOKEN';
const modalID = 'token-reset-modal';
@@ -34,7 +34,6 @@ describe('RegistrationTokenResetDropdownItem', () => {
const createComponent = ({ props, provide = {} } = {}) => {
wrapper = shallowMount(RegistrationTokenResetDropdownItem, {
- localVue,
provide,
propsData: {
type: INSTANCE_TYPE,
@@ -163,10 +162,10 @@ describe('RegistrationTokenResetDropdownItem', () => {
await waitForPromises();
expect(createAlert).toHaveBeenLastCalledWith({
- message: `Network error: ${mockErrorMsg}`,
+ message: mockErrorMsg,
});
expect(captureException).toHaveBeenCalledWith({
- error: new Error(`Network error: ${mockErrorMsg}`),
+ error: new Error(mockErrorMsg),
component: 'RunnerRegistrationTokenReset',
});
});
diff --git a/spec/frontend/runner/components/registration/registration_token_spec.js b/spec/frontend/runner/components/registration/registration_token_spec.js
index f53ae165344..6b9708cc525 100644
--- a/spec/frontend/runner/components/registration/registration_token_spec.js
+++ b/spec/frontend/runner/components/registration/registration_token_spec.js
@@ -1,7 +1,7 @@
import { nextTick } from 'vue';
import { GlToast } from '@gitlab/ui';
-import { createLocalVue, shallowMount } from '@vue/test-utils';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { createLocalVue } from '@vue/test-utils';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import RegistrationToken from '~/runner/components/registration/registration_token.vue';
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
@@ -25,15 +25,13 @@ describe('RegistrationToken', () => {
const createComponent = ({ props = {}, withGlToast = true } = {}) => {
const localVue = withGlToast ? vueWithGlToast() : undefined;
- wrapper = extendedWrapper(
- shallowMount(RegistrationToken, {
- propsData: {
- value: mockToken,
- ...props,
- },
- localVue,
- }),
- );
+ wrapper = shallowMountExtended(RegistrationToken, {
+ propsData: {
+ value: mockToken,
+ ...props,
+ },
+ localVue,
+ });
showToast = wrapper.vm.$toast ? jest.spyOn(wrapper.vm.$toast, 'show') : null;
};
diff --git a/spec/frontend/runner/components/runner_assigned_item_spec.js b/spec/frontend/runner/components/runner_assigned_item_spec.js
new file mode 100644
index 00000000000..c6156c16d4a
--- /dev/null
+++ b/spec/frontend/runner/components/runner_assigned_item_spec.js
@@ -0,0 +1,53 @@
+import { GlAvatar } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import RunnerAssignedItem from '~/runner/components/runner_assigned_item.vue';
+
+const mockHref = '/group/project';
+const mockName = 'Project';
+const mockFullName = 'Group / Project';
+const mockAvatarUrl = '/avatar.png';
+
+describe('RunnerAssignedItem', () => {
+ let wrapper;
+
+ const findAvatar = () => wrapper.findByTestId('item-avatar');
+
+ const createComponent = ({ props = {} } = {}) => {
+ wrapper = shallowMountExtended(RunnerAssignedItem, {
+ propsData: {
+ href: mockHref,
+ name: mockName,
+ fullName: mockFullName,
+ avatarUrl: mockAvatarUrl,
+ ...props,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('Shows an avatar', () => {
+ const avatar = findAvatar();
+
+ expect(avatar.attributes('href')).toBe(mockHref);
+ expect(avatar.findComponent(GlAvatar).props()).toMatchObject({
+ alt: mockName,
+ entityName: mockName,
+ src: mockAvatarUrl,
+ shape: 'rect',
+ size: 48,
+ });
+ });
+
+ it('Shows an item link', () => {
+ const groupFullName = wrapper.findByText(mockFullName);
+
+ expect(groupFullName.attributes('href')).toBe(mockHref);
+ });
+});
diff --git a/spec/frontend/runner/components/runner_details_spec.js b/spec/frontend/runner/components/runner_details_spec.js
new file mode 100644
index 00000000000..6bf4a52a799
--- /dev/null
+++ b/spec/frontend/runner/components/runner_details_spec.js
@@ -0,0 +1,189 @@
+import { GlSprintf, GlIntersperse, GlTab } from '@gitlab/ui';
+import { createWrapper, ErrorWrapper } from '@vue/test-utils';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
+import { useFakeDate } from 'helpers/fake_date';
+import { ACCESS_LEVEL_REF_PROTECTED, ACCESS_LEVEL_NOT_PROTECTED } from '~/runner/constants';
+
+import RunnerDetails from '~/runner/components/runner_details.vue';
+import RunnerDetail from '~/runner/components/runner_detail.vue';
+import RunnerGroups from '~/runner/components/runner_groups.vue';
+import RunnersJobs from '~/runner/components/runner_jobs.vue';
+import RunnerTags from '~/runner/components/runner_tags.vue';
+import RunnerTag from '~/runner/components/runner_tag.vue';
+
+import { runnerData, runnerWithGroupData } from '../mock_data';
+
+const mockRunner = runnerData.data.runner;
+const mockGroupRunner = runnerWithGroupData.data.runner;
+
+describe('RunnerDetails', () => {
+ let wrapper;
+ const mockNow = '2021-01-15T12:00:00Z';
+ const mockOneHourAgo = '2021-01-15T11:00:00Z';
+
+ useFakeDate(mockNow);
+
+ /**
+ * Find the definition (<dd>) that corresponds to this term (<dt>)
+ * @param {string} dtLabel - Label for this value
+ * @returns Wrapper
+ */
+ const findDd = (dtLabel) => {
+ const dt = wrapper.findByText(dtLabel).element;
+ const dd = dt.nextElementSibling;
+ if (dt.tagName === 'DT' && dd.tagName === 'DD') {
+ return createWrapper(dd, {});
+ }
+ return ErrorWrapper(dtLabel);
+ };
+
+ const findDetailGroups = () => wrapper.findComponent(RunnerGroups);
+ const findRunnersJobs = () => wrapper.findComponent(RunnersJobs);
+ const findJobCountBadge = () => wrapper.findByTestId('job-count-badge');
+
+ const createComponent = ({ props = {}, mountFn = shallowMountExtended, stubs } = {}) => {
+ wrapper = mountFn(RunnerDetails, {
+ propsData: {
+ ...props,
+ },
+ stubs: {
+ RunnerDetail,
+ ...stubs,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('when no runner is present, no contents are shown', () => {
+ createComponent({
+ props: {
+ runner: null,
+ },
+ });
+
+ expect(wrapper.text()).toBe('');
+ });
+
+ describe('Details tab', () => {
+ describe.each`
+ field | runner | expectedValue
+ ${'Description'} | ${{ description: 'My runner' }} | ${'My runner'}
+ ${'Description'} | ${{ description: null }} | ${'None'}
+ ${'Last contact'} | ${{ contactedAt: mockOneHourAgo }} | ${'1 hour ago'}
+ ${'Last contact'} | ${{ contactedAt: null }} | ${'Never contacted'}
+ ${'Version'} | ${{ version: '12.3' }} | ${'12.3'}
+ ${'Version'} | ${{ version: null }} | ${'None'}
+ ${'IP Address'} | ${{ ipAddress: '127.0.0.1' }} | ${'127.0.0.1'}
+ ${'IP Address'} | ${{ ipAddress: null }} | ${'None'}
+ ${'Configuration'} | ${{ accessLevel: ACCESS_LEVEL_REF_PROTECTED, runUntagged: true }} | ${'Protected, Runs untagged jobs'}
+ ${'Configuration'} | ${{ accessLevel: ACCESS_LEVEL_REF_PROTECTED, runUntagged: false }} | ${'Protected'}
+ ${'Configuration'} | ${{ accessLevel: ACCESS_LEVEL_NOT_PROTECTED, runUntagged: true }} | ${'Runs untagged jobs'}
+ ${'Configuration'} | ${{ accessLevel: ACCESS_LEVEL_NOT_PROTECTED, runUntagged: false }} | ${'None'}
+ ${'Maximum job timeout'} | ${{ maximumTimeout: null }} | ${'None'}
+ ${'Maximum job timeout'} | ${{ maximumTimeout: 0 }} | ${'0 seconds'}
+ ${'Maximum job timeout'} | ${{ maximumTimeout: 59 }} | ${'59 seconds'}
+ ${'Maximum job timeout'} | ${{ maximumTimeout: 10 * 60 + 5 }} | ${'10 minutes 5 seconds'}
+ `('"$field" field', ({ field, runner, expectedValue }) => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ runner: {
+ ...mockRunner,
+ ...runner,
+ },
+ },
+ stubs: {
+ GlIntersperse,
+ GlSprintf,
+ TimeAgo,
+ },
+ });
+ });
+
+ it(`displays expected value "${expectedValue}"`, () => {
+ expect(findDd(field).text()).toBe(expectedValue);
+ });
+ });
+
+ describe('"Tags" field', () => {
+ const stubs = { RunnerTags, RunnerTag };
+
+ it('displays expected value "tag-1 tag-2"', () => {
+ createComponent({
+ props: {
+ runner: { ...mockRunner, tagList: ['tag-1', 'tag-2'] },
+ },
+ stubs,
+ });
+
+ expect(findDd('Tags').text().replace(/\s+/g, ' ')).toBe('tag-1 tag-2');
+ });
+
+ it('displays "None" when runner has no tags', () => {
+ createComponent({
+ props: {
+ runner: { ...mockRunner, tagList: [] },
+ },
+ stubs,
+ });
+
+ expect(findDd('Tags').text().replace(/\s+/g, ' ')).toBe('None');
+ });
+ });
+
+ describe('Group runners', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ runner: mockGroupRunner,
+ },
+ });
+ });
+
+ it('Shows a group runner details', () => {
+ expect(findDetailGroups().props('runner')).toEqual(mockGroupRunner);
+ });
+ });
+ });
+
+ describe('Jobs tab', () => {
+ const stubs = { GlTab };
+
+ it('without a runner, shows no jobs', () => {
+ createComponent({
+ props: { runner: null },
+ stubs,
+ });
+
+ expect(findJobCountBadge().exists()).toBe(false);
+ expect(findRunnersJobs().exists()).toBe(false);
+ });
+
+ it('without a job count, shows no jobs count', () => {
+ createComponent({
+ props: {
+ runner: { ...mockRunner, jobCount: undefined },
+ },
+ stubs,
+ });
+
+ expect(findJobCountBadge().exists()).toBe(false);
+ });
+
+ it('with a job count, shows jobs count', () => {
+ const runner = { ...mockRunner, jobCount: 3 };
+
+ createComponent({
+ props: { runner },
+ stubs,
+ });
+
+ expect(findJobCountBadge().text()).toBe('3');
+ expect(findRunnersJobs().props('runner')).toBe(runner);
+ });
+ });
+});
diff --git a/spec/frontend/runner/components/runner_edit_button_spec.js b/spec/frontend/runner/components/runner_edit_button_spec.js
new file mode 100644
index 00000000000..428c1ef07e9
--- /dev/null
+++ b/spec/frontend/runner/components/runner_edit_button_spec.js
@@ -0,0 +1,41 @@
+import { shallowMount, mount } from '@vue/test-utils';
+import RunnerEditButton from '~/runner/components/runner_edit_button.vue';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+
+describe('RunnerEditButton', () => {
+ let wrapper;
+
+ const getTooltipValue = () => getBinding(wrapper.element, 'gl-tooltip').value;
+
+ const createComponent = ({ attrs = {}, mountFn = shallowMount } = {}) => {
+ wrapper = mountFn(RunnerEditButton, {
+ attrs,
+ directives: {
+ GlTooltip: createMockDirective(),
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('Displays Edit text', () => {
+ expect(wrapper.attributes('aria-label')).toBe('Edit');
+ });
+
+ it('Displays Edit tooltip', () => {
+ expect(getTooltipValue()).toBe('Edit');
+ });
+
+ it('Renders a link and adds an href attribute', () => {
+ createComponent({ attrs: { href: '/edit' }, mountFn: mount });
+
+ expect(wrapper.element.tagName).toBe('A');
+ expect(wrapper.attributes('href')).toBe('/edit');
+ });
+});
diff --git a/spec/frontend/runner/components/runner_filtered_search_bar_spec.js b/spec/frontend/runner/components/runner_filtered_search_bar_spec.js
index 5ab0db019a3..fda96e5918e 100644
--- a/spec/frontend/runner/components/runner_filtered_search_bar_spec.js
+++ b/spec/frontend/runner/components/runner_filtered_search_bar_spec.js
@@ -1,6 +1,5 @@
import { GlFilteredSearch, GlDropdown, GlDropdownItem } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
import { statusTokenConfig } from '~/runner/components/search_tokens/status_token_config';
import TagToken from '~/runner/components/search_tokens/tag_token.vue';
@@ -29,27 +28,25 @@ describe('RunnerList', () => {
};
const createComponent = ({ props = {}, options = {} } = {}) => {
- wrapper = extendedWrapper(
- shallowMount(RunnerFilteredSearchBar, {
- propsData: {
- namespace: 'runners',
- tokens: [],
- value: {
- runnerType: null,
- filters: [],
- sort: mockDefaultSort,
- },
- ...props,
+ wrapper = shallowMountExtended(RunnerFilteredSearchBar, {
+ propsData: {
+ namespace: 'runners',
+ tokens: [],
+ value: {
+ runnerType: null,
+ filters: [],
+ sort: mockDefaultSort,
},
- stubs: {
- FilteredSearch,
- GlFilteredSearch,
- GlDropdown,
- GlDropdownItem,
- },
- ...options,
- }),
- );
+ ...props,
+ },
+ stubs: {
+ FilteredSearch,
+ GlFilteredSearch,
+ GlDropdown,
+ GlDropdownItem,
+ },
+ ...options,
+ });
};
beforeEach(() => {
diff --git a/spec/frontend/runner/components/runner_groups_spec.js b/spec/frontend/runner/components/runner_groups_spec.js
new file mode 100644
index 00000000000..b83733b9972
--- /dev/null
+++ b/spec/frontend/runner/components/runner_groups_spec.js
@@ -0,0 +1,67 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+import RunnerGroups from '~/runner/components/runner_groups.vue';
+import RunnerAssignedItem from '~/runner/components/runner_assigned_item.vue';
+
+import { runnerData, runnerWithGroupData } from '../mock_data';
+
+const mockInstanceRunner = runnerData.data.runner;
+const mockGroupRunner = runnerWithGroupData.data.runner;
+const mockGroup = mockGroupRunner.groups.nodes[0];
+
+describe('RunnerGroups', () => {
+ let wrapper;
+
+ const findHeading = () => wrapper.find('h3');
+ const findRunnerAssignedItems = () => wrapper.findAllComponents(RunnerAssignedItem);
+
+ const createComponent = ({ runner = mockGroupRunner, mountFn = shallowMountExtended } = {}) => {
+ wrapper = mountFn(RunnerGroups, {
+ propsData: {
+ runner,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('Shows a heading', () => {
+ createComponent();
+
+ expect(findHeading().text()).toBe('Assigned Group');
+ });
+
+ describe('When there is a group runner', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('Shows a project', () => {
+ createComponent();
+
+ const item = findRunnerAssignedItems().at(0);
+ const { webUrl, name, fullName, avatarUrl } = mockGroup;
+
+ expect(item.props()).toMatchObject({
+ href: webUrl,
+ name,
+ fullName,
+ avatarUrl,
+ });
+ });
+ });
+
+ describe('When there are no groups', () => {
+ beforeEach(() => {
+ createComponent({
+ runner: mockInstanceRunner,
+ });
+ });
+
+ it('Shows a "None" label', () => {
+ expect(wrapper.findByText('None').exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/runner/components/runner_header_spec.js b/spec/frontend/runner/components/runner_header_spec.js
index 50699df3a44..8799c218b06 100644
--- a/spec/frontend/runner/components/runner_header_spec.js
+++ b/spec/frontend/runner/components/runner_header_spec.js
@@ -1,5 +1,5 @@
import { GlSprintf } from '@gitlab/ui';
-import { mount, shallowMount } from '@vue/test-utils';
+import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { GROUP_TYPE, STATUS_ONLINE } from '~/runner/constants';
import { TYPE_CI_RUNNER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
@@ -18,9 +18,10 @@ describe('RunnerHeader', () => {
const findRunnerTypeBadge = () => wrapper.findComponent(RunnerTypeBadge);
const findRunnerStatusBadge = () => wrapper.findComponent(RunnerStatusBadge);
+ const findRunnerLockedIcon = () => wrapper.findByTestId('lock-icon');
const findTimeAgo = () => wrapper.findComponent(TimeAgo);
- const createComponent = ({ runner = {}, mountFn = shallowMount } = {}) => {
+ const createComponent = ({ runner = {}, options = {}, mountFn = shallowMountExtended } = {}) => {
wrapper = mountFn(RunnerHeader, {
propsData: {
runner: {
@@ -32,6 +33,7 @@ describe('RunnerHeader', () => {
GlSprintf,
TimeAgo,
},
+ ...options,
});
};
@@ -41,24 +43,24 @@ describe('RunnerHeader', () => {
it('displays the runner status', () => {
createComponent({
- mountFn: mount,
+ mountFn: mountExtended,
runner: {
status: STATUS_ONLINE,
},
});
- expect(findRunnerStatusBadge().text()).toContain(`online`);
+ expect(findRunnerStatusBadge().text()).toContain('online');
});
it('displays the runner type', () => {
createComponent({
- mountFn: mount,
+ mountFn: mountExtended,
runner: {
runnerType: GROUP_TYPE,
},
});
- expect(findRunnerTypeBadge().text()).toContain(`group`);
+ expect(findRunnerTypeBadge().text()).toContain('group');
});
it('displays the runner id', () => {
@@ -68,7 +70,18 @@ describe('RunnerHeader', () => {
},
});
- expect(wrapper.text()).toContain(`Runner #99`);
+ expect(wrapper.text()).toContain('Runner #99');
+ });
+
+ it('displays the runner locked icon', () => {
+ createComponent({
+ runner: {
+ locked: true,
+ },
+ mountFn: mountExtended,
+ });
+
+ expect(findRunnerLockedIcon().exists()).toBe(true);
});
it('displays the runner creation time', () => {
@@ -78,7 +91,7 @@ describe('RunnerHeader', () => {
expect(findTimeAgo().props('time')).toBe(mockRunner.createdAt);
});
- it('does not display runner creation time if createdAt missing', () => {
+ it('does not display runner creation time if "createdAt" is missing', () => {
createComponent({
runner: {
id: convertToGraphQLId(TYPE_CI_RUNNER, 99),
@@ -86,8 +99,21 @@ describe('RunnerHeader', () => {
},
});
- expect(wrapper.text()).toContain(`Runner #99`);
+ expect(wrapper.text()).toContain('Runner #99');
expect(wrapper.text()).not.toMatch(/created .+/);
expect(findTimeAgo().exists()).toBe(false);
});
+
+ it('displays actions in a slot', () => {
+ createComponent({
+ options: {
+ slots: {
+ actions: '<div data-testid="actions-content">My Actions</div>',
+ },
+ mountFn: mountExtended,
+ },
+ });
+
+ expect(wrapper.findByTestId('actions-content').text()).toBe('My Actions');
+ });
});
diff --git a/spec/frontend/runner/components/runner_jobs_spec.js b/spec/frontend/runner/components/runner_jobs_spec.js
new file mode 100644
index 00000000000..97339056370
--- /dev/null
+++ b/spec/frontend/runner/components/runner_jobs_spec.js
@@ -0,0 +1,156 @@
+import { GlSkeletonLoading } from '@gitlab/ui';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { createAlert } from '~/flash';
+import RunnerJobs from '~/runner/components/runner_jobs.vue';
+import RunnerJobsTable from '~/runner/components/runner_jobs_table.vue';
+import RunnerPagination from '~/runner/components/runner_pagination.vue';
+import { captureException } from '~/runner/sentry_utils';
+import { I18N_NO_JOBS_FOUND, RUNNER_DETAILS_JOBS_PAGE_SIZE } from '~/runner/constants';
+
+import getRunnerJobsQuery from '~/runner/graphql/get_runner_jobs.query.graphql';
+
+import { runnerData, runnerJobsData } from '../mock_data';
+
+jest.mock('~/flash');
+jest.mock('~/runner/sentry_utils');
+
+const mockRunner = runnerData.data.runner;
+const mockRunnerWithJobs = runnerJobsData.data.runner;
+const mockJobs = mockRunnerWithJobs.jobs.nodes;
+
+Vue.use(VueApollo);
+
+describe('RunnerJobs', () => {
+ let wrapper;
+ let mockRunnerJobsQuery;
+
+ const findGlSkeletonLoading = () => wrapper.findComponent(GlSkeletonLoading);
+ const findRunnerJobsTable = () => wrapper.findComponent(RunnerJobsTable);
+ const findRunnerPagination = () => wrapper.findComponent(RunnerPagination);
+
+ const createComponent = ({ mountFn = shallowMountExtended } = {}) => {
+ wrapper = mountFn(RunnerJobs, {
+ apolloProvider: createMockApollo([[getRunnerJobsQuery, mockRunnerJobsQuery]]),
+ propsData: {
+ runner: mockRunner,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ mockRunnerJobsQuery = jest.fn();
+ });
+
+ afterEach(() => {
+ mockRunnerJobsQuery.mockReset();
+ wrapper.destroy();
+ });
+
+ it('Requests runner jobs', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ expect(mockRunnerJobsQuery).toHaveBeenCalledTimes(1);
+ expect(mockRunnerJobsQuery).toHaveBeenCalledWith({
+ id: mockRunner.id,
+ first: RUNNER_DETAILS_JOBS_PAGE_SIZE,
+ });
+ });
+
+ describe('When there are jobs assigned', () => {
+ beforeEach(async () => {
+ mockRunnerJobsQuery.mockResolvedValueOnce(runnerJobsData);
+
+ createComponent();
+ await waitForPromises();
+ });
+
+ it('Shows jobs', () => {
+ const jobs = findRunnerJobsTable().props('jobs');
+
+ expect(jobs).toHaveLength(mockJobs.length);
+ expect(jobs[0]).toMatchObject(mockJobs[0]);
+ });
+
+ describe('When "Next" page is clicked', () => {
+ beforeEach(async () => {
+ findRunnerPagination().vm.$emit('input', { page: 2, after: 'AFTER_CURSOR' });
+
+ await waitForPromises();
+ });
+
+ it('A new page is requested', () => {
+ expect(mockRunnerJobsQuery).toHaveBeenCalledTimes(2);
+ expect(mockRunnerJobsQuery).toHaveBeenLastCalledWith({
+ id: mockRunner.id,
+ first: RUNNER_DETAILS_JOBS_PAGE_SIZE,
+ after: 'AFTER_CURSOR',
+ });
+ });
+ });
+ });
+
+ describe('When loading', () => {
+ it('shows loading indicator and no other content', () => {
+ createComponent();
+
+ expect(findGlSkeletonLoading().exists()).toBe(true);
+ expect(findRunnerJobsTable().exists()).toBe(false);
+ expect(findRunnerPagination().attributes('disabled')).toBe('true');
+ });
+ });
+
+ describe('When there are no jobs', () => {
+ beforeEach(async () => {
+ mockRunnerJobsQuery.mockResolvedValueOnce({
+ data: {
+ runner: {
+ id: mockRunner.id,
+ projectCount: 0,
+ jobs: {
+ nodes: [],
+ pageInfo: {
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: '',
+ endCursor: '',
+ },
+ },
+ },
+ },
+ });
+
+ createComponent();
+ await waitForPromises();
+ });
+
+ it('Shows a "None" label', () => {
+ expect(wrapper.text()).toBe(I18N_NO_JOBS_FOUND);
+ });
+ });
+
+ describe('When an error occurs', () => {
+ beforeEach(async () => {
+ mockRunnerJobsQuery.mockRejectedValue(new Error('Error!'));
+
+ createComponent();
+ await waitForPromises();
+ });
+
+ it('shows an error', () => {
+ expect(createAlert).toHaveBeenCalled();
+ });
+
+ it('reports an error', () => {
+ expect(captureException).toHaveBeenCalledWith({
+ component: 'RunnerJobs',
+ error: expect.any(Error),
+ });
+ });
+ });
+});
diff --git a/spec/frontend/runner/components/runner_jobs_table_spec.js b/spec/frontend/runner/components/runner_jobs_table_spec.js
new file mode 100644
index 00000000000..5f4905ad2a8
--- /dev/null
+++ b/spec/frontend/runner/components/runner_jobs_table_spec.js
@@ -0,0 +1,119 @@
+import { GlTableLite } from '@gitlab/ui';
+import {
+ extendedWrapper,
+ shallowMountExtended,
+ mountExtended,
+} from 'helpers/vue_test_utils_helper';
+import { __, s__ } from '~/locale';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import RunnerJobsTable from '~/runner/components/runner_jobs_table.vue';
+import { useFakeDate } from 'helpers/fake_date';
+import { runnerJobsData } from '../mock_data';
+
+const mockJobs = runnerJobsData.data.runner.jobs.nodes;
+
+describe('RunnerJobsTable', () => {
+ let wrapper;
+ const mockNow = '2021-01-15T12:00:00Z';
+ const mockOneHourAgo = '2021-01-15T11:00:00Z';
+
+ useFakeDate(mockNow);
+
+ const findTable = () => wrapper.findComponent(GlTableLite);
+ const findHeaders = () => wrapper.findAll('th');
+ const findRows = () => wrapper.findAll('[data-testid^="job-row-"]');
+ const findCell = ({ field }) =>
+ extendedWrapper(findRows().at(0).find(`[data-testid="td-${field}"]`));
+
+ const createComponent = ({ props = {} } = {}, mountFn = shallowMountExtended) => {
+ wrapper = mountFn(RunnerJobsTable, {
+ propsData: {
+ jobs: mockJobs,
+ ...props,
+ },
+ stubs: {
+ GlTableLite,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('Sets job id as a row key', () => {
+ createComponent();
+
+ expect(findTable().attributes('primarykey')).toBe('id');
+ });
+
+ describe('Table data', () => {
+ beforeEach(() => {
+ createComponent({}, mountExtended);
+ });
+
+ it('Displays headers', () => {
+ const headerLabels = findHeaders().wrappers.map((w) => w.text());
+
+ expect(headerLabels).toEqual([
+ s__('Job|Status'),
+ __('Job'),
+ __('Project'),
+ __('Commit'),
+ s__('Job|Finished at'),
+ s__('Runners|Tags'),
+ ]);
+ });
+
+ it('Displays a list of jobs', () => {
+ expect(findRows()).toHaveLength(1);
+ });
+
+ it('Displays details of a job', () => {
+ const { id, detailedStatus, pipeline, shortSha, commitPath } = mockJobs[0];
+
+ expect(findCell({ field: 'status' }).text()).toMatchInterpolatedText(detailedStatus.text);
+
+ expect(findCell({ field: 'job' }).text()).toContain(`#${getIdFromGraphQLId(id)}`);
+ expect(findCell({ field: 'job' }).find('a').attributes('href')).toBe(
+ detailedStatus.detailsPath,
+ );
+
+ expect(findCell({ field: 'project' }).text()).toBe(pipeline.project.name);
+ expect(findCell({ field: 'project' }).find('a').attributes('href')).toBe(
+ pipeline.project.webUrl,
+ );
+
+ expect(findCell({ field: 'commit' }).text()).toBe(shortSha);
+ expect(findCell({ field: 'commit' }).find('a').attributes('href')).toBe(commitPath);
+ });
+ });
+
+ describe('Table data formatting', () => {
+ let mockJobsCopy;
+
+ beforeEach(() => {
+ mockJobsCopy = [
+ {
+ ...mockJobs[0],
+ },
+ ];
+ });
+
+ it('Formats finishedAt time', () => {
+ mockJobsCopy[0].finishedAt = mockOneHourAgo;
+
+ createComponent({ props: { jobs: mockJobsCopy } }, mountExtended);
+
+ expect(findCell({ field: 'finished_at' }).text()).toBe('1 hour ago');
+ });
+
+ it('Formats tags', () => {
+ mockJobsCopy[0].tags = ['tag-1', 'tag-2'];
+
+ createComponent({ props: { jobs: mockJobsCopy } }, mountExtended);
+
+ expect(findCell({ field: 'tags' }).text()).toMatchInterpolatedText('tag-1 tag-2');
+ });
+ });
+});
diff --git a/spec/frontend/runner/components/runner_list_spec.js b/spec/frontend/runner/components/runner_list_spec.js
index 452430b7237..42d6ecca09e 100644
--- a/spec/frontend/runner/components/runner_list_spec.js
+++ b/spec/frontend/runner/components/runner_list_spec.js
@@ -1,8 +1,13 @@
import { GlTable, GlSkeletonLoader } from '@gitlab/ui';
-import { mount, shallowMount } from '@vue/test-utils';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import {
+ extendedWrapper,
+ shallowMountExtended,
+ mountExtended,
+} from 'helpers/vue_test_utils_helper';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import RunnerList from '~/runner/components/runner_list.vue';
+import RunnerEditButton from '~/runner/components/runner_edit_button.vue';
+import RunnerPauseButton from '~/runner/components/runner_pause_button.vue';
import { runnersData } from '../mock_data';
const mockRunners = runnersData.data.runners.nodes;
@@ -18,20 +23,18 @@ describe('RunnerList', () => {
const findCell = ({ row = 0, fieldKey }) =>
extendedWrapper(findRows().at(row).find(`[data-testid="td-${fieldKey}"]`));
- const createComponent = ({ props = {} } = {}, mountFn = shallowMount) => {
- wrapper = extendedWrapper(
- mountFn(RunnerList, {
- propsData: {
- runners: mockRunners,
- activeRunnersCount: mockActiveRunnersCount,
- ...props,
- },
- }),
- );
+ const createComponent = ({ props = {} } = {}, mountFn = shallowMountExtended) => {
+ wrapper = mountFn(RunnerList, {
+ propsData: {
+ runners: mockRunners,
+ activeRunnersCount: mockActiveRunnersCount,
+ ...props,
+ },
+ });
};
beforeEach(() => {
- createComponent({}, mount);
+ createComponent({}, mountExtended);
});
afterEach(() => {
@@ -43,9 +46,9 @@ describe('RunnerList', () => {
expect(headerLabels).toEqual([
'Status',
- 'Runner ID',
+ 'Runner',
'Version',
- 'IP Address',
+ 'IP',
'Jobs',
'Tags',
'Last contact',
@@ -54,7 +57,7 @@ describe('RunnerList', () => {
});
it('Sets runner id as a row key', () => {
- createComponent({}, shallowMount);
+ createComponent({});
expect(findTable().attributes('primary-key')).toBe('id');
});
@@ -89,8 +92,9 @@ describe('RunnerList', () => {
// Actions
const actions = findCell({ fieldKey: 'actions' });
- expect(actions.findByTestId('edit-runner').exists()).toBe(true);
- expect(actions.findByTestId('toggle-active-runner').exists()).toBe(true);
+ expect(actions.findComponent(RunnerEditButton).exists()).toBe(true);
+ expect(actions.findComponent(RunnerPauseButton).exists()).toBe(true);
+ expect(actions.findByTestId('delete-runner').exists()).toBe(true);
});
describe('Table data formatting', () => {
@@ -107,7 +111,7 @@ describe('RunnerList', () => {
it('Formats job counts', () => {
mockRunnersCopy[0].jobCount = 1;
- createComponent({ props: { runners: mockRunnersCopy } }, mount);
+ createComponent({ props: { runners: mockRunnersCopy } }, mountExtended);
expect(findCell({ fieldKey: 'jobCount' }).text()).toBe('1');
});
@@ -115,7 +119,7 @@ describe('RunnerList', () => {
it('Formats large job counts', () => {
mockRunnersCopy[0].jobCount = 1000;
- createComponent({ props: { runners: mockRunnersCopy } }, mount);
+ createComponent({ props: { runners: mockRunnersCopy } }, mountExtended);
expect(findCell({ fieldKey: 'jobCount' }).text()).toBe('1,000');
});
@@ -123,7 +127,7 @@ describe('RunnerList', () => {
it('Formats large job counts with a plus symbol', () => {
mockRunnersCopy[0].jobCount = 1001;
- createComponent({ props: { runners: mockRunnersCopy } }, mount);
+ createComponent({ props: { runners: mockRunnersCopy } }, mountExtended);
expect(findCell({ fieldKey: 'jobCount' }).text()).toBe('1,000+');
});
@@ -143,13 +147,13 @@ describe('RunnerList', () => {
});
it('when there are no runners, shows an skeleton loader', () => {
- createComponent({ props: { runners: [], loading: true } }, mount);
+ createComponent({ props: { runners: [], loading: true } }, mountExtended);
expect(findSkeletonLoader().exists()).toBe(true);
});
it('when there are runners, shows a busy indicator skeleton loader', () => {
- createComponent({ props: { loading: true } }, mount);
+ createComponent({ props: { loading: true } }, mountExtended);
expect(findSkeletonLoader().exists()).toBe(false);
});
diff --git a/spec/frontend/runner/components/runner_pagination_spec.js b/spec/frontend/runner/components/runner_pagination_spec.js
index 59feb32dd2a..ecd6e6bd7f9 100644
--- a/spec/frontend/runner/components/runner_pagination_spec.js
+++ b/spec/frontend/runner/components/runner_pagination_spec.js
@@ -104,7 +104,6 @@ describe('RunnerPagination', () => {
expect(wrapper.emitted('input')[0]).toEqual([
{
- before: mockStartCursor,
page: 1,
},
]);
diff --git a/spec/frontend/runner/components/runner_pause_button_spec.js b/spec/frontend/runner/components/runner_pause_button_spec.js
new file mode 100644
index 00000000000..278f3dec2ee
--- /dev/null
+++ b/spec/frontend/runner/components/runner_pause_button_spec.js
@@ -0,0 +1,239 @@
+import Vue from 'vue';
+import { GlButton } from '@gitlab/ui';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
+import runnerToggleActiveMutation from '~/runner/graphql/runner_toggle_active.mutation.graphql';
+import waitForPromises from 'helpers/wait_for_promises';
+import { captureException } from '~/runner/sentry_utils';
+import { createAlert } from '~/flash';
+
+import RunnerPauseButton from '~/runner/components/runner_pause_button.vue';
+import { runnersData } from '../mock_data';
+
+const mockRunner = runnersData.data.runners.nodes[0];
+
+Vue.use(VueApollo);
+
+jest.mock('~/flash');
+jest.mock('~/runner/sentry_utils');
+
+describe('RunnerPauseButton', () => {
+ let wrapper;
+ let runnerToggleActiveHandler;
+
+ const getTooltip = () => getBinding(wrapper.element, 'gl-tooltip').value;
+ const findBtn = () => wrapper.findComponent(GlButton);
+
+ const createComponent = ({ props = {}, mountFn = shallowMountExtended } = {}) => {
+ const { runner, ...propsData } = props;
+
+ wrapper = mountFn(RunnerPauseButton, {
+ propsData: {
+ runner: {
+ id: mockRunner.id,
+ active: mockRunner.active,
+ ...runner,
+ },
+ ...propsData,
+ },
+ apolloProvider: createMockApollo([[runnerToggleActiveMutation, runnerToggleActiveHandler]]),
+ directives: {
+ GlTooltip: createMockDirective(),
+ },
+ });
+ };
+
+ const clickAndWait = async () => {
+ findBtn().vm.$emit('click');
+ await waitForPromises();
+ };
+
+ beforeEach(() => {
+ runnerToggleActiveHandler = jest.fn().mockImplementation(({ input }) => {
+ return Promise.resolve({
+ data: {
+ runnerUpdate: {
+ runner: {
+ id: input.id,
+ active: input.active,
+ },
+ errors: [],
+ },
+ },
+ });
+ });
+
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('Pause/Resume action', () => {
+ describe.each`
+ runnerState | icon | content | isActive | newActiveValue
+ ${'paused'} | ${'play'} | ${'Resume'} | ${false} | ${true}
+ ${'active'} | ${'pause'} | ${'Pause'} | ${true} | ${false}
+ `('When the runner is $runnerState', ({ icon, content, isActive, newActiveValue }) => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ runner: {
+ active: isActive,
+ },
+ },
+ });
+ });
+
+ it(`Displays a ${icon} button`, () => {
+ expect(findBtn().props('loading')).toBe(false);
+ expect(findBtn().props('icon')).toBe(icon);
+ expect(findBtn().text()).toBe(content);
+ });
+
+ it('Does not display redundant text for screen readers', () => {
+ expect(findBtn().attributes('aria-label')).toBe(undefined);
+ });
+
+ describe(`Before the ${icon} button is clicked`, () => {
+ it('The mutation has not been called', () => {
+ expect(runnerToggleActiveHandler).toHaveBeenCalledTimes(0);
+ });
+ });
+
+ describe(`Immediately after the ${icon} button is clicked`, () => {
+ beforeEach(async () => {
+ findBtn().vm.$emit('click');
+ });
+
+ it('The button has a loading state', async () => {
+ expect(findBtn().props('loading')).toBe(true);
+ });
+
+ it('The stale tooltip is removed', async () => {
+ expect(getTooltip()).toBe('');
+ });
+ });
+
+ describe(`After clicking on the ${icon} button`, () => {
+ beforeEach(async () => {
+ await clickAndWait();
+ });
+
+ it(`The mutation to that sets active to ${newActiveValue} is called`, async () => {
+ expect(runnerToggleActiveHandler).toHaveBeenCalledTimes(1);
+ expect(runnerToggleActiveHandler).toHaveBeenCalledWith({
+ input: {
+ id: mockRunner.id,
+ active: newActiveValue,
+ },
+ });
+ });
+
+ it('The button does not have a loading state', () => {
+ expect(findBtn().props('loading')).toBe(false);
+ });
+ });
+
+ describe('When update fails', () => {
+ describe('On a network error', () => {
+ const mockErrorMsg = 'Update error!';
+
+ beforeEach(async () => {
+ runnerToggleActiveHandler.mockRejectedValueOnce(new Error(mockErrorMsg));
+
+ await clickAndWait();
+ });
+
+ it('error is reported to sentry', () => {
+ expect(captureException).toHaveBeenCalledWith({
+ error: new Error(mockErrorMsg),
+ component: 'RunnerPauseButton',
+ });
+ });
+
+ it('error is shown to the user', () => {
+ expect(createAlert).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('On a validation error', () => {
+ const mockErrorMsg = 'Runner not found!';
+ const mockErrorMsg2 = 'User not allowed!';
+
+ beforeEach(async () => {
+ runnerToggleActiveHandler.mockResolvedValueOnce({
+ data: {
+ runnerUpdate: {
+ runner: {
+ id: mockRunner.id,
+ active: isActive,
+ },
+ errors: [mockErrorMsg, mockErrorMsg2],
+ },
+ },
+ });
+
+ await clickAndWait();
+ });
+
+ it('error is reported to sentry', () => {
+ expect(captureException).toHaveBeenCalledWith({
+ error: new Error(`${mockErrorMsg} ${mockErrorMsg2}`),
+ component: 'RunnerPauseButton',
+ });
+ });
+
+ it('error is shown to the user', () => {
+ expect(createAlert).toHaveBeenCalledTimes(1);
+ });
+ });
+ });
+ });
+ });
+
+ describe('When displaying a compact button for an active runner', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ runner: {
+ active: true,
+ },
+ compact: true,
+ },
+ mountFn: mountExtended,
+ });
+ });
+
+ it('Displays no text', () => {
+ expect(findBtn().text()).toBe('');
+
+ // Note: Use <template v-if> to ensure rendering a
+ // text-less button. Ensure we don't send even empty an
+ // content slot to prevent a distorted/rectangular button.
+ expect(wrapper.find('.gl-button-text').exists()).toBe(false);
+ });
+
+ it('Display correctly for screen readers', () => {
+ expect(findBtn().attributes('aria-label')).toBe('Pause');
+ expect(getTooltip()).toBe('Pause');
+ });
+
+ describe('Immediately after the button is clicked', () => {
+ beforeEach(async () => {
+ findBtn().vm.$emit('click');
+ });
+
+ it('The button has a loading state', async () => {
+ expect(findBtn().props('loading')).toBe(true);
+ });
+
+ it('The stale tooltip is removed', async () => {
+ expect(getTooltip()).toBe('');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/runner/components/runner_projects_spec.js b/spec/frontend/runner/components/runner_projects_spec.js
new file mode 100644
index 00000000000..68a2130d6d9
--- /dev/null
+++ b/spec/frontend/runner/components/runner_projects_spec.js
@@ -0,0 +1,193 @@
+import { GlSkeletonLoading } from '@gitlab/ui';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { createAlert } from '~/flash';
+import { sprintf } from '~/locale';
+import {
+ I18N_ASSIGNED_PROJECTS,
+ I18N_NONE,
+ RUNNER_DETAILS_PROJECTS_PAGE_SIZE,
+} from '~/runner/constants';
+import RunnerProjects from '~/runner/components/runner_projects.vue';
+import RunnerAssignedItem from '~/runner/components/runner_assigned_item.vue';
+import RunnerPagination from '~/runner/components/runner_pagination.vue';
+import { captureException } from '~/runner/sentry_utils';
+
+import getRunnerProjectsQuery from '~/runner/graphql/get_runner_projects.query.graphql';
+
+import { runnerData, runnerProjectsData } from '../mock_data';
+
+jest.mock('~/flash');
+jest.mock('~/runner/sentry_utils');
+
+const mockRunner = runnerData.data.runner;
+const mockRunnerWithProjects = runnerProjectsData.data.runner;
+const mockProjects = mockRunnerWithProjects.projects.nodes;
+
+Vue.use(VueApollo);
+
+describe('RunnerProjects', () => {
+ let wrapper;
+ let mockRunnerProjectsQuery;
+
+ const findHeading = () => wrapper.find('h3');
+ const findGlSkeletonLoading = () => wrapper.findComponent(GlSkeletonLoading);
+ const findRunnerAssignedItems = () => wrapper.findAllComponents(RunnerAssignedItem);
+ const findRunnerPagination = () => wrapper.findComponent(RunnerPagination);
+
+ const createComponent = ({ mountFn = shallowMountExtended } = {}) => {
+ wrapper = mountFn(RunnerProjects, {
+ apolloProvider: createMockApollo([[getRunnerProjectsQuery, mockRunnerProjectsQuery]]),
+ propsData: {
+ runner: mockRunner,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ mockRunnerProjectsQuery = jest.fn();
+ });
+
+ afterEach(() => {
+ mockRunnerProjectsQuery.mockReset();
+ wrapper.destroy();
+ });
+
+ it('Requests runner projects', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ expect(mockRunnerProjectsQuery).toHaveBeenCalledTimes(1);
+ expect(mockRunnerProjectsQuery).toHaveBeenCalledWith({
+ id: mockRunner.id,
+ first: RUNNER_DETAILS_PROJECTS_PAGE_SIZE,
+ });
+ });
+
+ describe('When there are projects assigned', () => {
+ beforeEach(async () => {
+ mockRunnerProjectsQuery.mockResolvedValueOnce(runnerProjectsData);
+
+ createComponent();
+ await waitForPromises();
+ });
+
+ it('Shows a heading', async () => {
+ const expected = sprintf(I18N_ASSIGNED_PROJECTS, { projectCount: mockProjects.length });
+
+ expect(findHeading().text()).toBe(expected);
+ });
+
+ it('Shows projects', () => {
+ expect(findRunnerAssignedItems().length).toBe(mockProjects.length);
+ });
+
+ it('Shows a project', () => {
+ const item = findRunnerAssignedItems().at(0);
+ const { webUrl, name, nameWithNamespace, avatarUrl } = mockProjects[0];
+
+ expect(item.props()).toMatchObject({
+ href: webUrl,
+ name,
+ fullName: nameWithNamespace,
+ avatarUrl,
+ });
+ });
+
+ describe('When "Next" page is clicked', () => {
+ beforeEach(async () => {
+ findRunnerPagination().vm.$emit('input', { page: 3, after: 'AFTER_CURSOR' });
+
+ await waitForPromises();
+ });
+
+ it('A new page is requested', () => {
+ expect(mockRunnerProjectsQuery).toHaveBeenCalledTimes(2);
+ expect(mockRunnerProjectsQuery).toHaveBeenLastCalledWith({
+ id: mockRunner.id,
+ first: RUNNER_DETAILS_PROJECTS_PAGE_SIZE,
+ after: 'AFTER_CURSOR',
+ });
+ });
+
+ it('When "Prev" page is clicked, the previous page is requested', async () => {
+ findRunnerPagination().vm.$emit('input', { page: 2, before: 'BEFORE_CURSOR' });
+
+ await waitForPromises();
+
+ expect(mockRunnerProjectsQuery).toHaveBeenCalledTimes(3);
+ expect(mockRunnerProjectsQuery).toHaveBeenLastCalledWith({
+ id: mockRunner.id,
+ last: RUNNER_DETAILS_PROJECTS_PAGE_SIZE,
+ before: 'BEFORE_CURSOR',
+ });
+ });
+ });
+ });
+
+ describe('When loading', () => {
+ it('shows loading indicator and no other content', () => {
+ createComponent();
+
+ expect(findGlSkeletonLoading().exists()).toBe(true);
+
+ expect(wrapper.findByText(I18N_NONE).exists()).toBe(false);
+ expect(findRunnerAssignedItems().length).toBe(0);
+
+ expect(findRunnerPagination().attributes('disabled')).toBe('true');
+ });
+ });
+
+ describe('When there are no projects', () => {
+ beforeEach(async () => {
+ mockRunnerProjectsQuery.mockResolvedValueOnce({
+ data: {
+ runner: {
+ id: mockRunner.id,
+ projectCount: 0,
+ projects: {
+ nodes: [],
+ pageInfo: {
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: '',
+ endCursor: '',
+ },
+ },
+ },
+ },
+ });
+
+ createComponent();
+ await waitForPromises();
+ });
+
+ it('Shows a "None" label', () => {
+ expect(wrapper.findByText(I18N_NONE).exists()).toBe(true);
+ });
+ });
+
+ describe('When an error occurs', () => {
+ beforeEach(async () => {
+ mockRunnerProjectsQuery.mockRejectedValue(new Error('Error!'));
+
+ createComponent();
+ await waitForPromises();
+ });
+
+ it('shows an error', () => {
+ expect(createAlert).toHaveBeenCalled();
+ });
+
+ it('reports an error', () => {
+ expect(captureException).toHaveBeenCalledWith({
+ component: 'RunnerProjects',
+ error: expect.any(Error),
+ });
+ });
+ });
+});
diff --git a/spec/frontend/runner/components/runner_type_tabs_spec.js b/spec/frontend/runner/components/runner_type_tabs_spec.js
index 4871d9c470a..9da5d842d8f 100644
--- a/spec/frontend/runner/components/runner_type_tabs_spec.js
+++ b/spec/frontend/runner/components/runner_type_tabs_spec.js
@@ -1,7 +1,7 @@
import { GlTab } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue';
-import { INSTANCE_TYPE, GROUP_TYPE } from '~/runner/constants';
+import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/runner/constants';
const mockSearch = { runnerType: null, filters: [], pagination: { page: 1 }, sort: 'CREATED_DESC' };
@@ -13,6 +13,7 @@ describe('RunnerTypeTabs', () => {
findTabs()
.filter((tab) => tab.attributes('active') === 'true')
.at(0);
+ const getTabsTitles = () => findTabs().wrappers.map((tab) => tab.text());
const createComponent = ({ props, ...options } = {}) => {
wrapper = shallowMount(RunnerTypeTabs, {
@@ -35,13 +36,18 @@ describe('RunnerTypeTabs', () => {
wrapper.destroy();
});
- it('Renders options to filter runners', () => {
- expect(findTabs().wrappers.map((tab) => tab.text())).toEqual([
- 'All',
- 'Instance',
- 'Group',
- 'Project',
- ]);
+ it('Renders all options to filter runners by default', () => {
+ expect(getTabsTitles()).toEqual(['All', 'Instance', 'Group', 'Project']);
+ });
+
+ it('Renders fewer options to filter runners', () => {
+ createComponent({
+ props: {
+ runnerTypes: [GROUP_TYPE, PROJECT_TYPE],
+ },
+ });
+
+ expect(getTabsTitles()).toEqual(['All', 'Group', 'Project']);
});
it('"All" is selected by default', () => {
diff --git a/spec/frontend/runner/components/runner_update_form_spec.js b/spec/frontend/runner/components/runner_update_form_spec.js
index ebb2e67d1e2..8b76be396ef 100644
--- a/spec/frontend/runner/components/runner_update_form_spec.js
+++ b/spec/frontend/runner/components/runner_update_form_spec.js
@@ -1,9 +1,8 @@
+import Vue, { nextTick } from 'vue';
import { GlForm } from '@gitlab/ui';
-import { createLocalVue, mount } from '@vue/test-utils';
-import { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert, VARIANT_SUCCESS } from '~/flash';
import RunnerUpdateForm from '~/runner/components/runner_update_form.vue';
@@ -23,8 +22,7 @@ jest.mock('~/runner/sentry_utils');
const mockRunner = runnerData.data.runner;
-const localVue = createLocalVue();
-localVue.use(VueApollo);
+Vue.use(VueApollo);
describe('RunnerUpdateForm', () => {
let wrapper;
@@ -61,16 +59,13 @@ describe('RunnerUpdateForm', () => {
});
const createComponent = ({ props } = {}) => {
- wrapper = extendedWrapper(
- mount(RunnerUpdateForm, {
- localVue,
- propsData: {
- runner: mockRunner,
- ...props,
- },
- apolloProvider: createMockApollo([[runnerUpdateMutation, runnerUpdateHandler]]),
- }),
- );
+ wrapper = mountExtended(RunnerUpdateForm, {
+ propsData: {
+ runner: mockRunner,
+ ...props,
+ },
+ apolloProvider: createMockApollo([[runnerUpdateMutation, runnerUpdateHandler]]),
+ });
};
const expectToHaveSubmittedRunnerContaining = (submittedRunner) => {
@@ -126,8 +121,21 @@ describe('RunnerUpdateForm', () => {
it('Updates runner with no changes', async () => {
await submitFormAndWait();
- // Some fields are not submitted
- const { ipAddress, runnerType, createdAt, status, ...submitted } = mockRunner;
+ // Some read-only fields are not submitted
+ const {
+ __typename,
+ ipAddress,
+ runnerType,
+ createdAt,
+ status,
+ editAdminUrl,
+ contactedAt,
+ userPermissions,
+ version,
+ groups,
+ jobCount,
+ ...submitted
+ } = mockRunner;
expectToHaveSubmittedRunnerContaining(submitted);
});
@@ -239,11 +247,11 @@ describe('RunnerUpdateForm', () => {
await submitFormAndWait();
expect(createAlert).toHaveBeenLastCalledWith({
- message: `Network error: ${mockErrorMsg}`,
+ message: mockErrorMsg,
});
expect(captureException).toHaveBeenCalledWith({
component: 'RunnerUpdateForm',
- error: new Error(`Network error: ${mockErrorMsg}`),
+ error: new Error(mockErrorMsg),
});
expect(findSubmitDisabledAttr()).toBeUndefined();
});
diff --git a/spec/frontend/runner/group_runners/group_runners_app_spec.js b/spec/frontend/runner/group_runners/group_runners_app_spec.js
index 034b7848f35..7cb1f49d4f7 100644
--- a/spec/frontend/runner/group_runners/group_runners_app_spec.js
+++ b/spec/frontend/runner/group_runners/group_runners_app_spec.js
@@ -1,15 +1,19 @@
-import { nextTick } from 'vue';
+import Vue, { nextTick } from 'vue';
import { GlLink } from '@gitlab/ui';
-import { createLocalVue, shallowMount, mount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import {
+ extendedWrapper,
+ shallowMountExtended,
+ mountExtended,
+} from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { updateHistory } from '~/lib/utils/url_utility';
+import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue';
import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
import RunnerList from '~/runner/components/runner_list.vue';
import RunnerStats from '~/runner/components/stat/runner_stats.vue';
@@ -22,6 +26,7 @@ import {
DEFAULT_SORT,
INSTANCE_TYPE,
GROUP_TYPE,
+ PROJECT_TYPE,
PARAM_KEY_STATUS,
STATUS_ACTIVE,
RUNNER_PAGE_SIZE,
@@ -33,8 +38,7 @@ import { captureException } from '~/runner/sentry_utils';
import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import { groupRunnersData, groupRunnersDataPaginated, groupRunnersCountData } from '../mock_data';
-const localVue = createLocalVue();
-localVue.use(VueApollo);
+Vue.use(VueApollo);
const mockGroupFullPath = 'group1';
const mockRegistrationToken = 'AABBCC';
@@ -54,6 +58,7 @@ describe('GroupRunnersApp', () => {
const findRunnerStats = () => wrapper.findComponent(RunnerStats);
const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown);
+ const findRunnerTypeTabs = () => wrapper.findComponent(RunnerTypeTabs);
const findRunnerList = () => wrapper.findComponent(RunnerList);
const findRunnerPagination = () => extendedWrapper(wrapper.findComponent(RunnerPagination));
const findRunnerPaginationPrev = () =>
@@ -62,14 +67,18 @@ describe('GroupRunnersApp', () => {
const findRunnerFilteredSearchBar = () => wrapper.findComponent(RunnerFilteredSearchBar);
const findFilteredSearch = () => wrapper.findComponent(FilteredSearch);
- const createComponent = ({ props = {}, mountFn = shallowMount } = {}) => {
+ const mockCountQueryResult = (count) =>
+ Promise.resolve({
+ data: { group: { id: groupRunnersCountData.data.group.id, runners: { count } } },
+ });
+
+ const createComponent = ({ props = {}, mountFn = shallowMountExtended } = {}) => {
const handlers = [
[getGroupRunnersQuery, mockGroupRunnersQuery],
[getGroupRunnersCountQuery, mockGroupRunnersCountQuery],
];
wrapper = mountFn(GroupRunnersApp, {
- localVue,
apolloProvider: createMockApollo(handlers),
propsData: {
registrationToken: mockRegistrationToken,
@@ -91,7 +100,7 @@ describe('GroupRunnersApp', () => {
});
it('shows total runner counts', async () => {
- createComponent({ mountFn: mount });
+ createComponent({ mountFn: mountExtended });
await waitForPromises();
@@ -102,6 +111,44 @@ describe('GroupRunnersApp', () => {
expect(stats).toMatch('Stale runners 2');
});
+ it('shows the runner tabs with a runner count for each type', async () => {
+ mockGroupRunnersCountQuery.mockImplementation(({ type }) => {
+ switch (type) {
+ case GROUP_TYPE:
+ return mockCountQueryResult(2);
+ case PROJECT_TYPE:
+ return mockCountQueryResult(1);
+ default:
+ return mockCountQueryResult(4);
+ }
+ });
+
+ createComponent({ mountFn: mountExtended });
+ await waitForPromises();
+
+ expect(findRunnerTypeTabs().text()).toMatchInterpolatedText('All 4 Group 2 Project 1');
+ });
+
+ it('shows the runner tabs with a formatted runner count', async () => {
+ mockGroupRunnersCountQuery.mockImplementation(({ type }) => {
+ switch (type) {
+ case GROUP_TYPE:
+ return mockCountQueryResult(2000);
+ case PROJECT_TYPE:
+ return mockCountQueryResult(1000);
+ default:
+ return mockCountQueryResult(3000);
+ }
+ });
+
+ createComponent({ mountFn: mountExtended });
+ await waitForPromises();
+
+ expect(findRunnerTypeTabs().text()).toMatchInterpolatedText(
+ 'All 3,000 Group 2,000 Project 1,000',
+ );
+ });
+
it('shows the runner setup instructions', () => {
expect(findRegistrationDropdown().props('registrationToken')).toBe(mockRegistrationToken);
expect(findRegistrationDropdown().props('type')).toBe(GROUP_TYPE);
@@ -116,7 +163,7 @@ describe('GroupRunnersApp', () => {
const { webUrl, node } = groupRunnersData.data.group.runners.edges[0];
const { id, shortSha } = node;
- createComponent({ mountFn: mount });
+ createComponent({ mountFn: mountExtended });
await waitForPromises();
@@ -136,7 +183,7 @@ describe('GroupRunnersApp', () => {
});
it('sets tokens in the filtered search', () => {
- createComponent({ mountFn: mount });
+ createComponent({ mountFn: mountExtended });
const tokens = findFilteredSearch().props('tokens');
@@ -215,11 +262,13 @@ describe('GroupRunnersApp', () => {
mockGroupRunnersQuery = jest.fn().mockResolvedValue({
data: {
group: {
+ id: '1',
runners: { nodes: [] },
},
},
});
createComponent();
+ await waitForPromises();
});
it('shows a message for no results', async () => {
@@ -228,9 +277,10 @@ describe('GroupRunnersApp', () => {
});
describe('when runners query fails', () => {
- beforeEach(() => {
+ beforeEach(async () => {
mockGroupRunnersQuery = jest.fn().mockRejectedValue(new Error('Error!'));
createComponent();
+ await waitForPromises();
});
it('error is shown to the user', async () => {
@@ -239,17 +289,18 @@ describe('GroupRunnersApp', () => {
it('error is reported to sentry', async () => {
expect(captureException).toHaveBeenCalledWith({
- error: new Error('Network error: Error!'),
+ error: new Error('Error!'),
component: 'GroupRunnersApp',
});
});
});
describe('Pagination', () => {
- beforeEach(() => {
+ beforeEach(async () => {
mockGroupRunnersQuery = jest.fn().mockResolvedValue(groupRunnersDataPaginated);
- createComponent({ mountFn: mount });
+ createComponent({ mountFn: mountExtended });
+ await waitForPromises();
});
it('more pages can be selected', () => {
diff --git a/spec/frontend/runner/mock_data.js b/spec/frontend/runner/mock_data.js
index 9c430e205ea..d80caa47752 100644
--- a/spec/frontend/runner/mock_data.js
+++ b/spec/frontend/runner/mock_data.js
@@ -5,6 +5,9 @@ import runnersData from 'test_fixtures/graphql/runner/get_runners.query.graphql.
import runnersCountData from 'test_fixtures/graphql/runner/get_runners_count.query.graphql.json';
import runnersDataPaginated from 'test_fixtures/graphql/runner/get_runners.query.graphql.paginated.json';
import runnerData from 'test_fixtures/graphql/runner/get_runner.query.graphql.json';
+import runnerWithGroupData from 'test_fixtures/graphql/runner/get_runner.query.graphql.with_group.json';
+import runnerProjectsData from 'test_fixtures/graphql/runner/get_runner_projects.query.graphql.json';
+import runnerJobsData from 'test_fixtures/graphql/runner/get_runner_jobs.query.graphql.json';
// Group queries
import groupRunnersData from 'test_fixtures/graphql/runner/get_group_runners.query.graphql.json';
@@ -12,10 +15,13 @@ import groupRunnersCountData from 'test_fixtures/graphql/runner/get_group_runner
import groupRunnersDataPaginated from 'test_fixtures/graphql/runner/get_group_runners.query.graphql.paginated.json';
export {
- runnerData,
+ runnersData,
runnersCountData,
runnersDataPaginated,
- runnersData,
+ runnerData,
+ runnerWithGroupData,
+ runnerProjectsData,
+ runnerJobsData,
groupRunnersData,
groupRunnersCountData,
groupRunnersDataPaginated,
diff --git a/spec/frontend/runner/utils_spec.js b/spec/frontend/runner/utils_spec.js
new file mode 100644
index 00000000000..3fa9784ecdf
--- /dev/null
+++ b/spec/frontend/runner/utils_spec.js
@@ -0,0 +1,65 @@
+import { formatJobCount, tableField, getPaginationVariables } from '~/runner/utils';
+
+describe('~/runner/utils', () => {
+ describe('formatJobCount', () => {
+ it('formats a number', () => {
+ expect(formatJobCount(1)).toBe('1');
+ expect(formatJobCount(99)).toBe('99');
+ });
+
+ it('formats a large count', () => {
+ expect(formatJobCount(1000)).toBe('1,000');
+ expect(formatJobCount(1001)).toBe('1,000+');
+ });
+
+ it('returns an empty string for non-numeric values', () => {
+ expect(formatJobCount(undefined)).toBe('');
+ expect(formatJobCount(null)).toBe('');
+ expect(formatJobCount('number')).toBe('');
+ });
+ });
+
+ describe('tableField', () => {
+ it('a field with options', () => {
+ expect(tableField({ key: 'name' })).toEqual({
+ key: 'name',
+ label: '',
+ tdAttr: { 'data-testid': 'td-name' },
+ thClass: expect.any(Array),
+ });
+ });
+
+ it('a field with a label', () => {
+ const label = 'A field name';
+
+ expect(tableField({ key: 'name', label })).toMatchObject({
+ label,
+ });
+ });
+
+ it('a field with custom classes', () => {
+ const mockClasses = ['foo', 'bar'];
+
+ expect(tableField({ thClasses: mockClasses })).toMatchObject({
+ thClass: expect.arrayContaining(mockClasses),
+ });
+ });
+ });
+
+ describe('getPaginationVariables', () => {
+ const after = 'AFTER_CURSOR';
+ const before = 'BEFORE_CURSOR';
+
+ it.each`
+ case | pagination | pageSize | variables
+ ${'next page'} | ${{ after }} | ${undefined} | ${{ after, first: 10 }}
+ ${'prev page'} | ${{ before }} | ${undefined} | ${{ before, last: 10 }}
+ ${'first page'} | ${{}} | ${undefined} | ${{ first: 10 }}
+ ${'next page with N items'} | ${{ after }} | ${20} | ${{ after, first: 20 }}
+ ${'prev page with N items'} | ${{ before }} | ${20} | ${{ before, last: 20 }}
+ ${'first page with N items'} | ${{}} | ${20} | ${{ first: 20 }}
+ `('navigates to $case', ({ pagination, pageSize, variables }) => {
+ expect(getPaginationVariables(pagination, pageSize)).toEqual(variables);
+ });
+ });
+});
diff --git a/spec/frontend/search/sidebar/components/confidentiality_filter_spec.js b/spec/frontend/search/sidebar/components/confidentiality_filter_spec.js
index 3713e1d414f..a377ddae0eb 100644
--- a/spec/frontend/search/sidebar/components/confidentiality_filter_spec.js
+++ b/spec/frontend/search/sidebar/components/confidentiality_filter_spec.js
@@ -1,11 +1,11 @@
-import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import { MOCK_QUERY } from 'jest/search/mock_data';
import ConfidentialityFilter from '~/search/sidebar/components/confidentiality_filter.vue';
import RadioFilter from '~/search/sidebar/components/radio_filter.vue';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('ConfidentialityFilter', () => {
let wrapper;
@@ -25,7 +25,6 @@ describe('ConfidentialityFilter', () => {
});
wrapper = shallowMount(ConfidentialityFilter, {
- localVue,
store,
});
};
diff --git a/spec/frontend/search/sidebar/components/radio_filter_spec.js b/spec/frontend/search/sidebar/components/radio_filter_spec.js
index 4c81312e479..39d5ee581ec 100644
--- a/spec/frontend/search/sidebar/components/radio_filter_spec.js
+++ b/spec/frontend/search/sidebar/components/radio_filter_spec.js
@@ -1,13 +1,13 @@
import { GlFormRadioGroup, GlFormRadio } from '@gitlab/ui';
-import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import { MOCK_QUERY } from 'jest/search/mock_data';
import RadioFilter from '~/search/sidebar/components/radio_filter.vue';
import { confidentialFilterData } from '~/search/sidebar/constants/confidential_filter_data';
import { stateFilterData } from '~/search/sidebar/constants/state_filter_data';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('RadioFilter', () => {
let wrapper;
@@ -30,7 +30,6 @@ describe('RadioFilter', () => {
});
wrapper = shallowMount(RadioFilter, {
- localVue,
store,
propsData: {
...defaultProps,
diff --git a/spec/frontend/search/sidebar/components/status_filter_spec.js b/spec/frontend/search/sidebar/components/status_filter_spec.js
index 08ce57b206b..5d8ecd8733a 100644
--- a/spec/frontend/search/sidebar/components/status_filter_spec.js
+++ b/spec/frontend/search/sidebar/components/status_filter_spec.js
@@ -1,11 +1,11 @@
-import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import { MOCK_QUERY } from 'jest/search/mock_data';
import RadioFilter from '~/search/sidebar/components/radio_filter.vue';
import StatusFilter from '~/search/sidebar/components/status_filter.vue';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('StatusFilter', () => {
let wrapper;
@@ -25,7 +25,6 @@ describe('StatusFilter', () => {
});
wrapper = shallowMount(StatusFilter, {
- localVue,
store,
});
};
diff --git a/spec/frontend/search/sort/components/app_spec.js b/spec/frontend/search/sort/components/app_spec.js
index 5806d6b51d2..04520a3e704 100644
--- a/spec/frontend/search/sort/components/app_spec.js
+++ b/spec/frontend/search/sort/components/app_spec.js
@@ -1,12 +1,12 @@
import { GlButtonGroup, GlButton, GlDropdown, GlDropdownItem } from '@gitlab/ui';
-import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import { MOCK_QUERY, MOCK_SORT_OPTIONS } from 'jest/search/mock_data';
import GlobalSearchSort from '~/search/sort/components/app.vue';
import { SORT_DIRECTION_UI } from '~/search/sort/constants';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('GlobalSearchSort', () => {
let wrapper;
@@ -30,7 +30,6 @@ describe('GlobalSearchSort', () => {
});
wrapper = shallowMount(GlobalSearchSort, {
- localVue,
store,
propsData: {
...defaultProps,
diff --git a/spec/frontend/security_configuration/components/app_spec.js b/spec/frontend/security_configuration/components/app_spec.js
index cbdf7f53913..963577fa763 100644
--- a/spec/frontend/security_configuration/components/app_spec.js
+++ b/spec/frontend/security_configuration/components/app_spec.js
@@ -32,7 +32,7 @@ const upgradePath = '/upgrade';
const autoDevopsHelpPagePath = '/autoDevopsHelpPagePath';
const autoDevopsPath = '/autoDevopsPath';
const gitlabCiHistoryPath = 'test/historyPath';
-const projectPath = 'namespace/project';
+const projectFullPath = 'namespace/project';
useLocalStorageSpy();
@@ -54,7 +54,7 @@ describe('App component', () => {
upgradePath,
autoDevopsHelpPagePath,
autoDevopsPath,
- projectPath,
+ projectFullPath,
glFeatures: {
secureVulnerabilityTraining,
},
@@ -274,11 +274,11 @@ describe('App component', () => {
describe('Auto DevOps enabled alert', () => {
describe.each`
- context | autoDevopsEnabled | localStorageValue | shouldRender
- ${'enabled'} | ${true} | ${null} | ${true}
- ${'enabled, alert dismissed on other project'} | ${true} | ${['foo/bar']} | ${true}
- ${'enabled, alert dismissed on this project'} | ${true} | ${[projectPath]} | ${false}
- ${'not enabled'} | ${false} | ${null} | ${false}
+ context | autoDevopsEnabled | localStorageValue | shouldRender
+ ${'enabled'} | ${true} | ${null} | ${true}
+ ${'enabled, alert dismissed on other project'} | ${true} | ${['foo/bar']} | ${true}
+ ${'enabled, alert dismissed on this project'} | ${true} | ${[projectFullPath]} | ${false}
+ ${'not enabled'} | ${false} | ${null} | ${false}
`('given Auto DevOps is $context', ({ autoDevopsEnabled, localStorageValue, shouldRender }) => {
beforeEach(() => {
if (localStorageValue !== null) {
@@ -302,11 +302,11 @@ describe('App component', () => {
describe('dismissing', () => {
describe.each`
- dismissedProjects | expectedWrittenValue
- ${null} | ${[projectPath]}
- ${[]} | ${[projectPath]}
- ${['foo/bar']} | ${['foo/bar', projectPath]}
- ${[projectPath]} | ${[projectPath]}
+ dismissedProjects | expectedWrittenValue
+ ${null} | ${[projectFullPath]}
+ ${[]} | ${[projectFullPath]}
+ ${['foo/bar']} | ${['foo/bar', projectFullPath]}
+ ${[projectFullPath]} | ${[projectFullPath]}
`(
'given dismissed projects $dismissedProjects',
({ dismissedProjects, expectedWrittenValue }) => {
diff --git a/spec/frontend/security_configuration/components/feature_card_spec.js b/spec/frontend/security_configuration/components/feature_card_spec.js
index 0eca2c27075..2b74be19480 100644
--- a/spec/frontend/security_configuration/components/feature_card_spec.js
+++ b/spec/frontend/security_configuration/components/feature_card_spec.js
@@ -113,7 +113,6 @@ describe('FeatureCard component', () => {
context | available | configured | expectedStatus
${'a configured feature'} | ${true} | ${true} | ${'Enabled'}
${'an unconfigured feature'} | ${true} | ${false} | ${'Not enabled'}
- ${'an available feature with unknown status'} | ${true} | ${undefined} | ${''}
${'an unavailable feature'} | ${false} | ${false} | ${'Available with Ultimate'}
${'an unavailable feature with unknown status'} | ${false} | ${undefined} | ${'Available with Ultimate'}
`('given $context', ({ available, configured, expectedStatus }) => {
diff --git a/spec/frontend/security_configuration/components/training_provider_list_spec.js b/spec/frontend/security_configuration/components/training_provider_list_spec.js
index 578248e696f..18c9ada6bde 100644
--- a/spec/frontend/security_configuration/components/training_provider_list_spec.js
+++ b/spec/frontend/security_configuration/components/training_provider_list_spec.js
@@ -1,14 +1,26 @@
+import * as Sentry from '@sentry/browser';
import { GlAlert, GlLink, GlToggle, GlCard, GlSkeletonLoader } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
+import {
+ TRACK_TOGGLE_TRAINING_PROVIDER_ACTION,
+ TRACK_TOGGLE_TRAINING_PROVIDER_LABEL,
+} from '~/security_configuration/constants';
import TrainingProviderList from '~/security_configuration/components/training_provider_list.vue';
+import securityTrainingProvidersQuery from '~/security_configuration/graphql/security_training_providers.query.graphql';
import configureSecurityTrainingProvidersMutation from '~/security_configuration/graphql/configure_security_training_providers.mutation.graphql';
+import dismissUserCalloutMutation from '~/graphql_shared/mutations/dismiss_user_callout.mutation.graphql';
import waitForPromises from 'helpers/wait_for_promises';
import {
+ dismissUserCalloutResponse,
+ dismissUserCalloutErrorResponse,
securityTrainingProviders,
- createMockResolvers,
+ securityTrainingProvidersResponse,
+ updateSecurityTrainingProvidersResponse,
+ updateSecurityTrainingProvidersErrorResponse,
testProjectPath,
textProviderIds,
} from '../mock_data';
@@ -19,14 +31,28 @@ describe('TrainingProviderList component', () => {
let wrapper;
let apolloProvider;
- const createApolloProvider = ({ resolvers } = {}) => {
- apolloProvider = createMockApollo([], createMockResolvers({ resolvers }));
+ const createApolloProvider = ({ handlers = [] } = {}) => {
+ const defaultHandlers = [
+ [
+ securityTrainingProvidersQuery,
+ jest.fn().mockResolvedValue(securityTrainingProvidersResponse),
+ ],
+ [
+ configureSecurityTrainingProvidersMutation,
+ jest.fn().mockResolvedValue(updateSecurityTrainingProvidersResponse),
+ ],
+ ];
+
+ // make sure we don't have any duplicate handlers to avoid 'Request handler already defined for query` errors
+ const mergedHandlers = [...new Map([...defaultHandlers, ...handlers])];
+
+ apolloProvider = createMockApollo(mergedHandlers);
};
const createComponent = () => {
wrapper = shallowMount(TrainingProviderList, {
provide: {
- projectPath: testProjectPath,
+ projectFullPath: testProjectPath,
},
apolloProvider,
});
@@ -42,27 +68,49 @@ describe('TrainingProviderList component', () => {
const findLoader = () => wrapper.findComponent(GlSkeletonLoader);
const findErrorAlert = () => wrapper.findComponent(GlAlert);
- const toggleFirstProvider = () => findFirstToggle().vm.$emit('change');
+ const toggleFirstProvider = () => findFirstToggle().vm.$emit('change', textProviderIds[0]);
afterEach(() => {
wrapper.destroy();
apolloProvider = null;
});
- describe('with a successful response', () => {
+ describe('when loading', () => {
beforeEach(() => {
- createApolloProvider();
+ const pendingHandler = () => new Promise(() => {});
+
+ createApolloProvider({
+ handlers: [[securityTrainingProvidersQuery, pendingHandler]],
+ });
createComponent();
});
- describe('when loading', () => {
- it('shows the loader', () => {
- expect(findLoader().exists()).toBe(true);
- });
+ it('shows the loader', () => {
+ expect(findLoader().exists()).toBe(true);
+ });
- it('does not show the cards', () => {
- expect(findCards().exists()).toBe(false);
+ it('does not show the cards', () => {
+ expect(findCards().exists()).toBe(false);
+ });
+ });
+
+ describe('with a successful response', () => {
+ beforeEach(() => {
+ createApolloProvider({
+ handlers: [
+ [dismissUserCalloutMutation, jest.fn().mockResolvedValue(dismissUserCalloutResponse)],
+ ],
+ resolvers: {
+ Mutation: {
+ configureSecurityTrainingProviders: () => ({
+ errors: [],
+ securityTrainingProviders: [],
+ }),
+ },
+ },
});
+
+ createComponent();
});
describe('basic structure', () => {
@@ -104,9 +152,9 @@ describe('TrainingProviderList component', () => {
beforeEach(async () => {
jest.spyOn(apolloProvider.defaultClient, 'mutate');
- await waitForMutationToBeLoaded();
+ await waitForQueryToBeLoaded();
- toggleFirstProvider();
+ await toggleFirstProvider();
});
it.each`
@@ -124,10 +172,78 @@ describe('TrainingProviderList component', () => {
expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith(
expect.objectContaining({
mutation: configureSecurityTrainingProvidersMutation,
- variables: { input: { enabledProviders: textProviderIds, fullPath: testProjectPath } },
+ variables: {
+ input: {
+ providerId: textProviderIds[0],
+ isEnabled: true,
+ isPrimary: false,
+ projectPath: testProjectPath,
+ },
+ },
}),
);
});
+
+ it('dismisses the callout when the feature gets first enabled', async () => {
+ // wait for configuration update mutation to complete
+ await waitForMutationToBeLoaded();
+
+ // both the config and dismiss mutations have been called
+ expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledTimes(2);
+ expect(apolloProvider.defaultClient.mutate).toHaveBeenNthCalledWith(
+ 2,
+ expect.objectContaining({
+ mutation: dismissUserCalloutMutation,
+ variables: {
+ input: {
+ featureName: 'security_training_feature_promotion',
+ },
+ },
+ }),
+ );
+
+ toggleFirstProvider();
+ await waitForMutationToBeLoaded();
+
+ // the config mutation has been called again but not the dismiss mutation
+ expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledTimes(3);
+ expect(apolloProvider.defaultClient.mutate).toHaveBeenNthCalledWith(
+ 3,
+ expect.objectContaining({
+ mutation: configureSecurityTrainingProvidersMutation,
+ }),
+ );
+ });
+ });
+
+ describe('metrics', () => {
+ let trackingSpy;
+
+ beforeEach(() => {
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ });
+
+ afterEach(() => {
+ unmockTracking();
+ });
+
+ it('tracks when a provider gets toggled', () => {
+ expect(trackingSpy).not.toHaveBeenCalled();
+
+ toggleFirstProvider();
+
+ // Note: Ideally we also want to test that the tracking event is called correctly when a
+ // provider gets disabled, but that's a bit tricky to do with the current implementation
+ // Once https://gitlab.com/gitlab-org/gitlab/-/issues/348985 and https://gitlab.com/gitlab-org/gitlab/-/merge_requests/79492
+ // are merged this will be much easer to do and should be tackled then.
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, TRACK_TOGGLE_TRAINING_PROVIDER_ACTION, {
+ property: securityTrainingProviders[0].id,
+ label: TRACK_TOGGLE_TRAINING_PROVIDER_LABEL,
+ extra: {
+ providerIsEnabled: true,
+ },
+ });
+ });
});
});
@@ -142,11 +258,7 @@ describe('TrainingProviderList component', () => {
describe('when fetching training providers', () => {
beforeEach(async () => {
createApolloProvider({
- resolvers: {
- Query: {
- securityTrainingProviders: jest.fn().mockReturnValue(new Error()),
- },
- },
+ handlers: [[securityTrainingProvidersQuery, jest.fn().mockRejectedValue()]],
});
createComponent();
@@ -165,10 +277,43 @@ describe('TrainingProviderList component', () => {
describe('when storing training provider configurations', () => {
beforeEach(async () => {
createApolloProvider({
+ handlers: [
+ [
+ configureSecurityTrainingProvidersMutation,
+ jest.fn().mockReturnValue(updateSecurityTrainingProvidersErrorResponse),
+ ],
+ ],
+ });
+ createComponent();
+
+ await waitForQueryToBeLoaded();
+ toggleFirstProvider();
+ await waitForMutationToBeLoaded();
+ });
+
+ it('shows an non-dismissible error alert', () => {
+ expectErrorAlertToExist();
+ });
+
+ it('shows an error description', () => {
+ expect(findErrorAlert().text()).toBe(TrainingProviderList.i18n.configMutationErrorMessage);
+ });
+ });
+
+ describe.each`
+ errorType | mutationHandler
+ ${'backend error'} | ${jest.fn().mockReturnValue(dismissUserCalloutErrorResponse)}
+ ${'network error'} | ${jest.fn().mockRejectedValue()}
+ `('when dismissing the callout and a "$errorType" happens', ({ mutationHandler }) => {
+ beforeEach(async () => {
+ jest.spyOn(Sentry, 'captureException').mockImplementation();
+
+ createApolloProvider({
+ handlers: [[dismissUserCalloutMutation, mutationHandler]],
resolvers: {
Mutation: {
configureSecurityTrainingProviders: () => ({
- errors: ['something went wrong!'],
+ errors: [],
securityTrainingProviders: [],
}),
},
@@ -178,15 +323,14 @@ describe('TrainingProviderList component', () => {
await waitForQueryToBeLoaded();
toggleFirstProvider();
- await waitForMutationToBeLoaded();
});
- it('shows an non-dismissible error alert', () => {
- expectErrorAlertToExist();
- });
+ it('logs the error to sentry', async () => {
+ expect(Sentry.captureException).not.toHaveBeenCalled();
- it('shows an error description', () => {
- expect(findErrorAlert().text()).toBe(TrainingProviderList.i18n.configMutationErrorMessage);
+ await waitForMutationToBeLoaded();
+
+ expect(Sentry.captureException).toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/security_configuration/components/upgrade_banner_spec.js b/spec/frontend/security_configuration/components/upgrade_banner_spec.js
index a35fded72fb..ff44acfc4f9 100644
--- a/spec/frontend/security_configuration/components/upgrade_banner_spec.js
+++ b/spec/frontend/security_configuration/components/upgrade_banner_spec.js
@@ -1,15 +1,22 @@
import { GlBanner } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import UpgradeBanner from '~/security_configuration/components/upgrade_banner.vue';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
+import UpgradeBanner, {
+ SECURITY_UPGRADE_BANNER,
+ UPGRADE_OR_FREE_TRIAL,
+} from '~/security_configuration/components/upgrade_banner.vue';
const upgradePath = '/upgrade';
describe('UpgradeBanner component', () => {
let wrapper;
let closeSpy;
+ let primarySpy;
+ let trackingSpy;
const createComponent = (propsData) => {
closeSpy = jest.fn();
+ primarySpy = jest.fn();
wrapper = shallowMountExtended(UpgradeBanner, {
provide: {
@@ -18,43 +25,83 @@ describe('UpgradeBanner component', () => {
propsData,
listeners: {
close: closeSpy,
+ primary: primarySpy,
},
});
};
const findGlBanner = () => wrapper.findComponent(GlBanner);
+ const expectTracking = (action, label) => {
+ return expect(trackingSpy).toHaveBeenCalledWith(undefined, action, {
+ label,
+ property: SECURITY_UPGRADE_BANNER,
+ });
+ };
+
beforeEach(() => {
- createComponent();
+ trackingSpy = mockTracking(undefined, undefined, jest.spyOn);
});
afterEach(() => {
wrapper.destroy();
+ unmockTracking();
});
- it('passes the expected props to GlBanner', () => {
- expect(findGlBanner().props()).toMatchObject({
- title: UpgradeBanner.i18n.title,
- buttonText: UpgradeBanner.i18n.buttonText,
- buttonLink: upgradePath,
+ describe('when the component renders', () => {
+ it('tracks an event', () => {
+ expect(trackingSpy).not.toHaveBeenCalled();
+
+ createComponent();
+
+ expectTracking('render', SECURITY_UPGRADE_BANNER);
});
});
- it('renders the list of benefits', () => {
- const wrapperText = wrapper.text();
+ describe('when ready', () => {
+ beforeEach(() => {
+ createComponent();
+ trackingSpy.mockClear();
+ });
- expect(wrapperText).toContain('Immediately begin risk analysis and remediation');
- expect(wrapperText).toContain('statistics in the merge request');
- expect(wrapperText).toContain('statistics across projects');
- expect(wrapperText).toContain('Runtime security metrics');
- expect(wrapperText).toContain('More scan types, including Container Scanning,');
- });
+ it('passes the expected props to GlBanner', () => {
+ expect(findGlBanner().props()).toMatchObject({
+ title: UpgradeBanner.i18n.title,
+ buttonText: UpgradeBanner.i18n.buttonText,
+ buttonLink: upgradePath,
+ });
+ });
- it(`re-emits GlBanner's close event`, () => {
- expect(closeSpy).not.toHaveBeenCalled();
+ it('renders the list of benefits', () => {
+ const wrapperText = wrapper.text();
- wrapper.findComponent(GlBanner).vm.$emit('close');
+ expect(wrapperText).toContain('Immediately begin risk analysis and remediation');
+ expect(wrapperText).toContain('statistics in the merge request');
+ expect(wrapperText).toContain('statistics across projects');
+ expect(wrapperText).toContain('Runtime security metrics');
+ expect(wrapperText).toContain('More scan types, including Container Scanning,');
+ });
+
+ describe('when user interacts', () => {
+ it(`re-emits GlBanner's close event & tracks an event`, () => {
+ expect(closeSpy).not.toHaveBeenCalled();
+ expect(trackingSpy).not.toHaveBeenCalled();
+
+ wrapper.findComponent(GlBanner).vm.$emit('close');
+
+ expect(closeSpy).toHaveBeenCalledTimes(1);
+ expectTracking('dismiss_banner', SECURITY_UPGRADE_BANNER);
+ });
- expect(closeSpy).toHaveBeenCalledTimes(1);
+ it(`re-emits GlBanner's primary event & tracks an event`, () => {
+ expect(primarySpy).not.toHaveBeenCalled();
+ expect(trackingSpy).not.toHaveBeenCalled();
+
+ wrapper.findComponent(GlBanner).vm.$emit('primary');
+
+ expect(primarySpy).toHaveBeenCalledTimes(1);
+ expectTracking('click_button', UPGRADE_OR_FREE_TRIAL);
+ });
+ });
});
});
diff --git a/spec/frontend/security_configuration/mock_data.js b/spec/frontend/security_configuration/mock_data.js
index 37ecce3886d..b042e870467 100644
--- a/spec/frontend/security_configuration/mock_data.js
+++ b/spec/frontend/security_configuration/mock_data.js
@@ -9,6 +9,7 @@ export const securityTrainingProviders = [
description: 'Interactive developer security education',
url: 'https://www.example.org/security/training',
isEnabled: false,
+ isPrimary: false,
},
{
id: textProviderIds[1],
@@ -16,24 +17,62 @@ export const securityTrainingProviders = [
description: 'Security training with guide and learning pathways.',
url: 'https://www.vendornametwo.com/',
isEnabled: true,
+ isPrimary: false,
},
];
export const securityTrainingProvidersResponse = {
data: {
- securityTrainingProviders,
+ project: {
+ id: 1,
+ securityTrainingProviders,
+ },
+ },
+};
+
+export const dismissUserCalloutResponse = {
+ data: {
+ userCalloutCreate: {
+ errors: [],
+ userCallout: {
+ dismissedAt: '2022-02-02T04:36:57Z',
+ featureName: 'SECURITY_TRAINING_FEATURE_PROMOTION',
+ },
+ },
+ },
+};
+
+export const dismissUserCalloutErrorResponse = {
+ data: {
+ userCalloutCreate: {
+ errors: ['Something went wrong'],
+ userCallout: {
+ dismissedAt: '',
+ featureName: 'SECURITY_TRAINING_FEATURE_PROMOTION',
+ },
+ },
},
};
-const defaultMockResolvers = {
- Query: {
- securityTrainingProviders() {
- return securityTrainingProviders;
+export const updateSecurityTrainingProvidersResponse = {
+ data: {
+ securityTrainingUpdate: {
+ errors: [],
+ training: {
+ id: 101,
+ name: 'Acme',
+ isEnabled: true,
+ isPrimary: false,
+ },
},
},
};
-export const createMockResolvers = ({ resolvers: customMockResolvers = {} } = {}) => ({
- ...defaultMockResolvers,
- ...customMockResolvers,
-});
+export const updateSecurityTrainingProvidersErrorResponse = {
+ data: {
+ securityTrainingUpdate: {
+ errors: ['something went wrong!'],
+ training: null,
+ },
+ },
+};
diff --git a/spec/frontend/serverless/components/__snapshots__/empty_state_spec.js.snap b/spec/frontend/serverless/components/__snapshots__/empty_state_spec.js.snap
index 350055cb935..f57b9418be5 100644
--- a/spec/frontend/serverless/components/__snapshots__/empty_state_spec.js.snap
+++ b/spec/frontend/serverless/components/__snapshots__/empty_state_spec.js.snap
@@ -10,7 +10,7 @@ exports[`EmptyStateComponent should render content 1`] = `
<h1 class=\\"gl-font-size-h-display gl-line-height-36 h4\\">
Getting started with serverless
</h1>
- <p class=\\"gl-mt-3\\">In order to start using functions as a service, you must first install Knative on your Kubernetes cluster. <gl-link-stub href=\\"/help\\">More information</gl-link-stub>
+ <p class=\\"gl-mt-3\\">Serverless was <gl-link-stub target=\\"_blank\\" href=\\"https://about.gitlab.com/releases/2021/09/22/gitlab-14-3-released/#gitlab-serverless\\">deprecated</gl-link-stub>. But if you opt to use it, you must install Knative in your Kubernetes cluster first. <gl-link-stub href=\\"/help\\">Learn more.</gl-link-stub>
</p>
<div class=\\"gl-display-flex gl-flex-wrap gl-justify-content-center\\">
<!---->
diff --git a/spec/frontend/serverless/components/function_details_spec.js b/spec/frontend/serverless/components/function_details_spec.js
index d2b8de71e01..0c9b2498589 100644
--- a/spec/frontend/serverless/components/function_details_spec.js
+++ b/spec/frontend/serverless/components/function_details_spec.js
@@ -1,17 +1,16 @@
-import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import functionDetailsComponent from '~/serverless/components/function_details.vue';
import { createStore } from '~/serverless/store';
describe('functionDetailsComponent', () => {
- let localVue;
let component;
let store;
beforeEach(() => {
- localVue = createLocalVue();
- localVue.use(Vuex);
+ Vue.use(Vuex);
store = createStore({ clustersPath: '/clusters', helpPath: '/help' });
});
@@ -33,7 +32,6 @@ describe('functionDetailsComponent', () => {
it('has a name, description, URL, and no pods loaded', () => {
component = shallowMount(functionDetailsComponent, {
- localVue,
store,
propsData: {
func: serviceStub,
@@ -58,7 +56,6 @@ describe('functionDetailsComponent', () => {
serviceStub.podcount = 1;
component = shallowMount(functionDetailsComponent, {
- localVue,
store,
propsData: {
func: serviceStub,
@@ -73,7 +70,6 @@ describe('functionDetailsComponent', () => {
serviceStub.podcount = 3;
component = shallowMount(functionDetailsComponent, {
- localVue,
store,
propsData: {
func: serviceStub,
@@ -88,7 +84,6 @@ describe('functionDetailsComponent', () => {
serviceStub.description = null;
component = shallowMount(functionDetailsComponent, {
- localVue,
store,
propsData: {
func: serviceStub,
diff --git a/spec/frontend/serverless/components/functions_spec.js b/spec/frontend/serverless/components/functions_spec.js
index 01dd512c5d3..846fd63e918 100644
--- a/spec/frontend/serverless/components/functions_spec.js
+++ b/spec/frontend/serverless/components/functions_spec.js
@@ -1,5 +1,6 @@
-import { GlLoadingIcon } from '@gitlab/ui';
-import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { GlLoadingIcon, GlAlert, GlSprintf } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import AxiosMockAdapter from 'axios-mock-adapter';
import Vuex from 'vuex';
import { TEST_HOST } from 'helpers/test_constants';
@@ -15,17 +16,16 @@ describe('functionsComponent', () => {
let component;
let store;
- let localVue;
let axiosMock;
beforeEach(() => {
axiosMock = new AxiosMockAdapter(axios);
axiosMock.onGet(statusPath).reply(200);
- localVue = createLocalVue();
- localVue.use(Vuex);
+ Vue.use(Vuex);
store = createStore({});
+ component = shallowMount(functionsComponent, { store, stubs: { GlSprintf } });
});
afterEach(() => {
@@ -33,23 +33,26 @@ describe('functionsComponent', () => {
axiosMock.restore();
});
- it('should render empty state when Knative is not installed', () => {
- store.dispatch('receiveFunctionsSuccess', { knative_installed: false });
- component = shallowMount(functionsComponent, { localVue, store });
+ it('should render deprecation notice', () => {
+ expect(component.findComponent(GlAlert).text()).toBe(
+ 'Serverless was deprecated in GitLab 14.3.',
+ );
+ });
+
+ it('should render empty state when Knative is not installed', async () => {
+ await store.dispatch('receiveFunctionsSuccess', { knative_installed: false });
- expect(component.find(EmptyState).exists()).toBe(true);
+ expect(component.findComponent(EmptyState).exists()).toBe(true);
});
- it('should render a loading component', () => {
- store.dispatch('requestFunctionsLoading');
- component = shallowMount(functionsComponent, { localVue, store });
+ it('should render a loading component', async () => {
+ await store.dispatch('requestFunctionsLoading');
- expect(component.find(GlLoadingIcon).exists()).toBe(true);
+ expect(component.findComponent(GlLoadingIcon).exists()).toBe(true);
});
- it('should render empty state when there is no function data', () => {
- store.dispatch('receiveFunctionsNoDataSuccess', { knative_installed: true });
- component = shallowMount(functionsComponent, { localVue, store });
+ it('should render empty state when there is no function data', async () => {
+ await store.dispatch('receiveFunctionsNoDataSuccess', { knative_installed: true });
expect(
component.vm.$el
@@ -62,27 +65,22 @@ describe('functionsComponent', () => {
);
});
- it('should render functions and a loader when functions are partially fetched', () => {
- store.dispatch('receiveFunctionsPartial', {
+ it('should render functions and a loader when functions are partially fetched', async () => {
+ await store.dispatch('receiveFunctionsPartial', {
...mockServerlessFunctions,
knative_installed: 'checking',
});
- component = shallowMount(functionsComponent, { localVue, store });
-
expect(component.find('.js-functions-wrapper').exists()).toBe(true);
expect(component.find('.js-functions-loader').exists()).toBe(true);
});
- it('should render the functions list', () => {
+ it('should render the functions list', async () => {
store = createStore({ clustersPath: 'clustersPath', helpPath: 'helpPath', statusPath });
- component = shallowMount(functionsComponent, { localVue, store });
-
- component.vm.$store.dispatch('receiveFunctionsSuccess', mockServerlessFunctions);
+ await component.vm.$store.dispatch('receiveFunctionsSuccess', mockServerlessFunctions);
- return component.vm.$nextTick().then(() => {
- expect(component.find(EnvironmentRow).exists()).toBe(true);
- });
+ await nextTick();
+ expect(component.findComponent(EnvironmentRow).exists()).toBe(true);
});
});
diff --git a/spec/frontend/serverless/survey_banner_spec.js b/spec/frontend/serverless/survey_banner_spec.js
deleted file mode 100644
index 4682c2328c3..00000000000
--- a/spec/frontend/serverless/survey_banner_spec.js
+++ /dev/null
@@ -1,51 +0,0 @@
-import { GlBanner } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import Cookies from 'js-cookie';
-import SurveyBanner from '~/serverless/survey_banner.vue';
-
-describe('Knative survey banner', () => {
- let wrapper;
-
- function mountBanner() {
- wrapper = shallowMount(SurveyBanner, {
- propsData: {
- surveyUrl: 'http://somesurvey.com/',
- },
- });
- }
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- it('should render the banner when the cookie is absent', () => {
- jest.spyOn(Cookies, 'get').mockReturnValue(undefined);
- mountBanner();
-
- expect(Cookies.get).toHaveBeenCalled();
- expect(wrapper.find(GlBanner).exists()).toBe(true);
- });
-
- it('should close the banner and set a cookie when close button is clicked', () => {
- jest.spyOn(Cookies, 'get').mockReturnValue(undefined);
- jest.spyOn(Cookies, 'set');
- mountBanner();
-
- expect(wrapper.find(GlBanner).exists()).toBe(true);
- wrapper.find(GlBanner).vm.$emit('close');
-
- return wrapper.vm.$nextTick().then(() => {
- expect(Cookies.set).toHaveBeenCalledWith('hide_serverless_survey', 'true', { expires: 3650 });
- expect(wrapper.find(GlBanner).exists()).toBe(false);
- });
- });
-
- it('should not render the banner when the cookie is set', () => {
- jest.spyOn(Cookies, 'get').mockReturnValue('true');
- mountBanner();
-
- expect(Cookies.get).toHaveBeenCalled();
- expect(wrapper.find(GlBanner).exists()).toBe(false);
- });
-});
diff --git a/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js b/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js
index 0c6ed998747..c105810e11c 100644
--- a/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js
+++ b/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js
@@ -1,5 +1,6 @@
import { GlModal, GlFormCheckbox } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import { initEmojiMock, clearEmojiMock } from 'helpers/emoji';
import * as UserApi from '~/api/user_api';
import EmojiPicker from '~/emoji/components/picker.vue';
@@ -48,7 +49,7 @@ describe('SetStatusModalWrapper', () => {
const findAvailabilityCheckbox = () => wrapper.find(GlFormCheckbox);
const findClearStatusAtMessage = () => wrapper.find('[data-testid="clear-status-at-message"]');
- const initModal = ({ mockOnUpdateSuccess = true, mockOnUpdateFailure = true } = {}) => {
+ const initModal = async ({ mockOnUpdateSuccess = true, mockOnUpdateFailure = true } = {}) => {
const modal = findModal();
// mock internal emoji methods
wrapper.vm.showEmojiMenu = jest.fn();
@@ -57,7 +58,7 @@ describe('SetStatusModalWrapper', () => {
if (mockOnUpdateFailure) wrapper.vm.onUpdateFail = jest.fn();
modal.vm.$emit('shown');
- return wrapper.vm.$nextTick();
+ await nextTick();
};
afterEach(() => {
@@ -207,7 +208,7 @@ describe('SetStatusModalWrapper', () => {
it('clicking "removeStatus" clears the emoji and message fields', async () => {
findModal().vm.$emit('secondary');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findFormField('message').element.value).toBe('');
expect(findFormField('emoji').element.value).toBe('');
@@ -215,7 +216,7 @@ describe('SetStatusModalWrapper', () => {
it('clicking "setStatus" submits the user status', async () => {
findModal().vm.$emit('primary');
- await wrapper.vm.$nextTick();
+ await nextTick();
// set the availability status
findAvailabilityCheckbox().vm.$emit('input', true);
@@ -224,7 +225,7 @@ describe('SetStatusModalWrapper', () => {
wrapper.find('[data-testid="thirtyMinutes"]').vm.$emit('click');
findModal().vm.$emit('primary');
- await wrapper.vm.$nextTick();
+ await nextTick();
const commonParams = {
emoji: defaultEmoji,
@@ -246,7 +247,7 @@ describe('SetStatusModalWrapper', () => {
it('calls the "onUpdateSuccess" handler', async () => {
findModal().vm.$emit('primary');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.vm.onUpdateSuccess).toHaveBeenCalled();
});
@@ -262,7 +263,7 @@ describe('SetStatusModalWrapper', () => {
it('displays a toast success message', async () => {
findModal().vm.$emit('primary');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect($toast.show).toHaveBeenCalledWith('Status updated');
});
@@ -279,7 +280,7 @@ describe('SetStatusModalWrapper', () => {
it('calls the "onUpdateFail" handler', async () => {
findModal().vm.$emit('primary');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.vm.onUpdateFail).toHaveBeenCalled();
});
@@ -295,7 +296,7 @@ describe('SetStatusModalWrapper', () => {
it('flashes an error message', async () => {
findModal().vm.$emit('primary');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(createFlash).toHaveBeenCalledWith({
message: "Sorry, we weren't able to set your status. Please try again later.",
diff --git a/spec/frontend/settings_panels_spec.js b/spec/frontend/settings_panels_spec.js
index 6b739617b97..3a62cd703ab 100644
--- a/spec/frontend/settings_panels_spec.js
+++ b/spec/frontend/settings_panels_spec.js
@@ -24,6 +24,20 @@ describe('Settings Panels', () => {
expect(isExpanded(panel)).toBe(true);
});
+
+ it('should expand panel containing linked hash', () => {
+ window.location.hash = '#group_description';
+
+ const panel = document.querySelector('#js-general-settings');
+ // Our test environment automatically expands everything so we need to clear that out first
+ panel.classList.remove('expanded');
+
+ expect(isExpanded(panel)).toBe(false);
+
+ initSettingsPanels();
+
+ expect(isExpanded(panel)).toBe(true);
+ });
});
it('does not change the text content of triggers', () => {
diff --git a/spec/frontend/sidebar/assignees_realtime_spec.js b/spec/frontend/sidebar/assignees_realtime_spec.js
index ecf33d6de37..2249a1c08b8 100644
--- a/spec/frontend/sidebar/assignees_realtime_spec.js
+++ b/spec/frontend/sidebar/assignees_realtime_spec.js
@@ -1,4 +1,5 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import AssigneesRealtime from '~/sidebar/components/assignees/assignees_realtime.vue';
@@ -7,8 +8,7 @@ import SidebarMediator from '~/sidebar/sidebar_mediator';
import getIssueAssigneesQuery from '~/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql';
import Mock, { issuableQueryResponse, subscriptionNullResponse } from './mock_data';
-const localVue = createLocalVue();
-localVue.use(VueApollo);
+Vue.use(VueApollo);
describe('Assignees Realtime', () => {
let wrapper;
@@ -38,7 +38,6 @@ describe('Assignees Realtime', () => {
mediator,
},
apolloProvider: fakeApollo,
- localVue,
});
};
diff --git a/spec/frontend/sidebar/assignees_spec.js b/spec/frontend/sidebar/assignees_spec.js
index b3a67f18f82..a4474ead956 100644
--- a/spec/frontend/sidebar/assignees_spec.js
+++ b/spec/frontend/sidebar/assignees_spec.js
@@ -1,5 +1,6 @@
import { GlIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import { trimText } from 'helpers/text_helper';
import UsersMockHelper from 'helpers/user_mock_data_helper';
import Assignee from '~/sidebar/components/assignees/assignees.vue';
@@ -59,7 +60,7 @@ describe('Assignee component', () => {
expect(componentTextNoUsers).toContain('assign yourself');
});
- it('emits the assign-self event when "assign yourself" is clicked', () => {
+ it('emits the assign-self event when "assign yourself" is clicked', async () => {
createWrapper({
...getDefaultProps(),
editable: true,
@@ -68,9 +69,8 @@ describe('Assignee component', () => {
jest.spyOn(wrapper.vm, '$emit');
wrapper.find('[data-testid="assign-yourself"]').trigger('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted('assign-self')).toBeTruthy();
- });
+ await nextTick();
+ expect(wrapper.emitted('assign-self')).toBeTruthy();
});
});
diff --git a/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js b/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js
index 07da4acef8c..def46255994 100644
--- a/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js
+++ b/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js
@@ -1,6 +1,7 @@
import { GlSearchBoxByType, GlDropdown } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
+
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -23,8 +24,7 @@ const updateIssueAssigneesMutationSuccess = jest
.mockResolvedValue(updateIssueAssigneesMutationResponse);
const mockError = jest.fn().mockRejectedValue('Error!');
-const localVue = createLocalVue();
-localVue.use(VueApollo);
+Vue.use(VueApollo);
const initialAssignees = [
{
@@ -59,7 +59,6 @@ describe('Sidebar assignees widget', () => {
[updateIssueAssigneesMutation, updateIssueAssigneesMutationHandler],
]);
wrapper = shallowMount(SidebarAssigneesWidget, {
- localVue,
apolloProvider: fakeApollo,
propsData: {
iid: '1',
@@ -138,9 +137,17 @@ describe('Sidebar assignees widget', () => {
createComponent();
await waitForPromises();
- expect(findAssignees().props('users')).toEqual(
- issuableQueryResponse.data.workspace.issuable.assignees.nodes,
- );
+ expect(findAssignees().props('users')).toEqual([
+ {
+ id: 'gid://gitlab/User/2',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/a95e5b71488f4b9d69ce5ff58bfd28d6?s=80\u0026d=identicon',
+ name: 'Jacki Kub',
+ username: 'francina.skiles',
+ webUrl: '/franc',
+ status: null,
+ },
+ ]);
});
it('renders an error when issuable query is rejected', async () => {
@@ -196,7 +203,7 @@ describe('Sidebar assignees widget', () => {
{
assignees: [
{
- __typename: 'User',
+ __typename: 'UserCore',
avatarUrl:
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
id: 'gid://gitlab/User/1',
@@ -322,9 +329,10 @@ describe('Sidebar assignees widget', () => {
});
describe('when user is not signed in', () => {
- beforeEach(() => {
+ beforeEach(async () => {
gon.current_username = undefined;
createComponent();
+ await waitForPromises();
});
it('passes signedIn prop as false to IssuableAssignees', () => {
@@ -353,6 +361,7 @@ describe('Sidebar assignees widget', () => {
describe('when making changes to participants list', () => {
beforeEach(async () => {
createComponent();
+ await waitForPromises();
});
it('passes falsy `isDirty` prop to editable item if no changes to selected users were made', () => {
diff --git a/spec/frontend/sidebar/components/assignees/sidebar_editable_item_spec.js b/spec/frontend/sidebar/components/assignees/sidebar_editable_item_spec.js
index 84b192aaf41..c870bbecd76 100644
--- a/spec/frontend/sidebar/components/assignees/sidebar_editable_item_spec.js
+++ b/spec/frontend/sidebar/components/assignees/sidebar_editable_item_spec.js
@@ -1,5 +1,6 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
describe('boards sidebar remove issue', () => {
@@ -71,7 +72,7 @@ describe('boards sidebar remove issue', () => {
createComponent({ canUpdate: true, slots });
findEditButton().vm.$emit('click');
- await wrapper.vm.$nextTick;
+ await nextTick;
expect(findCollapsed().isVisible()).toBe(false);
expect(findExpanded().isVisible()).toBe(true);
@@ -82,14 +83,14 @@ describe('boards sidebar remove issue', () => {
beforeEach(async () => {
createComponent({ canUpdate: true });
findEditButton().vm.$emit('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
});
it('hides expanded section and displays collapsed section', async () => {
expect(findExpanded().isVisible()).toBe(true);
document.body.click();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findCollapsed().isVisible()).toBe(true);
expect(findExpanded().isVisible()).toBe(false);
@@ -101,7 +102,7 @@ describe('boards sidebar remove issue', () => {
findEditButton().vm.$emit('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.emitted().open.length).toBe(1);
});
@@ -111,7 +112,7 @@ describe('boards sidebar remove issue', () => {
findEditButton().vm.$emit('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
wrapper.vm.collapse({ emitEvent: false });
diff --git a/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js b/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js
index c72c23a3a60..90aae85e1ca 100644
--- a/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js
+++ b/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js
@@ -1,4 +1,5 @@
import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import { TEST_HOST } from 'helpers/test_constants';
import UsersMockHelper from 'helpers/user_mock_data_helper';
import AssigneeAvatarLink from '~/sidebar/components/assignees/assignee_avatar_link.vue';
@@ -84,10 +85,10 @@ describe('UncollapsedAssigneeList component', () => {
});
describe('when more button is clicked', () => {
- beforeEach(() => {
+ beforeEach(async () => {
findMoreButton().trigger('click');
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('shows "show less" label', () => {
diff --git a/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_widget_spec.js b/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_widget_spec.js
index 707215d0739..1de71e52264 100644
--- a/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_widget_spec.js
+++ b/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_widget_spec.js
@@ -1,5 +1,6 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
+
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -15,8 +16,7 @@ import { issueConfidentialityResponse } from '../../mock_data';
jest.mock('~/flash');
-const localVue = createLocalVue();
-localVue.use(VueApollo);
+Vue.use(VueApollo);
describe('Sidebar Confidentiality Widget', () => {
let wrapper;
@@ -32,7 +32,6 @@ describe('Sidebar Confidentiality Widget', () => {
fakeApollo = createMockApollo([[issueConfidentialQuery, confidentialQueryHandler]]);
wrapper = shallowMount(SidebarConfidentialityWidget, {
- localVue,
apolloProvider: fakeApollo,
provide: {
canUpdate: true,
diff --git a/spec/frontend/sidebar/components/mock_data.js b/spec/frontend/sidebar/components/mock_data.js
index 70c3f8a3012..a9a00b3cfdf 100644
--- a/spec/frontend/sidebar/components/mock_data.js
+++ b/spec/frontend/sidebar/components/mock_data.js
@@ -1,6 +1,7 @@
export const getIssueCrmContactsQueryResponse = {
data: {
issue: {
+ __typename: 'Issue',
id: 'gid://gitlab/Issue/123',
customerRelationsContacts: {
nodes: [
@@ -37,6 +38,7 @@ export const issueCrmContactsUpdateNullResponse = {
export const issueCrmContactsUpdateResponse = {
data: {
issueCrmContactsUpdated: {
+ __typename: 'Issue',
id: 'gid://gitlab/Issue/123',
customerRelationsContacts: {
nodes: [
diff --git a/spec/frontend/sidebar/components/participants/sidebar_participants_widget_spec.js b/spec/frontend/sidebar/components/participants/sidebar_participants_widget_spec.js
index 859e63b3df6..338ecf944f3 100644
--- a/spec/frontend/sidebar/components/participants/sidebar_participants_widget_spec.js
+++ b/spec/frontend/sidebar/components/participants/sidebar_participants_widget_spec.js
@@ -1,6 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
+import { stripTypenames } from 'helpers/graphql_helpers';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import Participants from '~/sidebar/components/participants/participants.vue';
@@ -66,9 +67,11 @@ describe('Sidebar Participants Widget', () => {
});
it('passes participants to child component', () => {
- expect(findParticipants().props('participants')).toEqual(
+ const participantsWithoutTypename = stripTypenames(
epicParticipantsResponse().data.workspace.issuable.participants.nodes,
);
+
+ expect(findParticipants().props('participants')).toEqual(participantsWithoutTypename);
});
});
diff --git a/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js b/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js
index 6116bc68927..5d80a221d8e 100644
--- a/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js
+++ b/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js
@@ -97,14 +97,14 @@ describe('SidebarSeverity', () => {
});
});
- it('shows error alert when severity update fails ', () => {
+ it('shows error alert when severity update fails ', async () => {
const errorMsg = 'Something went wrong';
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValueOnce(errorMsg);
findCriticalSeverityDropdownItem().vm.$emit('click');
- setImmediate(() => {
- expect(createFlash).toHaveBeenCalled();
- });
+ await waitForPromises();
+
+ expect(createFlash).toHaveBeenCalled();
});
it('shows loading icon while updating', async () => {
diff --git a/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js b/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js
index d7471d99477..3ddd41c0bd4 100644
--- a/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js
+++ b/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js
@@ -8,9 +8,9 @@ import {
GlLoadingIcon,
} from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
-import { createLocalVue, shallowMount, mount } from '@vue/test-utils';
+import { shallowMount, mount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
-
import createMockApollo from 'helpers/mock_apollo_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
@@ -37,8 +37,6 @@ import {
jest.mock('~/flash');
-const localVue = createLocalVue();
-
describe('SidebarDropdownWidget', () => {
let wrapper;
let mockApollo;
@@ -78,7 +76,7 @@ describe('SidebarDropdownWidget', () => {
// It then emits `shown` event in a watcher for `visible`
// Hence we need both of these:
await waitForPromises();
- await wrapper.vm.$nextTick();
+ await nextTick();
};
const waitForApollo = async () => {
@@ -108,7 +106,7 @@ describe('SidebarDropdownWidget', () => {
projectMilestonesSpy = jest.fn().mockResolvedValue(mockProjectMilestonesResponse),
currentMilestoneSpy = jest.fn().mockResolvedValue(noCurrentMilestoneResponse),
} = {}) => {
- localVue.use(VueApollo);
+ Vue.use(VueApollo);
mockApollo = createMockApollo([
[projectMilestonesQuery, projectMilestonesSpy],
[projectIssueMilestoneQuery, currentMilestoneSpy],
@@ -117,7 +115,6 @@ describe('SidebarDropdownWidget', () => {
wrapper = extendedWrapper(
mount(SidebarDropdownWidget, {
- localVue,
provide: { canUpdate: true },
apolloProvider: mockApollo,
propsData: {
@@ -354,7 +351,7 @@ describe('SidebarDropdownWidget', () => {
});
it(`calls createFlash with "${expectedMsg}"`, async () => {
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(createFlash).toHaveBeenCalledWith({
message: expectedMsg,
captureError: true,
@@ -377,7 +374,7 @@ describe('SidebarDropdownWidget', () => {
findSearchBox().vm.$emit('input', 'non existing milestones');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findDropdownText().text()).toBe('No milestone found');
});
@@ -482,7 +479,7 @@ describe('SidebarDropdownWidget', () => {
it('sends a projectMilestones query with the entered search term "foo"', async () => {
findSearchBox().vm.$emit('input', mockSearchTerm);
- await wrapper.vm.$nextTick();
+ await nextTick();
// Account for debouncing
jest.runAllTimers();
diff --git a/spec/frontend/sidebar/components/time_tracking/report_spec.js b/spec/frontend/sidebar/components/time_tracking/report_spec.js
index 64d143615a0..2b17e6dd6c3 100644
--- a/spec/frontend/sidebar/components/time_tracking/report_spec.js
+++ b/spec/frontend/sidebar/components/time_tracking/report_spec.js
@@ -1,6 +1,7 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { getAllByRole, getByRole } from '@testing-library/dom';
-import { shallowMount, createLocalVue, mount } from '@vue/test-utils';
+import { shallowMount, mount } from '@vue/test-utils';
+import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -13,8 +14,7 @@ import { getIssueTimelogsQueryResponse, getMrTimelogsQueryResponse } from './moc
jest.mock('~/flash');
describe('Issuable Time Tracking Report', () => {
- const localVue = createLocalVue();
- localVue.use(VueApollo);
+ Vue.use(VueApollo);
let wrapper;
let fakeApollo;
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
@@ -37,7 +37,6 @@ describe('Issuable Time Tracking Report', () => {
issuableType,
},
propsData: { limitToHours, issuableId: '1' },
- localVue,
apolloProvider: fakeApollo,
});
};
diff --git a/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js b/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js
index eb202a8cfcc..835e700e63c 100644
--- a/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js
+++ b/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js
@@ -1,5 +1,6 @@
import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import { stubTransition } from 'helpers/stub_transition';
import { createMockDirective } from 'helpers/vue_mock_directive';
import TimeTracker from '~/sidebar/components/time_tracking/time_tracker.vue';
@@ -161,7 +162,7 @@ describe('Issuable Time Tracker', () => {
it('should show the correct tooltip text', async () => {
expect(findByTestId('timeTrackingComparisonPane').exists()).toBe(true);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findComparisonMeter()).toBe('Time remaining: 26h 23m');
});
@@ -179,7 +180,7 @@ describe('Issuable Time Tracker', () => {
},
},
});
- await wrapper.vm.$nextTick();
+ await nextTick();
});
it('should display the human readable version of time estimated', () => {
@@ -282,7 +283,7 @@ describe('Issuable Time Tracker', () => {
},
},
});
- await wrapper.vm.$nextTick();
+ await nextTick();
});
it('should not show the "Help" pane by default', () => {
@@ -292,19 +293,19 @@ describe('Issuable Time Tracker', () => {
it('should show the "Help" pane when help button is clicked', async () => {
findHelpButton().trigger('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findByTestId('helpPane').exists()).toBe(true);
});
it('should not show the "Help" pane when help button is clicked and then closed', async () => {
findHelpButton().trigger('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findByTestId('helpPane').exists()).toBe(true);
findCloseHelpButton().trigger('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findByTestId('helpPane').exists()).toBe(false);
});
@@ -315,7 +316,7 @@ describe('Issuable Time Tracker', () => {
it('refetches issuableTimeTracking query when eventHub emits `timeTracker:refresh` event', async () => {
SidebarEventHub.$emit('timeTracker:refresh');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(issuableTimeTrackingRefetchSpy).toHaveBeenCalled();
});
diff --git a/spec/frontend/sidebar/components/todo_toggle/sidebar_todo_widget_spec.js b/spec/frontend/sidebar/components/todo_toggle/sidebar_todo_widget_spec.js
index 23f1753c4bf..ea931782d1e 100644
--- a/spec/frontend/sidebar/components/todo_toggle/sidebar_todo_widget_spec.js
+++ b/spec/frontend/sidebar/components/todo_toggle/sidebar_todo_widget_spec.js
@@ -1,6 +1,6 @@
-import { GlIcon } from '@gitlab/ui';
+import { GlIcon, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -103,7 +103,7 @@ describe('Sidebar Todo Widget', () => {
});
it('sets default tooltip title', () => {
- expect(wrapper.find(GlIcon).attributes('title')).toBe('Add a to do');
+ expect(wrapper.find(GlButton).attributes('title')).toBe('Add a to do');
});
it('when user has a to do', async () => {
@@ -113,13 +113,13 @@ describe('Sidebar Todo Widget', () => {
await waitForPromises();
expect(wrapper.find(GlIcon).props('name')).toBe('todo-done');
- expect(wrapper.find(GlIcon).attributes('title')).toBe('Mark as done');
+ expect(wrapper.find(GlButton).attributes('title')).toBe('Mark as done');
});
it('emits `todoUpdated` event on click on icon', async () => {
wrapper.find(GlIcon).vm.$emit('click', event);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.emitted('todoUpdated')).toEqual([[false]]);
});
});
diff --git a/spec/frontend/sidebar/lock/edit_form_buttons_spec.js b/spec/frontend/sidebar/lock/edit_form_buttons_spec.js
index 1673425947e..971744edb0f 100644
--- a/spec/frontend/sidebar/lock/edit_form_buttons_spec.js
+++ b/spec/frontend/sidebar/lock/edit_form_buttons_spec.js
@@ -1,4 +1,5 @@
import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import createFlash from '~/flash';
import { createStore as createMrStore } from '~/mr_notes/stores';
import createStore from '~/notes/stores';
@@ -118,15 +119,13 @@ describe('EditFormButtons', () => {
});
it('resets loading', async () => {
- await wrapper.vm.$nextTick().then(() => {
- expect(findLockToggle().props('loading')).toBe(false);
- });
+ await nextTick();
+ expect(findLockToggle().props('loading')).toBe(false);
});
- it('emits close form', () => {
- return wrapper.vm.$nextTick().then(() => {
- expect(eventHub.$emit).toHaveBeenCalledWith('closeLockForm');
- });
+ it('emits close form', async () => {
+ await nextTick();
+ expect(eventHub.$emit).toHaveBeenCalledWith('closeLockForm');
});
it('does not flash an error message', () => {
@@ -153,15 +152,13 @@ describe('EditFormButtons', () => {
});
it('resets loading', async () => {
- await wrapper.vm.$nextTick().then(() => {
- expect(findLockToggle().props('loading')).toBe(false);
- });
+ await nextTick();
+ expect(findLockToggle().props('loading')).toBe(false);
});
- it('emits close form', () => {
- return wrapper.vm.$nextTick().then(() => {
- expect(eventHub.$emit).toHaveBeenCalledWith('closeLockForm');
- });
+ it('emits close form', async () => {
+ await nextTick();
+ expect(eventHub.$emit).toHaveBeenCalledWith('closeLockForm');
});
it('calls flash with the correct message', () => {
diff --git a/spec/frontend/sidebar/lock/issuable_lock_form_spec.js b/spec/frontend/sidebar/lock/issuable_lock_form_spec.js
index 1743e114bb0..7bf7e563a01 100644
--- a/spec/frontend/sidebar/lock/issuable_lock_form_spec.js
+++ b/spec/frontend/sidebar/lock/issuable_lock_form_spec.js
@@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import { mockTracking, triggerEvent } from 'helpers/tracking_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { createStore as createMrStore } from '~/mr_notes/stores';
@@ -80,13 +81,12 @@ describe('IssuableLockForm', () => {
});
describe('when not editable', () => {
- it('does not display the edit form when opened if not editable', () => {
+ it('does not display the edit form when opened if not editable', async () => {
expect(findEditForm().exists()).toBe(false);
findSidebarCollapseIcon().trigger('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(findEditForm().exists()).toBe(false);
- });
+ await nextTick();
+ expect(findEditForm().exists()).toBe(false);
});
});
@@ -102,13 +102,12 @@ describe('IssuableLockForm', () => {
});
describe("when 'Edit' is clicked", () => {
- it('displays the edit form when editable', () => {
+ it('displays the edit form when editable', async () => {
expect(findEditForm().exists()).toBe(false);
findEditLink().trigger('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(findEditForm().exists()).toBe(true);
- });
+ await nextTick();
+ expect(findEditForm().exists()).toBe(true);
});
it('tracks the event ', () => {
@@ -123,13 +122,12 @@ describe('IssuableLockForm', () => {
});
describe('When sidebar is collapsed', () => {
- it('displays the edit form when opened', () => {
+ it('displays the edit form when opened', async () => {
expect(findEditForm().exists()).toBe(false);
findSidebarCollapseIcon().trigger('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(findEditForm().exists()).toBe(true);
- });
+ await nextTick();
+ expect(findEditForm().exists()).toBe(true);
});
it('renders a tooltip with the lock status text', () => {
diff --git a/spec/frontend/sidebar/mock_data.js b/spec/frontend/sidebar/mock_data.js
index 42e89a3ba84..30972484a08 100644
--- a/spec/frontend/sidebar/mock_data.js
+++ b/spec/frontend/sidebar/mock_data.js
@@ -276,6 +276,7 @@ export const epicParticipantsResponse = () => ({
participants: {
nodes: [
{
+ __typename: 'UserCore',
id: 'gid://gitlab/User/2',
avatarUrl:
'https://www.gravatar.com/avatar/a95e5b71488f4b9d69ce5ff58bfd28d6?s=80\u0026d=identicon',
@@ -332,6 +333,7 @@ export const issuableQueryResponse = {
assignees: {
nodes: [
{
+ __typename: 'UserCore',
id: 'gid://gitlab/User/2',
avatarUrl:
'https://www.gravatar.com/avatar/a95e5b71488f4b9d69ce5ff58bfd28d6?s=80\u0026d=identicon',
@@ -389,7 +391,7 @@ export const updateIssueAssigneesMutationResponse = {
assignees: {
nodes: [
{
- __typename: 'User',
+ __typename: 'UserCore',
id: 'gid://gitlab/User/1',
avatarUrl:
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
@@ -414,6 +416,7 @@ export const subscriptionNullResponse = {
};
const mockUser1 = {
+ __typename: 'UserCore',
id: 'gid://gitlab/User/1',
avatarUrl:
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
@@ -424,6 +427,7 @@ const mockUser1 = {
};
export const mockUser2 = {
+ __typename: 'UserCore',
id: 'gid://gitlab/User/4',
avatarUrl: '/avatar2',
name: 'rookie',
@@ -470,6 +474,7 @@ export const projectMembersResponse = {
{
id: 'user-4',
user: {
+ __typename: 'UserCore',
id: 'gid://gitlab/User/2',
avatarUrl:
'https://www.gravatar.com/avatar/a95e5b71488f4b9d69ce5ff58bfd28d6?s=80\u0026d=identicon',
@@ -503,6 +508,7 @@ export const groupMembersResponse = {
{
id: 'user-3',
user: {
+ __typename: 'UserCore',
id: 'gid://gitlab/User/2',
avatarUrl:
'https://www.gravatar.com/avatar/a95e5b71488f4b9d69ce5ff58bfd28d6?s=80\u0026d=identicon',
@@ -535,6 +541,7 @@ export const participantsQueryResponse = {
mockUser1,
mockUser1,
{
+ __typename: 'UserCore',
id: 'gid://gitlab/User/2',
avatarUrl:
'https://www.gravatar.com/avatar/a95e5b71488f4b9d69ce5ff58bfd28d6?s=80\u0026d=identicon',
@@ -546,6 +553,7 @@ export const participantsQueryResponse = {
},
},
{
+ __typename: 'UserCore',
id: 'gid://gitlab/User/3',
avatarUrl: '/avatar',
name: 'John Doe',
diff --git a/spec/frontend/sidebar/participants_spec.js b/spec/frontend/sidebar/participants_spec.js
index 94cdbe7f2ef..356628849d9 100644
--- a/spec/frontend/sidebar/participants_spec.js
+++ b/spec/frontend/sidebar/participants_spec.js
@@ -1,6 +1,6 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
+import { nextTick } from 'vue';
import Participants from '~/sidebar/components/participants/participants.vue';
const PARTICIPANT = {
@@ -77,7 +77,7 @@ describe('Participants', () => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
- it('when only showing visible participants, shows an avatar only for each participant under the limit', () => {
+ it('when only showing visible participants, shows an avatar only for each participant under the limit', async () => {
const numberOfLessParticipants = 2;
wrapper = mountComponent({
loading: false,
@@ -91,12 +91,11 @@ describe('Participants', () => {
isShowingMoreParticipants: false,
});
- return Vue.nextTick().then(() => {
- expect(wrapper.findAll('.participants-author')).toHaveLength(numberOfLessParticipants);
- });
+ await nextTick();
+ expect(wrapper.findAll('.participants-author')).toHaveLength(numberOfLessParticipants);
});
- it('when only showing all participants, each has an avatar', () => {
+ it('when only showing all participants, each has an avatar', async () => {
wrapper = mountComponent({
loading: false,
participants: PARTICIPANT_LIST,
@@ -109,9 +108,8 @@ describe('Participants', () => {
isShowingMoreParticipants: true,
});
- return Vue.nextTick().then(() => {
- expect(wrapper.findAll('.participants-author')).toHaveLength(PARTICIPANT_LIST.length);
- });
+ await nextTick();
+ expect(wrapper.findAll('.participants-author')).toHaveLength(PARTICIPANT_LIST.length);
});
it('does not have more participants link when they can all be shown', () => {
@@ -126,7 +124,7 @@ describe('Participants', () => {
expect(getMoreParticipantsButton().exists()).toBe(false);
});
- it('when too many participants, has more participants link to show more', () => {
+ it('when too many participants, has more participants link to show more', async () => {
wrapper = mountComponent({
loading: false,
participants: PARTICIPANT_LIST,
@@ -139,12 +137,11 @@ describe('Participants', () => {
isShowingMoreParticipants: false,
});
- return Vue.nextTick().then(() => {
- expect(getMoreParticipantsButton().text()).toBe('+ 1 more');
- });
+ await nextTick();
+ expect(getMoreParticipantsButton().text()).toBe('+ 1 more');
});
- it('when too many participants and already showing them, has more participants link to show less', () => {
+ it('when too many participants and already showing them, has more participants link to show less', async () => {
wrapper = mountComponent({
loading: false,
participants: PARTICIPANT_LIST,
@@ -157,9 +154,8 @@ describe('Participants', () => {
isShowingMoreParticipants: true,
});
- return Vue.nextTick().then(() => {
- expect(getMoreParticipantsButton().text()).toBe('- show less');
- });
+ await nextTick();
+ expect(getMoreParticipantsButton().text()).toBe('- show less');
});
it('clicking more participants link emits event', () => {
@@ -176,7 +172,7 @@ describe('Participants', () => {
expect(wrapper.vm.isShowingMoreParticipants).toBe(true);
});
- it('clicking on participants icon emits `toggleSidebar` event', () => {
+ it('clicking on participants icon emits `toggleSidebar` event', async () => {
wrapper = mountComponent({
loading: false,
participants: PARTICIPANT_LIST,
@@ -187,11 +183,9 @@ describe('Participants', () => {
wrapper.find('.sidebar-collapsed-icon').trigger('click');
- return Vue.nextTick(() => {
- expect(spy).toHaveBeenCalledWith('toggleSidebar');
-
- spy.mockRestore();
- });
+ await nextTick();
+ expect(spy).toHaveBeenCalledWith('toggleSidebar');
+ spy.mockRestore();
});
});
diff --git a/spec/frontend/sidebar/sidebar_assignees_spec.js b/spec/frontend/sidebar/sidebar_assignees_spec.js
index dc121dcb897..5f77e21c1f8 100644
--- a/spec/frontend/sidebar/sidebar_assignees_spec.js
+++ b/spec/frontend/sidebar/sidebar_assignees_spec.js
@@ -1,6 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import axios from 'axios';
import AxiosMockAdapter from 'axios-mock-adapter';
+import { nextTick } from 'vue';
import Assigness from '~/sidebar/components/assignees/assignees.vue';
import AssigneesRealtime from '~/sidebar/components/assignees/assignees_realtime.vue';
import SidebarAssignees from '~/sidebar/components/assignees/sidebar_assignees.vue';
@@ -74,16 +75,15 @@ describe('sidebar assignees', () => {
expect(mediator.store.assignees.length).toBe(1);
});
- it('hides assignees until fetched', () => {
+ it('hides assignees until fetched', async () => {
createComponent();
expect(wrapper.find(Assigness).exists()).toBe(false);
wrapper.vm.store.isFetching.assignees = false;
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.find(Assigness).exists()).toBe(true);
- });
+ await nextTick();
+ expect(wrapper.find(Assigness).exists()).toBe(true);
});
describe('when realTimeIssueSidebar is turned on', () => {
diff --git a/spec/frontend/sidebar/sidebar_move_issue_spec.js b/spec/frontend/sidebar/sidebar_move_issue_spec.js
index d9972ae75c3..7bb7b18adf8 100644
--- a/spec/frontend/sidebar/sidebar_move_issue_spec.js
+++ b/spec/frontend/sidebar/sidebar_move_issue_spec.js
@@ -1,5 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
+import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import SidebarMoveIssue from '~/sidebar/lib/sidebar_move_issue';
@@ -77,15 +78,14 @@ describe('SidebarMoveIssue', () => {
expect(test.sidebarMoveIssue.$dropdownToggle.data('deprecatedJQueryDropdown')).toBeTruthy();
});
- it('escapes html from project name', (done) => {
+ it('escapes html from project name', async () => {
test.$toggleButton.dropdown('toggle');
- setImmediate(() => {
- expect(test.$content.find('.js-move-issue-dropdown-item')[1].innerHTML.trim()).toEqual(
- '&lt;img src=x onerror=alert(document.domain)&gt; foo / bar',
- );
- done();
- });
+ await waitForPromises();
+
+ expect(test.$content.find('.js-move-issue-dropdown-item')[1].innerHTML.trim()).toEqual(
+ '&lt;img src=x onerror=alert(document.domain)&gt; foo / bar',
+ );
});
});
@@ -101,20 +101,20 @@ describe('SidebarMoveIssue', () => {
expect(test.$confirmButton.hasClass('is-loading')).toBe(true);
});
- it('should remove loading state from confirm button on failure', (done) => {
+ it('should remove loading state from confirm button on failure', async () => {
jest.spyOn(test.mediator, 'moveIssue').mockReturnValue(Promise.reject());
test.mediator.setMoveToProjectId(7);
test.sidebarMoveIssue.onConfirmClicked();
expect(test.mediator.moveIssue).toHaveBeenCalled();
+
// Wait for the move issue request to fail
- setImmediate(() => {
- expect(createFlash).toHaveBeenCalled();
- expect(test.$confirmButton.prop('disabled')).toBeFalsy();
- expect(test.$confirmButton.hasClass('is-loading')).toBe(false);
- done();
- });
+ await waitForPromises();
+
+ expect(createFlash).toHaveBeenCalled();
+ expect(test.$confirmButton.prop('disabled')).toBeFalsy();
+ expect(test.$confirmButton.hasClass('is-loading')).toBe(false);
});
it('should not move the issue with id=0', () => {
@@ -127,35 +127,33 @@ describe('SidebarMoveIssue', () => {
});
});
- it('should set moveToProjectId on dropdown item "No project" click', (done) => {
+ it('should set moveToProjectId on dropdown item "No project" click', async () => {
jest.spyOn(test.mediator, 'setMoveToProjectId').mockImplementation(() => {});
// Open the dropdown
test.$toggleButton.dropdown('toggle');
// Wait for the autocomplete request to finish
- setImmediate(() => {
- test.$content.find('.js-move-issue-dropdown-item').eq(0).trigger('click');
+ await waitForPromises();
- expect(test.mediator.setMoveToProjectId).toHaveBeenCalledWith(0);
- expect(test.$confirmButton.prop('disabled')).toBeTruthy();
- done();
- });
+ test.$content.find('.js-move-issue-dropdown-item').eq(0).trigger('click');
+
+ expect(test.mediator.setMoveToProjectId).toHaveBeenCalledWith(0);
+ expect(test.$confirmButton.prop('disabled')).toBeTruthy();
});
- it('should set moveToProjectId on dropdown item click', (done) => {
+ it('should set moveToProjectId on dropdown item click', async () => {
jest.spyOn(test.mediator, 'setMoveToProjectId').mockImplementation(() => {});
// Open the dropdown
test.$toggleButton.dropdown('toggle');
// Wait for the autocomplete request to finish
- setImmediate(() => {
- test.$content.find('.js-move-issue-dropdown-item').eq(1).trigger('click');
+ await waitForPromises();
- expect(test.mediator.setMoveToProjectId).toHaveBeenCalledWith(20);
- expect(test.$confirmButton.attr('disabled')).toBe(undefined);
- done();
- });
+ test.$content.find('.js-move-issue-dropdown-item').eq(1).trigger('click');
+
+ expect(test.mediator.setMoveToProjectId).toHaveBeenCalledWith(20);
+ expect(test.$confirmButton.attr('disabled')).toBe(undefined);
});
});
diff --git a/spec/frontend/sidebar/todo_spec.js b/spec/frontend/sidebar/todo_spec.js
index 6829e688c65..9316268d2ad 100644
--- a/spec/frontend/sidebar/todo_spec.js
+++ b/spec/frontend/sidebar/todo_spec.js
@@ -1,6 +1,7 @@
import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import SidebarTodos from '~/sidebar/components/todo_toggle/todo.vue';
const defaultProps = {
@@ -49,13 +50,12 @@ describe('SidebarTodo', () => {
);
describe('template', () => {
- it('emits `toggleTodo` event when clicked on button', () => {
+ it('emits `toggleTodo` event when clicked on button', async () => {
createComponent();
wrapper.find('button').trigger('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted().toggleTodo).toBeTruthy();
- });
+ await nextTick();
+ expect(wrapper.emitted().toggleTodo).toBeTruthy();
});
it('renders component container element with proper data attributes', () => {
diff --git a/spec/frontend/snippets/components/edit_spec.js b/spec/frontend/snippets/components/edit_spec.js
index 80a8b8ec489..61424fa1eb2 100644
--- a/spec/frontend/snippets/components/edit_spec.js
+++ b/spec/frontend/snippets/components/edit_spec.js
@@ -1,7 +1,8 @@
import { GlLoadingIcon } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import { merge } from 'lodash';
-import { nextTick } from 'vue';
+
import VueApollo, { ApolloMutation } from 'vue-apollo';
import { useFakeDate } from 'helpers/fake_date';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -78,8 +79,7 @@ const getApiData = ({
blobActions: [],
});
-const localVue = createLocalVue();
-localVue.use(VueApollo);
+Vue.use(VueApollo);
describe('Snippet Edit app', () => {
useFakeDate();
@@ -141,7 +141,6 @@ describe('Snippet Edit app', () => {
wrapper = shallowMount(SnippetEditApp, {
apolloProvider,
- localVue,
stubs: {
ApolloMutation,
FormFooterActions,
@@ -330,6 +329,7 @@ describe('Snippet Edit app', () => {
mutateSpy.mockRejectedValue(TEST_API_ERROR);
await createComponentAndSubmit();
+ await nextTick();
});
it('should not redirect', () => {
@@ -339,7 +339,7 @@ describe('Snippet Edit app', () => {
it('should flash', () => {
// Apollo automatically wraps the resolver's error in a NetworkError
expect(createFlash).toHaveBeenCalledWith({
- message: `Can't update snippet: Network error: ${TEST_API_ERROR.message}`,
+ message: `Can't update snippet: ${TEST_API_ERROR.message}`,
});
});
@@ -349,7 +349,7 @@ describe('Snippet Edit app', () => {
// eslint-disable-next-line no-console
expect(console.error).toHaveBeenCalledWith(
'[gitlab] unexpected error while updating snippet',
- expect.objectContaining({ message: `Network error: ${TEST_API_ERROR.message}` }),
+ expect.objectContaining({ message: `${TEST_API_ERROR.message}` }),
);
});
});
diff --git a/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js b/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js
index 2693b26aeae..8174ba5c693 100644
--- a/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js
+++ b/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js
@@ -1,5 +1,6 @@
import { shallowMount } from '@vue/test-utils';
import { times } from 'lodash';
+import { nextTick } from 'vue';
import SnippetBlobActionsEdit from '~/snippets/components/snippet_blob_actions_edit.vue';
import SnippetBlobEdit from '~/snippets/components/snippet_blob_edit.vue';
import {
@@ -193,7 +194,7 @@ describe('snippets/components/snippet_blob_actions_edit', () => {
it('emits an action when content changes again', async () => {
triggerBlobUpdate(0, { content });
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(getLastActions()).toEqual([testEntries.updated.diff]);
});
diff --git a/spec/frontend/snippets/components/snippet_blob_view_spec.js b/spec/frontend/snippets/components/snippet_blob_view_spec.js
index 172089f9ee6..c395112e313 100644
--- a/spec/frontend/snippets/components/snippet_blob_view_spec.js
+++ b/spec/frontend/snippets/components/snippet_blob_view_spec.js
@@ -86,21 +86,17 @@ describe('Blob Embeddable', () => {
expect(wrapper.find(RichViewer).exists()).toBe(true);
});
- it('correctly switches viewer type', () => {
+ it('correctly switches viewer type', async () => {
createComponent();
expect(wrapper.find(SimpleViewer).exists()).toBe(true);
wrapper.vm.switchViewer(RichViewerMock.type);
- return wrapper.vm
- .$nextTick()
- .then(() => {
- expect(wrapper.find(RichViewer).exists()).toBe(true);
- wrapper.vm.switchViewer(SimpleViewerMock.type);
- })
- .then(() => {
- expect(wrapper.find(SimpleViewer).exists()).toBe(true);
- });
+ await nextTick();
+ expect(wrapper.find(RichViewer).exists()).toBe(true);
+ await wrapper.vm.switchViewer(SimpleViewerMock.type);
+
+ expect(wrapper.find(SimpleViewer).exists()).toBe(true);
});
it('passes information about render error down to blob header', () => {
@@ -191,22 +187,18 @@ describe('Blob Embeddable', () => {
});
describe('switchViewer()', () => {
- it('switches to the passed viewer', () => {
+ it('switches to the passed viewer', async () => {
createComponent();
wrapper.vm.switchViewer(RichViewerMock.type);
- return wrapper.vm
- .$nextTick()
- .then(() => {
- expect(wrapper.vm.activeViewerType).toBe(RichViewerMock.type);
- expect(wrapper.find(RichViewer).exists()).toBe(true);
-
- wrapper.vm.switchViewer(SimpleViewerMock.type);
- })
- .then(() => {
- expect(wrapper.vm.activeViewerType).toBe(SimpleViewerMock.type);
- expect(wrapper.find(SimpleViewer).exists()).toBe(true);
- });
+
+ await nextTick();
+ expect(wrapper.vm.activeViewerType).toBe(RichViewerMock.type);
+ expect(wrapper.find(RichViewer).exists()).toBe(true);
+
+ await wrapper.vm.switchViewer(SimpleViewerMock.type);
+ expect(wrapper.vm.activeViewerType).toBe(SimpleViewerMock.type);
+ expect(wrapper.find(SimpleViewer).exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/snippets/components/snippet_header_spec.js b/spec/frontend/snippets/components/snippet_header_spec.js
index daa9d6345b0..1b9d170556b 100644
--- a/spec/frontend/snippets/components/snippet_header_spec.js
+++ b/spec/frontend/snippets/components/snippet_header_spec.js
@@ -2,6 +2,7 @@ import { GlButton, GlModal, GlDropdown } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { ApolloMutation } from 'vue-apollo';
import MockAdapter from 'axios-mock-adapter';
+import { nextTick } from 'vue';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { Blob, BinaryBlob } from 'jest/blob/components/mock_data';
@@ -245,7 +246,7 @@ describe('Snippet header component', () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ canCreateSnippet: true });
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findButtonsAsModel()).toEqual(
expect.arrayContaining([
@@ -348,33 +349,31 @@ describe('Snippet header component', () => {
describe('in case of successful mutation, closes modal and redirects to correct listing', () => {
useMockLocationHelper();
- const createDeleteSnippet = (snippetProps = {}) => {
+ const createDeleteSnippet = async (snippetProps = {}) => {
createComponent({
snippetProps,
});
wrapper.vm.closeDeleteModal = jest.fn();
wrapper.vm.deleteSnippet();
- return wrapper.vm.$nextTick();
+ await nextTick();
};
- it('redirects to dashboard/snippets for personal snippet', () => {
- return createDeleteSnippet().then(() => {
- expect(wrapper.vm.closeDeleteModal).toHaveBeenCalled();
- expect(window.location.pathname).toBe(`${gon.relative_url_root}dashboard/snippets`);
- });
+ it('redirects to dashboard/snippets for personal snippet', async () => {
+ await createDeleteSnippet();
+ expect(wrapper.vm.closeDeleteModal).toHaveBeenCalled();
+ expect(window.location.pathname).toBe(`${gon.relative_url_root}dashboard/snippets`);
});
- it('redirects to project snippets for project snippet', () => {
+ it('redirects to project snippets for project snippet', async () => {
const fullPath = 'foo/bar';
- return createDeleteSnippet({
+ await createDeleteSnippet({
project: {
fullPath,
},
- }).then(() => {
- expect(wrapper.vm.closeDeleteModal).toHaveBeenCalled();
- expect(window.location.pathname).toBe(`${fullPath}/-/snippets`);
});
+ expect(wrapper.vm.closeDeleteModal).toHaveBeenCalled();
+ expect(window.location.pathname).toBe(`${fullPath}/-/snippets`);
});
});
});
diff --git a/spec/frontend/static_site_editor/components/edit_meta_controls_spec.js b/spec/frontend/static_site_editor/components/edit_meta_controls_spec.js
index 7a8834933e0..f6b29e98e5f 100644
--- a/spec/frontend/static_site_editor/components/edit_meta_controls_spec.js
+++ b/spec/frontend/static_site_editor/components/edit_meta_controls_spec.js
@@ -1,6 +1,7 @@
import { GlDropdown, GlDropdownItem, GlFormInput, GlFormTextarea } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import EditMetaControls from '~/static_site_editor/components/edit_meta_controls.vue';
import { mergeRequestMeta, mergeRequestTemplates } from '../mock_data';
@@ -38,11 +39,11 @@ describe('~/static_site_editor/components/edit_meta_controls.vue', () => {
const findGlFormTextAreaDescription = () => wrapper.find(GlFormTextarea);
- beforeEach(() => {
+ beforeEach(async () => {
buildWrapper();
buildMocks();
- return wrapper.vm.$nextTick();
+ await nextTick();
});
afterEach(() => {
diff --git a/spec/frontend/static_site_editor/components/edit_meta_modal_spec.js b/spec/frontend/static_site_editor/components/edit_meta_modal_spec.js
index 3a336f6a230..bf3f8b7f571 100644
--- a/spec/frontend/static_site_editor/components/edit_meta_modal_spec.js
+++ b/spec/frontend/static_site_editor/components/edit_meta_modal_spec.js
@@ -1,6 +1,7 @@
import { GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+import { nextTick } from 'vue';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import axios from '~/lib/utils/axios_utils';
import EditMetaControls from '~/static_site_editor/components/edit_meta_controls.vue';
@@ -50,14 +51,14 @@ describe('~/static_site_editor/components/edit_meta_modal.vue', () => {
const findEditMetaControls = () => wrapper.find(EditMetaControls);
const findLocalStorageSync = () => wrapper.find(LocalStorageSync);
- beforeEach(() => {
+ beforeEach(async () => {
localStorage.setItem(MR_META_LOCAL_STORAGE_KEY);
buildMockAxios();
buildWrapper();
buildMockRefs();
- return wrapper.vm.$nextTick();
+ await nextTick();
});
afterEach(() => {
@@ -77,7 +78,7 @@ describe('~/static_site_editor/components/edit_meta_modal.vue', () => {
findLocalStorageSync().vm.$emit('input', localStorageMeta);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findEditMetaControls().props()).toEqual(localStorageMeta);
});
@@ -134,13 +135,13 @@ describe('~/static_site_editor/components/edit_meta_modal.vue', () => {
it('sets the currentTemplate on the changeTemplate event', async () => {
findEditMetaControls().vm.$emit('changeTemplate', template1);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findEditMetaControls().props().currentTemplate).toBe(template1);
findEditMetaControls().vm.$emit('changeTemplate', null);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findEditMetaControls().props().currentTemplate).toBe(null);
});
@@ -148,7 +149,7 @@ describe('~/static_site_editor/components/edit_meta_modal.vue', () => {
it('updates the description on the changeTemplate event', async () => {
findEditMetaControls().vm.$emit('changeTemplate', template1);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findEditMetaControls().props().description).toEqual(template1.content);
});
@@ -164,7 +165,7 @@ describe('~/static_site_editor/components/edit_meta_modal.vue', () => {
findEditMetaControls().vm.$emit('updateSettings', newMeta);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findLocalStorageSync().props('value')).toEqual(newMeta);
});
diff --git a/spec/frontend/static_site_editor/pages/home_spec.js b/spec/frontend/static_site_editor/pages/home_spec.js
index eb056469603..6571d295c36 100644
--- a/spec/frontend/static_site_editor/pages/home_spec.js
+++ b/spec/frontend/static_site_editor/pages/home_spec.js
@@ -1,4 +1,5 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import EditArea from '~/static_site_editor/components/edit_area.vue';
import EditMetaModal from '~/static_site_editor/components/edit_meta_modal.vue';
@@ -29,8 +30,6 @@ import {
imageRoot,
} from '../mock_data';
-const localVue = createLocalVue();
-
describe('static_site_editor/pages/home', () => {
let wrapper;
let store;
@@ -78,7 +77,6 @@ describe('static_site_editor/pages/home', () => {
const buildWrapper = (data = {}) => {
wrapper = shallowMount(Home, {
- localVue,
store,
mocks: {
$apollo,
@@ -182,7 +180,7 @@ describe('static_site_editor/pages/home', () => {
});
describe('when preparing submission', () => {
- it('calls the show method when the edit-area submit event is emitted', () => {
+ it('calls the show method when the edit-area submit event is emitted', async () => {
buildWrapper();
const mockInstance = { show: jest.fn() };
@@ -190,9 +188,8 @@ describe('static_site_editor/pages/home', () => {
findEditArea().vm.$emit('submit', { content });
- return wrapper.vm.$nextTick().then(() => {
- expect(mockInstance.show).toHaveBeenCalled();
- });
+ await nextTick();
+ expect(mockInstance.show).toHaveBeenCalled();
});
});
@@ -203,13 +200,13 @@ describe('static_site_editor/pages/home', () => {
.mockRejectedValueOnce(new Error(submitChangesError));
};
- beforeEach(() => {
+ beforeEach(async () => {
setupMutateMock();
buildWrapper({ content });
findEditMetaModal().vm.$emit('primary', mergeRequestMeta);
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('displays submit changes error message', () => {
@@ -224,12 +221,11 @@ describe('static_site_editor/pages/home', () => {
expect(mutateMock).toHaveBeenCalled();
});
- it('hides submit changes error message when dismiss button is clicked', () => {
+ it('hides submit changes error message when dismiss button is clicked', async () => {
findSubmitChangesError().vm.$emit('dismiss');
- return wrapper.vm.$nextTick().then(() => {
- expect(findSubmitChangesError().exists()).toBe(false);
- });
+ await nextTick();
+ expect(findSubmitChangesError().exists()).toBe(false);
});
});
@@ -237,7 +233,7 @@ describe('static_site_editor/pages/home', () => {
const newContent = `new ${content}`;
const formattedMarkdown = `formatted ${content}`;
- beforeEach(() => {
+ beforeEach(async () => {
mutateMock.mockResolvedValueOnce(hasSubmittedChangesMutationPayload).mockResolvedValueOnce({
data: {
submitContentChanges: savedContentMeta,
@@ -252,7 +248,7 @@ describe('static_site_editor/pages/home', () => {
findEditMetaModal().vm.$emit('primary', mergeRequestMeta);
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('dispatches hasSubmittedChanges mutation', () => {
diff --git a/spec/frontend/terraform/components/states_table_actions_spec.js b/spec/frontend/terraform/components/states_table_actions_spec.js
index fbe55306f37..a6c80b95af4 100644
--- a/spec/frontend/terraform/components/states_table_actions_spec.js
+++ b/spec/frontend/terraform/components/states_table_actions_spec.js
@@ -1,5 +1,6 @@
import { GlDropdown, GlModal, GlSprintf } from '@gitlab/ui';
-import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -9,8 +10,7 @@ import lockStateMutation from '~/terraform/graphql/mutations/lock_state.mutation
import removeStateMutation from '~/terraform/graphql/mutations/remove_state.mutation.graphql';
import unlockStateMutation from '~/terraform/graphql/mutations/unlock_state.mutation.graphql';
-const localVue = createLocalVue();
-localVue.use(VueApollo);
+Vue.use(VueApollo);
describe('StatesTableActions', () => {
let lockResponse;
@@ -58,20 +58,19 @@ describe('StatesTableActions', () => {
);
};
- const createComponent = (propsData = defaultProps) => {
+ const createComponent = async (propsData = defaultProps) => {
const apolloProvider = createMockApolloProvider();
toast = jest.fn();
wrapper = shallowMount(StateActions, {
apolloProvider,
- localVue,
propsData,
mocks: { $toast: { show: toast } },
stubs: { GlDropdown, GlModal, GlSprintf },
});
- return wrapper.vm.$nextTick();
+ await nextTick();
};
const findActionsDropdown = () => wrapper.findComponent(GlDropdown);
diff --git a/spec/frontend/terraform/components/states_table_spec.js b/spec/frontend/terraform/components/states_table_spec.js
index 100e577f514..fa9c8320b4f 100644
--- a/spec/frontend/terraform/components/states_table_spec.js
+++ b/spec/frontend/terraform/components/states_table_spec.js
@@ -1,5 +1,6 @@
-import { GlIcon, GlLoadingIcon, GlTooltip } from '@gitlab/ui';
+import { GlBadge, GlLoadingIcon, GlTooltip } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import { useFakeDate } from 'helpers/fake_date';
import StatesTable from '~/terraform/components/states_table.vue';
import StateActions from '~/terraform/components/states_table_actions.vue';
@@ -106,9 +107,9 @@ describe('StatesTable', () => {
],
};
- const createComponent = (propsData = defaultProps) => {
+ const createComponent = async (propsData = defaultProps) => {
wrapper = mount(StatesTable, { propsData });
- return wrapper.vm.$nextTick();
+ await nextTick();
};
const findActions = () => wrapper.findAll(StateActions);
@@ -138,7 +139,7 @@ describe('StatesTable', () => {
const toolTip = state.find(GlTooltip);
expect(state.text()).toContain(name);
- expect(state.find(GlIcon).exists()).toBe(locked);
+ expect(state.find(GlBadge).exists()).toBe(locked);
expect(state.find(GlLoadingIcon).exists()).toBe(loading);
expect(toolTip.exists()).toBe(locked);
diff --git a/spec/frontend/terraform/components/terraform_list_spec.js b/spec/frontend/terraform/components/terraform_list_spec.js
index 8e565df81ae..c8b4cd564d9 100644
--- a/spec/frontend/terraform/components/terraform_list_spec.js
+++ b/spec/frontend/terraform/components/terraform_list_spec.js
@@ -1,5 +1,6 @@
import { GlAlert, GlBadge, GlKeysetPagination, GlLoadingIcon, GlTab } from '@gitlab/ui';
-import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -8,8 +9,7 @@ import StatesTable from '~/terraform/components/states_table.vue';
import TerraformList from '~/terraform/components/terraform_list.vue';
import getStatesQuery from '~/terraform/graphql/queries/get_states.query.graphql';
-const localVue = createLocalVue();
-localVue.use(VueApollo);
+Vue.use(VueApollo);
describe('TerraformList', () => {
let wrapper;
@@ -45,7 +45,6 @@ describe('TerraformList', () => {
const apolloProvider = createMockApollo([[getStatesQuery, statsQueryResponse]], mockResolvers);
wrapper = shallowMount(TerraformList, {
- localVue,
apolloProvider,
propsData,
stubs: {
@@ -100,6 +99,7 @@ describe('TerraformList', () => {
nodes: states,
count: states.length,
pageInfo: {
+ __typename: 'PageInfo',
hasNextPage: true,
hasPreviousPage: false,
startCursor: 'prev',
diff --git a/spec/frontend/toggles/index_spec.js b/spec/frontend/toggles/index_spec.js
new file mode 100644
index 00000000000..575b1b6080c
--- /dev/null
+++ b/spec/frontend/toggles/index_spec.js
@@ -0,0 +1,149 @@
+import { createWrapper } from '@vue/test-utils';
+import { GlToggle } from '@gitlab/ui';
+import { initToggle } from '~/toggles';
+
+// Selectors
+const TOGGLE_WRAPPER_CLASS = '.gl-toggle-wrapper';
+const TOGGLE_LABEL_CLASS = '.gl-toggle-label';
+const CHECKED_CLASS = '.is-checked';
+const DISABLED_CLASS = '.is-disabled';
+const LOADING_CLASS = '.toggle-loading';
+const HELP_TEXT_SELECTOR = '[data-testid="toggle-help"]';
+
+// Toggle settings
+const toggleClassName = 'js-custom-toggle-class';
+const toggleLabel = 'Toggle label';
+
+describe('toggles/index.js', () => {
+ let instance;
+ let toggleWrapper;
+
+ const createRootEl = (dataAttrs) => {
+ const dataset = {
+ label: toggleLabel,
+ ...dataAttrs,
+ };
+ const el = document.createElement('span');
+ el.classList.add(toggleClassName);
+
+ Object.entries(dataset).forEach(([key, value]) => {
+ el.dataset[key] = value;
+ });
+
+ document.body.appendChild(el);
+
+ return el;
+ };
+
+ const initToggleWithOptions = (options = {}) => {
+ const el = createRootEl(options);
+ instance = initToggle(el);
+ toggleWrapper = document.querySelector(TOGGLE_WRAPPER_CLASS);
+ };
+
+ afterEach(() => {
+ document.body.innerHTML = '';
+ instance = null;
+ toggleWrapper = null;
+ });
+
+ describe('initToggle', () => {
+ describe('default state', () => {
+ beforeEach(() => {
+ initToggleWithOptions();
+ });
+
+ it('attaches a GlToggle to the element', async () => {
+ expect(toggleWrapper).not.toBe(null);
+ expect(toggleWrapper.querySelector(TOGGLE_LABEL_CLASS).textContent).toBe(toggleLabel);
+ });
+
+ it('passes CSS classes down to GlToggle', () => {
+ expect(toggleWrapper.className).toContain(toggleClassName);
+ });
+
+ it('is not checked', () => {
+ expect(toggleWrapper.querySelector(CHECKED_CLASS)).toBe(null);
+ });
+
+ it('is enabled', () => {
+ expect(toggleWrapper.querySelector(DISABLED_CLASS)).toBe(null);
+ });
+
+ it('is not loading', () => {
+ expect(toggleWrapper.querySelector(LOADING_CLASS)).toBe(null);
+ });
+
+ it('emits "change" event when value changes', () => {
+ const wrapper = createWrapper(instance);
+ const event = 'change';
+ const listener = jest.fn();
+
+ instance.$on(event, listener);
+
+ expect(listener).toHaveBeenCalledTimes(0);
+
+ wrapper.find(GlToggle).vm.$emit(event, true);
+
+ expect(listener).toHaveBeenCalledTimes(1);
+ expect(listener).toHaveBeenLastCalledWith(true);
+
+ wrapper.find(GlToggle).vm.$emit(event, false);
+
+ expect(listener).toHaveBeenCalledTimes(2);
+ expect(listener).toHaveBeenLastCalledWith(false);
+ });
+ });
+
+ describe('with custom options', () => {
+ const name = 'toggle-name';
+ const help = 'Help text';
+ const foo = 'bar';
+
+ beforeEach(() => {
+ initToggleWithOptions({
+ name,
+ isChecked: true,
+ disabled: true,
+ isLoading: true,
+ help,
+ labelPosition: 'hidden',
+ foo,
+ });
+ toggleWrapper = document.querySelector(TOGGLE_WRAPPER_CLASS);
+ });
+
+ it('sets the custom name', () => {
+ const input = toggleWrapper.querySelector('input[type="hidden"]');
+
+ expect(input.name).toBe(name);
+ });
+
+ it('is checked', () => {
+ expect(toggleWrapper.querySelector(CHECKED_CLASS)).not.toBe(null);
+ });
+
+ it('is disabled', () => {
+ expect(toggleWrapper.querySelector(DISABLED_CLASS)).not.toBe(null);
+ });
+
+ it('is loading', () => {
+ expect(toggleWrapper.querySelector(LOADING_CLASS)).not.toBe(null);
+ });
+
+ it('sets the custom help text', () => {
+ expect(toggleWrapper.querySelector(HELP_TEXT_SELECTOR).textContent).toBe(help);
+ });
+
+ it('hides the label', () => {
+ expect(
+ toggleWrapper.querySelector(TOGGLE_LABEL_CLASS).classList.contains('gl-sr-only'),
+ ).toBe(true);
+ });
+
+ it('passes custom dataset to the wrapper', () => {
+ expect(toggleWrapper.dataset.foo).toBe('bar');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/token_access/token_access_spec.js b/spec/frontend/token_access/token_access_spec.js
index c4e29a52f1c..5aaeebd5af4 100644
--- a/spec/frontend/token_access/token_access_spec.js
+++ b/spec/frontend/token_access/token_access_spec.js
@@ -1,5 +1,5 @@
import { GlToggle, GlLoadingIcon } from '@gitlab/ui';
-import { createLocalVue } from '@vue/test-utils';
+import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
@@ -22,9 +22,8 @@ import {
const projectPath = 'root/my-repo';
const error = new Error('Error');
-const localVue = createLocalVue();
-localVue.use(VueApollo);
+Vue.use(VueApollo);
jest.mock('~/flash');
@@ -52,7 +51,6 @@ describe('TokenAccess component', () => {
const createComponent = (requestHandlers, mountFn = shallowMountExtended) => {
wrapper = mountFn(TokenAccess, {
- localVue,
provide: {
fullPath: projectPath,
},
diff --git a/spec/frontend/tooltips/components/tooltips_spec.js b/spec/frontend/tooltips/components/tooltips_spec.js
index 9b703b74a1a..eef352a72ff 100644
--- a/spec/frontend/tooltips/components/tooltips_spec.js
+++ b/spec/frontend/tooltips/components/tooltips_spec.js
@@ -1,5 +1,6 @@
import { GlTooltip } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import { useMockMutationObserver } from 'helpers/mock_dom_observer';
import Tooltips from '~/tooltips/components/tooltips.vue';
@@ -46,7 +47,7 @@ describe('tooltips/components/tooltips.vue', () => {
it('attaches tooltips to the targets specified', async () => {
wrapper.vm.addTooltips([target]);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.find(GlTooltip).props('target')).toBe(target);
});
@@ -56,7 +57,7 @@ describe('tooltips/components/tooltips.vue', () => {
wrapper.vm.addTooltips([target]);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.find(GlTooltip).exists()).toBe(false);
});
@@ -65,7 +66,7 @@ describe('tooltips/components/tooltips.vue', () => {
wrapper.vm.addTooltips([target]);
wrapper.vm.addTooltips([target]);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.findAll(GlTooltip)).toHaveLength(1);
});
@@ -73,7 +74,7 @@ describe('tooltips/components/tooltips.vue', () => {
it('sets tooltip content from title attribute', async () => {
wrapper.vm.addTooltips([target]);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.find(GlTooltip).text()).toBe(target.getAttribute('title'));
});
@@ -85,7 +86,7 @@ describe('tooltips/components/tooltips.vue', () => {
});
wrapper.vm.addTooltips([target]);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.find(GlTooltip).html()).toContain(target.getAttribute('title'));
});
@@ -94,7 +95,7 @@ describe('tooltips/components/tooltips.vue', () => {
const config = { show: true };
target = createTooltipTarget();
wrapper.vm.addTooltips([target], config);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.find(GlTooltip).props()).toMatchObject(config);
});
@@ -110,7 +111,7 @@ describe('tooltips/components/tooltips.vue', () => {
target = createTooltipTarget({ [attribute]: value });
wrapper.vm.addTooltips([target]);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.find(GlTooltip).props(prop)).toBe(value);
},
@@ -124,10 +125,10 @@ describe('tooltips/components/tooltips.vue', () => {
it('removes all tooltips when elements is nil', async () => {
wrapper.vm.addTooltips([createTooltipTarget(), createTooltipTarget()]);
- await wrapper.vm.$nextTick();
+ await nextTick();
wrapper.vm.dispose();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(allTooltips()).toHaveLength(0);
});
@@ -136,10 +137,10 @@ describe('tooltips/components/tooltips.vue', () => {
const target = createTooltipTarget();
wrapper.vm.addTooltips([target, createTooltipTarget()]);
- await wrapper.vm.$nextTick();
+ await nextTick();
wrapper.vm.dispose(target);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(allTooltips()).toHaveLength(1);
});
@@ -154,13 +155,13 @@ describe('tooltips/components/tooltips.vue', () => {
const target = createTooltipTarget();
wrapper.vm.addTooltips([target, createTooltipTarget()]);
- await wrapper.vm.$nextTick();
+ await nextTick();
triggerMutate(document.body, {
entry: { removedNodes: [target] },
options: { childList: true },
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(allTooltips()).toHaveLength(1);
});
@@ -175,7 +176,7 @@ describe('tooltips/components/tooltips.vue', () => {
wrapper.vm.addTooltips([target]);
- await wrapper.vm.$nextTick();
+ await nextTick();
wrapper.vm.triggerEvent(target, event);
@@ -195,14 +196,14 @@ describe('tooltips/components/tooltips.vue', () => {
wrapper.vm.addTooltips([target]);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.find(GlTooltip).text()).toBe(currentTitle);
target.setAttribute('title', newTitle);
wrapper.vm.fixTitle(target);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.find(GlTooltip).text()).toBe(newTitle);
});
@@ -225,7 +226,7 @@ describe('tooltips/components/tooltips.vue', () => {
buildWrapper();
wrapper.vm.addTooltips([createTooltipTarget()]);
- await wrapper.vm.$nextTick();
+ await nextTick();
wrapper.findComponent(GlTooltip).vm.$emit('hidden');
expect(wrapper.emitted('hidden')).toHaveLength(1);
diff --git a/spec/frontend/tooltips/index_spec.js b/spec/frontend/tooltips/index_spec.js
index 9c03ca8f4c9..198a0315ef7 100644
--- a/spec/frontend/tooltips/index_spec.js
+++ b/spec/frontend/tooltips/index_spec.js
@@ -1,3 +1,4 @@
+import { nextTick } from 'vue';
import {
add,
initTooltips,
@@ -57,7 +58,7 @@ describe('tooltips/index.js', () => {
triggerEvent(target);
- await tooltipsApp.$nextTick();
+ await nextTick();
expect(document.querySelector('.gl-tooltip')).not.toBe(null);
expect(document.querySelector('.gl-tooltip').innerHTML).toContain('default title');
@@ -69,7 +70,7 @@ describe('tooltips/index.js', () => {
buildTooltipsApp();
triggerEvent(target, 'click');
- await tooltipsApp.$nextTick();
+ await nextTick();
expect(document.querySelector('.gl-tooltip')).not.toBe(null);
expect(document.querySelector('.gl-tooltip').innerHTML).toContain('default title');
@@ -83,7 +84,7 @@ describe('tooltips/index.js', () => {
buildTooltipsApp();
add([target], { title: 'custom title' });
- await tooltipsApp.$nextTick();
+ await nextTick();
expect(document.querySelector('.gl-tooltip')).not.toBe(null);
expect(document.querySelector('.gl-tooltip').innerHTML).toContain('custom title');
@@ -97,13 +98,13 @@ describe('tooltips/index.js', () => {
buildTooltipsApp();
triggerEvent(target);
- await tooltipsApp.$nextTick();
+ await nextTick();
expect(document.querySelector('.gl-tooltip')).not.toBe(null);
dispose([target]);
- await tooltipsApp.$nextTick();
+ await nextTick();
expect(document.querySelector('.gl-tooltip')).toBe(null);
});
@@ -122,7 +123,7 @@ describe('tooltips/index.js', () => {
buildTooltipsApp();
- await tooltipsApp.$nextTick();
+ await nextTick();
jest.spyOn(tooltipsApp, 'triggerEvent');
@@ -137,7 +138,7 @@ describe('tooltips/index.js', () => {
buildTooltipsApp();
- await tooltipsApp.$nextTick();
+ await nextTick();
jest.spyOn(tooltipsApp, 'fixTitle');
diff --git a/spec/frontend/user_lists/components/add_user_modal_spec.js b/spec/frontend/user_lists/components/add_user_modal_spec.js
index c9ad40ed228..cd04836a8c4 100644
--- a/spec/frontend/user_lists/components/add_user_modal_spec.js
+++ b/spec/frontend/user_lists/components/add_user_modal_spec.js
@@ -1,4 +1,5 @@
import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import AddUserModal from '~/user_lists/components/add_user_modal.vue';
describe('Add User Modal', () => {
@@ -30,7 +31,7 @@ describe('Add User Modal', () => {
it('should clear the input after emitting', async () => {
click('confirm-add-user-ids');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.find('#add-user-ids').element.value).toBe('');
});
@@ -42,7 +43,7 @@ describe('Add User Modal', () => {
it('should clear the input after cancelling', async () => {
click('cancel-add-user-ids');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.find('#add-user-ids').element.value).toBe('');
});
diff --git a/spec/frontend/user_lists/components/edit_user_list_spec.js b/spec/frontend/user_lists/components/edit_user_list_spec.js
index bd71a677a24..7cafe5e1f56 100644
--- a/spec/frontend/user_lists/components/edit_user_list_spec.js
+++ b/spec/frontend/user_lists/components/edit_user_list_spec.js
@@ -1,6 +1,6 @@
import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
-import { createLocalVue, mount } from '@vue/test-utils';
-import Vue from 'vue';
+import { mount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api';
@@ -13,8 +13,7 @@ import { userList } from '../../feature_flags/mock_data';
jest.mock('~/api');
jest.mock('~/lib/utils/url_utility');
-const localVue = createLocalVue(Vue);
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('user_lists/components/edit_user_list', () => {
let wrapper;
@@ -30,7 +29,6 @@ describe('user_lists/components/edit_user_list', () => {
destroy();
wrapper = mount(EditUserList, {
- localVue,
store: createStore({ projectId: '1', userListIid: '2' }),
provide: {
userListsDocsPath: '/docs/user_lists',
@@ -79,11 +77,11 @@ describe('user_lists/components/edit_user_list', () => {
});
describe('update', () => {
- beforeEach(() => {
+ beforeEach(async () => {
Api.fetchFeatureFlagUserList.mockResolvedValue({ data: userList });
factory();
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('should link to the documentation', () => {
@@ -101,11 +99,11 @@ describe('user_lists/components/edit_user_list', () => {
});
describe('success', () => {
- beforeEach(() => {
+ beforeEach(async () => {
Api.updateFeatureFlagUserList.mockResolvedValue({ data: userList });
setInputValue('test');
clickSave();
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('should create a user list with the entered name', () => {
@@ -141,7 +139,7 @@ describe('user_lists/components/edit_user_list', () => {
it('should dismiss the error if dismiss is clicked', async () => {
alert.find('button').trigger('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(alert.exists()).toBe(false);
});
diff --git a/spec/frontend/user_lists/components/new_user_list_spec.js b/spec/frontend/user_lists/components/new_user_list_spec.js
index a81e8912714..5eb44970fe4 100644
--- a/spec/frontend/user_lists/components/new_user_list_spec.js
+++ b/spec/frontend/user_lists/components/new_user_list_spec.js
@@ -1,6 +1,6 @@
import { GlAlert } from '@gitlab/ui';
-import { mount, createLocalVue } from '@vue/test-utils';
-import Vue from 'vue';
+import { mount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api';
@@ -12,8 +12,7 @@ import { userList } from '../../feature_flags/mock_data';
jest.mock('~/api');
jest.mock('~/lib/utils/url_utility');
-const localVue = createLocalVue(Vue);
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('user_lists/components/new_user_list', () => {
let wrapper;
@@ -24,7 +23,6 @@ describe('user_lists/components/new_user_list', () => {
beforeEach(() => {
wrapper = mount(NewUserList, {
- localVue,
store: createStore({ projectId: '1' }),
provide: {
featureFlagsPath: '/feature_flags',
@@ -45,11 +43,11 @@ describe('user_lists/components/new_user_list', () => {
describe('create', () => {
describe('success', () => {
- beforeEach(() => {
+ beforeEach(async () => {
Api.createFeatureFlagUserList.mockResolvedValue({ data: userList });
setInputValue('test');
click('save-user-list');
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('should create a user list with the entered name', () => {
@@ -84,7 +82,7 @@ describe('user_lists/components/new_user_list', () => {
it('should dismiss the error when the dismiss button is clicked', async () => {
alert.find('button').trigger('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(alert.exists()).toBe(false);
});
diff --git a/spec/frontend/user_lists/components/user_list_spec.js b/spec/frontend/user_lists/components/user_list_spec.js
index f016b5091d9..88dad06938b 100644
--- a/spec/frontend/user_lists/components/user_list_spec.js
+++ b/spec/frontend/user_lists/components/user_list_spec.js
@@ -1,7 +1,7 @@
import { GlAlert, GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { uniq } from 'lodash';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import Api from '~/api';
import UserList from '~/user_lists/components/user_list.vue';
@@ -57,12 +57,12 @@ describe('User List', () => {
describe('success', () => {
let userIds;
- beforeEach(() => {
+ beforeEach(async () => {
userIds = parseUserIds(userList.user_xids);
Api.fetchFeatureFlagUserList.mockResolvedValueOnce({ data: userList });
factory();
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('requests the user list on mount', () => {
@@ -101,10 +101,10 @@ describe('User List', () => {
beforeEach(async () => {
Api.updateFeatureFlagUserList.mockResolvedValue(userList);
click('add-users');
- await wrapper.vm.$nextTick();
+ await nextTick();
wrapper.find('#add-user-ids').setValue(`${stringifyUserIds(newIds)},`);
click('confirm-add-user-ids');
- await wrapper.vm.$nextTick();
+ await nextTick();
[[, { user_xids: receivedUserIds }]] = Api.updateFeatureFlagUserList.mock.calls;
parsedReceivedUserIds = parseUserIds(receivedUserIds);
});
@@ -140,7 +140,7 @@ describe('User List', () => {
beforeEach(async () => {
Api.updateFeatureFlagUserList.mockResolvedValue(userList);
click('delete-user-id');
- await wrapper.vm.$nextTick();
+ await nextTick();
[[, { user_xids: receivedUserIds }]] = Api.updateFeatureFlagUserList.mock.calls;
});
@@ -159,11 +159,11 @@ describe('User List', () => {
describe('error', () => {
const findAlert = () => wrapper.find(GlAlert);
- beforeEach(() => {
+ beforeEach(async () => {
Api.fetchFeatureFlagUserList.mockRejectedValue();
factory();
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('displays the alert message', () => {
@@ -175,18 +175,18 @@ describe('User List', () => {
const alert = findAlert();
alert.find('button').trigger('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(alert.exists()).toBe(false);
});
});
describe('empty list', () => {
- beforeEach(() => {
+ beforeEach(async () => {
Api.fetchFeatureFlagUserList.mockResolvedValueOnce({ data: { ...userList, user_xids: '' } });
factory();
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('displays an empty state', () => {
diff --git a/spec/frontend/user_lists/components/user_lists_spec.js b/spec/frontend/user_lists/components/user_lists_spec.js
index 7a33c6faac9..10742c029c1 100644
--- a/spec/frontend/user_lists/components/user_lists_spec.js
+++ b/spec/frontend/user_lists/components/user_lists_spec.js
@@ -1,7 +1,7 @@
import { GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
import { within } from '@testing-library/dom';
import { mount, createWrapper } from '@vue/test-utils';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api';
@@ -82,7 +82,7 @@ describe('~/user_lists/components/user_lists.vue', () => {
factory();
await waitForPromises();
- await Vue.nextTick();
+ await nextTick();
emptyState = wrapper.findComponent(GlEmptyState);
});
@@ -130,7 +130,7 @@ describe('~/user_lists/components/user_lists.vue', () => {
factory();
jest.spyOn(store, 'dispatch');
- await Vue.nextTick();
+ await nextTick();
table = wrapper.findComponent(UserListsTable);
});
@@ -171,7 +171,7 @@ describe('~/user_lists/components/user_lists.vue', () => {
Api.fetchFeatureFlagUserLists.mockRejectedValue();
factory();
- await Vue.nextTick();
+ await nextTick();
});
it('should render error state', () => {
diff --git a/spec/frontend/user_lists/components/user_lists_table_spec.js b/spec/frontend/user_lists/components/user_lists_table_spec.js
index 7f4d510a39c..63587703392 100644
--- a/spec/frontend/user_lists/components/user_lists_table_spec.js
+++ b/spec/frontend/user_lists/components/user_lists_table_spec.js
@@ -1,6 +1,7 @@
import { GlModal } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import * as timeago from 'timeago.js';
+import { nextTick } from 'vue';
import UserListsTable from '~/user_lists/components/user_lists_table.vue';
import { userList } from '../../feature_flags/mock_data';
@@ -56,43 +57,40 @@ describe('User Lists Table', () => {
});
describe('delete button', () => {
- it('should display the confirmation modal', () => {
+ it('should display the confirmation modal', async () => {
const modal = wrapper.find(GlModal);
wrapper.find('[data-testid="delete-user-list"]').trigger('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(modal.text()).toContain(`Delete ${userList.name}?`);
- expect(modal.text()).toContain(`User list ${userList.name} will be removed.`);
- });
+ await nextTick();
+ expect(modal.text()).toContain(`Delete ${userList.name}?`);
+ expect(modal.text()).toContain(`User list ${userList.name} will be removed.`);
});
});
describe('confirmation modal', () => {
let modal;
- beforeEach(() => {
+ beforeEach(async () => {
modal = wrapper.find(GlModal);
wrapper.find('button').trigger('click');
- return wrapper.vm.$nextTick();
+ await nextTick();
});
- it('should emit delete with list on confirmation', () => {
+ it('should emit delete with list on confirmation', async () => {
modal.find('[data-testid="modal-confirm"]').trigger('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted('delete')).toEqual([[userLists[0]]]);
- });
+ await nextTick();
+ expect(wrapper.emitted('delete')).toEqual([[userLists[0]]]);
});
- it('should not emit delete with list when not confirmed', () => {
+ it('should not emit delete with list when not confirmed', async () => {
modal.find('button').trigger('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted('delete')).toBeUndefined();
- });
+ await nextTick();
+ expect(wrapper.emitted('delete')).toBeUndefined();
});
});
});
diff --git a/spec/frontend/vue_alerts_spec.js b/spec/frontend/vue_alerts_spec.js
index 30be606292f..1952eea4a01 100644
--- a/spec/frontend/vue_alerts_spec.js
+++ b/spec/frontend/vue_alerts_spec.js
@@ -1,4 +1,4 @@
-import Vue from 'vue';
+import { nextTick } from 'vue';
import { setHTMLFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'helpers/test_constants';
import initVueAlerts from '~/vue_alerts';
@@ -75,10 +75,9 @@ describe('VueAlerts', () => {
});
describe('when dismissed', () => {
- beforeEach(() => {
+ beforeEach(async () => {
findAlertDismiss(findAlerts()[0]).click();
-
- return Vue.nextTick();
+ await nextTick();
});
it('hides the alert', () => {
diff --git a/spec/frontend/vue_mr_widget/components/mr_collapsible_extension_spec.js b/spec/frontend/vue_mr_widget/components/mr_collapsible_extension_spec.js
index 1aeb080aa04..82526af7afa 100644
--- a/spec/frontend/vue_mr_widget/components/mr_collapsible_extension_spec.js
+++ b/spec/frontend/vue_mr_widget/components/mr_collapsible_extension_spec.js
@@ -1,5 +1,6 @@
import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import MrCollapsibleSection from '~/vue_merge_request_widget/components/mr_collapsible_extension.vue';
describe('Merge Request Collapsible Extension', () => {
@@ -46,9 +47,9 @@ describe('Merge Request Collapsible Extension', () => {
});
describe('onClick', () => {
- beforeEach(() => {
+ beforeEach(async () => {
wrapper.find('button').trigger('click');
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('rendes the provided slot', () => {
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_author_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_author_spec.js
index e7c10ab4c2d..8a42e2e2ce7 100644
--- a/spec/frontend/vue_mr_widget/components/mr_widget_author_spec.js
+++ b/spec/frontend/vue_mr_widget/components/mr_widget_author_spec.js
@@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import MrWidgetAuthor from '~/vue_merge_request_widget/components/mr_widget_author.vue';
window.gl = window.gl || {};
@@ -50,7 +51,7 @@ describe('MrWidgetAuthor', () => {
},
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.find('img').attributes('src')).toBe('no_avatar.png');
});
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_expandable_section_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_expandable_section_spec.js
index 3e111cd308a..631aef412a6 100644
--- a/spec/frontend/vue_mr_widget/components/mr_widget_expandable_section_spec.js
+++ b/spec/frontend/vue_mr_widget/components/mr_widget_expandable_section_spec.js
@@ -1,5 +1,6 @@
import { GlButton, GlCollapse, GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import MrCollapsibleSection from '~/vue_merge_request_widget/components/mr_widget_expandable_section.vue';
describe('MrWidgetExpanableSection', () => {
@@ -43,9 +44,9 @@ describe('MrWidgetExpanableSection', () => {
});
describe('when collapse section is open', () => {
- beforeEach(() => {
+ beforeEach(async () => {
findButton().vm.$emit('click');
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('renders button with collapse text', () => {
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_memory_usage_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_memory_usage_spec.js
index f55d313a719..c0a30a5093d 100644
--- a/spec/frontend/vue_mr_widget/components/mr_widget_memory_usage_spec.js
+++ b/spec/frontend/vue_mr_widget/components/mr_widget_memory_usage_spec.js
@@ -1,6 +1,7 @@
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
+import waitForPromises from 'helpers/wait_for_promises';
import MemoryUsage from '~/vue_merge_request_widget/components/deployment/memory_usage.vue';
import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
@@ -152,23 +153,18 @@ describe('MemoryUsage', () => {
});
describe('loadMetrics', () => {
- const returnServicePromise = () =>
- new Promise((resolve) => {
- resolve({
- data: metricsMockData,
- });
+ it('should load metrics data using MRWidgetService', async () => {
+ jest.spyOn(MRWidgetService, 'fetchMetrics').mockResolvedValue({
+ data: metricsMockData,
});
-
- it('should load metrics data using MRWidgetService', (done) => {
- jest.spyOn(MRWidgetService, 'fetchMetrics').mockReturnValue(returnServicePromise(true));
jest.spyOn(vm, 'computeGraphData').mockImplementation(() => {});
vm.loadMetrics();
- setImmediate(() => {
- expect(MRWidgetService.fetchMetrics).toHaveBeenCalledWith(url);
- expect(vm.computeGraphData).toHaveBeenCalledWith(metrics, deployment_time);
- done();
- });
+
+ await waitForPromises();
+
+ expect(MRWidgetService.fetchMetrics).toHaveBeenCalledWith(url);
+ expect(vm.computeGraphData).toHaveBeenCalledWith(metrics, deployment_time);
});
});
});
@@ -184,7 +180,7 @@ describe('MemoryUsage', () => {
vm.hasMetrics = false;
vm.loadFailed = false;
- Vue.nextTick(() => {
+ nextTick(() => {
expect(el.querySelector('.js-usage-info.usage-info-loading')).toBeDefined();
expect(el.querySelector('.js-usage-info .usage-info-load-spinner')).toBeDefined();
@@ -203,7 +199,7 @@ describe('MemoryUsage', () => {
vm.loadFailed = false;
vm.memoryMetrics = metricsMockData.metrics.memory_values[0].values;
- Vue.nextTick(() => {
+ nextTick(() => {
expect(el.querySelector('.memory-graph-container')).toBeDefined();
expect(el.querySelector('.js-usage-info').innerText).toContain(messages.hasMetrics);
done();
@@ -215,7 +211,7 @@ describe('MemoryUsage', () => {
vm.hasMetrics = false;
vm.loadFailed = true;
- Vue.nextTick(() => {
+ nextTick(() => {
expect(el.querySelector('.js-usage-info.usage-info-failed')).toBeDefined();
expect(el.querySelector('.js-usage-info').innerText).toContain(messages.loadFailed);
@@ -228,7 +224,7 @@ describe('MemoryUsage', () => {
vm.hasMetrics = false;
vm.loadFailed = false;
- Vue.nextTick(() => {
+ nextTick(() => {
expect(el.querySelector('.js-usage-info.usage-info-unavailable')).toBeDefined();
expect(el.querySelector('.js-usage-info').innerText).toContain(messages.metricsUnavailable);
diff --git a/spec/frontend/vue_mr_widget/components/states/__snapshots__/mr_widget_pipeline_failed_spec.js.snap b/spec/frontend/vue_mr_widget/components/states/__snapshots__/mr_widget_pipeline_failed_spec.js.snap
index a124008b36a..98297630792 100644
--- a/spec/frontend/vue_mr_widget/components/states/__snapshots__/mr_widget_pipeline_failed_spec.js.snap
+++ b/spec/frontend/vue_mr_widget/components/states/__snapshots__/mr_widget_pipeline_failed_spec.js.snap
@@ -16,7 +16,7 @@ exports[`PipelineFailed should render error message with a disabled merge button
class="bold"
>
<gl-sprintf-stub
- message="The pipeline for this merge request did not complete. Push a new commit to fix the failure, or check the %{linkStart}troubleshooting documentation%{linkEnd} to see other possible actions."
+ message="Merge blocked: pipeline must succeed. Push a commit that fixes the failure, or %{linkStart}learn about other solutions.%{linkEnd}"
/>
</span>
</div>
diff --git a/spec/frontend/vue_mr_widget/components/states/commit_edit_spec.js b/spec/frontend/vue_mr_widget/components/states/commit_edit_spec.js
index c30f6f1dfd1..c0add94e6ed 100644
--- a/spec/frontend/vue_mr_widget/components/states/commit_edit_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/commit_edit_spec.js
@@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import CommitEdit from '~/vue_merge_request_widget/components/states/commit_edit.vue';
const testCommitMessage = 'Test commit message';
@@ -46,16 +47,15 @@ describe('Commits edit component', () => {
expect(findTextarea().element.value).toBe(testCommitMessage);
});
- it('emits an input event and receives changed value', () => {
+ it('emits an input event and receives changed value', async () => {
const changedCommitMessage = 'Changed commit message';
findTextarea().element.value = changedCommitMessage;
findTextarea().trigger('input');
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted().input[0]).toEqual([changedCommitMessage]);
- expect(findTextarea().element.value).toBe(changedCommitMessage);
- });
+ await nextTick();
+ expect(wrapper.emitted().input[0]).toEqual([changedCommitMessage]);
+ expect(findTextarea().element.value).toBe(changedCommitMessage);
});
});
diff --git a/spec/frontend/vue_mr_widget/components/states/merge_failed_pipeline_confirmation_dialog_spec.js b/spec/frontend/vue_mr_widget/components/states/merge_failed_pipeline_confirmation_dialog_spec.js
new file mode 100644
index 00000000000..0e1c38437f0
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/components/states/merge_failed_pipeline_confirmation_dialog_spec.js
@@ -0,0 +1,78 @@
+import { shallowMount } from '@vue/test-utils';
+import MergeFailedPipelineConfirmationDialog from '~/vue_merge_request_widget/components/states/merge_failed_pipeline_confirmation_dialog.vue';
+import { trimText } from 'helpers/text_helper';
+
+describe('MergeFailedPipelineConfirmationDialog', () => {
+ let wrapper;
+
+ const GlModal = {
+ template: `
+ <div>
+ <slot></slot>
+ <slot name="modal-footer"></slot>
+ </div>
+ `,
+ methods: {
+ hide: jest.fn(),
+ },
+ };
+
+ const createComponent = () => {
+ wrapper = shallowMount(MergeFailedPipelineConfirmationDialog, {
+ propsData: {
+ visible: true,
+ },
+ stubs: {
+ GlModal,
+ },
+ attachTo: document.body,
+ });
+ };
+
+ const findModal = () => wrapper.findComponent(GlModal);
+ const findMergeBtn = () => wrapper.find('[data-testid="merge-unverified-changes"]');
+ const findCancelBtn = () => wrapper.find('[data-testid="merge-cancel-btn"]');
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('should render informational text explaining why merging immediately can be dangerous', () => {
+ expect(trimText(wrapper.text())).toContain(
+ 'The latest pipeline for this merge request did not succeed. The latest changes are unverified. Are you sure you want to attempt to merge?',
+ );
+ });
+
+ it('should emit the mergeWithFailedPipeline event', () => {
+ findMergeBtn().vm.$emit('click');
+
+ expect(wrapper.emitted('mergeWithFailedPipeline')).toBeTruthy();
+ });
+
+ it('when the cancel button is clicked should emit cancel and call hide', () => {
+ jest.spyOn(findModal().vm, 'hide');
+
+ findCancelBtn().vm.$emit('click');
+
+ expect(wrapper.emitted('cancel')).toBeTruthy();
+ expect(findModal().vm.hide).toHaveBeenCalled();
+ });
+
+ it('should emit cancel when the hide event is emitted', () => {
+ findModal().vm.$emit('hide');
+
+ expect(wrapper.emitted('cancel')).toBeTruthy();
+ });
+
+ it('when modal is shown it will focus the cancel button', () => {
+ jest.spyOn(findCancelBtn().element, 'focus');
+
+ findModal().vm.$emit('shown');
+
+ expect(findCancelBtn().element.focus).toHaveBeenCalled();
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js
index 52a56af454f..7387ed2d5e9 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js
@@ -2,6 +2,7 @@ import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { trimText } from 'helpers/text_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import autoMergeEnabledComponent from '~/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue';
import { MWPS_MERGE_STRATEGY } from '~/vue_merge_request_widget/constants';
import eventHub from '~/vue_merge_request_widget/event_hub';
@@ -185,7 +186,7 @@ describe('MRWidgetAutoMergeEnabled', () => {
describe('methods', () => {
describe('cancelAutomaticMerge', () => {
- it('should set flag and call service then tell main component to update the widget with data', (done) => {
+ it('should set flag and call service then tell main component to update the widget with data', async () => {
factory({
...defaultMrProps(),
});
@@ -201,20 +202,20 @@ describe('MRWidgetAutoMergeEnabled', () => {
);
wrapper.vm.cancelAutomaticMerge();
- setImmediate(() => {
- expect(wrapper.vm.isCancellingAutoMerge).toBeTruthy();
- if (mergeRequestWidgetGraphql) {
- expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
- } else {
- expect(eventHub.$emit).toHaveBeenCalledWith('UpdateWidgetData', mrObj);
- }
- done();
- });
+
+ await waitForPromises();
+
+ expect(wrapper.vm.isCancellingAutoMerge).toBeTruthy();
+ if (mergeRequestWidgetGraphql) {
+ expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
+ } else {
+ expect(eventHub.$emit).toHaveBeenCalledWith('UpdateWidgetData', mrObj);
+ }
});
});
describe('removeSourceBranch', () => {
- it('should set flag and call service then request main component to update the widget', (done) => {
+ it('should set flag and call service then request main component to update the widget', async () => {
factory({
...defaultMrProps(),
});
@@ -227,14 +228,14 @@ describe('MRWidgetAutoMergeEnabled', () => {
);
wrapper.vm.removeSourceBranch();
- setImmediate(() => {
- expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
- expect(wrapper.vm.service.merge).toHaveBeenCalledWith({
- sha,
- auto_merge_strategy: MWPS_MERGE_STRATEGY,
- should_remove_source_branch: true,
- });
- done();
+
+ await waitForPromises();
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
+ expect(wrapper.vm.service.merge).toHaveBeenCalledWith({
+ sha,
+ auto_merge_strategy: MWPS_MERGE_STRATEGY,
+ should_remove_source_branch: true,
});
});
});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_commit_message_dropdown_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_commit_message_dropdown_spec.js
index 4c763f40cbe..663fabb761c 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_commit_message_dropdown_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_commit_message_dropdown_spec.js
@@ -1,5 +1,6 @@
import { GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import CommitMessageDropdown from '~/vue_merge_request_widget/components/states/commit_message_dropdown.vue';
const commits = [
@@ -51,11 +52,10 @@ describe('Commits message dropdown component', () => {
expect(findFirstDropdownElement().text()).toContain('Commit 1');
});
- it('should emit a commit title on selecting commit', () => {
+ it('should emit a commit title on selecting commit', async () => {
findFirstDropdownElement().vm.$emit('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted().input[0]).toEqual(['Update test.txt']);
- });
+ await nextTick();
+ expect(wrapper.emitted().input[0]).toEqual(['Update test.txt']);
});
});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_commits_header_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_commits_header_spec.js
index 4d05e732f48..2796403b7d0 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_commits_header_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_commits_header_spec.js
@@ -1,5 +1,6 @@
import { mount } from '@vue/test-utils';
import { GlSprintf } from '@gitlab/ui';
+import { nextTick } from 'vue';
import CommitsHeader from '~/vue_merge_request_widget/components/states/commits_header.vue';
describe('Commits header component', () => {
@@ -58,15 +59,14 @@ describe('Commits header component', () => {
expect(findCommitToggle().attributes('aria-label')).toBe('Expand');
});
- it('has a chevron-right icon', () => {
+ it('has a chevron-right icon', async () => {
createComponent();
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ expanded: false });
- return wrapper.vm.$nextTick().then(() => {
- expect(findCommitToggle().props('icon')).toBe('chevron-right');
- });
+ await nextTick();
+ expect(findCommitToggle().props('icon')).toBe('chevron-right');
});
describe('when squash is disabled', () => {
@@ -118,25 +118,19 @@ describe('Commits header component', () => {
wrapper.setData({ expanded: true });
});
- it('toggle has aria-label equal to collapse', (done) => {
- wrapper.vm.$nextTick(() => {
- expect(findCommitToggle().attributes('aria-label')).toBe('Collapse');
- done();
- });
+ it('toggle has aria-label equal to collapse', async () => {
+ await nextTick();
+ expect(findCommitToggle().attributes('aria-label')).toBe('Collapse');
});
- it('has a chevron-down icon', (done) => {
- wrapper.vm.$nextTick(() => {
- expect(findCommitToggle().props('icon')).toBe('chevron-down');
- done();
- });
+ it('has a chevron-down icon', async () => {
+ await nextTick();
+ expect(findCommitToggle().props('icon')).toBe('chevron-down');
});
- it('has a collapse text', (done) => {
- wrapper.vm.$nextTick(() => {
- expect(findHeaderWrapper().text()).toBe('Collapse');
- done();
- });
+ it('has a collapse text', async () => {
+ await nextTick();
+ expect(findHeaderWrapper().text()).toBe('Collapse');
});
});
});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
index ec222e66a97..9dcde3e4f33 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
@@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import { TEST_HOST } from 'helpers/test_constants';
import { removeBreakLine } from 'helpers/text_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
@@ -20,7 +21,7 @@ describe('MRWidgetConflicts', () => {
const resolveConflictsBtnText = 'Resolve conflicts';
const mergeLocallyBtnText = 'Merge locally';
- function createComponent(propsData = {}) {
+ async function createComponent(propsData = {}) {
wrapper = extendedWrapper(
shallowMount(ConflictsComponent, {
propsData,
@@ -55,7 +56,7 @@ describe('MRWidgetConflicts', () => {
});
}
- return wrapper.vm.$nextTick();
+ await nextTick();
}
afterEach(() => {
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js
index e0f1f091129..7d86e453bc7 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js
@@ -1,6 +1,7 @@
import { getByRole } from '@testing-library/dom';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import { OPEN_REVERT_MODAL, OPEN_CHERRY_PICK_MODAL } from '~/projects/commit/constants';
import modalEventHub from '~/projects/commit/event_hub';
import mergedComponent from '~/vue_merge_request_widget/components/states/mr_widget_merged.vue';
@@ -127,7 +128,7 @@ describe('MRWidgetMerged', () => {
describe('methods', () => {
describe('removeSourceBranch', () => {
- it('should set flag and call service then request main component to update the widget', (done) => {
+ it('should set flag and call service then request main component to update the widget', async () => {
jest.spyOn(vm.service, 'removeSourceBranch').mockReturnValue(
new Promise((resolve) => {
resolve({
@@ -139,14 +140,14 @@ describe('MRWidgetMerged', () => {
);
vm.removeSourceBranch();
- setImmediate(() => {
- const args = eventHub.$emit.mock.calls[0];
-
- expect(vm.isMakingRequest).toEqual(true);
- expect(args[0]).toEqual('MRWidgetUpdateRequested');
- expect(args[1]).not.toThrow();
- done();
- });
+
+ await waitForPromises();
+
+ const args = eventHub.$emit.mock.calls[0];
+
+ expect(vm.isMakingRequest).toEqual(true);
+ expect(args[0]).toEqual('MRWidgetUpdateRequested');
+ expect(args[1]).not.toThrow();
});
});
});
@@ -200,7 +201,7 @@ describe('MRWidgetMerged', () => {
it('hides button to copy commit SHA if SHA does not exist', (done) => {
vm.mr.mergeCommitSha = null;
- Vue.nextTick(() => {
+ nextTick(() => {
expect(selectors.copyMergeShaButton).toBe(null);
expect(vm.$el.querySelector('.mr-info-list').innerText).not.toContain('with');
done();
@@ -216,7 +217,7 @@ describe('MRWidgetMerged', () => {
it('should not show source branch deleted text', (done) => {
vm.mr.sourceBranchRemoved = false;
- Vue.nextTick(() => {
+ nextTick(() => {
expect(vm.$el.innerText).not.toContain('The source branch has been deleted');
done();
});
@@ -226,7 +227,7 @@ describe('MRWidgetMerged', () => {
vm.mr.isRemovingSourceBranch = true;
vm.mr.sourceBranchRemoved = false;
- Vue.nextTick(() => {
+ nextTick(() => {
expect(vm.$el.innerText).toContain('The source branch is being deleted');
expect(vm.$el.innerText).not.toContain('The source branch has been deleted');
done();
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_merging_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_merging_spec.js
index e6b2e9fa176..e16c897a49b 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_merging_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_merging_spec.js
@@ -1,6 +1,11 @@
import { shallowMount } from '@vue/test-utils';
+import simplePoll from '~/lib/utils/simple_poll';
import MrWidgetMerging from '~/vue_merge_request_widget/components/states/mr_widget_merging.vue';
+jest.mock('~/lib/utils/simple_poll', () =>
+ jest.fn().mockImplementation(jest.requireActual('~/lib/utils/simple_poll').default),
+);
+
describe('MRWidgetMerging', () => {
let wrapper;
@@ -11,6 +16,10 @@ describe('MRWidgetMerging', () => {
mr: {
targetBranchPath: '/branch-path',
targetBranch: 'branch',
+ transitionStateMachine() {},
+ },
+ service: {
+ poll: jest.fn().mockResolvedValue(),
},
},
stubs: {
@@ -46,4 +55,20 @@ describe('MRWidgetMerging', () => {
expect(wrapper.find('a').attributes('href')).toBe('/branch-path');
});
+
+ describe('initiateMergePolling', () => {
+ it('should call simplePoll', () => {
+ wrapper.vm.initiateMergePolling();
+
+ expect(simplePoll).toHaveBeenCalledWith(expect.any(Function), { timeout: 0 });
+ });
+
+ it('should call handleMergePolling', () => {
+ jest.spyOn(wrapper.vm, 'handleMergePolling').mockImplementation(() => {});
+
+ wrapper.vm.initiateMergePolling();
+
+ expect(wrapper.vm.handleMergePolling).toHaveBeenCalled();
+ });
+ });
});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js
index 936d673768c..ddce07954ab 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js
@@ -1,9 +1,10 @@
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import MissingBranchComponent from '~/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue';
let wrapper;
-function factory(sourceBranchRemoved, mergeRequestWidgetGraphql) {
+async function factory(sourceBranchRemoved, mergeRequestWidgetGraphql) {
wrapper = shallowMount(MissingBranchComponent, {
propsData: {
mr: { sourceBranchRemoved },
@@ -19,7 +20,7 @@ function factory(sourceBranchRemoved, mergeRequestWidgetGraphql) {
wrapper.setData({ state: { sourceBranchExists: !sourceBranchRemoved } });
}
- return wrapper.vm.$nextTick();
+ await nextTick();
}
describe('MRWidgetMissingBranch', () => {
@@ -40,7 +41,7 @@ describe('MRWidgetMissingBranch', () => {
async ({ sourceBranchRemoved, branchName }) => {
await factory(sourceBranchRemoved, mergeRequestWidgetGraphql);
- expect(wrapper.find('[data-testid="missingBranchName"]').text()).toContain(branchName);
+ expect(wrapper.find('[data-testid="widget-content"]').text()).toContain(branchName);
},
);
});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_nothing_to_merge_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_nothing_to_merge_spec.js
index 2c04905d3a9..c7c0b69425d 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_nothing_to_merge_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_nothing_to_merge_spec.js
@@ -1,4 +1,4 @@
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import NothingToMerge from '~/vue_merge_request_widget/components/states/nothing_to_merge.vue';
describe('NothingToMerge', () => {
@@ -20,7 +20,7 @@ describe('NothingToMerge', () => {
it('should not show new blob link if there is no link available', () => {
vm.mr.newBlobPath = null;
- Vue.nextTick(() => {
+ nextTick(() => {
expect(vm.$el.querySelector('[data-testid="createFileButton"]')).toEqual(null);
});
});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
index f4ecebbb40c..78585ed75bc 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
@@ -1,12 +1,14 @@
import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
+import { nextTick } from 'vue';
import { GlSprintf } from '@gitlab/ui';
+import waitForPromises from 'helpers/wait_for_promises';
import simplePoll from '~/lib/utils/simple_poll';
import CommitEdit from '~/vue_merge_request_widget/components/states/commit_edit.vue';
import CommitMessageDropdown from '~/vue_merge_request_widget/components/states/commit_message_dropdown.vue';
import CommitsHeader from '~/vue_merge_request_widget/components/states/commits_header.vue';
import ReadyToMerge from '~/vue_merge_request_widget/components/states/ready_to_merge.vue';
import SquashBeforeMerge from '~/vue_merge_request_widget/components/states/squash_before_merge.vue';
+import MergeFailedPipelineConfirmationDialog from '~/vue_merge_request_widget/components/states/merge_failed_pipeline_confirmation_dialog.vue';
import { MWPS_MERGE_STRATEGY } from '~/vue_merge_request_widget/constants';
import eventHub from '~/vue_merge_request_widget/event_hub';
@@ -61,6 +63,11 @@ const createTestService = () => ({
});
let wrapper;
+
+const findMergeButton = () => wrapper.find('[data-testid="merge-button"]');
+const findPipelineFailedConfirmModal = () =>
+ wrapper.findComponent(MergeFailedPipelineConfirmationDialog);
+
const createComponent = (customConfig = {}, mergeRequestWidgetGraphql = false) => {
wrapper = shallowMount(ReadyToMerge, {
propsData: {
@@ -132,33 +139,13 @@ describe('ReadyToMerge', () => {
});
});
- describe('mergeButtonVariant', () => {
+ describe('Merge Button Variant', () => {
it('defaults to confirm class', () => {
createComponent({
mr: { availableAutoMergeStrategies: [] },
});
- expect(wrapper.vm.mergeButtonVariant).toEqual('confirm');
- });
-
- it('returns confirm class for success status', () => {
- createComponent({
- mr: { availableAutoMergeStrategies: [], pipeline: true },
- });
-
- expect(wrapper.vm.mergeButtonVariant).toEqual('confirm');
- });
-
- it('returns confirm class for pending status', () => {
- createComponent();
-
- expect(wrapper.vm.mergeButtonVariant).toEqual('confirm');
- });
-
- it('returns danger class for failed status', () => {
- createComponent({ mr: { hasCI: true } });
-
- expect(wrapper.vm.mergeButtonVariant).toEqual('danger');
+ expect(findMergeButton().attributes('variant')).toBe('confirm');
});
});
@@ -196,7 +183,7 @@ describe('ReadyToMerge', () => {
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ isMergingImmediately: true });
- await Vue.nextTick();
+ await nextTick();
expect(wrapper.vm.mergeButtonText).toEqual('Merge in progress');
});
@@ -266,7 +253,7 @@ describe('ReadyToMerge', () => {
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ isMakingRequest: true });
- await Vue.nextTick();
+ await nextTick();
expect(wrapper.vm.isMergeButtonDisabled).toBe(true);
});
@@ -275,110 +262,86 @@ describe('ReadyToMerge', () => {
describe('methods', () => {
describe('handleMergeButtonClick', () => {
- const returnPromise = (status) =>
- new Promise((resolve) => {
- resolve({
- data: {
- status,
- },
- });
- });
+ const response = (status) => ({
+ data: {
+ status,
+ },
+ });
- it('should handle merge when pipeline succeeds', (done) => {
+ it('should handle merge when pipeline succeeds', async () => {
createComponent();
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
jest
.spyOn(wrapper.vm.service, 'merge')
- .mockReturnValue(returnPromise('merge_when_pipeline_succeeds'));
+ .mockResolvedValue(response('merge_when_pipeline_succeeds'));
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ removeSourceBranch: false });
wrapper.vm.handleMergeButtonClick(true);
- setImmediate(() => {
- expect(wrapper.vm.isMakingRequest).toBeTruthy();
- expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
- expect(eventHub.$emit).toHaveBeenCalledWith('StateMachineValueChanged', {
- transition: 'start-auto-merge',
- });
+ await waitForPromises();
- const params = wrapper.vm.service.merge.mock.calls[0][0];
-
- expect(params).toEqual(
- expect.objectContaining({
- sha: wrapper.vm.mr.sha,
- commit_message: wrapper.vm.mr.commitMessage,
- should_remove_source_branch: false,
- auto_merge_strategy: 'merge_when_pipeline_succeeds',
- }),
- );
- done();
+ expect(wrapper.vm.isMakingRequest).toBeTruthy();
+ expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
+ expect(eventHub.$emit).toHaveBeenCalledWith('StateMachineValueChanged', {
+ transition: 'start-auto-merge',
});
+
+ const params = wrapper.vm.service.merge.mock.calls[0][0];
+
+ expect(params).toEqual(
+ expect.objectContaining({
+ sha: wrapper.vm.mr.sha,
+ commit_message: wrapper.vm.mr.commitMessage,
+ should_remove_source_branch: false,
+ auto_merge_strategy: 'merge_when_pipeline_succeeds',
+ }),
+ );
});
- it('should handle merge failed', (done) => {
+ it('should handle merge failed', async () => {
createComponent();
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
- jest.spyOn(wrapper.vm.service, 'merge').mockReturnValue(returnPromise('failed'));
+ jest.spyOn(wrapper.vm.service, 'merge').mockResolvedValue(response('failed'));
wrapper.vm.handleMergeButtonClick(false, true);
- setImmediate(() => {
- expect(wrapper.vm.isMakingRequest).toBeTruthy();
- expect(eventHub.$emit).toHaveBeenCalledWith('FailedToMerge', undefined);
+ await waitForPromises();
- const params = wrapper.vm.service.merge.mock.calls[0][0];
+ expect(wrapper.vm.isMakingRequest).toBeTruthy();
+ expect(eventHub.$emit).toHaveBeenCalledWith('FailedToMerge', undefined);
- expect(params.should_remove_source_branch).toBeTruthy();
- expect(params.auto_merge_strategy).toBeUndefined();
- done();
- });
+ const params = wrapper.vm.service.merge.mock.calls[0][0];
+
+ expect(params.should_remove_source_branch).toBeTruthy();
+ expect(params.auto_merge_strategy).toBeUndefined();
});
- it('should handle merge action accepted case', (done) => {
+ it('should handle merge action accepted case', async () => {
createComponent();
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
- jest.spyOn(wrapper.vm.service, 'merge').mockReturnValue(returnPromise('success'));
- jest.spyOn(wrapper.vm, 'initiateMergePolling').mockImplementation(() => {});
+ jest.spyOn(wrapper.vm.service, 'merge').mockResolvedValue(response('success'));
+ jest.spyOn(wrapper.vm.mr, 'transitionStateMachine');
wrapper.vm.handleMergeButtonClick();
expect(eventHub.$emit).toHaveBeenCalledWith('StateMachineValueChanged', {
transition: 'start-merge',
});
- setImmediate(() => {
- expect(wrapper.vm.isMakingRequest).toBeTruthy();
- expect(wrapper.vm.initiateMergePolling).toHaveBeenCalled();
-
- const params = wrapper.vm.service.merge.mock.calls[0][0];
+ await waitForPromises();
- expect(params.should_remove_source_branch).toBeTruthy();
- expect(params.auto_merge_strategy).toBeUndefined();
- done();
+ expect(wrapper.vm.isMakingRequest).toBeTruthy();
+ expect(wrapper.vm.mr.transitionStateMachine).toHaveBeenCalledWith({
+ transition: 'start-merge',
});
- });
- });
-
- describe('initiateMergePolling', () => {
- it('should call simplePoll', () => {
- createComponent();
-
- wrapper.vm.initiateMergePolling();
-
- expect(simplePoll).toHaveBeenCalledWith(expect.any(Function), { timeout: 0 });
- });
- it('should call handleMergePolling', () => {
- createComponent();
-
- jest.spyOn(wrapper.vm, 'handleMergePolling').mockImplementation(() => {});
+ const params = wrapper.vm.service.merge.mock.calls[0][0];
- wrapper.vm.initiateMergePolling();
-
- expect(wrapper.vm.handleMergePolling).toHaveBeenCalled();
+ expect(params.should_remove_source_branch).toBeTruthy();
+ expect(params.auto_merge_strategy).toBeUndefined();
});
});
@@ -396,20 +359,17 @@ describe('ReadyToMerge', () => {
});
describe('handleRemoveBranchPolling', () => {
- const returnPromise = (state) =>
- new Promise((resolve) => {
- resolve({
- data: {
- source_branch_exists: state,
- },
- });
- });
+ const response = (state) => ({
+ data: {
+ source_branch_exists: state,
+ },
+ });
- it('should call start and stop polling when MR merged', (done) => {
+ it('should call start and stop polling when MR merged', async () => {
createComponent();
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
- jest.spyOn(wrapper.vm.service, 'poll').mockReturnValue(returnPromise(false));
+ jest.spyOn(wrapper.vm.service, 'poll').mockResolvedValue(response(false));
let cpc = false; // continuePollingCalled
let spc = false; // stopPollingCalled
@@ -422,28 +382,27 @@ describe('ReadyToMerge', () => {
spc = true;
},
);
- setImmediate(() => {
- expect(wrapper.vm.service.poll).toHaveBeenCalled();
- const args = eventHub.$emit.mock.calls[0];
+ await waitForPromises();
- expect(args[0]).toEqual('MRWidgetUpdateRequested');
- expect(args[1]).toBeDefined();
- args[1]();
+ expect(wrapper.vm.service.poll).toHaveBeenCalled();
- expect(eventHub.$emit).toHaveBeenCalledWith('SetBranchRemoveFlag', [false]);
+ const args = eventHub.$emit.mock.calls[0];
- expect(cpc).toBeFalsy();
- expect(spc).toBeTruthy();
+ expect(args[0]).toEqual('MRWidgetUpdateRequested');
+ expect(args[1]).toBeDefined();
+ args[1]();
- done();
- });
+ expect(eventHub.$emit).toHaveBeenCalledWith('SetBranchRemoveFlag', [false]);
+
+ expect(cpc).toBeFalsy();
+ expect(spc).toBeTruthy();
});
- it('should continue polling until MR is merged', (done) => {
+ it('should continue polling until MR is merged', async () => {
createComponent();
- jest.spyOn(wrapper.vm.service, 'poll').mockReturnValue(returnPromise(true));
+ jest.spyOn(wrapper.vm.service, 'poll').mockResolvedValue(response(true));
let cpc = false; // continuePollingCalled
let spc = false; // stopPollingCalled
@@ -456,12 +415,11 @@ describe('ReadyToMerge', () => {
spc = true;
},
);
- setImmediate(() => {
- expect(cpc).toBeTruthy();
- expect(spc).toBeFalsy();
- done();
- });
+ await waitForPromises();
+
+ expect(cpc).toBeTruthy();
+ expect(spc).toBeFalsy();
});
});
});
@@ -710,7 +668,7 @@ describe('ReadyToMerge', () => {
commitsWithoutMergeCommits: {},
},
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findCommitEditElements().length).toBe(2);
});
@@ -794,4 +752,24 @@ describe('ReadyToMerge', () => {
});
});
});
+
+ describe('Merge button when pipeline has failed', () => {
+ beforeEach(() => {
+ createComponent({
+ mr: { pipeline: {}, isPipelineFailed: true, availableAutoMergeStrategies: [] },
+ });
+ });
+
+ it('should display the correct merge text', () => {
+ expect(findMergeButton().text()).toBe('Merge...');
+ });
+
+ it('should display confirmation modal when merge button is clicked', async () => {
+ expect(findPipelineFailedConfirmModal().props()).toEqual({ visible: false });
+
+ await findMergeButton().vm.$emit('click');
+
+ expect(findPipelineFailedConfirmModal().props()).toEqual({ visible: true });
+ });
+ });
});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_squash_before_merge_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_squash_before_merge_spec.js
index 6abdbd11f5e..6ea2e8675d3 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_squash_before_merge_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_squash_before_merge_spec.js
@@ -1,16 +1,13 @@
import { GlFormCheckbox, GlLink } from '@gitlab/ui';
-import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
import SquashBeforeMerge from '~/vue_merge_request_widget/components/states/squash_before_merge.vue';
import { SQUASH_BEFORE_MERGE } from '~/vue_merge_request_widget/i18n';
-const localVue = createLocalVue();
-
describe('Squash before merge component', () => {
let wrapper;
const createComponent = (props) => {
- wrapper = shallowMount(localVue.extend(SquashBeforeMerge), {
- localVue,
+ wrapper = shallowMount(SquashBeforeMerge, {
propsData: {
...props,
},
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js
index 4070ca8d8dc..4998147c6b6 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js
@@ -1,4 +1,5 @@
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
+import waitForPromises from 'helpers/wait_for_promises';
import WorkInProgress from '~/vue_merge_request_widget/components/states/work_in_progress.vue';
import toast from '~/vue_shared/plugins/global_toast';
import eventHub from '~/vue_merge_request_widget/event_hub';
@@ -47,7 +48,7 @@ describe('Wip', () => {
};
describe('handleRemoveDraft', () => {
- it('should make a request to service and handle response', (done) => {
+ it('should make a request to service and handle response', async () => {
const vm = createComponent();
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
@@ -60,12 +61,12 @@ describe('Wip', () => {
);
vm.handleRemoveDraft();
- setImmediate(() => {
- expect(vm.isMakingRequest).toBeTruthy();
- expect(eventHub.$emit).toHaveBeenCalledWith('UpdateWidgetData', mrObj);
- expect(toast).toHaveBeenCalledWith('Marked as ready. Merging is now allowed.');
- done();
- });
+
+ await waitForPromises();
+
+ expect(vm.isMakingRequest).toBeTruthy();
+ expect(eventHub.$emit).toHaveBeenCalledWith('UpdateWidgetData', mrObj);
+ expect(toast).toHaveBeenCalledWith('Marked as ready. Merging is now allowed.');
});
});
});
@@ -91,13 +92,12 @@ describe('Wip', () => {
);
});
- it('should not show removeWIP button is user cannot update MR', (done) => {
+ it('should not show removeWIP button is user cannot update MR', async () => {
vm.mr.removeWIPPath = '';
- Vue.nextTick(() => {
- expect(el.querySelector('.js-remove-draft')).toEqual(null);
- done();
- });
+ await nextTick();
+
+ expect(el.querySelector('.js-remove-draft')).toEqual(null);
});
});
});
diff --git a/spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js b/spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js
index 9048975875a..b7c22b403aa 100644
--- a/spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js
+++ b/spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js
@@ -1,6 +1,7 @@
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+import { nextTick } from 'vue';
import axios from '~/lib/utils/axios_utils';
import Poll from '~/lib/utils/poll';
import MrWidgetExpanableSection from '~/vue_merge_request_widget/components/mr_widget_expandable_section.vue';
@@ -39,15 +40,14 @@ describe('MrWidgetTerraformConainer', () => {
});
describe('when data is loading', () => {
- beforeEach(() => {
+ beforeEach(async () => {
mockPollingApi(200, plans, {});
- return mountWrapper().then(() => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ loading: true });
- return wrapper.vm.$nextTick();
- });
+ await mountWrapper();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
+ wrapper.setData({ loading: true });
+ await nextTick();
});
it('diplays loading skeleton', () => {
diff --git a/spec/frontend/vue_mr_widget/deployment/deployment_actions_spec.js b/spec/frontend/vue_mr_widget/deployment/deployment_actions_spec.js
index 31ade17e50a..a285d26f404 100644
--- a/spec/frontend/vue_mr_widget/deployment/deployment_actions_spec.js
+++ b/spec/frontend/vue_mr_widget/deployment/deployment_actions_spec.js
@@ -1,5 +1,7 @@
import { mount } from '@vue/test-utils';
+import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
+import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import { visitUrl } from '~/lib/utils/url_utility';
import {
CREATED,
@@ -20,6 +22,11 @@ import {
jest.mock('~/flash');
jest.mock('~/lib/utils/url_utility');
+jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal', () => {
+ return {
+ confirmAction: jest.fn(),
+ };
+});
describe('DeploymentAction component', () => {
let wrapper;
@@ -51,6 +58,7 @@ describe('DeploymentAction component', () => {
afterEach(() => {
wrapper.destroy();
+ confirmAction.mockReset();
});
describe('actions do not appear when conditions are unmet', () => {
@@ -95,16 +103,6 @@ describe('DeploymentAction component', () => {
'$configConst action',
({ configConst, computedDeploymentStatus, displayConditionChanges, finderFn, endpoint }) => {
describe(`${configConst} action`, () => {
- const confirmAction = () => {
- jest.spyOn(window, 'confirm').mockReturnValueOnce(true);
- finderFn().trigger('click');
- };
-
- const rejectAction = () => {
- jest.spyOn(window, 'confirm').mockReturnValueOnce(false);
- finderFn().trigger('click');
- };
-
beforeEach(() => {
factory({
propsData: {
@@ -125,13 +123,18 @@ describe('DeploymentAction component', () => {
describe('should show a confirm dialog but not call executeInlineAction when declined', () => {
beforeEach(() => {
executeActionSpy.mockResolvedValueOnce();
- rejectAction();
+ confirmAction.mockResolvedValueOnce(false);
+ finderFn().trigger('click');
});
it('should show the confirm dialog', () => {
- expect(window.confirm).toHaveBeenCalled();
- expect(window.confirm).toHaveBeenCalledWith(
+ expect(confirmAction).toHaveBeenCalled();
+ expect(confirmAction).toHaveBeenCalledWith(
actionButtonMocks[configConst].confirmMessage,
+ {
+ primaryBtnVariant: actionButtonMocks[configConst].buttonVariant,
+ primaryBtnText: actionButtonMocks[configConst].buttonText,
+ },
);
});
@@ -143,13 +146,18 @@ describe('DeploymentAction component', () => {
describe('should show a confirm dialog and call executeInlineAction when accepted', () => {
beforeEach(() => {
executeActionSpy.mockResolvedValueOnce();
- confirmAction();
+ confirmAction.mockResolvedValueOnce(true);
+ finderFn().trigger('click');
});
it('should show the confirm dialog', () => {
- expect(window.confirm).toHaveBeenCalled();
- expect(window.confirm).toHaveBeenCalledWith(
+ expect(confirmAction).toHaveBeenCalled();
+ expect(confirmAction).toHaveBeenCalledWith(
actionButtonMocks[configConst].confirmMessage,
+ {
+ primaryBtnVariant: actionButtonMocks[configConst].buttonVariant,
+ primaryBtnText: actionButtonMocks[configConst].buttonText,
+ },
);
});
@@ -164,11 +172,15 @@ describe('DeploymentAction component', () => {
describe('response includes redirect_url', () => {
const url = '/root/example';
- beforeEach(() => {
+ beforeEach(async () => {
executeActionSpy.mockResolvedValueOnce({
data: { redirect_url: url },
});
- confirmAction();
+
+ await waitForPromises();
+
+ confirmAction.mockResolvedValueOnce(true);
+ finderFn().trigger('click');
});
it('calls visit url with the redirect_url', () => {
@@ -178,9 +190,13 @@ describe('DeploymentAction component', () => {
});
describe('it should call the executeAction method ', () => {
- beforeEach(() => {
+ beforeEach(async () => {
jest.spyOn(wrapper.vm, 'executeAction').mockImplementation();
- confirmAction();
+
+ await waitForPromises();
+
+ confirmAction.mockResolvedValueOnce(true);
+ finderFn().trigger('click');
});
it('calls with the expected arguments', () => {
@@ -193,9 +209,13 @@ describe('DeploymentAction component', () => {
});
describe('when executeInlineAction errors', () => {
- beforeEach(() => {
+ beforeEach(async () => {
executeActionSpy.mockRejectedValueOnce();
- confirmAction();
+
+ await waitForPromises();
+
+ confirmAction.mockResolvedValueOnce(true);
+ finderFn().trigger('click');
});
it('should call createFlash with error message', () => {
diff --git a/spec/frontend/vue_mr_widget/deployment/deployment_mock_data.js b/spec/frontend/vue_mr_widget/deployment/deployment_mock_data.js
index 2083dc88681..e98b1160ae4 100644
--- a/spec/frontend/vue_mr_widget/deployment/deployment_mock_data.js
+++ b/spec/frontend/vue_mr_widget/deployment/deployment_mock_data.js
@@ -9,6 +9,7 @@ const actionButtonMocks = {
[STOPPING]: {
actionName: STOPPING,
buttonText: 'Stop environment',
+ buttonVariant: 'danger',
busyText: 'This environment is being deployed',
confirmMessage: 'Are you sure you want to stop this environment?',
errorMessage: 'Something went wrong while stopping this environment. Please try again.',
@@ -16,6 +17,7 @@ const actionButtonMocks = {
[DEPLOYING]: {
actionName: DEPLOYING,
buttonText: 'Deploy',
+ buttonVariant: 'confirm',
busyText: 'This environment is being deployed',
confirmMessage: 'Are you sure you want to deploy this environment?',
errorMessage: 'Something went wrong while deploying this environment. Please try again.',
@@ -23,6 +25,7 @@ const actionButtonMocks = {
[REDEPLOYING]: {
actionName: REDEPLOYING,
buttonText: 'Re-deploy',
+ buttonVariant: 'confirm',
busyText: 'This environment is being re-deployed',
confirmMessage: 'Are you sure you want to re-deploy this environment?',
errorMessage: 'Something went wrong while deploying this environment. Please try again.',
diff --git a/spec/frontend/vue_mr_widget/extentions/accessibility/index_spec.js b/spec/frontend/vue_mr_widget/extentions/accessibility/index_spec.js
new file mode 100644
index 00000000000..a9fe29a484a
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/extentions/accessibility/index_spec.js
@@ -0,0 +1,125 @@
+import MockAdapter from 'axios-mock-adapter';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { trimText } from 'helpers/text_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import axios from '~/lib/utils/axios_utils';
+import extensionsContainer from '~/vue_merge_request_widget/components/extensions/container';
+import { registerExtension } from '~/vue_merge_request_widget/components/extensions';
+import accessibilityExtension from '~/vue_merge_request_widget/extensions/accessibility';
+import httpStatusCodes from '~/lib/utils/http_status';
+import { accessibilityReportResponseErrors, accessibilityReportResponseSuccess } from './mock_data';
+
+describe('Accessibility extension', () => {
+ let wrapper;
+ let mock;
+
+ registerExtension(accessibilityExtension);
+
+ const endpoint = '/root/repo/-/merge_requests/4/accessibility_reports.json';
+
+ const mockApi = (statusCode, data) => {
+ mock.onGet(endpoint).reply(statusCode, data);
+ };
+
+ const findToggleCollapsedButton = () => wrapper.findByTestId('toggle-button');
+ const findAllExtensionListItems = () => wrapper.findAllByTestId('extension-list-item');
+
+ const createComponent = () => {
+ wrapper = mountExtended(extensionsContainer, {
+ propsData: {
+ mr: {
+ accessibilityReportPath: endpoint,
+ },
+ },
+ });
+ };
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ mock.restore();
+ });
+
+ describe('summary', () => {
+ it('displays loading text', () => {
+ mockApi(httpStatusCodes.OK, accessibilityReportResponseErrors);
+
+ createComponent();
+
+ expect(wrapper.text()).toBe('Accessibility scanning results are being parsed');
+ });
+
+ it('displays failed loading text', async () => {
+ mockApi(httpStatusCodes.INTERNAL_SERVER_ERROR);
+
+ createComponent();
+
+ await waitForPromises();
+
+ expect(wrapper.text()).toBe('Accessibility scanning failed loading results');
+ });
+
+ it('displays detected errors', async () => {
+ mockApi(httpStatusCodes.OK, accessibilityReportResponseErrors);
+
+ createComponent();
+
+ await waitForPromises();
+
+ expect(wrapper.text()).toBe(
+ 'Accessibility scanning detected 8 issues for the source branch only',
+ );
+ });
+
+ it('displays no detected errors', async () => {
+ mockApi(httpStatusCodes.OK, accessibilityReportResponseSuccess);
+
+ createComponent();
+
+ await waitForPromises();
+
+ expect(wrapper.text()).toBe(
+ 'Accessibility scanning detected no issues for the source branch only',
+ );
+ });
+ });
+
+ describe('expanded data', () => {
+ beforeEach(async () => {
+ mockApi(httpStatusCodes.OK, accessibilityReportResponseErrors);
+
+ createComponent();
+
+ await waitForPromises();
+
+ findToggleCollapsedButton().trigger('click');
+
+ await waitForPromises();
+ });
+
+ it('displays all report list items', async () => {
+ expect(findAllExtensionListItems()).toHaveLength(10);
+ });
+
+ it('displays report list item formatted', () => {
+ const text = {
+ newError: trimText(findAllExtensionListItems().at(0).text()),
+ resolvedError: findAllExtensionListItems().at(3).text(),
+ existingError: trimText(findAllExtensionListItems().at(8).text()),
+ };
+
+ expect(text.newError).toBe(
+ 'New The accessibility scanning found an error of the following type: WCAG2AA.Principle2.Guideline2_4.2_4_1.H64.1 Learn more Message: Iframe element requires a non-empty title attribute that identifies the frame.',
+ );
+ expect(text.resolvedError).toBe(
+ 'The accessibility scanning found an error of the following type: WCAG2AA.Principle1.Guideline1_1.1_1_1.H30.2 Learn more Message: Img element is the only content of the link, but is missing alt text. The alt text should describe the purpose of the link.',
+ );
+ expect(text.existingError).toBe(
+ 'The accessibility scanning found an error of the following type: WCAG2AA.Principle2.Guideline2_4.2_4_1.H64.1 Learn more Message: Iframe element requires a non-empty title attribute that identifies the frame.',
+ );
+ });
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/extentions/accessibility/mock_data.js b/spec/frontend/vue_mr_widget/extentions/accessibility/mock_data.js
new file mode 100644
index 00000000000..06dc93d101f
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/extentions/accessibility/mock_data.js
@@ -0,0 +1,137 @@
+export const accessibilityReportResponseErrors = {
+ status: 'failed',
+ new_errors: [
+ {
+ code: 'WCAG2AA.Principle2.Guideline2_4.2_4_1.H64.1',
+ type: 'error',
+ type_code: 1,
+ message: 'Iframe element requires a non-empty title attribute that identifies the frame.',
+ context:
+ '<iframe height="0" width="0" style="display: none; visibility: hidden;" src="//10421980.fls.doubleclick.net/activityi;src=10421980;type=count0;cat=globa0;ord=6271888671448;gtm=2wg1c0;auiddc=40010797.1642181125;u1=undefined;u2=undefined;u3=undefined;u...',
+ selector: 'html > body > iframe:nth-child(42)',
+ runner: 'htmlcs',
+ runner_extras: {},
+ },
+ {
+ code: 'WCAG2AA.Principle3.Guideline3_2.3_2_2.H32.2',
+ type: 'error',
+ type_code: 1,
+ message:
+ 'This form does not contain a submit button, which creates issues for those who cannot submit the form using the keyboard. Submit buttons are INPUT elements with type attribute "submit" or "image", or BUTTON elements with type "submit" or omitted/invalid.',
+ context:
+ '<form class="challenge-form" id="challenge-form" action="/users/sign_in?__cf_chl_jschl_tk__=xoagAHj9DXTTDveypAmMkakkNQgeWc6LmZA53YyDeSg-1642181129-0-gaNycGzNB1E" method="POST" enctype="application/x-www-form-urlencoded">\n <input type="hidden" name...',
+ selector: '#challenge-form',
+ runner: 'htmlcs',
+ runner_extras: {},
+ },
+ {
+ code: 'WCAG2AA.Principle2.Guideline2_4.2_4_1.H64.1',
+ type: 'error',
+ type_code: 1,
+ message: 'Iframe element requires a non-empty title attribute that identifies the frame.',
+ context: '<iframe style="display: none;"></iframe>',
+ selector: 'html > body > iframe',
+ runner: 'htmlcs',
+ runner_extras: {},
+ },
+ ],
+ resolved_errors: [
+ {
+ code: 'WCAG2AA.Principle2.Guideline2_4.2_4_1.H64.1',
+ type: 'error',
+ type_code: 1,
+ message: 'Iframe element requires a non-empty title attribute that identifies the frame.',
+ context:
+ '<iframe height="0" width="0" style="display: none; visibility: hidden;" src="//10421980.fls.doubleclick.net/activityi;src=10421980;type=count0;cat=globa0;ord=6722452746146;gtm=2wg1a0;auiddc=716711306.1642082367;u1=undefined;u2=undefined;u3=undefined;...',
+ selector: 'html > body > iframe:nth-child(42)',
+ runner: 'htmlcs',
+ runner_extras: {},
+ },
+ {
+ code: 'WCAG2AA.Principle3.Guideline3_2.3_2_2.H32.2',
+ type: 'error',
+ type_code: 1,
+ message:
+ 'This form does not contain a submit button, which creates issues for those who cannot submit the form using the keyboard. Submit buttons are INPUT elements with type attribute "submit" or "image", or BUTTON elements with type "submit" or omitted/invalid.',
+ context:
+ '<form class="challenge-form" id="challenge-form" action="/users/sign_in?__cf_chl_jschl_tk__=vDKZT2hjxWCstlWz2wtxsLdqLF79rM4IsoxzMgY6Lfw-1642082370-0-gaNycGzNB2U" method="POST" enctype="application/x-www-form-urlencoded">\n <input type="hidden" name...',
+ selector: '#challenge-form',
+ runner: 'htmlcs',
+ runner_extras: {},
+ },
+ ],
+ existing_errors: [
+ {
+ code: 'WCAG2AA.Principle1.Guideline1_1.1_1_1.H30.2',
+ type: 'error',
+ type_code: 1,
+ message:
+ 'Img element is the only content of the link, but is missing alt text. The alt text should describe the purpose of the link.',
+ context: '<a href="/" data-nav="logo">\n<img src="/images/icons/logos/...</a>',
+ selector: '#navigation-mobile > header > a',
+ runner: 'htmlcs',
+ runner_extras: {},
+ },
+ {
+ code: 'WCAG2AA.Principle1.Guideline1_1.1_1_1.H37',
+ type: 'error',
+ type_code: 1,
+ message:
+ 'Img element missing an alt attribute. Use the alt attribute to specify a short text alternative.',
+ context: '<img src="/images/icons/slp-hamburger.svg" class="slp-inline-block slp-mr-8">',
+ selector: '#slpMobileNavActive > img',
+ runner: 'htmlcs',
+ runner_extras: {},
+ },
+ {
+ code: 'WCAG2AA.Principle1.Guideline1_1.1_1_1.H37',
+ type: 'error',
+ type_code: 1,
+ message:
+ 'Img element missing an alt attribute. Use the alt attribute to specify a short text alternative.',
+ context: '<img src="/images/icons/slp-caret-down.svg">',
+ selector: '#navigation-mobile > div:nth-child(2) > div:nth-child(2) > button > div > img',
+ runner: 'htmlcs',
+ runner_extras: {},
+ },
+ {
+ code: 'WCAG2AA.Principle1.Guideline1_1.1_1_1.H37',
+ type: 'error',
+ type_code: 1,
+ message:
+ 'Img element missing an alt attribute. Use the alt attribute to specify a short text alternative.',
+ context: '<img src="/images/icons/slp-caret-down.svg">',
+ selector: '#navigation-mobile > div:nth-child(2) > div:nth-child(3) > button > div > img',
+ runner: 'htmlcs',
+ runner_extras: {},
+ },
+ {
+ code: 'WCAG2AA.Principle1.Guideline1_1.1_1_1.H37',
+ type: 'error',
+ type_code: 1,
+ message:
+ 'Img element missing an alt attribute. Use the alt attribute to specify a short text alternative.',
+ context: '<img src="/images/icons/slp-caret-down.svg">',
+ selector: '#navigation-mobile > div:nth-child(2) > div:nth-child(4) > button > div > img',
+ runner: 'htmlcs',
+ runner_extras: {},
+ },
+ ],
+ summary: {
+ total: 8,
+ resolved: 2,
+ errored: 8,
+ },
+};
+
+export const accessibilityReportResponseSuccess = {
+ status: 'success',
+ new_errors: [],
+ resolved_errors: [],
+ existing_errors: [],
+ summary: {
+ total: 0,
+ resolved: 0,
+ errored: 0,
+ },
+};
diff --git a/spec/frontend/vue_shared/alert_details/alert_details_spec.js b/spec/frontend/vue_shared/alert_details/alert_details_spec.js
index 221beed744b..7ee6e29e6de 100644
--- a/spec/frontend/vue_shared/alert_details/alert_details_spec.js
+++ b/spec/frontend/vue_shared/alert_details/alert_details_spec.js
@@ -2,6 +2,7 @@ import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
+import { nextTick } from 'vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { joinPaths } from '~/lib/utils/url_utility';
@@ -216,17 +217,16 @@ describe('AlertDetails', () => {
expect(findCreateIncidentBtn().exists()).toBe(false);
});
- it('should display "Create incident" button when incident doesn\'t exist yet', () => {
+ it('should display "Create incident" button when incident doesn\'t exist yet', async () => {
const issue = null;
mountComponent({
mountMethod: mount,
data: { alert: { ...mockAlert, issue } },
});
- return wrapper.vm.$nextTick().then(() => {
- expect(findViewIncidentBtn().exists()).toBe(false);
- expect(findCreateIncidentBtn().exists()).toBe(true);
- });
+ await nextTick();
+ expect(findViewIncidentBtn().exists()).toBe(false);
+ expect(findCreateIncidentBtn().exists()).toBe(true);
});
it('calls `$apollo.mutate` with `createIssueQuery`', () => {
diff --git a/spec/frontend/vue_shared/alert_details/alert_management_sidebar_todo_spec.js b/spec/frontend/vue_shared/alert_details/alert_management_sidebar_todo_spec.js
index 87ad5e36564..12c5c190e26 100644
--- a/spec/frontend/vue_shared/alert_details/alert_management_sidebar_todo_spec.js
+++ b/spec/frontend/vue_shared/alert_details/alert_management_sidebar_todo_spec.js
@@ -1,4 +1,5 @@
import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import todoMarkDoneMutation from '~/graphql_shared/mutations/todo_mark_done.mutation.graphql';
import SidebarTodo from '~/vue_shared/alert_details/components/sidebar/sidebar_todo.vue';
import createAlertTodoMutation from '~/vue_shared/alert_details/graphql/mutations/alert_todo_create.mutation.graphql';
@@ -57,7 +58,7 @@ describe('Alert Details Sidebar To Do', () => {
});
it('renders a button for adding a To-Do', async () => {
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findToDoButton().text()).toBe('Add a to do');
});
@@ -66,7 +67,7 @@ describe('Alert Details Sidebar To Do', () => {
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult);
findToDoButton().trigger('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
mutation: createAlertTodoMutation,
@@ -88,7 +89,7 @@ describe('Alert Details Sidebar To Do', () => {
});
it('renders a Mark As Done button when todo is present', async () => {
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findToDoButton().text()).toBe('Mark as done');
});
@@ -97,7 +98,7 @@ describe('Alert Details Sidebar To Do', () => {
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult);
findToDoButton().trigger('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
mutation: todoMarkDoneMutation,
diff --git a/spec/frontend/vue_shared/alert_details/alert_metrics_spec.js b/spec/frontend/vue_shared/alert_details/alert_metrics_spec.js
index b5a61a4adc1..1216681038f 100644
--- a/spec/frontend/vue_shared/alert_details/alert_metrics_spec.js
+++ b/spec/frontend/vue_shared/alert_details/alert_metrics_spec.js
@@ -1,6 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
+import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
import MetricEmbed from '~/monitoring/components/embeds/metric_embed.vue';
import AlertMetrics from '~/vue_shared/alert_details/components/alert_metrics.vue';
@@ -53,7 +54,7 @@ describe('Alert Metrics', () => {
mountComponent({ props: { dashboardUrl: 'metrics.url' } });
await waitForPromises();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findEmptyState().exists()).toBe(false);
expect(findChart().exists()).toBe(true);
diff --git a/spec/frontend/vue_shared/alert_details/alert_status_spec.js b/spec/frontend/vue_shared/alert_details/alert_status_spec.js
index 3fc13243bce..ba3b0335a8e 100644
--- a/spec/frontend/vue_shared/alert_details/alert_status_spec.js
+++ b/spec/frontend/vue_shared/alert_details/alert_status_spec.js
@@ -1,4 +1,5 @@
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import updateAlertStatusMutation from '~/graphql_shared//mutations/alert_status_update.mutation.graphql';
@@ -121,7 +122,7 @@ describe('AlertManagementStatus', () => {
it('emits an error when triggered a second time', async () => {
await selectFirstStatusOption();
- await wrapper.vm.$nextTick();
+ await nextTick();
await selectFirstStatusOption();
// Should emit two errors [0,1]
expect(wrapper.emitted('alert-error').length > 1).toBe(true);
@@ -175,17 +176,18 @@ describe('AlertManagementStatus', () => {
jest.spyOn(Tracking, 'event');
});
- it('should not track alert status updates when the tracking options do not exist', () => {
+ it('should not track alert status updates when the tracking options do not exist', async () => {
mountComponent({});
Tracking.event.mockClear();
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({});
findFirstStatusOption().vm.$emit('click');
- setImmediate(() => {
- expect(Tracking.event).not.toHaveBeenCalled();
- });
+
+ await nextTick();
+
+ expect(Tracking.event).not.toHaveBeenCalled();
});
- it('should track alert status updates when the tracking options exist', () => {
+ it('should track alert status updates when the tracking options exist', async () => {
const trackAlertStatusUpdateOptions = {
category: 'Alert Management',
action: 'update_alert_status',
@@ -195,11 +197,12 @@ describe('AlertManagementStatus', () => {
Tracking.event.mockClear();
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({});
findFirstStatusOption().vm.$emit('click');
+
+ await nextTick();
+
const status = findFirstStatusOption().text();
- setImmediate(() => {
- const { category, action, label } = trackAlertStatusUpdateOptions;
- expect(Tracking.event).toHaveBeenCalledWith(category, action, { label, property: status });
- });
+ const { category, action, label } = trackAlertStatusUpdateOptions;
+ expect(Tracking.event).toHaveBeenCalledWith(category, action, { label, property: status });
});
});
});
diff --git a/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_assignees_spec.js b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_assignees_spec.js
index 29e0eee2c9a..29569734621 100644
--- a/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_assignees_spec.js
+++ b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_assignees_spec.js
@@ -1,6 +1,7 @@
import { GlDropdownItem } from '@gitlab/ui';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
+import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import SidebarAssignee from '~/vue_shared/alert_details/components/sidebar/sidebar_assignee.vue';
import SidebarAssignees from '~/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue';
@@ -112,7 +113,7 @@ describe('Alert Details Sidebar Assignees', () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ isDropdownSearching: false });
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findDropdown().text()).toBe('Unassigned');
});
@@ -126,7 +127,7 @@ describe('Alert Details Sidebar Assignees', () => {
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ isDropdownSearching: false });
- await wrapper.vm.$nextTick();
+ await nextTick();
wrapper.find(SidebarAssignee).vm.$emit('update-alert-assignees', 'root');
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
@@ -139,7 +140,7 @@ describe('Alert Details Sidebar Assignees', () => {
});
});
- it('emits an error when request contains error messages', () => {
+ it('emits an error when request contains error messages', async () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ isDropdownSearching: false });
@@ -153,15 +154,11 @@ describe('Alert Details Sidebar Assignees', () => {
};
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(errorMutationResult);
- return wrapper.vm
- .$nextTick()
- .then(() => {
- const SideBarAssigneeItem = wrapper.findAll(SidebarAssignee).at(0);
- SideBarAssigneeItem.vm.$emit('update-alert-assignees');
- })
- .then(() => {
- expect(wrapper.emitted('alert-error')).toBeDefined();
- });
+
+ await nextTick();
+ const SideBarAssigneeItem = wrapper.findAll(SidebarAssignee).at(0);
+ await SideBarAssigneeItem.vm.$emit('update-alert-assignees');
+ expect(wrapper.emitted('alert-error')).toBeDefined();
});
it('stops updating and cancels loading when the request fails', () => {
diff --git a/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_status_spec.js b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_status_spec.js
index b00a20dab1a..a3adbcf8d3a 100644
--- a/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_status_spec.js
+++ b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_status_spec.js
@@ -1,4 +1,5 @@
import { GlDropdown, GlLoadingIcon } from '@gitlab/ui';
+import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import AlertStatus from '~/vue_shared/alert_details/components/alert_status.vue';
import AlertSidebarStatus from '~/vue_shared/alert_details/components/sidebar/sidebar_status.vue';
@@ -75,7 +76,7 @@ describe('Alert Details Sidebar Status', () => {
loading: false,
});
findAlertStatus().vm.$emit('handle-updating', true);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findStatusLoadingIcon().exists()).toBe(true);
});
diff --git a/spec/frontend/vue_shared/components/blob_viewers/__snapshots__/simple_viewer_spec.js.snap b/spec/frontend/vue_shared/components/blob_viewers/__snapshots__/simple_viewer_spec.js.snap
index 06753044e93..fbf3d17fd64 100644
--- a/spec/frontend/vue_shared/components/blob_viewers/__snapshots__/simple_viewer_spec.js.snap
+++ b/spec/frontend/vue_shared/components/blob_viewers/__snapshots__/simple_viewer_spec.js.snap
@@ -6,7 +6,7 @@ exports[`Blob Simple Viewer component rendering matches the snapshot 1`] = `
class="file-content code js-syntax-highlight"
>
<div
- class="line-numbers"
+ class="line-numbers gl-pt-0!"
>
<a
class="diff-line-num js-line-number"
@@ -56,7 +56,7 @@ exports[`Blob Simple Viewer component rendering matches the snapshot 1`] = `
class="blob-content"
>
<pre
- class="code highlight"
+ class="code highlight gl-p-0! gl-display-flex"
>
<code
data-blob-hash="foo-bar"
diff --git a/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js b/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js
index 3277aab43f0..663ebd3e12f 100644
--- a/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js
@@ -1,16 +1,21 @@
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import { HIGHLIGHT_CLASS_NAME } from '~/vue_shared/components/blob_viewers/constants';
import SimpleViewer from '~/vue_shared/components/blob_viewers/simple_viewer.vue';
+import LineHighlighter from '~/blob/line_highlighter';
+
+jest.mock('~/blob/line_highlighter');
describe('Blob Simple Viewer component', () => {
let wrapper;
const contentMock = `<span id="LC1">First</span>\n<span id="LC2">Second</span>\n<span id="LC3">Third</span>`;
const blobHash = 'foo-bar';
- function createComponent(content = contentMock, isRawContent = false) {
+ function createComponent(content = contentMock, isRawContent = false, glFeatures = {}) {
wrapper = shallowMount(SimpleViewer, {
provide: {
blobHash,
+ glFeatures,
},
propsData: {
content,
@@ -25,6 +30,20 @@ describe('Blob Simple Viewer component', () => {
wrapper.destroy();
});
+ describe('refactorBlobViewer feature flag', () => {
+ it('loads the LineHighlighter if refactorBlobViewer is enabled', () => {
+ createComponent('', false, { refactorBlobViewer: true });
+
+ expect(LineHighlighter).toHaveBeenCalled();
+ });
+
+ it('does not load the LineHighlighter if refactorBlobViewer is disabled', () => {
+ createComponent('', false, { refactorBlobViewer: false });
+
+ expect(LineHighlighter).not.toHaveBeenCalled();
+ });
+ });
+
it('does not fail if content is empty', () => {
const spy = jest.spyOn(window.console, 'error');
createComponent('');
@@ -69,7 +88,7 @@ describe('Blob Simple Viewer component', () => {
expect(linetoBeHighlighted.classes()).toContain(HIGHLIGHT_CLASS_NAME);
});
- it('switches highlighting when another line is selected', () => {
+ it('switches highlighting when another line is selected', async () => {
const currentlyHighlighted = wrapper.find('#LC2');
const hash = '#LC3';
const linetoBeHighlighted = wrapper.find(hash);
@@ -78,11 +97,10 @@ describe('Blob Simple Viewer component', () => {
wrapper.vm.scrollToLine(hash);
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.vm.highlightedLine).toBe(linetoBeHighlighted.element);
- expect(currentlyHighlighted.classes()).not.toContain(HIGHLIGHT_CLASS_NAME);
- expect(linetoBeHighlighted.classes()).toContain(HIGHLIGHT_CLASS_NAME);
- });
+ await nextTick();
+ expect(wrapper.vm.highlightedLine).toBe(linetoBeHighlighted.element);
+ expect(currentlyHighlighted.classes()).not.toContain(HIGHLIGHT_CLASS_NAME);
+ expect(linetoBeHighlighted.classes()).toContain(HIGHLIGHT_CLASS_NAME);
});
});
});
diff --git a/spec/frontend/vue_shared/components/chronic_duration_input_spec.js b/spec/frontend/vue_shared/components/chronic_duration_input_spec.js
index 083a5f60d1d..6932a812287 100644
--- a/spec/frontend/vue_shared/components/chronic_duration_input_spec.js
+++ b/spec/frontend/vue_shared/components/chronic_duration_input_spec.js
@@ -1,4 +1,5 @@
import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import ChronicDurationInput from '~/vue_shared/components/chronic_duration_input.vue';
const MOCK_VALUE = 2 * 3600 + 20 * 60;
@@ -48,7 +49,7 @@ describe('vue_shared/components/chronic_duration_input', () => {
describe('change', () => {
const createAndDispatch = async (initialValue, humanReadableInput) => {
createComponent({ value: initialValue });
- await wrapper.vm.$nextTick();
+ await nextTick();
textElement.value = humanReadableInput;
textElement.dispatchEvent(new Event('input'));
};
@@ -118,7 +119,7 @@ describe('vue_shared/components/chronic_duration_input', () => {
it('emits valid with user input', async () => {
textElement.value = '1m10s';
textElement.dispatchEvent(new Event('input'));
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.emitted('valid')).toEqual([
[{ valid: true, feedback: '' }],
@@ -133,7 +134,7 @@ describe('vue_shared/components/chronic_duration_input', () => {
textElement.value = '';
textElement.dispatchEvent(new Event('input'));
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.emitted('valid')).toEqual([
[{ valid: true, feedback: '' }],
@@ -151,7 +152,7 @@ describe('vue_shared/components/chronic_duration_input', () => {
it('emits invalid with user input', async () => {
textElement.value = 'gobbledygook';
textElement.dispatchEvent(new Event('input'));
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.emitted('valid')).toEqual([
[{ valid: true, feedback: '' }],
@@ -186,7 +187,7 @@ describe('vue_shared/components/chronic_duration_input', () => {
it('emits valid with updated value', async () => {
wrapper.setProps({ value: MOCK_VALUE });
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.emitted('valid')).toEqual([
[{ valid: null, feedback: '' }],
@@ -210,7 +211,7 @@ describe('vue_shared/components/chronic_duration_input', () => {
it('emits valid when input is integer', async () => {
textElement.value = '2hr20min';
textElement.dispatchEvent(new Event('input'));
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.emitted('change')).toEqual([[MOCK_VALUE]]);
expect(wrapper.emitted('valid')).toEqual([
@@ -228,7 +229,7 @@ describe('vue_shared/components/chronic_duration_input', () => {
it('emits valid when input is decimal', async () => {
textElement.value = '1.5s';
textElement.dispatchEvent(new Event('input'));
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.emitted('change')).toEqual([[1.5]]);
expect(wrapper.emitted('valid')).toEqual([
@@ -252,7 +253,7 @@ describe('vue_shared/components/chronic_duration_input', () => {
it('emits valid when input is integer', async () => {
textElement.value = '2hr20min';
textElement.dispatchEvent(new Event('input'));
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.emitted('change')).toEqual([[MOCK_VALUE]]);
expect(wrapper.emitted('valid')).toEqual([
@@ -270,7 +271,7 @@ describe('vue_shared/components/chronic_duration_input', () => {
it('emits invalid when input is decimal', async () => {
textElement.value = '1.5s';
textElement.dispatchEvent(new Event('input'));
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.emitted('change')).toBeUndefined();
expect(wrapper.emitted('valid')).toEqual([
@@ -318,7 +319,7 @@ describe('vue_shared/components/chronic_duration_input', () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ value: MOCK_VALUE });
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(textElement.value).toBe('2 hrs 20 mins');
expect(hiddenElement.value).toBe(MOCK_VALUE.toString());
@@ -329,7 +330,7 @@ describe('vue_shared/components/chronic_duration_input', () => {
it('passes user input to parent via v-model', async () => {
textElement.value = '2hr20min';
textElement.dispatchEvent(new Event('input'));
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.findComponent(ChronicDurationInput).props('value')).toBe(MOCK_VALUE);
expect(textElement.value).toBe('2hr20min');
@@ -377,7 +378,7 @@ describe('vue_shared/components/chronic_duration_input', () => {
it('creates form data with user-specified value', async () => {
textElement.value = '1m10s';
textElement.dispatchEvent(new Event('input'));
- await wrapper.vm.$nextTick();
+ await nextTick();
const formData = new FormData(wrapper.find('[data-testid=myForm]').element);
const iter = formData.entries();
diff --git a/spec/frontend/vue_shared/components/confirm_fork_modal_spec.js b/spec/frontend/vue_shared/components/confirm_fork_modal_spec.js
new file mode 100644
index 00000000000..1cde92cf522
--- /dev/null
+++ b/spec/frontend/vue_shared/components/confirm_fork_modal_spec.js
@@ -0,0 +1,80 @@
+import { GlModal } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ConfirmForkModal, { i18n } from '~/vue_shared/components/confirm_fork_modal.vue';
+
+describe('vue_shared/components/confirm_fork_modal', () => {
+ let wrapper = null;
+
+ const forkPath = '/fake/fork/path';
+ const modalId = 'confirm-fork-modal';
+ const defaultProps = { modalId, forkPath };
+
+ const findModal = () => wrapper.findComponent(GlModal);
+ const findModalProp = (prop) => findModal().props(prop);
+ const findModalActionProps = () => findModalProp('actionPrimary');
+
+ const createComponent = (props = {}) =>
+ shallowMountExtended(ConfirmForkModal, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('visible = false', () => {
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ it('sets the visible prop to `false`', () => {
+ expect(findModalProp('visible')).toBe(false);
+ });
+
+ it('sets the modal title', () => {
+ const title = findModalProp('title');
+ expect(title).toBe(i18n.title);
+ });
+
+ it('sets the modal id', () => {
+ const fakeModalId = findModalProp('modalId');
+ expect(fakeModalId).toBe(modalId);
+ });
+
+ it('has the fork path button', () => {
+ const modalProps = findModalActionProps();
+ expect(modalProps.text).toBe(i18n.btnText);
+ expect(modalProps.attributes.variant).toBe('confirm');
+ });
+
+ it('sets the correct fork path', () => {
+ const modalProps = findModalActionProps();
+ expect(modalProps.attributes.href).toBe(forkPath);
+ });
+
+ it('has the fork message', () => {
+ expect(findModal().text()).toContain(i18n.message);
+ });
+ });
+
+ describe('visible = true', () => {
+ beforeEach(() => {
+ wrapper = createComponent({ visible: true });
+ });
+
+ it('sets the visible prop to `true`', () => {
+ expect(findModalProp('visible')).toBe(true);
+ });
+
+ it('emits the `change` event if the modal is hidden', () => {
+ expect(wrapper.emitted('change')).toBeUndefined();
+
+ findModal().vm.$emit('change', false);
+
+ expect(wrapper.emitted('change')).toEqual([[false]]);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js
index 33667a1bb71..d4b6b987c69 100644
--- a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js
+++ b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js
@@ -1,5 +1,6 @@
import { mount } from '@vue/test-utils';
import timezoneMock from 'timezone-mock';
+import { nextTick } from 'vue';
import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
import {
defaultTimeRanges,
@@ -29,26 +30,23 @@ describe('DateTimePicker', () => {
wrapper.destroy();
});
- it('renders dropdown toggle button with selected text', () => {
+ it('renders dropdown toggle button with selected text', async () => {
createComponent();
- return wrapper.vm.$nextTick(() => {
- expect(dropdownToggle().text()).toBe(defaultTimeRange.label);
- });
+ await nextTick();
+ expect(dropdownToggle().text()).toBe(defaultTimeRange.label);
});
- it('renders dropdown toggle button with selected text and utc label', () => {
+ it('renders dropdown toggle button with selected text and utc label', async () => {
createComponent({ utc: true });
- return wrapper.vm.$nextTick(() => {
- expect(dropdownToggle().text()).toContain(defaultTimeRange.label);
- expect(dropdownToggle().text()).toContain('UTC');
- });
+ await nextTick();
+ expect(dropdownToggle().text()).toContain(defaultTimeRange.label);
+ expect(dropdownToggle().text()).toContain('UTC');
});
- it('renders dropdown with 2 custom time range inputs', () => {
+ it('renders dropdown with 2 custom time range inputs', async () => {
createComponent();
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.findAll('input').length).toBe(2);
- });
+ await nextTick();
+ expect(wrapper.findAll('input').length).toBe(2);
});
describe('renders label with h/m/s truncated if possible', () => {
@@ -80,33 +78,30 @@ describe('DateTimePicker', () => {
label: '2019-10-10 00:00:01 to 2019-10-10 00:00:01 UTC',
},
].forEach(({ start, end, utc, label }) => {
- it(`for start ${start}, end ${end}, and utc ${utc}, label is ${label}`, () => {
+ it(`for start ${start}, end ${end}, and utc ${utc}, label is ${label}`, async () => {
createComponent({
value: { start, end },
utc,
});
- return wrapper.vm.$nextTick(() => {
- expect(dropdownToggle().text()).toBe(label);
- });
+ await nextTick();
+ expect(dropdownToggle().text()).toBe(label);
});
});
});
- it(`renders dropdown with ${optionsCount} (default) items in quick range`, () => {
+ it(`renders dropdown with ${optionsCount} (default) items in quick range`, async () => {
createComponent();
dropdownToggle().trigger('click');
- return wrapper.vm.$nextTick(() => {
- expect(findQuickRangeItems().length).toBe(optionsCount);
- });
+ await nextTick();
+ expect(findQuickRangeItems().length).toBe(optionsCount);
});
- it('renders dropdown with a default quick range item selected', () => {
+ it('renders dropdown with a default quick range item selected', async () => {
createComponent();
dropdownToggle().trigger('click');
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.find('.dropdown-item.active').exists()).toBe(true);
- expect(wrapper.find('.dropdown-item.active').text()).toBe(defaultTimeRange.label);
- });
+ await nextTick();
+ expect(wrapper.find('.dropdown-item.active').exists()).toBe(true);
+ expect(wrapper.find('.dropdown-item.active').text()).toBe(defaultTimeRange.label);
});
it('renders a disabled apply button on wrong input', () => {
@@ -118,74 +113,63 @@ describe('DateTimePicker', () => {
});
describe('user input', () => {
- const fillInputAndBlur = (input, val) => {
+ const fillInputAndBlur = async (input, val) => {
wrapper.find(input).setValue(val);
- return wrapper.vm.$nextTick().then(() => {
- wrapper.find(input).trigger('blur');
- return wrapper.vm.$nextTick();
- });
+ await nextTick();
+ wrapper.find(input).trigger('blur');
+ await nextTick();
};
- beforeEach(() => {
+ beforeEach(async () => {
createComponent();
- return wrapper.vm.$nextTick();
+ await nextTick();
});
- it('displays inline error message if custom time range inputs are invalid', () => {
- return fillInputAndBlur('#custom-time-from', '2019-10-01abc')
- .then(() => fillInputAndBlur('#custom-time-to', '2019-10-10abc'))
- .then(() => {
- expect(wrapper.findAll('.invalid-feedback').length).toBe(2);
- });
+ it('displays inline error message if custom time range inputs are invalid', async () => {
+ await fillInputAndBlur('#custom-time-from', '2019-10-01abc');
+ await fillInputAndBlur('#custom-time-to', '2019-10-10abc');
+ expect(wrapper.findAll('.invalid-feedback').length).toBe(2);
});
- it('keeps apply button disabled with invalid custom time range inputs', () => {
- return fillInputAndBlur('#custom-time-from', '2019-10-01abc')
- .then(() => fillInputAndBlur('#custom-time-to', '2019-09-19'))
- .then(() => {
- expect(applyButtonElement().getAttribute('disabled')).toBe('disabled');
- });
+ it('keeps apply button disabled with invalid custom time range inputs', async () => {
+ await fillInputAndBlur('#custom-time-from', '2019-10-01abc');
+ await fillInputAndBlur('#custom-time-to', '2019-09-19');
+ expect(applyButtonElement().getAttribute('disabled')).toBe('disabled');
});
- it('enables apply button with valid custom time range inputs', () => {
- return fillInputAndBlur('#custom-time-from', '2019-10-01')
- .then(() => fillInputAndBlur('#custom-time-to', '2019-10-19'))
- .then(() => {
- expect(applyButtonElement().getAttribute('disabled')).toBeNull();
- });
+ it('enables apply button with valid custom time range inputs', async () => {
+ await fillInputAndBlur('#custom-time-from', '2019-10-01');
+ await fillInputAndBlur('#custom-time-to', '2019-10-19');
+ expect(applyButtonElement().getAttribute('disabled')).toBeNull();
});
describe('when "apply" is clicked', () => {
- it('emits iso dates', () => {
- return fillInputAndBlur('#custom-time-from', '2019-10-01 00:00:00')
- .then(() => fillInputAndBlur('#custom-time-to', '2019-10-19 00:00:00'))
- .then(() => {
- applyButtonElement().click();
-
- expect(wrapper.emitted().input).toHaveLength(1);
- expect(wrapper.emitted().input[0]).toEqual([
- {
- end: '2019-10-19T00:00:00Z',
- start: '2019-10-01T00:00:00Z',
- },
- ]);
- });
+ it('emits iso dates', async () => {
+ await fillInputAndBlur('#custom-time-from', '2019-10-01 00:00:00');
+ await fillInputAndBlur('#custom-time-to', '2019-10-19 00:00:00');
+ applyButtonElement().click();
+
+ expect(wrapper.emitted().input).toHaveLength(1);
+ expect(wrapper.emitted().input[0]).toEqual([
+ {
+ end: '2019-10-19T00:00:00Z',
+ start: '2019-10-01T00:00:00Z',
+ },
+ ]);
});
- it('emits iso dates, for dates without time of day', () => {
- return fillInputAndBlur('#custom-time-from', '2019-10-01')
- .then(() => fillInputAndBlur('#custom-time-to', '2019-10-19'))
- .then(() => {
- applyButtonElement().click();
-
- expect(wrapper.emitted().input).toHaveLength(1);
- expect(wrapper.emitted().input[0]).toEqual([
- {
- end: '2019-10-19T00:00:00Z',
- start: '2019-10-01T00:00:00Z',
- },
- ]);
- });
+ it('emits iso dates, for dates without time of day', async () => {
+ await fillInputAndBlur('#custom-time-from', '2019-10-01');
+ await fillInputAndBlur('#custom-time-to', '2019-10-19');
+ applyButtonElement().click();
+
+ expect(wrapper.emitted().input).toHaveLength(1);
+ expect(wrapper.emitted().input[0]).toEqual([
+ {
+ end: '2019-10-19T00:00:00Z',
+ start: '2019-10-01T00:00:00Z',
+ },
+ ]);
});
describe('when timezone is different', () => {
@@ -196,52 +180,46 @@ describe('DateTimePicker', () => {
timezoneMock.unregister();
});
- it('emits iso dates', () => {
- return fillInputAndBlur('#custom-time-from', '2019-10-01 00:00:00')
- .then(() => fillInputAndBlur('#custom-time-to', '2019-10-19 12:00:00'))
- .then(() => {
- applyButtonElement().click();
-
- expect(wrapper.emitted().input).toHaveLength(1);
- expect(wrapper.emitted().input[0]).toEqual([
- {
- start: '2019-10-01T07:00:00Z',
- end: '2019-10-19T19:00:00Z',
- },
- ]);
- });
+ it('emits iso dates', async () => {
+ await fillInputAndBlur('#custom-time-from', '2019-10-01 00:00:00');
+ await fillInputAndBlur('#custom-time-to', '2019-10-19 12:00:00');
+ applyButtonElement().click();
+
+ expect(wrapper.emitted().input).toHaveLength(1);
+ expect(wrapper.emitted().input[0]).toEqual([
+ {
+ start: '2019-10-01T07:00:00Z',
+ end: '2019-10-19T19:00:00Z',
+ },
+ ]);
});
- it('emits iso dates with utc format', () => {
+ it('emits iso dates with utc format', async () => {
wrapper.setProps({ utc: true });
- return wrapper.vm
- .$nextTick()
- .then(() => fillInputAndBlur('#custom-time-from', '2019-10-01 00:00:00'))
- .then(() => fillInputAndBlur('#custom-time-to', '2019-10-19 12:00:00'))
- .then(() => {
- applyButtonElement().click();
-
- expect(wrapper.emitted().input).toHaveLength(1);
- expect(wrapper.emitted().input[0]).toEqual([
- {
- start: '2019-10-01T00:00:00Z',
- end: '2019-10-19T12:00:00Z',
- },
- ]);
- });
+ await nextTick();
+ await fillInputAndBlur('#custom-time-from', '2019-10-01 00:00:00');
+ await fillInputAndBlur('#custom-time-to', '2019-10-19 12:00:00');
+ applyButtonElement().click();
+
+ expect(wrapper.emitted().input).toHaveLength(1);
+ expect(wrapper.emitted().input[0]).toEqual([
+ {
+ start: '2019-10-01T00:00:00Z',
+ end: '2019-10-19T12:00:00Z',
+ },
+ ]);
});
});
});
- it('unchecks quick range when text is input is clicked', () => {
+ it('unchecks quick range when text is input is clicked', async () => {
const findActiveItems = () =>
findQuickRangeItems().filter((w) => w.classes().includes('active'));
expect(findActiveItems().length).toBe(1);
- return fillInputAndBlur('#custom-time-from', '2019-10-01').then(() => {
- expect(findActiveItems().length).toBe(0);
- });
+ await fillInputAndBlur('#custom-time-from', '2019-10-01');
+ expect(findActiveItems().length).toBe(0);
});
it('emits dates in an object when a is clicked', () => {
@@ -257,16 +235,14 @@ describe('DateTimePicker', () => {
});
});
- it('hides the popover with cancel button', () => {
+ it('hides the popover with cancel button', async () => {
dropdownToggle().trigger('click');
- return wrapper.vm.$nextTick(() => {
- cancelButton().trigger('click');
+ await nextTick();
+ cancelButton().trigger('click');
- return wrapper.vm.$nextTick(() => {
- expect(dropdownMenu().classes('show')).toBe(false);
- });
- });
+ await nextTick();
+ expect(dropdownMenu().classes('show')).toBe(false);
});
});
@@ -293,7 +269,7 @@ describe('DateTimePicker', () => {
jest.spyOn(Date, 'now').mockImplementation(() => MOCK_NOW);
});
- it('renders dropdown with a label in the quick range', () => {
+ it('renders dropdown with a label in the quick range', async () => {
createComponent({
value: {
duration: { seconds: 60 * 5 },
@@ -301,12 +277,11 @@ describe('DateTimePicker', () => {
options: otherTimeRanges,
});
dropdownToggle().trigger('click');
- return wrapper.vm.$nextTick(() => {
- expect(dropdownToggle().text()).toBe('5 minutes');
- });
+ await nextTick();
+ expect(dropdownToggle().text()).toBe('5 minutes');
});
- it('renders dropdown with a label in the quick range and utc label', () => {
+ it('renders dropdown with a label in the quick range and utc label', async () => {
createComponent({
value: {
duration: { seconds: 60 * 5 },
@@ -315,12 +290,11 @@ describe('DateTimePicker', () => {
options: otherTimeRanges,
});
dropdownToggle().trigger('click');
- return wrapper.vm.$nextTick(() => {
- expect(dropdownToggle().text()).toBe('5 minutes UTC');
- });
+ await nextTick();
+ expect(dropdownToggle().text()).toBe('5 minutes UTC');
});
- it('renders dropdown with quick range items', () => {
+ it('renders dropdown with quick range items', async () => {
createComponent({
value: {
duration: { seconds: 60 * 2 },
@@ -328,31 +302,29 @@ describe('DateTimePicker', () => {
options: otherTimeRanges,
});
dropdownToggle().trigger('click');
- return wrapper.vm.$nextTick(() => {
- const items = findQuickRangeItems();
+ await nextTick();
+ const items = findQuickRangeItems();
- expect(items.length).toBe(Object.keys(otherTimeRanges).length);
- expect(items.at(0).text()).toBe('1 minute');
- expect(items.at(0).classes()).not.toContain('active');
+ expect(items.length).toBe(Object.keys(otherTimeRanges).length);
+ expect(items.at(0).text()).toBe('1 minute');
+ expect(items.at(0).classes()).not.toContain('active');
- expect(items.at(1).text()).toBe('2 minutes');
- expect(items.at(1).classes()).toContain('active');
+ expect(items.at(1).text()).toBe('2 minutes');
+ expect(items.at(1).classes()).toContain('active');
- expect(items.at(2).text()).toBe('5 minutes');
- expect(items.at(2).classes()).not.toContain('active');
- });
+ expect(items.at(2).text()).toBe('5 minutes');
+ expect(items.at(2).classes()).not.toContain('active');
});
- it('renders dropdown with a label not in the quick range', () => {
+ it('renders dropdown with a label not in the quick range', async () => {
createComponent({
value: {
duration: { seconds: 60 * 4 },
},
});
dropdownToggle().trigger('click');
- return wrapper.vm.$nextTick(() => {
- expect(dropdownToggle().text()).toBe('2020-01-23 19:56:00 to 2020-01-23 20:00:00');
- });
+ await nextTick();
+ expect(dropdownToggle().text()).toBe('2020-01-23 19:56:00 to 2020-01-23 20:00:00');
});
});
});
diff --git a/spec/frontend/vue_shared/components/deployment_instance/deployment_instance_spec.js b/spec/frontend/vue_shared/components/deployment_instance/deployment_instance_spec.js
index b812ced72c9..59653a0ec13 100644
--- a/spec/frontend/vue_shared/components/deployment_instance/deployment_instance_spec.js
+++ b/spec/frontend/vue_shared/components/deployment_instance/deployment_instance_spec.js
@@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import DeployBoardInstance from '~/vue_shared/components/deployment_instance.vue';
import { folder } from './mock_data';
@@ -28,17 +29,15 @@ describe('Deploy Board Instance', () => {
expect(wrapper.attributes('title')).toEqual('This is a pod');
});
- it('should render a div without tooltip data', (done) => {
+ it('should render a div without tooltip data', async () => {
wrapper = createComponent({
status: 'deploying',
tooltipText: '',
});
- wrapper.vm.$nextTick(() => {
- expect(wrapper.classes('deployment-instance-deploying')).toBe(true);
- expect(wrapper.attributes('title')).toEqual('');
- done();
- });
+ await nextTick();
+ expect(wrapper.classes('deployment-instance-deploying')).toBe(true);
+ expect(wrapper.attributes('title')).toEqual('');
});
it('should have a log path computed with a pod name as a parameter', () => {
@@ -58,15 +57,13 @@ describe('Deploy Board Instance', () => {
wrapper.destroy();
});
- it('should render a div with canary class when stable prop is provided as false', (done) => {
+ it('should render a div with canary class when stable prop is provided as false', async () => {
wrapper = createComponent({
stable: false,
});
- wrapper.vm.$nextTick(() => {
- expect(wrapper.classes('deployment-instance-canary')).toBe(true);
- done();
- });
+ await nextTick();
+ expect(wrapper.classes('deployment-instance-canary')).toBe(true);
});
});
@@ -75,17 +72,15 @@ describe('Deploy Board Instance', () => {
wrapper.destroy();
});
- it('should not be a link without a logsPath prop', (done) => {
+ it('should not be a link without a logsPath prop', async () => {
wrapper = createComponent({
stable: false,
logsPath: '',
});
- wrapper.vm.$nextTick(() => {
- expect(wrapper.vm.computedLogPath).toBeNull();
- expect(wrapper.vm.isLink).toBeFalsy();
- done();
- });
+ await nextTick();
+ expect(wrapper.vm.computedLogPath).toBeNull();
+ expect(wrapper.vm.isLink).toBeFalsy();
});
it('should render a link without href if path is not passed', () => {
diff --git a/spec/frontend/vue_shared/components/design_management/design_note_pin_spec.js b/spec/frontend/vue_shared/components/design_management/design_note_pin_spec.js
index 984a28c93d6..353d493add9 100644
--- a/spec/frontend/vue_shared/components/design_management/design_note_pin_spec.js
+++ b/spec/frontend/vue_shared/components/design_management/design_note_pin_spec.js
@@ -39,4 +39,72 @@ describe('Design note pin component', () => {
createComponent({ position: null });
expect(wrapper.element).toMatchSnapshot();
});
+
+ it('applies `on-image` class when isOnImage is true', () => {
+ createComponent({ isOnImage: true });
+
+ expect(wrapper.find('.on-image').exists()).toBe(true);
+ });
+
+ it('applies `draft` class when isDraft is true', () => {
+ createComponent({ isDraft: true });
+
+ expect(wrapper.find('.draft').exists()).toBe(true);
+ });
+
+ describe('size', () => {
+ it('is `sm` it applies `small` class', () => {
+ createComponent({ size: 'sm' });
+ expect(wrapper.find('.small').exists()).toBe(true);
+ });
+
+ it('is `md` it applies no size class', () => {
+ createComponent({ size: 'md' });
+ expect(wrapper.find('.small').exists()).toBe(false);
+ expect(wrapper.find('.medium').exists()).toBe(false);
+ });
+
+ it('throws when passed any other value except `sm` or `md`', () => {
+ jest.spyOn(console, 'error').mockImplementation(() => {});
+
+ createComponent({ size: 'lg' });
+
+ // eslint-disable-next-line no-console
+ expect(console.error).toHaveBeenCalled();
+ });
+ });
+
+ describe('ariaLabel', () => {
+ describe('when value is passed', () => {
+ it('overrides default aria-label', () => {
+ const ariaLabel = 'Aria Label';
+
+ createComponent({ ariaLabel });
+
+ const button = wrapper.find('button');
+
+ expect(button.attributes('aria-label')).toBe(ariaLabel);
+ });
+ });
+
+ describe('when no value is passed', () => {
+ it('shows new note label as aria-label when label is absent', () => {
+ createComponent({ label: null });
+
+ const button = wrapper.find('button');
+
+ expect(button.attributes('aria-label')).toBe('Comment form position');
+ });
+
+ it('shows label position as aria-label when label is present', () => {
+ const label = 1;
+
+ createComponent({ label, isNewNote: false });
+
+ const button = wrapper.find('button');
+
+ expect(button.attributes('aria-label')).toBe(`Comment '${label}' position`);
+ });
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/diff_viewer/diff_viewer_spec.js b/spec/frontend/vue_shared/components/diff_viewer/diff_viewer_spec.js
index 68e3ee11a0d..69964b2687d 100644
--- a/spec/frontend/vue_shared/components/diff_viewer/diff_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/diff_viewer/diff_viewer_spec.js
@@ -1,4 +1,4 @@
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import { GREEN_BOX_IMAGE_URL, RED_BOX_IMAGE_URL } from 'spec/test_constants';
@@ -26,27 +26,25 @@ describe('DiffViewer', () => {
vm.$destroy();
});
- it('renders image diff', (done) => {
+ it('renders image diff', async () => {
window.gon = {
relative_url_root: '',
};
createComponent({ ...requiredProps, projectPath: '' });
- setImmediate(() => {
- expect(vm.$el.querySelector('.deleted img').getAttribute('src')).toBe(
- `//-/raw/DEF/${RED_BOX_IMAGE_URL}`,
- );
+ await nextTick();
- expect(vm.$el.querySelector('.added img').getAttribute('src')).toBe(
- `//-/raw/ABC/${GREEN_BOX_IMAGE_URL}`,
- );
+ expect(vm.$el.querySelector('.deleted img').getAttribute('src')).toBe(
+ `//-/raw/DEF/${RED_BOX_IMAGE_URL}`,
+ );
- done();
- });
+ expect(vm.$el.querySelector('.added img').getAttribute('src')).toBe(
+ `//-/raw/ABC/${GREEN_BOX_IMAGE_URL}`,
+ );
});
- it('renders fallback download diff display', (done) => {
+ it('renders fallback download diff display', async () => {
createComponent({
...requiredProps,
diffViewerMode: 'added',
@@ -54,22 +52,18 @@ describe('DiffViewer', () => {
oldPath: 'testold.abc',
});
- setImmediate(() => {
- expect(vm.$el.querySelector('.deleted .file-info').textContent.trim()).toContain(
- 'testold.abc',
- );
+ await nextTick();
- expect(vm.$el.querySelector('.deleted .btn.btn-default').textContent.trim()).toContain(
- 'Download',
- );
+ expect(vm.$el.querySelector('.deleted .file-info').textContent.trim()).toContain('testold.abc');
- expect(vm.$el.querySelector('.added .file-info').textContent.trim()).toContain('test.abc');
- expect(vm.$el.querySelector('.added .btn.btn-default').textContent.trim()).toContain(
- 'Download',
- );
+ expect(vm.$el.querySelector('.deleted .btn.btn-default').textContent.trim()).toContain(
+ 'Download',
+ );
- done();
- });
+ expect(vm.$el.querySelector('.added .file-info').textContent.trim()).toContain('test.abc');
+ expect(vm.$el.querySelector('.added .btn.btn-default').textContent.trim()).toContain(
+ 'Download',
+ );
});
describe('renamed file', () => {
diff --git a/spec/frontend/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js b/spec/frontend/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js
index 8deb466b33c..d0fa8b8dacb 100644
--- a/spec/frontend/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js
@@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import { compileToFunctions } from 'vue-template-compiler';
import { GREEN_BOX_IMAGE_URL, RED_BOX_IMAGE_URL } from 'spec/test_constants';
@@ -51,60 +51,53 @@ describe('ImageDiffViewer', () => {
wrapper.destroy();
});
- it('renders image diff for replaced', (done) => {
+ it('renders image diff for replaced', async () => {
createComponent({ ...allProps });
- vm.$nextTick(() => {
- const metaInfoElements = vm.$el.querySelectorAll('.image-info');
+ await nextTick();
+ const metaInfoElements = vm.$el.querySelectorAll('.image-info');
- expect(vm.$el.querySelector('.added img').getAttribute('src')).toBe(GREEN_BOX_IMAGE_URL);
+ expect(vm.$el.querySelector('.added img').getAttribute('src')).toBe(GREEN_BOX_IMAGE_URL);
- expect(vm.$el.querySelector('.deleted img').getAttribute('src')).toBe(RED_BOX_IMAGE_URL);
+ expect(vm.$el.querySelector('.deleted img').getAttribute('src')).toBe(RED_BOX_IMAGE_URL);
- expect(vm.$el.querySelector('.view-modes-menu li.active').textContent.trim()).toBe('2-up');
- expect(vm.$el.querySelector('.view-modes-menu li:nth-child(2)').textContent.trim()).toBe(
- 'Swipe',
- );
+ expect(vm.$el.querySelector('.view-modes-menu li.active').textContent.trim()).toBe('2-up');
+ expect(vm.$el.querySelector('.view-modes-menu li:nth-child(2)').textContent.trim()).toBe(
+ 'Swipe',
+ );
- expect(vm.$el.querySelector('.view-modes-menu li:nth-child(3)').textContent.trim()).toBe(
- 'Onion skin',
- );
+ expect(vm.$el.querySelector('.view-modes-menu li:nth-child(3)').textContent.trim()).toBe(
+ 'Onion skin',
+ );
- expect(metaInfoElements.length).toBe(2);
- expect(metaInfoElements[0]).toHaveText('2.00 KiB');
- expect(metaInfoElements[1]).toHaveText('1.00 KiB');
-
- done();
- });
+ expect(metaInfoElements.length).toBe(2);
+ expect(metaInfoElements[0]).toHaveText('2.00 KiB');
+ expect(metaInfoElements[1]).toHaveText('1.00 KiB');
});
- it('renders image diff for new', (done) => {
+ it('renders image diff for new', async () => {
createComponent({ ...allProps, diffMode: 'new', oldPath: '' });
- setImmediate(() => {
- const metaInfoElement = vm.$el.querySelector('.image-info');
+ await nextTick();
- expect(vm.$el.querySelector('.added img').getAttribute('src')).toBe(GREEN_BOX_IMAGE_URL);
- expect(metaInfoElement).toHaveText('1.00 KiB');
+ const metaInfoElement = vm.$el.querySelector('.image-info');
- done();
- });
+ expect(vm.$el.querySelector('.added img').getAttribute('src')).toBe(GREEN_BOX_IMAGE_URL);
+ expect(metaInfoElement).toHaveText('1.00 KiB');
});
- it('renders image diff for deleted', (done) => {
+ it('renders image diff for deleted', async () => {
createComponent({ ...allProps, diffMode: 'deleted', newPath: '' });
- setImmediate(() => {
- const metaInfoElement = vm.$el.querySelector('.image-info');
+ await nextTick();
- expect(vm.$el.querySelector('.deleted img').getAttribute('src')).toBe(RED_BOX_IMAGE_URL);
- expect(metaInfoElement).toHaveText('2.00 KiB');
+ const metaInfoElement = vm.$el.querySelector('.image-info');
- done();
- });
+ expect(vm.$el.querySelector('.deleted img').getAttribute('src')).toBe(RED_BOX_IMAGE_URL);
+ expect(metaInfoElement).toHaveText('2.00 KiB');
});
- it('renders image diff for renamed', (done) => {
+ it('renders image diff for renamed', async () => {
vm = new Vue({
components: {
imageDiffViewer,
@@ -130,69 +123,56 @@ describe('ImageDiffViewer', () => {
`),
}).$mount();
- setImmediate(() => {
- const metaInfoElement = vm.$el.querySelector('.image-info');
+ await nextTick();
- expect(vm.$el.querySelector('img').getAttribute('src')).toBe(GREEN_BOX_IMAGE_URL);
- expect(vm.$el.querySelector('.overlay')).not.toBe(null);
+ const metaInfoElement = vm.$el.querySelector('.image-info');
- expect(metaInfoElement).toHaveText('2.00 KiB');
+ expect(vm.$el.querySelector('img').getAttribute('src')).toBe(GREEN_BOX_IMAGE_URL);
+ expect(vm.$el.querySelector('.overlay')).not.toBe(null);
- done();
- });
+ expect(metaInfoElement).toHaveText('2.00 KiB');
});
describe('swipeMode', () => {
- beforeEach((done) => {
+ beforeEach(() => {
createComponent({ ...requiredProps });
- setImmediate(() => {
- done();
- });
+ return nextTick();
});
- it('switches to Swipe Mode', (done) => {
+ it('switches to Swipe Mode', async () => {
vm.$el.querySelector('.view-modes-menu li:nth-child(2)').click();
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.view-modes-menu li.active').textContent.trim()).toBe('Swipe');
- done();
- });
+ await nextTick();
+ expect(vm.$el.querySelector('.view-modes-menu li.active').textContent.trim()).toBe('Swipe');
});
});
describe('onionSkin', () => {
- beforeEach((done) => {
+ beforeEach(() => {
createComponent({ ...requiredProps });
- setImmediate(() => {
- done();
- });
+ return nextTick();
});
- it('switches to Onion Skin Mode', (done) => {
+ it('switches to Onion Skin Mode', async () => {
vm.$el.querySelector('.view-modes-menu li:nth-child(3)').click();
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.view-modes-menu li.active').textContent.trim()).toBe(
- 'Onion skin',
- );
- done();
- });
+ await nextTick();
+ expect(vm.$el.querySelector('.view-modes-menu li.active').textContent.trim()).toBe(
+ 'Onion skin',
+ );
});
- it('has working drag handler', (done) => {
+ it('has working drag handler', async () => {
vm.$el.querySelector('.view-modes-menu li:nth-child(3)').click();
- vm.$nextTick(() => {
- dragSlider(vm.$el.querySelector('.dragger'), document, 20);
+ await nextTick();
+ dragSlider(vm.$el.querySelector('.dragger'), document, 20);
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.dragger').style.left).toBe('20px');
- expect(vm.$el.querySelector('.added.frame').style.opacity).toBe('0.2');
- done();
- });
- });
+ await nextTick();
+ expect(vm.$el.querySelector('.dragger').style.left).toBe('20px');
+ expect(vm.$el.querySelector('.added.frame').style.opacity).toBe('0.2');
});
});
});
diff --git a/spec/frontend/vue_shared/components/diff_viewer/viewers/renamed_spec.js b/spec/frontend/vue_shared/components/diff_viewer/viewers/renamed_spec.js
index b8d3cbebe16..549388c1a5c 100644
--- a/spec/frontend/vue_shared/components/diff_viewer/viewers/renamed_spec.js
+++ b/spec/frontend/vue_shared/components/diff_viewer/viewers/renamed_spec.js
@@ -1,5 +1,5 @@
import { shallowMount, mount } from '@vue/test-utils';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import {
TRANSITION_LOAD_START,
@@ -126,15 +126,14 @@ describe('Renamed Diff Viewer', () => {
store = null;
});
- it('calls the switchToFullDiffFromRenamedFile action when the method is triggered', () => {
+ it('calls the switchToFullDiffFromRenamedFile action when the method is triggered', async () => {
store.dispatch.mockResolvedValue();
wrapper.vm.switchToFull();
- return wrapper.vm.$nextTick().then(() => {
- expect(store.dispatch).toHaveBeenCalledWith('diffs/switchToFullDiffFromRenamedFile', {
- diffFile,
- });
+ await nextTick();
+ expect(store.dispatch).toHaveBeenCalledWith('diffs/switchToFullDiffFromRenamedFile', {
+ diffFile,
});
});
@@ -144,7 +143,7 @@ describe('Renamed Diff Viewer', () => {
${STATE_ERRORED} | ${'mockRejectedValue'} | ${'rejected'}
`(
'moves through the correct states during a $resolution request',
- ({ after, resolvePromise }) => {
+ async ({ after, resolvePromise }) => {
store.dispatch[resolvePromise]();
expect(wrapper.vm.state).toEqual(STATE_IDLING);
@@ -153,16 +152,9 @@ describe('Renamed Diff Viewer', () => {
expect(wrapper.vm.state).toEqual(STATE_LOADING);
- return (
- wrapper.vm
- // This tick is needed for when the action (promise) finishes
- .$nextTick()
- // This tick waits for the state change in the promise .then/.catch to bubble into the component
- .then(() => wrapper.vm.$nextTick())
- .then(() => {
- expect(wrapper.vm.state).toEqual(after);
- })
- );
+ await nextTick(); // This tick is needed for when the action (promise) finishes
+ await nextTick(); // This tick waits for the state change in the promise .then/.catch to bubble into the component
+ expect(wrapper.vm.state).toEqual(after);
},
);
});
diff --git a/spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js b/spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js
index 194681a6138..4b32fbffebe 100644
--- a/spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js
+++ b/spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js
@@ -1,5 +1,6 @@
import { GlAlert, GlSprintf } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import Component from '~/vue_shared/components/dismissible_feedback_alert.vue';
@@ -64,7 +65,7 @@ describe('Dismissible Feedback Alert', () => {
it('should not show the alert once dismissed', async () => {
localStorage.setItem(STORAGE_DISMISSAL_KEY, 'true');
createFullComponent();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findAlert().exists()).toBe(false);
});
diff --git a/spec/frontend/vue_shared/components/dropdown/dropdown_search_input_spec.js b/spec/frontend/vue_shared/components/dropdown/dropdown_search_input_spec.js
index ec553c52236..b32dbeb8852 100644
--- a/spec/frontend/vue_shared/components/dropdown/dropdown_search_input_spec.js
+++ b/spec/frontend/vue_shared/components/dropdown/dropdown_search_input_spec.js
@@ -1,4 +1,5 @@
import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import DropdownSearchInputComponent from '~/vue_shared/components/dropdown/dropdown_search_input.vue';
describe('DropdownSearchInputComponent', () => {
@@ -36,16 +37,15 @@ describe('DropdownSearchInputComponent', () => {
expect(findInputEl().attributes('placeholder')).toBe(defaultProps.placeholderText);
});
- it('focuses input element when focused property equals true', () => {
+ it('focuses input element when focused property equals true', async () => {
const inputEl = findInputEl().element;
jest.spyOn(inputEl, 'focus');
wrapper.setProps({ focused: true });
- return wrapper.vm.$nextTick().then(() => {
- expect(inputEl.focus).toHaveBeenCalled();
- });
+ await nextTick();
+ expect(inputEl.focus).toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js b/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js
index b3af5fd3feb..084d0559665 100644
--- a/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js
+++ b/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js
@@ -1,6 +1,7 @@
import { GlDropdown, GlSearchBoxByType, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import DropdownWidget from '~/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue';
describe('DropdownWidget component', () => {
@@ -53,7 +54,7 @@ describe('DropdownWidget component', () => {
describe('when dropdown is open', () => {
beforeEach(async () => {
findDropdown().vm.$emit('show');
- await wrapper.vm.$nextTick();
+ await nextTick();
});
it('emits search event when typing in search box', () => {
@@ -69,7 +70,7 @@ describe('DropdownWidget component', () => {
it('emits set-option event when clicking on an option', async () => {
wrapper.findAll('[data-testid="unselected-option"]').at(1).trigger('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.emitted('set-option')).toEqual([[wrapper.props().options[1]]]);
});
diff --git a/spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js b/spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js
index 996df34f2ff..c34041f9305 100644
--- a/spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js
+++ b/spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js
@@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import DropdownKeyboardNavigation from '~/vue_shared/components/dropdown_keyboard_navigation.vue';
import { UP_KEY_CODE, DOWN_KEY_CODE, TAB_KEY_CODE } from '~/lib/utils/keycodes';
@@ -53,7 +54,7 @@ describe('DropdownKeyboardNavigation', () => {
it('should $emit @change with the default index when max changes', async () => {
wrapper.setProps({ max: 20 });
- await wrapper.vm.$nextTick();
+ await nextTick();
// The first @change`call happens on created() so we test for the second [1]
expect(wrapper.emitted('change')[1]).toStrictEqual([MOCK_DEFAULT_INDEX]);
});
diff --git a/spec/frontend/vue_shared/components/expand_button_spec.js b/spec/frontend/vue_shared/components/expand_button_spec.js
index 7874658cc0f..87d6ed6b21f 100644
--- a/spec/frontend/vue_shared/components/expand_button_spec.js
+++ b/spec/frontend/vue_shared/components/expand_button_spec.js
@@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils';
-import Vue from 'vue';
+import { nextTick } from 'vue';
import ExpandButton from '~/vue_shared/components/expand_button.vue';
const text = {
@@ -66,9 +66,9 @@ describe('Expand button', () => {
});
describe('on click', () => {
- beforeEach((done) => {
+ beforeEach(async () => {
expanderPrependEl().trigger('click');
- Vue.nextTick(done);
+ await nextTick();
});
afterEach(() => {
@@ -85,7 +85,7 @@ describe('Expand button', () => {
});
describe('when short text is provided', () => {
- beforeEach((done) => {
+ beforeEach(async () => {
factory({
slots: {
expanded: `<p>${text.expanded}</p>`,
@@ -94,7 +94,7 @@ describe('Expand button', () => {
});
expanderPrependEl().trigger('click');
- Vue.nextTick(done);
+ await nextTick();
});
it('only renders expanded text', () => {
@@ -110,31 +110,29 @@ describe('Expand button', () => {
});
describe('append button', () => {
- beforeEach((done) => {
+ beforeEach(async () => {
expanderPrependEl().trigger('click');
- Vue.nextTick(done);
+ await nextTick();
});
- it('clicking hides itself and shows prepend', () => {
+ it('clicking hides itself and shows prepend', async () => {
expect(expanderAppendEl().isVisible()).toBe(true);
expanderAppendEl().trigger('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(expanderPrependEl().isVisible()).toBe(true);
- });
+ await nextTick();
+ expect(expanderPrependEl().isVisible()).toBe(true);
});
- it('clicking hides expanded text', () => {
+ it('clicking hides expanded text', async () => {
expect(wrapper.find(ExpandButton).text().trim()).toBe(text.expanded);
expanderAppendEl().trigger('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(ExpandButton).text().trim()).not.toBe(text.expanded);
- });
+ await nextTick();
+ expect(wrapper.find(ExpandButton).text().trim()).not.toBe(text.expanded);
});
describe('when short text is provided', () => {
- beforeEach((done) => {
+ beforeEach(async () => {
factory({
slots: {
expanded: `<p>${text.expanded}</p>`,
@@ -143,16 +141,15 @@ describe('Expand button', () => {
});
expanderPrependEl().trigger('click');
- Vue.nextTick(done);
+ await nextTick();
});
- it('clicking reveals short text', () => {
+ it('clicking reveals short text', async () => {
expect(wrapper.find(ExpandButton).text().trim()).toBe(text.expanded);
expanderAppendEl().trigger('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(ExpandButton).text().trim()).toBe(text.short);
- });
+ await nextTick();
+ expect(wrapper.find(ExpandButton).text().trim()).toBe(text.short);
});
});
});
diff --git a/spec/frontend/vue_shared/components/file_finder/index_spec.js b/spec/frontend/vue_shared/components/file_finder/index_spec.js
index 181fc4017a3..921091c5b84 100644
--- a/spec/frontend/vue_shared/components/file_finder/index_spec.js
+++ b/spec/frontend/vue_shared/components/file_finder/index_spec.js
@@ -1,6 +1,5 @@
import Mousetrap from 'mousetrap';
-import Vue from 'vue';
-import waitForPromises from 'helpers/wait_for_promises';
+import Vue, { nextTick } from 'vue';
import { file } from 'jest/ide/helpers';
import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
import FindFileComponent from '~/vue_shared/components/file_finder/index.vue';
@@ -31,7 +30,7 @@ describe('File finder item spec', () => {
});
describe('with entries', () => {
- beforeEach((done) => {
+ beforeEach(() => {
createComponent({
files: [
{
@@ -48,7 +47,7 @@ describe('File finder item spec', () => {
],
});
- setImmediate(done);
+ return nextTick();
});
it('renders list of blobs', () => {
@@ -57,68 +56,48 @@ describe('File finder item spec', () => {
expect(vm.$el.textContent).not.toContain('folder');
});
- it('filters entries', (done) => {
+ it('filters entries', async () => {
vm.searchText = 'index';
- setImmediate(() => {
- expect(vm.$el.textContent).toContain('index.js');
- expect(vm.$el.textContent).not.toContain('component.js');
+ await nextTick();
- done();
- });
+ expect(vm.$el.textContent).toContain('index.js');
+ expect(vm.$el.textContent).not.toContain('component.js');
});
- it('shows clear button when searchText is not empty', (done) => {
+ it('shows clear button when searchText is not empty', async () => {
vm.searchText = 'index';
- setImmediate(() => {
- expect(vm.$el.querySelector('.dropdown-input').classList).toContain('has-value');
- expect(vm.$el.querySelector('.dropdown-input-search').classList).toContain('hidden');
+ await nextTick();
- done();
- });
+ expect(vm.$el.querySelector('.dropdown-input').classList).toContain('has-value');
+ expect(vm.$el.querySelector('.dropdown-input-search').classList).toContain('hidden');
});
- it('clear button resets searchText', (done) => {
+ it('clear button resets searchText', async () => {
vm.searchText = 'index';
- waitForPromises()
- .then(() => {
- vm.clearSearchInput();
- })
- .then(waitForPromises)
- .then(() => {
- expect(vm.searchText).toBe('');
- })
- .then(done)
- .catch(done.fail);
+ vm.clearSearchInput();
+
+ expect(vm.searchText).toBe('');
});
- it('clear button focuses search input', (done) => {
+ it('clear button focuses search input', async () => {
jest.spyOn(vm.$refs.searchInput, 'focus').mockImplementation(() => {});
vm.searchText = 'index';
- waitForPromises()
- .then(() => {
- vm.clearSearchInput();
- })
- .then(waitForPromises)
- .then(() => {
- expect(vm.$refs.searchInput.focus).toHaveBeenCalled();
- })
- .then(done)
- .catch(done.fail);
+ vm.clearSearchInput();
+
+ await nextTick();
+
+ expect(vm.$refs.searchInput.focus).toHaveBeenCalled();
});
describe('listShowCount', () => {
- it('returns 1 when no filtered entries exist', (done) => {
+ it('returns 1 when no filtered entries exist', () => {
vm.searchText = 'testing 123';
- setImmediate(() => {
- expect(vm.listShowCount).toBe(1);
-
- done();
- });
+ expect(vm.listShowCount).toBe(1);
});
it('returns entries length when not filtered', () => {
@@ -131,26 +110,18 @@ describe('File finder item spec', () => {
expect(vm.listHeight).toBe(55);
});
- it('returns 33 when entries dont exist', (done) => {
+ it('returns 33 when entries dont exist', () => {
vm.searchText = 'testing 123';
- setImmediate(() => {
- expect(vm.listHeight).toBe(33);
-
- done();
- });
+ expect(vm.listHeight).toBe(33);
});
});
describe('filteredBlobsLength', () => {
- it('returns length of filtered blobs', (done) => {
+ it('returns length of filtered blobs', () => {
vm.searchText = 'index';
- setImmediate(() => {
- expect(vm.filteredBlobsLength).toBe(1);
-
- done();
- });
+ expect(vm.filteredBlobsLength).toBe(1);
});
});
@@ -158,7 +129,7 @@ describe('File finder item spec', () => {
it('renders less DOM nodes if not visible by utilizing v-if', async () => {
vm.visible = false;
- await waitForPromises();
+ await nextTick();
expect(vm.$el).toBeInstanceOf(Comment);
});
@@ -166,33 +137,24 @@ describe('File finder item spec', () => {
describe('watches', () => {
describe('searchText', () => {
- it('resets focusedIndex when updated', (done) => {
+ it('resets focusedIndex when updated', async () => {
vm.focusedIndex = 1;
vm.searchText = 'test';
- setImmediate(() => {
- expect(vm.focusedIndex).toBe(0);
+ await nextTick();
- done();
- });
+ expect(vm.focusedIndex).toBe(0);
});
});
describe('visible', () => {
- it('resets searchText when changed to false', (done) => {
+ it('resets searchText when changed to false', async () => {
vm.searchText = 'test';
- vm.visible = true;
-
- waitForPromises()
- .then(() => {
- vm.visible = false;
- })
- .then(waitForPromises)
- .then(() => {
- expect(vm.searchText).toBe('');
- })
- .then(done)
- .catch(done.fail);
+ vm.visible = false;
+
+ await nextTick();
+
+ expect(vm.searchText).toBe('');
});
});
});
@@ -216,7 +178,7 @@ describe('File finder item spec', () => {
});
describe('onKeyup', () => {
- it('opens file on enter key', (done) => {
+ it('opens file on enter key', async () => {
const event = new CustomEvent('keyup');
event.keyCode = ENTER_KEY_CODE;
@@ -224,14 +186,12 @@ describe('File finder item spec', () => {
vm.$refs.searchInput.dispatchEvent(event);
- setImmediate(() => {
- expect(vm.openFile).toHaveBeenCalledWith(vm.files[0]);
+ await nextTick();
- done();
- });
+ expect(vm.openFile).toHaveBeenCalledWith(vm.files[0]);
});
- it('closes file finder on esc key', (done) => {
+ it('closes file finder on esc key', async () => {
const event = new CustomEvent('keyup');
event.keyCode = ESC_KEY_CODE;
@@ -239,11 +199,9 @@ describe('File finder item spec', () => {
vm.$refs.searchInput.dispatchEvent(event);
- setImmediate(() => {
- expect(vm.$emit).toHaveBeenCalledWith('toggle', false);
+ await nextTick();
- done();
- });
+ expect(vm.$emit).toHaveBeenCalledWith('toggle', false);
});
});
@@ -310,34 +268,26 @@ describe('File finder item spec', () => {
});
describe('keyboard shortcuts', () => {
- beforeEach((done) => {
+ beforeEach(async () => {
createComponent();
jest.spyOn(vm, 'toggle').mockImplementation(() => {});
- vm.$nextTick(done);
+ await nextTick();
});
- it('calls toggle on `t` key press', (done) => {
+ it('calls toggle on `t` key press', async () => {
Mousetrap.trigger('t');
- vm.$nextTick()
- .then(() => {
- expect(vm.toggle).toHaveBeenCalled();
- })
- .then(done)
- .catch(done.fail);
+ await nextTick();
+ expect(vm.toggle).toHaveBeenCalled();
});
- it('calls toggle on `mod+p` key press', (done) => {
+ it('calls toggle on `mod+p` key press', async () => {
Mousetrap.trigger('mod+p');
- vm.$nextTick()
- .then(() => {
- expect(vm.toggle).toHaveBeenCalled();
- })
- .then(done)
- .catch(done.fail);
+ await nextTick();
+ expect(vm.toggle).toHaveBeenCalled();
});
it('always allows `mod+p` to trigger toggle', () => {
diff --git a/spec/frontend/vue_shared/components/file_finder/item_spec.js b/spec/frontend/vue_shared/components/file_finder/item_spec.js
index 1a4a97efb95..b69c33055c1 100644
--- a/spec/frontend/vue_shared/components/file_finder/item_spec.js
+++ b/spec/frontend/vue_shared/components/file_finder/item_spec.js
@@ -1,4 +1,4 @@
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import createComponent from 'helpers/vue_mount_component_helper';
import { file } from 'jest/ide/helpers';
import ItemComponent from '~/vue_shared/components/file_finder/item.vue';
@@ -37,14 +37,11 @@ describe('File finder item spec', () => {
expect(vm.$el.classList).toContain('is-focused');
});
- it('does not have is-focused class when not focused', (done) => {
+ it('does not have is-focused class when not focused', async () => {
vm.focused = false;
- vm.$nextTick(() => {
- expect(vm.$el.classList).not.toContain('is-focused');
-
- done();
- });
+ await nextTick();
+ expect(vm.$el.classList).not.toContain('is-focused');
});
});
@@ -53,24 +50,18 @@ describe('File finder item spec', () => {
expect(vm.$el.querySelector('.diff-changed-stats')).toBe(null);
});
- it('renders when a changed file', (done) => {
+ it('renders when a changed file', async () => {
vm.file.changed = true;
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.diff-changed-stats')).not.toBe(null);
-
- done();
- });
+ await nextTick();
+ expect(vm.$el.querySelector('.diff-changed-stats')).not.toBe(null);
});
- it('renders when a temp file', (done) => {
+ it('renders when a temp file', async () => {
vm.file.tempFile = true;
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.diff-changed-stats')).not.toBe(null);
-
- done();
- });
+ await nextTick();
+ expect(vm.$el.querySelector('.diff-changed-stats')).not.toBe(null);
});
});
@@ -85,56 +76,52 @@ describe('File finder item spec', () => {
describe('path', () => {
let el;
- beforeEach((done) => {
+ beforeEach(async () => {
vm.searchText = 'file';
el = vm.$el.querySelector('.diff-changed-file-path');
- vm.$nextTick(done);
+ nextTick();
});
it('highlights text', () => {
expect(el.querySelectorAll('.highlighted').length).toBe(4);
});
- it('adds ellipsis to long text', (done) => {
+ it('adds ellipsis to long text', async () => {
vm.file.path = new Array(70)
.fill()
.map((_, i) => `${i}-`)
.join('');
- vm.$nextTick(() => {
- expect(el.textContent).toBe(`...${vm.file.path.substr(vm.file.path.length - 60)}`);
- done();
- });
+ await nextTick();
+ expect(el.textContent).toBe(`...${vm.file.path.substr(vm.file.path.length - 60)}`);
});
});
describe('name', () => {
let el;
- beforeEach((done) => {
+ beforeEach(async () => {
vm.searchText = 'file';
el = vm.$el.querySelector('.diff-changed-file-name');
- vm.$nextTick(done);
+ await nextTick();
});
it('highlights text', () => {
expect(el.querySelectorAll('.highlighted').length).toBe(4);
});
- it('does not add ellipsis to long text', (done) => {
+ it('does not add ellipsis to long text', async () => {
vm.file.name = new Array(70)
.fill()
.map((_, i) => `${i}-`)
.join('');
- vm.$nextTick(() => {
- expect(el.textContent).not.toBe(`...${vm.file.name.substr(vm.file.name.length - 60)}`);
- done();
- });
+ await nextTick();
+ expect(el.textContent).not.toBe(`...${vm.file.name.substr(vm.file.name.length - 60)}`);
});
});
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
index 4e9eac2dde2..575e8a73050 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
@@ -8,6 +8,7 @@ import {
} from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import RecentSearchesService from '~/filtered_search/services/recent_searches_service';
import RecentSearchesStore from '~/filtered_search/stores/recent_searches_store';
import { SortDirection } from '~/vue_shared/components/filtered_search_bar/constants';
@@ -172,7 +173,7 @@ describe('FilteredSearchBarRoot', () => {
recentSearches: [{ foo: 'bar' }, 'foo'],
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.vm.filteredRecentSearches).toHaveLength(1);
expect(wrapper.vm.filteredRecentSearches[0]).toEqual({ foo: 'bar' });
@@ -188,7 +189,7 @@ describe('FilteredSearchBarRoot', () => {
],
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.vm.filteredRecentSearches).toHaveLength(2);
expect(uniqueTokens).toHaveBeenCalled();
@@ -199,7 +200,7 @@ describe('FilteredSearchBarRoot', () => {
recentSearchesStorageKey: '',
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.vm.filteredRecentSearches).not.toBeDefined();
});
@@ -208,7 +209,7 @@ describe('FilteredSearchBarRoot', () => {
describe('watchers', () => {
describe('filterValue', () => {
- it('emits component event `onFilter` with empty array and false when filter was never selected', () => {
+ it('emits component event `onFilter` with empty array and false when filter was never selected', async () => {
wrapper = createComponent({ initialFilterValue: [tokenValueEmpty] });
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
@@ -217,12 +218,11 @@ describe('FilteredSearchBarRoot', () => {
filterValue: [tokenValueEmpty],
});
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.emitted('onFilter')[0]).toEqual([[], false]);
- });
+ await nextTick();
+ expect(wrapper.emitted('onFilter')[0]).toEqual([[], false]);
});
- it('emits component event `onFilter` with empty array and true when initially selected filter value was cleared', () => {
+ it('emits component event `onFilter` with empty array and true when initially selected filter value was cleared', async () => {
wrapper = createComponent({ initialFilterValue: [tokenValueLabel] });
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
@@ -231,9 +231,8 @@ describe('FilteredSearchBarRoot', () => {
filterValue: [tokenValueEmpty],
});
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.emitted('onFilter')[0]).toEqual([[], true]);
- });
+ await nextTick();
+ expect(wrapper.emitted('onFilter')[0]).toEqual([[], true]);
});
});
});
@@ -336,7 +335,7 @@ describe('FilteredSearchBarRoot', () => {
filterValue: mockFilters,
});
- await wrapper.vm.$nextTick();
+ await nextTick();
});
it('calls `uniqueTokens` on `filterValue` prop to remove duplicates', () => {
@@ -395,7 +394,7 @@ describe('FilteredSearchBarRoot', () => {
});
describe('template', () => {
- beforeEach(() => {
+ beforeEach(async () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({
@@ -404,7 +403,7 @@ describe('FilteredSearchBarRoot', () => {
recentSearches: mockHistoryItems,
});
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('renders gl-filtered-search component', () => {
@@ -439,7 +438,7 @@ describe('FilteredSearchBarRoot', () => {
const wrapperFullMount = createComponent({ sortOptions: mockSortOptions, shallow: false });
wrapperFullMount.vm.recentSearchesStore.addRecentSearch(mockHistoryItems[0]);
- await wrapperFullMount.vm.$nextTick();
+ await nextTick();
const searchHistoryItemsEl = wrapperFullMount.findAll(
'.gl-search-box-by-click-menu .gl-search-box-by-click-history-item',
@@ -462,7 +461,7 @@ describe('FilteredSearchBarRoot', () => {
wrapperFullMount.vm.recentSearchesStore.addRecentSearch([tokenValueMembership]);
- await wrapperFullMount.vm.$nextTick();
+ await nextTick();
expect(wrapperFullMount.find(GlDropdownItem).text()).toBe('Membership := Direct');
@@ -480,7 +479,7 @@ describe('FilteredSearchBarRoot', () => {
wrapperFullMount.vm.recentSearchesStore.addRecentSearch([tokenValueMembership]);
- await wrapperFullMount.vm.$nextTick();
+ await nextTick();
expect(wrapperFullMount.find(GlDropdownItem).text()).toBe('Membership := exclude');
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js
index 5865c6a41b8..87066b70023 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js
@@ -6,6 +6,7 @@ import {
} from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
@@ -167,7 +168,7 @@ describe('AuthorToken', () => {
const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment);
const suggestionsSegment = tokenSegments.at(2);
suggestionsSegment.vm.$emit('activate');
- await wrapper.vm.$nextTick();
+ await nextTick();
};
it('renders base-token component', () => {
@@ -185,23 +186,22 @@ describe('AuthorToken', () => {
});
});
- it('renders token item when value is selected', () => {
+ it('renders token item when value is selected', async () => {
wrapper = createComponent({
value: { data: mockAuthors[0].username },
data: { authors: mockAuthors },
stubs: { Portal: true },
});
- return wrapper.vm.$nextTick(() => {
- const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+ await nextTick();
+ const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
- expect(tokenSegments).toHaveLength(3); // Author, =, "Administrator"
+ expect(tokenSegments).toHaveLength(3); // Author, =, "Administrator"
- const tokenValue = tokenSegments.at(2);
+ const tokenValue = tokenSegments.at(2);
- expect(tokenValue.findComponent(GlAvatar).props('src')).toBe(mockAuthors[0].avatar_url);
- expect(tokenValue.text()).toBe(mockAuthors[0].name); // "Administrator"
- });
+ expect(tokenValue.findComponent(GlAvatar).props('src')).toBe(mockAuthors[0].avatar_url);
+ expect(tokenValue.text()).toBe(mockAuthors[0].name); // "Administrator"
});
it('renders token value with correct avatarUrl from author object', async () => {
@@ -220,7 +220,7 @@ describe('AuthorToken', () => {
stubs: { Portal: true },
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(getAvatarEl().props('src')).toBe(mockAuthors[0].avatar_url);
@@ -236,7 +236,7 @@ describe('AuthorToken', () => {
],
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(getAvatarEl().props('src')).toBe(mockAuthors[0].avatar_url);
});
@@ -268,7 +268,7 @@ describe('AuthorToken', () => {
const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
const suggestionsSegment = tokenSegments.at(2);
suggestionsSegment.vm.$emit('activate');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.find(GlDropdownDivider).exists()).toBe(false);
});
@@ -323,7 +323,7 @@ describe('AuthorToken', () => {
it('does not show current user while searching', async () => {
wrapper.findComponent(BaseToken).vm.handleInput({ data: 'foo' });
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.findComponent(GlFilteredSearchSuggestion).exists()).toBe(false);
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js
index 84f0151d9db..dd9bf2ff598 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js
@@ -1,5 +1,6 @@
-import { GlFilteredSearchToken } from '@gitlab/ui';
+import { GlFilteredSearchToken, GlLoadingIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import {
mockRegularLabel,
mockLabels,
@@ -61,13 +62,10 @@ const mockProps = {
getActiveTokenValue: (labels, data) => labels.find((label) => label.title === data),
};
-function createComponent({
- props = { ...mockProps },
- stubs = defaultStubs,
- slots = defaultSlots,
-} = {}) {
+function createComponent({ props = {}, stubs = defaultStubs, slots = defaultSlots } = {}) {
return mount(BaseToken, {
propsData: {
+ ...mockProps,
...props,
},
provide: {
@@ -83,15 +81,7 @@ function createComponent({
describe('BaseToken', () => {
let wrapper;
- beforeEach(() => {
- wrapper = createComponent({
- props: {
- ...mockProps,
- value: { data: `"${mockRegularLabel.title}"` },
- suggestions: mockLabels,
- },
- });
- });
+ const findGlFilteredSearchToken = () => wrapper.findComponent(GlFilteredSearchToken);
afterEach(() => {
wrapper.destroy();
@@ -99,21 +89,25 @@ describe('BaseToken', () => {
describe('data', () => {
it('calls `getRecentlyUsedSuggestions` to populate `recentSuggestions` when `recentSuggestionsStorageKey` is defined', () => {
+ wrapper = createComponent();
+
expect(getRecentlyUsedSuggestions).toHaveBeenCalledWith(mockStorageKey);
});
});
describe('computed', () => {
describe('activeTokenValue', () => {
- it('calls `getActiveTokenValue` when it is provided', async () => {
+ it('calls `getActiveTokenValue` when it is provided', () => {
const mockGetActiveTokenValue = jest.fn();
- wrapper.setProps({
- getActiveTokenValue: mockGetActiveTokenValue,
+ wrapper = createComponent({
+ props: {
+ value: { data: `"${mockRegularLabel.title}"` },
+ suggestions: mockLabels,
+ getActiveTokenValue: mockGetActiveTokenValue,
+ },
});
- await wrapper.vm.$nextTick();
-
expect(mockGetActiveTokenValue).toHaveBeenCalledTimes(1);
expect(mockGetActiveTokenValue).toHaveBeenCalledWith(
mockLabels,
@@ -125,33 +119,19 @@ describe('BaseToken', () => {
describe('watch', () => {
describe('active', () => {
- let wrapperWithTokenActive;
-
beforeEach(() => {
- wrapperWithTokenActive = createComponent({
+ wrapper = createComponent({
props: {
- ...mockProps,
value: { data: `"${mockRegularLabel.title}"` },
active: true,
},
});
});
- afterEach(() => {
- wrapperWithTokenActive.destroy();
- });
-
it('emits `fetch-suggestions` event on the component when value of this prop is changed to false and `suggestions` array is empty', async () => {
- wrapperWithTokenActive.setProps({
- active: false,
- });
-
- await wrapperWithTokenActive.vm.$nextTick();
+ await wrapper.setProps({ active: false });
- expect(wrapperWithTokenActive.emitted('fetch-suggestions')).toBeTruthy();
- expect(wrapperWithTokenActive.emitted('fetch-suggestions')).toEqual([
- [`"${mockRegularLabel.title}"`],
- ]);
+ expect(wrapper.emitted('fetch-suggestions')).toEqual([[`"${mockRegularLabel.title}"`]]);
});
});
});
@@ -161,17 +141,15 @@ describe('BaseToken', () => {
const mockTokenValue = mockLabels[0];
it('calls `setTokenValueToRecentlyUsed` when `recentSuggestionsStorageKey` is defined', () => {
+ wrapper = createComponent({ props: { suggestions: mockLabels } });
+
wrapper.vm.handleTokenValueSelected(mockTokenValue.title);
expect(setTokenValueToRecentlyUsed).toHaveBeenCalledWith(mockStorageKey, mockTokenValue);
});
- it('does not add token from preloadedSuggestions', async () => {
- wrapper.setProps({
- preloadedSuggestions: [mockTokenValue],
- });
-
- await wrapper.vm.$nextTick();
+ it('does not add token from preloadedSuggestions', () => {
+ wrapper = createComponent({ props: { preloadedSuggestions: [mockTokenValue] } });
wrapper.vm.handleTokenValueSelected(mockTokenValue.title);
@@ -182,58 +160,60 @@ describe('BaseToken', () => {
describe('template', () => {
it('renders gl-filtered-search-token component', () => {
- const wrapperWithNoStubs = createComponent({
- stubs: {},
- });
- const glFilteredSearchToken = wrapperWithNoStubs.find(GlFilteredSearchToken);
-
- expect(glFilteredSearchToken.exists()).toBe(true);
- expect(glFilteredSearchToken.props('config')).toEqual(mockProps.config);
+ wrapper = createComponent({ stubs: {} });
- wrapperWithNoStubs.destroy();
+ expect(findGlFilteredSearchToken().props('config')).toEqual(mockProps.config);
});
it('renders `view-token` slot when present', () => {
+ wrapper = createComponent();
+
expect(wrapper.find('.js-view-token').exists()).toBe(true);
});
it('renders `view` slot when present', () => {
+ wrapper = createComponent();
+
expect(wrapper.find('.js-view').exists()).toBe(true);
});
- describe('events', () => {
- let wrapperWithNoStubs;
-
- afterEach(() => {
- wrapperWithNoStubs.destroy();
+ it('renders loading spinner when loading', () => {
+ wrapper = createComponent({
+ props: {
+ active: true,
+ config: mockLabelToken,
+ suggestionsLoading: true,
+ },
+ stubs: { Portal: true },
});
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
+ });
+
+ describe('events', () => {
describe('when activeToken has been selected', () => {
beforeEach(() => {
- wrapperWithNoStubs = createComponent({
- props: {
- ...mockProps,
- getActiveTokenValue: () => ({ title: '' }),
- suggestionsLoading: true,
- },
+ wrapper = createComponent({
+ props: { getActiveTokenValue: () => ({ title: '' }) },
stubs: { Portal: true },
});
});
+
it('does not emit `fetch-suggestions` event on component after a delay when component emits `input` event', async () => {
jest.useFakeTimers();
- wrapperWithNoStubs.find(GlFilteredSearchToken).vm.$emit('input', { data: 'foo' });
- await wrapperWithNoStubs.vm.$nextTick();
+ findGlFilteredSearchToken().vm.$emit('input', { data: 'foo' });
+ await nextTick();
jest.runAllTimers();
- expect(wrapperWithNoStubs.emitted('fetch-suggestions')).toEqual([['']]);
+ expect(wrapper.emitted('fetch-suggestions')).toEqual([['']]);
});
});
describe('when activeToken has not been selected', () => {
beforeEach(() => {
- wrapperWithNoStubs = createComponent({
+ wrapper = createComponent({
stubs: { Portal: true },
});
});
@@ -241,38 +221,27 @@ describe('BaseToken', () => {
it('emits `fetch-suggestions` event on component after a delay when component emits `input` event', async () => {
jest.useFakeTimers();
- wrapperWithNoStubs.find(GlFilteredSearchToken).vm.$emit('input', { data: 'foo' });
- await wrapperWithNoStubs.vm.$nextTick();
+ findGlFilteredSearchToken().vm.$emit('input', { data: 'foo' });
+ await nextTick();
jest.runAllTimers();
- expect(wrapperWithNoStubs.emitted('fetch-suggestions')).toBeTruthy();
- expect(wrapperWithNoStubs.emitted('fetch-suggestions')[2]).toEqual(['foo']);
+ expect(wrapper.emitted('fetch-suggestions')[2]).toEqual(['foo']);
});
describe('when search is started with a quote', () => {
- it('emits `fetch-suggestions` with filtered value', async () => {
- jest.useFakeTimers();
-
- wrapperWithNoStubs.find(GlFilteredSearchToken).vm.$emit('input', { data: '"foo' });
- await wrapperWithNoStubs.vm.$nextTick();
+ it('emits `fetch-suggestions` with filtered value', () => {
+ findGlFilteredSearchToken().vm.$emit('input', { data: '"foo' });
- jest.runAllTimers();
-
- expect(wrapperWithNoStubs.emitted('fetch-suggestions')[2]).toEqual(['foo']);
+ expect(wrapper.emitted('fetch-suggestions')[2]).toEqual(['foo']);
});
});
describe('when search starts and ends with a quote', () => {
- it('emits `fetch-suggestions` with filtered value', async () => {
- jest.useFakeTimers();
-
- wrapperWithNoStubs.find(GlFilteredSearchToken).vm.$emit('input', { data: '"foo"' });
- await wrapperWithNoStubs.vm.$nextTick();
-
- jest.runAllTimers();
+ it('emits `fetch-suggestions` with filtered value', () => {
+ findGlFilteredSearchToken().vm.$emit('input', { data: '"foo"' });
- expect(wrapperWithNoStubs.emitted('fetch-suggestions')[2]).toEqual(['foo']);
+ expect(wrapper.emitted('fetch-suggestions')[2]).toEqual(['foo']);
});
});
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js
index cd8be765fb5..7a7db434052 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js
@@ -6,6 +6,7 @@ import {
} from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
@@ -115,7 +116,7 @@ describe('BranchToken', () => {
const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
const suggestionsSegment = tokenSegments.at(2);
suggestionsSegment.vm.$emit('activate');
- await wrapper.vm.$nextTick();
+ await nextTick();
}
beforeEach(async () => {
@@ -127,7 +128,7 @@ describe('BranchToken', () => {
branches: mockBranches,
});
- await wrapper.vm.$nextTick();
+ await nextTick();
});
it('renders gl-filtered-search-token component', () => {
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js
index ed9ac7c271e..b163563cea4 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js
@@ -6,6 +6,7 @@ import {
} from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
@@ -129,7 +130,7 @@ describe('EmojiToken', () => {
emojis: mockEmojis,
});
- await wrapper.vm.$nextTick();
+ await nextTick();
});
it('renders gl-filtered-search-token component', () => {
@@ -152,7 +153,7 @@ describe('EmojiToken', () => {
const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
const suggestionsSegment = tokenSegments.at(2);
suggestionsSegment.vm.$emit('activate');
- await wrapper.vm.$nextTick();
+ await nextTick();
const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
@@ -171,7 +172,7 @@ describe('EmojiToken', () => {
const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
const suggestionsSegment = tokenSegments.at(2);
suggestionsSegment.vm.$emit('activate');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.find(GlFilteredSearchSuggestion).exists()).toBe(false);
expect(wrapper.find(GlDropdownDivider).exists()).toBe(false);
@@ -186,7 +187,7 @@ describe('EmojiToken', () => {
const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
const suggestionsSegment = tokenSegments.at(2);
suggestionsSegment.vm.$emit('activate');
- await wrapper.vm.$nextTick();
+ await nextTick();
const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
index b9af71ad8a7..52df27c2d00 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
@@ -5,6 +5,7 @@ import {
} from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
import {
mockRegularLabel,
@@ -150,7 +151,7 @@ describe('LabelToken', () => {
labels: mockLabels,
});
- await wrapper.vm.$nextTick();
+ await nextTick();
});
it('renders base-token component', () => {
@@ -182,7 +183,7 @@ describe('LabelToken', () => {
const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
const suggestionsSegment = tokenSegments.at(2);
suggestionsSegment.vm.$emit('activate');
- await wrapper.vm.$nextTick();
+ await nextTick();
const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
@@ -201,7 +202,7 @@ describe('LabelToken', () => {
const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
const suggestionsSegment = tokenSegments.at(2);
suggestionsSegment.vm.$emit('activate');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.find(GlFilteredSearchSuggestion).exists()).toBe(false);
expect(wrapper.find(GlDropdownDivider).exists()).toBe(false);
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js
index c0d8b5fd139..de9ec863dd5 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js
@@ -6,6 +6,7 @@ import {
} from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
@@ -31,7 +32,7 @@ const defaultStubs = {
function createComponent(options = {}) {
const {
- config = mockMilestoneToken,
+ config = { ...mockMilestoneToken, shouldSkipSort: true },
value = { data: '' },
active = false,
stubs = defaultStubs,
@@ -67,6 +68,27 @@ describe('MilestoneToken', () => {
describe('methods', () => {
describe('fetchMilestones', () => {
+ describe('when config.shouldSkipSort is true', () => {
+ beforeEach(() => {
+ wrapper.vm.config.shouldSkipSort = true;
+ });
+
+ afterEach(() => {
+ wrapper.vm.config.shouldSkipSort = false;
+ });
+ it('does not call sortMilestonesByDueDate', async () => {
+ jest.spyOn(wrapper.vm.config, 'fetchMilestones').mockResolvedValue({
+ data: mockMilestones,
+ });
+
+ wrapper.vm.fetchMilestones();
+
+ await waitForPromises();
+
+ expect(sortMilestonesByDueDate).toHaveBeenCalledTimes(0);
+ });
+ });
+
it('calls `config.fetchMilestones` with provided searchTerm param', () => {
jest.spyOn(wrapper.vm.config, 'fetchMilestones');
@@ -76,10 +98,11 @@ describe('MilestoneToken', () => {
});
it('sets response to `milestones` when request is successful', () => {
+ wrapper.vm.config.shouldSkipSort = false;
+
jest.spyOn(wrapper.vm.config, 'fetchMilestones').mockResolvedValue({
data: mockMilestones,
});
-
wrapper.vm.fetchMilestones();
return waitForPromises().then(() => {
@@ -127,7 +150,7 @@ describe('MilestoneToken', () => {
milestones: mockMilestones,
});
- await wrapper.vm.$nextTick();
+ await nextTick();
});
it('renders gl-filtered-search-token component', () => {
@@ -150,7 +173,7 @@ describe('MilestoneToken', () => {
const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
const suggestionsSegment = tokenSegments.at(2);
suggestionsSegment.vm.$emit('activate');
- await wrapper.vm.$nextTick();
+ await nextTick();
const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
@@ -169,7 +192,7 @@ describe('MilestoneToken', () => {
const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
const suggestionsSegment = tokenSegments.at(2);
suggestionsSegment.vm.$emit('activate');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.find(GlFilteredSearchSuggestion).exists()).toBe(false);
expect(wrapper.find(GlDropdownDivider).exists()).toBe(false);
@@ -184,7 +207,7 @@ describe('MilestoneToken', () => {
const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
const suggestionsSegment = tokenSegments.at(2);
suggestionsSegment.vm.$emit('activate');
- await wrapper.vm.$nextTick();
+ await nextTick();
const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js
index b2f246a5985..8be21b35414 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js
@@ -1,5 +1,6 @@
import { GlFilteredSearchToken, GlFilteredSearchTokenSegment } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import ReleaseToken from '~/vue_shared/components/filtered_search_bar/tokens/release_token.vue';
@@ -31,7 +32,7 @@ describe('ReleaseToken', () => {
it('renders release value', async () => {
wrapper = createComponent({ value: { data: id } });
- await wrapper.vm.$nextTick();
+ await nextTick();
const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment);
diff --git a/spec/frontend/vue_shared/components/gfm_autocomplete/__snapshots__/utils_spec.js.snap b/spec/frontend/vue_shared/components/gfm_autocomplete/__snapshots__/utils_spec.js.snap
deleted file mode 100644
index 370b6eb01bc..00000000000
--- a/spec/frontend/vue_shared/components/gfm_autocomplete/__snapshots__/utils_spec.js.snap
+++ /dev/null
@@ -1,54 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`gfm_autocomplete/utils emojis config shows the emoji name and icon in the menu item 1`] = `"raised_hands <gl-emoji data-name=\\"raised_hands\\"></gl-emoji>"`;
-
-exports[`gfm_autocomplete/utils issues config shows the iid and title in the menu item within a project context 1`] = `"<small>123456</small> Project context issue title &lt;script&gt;alert(&#39;hi&#39;)&lt;/script&gt;"`;
-
-exports[`gfm_autocomplete/utils issues config shows the reference and title in the menu item within a group context 1`] = `"<small>gitlab#987654</small> Group context issue title &lt;script&gt;alert(&#39;hi&#39;)&lt;/script&gt;"`;
-
-exports[`gfm_autocomplete/utils labels config shows the title in the menu item 1`] = `
-"
- <span class=\\"dropdown-label-box\\" style=\\"background: #123456;\\"></span>
- bug &lt;script&gt;alert(&#39;hi&#39;)&lt;/script&gt;"
-`;
-
-exports[`gfm_autocomplete/utils members config shows an avatar character, name, parent name, and count in the menu item for a group 1`] = `
-"
- <div class=\\"gl-display-flex gl-align-items-center\\">
- <div class=\\"gl-avatar gl-avatar-s32 gl-flex-shrink-0 gl-rounded-small
- gl-display-flex gl-align-items-center gl-justify-content-center\\" aria-hidden=\\"true\\">
- G</div>
- <div class=\\"gl-line-height-normal gl-ml-4\\">
- <div>1-1s &lt;script&gt;alert(&#39;hi&#39;)&lt;/script&gt; (2)</div>
- <div class=\\"gl-text-gray-700\\">GitLab Support Team</div>
- </div>
-
- </div>
- "
-`;
-
-exports[`gfm_autocomplete/utils members config shows the avatar, name and username in the menu item for a user 1`] = `
-"
- <div class=\\"gl-display-flex gl-align-items-center\\">
- <img class=\\"gl-avatar gl-avatar-s32 gl-flex-shrink-0 gl-avatar-circle\\" src=\\"/uploads/-/system/user/avatar/123456/avatar.png\\" alt=\\"\\" />
- <div class=\\"gl-line-height-normal gl-ml-4\\">
- <div>My Name &lt;script&gt;alert(&#39;hi&#39;)&lt;/script&gt;</div>
- <div class=\\"gl-text-gray-700\\">@myusername</div>
- </div>
-
- </div>
- "
-`;
-
-exports[`gfm_autocomplete/utils merge requests config shows the iid and title in the menu item within a project context 1`] = `"<small>123456</small> Project context merge request title &lt;script&gt;alert(&#39;hi&#39;)&lt;/script&gt;"`;
-
-exports[`gfm_autocomplete/utils merge requests config shows the reference and title in the menu item within a group context 1`] = `"<small>gitlab!456789</small> Group context merge request title &lt;script&gt;alert(&#39;hi&#39;)&lt;/script&gt;"`;
-
-exports[`gfm_autocomplete/utils milestones config shows the title in the menu item 1`] = `"13.2 &lt;script&gt;alert(&#39;hi&#39;)&lt;/script&gt;"`;
-
-exports[`gfm_autocomplete/utils quick actions config shows the name, aliases, params and description in the menu item 1`] = `
-"<div>/unlabel <small>(or /remove_label)</small> <small>~label1 ~\\"label 2\\"</small></div>
- <div><small><em>Remove all or specific label(s)</em></small></div>"
-`;
-
-exports[`gfm_autocomplete/utils snippets config shows the id and title in the menu item 1`] = `"<small>123456</small> Snippet title &lt;script&gt;alert(&#39;hi&#39;)&lt;/script&gt;"`;
diff --git a/spec/frontend/vue_shared/components/gfm_autocomplete/gfm_autocomplete_spec.js b/spec/frontend/vue_shared/components/gfm_autocomplete/gfm_autocomplete_spec.js
deleted file mode 100644
index b4002fdf4ec..00000000000
--- a/spec/frontend/vue_shared/components/gfm_autocomplete/gfm_autocomplete_spec.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import Tribute from '@gitlab/tributejs';
-import { shallowMount } from '@vue/test-utils';
-import GfmAutocomplete from '~/vue_shared/components/gfm_autocomplete/gfm_autocomplete.vue';
-
-describe('GfmAutocomplete', () => {
- let wrapper;
-
- describe('tribute', () => {
- const mentions = '/gitlab-org/gitlab-test/-/autocomplete_sources/members?type=Issue&type_id=1';
-
- beforeEach(() => {
- wrapper = shallowMount(GfmAutocomplete, {
- propsData: {
- dataSources: {
- mentions,
- },
- },
- slots: {
- default: ['<input/>'],
- },
- });
- });
-
- it('is set to tribute instance variable', () => {
- expect(wrapper.vm.tribute instanceof Tribute).toBe(true);
- });
-
- it('contains the slot input element', () => {
- wrapper.find('input').setValue('@');
-
- expect(wrapper.vm.tribute.current.element).toBe(wrapper.find('input').element);
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/gfm_autocomplete/utils_spec.js b/spec/frontend/vue_shared/components/gfm_autocomplete/utils_spec.js
deleted file mode 100644
index 7ec3fbd4e3b..00000000000
--- a/spec/frontend/vue_shared/components/gfm_autocomplete/utils_spec.js
+++ /dev/null
@@ -1,427 +0,0 @@
-import { escape, last } from 'lodash';
-import { GfmAutocompleteType, tributeConfig } from '~/vue_shared/components/gfm_autocomplete/utils';
-
-describe('gfm_autocomplete/utils', () => {
- describe('emojis config', () => {
- const emojisConfig = tributeConfig[GfmAutocompleteType.Emojis].config;
- const emoji = 'raised_hands';
-
- it('uses : as the trigger', () => {
- expect(emojisConfig.trigger).toBe(':');
- });
-
- it('searches using the emoji name', () => {
- expect(emojisConfig.lookup(emoji)).toBe(emoji);
- });
-
- it('limits the number of rendered items to 100', () => {
- expect(emojisConfig.menuItemLimit).toBe(100);
- });
-
- it('shows the emoji name and icon in the menu item', () => {
- expect(emojisConfig.menuItemTemplate({ original: emoji })).toMatchSnapshot();
- });
-
- it('inserts the emoji name on autocomplete selection', () => {
- expect(emojisConfig.selectTemplate({ original: emoji })).toBe(`:${emoji}:`);
- });
- });
-
- describe('issues config', () => {
- const issuesConfig = tributeConfig[GfmAutocompleteType.Issues].config;
- const groupContextIssue = {
- iid: 987654,
- reference: 'gitlab#987654',
- title: "Group context issue title <script>alert('hi')</script>",
- };
- const projectContextIssue = {
- id: null,
- iid: 123456,
- time_estimate: 0,
- title: "Project context issue title <script>alert('hi')</script>",
- };
-
- it('uses # as the trigger', () => {
- expect(issuesConfig.trigger).toBe('#');
- });
-
- it('searches using both the iid and title', () => {
- expect(issuesConfig.lookup(projectContextIssue)).toBe(
- `${projectContextIssue.iid}${projectContextIssue.title}`,
- );
- });
-
- it('limits the number of rendered items to 100', () => {
- expect(issuesConfig.menuItemLimit).toBe(100);
- });
-
- it('shows the reference and title in the menu item within a group context', () => {
- expect(issuesConfig.menuItemTemplate({ original: groupContextIssue })).toMatchSnapshot();
- });
-
- it('shows the iid and title in the menu item within a project context', () => {
- expect(issuesConfig.menuItemTemplate({ original: projectContextIssue })).toMatchSnapshot();
- });
-
- it('inserts the reference on autocomplete selection within a group context', () => {
- expect(issuesConfig.selectTemplate({ original: groupContextIssue })).toBe(
- groupContextIssue.reference,
- );
- });
-
- it('inserts the iid on autocomplete selection within a project context', () => {
- expect(issuesConfig.selectTemplate({ original: projectContextIssue })).toBe(
- `#${projectContextIssue.iid}`,
- );
- });
- });
-
- describe('labels config', () => {
- const labelsConfig = tributeConfig[GfmAutocompleteType.Labels].config;
- const labelsFilter = tributeConfig[GfmAutocompleteType.Labels].filterValues;
- const label = {
- color: '#123456',
- textColor: '#FFFFFF',
- title: `bug <script>alert('hi')</script>`,
- type: 'GroupLabel',
- };
- const singleWordLabel = {
- color: '#456789',
- textColor: '#DDD',
- title: `bug`,
- type: 'GroupLabel',
- };
- const numericalLabel = {
- color: '#abcdef',
- textColor: '#AAA',
- title: 123456,
- type: 'ProjectLabel',
- };
-
- it('uses ~ as the trigger', () => {
- expect(labelsConfig.trigger).toBe('~');
- });
-
- it('searches using `title`', () => {
- expect(labelsConfig.lookup).toBe('title');
- });
-
- it('limits the number of rendered items to 100', () => {
- expect(labelsConfig.menuItemLimit).toBe(100);
- });
-
- it('shows the title in the menu item', () => {
- expect(labelsConfig.menuItemTemplate({ original: label })).toMatchSnapshot();
- });
-
- it('inserts the title on autocomplete selection', () => {
- expect(labelsConfig.selectTemplate({ original: singleWordLabel })).toBe(
- `~${escape(singleWordLabel.title)}`,
- );
- });
-
- it('inserts the title enclosed with quotes on autocomplete selection when the title is numerical', () => {
- expect(labelsConfig.selectTemplate({ original: numericalLabel })).toBe(
- `~"${escape(numericalLabel.title)}"`,
- );
- });
-
- it('inserts the title enclosed with quotes on autocomplete selection when the title contains multiple words', () => {
- expect(labelsConfig.selectTemplate({ original: label })).toBe(`~"${escape(label.title)}"`);
- });
-
- describe('filter', () => {
- const collection = [label, singleWordLabel, { ...numericalLabel, set: true }];
-
- describe('/label quick action', () => {
- describe('when the line starts with `/label`', () => {
- it('shows labels that are not currently selected', () => {
- const fullText = '/label ~';
- const selectionStart = 8;
-
- expect(labelsFilter({ collection, fullText, selectionStart })).toEqual([
- collection[0],
- collection[1],
- ]);
- });
- });
-
- describe('when the line does not start with `/label`', () => {
- it('shows all labels', () => {
- const fullText = '~';
- const selectionStart = 1;
-
- expect(labelsFilter({ collection, fullText, selectionStart })).toEqual(collection);
- });
- });
- });
-
- describe('/unlabel quick action', () => {
- describe('when the line starts with `/unlabel`', () => {
- it('shows labels that are currently selected', () => {
- const fullText = '/unlabel ~';
- const selectionStart = 10;
-
- expect(labelsFilter({ collection, fullText, selectionStart })).toEqual([collection[2]]);
- });
- });
-
- describe('when the line does not start with `/unlabel`', () => {
- it('shows all labels', () => {
- const fullText = '~';
- const selectionStart = 1;
-
- expect(labelsFilter({ collection, fullText, selectionStart })).toEqual(collection);
- });
- });
- });
- });
- });
-
- describe('members config', () => {
- const membersConfig = tributeConfig[GfmAutocompleteType.Members].config;
- const membersFilter = tributeConfig[GfmAutocompleteType.Members].filterValues;
- const userMember = {
- type: 'User',
- username: 'myusername',
- name: "My Name <script>alert('hi')</script>",
- avatar_url: '/uploads/-/system/user/avatar/123456/avatar.png',
- availability: null,
- };
- const groupMember = {
- type: 'Group',
- username: 'gitlab-com/support/1-1s',
- name: "GitLab.com / GitLab Support Team / 1-1s <script>alert('hi')</script>",
- avatar_url: null,
- count: 2,
- mentionsDisabled: null,
- };
-
- it('uses @ as the trigger', () => {
- expect(membersConfig.trigger).toBe('@');
- });
-
- it('inserts the username on autocomplete selection', () => {
- expect(membersConfig.fillAttr).toBe('username');
- });
-
- it('searches using both the name and username for a user', () => {
- expect(membersConfig.lookup(userMember)).toBe(`${userMember.name}${userMember.username}`);
- });
-
- it('searches using only its own name and not its ancestors for a group', () => {
- expect(membersConfig.lookup(groupMember)).toBe(last(groupMember.name.split(' / ')));
- });
-
- it('limits the items in the autocomplete menu to 10', () => {
- expect(membersConfig.menuItemLimit).toBe(10);
- });
-
- it('shows the avatar, name and username in the menu item for a user', () => {
- expect(membersConfig.menuItemTemplate({ original: userMember })).toMatchSnapshot();
- });
-
- it('shows an avatar character, name, parent name, and count in the menu item for a group', () => {
- expect(membersConfig.menuItemTemplate({ original: groupMember })).toMatchSnapshot();
- });
-
- describe('filter', () => {
- const assignees = [userMember.username];
- const collection = [userMember, groupMember];
-
- describe('/assign quick action', () => {
- describe('when the line starts with `/assign`', () => {
- it('shows members that are not currently selected', () => {
- const fullText = '/assign @';
- const selectionStart = 9;
-
- expect(membersFilter({ assignees, collection, fullText, selectionStart })).toEqual([
- collection[1],
- ]);
- });
- });
-
- describe('when the line does not start with `/assign`', () => {
- it('shows all labels', () => {
- const fullText = '@';
- const selectionStart = 1;
-
- expect(membersFilter({ assignees, collection, fullText, selectionStart })).toEqual(
- collection,
- );
- });
- });
- });
-
- describe('/unassign quick action', () => {
- describe('when the line starts with `/unassign`', () => {
- it('shows members that are currently selected', () => {
- const fullText = '/unassign @';
- const selectionStart = 11;
-
- expect(membersFilter({ assignees, collection, fullText, selectionStart })).toEqual([
- collection[0],
- ]);
- });
- });
-
- describe('when the line does not start with `/unassign`', () => {
- it('shows all members', () => {
- const fullText = '@';
- const selectionStart = 1;
-
- expect(membersFilter({ assignees, collection, fullText, selectionStart })).toEqual(
- collection,
- );
- });
- });
- });
- });
- });
-
- describe('merge requests config', () => {
- const mergeRequestsConfig = tributeConfig[GfmAutocompleteType.MergeRequests].config;
- const groupContextMergeRequest = {
- iid: 456789,
- reference: 'gitlab!456789',
- title: "Group context merge request title <script>alert('hi')</script>",
- };
- const projectContextMergeRequest = {
- id: null,
- iid: 123456,
- time_estimate: 0,
- title: "Project context merge request title <script>alert('hi')</script>",
- };
-
- it('uses ! as the trigger', () => {
- expect(mergeRequestsConfig.trigger).toBe('!');
- });
-
- it('searches using both the iid and title', () => {
- expect(mergeRequestsConfig.lookup(projectContextMergeRequest)).toBe(
- `${projectContextMergeRequest.iid}${projectContextMergeRequest.title}`,
- );
- });
-
- it('limits the number of rendered items to 100', () => {
- expect(mergeRequestsConfig.menuItemLimit).toBe(100);
- });
-
- it('shows the reference and title in the menu item within a group context', () => {
- expect(
- mergeRequestsConfig.menuItemTemplate({ original: groupContextMergeRequest }),
- ).toMatchSnapshot();
- });
-
- it('shows the iid and title in the menu item within a project context', () => {
- expect(
- mergeRequestsConfig.menuItemTemplate({ original: projectContextMergeRequest }),
- ).toMatchSnapshot();
- });
-
- it('inserts the reference on autocomplete selection within a group context', () => {
- expect(mergeRequestsConfig.selectTemplate({ original: groupContextMergeRequest })).toBe(
- groupContextMergeRequest.reference,
- );
- });
-
- it('inserts the iid on autocomplete selection within a project context', () => {
- expect(mergeRequestsConfig.selectTemplate({ original: projectContextMergeRequest })).toBe(
- `!${projectContextMergeRequest.iid}`,
- );
- });
- });
-
- describe('milestones config', () => {
- const milestonesConfig = tributeConfig[GfmAutocompleteType.Milestones].config;
- const milestone = {
- id: null,
- iid: 49,
- title: "13.2 <script>alert('hi')</script>",
- };
-
- it('uses % as the trigger', () => {
- expect(milestonesConfig.trigger).toBe('%');
- });
-
- it('searches using the title', () => {
- expect(milestonesConfig.lookup).toBe('title');
- });
-
- it('limits the number of rendered items to 100', () => {
- expect(milestonesConfig.menuItemLimit).toBe(100);
- });
-
- it('shows the title in the menu item', () => {
- expect(milestonesConfig.menuItemTemplate({ original: milestone })).toMatchSnapshot();
- });
-
- it('inserts the title on autocomplete selection', () => {
- expect(milestonesConfig.selectTemplate({ original: milestone })).toBe(
- `%"${escape(milestone.title)}"`,
- );
- });
- });
-
- describe('quick actions config', () => {
- const quickActionsConfig = tributeConfig[GfmAutocompleteType.QuickActions].config;
- const quickAction = {
- name: 'unlabel',
- aliases: ['remove_label'],
- description: 'Remove all or specific label(s)',
- warning: '',
- icon: '',
- params: ['~label1 ~"label 2"'],
- };
-
- it('uses / as the trigger', () => {
- expect(quickActionsConfig.trigger).toBe('/');
- });
-
- it('inserts the name on autocomplete selection', () => {
- expect(quickActionsConfig.fillAttr).toBe('name');
- });
-
- it('searches using both the name and aliases', () => {
- expect(quickActionsConfig.lookup(quickAction)).toBe(
- `${quickAction.name}${quickAction.aliases.join(', /')}`,
- );
- });
-
- it('limits the number of rendered items to 100', () => {
- expect(quickActionsConfig.menuItemLimit).toBe(100);
- });
-
- it('shows the name, aliases, params and description in the menu item', () => {
- expect(quickActionsConfig.menuItemTemplate({ original: quickAction })).toMatchSnapshot();
- });
- });
-
- describe('snippets config', () => {
- const snippetsConfig = tributeConfig[GfmAutocompleteType.Snippets].config;
- const snippet = {
- id: 123456,
- title: "Snippet title <script>alert('hi')</script>",
- };
-
- it('uses $ as the trigger', () => {
- expect(snippetsConfig.trigger).toBe('$');
- });
-
- it('inserts the id on autocomplete selection', () => {
- expect(snippetsConfig.fillAttr).toBe('id');
- });
-
- it('searches using both the id and title', () => {
- expect(snippetsConfig.lookup(snippet)).toBe(`${snippet.id}${snippet.title}`);
- });
-
- it('limits the number of rendered items to 100', () => {
- expect(snippetsConfig.menuItemLimit).toBe(100);
- });
-
- it('shows the id and title in the menu item', () => {
- expect(snippetsConfig.menuItemTemplate({ original: snippet })).toMatchSnapshot();
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/gl_countdown_spec.js b/spec/frontend/vue_shared/components/gl_countdown_spec.js
index 82d18c7fd3f..0d1d42082ab 100644
--- a/spec/frontend/vue_shared/components/gl_countdown_spec.js
+++ b/spec/frontend/vue_shared/components/gl_countdown_spec.js
@@ -1,4 +1,4 @@
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
@@ -17,38 +17,34 @@ describe('GlCountdown', () => {
});
describe('when there is time remaining', () => {
- beforeEach((done) => {
+ beforeEach(async () => {
vm = mountComponent(Component, {
endDateString: '2000-01-01T01:02:03Z',
});
- Vue.nextTick().then(done).catch(done.fail);
+ await nextTick();
});
it('displays remaining time', () => {
expect(vm.$el.textContent).toContain('01:02:03');
});
- it('updates remaining time', (done) => {
+ it('updates remaining time', async () => {
now = '2000-01-01T00:00:01Z';
jest.advanceTimersByTime(1000);
- Vue.nextTick()
- .then(() => {
- expect(vm.$el.textContent).toContain('01:02:02');
- done();
- })
- .catch(done.fail);
+ await nextTick();
+ expect(vm.$el.textContent).toContain('01:02:02');
});
});
describe('when there is no time remaining', () => {
- beforeEach((done) => {
+ beforeEach(async () => {
vm = mountComponent(Component, {
endDateString: '1900-01-01T00:00:00Z',
});
- Vue.nextTick().then(done).catch(done.fail);
+ await nextTick();
});
it('displays 00:00:00', () => {
diff --git a/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js b/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js
index b837a998cd6..c0a6588833e 100644
--- a/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js
+++ b/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js
@@ -1,6 +1,6 @@
import { GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
import GlModalVuex from '~/vue_shared/components/gl_modal_vuex.vue';
@@ -118,7 +118,7 @@ describe('GlModalVuex', () => {
expect(actions.hide).toHaveBeenCalledTimes(1);
});
- it('calls bootstrap show when isVisible changes', (done) => {
+ it('calls bootstrap show when isVisible changes', async () => {
state.isVisible = false;
factory();
@@ -126,16 +126,11 @@ describe('GlModalVuex', () => {
state.isVisible = true;
- wrapper.vm
- .$nextTick()
- .then(() => {
- expect(rootEmit).toHaveBeenCalledWith(BV_SHOW_MODAL, TEST_MODAL_ID);
- })
- .then(done)
- .catch(done.fail);
+ await nextTick();
+ expect(rootEmit).toHaveBeenCalledWith(BV_SHOW_MODAL, TEST_MODAL_ID);
});
- it('calls bootstrap hide when isVisible changes', (done) => {
+ it('calls bootstrap hide when isVisible changes', async () => {
state.isVisible = true;
factory();
@@ -143,13 +138,8 @@ describe('GlModalVuex', () => {
state.isVisible = false;
- wrapper.vm
- .$nextTick()
- .then(() => {
- expect(rootEmit).toHaveBeenCalledWith(BV_HIDE_MODAL, TEST_MODAL_ID);
- })
- .then(done)
- .catch(done.fail);
+ await nextTick();
+ expect(rootEmit).toHaveBeenCalledWith(BV_HIDE_MODAL, TEST_MODAL_ID);
});
it.each(['ok', 'cancel'])(
diff --git a/spec/frontend/vue_shared/components/help_popover_spec.js b/spec/frontend/vue_shared/components/help_popover_spec.js
index 30c6fa04032..597fb63d95c 100644
--- a/spec/frontend/vue_shared/components/help_popover_spec.js
+++ b/spec/frontend/vue_shared/components/help_popover_spec.js
@@ -9,59 +9,117 @@ describe('HelpPopover', () => {
const findQuestionButton = () => wrapper.find(GlButton);
const findPopover = () => wrapper.find(GlPopover);
- const buildWrapper = (options = {}) => {
+
+ const createComponent = ({ props, ...opts } = {}) => {
wrapper = mount(HelpPopover, {
propsData: {
options: {
title,
content,
- ...options,
},
+ ...props,
},
+ ...opts,
});
};
- beforeEach(() => {
- buildWrapper();
- });
-
afterEach(() => {
wrapper.destroy();
});
- it('renders a link button with an icon question', () => {
- expect(findQuestionButton().props()).toMatchObject({
- icon: 'question',
- variant: 'link',
+ describe('with title and content', () => {
+ beforeEach(() => {
+ createComponent();
});
- });
- it('renders popover that uses the question button as target', () => {
- expect(findPopover().props().target()).toBe(findQuestionButton().vm.$el);
- });
+ it('renders a link button with an icon question', () => {
+ expect(findQuestionButton().props()).toMatchObject({
+ icon: 'question',
+ variant: 'link',
+ });
+ });
- it('allows rendering title with HTML tags', () => {
- expect(findPopover().find('strong').exists()).toBe(true);
- });
+ it('renders popover that uses the question button as target', () => {
+ expect(findPopover().props().target()).toBe(findQuestionButton().vm.$el);
+ });
- it('allows rendering content with HTML tags', () => {
- expect(findPopover().find('b').exists()).toBe(true);
+ it('shows title and content', () => {
+ expect(findPopover().html()).toContain(title);
+ expect(findPopover().html()).toContain(content);
+ });
+
+ it('allows rendering title with HTML tags', () => {
+ expect(findPopover().find('strong').exists()).toBe(true);
+ });
+
+ it('allows rendering content with HTML tags', () => {
+ expect(findPopover().find('b').exists()).toBe(true);
+ });
});
describe('without title', () => {
- it('does not render title', () => {
- buildWrapper({ title: null });
+ beforeEach(() => {
+ createComponent({
+ props: {
+ options: {
+ title: null,
+ content,
+ },
+ },
+ });
+ });
+
+ it('does not show title', () => {
+ expect(findPopover().html()).not.toContain(title);
+ });
- expect(findPopover().find('span').exists()).toBe(false);
+ it('shows content', () => {
+ expect(findPopover().html()).toContain(content);
});
});
- it('binds other popover options to the popover instance', () => {
+ describe('with other options', () => {
const placement = 'bottom';
- wrapper.destroy();
- buildWrapper({ placement });
+ beforeEach(() => {
+ createComponent({
+ props: {
+ options: {
+ placement,
+ },
+ },
+ });
+ });
+
+ it('options bind to the popover', () => {
+ expect(findPopover().props().placement).toBe(placement);
+ });
+ });
+
+ describe('with custom slots', () => {
+ const titleSlot = '<h1>title</h1>';
+ const defaultSlot = '<strong>content</strong>';
- expect(findPopover().props().placement).toBe(placement);
+ beforeEach(() => {
+ createComponent({
+ slots: {
+ title: titleSlot,
+ default: defaultSlot,
+ },
+ });
+ });
+
+ it('shows title slot', () => {
+ expect(findPopover().html()).toContain(titleSlot);
+ });
+
+ it('shows default content slot', () => {
+ expect(findPopover().html()).toContain(defaultSlot);
+ });
+
+ it('overrides title and content from options', () => {
+ expect(findPopover().html()).not.toContain(title);
+ expect(findPopover().html()).toContain(content);
+ });
});
});
diff --git a/spec/frontend/vue_shared/components/local_storage_sync_spec.js b/spec/frontend/vue_shared/components/local_storage_sync_spec.js
index 4c5a0c1e601..dac633fe6c8 100644
--- a/spec/frontend/vue_shared/components/local_storage_sync_spec.js
+++ b/spec/frontend/vue_shared/components/local_storage_sync_spec.js
@@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
describe('Local Storage Sync', () => {
@@ -49,7 +50,7 @@ describe('Local Storage Sync', () => {
it.each('foo', 3, true, ['foo', 'bar'], { foo: 'bar' })(
'saves updated value to localStorage',
- (newValue) => {
+ async (newValue) => {
createComponent({
props: {
storageKey,
@@ -59,9 +60,8 @@ describe('Local Storage Sync', () => {
wrapper.setProps({ value: newValue });
- return wrapper.vm.$nextTick().then(() => {
- expect(localStorage.getItem(storageKey)).toBe(String(newValue));
- });
+ await nextTick();
+ expect(localStorage.getItem(storageKey)).toBe(String(newValue));
},
);
@@ -109,7 +109,7 @@ describe('Local Storage Sync', () => {
expect(localStorage.getItem(storageKey)).toBe(savedValue);
});
- it('updating the value updates localStorage', () => {
+ it('updating the value updates localStorage', async () => {
createComponent({
props: {
storageKey,
@@ -122,9 +122,8 @@ describe('Local Storage Sync', () => {
value: newValue,
});
- return wrapper.vm.$nextTick().then(() => {
- expect(localStorage.getItem(storageKey)).toBe(newValue);
- });
+ await nextTick();
+ expect(localStorage.getItem(storageKey)).toBe(newValue);
});
it('persists the value by default', async () => {
@@ -137,7 +136,7 @@ describe('Local Storage Sync', () => {
});
wrapper.setProps({ value: persistedValue });
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(localStorage.getItem(storageKey)).toBe(persistedValue);
});
@@ -151,7 +150,7 @@ describe('Local Storage Sync', () => {
});
wrapper.setProps({ persist: false, value: notPersistedValue });
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(localStorage.getItem(storageKey)).not.toBe(notPersistedValue);
});
});
@@ -172,7 +171,7 @@ describe('Local Storage Sync', () => {
${{ foo: 'bar' }} | ${'{"foo":"bar"}'}
`('given $value', ({ value, serializedValue }) => {
describe('is a new value', () => {
- beforeEach(() => {
+ beforeEach(async () => {
createComponent({
props: {
storageKey,
@@ -183,7 +182,7 @@ describe('Local Storage Sync', () => {
wrapper.setProps({ value });
- return wrapper.vm.$nextTick();
+ await nextTick();
});
it('serializes the value correctly to localStorage', () => {
@@ -253,7 +252,7 @@ describe('Local Storage Sync', () => {
value,
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(localStorage.getItem(storageKey)).toBe(value);
@@ -261,7 +260,7 @@ describe('Local Storage Sync', () => {
clear: true,
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(localStorage.getItem(storageKey)).toBe(null);
});
diff --git a/spec/frontend/vue_shared/components/markdown/field_spec.js b/spec/frontend/vue_shared/components/markdown/field_spec.js
index 0d90ca7f1f6..c7ad47b6ef7 100644
--- a/spec/frontend/vue_shared/components/markdown/field_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/field_spec.js
@@ -1,10 +1,10 @@
-import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import AxiosMockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import { TEST_HOST, FIXTURES_PATH } from 'spec/test_constants';
import axios from '~/lib/utils/axios_utils';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
const markdownPreviewPath = `${TEST_HOST}/preview`;
const markdownDocsPath = `${TEST_HOST}/docs`;
@@ -12,8 +12,8 @@ const textareaValue = 'testing\n123';
const uploadsPath = 'test/uploads';
function assertMarkdownTabs(isWrite, writeLink, previewLink, wrapper) {
- expect(writeLink.element.parentNode.classList.contains('active')).toBe(isWrite);
- expect(previewLink.element.parentNode.classList.contains('active')).toBe(!isWrite);
+ expect(writeLink.element.children[0].classList.contains('active')).toBe(isWrite);
+ expect(previewLink.element.children[0].classList.contains('active')).toBe(!isWrite);
expect(wrapper.find('.md-preview-holder').element.style.display).toBe(isWrite ? 'none' : '');
}
@@ -29,14 +29,13 @@ describe('Markdown field component', () => {
afterEach(() => {
subject.destroy();
- subject = null;
axiosMock.restore();
});
function createSubject(lines = []) {
// We actually mount a wrapper component so that we can force Vue to rerender classes in order to test a regression
// caused by mixing Vanilla JS and Vue.
- subject = mount(
+ subject = mountExtended(
{
components: {
MarkdownField,
@@ -63,12 +62,17 @@ describe('Markdown field component', () => {
textareaValue,
lines,
},
+ provide: {
+ glFeatures: {
+ contactsAutocomplete: true,
+ },
+ },
},
);
}
- const getPreviewLink = () => subject.find('.nav-links .js-preview-link');
- const getWriteLink = () => subject.find('.nav-links .js-write-link');
+ const getPreviewLink = () => subject.findByTestId('preview-tab');
+ const getWriteLink = () => subject.findByTestId('write-tab');
const getMarkdownButton = () => subject.find('.js-md');
const getAllMarkdownButtons = () => subject.findAll('.js-md');
const getVideo = () => subject.find('video');
@@ -100,115 +104,100 @@ describe('Markdown field component', () => {
axiosMock.onPost(markdownPreviewPath).reply(200, { body: previewHTML });
});
- it('sets preview link as active', () => {
+ it('sets preview link as active', async () => {
previewLink = getPreviewLink();
- previewLink.trigger('click');
+ previewLink.vm.$emit('click', { target: {} });
- return subject.vm.$nextTick().then(() => {
- expect(previewLink.element.parentNode.classList.contains('active')).toBeTruthy();
- });
+ await nextTick();
+ expect(previewLink.element.children[0].classList.contains('active')).toBe(true);
});
- it('shows preview loading text', () => {
+ it('shows preview loading text', async () => {
previewLink = getPreviewLink();
- previewLink.trigger('click');
+ previewLink.vm.$emit('click', { target: {} });
- return subject.vm.$nextTick(() => {
- expect(subject.find('.md-preview-holder').element.textContent.trim()).toContain(
- 'Loading…',
- );
- });
+ await nextTick();
+ expect(subject.find('.md-preview-holder').element.textContent.trim()).toContain('Loading…');
});
- it('renders markdown preview and GFM', () => {
+ it('renders markdown preview and GFM', async () => {
const renderGFMSpy = jest.spyOn($.fn, 'renderGFM');
previewLink = getPreviewLink();
- previewLink.trigger('click');
+ previewLink.vm.$emit('click', { target: {} });
- return axios.waitFor(markdownPreviewPath).then(() => {
- expect(subject.find('.md-preview-holder').element.innerHTML).toContain(previewHTML);
- expect(renderGFMSpy).toHaveBeenCalled();
- });
+ await axios.waitFor(markdownPreviewPath);
+ expect(subject.find('.md-preview-holder').element.innerHTML).toContain(previewHTML);
+ expect(renderGFMSpy).toHaveBeenCalled();
});
- it('calls video.pause() on comment input when isSubmitting is changed to true', () => {
+ it('calls video.pause() on comment input when isSubmitting is changed to true', async () => {
previewLink = getPreviewLink();
- previewLink.trigger('click');
+ previewLink.vm.$emit('click', { target: {} });
- let callPause;
+ await axios.waitFor(markdownPreviewPath);
+ const video = getVideo();
+ const callPause = jest.spyOn(video.element, 'pause').mockImplementation(() => true);
- return axios
- .waitFor(markdownPreviewPath)
- .then(() => {
- const video = getVideo();
- callPause = jest.spyOn(video.element, 'pause').mockImplementation(() => true);
+ subject.setProps({ isSubmitting: true });
- subject.setProps({ isSubmitting: true });
-
- return subject.vm.$nextTick();
- })
- .then(() => {
- expect(callPause).toHaveBeenCalled();
- });
+ await nextTick();
+ expect(callPause).toHaveBeenCalled();
});
it('clicking already active write or preview link does nothing', async () => {
writeLink = getWriteLink();
previewLink = getPreviewLink();
- writeLink.trigger('click');
- await subject.vm.$nextTick();
+ writeLink.vm.$emit('click', { target: {} });
+ await nextTick();
assertMarkdownTabs(true, writeLink, previewLink, subject);
- writeLink.trigger('click');
- await subject.vm.$nextTick();
+ writeLink.vm.$emit('click', { target: {} });
+ await nextTick();
assertMarkdownTabs(true, writeLink, previewLink, subject);
- previewLink.trigger('click');
- await subject.vm.$nextTick();
+ previewLink.vm.$emit('click', { target: {} });
+ await nextTick();
assertMarkdownTabs(false, writeLink, previewLink, subject);
- previewLink.trigger('click');
- await subject.vm.$nextTick();
+ previewLink.vm.$emit('click', { target: {} });
+ await nextTick();
assertMarkdownTabs(false, writeLink, previewLink, subject);
});
});
describe('markdown buttons', () => {
- it('converts single words', () => {
+ it('converts single words', async () => {
const textarea = subject.find('textarea').element;
textarea.setSelectionRange(0, 7);
const markdownButton = getMarkdownButton();
markdownButton.trigger('click');
- return subject.vm.$nextTick(() => {
- expect(textarea.value).toContain('**testing**');
- });
+ await nextTick();
+ expect(textarea.value).toContain('**testing**');
});
- it('converts a line', () => {
+ it('converts a line', async () => {
const textarea = subject.find('textarea').element;
textarea.setSelectionRange(0, 0);
const markdownButton = getAllMarkdownButtons().wrappers[5];
markdownButton.trigger('click');
- return subject.vm.$nextTick(() => {
- expect(textarea.value).toContain('- testing');
- });
+ await nextTick();
+ expect(textarea.value).toContain('- testing');
});
- it('converts multiple lines', () => {
+ it('converts multiple lines', async () => {
const textarea = subject.find('textarea').element;
textarea.setSelectionRange(0, 50);
const markdownButton = getAllMarkdownButtons().wrappers[5];
markdownButton.trigger('click');
- return subject.vm.$nextTick(() => {
- expect(textarea.value).toContain('- testing\n- 123');
- });
+ await nextTick();
+ expect(textarea.value).toContain('- testing\n- 123');
});
});
@@ -229,7 +218,7 @@ describe('Markdown field component', () => {
// Do something to trigger rerendering the class
subject.setProps({ wrapperClasses: 'foo' });
- await subject.vm.$nextTick();
+ await nextTick();
});
it('should have rerendered classes and kept gfm-form', () => {
diff --git a/spec/frontend/vue_shared/components/markdown/header_spec.js b/spec/frontend/vue_shared/components/markdown/header_spec.js
index fec6abc9639..93ce3935fab 100644
--- a/spec/frontend/vue_shared/components/markdown/header_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/header_spec.js
@@ -1,20 +1,25 @@
-import { shallowMount } from '@vue/test-utils';
import $ from 'jquery';
+import { nextTick } from 'vue';
+import { GlTabs } from '@gitlab/ui';
import HeaderComponent from '~/vue_shared/components/markdown/header.vue';
import ToolbarButton from '~/vue_shared/components/markdown/toolbar_button.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
describe('Markdown field header component', () => {
let wrapper;
const createWrapper = (props) => {
- wrapper = shallowMount(HeaderComponent, {
+ wrapper = shallowMountExtended(HeaderComponent, {
propsData: {
previewMarkdown: false,
...props,
},
+ stubs: { GlTabs },
});
};
+ const findWriteTab = () => wrapper.findByTestId('write-tab');
+ const findPreviewTab = () => wrapper.findByTestId('preview-tab');
const findToolbarButtons = () => wrapper.findAll(ToolbarButton);
const findToolbarButtonByProp = (prop, value) =>
findToolbarButtons()
@@ -33,7 +38,6 @@ describe('Markdown field header component', () => {
afterEach(() => {
wrapper.destroy();
- wrapper = null;
});
describe('markdown header buttons', () => {
@@ -74,30 +78,29 @@ describe('Markdown field header component', () => {
});
});
- it('renders `write` link as active when previewMarkdown is false', () => {
- expect(wrapper.find('li:nth-child(1)').classes()).toContain('active');
+ it('activates `write` tab when previewMarkdown is false', () => {
+ expect(findWriteTab().attributes('active')).toBe('true');
+ expect(findPreviewTab().attributes('active')).toBeUndefined();
});
- it('renders `preview` link as active when previewMarkdown is true', () => {
+ it('activates `preview` tab when previewMarkdown is true', () => {
createWrapper({ previewMarkdown: true });
- expect(wrapper.find('li:nth-child(2)').classes()).toContain('active');
+ expect(findWriteTab().attributes('active')).toBeUndefined();
+ expect(findPreviewTab().attributes('active')).toBe('true');
});
- it('emits toggle markdown event when clicking preview', () => {
- wrapper.find('.js-preview-link').trigger('click');
+ it('emits toggle markdown event when clicking preview tab', async () => {
+ const eventData = { target: {} };
+ findPreviewTab().vm.$emit('click', eventData);
- return wrapper.vm
- .$nextTick()
- .then(() => {
- expect(wrapper.emitted('preview-markdown').length).toEqual(1);
+ await nextTick();
+ expect(wrapper.emitted('preview-markdown').length).toEqual(1);
- wrapper.find('.js-write-link').trigger('click');
- return wrapper.vm.$nextTick();
- })
- .then(() => {
- expect(wrapper.emitted('write-markdown').length).toEqual(1);
- });
+ findWriteTab().vm.$emit('click', eventData);
+
+ await nextTick();
+ expect(wrapper.emitted('write-markdown').length).toEqual(1);
});
it('does not emit toggle markdown event when triggered from another form', () => {
@@ -112,12 +115,10 @@ describe('Markdown field header component', () => {
});
it('blurs preview link after click', () => {
- const link = wrapper.find('li:nth-child(2) button');
- jest.spyOn(HTMLElement.prototype, 'blur').mockImplementation();
-
- link.trigger('click');
+ const target = { blur: jest.fn() };
+ findPreviewTab().vm.$emit('click', { target });
- expect(link.element.blur).toHaveBeenCalled();
+ expect(target.blur).toHaveBeenCalled();
});
it('renders markdown table template', () => {
diff --git a/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js b/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js
index 9bc2aad1895..9944267cf24 100644
--- a/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js
@@ -1,5 +1,6 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import ApplySuggestion from '~/vue_shared/components/markdown/apply_suggestion.vue';
import SuggestionDiffHeader from '~/vue_shared/components/markdown/suggestion_diff_header.vue';
@@ -103,15 +104,14 @@ describe('Suggestion Diff component', () => {
expect(wrapper.text()).toContain('Applying suggestion...');
});
- it('when callback of apply is called, hides loading', () => {
+ it('when callback of apply is called, hides loading', async () => {
const [callback] = wrapper.emitted().apply[0];
callback();
- return wrapper.vm.$nextTick().then(() => {
- expect(findApplyButton().exists()).toBe(true);
- expect(findLoading().exists()).toBe(false);
- });
+ await nextTick();
+ expect(findApplyButton().exists()).toBe(true);
+ expect(findLoading().exists()).toBe(false);
});
});
diff --git a/spec/frontend/vue_shared/components/markdown/suggestions_spec.js b/spec/frontend/vue_shared/components/markdown/suggestions_spec.js
index 6fcac2df0b6..8f4235cfe41 100644
--- a/spec/frontend/vue_shared/components/markdown/suggestions_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/suggestions_spec.js
@@ -1,4 +1,4 @@
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import SuggestionsComponent from '~/vue_shared/components/markdown/suggestions.vue';
const MOCK_DATA = {
@@ -51,7 +51,7 @@ describe('Suggestion component', () => {
let vm;
let diffTable;
- beforeEach((done) => {
+ beforeEach(async () => {
const Component = Vue.extend(SuggestionsComponent);
vm = new Component({
@@ -62,7 +62,7 @@ describe('Suggestion component', () => {
jest.spyOn(vm, 'renderSuggestions').mockImplementation(() => {});
vm.renderSuggestions();
- Vue.nextTick(done);
+ await nextTick();
});
describe('mounted', () => {
diff --git a/spec/frontend/vue_shared/components/modal_copy_button_spec.js b/spec/frontend/vue_shared/components/modal_copy_button_spec.js
index adb72c3ef85..b57efc88d57 100644
--- a/spec/frontend/vue_shared/components/modal_copy_button_spec.js
+++ b/spec/frontend/vue_shared/components/modal_copy_button_spec.js
@@ -1,4 +1,5 @@
import { shallowMount, createWrapper } from '@vue/test-utils';
+import { nextTick } from 'vue';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
@@ -20,7 +21,7 @@ describe('modal copy button', () => {
});
describe('clipboard', () => {
- it('should fire a `success` event on click', () => {
+ it('should fire a `success` event on click', async () => {
const root = createWrapper(wrapper.vm.$root);
document.execCommand = jest.fn(() => true);
window.getSelection = jest.fn(() => ({
@@ -29,20 +30,18 @@ describe('modal copy button', () => {
}));
wrapper.trigger('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted().success).not.toBeEmpty();
- expect(document.execCommand).toHaveBeenCalledWith('copy');
- expect(root.emitted(BV_HIDE_TOOLTIP)).toEqual([['test-id']]);
- });
+ await nextTick();
+ expect(wrapper.emitted().success).not.toBeEmpty();
+ expect(document.execCommand).toHaveBeenCalledWith('copy');
+ expect(root.emitted(BV_HIDE_TOOLTIP)).toEqual([['test-id']]);
});
- it("should propagate the clipboard error event if execCommand doesn't work", () => {
+ it("should propagate the clipboard error event if execCommand doesn't work", async () => {
document.execCommand = jest.fn(() => false);
wrapper.trigger('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted().error).not.toBeEmpty();
- expect(document.execCommand).toHaveBeenCalledWith('copy');
- });
+ await nextTick();
+ expect(wrapper.emitted().error).not.toBeEmpty();
+ expect(document.execCommand).toHaveBeenCalledWith('copy');
});
});
});
diff --git a/spec/frontend/vue_shared/components/multiselect_dropdown_spec.js b/spec/frontend/vue_shared/components/multiselect_dropdown_spec.js
deleted file mode 100644
index 566ca1817f2..00000000000
--- a/spec/frontend/vue_shared/components/multiselect_dropdown_spec.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import { GlDropdown } from '@gitlab/ui';
-import { getByText } from '@testing-library/dom';
-import { shallowMount } from '@vue/test-utils';
-import MultiSelectDropdown from '~/vue_shared/components/sidebar/multiselect_dropdown.vue';
-
-describe('MultiSelectDropdown Component', () => {
- it('renders items slot', () => {
- const wrapper = shallowMount(MultiSelectDropdown, {
- propsData: {
- text: '',
- headerText: '',
- },
- slots: {
- items: '<p>Test</p>',
- },
- });
- expect(getByText(wrapper.element, 'Test')).toBeDefined();
- });
-
- it('renders search slot', () => {
- const wrapper = shallowMount(MultiSelectDropdown, {
- propsData: {
- text: '',
- headerText: '',
- },
- slots: {
- search: '<p>Search</p>',
- },
- stubs: {
- GlDropdown,
- },
- });
- expect(getByText(wrapper.element, 'Search')).toBeDefined();
- });
-});
diff --git a/spec/frontend/vue_shared/components/namespace_select/mock_data.js b/spec/frontend/vue_shared/components/namespace_select/mock_data.js
index c9d96672e85..cfd521c67cb 100644
--- a/spec/frontend/vue_shared/components/namespace_select/mock_data.js
+++ b/spec/frontend/vue_shared/components/namespace_select/mock_data.js
@@ -1,11 +1,6 @@
-export const group = [
+export const groupNamespaces = [
{ id: 1, name: 'Group 1', humanName: 'Group 1' },
{ id: 2, name: 'Subgroup 1', humanName: 'Group 1 / Subgroup 1' },
];
-export const user = [{ id: 3, name: 'User namespace 1', humanName: 'User namespace 1' }];
-
-export const namespaces = {
- group,
- user,
-};
+export const userNamespaces = [{ id: 3, name: 'User namespace 1', humanName: 'User namespace 1' }];
diff --git a/spec/frontend/vue_shared/components/namespace_select/namespace_select_spec.js b/spec/frontend/vue_shared/components/namespace_select/namespace_select_spec.js
index 8f07f63993d..c11b20a692e 100644
--- a/spec/frontend/vue_shared/components/namespace_select/namespace_select_spec.js
+++ b/spec/frontend/vue_shared/components/namespace_select/namespace_select_spec.js
@@ -1,9 +1,15 @@
-import { GlDropdown, GlDropdownItem, GlDropdownSectionHeader } from '@gitlab/ui';
+import { nextTick } from 'vue';
+import { GlDropdown, GlDropdownItem, GlDropdownSectionHeader, GlSearchBoxByType } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import NamespaceSelect, {
i18n,
+ EMPTY_NAMESPACE_ID,
} from '~/vue_shared/components/namespace_select/namespace_select.vue';
-import { user, group, namespaces } from './mock_data';
+import { userNamespaces, groupNamespaces } from './mock_data';
+
+const FLAT_NAMESPACES = [...groupNamespaces, ...userNamespaces];
+const EMPTY_NAMESPACE_TITLE = 'Empty namespace TEST';
+const EMPTY_NAMESPACE_ITEM = { id: EMPTY_NAMESPACE_ID, humanName: EMPTY_NAMESPACE_TITLE };
describe('Namespace Select', () => {
let wrapper;
@@ -11,71 +17,115 @@ describe('Namespace Select', () => {
const createComponent = (props = {}) =>
shallowMountExtended(NamespaceSelect, {
propsData: {
- data: namespaces,
+ userNamespaces,
+ groupNamespaces,
...props,
},
+ stubs: {
+ // We have to "full" mount GlDropdown so that slot children will render
+ GlDropdown,
+ },
});
const wrappersText = (arr) => arr.wrappers.map((w) => w.text());
- const flatNamespaces = () => [...group, ...user];
const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findDropdownAttributes = (attr) => findDropdown().attributes(attr);
- const selectedDropdownItemText = () => findDropdownAttributes('text');
+ const findDropdownText = () => findDropdown().props('text');
const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
+ const findDropdownItemsTexts = () => findDropdownItems().wrappers.map((x) => x.text());
const findSectionHeaders = () => wrapper.findAllComponents(GlDropdownSectionHeader);
-
- beforeEach(() => {
- wrapper = createComponent();
- });
+ const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
+ const search = (term) => findSearchBox().vm.$emit('input', term);
afterEach(() => {
wrapper.destroy();
});
- it('renders the dropdown', () => {
- expect(findDropdown().exists()).toBe(true);
- });
+ describe('default', () => {
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
- it('renders each dropdown item', () => {
- const items = findDropdownItems().wrappers;
- expect(items).toHaveLength(flatNamespaces().length);
- });
+ it('renders the dropdown', () => {
+ expect(findDropdown().exists()).toBe(true);
+ });
- it('renders the human name for each item', () => {
- const dropdownItems = wrappersText(findDropdownItems());
- const flatNames = flatNamespaces().map(({ humanName }) => humanName);
- expect(dropdownItems).toEqual(flatNames);
- });
+ it('renders each dropdown item', () => {
+ expect(findDropdownItemsTexts()).toEqual(FLAT_NAMESPACES.map((x) => x.humanName));
+ });
+
+ it('renders default dropdown text', () => {
+ expect(findDropdownText()).toBe(i18n.DEFAULT_TEXT);
+ });
+
+ it('splits group and user namespaces', () => {
+ const headers = findSectionHeaders();
+ expect(wrappersText(headers)).toEqual([i18n.GROUPS, i18n.USERS]);
+ });
- it('sets the initial dropdown text', () => {
- expect(selectedDropdownItemText()).toBe(i18n.DEFAULT_TEXT);
+ it('does not render wrapper as full width', () => {
+ expect(findDropdown().attributes('block')).toBeUndefined();
+ });
});
- it('splits group and user namespaces', () => {
- const headers = findSectionHeaders();
- expect(headers).toHaveLength(2);
- expect(wrappersText(headers)).toEqual([i18n.GROUPS, i18n.USERS]);
+ it('with defaultText, it overrides dropdown text', () => {
+ const textOverride = 'Select an option';
+
+ wrapper = createComponent({ defaultText: textOverride });
+
+ expect(findDropdownText()).toBe(textOverride);
});
- it('sets the dropdown to full width', () => {
- expect(findDropdownAttributes('block')).toBeUndefined();
+ it('with includeHeaders=false, hides group/user headers', () => {
+ wrapper = createComponent({ includeHeaders: false });
+
+ expect(findSectionHeaders()).toHaveLength(0);
+ });
+ it('with fullWidth=true, sets the dropdown to full width', () => {
wrapper = createComponent({ fullWidth: true });
- expect(findDropdownAttributes('block')).not.toBeUndefined();
- expect(findDropdownAttributes('block')).toBe('true');
+ expect(findDropdown().attributes('block')).toBe('true');
+ });
+
+ describe('with search', () => {
+ it.each`
+ term | includeEmptyNamespace | expectedItems
+ ${''} | ${false} | ${[...groupNamespaces, ...userNamespaces]}
+ ${'sub'} | ${false} | ${[groupNamespaces[1]]}
+ ${'User'} | ${false} | ${[...userNamespaces]}
+ ${'User'} | ${true} | ${[...userNamespaces]}
+ ${'namespace'} | ${true} | ${[EMPTY_NAMESPACE_ITEM, ...userNamespaces]}
+ `(
+ 'with term=$term and includeEmptyNamespace=$includeEmptyNamespace, should show $expectedItems.length',
+ async ({ term, includeEmptyNamespace, expectedItems }) => {
+ wrapper = createComponent({
+ includeEmptyNamespace,
+ emptyNamespaceTitle: EMPTY_NAMESPACE_TITLE,
+ });
+
+ search(term);
+
+ await nextTick();
+
+ const expected = expectedItems.map((x) => x.humanName);
+
+ expect(findDropdownItemsTexts()).toEqual(expected);
+ },
+ );
});
describe('with a selected namespace', () => {
const selectedGroupIndex = 1;
- const selectedItem = group[selectedGroupIndex];
+ const selectedItem = groupNamespaces[selectedGroupIndex];
beforeEach(() => {
+ wrapper = createComponent();
+
findDropdownItems().at(selectedGroupIndex).vm.$emit('click');
});
it('sets the dropdown text', () => {
- expect(selectedDropdownItemText()).toBe(selectedItem.humanName);
+ expect(findDropdownText()).toBe(selectedItem.humanName);
});
it('emits the `select` event when a namespace is selected', () => {
@@ -83,4 +133,37 @@ describe('Namespace Select', () => {
expect(wrapper.emitted('select')).toEqual([args]);
});
});
+
+ describe('with an empty namespace option', () => {
+ beforeEach(() => {
+ wrapper = createComponent({
+ includeEmptyNamespace: true,
+ emptyNamespaceTitle: EMPTY_NAMESPACE_TITLE,
+ });
+ });
+
+ it('includes the empty namespace', () => {
+ const first = findDropdownItems().at(0);
+
+ expect(first.text()).toBe(EMPTY_NAMESPACE_TITLE);
+ });
+
+ it('emits the `select` event when a namespace is selected', () => {
+ findDropdownItems().at(0).vm.$emit('click');
+
+ expect(wrapper.emitted('select')).toEqual([[EMPTY_NAMESPACE_ITEM]]);
+ });
+
+ it.each`
+ desc | term | shouldShow
+ ${'should hide empty option'} | ${'group'} | ${false}
+ ${'should show empty option'} | ${'Empty'} | ${true}
+ `('when search for $term, $desc', async ({ term, shouldShow }) => {
+ search(term);
+
+ await nextTick();
+
+ expect(findDropdownItemsTexts().includes(EMPTY_NAMESPACE_TITLE)).toBe(shouldShow);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js b/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js
index 835759b1f20..accbf14572d 100644
--- a/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js
+++ b/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js
@@ -1,5 +1,6 @@
import { GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import NoteableWarning from '~/vue_shared/components/notes/noteable_warning.vue';
describe('Issue Warning Component', () => {
@@ -64,7 +65,7 @@ describe('Issue Warning Component', () => {
expect(findConfidentialBlock().exists()).toBe(true);
expect(findConfidentialBlock().element).toMatchSnapshot();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findConfidentialBlock(wrapper).text()).toContain('This is a confidential issue.');
});
@@ -154,15 +155,15 @@ describe('Issue Warning Component', () => {
noteableType: 'Epic',
});
- await wrapperLocked.vm.$nextTick();
+ await nextTick();
expect(findLockedBlock(wrapperLocked).text()).toContain('This epic is locked.');
- await wrapperConfidential.vm.$nextTick();
+ await nextTick();
expect(findConfidentialBlock(wrapperConfidential).text()).toContain(
'This is a confidential epic.',
);
- await wrapperLockedAndConfidential.vm.$nextTick();
+ await nextTick();
expect(findLockedAndConfidentialBlock(wrapperLockedAndConfidential).text()).toContain(
'This epic is confidential and locked.',
);
@@ -179,15 +180,15 @@ describe('Issue Warning Component', () => {
noteableType: 'MergeRequest',
});
- await wrapperLocked.vm.$nextTick();
+ await nextTick();
expect(findLockedBlock(wrapperLocked).text()).toContain('This merge request is locked.');
- await wrapperConfidential.vm.$nextTick();
+ await nextTick();
expect(findConfidentialBlock(wrapperConfidential).text()).toContain(
'This is a confidential merge request.',
);
- await wrapperLockedAndConfidential.vm.$nextTick();
+ await nextTick();
expect(findLockedAndConfidentialBlock(wrapperLockedAndConfidential).text()).toContain(
'This merge request is confidential and locked.',
);
diff --git a/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js b/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js
index b330b4f5657..36050a42da7 100644
--- a/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js
+++ b/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js
@@ -1,5 +1,6 @@
import { GlAlert, GlBadge, GlPagination, GlTabs, GlTab } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import Tracking from '~/tracking';
import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
@@ -219,21 +220,21 @@ describe('AlertManagementEmptyState', () => {
it('returns prevPage button', async () => {
findPagination().vm.$emit('input', 3);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findPagination().findAll('.page-item').at(0).text()).toBe('Prev');
});
it('returns prevPage number', async () => {
findPagination().vm.$emit('input', 3);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.vm.previousPage).toBe(2);
});
it('returns 0 when it is the first page', async () => {
findPagination().vm.$emit('input', 1);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.vm.previousPage).toBe(0);
});
});
@@ -242,7 +243,7 @@ describe('AlertManagementEmptyState', () => {
it('returns nextPage button', async () => {
findPagination().vm.$emit('input', 3);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findPagination().findAll('.page-item').at(1).text()).toBe('Next');
});
@@ -257,14 +258,14 @@ describe('AlertManagementEmptyState', () => {
});
findPagination().vm.$emit('input', 1);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.vm.nextPage).toBe(2);
});
it('returns `null` when currentPage is already last page', async () => {
findStatusTabs().vm.$emit('input', 1);
findPagination().vm.$emit('input', 1);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.vm.nextPage).toBeNull();
});
});
@@ -319,7 +320,7 @@ describe('AlertManagementEmptyState', () => {
searchTerm,
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.vm.filteredSearchValue).toEqual([searchTerm]);
});
diff --git a/spec/frontend/vue_shared/components/pikaday_spec.js b/spec/frontend/vue_shared/components/pikaday_spec.js
deleted file mode 100644
index fed4ce5e696..00000000000
--- a/spec/frontend/vue_shared/components/pikaday_spec.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import { GlDatepicker } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import datePicker from '~/vue_shared/components/pikaday.vue';
-
-describe('datePicker', () => {
- let wrapper;
-
- const buildWrapper = (propsData = {}) => {
- wrapper = shallowMount(datePicker, {
- propsData,
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
- it('should emit newDateSelected when GlDatePicker emits the input event', () => {
- const minDate = new Date();
- const maxDate = new Date();
- const selectedDate = new Date();
- const theDate = selectedDate.toISOString().slice(0, 10);
-
- buildWrapper({ minDate, maxDate, selectedDate });
-
- expect(wrapper.find(GlDatepicker).props()).toMatchObject({
- minDate,
- maxDate,
- value: selectedDate,
- });
- wrapper.find(GlDatepicker).vm.$emit('input', selectedDate);
- expect(wrapper.emitted('newDateSelected')[0][0]).toBe(theDate);
- });
- it('should emit the hidePicker event when GlDatePicker emits the close event', () => {
- buildWrapper();
-
- wrapper.find(GlDatepicker).vm.$emit('close');
-
- expect(wrapper.emitted('hidePicker')).toHaveLength(1);
- });
-});
diff --git a/spec/frontend/vue_shared/components/project_avatar/default_spec.js b/spec/frontend/vue_shared/components/project_avatar/default_spec.js
index 84dad2374cb..d042db6051c 100644
--- a/spec/frontend/vue_shared/components/project_avatar/default_spec.js
+++ b/spec/frontend/vue_shared/components/project_avatar/default_spec.js
@@ -1,4 +1,4 @@
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import { projectData } from 'jest/ide/mock_data';
import { TEST_HOST } from 'spec/test_constants';
@@ -19,7 +19,7 @@ describe('ProjectAvatarDefault component', () => {
vm.$destroy();
});
- it('renders identicon if project has no avatar_url', (done) => {
+ it('renders identicon if project has no avatar_url', async () => {
const expectedText = getFirstCharacterCapitalized(projectData.name);
vm.project = {
@@ -27,18 +27,14 @@ describe('ProjectAvatarDefault component', () => {
avatar_url: null,
};
- vm.$nextTick()
- .then(() => {
- const identiconEl = vm.$el.querySelector('.identicon');
+ await nextTick();
+ const identiconEl = vm.$el.querySelector('.identicon');
- expect(identiconEl).not.toBe(null);
- expect(identiconEl.textContent.trim()).toEqual(expectedText);
- })
- .then(done)
- .catch(done.fail);
+ expect(identiconEl).not.toBe(null);
+ expect(identiconEl.textContent.trim()).toEqual(expectedText);
});
- it('renders avatar image if project has avatar_url', (done) => {
+ it('renders avatar image if project has avatar_url', async () => {
const avatarUrl = `${TEST_HOST}/images/home/nasa.svg`;
vm.project = {
@@ -46,13 +42,9 @@ describe('ProjectAvatarDefault component', () => {
avatar_url: avatarUrl,
};
- vm.$nextTick()
- .then(() => {
- expect(vm.$el.querySelector('.avatar')).not.toBeNull();
- expect(vm.$el.querySelector('.identicon')).toBeNull();
- expect(vm.$el.querySelector('img')).toHaveAttr('src', avatarUrl);
- })
- .then(done)
- .catch(done.fail);
+ await nextTick();
+ expect(vm.$el.querySelector('.avatar')).not.toBeNull();
+ expect(vm.$el.querySelector('.identicon')).toBeNull();
+ expect(vm.$el.querySelector('img')).toHaveAttr('src', avatarUrl);
});
});
diff --git a/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js b/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js
index 34cee10392d..379e60c1b2d 100644
--- a/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js
+++ b/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js
@@ -1,7 +1,7 @@
import { GlSearchBoxByType, GlInfiniteScroll } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { head } from 'lodash';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import mockProjects from 'test_fixtures_static/projects.json';
import { trimText } from 'helpers/text_helper';
import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue';
@@ -77,39 +77,36 @@ describe('ProjectSelector component', () => {
expect(vm.$emit).toHaveBeenCalledWith('projectClicked', head(searchResults));
});
- it(`shows a "no results" message if showNoResultsMessage === true`, () => {
+ it(`shows a "no results" message if showNoResultsMessage === true`, async () => {
wrapper.setProps({ showNoResultsMessage: true });
- return vm.$nextTick().then(() => {
- const noResultsEl = wrapper.find('.js-no-results-message');
+ await nextTick();
+ const noResultsEl = wrapper.find('.js-no-results-message');
- expect(noResultsEl.exists()).toBe(true);
- expect(trimText(noResultsEl.text())).toEqual('Sorry, no projects matched your search');
- });
+ expect(noResultsEl.exists()).toBe(true);
+ expect(trimText(noResultsEl.text())).toEqual('Sorry, no projects matched your search');
});
- it(`shows a "minimum search query" message if showMinimumSearchQueryMessage === true`, () => {
+ it(`shows a "minimum search query" message if showMinimumSearchQueryMessage === true`, async () => {
wrapper.setProps({ showMinimumSearchQueryMessage: true });
- return vm.$nextTick().then(() => {
- const minimumSearchEl = wrapper.find('.js-minimum-search-query-message');
+ await nextTick();
+ const minimumSearchEl = wrapper.find('.js-minimum-search-query-message');
- expect(minimumSearchEl.exists()).toBe(true);
- expect(trimText(minimumSearchEl.text())).toEqual('Enter at least three characters to search');
- });
+ expect(minimumSearchEl.exists()).toBe(true);
+ expect(trimText(minimumSearchEl.text())).toEqual('Enter at least three characters to search');
});
- it(`shows a error message if showSearchErrorMessage === true`, () => {
+ it(`shows a error message if showSearchErrorMessage === true`, async () => {
wrapper.setProps({ showSearchErrorMessage: true });
- return vm.$nextTick().then(() => {
- const errorMessageEl = wrapper.find('.js-search-error-message');
+ await nextTick();
+ const errorMessageEl = wrapper.find('.js-search-error-message');
- expect(errorMessageEl.exists()).toBe(true);
- expect(trimText(errorMessageEl.text())).toEqual(
- 'Something went wrong, unable to search projects',
- );
- });
+ expect(errorMessageEl.exists()).toBe(true);
+ expect(trimText(errorMessageEl.text())).toEqual(
+ 'Something went wrong, unable to search projects',
+ );
});
describe('the search results legend', () => {
@@ -121,7 +118,7 @@ describe('ProjectSelector component', () => {
${2} | ${3} | ${'Showing 2 of 3 projects'}
`(
'is "$expected" given $count results are showing out of $total',
- ({ count, total, expected }) => {
+ async ({ count, total, expected }) => {
search('gitlab ui');
wrapper.setProps({
@@ -129,9 +126,8 @@ describe('ProjectSelector component', () => {
totalResults: total,
});
- return wrapper.vm.$nextTick().then(() => {
- expect(findLegendText()).toBe(expected);
- });
+ await nextTick();
+ expect(findLegendText()).toBe(expected);
},
);
diff --git a/spec/frontend/vue_shared/components/registry/list_item_spec.js b/spec/frontend/vue_shared/components/registry/list_item_spec.js
index ca4bf0b0652..1b93292e37b 100644
--- a/spec/frontend/vue_shared/components/registry/list_item_spec.js
+++ b/spec/frontend/vue_shared/components/registry/list_item_spec.js
@@ -1,5 +1,6 @@
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import component from '~/vue_shared/components/registry/list_item.vue';
describe('list item', () => {
@@ -70,10 +71,10 @@ describe('list item', () => {
it('are visible when details is shown', async () => {
mountComponent({}, slotMocks);
- await wrapper.vm.$nextTick();
+ await nextTick();
findToggleDetailsButton().vm.$emit('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
slotNames.forEach((name) => {
expect(findDetailsSlot(name).exists()).toBe(true);
});
@@ -90,7 +91,7 @@ describe('list item', () => {
describe('details toggle button', () => {
it('is visible when at least one details slot exists', async () => {
mountComponent({}, { 'details-foo': '<span></span>' });
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findToggleDetailsButton().exists()).toBe(true);
});
diff --git a/spec/frontend/vue_shared/components/resizable_chart/resizable_chart_container_spec.js b/spec/frontend/vue_shared/components/resizable_chart/resizable_chart_container_spec.js
index 40f0c0f29f2..7536df24ac6 100644
--- a/spec/frontend/vue_shared/components/resizable_chart/resizable_chart_container_spec.js
+++ b/spec/frontend/vue_shared/components/resizable_chart/resizable_chart_container_spec.js
@@ -1,6 +1,6 @@
import { mount } from '@vue/test-utils';
import $ from 'jquery';
-import Vue from 'vue';
+import { nextTick } from 'vue';
import ResizableChartContainer from '~/vue_shared/components/resizable_chart/resizable_chart_container.vue';
jest.mock('~/lib/utils/common_utils', () => ({
@@ -35,7 +35,7 @@ describe('Resizable Chart Container', () => {
expect(wrapper.element).toMatchSnapshot();
});
- it('updates the slot width and height props', () => {
+ it('updates the slot width and height props', async () => {
const width = 1920;
const height = 1080;
@@ -44,13 +44,12 @@ describe('Resizable Chart Container', () => {
$(document).trigger('content.resize');
- return Vue.nextTick().then(() => {
- const widthNode = wrapper.find('.slot > .width');
- const heightNode = wrapper.find('.slot > .height');
+ await nextTick();
+ const widthNode = wrapper.find('.slot > .width');
+ const heightNode = wrapper.find('.slot > .height');
- expect(parseInt(widthNode.text(), 10)).toEqual(width);
- expect(parseInt(heightNode.text(), 10)).toEqual(height);
- });
+ expect(parseInt(widthNode.text(), 10)).toEqual(width);
+ expect(parseInt(heightNode.text(), 10)).toEqual(height);
});
it('calls onResize on manual resize', () => {
diff --git a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js
index e74a867ec97..0da9939e97f 100644
--- a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js
+++ b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js
@@ -77,8 +77,7 @@ describe('RunnerInstructionsModal component', () => {
runnerSetupInstructionsHandler = jest.fn().mockResolvedValue(mockGraphqlInstructions);
createComponent();
-
- await nextTick();
+ await waitForPromises();
});
afterEach(() => {
@@ -113,13 +112,15 @@ describe('RunnerInstructionsModal component', () => {
});
});
- it('binary instructions are shown', () => {
+ it('binary instructions are shown', async () => {
+ await waitForPromises();
const instructions = findBinaryInstructions().text();
expect(instructions).toBe(installInstructions);
});
- it('register command is shown with a replaced token', () => {
+ it('register command is shown with a replaced token', async () => {
+ await waitForPromises();
const instructions = findRegisterCommand().text();
expect(instructions).toBe(
@@ -130,7 +131,7 @@ describe('RunnerInstructionsModal component', () => {
describe('when a register token is not shown', () => {
beforeEach(async () => {
createComponent({ props: { registrationToken: undefined } });
- await nextTick();
+ await waitForPromises();
});
it('register command is shown without a defined registration token', () => {
@@ -198,16 +199,17 @@ describe('RunnerInstructionsModal component', () => {
expect(findSkeletonLoader().exists()).toBe(true);
expect(findGlLoadingIcon().exists()).toBe(false);
- await nextTick(); // wait for platforms
+ await nextTick();
+ jest.runOnlyPendingTimers();
+ await nextTick();
+ await nextTick();
expect(findGlLoadingIcon().exists()).toBe(true);
});
it('once loaded, should not show a loading state', async () => {
createComponent();
-
- await nextTick(); // wait for platforms
- await nextTick(); // wait for architectures
+ await waitForPromises();
expect(findSkeletonLoader().exists()).toBe(false);
expect(findGlLoadingIcon().exists()).toBe(false);
diff --git a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js
index 23f8d6afcb5..9a95a838291 100644
--- a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js
+++ b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js
@@ -22,9 +22,9 @@ describe('RunnerInstructions component', () => {
wrapper.destroy();
});
- it('should show the "Show Runner installation instructions" button', () => {
+ it('should show the "Show runner installation instructions" button', () => {
expect(findModalButton().exists()).toBe(true);
- expect(findModalButton().text()).toBe('Show Runner installation instructions');
+ expect(findModalButton().text()).toBe('Show runner installation instructions');
});
it('should not render the modal once mounted', () => {
diff --git a/spec/frontend/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js b/spec/frontend/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js
deleted file mode 100644
index 79e41ed0c9e..00000000000
--- a/spec/frontend/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js
+++ /dev/null
@@ -1,68 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { GlIcon } from '@gitlab/ui';
-import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-
-import CollapsedCalendarIcon from '~/vue_shared/components/sidebar/collapsed_calendar_icon.vue';
-
-describe('CollapsedCalendarIcon', () => {
- let wrapper;
-
- const defaultProps = {
- containerClass: 'test-class',
- text: 'text',
- tooltipText: 'tooltip text',
- showIcon: false,
- };
-
- const createComponent = ({ props = {} } = {}) => {
- wrapper = shallowMount(CollapsedCalendarIcon, {
- propsData: { ...defaultProps, ...props },
- directives: {
- GlTooltip: createMockDirective(),
- },
- });
- };
-
- beforeEach(() => {
- createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- const findGlIcon = () => wrapper.findComponent(GlIcon);
- const getTooltip = () => getBinding(wrapper.element, 'gl-tooltip');
-
- it('adds class to container', () => {
- expect(wrapper.classes()).toContain(defaultProps.containerClass);
- });
-
- it('does not render calendar icon when showIcon is false', () => {
- expect(findGlIcon().exists()).toBe(false);
- });
-
- it('renders calendar icon when showIcon is true', () => {
- createComponent({
- props: { showIcon: true },
- });
-
- expect(findGlIcon().exists()).toBe(true);
- });
-
- it('renders text', () => {
- expect(wrapper.text()).toBe(defaultProps.text);
- });
-
- it('renders tooltipText as tooltip', () => {
- expect(getTooltip().value).toBe(defaultProps.tooltipText);
- });
-
- it('emits click event when container is clicked', async () => {
- wrapper.trigger('click');
-
- await wrapper.vm.$nextTick();
-
- expect(wrapper.emitted('click')[0]).toBeDefined();
- });
-});
diff --git a/spec/frontend/vue_shared/components/sidebar/date_picker_spec.js b/spec/frontend/vue_shared/components/sidebar/date_picker_spec.js
deleted file mode 100644
index 263d1e9d947..00000000000
--- a/spec/frontend/vue_shared/components/sidebar/date_picker_spec.js
+++ /dev/null
@@ -1,125 +0,0 @@
-import { GlLoadingIcon } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
-import DatePicker from '~/vue_shared/components/pikaday.vue';
-import SidebarDatePicker from '~/vue_shared/components/sidebar/date_picker.vue';
-
-describe('SidebarDatePicker', () => {
- let wrapper;
-
- const createComponent = (propsData = {}, data = {}) => {
- wrapper = mount(SidebarDatePicker, {
- propsData,
- data: () => data,
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- const findDatePicker = () => wrapper.findComponent(DatePicker);
- const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findEditButton = () => wrapper.find('.title .btn-blank');
- const findRemoveButton = () => wrapper.find('.value-content .btn-blank');
- const findSidebarToggle = () => wrapper.find('.title .gutter-toggle');
- const findValueContent = () => wrapper.find('.value-content');
-
- it('should emit toggleCollapse when collapsed toggle sidebar is clicked', () => {
- createComponent();
-
- wrapper.find('.issuable-sidebar-header .gutter-toggle').trigger('click');
-
- expect(wrapper.emitted('toggleCollapse')).toEqual([[]]);
- });
-
- it('should render collapsed-calendar-icon', () => {
- createComponent();
-
- expect(wrapper.find('.sidebar-collapsed-icon').exists()).toBe(true);
- });
-
- it('should render value when not editing', () => {
- createComponent();
-
- expect(findValueContent().exists()).toBe(true);
- });
-
- it('should render None if there is no selectedDate', () => {
- createComponent();
-
- expect(findValueContent().text()).toBe('None');
- });
-
- it('should render date-picker when editing', () => {
- createComponent({}, { editing: true });
-
- expect(findDatePicker().exists()).toBe(true);
- });
-
- it('should render label', () => {
- const label = 'label';
- createComponent({ label });
- expect(wrapper.find('.title').text()).toBe(label);
- });
-
- it('should render loading-icon when isLoading', () => {
- createComponent({ isLoading: true });
- expect(findLoadingIcon().exists()).toBe(true);
- });
-
- describe('editable', () => {
- beforeEach(() => {
- createComponent({ editable: true });
- });
-
- it('should render edit button', () => {
- expect(findEditButton().text()).toBe('Edit');
- });
-
- it('should enable editing when edit button is clicked', async () => {
- findEditButton().trigger('click');
-
- await wrapper.vm.$nextTick();
-
- expect(wrapper.vm.editing).toBe(true);
- });
- });
-
- it('should render date if selectedDate', () => {
- createComponent({ selectedDate: new Date('07/07/2017') });
-
- expect(wrapper.find('.value-content strong').text()).toBe('Jul 7, 2017');
- });
-
- describe('selectedDate and editable', () => {
- beforeEach(() => {
- createComponent({ selectedDate: new Date('07/07/2017'), editable: true });
- });
-
- it('should render remove button if selectedDate and editable', () => {
- expect(findRemoveButton().text()).toBe('remove');
- });
-
- it('should emit saveDate with null when remove button is clicked', () => {
- findRemoveButton().trigger('click');
-
- expect(wrapper.emitted('saveDate')).toEqual([[null]]);
- });
- });
-
- describe('showToggleSidebar', () => {
- beforeEach(() => {
- createComponent({ showToggleSidebar: true });
- });
-
- it('should render toggle-sidebar when showToggleSidebar', () => {
- expect(findSidebarToggle().exists()).toBe(true);
- });
-
- it('should emit toggleCollapse when toggle sidebar is clicked', () => {
- findSidebarToggle().trigger('click');
-
- expect(wrapper.emitted('toggleCollapse')).toEqual([[]]);
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js b/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js
index 5336ecc614c..f213e37cbc1 100644
--- a/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js
@@ -10,6 +10,7 @@ import {
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+import { nextTick } from 'vue';
import axios from '~/lib/utils/axios_utils';
import IssuableMoveDropdown from '~/vue_shared/components/sidebar/issuable_move_dropdown.vue';
@@ -74,7 +75,7 @@ describe('IssuableMoveDropdown', () => {
searchKey: 'foo',
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.vm.fetchProjects).toHaveBeenCalledWith('foo');
});
@@ -151,7 +152,7 @@ describe('IssuableMoveDropdown', () => {
selectedProject,
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.vm.isSelectedProject(project)).toBe(returnValue);
},
@@ -164,7 +165,7 @@ describe('IssuableMoveDropdown', () => {
selectedProject: null,
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.vm.isSelectedProject(mockProjects[0])).toBe(false);
});
@@ -218,7 +219,7 @@ describe('IssuableMoveDropdown', () => {
projectsListLoading: true,
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findDropdownEl().find(GlLoadingIcon).exists()).toBe(true);
});
@@ -231,7 +232,7 @@ describe('IssuableMoveDropdown', () => {
selectedProject: mockProjects[0],
});
- await wrapper.vm.$nextTick();
+ await nextTick();
const dropdownItems = wrapper.findAll(GlDropdownItem);
@@ -251,7 +252,7 @@ describe('IssuableMoveDropdown', () => {
});
// Wait for `searchKey` watcher to run.
- await wrapper.vm.$nextTick();
+ await nextTick();
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
@@ -260,7 +261,7 @@ describe('IssuableMoveDropdown', () => {
projectsListLoading: false,
});
- await wrapper.vm.$nextTick();
+ await nextTick();
const dropdownContentEl = wrapper.find('[data-testid="content"]');
@@ -276,7 +277,7 @@ describe('IssuableMoveDropdown', () => {
projectsListLoadFailed: true,
});
- await wrapper.vm.$nextTick();
+ await nextTick();
const dropdownContentEl = wrapper.find('[data-testid="content"]');
@@ -295,7 +296,7 @@ describe('IssuableMoveDropdown', () => {
selectedProject: mockProjects[0],
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(
wrapper.find('[data-testid="footer"]').find(GlButton).attributes('disabled'),
@@ -352,7 +353,7 @@ describe('IssuableMoveDropdown', () => {
projects: mockProjects,
});
- await wrapper.vm.$nextTick();
+ await nextTick();
wrapper.findAll(GlDropdownItem).at(0).vm.$emit('click', mockEvent);
@@ -366,7 +367,7 @@ describe('IssuableMoveDropdown', () => {
selectedProject: mockProjects[0],
});
- await wrapper.vm.$nextTick();
+ await nextTick();
wrapper.find('[data-testid="footer"]').find(GlButton).vm.$emit('click');
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js
index c4ed975e746..c05513a6d5f 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js
@@ -1,6 +1,6 @@
import { GlIcon, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import DropdownButton from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue';
@@ -71,13 +71,12 @@ describe('DropdownButton', () => {
expect(dropdownTextEl.text()).toBe('Label');
});
- it('renders provided button text element', () => {
+ it('renders provided button text element', async () => {
store.state.dropdownButtonText = 'Custom label';
const dropdownTextEl = findDropdownText();
- return wrapper.vm.$nextTick().then(() => {
- expect(dropdownTextEl.text()).toBe('Custom label');
- });
+ await nextTick();
+ expect(dropdownTextEl.text()).toBe('Custom label');
});
it('renders chevron icon element', () => {
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js
index 0eff6a1dace..0673ffee22b 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js
@@ -1,6 +1,6 @@
import { GlButton, GlFormInput, GlLink, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import DropdownContentsCreateView from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue';
@@ -42,7 +42,7 @@ describe('DropdownContentsCreateView', () => {
expect(wrapper.vm.disableCreate).toBe(true);
});
- it('returns `true` when `labelCreateInProgress` is true', () => {
+ it('returns `true` when `labelCreateInProgress` is true', async () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({
@@ -51,12 +51,11 @@ describe('DropdownContentsCreateView', () => {
});
wrapper.vm.$store.dispatch('requestCreateLabel');
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.vm.disableCreate).toBe(true);
- });
+ await nextTick();
+ expect(wrapper.vm.disableCreate).toBe(true);
});
- it('returns `false` when label title and color is defined and create request is not already in progress', () => {
+ it('returns `false` when label title and color is defined and create request is not already in progress', async () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({
@@ -64,9 +63,8 @@ describe('DropdownContentsCreateView', () => {
selectedColor: '#ff0000',
});
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.vm.disableCreate).toBe(false);
- });
+ await nextTick();
+ expect(wrapper.vm.disableCreate).toBe(false);
});
});
@@ -101,7 +99,7 @@ describe('DropdownContentsCreateView', () => {
});
describe('handleCreateClick', () => {
- it('calls action `createLabel` with object containing `labelTitle` & `selectedColor`', () => {
+ it('calls action `createLabel` with object containing `labelTitle` & `selectedColor`', async () => {
jest.spyOn(wrapper.vm, 'createLabel').mockImplementation();
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
@@ -112,14 +110,13 @@ describe('DropdownContentsCreateView', () => {
wrapper.vm.handleCreateClick();
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.vm.createLabel).toHaveBeenCalledWith(
- expect.objectContaining({
- title: 'Foo',
- color: '#ff0000',
- }),
- );
- });
+ await nextTick();
+ expect(wrapper.vm.createLabel).toHaveBeenCalledWith(
+ expect.objectContaining({
+ title: 'Foo',
+ color: '#ff0000',
+ }),
+ );
});
});
});
@@ -169,25 +166,22 @@ describe('DropdownContentsCreateView', () => {
});
});
- it('renders color input element', () => {
+ it('renders color input element', async () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({
selectedColor: '#ff0000',
});
- return wrapper.vm.$nextTick(() => {
- const colorPreviewEl = wrapper.find(
- '.color-input-container > .dropdown-label-color-preview',
- );
- const colorInputEl = wrapper.find('.color-input-container').find(GlFormInput);
+ await nextTick();
+ const colorPreviewEl = wrapper.find('.color-input-container > .dropdown-label-color-preview');
+ const colorInputEl = wrapper.find('.color-input-container').find(GlFormInput);
- expect(colorPreviewEl.exists()).toBe(true);
- expect(colorPreviewEl.attributes('style')).toContain('background-color');
- expect(colorInputEl.exists()).toBe(true);
- expect(colorInputEl.attributes('placeholder')).toBe('Use custom color #FF0000');
- expect(colorInputEl.attributes('value')).toBe('#ff0000');
- });
+ expect(colorPreviewEl.exists()).toBe(true);
+ expect(colorPreviewEl.attributes('style')).toContain('background-color');
+ expect(colorInputEl.exists()).toBe(true);
+ expect(colorInputEl.attributes('placeholder')).toBe('Use custom color #FF0000');
+ expect(colorInputEl.attributes('value')).toBe('#ff0000');
});
it('renders create button element', () => {
@@ -197,15 +191,14 @@ describe('DropdownContentsCreateView', () => {
expect(createBtnEl.text()).toContain('Create');
});
- it('shows gl-loading-icon within create button element when `labelCreateInProgress` is `true`', () => {
+ it('shows gl-loading-icon within create button element when `labelCreateInProgress` is `true`', async () => {
wrapper.vm.$store.dispatch('requestCreateLabel');
- return wrapper.vm.$nextTick(() => {
- const loadingIconEl = wrapper.find('.dropdown-actions').find(GlLoadingIcon);
+ await nextTick();
+ const loadingIconEl = wrapper.find('.dropdown-actions').find(GlLoadingIcon);
- expect(loadingIconEl.exists()).toBe(true);
- expect(loadingIconEl.isVisible()).toBe(true);
- });
+ expect(loadingIconEl.exists()).toBe(true);
+ expect(loadingIconEl.isVisible()).toBe(true);
});
it('renders cancel button element', () => {
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js
index 93a0e2f75bb..42202db4935 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js
@@ -6,7 +6,7 @@ import {
GlLink,
} from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
import DropdownContentsLabelsView from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue';
@@ -114,7 +114,7 @@ describe('DropdownContentsLabelsView', () => {
wrapper.vm.$store.dispatch('receiveLabelsSuccess', labels);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.vm.showNoMatchingResultsMessage).toBe(returnValue);
},
@@ -249,7 +249,7 @@ describe('DropdownContentsLabelsView', () => {
expect(wrapper.vm.toggleDropdownContents).toHaveBeenCalled();
});
- it('calls action `scrollIntoViewIfNeeded` in next tick when any key is pressed', () => {
+ it('calls action `scrollIntoViewIfNeeded` in next tick when any key is pressed', async () => {
jest.spyOn(wrapper.vm, 'scrollIntoViewIfNeeded').mockImplementation();
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
@@ -261,9 +261,8 @@ describe('DropdownContentsLabelsView', () => {
keyCode: DOWN_KEY_CODE,
});
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.vm.scrollIntoViewIfNeeded).toHaveBeenCalled();
- });
+ await nextTick();
+ expect(wrapper.vm.scrollIntoViewIfNeeded).toHaveBeenCalled();
});
});
@@ -294,15 +293,14 @@ describe('DropdownContentsLabelsView', () => {
expect(wrapper.find(GlIntersectionObserver).exists()).toBe(true);
});
- it('renders gl-loading-icon component when `labelsFetchInProgress` prop is true', () => {
+ it('renders gl-loading-icon component when `labelsFetchInProgress` prop is true', async () => {
wrapper.vm.$store.dispatch('requestLabels');
- return wrapper.vm.$nextTick(() => {
- const loadingIconEl = findLoadingIcon();
+ await nextTick();
+ const loadingIconEl = findLoadingIcon();
- expect(loadingIconEl.exists()).toBe(true);
- expect(loadingIconEl.attributes('class')).toContain('labels-fetch-loading');
- });
+ expect(loadingIconEl.exists()).toBe(true);
+ expect(loadingIconEl.attributes('class')).toContain('labels-fetch-loading');
});
it('renders dropdown title element', () => {
@@ -339,47 +337,44 @@ describe('DropdownContentsLabelsView', () => {
expect(wrapper.findAll(LabelItem)).toHaveLength(mockLabels.length);
});
- it('renders label element with `highlight` set to true when value of `currentHighlightItem` is more than -1', () => {
+ it('renders label element with `highlight` set to true when value of `currentHighlightItem` is more than -1', async () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({
currentHighlightItem: 0,
});
- return wrapper.vm.$nextTick(() => {
- const labelItemEl = findDropdownContent().find(LabelItem);
+ await nextTick();
+ const labelItemEl = findDropdownContent().find(LabelItem);
- expect(labelItemEl.attributes('highlight')).toBe('true');
- });
+ expect(labelItemEl.attributes('highlight')).toBe('true');
});
- it('renders element containing "No matching results" when `searchKey` does not match with any label', () => {
+ it('renders element containing "No matching results" when `searchKey` does not match with any label', async () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({
searchKey: 'abc',
});
- return wrapper.vm.$nextTick(() => {
- const noMatchEl = findDropdownContent().find('li');
+ await nextTick();
+ const noMatchEl = findDropdownContent().find('li');
- expect(noMatchEl.isVisible()).toBe(true);
- expect(noMatchEl.text()).toContain('No matching results');
- });
+ expect(noMatchEl.isVisible()).toBe(true);
+ expect(noMatchEl.text()).toContain('No matching results');
});
- it('renders empty content while loading', () => {
+ it('renders empty content while loading', async () => {
wrapper.vm.$store.state.labelsFetchInProgress = true;
- return wrapper.vm.$nextTick(() => {
- const dropdownContent = findDropdownContent();
- const loadingIcon = findLoadingIcon();
+ await nextTick();
+ const dropdownContent = findDropdownContent();
+ const loadingIcon = findLoadingIcon();
- expect(dropdownContent.exists()).toBe(true);
- expect(dropdownContent.isVisible()).toBe(true);
- expect(loadingIcon.exists()).toBe(true);
- expect(loadingIcon.isVisible()).toBe(true);
- });
+ expect(dropdownContent.exists()).toBe(true);
+ expect(dropdownContent.isVisible()).toBe(true);
+ expect(loadingIcon.exists()).toBe(true);
+ expect(loadingIcon.isVisible()).toBe(true);
});
it('renders footer list items', () => {
@@ -393,14 +388,13 @@ describe('DropdownContentsLabelsView', () => {
expect(manageLabelsLink.text()).toBe('Manage labels');
});
- it('does not render "Create label" footer link when `state.allowLabelCreate` is `false`', () => {
+ it('does not render "Create label" footer link when `state.allowLabelCreate` is `false`', async () => {
wrapper.vm.$store.state.allowLabelCreate = false;
- return wrapper.vm.$nextTick(() => {
- const createLabelLink = findDropdownFooter().findAll(GlLink).at(0);
+ await nextTick();
+ const createLabelLink = findDropdownFooter().findAll(GlLink).at(0);
- expect(createLabelLink.text()).not.toBe('Create label');
- });
+ expect(createLabelLink.text()).not.toBe('Create label');
});
it('does not render footer list items when `state.variant` is "standalone"', () => {
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_title_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_title_spec.js
index 110c1d1b7eb..84e9f3f41c3 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_title_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_title_spec.js
@@ -1,6 +1,6 @@
import { GlButton, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import DropdownTitle from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue';
@@ -47,14 +47,13 @@ describe('DropdownTitle', () => {
expect(editBtnEl.text()).toBe('Edit');
});
- it('renders loading icon element when `labelsSelectInProgress` prop is true', () => {
+ it('renders loading icon element when `labelsSelectInProgress` prop is true', async () => {
wrapper.setProps({
labelsSelectInProgress: true,
});
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.find(GlLoadingIcon).isVisible()).toBe(true);
- });
+ await nextTick();
+ expect(wrapper.find(GlLoadingIcon).isVisible()).toBe(true);
});
});
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed_spec.js
index a7f9391cb5f..c6400320dea 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed_spec.js
@@ -1,5 +1,6 @@
import { shallowMount } from '@vue/test-utils';
import { GlIcon } from '@gitlab/ui';
+import { nextTick } from 'vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import DropdownValueCollapsedComponent from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed.vue';
@@ -42,7 +43,7 @@ describe('DropdownValueCollapsedComponent', () => {
wrapper.trigger('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.emitted('onValueClick')[0]).toBeDefined();
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js
index 4b0ba075eda..31819d0e2f7 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { isInViewport } from '~/lib/utils/common_utils';
@@ -139,27 +139,26 @@ describe('LabelsSelectRoot', () => {
${'embedded'} | ${'is-embedded'}
`(
'renders component root element with CSS class `$cssClass` when `state.variant` is "$variant"',
- ({ variant, cssClass }) => {
+ async ({ variant, cssClass }) => {
createComponent({
...mockConfig,
variant,
});
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.classes()).toContain(cssClass);
- });
+ await nextTick();
+ expect(wrapper.classes()).toContain(cssClass);
},
);
it('renders `dropdown-value-collapsed` component when `allowLabelCreate` prop is `true`', async () => {
createComponent();
- await wrapper.vm.$nextTick;
+ await nextTick;
expect(wrapper.find(DropdownValueCollapsed).exists()).toBe(true);
});
it('renders `dropdown-title` component', async () => {
createComponent();
- await wrapper.vm.$nextTick;
+ await nextTick;
expect(wrapper.find(DropdownTitle).exists()).toBe(true);
});
@@ -167,7 +166,7 @@ describe('LabelsSelectRoot', () => {
createComponent(mockConfig, {
default: 'None',
});
- await wrapper.vm.$nextTick;
+ await nextTick;
const valueComp = wrapper.find(DropdownValue);
@@ -178,14 +177,14 @@ describe('LabelsSelectRoot', () => {
it('renders `dropdown-button` component when `showDropdownButton` prop is `true`', async () => {
createComponent();
wrapper.vm.$store.dispatch('toggleDropdownButton');
- await wrapper.vm.$nextTick;
+ await nextTick;
expect(wrapper.find(DropdownButton).exists()).toBe(true);
});
it('renders `dropdown-contents` component when `showDropdownButton` & `showDropdownContents` prop is `true`', async () => {
createComponent();
wrapper.vm.$store.dispatch('toggleDropdownContents');
- await wrapper.vm.$nextTick;
+ await nextTick;
expect(wrapper.find(DropdownContents).exists()).toBe(true);
});
@@ -198,22 +197,20 @@ describe('LabelsSelectRoot', () => {
wrapper.vm.$store.dispatch('toggleDropdownContents');
});
- it('set direction when out of viewport', () => {
+ it('set direction when out of viewport', async () => {
isInViewport.mockImplementation(() => false);
wrapper.vm.setContentIsOnViewport(wrapper.vm.$store.state);
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(DropdownContents).props('renderOnTop')).toBe(true);
- });
+ await nextTick();
+ expect(wrapper.find(DropdownContents).props('renderOnTop')).toBe(true);
});
- it('does not set direction when inside of viewport', () => {
+ it('does not set direction when inside of viewport', async () => {
isInViewport.mockImplementation(() => true);
wrapper.vm.setContentIsOnViewport(wrapper.vm.$store.state);
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(DropdownContents).props('renderOnTop')).toBe(false);
- });
+ await nextTick();
+ expect(wrapper.find(DropdownContents).props('renderOnTop')).toBe(false);
});
},
);
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js
index a4199bb3e27..67e1a3ce932 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js
@@ -117,9 +117,15 @@ describe('LabelsSelectRoot', () => {
it('renders dropdown value component when query labels is resolved', () => {
expect(findDropdownValue().exists()).toBe(true);
- expect(findDropdownValue().props('selectedLabels')).toEqual(
- issuableLabelsQueryResponse.data.workspace.issuable.labels.nodes,
- );
+ expect(findDropdownValue().props('selectedLabels')).toEqual([
+ {
+ color: '#330066',
+ description: null,
+ id: 'gid://gitlab/ProjectLabel/1',
+ title: 'Label1',
+ textColor: '#000000',
+ },
+ ]);
});
it('emits `onLabelRemove` event on dropdown value label remove event', () => {
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js
index 6ef54ce37ce..49224fb915c 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js
@@ -96,6 +96,7 @@ export const workspaceLabelsQueryResponse = {
labels: {
nodes: [
{
+ __typename: 'Label',
color: '#330066',
description: null,
id: 'gid://gitlab/ProjectLabel/1',
@@ -103,6 +104,7 @@ export const workspaceLabelsQueryResponse = {
textColor: '#000000',
},
{
+ __typename: 'Label',
color: '#2f7b2e',
description: null,
id: 'gid://gitlab/ProjectLabel/2',
@@ -125,6 +127,7 @@ export const issuableLabelsQueryResponse = {
labels: {
nodes: [
{
+ __typename: 'Label',
color: '#330066',
description: null,
id: 'gid://gitlab/ProjectLabel/1',
diff --git a/spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js b/spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js
index a6c9bda1aa2..267a467059d 100644
--- a/spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js
@@ -1,6 +1,7 @@
import { GlButton } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import ToggleSidebar from '~/vue_shared/components/sidebar/toggle_sidebar.vue';
describe('ToggleSidebar', () => {
@@ -38,7 +39,7 @@ describe('ToggleSidebar', () => {
createComponent({ mountFn: mount });
findGlButton().trigger('click');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.emitted('toggle')[0]).toBeDefined();
});
diff --git a/spec/frontend/vue_shared/components/source_viewer_spec.js b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js
index 094d8d42a47..2010bac7060 100644
--- a/spec/frontend/vue_shared/components/source_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js
@@ -1,8 +1,10 @@
import hljs from 'highlight.js/lib/core';
+import { GlLoadingIcon } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueRouter from 'vue-router';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import SourceViewer from '~/vue_shared/components/source_viewer.vue';
+import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer.vue';
+import { ROUGE_TO_HLJS_LANGUAGE_MAP } from '~/vue_shared/components/source_viewer/constants';
import LineNumbers from '~/vue_shared/components/line_numbers.vue';
import waitForPromises from 'helpers/wait_for_promises';
@@ -12,42 +14,50 @@ const router = new VueRouter();
describe('Source Viewer component', () => {
let wrapper;
+ const language = 'docker';
+ const mappedLanguage = ROUGE_TO_HLJS_LANGUAGE_MAP[language];
const content = `// Some source code`;
+ const DEFAULT_BLOB_DATA = { language, rawTextBlob: content };
const highlightedContent = `<span data-testid='test-highlighted' id='LC1'>${content}</span><span id='LC2'></span>`;
- const language = 'javascript';
- hljs.highlight.mockImplementation(() => ({ value: highlightedContent }));
- hljs.highlightAuto.mockImplementation(() => ({ value: highlightedContent }));
-
- const createComponent = async (props = {}) => {
+ const createComponent = async (blob = {}) => {
wrapper = shallowMountExtended(SourceViewer, {
router,
- propsData: { content, language, ...props },
+ propsData: { blob: { ...DEFAULT_BLOB_DATA, ...blob } },
});
await waitForPromises();
};
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findLineNumbers = () => wrapper.findComponent(LineNumbers);
const findHighlightedContent = () => wrapper.findByTestId('test-highlighted');
const findFirstLine = () => wrapper.find('#LC1');
- beforeEach(() => createComponent());
+ beforeEach(() => {
+ hljs.highlight.mockImplementation(() => ({ value: highlightedContent }));
+ hljs.highlightAuto.mockImplementation(() => ({ value: highlightedContent }));
+
+ return createComponent();
+ });
afterEach(() => wrapper.destroy());
describe('highlight.js', () => {
it('registers the language definition', async () => {
- const languageDefinition = await import(`highlight.js/lib/languages/${language}`);
+ const languageDefinition = await import(`highlight.js/lib/languages/${mappedLanguage}`);
- expect(hljs.registerLanguage).toHaveBeenCalledWith(language, languageDefinition.default);
+ expect(hljs.registerLanguage).toHaveBeenCalledWith(
+ mappedLanguage,
+ languageDefinition.default,
+ );
});
it('highlights the content', () => {
- expect(hljs.highlight).toHaveBeenCalledWith(content, { language });
+ expect(hljs.highlight).toHaveBeenCalledWith(content, { language: mappedLanguage });
});
- describe('auto-detect enabled', () => {
- beforeEach(() => createComponent({ autoDetect: true }));
+ describe('auto-detects if a language cannot be loaded', () => {
+ beforeEach(() => createComponent({ language: 'some_unknown_language' }));
it('highlights the content with auto-detection', () => {
expect(hljs.highlightAuto).toHaveBeenCalledWith(content);
@@ -56,6 +66,13 @@ describe('Source Viewer component', () => {
});
describe('rendering', () => {
+ it('renders a loading icon if no highlighted content is available yet', async () => {
+ hljs.highlight.mockImplementation(() => ({ value: null }));
+ await createComponent();
+
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+
it('renders Line Numbers', () => {
expect(findLineNumbers().props('lines')).toBe(1);
});
diff --git a/spec/frontend/vue_shared/components/source_viewer/utils_spec.js b/spec/frontend/vue_shared/components/source_viewer/utils_spec.js
new file mode 100644
index 00000000000..937c3b26c67
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/utils_spec.js
@@ -0,0 +1,13 @@
+import { wrapLines } from '~/vue_shared/components/source_viewer/utils';
+
+describe('Wrap lines', () => {
+ it.each`
+ input | output
+ ${'line 1'} | ${'<span id="LC1" class="line">line 1</span>'}
+ ${'line 1\nline 2'} | ${`<span id="LC1" class="line">line 1</span>\n<span id="LC2" class="line">line 2</span>`}
+ ${'<span class="hljs-code">line 1\nline 2</span>'} | ${`<span id="LC1" class="hljs-code">line 1\n<span id="LC2" class="line">line 2</span></span>`}
+ ${'<span class="hljs-code">```bash'} | ${'<span id="LC1" class="hljs-code">```bash'}
+ `('returns lines wrapped in spans containing line numbers', ({ input, output }) => {
+ expect(wrapLines(input)).toBe(output);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/split_button_spec.js b/spec/frontend/vue_shared/components/split_button_spec.js
index ad11e6519c4..4965969bc3e 100644
--- a/spec/frontend/vue_shared/components/split_button_spec.js
+++ b/spec/frontend/vue_shared/components/split_button_spec.js
@@ -1,6 +1,7 @@
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import SplitButton from '~/vue_shared/components/split_button.vue';
const mockActionItems = [
@@ -27,15 +28,15 @@ describe('SplitButton', () => {
const findDropdown = () => wrapper.find(GlDropdown);
const findDropdownItem = (index = 0) => findDropdown().findAll(GlDropdownItem).at(index);
- const selectItem = (index) => {
+ const selectItem = async (index) => {
findDropdownItem(index).vm.$emit('click');
- return wrapper.vm.$nextTick();
+ await nextTick();
};
- const clickToggleButton = () => {
+ const clickToggleButton = async () => {
findDropdown().vm.$emit('click');
- return wrapper.vm.$nextTick();
+ await nextTick();
};
it('fails for empty actionItems', () => {
diff --git a/spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap b/spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap
index 0f1e118d44c..a613b325462 100644
--- a/spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap
+++ b/spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap
@@ -6,6 +6,7 @@ exports[`Upload dropzone component correctly overrides description and drop mess
>
<button
class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3"
+ type="button"
>
<div
class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column"
@@ -86,6 +87,7 @@ exports[`Upload dropzone component when dragging renders correct template when d
>
<button
class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3"
+ type="button"
>
<div
class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column"
@@ -170,6 +172,7 @@ exports[`Upload dropzone component when dragging renders correct template when d
>
<button
class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3"
+ type="button"
>
<div
class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column"
@@ -254,6 +257,7 @@ exports[`Upload dropzone component when dragging renders correct template when d
>
<button
class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3"
+ type="button"
>
<div
class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column"
@@ -339,6 +343,7 @@ exports[`Upload dropzone component when dragging renders correct template when d
>
<button
class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3"
+ type="button"
>
<div
class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column"
@@ -424,6 +429,7 @@ exports[`Upload dropzone component when dragging renders correct template when d
>
<button
class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3"
+ type="button"
>
<div
class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column"
@@ -509,6 +515,7 @@ exports[`Upload dropzone component when no slot provided renders default dropzon
>
<button
class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3"
+ type="button"
>
<div
class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column"
diff --git a/spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js b/spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js
index b3cdbccb271..21e9b401215 100644
--- a/spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js
+++ b/spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js
@@ -1,5 +1,6 @@
import { GlIcon, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
jest.mock('~/flash');
@@ -15,6 +16,7 @@ describe('Upload dropzone component', () => {
const findDropzoneArea = () => wrapper.find('[data-testid="dropzone-area"]');
const findIcon = () => wrapper.find(GlIcon);
const findUploadText = () => wrapper.find('[data-testid="upload-text"]').text();
+ const findFileInput = () => wrapper.find('input[type="file"]');
function createComponent({ slots = {}, data = {}, props = {} } = {}) {
wrapper = shallowMount(UploadDropzone, {
@@ -84,47 +86,40 @@ describe('Upload dropzone component', () => {
${'contains text'} | ${mockDragEvent({ types: ['text'] })}
${'contains files and text'} | ${mockDragEvent({ types: ['Files', 'text'] })}
${'contains files'} | ${mockDragEvent({ types: ['Files'] })}
- `('renders correct template when drag event $description', ({ eventPayload }) => {
+ `('renders correct template when drag event $description', async ({ eventPayload }) => {
createComponent();
wrapper.trigger('dragenter', eventPayload);
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.element).toMatchSnapshot();
- });
+ await nextTick();
+ expect(wrapper.element).toMatchSnapshot();
});
- it('renders correct template when dragging stops', () => {
+ it('renders correct template when dragging stops', async () => {
createComponent();
wrapper.trigger('dragenter');
- return wrapper.vm
- .$nextTick()
- .then(() => {
- wrapper.trigger('dragleave');
- return wrapper.vm.$nextTick();
- })
- .then(() => {
- expect(wrapper.element).toMatchSnapshot();
- });
+
+ await nextTick();
+ wrapper.trigger('dragleave');
+
+ await nextTick();
+ expect(wrapper.element).toMatchSnapshot();
});
});
describe('when dropping', () => {
- it('emits upload event', () => {
+ it('emits upload event', async () => {
createComponent();
const mockFile = { name: 'test', type: 'image/jpg' };
const mockEvent = mockDragEvent({ files: [mockFile] });
wrapper.trigger('dragenter', mockEvent);
- return wrapper.vm
- .$nextTick()
- .then(() => {
- wrapper.trigger('drop', mockEvent);
- return wrapper.vm.$nextTick();
- })
- .then(() => {
- expect(wrapper.emitted().change[0]).toEqual([[mockFile]]);
- });
+
+ await nextTick();
+ wrapper.trigger('drop', mockEvent);
+
+ await nextTick();
+ expect(wrapper.emitted().change[0]).toEqual([[mockFile]]);
});
});
@@ -203,4 +198,60 @@ describe('Upload dropzone component', () => {
expect(wrapper.element).toMatchSnapshot();
});
+
+ describe('file input form name', () => {
+ it('applies inputFieldName as file input name', () => {
+ createComponent({ props: { inputFieldName: 'test_field_name' } });
+ expect(findFileInput().attributes('name')).toBe('test_field_name');
+ });
+
+ it('uses default file input name if no inputFieldName provided', () => {
+ createComponent();
+ expect(findFileInput().attributes('name')).toBe('upload_file');
+ });
+ });
+
+ describe('updates file input files value', () => {
+ // NOTE: the component assigns dropped files from the drop event to the
+ // input.files property. There's a restriction that nothing but a FileList
+ // can be assigned to this property. While FileList can't be created
+ // manually: it has no constructor. And currently there's no good workaround
+ // for jsdom. So we have to stub the file input in vm.$refs to ensure that
+ // the files property is updated. This enforces following tests to know a
+ // bit too much about the SUT internals See this thread for more details on
+ // FileList in jsdom: https://github.com/jsdom/jsdom/issues/1272
+ function stubFileInputOnWrapper() {
+ const fakeFileInput = { files: [] };
+ wrapper.vm.$refs.fileUpload = fakeFileInput;
+ }
+
+ it('assigns dragged files to the input files property', async () => {
+ const mockFile = { name: 'test', type: 'image/jpg' };
+ const mockEvent = mockDragEvent({ files: [mockFile] });
+ createComponent({ props: { shouldUpdateInputOnFileDrop: true } });
+ stubFileInputOnWrapper();
+
+ wrapper.trigger('dragenter', mockEvent);
+ await nextTick();
+ wrapper.trigger('drop', mockEvent);
+ await nextTick();
+
+ expect(wrapper.vm.$refs.fileUpload.files).toEqual([mockFile]);
+ });
+
+ it('throws an error when multiple files are dropped on a single file input dropzone', async () => {
+ const mockFile = { name: 'test', type: 'image/jpg' };
+ const mockEvent = mockDragEvent({ files: [mockFile, mockFile] });
+ createComponent({ props: { shouldUpdateInputOnFileDrop: true, singleFileSelection: true } });
+ stubFileInputOnWrapper();
+
+ wrapper.trigger('dragenter', mockEvent);
+ await nextTick();
+ wrapper.trigger('drop', mockEvent);
+ await nextTick();
+
+ expect(wrapper.vm.$refs.fileUpload.files).toEqual([]);
+ expect(wrapper.emitted('error')).toHaveLength(1);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js
index 1d15da491cd..66bb234aef6 100644
--- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js
+++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js
@@ -1,5 +1,6 @@
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import { TEST_HOST } from 'spec/test_constants';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import UserAvatarList from '~/vue_shared/components/user_avatar/user_avatar_list.vue';
@@ -142,14 +143,13 @@ describe('UserAvatarList', () => {
expect(links.length).toEqual(props.items.length);
});
- it('with collapse clicked, it renders avatars up to breakpoint', () => {
+ it('with collapse clicked, it renders avatars up to breakpoint', async () => {
clickButton();
- return wrapper.vm.$nextTick(() => {
- const links = wrapper.findAll(UserAvatarLink);
+ await nextTick();
+ const links = wrapper.findAll(UserAvatarLink);
- expect(links.length).toEqual(TEST_BREAKPOINT);
- });
+ expect(links.length).toEqual(TEST_BREAKPOINT);
});
});
});
diff --git a/spec/frontend/vue_shared/components/user_select_spec.js b/spec/frontend/vue_shared/components/user_select_spec.js
index 8994e16e517..411a15e1c74 100644
--- a/spec/frontend/vue_shared/components/user_select_spec.js
+++ b/spec/frontend/vue_shared/components/user_select_spec.js
@@ -104,14 +104,14 @@ describe('User select dropdown', () => {
createComponent({ participantsQueryHandler: mockError });
await waitForPromises();
- expect(wrapper.emitted('error')).toEqual([[], []]);
+ expect(wrapper.emitted('error')).toEqual([[]]);
});
it('emits an `error` event if search query was rejected', async () => {
createComponent({ searchQueryHandler: mockError });
await waitForSearch();
- expect(wrapper.emitted('error')).toEqual([[], []]);
+ expect(wrapper.emitted('error')).toEqual([[]]);
});
it('renders current user if they are not in participants or assignees', async () => {
diff --git a/spec/frontend/vue_shared/components/web_ide_link_spec.js b/spec/frontend/vue_shared/components/web_ide_link_spec.js
index 659d93d6597..5589cbfd08f 100644
--- a/spec/frontend/vue_shared/components/web_ide_link_spec.js
+++ b/spec/frontend/vue_shared/components/web_ide_link_spec.js
@@ -4,6 +4,7 @@ import { nextTick } from 'vue';
import ActionsButton from '~/vue_shared/components/actions_button.vue';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import WebIdeLink from '~/vue_shared/components/web_ide_link.vue';
+import ConfirmForkModal from '~/vue_shared/components/confirm_fork_modal.vue';
import { stubComponent } from 'helpers/stub_component';
import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
@@ -13,6 +14,7 @@ const TEST_WEB_IDE_URL = '/-/ide/project/gitlab-test/test/edit/main/-/';
const TEST_GITPOD_URL = 'https://gitpod.test/';
const TEST_USER_PREFERENCES_GITPOD_PATH = '/-/profile/preferences#user_gitpod_enabled';
const TEST_USER_PROFILE_ENABLE_GITPOD_PATH = '/-/profile?user%5Bgitpod_enabled%5D=true';
+const forkPath = '/some/fork/path';
const ACTION_EDIT = {
href: TEST_EDIT_URL,
@@ -74,6 +76,7 @@ describe('Web IDE link component', () => {
editUrl: TEST_EDIT_URL,
webIdeUrl: TEST_WEB_IDE_URL,
gitpodUrl: TEST_GITPOD_URL,
+ forkPath,
...props,
},
stubs: {
@@ -96,6 +99,7 @@ describe('Web IDE link component', () => {
const findActionsButton = () => wrapper.find(ActionsButton);
const findLocalStorageSync = () => wrapper.find(LocalStorageSync);
const findModal = () => wrapper.findComponent(GlModal);
+ const findForkConfirmModal = () => wrapper.findComponent(ConfirmForkModal);
it.each([
{
@@ -213,7 +217,7 @@ describe('Web IDE link component', () => {
findLocalStorageSync().vm.$emit('input', ACTION_GITPOD.key);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findActionsButton().props('selectedKey')).toBe(ACTION_GITPOD.key);
});
@@ -223,7 +227,7 @@ describe('Web IDE link component', () => {
findActionsButton().vm.$emit('select', ACTION_GITPOD.key);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findActionsButton().props('selectedKey')).toBe(ACTION_GITPOD.key);
expect(findLocalStorageSync().props('value')).toBe(ACTION_GITPOD.key);
@@ -231,16 +235,28 @@ describe('Web IDE link component', () => {
});
describe('edit actions', () => {
- it.each([
+ const testActions = [
{
- props: { showWebIdeButton: true, showEditButton: false },
+ props: {
+ showWebIdeButton: true,
+ showEditButton: false,
+ forkPath,
+ forkModalId: 'edit-modal',
+ },
expectedEventPayload: 'ide',
},
{
- props: { showWebIdeButton: false, showEditButton: true },
+ props: {
+ showWebIdeButton: false,
+ showEditButton: true,
+ forkPath,
+ forkModalId: 'webide-modal',
+ },
expectedEventPayload: 'simple',
},
- ])(
+ ];
+
+ it.each(testActions)(
'emits the correct event when an action handler is called',
async ({ props, expectedEventPayload }) => {
createComponent({ ...props, needsToFork: true, disableForkModal: true });
@@ -250,6 +266,29 @@ describe('Web IDE link component', () => {
expect(wrapper.emitted('edit')).toEqual([[expectedEventPayload]]);
},
);
+
+ it.each(testActions)('renders the fork confirmation modal', async ({ props }) => {
+ createComponent({ ...props, needsToFork: true });
+
+ expect(findForkConfirmModal().exists()).toBe(true);
+ expect(findForkConfirmModal().props()).toEqual({
+ visible: false,
+ forkPath,
+ modalId: props.forkModalId,
+ });
+ });
+
+ it.each(testActions)('opens the modal when the button is clicked', async ({ props }) => {
+ createComponent({ ...props, needsToFork: true }, mountExtended);
+
+ await findActionsButton().trigger('click');
+
+ expect(findForkConfirmModal().props()).toEqual({
+ visible: true,
+ forkPath,
+ modalId: props.forkModalId,
+ });
+ });
});
describe('when Gitpod is not enabled', () => {
diff --git a/spec/frontend/vue_shared/directives/track_event_spec.js b/spec/frontend/vue_shared/directives/track_event_spec.js
index b3f94d0242a..4bf84b06246 100644
--- a/spec/frontend/vue_shared/directives/track_event_spec.js
+++ b/spec/frontend/vue_shared/directives/track_event_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import Tracking from '~/tracking';
import TrackEvent from '~/vue_shared/directives/track_event';
@@ -31,7 +31,7 @@ describe('Error Tracking directive', () => {
expect(Tracking.event).not.toHaveBeenCalled();
});
- it('should track event on click if tracking info provided', () => {
+ it('should track event on click if tracking info provided', async () => {
const trackingOptions = {
category: 'Tracking',
action: 'click_trackable_btn',
@@ -43,9 +43,8 @@ describe('Error Tracking directive', () => {
wrapper.setData({ trackingOptions });
const { category, action, label, property, value } = trackingOptions;
- return wrapper.vm.$nextTick(() => {
- button.trigger('click');
- expect(Tracking.event).toHaveBeenCalledWith(category, action, { label, property, value });
- });
+ await nextTick();
+ button.trigger('click');
+ expect(Tracking.event).toHaveBeenCalledWith(category, action, { label, property, value });
});
});
diff --git a/spec/frontend/vue_shared/issuable/list/components/issuable_bulk_edit_sidebar_spec.js b/spec/frontend/vue_shared/issuable/list/components/issuable_bulk_edit_sidebar_spec.js
index 0f33a3d1122..7dfeced571a 100644
--- a/spec/frontend/vue_shared/issuable/list/components/issuable_bulk_edit_sidebar_spec.js
+++ b/spec/frontend/vue_shared/issuable/list/components/issuable_bulk_edit_sidebar_spec.js
@@ -1,5 +1,6 @@
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import IssuableBulkEditSidebar from '~/vue_shared/issuable/list/components/issuable_bulk_edit_sidebar.vue';
const createComponent = ({ expanded = true } = {}) =>
@@ -48,7 +49,7 @@ describe('IssuableBulkEditSidebar', () => {
expanded,
});
- await wrappeCustom.vm.$nextTick();
+ await nextTick();
expect(document.querySelector('.layout-page').classList.contains(layoutPageClass)).toBe(
true,
@@ -78,7 +79,7 @@ describe('IssuableBulkEditSidebar', () => {
expanded,
});
- await wrappeCustom.vm.$nextTick();
+ await nextTick();
expect(wrappeCustom.classes()).toContain(layoutPageClass);
diff --git a/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js b/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js
index e38a80e7734..65eb42ef053 100644
--- a/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js
+++ b/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js
@@ -1,4 +1,5 @@
import { GlLink, GlLabel, GlIcon, GlFormCheckbox, GlSprintf } from '@gitlab/ui';
+import { nextTick } from 'vue';
import { useFakeDate } from 'helpers/fake_date';
import { shallowMountExtended as shallowMount } from 'helpers/vue_test_utils_helper';
import IssuableItem from '~/vue_shared/issuable/list/components/issuable_item.vue';
@@ -9,7 +10,6 @@ import { mockIssuable, mockRegularLabel, mockScopedLabel } from '../mock_data';
const createComponent = ({
issuableSymbol = '#',
issuable = mockIssuable,
- enableLabelPermalinks = true,
showCheckbox = true,
slots = {},
} = {}) =>
@@ -17,7 +17,6 @@ const createComponent = ({
propsData: {
issuableSymbol,
issuable,
- enableLabelPermalinks,
showDiscussions: true,
showCheckbox,
},
@@ -76,7 +75,7 @@ describe('IssuableItem', () => {
},
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.vm.authorId).toBe(returnValue);
},
@@ -100,7 +99,7 @@ describe('IssuableItem', () => {
},
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.vm.isIssuableUrlExternal).toBe(returnValue);
},
@@ -122,7 +121,7 @@ describe('IssuableItem', () => {
},
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.vm.labels).toEqual(mockLabels);
});
@@ -135,7 +134,7 @@ describe('IssuableItem', () => {
},
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.vm.labels).toEqual([]);
});
@@ -211,23 +210,13 @@ describe('IssuableItem', () => {
});
describe('labelTarget', () => {
- it('returns target string for a provided label param when `enableLabelPermalinks` is true', () => {
+ it('returns target string for a provided label param', () => {
wrapper = createComponent();
expect(wrapper.vm.labelTarget(mockRegularLabel)).toBe(
'?label_name[]=Documentation%20Update',
);
});
-
- it('returns string "#" for a provided label param when `enableLabelPermalinks` is false', async () => {
- wrapper = createComponent({
- enableLabelPermalinks: false,
- });
-
- await wrapper.vm.$nextTick();
-
- expect(wrapper.vm.labelTarget(mockRegularLabel)).toBe('#');
- });
});
});
@@ -248,7 +237,7 @@ describe('IssuableItem', () => {
},
});
- await wrapper.vm.$nextTick();
+ await nextTick();
const titleEl = wrapper.find('[data-testid="issuable-title"]');
@@ -264,7 +253,7 @@ describe('IssuableItem', () => {
showCheckbox: true,
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.find(GlFormCheckbox).exists()).toBe(true);
expect(wrapper.find(GlFormCheckbox).attributes('checked')).not.toBeDefined();
@@ -273,7 +262,7 @@ describe('IssuableItem', () => {
checked: true,
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.find(GlFormCheckbox).attributes('checked')).toBe('true');
});
@@ -286,7 +275,7 @@ describe('IssuableItem', () => {
},
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.find('[data-testid="issuable-title"]').find(GlLink).attributes('target')).toBe(
'_blank',
@@ -301,7 +290,7 @@ describe('IssuableItem', () => {
},
});
- await wrapper.vm.$nextTick();
+ await nextTick();
const confidentialEl = wrapper.find('[data-testid="issuable-title"]').find(GlIcon);
diff --git a/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js b/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js
index 14e93108447..64823cd4c6c 100644
--- a/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js
+++ b/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js
@@ -2,6 +2,7 @@ import { GlAlert, GlKeysetPagination, GlSkeletonLoading, GlPagination } from '@g
import { shallowMount } from '@vue/test-utils';
import VueDraggable from 'vuedraggable';
+import { nextTick } from 'vue';
import { TEST_HOST } from 'helpers/test_constants';
import IssuableItem from '~/vue_shared/issuable/list/components/issuable_item.vue';
@@ -77,7 +78,7 @@ describe('IssuableListRoot', () => {
currentPage,
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.vm.skeletonItemCount).toBe(returnValue);
},
@@ -96,7 +97,7 @@ describe('IssuableListRoot', () => {
issuables,
});
- await wrapper.vm.$nextTick();
+ await nextTick();
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
@@ -104,7 +105,7 @@ describe('IssuableListRoot', () => {
checkedIssuables,
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.vm.allIssuablesChecked).toBe(returnValue);
},
@@ -119,7 +120,7 @@ describe('IssuableListRoot', () => {
checkedIssuables: mockCheckedIssuables,
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.vm.bulkEditIssuables).toHaveLength(mIssuables.length);
});
@@ -137,7 +138,7 @@ describe('IssuableListRoot', () => {
issuables: [mockIssuables[0]],
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(Object.keys(wrapper.vm.checkedIssuables)).toHaveLength(1);
expect(wrapper.vm.checkedIssuables[mockIssuables[0].iid]).toEqual({
@@ -160,7 +161,7 @@ describe('IssuableListRoot', () => {
urlParams,
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(global.window.location.href).toBe(
`${TEST_HOST}/?state=${urlParams.state}&sort=${urlParams.sort}&page=${urlParams.page}&search=${urlParams.search}`,
@@ -192,7 +193,7 @@ describe('IssuableListRoot', () => {
},
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.vm.issuableChecked(mockIssuables[0])).toBe(true);
});
diff --git a/spec/frontend/vue_shared/issuable/list/components/issuable_tabs_spec.js b/spec/frontend/vue_shared/issuable/list/components/issuable_tabs_spec.js
index 5723e2da586..27985895c62 100644
--- a/spec/frontend/vue_shared/issuable/list/components/issuable_tabs_spec.js
+++ b/spec/frontend/vue_shared/issuable/list/components/issuable_tabs_spec.js
@@ -1,5 +1,6 @@
import { GlTab, GlBadge } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import { mount, shallowMount } from '@vue/test-utils';
import { setLanguage } from 'helpers/locale_helper';
import IssuableTabs from '~/vue_shared/issuable/list/components/issuable_tabs.vue';
@@ -10,17 +11,18 @@ const createComponent = ({
tabs = mockIssuableListProps.tabs,
tabCounts = mockIssuableListProps.tabCounts,
currentTab = mockIssuableListProps.currentTab,
+ truncateCounts = false,
+ mountFn = shallowMount,
} = {}) =>
- mount(IssuableTabs, {
+ mountFn(IssuableTabs, {
propsData: {
tabs,
tabCounts,
currentTab,
+ truncateCounts,
},
slots: {
- 'nav-actions': `
- <button class="js-new-issuable">New issuable</button>
- `,
+ 'nav-actions': `<button class="js-new-issuable">New issuable</button>`,
},
});
@@ -29,7 +31,6 @@ describe('IssuableTabs', () => {
beforeEach(() => {
setLanguage('en');
- wrapper = createComponent();
});
afterEach(() => {
@@ -40,60 +41,71 @@ describe('IssuableTabs', () => {
const findAllGlBadges = () => wrapper.findAllComponents(GlBadge);
const findAllGlTabs = () => wrapper.findAllComponents(GlTab);
- describe('methods', () => {
- describe('isTabActive', () => {
- it.each`
- tabName | currentTab | returnValue
- ${'opened'} | ${'opened'} | ${true}
- ${'opened'} | ${'closed'} | ${false}
- `(
- 'returns $returnValue when tab name is "$tabName" is current tab is "$currentTab"',
- async ({ tabName, currentTab, returnValue }) => {
- wrapper.setProps({
- currentTab,
- });
-
- await wrapper.vm.$nextTick();
-
- expect(wrapper.vm.isTabActive(tabName)).toBe(returnValue);
- },
- );
- });
+ describe('tabs', () => {
+ it.each`
+ currentTab | returnValue
+ ${'opened'} | ${'true'}
+ ${'closed'} | ${undefined}
+ `(
+ 'when "$currentTab" is the selected tab, the Open tab is active=$returnValue',
+ ({ currentTab, returnValue }) => {
+ wrapper = createComponent({ currentTab });
+
+ const openTab = findAllGlTabs().at(0);
+
+ expect(openTab.attributes('active')).toBe(returnValue);
+ },
+ );
});
describe('template', () => {
it('renders gl-tab for each tab within `tabs` array', () => {
- const tabsEl = findAllGlTabs();
+ wrapper = createComponent();
+
+ const tabs = findAllGlTabs();
- expect(tabsEl.exists()).toBe(true);
- expect(tabsEl).toHaveLength(mockIssuableListProps.tabs.length);
+ expect(tabs).toHaveLength(mockIssuableListProps.tabs.length);
});
- it('renders gl-badge component within a tab', () => {
+ it('renders gl-badge component within a tab', async () => {
+ wrapper = createComponent({ mountFn: mount });
+ await nextTick();
+
const badges = findAllGlBadges();
// Does not render `All` badge since it has an undefined count
expect(badges).toHaveLength(2);
- expect(badges.at(0).text()).toBe('5,000');
+ expect(badges.at(0).text()).toBe('5,678');
expect(badges.at(1).text()).toBe(`${mockIssuableListProps.tabCounts.closed}`);
});
it('renders contents for slot "nav-actions"', () => {
- const buttonEl = wrapper.find('button.js-new-issuable');
+ wrapper = createComponent();
- expect(buttonEl.exists()).toBe(true);
- expect(buttonEl.text()).toBe('New issuable');
+ const button = wrapper.find('button.js-new-issuable');
+
+ expect(button.text()).toBe('New issuable');
+ });
+ });
+
+ describe('counts', () => {
+ it('can display as truncated', async () => {
+ wrapper = createComponent({ truncateCounts: true, mountFn: mount });
+ await nextTick();
+
+ expect(findAllGlBadges().at(0).text()).toBe('5.7k');
});
});
describe('events', () => {
it('gl-tab component emits `click` event on `click` event', () => {
- const tabEl = findAllGlTabs().at(0);
+ wrapper = createComponent();
+
+ const openTab = findAllGlTabs().at(0);
- tabEl.vm.$emit('click', 'opened');
+ openTab.vm.$emit('click', 'opened');
- expect(wrapper.emitted('click')).toBeTruthy();
- expect(wrapper.emitted('click')[0]).toEqual(['opened']);
+ expect(wrapper.emitted('click')).toEqual([['opened']]);
});
});
});
diff --git a/spec/frontend/vue_shared/issuable/list/mock_data.js b/spec/frontend/vue_shared/issuable/list/mock_data.js
index cfc7937b412..8640f4a2cd5 100644
--- a/spec/frontend/vue_shared/issuable/list/mock_data.js
+++ b/spec/frontend/vue_shared/issuable/list/mock_data.js
@@ -133,7 +133,7 @@ export const mockTabs = [
];
export const mockTabCounts = {
- opened: 5000,
+ opened: 5678,
closed: 0,
all: undefined,
};
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js
index 41bacf18a68..1a93838b03f 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js
@@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import { useFakeDate } from 'helpers/fake_date';
import IssuableBody from '~/vue_shared/issuable/show/components/issuable_body.vue';
@@ -68,7 +69,7 @@ describe('IssuableBody', () => {
},
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.vm.isUpdated).toBe(returnValue);
},
@@ -90,13 +91,13 @@ describe('IssuableBody', () => {
editFormVisible: true,
});
- await wrapper.vm.$nextTick();
+ await nextTick();
wrapper.setProps({
editFormVisible: false,
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.vm.initTaskList).toHaveBeenCalled();
});
@@ -182,7 +183,7 @@ describe('IssuableBody', () => {
editFormVisible: true,
});
- await wrapper.vm.$nextTick();
+ await nextTick();
const editFormEl = wrapper.find(IssuableEditForm);
expect(editFormEl.exists()).toBe(true);
@@ -221,7 +222,7 @@ describe('IssuableBody', () => {
editFormVisible: true,
});
- await wrapper.vm.$nextTick();
+ await nextTick();
const issuableEditForm = wrapper.find(IssuableEditForm);
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js
index 051ffd27af4..b79dc0bf976 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js
@@ -1,6 +1,7 @@
import { GlFormInput } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import IssuableEditForm from '~/vue_shared/issuable/show/components/issuable_edit_form.vue';
import IssuableEventHub from '~/vue_shared/issuable/show/event_hub';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
@@ -35,6 +36,7 @@ describe('IssuableEditForm', () => {
beforeEach(() => {
wrapper = createComponent();
+ gon.features = { markdownContinueLists: true };
});
afterEach(() => {
@@ -52,7 +54,7 @@ describe('IssuableEditForm', () => {
},
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.vm.title).toBe('Foo');
expect(wrapper.vm.description).toBe('Foobar');
@@ -67,7 +69,7 @@ describe('IssuableEditForm', () => {
},
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(wrapper.vm.title).toBe('');
expect(wrapper.vm.description).toBe('');
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js
index 41735923957..1cdd709159f 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js
@@ -1,5 +1,6 @@
import { GlIcon, GlAvatarLabeled } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import IssuableHeader from '~/vue_shared/issuable/show/components/issuable_header.vue';
@@ -78,7 +79,7 @@ describe('IssuableHeader', () => {
blocked: true,
});
- await wrapper.vm.$nextTick();
+ await nextTick();
const blockedEl = wrapper.findByTestId('blocked');
@@ -91,7 +92,7 @@ describe('IssuableHeader', () => {
confidential: true,
});
- await wrapper.vm.$nextTick();
+ await nextTick();
const confidentialEl = wrapper.findByTestId('confidential');
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js
index cb418371760..93de6dbe306 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js
@@ -1,5 +1,6 @@
import { GlIcon, GlButton, GlIntersectionObserver } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import IssuableTitle from '~/vue_shared/issuable/show/components/issuable_title.vue';
@@ -64,7 +65,7 @@ describe('IssuableTitle', () => {
},
});
- await wrapperWithTitle.vm.$nextTick();
+ await nextTick();
const titleEl = wrapperWithTitle.find('h2');
expect(titleEl.exists()).toBe(true);
@@ -90,7 +91,7 @@ describe('IssuableTitle', () => {
stickyTitleVisible: true,
});
- await wrapper.vm.$nextTick();
+ await nextTick();
const stickyHeaderEl = wrapper.find('[data-testid="header"]');
expect(stickyHeaderEl.exists()).toBe(true);
diff --git a/spec/frontend/vue_shared/issuable/sidebar/components/issuable_sidebar_root_spec.js b/spec/frontend/vue_shared/issuable/sidebar/components/issuable_sidebar_root_spec.js
index 788ba70ddc0..47bf3c8ed83 100644
--- a/spec/frontend/vue_shared/issuable/sidebar/components/issuable_sidebar_root_spec.js
+++ b/spec/frontend/vue_shared/issuable/sidebar/components/issuable_sidebar_root_spec.js
@@ -1,5 +1,6 @@
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import Cookies from 'js-cookie';
+import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import IssuableSidebarRoot from '~/vue_shared/issuable/sidebar/components/issuable_sidebar_root.vue';
@@ -69,7 +70,10 @@ describe('IssuableSidebarRoot', () => {
it('updates "collapsed_gutter" cookie value and layout classes', async () => {
await findToggleSidebarButton().trigger('click');
- expect(Cookies.set).toHaveBeenCalledWith(USER_COLLAPSED_GUTTER_COOKIE, true);
+ expect(Cookies.set).toHaveBeenCalledWith(USER_COLLAPSED_GUTTER_COOKIE, true, {
+ expires: 365,
+ secure: false,
+ });
assertPageLayoutClasses({ isExpanded: false });
});
});
@@ -88,7 +92,7 @@ describe('IssuableSidebarRoot', () => {
jest.spyOn(bp, 'isDesktop').mockReturnValue(breakpoint === 'lg' || breakpoint === 'xl');
window.dispatchEvent(new Event('resize'));
- await wrapper.vm.$nextTick();
+ await nextTick();
assertPageLayoutClasses({ isExpanded: isExpandedValue });
},
diff --git a/spec/frontend/vue_shared/new_namespace/components/welcome_spec.js b/spec/frontend/vue_shared/new_namespace/components/welcome_spec.js
index 2d51f6dbeeb..c90131fea9a 100644
--- a/spec/frontend/vue_shared/new_namespace/components/welcome_spec.js
+++ b/spec/frontend/vue_shared/new_namespace/components/welcome_spec.js
@@ -37,9 +37,8 @@ describe('Welcome page', () => {
const link = wrapper.find('a');
link.trigger('click');
await nextTick();
- return wrapper.vm.$nextTick().then(() => {
- expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_tab', { label: 'test' });
- });
+ await nextTick();
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_tab', { label: 'test' });
});
it('renders footer slot if provided', () => {
diff --git a/spec/frontend/vue_shared/security_reports/components/manage_via_mr_spec.js b/spec/frontend/vue_shared/security_reports/components/manage_via_mr_spec.js
index facbd51168c..39909e26ef0 100644
--- a/spec/frontend/vue_shared/security_reports/components/manage_via_mr_spec.js
+++ b/spec/frontend/vue_shared/security_reports/components/manage_via_mr_spec.js
@@ -16,7 +16,7 @@ jest.mock('~/lib/utils/url_utility');
Vue.use(VueApollo);
-const projectPath = 'namespace/project';
+const projectFullPath = 'namespace/project';
describe('ManageViaMr component', () => {
let wrapper;
@@ -40,7 +40,7 @@ describe('ManageViaMr component', () => {
wrapper = extendedWrapper(
mount(ManageViaMr, {
provide: {
- projectPath,
+ projectFullPath,
},
propsData: {
feature: {
@@ -65,7 +65,7 @@ describe('ManageViaMr component', () => {
// the ones available in the current test context.
const supportedReportTypes = Object.entries(featureToMutationMap).map(
([featureType, { getMutationPayload, mutationId }]) => {
- const { mutation, variables: mutationVariables } = getMutationPayload(projectPath);
+ const { mutation, variables: mutationVariables } = getMutationPayload(projectFullPath);
return [humanize(featureType), featureType, mutation, mutationId, mutationVariables];
},
);
diff --git a/spec/frontend/vue_shared/security_reports/mock_data.js b/spec/frontend/vue_shared/security_reports/mock_data.js
index 2b1513bb0f8..dac9accbbf5 100644
--- a/spec/frontend/vue_shared/security_reports/mock_data.js
+++ b/spec/frontend/vue_shared/security_reports/mock_data.js
@@ -324,7 +324,9 @@ export const secretDetectionDiffSuccessMock = {
export const securityReportMergeRequestDownloadPathsQueryNoArtifactsResponse = {
project: {
+ id: 'project-1',
mergeRequest: {
+ id: 'mr-1',
headPipeline: {
id: 'gid://gitlab/Ci::Pipeline/176',
jobs: {
diff --git a/spec/frontend/whats_new/components/app_spec.js b/spec/frontend/whats_new/components/app_spec.js
index 12034346aba..945727cd664 100644
--- a/spec/frontend/whats_new/components/app_spec.js
+++ b/spec/frontend/whats_new/components/app_spec.js
@@ -1,5 +1,6 @@
import { GlDrawer, GlInfiniteScroll } from '@gitlab/ui';
-import { createLocalVue, mount } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { mockTracking, unmockTracking, triggerEvent } from 'helpers/tracking_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
@@ -12,8 +13,7 @@ jest.mock('~/whats_new/utils/get_drawer_body_height', () => ({
getDrawerBodyHeight: jest.fn().mockImplementation(() => MOCK_DRAWER_BODY_HEIGHT),
}));
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('App', () => {
let wrapper;
@@ -46,7 +46,6 @@ describe('App', () => {
});
wrapper = mount(App, {
- localVue,
store,
propsData: buildProps(),
directives: {
@@ -68,7 +67,7 @@ describe('App', () => {
{ title: 'Whats New Drawer', url: 'www.url.com', release: 3.11 },
];
wrapper.vm.$store.state.drawerBodyHeight = MOCK_DRAWER_BODY_HEIGHT;
- await wrapper.vm.$nextTick();
+ await nextTick();
};
afterEach(() => {
@@ -109,7 +108,7 @@ describe('App', () => {
it.each([true, false])('passes open property', async (openState) => {
wrapper.vm.$store.state.open = openState;
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(getDrawer().props('open')).toBe(openState);
});
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
index 9741a193258..a98722bc465 100644
--- a/spec/frontend/work_items/mock_data.js
+++ b/spec/frontend/work_items/mock_data.js
@@ -34,3 +34,17 @@ export const updateWorkItemMutationResponse = {
},
},
};
+
+export const projectWorkItemTypesQueryResponse = {
+ data: {
+ workspace: {
+ id: '1',
+ workItemTypes: {
+ nodes: [
+ { id: 'work-item-1', name: 'Issue' },
+ { id: 'work-item-2', name: 'Incident' },
+ ],
+ },
+ },
+ },
+};
diff --git a/spec/frontend/work_items/pages/create_work_item_spec.js b/spec/frontend/work_items/pages/create_work_item_spec.js
index 71e153d30c3..b9fef0eaa6a 100644
--- a/spec/frontend/work_items/pages/create_work_item_spec.js
+++ b/spec/frontend/work_items/pages/create_work_item_spec.js
@@ -1,12 +1,14 @@
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
-import { GlAlert } from '@gitlab/ui';
+import { GlAlert, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import CreateWorkItem from '~/work_items/pages/create_work_item.vue';
import ItemTitle from '~/work_items/components/item_title.vue';
import { resolvers } from '~/work_items/graphql/resolvers';
+import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
+import { projectWorkItemTypesQueryResponse } from '../mock_data';
Vue.use(VueApollo);
@@ -14,13 +16,20 @@ describe('Create work item component', () => {
let wrapper;
let fakeApollo;
+ const querySuccessHandler = jest.fn().mockResolvedValue(projectWorkItemTypesQueryResponse);
+
const findAlert = () => wrapper.findComponent(GlAlert);
const findTitleInput = () => wrapper.findComponent(ItemTitle);
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
+
const findCreateButton = () => wrapper.find('[data-testid="create-button"]');
const findCancelButton = () => wrapper.find('[data-testid="cancel-button"]');
+ const findContent = () => wrapper.find('[data-testid="content"]');
+ const findLoadingTypesIcon = () => wrapper.find('[data-testid="loading-types"]');
- const createComponent = ({ data = {} } = {}) => {
- fakeApollo = createMockApollo([], resolvers);
+ const createComponent = ({ data = {}, props = {}, queryHandler = querySuccessHandler } = {}) => {
+ fakeApollo = createMockApollo([[projectWorkItemTypesQuery, queryHandler]], resolvers);
wrapper = shallowMount(CreateWorkItem, {
apolloProvider: fakeApollo,
data() {
@@ -28,12 +37,18 @@ describe('Create work item component', () => {
...data,
};
},
+ propsData: {
+ ...props,
+ },
mocks: {
$router: {
go: jest.fn(),
push: jest.fn(),
},
},
+ provide: {
+ fullPath: 'full-path',
+ },
});
};
@@ -54,40 +69,141 @@ describe('Create work item component', () => {
expect(findCreateButton().props('disabled')).toBe(true);
});
- it('redirects to the previous page on Cancel button click', () => {
+ describe('when displayed on a separate route', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('redirects to the previous page on Cancel button click', () => {
+ findCancelButton().vm.$emit('click');
+
+ expect(wrapper.vm.$router.go).toHaveBeenCalledWith(-1);
+ });
+
+ it('redirects to the work item page on successful mutation', async () => {
+ findTitleInput().vm.$emit('title-input', 'Test title');
+
+ wrapper.find('form').trigger('submit');
+ await waitForPromises();
+
+ expect(wrapper.vm.$router.push).toHaveBeenCalled();
+ });
+
+ it('adds right margin for create button', () => {
+ expect(findCreateButton().classes()).toContain('gl-mr-3');
+ });
+
+ it('does not add right margin for cancel button', () => {
+ expect(findCancelButton().classes()).not.toContain('gl-mr-3');
+ });
+
+ it('does not add padding for content', () => {
+ expect(findContent().classes('gl-px-5')).toBe(false);
+ });
+ });
+
+ describe('when displayed in a modal', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ isModal: true,
+ },
+ });
+ });
+
+ it('emits `closeModal` event on Cancel button click', () => {
+ findCancelButton().vm.$emit('click');
+
+ expect(wrapper.emitted('closeModal')).toEqual([[]]);
+ });
+
+ it('emits `onCreate` on successful mutation', async () => {
+ const mockTitle = 'Test title';
+ findTitleInput().vm.$emit('title-input', 'Test title');
+
+ wrapper.find('form').trigger('submit');
+ await waitForPromises();
+
+ expect(wrapper.emitted('onCreate')).toEqual([[mockTitle]]);
+ });
+
+ it('does not right margin for create button', () => {
+ expect(findCreateButton().classes()).not.toContain('gl-mr-3');
+ });
+
+ it('adds right margin for cancel button', () => {
+ expect(findCancelButton().classes()).toContain('gl-mr-3');
+ });
+
+ it('adds padding for content', () => {
+ expect(findContent().classes('gl-px-5')).toBe(true);
+ });
+ });
+
+ it('displays a loading icon inside dropdown when work items query is loading', () => {
createComponent();
- findCancelButton().vm.$emit('click');
- expect(wrapper.vm.$router.go).toHaveBeenCalledWith(-1);
+ expect(findLoadingTypesIcon().exists()).toBe(true);
+ });
+
+ it('displays an alert when work items query is rejected', async () => {
+ createComponent({ queryHandler: jest.fn().mockRejectedValue('Houston, we have a problem') });
+ await waitForPromises();
+
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().text()).toContain('fetching work item types');
+ });
+
+ describe('when work item types are fetched', () => {
+ beforeEach(() => {
+ createComponent();
+ return waitForPromises();
+ });
+
+ it('displays a list of work item types', () => {
+ expect(findDropdownItems()).toHaveLength(2);
+ expect(findDropdownItems().at(0).text()).toContain('Issue');
+ });
+
+ it('selects a work item type on click', async () => {
+ expect(findDropdown().props('text')).toBe('Type');
+ findDropdownItems().at(0).vm.$emit('click');
+ await nextTick();
+
+ expect(findDropdown().props('text')).toBe('Issue');
+ });
});
it('hides the alert on dismissing the error', async () => {
createComponent({ data: { error: true } });
+
expect(findAlert().exists()).toBe(true);
findAlert().vm.$emit('dismiss');
await nextTick();
+
expect(findAlert().exists()).toBe(false);
});
+ it('displays an initial title if passed', () => {
+ const initialTitle = 'Initial Title';
+ createComponent({
+ props: { initialTitle },
+ });
+ expect(findTitleInput().props('initialTitle')).toBe(initialTitle);
+ });
+
describe('when title input field has a text', () => {
- beforeEach(async () => {
+ beforeEach(() => {
const mockTitle = 'Test title';
createComponent();
- await findTitleInput().vm.$emit('title-input', mockTitle);
+ findTitleInput().vm.$emit('title-input', mockTitle);
});
it('renders a non-disabled Create button', () => {
expect(findCreateButton().props('disabled')).toBe(false);
});
- it('redirects to the work item page on successful mutation', async () => {
- wrapper.find('form').trigger('submit');
- await waitForPromises();
-
- expect(wrapper.vm.$router.push).toHaveBeenCalled();
- });
-
// TODO: write a proper test here when we have a backend implementation
it.todo('shows an alert on mutation error');
});
diff --git a/spec/frontend/work_items/pages/work_item_root_spec.js b/spec/frontend/work_items/pages/work_item_root_spec.js
index ea26b2b4fb3..d0e40680b55 100644
--- a/spec/frontend/work_items/pages/work_item_root_spec.js
+++ b/spec/frontend/work_items/pages/work_item_root_spec.js
@@ -23,7 +23,11 @@ describe('Work items root component', () => {
const findTitle = () => wrapper.findComponent(ItemTitle);
const createComponent = ({ queryResponse = workItemQueryResponse } = {}) => {
- fakeApollo = createMockApollo([], resolvers);
+ fakeApollo = createMockApollo([], resolvers, {
+ possibleTypes: {
+ LocalWorkItemWidget: ['LocalTitleWidget'],
+ },
+ });
fakeApollo.clients.defaultClient.cache.writeQuery({
query: workItemQuery,
variables: {
diff --git a/spec/frontend/work_items/router_spec.js b/spec/frontend/work_items/router_spec.js
index 6017c9d9dbb..c583b5a5d4f 100644
--- a/spec/frontend/work_items/router_spec.js
+++ b/spec/frontend/work_items/router_spec.js
@@ -15,6 +15,16 @@ describe('Work items router', () => {
wrapper = mount(App, {
router,
+ provide: {
+ fullPath: 'full-path',
+ },
+ mocks: {
+ $apollo: {
+ queries: {
+ workItemTypes: {},
+ },
+ },
+ },
});
};
diff --git a/spec/frontend/work_items_hierarchy/components/app_spec.js b/spec/frontend/work_items_hierarchy/components/app_spec.js
new file mode 100644
index 00000000000..092e9c90553
--- /dev/null
+++ b/spec/frontend/work_items_hierarchy/components/app_spec.js
@@ -0,0 +1,63 @@
+import { nextTick } from 'vue';
+import { createLocalVue, mount } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import { GlBanner } from '@gitlab/ui';
+import App from '~/work_items_hierarchy/components/app.vue';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+
+const localVue = createLocalVue();
+localVue.use(VueApollo);
+
+describe('WorkItemsHierarchy App', () => {
+ let wrapper;
+ const createComponent = (props = {}, data = {}) => {
+ wrapper = extendedWrapper(
+ mount(App, {
+ localVue,
+ provide: {
+ illustrationPath: '/foo.svg',
+ licensePlan: 'free',
+ ...props,
+ },
+ data() {
+ return data;
+ },
+ }),
+ );
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('survey banner', () => {
+ it('shows when the banner is visible', () => {
+ createComponent({}, { bannerVisible: true });
+
+ expect(wrapper.find(GlBanner).exists()).toBe(true);
+ });
+
+ it('hide when close is called', async () => {
+ createComponent({}, { bannerVisible: true });
+
+ wrapper.findByTestId('close-icon').trigger('click');
+
+ await nextTick();
+
+ expect(wrapper.find(GlBanner).exists()).toBe(false);
+ });
+ });
+
+ describe('Unavailable structure', () => {
+ it.each`
+ licensePlan | visible
+ ${'free'} | ${true}
+ ${'premium'} | ${true}
+ ${'ultimate'} | ${false}
+ `('visibility is $visible when plan is $licensePlan', ({ licensePlan, visible }) => {
+ createComponent({ licensePlan });
+
+ expect(wrapper.findByTestId('unavailable-structure').exists()).toBe(visible);
+ });
+ });
+});
diff --git a/spec/frontend/work_items_hierarchy/components/hierarchy_spec.js b/spec/frontend/work_items_hierarchy/components/hierarchy_spec.js
new file mode 100644
index 00000000000..74774e38d6b
--- /dev/null
+++ b/spec/frontend/work_items_hierarchy/components/hierarchy_spec.js
@@ -0,0 +1,118 @@
+import { createLocalVue, mount } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import { GlBadge } from '@gitlab/ui';
+import Hierarchy from '~/work_items_hierarchy/components/hierarchy.vue';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import RESPONSE from '~/work_items_hierarchy/static_response';
+import { workItemTypes } from '~/work_items_hierarchy/constants';
+
+const localVue = createLocalVue();
+localVue.use(VueApollo);
+
+describe('WorkItemsHierarchy Hierarchy', () => {
+ let wrapper;
+
+ const workItemsFromResponse = (response) => {
+ return response.reduce(
+ (itemTypes, item) => {
+ const key = item.available ? 'available' : 'unavailable';
+ itemTypes[key].push({
+ ...item,
+ ...workItemTypes[item.type],
+ nestedTypes: item.nestedTypes
+ ? item.nestedTypes.map((type) => workItemTypes[type])
+ : null,
+ });
+ return itemTypes;
+ },
+ { available: [], unavailable: [] },
+ );
+ };
+
+ const createComponent = (props = {}) => {
+ wrapper = extendedWrapper(
+ mount(Hierarchy, {
+ localVue,
+ propsData: {
+ workItemTypes: props.workItemTypes,
+ ...props,
+ },
+ }),
+ );
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('available structure', () => {
+ let items = [];
+
+ beforeEach(() => {
+ items = workItemsFromResponse(RESPONSE.ultimate).available;
+ createComponent({ workItemTypes: items });
+ });
+
+ it('renders all work items', () => {
+ expect(wrapper.findAllByTestId('work-item-wrapper')).toHaveLength(items.length);
+ });
+
+ it('does not render badges', () => {
+ expect(wrapper.find(GlBadge).exists()).toBe(false);
+ });
+ });
+
+ describe('unavailable structure', () => {
+ let items = [];
+
+ beforeEach(() => {
+ items = workItemsFromResponse(RESPONSE.premium).unavailable;
+ createComponent({ workItemTypes: items });
+ });
+
+ it('renders all work items', () => {
+ expect(wrapper.findAllByTestId('work-item-wrapper')).toHaveLength(items.length);
+ });
+
+ it('renders license badges for all work items', () => {
+ expect(wrapper.findAll(GlBadge)).toHaveLength(items.length);
+ });
+
+ it('does not render svg icon for linking', () => {
+ expect(wrapper.findByTestId('hierarchy-rounded-arrow-tail').exists()).toBe(false);
+ expect(wrapper.findByTestId('level-up-icon').exists()).toBe(false);
+ });
+ });
+
+ describe('nested work items', () => {
+ describe.each`
+ licensePlan | arrowTailVisible | levelUpIconVisible | arrowDownIconVisible
+ ${'ultimate'} | ${true} | ${true} | ${true}
+ ${'premium'} | ${false} | ${false} | ${true}
+ ${'free'} | ${false} | ${false} | ${false}
+ `(
+ 'when $licensePlan license',
+ ({ licensePlan, arrowTailVisible, levelUpIconVisible, arrowDownIconVisible }) => {
+ let items = [];
+ beforeEach(() => {
+ items = workItemsFromResponse(RESPONSE[licensePlan]).available;
+ createComponent({ workItemTypes: items });
+ });
+
+ it(`${arrowTailVisible ? 'render' : 'does not render'} arrow tail svg`, () => {
+ expect(wrapper.findByTestId('hierarchy-rounded-arrow-tail').exists()).toBe(
+ arrowTailVisible,
+ );
+ });
+
+ it(`${levelUpIconVisible ? 'render' : 'does not render'} arrow tail svg`, () => {
+ expect(wrapper.findByTestId('level-up-icon').exists()).toBe(levelUpIconVisible);
+ });
+
+ it(`${arrowDownIconVisible ? 'render' : 'does not render'} arrow tail svg`, () => {
+ expect(wrapper.findByTestId('arrow-down-icon').exists()).toBe(arrowDownIconVisible);
+ });
+ },
+ );
+ });
+});
diff --git a/spec/frontend/work_items_hierarchy/hierarchy_util_spec.js b/spec/frontend/work_items_hierarchy/hierarchy_util_spec.js
new file mode 100644
index 00000000000..9042fa27d16
--- /dev/null
+++ b/spec/frontend/work_items_hierarchy/hierarchy_util_spec.js
@@ -0,0 +1,16 @@
+import { inferLicensePlan } from '~/work_items_hierarchy/hierarchy_util';
+import { LICENSE_PLAN } from '~/work_items_hierarchy/constants';
+
+describe('inferLicensePlan', () => {
+ it.each`
+ epics | subEpics | licensePlan
+ ${true} | ${true} | ${LICENSE_PLAN.ULTIMATE}
+ ${true} | ${false} | ${LICENSE_PLAN.PREMIUM}
+ ${false} | ${false} | ${LICENSE_PLAN.FREE}
+ `(
+ 'returns $licensePlan when epic is $epics and sub-epic is $subEpics',
+ ({ epics, subEpics, licensePlan }) => {
+ expect(inferLicensePlan({ hasEpics: epics, hasSubEpics: subEpics })).toBe(licensePlan);
+ },
+ );
+});
diff --git a/spec/frontend/zen_mode_spec.js b/spec/frontend/zen_mode_spec.js
index 13f221fd9d9..44684619fae 100644
--- a/spec/frontend/zen_mode_spec.js
+++ b/spec/frontend/zen_mode_spec.js
@@ -45,6 +45,8 @@ describe('ZenMode', () => {
// Set this manually because we can't actually scroll the window
zen.scroll_position = 456;
+
+ gon.features = { markdownContinueLists: true };
});
describe('enabling dropzone', () => {
diff --git a/spec/frontend_integration/ide/ide_integration_spec.js b/spec/frontend_integration/ide/ide_integration_spec.js
index 5f1a5b0d048..aad9b9e526c 100644
--- a/spec/frontend_integration/ide/ide_integration_spec.js
+++ b/spec/frontend_integration/ide/ide_integration_spec.js
@@ -1,3 +1,4 @@
+import { nextTick } from 'vue';
import { setTestTimeout } from 'helpers/timeout';
import waitForPromises from 'helpers/wait_for_promises';
import { waitForText } from 'helpers/wait_for_text';
@@ -134,7 +135,7 @@ describe('WebIDE', () => {
describe('when editor position changes', () => {
beforeEach(async () => {
editor.setPosition({ lineNumber: 4, column: 10 });
- await vm.$nextTick();
+ await nextTick();
});
it('shows new line position', () => {
@@ -145,7 +146,7 @@ describe('WebIDE', () => {
it('updates after rename', async () => {
await ideHelper.renameFile('README.md', 'READMEZ.txt');
await ideHelper.waitForEditorModelChange(editor);
- await vm.$nextTick();
+ await nextTick();
expect(statusBar).toHaveText('1:1');
expect(statusBar).toHaveText('plaintext');
@@ -166,7 +167,7 @@ describe('WebIDE', () => {
await ideHelper.closeFile('README.md');
await ideHelper.openFile('README.md');
await ideHelper.waitForMonacoEditor();
- await vm.$nextTick();
+ await nextTick();
expect(statusBar).toHaveText('4:10');
expect(statusBar).toHaveText('markdown');
diff --git a/spec/graphql/features/authorization_spec.rb b/spec/graphql/features/authorization_spec.rb
index faf19104731..514f63a6f5a 100644
--- a/spec/graphql/features/authorization_spec.rb
+++ b/spec/graphql/features/authorization_spec.rb
@@ -326,7 +326,7 @@ RSpec.describe 'DeclarativePolicy authorization in GraphQL ' do
let!(:other_project) { create(:project, :private) }
let!(:visible_issues) { create_list(:issue, 2, project: visible_project) }
let!(:other_issues) { create_list(:issue, 2, project: other_project) }
- let!(:user) { visible_project.owner }
+ let!(:user) { visible_project.first_owner }
let(:issue_type) do
type_factory do |type|
diff --git a/spec/graphql/graphql_triggers_spec.rb b/spec/graphql/graphql_triggers_spec.rb
index 0b53c633077..2d83edca363 100644
--- a/spec/graphql/graphql_triggers_spec.rb
+++ b/spec/graphql/graphql_triggers_spec.rb
@@ -17,4 +17,18 @@ RSpec.describe GraphqlTriggers do
GraphqlTriggers.issuable_assignees_updated(issue)
end
end
+
+ describe '.issuable_title_updated' do
+ it 'triggers the issuableTitleUpdated subscription' do
+ work_item = create(:work_item)
+
+ expect(GitlabSchema.subscriptions).to receive(:trigger).with(
+ 'issuableTitleUpdated',
+ { issuable_id: work_item.to_gid },
+ work_item
+ ).and_call_original
+
+ GraphqlTriggers.issuable_title_updated(work_item)
+ end
+ end
end
diff --git a/spec/graphql/mutations/alert_management/alerts/todo/create_spec.rb b/spec/graphql/mutations/alert_management/alerts/todo/create_spec.rb
index 8ec99070c91..ea5e21ec4b8 100644
--- a/spec/graphql/mutations/alert_management/alerts/todo/create_spec.rb
+++ b/spec/graphql/mutations/alert_management/alerts/todo/create_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe Mutations::AlertManagement::Alerts::Todo::Create do
let_it_be(:alert) { create(:alert_management_alert) }
let_it_be(:project) { alert.project }
- let(:current_user) { project.owner }
+ let(:current_user) { project.first_owner }
let(:args) { { project_path: project.full_path, iid: alert.iid } }
diff --git a/spec/graphql/mutations/ci/runner/delete_spec.rb b/spec/graphql/mutations/ci/runner/delete_spec.rb
index 9f30c95edd5..b53ee30f826 100644
--- a/spec/graphql/mutations/ci/runner/delete_spec.rb
+++ b/spec/graphql/mutations/ci/runner/delete_spec.rb
@@ -11,9 +11,7 @@ RSpec.describe Mutations::Ci::Runner::Delete do
let(:current_ctx) { { current_user: user } }
let(:mutation_params) do
- {
- id: runner.to_global_id
- }
+ { id: runner.to_global_id }
end
specify { expect(described_class).to require_graphql_authorizations(:delete_runner) }
@@ -57,6 +55,10 @@ RSpec.describe Mutations::Ci::Runner::Delete do
it 'deletes runner' do
mutation_params[:id] = project_runner.to_global_id
+ expect_next_instance_of(::Ci::UnregisterRunnerService, project_runner) do |service|
+ expect(service).to receive(:execute).once.and_call_original
+ end
+
expect { subject }.to change { Ci::Runner.count }.by(-1)
expect(subject[:errors]).to be_empty
end
@@ -73,6 +75,9 @@ RSpec.describe Mutations::Ci::Runner::Delete do
it 'does not delete project runner' do
mutation_params[:id] = two_projects_runner.to_global_id
+ allow_next_instance_of(::Ci::UnregisterRunnerService) do |service|
+ expect(service).not_to receive(:execute).once
+ end
expect { subject }.not_to change { Ci::Runner.count }
expect(subject[:errors]).to contain_exactly("Runner #{two_projects_runner.to_global_id} associated with more than one project")
end
@@ -84,6 +89,10 @@ RSpec.describe Mutations::Ci::Runner::Delete do
let(:current_ctx) { { current_user: admin_user } }
it 'deletes runner' do
+ expect_next_instance_of(::Ci::UnregisterRunnerService, runner) do |service|
+ expect(service).to receive(:execute).once.and_call_original
+ end
+
expect { subject }.to change { Ci::Runner.count }.by(-1)
expect(subject[:errors]).to be_empty
end
diff --git a/spec/graphql/mutations/issues/create_spec.rb b/spec/graphql/mutations/issues/create_spec.rb
index 825d04ff827..e3094e84703 100644
--- a/spec/graphql/mutations/issues/create_spec.rb
+++ b/spec/graphql/mutations/issues/create_spec.rb
@@ -121,7 +121,7 @@ RSpec.describe Mutations::Issues::Create do
end
context 'when creating an issue as owner' do
- let_it_be(:user) { project.owner }
+ let_it_be(:user) { project.first_owner }
before do
mutation_params.merge!(special_params)
diff --git a/spec/graphql/resolvers/ci/project_pipeline_counts_resolver_spec.rb b/spec/graphql/resolvers/ci/project_pipeline_counts_resolver_spec.rb
new file mode 100644
index 00000000000..07b4a5509b2
--- /dev/null
+++ b/spec/graphql/resolvers/ci/project_pipeline_counts_resolver_spec.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::Ci::ProjectPipelineCountsResolver do
+ include GraphqlHelpers
+
+ let(:current_user) { create(:user) }
+
+ let_it_be(:project) { create(:project, :private) }
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+ let_it_be(:failed_pipeline) { create(:ci_pipeline, :failed, project: project) }
+ let_it_be(:success_pipeline) { create(:ci_pipeline, :success, project: project) }
+ let_it_be(:ref_pipeline) { create(:ci_pipeline, project: project, ref: 'awesome-feature') }
+ let_it_be(:sha_pipeline) { create(:ci_pipeline, :running, project: project, sha: 'deadbeef') }
+ let_it_be(:on_demand_dast_scan) { create(:ci_pipeline, :success, project: project, source: 'ondemand_dast_scan') }
+
+ before do
+ project.add_developer(current_user)
+ end
+
+ describe '#resolve' do
+ it 'counts pipelines' do
+ expect(resolve_pipeline_counts).to have_attributes(
+ all: 6,
+ finished: 3,
+ running: 1,
+ pending: 2
+ )
+ end
+
+ it 'counts by ref' do
+ expect(resolve_pipeline_counts(ref: "awesome-feature")).to have_attributes(
+ all: 1,
+ finished: 0,
+ running: 0,
+ pending: 1
+ )
+ end
+
+ it 'counts by sha' do
+ expect(resolve_pipeline_counts(sha: "deadbeef")).to have_attributes(
+ all: 1,
+ finished: 0,
+ running: 1,
+ pending: 0
+ )
+ end
+
+ it 'counts by source' do
+ expect(resolve_pipeline_counts(source: "ondemand_dast_scan")).to have_attributes(
+ all: 1,
+ finished: 1,
+ running: 0,
+ pending: 0
+ )
+ end
+ end
+
+ def resolve_pipeline_counts(args = {}, context = { current_user: current_user })
+ resolve(described_class, obj: project, args: args, ctx: context)
+ end
+end
diff --git a/spec/graphql/resolvers/ci/runner_jobs_resolver_spec.rb b/spec/graphql/resolvers/ci/runner_jobs_resolver_spec.rb
new file mode 100644
index 00000000000..53b673e255b
--- /dev/null
+++ b/spec/graphql/resolvers/ci/runner_jobs_resolver_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::Ci::RunnerJobsResolver do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+ let_it_be(:irrelevant_pipeline) { create(:ci_pipeline, project: project) }
+
+ let!(:build_one) { create(:ci_build, :success, name: 'Build One', runner: runner, pipeline: pipeline) }
+ let!(:build_two) { create(:ci_build, :success, name: 'Build Two', runner: runner, pipeline: pipeline) }
+ let!(:build_three) { create(:ci_build, :failed, name: 'Build Three', runner: runner, pipeline: pipeline) }
+ let!(:irrelevant_build) { create(:ci_build, name: 'Irrelevant Build', pipeline: irrelevant_pipeline)}
+
+ let(:args) { {} }
+ let(:runner) { create(:ci_runner, :project, projects: [project]) }
+
+ subject { resolve_jobs(args) }
+
+ describe '#resolve' do
+ context 'with authorized user', :enable_admin_mode do
+ let(:current_user) { create(:user, :admin) }
+
+ context 'with statuses argument' do
+ let(:args) { { statuses: [Types::Ci::JobStatusEnum.coerce_isolated_input('SUCCESS')] } }
+
+ it { is_expected.to contain_exactly(build_one, build_two) }
+ end
+
+ context 'without statuses argument' do
+ it { is_expected.to contain_exactly(build_one, build_two, build_three) }
+ end
+ end
+
+ context 'with unauthorized user' do
+ let(:current_user) { nil }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ private
+
+ def resolve_jobs(args = {}, context = { current_user: current_user })
+ resolve(described_class, obj: runner, args: args, ctx: context)
+ end
+end
diff --git a/spec/graphql/resolvers/ci/runners_resolver_spec.rb b/spec/graphql/resolvers/ci/runners_resolver_spec.rb
index df6490df915..9251fbf24d9 100644
--- a/spec/graphql/resolvers/ci/runners_resolver_spec.rb
+++ b/spec/graphql/resolvers/ci/runners_resolver_spec.rb
@@ -43,34 +43,99 @@ RSpec.describe Resolvers::Ci::RunnersResolver do
# Only thing we can do is to verify that args from the resolver is correctly transformed to params of the Finder and we return the Finder's result back.
describe 'Allowed query arguments' do
let(:finder) { instance_double(::Ci::RunnersFinder) }
- let(:args) do
- {
- active: true,
- status: 'active',
- type: :instance_type,
- tag_list: ['active_runner'],
- search: 'abc',
- sort: :contacted_asc
- }
+
+ context 'with active filter' do
+ let(:args) do
+ {
+ active: true,
+ status: 'active',
+ type: :instance_type,
+ tag_list: ['active_runner'],
+ search: 'abc',
+ sort: :contacted_asc
+ }
+ end
+
+ let(:expected_params) do
+ {
+ active: true,
+ status_status: 'active',
+ type_type: :instance_type,
+ tag_name: ['active_runner'],
+ preload: { tag_name: nil },
+ search: 'abc',
+ sort: 'contacted_asc'
+ }
+ end
+
+ it 'calls RunnersFinder with expected arguments' do
+ expect(::Ci::RunnersFinder).to receive(:new).with(current_user: user, params: expected_params).once.and_return(finder)
+ allow(finder).to receive(:execute).once.and_return([:execute_return_value])
+
+ expect(subject.items.to_a).to eq([:execute_return_value])
+ end
+ end
+
+ context 'with both active and paused filter' do
+ let(:args) do
+ {
+ active: true,
+ paused: true
+ }
+ end
+
+ let(:expected_params) do
+ {
+ active: false,
+ preload: { tag_name: nil }
+ }
+ end
+
+ it 'calls RunnersFinder with expected arguments' do
+ expect(::Ci::RunnersFinder).to receive(:new).with(current_user: user, params: expected_params).once.and_return(finder)
+ allow(finder).to receive(:execute).once.and_return([:execute_return_value])
+
+ expect(subject.items.to_a).to eq([:execute_return_value])
+ end
end
- let(:expected_params) do
- {
- active: true,
- status_status: 'active',
- type_type: :instance_type,
- tag_name: ['active_runner'],
- preload: { tag_name: nil },
- search: 'abc',
- sort: 'contacted_asc'
- }
+ context 'with paused filter' do
+ let(:args) do
+ { paused: true }
+ end
+
+ let(:expected_params) do
+ {
+ active: false,
+ preload: { tag_name: nil }
+ }
+ end
+
+ it 'calls RunnersFinder with expected arguments' do
+ expect(::Ci::RunnersFinder).to receive(:new).with(current_user: user, params: expected_params).once.and_return(finder)
+ allow(finder).to receive(:execute).once.and_return([:execute_return_value])
+
+ expect(subject.items.to_a).to eq([:execute_return_value])
+ end
end
- it 'calls RunnersFinder with expected arguments' do
- allow(::Ci::RunnersFinder).to receive(:new).with(current_user: user, params: expected_params).once.and_return(finder)
- allow(finder).to receive(:execute).once.and_return([:execute_return_value])
+ context 'with neither paused or active filters' do
+ let(:args) do
+ {}
+ end
+
+ let(:expected_params) do
+ {
+ preload: { tag_name: nil }
+ }
+ end
+
+ it 'calls RunnersFinder with expected arguments' do
+ expect(::Ci::RunnersFinder).to receive(:new).with(current_user: user, params: expected_params).once.and_return(finder)
+ allow(finder).to receive(:execute).once.and_return([:execute_return_value])
- expect(subject.items.to_a).to eq([:execute_return_value])
+ expect(subject.items.to_a).to eq([:execute_return_value])
+ end
end
end
end
diff --git a/spec/graphql/resolvers/clusters/agent_tokens_resolver_spec.rb b/spec/graphql/resolvers/clusters/agent_tokens_resolver_spec.rb
index 9b54d466681..866f4ce7b5a 100644
--- a/spec/graphql/resolvers/clusters/agent_tokens_resolver_spec.rb
+++ b/spec/graphql/resolvers/clusters/agent_tokens_resolver_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe Resolvers::Clusters::AgentTokensResolver do
describe '#resolve' do
let(:agent) { create(:cluster_agent) }
- let(:user) { create(:user, maintainer_projects: [agent.project]) }
+ let(:user) { create(:user, developer_projects: [agent.project]) }
let(:ctx) { Hash(current_user: user) }
let!(:matching_token1) { create(:cluster_agent_token, agent: agent, last_used_at: 5.days.ago) }
@@ -33,7 +33,11 @@ RSpec.describe Resolvers::Clusters::AgentTokensResolver do
end
context 'user does not have permission' do
- let(:user) { create(:user, developer_projects: [agent.project]) }
+ let(:user) { create(:user) }
+
+ before do
+ agent.project.add_reporter(user)
+ end
it { is_expected.to be_empty }
end
diff --git a/spec/graphql/resolvers/clusters/agents_resolver_spec.rb b/spec/graphql/resolvers/clusters/agents_resolver_spec.rb
index 70f40748e1d..152d7fa22c4 100644
--- a/spec/graphql/resolvers/clusters/agents_resolver_spec.rb
+++ b/spec/graphql/resolvers/clusters/agents_resolver_spec.rb
@@ -15,10 +15,14 @@ RSpec.describe Resolvers::Clusters::AgentsResolver do
describe '#resolve' do
let_it_be(:project) { create(:project) }
- let_it_be(:maintainer) { create(:user, maintainer_projects: [project]) }
- let_it_be(:developer) { create(:user, developer_projects: [project]) }
+ let_it_be(:maintainer) { create(:user, developer_projects: [project]) }
+ let_it_be(:reporter) { create(:user) }
let_it_be(:agents) { create_list(:cluster_agent, 2, project: project) }
+ before do
+ project.add_reporter(reporter)
+ end
+
let(:ctx) { { current_user: current_user } }
subject { resolve_agents }
@@ -32,7 +36,7 @@ RSpec.describe Resolvers::Clusters::AgentsResolver do
end
context 'the current user does not have access to clusters' do
- let(:current_user) { developer }
+ let(:current_user) { reporter }
it 'returns an empty result' do
expect(subject).to be_empty
diff --git a/spec/graphql/resolvers/merge_requests_resolver_spec.rb b/spec/graphql/resolvers/merge_requests_resolver_spec.rb
index 1d0eac30a23..e4eaeb9bc3c 100644
--- a/spec/graphql/resolvers/merge_requests_resolver_spec.rb
+++ b/spec/graphql/resolvers/merge_requests_resolver_spec.rb
@@ -321,7 +321,7 @@ RSpec.describe Resolvers::MergeRequestsResolver do
end
describe 'sorting' do
- let(:mrs) do
+ let_it_be(:mrs) do
[
merge_request_with_milestone, merge_request_6, merge_request_5, merge_request_4,
merge_request_3, merge_request_2, merge_request_1
@@ -363,28 +363,44 @@ RSpec.describe Resolvers::MergeRequestsResolver do
def merged_at(mr)
nils_last(mr.metrics.merged_at)
end
+ end
+
+ context 'when sorting by closed at' do
+ before do
+ merge_request_1.metrics.update!(latest_closed_at: 10.days.ago)
+ merge_request_3.metrics.update!(latest_closed_at: 5.days.ago)
+ end
+
+ it 'sorts merge requests ascending' do
+ expect(resolve_mr(project, sort: :closed_at_asc))
+ .to match_array(mrs)
+ .and be_sorted(->(mr) { [closed_at(mr), -mr.id] })
+ end
+
+ it 'sorts merge requests descending' do
+ expect(resolve_mr(project, sort: :closed_at_desc))
+ .to match_array(mrs)
+ .and be_sorted(->(mr) { [-closed_at(mr), -mr.id] })
+ end
+
+ def closed_at(mr)
+ nils_last(mr.metrics.latest_closed_at)
+ end
+ end
+
+ context 'when sorting by title' do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:mr1) { create(:merge_request, :unique_branches, title: 'foo', source_project: project) }
+ let_it_be(:mr2) { create(:merge_request, :unique_branches, title: 'bar', source_project: project) }
+ let_it_be(:mr3) { create(:merge_request, :unique_branches, title: 'baz', source_project: project) }
+ let_it_be(:mr4) { create(:merge_request, :unique_branches, title: 'Baz 2', source_project: project) }
+
+ it 'sorts issues ascending' do
+ expect(resolve_mr(project, sort: :title_asc).to_a).to eq [mr2, mr3, mr4, mr1]
+ end
- context 'when sorting by closed at' do
- before do
- merge_request_1.metrics.update!(latest_closed_at: 10.days.ago)
- merge_request_3.metrics.update!(latest_closed_at: 5.days.ago)
- end
-
- it 'sorts merge requests ascending' do
- expect(resolve_mr(project, sort: :closed_at_asc))
- .to match_array(mrs)
- .and be_sorted(->(mr) { [closed_at(mr), -mr.id] })
- end
-
- it 'sorts merge requests descending' do
- expect(resolve_mr(project, sort: :closed_at_desc))
- .to match_array(mrs)
- .and be_sorted(->(mr) { [-closed_at(mr), -mr.id] })
- end
-
- def closed_at(mr)
- nils_last(mr.metrics.latest_closed_at)
- end
+ it 'sorts issues descending' do
+ expect(resolve_mr(project, sort: :title_desc).to_a).to eq [mr1, mr4, mr3, mr2]
end
end
end
diff --git a/spec/graphql/resolvers/package_details_resolver_spec.rb b/spec/graphql/resolvers/package_details_resolver_spec.rb
index d6acb31d4e3..c8ee489a034 100644
--- a/spec/graphql/resolvers/package_details_resolver_spec.rb
+++ b/spec/graphql/resolvers/package_details_resolver_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe Resolvers::PackageDetailsResolver do
include GraphqlHelpers
let_it_be_with_reload(:project) { create(:project) }
- let_it_be(:user) { project.owner }
+ let_it_be(:user) { project.first_owner }
let_it_be(:package) { create(:composer_package, project: project) }
describe '#resolve' do
diff --git a/spec/graphql/resolvers/package_pipelines_resolver_spec.rb b/spec/graphql/resolvers/package_pipelines_resolver_spec.rb
index d48d4d8ae01..892dc641201 100644
--- a/spec/graphql/resolvers/package_pipelines_resolver_spec.rb
+++ b/spec/graphql/resolvers/package_pipelines_resolver_spec.rb
@@ -8,15 +8,16 @@ RSpec.describe Resolvers::PackagePipelinesResolver do
let_it_be_with_reload(:package) { create(:package) }
let_it_be(:pipelines) { create_list(:ci_pipeline, 3, project: package.project) }
- let(:user) { package.project.owner }
+ let(:user) { package.project.first_owner }
let(:args) { {} }
describe '#resolve' do
subject { resolve(described_class, obj: package, args: args, ctx: { current_user: user }) }
before do
- package.pipelines = pipelines
- package.save!
+ pipelines.each do |pipeline|
+ create(:package_build_info, package: package, pipeline: pipeline)
+ end
end
it { is_expected.to contain_exactly(*pipelines) }
diff --git a/spec/graphql/resolvers/recent_boards_resolver_spec.rb b/spec/graphql/resolvers/recent_boards_resolver_spec.rb
new file mode 100644
index 00000000000..1afdcd42b4f
--- /dev/null
+++ b/spec/graphql/resolvers/recent_boards_resolver_spec.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::RecentBoardsResolver do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+
+ shared_examples_for 'group and project recent boards resolver' do
+ let_it_be(:board1) { create(:board, name: 'One', resource_parent: board_parent) }
+ let_it_be(:board2) { create(:board, name: 'Two', resource_parent: board_parent) }
+
+ before do
+ [board1, board2].each { |board| visit_board(board, board_parent) }
+ end
+
+ it 'calls ::Boards::VisitsFinder' do
+ expect_any_instance_of(::Boards::VisitsFinder) do |finder|
+ expect(finder).to receive(:latest)
+ end
+
+ resolve_recent_boards
+ end
+
+ it 'avoids N+1 queries' do
+ control = ActiveRecord::QueryRecorder.new { resolve_recent_boards }
+
+ board3 = create(:board, resource_parent: board_parent)
+ visit_board(board3, board_parent)
+
+ expect { resolve_recent_boards(args: {}) }.not_to exceed_query_limit(control)
+ end
+
+ it 'returns most recent visited boards' do
+ expect(resolve_recent_boards).to match_array [board2, board1]
+ end
+
+ it 'returns a set number of boards' do
+ stub_const('Board::RECENT_BOARDS_SIZE', 1)
+
+ expect(resolve_recent_boards).to match_array [board2]
+ end
+ end
+
+ describe '#resolve' do
+ context 'when there is no parent' do
+ let_it_be(:board_parent) { nil }
+
+ it 'returns none if parent is nil' do
+ expect(resolve_recent_boards).to eq(Board.none)
+ end
+ end
+
+ context 'when project boards' do
+ let_it_be(:board_parent) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) }
+
+ it_behaves_like 'group and project recent boards resolver'
+ end
+
+ context 'when group boards' do
+ let_it_be(:board_parent) { create(:group) }
+
+ it_behaves_like 'group and project recent boards resolver'
+ end
+ end
+
+ def resolve_recent_boards(args: {})
+ resolve(described_class, obj: board_parent, args: args, ctx: { current_user: user })
+ end
+
+ def visit_board(board, parent)
+ if parent.is_a?(Group)
+ create(:board_group_recent_visit, group: parent, board: board, user: user)
+ else
+ create(:board_project_recent_visit, project: parent, board: board, user: user)
+ end
+ end
+end
diff --git a/spec/graphql/types/ci/pipeline_counts_type_spec.rb b/spec/graphql/types/ci/pipeline_counts_type_spec.rb
new file mode 100644
index 00000000000..7fdb286d253
--- /dev/null
+++ b/spec/graphql/types/ci/pipeline_counts_type_spec.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['PipelineCounts'] do
+ include GraphqlHelpers
+
+ let(:current_user) { create(:user) }
+
+ let_it_be(:project) { create(:project, :private) }
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+ let_it_be(:failed_pipeline) { create(:ci_pipeline, :failed, project: project) }
+ let_it_be(:success_pipeline) { create(:ci_pipeline, :success, project: project) }
+ let_it_be(:ref_pipeline) { create(:ci_pipeline, project: project, ref: 'awesome-feature') }
+ let_it_be(:sha_pipeline) { create(:ci_pipeline, :running, project: project, sha: 'deadbeef') }
+ let_it_be(:on_demand_dast_scan) { create(:ci_pipeline, :success, project: project, source: 'ondemand_dast_scan') }
+
+ before do
+ project.add_developer(current_user)
+ end
+
+ specify { expect(described_class.graphql_name).to eq('PipelineCounts') }
+
+ it 'has the expected fields' do
+ expected_fields = %w[
+ all
+ finished
+ pending
+ running
+ ]
+
+ expect(described_class).to include_graphql_fields(*expected_fields)
+ end
+
+ shared_examples 'pipeline counts query' do |args: "", expected_counts:|
+ let_it_be(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ pipelineCounts#{args} {
+ all
+ finished
+ pending
+ running
+ }
+ }
+ }
+ )
+ end
+
+ subject { GitlabSchema.execute(query, context: { current_user: current_user }).as_json }
+
+ it 'returns pipeline counts' do
+ actual_counts = subject.dig('data', 'project', 'pipelineCounts')
+
+ expect(actual_counts).to eq(expected_counts)
+ end
+ end
+
+ it_behaves_like "pipeline counts query", args: "", expected_counts: {
+ "all" => 6,
+ "finished" => 3,
+ "pending" => 2,
+ "running" => 1
+ }
+
+ it_behaves_like "pipeline counts query", args: '(ref: "awesome-feature")', expected_counts: {
+ "all" => 1,
+ "finished" => 0,
+ "pending" => 1,
+ "running" => 0
+ }
+
+ it_behaves_like "pipeline counts query", args: '(sha: "deadbeef")', expected_counts: {
+ "all" => 1,
+ "finished" => 0,
+ "pending" => 0,
+ "running" => 1
+ }
+
+ it_behaves_like "pipeline counts query", args: '(source: "ondemand_dast_scan")', expected_counts: {
+ "all" => 1,
+ "finished" => 1,
+ "pending" => 0,
+ "running" => 0
+ }
+end
diff --git a/spec/graphql/types/ci/runner_type_spec.rb b/spec/graphql/types/ci/runner_type_spec.rb
index 43d8b585d6b..7697cd0ef79 100644
--- a/spec/graphql/types/ci/runner_type_spec.rb
+++ b/spec/graphql/types/ci/runner_type_spec.rb
@@ -9,9 +9,10 @@ RSpec.describe GitlabSchema.types['CiRunner'] do
it 'contains attributes related to a runner' do
expected_fields = %w[
- id description created_at contacted_at maximum_timeout access_level active status
+ id description created_at contacted_at maximum_timeout access_level active paused status
version short_sha revision locked run_untagged ip_address runner_type tag_list
project_count job_count admin_url edit_admin_url user_permissions executor_name
+ groups projects jobs token_expires_at
]
expect(described_class).to include_graphql_fields(*expected_fields)
diff --git a/spec/graphql/types/clusters/agent_activity_event_type_spec.rb b/spec/graphql/types/clusters/agent_activity_event_type_spec.rb
index 7773bad749d..cae75485846 100644
--- a/spec/graphql/types/clusters/agent_activity_event_type_spec.rb
+++ b/spec/graphql/types/clusters/agent_activity_event_type_spec.rb
@@ -6,6 +6,6 @@ RSpec.describe GitlabSchema.types['ClusterAgentActivityEvent'] do
let(:fields) { %i[recorded_at kind level user agent_token] }
it { expect(described_class.graphql_name).to eq('ClusterAgentActivityEvent') }
- it { expect(described_class).to require_graphql_authorizations(:admin_cluster) }
+ it { expect(described_class).to require_graphql_authorizations(:read_cluster) }
it { expect(described_class).to have_graphql_fields(fields) }
end
diff --git a/spec/graphql/types/clusters/agent_token_type_spec.rb b/spec/graphql/types/clusters/agent_token_type_spec.rb
index 3f0720cb4b5..1ca6d690c80 100644
--- a/spec/graphql/types/clusters/agent_token_type_spec.rb
+++ b/spec/graphql/types/clusters/agent_token_type_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe GitlabSchema.types['ClusterAgentToken'] do
it { expect(described_class.graphql_name).to eq('ClusterAgentToken') }
- it { expect(described_class).to require_graphql_authorizations(:admin_cluster) }
+ it { expect(described_class).to require_graphql_authorizations(:read_cluster) }
it { expect(described_class).to have_graphql_fields(fields) }
end
diff --git a/spec/graphql/types/clusters/agent_type_spec.rb b/spec/graphql/types/clusters/agent_type_spec.rb
index a1e5952bf73..3f4faccf15d 100644
--- a/spec/graphql/types/clusters/agent_type_spec.rb
+++ b/spec/graphql/types/clusters/agent_type_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe GitlabSchema.types['ClusterAgent'] do
it { expect(described_class.graphql_name).to eq('ClusterAgent') }
- it { expect(described_class).to require_graphql_authorizations(:admin_cluster) }
+ it { expect(described_class).to require_graphql_authorizations(:read_cluster) }
it { expect(described_class).to have_graphql_fields(fields) }
end
diff --git a/spec/graphql/types/global_id_type_spec.rb b/spec/graphql/types/global_id_type_spec.rb
index 4efa3018dad..e7e69cfad9e 100644
--- a/spec/graphql/types/global_id_type_spec.rb
+++ b/spec/graphql/types/global_id_type_spec.rb
@@ -191,7 +191,7 @@ RSpec.describe Types::GlobalIDType do
describe 'executing against the schema' do
let(:query_result) do
- context = { current_user: issue.project.owner }
+ context = { current_user: issue.project.first_owner }
variables = { 'id' => gid }
run_with_clean_state(query, context: context, variables: variables).to_h
diff --git a/spec/graphql/types/group_type_spec.rb b/spec/graphql/types/group_type_spec.rb
index 0ba322a100a..82703948cea 100644
--- a/spec/graphql/types/group_type_spec.rb
+++ b/spec/graphql/types/group_type_spec.rb
@@ -23,6 +23,7 @@ RSpec.describe GitlabSchema.types['Group'] do
dependency_proxy_blob_count dependency_proxy_total_size
dependency_proxy_image_prefix dependency_proxy_image_ttl_policy
shared_runners_setting timelogs organizations contacts work_item_types
+ recent_issue_boards
]
expect(described_class).to include_graphql_fields(*expected_fields)
diff --git a/spec/graphql/types/issuable_type_spec.rb b/spec/graphql/types/issuable_type_spec.rb
index 992a58f524b..cb18bbe2eab 100644
--- a/spec/graphql/types/issuable_type_spec.rb
+++ b/spec/graphql/types/issuable_type_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe GitlabSchema.types['Issuable'] do
it 'returns possible types' do
- expect(described_class.possible_types).to include(Types::IssueType, Types::MergeRequestType)
+ expect(described_class.possible_types).to include(Types::IssueType, Types::MergeRequestType, Types::WorkItemType)
end
describe '.resolve_type' do
@@ -16,6 +16,10 @@ RSpec.describe GitlabSchema.types['Issuable'] do
expect(described_class.resolve_type(build(:merge_request), {})).to eq(Types::MergeRequestType)
end
+ it 'resolves work items' do
+ expect(described_class.resolve_type(build(:work_item), {})).to eq(Types::WorkItemType)
+ end
+
it 'raises an error for invalid types' do
expect { described_class.resolve_type(build(:user), {}) }.to raise_error 'Unsupported issuable type'
end
diff --git a/spec/graphql/types/member_interface_spec.rb b/spec/graphql/types/member_interface_spec.rb
index 11fd09eb335..8ecaaa46bed 100644
--- a/spec/graphql/types/member_interface_spec.rb
+++ b/spec/graphql/types/member_interface_spec.rb
@@ -12,6 +12,7 @@ RSpec.describe Types::MemberInterface do
updated_at
expires_at
user
+ merge_request_interaction
]
expect(described_class).to have_graphql_fields(*expected_fields)
@@ -40,4 +41,16 @@ RSpec.describe Types::MemberInterface do
end
end
end
+
+ describe '#merge_request_interaction' do
+ subject { described_class.fields['mergeRequestInteraction'] }
+
+ it 'returns the correct type' do
+ is_expected.to have_graphql_type(Types::UserMergeRequestInteractionType)
+ end
+
+ it 'has the correct arguments' do
+ expect(subject.arguments).to have_key('id')
+ end
+ end
end
diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb
index 961e12288d4..7433d465b38 100644
--- a/spec/graphql/types/project_type_spec.rb
+++ b/spec/graphql/types/project_type_spec.rb
@@ -25,7 +25,7 @@ RSpec.describe GitlabSchema.types['Project'] do
only_allow_merge_if_pipeline_succeeds request_access_enabled
only_allow_merge_if_all_discussions_are_resolved printing_merge_request_link_enabled
namespace group statistics repository merge_requests merge_request issues
- issue milestones pipelines removeSourceBranchAfterMerge sentryDetailedError snippets
+ issue milestones pipelines removeSourceBranchAfterMerge pipeline_counts sentryDetailedError snippets
grafanaIntegration autocloseReferencedIssues suggestion_commit_message environments
environment boards jira_import_status jira_imports services releases release
alert_management_alerts alert_management_alert alert_management_alert_status_counts
@@ -35,6 +35,7 @@ RSpec.describe GitlabSchema.types['Project'] do
pipeline_analytics squash_read_only sast_ci_configuration
cluster_agent cluster_agents agent_configurations
ci_template timelogs merge_commit_template squash_commit_template work_item_types
+ recent_issue_boards ci_config_path_or_default
]
expect(described_class).to include_graphql_fields(*expected_fields)
@@ -299,6 +300,8 @@ RSpec.describe GitlabSchema.types['Project'] do
:merged_before,
:created_after,
:created_before,
+ :updated_after,
+ :updated_before,
:author_username,
:assignee_username,
:reviewer_username,
@@ -309,6 +312,13 @@ RSpec.describe GitlabSchema.types['Project'] do
end
end
+ describe 'pipelineCounts field' do
+ subject { described_class.fields['pipelineCounts'] }
+
+ it { is_expected.to have_graphql_type(Types::Ci::PipelineCountsType) }
+ it { is_expected.to have_graphql_resolver(Resolvers::Ci::ProjectPipelineCountsResolver) }
+ end
+
describe 'snippets field' do
subject { described_class.fields['snippets'] }
diff --git a/spec/graphql/types/repository/blob_type_spec.rb b/spec/graphql/types/repository/blob_type_spec.rb
index 8d845e5d814..565341d15b9 100644
--- a/spec/graphql/types/repository/blob_type_spec.rb
+++ b/spec/graphql/types/repository/blob_type_spec.rb
@@ -29,6 +29,8 @@ RSpec.describe Types::Repository::BlobType do
:blame_path,
:history_path,
:permalink_path,
+ :environment_formatted_external_url,
+ :environment_external_url_for_route_map,
:code_owners,
:simple_viewer,
:rich_viewer,
@@ -39,7 +41,8 @@ RSpec.describe Types::Repository::BlobType do
:ide_edit_path,
:external_storage_url,
:fork_and_edit_path,
- :ide_fork_and_edit_path
+ :ide_fork_and_edit_path,
+ :language
)
end
end
diff --git a/spec/graphql/types/root_storage_statistics_type_spec.rb b/spec/graphql/types/root_storage_statistics_type_spec.rb
index 4fef8f6eafd..7818be6ee02 100644
--- a/spec/graphql/types/root_storage_statistics_type_spec.rb
+++ b/spec/graphql/types/root_storage_statistics_type_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe GitlabSchema.types['RootStorageStatistics'] do
it 'has all the required fields' do
expect(described_class).to have_graphql_fields(:storage_size, :repository_size, :lfs_objects_size,
:build_artifacts_size, :packages_size, :wiki_size, :snippets_size,
- :pipeline_artifacts_size, :uploads_size)
+ :pipeline_artifacts_size, :uploads_size, :dependency_proxy_size)
end
specify { expect(described_class).to require_graphql_authorizations(:read_statistics) }
diff --git a/spec/graphql/types/subscription_type_spec.rb b/spec/graphql/types/subscription_type_spec.rb
index bf933945a31..593795de004 100644
--- a/spec/graphql/types/subscription_type_spec.rb
+++ b/spec/graphql/types/subscription_type_spec.rb
@@ -7,6 +7,7 @@ RSpec.describe GitlabSchema.types['Subscription'] do
expected_fields = %i[
issuable_assignees_updated
issue_crm_contacts_updated
+ issuable_title_updated
]
expect(described_class).to have_graphql_fields(*expected_fields).only
diff --git a/spec/graphql/types/user_preferences_type_spec.rb b/spec/graphql/types/user_preferences_type_spec.rb
new file mode 100644
index 00000000000..fac45443290
--- /dev/null
+++ b/spec/graphql/types/user_preferences_type_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::UserPreferencesType do
+ specify { expect(described_class.graphql_name).to eq('UserPreferences') }
+
+ it 'exposes the expected fields' do
+ expected_fields = %i[
+ issues_sort
+ ]
+
+ expect(described_class).to have_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/graphql/types/user_type_spec.rb b/spec/graphql/types/user_type_spec.rb
index 4e3f442dc71..a2fc8f4c954 100644
--- a/spec/graphql/types/user_type_spec.rb
+++ b/spec/graphql/types/user_type_spec.rb
@@ -67,14 +67,14 @@ RSpec.describe GitlabSchema.types['User'] do
)
end
- subject { GitlabSchema.execute(query, context: { current_user: current_user }).as_json.dig('data', 'user', 'name') }
+ subject(:user_name) { GitlabSchema.execute(query, context: { current_user: current_user }).as_json.dig('data', 'user', 'name') }
context 'user requests' do
let(:current_user) { user }
context 'a user' do
it 'returns name' do
- expect(subject).to eq('John Smith')
+ expect(user_name).to eq('John Smith')
end
end
@@ -85,21 +85,39 @@ RSpec.describe GitlabSchema.types['User'] do
let(:current_user) { nil }
it 'returns `****`' do
- expect(subject).to eq('****')
+ expect(user_name).to eq('****')
end
end
- it 'returns `****` for a regular user' do
- expect(subject).to eq('****')
+ context 'when the requester is not a project member' do
+ it 'returns `Project bot` for a non project member in a public project' do
+ expect(user_name).to eq('Project bot')
+ end
+
+ context 'in a private project' do
+ let(:project) { create(:project, :private) }
+
+ it 'returns `****` for a non project member in a private project' do
+ expect(user_name).to eq('****')
+ end
+ end
end
- context 'when requester is a project maintainer' do
+ context 'with a project member' do
before do
- project.add_maintainer(user)
+ project.add_guest(user)
+ end
+
+ it 'returns `Project bot` for a project member' do
+ expect(user_name).to eq('Project bot')
end
- it 'returns name' do
- expect(subject).to eq('Project bot')
+ context 'in a private project' do
+ let(:project) { create(:project, :private) }
+
+ it 'returns `Project bot` for a project member in a private project' do
+ expect(user_name).to eq('Project bot')
+ end
end
end
end
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index 8c2b4b16075..e6a2e3f8211 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -289,7 +289,7 @@ RSpec.describe ApplicationHelper do
it 'returns paths for autocomplete_sources_controller' do
sources = helper.autocomplete_data_sources(project, noteable_type)
- expect(sources.keys).to match_array([:members, :issues, :mergeRequests, :labels, :milestones, :commands, :snippets])
+ expect(sources.keys).to match_array([:members, :issues, :mergeRequests, :labels, :milestones, :commands, :snippets, :contacts])
sources.keys.each do |key|
expect(sources[key]).not_to be_nil
end
diff --git a/spec/helpers/application_settings_helper_spec.rb b/spec/helpers/application_settings_helper_spec.rb
index e722f301522..169b1c75995 100644
--- a/spec/helpers/application_settings_helper_spec.rb
+++ b/spec/helpers/application_settings_helper_spec.rb
@@ -46,6 +46,15 @@ RSpec.describe ApplicationSettingsHelper do
expect(helper.visible_attributes).to include(:deactivate_dormant_users)
end
+ it 'contains rate limit parameters' do
+ expect(helper.visible_attributes).to include(*%i(
+ issues_create_limit notes_create_limit project_export_limit
+ project_download_export_limit project_export_limit project_import_limit
+ raw_blob_request_limit group_export_limit group_download_export_limit
+ group_import_limit users_get_by_id_limit user_email_lookup_limit
+ ))
+ end
+
context 'when GitLab.com' do
before do
allow(Gitlab).to receive(:com?).and_return(true)
diff --git a/spec/helpers/avatars_helper_spec.rb b/spec/helpers/avatars_helper_spec.rb
index 7190f2fcd4a..192e48f43e5 100644
--- a/spec/helpers/avatars_helper_spec.rb
+++ b/spec/helpers/avatars_helper_spec.rb
@@ -146,11 +146,52 @@ RSpec.describe AvatarsHelper do
describe '#avatar_icon_for_user' do
let(:user) { create(:user, avatar: File.open(uploaded_image_temp_path)) }
+ shared_examples 'blocked or unconfirmed user with avatar' do
+ context 'when the viewer is not an admin' do
+ let!(:viewing_user) { create(:user) }
+
+ it 'returns the default avatar' do
+ expect(helper.avatar_icon_for_user(user, current_user: viewing_user).to_s)
+ .to match_asset_path(described_class::DEFAULT_AVATAR_PATH)
+ end
+ end
+
+ context 'when the viewer is an admin', :enable_admin_mode do
+ let!(:viewing_user) { create(:user, :admin) }
+
+ it 'returns the default avatar when the user is not passed' do
+ expect(helper.avatar_icon_for_user(user).to_s)
+ .to match_asset_path(described_class::DEFAULT_AVATAR_PATH)
+ end
+
+ it 'returns the user avatar when the user is passed' do
+ expect(helper.avatar_icon_for_user(user, current_user: viewing_user).to_s)
+ .to eq(user.avatar.url)
+ end
+ end
+ end
+
context 'with a user object passed' do
it 'returns a relative URL for the avatar' do
expect(helper.avatar_icon_for_user(user).to_s)
.to eq(user.avatar.url)
end
+
+ context 'when the user is blocked' do
+ before do
+ user.block!
+ end
+
+ it_behaves_like 'blocked or unconfirmed user with avatar'
+ end
+
+ context 'when the user is unconfirmed' do
+ before do
+ user.update!(confirmed_at: nil)
+ end
+
+ it_behaves_like 'blocked or unconfirmed user with avatar'
+ end
end
context 'without a user object passed' do
@@ -171,7 +212,7 @@ RSpec.describe AvatarsHelper do
end
it 'returns a generic avatar' do
- expect(helper.gravatar_icon(user_email)).to match_asset_path('no_avatar.png')
+ expect(helper.gravatar_icon(user_email)).to match_asset_path(described_class::DEFAULT_AVATAR_PATH)
end
end
@@ -181,7 +222,7 @@ RSpec.describe AvatarsHelper do
end
it 'returns a generic avatar when email is blank' do
- expect(helper.gravatar_icon('')).to match_asset_path('no_avatar.png')
+ expect(helper.gravatar_icon('')).to match_asset_path(described_class::DEFAULT_AVATAR_PATH)
end
it 'returns a valid Gravatar URL' do
@@ -428,7 +469,7 @@ RSpec.describe AvatarsHelper do
subject { helper.avatar_without_link(resource, options) }
context 'with users' do
- let(:resource) { user }
+ let(:resource) { user.namespace }
it 'displays user avatar' do
is_expected.to eq tag(
diff --git a/spec/helpers/bizible_helper_spec.rb b/spec/helpers/bizible_helper_spec.rb
new file mode 100644
index 00000000000..b82211d51ec
--- /dev/null
+++ b/spec/helpers/bizible_helper_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe BizibleHelper do
+ describe '#bizible_enabled?' do
+ before do
+ stub_config(extra: { bizible: SecureRandom.uuid })
+ end
+
+ context 'when bizible is disabled' do
+ before do
+ allow(helper).to receive(:bizible_enabled?).and_return(false)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when bizible is enabled' do
+ before do
+ allow(helper).to receive(:bizible_enabled?).and_return(true)
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ subject(:bizible_enabled?) { helper.bizible_enabled? }
+
+ context 'with ecomm_instrumentation feature flag disabled' do
+ before do
+ stub_feature_flags(ecomm_instrumentation: false)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'with ecomm_instrumentation feature flag enabled' do
+ context 'when no id is set' do
+ before do
+ stub_config(extra: {})
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+ end
+end
diff --git a/spec/helpers/ci/pipeline_editor_helper_spec.rb b/spec/helpers/ci/pipeline_editor_helper_spec.rb
index b15569f03c7..b844cc2e22b 100644
--- a/spec/helpers/ci/pipeline_editor_helper_spec.rb
+++ b/spec/helpers/ci/pipeline_editor_helper_spec.rb
@@ -88,6 +88,17 @@ RSpec.describe Ci::PipelineEditorHelper do
end
end
+ context 'with a project with no repository' do
+ let(:project) { create(:project) }
+
+ it 'returns pipeline editor data' do
+ expect(pipeline_editor_data).to include({
+ "pipeline_etag" => '',
+ "total-branches" => 0
+ })
+ end
+ end
+
context 'with a non-default branch name' do
let(:user) { create(:user) }
diff --git a/spec/helpers/clusters_helper_spec.rb b/spec/helpers/clusters_helper_spec.rb
index 51f111917d1..18d233fcd63 100644
--- a/spec/helpers/clusters_helper_spec.rb
+++ b/spec/helpers/clusters_helper_spec.rb
@@ -93,8 +93,9 @@ RSpec.describe ClustersHelper do
end
context 'user has no permissions to create a cluster' do
- it 'displays that user can\t add cluster' do
+ it 'displays that user can\'t add cluster' do
expect(subject[:can_add_cluster]).to eq("false")
+ expect(subject[:can_admin_cluster]).to eq("false")
end
end
@@ -105,6 +106,7 @@ RSpec.describe ClustersHelper do
it 'displays that the user can add cluster' do
expect(subject[:can_add_cluster]).to eq("true")
+ expect(subject[:can_admin_cluster]).to eq("true")
end
end
@@ -150,6 +152,10 @@ RSpec.describe ClustersHelper do
it 'displays kas address' do
expect(subject[:kas_address]).to eq(Gitlab::Kas.external_url)
end
+
+ it 'displays GitLab version' do
+ expect(subject[:gitlab_version]).to eq(Gitlab.version_info)
+ end
end
describe '#js_cluster_new' do
diff --git a/spec/helpers/invite_members_helper_spec.rb b/spec/helpers/invite_members_helper_spec.rb
index d8a97b93bc9..6a854a65920 100644
--- a/spec/helpers/invite_members_helper_spec.rb
+++ b/spec/helpers/invite_members_helper_spec.rb
@@ -15,6 +15,22 @@ RSpec.describe InviteMembersHelper do
helper.extend(Gitlab::Experimentation::ControllerConcern)
end
+ describe '#common_invite_group_modal_data' do
+ it 'has expected common attributes' do
+ attributes = {
+ id: project.id,
+ name: project.name,
+ default_access_level: Gitlab::Access::GUEST,
+ invalid_groups: project.related_group_ids,
+ help_link: help_page_url('user/permissions'),
+ is_project: 'true',
+ access_levels: ProjectMember.access_level_roles.to_json
+ }
+
+ expect(helper.common_invite_group_modal_data(project, ProjectMember, 'true')).to include(attributes)
+ end
+ end
+
describe '#common_invite_modal_dataset' do
it 'has expected common attributes' do
attributes = {
@@ -155,4 +171,28 @@ RSpec.describe InviteMembersHelper do
end
end
end
+
+ describe '#group_select_data' do
+ let_it_be(:group) { create(:group) }
+
+ context 'when sharing with groups outside the hierarchy is disabled' do
+ before do
+ group.namespace_settings.update!(prevent_sharing_groups_outside_hierarchy: true)
+ end
+
+ it 'provides the correct attributes' do
+ expect(helper.group_select_data(group)).to eq({ groups_filter: 'descendant_groups', parent_id: group.id })
+ end
+ end
+
+ context 'when sharing with groups outside the hierarchy is enabled' do
+ before do
+ group.namespace_settings.update!(prevent_sharing_groups_outside_hierarchy: false)
+ end
+
+ it 'returns an empty hash' do
+ expect(helper.group_select_data(project.group)).to eq({})
+ end
+ end
+ end
end
diff --git a/spec/helpers/issuables_description_templates_helper_spec.rb b/spec/helpers/issuables_description_templates_helper_spec.rb
index 6b05bab7432..768ce5975c1 100644
--- a/spec/helpers/issuables_description_templates_helper_spec.rb
+++ b/spec/helpers/issuables_description_templates_helper_spec.rb
@@ -72,6 +72,37 @@ RSpec.describe IssuablesDescriptionTemplatesHelper, :clean_gitlab_redis_cache do
].to_json
expect(helper.available_service_desk_templates_for(@project)).to eq(value)
end
+
+ context 'when no issuable_template parameter or default template is present' do
+ it 'does not select a template' do
+ expect(helper.selected_template(project)).to be(nil)
+ end
+ end
+
+ context 'when an issuable_template parameter has been provided' do
+ before do
+ allow(helper).to receive(:params).and_return({ issuable_template: 'another_issue_template' })
+ end
+
+ it 'selects the issuable template' do
+ expect(helper.selected_template(project)).to eq('another_issue_template')
+ end
+ end
+
+ context 'when there is a default template' do
+ let(:templates) do
+ {
+ "" => [
+ { name: "another_issue_template", id: "another_issue_template", project_id: project.id },
+ { name: "default", id: "default", project_id: project.id }
+ ]
+ }
+ end
+
+ it 'selects the default template' do
+ expect(helper.selected_template(project)).to eq('default')
+ end
+ end
end
context 'when there are not templates in the project' do
diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb
index fa19395ebc7..ed50a4daae8 100644
--- a/spec/helpers/issuables_helper_spec.rb
+++ b/spec/helpers/issuables_helper_spec.rb
@@ -133,13 +133,13 @@ RSpec.describe IssuablesHelper do
it 'returns navigation with badges' do
expect(helper.issuables_state_counter_text(:issues, :opened, true))
- .to eq('<span>Open</span> <span class="badge badge-muted badge-pill gl-badge gl-tab-counter-badge sm gl-display-none gl-sm-display-inline-flex">42</span>')
+ .to eq('<span>Open</span> <span class="gl-badge badge badge-pill badge-muted sm gl-tab-counter-badge gl-display-none gl-sm-display-inline-flex">42</span>')
expect(helper.issuables_state_counter_text(:issues, :closed, true))
- .to eq('<span>Closed</span> <span class="badge badge-muted badge-pill gl-badge gl-tab-counter-badge sm gl-display-none gl-sm-display-inline-flex">42</span>')
+ .to eq('<span>Closed</span> <span class="gl-badge badge badge-pill badge-muted sm gl-tab-counter-badge gl-display-none gl-sm-display-inline-flex">42</span>')
expect(helper.issuables_state_counter_text(:merge_requests, :merged, true))
- .to eq('<span>Merged</span> <span class="badge badge-muted badge-pill gl-badge gl-tab-counter-badge sm gl-display-none gl-sm-display-inline-flex">42</span>')
+ .to eq('<span>Merged</span> <span class="gl-badge badge badge-pill badge-muted sm gl-tab-counter-badge gl-display-none gl-sm-display-inline-flex">42</span>')
expect(helper.issuables_state_counter_text(:merge_requests, :all, true))
- .to eq('<span>All</span> <span class="badge badge-muted badge-pill gl-badge gl-tab-counter-badge sm gl-display-none gl-sm-display-inline-flex">42</span>')
+ .to eq('<span>All</span> <span class="gl-badge badge badge-pill badge-muted sm gl-tab-counter-badge gl-display-none gl-sm-display-inline-flex">42</span>')
end
end
@@ -171,7 +171,7 @@ RSpec.describe IssuablesHelper do
it 'returns truncated count' do
expect(helper.issuables_state_counter_text(:issues, :opened, true))
- .to eq('<span>Open</span> <span class="badge badge-muted badge-pill gl-badge gl-tab-counter-badge sm gl-display-none gl-sm-display-inline-flex">1.1k</span>')
+ .to eq('<span>Open</span> <span class="gl-badge badge badge-pill badge-muted sm gl-tab-counter-badge gl-display-none gl-sm-display-inline-flex">1.1k</span>')
end
end
end
diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb
index 065ac526ae4..2f57657736d 100644
--- a/spec/helpers/issues_helper_spec.rb
+++ b/spec/helpers/issues_helper_spec.rb
@@ -300,6 +300,7 @@ RSpec.describe IssuesHelper do
has_any_issues: project_issues(project).exists?.to_s,
import_csv_issues_path: '#',
initial_email: project.new_issuable_address(current_user, 'issue'),
+ initial_sort: current_user&.user_preference&.issues_sort,
is_anonymous_search_disabled: 'true',
is_issue_repositioning_disabled: 'true',
is_project: 'true',
@@ -342,8 +343,6 @@ RSpec.describe IssuesHelper do
describe '#group_issues_list_data' do
let(:group) { create(:group) }
let(:current_user) { double.as_null_object }
- let(:issues) { [] }
- let(:projects) { [] }
it 'returns expected result' do
allow(helper).to receive(:current_user).and_return(current_user)
@@ -351,20 +350,23 @@ RSpec.describe IssuesHelper do
allow(helper).to receive(:image_path).and_return('#')
allow(helper).to receive(:url_for).and_return('#')
+ assign(:has_issues, false)
+ assign(:has_projects, true)
+
expected = {
autocomplete_award_emojis_path: autocomplete_award_emojis_path,
calendar_path: '#',
empty_state_svg_path: '#',
full_path: group.full_path,
- has_any_issues: issues.to_a.any?.to_s,
- has_any_projects: any_projects?(projects).to_s,
+ has_any_issues: false.to_s,
+ has_any_projects: true.to_s,
is_signed_in: current_user.present?.to_s,
jira_integration_path: help_page_url('integration/jira/issues', anchor: 'view-jira-issues'),
rss_path: '#',
sign_in_path: new_user_session_path
}
- expect(helper.group_issues_list_data(group, current_user, issues, projects)).to include(expected)
+ expect(helper.group_issues_list_data(group, current_user)).to include(expected)
end
end
diff --git a/spec/helpers/listbox_helper_spec.rb b/spec/helpers/listbox_helper_spec.rb
new file mode 100644
index 00000000000..8935d69d4f7
--- /dev/null
+++ b/spec/helpers/listbox_helper_spec.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ListboxHelper do
+ subject do
+ tag = helper.gl_redirect_listbox_tag(items, selected, html_options)
+ Nokogiri::HTML.fragment(tag).children.first
+ end
+
+ before do
+ allow(helper).to receive(:sprite_icon).with(
+ 'chevron-down',
+ css_class: 'gl-button-icon dropdown-chevron gl-icon'
+ ).and_return('<span class="icon"></span>'.html_safe)
+ end
+
+ let(:selected) { 'bar' }
+ let(:html_options) { {} }
+ let(:items) do
+ [
+ { value: 'foo', text: 'Foo' },
+ { value: 'bar', text: 'Bar' }
+ ]
+ end
+
+ describe '#gl_redirect_listbox_tag' do
+ it 'creates root element with expected classes' do
+ expect(subject.classes).to include(*%w[
+ dropdown
+ b-dropdown
+ gl-new-dropdown
+ btn-group
+ js-redirect-listbox
+ ])
+ end
+
+ it 'sets data attributes for items and selected' do
+ expect(subject.attributes['data-items'].value).to eq(items.to_json)
+ expect(subject.attributes['data-selected'].value).to eq(selected)
+ end
+
+ it 'adds styled button' do
+ expect(subject.at_css('button').classes).to include(*%w[
+ btn
+ dropdown-toggle
+ btn-default
+ btn-md
+ gl-button
+ gl-dropdown-toggle
+ ])
+ end
+
+ it 'sets button text to selected item' do
+ expect(subject.at_css('button').content).to eq('Bar')
+ end
+
+ context 'given html_options' do
+ let(:html_options) { { class: 'test-class', data: { qux: 'qux' } } }
+
+ it 'applies them to the root element' do
+ expect(subject.attributes['data-qux'].value).to eq('qux')
+ expect(subject.classes).to include('test-class')
+ end
+ end
+
+ context 'when selected does not match any item' do
+ let(:selected) { 'qux' }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(ArgumentError, /cannot find qux/)
+ end
+ end
+ end
+end
diff --git a/spec/helpers/projects/cluster_agents_helper_spec.rb b/spec/helpers/projects/cluster_agents_helper_spec.rb
index 632544797ee..d94a5fa9f8a 100644
--- a/spec/helpers/projects/cluster_agents_helper_spec.rb
+++ b/spec/helpers/projects/cluster_agents_helper_spec.rb
@@ -5,22 +5,29 @@ require 'spec_helper'
RSpec.describe Projects::ClusterAgentsHelper do
describe '#js_cluster_agent_details_data' do
let_it_be(:project) { create(:project) }
+ let_it_be(:current_user) { create(:user) }
+ let(:user_can_admin_vulerability) { true }
let(:agent_name) { 'agent-name' }
- subject { helper.js_cluster_agent_details_data(agent_name, project) }
-
- it 'returns name' do
- expect(subject[:agent_name]).to eq(agent_name)
+ before do
+ allow(helper).to receive(:current_user).and_return(current_user)
+ allow(helper)
+ .to receive(:can?)
+ .with(current_user, :admin_vulnerability, project)
+ .and_return(user_can_admin_vulerability)
end
- it 'returns project path' do
- expect(subject[:project_path]).to eq(project.full_path)
- end
+ subject { helper.js_cluster_agent_details_data(agent_name, project) }
- it 'returns string contants' do
- expect(subject[:activity_empty_state_image]).to be_kind_of(String)
- expect(subject[:empty_state_svg_path]).to be_kind_of(String)
- end
+ it {
+ is_expected.to match({
+ agent_name: agent_name,
+ project_path: project.full_path,
+ activity_empty_state_image: kind_of(String),
+ empty_state_svg_path: kind_of(String),
+ can_admin_vulnerability: "true"
+ })
+ }
end
end
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index cc443afee6e..604ce0fe0c1 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -345,6 +345,14 @@ RSpec.describe ProjectsHelper do
expect(link).not_to include(user.name)
end
end
+
+ context 'when user is nil' do
+ it 'returns "(deleted)"' do
+ link = helper.link_to_member(project, nil)
+
+ expect(link).to eq("(deleted)")
+ end
+ end
end
describe 'default_clone_protocol' do
@@ -1018,4 +1026,26 @@ RSpec.describe ProjectsHelper do
end
end
end
+
+ describe '#import_from_bitbucket_message' do
+ before do
+ allow(helper).to receive(:current_user).and_return(user)
+ end
+
+ context 'as a user' do
+ it 'returns a link to contact an administrator' do
+ allow(user).to receive(:admin?).and_return(false)
+
+ expect(helper.import_from_bitbucket_message).to have_text('To enable importing projects from Bitbucket, ask your GitLab administrator to configure OAuth integration')
+ end
+ end
+
+ context 'as an administrator' do
+ it 'returns a link to configure bitbucket' do
+ allow(user).to receive(:admin?).and_return(true)
+
+ expect(helper.import_from_bitbucket_message).to have_text('To enable importing projects from Bitbucket, as administrator you need to configure OAuth integration')
+ end
+ end
+ end
end
diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb
index 40cfdafc9ac..78cc1dcee01 100644
--- a/spec/helpers/search_helper_spec.rb
+++ b/spec/helpers/search_helper_spec.rb
@@ -658,4 +658,152 @@ RSpec.describe SearchHelper do
expect(search_sort_options).to eq(mock_created_sort)
end
end
+
+ describe '#header_search_context' do
+ let(:user) { create(:user) }
+ let(:can_download) { false }
+
+ let(:for_group) { false }
+ let(:group) { nil }
+ let(:group_metadata) { nil }
+
+ let(:for_project) { false }
+ let(:project) { nil }
+ let(:project_metadata) { nil }
+
+ let(:scope) { nil }
+ let(:code_search) { false }
+ let(:ref) { nil }
+ let(:for_snippets) { false }
+
+ let(:search_context) do
+ instance_double(Gitlab::SearchContext,
+ group: group,
+ group_metadata: group_metadata,
+ project: project,
+ project_metadata: project_metadata,
+ scope: scope,
+ ref: ref)
+ end
+
+ before do
+ allow(self).to receive(:search_context).and_return(search_context)
+ allow(self).to receive(:current_user).and_return(user)
+ allow(self).to receive(:can?).and_return(can_download)
+
+ allow(search_context).to receive(:for_group?).and_return(for_group)
+ allow(search_context).to receive(:for_project?).and_return(for_project)
+
+ allow(search_context).to receive(:code_search?).and_return(code_search)
+ allow(search_context).to receive(:for_snippets?).and_return(for_snippets)
+ end
+
+ context 'group data' do
+ let(:group) { create(:group) }
+ let(:group_metadata) { { group_path: group.path, issues_path: "/issues" } }
+ let(:scope) { 'issues' }
+ let(:code_search) { true }
+
+ context 'when for_group? is true' do
+ let(:for_group) { true }
+
+ it 'adds the :group and :group_metadata correctly to hash' do
+ expect(header_search_context[:group]).to eq({ id: group.id, name: group.name })
+ expect(header_search_context[:group_metadata]).to eq(group_metadata)
+ end
+
+ it 'adds scope and code_search? correctly to hash' do
+ expect(header_search_context[:scope]).to eq(scope)
+ expect(header_search_context[:code_search]).to eq(code_search)
+ end
+ end
+
+ context 'when for_group? is false' do
+ let(:for_group) { false }
+
+ it 'does not add the :group and :group_metadata to hash' do
+ expect(header_search_context[:group]).to eq(nil)
+ expect(header_search_context[:group_metadata]).to eq(nil)
+ end
+
+ it 'does not add scope and code_search? to hash' do
+ expect(header_search_context[:scope]).to eq(nil)
+ expect(header_search_context[:code_search]).to eq(nil)
+ end
+ end
+ end
+
+ context 'project data' do
+ let(:project) { create(:project) }
+ let(:project_metadata) { { project_path: project.path, issues_path: "/issues" } }
+ let(:scope) { 'issues' }
+ let(:code_search) { true }
+
+ context 'when for_project? is true' do
+ let(:for_project) { true }
+
+ it 'adds the :project and :project_metadata correctly to hash' do
+ expect(header_search_context[:project]).to eq({ id: project.id, name: project.name })
+ expect(header_search_context[:project_metadata]).to eq(project_metadata)
+ end
+
+ it 'adds scope and code_search? correctly to hash' do
+ expect(header_search_context[:scope]).to eq(scope)
+ expect(header_search_context[:code_search]).to eq(code_search)
+ end
+ end
+
+ context 'when for_project? is false' do
+ let(:for_project) { false }
+
+ it 'does not add the :project and :project_metadata to hash' do
+ expect(header_search_context[:project]).to eq(nil)
+ expect(header_search_context[:project_metadata]).to eq(nil)
+ end
+
+ it 'does not add scope and code_search? to hash' do
+ expect(header_search_context[:scope]).to eq(nil)
+ expect(header_search_context[:code_search]).to eq(nil)
+ end
+ end
+ end
+
+ context 'ref data' do
+ let(:ref) { 'test-branch' }
+
+ context 'when user can? download project data' do
+ let(:can_download) { true }
+
+ it 'adds the :ref correctly to hash' do
+ expect(header_search_context[:ref]).to eq(ref)
+ end
+ end
+
+ context 'when user cannot download project data' do
+ let(:can_download) { false }
+
+ it 'does not add the :ref to hash' do
+ expect(header_search_context[:ref]).to eq(nil)
+ end
+ end
+ end
+
+ context 'snippets' do
+ context 'when for_snippets? is true' do
+ let(:for_snippets) { true }
+
+ it 'adds :for_snippets correctly to hash' do
+ expect(header_search_context[:for_snippets]).to eq(for_snippets)
+ end
+ end
+
+ context 'when for_snippets? is false' do
+ let(:for_snippets) { false }
+
+ it 'adds :for_snippets correctly to hash' do
+ expect(header_search_context[:for_snippets]).to eq(for_snippets)
+ end
+ end
+ end
+ end
end
diff --git a/spec/helpers/ssh_keys_helper_spec.rb b/spec/helpers/ssh_keys_helper_spec.rb
index 1aa604f19be..522331090e4 100644
--- a/spec/helpers/ssh_keys_helper_spec.rb
+++ b/spec/helpers/ssh_keys_helper_spec.rb
@@ -17,9 +17,9 @@ RSpec.describe SshKeysHelper do
end
it 'returns only allowed algorithms' do
- expect(ssh_key_allowed_algorithms).to match('ed25519')
- stub_application_setting(ed25519_key_restriction: ApplicationSetting::FORBIDDEN_KEY_VALUE)
- expect(ssh_key_allowed_algorithms).not_to match('ed25519')
+ expect(ssh_key_allowed_algorithms).to match('rsa')
+ stub_application_setting(rsa_key_restriction: ApplicationSetting::FORBIDDEN_KEY_VALUE)
+ expect(ssh_key_allowed_algorithms).not_to match('rsa')
end
end
end
diff --git a/spec/helpers/storage_helper_spec.rb b/spec/helpers/storage_helper_spec.rb
index d0646b30161..82b78ed831c 100644
--- a/spec/helpers/storage_helper_spec.rb
+++ b/spec/helpers/storage_helper_spec.rb
@@ -50,4 +50,87 @@ RSpec.describe StorageHelper do
expect(helper.storage_counters_details(namespace_stats)).to eq(message)
end
end
+
+ describe "storage_enforcement_banner" do
+ let_it_be_with_refind(:current_user) { create(:user) }
+ let_it_be(:free_group) { create(:group) }
+ let_it_be(:paid_group) { create(:group) }
+
+ before do
+ allow(helper).to receive(:current_user) { current_user }
+ allow(Gitlab).to receive(:com?).and_return(true)
+ allow(paid_group).to receive(:paid?).and_return(true)
+ end
+
+ describe "#storage_enforcement_banner_info" do
+ it 'returns nil when namespace is not free' do
+ expect(storage_enforcement_banner_info(paid_group)).to be(nil)
+ end
+
+ it 'returns nil when storage_enforcement_date is not set' do
+ allow(free_group).to receive(:storage_enforcement_date).and_return(nil)
+
+ expect(storage_enforcement_banner_info(free_group)).to be(nil)
+ end
+
+ it 'returns a hash when storage_enforcement_date is set' do
+ storage_enforcement_date = Date.today + 30
+ allow(free_group).to receive(:storage_enforcement_date).and_return(storage_enforcement_date)
+
+ expect(storage_enforcement_banner_info(free_group)).to eql({
+ text: "From #{storage_enforcement_date} storage limits will apply to this namespace. View and manage your usage in <strong>Group Settings &gt; Usage quotas</strong>.",
+ variant: 'warning',
+ callouts_feature_name: 'storage_enforcement_banner_second_enforcement_threshold',
+ callouts_path: '/-/users/group_callouts',
+ learn_more_link: '<a rel="noopener noreferrer" target="_blank" href="/help//">Learn more.</a>'
+ })
+ end
+
+ context 'when storage_enforcement_date is set and dismissed callout exists' do
+ before do
+ create(:group_callout,
+ user: current_user,
+ group_id: free_group.id,
+ feature_name: 'storage_enforcement_banner_second_enforcement_threshold')
+ storage_enforcement_date = Date.today + 30
+ allow(free_group).to receive(:storage_enforcement_date).and_return(storage_enforcement_date)
+ end
+
+ it { expect(storage_enforcement_banner_info(free_group)).to be(nil) }
+ end
+
+ context 'callouts_feature_name' do
+ let(:days_from_now) { 45 }
+
+ subject do
+ storage_enforcement_date = Date.today + days_from_now
+ allow(free_group).to receive(:storage_enforcement_date).and_return(storage_enforcement_date)
+
+ storage_enforcement_banner_info(free_group)[:callouts_feature_name]
+ end
+
+ it 'returns first callouts_feature_name' do
+ is_expected.to eq('storage_enforcement_banner_first_enforcement_threshold')
+ end
+
+ context 'returns second callouts_feature_name' do
+ let(:days_from_now) { 20 }
+
+ it { is_expected.to eq('storage_enforcement_banner_second_enforcement_threshold') }
+ end
+
+ context 'returns third callouts_feature_name' do
+ let(:days_from_now) { 13 }
+
+ it { is_expected.to eq('storage_enforcement_banner_third_enforcement_threshold') }
+ end
+
+ context 'returns fourth callouts_feature_name' do
+ let(:days_from_now) { 3 }
+
+ it { is_expected.to eq('storage_enforcement_banner_fourth_enforcement_threshold') }
+ end
+ end
+ end
+ end
end
diff --git a/spec/helpers/tab_helper_spec.rb b/spec/helpers/tab_helper_spec.rb
index f338eddedfd..dd5707e2aff 100644
--- a/spec/helpers/tab_helper_spec.rb
+++ b/spec/helpers/tab_helper_spec.rb
@@ -45,7 +45,7 @@ RSpec.describe TabHelper do
end
it 'creates an active tab with item_active = true' do
- expect(helper.gl_tab_link_to('Link', '/url', { item_active: true })).to match(/<a class=".*active gl-tab-nav-item-active gl-tab-nav-item-active-indigo.*"/)
+ expect(helper.gl_tab_link_to('Link', '/url', { item_active: true })).to match(/<a class=".*active gl-tab-nav-item-active.*"/)
end
context 'when on the active page' do
@@ -54,7 +54,7 @@ RSpec.describe TabHelper do
end
it 'creates an active tab' do
- expect(helper.gl_tab_link_to('Link', '/url')).to match(/<a class=".*active gl-tab-nav-item-active gl-tab-nav-item-active-indigo.*"/)
+ expect(helper.gl_tab_link_to('Link', '/url')).to match(/<a class=".*active gl-tab-nav-item-active.*"/)
end
it 'creates an inactive tab with item_active = false' do
diff --git a/spec/helpers/users_helper_spec.rb b/spec/helpers/users_helper_spec.rb
index 2b55319c70c..82f4ae596e1 100644
--- a/spec/helpers/users_helper_spec.rb
+++ b/spec/helpers/users_helper_spec.rb
@@ -11,6 +11,20 @@ RSpec.describe UsersHelper do
badges.reject { |badge| badge[:text] == 'Is using seat' }
end
+ describe 'display_public_email?' do
+ let_it_be(:user) { create(:user, :public_email) }
+
+ subject { helper.display_public_email?(user) }
+
+ it { is_expected.to be true }
+
+ context 'when user public email is blank' do
+ let_it_be(:user) { create(:user, public_email: '') }
+
+ it { is_expected.to be false }
+ end
+ end
+
describe '#user_link' do
subject { helper.user_link(user) }
@@ -122,7 +136,7 @@ RSpec.describe UsersHelper do
badges = helper.user_badges_in_admin_section(blocked_user)
- expect(filter_ee_badges(badges)).to eq([text: "Blocked", variant: "danger"])
+ expect(filter_ee_badges(badges)).to match_array([text: "Blocked", variant: "danger"])
end
end
@@ -132,7 +146,7 @@ RSpec.describe UsersHelper do
badges = helper.user_badges_in_admin_section(blocked_pending_approval_user)
- expect(filter_ee_badges(badges)).to eq([text: 'Pending approval', variant: 'info'])
+ expect(filter_ee_badges(badges)).to match_array([text: 'Pending approval', variant: 'info'])
end
end
@@ -142,7 +156,7 @@ RSpec.describe UsersHelper do
badges = helper.user_badges_in_admin_section(banned_user)
- expect(filter_ee_badges(badges)).to eq([text: 'Banned', variant: 'danger'])
+ expect(filter_ee_badges(badges)).to match_array([text: 'Banned', variant: 'danger'])
end
end
@@ -152,7 +166,7 @@ RSpec.describe UsersHelper do
badges = helper.user_badges_in_admin_section(admin_user)
- expect(filter_ee_badges(badges)).to eq([text: "Admin", variant: "success"])
+ expect(filter_ee_badges(badges)).to match_array([text: "Admin", variant: "success"])
end
end
@@ -162,7 +176,7 @@ RSpec.describe UsersHelper do
badges = helper.user_badges_in_admin_section(external_user)
- expect(filter_ee_badges(badges)).to eq([text: "External", variant: "secondary"])
+ expect(filter_ee_badges(badges)).to match_array([text: "External", variant: "secondary"])
end
end
@@ -170,7 +184,7 @@ RSpec.describe UsersHelper do
it 'returns the "It\'s You" badge' do
badges = helper.user_badges_in_admin_section(user)
- expect(filter_ee_badges(badges)).to eq([text: "It's you!", variant: "muted"])
+ expect(filter_ee_badges(badges)).to match_array([text: "It's you!", variant: "muted"])
end
end
@@ -180,7 +194,7 @@ RSpec.describe UsersHelper do
badges = helper.user_badges_in_admin_section(user)
- expect(badges).to eq([
+ expect(badges).to match_array([
{ text: "Blocked", variant: "danger" },
{ text: "Admin", variant: "success" },
{ text: "External", variant: "secondary" }
@@ -188,6 +202,16 @@ RSpec.describe UsersHelper do
end
end
+ context 'with a locked user', time_travel_to: '2020-02-25 10:30:45 -0700' do
+ it 'returns the "Locked" badge' do
+ locked_user = create(:user, locked_at: DateTime.parse('2020-02-25 10:30:00 -0700'))
+
+ badges = helper.user_badges_in_admin_section(locked_user)
+
+ expect(filter_ee_badges(badges)).to match_array([text: "Locked", variant: "warning"])
+ end
+ end
+
context 'get badges for normal user' do
it 'returns no badges' do
user = create(:user)
diff --git a/spec/initializers/google_api_client_spec.rb b/spec/initializers/google_api_client_spec.rb
new file mode 100644
index 00000000000..0ed82d7debe
--- /dev/null
+++ b/spec/initializers/google_api_client_spec.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+# Extracted from https://github.com/googleapis/google-api-ruby-client/blob/main/google-apis-core/spec/google/apis/core/http_command_spec.rb
+
+require 'spec_helper'
+require 'google/apis/core/base_service'
+
+RSpec.describe Google::Apis::Core::HttpCommand do # rubocop:disable RSpec/FilePath
+ context('with a successful response') do
+ let(:client) { Google::Apis::Core::BaseService.new('', '').client }
+ let(:command) { Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals') }
+
+ before do
+ stub_request(:get, 'https://www.googleapis.com/zoo/animals').to_return(body: %(Hello world))
+ end
+
+ it 'returns the response body if block not present' do
+ result = command.execute(client)
+ expect(result).to eql 'Hello world'
+ end
+
+ it 'calls block if present' do
+ expect { |b| command.execute(client, &b) }.to yield_with_args('Hello world', nil)
+ end
+
+ it 'retries with max elapsed_time and retries' do
+ expect(Retriable).to receive(:retriable).with(
+ tries: Google::Apis::RequestOptions.default.retries + 1,
+ max_elapsed_time: 3600,
+ base_interval: 1,
+ multiplier: 2,
+ on: described_class::RETRIABLE_ERRORS).and_call_original
+ allow(Retriable).to receive(:retriable).and_call_original
+
+ command.execute(client)
+ end
+ end
+end
diff --git a/spec/initializers/net_http_patch_spec.rb b/spec/initializers/net_http_patch_spec.rb
index e5205abbed2..d6b003d84fa 100644
--- a/spec/initializers/net_http_patch_spec.rb
+++ b/spec/initializers/net_http_patch_spec.rb
@@ -1,6 +1,9 @@
# frozen_string_literal: true
+
require 'fast_spec_helper'
+require_relative '../../config/initializers/net_http_patch'
+
RSpec.describe 'Net::HTTP patch proxy user and password encoding' do
let(:net_http) { Net::HTTP.new('hostname.example') }
diff --git a/spec/lib/api/entities/basic_project_details_spec.rb b/spec/lib/api/entities/basic_project_details_spec.rb
index dc7c4fdce4e..8419eb0a932 100644
--- a/spec/lib/api/entities/basic_project_details_spec.rb
+++ b/spec/lib/api/entities/basic_project_details_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe API::Entities::BasicProjectDetails do
let_it_be(:project) { create(:project) }
- let(:current_user) { project.owner }
+ let(:current_user) { project.first_owner }
subject(:output) { described_class.new(project, current_user: current_user).as_json }
diff --git a/spec/lib/api/entities/deployment_extended_spec.rb b/spec/lib/api/entities/deployment_extended_spec.rb
new file mode 100644
index 00000000000..733c47362be
--- /dev/null
+++ b/spec/lib/api/entities/deployment_extended_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::Entities::DeploymentExtended do
+ describe '#as_json' do
+ subject { described_class.new(deployment).as_json }
+
+ let(:deployment) { create(:deployment) }
+
+ it 'includes fields from deployment entity' do
+ is_expected.to include(:id, :iid, :ref, :sha, :created_at, :updated_at, :user, :environment, :deployable, :status)
+ end
+ end
+end
diff --git a/spec/lib/api/helpers_spec.rb b/spec/lib/api/helpers_spec.rb
index 2277bd78e86..b2d4a3094af 100644
--- a/spec/lib/api/helpers_spec.rb
+++ b/spec/lib/api/helpers_spec.rb
@@ -76,6 +76,12 @@ RSpec.describe API::Helpers do
expect(subject.find_project(non_existing_id)).to be_nil
end
end
+
+ context 'when project id is not provided' do
+ it 'returns nil' do
+ expect(subject.find_project(nil)).to be_nil
+ end
+ end
end
context 'when ID is used as an argument' do
@@ -160,7 +166,7 @@ RSpec.describe API::Helpers do
describe '#find_project!' do
let_it_be(:project) { create(:project) }
- let(:user) { project.owner}
+ let(:user) { project.first_owner}
before do
allow(subject).to receive(:current_user).and_return(user)
diff --git a/spec/lib/backup/database_spec.rb b/spec/lib/backup/database_spec.rb
index f57037d5652..4345778ba92 100644
--- a/spec/lib/backup/database_spec.rb
+++ b/spec/lib/backup/database_spec.rb
@@ -6,6 +6,10 @@ RSpec.describe Backup::Database do
let(:progress) { StringIO.new }
let(:output) { progress.string }
+ before do
+ allow(Gitlab::TaskHelpers).to receive(:ask_to_continue)
+ end
+
describe '#restore' do
let(:cmd) { %W[#{Gem.ruby} -e $stdout.puts(1)] }
let(:data) { Rails.root.join("spec/fixtures/pages_empty.tar.gz").to_s }
@@ -20,7 +24,7 @@ RSpec.describe Backup::Database do
let(:data) { Rails.root.join("spec/fixtures/pages_empty.tar.gz").to_s }
it 'returns successfully' do
- expect(subject.restore).to eq([])
+ subject.restore
expect(output).to include("Restoring PostgreSQL database")
expect(output).to include("[DONE]")
@@ -42,7 +46,8 @@ RSpec.describe Backup::Database do
let(:cmd) { %W[#{Gem.ruby} -e $stderr.write("#{noise}#{visible_error}")] }
it 'filters out noise from errors' do
- expect(subject.restore).to eq([visible_error])
+ subject.restore
+
expect(output).to include("ERRORS")
expect(output).not_to include(noise)
expect(output).to include(visible_error)
diff --git a/spec/lib/backup/gitaly_backup_spec.rb b/spec/lib/backup/gitaly_backup_spec.rb
index cd0d984fbdb..6bf4f833c1f 100644
--- a/spec/lib/backup/gitaly_backup_spec.rb
+++ b/spec/lib/backup/gitaly_backup_spec.rb
@@ -38,7 +38,7 @@ RSpec.describe Backup::GitalyBackup do
create(:wiki_page, container: project)
create(:design, :with_file, issue: create(:issue, project: project))
project_snippet = create(:project_snippet, :repository, project: project)
- personal_snippet = create(:personal_snippet, :repository, author: project.owner)
+ personal_snippet = create(:personal_snippet, :repository, author: project.first_owner)
expect(Open3).to receive(:popen2).with(expected_env, anything, 'create', '-path', anything).and_call_original
@@ -122,8 +122,8 @@ RSpec.describe Backup::GitalyBackup do
context 'restore' do
let_it_be(:project) { create(:project, :repository) }
- let_it_be(:personal_snippet) { create(:personal_snippet, author: project.owner) }
- let_it_be(:project_snippet) { create(:project_snippet, project: project, author: project.owner) }
+ let_it_be(:personal_snippet) { create(:personal_snippet, author: project.first_owner) }
+ let_it_be(:project_snippet) { create(:project_snippet, project: project, author: project.first_owner) }
def copy_bundle_to_backup_path(bundle_name, destination)
FileUtils.mkdir_p(File.join(Gitlab.config.backup.path, 'repositories', File.dirname(destination)))
diff --git a/spec/lib/backup/gitaly_rpc_backup_spec.rb b/spec/lib/backup/gitaly_rpc_backup_spec.rb
index 14f9d27ca6e..4829d51ac9d 100644
--- a/spec/lib/backup/gitaly_rpc_backup_spec.rb
+++ b/spec/lib/backup/gitaly_rpc_backup_spec.rb
@@ -25,7 +25,7 @@ RSpec.describe Backup::GitalyRpcBackup do
create(:wiki_page, container: project)
create(:design, :with_file, issue: create(:issue, project: project))
project_snippet = create(:project_snippet, :repository, project: project)
- personal_snippet = create(:personal_snippet, :repository, author: project.owner)
+ personal_snippet = create(:personal_snippet, :repository, author: project.first_owner)
subject.start(:create)
subject.enqueue(project, Gitlab::GlRepository::PROJECT)
@@ -75,8 +75,8 @@ RSpec.describe Backup::GitalyRpcBackup do
context 'restore' do
let_it_be(:project) { create(:project, :repository) }
- let_it_be(:personal_snippet) { create(:personal_snippet, author: project.owner) }
- let_it_be(:project_snippet) { create(:project_snippet, project: project, author: project.owner) }
+ let_it_be(:personal_snippet) { create(:personal_snippet, author: project.first_owner) }
+ let_it_be(:project_snippet) { create(:project_snippet, project: project, author: project.first_owner) }
def copy_bundle_to_backup_path(bundle_name, destination)
FileUtils.mkdir_p(File.join(Gitlab.config.backup.path, 'repositories', File.dirname(destination)))
diff --git a/spec/lib/backup/manager_spec.rb b/spec/lib/backup/manager_spec.rb
index 31cc3012eb1..ac693ad8b98 100644
--- a/spec/lib/backup/manager_spec.rb
+++ b/spec/lib/backup/manager_spec.rb
@@ -12,6 +12,11 @@ RSpec.describe Backup::Manager do
before do
allow(progress).to receive(:puts)
allow(progress).to receive(:print)
+ FileUtils.mkdir_p('tmp/tests/public/uploads')
+ end
+
+ after do
+ FileUtils.rm_rf('tmp/tests/public/uploads', secure: true)
end
describe '#pack' do
@@ -409,7 +414,7 @@ RSpec.describe Backup::Manager do
# the Fog mock only knows about directories we create explicitly
connection = ::Fog::Storage.new(Gitlab.config.backup.upload.connection.symbolize_keys)
- connection.directories.create(key: Gitlab.config.backup.upload.remote_directory)
+ connection.directories.create(key: Gitlab.config.backup.upload.remote_directory) # rubocop:disable Rails/SaveBang
end
context 'target path' do
@@ -455,7 +460,7 @@ RSpec.describe Backup::Manager do
}
)
- connection.directories.create(key: Gitlab.config.backup.upload.remote_directory)
+ connection.directories.create(key: Gitlab.config.backup.upload.remote_directory) # rubocop:disable Rails/SaveBang
end
context 'with SSE-S3 without using storage_options' do
@@ -521,7 +526,7 @@ RSpec.describe Backup::Manager do
)
connection = ::Fog::Storage.new(Gitlab.config.backup.upload.connection.symbolize_keys)
- connection.directories.create(key: Gitlab.config.backup.upload.remote_directory)
+ connection.directories.create(key: Gitlab.config.backup.upload.remote_directory) # rubocop:disable Rails/SaveBang
end
it 'does not attempt to set ACL' do
diff --git a/spec/lib/backup/repositories_spec.rb b/spec/lib/backup/repositories_spec.rb
index f3830da344b..0b29a25360d 100644
--- a/spec/lib/backup/repositories_spec.rb
+++ b/spec/lib/backup/repositories_spec.rb
@@ -6,8 +6,17 @@ RSpec.describe Backup::Repositories do
let(:progress) { spy(:stdout) }
let(:parallel_enqueue) { true }
let(:strategy) { spy(:strategy, parallel_enqueue?: parallel_enqueue) }
-
- subject { described_class.new(progress, strategy: strategy) }
+ let(:max_concurrency) { 1 }
+ let(:max_storage_concurrency) { 1 }
+
+ subject do
+ described_class.new(
+ progress,
+ strategy: strategy,
+ max_concurrency: max_concurrency,
+ max_storage_concurrency: max_storage_concurrency
+ )
+ end
describe '#dump' do
let_it_be(:projects) { create_list(:project, 5, :repository) }
@@ -15,9 +24,9 @@ RSpec.describe Backup::Repositories do
RSpec.shared_examples 'creates repository bundles' do
it 'calls enqueue for each repository type', :aggregate_failures do
project_snippet = create(:project_snippet, :repository, project: project)
- personal_snippet = create(:personal_snippet, :repository, author: project.owner)
+ personal_snippet = create(:personal_snippet, :repository, author: project.first_owner)
- subject.dump(max_concurrency: 1, max_storage_concurrency: 1)
+ subject.dump
expect(strategy).to have_received(:start).with(:create)
expect(strategy).to have_received(:enqueue).with(project, Gitlab::GlRepository::PROJECT)
@@ -51,38 +60,40 @@ RSpec.describe Backup::Repositories do
end
expect(strategy).to receive(:finish!)
- subject.dump(max_concurrency: 1, max_storage_concurrency: 1)
+ subject.dump
end
describe 'command failure' do
it 'enqueue_project raises an error' do
allow(strategy).to receive(:enqueue).with(anything, Gitlab::GlRepository::PROJECT).and_raise(IOError)
- expect { subject.dump(max_concurrency: 1, max_storage_concurrency: 1) }.to raise_error(IOError)
+ expect { subject.dump }.to raise_error(IOError)
end
it 'project query raises an error' do
allow(Project).to receive_message_chain(:includes, :find_each).and_raise(ActiveRecord::StatementTimeout)
- expect { subject.dump(max_concurrency: 1, max_storage_concurrency: 1) }.to raise_error(ActiveRecord::StatementTimeout)
+ expect { subject.dump }.to raise_error(ActiveRecord::StatementTimeout)
end
end
it 'avoids N+1 database queries' do
control_count = ActiveRecord::QueryRecorder.new do
- subject.dump(max_concurrency: 1, max_storage_concurrency: 1)
+ subject.dump
end.count
create_list(:project, 2, :repository)
expect do
- subject.dump(max_concurrency: 1, max_storage_concurrency: 1)
+ subject.dump
end.not_to exceed_query_limit(control_count)
end
end
context 'concurrency with a strategy without parallel enqueueing support' do
let(:parallel_enqueue) { false }
+ let(:max_concurrency) { 2 }
+ let(:max_storage_concurrency) { 2 }
it 'enqueues all projects sequentially' do
expect(Thread).not_to receive(:new)
@@ -93,13 +104,14 @@ RSpec.describe Backup::Repositories do
end
expect(strategy).to receive(:finish!)
- subject.dump(max_concurrency: 2, max_storage_concurrency: 2)
+ subject.dump
end
end
[4, 10].each do |max_storage_concurrency|
context "max_storage_concurrency #{max_storage_concurrency}", quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/241701' do
let(:storage_keys) { %w[default test_second_storage] }
+ let(:max_storage_concurrency) { max_storage_concurrency }
before do
allow(Gitlab.config.repositories.storages).to receive(:keys).and_return(storage_keys)
@@ -116,54 +128,58 @@ RSpec.describe Backup::Repositories do
end
expect(strategy).to receive(:finish!)
- subject.dump(max_concurrency: 1, max_storage_concurrency: max_storage_concurrency)
+ subject.dump
end
- it 'creates the expected number of threads with extra max concurrency' do
- expect(Thread).to receive(:new)
- .exactly(storage_keys.length * (max_storage_concurrency + 1)).times
- .and_call_original
+ context 'with extra max concurrency' do
+ let(:max_concurrency) { 3 }
- expect(strategy).to receive(:start).with(:create)
- projects.each do |project|
- expect(strategy).to receive(:enqueue).with(project, Gitlab::GlRepository::PROJECT)
- end
- expect(strategy).to receive(:finish!)
+ it 'creates the expected number of threads' do
+ expect(Thread).to receive(:new)
+ .exactly(storage_keys.length * (max_storage_concurrency + 1)).times
+ .and_call_original
- subject.dump(max_concurrency: 3, max_storage_concurrency: max_storage_concurrency)
+ expect(strategy).to receive(:start).with(:create)
+ projects.each do |project|
+ expect(strategy).to receive(:enqueue).with(project, Gitlab::GlRepository::PROJECT)
+ end
+ expect(strategy).to receive(:finish!)
+
+ subject.dump
+ end
end
describe 'command failure' do
it 'enqueue_project raises an error' do
allow(strategy).to receive(:enqueue).and_raise(IOError)
- expect { subject.dump(max_concurrency: 1, max_storage_concurrency: max_storage_concurrency) }.to raise_error(IOError)
+ expect { subject.dump }.to raise_error(IOError)
end
it 'project query raises an error' do
allow(Project).to receive_message_chain(:for_repository_storage, :includes, :find_each).and_raise(ActiveRecord::StatementTimeout)
- expect { subject.dump(max_concurrency: 1, max_storage_concurrency: max_storage_concurrency) }.to raise_error(ActiveRecord::StatementTimeout)
+ expect { subject.dump }.to raise_error(ActiveRecord::StatementTimeout)
end
context 'misconfigured storages' do
let(:storage_keys) { %w[test_second_storage] }
it 'raises an error' do
- expect { subject.dump(max_concurrency: 1, max_storage_concurrency: max_storage_concurrency) }.to raise_error(Backup::Error, 'repositories.storages in gitlab.yml is misconfigured')
+ expect { subject.dump }.to raise_error(Backup::Error, 'repositories.storages in gitlab.yml is misconfigured')
end
end
end
it 'avoids N+1 database queries' do
control_count = ActiveRecord::QueryRecorder.new do
- subject.dump(max_concurrency: 1, max_storage_concurrency: max_storage_concurrency)
+ subject.dump
end.count
create_list(:project, 2, :repository)
expect do
- subject.dump(max_concurrency: 1, max_storage_concurrency: max_storage_concurrency)
+ subject.dump
end.not_to exceed_query_limit(control_count)
end
end
@@ -172,8 +188,8 @@ RSpec.describe Backup::Repositories do
describe '#restore' do
let_it_be(:project) { create(:project) }
- let_it_be(:personal_snippet) { create(:personal_snippet, author: project.owner) }
- let_it_be(:project_snippet) { create(:project_snippet, project: project, author: project.owner) }
+ let_it_be(:personal_snippet) { create(:personal_snippet, author: project.first_owner) }
+ let_it_be(:project_snippet) { create(:project_snippet, project: project, author: project.first_owner) }
it 'calls enqueue for each repository type', :aggregate_failures do
subject.restore
diff --git a/spec/lib/banzai/filter/external_link_filter_spec.rb b/spec/lib/banzai/filter/external_link_filter_spec.rb
index 24d13bdb42c..036817834d5 100644
--- a/spec/lib/banzai/filter/external_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/external_link_filter_spec.rb
@@ -71,6 +71,13 @@ RSpec.describe Banzai::Filter::ExternalLinkFilter do
expect(doc.to_html).to eq(expected)
end
+
+ it 'adds rel and target attributes to improperly formatted protocols' do
+ doc = filter %q(<p><a target="_blank" href="http:evil.com">Reverse Tabnabbing</a></p>)
+ expected = %q(<p><a target="_blank" href="http:evil.com" rel="nofollow noreferrer noopener">Reverse Tabnabbing</a></p>)
+
+ expect(doc.to_html).to eq(expected)
+ end
end
context 'for links with a username' do
diff --git a/spec/lib/banzai/filter/references/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/references/issue_reference_filter_spec.rb
index b3523a25116..c493cb77c98 100644
--- a/spec/lib/banzai/filter/references/issue_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/issue_reference_filter_spec.rb
@@ -515,7 +515,7 @@ RSpec.describe Banzai::Filter::References::IssueReferenceFilter do
enable_design_management(enabled)
end
- let(:current_user) { project.owner }
+ let(:current_user) { project.first_owner }
let(:enabled) { true }
let(:matches) { Issue.link_reference_pattern.match(input_text) }
let(:extras) { subject.object_link_text_extras(issue, matches) }
diff --git a/spec/lib/banzai/filter/table_of_contents_filter_spec.rb b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb
index 6e90f4457fa..91c644cb16a 100644
--- a/spec/lib/banzai/filter/table_of_contents_filter_spec.rb
+++ b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb
@@ -91,6 +91,12 @@ RSpec.describe Banzai::Filter::TableOfContentsFilter do
# ExternalLinkFilter (see https://gitlab.com/gitlab-org/gitlab/issues/26210)
expect(doc.css('h1 a').first.attr('href')).to eq "##{CGI.escape('한글')}"
end
+
+ it 'limits header href length with 255 characters' do
+ doc = filter(header(1, 'a' * 500))
+
+ expect(doc.css('h1 a').first.attr('href')).to eq "##{'a' * 255}"
+ end
end
end
diff --git a/spec/lib/banzai/object_renderer_spec.rb b/spec/lib/banzai/object_renderer_spec.rb
index e64ab5dfce3..8f69480c65f 100644
--- a/spec/lib/banzai/object_renderer_spec.rb
+++ b/spec/lib/banzai/object_renderer_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Banzai::ObjectRenderer do
let(:project) { create(:project, :repository) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:renderer) do
described_class.new(
default_project: project,
diff --git a/spec/lib/bitbucket_server/representation/repo_spec.rb b/spec/lib/bitbucket_server/representation/repo_spec.rb
index 7a773f47ca5..5de4360bbd0 100644
--- a/spec/lib/bitbucket_server/representation/repo_spec.rb
+++ b/spec/lib/bitbucket_server/representation/repo_spec.rb
@@ -9,6 +9,7 @@ RSpec.describe BitbucketServer::Representation::Repo do
"slug": "rouge",
"id": 1,
"name": "rouge",
+ "description": "Rogue Repo",
"scmId": "git",
"state": "AVAILABLE",
"statusMessage": "Available",
@@ -17,7 +18,7 @@ RSpec.describe BitbucketServer::Representation::Repo do
"key": "TEST",
"id": 1,
"name": "test",
- "description": "Test",
+ "description": "Test Project",
"public": false,
"type": "NORMAL",
"links": {
@@ -73,7 +74,7 @@ RSpec.describe BitbucketServer::Representation::Repo do
end
describe '#description' do
- it { expect(subject.description).to eq('Test') }
+ it { expect(subject.description).to eq('Rogue Repo') }
end
describe '#full_name' do
diff --git a/spec/lib/bulk_imports/common/extractors/graphql_extractor_spec.rb b/spec/lib/bulk_imports/common/extractors/graphql_extractor_spec.rb
index 80607485b6e..50c54a7b47f 100644
--- a/spec/lib/bulk_imports/common/extractors/graphql_extractor_spec.rb
+++ b/spec/lib/bulk_imports/common/extractors/graphql_extractor_spec.rb
@@ -8,12 +8,15 @@ RSpec.describe BulkImports::Common::Extractors::GraphqlExtractor do
let(:response) { double(original_hash: { 'data' => { 'foo' => 'bar' }, 'page_info' => {} }) }
let(:options) do
{
- query: double(
- to_s: 'test',
- variables: {},
- data_path: %w[data foo],
- page_info_path: %w[data page_info]
- )
+ query:
+ double(
+ new: double(
+ to_s: 'test',
+ variables: {},
+ data_path: %w[data foo],
+ page_info_path: %w[data page_info]
+ )
+ )
}
end
diff --git a/spec/lib/bulk_imports/common/graphql/get_members_query_spec.rb b/spec/lib/bulk_imports/common/graphql/get_members_query_spec.rb
new file mode 100644
index 00000000000..e3a7335a238
--- /dev/null
+++ b/spec/lib/bulk_imports/common/graphql/get_members_query_spec.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::Common::Graphql::GetMembersQuery do
+ let(:entity) { create(:bulk_import_entity, :group_entity) }
+ let(:tracker) { create(:bulk_import_tracker, entity: entity) }
+ let(:context) { BulkImports::Pipeline::Context.new(tracker) }
+
+ subject(:query) { described_class.new(context: context) }
+
+ it 'has a valid query' do
+ parsed_query = GraphQL::Query.new(
+ GitlabSchema,
+ query.to_s,
+ variables: query.variables
+ )
+ result = GitlabSchema.static_validator.validate(parsed_query)
+
+ expect(result[:errors]).to be_empty
+ end
+
+ describe '#data_path' do
+ it 'returns data path' do
+ expected = %w[data portable members nodes]
+
+ expect(query.data_path).to eq(expected)
+ end
+ end
+
+ describe '#page_info_path' do
+ it 'returns pagination information path' do
+ expected = %w[data portable members page_info]
+
+ expect(query.page_info_path).to eq(expected)
+ end
+ end
+
+ describe '#to_s' do
+ context 'when entity is group' do
+ it 'queries group & group members' do
+ expect(query.to_s).to include('group')
+ expect(query.to_s).to include('groupMembers')
+ end
+ end
+
+ context 'when entity is project' do
+ let(:entity) { create(:bulk_import_entity, :project_entity) }
+
+ it 'queries project & project members' do
+ expect(query.to_s).to include('project')
+ expect(query.to_s).to include('projectMembers')
+ end
+ end
+ end
+end
diff --git a/spec/lib/bulk_imports/common/pipelines/lfs_objects_pipeline_spec.rb b/spec/lib/bulk_imports/common/pipelines/lfs_objects_pipeline_spec.rb
new file mode 100644
index 00000000000..b769aa4af5a
--- /dev/null
+++ b/spec/lib/bulk_imports/common/pipelines/lfs_objects_pipeline_spec.rb
@@ -0,0 +1,210 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::Common::Pipelines::LfsObjectsPipeline do
+ let_it_be(:portable) { create(:project) }
+ let_it_be(:oid) { 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' }
+
+ let(:tmpdir) { Dir.mktmpdir }
+ let(:entity) { create(:bulk_import_entity, :project_entity, project: portable, source_full_path: 'test') }
+ let(:tracker) { create(:bulk_import_tracker, entity: entity) }
+ let(:context) { BulkImports::Pipeline::Context.new(tracker) }
+ let(:lfs_dir_path) { tmpdir }
+ let(:lfs_json_file_path) { File.join(lfs_dir_path, 'lfs_objects.json')}
+ let(:lfs_file_path) { File.join(lfs_dir_path, oid)}
+
+ subject(:pipeline) { described_class.new(context) }
+
+ before do
+ FileUtils.mkdir_p(lfs_dir_path)
+ FileUtils.touch(lfs_json_file_path)
+ FileUtils.touch(lfs_file_path)
+ File.write(lfs_json_file_path, { oid => [0, 1, 2, nil] }.to_json )
+
+ allow(Dir).to receive(:mktmpdir).with('bulk_imports').and_return(tmpdir)
+ end
+
+ after do
+ FileUtils.remove_entry(tmpdir) if Dir.exist?(tmpdir)
+ end
+
+ describe '#run' do
+ it 'imports lfs objects into destination project and removes tmpdir' do
+ allow(pipeline)
+ .to receive(:extract)
+ .and_return(BulkImports::Pipeline::ExtractedData.new(data: [lfs_json_file_path, lfs_file_path]))
+
+ pipeline.run
+
+ expect(portable.lfs_objects.count).to eq(1)
+ expect(portable.lfs_objects_projects.count).to eq(4)
+ expect(Dir.exist?(tmpdir)).to eq(false)
+ end
+ end
+
+ describe '#extract' do
+ it 'downloads & extracts lfs objects filepaths' do
+ download_service = instance_double("BulkImports::FileDownloadService")
+ decompression_service = instance_double("BulkImports::FileDecompressionService")
+ extraction_service = instance_double("BulkImports::ArchiveExtractionService")
+
+ expect(BulkImports::FileDownloadService)
+ .to receive(:new)
+ .with(
+ configuration: context.configuration,
+ relative_url: "/#{entity.pluralized_name}/test/export_relations/download?relation=lfs_objects",
+ tmpdir: tmpdir,
+ filename: 'lfs_objects.tar.gz')
+ .and_return(download_service)
+ expect(BulkImports::FileDecompressionService).to receive(:new).with(tmpdir: tmpdir, filename: 'lfs_objects.tar.gz').and_return(decompression_service)
+ expect(BulkImports::ArchiveExtractionService).to receive(:new).with(tmpdir: tmpdir, filename: 'lfs_objects.tar').and_return(extraction_service)
+
+ expect(download_service).to receive(:execute)
+ expect(decompression_service).to receive(:execute)
+ expect(extraction_service).to receive(:execute)
+
+ extracted_data = pipeline.extract(context)
+
+ expect(extracted_data.data).to contain_exactly(lfs_json_file_path, lfs_file_path)
+ end
+ end
+
+ describe '#load' do
+ before do
+ allow(pipeline)
+ .to receive(:extract)
+ .and_return(BulkImports::Pipeline::ExtractedData.new(data: [lfs_json_file_path, lfs_file_path]))
+ end
+
+ context 'when file path is lfs json' do
+ it 'returns' do
+ filepath = File.join(tmpdir, 'lfs_objects.json')
+
+ allow(Gitlab::Json).to receive(:parse).with(filepath).and_return({})
+
+ expect { pipeline.load(context, filepath) }.not_to change { portable.lfs_objects.count }
+ end
+ end
+
+ context 'when file path is tar file' do
+ it 'returns' do
+ filepath = File.join(tmpdir, 'lfs_objects.tar')
+
+ expect { pipeline.load(context, filepath) }.not_to change { portable.lfs_objects.count }
+ end
+ end
+
+ context 'when lfs json read failed' do
+ it 'raises an error' do
+ File.write(lfs_json_file_path, 'invalid json')
+
+ expect { pipeline.load(context, lfs_file_path) }.to raise_error(BulkImports::Error, 'LFS Objects JSON read failed')
+ end
+ end
+
+ context 'when file path is being traversed' do
+ it 'raises an error' do
+ expect { pipeline.load(context, File.join(tmpdir, '..')) }.to raise_error(Gitlab::Utils::PathTraversalAttackError, 'Invalid path')
+ end
+ end
+
+ context 'when file path is not under tmpdir' do
+ it 'returns' do
+ expect { pipeline.load(context, '/home/test.txt') }.to raise_error(StandardError, 'path /home/test.txt is not allowed')
+ end
+ end
+
+ context 'when file path is symlink' do
+ it 'returns' do
+ symlink = File.join(tmpdir, 'symlink')
+
+ FileUtils.ln_s(File.join(tmpdir, lfs_file_path), symlink)
+
+ expect { pipeline.load(context, symlink) }.not_to change { portable.lfs_objects.count }
+ end
+ end
+
+ context 'when path is a directory' do
+ it 'returns' do
+ expect { pipeline.load(context, Dir.tmpdir) }.not_to change { portable.lfs_objects.count }
+ end
+ end
+
+ context 'lfs objects project' do
+ context 'when lfs objects json is invalid' do
+ context 'when oid value is not Array' do
+ it 'does not create lfs objects project' do
+ File.write(lfs_json_file_path, { oid => 'test' }.to_json )
+
+ expect { pipeline.load(context, lfs_file_path) }.not_to change { portable.lfs_objects_projects.count }
+ end
+ end
+
+ context 'when oid value is nil' do
+ it 'does not create lfs objects project' do
+ File.write(lfs_json_file_path, { oid => nil }.to_json )
+
+ expect { pipeline.load(context, lfs_file_path) }.not_to change { portable.lfs_objects_projects.count }
+ end
+ end
+
+ context 'when oid value is not allowed' do
+ it 'does not create lfs objects project' do
+ File.write(lfs_json_file_path, { oid => ['invalid'] }.to_json )
+
+ expect { pipeline.load(context, lfs_file_path) }.not_to change { portable.lfs_objects_projects.count }
+ end
+ end
+
+ context 'when repository type is duplicated' do
+ it 'creates only one lfs objects project' do
+ File.write(lfs_json_file_path, { oid => [0, 0, 1, 1, 2, 2] }.to_json )
+
+ expect { pipeline.load(context, lfs_file_path) }.to change { portable.lfs_objects_projects.count }.by(3)
+ end
+ end
+ end
+
+ context 'when lfs objects project fails to be created' do
+ it 'logs the failure' do
+ allow_next_instance_of(LfsObjectsProject) do |object|
+ allow(object).to receive(:persisted?).and_return(false)
+ end
+
+ expect_next_instance_of(Gitlab::Import::Logger) do |logger|
+ expect(logger)
+ .to receive(:warn)
+ .with(project_id: portable.id,
+ message: 'Failed to save lfs objects project',
+ errors: '', **Gitlab::ApplicationContext.current)
+ .exactly(4).times
+ end
+
+ pipeline.load(context, lfs_file_path)
+ end
+ end
+ end
+ end
+
+ describe '#after_run' do
+ it 'removes tmpdir' do
+ allow(FileUtils).to receive(:remove_entry).and_call_original
+ expect(FileUtils).to receive(:remove_entry).with(tmpdir).and_call_original
+
+ pipeline.after_run(nil)
+
+ expect(Dir.exist?(tmpdir)).to eq(false)
+ end
+
+ context 'when tmpdir does not exist' do
+ it 'does not attempt to remove tmpdir' do
+ FileUtils.remove_entry(tmpdir)
+
+ expect(FileUtils).not_to receive(:remove_entry).with(tmpdir)
+
+ pipeline.after_run(nil)
+ end
+ end
+ end
+end
diff --git a/spec/lib/bulk_imports/common/pipelines/members_pipeline_spec.rb b/spec/lib/bulk_imports/common/pipelines/members_pipeline_spec.rb
new file mode 100644
index 00000000000..f9b95f79104
--- /dev/null
+++ b/spec/lib/bulk_imports/common/pipelines/members_pipeline_spec.rb
@@ -0,0 +1,161 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::Common::Pipelines::MembersPipeline do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:bulk_import) { create(:bulk_import, user: user) }
+ let_it_be(:member_user1) { create(:user, email: 'email1@email.com') }
+ let_it_be(:member_user2) { create(:user, email: 'email2@email.com') }
+ let_it_be(:member_data) do
+ {
+ user_id: member_user1.id,
+ created_by_id: member_user2.id,
+ access_level: 30,
+ created_at: '2020-01-01T00:00:00Z',
+ updated_at: '2020-01-01T00:00:00Z',
+ expires_at: nil
+ }
+ end
+
+ let(:parent) { create(:group) }
+ let(:tracker) { create(:bulk_import_tracker, entity: entity) }
+ let(:context) { BulkImports::Pipeline::Context.new(tracker) }
+ let(:members) { portable.members.map { |m| m.slice(:user_id, :access_level) } }
+
+ subject(:pipeline) { described_class.new(context) }
+
+ def extracted_data(email:, has_next_page: false)
+ data = {
+ 'created_at' => '2020-01-01T00:00:00Z',
+ 'updated_at' => '2020-01-02T00:00:00Z',
+ 'expires_at' => nil,
+ 'access_level' => {
+ 'integer_value' => 30
+ },
+ 'user' => {
+ 'public_email' => email
+ }
+ }
+
+ page_info = {
+ 'has_next_page' => has_next_page,
+ 'next_page' => has_next_page ? 'cursor' : nil
+ }
+
+ BulkImports::Pipeline::ExtractedData.new(data: data, page_info: page_info)
+ end
+
+ shared_examples 'members import' do
+ before do
+ portable.members.delete_all
+ end
+
+ describe '#run' do
+ it 'creates memberships for existing users' do
+ first_page = extracted_data(email: member_user1.email, has_next_page: true)
+ last_page = extracted_data(email: member_user2.email)
+
+ allow_next_instance_of(BulkImports::Common::Extractors::GraphqlExtractor) do |extractor|
+ allow(extractor).to receive(:extract).and_return(first_page, last_page)
+ end
+
+ expect { pipeline.run }.to change(portable.members, :count).by(2)
+
+ expect(members).to contain_exactly(
+ { user_id: member_user1.id, access_level: 30 },
+ { user_id: member_user2.id, access_level: 30 }
+ )
+ end
+ end
+
+ describe '#load' do
+ it 'creates new membership' do
+ expect { subject.load(context, member_data) }.to change(portable.members, :count).by(1)
+
+ member = portable.members.find_by_user_id(member_user1.id)
+
+ expect(member.user).to eq(member_user1)
+ expect(member.created_by).to eq(member_user2)
+ expect(member.access_level).to eq(30)
+ expect(member.created_at).to eq('2020-01-01T00:00:00Z')
+ expect(member.updated_at).to eq('2020-01-01T00:00:00Z')
+ expect(member.expires_at).to eq(nil)
+ end
+
+ context 'when user_id is current user id' do
+ it 'does not create new membership' do
+ data = { user_id: user.id }
+
+ expect { pipeline.load(context, data) }.not_to change(portable.members, :count)
+ end
+ end
+
+ context 'when data is nil' do
+ it 'does not create new membership' do
+ expect { pipeline.load(context, nil) }.not_to change(portable.members, :count)
+ end
+ end
+
+ context 'when user membership already exists with the same access level' do
+ it 'does not create new membership' do
+ portable.members.create!(member_data)
+
+ expect { pipeline.load(context, member_data) }.not_to change(portable.members, :count)
+ end
+ end
+
+ context 'when portable is in a parent group' do
+ let(:tracker) { create(:bulk_import_tracker, entity: entity_with_parent) }
+
+ before do
+ parent.members.create!(member_data)
+ end
+
+ context 'when the same membership exists in parent group' do
+ it 'does not create new membership' do
+ expect { pipeline.load(context, member_data) }.not_to change(portable_with_parent.members, :count)
+ end
+ end
+
+ context 'when membership with higher access level exists in parent group' do
+ it 'creates new direct membership' do
+ data = member_data.merge(access_level: Gitlab::Access::MAINTAINER)
+
+ expect { pipeline.load(context, data) }.to change(portable_with_parent.members, :count)
+
+ member = portable_with_parent.members.find_by_user_id(member_user1.id)
+
+ expect(member.access_level).to eq(Gitlab::Access::MAINTAINER)
+ end
+ end
+
+ context 'when membership with lower access level exists in parent group' do
+ it 'does not create new membership' do
+ data = member_data.merge(access_level: Gitlab::Access::GUEST)
+
+ expect { pipeline.load(context, data) }.not_to change(portable_with_parent.members, :count)
+ end
+ end
+ end
+ end
+ end
+
+ context 'when importing to group' do
+ let(:portable) { create(:group) }
+ let(:portable_with_parent) { create(:group, parent: parent) }
+ let(:entity) { create(:bulk_import_entity, :group_entity, group: portable, bulk_import: bulk_import) }
+ let(:entity_with_parent) { create(:bulk_import_entity, :group_entity, group: portable_with_parent, bulk_import: bulk_import) }
+
+ include_examples 'members import'
+ end
+
+ context 'when importing to project' do
+ let(:portable) { create(:project) }
+ let(:portable_with_parent) { create(:project, namespace: parent) }
+ let(:entity) { create(:bulk_import_entity, :project_entity, project: portable, bulk_import: bulk_import) }
+ let(:entity_with_parent) { create(:bulk_import_entity, :project_entity, project: portable_with_parent, bulk_import: bulk_import) }
+
+ include_examples 'members import'
+ end
+end
diff --git a/spec/lib/bulk_imports/groups/graphql/get_group_query_spec.rb b/spec/lib/bulk_imports/groups/graphql/get_group_query_spec.rb
index b0f8f74783b..d03b8d8b5b2 100644
--- a/spec/lib/bulk_imports/groups/graphql/get_group_query_spec.rb
+++ b/spec/lib/bulk_imports/groups/graphql/get_group_query_spec.rb
@@ -3,14 +3,27 @@
require 'spec_helper'
RSpec.describe BulkImports::Groups::Graphql::GetGroupQuery do
+ let_it_be(:tracker) { create(:bulk_import_tracker) }
+ let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) }
+
+ subject(:query) { described_class.new(context: context) }
+
+ it 'has a valid query' do
+ parsed_query = GraphQL::Query.new(
+ GitlabSchema,
+ query.to_s,
+ variables: query.variables
+ )
+ result = GitlabSchema.static_validator.validate(parsed_query)
+
+ expect(result[:errors]).to be_empty
+ end
+
describe '#variables' do
it 'returns query variables based on entity information' do
- entity = double(source_full_path: 'test', bulk_import: nil)
- tracker = double(entity: entity)
- context = BulkImports::Pipeline::Context.new(tracker)
- expected = { full_path: entity.source_full_path }
+ expected = { full_path: tracker.entity.source_full_path }
- expect(described_class.variables(context)).to eq(expected)
+ expect(subject.variables).to eq(expected)
end
end
@@ -18,7 +31,7 @@ RSpec.describe BulkImports::Groups::Graphql::GetGroupQuery do
it 'returns data path' do
expected = %w[data group]
- expect(described_class.data_path).to eq(expected)
+ expect(subject.data_path).to eq(expected)
end
end
@@ -26,7 +39,7 @@ RSpec.describe BulkImports::Groups::Graphql::GetGroupQuery do
it 'returns pagination information path' do
expected = %w[data group page_info]
- expect(described_class.page_info_path).to eq(expected)
+ expect(subject.page_info_path).to eq(expected)
end
end
end
diff --git a/spec/lib/bulk_imports/groups/graphql/get_members_query_spec.rb b/spec/lib/bulk_imports/groups/graphql/get_members_query_spec.rb
deleted file mode 100644
index d0c4bb817b2..00000000000
--- a/spec/lib/bulk_imports/groups/graphql/get_members_query_spec.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe BulkImports::Groups::Graphql::GetMembersQuery do
- it 'has a valid query' do
- tracker = create(:bulk_import_tracker)
- context = BulkImports::Pipeline::Context.new(tracker)
-
- query = GraphQL::Query.new(
- GitlabSchema,
- described_class.to_s,
- variables: described_class.variables(context)
- )
- result = GitlabSchema.static_validator.validate(query)
-
- expect(result[:errors]).to be_empty
- end
-
- describe '#data_path' do
- it 'returns data path' do
- expected = %w[data group group_members nodes]
-
- expect(described_class.data_path).to eq(expected)
- end
- end
-
- describe '#page_info_path' do
- it 'returns pagination information path' do
- expected = %w[data group group_members page_info]
-
- expect(described_class.page_info_path).to eq(expected)
- end
- end
-end
diff --git a/spec/lib/bulk_imports/groups/graphql/get_projects_query_spec.rb b/spec/lib/bulk_imports/groups/graphql/get_projects_query_spec.rb
index 1a7c5a4993c..fe28e3959a0 100644
--- a/spec/lib/bulk_imports/groups/graphql/get_projects_query_spec.rb
+++ b/spec/lib/bulk_imports/groups/graphql/get_projects_query_spec.rb
@@ -3,25 +3,25 @@
require 'spec_helper'
RSpec.describe BulkImports::Groups::Graphql::GetProjectsQuery do
- describe '#variables' do
- it 'returns valid variables based on entity information' do
- tracker = create(:bulk_import_tracker)
- context = BulkImports::Pipeline::Context.new(tracker)
-
- query = GraphQL::Query.new(
- GitlabSchema,
- described_class.to_s,
- variables: described_class.variables(context)
- )
- result = GitlabSchema.static_validator.validate(query)
-
- expect(result[:errors]).to be_empty
- end
+ let_it_be(:tracker) { create(:bulk_import_tracker) }
+ let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) }
+
+ subject(:query) { described_class.new(context: context) }
+
+ it 'has a valid query' do
+ parsed_query = GraphQL::Query.new(
+ GitlabSchema,
+ query.to_s,
+ variables: query.variables
+ )
+ result = GitlabSchema.static_validator.validate(parsed_query)
+
+ expect(result[:errors]).to be_empty
+ end
- context 'with invalid variables' do
- it 'raises an error' do
- expect { GraphQL::Query.new(GitlabSchema, described_class.to_s, variables: 'invalid') }.to raise_error(ArgumentError)
- end
+ context 'with invalid variables' do
+ it 'raises an error' do
+ expect { GraphQL::Query.new(GitlabSchema, subject.to_s, variables: 'invalid') }.to raise_error(ArgumentError)
end
end
@@ -29,7 +29,7 @@ RSpec.describe BulkImports::Groups::Graphql::GetProjectsQuery do
it 'returns data path' do
expected = %w[data group projects nodes]
- expect(described_class.data_path).to eq(expected)
+ expect(subject.data_path).to eq(expected)
end
end
@@ -37,7 +37,7 @@ RSpec.describe BulkImports::Groups::Graphql::GetProjectsQuery do
it 'returns pagination information path' do
expected = %w[data group projects page_info]
- expect(described_class.page_info_path).to eq(expected)
+ expect(subject.page_info_path).to eq(expected)
end
end
end
diff --git a/spec/lib/bulk_imports/groups/pipelines/members_pipeline_spec.rb b/spec/lib/bulk_imports/groups/pipelines/members_pipeline_spec.rb
deleted file mode 100644
index 0126acb320b..00000000000
--- a/spec/lib/bulk_imports/groups/pipelines/members_pipeline_spec.rb
+++ /dev/null
@@ -1,119 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe BulkImports::Groups::Pipelines::MembersPipeline do
- let_it_be(:member_user1) { create(:user, email: 'email1@email.com') }
- let_it_be(:member_user2) { create(:user, email: 'email2@email.com') }
-
- let_it_be(:user) { create(:user) }
- let_it_be(:group) { create(:group) }
- let_it_be(:bulk_import) { create(:bulk_import, user: user) }
- let_it_be(:entity) { create(:bulk_import_entity, bulk_import: bulk_import, group: group) }
- let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) }
- let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) }
-
- subject { described_class.new(context) }
-
- describe '#run' do
- it 'maps existing users to the imported group' do
- first_page = extracted_data(email: member_user1.email, has_next_page: true)
- last_page = extracted_data(email: member_user2.email)
-
- allow_next_instance_of(BulkImports::Common::Extractors::GraphqlExtractor) do |extractor|
- allow(extractor)
- .to receive(:extract)
- .and_return(first_page, last_page)
- end
-
- expect { subject.run }.to change(GroupMember, :count).by(2)
-
- members = group.members.map { |m| m.slice(:user_id, :access_level) }
-
- expect(members).to contain_exactly(
- { user_id: member_user1.id, access_level: 30 },
- { user_id: member_user2.id, access_level: 30 }
- )
- end
- end
-
- describe '#load' do
- it 'does nothing when there is no data' do
- expect { subject.load(context, nil) }.not_to change(GroupMember, :count)
- end
-
- it 'creates the member' do
- data = {
- 'user_id' => member_user1.id,
- 'created_by_id' => member_user2.id,
- 'access_level' => 30,
- 'created_at' => '2020-01-01T00:00:00Z',
- 'updated_at' => '2020-01-01T00:00:00Z',
- 'expires_at' => nil
- }
-
- expect { subject.load(context, data) }.to change(GroupMember, :count).by(1)
-
- member = group.members.last
-
- expect(member.user).to eq(member_user1)
- expect(member.created_by).to eq(member_user2)
- expect(member.access_level).to eq(30)
- expect(member.created_at).to eq('2020-01-01T00:00:00Z')
- expect(member.updated_at).to eq('2020-01-01T00:00:00Z')
- expect(member.expires_at).to eq(nil)
- end
-
- context 'when user_id is current user id' do
- it 'does not create new member' do
- data = { 'user_id' => user.id }
-
- expect { subject.load(context, data) }.not_to change(GroupMember, :count)
- end
- end
- end
-
- describe 'pipeline parts' do
- it { expect(described_class).to include_module(BulkImports::Pipeline) }
- it { expect(described_class).to include_module(BulkImports::Pipeline::Runner) }
-
- it 'has extractors' do
- expect(described_class.get_extractor)
- .to eq(
- klass: BulkImports::Common::Extractors::GraphqlExtractor,
- options: {
- query: BulkImports::Groups::Graphql::GetMembersQuery
- }
- )
- end
-
- it 'has transformers' do
- expect(described_class.transformers)
- .to contain_exactly(
- { klass: BulkImports::Common::Transformers::ProhibitedAttributesTransformer, options: nil },
- { klass: BulkImports::Groups::Transformers::MemberAttributesTransformer, options: nil }
- )
- end
- end
-
- def extracted_data(email:, has_next_page: false)
- data = {
- 'created_at' => '2020-01-01T00:00:00Z',
- 'updated_at' => '2020-01-01T00:00:00Z',
- 'expires_at' => nil,
- 'access_level' => {
- 'integer_value' => 30
- },
- 'user' => {
- 'public_email' => email
- }
- }
-
- page_info = {
- 'has_next_page' => has_next_page,
- 'next_page' => has_next_page ? 'cursor' : nil
- }
-
- BulkImports::Pipeline::ExtractedData.new(data: data, page_info: page_info)
- end
-end
diff --git a/spec/lib/bulk_imports/groups/stage_spec.rb b/spec/lib/bulk_imports/groups/stage_spec.rb
index 55a8e40f480..b6bb8a7d195 100644
--- a/spec/lib/bulk_imports/groups/stage_spec.rb
+++ b/spec/lib/bulk_imports/groups/stage_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe BulkImports::Groups::Stage do
[
[0, BulkImports::Groups::Pipelines::GroupPipeline],
[1, BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline],
- [1, BulkImports::Groups::Pipelines::MembersPipeline],
+ [1, BulkImports::Common::Pipelines::MembersPipeline],
[1, BulkImports::Common::Pipelines::LabelsPipeline],
[1, BulkImports::Common::Pipelines::MilestonesPipeline],
[1, BulkImports::Common::Pipelines::BadgesPipeline],
diff --git a/spec/lib/bulk_imports/groups/transformers/member_attributes_transformer_spec.rb b/spec/lib/bulk_imports/groups/transformers/member_attributes_transformer_spec.rb
index af99428e0c1..c8935f71f10 100644
--- a/spec/lib/bulk_imports/groups/transformers/member_attributes_transformer_spec.rb
+++ b/spec/lib/bulk_imports/groups/transformers/member_attributes_transformer_spec.rb
@@ -48,12 +48,12 @@ RSpec.describe BulkImports::Groups::Transformers::MemberAttributesTransformer do
data = member_data(email: user.email)
expect(subject.transform(context, data)).to eq(
- 'access_level' => 30,
- 'user_id' => user.id,
- 'created_by_id' => user.id,
- 'created_at' => '2020-01-01T00:00:00Z',
- 'updated_at' => '2020-01-01T00:00:00Z',
- 'expires_at' => nil
+ access_level: 30,
+ user_id: user.id,
+ created_by_id: user.id,
+ created_at: '2020-01-01T00:00:00Z',
+ updated_at: '2020-01-01T00:00:00Z',
+ expires_at: nil
)
end
@@ -62,12 +62,12 @@ RSpec.describe BulkImports::Groups::Transformers::MemberAttributesTransformer do
data = member_data(email: secondary_email)
expect(subject.transform(context, data)).to eq(
- 'access_level' => 30,
- 'user_id' => user.id,
- 'created_by_id' => user.id,
- 'created_at' => '2020-01-01T00:00:00Z',
- 'updated_at' => '2020-01-01T00:00:00Z',
- 'expires_at' => nil
+ access_level: 30,
+ user_id: user.id,
+ created_by_id: user.id,
+ created_at: '2020-01-01T00:00:00Z',
+ updated_at: '2020-01-01T00:00:00Z',
+ expires_at: nil
)
end
diff --git a/spec/lib/bulk_imports/projects/graphql/get_project_query_spec.rb b/spec/lib/bulk_imports/projects/graphql/get_project_query_spec.rb
new file mode 100644
index 00000000000..6593aa56506
--- /dev/null
+++ b/spec/lib/bulk_imports/projects/graphql/get_project_query_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::Projects::Graphql::GetProjectQuery do
+ let_it_be(:tracker) { create(:bulk_import_tracker) }
+ let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) }
+
+ subject(:query) { described_class.new(context: context) }
+
+ it 'has a valid query' do
+ parsed_query = GraphQL::Query.new(
+ GitlabSchema,
+ query.to_s,
+ variables: query.variables
+ )
+ result = GitlabSchema.static_validator.validate(parsed_query)
+
+ expect(result[:errors]).to be_empty
+ end
+
+ it 'queries project based on source_full_path' do
+ expected = { full_path: tracker.entity.source_full_path }
+
+ expect(subject.variables).to eq(expected)
+ end
+end
diff --git a/spec/lib/bulk_imports/projects/graphql/get_repository_query_spec.rb b/spec/lib/bulk_imports/projects/graphql/get_repository_query_spec.rb
index 4dba81dc0d2..8ed105bc0c9 100644
--- a/spec/lib/bulk_imports/projects/graphql/get_repository_query_spec.rb
+++ b/spec/lib/bulk_imports/projects/graphql/get_repository_query_spec.rb
@@ -3,19 +3,29 @@
require 'spec_helper'
RSpec.describe BulkImports::Projects::Graphql::GetRepositoryQuery do
- describe 'query repository based on full_path' do
- let(:entity) { double(source_full_path: 'test', bulk_import: nil) }
- let(:tracker) { double(entity: entity) }
- let(:context) { BulkImports::Pipeline::Context.new(tracker) }
+ let_it_be(:tracker) { create(:bulk_import_tracker) }
+ let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) }
- it 'returns project repository url' do
- expect(described_class.to_s).to include('httpUrlToRepo')
- end
+ subject(:query) { described_class.new(context: context) }
- it 'queries project based on source_full_path' do
- expected = { full_path: entity.source_full_path }
+ it 'has a valid query' do
+ parsed_query = GraphQL::Query.new(
+ GitlabSchema,
+ query.to_s,
+ variables: query.variables
+ )
+ result = GitlabSchema.static_validator.validate(parsed_query)
- expect(described_class.variables(context)).to eq(expected)
- end
+ expect(result[:errors]).to be_empty
+ end
+
+ it 'returns project repository url' do
+ expect(subject.to_s).to include('httpUrlToRepo')
+ end
+
+ it 'queries project based on source_full_path' do
+ expected = { full_path: tracker.entity.source_full_path }
+
+ expect(subject.variables).to eq(expected)
end
end
diff --git a/spec/lib/bulk_imports/projects/graphql/get_snippet_repository_query_spec.rb b/spec/lib/bulk_imports/projects/graphql/get_snippet_repository_query_spec.rb
index b680fa5cbfc..1bd4106297d 100644
--- a/spec/lib/bulk_imports/projects/graphql/get_snippet_repository_query_spec.rb
+++ b/spec/lib/bulk_imports/projects/graphql/get_snippet_repository_query_spec.rb
@@ -3,56 +3,56 @@
require 'spec_helper'
RSpec.describe BulkImports::Projects::Graphql::GetSnippetRepositoryQuery do
- describe 'query repository based on full_path' do
- let_it_be(:entity) { create(:bulk_import_entity) }
- let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) }
- let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) }
-
- it 'has a valid query' do
- query = GraphQL::Query.new(
- GitlabSchema,
- described_class.to_s,
- variables: described_class.variables(context)
- )
- result = GitlabSchema.static_validator.validate(query)
-
- expect(result[:errors]).to be_empty
- end
+ let_it_be(:entity) { create(:bulk_import_entity) }
+ let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) }
+ let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) }
- it 'returns snippet httpUrlToRepo' do
- expect(described_class.to_s).to include('httpUrlToRepo')
- end
+ subject(:query) { described_class.new(context: context) }
- it 'returns snippet createdAt' do
- expect(described_class.to_s).to include('createdAt')
- end
+ it 'has a valid query' do
+ parsed_query = GraphQL::Query.new(
+ GitlabSchema,
+ query.to_s,
+ variables: query.variables
+ )
+ result = GitlabSchema.static_validator.validate(parsed_query)
- it 'returns snippet title' do
- expect(described_class.to_s).to include('title')
- end
+ expect(result[:errors]).to be_empty
+ end
- describe '.variables' do
- it 'queries project based on source_full_path and pagination' do
- expected = { full_path: entity.source_full_path, cursor: nil, per_page: 500 }
+ it 'returns snippet httpUrlToRepo' do
+ expect(subject.to_s).to include('httpUrlToRepo')
+ end
- expect(described_class.variables(context)).to eq(expected)
- end
+ it 'returns snippet createdAt' do
+ expect(subject.to_s).to include('createdAt')
+ end
+
+ it 'returns snippet title' do
+ expect(subject.to_s).to include('title')
+ end
+
+ describe '.variables' do
+ it 'queries project based on source_full_path and pagination' do
+ expected = { full_path: entity.source_full_path, cursor: nil, per_page: 500 }
+
+ expect(subject.variables).to eq(expected)
end
+ end
- describe '.data_path' do
- it '.data_path returns data path' do
- expected = %w[data project snippets nodes]
+ describe '.data_path' do
+ it '.data_path returns data path' do
+ expected = %w[data project snippets nodes]
- expect(described_class.data_path).to eq(expected)
- end
+ expect(subject.data_path).to eq(expected)
end
+ end
- describe '.page_info_path' do
- it '.page_info_path returns pagination information path' do
- expected = %w[data project snippets page_info]
+ describe '.page_info_path' do
+ it '.page_info_path returns pagination information path' do
+ expected = %w[data project snippets page_info]
- expect(described_class.page_info_path).to eq(expected)
- end
+ expect(subject.page_info_path).to eq(expected)
end
end
end
diff --git a/spec/lib/bulk_imports/projects/stage_spec.rb b/spec/lib/bulk_imports/projects/stage_spec.rb
index 81cbdcae9d1..ef98613dc25 100644
--- a/spec/lib/bulk_imports/projects/stage_spec.rb
+++ b/spec/lib/bulk_imports/projects/stage_spec.rb
@@ -26,6 +26,7 @@ RSpec.describe BulkImports::Projects::Stage do
[4, BulkImports::Projects::Pipelines::ServiceDeskSettingPipeline],
[5, BulkImports::Common::Pipelines::WikiPipeline],
[5, BulkImports::Common::Pipelines::UploadsPipeline],
+ [5, BulkImports::Common::Pipelines::LfsObjectsPipeline],
[5, BulkImports::Projects::Pipelines::AutoDevopsPipeline],
[5, BulkImports::Projects::Pipelines::PipelineSchedulesPipeline],
[6, BulkImports::Common::Pipelines::EntityFinisher]
diff --git a/spec/lib/container_registry/client_spec.rb b/spec/lib/container_registry/client_spec.rb
index 259d7d5ad13..974c3478ddc 100644
--- a/spec/lib/container_registry/client_spec.rb
+++ b/spec/lib/container_registry/client_spec.rb
@@ -5,29 +5,7 @@ require 'spec_helper'
RSpec.describe ContainerRegistry::Client do
using RSpec::Parameterized::TableSyntax
- let(:token) { '12345' }
- let(:options) { { token: token } }
- let(:registry_api_url) { 'http://container-registry' }
- let(:client) { described_class.new(registry_api_url, options) }
- let(:push_blob_headers) do
- {
- 'Accept' => 'application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json',
- 'Authorization' => "bearer #{token}",
- 'Content-Type' => 'application/octet-stream',
- 'User-Agent' => "GitLab/#{Gitlab::VERSION}"
- }
- end
-
- let(:headers_with_accept_types) do
- {
- 'Accept' => 'application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json',
- 'Authorization' => "bearer #{token}",
- 'User-Agent' => "GitLab/#{Gitlab::VERSION}"
- }
- end
-
- let(:expected_faraday_headers) { { user_agent: "GitLab/#{Gitlab::VERSION}" } }
- let(:expected_faraday_request_options) { Gitlab::HTTP::DEFAULT_TIMEOUT_OPTIONS }
+ include_context 'container registry client'
shared_examples 'handling timeouts' do
let(:retry_options) do
@@ -48,14 +26,14 @@ RSpec.describe ContainerRegistry::Client do
retry_block: -> (_, _, _, _) { actual_retries += 1 }
)
- stub_const('ContainerRegistry::Client::RETRY_OPTIONS', retry_options_with_block)
+ stub_const('ContainerRegistry::BaseClient::RETRY_OPTIONS', retry_options_with_block)
expect { subject }.to raise_error(Faraday::ConnectionFailed)
expect(actual_retries).to eq(retry_options_with_block[:max])
end
it 'logs the error' do
- stub_const('ContainerRegistry::Client::RETRY_OPTIONS', retry_options)
+ stub_const('ContainerRegistry::BaseClient::RETRY_OPTIONS', retry_options)
expect(Gitlab::ErrorTracking)
.to receive(:log_exception)
@@ -63,7 +41,7 @@ RSpec.describe ContainerRegistry::Client do
.times
.with(
an_instance_of(Faraday::ConnectionFailed),
- class: described_class.name,
+ class: ::ContainerRegistry::BaseClient.name,
url: URI(url)
)
@@ -325,14 +303,14 @@ RSpec.describe ContainerRegistry::Client do
subject { client.supports_tag_delete? }
where(:registry_tags_support_enabled, :is_on_dot_com, :container_registry_features, :expect_registry_to_be_pinged, :expected_result) do
- true | true | [ContainerRegistry::Client::REGISTRY_TAG_DELETE_FEATURE] | false | true
- true | false | [ContainerRegistry::Client::REGISTRY_TAG_DELETE_FEATURE] | true | true
- true | true | [] | true | true
- true | false | [] | true | true
- false | true | [ContainerRegistry::Client::REGISTRY_TAG_DELETE_FEATURE] | false | true
- false | false | [ContainerRegistry::Client::REGISTRY_TAG_DELETE_FEATURE] | true | false
- false | true | [] | true | false
- false | false | [] | true | false
+ true | true | [described_class::REGISTRY_TAG_DELETE_FEATURE] | false | true
+ true | false | [described_class::REGISTRY_TAG_DELETE_FEATURE] | true | true
+ true | true | [] | true | true
+ true | false | [] | true | true
+ false | true | [described_class::REGISTRY_TAG_DELETE_FEATURE] | false | true
+ false | false | [described_class::REGISTRY_TAG_DELETE_FEATURE] | true | false
+ false | true | [] | true | false
+ false | false | [] | true | false
end
with_them do
@@ -366,38 +344,38 @@ RSpec.describe ContainerRegistry::Client do
subject { described_class.supports_tag_delete? }
where(:registry_api_url, :registry_enabled, :registry_tags_support_enabled, :is_on_dot_com, :container_registry_features, :expect_registry_to_be_pinged, :expected_result) do
- 'http://sandbox.local' | true | true | true | [ContainerRegistry::Client::REGISTRY_TAG_DELETE_FEATURE] | false | true
- 'http://sandbox.local' | true | true | false | [ContainerRegistry::Client::REGISTRY_TAG_DELETE_FEATURE] | true | true
- 'http://sandbox.local' | true | false | true | [ContainerRegistry::Client::REGISTRY_TAG_DELETE_FEATURE] | false | true
- 'http://sandbox.local' | true | false | false | [ContainerRegistry::Client::REGISTRY_TAG_DELETE_FEATURE] | true | false
- 'http://sandbox.local' | false | true | true | [ContainerRegistry::Client::REGISTRY_TAG_DELETE_FEATURE] | false | false
- 'http://sandbox.local' | false | true | false | [ContainerRegistry::Client::REGISTRY_TAG_DELETE_FEATURE] | false | false
- 'http://sandbox.local' | false | false | true | [ContainerRegistry::Client::REGISTRY_TAG_DELETE_FEATURE] | false | false
- 'http://sandbox.local' | false | false | false | [ContainerRegistry::Client::REGISTRY_TAG_DELETE_FEATURE] | false | false
- 'http://sandbox.local' | true | true | true | [] | true | true
- 'http://sandbox.local' | true | true | false | [] | true | true
- 'http://sandbox.local' | true | false | true | [] | true | false
- 'http://sandbox.local' | true | false | false | [] | true | false
- 'http://sandbox.local' | false | true | true | [] | false | false
- 'http://sandbox.local' | false | true | false | [] | false | false
- 'http://sandbox.local' | false | false | true | [] | false | false
- 'http://sandbox.local' | false | false | false | [] | false | false
- '' | true | true | true | [ContainerRegistry::Client::REGISTRY_TAG_DELETE_FEATURE] | false | false
- '' | true | true | false | [ContainerRegistry::Client::REGISTRY_TAG_DELETE_FEATURE] | false | false
- '' | true | false | true | [ContainerRegistry::Client::REGISTRY_TAG_DELETE_FEATURE] | false | false
- '' | true | false | false | [ContainerRegistry::Client::REGISTRY_TAG_DELETE_FEATURE] | false | false
- '' | false | true | true | [ContainerRegistry::Client::REGISTRY_TAG_DELETE_FEATURE] | false | false
- '' | false | true | false | [ContainerRegistry::Client::REGISTRY_TAG_DELETE_FEATURE] | false | false
- '' | false | false | true | [ContainerRegistry::Client::REGISTRY_TAG_DELETE_FEATURE] | false | false
- '' | false | false | false | [ContainerRegistry::Client::REGISTRY_TAG_DELETE_FEATURE] | false | false
- '' | true | true | true | [] | false | false
- '' | true | true | false | [] | false | false
- '' | true | false | true | [] | false | false
- '' | true | false | false | [] | false | false
- '' | false | true | true | [] | false | false
- '' | false | true | false | [] | false | false
- '' | false | false | true | [] | false | false
- '' | false | false | false | [] | false | false
+ 'http://sandbox.local' | true | true | true | [described_class::REGISTRY_TAG_DELETE_FEATURE] | false | true
+ 'http://sandbox.local' | true | true | false | [described_class::REGISTRY_TAG_DELETE_FEATURE] | true | true
+ 'http://sandbox.local' | true | false | true | [described_class::REGISTRY_TAG_DELETE_FEATURE] | false | true
+ 'http://sandbox.local' | true | false | false | [described_class::REGISTRY_TAG_DELETE_FEATURE] | true | false
+ 'http://sandbox.local' | false | true | true | [described_class::REGISTRY_TAG_DELETE_FEATURE] | false | false
+ 'http://sandbox.local' | false | true | false | [described_class::REGISTRY_TAG_DELETE_FEATURE] | false | false
+ 'http://sandbox.local' | false | false | true | [described_class::REGISTRY_TAG_DELETE_FEATURE] | false | false
+ 'http://sandbox.local' | false | false | false | [described_class::REGISTRY_TAG_DELETE_FEATURE] | false | false
+ 'http://sandbox.local' | true | true | true | [] | true | true
+ 'http://sandbox.local' | true | true | false | [] | true | true
+ 'http://sandbox.local' | true | false | true | [] | true | false
+ 'http://sandbox.local' | true | false | false | [] | true | false
+ 'http://sandbox.local' | false | true | true | [] | false | false
+ 'http://sandbox.local' | false | true | false | [] | false | false
+ 'http://sandbox.local' | false | false | true | [] | false | false
+ 'http://sandbox.local' | false | false | false | [] | false | false
+ '' | true | true | true | [described_class::REGISTRY_TAG_DELETE_FEATURE] | false | false
+ '' | true | true | false | [described_class::REGISTRY_TAG_DELETE_FEATURE] | false | false
+ '' | true | false | true | [described_class::REGISTRY_TAG_DELETE_FEATURE] | false | false
+ '' | true | false | false | [described_class::REGISTRY_TAG_DELETE_FEATURE] | false | false
+ '' | false | true | true | [described_class::REGISTRY_TAG_DELETE_FEATURE] | false | false
+ '' | false | true | false | [described_class::REGISTRY_TAG_DELETE_FEATURE] | false | false
+ '' | false | false | true | [described_class::REGISTRY_TAG_DELETE_FEATURE] | false | false
+ '' | false | false | false | [described_class::REGISTRY_TAG_DELETE_FEATURE] | false | false
+ '' | true | true | true | [] | false | false
+ '' | true | true | false | [] | false | false
+ '' | true | false | true | [] | false | false
+ '' | true | false | false | [] | false | false
+ '' | false | true | true | [] | false | false
+ '' | false | true | false | [] | false | false
+ '' | false | false | true | [] | false | false
+ '' | false | false | false | [] | false | false
end
with_them do
diff --git a/spec/lib/container_registry/gitlab_api_client_spec.rb b/spec/lib/container_registry/gitlab_api_client_spec.rb
new file mode 100644
index 00000000000..292582a8d83
--- /dev/null
+++ b/spec/lib/container_registry/gitlab_api_client_spec.rb
@@ -0,0 +1,204 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ContainerRegistry::GitlabApiClient do
+ using RSpec::Parameterized::TableSyntax
+
+ include_context 'container registry client'
+
+ let(:path) { 'namespace/path/to/repository' }
+
+ describe '#supports_gitlab_api?' do
+ subject { client.supports_gitlab_api? }
+
+ where(:registry_gitlab_api_enabled, :is_on_dot_com, :container_registry_features, :expect_registry_to_be_pinged, :expected_result) do
+ false | true | [described_class::REGISTRY_GITLAB_V1_API_FEATURE] | false | true
+ true | false | [described_class::REGISTRY_GITLAB_V1_API_FEATURE] | true | true
+ true | true | [] | true | true
+ true | false | [] | true | true
+ false | true | [described_class::REGISTRY_GITLAB_V1_API_FEATURE] | false | true
+ false | false | [described_class::REGISTRY_GITLAB_V1_API_FEATURE] | true | false
+ false | true | [] | true | false
+ false | false | [] | true | false
+ end
+
+ with_them do
+ before do
+ allow(::Gitlab).to receive(:com?).and_return(is_on_dot_com)
+ stub_registry_gitlab_api_support(registry_gitlab_api_enabled)
+ stub_application_setting(container_registry_features: container_registry_features)
+ end
+
+ it 'returns the expected result' do
+ if expect_registry_to_be_pinged
+ expect(Faraday::Connection).to receive(:new).and_call_original
+ else
+ expect(Faraday::Connection).not_to receive(:new)
+ end
+
+ expect(subject).to be expected_result
+ end
+ end
+
+ context 'with 401 response' do
+ before do
+ allow(::Gitlab).to receive(:com?).and_return(false)
+ stub_application_setting(container_registry_features: [])
+ stub_request(:get, "#{registry_api_url}/gitlab/v1/")
+ .to_return(status: 401, body: '')
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ describe '#pre_import_repository' do
+ subject { client.pre_import_repository(path) }
+
+ where(:status_code, :expected_result) do
+ 200 | :already_imported
+ 202 | :ok
+ 401 | :unauthorized
+ 404 | :not_found
+ 409 | :already_being_imported
+ 418 | :error
+ 424 | :pre_import_failed
+ 425 | :already_being_imported
+ 429 | :too_many_imports
+ end
+
+ with_them do
+ before do
+ stub_pre_import(path, status_code, pre: true)
+ end
+
+ it { is_expected.to eq(expected_result) }
+ end
+ end
+
+ describe '#import_repository' do
+ subject { client.import_repository(path) }
+
+ where(:status_code, :expected_result) do
+ 200 | :already_imported
+ 202 | :ok
+ 401 | :unauthorized
+ 404 | :not_found
+ 409 | :already_being_imported
+ 418 | :error
+ 424 | :pre_import_failed
+ 425 | :already_being_imported
+ 429 | :too_many_imports
+ end
+
+ with_them do
+ before do
+ stub_pre_import(path, status_code, pre: false)
+ end
+
+ it { is_expected.to eq(expected_result) }
+ end
+ end
+
+ describe '#import_status' do
+ subject { client.import_status(path) }
+
+ before do
+ stub_import_status(path, status)
+ end
+
+ context 'with a status' do
+ let(:status) { 'this_is_a_test' }
+
+ it { is_expected.to eq(status) }
+ end
+
+ context 'with no status' do
+ let(:status) { nil }
+
+ it { is_expected.to eq('error') }
+ end
+ end
+
+ describe '.supports_gitlab_api?' do
+ subject { described_class.supports_gitlab_api? }
+
+ where(:registry_gitlab_api_enabled, :is_on_dot_com, :container_registry_features, :expect_registry_to_be_pinged, :expected_result) do
+ true | true | [described_class::REGISTRY_GITLAB_V1_API_FEATURE] | false | true
+ true | false | [described_class::REGISTRY_GITLAB_V1_API_FEATURE] | true | true
+ false | true | [described_class::REGISTRY_GITLAB_V1_API_FEATURE] | false | true
+ false | false | [described_class::REGISTRY_GITLAB_V1_API_FEATURE] | true | false
+ true | true | [] | true | true
+ true | false | [] | true | true
+ false | true | [] | true | false
+ false | false | [] | true | false
+ end
+
+ with_them do
+ before do
+ allow(::Gitlab).to receive(:com?).and_return(is_on_dot_com)
+ stub_container_registry_config(enabled: true, api_url: registry_api_url, key: 'spec/fixtures/x509_certificate_pk.key')
+ stub_registry_gitlab_api_support(registry_gitlab_api_enabled)
+ stub_application_setting(container_registry_features: container_registry_features)
+ end
+
+ it 'returns the expected result' do
+ if expect_registry_to_be_pinged
+ expect(Faraday::Connection).to receive(:new).and_call_original
+ else
+ expect(Faraday::Connection).not_to receive(:new)
+ end
+
+ expect(subject).to be expected_result
+ end
+ end
+
+ context 'with the registry disabled' do
+ before do
+ stub_container_registry_config(enabled: false, api_url: 'http://sandbox.local', key: 'spec/fixtures/x509_certificate_pk.key')
+ end
+
+ it 'returns false' do
+ expect(Faraday::Connection).not_to receive(:new)
+
+ expect(subject).to be_falsey
+ end
+ end
+
+ context 'with a blank registry url' do
+ before do
+ stub_container_registry_config(enabled: true, api_url: '', key: 'spec/fixtures/x509_certificate_pk.key')
+ end
+
+ it 'returns false' do
+ expect(Faraday::Connection).not_to receive(:new)
+
+ expect(subject).to be_falsey
+ end
+ end
+ end
+
+ def stub_pre_import(path, status_code, pre:)
+ stub_request(:put, "#{registry_api_url}/gitlab/v1/import/#{path}/?pre=#{pre}")
+ .with(headers: { 'Accept' => described_class::JSON_TYPE })
+ .to_return(status: status_code, body: '')
+ end
+
+ def stub_registry_gitlab_api_support(supported = true)
+ status_code = supported ? 200 : 404
+ stub_request(:get, "#{registry_api_url}/gitlab/v1/")
+ .with(headers: { 'Accept' => described_class::JSON_TYPE })
+ .to_return(status: status_code, body: '')
+ end
+
+ def stub_import_status(path, status)
+ stub_request(:get, "#{registry_api_url}/gitlab/v1/import/#{path}/")
+ .with(headers: { 'Accept' => described_class::JSON_TYPE })
+ .to_return(
+ status: 200,
+ body: { status: status }.to_json,
+ headers: { content_type: 'application/json' }
+ )
+ end
+end
diff --git a/spec/lib/container_registry/migration_spec.rb b/spec/lib/container_registry/migration_spec.rb
new file mode 100644
index 00000000000..ffbbfb249e3
--- /dev/null
+++ b/spec/lib/container_registry/migration_spec.rb
@@ -0,0 +1,168 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ContainerRegistry::Migration do
+ using RSpec::Parameterized::TableSyntax
+
+ describe '.enabled?' do
+ subject { described_class.enabled? }
+
+ it { is_expected.to eq(true) }
+
+ context 'feature flag disabled' do
+ before do
+ stub_feature_flags(container_registry_migration_phase2_enabled: false)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
+ describe '.limit_gitlab_org?' do
+ subject { described_class.limit_gitlab_org? }
+
+ it { is_expected.to eq(true) }
+
+ context 'feature flag disabled' do
+ before do
+ stub_feature_flags(container_registry_migration_limit_gitlab_org: false)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
+ describe '.enqueue_waiting_time' do
+ subject { described_class.enqueue_waiting_time }
+
+ where(:slow_enabled, :fast_enabled, :expected_result) do
+ false | false | 1.hour
+ true | false | 6.hours
+ false | true | 0
+ true | true | 0
+ end
+
+ with_them do
+ before do
+ stub_feature_flags(
+ container_registry_migration_phase2_enqueue_speed_slow: slow_enabled,
+ container_registry_migration_phase2_enqueue_speed_fast: fast_enabled
+ )
+ end
+
+ it { is_expected.to eq(expected_result) }
+ end
+ end
+
+ describe '.capacity' do
+ subject { described_class.capacity }
+
+ where(:ff_1_enabled, :ff_10_enabled, :ff_25_enabled, :expected_result) do
+ false | false | false | 0
+ true | false | false | 1
+ true | true | false | 10
+ true | true | true | 25
+ false | true | false | 10
+ false | true | true | 25
+ false | false | true | 25
+ true | false | true | 25
+ end
+
+ with_them do
+ before do
+ stub_feature_flags(
+ container_registry_migration_phase2_capacity_1: ff_1_enabled,
+ container_registry_migration_phase2_capacity_10: ff_10_enabled,
+ container_registry_migration_phase2_capacity_25: ff_25_enabled
+ )
+ end
+
+ it { is_expected.to eq(expected_result) }
+ end
+ end
+
+ describe '.max_tags_count' do
+ let(:value) { 1 }
+
+ before do
+ stub_application_setting(container_registry_import_max_tags_count: value)
+ end
+
+ it 'returns the matching application_setting' do
+ expect(described_class.max_tags_count).to eq(value)
+ end
+ end
+
+ describe '.max_retries' do
+ let(:value) { 1 }
+
+ before do
+ stub_application_setting(container_registry_import_max_retries: value)
+ end
+
+ it 'returns the matching application_setting' do
+ expect(described_class.max_retries).to eq(value)
+ end
+ end
+
+ describe '.start_max_retries' do
+ let(:value) { 1 }
+
+ before do
+ stub_application_setting(container_registry_import_start_max_retries: value)
+ end
+
+ it 'returns the matching application_setting' do
+ expect(described_class.start_max_retries).to eq(value)
+ end
+ end
+
+ describe '.max_step_duration' do
+ let(:value) { 5.minutes }
+
+ before do
+ stub_application_setting(container_registry_import_max_step_duration: value)
+ end
+
+ it 'returns the matching application_setting' do
+ expect(described_class.max_step_duration).to eq(value)
+ end
+ end
+
+ describe '.target_plan_name' do
+ let(:value) { 'free' }
+
+ before do
+ stub_application_setting(container_registry_import_target_plan: value)
+ end
+
+ it 'returns the matching application_setting' do
+ expect(described_class.target_plan_name).to eq(value)
+ end
+ end
+
+ describe '.created_before' do
+ let(:value) { 1.day.ago }
+
+ before do
+ stub_application_setting(container_registry_import_created_before: value)
+ end
+
+ it 'returns the matching application_setting' do
+ expect(described_class.created_before).to eq(value)
+ end
+ end
+
+ describe '.target_plan' do
+ let_it_be(:plan) { create(:plan) }
+
+ before do
+ stub_application_setting(container_registry_import_target_plan: plan.name)
+ end
+
+ it 'returns the matching application_setting' do
+ expect(described_class.target_plan).to eq(plan)
+ end
+ end
+end
diff --git a/spec/lib/container_registry/registry_spec.rb b/spec/lib/container_registry/registry_spec.rb
index d6e2b17f53b..c690d96b4f5 100644
--- a/spec/lib/container_registry/registry_spec.rb
+++ b/spec/lib/container_registry/registry_spec.rb
@@ -27,4 +27,14 @@ RSpec.describe ContainerRegistry::Registry do
it { is_expected.to eq(path) }
end
end
+
+ describe '#gitlab_api_client' do
+ subject { registry.gitlab_api_client }
+
+ it 'returns a GitLabApiClient with an import token' do
+ expect(Auth::ContainerRegistryAuthenticationService).to receive(:import_access_token)
+
+ expect(subject).to be_instance_of(ContainerRegistry::GitlabApiClient)
+ end
+ end
end
diff --git a/spec/lib/extracts_path_spec.rb b/spec/lib/extracts_path_spec.rb
index 9b2bb024fa6..5db2fbd923e 100644
--- a/spec/lib/extracts_path_spec.rb
+++ b/spec/lib/extracts_path_spec.rb
@@ -32,7 +32,7 @@ RSpec.describe ExtractsPath do
describe '#assign_ref_vars' do
let(:ref) { sample_commit[:id] }
let(:path) { sample_commit[:line_code_path] }
- let(:params) { { path: path, ref: ref } }
+ let(:params) { ActionController::Parameters.new(path: path, ref: ref) }
it_behaves_like 'assigns ref vars'
@@ -54,7 +54,8 @@ RSpec.describe ExtractsPath do
context 'ref only exists without .atom suffix' do
context 'with a path' do
- let(:params) { { ref: 'v1.0.0.atom', path: 'README.md' } }
+ let(:ref) { 'v1.0.0.atom' }
+ let(:path) { 'README.md' }
it 'renders a 404' do
expect(self).to receive(:render_404)
@@ -64,7 +65,8 @@ RSpec.describe ExtractsPath do
end
context 'without a path' do
- let(:params) { { ref: 'v1.0.0.atom' } }
+ let(:ref) { 'v1.0.0.atom' }
+ let(:path) { nil }
before do
assign_ref_vars
@@ -82,7 +84,8 @@ RSpec.describe ExtractsPath do
context 'ref exists with .atom suffix' do
context 'with a path' do
- let(:params) { { ref: 'master.atom', path: 'README.md' } }
+ let(:ref) { 'master.atom' }
+ let(:path) { 'README.md' }
before do
repository = @project.repository
@@ -102,7 +105,8 @@ RSpec.describe ExtractsPath do
end
context 'without a path' do
- let(:params) { { ref: 'master.atom' } }
+ let(:ref) { 'master.atom' }
+ let(:path) { nil }
before do
repository = @project.repository
@@ -125,7 +129,8 @@ RSpec.describe ExtractsPath do
end
context 'ref and path are nil' do
- let(:params) { { path: nil, ref: nil } }
+ let(:path) { nil }
+ let(:ref) { nil }
it 'does not set commit' do
expect(container.repository).not_to receive(:commit).with('')
@@ -209,8 +214,8 @@ RSpec.describe ExtractsPath do
expect(extract_ref_without_atom('release/app/v1.0.0.atom')).to eq('release/app/v1.0.0')
end
- it 'returns nil if there are no matching refs' do
- expect(extract_ref_without_atom('foo.atom')).to eq(nil)
+ it 'raises an error if there are no matching refs' do
+ expect { extract_ref_without_atom('foo.atom') }.to raise_error(ExtractsRef::InvalidPathError)
end
end
end
diff --git a/spec/lib/extracts_ref_spec.rb b/spec/lib/extracts_ref_spec.rb
index 3cdce150de9..3e9a7499fdd 100644
--- a/spec/lib/extracts_ref_spec.rb
+++ b/spec/lib/extracts_ref_spec.rb
@@ -10,7 +10,8 @@ RSpec.describe ExtractsRef do
let_it_be(:container) { create(:snippet, :repository, author: owner) }
let(:ref) { sample_commit[:id] }
- let(:params) { { path: sample_commit[:line_code_path], ref: ref } }
+ let(:path) { sample_commit[:line_code_path] }
+ let(:params) { ActionController::Parameters.new(path: path, ref: ref) }
before do
ref_names = ['master', 'foo/bar/baz', 'v1.0.0', 'v2.0.0', 'release/app', 'release/app/v1.0.0']
@@ -23,7 +24,8 @@ RSpec.describe ExtractsRef do
it_behaves_like 'assigns ref vars'
context 'ref and path are nil' do
- let(:params) { { path: nil, ref: nil } }
+ let(:ref) { nil }
+ let(:path) { nil }
it 'does not set commit' do
expect(container.repository).not_to receive(:commit).with('')
@@ -33,6 +35,15 @@ RSpec.describe ExtractsRef do
expect(@commit).to be_nil
end
end
+
+ context 'when ref and path have incorrect format' do
+ let(:ref) { { wrong: :format } }
+ let(:path) { { also: :wrong } }
+
+ it 'does not raise an exception' do
+ expect { assign_ref_vars }.not_to raise_error
+ end
+ end
end
it_behaves_like 'extracts refs'
diff --git a/spec/lib/feature_spec.rb b/spec/lib/feature_spec.rb
index 8c546390201..5080d21d564 100644
--- a/spec/lib/feature_spec.rb
+++ b/spec/lib/feature_spec.rb
@@ -728,13 +728,13 @@ RSpec.describe Feature, stub_feature_flags: false do
describe '#targets' do
let(:project) { create(:project) }
let(:group) { create(:group) }
- let(:user_name) { project.owner.username }
+ let(:user_name) { project.first_owner.username }
subject { described_class.new(user: user_name, project: project.full_path, group: group.full_path) }
it 'returns all found targets' do
expect(subject.targets).to be_an(Array)
- expect(subject.targets).to eq([project.owner, project, group])
+ expect(subject.targets).to eq([project.first_owner, project, group])
end
end
end
diff --git a/spec/lib/generators/gitlab/snowplow_event_definition_generator_spec.rb b/spec/lib/generators/gitlab/snowplow_event_definition_generator_spec.rb
index 4e172dd32f0..d9fa6b931ad 100644
--- a/spec/lib/generators/gitlab/snowplow_event_definition_generator_spec.rb
+++ b/spec/lib/generators/gitlab/snowplow_event_definition_generator_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe Gitlab::SnowplowEventDefinitionGenerator, :silence_stdout do
let(:ce_temp_dir) { Dir.mktmpdir }
let(:ee_temp_dir) { Dir.mktmpdir }
+ let(:timestamp) { Time.current.to_i }
let(:generator_options) { { 'category' => 'Groups::EmailCampaignsController', 'action' => 'click' } }
before do
@@ -12,6 +13,10 @@ RSpec.describe Gitlab::SnowplowEventDefinitionGenerator, :silence_stdout do
stub_const("#{described_class}::EE_DIR", ee_temp_dir)
end
+ around do |example|
+ freeze_time { example.run }
+ end
+
after do
FileUtils.rm_rf([ce_temp_dir, ee_temp_dir])
end
@@ -22,16 +27,41 @@ RSpec.describe Gitlab::SnowplowEventDefinitionGenerator, :silence_stdout do
end
let(:sample_event_dir) { 'lib/generators/gitlab/snowplow_event_definition_generator' }
+ let(:file_name) { Dir.children(ce_temp_dir).first }
it 'creates CE event definition file using the template' do
sample_event = ::Gitlab::Config::Loader::Yaml.new(fixture_file(File.join(sample_event_dir, 'sample_event.yml'))).load_raw!
described_class.new([], generator_options).invoke_all
- event_definition_path = File.join(ce_temp_dir, 'groups__email_campaigns_controller_click.yml')
+ event_definition_path = File.join(ce_temp_dir, file_name)
expect(::Gitlab::Config::Loader::Yaml.new(File.read(event_definition_path)).load_raw!).to eq(sample_event)
end
+ describe 'generated filename' do
+ it 'includes timestamp' do
+ described_class.new([], generator_options).invoke_all
+
+ expect(file_name).to include(timestamp.to_s)
+ end
+
+ it 'removes special characters' do
+ generator_options = { 'category' => '"`ui:[mavenpackages | t5%348()-=@ ]`"', 'action' => 'click' }
+
+ described_class.new([], generator_options).invoke_all
+
+ expect(file_name).to include('uimavenpackagest')
+ end
+
+ it 'cuts name if longer than 100 characters' do
+ generator_options = { 'category' => 'a' * 100, 'action' => 'click' }
+
+ described_class.new([], generator_options).invoke_all
+
+ expect(file_name.length).to eq(100)
+ end
+ end
+
context 'event definition already exists' do
before do
stub_const('Gitlab::VERSION', '12.11.0-pre')
@@ -44,7 +74,7 @@ RSpec.describe Gitlab::SnowplowEventDefinitionGenerator, :silence_stdout do
stub_const('Gitlab::VERSION', '13.11.0-pre')
described_class.new([], generator_options.merge('force' => true)).invoke_all
- event_definition_path = File.join(ce_temp_dir, 'groups__email_campaigns_controller_click.yml')
+ event_definition_path = File.join(ce_temp_dir, file_name)
event_data = ::Gitlab::Config::Loader::Yaml.new(File.read(event_definition_path)).load_raw!
expect(event_data).to eq(sample_event)
@@ -56,13 +86,17 @@ RSpec.describe Gitlab::SnowplowEventDefinitionGenerator, :silence_stdout do
end
end
- it 'creates EE event definition file using the template' do
- sample_event = ::Gitlab::Config::Loader::Yaml.new(fixture_file(File.join(sample_event_dir, 'sample_event_ee.yml'))).load_raw!
+ describe 'EE' do
+ let(:file_name) { Dir.children(ee_temp_dir).first }
- described_class.new([], generator_options.merge('ee' => true)).invoke_all
+ it 'creates EE event definition file using the template' do
+ sample_event = ::Gitlab::Config::Loader::Yaml.new(fixture_file(File.join(sample_event_dir, 'sample_event_ee.yml'))).load_raw!
- event_definition_path = File.join(ee_temp_dir, 'groups__email_campaigns_controller_click.yml')
- expect(::Gitlab::Config::Loader::Yaml.new(File.read(event_definition_path)).load_raw!).to eq(sample_event)
+ described_class.new([], generator_options.merge('ee' => true)).invoke_all
+
+ event_definition_path = File.join(ee_temp_dir, file_name)
+ expect(::Gitlab::Config::Loader::Yaml.new(File.read(event_definition_path)).load_raw!).to eq(sample_event)
+ end
end
end
end
diff --git a/spec/lib/gitlab/analytics/cycle_analytics/aggregated/records_fetcher_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/aggregated/records_fetcher_spec.rb
index 55ba6e56237..8eb75feaa8d 100644
--- a/spec/lib/gitlab/analytics/cycle_analytics/aggregated/records_fetcher_spec.rb
+++ b/spec/lib/gitlab/analytics/cycle_analytics/aggregated/records_fetcher_spec.rb
@@ -8,16 +8,17 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::Aggregated::RecordsFetcher do
let_it_be(:issue_2) { create(:issue, project: project) }
let_it_be(:issue_3) { create(:issue, project: project) }
- let_it_be(:stage_event_1) { create(:cycle_analytics_issue_stage_event, issue_id: issue_1.id, start_event_timestamp: 2.years.ago, end_event_timestamp: 1.year.ago) } # duration: 1 year
- let_it_be(:stage_event_2) { create(:cycle_analytics_issue_stage_event, issue_id: issue_2.id, start_event_timestamp: 5.years.ago, end_event_timestamp: 2.years.ago) } # duration: 3 years
- let_it_be(:stage_event_3) { create(:cycle_analytics_issue_stage_event, issue_id: issue_3.id, start_event_timestamp: 6.years.ago, end_event_timestamp: 3.months.ago) } # duration: 5+ years
-
let_it_be(:stage) { create(:cycle_analytics_project_stage, start_event_identifier: :issue_created, end_event_identifier: :issue_deployed_to_production, project: project) }
- let(:params) { {} }
+ let_it_be(:stage_event_1) { create(:cycle_analytics_issue_stage_event, stage_event_hash_id: stage.stage_event_hash_id, project_id: project.id, issue_id: issue_1.id, start_event_timestamp: 2.years.ago, end_event_timestamp: 1.year.ago) } # duration: 1 year
+ let_it_be(:stage_event_2) { create(:cycle_analytics_issue_stage_event, stage_event_hash_id: stage.stage_event_hash_id, project_id: project.id, issue_id: issue_2.id, start_event_timestamp: 5.years.ago, end_event_timestamp: 2.years.ago) } # duration: 3 years
+ let_it_be(:stage_event_3) { create(:cycle_analytics_issue_stage_event, stage_event_hash_id: stage.stage_event_hash_id, project_id: project.id, issue_id: issue_3.id, start_event_timestamp: 6.years.ago, end_event_timestamp: 3.months.ago) } # duration: 5+ years
+
+ let(:params) { { from: 10.years.ago, to: Date.today } }
subject(:records_fetcher) do
- described_class.new(stage: stage, query: Analytics::CycleAnalytics::IssueStageEvent.all, params: params)
+ query_builder = Gitlab::Analytics::CycleAnalytics::Aggregated::BaseQueryBuilder.new(stage: stage, params: params)
+ described_class.new(stage: stage, query: query_builder.build_sorted_query, params: params)
end
shared_examples 'match returned records' do
@@ -123,6 +124,8 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::Aggregated::RecordsFetcher do
it 'skips non-existing issue records' do
create(:cycle_analytics_issue_stage_event, {
issue_id: 0, # non-existing id
+ stage_event_hash_id: stage.stage_event_hash_id,
+ project_id: project.id,
start_event_timestamp: 5.months.ago,
end_event_timestamp: 3.months.ago
})
diff --git a/spec/lib/gitlab/analytics/cycle_analytics/records_fetcher_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/records_fetcher_spec.rb
index 4fe55ba0c0c..dc46dade87e 100644
--- a/spec/lib/gitlab/analytics/cycle_analytics/records_fetcher_spec.rb
+++ b/spec/lib/gitlab/analytics/cycle_analytics/records_fetcher_spec.rb
@@ -43,8 +43,8 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::RecordsFetcher do
end
before do
- issue1.metrics.update(first_added_to_board_at: 3.days.ago, first_mentioned_in_commit_at: 2.days.ago)
- issue2.metrics.update(first_added_to_board_at: 3.days.ago, first_mentioned_in_commit_at: 2.days.ago)
+ issue1.metrics.update!(first_added_to_board_at: 3.days.ago, first_mentioned_in_commit_at: 2.days.ago)
+ issue2.metrics.update!(first_added_to_board_at: 3.days.ago, first_mentioned_in_commit_at: 2.days.ago)
end
context 'when records are loaded by guest' do
@@ -73,8 +73,8 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::RecordsFetcher do
end
before do
- mr1.metrics.update(merged_at: 3.days.ago)
- mr2.metrics.update(merged_at: 3.days.ago)
+ mr1.metrics.update!(merged_at: 3.days.ago)
+ mr2.metrics.update!(merged_at: 3.days.ago)
end
include_context 'when records are loaded by maintainer'
@@ -95,9 +95,9 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::RecordsFetcher do
end
before(:all) do
- issue1.metrics.update(first_added_to_board_at: 3.days.ago, first_mentioned_in_commit_at: 2.days.ago)
- issue2.metrics.update(first_added_to_board_at: 3.days.ago, first_mentioned_in_commit_at: 2.days.ago)
- issue3.metrics.update(first_added_to_board_at: 3.days.ago, first_mentioned_in_commit_at: 2.days.ago)
+ issue1.metrics.update!(first_added_to_board_at: 3.days.ago, first_mentioned_in_commit_at: 2.days.ago)
+ issue2.metrics.update!(first_added_to_board_at: 3.days.ago, first_mentioned_in_commit_at: 2.days.ago)
+ issue3.metrics.update!(first_added_to_board_at: 3.days.ago, first_mentioned_in_commit_at: 2.days.ago)
end
before do
diff --git a/spec/lib/gitlab/application_context_spec.rb b/spec/lib/gitlab/application_context_spec.rb
index 5ecec978017..55f5ae7d7dc 100644
--- a/spec/lib/gitlab/application_context_spec.rb
+++ b/spec/lib/gitlab/application_context_spec.rb
@@ -125,6 +125,17 @@ RSpec.describe Gitlab::ApplicationContext do
.to include(project: project.full_path, root_namespace: project.full_path_components.first)
end
+ it 'contains known keys' do
+ context = described_class.new(project: project)
+
+ # Make sure all possible keys would be included
+ allow(context).to receive_message_chain(:set_values, :include?).and_return(true)
+
+ # If a newly added key is added to the context hash, we need to list it in
+ # the known keys constant. This spec ensures that we do.
+ expect(context.to_lazy_hash.keys).to contain_exactly(*described_class.known_keys)
+ end
+
describe 'setting the client' do
let_it_be(:remote_ip) { '127.0.0.1' }
let_it_be(:runner) { create(:ci_runner) }
diff --git a/spec/lib/gitlab/audit/ci_runner_token_author_spec.rb b/spec/lib/gitlab/audit/ci_runner_token_author_spec.rb
new file mode 100644
index 00000000000..f55e1b44936
--- /dev/null
+++ b/spec/lib/gitlab/audit/ci_runner_token_author_spec.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Audit::CiRunnerTokenAuthor do
+ describe '.initialize' do
+ subject { described_class.new(audit_event) }
+
+ let(:details) { }
+ let(:audit_event) { instance_double(AuditEvent, details: details, entity_type: 'Project', entity_path: 'd/e') }
+
+ context 'with runner_authentication_token' do
+ let(:details) do
+ { runner_authentication_token: 'abc1234567' }
+ end
+
+ it 'returns CiRunnerTokenAuthor with expected attributes' do
+ is_expected.to have_attributes(id: -1, name: 'Authentication token: abc1234567')
+ end
+ end
+
+ context 'with runner_registration_token' do
+ let(:details) do
+ { runner_registration_token: 'abc1234567' }
+ end
+
+ it 'returns CiRunnerTokenAuthor with expected attributes' do
+ is_expected.to have_attributes(id: -1, name: 'Registration token: abc1234567')
+ end
+ end
+
+ context 'with runner token missing' do
+ let(:details) do
+ {}
+ end
+
+ it 'raises ArgumentError' do
+ expect { subject }.to raise_error ArgumentError, 'Runner token missing'
+ end
+ end
+ end
+
+ describe '#full_path' do
+ subject { author.full_path }
+
+ let(:author) { described_class.new(audit_event) }
+
+ context 'with instance registration token' do
+ let(:audit_event) { instance_double(AuditEvent, details: { runner_registration_token: 'abc1234567' }, entity_type: 'User', entity_path: nil) }
+
+ it 'returns correct url' do
+ is_expected.to eq('/admin/runners')
+ end
+ end
+
+ context 'with group registration token' do
+ let(:audit_event) { instance_double(AuditEvent, details: { runner_registration_token: 'abc1234567' }, entity_type: 'Group', entity_path: 'a/b') }
+
+ it 'returns correct url' do
+ expect(::Gitlab::Routing.url_helpers).to receive(:group_settings_ci_cd_path)
+ .once
+ .with('a/b', { anchor: 'js-runners-settings' })
+ .and_return('/path/to/group/runners')
+
+ is_expected.to eq('/path/to/group/runners')
+ end
+ end
+
+ context 'with project registration token' do
+ let(:audit_event) { instance_double(AuditEvent, details: { runner_registration_token: 'abc1234567' }, entity_type: 'Project', entity_path: project.full_path) }
+ let(:project) { create(:project) }
+
+ it 'returns correct url' do
+ expect(::Gitlab::Routing.url_helpers).to receive(:project_settings_ci_cd_path)
+ .once
+ .with(project, { anchor: 'js-runners-settings' })
+ .and_return('/path/to/project/runners')
+
+ is_expected.to eq('/path/to/project/runners')
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/audit/null_author_spec.rb b/spec/lib/gitlab/audit/null_author_spec.rb
index eb80e5faa89..7203a0cd816 100644
--- a/spec/lib/gitlab/audit/null_author_spec.rb
+++ b/spec/lib/gitlab/audit/null_author_spec.rb
@@ -6,13 +6,47 @@ RSpec.describe Gitlab::Audit::NullAuthor do
subject { described_class }
describe '.for' do
+ let(:audit_event) { instance_double(AuditEvent) }
+
it 'returns an DeletedAuthor' do
- expect(subject.for(666, 'Old Hat')).to be_a(Gitlab::Audit::DeletedAuthor)
+ allow(audit_event).to receive(:[]).with(:author_name).and_return('Old Hat')
+ allow(audit_event).to receive(:details).and_return({})
+ allow(audit_event).to receive(:target_type)
+
+ expect(subject.for(666, audit_event)).to be_a(Gitlab::Audit::DeletedAuthor)
end
it 'returns an UnauthenticatedAuthor when id equals -1', :aggregate_failures do
- expect(subject.for(-1, 'Frank')).to be_a(Gitlab::Audit::UnauthenticatedAuthor)
- expect(subject.for(-1, 'Frank')).to have_attributes(id: -1, name: 'Frank')
+ allow(audit_event).to receive(:[]).with(:author_name).and_return('Frank')
+ allow(audit_event).to receive(:details).and_return({})
+ allow(audit_event).to receive(:target_type)
+
+ expect(subject.for(-1, audit_event)).to be_a(Gitlab::Audit::UnauthenticatedAuthor)
+ expect(subject.for(-1, audit_event)).to have_attributes(id: -1, name: 'Frank')
+ end
+
+ it 'returns a CiRunnerTokenAuthor when details contain runner registration token', :aggregate_failures do
+ allow(audit_event).to receive(:[]).with(:author_name).and_return('cde456')
+ allow(audit_event).to receive(:entity_type).and_return('User')
+ allow(audit_event).to receive(:entity_path).and_return('/a/b')
+ allow(audit_event).to receive(:target_type).and_return(::Ci::Runner.name)
+ allow(audit_event).to receive(:details)
+ .and_return({ runner_registration_token: 'cde456', author_name: 'cde456', entity_type: 'User', entity_path: '/a/b' })
+
+ expect(subject.for(-1, audit_event)).to be_a(Gitlab::Audit::CiRunnerTokenAuthor)
+ expect(subject.for(-1, audit_event)).to have_attributes(id: -1, name: 'Registration token: cde456')
+ end
+
+ it 'returns a CiRunnerTokenAuthor when details contain runner authentication token', :aggregate_failures do
+ allow(audit_event).to receive(:[]).with(:author_name).and_return('cde456')
+ allow(audit_event).to receive(:entity_type).and_return('User')
+ allow(audit_event).to receive(:entity_path).and_return('/a/b')
+ allow(audit_event).to receive(:target_type).and_return(::Ci::Runner.name)
+ allow(audit_event).to receive(:details)
+ .and_return({ runner_authentication_token: 'cde456', author_name: 'cde456', entity_type: 'User', entity_path: '/a/b' })
+
+ expect(subject.for(-1, audit_event)).to be_a(Gitlab::Audit::CiRunnerTokenAuthor)
+ expect(subject.for(-1, audit_event)).to have_attributes(id: -1, name: 'Authentication token: cde456')
end
end
diff --git a/spec/lib/gitlab/auth/ldap/user_spec.rb b/spec/lib/gitlab/auth/ldap/user_spec.rb
index e910ac09448..da0bb5fe675 100644
--- a/spec/lib/gitlab/auth/ldap/user_spec.rb
+++ b/spec/lib/gitlab/auth/ldap/user_spec.rb
@@ -53,12 +53,12 @@ RSpec.describe Gitlab::Auth::Ldap::User do
it "finds the user if already existing" do
create(:omniauth_user, extern_uid: 'uid=john smith,ou=people,dc=example,dc=com', provider: 'ldapmain')
- expect { ldap_user.save }.not_to change { User.count }
+ expect { ldap_user.save }.not_to change { User.count } # rubocop:disable Rails/SaveBang
end
it "connects to existing non-ldap user if the email matches" do
existing_user = create(:omniauth_user, email: 'john@example.com', provider: "twitter")
- expect { ldap_user.save }.not_to change { User.count }
+ expect { ldap_user.save }.not_to change { User.count } # rubocop:disable Rails/SaveBang
existing_user.reload
expect(existing_user.ldap_identity.extern_uid).to eql 'uid=john smith,ou=people,dc=example,dc=com'
@@ -67,7 +67,7 @@ RSpec.describe Gitlab::Auth::Ldap::User do
it 'connects to existing ldap user if the extern_uid changes' do
existing_user = create(:omniauth_user, email: 'john@example.com', extern_uid: 'old-uid', provider: 'ldapmain')
- expect { ldap_user.save }.not_to change { User.count }
+ expect { ldap_user.save }.not_to change { User.count } # rubocop:disable Rails/SaveBang
existing_user.reload
expect(existing_user.ldap_identity.extern_uid).to eql 'uid=john smith,ou=people,dc=example,dc=com'
@@ -77,7 +77,7 @@ RSpec.describe Gitlab::Auth::Ldap::User do
it 'connects to existing ldap user if the extern_uid changes and email address has upper case characters' do
existing_user = create(:omniauth_user, email: 'john@example.com', extern_uid: 'old-uid', provider: 'ldapmain')
- expect { ldap_user_upper_case.save }.not_to change { User.count }
+ expect { ldap_user_upper_case.save }.not_to change { User.count } # rubocop:disable Rails/SaveBang
existing_user.reload
expect(existing_user.ldap_identity.extern_uid).to eql 'uid=john smith,ou=people,dc=example,dc=com'
@@ -89,7 +89,7 @@ RSpec.describe Gitlab::Auth::Ldap::User do
existing_user = create(:omniauth_user, email: 'john@example.com', provider: 'twitter')
expect(existing_user.identities.count).to be(1)
- ldap_user.save
+ ldap_user.save # rubocop:disable Rails/SaveBang
expect(ldap_user.gl_user.identities.count).to be(2)
# Expect that find_by provider only returns a single instance of an identity and not an Enumerable
@@ -98,7 +98,7 @@ RSpec.describe Gitlab::Auth::Ldap::User do
end
it "creates a new user if not found" do
- expect { ldap_user.save }.to change { User.count }.by(1)
+ expect { ldap_user.save }.to change { User.count }.by(1) # rubocop:disable Rails/SaveBang
end
context 'when signup is disabled' do
@@ -107,7 +107,7 @@ RSpec.describe Gitlab::Auth::Ldap::User do
end
it 'creates the user' do
- ldap_user.save
+ ldap_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_persisted
end
@@ -119,7 +119,7 @@ RSpec.describe Gitlab::Auth::Ldap::User do
end
it 'creates and confirms the user anyway' do
- ldap_user.save
+ ldap_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_persisted
expect(gl_user).to be_confirmed
@@ -132,7 +132,7 @@ RSpec.describe Gitlab::Auth::Ldap::User do
end
it 'creates the user' do
- ldap_user.save
+ ldap_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_persisted
end
@@ -189,7 +189,7 @@ RSpec.describe Gitlab::Auth::Ldap::User do
end
it do
- ldap_user.save
+ ldap_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user).not_to be_blocked
end
@@ -201,7 +201,7 @@ RSpec.describe Gitlab::Auth::Ldap::User do
end
it do
- ldap_user.save
+ ldap_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user).to be_blocked
end
@@ -210,7 +210,7 @@ RSpec.describe Gitlab::Auth::Ldap::User do
context 'sign-in' do
before do
- ldap_user.save
+ ldap_user.save # rubocop:disable Rails/SaveBang
ldap_user.gl_user.activate
end
@@ -220,7 +220,7 @@ RSpec.describe Gitlab::Auth::Ldap::User do
end
it do
- ldap_user.save
+ ldap_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user).not_to be_blocked
end
@@ -232,7 +232,7 @@ RSpec.describe Gitlab::Auth::Ldap::User do
end
it do
- ldap_user.save
+ ldap_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user).not_to be_blocked
end
diff --git a/spec/lib/gitlab/auth/o_auth/user_spec.rb b/spec/lib/gitlab/auth/o_auth/user_spec.rb
index 7a8e6e77d52..8d36507ec7a 100644
--- a/spec/lib/gitlab/auth/o_auth/user_spec.rb
+++ b/spec/lib/gitlab/auth/o_auth/user_spec.rb
@@ -67,7 +67,7 @@ RSpec.describe Gitlab::Auth::OAuth::User do
create(:omniauth_user, extern_uid: 'my-uid', provider: provider)
stub_omniauth_config(allow_single_sign_on: [provider], external_providers: [provider])
- oauth_user.save
+ oauth_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user.external).to be_falsey
@@ -83,7 +83,7 @@ RSpec.describe Gitlab::Auth::OAuth::User do
it 'creates the user' do
stub_omniauth_config(allow_single_sign_on: [provider])
- oauth_user.save
+ oauth_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_persisted
end
@@ -97,7 +97,7 @@ RSpec.describe Gitlab::Auth::OAuth::User do
it 'creates and confirms the user anyway' do
stub_omniauth_config(allow_single_sign_on: [provider])
- oauth_user.save
+ oauth_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_persisted
expect(gl_user).to be_confirmed
@@ -112,7 +112,7 @@ RSpec.describe Gitlab::Auth::OAuth::User do
it 'creates the user' do
stub_omniauth_config(allow_single_sign_on: [provider])
- oauth_user.save
+ oauth_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_persisted
end
@@ -121,7 +121,7 @@ RSpec.describe Gitlab::Auth::OAuth::User do
it 'marks user as having password_automatically_set' do
stub_omniauth_config(allow_single_sign_on: [provider], external_providers: [provider])
- oauth_user.save
+ oauth_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_persisted
expect(gl_user).to be_password_automatically_set
@@ -131,7 +131,7 @@ RSpec.describe Gitlab::Auth::OAuth::User do
context 'provider is marked as external' do
it 'marks user as external' do
stub_omniauth_config(allow_single_sign_on: [provider], external_providers: [provider])
- oauth_user.save
+ oauth_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user.external).to be_truthy
end
@@ -141,7 +141,7 @@ RSpec.describe Gitlab::Auth::OAuth::User do
it 'does not mark external user as internal' do
create(:omniauth_user, extern_uid: 'my-uid', provider: provider, external: true)
stub_omniauth_config(allow_single_sign_on: [provider], external_providers: ['facebook'])
- oauth_user.save
+ oauth_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user.external).to be_truthy
end
@@ -151,9 +151,9 @@ RSpec.describe Gitlab::Auth::OAuth::User do
context 'when adding a new OAuth identity' do
it 'does not promote an external user to internal' do
user = create(:user, email: 'john@mail.com', external: true)
- user.identities.create(provider: provider, extern_uid: uid)
+ user.identities.create!(provider: provider, extern_uid: uid)
- oauth_user.save
+ oauth_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user.external).to be_truthy
end
@@ -166,7 +166,7 @@ RSpec.describe Gitlab::Auth::OAuth::User do
end
it "creates a user from Omniauth" do
- oauth_user.save
+ oauth_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
identity = gl_user.identities.first
@@ -181,7 +181,7 @@ RSpec.describe Gitlab::Auth::OAuth::User do
end
it "creates a user from Omniauth" do
- oauth_user.save
+ oauth_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
identity = gl_user.identities.first
@@ -196,7 +196,7 @@ RSpec.describe Gitlab::Auth::OAuth::User do
end
it 'throws an error' do
- expect { oauth_user.save }.to raise_error StandardError
+ expect { oauth_user.save }.to raise_error StandardError # rubocop:disable Rails/SaveBang
end
end
@@ -206,7 +206,7 @@ RSpec.describe Gitlab::Auth::OAuth::User do
end
it 'throws an error' do
- expect { oauth_user.save }.to raise_error StandardError
+ expect { oauth_user.save }.to raise_error StandardError # rubocop:disable Rails/SaveBang
end
end
end
@@ -228,7 +228,7 @@ RSpec.describe Gitlab::Auth::OAuth::User do
let!(:existing_user) { create(:user, email: 'john@mail.com', username: 'john') }
it "adds the OmniAuth identity to the GitLab user account" do
- oauth_user.save
+ oauth_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).not_to be_valid
end
@@ -248,7 +248,7 @@ RSpec.describe Gitlab::Auth::OAuth::User do
let!(:existing_user) { create(:user, email: 'john@mail.com', username: 'john') }
it "adds the OmniAuth identity to the GitLab user account" do
- oauth_user.save
+ oauth_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user.username).to eql 'john'
@@ -277,7 +277,7 @@ RSpec.describe Gitlab::Auth::OAuth::User do
let!(:existing_user) { create(:user, email: 'john@mail.com', username: 'john') }
it "adds the OmniAuth identity to the GitLab user account" do
- oauth_user.save
+ oauth_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user.username).to eql 'john'
@@ -337,7 +337,7 @@ RSpec.describe Gitlab::Auth::OAuth::User do
before do
allow(Gitlab::Auth::Ldap::Person).to receive(:find_by_uid).and_return(ldap_user)
- oauth_user.save
+ oauth_user.save # rubocop:disable Rails/SaveBang
end
it "creates a user with dual LDAP and omniauth identities" do
@@ -376,7 +376,7 @@ RSpec.describe Gitlab::Auth::OAuth::User do
allow(Gitlab::Auth::Ldap::Person).to receive(:find_by_email).with(uid, any_args).and_return(nil)
allow(Gitlab::Auth::Ldap::Person).to receive(:find_by_email).with(info_hash[:email], any_args).and_return(ldap_user)
- oauth_user.save
+ oauth_user.save # rubocop:disable Rails/SaveBang
end
it 'creates the LDAP identity' do
@@ -392,7 +392,7 @@ RSpec.describe Gitlab::Auth::OAuth::User do
it "adds the omniauth identity to the LDAP account" do
allow(Gitlab::Auth::Ldap::Person).to receive(:find_by_uid).and_return(ldap_user)
- oauth_user.save
+ oauth_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user.username).to eql 'john'
@@ -414,7 +414,7 @@ RSpec.describe Gitlab::Auth::OAuth::User do
allow(Gitlab::Auth::Ldap::Person).to receive(:find_by_uid).and_return(nil)
allow(Gitlab::Auth::Ldap::Person).to receive(:find_by_email).and_return(ldap_user)
- oauth_user.save
+ oauth_user.save # rubocop:disable Rails/SaveBang
identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } }
expect(identities_as_hash).to match_array(result_identities(dn, uid))
@@ -426,7 +426,7 @@ RSpec.describe Gitlab::Auth::OAuth::User do
allow(Gitlab::Auth::Ldap::Person).to receive(:find_by_email).and_return(nil)
allow(Gitlab::Auth::Ldap::Person).to receive(:find_by_dn).and_return(ldap_user)
- oauth_user.save
+ oauth_user.save # rubocop:disable Rails/SaveBang
identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } }
expect(identities_as_hash).to match_array(result_identities(dn, uid))
@@ -447,7 +447,7 @@ RSpec.describe Gitlab::Auth::OAuth::User do
end
it 'does not save the identity' do
- oauth_user.save
+ oauth_user.save # rubocop:disable Rails/SaveBang
identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } }
expect(identities_as_hash).to match_array([{ provider: 'twitter', extern_uid: uid }])
@@ -467,7 +467,7 @@ RSpec.describe Gitlab::Auth::OAuth::User do
it 'creates a user favoring the LDAP username and strips email domain' do
allow(Gitlab::Auth::Ldap::Person).to receive(:find_by_uid).and_return(ldap_user)
- oauth_user.save
+ oauth_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user.username).to eql 'johndoe'
@@ -510,7 +510,7 @@ RSpec.describe Gitlab::Auth::OAuth::User do
before do
allow(Gitlab::Auth::Ldap::Person).to receive(:find_by_uid).and_return(ldap_user)
- oauth_user.save
+ oauth_user.save # rubocop:disable Rails/SaveBang
end
it "creates a user with dual LDAP and omniauth identities" do
@@ -549,7 +549,7 @@ RSpec.describe Gitlab::Auth::OAuth::User do
it "adds the omniauth identity to the LDAP account" do
allow(Gitlab::Auth::Ldap::Person).to receive(:find_by_uid).and_return(ldap_user)
- oauth_user.save
+ oauth_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user.username).to eql 'john'
@@ -584,7 +584,7 @@ RSpec.describe Gitlab::Auth::OAuth::User do
end
it do
- oauth_user.save
+ oauth_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user).not_to be_blocked
end
@@ -596,7 +596,7 @@ RSpec.describe Gitlab::Auth::OAuth::User do
end
it do
- oauth_user.save
+ oauth_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user).to be_blocked
end
@@ -622,7 +622,7 @@ RSpec.describe Gitlab::Auth::OAuth::User do
end
it do
- oauth_user.save
+ oauth_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user).not_to be_blocked
end
@@ -636,7 +636,7 @@ RSpec.describe Gitlab::Auth::OAuth::User do
end
it do
- oauth_user.save
+ oauth_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user).to be_blocked
end
@@ -654,7 +654,7 @@ RSpec.describe Gitlab::Auth::OAuth::User do
end
it do
- oauth_user.save
+ oauth_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user).not_to be_blocked
end
@@ -668,7 +668,7 @@ RSpec.describe Gitlab::Auth::OAuth::User do
end
it do
- oauth_user.save
+ oauth_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user).not_to be_blocked
end
@@ -678,7 +678,7 @@ RSpec.describe Gitlab::Auth::OAuth::User do
context 'sign-in' do
before do
- oauth_user.save
+ oauth_user.save # rubocop:disable Rails/SaveBang
oauth_user.gl_user.activate
end
@@ -688,7 +688,7 @@ RSpec.describe Gitlab::Auth::OAuth::User do
end
it do
- oauth_user.save
+ oauth_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user).not_to be_blocked
end
@@ -700,7 +700,7 @@ RSpec.describe Gitlab::Auth::OAuth::User do
end
it do
- oauth_user.save
+ oauth_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user).not_to be_blocked
end
@@ -714,7 +714,7 @@ RSpec.describe Gitlab::Auth::OAuth::User do
end
it do
- oauth_user.save
+ oauth_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user).not_to be_blocked
end
@@ -728,7 +728,7 @@ RSpec.describe Gitlab::Auth::OAuth::User do
end
it do
- oauth_user.save
+ oauth_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user).not_to be_blocked
end
@@ -791,7 +791,7 @@ RSpec.describe Gitlab::Auth::OAuth::User do
context 'when collision with existing user' do
it 'generates the username with a counter' do
- oauth_user.save
+ oauth_user.save # rubocop:disable Rails/SaveBang
oauth_user2 = described_class.new(OmniAuth::AuthHash.new(uid: 'my-uid2', provider: provider, info: { nickname: 'johngitlab-ETC@othermail.com', email: 'john@othermail.com' }))
expect(oauth_user2.gl_user.username).to eq('johngitlab-ETC1')
diff --git a/spec/lib/gitlab/auth/request_authenticator_spec.rb b/spec/lib/gitlab/auth/request_authenticator_spec.rb
index 6f3d6187076..5e9d07a8bf7 100644
--- a/spec/lib/gitlab/auth/request_authenticator_spec.rb
+++ b/spec/lib/gitlab/auth/request_authenticator_spec.rb
@@ -21,8 +21,10 @@ RSpec.describe Gitlab::Auth::RequestAuthenticator do
let_it_be(:session_user) { build(:user) }
it 'returns sessionless user first' do
- allow_any_instance_of(described_class).to receive(:find_sessionless_user).and_return(sessionless_user)
- allow_any_instance_of(described_class).to receive(:find_user_from_warden).and_return(session_user)
+ allow_next_instance_of(described_class) do |instance|
+ allow(instance).to receive(:find_sessionless_user).and_return(sessionless_user)
+ allow(instance).to receive(:find_user_from_warden).and_return(session_user)
+ end
expect(subject.user([:api])).to eq sessionless_user
end
diff --git a/spec/lib/gitlab/auth/saml/user_spec.rb b/spec/lib/gitlab/auth/saml/user_spec.rb
index fd48492f18d..796512bc52b 100644
--- a/spec/lib/gitlab/auth/saml/user_spec.rb
+++ b/spec/lib/gitlab/auth/saml/user_spec.rb
@@ -36,7 +36,7 @@ RSpec.describe Gitlab::Auth::Saml::User do
context 'and should bind with SAML' do
it 'adds the SAML identity to the existing user' do
- saml_user.save
+ saml_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user).to eq existing_user
identity = gl_user.identities.first
@@ -49,7 +49,7 @@ RSpec.describe Gitlab::Auth::Saml::User do
context 'are defined' do
it 'marks the user as external' do
stub_saml_group_config(%w(Freelancers))
- saml_user.save
+ saml_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user.external).to be_truthy
end
@@ -61,7 +61,7 @@ RSpec.describe Gitlab::Auth::Saml::User do
context 'are defined but the user does not belong there' do
it 'does not mark the user as external' do
- saml_user.save
+ saml_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user.external).to be_falsey
end
@@ -70,7 +70,7 @@ RSpec.describe Gitlab::Auth::Saml::User do
context 'user was external, now should not be' do
it 'makes user internal' do
existing_user.update_attribute('external', true)
- saml_user.save
+ saml_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user.external).to be_falsey
end
@@ -86,7 +86,7 @@ RSpec.describe Gitlab::Auth::Saml::User do
end
it 'creates a user from SAML' do
- saml_user.save
+ saml_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
identity = gl_user.identities.first
@@ -101,7 +101,7 @@ RSpec.describe Gitlab::Auth::Saml::User do
end
it 'does not throw an error' do
- expect { saml_user.save }.not_to raise_error
+ expect { saml_user.save }.not_to raise_error # rubocop:disable Rails/SaveBang
end
end
@@ -111,7 +111,7 @@ RSpec.describe Gitlab::Auth::Saml::User do
end
it 'throws an error' do
- expect { saml_user.save }.to raise_error StandardError
+ expect { saml_user.save }.to raise_error StandardError # rubocop:disable Rails/SaveBang
end
end
end
@@ -120,7 +120,7 @@ RSpec.describe Gitlab::Auth::Saml::User do
context 'are defined' do
it 'marks the user as external' do
stub_saml_group_config(%w(Freelancers))
- saml_user.save
+ saml_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user.external).to be_truthy
end
@@ -129,7 +129,7 @@ RSpec.describe Gitlab::Auth::Saml::User do
context 'are defined but the user does not belong there' do
it 'does not mark the user as external' do
stub_saml_group_config(%w(Interns))
- saml_user.save
+ saml_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user.external).to be_falsey
end
@@ -170,7 +170,7 @@ RSpec.describe Gitlab::Auth::Saml::User do
context 'and no account for the LDAP user' do
it 'creates a user with dual LDAP and SAML identities' do
- saml_user.save
+ saml_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user.username).to eql uid
@@ -230,7 +230,7 @@ RSpec.describe Gitlab::Auth::Saml::User do
{ provider: id.provider, extern_uid: id.extern_uid }
end
- saml_user.save
+ saml_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user.username).to eql 'john'
@@ -259,7 +259,7 @@ RSpec.describe Gitlab::Auth::Saml::User do
end
it 'adds the omniauth identity to the LDAP account' do
- saml_user.save
+ saml_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user.username).to eql 'john'
@@ -271,9 +271,9 @@ RSpec.describe Gitlab::Auth::Saml::User do
end
it 'saves successfully on subsequent tries, when both identities are present' do
- saml_user.save
+ saml_user.save # rubocop:disable Rails/SaveBang
local_saml_user = described_class.new(auth_hash)
- local_saml_user.save
+ local_saml_user.save # rubocop:disable Rails/SaveBang
expect(local_saml_user.gl_user).to be_valid
expect(local_saml_user.gl_user).to be_persisted
@@ -289,7 +289,7 @@ RSpec.describe Gitlab::Auth::Saml::User do
local_hash = OmniAuth::AuthHash.new(uid: dn, provider: provider, info: info_hash)
local_saml_user = described_class.new(local_hash)
- local_saml_user.save
+ local_saml_user.save # rubocop:disable Rails/SaveBang
local_gl_user = local_saml_user.gl_user
expect(local_gl_user).to be_valid
@@ -309,7 +309,7 @@ RSpec.describe Gitlab::Auth::Saml::User do
end
it 'creates the user' do
- saml_user.save
+ saml_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_persisted
end
@@ -321,7 +321,7 @@ RSpec.describe Gitlab::Auth::Saml::User do
end
it 'creates and confirms the user anyway' do
- saml_user.save
+ saml_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_persisted
expect(gl_user).to be_confirmed
@@ -334,7 +334,7 @@ RSpec.describe Gitlab::Auth::Saml::User do
end
it 'creates the user' do
- saml_user.save
+ saml_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_persisted
end
@@ -353,7 +353,7 @@ RSpec.describe Gitlab::Auth::Saml::User do
end
it 'does not block the user' do
- saml_user.save
+ saml_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user).not_to be_blocked
end
@@ -365,7 +365,7 @@ RSpec.describe Gitlab::Auth::Saml::User do
end
it 'blocks user' do
- saml_user.save
+ saml_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user).to be_blocked
end
@@ -374,7 +374,7 @@ RSpec.describe Gitlab::Auth::Saml::User do
context 'sign-in' do
before do
- saml_user.save
+ saml_user.save # rubocop:disable Rails/SaveBang
saml_user.gl_user.activate
end
@@ -384,7 +384,7 @@ RSpec.describe Gitlab::Auth::Saml::User do
end
it do
- saml_user.save
+ saml_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user).not_to be_blocked
end
@@ -396,7 +396,7 @@ RSpec.describe Gitlab::Auth::Saml::User do
end
it do
- saml_user.save
+ saml_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user).not_to be_blocked
end
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index 611c70d73a1..706344831b8 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -10,29 +10,29 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
describe 'constants' do
it 'API_SCOPES contains all scopes for API access' do
- expect(subject::API_SCOPES).to eq %i[api read_user read_api]
+ expect(subject::API_SCOPES).to match_array %i[api read_user read_api]
end
it 'ADMIN_SCOPES contains all scopes for ADMIN access' do
- expect(subject::ADMIN_SCOPES).to eq %i[sudo]
+ expect(subject::ADMIN_SCOPES).to match_array %i[sudo]
end
it 'REPOSITORY_SCOPES contains all scopes for REPOSITORY access' do
- expect(subject::REPOSITORY_SCOPES).to eq %i[read_repository write_repository]
+ expect(subject::REPOSITORY_SCOPES).to match_array %i[read_repository write_repository]
end
it 'OPENID_SCOPES contains all scopes for OpenID Connect' do
- expect(subject::OPENID_SCOPES).to eq [:openid]
+ expect(subject::OPENID_SCOPES).to match_array [:openid]
end
it 'DEFAULT_SCOPES contains all default scopes' do
- expect(subject::DEFAULT_SCOPES).to eq [:api]
+ expect(subject::DEFAULT_SCOPES).to match_array [:api]
end
it 'optional_scopes contains all non-default scopes' do
stub_container_registry_config(enabled: true)
- expect(subject.optional_scopes).to eq %i[read_user read_api read_repository write_repository read_registry write_registry sudo openid profile email]
+ expect(subject.optional_scopes).to match_array %i[read_user read_api read_repository write_repository read_registry write_registry sudo openid profile email]
end
end
@@ -40,21 +40,21 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
it 'contains all non-default scopes' do
stub_container_registry_config(enabled: true)
- expect(subject.all_available_scopes).to eq %i[api read_user read_api read_repository write_repository read_registry write_registry sudo]
+ expect(subject.all_available_scopes).to match_array %i[api read_user read_api read_repository write_repository read_registry write_registry sudo]
end
it 'contains for non-admin user all non-default scopes without ADMIN access' do
stub_container_registry_config(enabled: true)
user = create(:user, admin: false)
- expect(subject.available_scopes_for(user)).to eq %i[api read_user read_api read_repository write_repository read_registry write_registry]
+ expect(subject.available_scopes_for(user)).to match_array %i[api read_user read_api read_repository write_repository read_registry write_registry]
end
it 'contains for admin user all non-default scopes with ADMIN access' do
stub_container_registry_config(enabled: true)
user = create(:user, admin: true)
- expect(subject.available_scopes_for(user)).to eq %i[api read_user read_api read_repository write_repository read_registry write_registry sudo]
+ expect(subject.available_scopes_for(user)).to match_array %i[api read_user read_api read_repository write_repository read_registry write_registry sudo]
end
context 'registry_scopes' do
@@ -156,21 +156,36 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
let(:username) { 'gitlab-ci-token' }
context 'for running build' do
- let!(:build) { create(:ci_build, :running) }
- let(:project) { build.project }
+ let!(:group) { create(:group) }
+ let!(:project) { create(:project, group: group) }
+ let!(:build) { create(:ci_build, :running, project: project) }
it 'recognises user-less build' do
expect(subject).to have_attributes(actor: nil, project: build.project, type: :ci, authentication_abilities: described_class.build_authentication_abilities)
end
it 'recognises user token' do
- build.update(user: create(:user))
+ build.update!(user: create(:user))
+
+ expect(subject).to have_attributes(actor: build.user, project: build.project, type: :build, authentication_abilities: described_class.build_authentication_abilities)
+ end
+
+ it 'recognises project level bot access token' do
+ build.update!(user: create(:user, :project_bot))
+ project.add_maintainer(build.user)
+
+ expect(subject).to have_attributes(actor: build.user, project: build.project, type: :build, authentication_abilities: described_class.build_authentication_abilities)
+ end
+
+ it 'recognises group level bot access token' do
+ build.update!(user: create(:user, :project_bot))
+ group.add_maintainer(build.user)
expect(subject).to have_attributes(actor: build.user, project: build.project, type: :build, authentication_abilities: described_class.build_authentication_abilities)
end
it 'fails with blocked user token' do
- build.update(user: create(:user, :blocked))
+ build.update!(user: create(:user, :blocked))
expect(subject).to have_attributes(auth_failure)
end
@@ -198,7 +213,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
it 'recognizes other ci services' do
project.create_drone_ci_integration(active: true)
- project.drone_ci_integration.update(token: 'token')
+ project.drone_ci_integration.update!(token: 'token', drone_url: generate(:url))
expect(gl_auth.find_for_git_client('drone-ci-token', 'token', project: project, ip: 'ip')).to have_attributes(actor: nil, project: project, type: :ci, authentication_abilities: described_class.build_authentication_abilities)
end
@@ -311,7 +326,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
context 'orphaned token' do
before do
- user.destroy
+ user.destroy!
end
it_behaves_like 'an oauth failure'
@@ -888,7 +903,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
it 'resets failed_attempts when true and password is correct' do
user.failed_attempts = 2
- user.save
+ user.save!
expect do
gl_auth.find_with_user_password(username, password, increment_failed_attempts: true)
@@ -917,7 +932,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
it 'does not reset failed_attempts when true and password is correct' do
user.failed_attempts = 2
- user.save
+ user.save!
expect do
gl_auth.find_with_user_password(username, password, increment_failed_attempts: true)
diff --git a/spec/lib/gitlab/authorized_keys_spec.rb b/spec/lib/gitlab/authorized_keys_spec.rb
index 1053ae2e325..073cee96ede 100644
--- a/spec/lib/gitlab/authorized_keys_spec.rb
+++ b/spec/lib/gitlab/authorized_keys_spec.rb
@@ -38,7 +38,7 @@ RSpec.describe Gitlab::AuthorizedKeys do
end
describe '#create' do
- subject { authorized_keys.create }
+ subject { authorized_keys.create } # rubocop:disable Rails/SaveBang
context 'authorized_keys file exists' do
before do
diff --git a/spec/lib/gitlab/background_migration/backfill_ci_queuing_tables_spec.rb b/spec/lib/gitlab/background_migration/backfill_ci_queuing_tables_spec.rb
new file mode 100644
index 00000000000..1aac5970a77
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/backfill_ci_queuing_tables_spec.rb
@@ -0,0 +1,244 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::BackfillCiQueuingTables, :migration, schema: 20220208115439 do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:ci_cd_settings) { table(:project_ci_cd_settings) }
+ let(:builds) { table(:ci_builds) }
+ let(:queuing_entries) { table(:ci_pending_builds) }
+ let(:tags) { table(:tags) }
+ let(:taggings) { table(:taggings) }
+
+ subject { described_class.new }
+
+ describe '#perform' do
+ let!(:namespace) do
+ namespaces.create!(
+ id: 10,
+ name: 'namespace10',
+ path: 'namespace10',
+ traversal_ids: [10])
+ end
+
+ let!(:other_namespace) do
+ namespaces.create!(
+ id: 11,
+ name: 'namespace11',
+ path: 'namespace11',
+ traversal_ids: [11])
+ end
+
+ let!(:project) do
+ projects.create!(id: 5, namespace_id: 10, name: 'test1', path: 'test1')
+ end
+
+ let!(:ci_cd_setting) do
+ ci_cd_settings.create!(id: 5, project_id: 5, group_runners_enabled: true)
+ end
+
+ let!(:other_project) do
+ projects.create!(id: 7, namespace_id: 11, name: 'test2', path: 'test2')
+ end
+
+ let!(:other_ci_cd_setting) do
+ ci_cd_settings.create!(id: 7, project_id: 7, group_runners_enabled: false)
+ end
+
+ let!(:another_project) do
+ projects.create!(id: 9, namespace_id: 10, name: 'test3', path: 'test3', shared_runners_enabled: false)
+ end
+
+ let!(:ruby_tag) do
+ tags.create!(id: 22, name: 'ruby')
+ end
+
+ let!(:postgres_tag) do
+ tags.create!(id: 23, name: 'postgres')
+ end
+
+ it 'creates ci_pending_builds for all pending builds in range' do
+ builds.create!(id: 50, status: :pending, name: 'test1', project_id: 5, type: 'Ci::Build')
+ builds.create!(id: 51, status: :created, name: 'test2', project_id: 5, type: 'Ci::Build')
+ builds.create!(id: 52, status: :pending, name: 'test3', project_id: 5, protected: true, type: 'Ci::Build')
+
+ taggings.create!(taggable_id: 52, taggable_type: 'CommitStatus', tag_id: 22)
+ taggings.create!(taggable_id: 52, taggable_type: 'CommitStatus', tag_id: 23)
+
+ builds.create!(id: 60, status: :pending, name: 'test1', project_id: 7, type: 'Ci::Build')
+ builds.create!(id: 61, status: :running, name: 'test2', project_id: 7, protected: true, type: 'Ci::Build')
+ builds.create!(id: 62, status: :pending, name: 'test3', project_id: 7, type: 'Ci::Build')
+
+ taggings.create!(taggable_id: 60, taggable_type: 'CommitStatus', tag_id: 23)
+ taggings.create!(taggable_id: 62, taggable_type: 'CommitStatus', tag_id: 22)
+
+ builds.create!(id: 70, status: :pending, name: 'test1', project_id: 9, protected: true, type: 'Ci::Build')
+ builds.create!(id: 71, status: :failed, name: 'test2', project_id: 9, type: 'Ci::Build')
+ builds.create!(id: 72, status: :pending, name: 'test3', project_id: 9, type: 'Ci::Build')
+
+ taggings.create!(taggable_id: 71, taggable_type: 'CommitStatus', tag_id: 22)
+
+ subject.perform(1, 100)
+
+ expect(queuing_entries.all).to contain_exactly(
+ an_object_having_attributes(
+ build_id: 50,
+ project_id: 5,
+ namespace_id: 10,
+ protected: false,
+ instance_runners_enabled: true,
+ minutes_exceeded: false,
+ tag_ids: [],
+ namespace_traversal_ids: [10]),
+ an_object_having_attributes(
+ build_id: 52,
+ project_id: 5,
+ namespace_id: 10,
+ protected: true,
+ instance_runners_enabled: true,
+ minutes_exceeded: false,
+ tag_ids: match_array([22, 23]),
+ namespace_traversal_ids: [10]),
+ an_object_having_attributes(
+ build_id: 60,
+ project_id: 7,
+ namespace_id: 11,
+ protected: false,
+ instance_runners_enabled: true,
+ minutes_exceeded: false,
+ tag_ids: [23],
+ namespace_traversal_ids: []),
+ an_object_having_attributes(
+ build_id: 62,
+ project_id: 7,
+ namespace_id: 11,
+ protected: false,
+ instance_runners_enabled: true,
+ minutes_exceeded: false,
+ tag_ids: [22],
+ namespace_traversal_ids: []),
+ an_object_having_attributes(
+ build_id: 70,
+ project_id: 9,
+ namespace_id: 10,
+ protected: true,
+ instance_runners_enabled: false,
+ minutes_exceeded: false,
+ tag_ids: [],
+ namespace_traversal_ids: []),
+ an_object_having_attributes(
+ build_id: 72,
+ project_id: 9,
+ namespace_id: 10,
+ protected: false,
+ instance_runners_enabled: false,
+ minutes_exceeded: false,
+ tag_ids: [],
+ namespace_traversal_ids: [])
+ )
+ end
+
+ it 'skips builds that already have ci_pending_builds' do
+ builds.create!(id: 50, status: :pending, name: 'test1', project_id: 5, type: 'Ci::Build')
+ builds.create!(id: 51, status: :created, name: 'test2', project_id: 5, type: 'Ci::Build')
+ builds.create!(id: 52, status: :pending, name: 'test3', project_id: 5, protected: true, type: 'Ci::Build')
+
+ taggings.create!(taggable_id: 50, taggable_type: 'CommitStatus', tag_id: 22)
+ taggings.create!(taggable_id: 52, taggable_type: 'CommitStatus', tag_id: 23)
+
+ queuing_entries.create!(build_id: 50, project_id: 5, namespace_id: 10)
+
+ subject.perform(1, 100)
+
+ expect(queuing_entries.all).to contain_exactly(
+ an_object_having_attributes(
+ build_id: 50,
+ project_id: 5,
+ namespace_id: 10,
+ protected: false,
+ instance_runners_enabled: false,
+ minutes_exceeded: false,
+ tag_ids: [],
+ namespace_traversal_ids: []),
+ an_object_having_attributes(
+ build_id: 52,
+ project_id: 5,
+ namespace_id: 10,
+ protected: true,
+ instance_runners_enabled: true,
+ minutes_exceeded: false,
+ tag_ids: [23],
+ namespace_traversal_ids: [10])
+ )
+ end
+
+ it 'upserts values in case of conflicts' do
+ builds.create!(id: 50, status: :pending, name: 'test1', project_id: 5, type: 'Ci::Build')
+ queuing_entries.create!(build_id: 50, project_id: 5, namespace_id: 10)
+
+ build = described_class::Ci::Build.find(50)
+ described_class::Ci::PendingBuild.upsert_from_build!(build)
+
+ expect(queuing_entries.all).to contain_exactly(
+ an_object_having_attributes(
+ build_id: 50,
+ project_id: 5,
+ namespace_id: 10,
+ protected: false,
+ instance_runners_enabled: true,
+ minutes_exceeded: false,
+ tag_ids: [],
+ namespace_traversal_ids: [10])
+ )
+ end
+ end
+
+ context 'Ci::Build' do
+ describe '.each_batch' do
+ let(:model) { described_class::Ci::Build }
+
+ before do
+ builds.create!(id: 1, status: :pending, name: 'test1', project_id: 5, type: 'Ci::Build')
+ builds.create!(id: 2, status: :pending, name: 'test2', project_id: 5, type: 'Ci::Build')
+ builds.create!(id: 3, status: :pending, name: 'test3', project_id: 5, type: 'Ci::Build')
+ builds.create!(id: 4, status: :pending, name: 'test4', project_id: 5, type: 'Ci::Build')
+ builds.create!(id: 5, status: :pending, name: 'test5', project_id: 5, type: 'Ci::Build')
+ end
+
+ it 'yields an ActiveRecord::Relation when a block is given' do
+ model.each_batch do |relation|
+ expect(relation).to be_a_kind_of(ActiveRecord::Relation)
+ end
+ end
+
+ it 'yields a batch index as the second argument' do
+ model.each_batch do |_, index|
+ expect(index).to eq(1)
+ end
+ end
+
+ it 'accepts a custom batch size' do
+ amount = 0
+
+ model.each_batch(of: 1) { amount += 1 }
+
+ expect(amount).to eq(5)
+ end
+
+ it 'does not include ORDER BYs in the yielded relations' do
+ model.each_batch do |relation|
+ expect(relation.to_sql).not_to include('ORDER BY')
+ end
+ end
+
+ it 'orders ascending' do
+ ids = []
+
+ model.each_batch(of: 1) { |rel| ids.concat(rel.ids) }
+
+ expect(ids).to eq(ids.sort)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/backfill_legacy_project_repositories_spec.rb b/spec/lib/gitlab/background_migration/backfill_legacy_project_repositories_spec.rb
deleted file mode 100644
index c4013d002b2..00000000000
--- a/spec/lib/gitlab/background_migration/backfill_legacy_project_repositories_spec.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::BackgroundMigration::BackfillLegacyProjectRepositories do
- it_behaves_like 'backfill migration for project repositories', :legacy
-end
diff --git a/spec/lib/gitlab/background_migration/backfill_namespace_id_for_namespace_route_spec.rb b/spec/lib/gitlab/background_migration/backfill_namespace_id_for_namespace_route_spec.rb
new file mode 100644
index 00000000000..b821efcadb0
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/backfill_namespace_id_for_namespace_route_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::BackfillNamespaceIdForNamespaceRoute, :migration, schema: 20220120123800 do
+ let(:migration) { described_class.new }
+ let(:namespaces_table) { table(:namespaces) }
+ let(:projects_table) { table(:projects) }
+ let(:routes_table) { table(:routes) }
+
+ let(:table_name) { 'routes' }
+ let(:batch_column) { :id }
+ let(:sub_batch_size) { 200 }
+ let(:pause_ms) { 0 }
+
+ let(:namespace1) { namespaces_table.create!(name: 'namespace1', path: 'namespace1', type: 'User') }
+ let(:namespace2) { namespaces_table.create!(name: 'namespace2', path: 'namespace2', type: 'Group') }
+ let(:namespace3) { namespaces_table.create!(name: 'namespace3', path: 'namespace3', type: 'Group') }
+ let(:namespace4) { namespaces_table.create!(name: 'namespace4', path: 'namespace4', type: 'Group') }
+ let(:project1) { projects_table.create!(name: 'project1', namespace_id: namespace1.id) }
+
+ subject(:perform_migration) { migration.perform(1, 10, table_name, batch_column, sub_batch_size, pause_ms) }
+
+ before do
+ routes_table.create!(id: 1, name: 'test1', path: 'test1', source_id: namespace1.id,
+ source_type: namespace1.class.sti_name)
+ routes_table.create!(id: 2, name: 'test2', path: 'test2', source_id: namespace2.id,
+ source_type: namespace2.class.sti_name)
+ routes_table.create!(id: 5, name: 'test3', path: 'test3', source_id: project1.id,
+ source_type: project1.class.sti_name) # should be ignored - project route
+ routes_table.create!(id: 6, name: 'test4', path: 'test4', source_id: non_existing_record_id,
+ source_type: namespace3.class.sti_name) # should be ignored - invalid source_id
+ routes_table.create!(id: 10, name: 'test5', path: 'test5', source_id: namespace3.id,
+ source_type: namespace3.class.sti_name)
+ routes_table.create!(id: 11, name: 'test6', path: 'test6', source_id: namespace4.id,
+ source_type: namespace4.class.sti_name) # should be ignored - outside the scope
+ end
+
+ it 'backfills `type` for the selected records', :aggregate_failures do
+ perform_migration
+
+ expect(routes_table.where.not(namespace_id: nil).pluck(:id)).to match_array([1, 2, 10])
+ end
+
+ it 'tracks timings of queries' do
+ expect(migration.batch_metrics.timings).to be_empty
+
+ expect { perform_migration }.to change { migration.batch_metrics.timings }
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/backfill_project_updated_at_after_repository_storage_move_spec.rb b/spec/lib/gitlab/background_migration/backfill_project_updated_at_after_repository_storage_move_spec.rb
deleted file mode 100644
index ed44b819a97..00000000000
--- a/spec/lib/gitlab/background_migration/backfill_project_updated_at_after_repository_storage_move_spec.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::BackgroundMigration::BackfillProjectUpdatedAtAfterRepositoryStorageMove, :migration, schema: 20210301200959 do
- let(:projects) { table(:projects) }
- let(:project_repository_storage_moves) { table(:project_repository_storage_moves) }
- let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
-
- subject { described_class.new }
-
- describe '#perform' do
- it 'updates project updated_at column if they were moved to a different repository storage' do
- freeze_time do
- project_1 = projects.create!(id: 1, namespace_id: namespace.id, updated_at: 1.day.ago)
- project_2 = projects.create!(id: 2, namespace_id: namespace.id, updated_at: Time.current)
- original_project_3_updated_at = 2.minutes.from_now
- project_3 = projects.create!(id: 3, namespace_id: namespace.id, updated_at: original_project_3_updated_at)
- original_project_4_updated_at = 10.days.ago
- project_4 = projects.create!(id: 4, namespace_id: namespace.id, updated_at: original_project_4_updated_at)
-
- repository_storage_move_1 = project_repository_storage_moves.create!(project_id: project_1.id, updated_at: 2.hours.ago, source_storage_name: 'default', destination_storage_name: 'default')
- repository_storage_move_2 = project_repository_storage_moves.create!(project_id: project_2.id, updated_at: Time.current, source_storage_name: 'default', destination_storage_name: 'default')
- project_repository_storage_moves.create!(project_id: project_3.id, updated_at: Time.current, source_storage_name: 'default', destination_storage_name: 'default')
-
- subject.perform([1, 2, 3, 4, non_existing_record_id])
-
- expect(project_1.reload.updated_at).to eq(repository_storage_move_1.updated_at + 1.second)
- expect(project_2.reload.updated_at).to eq(repository_storage_move_2.updated_at + 1.second)
- expect(project_3.reload.updated_at).to eq(original_project_3_updated_at)
- expect(project_4.reload.updated_at).to eq(original_project_4_updated_at)
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/background_migration/batching_strategies/backfill_project_namespace_per_group_batching_strategy_spec.rb b/spec/lib/gitlab/background_migration/batching_strategies/backfill_project_namespace_per_group_batching_strategy_spec.rb
new file mode 100644
index 00000000000..7b8a466b37c
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/batching_strategies/backfill_project_namespace_per_group_batching_strategy_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::BatchingStrategies::BackfillProjectNamespacePerGroupBatchingStrategy, '#next_batch' do
+ let!(:namespaces) { table(:namespaces) }
+ let!(:projects) { table(:projects) }
+ let!(:background_migrations) { table(:batched_background_migrations) }
+
+ let!(:namespace1) { namespaces.create!(name: 'batchtest1', type: 'Group', path: 'batch-test1') }
+ let!(:namespace2) { namespaces.create!(name: 'batchtest2', type: 'Group', parent_id: namespace1.id, path: 'batch-test2') }
+ let!(:namespace3) { namespaces.create!(name: 'batchtest3', type: 'Group', parent_id: namespace2.id, path: 'batch-test3') }
+
+ let!(:project1) { projects.create!(name: 'project1', path: 'project1', namespace_id: namespace1.id, visibility_level: 20) }
+ let!(:project2) { projects.create!(name: 'project2', path: 'project2', namespace_id: namespace2.id, visibility_level: 20) }
+ let!(:project3) { projects.create!(name: 'project3', path: 'project3', namespace_id: namespace3.id, visibility_level: 20) }
+ let!(:project4) { projects.create!(name: 'project4', path: 'project4', namespace_id: namespace3.id, visibility_level: 20) }
+ let!(:batching_strategy) { described_class.new }
+
+ let(:job_arguments) { [namespace1.id, 'up'] }
+
+ context 'when starting on the first batch' do
+ it 'returns the bounds of the next batch' do
+ batch_bounds = batching_strategy.next_batch(:projects, :id, batch_min_value: project1.id, batch_size: 3, job_arguments: job_arguments)
+
+ expect(batch_bounds).to match_array([project1.id, project3.id])
+ end
+ end
+
+ context 'when additional batches remain' do
+ it 'returns the bounds of the next batch' do
+ batch_bounds = batching_strategy.next_batch(:projects, :id, batch_min_value: project2.id, batch_size: 3, job_arguments: job_arguments)
+
+ expect(batch_bounds).to match_array([project2.id, project4.id])
+ end
+ end
+
+ context 'when on the final batch' do
+ it 'returns the bounds of the next batch' do
+ batch_bounds = batching_strategy.next_batch(:projects, :id, batch_min_value: project4.id, batch_size: 3, job_arguments: job_arguments)
+
+ expect(batch_bounds).to match_array([project4.id, project4.id])
+ end
+ end
+
+ context 'when no additional batches remain' do
+ it 'returns nil' do
+ batch_bounds = batching_strategy.next_batch(:projects, :id, batch_min_value: project4.id + 1, batch_size: 1, job_arguments: job_arguments)
+
+ expect(batch_bounds).to be_nil
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy_spec.rb b/spec/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy_spec.rb
index 8febe850e04..39030039125 100644
--- a/spec/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy_spec.rb
+++ b/spec/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe Gitlab::BackgroundMigration::BatchingStrategies::PrimaryKeyBatchi
context 'when starting on the first batch' do
it 'returns the bounds of the next batch' do
- batch_bounds = batching_strategy.next_batch(:namespaces, :id, batch_min_value: namespace1.id, batch_size: 3)
+ batch_bounds = batching_strategy.next_batch(:namespaces, :id, batch_min_value: namespace1.id, batch_size: 3, job_arguments: nil)
expect(batch_bounds).to eq([namespace1.id, namespace3.id])
end
@@ -21,7 +21,7 @@ RSpec.describe Gitlab::BackgroundMigration::BatchingStrategies::PrimaryKeyBatchi
context 'when additional batches remain' do
it 'returns the bounds of the next batch' do
- batch_bounds = batching_strategy.next_batch(:namespaces, :id, batch_min_value: namespace2.id, batch_size: 3)
+ batch_bounds = batching_strategy.next_batch(:namespaces, :id, batch_min_value: namespace2.id, batch_size: 3, job_arguments: nil)
expect(batch_bounds).to eq([namespace2.id, namespace4.id])
end
@@ -29,7 +29,7 @@ RSpec.describe Gitlab::BackgroundMigration::BatchingStrategies::PrimaryKeyBatchi
context 'when on the final batch' do
it 'returns the bounds of the next batch' do
- batch_bounds = batching_strategy.next_batch(:namespaces, :id, batch_min_value: namespace4.id, batch_size: 3)
+ batch_bounds = batching_strategy.next_batch(:namespaces, :id, batch_min_value: namespace4.id, batch_size: 3, job_arguments: nil)
expect(batch_bounds).to eq([namespace4.id, namespace4.id])
end
@@ -37,7 +37,7 @@ RSpec.describe Gitlab::BackgroundMigration::BatchingStrategies::PrimaryKeyBatchi
context 'when no additional batches remain' do
it 'returns nil' do
- batch_bounds = batching_strategy.next_batch(:namespaces, :id, batch_min_value: namespace4.id + 1, batch_size: 1)
+ batch_bounds = batching_strategy.next_batch(:namespaces, :id, batch_min_value: namespace4.id + 1, batch_size: 1, job_arguments: nil)
expect(batch_bounds).to be_nil
end
diff --git a/spec/lib/gitlab/background_migration/copy_column_using_background_migration_job_spec.rb b/spec/lib/gitlab/background_migration/copy_column_using_background_migration_job_spec.rb
index d4fc24d0559..90d9bbb42c3 100644
--- a/spec/lib/gitlab/background_migration/copy_column_using_background_migration_job_spec.rb
+++ b/spec/lib/gitlab/background_migration/copy_column_using_background_migration_job_spec.rb
@@ -7,13 +7,14 @@ RSpec.describe Gitlab::BackgroundMigration::CopyColumnUsingBackgroundMigrationJo
let(:test_table) { table(table_name) }
let(:sub_batch_size) { 1000 }
let(:pause_ms) { 0 }
+ let(:connection) { ApplicationRecord.connection }
let(:helpers) do
ActiveRecord::Migration.new.extend(Gitlab::Database::MigrationHelpers)
end
before do
- ActiveRecord::Base.connection.execute(<<~SQL)
+ connection.execute(<<~SQL)
CREATE TABLE #{table_name}
(
id integer NOT NULL,
@@ -34,12 +35,14 @@ RSpec.describe Gitlab::BackgroundMigration::CopyColumnUsingBackgroundMigrationJo
after do
# Make sure that the temp table we created is dropped (it is not removed by the database_cleaner)
- ActiveRecord::Base.connection.execute(<<~SQL)
+ connection.execute(<<~SQL)
DROP TABLE IF EXISTS #{table_name};
SQL
end
- subject(:copy_columns) { described_class.new }
+ subject(:copy_columns) { described_class.new(connection: connection) }
+
+ it { expect(described_class).to be < Gitlab::BackgroundMigration::BaseJob }
describe '#perform' do
let(:migration_class) { described_class.name }
diff --git a/spec/lib/gitlab/background_migration/populate_finding_uuid_for_vulnerability_feedback_spec.rb b/spec/lib/gitlab/background_migration/populate_finding_uuid_for_vulnerability_feedback_spec.rb
deleted file mode 100644
index 68fe8f39f59..00000000000
--- a/spec/lib/gitlab/background_migration/populate_finding_uuid_for_vulnerability_feedback_spec.rb
+++ /dev/null
@@ -1,134 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::BackgroundMigration::PopulateFindingUuidForVulnerabilityFeedback, schema: 20210301200959 do
- let(:namespaces) { table(:namespaces) }
- let(:projects) { table(:projects) }
- let(:users) { table(:users) }
- let(:scanners) { table(:vulnerability_scanners) }
- let(:identifiers) { table(:vulnerability_identifiers) }
- let(:findings) { table(:vulnerability_occurrences) }
- let(:vulnerability_feedback) { table(:vulnerability_feedback) }
-
- let(:namespace) { namespaces.create!(name: 'gitlab', path: 'gitlab-org') }
- let(:project) { projects.create!(namespace_id: namespace.id, name: 'foo') }
- let(:user) { users.create!(username: 'john.doe', projects_limit: 5) }
- let(:scanner) { scanners.create!(project_id: project.id, external_id: 'foo', name: 'bar') }
- let(:identifier) { identifiers.create!(project_id: project.id, fingerprint: 'foo', external_type: 'bar', external_id: 'zoo', name: 'baz') }
- let(:sast_report) { 0 }
- let(:dependency_scanning_report) { 1 }
- let(:dast_report) { 3 }
- let(:secret_detection_report) { 4 }
- let(:project_fingerprint) { Digest::SHA1.hexdigest(SecureRandom.uuid) }
- let(:location_fingerprint_1) { Digest::SHA1.hexdigest(SecureRandom.uuid) }
- let(:location_fingerprint_2) { Digest::SHA1.hexdigest(SecureRandom.uuid) }
- let(:location_fingerprint_3) { Digest::SHA1.hexdigest(SecureRandom.uuid) }
- let(:finding_1) { finding_creator.call(sast_report, location_fingerprint_1) }
- let(:finding_2) { finding_creator.call(dast_report, location_fingerprint_2) }
- let(:finding_3) { finding_creator.call(secret_detection_report, location_fingerprint_3) }
- let(:expected_uuid_1) do
- Security::VulnerabilityUUID.generate(
- report_type: 'sast',
- primary_identifier_fingerprint: identifier.fingerprint,
- location_fingerprint: location_fingerprint_1,
- project_id: project.id
- )
- end
-
- let(:expected_uuid_2) do
- Security::VulnerabilityUUID.generate(
- report_type: 'dast',
- primary_identifier_fingerprint: identifier.fingerprint,
- location_fingerprint: location_fingerprint_2,
- project_id: project.id
- )
- end
-
- let(:expected_uuid_3) do
- Security::VulnerabilityUUID.generate(
- report_type: 'secret_detection',
- primary_identifier_fingerprint: identifier.fingerprint,
- location_fingerprint: location_fingerprint_3,
- project_id: project.id
- )
- end
-
- let(:finding_creator) do
- -> (report_type, location_fingerprint) do
- findings.create!(
- project_id: project.id,
- primary_identifier_id: identifier.id,
- scanner_id: scanner.id,
- report_type: report_type,
- uuid: SecureRandom.uuid,
- name: 'Foo',
- location_fingerprint: Gitlab::Database::ShaAttribute.serialize(location_fingerprint),
- project_fingerprint: Gitlab::Database::ShaAttribute.serialize(project_fingerprint),
- metadata_version: '1',
- severity: 0,
- confidence: 5,
- raw_metadata: '{}'
- )
- end
- end
-
- let(:feedback_creator) do
- -> (category, project_fingerprint) do
- vulnerability_feedback.create!(
- project_id: project.id,
- author_id: user.id,
- feedback_type: 0,
- category: category,
- project_fingerprint: project_fingerprint
- )
- end
- end
-
- let!(:feedback_1) { feedback_creator.call(finding_1.report_type, project_fingerprint) }
- let!(:feedback_2) { feedback_creator.call(finding_2.report_type, project_fingerprint) }
- let!(:feedback_3) { feedback_creator.call(finding_3.report_type, project_fingerprint) }
- let!(:feedback_4) { feedback_creator.call(finding_1.report_type, 'foo') }
- let!(:feedback_5) { feedback_creator.call(dependency_scanning_report, project_fingerprint) }
-
- subject(:populate_finding_uuids) { described_class.new.perform(feedback_1.id, feedback_5.id) }
-
- before do
- allow(Gitlab::BackgroundMigration::Logger).to receive(:info)
- end
-
- describe '#perform' do
- it 'updates the `finding_uuid` attributes of the feedback records' do
- expect { populate_finding_uuids }.to change { feedback_1.reload.finding_uuid }.from(nil).to(expected_uuid_1)
- .and change { feedback_2.reload.finding_uuid }.from(nil).to(expected_uuid_2)
- .and change { feedback_3.reload.finding_uuid }.from(nil).to(expected_uuid_3)
- .and not_change { feedback_4.reload.finding_uuid }
- .and not_change { feedback_5.reload.finding_uuid }
-
- expect(Gitlab::BackgroundMigration::Logger).to have_received(:info).once
- end
-
- it 'preloads the finding and identifier records to prevent N+1 queries' do
- # Load feedback records(1), load findings(2), load identifiers(3) and finally update feedback records one by one(6)
- expect { populate_finding_uuids }.not_to exceed_query_limit(6)
- end
-
- context 'when setting the `finding_uuid` attribute of a feedback record fails' do
- let(:expected_error) { RuntimeError.new }
-
- before do
- allow(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception)
-
- allow_next_found_instance_of(described_class::VulnerabilityFeedback) do |feedback|
- allow(feedback).to receive(:update_column).and_raise(expected_error)
- end
- end
-
- it 'captures the errors and does not crash entirely' do
- expect { populate_finding_uuids }.not_to raise_error
-
- expect(Gitlab::ErrorTracking).to have_received(:track_and_raise_for_dev_exception).with(expected_error).exactly(3).times
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/background_migration/populate_issue_email_participants_spec.rb b/spec/lib/gitlab/background_migration/populate_issue_email_participants_spec.rb
deleted file mode 100644
index b00eb185b34..00000000000
--- a/spec/lib/gitlab/background_migration/populate_issue_email_participants_spec.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::BackgroundMigration::PopulateIssueEmailParticipants, schema: 20210301200959 do
- let!(:namespace) { table(:namespaces).create!(name: 'namespace', path: 'namespace') }
- let!(:project) { table(:projects).create!(id: 1, namespace_id: namespace.id) }
- let!(:issue1) { table(:issues).create!(id: 1, project_id: project.id, service_desk_reply_to: "a@gitlab.com") }
- let!(:issue2) { table(:issues).create!(id: 2, project_id: project.id, service_desk_reply_to: "b@gitlab.com") }
- let(:issue_email_participants) { table(:issue_email_participants) }
-
- describe '#perform' do
- it 'migrates email addresses from service desk issues', :aggregate_failures do
- expect { subject.perform(1, 2) }.to change { issue_email_participants.count }.by(2)
-
- expect(issue_email_participants.find_by(issue_id: 1).email).to eq("a@gitlab.com")
- expect(issue_email_participants.find_by(issue_id: 2).email).to eq("b@gitlab.com")
- end
- end
-end
diff --git a/spec/lib/gitlab/background_migration/populate_topics_non_private_projects_count_spec.rb b/spec/lib/gitlab/background_migration/populate_topics_non_private_projects_count_spec.rb
new file mode 100644
index 00000000000..e72e3392210
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/populate_topics_non_private_projects_count_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::PopulateTopicsNonPrivateProjectsCount, schema: 20220125122640 do
+ it 'correctly populates the non private projects counters' do
+ namespaces = table(:namespaces)
+ projects = table(:projects)
+ topics = table(:topics)
+ project_topics = table(:project_topics)
+
+ group = namespaces.create!(name: 'group', path: 'group')
+ project_public = projects.create!(namespace_id: group.id, visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ project_internal = projects.create!(namespace_id: group.id, visibility_level: Gitlab::VisibilityLevel::INTERNAL)
+ project_private = projects.create!(namespace_id: group.id, visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+ topic_1 = topics.create!(name: 'Topic1')
+ topic_2 = topics.create!(name: 'Topic2')
+ topic_3 = topics.create!(name: 'Topic3')
+ topic_4 = topics.create!(name: 'Topic4')
+ topic_5 = topics.create!(name: 'Topic5')
+ topic_6 = topics.create!(name: 'Topic6')
+ topic_7 = topics.create!(name: 'Topic7')
+ topic_8 = topics.create!(name: 'Topic8')
+
+ project_topics.create!(topic_id: topic_1.id, project_id: project_public.id)
+ project_topics.create!(topic_id: topic_2.id, project_id: project_internal.id)
+ project_topics.create!(topic_id: topic_3.id, project_id: project_private.id)
+ project_topics.create!(topic_id: topic_4.id, project_id: project_public.id)
+ project_topics.create!(topic_id: topic_4.id, project_id: project_internal.id)
+ project_topics.create!(topic_id: topic_5.id, project_id: project_public.id)
+ project_topics.create!(topic_id: topic_5.id, project_id: project_private.id)
+ project_topics.create!(topic_id: topic_6.id, project_id: project_internal.id)
+ project_topics.create!(topic_id: topic_6.id, project_id: project_private.id)
+ project_topics.create!(topic_id: topic_7.id, project_id: project_public.id)
+ project_topics.create!(topic_id: topic_7.id, project_id: project_internal.id)
+ project_topics.create!(topic_id: topic_7.id, project_id: project_private.id)
+ project_topics.create!(topic_id: topic_8.id, project_id: project_public.id)
+
+ subject.perform(topic_1.id, topic_7.id)
+
+ expect(topic_1.reload.non_private_projects_count).to eq(1)
+ expect(topic_2.reload.non_private_projects_count).to eq(1)
+ expect(topic_3.reload.non_private_projects_count).to eq(0)
+ expect(topic_4.reload.non_private_projects_count).to eq(2)
+ expect(topic_5.reload.non_private_projects_count).to eq(1)
+ expect(topic_6.reload.non_private_projects_count).to eq(1)
+ expect(topic_7.reload.non_private_projects_count).to eq(2)
+ expect(topic_8.reload.non_private_projects_count).to eq(0)
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/populate_vulnerability_reads_spec.rb b/spec/lib/gitlab/background_migration/populate_vulnerability_reads_spec.rb
new file mode 100644
index 00000000000..a265fa95b23
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/populate_vulnerability_reads_spec.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::PopulateVulnerabilityReads do
+ let(:vulnerabilities) { table(:vulnerabilities) }
+ let(:vulnerability_reads) { table(:vulnerability_reads) }
+ let(:vulnerabilities_findings) { table(:vulnerability_occurrences) }
+ let(:vulnerability_issue_links) { table(:vulnerability_issue_links) }
+ let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
+ let(:user) { table(:users).create!(email: 'author@example.com', username: 'author', projects_limit: 10) }
+ let(:project) { table(:projects).create!(namespace_id: namespace.id) }
+ let(:scanner) { table(:vulnerability_scanners).create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') }
+ let(:sub_batch_size) { 1000 }
+
+ before do
+ vulnerabilities_findings.connection.execute 'ALTER TABLE vulnerability_occurrences DISABLE TRIGGER "trigger_insert_or_update_vulnerability_reads_from_occurrences"'
+ vulnerabilities.connection.execute 'ALTER TABLE vulnerabilities DISABLE TRIGGER "trigger_update_vulnerability_reads_on_vulnerability_update"'
+ vulnerability_issue_links.connection.execute 'ALTER TABLE vulnerability_issue_links DISABLE TRIGGER "trigger_update_has_issues_on_vulnerability_issue_links_update"'
+
+ 10.times.each do |x|
+ vulnerability = create_vulnerability!(
+ project_id: project.id,
+ report_type: 7,
+ author_id: user.id
+ )
+ identifier = table(:vulnerability_identifiers).create!(
+ project_id: project.id,
+ external_type: 'uuid-v5',
+ external_id: 'uuid-v5',
+ fingerprint: Digest::SHA1.hexdigest("#{vulnerability.id}"),
+ name: 'Identifier for UUIDv5')
+
+ create_finding!(
+ vulnerability_id: vulnerability.id,
+ project_id: project.id,
+ scanner_id: scanner.id,
+ primary_identifier_id: identifier.id
+ )
+ end
+ end
+
+ it 'creates vulnerability_reads for the given records' do
+ described_class.new.perform(vulnerabilities.first.id, vulnerabilities.last.id, sub_batch_size)
+
+ expect(vulnerability_reads.count).to eq(10)
+ end
+
+ it 'does not create new records when records already exists' do
+ described_class.new.perform(vulnerabilities.first.id, vulnerabilities.last.id, sub_batch_size)
+ described_class.new.perform(vulnerabilities.first.id, vulnerabilities.last.id, sub_batch_size)
+
+ expect(vulnerability_reads.count).to eq(10)
+ end
+
+ private
+
+ def create_vulnerability!(project_id:, author_id:, title: 'test', severity: 7, confidence: 7, report_type: 0)
+ vulnerabilities.create!(
+ project_id: project_id,
+ author_id: author_id,
+ title: title,
+ severity: severity,
+ confidence: confidence,
+ report_type: report_type
+ )
+ end
+
+ # rubocop:disable Metrics/ParameterLists
+ def create_finding!(
+ vulnerability_id: nil, project_id:, scanner_id:, primary_identifier_id:,
+ name: "test", severity: 7, confidence: 7, report_type: 0,
+ project_fingerprint: '123qweasdzxc', location: { "image" => "alpine:3.4" }, location_fingerprint: 'test',
+ metadata_version: 'test', raw_metadata: 'test', uuid: SecureRandom.uuid)
+ vulnerabilities_findings.create!(
+ vulnerability_id: vulnerability_id,
+ project_id: project_id,
+ name: name,
+ severity: severity,
+ confidence: confidence,
+ report_type: report_type,
+ project_fingerprint: project_fingerprint,
+ scanner_id: scanner_id,
+ primary_identifier_id: primary_identifier_id,
+ location: location,
+ location_fingerprint: location_fingerprint,
+ metadata_version: metadata_version,
+ raw_metadata: raw_metadata,
+ uuid: uuid
+ )
+ end
+ # rubocop:enable Metrics/ParameterLists
+end
diff --git a/spec/lib/gitlab/background_migration/project_namespaces/backfill_project_namespaces_spec.rb b/spec/lib/gitlab/background_migration/project_namespaces/backfill_project_namespaces_spec.rb
index 24259b06469..2c5de448fbc 100644
--- a/spec/lib/gitlab/background_migration/project_namespaces/backfill_project_namespaces_spec.rb
+++ b/spec/lib/gitlab/background_migration/project_namespaces/backfill_project_namespaces_spec.rb
@@ -30,7 +30,7 @@ RSpec.describe Gitlab::BackgroundMigration::ProjectNamespaces::BackfillProjectNa
start_id = ::Project.minimum(:id)
end_id = ::Project.maximum(:id)
projects_count = ::Project.count
- batches_count = (projects_count / described_class::BATCH_SIZE.to_f).ceil
+ batches_count = (projects_count / described_class::SUB_BATCH_SIZE.to_f).ceil
project_namespaces_count = ::Namespace.where(type: 'Project').count
migration = described_class.new
@@ -39,7 +39,7 @@ RSpec.describe Gitlab::BackgroundMigration::ProjectNamespaces::BackfillProjectNa
expect(migration).to receive(:batch_update_projects).exactly(batches_count).and_call_original
expect(migration).to receive(:batch_update_project_namespaces_traversal_ids).exactly(batches_count).and_call_original
- expect { migration.perform(start_id, end_id, nil, 'up') }.to change(Namespace.where(type: 'Project'), :count)
+ expect { migration.perform(start_id, end_id, nil, nil, nil, nil, nil, 'up') }.to change(Namespace.where(type: 'Project'), :count)
expect(projects_count).to eq(::Namespace.where(type: 'Project').count)
check_projects_in_sync_with(Namespace.where(type: 'Project'))
@@ -53,7 +53,7 @@ RSpec.describe Gitlab::BackgroundMigration::ProjectNamespaces::BackfillProjectNa
start_id = backfilled_namespace_projects.minimum(:id)
end_id = backfilled_namespace_projects.maximum(:id)
group_projects_count = backfilled_namespace_projects.count
- batches_count = (group_projects_count / described_class::BATCH_SIZE.to_f).ceil
+ batches_count = (group_projects_count / described_class::SUB_BATCH_SIZE.to_f).ceil
project_namespaces_in_hierarchy = project_namespaces_in_hierarchy(base_ancestor(backfilled_namespace))
migration = described_class.new
@@ -66,7 +66,7 @@ RSpec.describe Gitlab::BackgroundMigration::ProjectNamespaces::BackfillProjectNa
expect(group_projects_count).to eq(14)
expect(project_namespaces_in_hierarchy.count).to eq(0)
- migration.perform(start_id, end_id, backfilled_namespace.id, 'up')
+ migration.perform(start_id, end_id, nil, nil, nil, nil, backfilled_namespace.id, 'up')
expect(project_namespaces_in_hierarchy.count).to eq(14)
check_projects_in_sync_with(project_namespaces_in_hierarchy)
@@ -79,7 +79,7 @@ RSpec.describe Gitlab::BackgroundMigration::ProjectNamespaces::BackfillProjectNa
start_id = hierarchy1_projects.minimum(:id)
end_id = hierarchy1_projects.maximum(:id)
- described_class.new.perform(start_id, end_id, parent_group1.id, 'up')
+ described_class.new.perform(start_id, end_id, nil, nil, nil, nil, parent_group1.id, 'up')
end
it 'does not duplicate project namespaces' do
@@ -87,7 +87,7 @@ RSpec.describe Gitlab::BackgroundMigration::ProjectNamespaces::BackfillProjectNa
projects_count = ::Project.count
start_id = ::Project.minimum(:id)
end_id = ::Project.maximum(:id)
- batches_count = (projects_count / described_class::BATCH_SIZE.to_f).ceil
+ batches_count = (projects_count / described_class::SUB_BATCH_SIZE.to_f).ceil
project_namespaces = ::Namespace.where(type: 'Project')
migration = described_class.new
@@ -100,7 +100,7 @@ RSpec.describe Gitlab::BackgroundMigration::ProjectNamespaces::BackfillProjectNa
expect(migration).to receive(:batch_update_projects).exactly(batches_count).and_call_original
expect(migration).to receive(:batch_update_project_namespaces_traversal_ids).exactly(batches_count).and_call_original
- expect { migration.perform(start_id, end_id, nil, 'up') }.to change(project_namespaces, :count).by(14)
+ expect { migration.perform(start_id, end_id, nil, nil, nil, nil, nil, 'up') }.to change(project_namespaces, :count).by(14)
expect(projects_count).to eq(project_namespaces.count)
end
@@ -125,7 +125,7 @@ RSpec.describe Gitlab::BackgroundMigration::ProjectNamespaces::BackfillProjectNa
context 'back-fill project namespaces in batches' do
before do
- stub_const("#{described_class.name}::BATCH_SIZE", 2)
+ stub_const("#{described_class.name}::SUB_BATCH_SIZE", 2)
end
it_behaves_like 'back-fill project namespaces'
@@ -137,7 +137,7 @@ RSpec.describe Gitlab::BackgroundMigration::ProjectNamespaces::BackfillProjectNa
start_id = ::Project.minimum(:id)
end_id = ::Project.maximum(:id)
# back-fill first
- described_class.new.perform(start_id, end_id, nil, 'up')
+ described_class.new.perform(start_id, end_id, nil, nil, nil, nil, nil, 'up')
end
shared_examples 'cleanup project namespaces' do
@@ -146,7 +146,7 @@ RSpec.describe Gitlab::BackgroundMigration::ProjectNamespaces::BackfillProjectNa
start_id = ::Project.minimum(:id)
end_id = ::Project.maximum(:id)
migration = described_class.new
- batches_count = (projects_count / described_class::BATCH_SIZE.to_f).ceil
+ batches_count = (projects_count / described_class::SUB_BATCH_SIZE.to_f).ceil
expect(projects_count).to be > 0
expect(projects_count).to eq(::Namespace.where(type: 'Project').count)
@@ -154,7 +154,7 @@ RSpec.describe Gitlab::BackgroundMigration::ProjectNamespaces::BackfillProjectNa
expect(migration).to receive(:nullify_project_namespaces_in_projects).exactly(batches_count).and_call_original
expect(migration).to receive(:delete_project_namespace_records).exactly(batches_count).and_call_original
- migration.perform(start_id, end_id, nil, 'down')
+ migration.perform(start_id, end_id, nil, nil, nil, nil, nil, 'down')
expect(::Project.count).to be > 0
expect(::Namespace.where(type: 'Project').count).to eq(0)
@@ -168,7 +168,7 @@ RSpec.describe Gitlab::BackgroundMigration::ProjectNamespaces::BackfillProjectNa
start_id = backfilled_namespace_projects.minimum(:id)
end_id = backfilled_namespace_projects.maximum(:id)
group_projects_count = backfilled_namespace_projects.count
- batches_count = (group_projects_count / described_class::BATCH_SIZE.to_f).ceil
+ batches_count = (group_projects_count / described_class::SUB_BATCH_SIZE.to_f).ceil
project_namespaces_in_hierarchy = project_namespaces_in_hierarchy(base_ancestor(backfilled_namespace))
migration = described_class.new
@@ -176,7 +176,7 @@ RSpec.describe Gitlab::BackgroundMigration::ProjectNamespaces::BackfillProjectNa
expect(migration).to receive(:nullify_project_namespaces_in_projects).exactly(batches_count).and_call_original
expect(migration).to receive(:delete_project_namespace_records).exactly(batches_count).and_call_original
- migration.perform(start_id, end_id, backfilled_namespace.id, 'down')
+ migration.perform(start_id, end_id, nil, nil, nil, nil, backfilled_namespace.id, 'down')
expect(::Namespace.where(type: 'Project').count).to be > 0
expect(project_namespaces_in_hierarchy.count).to eq(0)
@@ -190,7 +190,7 @@ RSpec.describe Gitlab::BackgroundMigration::ProjectNamespaces::BackfillProjectNa
context 'cleanup project namespaces in batches' do
before do
- stub_const("#{described_class.name}::BATCH_SIZE", 2)
+ stub_const("#{described_class.name}::SUB_BATCH_SIZE", 2)
end
it_behaves_like 'cleanup project namespaces'
diff --git a/spec/lib/gitlab/background_migration/remove_duplicate_vulnerabilities_findings_spec.rb b/spec/lib/gitlab/background_migration/remove_duplicate_vulnerabilities_findings_spec.rb
index 7214225c32c..f6f4a3f6115 100644
--- a/spec/lib/gitlab/background_migration/remove_duplicate_vulnerabilities_findings_spec.rb
+++ b/spec/lib/gitlab/background_migration/remove_duplicate_vulnerabilities_findings_spec.rb
@@ -87,7 +87,7 @@ RSpec.describe Gitlab::BackgroundMigration::RemoveDuplicateVulnerabilitiesFindin
let!(:unrelated_finding) do
create_finding!(
id: 9999999,
- uuid: "unreleated_finding",
+ uuid: Gitlab::UUID.v5(SecureRandom.hex),
vulnerability_id: nil,
report_type: 1,
location_fingerprint: 'random_location_fingerprint',
diff --git a/spec/lib/gitlab/background_migration/wrongfully_confirmed_email_unconfirmer_spec.rb b/spec/lib/gitlab/background_migration/wrongfully_confirmed_email_unconfirmer_spec.rb
deleted file mode 100644
index 17fe25c7f71..00000000000
--- a/spec/lib/gitlab/background_migration/wrongfully_confirmed_email_unconfirmer_spec.rb
+++ /dev/null
@@ -1,130 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::BackgroundMigration::WrongfullyConfirmedEmailUnconfirmer, schema: 20210301200959 do
- let(:users) { table(:users) }
- let(:emails) { table(:emails) }
- let(:user_synced_attributes_metadata) { table(:user_synced_attributes_metadata) }
- let(:confirmed_at_2_days_ago) { 2.days.ago }
- let(:confirmed_at_3_days_ago) { 3.days.ago }
- let(:one_year_ago) { 1.year.ago }
-
- let!(:user_needs_migration_1) { users.create!(name: 'user1', email: 'test1@test.com', state: 'active', projects_limit: 1, confirmed_at: confirmed_at_2_days_ago, confirmation_sent_at: one_year_ago) }
- let!(:user_needs_migration_2) { users.create!(name: 'user2', email: 'test2@test.com', unconfirmed_email: 'unconfirmed@test.com', state: 'active', projects_limit: 1, confirmed_at: confirmed_at_3_days_ago, confirmation_sent_at: one_year_ago) }
- let!(:user_does_not_need_migration) { users.create!(name: 'user3', email: 'test3@test.com', state: 'active', projects_limit: 1) }
- let!(:inactive_user) { users.create!(name: 'user4', email: 'test4@test.com', state: 'blocked', projects_limit: 1, confirmed_at: confirmed_at_3_days_ago, confirmation_sent_at: one_year_ago) }
- let!(:alert_bot_user) { users.create!(name: 'user5', email: 'test5@test.com', state: 'active', user_type: 2, projects_limit: 1, confirmed_at: confirmed_at_3_days_ago, confirmation_sent_at: one_year_ago) }
- let!(:user_has_synced_email) { users.create!(name: 'user6', email: 'test6@test.com', state: 'active', projects_limit: 1, confirmed_at: confirmed_at_2_days_ago, confirmation_sent_at: one_year_ago) }
- let!(:synced_attributes_metadata_for_user) { user_synced_attributes_metadata.create!(user_id: user_has_synced_email.id, email_synced: true) }
-
- let!(:bad_email_1) { emails.create!(user_id: user_needs_migration_1.id, email: 'other1@test.com', confirmed_at: confirmed_at_2_days_ago, confirmation_sent_at: one_year_ago) }
- let!(:bad_email_2) { emails.create!(user_id: user_needs_migration_2.id, email: 'other2@test.com', confirmed_at: confirmed_at_3_days_ago, confirmation_sent_at: one_year_ago) }
- let!(:bad_email_3_inactive_user) { emails.create!(user_id: inactive_user.id, email: 'other-inactive@test.com', confirmed_at: confirmed_at_3_days_ago, confirmation_sent_at: one_year_ago) }
- let!(:bad_email_4_bot_user) { emails.create!(user_id: alert_bot_user.id, email: 'other-bot@test.com', confirmed_at: confirmed_at_3_days_ago, confirmation_sent_at: one_year_ago) }
-
- let!(:good_email_1) { emails.create!(user_id: user_needs_migration_2.id, email: 'other3@test.com', confirmed_at: confirmed_at_2_days_ago, confirmation_sent_at: one_year_ago) }
- let!(:good_email_2) { emails.create!(user_id: user_needs_migration_2.id, email: 'other4@test.com', confirmed_at: nil) }
- let!(:good_email_3) { emails.create!(user_id: user_does_not_need_migration.id, email: 'other5@test.com', confirmed_at: confirmed_at_2_days_ago, confirmation_sent_at: one_year_ago) }
-
- let!(:second_email_for_user_with_synced_email) { emails.create!(user_id: user_has_synced_email.id, email: 'other6@test.com', confirmed_at: confirmed_at_2_days_ago, confirmation_sent_at: one_year_ago) }
-
- subject do
- email_ids = [bad_email_1, bad_email_2, good_email_1, good_email_2, good_email_3, second_email_for_user_with_synced_email].map(&:id)
-
- described_class.new.perform(email_ids.min, email_ids.max)
- end
-
- it 'does not change irrelevant email records' do
- subject
-
- expect(good_email_1.reload.confirmed_at).to be_within(1.second).of(confirmed_at_2_days_ago)
- expect(good_email_2.reload.confirmed_at).to be_nil
- expect(good_email_3.reload.confirmed_at).to be_within(1.second).of(confirmed_at_2_days_ago)
-
- expect(bad_email_3_inactive_user.reload.confirmed_at).to be_within(1.second).of(confirmed_at_3_days_ago)
- expect(bad_email_4_bot_user.reload.confirmed_at).to be_within(1.second).of(confirmed_at_3_days_ago)
-
- expect(good_email_1.reload.confirmation_sent_at).to be_within(1.second).of(one_year_ago)
- expect(good_email_2.reload.confirmation_sent_at).to be_nil
- expect(good_email_3.reload.confirmation_sent_at).to be_within(1.second).of(one_year_ago)
-
- expect(bad_email_3_inactive_user.reload.confirmation_sent_at).to be_within(1.second).of(one_year_ago)
- expect(bad_email_4_bot_user.reload.confirmation_sent_at).to be_within(1.second).of(one_year_ago)
- end
-
- it 'clears the `unconfirmed_email` field' do
- subject
-
- user_needs_migration_2.reload
- expect(user_needs_migration_2.unconfirmed_email).to be_nil
- end
-
- it 'does not change irrelevant user records' do
- subject
-
- expect(user_does_not_need_migration.reload.confirmed_at).to be_nil
- expect(inactive_user.reload.confirmed_at).to be_within(1.second).of(confirmed_at_3_days_ago)
- expect(alert_bot_user.reload.confirmed_at).to be_within(1.second).of(confirmed_at_3_days_ago)
- expect(user_has_synced_email.reload.confirmed_at).to be_within(1.second).of(confirmed_at_2_days_ago)
-
- expect(user_does_not_need_migration.reload.confirmation_sent_at).to be_nil
- expect(inactive_user.reload.confirmation_sent_at).to be_within(1.second).of(one_year_ago)
- expect(alert_bot_user.reload.confirmation_sent_at).to be_within(1.second).of(one_year_ago)
- expect(user_has_synced_email.confirmation_sent_at).to be_within(1.second).of(one_year_ago)
- end
-
- it 'updates confirmation_sent_at column' do
- subject
-
- expect(user_needs_migration_1.reload.confirmation_sent_at).to be_within(1.minute).of(Time.now)
- expect(user_needs_migration_2.reload.confirmation_sent_at).to be_within(1.minute).of(Time.now)
-
- expect(bad_email_1.reload.confirmation_sent_at).to be_within(1.minute).of(Time.now)
- expect(bad_email_2.reload.confirmation_sent_at).to be_within(1.minute).of(Time.now)
- end
-
- it 'unconfirms bad email records' do
- subject
-
- expect(bad_email_1.reload.confirmed_at).to be_nil
- expect(bad_email_2.reload.confirmed_at).to be_nil
-
- expect(bad_email_1.reload.confirmation_token).not_to be_nil
- expect(bad_email_2.reload.confirmation_token).not_to be_nil
- end
-
- it 'unconfirms user records' do
- subject
-
- expect(user_needs_migration_1.reload.confirmed_at).to be_nil
- expect(user_needs_migration_2.reload.confirmed_at).to be_nil
-
- expect(user_needs_migration_1.reload.confirmation_token).not_to be_nil
- expect(user_needs_migration_2.reload.confirmation_token).not_to be_nil
- end
-
- context 'enqueued jobs' do
- let(:user_1) { User.find(user_needs_migration_1.id) }
- let(:user_2) { User.find(user_needs_migration_2.id) }
-
- let(:email_1) { Email.find(bad_email_1.id) }
- let(:email_2) { Email.find(bad_email_2.id) }
-
- it 'enqueues the email confirmation and the unconfirm notification mailer jobs' do
- allow(DeviseMailer).to receive(:confirmation_instructions).and_call_original
- allow(Gitlab::BackgroundMigration::Mailers::UnconfirmMailer).to receive(:unconfirm_notification_email).and_call_original
-
- subject
-
- expect(DeviseMailer).to have_received(:confirmation_instructions).with(email_1, email_1.confirmation_token)
- expect(DeviseMailer).to have_received(:confirmation_instructions).with(email_2, email_2.confirmation_token)
-
- expect(Gitlab::BackgroundMigration::Mailers::UnconfirmMailer).to have_received(:unconfirm_notification_email).with(user_1)
- expect(DeviseMailer).to have_received(:confirmation_instructions).with(user_1, user_1.confirmation_token)
-
- expect(Gitlab::BackgroundMigration::Mailers::UnconfirmMailer).to have_received(:unconfirm_notification_email).with(user_2)
- expect(DeviseMailer).to have_received(:confirmation_instructions).with(user_2, user_2.confirmation_token)
- end
- end
-end
diff --git a/spec/lib/gitlab/bitbucket_server_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_server_import/importer_spec.rb
index 0380ddd9a2e..d2abdb740f8 100644
--- a/spec/lib/gitlab/bitbucket_server_import/importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_server_import/importer_spec.rb
@@ -22,8 +22,8 @@ RSpec.describe Gitlab::BitbucketServerImport::Importer do
data: { project_key: project_key, repo_slug: repo_slug },
credentials: { base_uri: import_url, user: bitbucket_user, password: password }
)
- data.save
- project.save
+ data.save!
+ project.save!
end
describe '#import_repository' do
diff --git a/spec/lib/gitlab/buffered_io_spec.rb b/spec/lib/gitlab/buffered_io_spec.rb
new file mode 100644
index 00000000000..f8896abd46e
--- /dev/null
+++ b/spec/lib/gitlab/buffered_io_spec.rb
@@ -0,0 +1,54 @@
+# rubocop:disable Style/FrozenStringLiteralComment
+require 'spec_helper'
+
+RSpec.describe Gitlab::BufferedIo do
+ describe '#readuntil' do
+ let(:never_ending_tcp_socket) do
+ Class.new do
+ def initialize(*_)
+ @read_counter = 0
+ end
+
+ def setsockopt(*_); end
+
+ def closed?
+ false
+ end
+
+ def close
+ true
+ end
+
+ def to_io
+ StringIO.new('Hello World!')
+ end
+
+ def write_nonblock(data, *_)
+ data.size
+ end
+
+ def read_nonblock(buffer_size, *_)
+ sleep 0.01
+ @read_counter += 1
+
+ raise 'Test did not raise HeaderReadTimeout' if @read_counter > 10
+
+ 'H' * buffer_size
+ end
+ end
+ end
+
+ before do
+ stub_const('Gitlab::BufferedIo::HEADER_READ_TIMEOUT', 0.1)
+ end
+
+ subject(:readuntil) do
+ Gitlab::BufferedIo.new(never_ending_tcp_socket.new).readuntil('a')
+ end
+
+ it 'raises a timeout error' do
+ expect { readuntil }.to raise_error(Gitlab::HTTP::HeaderReadTimeout, /Request timed out after reading headers for 0\.[0-9]+ seconds/)
+ end
+ end
+end
+# rubocop:enable Style/FrozenStringLiteralComment
diff --git a/spec/lib/gitlab/changelog/config_spec.rb b/spec/lib/gitlab/changelog/config_spec.rb
index c410ba4d116..600682d30ad 100644
--- a/spec/lib/gitlab/changelog/config_spec.rb
+++ b/spec/lib/gitlab/changelog/config_spec.rb
@@ -31,6 +31,20 @@ RSpec.describe Gitlab::Changelog::Config do
described_class.from_git(project)
end
+
+ context 'when changelog is empty' do
+ it 'returns the default configuration' do
+ allow(project.repository)
+ .to receive(:changelog_config)
+ .and_return("")
+
+ expect(described_class)
+ .to receive(:new)
+ .with(project)
+
+ described_class.from_git(project)
+ end
+ end
end
describe '.from_hash' do
diff --git a/spec/lib/gitlab/changelog/release_spec.rb b/spec/lib/gitlab/changelog/release_spec.rb
index d8434821640..defcec5aa65 100644
--- a/spec/lib/gitlab/changelog/release_spec.rb
+++ b/spec/lib/gitlab/changelog/release_spec.rb
@@ -139,6 +139,16 @@ RSpec.describe Gitlab::Changelog::Release do
OUT
end
end
+
+ context 'when template parser raises an error' do
+ before do
+ allow(config).to receive(:template).and_raise(Gitlab::TemplateParser::Error)
+ end
+
+ it 'raises a Changelog error' do
+ expect { release.to_markdown }.to raise_error(Gitlab::Changelog::Error)
+ end
+ end
end
describe '#header_start_position' do
diff --git a/spec/lib/gitlab/checks/branch_check_spec.rb b/spec/lib/gitlab/checks/branch_check_spec.rb
index f503759f3f8..c06d26d1441 100644
--- a/spec/lib/gitlab/checks/branch_check_spec.rb
+++ b/spec/lib/gitlab/checks/branch_check_spec.rb
@@ -40,15 +40,6 @@ RSpec.describe Gitlab::Checks::BranchCheck do
expect { subject.validate! }.not_to raise_error
end
end
-
- context "the feature flag is disabled" do
- it "doesn't prohibit a 40-character hexadecimal branch name" do
- stub_feature_flags(prohibit_hexadecimal_branch_names: false)
- allow(subject).to receive(:branch_name).and_return("267208abfe40e546f5e847444276f7d43a39503e")
-
- expect { subject.validate! }.not_to raise_error
- end
- end
end
context 'protected branches check' do
diff --git a/spec/lib/gitlab/ci/badge/release/latest_release_spec.rb b/spec/lib/gitlab/ci/badge/release/latest_release_spec.rb
new file mode 100644
index 00000000000..36f9f4fb321
--- /dev/null
+++ b/spec/lib/gitlab/ci/badge/release/latest_release_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Badge::Release::LatestRelease do
+ let(:project) { create(:project, :repository) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_guest(user)
+ create(:release, project: project, released_at: 1.day.ago)
+ end
+
+ subject { described_class.new(project, user) }
+
+ describe '#entity' do
+ it 'describes latest release' do
+ expect(subject.entity).to eq 'Latest Release'
+ end
+ end
+
+ describe '#tag' do
+ it 'returns latest release tag for the project ordered using release_at' do
+ create(:release, tag: "v1.0.0", project: project, released_at: 1.hour.ago)
+ latest_release = create(:release, tag: "v2.0.0", project: project, released_at: Time.current)
+
+ expect(subject.tag).to eq latest_release.tag
+ end
+ end
+
+ describe '#metadata' do
+ it 'returns correct metadata' do
+ expect(subject.metadata.image_url).to include 'release.svg'
+ end
+ end
+
+ describe '#template' do
+ it 'returns correct template' do
+ expect(subject.template.key_text).to eq 'Latest Release'
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/badge/release/metadata_spec.rb b/spec/lib/gitlab/ci/badge/release/metadata_spec.rb
new file mode 100644
index 00000000000..d68358f1458
--- /dev/null
+++ b/spec/lib/gitlab/ci/badge/release/metadata_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require 'lib/gitlab/ci/badge/shared/metadata'
+
+RSpec.describe Gitlab::Ci::Badge::Release::Metadata do
+ let(:project) { create(:project) }
+ let(:ref) { 'feature' }
+ let!(:release) { create(:release, tag: ref, project: project) }
+ let(:user) { create(:user) }
+ let(:badge) do
+ Gitlab::Ci::Badge::Release::LatestRelease.new(project, user)
+ end
+
+ let(:metadata) { described_class.new(badge) }
+
+ before do
+ project.add_guest(user)
+ end
+
+ it_behaves_like 'badge metadata'
+
+ describe '#title' do
+ it 'returns latest release title' do
+ expect(metadata.title).to eq 'Latest Release'
+ end
+ end
+
+ describe '#image_url' do
+ it 'returns valid url' do
+ expect(metadata.image_url).to include "/-/badges/release.svg"
+ end
+ end
+
+ describe '#link_url' do
+ it 'returns valid link' do
+ expect(metadata.link_url).to include "/-/releases"
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/badge/release/template_spec.rb b/spec/lib/gitlab/ci/badge/release/template_spec.rb
new file mode 100644
index 00000000000..2b66c296a94
--- /dev/null
+++ b/spec/lib/gitlab/ci/badge/release/template_spec.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Badge::Release::Template do
+ let(:project) { create(:project) }
+ let(:ref) { 'v1.2.3' }
+ let(:user) { create(:user) }
+ let!(:release) { create(:release, tag: ref, project: project) }
+ let(:badge) { Gitlab::Ci::Badge::Release::LatestRelease.new(project, user) }
+ let(:template) { described_class.new(badge) }
+
+ before do
+ project.add_guest(user)
+ end
+
+ describe '#key_text' do
+ it 'defaults to latest release' do
+ expect(template.key_text).to eq 'Latest Release'
+ end
+
+ it 'returns custom key text' do
+ key_text = 'Test Release'
+ badge = Gitlab::Ci::Badge::Release::LatestRelease.new(project, user, opts: { key_text: key_text })
+
+ expect(described_class.new(badge).key_text).to eq key_text
+ end
+ end
+
+ describe '#value_text' do
+ context 'when a release exists' do
+ it 'returns the tag of the release' do
+ expect(template.value_text).to eq ref
+ end
+ end
+
+ context 'no releases exist' do
+ before do
+ allow(badge).to receive(:tag).and_return(nil)
+ end
+
+ it 'returns string that latest release is none' do
+ expect(template.value_text).to eq 'none'
+ end
+ end
+ end
+
+ describe '#key_width' do
+ it 'returns the default key width' do
+ expect(template.key_width).to eq 90
+ end
+
+ it 'returns custom key width' do
+ key_width = 100
+ badge = Gitlab::Ci::Badge::Release::LatestRelease.new(project, user, opts: { key_width: key_width })
+
+ expect(described_class.new(badge).key_width).to eq key_width
+ end
+ end
+
+ describe '#value_width' do
+ it 'has a fixed value width' do
+ expect(template.value_width).to eq 54
+ end
+ end
+
+ describe '#key_color' do
+ it 'always has the same color' do
+ expect(template.key_color).to eq '#555'
+ end
+ end
+
+ describe '#value_color' do
+ context 'when release exists' do
+ it 'is blue' do
+ expect(template.value_color).to eq '#3076af'
+ end
+ end
+
+ context 'when release does not exist' do
+ before do
+ allow(badge).to receive(:tag).and_return(nil)
+ end
+
+ it 'is red' do
+ expect(template.value_color).to eq '#e05d44'
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/build/artifacts/expire_in_parser_spec.rb b/spec/lib/gitlab/ci/build/artifacts/expire_in_parser_spec.rb
index 0e26a9fa571..889878cf3ef 100644
--- a/spec/lib/gitlab/ci/build/artifacts/expire_in_parser_spec.rb
+++ b/spec/lib/gitlab/ci/build/artifacts/expire_in_parser_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Gitlab::Ci::Build::Artifacts::ExpireInParser do
- describe '.validate_duration' do
+ describe '.validate_duration', :request_store do
subject { described_class.validate_duration(value) }
context 'with never' do
@@ -20,14 +20,33 @@ RSpec.describe Gitlab::Ci::Build::Artifacts::ExpireInParser do
context 'with a duration' do
let(:value) { '1 Day' }
+ let(:other_value) { '30 seconds' }
it { is_expected.to be_truthy }
+
+ it 'caches data' do
+ expect(ChronicDuration).to receive(:parse).with(value).once.and_call_original
+ expect(ChronicDuration).to receive(:parse).with(other_value).once.and_call_original
+
+ 2.times do
+ expect(described_class.validate_duration(value)).to eq(86400)
+ expect(described_class.validate_duration(other_value)).to eq(30)
+ end
+ end
end
context 'without a duration' do
let(:value) { 'something' }
it { is_expected.to be_falsy }
+
+ it 'caches data' do
+ expect(ChronicDuration).to receive(:parse).with(value).once.and_call_original
+
+ 2.times do
+ expect(described_class.validate_duration(value)).to be_falsey
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb b/spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb
index 532c83f6768..4ac8bf61738 100644
--- a/spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb
+++ b/spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb
@@ -4,14 +4,23 @@ require 'spec_helper'
RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Changes do
describe '#satisfied_by?' do
+ subject { described_class.new(globs).satisfied_by?(pipeline, context) }
+
it_behaves_like 'a glob matching rule' do
let(:pipeline) { build(:ci_pipeline) }
+ let(:context) {}
before do
allow(pipeline).to receive(:modified_paths).and_return(files.keys)
end
+ end
- subject { described_class.new(globs).satisfied_by?(pipeline, nil) }
+ context 'when pipeline is nil' do
+ let(:pipeline) {}
+ let(:context) {}
+ let(:globs) { [] }
+
+ it { is_expected.to be_truthy }
end
context 'when using variable expansion' do
@@ -20,8 +29,6 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Changes do
let(:globs) { ['$HELM_DIR/**/*'] }
let(:context) { double('context') }
- subject { described_class.new(globs).satisfied_by?(pipeline, context) }
-
before do
allow(pipeline).to receive(:modified_paths).and_return(modified_paths)
end
@@ -32,6 +39,12 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Changes do
it { is_expected.to be_falsey }
end
+ context 'when modified paths are nil' do
+ let(:modified_paths) {}
+
+ it { is_expected.to be_truthy }
+ end
+
context 'when context has the specified variables' do
let(:variables_hash) do
{ 'HELM_DIR' => 'helm' }
diff --git a/spec/lib/gitlab/ci/config/entry/include/rules/rule_spec.rb b/spec/lib/gitlab/ci/config/entry/include/rules/rule_spec.rb
index 0505b17ea91..e83d4974bb7 100644
--- a/spec/lib/gitlab/ci/config/entry/include/rules/rule_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/include/rules/rule_spec.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require 'fast_spec_helper'
+require_dependency 'active_model'
RSpec.describe Gitlab::Ci::Config::Entry::Include::Rules::Rule do
let(:factory) do
diff --git a/spec/lib/gitlab/ci/config/entry/include/rules_spec.rb b/spec/lib/gitlab/ci/config/entry/include/rules_spec.rb
index c255d6e9dd6..d5988dbbb58 100644
--- a/spec/lib/gitlab/ci/config/entry/include/rules_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/include/rules_spec.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require 'fast_spec_helper'
+require_dependency 'active_model'
RSpec.describe Gitlab::Ci::Config::Entry::Include::Rules do
let(:factory) do
diff --git a/spec/lib/gitlab/ci/config/entry/include_spec.rb b/spec/lib/gitlab/ci/config/entry/include_spec.rb
index 275cdcddeb0..fd7f85c9298 100644
--- a/spec/lib/gitlab/ci/config/entry/include_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/include_spec.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require 'fast_spec_helper'
+require_dependency 'active_model'
RSpec.describe ::Gitlab::Ci::Config::Entry::Include do
subject(:include_entry) { described_class.new(config) }
diff --git a/spec/lib/gitlab/ci/config/entry/jobs_spec.rb b/spec/lib/gitlab/ci/config/entry/jobs_spec.rb
index 9a2a67389fc..b03175cd80f 100644
--- a/spec/lib/gitlab/ci/config/entry/jobs_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/jobs_spec.rb
@@ -70,6 +70,14 @@ RSpec.describe Gitlab::Ci::Config::Entry::Jobs do
it 'reports error' do
expect(entry.errors).to include 'jobs rspec config should implement a script: or a trigger: keyword'
end
+
+ context 'when the job name cannot be cast directly to a symbol' do
+ let(:config) { { true => nil } }
+
+ it 'properly parses the job name without raising a NoMethodError' do
+ expect(entry.errors).to include 'jobs true config should implement a script: or a trigger: keyword'
+ end
+ end
end
context 'when no visible jobs present' do
diff --git a/spec/lib/gitlab/ci/config/entry/policy_spec.rb b/spec/lib/gitlab/ci/config/entry/policy_spec.rb
index 46800055dd9..e5de0fb38e3 100644
--- a/spec/lib/gitlab/ci/config/entry/policy_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/policy_spec.rb
@@ -1,8 +1,6 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
-require 'support/helpers/stub_feature_flags'
-require_dependency 'active_model'
+require 'spec_helper'
RSpec.describe Gitlab::Ci::Config::Entry::Policy do
let(:entry) { described_class.new(config) }
@@ -47,6 +45,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Policy do
end
context 'when using unsafe regexp' do
+ # When removed we could use `require 'fast_spec_helper'` again.
include StubFeatureFlags
let(:config) { ['/^(?!master).+/'] }
@@ -89,7 +88,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Policy do
describe '#errors' do
it 'saves errors' do
expect(entry.errors)
- .to include /policy config should be an array of strings or regexps/
+ .to include /policy config should be an array of strings or regular expressions/
end
end
end
@@ -107,6 +106,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Policy do
end
context 'when using unsafe regexp' do
+ # When removed we could use `require 'fast_spec_helper'` again.
include StubFeatureFlags
let(:config) { { refs: ['/^(?!master).+/'] } }
diff --git a/spec/lib/gitlab/ci/config/entry/root_spec.rb b/spec/lib/gitlab/ci/config/entry/root_spec.rb
index 749d1386ed9..daf58aff116 100644
--- a/spec/lib/gitlab/ci/config/entry/root_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/root_spec.rb
@@ -55,13 +55,13 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
}
end
- context 'when deprecated types keyword is defined' do
+ context 'when deprecated types/type keywords are defined' do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:hash) do
{ types: %w(test deploy),
- rspec: { script: 'rspec' } }
+ rspec: { script: 'rspec', type: 'test' } }
end
before do
@@ -69,11 +69,15 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
end
it 'returns array of types as stages with a warning' do
+ expect(root.jobs_value[:rspec][:stage]).to eq 'test'
expect(root.stages_value).to eq %w[test deploy]
- expect(root.warnings).to match_array(["root `types` is deprecated in 9.0 and will be removed in 15.0."])
+ expect(root.warnings).to match_array([
+ "root `types` is deprecated in 9.0 and will be removed in 15.0.",
+ "jobs:rspec `type` is deprecated in 9.0 and will be removed in 15.0."
+ ])
end
- it 'logs usage of types keyword' do
+ it 'logs usage of keywords' do
expect(Gitlab::AppJsonLogger).to(
receive(:info)
.with(event: 'ci_used_deprecated_keyword',
@@ -350,9 +354,9 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
root.compose!
end
- context 'when before script is not an array' do
+ context 'when before script is a number' do
let(:hash) do
- { before_script: 'ls' }
+ { before_script: 123 }
end
describe '#valid?' do
@@ -364,7 +368,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
describe '#errors' do
it 'reports errors from child nodes' do
expect(root.errors)
- .to include 'before_script config should be an array containing strings and arrays of strings'
+ .to include 'before_script config should be a string or a nested array of strings up to 10 levels deep'
end
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/script_spec.rb b/spec/lib/gitlab/ci/config/entry/script_spec.rb
deleted file mode 100644
index 1ddf7881e81..00000000000
--- a/spec/lib/gitlab/ci/config/entry/script_spec.rb
+++ /dev/null
@@ -1,109 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Ci::Config::Entry::Script do
- let(:entry) { described_class.new(config) }
-
- describe 'validations' do
- context 'when entry config value is array of strings' do
- let(:config) { %w(ls pwd) }
-
- describe '#value' do
- it 'returns array of strings' do
- expect(entry.value).to eq config
- end
- end
-
- describe '#errors' do
- it 'does not append errors' do
- expect(entry.errors).to be_empty
- end
- end
-
- describe '#valid?' do
- it 'is valid' do
- expect(entry).to be_valid
- end
- end
- end
-
- context 'when entry config value is array of arrays of strings' do
- let(:config) { [['ls'], ['pwd', 'echo 1']] }
-
- describe '#value' do
- it 'returns array of strings' do
- expect(entry.value).to eq ['ls', 'pwd', 'echo 1']
- end
- end
-
- describe '#errors' do
- it 'does not append errors' do
- expect(entry.errors).to be_empty
- end
- end
-
- describe '#valid?' do
- it 'is valid' do
- expect(entry).to be_valid
- end
- end
- end
-
- context 'when entry config value is array containing strings and arrays of strings' do
- let(:config) { ['ls', ['pwd', 'echo 1']] }
-
- describe '#value' do
- it 'returns array of strings' do
- expect(entry.value).to eq ['ls', 'pwd', 'echo 1']
- end
- end
-
- describe '#errors' do
- it 'does not append errors' do
- expect(entry.errors).to be_empty
- end
- end
-
- describe '#valid?' do
- it 'is valid' do
- expect(entry).to be_valid
- end
- end
- end
-
- context 'when entry value is string' do
- let(:config) { 'ls' }
-
- describe '#errors' do
- it 'saves errors' do
- expect(entry.errors)
- .to include 'script config should be an array containing strings and arrays of strings'
- end
- end
-
- describe '#valid?' do
- it 'is not valid' do
- expect(entry).not_to be_valid
- end
- end
- end
-
- context 'when entry value is multi-level nested array' do
- let(:config) { [['ls', ['echo 1']], 'pwd'] }
-
- describe '#errors' do
- it 'saves errors' do
- expect(entry.errors)
- .to include 'script config should be an array containing strings and arrays of strings'
- end
- end
-
- describe '#valid?' do
- it 'is not valid' do
- expect(entry).not_to be_valid
- end
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/ci/config/external/mapper_spec.rb b/spec/lib/gitlab/ci/config/external/mapper_spec.rb
index cebe8984741..f8754d7e124 100644
--- a/spec/lib/gitlab/ci/config/external/mapper_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/mapper_spec.rb
@@ -175,27 +175,35 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper do
end
end
- context "when duplicate 'include' is defined" do
+ context "when duplicate 'include's are defined" do
+ let(:values) do
+ { include: [
+ { 'local' => local_file },
+ { 'local' => local_file }
+ ],
+ image: 'ruby:2.7' }
+ end
+
+ it 'does not raise an exception' do
+ expect { subject }.not_to raise_error
+ end
+ end
+
+ context 'when passing max number of files' do
let(:values) do
{ include: [
{ 'local' => local_file },
- { 'local' => local_file }
+ { 'remote' => remote_url }
],
image: 'ruby:2.7' }
end
- it 'raises an exception' do
- expect { subject }.to raise_error(described_class::DuplicateIncludesError)
+ before do
+ stub_const("#{described_class}::MAX_INCLUDES", 2)
end
- context 'when including multiple files from a project' do
- let(:values) do
- { include: { project: project.full_path, file: [local_file, local_file] } }
- end
-
- it 'raises an exception' do
- expect { subject }.to raise_error(described_class::DuplicateIncludesError)
- end
+ it 'does not raise an exception' do
+ expect { subject }.not_to raise_error
end
end
diff --git a/spec/lib/gitlab/ci/config/external/rules_spec.rb b/spec/lib/gitlab/ci/config/external/rules_spec.rb
index 091bd3b07e6..e2bb55f3854 100644
--- a/spec/lib/gitlab/ci/config/external/rules_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/rules_spec.rb
@@ -45,7 +45,7 @@ RSpec.describe Gitlab::Ci::Config::External::Rules do
let(:context) { double(project: project, sha: project.repository.tree.sha, top_level_worktree_paths: ['Dockerfile']) }
before do
- project.repository.create_file(project.owner, 'Dockerfile', "commit", message: 'test', branch_name: "master")
+ project.repository.create_file(project.first_owner, 'Dockerfile', "commit", message: 'test', branch_name: "master")
end
it { is_expected.to eq(true) }
diff --git a/spec/lib/gitlab/ci/config/normalizer/matrix_strategy_spec.rb b/spec/lib/gitlab/ci/config/normalizer/matrix_strategy_spec.rb
index a29471706cc..1cc8b462224 100644
--- a/spec/lib/gitlab/ci/config/normalizer/matrix_strategy_spec.rb
+++ b/spec/lib/gitlab/ci/config/normalizer/matrix_strategy_spec.rb
@@ -1,12 +1,8 @@
# frozen_string_literal: true
require 'fast_spec_helper'
-require 'support/helpers/stubbed_feature'
-require 'support/helpers/stub_feature_flags'
RSpec.describe Gitlab::Ci::Config::Normalizer::MatrixStrategy do
- include StubFeatureFlags
-
describe '.applies_to?' do
subject { described_class.applies_to?(config) }
diff --git a/spec/lib/gitlab/ci/config_spec.rb b/spec/lib/gitlab/ci/config_spec.rb
index 1b3e8a2ce4a..05ff1f3618b 100644
--- a/spec/lib/gitlab/ci/config_spec.rb
+++ b/spec/lib/gitlab/ci/config_spec.rb
@@ -462,7 +462,7 @@ RSpec.describe Gitlab::Ci::Config do
expect(project.repository).to receive(:blob_data_at)
.with('eeff1122', local_location)
- described_class.new(gitlab_ci_yml, project: project, sha: 'eeff1122', user: user)
+ described_class.new(gitlab_ci_yml, project: project, sha: 'eeff1122', user: user, pipeline: pipeline)
end
end
@@ -470,7 +470,7 @@ RSpec.describe Gitlab::Ci::Config do
it 'is using latest SHA on the default branch' do
expect(project.repository).to receive(:root_ref_sha)
- described_class.new(gitlab_ci_yml, project: project, sha: nil, user: user)
+ described_class.new(gitlab_ci_yml, project: project, sha: nil, user: user, pipeline: pipeline)
end
end
end
diff --git a/spec/lib/gitlab/ci/lint_spec.rb b/spec/lib/gitlab/ci/lint_spec.rb
index 1e433d7854a..747ff13c840 100644
--- a/spec/lib/gitlab/ci/lint_spec.rb
+++ b/spec/lib/gitlab/ci/lint_spec.rb
@@ -7,9 +7,10 @@ RSpec.describe Gitlab::Ci::Lint do
let_it_be(:user) { create(:user) }
let(:lint) { described_class.new(project: project, current_user: user) }
+ let(:ref) { project.default_branch }
describe '#validate' do
- subject { lint.validate(content, dry_run: dry_run) }
+ subject { lint.validate(content, dry_run: dry_run, ref: ref) }
shared_examples 'content is valid' do
let(:content) do
@@ -251,6 +252,29 @@ RSpec.describe Gitlab::Ci::Lint do
end
end
+ context 'when using a ref other than the default branch' do
+ let(:ref) { 'feature' }
+ let(:content) do
+ <<~YAML
+ build:
+ stage: build
+ script: echo 1
+ rules:
+ - if: "$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH"
+ test:
+ stage: test
+ script: echo 2
+ rules:
+ - if: "$CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH"
+ YAML
+ end
+
+ it 'includes only jobs that are excluded on the default branch' do
+ expect(subject.jobs.size).to eq(1)
+ expect(subject.jobs[0][:name]).to eq('test')
+ end
+ end
+
it_behaves_like 'sets merged yaml'
include_context 'advanced validations' do
@@ -298,4 +322,102 @@ RSpec.describe Gitlab::Ci::Lint do
end
end
end
+
+ context 'pipeline logger' do
+ let(:counters) do
+ {
+ 'count' => a_kind_of(Numeric),
+ 'avg' => a_kind_of(Numeric),
+ 'max' => a_kind_of(Numeric),
+ 'min' => a_kind_of(Numeric)
+ }
+ end
+
+ let(:loggable_data) do
+ {
+ 'class' => 'Gitlab::Ci::Pipeline::Logger',
+ 'config_build_context_duration_s' => counters,
+ 'config_build_variables_duration_s' => counters,
+ 'config_compose_duration_s' => counters,
+ 'config_expand_duration_s' => counters,
+ 'config_external_process_duration_s' => counters,
+ 'config_stages_inject_duration_s' => counters,
+ 'config_tags_resolve_duration_s' => counters,
+ 'config_yaml_extend_duration_s' => counters,
+ 'config_yaml_load_duration_s' => counters,
+ 'pipeline_creation_caller' => 'Gitlab::Ci::Lint',
+ 'pipeline_creation_service_duration_s' => a_kind_of(Numeric),
+ 'pipeline_persisted' => false,
+ 'pipeline_source' => 'unknown',
+ 'project_id' => project&.id,
+ 'yaml_process_duration_s' => counters
+ }
+ end
+
+ let(:content) do
+ <<~YAML
+ build:
+ script: echo
+ YAML
+ end
+
+ subject(:validate) { lint.validate(content, dry_run: false) }
+
+ before do
+ project&.add_developer(user)
+ end
+
+ context 'when the duration is under the threshold' do
+ it 'does not create a log entry' do
+ expect(Gitlab::AppJsonLogger).not_to receive(:info)
+
+ validate
+ end
+ end
+
+ context 'when the durations exceeds the threshold' do
+ let(:timer) do
+ proc do
+ @timer = @timer.to_i + 30
+ end
+ end
+
+ before do
+ allow(Gitlab::Ci::Pipeline::Logger)
+ .to receive(:current_monotonic_time) { timer.call }
+ end
+
+ it 'creates a log entry' do
+ expect(Gitlab::AppJsonLogger).to receive(:info).with(loggable_data)
+
+ validate
+ end
+
+ context 'when the feature flag is disabled' do
+ before do
+ stub_feature_flags(ci_pipeline_creation_logger: false)
+ end
+
+ it 'does not create a log entry' do
+ expect(Gitlab::AppJsonLogger).not_to receive(:info)
+
+ validate
+ end
+ end
+
+ context 'when project is not provided' do
+ let(:project) { nil }
+
+ let(:project_nil_loggable_data) do
+ loggable_data.except('project_id')
+ end
+
+ it 'creates a log entry without project_id' do
+ expect(Gitlab::AppJsonLogger).to receive(:info).with(project_nil_loggable_data)
+
+ validate
+ end
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/parsers/security/common_spec.rb b/spec/lib/gitlab/ci/parsers/security/common_spec.rb
index c49673f5a4a..7eec78ff186 100644
--- a/spec/lib/gitlab/ci/parsers/security/common_spec.rb
+++ b/spec/lib/gitlab/ci/parsers/security/common_spec.rb
@@ -40,60 +40,142 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do
allow(validator_class).to receive(:new).and_call_original
end
- context 'when the validate flag is set as `false`' do
- let(:validate) { false }
+ context 'when enforce_security_report_validation is enabled' do
+ before do
+ stub_feature_flags(enforce_security_report_validation: true)
+ end
- it 'does not run the validation logic' do
- parse_report
+ context 'when the validate flag is set as `true`' do
+ let(:validate) { true }
- expect(validator_class).not_to have_received(:new)
- end
- end
+ it 'instantiates the validator with correct params' do
+ parse_report
- context 'when the validate flag is set as `true`' do
- let(:validate) { true }
- let(:valid?) { false }
+ expect(validator_class).to have_received(:new).with(report.type, {})
+ end
- before do
- allow_next_instance_of(validator_class) do |instance|
- allow(instance).to receive(:valid?).and_return(valid?)
- allow(instance).to receive(:errors).and_return(['foo'])
+ context 'when the report data is valid according to the schema' do
+ let(:valid?) { true }
+
+ before do
+ allow_next_instance_of(validator_class) do |instance|
+ allow(instance).to receive(:valid?).and_return(valid?)
+ allow(instance).to receive(:errors).and_return([])
+ end
+
+ allow(parser).to receive_messages(create_scanner: true, create_scan: true)
+ end
+
+ it 'does not add errors to the report' do
+ expect { parse_report }.not_to change { report.errors }.from([])
+ end
+
+ it 'adds the schema validation status to the report' do
+ parse_report
+
+ expect(report.schema_validation_status).to eq(:valid_schema)
+ end
+
+ it 'keeps the execution flow as normal' do
+ parse_report
+
+ expect(parser).to have_received(:create_scanner)
+ expect(parser).to have_received(:create_scan)
+ end
end
- allow(parser).to receive_messages(create_scanner: true, create_scan: true)
- end
+ context 'when the report data is not valid according to the schema' do
+ let(:valid?) { false }
- it 'instantiates the validator with correct params' do
- parse_report
+ before do
+ allow_next_instance_of(validator_class) do |instance|
+ allow(instance).to receive(:valid?).and_return(valid?)
+ allow(instance).to receive(:errors).and_return(['foo'])
+ end
- expect(validator_class).to have_received(:new).with(report.type, {})
- end
+ allow(parser).to receive_messages(create_scanner: true, create_scan: true)
+ end
+
+ it 'adds errors to the report' do
+ expect { parse_report }.to change { report.errors }.from([]).to([{ message: 'foo', type: 'Schema' }])
+ end
+
+ it 'adds the schema validation status to the report' do
+ parse_report
- context 'when the report data is not valid according to the schema' do
- it 'adds errors to the report' do
- expect { parse_report }.to change { report.errors }.from([]).to([{ message: 'foo', type: 'Schema' }])
+ expect(report.schema_validation_status).to eq(:invalid_schema)
+ end
+
+ it 'does not try to create report entities' do
+ parse_report
+
+ expect(parser).not_to have_received(:create_scanner)
+ expect(parser).not_to have_received(:create_scan)
+ end
end
+ end
+ end
+
+ context 'when enforce_security_report_validation is disabled' do
+ before do
+ stub_feature_flags(enforce_security_report_validation: false)
+ end
+
+ context 'when the validate flag is set as `false`' do
+ let(:validate) { false }
- it 'does not try to create report entities' do
+ it 'does not run the validation logic' do
parse_report
- expect(parser).not_to have_received(:create_scanner)
- expect(parser).not_to have_received(:create_scan)
+ expect(validator_class).not_to have_received(:new)
end
end
- context 'when the report data is valid according to the schema' do
- let(:valid?) { true }
+ context 'when the validate flag is set as `true`' do
+ let(:validate) { true }
+ let(:valid?) { false }
- it 'does not add errors to the report' do
- expect { parse_report }.not_to change { report.errors }.from([])
+ before do
+ allow_next_instance_of(validator_class) do |instance|
+ allow(instance).to receive(:valid?).and_return(valid?)
+ allow(instance).to receive(:errors).and_return(['foo'])
+ end
+
+ allow(parser).to receive_messages(create_scanner: true, create_scan: true)
end
- it 'keeps the execution flow as normal' do
+ it 'instantiates the validator with correct params' do
parse_report
- expect(parser).to have_received(:create_scanner)
- expect(parser).to have_received(:create_scan)
+ expect(validator_class).to have_received(:new).with(report.type, {})
+ end
+
+ context 'when the report data is not valid according to the schema' do
+ it 'adds errors to the report' do
+ expect { parse_report }.to change { report.errors }.from([]).to([{ message: 'foo', type: 'Schema' }])
+ end
+
+ it 'does not try to create report entities' do
+ parse_report
+
+ expect(parser).not_to have_received(:create_scanner)
+ expect(parser).not_to have_received(:create_scan)
+ end
+ end
+
+ context 'when the report data is valid according to the schema' do
+ let(:valid?) { true }
+
+ it 'does not add errors to the report' do
+ expect { parse_report }.not_to change { report.errors }.from([])
+ end
+
+ it 'keeps the execution flow as normal' do
+ parse_report
+
+ expect(parser).to have_received(:create_scanner)
+ expect(parser).to have_received(:create_scan)
+ end
end
end
end
diff --git a/spec/lib/gitlab/ci/parsers/test/junit_spec.rb b/spec/lib/gitlab/ci/parsers/test/junit_spec.rb
index 4ca8f74e57f..82fa11d5f98 100644
--- a/spec/lib/gitlab/ci/parsers/test/junit_spec.rb
+++ b/spec/lib/gitlab/ci/parsers/test/junit_spec.rb
@@ -99,6 +99,19 @@ RSpec.describe Gitlab::Ci::Parsers::Test::Junit do
'Some failure'
end
+ context 'and has failure with no message but has system-err' do
+ let(:testcase_content) do
+ <<-EOF.strip_heredoc
+ <failure></failure>
+ <system-err>Some failure</system-err>
+ EOF
+ end
+
+ it_behaves_like '<testcase> XML parser',
+ ::Gitlab::Ci::Reports::TestCase::STATUS_FAILED,
+ 'Some failure'
+ end
+
context 'and has error' do
let(:testcase_content) { '<error>Some error</error>' }
@@ -107,6 +120,19 @@ RSpec.describe Gitlab::Ci::Parsers::Test::Junit do
'Some error'
end
+ context 'and has error with no message but has system-err' do
+ let(:testcase_content) do
+ <<-EOF.strip_heredoc
+ <error></error>
+ <system-err>Some error</system-err>
+ EOF
+ end
+
+ it_behaves_like '<testcase> XML parser',
+ ::Gitlab::Ci::Reports::TestCase::STATUS_ERROR,
+ 'Some error'
+ end
+
context 'and has skipped' do
let(:testcase_content) { '<skipped/>' }
diff --git a/spec/lib/gitlab/ci/pipeline/chain/create_deployments_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/create_deployments_spec.rb
index 0a592395c3a..375841ce236 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/create_deployments_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/create_deployments_spec.rb
@@ -47,18 +47,6 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::CreateDeployments do
expect(job.deployment).to be_nil
end
end
-
- context 'when create_deployment_in_separate_transaction feature flag is disabled' do
- before do
- stub_feature_flags(create_deployment_in_separate_transaction: false)
- end
-
- it 'does not create a deployment record' do
- expect { subject }.not_to change { Deployment.count }
-
- expect(job.deployment).to be_nil
- end
- end
end
context 'when a pipeline contains a teardown job' do
diff --git a/spec/lib/gitlab/ci/pipeline/chain/ensure_environments_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/ensure_environments_spec.rb
index 253928e1a19..6a7d9b58a05 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/ensure_environments_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/ensure_environments_spec.rb
@@ -57,18 +57,6 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::EnsureEnvironments do
expect(job.persisted_environment).to be_nil
end
end
-
- context 'when create_deployment_in_separate_transaction feature flag is disabled' do
- before do
- stub_feature_flags(create_deployment_in_separate_transaction: false)
- end
-
- it 'does not create any environments' do
- expect { subject }.not_to change { Environment.count }
-
- expect(job.persisted_environment).to be_nil
- end
- end
end
context 'when a pipeline contains a teardown job' do
diff --git a/spec/lib/gitlab/ci/pipeline/chain/ensure_resource_groups_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/ensure_resource_groups_spec.rb
index 87df5a3e21b..571455d6279 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/ensure_resource_groups_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/ensure_resource_groups_spec.rb
@@ -60,18 +60,6 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::EnsureResourceGroups do
expect(job.resource_group).to be_nil
end
end
-
- context 'when create_deployment_in_separate_transaction feature flag is disabled' do
- before do
- stub_feature_flags(create_deployment_in_separate_transaction: false)
- end
-
- it 'does not create any resource groups' do
- expect { subject }.not_to change { Ci::ResourceGroup.count }
-
- expect(job.resource_group).to be_nil
- end
- end
end
context 'when a pipeline does not contain a job that requires a resource group' do
diff --git a/spec/lib/gitlab/ci/pipeline/logger_spec.rb b/spec/lib/gitlab/ci/pipeline/logger_spec.rb
index a488bc184f8..f31361431f2 100644
--- a/spec/lib/gitlab/ci/pipeline/logger_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/logger_spec.rb
@@ -47,13 +47,15 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Logger do
end
def loggable_data(count:, db_count: nil)
- keys = %w[
+ database_name = Ci::ApplicationRecord.connection.pool.db_config.name
+
+ keys = %W[
expensive_operation_duration_s
expensive_operation_db_count
expensive_operation_db_primary_count
expensive_operation_db_primary_duration_s
- expensive_operation_db_main_count
- expensive_operation_db_main_duration_s
+ expensive_operation_db_#{database_name}_count
+ expensive_operation_db_#{database_name}_duration_s
]
data = keys.each.with_object({}) do |key, accumulator|
@@ -75,7 +77,7 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Logger do
end
context 'with a single query' do
- let(:operation) { -> { Project.count } }
+ let(:operation) { -> { Ci::Pipeline.count } }
it { is_expected.to eq(operation.call) }
@@ -201,6 +203,35 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Logger do
expect(commit).to be_truthy
end
end
+
+ context 'when project is not passed and pipeline is not persisted' do
+ let(:project) {}
+ let(:pipeline) { build(:ci_pipeline) }
+
+ let(:loggable_data) do
+ {
+ 'class' => described_class.name.to_s,
+ 'pipeline_persisted' => false,
+ 'pipeline_creation_service_duration_s' => a_kind_of(Numeric),
+ 'pipeline_creation_caller' => 'source',
+ 'pipeline_save_duration_s' => {
+ 'avg' => 60, 'count' => 1, 'max' => 60, 'min' => 60
+ },
+ 'pipeline_creation_duration_s' => {
+ 'avg' => 20, 'count' => 2, 'max' => 30, 'min' => 10
+ }
+ }
+ end
+
+ it 'logs to application.json' do
+ expect(Gitlab::AppJsonLogger)
+ .to receive(:info)
+ .with(a_hash_including(loggable_data))
+ .and_call_original
+
+ expect(commit).to be_truthy
+ end
+ end
end
context 'when the feature flag is disabled' do
diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
index 2f9fcd7caac..49505d397c2 100644
--- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
@@ -411,171 +411,6 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do
describe '#to_resource' do
subject { seed_build.to_resource }
- before do
- stub_feature_flags(create_deployment_in_separate_transaction: false)
- end
-
- context 'when job is Ci::Build' do
- it { is_expected.to be_a(::Ci::Build) }
- it { is_expected.to be_valid }
-
- shared_examples_for 'deployment job' do
- it 'returns a job with deployment' do
- expect { subject }.to change { Environment.count }.by(1)
-
- expect(subject.deployment).not_to be_nil
- expect(subject.deployment.deployable).to eq(subject)
- expect(subject.deployment.environment.name).to eq(expected_environment_name)
- end
- end
-
- shared_examples_for 'non-deployment job' do
- it 'returns a job without deployment' do
- expect(subject.deployment).to be_nil
- end
- end
-
- shared_examples_for 'ensures environment existence' do
- it 'has environment' do
- expect { subject }.to change { Environment.count }.by(1)
-
- expect(subject).to be_has_environment
- expect(subject.environment).to eq(environment_name)
- expect(subject.metadata.expanded_environment_name).to eq(expected_environment_name)
- expect(Environment.exists?(name: expected_environment_name)).to eq(true)
- end
- end
-
- shared_examples_for 'ensures environment inexistence' do
- it 'does not have environment' do
- expect { subject }.not_to change { Environment.count }
-
- expect(subject).not_to be_has_environment
- expect(subject.environment).to be_nil
- expect(subject.metadata&.expanded_environment_name).to be_nil
- expect(Environment.exists?(name: expected_environment_name)).to eq(false)
- end
- end
-
- context 'when job deploys to production' do
- let(:environment_name) { 'production' }
- let(:expected_environment_name) { 'production' }
- let(:attributes) { { name: 'deploy', ref: 'master', environment: 'production' } }
-
- it_behaves_like 'deployment job'
- it_behaves_like 'ensures environment existence'
-
- context 'when create_deployment_in_separate_transaction feature flag is enabled' do
- before do
- stub_feature_flags(create_deployment_in_separate_transaction: true)
- end
-
- it 'does not create any deployments nor environments' do
- expect(subject.deployment).to be_nil
- expect(Environment.count).to eq(0)
- expect(Deployment.count).to eq(0)
- end
- end
-
- context 'when the environment name is invalid' do
- let(:attributes) { { name: 'deploy', ref: 'master', environment: '!!!' } }
-
- it 'fails the job with a failure reason and does not create an environment' do
- expect(subject).to be_failed
- expect(subject).to be_environment_creation_failure
- expect(subject.metadata.expanded_environment_name).to be_nil
- expect(Environment.exists?(name: expected_environment_name)).to eq(false)
- end
- end
- end
-
- context 'when job starts a review app' do
- let(:environment_name) { 'review/$CI_COMMIT_REF_NAME' }
- let(:expected_environment_name) { "review/#{pipeline.ref}" }
-
- let(:attributes) do
- {
- name: 'deploy', ref: 'master', environment: environment_name,
- options: { environment: { name: environment_name } }
- }
- end
-
- it_behaves_like 'deployment job'
- it_behaves_like 'ensures environment existence'
- end
-
- context 'when job stops a review app' do
- let(:environment_name) { 'review/$CI_COMMIT_REF_NAME' }
- let(:expected_environment_name) { "review/#{pipeline.ref}" }
-
- let(:attributes) do
- {
- name: 'deploy', ref: 'master', environment: environment_name,
- options: { environment: { name: environment_name, action: 'stop' } }
- }
- end
-
- it 'returns a job without deployment' do
- expect(subject.deployment).to be_nil
- end
-
- it_behaves_like 'non-deployment job'
- it_behaves_like 'ensures environment existence'
- end
-
- context 'when job belongs to a resource group' do
- let(:resource_group) { 'iOS' }
- let(:attributes) { { name: 'rspec', ref: 'master', resource_group_key: resource_group, environment: 'production' }}
-
- it 'returns a job with resource group' do
- expect(subject.resource_group).not_to be_nil
- expect(subject.resource_group.key).to eq('iOS')
- expect(Ci::ResourceGroup.count).to eq(1)
- end
-
- context 'when create_deployment_in_separate_transaction feature flag is enabled' do
- before do
- stub_feature_flags(create_deployment_in_separate_transaction: true)
- end
-
- it 'does not create any resource groups' do
- expect(subject.resource_group).to be_nil
- expect(Ci::ResourceGroup.count).to eq(0)
- end
- end
-
- context 'when resource group has $CI_ENVIRONMENT_NAME in it' do
- let(:resource_group) { 'test/$CI_ENVIRONMENT_NAME' }
-
- it 'expands environment name' do
- expect(subject.resource_group.key).to eq('test/production')
- end
- end
- end
- end
-
- context 'when job is a bridge' do
- let(:base_attributes) do
- {
- name: 'rspec', ref: 'master', options: { trigger: 'my/project' }, scheduling_type: :stage
- }
- end
-
- let(:attributes) { base_attributes }
-
- it { is_expected.to be_a(::Ci::Bridge) }
- it { is_expected.to be_valid }
-
- context 'when job belongs to a resource group' do
- let(:attributes) { base_attributes.merge(resource_group_key: 'iOS') }
-
- it 'returns a job with resource group' do
- expect(subject.resource_group).not_to be_nil
- expect(subject.resource_group.key).to eq('iOS')
- end
- end
- end
-
it 'memoizes a resource object' do
expect(subject.object_id).to eq seed_build.to_resource.object_id
end
diff --git a/spec/lib/gitlab/ci/reports/codequality_reports_spec.rb b/spec/lib/gitlab/ci/reports/codequality_reports_spec.rb
index 3b0eaffc54e..f4b47893805 100644
--- a/spec/lib/gitlab/ci/reports/codequality_reports_spec.rb
+++ b/spec/lib/gitlab/ci/reports/codequality_reports_spec.rb
@@ -85,6 +85,9 @@ RSpec.describe Gitlab::Ci::Reports::CodequalityReports do
let(:info) { build(:codequality_degradation, :info) }
let(:major_2) { build(:codequality_degradation, :major) }
let(:critical) { build(:codequality_degradation, :critical) }
+ let(:uppercase_major) { build(:codequality_degradation, severity: 'MAJOR') }
+ let(:unknown) { build(:codequality_degradation, severity: 'unknown') }
+
let(:codequality_report) { described_class.new }
before do
@@ -94,6 +97,7 @@ RSpec.describe Gitlab::Ci::Reports::CodequalityReports do
codequality_report.add_degradation(major_2)
codequality_report.add_degradation(info)
codequality_report.add_degradation(critical)
+ codequality_report.add_degradation(unknown)
codequality_report.sort_degradations!
end
@@ -105,8 +109,30 @@ RSpec.describe Gitlab::Ci::Reports::CodequalityReports do
major,
major_2,
minor,
- info
+ info,
+ unknown
])
end
+
+ context 'with non-existence and uppercase severities' do
+ let(:other_report) { described_class.new }
+ let(:non_existent) { build(:codequality_degradation, severity: 'non-existent') }
+
+ before do
+ other_report.add_degradation(blocker)
+ other_report.add_degradation(uppercase_major)
+ other_report.add_degradation(minor)
+ other_report.add_degradation(non_existent)
+ end
+
+ it 'sorts unknown last' do
+ expect(other_report.degradations.values).to eq([
+ blocker,
+ uppercase_major,
+ minor,
+ non_existent
+ ])
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/ci/reports/security/finding_key_spec.rb b/spec/lib/gitlab/ci/reports/security/finding_key_spec.rb
index 784c1183320..2320f011cf5 100644
--- a/spec/lib/gitlab/ci/reports/security/finding_key_spec.rb
+++ b/spec/lib/gitlab/ci/reports/security/finding_key_spec.rb
@@ -6,36 +6,47 @@ RSpec.describe Gitlab::Ci::Reports::Security::FindingKey do
using RSpec::Parameterized::TableSyntax
describe '#==' do
- where(:location_fp_1, :location_fp_2, :identifier_fp_1, :identifier_fp_2, :equals?) do
- nil | 'different location fp' | 'identifier fp' | 'different identifier fp' | false
- 'location fp' | nil | 'identifier fp' | 'different identifier fp' | false
- 'location fp' | 'different location fp' | nil | 'different identifier fp' | false
- 'location fp' | 'different location fp' | 'identifier fp' | nil | false
- nil | nil | 'identifier fp' | 'identifier fp' | false
- 'location fp' | 'location fp' | nil | nil | false
- nil | nil | nil | nil | false
- 'location fp' | 'different location fp' | 'identifier fp' | 'different identifier fp' | false
- 'location fp' | 'different location fp' | 'identifier fp' | 'identifier fp' | false
- 'location fp' | 'location fp' | 'identifier fp' | 'different identifier fp' | false
- 'location fp' | 'location fp' | 'identifier fp' | 'identifier fp' | true
- end
-
- with_them do
- let(:finding_key_1) do
- build(:ci_reports_security_finding_key,
- location_fingerprint: location_fp_1,
- identifier_fingerprint: identifier_fp_1)
+ context 'when the comparison is done between FindingKey instances' do
+ where(:location_fp_1, :location_fp_2, :identifier_fp_1, :identifier_fp_2, :equals?) do
+ nil | 'different location fp' | 'identifier fp' | 'different identifier fp' | false
+ 'location fp' | nil | 'identifier fp' | 'different identifier fp' | false
+ 'location fp' | 'different location fp' | nil | 'different identifier fp' | false
+ 'location fp' | 'different location fp' | 'identifier fp' | nil | false
+ nil | nil | 'identifier fp' | 'identifier fp' | false
+ 'location fp' | 'location fp' | nil | nil | false
+ nil | nil | nil | nil | false
+ 'location fp' | 'different location fp' | 'identifier fp' | 'different identifier fp' | false
+ 'location fp' | 'different location fp' | 'identifier fp' | 'identifier fp' | false
+ 'location fp' | 'location fp' | 'identifier fp' | 'different identifier fp' | false
+ 'location fp' | 'location fp' | 'identifier fp' | 'identifier fp' | true
end
- let(:finding_key_2) do
- build(:ci_reports_security_finding_key,
- location_fingerprint: location_fp_2,
- identifier_fingerprint: identifier_fp_2)
+ with_them do
+ let(:finding_key_1) do
+ build(:ci_reports_security_finding_key,
+ location_fingerprint: location_fp_1,
+ identifier_fingerprint: identifier_fp_1)
+ end
+
+ let(:finding_key_2) do
+ build(:ci_reports_security_finding_key,
+ location_fingerprint: location_fp_2,
+ identifier_fingerprint: identifier_fp_2)
+ end
+
+ subject { finding_key_1 == finding_key_2 }
+
+ it { is_expected.to be(equals?) }
end
+ end
+
+ context 'when the comparison is not done between FindingKey instances' do
+ let(:finding_key) { build(:ci_reports_security_finding_key) }
+ let(:uuid) { SecureRandom.uuid }
- subject { finding_key_1 == finding_key_2 }
+ subject { finding_key == uuid }
- it { is_expected.to be(equals?) }
+ it { is_expected.to be_falsey }
end
end
end
diff --git a/spec/lib/gitlab/ci/templates/5_minute_production_app_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/5_minute_production_app_ci_yaml_spec.rb
index f8df2266689..8204b104832 100644
--- a/spec/lib/gitlab/ci/templates/5_minute_production_app_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/5_minute_production_app_ci_yaml_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe '5-Minute-Production-App.gitlab-ci.yml' do
describe 'the created pipeline' do
let_it_be(:project) { create(:project, :auto_devops, :custom_repo, files: { 'README.md' => '' }) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:default_branch) { 'master' }
let(:pipeline_branch) { default_branch }
let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) }
diff --git a/spec/lib/gitlab/ci/templates/AWS/deploy_ecs_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/AWS/deploy_ecs_gitlab_ci_yaml_spec.rb
index ca6f6872f89..27de8324206 100644
--- a/spec/lib/gitlab/ci/templates/AWS/deploy_ecs_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/AWS/deploy_ecs_gitlab_ci_yaml_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe 'Deploy-ECS.gitlab-ci.yml' do
let(:default_branch) { project.default_branch_or_main }
let(:pipeline_branch) { default_branch }
let(:project) { create(:project, :auto_devops, :custom_repo, files: { 'README.md' => '' }) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) }
let(:pipeline) { service.execute!(:push).payload }
let(:build_names) { pipeline.builds.pluck(:name) }
diff --git a/spec/lib/gitlab/ci/templates/Jobs/build_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/build_gitlab_ci_yaml_spec.rb
index bd701aec8fc..21052f03cb8 100644
--- a/spec/lib/gitlab/ci/templates/Jobs/build_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/Jobs/build_gitlab_ci_yaml_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe 'Jobs/Build.gitlab-ci.yml' do
describe 'the created pipeline' do
let_it_be(:project) { create(:project, :repository) }
- let_it_be(:user) { project.owner }
+ let_it_be(:user) { project.first_owner }
let(:default_branch) { 'master' }
let(:pipeline_ref) { default_branch }
diff --git a/spec/lib/gitlab/ci/templates/Jobs/code_quality_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/code_quality_gitlab_ci_yaml_spec.rb
index 64243f2d205..d88d9782021 100644
--- a/spec/lib/gitlab/ci/templates/Jobs/code_quality_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/Jobs/code_quality_gitlab_ci_yaml_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe 'Jobs/Code-Quality.gitlab-ci.yml' do
describe 'the created pipeline' do
let_it_be(:project) { create(:project, :repository) }
- let_it_be(:user) { project.owner }
+ let_it_be(:user) { project.first_owner }
let(:default_branch) { 'master' }
let(:pipeline_ref) { default_branch }
diff --git a/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb
index 789f694b4b4..b657f73fa77 100644
--- a/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb
@@ -29,7 +29,7 @@ RSpec.describe 'Jobs/Deploy.gitlab-ci.yml' do
describe 'the created pipeline' do
let_it_be(:project, refind: true) { create(:project, :repository) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:default_branch) { 'master' }
let(:pipeline_ref) { default_branch }
let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref) }
@@ -66,6 +66,11 @@ RSpec.describe 'Jobs/Deploy.gitlab-ci.yml' do
expect(build_names).not_to include('review')
end
+ it 'when CI_DEPLOY_FREEZE is present' do
+ create(:ci_variable, project: project, key: 'CI_DEPLOY_FREEZE', value: 'true')
+ expect(build_names).to eq %w(placeholder)
+ end
+
it 'when CANARY_ENABLED' do
create(:ci_variable, project: project, key: 'CANARY_ENABLED', value: 'true')
diff --git a/spec/lib/gitlab/ci/templates/Jobs/sast_iac_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/sast_iac_gitlab_ci_yaml_spec.rb
index b9256ece78b..0f97bc06a4e 100644
--- a/spec/lib/gitlab/ci/templates/Jobs/sast_iac_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/Jobs/sast_iac_gitlab_ci_yaml_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe 'Jobs/SAST-IaC.latest.gitlab-ci.yml' do
describe 'the created pipeline' do
let_it_be(:project) { create(:project, :repository) }
- let_it_be(:user) { project.owner }
+ let_it_be(:user) { project.first_owner }
let(:default_branch) { 'main' }
let(:pipeline_ref) { default_branch }
diff --git a/spec/lib/gitlab/ci/templates/Jobs/test_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/test_gitlab_ci_yaml_spec.rb
index db9d7496251..a92a8397e96 100644
--- a/spec/lib/gitlab/ci/templates/Jobs/test_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/Jobs/test_gitlab_ci_yaml_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe 'Jobs/Test.gitlab-ci.yml' do
describe 'the created pipeline' do
let_it_be(:project) { create(:project, :repository) }
- let_it_be(:user) { project.owner }
+ let_it_be(:user) { project.first_owner }
let(:default_branch) { 'master' }
let(:pipeline_ref) { default_branch }
diff --git a/spec/lib/gitlab/ci/templates/Terraform/base_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Terraform/base_gitlab_ci_yaml_spec.rb
index 4685d843ce0..5e9224cebd9 100644
--- a/spec/lib/gitlab/ci/templates/Terraform/base_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/Terraform/base_gitlab_ci_yaml_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe 'Terraform/Base.gitlab-ci.yml' do
let(:default_branch) { 'master' }
let(:pipeline_branch) { default_branch }
let(:project) { create(:project, :custom_repo, files: { 'README.md' => '' }) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) }
let(:pipeline) { service.execute!(:push).payload }
let(:build_names) { pipeline.builds.pluck(:name) }
diff --git a/spec/lib/gitlab/ci/templates/Terraform/base_latest_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Terraform/base_latest_gitlab_ci_yaml_spec.rb
index e35f2eabe8e..0ab81f97f20 100644
--- a/spec/lib/gitlab/ci/templates/Terraform/base_latest_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/Terraform/base_latest_gitlab_ci_yaml_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe 'Terraform/Base.latest.gitlab-ci.yml' do
let(:default_branch) { 'master' }
let(:pipeline_branch) { default_branch }
let(:project) { create(:project, :custom_repo, files: { 'README.md' => '' }) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) }
let(:pipeline) { service.execute!(:push).payload }
let(:build_names) { pipeline.builds.pluck(:name) }
diff --git a/spec/lib/gitlab/ci/templates/Verify/load_performance_testing_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Verify/load_performance_testing_gitlab_ci_yaml_spec.rb
index 004261bc617..d6c7cd32f79 100644
--- a/spec/lib/gitlab/ci/templates/Verify/load_performance_testing_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/Verify/load_performance_testing_gitlab_ci_yaml_spec.rb
@@ -20,7 +20,7 @@ RSpec.describe 'Verify/Load-Performance-Testing.gitlab-ci.yml' do
describe 'the created pipeline' do
let(:project) { create(:project, :repository) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:default_branch) { 'master' }
let(:pipeline_ref) { default_branch }
diff --git a/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb
index 64ef6ecd7f8..6a4be1fa072 100644
--- a/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe 'Auto-DevOps.gitlab-ci.yml' do
describe 'the created pipeline' do
let(:pipeline_branch) { default_branch }
let(:project) { create(:project, :auto_devops, :custom_repo, files: { 'README.md' => '' }) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) }
let(:pipeline) { service.execute!(:push).payload }
let(:build_names) { pipeline.builds.pluck(:name) }
@@ -276,7 +276,7 @@ RSpec.describe 'Auto-DevOps.gitlab-ci.yml' do
with_them do
let(:project) { create(:project, :custom_repo, files: files) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:service) { Ci::CreatePipelineService.new(project, user, ref: default_branch ) }
let(:pipeline) { service.execute(:push).payload }
let(:build_names) { pipeline.builds.pluck(:name) }
diff --git a/spec/lib/gitlab/ci/templates/flutter_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/flutter_gitlab_ci_yaml_spec.rb
index 3d97b47473d..de94eec09fe 100644
--- a/spec/lib/gitlab/ci/templates/flutter_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/flutter_gitlab_ci_yaml_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe 'Flutter.gitlab-ci.yml' do
describe 'the created pipeline' do
let(:pipeline_branch) { 'master' }
let(:project) { create(:project, :custom_repo, files: { 'README.md' => '' }) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) }
let(:pipeline) { service.execute!(:push).payload }
let(:build_names) { pipeline.builds.pluck(:name) }
diff --git a/spec/lib/gitlab/ci/templates/kaniko_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/kaniko_gitlab_ci_yaml_spec.rb
index c7dbbea4622..ebf52e6d65a 100644
--- a/spec/lib/gitlab/ci/templates/kaniko_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/kaniko_gitlab_ci_yaml_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe 'Kaniko.gitlab-ci.yml' do
describe 'the created pipeline' do
let(:pipeline_branch) { 'master' }
let(:project) { create(:project, :custom_repo, files: { 'Dockerfile' => 'FROM alpine:latest' }) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) }
let(:pipeline) { service.execute!(:push).payload }
let(:build_names) { pipeline.builds.pluck(:name) }
diff --git a/spec/lib/gitlab/ci/templates/npm_spec.rb b/spec/lib/gitlab/ci/templates/npm_spec.rb
index ea954690133..d86a3a67823 100644
--- a/spec/lib/gitlab/ci/templates/npm_spec.rb
+++ b/spec/lib/gitlab/ci/templates/npm_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe 'npm.gitlab-ci.yml' do
let(:repo_files) { { 'package.json' => '{}', 'README.md' => '' } }
let(:modified_files) { %w[package.json] }
let(:project) { create(:project, :custom_repo, files: repo_files) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:pipeline_branch) { project.default_branch }
let(:pipeline_tag) { 'v1.2.1' }
let(:pipeline_ref) { pipeline_branch }
diff --git a/spec/lib/gitlab/ci/templates/terraform_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/terraform_gitlab_ci_yaml_spec.rb
index 936cd6ac8aa..346ab9f7af7 100644
--- a/spec/lib/gitlab/ci/templates/terraform_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/terraform_gitlab_ci_yaml_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe 'Terraform.gitlab-ci.yml' do
let(:default_branch) { project.default_branch_or_main }
let(:pipeline_branch) { default_branch }
let(:project) { create(:project, :custom_repo, files: { 'README.md' => '' }) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) }
let(:pipeline) { service.execute!(:push).payload }
let(:build_names) { pipeline.builds.pluck(:name) }
diff --git a/spec/lib/gitlab/ci/templates/terraform_latest_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/terraform_latest_gitlab_ci_yaml_spec.rb
index fd5d5d6af7f..6c06403adff 100644
--- a/spec/lib/gitlab/ci/templates/terraform_latest_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/terraform_latest_gitlab_ci_yaml_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe 'Terraform.latest.gitlab-ci.yml' do
let(:default_branch) { project.default_branch_or_main }
let(:pipeline_branch) { default_branch }
let(:project) { create(:project, :custom_repo, files: { 'README.md' => '' }) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) }
let(:pipeline) { service.execute!(:push).payload }
let(:build_names) { pipeline.builds.pluck(:name) }
diff --git a/spec/lib/gitlab/ci/variables/builder/instance_spec.rb b/spec/lib/gitlab/ci/variables/builder/instance_spec.rb
new file mode 100644
index 00000000000..7abda2bd615
--- /dev/null
+++ b/spec/lib/gitlab/ci/variables/builder/instance_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Variables::Builder::Instance do
+ let_it_be(:variable) { create(:ci_instance_variable, protected: false) }
+ let_it_be(:protected_variable) { create(:ci_instance_variable, protected: true) }
+
+ let(:builder) { described_class.new }
+
+ describe '#secret_variables' do
+ let(:variable_item) { item(variable) }
+ let(:protected_variable_item) { item(protected_variable) }
+
+ subject do
+ builder.secret_variables(protected_ref: protected_ref)
+ end
+
+ context 'when the ref is protected' do
+ let(:protected_ref) { true }
+
+ it 'contains all the variables' do
+ is_expected.to contain_exactly(variable_item, protected_variable_item)
+ end
+ end
+
+ context 'when the ref is not protected' do
+ let(:protected_ref) { false }
+
+ it 'contains only unprotected variables' do
+ is_expected.to contain_exactly(variable_item)
+ end
+ end
+ end
+
+ def item(variable)
+ Gitlab::Ci::Variables::Collection::Item.fabricate(variable)
+ end
+end
diff --git a/spec/lib/gitlab/ci/variables/builder/project_spec.rb b/spec/lib/gitlab/ci/variables/builder/project_spec.rb
new file mode 100644
index 00000000000..b64b6ea98e2
--- /dev/null
+++ b/spec/lib/gitlab/ci/variables/builder/project_spec.rb
@@ -0,0 +1,149 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Variables::Builder::Project do
+ let_it_be(:project) { create(:project, :repository) }
+
+ let(:builder) { described_class.new(project) }
+
+ describe '#secret_variables' do
+ let(:environment) { '*' }
+ let(:protected_ref) { false }
+
+ let_it_be(:variable) do
+ create(:ci_variable,
+ value: 'secret',
+ project: project)
+ end
+
+ let_it_be(:protected_variable) do
+ create(:ci_variable, :protected,
+ value: 'protected',
+ project: project)
+ end
+
+ let(:variable_item) { item(variable) }
+ let(:protected_variable_item) { item(protected_variable) }
+
+ subject do
+ builder.secret_variables(
+ environment: environment,
+ protected_ref: protected_ref)
+ end
+
+ context 'when the ref is protected' do
+ let(:protected_ref) { true }
+
+ it 'contains all the variables' do
+ is_expected.to contain_exactly(variable_item, protected_variable_item)
+ end
+ end
+
+ context 'when the ref is not protected' do
+ let(:protected_ref) { false }
+
+ it 'contains only the unprotected variables' do
+ is_expected.to contain_exactly(variable_item)
+ end
+ end
+
+ context 'when environment name is specified' do
+ let(:environment) { 'review/name' }
+
+ before do
+ Ci::Variable.update_all(environment_scope: environment_scope)
+ end
+
+ context 'when environment scope is exactly matched' do
+ let(:environment_scope) { 'review/name' }
+
+ it { is_expected.to contain_exactly(variable_item) }
+ end
+
+ context 'when environment scope is matched by wildcard' do
+ let(:environment_scope) { 'review/*' }
+
+ it { is_expected.to contain_exactly(variable_item) }
+ end
+
+ context 'when environment scope does not match' do
+ let(:environment_scope) { 'review/*/special' }
+
+ it { is_expected.not_to contain_exactly(variable_item) }
+ end
+
+ context 'when environment scope has _' do
+ let(:environment_scope) { '*_*' }
+
+ it 'does not treat it as wildcard' do
+ is_expected.not_to contain_exactly(variable_item)
+ end
+ end
+
+ context 'when environment name contains underscore' do
+ let(:environment) { 'foo_bar/test' }
+ let(:environment_scope) { 'foo_bar/*' }
+
+ it 'matches literally for _' do
+ is_expected.to contain_exactly(variable_item)
+ end
+ end
+
+ # The environment name and scope cannot have % at the moment,
+ # but we're considering relaxing it and we should also make sure
+ # it doesn't break in case some data sneaked in somehow as we're
+ # not checking this integrity in database level.
+ context 'when environment scope has %' do
+ let(:environment_scope) { '*%*' }
+
+ it 'does not treat it as wildcard' do
+ is_expected.not_to contain_exactly(variable_item)
+ end
+ end
+
+ context 'when environment name contains a percent' do
+ let(:environment) { 'foo%bar/test' }
+ let(:environment_scope) { 'foo%bar/*' }
+
+ it 'matches literally for _' do
+ is_expected.to contain_exactly(variable_item)
+ end
+ end
+ end
+
+ context 'when variables with the same name have different environment scopes' do
+ let(:environment) { 'review/name' }
+
+ let_it_be(:partially_matched_variable) do
+ create(:ci_variable,
+ key: variable.key,
+ value: 'partial',
+ environment_scope: 'review/*',
+ project: project)
+ end
+
+ let_it_be(:perfectly_matched_variable) do
+ create(:ci_variable,
+ key: variable.key,
+ value: 'prefect',
+ environment_scope: 'review/name',
+ project: project)
+ end
+
+ it 'puts variables matching environment scope more in the end' do
+ variables_collection = Gitlab::Ci::Variables::Collection.new([
+ variable,
+ partially_matched_variable,
+ perfectly_matched_variable
+ ]).to_runner_variables
+
+ expect(subject.to_runner_variables).to eq(variables_collection)
+ end
+ end
+ end
+
+ def item(variable)
+ Gitlab::Ci::Variables::Collection::Item.fabricate(variable)
+ end
+end
diff --git a/spec/lib/gitlab/ci/variables/builder_spec.rb b/spec/lib/gitlab/ci/variables/builder_spec.rb
index 8a87cbe45c1..6e144d62ac0 100644
--- a/spec/lib/gitlab/ci/variables/builder_spec.rb
+++ b/spec/lib/gitlab/ci/variables/builder_spec.rb
@@ -3,10 +3,11 @@
require 'spec_helper'
RSpec.describe Gitlab::Ci::Variables::Builder do
- let_it_be(:project) { create(:project, :repository) }
- let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
- let_it_be(:user) { project.owner }
- let_it_be(:job) do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, :repository, namespace: group) }
+ let_it_be_with_reload(:pipeline) { create(:ci_pipeline, project: project) }
+ let_it_be(:user) { create(:user) }
+ let_it_be_with_reload(:job) do
create(:ci_build,
pipeline: pipeline,
user: user,
@@ -153,7 +154,7 @@ RSpec.describe Gitlab::Ci::Variables::Builder do
before do
allow(builder).to receive(:predefined_variables) { [var('A', 1), var('B', 1)] }
- allow(project).to receive(:predefined_variables) { [var('B', 2), var('C', 2)] }
+ allow(pipeline.project).to receive(:predefined_variables) { [var('B', 2), var('C', 2)] }
allow(pipeline).to receive(:predefined_variables) { [var('C', 3), var('D', 3)] }
allow(job).to receive(:runner) { double(predefined_variables: [var('D', 4), var('E', 4)]) }
allow(builder).to receive(:kubernetes_variables) { [var('E', 5), var('F', 5)] }
@@ -201,4 +202,240 @@ RSpec.describe Gitlab::Ci::Variables::Builder do
end
end
end
+
+ describe '#user_variables' do
+ context 'with user' do
+ subject { builder.user_variables(user).to_hash }
+
+ let(:expected_variables) do
+ {
+ 'GITLAB_USER_EMAIL' => user.email,
+ 'GITLAB_USER_ID' => user.id.to_s,
+ 'GITLAB_USER_LOGIN' => user.username,
+ 'GITLAB_USER_NAME' => user.name
+ }
+ end
+
+ it { is_expected.to eq(expected_variables) }
+ end
+
+ context 'without user' do
+ subject { builder.user_variables(nil).to_hash }
+
+ it { is_expected.to be_empty }
+ end
+ end
+
+ describe '#kubernetes_variables' do
+ let(:service) { double(execute: template) }
+ let(:template) { double(to_yaml: 'example-kubeconfig', valid?: template_valid) }
+ let(:template_valid) { true }
+
+ subject { builder.kubernetes_variables(job) }
+
+ before do
+ allow(Ci::GenerateKubeconfigService).to receive(:new).with(job).and_return(service)
+ end
+
+ it { is_expected.to include(key: 'KUBECONFIG', value: 'example-kubeconfig', public: false, file: true) }
+
+ context 'generated config is invalid' do
+ let(:template_valid) { false }
+
+ it { is_expected.not_to include(key: 'KUBECONFIG', value: 'example-kubeconfig', public: false, file: true) }
+ end
+ end
+
+ describe '#deployment_variables' do
+ let(:environment) { 'production' }
+ let(:kubernetes_namespace) { 'namespace' }
+ let(:project_variables) { double }
+
+ subject { builder.deployment_variables(environment: environment, job: job) }
+
+ before do
+ allow(job).to receive(:expanded_kubernetes_namespace)
+ .and_return(kubernetes_namespace)
+
+ allow(project).to receive(:deployment_variables)
+ .with(environment: environment, kubernetes_namespace: kubernetes_namespace)
+ .and_return(project_variables)
+ end
+
+ context 'environment is nil' do
+ let(:environment) { nil }
+
+ it { is_expected.to be_empty }
+ end
+ end
+
+ shared_examples "secret CI variables" do
+ context 'when ref is branch' do
+ context 'when ref is protected' do
+ before do
+ create(:protected_branch, :developers_can_merge, name: job.ref, project: project)
+ end
+
+ it { is_expected.to contain_exactly(protected_variable_item, unprotected_variable_item) }
+ end
+
+ context 'when ref is not protected' do
+ it { is_expected.to contain_exactly(unprotected_variable_item) }
+ end
+ end
+
+ context 'when ref is tag' do
+ before do
+ job.update!(ref: 'v1.1.0', tag: true)
+ pipeline.update!(ref: 'v1.1.0', tag: true)
+ end
+
+ context 'when ref is protected' do
+ before do
+ create(:protected_tag, project: project, name: 'v*')
+ end
+
+ it { is_expected.to contain_exactly(protected_variable_item, unprotected_variable_item) }
+ end
+
+ context 'when ref is not protected' do
+ it { is_expected.to contain_exactly(unprotected_variable_item) }
+ end
+ end
+
+ context 'when ref is merge request' do
+ let_it_be(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline, source_project: project) }
+ let_it_be(:pipeline) { merge_request.pipelines_for_merge_request.first }
+ let_it_be(:job) { create(:ci_build, ref: merge_request.source_branch, tag: false, pipeline: pipeline) }
+
+ context 'when ref is protected' do
+ before do
+ create(:protected_branch, :developers_can_merge, name: merge_request.source_branch, project: project)
+ end
+
+ it 'does not return protected variables as it is not supported for merge request pipelines' do
+ is_expected.to contain_exactly(unprotected_variable_item)
+ end
+ end
+
+ context 'when ref is not protected' do
+ it { is_expected.to contain_exactly(unprotected_variable_item) }
+ end
+ end
+ end
+
+ describe '#secret_instance_variables' do
+ subject { builder.secret_instance_variables }
+
+ let_it_be(:protected_variable) { create(:ci_instance_variable, protected: true) }
+ let_it_be(:unprotected_variable) { create(:ci_instance_variable, protected: false) }
+
+ let(:protected_variable_item) { Gitlab::Ci::Variables::Collection::Item.fabricate(protected_variable) }
+ let(:unprotected_variable_item) { Gitlab::Ci::Variables::Collection::Item.fabricate(unprotected_variable) }
+
+ include_examples "secret CI variables"
+ end
+
+ describe '#secret_group_variables' do
+ subject { builder.secret_group_variables(ref: job.git_ref, environment: job.expanded_environment_name) }
+
+ let_it_be(:protected_variable) { create(:ci_group_variable, protected: true, group: group) }
+ let_it_be(:unprotected_variable) { create(:ci_group_variable, protected: false, group: group) }
+
+ let(:protected_variable_item) { protected_variable }
+ let(:unprotected_variable_item) { unprotected_variable }
+
+ include_examples "secret CI variables"
+ end
+
+ describe '#secret_project_variables' do
+ let_it_be(:protected_variable) { create(:ci_variable, protected: true, project: project) }
+ let_it_be(:unprotected_variable) { create(:ci_variable, protected: false, project: project) }
+
+ let(:ref) { job.git_ref }
+ let(:environment) { job.expanded_environment_name }
+
+ subject { builder.secret_project_variables(ref: ref, environment: environment) }
+
+ context 'with ci_variables_builder_memoize_secret_variables disabled' do
+ before do
+ stub_feature_flags(ci_variables_builder_memoize_secret_variables: false)
+ end
+
+ let(:protected_variable_item) { protected_variable }
+ let(:unprotected_variable_item) { unprotected_variable }
+
+ include_examples "secret CI variables"
+ end
+
+ context 'with ci_variables_builder_memoize_secret_variables enabled' do
+ before do
+ stub_feature_flags(ci_variables_builder_memoize_secret_variables: true)
+ end
+
+ let(:protected_variable_item) { Gitlab::Ci::Variables::Collection::Item.fabricate(protected_variable) }
+ let(:unprotected_variable_item) { Gitlab::Ci::Variables::Collection::Item.fabricate(unprotected_variable) }
+
+ include_examples "secret CI variables"
+
+ context 'variables memoization' do
+ let_it_be(:scoped_variable) { create(:ci_variable, project: project, environment_scope: 'scoped') }
+
+ let(:scoped_variable_item) { Gitlab::Ci::Variables::Collection::Item.fabricate(scoped_variable) }
+
+ context 'with protected environments' do
+ it 'memoizes the result by environment' do
+ expect(pipeline.project)
+ .to receive(:protected_for?)
+ .with(pipeline.jobs_git_ref)
+ .once.and_return(true)
+
+ expect_next_instance_of(described_class::Project) do |project_variables_builder|
+ expect(project_variables_builder)
+ .to receive(:secret_variables)
+ .with(environment: 'production', protected_ref: true)
+ .once
+ .and_call_original
+ end
+
+ 2.times do
+ expect(builder.secret_project_variables(ref: ref, environment: 'production'))
+ .to contain_exactly(unprotected_variable_item, protected_variable_item)
+ end
+ end
+ end
+
+ context 'with unprotected environments' do
+ it 'memoizes the result by environment' do
+ expect(pipeline.project)
+ .to receive(:protected_for?)
+ .with(pipeline.jobs_git_ref)
+ .once.and_return(false)
+
+ expect_next_instance_of(described_class::Project) do |project_variables_builder|
+ expect(project_variables_builder)
+ .to receive(:secret_variables)
+ .with(environment: nil, protected_ref: false)
+ .once
+ .and_call_original
+
+ expect(project_variables_builder)
+ .to receive(:secret_variables)
+ .with(environment: 'scoped', protected_ref: false)
+ .once
+ .and_call_original
+ end
+
+ 2.times do
+ expect(builder.secret_project_variables(ref: 'other', environment: nil))
+ .to contain_exactly(unprotected_variable_item)
+
+ expect(builder.secret_project_variables(ref: 'other', environment: 'scoped'))
+ .to contain_exactly(unprotected_variable_item, scoped_variable_item)
+ end
+ end
+ end
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index 20af84ce648..5f46607b042 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -9,6 +9,10 @@ module Gitlab
subject { described_class.new(config, user: nil).execute }
+ before do
+ stub_feature_flags(allow_unsafe_ruby_regexp: false)
+ end
+
shared_examples 'returns errors' do |error_message|
it 'adds a message when an error is encountered' do
expect(subject.errors).to include(error_message)
@@ -609,13 +613,13 @@ module Gitlab
context 'when it is an array of integers' do
let(:only) { [1, 1] }
- it_behaves_like 'returns errors', 'jobs:rspec:only config should be an array of strings or regexps'
+ it_behaves_like 'returns errors', 'jobs:rspec:only config should be an array of strings or regular expressions using re2 syntax'
end
context 'when it is invalid regex' do
let(:only) { ["/*invalid/"] }
- it_behaves_like 'returns errors', 'jobs:rspec:only config should be an array of strings or regexps'
+ it_behaves_like 'returns errors', 'jobs:rspec:only config should be an array of strings or regular expressions using re2 syntax'
end
end
@@ -633,13 +637,13 @@ module Gitlab
context 'when it is an array of integers' do
let(:except) { [1, 1] }
- it_behaves_like 'returns errors', 'jobs:rspec:except config should be an array of strings or regexps'
+ it_behaves_like 'returns errors', 'jobs:rspec:except config should be an array of strings or regular expressions using re2 syntax'
end
context 'when it is invalid regex' do
let(:except) { ["/*invalid/"] }
- it_behaves_like 'returns errors', 'jobs:rspec:except config should be an array of strings or regexps'
+ it_behaves_like 'returns errors', 'jobs:rspec:except config should be an array of strings or regular expressions using re2 syntax'
end
end
end
@@ -710,16 +714,16 @@ module Gitlab
end
end
- context 'when script is array of arrays of strings' do
+ context 'when script is nested arrays of strings' do
let(:config) do
{
- before_script: [["global script", "echo 1"], ["ls"], "pwd"],
+ before_script: [[["global script"], "echo 1"], "echo 2", ["ls"], "pwd"],
test: { script: ["script"] }
}
end
it "return commands with scripts concatenated" do
- expect(subject[:options][:before_script]).to eq(["global script", "echo 1", "ls", "pwd"])
+ expect(subject[:options][:before_script]).to eq(["global script", "echo 1", "echo 2", "ls", "pwd"])
end
end
end
@@ -737,15 +741,15 @@ module Gitlab
end
end
- context 'when script is array of arrays of strings' do
+ context 'when script is nested arrays of strings' do
let(:config) do
{
- test: { script: [["script"], ["echo 1"], "ls"] }
+ test: { script: [[["script"], "echo 1", "echo 2"], "ls"] }
}
end
it "return commands with scripts concatenated" do
- expect(subject[:options][:script]).to eq(["script", "echo 1", "ls"])
+ expect(subject[:options][:script]).to eq(["script", "echo 1", "echo 2", "ls"])
end
end
end
@@ -790,16 +794,16 @@ module Gitlab
end
end
- context 'when script is array of arrays of strings' do
+ context 'when script is nested arrays of strings' do
let(:config) do
{
- after_script: [["global script", "echo 1"], ["ls"], "pwd"],
+ after_script: [[["global script"], "echo 1"], "echo 2", ["ls"], "pwd"],
test: { script: ["script"] }
}
end
it "return after_script in options" do
- expect(subject[:options][:after_script]).to eq(["global script", "echo 1", "ls", "pwd"])
+ expect(subject[:options][:after_script]).to eq(["global script", "echo 1", "echo 2", "ls", "pwd"])
end
end
end
@@ -2469,40 +2473,16 @@ module Gitlab
it_behaves_like 'returns errors', 'jobs:rspec:tags config should be an array of strings'
end
- context 'returns errors if before_script parameter is invalid' do
- let(:config) { YAML.dump({ before_script: "bundle update", rspec: { script: "test" } }) }
-
- it_behaves_like 'returns errors', 'before_script config should be an array containing strings and arrays of strings'
- end
-
context 'returns errors if job before_script parameter is not an array of strings' do
let(:config) { YAML.dump({ rspec: { script: "test", before_script: [10, "test"] } }) }
- it_behaves_like 'returns errors', 'jobs:rspec:before_script config should be an array containing strings and arrays of strings'
- end
-
- context 'returns errors if job before_script parameter is multi-level nested array of strings' do
- let(:config) { YAML.dump({ rspec: { script: "test", before_script: [["ls", ["pwd"]], "test"] } }) }
-
- it_behaves_like 'returns errors', 'jobs:rspec:before_script config should be an array containing strings and arrays of strings'
- end
-
- context 'returns errors if after_script parameter is invalid' do
- let(:config) { YAML.dump({ after_script: "bundle update", rspec: { script: "test" } }) }
-
- it_behaves_like 'returns errors', 'after_script config should be an array containing strings and arrays of strings'
+ it_behaves_like 'returns errors', 'jobs:rspec:before_script config should be a string or a nested array of strings up to 10 levels deep'
end
context 'returns errors if job after_script parameter is not an array of strings' do
let(:config) { YAML.dump({ rspec: { script: "test", after_script: [10, "test"] } }) }
- it_behaves_like 'returns errors', 'jobs:rspec:after_script config should be an array containing strings and arrays of strings'
- end
-
- context 'returns errors if job after_script parameter is multi-level nested array of strings' do
- let(:config) { YAML.dump({ rspec: { script: "test", after_script: [["ls", ["pwd"]], "test"] } }) }
-
- it_behaves_like 'returns errors', 'jobs:rspec:after_script config should be an array containing strings and arrays of strings'
+ it_behaves_like 'returns errors', 'jobs:rspec:after_script config should be a string or a nested array of strings up to 10 levels deep'
end
context 'returns errors if image parameter is invalid' do
diff --git a/spec/lib/gitlab/cluster/lifecycle_events_spec.rb b/spec/lib/gitlab/cluster/lifecycle_events_spec.rb
index 4ed68d54680..5eea78acd98 100644
--- a/spec/lib/gitlab/cluster/lifecycle_events_spec.rb
+++ b/spec/lib/gitlab/cluster/lifecycle_events_spec.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
-require 'rspec-parameterized'
+require 'spec_helper'
RSpec.describe Gitlab::Cluster::LifecycleEvents do
# we create a new instance to ensure that we do not touch existing hooks
diff --git a/spec/lib/gitlab/config/entry/factory_spec.rb b/spec/lib/gitlab/config/entry/factory_spec.rb
index 260b5cf0ade..be4dfd31651 100644
--- a/spec/lib/gitlab/config/entry/factory_spec.rb
+++ b/spec/lib/gitlab/config/entry/factory_spec.rb
@@ -5,8 +5,8 @@ require 'spec_helper'
RSpec.describe Gitlab::Config::Entry::Factory do
describe '#create!' do
before do
- stub_const('Script', Class.new(Gitlab::Config::Entry::Node))
- Script.class_eval do
+ stub_const('Commands', Class.new(Gitlab::Config::Entry::Node))
+ Commands.class_eval do
include Gitlab::Config::Entry::Validatable
validations do
@@ -15,7 +15,7 @@ RSpec.describe Gitlab::Config::Entry::Factory do
end
end
- let(:entry) { Script }
+ let(:entry) { Commands }
let(:factory) { described_class.new(entry) }
context 'when setting a concrete value' do
diff --git a/spec/lib/gitlab/console_spec.rb b/spec/lib/gitlab/console_spec.rb
new file mode 100644
index 00000000000..f043433b4c5
--- /dev/null
+++ b/spec/lib/gitlab/console_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+RSpec.describe Gitlab::Console do
+ describe '.welcome!' do
+ context 'when running in the Rails console' do
+ before do
+ allow(Gitlab::Runtime).to receive(:console?).and_return(true)
+ allow(Gitlab::Metrics::BootTimeTracker.instance).to receive(:startup_time).and_return(42)
+ end
+
+ shared_examples 'console messages' do
+ it 'prints system info' do
+ expect($stdout).to receive(:puts).ordered.with(include("--"))
+ expect($stdout).to receive(:puts).ordered.with(include("Ruby:"))
+ expect($stdout).to receive(:puts).ordered.with(include("GitLab:"))
+ expect($stdout).to receive(:puts).ordered.with(include("GitLab Shell:"))
+ expect($stdout).to receive(:puts).ordered.with(include("PostgreSQL:"))
+ expect($stdout).to receive(:puts).ordered.with(include("--"))
+ expect($stdout).not_to receive(:puts).ordered
+
+ described_class.welcome!
+ end
+ end
+
+ # This is to add line coverage, not to actually verify behavior on macOS.
+ context 'on darwin' do
+ before do
+ stub_const('RUBY_PLATFORM', 'x86_64-darwin-19')
+ end
+
+ it_behaves_like 'console messages'
+ end
+
+ it_behaves_like 'console messages'
+ end
+
+ context 'when not running in the Rails console' do
+ before do
+ allow(Gitlab::Runtime).to receive(:console?).and_return(false)
+ end
+
+ it 'does not print anything' do
+ expect($stdout).not_to receive(:puts)
+
+ described_class.welcome!
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/current_settings_spec.rb b/spec/lib/gitlab/current_settings_spec.rb
index 46c33d7b7b2..73540a9b0f3 100644
--- a/spec/lib/gitlab/current_settings_spec.rb
+++ b/spec/lib/gitlab/current_settings_spec.rb
@@ -118,7 +118,7 @@ RSpec.describe Gitlab::CurrentSettings do
allow(Gitlab::Runtime).to receive(:rake?).and_return(true)
# For some reason, `allow(described_class).to receive(:connect_to_db?).and_return(false)` causes issues
# during the initialization phase of the test suite, so instead let's mock the internals of it
- allow(ActiveRecord::Base.connection).to receive(:active?).and_return(false)
+ allow(ApplicationSetting.connection).to receive(:active?).and_return(false)
end
context 'and no settings in cache' do
@@ -150,8 +150,8 @@ RSpec.describe Gitlab::CurrentSettings do
it 'fetches the settings from cache' do
# For some reason, `allow(described_class).to receive(:connect_to_db?).and_return(true)` causes issues
# during the initialization phase of the test suite, so instead let's mock the internals of it
- expect(ActiveRecord::Base.connection).not_to receive(:active?)
- expect(ActiveRecord::Base.connection).not_to receive(:cached_table_exists?)
+ expect(ApplicationSetting.connection).not_to receive(:active?)
+ expect(ApplicationSetting.connection).not_to receive(:cached_table_exists?)
expect_any_instance_of(ActiveRecord::MigrationContext).not_to receive(:needs_migration?)
expect(ActiveRecord::QueryRecorder.new { described_class.current_application_settings }.count).to eq(0)
end
@@ -159,8 +159,8 @@ RSpec.describe Gitlab::CurrentSettings do
context 'and no settings in cache' do
before do
- allow(ActiveRecord::Base.connection).to receive(:active?).and_return(true)
- allow(ActiveRecord::Base.connection).to receive(:cached_table_exists?).with('application_settings').and_return(true)
+ allow(ApplicationSetting.connection).to receive(:active?).and_return(true)
+ allow(ApplicationSetting.connection).to receive(:cached_table_exists?).with('application_settings').and_return(true)
end
context 'with RequestStore enabled', :request_store do
@@ -181,7 +181,7 @@ RSpec.describe Gitlab::CurrentSettings do
context 'when ApplicationSettings does not have a primary key' do
before do
- allow(ActiveRecord::Base.connection).to receive(:primary_key).with('application_settings').and_return(nil)
+ allow(ApplicationSetting.connection).to receive(:primary_key).with('application_settings').and_return(nil)
end
it 'raises an exception if ApplicationSettings does not have a primary key' do
diff --git a/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb b/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb
index 8053f5261c0..7173ea43450 100644
--- a/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb
+++ b/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb
@@ -15,6 +15,13 @@ RSpec.describe Gitlab::CycleAnalytics::StageSummary do
let(:stage_summary) { described_class.new(project, **args).data }
+ describe '#identifier' do
+ it 'returns identifiers for each metric' do
+ identifiers = stage_summary.pluck(:identifier)
+ expect(identifiers).to eq(%i[issues commits deploys deployment_frequency])
+ end
+ end
+
describe "#new_issues" do
subject { stage_summary.first }
diff --git a/spec/lib/gitlab/database/background_migration/batch_metrics_spec.rb b/spec/lib/gitlab/database/background_migration/batch_metrics_spec.rb
index e96862fbc2d..66983733411 100644
--- a/spec/lib/gitlab/database/background_migration/batch_metrics_spec.rb
+++ b/spec/lib/gitlab/database/background_migration/batch_metrics_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'spec_helper'
RSpec.describe Gitlab::Database::BackgroundMigration::BatchMetrics do
let(:batch_metrics) { described_class.new }
diff --git a/spec/lib/gitlab/database/background_migration/batch_optimizer_spec.rb b/spec/lib/gitlab/database/background_migration/batch_optimizer_spec.rb
index 95863ce3765..c367f4a4493 100644
--- a/spec/lib/gitlab/database/background_migration/batch_optimizer_spec.rb
+++ b/spec/lib/gitlab/database/background_migration/batch_optimizer_spec.rb
@@ -6,7 +6,11 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchOptimizer do
describe '#optimize' do
subject { described_class.new(migration, number_of_jobs: number_of_jobs, ema_alpha: ema_alpha).optimize! }
- let(:migration) { create(:batched_background_migration, batch_size: batch_size, sub_batch_size: 100, interval: 120) }
+ let(:migration_params) { {} }
+ let(:migration) do
+ params = { batch_size: batch_size, sub_batch_size: 100, interval: 120 }.merge(migration_params)
+ create(:batched_background_migration, params)
+ end
let(:batch_size) { 10_000 }
@@ -87,6 +91,17 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchOptimizer do
expect { subject }.to change { migration.reload.batch_size }.to(2_000_000)
end
+
+ context 'when max_batch_size is set' do
+ let(:max_batch_size) { 10000 }
+ let(:migration_params) { { max_batch_size: max_batch_size } }
+
+ it 'caps the batch size at max_batch_size' do
+ mock_efficiency(0.7)
+
+ expect { subject }.to change { migration.reload.batch_size }.to(max_batch_size)
+ end
+ end
end
context 'reaching the lower limit for the batch size' do
diff --git a/spec/lib/gitlab/database/background_migration/batched_job_spec.rb b/spec/lib/gitlab/database/background_migration/batched_job_spec.rb
index c4364826ee2..7338ea657b9 100644
--- a/spec/lib/gitlab/database/background_migration/batched_job_spec.rb
+++ b/spec/lib/gitlab/database/background_migration/batched_job_spec.rb
@@ -7,18 +7,85 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedJob, type: :model d
describe 'associations' do
it { is_expected.to belong_to(:batched_migration).with_foreign_key(:batched_background_migration_id) }
+ it { is_expected.to have_many(:batched_job_transition_logs).with_foreign_key(:batched_background_migration_job_id) }
+ end
+
+ describe 'state machine' do
+ let_it_be(:job) { create(:batched_background_migration_job, :failed) }
+
+ context 'when a job is running' do
+ it 'logs the transition' do
+ expect(Gitlab::AppLogger).to receive(:info).with( { batched_job_id: job.id, message: 'BatchedJob transition', new_state: :running, previous_state: :failed } )
+
+ expect { job.run! }.to change(job, :started_at)
+ end
+ end
+
+ context 'when a job succeed' do
+ let(:job) { create(:batched_background_migration_job, :running) }
+
+ it 'logs the transition' do
+ expect(Gitlab::AppLogger).to receive(:info).with( { batched_job_id: job.id, message: 'BatchedJob transition', new_state: :succeeded, previous_state: :running } )
+
+ job.succeed!
+ end
+
+ it 'updates the finished_at' do
+ expect { job.succeed! }.to change(job, :finished_at).from(nil).to(Time)
+ end
+
+ it 'creates a new transition log' do
+ job.succeed!
+
+ transition_log = job.batched_job_transition_logs.first
+
+ expect(transition_log.next_status).to eq('succeeded')
+ expect(transition_log.exception_class).to be_nil
+ expect(transition_log.exception_message).to be_nil
+ end
+ end
+
+ context 'when a job fails' do
+ let(:job) { create(:batched_background_migration_job, :running) }
+
+ it 'logs the transition' do
+ expect(Gitlab::AppLogger).to receive(:info).with( { batched_job_id: job.id, message: 'BatchedJob transition', new_state: :failed, previous_state: :running } )
+
+ job.failure!
+ end
+
+ it 'tracks the exception' do
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).with(RuntimeError, { batched_job_id: job.id } )
+
+ job.failure!(error: RuntimeError.new)
+ end
+
+ it 'updates the finished_at' do
+ expect { job.failure! }.to change(job, :finished_at).from(nil).to(Time)
+ end
+
+ it 'creates a new transition log' do
+ job.failure!(error: RuntimeError.new)
+
+ transition_log = job.batched_job_transition_logs.first
+
+ expect(transition_log.next_status).to eq('failed')
+ expect(transition_log.exception_class).to eq('RuntimeError')
+ expect(transition_log.exception_message).to eq('RuntimeError')
+ end
+ end
end
describe 'scopes' do
let_it_be(:fixed_time) { Time.new(2021, 04, 27, 10, 00, 00, 00) }
- let_it_be(:pending_job) { create(:batched_background_migration_job, status: :pending, updated_at: fixed_time) }
- let_it_be(:running_job) { create(:batched_background_migration_job, status: :running, updated_at: fixed_time) }
- let_it_be(:stuck_job) { create(:batched_background_migration_job, status: :pending, updated_at: fixed_time - described_class::STUCK_JOBS_TIMEOUT) }
- let_it_be(:failed_job) { create(:batched_background_migration_job, status: :failed, attempts: 1) }
+ let_it_be(:pending_job) { create(:batched_background_migration_job, :pending, updated_at: fixed_time) }
+ let_it_be(:running_job) { create(:batched_background_migration_job, :running, updated_at: fixed_time) }
+ let_it_be(:stuck_job) { create(:batched_background_migration_job, :pending, updated_at: fixed_time - described_class::STUCK_JOBS_TIMEOUT) }
+ let_it_be(:failed_job) { create(:batched_background_migration_job, :failed, attempts: 1) }
- let!(:max_attempts_failed_job) { create(:batched_background_migration_job, status: :failed, attempts: described_class::MAX_ATTEMPTS) }
- let!(:succeeded_job) { create(:batched_background_migration_job, status: :succeeded) }
+ let!(:max_attempts_failed_job) { create(:batched_background_migration_job, :failed, attempts: described_class::MAX_ATTEMPTS) }
+ let!(:succeeded_job) { create(:batched_background_migration_job, :succeeded) }
before do
travel_to fixed_time
@@ -82,10 +149,10 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedJob, type: :model d
subject { job.time_efficiency }
let(:migration) { build(:batched_background_migration, interval: 120.seconds) }
- let(:job) { build(:batched_background_migration_job, status: :succeeded, batched_migration: migration) }
+ let(:job) { build(:batched_background_migration_job, :succeeded, batched_migration: migration) }
context 'when job has not yet succeeded' do
- let(:job) { build(:batched_background_migration_job, status: :running) }
+ let(:job) { build(:batched_background_migration_job, :running) }
it 'returns nil' do
expect(subject).to be_nil
@@ -130,7 +197,7 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedJob, type: :model d
end
describe '#split_and_retry!' do
- let!(:job) { create(:batched_background_migration_job, batch_size: 10, min_value: 6, max_value: 15, status: :failed, attempts: 3) }
+ let!(:job) { create(:batched_background_migration_job, :failed, batch_size: 10, min_value: 6, max_value: 15, attempts: 3) }
context 'when job can be split' do
before do
@@ -146,7 +213,7 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedJob, type: :model d
min_value: 6,
max_value: 10,
batch_size: 5,
- status: 'failed',
+ status_name: :failed,
attempts: 0,
started_at: nil,
finished_at: nil,
@@ -160,7 +227,7 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedJob, type: :model d
min_value: 11,
max_value: 15,
batch_size: 5,
- status: 'failed',
+ status_name: :failed,
attempts: 0,
started_at: nil,
finished_at: nil,
@@ -177,7 +244,7 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedJob, type: :model d
end
context 'when job is not failed' do
- let!(:job) { create(:batched_background_migration_job, status: :succeeded) }
+ let!(:job) { create(:batched_background_migration_job, :succeeded) }
it 'raises an exception' do
expect { job.split_and_retry! }.to raise_error 'Only failed jobs can be split'
@@ -185,7 +252,7 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedJob, type: :model d
end
context 'when batch size is already 1' do
- let!(:job) { create(:batched_background_migration_job, batch_size: 1, status: :failed) }
+ let!(:job) { create(:batched_background_migration_job, :failed, batch_size: 1) }
it 'raises an exception' do
expect { job.split_and_retry! }.to raise_error 'Job cannot be split further'
@@ -204,7 +271,7 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedJob, type: :model d
expect(job.batch_size).to eq(5)
expect(job.attempts).to eq(0)
- expect(job.status).to eq('failed')
+ expect(job.status_name).to eq(:failed)
end
end
end
diff --git a/spec/lib/gitlab/database/background_migration/batched_job_transition_log_spec.rb b/spec/lib/gitlab/database/background_migration/batched_job_transition_log_spec.rb
new file mode 100644
index 00000000000..c42a0fc5e05
--- /dev/null
+++ b/spec/lib/gitlab/database/background_migration/batched_job_transition_log_spec.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::BackgroundMigration::BatchedJobTransitionLog, type: :model do
+ describe 'associations' do
+ it { is_expected.to belong_to(:batched_job).with_foreign_key(:batched_background_migration_job_id) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:previous_status) }
+ it { is_expected.to validate_presence_of(:next_status) }
+ it { is_expected.to validate_presence_of(:batched_job) }
+ it { is_expected.to validate_length_of(:exception_class).is_at_most(100) }
+ it { is_expected.to validate_length_of(:exception_message).is_at_most(1000) }
+ it { is_expected.to define_enum_for(:previous_status).with_values(%i(pending running failed succeeded)).with_prefix }
+ it { is_expected.to define_enum_for(:next_status).with_values(%i(pending running failed succeeded)).with_prefix }
+ end
+end
diff --git a/spec/lib/gitlab/database/background_migration/batched_migration_runner_spec.rb b/spec/lib/gitlab/database/background_migration/batched_migration_runner_spec.rb
index 04c18a98ee6..bb2c6b9a3ae 100644
--- a/spec/lib/gitlab/database/background_migration/batched_migration_runner_spec.rb
+++ b/spec/lib/gitlab/database/background_migration/batched_migration_runner_spec.rb
@@ -96,13 +96,12 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationRunner do
end
let!(:previous_job) do
- create(:batched_background_migration_job,
+ create(:batched_background_migration_job, :succeeded,
batched_migration: migration,
min_value: event1.id,
max_value: event2.id,
batch_size: 2,
- sub_batch_size: 1,
- status: :succeeded
+ sub_batch_size: 1
)
end
@@ -144,7 +143,7 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationRunner do
context 'when migration has failed jobs' do
before do
- previous_job.update!(status: :failed)
+ previous_job.failure!
end
it 'retries the failed job' do
@@ -172,7 +171,7 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationRunner do
context 'when migration has stuck jobs' do
before do
- previous_job.update!(status: :running, updated_at: 1.hour.ago - Gitlab::Database::BackgroundMigration::BatchedJob::STUCK_JOBS_TIMEOUT)
+ previous_job.update!(status_event: 'run', updated_at: 1.hour.ago - Gitlab::Database::BackgroundMigration::BatchedJob::STUCK_JOBS_TIMEOUT)
end
it 'retries the stuck job' do
@@ -186,7 +185,7 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationRunner do
context 'when migration has possible stuck jobs' do
before do
- previous_job.update!(status: :running, updated_at: 1.hour.from_now - Gitlab::Database::BackgroundMigration::BatchedJob::STUCK_JOBS_TIMEOUT)
+ previous_job.update!(status_event: 'run', updated_at: 1.hour.from_now - Gitlab::Database::BackgroundMigration::BatchedJob::STUCK_JOBS_TIMEOUT)
end
it 'keeps the migration active' do
@@ -201,13 +200,13 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationRunner do
context 'when the migration has batches to process and failed jobs' do
before do
migration.update!(max_value: event3.id)
- previous_job.update!(status: :failed)
+ previous_job.failure!
end
it 'runs next batch then retries the failed job' do
expect(migration_wrapper).to receive(:perform) do |job_record|
expect(job_record).to eq(job_relation.last)
- job_record.update!(status: :succeeded)
+ job_record.succeed!
end
expect { runner.run_migration_job(migration) }.to change { job_relation.count }.by(1)
@@ -264,12 +263,12 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationRunner do
it 'runs all jobs inline until finishing the migration' do
expect(migration_wrapper).to receive(:perform) do |job_record|
expect(job_record).to eq(job_relation.first)
- job_record.update!(status: :succeeded)
+ job_record.succeed!
end
expect(migration_wrapper).to receive(:perform) do |job_record|
expect(job_record).to eq(job_relation.last)
- job_record.update!(status: :succeeded)
+ job_record.succeed!
end
expect { runner.run_entire_migration(migration) }.to change { job_relation.count }.by(2)
@@ -330,9 +329,9 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationRunner do
pause_ms: 0
}
- create(:batched_background_migration_job, common_attributes.merge(status: :succeeded, min_value: 1, max_value: 2))
- create(:batched_background_migration_job, common_attributes.merge(status: :pending, min_value: 3, max_value: 4))
- create(:batched_background_migration_job, common_attributes.merge(status: :failed, min_value: 5, max_value: 6, attempts: 1))
+ create(:batched_background_migration_job, :succeeded, common_attributes.merge(min_value: 1, max_value: 2))
+ create(:batched_background_migration_job, :pending, common_attributes.merge(min_value: 3, max_value: 4))
+ create(:batched_background_migration_job, :failed, common_attributes.merge(min_value: 5, max_value: 6, attempts: 1))
end
it 'completes the migration' do
@@ -359,7 +358,7 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationRunner do
context 'when migration fails to complete' do
it 'raises an error' do
- batched_migration.batched_jobs.failed.update_all(attempts: Gitlab::Database::BackgroundMigration::BatchedJob::MAX_ATTEMPTS)
+ batched_migration.batched_jobs.with_status(:failed).update_all(attempts: Gitlab::Database::BackgroundMigration::BatchedJob::MAX_ATTEMPTS)
expect do
runner.finalize(
diff --git a/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb b/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb
index 01d61a525e6..ea4ba4dd137 100644
--- a/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb
+++ b/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb
@@ -26,7 +26,7 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :m
context 'when there are failed jobs' do
let(:batched_migration) { create(:batched_background_migration, status: :active, total_tuple_count: 100) }
- let!(:batched_job) { create(:batched_background_migration_job, batched_migration: batched_migration, status: :failed) }
+ let!(:batched_job) { create(:batched_background_migration_job, :failed, batched_migration: batched_migration) }
it 'raises an exception' do
expect { batched_migration.finished! }.to raise_error(ActiveRecord::RecordInvalid)
@@ -37,7 +37,7 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :m
context 'when the jobs are completed' do
let(:batched_migration) { create(:batched_background_migration, status: :active, total_tuple_count: 100) }
- let!(:batched_job) { create(:batched_background_migration_job, batched_migration: batched_migration, status: :succeeded) }
+ let!(:batched_job) { create(:batched_background_migration_job, :succeeded, batched_migration: batched_migration) }
it 'finishes the migration' do
batched_migration.finished!
@@ -64,7 +64,7 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :m
it 'returns the first active migration according to queue order' do
expect(described_class.active_migration).to eq(migration2)
- create(:batched_background_migration_job, batched_migration: migration1, batch_size: 1000, status: :succeeded)
+ create(:batched_background_migration_job, :succeeded, batched_migration: migration1, batch_size: 1000)
end
end
@@ -84,10 +84,10 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :m
let!(:migration_without_jobs) { create(:batched_background_migration) }
before do
- create(:batched_background_migration_job, batched_migration: migration1, batch_size: 1000, status: :succeeded)
- create(:batched_background_migration_job, batched_migration: migration1, batch_size: 200, status: :failed)
- create(:batched_background_migration_job, batched_migration: migration2, batch_size: 500, status: :succeeded)
- create(:batched_background_migration_job, batched_migration: migration2, batch_size: 200, status: :running)
+ create(:batched_background_migration_job, :succeeded, batched_migration: migration1, batch_size: 1000)
+ create(:batched_background_migration_job, :failed, batched_migration: migration1, batch_size: 200)
+ create(:batched_background_migration_job, :succeeded, batched_migration: migration2, batch_size: 500)
+ create(:batched_background_migration_job, :running, batched_migration: migration2, batch_size: 200)
end
it 'returns totals from successful jobs' do
@@ -268,7 +268,7 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :m
subject(:retry_failed_jobs) { batched_migration.retry_failed_jobs! }
context 'when there are failed migration jobs' do
- let!(:batched_background_migration_job) { create(:batched_background_migration_job, batched_migration: batched_migration, batch_size: 10, min_value: 6, max_value: 15, status: :failed, attempts: 3) }
+ let!(:batched_background_migration_job) { create(:batched_background_migration_job, :failed, batched_migration: batched_migration, batch_size: 10, min_value: 6, max_value: 15, attempts: 3) }
before do
allow_next_instance_of(Gitlab::BackgroundMigration::BatchingStrategies::PrimaryKeyBatchingStrategy) do |batch_class|
@@ -312,9 +312,9 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :m
let(:batched_migration) { create(:batched_background_migration) }
before do
- create_list(:batched_background_migration_job, 5, status: :succeeded, batch_size: 1_000, batched_migration: batched_migration)
- create_list(:batched_background_migration_job, 1, status: :running, batch_size: 1_000, batched_migration: batched_migration)
- create_list(:batched_background_migration_job, 1, status: :failed, batch_size: 1_000, batched_migration: batched_migration)
+ create_list(:batched_background_migration_job, 5, :succeeded, batch_size: 1_000, batched_migration: batched_migration)
+ create_list(:batched_background_migration_job, 1, :running, batch_size: 1_000, batched_migration: batched_migration)
+ create_list(:batched_background_migration_job, 1, :failed, batch_size: 1_000, batched_migration: batched_migration)
end
it 'sums the batch_size of succeeded jobs' do
@@ -347,7 +347,6 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :m
let_it_be(:common_attrs) do
{
- status: :succeeded,
batched_migration: migration,
finished_at: end_time
}
@@ -357,7 +356,7 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :m
subject { migration.smoothed_time_efficiency(number_of_jobs: 10) }
it 'returns nil' do
- create_list(:batched_background_migration_job, 9, **common_attrs)
+ create_list(:batched_background_migration_job, 9, :succeeded, **common_attrs)
expect(subject).to be_nil
end
@@ -369,6 +368,8 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :m
subject { migration.smoothed_time_efficiency(number_of_jobs: number_of_jobs) }
+ let!(:jobs) { create_list(:batched_background_migration_job, number_of_jobs, :succeeded, **common_attrs.merge(batched_migration: migration)) }
+
before do
expect(migration).to receive_message_chain(:batched_jobs, :successful_in_execution_order, :reverse_order, :limit, :with_preloads)
.and_return(jobs)
diff --git a/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb b/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb
index c1183a15e37..4f5536d8771 100644
--- a/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb
+++ b/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb
@@ -35,8 +35,6 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper, '
expect(job_instance).to receive(:perform)
expect(job_instance).to receive(:batch_metrics).and_return(test_metrics)
- expect(job_record).to receive(:update!).with(hash_including(attempts: 1, status: :running)).and_call_original
-
freeze_time do
subject
@@ -51,11 +49,10 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper, '
context 'when running a job that failed previously' do
let!(:job_record) do
- create(:batched_background_migration_job,
+ create(:batched_background_migration_job, :failed,
batched_migration: active_migration,
pause_ms: pause_ms,
attempts: 1,
- status: :failed,
finished_at: 1.hour.ago,
metrics: { 'my_metrics' => 'some_value' }
)
@@ -67,10 +64,6 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper, '
expect(job_instance).to receive(:perform)
expect(job_instance).to receive(:batch_metrics).and_return(updated_metrics)
- expect(job_record).to receive(:update!).with(
- hash_including(attempts: 2, status: :running, finished_at: nil, metrics: {})
- ).and_call_original
-
freeze_time do
subject
@@ -201,4 +194,44 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper, '
it_behaves_like 'an error is raised', RuntimeError.new('Something broke!')
it_behaves_like 'an error is raised', SignalException.new('SIGTERM')
end
+
+ context 'when the batched background migration does not inherit from BaseJob' do
+ let(:migration_class) { Class.new }
+
+ before do
+ stub_const('Gitlab::BackgroundMigration::Foo', migration_class)
+ end
+
+ let(:connection) { double(:connection) }
+ let(:active_migration) { create(:batched_background_migration, :active, job_class_name: 'Foo') }
+ let!(:job_record) { create(:batched_background_migration_job, batched_migration: active_migration) }
+
+ it 'does not pass any argument' do
+ expect(Gitlab::BackgroundMigration::Foo).to receive(:new).with(no_args).and_return(job_instance)
+
+ expect(job_instance).to receive(:perform)
+
+ described_class.new(connection: connection).perform(job_record)
+ end
+ end
+
+ context 'when the batched background migration inherits from BaseJob' do
+ let(:connection) { double(:connection) }
+ let(:active_migration) { create(:batched_background_migration, :active, job_class_name: 'Foo') }
+ let!(:job_record) { create(:batched_background_migration_job, batched_migration: active_migration) }
+
+ let(:migration_class) { Class.new(::Gitlab::BackgroundMigration::BaseJob) }
+
+ before do
+ stub_const('Gitlab::BackgroundMigration::Foo', migration_class)
+ end
+
+ it 'passes the correct connection' do
+ expect(Gitlab::BackgroundMigration::Foo).to receive(:new).with(connection: connection).and_return(job_instance)
+
+ expect(job_instance).to receive(:perform)
+
+ described_class.new(connection: connection).perform(job_record)
+ end
+ end
end
diff --git a/spec/lib/gitlab/database/dynamic_model_helpers_spec.rb b/spec/lib/gitlab/database/dynamic_model_helpers_spec.rb
index 0844616ee1c..31486240bfa 100644
--- a/spec/lib/gitlab/database/dynamic_model_helpers_spec.rb
+++ b/spec/lib/gitlab/database/dynamic_model_helpers_spec.rb
@@ -4,10 +4,11 @@ require 'spec_helper'
RSpec.describe Gitlab::Database::DynamicModelHelpers do
let(:including_class) { Class.new.include(described_class) }
- let(:table_name) { 'projects' }
+ let(:table_name) { Project.table_name }
+ let(:connection) { Project.connection }
describe '#define_batchable_model' do
- subject { including_class.new.define_batchable_model(table_name) }
+ subject { including_class.new.define_batchable_model(table_name, connection: connection) }
it 'is an ActiveRecord model' do
expect(subject.ancestors).to include(ActiveRecord::Base)
@@ -40,7 +41,7 @@ RSpec.describe Gitlab::Database::DynamicModelHelpers do
it 'iterates table in batches' do
each_batch_size = ->(&block) do
- subject.each_batch(table_name, of: 1) do |batch|
+ subject.each_batch(table_name, connection: connection, of: 1) do |batch|
block.call(batch.size)
end
end
@@ -56,7 +57,7 @@ RSpec.describe Gitlab::Database::DynamicModelHelpers do
end
it 'raises an error' do
- expect { subject.each_batch(table_name, of: 1) { |batch| batch.size } }
+ expect { subject.each_batch(table_name, connection: connection, of: 1) { |batch| batch.size } }
.to raise_error(RuntimeError, /each_batch should not run inside a transaction/)
end
end
@@ -74,7 +75,7 @@ RSpec.describe Gitlab::Database::DynamicModelHelpers do
end
it 'iterates table in batch ranges' do
- expect { |b| subject.each_batch_range(table_name, of: 1, &b) }
+ expect { |b| subject.each_batch_range(table_name, connection: connection, of: 1, &b) }
.to yield_successive_args(
[first_project.id, first_project.id],
[second_project.id, second_project.id]
@@ -82,13 +83,13 @@ RSpec.describe Gitlab::Database::DynamicModelHelpers do
end
it 'yields only one batch if bigger than the table size' do
- expect { |b| subject.each_batch_range(table_name, of: 2, &b) }
+ expect { |b| subject.each_batch_range(table_name, connection: connection, of: 2, &b) }
.to yield_successive_args([first_project.id, second_project.id])
end
it 'makes it possible to apply a scope' do
each_batch_limited = ->(&b) do
- subject.each_batch_range(table_name, scope: ->(table) { table.limit(1) }, of: 1, &b)
+ subject.each_batch_range(table_name, connection: connection, scope: ->(table) { table.limit(1) }, of: 1, &b)
end
expect { |b| each_batch_limited.call(&b) }
@@ -102,7 +103,7 @@ RSpec.describe Gitlab::Database::DynamicModelHelpers do
end
it 'raises an error' do
- expect { subject.each_batch_range(table_name, of: 1) { 1 } }
+ expect { subject.each_batch_range(table_name, connection: connection, of: 1) { 1 } }
.to raise_error(RuntimeError, /each_batch should not run inside a transaction/)
end
end
diff --git a/spec/lib/gitlab/database/each_database_spec.rb b/spec/lib/gitlab/database/each_database_spec.rb
index 9327fc4ff78..d526b3bc1ac 100644
--- a/spec/lib/gitlab/database/each_database_spec.rb
+++ b/spec/lib/gitlab/database/each_database_spec.rb
@@ -4,45 +4,97 @@ require 'spec_helper'
RSpec.describe Gitlab::Database::EachDatabase do
describe '.each_database_connection' do
- let(:expected_connections) do
- Gitlab::Database.database_base_models.map { |name, model| [model.connection, name] }
+ before do
+ allow(Gitlab::Database).to receive(:database_base_models)
+ .and_return({ main: ActiveRecord::Base, ci: Ci::ApplicationRecord }.with_indifferent_access)
end
- it 'yields each connection after connecting SharedModel' do
- expected_connections.each do |connection, _|
- expect(Gitlab::Database::SharedModel).to receive(:using_connection).with(connection).and_yield
- end
+ it 'yields each connection after connecting SharedModel', :add_ci_connection do
+ expect(Gitlab::Database::SharedModel).to receive(:using_connection)
+ .with(ActiveRecord::Base.connection).ordered.and_yield
- yielded_connections = []
+ expect(Gitlab::Database::SharedModel).to receive(:using_connection)
+ .with(Ci::ApplicationRecord.connection).ordered.and_yield
- described_class.each_database_connection do |connection, name|
- yielded_connections << [connection, name]
- end
-
- expect(yielded_connections).to match_array(expected_connections)
+ expect { |b| described_class.each_database_connection(&b) }
+ .to yield_successive_args(
+ [ActiveRecord::Base.connection, 'main'],
+ [Ci::ApplicationRecord.connection, 'ci']
+ )
end
end
describe '.each_model_connection' do
- let(:model1) { double(connection: double, table_name: 'table1') }
- let(:model2) { double(connection: double, table_name: 'table2') }
+ context 'when the model inherits from SharedModel', :add_ci_connection do
+ let(:model1) { Class.new(Gitlab::Database::SharedModel) }
+ let(:model2) { Class.new(Gitlab::Database::SharedModel) }
- before do
- allow(model1.connection).to receive_message_chain('pool.db_config.name').and_return('name1')
- allow(model2.connection).to receive_message_chain('pool.db_config.name').and_return('name2')
+ before do
+ allow(Gitlab::Database).to receive(:database_base_models)
+ .and_return({ main: ActiveRecord::Base, ci: Ci::ApplicationRecord }.with_indifferent_access)
+ end
+
+ it 'yields each model with SharedModel connected to each database connection' do
+ expect_yielded_models([model1, model2], [
+ { model: model1, connection: ActiveRecord::Base.connection, name: 'main' },
+ { model: model1, connection: Ci::ApplicationRecord.connection, name: 'ci' },
+ { model: model2, connection: ActiveRecord::Base.connection, name: 'main' },
+ { model: model2, connection: Ci::ApplicationRecord.connection, name: 'ci' }
+ ])
+ end
+
+ context 'when the model limits connection names' do
+ before do
+ model1.limit_connection_names = %i[main]
+ model2.limit_connection_names = %i[ci]
+ end
+
+ it 'only yields the model with SharedModel connected to the limited connections' do
+ expect_yielded_models([model1, model2], [
+ { model: model1, connection: ActiveRecord::Base.connection, name: 'main' },
+ { model: model2, connection: Ci::ApplicationRecord.connection, name: 'ci' }
+ ])
+ end
+ end
+ end
+
+ context 'when the model does not inherit from SharedModel' do
+ let(:main_model) { Class.new(ActiveRecord::Base) }
+ let(:ci_model) { Class.new(Ci::ApplicationRecord) }
+
+ let(:main_connection) { double(:connection) }
+ let(:ci_connection) { double(:connection) }
+
+ before do
+ allow(main_model).to receive(:connection).and_return(main_connection)
+ allow(ci_model).to receive(:connection).and_return(ci_connection)
+
+ allow(main_connection).to receive_message_chain('pool.db_config.name').and_return('main')
+ allow(ci_connection).to receive_message_chain('pool.db_config.name').and_return('ci')
+ end
+
+ it 'yields each model after connecting SharedModel' do
+ expect_yielded_models([main_model, ci_model], [
+ { model: main_model, connection: main_connection, name: 'main' },
+ { model: ci_model, connection: ci_connection, name: 'ci' }
+ ])
+ end
end
- it 'yields each model after connecting SharedModel' do
- expect(Gitlab::Database::SharedModel).to receive(:using_connection).with(model1.connection).and_yield
- expect(Gitlab::Database::SharedModel).to receive(:using_connection).with(model2.connection).and_yield
+ def expect_yielded_models(models_to_iterate, expected_values)
+ times_yielded = 0
+
+ described_class.each_model_connection(models_to_iterate) do |model, name|
+ expected = expected_values[times_yielded]
- yielded_models = []
+ expect(model).to be(expected[:model])
+ expect(model.connection).to be(expected[:connection])
+ expect(name).to eq(expected[:name])
- described_class.each_model_connection([model1, model2]) do |model, name|
- yielded_models << [model, name]
+ times_yielded += 1
end
- expect(yielded_models).to match_array([[model1, 'name1'], [model2, 'name2']])
+ expect(times_yielded).to eq(expected_values.size)
end
end
end
diff --git a/spec/lib/gitlab/database/gitlab_schema_spec.rb b/spec/lib/gitlab/database/gitlab_schema_spec.rb
index 255efc99ff6..a5a67c2c918 100644
--- a/spec/lib/gitlab/database/gitlab_schema_spec.rb
+++ b/spec/lib/gitlab/database/gitlab_schema_spec.rb
@@ -44,6 +44,8 @@ RSpec.describe Gitlab::Database::GitlabSchema do
'my_schema.ci_builds' | :gitlab_ci
'information_schema.columns' | :gitlab_shared
'audit_events_part_5fc467ac26' | :gitlab_main
+ '_test_gitlab_main_table' | :gitlab_main
+ '_test_gitlab_ci_table' | :gitlab_ci
'_test_my_table' | :gitlab_shared
'pg_attribute' | :gitlab_shared
'my_other_table' | :undefined_my_other_table
diff --git a/spec/lib/gitlab/database/load_balancing/configuration_spec.rb b/spec/lib/gitlab/database/load_balancing/configuration_spec.rb
index 796c14c1038..e87c9c20707 100644
--- a/spec/lib/gitlab/database/load_balancing/configuration_spec.rb
+++ b/spec/lib/gitlab/database/load_balancing/configuration_spec.rb
@@ -2,11 +2,18 @@
require 'spec_helper'
-RSpec.describe Gitlab::Database::LoadBalancing::Configuration do
+RSpec.describe Gitlab::Database::LoadBalancing::Configuration, :request_store do
let(:configuration_hash) { {} }
let(:db_config) { ActiveRecord::DatabaseConfigurations::HashConfig.new('test', 'ci', configuration_hash) }
let(:model) { double(:model, connection_db_config: db_config) }
+ before do
+ # It's confusing to think about these specs with this enabled by default so
+ # we make it disabled by default and just write the specific spec for when
+ # it's enabled
+ stub_feature_flags(force_no_sharing_primary_model: false)
+ end
+
describe '.for_model' do
context 'when load balancing is not configured' do
it 'uses the default settings' do
@@ -233,11 +240,23 @@ RSpec.describe Gitlab::Database::LoadBalancing::Configuration do
end
context 'when GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci=main' do
- it 'the primary connection uses main connection' do
+ before do
stub_env('GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci', 'main')
+ end
+ it 'the primary connection uses main connection' do
expect(config.primary_connection_specification_name).to eq('ActiveRecord::Base')
end
+
+ context 'when force_no_sharing_primary_model feature flag is enabled' do
+ before do
+ stub_feature_flags(force_no_sharing_primary_model: true)
+ end
+
+ it 'the primary connection uses ci connection' do
+ expect(config.primary_connection_specification_name).to eq('Ci::ApplicationRecord')
+ end
+ end
end
context 'when GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci=unknown' do
diff --git a/spec/lib/gitlab/database/load_balancing/setup_spec.rb b/spec/lib/gitlab/database/load_balancing/setup_spec.rb
index 953d83d3b48..20519a759b2 100644
--- a/spec/lib/gitlab/database/load_balancing/setup_spec.rb
+++ b/spec/lib/gitlab/database/load_balancing/setup_spec.rb
@@ -130,6 +130,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::Setup do
env_GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci: nil,
request_store_active: false,
ff_use_model_load_balancing: nil,
+ ff_force_no_sharing_primary_model: false,
expectations: {
main: { read: 'main_replica', write: 'main' },
ci: { read: 'ci_replica', write: 'ci' }
@@ -140,6 +141,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::Setup do
env_GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci: 'main',
request_store_active: false,
ff_use_model_load_balancing: nil,
+ ff_force_no_sharing_primary_model: false,
expectations: {
main: { read: 'main_replica', write: 'main' },
ci: { read: 'ci_replica', write: 'main' }
@@ -150,6 +152,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::Setup do
env_GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci: nil,
request_store_active: false,
ff_use_model_load_balancing: nil,
+ ff_force_no_sharing_primary_model: false,
expectations: {
main: { read: 'main_replica', write: 'main' },
ci: { read: 'main_replica', write: 'main' }
@@ -160,60 +163,77 @@ RSpec.describe Gitlab::Database::LoadBalancing::Setup do
env_GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci: 'main',
request_store_active: false,
ff_use_model_load_balancing: nil,
+ ff_force_no_sharing_primary_model: false,
expectations: {
main: { read: 'main_replica', write: 'main' },
ci: { read: 'main_replica', write: 'main' }
}
},
- "with FF disabled without RequestStore it uses main" => {
+ "with FF use_model_load_balancing disabled without RequestStore it uses main" => {
env_GITLAB_USE_MODEL_LOAD_BALANCING: nil,
env_GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci: nil,
request_store_active: false,
ff_use_model_load_balancing: false,
+ ff_force_no_sharing_primary_model: false,
expectations: {
main: { read: 'main_replica', write: 'main' },
ci: { read: 'main_replica', write: 'main' }
}
},
- "with FF enabled without RequestStore sticking of FF does not work, so it fallbacks to use main" => {
+ "with FF use_model_load_balancing enabled without RequestStore sticking of FF does not work, so it fallbacks to use main" => {
env_GITLAB_USE_MODEL_LOAD_BALANCING: nil,
env_GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci: nil,
request_store_active: false,
ff_use_model_load_balancing: true,
+ ff_force_no_sharing_primary_model: false,
expectations: {
main: { read: 'main_replica', write: 'main' },
ci: { read: 'main_replica', write: 'main' }
}
},
- "with FF disabled with RequestStore it uses main" => {
+ "with FF use_model_load_balancing disabled with RequestStore it uses main" => {
env_GITLAB_USE_MODEL_LOAD_BALANCING: nil,
env_GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci: nil,
request_store_active: true,
ff_use_model_load_balancing: false,
+ ff_force_no_sharing_primary_model: false,
expectations: {
main: { read: 'main_replica', write: 'main' },
ci: { read: 'main_replica', write: 'main' }
}
},
- "with FF enabled with RequestStore it sticks FF and uses CI connection" => {
+ "with FF use_model_load_balancing enabled with RequestStore it sticks FF and uses CI connection" => {
env_GITLAB_USE_MODEL_LOAD_BALANCING: nil,
env_GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci: nil,
request_store_active: true,
ff_use_model_load_balancing: true,
+ ff_force_no_sharing_primary_model: false,
expectations: {
main: { read: 'main_replica', write: 'main' },
ci: { read: 'ci_replica', write: 'ci' }
}
},
- "with re-use and FF enabled with RequestStore it sticks FF and uses CI connection for reads" => {
+ "with re-use and ff_use_model_load_balancing enabled and FF force_no_sharing_primary_model disabled with RequestStore it sticks FF and uses CI connection for reads" => {
env_GITLAB_USE_MODEL_LOAD_BALANCING: nil,
env_GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci: 'main',
request_store_active: true,
ff_use_model_load_balancing: true,
+ ff_force_no_sharing_primary_model: false,
expectations: {
main: { read: 'main_replica', write: 'main' },
ci: { read: 'ci_replica', write: 'main' }
}
+ },
+ "with re-use and ff_use_model_load_balancing enabled and FF force_no_sharing_primary_model enabled with RequestStore it sticks FF and uses CI connection for reads" => {
+ env_GITLAB_USE_MODEL_LOAD_BALANCING: nil,
+ env_GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci: 'main',
+ request_store_active: true,
+ ff_use_model_load_balancing: true,
+ ff_force_no_sharing_primary_model: true,
+ expectations: {
+ main: { read: 'main_replica', write: 'main' },
+ ci: { read: 'ci_replica', write: 'ci' }
+ }
}
}
end
@@ -243,6 +263,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::Setup do
around do |example|
if request_store_active
Gitlab::WithRequestStore.with_request_store do
+ stub_feature_flags(force_no_sharing_primary_model: ff_force_no_sharing_primary_model)
RequestStore.clear!
example.run
diff --git a/spec/lib/gitlab/database/loose_foreign_keys_spec.rb b/spec/lib/gitlab/database/loose_foreign_keys_spec.rb
index 13f2d31bc32..ed11699e494 100644
--- a/spec/lib/gitlab/database/loose_foreign_keys_spec.rb
+++ b/spec/lib/gitlab/database/loose_foreign_keys_spec.rb
@@ -18,6 +18,45 @@ RSpec.describe Gitlab::Database::LooseForeignKeys do
))
end
+ context 'ensure keys are sorted' do
+ it 'does not have any keys that are out of order' do
+ parsed = YAML.parse_file(described_class.loose_foreign_keys_yaml_path)
+ mapping = parsed.children.first
+ table_names = mapping.children.select(&:scalar?).map(&:value)
+ expect(table_names).to eq(table_names.sort), "expected sorted table names in the YAML file"
+ end
+ end
+
+ context 'ensure no duplicates are found' do
+ it 'does not have duplicate tables defined' do
+ # since we use hash to detect duplicate hash keys we need to parse YAML document
+ parsed = YAML.parse_file(described_class.loose_foreign_keys_yaml_path)
+ expect(parsed).to be_document
+ expect(parsed.children).to be_one, "YAML has a single document"
+
+ # require hash
+ mapping = parsed.children.first
+ expect(mapping).to be_mapping, "YAML has a top-level hash"
+
+ # find all scalars with names
+ table_names = mapping.children.select(&:scalar?).map(&:value)
+ expect(table_names).not_to be_empty, "YAML has a non-zero tables defined"
+
+ # expect to not have duplicates
+ expect(table_names).to contain_exactly(*table_names.uniq)
+ end
+
+ it 'does not have duplicate column definitions' do
+ # ignore other modifiers
+ all_definitions = definitions.map do |definition|
+ { from_table: definition.from_table, to_table: definition.to_table, column: definition.column }
+ end
+
+ # expect to not have duplicates
+ expect(all_definitions).to contain_exactly(*all_definitions.uniq)
+ end
+ end
+
describe 'ensuring database integrity' do
def base_models_for(table)
parent_table_schema = Gitlab::Database::GitlabSchema.table_schema(table)
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index 7e3de32b965..d71a4f81901 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -14,6 +14,54 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
allow(model).to receive(:puts)
end
+ describe 'overridden dynamic model helpers' do
+ let(:test_table) { '__test_batching_table' }
+
+ before do
+ model.connection.execute(<<~SQL)
+ CREATE TABLE #{test_table} (
+ id integer NOT NULL PRIMARY KEY,
+ name text NOT NULL
+ );
+
+ INSERT INTO #{test_table} (id, name)
+ VALUES (1, 'bob'), (2, 'mary'), (3, 'amy');
+ SQL
+ end
+
+ describe '#define_batchable_model' do
+ it 'defines a batchable model with the migration connection' do
+ expect(model.define_batchable_model(test_table).count).to eq(3)
+ end
+ end
+
+ describe '#each_batch' do
+ before do
+ allow(model).to receive(:transaction_open?).and_return(false)
+ end
+
+ it 'calls each_batch with the migration connection' do
+ each_batch_name = ->(&block) do
+ model.each_batch(test_table, of: 2) do |batch|
+ block.call(batch.pluck(:name))
+ end
+ end
+
+ expect { |b| each_batch_name.call(&b) }.to yield_successive_args(%w[bob mary], %w[amy])
+ end
+ end
+
+ describe '#each_batch_range' do
+ before do
+ allow(model).to receive(:transaction_open?).and_return(false)
+ end
+
+ it 'calls each_batch with the migration connection' do
+ expect { |b| model.each_batch_range(test_table, of: 2, &b) }.to yield_successive_args([1, 2], [3, 3])
+ end
+ end
+ end
+
describe '#remove_timestamps' do
it 'can remove the default timestamps' do
Gitlab::Database::MigrationHelpers::DEFAULT_TIMESTAMP_COLUMNS.each do |column_name|
@@ -442,6 +490,60 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
end
end
+ describe '#remove_foreign_key_if_exists' do
+ context 'when the foreign key does not exist' do
+ before do
+ allow(model).to receive(:foreign_key_exists?).and_return(false)
+ end
+
+ it 'does nothing' do
+ expect(model).not_to receive(:remove_foreign_key)
+
+ model.remove_foreign_key_if_exists(:projects, :users, column: :user_id)
+ end
+ end
+
+ context 'when the foreign key exists' do
+ before do
+ allow(model).to receive(:foreign_key_exists?).and_return(true)
+ end
+
+ it 'removes the foreign key' do
+ expect(model).to receive(:remove_foreign_key).with(:projects, :users, { column: :user_id })
+
+ model.remove_foreign_key_if_exists(:projects, :users, column: :user_id)
+ end
+
+ context 'when the target table is not given' do
+ it 'passes the options as the second parameter' do
+ expect(model).to receive(:remove_foreign_key).with(:projects, { column: :user_id })
+
+ model.remove_foreign_key_if_exists(:projects, column: :user_id)
+ end
+ end
+
+ context 'when the reverse_lock_order option is given' do
+ it 'requests for lock before removing the foreign key' do
+ expect(model).to receive(:transaction_open?).and_return(true)
+ expect(model).to receive(:execute).with(/LOCK TABLE users, projects/)
+ expect(model).not_to receive(:remove_foreign_key).with(:projects, :users)
+
+ model.remove_foreign_key_if_exists(:projects, :users, column: :user_id, reverse_lock_order: true)
+ end
+
+ context 'when not inside a transaction' do
+ it 'does not lock' do
+ expect(model).to receive(:transaction_open?).and_return(false)
+ expect(model).not_to receive(:execute).with(/LOCK TABLE users, projects/)
+ expect(model).to receive(:remove_foreign_key).with(:projects, :users, { column: :user_id })
+
+ model.remove_foreign_key_if_exists(:projects, :users, column: :user_id, reverse_lock_order: true)
+ end
+ end
+ end
+ end
+ end
+
describe '#add_concurrent_foreign_key' do
before do
allow(model).to receive(:foreign_key_exists?).and_return(false)
diff --git a/spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb b/spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb
index 0abb76b9f8a..96dc3a0fc28 100644
--- a/spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb
@@ -299,7 +299,7 @@ RSpec.describe Gitlab::Database::Migrations::BackgroundMigrationHelpers do
before do
allow(Gitlab::BackgroundMigration).to receive(:coordinator_for_database)
- .with('main').and_return(coordinator)
+ .with(tracking_database).and_return(coordinator)
expect(coordinator).to receive(:migration_class_for)
.with(job_class_name).at_least(:once) { job_class }
@@ -403,7 +403,7 @@ RSpec.describe Gitlab::Database::Migrations::BackgroundMigrationHelpers do
end
context 'when a specific coordinator is given' do
- let(:coordinator) { Gitlab::BackgroundMigration::JobCoordinator.for_tracking_database('main') }
+ let(:coordinator) { Gitlab::BackgroundMigration::JobCoordinator.for_tracking_database(tracking_database) }
it 'uses that coordinator' do
expect(coordinator).to receive(:perform_in).with(10.minutes, 'Class', 'Hello', 'World').and_call_original
@@ -438,6 +438,16 @@ RSpec.describe Gitlab::Database::Migrations::BackgroundMigrationHelpers do
it_behaves_like 'helpers that enqueue background migrations', BackgroundMigrationWorker, 'main'
end
+ context 'when the migration is running against the ci database', if: Gitlab::Database.has_config?(:ci) do
+ around do |example|
+ Gitlab::Database::SharedModel.using_connection(::Ci::ApplicationRecord.connection) do
+ example.run
+ end
+ end
+
+ it_behaves_like 'helpers that enqueue background migrations', BackgroundMigration::CiDatabaseWorker, 'ci'
+ end
+
describe '#delete_job_tracking' do
let!(:job_class_name) { 'TestJob' }
diff --git a/spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb b/spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb
index c45149d67bf..37efff165c7 100644
--- a/spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb
@@ -59,6 +59,7 @@ RSpec.describe Gitlab::Database::Migrations::BatchedBackgroundMigrationHelpers d
batch_max_value: 1000,
batch_class_name: 'MyBatchClass',
batch_size: 100,
+ max_batch_size: 10000,
sub_batch_size: 10)
end.to change { Gitlab::Database::BackgroundMigration::BatchedMigration.count }.by(1)
@@ -71,6 +72,7 @@ RSpec.describe Gitlab::Database::Migrations::BatchedBackgroundMigrationHelpers d
max_value: 1000,
batch_class_name: 'MyBatchClass',
batch_size: 100,
+ max_batch_size: 10000,
sub_batch_size: 10,
job_arguments: %w[],
status: 'active',
diff --git a/spec/lib/gitlab/database/migrations/instrumentation_spec.rb b/spec/lib/gitlab/database/migrations/instrumentation_spec.rb
index 902d8e13a63..fd8303c379c 100644
--- a/spec/lib/gitlab/database/migrations/instrumentation_spec.rb
+++ b/spec/lib/gitlab/database/migrations/instrumentation_spec.rb
@@ -66,55 +66,43 @@ RSpec.describe Gitlab::Database::Migrations::Instrumentation do
context 'on successful execution' do
subject { described_class.new(result_dir: result_dir).observe(version: migration_version, name: migration_name, connection: connection) {} }
- it 'records walltime' do
+ it 'records a valid observation', :aggregate_failures do
expect(subject.walltime).not_to be_nil
- end
-
- it 'records success' do
expect(subject.success).to be_truthy
- end
-
- it 'records the migration version' do
expect(subject.version).to eq(migration_version)
- end
-
- it 'records the migration name' do
expect(subject.name).to eq(migration_name)
end
end
context 'upon failure' do
- subject { described_class.new(result_dir: result_dir).observe(version: migration_version, name: migration_name, connection: connection) { raise 'something went wrong' } }
-
- it 'raises the exception' do
- expect { subject }.to raise_error(/something went wrong/)
- end
-
- context 'retrieving observations' do
- subject { instance.observations.first }
-
- before do
- instance.observe(version: migration_version, name: migration_name, connection: connection) { raise 'something went wrong' }
- rescue StandardError
- # ignore
- end
+ where(exception: ['something went wrong', SystemStackError, Interrupt])
+ with_them do
let(:instance) { described_class.new(result_dir: result_dir) }
- it 'records walltime' do
- expect(subject.walltime).not_to be_nil
- end
-
- it 'records failure' do
- expect(subject.success).to be_falsey
- end
+ subject(:observe) { instance.observe(version: migration_version, name: migration_name, connection: connection) { raise exception } }
- it 'records the migration version' do
- expect(subject.version).to eq(migration_version)
+ it 'raises the exception' do
+ expect { observe }.to raise_error(exception)
end
- it 'records the migration name' do
- expect(subject.name).to eq(migration_name)
+ context 'retrieving observations' do
+ subject { instance.observations.first }
+
+ before do
+ observe
+ # rubocop:disable Lint/RescueException
+ rescue Exception
+ # rubocop:enable Lint/RescueException
+ # ignore (we expect this exception)
+ end
+
+ it 'records a valid observation', :aggregate_failures do
+ expect(subject.walltime).not_to be_nil
+ expect(subject.success).to be_falsey
+ expect(subject.version).to eq(migration_version)
+ expect(subject.name).to eq(migration_name)
+ end
end
end
end
diff --git a/spec/lib/gitlab/database/migrations/lock_retry_mixin_spec.rb b/spec/lib/gitlab/database/migrations/lock_retry_mixin_spec.rb
index 076fb9e8215..50ad77caaf1 100644
--- a/spec/lib/gitlab/database/migrations/lock_retry_mixin_spec.rb
+++ b/spec/lib/gitlab/database/migrations/lock_retry_mixin_spec.rb
@@ -3,7 +3,8 @@ require 'spec_helper'
RSpec.describe Gitlab::Database::Migrations::LockRetryMixin do
describe Gitlab::Database::Migrations::LockRetryMixin::ActiveRecordMigrationProxyLockRetries do
- let(:migration) { double }
+ let(:connection) { ActiveRecord::Base.connection }
+ let(:migration) { double(connection: connection) }
let(:return_value) { double }
let(:class_def) do
Class.new do
@@ -40,6 +41,18 @@ RSpec.describe Gitlab::Database::Migrations::LockRetryMixin do
expect(result).to eq(return_value)
end
end
+
+ describe '#migration_connection' do
+ subject { class_def.new(migration).migration_connection }
+
+ it 'retrieves actual migration connection from #migration' do
+ expect(migration).to receive(:connection).and_return(return_value)
+
+ result = subject
+
+ expect(result).to eq(return_value)
+ end
+ end
end
describe Gitlab::Database::Migrations::LockRetryMixin::ActiveRecordMigratorLockRetries do
@@ -96,7 +109,8 @@ RSpec.describe Gitlab::Database::Migrations::LockRetryMixin do
context 'with transactions enabled and lock retries enabled' do
let(:receiver) { double('receiver', use_transaction?: true)}
- let(:migration) { double('migration', enable_lock_retries?: true) }
+ let(:migration) { double('migration', migration_connection: connection, enable_lock_retries?: true) }
+ let(:connection) { ActiveRecord::Base.connection }
it 'calls super method' do
p = proc { }
diff --git a/spec/lib/gitlab/database/migrations/observers/query_details_spec.rb b/spec/lib/gitlab/database/migrations/observers/query_details_spec.rb
index 5a19ae6581d..a757cac0a2a 100644
--- a/spec/lib/gitlab/database/migrations/observers/query_details_spec.rb
+++ b/spec/lib/gitlab/database/migrations/observers/query_details_spec.rb
@@ -5,7 +5,7 @@ RSpec.describe Gitlab::Database::Migrations::Observers::QueryDetails do
subject { described_class.new(observation, directory_path, connection) }
let(:connection) { ActiveRecord::Migration.connection }
- let(:observation) { Gitlab::Database::Migrations::Observation.new(migration_version, migration_name) }
+ let(:observation) { Gitlab::Database::Migrations::Observation.new(version: migration_version, name: migration_name) }
let(:query) { "select date_trunc('day', $1::timestamptz) + $2 * (interval '1 hour')" }
let(:query_binds) { [Time.current, 3] }
let(:directory_path) { Dir.mktmpdir }
diff --git a/spec/lib/gitlab/database/migrations/observers/query_log_spec.rb b/spec/lib/gitlab/database/migrations/observers/query_log_spec.rb
index 7b01e39f5f1..eb66972e5ab 100644
--- a/spec/lib/gitlab/database/migrations/observers/query_log_spec.rb
+++ b/spec/lib/gitlab/database/migrations/observers/query_log_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Gitlab::Database::Migrations::Observers::QueryLog do
subject { described_class.new(observation, directory_path, connection) }
- let(:observation) { Gitlab::Database::Migrations::Observation.new(migration_version, migration_name) }
+ let(:observation) { Gitlab::Database::Migrations::Observation.new(version: migration_version, name: migration_name) }
let(:connection) { ActiveRecord::Migration.connection }
let(:query) { 'select 1' }
let(:directory_path) { Dir.mktmpdir }
diff --git a/spec/lib/gitlab/database/migrations/observers/transaction_duration_spec.rb b/spec/lib/gitlab/database/migrations/observers/transaction_duration_spec.rb
index b26bb8fbe41..f433e25b2ba 100644
--- a/spec/lib/gitlab/database/migrations/observers/transaction_duration_spec.rb
+++ b/spec/lib/gitlab/database/migrations/observers/transaction_duration_spec.rb
@@ -5,7 +5,7 @@ RSpec.describe Gitlab::Database::Migrations::Observers::TransactionDuration do
subject(:transaction_duration_observer) { described_class.new(observation, directory_path, connection) }
let(:connection) { ActiveRecord::Migration.connection }
- let(:observation) { Gitlab::Database::Migrations::Observation.new(migration_version, migration_name) }
+ let(:observation) { Gitlab::Database::Migrations::Observation.new(version: migration_version, name: migration_name) }
let(:directory_path) { Dir.mktmpdir }
let(:log_file) { "#{directory_path}/#{migration_version}_#{migration_name}-transaction-duration.json" }
let(:transaction_duration) { Gitlab::Json.parse(File.read(log_file)) }
diff --git a/spec/lib/gitlab/database/no_cross_db_foreign_keys_spec.rb b/spec/lib/gitlab/database/no_cross_db_foreign_keys_spec.rb
index e5a8143fcc3..f94a40c93e1 100644
--- a/spec/lib/gitlab/database/no_cross_db_foreign_keys_spec.rb
+++ b/spec/lib/gitlab/database/no_cross_db_foreign_keys_spec.rb
@@ -3,59 +3,12 @@
require 'spec_helper'
RSpec.describe 'cross-database foreign keys' do
- # TODO: We are trying to empty out this list in
- # https://gitlab.com/groups/gitlab-org/-/epics/7249 . Once we are done we can
- # keep this test and assert that there are no cross-db foreign keys. We
- # should not be adding anything to this list but should instead only add new
- # loose foreign keys
- # https://docs.gitlab.com/ee/development/database/loose_foreign_keys.html .
+ # Since we don't expect to have any cross-database foreign keys
+ # this is empty. If we will have an entry like
+ # `ci_daily_build_group_report_results.project_id`
+ # should be added.
let(:allowed_cross_database_foreign_keys) do
- %w(
- ci_build_report_results.project_id
- ci_builds.project_id
- ci_builds_metadata.project_id
- ci_daily_build_group_report_results.group_id
- ci_daily_build_group_report_results.project_id
- ci_freeze_periods.project_id
- ci_job_artifacts.project_id
- ci_job_token_project_scope_links.added_by_id
- ci_job_token_project_scope_links.source_project_id
- ci_job_token_project_scope_links.target_project_id
- ci_pending_builds.namespace_id
- ci_pending_builds.project_id
- ci_pipeline_schedules.owner_id
- ci_pipeline_schedules.project_id
- ci_pipelines.merge_request_id
- ci_pipelines.project_id
- ci_project_monthly_usages.project_id
- ci_refs.project_id
- ci_resource_groups.project_id
- ci_runner_namespaces.namespace_id
- ci_runner_projects.project_id
- ci_running_builds.project_id
- ci_sources_pipelines.project_id
- ci_sources_pipelines.source_project_id
- ci_sources_projects.source_project_id
- ci_stages.project_id
- ci_subscriptions_projects.downstream_project_id
- ci_subscriptions_projects.upstream_project_id
- ci_triggers.owner_id
- ci_triggers.project_id
- ci_unit_tests.project_id
- ci_variables.project_id
- dast_profiles_pipelines.ci_pipeline_id
- dast_scanner_profiles_builds.ci_build_id
- dast_site_profiles_builds.ci_build_id
- dast_site_profiles_pipelines.ci_pipeline_id
- external_pull_requests.project_id
- merge_requests.head_pipeline_id
- merge_trains.pipeline_id
- requirements_management_test_reports.build_id
- security_scans.build_id
- vulnerability_feedback.pipeline_id
- vulnerability_occurrence_pipelines.pipeline_id
- vulnerability_statistics.latest_pipeline_id
- ).freeze
+ %w[].freeze
end
def foreign_keys_for(table_name)
diff --git a/spec/lib/gitlab/database/query_analyzers/prevent_cross_database_modification_spec.rb b/spec/lib/gitlab/database/query_analyzers/prevent_cross_database_modification_spec.rb
index c41b4eeea10..22a70dc7df0 100644
--- a/spec/lib/gitlab/database/query_analyzers/prevent_cross_database_modification_spec.rb
+++ b/spec/lib/gitlab/database/query_analyzers/prevent_cross_database_modification_spec.rb
@@ -14,23 +14,41 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModificatio
Gitlab::Database::QueryAnalyzer.instance.within { example.run }
end
- shared_examples 'successful examples' do
+ describe 'context and suppress key names' do
+ describe '.context_key' do
+ it 'contains class name' do
+ expect(described_class.context_key)
+ .to eq 'analyzer_prevent_cross_database_modification_context'.to_sym
+ end
+ end
+
+ describe '.suppress_key' do
+ it 'contains class name' do
+ expect(described_class.suppress_key)
+ .to eq 'analyzer_prevent_cross_database_modification_suppressed'.to_sym
+ end
+ end
+ end
+
+ shared_examples 'successful examples' do |model:|
+ let(:model) { model }
+
context 'outside transaction' do
it { expect { run_queries }.not_to raise_error }
end
- context 'within transaction' do
+ context "within #{model} transaction" do
it do
- Project.transaction do
+ model.transaction do
expect { run_queries }.not_to raise_error
end
end
end
- context 'within nested transaction' do
+ context "within nested #{model} transaction" do
it do
- Project.transaction(requires_new: true) do
- Project.transaction(requires_new: true) do
+ model.transaction(requires_new: true) do
+ model.transaction(requires_new: true) do
expect { run_queries }.not_to raise_error
end
end
@@ -38,13 +56,26 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModificatio
end
end
+ shared_examples 'cross-database modification errors' do |model:|
+ let(:model) { model }
+
+ context "within #{model} transaction" do
+ it 'raises error' do
+ model.transaction do
+ expect { run_queries }.to raise_error /Cross-database data modification/
+ end
+ end
+ end
+ end
+
context 'when CI and other tables are read in a transaction' do
def run_queries
pipeline.reload
project.reload
end
- include_examples 'successful examples'
+ include_examples 'successful examples', model: Project
+ include_examples 'successful examples', model: Ci::Pipeline
end
context 'when only CI data is modified' do
@@ -53,7 +84,9 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModificatio
project.reload
end
- include_examples 'successful examples'
+ include_examples 'successful examples', model: Ci::Pipeline
+
+ include_examples 'cross-database modification errors', model: Project
end
context 'when other data is modified' do
@@ -62,7 +95,9 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModificatio
project.touch
end
- include_examples 'successful examples'
+ include_examples 'successful examples', model: Project
+
+ include_examples 'cross-database modification errors', model: Ci::Pipeline
end
context 'when both CI and other data is modified' do
@@ -144,7 +179,9 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModificatio
project.save!
end
- include_examples 'successful examples'
+ include_examples 'successful examples', model: Ci::Pipeline
+
+ include_examples 'cross-database modification errors', model: Project
end
describe '.allow_cross_database_modification_within_transaction' do
diff --git a/spec/lib/gitlab/database/with_lock_retries_outside_transaction_spec.rb b/spec/lib/gitlab/database/with_lock_retries_outside_transaction_spec.rb
index 0282a7af0df..6c32fb3ca17 100644
--- a/spec/lib/gitlab/database/with_lock_retries_outside_transaction_spec.rb
+++ b/spec/lib/gitlab/database/with_lock_retries_outside_transaction_spec.rb
@@ -5,7 +5,8 @@ require 'spec_helper'
RSpec.describe Gitlab::Database::WithLockRetriesOutsideTransaction do
let(:env) { {} }
let(:logger) { Gitlab::Database::WithLockRetries::NULL_LOGGER }
- let(:subject) { described_class.new(env: env, logger: logger, timing_configuration: timing_configuration) }
+ let(:subject) { described_class.new(connection: connection, env: env, logger: logger, timing_configuration: timing_configuration) }
+ let(:connection) { ActiveRecord::Base.retrieve_connection }
let(:timing_configuration) do
[
@@ -67,7 +68,7 @@ RSpec.describe Gitlab::Database::WithLockRetriesOutsideTransaction do
WHERE t.relkind = 'r' AND l.mode = 'ExclusiveLock' AND t.relname = '#{Project.table_name}'
"""
- expect(ActiveRecord::Base.connection.execute(check_exclusive_lock_query).to_a).to be_present
+ expect(connection.execute(check_exclusive_lock_query).to_a).to be_present
end
end
@@ -96,8 +97,8 @@ RSpec.describe Gitlab::Database::WithLockRetriesOutsideTransaction do
lock_fiber.resume
end
- ActiveRecord::Base.transaction do
- ActiveRecord::Base.connection.execute("LOCK TABLE #{Project.table_name} in exclusive mode")
+ connection.transaction do
+ connection.execute("LOCK TABLE #{Project.table_name} in exclusive mode")
lock_acquired = true
end
end
@@ -115,7 +116,7 @@ RSpec.describe Gitlab::Database::WithLockRetriesOutsideTransaction do
context 'setting the idle transaction timeout' do
context 'when there is no outer transaction: disable_ddl_transaction! is set in the migration' do
it 'does not disable the idle transaction timeout' do
- allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false)
+ allow(connection).to receive(:transaction_open?).and_return(false)
allow(subject).to receive(:run_block_with_lock_timeout).once.and_raise(ActiveRecord::LockWaitTimeout)
allow(subject).to receive(:run_block_with_lock_timeout).once
@@ -127,7 +128,7 @@ RSpec.describe Gitlab::Database::WithLockRetriesOutsideTransaction do
context 'when there is outer transaction: disable_ddl_transaction! is not set in the migration' do
it 'disables the idle transaction timeout so the code can sleep and retry' do
- allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(true)
+ allow(connection).to receive(:transaction_open?).and_return(true)
n = 0
allow(subject).to receive(:run_block_with_lock_timeout).twice do
@@ -184,8 +185,8 @@ RSpec.describe Gitlab::Database::WithLockRetriesOutsideTransaction do
subject.run(raise_on_exhaustion: true) do
lock_attempts += 1
- ActiveRecord::Base.transaction do
- ActiveRecord::Base.connection.execute("LOCK TABLE #{Project.table_name} in exclusive mode")
+ connection.transaction do
+ connection.execute("LOCK TABLE #{Project.table_name} in exclusive mode")
lock_acquired = true
end
end
@@ -199,11 +200,11 @@ RSpec.describe Gitlab::Database::WithLockRetriesOutsideTransaction do
context 'when statement timeout is reached' do
it 'raises StatementInvalid error' do
lock_acquired = false
- ActiveRecord::Base.connection.execute("SET statement_timeout='100ms'")
+ connection.execute("SET statement_timeout='100ms'")
expect do
subject.run do
- ActiveRecord::Base.connection.execute("SELECT 1 FROM pg_sleep(0.11)") # 110ms
+ connection.execute("SELECT 1 FROM pg_sleep(0.11)") # 110ms
lock_acquired = true
end
end.to raise_error(ActiveRecord::StatementInvalid)
@@ -216,11 +217,11 @@ RSpec.describe Gitlab::Database::WithLockRetriesOutsideTransaction do
context 'restore local database variables' do
it do
- expect { subject.run {} }.not_to change { ActiveRecord::Base.connection.execute("SHOW lock_timeout").to_a }
+ expect { subject.run {} }.not_to change { connection.execute("SHOW lock_timeout").to_a }
end
it do
- expect { subject.run {} }.not_to change { ActiveRecord::Base.connection.execute("SHOW idle_in_transaction_session_timeout").to_a }
+ expect { subject.run {} }.not_to change { connection.execute("SHOW idle_in_transaction_session_timeout").to_a }
end
end
@@ -228,8 +229,8 @@ RSpec.describe Gitlab::Database::WithLockRetriesOutsideTransaction do
let(:timing_configuration) { [[0.015.seconds, 0.025.seconds], [0.015.seconds, 0.025.seconds]] } # 15ms, 25ms
it 'executes `SET lock_timeout` using the configured timeout value in milliseconds' do
- expect(ActiveRecord::Base.connection).to receive(:execute).with('RESET idle_in_transaction_session_timeout; RESET lock_timeout').and_call_original
- expect(ActiveRecord::Base.connection).to receive(:execute).with("SET lock_timeout TO '15ms'").and_call_original
+ expect(connection).to receive(:execute).with('RESET idle_in_transaction_session_timeout; RESET lock_timeout').and_call_original
+ expect(connection).to receive(:execute).with("SET lock_timeout TO '15ms'").and_call_original
subject.run { }
end
diff --git a/spec/lib/gitlab/database/with_lock_retries_spec.rb b/spec/lib/gitlab/database/with_lock_retries_spec.rb
index c2c818aa106..6b35ccafabc 100644
--- a/spec/lib/gitlab/database/with_lock_retries_spec.rb
+++ b/spec/lib/gitlab/database/with_lock_retries_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Gitlab::Database::WithLockRetries do
let(:env) { {} }
let(:logger) { Gitlab::Database::WithLockRetries::NULL_LOGGER }
- let(:subject) { described_class.new(env: env, logger: logger, allow_savepoints: allow_savepoints, timing_configuration: timing_configuration) }
+ let(:subject) { described_class.new(connection: connection, env: env, logger: logger, allow_savepoints: allow_savepoints, timing_configuration: timing_configuration) }
let(:allow_savepoints) { true }
let(:connection) { ActiveRecord::Base.retrieve_connection }
diff --git a/spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb b/spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb
index f5ea660ee1e..6601b6658d5 100644
--- a/spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb
+++ b/spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb
@@ -60,7 +60,7 @@ RSpec.describe Gitlab::DatabaseImporters::SelfMonitoring::Project::CreateService
before do
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
- application_setting.update(allow_local_requests_from_web_hooks_and_services: true)
+ application_setting.update!(allow_local_requests_from_web_hooks_and_services: true)
end
shared_examples 'has prometheus integration' do |server_address|
@@ -181,7 +181,7 @@ RSpec.describe Gitlab::DatabaseImporters::SelfMonitoring::Project::CreateService
let(:existing_project) { create(:project, namespace: existing_group) }
before do
- application_setting.update(instance_administrators_group_id: existing_group.id,
+ application_setting.update!(instance_administrators_group_id: existing_group.id,
self_monitoring_project_id: existing_project.id)
end
@@ -195,7 +195,7 @@ RSpec.describe Gitlab::DatabaseImporters::SelfMonitoring::Project::CreateService
context 'when local requests from hooks and integrations are not allowed' do
before do
- application_setting.update(allow_local_requests_from_web_hooks_and_services: false)
+ application_setting.update!(allow_local_requests_from_web_hooks_and_services: false)
end
it_behaves_like 'has prometheus integration', 'http://localhost:9090'
diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb
index 5ec7c338a2a..b3b7c81e9e7 100644
--- a/spec/lib/gitlab/database_spec.rb
+++ b/spec/lib/gitlab/database_spec.rb
@@ -104,6 +104,34 @@ RSpec.describe Gitlab::Database do
end
end
+ describe '.check_for_non_superuser' do
+ subject { described_class.check_for_non_superuser }
+
+ let(:non_superuser) { Gitlab::Database::PgUser.new(usename: 'foo', usesuper: false ) }
+ let(:superuser) { Gitlab::Database::PgUser.new(usename: 'bar', usesuper: true) }
+
+ it 'prints user details if not superuser' do
+ allow(Gitlab::Database::PgUser).to receive(:find_by).with('usename = CURRENT_USER').and_return(non_superuser)
+
+ expect(Gitlab::AppLogger).to receive(:info).with("Account details: User: \"foo\", UseSuper: (false)")
+
+ subject
+ end
+
+ it 'raises an exception if superuser' do
+ allow(Gitlab::Database::PgUser).to receive(:find_by).with('usename = CURRENT_USER').and_return(superuser)
+
+ expect(Gitlab::AppLogger).to receive(:info).with("Account details: User: \"bar\", UseSuper: (true)")
+ expect { subject }.to raise_error('Error: detected superuser')
+ end
+
+ it 'catches exception if find_by fails' do
+ allow(Gitlab::Database::PgUser).to receive(:find_by).with('usename = CURRENT_USER').and_raise(ActiveRecord::StatementInvalid)
+
+ expect { subject }.to raise_error('User CURRENT_USER not found')
+ end
+ end
+
describe '.check_postgres_version_and_print_warning' do
let(:reflect) { instance_spy(Gitlab::Database::Reflection) }
diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb
index 45a49a36fe2..7c1a8f4c3c8 100644
--- a/spec/lib/gitlab/diff/file_spec.rb
+++ b/spec/lib/gitlab/diff/file_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe Gitlab::Diff::File do
def create_file(file_name, content)
Files::CreateService.new(
project,
- project.owner,
+ project.first_owner,
commit_message: 'Update',
start_branch: branch_name,
branch_name: branch_name,
@@ -27,7 +27,7 @@ RSpec.describe Gitlab::Diff::File do
def update_file(file_name, content)
Files::UpdateService.new(
project,
- project.owner,
+ project.first_owner,
commit_message: 'Update',
start_branch: branch_name,
branch_name: branch_name,
@@ -41,7 +41,7 @@ RSpec.describe Gitlab::Diff::File do
def delete_file(file_name)
Files::DeleteService.new(
project,
- project.owner,
+ project.first_owner,
commit_message: 'Update',
start_branch: branch_name,
branch_name: branch_name,
diff --git a/spec/lib/gitlab/diff/position_tracer/image_strategy_spec.rb b/spec/lib/gitlab/diff/position_tracer/image_strategy_spec.rb
index 7dceb64b776..1414056ad6a 100644
--- a/spec/lib/gitlab/diff/position_tracer/image_strategy_spec.rb
+++ b/spec/lib/gitlab/diff/position_tracer/image_strategy_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe Gitlab::Diff::PositionTracer::ImageStrategy do
include PositionTracerHelpers
let(:project) { create(:project, :repository) }
- let(:current_user) { project.owner }
+ let(:current_user) { project.first_owner }
let(:file_name) { 'test-file' }
let(:new_file_name) { "#{file_name}-new" }
let(:second_file_name) { "#{file_name}-2" }
diff --git a/spec/lib/gitlab/diff/position_tracer/line_strategy_spec.rb b/spec/lib/gitlab/diff/position_tracer/line_strategy_spec.rb
index c46f476899e..ea56a87dec2 100644
--- a/spec/lib/gitlab/diff/position_tracer/line_strategy_spec.rb
+++ b/spec/lib/gitlab/diff/position_tracer/line_strategy_spec.rb
@@ -55,7 +55,7 @@ RSpec.describe Gitlab::Diff::PositionTracer::LineStrategy, :clean_gitlab_redis_c
include PositionTracerHelpers
let(:project) { create(:project, :repository) }
- let(:current_user) { project.owner }
+ let(:current_user) { project.first_owner }
let(:repository) { project.repository }
let(:file_name) { "test-file" }
let(:new_file_name) { "#{file_name}-new" }
diff --git a/spec/lib/gitlab/diff/position_tracer_spec.rb b/spec/lib/gitlab/diff/position_tracer_spec.rb
index fc649812b0a..9b0ea892f91 100644
--- a/spec/lib/gitlab/diff/position_tracer_spec.rb
+++ b/spec/lib/gitlab/diff/position_tracer_spec.rb
@@ -52,7 +52,7 @@ RSpec.describe Gitlab::Diff::PositionTracer do
describe 'diffs methods' do
let(:project) { create(:project, :repository) }
- let(:current_user) { project.owner }
+ let(:current_user) { project.first_owner }
let(:old_diff_refs) do
diff_refs(
diff --git a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
index c0ac40e3249..59b87c5d8e7 100644
--- a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Gitlab::Email::Handler::CreateNoteHandler do
include_context :email_shared_context
- let_it_be(:user) { create(:user) }
+ let_it_be(:user) { create(:user, email: 'jake@adventuretime.ooo') }
let_it_be(:project) { create(:project, :public, :repository) }
let(:noteable) { note.noteable }
@@ -39,6 +39,43 @@ RSpec.describe Gitlab::Email::Handler::CreateNoteHandler do
end
end
+ context 'when the incoming email is from a different email address' do
+ before do
+ SentNotification.find_by(reply_key: mail_key).update!(recipient: original_recipient)
+ end
+
+ context 'when the issue is not a Service Desk issue' do
+ let(:original_recipient) { create(:user, email: 'john@somethingelse.com') }
+
+ context 'with only one email address' do
+ it 'raises a UserNotFoundError' do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::UserNotFoundError)
+ end
+ end
+
+ context 'with a secondary verified email address' do
+ let(:verified_email) { 'alan@adventuretime.ooo'}
+ let(:email_raw) { fixture_file('emails/valid_reply.eml').gsub('jake@adventuretime.ooo', verified_email) }
+
+ before do
+ create(:email, :confirmed, user: original_recipient, email: verified_email)
+ end
+
+ it 'does not raise a UserNotFoundError' do
+ expect { receiver.execute }.not_to raise_error(Gitlab::Email::UserNotFoundError)
+ end
+ end
+ end
+
+ context 'when the issue is a Service Desk issue' do
+ let(:original_recipient) { User.support_bot }
+
+ it 'does not raise a UserNotFoundError' do
+ expect { receiver.execute }.not_to raise_error(Gitlab::Email::UserNotFoundError)
+ end
+ end
+ end
+
context 'when no sent notification for the mail key could be found' do
let(:email_raw) { fixture_file('emails/wrong_mail_key.eml') }
diff --git a/spec/lib/gitlab/endpoint_attributes_spec.rb b/spec/lib/gitlab/endpoint_attributes_spec.rb
index 4d4cfed57fa..53f5b302f05 100644
--- a/spec/lib/gitlab/endpoint_attributes_spec.rb
+++ b/spec/lib/gitlab/endpoint_attributes_spec.rb
@@ -1,8 +1,9 @@
# frozen_string_literal: true
require 'fast_spec_helper'
-require_relative "../../support/matchers/be_request_urgency"
-require_relative "../../../lib/gitlab/endpoint_attributes"
+require_relative '../../support/matchers/be_request_urgency'
+require_relative '../../../lib/gitlab/endpoint_attributes/config'
+require_relative '../../../lib/gitlab/endpoint_attributes'
RSpec.describe Gitlab::EndpointAttributes do
let(:base_controller) do
diff --git a/spec/lib/gitlab/error_tracking/context_payload_generator_spec.rb b/spec/lib/gitlab/error_tracking/context_payload_generator_spec.rb
index 0e72dd7ec5e..38745fe0cde 100644
--- a/spec/lib/gitlab/error_tracking/context_payload_generator_spec.rb
+++ b/spec/lib/gitlab/error_tracking/context_payload_generator_spec.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
-require 'rspec-parameterized'
+require 'spec_helper'
RSpec.describe Gitlab::ErrorTracking::ContextPayloadGenerator do
subject(:generator) { described_class.new }
diff --git a/spec/lib/gitlab/error_tracking/log_formatter_spec.rb b/spec/lib/gitlab/error_tracking/log_formatter_spec.rb
index 188ccd000a1..15d201401f4 100644
--- a/spec/lib/gitlab/error_tracking/log_formatter_spec.rb
+++ b/spec/lib/gitlab/error_tracking/log_formatter_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'spec_helper'
RSpec.describe Gitlab::ErrorTracking::LogFormatter do
let(:exception) { StandardError.new('boom') }
diff --git a/spec/lib/gitlab/event_store/store_spec.rb b/spec/lib/gitlab/event_store/store_spec.rb
index 711e1d5b4d5..94e8f0ff2ff 100644
--- a/spec/lib/gitlab/event_store/store_spec.rb
+++ b/spec/lib/gitlab/event_store/store_spec.rb
@@ -10,7 +10,6 @@ RSpec.describe Gitlab::EventStore::Store do
let(:worker) do
stub_const('EventSubscriber', Class.new).tap do |klass|
klass.class_eval do
- include ApplicationWorker
include Gitlab::EventStore::Subscriber
def handle_event(event)
@@ -23,7 +22,6 @@ RSpec.describe Gitlab::EventStore::Store do
let(:another_worker) do
stub_const('AnotherEventSubscriber', Class.new).tap do |klass|
klass.class_eval do
- include ApplicationWorker
include Gitlab::EventStore::Subscriber
end
end
@@ -32,7 +30,6 @@ RSpec.describe Gitlab::EventStore::Store do
let(:unrelated_worker) do
stub_const('UnrelatedEventSubscriber', Class.new).tap do |klass|
klass.class_eval do
- include ApplicationWorker
include Gitlab::EventStore::Subscriber
end
end
@@ -224,6 +221,26 @@ RSpec.describe Gitlab::EventStore::Store do
store.publish(event)
end
end
+
+ context 'when the event does not have any subscribers' do
+ let(:store) do
+ described_class.new do |s|
+ s.subscribe unrelated_worker, to: another_event_klass
+ end
+ end
+
+ let(:event) { event_klass.new(data: data) }
+
+ it 'returns successfully' do
+ expect { store.publish(event) }.not_to raise_error
+ end
+
+ it 'does not dispatch the event to another subscription' do
+ expect(unrelated_worker).not_to receive(:perform_async)
+
+ store.publish(event)
+ end
+ end
end
describe 'subscriber' do
@@ -233,6 +250,10 @@ RSpec.describe Gitlab::EventStore::Store do
subject { worker_instance.perform(event_name, data) }
+ it 'is a Sidekiq worker' do
+ expect(worker_instance).to be_a(ApplicationWorker)
+ end
+
it 'handles the event' do
expect(worker_instance).to receive(:handle_event).with(instance_of(event.class))
diff --git a/spec/lib/gitlab/experiment/rollout/feature_spec.rb b/spec/lib/gitlab/experiment/rollout/feature_spec.rb
new file mode 100644
index 00000000000..d73757be79b
--- /dev/null
+++ b/spec/lib/gitlab/experiment/rollout/feature_spec.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Experiment::Rollout::Feature, :experiment do
+ subject { described_class.new.for(subject_experiment) }
+
+ let(:subject_experiment) { experiment('namespaced/stub') }
+
+ describe "#enabled?" do
+ before do
+ allow(Feature::Definition).to receive(:get).and_return('_instance_')
+ allow(Gitlab).to receive(:dev_env_or_com?).and_return(true)
+ allow(Feature).to receive(:get).and_return(double(state: :on))
+ end
+
+ it "is enabled when all criteria are met" do
+ expect(subject).to be_enabled
+ end
+
+ it "isn't enabled if the feature definition doesn't exist" do
+ expect(Feature::Definition).to receive(:get).with('namespaced_stub').and_return(nil)
+
+ expect(subject).not_to be_enabled
+ end
+
+ it "isn't enabled if we're not in dev or dotcom environments" do
+ expect(Gitlab).to receive(:dev_env_or_com?).and_return(false)
+
+ expect(subject).not_to be_enabled
+ end
+
+ it "isn't enabled if the feature flag state is :off" do
+ expect(Feature).to receive(:get).with('namespaced_stub').and_return(double(state: :off))
+
+ expect(subject).not_to be_enabled
+ end
+ end
+
+ describe "#execute_assignment" do
+ before do
+ allow(Feature).to receive(:enabled?).with('namespaced_stub', any_args).and_return(true)
+ end
+
+ it "uses the default value as specified in the yaml" do
+ expect(Feature).to receive(:enabled?).with(
+ 'namespaced_stub',
+ subject,
+ type: :experiment,
+ default_enabled: :yaml
+ ).and_return(false)
+
+ expect(subject.execute_assignment).to be_nil
+ end
+
+ it "returns an assigned name" do
+ allow(subject).to receive(:behavior_names).and_return([:variant1, :variant2])
+
+ expect(subject.execute_assignment).to eq(:variant2)
+ end
+ end
+
+ describe "#flipper_id" do
+ it "returns the expected flipper id if the experiment doesn't provide one" do
+ subject.instance_variable_set(:@experiment, double(id: '__id__'))
+ expect(subject.flipper_id).to eq('Experiment;__id__')
+ end
+
+ it "lets the experiment provide a flipper id so it can override the default" do
+ allow(subject_experiment).to receive(:flipper_id).and_return('_my_overridden_id_')
+
+ expect(subject.flipper_id).to eq('_my_overridden_id_')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/feature_categories_spec.rb b/spec/lib/gitlab/feature_categories_spec.rb
index daced154a69..477da900d0a 100644
--- a/spec/lib/gitlab/feature_categories_spec.rb
+++ b/spec/lib/gitlab/feature_categories_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'spec_helper'
RSpec.describe Gitlab::FeatureCategories do
let(:fake_categories) { %w(foo bar) }
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index f1b6a59abf9..ae6ca728573 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -2252,44 +2252,6 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
end
end
- describe '#clean_stale_repository_files' do
- let(:worktree_id) { 'rebase-1' }
- let(:gitlab_worktree_path) { File.join(repository_path, 'gitlab-worktree', worktree_id) }
- let(:admin_dir) { File.join(repository_path, 'worktrees') }
-
- it 'cleans up the files' do
- create_worktree = %W[git -C #{repository_path} worktree add --detach #{gitlab_worktree_path} master]
- raise 'preparation failed' unless system(*create_worktree, err: '/dev/null')
-
- FileUtils.touch(gitlab_worktree_path, mtime: Time.now - 8.hours)
- # git rev-list --all will fail in git 2.16 if HEAD is pointing to a non-existent object,
- # but the HEAD must be 40 characters long or git will ignore it.
- File.write(File.join(admin_dir, worktree_id, 'HEAD'), Gitlab::Git::BLANK_SHA)
-
- expect(rev_list_all).to be(false)
- repository.clean_stale_repository_files
-
- expect(rev_list_all).to be(true)
- expect(File.exist?(gitlab_worktree_path)).to be_falsey
- end
-
- def rev_list_all
- system(*%W[git -C #{repository_path} rev-list --all], out: '/dev/null', err: '/dev/null')
- end
-
- it 'increments a counter upon an error' do
- expect(repository.gitaly_repository_client).to receive(:cleanup).and_raise(Gitlab::Git::CommandError)
-
- counter = double(:counter)
-
- expect(counter).to receive(:increment)
- expect(Gitlab::Metrics).to receive(:counter).with(:failed_repository_cleanup_total,
- 'Number of failed repository cleanup events').and_return(counter)
-
- repository.clean_stale_repository_files
- end
- end
-
describe '#squash' do
let(:branch_name) { 'fix' }
let(:start_sha) { '4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6' }
diff --git a/spec/lib/gitlab/git/wiki_spec.rb b/spec/lib/gitlab/git/wiki_spec.rb
index eb7deb08063..ee0c0e2708e 100644
--- a/spec/lib/gitlab/git/wiki_spec.rb
+++ b/spec/lib/gitlab/git/wiki_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe Gitlab::Git::Wiki do
using RSpec::Parameterized::TableSyntax
let(:project) { create(:project) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:project_wiki) { ProjectWiki.new(project, user) }
subject(:wiki) { project_wiki.wiki }
diff --git a/spec/lib/gitlab/git_access_design_spec.rb b/spec/lib/gitlab/git_access_design_spec.rb
index 9fd1f2dcb0c..c90d9802300 100644
--- a/spec/lib/gitlab/git_access_design_spec.rb
+++ b/spec/lib/gitlab/git_access_design_spec.rb
@@ -5,7 +5,7 @@ RSpec.describe Gitlab::GitAccessDesign do
include DesignManagementTestHelpers
let_it_be(:project) { create(:project) }
- let_it_be(:user) { project.owner }
+ let_it_be(:user) { project.first_owner }
let(:protocol) { 'web' }
let(:actor) { user }
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index 4bf7994f4dd..d6ef1836ad9 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -1008,11 +1008,6 @@ RSpec.describe Gitlab::GitAccess do
end
end
- it 'cleans up the files' do
- expect(project.repository).to receive(:clean_stale_repository_files).and_call_original
- expect { push_access_check }.not_to raise_error
- end
-
it 'avoids N+1 queries', :request_store do
# Run this once to establish a baseline. Cached queries should get
# cached, so that when we introduce another change we shouldn't see
diff --git a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
index 27e7d446770..f0115aa6b2b 100644
--- a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
@@ -2,6 +2,9 @@
require 'spec_helper'
+require 'google/rpc/status_pb'
+require 'google/protobuf/well_known_types'
+
RSpec.describe Gitlab::GitalyClient::OperationService do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository) }
@@ -185,11 +188,16 @@ RSpec.describe Gitlab::GitalyClient::OperationService do
context 'with an exception with the UserMergeBranchError' do
let(:permission_error) do
- GRPC::PermissionDenied.new(
+ new_detailed_error(
+ GRPC::Core::StatusCodes::PERMISSION_DENIED,
"GitLab: You are not allowed to push code to this project.",
- { "grpc-status-details-bin" =>
- "\b\a\x129GitLab: You are not allowed to push code to this project.\x1A\xDE\x01\n/type.googleapis.com/gitaly.UserMergeBranchError\x12\xAA\x01\n\xA7\x01\n1You are not allowed to push code to this project.\x12\x03web\x1A\auser-15\"df15b32277d2c55c6c595845a87109b09c913c556 5d6e0f935ad9240655f64e883cd98fad6f9a17ee refs/heads/master\n" }
- )
+ Gitaly::UserMergeBranchError.new(
+ access_check: Gitaly::AccessCheckError.new(
+ error_message: "You are not allowed to push code to this project.",
+ protocol: "web",
+ user_id: "user-15",
+ changes: "df15b32277d2c55c6c595845a87109b09c913c556 5d6e0f935ad9240655f64e883cd98fad6f9a17ee refs/heads/master\n"
+ )))
end
it 'raises PreRecieveError with the error message' do
@@ -217,6 +225,27 @@ RSpec.describe Gitlab::GitalyClient::OperationService do
expect { subject }.to raise_error(GRPC::PermissionDenied)
end
end
+
+ context 'with ReferenceUpdateError' do
+ let(:reference_update_error) do
+ new_detailed_error(GRPC::Core::StatusCodes::FAILED_PRECONDITION,
+ "some ignored error message",
+ Gitaly::UserMergeBranchError.new(
+ reference_update: Gitaly::ReferenceUpdateError.new(
+ reference_name: "refs/heads/something",
+ old_oid: "1234",
+ new_oid: "6789"
+ )))
+ end
+
+ it 'returns nil' do
+ expect_any_instance_of(Gitaly::OperationService::Stub)
+ .to receive(:user_merge_branch).with(kind_of(Enumerator), kind_of(Hash))
+ .and_raise(reference_update_error)
+
+ expect(subject).to be_nil
+ end
+ end
end
describe '#user_ff_branch' do
@@ -478,4 +507,14 @@ RSpec.describe Gitlab::GitalyClient::OperationService do
end
end
end
+
+ def new_detailed_error(error_code, error_message, details)
+ status_error = Google::Rpc::Status.new(
+ code: error_code,
+ message: error_message,
+ details: [Google::Protobuf::Any.pack(details)]
+ )
+
+ GRPC::BadStatus.new(error_code, error_message, { "grpc-status-details-bin" => Google::Rpc::Status.encode(status_error) })
+ end
end
diff --git a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
index e5502a883b5..353726b56f6 100644
--- a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
@@ -21,16 +21,6 @@ RSpec.describe Gitlab::GitalyClient::RepositoryService do
end
end
- describe '#cleanup' do
- it 'sends a cleanup message' do
- expect_any_instance_of(Gitaly::RepositoryService::Stub)
- .to receive(:cleanup)
- .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
-
- client.cleanup
- end
- end
-
describe '#garbage_collect' do
it 'sends a garbage_collect message' do
expect_any_instance_of(Gitaly::RepositoryService::Stub)
diff --git a/spec/lib/gitlab/github_import/importer/diff_note_importer_spec.rb b/spec/lib/gitlab/github_import/importer/diff_note_importer_spec.rb
index a0e78186caa..c8e744ab262 100644
--- a/spec/lib/gitlab/github_import/importer/diff_note_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/diff_note_importer_spec.rb
@@ -119,123 +119,80 @@ RSpec.describe Gitlab::GithubImport::Importer::DiffNoteImporter, :aggregate_fail
.and_return(discussion_id)
end
- context 'when github_importer_use_diff_note_with_suggestions is disabled' do
- before do
- stub_feature_flags(github_importer_use_diff_note_with_suggestions: false)
+ it_behaves_like 'diff notes without suggestion'
+
+ context 'when the note has suggestions' do
+ let(:note_body) do
+ <<~EOB
+ Suggestion:
+ ```suggestion
+ what do you think to do it like this
+ ```
+ EOB
end
- it_behaves_like 'diff notes without suggestion'
+ before do
+ stub_user_finder(user.id, true)
+ end
- context 'when the note has suggestions' do
- let(:note_body) do
- <<~EOB
+ it 'imports the note as diff note' do
+ expect { subject.execute }
+ .to change(DiffNote, :count)
+ .by(1)
+
+ note = project.notes.diff_notes.take
+ expect(note).to be_valid
+ expect(note.noteable_type).to eq('MergeRequest')
+ expect(note.noteable_id).to eq(merge_request.id)
+ expect(note.project_id).to eq(project.id)
+ expect(note.author_id).to eq(user.id)
+ expect(note.system).to eq(false)
+ expect(note.discussion_id).to eq(discussion_id)
+ expect(note.commit_id).to eq('original123abc')
+ expect(note.line_code).to eq(note_representation.line_code)
+ expect(note.type).to eq('DiffNote')
+ expect(note.created_at).to eq(created_at)
+ expect(note.updated_at).to eq(updated_at)
+ expect(note.position.to_h).to eq({
+ base_sha: merge_request.diffs.diff_refs.base_sha,
+ head_sha: merge_request.diffs.diff_refs.head_sha,
+ start_sha: merge_request.diffs.diff_refs.start_sha,
+ new_line: 15,
+ old_line: nil,
+ new_path: file_path,
+ old_path: file_path,
+ position_type: 'text',
+ line_range: nil
+ })
+ expect(note.note)
+ .to eq <<~NOTE
Suggestion:
- ```suggestion
+ ```suggestion:-0+0
what do you think to do it like this
```
- EOB
- end
-
- it 'imports the note' do
- stub_user_finder(user.id, true)
-
- expect { subject.execute }
- .to change(LegacyDiffNote, :count)
- .and not_change(DiffNote, :count)
-
- note = project.notes.diff_notes.take
- expect(note).to be_valid
- expect(note.note)
- .to eq <<~NOTE
- Suggestion:
- ```suggestion:-0+0
- what do you think to do it like this
- ```
- NOTE
- end
- end
- end
-
- context 'when github_importer_use_diff_note_with_suggestions is enabled' do
- before do
- stub_feature_flags(github_importer_use_diff_note_with_suggestions: true)
+ NOTE
end
- it_behaves_like 'diff notes without suggestion'
+ context 'when the note diff file creation fails' do
+ it 'falls back to the LegacyDiffNote' do
+ exception = ::DiffNote::NoteDiffFileCreationError.new('Failed to create diff note file')
- context 'when the note has suggestions' do
- let(:note_body) do
- <<~EOB
- Suggestion:
- ```suggestion
- what do you think to do it like this
- ```
- EOB
- end
+ expect_next_instance_of(::Import::Github::Notes::CreateService) do |service|
+ expect(service)
+ .to receive(:execute)
+ .and_raise(exception)
+ end
- before do
- stub_user_finder(user.id, true)
- end
+ expect(Gitlab::GithubImport::Logger)
+ .to receive(:warn)
+ .with(
+ message: 'Failed to create diff note file',
+ 'error.class': 'DiffNote::NoteDiffFileCreationError'
+ )
- it 'imports the note as diff note' do
expect { subject.execute }
- .to change(DiffNote, :count)
- .by(1)
-
- note = project.notes.diff_notes.take
- expect(note).to be_valid
- expect(note.noteable_type).to eq('MergeRequest')
- expect(note.noteable_id).to eq(merge_request.id)
- expect(note.project_id).to eq(project.id)
- expect(note.author_id).to eq(user.id)
- expect(note.system).to eq(false)
- expect(note.discussion_id).to eq(discussion_id)
- expect(note.commit_id).to eq('original123abc')
- expect(note.line_code).to eq(note_representation.line_code)
- expect(note.type).to eq('DiffNote')
- expect(note.created_at).to eq(created_at)
- expect(note.updated_at).to eq(updated_at)
- expect(note.position.to_h).to eq({
- base_sha: merge_request.diffs.diff_refs.base_sha,
- head_sha: merge_request.diffs.diff_refs.head_sha,
- start_sha: merge_request.diffs.diff_refs.start_sha,
- new_line: 15,
- old_line: nil,
- new_path: file_path,
- old_path: file_path,
- position_type: 'text',
- line_range: nil
- })
- expect(note.note)
- .to eq <<~NOTE
- Suggestion:
- ```suggestion:-0+0
- what do you think to do it like this
- ```
- NOTE
- end
-
- context 'when the note diff file creation fails' do
- it 'falls back to the LegacyDiffNote' do
- exception = ::DiffNote::NoteDiffFileCreationError.new('Failed to create diff note file')
-
- expect_next_instance_of(::Import::Github::Notes::CreateService) do |service|
- expect(service)
- .to receive(:execute)
- .and_raise(exception)
- end
-
- expect(Gitlab::GithubImport::Logger)
- .to receive(:warn)
- .with(
- message: 'Failed to create diff note file',
- 'error.class': 'DiffNote::NoteDiffFileCreationError'
- )
-
- expect { subject.execute }
- .to change(LegacyDiffNote, :count)
- .and not_change(DiffNote, :count)
- end
+ .to change(LegacyDiffNote, :count)
+ .and not_change(DiffNote, :count)
end
end
end
diff --git a/spec/lib/gitlab/github_import/importer/releases_importer_spec.rb b/spec/lib/gitlab/github_import/importer/releases_importer_spec.rb
index 1a25824bc8a..6b3d18f20e9 100644
--- a/spec/lib/gitlab/github_import/importer/releases_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/releases_importer_spec.rb
@@ -52,6 +52,12 @@ RSpec.describe Gitlab::GithubImport::Importer::ReleasesImporter do
expect { importer.execute }.to change { Release.count }.by(1)
end
+
+ it 'is idempotent' do
+ allow(importer).to receive(:each_release).and_return([github_release])
+ expect { importer.execute }.to change { Release.count }.by(1)
+ expect { importer.execute }.to change { Release.count }.by(0) # Idempotency check
+ end
end
describe '#build_releases' do
@@ -79,6 +85,24 @@ RSpec.describe Gitlab::GithubImport::Importer::ReleasesImporter do
expect(release[:description]).to eq('Release for tag 1.0')
end
+
+ it 'does not create releases that have a NULL tag' do
+ null_tag_release = double(
+ name: 'NULL Test',
+ tag_name: nil
+ )
+
+ expect(importer).to receive(:each_release).and_return([null_tag_release])
+ expect(importer.build_releases).to be_empty
+ end
+
+ it 'does not create duplicate release tags' do
+ expect(importer).to receive(:each_release).and_return([github_release, github_release])
+
+ releases = importer.build_releases
+ expect(releases.length).to eq(1)
+ expect(releases[0][:description]).to eq('This is my release')
+ end
end
describe '#build' do
diff --git a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
index 58a8fb1b7e4..f2730ba74ec 100644
--- a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
@@ -264,8 +264,8 @@ RSpec.describe Gitlab::GithubImport::Importer::RepositoryImporter do
it 'sets the timestamp for when the cloning process finished' do
freeze_time do
expect(project)
- .to receive(:update_column)
- .with(:last_repository_updated_at, Time.zone.now)
+ .to receive(:touch)
+ .with(:last_repository_updated_at)
importer.update_clone_time
end
diff --git a/spec/lib/gitlab/github_import/representation/diff_note_spec.rb b/spec/lib/gitlab/github_import/representation/diff_note_spec.rb
index 63834cfdb94..fe3040c102b 100644
--- a/spec/lib/gitlab/github_import/representation/diff_note_spec.rb
+++ b/spec/lib/gitlab/github_import/representation/diff_note_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::Representation::DiffNote, :clean_gitlab_redis_shared_state do
+RSpec.describe Gitlab::GithubImport::Representation::DiffNote, :clean_gitlab_redis_cache do
let(:hunk) do
'@@ -1 +1 @@
-Hello
@@ -166,6 +166,23 @@ RSpec.describe Gitlab::GithubImport::Representation::DiffNote, :clean_gitlab_red
expect(new_discussion_note.discussion_id)
.to eq('SECOND_DISCUSSION_ID')
end
+
+ context 'when cached value does not exist' do
+ it 'falls back to generating a new discussion_id' do
+ expect(Discussion)
+ .to receive(:discussion_id)
+ .and_return('NEW_DISCUSSION_ID')
+
+ reply_note = described_class.from_json_hash(
+ 'note_id' => note.note_id + 1,
+ 'in_reply_to_id' => note.note_id
+ )
+ reply_note.project = project
+ reply_note.merge_request = merge_request
+
+ expect(reply_note.discussion_id).to eq('NEW_DISCUSSION_ID')
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/gl_repository/identifier_spec.rb b/spec/lib/gitlab/gl_repository/identifier_spec.rb
index e0622e30e7a..0a8559dd800 100644
--- a/spec/lib/gitlab/gl_repository/identifier_spec.rb
+++ b/spec/lib/gitlab/gl_repository/identifier_spec.rb
@@ -4,8 +4,8 @@ require 'spec_helper'
RSpec.describe Gitlab::GlRepository::Identifier do
let_it_be(:project) { create(:project) }
- let_it_be(:personal_snippet) { create(:personal_snippet, author: project.owner) }
- let_it_be(:project_snippet) { create(:project_snippet, project: project, author: project.owner) }
+ let_it_be(:personal_snippet) { create(:personal_snippet, author: project.first_owner) }
+ let_it_be(:project_snippet) { create(:project_snippet, project: project, author: project.first_owner) }
describe 'project repository' do
it_behaves_like 'parsing gl_repository identifier' do
diff --git a/spec/lib/gitlab/gl_repository/repo_type_spec.rb b/spec/lib/gitlab/gl_repository/repo_type_spec.rb
index 71a4c693f9d..0ec94563cbb 100644
--- a/spec/lib/gitlab/gl_repository/repo_type_spec.rb
+++ b/spec/lib/gitlab/gl_repository/repo_type_spec.rb
@@ -3,8 +3,8 @@ require 'spec_helper'
RSpec.describe Gitlab::GlRepository::RepoType do
let_it_be(:project) { create(:project) }
- let_it_be(:personal_snippet) { create(:personal_snippet, author: project.owner) }
- let_it_be(:project_snippet) { create(:project_snippet, project: project, author: project.owner) }
+ let_it_be(:personal_snippet) { create(:personal_snippet, author: project.first_owner) }
+ let_it_be(:project_snippet) { create(:project_snippet, project: project, author: project.first_owner) }
let(:project_path) { project.repository.full_path }
let(:wiki_path) { project.wiki.repository.full_path }
diff --git a/spec/lib/gitlab/gon_helper_spec.rb b/spec/lib/gitlab/gon_helper_spec.rb
index b8ed4cf608d..047873d8237 100644
--- a/spec/lib/gitlab/gon_helper_spec.rb
+++ b/spec/lib/gitlab/gon_helper_spec.rb
@@ -6,9 +6,41 @@ RSpec.describe Gitlab::GonHelper do
let(:helper) do
Class.new do
include Gitlab::GonHelper
+
+ def current_user
+ nil
+ end
end.new
end
+ describe '#add_gon_variables' do
+ let(:gon) { double('gon').as_null_object }
+ let(:https) { true }
+
+ before do
+ allow(helper).to receive(:gon).and_return(gon)
+ stub_config_setting(https: https)
+ end
+
+ context 'when HTTPS is enabled' do
+ it 'sets the secure flag to true' do
+ expect(gon).to receive(:secure=).with(true)
+
+ helper.add_gon_variables
+ end
+ end
+
+ context 'when HTTP is enabled' do
+ let(:https) { false }
+
+ it 'sets the secure flag to false' do
+ expect(gon).to receive(:secure=).with(false)
+
+ helper.add_gon_variables
+ end
+ end
+ end
+
describe '#push_frontend_feature_flag' do
before do
skip_feature_flags_yaml_validation
diff --git a/spec/lib/gitlab/graphql/authorize/object_authorization_spec.rb b/spec/lib/gitlab/graphql/authorize/object_authorization_spec.rb
index 73e25f23848..274cc83a6be 100644
--- a/spec/lib/gitlab/graphql/authorize/object_authorization_spec.rb
+++ b/spec/lib/gitlab/graphql/authorize/object_authorization_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'spec_helper'
RSpec.describe ::Gitlab::Graphql::Authorize::ObjectAuthorization do
describe '#ok?' do
diff --git a/spec/lib/gitlab/graphql/batch_key_spec.rb b/spec/lib/gitlab/graphql/batch_key_spec.rb
index 7b73b27f24b..43e248885c2 100644
--- a/spec/lib/gitlab/graphql/batch_key_spec.rb
+++ b/spec/lib/gitlab/graphql/batch_key_spec.rb
@@ -1,12 +1,10 @@
# frozen_string_literal: true
require 'fast_spec_helper'
-require 'test_prof/recipes/rspec/let_it_be'
RSpec.describe ::Gitlab::Graphql::BatchKey do
- let_it_be(:rect) { Struct.new(:len, :width) }
- let_it_be(:circle) { Struct.new(:radius) }
-
+ let(:rect) { Struct.new(:len, :width) }
+ let(:circle) { Struct.new(:radius) }
let(:lookahead) { nil }
let(:object) { rect.new(2, 3) }
diff --git a/spec/lib/gitlab/graphql/markdown_field_spec.rb b/spec/lib/gitlab/graphql/markdown_field_spec.rb
index a3fb0bbbed8..c2253811e91 100644
--- a/spec/lib/gitlab/graphql/markdown_field_spec.rb
+++ b/spec/lib/gitlab/graphql/markdown_field_spec.rb
@@ -73,7 +73,7 @@ RSpec.describe Gitlab::Graphql::MarkdownField do
end
it 'shows the reference to users that are allowed to see it' do
- context = GraphQL::Query::Context.new(query: query, values: { current_user: project.owner }, object: nil)
+ context = GraphQL::Query::Context.new(query: query, values: { current_user: project.first_owner }, object: nil)
type_instance = type_class.authorized_new(note, context)
expect(field.to_graphql.resolve(type_instance, {}, context)).to include(issue_path(issue))
diff --git a/spec/lib/gitlab/graphql/queries_spec.rb b/spec/lib/gitlab/graphql/queries_spec.rb
index 8b7f4ca7933..ad1aaac712e 100644
--- a/spec/lib/gitlab/graphql/queries_spec.rb
+++ b/spec/lib/gitlab/graphql/queries_spec.rb
@@ -1,8 +1,6 @@
# frozen_string_literal: true
require 'spec_helper'
-require 'fast_spec_helper'
-require "test_prof/recipes/rspec/let_it_be"
RSpec.describe Gitlab::Graphql::Queries do
shared_examples 'a valid GraphQL query for the blog schema' do
diff --git a/spec/lib/gitlab/graphql/tracers/application_context_tracer_spec.rb b/spec/lib/gitlab/graphql/tracers/application_context_tracer_spec.rb
index 6eff816b95a..264fe993197 100644
--- a/spec/lib/gitlab/graphql/tracers/application_context_tracer_spec.rb
+++ b/spec/lib/gitlab/graphql/tracers/application_context_tracer_spec.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
-require "fast_spec_helper"
-require "support/graphql/fake_tracer"
-require "support/graphql/fake_query_type"
+
+require 'spec_helper'
RSpec.describe Gitlab::Graphql::Tracers::ApplicationContextTracer do
let(:tracer_spy) { spy('tracer_spy') }
diff --git a/spec/lib/gitlab/hook_data/project_builder_spec.rb b/spec/lib/gitlab/hook_data/project_builder_spec.rb
index 672dbab918f..e86ac66b1ad 100644
--- a/spec/lib/gitlab/hook_data/project_builder_spec.rb
+++ b/spec/lib/gitlab/hook_data/project_builder_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe Gitlab::HookData::ProjectBuilder do
let(:attributes) do
[
:event_name, :created_at, :updated_at, :name, :path, :path_with_namespace, :project_id,
- :owner_name, :owner_email, :project_visibility
+ :owners, :owner_name, :owner_email, :project_visibility
]
end
@@ -30,6 +30,7 @@ RSpec.describe Gitlab::HookData::ProjectBuilder do
expect(data[:project_id]).to eq(project.id)
expect(data[:owner_name]).to eq('John')
expect(data[:owner_email]).to eq('john@example.com')
+ expect(data[:owners]).to contain_exactly({ name: 'John', email: 'john@example.com' })
expect(data[:project_visibility]).to eq('internal')
end
end
diff --git a/spec/lib/gitlab/http_connection_adapter_spec.rb b/spec/lib/gitlab/http_connection_adapter_spec.rb
index 7c57d162e9b..e9e517f1fe6 100644
--- a/spec/lib/gitlab/http_connection_adapter_spec.rb
+++ b/spec/lib/gitlab/http_connection_adapter_spec.rb
@@ -15,11 +15,33 @@ RSpec.describe Gitlab::HTTPConnectionAdapter do
stub_all_dns('https://example.org', ip_address: '93.184.216.34')
end
+ context 'with use_read_total_timeout option' do
+ let(:options) { { use_read_total_timeout: true } }
+
+ it 'sets up the connection using the Gitlab::NetHttpAdapter' do
+ expect(connection).to be_a(Gitlab::NetHttpAdapter)
+ expect(connection.address).to eq('93.184.216.34')
+ expect(connection.hostname_override).to eq('example.org')
+ expect(connection.addr_port).to eq('example.org')
+ expect(connection.port).to eq(443)
+ end
+ end
+
+ context 'with header_read_timeout_buffered_io feature disabled' do
+ before do
+ stub_feature_flags(header_read_timeout_buffered_io: false)
+ end
+
+ it 'uses the regular Net::HTTP class' do
+ expect(connection).to be_a(Net::HTTP)
+ end
+ end
+
context 'when local requests are allowed' do
let(:options) { { allow_local_requests: true } }
it 'sets up the connection' do
- expect(connection).to be_a(Net::HTTP)
+ expect(connection).to be_a(Gitlab::NetHttpAdapter)
expect(connection.address).to eq('93.184.216.34')
expect(connection.hostname_override).to eq('example.org')
expect(connection.addr_port).to eq('example.org')
@@ -31,7 +53,7 @@ RSpec.describe Gitlab::HTTPConnectionAdapter do
let(:options) { { allow_local_requests: false } }
it 'sets up the connection' do
- expect(connection).to be_a(Net::HTTP)
+ expect(connection).to be_a(Gitlab::NetHttpAdapter)
expect(connection.address).to eq('93.184.216.34')
expect(connection.hostname_override).to eq('example.org')
expect(connection.addr_port).to eq('example.org')
@@ -52,7 +74,7 @@ RSpec.describe Gitlab::HTTPConnectionAdapter do
let(:options) { { allow_local_requests: true } }
it 'sets up the connection' do
- expect(connection).to be_a(Net::HTTP)
+ expect(connection).to be_a(Gitlab::NetHttpAdapter)
expect(connection.address).to eq('172.16.0.0')
expect(connection.hostname_override).to be(nil)
expect(connection.addr_port).to eq('172.16.0.0')
@@ -75,7 +97,7 @@ RSpec.describe Gitlab::HTTPConnectionAdapter do
let(:options) { { allow_local_requests: true } }
it 'sets up the connection' do
- expect(connection).to be_a(Net::HTTP)
+ expect(connection).to be_a(Gitlab::NetHttpAdapter)
expect(connection.address).to eq('127.0.0.1')
expect(connection.hostname_override).to be(nil)
expect(connection.addr_port).to eq('127.0.0.1')
@@ -88,7 +110,7 @@ RSpec.describe Gitlab::HTTPConnectionAdapter do
let(:uri) { URI('https://example.org:8080') }
it 'sets up the addr_port accordingly' do
- expect(connection).to be_a(Net::HTTP)
+ expect(connection).to be_a(Gitlab::NetHttpAdapter)
expect(connection.address).to eq('93.184.216.34')
expect(connection.hostname_override).to eq('example.org')
expect(connection.addr_port).to eq('example.org:8080')
@@ -103,7 +125,7 @@ RSpec.describe Gitlab::HTTPConnectionAdapter do
end
it 'sets up the connection' do
- expect(connection).to be_a(Net::HTTP)
+ expect(connection).to be_a(Gitlab::NetHttpAdapter)
expect(connection.address).to eq('example.org')
expect(connection.hostname_override).to eq(nil)
expect(connection.addr_port).to eq('example.org')
@@ -117,7 +139,7 @@ RSpec.describe Gitlab::HTTPConnectionAdapter do
end
it 'sets up the connection' do
- expect(connection).to be_a(Net::HTTP)
+ expect(connection).to be_a(Gitlab::NetHttpAdapter)
expect(connection.address).to eq('example.org')
expect(connection.hostname_override).to eq(nil)
expect(connection.addr_port).to eq('example.org')
diff --git a/spec/lib/gitlab/http_spec.rb b/spec/lib/gitlab/http_spec.rb
index 7d459f2d88a..7dbd21e6914 100644
--- a/spec/lib/gitlab/http_spec.rb
+++ b/spec/lib/gitlab/http_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe Gitlab::HTTP do
end
context 'when reading the response is too slow' do
- before do
+ before(:all) do
# Override Net::HTTP to add a delay between sending each response chunk
mocked_http = Class.new(Net::HTTP) do
def request(*)
@@ -51,8 +51,17 @@ RSpec.describe Gitlab::HTTP do
end
@original_net_http = Net.send(:remove_const, :HTTP)
+ @webmock_net_http = WebMock::HttpLibAdapters::NetHttpAdapter.instance_variable_get('@webMockNetHTTP')
+
Net.send(:const_set, :HTTP, mocked_http)
+ WebMock::HttpLibAdapters::NetHttpAdapter.instance_variable_set('@webMockNetHTTP', mocked_http)
+
+ # Reload Gitlab::NetHttpAdapter
+ Gitlab.send(:remove_const, :NetHttpAdapter)
+ load "#{Rails.root}/lib/gitlab/net_http_adapter.rb"
+ end
+ before do
stub_const("#{described_class}::DEFAULT_READ_TOTAL_TIMEOUT", 0.001.seconds)
WebMock.stub_request(:post, /.*/).to_return do |request|
@@ -60,9 +69,14 @@ RSpec.describe Gitlab::HTTP do
end
end
- after do
+ after(:all) do
Net.send(:remove_const, :HTTP)
Net.send(:const_set, :HTTP, @original_net_http)
+ WebMock::HttpLibAdapters::NetHttpAdapter.instance_variable_set('@webMockNetHTTP', @webmock_net_http)
+
+ # Reload Gitlab::NetHttpAdapter
+ Gitlab.send(:remove_const, :NetHttpAdapter)
+ load "#{Rails.root}/lib/gitlab/net_http_adapter.rb"
end
let(:options) { {} }
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index f4a112d35aa..ce13f405459 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -529,6 +529,7 @@ project:
- vulnerability_feedback
- vulnerability_identifiers
- vulnerability_scanners
+- dast_profiles
- dast_site_profiles
- dast_scanner_profiles
- dast_sites
@@ -605,6 +606,7 @@ project:
- ci_project_mirror
- sync_events
- secure_files
+- security_trainings
award_emoji:
- awardable
- user
diff --git a/spec/lib/gitlab/import_export/command_line_util_spec.rb b/spec/lib/gitlab/import_export/command_line_util_spec.rb
index 31512259bb1..738a76d3360 100644
--- a/spec/lib/gitlab/import_export/command_line_util_spec.rb
+++ b/spec/lib/gitlab/import_export/command_line_util_spec.rb
@@ -97,7 +97,7 @@ RSpec.describe Gitlab::ImportExport::CommandLineUtil do
include Gitlab::ImportExport::CommandLineUtil
end.new
- expect { klass.tar_cf(archive: 'test', dir: 'test') }.to raise_error(Gitlab::ImportExport::Error, 'System call failed')
+ expect { klass.tar_cf(archive: 'test', dir: 'test') }.to raise_error(Gitlab::ImportExport::Error, 'command exited with error code 1: Error')
end
end
end
@@ -125,14 +125,31 @@ RSpec.describe Gitlab::ImportExport::CommandLineUtil do
end
context 'when something goes wrong' do
- it 'raises an error' do
+ before do
expect(Gitlab::Popen).to receive(:popen).and_return(['Error', 1])
+ end
+ it 'raises an error' do
klass = Class.new do
include Gitlab::ImportExport::CommandLineUtil
end.new
- expect { klass.untar_xf(archive: 'test', dir: 'test') }.to raise_error(Gitlab::ImportExport::Error, 'System call failed')
+ expect { klass.untar_xf(archive: 'test', dir: 'test') }.to raise_error(Gitlab::ImportExport::Error, 'command exited with error code 1: Error')
+ end
+
+ it 'returns false and includes error status' do
+ klass = Class.new do
+ include Gitlab::ImportExport::CommandLineUtil
+
+ attr_accessor :shared
+
+ def initialize
+ @shared = Gitlab::ImportExport::Shared.new(nil)
+ end
+ end.new
+
+ expect(klass.tar_czf(archive: 'test', dir: 'test')).to eq(false)
+ expect(klass.shared.errors).to eq(['command exited with error code 1: Error'])
end
end
end
diff --git a/spec/lib/gitlab/import_export/config_spec.rb b/spec/lib/gitlab/import_export/config_spec.rb
index 7ad5d3d846c..fcb48678b88 100644
--- a/spec/lib/gitlab/import_export/config_spec.rb
+++ b/spec/lib/gitlab/import_export/config_spec.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
-require 'rspec-parameterized'
+require 'spec_helper'
RSpec.describe Gitlab::ImportExport::Config do
let(:yaml_file) { described_class.new }
diff --git a/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb b/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb
index adb613c3abc..ce888b71d5e 100644
--- a/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb
+++ b/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb
@@ -240,7 +240,7 @@ RSpec.describe Gitlab::ImportExport::FastHashSerializer do
merge_request = create(:merge_request, source_project: project, milestone: milestone)
ci_build = create(:ci_build, project: project, when: nil)
- ci_build.pipeline.update(project: project)
+ ci_build.pipeline.update!(project: project)
create(:commit_status, project: project, pipeline: ci_build.pipeline)
create_list(:ci_pipeline, 5, :success, project: project)
diff --git a/spec/lib/gitlab/import_export/fork_spec.rb b/spec/lib/gitlab/import_export/fork_spec.rb
index 65c28a8b8a2..25c82588c13 100644
--- a/spec/lib/gitlab/import_export/fork_spec.rb
+++ b/spec/lib/gitlab/import_export/fork_spec.rb
@@ -38,8 +38,8 @@ RSpec.describe 'forked project import' do
allow(instance).to receive(:storage_path).and_return(export_path)
end
- saver.save
- repo_saver.save
+ saver.save # rubocop:disable Rails/SaveBang
+ repo_saver.save # rubocop:disable Rails/SaveBang
repo_restorer.restore
restorer.restore
diff --git a/spec/lib/gitlab/import_export/group/legacy_tree_saver_spec.rb b/spec/lib/gitlab/import_export/group/legacy_tree_saver_spec.rb
index e075c5acfea..31d647f883a 100644
--- a/spec/lib/gitlab/import_export/group/legacy_tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/group/legacy_tree_saver_spec.rb
@@ -31,7 +31,7 @@ RSpec.describe Gitlab::ImportExport::Group::LegacyTreeSaver do
# ^ These are specific for the Group::LegacyTreeSaver
context 'JSON' do
let(:saved_group_json) do
- group_tree_saver.save
+ group_tree_saver.save # rubocop:disable Rails/SaveBang
group_json(group_tree_saver.full_path)
end
@@ -88,7 +88,7 @@ RSpec.describe Gitlab::ImportExport::Group::LegacyTreeSaver do
end
before do
- user2.update(public_email: user2.email)
+ user2.update!(public_email: user2.email)
group.add_developer(user2)
end
diff --git a/spec/lib/gitlab/import_export/group/relation_factory_spec.rb b/spec/lib/gitlab/import_export/group/relation_factory_spec.rb
index 63286fc0719..8e7fe8849d4 100644
--- a/spec/lib/gitlab/import_export/group/relation_factory_spec.rb
+++ b/spec/lib/gitlab/import_export/group/relation_factory_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe Gitlab::ImportExport::Group::RelationFactory do
let(:importer_user) { admin }
let(:excluded_keys) { [] }
let(:created_object) do
- described_class.create(
+ described_class.create( # rubocop:disable Rails/SaveBang
relation_sym: relation_sym,
relation_hash: relation_hash,
relation_index: 1,
diff --git a/spec/lib/gitlab/import_export/group/tree_saver_spec.rb b/spec/lib/gitlab/import_export/group/tree_saver_spec.rb
index c52daa8ccfd..de4d193a21c 100644
--- a/spec/lib/gitlab/import_export/group/tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/group/tree_saver_spec.rb
@@ -42,7 +42,7 @@ RSpec.describe Gitlab::ImportExport::Group::TreeSaver do
context 'exported files' do
before do
- group_tree_saver.save
+ group_tree_saver.save # rubocop:disable Rails/SaveBang
end
it 'has one group per line' do
diff --git a/spec/lib/gitlab/import_export/importer_spec.rb b/spec/lib/gitlab/import_export/importer_spec.rb
index 20f0f6af6f3..c9d559c992c 100644
--- a/spec/lib/gitlab/import_export/importer_spec.rb
+++ b/spec/lib/gitlab/import_export/importer_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe Gitlab::ImportExport::Importer do
stub_uploads_object_storage(FileUploader)
FileUtils.mkdir_p(shared.export_path)
- ImportExportUpload.create(project: project, import_file: import_file)
+ ImportExportUpload.create!(project: project, import_file: import_file)
allow(FileUtils).to receive(:rm_rf).and_call_original
end
diff --git a/spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb b/spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb
index d69d775fffb..352af18c822 100644
--- a/spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb
+++ b/spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb
@@ -183,24 +183,8 @@ RSpec.describe Gitlab::ImportExport::Json::StreamingSerializer do
end
describe '.batch_size' do
- context 'when export_reduce_relation_batch_size feature flag is enabled' do
- before do
- stub_feature_flags(export_reduce_relation_batch_size: true)
- end
-
- it 'returns 20' do
- expect(described_class.batch_size(exportable)).to eq(described_class::SMALLER_BATCH_SIZE)
- end
- end
-
- context 'when export_reduce_relation_batch_size feature flag is disabled' do
- before do
- stub_feature_flags(export_reduce_relation_batch_size: false)
- end
-
- it 'returns default batch size' do
- expect(described_class.batch_size(exportable)).to eq(described_class::BATCH_SIZE)
- end
+ it 'returns default batch size' do
+ expect(described_class.batch_size(exportable)).to eq(described_class::BATCH_SIZE)
end
end
end
diff --git a/spec/lib/gitlab/import_export/legacy_relation_tree_saver_spec.rb b/spec/lib/gitlab/import_export/legacy_relation_tree_saver_spec.rb
index 3b7ed7cb32b..0d372def8b0 100644
--- a/spec/lib/gitlab/import_export/legacy_relation_tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/legacy_relation_tree_saver_spec.rb
@@ -8,35 +8,17 @@ RSpec.describe Gitlab::ImportExport::LegacyRelationTreeSaver do
let(:tree) { {} }
describe '#serialize' do
- shared_examples 'FastHashSerializer with batch size' do |batch_size|
- let(:serializer) { instance_double(Gitlab::ImportExport::FastHashSerializer) }
+ let(:serializer) { instance_double(Gitlab::ImportExport::FastHashSerializer) }
- it 'uses FastHashSerializer' do
- expect(Gitlab::ImportExport::FastHashSerializer)
- .to receive(:new)
- .with(exportable, tree, batch_size: batch_size)
- .and_return(serializer)
+ it 'uses FastHashSerializer' do
+ expect(Gitlab::ImportExport::FastHashSerializer)
+ .to receive(:new)
+ .with(exportable, tree, batch_size: Gitlab::ImportExport::Json::StreamingSerializer::BATCH_SIZE)
+ .and_return(serializer)
- expect(serializer).to receive(:execute)
+ expect(serializer).to receive(:execute)
- relation_tree_saver.serialize(exportable, tree)
- end
- end
-
- context 'when export_reduce_relation_batch_size feature flag is enabled' do
- before do
- stub_feature_flags(export_reduce_relation_batch_size: true)
- end
-
- include_examples 'FastHashSerializer with batch size', Gitlab::ImportExport::Json::StreamingSerializer::SMALLER_BATCH_SIZE
- end
-
- context 'when export_reduce_relation_batch_size feature flag is disabled' do
- before do
- stub_feature_flags(export_reduce_relation_batch_size: false)
- end
-
- include_examples 'FastHashSerializer with batch size', Gitlab::ImportExport::Json::StreamingSerializer::BATCH_SIZE
+ relation_tree_saver.serialize(exportable, tree)
end
end
end
diff --git a/spec/lib/gitlab/import_export/lfs_restorer_spec.rb b/spec/lib/gitlab/import_export/lfs_restorer_spec.rb
index c8887b0ded1..fe064c50b9e 100644
--- a/spec/lib/gitlab/import_export/lfs_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/lfs_restorer_spec.rb
@@ -36,7 +36,7 @@ RSpec.describe Gitlab::ImportExport::LfsRestorer do
)
end
- saver.save
+ saver.save # rubocop:disable Rails/SaveBang
project.lfs_objects.delete_all
end
@@ -81,7 +81,7 @@ RSpec.describe Gitlab::ImportExport::LfsRestorer do
context 'when there is not an existing `LfsObject`' do
before do
- lfs_object.destroy
+ lfs_object.destroy!
end
it 'creates a new lfs object' do
diff --git a/spec/lib/gitlab/import_export/lfs_saver_spec.rb b/spec/lib/gitlab/import_export/lfs_saver_spec.rb
index 55b4f7479b8..84bd782c467 100644
--- a/spec/lib/gitlab/import_export/lfs_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/lfs_saver_spec.rb
@@ -34,13 +34,13 @@ RSpec.describe Gitlab::ImportExport::LfsSaver do
end
it 'does not cause errors' do
- saver.save
+ saver.save # rubocop:disable Rails/SaveBang
expect(shared.errors).to be_empty
end
it 'copies the file in the correct location when there is an lfs object' do
- saver.save
+ saver.save # rubocop:disable Rails/SaveBang
expect(File).to exist("#{shared.export_path}/lfs-objects/#{lfs_object.oid}")
end
@@ -61,7 +61,7 @@ RSpec.describe Gitlab::ImportExport::LfsSaver do
end
it 'saves a json file correctly' do
- saver.save
+ saver.save # rubocop:disable Rails/SaveBang
expect(File.exist?(lfs_json_file)).to eq(true)
expect(lfs_json).to eq(
@@ -96,7 +96,7 @@ RSpec.describe Gitlab::ImportExport::LfsSaver do
expect(fake_uri).to receive(:open).and_return(StringIO.new('LFS file content'))
expect(URI).to receive(:parse).with('http://my-object-storage.local').and_return(fake_uri)
- saver.save
+ saver.save # rubocop:disable Rails/SaveBang
expect(File.read(exported_file_path)).to eq('LFS file content')
end
diff --git a/spec/lib/gitlab/import_export/members_mapper_spec.rb b/spec/lib/gitlab/import_export/members_mapper_spec.rb
index 8b9ca90a280..8d9bff9c610 100644
--- a/spec/lib/gitlab/import_export/members_mapper_spec.rb
+++ b/spec/lib/gitlab/import_export/members_mapper_spec.rb
@@ -243,7 +243,6 @@ RSpec.describe Gitlab::ImportExport::MembersMapper do
before do
group.add_users([user, user2], GroupMember::DEVELOPER)
- user.update(public_email: 'invite@test.com')
end
it 'maps the importer' do
diff --git a/spec/lib/gitlab/import_export/project/relation_factory_spec.rb b/spec/lib/gitlab/import_export/project/relation_factory_spec.rb
index ea8b10675af..ffbbf9326ec 100644
--- a/spec/lib/gitlab/import_export/project/relation_factory_spec.rb
+++ b/spec/lib/gitlab/import_export/project/relation_factory_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe Gitlab::ImportExport::Project::RelationFactory, :use_clean_rails_
let(:importer_user) { admin }
let(:excluded_keys) { [] }
let(:created_object) do
- described_class.create(
+ described_class.create( # rubocop:disable Rails/SaveBang
relation_sym: relation_sym,
relation_hash: relation_hash,
relation_index: 1,
diff --git a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
index 1d8b137c196..8884722254d 100644
--- a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
@@ -880,7 +880,7 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer do
before do
group = create(:group, visibility_level: group_visibility)
group.add_users([user], GroupMember::MAINTAINER)
- project.update(group: group)
+ project.update!(group: group)
end
context 'private group visibility' do
diff --git a/spec/lib/gitlab/import_export/project/tree_saver_spec.rb b/spec/lib/gitlab/import_export/project/tree_saver_spec.rb
index f68ec21039d..ba781ae78b7 100644
--- a/spec/lib/gitlab/import_export/project/tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/project/tree_saver_spec.rb
@@ -36,7 +36,7 @@ RSpec.describe Gitlab::ImportExport::Project::TreeSaver do
project_tree_saver = described_class.new(project: project, current_user: user, shared: shared)
- project_tree_saver.save
+ project_tree_saver.save # rubocop:disable Rails/SaveBang
end
end
@@ -305,14 +305,14 @@ RSpec.describe Gitlab::ImportExport::Project::TreeSaver do
end
before do
- user2.update(public_email: user2.email)
+ user2.update!(public_email: user2.email)
group.add_developer(user2)
end
context 'when has no permission' do
before do
group.add_developer(user)
- project_tree_saver.save
+ project_tree_saver.save # rubocop:disable Rails/SaveBang
end
it 'does not export group members' do
@@ -324,7 +324,7 @@ RSpec.describe Gitlab::ImportExport::Project::TreeSaver do
before do
group.add_maintainer(user)
- project_tree_saver.save
+ project_tree_saver.save # rubocop:disable Rails/SaveBang
end
it 'does not export group members' do
@@ -336,7 +336,7 @@ RSpec.describe Gitlab::ImportExport::Project::TreeSaver do
before do
group.add_owner(user)
- project_tree_saver.save
+ project_tree_saver.save # rubocop:disable Rails/SaveBang
end
it 'exports group members as group owner' do
@@ -348,7 +348,7 @@ RSpec.describe Gitlab::ImportExport::Project::TreeSaver do
let(:user) { create(:admin) }
before do
- project_tree_saver.save
+ project_tree_saver.save # rubocop:disable Rails/SaveBang
end
context 'when admin mode is enabled', :enable_admin_mode do
@@ -376,7 +376,7 @@ RSpec.describe Gitlab::ImportExport::Project::TreeSaver do
let(:relation_name) { :projects }
before do
- project_tree_saver.save
+ project_tree_saver.save # rubocop:disable Rails/SaveBang
end
it { is_expected.to include({ 'description' => params[:description] }) }
@@ -471,7 +471,7 @@ RSpec.describe Gitlab::ImportExport::Project::TreeSaver do
merge_request = create(:merge_request, source_project: project, milestone: milestone)
ci_build = create(:ci_build, project: project, when: nil)
- ci_build.pipeline.update(project: project)
+ ci_build.pipeline.update!(project: project)
create(:commit_status, project: project, pipeline: ci_build.pipeline)
create(:milestone, project: project)
diff --git a/spec/lib/gitlab/import_export/repo_restorer_spec.rb b/spec/lib/gitlab/import_export/repo_restorer_spec.rb
index 718a23f80a1..c0215ff5843 100644
--- a/spec/lib/gitlab/import_export/repo_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/repo_restorer_spec.rb
@@ -19,7 +19,7 @@ RSpec.describe Gitlab::ImportExport::RepoRestorer do
before do
allow(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
- bundler.save
+ bundler.save # rubocop:disable Rails/SaveBang
end
after do
diff --git a/spec/lib/gitlab/import_export/saver_spec.rb b/spec/lib/gitlab/import_export/saver_spec.rb
index 877474dd862..f5eed81f73c 100644
--- a/spec/lib/gitlab/import_export/saver_spec.rb
+++ b/spec/lib/gitlab/import_export/saver_spec.rb
@@ -30,19 +30,63 @@ RSpec.describe Gitlab::ImportExport::Saver do
it 'saves the repo using object storage' do
stub_uploads_object_storage(ImportExportUploader)
- subject.save
+ subject.save # rubocop:disable Rails/SaveBang
expect(ImportExportUpload.find_by(project: project).export_file.url)
.to match(%r[\/uploads\/-\/system\/import_export_upload\/export_file.*])
end
+ it 'logs metrics after saving' do
+ stub_uploads_object_storage(ImportExportUploader)
+ expect(Gitlab::Export::Logger).to receive(:info).with(
+ hash_including(
+ message: 'Export archive saved',
+ exportable_class: 'Project',
+ 'correlation_id' => anything,
+ archive_file: anything,
+ compress_duration_s: anything
+ )).and_call_original
+
+ expect(Gitlab::Export::Logger).to receive(:info).with(
+ hash_including(
+ message: 'Export archive uploaded',
+ exportable_class: 'Project',
+ 'correlation_id' => anything,
+ archive_file: anything,
+ compress_duration_s: anything,
+ assign_duration_s: anything,
+ upload_duration_s: anything,
+ upload_bytes: anything
+ )).and_call_original
+
+ subject.save # rubocop:disable Rails/SaveBang
+ end
+
it 'removes archive path and keeps base path untouched' do
allow(shared).to receive(:archive_path).and_return(archive_path)
- subject.save
+ subject.save # rubocop:disable Rails/SaveBang
expect(FileUtils).not_to have_received(:rm_rf).with(base_path)
expect(FileUtils).to have_received(:rm_rf).with(archive_path)
expect(Dir.exist?(archive_path)).to eq(false)
end
+
+ context 'when save throws an exception' do
+ before do
+ expect(subject).to receive(:save_upload).and_raise(SocketError.new)
+ end
+
+ it 'logs a saver error' do
+ allow(Gitlab::Export::Logger).to receive(:info).with(anything).and_call_original
+ expect(Gitlab::Export::Logger).to receive(:info).with(
+ hash_including(
+ message: 'Export archive saver failed',
+ exportable_class: 'Project',
+ 'correlation_id' => anything
+ )).and_call_original
+
+ subject.save # rubocop:disable Rails/SaveBang
+ end
+ end
end
diff --git a/spec/lib/gitlab/import_export/snippet_repo_restorer_spec.rb b/spec/lib/gitlab/import_export/snippet_repo_restorer_spec.rb
index 7d719b6028f..2f39cb560d0 100644
--- a/spec/lib/gitlab/import_export/snippet_repo_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/snippet_repo_restorer_spec.rb
@@ -70,7 +70,7 @@ RSpec.describe Gitlab::ImportExport::SnippetRepoRestorer do
let!(:snippet_with_repo) { create(:project_snippet, :repository, project: project, author: user) }
let(:bundle_path) { ::Gitlab::ImportExport.snippets_repo_bundle_path(shared.export_path) }
let(:snippet_bundle_path) { File.join(bundle_path, "#{snippet_with_repo.hexdigest}.bundle") }
- let(:result) { exporter.save }
+ let(:result) { exporter.save } # rubocop:disable Rails/SaveBang
let(:repository) { snippet.repository }
before do
diff --git a/spec/lib/gitlab/import_export/snippet_repo_saver_spec.rb b/spec/lib/gitlab/import_export/snippet_repo_saver_spec.rb
index 9f3e8d2fa86..9a9f40b3d0c 100644
--- a/spec/lib/gitlab/import_export/snippet_repo_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/snippet_repo_saver_spec.rb
@@ -38,7 +38,7 @@ RSpec.describe Gitlab::ImportExport::SnippetRepoSaver do
aggregate_failures do
expect(snippet.repository).not_to receive(:bundle_to_disk)
- bundler.save
+ bundler.save # rubocop:disable Rails/SaveBang
expect(Dir.empty?(bundle_path)).to be_truthy
end
diff --git a/spec/lib/gitlab/import_export/snippets_repo_restorer_spec.rb b/spec/lib/gitlab/import_export/snippets_repo_restorer_spec.rb
index 7ca365762b5..e529d36fd11 100644
--- a/spec/lib/gitlab/import_export/snippets_repo_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/snippets_repo_restorer_spec.rb
@@ -64,7 +64,7 @@ RSpec.describe Gitlab::ImportExport::SnippetsRepoRestorer do
let!(:snippet2) { create(:project_snippet, project: project, author: user) }
before do
- exporter.save
+ exporter.save # rubocop:disable Rails/SaveBang
expect(File.exist?(bundle_path(snippet1))).to be true
expect(File.exist?(bundle_path(snippet2))).to be false
@@ -78,7 +78,7 @@ RSpec.describe Gitlab::ImportExport::SnippetsRepoRestorer do
let!(:snippet2) { create(:project_snippet, :repository, project: project, author: user) }
before do
- exporter.save
+ exporter.save # rubocop:disable Rails/SaveBang
expect(File.exist?(bundle_path(snippet1))).to be true
expect(File.exist?(bundle_path(snippet2))).to be true
diff --git a/spec/lib/gitlab/import_export/snippets_repo_saver_spec.rb b/spec/lib/gitlab/import_export/snippets_repo_saver_spec.rb
index aa284c60e73..eaa58c77aff 100644
--- a/spec/lib/gitlab/import_export/snippets_repo_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/snippets_repo_saver_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe Gitlab::ImportExport::SnippetsRepoSaver do
snippets_dir = ::Gitlab::ImportExport.snippets_repo_bundle_path(shared.export_path)
expect(Dir.exist?(snippets_dir)).to be_falsey
- bundler.save
+ bundler.save # rubocop:disable Rails/SaveBang
expect(Dir.exist?(snippets_dir)).to be_truthy
end
@@ -27,7 +27,7 @@ RSpec.describe Gitlab::ImportExport::SnippetsRepoSaver do
it 'does not perform any action' do
expect(Gitlab::ImportExport::SnippetRepoSaver).not_to receive(:new)
- bundler.save
+ bundler.save # rubocop:disable Rails/SaveBang
end
end
@@ -40,7 +40,7 @@ RSpec.describe Gitlab::ImportExport::SnippetsRepoSaver do
allow(Gitlab::ImportExport::SnippetRepoSaver).to receive(:new).and_return(service)
expect(service).to receive(:save).and_return(true).twice
- bundler.save
+ bundler.save # rubocop:disable Rails/SaveBang
end
context 'when one snippet cannot be saved' do
diff --git a/spec/lib/gitlab/import_export/uploads_manager_spec.rb b/spec/lib/gitlab/import_export/uploads_manager_spec.rb
index 8282ad9a070..0cfe3a69a09 100644
--- a/spec/lib/gitlab/import_export/uploads_manager_spec.rb
+++ b/spec/lib/gitlab/import_export/uploads_manager_spec.rb
@@ -31,13 +31,13 @@ RSpec.describe Gitlab::ImportExport::UploadsManager do
let(:upload) { create(:upload, :issuable_upload, :with_file, model: project) }
it 'does not cause errors' do
- manager.save
+ manager.save # rubocop:disable Rails/SaveBang
expect(shared.errors).to be_empty
end
it 'copies the file in the correct location when there is an upload' do
- manager.save
+ manager.save # rubocop:disable Rails/SaveBang
expect(File).to exist(exported_file_path)
end
@@ -56,7 +56,7 @@ RSpec.describe Gitlab::ImportExport::UploadsManager do
end
it 'excludes orphaned upload files' do
- manager.save
+ manager.save # rubocop:disable Rails/SaveBang
expect(File).not_to exist(exported_orphan_path)
end
@@ -68,7 +68,7 @@ RSpec.describe Gitlab::ImportExport::UploadsManager do
end
it 'does not cause errors' do
- manager.save
+ manager.save # rubocop:disable Rails/SaveBang
expect(shared.errors).to be_empty
end
@@ -84,7 +84,7 @@ RSpec.describe Gitlab::ImportExport::UploadsManager do
it 'ignores problematic upload and logs exception' do
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(instance_of(Errno::ENAMETOOLONG), project_id: project.id)
- manager.save
+ manager.save # rubocop:disable Rails/SaveBang
expect(shared.errors).to be_empty
expect(File).not_to exist(exported_file_path)
diff --git a/spec/lib/gitlab/incident_management/pager_duty/incident_issue_description_spec.rb b/spec/lib/gitlab/incident_management/pager_duty/incident_issue_description_spec.rb
index 535cce6aa04..c5288b9afbc 100644
--- a/spec/lib/gitlab/incident_management/pager_duty/incident_issue_description_spec.rb
+++ b/spec/lib/gitlab/incident_management/pager_duty/incident_issue_description_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'spec_helper'
RSpec.describe Gitlab::IncidentManagement::PagerDuty::IncidentIssueDescription do
describe '#to_s' do
diff --git a/spec/lib/gitlab/json_spec.rb b/spec/lib/gitlab/json_spec.rb
index f9f57752b0a..5ffe736da54 100644
--- a/spec/lib/gitlab/json_spec.rb
+++ b/spec/lib/gitlab/json_spec.rb
@@ -317,36 +317,14 @@ RSpec.describe Gitlab::Json do
let(:env) { {} }
let(:result) { "{\"test\":true}" }
- context "grape_gitlab_json flag is enabled" do
- before do
- stub_feature_flags(grape_gitlab_json: true)
- end
-
- it "generates JSON" do
- expect(subject).to eq(result)
- end
-
- it "uses Gitlab::Json" do
- expect(Gitlab::Json).to receive(:dump).with(obj)
-
- subject
- end
+ it "generates JSON" do
+ expect(subject).to eq(result)
end
- context "grape_gitlab_json flag is disabled" do
- before do
- stub_feature_flags(grape_gitlab_json: false)
- end
+ it "uses Gitlab::Json" do
+ expect(Gitlab::Json).to receive(:dump).with(obj)
- it "generates JSON" do
- expect(subject).to eq(result)
- end
-
- it "uses Grape::Formatter::Json" do
- expect(Grape::Formatter::Json).to receive(:call).with(obj, env)
-
- subject
- end
+ subject
end
context "precompiled JSON" do
@@ -440,15 +418,5 @@ RSpec.describe Gitlab::Json do
expect(subject.size).to eq(10001)
end
end
-
- context 'when json_limited_encoder is disabled' do
- let(:obj) { [{ test: true }] * 1000 }
-
- it 'does not raise an error' do
- stub_feature_flags(json_limited_encoder: false)
-
- expect { subject }.not_to raise_error
- end
- end
end
end
diff --git a/spec/lib/gitlab/kubernetes/kubectl_cmd_spec.rb b/spec/lib/gitlab/kubernetes/kubectl_cmd_spec.rb
index 2e373613269..3028e0a13aa 100644
--- a/spec/lib/gitlab/kubernetes/kubectl_cmd_spec.rb
+++ b/spec/lib/gitlab/kubernetes/kubectl_cmd_spec.rb
@@ -2,6 +2,8 @@
require 'fast_spec_helper'
+require_relative '../../../../lib/gitlab/kubernetes/pod_cmd'
+
RSpec.describe Gitlab::Kubernetes::KubectlCmd do
describe '.delete' do
it 'constructs string properly' do
diff --git a/spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb b/spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb
index 73b35d3a4e7..cbd1a30c417 100644
--- a/spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb
+++ b/spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb
@@ -63,5 +63,13 @@ RSpec.describe Gitlab::LegacyGithubImport::ReleaseFormatter do
expect(release.valid?).to eq false
end
end
+
+ context 'when release has NULL tag' do
+ let(:raw_data) { double(base_data.merge(tag_name: '')) }
+
+ it 'returns false' do
+ expect(release.valid?).to eq false
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/metrics/boot_time_tracker_spec.rb b/spec/lib/gitlab/metrics/boot_time_tracker_spec.rb
new file mode 100644
index 00000000000..8a17fa8dd2e
--- /dev/null
+++ b/spec/lib/gitlab/metrics/boot_time_tracker_spec.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+RSpec.describe Gitlab::Metrics::BootTimeTracker do
+ let(:logger) { double('logger') }
+ let(:gauge) { double('gauge') }
+
+ subject(:tracker) { described_class.instance }
+
+ before do
+ described_class.instance.reset!
+
+ allow(logger).to receive(:info)
+ allow(gauge).to receive(:set)
+ allow(Gitlab::Metrics).to receive(:gauge).and_return(gauge)
+ end
+
+ describe '#track_boot_time!' do
+ described_class::SUPPORTED_RUNTIMES.each do |runtime|
+ context "when called on #{runtime} for the first time" do
+ before do
+ expect(Gitlab::Runtime).to receive(:safe_identify).and_return(runtime)
+ end
+
+ it 'set the startup_time' do
+ tracker.track_boot_time!(logger: logger)
+
+ expect(tracker.startup_time).to be > 0
+ end
+
+ it 'records the current process runtime' do
+ expect(Gitlab::Metrics::System).to receive(:process_runtime_elapsed_seconds).once
+
+ tracker.track_boot_time!(logger: logger)
+ end
+
+ it 'logs the application boot time' do
+ expect(Gitlab::Metrics::System).to receive(:process_runtime_elapsed_seconds).and_return(42)
+ expect(logger).to receive(:info).with(message: 'Application boot finished', runtime: runtime.to_s, duration_s: 42)
+
+ tracker.track_boot_time!(logger: logger)
+ end
+
+ it 'tracks boot time in a prometheus gauge' do
+ expect(Gitlab::Metrics::System).to receive(:process_runtime_elapsed_seconds).and_return(42)
+ expect(gauge).to receive(:set).with({}, 42)
+
+ tracker.track_boot_time!(logger: logger)
+ end
+
+ context 'on subsequent calls' do
+ it 'does nothing' do
+ tracker.track_boot_time!(logger: logger)
+
+ expect(Gitlab::Metrics::System).not_to receive(:process_runtime_elapsed_seconds)
+ expect(logger).not_to receive(:info)
+ expect(gauge).not_to receive(:set)
+
+ tracker.track_boot_time!(logger: logger)
+ end
+ end
+ end
+ end
+
+ context 'when called on other runtimes' do
+ it 'does nothing' do
+ tracker.track_boot_time!(logger: logger)
+
+ expect(Gitlab::Metrics::System).not_to receive(:process_runtime_elapsed_seconds)
+ expect(logger).not_to receive(:info)
+ expect(gauge).not_to receive(:set)
+
+ tracker.track_boot_time!(logger: logger)
+ end
+ end
+ end
+
+ describe '#startup_time' do
+ it 'returns 0 when boot time not tracked' do
+ expect(tracker.startup_time).to eq(0)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/exporter/web_exporter_spec.rb b/spec/lib/gitlab/metrics/exporter/web_exporter_spec.rb
deleted file mode 100644
index 0531bccf4b4..00000000000
--- a/spec/lib/gitlab/metrics/exporter/web_exporter_spec.rb
+++ /dev/null
@@ -1,53 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Metrics::Exporter::WebExporter do
- let(:exporter) { described_class.new }
- let(:readiness_probe) { exporter.send(:readiness_probe).execute }
-
- before do
- stub_config(
- monitoring: {
- web_exporter: {
- enabled: true,
- port: 0,
- address: '127.0.0.1'
- }
- }
- )
-
- exporter.start
- end
-
- after do
- exporter.stop
- end
-
- context 'when running server', :prometheus do
- it 'readiness probe returns succesful status' do
- expect(readiness_probe.http_status).to eq(200)
- expect(readiness_probe.json).to include(status: 'ok')
- expect(readiness_probe.json).to include('web_exporter' => [{ 'status': 'ok' }])
- end
-
- it 'initializes request metrics' do
- expect(Gitlab::Metrics::RailsSlis).to receive(:initialize_request_slis_if_needed!).and_call_original
-
- http = Net::HTTP.new(exporter.server.config[:BindAddress], exporter.server.config[:Port])
- response = http.request(Net::HTTP::Get.new('/metrics'))
-
- expect(response.body).to include('gitlab_sli:rails_request_apdex')
- end
- end
-
- describe '#mark_as_not_running!' do
- it 'readiness probe returns a failure status', :prometheus do
- exporter.mark_as_not_running!
-
- expect(readiness_probe.http_status).to eq(503)
- expect(readiness_probe.json).to include(status: 'failed')
- expect(readiness_probe.json).to include('web_exporter' => [{ 'status': 'failed' }])
- end
- end
-end
diff --git a/spec/lib/gitlab/metrics/rails_slis_spec.rb b/spec/lib/gitlab/metrics/rails_slis_spec.rb
index a5ccf7fafa4..0c77dc9f582 100644
--- a/spec/lib/gitlab/metrics/rails_slis_spec.rb
+++ b/spec/lib/gitlab/metrics/rails_slis_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe Gitlab::Metrics::RailsSlis do
allow(Gitlab::Graphql::KnownOperations).to receive(:default).and_return(Gitlab::Graphql::KnownOperations.new(%w(foo bar)))
end
- describe '.initialize_request_slis_if_needed!' do
+ describe '.initialize_request_slis!' do
it "initializes the SLI for all possible endpoints if they weren't", :aggregate_failures do
possible_labels = [
{
@@ -41,7 +41,7 @@ RSpec.describe Gitlab::Metrics::RailsSlis do
expect(Gitlab::Metrics::Sli).to receive(:initialize_sli).with(:rails_request_apdex, array_including(*possible_labels)).and_call_original
expect(Gitlab::Metrics::Sli).to receive(:initialize_sli).with(:graphql_query_apdex, array_including(*possible_graphql_labels)).and_call_original
- described_class.initialize_request_slis_if_needed!
+ described_class.initialize_request_slis!
end
it 'does not initialize the SLI if they were initialized already', :aggregate_failures do
@@ -49,13 +49,13 @@ RSpec.describe Gitlab::Metrics::RailsSlis do
expect(Gitlab::Metrics::Sli).to receive(:initialized?).with(:graphql_query_apdex) { true }
expect(Gitlab::Metrics::Sli).not_to receive(:initialize_sli)
- described_class.initialize_request_slis_if_needed!
+ described_class.initialize_request_slis!
end
end
describe '.request_apdex' do
it 'returns the initialized request apdex SLI object' do
- described_class.initialize_request_slis_if_needed!
+ described_class.initialize_request_slis!
expect(described_class.request_apdex).to be_initialized
end
@@ -63,7 +63,7 @@ RSpec.describe Gitlab::Metrics::RailsSlis do
describe '.graphql_query_apdex' do
it 'returns the initialized request apdex SLI object' do
- described_class.initialize_request_slis_if_needed!
+ described_class.initialize_request_slis!
expect(described_class.graphql_query_apdex).to be_initialized
end
diff --git a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb
index a4877208bcf..dfae5aa6784 100644
--- a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb
+++ b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb
@@ -18,6 +18,20 @@ RSpec.describe Gitlab::Metrics::Samplers::RubySampler do
expect(sampler.metrics[:process_start_time_seconds].get).to eq(Time.now.to_i)
end
end
+
+ context 'when not setting a prefix' do
+ it 'does not prepend metrics with that prefix' do
+ expect(sampler.metrics[:process_start_time_seconds].name).to eq(:ruby_process_start_time_seconds)
+ end
+ end
+
+ context 'when using custom prefix' do
+ let(:sampler) { described_class.new(prefix: 'custom') }
+
+ it 'prepends metrics with that prefix' do
+ expect(sampler.metrics[:process_start_time_seconds].name).to eq(:custom_ruby_process_start_time_seconds)
+ end
+ end
end
describe '#sample' do
diff --git a/spec/lib/gitlab/metrics/system_spec.rb b/spec/lib/gitlab/metrics/system_spec.rb
index 732aa553737..ce3caf8cdfe 100644
--- a/spec/lib/gitlab/metrics/system_spec.rb
+++ b/spec/lib/gitlab/metrics/system_spec.rb
@@ -4,6 +4,13 @@ require 'spec_helper'
RSpec.describe Gitlab::Metrics::System do
context 'when /proc files exist' do
+ # Modified column 22 to be 1000 (starttime ticks)
+ let(:proc_stat) do
+ <<~SNIP
+ 2095 (ruby) R 0 2095 2095 34818 2095 4194560 211267 7897 2 0 287 51 10 1 20 0 5 0 1000 566210560 80885 18446744073709551615 94736211292160 94736211292813 140720919612064 0 0 0 0 0 1107394127 0 0 0 17 3 0 0 0 0 0 94736211303768 94736211304544 94736226689024 140720919619473 140720919619513 140720919619513 140720919621604 0
+ SNIP
+ end
+
# Fixtures pulled from:
# Linux carbon 5.3.0-7648-generic #41~1586789791~19.10~9593806-Ubuntu SMP Mon Apr 13 17:50:40 UTC x86_64 x86_64 x86_64 GNU/Linux
let(:proc_status) do
@@ -97,6 +104,29 @@ RSpec.describe Gitlab::Metrics::System do
end
end
+ describe '.process_runtime_elapsed_seconds' do
+ it 'returns the seconds elapsed since the process was started' do
+ # sets process starttime ticks to 1000
+ mock_existing_proc_file('/proc/self/stat', proc_stat)
+ # system clock ticks/sec
+ expect(Etc).to receive(:sysconf).with(Etc::SC_CLK_TCK).and_return(100)
+ # system uptime in seconds
+ expect(::Process).to receive(:clock_gettime).and_return(15)
+
+ # uptime - (starttime_ticks / ticks_per_sec)
+ expect(described_class.process_runtime_elapsed_seconds).to eq(5)
+ end
+
+ context 'when inputs are not available' do
+ it 'returns 0' do
+ mock_missing_proc_file
+ expect(::Process).to receive(:clock_gettime).and_raise(NameError)
+
+ expect(described_class.process_runtime_elapsed_seconds).to eq(0)
+ end
+ end
+ end
+
describe '.summary' do
it 'contains a selection of the available fields' do
stub_const('RUBY_DESCRIPTION', 'ruby-3.0-patch1')
@@ -223,10 +253,10 @@ RSpec.describe Gitlab::Metrics::System do
end
def mock_existing_proc_file(path, content)
- allow(File).to receive(:foreach).with(path) { |_path, &block| content.each_line(&block) }
+ allow(File).to receive(:open).with(path) { |_path, &block| block.call(StringIO.new(content)) }
end
def mock_missing_proc_file
- allow(File).to receive(:foreach).and_raise(Errno::ENOENT)
+ allow(File).to receive(:open).and_raise(Errno::ENOENT)
end
end
diff --git a/spec/lib/gitlab/middleware/memory_report_spec.rb b/spec/lib/gitlab/middleware/memory_report_spec.rb
new file mode 100644
index 00000000000..e063866b056
--- /dev/null
+++ b/spec/lib/gitlab/middleware/memory_report_spec.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require 'memory_profiler'
+
+RSpec.describe Gitlab::Middleware::MemoryReport do
+ let(:app) { proc { |env| [200, { 'Content-Type' => 'text/plain' }, ['Hello world!']] } }
+ let(:middleware) { described_class.new(app) }
+
+ describe '#call' do
+ shared_examples 'returns original response' do
+ it 'returns original response' do
+ expect(MemoryProfiler).not_to receive(:report)
+
+ status, headers, body = middleware.call(env)
+
+ expect(status).to eq(200)
+ expect(headers).to eq({ 'Content-Type' => 'text/plain' })
+ expect(body.first).to eq('Hello world!')
+ end
+
+ it 'does not call the MemoryProfiler' do
+ expect(MemoryProfiler).not_to receive(:report)
+
+ middleware.call(env)
+ end
+ end
+
+ context 'when the Rails environment is not development' do
+ let(:env) { Rack::MockRequest.env_for('/') }
+
+ it_behaves_like 'returns original response'
+ end
+
+ context 'when the Rails environment is development' do
+ before do
+ allow(Rails.env).to receive(:development?).and_return(true)
+ end
+
+ context 'when memory report is not requested' do
+ let(:env) { Rack::MockRequest.env_for('/') }
+
+ it_behaves_like 'returns original response'
+ end
+
+ context 'when memory report is requested' do
+ let(:env) { Rack::MockRequest.env_for('/', params: { 'performance_bar' => 'memory' }) }
+
+ before do
+ allow(env).to receive(:[]).and_call_original
+ allow(app).to receive(:call).and_return(empty_memory_report)
+ end
+
+ let(:empty_memory_report) do
+ report = MemoryProfiler::Results.new
+ report.register_results(MemoryProfiler::StatHash.new, MemoryProfiler::StatHash.new, 1)
+ end
+
+ it 'returns a memory report' do
+ expect(MemoryProfiler).to receive(:report).and_yield
+
+ status, headers, body = middleware.call(env)
+
+ expect(status).to eq(200)
+ expect(headers).to eq({ 'Content-Type' => 'text/plain' })
+ expect(body.first).to include('Total allocated: 0 B')
+ end
+
+ context 'when something goes wrong with creating the report' do
+ before do
+ expect(MemoryProfiler).to receive(:report).and_raise(StandardError, 'something went terribly wrong!')
+ end
+
+ it 'logs the error' do
+ expect(::Gitlab::ErrorTracking).to receive(:track_exception)
+
+ middleware.call(env)
+ end
+
+ it 'returns the error' do
+ status, headers, body = middleware.call(env)
+
+ expect(status).to eq(500)
+ expect(headers).to eq({ 'Content-Type' => 'text/plain' })
+ expect(body.first).to include('Could not generate memory report: something went terribly wrong!')
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/net_http_adapter_spec.rb b/spec/lib/gitlab/net_http_adapter_spec.rb
new file mode 100644
index 00000000000..21c1a1ebe25
--- /dev/null
+++ b/spec/lib/gitlab/net_http_adapter_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::NetHttpAdapter do
+ describe '#connect' do
+ let(:url) { 'https://example.org' }
+ let(:net_http_adapter) { described_class.new(url) }
+
+ subject(:connect) { net_http_adapter.send(:connect) }
+
+ before do
+ allow(TCPSocket).to receive(:open).and_return(Socket.new(:INET, :STREAM))
+ end
+
+ it 'uses a Gitlab::BufferedIo instance as @socket' do
+ connect
+
+ expect(net_http_adapter.instance_variable_get(:@socket)).to be_a(Gitlab::BufferedIo)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/omniauth_initializer_spec.rb b/spec/lib/gitlab/omniauth_initializer_spec.rb
index 577d15b8495..42ae5844b95 100644
--- a/spec/lib/gitlab/omniauth_initializer_spec.rb
+++ b/spec/lib/gitlab/omniauth_initializer_spec.rb
@@ -101,5 +101,19 @@ RSpec.describe Gitlab::OmniauthInitializer do
subject.execute([google_config])
end
+
+ it 'configures defaults for gitlab' do
+ conf = {
+ 'name' => 'gitlab',
+ "args" => {}
+ }
+
+ expect(devise_config).to receive(:omniauth).with(
+ :gitlab,
+ authorize_params: { gl_auth_type: 'login' }
+ )
+
+ subject.execute([conf])
+ end
end
end
diff --git a/spec/lib/gitlab/pagination/keyset/in_operator_optimization/array_scope_columns_spec.rb b/spec/lib/gitlab/pagination/keyset/in_operator_optimization/array_scope_columns_spec.rb
index 2cebf0d9473..087bfb197ec 100644
--- a/spec/lib/gitlab/pagination/keyset/in_operator_optimization/array_scope_columns_spec.rb
+++ b/spec/lib/gitlab/pagination/keyset/in_operator_optimization/array_scope_columns_spec.rb
@@ -16,4 +16,13 @@ RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::ArrayScopeCol
it { expect { array_scope_columns }.to raise_error /No array columns were given/ }
end
+
+ context 'when Arel AS node is given as input' do
+ let(:scope) { Issue.select(Issue.arel_table[:id].as('id'), :title) }
+ let(:columns) { scope.select_values }
+
+ it 'works with Arel AS nodes' do
+ expect(array_scope_columns.array_aggregated_column_names).to eq(%w[array_cte_id_array array_cte_title_array])
+ end
+ end
end
diff --git a/spec/lib/gitlab/pipeline_scope_counts_spec.rb b/spec/lib/gitlab/pipeline_scope_counts_spec.rb
new file mode 100644
index 00000000000..a9187ecfb54
--- /dev/null
+++ b/spec/lib/gitlab/pipeline_scope_counts_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::PipelineScopeCounts do
+ let(:current_user) { create(:user) }
+
+ let_it_be(:project) { create(:project, :private) }
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+ let_it_be(:failed_pipeline) { create(:ci_pipeline, :failed, project: project) }
+ let_it_be(:success_pipeline) { create(:ci_pipeline, :success, project: project) }
+ let_it_be(:ref_pipeline) { create(:ci_pipeline, project: project, ref: 'awesome-feature') }
+ let_it_be(:sha_pipeline) { create(:ci_pipeline, :running, project: project, sha: 'deadbeef') }
+ let_it_be(:on_demand_dast_scan) { create(:ci_pipeline, :success, project: project, source: 'ondemand_dast_scan') }
+
+ before do
+ project.add_developer(current_user)
+ end
+
+ it 'has policy class' do
+ expect(described_class.declarative_policy_class).to be("Ci::ProjectPipelinesPolicy")
+ end
+
+ it 'has expected attributes' do
+ expect(described_class.new(current_user, project, {})).to have_attributes(
+ all: 6,
+ finished: 3,
+ pending: 2,
+ running: 1
+ )
+ end
+
+ describe 'with large amount of pipelines' do
+ it 'sets the PIPELINES_COUNT_LIMIT constant to a value of 1_000' do
+ expect(described_class::PIPELINES_COUNT_LIMIT).to eq(1_000)
+ end
+
+ context 'when there are more records than the limit' do
+ before do
+ stub_const('Gitlab::PipelineScopeCounts::PIPELINES_COUNT_LIMIT', 3)
+ end
+
+ it 'limits the found items' do
+ expect(described_class.new(current_user, project, {}).all).to eq(3)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/popen_spec.rb b/spec/lib/gitlab/popen_spec.rb
index 891482a5f17..8211806a809 100644
--- a/spec/lib/gitlab/popen_spec.rb
+++ b/spec/lib/gitlab/popen_spec.rb
@@ -40,6 +40,17 @@ RSpec.describe Gitlab::Popen do
it { expect(@output).to include('No such file or directory') }
end
+ context 'non-zero status with a kill' do
+ let(:cmd) { [Gem.ruby, "-e", "thr = Thread.new { sleep 5 }; Process.kill(9, Process.pid); thr.join"] }
+
+ before do
+ @output, @status = @klass.new.popen(cmd)
+ end
+
+ it { expect(@status).to eq(9) }
+ it { expect(@output).to be_empty }
+ end
+
context 'unsafe string command' do
it 'raises an error when it gets called with a string argument' do
expect { @klass.new.popen('ls', path) }.to raise_error(RuntimeError)
diff --git a/spec/lib/gitlab/process_memory_cache/helper_spec.rb b/spec/lib/gitlab/process_memory_cache/helper_spec.rb
index 27d7fd0bdcf..bad4f61282c 100644
--- a/spec/lib/gitlab/process_memory_cache/helper_spec.rb
+++ b/spec/lib/gitlab/process_memory_cache/helper_spec.rb
@@ -33,13 +33,20 @@ RSpec.describe Gitlab::ProcessMemoryCache::Helper, :use_clean_rails_memory_store
end
it 'resets the cache when the shared key is missing', :aggregate_failures do
- expect(Rails.cache).to receive(:read).with(:cached_content_instance_key).twice.and_return(nil)
+ allow(Rails.cache).to receive(:read).with(:cached_content_instance_key).and_return(nil)
is_expected.to receive(:expensive_computation).thrice.and_return(1, 2, 3)
3.times do |index|
expect(subject.cached_content).to eq(index + 1)
end
end
+
+ it 'does not set the shared timestamp if it is already present', :redis do
+ subject.clear_cached_content
+ is_expected.to receive(:expensive_computation).once.and_return(1)
+
+ expect { subject.cached_content }.not_to change { Rails.cache.read(:cached_content_instance_key) }
+ end
end
describe '.invalidate_memory_cache' do
diff --git a/spec/lib/gitlab/project_authorizations_spec.rb b/spec/lib/gitlab/project_authorizations_spec.rb
index 16066934194..7852470196b 100644
--- a/spec/lib/gitlab/project_authorizations_spec.rb
+++ b/spec/lib/gitlab/project_authorizations_spec.rb
@@ -334,7 +334,7 @@ RSpec.describe Gitlab::ProjectAuthorizations do
let(:common_id) { non_existing_record_id }
let!(:group) { create(:group, id: common_id) }
let!(:unrelated_project) { create(:project, id: common_id) }
- let(:user) { unrelated_project.owner }
+ let(:user) { unrelated_project.first_owner }
it 'does not have access to group and its projects' do
mapping = map_access_levels(authorizations)
@@ -345,4 +345,76 @@ RSpec.describe Gitlab::ProjectAuthorizations do
end
end
end
+
+ context 'with pending memberships' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:user) { create(:user) }
+
+ subject(:mapping) { map_access_levels(authorizations) }
+
+ context 'group membership' do
+ let!(:group_project) { create(:project, namespace: group) }
+
+ before do
+ create(:group_member, :developer, :awaiting, user: user, group: group)
+ end
+
+ it 'does not create authorization' do
+ expect(mapping[group_project.id]).to be_nil
+ end
+ end
+
+ context 'inherited group membership' do
+ let!(:sub_group) { create(:group, parent: group) }
+ let!(:sub_group_project) { create(:project, namespace: sub_group) }
+
+ before do
+ create(:group_member, :developer, :awaiting, user: user, group: group)
+ end
+
+ it 'does not create authorization' do
+ expect(mapping[sub_group_project.id]).to be_nil
+ end
+ end
+
+ context 'project membership' do
+ let!(:group_project) { create(:project, namespace: group) }
+
+ before do
+ create(:project_member, :developer, :awaiting, user: user, project: group_project)
+ end
+
+ it 'does not create authorization' do
+ expect(mapping[group_project.id]).to be_nil
+ end
+ end
+
+ context 'shared group' do
+ let!(:shared_group) { create(:group) }
+ let!(:shared_group_project) { create(:project, namespace: shared_group) }
+
+ before do
+ create(:group_group_link, shared_group: shared_group, shared_with_group: group)
+ create(:group_member, :developer, :awaiting, user: user, group: group)
+ end
+
+ it 'does not create authorization' do
+ expect(mapping[shared_group_project.id]).to be_nil
+ end
+ end
+
+ context 'shared project' do
+ let!(:another_group) { create(:group) }
+ let!(:shared_project) { create(:project, namespace: another_group) }
+
+ before do
+ create(:project_group_link, group: group, project: shared_project)
+ create(:group_member, :developer, :awaiting, user: user, group: group)
+ end
+
+ it 'does not create authorization' do
+ expect(mapping[shared_project.id]).to be_nil
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/rack_attack/request_spec.rb b/spec/lib/gitlab/rack_attack/request_spec.rb
index ecdcc23e588..b8a26a64e5b 100644
--- a/spec/lib/gitlab/rack_attack/request_spec.rb
+++ b/spec/lib/gitlab/rack_attack/request_spec.rb
@@ -5,6 +5,20 @@ require 'spec_helper'
RSpec.describe Gitlab::RackAttack::Request do
using RSpec::Parameterized::TableSyntax
+ let(:path) { '/' }
+ let(:env) { {} }
+ let(:session) { {} }
+ let(:request) do
+ ::Rack::Attack::Request.new(
+ env.reverse_merge(
+ 'REQUEST_METHOD' => 'GET',
+ 'PATH_INFO' => Gitlab.config.gitlab.relative_url_root + path,
+ 'rack.input' => StringIO.new,
+ 'rack.session' => session
+ )
+ )
+ end
+
describe 'FILES_PATH_REGEX' do
subject { described_class::FILES_PATH_REGEX }
@@ -16,11 +30,249 @@ RSpec.describe Gitlab::RackAttack::Request do
it { is_expected.not_to match('/api/v4/projects/some/nested/repo/repository/files/README') }
end
+ describe '#api_request?' do
+ subject { request.api_request? }
+
+ where(:path, :expected) do
+ '/' | false
+ '/groups' | false
+ '/foo/api' | false
+
+ '/api' | true
+ '/api/v4/groups/1' | true
+ end
+
+ with_them do
+ it { is_expected.to eq(expected) }
+
+ context 'when the application is mounted at a relative URL' do
+ before do
+ stub_config_setting(relative_url_root: '/gitlab/root')
+ end
+
+ it { is_expected.to eq(expected) }
+ end
+ end
+ end
+
+ describe '#api_internal_request?' do
+ subject { request.api_internal_request? }
+
+ where(:path, :expected) do
+ '/' | false
+ '/groups' | false
+ '/api' | false
+ '/api/v4/groups/1' | false
+ '/api/v4/internal' | false
+ '/foo/api/v4/internal' | false
+
+ '/api/v4/internal/' | true
+ '/api/v4/internal/foo' | true
+ '/api/v1/internal/foo' | true
+ end
+
+ with_them do
+ it { is_expected.to eq(expected) }
+
+ context 'when the application is mounted at a relative URL' do
+ before do
+ stub_config_setting(relative_url_root: '/gitlab/root')
+ end
+
+ it { is_expected.to eq(expected) }
+ end
+ end
+ end
+
+ describe '#health_check_request?' do
+ subject { request.health_check_request? }
+
+ where(:path, :expected) do
+ '/' | false
+ '/groups' | false
+ '/foo/-/health' | false
+
+ '/-/health' | true
+ '/-/liveness' | true
+ '/-/readiness' | true
+ '/-/metrics' | true
+ '/-/health/foo' | true
+ '/-/liveness/foo' | true
+ '/-/readiness/foo' | true
+ '/-/metrics/foo' | true
+ end
+
+ with_them do
+ it { is_expected.to eq(expected) }
+
+ context 'when the application is mounted at a relative URL' do
+ before do
+ stub_config_setting(relative_url_root: '/gitlab/root')
+ end
+
+ it { is_expected.to eq(expected) }
+ end
+ end
+ end
+
+ describe '#container_registry_event?' do
+ subject { request.container_registry_event? }
+
+ where(:path, :expected) do
+ '/' | false
+ '/groups' | false
+ '/api/v4/container_registry_event' | false
+ '/foo/api/v4/container_registry_event/' | false
+
+ '/api/v4/container_registry_event/' | true
+ '/api/v4/container_registry_event/foo' | true
+ '/api/v1/container_registry_event/foo' | true
+ end
+
+ with_them do
+ it { is_expected.to eq(expected) }
+
+ context 'when the application is mounted at a relative URL' do
+ before do
+ stub_config_setting(relative_url_root: '/gitlab/root')
+ end
+
+ it { is_expected.to eq(expected) }
+ end
+ end
+ end
+
+ describe '#product_analytics_collector_request?' do
+ subject { request.product_analytics_collector_request? }
+
+ where(:path, :expected) do
+ '/' | false
+ '/groups' | false
+ '/-/collector' | false
+ '/-/collector/foo' | false
+ '/foo/-/collector/i' | false
+
+ '/-/collector/i' | true
+ '/-/collector/ifoo' | true
+ '/-/collector/i/foo' | true
+ end
+
+ with_them do
+ it { is_expected.to eq(expected) }
+
+ context 'when the application is mounted at a relative URL' do
+ before do
+ stub_config_setting(relative_url_root: '/gitlab/root')
+ end
+
+ it { is_expected.to eq(expected) }
+ end
+ end
+ end
+
+ describe '#should_be_skipped?' do
+ where(
+ api_internal_request: [true, false],
+ health_check_request: [true, false],
+ container_registry_event: [true, false]
+ )
+
+ with_them do
+ it 'returns true if any condition is true' do
+ allow(request).to receive(:api_internal_request?).and_return(api_internal_request)
+ allow(request).to receive(:health_check_request?).and_return(health_check_request)
+ allow(request).to receive(:container_registry_event?).and_return(container_registry_event)
+
+ expect(request.should_be_skipped?).to be(api_internal_request || health_check_request || container_registry_event)
+ end
+ end
+ end
+
+ describe '#web_request?' do
+ subject { request.web_request? }
+
+ where(:path, :expected) do
+ '/' | true
+ '/groups' | true
+ '/foo/api' | true
+
+ '/api' | false
+ '/api/v4/groups/1' | false
+ end
+
+ with_them do
+ it { is_expected.to eq(expected) }
+
+ context 'when the application is mounted at a relative URL' do
+ before do
+ stub_config_setting(relative_url_root: '/gitlab/root')
+ end
+
+ it { is_expected.to eq(expected) }
+ end
+ end
+ end
+
+ describe '#protected_path?' do
+ subject { request.protected_path? }
+
+ before do
+ stub_application_setting(protected_paths: [
+ '/protected',
+ '/secure'
+ ])
+ end
+
+ where(:path, :expected) do
+ '/' | false
+ '/groups' | false
+ '/foo/protected' | false
+ '/foo/secure' | false
+
+ '/protected' | true
+ '/secure' | true
+ '/secure/' | true
+ '/secure/foo' | true
+ end
+
+ with_them do
+ it { is_expected.to eq(expected) }
+
+ context 'when the application is mounted at a relative URL' do
+ before do
+ stub_config_setting(relative_url_root: '/gitlab/root')
+ end
+
+ it { is_expected.to eq(expected) }
+ end
+ end
+ end
+
+ describe '#frontend_request?', :allow_forgery_protection do
+ subject { request.send(:frontend_request?) }
+
+ let(:path) { '/' }
+
+ # Define these as local variables so we can use them in the `where` block.
+ valid_token = SecureRandom.base64(ActionController::RequestForgeryProtection::AUTHENTICITY_TOKEN_LENGTH)
+ other_token = SecureRandom.base64(ActionController::RequestForgeryProtection::AUTHENTICITY_TOKEN_LENGTH)
+
+ where(:session, :env, :expected) do
+ {} | {} | false # rubocop:disable Lint/BinaryOperatorWithIdenticalOperands
+ {} | { 'HTTP_X_CSRF_TOKEN' => valid_token } | false
+ { _csrf_token: valid_token } | { 'HTTP_X_CSRF_TOKEN' => other_token } | false
+ { _csrf_token: valid_token } | { 'HTTP_X_CSRF_TOKEN' => valid_token } | true
+ end
+
+ with_them do
+ it { is_expected.to eq(expected) }
+ end
+ end
+
describe '#deprecated_api_request?' do
- let(:env) { { 'REQUEST_METHOD' => 'GET', 'rack.input' => StringIO.new, 'PATH_INFO' => path, 'QUERY_STRING' => query } }
- let(:request) { ::Rack::Attack::Request.new(env) }
+ subject { request.send(:deprecated_api_request?) }
- subject { !!request.__send__(:deprecated_api_request?) }
+ let(:env) { { 'QUERY_STRING' => query } }
where(:path, :query, :expected) do
'/' | '' | false
@@ -42,6 +294,14 @@ RSpec.describe Gitlab::RackAttack::Request do
with_them do
it { is_expected.to eq(expected) }
+
+ context 'when the application is mounted at a relative URL' do
+ before do
+ stub_config_setting(relative_url_root: '/gitlab/root')
+ end
+
+ it { is_expected.to eq(expected) }
+ end
end
end
end
diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb
index 8d67350f0f3..54a0b282e99 100644
--- a/spec/lib/gitlab/regex_spec.rb
+++ b/spec/lib/gitlab/regex_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'spec_helper'
RSpec.describe Gitlab::Regex do
shared_examples_for 'project/group name chars regex' do
diff --git a/spec/lib/gitlab/request_profiler/profile_spec.rb b/spec/lib/gitlab/request_profiler/profile_spec.rb
index 2e9c75dde87..30e23a99b22 100644
--- a/spec/lib/gitlab/request_profiler/profile_spec.rb
+++ b/spec/lib/gitlab/request_profiler/profile_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'spec_helper'
RSpec.describe Gitlab::RequestProfiler::Profile do
let(:profile) { described_class.new(filename) }
diff --git a/spec/lib/gitlab/runtime_spec.rb b/spec/lib/gitlab/runtime_spec.rb
index 4627a8db82e..402b72b9220 100644
--- a/spec/lib/gitlab/runtime_spec.rb
+++ b/spec/lib/gitlab/runtime_spec.rb
@@ -26,8 +26,16 @@ RSpec.describe Gitlab::Runtime do
end
context "when unknown" do
- it "raises an exception when trying to identify" do
- expect { subject.identify }.to raise_error(subject::UnknownProcessError)
+ describe '.identify' do
+ it "raises an exception when trying to identify" do
+ expect { subject.identify }.to raise_error(subject::UnknownProcessError)
+ end
+ end
+
+ describe '.safe_identify' do
+ it "returns nil" do
+ expect(subject.safe_identify).to be_nil
+ end
end
end
@@ -37,8 +45,16 @@ RSpec.describe Gitlab::Runtime do
stub_const('::Rails::Console', double)
end
- it "raises an exception when trying to identify" do
- expect { subject.identify }.to raise_error(subject::AmbiguousProcessError)
+ describe '.identify' do
+ it "raises an exception when trying to identify" do
+ expect { subject.identify }.to raise_error(subject::AmbiguousProcessError)
+ end
+ end
+
+ describe '.safe_identify' do
+ it "returns nil" do
+ expect(subject.safe_identify).to be_nil
+ end
end
end
diff --git a/spec/lib/gitlab/security/scan_configuration_spec.rb b/spec/lib/gitlab/security/scan_configuration_spec.rb
index 0af029968e8..2e8a11dfda3 100644
--- a/spec/lib/gitlab/security/scan_configuration_spec.rb
+++ b/spec/lib/gitlab/security/scan_configuration_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe ::Gitlab::Security::ScanConfiguration do
+ using RSpec::Parameterized::TableSyntax
+
let_it_be(:project) { create(:project, :repository) }
let(:scan) { described_class.new(project: project, type: type, configured: configured) }
@@ -13,9 +15,11 @@ RSpec.describe ::Gitlab::Security::ScanConfiguration do
let(:configured) { true }
context 'with a core scanner' do
- let(:type) { :sast }
+ where(type: %i(sast sast_iac secret_detection))
- it { is_expected.to be_truthy }
+ with_them do
+ it { is_expected.to be_truthy }
+ end
end
context 'with custom scanner' do
@@ -38,27 +42,28 @@ RSpec.describe ::Gitlab::Security::ScanConfiguration do
subject { scan.configuration_path }
let(:configured) { true }
+ let(:type) { :sast }
- context 'with a non configurable scanner' do
- let(:type) { :secret_detection }
+ it { is_expected.to be_nil }
+ end
- it { is_expected.to be_nil }
- end
+ describe '#can_enable_by_merge_request?' do
+ subject { scan.can_enable_by_merge_request? }
- context 'with licensed scanner for FOSS environment' do
- let(:type) { :dast }
+ let(:configured) { true }
- before do
- stub_env('FOSS_ONLY', '1')
- end
+ context 'with a core scanner' do
+ where(type: %i(sast sast_iac secret_detection))
- it { is_expected.to be_nil }
+ with_them do
+ it { is_expected.to be_truthy }
+ end
end
- context 'with custom scanner' do
+ context 'with a custom scanner' do
let(:type) { :my_scanner }
- it { is_expected.to be_nil }
+ it { is_expected.to be_falsey }
end
end
end
diff --git a/spec/lib/gitlab/ssh_public_key_spec.rb b/spec/lib/gitlab/ssh_public_key_spec.rb
index 38486b313cb..cf5d2c3b455 100644
--- a/spec/lib/gitlab/ssh_public_key_spec.rb
+++ b/spec/lib/gitlab/ssh_public_key_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe Gitlab::SSHPublicKey, lib: true do
end
where(:name) do
- [:rsa, :dsa, :ecdsa, :ed25519]
+ [:rsa, :dsa, :ecdsa, :ed25519, :ecdsa_sk, :ed25519_sk]
end
with_them do
@@ -24,7 +24,7 @@ RSpec.describe Gitlab::SSHPublicKey, lib: true do
describe '.supported_types' do
it 'returns array with the names of supported technologies' do
expect(described_class.supported_types).to eq(
- [:rsa, :dsa, :ecdsa, :ed25519]
+ [:rsa, :dsa, :ecdsa, :ed25519, :ecdsa_sk, :ed25519_sk]
)
end
end
@@ -35,7 +35,9 @@ RSpec.describe Gitlab::SSHPublicKey, lib: true do
[:rsa, [1024, 2048, 3072, 4096]],
[:dsa, [1024, 2048, 3072]],
[:ecdsa, [256, 384, 521]],
- [:ed25519, [256]]
+ [:ed25519, [256]],
+ [:ecdsa_sk, [256]],
+ [:ed25519_sk, [256]]
]
end
@@ -53,6 +55,8 @@ RSpec.describe Gitlab::SSHPublicKey, lib: true do
ssh-dss
ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521
ssh-ed25519
+ sk-ecdsa-sha2-nistp256@openssh.com
+ sk-ssh-ed25519@openssh.com
)
)
end
@@ -64,7 +68,9 @@ RSpec.describe Gitlab::SSHPublicKey, lib: true do
[:rsa, %w(ssh-rsa)],
[:dsa, %w(ssh-dss)],
[:ecdsa, %w(ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521)],
- [:ed25519, %w(ssh-ed25519)]
+ [:ed25519, %w(ssh-ed25519)],
+ [:ecdsa_sk, %w(sk-ecdsa-sha2-nistp256@openssh.com)],
+ [:ed25519_sk, %w(sk-ssh-ed25519@openssh.com)]
]
end
@@ -122,13 +128,35 @@ RSpec.describe Gitlab::SSHPublicKey, lib: true do
rsa_key_8192
dsa_key_2048
ecdsa_key_256
- ed25519_key_256)
+ ed25519_key_256
+ ecdsa_sk_key_256
+ ed25519_sk_key_256)
end
with_them do
let(:key) { attributes_for(factory)[:key] }
it { is_expected.to be_valid }
+
+ context 'when key begins with options' do
+ let(:key) { "restrict,command='dump /home' #{attributes_for(factory)[:key]}" }
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'when key is in known_hosts format' do
+ context "when key begins with 'example.com'" do
+ let(:key) { "example.com #{attributes_for(factory)[:key]}" }
+
+ it { is_expected.to be_valid }
+ end
+
+ context "when key begins with '@revoked other.example.com'" do
+ let(:key) { "@revoked other.example.com #{attributes_for(factory)[:key]}" }
+
+ it { is_expected.to be_valid }
+ end
+ end
end
end
@@ -137,6 +165,40 @@ RSpec.describe Gitlab::SSHPublicKey, lib: true do
it { is_expected.not_to be_valid }
end
+
+ context 'when an unsupported SSH key algorithm' do
+ let(:key) { "unsupported-#{attributes_for(:rsa_key_2048)[:key]}" }
+
+ it { is_expected.not_to be_valid }
+ end
+ end
+
+ shared_examples 'raises error when the key is represented by a class that is not in the list of supported technologies' do
+ context 'when the key is represented by a class that is not in the list of supported technologies' do
+ it 'raises error' do
+ klass = Class.new
+ key = klass.new
+
+ allow(public_key).to receive(:key).and_return(key)
+
+ expect { subject }.to raise_error("Unsupported key type: #{key.class}")
+ end
+ end
+
+ context 'when the key is represented by a subclass of the class that is in the list of supported technologies' do
+ it 'raises error' do
+ rsa_subclass = Class.new(described_class.technology(:rsa).key_class) do
+ def initialize
+ end
+ end
+
+ key = rsa_subclass.new
+
+ allow(public_key).to receive(:key).and_return(key)
+
+ expect { subject }.to raise_error("Unsupported key type: #{key.class}")
+ end
+ end
end
describe '#type' do
@@ -147,7 +209,9 @@ RSpec.describe Gitlab::SSHPublicKey, lib: true do
[:rsa_key_2048, :rsa],
[:dsa_key_2048, :dsa],
[:ecdsa_key_256, :ecdsa],
- [:ed25519_key_256, :ed25519]
+ [:ed25519_key_256, :ed25519],
+ [:ecdsa_sk_key_256, :ecdsa_sk],
+ [:ed25519_sk_key_256, :ed25519_sk]
]
end
@@ -162,6 +226,8 @@ RSpec.describe Gitlab::SSHPublicKey, lib: true do
it { is_expected.to be_nil }
end
+
+ include_examples 'raises error when the key is represented by a class that is not in the list of supported technologies'
end
describe '#bits' do
@@ -175,7 +241,9 @@ RSpec.describe Gitlab::SSHPublicKey, lib: true do
[:rsa_key_8192, 8192],
[:dsa_key_2048, 2048],
[:ecdsa_key_256, 256],
- [:ed25519_key_256, 256]
+ [:ed25519_key_256, 256],
+ [:ecdsa_sk_key_256, 256],
+ [:ed25519_sk_key_256, 256]
]
end
@@ -190,6 +258,8 @@ RSpec.describe Gitlab::SSHPublicKey, lib: true do
it { is_expected.to be_nil }
end
+
+ include_examples 'raises error when the key is represented by a class that is not in the list of supported technologies'
end
describe '#fingerprint' do
@@ -203,7 +273,9 @@ RSpec.describe Gitlab::SSHPublicKey, lib: true do
[:rsa_key_8192, 'fb:53:7f:e9:2f:f7:17:aa:c8:32:52:06:8e:05:e2:82'],
[:dsa_key_2048, 'c8:85:1e:df:44:0f:20:00:3c:66:57:2b:21:10:5a:27'],
[:ecdsa_key_256, '67:a3:a9:7d:b8:e1:15:d4:80:40:21:34:bb:ed:97:38'],
- [:ed25519_key_256, 'e6:eb:45:8a:3c:59:35:5f:e9:5b:80:12:be:7e:22:73']
+ [:ed25519_key_256, 'e6:eb:45:8a:3c:59:35:5f:e9:5b:80:12:be:7e:22:73'],
+ [:ecdsa_sk_key_256, '56:b9:bc:99:3d:2f:cf:63:6b:70:d8:f9:40:7e:09:4c'],
+ [:ed25519_sk_key_256, 'f9:a0:64:0b:4b:72:72:0e:62:92:d7:04:14:74:1c:c9']
]
end
@@ -220,18 +292,20 @@ RSpec.describe Gitlab::SSHPublicKey, lib: true do
end
end
- describe '#fingerprint in SHA256 format' do
- subject { public_key.fingerprint("SHA256").gsub("SHA256:", "") if public_key.fingerprint("SHA256") }
+ describe '#fingerprint_sha256' do
+ subject { public_key.fingerprint_sha256 }
where(:factory, :fingerprint_sha256) do
[
- [:rsa_key_2048, 'GdtgO0eHbwLB+mK47zblkoXujkqKRZjgMQrHH6Kks3E'],
- [:rsa_key_4096, 'ByDU7hQ1JB95l6p53rHrffc4eXvEtqGUtQhS+Dhyy7g'],
- [:rsa_key_5120, 'PCCupLbFHScm4AbEufbGDvhBU27IM0MVAor715qKQK8'],
- [:rsa_key_8192, 'CtHFQAS+9Hb8z4vrv4gVQPsHjNN0WIZhWODaB1mQLs4'],
- [:dsa_key_2048, '+a3DQ7cU5GM+gaYOfmc0VWNnykHQSuth3VRcCpWuYNI'],
- [:ecdsa_key_256, 'C+I5k3D+IGeM6k5iBR1ZsphqTKV+7uvL/XZ5hcrTr7g'],
- [:ed25519_key_256, 'DCKAjzxWrdOTjaGKBBjtCW8qY5++GaiAJflrHPmp6W0']
+ [:rsa_key_2048, 'SHA256:GdtgO0eHbwLB+mK47zblkoXujkqKRZjgMQrHH6Kks3E'],
+ [:rsa_key_4096, 'SHA256:ByDU7hQ1JB95l6p53rHrffc4eXvEtqGUtQhS+Dhyy7g'],
+ [:rsa_key_5120, 'SHA256:PCCupLbFHScm4AbEufbGDvhBU27IM0MVAor715qKQK8'],
+ [:rsa_key_8192, 'SHA256:CtHFQAS+9Hb8z4vrv4gVQPsHjNN0WIZhWODaB1mQLs4'],
+ [:dsa_key_2048, 'SHA256:+a3DQ7cU5GM+gaYOfmc0VWNnykHQSuth3VRcCpWuYNI'],
+ [:ecdsa_key_256, 'SHA256:C+I5k3D+IGeM6k5iBR1ZsphqTKV+7uvL/XZ5hcrTr7g'],
+ [:ed25519_key_256, 'SHA256:DCKAjzxWrdOTjaGKBBjtCW8qY5++GaiAJflrHPmp6W0'],
+ [:ecdsa_sk_key_256, 'SHA256:N0sNKBgWKK8usPuPegtgzHQQA9vQ/dRhAEhwFDAnLA4'],
+ [:ed25519_sk_key_256, 'SHA256:U8IKRkIHed6vFMTflwweA3HhIf2DWgZ8EFTm9fgwOUk']
]
end
@@ -249,10 +323,19 @@ RSpec.describe Gitlab::SSHPublicKey, lib: true do
end
describe '#key_text' do
- let(:key) { 'this is not a key' }
+ where(:key_value) do
+ [
+ 'this is not a key',
+ nil
+ ]
+ end
- it 'carries the unmodified key data' do
- expect(public_key.key_text).to eq(key)
+ with_them do
+ let(:key) { key_value }
+
+ it 'carries the unmodified key data' do
+ expect(public_key.key_text).to eq(key)
+ end
end
end
end
diff --git a/spec/lib/gitlab/subscription_portal_spec.rb b/spec/lib/gitlab/subscription_portal_spec.rb
index 627d3bb42c7..fd3654afee0 100644
--- a/spec/lib/gitlab/subscription_portal_spec.rb
+++ b/spec/lib/gitlab/subscription_portal_spec.rb
@@ -61,7 +61,6 @@ RSpec.describe ::Gitlab::SubscriptionPortal do
:subscriptions_more_minutes_url | 'https://customers.staging.gitlab.com/buy_pipeline_minutes'
:subscriptions_more_storage_url | 'https://customers.staging.gitlab.com/buy_storage'
:subscriptions_manage_url | 'https://customers.staging.gitlab.com/subscriptions'
- :subscriptions_plans_url | 'https://about.gitlab.com/pricing/'
:subscriptions_instance_review_url | 'https://customers.staging.gitlab.com/instance_review'
:subscriptions_gitlab_plans_url | 'https://customers.staging.gitlab.com/gitlab_plans'
:edit_account_url | 'https://customers.staging.gitlab.com/customers/edit'
diff --git a/spec/lib/gitlab/untrusted_regexp/ruby_syntax_spec.rb b/spec/lib/gitlab/untrusted_regexp/ruby_syntax_spec.rb
index 42fc84cf076..b021abc9f25 100644
--- a/spec/lib/gitlab/untrusted_regexp/ruby_syntax_spec.rb
+++ b/spec/lib/gitlab/untrusted_regexp/ruby_syntax_spec.rb
@@ -1,8 +1,6 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
-require 'support/shared_examples/lib/gitlab/malicious_regexp_shared_examples'
-require 'support/helpers/stub_feature_flags'
+require 'spec_helper'
RSpec.describe Gitlab::UntrustedRegexp::RubySyntax do
describe '.matches_syntax?' do
@@ -77,6 +75,7 @@ RSpec.describe Gitlab::UntrustedRegexp::RubySyntax do
include StubFeatureFlags
before do
+ # When removed we could use `require 'fast_spec_helper'` again.
stub_feature_flags(allow_unsafe_ruby_regexp: true)
allow(Gitlab::UntrustedRegexp).to receive(:new).and_raise(RegexpError)
diff --git a/spec/lib/gitlab/usage/service_ping_report_spec.rb b/spec/lib/gitlab/usage/service_ping_report_spec.rb
new file mode 100644
index 00000000000..9b9b24ad128
--- /dev/null
+++ b/spec/lib/gitlab/usage/service_ping_report_spec.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::ServicePingReport, :use_clean_rails_memory_store_caching do
+ let(:usage_data) { { uuid: "1111" } }
+
+ context 'for output: :all_metrics_values' do
+ it 'generates the service ping' do
+ expect(Gitlab::UsageData).to receive(:data)
+
+ described_class.for(output: :all_metrics_values)
+ end
+ end
+
+ context 'for output: :metrics_queries' do
+ it 'generates the service ping' do
+ expect(Gitlab::UsageDataQueries).to receive(:data)
+
+ described_class.for(output: :metrics_queries)
+ end
+ end
+
+ context 'for output: :non_sql_metrics_values' do
+ it 'generates the service ping' do
+ expect(Gitlab::UsageDataNonSqlMetrics).to receive(:data)
+
+ described_class.for(output: :non_sql_metrics_values)
+ end
+ end
+
+ context 'when using cached' do
+ context 'for cached: true' do
+ let(:new_usage_data) { { uuid: "1112" } }
+
+ it 'caches the values' do
+ allow(Gitlab::UsageData).to receive(:data).and_return(usage_data, new_usage_data)
+
+ expect(described_class.for(output: :all_metrics_values)).to eq(usage_data)
+ expect(described_class.for(output: :all_metrics_values, cached: true)).to eq(usage_data)
+
+ expect(Rails.cache.fetch('usage_data')).to eq(usage_data)
+ end
+
+ it 'writes to cache and returns fresh data' do
+ allow(Gitlab::UsageData).to receive(:data).and_return(usage_data, new_usage_data)
+
+ expect(described_class.for(output: :all_metrics_values)).to eq(usage_data)
+ expect(described_class.for(output: :all_metrics_values)).to eq(new_usage_data)
+ expect(described_class.for(output: :all_metrics_values, cached: true)).to eq(new_usage_data)
+
+ expect(Rails.cache.fetch('usage_data')).to eq(new_usage_data)
+ end
+ end
+
+ context 'when no caching' do
+ let(:new_usage_data) { { uuid: "1112" } }
+
+ it 'returns fresh data' do
+ allow(Gitlab::UsageData).to receive(:data).and_return(usage_data, new_usage_data)
+
+ expect(described_class.for(output: :all_metrics_values)).to eq(usage_data)
+ expect(described_class.for(output: :all_metrics_values)).to eq(new_usage_data)
+
+ expect(Rails.cache.fetch('usage_data')).to eq(new_usage_data)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb
index f7ff68af8a2..5e74ea3293c 100644
--- a/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb
@@ -49,7 +49,8 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
'secure',
'importer',
'network_policies',
- 'geo'
+ 'geo',
+ 'growth'
)
end
end
diff --git a/spec/lib/gitlab/usage_data_counters/jetbrains_plugin_activity_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/jetbrains_plugin_activity_unique_counter_spec.rb
new file mode 100644
index 00000000000..4169546edad
--- /dev/null
+++ b/spec/lib/gitlab/usage_data_counters/jetbrains_plugin_activity_unique_counter_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::UsageDataCounters::JetBrainsPluginActivityUniqueCounter, :clean_gitlab_redis_shared_state do # rubocop:disable RSpec/FilePath
+ let(:user1) { build(:user, id: 1) }
+ let(:user2) { build(:user, id: 2) }
+ let(:time) { Time.current }
+ let(:action) { described_class::JETBRAINS_API_REQUEST_ACTION }
+ let(:user_agent) { { user_agent: 'gitlab-jetbrains-plugin/0.0.1 intellij-idea/2021.2.4 java/11.0.13 mac-os-x/aarch64/12.1' } }
+
+ context 'when tracking a jetbrains api request' do
+ it_behaves_like 'a request from an extension'
+ end
+end
diff --git a/spec/lib/gitlab/usage_data_counters/vscode_extension_activity_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/vscode_extension_activity_unique_counter_spec.rb
new file mode 100644
index 00000000000..640dadd8c0b
--- /dev/null
+++ b/spec/lib/gitlab/usage_data_counters/vscode_extension_activity_unique_counter_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::UsageDataCounters::VSCodeExtensionActivityUniqueCounter, :clean_gitlab_redis_shared_state do # rubocop:disable RSpec/FilePath
+ let(:user1) { build(:user, id: 1) }
+ let(:user2) { build(:user, id: 2) }
+ let(:time) { Time.current }
+ let(:action) { described_class::VS_CODE_API_REQUEST_ACTION }
+ let(:user_agent) { { user_agent: 'vs-code-gitlab-workflow/3.11.1 VSCode/1.52.1 Node.js/12.14.1 (darwin; x64)' } }
+
+ context 'when tracking a vs code api request' do
+ it_behaves_like 'a request from an extension'
+ end
+end
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index 427e8e67090..bea07dd9c43 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -12,8 +12,8 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
stub_database_flavor_check('Cloud SQL for PostgreSQL')
end
- describe '.uncached_data' do
- subject { described_class.uncached_data }
+ describe '.data' do
+ subject { described_class.data }
it 'includes basic top and second level keys' do
is_expected.to include(:counts)
@@ -556,8 +556,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
expect(count_data[:issues_created_from_alerts]).to eq(3)
expect(count_data[:issues_created_manually_from_alerts]).to eq(1)
expect(count_data[:alert_bot_incident_issues]).to eq(4)
- expect(count_data[:incident_labeled_issues]).to eq(3)
-
expect(count_data[:clusters_enabled]).to eq(6)
expect(count_data[:project_clusters_enabled]).to eq(4)
expect(count_data[:group_clusters_enabled]).to eq(1)
diff --git a/spec/lib/gitlab/utils_spec.rb b/spec/lib/gitlab/utils_spec.rb
index d756ec5ef83..ba6997adbf6 100644
--- a/spec/lib/gitlab/utils_spec.rb
+++ b/spec/lib/gitlab/utils_spec.rb
@@ -439,6 +439,23 @@ RSpec.describe Gitlab::Utils do
end
end
+ describe '.add_url_parameters' do
+ subject { described_class.add_url_parameters(url, params) }
+
+ where(:url, :params, :expected_url) do
+ nil | nil | ''
+ nil | { b: 3, a: 2 } | '?a=2&b=3'
+ 'https://gitlab.com' | nil | 'https://gitlab.com'
+ 'https://gitlab.com' | { b: 3, a: 2 } | 'https://gitlab.com?a=2&b=3'
+ 'https://gitlab.com?a=1#foo' | { b: 3, 'a': 2 } | 'https://gitlab.com?a=2&b=3#foo'
+ 'https://gitlab.com?a=1#foo' | [[:b, 3], [:a, 2]] | 'https://gitlab.com?a=2&b=3#foo'
+ end
+
+ with_them do
+ it { is_expected.to eq(expected_url) }
+ end
+ end
+
describe '.removes_sensitive_data_from_url' do
it 'returns string object' do
expect(described_class.removes_sensitive_data_from_url('http://gitlab.com')).to be_instance_of(String)
diff --git a/spec/lib/gitlab/web_ide/config/entry/global_spec.rb b/spec/lib/gitlab/web_ide/config/entry/global_spec.rb
index 9af21685c9e..66c9bb00ee9 100644
--- a/spec/lib/gitlab/web_ide/config/entry/global_spec.rb
+++ b/spec/lib/gitlab/web_ide/config/entry/global_spec.rb
@@ -108,7 +108,7 @@ RSpec.describe Gitlab::WebIde::Config::Entry::Global do
describe '#errors' do
it 'reports errors about missing script' do
expect(global.errors)
- .to include "terminal:before_script config should be an array containing strings and arrays of strings"
+ .to include "terminal:before_script config should be a string or a nested array of strings up to 10 levels deep"
end
end
end
diff --git a/spec/lib/gitlab/web_ide/config_spec.rb b/spec/lib/gitlab/web_ide/config_spec.rb
index 7a9011d03c0..7ee9d40410c 100644
--- a/spec/lib/gitlab/web_ide/config_spec.rb
+++ b/spec/lib/gitlab/web_ide/config_spec.rb
@@ -56,7 +56,7 @@ RSpec.describe Gitlab::WebIde::Config do
end
context 'when config logic is incorrect' do
- let(:yml) { 'terminal: { before_script: "ls" }' }
+ let(:yml) { 'terminal: { before_script: 123 }' }
describe '#valid?' do
it 'is not valid' do
diff --git a/spec/lib/gitlab/webpack/file_loader_spec.rb b/spec/lib/gitlab/webpack/file_loader_spec.rb
index 34d00b9f106..6475ef58611 100644
--- a/spec/lib/gitlab/webpack/file_loader_spec.rb
+++ b/spec/lib/gitlab/webpack/file_loader_spec.rb
@@ -1,8 +1,6 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
-require 'support/helpers/file_read_helpers'
-require 'support/webmock'
+require 'spec_helper'
RSpec.describe Gitlab::Webpack::FileLoader do
include FileReadHelpers
diff --git a/spec/lib/gitlab_edition_spec.rb b/spec/lib/gitlab_edition_spec.rb
index 2f1316819ec..6fc4312252d 100644
--- a/spec/lib/gitlab_edition_spec.rb
+++ b/spec/lib/gitlab_edition_spec.rb
@@ -3,18 +3,22 @@
require 'spec_helper'
RSpec.describe GitlabEdition do
+ def remove_instance_variable(ivar)
+ described_class.remove_instance_variable(ivar) if described_class.instance_variable_defined?(ivar)
+ end
+
before do
# Make sure the ENV is clean
stub_env('FOSS_ONLY', nil)
stub_env('EE_ONLY', nil)
- described_class.instance_variable_set(:@is_ee, nil)
- described_class.instance_variable_set(:@is_jh, nil)
+ remove_instance_variable(:@is_ee)
+ remove_instance_variable(:@is_jh)
end
after do
- described_class.instance_variable_set(:@is_ee, nil)
- described_class.instance_variable_set(:@is_jh, nil)
+ remove_instance_variable(:@is_ee)
+ remove_instance_variable(:@is_jh)
end
describe '.root' do
@@ -51,7 +55,7 @@ RSpec.describe GitlabEdition do
allow(described_class).to receive(:ee?).and_return(false)
end
- it 'returns the exyensions according to the current edition' do
+ it 'returns the extensions according to the current edition' do
expect(described_class.extensions).to be_empty
end
end
@@ -77,7 +81,7 @@ RSpec.describe GitlabEdition do
end
describe '.ee?' do
- context 'for EE' do
+ context 'when EE' do
before do
stub_path('ee/app/models/license.rb', exist?: true)
end
@@ -109,7 +113,7 @@ RSpec.describe GitlabEdition do
end
end
- context 'for CE' do
+ context 'when CE' do
before do
stub_path('ee/app/models/license.rb', exist?: false)
end
@@ -121,12 +125,9 @@ RSpec.describe GitlabEdition do
end
describe '.jh?' do
- context 'for JH' do
+ context 'when JH' do
before do
- stub_path(
- 'ee/app/models/license.rb',
- 'jh',
- exist?: true)
+ stub_path('ee/app/models/license.rb', 'jh', exist?: true)
end
context 'when using default FOSS_ONLY and EE_ONLY' do
diff --git a/spec/lib/gitlab_spec.rb b/spec/lib/gitlab_spec.rb
index 49ba4debe31..57a4bdc9bb5 100644
--- a/spec/lib/gitlab_spec.rb
+++ b/spec/lib/gitlab_spec.rb
@@ -99,6 +99,13 @@ RSpec.describe Gitlab do
expect(described_class.com?).to eq true
end
+ it 'is true when on other gitlab subdomain with hyphen' do
+ url_with_subdomain = Gitlab::Saas.com_url.gsub('https://', 'https://test-example.')
+ stub_config_setting(url: url_with_subdomain)
+
+ expect(described_class.com?).to eq true
+ end
+
it 'is false when not on GitLab.com' do
stub_config_setting(url: 'http://example.com')
diff --git a/spec/lib/google_api/cloud_platform/client_spec.rb b/spec/lib/google_api/cloud_platform/client_spec.rb
index 3284c9cd0d1..29e5445cfaa 100644
--- a/spec/lib/google_api/cloud_platform/client_spec.rb
+++ b/spec/lib/google_api/cloud_platform/client_spec.rb
@@ -6,6 +6,8 @@ RSpec.describe GoogleApi::CloudPlatform::Client do
let(:token) { 'token' }
let(:client) { described_class.new(token, nil) }
let(:user_agent_options) { client.instance_eval { user_agent_header } }
+ let(:gcp_project_id) { String('gcp_proj_id') }
+ let(:operation) { true }
describe '.session_key_for_redirect_uri' do
let(:state) { 'random_string' }
@@ -60,7 +62,7 @@ RSpec.describe GoogleApi::CloudPlatform::Client do
before do
allow_any_instance_of(Google::Apis::ContainerV1::ContainerService)
.to receive(:get_zone_cluster).with(any_args, options: user_agent_options)
- .and_return(gke_cluster)
+ .and_return(gke_cluster)
end
it { is_expected.to eq(gke_cluster) }
@@ -122,7 +124,7 @@ RSpec.describe GoogleApi::CloudPlatform::Client do
before do
allow_any_instance_of(Google::Apis::ContainerV1beta1::ContainerService)
.to receive(:create_cluster).with(any_args)
- .and_return(operation)
+ .and_return(operation)
end
it 'sets corresponded parameters' do
@@ -172,7 +174,7 @@ RSpec.describe GoogleApi::CloudPlatform::Client do
before do
allow_any_instance_of(Google::Apis::ContainerV1::ContainerService)
.to receive(:get_zone_operation).with(any_args, options: user_agent_options)
- .and_return(operation)
+ .and_return(operation)
end
it { is_expected.to eq(operation) }
@@ -244,7 +246,7 @@ RSpec.describe GoogleApi::CloudPlatform::Client do
let(:operation) { double('Service Account Key') }
- it 'class Google Api IamService#create_service_account_key' do
+ it 'calls Google Api IamService#create_service_account_key' do
expect_any_instance_of(Google::Apis::IamV1::IamService)
.to receive(:create_service_account_key)
.with(any_args)
@@ -252,4 +254,84 @@ RSpec.describe GoogleApi::CloudPlatform::Client do
is_expected.to eq(operation)
end
end
+
+ describe 'grant_service_account_roles' do
+ subject { client.grant_service_account_roles(spy, spy) }
+
+ it 'calls Google Api CloudResourceManager#set_iam_policy' do
+ mock_gcp_id = 'mock-gcp-id'
+ mock_email = 'mock@email.com'
+ mock_policy = Struct.new(:bindings).new([])
+ mock_body = []
+
+ expect(Google::Apis::CloudresourcemanagerV1::Binding).to receive(:new)
+ .with({ 'role': 'roles/iam.serviceAccountUser', 'members': ["serviceAccount:#{mock_email}"] })
+
+ expect(Google::Apis::CloudresourcemanagerV1::Binding).to receive(:new)
+ .with({ 'role': 'roles/artifactregistry.admin', 'members': ["serviceAccount:#{mock_email}"] })
+
+ expect(Google::Apis::CloudresourcemanagerV1::Binding).to receive(:new)
+ .with({ 'role': 'roles/cloudbuild.builds.builder', 'members': ["serviceAccount:#{mock_email}"] })
+
+ expect(Google::Apis::CloudresourcemanagerV1::Binding).to receive(:new)
+ .with({ 'role': 'roles/run.admin', 'members': ["serviceAccount:#{mock_email}"] })
+
+ expect(Google::Apis::CloudresourcemanagerV1::Binding).to receive(:new)
+ .with({ 'role': 'roles/storage.admin', 'members': ["serviceAccount:#{mock_email}"] })
+
+ expect(Google::Apis::CloudresourcemanagerV1::Binding).to receive(:new)
+ .with({ 'role': 'roles/cloudsql.admin', 'members': ["serviceAccount:#{mock_email}"] })
+
+ expect(Google::Apis::CloudresourcemanagerV1::Binding).to receive(:new)
+ .with({ 'role': 'roles/browser', 'members': ["serviceAccount:#{mock_email}"] })
+
+ expect(Google::Apis::CloudresourcemanagerV1::SetIamPolicyRequest).to receive(:new).and_return([])
+
+ expect_next_instance_of(Google::Apis::CloudresourcemanagerV1::CloudResourceManagerService) do |instance|
+ expect(instance).to receive(:get_project_iam_policy)
+ .with(mock_gcp_id)
+ .and_return(mock_policy)
+ expect(instance).to receive(:set_project_iam_policy)
+ .with(mock_gcp_id, mock_body)
+ end
+
+ client.grant_service_account_roles(mock_gcp_id, mock_email)
+ end
+ end
+
+ describe '#enable_cloud_run' do
+ subject { client.enable_cloud_run(gcp_project_id) }
+
+ it 'calls Google Api IamService#create_service_account_key' do
+ expect_any_instance_of(Google::Apis::ServiceusageV1::ServiceUsageService)
+ .to receive(:enable_service)
+ .with("projects/#{gcp_project_id}/services/run.googleapis.com")
+ .and_return(operation)
+ is_expected.to eq(operation)
+ end
+ end
+
+ describe '#enable_artifacts_registry' do
+ subject { client.enable_artifacts_registry(gcp_project_id) }
+
+ it 'calls Google Api IamService#create_service_account_key' do
+ expect_any_instance_of(Google::Apis::ServiceusageV1::ServiceUsageService)
+ .to receive(:enable_service)
+ .with("projects/#{gcp_project_id}/services/artifactregistry.googleapis.com")
+ .and_return(operation)
+ is_expected.to eq(operation)
+ end
+ end
+
+ describe '#enable_cloud_build' do
+ subject { client.enable_cloud_build(gcp_project_id) }
+
+ it 'calls Google Api IamService#create_service_account_key' do
+ expect_any_instance_of(Google::Apis::ServiceusageV1::ServiceUsageService)
+ .to receive(:enable_service)
+ .with("projects/#{gcp_project_id}/services/cloudbuild.googleapis.com")
+ .and_return(operation)
+ is_expected.to eq(operation)
+ end
+ end
end
diff --git a/spec/lib/learn_gitlab/project_spec.rb b/spec/lib/learn_gitlab/project_spec.rb
index 523703761bf..5d649740c65 100644
--- a/spec/lib/learn_gitlab/project_spec.rb
+++ b/spec/lib/learn_gitlab/project_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe LearnGitlab::Project do
let_it_be(:current_user) { create(:user) }
let_it_be(:learn_gitlab_project) { create(:project, name: LearnGitlab::Project::PROJECT_NAME) }
+ let_it_be(:learn_gitlab_ultimate_trial_project) { create(:project, name: LearnGitlab::Project::PROJECT_NAME_ULTIMATE_TRIAL) }
let_it_be(:learn_gitlab_board) { create(:board, project: learn_gitlab_project, name: LearnGitlab::Project::BOARD_NAME) }
let_it_be(:learn_gitlab_label) { create(:label, project: learn_gitlab_project, name: LearnGitlab::Project::LABEL_NAME) }
@@ -45,6 +46,12 @@ RSpec.describe LearnGitlab::Project do
subject { described_class.new(current_user).project }
it { is_expected.to eq learn_gitlab_project }
+
+ context 'when it is created during trial signup' do
+ let_it_be(:learn_gitlab_project) { create(:project, name: LearnGitlab::Project::PROJECT_NAME_ULTIMATE_TRIAL) }
+
+ it { is_expected.to eq learn_gitlab_project }
+ end
end
describe '.board' do
diff --git a/spec/lib/peek/views/detailed_view_spec.rb b/spec/lib/peek/views/detailed_view_spec.rb
index 8d6d9a829ef..149685b243a 100644
--- a/spec/lib/peek/views/detailed_view_spec.rb
+++ b/spec/lib/peek/views/detailed_view_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'spec_helper'
RSpec.describe Peek::Views::DetailedView, :request_store do
context 'when a class defines thresholds' do
diff --git a/spec/lib/security/ci_configuration/container_scanning_build_action_spec.rb b/spec/lib/security/ci_configuration/container_scanning_build_action_spec.rb
new file mode 100644
index 00000000000..38066e41c53
--- /dev/null
+++ b/spec/lib/security/ci_configuration/container_scanning_build_action_spec.rb
@@ -0,0 +1,191 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Security::CiConfiguration::ContainerScanningBuildAction do
+ subject(:result) { described_class.new(auto_devops_enabled, gitlab_ci_content).generate }
+
+ let(:params) { {} }
+
+ context 'with existing .gitlab-ci.yml' do
+ let(:auto_devops_enabled) { false }
+
+ context 'container_scanning has not been included' do
+ let(:expected_yml) do
+ <<-CI_YML.strip_heredoc
+ # You can override the included template(s) by including variable overrides
+ # SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
+ # Secret Detection customization: https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings
+ # Dependency Scanning customization: https://docs.gitlab.com/ee/user/application_security/dependency_scanning/#customizing-the-dependency-scanning-settings
+ # Container Scanning customization: https://docs.gitlab.com/ee/user/application_security/container_scanning/#customizing-the-container-scanning-settings
+ # Note that environment variables can be set in several places
+ # See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence
+
+ # container_scanning:
+ # variables:
+ # DOCKER_IMAGE: ...
+ # DOCKER_USER: ...
+ # DOCKER_PASSWORD: ...
+ stages:
+ - test
+ - security
+ variables:
+ RANDOM: make sure this persists
+ include:
+ - template: existing.yml
+ - template: Security/Container-Scanning.gitlab-ci.yml
+ CI_YML
+ end
+
+ context 'template includes are an array' do
+ let(:gitlab_ci_content) do
+ { "stages" => %w(test security),
+ "variables" => { "RANDOM" => "make sure this persists" },
+ "include" => [{ "template" => "existing.yml" }] }
+ end
+
+ it 'generates the correct YML' do
+ expect(result[:action]).to eq('update')
+ expect(result[:content]).to eq(expected_yml)
+ end
+ end
+
+ context 'template include is not an array' do
+ let(:gitlab_ci_content) do
+ { "stages" => %w(test security),
+ "variables" => { "RANDOM" => "make sure this persists" },
+ "include" => { "template" => "existing.yml" } }
+ end
+
+ it 'generates the correct YML' do
+ expect(result[:action]).to eq('update')
+ expect(result[:content]).to eq(expected_yml)
+ end
+ end
+ end
+
+ context 'container_scanning has been included' do
+ let(:expected_yml) do
+ <<-CI_YML.strip_heredoc
+ # You can override the included template(s) by including variable overrides
+ # SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
+ # Secret Detection customization: https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings
+ # Dependency Scanning customization: https://docs.gitlab.com/ee/user/application_security/dependency_scanning/#customizing-the-dependency-scanning-settings
+ # Container Scanning customization: https://docs.gitlab.com/ee/user/application_security/container_scanning/#customizing-the-container-scanning-settings
+ # Note that environment variables can be set in several places
+ # See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence
+
+ # container_scanning:
+ # variables:
+ # DOCKER_IMAGE: ...
+ # DOCKER_USER: ...
+ # DOCKER_PASSWORD: ...
+ stages:
+ - test
+ variables:
+ RANDOM: make sure this persists
+ include:
+ - template: Security/Container-Scanning.gitlab-ci.yml
+ CI_YML
+ end
+
+ context 'container_scanning template include are an array' do
+ let(:gitlab_ci_content) do
+ { "stages" => %w(test),
+ "variables" => { "RANDOM" => "make sure this persists" },
+ "include" => [{ "template" => "Security/Container-Scanning.gitlab-ci.yml" }] }
+ end
+
+ it 'generates the correct YML' do
+ expect(result[:action]).to eq('update')
+ expect(result[:content]).to eq(expected_yml)
+ end
+ end
+
+ context 'container_scanning template include is not an array' do
+ let(:gitlab_ci_content) do
+ { "stages" => %w(test),
+ "variables" => { "RANDOM" => "make sure this persists" },
+ "include" => { "template" => "Security/Container-Scanning.gitlab-ci.yml" } }
+ end
+
+ it 'generates the correct YML' do
+ expect(result[:action]).to eq('update')
+ expect(result[:content]).to eq(expected_yml)
+ end
+ end
+ end
+ end
+
+ context 'with no .gitlab-ci.yml' do
+ let(:gitlab_ci_content) { nil }
+
+ context 'autodevops disabled' do
+ let(:auto_devops_enabled) { false }
+ let(:expected_yml) do
+ <<-CI_YML.strip_heredoc
+ # You can override the included template(s) by including variable overrides
+ # SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
+ # Secret Detection customization: https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings
+ # Dependency Scanning customization: https://docs.gitlab.com/ee/user/application_security/dependency_scanning/#customizing-the-dependency-scanning-settings
+ # Container Scanning customization: https://docs.gitlab.com/ee/user/application_security/container_scanning/#customizing-the-container-scanning-settings
+ # Note that environment variables can be set in several places
+ # See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence
+
+ # container_scanning:
+ # variables:
+ # DOCKER_IMAGE: ...
+ # DOCKER_USER: ...
+ # DOCKER_PASSWORD: ...
+ include:
+ - template: Security/Container-Scanning.gitlab-ci.yml
+ CI_YML
+ end
+
+ it 'generates the correct YML' do
+ expect(result[:action]).to eq('create')
+ expect(result[:content]).to eq(expected_yml)
+ end
+ end
+
+ context 'with autodevops enabled' do
+ let(:auto_devops_enabled) { true }
+ let(:expected_yml) do
+ <<-CI_YML.strip_heredoc
+ # You can override the included template(s) by including variable overrides
+ # SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
+ # Secret Detection customization: https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings
+ # Dependency Scanning customization: https://docs.gitlab.com/ee/user/application_security/dependency_scanning/#customizing-the-dependency-scanning-settings
+ # Container Scanning customization: https://docs.gitlab.com/ee/user/application_security/container_scanning/#customizing-the-container-scanning-settings
+ # Note that environment variables can be set in several places
+ # See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence
+
+ # container_scanning:
+ # variables:
+ # DOCKER_IMAGE: ...
+ # DOCKER_USER: ...
+ # DOCKER_PASSWORD: ...
+ include:
+ - template: Auto-DevOps.gitlab-ci.yml
+ CI_YML
+ end
+
+ before do
+ allow_next_instance_of(described_class) do |secret_detection_build_actions|
+ allow(secret_detection_build_actions).to receive(:auto_devops_stages).and_return(fast_auto_devops_stages)
+ end
+ end
+
+ it 'generates the correct YML' do
+ expect(result[:action]).to eq('create')
+ expect(result[:content]).to eq(expected_yml)
+ end
+ end
+ end
+
+ # stubbing this method allows this spec file to use fast_spec_helper
+ def fast_auto_devops_stages
+ auto_devops_template = YAML.safe_load( File.read('lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml') )
+ auto_devops_template['stages']
+ end
+end
diff --git a/spec/lib/security/ci_configuration/sast_build_action_spec.rb b/spec/lib/security/ci_configuration/sast_build_action_spec.rb
index d93175249f5..6f702e51b73 100644
--- a/spec/lib/security/ci_configuration/sast_build_action_spec.rb
+++ b/spec/lib/security/ci_configuration/sast_build_action_spec.rb
@@ -324,6 +324,7 @@ RSpec.describe Security::CiConfiguration::SastBuildAction do
# SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
# Secret Detection customization: https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings
# Dependency Scanning customization: https://docs.gitlab.com/ee/user/application_security/dependency_scanning/#customizing-the-dependency-scanning-settings
+ # Container Scanning customization: https://docs.gitlab.com/ee/user/application_security/container_scanning/#customizing-the-container-scanning-settings
# Note that environment variables can be set in several places
# See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence
stages:
@@ -344,6 +345,7 @@ RSpec.describe Security::CiConfiguration::SastBuildAction do
# SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
# Secret Detection customization: https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings
# Dependency Scanning customization: https://docs.gitlab.com/ee/user/application_security/dependency_scanning/#customizing-the-dependency-scanning-settings
+ # Container Scanning customization: https://docs.gitlab.com/ee/user/application_security/container_scanning/#customizing-the-container-scanning-settings
# Note that environment variables can be set in several places
# See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence
stages:
@@ -361,6 +363,7 @@ RSpec.describe Security::CiConfiguration::SastBuildAction do
# SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
# Secret Detection customization: https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings
# Dependency Scanning customization: https://docs.gitlab.com/ee/user/application_security/dependency_scanning/#customizing-the-dependency-scanning-settings
+ # Container Scanning customization: https://docs.gitlab.com/ee/user/application_security/container_scanning/#customizing-the-container-scanning-settings
# Note that environment variables can be set in several places
# See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence
stages:
@@ -384,6 +387,7 @@ RSpec.describe Security::CiConfiguration::SastBuildAction do
# SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
# Secret Detection customization: https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings
# Dependency Scanning customization: https://docs.gitlab.com/ee/user/application_security/dependency_scanning/#customizing-the-dependency-scanning-settings
+ # Container Scanning customization: https://docs.gitlab.com/ee/user/application_security/container_scanning/#customizing-the-container-scanning-settings
# Note that environment variables can be set in several places
# See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence
stages:
@@ -420,6 +424,7 @@ RSpec.describe Security::CiConfiguration::SastBuildAction do
# SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
# Secret Detection customization: https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings
# Dependency Scanning customization: https://docs.gitlab.com/ee/user/application_security/dependency_scanning/#customizing-the-dependency-scanning-settings
+ # Container Scanning customization: https://docs.gitlab.com/ee/user/application_security/container_scanning/#customizing-the-container-scanning-settings
# Note that environment variables can be set in several places
# See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence
stages:
@@ -445,6 +450,7 @@ RSpec.describe Security::CiConfiguration::SastBuildAction do
# SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
# Secret Detection customization: https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings
# Dependency Scanning customization: https://docs.gitlab.com/ee/user/application_security/dependency_scanning/#customizing-the-dependency-scanning-settings
+ # Container Scanning customization: https://docs.gitlab.com/ee/user/application_security/container_scanning/#customizing-the-container-scanning-settings
# Note that environment variables can be set in several places
# See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence
stages:
@@ -468,6 +474,7 @@ RSpec.describe Security::CiConfiguration::SastBuildAction do
# SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
# Secret Detection customization: https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings
# Dependency Scanning customization: https://docs.gitlab.com/ee/user/application_security/dependency_scanning/#customizing-the-dependency-scanning-settings
+ # Container Scanning customization: https://docs.gitlab.com/ee/user/application_security/container_scanning/#customizing-the-container-scanning-settings
# Note that environment variables can be set in several places
# See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence
stages:
@@ -492,6 +499,7 @@ RSpec.describe Security::CiConfiguration::SastBuildAction do
# SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
# Secret Detection customization: https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings
# Dependency Scanning customization: https://docs.gitlab.com/ee/user/application_security/dependency_scanning/#customizing-the-dependency-scanning-settings
+ # Container Scanning customization: https://docs.gitlab.com/ee/user/application_security/container_scanning/#customizing-the-container-scanning-settings
# Note that environment variables can be set in several places
# See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence
stages:
@@ -516,6 +524,7 @@ RSpec.describe Security::CiConfiguration::SastBuildAction do
# SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
# Secret Detection customization: https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings
# Dependency Scanning customization: https://docs.gitlab.com/ee/user/application_security/dependency_scanning/#customizing-the-dependency-scanning-settings
+ # Container Scanning customization: https://docs.gitlab.com/ee/user/application_security/container_scanning/#customizing-the-container-scanning-settings
# Note that environment variables can be set in several places
# See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence
stages:
diff --git a/spec/lib/security/ci_configuration/sast_iac_build_action_spec.rb b/spec/lib/security/ci_configuration/sast_iac_build_action_spec.rb
index ecd1602dd9e..4c459058368 100644
--- a/spec/lib/security/ci_configuration/sast_iac_build_action_spec.rb
+++ b/spec/lib/security/ci_configuration/sast_iac_build_action_spec.rb
@@ -17,6 +17,7 @@ RSpec.describe Security::CiConfiguration::SastIacBuildAction do
# SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
# Secret Detection customization: https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings
# Dependency Scanning customization: https://docs.gitlab.com/ee/user/application_security/dependency_scanning/#customizing-the-dependency-scanning-settings
+ # Container Scanning customization: https://docs.gitlab.com/ee/user/application_security/container_scanning/#customizing-the-container-scanning-settings
# Note that environment variables can be set in several places
# See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence
stages:
@@ -64,6 +65,7 @@ RSpec.describe Security::CiConfiguration::SastIacBuildAction do
# SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
# Secret Detection customization: https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings
# Dependency Scanning customization: https://docs.gitlab.com/ee/user/application_security/dependency_scanning/#customizing-the-dependency-scanning-settings
+ # Container Scanning customization: https://docs.gitlab.com/ee/user/application_security/container_scanning/#customizing-the-container-scanning-settings
# Note that environment variables can be set in several places
# See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence
stages:
@@ -114,6 +116,7 @@ RSpec.describe Security::CiConfiguration::SastIacBuildAction do
# SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
# Secret Detection customization: https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings
# Dependency Scanning customization: https://docs.gitlab.com/ee/user/application_security/dependency_scanning/#customizing-the-dependency-scanning-settings
+ # Container Scanning customization: https://docs.gitlab.com/ee/user/application_security/container_scanning/#customizing-the-container-scanning-settings
# Note that environment variables can be set in several places
# See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence
include:
@@ -135,6 +138,7 @@ RSpec.describe Security::CiConfiguration::SastIacBuildAction do
# SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
# Secret Detection customization: https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings
# Dependency Scanning customization: https://docs.gitlab.com/ee/user/application_security/dependency_scanning/#customizing-the-dependency-scanning-settings
+ # Container Scanning customization: https://docs.gitlab.com/ee/user/application_security/container_scanning/#customizing-the-container-scanning-settings
# Note that environment variables can be set in several places
# See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence
include:
diff --git a/spec/lib/security/ci_configuration/secret_detection_build_action_spec.rb b/spec/lib/security/ci_configuration/secret_detection_build_action_spec.rb
index 146c60ffb6e..4d9860ca4a5 100644
--- a/spec/lib/security/ci_configuration/secret_detection_build_action_spec.rb
+++ b/spec/lib/security/ci_configuration/secret_detection_build_action_spec.rb
@@ -17,6 +17,7 @@ RSpec.describe Security::CiConfiguration::SecretDetectionBuildAction do
# SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
# Secret Detection customization: https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings
# Dependency Scanning customization: https://docs.gitlab.com/ee/user/application_security/dependency_scanning/#customizing-the-dependency-scanning-settings
+ # Container Scanning customization: https://docs.gitlab.com/ee/user/application_security/container_scanning/#customizing-the-container-scanning-settings
# Note that environment variables can be set in several places
# See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence
stages:
@@ -64,6 +65,7 @@ RSpec.describe Security::CiConfiguration::SecretDetectionBuildAction do
# SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
# Secret Detection customization: https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings
# Dependency Scanning customization: https://docs.gitlab.com/ee/user/application_security/dependency_scanning/#customizing-the-dependency-scanning-settings
+ # Container Scanning customization: https://docs.gitlab.com/ee/user/application_security/container_scanning/#customizing-the-container-scanning-settings
# Note that environment variables can be set in several places
# See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence
stages:
@@ -114,6 +116,7 @@ RSpec.describe Security::CiConfiguration::SecretDetectionBuildAction do
# SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
# Secret Detection customization: https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings
# Dependency Scanning customization: https://docs.gitlab.com/ee/user/application_security/dependency_scanning/#customizing-the-dependency-scanning-settings
+ # Container Scanning customization: https://docs.gitlab.com/ee/user/application_security/container_scanning/#customizing-the-container-scanning-settings
# Note that environment variables can be set in several places
# See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence
include:
@@ -135,6 +138,7 @@ RSpec.describe Security::CiConfiguration::SecretDetectionBuildAction do
# SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
# Secret Detection customization: https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings
# Dependency Scanning customization: https://docs.gitlab.com/ee/user/application_security/dependency_scanning/#customizing-the-dependency-scanning-settings
+ # Container Scanning customization: https://docs.gitlab.com/ee/user/application_security/container_scanning/#customizing-the-container-scanning-settings
# Note that environment variables can be set in several places
# See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence
include:
diff --git a/spec/lib/serializers/json_spec.rb b/spec/lib/serializers/json_spec.rb
index 0c1801b34f9..96a57cde056 100644
--- a/spec/lib/serializers/json_spec.rb
+++ b/spec/lib/serializers/json_spec.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require 'fast_spec_helper'
+require 'oj'
RSpec.describe Serializers::Json do
describe '.dump' do
diff --git a/spec/lib/serializers/symbolized_json_spec.rb b/spec/lib/serializers/symbolized_json_spec.rb
index b30fb074ddd..b9217854d9a 100644
--- a/spec/lib/serializers/symbolized_json_spec.rb
+++ b/spec/lib/serializers/symbolized_json_spec.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require 'fast_spec_helper'
+require 'oj'
RSpec.describe Serializers::SymbolizedJson do
describe '.dump' do
diff --git a/spec/lib/sidebars/concerns/work_item_hierarchy_spec.rb b/spec/lib/sidebars/concerns/work_item_hierarchy_spec.rb
new file mode 100644
index 00000000000..2120341bf23
--- /dev/null
+++ b/spec/lib/sidebars/concerns/work_item_hierarchy_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::Concerns::WorkItemHierarchy do
+ shared_examples 'hierarchy menu' do
+ let(:item_id) { :hierarchy }
+ specify { is_expected.not_to be_nil }
+ end
+
+ describe 'Project hierarchy menu item' do
+ let_it_be_with_reload(:project) { create(:project, :repository) }
+
+ let(:user) { project.owner }
+ let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) }
+
+ subject { Sidebars::Projects::Menus::ProjectInformationMenu.new(context).renderable_items.index { |e| e.item_id == item_id } }
+
+ it_behaves_like 'hierarchy menu'
+ end
+end
diff --git a/spec/lib/sidebars/projects/menus/analytics_menu_spec.rb b/spec/lib/sidebars/projects/menus/analytics_menu_spec.rb
index 6f2ca719bc9..25a65015847 100644
--- a/spec/lib/sidebars/projects/menus/analytics_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/analytics_menu_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe Sidebars::Projects::Menus::AnalyticsMenu do
create(:user).tap { |u| project.add_guest(u) }
end
- let(:owner) { project.owner }
+ let(:owner) { project.first_owner }
let(:current_user) { owner }
let(:context) { Sidebars::Projects::Context.new(current_user: current_user, container: project, current_ref: project.repository.root_ref) }
diff --git a/spec/lib/sidebars/projects/menus/ci_cd_menu_spec.rb b/spec/lib/sidebars/projects/menus/ci_cd_menu_spec.rb
index dee2716e4c2..2ceb9dcada9 100644
--- a/spec/lib/sidebars/projects/menus/ci_cd_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/ci_cd_menu_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Sidebars::Projects::Menus::CiCdMenu do
let(:project) { build(:project) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:can_view_pipeline_editor) { true }
let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project, current_ref: 'master', can_view_pipeline_editor: can_view_pipeline_editor) }
diff --git a/spec/lib/sidebars/projects/menus/confluence_menu_spec.rb b/spec/lib/sidebars/projects/menus/confluence_menu_spec.rb
index e3ae3add4fd..836c6d26c6c 100644
--- a/spec/lib/sidebars/projects/menus/confluence_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/confluence_menu_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Sidebars::Projects::Menus::ConfluenceMenu do
let_it_be_with_refind(:project) { create(:project, has_external_wiki: true) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) }
subject { described_class.new(context) }
diff --git a/spec/lib/sidebars/projects/menus/deployments_menu_spec.rb b/spec/lib/sidebars/projects/menus/deployments_menu_spec.rb
index 3149c316c63..56eb082e101 100644
--- a/spec/lib/sidebars/projects/menus/deployments_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/deployments_menu_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Sidebars::Projects::Menus::DeploymentsMenu do
let_it_be(:project) { create(:project, :repository) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) }
describe '#render?' do
diff --git a/spec/lib/sidebars/projects/menus/external_issue_tracker_menu_spec.rb b/spec/lib/sidebars/projects/menus/external_issue_tracker_menu_spec.rb
index 0585eb2254c..2033d40897e 100644
--- a/spec/lib/sidebars/projects/menus/external_issue_tracker_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/external_issue_tracker_menu_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Sidebars::Projects::Menus::ExternalIssueTrackerMenu do
let(:project) { build(:project) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:jira_issues_integration_active) { false }
let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project, jira_issues_integration: jira_issues_integration_active) }
diff --git a/spec/lib/sidebars/projects/menus/external_wiki_menu_spec.rb b/spec/lib/sidebars/projects/menus/external_wiki_menu_spec.rb
index a8f4b039b8c..9cf2d19f85c 100644
--- a/spec/lib/sidebars/projects/menus/external_wiki_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/external_wiki_menu_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Sidebars::Projects::Menus::ExternalWikiMenu do
let(:project) { build(:project) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) }
subject { described_class.new(context) }
diff --git a/spec/lib/sidebars/projects/menus/hidden_menu_spec.rb b/spec/lib/sidebars/projects/menus/hidden_menu_spec.rb
index 44013898721..e64b0de9c62 100644
--- a/spec/lib/sidebars/projects/menus/hidden_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/hidden_menu_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Sidebars::Projects::Menus::HiddenMenu do
let_it_be(:project) { create(:project, :repository) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project, current_ref: project.repository.root_ref) }
describe '#render?' do
diff --git a/spec/lib/sidebars/projects/menus/infrastructure_menu_spec.rb b/spec/lib/sidebars/projects/menus/infrastructure_menu_spec.rb
index 55281171634..0e415ec6014 100644
--- a/spec/lib/sidebars/projects/menus/infrastructure_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/infrastructure_menu_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Sidebars::Projects::Menus::InfrastructureMenu do
let(:project) { build(:project) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project, show_cluster_hint: false) }
describe '#render?' do
diff --git a/spec/lib/sidebars/projects/menus/invite_team_members_menu_spec.rb b/spec/lib/sidebars/projects/menus/invite_team_members_menu_spec.rb
index df9b260d211..9838aa8c3e3 100644
--- a/spec/lib/sidebars/projects/menus/invite_team_members_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/invite_team_members_menu_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe Sidebars::Projects::Menus::InviteTeamMembersMenu do
subject(:invite_menu) { described_class.new(context) }
context 'when the project is viewed by an owner of the group' do
- let(:owner) { project.owner }
+ let(:owner) { project.first_owner }
describe '#render?' do
it 'renders the Invite team members link' do
diff --git a/spec/lib/sidebars/projects/menus/issues_menu_spec.rb b/spec/lib/sidebars/projects/menus/issues_menu_spec.rb
index e5d486bbe8f..4c0016a77a1 100644
--- a/spec/lib/sidebars/projects/menus/issues_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/issues_menu_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Sidebars::Projects::Menus::IssuesMenu do
let(:project) { build(:project) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) }
subject { described_class.new(context) }
diff --git a/spec/lib/sidebars/projects/menus/merge_requests_menu_spec.rb b/spec/lib/sidebars/projects/menus/merge_requests_menu_spec.rb
index cef303fb068..45c49500e46 100644
--- a/spec/lib/sidebars/projects/menus/merge_requests_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/merge_requests_menu_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Sidebars::Projects::Menus::MergeRequestsMenu do
let_it_be(:project) { create(:project, :repository) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) }
subject { described_class.new(context) }
diff --git a/spec/lib/sidebars/projects/menus/monitor_menu_spec.rb b/spec/lib/sidebars/projects/menus/monitor_menu_spec.rb
index 77efe99aaa9..e8c6fb790c3 100644
--- a/spec/lib/sidebars/projects/menus/monitor_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/monitor_menu_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Sidebars::Projects::Menus::MonitorMenu do
let_it_be_with_refind(:project) { create(:project) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:show_cluster_hint) { true }
let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project, show_cluster_hint: show_cluster_hint) }
diff --git a/spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb b/spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb
index d6807451a25..afe0b2a8951 100644
--- a/spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Sidebars::Projects::Menus::PackagesRegistriesMenu do
let_it_be(:project) { create(:project) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) }
subject { described_class.new(context) }
diff --git a/spec/lib/sidebars/projects/menus/project_information_menu_spec.rb b/spec/lib/sidebars/projects/menus/project_information_menu_spec.rb
index 7e8d0ab0518..24625413ded 100644
--- a/spec/lib/sidebars/projects/menus/project_information_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/project_information_menu_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Sidebars::Projects::Menus::ProjectInformationMenu do
let_it_be_with_reload(:project) { create(:project, :repository) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) }
describe '#container_html_options' do
@@ -59,5 +59,11 @@ RSpec.describe Sidebars::Projects::Menus::ProjectInformationMenu do
specify { is_expected.to be_nil }
end
end
+
+ describe 'Hierarchy' do
+ let(:item_id) { :hierarchy }
+
+ specify { is_expected.not_to be_nil }
+ end
end
end
diff --git a/spec/lib/sidebars/projects/menus/repository_menu_spec.rb b/spec/lib/sidebars/projects/menus/repository_menu_spec.rb
index 554a4e3f532..fc181947e60 100644
--- a/spec/lib/sidebars/projects/menus/repository_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/repository_menu_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Sidebars::Projects::Menus::RepositoryMenu do
let_it_be(:project) { create(:project, :repository) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project, current_ref: 'master') }
subject { described_class.new(context) }
diff --git a/spec/lib/sidebars/projects/menus/scope_menu_spec.rb b/spec/lib/sidebars/projects/menus/scope_menu_spec.rb
index 980ab2f7c71..4e87f3b8ead 100644
--- a/spec/lib/sidebars/projects/menus/scope_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/scope_menu_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Sidebars::Projects::Menus::ScopeMenu do
let(:project) { build(:project) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) }
describe '#container_html_options' do
diff --git a/spec/lib/sidebars/projects/menus/security_compliance_menu_spec.rb b/spec/lib/sidebars/projects/menus/security_compliance_menu_spec.rb
index 6e84beeb274..41158bd58dc 100644
--- a/spec/lib/sidebars/projects/menus/security_compliance_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/security_compliance_menu_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Sidebars::Projects::Menus::SecurityComplianceMenu do
let_it_be(:project) { create(:project) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:show_promotions) { true }
let(:show_discover_project_security) { true }
let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project, show_promotions: show_promotions, show_discover_project_security: show_discover_project_security) }
diff --git a/spec/lib/sidebars/projects/menus/settings_menu_spec.rb b/spec/lib/sidebars/projects/menus/settings_menu_spec.rb
index 1e5d41dfec4..d6136dddf40 100644
--- a/spec/lib/sidebars/projects/menus/settings_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/settings_menu_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Sidebars::Projects::Menus::SettingsMenu do
let_it_be(:project) { create(:project) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) }
subject { described_class.new(context) }
diff --git a/spec/lib/sidebars/projects/menus/shimo_menu_spec.rb b/spec/lib/sidebars/projects/menus/shimo_menu_spec.rb
index 534267a329e..e74647894fa 100644
--- a/spec/lib/sidebars/projects/menus/shimo_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/shimo_menu_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Sidebars::Projects::Menus::ShimoMenu do
let_it_be_with_reload(:project) { create(:project) }
- let(:context) { Sidebars::Projects::Context.new(current_user: project.owner, container: project) }
+ let(:context) { Sidebars::Projects::Context.new(current_user: project.first_owner, container: project) }
subject(:shimo_menu) { described_class.new(context) }
diff --git a/spec/lib/sidebars/projects/menus/snippets_menu_spec.rb b/spec/lib/sidebars/projects/menus/snippets_menu_spec.rb
index af219e4a742..04b8c128e3d 100644
--- a/spec/lib/sidebars/projects/menus/snippets_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/snippets_menu_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Sidebars::Projects::Menus::SnippetsMenu do
let(:project) { build(:project) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) }
subject { described_class.new(context) }
diff --git a/spec/lib/sidebars/projects/menus/wiki_menu_spec.rb b/spec/lib/sidebars/projects/menus/wiki_menu_spec.rb
index 41447ee24a9..362da3e7b50 100644
--- a/spec/lib/sidebars/projects/menus/wiki_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/wiki_menu_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Sidebars::Projects::Menus::WikiMenu do
let(:project) { build(:project) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) }
subject { described_class.new(context) }
diff --git a/spec/lib/system_check/incoming_email/imap_authentication_check_spec.rb b/spec/lib/system_check/incoming_email/imap_authentication_check_spec.rb
index d7a77a84472..9c4aebaedd8 100644
--- a/spec/lib/system_check/incoming_email/imap_authentication_check_spec.rb
+++ b/spec/lib/system_check/incoming_email/imap_authentication_check_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'spec_helper'
MAIL_ROOM_CONFIG_ENABLED_SAMPLE =
":mailboxes:\n"\
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 0fbdc09a206..978118ed1b1 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -714,7 +714,7 @@ RSpec.describe Notify do
describe 'project access requested' do
let(:project) do
create(:project, :public) do |project|
- project.add_maintainer(project.owner)
+ project.add_maintainer(project.first_owner)
end
end
diff --git a/spec/metrics_server/metrics_server_spec.rb b/spec/metrics_server/metrics_server_spec.rb
index fc18df9b5cd..860a3299d85 100644
--- a/spec/metrics_server/metrics_server_spec.rb
+++ b/spec/metrics_server/metrics_server_spec.rb
@@ -15,9 +15,16 @@ RSpec.describe MetricsServer do # rubocop:disable RSpec/FilePath
# we need to reset it after testing.
let!(:old_multiprocess_files_dir) { prometheus_config.multiprocess_files_dir }
+ let(:ruby_sampler_double) { double(Gitlab::Metrics::Samplers::RubySampler) }
+
before do
# We do not want this to have knock-on effects on the test process.
allow(Gitlab::ProcessManagement).to receive(:modify_signals)
+
+ # This being a singleton, we stub it out because only one instance is allowed
+ # to exist per process.
+ allow(Gitlab::Metrics::Samplers::RubySampler).to receive(:initialize_instance).and_return(ruby_sampler_double)
+ allow(ruby_sampler_double).to receive(:start)
end
after do
@@ -27,101 +34,175 @@ RSpec.describe MetricsServer do # rubocop:disable RSpec/FilePath
FileUtils.rm_rf(metrics_dir, secure: true)
end
- describe '.spawn' do
- context 'when in parent process' do
- it 'forks into a new process and detaches it' do
- expect(Process).to receive(:fork).and_return(99)
- expect(Process).to receive(:detach).with(99)
+ %w(puma sidekiq).each do |target|
+ context "when targeting #{target}" do
+ describe '.fork' do
+ context 'when in parent process' do
+ it 'forks into a new process and detaches it' do
+ expect(Process).to receive(:fork).and_return(99)
+ expect(Process).to receive(:detach).with(99)
- described_class.spawn('sidekiq', metrics_dir: metrics_dir)
- end
- end
+ described_class.fork(target, metrics_dir: metrics_dir)
+ end
+ end
+
+ context 'when in child process' do
+ before do
+ # This signals the process that it's "inside" the fork
+ expect(Process).to receive(:fork).and_return(nil)
+ expect(Process).not_to receive(:detach)
+ end
- context 'when in child process' do
- before do
- # This signals the process that it's "inside" the fork
- expect(Process).to receive(:fork).and_return(nil)
- expect(Process).not_to receive(:detach)
+ it 'starts the metrics server with the given arguments' do
+ expect_next_instance_of(MetricsServer) do |server|
+ expect(server).to receive(:start)
+ end
+
+ described_class.fork(target, metrics_dir: metrics_dir)
+ end
+
+ it 'resets signal handlers from parent process' do
+ expect(Gitlab::ProcessManagement).to receive(:modify_signals).with(%i[A B], 'DEFAULT')
+
+ described_class.fork(target, metrics_dir: metrics_dir, reset_signals: %i[A B])
+ end
+ end
end
- it 'starts the metrics server with the given arguments' do
- expect_next_instance_of(MetricsServer) do |server|
- expect(server).to receive(:start)
+ describe '.spawn' do
+ let(:expected_env) do
+ {
+ 'METRICS_SERVER_TARGET' => target,
+ 'WIPE_METRICS_DIR' => '0'
+ }
+ end
+
+ it 'spawns a new server process and returns its PID' do
+ expect(Process).to receive(:spawn).with(
+ expected_env,
+ end_with('bin/metrics-server'),
+ hash_including(pgroup: true)
+ ).and_return(99)
+ expect(Process).to receive(:detach).with(99)
+
+ pid = described_class.spawn(target, metrics_dir: metrics_dir)
+
+ expect(pid).to eq(99)
end
- described_class.spawn('sidekiq', metrics_dir: metrics_dir)
+ context 'when path to gitlab.yml is passed' do
+ it 'sets the GITLAB_CONFIG environment variable' do
+ expect(Process).to receive(:spawn).with(
+ expected_env.merge('GITLAB_CONFIG' => 'path/to/config/gitlab.yml'),
+ end_with('bin/metrics-server'),
+ hash_including(pgroup: true)
+ ).and_return(99)
+
+ described_class.spawn(target, metrics_dir: metrics_dir, gitlab_config: 'path/to/config/gitlab.yml')
+ end
+ end
end
+ end
+ end
- it 'resets signal handlers from parent process' do
- expect(Gitlab::ProcessManagement).to receive(:modify_signals).with(%i[A B], 'DEFAULT')
+ context 'when targeting invalid target' do
+ describe '.fork' do
+ it 'raises an error' do
+ expect { described_class.fork('unsupported', metrics_dir: metrics_dir) }.to(
+ raise_error('Target must be one of [puma,sidekiq]')
+ )
+ end
+ end
- described_class.spawn('sidekiq', metrics_dir: metrics_dir, trapped_signals: %i[A B])
+ describe '.spawn' do
+ it 'raises an error' do
+ expect { described_class.spawn('unsupported', metrics_dir: metrics_dir) }.to(
+ raise_error('Target must be one of [puma,sidekiq]')
+ )
end
end
end
- describe '#start' do
- let(:exporter_class) { Class.new(Gitlab::Metrics::Exporter::BaseExporter) }
- let(:exporter_double) { double('fake_exporter', start: true) }
- let(:settings) { { "fake_exporter" => { "enabled" => true } } }
- let(:ruby_sampler_double) { double(Gitlab::Metrics::Samplers::RubySampler) }
+ shared_examples 'a metrics exporter' do |target, expected_name|
+ describe '#start' do
+ let(:exporter_double) { double('exporter', start: true) }
+ let(:wipe_metrics_dir) { true }
- subject(:metrics_server) { described_class.new('fake', metrics_dir, true)}
+ subject(:metrics_server) { described_class.new(target, metrics_dir, wipe_metrics_dir) }
- before do
- stub_const('Gitlab::Metrics::Exporter::FakeExporter', exporter_class)
- expect(exporter_class).to receive(:instance).with(
- settings['fake_exporter'], gc_requests: true, synchronous: true
- ).and_return(exporter_double)
- expect(Settings).to receive(:monitoring).and_return(settings)
+ it 'configures ::Prometheus::Client' do
+ metrics_server.start
- allow(Gitlab::Metrics::Samplers::RubySampler).to receive(:initialize_instance).and_return(ruby_sampler_double)
- allow(ruby_sampler_double).to receive(:start)
- end
+ expect(prometheus_config.multiprocess_files_dir).to eq metrics_dir
+ expect(::Prometheus::Client.configuration.pid_provider.call).to eq expected_name
+ end
- it 'configures ::Prometheus::Client' do
- metrics_server.start
+ it 'ensures that metrics directory exists in correct mode (0700)' do
+ expect(FileUtils).to receive(:mkdir_p).with(metrics_dir, mode: 0700)
- expect(prometheus_config.multiprocess_files_dir).to eq metrics_dir
- expect(::Prometheus::Client.configuration.pid_provider.call).to eq 'fake_exporter'
- end
+ metrics_server.start
+ end
- it 'ensures that metrics directory exists in correct mode (0700)' do
- expect(FileUtils).to receive(:mkdir_p).with(metrics_dir, mode: 0700)
+ context 'when wipe_metrics_dir is true' do
+ it 'removes any old metrics files' do
+ FileUtils.touch("#{metrics_dir}/remove_this.db")
- metrics_server.start
- end
+ expect { metrics_server.start }.to change { Dir.empty?(metrics_dir) }.from(false).to(true)
+ end
+ end
- context 'when wipe_metrics_dir is true' do
- subject(:metrics_server) { described_class.new('fake', metrics_dir, true)}
+ context 'when wipe_metrics_dir is false' do
+ let(:wipe_metrics_dir) { false }
- it 'removes any old metrics files' do
- FileUtils.touch("#{metrics_dir}/remove_this.db")
+ it 'does not remove any old metrics files' do
+ FileUtils.touch("#{metrics_dir}/remove_this.db")
- expect { metrics_server.start }.to change { Dir.empty?(metrics_dir) }.from(false).to(true)
+ expect { metrics_server.start }.not_to change { Dir.empty?(metrics_dir) }.from(false)
+ end
end
- end
- context 'when wipe_metrics_dir is false' do
- subject(:metrics_server) { described_class.new('fake', metrics_dir, false)}
+ it 'starts a metrics server' do
+ expect(exporter_double).to receive(:start)
+
+ metrics_server.start
+ end
- it 'does not remove any old metrics files' do
- FileUtils.touch("#{metrics_dir}/remove_this.db")
+ it 'starts a RubySampler instance' do
+ expect(ruby_sampler_double).to receive(:start)
- expect { metrics_server.start }.not_to change { Dir.empty?(metrics_dir) }.from(false)
+ subject.start
end
end
- it 'starts a metrics server' do
- expect(exporter_double).to receive(:start)
+ describe '#name' do
+ let(:exporter_double) { double('exporter', start: true) }
- metrics_server.start
+ subject(:name) { described_class.new(target, metrics_dir, true).name }
+
+ it { is_expected.to eq(expected_name) }
end
+ end
- it 'starts a RubySampler instance' do
- expect(ruby_sampler_double).to receive(:start)
+ context 'for puma' do
+ before do
+ allow(Gitlab::Metrics::Exporter::WebExporter).to receive(:instance).with(
+ gc_requests: true, synchronous: true
+ ).and_return(exporter_double)
+ end
- subject.start
+ it_behaves_like 'a metrics exporter', 'puma', 'web_exporter'
+ end
+
+ context 'for sidekiq' do
+ let(:settings) { { "sidekiq_exporter" => { "enabled" => true } } }
+
+ before do
+ allow(::Settings).to receive(:monitoring).and_return(settings)
+ allow(Gitlab::Metrics::Exporter::SidekiqExporter).to receive(:instance).with(
+ settings['sidekiq_exporter'], gc_requests: true, synchronous: true
+ ).and_return(exporter_double)
end
+
+ it_behaves_like 'a metrics exporter', 'sidekiq', 'sidekiq_exporter'
end
end
diff --git a/spec/migrations/20220106111958_add_insert_or_update_vulnerability_reads_trigger_spec.rb b/spec/migrations/20220106111958_add_insert_or_update_vulnerability_reads_trigger_spec.rb
new file mode 100644
index 00000000000..3e450546315
--- /dev/null
+++ b/spec/migrations/20220106111958_add_insert_or_update_vulnerability_reads_trigger_spec.rb
@@ -0,0 +1,151 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require_migration!
+
+RSpec.describe AddInsertOrUpdateVulnerabilityReadsTrigger do
+ let(:migration) { described_class.new }
+ let(:vulnerabilities) { table(:vulnerabilities) }
+ let(:vulnerability_reads) { table(:vulnerability_reads) }
+ let(:vulnerabilities_findings) { table(:vulnerability_occurrences) }
+ let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
+ let(:user) { table(:users).create!(id: 13, email: 'author@example.com', username: 'author', projects_limit: 10) }
+ let(:project) { table(:projects).create!(id: 123, namespace_id: namespace.id) }
+ let(:scanner) { table(:vulnerability_scanners).create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') }
+
+ let(:vulnerability) do
+ create_vulnerability!(
+ project_id: project.id,
+ author_id: user.id
+ )
+ end
+
+ let(:vulnerability2) do
+ create_vulnerability!(
+ project_id: project.id,
+ author_id: user.id
+ )
+ end
+
+ let(:identifier) do
+ table(:vulnerability_identifiers).create!(
+ project_id: project.id,
+ external_type: 'uuid-v5',
+ external_id: 'uuid-v5',
+ fingerprint: '7e394d1b1eb461a7406d7b1e08f057a1cf11287a',
+ name: 'Identifier for UUIDv5')
+ end
+
+ let(:finding) do
+ create_finding!(
+ project_id: project.id,
+ scanner_id: scanner.id,
+ primary_identifier_id: identifier.id
+ )
+ end
+
+ describe '#up' do
+ before do
+ migrate!
+ end
+
+ describe 'UPDATE trigger' do
+ context 'when vulnerability_id is updated' do
+ it 'creates a new vulnerability_reads row' do
+ expect do
+ finding.update!(vulnerability_id: vulnerability.id)
+ end.to change { vulnerability_reads.count }.from(0).to(1)
+ end
+ end
+
+ context 'when vulnerability_id is not updated' do
+ it 'does not create a new vulnerability_reads row' do
+ finding.update!(vulnerability_id: nil)
+
+ expect do
+ finding.update!(location: '')
+ end.not_to change { vulnerability_reads.count }
+ end
+ end
+ end
+
+ describe 'INSERT trigger' do
+ context 'when vulnerability_id is set' do
+ it 'creates a new vulnerability_reads row' do
+ expect do
+ create_finding!(
+ vulnerability_id: vulnerability2.id,
+ project_id: project.id,
+ scanner_id: scanner.id,
+ primary_identifier_id: identifier.id
+ )
+ end.to change { vulnerability_reads.count }.from(0).to(1)
+ end
+ end
+
+ context 'when vulnerability_id is not set' do
+ it 'does not create a new vulnerability_reads row' do
+ expect do
+ create_finding!(
+ project_id: project.id,
+ scanner_id: scanner.id,
+ primary_identifier_id: identifier.id
+ )
+ end.not_to change { vulnerability_reads.count }
+ end
+ end
+ end
+ end
+
+ describe '#down' do
+ before do
+ migration.up
+ migration.down
+ end
+
+ it 'drops the trigger' do
+ expect do
+ finding.update!(vulnerability_id: vulnerability.id)
+ end.not_to change { vulnerability_reads.count }
+ end
+ end
+
+ private
+
+ def create_vulnerability!(project_id:, author_id:, title: 'test', severity: 7, confidence: 7, report_type: 0)
+ vulnerabilities.create!(
+ project_id: project_id,
+ author_id: author_id,
+ title: title,
+ severity: severity,
+ confidence: confidence,
+ report_type: report_type
+ )
+ end
+
+ # rubocop:disable Metrics/ParameterLists
+ def create_finding!(
+ vulnerability_id: nil, project_id:, scanner_id:, primary_identifier_id:,
+ name: "test", severity: 7, confidence: 7, report_type: 0,
+ project_fingerprint: '123qweasdzxc', location: { "image" => "alpine:3.4" }, location_fingerprint: 'test',
+ metadata_version: 'test', raw_metadata: 'test', uuid: SecureRandom.uuid)
+ vulnerabilities_findings.create!(
+ vulnerability_id: vulnerability_id,
+ project_id: project_id,
+ name: name,
+ severity: severity,
+ confidence: confidence,
+ report_type: report_type,
+ project_fingerprint: project_fingerprint,
+ scanner_id: scanner_id,
+ primary_identifier_id: primary_identifier_id,
+ location: location,
+ location_fingerprint: location_fingerprint,
+ metadata_version: metadata_version,
+ raw_metadata: raw_metadata,
+ uuid: uuid
+ )
+ end
+ # rubocop:enable Metrics/ParameterLists
+end
diff --git a/spec/migrations/20220106112043_add_update_vulnerability_reads_trigger_spec.rb b/spec/migrations/20220106112043_add_update_vulnerability_reads_trigger_spec.rb
new file mode 100644
index 00000000000..d988b1e42b9
--- /dev/null
+++ b/spec/migrations/20220106112043_add_update_vulnerability_reads_trigger_spec.rb
@@ -0,0 +1,128 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require_migration!
+
+RSpec.describe AddUpdateVulnerabilityReadsTrigger do
+ let(:migration) { described_class.new }
+ let(:vulnerability_reads) { table(:vulnerability_reads) }
+ let(:issue_links) { table(:vulnerability_issue_links) }
+ let(:vulnerabilities) { table(:vulnerabilities) }
+ let(:vulnerabilities_findings) { table(:vulnerability_occurrences) }
+
+ let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
+ let(:user) { table(:users).create!(id: 13, email: 'author@example.com', username: 'author', projects_limit: 10) }
+ let(:project) { table(:projects).create!(id: 123, namespace_id: namespace.id) }
+ let(:issue) { table(:issues).create!(description: '1234', state_id: 1, project_id: project.id) }
+ let(:scanner) { table(:vulnerability_scanners).create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') }
+
+ let(:vulnerability) do
+ create_vulnerability!(
+ project_id: project.id,
+ report_type: 7,
+ author_id: user.id
+ )
+ end
+
+ let(:identifier) do
+ table(:vulnerability_identifiers).create!(
+ project_id: project.id,
+ external_type: 'uuid-v5',
+ external_id: 'uuid-v5',
+ fingerprint: '7e394d1b1eb461a7406d7b1e08f057a1cf11287a',
+ name: 'Identifier for UUIDv5')
+ end
+
+ describe '#up' do
+ before do
+ migrate!
+ end
+
+ describe 'UPDATE trigger' do
+ before do
+ create_finding!(
+ vulnerability_id: vulnerability.id,
+ project_id: project.id,
+ scanner_id: scanner.id,
+ report_type: 7,
+ primary_identifier_id: identifier.id
+ )
+ end
+
+ context 'when vulnerability attributes are updated' do
+ it 'updates vulnerability attributes in vulnerability_reads' do
+ expect do
+ vulnerability.update!(severity: 6)
+ end.to change { vulnerability_reads.first.severity }.from(7).to(6)
+ end
+ end
+
+ context 'when vulnerability attributes are not updated' do
+ it 'does not update vulnerability attributes in vulnerability_reads' do
+ expect do
+ vulnerability.update!(title: "New vulnerability")
+ end.not_to change { vulnerability_reads.first }
+ end
+ end
+ end
+ end
+
+ describe '#down' do
+ before do
+ migration.up
+ migration.down
+ create_finding!(
+ vulnerability_id: vulnerability.id,
+ project_id: project.id,
+ scanner_id: scanner.id,
+ report_type: 7,
+ primary_identifier_id: identifier.id
+ )
+ end
+
+ it 'drops the trigger' do
+ expect do
+ vulnerability.update!(severity: 6)
+ end.not_to change { vulnerability_reads.first.severity }
+ end
+ end
+
+ private
+
+ def create_vulnerability!(project_id:, author_id:, title: 'test', severity: 7, confidence: 7, report_type: 0)
+ vulnerabilities.create!(
+ project_id: project_id,
+ author_id: author_id,
+ title: title,
+ severity: severity,
+ confidence: confidence,
+ report_type: report_type
+ )
+ end
+
+ # rubocop:disable Metrics/ParameterLists
+ def create_finding!(
+ vulnerability_id: nil, project_id:, scanner_id:, primary_identifier_id:,
+ name: "test", severity: 7, confidence: 7, report_type: 0,
+ project_fingerprint: '123qweasdzxc', location: { "image" => "alpine:3.4" }, location_fingerprint: 'test',
+ metadata_version: 'test', raw_metadata: 'test', uuid: SecureRandom.uuid)
+ vulnerabilities_findings.create!(
+ vulnerability_id: vulnerability_id,
+ project_id: project_id,
+ name: name,
+ severity: severity,
+ confidence: confidence,
+ report_type: report_type,
+ project_fingerprint: project_fingerprint,
+ scanner_id: scanner_id,
+ primary_identifier_id: primary_identifier_id,
+ location: location,
+ location_fingerprint: location_fingerprint,
+ metadata_version: metadata_version,
+ raw_metadata: raw_metadata,
+ uuid: uuid
+ )
+ end
+ # rubocop:enable Metrics/ParameterLists
+end
diff --git a/spec/migrations/20220106112085_add_update_vulnerability_reads_location_trigger_spec.rb b/spec/migrations/20220106112085_add_update_vulnerability_reads_location_trigger_spec.rb
new file mode 100644
index 00000000000..901f1cf6041
--- /dev/null
+++ b/spec/migrations/20220106112085_add_update_vulnerability_reads_location_trigger_spec.rb
@@ -0,0 +1,136 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require_migration!
+
+RSpec.describe AddUpdateVulnerabilityReadsLocationTrigger do
+ let(:migration) { described_class.new }
+ let(:vulnerability_reads) { table(:vulnerability_reads) }
+ let(:issue_links) { table(:vulnerability_issue_links) }
+ let(:vulnerabilities) { table(:vulnerabilities) }
+ let(:vulnerabilities_findings) { table(:vulnerability_occurrences) }
+
+ let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
+ let(:user) { table(:users).create!(id: 13, email: 'author@example.com', username: 'author', projects_limit: 10) }
+ let(:project) { table(:projects).create!(id: 123, namespace_id: namespace.id) }
+ let(:issue) { table(:issues).create!(description: '1234', state_id: 1, project_id: project.id) }
+ let(:scanner) { table(:vulnerability_scanners).create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') }
+
+ let(:vulnerability) do
+ create_vulnerability!(
+ project_id: project.id,
+ report_type: 7,
+ author_id: user.id
+ )
+ end
+
+ let(:identifier) do
+ table(:vulnerability_identifiers).create!(
+ project_id: project.id,
+ external_type: 'uuid-v5',
+ external_id: 'uuid-v5',
+ fingerprint: '7e394d1b1eb461a7406d7b1e08f057a1cf11287a',
+ name: 'Identifier for UUIDv5')
+ end
+
+ describe '#up' do
+ before do
+ migrate!
+ end
+
+ describe 'UPDATE trigger' do
+ context 'when image is updated' do
+ it 'updates location_image in vulnerability_reads' do
+ finding = create_finding!(
+ vulnerability_id: vulnerability.id,
+ project_id: project.id,
+ scanner_id: scanner.id,
+ report_type: 7,
+ location: { "image" => "alpine:3.4" },
+ primary_identifier_id: identifier.id
+ )
+
+ expect do
+ finding.update!(location: { "image" => "alpine:4", "kubernetes_resource" => { "agent_id" => "1234" } })
+ end.to change { vulnerability_reads.first.location_image }.from("alpine:3.4").to("alpine:4")
+ end
+ end
+
+ context 'when image is not updated' do
+ it 'updates location_image in vulnerability_reads' do
+ finding = create_finding!(
+ vulnerability_id: vulnerability.id,
+ project_id: project.id,
+ scanner_id: scanner.id,
+ report_type: 7,
+ location: { "image" => "alpine:3.4", "kubernetes_resource" => { "agent_id" => "1234" } },
+ primary_identifier_id: identifier.id
+ )
+
+ expect do
+ finding.update!(project_fingerprint: "123qweasdzx")
+ end.not_to change { vulnerability_reads.first.location_image }
+ end
+ end
+ end
+ end
+
+ describe '#down' do
+ before do
+ migration.up
+ migration.down
+ end
+
+ it 'drops the trigger' do
+ finding = create_finding!(
+ vulnerability_id: vulnerability.id,
+ project_id: project.id,
+ scanner_id: scanner.id,
+ primary_identifier_id: identifier.id
+ )
+
+ expect do
+ finding.update!(location: '{"image":"alpine:4"}')
+ end.not_to change { vulnerability_reads.first.location_image }
+ end
+ end
+
+ private
+
+ def create_vulnerability!(project_id:, author_id:, title: 'test', severity: 7, confidence: 7, report_type: 0)
+ vulnerabilities.create!(
+ project_id: project_id,
+ author_id: author_id,
+ title: title,
+ severity: severity,
+ confidence: confidence,
+ report_type: report_type
+ )
+ end
+
+ # rubocop:disable Metrics/ParameterLists
+ def create_finding!(
+ vulnerability_id: nil, project_id:, scanner_id:, primary_identifier_id:,
+ name: "test", severity: 7, confidence: 7, report_type: 0,
+ project_fingerprint: '123qweasdzxc', location: { "image" => "alpine:3.4" }, location_fingerprint: 'test',
+ metadata_version: 'test', raw_metadata: 'test', uuid: SecureRandom.uuid)
+ vulnerabilities_findings.create!(
+ vulnerability_id: vulnerability_id,
+ project_id: project_id,
+ name: name,
+ severity: severity,
+ confidence: confidence,
+ report_type: report_type,
+ project_fingerprint: project_fingerprint,
+ scanner_id: scanner_id,
+ primary_identifier_id: primary_identifier_id,
+ location: location,
+ location_fingerprint: location_fingerprint,
+ metadata_version: metadata_version,
+ raw_metadata: raw_metadata,
+ uuid: uuid
+ )
+ end
+ # rubocop:enable Metrics/ParameterLists
+end
diff --git a/spec/migrations/20220106163326_add_has_issues_on_vulnerability_reads_trigger_spec.rb b/spec/migrations/20220106163326_add_has_issues_on_vulnerability_reads_trigger_spec.rb
new file mode 100644
index 00000000000..8e50b74eb9c
--- /dev/null
+++ b/spec/migrations/20220106163326_add_has_issues_on_vulnerability_reads_trigger_spec.rb
@@ -0,0 +1,134 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require_migration!
+
+RSpec.describe AddHasIssuesOnVulnerabilityReadsTrigger do
+ let(:migration) { described_class.new }
+ let(:vulnerability_reads) { table(:vulnerability_reads) }
+ let(:issue_links) { table(:vulnerability_issue_links) }
+ let(:vulnerabilities) { table(:vulnerabilities) }
+ let(:vulnerabilities_findings) { table(:vulnerability_occurrences) }
+
+ let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
+ let(:user) { table(:users).create!(id: 13, email: 'author@example.com', username: 'author', projects_limit: 10) }
+ let(:project) { table(:projects).create!(id: 123, namespace_id: namespace.id) }
+ let(:issue) { table(:issues).create!(description: '1234', state_id: 1, project_id: project.id) }
+ let(:scanner) { table(:vulnerability_scanners).create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') }
+
+ let(:vulnerability) do
+ create_vulnerability!(
+ project_id: project.id,
+ author_id: user.id
+ )
+ end
+
+ let(:identifier) do
+ table(:vulnerability_identifiers).create!(
+ project_id: project.id,
+ external_type: 'uuid-v5',
+ external_id: 'uuid-v5',
+ fingerprint: '7e394d1b1eb461a7406d7b1e08f057a1cf11287a',
+ name: 'Identifier for UUIDv5')
+ end
+
+ before do
+ create_finding!(
+ vulnerability_id: vulnerability.id,
+ project_id: project.id,
+ scanner_id: scanner.id,
+ primary_identifier_id: identifier.id
+ )
+
+ @vulnerability_read = vulnerability_reads.first
+ end
+
+ describe '#up' do
+ before do
+ migrate!
+ end
+
+ describe 'INSERT trigger' do
+ it 'updates has_issues in vulnerability_reads' do
+ expect do
+ issue_links.create!(vulnerability_id: vulnerability.id, issue_id: issue.id)
+ end.to change { @vulnerability_read.reload.has_issues }.from(false).to(true)
+ end
+ end
+
+ describe 'DELETE trigger' do
+ let(:issue2) { table(:issues).create!(description: '1234', state_id: 1, project_id: project.id) }
+
+ it 'does not change has_issues when there exists another issue' do
+ issue_link1 = issue_links.create!(vulnerability_id: vulnerability.id, issue_id: issue.id)
+ issue_links.create!(vulnerability_id: vulnerability.id, issue_id: issue2.id)
+
+ expect do
+ issue_link1.delete
+ end.not_to change { @vulnerability_read.reload.has_issues }
+ end
+
+ it 'unsets has_issues when all issues are deleted' do
+ issue_link1 = issue_links.create!(vulnerability_id: vulnerability.id, issue_id: issue.id)
+ issue_link2 = issue_links.create!(vulnerability_id: vulnerability.id, issue_id: issue2.id)
+
+ expect do
+ issue_link1.delete
+ issue_link2.delete
+ end.to change { @vulnerability_read.reload.has_issues }.from(true).to(false)
+ end
+ end
+ end
+
+ describe '#down' do
+ before do
+ migration.up
+ migration.down
+ end
+
+ it 'drops the trigger' do
+ expect do
+ issue_links.create!(vulnerability_id: vulnerability.id, issue_id: issue.id)
+ end.not_to change { @vulnerability_read.has_issues }
+ end
+ end
+
+ private
+
+ def create_vulnerability!(project_id:, author_id:, title: 'test', severity: 7, confidence: 7, report_type: 0)
+ vulnerabilities.create!(
+ project_id: project_id,
+ author_id: author_id,
+ title: title,
+ severity: severity,
+ confidence: confidence,
+ report_type: report_type
+ )
+ end
+
+ # rubocop:disable Metrics/ParameterLists
+ def create_finding!(
+ vulnerability_id: nil, project_id:, scanner_id:, primary_identifier_id:,
+ name: "test", severity: 7, confidence: 7, report_type: 0,
+ project_fingerprint: '123qweasdzxc', location: { "image" => "alpine:3.4" }, location_fingerprint: 'test',
+ metadata_version: 'test', raw_metadata: 'test', uuid: SecureRandom.uuid)
+ vulnerabilities_findings.create!(
+ vulnerability_id: vulnerability_id,
+ project_id: project_id,
+ name: name,
+ severity: severity,
+ confidence: confidence,
+ report_type: report_type,
+ project_fingerprint: project_fingerprint,
+ scanner_id: scanner_id,
+ primary_identifier_id: primary_identifier_id,
+ location: location,
+ location_fingerprint: location_fingerprint,
+ metadata_version: metadata_version,
+ raw_metadata: raw_metadata,
+ uuid: uuid
+ )
+ end
+ # rubocop:enable Metrics/ParameterLists
+end
diff --git a/spec/migrations/20220107064845_populate_vulnerability_reads_spec.rb b/spec/migrations/20220107064845_populate_vulnerability_reads_spec.rb
new file mode 100644
index 00000000000..ece971a50c9
--- /dev/null
+++ b/spec/migrations/20220107064845_populate_vulnerability_reads_spec.rb
@@ -0,0 +1,107 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+require_migration!
+
+RSpec.describe PopulateVulnerabilityReads, :migration do
+ let_it_be(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
+ let_it_be(:user) { table(:users).create!(email: 'author@example.com', username: 'author', projects_limit: 10) }
+ let_it_be(:project) { table(:projects).create!(namespace_id: namespace.id) }
+ let_it_be(:scanner) { table(:vulnerability_scanners).create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') }
+ let_it_be(:background_migration_jobs) { table(:background_migration_jobs) }
+ let_it_be(:vulnerabilities) { table(:vulnerabilities) }
+ let_it_be(:vulnerability_reads) { table(:vulnerability_reads) }
+ let_it_be(:vulnerabilities_findings) { table(:vulnerability_occurrences) }
+ let_it_be(:vulnerability_issue_links) { table(:vulnerability_issue_links) }
+ let_it_be(:vulnerability_ids) { [] }
+
+ before do
+ stub_const("#{described_class}::BATCH_SIZE", 1)
+ stub_const("#{described_class}::SUB_BATCH_SIZE", 1)
+
+ 5.times.each do |x|
+ vulnerability = create_vulnerability!(
+ project_id: project.id,
+ report_type: 7,
+ author_id: user.id
+ )
+ identifier = table(:vulnerability_identifiers).create!(
+ project_id: project.id,
+ external_type: 'uuid-v5',
+ external_id: 'uuid-v5',
+ fingerprint: Digest::SHA1.hexdigest("#{vulnerability.id}"),
+ name: 'Identifier for UUIDv5')
+
+ create_finding!(
+ vulnerability_id: vulnerability.id,
+ project_id: project.id,
+ scanner_id: scanner.id,
+ primary_identifier_id: identifier.id
+ )
+
+ vulnerability_ids << vulnerability.id
+ end
+ end
+
+ around do |example|
+ freeze_time { Sidekiq::Testing.fake! { example.run } }
+ end
+
+ it 'schedules background migrations' do
+ migrate!
+
+ expect(background_migration_jobs.count).to eq(5)
+ expect(background_migration_jobs.first.arguments).to match_array([vulnerability_ids.first, vulnerability_ids.first, 1])
+ expect(background_migration_jobs.second.arguments).to match_array([vulnerability_ids.second, vulnerability_ids.second, 1])
+ expect(background_migration_jobs.third.arguments).to match_array([vulnerability_ids.third, vulnerability_ids.third, 1])
+ expect(background_migration_jobs.fourth.arguments).to match_array([vulnerability_ids.fourth, vulnerability_ids.fourth, 1])
+ expect(background_migration_jobs.fifth.arguments).to match_array([vulnerability_ids.fifth, vulnerability_ids.fifth, 1])
+
+ expect(BackgroundMigrationWorker.jobs.size).to eq(5)
+ expect(described_class::MIGRATION_NAME).to be_scheduled_delayed_migration(2.minutes, vulnerability_ids.first, vulnerability_ids.first, 1)
+ expect(described_class::MIGRATION_NAME).to be_scheduled_delayed_migration(4.minutes, vulnerability_ids.second, vulnerability_ids.second, 1)
+ expect(described_class::MIGRATION_NAME).to be_scheduled_delayed_migration(6.minutes, vulnerability_ids.third, vulnerability_ids.third, 1)
+ expect(described_class::MIGRATION_NAME).to be_scheduled_delayed_migration(8.minutes, vulnerability_ids.fourth, vulnerability_ids.fourth, 1)
+ expect(described_class::MIGRATION_NAME).to be_scheduled_delayed_migration(10.minutes, vulnerability_ids.fifth, vulnerability_ids.fifth, 1)
+ end
+
+ private
+
+ def create_vulnerability!(project_id:, author_id:, title: 'test', severity: 7, confidence: 7, report_type: 0)
+ vulnerabilities.create!(
+ project_id: project_id,
+ author_id: author_id,
+ title: title,
+ severity: severity,
+ confidence: confidence,
+ report_type: report_type
+ )
+ end
+
+ # rubocop:disable Metrics/ParameterLists
+ def create_finding!(
+ id: nil,
+ vulnerability_id:, project_id:, scanner_id:, primary_identifier_id:,
+ name: "test", severity: 7, confidence: 7, report_type: 0,
+ project_fingerprint: '123qweasdzxc', location_fingerprint: 'test',
+ metadata_version: 'test', raw_metadata: 'test', uuid: SecureRandom.uuid)
+ params = {
+ vulnerability_id: vulnerability_id,
+ project_id: project_id,
+ name: name,
+ severity: severity,
+ confidence: confidence,
+ report_type: report_type,
+ project_fingerprint: project_fingerprint,
+ scanner_id: scanner_id,
+ primary_identifier_id: primary_identifier_id,
+ location_fingerprint: location_fingerprint,
+ metadata_version: metadata_version,
+ raw_metadata: raw_metadata,
+ uuid: uuid
+ }
+ params[:id] = id unless id.nil?
+ vulnerabilities_findings.create!(params)
+ end
+ # rubocop:enable Metrics/ParameterLists
+end
diff --git a/spec/migrations/20220120094340_drop_position_from_security_findings_spec.rb b/spec/migrations/20220120094340_drop_position_from_security_findings_spec.rb
new file mode 100644
index 00000000000..2ad9a8220c3
--- /dev/null
+++ b/spec/migrations/20220120094340_drop_position_from_security_findings_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!('drop_position_from_security_findings')
+
+RSpec.describe DropPositionFromSecurityFindings do
+ let(:events) { table(:security_findings) }
+
+ it 'correctly migrates up and down' do
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(events.column_names).to include('position')
+ }
+
+ migration.after -> {
+ events.reset_column_information
+ expect(events.column_names).not_to include('position')
+ }
+ end
+ end
+end
diff --git a/spec/migrations/20220124130028_dedup_runner_projects_spec.rb b/spec/migrations/20220124130028_dedup_runner_projects_spec.rb
new file mode 100644
index 00000000000..2698af6f6f5
--- /dev/null
+++ b/spec/migrations/20220124130028_dedup_runner_projects_spec.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20220124130028_dedup_runner_projects.rb')
+
+RSpec.describe DedupRunnerProjects, :migration, schema: 20220120085655 do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:runners) { table(:ci_runners) }
+ let(:runner_projects) { table(:ci_runner_projects) }
+
+ let!(:namespace) { namespaces.create!(name: 'foo', path: 'foo') }
+ let!(:project) { projects.create!(namespace_id: namespace.id) }
+ let!(:project_2) { projects.create!(namespace_id: namespace.id) }
+ let!(:runner) { runners.create!(runner_type: 'project_type') }
+ let!(:runner_2) { runners.create!(runner_type: 'project_type') }
+ let!(:runner_3) { runners.create!(runner_type: 'project_type') }
+
+ let!(:duplicated_runner_project_1) { runner_projects.create!(runner_id: runner.id, project_id: project.id) }
+ let!(:duplicated_runner_project_2) { runner_projects.create!(runner_id: runner.id, project_id: project.id) }
+ let!(:duplicated_runner_project_3) { runner_projects.create!(runner_id: runner_2.id, project_id: project_2.id) }
+ let!(:duplicated_runner_project_4) { runner_projects.create!(runner_id: runner_2.id, project_id: project_2.id) }
+
+ let!(:non_duplicated_runner_project) { runner_projects.create!(runner_id: runner_3.id, project_id: project.id) }
+
+ it 'deduplicates ci_runner_projects table' do
+ expect { migrate! }.to change { runner_projects.count }.from(5).to(3)
+ end
+
+ it 'merges `duplicated_runner_project_1` with `duplicated_runner_project_2`', :aggregate_failures do
+ migrate!
+
+ expect(runner_projects.where(id: duplicated_runner_project_1.id)).not_to(exist)
+
+ merged_runner_projects = runner_projects.find_by(id: duplicated_runner_project_2.id)
+
+ expect(merged_runner_projects).to be_present
+ expect(merged_runner_projects.created_at).to be_like_time(duplicated_runner_project_1.created_at)
+ expect(merged_runner_projects.created_at).to be_like_time(duplicated_runner_project_2.created_at)
+ end
+
+ it 'merges `duplicated_runner_project_3` with `duplicated_runner_project_4`', :aggregate_failures do
+ migrate!
+
+ expect(runner_projects.where(id: duplicated_runner_project_3.id)).not_to(exist)
+
+ merged_runner_projects = runner_projects.find_by(id: duplicated_runner_project_4.id)
+
+ expect(merged_runner_projects).to be_present
+ expect(merged_runner_projects.created_at).to be_like_time(duplicated_runner_project_3.created_at)
+ expect(merged_runner_projects.created_at).to be_like_time(duplicated_runner_project_4.created_at)
+ end
+
+ it 'does not change non duplicated records' do
+ expect { migrate! }.not_to change { non_duplicated_runner_project.reload.attributes }
+ end
+
+ it 'does nothing when there are no runner projects' do
+ runner_projects.delete_all
+
+ migrate!
+
+ expect(runner_projects.count).to eq(0)
+ end
+end
diff --git a/spec/migrations/20220128155251_remove_dangling_running_builds_spec.rb b/spec/migrations/20220128155251_remove_dangling_running_builds_spec.rb
new file mode 100644
index 00000000000..a48464befdf
--- /dev/null
+++ b/spec/migrations/20220128155251_remove_dangling_running_builds_spec.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!('remove_dangling_running_builds')
+
+RSpec.describe RemoveDanglingRunningBuilds do
+ let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
+ let(:project) { table(:projects).create!(namespace_id: namespace.id) }
+ let(:runner) { table(:ci_runners).create!(runner_type: 1) }
+ let(:builds) { table(:ci_builds) }
+ let(:running_builds) { table(:ci_running_builds) }
+
+ let(:running_build) do
+ builds.create!(
+ name: 'test 1',
+ status: 'running',
+ project_id: project.id,
+ type: 'Ci::Build')
+ end
+
+ let(:failed_build) do
+ builds.create!(
+ name: 'test 2',
+ status: 'failed',
+ project_id: project.id,
+ type: 'Ci::Build')
+ end
+
+ let!(:running_metadata) do
+ running_builds.create!(
+ build_id: running_build.id,
+ project_id: project.id,
+ runner_id: runner.id,
+ runner_type:
+ runner.runner_type)
+ end
+
+ let!(:failed_metadata) do
+ running_builds.create!(
+ build_id: failed_build.id,
+ project_id: project.id,
+ runner_id: runner.id,
+ runner_type: runner.runner_type)
+ end
+
+ it 'removes failed builds' do
+ migrate!
+
+ expect(running_metadata.reload).to be_present
+ expect { failed_metadata.reload } .to raise_error(ActiveRecord::RecordNotFound)
+ end
+end
diff --git a/spec/migrations/20220128155814_fix_approval_rules_code_owners_rule_type_index_spec.rb b/spec/migrations/20220128155814_fix_approval_rules_code_owners_rule_type_index_spec.rb
new file mode 100644
index 00000000000..1558facdf96
--- /dev/null
+++ b/spec/migrations/20220128155814_fix_approval_rules_code_owners_rule_type_index_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!('fix_approval_rules_code_owners_rule_type_index')
+
+RSpec.describe FixApprovalRulesCodeOwnersRuleTypeIndex, :migration do
+ let(:table_name) { :approval_merge_request_rules }
+ let(:index_name) { 'index_approval_rules_code_owners_rule_type' }
+
+ it 'correctly migrates up and down' do
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(subject.index_exists_by_name?(table_name, index_name)).to be_truthy
+ }
+
+ migration.after -> {
+ expect(subject.index_exists_by_name?(table_name, index_name)).to be_truthy
+ }
+ end
+ end
+
+ context 'when the index already exists' do
+ before do
+ subject.add_concurrent_index table_name, :merge_request_id, where: 'rule_type = 2', name: index_name
+ end
+
+ it 'keeps the index' do
+ migrate!
+
+ expect(subject.index_exists_by_name?(table_name, index_name)).to be_truthy
+ end
+ end
+end
diff --git a/spec/migrations/20220202105733_delete_service_template_records_spec.rb b/spec/migrations/20220202105733_delete_service_template_records_spec.rb
new file mode 100644
index 00000000000..c9f6b5cbe66
--- /dev/null
+++ b/spec/migrations/20220202105733_delete_service_template_records_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+require_migration!
+
+RSpec.describe DeleteServiceTemplateRecords do
+ let(:integrations) { table(:integrations) }
+ let(:chat_names) { table(:chat_names) }
+ let(:web_hooks) { table(:web_hooks) }
+ let(:slack_integrations) { table(:slack_integrations) }
+ let(:zentao_tracker_data) { table(:zentao_tracker_data) }
+ let(:jira_tracker_data) { table(:jira_tracker_data) }
+ let(:issue_tracker_data) { table(:issue_tracker_data) }
+
+ before do
+ template = integrations.create!(template: true)
+ chat_names.create!(service_id: template.id, user_id: 1, team_id: 1, chat_id: 1)
+ web_hooks.create!(service_id: template.id)
+ slack_integrations.create!(service_id: template.id, team_id: 1, team_name: 'team', alias: 'alias', user_id: 1)
+ zentao_tracker_data.create!(integration_id: template.id)
+ jira_tracker_data.create!(service_id: template.id)
+ issue_tracker_data.create!(service_id: template.id)
+
+ integrations.create!(template: false)
+ end
+
+ it 'deletes template records and associated data' do
+ expect { migrate! }
+ .to change { integrations.where(template: true).count }.from(1).to(0)
+ .and change { chat_names.count }.from(1).to(0)
+ .and change { web_hooks.count }.from(1).to(0)
+ .and change { slack_integrations.count }.from(1).to(0)
+ .and change { zentao_tracker_data.count }.from(1).to(0)
+ .and change { jira_tracker_data.count }.from(1).to(0)
+ .and change { issue_tracker_data.count }.from(1).to(0)
+ end
+
+ it 'does not delete non template records' do
+ expect { migrate! }
+ .not_to change { integrations.where(template: false).count }
+ end
+end
diff --git a/spec/migrations/20220211214605_update_integrations_trigger_type_new_on_insert_null_safe_spec.rb b/spec/migrations/20220211214605_update_integrations_trigger_type_new_on_insert_null_safe_spec.rb
new file mode 100644
index 00000000000..bf79ee02ff1
--- /dev/null
+++ b/spec/migrations/20220211214605_update_integrations_trigger_type_new_on_insert_null_safe_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require_migration!
+
+RSpec.describe UpdateIntegrationsTriggerTypeNewOnInsertNullSafe, :migration do
+ let(:integrations) { table(:integrations) }
+
+ before do
+ migrate!
+ end
+
+ it 'leaves defined values alone' do
+ record = integrations.create!(type: 'XService', type_new: 'Integrations::Y')
+
+ expect(integrations.find(record.id)).to have_attributes(type: 'XService', type_new: 'Integrations::Y')
+ end
+
+ it 'keeps type_new synchronized with type' do
+ record = integrations.create!(type: 'AbcService', type_new: nil)
+
+ expect(integrations.find(record.id)).to have_attributes(
+ type: 'AbcService',
+ type_new: 'Integrations::Abc'
+ )
+ end
+
+ it 'keeps type synchronized with type_new' do
+ record = integrations.create!(type: nil, type_new: 'Integrations::Abc')
+
+ expect(integrations.find(record.id)).to have_attributes(
+ type: 'AbcService',
+ type_new: 'Integrations::Abc'
+ )
+ end
+end
diff --git a/spec/migrations/backfill_namespace_id_for_namespace_routes_spec.rb b/spec/migrations/backfill_namespace_id_for_namespace_routes_spec.rb
new file mode 100644
index 00000000000..913ec404795
--- /dev/null
+++ b/spec/migrations/backfill_namespace_id_for_namespace_routes_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe BackfillNamespaceIdForNamespaceRoutes do
+ let_it_be(:migration) { described_class::MIGRATION }
+
+ describe '#up' do
+ it 'schedules background jobs for each batch of routes' do
+ migrate!
+
+ expect(migration).to have_scheduled_batched_migration(
+ table_name: :routes,
+ column_name: :id,
+ interval: described_class::INTERVAL
+ )
+ end
+ end
+
+ describe '#down' do
+ it 'deletes all batched migration records' do
+ migrate!
+ schema_migrate_down!
+
+ expect(migration).not_to have_scheduled_batched_migration
+ end
+ end
+end
diff --git a/spec/migrations/backfill_project_namespaces_for_group_spec.rb b/spec/migrations/backfill_project_namespaces_for_group_spec.rb
new file mode 100644
index 00000000000..0d34d19d42a
--- /dev/null
+++ b/spec/migrations/backfill_project_namespaces_for_group_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe BackfillProjectNamespacesForGroup do
+ let_it_be(:migration) { described_class::MIGRATION }
+
+ let(:projects) { table(:projects) }
+ let(:namespaces) { table(:namespaces) }
+ let(:parent_group1) { namespaces.create!(name: 'parent_group1', path: 'parent_group1', visibility_level: 20, type: 'Group') }
+ let!(:parent_group1_project) { projects.create!(name: 'parent_group1_project', path: 'parent_group1_project', namespace_id: parent_group1.id, visibility_level: 20) }
+
+ before do
+ allow(Gitlab).to receive(:com?).and_return(true)
+ end
+
+ describe '#up' do
+ before do
+ stub_const("BackfillProjectNamespacesForGroup::GROUP_ID", parent_group1.id)
+ end
+
+ it 'schedules background jobs for each batch of namespaces' do
+ migrate!
+
+ expect(migration).to have_scheduled_batched_migration(
+ table_name: :projects,
+ column_name: :id,
+ job_arguments: [described_class::GROUP_ID, 'up'],
+ interval: described_class::DELAY_INTERVAL
+ )
+ end
+ end
+
+ describe '#down' do
+ it 'deletes all batched migration records' do
+ migrate!
+ schema_migrate_down!
+
+ expect(migration).not_to have_scheduled_batched_migration
+ end
+ end
+end
diff --git a/spec/migrations/populate_audit_event_streaming_verification_token_spec.rb b/spec/migrations/populate_audit_event_streaming_verification_token_spec.rb
new file mode 100644
index 00000000000..b3fe1776183
--- /dev/null
+++ b/spec/migrations/populate_audit_event_streaming_verification_token_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe PopulateAuditEventStreamingVerificationToken do
+ let(:groups) { table(:namespaces) }
+ let(:destinations) { table(:audit_events_external_audit_event_destinations) }
+ let(:migration) { described_class.new }
+
+ let!(:group) { groups.create!(name: 'test-group', path: 'test-group') }
+ let!(:destination) { destinations.create!(namespace_id: group.id, destination_url: 'https://example.com/destination', verification_token: nil) }
+
+ describe '#up' do
+ it 'adds verification tokens to records created before the migration' do
+ expect do
+ migrate!
+ destination.reload
+ end.to change { destination.verification_token }.from(nil).to(a_string_matching(/\w{24}/))
+ end
+ end
+end
diff --git a/spec/migrations/schedule_fix_incorrect_max_seats_used2_spec.rb b/spec/migrations/schedule_fix_incorrect_max_seats_used2_spec.rb
new file mode 100644
index 00000000000..3720be6cf3e
--- /dev/null
+++ b/spec/migrations/schedule_fix_incorrect_max_seats_used2_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe ScheduleFixIncorrectMaxSeatsUsed2, :migration do
+ let(:migration_name) { described_class::MIGRATION.to_s.demodulize }
+
+ describe '#up' do
+ it 'schedules a job on Gitlab.com' do
+ allow(Gitlab).to receive(:com?).and_return(true)
+
+ Sidekiq::Testing.fake! do
+ freeze_time do
+ migrate!
+
+ expect(migration_name).to be_scheduled_delayed_migration(1.hour, 'batch_2_for_start_date_before_02_aug_2021')
+ expect(BackgroundMigrationWorker.jobs.size).to eq(1)
+ end
+ end
+ end
+
+ it 'does not schedule any jobs when not Gitlab.com' do
+ allow(Gitlab).to receive(:com?).and_return(false)
+
+ Sidekiq::Testing.fake! do
+ migrate!
+
+ expect(migration_name).not_to be_scheduled_delayed_migration
+ expect(BackgroundMigrationWorker.jobs.size).to eq(0)
+ end
+ end
+ end
+end
diff --git a/spec/migrations/schedule_fix_incorrect_max_seats_used_spec.rb b/spec/migrations/schedule_fix_incorrect_max_seats_used_spec.rb
new file mode 100644
index 00000000000..74258f03630
--- /dev/null
+++ b/spec/migrations/schedule_fix_incorrect_max_seats_used_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe ScheduleFixIncorrectMaxSeatsUsed, :migration do
+ let(:migration) { described_class.new }
+
+ describe '#up' do
+ it 'schedules a job on Gitlab.com' do
+ allow(Gitlab).to receive(:com?).and_return(true)
+
+ expect(migration).to receive(:migrate_in).with(1.hour, 'FixIncorrectMaxSeatsUsed')
+
+ migration.up
+ end
+
+ it 'does not schedule any jobs when not Gitlab.com' do
+ allow(Gitlab::CurrentSettings).to receive(:com?).and_return(false)
+
+ expect(migration).not_to receive(:migrate_in)
+
+ migration.up
+ end
+ end
+end
diff --git a/spec/migrations/schedule_recalculate_vulnerability_finding_signatures_for_findings_spec.rb b/spec/migrations/schedule_recalculate_vulnerability_finding_signatures_for_findings_spec.rb
index 2545bb4a66c..9b62dd79e08 100644
--- a/spec/migrations/schedule_recalculate_vulnerability_finding_signatures_for_findings_spec.rb
+++ b/spec/migrations/schedule_recalculate_vulnerability_finding_signatures_for_findings_spec.rb
@@ -51,16 +51,17 @@ RSpec.describe ScheduleRecalculateVulnerabilityFindingSignaturesForFindings, :mi
let_it_be(:finding3) { findings.create!(finding_params) }
let_it_be(:signature3) { vulnerability_finding_signatures.create!(finding_id: finding3.id, algorithm_type: 0, signature_sha: ::Digest::SHA1.digest(SecureRandom.hex(50))) }
- it 'schedules the background jobs', :aggregate_failure do
+ # this migration is now a no-op
+ it 'does not schedule the background jobs', :aggregate_failure do
Sidekiq::Testing.fake! do
freeze_time do
migrate!
- expect(BackgroundMigrationWorker.jobs.size).to eq(2)
+ expect(BackgroundMigrationWorker.jobs.size).to eq(0)
expect(described_class::MIGRATION)
- .to be_scheduled_migration_with_multiple_args(signature1.id, signature2.id)
+ .not_to be_scheduled_migration_with_multiple_args(signature1.id, signature2.id)
expect(described_class::MIGRATION)
- .to be_scheduled_migration_with_multiple_args(signature3.id, signature3.id)
+ .not_to be_scheduled_migration_with_multiple_args(signature3.id, signature3.id)
end
end
end
diff --git a/spec/migrations/start_backfill_ci_queuing_tables_spec.rb b/spec/migrations/start_backfill_ci_queuing_tables_spec.rb
new file mode 100644
index 00000000000..a1e4179efb6
--- /dev/null
+++ b/spec/migrations/start_backfill_ci_queuing_tables_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe StartBackfillCiQueuingTables do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:builds) { table(:ci_builds) }
+
+ let!(:namespace) do
+ namespaces.create!(name: 'namespace1', path: 'namespace1')
+ end
+
+ let!(:project) do
+ projects.create!(namespace_id: namespace.id, name: 'test1', path: 'test1')
+ end
+
+ let!(:pending_build_1) do
+ builds.create!(status: :pending, name: 'test1', type: 'Ci::Build', project_id: project.id)
+ end
+
+ let!(:running_build) do
+ builds.create!(status: :running, name: 'test2', type: 'Ci::Build', project_id: project.id)
+ end
+
+ let!(:pending_build_2) do
+ builds.create!(status: :pending, name: 'test3', type: 'Ci::Build', project_id: project.id)
+ end
+
+ before do
+ stub_const("#{described_class.name}::BATCH_SIZE", 1)
+ end
+
+ it 'schedules jobs for builds that are pending' do
+ Sidekiq::Testing.fake! do
+ freeze_time do
+ migrate!
+
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(
+ 2.minutes, pending_build_1.id, pending_build_1.id)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(
+ 4.minutes, pending_build_2.id, pending_build_2.id)
+ expect(BackgroundMigrationWorker.jobs.size).to eq(2)
+ end
+ end
+ end
+end
diff --git a/spec/migrations/update_default_scan_method_of_dast_site_profile_spec.rb b/spec/migrations/update_default_scan_method_of_dast_site_profile_spec.rb
new file mode 100644
index 00000000000..b73aa7016a1
--- /dev/null
+++ b/spec/migrations/update_default_scan_method_of_dast_site_profile_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe UpdateDefaultScanMethodOfDastSiteProfile do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:dast_sites) { table(:dast_sites) }
+ let(:dast_site_profiles) { table(:dast_site_profiles) }
+
+ before do
+ namespace = namespaces.create!(name: 'test', path: 'test')
+ project = projects.create!(id: 12, namespace_id: namespace.id, name: 'gitlab', path: 'gitlab')
+ dast_site = dast_sites.create!(id: 1, url: 'https://www.gitlab.com', project_id: project.id)
+
+ dast_site_profiles.create!(id: 1, project_id: project.id, dast_site_id: dast_site.id,
+ name: "#{FFaker::Product.product_name.truncate(192)} #{SecureRandom.hex(4)} - 0",
+ scan_method: 0, target_type: 0)
+
+ dast_site_profiles.create!(id: 2, project_id: project.id, dast_site_id: dast_site.id,
+ name: "#{FFaker::Product.product_name.truncate(192)} #{SecureRandom.hex(4)} - 1",
+ scan_method: 0, target_type: 1)
+ end
+
+ it 'updates the scan_method to 1 for profiles with target_type 1' do
+ migrate!
+
+ expect(dast_site_profiles.where(scan_method: 1).count).to eq 1
+ expect(dast_site_profiles.where(scan_method: 0).count).to eq 1
+ end
+end
diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb
index bb8d476f257..5bd69ad9fad 100644
--- a/spec/models/ability_spec.rb
+++ b/spec/models/ability_spec.rb
@@ -394,7 +394,7 @@ RSpec.describe Ability do
describe '.project_disabled_features_rules' do
let(:project) { create(:project, :wiki_disabled) }
- subject { described_class.policy_for(project.owner, project) }
+ subject { described_class.policy_for(project.first_owner, project) }
context 'wiki named abilities' do
it 'disables wiki abilities if the project has no wiki' do
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 0ece212d692..a962703d460 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -141,7 +141,7 @@ RSpec.describe ApplicationSetting do
it { is_expected.not_to allow_value('default' => 101).for(:repository_storages_weighted).with_message("value for 'default' must be between 0 and 100") }
it { is_expected.not_to allow_value('default' => 100, shouldntexist: 50).for(:repository_storages_weighted).with_message("can't include: shouldntexist") }
- %i[notes_create_limit user_email_lookup_limit].each do |setting|
+ %i[notes_create_limit user_email_lookup_limit users_get_by_id_limit].each do |setting|
it { is_expected.to allow_value(400).for(setting) }
it { is_expected.not_to allow_value('two').for(setting) }
it { is_expected.not_to allow_value(nil).for(setting) }
@@ -158,6 +158,11 @@ RSpec.describe ApplicationSetting do
it { is_expected.not_to allow_value(nil).for(:notes_create_limit_allowlist) }
it { is_expected.to allow_value([]).for(:notes_create_limit_allowlist) }
+ it { is_expected.to allow_value(many_usernames(100)).for(:users_get_by_id_limit_allowlist) }
+ it { is_expected.not_to allow_value(many_usernames(101)).for(:users_get_by_id_limit_allowlist) }
+ it { is_expected.not_to allow_value(nil).for(:users_get_by_id_limit_allowlist) }
+ it { is_expected.to allow_value([]).for(:users_get_by_id_limit_allowlist) }
+
it { is_expected.to allow_value('all_tiers').for(:whats_new_variant) }
it { is_expected.to allow_value('current_tier').for(:whats_new_variant) }
it { is_expected.to allow_value('disabled').for(:whats_new_variant) }
diff --git a/spec/models/audit_event_spec.rb b/spec/models/audit_event_spec.rb
index 4fba5fddc92..9f2724cebee 100644
--- a/spec/models/audit_event_spec.rb
+++ b/spec/models/audit_event_spec.rb
@@ -93,4 +93,37 @@ RSpec.describe AuditEvent do
end
end
end
+
+ describe '#author' do
+ subject { audit_event.author }
+
+ context "when the target type is not Ci::Runner" do
+ let(:audit_event) { build(:project_audit_event, target_id: 678) }
+
+ it 'returns a NullAuthor' do
+ expect(::Gitlab::Audit::NullAuthor).to receive(:for)
+ .and_call_original
+ .once
+
+ is_expected.to be_a_kind_of(::Gitlab::Audit::NullAuthor)
+ end
+ end
+
+ context 'when the target type is Ci::Runner and details contain runner_registration_token' do
+ let(:audit_event) { build(:project_audit_event, target_type: ::Ci::Runner.name, target_id: 678, details: { runner_registration_token: 'abc123' }) }
+
+ it 'returns a CiRunnerTokenAuthor' do
+ expect(::Gitlab::Audit::CiRunnerTokenAuthor).to receive(:new)
+ .with(audit_event)
+ .and_call_original
+ .once
+
+ is_expected.to be_an_instance_of(::Gitlab::Audit::CiRunnerTokenAuthor)
+ end
+
+ it 'name consists of prefix and token' do
+ expect(subject.name).to eq('Registration token: abc123')
+ end
+ end
+ end
end
diff --git a/spec/models/blob_spec.rb b/spec/models/blob_spec.rb
index bd4832bd978..6eba9ca63b0 100644
--- a/spec/models/blob_spec.rb
+++ b/spec/models/blob_spec.rb
@@ -215,6 +215,20 @@ RSpec.describe Blob do
end
end
+ describe '#symlink?' do
+ it 'is true for symlinks' do
+ symlink_blob = fake_blob(path: 'file', mode: '120000')
+
+ expect(symlink_blob.symlink?).to eq true
+ end
+
+ it 'is false for non-symlinks' do
+ non_symlink_blob = fake_blob(path: 'file', mode: '100755')
+
+ expect(non_symlink_blob.symlink?).to eq false
+ end
+ end
+
describe '#extension' do
it 'returns the extension' do
blob = fake_blob(path: 'file.md')
diff --git a/spec/models/board_spec.rb b/spec/models/board_spec.rb
index 0b7c21fd0c3..90a06b80f9c 100644
--- a/spec/models/board_spec.rb
+++ b/spec/models/board_spec.rb
@@ -16,6 +16,10 @@ RSpec.describe Board do
it { is_expected.to validate_presence_of(:project) }
end
+ describe 'constants' do
+ it { expect(described_class::RECENT_BOARDS_SIZE).to be_a(Integer) }
+ end
+
describe '#order_by_name_asc' do
let!(:board_B) { create(:board, project: project, name: 'B') }
let!(:board_C) { create(:board, project: project, name: 'C') }
diff --git a/spec/models/ci/build_metadata_spec.rb b/spec/models/ci/build_metadata_spec.rb
index b2ffb34da1d..5e30f9160cd 100644
--- a/spec/models/ci/build_metadata_spec.rb
+++ b/spec/models/ci/build_metadata_spec.rb
@@ -133,4 +133,11 @@ RSpec.describe Ci::BuildMetadata do
expect(build.cancel_gracefully?).to be false
end
end
+
+ context 'loose foreign key on ci_builds_metadata.project_id' do
+ it_behaves_like 'cleanup by a loose foreign key' do
+ let!(:parent) { create(:project) }
+ let!(:model) { create(:ci_build_metadata, project: parent) }
+ end
+ end
end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index b8c5af5a911..90298f0e973 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -3618,20 +3618,6 @@ RSpec.describe Ci::Build do
build.scoped_variables
end
-
- context 'when variables builder is used' do
- it 'returns the same variables' do
- build.user = create(:user)
-
- allow(build.pipeline).to receive(:use_variables_builder_definitions?).and_return(false)
- legacy_variables = build.scoped_variables.to_hash
-
- allow(build.pipeline).to receive(:use_variables_builder_definitions?).and_return(true)
- new_variables = build.scoped_variables.to_hash
-
- expect(new_variables).to eq(legacy_variables)
- end
- end
end
describe '#simple_variables_without_dependencies' do
@@ -3642,160 +3628,6 @@ RSpec.describe Ci::Build do
end
end
- shared_examples "secret CI variables" do
- context 'when ref is branch' do
- let(:pipeline) { create(:ci_pipeline, project: project) }
- let(:build) { create(:ci_build, ref: 'master', tag: false, pipeline: pipeline, project: project) }
-
- context 'when ref is protected' do
- before do
- create(:protected_branch, :developers_can_merge, name: 'master', project: project)
- end
-
- it { is_expected.to include(variable) }
- end
-
- context 'when ref is not protected' do
- it { is_expected.not_to include(variable) }
- end
- end
-
- context 'when ref is tag' do
- let(:pipeline) { create(:ci_pipeline, project: project) }
- let(:build) { create(:ci_build, ref: 'v1.1.0', tag: true, pipeline: pipeline, project: project) }
-
- context 'when ref is protected' do
- before do
- create(:protected_tag, project: project, name: 'v*')
- end
-
- it { is_expected.to include(variable) }
- end
-
- context 'when ref is not protected' do
- it { is_expected.not_to include(variable) }
- end
- end
-
- context 'when ref is merge request' do
- let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) }
- let(:pipeline) { merge_request.pipelines_for_merge_request.first }
- let(:build) { create(:ci_build, ref: merge_request.source_branch, tag: false, pipeline: pipeline, project: project) }
-
- context 'when ref is protected' do
- before do
- create(:protected_branch, :developers_can_merge, name: merge_request.source_branch, project: project)
- end
-
- it 'does not return protected variables as it is not supported for merge request pipelines' do
- is_expected.not_to include(variable)
- end
- end
-
- context 'when ref is not protected' do
- it { is_expected.not_to include(variable) }
- end
- end
- end
-
- describe '#secret_instance_variables' do
- subject { build.secret_instance_variables }
-
- let_it_be(:variable) { create(:ci_instance_variable, protected: true) }
-
- include_examples "secret CI variables"
- end
-
- describe '#secret_group_variables' do
- subject { build.secret_group_variables }
-
- let_it_be(:variable) { create(:ci_group_variable, protected: true, group: group) }
-
- include_examples "secret CI variables"
- end
-
- describe '#secret_project_variables' do
- subject { build.secret_project_variables }
-
- let_it_be(:variable) { create(:ci_variable, protected: true, project: project) }
-
- include_examples "secret CI variables"
- end
-
- describe '#kubernetes_variables' do
- let(:build) { create(:ci_build) }
- let(:service) { double(execute: template) }
- let(:template) { double(to_yaml: 'example-kubeconfig', valid?: template_valid) }
- let(:template_valid) { true }
-
- subject { build.kubernetes_variables }
-
- before do
- allow(Ci::GenerateKubeconfigService).to receive(:new).with(build).and_return(service)
- end
-
- it { is_expected.to include(key: 'KUBECONFIG', value: 'example-kubeconfig', public: false, file: true) }
-
- context 'generated config is invalid' do
- let(:template_valid) { false }
-
- it { is_expected.not_to include(key: 'KUBECONFIG', value: 'example-kubeconfig', public: false, file: true) }
- end
- end
-
- describe '#deployment_variables' do
- let(:build) { create(:ci_build, environment: environment) }
- let(:environment) { 'production' }
- let(:kubernetes_namespace) { 'namespace' }
- let(:project_variables) { double }
-
- subject { build.deployment_variables(environment: environment) }
-
- before do
- allow(build).to receive(:expanded_kubernetes_namespace)
- .and_return(kubernetes_namespace)
-
- allow(build.project).to receive(:deployment_variables)
- .with(environment: environment, kubernetes_namespace: kubernetes_namespace)
- .and_return(project_variables)
- end
-
- context 'environment is nil' do
- let(:environment) { nil }
-
- it { is_expected.to be_empty }
- end
- end
-
- describe '#user_variables' do
- subject { build.user_variables.to_hash }
-
- context 'with user' do
- let(:expected_variables) do
- {
- 'GITLAB_USER_EMAIL' => user.email,
- 'GITLAB_USER_ID' => user.id.to_s,
- 'GITLAB_USER_LOGIN' => user.username,
- 'GITLAB_USER_NAME' => user.name
- }
- end
-
- before do
- build.user = user
- end
-
- it { is_expected.to eq(expected_variables) }
- end
-
- context 'without user' do
- before do
- expect(build).to receive(:user).and_return(nil)
- end
-
- it { is_expected.to be_empty }
- end
- end
-
describe '#any_unmet_prerequisites?' do
let(:build) { create(:ci_build, :created) }
diff --git a/spec/models/ci/build_trace_chunk_spec.rb b/spec/models/ci/build_trace_chunk_spec.rb
index 31c7c7a44bc..e08fe196d65 100644
--- a/spec/models/ci/build_trace_chunk_spec.rb
+++ b/spec/models/ci/build_trace_chunk_spec.rb
@@ -851,7 +851,7 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state, :clean_git
context 'when project is destroyed' do
let(:subject) do
- Projects::DestroyService.new(project, project.owner).execute
+ Projects::DestroyService.new(project, project.first_owner).execute
end
it_behaves_like 'deletes all build_trace_chunk and data in redis'
diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb
index 2e8c41b410a..bd0397e0396 100644
--- a/spec/models/ci/job_artifact_spec.rb
+++ b/spec/models/ci/job_artifact_spec.rb
@@ -703,4 +703,11 @@ RSpec.describe Ci::JobArtifact do
it_behaves_like 'it has loose foreign keys' do
let(:factory_name) { :ci_job_artifact }
end
+
+ context 'loose foreign key on ci_job_artifacts.project_id' do
+ it_behaves_like 'cleanup by a loose foreign key' do
+ let!(:parent) { create(:project) }
+ let!(:model) { create(:ci_job_artifact, project: parent) }
+ end
+ end
end
diff --git a/spec/models/ci/job_token/project_scope_link_spec.rb b/spec/models/ci/job_token/project_scope_link_spec.rb
index 8d7bb44bd16..c000a3e29f7 100644
--- a/spec/models/ci/job_token/project_scope_link_spec.rb
+++ b/spec/models/ci/job_token/project_scope_link_spec.rb
@@ -88,4 +88,18 @@ RSpec.describe Ci::JobToken::ProjectScopeLink do
it { is_expected.to be_nil }
end
end
+
+ context 'loose foreign key on ci_job_token_project_scope_links.source_project_id' do
+ it_behaves_like 'cleanup by a loose foreign key' do
+ let!(:parent) { create(:project) }
+ let!(:model) { create(:ci_job_token_project_scope_link, source_project: parent) }
+ end
+ end
+
+ context 'loose foreign key on ci_job_token_project_scope_links.target_project_id' do
+ it_behaves_like 'cleanup by a loose foreign key' do
+ let!(:parent) { create(:project) }
+ let!(:model) { create(:ci_job_token_project_scope_link, target_project: parent) }
+ end
+ end
end
diff --git a/spec/models/ci/namespace_mirror_spec.rb b/spec/models/ci/namespace_mirror_spec.rb
index a9d916115fc..38471f15849 100644
--- a/spec/models/ci/namespace_mirror_spec.rb
+++ b/spec/models/ci/namespace_mirror_spec.rb
@@ -20,10 +20,10 @@ RSpec.describe Ci::NamespaceMirror do
end
context 'scopes' do
- describe '.contains_namespace' do
+ describe '.by_group_and_descendants' do
let_it_be(:another_group) { create(:group) }
- subject(:result) { described_class.contains_namespace(group2.id) }
+ subject(:result) { described_class.by_group_and_descendants(group2.id) }
it 'returns groups having group2.id in traversal_ids' do
expect(result.pluck(:namespace_id)).to contain_exactly(group2.id, group3.id, group4.id)
diff --git a/spec/models/ci/pipeline_schedule_spec.rb b/spec/models/ci/pipeline_schedule_spec.rb
index 0f1cb721e95..0f4f148775e 100644
--- a/spec/models/ci/pipeline_schedule_spec.rb
+++ b/spec/models/ci/pipeline_schedule_spec.rb
@@ -227,4 +227,11 @@ RSpec.describe Ci::PipelineSchedule do
it { is_expected.to eq(144) }
end
end
+
+ context 'loose foreign key on ci_pipeline_schedules.project_id' do
+ it_behaves_like 'cleanup by a loose foreign key' do
+ let!(:parent) { create(:project) }
+ let!(:model) { create(:ci_pipeline_schedule, project: parent) }
+ end
+ end
end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 90f56c1e0a4..c29cc04e0e9 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -390,20 +390,63 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end
describe '#merge_request?' do
- let(:pipeline) { create(:ci_pipeline, merge_request: merge_request) }
- let(:merge_request) { create(:merge_request) }
+ let_it_be(:merge_request) { create(:merge_request) }
+ let_it_be_with_reload(:pipeline) do
+ create(:ci_pipeline, project: project, merge_request_id: merge_request.id)
+ end
+
+ it { expect(pipeline).to be_merge_request }
- it 'returns true' do
- expect(pipeline).to be_merge_request
+ context 'when merge request is already loaded' do
+ it 'does not reload the record and returns true' do
+ expect(pipeline.merge_request).to be_present
+
+ expect { pipeline.merge_request? }.not_to exceed_query_limit(0)
+ expect(pipeline).to be_merge_request
+ end
end
- context 'when merge request is nil' do
- let(:merge_request) { nil }
+ context 'when merge request is not loaded' do
+ it 'executes a database query and returns true' do
+ expect(pipeline).to be_present
- it 'returns false' do
+ expect { pipeline.merge_request? }.not_to exceed_query_limit(1)
+ expect(pipeline).to be_merge_request
+ end
+
+ it 'caches the result' do
+ expect(pipeline).to be_merge_request
+ expect { pipeline.merge_request? }.not_to exceed_query_limit(0)
+ end
+ end
+
+ context 'when merge request was removed' do
+ before do
+ pipeline.update!(merge_request_id: non_existing_record_id)
+ end
+
+ it 'executes a database query and returns false' do
+ expect { pipeline.merge_request? }.not_to exceed_query_limit(1)
expect(pipeline).not_to be_merge_request
end
end
+
+ context 'when merge request id is not present' do
+ before do
+ pipeline.update!(merge_request_id: nil)
+ end
+
+ it { expect(pipeline).not_to be_merge_request }
+ end
+
+ context 'when the feature flag is disabled' do
+ before do
+ stub_feature_flags(ci_pipeline_merge_request_presence_check: false)
+ pipeline.update!(merge_request_id: non_existing_record_id)
+ end
+
+ it { expect(pipeline).to be_merge_request }
+ end
end
describe '#detached_merge_request_pipeline?' do
@@ -3384,7 +3427,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
create(:ci_pipeline,
project: project,
sha: project.commit('master').sha,
- user: project.owner)
+ user: project.first_owner)
end
before do
@@ -4459,7 +4502,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
describe '#reset_source_bridge!' do
let(:pipeline) { create(:ci_pipeline, :created, project: project) }
- subject(:reset_bridge) { pipeline.reset_source_bridge!(project.owner) }
+ subject(:reset_bridge) { pipeline.reset_source_bridge!(project.first_owner) }
context 'when the pipeline is a child pipeline and the bridge is depended' do
let!(:parent_pipeline) { create(:ci_pipeline) }
@@ -4656,9 +4699,11 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
let(:factory_name) { :ci_pipeline }
end
- it_behaves_like 'cleanup by a loose foreign key' do
- let!(:model) { create(:ci_pipeline, user: create(:user)) }
- let!(:parent) { model.user }
+ context 'loose foreign key on ci_pipelines.user_id' do
+ it_behaves_like 'cleanup by a loose foreign key' do
+ let!(:model) { create(:ci_pipeline, user: create(:user)) }
+ let!(:parent) { model.user }
+ end
end
describe 'tags count' do
@@ -4679,4 +4724,55 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
it { expect(pipeline.distinct_tags_count).to eq(3) }
end
end
+
+ context 'loose foreign key on ci_pipelines.merge_request_id' do
+ it_behaves_like 'cleanup by a loose foreign key' do
+ let!(:parent) { create(:merge_request) }
+ let!(:model) { create(:ci_pipeline, merge_request: parent) }
+ end
+ end
+
+ context 'loose foreign key on ci_pipelines.project_id' do
+ it_behaves_like 'cleanup by a loose foreign key' do
+ let!(:parent) { create(:project) }
+ let!(:model) { create(:ci_pipeline, project: parent) }
+ end
+ end
+
+ describe '#jobs_git_ref' do
+ subject { pipeline.jobs_git_ref }
+
+ context 'when tag is true' do
+ let(:pipeline) { build(:ci_pipeline, tag: true) }
+
+ it 'returns a tag ref' do
+ is_expected.to start_with(Gitlab::Git::TAG_REF_PREFIX)
+ end
+ end
+
+ context 'when tag is false' do
+ let(:pipeline) { build(:ci_pipeline, tag: false) }
+
+ it 'returns a branch ref' do
+ is_expected.to start_with(Gitlab::Git::BRANCH_REF_PREFIX)
+ end
+ end
+
+ context 'when tag is nil' do
+ let(:pipeline) { build(:ci_pipeline, tag: nil) }
+
+ it 'returns a branch ref' do
+ is_expected.to start_with(Gitlab::Git::BRANCH_REF_PREFIX)
+ end
+ end
+
+ context 'when it is triggered by a merge request' do
+ let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) }
+ let(:pipeline) { merge_request.pipelines_for_merge_request.first }
+
+ it 'returns nil' do
+ is_expected.to be_nil
+ end
+ end
+ end
end
diff --git a/spec/models/ci/ref_spec.rb b/spec/models/ci/ref_spec.rb
index 0a9cd5ef2ec..ffbda4b459f 100644
--- a/spec/models/ci/ref_spec.rb
+++ b/spec/models/ci/ref_spec.rb
@@ -231,4 +231,11 @@ RSpec.describe Ci::Ref do
it_behaves_like 'no-op'
end
end
+
+ context 'loose foreign key on ci_refs.project_id' do
+ it_behaves_like 'cleanup by a loose foreign key' do
+ let!(:parent) { create(:project) }
+ let!(:model) { create(:ci_ref, project: parent) }
+ end
+ end
end
diff --git a/spec/models/ci/runner_project_spec.rb b/spec/models/ci/runner_project_spec.rb
index 13369dba2cf..40ad79c7295 100644
--- a/spec/models/ci/runner_project_spec.rb
+++ b/spec/models/ci/runner_project_spec.rb
@@ -6,4 +6,11 @@ RSpec.describe Ci::RunnerProject do
it_behaves_like 'includes Limitable concern' do
subject { build(:ci_runner_project, project: create(:project), runner: create(:ci_runner, :project)) }
end
+
+ context 'loose foreign key on ci_runner_projects.project_id' do
+ it_behaves_like 'cleanup by a loose foreign key' do
+ let!(:parent) { create(:project) }
+ let!(:model) { create(:ci_runner_project, project: parent) }
+ end
+ end
end
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index 6830a8daa3b..eb29db697a5 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe Ci::Runner do
+ include StubGitlabCalls
+
it_behaves_like 'having unique enum values'
it_behaves_like 'it has loose foreign keys' do
@@ -23,6 +25,16 @@ RSpec.describe Ci::Runner do
end
end
+ describe 'projects association' do
+ let(:runner) { create(:ci_runner, :project) }
+
+ it 'does not create a cross-database query' do
+ with_cross_joins_prevented do
+ expect(runner.projects.count).to eq(1)
+ end
+ end
+ end
+
describe 'validation' do
it { is_expected.to validate_presence_of(:access_level) }
it { is_expected.to validate_presence_of(:runner_type) }
@@ -255,22 +267,89 @@ RSpec.describe Ci::Runner do
it_behaves_like '.belonging_to_parent_group_of_project'
end
- describe '.owned_or_instance_wide' do
- it 'returns a globally shared, a project specific and a group specific runner' do
- # group specific
- group = create(:group)
- project = create(:project, group: group)
- group_runner = create(:ci_runner, :group, groups: [group])
+ context 'with instance runners sharing enabled' do
+ # group specific
+ let_it_be(:group) { create(:group, shared_runners_enabled: true) }
+ let_it_be(:project) { create(:project, group: group, shared_runners_enabled: true) }
+ let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group]) }
- # project specific
- project_runner = create(:ci_runner, :project, projects: [project])
+ # project specific
+ let_it_be(:project_runner) { create(:ci_runner, :project, projects: [project]) }
- # globally shared
- shared_runner = create(:ci_runner, :instance)
+ # globally shared
+ let_it_be(:shared_runner) { create(:ci_runner, :instance) }
- expect(described_class.owned_or_instance_wide(project.id)).to contain_exactly(
- group_runner, project_runner, shared_runner
- )
+ describe '.owned_or_instance_wide' do
+ subject { described_class.owned_or_instance_wide(project.id) }
+
+ it 'returns a globally shared, a project specific and a group specific runner' do
+ is_expected.to contain_exactly(group_runner, project_runner, shared_runner)
+ end
+ end
+
+ describe '.group_or_instance_wide' do
+ subject { described_class.group_or_instance_wide(group) }
+
+ before do
+ # Ensure the project runner is instantiated
+ project_runner
+ end
+
+ it 'returns a globally shared and a group specific runner' do
+ is_expected.to contain_exactly(group_runner, shared_runner)
+ end
+ end
+ end
+
+ context 'with instance runners sharing disabled' do
+ # group specific
+ let_it_be(:group) { create(:group, shared_runners_enabled: false) }
+ let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group]) }
+
+ let(:group_runners_enabled) { true }
+ let(:project) { create(:project, group: group, shared_runners_enabled: false) }
+
+ # project specific
+ let(:project_runner) { create(:ci_runner, :project, projects: [project]) }
+
+ # globally shared
+ let_it_be(:shared_runner) { create(:ci_runner, :instance) }
+
+ before do
+ project.update!(group_runners_enabled: group_runners_enabled)
+ end
+
+ describe '.owned_or_instance_wide' do
+ subject { described_class.owned_or_instance_wide(project.id) }
+
+ context 'with group runners disabled' do
+ let(:group_runners_enabled) { false }
+
+ it 'returns only the project specific runner' do
+ is_expected.to contain_exactly(project_runner)
+ end
+ end
+
+ context 'with group runners enabled' do
+ let(:group_runners_enabled) { true }
+
+ it 'returns a project specific and a group specific runner' do
+ is_expected.to contain_exactly(group_runner, project_runner)
+ end
+ end
+ end
+
+ describe '.group_or_instance_wide' do
+ subject { described_class.group_or_instance_wide(group) }
+
+ before do
+ # Ensure the project runner is instantiated
+ project_runner
+ end
+
+ it 'returns a group specific runner' do
+ is_expected.to contain_exactly(group_runner)
+ end
end
end
@@ -291,6 +370,30 @@ RSpec.describe Ci::Runner do
end
end
+ describe '#only_for' do
+ let_it_be_with_reload(:runner) { create(:ci_runner, :project) }
+ let_it_be(:project) { runner.projects.first }
+
+ subject { runner.only_for?(project) }
+
+ context 'with matching project' do
+ it { is_expected.to be_truthy }
+ end
+
+ context 'without matching project' do
+ let_it_be(:project) { create(:project) }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'with runner having multiple projects' do
+ let_it_be(:other_project) { create(:project) }
+ let_it_be(:runner_project) { create(:ci_runner_project, project: other_project, runner: runner) }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
describe '#assign_to' do
let(:project) { create(:project) }
@@ -544,7 +647,7 @@ RSpec.describe Ci::Runner do
end
end
- describe '#can_pick?' do
+ describe '#matches_build?' do
using RSpec::Parameterized::TableSyntax
let_it_be(:pipeline) { create(:ci_pipeline) }
@@ -555,31 +658,15 @@ RSpec.describe Ci::Runner do
let(:tag_list) { [] }
let(:run_untagged) { true }
- subject { runner.can_pick?(build) }
-
- context 'a different runner' do
- let(:other_project) { create(:project) }
- let(:other_runner) { create(:ci_runner, :project, projects: [other_project], tag_list: tag_list, run_untagged: run_untagged) }
-
- before do
- # `can_pick?` is not used outside the runners available for the project
- stub_feature_flags(ci_runners_short_circuit_assignable_for: false)
- end
-
- it 'cannot handle builds' do
- expect(other_runner.can_pick?(build)).to be_falsey
- end
- end
+ subject { runner.matches_build?(build) }
context 'when runner does not have tags' do
- it 'can handle builds without tags' do
- expect(runner.can_pick?(build)).to be_truthy
- end
+ it { is_expected.to be_truthy }
it 'cannot handle build with tags' do
build.tag_list = ['aa']
- expect(runner.can_pick?(build)).to be_falsey
+ is_expected.to be_falsey
end
end
@@ -590,20 +677,18 @@ RSpec.describe Ci::Runner do
it 'can handle build with matching tags' do
build.tag_list = ['bb']
- expect(runner.can_pick?(build)).to be_truthy
+ is_expected.to be_truthy
end
it 'cannot handle build without matching tags' do
build.tag_list = ['aa']
- expect(runner.can_pick?(build)).to be_falsey
+ is_expected.to be_falsey
end
end
context 'when runner can pick untagged jobs' do
- it 'can handle builds without tags' do
- expect(runner.can_pick?(build)).to be_truthy
- end
+ it { is_expected.to be_truthy }
it_behaves_like 'tagged build picker'
end
@@ -611,9 +696,7 @@ RSpec.describe Ci::Runner do
context 'when runner cannot pick untagged jobs' do
let(:run_untagged) { false }
- it 'cannot handle builds without tags' do
- expect(runner.can_pick?(build)).to be_falsey
- end
+ it { is_expected.to be_falsey }
it_behaves_like 'tagged build picker'
end
@@ -622,64 +705,31 @@ RSpec.describe Ci::Runner do
context 'when runner is shared' do
let(:runner) { create(:ci_runner, :instance) }
- it 'can handle builds' do
- expect(runner.can_pick?(build)).to be_truthy
- end
+ it { is_expected.to be_truthy }
context 'when runner is locked' do
let(:runner) { create(:ci_runner, :instance, locked: true) }
- it 'can handle builds' do
- expect(runner.can_pick?(build)).to be_truthy
- end
+ it { is_expected.to be_truthy }
end
it 'does not query for owned or instance runners' do
expect(described_class).not_to receive(:owned_or_instance_wide)
- runner.can_pick?(build)
- end
-
- context 'when feature flag ci_runners_short_circuit_assignable_for is disabled' do
- before do
- stub_feature_flags(ci_runners_short_circuit_assignable_for: false)
- end
-
- it 'does not query for owned or instance runners' do
- expect(described_class).to receive(:owned_or_instance_wide).and_call_original
-
- runner.can_pick?(build)
- end
+ subject
end
end
context 'when runner is not shared' do
- before do
- # `can_pick?` is not used outside the runners available for the project
- stub_feature_flags(ci_runners_short_circuit_assignable_for: false)
- end
-
context 'when runner is assigned to a project' do
- it 'can handle builds' do
- expect(runner.can_pick?(build)).to be_truthy
- end
- end
-
- context 'when runner is assigned to another project' do
- let(:runner_project) { create(:project) }
-
- it 'cannot handle builds' do
- expect(runner.can_pick?(build)).to be_falsey
- end
+ it { is_expected.to be_truthy }
end
context 'when runner is assigned to a group' do
let(:group) { create(:group, projects: [build.project]) }
let(:runner) { create(:ci_runner, :group, tag_list: tag_list, run_untagged: run_untagged, groups: [group]) }
- it 'can handle builds' do
- expect(runner.can_pick?(build)).to be_truthy
- end
+ it { is_expected.to be_truthy }
it 'knows namespace id it is assigned to' do
expect(runner.namespace_ids).to eq [group.id]
@@ -1220,14 +1270,6 @@ RSpec.describe Ci::Runner do
runner.pick_build!(build)
end
end
-
- context 'build picking improvement' do
- it 'does not check if the build is assignable to a runner' do
- expect(runner).not_to receive(:can_pick?)
-
- runner.pick_build!(build)
- end
- end
end
describe 'project runner without projects is destroyable' do
@@ -1259,6 +1301,20 @@ RSpec.describe Ci::Runner do
expect(runners).to eq([runner2, runner1])
end
+
+ it 'supports ordering by the token expiration' do
+ runner1 = create(:ci_runner)
+ runner1.update!(token_expires_at: 1.year.from_now)
+ runner2 = create(:ci_runner)
+ runner3 = create(:ci_runner)
+ runner3.update!(token_expires_at: 1.month.from_now)
+
+ runners = described_class.order_by('token_expires_at_asc')
+ expect(runners).to eq([runner3, runner1, runner2])
+
+ runners = described_class.order_by('token_expires_at_desc')
+ expect(runners).to eq([runner2, runner1, runner3])
+ end
end
describe '.runner_matchers' do
@@ -1386,47 +1442,6 @@ RSpec.describe Ci::Runner do
it { is_expected.to eq(contacted_at_stored) }
end
- describe '.legacy_belonging_to_group' do
- shared_examples 'returns group runners' do
- it 'returns the specific group runner' do
- group = create(:group)
- runner = create(:ci_runner, :group, groups: [group])
- unrelated_group = create(:group)
- create(:ci_runner, :group, groups: [unrelated_group])
-
- expect(described_class.legacy_belonging_to_group(group.id)).to contain_exactly(runner)
- end
-
- context 'runner belonging to parent group' do
- let_it_be(:parent_group) { create(:group) }
- let_it_be(:parent_runner) { create(:ci_runner, :group, groups: [parent_group]) }
- let_it_be(:group) { create(:group, parent: parent_group) }
-
- context 'when include_parent option is passed' do
- it 'returns the group runner from the parent group' do
- expect(described_class.legacy_belonging_to_group(group.id, include_ancestors: true)).to contain_exactly(parent_runner)
- end
- end
-
- context 'when include_parent option is not passed' do
- it 'does not return the group runner from the parent group' do
- expect(described_class.legacy_belonging_to_group(group.id)).to be_empty
- end
- end
- end
- end
-
- it_behaves_like 'returns group runners'
-
- context 'when feature flag :linear_runner_ancestor_scopes is disabled' do
- before do
- stub_feature_flags(linear_runner_ancestor_scopes: false)
- end
-
- it_behaves_like 'returns group runners'
- end
- end
-
describe '.belonging_to_group' do
it 'returns the specific group runner' do
group = create(:group)
@@ -1470,4 +1485,182 @@ RSpec.describe Ci::Runner do
)
end
end
+
+ describe '#token_expires_at', :freeze_time do
+ shared_examples 'expiring token' do |interval:|
+ it 'expires' do
+ expect(runner.token_expires_at).to eq(interval.from_now)
+ end
+ end
+
+ shared_examples 'non-expiring token' do
+ it 'does not expire' do
+ expect(runner.token_expires_at).to be_nil
+ end
+ end
+
+ context 'no expiration' do
+ let(:runner) { create(:ci_runner) }
+
+ it_behaves_like 'non-expiring token'
+ end
+
+ context 'system-wide shared expiration' do
+ before do
+ stub_application_setting(runner_token_expiration_interval: 5.days.to_i)
+ end
+
+ let(:runner) { create(:ci_runner) }
+
+ it_behaves_like 'expiring token', interval: 5.days
+ end
+
+ context 'system-wide group expiration' do
+ before do
+ stub_application_setting(group_runner_token_expiration_interval: 5.days.to_i)
+ end
+
+ let(:runner) { create(:ci_runner) }
+
+ it_behaves_like 'non-expiring token'
+ end
+
+ context 'system-wide project expiration' do
+ before do
+ stub_application_setting(project_runner_token_expiration_interval: 5.days.to_i)
+ end
+
+ let(:runner) { create(:ci_runner) }
+
+ it_behaves_like 'non-expiring token'
+ end
+
+ context 'group expiration' do
+ let(:group_settings) { create(:namespace_settings, runner_token_expiration_interval: 6.days.to_i) }
+ let(:group) { create(:group, namespace_settings: group_settings) }
+ let(:runner) { create(:ci_runner, :group, groups: [group]) }
+
+ it_behaves_like 'expiring token', interval: 6.days
+ end
+
+ context 'human-readable group expiration' do
+ let(:group_settings) { create(:namespace_settings, runner_token_expiration_interval_human_readable: '7 days') }
+ let(:group) { create(:group, namespace_settings: group_settings) }
+ let(:runner) { create(:ci_runner, :group, groups: [group]) }
+
+ it_behaves_like 'expiring token', interval: 7.days
+ end
+
+ context 'project expiration' do
+ let(:project) { create(:project, runner_token_expiration_interval: 4.days.to_i).tap(&:save!) }
+ let(:runner) { create(:ci_runner, :project, projects: [project]) }
+
+ it_behaves_like 'expiring token', interval: 4.days
+ end
+
+ context 'human-readable project expiration' do
+ let(:project) { create(:project, runner_token_expiration_interval_human_readable: '5 days').tap(&:save!) }
+ let(:runner) { create(:ci_runner, :project, projects: [project]) }
+
+ it_behaves_like 'expiring token', interval: 5.days
+ end
+
+ context 'multiple projects' do
+ let(:project1) { create(:project, runner_token_expiration_interval: 8.days.to_i).tap(&:save!) }
+ let(:project2) { create(:project, runner_token_expiration_interval: 7.days.to_i).tap(&:save!) }
+ let(:project3) { create(:project, runner_token_expiration_interval: 9.days.to_i).tap(&:save!) }
+ let(:runner) { create(:ci_runner, :project, projects: [project1, project2, project3]) }
+
+ it_behaves_like 'expiring token', interval: 7.days
+ end
+
+ context 'with project runner token expiring' do
+ let_it_be(:project) { create(:project, runner_token_expiration_interval: 4.days.to_i).tap(&:save!) }
+
+ context 'project overrides system' do
+ before do
+ stub_application_setting(project_runner_token_expiration_interval: 5.days.to_i)
+ end
+
+ let(:runner) { create(:ci_runner, :project, projects: [project]) }
+
+ it_behaves_like 'expiring token', interval: 4.days
+ end
+
+ context 'system overrides project' do
+ before do
+ stub_application_setting(project_runner_token_expiration_interval: 3.days.to_i)
+ end
+
+ let(:runner) { create(:ci_runner, :project, projects: [project]) }
+
+ it_behaves_like 'expiring token', interval: 3.days
+ end
+ end
+
+ context 'with group runner token expiring' do
+ let_it_be(:group_settings) { create(:namespace_settings, runner_token_expiration_interval: 4.days.to_i) }
+ let_it_be(:group) { create(:group, namespace_settings: group_settings) }
+
+ context 'group overrides system' do
+ before do
+ stub_application_setting(group_runner_token_expiration_interval: 5.days.to_i)
+ end
+
+ let(:runner) { create(:ci_runner, :group, groups: [group]) }
+
+ it_behaves_like 'expiring token', interval: 4.days
+ end
+
+ context 'system overrides group' do
+ before do
+ stub_application_setting(group_runner_token_expiration_interval: 3.days.to_i)
+ end
+
+ let(:runner) { create(:ci_runner, :group, groups: [group]) }
+
+ it_behaves_like 'expiring token', interval: 3.days
+ end
+ end
+
+ context "with group's project runner token expiring" do
+ let_it_be(:parent_group_settings) { create(:namespace_settings, subgroup_runner_token_expiration_interval: 2.days.to_i) }
+ let_it_be(:parent_group) { create(:group, namespace_settings: parent_group_settings) }
+
+ context 'parent group overrides subgroup' do
+ let(:group_settings) { create(:namespace_settings, runner_token_expiration_interval: 3.days.to_i) }
+ let(:group) { create(:group, parent: parent_group, namespace_settings: group_settings) }
+ let(:runner) { create(:ci_runner, :group, groups: [group]) }
+
+ it_behaves_like 'expiring token', interval: 2.days
+ end
+
+ context 'subgroup overrides parent group' do
+ let(:group_settings) { create(:namespace_settings, runner_token_expiration_interval: 1.day.to_i) }
+ let(:group) { create(:group, parent: parent_group, namespace_settings: group_settings) }
+ let(:runner) { create(:ci_runner, :group, groups: [group]) }
+
+ it_behaves_like 'expiring token', interval: 1.day
+ end
+ end
+
+ context "with group's project runner token expiring" do
+ let_it_be(:group_settings) { create(:namespace_settings, project_runner_token_expiration_interval: 2.days.to_i) }
+ let_it_be(:group) { create(:group, namespace_settings: group_settings) }
+
+ context 'group overrides project' do
+ let(:project) { create(:project, group: group, runner_token_expiration_interval: 3.days.to_i).tap(&:save!) }
+ let(:runner) { create(:ci_runner, :project, projects: [project]) }
+
+ it_behaves_like 'expiring token', interval: 2.days
+ end
+
+ context 'project overrides group' do
+ let(:project) { create(:project, group: group, runner_token_expiration_interval: 1.day.to_i).tap(&:save!) }
+ let(:runner) { create(:ci_runner, :project, projects: [project]) }
+
+ it_behaves_like 'expiring token', interval: 1.day
+ end
+ end
+ end
end
diff --git a/spec/models/ci/sources/pipeline_spec.rb b/spec/models/ci/sources/pipeline_spec.rb
index ccf3140650b..73f7cfa739f 100644
--- a/spec/models/ci/sources/pipeline_spec.rb
+++ b/spec/models/ci/sources/pipeline_spec.rb
@@ -17,4 +17,18 @@ RSpec.describe Ci::Sources::Pipeline do
it { is_expected.to validate_presence_of(:source_project) }
it { is_expected.to validate_presence_of(:source_job) }
it { is_expected.to validate_presence_of(:source_pipeline) }
+
+ context 'loose foreign key on ci_sources_pipelines.source_project_id' do
+ it_behaves_like 'cleanup by a loose foreign key' do
+ let!(:parent) { create(:project) }
+ let!(:model) { create(:ci_sources_pipeline, source_project: parent) }
+ end
+ end
+
+ context 'loose foreign key on ci_sources_pipelines.project_id' do
+ it_behaves_like 'cleanup by a loose foreign key' do
+ let!(:parent) { create(:project) }
+ let!(:model) { create(:ci_sources_pipeline, project: parent) }
+ end
+ end
end
diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb
index 2b6f22e68f1..b91348eb408 100644
--- a/spec/models/ci/stage_spec.rb
+++ b/spec/models/ci/stage_spec.rb
@@ -362,4 +362,11 @@ RSpec.describe Ci::Stage, :models do
end
it_behaves_like 'manual playable stage', :ci_stage_entity
+
+ context 'loose foreign key on ci_stages.project_id' do
+ it_behaves_like 'cleanup by a loose foreign key' do
+ let!(:parent) { create(:project) }
+ let!(:model) { create(:ci_stage_entity, project: parent) }
+ end
+ end
end
diff --git a/spec/models/ci/trigger_spec.rb b/spec/models/ci/trigger_spec.rb
index c254279a32f..4ac8720780c 100644
--- a/spec/models/ci/trigger_spec.rb
+++ b/spec/models/ci/trigger_spec.rb
@@ -59,6 +59,20 @@ RSpec.describe Ci::Trigger do
end
it_behaves_like 'includes Limitable concern' do
- subject { build(:ci_trigger, owner: project.owner, project: project) }
+ subject { build(:ci_trigger, owner: project.first_owner, project: project) }
+ end
+
+ context 'loose foreign key on ci_triggers.owner_id' do
+ it_behaves_like 'cleanup by a loose foreign key' do
+ let!(:parent) { create(:user) }
+ let!(:model) { create(:ci_trigger, owner: parent) }
+ end
+ end
+
+ context 'loose foreign key on ci_triggers.project_id' do
+ it_behaves_like 'cleanup by a loose foreign key' do
+ let!(:parent) { create(:project) }
+ let!(:model) { create(:ci_trigger, project: parent) }
+ end
end
end
diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb
index 93a24ba9157..29ca088ee04 100644
--- a/spec/models/ci/variable_spec.rb
+++ b/spec/models/ci/variable_spec.rb
@@ -44,4 +44,11 @@ RSpec.describe Ci::Variable do
end
end
end
+
+ context 'loose foreign key on ci_variables.project_id' do
+ it_behaves_like 'cleanup by a loose foreign key' do
+ let!(:parent) { create(:project) }
+ let!(:model) { create(:ci_variable, project: parent) }
+ end
+ end
end
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index 2176eea75bc..7c67b9a3d63 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -486,7 +486,7 @@ eos
it 'uses the CachedMarkdownField cache instead of the Mentionable cache', :use_clean_rails_redis_caching do
expect(commit.title_html).not_to be_present
- commit.all_references(project.owner).all
+ commit.all_references(project.first_owner).all
expect(commit.title_html).to be_present
expect(Rails.cache.read("banzai/commit:#{commit.id}/safe_message/single_line")).to be_nil
@@ -748,29 +748,23 @@ eos
describe '#work_in_progress?' do
[
- 'squash! ', 'fixup! ', 'wip: ', 'WIP: ', '[WIP] ',
+ 'squash! ', 'fixup! ',
'draft: ', '[Draft] ', '(draft) ', 'Draft: '
- ].each do |wip_prefix|
- it "detects the '#{wip_prefix}' prefix" do
- commit.message = "#{wip_prefix}#{commit.message}"
+ ].each do |draft_prefix|
+ it "detects the '#{draft_prefix}' prefix" do
+ commit.message = "#{draft_prefix}#{commit.message}"
expect(commit).to be_work_in_progress
end
end
- it "detects WIP for a commit just saying 'wip'" do
- commit.message = "wip"
-
- expect(commit).to be_work_in_progress
- end
-
it "does not detect WIP for a commit just saying 'draft'" do
commit.message = "draft"
expect(commit).not_to be_work_in_progress
end
- ["FIXUP!", "Draft - ", "Wipeout"].each do |draft_prefix|
+ ["FIXUP!", "Draft - ", "Wipeout", "WIP: ", "[WIP] ", "wip: "].each do |draft_prefix|
it "doesn't detect '#{draft_prefix}' at the start of the title as a draft" do
commit.message = "#{draft_prefix} #{commit.message}"
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index d5e74d36b58..86ee159b97e 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -987,4 +987,11 @@ RSpec.describe CommitStatus do
commit_status.expire_etag_cache!
end
end
+
+ context 'loose foreign key on ci_builds.project_id' do
+ it_behaves_like 'cleanup by a loose foreign key' do
+ let!(:parent) { create(:project) }
+ let!(:model) { create(:ci_build, project: parent) }
+ end
+ end
end
diff --git a/spec/models/concerns/after_commit_queue_spec.rb b/spec/models/concerns/after_commit_queue_spec.rb
index 40cddde333e..8f091081dce 100644
--- a/spec/models/concerns/after_commit_queue_spec.rb
+++ b/spec/models/concerns/after_commit_queue_spec.rb
@@ -75,7 +75,7 @@ RSpec.describe AfterCommitQueue do
skip_if_multiple_databases_not_setup
table_sql = <<~SQL
- CREATE TABLE _test_ci_after_commit_queue (
+ CREATE TABLE _test_gitlab_ci_after_commit_queue (
id serial NOT NULL PRIMARY KEY);
SQL
@@ -84,7 +84,7 @@ RSpec.describe AfterCommitQueue do
let(:ci_klass) do
Class.new(Ci::ApplicationRecord) do
- self.table_name = '_test_ci_after_commit_queue'
+ self.table_name = '_test_gitlab_ci_after_commit_queue'
include AfterCommitQueue
diff --git a/spec/models/concerns/ci/has_variable_spec.rb b/spec/models/concerns/ci/has_variable_spec.rb
index e917ec6b802..bf699119a37 100644
--- a/spec/models/concerns/ci/has_variable_spec.rb
+++ b/spec/models/concerns/ci/has_variable_spec.rb
@@ -68,9 +68,48 @@ RSpec.describe Ci::HasVariable do
end
describe '#to_runner_variable' do
+ let_it_be(:ci_variable) { create(:ci_variable) }
+
+ subject { ci_variable }
+
it 'returns a hash for the runner' do
expect(subject.to_runner_variable)
.to include(key: subject.key, value: subject.value, public: false)
end
+
+ context 'with RequestStore enabled', :request_store do
+ let(:expected) do
+ {
+ file: false,
+ key: subject.key,
+ value: subject.value,
+ public: false,
+ masked: false
+ }
+ end
+
+ it 'decrypts once' do
+ expect(OpenSSL::PKCS5).to receive(:pbkdf2_hmac).once.and_call_original
+
+ 2.times { expect(subject.reload.to_runner_variable).to eq(expected) }
+ end
+
+ it 'does not cache similar keys', :aggregate_failures do
+ group_var = create(:ci_group_variable, key: subject.key, value: 'group')
+ project_var = create(:ci_variable, key: subject.key, value: 'project')
+
+ expect(subject.to_runner_variable).to include(key: subject.key, value: subject.value)
+ expect(group_var.to_runner_variable).to include(key: subject.key, value: 'group')
+ expect(project_var.to_runner_variable).to include(key: subject.key, value: 'project')
+ end
+
+ it 'does not cache unpersisted values' do
+ new_variable = Ci::Variable.new(key: SecureRandom.hex, value: "12345")
+ old_value = new_variable.to_runner_variable
+ new_variable.value = '98765'
+
+ expect(new_variable.to_runner_variable).not_to eq(old_value)
+ end
+ end
end
end
diff --git a/spec/models/concerns/cross_database_modification_spec.rb b/spec/models/concerns/cross_database_modification_spec.rb
new file mode 100644
index 00000000000..72544536953
--- /dev/null
+++ b/spec/models/concerns/cross_database_modification_spec.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe CrossDatabaseModification do
+ describe '.transaction' do
+ context 'feature flag disabled' do
+ before do
+ stub_feature_flags(track_gitlab_schema_in_current_transaction: false)
+ end
+
+ it 'does not add to gitlab_transactions_stack' do
+ ApplicationRecord.transaction do
+ expect(ApplicationRecord.gitlab_transactions_stack).to be_empty
+
+ Project.first
+ end
+
+ expect(ApplicationRecord.gitlab_transactions_stack).to be_empty
+ end
+ end
+
+ context 'feature flag is not yet setup' do
+ before do
+ allow(Feature::FlipperFeature).to receive(:table_exists?).and_raise(ActiveRecord::NoDatabaseError)
+ end
+
+ it 'does not add to gitlab_transactions_stack' do
+ ApplicationRecord.transaction do
+ expect(ApplicationRecord.gitlab_transactions_stack).to be_empty
+
+ Project.first
+ end
+
+ expect(ApplicationRecord.gitlab_transactions_stack).to be_empty
+ end
+ end
+
+ it 'adds the current gitlab schema to gitlab_transactions_stack', :aggregate_failures do
+ ApplicationRecord.transaction do
+ expect(ApplicationRecord.gitlab_transactions_stack).to contain_exactly(:gitlab_main)
+
+ Project.first
+ end
+
+ expect(ApplicationRecord.gitlab_transactions_stack).to be_empty
+
+ Ci::ApplicationRecord.transaction do
+ expect(ApplicationRecord.gitlab_transactions_stack).to contain_exactly(:gitlab_ci)
+
+ Project.first
+ end
+
+ expect(ApplicationRecord.gitlab_transactions_stack).to be_empty
+
+ Project.transaction do
+ expect(ApplicationRecord.gitlab_transactions_stack).to contain_exactly(:gitlab_main)
+
+ Project.first
+ end
+
+ expect(ApplicationRecord.gitlab_transactions_stack).to be_empty
+
+ Ci::Pipeline.transaction do
+ expect(ApplicationRecord.gitlab_transactions_stack).to contain_exactly(:gitlab_ci)
+
+ Project.first
+ end
+
+ expect(ApplicationRecord.gitlab_transactions_stack).to be_empty
+
+ ApplicationRecord.transaction do
+ expect(ApplicationRecord.gitlab_transactions_stack).to contain_exactly(:gitlab_main)
+
+ Ci::Pipeline.transaction do
+ expect(ApplicationRecord.gitlab_transactions_stack).to contain_exactly(:gitlab_main, :gitlab_ci)
+
+ Project.first
+ end
+ end
+
+ expect(ApplicationRecord.gitlab_transactions_stack).to be_empty
+ end
+
+ it 'yields' do
+ expect { |block| ApplicationRecord.transaction(&block) }.to yield_control
+ end
+ end
+end
diff --git a/spec/models/concerns/has_environment_scope_spec.rb b/spec/models/concerns/has_environment_scope_spec.rb
index 0cc997709c9..6e8394b6fa5 100644
--- a/spec/models/concerns/has_environment_scope_spec.rb
+++ b/spec/models/concerns/has_environment_scope_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe HasEnvironmentScope do
+ let_it_be(:project) { create(:project) }
+
subject { build(:ci_variable) }
it { is_expected.to allow_value('*').for(:environment_scope) }
@@ -17,8 +19,6 @@ RSpec.describe HasEnvironmentScope do
end
describe '.on_environment' do
- let(:project) { create(:project) }
-
it 'returns scoped objects' do
variable1 = create(:ci_variable, project: project, environment_scope: '*')
variable2 = create(:ci_variable, project: project, environment_scope: 'product/*')
@@ -63,4 +63,32 @@ RSpec.describe HasEnvironmentScope do
end
end
end
+
+ describe '.for_environment' do
+ subject { project.variables.for_environment(environment) }
+
+ let_it_be(:variable1) do
+ create(:ci_variable, project: project, environment_scope: '*')
+ end
+
+ let_it_be(:variable2) do
+ create(:ci_variable, project: project, environment_scope: 'production/*')
+ end
+
+ let_it_be(:variable3) do
+ create(:ci_variable, project: project, environment_scope: 'staging/*')
+ end
+
+ context 'when the environment is present' do
+ let(:environment) { 'production/canary-1' }
+
+ it { is_expected.to eq([variable1, variable2]) }
+ end
+
+ context 'when the environment is nil' do
+ let(:environment) {}
+
+ it { is_expected.to eq([variable1]) }
+ end
+ end
end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index e9c3d1dc646..832d5b44e5d 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -935,6 +935,14 @@ RSpec.describe Issuable do
subject { issuable.supports_escalation? }
it { is_expected.to eq(supports_escalation) }
+
+ context 'with feature disabled' do
+ before do
+ stub_feature_flags(incident_escalations: false)
+ end
+
+ it { is_expected.to eq(false) }
+ end
end
end
diff --git a/spec/models/concerns/resolvable_discussion_spec.rb b/spec/models/concerns/resolvable_discussion_spec.rb
index fc154738f11..7e08f47fb5a 100644
--- a/spec/models/concerns/resolvable_discussion_spec.rb
+++ b/spec/models/concerns/resolvable_discussion_spec.rb
@@ -584,4 +584,14 @@ RSpec.describe Discussion, ResolvableDiscussion do
expect(subject.last_resolved_note).to eq(second_note)
end
end
+
+ describe '#clear_memoized_values' do
+ it 'resets the memoized values' do
+ described_class.memoized_values.each do |memo|
+ subject.instance_variable_set("@#{memo}", 'memoized')
+ expect { subject.clear_memoized_values }.to change { subject.instance_variable_get("@#{memo}") }
+ .from('memoized').to(nil)
+ end
+ end
+ end
end
diff --git a/spec/models/concerns/taskable_spec.rb b/spec/models/concerns/taskable_spec.rb
new file mode 100644
index 00000000000..6b41174a046
--- /dev/null
+++ b/spec/models/concerns/taskable_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Taskable do
+ using RSpec::Parameterized::TableSyntax
+
+ describe '.get_tasks' do
+ let(:description) do
+ <<~MARKDOWN
+ Any text before the list
+ - [ ] First item
+ - [x] Second item
+ * [x] First item
+ * [ ] Second item
+ MARKDOWN
+ end
+
+ let(:expected_result) do
+ [
+ TaskList::Item.new('- [ ]', 'First item'),
+ TaskList::Item.new('- [x]', 'Second item'),
+ TaskList::Item.new('* [x]', 'First item'),
+ TaskList::Item.new('* [ ]', 'Second item')
+ ]
+ end
+
+ subject { described_class.get_tasks(description) }
+
+ it { is_expected.to match(expected_result) }
+ end
+
+ describe '#task_list_items' do
+ where(issuable_type: [:issue, :merge_request])
+
+ with_them do
+ let(:issuable) { build(issuable_type, description: description) }
+
+ subject(:result) { issuable.task_list_items }
+
+ context 'when description is present' do
+ let(:description) { 'markdown' }
+
+ it 'gets tasks from markdown' do
+ expect(described_class).to receive(:get_tasks)
+
+ result
+ end
+ end
+
+ context 'when description is blank' do
+ let(:description) { '' }
+
+ it 'returns empty array' do
+ expect(result).to be_empty
+ end
+
+ it 'does not try to get tasks from markdown' do
+ expect(described_class).not_to receive(:get_tasks)
+
+ result
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb
index 4bdb3e0a32a..2e82a12a61a 100644
--- a/spec/models/concerns/token_authenticatable_spec.rb
+++ b/spec/models/concerns/token_authenticatable_spec.rb
@@ -289,4 +289,142 @@ RSpec.describe Ci::Build, 'TokenAuthenticatable' do
expect(build.read_attribute('token')).to be_nil
end
end
+
+ describe '#token_with_expiration' do
+ describe '#expirable?' do
+ subject { build.token_with_expiration.expirable? }
+
+ it { is_expected.to eq(false) }
+ end
+ end
+end
+
+RSpec.describe Ci::Runner, 'TokenAuthenticatable', :freeze_time do
+ let_it_be(:non_expirable_runner) { create(:ci_runner) }
+ let_it_be(:non_expired_runner) { create(:ci_runner).tap { |r| r.update!(token_expires_at: 5.seconds.from_now) } }
+ let_it_be(:expired_runner) { create(:ci_runner).tap { |r| r.update!(token_expires_at: 5.seconds.ago) } }
+
+ describe '#token_expired?' do
+ subject { runner.token_expired? }
+
+ context 'when enforce_runner_token_expires_at feature flag is disabled' do
+ before do
+ stub_feature_flags(enforce_runner_token_expires_at: false)
+ end
+
+ context 'when runner has no token expiration' do
+ let(:runner) { non_expirable_runner }
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when runner token is not expired' do
+ let(:runner) { non_expired_runner }
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when runner token is expired' do
+ let(:runner) { expired_runner }
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
+ context 'when enforce_runner_token_expires_at feature flag is enabled' do
+ before do
+ stub_feature_flags(enforce_runner_token_expires_at: true)
+ end
+
+ context 'when runner has no token expiration' do
+ let(:runner) { non_expirable_runner }
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when runner token is not expired' do
+ let(:runner) { non_expired_runner }
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when runner token is expired' do
+ let(:runner) { expired_runner }
+
+ it { is_expected.to eq(true) }
+ end
+ end
+ end
+
+ describe '#token_with_expiration' do
+ describe '#token' do
+ subject { non_expired_runner.token_with_expiration.token }
+
+ it { is_expected.to eq(non_expired_runner.token) }
+ end
+
+ describe '#token_expires_at' do
+ subject { non_expired_runner.token_with_expiration.token_expires_at }
+
+ it { is_expected.to eq(non_expired_runner.token_expires_at) }
+ end
+
+ describe '#expirable?' do
+ subject { non_expired_runner.token_with_expiration.expirable? }
+
+ it { is_expected.to eq(true) }
+ end
+ end
+
+ describe '.find_by_token' do
+ subject { Ci::Runner.find_by_token(runner.token) }
+
+ context 'when enforce_runner_token_expires_at feature flag is disabled' do
+ before do
+ stub_feature_flags(enforce_runner_token_expires_at: false)
+ end
+
+ context 'when runner has no token expiration' do
+ let(:runner) { non_expirable_runner }
+
+ it { is_expected.to eq(non_expirable_runner) }
+ end
+
+ context 'when runner token is not expired' do
+ let(:runner) { non_expired_runner }
+
+ it { is_expected.to eq(non_expired_runner) }
+ end
+
+ context 'when runner token is expired' do
+ let(:runner) { expired_runner }
+
+ it { is_expected.to eq(expired_runner) }
+ end
+ end
+
+ context 'when enforce_runner_token_expires_at feature flag is enabled' do
+ before do
+ stub_feature_flags(enforce_runner_token_expires_at: true)
+ end
+
+ context 'when runner has no token expiration' do
+ let(:runner) { non_expirable_runner }
+
+ it { is_expected.to eq(non_expirable_runner) }
+ end
+
+ context 'when runner token is not expired' do
+ let(:runner) { non_expired_runner }
+
+ it { is_expected.to eq(non_expired_runner) }
+ end
+
+ context 'when runner token is expired' do
+ let(:runner) { expired_runner }
+
+ it { is_expected.to be_nil }
+ end
+ end
+ end
end
diff --git a/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb b/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb
index b311e302a31..1772fd0ff95 100644
--- a/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb
+++ b/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb
@@ -23,6 +23,8 @@ RSpec.describe TokenAuthenticatableStrategies::Encrypted do
let(:options) { { encrypted: :required } }
it 'finds the encrypted resource by cleartext' do
+ allow(model).to receive(:where)
+ .and_return(model)
allow(model).to receive(:find_by)
.with('some_field_encrypted' => [encrypted, encrypted_with_static_iv])
.and_return('encrypted resource')
@@ -36,6 +38,8 @@ RSpec.describe TokenAuthenticatableStrategies::Encrypted do
let(:options) { { encrypted: :optional } }
it 'finds the encrypted resource by cleartext' do
+ allow(model).to receive(:where)
+ .and_return(model)
allow(model).to receive(:find_by)
.with('some_field_encrypted' => [encrypted, encrypted_with_static_iv])
.and_return('encrypted resource')
@@ -49,6 +53,8 @@ RSpec.describe TokenAuthenticatableStrategies::Encrypted do
.to receive(:find_token_authenticatable)
.and_return('plaintext resource')
+ allow(model).to receive(:where)
+ .and_return(model)
allow(model).to receive(:find_by)
.with('some_field_encrypted' => [encrypted, encrypted_with_static_iv])
.and_return(nil)
@@ -62,6 +68,8 @@ RSpec.describe TokenAuthenticatableStrategies::Encrypted do
let(:options) { { encrypted: :migrating } }
it 'finds the cleartext resource by cleartext' do
+ allow(model).to receive(:where)
+ .and_return(model)
allow(model).to receive(:find_by)
.with('some_field' => 'my-value')
.and_return('cleartext resource')
@@ -71,6 +79,8 @@ RSpec.describe TokenAuthenticatableStrategies::Encrypted do
end
it 'returns nil if resource cannot be found' do
+ allow(model).to receive(:where)
+ .and_return(model)
allow(model).to receive(:find_by)
.with('some_field' => 'my-value')
.and_return(nil)
diff --git a/spec/models/container_repository_spec.rb b/spec/models/container_repository_spec.rb
index 8f7c13d7ae6..7c0ae51223b 100644
--- a/spec/models/container_repository_spec.rb
+++ b/spec/models/container_repository_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ContainerRepository do
+RSpec.describe ContainerRepository, :aggregate_failures do
using RSpec::Parameterized::TableSyntax
let(:group) { create(:group, name: 'group') }
@@ -36,7 +36,467 @@ RSpec.describe ContainerRepository do
describe 'validations' do
it { is_expected.to validate_presence_of(:migration_retries_count) }
it { is_expected.to validate_numericality_of(:migration_retries_count).is_greater_than_or_equal_to(0) }
- it { is_expected.to validate_presence_of(:migration_state) }
+
+ it { is_expected.to validate_inclusion_of(:migration_aborted_in_state).in_array(described_class::ABORTABLE_MIGRATION_STATES) }
+ it { is_expected.to allow_value(nil).for(:migration_aborted_in_state) }
+
+ context 'migration_state' do
+ it { is_expected.to validate_presence_of(:migration_state) }
+ it { is_expected.to validate_inclusion_of(:migration_state).in_array(described_class::MIGRATION_STATES) }
+
+ describe 'pre_importing' do
+ it 'validates expected attributes' do
+ expect(build(:container_repository, migration_state: 'pre_importing')).to be_invalid
+ expect(build(:container_repository, :pre_importing)).to be_valid
+ end
+ end
+
+ describe 'pre_import_done' do
+ it 'validates expected attributes' do
+ expect(build(:container_repository, migration_state: 'pre_import_done')).to be_invalid
+ expect(build(:container_repository, :pre_import_done)).to be_valid
+ end
+ end
+
+ describe 'importing' do
+ it 'validates expected attributes' do
+ expect(build(:container_repository, migration_state: 'importing')).to be_invalid
+ expect(build(:container_repository, :importing)).to be_valid
+ end
+ end
+
+ describe 'import_skipped' do
+ it 'validates expected attributes' do
+ expect(build(:container_repository, migration_state: 'import_skipped')).to be_invalid
+ expect(build(:container_repository, :import_skipped)).to be_valid
+ end
+ end
+
+ describe 'import_aborted' do
+ it 'validates expected attributes' do
+ expect(build(:container_repository, migration_state: 'import_aborted')).to be_invalid
+ expect(build(:container_repository, :import_aborted)).to be_valid
+ end
+ end
+ end
+ end
+
+ context ':migration_state state_machine' do
+ shared_examples 'no action when feature flag is disabled' do
+ context 'feature flag disabled' do
+ before do
+ stub_feature_flags(container_registry_migration_phase2_enabled: false)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
+ shared_examples 'transitioning to pre_importing', skip_pre_import_success: true do
+ before do
+ repository.update_column(:migration_pre_import_done_at, Time.zone.now)
+ end
+
+ it_behaves_like 'no action when feature flag is disabled'
+
+ context 'successful pre_import request' do
+ it 'sets migration_pre_import_started_at and resets migration_pre_import_done_at' do
+ expect(repository).to receive(:migration_pre_import).and_return(:ok)
+
+ expect { subject }.to change { repository.reload.migration_pre_import_started_at }
+ .and change { repository.migration_pre_import_done_at }.to(nil)
+
+ expect(repository).to be_pre_importing
+ end
+ end
+
+ context 'failed pre_import request' do
+ it 'sets migration_pre_import_started_at and resets migration_pre_import_done_at' do
+ expect(repository).to receive(:migration_pre_import).and_return(:error)
+
+ expect { subject }.to change { repository.reload.migration_pre_import_started_at }
+ .and change { repository.migration_aborted_at }
+ .and change { repository.migration_pre_import_done_at }.to(nil)
+
+ expect(repository.migration_aborted_in_state).to eq('pre_importing')
+ expect(repository).to be_import_aborted
+ end
+ end
+ end
+
+ shared_examples 'transitioning to importing', skip_import_success: true do
+ before do
+ repository.update_columns(migration_import_done_at: Time.zone.now)
+ end
+
+ context 'successful import request' do
+ it 'sets migration_import_started_at and resets migration_import_done_at' do
+ expect(repository).to receive(:migration_import).and_return(:ok)
+
+ expect { subject }.to change { repository.reload.migration_import_started_at }
+ .and change { repository.migration_import_done_at }.to(nil)
+
+ expect(repository).to be_importing
+ end
+ end
+
+ context 'failed import request' do
+ it 'sets migration_import_started_at and resets migration_import_done_at' do
+ expect(repository).to receive(:migration_import).and_return(:error)
+
+ expect { subject }.to change { repository.reload.migration_import_started_at }
+ .and change { repository.migration_aborted_at }
+
+ expect(repository.migration_aborted_in_state).to eq('importing')
+ expect(repository).to be_import_aborted
+ end
+ end
+ end
+
+ shared_examples 'transitioning out of import_aborted' do
+ it 'resets migration_aborted_at and migration_aborted_in_state' do
+ expect { subject }.to change { repository.reload.migration_aborted_in_state }.to(nil)
+ .and change { repository.migration_aborted_at }.to(nil)
+ end
+ end
+
+ shared_examples 'transitioning from allowed states' do |allowed_states|
+ described_class::MIGRATION_STATES.each do |state|
+ result = allowed_states.include?(state)
+
+ context "when transitioning from #{state}" do
+ let(:repository) { create(:container_repository, state.to_sym) }
+
+ it "returns #{result}" do
+ expect(subject).to eq(result)
+ end
+ end
+ end
+ end
+
+ shared_examples 'queueing the next import' do
+ it 'starts the worker' do
+ expect(::ContainerRegistry::Migration::EnqueuerWorker).to receive(:perform_async)
+
+ subject
+ end
+ end
+
+ describe '#start_pre_import' do
+ let_it_be_with_reload(:repository) { create(:container_repository) }
+
+ subject { repository.start_pre_import }
+
+ before do |example|
+ unless example.metadata[:skip_pre_import_success]
+ allow(repository).to receive(:migration_pre_import).and_return(:ok)
+ end
+ end
+
+ it_behaves_like 'transitioning from allowed states', %w[default]
+ it_behaves_like 'transitioning to pre_importing'
+ end
+
+ describe '#retry_pre_import' do
+ let_it_be_with_reload(:repository) { create(:container_repository, :import_aborted) }
+
+ subject { repository.retry_pre_import }
+
+ before do |example|
+ unless example.metadata[:skip_pre_import_success]
+ allow(repository).to receive(:migration_pre_import).and_return(:ok)
+ end
+ end
+
+ it_behaves_like 'transitioning from allowed states', %w[import_aborted]
+ it_behaves_like 'transitioning to pre_importing'
+ it_behaves_like 'transitioning out of import_aborted'
+ end
+
+ describe '#finish_pre_import' do
+ let_it_be_with_reload(:repository) { create(:container_repository, :pre_importing) }
+
+ subject { repository.finish_pre_import }
+
+ it_behaves_like 'transitioning from allowed states', %w[pre_importing import_aborted]
+
+ it 'sets migration_pre_import_done_at' do
+ expect { subject }.to change { repository.reload.migration_pre_import_done_at }
+
+ expect(repository).to be_pre_import_done
+ end
+ end
+
+ describe '#start_import' do
+ let_it_be_with_reload(:repository) { create(:container_repository, :pre_import_done) }
+
+ subject { repository.start_import }
+
+ before do |example|
+ unless example.metadata[:skip_import_success]
+ allow(repository).to receive(:migration_import).and_return(:ok)
+ end
+ end
+
+ it_behaves_like 'transitioning from allowed states', %w[pre_import_done]
+ it_behaves_like 'transitioning to importing'
+ end
+
+ describe '#retry_import' do
+ let_it_be_with_reload(:repository) { create(:container_repository, :import_aborted) }
+
+ subject { repository.retry_import }
+
+ before do |example|
+ unless example.metadata[:skip_import_success]
+ allow(repository).to receive(:migration_import).and_return(:ok)
+ end
+ end
+
+ it_behaves_like 'transitioning from allowed states', %w[import_aborted]
+ it_behaves_like 'transitioning to importing'
+ it_behaves_like 'no action when feature flag is disabled'
+ end
+
+ describe '#finish_import' do
+ let_it_be_with_reload(:repository) { create(:container_repository, :importing) }
+
+ subject { repository.finish_import }
+
+ it_behaves_like 'transitioning from allowed states', %w[importing import_aborted]
+ it_behaves_like 'queueing the next import'
+
+ it 'sets migration_import_done_at and queues the next import' do
+ expect { subject }.to change { repository.reload.migration_import_done_at }
+
+ expect(repository).to be_import_done
+ end
+ end
+
+ describe '#already_migrated' do
+ let_it_be_with_reload(:repository) { create(:container_repository) }
+
+ subject { repository.already_migrated }
+
+ it_behaves_like 'transitioning from allowed states', %w[default]
+
+ it 'sets migration_import_done_at' do
+ subject
+
+ expect(repository).to be_import_done
+ end
+ end
+
+ describe '#abort_import' do
+ let_it_be_with_reload(:repository) { create(:container_repository, :importing) }
+
+ subject { repository.abort_import }
+
+ it_behaves_like 'transitioning from allowed states', ContainerRepository::ABORTABLE_MIGRATION_STATES
+ it_behaves_like 'queueing the next import'
+
+ it 'sets migration_aborted_at and migration_aborted_at, increments the retry count, and queues the next import' do
+ expect { subject }.to change { repository.migration_aborted_at }
+ .and change { repository.reload.migration_retries_count }.by(1)
+
+ expect(repository.migration_aborted_in_state).to eq('importing')
+ expect(repository).to be_import_aborted
+ end
+ end
+
+ describe '#skip_import' do
+ let_it_be_with_reload(:repository) { create(:container_repository) }
+
+ subject { repository.skip_import(reason: :too_many_retries) }
+
+ it_behaves_like 'transitioning from allowed states', ContainerRepository::ABORTABLE_MIGRATION_STATES
+
+ it 'sets migration_skipped_at and migration_skipped_reason' do
+ expect { subject }.to change { repository.reload.migration_skipped_at }
+
+ expect(repository.migration_skipped_reason).to eq('too_many_retries')
+ expect(repository).to be_import_skipped
+ end
+
+ it 'raises and error if a reason is not given' do
+ expect { repository.skip_import }.to raise_error(ArgumentError)
+ end
+ end
+
+ describe '#finish_pre_import_and_start_import' do
+ let_it_be_with_reload(:repository) { create(:container_repository, :pre_importing) }
+
+ subject { repository.finish_pre_import_and_start_import }
+
+ before do |example|
+ unless example.metadata[:skip_import_success]
+ allow(repository).to receive(:migration_import).and_return(:ok)
+ end
+ end
+
+ it_behaves_like 'transitioning from allowed states', %w[pre_importing import_aborted]
+ it_behaves_like 'transitioning to importing'
+ end
+ end
+
+ context 'when triggering registry API requests' do
+ let(:repository_state) { nil }
+ let(:repository) { create(:container_repository, repository_state) }
+
+ shared_examples 'a state machine configured with use_transactions: false' do
+ it 'executes the registry API request outside of a transaction', :delete do
+ expect(repository).to receive(:save).and_call_original do
+ expect(ApplicationRecord.connection.transaction_open?).to be true
+ end
+
+ expect(repository).to receive(:try_import) do
+ expect(ApplicationRecord.connection.transaction_open?).to be false
+ end
+
+ subject
+ end
+ end
+
+ context 'when responding to a start_pre_import event' do
+ subject { repository.start_pre_import }
+
+ it_behaves_like 'a state machine configured with use_transactions: false'
+ end
+
+ context 'when responding to a retry_pre_import event' do
+ let(:repository_state) { :import_aborted }
+
+ subject { repository.retry_pre_import }
+
+ it_behaves_like 'a state machine configured with use_transactions: false'
+ end
+
+ context 'when responding to a start_import event' do
+ let(:repository_state) { :pre_import_done }
+
+ subject { repository.start_import }
+
+ it_behaves_like 'a state machine configured with use_transactions: false'
+ end
+
+ context 'when responding to a retry_import event' do
+ let(:repository_state) { :import_aborted }
+
+ subject { repository.retry_import }
+
+ it_behaves_like 'a state machine configured with use_transactions: false'
+ end
+ end
+
+ describe '#retry_aborted_migration' do
+ subject { repository.retry_aborted_migration }
+
+ shared_examples 'no action' do
+ it 'does nothing' do
+ expect { subject }.not_to change { repository.reload.migration_state }
+
+ expect(subject).to eq(nil)
+ end
+ end
+
+ shared_examples 'retrying the pre_import' do
+ it 'retries the pre_import' do
+ expect(repository).to receive(:migration_pre_import).and_return(:ok)
+
+ expect { subject }.to change { repository.reload.migration_state }.to('pre_importing')
+ end
+ end
+
+ shared_examples 'retrying the import' do
+ it 'retries the import' do
+ expect(repository).to receive(:migration_import).and_return(:ok)
+
+ expect { subject }.to change { repository.reload.migration_state }.to('importing')
+ end
+ end
+
+ context 'when migration_state is not aborted' do
+ it_behaves_like 'no action'
+ end
+
+ context 'when migration_state is aborted' do
+ before do
+ repository.abort_import
+
+ allow(repository.gitlab_api_client)
+ .to receive(:import_status).with(repository.path).and_return(client_response)
+ end
+
+ context 'native response' do
+ let(:client_response) { 'native' }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(described_class::NativeImportError)
+ end
+ end
+
+ context 'import_in_progress response' do
+ let(:client_response) { 'import_in_progress' }
+
+ it_behaves_like 'no action'
+ end
+
+ context 'import_complete response' do
+ let(:client_response) { 'import_complete' }
+
+ it 'finishes the import' do
+ expect { subject }.to change { repository.reload.migration_state }.to('import_done')
+ end
+ end
+
+ context 'import_failed response' do
+ let(:client_response) { 'import_failed' }
+
+ it_behaves_like 'retrying the import'
+ end
+
+ context 'pre_import_in_progress response' do
+ let(:client_response) { 'pre_import_in_progress' }
+
+ it_behaves_like 'no action'
+ end
+
+ context 'pre_import_complete response' do
+ let(:client_response) { 'pre_import_complete' }
+
+ it 'finishes the pre_import and starts the import' do
+ expect(repository).to receive(:finish_pre_import).and_call_original
+ expect(repository).to receive(:migration_import).and_return(:ok)
+
+ expect { subject }.to change { repository.reload.migration_state }.to('importing')
+ end
+ end
+
+ context 'pre_import_failed response' do
+ let(:client_response) { 'pre_import_failed' }
+
+ it_behaves_like 'retrying the pre_import'
+ end
+
+ context 'error response' do
+ let(:client_response) { 'error' }
+
+ context 'migration_pre_import_done_at is NULL' do
+ it_behaves_like 'retrying the pre_import'
+ end
+
+ context 'migration_pre_import_done_at is not NULL' do
+ before do
+ repository.update_columns(
+ migration_pre_import_started_at: 5.minutes.ago,
+ migration_pre_import_done_at: Time.zone.now
+ )
+ end
+
+ it_behaves_like 'retrying the import'
+ end
+ end
+ end
end
describe '#tag' do
@@ -209,6 +669,54 @@ RSpec.describe ContainerRepository do
end
end
+ context 'registry migration' do
+ shared_examples 'handling the migration step' do |step|
+ let(:client_response) { :foobar }
+
+ before do
+ allow(repository.gitlab_api_client).to receive(:supports_gitlab_api?).and_return(true)
+ end
+
+ it 'returns the same response as the client' do
+ expect(repository.gitlab_api_client)
+ .to receive(step).with(repository.path).and_return(client_response)
+ expect(subject).to eq(client_response)
+ end
+
+ context 'when the gitlab_api feature is not supported' do
+ before do
+ allow(repository.gitlab_api_client).to receive(:supports_gitlab_api?).and_return(false)
+ end
+
+ it 'returns :error' do
+ expect(repository.gitlab_api_client).not_to receive(step)
+
+ expect(subject).to eq(:error)
+ end
+ end
+
+ context 'too many imports' do
+ it 'raises an error when it receives too_many_imports as a response' do
+ expect(repository.gitlab_api_client)
+ .to receive(step).with(repository.path).and_return(:too_many_imports)
+ expect { subject }.to raise_error(described_class::TooManyImportsError)
+ end
+ end
+ end
+
+ describe '#migration_pre_import' do
+ subject { repository.migration_pre_import }
+
+ it_behaves_like 'handling the migration step', :pre_import_repository
+ end
+
+ describe '#migration_import' do
+ subject { repository.migration_import }
+
+ it_behaves_like 'handling the migration step', :import_repository
+ end
+ end
+
describe '.build_from_path' do
let(:registry_path) do
ContainerRegistry::Path.new(project.full_path + '/some/image')
@@ -304,7 +812,7 @@ RSpec.describe ContainerRepository do
let(:path) { ContainerRegistry::Path.new(project.full_path + '/some/image') }
it 'does not throw validation errors and only creates one repository' do
- expect { repository_creation_race(path) }.to change { ContainerRepository.count }.by(1)
+ expect { repository_creation_race(path) }.to change { described_class.count }.by(1)
end
it 'retrieves a persisted repository for all concurrent calls' do
@@ -322,7 +830,7 @@ RSpec.describe ContainerRepository do
Thread.new do
true while wait_for_it
- ::ContainerRepository.find_or_create_from_path(path)
+ described_class.find_or_create_from_path(path)
end
end
wait_for_it = false
@@ -330,6 +838,52 @@ RSpec.describe ContainerRepository do
end
end
+ describe '.find_by_path' do
+ let_it_be(:container_repository) { create(:container_repository) }
+ let_it_be(:repository_path) { container_repository.project.full_path }
+
+ let(:path) { ContainerRegistry::Path.new(repository_path + '/' + container_repository.name) }
+
+ subject { described_class.find_by_path(path) }
+
+ context 'when repository exists' do
+ it 'finds the repository' do
+ expect(subject).to eq(container_repository)
+ end
+ end
+
+ context 'when repository does not exist' do
+ let(:path) { ContainerRegistry::Path.new(repository_path + '/some/image') }
+
+ it 'returns nil' do
+ expect(subject).to be_nil
+ end
+ end
+ end
+
+ describe '.find_by_path!' do
+ let_it_be(:container_repository) { create(:container_repository) }
+ let_it_be(:repository_path) { container_repository.project.full_path }
+
+ let(:path) { ContainerRegistry::Path.new(repository_path + '/' + container_repository.name) }
+
+ subject { described_class.find_by_path!(path) }
+
+ context 'when repository exists' do
+ it 'finds the repository' do
+ expect(subject).to eq(container_repository)
+ end
+ end
+
+ context 'when repository does not exist' do
+ let(:path) { ContainerRegistry::Path.new(repository_path + '/some/image') }
+
+ it 'raises an exception' do
+ expect { subject }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+ end
+
describe '.build_root_repository' do
let(:repository) do
described_class.build_root_repository(project)
@@ -412,6 +966,36 @@ RSpec.describe ContainerRepository do
it { is_expected.to contain_exactly(repository1, repository2, repository4) }
end
+ describe '.with_migration_import_started_at_nil_or_before' do
+ let_it_be(:repository1) { create(:container_repository, migration_import_started_at: 5.minutes.ago) }
+ let_it_be(:repository2) { create(:container_repository, migration_import_started_at: nil) }
+ let_it_be(:repository3) { create(:container_repository, migration_import_started_at: 10.minutes.ago) }
+
+ subject { described_class.with_migration_import_started_at_nil_or_before(7.minutes.ago) }
+
+ it { is_expected.to contain_exactly(repository2, repository3) }
+ end
+
+ describe '.with_migration_pre_import_started_at_nil_or_before' do
+ let_it_be(:repository1) { create(:container_repository, migration_pre_import_started_at: 5.minutes.ago) }
+ let_it_be(:repository2) { create(:container_repository, migration_pre_import_started_at: nil) }
+ let_it_be(:repository3) { create(:container_repository, migration_pre_import_started_at: 10.minutes.ago) }
+
+ subject { described_class.with_migration_pre_import_started_at_nil_or_before(7.minutes.ago) }
+
+ it { is_expected.to contain_exactly(repository2, repository3) }
+ end
+
+ describe '.with_migration_pre_import_done_at_nil_or_before' do
+ let_it_be(:repository1) { create(:container_repository, migration_pre_import_done_at: 5.minutes.ago) }
+ let_it_be(:repository2) { create(:container_repository, migration_pre_import_done_at: nil) }
+ let_it_be(:repository3) { create(:container_repository, migration_pre_import_done_at: 10.minutes.ago) }
+
+ subject { described_class.with_migration_pre_import_done_at_nil_or_before(7.minutes.ago) }
+
+ it { is_expected.to contain_exactly(repository2, repository3) }
+ end
+
describe '.with_stale_ongoing_cleanup' do
let_it_be(:repository1) { create(:container_repository, :cleanup_ongoing, expiration_policy_started_at: 1.day.ago) }
let_it_be(:repository2) { create(:container_repository, :cleanup_ongoing, expiration_policy_started_at: 25.minutes.ago) }
@@ -458,6 +1042,97 @@ RSpec.describe ContainerRepository do
it { is_expected.to eq([repository]) }
end
+ describe '#migration_in_active_state?' do
+ subject { container_repository.migration_in_active_state? }
+
+ described_class::MIGRATION_STATES.each do |state|
+ context "when in #{state} migration_state" do
+ let(:container_repository) { create(:container_repository, state.to_sym)}
+
+ it { is_expected.to eq(state == 'importing' || state == 'pre_importing') }
+ end
+ end
+ end
+
+ describe '#migration_importing?' do
+ subject { container_repository.migration_importing? }
+
+ described_class::MIGRATION_STATES.each do |state|
+ context "when in #{state} migration_state" do
+ let(:container_repository) { create(:container_repository, state.to_sym)}
+
+ it { is_expected.to eq(state == 'importing') }
+ end
+ end
+ end
+
+ describe '#migration_pre_importing?' do
+ subject { container_repository.migration_pre_importing? }
+
+ described_class::MIGRATION_STATES.each do |state|
+ context "when in #{state} migration_state" do
+ let(:container_repository) { create(:container_repository, state.to_sym)}
+
+ it { is_expected.to eq(state == 'pre_importing') }
+ end
+ end
+ end
+
+ describe '#try_import' do
+ let_it_be_with_reload(:container_repository) { create(:container_repository) }
+
+ let(:response) { nil }
+
+ subject do
+ container_repository.try_import do
+ container_repository.foo
+ end
+ end
+
+ before do
+ allow(container_repository).to receive(:foo).and_return(response)
+ end
+
+ context 'successful request' do
+ let(:response) { :ok }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'TooManyImportsError' do
+ before do
+ stub_application_setting(container_registry_import_start_max_retries: 3)
+ allow(container_repository).to receive(:foo).and_raise(described_class::TooManyImportsError)
+ end
+
+ it 'tries again exponentially and aborts the migration' do
+ expect(container_repository).to receive(:sleep).with(a_value_within(0.01).of(0.1))
+ expect(container_repository).to receive(:sleep).with(a_value_within(0.01).of(0.2))
+ expect(container_repository).to receive(:sleep).with(a_value_within(0.01).of(0.3))
+
+ expect(subject).to eq(false)
+
+ expect(container_repository).to be_import_aborted
+ end
+ end
+
+ context 'other response' do
+ let(:response) { :error }
+
+ it 'aborts the migration' do
+ expect(subject).to eq(false)
+
+ expect(container_repository).to be_import_aborted
+ end
+ end
+
+ context 'with no block given' do
+ it 'raises an error' do
+ expect { container_repository.try_import }.to raise_error(ArgumentError)
+ end
+ end
+ end
+
context 'with repositories' do
let_it_be_with_reload(:repository) { create(:container_repository, :cleanup_unscheduled) }
let_it_be(:other_repository) { create(:container_repository, :cleanup_unscheduled) }
@@ -509,5 +1184,87 @@ RSpec.describe ContainerRepository do
it { is_expected.to eq([repository]) }
end
end
+
+ describe '.recently_done_migration_step' do
+ let_it_be(:import_done_repository) { create(:container_repository, :import_done, migration_pre_import_done_at: 3.days.ago, migration_import_done_at: 2.days.ago) }
+ let_it_be(:import_aborted_repository) { create(:container_repository, :import_aborted, migration_pre_import_done_at: 5.days.ago, migration_aborted_at: 1.day.ago) }
+ let_it_be(:pre_import_done_repository) { create(:container_repository, :pre_import_done, migration_pre_import_done_at: 1.hour.ago) }
+
+ subject { described_class.recently_done_migration_step }
+
+ it 'returns completed imports by done_at date' do
+ expect(subject.to_a).to eq([pre_import_done_repository, import_aborted_repository, import_done_repository])
+ end
+ end
+
+ describe '.ready_for_import' do
+ include_context 'importable repositories'
+
+ subject { described_class.ready_for_import }
+
+ before do
+ stub_application_setting(container_registry_import_target_plan: project.namespace.actual_plan_name)
+ end
+
+ it 'works' do
+ expect(subject).to contain_exactly(valid_container_repository, valid_container_repository2)
+ end
+ end
+
+ describe '#last_import_step_done_at' do
+ let_it_be(:aborted_at) { Time.zone.now - 1.hour }
+ let_it_be(:pre_import_done_at) { Time.zone.now - 2.hours }
+
+ subject { repository.last_import_step_done_at }
+
+ before do
+ repository.update_columns(
+ migration_pre_import_done_at: pre_import_done_at,
+ migration_aborted_at: aborted_at
+ )
+ end
+
+ it { is_expected.to eq(aborted_at) }
+ end
+ end
+
+ describe '#external_import_status' do
+ subject { repository.external_import_status }
+
+ it 'returns the response from the client' do
+ expect(repository.gitlab_api_client).to receive(:import_status).with(repository.path).and_return('test')
+
+ expect(subject).to eq('test')
+ end
+ end
+
+ describe '.with_stale_migration' do
+ let_it_be(:repository) { create(:container_repository) }
+ let_it_be(:stale_pre_importing_old_timestamp) { create(:container_repository, :pre_importing, migration_pre_import_started_at: 10.minutes.ago) }
+ let_it_be(:stale_pre_importing_nil_timestamp) { create(:container_repository, :pre_importing).tap { |r| r.update_column(:migration_pre_import_started_at, nil) } }
+ let_it_be(:stale_pre_importing_recent_timestamp) { create(:container_repository, :pre_importing, migration_pre_import_started_at: 2.minutes.ago) }
+
+ let_it_be(:stale_pre_import_done_old_timestamp) { create(:container_repository, :pre_import_done, migration_pre_import_done_at: 10.minutes.ago) }
+ let_it_be(:stale_pre_import_done_nil_timestamp) { create(:container_repository, :pre_import_done).tap { |r| r.update_column(:migration_pre_import_done_at, nil) } }
+ let_it_be(:stale_pre_import_done_recent_timestamp) { create(:container_repository, :pre_import_done, migration_pre_import_done_at: 2.minutes.ago) }
+
+ let_it_be(:stale_importing_old_timestamp) { create(:container_repository, :importing, migration_import_started_at: 10.minutes.ago) }
+ let_it_be(:stale_importing_nil_timestamp) { create(:container_repository, :importing).tap { |r| r.update_column(:migration_import_started_at, nil) } }
+ let_it_be(:stale_importing_recent_timestamp) { create(:container_repository, :importing, migration_import_started_at: 2.minutes.ago) }
+
+ let(:stale_migrations) do
+ [
+ stale_pre_importing_old_timestamp,
+ stale_pre_importing_nil_timestamp,
+ stale_pre_import_done_old_timestamp,
+ stale_pre_import_done_nil_timestamp,
+ stale_importing_old_timestamp,
+ stale_importing_nil_timestamp
+ ]
+ end
+
+ subject { described_class.with_stale_migration(5.minutes.ago) }
+
+ it { is_expected.to contain_exactly(*stale_migrations) }
end
end
diff --git a/spec/models/customer_relations/contact_spec.rb b/spec/models/customer_relations/contact_spec.rb
index 1225f9d089b..c7b0f1bd3d4 100644
--- a/spec/models/customer_relations/contact_spec.rb
+++ b/spec/models/customer_relations/contact_spec.rb
@@ -26,6 +26,18 @@ RSpec.describe CustomerRelations::Contact, type: :model do
it_behaves_like 'an object with RFC3696 compliant email-formatted attributes', :email
end
+ describe '.reference_prefix' do
+ it { expect(described_class.reference_prefix).to eq('[contact:') }
+ end
+
+ describe '.reference_prefix_quoted' do
+ it { expect(described_class.reference_prefix_quoted).to eq('["contact:') }
+ end
+
+ describe '.reference_postfix' do
+ it { expect(described_class.reference_postfix).to eq(']') }
+ end
+
describe '#unique_email_for_group_hierarchy' do
let_it_be(:parent) { create(:group) }
let_it_be(:group) { create(:group, parent: parent) }
@@ -98,4 +110,31 @@ RSpec.describe CustomerRelations::Contact, type: :model do
expect { described_class.find_ids_by_emails(group, Array(0..too_many_emails)) }.to raise_error(ArgumentError)
end
end
+
+ describe '#self.exists_for_group?' do
+ let(:group) { create(:group) }
+ let(:subgroup) { create(:group, parent: group) }
+
+ context 'with no contacts in group or parent' do
+ it 'returns false' do
+ expect(described_class.exists_for_group?(subgroup)).to be_falsey
+ end
+ end
+
+ context 'with contacts in group' do
+ it 'returns true' do
+ create(:contact, group: subgroup)
+
+ expect(described_class.exists_for_group?(subgroup)).to be_truthy
+ end
+ end
+
+ context 'with contacts in parent' do
+ it 'returns true' do
+ create(:contact, group: group)
+
+ expect(described_class.exists_for_group?(subgroup)).to be_truthy
+ end
+ end
+ end
end
diff --git a/spec/models/customer_relations/issue_contact_spec.rb b/spec/models/customer_relations/issue_contact_spec.rb
index c6373fddbfb..39da0b64ea0 100644
--- a/spec/models/customer_relations/issue_contact_spec.rb
+++ b/spec/models/customer_relations/issue_contact_spec.rb
@@ -80,4 +80,12 @@ RSpec.describe CustomerRelations::IssueContact do
expect { described_class.find_contact_ids_by_emails(issue.id, Array(0..too_many_emails)) }.to raise_error(ArgumentError)
end
end
+
+ describe '.delete_for_project' do
+ let_it_be(:issue_contacts) { create_list(:issue_customer_relations_contact, 3, :for_issue, issue: create(:issue, project: project)) }
+
+ it 'destroys all issue_contacts for project' do
+ expect { described_class.delete_for_project(project.id) }.to change { described_class.count }.by(-3)
+ end
+ end
end
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index 29b37ef7371..47c246d12cc 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -369,38 +369,6 @@ RSpec.describe Deployment do
end
end
- describe '#finished_at' do
- subject { deployment.finished_at }
-
- context 'when deployment status is created' do
- let(:deployment) { create(:deployment) }
-
- it { is_expected.to be_nil }
- end
-
- context 'when deployment status is success' do
- let(:deployment) { create(:deployment, :success) }
-
- it { is_expected.to eq(deployment.read_attribute(:finished_at)) }
- end
-
- context 'when deployment status is success' do
- let(:deployment) { create(:deployment, :success, finished_at: nil) }
-
- before do
- deployment.update_column(:finished_at, nil)
- end
-
- it { is_expected.to eq(deployment.read_attribute(:created_at)) }
- end
-
- context 'when deployment status is running' do
- let(:deployment) { create(:deployment, :running) }
-
- it { is_expected.to be_nil }
- end
- end
-
describe '#deployed_at' do
subject { deployment.deployed_at }
@@ -615,7 +583,7 @@ RSpec.describe Deployment do
it 'returns false' do
commit = project.commit('feature')
- expect(deployment.includes_commit?(commit)).to be false
+ expect(deployment.includes_commit?(commit.id)).to be false
end
end
@@ -623,7 +591,7 @@ RSpec.describe Deployment do
it 'returns true' do
commit = project.commit
- expect(deployment.includes_commit?(commit)).to be true
+ expect(deployment.includes_commit?(commit.id)).to be true
end
end
@@ -632,7 +600,7 @@ RSpec.describe Deployment do
deployment.update!(sha: Gitlab::Git::BLANK_SHA)
commit = project.commit
- expect(deployment.includes_commit?(commit)).to be false
+ expect(deployment.includes_commit?(commit.id)).to be false
end
end
end
diff --git a/spec/models/design_management/design_action_spec.rb b/spec/models/design_management/design_action_spec.rb
index 958b1dd9124..4d60ef77025 100644
--- a/spec/models/design_management/design_action_spec.rb
+++ b/spec/models/design_management/design_action_spec.rb
@@ -46,7 +46,7 @@ RSpec.describe DesignManagement::DesignAction do
describe '#gitaly_action' do
let(:path) { 'some/path/somewhere' }
- let(:design) { OpenStruct.new(full_path: path) }
+ let(:design) { double('path', full_path: path) }
subject { described_class.new(design, action, content) }
@@ -75,7 +75,7 @@ RSpec.describe DesignManagement::DesignAction do
describe '#issue_id' do
let(:issue_id) { :foo }
- let(:design) { OpenStruct.new(issue_id: issue_id) }
+ let(:design) { double('id', issue_id: issue_id) }
subject { described_class.new(design, :delete) }
diff --git a/spec/models/design_management/design_at_version_spec.rb b/spec/models/design_management/design_at_version_spec.rb
index a7cf6a9652b..7f1fe7b1e13 100644
--- a/spec/models/design_management/design_at_version_spec.rb
+++ b/spec/models/design_management/design_at_version_spec.rb
@@ -59,7 +59,7 @@ RSpec.describe DesignManagement::DesignAtVersion do
it 'rejects objects with the same id and the wrong class' do
dav = build_stubbed(:design_at_version)
- expect(dav).not_to eq(OpenStruct.new(id: dav.id))
+ expect(dav).not_to eq(double('id', id: dav.id))
end
it 'expects objects to be of the same type, not subtypes' do
diff --git a/spec/models/draft_note_spec.rb b/spec/models/draft_note_spec.rb
index 580a588ae1d..0f85871fd9e 100644
--- a/spec/models/draft_note_spec.rb
+++ b/spec/models/draft_note_spec.rb
@@ -20,6 +20,28 @@ RSpec.describe DraftNote do
it { is_expected.to delegate_method(:file_identifier_hash).to(:diff_file).allow_nil }
end
+ describe '#line_code' do
+ describe 'stored line_code' do
+ let(:draft_note) { build(:draft_note, merge_request: merge_request, line_code: '1234567890') }
+
+ it 'returns stored line_code' do
+ expect(draft_note.line_code).to eq('1234567890')
+ end
+ end
+
+ describe 'none stored line_code' do
+ let(:draft_note) { build(:draft_note, merge_request: merge_request) }
+
+ before do
+ allow(draft_note).to receive(:find_line_code).and_return('none stored line_code')
+ end
+
+ it 'returns found line_code' do
+ expect(draft_note.line_code).to eq('none stored line_code')
+ end
+ end
+ end
+
describe '#diff_file' do
let(:draft_note) { build(:draft_note, merge_request: merge_request) }
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index 3dd0e01d7b3..112dc93658f 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -412,7 +412,7 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
context 'in the same branch' do
it 'returns true' do
- expect(environment.includes_commit?(RepoHelpers.sample_commit)).to be true
+ expect(environment.includes_commit?(RepoHelpers.sample_commit.id)).to be true
end
end
@@ -422,7 +422,7 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
end
it 'returns false' do
- expect(environment.includes_commit?(RepoHelpers.sample_commit)).to be false
+ expect(environment.includes_commit?(RepoHelpers.sample_commit.id)).to be false
end
end
end
diff --git a/spec/models/environment_status_spec.rb b/spec/models/environment_status_spec.rb
index 1b9b38a0932..1db1171401c 100644
--- a/spec/models/environment_status_spec.rb
+++ b/spec/models/environment_status_spec.rb
@@ -161,7 +161,7 @@ RSpec.describe EnvironmentStatus do
let!(:build) { create(:ci_build, :with_deployment, :deploy_to_production, pipeline: pipeline) }
let(:environment) { build.deployment.environment }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
context 'when environment is created on a forked project', :sidekiq_inline do
let(:project) { create(:project, :repository) }
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index 97854086162..f099015e63e 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -25,20 +25,21 @@ RSpec.describe Event do
expect(instance).to receive(:reset_project_activity)
end
- create_push_event(project, project.owner)
+ create_push_event(project, project.first_owner)
end
end
describe 'after_create :set_last_repository_updated_at' do
context 'with a push event' do
- it 'updates the project last_repository_updated_at' do
- project.update!(last_repository_updated_at: 1.year.ago)
+ it 'updates the project last_repository_updated_at and updated_at' do
+ project.touch(:last_repository_updated_at, time: 1.year.ago) # rubocop: disable Rails/SkipsModelValidations
- create_push_event(project, project.owner)
+ event = create_push_event(project, project.first_owner)
project.reload
- expect(project.last_repository_updated_at).to be_within(1.minute).of(Time.current)
+ expect(project.last_repository_updated_at).to be_like_time(event.created_at)
+ expect(project.updated_at).to be_like_time(event.created_at)
end
end
@@ -46,7 +47,7 @@ RSpec.describe Event do
it 'does not update the project last_repository_updated_at' do
project.update!(last_repository_updated_at: 1.year.ago)
- create(:closed_issue_event, project: project, author: project.owner)
+ create(:closed_issue_event, project: project, author: project.first_owner)
project.reload
@@ -62,14 +63,14 @@ RSpec.describe Event do
project.reload # a reload removes fractions of seconds
expect do
- create_push_event(project, project.owner)
+ create_push_event(project, project.first_owner)
project.reload
end.not_to change { project.last_repository_updated_at }
end
end
describe 'after_create UserInteractedProject.track' do
- let(:event) { build(:push_event, project: project, author: project.owner) }
+ let(:event) { build(:push_event, project: project, author: project.first_owner) }
it 'passes event to UserInteractedProject.track' do
expect(UserInteractedProject).to receive(:track).with(event)
@@ -156,7 +157,7 @@ RSpec.describe Event do
describe "Push event" do
let(:project) { create(:project, :private) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:event) { create_push_event(project, user) }
it do
@@ -172,7 +173,7 @@ RSpec.describe Event do
describe '#target_title' do
let_it_be(:project) { create(:project) }
- let(:author) { project.owner }
+ let(:author) { project.first_owner }
let(:target) { nil }
let(:event) do
@@ -745,7 +746,7 @@ RSpec.describe Event do
target = kind == :project ? nil : build(kind, **extra_data)
- [kind, build(:event, :created, author: project.owner, project: project, target: target)]
+ [kind, build(:event, :created, author: project.first_owner, project: project, target: target)]
end
end
@@ -829,19 +830,20 @@ RSpec.describe Event do
expect(project).not_to receive(:update_column)
.with(:last_activity_at, a_kind_of(Time))
- create_push_event(project, project.owner)
+ create_push_event(project, project.first_owner)
end
end
context 'when a project was updated more than 1 hour ago' do
it 'updates the project' do
- project.update!(last_activity_at: 1.year.ago)
+ project.touch(:last_activity_at, time: 1.year.ago) # rubocop: disable Rails/SkipsModelValidations
- create_push_event(project, project.owner)
+ event = create_push_event(project, project.first_owner)
project.reload
- expect(project.last_activity_at).to be_within(1.minute).of(Time.current)
+ expect(project.last_activity_at).to be_like_time(event.created_at)
+ expect(project.updated_at).to be_like_time(event.created_at)
end
end
end
diff --git a/spec/models/external_pull_request_spec.rb b/spec/models/external_pull_request_spec.rb
index b141600c4fd..82da7cdf34b 100644
--- a/spec/models/external_pull_request_spec.rb
+++ b/spec/models/external_pull_request_spec.rb
@@ -236,4 +236,11 @@ RSpec.describe ExternalPullRequest do
it_behaves_like 'it has loose foreign keys' do
let(:factory_name) { :external_pull_request }
end
+
+ context 'loose foreign key on external_pull_requests.project_id' do
+ it_behaves_like 'cleanup by a loose foreign key' do
+ let!(:parent) { create(:project) }
+ let!(:model) { create(:external_pull_request, project: parent) }
+ end
+ end
end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 05ee2166245..4bc4df02c24 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -1249,7 +1249,7 @@ RSpec.describe Group do
let(:common_id) { [Project.maximum(:id).to_i, Namespace.maximum(:id).to_i].max + 999 }
let!(:group) { create(:group, id: common_id) }
let!(:unrelated_project) { create(:project, id: common_id) }
- let(:user) { unrelated_project.owner }
+ let(:user) { unrelated_project.first_owner }
it 'returns correct access level' do
expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
diff --git a/spec/models/hooks/service_hook_spec.rb b/spec/models/hooks/service_hook_spec.rb
index 85f433f5f81..0d65fe302e1 100644
--- a/spec/models/hooks/service_hook_spec.rb
+++ b/spec/models/hooks/service_hook_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe ServiceHook do
let(:data) { { key: 'value' } }
it '#execute' do
- expect(WebHookService).to receive(:new).with(hook, data, 'service_hook').and_call_original
+ expect(WebHookService).to receive(:new).with(hook, data, 'service_hook', force: false).and_call_original
expect_any_instance_of(WebHookService).to receive(:execute)
hook.execute(data)
diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb
index 89bfb742f5d..a3d36058b74 100644
--- a/spec/models/hooks/system_hook_spec.rb
+++ b/spec/models/hooks/system_hook_spec.rb
@@ -168,17 +168,17 @@ RSpec.describe SystemHook do
let(:data) { { key: 'value' } }
let(:hook_name) { 'system_hook' }
- before do
- expect(WebHookService).to receive(:new).with(hook, data, hook_name).and_call_original
- end
-
it '#execute' do
+ expect(WebHookService).to receive(:new).with(hook, data, hook_name, force: false).and_call_original
+
expect_any_instance_of(WebHookService).to receive(:execute)
hook.execute(data, hook_name)
end
it '#async_execute' do
+ expect(WebHookService).to receive(:new).with(hook, data, hook_name).and_call_original
+
expect_any_instance_of(WebHookService).to receive(:async_execute)
hook.async_execute(data, hook_name)
diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb
index c292e78b32d..482e372543c 100644
--- a/spec/models/hooks/web_hook_spec.rb
+++ b/spec/models/hooks/web_hook_spec.rb
@@ -100,12 +100,18 @@ RSpec.describe WebHook do
hook.execute(data, hook_name)
end
- it 'does not execute non-executable hooks' do
- hook.update!(disabled_until: 1.day.from_now)
+ it 'passes force: false to the web hook service by default' do
+ expect(WebHookService)
+ .to receive(:new).with(hook, data, hook_name, force: false).and_return(double(execute: :done))
- expect(WebHookService).not_to receive(:new)
+ expect(hook.execute(data, hook_name)).to eq :done
+ end
- hook.execute(data, hook_name)
+ it 'passes force: true to the web hook service if required' do
+ expect(WebHookService)
+ .to receive(:new).with(hook, data, hook_name, force: true).and_return(double(execute: :forced))
+
+ expect(hook.execute(data, hook_name, force: true)).to eq :forced
end
it '#async_execute' do
diff --git a/spec/models/instance_configuration_spec.rb b/spec/models/instance_configuration_spec.rb
index a47bc6a5b6d..6b0d8d7ca4a 100644
--- a/spec/models/instance_configuration_spec.rb
+++ b/spec/models/instance_configuration_spec.rb
@@ -206,7 +206,8 @@ RSpec.describe InstanceConfiguration do
group_download_export_limit: 1019,
group_import_limit: 1020,
raw_blob_request_limit: 1021,
- user_email_lookup_limit: 1022
+ user_email_lookup_limit: 1022,
+ users_get_by_id_limit: 1023
)
end
@@ -230,6 +231,7 @@ RSpec.describe InstanceConfiguration do
expect(rate_limits[:group_import]).to eq({ enabled: true, requests_per_period: 1020, period_in_seconds: 60 })
expect(rate_limits[:raw_blob]).to eq({ enabled: true, requests_per_period: 1021, period_in_seconds: 60 })
expect(rate_limits[:user_email_lookup]).to eq({ enabled: true, requests_per_period: 1022, period_in_seconds: 60 })
+ expect(rate_limits[:users_get_by_id]).to eq({ enabled: true, requests_per_period: 1023, period_in_seconds: 600 })
end
end
end
diff --git a/spec/models/instance_metadata_spec.rb b/spec/models/instance_metadata_spec.rb
index e3a9167620b..5fc073c392d 100644
--- a/spec/models/instance_metadata_spec.rb
+++ b/spec/models/instance_metadata_spec.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
require 'fast_spec_helper'
+require_relative '../../app/models/instance_metadata'
+require_relative '../../app/models/instance_metadata/kas'
RSpec.describe InstanceMetadata do
it 'has the correct properties' do
diff --git a/spec/models/integration_spec.rb b/spec/models/integration_spec.rb
index 7bc670302f1..e822620ab80 100644
--- a/spec/models/integration_spec.rb
+++ b/spec/models/integration_spec.rb
@@ -710,30 +710,21 @@ RSpec.describe Integration do
[
{ name: 'token' },
{ name: 'api_token' },
+ { name: 'token_api' },
+ { name: 'safe_token' },
{ name: 'key' },
{ name: 'api_key' },
{ name: 'password' },
{ name: 'password_field' },
+ { name: 'some_safe_field' },
{ name: 'safe_field' }
- ]
+ ].shuffle
end
end
end
- let(:integration) do
- fake_integration.new(properties: [
- { token: 'token-value' },
- { api_token: 'api_token-value' },
- { key: 'key-value' },
- { api_key: 'api_key-value' },
- { password: 'password-value' },
- { password_field: 'password_field-value' },
- { safe_field: 'safe_field-value' }
- ])
- end
-
it 'filters out sensitive fields' do
- expect(integration.api_field_names).to eq(['safe_field'])
+ expect(fake_integration.new).to have_attributes(api_field_names: match_array(%w[some_safe_field safe_field]))
end
end
diff --git a/spec/models/integrations/datadog_spec.rb b/spec/models/integrations/datadog_spec.rb
index 9856c53a390..cfc44b22a84 100644
--- a/spec/models/integrations/datadog_spec.rb
+++ b/spec/models/integrations/datadog_spec.rb
@@ -16,6 +16,7 @@ RSpec.describe Integrations::Datadog do
let(:api_key) { SecureRandom.hex(32) }
let(:dd_env) { 'ci' }
let(:dd_service) { 'awesome-gitlab' }
+ let(:dd_tags) { '' }
let(:expected_hook_url) { default_url + "?dd-api-key=#{api_key}&env=#{dd_env}&service=#{dd_service}" }
@@ -27,7 +28,8 @@ RSpec.describe Integrations::Datadog do
api_url: api_url,
api_key: api_key,
datadog_env: dd_env,
- datadog_service: dd_service
+ datadog_service: dd_service,
+ datadog_tags: dd_tags
)
end
@@ -95,6 +97,20 @@ RSpec.describe Integrations::Datadog do
it { is_expected.not_to allow_value('datadog hq.com').for(:datadog_site) }
it { is_expected.not_to allow_value('example.com').for(:api_url) }
end
+
+ context 'with custom tags' do
+ it { is_expected.to allow_value('').for(:datadog_tags) }
+ it { is_expected.to allow_value('key:value').for(:datadog_tags) }
+ it { is_expected.to allow_value("key:value\nkey2:value2").for(:datadog_tags) }
+ it { is_expected.to allow_value("key:value\nkey2:value with spaces and 123?&$").for(:datadog_tags) }
+ it { is_expected.to allow_value("key:value\n\n\n\nkey2:value2\n").for(:datadog_tags) }
+
+ it { is_expected.not_to allow_value('value').for(:datadog_tags) }
+ it { is_expected.not_to allow_value('key:').for(:datadog_tags) }
+ it { is_expected.not_to allow_value('key: ').for(:datadog_tags) }
+ it { is_expected.not_to allow_value(':value').for(:datadog_tags) }
+ it { is_expected.not_to allow_value("key:value\nINVALID").for(:datadog_tags) }
+ end
end
context 'when integration is not active' do
@@ -134,9 +150,23 @@ RSpec.describe Integrations::Datadog do
context 'without optional params' do
let(:dd_service) { '' }
let(:dd_env) { '' }
+ let(:dd_tags) { '' }
it { is_expected.to eq(default_url + "?dd-api-key=#{api_key}") }
end
+
+ context 'with custom tags' do
+ let(:dd_tags) { "key:value\nkey2:value, 2" }
+ let(:escaped_tags) { CGI.escape("key:value,\"key2:value, 2\"") }
+
+ it { is_expected.to eq(expected_hook_url + "&tags=#{escaped_tags}") }
+
+ context 'and empty lines' do
+ let(:dd_tags) { "key:value\r\n\n\n\nkey2:value, 2\n" }
+
+ it { is_expected.to eq(expected_hook_url + "&tags=#{escaped_tags}") }
+ end
+ end
end
describe '#test' do
diff --git a/spec/models/issue_collection_spec.rb b/spec/models/issue_collection_spec.rb
index d67bd8debce..183082bab26 100644
--- a/spec/models/issue_collection_spec.rb
+++ b/spec/models/issue_collection_spec.rb
@@ -50,7 +50,9 @@ RSpec.describe IssueCollection do
end
end
- context 'using a user that is the owner of a project' do
+ # TODO update when we have multiple owners of a project
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/350605
+ context 'using a user that is an owner of a project' do
it 'returns the issues of the project' do
expect(collection.updatable_by_user(project.namespace.owner))
.to eq([issue1, issue2])
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index c105f6c3439..5af42cc67ea 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -887,6 +887,8 @@ RSpec.describe Issue do
end
end
+ # TODO update when we have multiple owners of a project
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/350605
context 'with an owner' do
before do
project.add_maintainer(user)
@@ -1431,26 +1433,6 @@ RSpec.describe Issue do
end
end
- describe '.with_label_attributes' do
- subject { described_class.with_label_attributes(label_attributes) }
-
- let(:label_attributes) { { title: 'hello world', description: 'hi' } }
-
- it 'gets issues with given label attributes' do
- label = create(:label, **label_attributes)
- labeled_issue = create(:labeled_issue, project: label.project, labels: [label])
-
- expect(subject).to include(labeled_issue)
- end
-
- it 'excludes issues without given label attributes' do
- label = create(:label, title: 'GitLab', description: 'tanuki')
- labeled_issue = create(:labeled_issue, project: label.project, labels: [label])
-
- expect(subject).not_to include(labeled_issue)
- end
- end
-
describe 'banzai_render_context' do
let(:project) { build(:project_empty_repo) }
let(:issue) { build :issue, project: project }
diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb
index 19459561edf..6cf73de6cef 100644
--- a/spec/models/key_spec.rb
+++ b/spec/models/key_spec.rb
@@ -20,6 +20,8 @@ RSpec.describe Key, :mailer do
it { is_expected.to allow_value(attributes_for(:dsa_key_2048)[:key]).for(:key) }
it { is_expected.to allow_value(attributes_for(:ecdsa_key_256)[:key]).for(:key) }
it { is_expected.to allow_value(attributes_for(:ed25519_key_256)[:key]).for(:key) }
+ it { is_expected.to allow_value(attributes_for(:ecdsa_sk_key_256)[:key]).for(:key) }
+ it { is_expected.to allow_value(attributes_for(:ed25519_sk_key_256)[:key]).for(:key) }
it { is_expected.not_to allow_value('foo-bar').for(:key) }
context 'key format' do
@@ -187,10 +189,12 @@ RSpec.describe Key, :mailer do
forbidden = ApplicationSetting::FORBIDDEN_KEY_VALUE
[
- [:rsa_key_2048, 0, true],
- [:dsa_key_2048, 0, true],
- [:ecdsa_key_256, 0, true],
- [:ed25519_key_256, 0, true],
+ [:rsa_key_2048, 0, true],
+ [:dsa_key_2048, 0, true],
+ [:ecdsa_key_256, 0, true],
+ [:ed25519_key_256, 0, true],
+ [:ecdsa_sk_key_256, 0, true],
+ [:ed25519_sk_key_256, 0, true],
[:rsa_key_2048, 1024, true],
[:rsa_key_2048, 2048, true],
@@ -206,10 +210,18 @@ RSpec.describe Key, :mailer do
[:ed25519_key_256, 256, true],
[:ed25519_key_256, 384, false],
- [:rsa_key_2048, forbidden, false],
- [:dsa_key_2048, forbidden, false],
- [:ecdsa_key_256, forbidden, false],
- [:ed25519_key_256, forbidden, false]
+ [:ecdsa_sk_key_256, 256, true],
+ [:ecdsa_sk_key_256, 384, false],
+
+ [:ed25519_sk_key_256, 256, true],
+ [:ed25519_sk_key_256, 384, false],
+
+ [:rsa_key_2048, forbidden, false],
+ [:dsa_key_2048, forbidden, false],
+ [:ecdsa_key_256, forbidden, false],
+ [:ed25519_key_256, forbidden, false],
+ [:ecdsa_sk_key_256, forbidden, false],
+ [:ed25519_sk_key_256, forbidden, false]
]
end
diff --git a/spec/models/label_note_spec.rb b/spec/models/label_note_spec.rb
index ee4822c653d..145ddd44834 100644
--- a/spec/models/label_note_spec.rb
+++ b/spec/models/label_note_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe LabelNote do
+ include Gitlab::Routing.url_helpers
+
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
let_it_be(:label) { create(:label, project: project) }
@@ -14,11 +16,27 @@ RSpec.describe LabelNote do
let_it_be(:resource) { create(:issue, project: project) }
it_behaves_like 'label note created from events'
+
+ it 'includes a link to the list of issues filtered by the label' do
+ note = described_class.from_events([
+ create(:resource_label_event, label: label, issue: resource)
+ ])
+
+ expect(note.note_html).to include(project_issues_path(project, label_name: label.title))
+ end
end
context 'when resource is merge request' do
let_it_be(:resource) { create(:merge_request, source_project: project, target_project: project) }
it_behaves_like 'label note created from events'
+
+ it 'includes a link to the list of merge requests filtered by the label' do
+ note = described_class.from_events([
+ create(:resource_label_event, label: label, merge_request: resource)
+ ])
+
+ expect(note.note_html).to include(project_merge_requests_path(project, label_name: label.title))
+ end
end
end
diff --git a/spec/models/loose_foreign_keys/deleted_record_spec.rb b/spec/models/loose_foreign_keys/deleted_record_spec.rb
index 07ffff746a5..23e0ed1f39d 100644
--- a/spec/models/loose_foreign_keys/deleted_record_spec.rb
+++ b/spec/models/loose_foreign_keys/deleted_record_spec.rb
@@ -6,15 +6,15 @@ RSpec.describe LooseForeignKeys::DeletedRecord, type: :model do
let_it_be(:table) { 'public.projects' }
describe 'class methods' do
- let_it_be(:deleted_record_1) { described_class.create!(fully_qualified_table_name: table, primary_key_value: 5) }
- let_it_be(:deleted_record_2) { described_class.create!(fully_qualified_table_name: table, primary_key_value: 1) }
+ let_it_be(:deleted_record_1) { described_class.create!(fully_qualified_table_name: table, primary_key_value: 5, cleanup_attempts: 2) }
+ let_it_be(:deleted_record_2) { described_class.create!(fully_qualified_table_name: table, primary_key_value: 1, cleanup_attempts: 0) }
let_it_be(:deleted_record_3) { described_class.create!(fully_qualified_table_name: 'public.other_table', primary_key_value: 3) }
- let_it_be(:deleted_record_4) { described_class.create!(fully_qualified_table_name: table, primary_key_value: 1) } # duplicate
+ let_it_be(:deleted_record_4) { described_class.create!(fully_qualified_table_name: table, primary_key_value: 1, cleanup_attempts: 1) } # duplicate
+
+ let(:records) { described_class.load_batch_for_table(table, 10) }
describe '.load_batch_for_table' do
it 'loads records and orders them by creation date' do
- records = described_class.load_batch_for_table(table, 10)
-
expect(records).to eq([deleted_record_1, deleted_record_2, deleted_record_4])
end
@@ -27,13 +27,38 @@ RSpec.describe LooseForeignKeys::DeletedRecord, type: :model do
describe '.mark_records_processed' do
it 'updates all records' do
- records = described_class.load_batch_for_table(table, 10)
described_class.mark_records_processed(records)
expect(described_class.status_pending.count).to eq(1)
expect(described_class.status_processed.count).to eq(3)
end
end
+
+ describe '.reschedule' do
+ it 'reschedules all records' do
+ time = Time.zone.parse('2022-01-01').utc
+ update_count = described_class.reschedule(records, time)
+
+ expect(update_count).to eq(records.size)
+
+ records.each(&:reload)
+
+ expect(records).to all(have_attributes(
+ cleanup_attempts: 0,
+ consume_after: time
+ ))
+ end
+ end
+
+ describe '.increment_attempts' do
+ it 'increaments the cleanup_attempts column' do
+ described_class.increment_attempts(records)
+
+ expect(deleted_record_1.reload.cleanup_attempts).to eq(3)
+ expect(deleted_record_2.reload.cleanup_attempts).to eq(1)
+ expect(deleted_record_4.reload.cleanup_attempts).to eq(2)
+ end
+ end
end
describe 'sliding_list partitioning' do
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index 1957c58ec81..79491edba94 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -173,6 +173,8 @@ RSpec.describe Member do
let_it_be(:group) { create(:group) }
let_it_be(:blocked_pending_approval_user) { create(:user, :blocked_pending_approval ) }
let_it_be(:blocked_pending_approval_project_member) { create(:project_member, :invited, :developer, project: project, invite_email: blocked_pending_approval_user.email) }
+ let_it_be(:awaiting_group_member) { create(:group_member, :awaiting, group: group) }
+ let_it_be(:awaiting_project_member) { create(:project_member, :awaiting, project: project) }
before_all do
@owner_user = create(:user).tap { |u| group.add_owner(u) }
@@ -471,6 +473,8 @@ RSpec.describe Member do
it { is_expected.to include @blocked_maintainer }
it { is_expected.to include @blocked_developer }
it { is_expected.not_to include @member_with_minimal_access }
+ it { is_expected.not_to include awaiting_group_member }
+ it { is_expected.not_to include awaiting_project_member }
end
describe '.connected_to_user' do
@@ -509,6 +513,8 @@ RSpec.describe Member do
it { is_expected.not_to include @invited_member }
it { is_expected.not_to include @requested_member }
it { is_expected.not_to include @member_with_minimal_access }
+ it { is_expected.not_to include awaiting_group_member }
+ it { is_expected.not_to include awaiting_project_member }
end
describe '.distinct_on_user_with_max_access_level' do
@@ -561,6 +567,21 @@ RSpec.describe Member do
end
end
end
+
+ describe '.active_state' do
+ let_it_be(:active_group_member) { create(:group_member, group: group) }
+ let_it_be(:active_project_member) { create(:project_member, project: project) }
+
+ it 'includes members with an active state' do
+ expect(group.members.active_state).to include active_group_member
+ expect(project.members.active_state).to include active_project_member
+ end
+
+ it 'does not include members with an awaiting state' do
+ expect(group.members.active_state).not_to include awaiting_group_member
+ expect(project.members.active_state).not_to include awaiting_project_member
+ end
+ end
end
describe 'Delegate methods' do
@@ -894,4 +915,15 @@ RSpec.describe Member do
end
end
end
+
+ describe '#set_member_namespace_id' do
+ let(:group) { create(:group) }
+ let(:member) { create(:group_member, group: group) }
+
+ describe 'on create' do
+ it 'sets the member_namespace_id' do
+ expect(member.member_namespace_id).to eq group.id
+ end
+ end
+ end
end
diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index 031caefbd43..3923f4161cc 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -257,4 +257,15 @@ RSpec.describe ProjectMember do
it_behaves_like 'calls AuthorizedProjectUpdate::UserRefreshFromReplicaWorker with a delay to update project authorizations'
end
end
+
+ describe '#set_member_namespace_id' do
+ let(:project) { create(:project) }
+ let(:member) { create(:project_member, project: project) }
+
+ context 'on create' do
+ it 'sets the member_namespace_id' do
+ expect(member.member_namespace_id).to eq project.project_namespace_id
+ end
+ end
+ end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 4005a2ec6da..f2f2023a992 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -144,6 +144,20 @@ RSpec.describe MergeRequest, factory_default: :keep do
end
end
+ describe '.attention' do
+ let_it_be(:merge_request5) { create(:merge_request, :unique_branches, assignees: [user2])}
+ let_it_be(:merge_request6) { create(:merge_request, :unique_branches, assignees: [user2])}
+
+ before do
+ assignee = merge_request6.find_assignee(user2)
+ assignee.update!(state: :reviewed)
+ end
+
+ it 'returns MRs that have any attention requests' do
+ expect(described_class.attention(user2)).to eq([merge_request2, merge_request5])
+ end
+ end
+
describe '.drafts' do
it 'returns MRs where draft == true' do
expect(described_class.drafts).to eq([merge_request4])
@@ -585,6 +599,21 @@ RSpec.describe MergeRequest, factory_default: :keep do
expect(merge_requests).to eq([older_mr, newer_mr])
end
end
+
+ context 'title' do
+ let_it_be(:first_mr) { create(:merge_request, :closed, title: 'One') }
+ let_it_be(:second_mr) { create(:merge_request, :closed, title: 'Two') }
+
+ it 'sorts asc' do
+ merge_requests = described_class.sort_by_attribute(:title_asc)
+ expect(merge_requests).to eq([first_mr, second_mr])
+ end
+
+ it 'sorts desc' do
+ merge_requests = described_class.sort_by_attribute(:title_desc)
+ expect(merge_requests).to eq([second_mr, first_mr])
+ end
+ end
end
describe 'time to merge calculations' do
@@ -1354,17 +1383,17 @@ RSpec.describe MergeRequest, factory_default: :keep do
subject { build_stubbed(:merge_request) }
[
- 'WIP:', 'WIP: ', '[WIP]', '[WIP] ', ' [WIP] WIP: [WIP] WIP:',
'draft:', 'Draft: ', '[Draft]', '[DRAFT] '
- ].each do |wip_prefix|
- it "detects the '#{wip_prefix}' prefix" do
- subject.title = "#{wip_prefix}#{subject.title}"
+ ].each do |draft_prefix|
+ it "detects the '#{draft_prefix}' prefix" do
+ subject.title = "#{draft_prefix}#{subject.title}"
expect(subject.work_in_progress?).to eq true
end
end
[
+ 'WIP:', 'WIP: ', '[WIP]', '[WIP] ', ' [WIP] WIP: [WIP] WIP:',
"WIP ", "(WIP)",
"draft", "Draft", "Draft -", "draft - ", "Draft ", "draft "
].each do |draft_prefix|
@@ -1375,10 +1404,10 @@ RSpec.describe MergeRequest, factory_default: :keep do
end
end
- it "detects merge request title just saying 'wip'" do
+ it "doesn't detect merge request title just saying 'wip'" do
subject.title = "wip"
- expect(subject.work_in_progress?).to eq true
+ expect(subject.work_in_progress?).to eq false
end
it "does not detect merge request title just saying 'draft'" do
@@ -1444,29 +1473,30 @@ RSpec.describe MergeRequest, factory_default: :keep do
describe "#wipless_title" do
subject { build_stubbed(:merge_request) }
- [
- 'WIP:', 'WIP: ', '[WIP]', '[WIP] ', '[WIP] WIP: [WIP] WIP:',
- 'draft:', 'Draft: ', '[Draft]', '[DRAFT] '
- ].each do |wip_prefix|
- it "removes the '#{wip_prefix}' prefix" do
+ ['draft:', 'Draft: ', '[Draft]', '[DRAFT] '].each do |draft_prefix|
+ it "removes a '#{draft_prefix}' prefix" do
wipless_title = subject.title
- subject.title = "#{wip_prefix}#{subject.title}"
+ subject.title = "#{draft_prefix}#{subject.title}"
expect(subject.wipless_title).to eq wipless_title
end
it "is satisfies the #work_in_progress? method" do
- subject.title = "#{wip_prefix}#{subject.title}"
+ subject.title = "#{draft_prefix}#{subject.title}"
subject.title = subject.wipless_title
expect(subject.work_in_progress?).to eq false
end
end
- it 'removes only WIP prefix from the MR title' do
- subject.title = 'WIP: Implement feature called WIP'
+ [
+ 'WIP:', 'WIP: ', '[WIP]', '[WIP] ', '[WIP] WIP: [WIP] WIP:'
+ ].each do |wip_prefix|
+ it "doesn't remove a '#{wip_prefix}' prefix" do
+ subject.title = "#{wip_prefix}#{subject.title}"
- expect(subject.wipless_title).to eq 'Implement feature called WIP'
+ expect(subject.wipless_title).to eq subject.title
+ end
end
it 'removes only draft prefix from the MR title' do
@@ -1522,6 +1552,42 @@ RSpec.describe MergeRequest, factory_default: :keep do
end
end
+ describe '#permits_force_push?' do
+ let_it_be(:merge_request) { build_stubbed(:merge_request) }
+
+ subject { merge_request.permits_force_push? }
+
+ context 'when source branch is not protected' do
+ before do
+ allow(ProtectedBranch).to receive(:protected?).and_return(false)
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when source branch is protected' do
+ before do
+ allow(ProtectedBranch).to receive(:protected?).and_return(true)
+ end
+
+ context 'when force push is not allowed' do
+ before do
+ allow(ProtectedBranch).to receive(:allow_force_push?) { false }
+ end
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when force push is allowed' do
+ before do
+ allow(ProtectedBranch).to receive(:allow_force_push?) { true }
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
+ end
+
describe '#can_remove_source_branch?' do
let_it_be(:user) { create(:user) }
let_it_be(:merge_request, reload: true) { create(:merge_request, :simple) }
@@ -3509,7 +3575,7 @@ RSpec.describe MergeRequest, factory_default: :keep do
let!(:job) { create(:ci_build, :with_deployment, :start_review_app, pipeline: pipeline, project: project) }
it 'returns environments' do
- is_expected.to eq(pipeline.environments)
+ is_expected.to eq(pipeline.environments_in_self_and_descendants.to_a)
expect(subject.count).to be(1)
end
@@ -3562,21 +3628,38 @@ RSpec.describe MergeRequest, factory_default: :keep do
end
describe '#update_diff_discussion_positions' do
- let(:discussion) { create(:diff_note_on_merge_request, project: subject.project, noteable: subject).to_discussion }
- let(:commit) { subject.project.commit(sample_commit.id) }
- let(:old_diff_refs) { subject.diff_refs }
+ subject { create(:merge_request, source_project: project) }
- before do
- # Update merge_request_diff so that #diff_refs will return commit.diff_refs
- allow(subject).to receive(:create_merge_request_diff) do
- subject.merge_request_diffs.create!(
- base_commit_sha: commit.parent_id,
- start_commit_sha: commit.parent_id,
- head_commit_sha: commit.sha
- )
+ let(:project) { create(:project, :repository) }
+ let(:create_commit) { project.commit("913c66a37b4a45b9769037c55c2d238bd0942d2e") }
+ let(:modify_commit) { project.commit("874797c3a73b60d2187ed6e2fcabd289ff75171e") }
+ let(:edit_commit) { project.commit("570e7b2abdd848b95f2f578043fc23bd6f6fd24d") }
+ let(:discussion) { create(:diff_note_on_merge_request, noteable: subject, project: project, position: old_position).to_discussion }
+ let(:path) { "files/ruby/popen.rb" }
+ let(:new_line) { 9 }
+
+ let(:old_diff_refs) do
+ Gitlab::Diff::DiffRefs.new(
+ base_sha: create_commit.parent_id,
+ head_sha: modify_commit.sha
+ )
+ end
- subject.reload_merge_request_diff
- end
+ let(:new_diff_refs) do
+ Gitlab::Diff::DiffRefs.new(
+ base_sha: create_commit.parent_id,
+ head_sha: edit_commit.sha
+ )
+ end
+
+ let(:old_position) do
+ Gitlab::Diff::Position.new(
+ old_path: path,
+ new_path: path,
+ old_line: nil,
+ new_line: new_line,
+ diff_refs: old_diff_refs
+ )
end
it "updates diff discussion positions" do
@@ -3584,36 +3667,67 @@ RSpec.describe MergeRequest, factory_default: :keep do
subject.project,
subject.author,
old_diff_refs: old_diff_refs,
- new_diff_refs: commit.diff_refs,
+ new_diff_refs: new_diff_refs,
paths: discussion.position.paths
).and_call_original
expect_any_instance_of(Discussions::UpdateDiffPositionService).to receive(:execute).with(discussion).and_call_original
- expect_any_instance_of(DiffNote).to receive(:save).once
subject.update_diff_discussion_positions(old_diff_refs: old_diff_refs,
- new_diff_refs: commit.diff_refs,
+ new_diff_refs: new_diff_refs,
current_user: subject.author)
end
- context 'when resolve_outdated_diff_discussions is set' do
- let(:project) { create(:project, :repository) }
+ it 'does not call the resolve method' do
+ expect(MergeRequests::ResolvedDiscussionNotificationService).not_to receive(:new)
- subject { create(:merge_request, source_project: project) }
+ subject.update_diff_discussion_positions(old_diff_refs: old_diff_refs,
+ new_diff_refs: new_diff_refs,
+ current_user: subject.author)
+ end
+ context 'when resolve_outdated_diff_discussions is set' do
before do
discussion
subject.project.update!(resolve_outdated_diff_discussions: true)
end
- it 'calls MergeRequests::ResolvedDiscussionNotificationService' do
- expect_any_instance_of(MergeRequests::ResolvedDiscussionNotificationService)
- .to receive(:execute).with(subject)
+ context 'when the active discussion is resolved in the update' do
+ it 'calls MergeRequests::ResolvedDiscussionNotificationService' do
+ expect_any_instance_of(MergeRequests::ResolvedDiscussionNotificationService)
+ .to receive(:execute).with(subject)
- subject.update_diff_discussion_positions(old_diff_refs: old_diff_refs,
- new_diff_refs: commit.diff_refs,
- current_user: subject.author)
+ subject.update_diff_discussion_positions(old_diff_refs: old_diff_refs,
+ new_diff_refs: new_diff_refs,
+ current_user: subject.author)
+ end
+ end
+
+ context 'when the active discussion does not have resolved in the update' do
+ let(:new_line) { 16 }
+
+ it 'does not call the resolve method' do
+ expect(MergeRequests::ResolvedDiscussionNotificationService).not_to receive(:new)
+
+ subject.update_diff_discussion_positions(old_diff_refs: old_diff_refs,
+ new_diff_refs: new_diff_refs,
+ current_user: subject.author)
+ end
+ end
+
+ context 'when the active discussion was already resolved' do
+ before do
+ discussion.resolve!(subject.author)
+ end
+
+ it 'does not call the resolve method' do
+ expect(MergeRequests::ResolvedDiscussionNotificationService).not_to receive(:new)
+
+ subject.update_diff_discussion_positions(old_diff_refs: old_diff_refs,
+ new_diff_refs: new_diff_refs,
+ current_user: subject.author)
+ end
end
end
end
@@ -4965,4 +5079,11 @@ RSpec.describe MergeRequest, factory_default: :keep do
it_behaves_like 'it has loose foreign keys' do
let(:factory_name) { :merge_request }
end
+
+ context 'loose foreign key on merge_requests.head_pipeline_id' do
+ it_behaves_like 'cleanup by a loose foreign key' do
+ let!(:parent) { create(:ci_pipeline) }
+ let!(:model) { create(:merge_request, head_pipeline: parent) }
+ end
+ end
end
diff --git a/spec/models/namespace/root_storage_statistics_spec.rb b/spec/models/namespace/root_storage_statistics_spec.rb
index 51c191069ec..11852828eab 100644
--- a/spec/models/namespace/root_storage_statistics_spec.rb
+++ b/spec/models/namespace/root_storage_statistics_spec.rb
@@ -28,24 +28,24 @@ RSpec.describe Namespace::RootStorageStatistics, type: :model do
let(:project1) { create(:project, namespace: namespace) }
let(:project2) { create(:project, namespace: namespace) }
- let!(:stat1) { create(:project_statistics, project: project1, with_data: true, size_multiplier: 100) }
- let!(:stat2) { create(:project_statistics, project: project2, with_data: true, size_multiplier: 200) }
+ let!(:project_stat1) { create(:project_statistics, project: project1, with_data: true, size_multiplier: 100) }
+ let!(:project_stat2) { create(:project_statistics, project: project2, with_data: true, size_multiplier: 200) }
- shared_examples 'data refresh' do
+ shared_examples 'project data refresh' do
it 'aggregates project statistics' do
root_storage_statistics.recalculate!
root_storage_statistics.reload
- total_repository_size = stat1.repository_size + stat2.repository_size
- total_wiki_size = stat1.wiki_size + stat2.wiki_size
- total_lfs_objects_size = stat1.lfs_objects_size + stat2.lfs_objects_size
- total_build_artifacts_size = stat1.build_artifacts_size + stat2.build_artifacts_size
- total_packages_size = stat1.packages_size + stat2.packages_size
- total_storage_size = stat1.storage_size + stat2.storage_size
- total_snippets_size = stat1.snippets_size + stat2.snippets_size
- total_pipeline_artifacts_size = stat1.pipeline_artifacts_size + stat2.pipeline_artifacts_size
- total_uploads_size = stat1.uploads_size + stat2.uploads_size
+ total_repository_size = project_stat1.repository_size + project_stat2.repository_size
+ total_wiki_size = project_stat1.wiki_size + project_stat2.wiki_size
+ total_lfs_objects_size = project_stat1.lfs_objects_size + project_stat2.lfs_objects_size
+ total_build_artifacts_size = project_stat1.build_artifacts_size + project_stat2.build_artifacts_size
+ total_packages_size = project_stat1.packages_size + project_stat2.packages_size
+ total_storage_size = project_stat1.storage_size + project_stat2.storage_size
+ total_snippets_size = project_stat1.snippets_size + project_stat2.snippets_size
+ total_pipeline_artifacts_size = project_stat1.pipeline_artifacts_size + project_stat2.pipeline_artifacts_size
+ total_uploads_size = project_stat1.uploads_size + project_stat2.uploads_size
expect(root_storage_statistics.repository_size).to eq(total_repository_size)
expect(root_storage_statistics.wiki_size).to eq(total_wiki_size)
@@ -83,7 +83,7 @@ RSpec.describe Namespace::RootStorageStatistics, type: :model do
end
end
- it_behaves_like 'data refresh'
+ it_behaves_like 'project data refresh'
it_behaves_like 'does not include personal snippets'
context 'with subgroups' do
@@ -93,19 +93,81 @@ RSpec.describe Namespace::RootStorageStatistics, type: :model do
let(:project1) { create(:project, namespace: subgroup1) }
let(:project2) { create(:project, namespace: subgroup2) }
- it_behaves_like 'data refresh'
+ it_behaves_like 'project data refresh'
it_behaves_like 'does not include personal snippets'
end
+ context 'with a group namespace' do
+ let_it_be(:root_group) { create(:group) }
+ let_it_be(:group1) { create(:group, parent: root_group) }
+ let_it_be(:subgroup1) { create(:group, parent: group1) }
+ let_it_be(:group2) { create(:group, parent: root_group) }
+ let_it_be(:root_namespace_stat) { create(:namespace_statistics, namespace: root_group, storage_size: 100, dependency_proxy_size: 100) }
+ let_it_be(:group1_namespace_stat) { create(:namespace_statistics, namespace: group1, storage_size: 200, dependency_proxy_size: 200) }
+ let_it_be(:group2_namespace_stat) { create(:namespace_statistics, namespace: group2, storage_size: 300, dependency_proxy_size: 300) }
+ let_it_be(:subgroup1_namespace_stat) { create(:namespace_statistics, namespace: subgroup1, storage_size: 300, dependency_proxy_size: 100) }
+
+ let(:namespace) { root_group }
+
+ it 'aggregates namespace statistics' do
+ # This group is not a descendant of the root_group so it shouldn't be included in the final stats.
+ other_group = create(:group)
+ create(:namespace_statistics, namespace: other_group, storage_size: 500, dependency_proxy_size: 500)
+
+ root_storage_statistics.recalculate!
+
+ total_repository_size = project_stat1.repository_size + project_stat2.repository_size
+ total_lfs_objects_size = project_stat1.lfs_objects_size + project_stat2.lfs_objects_size
+ total_build_artifacts_size = project_stat1.build_artifacts_size + project_stat2.build_artifacts_size
+ total_packages_size = project_stat1.packages_size + project_stat2.packages_size
+ total_snippets_size = project_stat1.snippets_size + project_stat2.snippets_size
+ total_pipeline_artifacts_size = project_stat1.pipeline_artifacts_size + project_stat2.pipeline_artifacts_size
+ total_uploads_size = project_stat1.uploads_size + project_stat2.uploads_size
+ total_wiki_size = project_stat1.wiki_size + project_stat2.wiki_size
+ total_dependency_proxy_size = root_namespace_stat.dependency_proxy_size + group1_namespace_stat.dependency_proxy_size + group2_namespace_stat.dependency_proxy_size + subgroup1_namespace_stat.dependency_proxy_size
+ total_storage_size = project_stat1.storage_size + project_stat2.storage_size + root_namespace_stat.storage_size + group1_namespace_stat.storage_size + group2_namespace_stat.storage_size + subgroup1_namespace_stat.storage_size
+
+ expect(root_storage_statistics.repository_size).to eq(total_repository_size)
+ expect(root_storage_statistics.lfs_objects_size).to eq(total_lfs_objects_size)
+ expect(root_storage_statistics.build_artifacts_size).to eq(total_build_artifacts_size)
+ expect(root_storage_statistics.packages_size).to eq(total_packages_size)
+ expect(root_storage_statistics.snippets_size).to eq(total_snippets_size)
+ expect(root_storage_statistics.pipeline_artifacts_size).to eq(total_pipeline_artifacts_size)
+ expect(root_storage_statistics.uploads_size).to eq(total_uploads_size)
+ expect(root_storage_statistics.dependency_proxy_size).to eq(total_dependency_proxy_size)
+ expect(root_storage_statistics.wiki_size).to eq(total_wiki_size)
+ expect(root_storage_statistics.storage_size).to eq(total_storage_size)
+ end
+
+ it 'works when there are no namespace statistics' do
+ NamespaceStatistics.delete_all
+
+ root_storage_statistics.recalculate!
+
+ total_storage_size = project_stat1.storage_size + project_stat2.storage_size
+
+ expect(root_storage_statistics.storage_size).to eq(total_storage_size)
+ end
+ end
+
context 'with a personal namespace' do
let_it_be(:user) { create(:user) }
let(:namespace) { user.namespace }
- it_behaves_like 'data refresh'
+ it_behaves_like 'project data refresh'
+
+ it 'does not aggregate namespace statistics' do
+ create(:namespace_statistics, namespace: user.namespace, storage_size: 200, dependency_proxy_size: 200)
+
+ root_storage_statistics.recalculate!
+
+ expect(root_storage_statistics.storage_size).to eq(project_stat1.storage_size + project_stat2.storage_size)
+ expect(root_storage_statistics.dependency_proxy_size).to eq(0)
+ end
context 'when user has personal snippets' do
- let(:total_project_snippets_size) { stat1.snippets_size + stat2.snippets_size }
+ let(:total_project_snippets_size) { project_stat1.snippets_size + project_stat2.snippets_size }
it 'aggregates personal and project snippets size' do
# This is just a a snippet authored by other user
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 5da0f7a134c..1728d4fc3f3 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -23,6 +23,7 @@ RSpec.describe Namespace do
it { is_expected.to have_one :root_storage_statistics }
it { is_expected.to have_one :aggregation_schedule }
it { is_expected.to have_one :namespace_settings }
+ it { is_expected.to have_one(:namespace_statistics) }
it { is_expected.to have_many :custom_emoji }
it { is_expected.to have_one :package_setting_relation }
it { is_expected.to have_one :onboarding_progress }
@@ -361,10 +362,70 @@ RSpec.describe Namespace do
context 'linear' do
it_behaves_like 'namespace traversal scopes'
end
+
+ shared_examples 'makes recursive queries' do
+ specify do
+ expect { subject }.to make_queries_matching(/WITH RECURSIVE/)
+ end
+ end
+
+ shared_examples 'does not make recursive queries' do
+ specify do
+ expect { subject }.not_to make_queries_matching(/WITH RECURSIVE/)
+ end
+ end
+
+ describe '.self_and_descendants' do
+ let_it_be(:namespace) { create(:namespace) }
+
+ subject { described_class.where(id: namespace).self_and_descendants.load }
+
+ it_behaves_like 'does not make recursive queries'
+
+ context 'when feature flag :use_traversal_ids is disabled' do
+ before do
+ stub_feature_flags(use_traversal_ids: false)
+ end
+
+ it_behaves_like 'makes recursive queries'
+ end
+
+ context 'when feature flag :use_traversal_ids_for_descendants_scopes is disabled' do
+ before do
+ stub_feature_flags(use_traversal_ids_for_descendants_scopes: false)
+ end
+
+ it_behaves_like 'makes recursive queries'
+ end
+ end
+
+ describe '.self_and_descendant_ids' do
+ let_it_be(:namespace) { create(:namespace) }
+
+ subject { described_class.where(id: namespace).self_and_descendant_ids.load }
+
+ it_behaves_like 'does not make recursive queries'
+
+ context 'when feature flag :use_traversal_ids is disabled' do
+ before do
+ stub_feature_flags(use_traversal_ids: false)
+ end
+
+ it_behaves_like 'makes recursive queries'
+ end
+
+ context 'when feature flag :use_traversal_ids_for_descendants_scopes is disabled' do
+ before do
+ stub_feature_flags(use_traversal_ids_for_descendants_scopes: false)
+ end
+
+ it_behaves_like 'makes recursive queries'
+ end
+ end
end
context 'traversal_ids on create' do
- context 'default traversal_ids' do
+ shared_examples 'default traversal_ids' do
let(:namespace) { build(:namespace) }
before do
@@ -374,6 +435,18 @@ RSpec.describe Namespace do
it { expect(namespace.traversal_ids).to eq [namespace.id] }
end
+
+ context 'with before_commit callback' do
+ it_behaves_like 'default traversal_ids'
+ end
+
+ context 'with after_create callback' do
+ before do
+ stub_feature_flags(sync_traversal_ids_before_commit: false)
+ end
+
+ it_behaves_like 'default traversal_ids'
+ end
end
describe "after_commit :expire_child_caches" do
@@ -2158,4 +2231,13 @@ RSpec.describe Namespace do
end
end
end
+
+ describe 'storage_enforcement_date' do
+ let_it_be(:namespace) { create(:group) }
+
+ # Date TBD: https://gitlab.com/gitlab-org/gitlab/-/issues/350632
+ it 'returns false' do
+ expect(namespace.storage_enforcement_date).to be(nil)
+ end
+ end
end
diff --git a/spec/models/namespace_statistics_spec.rb b/spec/models/namespace_statistics_spec.rb
new file mode 100644
index 00000000000..ac747b70a9f
--- /dev/null
+++ b/spec/models/namespace_statistics_spec.rb
@@ -0,0 +1,207 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe NamespaceStatistics do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+
+ it { is_expected.to belong_to(:namespace) }
+
+ it { is_expected.to validate_presence_of(:namespace) }
+
+ describe '#refresh!' do
+ let(:namespace) { group }
+ let(:statistics) { create(:namespace_statistics, namespace: namespace) }
+ let(:columns) { [] }
+
+ subject(:refresh!) { statistics.refresh!(only: columns) }
+
+ context 'when database is read_only' do
+ it 'does not save the object' do
+ allow(Gitlab::Database).to receive(:read_only?).and_return(true)
+
+ expect(statistics).not_to receive(:save!)
+
+ refresh!
+ end
+ end
+
+ context 'when namespace belong to a user' do
+ let(:namespace) { user.namespace }
+
+ it 'does not save the object' do
+ expect(statistics).not_to receive(:save!)
+
+ refresh!
+ end
+ end
+
+ shared_examples 'creates the namespace statistics' do
+ specify do
+ expect(statistics).to receive(:save!)
+
+ refresh!
+ end
+ end
+
+ context 'when invalid option is passed' do
+ let(:columns) { [:foo] }
+
+ it 'does not update any column' do
+ create(:dependency_proxy_manifest, group: namespace, size: 50)
+
+ expect(statistics).not_to receive(:update_dependency_proxy_size)
+ expect { refresh! }.not_to change { statistics.reload.storage_size }
+ end
+
+ it_behaves_like 'creates the namespace statistics'
+ end
+
+ context 'when no option is passed' do
+ it 'updates the dependency proxy size' do
+ expect(statistics).to receive(:update_dependency_proxy_size)
+
+ refresh!
+ end
+
+ it_behaves_like 'creates the namespace statistics'
+ end
+
+ context 'when dependency_proxy_size option is passed' do
+ let(:columns) { [:dependency_proxy_size] }
+
+ it 'updates the dependency proxy size' do
+ expect(statistics).to receive(:update_dependency_proxy_size)
+
+ refresh!
+ end
+
+ it_behaves_like 'creates the namespace statistics'
+ end
+ end
+
+ describe '#update_storage_size' do
+ let_it_be(:statistics, reload: true) { create(:namespace_statistics, namespace: group) }
+
+ it 'sets storage_size to the dependency_proxy_size' do
+ statistics.dependency_proxy_size = 3
+
+ statistics.update_storage_size
+
+ expect(statistics.storage_size).to eq 3
+ end
+ end
+
+ describe '#update_dependency_proxy_size' do
+ let_it_be(:statistics, reload: true) { create(:namespace_statistics, namespace: group) }
+ let_it_be(:dependency_proxy_manifest) { create(:dependency_proxy_manifest, group: group, size: 50) }
+ let_it_be(:dependency_proxy_blob) { create(:dependency_proxy_blob, group: group, size: 50) }
+
+ subject(:update_dependency_proxy_size) { statistics.update_dependency_proxy_size }
+
+ it 'updates the dependency proxy size' do
+ update_dependency_proxy_size
+
+ expect(statistics.dependency_proxy_size).to eq 100
+ end
+
+ context 'when namespace does not belong to a group' do
+ let(:statistics) { create(:namespace_statistics, namespace: user.namespace) }
+
+ it 'does not update the dependency proxy size' do
+ update_dependency_proxy_size
+
+ expect(statistics.dependency_proxy_size).to be_zero
+ end
+ end
+ end
+
+ context 'before saving statistics' do
+ let(:statistics) { create(:namespace_statistics, namespace: group, dependency_proxy_size: 10) }
+
+ it 'updates storage size' do
+ expect(statistics).to receive(:update_storage_size).and_call_original
+
+ statistics.save!
+
+ expect(statistics.storage_size).to eq 10
+ end
+ end
+
+ context 'after saving statistics', :aggregate_failures do
+ let(:statistics) { create(:namespace_statistics, namespace: namespace) }
+ let(:namespace) { group }
+
+ context 'when storage_size is not updated' do
+ it 'does not enqueue the job to update root storage statistics' do
+ expect(statistics).not_to receive(:update_root_storage_statistics)
+ expect(Namespaces::ScheduleAggregationWorker).not_to receive(:perform_async)
+
+ statistics.save!
+ end
+ end
+
+ context 'when storage_size is updated' do
+ before do
+ # we have to update this value instead of `storage_size` because the before_save
+ # hook we have. If we don't do it, storage_size will be set to the dependency_proxy_size value
+ # which is 0.
+ statistics.dependency_proxy_size = 10
+ end
+
+ it 'enqueues the job to update root storage statistics' do
+ expect(statistics).to receive(:update_root_storage_statistics).and_call_original
+ expect(Namespaces::ScheduleAggregationWorker).to receive(:perform_async).with(group.id)
+
+ statistics.save!
+ end
+
+ context 'when namespace does not belong to a group' do
+ let(:namespace) { user.namespace }
+
+ it 'does not enqueue the job to update root storage statistics' do
+ expect(statistics).to receive(:update_root_storage_statistics).and_call_original
+ expect(Namespaces::ScheduleAggregationWorker).not_to receive(:perform_async)
+
+ statistics.save!
+ end
+ end
+ end
+
+ context 'when other columns are updated' do
+ it 'does not enqueue the job to update root storage statistics' do
+ columns_to_update = NamespaceStatistics.columns_hash.reject { |k, _| %w(id namespace_id).include?(k) || k.include?('_size') }.keys
+ columns_to_update.each { |c| statistics[c] = 10 }
+
+ expect(statistics).not_to receive(:update_root_storage_statistics)
+ expect(Namespaces::ScheduleAggregationWorker).not_to receive(:perform_async)
+
+ statistics.save!
+ end
+ end
+ end
+
+ context 'after destroy statistics', :aggregate_failures do
+ let(:statistics) { create(:namespace_statistics, namespace: namespace) }
+ let(:namespace) { group }
+
+ it 'enqueues the job to update root storage statistics' do
+ expect(statistics).to receive(:update_root_storage_statistics).and_call_original
+ expect(Namespaces::ScheduleAggregationWorker).to receive(:perform_async).with(group.id)
+
+ statistics.destroy!
+ end
+
+ context 'when namespace belongs to a group' do
+ let(:namespace) { user.namespace }
+
+ it 'does not enqueue the job to update root storage statistics' do
+ expect(statistics).to receive(:update_root_storage_statistics).and_call_original
+ expect(Namespaces::ScheduleAggregationWorker).not_to receive(:perform_async)
+
+ statistics.destroy!
+ end
+ end
+ end
+end
diff --git a/spec/models/namespaces/user_namespace_spec.rb b/spec/models/namespaces/user_namespace_spec.rb
index 7c00a597756..fb9e7571666 100644
--- a/spec/models/namespaces/user_namespace_spec.rb
+++ b/spec/models/namespaces/user_namespace_spec.rb
@@ -9,4 +9,13 @@ RSpec.describe Namespaces::UserNamespace, type: :model do
describe 'validations' do
it { is_expected.to validate_presence_of(:owner) }
end
+
+ describe '#owners' do
+ let(:owner) { build(:user) }
+ let(:namespace) { build(:namespace, owner: owner) }
+
+ specify do
+ expect(namespace.owners).to match_array([owner])
+ end
+ end
end
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 9d9cca0678a..34ce0031bd2 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -445,7 +445,7 @@ RSpec.describe Note do
end
end
- describe "#system_note_with_references_visible_for?" do
+ describe "#system_note_visible_for?" do
let(:project) { create(:project, :public) }
let(:user) { create(:user) }
let(:guest) { create(:project_member, :guest, project: project, user: create(:user)).user }
@@ -469,22 +469,25 @@ RSpec.describe Note do
end
it 'returns visible but not readable for non-member user' do
- expect(note.system_note_with_references_visible_for?(non_member)).to be_truthy
+ expect(note.system_note_visible_for?(non_member)).to be_truthy
expect(note.readable_by?(non_member)).to be_falsy
end
it 'returns visible but not readable for a nil user' do
- expect(note.system_note_with_references_visible_for?(nil)).to be_truthy
+ expect(note.system_note_visible_for?(nil)).to be_truthy
expect(note.readable_by?(nil)).to be_falsy
end
end
end
describe "#system_note_viewable_by?(user)" do
- let_it_be(:note) { create(:note) }
+ let_it_be(:group) { create(:group, :private) }
+ let_it_be(:project) { create(:project, group: group) }
+ let_it_be(:note) { create(:note, project: project) }
let_it_be(:user) { create(:user) }
- let!(:metadata) { create(:system_note_metadata, note: note, action: "branch") }
+ let(:action) { "commit" }
+ let!(:metadata) { create(:system_note_metadata, note: note, action: action) }
context "when system_note_metadata is not present" do
it "returns true" do
@@ -494,32 +497,50 @@ RSpec.describe Note do
end
end
- context "system_note_metadata isn't of type 'branch'" do
- before do
- metadata.action = "not_a_branch"
- end
-
+ context "system_note_metadata isn't of type 'branch' or 'contact'" do
it "returns true" do
expect(note.send(:system_note_viewable_by?, user)).to be_truthy
end
end
- context "user doesn't have :download_code ability" do
- it "returns false" do
- expect(note.send(:system_note_viewable_by?, user)).to be_falsey
+ context "system_note_metadata is of type 'branch'" do
+ let(:action) { "branch" }
+
+ context "user doesn't have :download_code ability" do
+ it "returns false" do
+ expect(note.send(:system_note_viewable_by?, user)).to be_falsey
+ end
+ end
+
+ context "user has the :download_code ability" do
+ it "returns true" do
+ expect(Ability).to receive(:allowed?).with(user, :download_code, note.project).and_return(true)
+
+ expect(note.send(:system_note_viewable_by?, user)).to be_truthy
+ end
end
end
- context "user has the :download_code ability" do
- it "returns true" do
- expect(Ability).to receive(:allowed?).with(user, :download_code, note.project).and_return(true)
+ context "system_note_metadata is of type 'contact'" do
+ let(:action) { "contact" }
- expect(note.send(:system_note_viewable_by?, user)).to be_truthy
+ context "user doesn't have :read_crm_contact ability" do
+ it "returns false" do
+ expect(note.send(:system_note_viewable_by?, user)).to be_falsey
+ end
+ end
+
+ context "user has the :read_crm_contact ability" do
+ it "returns true" do
+ expect(Ability).to receive(:allowed?).with(user, :read_crm_contact, note.project.group).and_return(true)
+
+ expect(note.send(:system_note_viewable_by?, user)).to be_truthy
+ end
end
end
end
- describe "system_note_with_references_visible_for?" do
+ describe "system_note_visible_for?" do
let_it_be(:private_user) { create(:user) }
let_it_be(:private_project) { create(:project, namespace: private_user.namespace) { |p| p.add_maintainer(private_user) } }
let_it_be(:private_issue) { create(:issue, project: private_project) }
@@ -529,11 +550,11 @@ RSpec.describe Note do
shared_examples "checks references" do
it "returns false" do
- expect(note.system_note_with_references_visible_for?(ext_issue.author)).to be_falsy
+ expect(note.system_note_visible_for?(ext_issue.author)).to be_falsy
end
it "returns true" do
- expect(note.system_note_with_references_visible_for?(private_user)).to be_truthy
+ expect(note.system_note_visible_for?(private_user)).to be_truthy
end
it "returns true if user visible reference count set" do
@@ -541,7 +562,7 @@ RSpec.describe Note do
note.total_reference_count = 1
expect(note).not_to receive(:reference_mentionables)
- expect(note.system_note_with_references_visible_for?(ext_issue.author)).to be_truthy
+ expect(note.system_note_visible_for?(ext_issue.author)).to be_truthy
end
it "returns false if user visible reference count set but does not match total reference count" do
@@ -549,14 +570,14 @@ RSpec.describe Note do
note.total_reference_count = 2
expect(note).not_to receive(:reference_mentionables)
- expect(note.system_note_with_references_visible_for?(ext_issue.author)).to be_falsy
+ expect(note.system_note_visible_for?(ext_issue.author)).to be_falsy
end
it "returns false if ref count is 0" do
note.user_visible_reference_count = 0
expect(note).not_to receive(:reference_mentionables)
- expect(note.system_note_with_references_visible_for?(ext_issue.author)).to be_falsy
+ expect(note.system_note_visible_for?(ext_issue.author)).to be_falsy
end
end
@@ -617,16 +638,16 @@ RSpec.describe Note do
let(:note) do
create :note,
noteable: ext_issue, project: ext_proj,
- note: "mentioned in #{ext_proj.owner.to_reference}",
+ note: "mentioned in #{ext_proj.first_owner.to_reference}",
system: true
end
it "returns true for other users" do
- expect(note.system_note_with_references_visible_for?(ext_issue.author)).to be_truthy
+ expect(note.system_note_visible_for?(ext_issue.author)).to be_truthy
end
it "returns true for anonymous users" do
- expect(note.system_note_with_references_visible_for?(nil)).to be_truthy
+ expect(note.system_note_visible_for?(nil)).to be_truthy
end
end
end
diff --git a/spec/models/packages/package_file_spec.rb b/spec/models/packages/package_file_spec.rb
index a86caa074f1..fd453d8e5a9 100644
--- a/spec/models/packages/package_file_spec.rb
+++ b/spec/models/packages/package_file_spec.rb
@@ -148,16 +148,6 @@ RSpec.describe Packages::PackageFile, type: :model do
it 'does not return them' do
expect(described_class.for_helm_with_channel(project, channel)).to contain_exactly(helm_file2)
end
-
- context 'with packages_installable_package_files disabled' do
- before do
- stub_feature_flags(packages_installable_package_files: false)
- end
-
- it 'returns them' do
- expect(described_class.for_helm_with_channel(project, channel)).to contain_exactly(helm_file2, package_file_pending_destruction)
- end
- end
end
end
@@ -232,16 +222,6 @@ RSpec.describe Packages::PackageFile, type: :model do
it 'does not return them' do
expect(subject).to contain_exactly(helm_package_file2)
end
-
- context 'with packages_installable_package_files disabled' do
- before do
- stub_feature_flags(packages_installable_package_files: false)
- end
-
- it 'returns them' do
- expect(subject).to contain_exactly(package_file_pending_destruction)
- end
- end
end
end
end
diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb
index 0735bf25690..2ebc9864d9b 100644
--- a/spec/models/pages_domain_spec.rb
+++ b/spec/models/pages_domain_spec.rb
@@ -36,9 +36,12 @@ RSpec.describe PagesDomain do
'123.456.789' => true,
'0x12345.com' => true,
'0123123' => true,
- '_foo.com' => false,
+ 'a-reserved.com' => true,
+ 'a.b-reserved.com' => true,
'reserved.com' => false,
+ '_foo.com' => false,
'a.reserved.com' => false,
+ 'a.b.reserved.com' => false,
nil => false
}.each do |value, validity|
context "domain #{value.inspect} validity" do
diff --git a/spec/models/personal_access_token_spec.rb b/spec/models/personal_access_token_spec.rb
index 8cd831d2f85..88206fbf48c 100644
--- a/spec/models/personal_access_token_spec.rb
+++ b/spec/models/personal_access_token_spec.rb
@@ -22,6 +22,16 @@ RSpec.describe PersonalAccessToken do
end
describe 'scopes' do
+ describe '.project_access_tokens' do
+ let_it_be(:user) { create(:user, :project_bot) }
+ let_it_be(:project_member) { create(:project_member, user: user) }
+ let_it_be(:project_access_token) { create(:personal_access_token, user: user) }
+
+ subject { described_class.project_access_token }
+
+ it { is_expected.to contain_exactly(project_access_token) }
+ end
+
describe '.for_user' do
it 'returns personal access tokens of specified user only' do
user_1 = create(:user)
diff --git a/spec/models/preloaders/users_max_access_level_in_projects_preloader_spec.rb b/spec/models/preloaders/users_max_access_level_in_projects_preloader_spec.rb
new file mode 100644
index 00000000000..7ecb6bb9861
--- /dev/null
+++ b/spec/models/preloaders/users_max_access_level_in_projects_preloader_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+RSpec.describe Preloaders::UsersMaxAccessLevelInProjectsPreloader do
+ let_it_be(:user1) { create(:user) }
+ let_it_be(:user2) { create(:user) }
+
+ let_it_be(:project_1) { create(:project) }
+ let_it_be(:project_2) { create(:project) }
+ let_it_be(:project_3) { create(:project) }
+
+ let(:projects) { [project_1, project_2, project_3] }
+ let(:users) { [user1, user2] }
+
+ before do
+ project_1.add_developer(user1)
+ project_1.add_developer(user2)
+
+ project_2.add_developer(user1)
+ project_2.add_developer(user2)
+
+ project_3.add_developer(user1)
+ project_3.add_developer(user2)
+ end
+
+ context 'preload maximum access level to avoid querying project_authorizations', :request_store do
+ it 'avoids N+1 queries', :request_store do
+ Preloaders::UsersMaxAccessLevelInProjectsPreloader.new(projects: projects, users: users).execute
+
+ expect(count_queries).to eq(0)
+ end
+
+ it 'runs N queries without preloading' do
+ query_count_without_preload = count_queries
+
+ Preloaders::UsersMaxAccessLevelInProjectsPreloader.new(projects: projects, users: users).execute
+ count_queries_with_preload = count_queries
+
+ expect(count_queries_with_preload).to be < query_count_without_preload
+ end
+ end
+
+ def count_queries
+ ActiveRecord::QueryRecorder.new do
+ projects.each do |project|
+ user1.can?(:read_project, project)
+ user2.can?(:read_project, project)
+ end
+ end.count
+ end
+end
diff --git a/spec/models/project_import_state_spec.rb b/spec/models/project_import_state_spec.rb
index 843beb4ce23..4ad2446f8d0 100644
--- a/spec/models/project_import_state_spec.rb
+++ b/spec/models/project_import_state_spec.rb
@@ -79,6 +79,29 @@ RSpec.describe ProjectImportState, type: :model do
expect(import_state.last_error).to eq(error_message)
end
+
+ it 'removes project import data' do
+ import_data = ProjectImportData.new(data: { 'test' => 'some data' })
+ project = create(:project, import_data: import_data)
+ import_state = create(:import_state, :started, project: project)
+
+ expect do
+ import_state.mark_as_failed(error_message)
+ end.to change { project.reload.import_data }.from(import_data).to(nil)
+ end
+
+ context 'when remove_import_data_on_failure feature flag is disabled' do
+ it 'removes project import data' do
+ stub_feature_flags(remove_import_data_on_failure: false)
+
+ project = create(:project, import_data: ProjectImportData.new(data: { 'test' => 'some data' }))
+ import_state = create(:import_state, :started, project: project)
+
+ expect do
+ import_state.mark_as_failed(error_message)
+ end.not_to change { project.reload.import_data }
+ end
+ end
end
describe '#human_status_name' do
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 30114d36a06..c487c87db1c 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -236,27 +236,42 @@ RSpec.describe Project, factory_default: :keep do
end
context 'with project namespaces' do
- it 'automatically creates a project namespace' do
- project = build(:project, path: 'hopefully-valid-path1')
- project.save!
+ shared_examples 'creates project namespace' do
+ it 'automatically creates a project namespace' do
+ project = build(:project, path: 'hopefully-valid-path1')
+ project.save!
- expect(project).to be_persisted
- expect(project.project_namespace).to be_persisted
- expect(project.project_namespace).to be_in_sync_with_project(project)
- end
+ expect(project).to be_persisted
+ expect(project.project_namespace).to be_persisted
+ expect(project.project_namespace).to be_in_sync_with_project(project)
+ expect(project.reload.project_namespace.traversal_ids).to eq([project.namespace.traversal_ids, project.project_namespace.id].flatten.compact)
+ end
- context 'with FF disabled' do
- before do
- stub_feature_flags(create_project_namespace_on_project_create: false)
+ context 'with FF disabled' do
+ before do
+ stub_feature_flags(create_project_namespace_on_project_create: false)
+ end
+
+ it 'does not create a project namespace' do
+ project = build(:project, path: 'hopefully-valid-path2')
+ project.save!
+
+ expect(project).to be_persisted
+ expect(project.project_namespace).to be_nil
+ end
end
+ end
- it 'does not create a project namespace' do
- project = build(:project, path: 'hopefully-valid-path2')
- project.save!
+ context 'sync-ing traversal_ids in before_commit callback' do
+ it_behaves_like 'creates project namespace'
+ end
- expect(project).to be_persisted
- expect(project.project_namespace).to be_nil
+ context 'sync-ing traversal_ids in after_create callback' do
+ before do
+ stub_feature_flags(sync_traversal_ids_before_commit: false)
end
+
+ it_behaves_like 'creates project namespace'
end
end
end
@@ -870,7 +885,88 @@ RSpec.describe Project, factory_default: :keep do
end
end
+ describe '#merge_commit_template_or_default' do
+ let_it_be(:project) { create(:project) }
+
+ it 'returns default merge commit template' do
+ expect(project.merge_commit_template_or_default).to eq(Project::DEFAULT_MERGE_COMMIT_TEMPLATE)
+ end
+
+ context 'when merge commit template is set and not nil' do
+ before do
+ project.merge_commit_template = '%{description}'
+ end
+
+ it 'returns current value' do
+ expect(project.merge_commit_template_or_default).to eq('%{description}')
+ end
+ end
+ end
+
+ describe '#merge_commit_template_or_default=' do
+ let_it_be(:project) { create(:project) }
+
+ it 'sets template to nil when set to default value' do
+ project.merge_commit_template_or_default = Project::DEFAULT_MERGE_COMMIT_TEMPLATE
+ expect(project.merge_commit_template).to be_nil
+ end
+
+ it 'sets template to nil when set to default value but with CRLF line endings' do
+ project.merge_commit_template_or_default = "Merge branch '%{source_branch}' into '%{target_branch}'\r\n\r\n%{title}\r\n\r\n%{issues}\r\n\r\nSee merge request %{reference}"
+ expect(project.merge_commit_template).to be_nil
+ end
+
+ it 'allows changing template' do
+ project.merge_commit_template_or_default = '%{description}'
+ expect(project.merge_commit_template).to eq('%{description}')
+ end
+
+ it 'allows setting template to nil' do
+ project.merge_commit_template_or_default = nil
+ expect(project.merge_commit_template).to be_nil
+ end
+ end
+
+ describe '#squash_commit_template_or_default' do
+ let_it_be(:project) { create(:project) }
+
+ it 'returns default squash commit template' do
+ expect(project.squash_commit_template_or_default).to eq(Project::DEFAULT_SQUASH_COMMIT_TEMPLATE)
+ end
+
+ context 'when squash commit template is set and not nil' do
+ before do
+ project.squash_commit_template = '%{description}'
+ end
+
+ it 'returns current value' do
+ expect(project.squash_commit_template_or_default).to eq('%{description}')
+ end
+ end
+ end
+
+ describe '#squash_commit_template_or_default=' do
+ let_it_be(:project) { create(:project) }
+
+ it 'sets template to nil when set to default value' do
+ project.squash_commit_template_or_default = Project::DEFAULT_SQUASH_COMMIT_TEMPLATE
+ expect(project.squash_commit_template).to be_nil
+ end
+
+ it 'allows changing template' do
+ project.squash_commit_template_or_default = '%{description}'
+ expect(project.squash_commit_template).to eq('%{description}')
+ end
+
+ it 'allows setting template to nil' do
+ project.squash_commit_template_or_default = nil
+ expect(project.squash_commit_template).to be_nil
+ end
+ end
+
describe 'reference methods' do
+ # TODO update when we have multiple owners of a project
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/350605
let_it_be(:owner) { create(:user, name: 'Gitlab') }
let_it_be(:namespace) { create(:namespace, name: 'Sample namespace', path: 'sample-namespace', owner: owner) }
let_it_be(:project) { create(:project, name: 'Sample project', path: 'sample-project', namespace: namespace) }
@@ -2874,7 +2970,7 @@ RSpec.describe Project, factory_default: :keep do
end
before do
- project.repository.rm_branch(project.owner, branch.name)
+ project.repository.rm_branch(project.first_owner, branch.name)
end
subject { project.latest_pipeline(branch.name) }
@@ -3869,45 +3965,6 @@ RSpec.describe Project, factory_default: :keep do
end
end
- describe '#ci_instance_variables_for' do
- let(:project) { build_stubbed(:project) }
-
- let!(:instance_variable) do
- create(:ci_instance_variable, value: 'secret')
- end
-
- let!(:protected_instance_variable) do
- create(:ci_instance_variable, :protected, value: 'protected')
- end
-
- subject { project.ci_instance_variables_for(ref: 'ref') }
-
- before do
- stub_application_setting(
- default_branch_protection: Gitlab::Access::PROTECTION_NONE)
- end
-
- context 'when the ref is not protected' do
- before do
- allow(project).to receive(:protected_for?).with('ref').and_return(false)
- end
-
- it 'contains only the CI variables' do
- is_expected.to contain_exactly(instance_variable)
- end
- end
-
- context 'when the ref is protected' do
- before do
- allow(project).to receive(:protected_for?).with('ref').and_return(true)
- end
-
- it 'contains all the variables' do
- is_expected.to contain_exactly(instance_variable, protected_instance_variable)
- end
- end
- end
-
describe '#any_lfs_file_locks?', :request_store do
let_it_be(:project) { create(:project) }
@@ -6238,6 +6295,21 @@ RSpec.describe Project, factory_default: :keep do
end
end
+ describe '.for_group_and_its_ancestor_groups' do
+ it 'returns projects for group and its ancestors' do
+ group_1 = create(:group)
+ project_1 = create(:project, namespace: group_1)
+ group_2 = create(:group, parent: group_1)
+ project_2 = create(:project, namespace: group_2)
+ group_3 = create(:group, parent: group_2)
+ project_3 = create(:project, namespace: group_2)
+ group_4 = create(:group, parent: group_3)
+ create(:project, namespace: group_4)
+
+ expect(described_class.for_group_and_its_ancestor_groups(group_3)).to match_array([project_1, project_2, project_3])
+ end
+ end
+
describe '.deployments' do
subject { project.deployments }
@@ -7116,6 +7188,29 @@ RSpec.describe Project, factory_default: :keep do
it { is_expected.to be true }
end
+ describe '#related_group_ids' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:sub_group) { create(:group, parent: group) }
+
+ context 'when associated with a namespace' do
+ let(:project) { create(:project, namespace: create(:namespace)) }
+ let!(:linked_group) { create(:project_group_link, project: project).group }
+
+ it 'only includes linked groups' do
+ expect(project.related_group_ids).to contain_exactly(linked_group.id)
+ end
+ end
+
+ context 'when associated with a group' do
+ let(:project) { create(:project, group: sub_group) }
+ let!(:linked_group) { create(:project_group_link, project: project).group }
+
+ it 'includes self, ancestors and linked groups' do
+ expect(project.related_group_ids).to contain_exactly(group.id, sub_group.id, linked_group.id)
+ end
+ end
+ end
+
describe '#package_already_taken?' do
let_it_be(:namespace) { create(:namespace, path: 'test') }
let_it_be(:project) { create(:project, :public, namespace: namespace) }
@@ -7410,6 +7505,67 @@ RSpec.describe Project, factory_default: :keep do
expect(project.reload.topics.map(&:name)).to eq(%w[topic1 topic2 topic3])
end
end
+
+ context 'public topics counter' do
+ let_it_be(:topic_1) { create(:topic, name: 't1') }
+ let_it_be(:topic_2) { create(:topic, name: 't2') }
+ let_it_be(:topic_3) { create(:topic, name: 't3') }
+
+ let(:private) { Gitlab::VisibilityLevel::PRIVATE }
+ let(:internal) { Gitlab::VisibilityLevel::INTERNAL }
+ let(:public) { Gitlab::VisibilityLevel::PUBLIC }
+
+ subject do
+ project_updates = {
+ visibility_level: new_visibility,
+ topic_list: new_topic_list
+ }.compact
+
+ project.update!(project_updates)
+ end
+
+ using RSpec::Parameterized::TableSyntax
+
+ # rubocop:disable Lint/BinaryOperatorWithIdenticalOperands
+ where(:initial_visibility, :new_visibility, :new_topic_list, :expected_count_changes) do
+ ref(:private) | nil | 't2, t3' | [0, 0, 0]
+ ref(:internal) | nil | 't2, t3' | [-1, 0, 1]
+ ref(:public) | nil | 't2, t3' | [-1, 0, 1]
+ ref(:private) | ref(:public) | nil | [1, 1, 0]
+ ref(:private) | ref(:internal) | nil | [1, 1, 0]
+ ref(:private) | ref(:private) | nil | [0, 0, 0]
+ ref(:internal) | ref(:public) | nil | [0, 0, 0]
+ ref(:internal) | ref(:internal) | nil | [0, 0, 0]
+ ref(:internal) | ref(:private) | nil | [-1, -1, 0]
+ ref(:public) | ref(:public) | nil | [0, 0, 0]
+ ref(:public) | ref(:internal) | nil | [0, 0, 0]
+ ref(:public) | ref(:private) | nil | [-1, -1, 0]
+ ref(:private) | ref(:public) | 't2, t3' | [0, 1, 1]
+ ref(:private) | ref(:internal) | 't2, t3' | [0, 1, 1]
+ ref(:private) | ref(:private) | 't2, t3' | [0, 0, 0]
+ ref(:internal) | ref(:public) | 't2, t3' | [-1, 0, 1]
+ ref(:internal) | ref(:internal) | 't2, t3' | [-1, 0, 1]
+ ref(:internal) | ref(:private) | 't2, t3' | [-1, -1, 0]
+ ref(:public) | ref(:public) | 't2, t3' | [-1, 0, 1]
+ ref(:public) | ref(:internal) | 't2, t3' | [-1, 0, 1]
+ ref(:public) | ref(:private) | 't2, t3' | [-1, -1, 0]
+ end
+ # rubocop:enable Lint/BinaryOperatorWithIdenticalOperands
+
+ with_them do
+ it 'increments or decrements counters of topics' do
+ project.reload.update!(
+ visibility_level: initial_visibility,
+ topic_list: [topic_1.name, topic_2.name]
+ )
+
+ expect { subject }
+ .to change { topic_1.reload.non_private_projects_count }.by(expected_count_changes[0])
+ .and change { topic_2.reload.non_private_projects_count }.by(expected_count_changes[1])
+ .and change { topic_3.reload.non_private_projects_count }.by(expected_count_changes[2])
+ end
+ end
+ end
end
shared_examples 'all_runners' do
@@ -7801,7 +7957,8 @@ RSpec.describe Project, factory_default: :keep do
end
describe '#context_commits_enabled?' do
- let_it_be(:project) { create(:project) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, namespace: group) }
subject(:result) { project.context_commits_enabled? }
@@ -7821,19 +7978,19 @@ RSpec.describe Project, factory_default: :keep do
it { is_expected.to be_falsey }
end
- context 'when context_commits feature flag is enabled on this project' do
+ context 'when context_commits feature flag is enabled on project group' do
before do
- stub_feature_flags(context_commits: project)
+ stub_feature_flags(context_commits: group)
end
it { is_expected.to be_truthy }
end
- context 'when context_commits feature flag is enabled on another project' do
- let(:another_project) { create(:project) }
+ context 'when context_commits feature flag is enabled on another group' do
+ let(:another_group) { create(:group) }
before do
- stub_feature_flags(context_commits: another_project)
+ stub_feature_flags(context_commits: another_group)
end
it { is_expected.to be_falsey }
diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb
index c0bad96effc..bfdebbc33df 100644
--- a/spec/models/project_team_spec.rb
+++ b/spec/models/project_team_spec.rb
@@ -77,15 +77,43 @@ RSpec.describe ProjectTeam do
end
end
+ describe 'owner methods' do
+ context 'personal project' do
+ let(:project) { create(:project) }
+ let(:owner) { project.first_owner }
+
+ specify { expect(project.team.owners).to contain_exactly(owner) }
+ specify { expect(project.team.owner?(owner)).to be_truthy }
+ end
+
+ context 'group project' do
+ let(:group) { create(:group) }
+ let(:project) { create(:project, group: group) }
+ let(:user1) { create(:user) }
+ let(:user2) { create(:user) }
+
+ before do
+ group.add_owner(user1)
+ group.add_owner(user2)
+ end
+
+ specify { expect(project.team.owners).to contain_exactly(user1, user2) }
+ specify { expect(project.team.owner?(user1)).to be_truthy }
+ specify { expect(project.team.owner?(user2)).to be_truthy }
+ end
+ end
+
describe '#fetch_members' do
context 'personal project' do
let(:project) { create(:project) }
it 'returns project members' do
+ # TODO this can be updated when we have multiple project owners
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/350605
user = create(:user)
project.add_guest(user)
- expect(project.team.members).to contain_exactly(user, project.owner)
+ expect(project.team.members).to contain_exactly(user, project.first_owner)
end
it 'returns project members of a specified level' do
@@ -103,7 +131,7 @@ RSpec.describe ProjectTeam do
group_access: Gitlab::Access::GUEST)
expect(project.team.members)
- .to contain_exactly(group_member.user, project.owner)
+ .to contain_exactly(group_member.user, project.first_owner)
end
it 'returns invited members of a group of a specified level' do
diff --git a/spec/models/state_note_spec.rb b/spec/models/state_note_spec.rb
index bd07af7ceca..e91150695b0 100644
--- a/spec/models/state_note_spec.rb
+++ b/spec/models/state_note_spec.rb
@@ -55,7 +55,7 @@ RSpec.describe StateNote do
it 'contains the expected values' do
expect(subject.author).to eq(author)
expect(subject.created_at).to eq(event.created_at)
- expect(subject.note).to eq('resolved the corresponding error and closed the issue.')
+ expect(subject.note).to eq('resolved the corresponding error and closed the issue')
end
end
@@ -65,7 +65,7 @@ RSpec.describe StateNote do
it 'contains the expected values' do
expect(subject.author).to eq(author)
expect(subject.created_at).to eq(event.created_at)
- expect(subject.note).to eq('automatically closed this issue because the alert resolved.')
+ expect(subject.note).to eq('automatically closed this incident because the alert resolved')
end
end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index c2535fd3698..e4f25c79e53 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -2612,6 +2612,12 @@ RSpec.describe User do
it 'returns users with a exact matching name shorter than 3 chars regardless of the casing' do
expect(described_class.search(user3.name.upcase)).to eq([user3])
end
+
+ context 'when use_minimum_char_limit is false' do
+ it 'returns users with a partially matching name' do
+ expect(described_class.search('u', use_minimum_char_limit: false)).to eq([user3, user, user2])
+ end
+ end
end
describe 'email matching' do
@@ -2671,204 +2677,20 @@ RSpec.describe User do
it 'returns users with a exact matching username shorter than 3 chars regardless of the casing' do
expect(described_class.search(user3.username.upcase)).to eq([user3])
end
- end
-
- it 'returns no matches for an empty string' do
- expect(described_class.search('')).to be_empty
- end
-
- it 'returns no matches for nil' do
- expect(described_class.search(nil)).to be_empty
- end
- end
-
- describe '.search_without_secondary_emails' do
- let_it_be(:user) { create(:user, name: 'John Doe', username: 'john.doe', email: 'someone.1@example.com' ) }
- let_it_be(:another_user) { create(:user, name: 'Albert Smith', username: 'albert.smith', email: 'another.2@example.com' ) }
- let_it_be(:email) { create(:email, user: another_user, email: 'alias@example.com') }
-
- it 'returns users with a matching name' do
- expect(described_class.search_without_secondary_emails(user.name)).to eq([user])
- end
-
- it 'returns users with a partially matching name' do
- expect(described_class.search_without_secondary_emails(user.name[0..2])).to eq([user])
- end
-
- it 'returns users with a matching name regardless of the casing' do
- expect(described_class.search_without_secondary_emails(user.name.upcase)).to eq([user])
- end
-
- it 'returns users with a matching email' do
- expect(described_class.search_without_secondary_emails(user.email)).to eq([user])
- end
-
- it 'does not return users with a partially matching email' do
- expect(described_class.search_without_secondary_emails(user.email[1...-1])).to be_empty
- end
-
- it 'returns users with a matching email regardless of the casing' do
- expect(described_class.search_without_secondary_emails(user.email.upcase)).to eq([user])
- end
-
- it 'returns users with a matching username' do
- expect(described_class.search_without_secondary_emails(user.username)).to eq([user])
- end
-
- it 'returns users with a partially matching username' do
- expect(described_class.search_without_secondary_emails(user.username[0..2])).to eq([user])
- end
-
- it 'returns users with a matching username regardless of the casing' do
- expect(described_class.search_without_secondary_emails(user.username.upcase)).to eq([user])
- end
-
- it 'does not return users with a matching whole secondary email' do
- expect(described_class.search_without_secondary_emails(email.email)).not_to include(email.user)
- end
- it 'does not return users with a matching part of secondary email' do
- expect(described_class.search_without_secondary_emails(email.email[1...-1])).to be_empty
- end
-
- it 'returns no matches for an empty string' do
- expect(described_class.search_without_secondary_emails('')).to be_empty
- end
-
- it 'returns no matches for nil' do
- expect(described_class.search_without_secondary_emails(nil)).to be_empty
- end
- end
-
- describe '.search_with_public_emails' do
- let_it_be(:user) { create(:user, name: 'John Doe', username: 'john.doe', email: 'someone.1@example.com' ) }
- let_it_be(:another_user) { create(:user, name: 'Albert Smith', username: 'albert.smith', email: 'another.2@example.com' ) }
- let_it_be(:public_email) do
- create(:email, :confirmed, user: another_user, email: 'alias@example.com').tap do |email|
- another_user.update!(public_email: email.email)
+ context 'when use_minimum_char_limit is false' do
+ it 'returns users with a partially matching username' do
+ expect(described_class.search('se', use_minimum_char_limit: false)).to eq([user3, user, user2])
+ end
end
end
- let_it_be(:secondary_email) do
- create(:email, :confirmed, user: another_user, email: 'secondary@example.com')
- end
-
- it 'returns users with a matching name' do
- expect(described_class.search_with_public_emails(user.name)).to match_array([user])
- end
-
- it 'returns users with a partially matching name' do
- expect(described_class.search_with_public_emails(user.name[0..2])).to match_array([user])
- end
-
- it 'returns users with a matching name regardless of the casing' do
- expect(described_class.search_with_public_emails(user.name.upcase)).to match_array([user])
- end
-
- it 'returns users with a matching public email' do
- expect(described_class.search_with_public_emails(another_user.public_email)).to match_array([another_user])
- end
-
- it 'does not return users with a partially matching email' do
- expect(described_class.search_with_public_emails(another_user.public_email[1...-1])).to be_empty
- end
-
- it 'returns users with a matching email regardless of the casing' do
- expect(described_class.search_with_public_emails(another_user.public_email.upcase)).to match_array([another_user])
- end
-
- it 'returns users with a matching username' do
- expect(described_class.search_with_public_emails(user.username)).to match_array([user])
- end
-
- it 'returns users with a partially matching username' do
- expect(described_class.search_with_public_emails(user.username[0..2])).to match_array([user])
- end
-
- it 'returns users with a matching username regardless of the casing' do
- expect(described_class.search_with_public_emails(user.username.upcase)).to match_array([user])
- end
-
- it 'does not return users with a matching whole private email' do
- expect(described_class.search_with_public_emails(user.email)).not_to include(user)
- end
-
- it 'does not return users with a matching whole private email' do
- expect(described_class.search_with_public_emails(secondary_email.email)).to be_empty
- end
-
- it 'does not return users with a matching part of secondary email' do
- expect(described_class.search_with_public_emails(secondary_email.email[1...-1])).to be_empty
- end
-
- it 'does not return users with a matching part of private email' do
- expect(described_class.search_with_public_emails(user.email[1...-1])).to be_empty
- end
-
- it 'returns no matches for an empty string' do
- expect(described_class.search_with_public_emails('')).to be_empty
- end
-
- it 'returns no matches for nil' do
- expect(described_class.search_with_public_emails(nil)).to be_empty
- end
- end
-
- describe '.search_with_secondary_emails' do
- let_it_be(:user) { create(:user, name: 'John Doe', username: 'john.doe', email: 'someone.1@example.com' ) }
- let_it_be(:another_user) { create(:user, name: 'Albert Smith', username: 'albert.smith', email: 'another.2@example.com' ) }
- let_it_be(:email) { create(:email, user: another_user, email: 'alias@example.com') }
-
- it 'returns users with a matching name' do
- expect(described_class.search_with_secondary_emails(user.name)).to eq([user])
- end
-
- it 'returns users with a partially matching name' do
- expect(described_class.search_with_secondary_emails(user.name[0..2])).to eq([user])
- end
-
- it 'returns users with a matching name regardless of the casing' do
- expect(described_class.search_with_secondary_emails(user.name.upcase)).to eq([user])
- end
-
- it 'returns users with a matching email' do
- expect(described_class.search_with_secondary_emails(user.email)).to eq([user])
- end
-
- it 'does not return users with a partially matching email' do
- expect(described_class.search_with_secondary_emails(user.email[1...-1])).to be_empty
- end
-
- it 'returns users with a matching email regardless of the casing' do
- expect(described_class.search_with_secondary_emails(user.email.upcase)).to eq([user])
- end
-
- it 'returns users with a matching username' do
- expect(described_class.search_with_secondary_emails(user.username)).to eq([user])
- end
-
- it 'returns users with a partially matching username' do
- expect(described_class.search_with_secondary_emails(user.username[0..2])).to eq([user])
- end
-
- it 'returns users with a matching username regardless of the casing' do
- expect(described_class.search_with_secondary_emails(user.username.upcase)).to eq([user])
- end
-
- it 'returns users with a matching whole secondary email' do
- expect(described_class.search_with_secondary_emails(email.email)).to eq([email.user])
- end
-
- it 'does not return users with a matching part of secondary email' do
- expect(described_class.search_with_secondary_emails(email.email[1...-1])).to be_empty
- end
-
it 'returns no matches for an empty string' do
- expect(described_class.search_with_secondary_emails('')).to be_empty
+ expect(described_class.search('')).to be_empty
end
it 'returns no matches for nil' do
- expect(described_class.search_with_secondary_emails(nil)).to be_empty
+ expect(described_class.search(nil)).to be_empty
end
end
@@ -3683,13 +3505,16 @@ RSpec.describe User do
let!(:project1) { create(:project) }
let!(:project2) { fork_project(project3) }
let!(:project3) { create(:project) }
+ let!(:project_aimed_for_deletion) { create(:project, marked_for_deletion_at: 2.days.ago, pending_delete: false) }
let!(:merge_request) { create(:merge_request, source_project: project2, target_project: project3, author: subject) }
let!(:push_event) { create(:push_event, project: project1, author: subject) }
let!(:merge_event) { create(:event, :created, project: project3, target: merge_request, author: subject) }
+ let!(:merge_event_2) { create(:event, :created, project: project_aimed_for_deletion, target: merge_request, author: subject) }
before do
project1.add_maintainer(subject)
project2.add_maintainer(subject)
+ project_aimed_for_deletion.add_maintainer(subject)
end
it 'includes IDs for projects the user has pushed to' do
@@ -3703,6 +3528,10 @@ RSpec.describe User do
it "doesn't include IDs for unrelated projects" do
expect(subject.contributed_projects).not_to include(project2)
end
+
+ it "doesn't include projects aimed for deletion" do
+ expect(subject.contributed_projects).not_to include(project_aimed_for_deletion)
+ end
end
describe '#fork_of' do
@@ -4365,6 +4194,8 @@ RSpec.describe User do
context 'when FF ci_owned_runners_cross_joins_fix is disabled' do
before do
+ skip_if_multiple_databases_are_setup
+
stub_feature_flags(ci_owned_runners_cross_joins_fix: false)
end
diff --git a/spec/models/work_item_spec.rb b/spec/models/work_item_spec.rb
new file mode 100644
index 00000000000..2fa1abda44a
--- /dev/null
+++ b/spec/models/work_item_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe WorkItem do
+ describe '#noteable_target_type_name' do
+ it 'returns `issue` as the target name' do
+ work_item = build(:work_item)
+
+ expect(work_item.noteable_target_type_name).to eq('issue')
+ end
+ end
+end
diff --git a/spec/policies/ci/pipeline_policy_spec.rb b/spec/policies/ci/pipeline_policy_spec.rb
index 9a65823c950..b68bb966820 100644
--- a/spec/policies/ci/pipeline_policy_spec.rb
+++ b/spec/policies/ci/pipeline_policy_spec.rb
@@ -89,7 +89,7 @@ RSpec.describe Ci::PipelinePolicy, :models do
let(:project) { create(:project, :public) }
context 'when user has owner access' do
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
it 'is enabled' do
expect(policy).to be_allowed :destroy_pipeline
@@ -107,7 +107,7 @@ RSpec.describe Ci::PipelinePolicy, :models do
let(:project) { create(:project, :public) }
context 'when user has owner access' do
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
it 'is enabled' do
expect(policy).to be_allowed :read_pipeline_variable
@@ -129,7 +129,7 @@ RSpec.describe Ci::PipelinePolicy, :models do
end
context 'when user is developer and it is not the creator of the pipeline' do
- let(:pipeline) { create(:ci_empty_pipeline, project: project, user: project.owner) }
+ let(:pipeline) { create(:ci_empty_pipeline, project: project, user: project.first_owner) }
before do
project.add_developer(user)
diff --git a/spec/policies/clusters/agent_token_policy_spec.rb b/spec/policies/clusters/agent_token_policy_spec.rb
index 9ae99e66f59..f5ac8bd67e6 100644
--- a/spec/policies/clusters/agent_token_policy_spec.rb
+++ b/spec/policies/clusters/agent_token_policy_spec.rb
@@ -10,13 +10,22 @@ RSpec.describe Clusters::AgentTokenPolicy do
let(:project) { token.agent.project }
describe 'rules' do
+ context 'when reporter' do
+ before do
+ project.add_reporter(user)
+ end
+
+ it { expect(policy).to be_disallowed :admin_cluster }
+ it { expect(policy).to be_disallowed :read_cluster }
+ end
+
context 'when developer' do
before do
project.add_developer(user)
end
it { expect(policy).to be_disallowed :admin_cluster }
- it { expect(policy).to be_disallowed :read_cluster }
+ it { expect(policy).to be_allowed :read_cluster }
end
context 'when maintainer' do
diff --git a/spec/policies/clusters/agents/activity_event_policy_spec.rb b/spec/policies/clusters/agents/activity_event_policy_spec.rb
index 1262fcfd9f2..365168de79f 100644
--- a/spec/policies/clusters/agents/activity_event_policy_spec.rb
+++ b/spec/policies/clusters/agents/activity_event_policy_spec.rb
@@ -10,13 +10,22 @@ RSpec.describe Clusters::Agents::ActivityEventPolicy do
let(:project) { event.agent.project }
describe 'rules' do
+ context 'reporter' do
+ before do
+ project.add_reporter(user)
+ end
+
+ it { expect(policy).to be_disallowed :admin_cluster }
+ it { expect(policy).to be_disallowed :read_cluster }
+ end
+
context 'developer' do
before do
project.add_developer(user)
end
it { expect(policy).to be_disallowed :admin_cluster }
- it { expect(policy).to be_disallowed :read_cluster }
+ it { expect(policy).to be_allowed :read_cluster }
end
context 'maintainer' do
diff --git a/spec/policies/namespaces/project_namespace_policy_spec.rb b/spec/policies/namespaces/project_namespace_policy_spec.rb
index f6fe4ae552a..f1022747fab 100644
--- a/spec/policies/namespaces/project_namespace_policy_spec.rb
+++ b/spec/policies/namespaces/project_namespace_policy_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe Namespaces::ProjectNamespacePolicy do
end
context 'parent owner' do
- let_it_be(:current_user) { parent.owner }
+ let_it_be(:current_user) { parent.first_owner }
it { is_expected.to be_disallowed(*permissions) }
end
diff --git a/spec/policies/project_member_policy_spec.rb b/spec/policies/project_member_policy_spec.rb
index aebbe685bb3..12b3e60fdb2 100644
--- a/spec/policies/project_member_policy_spec.rb
+++ b/spec/policies/project_member_policy_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe ProjectMemberPolicy do
end
context 'when user is project owner' do
- let(:member_user) { project.owner }
+ let(:member_user) { project.first_owner }
let(:member) { project.members.find_by!(user: member_user) }
it { is_expected.to be_allowed(:read_project) }
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index 38e4e18c894..793b1fffd5f 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -117,7 +117,7 @@ RSpec.describe ProjectPolicy do
end
describe 'for unconfirmed user' do
- let(:current_user) { project.owner.tap { |u| u.update!(confirmed_at: nil) } }
+ let(:current_user) { project.first_owner.tap { |u| u.update!(confirmed_at: nil) } }
it 'disallows to modify pipelines' do
expect_disallowed(:create_pipeline)
@@ -144,7 +144,7 @@ RSpec.describe ProjectPolicy do
end
describe 'for project owner' do
- let(:current_user) { project.owner }
+ let(:current_user) { project.first_owner }
it 'allows :destroy_pipeline' do
expect(current_user.can?(:destroy_pipeline, pipeline)).to be_truthy
diff --git a/spec/presenters/blob_presenter_spec.rb b/spec/presenters/blob_presenter_spec.rb
index 3bf592ed2b9..225386d9596 100644
--- a/spec/presenters/blob_presenter_spec.rb
+++ b/spec/presenters/blob_presenter_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe BlobPresenter do
let_it_be(:project) { create(:project, :repository) }
- let_it_be(:user) { project.owner }
+ let_it_be(:user) { project.first_owner }
let(:repository) { project.repository }
let(:blob) { repository.blob_at('HEAD', 'files/ruby/regex.rb') }
@@ -87,6 +87,33 @@ RSpec.describe BlobPresenter do
it { expect(presenter.permalink_path).to eq("/#{project.full_path}/-/blob/#{project.repository.commit.sha}/files/ruby/regex.rb") }
end
+ context 'environment has been deployed' do
+ let(:external_url) { "https://some.environment" }
+ let(:environment) { create(:environment, project: project, external_url: external_url) }
+ let!(:deployment) { create(:deployment, :success, environment: environment, project: project, sha: blob.commit_id) }
+
+ before do
+ allow(project).to receive(:public_path_for_source_path).with(blob.path, blob.commit_id).and_return(blob.path)
+ end
+
+ describe '#environment_formatted_external_url' do
+ it { expect(presenter.environment_formatted_external_url).to eq("some.environment") }
+ end
+
+ describe '#environment_external_url_for_route_map' do
+ it { expect(presenter.environment_external_url_for_route_map).to eq("#{external_url}/#{blob.path}") }
+ end
+
+ describe 'chooses the latest deployed environment for #environment_formatted_external_url and #environment_external_url_for_route_map' do
+ let(:another_external_url) { "https://another.environment" }
+ let(:another_environment) { create(:environment, project: project, external_url: another_external_url) }
+ let!(:another_deployment) { create(:deployment, :success, environment: another_environment, project: project, sha: blob.commit_id) }
+
+ it { expect(presenter.environment_formatted_external_url).to eq("another.environment") }
+ it { expect(presenter.environment_external_url_for_route_map).to eq("#{another_external_url}/#{blob.path}") }
+ end
+ end
+
describe '#code_owners' do
it { expect(presenter.code_owners).to match_array([]) }
end
@@ -143,13 +170,13 @@ RSpec.describe BlobPresenter do
let(:git_blob) { blob.__getobj__ }
it 'returns highlighted content' do
- expect(Gitlab::Highlight).to receive(:highlight).with('files/ruby/regex.rb', git_blob.data, plain: nil, language: nil)
+ expect(Gitlab::Highlight).to receive(:highlight).with('files/ruby/regex.rb', git_blob.data, plain: nil, language: 'ruby')
presenter.highlight
end
it 'returns plain content when :plain is true' do
- expect(Gitlab::Highlight).to receive(:highlight).with('files/ruby/regex.rb', git_blob.data, plain: true, language: nil)
+ expect(Gitlab::Highlight).to receive(:highlight).with('files/ruby/regex.rb', git_blob.data, plain: true, language: 'ruby')
presenter.highlight(plain: true)
end
@@ -162,7 +189,7 @@ RSpec.describe BlobPresenter do
end
it 'returns limited highlighted content' do
- expect(Gitlab::Highlight).to receive(:highlight).with('files/ruby/regex.rb', "line one\n", plain: nil, language: nil)
+ expect(Gitlab::Highlight).to receive(:highlight).with('files/ruby/regex.rb', "line one\n", plain: nil, language: 'ruby')
presenter.highlight(to: 1)
end
@@ -220,6 +247,36 @@ RSpec.describe BlobPresenter do
end
end
+ describe '#blob_language' do
+ subject { presenter.blob_language }
+
+ it { is_expected.to eq('ruby') }
+
+ context 'gitlab-language contains a match' do
+ before do
+ allow(blob).to receive(:language_from_gitattributes).and_return('cpp')
+ end
+
+ it { is_expected.to eq('cpp') }
+ end
+
+ context 'when blob is ipynb' do
+ let(:blob) { repository.blob_at('f6b7a707', 'files/ipython/markdown-table.ipynb') }
+
+ before do
+ allow(Gitlab::Diff::CustomDiff).to receive(:transformed_for_diff?).and_return(true)
+ end
+
+ it { is_expected.to eq('md') }
+ end
+
+ context 'when blob is binary' do
+ let(:blob) { repository.blob_at('HEAD', 'Gemfile.zip') }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
describe '#raw_plain_data' do
let(:blob) { repository.blob_at('HEAD', file) }
diff --git a/spec/presenters/blobs/unfold_presenter_spec.rb b/spec/presenters/blobs/unfold_presenter_spec.rb
index 4e9f83e8001..14c36461e90 100644
--- a/spec/presenters/blobs/unfold_presenter_spec.rb
+++ b/spec/presenters/blobs/unfold_presenter_spec.rb
@@ -206,6 +206,14 @@ RSpec.describe Blobs::UnfoldPresenter do
end
end
+ context 'when since exceeds number of lines' do
+ let(:params) { { since: 2 } }
+
+ it 'returns an empty list' do
+ expect(subject.lines.size).to eq(0)
+ end
+ end
+
context 'when full is true' do
let(:params) { { full: true } }
diff --git a/spec/presenters/clusterable_presenter_spec.rb b/spec/presenters/clusterable_presenter_spec.rb
index d19abd4e4d8..7c2e19728d5 100644
--- a/spec/presenters/clusterable_presenter_spec.rb
+++ b/spec/presenters/clusterable_presenter_spec.rb
@@ -79,6 +79,30 @@ RSpec.describe ClusterablePresenter do
end
end
+ describe '#can_admin_cluster?' do
+ let(:user) { create(:user) }
+
+ subject { described_class.new(clusterable).can_admin_cluster? }
+
+ before do
+ clusterable.add_maintainer(user)
+
+ allow(clusterable).to receive(:current_user).and_return(user)
+ end
+
+ context 'when clusterable is a group' do
+ let(:clusterable) { create(:group) }
+
+ it_behaves_like 'appropriate member permissions'
+ end
+
+ context 'when clusterable is a project' do
+ let(:clusterable) { create(:project, :repository) }
+
+ it_behaves_like 'appropriate member permissions'
+ end
+ end
+
describe '#environments_cluster_path' do
subject { described_class.new(clusterable).environments_cluster_path(cluster) }
diff --git a/spec/presenters/packages/conan/package_presenter_spec.rb b/spec/presenters/packages/conan/package_presenter_spec.rb
index 27ecf32b6f2..d35137cd820 100644
--- a/spec/presenters/packages/conan/package_presenter_spec.rb
+++ b/spec/presenters/packages/conan/package_presenter_spec.rb
@@ -6,6 +6,7 @@ RSpec.describe ::Packages::Conan::PackagePresenter do
let_it_be(:user) { create(:user) }
let_it_be(:package) { create(:conan_package) }
let_it_be(:project) { package.project }
+ let_it_be(:package_file_pending_destruction) { create(:package_file, :pending_destruction, package: package) }
let_it_be(:conan_package_reference) { '123456789'}
let(:params) { { package_scope: :instance } }
@@ -208,22 +209,4 @@ RSpec.describe ::Packages::Conan::PackagePresenter do
end
end
end
-
- # TODO when cleaning up packages_installable_package_files, consider removing this context and
- # add a dummy package file pending destruction on L8
- context 'with package files pending destruction' do
- let_it_be(:package_file_pending_destruction) { create(:package_file, :pending_destruction, package: package) }
-
- subject { presenter.send(:package_files).to_a }
-
- it { is_expected.not_to include(package_file_pending_destruction) }
-
- context 'with packages_installable_package_files disabled' do
- before do
- stub_feature_flags(packages_installable_package_files: false)
- end
-
- it { is_expected.to include(package_file_pending_destruction) }
- end
- end
end
diff --git a/spec/presenters/packages/detail/package_presenter_spec.rb b/spec/presenters/packages/detail/package_presenter_spec.rb
index 4e2645b27ff..71ec3ee2d67 100644
--- a/spec/presenters/packages/detail/package_presenter_spec.rb
+++ b/spec/presenters/packages/detail/package_presenter_spec.rb
@@ -161,14 +161,6 @@ RSpec.describe ::Packages::Detail::PackagePresenter do
subject { presenter.detail_view[:package_files].map { |e| e[:id] } }
it { is_expected.not_to include(package_file_pending_destruction.id) }
-
- context 'with packages_installable_package_files disabled' do
- before do
- stub_feature_flags(packages_installable_package_files: false)
- end
-
- it { is_expected.to include(package_file_pending_destruction.id) }
- end
end
end
end
diff --git a/spec/presenters/packages/npm/package_presenter_spec.rb b/spec/presenters/packages/npm/package_presenter_spec.rb
index 2308f928c92..8b99e6d8605 100644
--- a/spec/presenters/packages/npm/package_presenter_spec.rb
+++ b/spec/presenters/packages/npm/package_presenter_spec.rb
@@ -104,17 +104,6 @@ RSpec.describe ::Packages::Npm::PackagePresenter do
it 'does not return them' do
expect(shasums).not_to include(package_file_pending_destruction.file_sha1)
end
-
- context 'with packages_installable_package_files disabled' do
- before do
- stub_feature_flags(packages_installable_package_files: false)
- package2.package_files.id_not_in(package_file_pending_destruction.id).delete_all
- end
-
- it 'returns them' do
- expect(shasums).to include(package_file_pending_destruction.file_sha1)
- end
- end
end
end
diff --git a/spec/presenters/packages/nuget/package_metadata_presenter_spec.rb b/spec/presenters/packages/nuget/package_metadata_presenter_spec.rb
index 6e99b6bafec..6c56763e719 100644
--- a/spec/presenters/packages/nuget/package_metadata_presenter_spec.rb
+++ b/spec/presenters/packages/nuget/package_metadata_presenter_spec.rb
@@ -29,14 +29,6 @@ RSpec.describe Packages::Nuget::PackageMetadataPresenter do
let_it_be(:package_file_pending_destruction) { create(:package_file, :pending_destruction, package: package, file_name: 'pending_destruction.nupkg') }
it { is_expected.not_to include('pending_destruction.nupkg') }
-
- context 'with packages_installable_package_files disabled' do
- before do
- stub_feature_flags(packages_installable_package_files: false)
- end
-
- it { is_expected.to include('pending_destruction.nupkg') }
- end
end
end
diff --git a/spec/presenters/packages/pypi/package_presenter_spec.rb b/spec/presenters/packages/pypi/package_presenter_spec.rb
index 8a23c0ec3cb..b19abdbc17a 100644
--- a/spec/presenters/packages/pypi/package_presenter_spec.rb
+++ b/spec/presenters/packages/pypi/package_presenter_spec.rb
@@ -59,14 +59,6 @@ RSpec.describe ::Packages::Pypi::PackagePresenter do
let(:project_or_group) { project }
it { is_expected.not_to include(package_file_pending_destruction.file_name)}
-
- context 'with packages_installable_package_files disabled' do
- before do
- stub_feature_flags(packages_installable_package_files: false)
- end
-
- it { is_expected.to include(package_file_pending_destruction.file_name)}
- end
end
end
end
diff --git a/spec/presenters/projects/security/configuration_presenter_spec.rb b/spec/presenters/projects/security/configuration_presenter_spec.rb
index f9150179ae5..5f874ab5a3f 100644
--- a/spec/presenters/projects/security/configuration_presenter_spec.rb
+++ b/spec/presenters/projects/security/configuration_presenter_spec.rb
@@ -86,8 +86,9 @@ RSpec.describe Projects::Security::ConfigurationPresenter do
expect(feature['type']).to eq('sast')
expect(feature['configured']).to eq(true)
- expect(feature['configuration_path']).to eq(project_security_configuration_sast_path(project))
+ expect(feature['configuration_path']).to be_nil
expect(feature['available']).to eq(true)
+ expect(feature['can_enable_by_merge_request']).to eq(true)
end
context 'when checking features configured status' do
diff --git a/spec/controllers/abuse_reports_controller_spec.rb b/spec/requests/abuse_reports_controller_spec.rb
index 11371108375..94c80ccb89a 100644
--- a/spec/controllers/abuse_reports_controller_spec.rb
+++ b/spec/requests/abuse_reports_controller_spec.rb
@@ -21,7 +21,7 @@ RSpec.describe AbuseReportsController do
user_id = user.id
user.destroy!
- get :new, params: { user_id: user_id }
+ get new_abuse_report_path(user_id: user_id)
expect(response).to redirect_to root_path
expect(flash[:alert]).to eq(_('Cannot create the abuse report. The user has been deleted.'))
@@ -32,7 +32,7 @@ RSpec.describe AbuseReportsController do
it 'redirects the reporter to the user\'s profile' do
user.block
- get :new, params: { user_id: user.id }
+ get new_abuse_report_path(user_id: user.id)
expect(response).to redirect_to user
expect(flash[:alert]).to eq(_('Cannot create the abuse report. This user has been blocked.'))
@@ -44,7 +44,7 @@ RSpec.describe AbuseReportsController do
context 'with valid attributes' do
it 'saves the abuse report' do
expect do
- post :create, params: { abuse_report: attrs }
+ post abuse_reports_path(abuse_report: attrs)
end.to change { AbuseReport.count }.by(1)
end
@@ -53,22 +53,22 @@ RSpec.describe AbuseReportsController do
expect(instance).to receive(:notify)
end
- post :create, params: { abuse_report: attrs }
+ post abuse_reports_path(abuse_report: attrs)
end
it 'redirects back to root' do
- post :create, params: { abuse_report: attrs }
+ post abuse_reports_path(abuse_report: attrs)
expect(response).to redirect_to root_path
end
end
context 'with invalid attributes' do
- it 'renders new' do
+ it 'redirects back to root' do
attrs.delete(:user_id)
- post :create, params: { abuse_report: attrs }
+ post abuse_reports_path(abuse_report: attrs)
- expect(response).to render_template(:new)
+ expect(response).to redirect_to root_path
end
end
end
diff --git a/spec/requests/admin/background_migrations_controller_spec.rb b/spec/requests/admin/background_migrations_controller_spec.rb
index c7d5d5cae08..67c9c4df827 100644
--- a/spec/requests/admin/background_migrations_controller_spec.rb
+++ b/spec/requests/admin/background_migrations_controller_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe Admin::BackgroundMigrationsController, :enable_admin_mode do
let(:migration) { create(:batched_background_migration, status: 'failed') }
before do
- create(:batched_background_migration_job, batched_migration: migration, batch_size: 10, min_value: 6, max_value: 15, status: :failed, attempts: 3)
+ create(:batched_background_migration_job, :failed, batched_migration: migration, batch_size: 10, min_value: 6, max_value: 15, attempts: 3)
allow_next_instance_of(Gitlab::BackgroundMigration::BatchingStrategies::PrimaryKeyBatchingStrategy) do |batch_class|
allow(batch_class).to receive(:next_batch).with(anything, anything, batch_min_value: 6, batch_size: 5).and_return([6, 10])
diff --git a/spec/requests/api/api_spec.rb b/spec/requests/api/api_spec.rb
index 6a02f81fcae..df9be2616c5 100644
--- a/spec/requests/api/api_spec.rb
+++ b/spec/requests/api/api_spec.rb
@@ -102,7 +102,7 @@ RSpec.describe API::API do
describe 'logging', :aggregate_failures do
let_it_be(:project) { create(:project, :public) }
- let_it_be(:user) { project.owner }
+ let_it_be(:user) { project.first_owner }
context 'when the endpoint is handled by the application' do
context 'when the endpoint supports all possible fields' do
diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb
index ad517a05533..780e45cf443 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -188,6 +188,24 @@ RSpec.describe API::Branches do
end
end
+ context 'when sort parameter is passed' do
+ it 'sorts branches' do
+ get api(route, user), params: { sort: 'name_asc', per_page: 10 }
+
+ sorted_branch_names = json_response.map { |branch| branch['name'] }
+
+ project_branch_names = project.repository.branch_names.sort.take(10)
+
+ expect(sorted_branch_names).to eq(project_branch_names)
+ end
+
+ context 'when sort value is not supported' do
+ it_behaves_like '400 response' do
+ let(:request) { get api(route, user), params: { sort: 'unknown' }}
+ end
+ end
+ end
+
context 'when unauthenticated', 'and project is public' do
before do
project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
diff --git a/spec/requests/api/ci/pipelines_spec.rb b/spec/requests/api/ci/pipelines_spec.rb
index 13838cffd76..1b87a5e24f5 100644
--- a/spec/requests/api/ci/pipelines_spec.rb
+++ b/spec/requests/api/ci/pipelines_spec.rb
@@ -988,7 +988,7 @@ RSpec.describe API::Ci::Pipelines do
describe 'DELETE /projects/:id/pipelines/:pipeline_id' do
context 'authorized user' do
- let(:owner) { project.owner }
+ let(:owner) { project.first_owner }
it 'destroys the pipeline' do
delete api("/projects/#{project.id}/pipelines/#{pipeline.id}", owner)
diff --git a/spec/requests/api/ci/runner/runners_post_spec.rb b/spec/requests/api/ci/runner/runners_post_spec.rb
index 530b601add9..5eb5d3977a3 100644
--- a/spec/requests/api/ci/runner/runners_post_spec.rb
+++ b/spec/requests/api/ci/runner/runners_post_spec.rb
@@ -30,11 +30,11 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
post api('/runners'), params: {
token: 'valid token',
description: 'server.hostname',
- maintainer_note: 'Some maintainer notes',
+ maintenance_note: 'Some maintainer notes',
run_untagged: false,
tag_list: 'tag1, tag2',
locked: true,
- active: true,
+ paused: false,
access_level: 'ref_protected',
maximum_timeout: 9000
}
@@ -46,7 +46,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
allow_next_instance_of(::Ci::RegisterRunnerService) do |service|
expected_params = {
description: 'server.hostname',
- maintainer_note: 'Some maintainer notes',
+ maintenance_note: 'Some maintainer notes',
run_untagged: false,
tag_list: %w(tag1 tag2),
locked: true,
@@ -55,19 +55,33 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
maximum_timeout: 9000
}.stringify_keys
- allow(service).to receive(:execute)
+ expect(service).to receive(:execute)
.once
.with('valid token', a_hash_including(expected_params))
.and_return(new_runner)
end
end
- it 'creates runner' do
- request
+ context 'when token_expires_at is nil' do
+ it 'creates runner' do
+ request
- expect(response).to have_gitlab_http_status(:created)
- expect(json_response['id']).to eq(new_runner.id)
- expect(json_response['token']).to eq(new_runner.token)
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response).to eq({ 'id' => new_runner.id, 'token' => new_runner.token, 'token_expires_at' => nil })
+ end
+ end
+
+ context 'when token_expires_at is a valid date' do
+ before do
+ new_runner.token_expires_at = DateTime.new(2022, 1, 11, 14, 39, 24)
+ end
+
+ it 'creates runner' do
+ request
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response).to eq({ 'id' => new_runner.id, 'token' => new_runner.token, 'token_expires_at' => '2022-01-11T14:39:24.000Z' })
+ end
end
it_behaves_like 'storing arguments in the application context for the API' do
@@ -81,6 +95,59 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
end
end
+ context 'when deprecated maintainer_note field is provided' do
+ RSpec::Matchers.define_negated_matcher :excluding, :include
+
+ def request
+ post api('/runners'), params: {
+ token: 'valid token',
+ maintainer_note: 'Some maintainer notes'
+ }
+ end
+
+ let(:new_runner) { create(:ci_runner) }
+
+ it 'converts to maintenance_note param' do
+ allow_next_instance_of(::Ci::RegisterRunnerService) do |service|
+ expect(service).to receive(:execute)
+ .once
+ .with('valid token', a_hash_including('maintenance_note' => 'Some maintainer notes')
+ .and(excluding('maintainter_note' => anything)))
+ .and_return(new_runner)
+ end
+
+ request
+
+ expect(response).to have_gitlab_http_status(:created)
+ end
+ end
+
+ context 'when deprecated active parameter is provided' do
+ def request
+ post api('/runners'), params: {
+ token: 'valid token',
+ active: false
+ }
+ end
+
+ let_it_be(:new_runner) { create(:ci_runner) }
+
+ it 'uses active value in registration' do
+ expect_next_instance_of(::Ci::RegisterRunnerService) do |service|
+ expected_params = { active: false }.stringify_keys
+
+ expect(service).to receive(:execute)
+ .once
+ .with('valid token', a_hash_including(expected_params))
+ .and_return(new_runner)
+ end
+
+ request
+
+ expect(response).to have_gitlab_http_status(:created)
+ end
+ end
+
context 'calling actual register service' do
include StubGitlabCalls
diff --git a/spec/requests/api/ci/runner/runners_verify_post_spec.rb b/spec/requests/api/ci/runner/runners_verify_post_spec.rb
index 4680076acae..038e126deaa 100644
--- a/spec/requests/api/ci/runner/runners_verify_post_spec.rb
+++ b/spec/requests/api/ci/runner/runners_verify_post_spec.rb
@@ -49,6 +49,30 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
let(:expected_params) { { client_id: "runner/#{runner.id}" } }
end
end
+
+ context 'when non-expired token is provided' do
+ subject { post api('/runners/verify'), params: { token: runner.token } }
+
+ it 'verifies Runner credentials' do
+ runner["token_expires_at"] = 10.days.from_now
+ runner.save!
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'when expired token is provided' do
+ subject { post api('/runners/verify'), params: { token: runner.token } }
+
+ it 'does not verify Runner credentials' do
+ runner["token_expires_at"] = 10.days.ago
+ runner.save!
+ subject
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
end
end
end
diff --git a/spec/requests/api/ci/runners_reset_registration_token_spec.rb b/spec/requests/api/ci/runners_reset_registration_token_spec.rb
index df64c0bd22b..e1dc347f8dd 100644
--- a/spec/requests/api/ci/runners_reset_registration_token_spec.rb
+++ b/spec/requests/api/ci/runners_reset_registration_token_spec.rb
@@ -138,7 +138,7 @@ RSpec.describe API::Ci::Runners do
end
include_context 'when authorized', 'project' do
- let_it_be(:user) { project.owner }
+ let_it_be(:user) { project.first_owner }
def get_token
project.reload.runners_token
diff --git a/spec/requests/api/ci/runners_spec.rb b/spec/requests/api/ci/runners_spec.rb
index 305c0bd9df0..336ce70d8d2 100644
--- a/spec/requests/api/ci/runners_spec.rb
+++ b/spec/requests/api/ci/runners_spec.rb
@@ -86,14 +86,24 @@ RSpec.describe API::Ci::Runners do
expect(response).to have_gitlab_http_status(:bad_request)
end
- it 'filters runners by status' do
- create(:ci_runner, :project, :inactive, description: 'Inactive project runner', projects: [project])
+ context 'with an inactive runner' do
+ let_it_be(:runner) { create(:ci_runner, :project, :inactive, description: 'Inactive project runner', projects: [project]) }
- get api('/runners?status=paused', user)
+ it 'filters runners by paused state' do
+ get api('/runners?paused=true', user)
- expect(json_response).to match_array [
- a_hash_including('description' => 'Inactive project runner')
- ]
+ expect(json_response).to match_array [
+ a_hash_including('description' => 'Inactive project runner')
+ ]
+ end
+
+ it 'filters runners by status' do
+ get api('/runners?status=paused', user)
+
+ expect(json_response).to match_array [
+ a_hash_including('description' => 'Inactive project runner')
+ ]
+ end
end
it 'does not filter by invalid status' do
@@ -109,7 +119,7 @@ RSpec.describe API::Ci::Runners do
get api('/runners?tag_list=tag1,tag2', user)
expect(json_response).to match_array [
- a_hash_including('description' => 'Runner tagged with tag1 and tag2')
+ a_hash_including('description' => 'Runner tagged with tag1 and tag2', 'active' => true, 'paused' => false)
]
end
end
@@ -137,7 +147,7 @@ RSpec.describe API::Ci::Runners do
get api('/runners/all', admin)
expect(json_response).to match_array [
- a_hash_including('description' => 'Project runner', 'is_shared' => false, 'runner_type' => 'project_type'),
+ a_hash_including('description' => 'Project runner', 'is_shared' => false, 'active' => true, 'paused' => false, 'runner_type' => 'project_type'),
a_hash_including('description' => 'Two projects runner', 'is_shared' => false, 'runner_type' => 'project_type'),
a_hash_including('description' => 'Group runner A', 'is_shared' => false, 'runner_type' => 'group_type'),
a_hash_including('description' => 'Group runner B', 'is_shared' => false, 'runner_type' => 'group_type'),
@@ -199,14 +209,24 @@ RSpec.describe API::Ci::Runners do
expect(response).to have_gitlab_http_status(:bad_request)
end
- it 'filters runners by status' do
- create(:ci_runner, :project, :inactive, description: 'Inactive project runner', projects: [project])
+ context 'with an inactive runner' do
+ let_it_be(:runner) { create(:ci_runner, :project, :inactive, description: 'Inactive project runner', projects: [project]) }
- get api('/runners/all?status=paused', admin)
+ it 'filters runners by status' do
+ get api('/runners/all?paused=true', admin)
- expect(json_response).to match_array [
- a_hash_including('description' => 'Inactive project runner')
- ]
+ expect(json_response).to match_array [
+ a_hash_including('description' => 'Inactive project runner')
+ ]
+ end
+
+ it 'filters runners by status' do
+ get api('/runners/all?status=paused', admin)
+
+ expect(json_response).to match_array [
+ a_hash_including('description' => 'Inactive project runner')
+ ]
+ end
end
it 'does not filter by invalid status' do
@@ -255,6 +275,8 @@ RSpec.describe API::Ci::Runners do
expect(json_response['description']).to eq(shared_runner.description)
expect(json_response['maximum_timeout']).to be_nil
expect(json_response['status']).to eq("not_connected")
+ expect(json_response['active']).to eq(true)
+ expect(json_response['paused']).to eq(false)
end
end
@@ -359,6 +381,14 @@ RSpec.describe API::Ci::Runners do
expect(shared_runner.reload.active).to eq(!active)
end
+ it 'runner paused state' do
+ active = shared_runner.active
+ update_runner(shared_runner.id, admin, paused: active)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(shared_runner.reload.active).to eq(!active)
+ end
+
it 'runner tag list' do
update_runner(shared_runner.id, admin, tag_list: ['ruby2.1', 'pgsql', 'mysql'])
@@ -500,6 +530,10 @@ RSpec.describe API::Ci::Runners do
context 'admin user' do
context 'when runner is shared' do
it 'deletes runner' do
+ expect_next_instance_of(Ci::UnregisterRunnerService, shared_runner) do |service|
+ expect(service).to receive(:execute).once.and_call_original
+ end
+
expect do
delete api("/runners/#{shared_runner.id}", admin)
@@ -514,6 +548,10 @@ RSpec.describe API::Ci::Runners do
context 'when runner is not shared' do
it 'deletes used project runner' do
+ expect_next_instance_of(Ci::UnregisterRunnerService, project_runner) do |service|
+ expect(service).to receive(:execute).once.and_call_original
+ end
+
expect do
delete api("/runners/#{project_runner.id}", admin)
@@ -523,6 +561,10 @@ RSpec.describe API::Ci::Runners do
end
it 'returns 404 if runner does not exist' do
+ allow_next_instance_of(Ci::UnregisterRunnerService) do |service|
+ expect(service).not_to receive(:execute)
+ end
+
delete api('/runners/0', admin)
expect(response).to have_gitlab_http_status(:not_found)
@@ -604,6 +646,10 @@ RSpec.describe API::Ci::Runners do
context 'unauthorized user' do
it 'does not delete project runner' do
+ allow_next_instance_of(Ci::UnregisterRunnerService) do |service|
+ expect(service).not_to receive(:execute)
+ end
+
delete api("/runners/#{project_runner.id}")
expect(response).to have_gitlab_http_status(:unauthorized)
@@ -618,7 +664,7 @@ RSpec.describe API::Ci::Runners do
post api("/runners/#{shared_runner.id}/reset_authentication_token", admin)
expect(response).to have_gitlab_http_status(:success)
- expect(json_response).to eq({ 'token' => shared_runner.reload.token })
+ expect(json_response).to eq({ 'token' => shared_runner.reload.token, 'token_expires_at' => nil })
end.to change { shared_runner.reload.token }
end
@@ -642,7 +688,7 @@ RSpec.describe API::Ci::Runners do
post api("/runners/#{project_runner.id}/reset_authentication_token", user)
expect(response).to have_gitlab_http_status(:success)
- expect(json_response).to eq({ 'token' => project_runner.reload.token })
+ expect(json_response).to eq({ 'token' => project_runner.reload.token, 'token_expires_at' => nil })
end.to change { project_runner.reload.token }
end
@@ -683,7 +729,22 @@ RSpec.describe API::Ci::Runners do
post api("/runners/#{group_runner_a.id}/reset_authentication_token", user)
expect(response).to have_gitlab_http_status(:success)
- expect(json_response).to eq({ 'token' => group_runner_a.reload.token })
+ expect(json_response).to eq({ 'token' => group_runner_a.reload.token, 'token_expires_at' => nil })
+ end.to change { group_runner_a.reload.token }
+ end
+
+ it 'resets group runner authentication token with owner access with expiration time', :freeze_time do
+ expect(group_runner_a.reload.token_expires_at).to be_nil
+
+ group.update!(runner_token_expiration_interval: 5.days)
+
+ expect do
+ post api("/runners/#{group_runner_a.id}/reset_authentication_token", user)
+ group_runner_a.reload
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(json_response).to eq({ 'token' => group_runner_a.token, 'token_expires_at' => group_runner_a.token_expires_at.iso8601(3) })
+ expect(group_runner_a.token_expires_at).to eq(5.days.from_now)
end.to change { group_runner_a.reload.token }
end
end
@@ -908,9 +969,9 @@ RSpec.describe API::Ci::Runners do
get api("/projects/#{project.id}/runners", user)
expect(json_response).to match_array [
- a_hash_including('description' => 'Project runner'),
- a_hash_including('description' => 'Two projects runner'),
- a_hash_including('description' => 'Shared runner')
+ a_hash_including('description' => 'Project runner', 'active' => true, 'paused' => false),
+ a_hash_including('description' => 'Two projects runner', 'active' => true, 'paused' => false),
+ a_hash_including('description' => 'Shared runner', 'active' => true, 'paused' => false)
]
end
@@ -946,14 +1007,24 @@ RSpec.describe API::Ci::Runners do
expect(response).to have_gitlab_http_status(:bad_request)
end
- it 'filters runners by status' do
- create(:ci_runner, :project, :inactive, description: 'Inactive project runner', projects: [project])
+ context 'with an inactive runner' do
+ let_it_be(:runner) { create(:ci_runner, :project, :inactive, description: 'Inactive project runner', projects: [project]) }
- get api("/projects/#{project.id}/runners?status=paused", user)
+ it 'filters runners by status' do
+ get api("/projects/#{project.id}/runners?paused=true", user)
- expect(json_response).to match_array [
- a_hash_including('description' => 'Inactive project runner')
- ]
+ expect(json_response).to match_array [
+ a_hash_including('description' => 'Inactive project runner')
+ ]
+ end
+
+ it 'filters runners by status' do
+ get api("/projects/#{project.id}/runners?status=paused", user)
+
+ expect(json_response).to match_array [
+ a_hash_including('description' => 'Inactive project runner')
+ ]
+ end
end
it 'does not filter by invalid status' do
@@ -980,13 +1051,14 @@ RSpec.describe API::Ci::Runners do
end
end
- shared_context 'GET /groups/:id/runners' do
+ describe 'GET /groups/:id/runners' do
context 'authorized user with maintainer privileges' do
it 'returns all runners' do
get api("/groups/#{group.id}/runners", user)
expect(json_response).to match_array([
- a_hash_including('description' => 'Group runner A')
+ a_hash_including('description' => 'Group runner A', 'active' => true, 'paused' => false),
+ a_hash_including('description' => 'Shared runner', 'active' => true, 'paused' => false)
])
end
@@ -999,6 +1071,15 @@ RSpec.describe API::Ci::Runners do
])
end
+ it 'returns instance runners when instance_type is specified' do
+ get api("/groups/#{group.id}/runners?type=instance_type", user)
+
+ expect(json_response).to match_array([
+ a_hash_including('description' => 'Shared runner')
+ ])
+ end
+
+ # TODO: Remove in %15.0 (https://gitlab.com/gitlab-org/gitlab/-/issues/351466)
it 'returns empty result when type does not match' do
get api("/groups/#{group.id}/runners?type=project_type", user)
@@ -1012,21 +1093,31 @@ RSpec.describe API::Ci::Runners do
end
end
- context 'filter runners by status' do
- it 'returns runners by valid status' do
- create(:ci_runner, :group, :inactive, description: 'Inactive group runner', groups: [group])
+ context 'with an inactive runner' do
+ let_it_be(:runner) { create(:ci_runner, :group, :inactive, description: 'Inactive group runner', groups: [group]) }
- get api("/groups/#{group.id}/runners?status=paused", user)
+ it 'returns runners by paused state' do
+ get api("/groups/#{group.id}/runners?paused=true", user)
expect(json_response).to match_array([
a_hash_including('description' => 'Inactive group runner')
])
end
- it 'does not filter by invalid status' do
- get api("/groups/#{group.id}/runners?status=bogus", user)
+ context 'filter runners by status' do
+ it 'returns runners by valid status' do
+ get api("/groups/#{group.id}/runners?status=paused", user)
- expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response).to match_array([
+ a_hash_including('description' => 'Inactive group runner')
+ ])
+ end
+
+ it 'does not filter by invalid status' do
+ get api("/groups/#{group.id}/runners?status=bogus", user)
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
end
end
@@ -1048,16 +1139,6 @@ RSpec.describe API::Ci::Runners do
end
end
- it_behaves_like 'GET /groups/:id/runners'
-
- context 'when the FF ci_find_runners_by_ci_mirrors is disabled' do
- before do
- stub_feature_flags(ci_find_runners_by_ci_mirrors: false)
- end
-
- it_behaves_like 'GET /groups/:id/runners'
- end
-
describe 'POST /projects/:id/runners' do
context 'authorized user' do
let_it_be(:project_runner2) { create(:ci_runner, :project, projects: [project2]) }
diff --git a/spec/requests/api/ci/secure_files_spec.rb b/spec/requests/api/ci/secure_files_spec.rb
new file mode 100644
index 00000000000..5cf6999f60a
--- /dev/null
+++ b/spec/requests/api/ci/secure_files_spec.rb
@@ -0,0 +1,314 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::Ci::SecureFiles do
+ before do
+ stub_ci_secure_file_object_storage
+ stub_feature_flags(ci_secure_files: true)
+ end
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:user2) { create(:user) }
+ let_it_be(:project) { create(:project, creator_id: user.id) }
+ let_it_be(:maintainer) { create(:project_member, :maintainer, user: user, project: project) }
+ let_it_be(:developer) { create(:project_member, :developer, user: user2, project: project) }
+ let_it_be(:secure_file) { create(:ci_secure_file, project: project) }
+
+ describe 'GET /projects/:id/secure_files' do
+ context 'feature flag' do
+ it 'returns a 503 when the feature flag is disabled' do
+ stub_feature_flags(ci_secure_files: false)
+
+ get api("/projects/#{project.id}/secure_files", user)
+
+ expect(response).to have_gitlab_http_status(:service_unavailable)
+ end
+
+ it 'returns a 200 when the feature flag is enabled' do
+ get api("/projects/#{project.id}/secure_files", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_a(Array)
+ end
+ end
+
+ context 'authorized user with proper permissions' do
+ it 'returns project secure files' do
+ get api("/projects/#{project.id}/secure_files", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_a(Array)
+ end
+ end
+
+ context 'authorized user with invalid permissions' do
+ it 'does not return project secure files' do
+ get api("/projects/#{project.id}/secure_files", user2)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'does not return project secure files' do
+ get api("/projects/#{project.id}/secure_files")
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/secure_files/:secure_file_id' do
+ context 'authorized user with proper permissions' do
+ it 'returns project secure file details' do
+ get api("/projects/#{project.id}/secure_files/#{secure_file.id}", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['name']).to eq(secure_file.name)
+ expect(json_response['permissions']).to eq(secure_file.permissions)
+ end
+
+ it 'responds with 404 Not Found if requesting non-existing secure file' do
+ get api("/projects/#{project.id}/secure_files/99999", user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'authorized user with invalid permissions' do
+ it 'does not return project secure file details' do
+ get api("/projects/#{project.id}/secure_files/#{secure_file.id}", user2)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'does not return project secure file details' do
+ get api("/projects/#{project.id}/secure_files/#{secure_file.id}")
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/secure_files/:secure_file_id/download' do
+ context 'authorized user with proper permissions' do
+ it 'returns a secure file' do
+ sample_file = fixture_file('ci_secure_files/upload-keystore.jks')
+ secure_file.file = CarrierWaveStringFile.new(sample_file)
+ secure_file.save!
+
+ get api("/projects/#{project.id}/secure_files/#{secure_file.id}/download", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(Base64.encode64(response.body)).to eq(Base64.encode64(sample_file))
+ end
+
+ it 'responds with 404 Not Found if requesting non-existing secure file' do
+ get api("/projects/#{project.id}/secure_files/99999/download", user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'authorized user with invalid permissions' do
+ it 'does not return project secure file details' do
+ get api("/projects/#{project.id}/secure_files/#{secure_file.id}/download", user2)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'does not return project secure file details' do
+ get api("/projects/#{project.id}/secure_files/#{secure_file.id}/download")
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/secure_files' do
+ context 'authorized user with proper permissions' do
+ it 'creates a secure file' do
+ params = {
+ file: fixture_file_upload('spec/fixtures/ci_secure_files/upload-keystore.jks'),
+ name: 'upload-keystore.jks',
+ permissions: 'execute'
+ }
+
+ expect do
+ post api("/projects/#{project.id}/secure_files", user), params: params
+ end.to change {project.secure_files.count}.by(1)
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response['name']).to eq('upload-keystore.jks')
+ expect(json_response['permissions']).to eq('execute')
+ expect(json_response['checksum']).to eq(secure_file.checksum)
+ expect(json_response['checksum_algorithm']).to eq('sha256')
+
+ secure_file = Ci::SecureFile.find(json_response['id'])
+ expect(secure_file.checksum).to eq(
+ Digest::SHA256.hexdigest(fixture_file('ci_secure_files/upload-keystore.jks'))
+ )
+ expect(json_response['id']).to eq(secure_file.id)
+ end
+
+ it 'creates a secure file with read_only permissions by default' do
+ params = {
+ file: fixture_file_upload('spec/fixtures/ci_secure_files/upload-keystore.jks'),
+ name: 'upload-keystore.jks'
+ }
+
+ expect do
+ post api("/projects/#{project.id}/secure_files", user), params: params
+ end.to change {project.secure_files.count}.by(1)
+
+ expect(json_response['permissions']).to eq('read_only')
+ end
+
+ it 'uploads and downloads a secure file' do
+ post_params = {
+ file: fixture_file_upload('spec/fixtures/ci_secure_files/upload-keystore.jks'),
+ name: 'upload-keystore.jks',
+ permissions: 'read_write'
+ }
+
+ post api("/projects/#{project.id}/secure_files", user), params: post_params
+
+ secure_file_id = json_response['id']
+
+ get api("/projects/#{project.id}/secure_files/#{secure_file_id}/download", user)
+
+ expect(Base64.encode64(response.body)).to eq(Base64.encode64(fixture_file_upload('spec/fixtures/ci_secure_files/upload-keystore.jks').read))
+ end
+
+ it 'returns an error when the file checksum fails to validate' do
+ secure_file.update!(checksum: 'foo')
+
+ get api("/projects/#{project.id}/secure_files/#{secure_file.id}/download", user)
+
+ expect(response.code).to eq("500")
+ end
+
+ it 'returns an error when no file is uploaded' do
+ post_params = {
+ name: 'upload-keystore.jks'
+ }
+
+ post api("/projects/#{project.id}/secure_files", user), params: post_params
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error']).to eq('file is missing')
+ end
+
+ it 'returns an error when the file name is missing' do
+ post_params = {
+ file: fixture_file_upload('spec/fixtures/ci_secure_files/upload-keystore.jks')
+ }
+
+ post api("/projects/#{project.id}/secure_files", user), params: post_params
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error']).to eq('name is missing')
+ end
+
+ it 'returns an error when an unexpected permission is supplied' do
+ post_params = {
+ file: fixture_file_upload('spec/fixtures/ci_secure_files/upload-keystore.jks'),
+ name: 'upload-keystore.jks',
+ permissions: 'foo'
+ }
+
+ post api("/projects/#{project.id}/secure_files", user), params: post_params
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error']).to eq('permissions does not have a valid value')
+ end
+
+ it 'returns an error when an unexpected validation failure happens' do
+ allow_next_instance_of(Ci::SecureFile) do |instance|
+ allow(instance).to receive(:valid?).and_return(false)
+ allow(instance).to receive_message_chain(:errors, :any?).and_return(true)
+ allow(instance).to receive_message_chain(:errors, :messages).and_return(['Error 1', 'Error 2'])
+ end
+
+ post_params = {
+ file: fixture_file_upload('spec/fixtures/ci_secure_files/upload-keystore.jks'),
+ name: 'upload-keystore.jks'
+ }
+
+ post api("/projects/#{project.id}/secure_files", user), params: post_params
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+
+ it 'returns a 413 error when the file size is too large' do
+ allow_next_instance_of(Ci::SecureFile) do |instance|
+ allow(instance).to receive_message_chain(:file, :size).and_return(6.megabytes.to_i)
+ end
+
+ post_params = {
+ file: fixture_file_upload('spec/fixtures/ci_secure_files/upload-keystore.jks'),
+ name: 'upload-keystore.jks'
+ }
+
+ post api("/projects/#{project.id}/secure_files", user), params: post_params
+
+ expect(response).to have_gitlab_http_status(:payload_too_large)
+ end
+ end
+
+ context 'authorized user with invalid permissions' do
+ it 'does not create a secure file' do
+ post api("/projects/#{project.id}/secure_files", user2)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'does not create a secure file' do
+ post api("/projects/#{project.id}/secure_files")
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+ end
+
+ describe 'DELETE /projects/:id/secure_files/:secure_file_id' do
+ context 'authorized user with proper permissions' do
+ it 'deletes the secure file' do
+ expect do
+ delete api("/projects/#{project.id}/secure_files/#{secure_file.id}", user)
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ end.to change {project.secure_files.count}.by(-1)
+ end
+
+ it 'responds with 404 Not Found if requesting non-existing secure_file' do
+ delete api("/projects/#{project.id}/secure_files/99999", user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'authorized user with invalid permissions' do
+ it 'does not delete the secure_file' do
+ delete api("/projects/#{project.id}/secure_files/#{secure_file.id}", user2)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'does not delete the secure_file' do
+ delete api("/projects/#{project.id}/secure_files/#{secure_file.id}")
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index 2bc642f8b14..156a4cf5ff3 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -5,6 +5,7 @@ require 'mime/types'
RSpec.describe API::Commits do
include ProjectForksHelper
+ include SessionHelpers
let(:user) { create(:user) }
let(:guest) { create(:user).tap { |u| project.add_guest(u) } }
@@ -227,6 +228,12 @@ RSpec.describe API::Commits do
expect(response.headers['X-Page']).to eq('3')
end
end
+
+ context 'when per_page is 0' do
+ let(:per_page) { 0 }
+
+ it_behaves_like '400 response'
+ end
end
context 'with order parameter' do
@@ -378,14 +385,7 @@ RSpec.describe API::Commits do
context 'when using warden' do
it 'increments usage counters', :clean_gitlab_redis_sessions do
- session_id = Rack::Session::SessionId.new('6919a6f1bb119dd7396fadc38fd18d0d')
- session_hash = { 'warden.user.user.key' => [[user.id], user.encrypted_password[0, 29]] }
-
- Gitlab::Redis::Sessions.with do |redis|
- redis.set("session:gitlab:#{session_id.private_id}", Marshal.dump(session_hash))
- end
-
- cookies[Gitlab::Application.config.session_options[:key]] = session_id.public_id
+ stub_session('warden.user.user.key' => [[user.id], user.encrypted_password[0, 29]])
expect(::Gitlab::UsageDataCounters::WebIdeCounter).to receive(:increment_commits_count)
expect(::Gitlab::UsageDataCounters::EditorUniqueCounter).to receive(:track_web_ide_edit_action)
diff --git a/spec/requests/api/features_spec.rb b/spec/requests/api/features_spec.rb
index 35dba93b766..a265f67115a 100644
--- a/spec/requests/api/features_spec.rb
+++ b/spec/requests/api/features_spec.rb
@@ -167,76 +167,85 @@ RSpec.describe API::Features, stub_feature_flags: false do
end
end
+ shared_examples 'does not enable the flag' do |actor_type, actor_path|
+ it 'returns the current state of the flag without changes' do
+ post api("/features/#{feature_name}", admin), params: { value: 'true', actor_type => actor_path }
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response).to match(
+ "name" => feature_name,
+ "state" => "off",
+ "gates" => [
+ { "key" => "boolean", "value" => false }
+ ],
+ 'definition' => known_feature_flag_definition_hash
+ )
+ end
+ end
+
+ shared_examples 'enables the flag for the actor' do |actor_type|
+ it 'sets the feature gate' do
+ post api("/features/#{feature_name}", admin), params: { value: 'true', actor_type => actor.full_path }
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response).to match(
+ 'name' => feature_name,
+ 'state' => 'conditional',
+ 'gates' => [
+ { 'key' => 'boolean', 'value' => false },
+ { 'key' => 'actors', 'value' => ["#{actor.class}:#{actor.id}"] }
+ ],
+ 'definition' => known_feature_flag_definition_hash
+ )
+ end
+ end
+
context 'when enabling for a project by path' do
context 'when the project exists' do
- let!(:project) { create(:project) }
-
- it 'sets the feature gate' do
- post api("/features/#{feature_name}", admin), params: { value: 'true', project: project.full_path }
-
- expect(response).to have_gitlab_http_status(:created)
- expect(json_response).to match(
- 'name' => feature_name,
- 'state' => 'conditional',
- 'gates' => [
- { 'key' => 'boolean', 'value' => false },
- { 'key' => 'actors', 'value' => ["Project:#{project.id}"] }
- ],
- 'definition' => known_feature_flag_definition_hash
- )
+ it_behaves_like 'enables the flag for the actor', :project do
+ let(:actor) { create(:project) }
end
end
context 'when the project does not exist' do
- it 'sets no new values' do
- post api("/features/#{feature_name}", admin), params: { value: 'true', project: 'mep/to/the/mep/mep' }
-
- expect(response).to have_gitlab_http_status(:created)
- expect(json_response).to match(
- "name" => feature_name,
- "state" => "off",
- "gates" => [
- { "key" => "boolean", "value" => false }
- ],
- 'definition' => known_feature_flag_definition_hash
- )
- end
+ it_behaves_like 'does not enable the flag', :project, 'mep/to/the/mep/mep'
end
end
context 'when enabling for a group by path' do
context 'when the group exists' do
- it 'sets the feature gate' do
- group = create(:group)
-
- post api("/features/#{feature_name}", admin), params: { value: 'true', group: group.full_path }
-
- expect(response).to have_gitlab_http_status(:created)
- expect(json_response).to match(
- 'name' => feature_name,
- 'state' => 'conditional',
- 'gates' => [
- { 'key' => 'boolean', 'value' => false },
- { 'key' => 'actors', 'value' => ["Group:#{group.id}"] }
- ],
- 'definition' => known_feature_flag_definition_hash
- )
+ it_behaves_like 'enables the flag for the actor', :group do
+ let(:actor) { create(:group) }
end
end
context 'when the group does not exist' do
- it 'sets no new values and keeps the feature disabled' do
- post api("/features/#{feature_name}", admin), params: { value: 'true', group: 'not/a/group' }
-
- expect(response).to have_gitlab_http_status(:created)
- expect(json_response).to match(
- "name" => feature_name,
- "state" => "off",
- "gates" => [
- { "key" => "boolean", "value" => false }
- ],
- 'definition' => known_feature_flag_definition_hash
- )
+ it_behaves_like 'does not enable the flag', :group, 'not/a/group'
+ end
+ end
+
+ context 'when enabling for a namespace by path' do
+ context 'when the user namespace exists' do
+ it_behaves_like 'enables the flag for the actor', :namespace do
+ let(:actor) { create(:namespace) }
+ end
+ end
+
+ context 'when the group namespace exists' do
+ it_behaves_like 'enables the flag for the actor', :namespace do
+ let(:actor) { create(:group) }
+ end
+ end
+
+ context 'when the user namespace does not exist' do
+ it_behaves_like 'does not enable the flag', :namespace, 'not/a/group'
+ end
+
+ context 'when a project namespace exists' do
+ let(:project_namespace) { create(:project_namespace) }
+
+ it_behaves_like 'does not enable the flag', :namespace do
+ let(:actor_path) { project_namespace.full_path }
end
end
end
diff --git a/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb b/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb
index 578a71a7272..c19defa37e8 100644
--- a/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb
+++ b/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb
@@ -5,7 +5,7 @@ RSpec.describe 'Getting Ci Cd Setting' do
include GraphqlHelpers
let_it_be_with_reload(:project) { create(:project, :repository) }
- let_it_be(:current_user) { project.owner }
+ let_it_be(:current_user) { project.first_owner }
let(:fields) do
<<~QUERY
diff --git a/spec/requests/api/graphql/ci/config_spec.rb b/spec/requests/api/graphql/ci/config_spec.rb
index 755585f8e0e..62b15a8396c 100644
--- a/spec/requests/api/graphql/ci/config_spec.rb
+++ b/spec/requests/api/graphql/ci/config_spec.rb
@@ -225,7 +225,7 @@ RSpec.describe 'Query.ciConfig' do
context 'when using deprecated keywords' do
let_it_be(:content) do
YAML.dump(
- rspec: { script: 'ls' },
+ rspec: { script: 'ls', type: 'test' },
types: ['test']
)
end
@@ -233,7 +233,10 @@ RSpec.describe 'Query.ciConfig' do
it 'returns a warning' do
post_graphql_query
- expect(graphql_data['ciConfig']['warnings']).to include('root `types` is deprecated in 9.0 and will be removed in 15.0.')
+ expect(graphql_data['ciConfig']['warnings']).to include(
+ 'root `types` is deprecated in 9.0 and will be removed in 15.0.',
+ 'jobs:rspec `type` is deprecated in 9.0 and will be removed in 15.0.'
+ )
end
end
diff --git a/spec/requests/api/graphql/ci/runner_spec.rb b/spec/requests/api/graphql/ci/runner_spec.rb
index 8c919b48849..fa16b9e1ddd 100644
--- a/spec/requests/api/graphql/ci/runner_spec.rb
+++ b/spec/requests/api/graphql/ci/runner_spec.rb
@@ -25,6 +25,8 @@ RSpec.describe 'Query.runner(id)' do
access_level: 0, tag_list: %w[tag1 tag2], run_untagged: true, executor_type: :shell)
end
+ let_it_be(:active_project_runner) { create(:ci_runner, :project) }
+
def get_runner(id)
case id
when :active_instance_runner
@@ -33,6 +35,8 @@ RSpec.describe 'Query.runner(id)' do
inactive_instance_runner
when :active_group_runner
active_group_runner
+ when :active_project_runner
+ active_project_runner
end
end
@@ -55,7 +59,7 @@ RSpec.describe 'Query.runner(id)' do
runner = get_runner(runner_id)
expect(runner_data).to match a_hash_including(
- 'id' => "gid://gitlab/Ci::Runner/#{runner.id}",
+ 'id' => runner.to_global_id.to_s,
'description' => runner.description,
'createdAt' => runner.created_at&.iso8601,
'contactedAt' => runner.contacted_at&.iso8601,
@@ -64,6 +68,7 @@ RSpec.describe 'Query.runner(id)' do
'revision' => runner.revision,
'locked' => false,
'active' => runner.active,
+ 'paused' => !runner.active,
'status' => runner.status('14.5').to_s.upcase,
'maximumTimeout' => runner.maximum_timeout,
'accessLevel' => runner.access_level.to_s.upcase,
@@ -72,6 +77,7 @@ RSpec.describe 'Query.runner(id)' do
'runnerType' => runner.instance_type? ? 'INSTANCE_TYPE' : 'PROJECT_TYPE',
'executorName' => runner.executor_type&.dasherize,
'jobCount' => 0,
+ 'jobs' => a_hash_including("count" => 0, "nodes" => [], "pageInfo" => anything),
'projectCount' => nil,
'adminUrl' => "http://localhost/admin/runners/#{runner.id}",
'userPermissions' => {
@@ -103,7 +109,7 @@ RSpec.describe 'Query.runner(id)' do
runner = get_runner(runner_id)
expect(runner_data).to match a_hash_including(
- 'id' => "gid://gitlab/Ci::Runner/#{runner.id}",
+ 'id' => runner.to_global_id.to_s,
'adminUrl' => nil
)
expect(runner_data['tagList']).to match_array runner.tag_list
@@ -179,7 +185,7 @@ RSpec.describe 'Query.runner(id)' do
runner_data = graphql_data_at(:runner)
expect(runner_data).to match a_hash_including(
- 'id' => "gid://gitlab/Ci::Runner/#{project_runner.id}",
+ 'id' => project_runner.to_global_id.to_s,
'locked' => is_locked
)
end
@@ -216,13 +222,36 @@ RSpec.describe 'Query.runner(id)' do
a_hash_including(
'webUrl' => "http://localhost/groups/#{group.full_path}/-/runners/#{active_group_runner.id}",
'node' => {
- 'id' => "gid://gitlab/Ci::Runner/#{active_group_runner.id}"
+ 'id' => active_group_runner.to_global_id.to_s
}
)
]
end
end
+ describe 'for group runner request' do
+ let(:query) do
+ %(
+ query {
+ runner(id: "#{active_group_runner.to_global_id}") {
+ groups {
+ nodes {
+ id
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it 'retrieves groups field with expected value' do
+ post_graphql(query, current_user: user)
+
+ runner_data = graphql_data_at(:runner, :groups)
+ expect(runner_data).to eq 'nodes' => [{ 'id' => group.to_global_id.to_s }]
+ end
+ end
+
describe 'for runner with status' do
let_it_be(:stale_runner) { create(:ci_runner, description: 'Stale runner 1', created_at: 3.months.ago) }
let_it_be(:never_contacted_instance_runner) { create(:ci_runner, description: 'Missing runner 1', created_at: 1.month.ago, contacted_at: nil) }
@@ -279,21 +308,51 @@ RSpec.describe 'Query.runner(id)' do
let!(:job) { create(:ci_build, runner: project_runner1) }
- context 'requesting project and job counts' do
+ context 'requesting projects and counts for projects and jobs' do
+ let(:jobs_fragment) do
+ %(
+ jobs {
+ count
+ nodes {
+ id
+ status
+ }
+ }
+ )
+ end
+
let(:query) do
%(
query {
projectRunner1: runner(id: "#{project_runner1.to_global_id}") {
projectCount
jobCount
+ #{jobs_fragment}
+ projects {
+ nodes {
+ id
+ }
+ }
}
projectRunner2: runner(id: "#{project_runner2.to_global_id}") {
projectCount
jobCount
+ #{jobs_fragment}
+ projects {
+ nodes {
+ id
+ }
+ }
}
activeInstanceRunner: runner(id: "#{active_instance_runner.to_global_id}") {
projectCount
jobCount
+ #{jobs_fragment}
+ projects {
+ nodes {
+ id
+ }
+ }
}
}
)
@@ -312,13 +371,29 @@ RSpec.describe 'Query.runner(id)' do
expect(runner1_data).to match a_hash_including(
'jobCount' => 1,
- 'projectCount' => 2)
+ 'jobs' => a_hash_including(
+ "count" => 1,
+ "nodes" => [{ "id" => job.to_global_id.to_s, "status" => job.status.upcase }]
+ ),
+ 'projectCount' => 2,
+ 'projects' => {
+ 'nodes' => [
+ { 'id' => project1.to_global_id.to_s },
+ { 'id' => project2.to_global_id.to_s }
+ ]
+ })
expect(runner2_data).to match a_hash_including(
'jobCount' => 0,
- 'projectCount' => 0)
+ 'jobs' => nil, # returning jobs not allowed for more than 1 runner (see RunnerJobsResolver)
+ 'projectCount' => 0,
+ 'projects' => {
+ 'nodes' => []
+ })
expect(runner3_data).to match a_hash_including(
'jobCount' => 0,
- 'projectCount' => nil)
+ 'jobs' => nil, # returning jobs not allowed for more than 1 runner (see RunnerJobsResolver)
+ 'projectCount' => nil,
+ 'projects' => nil)
end
end
end
@@ -326,7 +401,17 @@ RSpec.describe 'Query.runner(id)' do
describe 'by regular user' do
let(:user) { create(:user) }
- it_behaves_like 'retrieval by unauthorized user', :active_instance_runner
+ context 'on instance runner' do
+ it_behaves_like 'retrieval by unauthorized user', :active_instance_runner
+ end
+
+ context 'on group runner' do
+ it_behaves_like 'retrieval by unauthorized user', :active_group_runner
+ end
+
+ context 'on project runner' do
+ it_behaves_like 'retrieval by unauthorized user', :active_project_runner
+ end
end
describe 'by non-admin user' do
diff --git a/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb b/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb
index 802ab847b3d..35a70a180a2 100644
--- a/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb
+++ b/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe 'container repository details' do
)
end
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:variables) { {} }
let(:tags) { %w[latest tag1 tag2 tag3 tag4 tag5] }
let(:container_repository_global_id) { container_repository.to_global_id.to_s }
diff --git a/spec/requests/api/graphql/gitlab_schema_spec.rb b/spec/requests/api/graphql/gitlab_schema_spec.rb
index 8bbeae97f57..e80f5e0e0ff 100644
--- a/spec/requests/api/graphql/gitlab_schema_spec.rb
+++ b/spec/requests/api/graphql/gitlab_schema_spec.rb
@@ -166,7 +166,7 @@ RSpec.describe 'GitlabSchema configurations' do
end
context 'authentication' do
- let(:current_user) { project.owner }
+ let(:current_user) { project.first_owner }
it 'authenticates all queries' do
subject
@@ -216,7 +216,7 @@ RSpec.describe 'GitlabSchema configurations' do
context "global id's" do
it 'uses GlobalID to expose ids' do
post_graphql(graphql_query_for('project', { 'fullPath' => project.full_path }, %w(id)),
- current_user: project.owner)
+ current_user: project.first_owner)
parsed_id = GlobalID.parse(graphql_data['project']['id'])
diff --git a/spec/requests/api/graphql/group/recent_issue_boards_query_spec.rb b/spec/requests/api/graphql/group/recent_issue_boards_query_spec.rb
new file mode 100644
index 00000000000..4914beec870
--- /dev/null
+++ b/spec/requests/api/graphql/group/recent_issue_boards_query_spec.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'getting group recent issue boards' do
+ include GraphqlHelpers
+
+ it_behaves_like 'querying a GraphQL type recent boards' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:parent) { create(:group, :public) }
+ let_it_be(:board) { create(:board, resource_parent: parent, name: 'test group board') }
+ let(:board_type) { 'group' }
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/ci/ci_cd_settings_update_spec.rb b/spec/requests/api/graphql/mutations/ci/ci_cd_settings_update_spec.rb
index 05f6804a208..30e7f196542 100644
--- a/spec/requests/api/graphql/mutations/ci/ci_cd_settings_update_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/ci_cd_settings_update_spec.rb
@@ -45,7 +45,7 @@ RSpec.describe 'CiCdSettingsUpdate' do
end
context 'when authorized' do
- let_it_be(:user) { project.owner }
+ let_it_be(:user) { project.first_owner }
it 'updates ci cd settings' do
post_graphql_mutation(mutation, current_user: user)
diff --git a/spec/requests/api/graphql/mutations/ci/job_token_scope/add_project_spec.rb b/spec/requests/api/graphql/mutations/ci/job_token_scope/add_project_spec.rb
index b53a7ddde32..5269c60b50a 100644
--- a/spec/requests/api/graphql/mutations/ci/job_token_scope/add_project_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/job_token_scope/add_project_spec.rb
@@ -49,7 +49,7 @@ RSpec.describe 'CiJobTokenScopeAddProject' do
end
context 'when authorized' do
- let_it_be(:current_user) { project.owner }
+ let_it_be(:current_user) { project.first_owner }
before do
target_project.add_developer(current_user)
diff --git a/spec/requests/api/graphql/mutations/ci/job_token_scope/remove_project_spec.rb b/spec/requests/api/graphql/mutations/ci/job_token_scope/remove_project_spec.rb
index f1f42b00ada..b62291d1ebd 100644
--- a/spec/requests/api/graphql/mutations/ci/job_token_scope/remove_project_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/job_token_scope/remove_project_spec.rb
@@ -55,7 +55,7 @@ RSpec.describe 'CiJobTokenScopeRemoveProject' do
end
context 'when authorized' do
- let_it_be(:current_user) { project.owner }
+ let_it_be(:current_user) { project.first_owner }
before do
target_project.add_guest(current_user)
diff --git a/spec/requests/api/graphql/mutations/ci/pipeline_destroy_spec.rb b/spec/requests/api/graphql/mutations/ci/pipeline_destroy_spec.rb
index 08959d354e2..37656ab4eea 100644
--- a/spec/requests/api/graphql/mutations/ci/pipeline_destroy_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/pipeline_destroy_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'PipelineDestroy' do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
- let_it_be(:user) { project.owner }
+ let_it_be(:user) { project.first_owner }
let_it_be(:pipeline) { create(:ci_pipeline, :success, project: project, user: user) }
let(:mutation) do
diff --git a/spec/requests/api/graphql/mutations/ci/runners_registration_token/reset_spec.rb b/spec/requests/api/graphql/mutations/ci/runners_registration_token/reset_spec.rb
index 322706be119..12368e7e9c5 100644
--- a/spec/requests/api/graphql/mutations/ci/runners_registration_token/reset_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/runners_registration_token/reset_spec.rb
@@ -71,7 +71,7 @@ RSpec.describe 'RunnersRegistrationTokenReset' do
end
include_context 'when authorized', 'project' do
- let_it_be(:user) { project.owner }
+ let_it_be(:user) { project.first_owner }
def get_token
project.reload.runners_token
diff --git a/spec/requests/api/graphql/mutations/issues/create_spec.rb b/spec/requests/api/graphql/mutations/issues/create_spec.rb
index 6baed352b37..3d81b456c9c 100644
--- a/spec/requests/api/graphql/mutations/issues/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/issues/create_spec.rb
@@ -52,5 +52,22 @@ RSpec.describe 'Create an issue' do
it_behaves_like 'has spam protection' do
let(:mutation_class) { ::Mutations::Issues::Create }
end
+
+ context 'when position params are provided' do
+ let(:existing_issue) { create(:issue, project: project, relative_position: 50) }
+
+ before do
+ input.merge!(
+ move_after_id: existing_issue.to_global_id.to_s
+ )
+ end
+
+ it 'sets the correct position' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['issue']['relativePosition']).to be < existing_issue.relative_position
+ end
+ end
end
end
diff --git a/spec/requests/api/graphql/mutations/security/ci_configuration/configure_sast_iac_spec.rb b/spec/requests/api/graphql/mutations/security/ci_configuration/configure_sast_iac_spec.rb
index 929609d4160..0c034f38dc8 100644
--- a/spec/requests/api/graphql/mutations/security/ci_configuration/configure_sast_iac_spec.rb
+++ b/spec/requests/api/graphql/mutations/security/ci_configuration/configure_sast_iac_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe 'ConfigureSastIac' do
let(:mutation_response) { graphql_mutation_response(:configureSastIac) }
context 'when authorized' do
- let_it_be(:user) { project.owner }
+ let_it_be(:user) { project.first_owner }
it 'creates a branch with sast iac configured' do
post_graphql_mutation(mutation, current_user: user)
diff --git a/spec/requests/api/graphql/mutations/security/ci_configuration/configure_secret_detection_spec.rb b/spec/requests/api/graphql/mutations/security/ci_configuration/configure_secret_detection_spec.rb
index 23a154b71a0..8fa6e44b208 100644
--- a/spec/requests/api/graphql/mutations/security/ci_configuration/configure_secret_detection_spec.rb
+++ b/spec/requests/api/graphql/mutations/security/ci_configuration/configure_secret_detection_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe 'ConfigureSecretDetection' do
let(:mutation_response) { graphql_mutation_response(:configureSecretDetection) }
context 'when authorized' do
- let_it_be(:user) { project.owner }
+ let_it_be(:user) { project.first_owner }
it 'creates a branch with secret detection configured' do
post_graphql_mutation(mutation, current_user: user)
diff --git a/spec/requests/api/graphql/mutations/user_preferences/update_spec.rb b/spec/requests/api/graphql/mutations/user_preferences/update_spec.rb
new file mode 100644
index 00000000000..e1c7fd9d60d
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/user_preferences/update_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::UserPreferences::Update do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+
+ let(:sort_value) { 'TITLE_ASC' }
+
+ let(:input) do
+ {
+ 'issuesSort' => sort_value
+ }
+ end
+
+ let(:mutation) { graphql_mutation(:userPreferencesUpdate, input) }
+ let(:mutation_response) { graphql_mutation_response(:userPreferencesUpdate) }
+
+ context 'when user has no existing preference' do
+ it 'creates the user preference record' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['userPreferences']['issuesSort']).to eq(sort_value)
+
+ expect(current_user.user_preference.persisted?).to eq(true)
+ expect(current_user.user_preference.issues_sort).to eq(Types::IssueSortEnum.values[sort_value].value.to_s)
+ end
+ end
+
+ context 'when user has existing preference' do
+ before do
+ current_user.create_user_preference!(issues_sort: Types::IssueSortEnum.values['TITLE_DESC'].value)
+ end
+
+ it 'updates the existing value' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ current_user.user_preference.reload
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['userPreferences']['issuesSort']).to eq(sort_value)
+
+ expect(current_user.user_preference.issues_sort).to eq(Types::IssueSortEnum.values[sort_value].value.to_s)
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/work_items/create_spec.rb b/spec/requests/api/graphql/mutations/work_items/create_spec.rb
index e7a0c7753fb..6abdaa2c850 100644
--- a/spec/requests/api/graphql/mutations/work_items/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/work_items/create_spec.rb
@@ -47,6 +47,18 @@ RSpec.describe 'Create a work item' do
)
end
+ context 'when input is invalid' do
+ let(:input) { { 'title' => '', 'workItemTypeId' => WorkItems::Type.default_by_type(:task).to_global_id.to_s } }
+
+ it 'does not create and returns validation errors' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ end.to not_change(WorkItem, :count)
+
+ expect(graphql_mutation_response(:work_item_create)['errors']).to contain_exactly("Title can't be blank")
+ end
+ end
+
it_behaves_like 'has spam protection' do
let(:mutation_class) { ::Mutations::WorkItems::Create }
end
@@ -56,8 +68,13 @@ RSpec.describe 'Create a work item' do
stub_feature_flags(work_items: false)
end
- it_behaves_like 'a mutation that returns top-level errors',
- errors: ["Field 'workItemCreate' doesn't exist on type 'Mutation'", "Variable $workItemCreateInput is declared by anonymous mutation but not used"]
+ it 'does not create the work item and returns an error' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ end.to not_change(WorkItem, :count)
+
+ expect(mutation_response['errors']).to contain_exactly('`work_items` feature flag disabled for this project')
+ end
end
end
end
diff --git a/spec/requests/api/graphql/mutations/work_items/delete_spec.rb b/spec/requests/api/graphql/mutations/work_items/delete_spec.rb
new file mode 100644
index 00000000000..14c8b757a57
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/work_items/delete_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Delete a work item' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:developer) { create(:user).tap { |user| project.add_developer(user) } }
+
+ let(:current_user) { developer }
+ let(:mutation) { graphql_mutation(:workItemDelete, { 'id' => work_item.to_global_id.to_s }) }
+ let(:mutation_response) { graphql_mutation_response(:work_item_delete) }
+
+ context 'when the user is not allowed to delete a work item' do
+ let(:work_item) { create(:work_item, project: project) }
+
+ it_behaves_like 'a mutation that returns a top-level access error'
+ end
+
+ context 'when user has permissions to delete a work item' do
+ let_it_be(:authored_work_item, refind: true) { create(:work_item, project: project, author: developer, assignees: [developer]) }
+
+ let(:work_item) { authored_work_item }
+
+ it 'deletes the work item' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ end.to change(WorkItem, :count).by(-1)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['project']).to include('id' => work_item.project.to_global_id.to_s)
+ end
+
+ context 'when the work_items feature flag is disabled' do
+ before do
+ stub_feature_flags(work_items: false)
+ end
+
+ it 'does not delete the work item' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ end.to not_change(WorkItem, :count)
+
+ expect(mutation_response['errors']).to contain_exactly('`work_items` feature flag disabled for this project')
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/work_items/update_spec.rb b/spec/requests/api/graphql/mutations/work_items/update_spec.rb
new file mode 100644
index 00000000000..71b03103115
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/work_items/update_spec.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Update a work item' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:developer) { create(:user).tap { |user| project.add_developer(user) } }
+ let_it_be(:work_item, refind: true) { create(:work_item, project: project) }
+
+ let(:work_item_event) { 'CLOSE' }
+ let(:input) { { 'stateEvent' => work_item_event, 'title' => 'updated title' } }
+
+ let(:mutation) { graphql_mutation(:workItemUpdate, input.merge('id' => work_item.to_global_id.to_s)) }
+
+ let(:mutation_response) { graphql_mutation_response(:work_item_update) }
+
+ context 'the user is not allowed to update a work item' do
+ let(:current_user) { create(:user) }
+
+ it_behaves_like 'a mutation that returns a top-level access error'
+ end
+
+ context 'when user has permissions to update a work item' do
+ let(:current_user) { developer }
+
+ context 'when the work item is open' do
+ it 'closes and updates the work item' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ work_item.reload
+ end.to change(work_item, :state).from('opened').to('closed').and(
+ change(work_item, :title).from(work_item.title).to('updated title')
+ )
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['workItem']).to include(
+ 'state' => 'CLOSED',
+ 'title' => 'updated title'
+ )
+ end
+ end
+
+ context 'when the work item is closed' do
+ let(:work_item_event) { 'REOPEN' }
+
+ before do
+ work_item.close!
+ end
+
+ it 'reopens the work item' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ work_item.reload
+ end.to change(work_item, :state).from('closed').to('opened')
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['workItem']).to include(
+ 'state' => 'OPEN'
+ )
+ end
+ end
+
+ it_behaves_like 'has spam protection' do
+ let(:mutation_class) { ::Mutations::WorkItems::Update }
+ end
+
+ context 'when the work_items feature flag is disabled' do
+ before do
+ stub_feature_flags(work_items: false)
+ end
+
+ it 'does not update the work item and returns and error' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ work_item.reload
+ end.to not_change(work_item, :title)
+
+ expect(mutation_response['errors']).to contain_exactly('`work_items` feature flag disabled for this project')
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/packages/package_spec.rb b/spec/requests/api/graphql/packages/package_spec.rb
index 2ff3bc7cc47..365efc514d4 100644
--- a/spec/requests/api/graphql/packages/package_spec.rb
+++ b/spec/requests/api/graphql/packages/package_spec.rb
@@ -102,18 +102,6 @@ RSpec.describe 'package details' do
expect(package_file_ids).to contain_exactly(package_file.to_global_id.to_s)
end
-
- context 'with packages_installable_package_files disabled' do
- before do
- stub_feature_flags(packages_installable_package_files: false)
- end
-
- it 'returns them' do
- subject
-
- expect(package_file_ids).to contain_exactly(package_file_pending_destruction.to_global_id.to_s, package_file.to_global_id.to_s)
- end
- end
end
context 'with a batched query' do
@@ -145,8 +133,9 @@ RSpec.describe 'package details' do
let(:pipeline_gids) { pipelines.sort_by(&:id).map(&:to_gid).map(&:to_s).reverse }
before do
- composer_package.pipelines = pipelines
- composer_package.save!
+ pipelines.each do |pipeline|
+ create(:package_build_info, package: composer_package, pipeline: pipeline)
+ end
end
def run_query(args)
diff --git a/spec/requests/api/graphql/project/container_expiration_policy_spec.rb b/spec/requests/api/graphql/project/container_expiration_policy_spec.rb
index dc16847a669..e3ea9e46353 100644
--- a/spec/requests/api/graphql/project/container_expiration_policy_spec.rb
+++ b/spec/requests/api/graphql/project/container_expiration_policy_spec.rb
@@ -5,7 +5,7 @@ RSpec.describe 'getting a repository in a project' do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
- let_it_be(:current_user) { project.owner }
+ let_it_be(:current_user) { project.first_owner }
let_it_be(:container_expiration_policy) { project.container_expiration_policy }
let(:fields) do
diff --git a/spec/requests/api/graphql/project/container_repositories_spec.rb b/spec/requests/api/graphql/project/container_repositories_spec.rb
index 692143b2215..bbab6012f3f 100644
--- a/spec/requests/api/graphql/project/container_repositories_spec.rb
+++ b/spec/requests/api/graphql/project/container_repositories_spec.rb
@@ -38,7 +38,7 @@ RSpec.describe 'getting container repositories in a project' do
)
end
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:variables) { {} }
let(:container_repositories_response) { graphql_data.dig('project', 'containerRepositories', 'edges') }
let(:container_repositories_count_response) { graphql_data.dig('project', 'containerRepositoriesCount') }
diff --git a/spec/requests/api/graphql/project/error_tracking/sentry_detailed_error_request_spec.rb b/spec/requests/api/graphql/project/error_tracking/sentry_detailed_error_request_spec.rb
index 40a3281d3b7..2b85704f479 100644
--- a/spec/requests/api/graphql/project/error_tracking/sentry_detailed_error_request_spec.rb
+++ b/spec/requests/api/graphql/project/error_tracking/sentry_detailed_error_request_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'getting a detailed sentry error' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:project_setting) { create(:project_error_tracking_setting, project: project) }
- let_it_be(:current_user) { project.owner }
+ let_it_be(:current_user) { project.first_owner }
let_it_be(:sentry_detailed_error) { build(:error_tracking_sentry_detailed_error) }
let(:sentry_gid) { sentry_detailed_error.to_global_id.to_s }
diff --git a/spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb b/spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb
index a540386a9de..3ca0e35882a 100644
--- a/spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb
+++ b/spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'sentry errors requests' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:project_setting) { create(:project_error_tracking_setting, project: project) }
- let_it_be(:current_user) { project.owner }
+ let_it_be(:current_user) { project.first_owner }
let(:query) do
graphql_query_for(
diff --git a/spec/requests/api/graphql/project/grafana_integration_spec.rb b/spec/requests/api/graphql/project/grafana_integration_spec.rb
index 9b24698f40c..e7534945e7a 100644
--- a/spec/requests/api/graphql/project/grafana_integration_spec.rb
+++ b/spec/requests/api/graphql/project/grafana_integration_spec.rb
@@ -5,7 +5,7 @@ RSpec.describe 'Getting Grafana Integration' do
include GraphqlHelpers
let_it_be(:project) { create(:project, :repository) }
- let_it_be(:current_user) { project.owner }
+ let_it_be(:current_user) { project.first_owner }
let_it_be(:grafana_integration) { create(:grafana_integration, project: project) }
let(:fields) do
diff --git a/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb b/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb
index 9d98498ca8a..46fd65db1c5 100644
--- a/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb
+++ b/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe 'Getting versions related to an issue' do
create(:design_version, issue: issue)
end
- let_it_be(:owner) { issue.project.owner }
+ let_it_be(:owner) { issue.project.first_owner }
def version_query(params = version_params)
query_graphql_field(:versions, params, version_query_fields)
diff --git a/spec/requests/api/graphql/project/issue/designs/designs_spec.rb b/spec/requests/api/graphql/project/issue/designs/designs_spec.rb
index def41efddde..f0205319983 100644
--- a/spec/requests/api/graphql/project/issue/designs/designs_spec.rb
+++ b/spec/requests/api/graphql/project/issue/designs/designs_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe 'Getting designs related to an issue' do
include DesignManagementTestHelpers
let_it_be(:design) { create(:design, :with_smaller_image_versions, versions_count: 1) }
- let_it_be(:current_user) { design.project.owner }
+ let_it_be(:current_user) { design.project.first_owner }
let(:design_query) do
<<~NODE
diff --git a/spec/requests/api/graphql/project/issue/designs/notes_spec.rb b/spec/requests/api/graphql/project/issue/designs/notes_spec.rb
index 7148750b6cb..de2ace95757 100644
--- a/spec/requests/api/graphql/project/issue/designs/notes_spec.rb
+++ b/spec/requests/api/graphql/project/issue/designs/notes_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe 'Getting designs related to an issue' do
let_it_be(:project) { create(:project, :public) }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:design) { create(:design, :with_file, versions_count: 1, issue: issue) }
- let_it_be(:current_user) { project.owner }
+ let_it_be(:current_user) { project.first_owner }
let_it_be(:note) { create(:diff_note_on_design, noteable: design, project: project) }
before do
diff --git a/spec/requests/api/graphql/project/merge_requests_spec.rb b/spec/requests/api/graphql/project/merge_requests_spec.rb
index b0bedd99fce..303748bc70e 100644
--- a/spec/requests/api/graphql/project/merge_requests_spec.rb
+++ b/spec/requests/api/graphql/project/merge_requests_spec.rb
@@ -29,6 +29,10 @@ RSpec.describe 'getting merge request listings nested in a project' do
create(:merge_request, :unique_branches, source_project: project)
end
+ let(:all_merge_requests) do
+ [merge_request_a, merge_request_b, merge_request_c, merge_request_d, merge_request_e]
+ end
+
let(:results) { graphql_data.dig('project', 'mergeRequests', 'nodes') }
let(:search_params) { nil }
@@ -180,6 +184,39 @@ RSpec.describe 'getting merge request listings nested in a project' do
it_behaves_like 'when searching with parameters'
end
+ context 'when searching by update time' do
+ let(:start_time) { 10.days.ago }
+ let(:cutoff) { start_time + 36.hours }
+
+ before do
+ all_merge_requests.each_with_index do |mr, i|
+ mr.updated_at = start_time + i.days
+ mr.save!(touch: false)
+ end
+ end
+
+ context 'when searching by updated_after' do
+ let(:search_params) { { updated_after: cutoff } }
+ let(:mrs) { all_merge_requests[2..] }
+
+ it_behaves_like 'when searching with parameters'
+ end
+
+ context 'when searching by updated_before' do
+ let(:search_params) { { updated_before: cutoff } }
+ let(:mrs) { all_merge_requests[0..1] }
+
+ it_behaves_like 'when searching with parameters'
+ end
+
+ context 'when searching by updated_before and updated_after' do
+ let(:search_params) { { updated_after: cutoff, updated_before: cutoff + 2.days } }
+ let(:mrs) { all_merge_requests[2..3] }
+
+ it_behaves_like 'when searching with parameters'
+ end
+ end
+
context 'when searching by combination' do
let(:search_params) { { state: :closed, labels: [label.title] } }
let(:mrs) { [merge_request_c] }
diff --git a/spec/requests/api/graphql/project/project_members_spec.rb b/spec/requests/api/graphql/project/project_members_spec.rb
index 466464f600c..315d44884ff 100644
--- a/spec/requests/api/graphql/project/project_members_spec.rb
+++ b/spec/requests/api/graphql/project/project_members_spec.rb
@@ -110,6 +110,102 @@ RSpec.describe 'getting project members information' do
end
end
+ context 'merge request interactions' do
+ let(:project_path) { var('ID!').with(parent_project.full_path) }
+ let(:mr_a) do
+ var('MergeRequestID!')
+ .with(global_id_of(create(:merge_request, source_project: parent_project, source_branch: 'branch-1')))
+ end
+
+ let(:mr_b) do
+ var('MergeRequestID!')
+ .with(global_id_of(create(:merge_request, source_project: parent_project, source_branch: 'branch-2')))
+ end
+
+ let(:interaction_query) do
+ <<~HEREDOC
+ edges {
+ node {
+ user {
+ id
+ }
+ mrA: #{query_graphql_field(:merge_request_interaction, { id: mr_a }, 'canMerge')}
+ }
+ }
+ HEREDOC
+ end
+
+ let(:interaction_b_query) do
+ <<~HEREDOC
+ edges {
+ node {
+ user {
+ id
+ }
+ mrA: #{query_graphql_field(:merge_request_interaction, { id: mr_a }, 'canMerge')}
+ mrB: #{query_graphql_field(:merge_request_interaction, { id: mr_b }, 'canMerge')}
+ }
+ }
+ HEREDOC
+ end
+
+ it 'avoids N+1 queries, when requesting multiple MRs' do
+ control_query = with_signature(
+ [project_path, mr_a],
+ graphql_query_for(:project, { full_path: project_path },
+ query_graphql_field(:project_members, nil, interaction_query))
+ )
+ query_two = with_signature(
+ [project_path, mr_a, mr_b],
+ graphql_query_for(:project, { full_path: project_path },
+ query_graphql_field(:project_members, nil, interaction_b_query))
+ )
+
+ control_count = ActiveRecord::QueryRecorder.new do
+ post_graphql(control_query, current_user: user, variables: [project_path, mr_a])
+ end
+
+ # two project members, neither of whom can merge
+ expect(can_merge(:mrA)).to eq [false, false]
+
+ expect do
+ post_graphql(query_two, current_user: user, variables: [project_path, mr_a, mr_b])
+
+ expect(can_merge(:mrA)).to eq [false, false]
+ expect(can_merge(:mrB)).to eq [false, false]
+ end.not_to exceed_query_limit(control_count)
+ end
+
+ it 'avoids N+1 queries, when more users are involved' do
+ new_user = create(:user)
+
+ query = with_signature(
+ [project_path, mr_a],
+ graphql_query_for(:project, { full_path: project_path },
+ query_graphql_field(:project_members, nil, interaction_query))
+ )
+
+ control_count = ActiveRecord::QueryRecorder.new do
+ post_graphql(query, current_user: user, variables: [project_path, mr_a])
+ end
+
+ # two project members, neither of whom can merge
+ expect(can_merge(:mrA)).to eq [false, false]
+
+ parent_project.add_guest(new_user)
+
+ expect do
+ post_graphql(query, current_user: user, variables: [project_path, mr_a])
+
+ expect(can_merge(:mrA)).to eq [false, false, false]
+ end.not_to exceed_query_limit(control_count)
+ end
+
+ def can_merge(name)
+ graphql_data_at(:project, :project_members, :edges, :node, name, :can_merge)
+ end
+ end
+
context 'when unauthenticated' do
it 'returns members' do
fetch_members(current_user: nil, project: parent_project)
diff --git a/spec/requests/api/graphql/project/recent_issue_boards_query_spec.rb b/spec/requests/api/graphql/project/recent_issue_boards_query_spec.rb
new file mode 100644
index 00000000000..b3daf86c4af
--- /dev/null
+++ b/spec/requests/api/graphql/project/recent_issue_boards_query_spec.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'getting project recent issue boards' do
+ include GraphqlHelpers
+
+ it_behaves_like 'querying a GraphQL type recent boards' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:parent) { create(:project, :public, namespace: user.namespace) }
+ let_it_be(:board) { create(:board, resource_parent: parent, name: 'test project board') }
+ let(:board_type) { 'project' }
+ end
+end
diff --git a/spec/requests/api/graphql/project/repository/blobs_spec.rb b/spec/requests/api/graphql/project/repository/blobs_spec.rb
index 12f6fbd793e..ba87f1100f2 100644
--- a/spec/requests/api/graphql/project/repository/blobs_spec.rb
+++ b/spec/requests/api/graphql/project/repository/blobs_spec.rb
@@ -5,7 +5,7 @@ RSpec.describe 'getting blobs in a project repository' do
include GraphqlHelpers
let(:project) { create(:project, :repository) }
- let(:current_user) { project.owner }
+ let(:current_user) { project.first_owner }
let(:paths) { ["CONTRIBUTING.md", "README.md"] }
let(:ref) { project.default_branch }
let(:fields) do
diff --git a/spec/requests/api/graphql/project/repository_spec.rb b/spec/requests/api/graphql/project/repository_spec.rb
index 8810f2fa3d5..b00f64c3db6 100644
--- a/spec/requests/api/graphql/project/repository_spec.rb
+++ b/spec/requests/api/graphql/project/repository_spec.rb
@@ -5,7 +5,7 @@ RSpec.describe 'getting a repository in a project' do
include GraphqlHelpers
let(:project) { create(:project, :repository) }
- let(:current_user) { project.owner }
+ let(:current_user) { project.first_owner }
let(:fields) do
<<~QUERY
#{all_graphql_fields_for('repository'.classify)}
diff --git a/spec/requests/api/graphql/project/tree/tree_spec.rb b/spec/requests/api/graphql/project/tree/tree_spec.rb
index f4cd316da96..25e878a5b1a 100644
--- a/spec/requests/api/graphql/project/tree/tree_spec.rb
+++ b/spec/requests/api/graphql/project/tree/tree_spec.rb
@@ -5,7 +5,7 @@ RSpec.describe 'getting a tree in a project' do
include GraphqlHelpers
let(:project) { create(:project, :repository) }
- let(:current_user) { project.owner }
+ let(:current_user) { project.first_owner }
let(:path) { "" }
let(:ref) { "master" }
let(:fields) do
diff --git a/spec/requests/api/group_clusters_spec.rb b/spec/requests/api/group_clusters_spec.rb
index f65f9384efa..c48b5007f91 100644
--- a/spec/requests/api/group_clusters_spec.rb
+++ b/spec/requests/api/group_clusters_spec.rb
@@ -6,11 +6,11 @@ RSpec.describe API::GroupClusters do
include KubernetesHelpers
let(:current_user) { create(:user) }
- let(:developer_user) { create(:user) }
+ let(:unauthorized_user) { create(:user) }
let(:group) { create(:group, :private) }
before do
- group.add_developer(developer_user)
+ group.add_reporter(unauthorized_user)
group.add_maintainer(current_user)
end
@@ -24,7 +24,7 @@ RSpec.describe API::GroupClusters do
context 'non-authorized user' do
it 'responds with 403' do
- get api("/groups/#{group.id}/clusters", developer_user)
+ get api("/groups/#{group.id}/clusters", unauthorized_user)
expect(response).to have_gitlab_http_status(:forbidden)
end
@@ -68,7 +68,7 @@ RSpec.describe API::GroupClusters do
context 'non-authorized user' do
it 'responds with 403' do
- get api("/groups/#{group.id}/clusters/#{cluster_id}", developer_user)
+ get api("/groups/#{group.id}/clusters/#{cluster_id}", unauthorized_user)
expect(response).to have_gitlab_http_status(:forbidden)
end
@@ -183,7 +183,7 @@ RSpec.describe API::GroupClusters do
context 'non-authorized user' do
it 'responds with 403' do
- post api("/groups/#{group.id}/clusters/user", developer_user), params: cluster_params
+ post api("/groups/#{group.id}/clusters/user", unauthorized_user), params: cluster_params
expect(response).to have_gitlab_http_status(:forbidden)
end
@@ -290,7 +290,7 @@ RSpec.describe API::GroupClusters do
context 'non-authorized user' do
before do
- post api("/groups/#{group.id}/clusters/user", developer_user), params: cluster_params
+ post api("/groups/#{group.id}/clusters/user", unauthorized_user), params: cluster_params
end
it 'responds with 403' do
@@ -364,7 +364,7 @@ RSpec.describe API::GroupClusters do
context 'non-authorized user' do
it 'responds with 403' do
- put api("/groups/#{group.id}/clusters/#{cluster.id}", developer_user), params: update_params
+ put api("/groups/#{group.id}/clusters/#{cluster.id}", unauthorized_user), params: update_params
expect(response).to have_gitlab_http_status(:forbidden)
end
@@ -505,7 +505,7 @@ RSpec.describe API::GroupClusters do
context 'non-authorized user' do
it 'responds with 403' do
- delete api("/groups/#{group.id}/clusters/#{cluster.id}", developer_user), params: cluster_params
+ delete api("/groups/#{group.id}/clusters/#{cluster.id}", unauthorized_user), params: cluster_params
expect(response).to have_gitlab_http_status(:forbidden)
end
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index 88c004345fc..7de3567dcdd 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -1163,17 +1163,33 @@ RSpec.describe API::Groups do
expect(json_response.length).to eq(3)
end
- it "returns projects including those in subgroups" do
- subgroup = create(:group, parent: group1)
- create(:project, group: subgroup)
- create(:project, group: subgroup)
+ context 'when include_subgroups is true' do
+ it "returns projects including those in subgroups" do
+ subgroup = create(:group, parent: group1)
+ create(:project, group: subgroup)
+ create(:project, group: subgroup)
- get api("/groups/#{group1.id}/projects", user1), params: { include_subgroups: true }
+ get api("/groups/#{group1.id}/projects", user1), params: { include_subgroups: true }
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an(Array)
- expect(json_response.length).to eq(5)
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an(Array)
+ expect(json_response.length).to eq(5)
+ end
+ end
+
+ context 'when include_ancestor_groups is true' do
+ it 'returns ancestors groups projects' do
+ subgroup = create(:group, parent: group1)
+ subgroup_project = create(:project, group: subgroup)
+
+ get api("/groups/#{subgroup.id}/projects", user1), params: { include_ancestor_groups: true }
+
+ records = Gitlab::Json.parse(response.body)
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(records.map { |r| r['id'] }).to match_array([project1.id, project3.id, subgroup_project.id, archived_project.id])
+ end
end
it "does not return a non existing group" do
diff --git a/spec/requests/api/internal/base_spec.rb b/spec/requests/api/internal/base_spec.rb
index 9aa8aaafc68..2b7963eadab 100644
--- a/spec/requests/api/internal/base_spec.rb
+++ b/spec/requests/api/internal/base_spec.rb
@@ -612,6 +612,30 @@ RSpec.describe API::Internal::Base do
expect(json_response["gitaly"]["features"]).to eq('gitaly-feature-mep-mep' => 'false')
end
end
+
+ context "with a sidechannels enabled for a project" do
+ before do
+ stub_feature_flags(gitlab_shell_upload_pack_sidechannel: project)
+ end
+
+ it "has the use_sidechannel field set to true for that project" do
+ pull(key, project)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response["gl_repository"]).to eq("project-#{project.id}")
+ expect(json_response["gitaly"]["use_sidechannel"]).to eq(true)
+ end
+
+ it "has the use_sidechannel field set to false for other projects" do
+ other_project = create(:project, :public, :repository)
+
+ pull(key, other_project)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response["gl_repository"]).to eq("project-#{other_project.id}")
+ expect(json_response["gitaly"]["use_sidechannel"]).to eq(false)
+ end
+ end
end
context "git push" do
@@ -724,6 +748,30 @@ RSpec.describe API::Internal::Base do
end
end
+ context 'with a pending membership' do
+ let_it_be(:project) { create(:project, :repository) }
+
+ before_all do
+ create(:project_member, :awaiting, :developer, source: project, user: user)
+ end
+
+ it 'returns not found for git pull' do
+ pull(key, project)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response["status"]).to be_falsey
+ expect(user.reload.last_activity_on).to be_nil
+ end
+
+ it 'returns not found for git push' do
+ push(key, project)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response["status"]).to be_falsey
+ expect(user.reload.last_activity_on).to be_nil
+ end
+ end
+
context "custom action" do
let(:access_checker) { double(Gitlab::GitAccess) }
let(:payload) do
diff --git a/spec/requests/api/internal/container_registry/migration_spec.rb b/spec/requests/api/internal/container_registry/migration_spec.rb
new file mode 100644
index 00000000000..27e99a21c65
--- /dev/null
+++ b/spec/requests/api/internal/container_registry/migration_spec.rb
@@ -0,0 +1,153 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::Internal::ContainerRegistry::Migration do
+ let_it_be_with_reload(:repository) { create(:container_repository) }
+
+ let(:secret_token) { 'secret_token' }
+ let(:sent_token) { secret_token }
+ let(:repository_path) { repository.path }
+ let(:status) { 'pre_import_complete' }
+ let(:params) { { path: repository.path, status: status } }
+
+ before do
+ allow(Gitlab.config.registry).to receive(:notification_secret) { secret_token }
+ end
+
+ describe 'PUT /internal/registry/repositories/:path/migration/status' do
+ subject do
+ put api("/internal/registry/repositories/#{repository_path}/migration/status"),
+ params: params,
+ headers: { 'Authorization' => sent_token }
+ end
+
+ shared_examples 'returning an error' do |with_message: nil, returning_status: :bad_request|
+ it "returns bad request response" do
+ expect { subject }
+ .not_to change { repository.reload.migration_state }
+
+ expect(response).to have_gitlab_http_status(returning_status)
+ expect(response.body).to include(with_message) if with_message
+ end
+ end
+
+ context 'with a valid sent token' do
+ shared_examples 'updating the repository migration status' do |from:, to:|
+ it "updates the migration status from #{from} to #{to}" do
+ expect { subject }
+ .to change { repository.reload.migration_state }.from(from).to(to)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'with status pre_import_complete' do
+ let(:status) { 'pre_import_complete' }
+
+ it_behaves_like 'returning an error', with_message: 'Wrong migration state (default)'
+
+ context 'with repository in pre_importing migration state' do
+ let(:repository) { create(:container_repository, :pre_importing) }
+
+ before do
+ allow_next_found_instance_of(ContainerRepository) do |found_repository|
+ allow(found_repository).to receive(:migration_import).and_return(:ok)
+ end
+ end
+
+ it_behaves_like 'updating the repository migration status', from: 'pre_importing', to: 'importing'
+
+ context 'with a failing transition' do
+ before do
+ allow_next_found_instance_of(ContainerRepository) do |found_repository|
+ allow(found_repository).to receive(:finish_pre_import_and_start_import).and_return(false)
+ end
+ end
+
+ it_behaves_like 'returning an error', with_message: "Couldn't transition from pre_importing to importing"
+ end
+ end
+
+ context 'with repository in importing migration state' do
+ let(:repository) { create(:container_repository, :importing) }
+
+ it_behaves_like 'returning an error', with_message: "Couldn't transition from pre_importing to importing"
+ end
+ end
+
+ context 'with status import_complete' do
+ let(:status) { 'import_complete' }
+
+ it_behaves_like 'returning an error', with_message: 'Wrong migration state (default)'
+
+ context 'with repository in importing migration state' do
+ let(:repository) { create(:container_repository, :importing) }
+ let(:transition_result) { true }
+
+ it_behaves_like 'updating the repository migration status', from: 'importing', to: 'import_done'
+
+ context 'with a failing transition' do
+ before do
+ allow_next_found_instance_of(ContainerRepository) do |found_repository|
+ allow(found_repository).to receive(:finish_import).and_return(false)
+ end
+ end
+
+ it_behaves_like 'returning an error', with_message: "Couldn't transition from importing to import_done"
+ end
+ end
+
+ context 'with repository in pre_importing migration state' do
+ let(:repository) { create(:container_repository, :pre_importing) }
+
+ it_behaves_like 'returning an error', with_message: "Couldn't transition from importing to import_done"
+ end
+ end
+
+ %w[pre_import_failed import_failed].each do |status|
+ context 'with status pre_import_failed' do
+ let(:status) { 'pre_import_failed' }
+
+ it_behaves_like 'returning an error', with_message: 'Wrong migration state (default)'
+
+ context 'with repository in importing migration state' do
+ let(:repository) { create(:container_repository, :importing) }
+
+ it_behaves_like 'updating the repository migration status', from: 'importing', to: 'import_aborted'
+ end
+
+ context 'with repository in pre_importing migration state' do
+ let(:repository) { create(:container_repository, :pre_importing) }
+
+ it_behaves_like 'updating the repository migration status', from: 'pre_importing', to: 'import_aborted'
+ end
+ end
+ end
+
+ context 'with a non existing path' do
+ let(:repository_path) { 'this/does/not/exist' }
+
+ it_behaves_like 'returning an error', returning_status: :not_found
+ end
+
+ context 'with invalid status' do
+ let(:params) { super().merge(status: nil).compact }
+
+ it_behaves_like 'returning an error', returning_status: :bad_request
+ end
+
+ context 'with invalid path' do
+ let(:repository_path) { nil }
+
+ it_behaves_like 'returning an error', returning_status: :not_found
+ end
+ end
+
+ context 'with an invalid sent token' do
+ let(:sent_token) { 'not_valid' }
+
+ it_behaves_like 'returning an error', returning_status: :unauthorized
+ end
+ end
+end
diff --git a/spec/requests/api/issues/issues_spec.rb b/spec/requests/api/issues/issues_spec.rb
index 9204ee4d7f0..c5e57b5b18b 100644
--- a/spec/requests/api/issues/issues_spec.rb
+++ b/spec/requests/api/issues/issues_spec.rb
@@ -488,6 +488,8 @@ RSpec.describe API::Issues do
let_it_be(:issue3) { create(:issue, project: project, author: user, due_date: frozen_time + 10.days) }
let_it_be(:issue4) { create(:issue, project: project, author: user, due_date: frozen_time + 34.days) }
let_it_be(:issue5) { create(:issue, project: project, author: user, due_date: frozen_time - 8.days) }
+ let_it_be(:issue6) { create(:issue, project: project, author: user, due_date: frozen_time) }
+ let_it_be(:issue7) { create(:issue, project: project, author: user, due_date: frozen_time + 1.day) }
before do
travel_to(frozen_time)
@@ -500,7 +502,13 @@ RSpec.describe API::Issues do
it 'returns them all when argument is empty' do
get api('/issues?due_date=', user)
- expect_paginated_array_response(issue5.id, issue4.id, issue3.id, issue2.id, issue.id, closed_issue.id)
+ expect_paginated_array_response(issue7.id, issue6.id, issue5.id, issue4.id, issue3.id, issue2.id, issue.id, closed_issue.id)
+ end
+
+ it 'returns issues with due date' do
+ get api('/issues?due_date=any', user)
+
+ expect_paginated_array_response(issue7.id, issue6.id, issue5.id, issue4.id, issue3.id, issue2.id)
end
it 'returns issues without due date' do
@@ -512,19 +520,31 @@ RSpec.describe API::Issues do
it 'returns issues due for this week' do
get api('/issues?due_date=week', user)
- expect_paginated_array_response(issue2.id)
+ expect_paginated_array_response(issue7.id, issue6.id, issue2.id)
end
it 'returns issues due for this month' do
get api('/issues?due_date=month', user)
- expect_paginated_array_response(issue3.id, issue2.id)
+ expect_paginated_array_response(issue7.id, issue6.id, issue3.id, issue2.id)
end
it 'returns issues that are due previous two weeks and next month' do
get api('/issues?due_date=next_month_and_previous_two_weeks', user)
- expect_paginated_array_response(issue5.id, issue4.id, issue3.id, issue2.id)
+ expect_paginated_array_response(issue7.id, issue6.id, issue5.id, issue4.id, issue3.id, issue2.id)
+ end
+
+ it 'returns issues that are due today' do
+ get api('/issues?due_date=today', user)
+
+ expect_paginated_array_response(issue6.id)
+ end
+
+ it 'returns issues that are due tomorrow' do
+ get api('/issues?due_date=tomorrow', user)
+
+ expect_paginated_array_response(issue7.id)
end
it 'returns issues that are overdue' do
@@ -1164,14 +1184,15 @@ RSpec.describe API::Issues do
end
describe 'PUT /projects/:id/issues/:issue_iid/reorder' do
- let_it_be(:project) { create(:project) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
let_it_be(:issue1) { create(:issue, project: project, relative_position: 10) }
let_it_be(:issue2) { create(:issue, project: project, relative_position: 20) }
let_it_be(:issue3) { create(:issue, project: project, relative_position: 30) }
context 'when user has access' do
- before do
- project.add_developer(user)
+ before_all do
+ group.add_developer(user)
end
context 'with valid params' do
@@ -1197,6 +1218,19 @@ RSpec.describe API::Issues do
expect(response).to have_gitlab_http_status(:not_found)
end
end
+
+ context 'with issue in different project' do
+ let(:other_project) { create(:project, group: group) }
+ let(:other_issue) { create(:issue, project: other_project, relative_position: 80) }
+
+ it 'reorders issues and returns a successful 200 response' do
+ put api("/projects/#{other_project.id}/issues/#{other_issue.iid}/reorder", user), params: { move_after_id: issue2.id, move_before_id: issue3.id }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(other_issue.reload.relative_position)
+ .to be_between(issue2.reload.relative_position, issue3.reload.relative_position)
+ end
+ end
end
context 'with unauthorized user' do
diff --git a/spec/requests/api/lint_spec.rb b/spec/requests/api/lint_spec.rb
index 7c1e731a99a..73bc4a5d1f3 100644
--- a/spec/requests/api/lint_spec.rb
+++ b/spec/requests/api/lint_spec.rb
@@ -110,7 +110,7 @@ RSpec.describe API::Lint do
context 'when authenticated' do
let_it_be(:api_user) { create(:user) }
- context 'with valid .gitlab-ci.yaml content' do
+ context 'with valid .gitlab-ci.yml content' do
let(:yaml_content) do
File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
end
@@ -140,7 +140,7 @@ RSpec.describe API::Lint do
end
end
- context 'with valid .gitlab-ci.yaml with warnings' do
+ context 'with valid .gitlab-ci.yml with warnings' do
let(:yaml_content) { { job: { script: 'ls', rules: [{ when: 'always' }] } }.to_yaml }
it 'passes validation but returns warnings' do
@@ -153,8 +153,8 @@ RSpec.describe API::Lint do
end
end
- context 'with valid .gitlab-ci.yaml using deprecated keywords' do
- let(:yaml_content) { { job: { script: 'ls' }, types: ['test'] }.to_yaml }
+ context 'with valid .gitlab-ci.yml using deprecated keywords' do
+ let(:yaml_content) { { job: { script: 'ls', type: 'test' }, types: ['test'] }.to_yaml }
it 'passes validation but returns warnings' do
post api('/ci/lint', api_user), params: { content: yaml_content }
@@ -166,7 +166,7 @@ RSpec.describe API::Lint do
end
end
- context 'with an invalid .gitlab_ci.yml' do
+ context 'with an invalid .gitlab-ci.yml' do
context 'with invalid syntax' do
let(:yaml_content) { 'invalid content' }
@@ -384,6 +384,15 @@ RSpec.describe API::Lint do
project.add_developer(api_user)
end
+ context 'with no commit' do
+ it 'returns error about providing content' do
+ ci_lint
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['errors']).to match_array(['Please provide content of .gitlab-ci.yml'])
+ end
+ end
+
context 'with valid .gitlab-ci.yml content' do
let(:yaml_content) do
{ include: { local: 'another-gitlab-ci.yml' }, test: { stage: 'test', script: 'echo 1' } }.to_yaml
diff --git a/spec/requests/api/markdown_spec.rb b/spec/requests/api/markdown_spec.rb
index faf671d350f..0488bce4663 100644
--- a/spec/requests/api/markdown_spec.rb
+++ b/spec/requests/api/markdown_spec.rb
@@ -71,7 +71,7 @@ RSpec.describe API::Markdown do
end
context "when authorized" do
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
it_behaves_like "rendered markdown text without GFM"
end
@@ -97,7 +97,7 @@ RSpec.describe API::Markdown do
context "with project" do
let(:params) { { text: text, gfm: true, project: project.full_path } }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
it "renders markdown text" do
expect(response).to have_gitlab_http_status(:created)
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index 02061bb8ab6..6186a43f992 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -416,6 +416,8 @@ RSpec.describe API::Members do
end
it "returns 409 if member already exists" do
+ source.add_guest(stranger)
+
post api("/#{source_type.pluralize}/#{source.id}/members", maintainer),
params: { user_id: maintainer.id, access_level: Member::MAINTAINER }
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index a751f785913..9e6fea9e5b4 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -436,6 +436,26 @@ RSpec.describe API::MergeRequests do
response_dates = json_response.map { |merge_request| merge_request['created_at'] }
expect(response_dates).to eq(response_dates.sort)
end
+
+ context 'returns an array of merge_requests ordered by title' do
+ it 'asc when requested' do
+ path = endpoint_path + '?order_by=title&sort=asc'
+
+ get api(path, user)
+
+ response_titles = json_response.map { |merge_request| merge_request['title'] }
+ expect(response_titles).to eq(response_titles.sort)
+ end
+
+ it 'desc when requested' do
+ path = endpoint_path + '?order_by=title&sort=desc'
+
+ get api(path, user)
+
+ response_titles = json_response.map { |merge_request| merge_request['title'] }
+ expect(response_titles).to eq(response_titles.sort.reverse)
+ end
+ end
end
context 'NOT params' do
@@ -985,14 +1005,6 @@ RSpec.describe API::MergeRequests do
it_behaves_like 'merge requests list'
- context 'when :api_caching_merge_requests is disabled' do
- before do
- stub_feature_flags(api_caching_merge_requests: false)
- end
-
- it_behaves_like 'merge requests list'
- end
-
it "returns 404 for non public projects" do
project = create(:project, :private)
@@ -2876,7 +2888,7 @@ RSpec.describe API::MergeRequests do
it 'is false for an unauthorized user' do
expect do
- put api("/projects/#{target_project.id}/merge_requests/#{merge_request.iid}", target_project.owner), params: { state_event: 'close', remove_source_branch: true }
+ put api("/projects/#{target_project.id}/merge_requests/#{merge_request.iid}", target_project.first_owner), params: { state_event: 'close', remove_source_branch: true }
end.not_to change { merge_request.reload.merge_params }
expect(response).to have_gitlab_http_status(:ok)
@@ -3324,6 +3336,18 @@ RSpec.describe API::MergeRequests do
end
end
+ context 'when merge request branch does not allow force push' do
+ before do
+ create(:protected_branch, project: project, name: merge_request.source_branch, allow_force_push: false)
+ end
+
+ it 'returns 403' do
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/rebase", user)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
it 'returns 403 if the user cannot push to the branch' do
guest = create(:user)
project.add_guest(guest)
diff --git a/spec/requests/api/package_files_spec.rb b/spec/requests/api/package_files_spec.rb
index a7e6a97fd0e..01c7ef1476f 100644
--- a/spec/requests/api/package_files_spec.rb
+++ b/spec/requests/api/package_files_spec.rb
@@ -87,18 +87,6 @@ RSpec.describe API::PackageFiles do
expect(package_file_ids).not_to include(package_file_pending_destruction.id)
end
-
- context 'with packages_installable_package_files disabled' do
- before do
- stub_feature_flags(packages_installable_package_files: false)
- end
-
- it 'returns them' do
- get api(url, user)
-
- expect(package_file_ids).to include(package_file_pending_destruction.id)
- end
- end
end
end
end
@@ -186,18 +174,6 @@ RSpec.describe API::PackageFiles do
expect(response).to have_gitlab_http_status(:not_found)
end
-
- context 'with packages_installable_package_files disabled' do
- before do
- stub_feature_flags(packages_installable_package_files: false)
- end
-
- it 'can be accessed', :aggregate_failures do
- expect { api_request }.not_to change { package.package_files.pending_destruction.count }
-
- expect(response).to have_gitlab_http_status(:no_content)
- end
- end
end
end
end
diff --git a/spec/requests/api/project_attributes.yml b/spec/requests/api/project_attributes.yml
index 01d2fb18f00..8a6e87944ec 100644
--- a/spec/requests/api/project_attributes.yml
+++ b/spec/requests/api/project_attributes.yml
@@ -121,7 +121,6 @@ project_feature:
- created_at
- metrics_dashboard_access_level
- project_id
- - requirements_access_level
- security_and_compliance_access_level
- updated_at
computed_attributes:
@@ -139,6 +138,7 @@ project_setting:
- has_confluence
- has_shimo
- has_vulnerabilities
+ - legacy_open_source_license_available
- prevent_merge_without_jira_issue
- warn_about_potentially_unwanted_characters
- previous_default_branch
diff --git a/spec/requests/api/project_clusters_spec.rb b/spec/requests/api/project_clusters_spec.rb
index 253b61e5865..b83b41a881a 100644
--- a/spec/requests/api/project_clusters_spec.rb
+++ b/spec/requests/api/project_clusters_spec.rb
@@ -5,13 +5,15 @@ require 'spec_helper'
RSpec.describe API::ProjectClusters do
include KubernetesHelpers
- let_it_be(:current_user) { create(:user) }
+ let_it_be(:maintainer_user) { create(:user) }
let_it_be(:developer_user) { create(:user) }
+ let_it_be(:reporter_user) { create(:user) }
let_it_be(:project) { create(:project) }
before do
- project.add_maintainer(current_user)
+ project.add_maintainer(maintainer_user)
project.add_developer(developer_user)
+ project.add_reporter(reporter_user)
end
describe 'GET /projects/:id/clusters' do
@@ -24,7 +26,7 @@ RSpec.describe API::ProjectClusters do
context 'non-authorized user' do
it 'responds with 403' do
- get api("/projects/#{project.id}/clusters", developer_user)
+ get api("/projects/#{project.id}/clusters", reporter_user)
expect(response).to have_gitlab_http_status(:forbidden)
end
@@ -32,7 +34,7 @@ RSpec.describe API::ProjectClusters do
context 'authorized user' do
before do
- get api("/projects/#{project.id}/clusters", current_user)
+ get api("/projects/#{project.id}/clusters", developer_user)
end
it 'includes pagination headers' do
@@ -61,13 +63,13 @@ RSpec.describe API::ProjectClusters do
let(:cluster) do
create(:cluster, :project, :provided_by_gcp, :with_domain,
platform_kubernetes: platform_kubernetes,
- user: current_user,
+ user: maintainer_user,
projects: [project])
end
context 'non-authorized user' do
it 'responds with 403' do
- get api("/projects/#{project.id}/clusters/#{cluster_id}", developer_user)
+ get api("/projects/#{project.id}/clusters/#{cluster_id}", reporter_user)
expect(response).to have_gitlab_http_status(:forbidden)
end
@@ -75,7 +77,7 @@ RSpec.describe API::ProjectClusters do
context 'authorized user' do
before do
- get api("/projects/#{project.id}/clusters/#{cluster_id}", current_user)
+ get api("/projects/#{project.id}/clusters/#{cluster_id}", developer_user)
end
it 'returns specific cluster' do
@@ -111,8 +113,8 @@ RSpec.describe API::ProjectClusters do
it 'returns user information' do
user = json_response['user']
- expect(user['id']).to eq(current_user.id)
- expect(user['username']).to eq(current_user.username)
+ expect(user['id']).to eq(maintainer_user.id)
+ expect(user['username']).to eq(maintainer_user.username)
end
it 'returns GCP provider information' do
@@ -156,7 +158,7 @@ RSpec.describe API::ProjectClusters do
let(:management_project_id) { management_project.id }
before do
- management_project.add_maintainer(current_user)
+ management_project.add_maintainer(maintainer_user)
end
let(:platform_kubernetes_attributes) do
@@ -190,7 +192,7 @@ RSpec.describe API::ProjectClusters do
context 'authorized user' do
before do
- post api("/projects/#{project.id}/clusters/user", current_user), params: cluster_params
+ post api("/projects/#{project.id}/clusters/user", maintainer_user), params: cluster_params
end
context 'with valid params' do
@@ -317,7 +319,7 @@ RSpec.describe API::ProjectClusters do
create(:cluster, :provided_by_gcp, :project,
projects: [project])
- post api("/projects/#{project.id}/clusters/user", current_user), params: cluster_params
+ post api("/projects/#{project.id}/clusters/user", maintainer_user), params: cluster_params
end
it 'responds with 201' do
@@ -369,9 +371,9 @@ RSpec.describe API::ProjectClusters do
context 'authorized user' do
before do
- management_project.add_maintainer(current_user)
+ management_project.add_maintainer(maintainer_user)
- put api("/projects/#{project.id}/clusters/#{cluster.id}", current_user), params: update_params
+ put api("/projects/#{project.id}/clusters/#{cluster.id}", maintainer_user), params: update_params
cluster.reload
end
@@ -501,7 +503,7 @@ RSpec.describe API::ProjectClusters do
context 'authorized user' do
before do
- delete api("/projects/#{project.id}/clusters/#{cluster.id}", current_user), params: cluster_params
+ delete api("/projects/#{project.id}/clusters/#{cluster.id}", maintainer_user), params: cluster_params
end
it 'deletes the cluster' do
diff --git a/spec/requests/api/project_export_spec.rb b/spec/requests/api/project_export_spec.rb
index b9c458373a8..2bc31153f2c 100644
--- a/spec/requests/api/project_export_spec.rb
+++ b/spec/requests/api/project_export_spec.rb
@@ -450,7 +450,7 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache do
expect_next_instance_of(Projects::ImportExport::ExportService) do |service|
expect(service).to receive(:execute)
end
- post api(path, project.owner), params: params
+ post api(path, project.first_owner), params: params
expect(response).to have_gitlab_http_status(:accepted)
end
diff --git a/spec/requests/api/project_snapshots_spec.rb b/spec/requests/api/project_snapshots_spec.rb
index 33c86d56ed4..bf78ff56206 100644
--- a/spec/requests/api/project_snapshots_spec.rb
+++ b/spec/requests/api/project_snapshots_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe API::ProjectSnapshots do
end
it 'returns authentication error as project owner' do
- get api("/projects/#{project.id}/snapshot", project.owner)
+ get api("/projects/#{project.id}/snapshot", project.first_owner)
expect(response).to have_gitlab_http_status(:forbidden)
end
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index bf41a808219..02df82d14a8 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -30,7 +30,7 @@ RSpec.shared_examples 'languages and percentages JSON response' do
context 'when the languages were detected before' do
before do
- Projects::DetectRepositoryLanguagesService.new(project, project.owner).execute
+ Projects::DetectRepositoryLanguagesService.new(project, project.first_owner).execute
end
it 'returns the detection from the database' do
@@ -2166,6 +2166,7 @@ RSpec.describe API::Projects do
approvals_before_merge
compliance_frameworks
mirror
+ requirements_access_level
requirements_enabled
security_and_compliance_enabled
issues_template
@@ -2710,7 +2711,7 @@ RSpec.describe API::Projects do
it 'returns the project users' do
get api("/projects/#{project.id}/users", current_user)
- user = project.namespace.owner
+ user = project.namespace.first_owner
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb
index 21a8622e08d..f42fc7aabc2 100644
--- a/spec/requests/api/repositories_spec.rb
+++ b/spec/requests/api/repositories_spec.rb
@@ -561,17 +561,6 @@ RSpec.describe API::Repositories do
let(:request) { get api(route, guest) }
end
end
-
- context 'api_caching_rate_limit_repository_compare is disabled' do
- before do
- stub_feature_flags(api_caching_rate_limit_repository_compare: false)
- end
-
- it_behaves_like 'repository compare' do
- let(:project) { create(:project, :public, :repository) }
- let(:current_user) { nil }
- end
- end
end
describe 'GET /projects/:id/repository/contributors' do
diff --git a/spec/requests/api/rubygem_packages_spec.rb b/spec/requests/api/rubygem_packages_spec.rb
index 0e63a7269e7..f0408d94137 100644
--- a/spec/requests/api/rubygem_packages_spec.rb
+++ b/spec/requests/api/rubygem_packages_spec.rb
@@ -187,19 +187,6 @@ RSpec.describe API::RubygemPackages do
expect(response).to have_gitlab_http_status(:ok)
expect(response.body).not_to eq(package_file_pending_destruction.file.file.read)
end
-
- context 'with packages_installable_package_files disabled' do
- before do
- stub_feature_flags(packages_installable_package_files: false)
- end
-
- it 'returns them' do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.body).to eq(package_file_pending_destruction.file.file.read)
- end
- end
end
end
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index 7e940d52a41..f7048a1ca6b 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -32,6 +32,8 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting do
expect(json_response['dsa_key_restriction']).to eq(0)
expect(json_response['ecdsa_key_restriction']).to eq(0)
expect(json_response['ed25519_key_restriction']).to eq(0)
+ expect(json_response['ecdsa_sk_key_restriction']).to eq(0)
+ expect(json_response['ed25519_sk_key_restriction']).to eq(0)
expect(json_response['performance_bar_allowed_group_id']).to be_nil
expect(json_response['allow_local_requests_from_hooks_and_services']).to be(false)
expect(json_response['allow_local_requests_from_web_hooks_and_services']).to be(false)
@@ -49,6 +51,9 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting do
expect(json_response['whats_new_variant']).to eq('all_tiers')
expect(json_response['user_deactivation_emails_enabled']).to be(true)
expect(json_response['suggest_pipeline_enabled']).to be(true)
+ expect(json_response['runner_token_expiration_interval']).to be_nil
+ expect(json_response['group_runner_token_expiration_interval']).to be_nil
+ expect(json_response['project_runner_token_expiration_interval']).to be_nil
end
end
@@ -111,6 +116,8 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting do
dsa_key_restriction: 2048,
ecdsa_key_restriction: 384,
ed25519_key_restriction: 256,
+ ecdsa_sk_key_restriction: 256,
+ ed25519_sk_key_restriction: 256,
enforce_terms: true,
terms: 'Hello world!',
performance_bar_allowed_group_path: group.full_path,
@@ -137,7 +144,8 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting do
personal_access_token_prefix: "GL-",
user_deactivation_emails_enabled: false,
admin_mode: true,
- suggest_pipeline_enabled: false
+ suggest_pipeline_enabled: false,
+ users_get_by_id_limit: 456
}
expect(response).to have_gitlab_http_status(:ok)
@@ -163,6 +171,8 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting do
expect(json_response['dsa_key_restriction']).to eq(2048)
expect(json_response['ecdsa_key_restriction']).to eq(384)
expect(json_response['ed25519_key_restriction']).to eq(256)
+ expect(json_response['ecdsa_sk_key_restriction']).to eq(256)
+ expect(json_response['ed25519_sk_key_restriction']).to eq(256)
expect(json_response['enforce_terms']).to be(true)
expect(json_response['terms']).to eq('Hello world!')
expect(json_response['performance_bar_allowed_group_id']).to eq(group.id)
@@ -190,6 +200,7 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting do
expect(json_response['admin_mode']).to be(true)
expect(json_response['user_deactivation_emails_enabled']).to be(false)
expect(json_response['suggest_pipeline_enabled']).to be(false)
+ expect(json_response['users_get_by_id_limit']).to eq(456)
end
end
@@ -644,5 +655,37 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting do
end
end
end
+
+ context 'runner token expiration_intervals' do
+ it 'updates the settings' do
+ put api("/application/settings", admin), params: {
+ runner_token_expiration_interval: 3600,
+ group_runner_token_expiration_interval: 3600 * 2,
+ project_runner_token_expiration_interval: 3600 * 3
+ }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to include(
+ 'runner_token_expiration_interval' => 3600,
+ 'group_runner_token_expiration_interval' => 3600 * 2,
+ 'project_runner_token_expiration_interval' => 3600 * 3
+ )
+ end
+
+ it 'updates the settings with empty values' do
+ put api("/application/settings", admin), params: {
+ runner_token_expiration_interval: nil,
+ group_runner_token_expiration_interval: nil,
+ project_runner_token_expiration_interval: nil
+ }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to include(
+ 'runner_token_expiration_interval' => nil,
+ 'group_runner_token_expiration_interval' => nil,
+ 'project_runner_token_expiration_interval' => nil
+ )
+ end
+ end
end
end
diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb
index bb56192a2ff..3558babf2f1 100644
--- a/spec/requests/api/tags_spec.rb
+++ b/spec/requests/api/tags_spec.rb
@@ -16,250 +16,232 @@ RSpec.describe API::Tags do
project.add_developer(user)
end
- describe 'GET /projects/:id/repository/tags' do
+ describe 'GET /projects/:id/repository/tags', :use_clean_rails_memory_store_caching do
before do
stub_feature_flags(tag_list_keyset_pagination: false)
end
- shared_examples "get repository tags" do
- let(:route) { "/projects/#{project_id}/repository/tags" }
+ let(:route) { "/projects/#{project_id}/repository/tags" }
- context 'sorting' do
- let(:current_user) { user }
+ context 'sorting' do
+ let(:current_user) { user }
- it 'sorts by descending order by default' do
- get api(route, current_user)
+ it 'sorts by descending order by default' do
+ get api(route, current_user)
- desc_order_tags = project.repository.tags.sort_by { |tag| tag.dereferenced_target.committed_date }
- desc_order_tags.reverse!.map! { |tag| tag.dereferenced_target.id }
+ desc_order_tags = project.repository.tags.sort_by { |tag| tag.dereferenced_target.committed_date }
+ desc_order_tags.reverse!.map! { |tag| tag.dereferenced_target.id }
- expect(json_response.map { |tag| tag['commit']['id'] }).to eq(desc_order_tags)
- end
+ expect(json_response.map { |tag| tag['commit']['id'] }).to eq(desc_order_tags)
+ end
- it 'sorts by ascending order if specified' do
- get api("#{route}?sort=asc", current_user)
+ it 'sorts by ascending order if specified' do
+ get api("#{route}?sort=asc", current_user)
- asc_order_tags = project.repository.tags.sort_by { |tag| tag.dereferenced_target.committed_date }
- asc_order_tags.map! { |tag| tag.dereferenced_target.id }
+ asc_order_tags = project.repository.tags.sort_by { |tag| tag.dereferenced_target.committed_date }
+ asc_order_tags.map! { |tag| tag.dereferenced_target.id }
- expect(json_response.map { |tag| tag['commit']['id'] }).to eq(asc_order_tags)
- end
+ expect(json_response.map { |tag| tag['commit']['id'] }).to eq(asc_order_tags)
+ end
- it 'sorts by name in descending order when requested' do
- get api("#{route}?order_by=name", current_user)
+ it 'sorts by name in descending order when requested' do
+ get api("#{route}?order_by=name", current_user)
- ordered_by_name = project.repository.tags.map { |tag| tag.name }.sort.reverse
+ ordered_by_name = project.repository.tags.map { |tag| tag.name }.sort.reverse
- expect(json_response.map { |tag| tag['name'] }).to eq(ordered_by_name)
- end
+ expect(json_response.map { |tag| tag['name'] }).to eq(ordered_by_name)
+ end
- it 'sorts by name in ascending order when requested' do
- get api("#{route}?order_by=name&sort=asc", current_user)
+ it 'sorts by name in ascending order when requested' do
+ get api("#{route}?order_by=name&sort=asc", current_user)
- ordered_by_name = project.repository.tags.map { |tag| tag.name }.sort
+ ordered_by_name = project.repository.tags.map { |tag| tag.name }.sort
- expect(json_response.map { |tag| tag['name'] }).to eq(ordered_by_name)
- end
+ expect(json_response.map { |tag| tag['name'] }).to eq(ordered_by_name)
end
+ end
- context 'searching' do
- it 'only returns searched tags' do
- get api("#{route}", user), params: { search: 'v1.1.0' }
+ context 'searching' do
+ it 'only returns searched tags' do
+ get api("#{route}", user), params: { search: 'v1.1.0' }
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.size).to eq(1)
- expect(json_response[0]['name']).to eq('v1.1.0')
- end
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(1)
+ expect(json_response[0]['name']).to eq('v1.1.0')
end
+ end
- shared_examples_for 'repository tags' do
- it 'returns the repository tags' do
- get api(route, current_user)
+ shared_examples_for 'repository tags' do
+ it 'returns the repository tags' do
+ get api(route, current_user)
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('public_api/v4/tags')
- expect(response).to include_pagination_headers
- expect(json_response.map { |r| r['name'] }).to include(tag_name)
- end
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('public_api/v4/tags')
+ expect(response).to include_pagination_headers
+ expect(json_response.map { |r| r['name'] }).to include(tag_name)
+ end
- context 'when repository is disabled' do
- include_context 'disabled repository'
+ context 'when repository is disabled' do
+ include_context 'disabled repository'
- it_behaves_like '403 response' do
- let(:request) { get api(route, current_user) }
- end
+ it_behaves_like '403 response' do
+ let(:request) { get api(route, current_user) }
end
end
+ end
- context 'when unauthenticated', 'and project is public' do
- let(:project) { create(:project, :public, :repository) }
+ context 'when unauthenticated', 'and project is public' do
+ let(:project) { create(:project, :public, :repository) }
- it_behaves_like 'repository tags'
- end
+ it_behaves_like 'repository tags'
+ end
- context 'when unauthenticated', 'and project is private' do
- it_behaves_like '404 response' do
- let(:request) { get api(route) }
- let(:message) { '404 Project Not Found' }
- end
+ context 'when unauthenticated', 'and project is private' do
+ it_behaves_like '404 response' do
+ let(:request) { get api(route) }
+ let(:message) { '404 Project Not Found' }
end
+ end
- context 'when authenticated', 'as a maintainer' do
- let(:current_user) { user }
+ context 'when authenticated', 'as a maintainer' do
+ let(:current_user) { user }
- it_behaves_like 'repository tags'
+ it_behaves_like 'repository tags'
- context 'requesting with the escaped project full path' do
- let(:project_id) { CGI.escape(project.full_path) }
+ context 'requesting with the escaped project full path' do
+ let(:project_id) { CGI.escape(project.full_path) }
- it_behaves_like 'repository tags'
- end
+ it_behaves_like 'repository tags'
end
+ end
- context 'when authenticated', 'as a guest' do
- it_behaves_like '403 response' do
- let(:request) { get api(route, guest) }
- end
+ context 'when authenticated', 'as a guest' do
+ it_behaves_like '403 response' do
+ let(:request) { get api(route, guest) }
end
+ end
- context 'with releases' do
- let(:description) { 'Awesome release!' }
+ context 'with releases' do
+ let(:description) { 'Awesome release!' }
- let!(:release) do
- create(:release,
- :legacy,
- project: project,
- tag: tag_name,
- description: description)
- end
+ let!(:release) do
+ create(:release,
+ :legacy,
+ project: project,
+ tag: tag_name,
+ description: description)
+ end
- it 'returns an array of project tags with release info' do
- get api(route, user)
+ it 'returns an array of project tags with release info' do
+ get api(route, user)
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('public_api/v4/tags')
- expect(response).to include_pagination_headers
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('public_api/v4/tags')
+ expect(response).to include_pagination_headers
- expected_tag = json_response.find { |r| r['name'] == tag_name }
- expect(expected_tag['message']).to eq(tag_message)
- expect(expected_tag['release']['description']).to eq(description)
- end
+ expected_tag = json_response.find { |r| r['name'] == tag_name }
+ expect(expected_tag['message']).to eq(tag_message)
+ expect(expected_tag['release']['description']).to eq(description)
end
+ end
- context 'with keyset pagination on', :aggregate_errors do
- before do
- stub_feature_flags(tag_list_keyset_pagination: true)
- end
+ context 'with keyset pagination on', :aggregate_errors do
+ before do
+ stub_feature_flags(tag_list_keyset_pagination: true)
+ end
- context 'with keyset pagination option' do
- let(:base_params) { { pagination: 'keyset' } }
+ context 'with keyset pagination option' do
+ let(:base_params) { { pagination: 'keyset' } }
- context 'with gitaly pagination params' do
- context 'with high limit' do
- let(:params) { base_params.merge(per_page: 100) }
+ context 'with gitaly pagination params' do
+ context 'with high limit' do
+ let(:params) { base_params.merge(per_page: 100) }
- it 'returns all repository tags' do
- get api(route, user), params: params
+ it 'returns all repository tags' do
+ get api(route, user), params: params
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('public_api/v4/tags')
- expect(response.headers).not_to include('Link')
- tag_names = json_response.map { |x| x['name'] }
- expect(tag_names).to match_array(project.repository.tag_names)
- end
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('public_api/v4/tags')
+ expect(response.headers).not_to include('Link')
+ tag_names = json_response.map { |x| x['name'] }
+ expect(tag_names).to match_array(project.repository.tag_names)
end
+ end
- context 'with low limit' do
- let(:params) { base_params.merge(per_page: 2) }
+ context 'with low limit' do
+ let(:params) { base_params.merge(per_page: 2) }
- it 'returns limited repository tags' do
- get api(route, user), params: params
+ it 'returns limited repository tags' do
+ get api(route, user), params: params
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('public_api/v4/tags')
- expect(response.headers).to include('Link')
- tag_names = json_response.map { |x| x['name'] }
- expect(tag_names).to match_array(%w(v1.1.0 v1.1.1))
- end
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('public_api/v4/tags')
+ expect(response.headers).to include('Link')
+ tag_names = json_response.map { |x| x['name'] }
+ expect(tag_names).to match_array(%w(v1.1.0 v1.1.1))
end
+ end
- context 'with missing page token' do
- let(:params) { base_params.merge(page_token: 'unknown') }
+ context 'with missing page token' do
+ let(:params) { base_params.merge(page_token: 'unknown') }
- it_behaves_like '422 response' do
- let(:request) { get api(route, user), params: params }
- let(:message) { 'Invalid page token: refs/tags/unknown' }
- end
+ it_behaves_like '422 response' do
+ let(:request) { get api(route, user), params: params }
+ let(:message) { 'Invalid page token: refs/tags/unknown' }
end
end
end
end
end
- context ":api_caching_tags flag enabled", :use_clean_rails_memory_store_caching do
+ describe "cache expiry" do
+ let(:route) { "/projects/#{project_id}/repository/tags" }
+ let(:current_user) { user }
+
before do
- stub_feature_flags(api_caching_tags: true)
+ # Set the cache
+ get api(route, current_user)
end
- it_behaves_like "get repository tags"
-
- describe "cache expiry" do
- let(:route) { "/projects/#{project_id}/repository/tags" }
- let(:current_user) { user }
+ it "is cached" do
+ expect(API::Entities::Tag).not_to receive(:represent)
- before do
- # Set the cache
- get api(route, current_user)
- end
+ get api(route, current_user)
+ end
- it "is cached" do
- expect(API::Entities::Tag).not_to receive(:represent)
+ shared_examples "cache expired" do
+ it "isn't cached" do
+ expect(API::Entities::Tag).to receive(:represent).exactly(3).times
get api(route, current_user)
end
+ end
- shared_examples "cache expired" do
- it "isn't cached" do
- expect(API::Entities::Tag).to receive(:represent).exactly(3).times
-
- get api(route, current_user)
- end
- end
-
- context "when protected tag is changed" do
- before do
- create(:protected_tag, name: tag_name, project: project)
- end
-
- it_behaves_like "cache expired"
+ context "when protected tag is changed" do
+ before do
+ create(:protected_tag, name: tag_name, project: project)
end
- context "when release is changed" do
- before do
- create(:release, :legacy, project: project, tag: tag_name)
- end
+ it_behaves_like "cache expired"
+ end
- it_behaves_like "cache expired"
+ context "when release is changed" do
+ before do
+ create(:release, :legacy, project: project, tag: tag_name)
end
- context "when project is changed" do
- before do
- project.touch
- end
+ it_behaves_like "cache expired"
+ end
- it_behaves_like "cache expired"
+ context "when project is changed" do
+ before do
+ project.touch
end
- end
- end
- context ":api_caching_tags flag disabled" do
- before do
- stub_feature_flags(api_caching_tags: false)
+ it_behaves_like "cache expired"
end
-
- it_behaves_like "get repository tags"
end
context 'when gitaly is unavailable' do
diff --git a/spec/requests/api/terraform/modules/v1/packages_spec.rb b/spec/requests/api/terraform/modules/v1/packages_spec.rb
index 8160113bbde..7d86244cb1b 100644
--- a/spec/requests/api/terraform/modules/v1/packages_spec.rb
+++ b/spec/requests/api/terraform/modules/v1/packages_spec.rb
@@ -232,20 +232,6 @@ RSpec.describe API::Terraform::Modules::V1::Packages do
expect(response.body).not_to eq(package_file_pending_destruction.file.file.read)
expect(response.body).to eq(package_file.file.file.read)
end
-
- context 'with packages_installable_package_files disabled' do
- before do
- stub_feature_flags(packages_installable_package_files: false)
- end
-
- it 'returns them' do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.body).to eq(package_file_pending_destruction.file.file.read)
- expect(response.body).not_to eq(package_file.file.file.read)
- end
- end
end
end
diff --git a/spec/requests/api/usage_data_spec.rb b/spec/requests/api/usage_data_spec.rb
index bacaf960e6a..aefccc4fbf7 100644
--- a/spec/requests/api/usage_data_spec.rb
+++ b/spec/requests/api/usage_data_spec.rb
@@ -57,13 +57,26 @@ RSpec.describe API::UsageData do
end
end
- %w[merge_requests commits].each do |postfix|
- context 'with correct params' do
- let(:known_event_postfix) { postfix }
+ context 'with correct params' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:prefix, :event) do
+ 'static_site_editor' | 'merge_requests'
+ 'static_site_editor' | 'commits'
+ end
+
+ before do
+ stub_application_setting(usage_ping_enabled: true)
+ stub_feature_flags(usage_data_api: true)
+ allow(Gitlab::RequestForgeryProtection).to receive(:verified?).and_return(true)
+ stub_feature_flags("usage_data_#{prefix}_#{event}" => true)
+ end
+
+ with_them do
+ it 'returns status :ok' do
+ expect(Gitlab::UsageDataCounters::BaseCounter).to receive(:count).with(event)
- it 'returns status ok' do
- expect(Gitlab::UsageDataCounters::BaseCounter).to receive(:count).with(known_event_postfix)
- post api(endpoint, user), params: { event: known_event }
+ post api(endpoint, user), params: { event: "#{prefix}_#{event}" }
expect(response).to have_gitlab_http_status(:ok)
end
@@ -73,6 +86,7 @@ RSpec.describe API::UsageData do
context 'with unknown event' do
before do
skip_feature_flags_yaml_validation
+ skip_default_enabled_yaml_check
end
it 'returns status ok' do
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 98875d7e8d2..985e07bf174 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -499,7 +499,8 @@ RSpec.describe API::Users do
let_it_be(:user2, reload: true) { create(:user, username: 'another_user') }
before do
- allow(Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:users_get_by_id, scope: user).and_return(false)
+ allow(Gitlab::ApplicationRateLimiter).to receive(:throttled?)
+ .with(:users_get_by_id, scope: user, users_allowlist: []).and_return(false)
end
it "returns a user by id" do
@@ -600,7 +601,7 @@ RSpec.describe API::Users do
context 'when the rate limit is not exceeded' do
it 'returns a success status' do
expect(Gitlab::ApplicationRateLimiter)
- .to receive(:throttled?).with(:users_get_by_id, scope: user)
+ .to receive(:throttled?).with(:users_get_by_id, scope: user, users_allowlist: [])
.and_return(false)
get api("/users/#{user.id}", user)
@@ -613,7 +614,7 @@ RSpec.describe API::Users do
context 'when feature flag is enabled' do
it 'returns "too many requests" status' do
expect(Gitlab::ApplicationRateLimiter)
- .to receive(:throttled?).with(:users_get_by_id, scope: user)
+ .to receive(:throttled?).with(:users_get_by_id, scope: user, users_allowlist: [])
.and_return(true)
get api("/users/#{user.id}", user)
@@ -629,6 +630,24 @@ RSpec.describe API::Users do
expect(response).to have_gitlab_http_status(:ok)
end
+
+ it 'allows users whose username is in the allowlist' do
+ allowlist = [user.username]
+ current_settings = Gitlab::CurrentSettings.current_application_settings
+
+ # Necessary to ensure the same object is returned on each call
+ allow(Gitlab::CurrentSettings).to receive(:current_application_settings).and_return current_settings
+
+ allow(current_settings).to receive(:users_get_by_id_limit_allowlist).and_return(allowlist)
+
+ expect(Gitlab::ApplicationRateLimiter)
+ .to receive(:throttled?).with(:users_get_by_id, scope: user, users_allowlist: allowlist)
+ .and_call_original
+
+ get api("/users/#{user.id}", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
end
context 'when feature flag is disabled' do
diff --git a/spec/requests/boards/lists_controller_spec.rb b/spec/requests/boards/lists_controller_spec.rb
index 4d9f1dace4d..47f4925d5b0 100644
--- a/spec/requests/boards/lists_controller_spec.rb
+++ b/spec/requests/boards/lists_controller_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Boards::ListsController do
describe '#index' do
let(:board) { create(:board) }
- let(:user) { board.project.owner }
+ let(:user) { board.project.first_owner }
it 'does not have N+1 queries' do
login_as(user)
diff --git a/spec/requests/concerns/planning_hierarchy_spec.rb b/spec/requests/concerns/planning_hierarchy_spec.rb
new file mode 100644
index 00000000000..ece9270b3a1
--- /dev/null
+++ b/spec/requests/concerns/planning_hierarchy_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe PlanningHierarchy, type: :request do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
+
+ before do
+ project.add_maintainer(user)
+ sign_in(user)
+ end
+
+ describe 'GET #planning_hierarchy' do
+ it 'renders planning hierarchy' do
+ get project_planning_hierarchy_path(project)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.body).to match(/id="js-work-items-hierarchy"/)
+ end
+ end
+end
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index 623cf24b9cb..340ed7bde53 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -836,6 +836,24 @@ RSpec.describe 'Git HTTP requests' do
end
end
end
+
+ context "when the user is admin" do
+ let(:admin) { create(:admin) }
+ let(:env) { { user: admin.username, password: admin.password } }
+
+ # Currently, the admin mode is bypassed for git operations.
+ # Once the admin mode is considered for git operations, this test will fail.
+ # Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/296509
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it_behaves_like 'pulls are allowed'
+ it_behaves_like 'pushes are allowed'
+ end
+
+ context 'when admin mode is disabled' do
+ it_behaves_like 'pulls are allowed'
+ it_behaves_like 'pushes are allowed'
+ end
+ end
end
end
@@ -929,10 +947,10 @@ RSpec.describe 'Git HTTP requests' do
context 'when admin mode is disabled' do
it_behaves_like 'can download code only'
- it 'downloads from other project get status 404' do
+ it 'downloads from other project get status 403' do
clone_get "#{other_project.full_path}.git", user: 'gitlab-ci-token', password: build.token
- expect(response).to have_gitlab_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
@@ -1534,10 +1552,10 @@ RSpec.describe 'Git HTTP requests' do
context 'when admin mode is disabled' do
it_behaves_like 'can download code only'
- it 'downloads from other project get status 404' do
+ it 'downloads from other project get status 403' do
clone_get "#{other_project.full_path}.git", user: 'gitlab-ci-token', password: build.token
- expect(response).to have_gitlab_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
diff --git a/spec/requests/import/gitlab_projects_controller_spec.rb b/spec/requests/import/gitlab_projects_controller_spec.rb
index 58843a7fec4..eed035608d0 100644
--- a/spec/requests/import/gitlab_projects_controller_spec.rb
+++ b/spec/requests/import/gitlab_projects_controller_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe Import::GitlabProjectsController do
include_context 'workhorse headers'
let_it_be(:namespace) { create(:namespace) }
- let_it_be(:user) { namespace.owner }
+ let_it_be(:user) { namespace.first_owner }
before do
login_as(user)
diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb
index f89395fccaf..4b2f11da77e 100644
--- a/spec/requests/lfs_http_spec.rb
+++ b/spec/requests/lfs_http_spec.rb
@@ -546,14 +546,6 @@ RSpec.describe 'Git LFS API and storage' do
expect(lfs_object.reload.projects.pluck(:id)).to match_array([other_project.id, project.id])
end
-
- context 'when feature flag is disabled' do
- before do
- stub_feature_flags(lfs_auto_link_fork_source: false)
- end
-
- it_behaves_like 'batch upload with existing LFS object'
- end
end
context 'when user does not have access to parent' do
diff --git a/spec/requests/openid_connect_spec.rb b/spec/requests/openid_connect_spec.rb
index 8ee752da44e..70a310ba0d5 100644
--- a/spec/requests/openid_connect_spec.rb
+++ b/spec/requests/openid_connect_spec.rb
@@ -275,7 +275,7 @@ RSpec.describe 'OpenID Connect requests' do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['issuer']).to eq('http://localhost')
expect(json_response['jwks_uri']).to eq('http://www.example.com/oauth/discovery/keys')
- expect(json_response['scopes_supported']).to eq(%w[api read_user read_api read_repository write_repository sudo openid profile email])
+ expect(json_response['scopes_supported']).to match_array %w[api read_user read_api read_repository write_repository sudo openid profile email]
end
context 'with a cross-origin request' do
@@ -285,7 +285,7 @@ RSpec.describe 'OpenID Connect requests' do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['issuer']).to eq('http://localhost')
expect(json_response['jwks_uri']).to eq('http://www.example.com/oauth/discovery/keys')
- expect(json_response['scopes_supported']).to eq(%w[api read_user read_api read_repository write_repository sudo openid profile email])
+ expect(json_response['scopes_supported']).to match_array %w[api read_user read_api read_repository write_repository sudo openid profile email]
end
it_behaves_like 'cross-origin GET request'
diff --git a/spec/requests/projects/cluster_agents_controller_spec.rb b/spec/requests/projects/cluster_agents_controller_spec.rb
index e4c4f537699..914d5b17ba8 100644
--- a/spec/requests/projects/cluster_agents_controller_spec.rb
+++ b/spec/requests/projects/cluster_agents_controller_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe Projects::ClusterAgentsController do
let_it_be(:user) { create(:user) }
before do
- project.add_developer(user)
+ project.add_reporter(user)
sign_in(user)
subject
end
diff --git a/spec/requests/projects/clusters/integrations_controller_spec.rb b/spec/requests/projects/clusters/integrations_controller_spec.rb
index 323c61b9af3..c05e3da675c 100644
--- a/spec/requests/projects/clusters/integrations_controller_spec.rb
+++ b/spec/requests/projects/clusters/integrations_controller_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe Projects::Clusters::IntegrationsController do
describe 'POST create_or_update' do
let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
let(:project) { cluster.project }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
it_behaves_like '#create_or_update action' do
let(:path) { create_or_update_project_cluster_integration_path(project, cluster) }
diff --git a/spec/requests/projects/google_cloud/deployments_controller_spec.rb b/spec/requests/projects/google_cloud/deployments_controller_spec.rb
index a5eccc43147..fd356bc61c7 100644
--- a/spec/requests/projects/google_cloud/deployments_controller_spec.rb
+++ b/spec/requests/projects/google_cloud/deployments_controller_spec.rb
@@ -3,7 +3,8 @@
require 'spec_helper'
RSpec.describe Projects::GoogleCloud::DeploymentsController do
- let_it_be(:project) { create(:project, :public) }
+ let_it_be(:project) { create(:project, :public, :repository) }
+ let_it_be(:repository) { project.repository }
let_it_be(:user_guest) { create(:user) }
let_it_be(:user_developer) { create(:user) }
@@ -36,8 +37,6 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do
it 'returns not found on GET request' do
urls_list.each do |url|
unauthorized_members.each do |unauthorized_member|
- sign_in(unauthorized_member)
-
get url
expect(response).to have_gitlab_http_status(:not_found)
@@ -65,18 +64,63 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do
let_it_be(:url) { "#{project_google_cloud_deployments_cloud_run_path(project)}" }
before do
+ sign_in(user_maintainer)
+
allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |client|
allow(client).to receive(:validate_token).and_return(true)
end
end
- it 'renders placeholder' do
- authorized_members.each do |authorized_member|
- sign_in(authorized_member)
+ it 'redirects to google_cloud home on enable service error' do
+ # since GPC_PROJECT_ID is not set, enable cloud run service should return an error
+
+ get url
+
+ expect(response).to redirect_to(project_google_cloud_index_path(project))
+ end
+
+ it 'tracks error and redirects to gcp_error' do
+ mock_google_error = Google::Apis::ClientError.new('some_error')
+
+ allow_next_instance_of(GoogleCloud::EnableCloudRunService) do |service|
+ allow(service).to receive(:execute).and_raise(mock_google_error)
+ end
+
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).with(mock_google_error, { project_id: project.id })
+
+ get url
+
+ expect(response).to render_template(:gcp_error)
+ end
+
+ context 'GCP_PROJECT_IDs are defined' do
+ it 'redirects to google_cloud home on generate pipeline error' do
+ allow_next_instance_of(GoogleCloud::EnableCloudRunService) do |enable_cloud_run_service|
+ allow(enable_cloud_run_service).to receive(:execute).and_return({ status: :success })
+ end
+
+ allow_next_instance_of(GoogleCloud::GeneratePipelineService) do |generate_pipeline_service|
+ allow(generate_pipeline_service).to receive(:execute).and_return({ status: :error })
+ end
get url
- expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to redirect_to(project_google_cloud_index_path(project))
+ end
+
+ it 'redirects to create merge request form' do
+ allow_next_instance_of(GoogleCloud::EnableCloudRunService) do |service|
+ allow(service).to receive(:execute).and_return({ status: :success })
+ end
+
+ allow_next_instance_of(GoogleCloud::GeneratePipelineService) do |service|
+ allow(service).to receive(:execute).and_return({ status: :success })
+ end
+
+ get url
+
+ expect(response).to have_gitlab_http_status(:found)
+ expect(response.location).to include(project_new_merge_request_path(project))
end
end
end
diff --git a/spec/requests/projects/google_cloud/service_accounts_controller_spec.rb b/spec/requests/projects/google_cloud/service_accounts_controller_spec.rb
index 6b4d1c490e2..0f243a6a7a9 100644
--- a/spec/requests/projects/google_cloud/service_accounts_controller_spec.rb
+++ b/spec/requests/projects/google_cloud/service_accounts_controller_spec.rb
@@ -2,10 +2,6 @@
require 'spec_helper'
-# Mock Types
-MockGoogleOAuth2Credentials = Struct.new(:app_id, :app_secret)
-MockServiceAccount = Struct.new(:project_id, :unique_id)
-
RSpec.describe Projects::GoogleCloud::ServiceAccountsController do
let_it_be(:project) { create(:project, :public) }
@@ -86,10 +82,12 @@ RSpec.describe Projects::GoogleCloud::ServiceAccountsController do
context 'and user has successfully completed the google oauth2 flow' do
before do
allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |client|
+ mock_service_account = Struct.new(:project_id, :unique_id, :email).new(123, 456, 'em@ai.l')
allow(client).to receive(:validate_token).and_return(true)
allow(client).to receive(:list_projects).and_return([{}, {}, {}])
- allow(client).to receive(:create_service_account).and_return(MockServiceAccount.new(123, 456))
+ allow(client).to receive(:create_service_account).and_return(mock_service_account)
allow(client).to receive(:create_service_account_key).and_return({})
+ allow(client).to receive(:grant_service_account_roles)
end
end
@@ -147,7 +145,8 @@ RSpec.describe Projects::GoogleCloud::ServiceAccountsController do
context 'but gitlab instance is not configured for google oauth2' do
before do
- unconfigured_google_oauth2 = MockGoogleOAuth2Credentials.new('', '')
+ unconfigured_google_oauth2 = Struct.new(:app_id, :app_secret)
+ .new('', '')
allow(Gitlab::Auth::OAuth::Provider).to receive(:config_for)
.with('google_oauth2')
.and_return(unconfigured_google_oauth2)
diff --git a/spec/requests/projects/merge_requests/creations_spec.rb b/spec/requests/projects/merge_requests/creations_spec.rb
index 0a3e663444f..842ad01656e 100644
--- a/spec/requests/projects/merge_requests/creations_spec.rb
+++ b/spec/requests/projects/merge_requests/creations_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe 'merge requests creations' do
include ProjectForksHelper
let(:project) { create(:project, :repository) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
before do
login_as(user)
diff --git a/spec/requests/projects/merge_requests_discussions_spec.rb b/spec/requests/projects/merge_requests_discussions_spec.rb
index 6cf7bfb1795..c761af86c16 100644
--- a/spec/requests/projects/merge_requests_discussions_spec.rb
+++ b/spec/requests/projects/merge_requests_discussions_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'merge requests discussions' do
# Further tests can be found at merge_requests_controller_spec.rb
describe 'GET /:namespace/:project/-/merge_requests/:iid/discussions' do
let(:project) { create(:project, :repository, :public) }
- let(:owner) { project.owner }
+ let(:owner) { project.first_owner }
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
diff --git a/spec/requests/projects/merge_requests_spec.rb b/spec/requests/projects/merge_requests_spec.rb
index 59fde803560..91153554e55 100644
--- a/spec/requests/projects/merge_requests_spec.rb
+++ b/spec/requests/projects/merge_requests_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe 'merge requests actions' do
reviewers: [user2])
end
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:user2) { create(:user) }
before do
diff --git a/spec/requests/projects/metrics_dashboard_spec.rb b/spec/requests/projects/metrics_dashboard_spec.rb
index c248463faa3..61bfe1c6edf 100644
--- a/spec/requests/projects/metrics_dashboard_spec.rb
+++ b/spec/requests/projects/metrics_dashboard_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'Projects::MetricsDashboardController' do
let_it_be(:project) { create(:project) }
let_it_be(:environment) { create(:environment, project: project) }
let_it_be(:environment2) { create(:environment, project: project) }
- let_it_be(:user) { project.owner }
+ let_it_be(:user) { project.first_owner }
before do
project.add_developer(user)
diff --git a/spec/requests/projects/noteable_notes_spec.rb b/spec/requests/projects/noteable_notes_spec.rb
index 2bf1ffb2edc..44ee50ca002 100644
--- a/spec/requests/projects/noteable_notes_spec.rb
+++ b/spec/requests/projects/noteable_notes_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe 'Project noteable notes' do
let(:etag_store) { Gitlab::EtagCaching::Store.new }
let(:notes_path) { project_noteable_notes_path(project, target_type: merge_request.class.name.underscore, target_id: merge_request.id) }
let(:project) { merge_request.project }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:response_etag) { response.headers['ETag'] }
let(:stored_etag) { "W/\"#{etag_store.get(notes_path)}\"" }
diff --git a/spec/requests/rack_attack_global_spec.rb b/spec/requests/rack_attack_global_spec.rb
index 793438808a5..f2126e3cf9c 100644
--- a/spec/requests/rack_attack_global_spec.rb
+++ b/spec/requests/rack_attack_global_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe 'Rack Attack global throttles', :use_clean_rails_memory_store_caching do
include RackAttackSpecHelpers
+ include SessionHelpers
let(:settings) { Gitlab::CurrentSettings.current_application_settings }
@@ -63,6 +64,22 @@ RSpec.describe 'Rack Attack global throttles', :use_clean_rails_memory_store_cac
end
end
+ describe 'API requests from the frontend', :api, :clean_gitlab_redis_sessions do
+ context 'when unauthenticated' do
+ it_behaves_like 'rate-limited frontend API requests' do
+ let(:throttle_setting_prefix) { 'throttle_unauthenticated' }
+ end
+ end
+
+ context 'when authenticated' do
+ it_behaves_like 'rate-limited frontend API requests' do
+ let_it_be(:personal_access_token) { create(:personal_access_token) }
+
+ let(:throttle_setting_prefix) { 'throttle_authenticated' }
+ end
+ end
+ end
+
describe 'API requests authenticated with personal access token', :api do
let_it_be(:user) { create(:user) }
let_it_be(:token) { create(:personal_access_token, user: user) }
@@ -184,6 +201,7 @@ RSpec.describe 'Rack Attack global throttles', :use_clean_rails_memory_store_cac
context 'unauthenticated requests' do
let(:protected_path_that_does_not_require_authentication) do
+ # This is one of the default values for `application_settings.protected_paths`
'/users/sign_in'
end
@@ -227,6 +245,20 @@ RSpec.describe 'Rack Attack global throttles', :use_clean_rails_memory_store_cac
expect_rejection { post protected_path_that_does_not_require_authentication, params: post_params }
end
+ it 'allows non-POST requests to protected paths over the rate limit' do
+ (1 + requests_per_period).times do
+ get protected_path_that_does_not_require_authentication
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ it 'allows POST requests to unprotected paths over the rate limit' do
+ (1 + requests_per_period).times do
+ post '/api/graphql'
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
it_behaves_like 'tracking when dry-run mode is set' do
let(:throttle_name) { 'throttle_unauthenticated_protected_paths' }
end
diff --git a/spec/requests/recursive_webhook_detection_spec.rb b/spec/requests/recursive_webhook_detection_spec.rb
index a3014bf1d73..fe27c90b6c8 100644
--- a/spec/requests/recursive_webhook_detection_spec.rb
+++ b/spec/requests/recursive_webhook_detection_spec.rb
@@ -11,6 +11,11 @@ RSpec.describe 'Recursive webhook detection', :sidekiq_inline, :clean_gitlab_red
let_it_be(:project_hook) { create(:project_hook, project: project, merge_requests_events: true) }
let_it_be(:system_hook) { create(:system_hook, merge_requests_events: true) }
+ let(:stubbed_project_hook_hostname) { stubbed_hostname(project_hook.url, hostname: stubbed_project_hook_ip_address) }
+ let(:stubbed_system_hook_hostname) { stubbed_hostname(system_hook.url, hostname: stubbed_system_hook_ip_address) }
+ let(:stubbed_project_hook_ip_address) { '8.8.8.8' }
+ let(:stubbed_system_hook_ip_address) { '8.8.8.9' }
+
# Trigger a change to the merge request to fire the webhooks.
def trigger_web_hooks
params = { merge_request: { description: FFaker::Lorem.sentence } }
@@ -18,8 +23,8 @@ RSpec.describe 'Recursive webhook detection', :sidekiq_inline, :clean_gitlab_red
end
def stub_requests
- stub_full_request(project_hook.url, method: :post, ip_address: '8.8.8.8')
- stub_full_request(system_hook.url, method: :post, ip_address: '8.8.8.9')
+ stub_full_request(project_hook.url, method: :post, ip_address: stubbed_project_hook_ip_address)
+ stub_full_request(system_hook.url, method: :post, ip_address: stubbed_system_hook_ip_address)
end
before do
@@ -37,10 +42,10 @@ RSpec.describe 'Recursive webhook detection', :sidekiq_inline, :clean_gitlab_red
trigger_web_hooks
- expect(WebMock).to have_requested(:post, stubbed_hostname(project_hook.url))
+ expect(WebMock).to have_requested(:post, stubbed_project_hook_hostname)
.with { |req| req.headers['X-Gitlab-Event-Uuid'] == uuid }
.once
- expect(WebMock).to have_requested(:post, stubbed_hostname(system_hook.url))
+ expect(WebMock).to have_requested(:post, stubbed_system_hook_hostname)
.with { |req| req.headers['X-Gitlab-Event-Uuid'] == uuid }
.once
end
@@ -54,24 +59,24 @@ RSpec.describe 'Recursive webhook detection', :sidekiq_inline, :clean_gitlab_red
Gitlab::WebHooks::RecursionDetection.set_request_uuid(nil)
end
- it 'executes all webhooks and logs an error for the recursive hook', :aggregate_failures do
+ it 'blocks and logs an error for the recursive webhook, but execute the non-recursive webhook', :aggregate_failures do
stub_requests
expect(Gitlab::AuthLogger).to receive(:error).with(
include(
- message: 'Webhook recursion detected and will be blocked in future',
+ message: 'Recursive webhook blocked from executing',
hook_id: project_hook.id,
recursion_detection: {
uuid: uuid,
ids: [project_hook.id]
}
)
- ).twice # Twice: once in `#async_execute`, and again in `#execute`.
+ ).once
trigger_web_hooks
- expect(WebMock).to have_requested(:post, stubbed_hostname(project_hook.url)).once
- expect(WebMock).to have_requested(:post, stubbed_hostname(system_hook.url)).once
+ expect(WebMock).not_to have_requested(:post, stubbed_project_hook_hostname)
+ expect(WebMock).to have_requested(:post, stubbed_system_hook_hostname).once
end
end
@@ -87,35 +92,35 @@ RSpec.describe 'Recursive webhook detection', :sidekiq_inline, :clean_gitlab_red
Gitlab::WebHooks::RecursionDetection.set_request_uuid(nil)
end
- it 'executes and logs errors for all hooks', :aggregate_failures do
+ it 'blocks and logs errors for all hooks', :aggregate_failures do
stub_requests
previous_hook_ids = previous_hooks.map(&:id)
expect(Gitlab::AuthLogger).to receive(:error).with(
include(
- message: 'Webhook recursion detected and will be blocked in future',
+ message: 'Recursive webhook blocked from executing',
hook_id: project_hook.id,
recursion_detection: {
uuid: uuid,
ids: include(*previous_hook_ids)
}
)
- ).twice
+ ).once
expect(Gitlab::AuthLogger).to receive(:error).with(
include(
- message: 'Webhook recursion detected and will be blocked in future',
+ message: 'Recursive webhook blocked from executing',
hook_id: system_hook.id,
recursion_detection: {
uuid: uuid,
ids: include(*previous_hook_ids)
}
)
- ).twice
+ ).once
trigger_web_hooks
- expect(WebMock).to have_requested(:post, stubbed_hostname(project_hook.url)).once
- expect(WebMock).to have_requested(:post, stubbed_hostname(system_hook.url)).once
+ expect(WebMock).not_to have_requested(:post, stubbed_project_hook_hostname)
+ expect(WebMock).not_to have_requested(:post, stubbed_system_hook_hostname)
end
end
end
@@ -156,10 +161,10 @@ RSpec.describe 'Recursive webhook detection', :sidekiq_inline, :clean_gitlab_red
expect(uuid_headers).to all(be_present)
expect(uuid_headers.uniq.length).to eq(2)
- expect(WebMock).to have_requested(:post, stubbed_hostname(project_hook.url))
+ expect(WebMock).to have_requested(:post, stubbed_project_hook_hostname)
.with { |req| uuid_headers.include?(req.headers['X-Gitlab-Event-Uuid']) }
.once
- expect(WebMock).to have_requested(:post, stubbed_hostname(system_hook.url))
+ expect(WebMock).to have_requested(:post, stubbed_system_hook_hostname)
.with { |req| uuid_headers.include?(req.headers['X-Gitlab-Event-Uuid']) }
.once
end
@@ -175,8 +180,8 @@ RSpec.describe 'Recursive webhook detection', :sidekiq_inline, :clean_gitlab_red
expect(uuid_headers).to all(be_present)
expect(uuid_headers.length).to eq(4)
expect(uuid_headers.uniq.length).to eq(4)
- expect(WebMock).to have_requested(:post, stubbed_hostname(project_hook.url)).twice
- expect(WebMock).to have_requested(:post, stubbed_hostname(system_hook.url)).twice
+ expect(WebMock).to have_requested(:post, stubbed_project_hook_hostname).twice
+ expect(WebMock).to have_requested(:post, stubbed_system_hook_hostname).twice
end
end
end
diff --git a/spec/requests/users_controller_spec.rb b/spec/requests/users_controller_spec.rb
index dacc11eece7..d033ce15b00 100644
--- a/spec/requests/users_controller_spec.rb
+++ b/spec/requests/users_controller_spec.rb
@@ -506,6 +506,7 @@ RSpec.describe UsersController do
describe 'GET #contributed' do
let(:project) { create(:project, :public) }
+ let(:aimed_for_deletion_project) { create(:project, :public, :archived, marked_for_deletion_at: 3.days.ago) }
subject do
get user_contributed_projects_url author.username, format: format
@@ -516,7 +517,10 @@ RSpec.describe UsersController do
project.add_developer(public_user)
project.add_developer(private_user)
+ aimed_for_deletion_project.add_developer(public_user)
+ aimed_for_deletion_project.add_developer(private_user)
create(:push_event, project: project, author: author)
+ create(:push_event, project: aimed_for_deletion_project, author: author)
subject
end
@@ -526,6 +530,11 @@ RSpec.describe UsersController do
expect(response).to have_gitlab_http_status(:ok)
expect(response.body).not_to be_empty
end
+
+ it 'does not list projects aimed for deletion' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(assigns(:contributed_projects)).to eq([project])
+ end
end
%i(html json).each do |format|
@@ -557,6 +566,7 @@ RSpec.describe UsersController do
describe 'GET #starred' do
let(:project) { create(:project, :public) }
+ let(:aimed_for_deletion_project) { create(:project, :public, :archived, marked_for_deletion_at: 3.days.ago) }
subject do
get user_starred_projects_url author.username, format: format
@@ -574,6 +584,11 @@ RSpec.describe UsersController do
expect(response).to have_gitlab_http_status(:ok)
expect(response.body).not_to be_empty
end
+
+ it 'does not list projects aimed for deletion' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(assigns(:starred_projects)).to eq([project])
+ end
end
%i(html json).each do |format|
@@ -634,13 +649,13 @@ RSpec.describe UsersController do
end
describe 'GET #exists' do
- before do
- sign_in(user)
+ context 'when user exists' do
+ before do
+ sign_in(user)
- allow(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).and_return(false)
- end
+ allow(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).and_return(false)
+ end
- context 'when user exists' do
it 'returns JSON indicating the user exists' do
get user_exists_url user.username
@@ -661,6 +676,15 @@ RSpec.describe UsersController do
end
context 'when the user does not exist' do
+ it 'will not show a signup page if registration is disabled' do
+ stub_application_setting(signup_enabled: false)
+ get user_exists_url 'foo'
+
+ expected_json = { error: "You must be authenticated to access this path." }.to_json
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ expect(response.body).to eq(expected_json)
+ end
+
it 'returns JSON indicating the user does not exist' do
get user_exists_url 'foo'
diff --git a/spec/rubocop/cop/file_decompression_spec.rb b/spec/rubocop/cop/file_decompression_spec.rb
new file mode 100644
index 00000000000..7be1a784001
--- /dev/null
+++ b/spec/rubocop/cop/file_decompression_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require_relative '../../../rubocop/cop/file_decompression'
+
+RSpec.describe RuboCop::Cop::FileDecompression do
+ subject(:cop) { described_class.new }
+
+ it 'does not flag when using a system command not related to file decompression' do
+ expect_no_offenses('system("ls")')
+ end
+
+ described_class::FORBIDDEN_COMMANDS.map { [_1, '^' * _1.length] }.each do |cmd, len|
+ it "flags the when using '#{cmd}' system command" do
+ expect_offense(<<~SOURCE)
+ system('#{cmd}')
+ ^^^^^^^^#{len}^^ While extracting files check for symlink to avoid arbitrary file reading[...]
+ SOURCE
+
+ expect_offense(<<~SOURCE)
+ exec('#{cmd}')
+ ^^^^^^#{len}^^ While extracting files check for symlink to avoid arbitrary file reading[...]
+ SOURCE
+
+ expect_offense(<<~SOURCE)
+ Kernel.spawn('#{cmd}')
+ ^^^^^^^^^^^^^^#{len}^^ While extracting files check for symlink to avoid arbitrary file reading[...]
+ SOURCE
+
+ expect_offense(<<~SOURCE)
+ IO.popen('#{cmd}')
+ ^^^^^^^^^^#{len}^^ While extracting files check for symlink to avoid arbitrary file reading[...]
+ SOURCE
+ end
+
+ it "flags the when using '#{cmd}' subshell command" do
+ expect_offense(<<~SOURCE)
+ `#{cmd}`
+ ^#{len}^ While extracting files check for symlink to avoid arbitrary file reading[...]
+ SOURCE
+
+ expect_offense(<<~SOURCE)
+ %x(#{cmd})
+ ^^^#{len}^ While extracting files check for symlink to avoid arbitrary file reading[...]
+ SOURCE
+ end
+ end
+end
diff --git a/spec/rubocop/cop/gitlab/event_store_subscriber_spec.rb b/spec/rubocop/cop/gitlab/event_store_subscriber_spec.rb
new file mode 100644
index 00000000000..e17fb71f9bc
--- /dev/null
+++ b/spec/rubocop/cop/gitlab/event_store_subscriber_spec.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+require_relative '../../../../rubocop/cop/gitlab/event_store_subscriber'
+
+RSpec.describe RuboCop::Cop::Gitlab::EventStoreSubscriber do
+ subject(:cop) { described_class.new }
+
+ context 'when an event store subscriber overrides #perform' do
+ it 'registers an offense' do
+ expect_offense(<<~WORKER)
+ class SomeWorker
+ include Gitlab::EventStore::Subscriber
+
+ def perform(*args)
+ ^^^^^^^^^^^^^^^^^^ Do not override `perform` in a `Gitlab::EventStore::Subscriber`.
+ end
+
+ def handle_event(event); end
+ end
+ WORKER
+ end
+ end
+
+ context 'when an event store subscriber does not override #perform' do
+ it 'does not register an offense' do
+ expect_no_offenses(<<~WORKER)
+ class SomeWorker
+ include Gitlab::EventStore::Subscriber
+
+ def handle_event(event); end
+ end
+ WORKER
+ end
+ end
+
+ context 'when an event store subscriber does not implement #handle_event' do
+ it 'registers an offense' do
+ expect_offense(<<~WORKER)
+ class SomeWorker
+ include Gitlab::EventStore::Subscriber
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ A `Gitlab::EventStore::Subscriber` must implement `#handle_event(event)`.
+ end
+ WORKER
+ end
+ end
+
+ context 'when a Sidekiq worker overrides #perform' do
+ it 'does not register an offense' do
+ expect_no_offenses(<<~WORKER)
+ class SomeWorker
+ include ApplicationWorker
+
+ def perform(*args); end
+ end
+ WORKER
+ end
+ end
+
+ context 'when a Sidekiq worker implements #handle_event' do
+ it 'does not register an offense' do
+ expect_no_offenses(<<~WORKER)
+ class SomeWorker
+ include ApplicationWorker
+
+ def handle_event(event); end
+ end
+ WORKER
+ end
+ end
+
+ context 'a non worker class' do
+ it 'does not register an offense' do
+ expect_no_offenses(<<~MODEL)
+ class Model < ApplicationRecord
+ include ActiveSupport::Concern
+ end
+ MODEL
+ end
+ end
+end
diff --git a/spec/rubocop/cop/migration/schedule_async_spec.rb b/spec/rubocop/cop/migration/schedule_async_spec.rb
index 5f848dd9b66..09d2c77369c 100644
--- a/spec/rubocop/cop/migration/schedule_async_spec.rb
+++ b/spec/rubocop/cop/migration/schedule_async_spec.rb
@@ -53,6 +53,17 @@ RSpec.describe RuboCop::Cop::Migration::ScheduleAsync do
end
end
+ context 'CiDatabaseWorker.perform_async' do
+ it 'adds an offense when calling `CiDatabaseWorker.peform_async`' do
+ expect_offense(<<~RUBY)
+ def up
+ CiDatabaseWorker.perform_async(ClazzName, "Bar", "Baz")
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't call [...]
+ end
+ RUBY
+ end
+ end
+
context 'BackgroundMigrationWorker.perform_in' do
it 'adds an offense' do
expect_offense(<<~RUBY)
@@ -65,6 +76,18 @@ RSpec.describe RuboCop::Cop::Migration::ScheduleAsync do
end
end
+ context 'CiDatabaseWorker.perform_in' do
+ it 'adds an offense' do
+ expect_offense(<<~RUBY)
+ def up
+ CiDatabaseWorker
+ ^^^^^^^^^^^^^^^^ Don't call [...]
+ .perform_in(delay, ClazzName, "Bar", "Baz")
+ end
+ RUBY
+ end
+ end
+
context 'BackgroundMigrationWorker.bulk_perform_async' do
it 'adds an offense' do
expect_offense(<<~RUBY)
@@ -77,12 +100,36 @@ RSpec.describe RuboCop::Cop::Migration::ScheduleAsync do
end
end
+ context 'CiDatabaseWorker.bulk_perform_async' do
+ it 'adds an offense' do
+ expect_offense(<<~RUBY)
+ def up
+ BackgroundMigration::CiDatabaseWorker
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't call [...]
+ .bulk_perform_async(jobs)
+ end
+ RUBY
+ end
+ end
+
context 'BackgroundMigrationWorker.bulk_perform_in' do
it 'adds an offense' do
expect_offense(<<~RUBY)
def up
- BackgroundMigrationWorker
- ^^^^^^^^^^^^^^^^^^^^^^^^^ Don't call [...]
+ ::BackgroundMigrationWorker
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't call [...]
+ .bulk_perform_in(5.minutes, jobs)
+ end
+ RUBY
+ end
+ end
+
+ context 'CiDatabaseWorker.bulk_perform_in' do
+ it 'adds an offense' do
+ expect_offense(<<~RUBY)
+ def up
+ ::BackgroundMigration::CiDatabaseWorker
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't call [...]
.bulk_perform_in(5.minutes, jobs)
end
RUBY
diff --git a/spec/rubocop/cop/scalability/bulk_perform_with_context_spec.rb b/spec/rubocop/cop/scalability/bulk_perform_with_context_spec.rb
index 01afaf3acb6..74912b53d37 100644
--- a/spec/rubocop/cop/scalability/bulk_perform_with_context_spec.rb
+++ b/spec/rubocop/cop/scalability/bulk_perform_with_context_spec.rb
@@ -39,9 +39,15 @@ RSpec.describe RuboCop::Cop::Scalability::BulkPerformWithContext do
CODE
end
- it "does not add an offense for scheduling BackgroundMigrations" do
+ it "does not add an offense for scheduling on the BackgroundMigrationWorker" do
expect_no_offenses(<<~CODE)
BackgroundMigrationWorker.bulk_perform_in(args)
CODE
end
+
+ it "does not add an offense for scheduling on the CiDatabaseWorker" do
+ expect_no_offenses(<<~CODE)
+ BackgroundMigration::CiDatabaseWorker.bulk_perform_in(args)
+ CODE
+ end
end
diff --git a/spec/rubocop/cop/sidekiq_load_balancing/worker_data_consistency_with_deduplication_spec.rb b/spec/rubocop/cop/sidekiq_load_balancing/worker_data_consistency_with_deduplication_spec.rb
deleted file mode 100644
index 6e7212b1002..00000000000
--- a/spec/rubocop/cop/sidekiq_load_balancing/worker_data_consistency_with_deduplication_spec.rb
+++ /dev/null
@@ -1,166 +0,0 @@
-# frozen_string_literal: true
-
-require 'fast_spec_helper'
-require 'rspec-parameterized'
-require_relative '../../../../rubocop/cop/sidekiq_load_balancing/worker_data_consistency_with_deduplication'
-
-RSpec.describe RuboCop::Cop::SidekiqLoadBalancing::WorkerDataConsistencyWithDeduplication do
- using RSpec::Parameterized::TableSyntax
-
- subject(:cop) { described_class.new }
-
- before do
- allow(cop)
- .to receive(:in_worker?)
- .and_return(true)
- end
-
- where(:data_consistency) { %i[delayed sticky] }
-
- with_them do
- let(:strategy) { described_class::DEFAULT_STRATEGY }
- let(:corrected) do
- <<~CORRECTED
- class SomeWorker
- include ApplicationWorker
-
- data_consistency :#{data_consistency}
-
- deduplicate #{strategy}, including_scheduled: true
- idempotent!
- end
- CORRECTED
- end
-
- context 'when deduplication strategy is not explicitly set' do
- it 'registers an offense and corrects using default strategy' do
- expect_offense(<<~CODE)
- class SomeWorker
- include ApplicationWorker
-
- data_consistency :#{data_consistency}
-
- idempotent!
- ^^^^^^^^^^^ Workers that declare either `:sticky` or `:delayed` data consistency [...]
- end
- CODE
-
- expect_correction(corrected)
- end
-
- context 'when identation is different' do
- let(:corrected) do
- <<~CORRECTED
- class SomeWorker
- include ApplicationWorker
-
- data_consistency :#{data_consistency}
-
- deduplicate #{strategy}, including_scheduled: true
- idempotent!
- end
- CORRECTED
- end
-
- it 'registers an offense and corrects with correct identation' do
- expect_offense(<<~CODE)
- class SomeWorker
- include ApplicationWorker
-
- data_consistency :#{data_consistency}
-
- idempotent!
- ^^^^^^^^^^^ Workers that declare either `:sticky` or `:delayed` data consistency [...]
- end
- CODE
-
- expect_correction(corrected)
- end
- end
- end
-
- context 'when deduplication strategy does not include including_scheduling option' do
- let(:strategy) { ':until_executed' }
-
- it 'registers an offense and corrects' do
- expect_offense(<<~CODE)
- class SomeWorker
- include ApplicationWorker
-
- data_consistency :#{data_consistency}
-
- deduplicate :until_executed
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^ Workers that declare either `:sticky` or `:delayed` data consistency [...]
- idempotent!
- end
- CODE
-
- expect_correction(corrected)
- end
- end
-
- context 'when deduplication strategy has including_scheduling option disabled' do
- let(:strategy) { ':until_executed' }
-
- it 'registers an offense and corrects' do
- expect_offense(<<~CODE)
- class SomeWorker
- include ApplicationWorker
-
- data_consistency :#{data_consistency}
-
- deduplicate :until_executed, including_scheduled: false
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Workers that declare either `:sticky` or `:delayed` data consistency [...]
- idempotent!
- end
- CODE
-
- expect_correction(corrected)
- end
- end
-
- context "when deduplication strategy is :none" do
- it 'does not register an offense' do
- expect_no_offenses(<<~CODE)
- class SomeWorker
- include ApplicationWorker
-
- data_consistency :always
-
- deduplicate :none
- idempotent!
- end
- CODE
- end
- end
-
- context "when deduplication strategy has including_scheduling option enabled" do
- it 'does not register an offense' do
- expect_no_offenses(<<~CODE)
- class SomeWorker
- include ApplicationWorker
-
- data_consistency :always
-
- deduplicate :until_executing, including_scheduled: true
- idempotent!
- end
- CODE
- end
- end
- end
-
- context "data_consistency: :always" do
- it 'does not register an offense' do
- expect_no_offenses(<<~CODE)
- class SomeWorker
- include ApplicationWorker
-
- data_consistency :always
-
- idempotent!
- end
- CODE
- end
- end
-end
diff --git a/spec/serializers/build_details_entity_spec.rb b/spec/serializers/build_details_entity_spec.rb
index a24841fe286..da2734feb51 100644
--- a/spec/serializers/build_details_entity_spec.rb
+++ b/spec/serializers/build_details_entity_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe BuildDetailsEntity do
describe '#as_json' do
let(:project) { create(:project, :repository) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:pipeline) { create(:ci_pipeline, project: project) }
let(:build) { create(:ci_build, :failed, pipeline: pipeline) }
let(:request) { double('request', project: project) }
diff --git a/spec/serializers/ci/lint/result_serializer_spec.rb b/spec/serializers/ci/lint/result_serializer_spec.rb
index a834ea05e14..3fdaba1808a 100644
--- a/spec/serializers/ci/lint/result_serializer_spec.rb
+++ b/spec/serializers/ci/lint/result_serializer_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Ci::Lint::ResultSerializer, :aggregate_failures do
let(:result) do
Gitlab::Ci::Lint
- .new(project: project, current_user: project.owner)
+ .new(project: project, current_user: project.first_owner)
.validate(yaml_content, dry_run: false)
end
@@ -64,7 +64,7 @@ RSpec.describe Ci::Lint::ResultSerializer, :aggregate_failures do
context 'when dry run is enabled' do
let(:result) do
Gitlab::Ci::Lint
- .new(project: project, current_user: project.owner)
+ .new(project: project, current_user: project.first_owner)
.validate(yaml_content, dry_run: true)
end
diff --git a/spec/serializers/codequality_degradation_entity_spec.rb b/spec/serializers/codequality_degradation_entity_spec.rb
index 315f00baa72..f56420bfdbd 100644
--- a/spec/serializers/codequality_degradation_entity_spec.rb
+++ b/spec/serializers/codequality_degradation_entity_spec.rb
@@ -30,6 +30,21 @@ RSpec.describe CodequalityDegradationEntity do
expect(subject[:line]).to eq(10)
end
end
+
+ context 'when severity is capitalized' do
+ let(:codequality_degradation) { build(:codequality_degradation_3) }
+
+ before do
+ codequality_degradation[:severity] = 'MINOR'
+ end
+
+ it 'lowercases severity', :aggregate_failures do
+ expect(subject[:description]).to eq("Avoid parameter lists longer than 5 parameters. [12/5]")
+ expect(subject[:severity]).to eq("minor")
+ expect(subject[:file_path]).to eq("file_b.rb")
+ expect(subject[:line]).to eq(10)
+ end
+ end
end
end
end
diff --git a/spec/serializers/deployment_cluster_entity_spec.rb b/spec/serializers/deployment_cluster_entity_spec.rb
index 95f2f8ce6fc..419ae746b74 100644
--- a/spec/serializers/deployment_cluster_entity_spec.rb
+++ b/spec/serializers/deployment_cluster_entity_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe DeploymentClusterEntity do
subject { described_class.new(deployment, request: request).as_json }
let(:maintainer) { create(:user) }
- let(:developer) { create(:user) }
+ let(:reporter) { create(:user) }
let(:current_user) { maintainer }
let(:request) { double(:request, current_user: current_user) }
let(:project) { create(:project) }
@@ -17,7 +17,7 @@ RSpec.describe DeploymentClusterEntity do
before do
project.add_maintainer(maintainer)
- project.add_developer(developer)
+ project.add_reporter(reporter)
end
it 'matches deployment_cluster entity schema' do
@@ -31,7 +31,7 @@ RSpec.describe DeploymentClusterEntity do
end
context 'when the user does not have permission to view the cluster' do
- let(:current_user) { developer }
+ let(:current_user) { reporter }
it 'does not include the path nor the namespace' do
expect(subject[:path]).to be_nil
diff --git a/spec/serializers/diff_file_base_entity_spec.rb b/spec/serializers/diff_file_base_entity_spec.rb
index 99dbaff4b7e..11ceb7991d7 100644
--- a/spec/serializers/diff_file_base_entity_spec.rb
+++ b/spec/serializers/diff_file_base_entity_spec.rb
@@ -142,7 +142,7 @@ RSpec.describe DiffFileBaseEntity do
end
context 'when source_project and target_project are different' do
- let(:target_project) { fork_project(source_project, source_project.owner, repository: true) }
+ let(:target_project) { fork_project(source_project, source_project.first_owner, repository: true) }
it 'returns the merge_request ide route with the target_project as param' do
expect(entity[:ide_edit_path]).to eq("#{expected_merge_request_path}?target_project=#{ERB::Util.url_encode(target_project.full_path)}")
diff --git a/spec/serializers/environment_serializer_spec.rb b/spec/serializers/environment_serializer_spec.rb
index 80b6f00d8c9..658062c9461 100644
--- a/spec/serializers/environment_serializer_spec.rb
+++ b/spec/serializers/environment_serializer_spec.rb
@@ -204,21 +204,6 @@ RSpec.describe EnvironmentSerializer do
json
end
-
- # Including for test coverage pipeline failure, remove along with feature flag.
- context 'when custom preload feature is disabled' do
- before do
- Feature.disable(:custom_preloader_for_deployments)
- end
-
- it 'avoids N+1 database queries' do
- control_count = ActiveRecord::QueryRecorder.new { json }.count
-
- create_environment_with_associations(project)
-
- expect { json }.not_to exceed_query_limit(control_count)
- end
- end
end
def create_environment_with_associations(project)
diff --git a/spec/serializers/group_child_entity_spec.rb b/spec/serializers/group_child_entity_spec.rb
index 59340181075..469189c0768 100644
--- a/spec/serializers/group_child_entity_spec.rb
+++ b/spec/serializers/group_child_entity_spec.rb
@@ -6,7 +6,8 @@ RSpec.describe GroupChildEntity do
include ExternalAuthorizationServiceHelpers
include Gitlab::Routing.url_helpers
- let(:user) { create(:user) }
+ let_it_be(:user) { create(:user) }
+
let(:request) { double('request') }
let(:entity) { described_class.new(object, request: request) }
@@ -103,6 +104,22 @@ RSpec.describe GroupChildEntity do
expect(json[:can_leave]).to be_truthy
end
+ it 'allows an owner to delete the group' do
+ expect(json[:can_remove]).to be_truthy
+ end
+
+ it 'allows admin to delete the group', :enable_admin_mode do
+ allow(request).to receive(:current_user).and_return(create(:admin))
+
+ expect(json[:can_remove]).to be_truthy
+ end
+
+ it 'disallows a maintainer to delete the group' do
+ object.add_maintainer(user)
+
+ expect(json[:can_remove]).to be_falsy
+ end
+
it 'has the correct edit path' do
expect(json[:edit_path]).to eq(edit_group_path(object))
end
diff --git a/spec/serializers/issue_sidebar_basic_entity_spec.rb b/spec/serializers/issue_sidebar_basic_entity_spec.rb
new file mode 100644
index 00000000000..da07290f349
--- /dev/null
+++ b/spec/serializers/issue_sidebar_basic_entity_spec.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe IssueSidebarBasicEntity do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { create(:user, developer_projects: [project]) }
+ let_it_be(:issue) { create(:issue, project: project, assignees: [user]) }
+
+ let(:serializer) { IssueSerializer.new(current_user: user, project: project) }
+
+ subject(:entity) { serializer.represent(issue, serializer: 'sidebar') }
+
+ it 'contains keys related to issuables' do
+ expect(entity).to include(
+ :id, :iid, :type, :author_id, :project_id, :discussion_locked, :reference, :milestone,
+ :labels, :current_user, :issuable_json_path, :namespace_path, :project_path,
+ :project_full_path, :project_issuables_path, :create_todo_path, :project_milestones_path,
+ :project_labels_path, :toggle_subscription_path, :move_issue_path, :projects_autocomplete_path,
+ :project_emails_disabled, :create_note_email, :supports_time_tracking, :supports_milestone,
+ :supports_severity, :supports_escalation
+ )
+ end
+
+ it 'contains attributes related to the issue' do
+ expect(entity).to include(:due_date, :confidential, :severity)
+ end
+
+ describe 'current_user' do
+ it 'contains attributes related to the current user' do
+ expect(entity[:current_user]).to include(
+ :id, :name, :username, :state, :avatar_url, :web_url, :todo,
+ :can_edit, :can_move, :can_admin_label
+ )
+ end
+
+ describe 'can_update_escalation_status' do
+ context 'for a standard issue' do
+ it 'is not present' do
+ expect(entity[:current_user]).not_to have_key(:can_update_escalation_status)
+ end
+ end
+
+ context 'for an incident issue' do
+ before do
+ issue.update!(issue_type: Issue.issue_types[:incident])
+ end
+
+ it 'is present and true' do
+ expect(entity[:current_user][:can_update_escalation_status]).to be(true)
+ end
+
+ context 'without permissions' do
+ let(:serializer) { IssueSerializer.new(current_user: create(:user), project: project) }
+
+ it 'is present and false' do
+ expect(entity[:current_user]).to have_key(:can_update_escalation_status)
+ expect(entity[:current_user][:can_update_escalation_status]).to be(false)
+ end
+ end
+
+ context 'with :incident_escalations feature flag disabled' do
+ before do
+ stub_feature_flags(incident_escalations: false)
+ end
+
+ it 'is not present' do
+ expect(entity[:current_user]).not_to include(:can_update_escalation_status)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/serializers/merge_request_poll_cached_widget_entity_spec.rb b/spec/serializers/merge_request_poll_cached_widget_entity_spec.rb
index ecc93219b53..e9c1fe23855 100644
--- a/spec/serializers/merge_request_poll_cached_widget_entity_spec.rb
+++ b/spec/serializers/merge_request_poll_cached_widget_entity_spec.rb
@@ -21,12 +21,6 @@ RSpec.describe MergeRequestPollCachedWidgetEntity do
is_expected.to include(:target_branch_sha)
end
- it 'has public_merge_status as merge_status' do
- expect(resource).to receive(:public_merge_status).and_return('checking')
-
- expect(subject[:merge_status]).to eq 'checking'
- end
-
it 'has blob path data' do
allow(resource).to receive_messages(
base_pipeline: pipeline,
@@ -38,6 +32,20 @@ RSpec.describe MergeRequestPollCachedWidgetEntity do
expect(subject[:blob_path]).to include(:head_path)
end
+ describe 'merge_status' do
+ it 'calls for MergeRequest#check_mergeability' do
+ expect(resource).to receive(:check_mergeability).with(async: true)
+
+ subject[:merge_status]
+ end
+
+ it 'has public_merge_status as merge_status' do
+ expect(resource).to receive(:public_merge_status).and_return('checking')
+
+ expect(subject[:merge_status]).to eq 'checking'
+ end
+ end
+
describe 'diverged_commits_count' do
context 'when MR open and its diverging' do
it 'returns diverged commits count' do
diff --git a/spec/serializers/merge_request_poll_widget_entity_spec.rb b/spec/serializers/merge_request_poll_widget_entity_spec.rb
index 3aebe16438c..581efd331ef 100644
--- a/spec/serializers/merge_request_poll_widget_entity_spec.rb
+++ b/spec/serializers/merge_request_poll_widget_entity_spec.rb
@@ -180,16 +180,6 @@ RSpec.describe MergeRequestPollWidgetEntity do
it 'calculates mergeability and returns true' do
expect(subject[:mergeable]).to eq(true)
end
-
- context 'when check_mergeability_async_in_widget is disabled' do
- before do
- stub_feature_flags(check_mergeability_async_in_widget: false)
- end
-
- it 'calculates mergeability and returns true' do
- expect(subject[:mergeable]).to eq(true)
- end
- end
end
end
end
diff --git a/spec/serializers/runner_entity_spec.rb b/spec/serializers/runner_entity_spec.rb
index 39cac65c5ac..f34cb794834 100644
--- a/spec/serializers/runner_entity_spec.rb
+++ b/spec/serializers/runner_entity_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe RunnerEntity do
let(:runner) { create(:ci_runner, :project, projects: [project]) }
let(:entity) { described_class.new(runner, request: request, current_user: user) }
let(:request) { double('request') }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
before do
allow(request).to receive(:current_user).and_return(user)
diff --git a/spec/serializers/test_case_entity_spec.rb b/spec/serializers/test_case_entity_spec.rb
index cdeefd2fec5..03fb88bc6c3 100644
--- a/spec/serializers/test_case_entity_spec.rb
+++ b/spec/serializers/test_case_entity_spec.rb
@@ -41,6 +41,18 @@ RSpec.describe TestCaseEntity do
end
end
+ context 'when no test name is entered' do
+ let(:test_case) { build(:report_test_case, name: "") }
+
+ it 'contains correct test case details' do
+ expect(subject[:status]).to eq('success')
+ expect(subject[:name]).to eq('(No name)')
+ expect(subject[:classname]).to eq('trace')
+ expect(subject[:file]).to eq('spec/trace_spec.rb')
+ expect(subject[:execution_time]).to eq(1.23)
+ end
+ end
+
context 'when attachment is present' do
let(:test_case) { build(:report_test_case, :failed_with_attachment, job: job) }
diff --git a/spec/serializers/trigger_variable_entity_spec.rb b/spec/serializers/trigger_variable_entity_spec.rb
index e90bfc24f9f..deabbb9d54b 100644
--- a/spec/serializers/trigger_variable_entity_spec.rb
+++ b/spec/serializers/trigger_variable_entity_spec.rb
@@ -31,7 +31,7 @@ RSpec.describe TriggerVariableEntity do
end
context 'when user is owner' do
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
it 'exposes the variable value' do
expect(subject).to include(:value)
diff --git a/spec/services/alert_management/alerts/update_service_spec.rb b/spec/services/alert_management/alerts/update_service_spec.rb
index 35697ac79a0..882543fd701 100644
--- a/spec/services/alert_management/alerts/update_service_spec.rb
+++ b/spec/services/alert_management/alerts/update_service_spec.rb
@@ -28,8 +28,11 @@ RSpec.describe AlertManagement::Alerts::UpdateService do
specify { expect { response }.not_to change(Note, :count) }
end
- shared_examples 'adds a system note' do
- specify { expect { response }.to change { alert.reload.notes.count }.by(1) }
+ shared_examples 'adds a system note' do |note_matcher = nil|
+ specify do
+ expect { response }.to change { alert.reload.notes.count }.by(1)
+ expect(alert.notes.last.note).to match(note_matcher) if note_matcher
+ end
end
shared_examples 'error response' do |message|
@@ -288,6 +291,12 @@ RSpec.describe AlertManagement::Alerts::UpdateService do
end
end
end
+
+ context 'when a status change reason is included' do
+ let(:params) { { status: new_status, status_change_reason: ' by changing the incident status' } }
+
+ it_behaves_like 'adds a system note', /changed the status to \*\*Acknowledged\*\* by changing the incident status/
+ end
end
end
end
diff --git a/spec/services/alert_management/create_alert_issue_service_spec.rb b/spec/services/alert_management/create_alert_issue_service_spec.rb
index 55f8e47717c..083e5b8c6f1 100644
--- a/spec/services/alert_management/create_alert_issue_service_spec.rb
+++ b/spec/services/alert_management/create_alert_issue_service_spec.rb
@@ -43,10 +43,10 @@ RSpec.describe AlertManagement::CreateAlertIssueService do
expect(execute).to be_success
end
- it 'updates alert.issue_id' do
+ it 'sets alert.issue_id in the same ActiveRecord query execution' do
execute
- expect(alert.reload.issue_id).to eq(created_issue.id)
+ expect(alert.issue_id).to eq(created_issue.id)
end
it 'creates a system note' do
diff --git a/spec/services/application_settings/update_service_spec.rb b/spec/services/application_settings/update_service_spec.rb
index 5c9d2c5e680..e20d59fb0ef 100644
--- a/spec/services/application_settings/update_service_spec.rb
+++ b/spec/services/application_settings/update_service_spec.rb
@@ -475,6 +475,24 @@ RSpec.describe ApplicationSettings::UpdateService do
end
end
+ context 'when users_get_by_id_limit and users_get_by_id_limit_allowlist_raw are passed' do
+ let(:params) do
+ {
+ users_get_by_id_limit: 456,
+ users_get_by_id_limit_allowlist_raw: 'someone, someone_else'
+ }
+ end
+
+ it 'updates users_get_by_id_limit and users_get_by_id_limit_allowlist value' do
+ subject.execute
+
+ application_settings.reload
+
+ expect(application_settings.users_get_by_id_limit).to eq(456)
+ expect(application_settings.users_get_by_id_limit_allowlist).to eq(%w[someone someone_else])
+ end
+ end
+
context 'when require_admin_approval_after_user_signup changes' do
context 'when it goes from enabled to disabled' do
let(:params) { { require_admin_approval_after_user_signup: false } }
diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb
index 83f77780b80..00841de9ff4 100644
--- a/spec/services/auth/container_registry_authentication_service_spec.rb
+++ b/spec/services/auth/container_registry_authentication_service_spec.rb
@@ -145,28 +145,4 @@ RSpec.describe Auth::ContainerRegistryAuthenticationService do
it_behaves_like 'an unmodified token'
end
end
-
- context 'CDN redirection' do
- include_context 'container registry auth service context'
-
- let_it_be(:current_user) { create(:user) }
- let_it_be(:project) { create(:project) }
- let_it_be(:current_params) { { scopes: ["repository:#{project.full_path}:pull"] } }
-
- before do
- project.add_developer(current_user)
- end
-
- it_behaves_like 'a valid token'
- it { expect(payload['access']).to include(include('cdn_redirect' => true)) }
-
- context 'when the feature flag is disabled' do
- before do
- stub_feature_flags(container_registry_cdn_redirect: false)
- end
-
- it_behaves_like 'a valid token'
- it { expect(payload['access']).not_to include(have_key('cdn_redirect')) }
- end
- end
end
diff --git a/spec/services/branches/create_service_spec.rb b/spec/services/branches/create_service_spec.rb
index 1962aca35e1..0d2f5838574 100644
--- a/spec/services/branches/create_service_spec.rb
+++ b/spec/services/branches/create_service_spec.rb
@@ -65,7 +65,7 @@ RSpec.describe Branches::CreateService do
allow(project.repository).to receive(:add_branch).and_raise(pre_receive_error)
- expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
+ expect(Gitlab::ErrorTracking).to receive(:log_exception).with(
pre_receive_error,
pre_receive_message: raw_message,
branch_name: 'new-feature',
diff --git a/spec/services/ci/copy_cross_database_associations_service_spec.rb b/spec/services/ci/copy_cross_database_associations_service_spec.rb
new file mode 100644
index 00000000000..5938ac258d0
--- /dev/null
+++ b/spec/services/ci/copy_cross_database_associations_service_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::CopyCrossDatabaseAssociationsService do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+ let_it_be(:old_build) { create(:ci_build, pipeline: pipeline) }
+ let_it_be(:new_build) { create(:ci_build, pipeline: pipeline) }
+
+ subject(:execute) { described_class.new.execute(old_build, new_build) }
+
+ describe '#execute' do
+ it 'returns a success response' do
+ expect(execute).to be_success
+ end
+ end
+end
diff --git a/spec/services/ci/create_downstream_pipeline_service_spec.rb b/spec/services/ci/create_downstream_pipeline_service_spec.rb
index d61abf6a6ee..43eb57df66c 100644
--- a/spec/services/ci/create_downstream_pipeline_service_spec.rb
+++ b/spec/services/ci/create_downstream_pipeline_service_spec.rb
@@ -441,44 +441,99 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
end
end
- context 'when relationship between pipelines is cyclical' do
- before do
- pipeline_a = create(:ci_pipeline, project: upstream_project)
- pipeline_b = create(:ci_pipeline, project: downstream_project)
- pipeline_c = create(:ci_pipeline, project: upstream_project)
+ describe 'cyclical dependency detection' do
+ shared_examples 'detects cyclical pipelines' do
+ it 'does not create a new pipeline' do
+ expect { service.execute(bridge) }
+ .not_to change { Ci::Pipeline.count }
+ end
+
+ it 'changes status of the bridge build' do
+ service.execute(bridge)
- create_source_pipeline(pipeline_a, pipeline_b)
- create_source_pipeline(pipeline_b, pipeline_c)
- create_source_pipeline(pipeline_c, upstream_pipeline)
+ expect(bridge.reload).to be_failed
+ expect(bridge.failure_reason).to eq 'pipeline_loop_detected'
+ end
end
- it 'does not create a new pipeline' do
- expect { service.execute(bridge) }
- .not_to change { Ci::Pipeline.count }
+ shared_examples 'passes cyclical pipeline precondition' do
+ it 'creates a new pipeline' do
+ expect { service.execute(bridge) }
+ .to change { Ci::Pipeline.count }
+ end
+
+ it 'expect bridge build not to be failed' do
+ service.execute(bridge)
+
+ expect(bridge.reload).not_to be_failed
+ end
end
- it 'changes status of the bridge build' do
- service.execute(bridge)
+ context 'when pipeline ancestry contains 2 cycles of dependencies' do
+ before do
+ # A(push on master) -> B(pipeline on master) -> A(push on master) ->
+ # B(pipeline on master) -> A(push on master)
+ pipeline_1 = create(:ci_pipeline, project: upstream_project, source: :push)
+ pipeline_2 = create(:ci_pipeline, project: downstream_project, source: :pipeline)
+ pipeline_3 = create(:ci_pipeline, project: upstream_project, source: :push)
+ pipeline_4 = create(:ci_pipeline, project: downstream_project, source: :pipeline)
+
+ create_source_pipeline(pipeline_1, pipeline_2)
+ create_source_pipeline(pipeline_2, pipeline_3)
+ create_source_pipeline(pipeline_3, pipeline_4)
+ create_source_pipeline(pipeline_4, upstream_pipeline)
+ end
- expect(bridge.reload).to be_failed
- expect(bridge.failure_reason).to eq 'pipeline_loop_detected'
+ it_behaves_like 'detects cyclical pipelines'
+
+ context 'when ci_drop_cyclical_triggered_pipelines is not enabled' do
+ before do
+ stub_feature_flags(ci_drop_cyclical_triggered_pipelines: false)
+ end
+
+ it_behaves_like 'passes cyclical pipeline precondition'
+ end
end
- context 'when ci_drop_cyclical_triggered_pipelines is not enabled' do
+ context 'when source in the ancestry differ' do
before do
- stub_feature_flags(ci_drop_cyclical_triggered_pipelines: false)
+ # A(push on master) -> B(pipeline on master) -> A(pipeline on master)
+ pipeline_1 = create(:ci_pipeline, project: upstream_project, source: :push)
+ pipeline_2 = create(:ci_pipeline, project: downstream_project, source: :pipeline)
+ upstream_pipeline.update!(source: :pipeline)
+
+ create_source_pipeline(pipeline_1, pipeline_2)
+ create_source_pipeline(pipeline_2, upstream_pipeline)
end
- it 'creates a new pipeline' do
- expect { service.execute(bridge) }
- .to change { Ci::Pipeline.count }
+ it_behaves_like 'passes cyclical pipeline precondition'
+ end
+
+ context 'when ref in the ancestry differ' do
+ before do
+ # A(push on master) -> B(pipeline on master) -> A(push on feature-1)
+ pipeline_1 = create(:ci_pipeline, ref: 'master', project: upstream_project, source: :push)
+ pipeline_2 = create(:ci_pipeline, ref: 'master', project: downstream_project, source: :pipeline)
+ upstream_pipeline.update!(ref: 'feature-1')
+
+ create_source_pipeline(pipeline_1, pipeline_2)
+ create_source_pipeline(pipeline_2, upstream_pipeline)
end
- it 'expect bridge build not to be failed' do
- service.execute(bridge)
+ it_behaves_like 'passes cyclical pipeline precondition'
+ end
- expect(bridge.reload).not_to be_failed
+ context 'when only 1 cycle is detected' do
+ before do
+ # A(push on master) -> B(pipeline on master) -> A(push on master)
+ pipeline_1 = create(:ci_pipeline, ref: 'master', project: upstream_project, source: :push)
+ pipeline_2 = create(:ci_pipeline, ref: 'master', project: downstream_project, source: :pipeline)
+
+ create_source_pipeline(pipeline_1, pipeline_2)
+ create_source_pipeline(pipeline_2, upstream_pipeline)
end
+
+ it_behaves_like 'passes cyclical pipeline precondition'
end
end
diff --git a/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb b/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb
index 26bc6f747e1..7365ad162d2 100644
--- a/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb
+++ b/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb
@@ -1043,22 +1043,6 @@ RSpec.describe Ci::PipelineProcessing::AtomicProcessingService do
expect(all_builds_names).to eq(%w[A1 A2 B])
expect(all_builds_statuses).to eq(%w[pending created created])
end
-
- context 'when the FF ci_order_subsequent_jobs_by_stage is disabled' do
- before do
- stub_feature_flags(ci_order_subsequent_jobs_by_stage: false)
- end
-
- it 'processes subsequent jobs in an incorrect order when playing first job' do
- expect(all_builds_names).to eq(%w[A1 A2 B])
- expect(all_builds_statuses).to eq(%w[manual skipped skipped])
-
- play_manual_action('A1')
-
- expect(all_builds_names).to eq(%w[A1 A2 B])
- expect(all_builds_statuses).to eq(%w[pending created skipped])
- end
- end
end
private
diff --git a/spec/services/ci/pipeline_schedule_service_spec.rb b/spec/services/ci/pipeline_schedule_service_spec.rb
index 65bbd13c5e7..b8e4fb19f5d 100644
--- a/spec/services/ci/pipeline_schedule_service_spec.rb
+++ b/spec/services/ci/pipeline_schedule_service_spec.rb
@@ -32,5 +32,22 @@ RSpec.describe Ci::PipelineScheduleService do
expect { subject }.not_to raise_error
end
end
+
+ context 'when the project is missing' do
+ before do
+ project.delete
+ end
+
+ it 'does not raise an exception' do
+ expect { subject }.not_to raise_error
+ end
+
+ it 'does not run RunPipelineScheduleWorker' do
+ expect(RunPipelineScheduleWorker)
+ .not_to receive(:perform_async).with(schedule.id, schedule.owner.id)
+
+ subject
+ end
+ end
end
end
diff --git a/spec/services/ci/process_sync_events_service_spec.rb b/spec/services/ci/process_sync_events_service_spec.rb
index 8b7717fe4bf..6b9717fe57d 100644
--- a/spec/services/ci/process_sync_events_service_spec.rb
+++ b/spec/services/ci/process_sync_events_service_spec.rb
@@ -25,6 +25,8 @@ RSpec.describe Ci::ProcessSyncEventsService do
project2.update!(group: parent_group_2)
end
+ it { is_expected.to eq(service_results(2, 2, 2)) }
+
it 'consumes events' do
expect { execute }.to change(Projects::SyncEvent, :count).from(2).to(0)
@@ -36,20 +38,32 @@ RSpec.describe Ci::ProcessSyncEventsService do
)
end
- it 'enqueues Projects::ProcessSyncEventsWorker if any left' do
- stub_const("#{described_class}::BATCH_SIZE", 1)
+ context 'when any event left after processing' do
+ before do
+ stub_const("#{described_class}::BATCH_SIZE", 1)
+ end
- expect(Projects::ProcessSyncEventsWorker).to receive(:perform_async)
+ it { is_expected.to eq(service_results(2, 1, 1)) }
- execute
+ it 'enqueues Projects::ProcessSyncEventsWorker' do
+ expect(Projects::ProcessSyncEventsWorker).to receive(:perform_async)
+
+ execute
+ end
end
- it 'does not enqueue Projects::ProcessSyncEventsWorker if no left' do
- stub_const("#{described_class}::BATCH_SIZE", 2)
+ context 'when no event left after processing' do
+ before do
+ stub_const("#{described_class}::BATCH_SIZE", 2)
+ end
- expect(Projects::ProcessSyncEventsWorker).not_to receive(:perform_async)
+ it { is_expected.to eq(service_results(2, 2, 2)) }
- execute
+ it 'does not enqueue Projects::ProcessSyncEventsWorker' do
+ expect(Projects::ProcessSyncEventsWorker).not_to receive(:perform_async)
+
+ execute
+ end
end
context 'when there is no event' do
@@ -57,37 +71,45 @@ RSpec.describe Ci::ProcessSyncEventsService do
Projects::SyncEvent.delete_all
end
+ it { is_expected.to eq(service_results(0, 0, nil)) }
+
it 'does nothing' do
expect { execute }.not_to change(Projects::SyncEvent, :count)
end
end
- context 'when the FF ci_namespace_project_mirrors is disabled' do
+ context 'when there is non-executed events' do
before do
- stub_feature_flags(ci_namespace_project_mirrors: false)
- end
+ new_project = create(:project)
+ sync_event_class.delete_all
- it 'does nothing' do
- expect { execute }.not_to change(Projects::SyncEvent, :count)
- end
- end
+ project1.update!(group: parent_group_2)
+ new_project.update!(group: parent_group_1)
+ project2.update!(group: parent_group_1)
- it 'does not delete non-executed events' do
- new_project = create(:project)
- sync_event_class.delete_all
+ @new_project_sync_event = new_project.sync_events.last
- project1.update!(group: parent_group_2)
- new_project.update!(group: parent_group_1)
- project2.update!(group: parent_group_1)
+ allow(sync_event_class).to receive(:preload_synced_relation).and_return(
+ sync_event_class.where.not(id: @new_project_sync_event)
+ )
+ end
- new_project_sync_event = new_project.sync_events.last
+ it { is_expected.to eq(service_results(3, 2, 2)) }
- allow(sync_event_class).to receive(:preload_synced_relation).and_return(
- sync_event_class.where.not(id: new_project_sync_event)
- )
+ it 'does not delete non-executed events' do
+ expect { execute }.to change(Projects::SyncEvent, :count).from(3).to(1)
+ expect(@new_project_sync_event.reload).to be_persisted
+ end
+ end
+
+ private
- expect { execute }.to change(Projects::SyncEvent, :count).from(3).to(1)
- expect(new_project_sync_event.reload).to be_persisted
+ def service_results(total, consumable, processed)
+ {
+ estimated_total_events: total,
+ consumable_events: consumable,
+ processed_events: processed
+ }.compact
end
end
diff --git a/spec/services/ci/register_job_service_spec.rb b/spec/services/ci/register_job_service_spec.rb
index 251159864f5..2127a4fa0fc 100644
--- a/spec/services/ci/register_job_service_spec.rb
+++ b/spec/services/ci/register_job_service_spec.rb
@@ -750,6 +750,8 @@ module Ci
context 'with ci_queuing_use_denormalized_data_strategy disabled' do
before do
+ skip_if_multiple_databases_are_setup
+
stub_feature_flags(ci_queuing_use_denormalized_data_strategy: false)
end
@@ -773,6 +775,8 @@ module Ci
context 'when not using pending builds table' do
before do
+ skip_if_multiple_databases_are_setup
+
stub_feature_flags(ci_pending_builds_queue_source: false)
end
diff --git a/spec/services/ci/register_runner_service_spec.rb b/spec/services/ci/register_runner_service_spec.rb
index e813a1d8b31..491582bbd13 100644
--- a/spec/services/ci/register_runner_service_spec.rb
+++ b/spec/services/ci/register_runner_service_spec.rb
@@ -2,8 +2,10 @@
require 'spec_helper'
-RSpec.describe ::Ci::RegisterRunnerService do
+RSpec.describe ::Ci::RegisterRunnerService, '#execute' do
let(:registration_token) { 'abcdefg123456' }
+ let(:token) { }
+ let(:args) { {} }
before do
stub_feature_flags(runner_registration_control: false)
@@ -11,213 +13,219 @@ RSpec.describe ::Ci::RegisterRunnerService do
stub_application_setting(valid_runner_registrars: ApplicationSetting::VALID_RUNNER_REGISTRAR_TYPES)
end
- describe '#execute' do
- let(:token) { }
- let(:args) { {} }
+ subject { described_class.new.execute(token, args) }
- subject { described_class.new.execute(token, args) }
+ context 'when no token is provided' do
+ let(:token) { '' }
- context 'when no token is provided' do
- let(:token) { '' }
-
- it 'returns nil' do
- is_expected.to be_nil
- end
+ it 'returns nil' do
+ is_expected.to be_nil
end
+ end
- context 'when invalid token is provided' do
- let(:token) { 'invalid' }
+ context 'when invalid token is provided' do
+ let(:token) { 'invalid' }
- it 'returns nil' do
- is_expected.to be_nil
- end
+ it 'returns nil' do
+ is_expected.to be_nil
end
+ end
- context 'when valid token is provided' do
- context 'with a registration token' do
- let(:token) { registration_token }
+ context 'when valid token is provided' do
+ context 'with a registration token' do
+ let(:token) { registration_token }
+
+ it 'creates runner with default values' do
+ is_expected.to be_an_instance_of(::Ci::Runner)
+ expect(subject.persisted?).to be_truthy
+ expect(subject.run_untagged).to be true
+ expect(subject.active).to be true
+ expect(subject.token).not_to eq(registration_token)
+ expect(subject).to be_instance_type
+ end
- it 'creates runner with default values' do
- is_expected.to be_an_instance_of(::Ci::Runner)
- expect(subject.persisted?).to be_truthy
- expect(subject.run_untagged).to be true
- expect(subject.active).to be true
- expect(subject.token).not_to eq(registration_token)
- expect(subject).to be_instance_type
- end
-
- context 'with non-default arguments' do
- let(:args) do
- {
- description: 'some description',
- active: false,
- locked: true,
- run_untagged: false,
- tag_list: %w(tag1 tag2),
- access_level: 'ref_protected',
- maximum_timeout: 600,
- name: 'some name',
- version: 'some version',
- revision: 'some revision',
- platform: 'some platform',
- architecture: 'some architecture',
- ip_address: '10.0.0.1',
- config: {
- gpus: 'some gpu config'
- }
+ context 'with non-default arguments' do
+ let(:args) do
+ {
+ description: 'some description',
+ active: false,
+ locked: true,
+ run_untagged: false,
+ tag_list: %w(tag1 tag2),
+ access_level: 'ref_protected',
+ maximum_timeout: 600,
+ name: 'some name',
+ version: 'some version',
+ revision: 'some revision',
+ platform: 'some platform',
+ architecture: 'some architecture',
+ ip_address: '10.0.0.1',
+ config: {
+ gpus: 'some gpu config'
}
- end
+ }
+ end
- it 'creates runner with specified values', :aggregate_failures do
- is_expected.to be_an_instance_of(::Ci::Runner)
- expect(subject.active).to eq args[:active]
- expect(subject.locked).to eq args[:locked]
- expect(subject.run_untagged).to eq args[:run_untagged]
- expect(subject.tags).to contain_exactly(
- an_object_having_attributes(name: 'tag1'),
- an_object_having_attributes(name: 'tag2')
- )
- expect(subject.access_level).to eq args[:access_level]
- expect(subject.maximum_timeout).to eq args[:maximum_timeout]
- expect(subject.name).to eq args[:name]
- expect(subject.version).to eq args[:version]
- expect(subject.revision).to eq args[:revision]
- expect(subject.platform).to eq args[:platform]
- expect(subject.architecture).to eq args[:architecture]
- expect(subject.ip_address).to eq args[:ip_address]
- end
+ it 'creates runner with specified values', :aggregate_failures do
+ is_expected.to be_an_instance_of(::Ci::Runner)
+ expect(subject.active).to eq args[:active]
+ expect(subject.locked).to eq args[:locked]
+ expect(subject.run_untagged).to eq args[:run_untagged]
+ expect(subject.tags).to contain_exactly(
+ an_object_having_attributes(name: 'tag1'),
+ an_object_having_attributes(name: 'tag2')
+ )
+ expect(subject.access_level).to eq args[:access_level]
+ expect(subject.maximum_timeout).to eq args[:maximum_timeout]
+ expect(subject.name).to eq args[:name]
+ expect(subject.version).to eq args[:version]
+ expect(subject.revision).to eq args[:revision]
+ expect(subject.platform).to eq args[:platform]
+ expect(subject.architecture).to eq args[:architecture]
+ expect(subject.ip_address).to eq args[:ip_address]
end
end
- context 'when project token is used' do
- let(:project) { create(:project) }
- let(:token) { project.runners_token }
+ context 'with runner token expiration interval', :freeze_time do
+ before do
+ stub_application_setting(runner_token_expiration_interval: 5.days)
+ end
- it 'creates project runner' do
+ it 'creates runner with token expiration' do
is_expected.to be_an_instance_of(::Ci::Runner)
- expect(project.runners.size).to eq(1)
- is_expected.to eq(project.runners.first)
- expect(subject.token).not_to eq(registration_token)
- expect(subject.token).not_to eq(project.runners_token)
- expect(subject).to be_project_type
+ expect(subject.token_expires_at).to eq(5.days.from_now)
end
+ end
+ end
- context 'when it exceeds the application limits' do
- before do
- create(:ci_runner, runner_type: :project_type, projects: [project], contacted_at: 1.second.ago)
- create(:plan_limits, :default_plan, ci_registered_project_runners: 1)
- end
+ context 'when project token is used' do
+ let(:project) { create(:project) }
+ let(:token) { project.runners_token }
+
+ it 'creates project runner' do
+ is_expected.to be_an_instance_of(::Ci::Runner)
+ expect(project.runners.size).to eq(1)
+ is_expected.to eq(project.runners.first)
+ expect(subject.token).not_to eq(registration_token)
+ expect(subject.token).not_to eq(project.runners_token)
+ expect(subject).to be_project_type
+ end
- it 'does not create runner' do
- is_expected.to be_an_instance_of(::Ci::Runner)
- expect(subject.persisted?).to be_falsey
- expect(subject.errors.messages).to eq('runner_projects.base': ['Maximum number of ci registered project runners (1) exceeded'])
- expect(project.runners.reload.size).to eq(1)
- end
+ context 'when it exceeds the application limits' do
+ before do
+ create(:ci_runner, runner_type: :project_type, projects: [project], contacted_at: 1.second.ago)
+ create(:plan_limits, :default_plan, ci_registered_project_runners: 1)
end
- context 'when abandoned runners cause application limits to not be exceeded' do
- before do
- create(:ci_runner, runner_type: :project_type, projects: [project], created_at: 14.months.ago, contacted_at: 13.months.ago)
- create(:plan_limits, :default_plan, ci_registered_project_runners: 1)
- end
+ it 'does not create runner' do
+ is_expected.to be_an_instance_of(::Ci::Runner)
+ expect(subject.persisted?).to be_falsey
+ expect(subject.errors.messages).to eq('runner_projects.base': ['Maximum number of ci registered project runners (1) exceeded'])
+ expect(project.runners.reload.size).to eq(1)
+ end
+ end
- it 'creates runner' do
- is_expected.to be_an_instance_of(::Ci::Runner)
- expect(subject.errors).to be_empty
- expect(project.runners.reload.size).to eq(2)
- expect(project.runners.recent.size).to eq(1)
- end
+ context 'when abandoned runners cause application limits to not be exceeded' do
+ before do
+ create(:ci_runner, runner_type: :project_type, projects: [project], created_at: 14.months.ago, contacted_at: 13.months.ago)
+ create(:plan_limits, :default_plan, ci_registered_project_runners: 1)
end
- context 'when valid runner registrars do not include project' do
+ it 'creates runner' do
+ is_expected.to be_an_instance_of(::Ci::Runner)
+ expect(subject.errors).to be_empty
+ expect(project.runners.reload.size).to eq(2)
+ expect(project.runners.recent.size).to eq(1)
+ end
+ end
+
+ context 'when valid runner registrars do not include project' do
+ before do
+ stub_application_setting(valid_runner_registrars: ['group'])
+ end
+
+ context 'when feature flag is enabled' do
before do
- stub_application_setting(valid_runner_registrars: ['group'])
+ stub_feature_flags(runner_registration_control: true)
end
- context 'when feature flag is enabled' do
- before do
- stub_feature_flags(runner_registration_control: true)
- end
-
- it 'returns 403 error' do
- is_expected.to be_nil
- end
+ it 'returns 403 error' do
+ is_expected.to be_nil
end
+ end
- context 'when feature flag is disabled' do
- it 'registers the runner' do
- is_expected.to be_an_instance_of(::Ci::Runner)
- expect(subject.errors).to be_empty
- expect(subject.active).to be true
- end
+ context 'when feature flag is disabled' do
+ it 'registers the runner' do
+ is_expected.to be_an_instance_of(::Ci::Runner)
+ expect(subject.errors).to be_empty
+ expect(subject.active).to be true
end
end
end
+ end
+
+ context 'when group token is used' do
+ let(:group) { create(:group) }
+ let(:token) { group.runners_token }
+
+ it 'creates a group runner' do
+ is_expected.to be_an_instance_of(::Ci::Runner)
+ expect(subject.errors).to be_empty
+ expect(group.runners.reload.size).to eq(1)
+ expect(subject.token).not_to eq(registration_token)
+ expect(subject.token).not_to eq(group.runners_token)
+ expect(subject).to be_group_type
+ end
- context 'when group token is used' do
- let(:group) { create(:group) }
- let(:token) { group.runners_token }
+ context 'when it exceeds the application limits' do
+ before do
+ create(:ci_runner, runner_type: :group_type, groups: [group], contacted_at: nil, created_at: 1.month.ago)
+ create(:plan_limits, :default_plan, ci_registered_group_runners: 1)
+ end
- it 'creates a group runner' do
+ it 'does not create runner' do
is_expected.to be_an_instance_of(::Ci::Runner)
- expect(subject.errors).to be_empty
+ expect(subject.persisted?).to be_falsey
+ expect(subject.errors.messages).to eq('runner_namespaces.base': ['Maximum number of ci registered group runners (1) exceeded'])
expect(group.runners.reload.size).to eq(1)
- expect(subject.token).not_to eq(registration_token)
- expect(subject.token).not_to eq(group.runners_token)
- expect(subject).to be_group_type
end
+ end
- context 'when it exceeds the application limits' do
- before do
- create(:ci_runner, runner_type: :group_type, groups: [group], contacted_at: nil, created_at: 1.month.ago)
- create(:plan_limits, :default_plan, ci_registered_group_runners: 1)
- end
-
- it 'does not create runner' do
- is_expected.to be_an_instance_of(::Ci::Runner)
- expect(subject.persisted?).to be_falsey
- expect(subject.errors.messages).to eq('runner_namespaces.base': ['Maximum number of ci registered group runners (1) exceeded'])
- expect(group.runners.reload.size).to eq(1)
- end
+ context 'when abandoned runners cause application limits to not be exceeded' do
+ before do
+ create(:ci_runner, runner_type: :group_type, groups: [group], created_at: 4.months.ago, contacted_at: 3.months.ago)
+ create(:ci_runner, runner_type: :group_type, groups: [group], contacted_at: nil, created_at: 4.months.ago)
+ create(:plan_limits, :default_plan, ci_registered_group_runners: 1)
end
- context 'when abandoned runners cause application limits to not be exceeded' do
- before do
- create(:ci_runner, runner_type: :group_type, groups: [group], created_at: 4.months.ago, contacted_at: 3.months.ago)
- create(:ci_runner, runner_type: :group_type, groups: [group], contacted_at: nil, created_at: 4.months.ago)
- create(:plan_limits, :default_plan, ci_registered_group_runners: 1)
- end
+ it 'creates runner' do
+ is_expected.to be_an_instance_of(::Ci::Runner)
+ expect(subject.errors).to be_empty
+ expect(group.runners.reload.size).to eq(3)
+ expect(group.runners.recent.size).to eq(1)
+ end
+ end
- it 'creates runner' do
- is_expected.to be_an_instance_of(::Ci::Runner)
- expect(subject.errors).to be_empty
- expect(group.runners.reload.size).to eq(3)
- expect(group.runners.recent.size).to eq(1)
- end
+ context 'when valid runner registrars do not include group' do
+ before do
+ stub_application_setting(valid_runner_registrars: ['project'])
end
- context 'when valid runner registrars do not include group' do
+ context 'when feature flag is enabled' do
before do
- stub_application_setting(valid_runner_registrars: ['project'])
+ stub_feature_flags(runner_registration_control: true)
end
- context 'when feature flag is enabled' do
- before do
- stub_feature_flags(runner_registration_control: true)
- end
-
- it 'returns nil' do
- is_expected.to be_nil
- end
+ it 'returns nil' do
+ is_expected.to be_nil
end
+ end
- context 'when feature flag is disabled' do
- it 'registers the runner' do
- is_expected.to be_an_instance_of(::Ci::Runner)
- expect(subject.errors).to be_empty
- expect(subject.active).to be true
- end
+ context 'when feature flag is disabled' do
+ it 'registers the runner' do
+ is_expected.to be_an_instance_of(::Ci::Runner)
+ expect(subject.errors).to be_empty
+ expect(subject.active).to be true
end
end
end
diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb
index 4e8e41ca6e6..2421fd56c47 100644
--- a/spec/services/ci/retry_build_service_spec.rb
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -60,7 +60,8 @@ RSpec.describe Ci::RetryBuildService do
artifacts_file artifacts_metadata artifacts_size commands
resource resource_group_id processed security_scans author
pipeline_id report_results pending_state pages_deployments
- queuing_entry runtime_metadata trace_metadata].freeze
+ queuing_entry runtime_metadata trace_metadata
+ dast_site_profile dast_scanner_profile].freeze
shared_examples 'build duplication' do
let_it_be(:another_pipeline) { create(:ci_empty_pipeline, project: project) }
@@ -370,23 +371,6 @@ RSpec.describe Ci::RetryBuildService do
it_behaves_like 'when build with deployment is retried'
it_behaves_like 'when build with dynamic environment is retried'
- context 'when create_deployment_in_separate_transaction feature flag is disabled' do
- let(:new_build) do
- travel_to(1.second.from_now) do
- ::Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification.allow_cross_database_modification_within_transaction(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/345668') do
- service.clone!(build)
- end
- end
- end
-
- before do
- stub_feature_flags(create_deployment_in_separate_transaction: false)
- end
-
- it_behaves_like 'when build with deployment is retried'
- it_behaves_like 'when build with dynamic environment is retried'
- end
-
context 'when build has needs' do
before do
create(:ci_build_need, build: build, name: 'build1')
diff --git a/spec/services/ci/unregister_runner_service_spec.rb b/spec/services/ci/unregister_runner_service_spec.rb
new file mode 100644
index 00000000000..f427e04f228
--- /dev/null
+++ b/spec/services/ci/unregister_runner_service_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Ci::UnregisterRunnerService, '#execute' do
+ subject { described_class.new(runner).execute }
+
+ let(:runner) { create(:ci_runner) }
+
+ it 'destroys runner' do
+ expect(runner).to receive(:destroy).once.and_call_original
+ expect { subject }.to change { Ci::Runner.count }.by(-1)
+ expect(runner[:errors]).to be_nil
+ end
+end
diff --git a/spec/services/ci/update_build_queue_service_spec.rb b/spec/services/ci/update_build_queue_service_spec.rb
index 2e2ef120f1b..ef43866d8d4 100644
--- a/spec/services/ci/update_build_queue_service_spec.rb
+++ b/spec/services/ci/update_build_queue_service_spec.rb
@@ -308,36 +308,12 @@ RSpec.describe Ci::UpdateBuildQueueService do
let!(:build) { create(:ci_build, pipeline: pipeline, tag_list: %w[a b]) }
let!(:project_runner) { create(:ci_runner, :project, :online, projects: [project], tag_list: %w[a b c]) }
- context 'when ci_preload_runner_tags is enabled' do
- before do
- stub_feature_flags(
- ci_preload_runner_tags: true
- )
- end
-
- it 'does execute the same amount of queries regardless of number of runners' do
- control_count = ActiveRecord::QueryRecorder.new { subject.tick(build) }.count
-
- create_list(:ci_runner, 10, :project, :online, projects: [project], tag_list: %w[b c d])
-
- expect { subject.tick(build) }.not_to exceed_all_query_limit(control_count)
- end
- end
-
- context 'when ci_preload_runner_tags are disabled' do
- before do
- stub_feature_flags(
- ci_preload_runner_tags: false
- )
- end
-
- it 'does execute more queries for more runners' do
- control_count = ActiveRecord::QueryRecorder.new { subject.tick(build) }.count
+ it 'does execute the same amount of queries regardless of number of runners' do
+ control_count = ActiveRecord::QueryRecorder.new { subject.tick(build) }.count
- create_list(:ci_runner, 10, :project, :online, projects: [project], tag_list: %w[b c d])
+ create_list(:ci_runner, 10, :project, :online, projects: [project], tag_list: %w[b c d])
- expect { subject.tick(build) }.to exceed_all_query_limit(control_count)
- end
+ expect { subject.tick(build) }.not_to exceed_all_query_limit(control_count)
end
end
end
diff --git a/spec/services/ci/update_runner_service_spec.rb b/spec/services/ci/update_runner_service_spec.rb
index 1c875b2f54a..eee80bfef47 100644
--- a/spec/services/ci/update_runner_service_spec.rb
+++ b/spec/services/ci/update_runner_service_spec.rb
@@ -23,6 +23,20 @@ RSpec.describe Ci::UpdateRunnerService do
end
end
+ context 'with paused param' do
+ let(:params) { { paused: true } }
+
+ it 'updates the runner and ticking the queue' do
+ expect(runner.active).to be_truthy
+ expect(update).to be_truthy
+
+ runner.reload
+
+ expect(runner).to have_received(:tick_runner_queue)
+ expect(runner.active).to be_falsey
+ end
+ end
+
context 'with cost factor params' do
let(:params) { { public_projects_minutes_cost_factor: 1.1, private_projects_minutes_cost_factor: 2.2 }}
diff --git a/spec/services/concerns/rate_limited_service_spec.rb b/spec/services/concerns/rate_limited_service_spec.rb
index f73871b7e44..97f5ca53c0d 100644
--- a/spec/services/concerns/rate_limited_service_spec.rb
+++ b/spec/services/concerns/rate_limited_service_spec.rb
@@ -6,11 +6,10 @@ RSpec.describe RateLimitedService do
let(:key) { :issues_create }
let(:scope) { [:project, :current_user] }
let(:opts) { { scope: scope, users_allowlist: -> { [User.support_bot.username] } } }
- let(:rate_limiter_klass) { ::Gitlab::ApplicationRateLimiter }
- let(:rate_limiter_instance) { rate_limiter_klass.new(key, **opts) }
+ let(:rate_limiter) { ::Gitlab::ApplicationRateLimiter }
describe 'RateLimitedError' do
- subject { described_class::RateLimitedError.new(key: key, rate_limiter: rate_limiter_instance) }
+ subject { described_class::RateLimitedError.new(key: key, rate_limiter: rate_limiter) }
describe '#headers' do
it 'returns a Hash of HTTP headers' do
@@ -26,7 +25,7 @@ RSpec.describe RateLimitedService do
request = instance_double(Grape::Request)
user = instance_double(User)
- expect(rate_limiter_klass).to receive(:log_request).with(request, "#{key}_request_limit".to_sym, user)
+ expect(rate_limiter).to receive(:log_request).with(request, "#{key}_request_limit".to_sym, user)
subject.log_request(request, user)
end
@@ -34,7 +33,7 @@ RSpec.describe RateLimitedService do
end
describe 'RateLimiterScopedAndKeyed' do
- subject { described_class::RateLimiterScopedAndKeyed.new(key: key, opts: opts, rate_limiter_klass: rate_limiter_klass) }
+ subject { described_class::RateLimiterScopedAndKeyed.new(key: key, opts: opts, rate_limiter: rate_limiter) }
describe '#rate_limit!' do
let(:project_with_feature_enabled) { create(:project) }
@@ -49,13 +48,12 @@ RSpec.describe RateLimitedService do
let(:rate_limited_service_issues_create_feature_enabled) { nil }
before do
- allow(rate_limiter_klass).to receive(:new).with(key, **evaluated_opts).and_return(rate_limiter_instance)
stub_feature_flags(rate_limited_service_issues_create: rate_limited_service_issues_create_feature_enabled)
end
shared_examples 'a service that does not attempt to throttle' do
it 'does not attempt to throttle' do
- expect(rate_limiter_instance).not_to receive(:throttled?)
+ expect(rate_limiter).not_to receive(:throttled?)
expect(subject.rate_limit!(service)).to be_nil
end
@@ -63,7 +61,7 @@ RSpec.describe RateLimitedService do
shared_examples 'a service that does attempt to throttle' do
before do
- allow(rate_limiter_instance).to receive(:throttled?).and_return(throttled)
+ allow(rate_limiter).to receive(:throttled?).and_return(throttled)
end
context 'when rate limiting is not in effect' do
@@ -134,7 +132,7 @@ RSpec.describe RateLimitedService do
end
before do
- allow(RateLimitedService::RateLimiterScopedAndKeyed).to receive(:new).with(key: key, opts: opts, rate_limiter_klass: rate_limiter_klass).and_return(rate_limiter_scoped_and_keyed)
+ allow(RateLimitedService::RateLimiterScopedAndKeyed).to receive(:new).with(key: key, opts: opts, rate_limiter: rate_limiter).and_return(rate_limiter_scoped_and_keyed)
end
context 'bypasses rate limiting' do
@@ -173,12 +171,12 @@ RSpec.describe RateLimitedService do
end
before do
- allow(RateLimitedService::RateLimiterScopedAndKeyed).to receive(:new).with(key: key, opts: opts, rate_limiter_klass: rate_limiter_klass).and_return(rate_limiter_scoped_and_keyed)
+ allow(RateLimitedService::RateLimiterScopedAndKeyed).to receive(:new).with(key: key, opts: opts, rate_limiter: rate_limiter).and_return(rate_limiter_scoped_and_keyed)
end
context 'and applies rate limiting' do
it 'raises an RateLimitedService::RateLimitedError exception' do
- expect(rate_limiter_scoped_and_keyed).to receive(:rate_limit!).with(subject).and_raise(RateLimitedService::RateLimitedError.new(key: key, rate_limiter: rate_limiter_instance))
+ expect(rate_limiter_scoped_and_keyed).to receive(:rate_limit!).with(subject).and_raise(RateLimitedService::RateLimitedError.new(key: key, rate_limiter: rate_limiter))
expect { subject.execute }.to raise_error(RateLimitedService::RateLimitedError)
end
diff --git a/spec/services/draft_notes/create_service_spec.rb b/spec/services/draft_notes/create_service_spec.rb
index 9e084dbed1c..528c8717525 100644
--- a/spec/services/draft_notes/create_service_spec.rb
+++ b/spec/services/draft_notes/create_service_spec.rb
@@ -92,7 +92,7 @@ RSpec.describe DraftNotes::CreateService do
expect(merge_request).to receive_message_chain(:diffs, :clear_cache)
- create_draft(note: 'This is a test')
+ create_draft(note: 'This is a test', line_code: '123')
end
end
@@ -104,7 +104,7 @@ RSpec.describe DraftNotes::CreateService do
expect(merge_request).not_to receive(:diffs)
- create_draft(note: 'This is a test')
+ create_draft(note: 'This is a test', line_code: '123')
end
end
end
diff --git a/spec/services/environments/stop_service_spec.rb b/spec/services/environments/stop_service_spec.rb
index acc9869002f..362071c1c26 100644
--- a/spec/services/environments/stop_service_spec.rb
+++ b/spec/services/environments/stop_service_spec.rb
@@ -185,7 +185,7 @@ RSpec.describe Environments::StopService do
end
it 'has active environment at first' do
- expect(pipeline.environments.first).to be_available
+ expect(pipeline.environments_in_self_and_descendants.first).to be_available
end
context 'when user is a developer' do
@@ -196,7 +196,7 @@ RSpec.describe Environments::StopService do
it 'stops the active environment' do
subject
- expect(pipeline.environments.first).to be_stopped
+ expect(pipeline.environments_in_self_and_descendants.first).to be_stopped
end
end
@@ -208,7 +208,7 @@ RSpec.describe Environments::StopService do
it 'does not stop the active environment' do
subject
- expect(pipeline.environments.first).to be_available
+ expect(pipeline.environments_in_self_and_descendants.first).to be_available
end
end
@@ -232,7 +232,7 @@ RSpec.describe Environments::StopService do
it 'does not stop the active environment' do
subject
- expect(pipeline.environments.first).to be_available
+ expect(pipeline.environments_in_self_and_descendants.first).to be_available
end
end
end
diff --git a/spec/services/feature_flags/update_service_spec.rb b/spec/services/feature_flags/update_service_spec.rb
index abe0112b27e..f5e94c4af0f 100644
--- a/spec/services/feature_flags/update_service_spec.rb
+++ b/spec/services/feature_flags/update_service_spec.rb
@@ -130,7 +130,7 @@ RSpec.describe FeatureFlags::UpdateService do
it 'executes hooks' do
hook = create(:project_hook, :all_events_enabled, project: project)
- expect(WebHookWorker).to receive(:perform_async).with(hook.id, an_instance_of(Hash), 'feature_flag_hooks')
+ expect(WebHookWorker).to receive(:perform_async).with(hook.id, an_instance_of(Hash), 'feature_flag_hooks', an_instance_of(Hash))
subject
end
diff --git a/spec/services/google_cloud/create_service_accounts_service_spec.rb b/spec/services/google_cloud/create_service_accounts_service_spec.rb
index 190e1a8098c..53d21df713a 100644
--- a/spec/services/google_cloud/create_service_accounts_service_spec.rb
+++ b/spec/services/google_cloud/create_service_accounts_service_spec.rb
@@ -2,22 +2,26 @@
require 'spec_helper'
-# Mock Types
-MockGoogleOAuth2Credentials = Struct.new(:app_id, :app_secret)
-MockServiceAccount = Struct.new(:project_id, :unique_id)
-
RSpec.describe GoogleCloud::CreateServiceAccountsService do
describe '#execute' do
before do
+ mock_google_oauth2_creds = Struct.new(:app_id, :app_secret)
+ .new('mock-app-id', 'mock-app-secret')
allow(Gitlab::Auth::OAuth::Provider).to receive(:config_for)
.with('google_oauth2')
- .and_return(MockGoogleOAuth2Credentials.new('mock-app-id', 'mock-app-secret'))
+ .and_return(mock_google_oauth2_creds)
allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |client|
+ mock_service_account = Struct.new(:project_id, :unique_id, :email)
+ .new('mock-project-id', 'mock-unique-id', 'mock-email')
allow(client).to receive(:create_service_account)
- .and_return(MockServiceAccount.new('mock-project-id', 'mock-unique-id'))
+ .and_return(mock_service_account)
+
allow(client).to receive(:create_service_account_key)
.and_return('mock-key')
+
+ allow(client)
+ .to receive(:grant_service_account_roles)
end
end
diff --git a/spec/services/google_cloud/enable_cloud_run_service_spec.rb b/spec/services/google_cloud/enable_cloud_run_service_spec.rb
new file mode 100644
index 00000000000..6d2b1f5cfd5
--- /dev/null
+++ b/spec/services/google_cloud/enable_cloud_run_service_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GoogleCloud::EnableCloudRunService do
+ describe 'when a project does not have any gcp projects' do
+ let_it_be(:project) { create(:project) }
+
+ it 'returns error' do
+ result = described_class.new(project).execute
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq('No GCP projects found. Configure a service account or GCP_PROJECT_ID ci variable.')
+ end
+ end
+
+ describe 'when a project has 3 gcp projects' do
+ let_it_be(:project) { create(:project) }
+
+ before do
+ project.variables.build(environment_scope: 'production', key: 'GCP_PROJECT_ID', value: 'prj-prod')
+ project.variables.build(environment_scope: 'staging', key: 'GCP_PROJECT_ID', value: 'prj-staging')
+ project.save!
+ end
+
+ it 'enables cloud run, artifacts registry and cloud build', :aggregate_failures do
+ expect_next_instance_of(GoogleApi::CloudPlatform::Client) do |instance|
+ expect(instance).to receive(:enable_cloud_run).with('prj-prod')
+ expect(instance).to receive(:enable_artifacts_registry).with('prj-prod')
+ expect(instance).to receive(:enable_cloud_build).with('prj-prod')
+ expect(instance).to receive(:enable_cloud_run).with('prj-staging')
+ expect(instance).to receive(:enable_artifacts_registry).with('prj-staging')
+ expect(instance).to receive(:enable_cloud_build).with('prj-staging')
+ end
+
+ result = described_class.new(project).execute
+
+ expect(result[:status]).to eq(:success)
+ end
+ end
+end
diff --git a/spec/services/google_cloud/generate_pipeline_service_spec.rb b/spec/services/google_cloud/generate_pipeline_service_spec.rb
new file mode 100644
index 00000000000..75494f229b5
--- /dev/null
+++ b/spec/services/google_cloud/generate_pipeline_service_spec.rb
@@ -0,0 +1,230 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GoogleCloud::GeneratePipelineService do
+ describe 'for cloud-run' do
+ describe 'when there is no existing pipeline' do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:maintainer) { create(:user) }
+ let_it_be(:service_params) { { action: described_class::ACTION_DEPLOY_TO_CLOUD_RUN } }
+ let_it_be(:service) { described_class.new(project, maintainer, service_params) }
+
+ before do
+ project.add_maintainer(maintainer)
+ end
+
+ it 'creates a new branch with commit for cloud-run deployment' do
+ response = service.execute
+
+ branch_name = response[:branch_name]
+ commit = response[:commit]
+ local_branches = project.repository.local_branches
+ created_branch = local_branches.find { |branch| branch.name == branch_name }
+
+ expect(response[:status]).to eq(:success)
+ expect(branch_name).to start_with('deploy-to-cloud-run-')
+ expect(created_branch).to be_present
+ expect(created_branch.target).to eq(commit[:result])
+ end
+
+ it 'generated pipeline includes cloud-run deployment' do
+ response = service.execute
+
+ ref = response[:commit][:result]
+ gitlab_ci_yml = project.repository.gitlab_ci_yml_for(ref)
+
+ expect(response[:status]).to eq(:success)
+ expect(gitlab_ci_yml).to include('https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/library/-/raw/main/gcp/cloud-run.gitlab-ci.yml')
+ end
+
+ context 'simulate errors' do
+ it 'fails to create branch' do
+ allow_next_instance_of(Branches::CreateService) do |create_service|
+ allow(create_service).to receive(:execute)
+ .and_return({ status: :error })
+ end
+
+ response = service.execute
+ expect(response[:status]).to eq(:error)
+ end
+
+ it 'fails to commit changes' do
+ allow_next_instance_of(Files::CreateService) do |create_service|
+ allow(create_service).to receive(:execute)
+ .and_return({ status: :error })
+ end
+
+ response = service.execute
+ expect(response[:status]).to eq(:error)
+ end
+ end
+ end
+
+ describe 'when there is an existing pipeline without `deploy` stage' do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:maintainer) { create(:user) }
+ let_it_be(:service_params) { { action: GoogleCloud::GeneratePipelineService::ACTION_DEPLOY_TO_CLOUD_RUN } }
+ let_it_be(:service) { described_class.new(project, maintainer, service_params) }
+
+ before do
+ project.add_maintainer(maintainer)
+
+ file_name = '.gitlab-ci.yml'
+ file_content = <<EOF
+stages:
+ - build
+ - test
+
+build-java:
+ stage: build
+ script: mvn clean install
+
+test-java:
+ stage: test
+ script: mvn clean test
+EOF
+ project.repository.create_file(maintainer,
+ file_name,
+ file_content,
+ message: 'Pipeline with three stages and two jobs',
+ branch_name: project.default_branch)
+ end
+
+ it 'introduces a `deploy` stage and includes the deploy-to-cloud-run job' do
+ response = service.execute
+
+ branch_name = response[:branch_name]
+ gitlab_ci_yml = project.repository.gitlab_ci_yml_for(branch_name)
+ pipeline = Gitlab::Config::Loader::Yaml.new(gitlab_ci_yml).load!
+
+ expect(response[:status]).to eq(:success)
+ expect(pipeline[:stages]).to eq(%w[build test deploy])
+ expect(pipeline[:include]).to be_present
+ expect(gitlab_ci_yml).to include('https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/library/-/raw/main/gcp/cloud-run.gitlab-ci.yml')
+ end
+ end
+
+ describe 'when there is an existing pipeline with `deploy` stage' do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:maintainer) { create(:user) }
+ let_it_be(:service_params) { { action: GoogleCloud::GeneratePipelineService::ACTION_DEPLOY_TO_CLOUD_RUN } }
+ let_it_be(:service) { described_class.new(project, maintainer, service_params) }
+
+ before do
+ project.add_maintainer(maintainer)
+
+ file_name = '.gitlab-ci.yml'
+ file_content = <<EOF
+stages:
+ - build
+ - test
+ - deploy
+
+build-java:
+ stage: build
+ script: mvn clean install
+
+test-java:
+ stage: test
+ script: mvn clean test
+EOF
+ project.repository.create_file(maintainer,
+ file_name,
+ file_content,
+ message: 'Pipeline with three stages and two jobs',
+ branch_name: project.default_branch)
+ end
+
+ it 'includes the deploy-to-cloud-run job' do
+ response = service.execute
+
+ branch_name = response[:branch_name]
+ gitlab_ci_yml = project.repository.gitlab_ci_yml_for(branch_name)
+ pipeline = Gitlab::Config::Loader::Yaml.new(gitlab_ci_yml).load!
+
+ expect(response[:status]).to eq(:success)
+ expect(pipeline[:stages]).to eq(%w[build test deploy])
+ expect(pipeline[:include]).to be_present
+ expect(gitlab_ci_yml).to include('https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/library/-/raw/main/gcp/cloud-run.gitlab-ci.yml')
+ end
+ end
+
+ describe 'when there is an existing pipeline with `includes`' do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:maintainer) { create(:user) }
+ let_it_be(:service_params) { { action: GoogleCloud::GeneratePipelineService::ACTION_DEPLOY_TO_CLOUD_RUN } }
+ let_it_be(:service) { described_class.new(project, maintainer, service_params) }
+
+ before do
+ project.add_maintainer(maintainer)
+
+ file_name = '.gitlab-ci.yml'
+ file_content = <<EOF
+stages:
+ - build
+ - test
+ - deploy
+
+include:
+ local: 'some-pipeline.yml'
+EOF
+ project.repository.create_file(maintainer,
+ file_name,
+ file_content,
+ message: 'Pipeline with three stages and two jobs',
+ branch_name: project.default_branch)
+ end
+
+ it 'includes the deploy-to-cloud-run job' do
+ response = service.execute
+
+ branch_name = response[:branch_name]
+ gitlab_ci_yml = project.repository.gitlab_ci_yml_for(branch_name)
+ pipeline = Gitlab::Config::Loader::Yaml.new(gitlab_ci_yml).load!
+
+ expect(response[:status]).to eq(:success)
+ expect(pipeline[:stages]).to eq(%w[build test deploy])
+ expect(pipeline[:include]).to be_present
+ expect(gitlab_ci_yml).to include('https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/library/-/raw/main/gcp/cloud-run.gitlab-ci.yml')
+ end
+ end
+ end
+
+ describe 'for cloud-storage' do
+ describe 'when there is no existing pipeline' do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:maintainer) { create(:user) }
+ let_it_be(:service_params) { { action: GoogleCloud::GeneratePipelineService::ACTION_DEPLOY_TO_CLOUD_STORAGE } }
+ let_it_be(:service) { described_class.new(project, maintainer, service_params) }
+
+ before do
+ project.add_maintainer(maintainer)
+ end
+
+ it 'creates a new branch with commit for cloud-storage deployment' do
+ response = service.execute
+
+ branch_name = response[:branch_name]
+ commit = response[:commit]
+ local_branches = project.repository.local_branches
+ search_for_created_branch = local_branches.find { |branch| branch.name == branch_name }
+
+ expect(response[:status]).to eq(:success)
+ expect(branch_name).to start_with('deploy-to-cloud-storage-')
+ expect(search_for_created_branch).to be_present
+ expect(search_for_created_branch.target).to eq(commit[:result])
+ end
+
+ it 'generated pipeline includes cloud-storage deployment' do
+ response = service.execute
+
+ ref = response[:commit][:result]
+ gitlab_ci_yml = project.repository.gitlab_ci_yml_for(ref)
+
+ expect(response[:status]).to eq(:success)
+ expect(gitlab_ci_yml).to include('https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/library/-/raw/main/gcp/cloud-storage.gitlab-ci.yml')
+ end
+ end
+ end
+end
diff --git a/spec/services/groups/create_service_spec.rb b/spec/services/groups/create_service_spec.rb
index 81cab973b30..7ec523a1f2b 100644
--- a/spec/services/groups/create_service_spec.rb
+++ b/spec/services/groups/create_service_spec.rb
@@ -8,6 +8,10 @@ RSpec.describe Groups::CreateService, '#execute' do
subject { service.execute }
+ shared_examples 'has sync-ed traversal_ids' do
+ specify { expect(subject.reload.traversal_ids).to eq([subject.parent&.traversal_ids, subject.id].flatten.compact) }
+ end
+
describe 'visibility level restrictions' do
let!(:service) { described_class.new(user, group_params) }
@@ -77,6 +81,18 @@ RSpec.describe Groups::CreateService, '#execute' do
it 'adds an onboarding progress record' do
expect { subject }.to change(OnboardingProgress, :count).from(0).to(1)
end
+
+ context 'with before_commit callback' do
+ it_behaves_like 'has sync-ed traversal_ids'
+ end
+
+ context 'with after_create callback' do
+ before do
+ stub_feature_flags(sync_traversal_ids_before_commit: false)
+ end
+
+ it_behaves_like 'has sync-ed traversal_ids'
+ end
end
context 'when user can not create a group' do
@@ -102,6 +118,18 @@ RSpec.describe Groups::CreateService, '#execute' do
it 'does not add an onboarding progress record' do
expect { subject }.not_to change(OnboardingProgress, :count).from(0)
end
+
+ context 'with before_commit callback' do
+ it_behaves_like 'has sync-ed traversal_ids'
+ end
+
+ context 'with after_create callback' do
+ before do
+ stub_feature_flags(sync_traversal_ids_before_commit: false)
+ end
+
+ it_behaves_like 'has sync-ed traversal_ids'
+ end
end
context 'as guest' do
@@ -289,4 +317,33 @@ RSpec.describe Groups::CreateService, '#execute' do
end
end
end
+
+ describe 'logged_out_marketing_header experiment', :experiment do
+ let(:service) { described_class.new(user, group_params) }
+
+ subject { service.execute }
+
+ before do
+ stub_experiments(logged_out_marketing_header: :candidate)
+ end
+
+ it 'tracks signed_up event' do
+ expect(experiment(:logged_out_marketing_header)).to track(
+ :namespace_created,
+ namespace: an_instance_of(Group)
+ ).on_next_instance.with_context(actor: user)
+
+ subject
+ end
+
+ context 'when group has not been persisted' do
+ let(:service) { described_class.new(user, group_params.merge(name: '<script>alert("Attack!")</script>')) }
+
+ it 'does not track signed_up event' do
+ expect(experiment(:logged_out_marketing_header)).not_to track(:namespace_created)
+
+ subject
+ end
+ end
+ end
end
diff --git a/spec/services/groups/update_statistics_service_spec.rb b/spec/services/groups/update_statistics_service_spec.rb
new file mode 100644
index 00000000000..5bef51c2727
--- /dev/null
+++ b/spec/services/groups/update_statistics_service_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Groups::UpdateStatisticsService do
+ let_it_be(:group, reload: true) { create(:group) }
+
+ let(:statistics) { %w(wiki_size) }
+
+ subject(:service) { described_class.new(group, statistics: statistics)}
+
+ describe '#execute', :aggregate_failures do
+ context 'when group is nil' do
+ let(:group) { nil }
+
+ it 'does nothing' do
+ expect(NamespaceStatistics).not_to receive(:new)
+
+ result = service.execute
+
+ expect(result).to be_error
+ end
+ end
+
+ context 'with an existing group' do
+ context 'when namespace statistics exists for the group' do
+ it 'uses the existing statistics and refreshes them' do
+ namespace_statistics = create(:namespace_statistics, namespace: group)
+
+ expect(namespace_statistics).to receive(:refresh!).with(only: statistics.map(&:to_sym)).and_call_original
+
+ result = service.execute
+
+ expect(result).to be_success
+ end
+ end
+
+ context 'when namespace statistics does not exist for the group' do
+ it 'creates the statistics and refreshes them' do
+ expect_next_instance_of(NamespaceStatistics) do |instance|
+ expect(instance).to receive(:refresh!).with(only: statistics.map(&:to_sym)).and_call_original
+ end
+
+ result = nil
+
+ expect do
+ result = service.execute
+ end.to change { NamespaceStatistics.count }.by(1)
+
+ expect(result).to be_success
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/incident_management/create_incident_label_service_spec.rb b/spec/services/incident_management/create_incident_label_service_spec.rb
deleted file mode 100644
index 441cddf1d2e..00000000000
--- a/spec/services/incident_management/create_incident_label_service_spec.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe IncidentManagement::CreateIncidentLabelService do
- it_behaves_like 'incident management label service'
-end
diff --git a/spec/services/incident_management/incidents/create_service_spec.rb b/spec/services/incident_management/incidents/create_service_spec.rb
index 47ce9d01f66..ac44bc4608c 100644
--- a/spec/services/incident_management/incidents/create_service_spec.rb
+++ b/spec/services/incident_management/incidents/create_service_spec.rb
@@ -14,7 +14,6 @@ RSpec.describe IncidentManagement::Incidents::CreateService do
context 'when incident has title and description' do
let(:title) { 'Incident title' }
let(:new_issue) { Issue.last! }
- let(:label_title) { attributes_for(:label, :incident)[:title] }
it 'responds with success' do
expect(create_incident).to be_success
@@ -38,8 +37,6 @@ RSpec.describe IncidentManagement::Incidents::CreateService do
end
let(:issue) { new_issue }
-
- include_examples 'does not have incident label'
end
context 'with default severity' do
@@ -69,20 +66,6 @@ RSpec.describe IncidentManagement::Incidents::CreateService do
end
end
end
-
- context 'when incident label does not exists' do
- it 'does not create incident label' do
- expect { create_incident }.to not_change { project.labels.where(title: label_title).count }
- end
- end
-
- context 'when incident label already exists' do
- let!(:label) { create(:label, project: project, title: label_title) }
-
- it 'does not create new labels' do
- expect { create_incident }.not_to change(Label, :count)
- end
- end
end
context 'when incident has no title' do
@@ -100,6 +83,25 @@ RSpec.describe IncidentManagement::Incidents::CreateService do
it 'result payload contains an Issue object' do
expect(create_incident.payload[:issue]).to be_kind_of(Issue)
end
+
+ context 'with alert' do
+ let(:alert) { create(:alert_management_alert, project: project) }
+
+ subject(:create_incident) { described_class.new(project, user, title: title, description: description, alert: alert).execute }
+
+ it 'associates the alert with the incident' do
+ expect(create_incident[:issue].alert_management_alert).to eq(alert)
+ end
+
+ context 'the alert prevents the issue from saving' do
+ let(:alert) { create(:alert_management_alert, :with_validation_errors, project: project) }
+
+ it 'responds with errors' do
+ expect(create_incident).to be_error
+ expect(create_incident.message).to eq('Hosts hosts array is over 255 chars')
+ end
+ end
+ end
end
end
end
diff --git a/spec/services/incident_management/issuable_escalation_statuses/after_update_service_spec.rb b/spec/services/incident_management/issuable_escalation_statuses/after_update_service_spec.rb
index e9db6ba8d28..731406613dd 100644
--- a/spec/services/incident_management/issuable_escalation_statuses/after_update_service_spec.rb
+++ b/spec/services/incident_management/issuable_escalation_statuses/after_update_service_spec.rb
@@ -30,7 +30,15 @@ RSpec.describe IncidentManagement::IssuableEscalationStatuses::AfterUpdateServic
end
end
+ shared_examples 'adds a status change system note' do
+ specify do
+ expect { result }.to change { issue.reload.notes.count }.by(1)
+ end
+ end
+
context 'with status attributes' do
+ it_behaves_like 'adds a status change system note'
+
it 'updates the alert with the new alert status' do
expect(::AlertManagement::Alerts::UpdateService).to receive(:new).once.and_call_original
expect(described_class).to receive(:new).once.and_call_original
@@ -45,12 +53,15 @@ RSpec.describe IncidentManagement::IssuableEscalationStatuses::AfterUpdateServic
end
it_behaves_like 'does not attempt to update the alert'
+ it_behaves_like 'adds a status change system note'
end
context 'when new status matches the current status' do
let(:status_event) { :trigger }
it_behaves_like 'does not attempt to update the alert'
+
+ specify { expect { result }.not_to change { issue.reload.notes.count } }
end
end
end
diff --git a/spec/services/incident_management/issuable_escalation_statuses/prepare_update_service_spec.rb b/spec/services/incident_management/issuable_escalation_statuses/prepare_update_service_spec.rb
index bfed5319028..b30b3a69ae6 100644
--- a/spec/services/incident_management/issuable_escalation_statuses/prepare_update_service_spec.rb
+++ b/spec/services/incident_management/issuable_escalation_statuses/prepare_update_service_spec.rb
@@ -37,7 +37,7 @@ RSpec.describe IncidentManagement::IssuableEscalationStatuses::PrepareUpdateServ
end
shared_examples 'invalid params error response' do
- include_examples 'error response', 'Invalid value was provided for a parameter.'
+ include_examples 'error response', 'Invalid value was provided for parameters: status'
end
it_behaves_like 'successful response', { status_event: :acknowledge }
@@ -105,4 +105,10 @@ RSpec.describe IncidentManagement::IssuableEscalationStatuses::PrepareUpdateServ
it_behaves_like 'successful response', { status_event: :acknowledge }
end
end
+
+ context 'with status_change_reason param' do
+ let(:params) { { status_change_reason: ' by changing the incident status' } }
+
+ it_behaves_like 'successful response', { status_change_reason: ' by changing the incident status' }
+ end
end
diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb
index 158f9dec83e..1f6118e9fcc 100644
--- a/spec/services/issues/close_service_spec.rb
+++ b/spec/services/issues/close_service_spec.rb
@@ -118,7 +118,7 @@ RSpec.describe Issues::CloseService do
expect { service.execute(issue) }.to change { issue.notes.count }.by(1)
new_note = issue.notes.last
- expect(new_note.note).to eq('changed the status to **Resolved** by closing the incident')
+ expect(new_note.note).to eq('changed the incident status to **Resolved** by closing the incident')
expect(new_note.author).to eq(user)
end
@@ -334,8 +334,12 @@ RSpec.describe Issues::CloseService do
let!(:alert) { create(:alert_management_alert, issue: issue, project: project) }
it 'resolves an alert and sends a system note' do
- expect_next_instance_of(SystemNotes::AlertManagementService) do |notes_service|
- expect(notes_service).to receive(:closed_alert_issue).with(issue)
+ expect_any_instance_of(SystemNoteService) do |notes_service|
+ expect(notes_service).to receive(:change_alert_status).with(
+ alert,
+ current_user,
+ " by closing issue #{issue.to_reference(project)}"
+ )
end
close_issue
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index b2dcfb5c6d3..f4bb1f0877b 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe Issues::CreateService do
expect(described_class.rate_limiter_scoped_and_keyed.key).to eq(:issues_create)
expect(described_class.rate_limiter_scoped_and_keyed.opts[:scope]).to eq(%i[project current_user external_author])
- expect(described_class.rate_limiter_scoped_and_keyed.rate_limiter_klass).to eq(Gitlab::ApplicationRateLimiter)
+ expect(described_class.rate_limiter_scoped_and_keyed.rate_limiter).to eq(Gitlab::ApplicationRateLimiter)
end
end
@@ -92,6 +92,21 @@ RSpec.describe Issues::CreateService do
end
end
+ context 'when setting a position' do
+ let(:issue_before) { create(:issue, project: project, relative_position: 10) }
+ let(:issue_after) { create(:issue, project: project, relative_position: 50) }
+
+ before do
+ opts.merge!(move_between_ids: [issue_before.id, issue_after.id])
+ end
+
+ it 'sets the correct relative position' do
+ expect(issue).to be_persisted
+ expect(issue.relative_position).to be_present
+ expect(issue.relative_position).to be_between(issue_before.relative_position, issue_after.relative_position)
+ end
+ end
+
it_behaves_like 'not an incident issue'
context 'when issue is incident type' do
@@ -100,7 +115,6 @@ RSpec.describe Issues::CreateService do
end
let(:current_user) { user }
- let(:incident_label_attributes) { attributes_for(:label, :incident) }
subject { issue }
@@ -114,12 +128,6 @@ RSpec.describe Issues::CreateService do
end
it_behaves_like 'incident issue'
- it_behaves_like 'does not have incident label'
-
- it 'does not create an incident label' do
- expect { subject }
- .to not_change { Label.where(incident_label_attributes).count }
- end
it 'calls IncidentManagement::Incidents::CreateEscalationStatusService' do
expect_next(::IncidentManagement::IssuableEscalationStatuses::CreateService, a_kind_of(Issue))
diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb
index ef501f47f0d..35a380e01d0 100644
--- a/spec/services/issues/move_service_spec.rb
+++ b/spec/services/issues/move_service_spec.rb
@@ -168,6 +168,48 @@ RSpec.describe Issues::MoveService do
end
end
+ context 'issue with contacts' do
+ let_it_be(:contacts) { create_list(:contact, 2, group: group) }
+
+ before do
+ old_issue.customer_relations_contacts = contacts
+ end
+
+ it 'preserves contacts' do
+ new_issue = move_service.execute(old_issue, new_project)
+
+ expect(new_issue.customer_relations_contacts).to eq(contacts)
+ end
+
+ context 'when moving to another root group' do
+ let(:another_project) { create(:project, namespace: create(:group)) }
+
+ before do
+ another_project.add_reporter(user)
+ end
+
+ it 'does not preserve contacts' do
+ new_issue = move_service.execute(old_issue, another_project)
+
+ expect(new_issue.customer_relations_contacts).to be_empty
+ end
+ end
+
+ context 'when customer_relations feature is disabled' do
+ let(:another_project) { create(:project, namespace: create(:group)) }
+
+ before do
+ stub_feature_flags(customer_relations: false)
+ end
+
+ it 'does not preserve contacts' do
+ new_issue = move_service.execute(old_issue, new_project)
+
+ expect(new_issue.customer_relations_contacts).to be_empty
+ end
+ end
+ end
+
context 'moving to same project' do
let(:new_project) { old_project }
diff --git a/spec/services/issues/reorder_service_spec.rb b/spec/services/issues/reorder_service_spec.rb
index 15668a3aa23..392930c1b9f 100644
--- a/spec/services/issues/reorder_service_spec.rb
+++ b/spec/services/issues/reorder_service_spec.rb
@@ -67,20 +67,10 @@ RSpec.describe Issues::ReorderService do
it_behaves_like 'issues reorder service'
context 'when ordering in a group issue list' do
- let(:params) { { move_after_id: issue2.id, move_before_id: issue3.id, group_full_path: group.full_path } }
+ let(:params) { { move_after_id: issue2.id, move_before_id: issue3.id } }
subject { service(params) }
- it 'sends the board_group_id parameter' do
- match_params = { move_between_ids: [issue2.id, issue3.id], board_group_id: group.id }
-
- expect(Issues::UpdateService)
- .to receive(:new).with(project: project, current_user: user, params: match_params)
- .and_return(double(execute: build(:issue)))
-
- subject.execute(issue1)
- end
-
it 'sorts issues' do
project2 = create(:project, namespace: group)
issue4 = create(:issue, project: project2)
diff --git a/spec/services/issues/set_crm_contacts_service_spec.rb b/spec/services/issues/set_crm_contacts_service_spec.rb
index 2418f317551..64011a7a003 100644
--- a/spec/services/issues/set_crm_contacts_service_spec.rb
+++ b/spec/services/issues/set_crm_contacts_service_spec.rb
@@ -7,20 +7,41 @@ RSpec.describe Issues::SetCrmContactsService do
let_it_be(:group) { create(:group, :crm_enabled) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:contacts) { create_list(:contact, 4, group: group) }
+ let_it_be(:issue, reload: true) { create(:issue, project: project) }
+ let_it_be(:issue_contact_1) do
+ create(:issue_customer_relations_contact, issue: issue, contact: contacts[0]).contact
+ end
- let(:issue) { create(:issue, project: project) }
- let(:does_not_exist_or_no_permission) { "The resource that you are attempting to access does not exist or you don't have permission to perform this action" }
-
- before do
- create(:issue_customer_relations_contact, issue: issue, contact: contacts[0])
- create(:issue_customer_relations_contact, issue: issue, contact: contacts[1])
+ let_it_be(:issue_contact_2) do
+ create(:issue_customer_relations_contact, issue: issue, contact: contacts[1]).contact
end
+ let(:does_not_exist_or_no_permission) { "The resource that you are attempting to access does not exist or you don't have permission to perform this action" }
+
subject(:set_crm_contacts) do
described_class.new(project: project, current_user: user, params: params).execute(issue)
end
describe '#execute' do
+ shared_examples 'setting contacts' do
+ it 'updates the issue with correct contacts' do
+ response = set_crm_contacts
+
+ expect(response).to be_success
+ expect(issue.customer_relations_contacts).to match_array(expected_contacts)
+ end
+ end
+
+ shared_examples 'adds system note' do |added_count, removed_count|
+ it 'calls SystemNoteService.change_issuable_contacts with correct counts' do
+ expect(SystemNoteService)
+ .to receive(:change_issuable_contacts)
+ .with(issue, project, user, added_count, removed_count)
+
+ set_crm_contacts
+ end
+ end
+
context 'when the user has no permission' do
let(:params) { { replace_ids: [contacts[1].id, contacts[2].id] } }
@@ -67,56 +88,63 @@ RSpec.describe Issues::SetCrmContactsService do
context 'replace' do
let(:params) { { replace_ids: [contacts[1].id, contacts[2].id] } }
+ let(:expected_contacts) { [contacts[1], contacts[2]] }
- it 'updates the issue with correct contacts' do
- response = set_crm_contacts
-
- expect(response).to be_success
- expect(issue.customer_relations_contacts).to match_array([contacts[1], contacts[2]])
- end
+ it_behaves_like 'setting contacts'
+ it_behaves_like 'adds system note', 1, 1
end
context 'add' do
- let(:params) { { add_ids: [contacts[3].id] } }
-
- it 'updates the issue with correct contacts' do
- response = set_crm_contacts
+ let(:added_contact) { contacts[3] }
+ let(:params) { { add_ids: [added_contact.id] } }
+ let(:expected_contacts) { [issue_contact_1, issue_contact_2, added_contact] }
- expect(response).to be_success
- expect(issue.customer_relations_contacts).to match_array([contacts[0], contacts[1], contacts[3]])
- end
+ it_behaves_like 'setting contacts'
+ it_behaves_like 'adds system note', 1, 0
end
context 'add by email' do
- let(:params) { { add_emails: [contacts[3].email] } }
+ let(:added_contact) { contacts[3] }
+ let(:expected_contacts) { [issue_contact_1, issue_contact_2, added_contact] }
- it 'updates the issue with correct contacts' do
- response = set_crm_contacts
+ context 'with pure emails in params' do
+ let(:params) { { add_emails: [contacts[3].email] } }
- expect(response).to be_success
- expect(issue.customer_relations_contacts).to match_array([contacts[0], contacts[1], contacts[3]])
+ it_behaves_like 'setting contacts'
+ it_behaves_like 'adds system note', 1, 0
+ end
+
+ context 'with autocomplete prefix emails in params' do
+ let(:params) { { add_emails: ["[\"contact:\"#{contacts[3].email}\"]"] } }
+
+ it_behaves_like 'setting contacts'
+ it_behaves_like 'adds system note', 1, 0
end
end
context 'remove' do
let(:params) { { remove_ids: [contacts[0].id] } }
+ let(:expected_contacts) { [contacts[1]] }
- it 'updates the issue with correct contacts' do
- response = set_crm_contacts
-
- expect(response).to be_success
- expect(issue.customer_relations_contacts).to match_array([contacts[1]])
- end
+ it_behaves_like 'setting contacts'
+ it_behaves_like 'adds system note', 0, 1
end
context 'remove by email' do
- let(:params) { { remove_emails: [contacts[0].email] } }
+ let(:expected_contacts) { [contacts[1]] }
- it 'updates the issue with correct contacts' do
- response = set_crm_contacts
+ context 'with pure email in params' do
+ let(:params) { { remove_emails: [contacts[0].email] } }
- expect(response).to be_success
- expect(issue.customer_relations_contacts).to match_array([contacts[1]])
+ it_behaves_like 'setting contacts'
+ it_behaves_like 'adds system note', 0, 1
+ end
+
+ context 'with autocomplete prefix and suffix email in params' do
+ let(:params) { { remove_emails: ["[contact:#{contacts[0].email}]"] } }
+
+ it_behaves_like 'setting contacts'
+ it_behaves_like 'adds system note', 0, 1
end
end
@@ -145,15 +173,19 @@ RSpec.describe Issues::SetCrmContactsService do
context 'when combining params' do
let(:error_invalid_params) { 'You cannot combine replace_ids with add_ids or remove_ids' }
+ let(:expected_contacts) { [contacts[0], contacts[3]] }
context 'add and remove' do
- let(:params) { { remove_ids: [contacts[1].id], add_ids: [contacts[3].id] } }
+ context 'with contact ids' do
+ let(:params) { { remove_ids: [contacts[1].id], add_ids: [contacts[3].id] } }
- it 'updates the issue with correct contacts' do
- response = set_crm_contacts
+ it_behaves_like 'setting contacts'
+ end
+
+ context 'with contact emails' do
+ let(:params) { { remove_emails: [contacts[1].email], add_emails: ["[\"contact:#{contacts[3].email}]"] } }
- expect(response).to be_success
- expect(issue.customer_relations_contacts).to match_array([contacts[0], contacts[3]])
+ it_behaves_like 'setting contacts'
end
end
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index 11ed47b84d9..95394ba6597 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -385,7 +385,6 @@ RSpec.describe Issues::UpdateService, :mailer do
[issue_1, issue_2, issue_3].map(&:save)
opts[:move_between_ids] = [issue_1.id, issue_2.id]
- opts[:board_group_id] = group.id
described_class.new(project: issue_3.project, current_user: user, params: opts).execute(issue_3)
expect(issue_2.relative_position).to be_between(issue_1.relative_position, issue_2.relative_position)
@@ -1147,11 +1146,11 @@ RSpec.describe Issues::UpdateService, :mailer do
let(:opts) { { escalation_status: { status: 'acknowledged' } } }
let(:escalation_update_class) { ::IncidentManagement::IssuableEscalationStatuses::AfterUpdateService }
- shared_examples 'updates the escalation status record' do |expected_status|
+ shared_examples 'updates the escalation status record' do |expected_status, expected_reason = nil|
let(:service_double) { instance_double(escalation_update_class) }
it 'has correct value' do
- expect(escalation_update_class).to receive(:new).with(issue, user).and_return(service_double)
+ expect(escalation_update_class).to receive(:new).with(issue, user, status_change_reason: expected_reason).and_return(service_double)
expect(service_double).to receive(:execute)
update_issue(opts)
@@ -1197,6 +1196,12 @@ RSpec.describe Issues::UpdateService, :mailer do
end
end
+ context 'with a status change reason provided' do
+ let(:opts) { { escalation_status: { status: 'acknowledged', status_change_reason: ' by changing the alert status' } } }
+
+ it_behaves_like 'updates the escalation status record', :acknowledged, ' by changing the alert status'
+ end
+
context 'with unsupported status value' do
let(:opts) { { escalation_status: { status: 'unsupported-status' } } }
@@ -1303,19 +1308,12 @@ RSpec.describe Issues::UpdateService, :mailer do
end
context 'when moving an issue ' do
- it 'raises an error for invalid move ids within a project' do
+ it 'raises an error for invalid move ids' do
opts = { move_between_ids: [9000, non_existing_record_id] }
expect { described_class.new(project: issue.project, current_user: user, params: opts).execute(issue) }
.to raise_error(ActiveRecord::RecordNotFound)
end
-
- it 'raises an error for invalid move ids within a group' do
- opts = { move_between_ids: [9000, non_existing_record_id], board_group_id: create(:group).id }
-
- expect { described_class.new(project: issue.project, current_user: user, params: opts).execute(issue) }
- .to raise_error(ActiveRecord::RecordNotFound)
- end
end
include_examples 'issuable update service' do
diff --git a/spec/services/loose_foreign_keys/batch_cleaner_service_spec.rb b/spec/services/loose_foreign_keys/batch_cleaner_service_spec.rb
index d3d57ea2444..538d9638879 100644
--- a/spec/services/loose_foreign_keys/batch_cleaner_service_spec.rb
+++ b/spec/services/loose_foreign_keys/batch_cleaner_service_spec.rb
@@ -115,4 +115,82 @@ RSpec.describe LooseForeignKeys::BatchCleanerService do
expect(loose_fk_child_table_2.where(parent_id_with_different_column: other_parent_record.id).count).to eq(2)
end
end
+
+ describe 'fair queueing' do
+ context 'when the execution is over the limit' do
+ let(:modification_tracker) { instance_double(LooseForeignKeys::ModificationTracker) }
+ let(:over_limit_return_values) { [true] }
+ let(:deleted_record) { LooseForeignKeys::DeletedRecord.load_batch_for_table('public._test_loose_fk_parent_table', 1).first }
+ let(:deleted_records_rescheduled_counter) { Gitlab::Metrics.registry.get(:loose_foreign_key_rescheduled_deleted_records) }
+ let(:deleted_records_incremented_counter) { Gitlab::Metrics.registry.get(:loose_foreign_key_incremented_deleted_records) }
+
+ let(:cleaner) do
+ described_class.new(parent_table: '_test_loose_fk_parent_table',
+ loose_foreign_key_definitions: loose_foreign_key_definitions,
+ deleted_parent_records: LooseForeignKeys::DeletedRecord.load_batch_for_table('public._test_loose_fk_parent_table', 100),
+ modification_tracker: modification_tracker
+ )
+ end
+
+ before do
+ parent_record_1.delete
+ allow(modification_tracker).to receive(:over_limit?).and_return(*over_limit_return_values)
+ allow(modification_tracker).to receive(:add_deletions)
+ end
+
+ context 'when the deleted record is under the maximum allowed cleanup attempts' do
+ it 'updates the cleanup_attempts column', :aggregate_failures do
+ deleted_record.update!(cleanup_attempts: 1)
+
+ cleaner.execute
+
+ expect(deleted_record.reload.cleanup_attempts).to eq(2)
+ expect(deleted_records_incremented_counter.get(table: loose_fk_parent_table.table_name, db_config_name: 'main')).to eq(1)
+ end
+
+ context 'when the deleted record is above the maximum allowed cleanup attempts' do
+ it 'reschedules the record', :aggregate_failures do
+ deleted_record.update!(cleanup_attempts: LooseForeignKeys::BatchCleanerService::CLEANUP_ATTEMPTS_BEFORE_RESCHEDULE + 1)
+
+ freeze_time do
+ cleaner.execute
+
+ expect(deleted_record.reload).to have_attributes(
+ cleanup_attempts: 0,
+ consume_after: 5.minutes.from_now
+ )
+ expect(deleted_records_rescheduled_counter.get(table: loose_fk_parent_table.table_name, db_config_name: 'main')).to eq(1)
+ end
+ end
+ end
+
+ describe 'when over limit happens on the second cleanup call without skip locked' do
+ # over_limit? is called twice, we test here the 2nd call
+ # - When invoking cleanup with SKIP LOCKED
+ # - When invoking cleanup (no SKIP LOCKED)
+ let(:over_limit_return_values) { [false, true] }
+
+ it 'updates the cleanup_attempts column' do
+ expect(cleaner).to receive(:run_cleaner_service).twice
+
+ deleted_record.update!(cleanup_attempts: 1)
+
+ cleaner.execute
+
+ expect(deleted_record.reload.cleanup_attempts).to eq(2)
+ end
+ end
+ end
+
+ context 'when the lfk_fair_queueing FF is off' do
+ before do
+ stub_feature_flags(lfk_fair_queueing: false)
+ end
+
+ it 'does nothing' do
+ expect { cleaner.execute }.not_to change { deleted_record.reload.cleanup_attempts }
+ end
+ end
+ end
+ end
end
diff --git a/spec/services/members/create_service_spec.rb b/spec/services/members/create_service_spec.rb
index 13f56fe7458..4d9e69719b4 100644
--- a/spec/services/members/create_service_spec.rb
+++ b/spec/services/members/create_service_spec.rb
@@ -39,6 +39,15 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_
expect(source.users).to include member
expect(OnboardingProgress.completed?(source, :user_added)).to be(true)
end
+
+ it 'triggers a members added event' do
+ expect(Gitlab::EventStore)
+ .to receive(:publish)
+ .with(an_instance_of(Members::MembersAddedEvent))
+ .and_call_original
+
+ expect(execute_service[:status]).to eq(:success)
+ end
end
end
@@ -108,6 +117,24 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_
user: user
)
end
+
+ context 'with an already existing member' do
+ before do
+ source.add_developer(member)
+ end
+
+ it 'tracks the invite source from params' do
+ execute_service
+
+ expect_snowplow_event(
+ category: described_class.name,
+ action: 'create_member',
+ label: '_invite_source_',
+ property: 'existing_user',
+ user: user
+ )
+ end
+ end
end
context 'when it is a net_new_user' do
diff --git a/spec/services/merge_requests/after_create_service_spec.rb b/spec/services/merge_requests/after_create_service_spec.rb
index 781be57d709..2155b4ffad1 100644
--- a/spec/services/merge_requests/after_create_service_spec.rb
+++ b/spec/services/merge_requests/after_create_service_spec.rb
@@ -89,10 +89,10 @@ RSpec.describe MergeRequests::AfterCreateService do
merge_request.mark_as_preparing!
end
- it 'marks the merge request as unchecked' do
- execute_service
+ it 'checks for mergeability' do
+ expect(merge_request).to receive(:check_mergeability).with(async: true)
- expect(merge_request.reload).to be_unchecked
+ execute_service
end
context 'when preparing for mergeability fails' do
@@ -108,17 +108,6 @@ RSpec.describe MergeRequests::AfterCreateService do
expect { execute_service }.to raise_error(StandardError)
expect(merge_request.reload).to be_preparing
end
-
- context 'when early_prepare_for_mergeability feature flag is disabled' do
- before do
- stub_feature_flags(early_prepare_for_mergeability: false)
- end
-
- it 'does not mark the merge request as unchecked' do
- expect { execute_service }.to raise_error(StandardError)
- expect(merge_request.reload).to be_preparing
- end
- end
end
context 'when preparing merge request fails' do
@@ -130,20 +119,9 @@ RSpec.describe MergeRequests::AfterCreateService do
.and_raise(StandardError)
end
- it 'still marks the merge request as unchecked' do
+ it 'still checks for mergeability' do
+ expect(merge_request).to receive(:check_mergeability).with(async: true)
expect { execute_service }.to raise_error(StandardError)
- expect(merge_request.reload).to be_unchecked
- end
-
- context 'when early_prepare_for_mergeability feature flag is disabled' do
- before do
- stub_feature_flags(early_prepare_for_mergeability: false)
- end
-
- it 'does not mark the merge request as unchecked' do
- expect { execute_service }.to raise_error(StandardError)
- expect(merge_request.reload).to be_preparing
- end
end
end
end
diff --git a/spec/services/merge_requests/bulk_remove_attention_requested_service_spec.rb b/spec/services/merge_requests/bulk_remove_attention_requested_service_spec.rb
index fe4ce0dab5e..ae8846974ce 100644
--- a/spec/services/merge_requests/bulk_remove_attention_requested_service_spec.rb
+++ b/spec/services/merge_requests/bulk_remove_attention_requested_service_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe MergeRequests::BulkRemoveAttentionRequestedService do
let(:reviewer) { merge_request.find_reviewer(user) }
let(:assignee) { merge_request.find_assignee(assignee_user) }
let(:project) { merge_request.project }
- let(:service) { described_class.new(project: project, current_user: current_user, merge_request: merge_request) }
+ let(:service) { described_class.new(project: project, current_user: current_user, merge_request: merge_request, users: [user, assignee_user]) }
let(:result) { service.execute }
before do
@@ -20,7 +20,7 @@ RSpec.describe MergeRequests::BulkRemoveAttentionRequestedService do
describe '#execute' do
context 'invalid permissions' do
- let(:service) { described_class.new(project: project, current_user: create(:user), merge_request: merge_request) }
+ let(:service) { described_class.new(project: project, current_user: create(:user), merge_request: merge_request, users: [user]) }
it 'returns an error' do
expect(result[:status]).to eq :error
diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb
index da547716e1e..a196c944eda 100644
--- a/spec/services/merge_requests/create_service_spec.rb
+++ b/spec/services/merge_requests/create_service_spec.rb
@@ -61,19 +61,19 @@ RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do
expect(merge_request.reload).to be_preparing
end
- describe 'when marked with /wip' do
+ describe 'when marked with /draft' do
context 'in title and in description' do
let(:opts) do
{
- title: 'WIP: Awesome merge_request',
- description: "well this is not done yet\n/wip",
+ title: 'Draft: Awesome merge_request',
+ description: "well this is not done yet\n/draft",
source_branch: 'feature',
target_branch: 'master',
assignees: [user2]
}
end
- it 'sets MR to WIP' do
+ it 'sets MR to draft' do
expect(merge_request.work_in_progress?).to be(true)
end
end
@@ -89,7 +89,7 @@ RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do
}
end
- it 'sets MR to WIP' do
+ it 'sets MR to draft' do
expect(merge_request.work_in_progress?).to be(true)
end
end
diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb
index 127c94763d9..ecb856bd1a4 100644
--- a/spec/services/merge_requests/merge_service_spec.rb
+++ b/spec/services/merge_requests/merge_service_spec.rb
@@ -388,7 +388,7 @@ RSpec.describe MergeRequests::MergeService do
end
it 'logs and saves error if there is an error when squashing' do
- error_message = 'Failed to squash. Should be done manually'
+ error_message = 'Squashing failed: Squash the commits locally, resolve any conflicts, then push the branch.'
allow_any_instance_of(MergeRequests::SquashService).to receive(:squash!).and_return(nil)
merge_request.update!(squash: true)
diff --git a/spec/services/merge_requests/mergeability_check_service_spec.rb b/spec/services/merge_requests/mergeability_check_service_spec.rb
index 65599b7e046..c24b83e21a6 100644
--- a/spec/services/merge_requests/mergeability_check_service_spec.rb
+++ b/spec/services/merge_requests/mergeability_check_service_spec.rb
@@ -73,12 +73,10 @@ RSpec.describe MergeRequests::MergeabilityCheckService, :clean_gitlab_redis_shar
let(:merge_request) { create(:merge_request, merge_status: :unchecked, source_project: project, target_project: project) }
describe '#async_execute' do
- shared_examples_for 'no job is enqueued' do
- it 'does not enqueue MergeRequestMergeabilityCheckWorker' do
- expect(MergeRequestMergeabilityCheckWorker).not_to receive(:perform_async)
+ it 'updates merge status to checking' do
+ described_class.new(merge_request).async_execute
- described_class.new(merge_request).async_execute
- end
+ expect(merge_request).to be_checking
end
it 'enqueues MergeRequestMergeabilityCheckWorker' do
@@ -92,15 +90,11 @@ RSpec.describe MergeRequests::MergeabilityCheckService, :clean_gitlab_redis_shar
allow(Gitlab::Database).to receive(:read_only?) { true }
end
- it_behaves_like 'no job is enqueued'
- end
+ it 'does not enqueue MergeRequestMergeabilityCheckWorker' do
+ expect(MergeRequestMergeabilityCheckWorker).not_to receive(:perform_async)
- context 'when merge_status is already checking' do
- before do
- merge_request.mark_as_checking
+ described_class.new(merge_request).async_execute
end
-
- it_behaves_like 'no job is enqueued'
end
end
diff --git a/spec/services/merge_requests/rebase_service_spec.rb b/spec/services/merge_requests/rebase_service_spec.rb
index e671bbf2cd6..a47e626666b 100644
--- a/spec/services/merge_requests/rebase_service_spec.rb
+++ b/spec/services/merge_requests/rebase_service_spec.rb
@@ -82,7 +82,7 @@ RSpec.describe MergeRequests::RebaseService do
context 'with a pre-receive failure' do
let(:pre_receive_error) { "Commit message does not follow the pattern 'ACME'" }
- let(:merge_error) { "Something went wrong during the rebase pre-receive hook: #{pre_receive_error}." }
+ let(:merge_error) { "The rebase pre-receive hook failed: #{pre_receive_error}." }
before do
allow(repository).to receive(:gitaly_operation_client).and_raise(Gitlab::Git::PreReceiveError, "GitLab: #{pre_receive_error}")
diff --git a/spec/services/merge_requests/squash_service_spec.rb b/spec/services/merge_requests/squash_service_spec.rb
index e5bea0e7b14..387be8471b5 100644
--- a/spec/services/merge_requests/squash_service_spec.rb
+++ b/spec/services/merge_requests/squash_service_spec.rb
@@ -168,7 +168,7 @@ RSpec.describe MergeRequests::SquashService do
it 'raises a squash error' do
expect(service.execute).to match(
status: :error,
- message: a_string_including('does not allow squashing commits when merge requests are accepted'))
+ message: a_string_including('allow you to squash commits when merging'))
end
end
@@ -205,7 +205,7 @@ RSpec.describe MergeRequests::SquashService do
end
it 'returns an error' do
- expect(service.execute).to match(status: :error, message: a_string_including('squash'))
+ expect(service.execute).to match(status: :error, message: a_string_including('Squash'))
end
end
end
@@ -232,7 +232,7 @@ RSpec.describe MergeRequests::SquashService do
end
it 'returns an error' do
- expect(service.execute).to match(status: :error, message: a_string_including('squash'))
+ expect(service.execute).to match(status: :error, message: a_string_including('Squash'))
end
it 'cleans up the temporary directory' do
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index 2925dad7f6b..48d9f019274 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -102,16 +102,16 @@ RSpec.describe MergeRequests::UpdateService, :mailer do
MergeRequests::UpdateService.new(project: project, current_user: user, params: opts).execute(merge_request2)
end
- it 'tracks Draft/WIP marking' do
+ it 'tracks Draft marking' do
expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
.to receive(:track_marked_as_draft_action).once.with(user: user)
- opts[:title] = "WIP: #{opts[:title]}"
+ opts[:title] = "Draft: #{opts[:title]}"
MergeRequests::UpdateService.new(project: project, current_user: user, params: opts).execute(merge_request2)
end
- it 'tracks Draft/WIP un-marking' do
+ it 'tracks Draft un-marking' do
expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
.to receive(:track_unmarked_as_draft_action).once.with(user: user)
diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb
index 1fb50b07b3b..babbd44a9f1 100644
--- a/spec/services/notes/create_service_spec.rb
+++ b/spec/services/notes/create_service_spec.rb
@@ -325,11 +325,11 @@ RSpec.describe Notes::CreateService do
expect(issuable.work_in_progress?).to eq(can_use_quick_action)
}
),
- # Remove WIP status
+ # Remove draft status
QuickAction.new(
action_text: "/draft",
before_action: -> {
- issuable.reload.update!(title: "WIP: title")
+ issuable.reload.update!(title: "Draft: title")
},
expectation: ->(noteable, can_use_quick_action) {
expect(noteable.work_in_progress?).not_to eq(can_use_quick_action)
diff --git a/spec/services/packages/maven/metadata/sync_service_spec.rb b/spec/services/packages/maven/metadata/sync_service_spec.rb
index a736ed281f0..9a704d749b3 100644
--- a/spec/services/packages/maven/metadata/sync_service_spec.rb
+++ b/spec/services/packages/maven/metadata/sync_service_spec.rb
@@ -9,6 +9,7 @@ RSpec.describe ::Packages::Maven::Metadata::SyncService do
let_it_be(:user) { create(:user) }
let_it_be_with_reload(:versionless_package_for_versions) { create(:maven_package, name: 'test', version: nil, project: project) }
let_it_be_with_reload(:metadata_file_for_versions) { create(:package_file, :xml, package: versionless_package_for_versions) }
+ let_it_be(:package_file_pending_destruction) { create(:package_file, :pending_destruction, package: versionless_package_for_versions, file_name: Packages::Maven::Metadata.filename) }
let(:service) { described_class.new(container: project, current_user: user, params: { package_name: versionless_package_for_versions.name }) }
@@ -265,22 +266,4 @@ RSpec.describe ::Packages::Maven::Metadata::SyncService do
end
end
end
-
- # TODO When cleaning up packages_installable_package_files, consider adding a
- # dummy package file pending for destruction on L10/11 and remove this context
- context 'with package files pending destruction' do
- let_it_be(:package_file_pending_destruction) { create(:package_file, :pending_destruction, package: versionless_package_for_versions, file_name: Packages::Maven::Metadata.filename) }
-
- subject { service.send(:metadata_package_file_for, versionless_package_for_versions) }
-
- it { is_expected.not_to eq(package_file_pending_destruction) }
-
- context 'with packages_installable_package_files disabled' do
- before do
- stub_feature_flags(packages_installable_package_files: false)
- end
-
- it { is_expected.to eq(package_file_pending_destruction) }
- end
- end
end
diff --git a/spec/services/packages/nuget/metadata_extraction_service_spec.rb b/spec/services/packages/nuget/metadata_extraction_service_spec.rb
index 8eddd27f8a2..fc21cfd502e 100644
--- a/spec/services/packages/nuget/metadata_extraction_service_spec.rb
+++ b/spec/services/packages/nuget/metadata_extraction_service_spec.rb
@@ -78,7 +78,7 @@ RSpec.describe Packages::Nuget::MetadataExtractionService do
end
context 'with invalid package file id' do
- let(:package_file) { OpenStruct.new(id: 555) }
+ let(:package_file) { double('file', id: 555) }
it { expect { subject }.to raise_error(::Packages::Nuget::MetadataExtractionService::ExtractionError, 'invalid package file') }
end
@@ -109,7 +109,7 @@ RSpec.describe Packages::Nuget::MetadataExtractionService do
context 'with a too big nuspec file' do
before do
- allow_any_instance_of(Zip::File).to receive(:glob).and_return([OpenStruct.new(size: 6.megabytes)])
+ allow_any_instance_of(Zip::File).to receive(:glob).and_return([double('file', size: 6.megabytes)])
end
it { expect { subject }.to raise_error(::Packages::Nuget::MetadataExtractionService::ExtractionError, 'nuspec file too big') }
diff --git a/spec/services/pages/zip_directory_service_spec.rb b/spec/services/pages/zip_directory_service_spec.rb
index 9cce90c6c0d..00fe75dbbfd 100644
--- a/spec/services/pages/zip_directory_service_spec.rb
+++ b/spec/services/pages/zip_directory_service_spec.rb
@@ -27,6 +27,10 @@ RSpec.describe Pages::ZipDirectoryService do
let(:archive) { result[:archive_path] }
let(:entries_count) { result[:entries_count] }
+ it 'returns true if ZIP64 is enabled' do
+ expect(::Zip.write_zip64_support).to be true
+ end
+
shared_examples 'handles invalid public directory' do
it 'returns success' do
expect(status).to eq(:success)
@@ -35,7 +39,7 @@ RSpec.describe Pages::ZipDirectoryService do
end
end
- context "when work direcotry doesn't exist" do
+ context "when work directory doesn't exist" do
let(:service_directory) { "/tmp/not/existing/dir" }
include_examples 'handles invalid public directory'
diff --git a/spec/services/projects/autocomplete_service_spec.rb b/spec/services/projects/autocomplete_service_spec.rb
index ef7741c2d0f..ed043bacf31 100644
--- a/spec/services/projects/autocomplete_service_spec.rb
+++ b/spec/services/projects/autocomplete_service_spec.rb
@@ -148,6 +148,32 @@ RSpec.describe Projects::AutocompleteService do
end
end
+ describe '#contacts' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group, :crm_enabled) }
+ let_it_be(:project) { create(:project, group: group) }
+ let_it_be(:contact_1) { create(:contact, group: group) }
+ let_it_be(:contact_2) { create(:contact, group: group) }
+
+ subject { described_class.new(project, user).contacts.as_json }
+
+ before do
+ stub_feature_flags(customer_relations: true)
+ group.add_developer(user)
+ end
+
+ it 'returns contact data correctly' do
+ expected_contacts = [
+ { 'id' => contact_1.id, 'email' => contact_1.email,
+ 'first_name' => contact_1.first_name, 'last_name' => contact_1.last_name },
+ { 'id' => contact_2.id, 'email' => contact_2.email,
+ 'first_name' => contact_2.first_name, 'last_name' => contact_2.last_name }
+ ]
+
+ expect(subject).to match_array(expected_contacts)
+ end
+ end
+
describe '#labels_as_hash' do
def expect_labels_to_equal(labels, expected_labels)
expect(labels.size).to eq(expected_labels.size)
diff --git a/spec/services/projects/container_repository/delete_tags_service_spec.rb b/spec/services/projects/container_repository/delete_tags_service_spec.rb
index 94037d6de1e..246ca301cfa 100644
--- a/spec/services/projects/container_repository/delete_tags_service_spec.rb
+++ b/spec/services/projects/container_repository/delete_tags_service_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe Projects::ContainerRepository::DeleteTagsService do
]
end
- RSpec.shared_examples 'logging a success response' do
+ shared_examples 'logging a success response' do
it 'logs an info message' do
expect(service).to receive(:log_info).with(
service_class: 'Projects::ContainerRepository::DeleteTagsService',
@@ -28,7 +28,7 @@ RSpec.describe Projects::ContainerRepository::DeleteTagsService do
end
end
- RSpec.shared_examples 'logging an error response' do |message: 'could not delete tags', extra_log: {}|
+ shared_examples 'logging an error response' do |message: 'could not delete tags', extra_log: {}|
it 'logs an error message' do
log_data = {
service_class: 'Projects::ContainerRepository::DeleteTagsService',
@@ -45,7 +45,7 @@ RSpec.describe Projects::ContainerRepository::DeleteTagsService do
end
end
- RSpec.shared_examples 'calling the correct delete tags service' do |expected_service_class|
+ shared_examples 'calling the correct delete tags service' do |expected_service_class|
let(:service_response) { { status: :success, deleted: tags } }
let(:excluded_service_class) { available_service_classes.excluding(expected_service_class).first }
@@ -69,7 +69,7 @@ RSpec.describe Projects::ContainerRepository::DeleteTagsService do
end
end
- RSpec.shared_examples 'handling invalid params' do
+ shared_examples 'handling invalid params' do
context 'with invalid params' do
before do
expect(::Projects::ContainerRepository::Gitlab::DeleteTagsService).not_to receive(:new)
@@ -91,7 +91,7 @@ RSpec.describe Projects::ContainerRepository::DeleteTagsService do
end
end
- RSpec.shared_examples 'supporting fast delete' do
+ shared_examples 'supporting fast delete' do
context 'when the registry supports fast delete' do
before do
allow(repository.client).to receive(:supports_tag_delete?).and_return(true)
@@ -155,6 +155,14 @@ RSpec.describe Projects::ContainerRepository::DeleteTagsService do
it_behaves_like 'handling invalid params'
end
+
+ context 'when the repository is importing' do
+ before do
+ repository.update_columns(migration_state: 'importing', migration_import_started_at: Time.zone.now)
+ end
+
+ it { is_expected.to include(status: :error, message: 'repository importing') }
+ end
end
context 'without user' do
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index d5fbf96ce74..10f694827e1 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -7,9 +7,10 @@ RSpec.describe Projects::CreateService, '#execute' do
include GitHelpers
let(:user) { create :user }
+ let(:project_name) { 'GitLab' }
let(:opts) do
{
- name: 'GitLab',
+ name: project_name,
namespace_id: user.namespace.id
}
end
@@ -144,6 +145,12 @@ RSpec.describe Projects::CreateService, '#execute' do
subject { create_project(user, opts) }
end
+
+ it 'logs creation' do
+ expect(Gitlab::AppLogger).to receive(:info).with(/#{user.name} created a new project/)
+
+ create_project(user, opts)
+ end
end
context "admin creates project with other user's namespace_id" do
@@ -183,9 +190,13 @@ RSpec.describe Projects::CreateService, '#execute' do
user.refresh_authorized_projects # Ensure cache is warm
end
- it do
- project = create_project(user, opts.merge!(namespace_id: group.id))
+ subject(:project) { create_project(user, opts.merge!(namespace_id: group.id)) }
+ shared_examples 'has sync-ed traversal_ids' do
+ specify { expect(project.reload.project_namespace.traversal_ids).to eq([project.namespace.traversal_ids, project.project_namespace.id].flatten.compact) }
+ end
+
+ it 'creates the project' do
expect(project).to be_valid
expect(project.owner).to eq(group)
expect(project.namespace).to eq(group)
@@ -193,6 +204,18 @@ RSpec.describe Projects::CreateService, '#execute' do
expect(user.authorized_projects).to include(project)
expect(project.project_namespace).to be_in_sync_with_project(project)
end
+
+ context 'with before_commit callback' do
+ it_behaves_like 'has sync-ed traversal_ids'
+ end
+
+ context 'with after_create callback' do
+ before do
+ stub_feature_flags(sync_traversal_ids_before_commit: false)
+ end
+
+ it_behaves_like 'has sync-ed traversal_ids'
+ end
end
context 'group sharing', :sidekiq_inline do
@@ -202,7 +225,7 @@ RSpec.describe Projects::CreateService, '#execute' do
let(:opts) do
{
- name: 'GitLab',
+ name: project_name,
namespace_id: shared_group.id
}
end
@@ -237,7 +260,7 @@ RSpec.describe Projects::CreateService, '#execute' do
let(:share_max_access_level) { Gitlab::Access::MAINTAINER }
let(:opts) do
{
- name: 'GitLab',
+ name: project_name,
namespace_id: subgroup_for_projects.id
}
end
@@ -583,58 +606,55 @@ RSpec.describe Projects::CreateService, '#execute' do
opts[:initialize_with_readme] = '1'
end
- shared_examples 'creates README.md' do
+ shared_examples 'a repo with a README.md' do
it { expect(project.repository.commit_count).to be(1) }
it { expect(project.repository.readme.name).to eql('README.md') }
- it { expect(project.repository.readme.data).to include('# GitLab') }
+ it { expect(project.repository.readme.data).to include(expected_content) }
end
- it_behaves_like 'creates README.md'
+ it_behaves_like 'a repo with a README.md' do
+ let(:expected_content) do
+ <<~MARKDOWN
+ cd existing_repo
+ git remote add origin #{project.http_url_to_repo}
+ git branch -M master
+ git push -uf origin master
+ MARKDOWN
+ end
+ end
- context 'and a default_branch_name is specified' do
+ context 'and a readme_template is specified' do
before do
- allow(Gitlab::CurrentSettings)
- .to receive(:default_branch_name)
- .and_return('example_branch')
+ opts[:readme_template] = "# GitLab\nThis is customized readme."
end
- it_behaves_like 'creates README.md'
+ it_behaves_like 'a repo with a README.md' do
+ let(:expected_content) { "# GitLab\nThis is customized readme." }
+ end
+ end
+
+ context 'and a default_branch_name is specified' do
+ before do
+ allow(Gitlab::CurrentSettings).to receive(:default_branch_name).and_return('example_branch')
+ end
- it 'creates README.md within the specified branch rather than master' do
+ it 'creates the correct branch' do
branches = project.repository.branches
expect(branches.size).to eq(1)
expect(branches.collect(&:name)).to contain_exactly('example_branch')
end
- describe 'advanced readme content', experiment: :new_project_readme_content do
- before do
- stub_experiments(new_project_readme_content: :advanced)
- end
-
- it_behaves_like 'creates README.md'
-
- it 'includes advanced content in the README.md' do
- content = project.repository.readme.data
- expect(content).to include <<~MARKDOWN
+ it_behaves_like 'a repo with a README.md' do
+ let(:expected_content) do
+ <<~MARKDOWN
+ cd existing_repo
git remote add origin #{project.http_url_to_repo}
git branch -M example_branch
git push -uf origin example_branch
MARKDOWN
end
end
-
- context 'and readme_template is specified' do
- before do
- opts[:readme_template] = "# GitLab\nThis is customized template."
- end
-
- it_behaves_like 'creates README.md'
-
- it 'creates README.md with specified template' do
- expect(project.repository.readme.data).to include('This is customized template.')
- end
- end
end
end
@@ -676,7 +696,7 @@ RSpec.describe Projects::CreateService, '#execute' do
let(:opts) do
{
- name: 'GitLab',
+ name: project_name,
namespace_id: group.id
}
end
@@ -697,7 +717,7 @@ RSpec.describe Projects::CreateService, '#execute' do
let(:opts) do
{
- name: 'GitLab',
+ name: project_name,
namespace_id: subgroup.id
}
end
@@ -808,7 +828,7 @@ RSpec.describe Projects::CreateService, '#execute' do
let(:opts) do
{
- name: 'GitLab',
+ name: project_name,
namespace_id: group.id
}
end
diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb
index 9475f562d71..d60ec8c2958 100644
--- a/spec/services/projects/destroy_service_spec.rb
+++ b/spec/services/projects/destroy_service_spec.rb
@@ -15,20 +15,39 @@ RSpec.describe Projects::DestroyService, :aggregate_failures do
before do
stub_container_registry_config(enabled: true)
stub_container_registry_tags(repository: :any, tags: [])
+ allow(Gitlab::EventStore).to receive(:publish)
end
shared_examples 'deleting the project' do
- before do
- # Run sidekiq immediately to check that renamed repository will be removed
+ it 'deletes the project', :sidekiq_inline do
destroy_project(project, user, {})
- end
- it 'deletes the project', :sidekiq_inline do
expect(Project.unscoped.all).not_to include(project)
expect(project.gitlab_shell.repository_exists?(project.repository_storage, path + '.git')).to be_falsey
expect(project.gitlab_shell.repository_exists?(project.repository_storage, remove_path + '.git')).to be_falsey
end
+
+ it 'publishes a ProjectDeleted event with project id and namespace id' do
+ expected_data = { project_id: project.id, namespace_id: project.namespace_id }
+ expect(Gitlab::EventStore)
+ .to receive(:publish)
+ .with(event_type(Projects::ProjectDeletedEvent).containing(expected_data))
+
+ destroy_project(project, user, {})
+ end
+
+ context 'when feature flag publish_project_deleted_event is disabled' do
+ before do
+ stub_feature_flags(publish_project_deleted_event: false)
+ end
+
+ it 'does not publish an event' do
+ expect(Gitlab::EventStore).not_to receive(:publish).with(event_type(Projects::ProjectDeletedEvent))
+
+ destroy_project(project, user, {})
+ end
+ end
end
shared_examples 'deleting the project with pipeline and build' do
@@ -97,6 +116,24 @@ RSpec.describe Projects::DestroyService, :aggregate_failures do
end
end
+ context "deleting a project with merge requests" do
+ let!(:merge_request) { create(:merge_request, source_project: project) }
+
+ before do
+ allow(project).to receive(:destroy!).and_return(true)
+ end
+
+ it "deletes merge request and related records" do
+ merge_request_diffs = merge_request.merge_request_diffs
+ expect(merge_request_diffs.size).to eq(1)
+
+ mrdc_count = MergeRequestDiffCommit.where(merge_request_diff_id: merge_request_diffs.first.id).count
+
+ expect { destroy_project(project, user, {}) }
+ .to change { MergeRequestDiffCommit.count }.by(-mrdc_count)
+ end
+ end
+
it_behaves_like 'deleting the project'
it 'invalidates personal_project_count cache' do
diff --git a/spec/services/projects/import_export/export_service_spec.rb b/spec/services/projects/import_export/export_service_spec.rb
index 6002aaf427a..54abbc04084 100644
--- a/spec/services/projects/import_export/export_service_spec.rb
+++ b/spec/services/projects/import_export/export_service_spec.rb
@@ -93,11 +93,23 @@ RSpec.describe Projects::ImportExport::ExportService do
end
it 'saves the project in the file system' do
- expect(Gitlab::ImportExport::Saver).to receive(:save).with(exportable: project, shared: shared)
+ expect(Gitlab::ImportExport::Saver).to receive(:save).with(exportable: project, shared: shared).and_return(true)
service.execute
end
+ context 'when the upload fails' do
+ before do
+ expect(Gitlab::ImportExport::Saver).to receive(:save).with(exportable: project, shared: shared).and_return(false)
+ end
+
+ it 'notifies the user of an error' do
+ expect(service).to receive(:notify_error).and_call_original
+
+ expect { service.execute }.to raise_error(Gitlab::ImportExport::Error)
+ end
+ end
+
it 'calls the after export strategy' do
expect(after_export_strategy).to receive(:execute)
@@ -107,6 +119,7 @@ RSpec.describe Projects::ImportExport::ExportService do
context 'when after export strategy fails' do
before do
allow(after_export_strategy).to receive(:execute).and_return(false)
+ expect(Gitlab::ImportExport::Saver).to receive(:save).with(exportable: project, shared: shared).and_return(true)
end
after do
diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb
index 1d63f72ec38..ccfd119b55b 100644
--- a/spec/services/projects/import_service_spec.rb
+++ b/spec/services/projects/import_service_spec.rb
@@ -298,7 +298,7 @@ RSpec.describe Projects::ImportService do
end
def stub_github_omniauth_provider
- provider = OpenStruct.new(
+ provider = ActiveSupport::InheritableOptions.new(
'name' => 'github',
'app_id' => 'asd123',
'app_secret' => 'asd123',
diff --git a/spec/services/projects/overwrite_project_service_spec.rb b/spec/services/projects/overwrite_project_service_spec.rb
index cc6a863a11d..7038910508f 100644
--- a/spec/services/projects/overwrite_project_service_spec.rb
+++ b/spec/services/projects/overwrite_project_service_spec.rb
@@ -81,16 +81,58 @@ RSpec.describe Projects::OverwriteProjectService do
end
end
- it 'removes the original project' do
- subject.execute(project_from)
+ it 'schedules original project for deletion' do
+ expect_next_instance_of(Projects::DestroyService) do |service|
+ expect(service).to receive(:async_execute)
+ end
- expect { Project.find(project_from.id) }.to raise_error(ActiveRecord::RecordNotFound)
+ subject.execute(project_from)
end
it 'renames the project' do
+ original_path = project_from.full_path
+
subject.execute(project_from)
- expect(project_to.full_path).to eq project_from.full_path
+ expect(project_to.full_path).to eq(original_path)
+ end
+
+ it 'renames source project to temp name' do
+ allow(SecureRandom).to receive(:hex).and_return('test')
+
+ subject.execute(project_from)
+
+ expect(project_from.full_path).to include('-old-test')
+ end
+
+ context 'when project rename fails' do
+ before do
+ expect(subject).to receive(:move_relationships_between).with(project_from, project_to)
+ expect(subject).to receive(:move_relationships_between).with(project_to, project_from)
+ end
+
+ context 'source rename' do
+ it 'moves relations back to source project and raises an exception' do
+ allow(subject).to receive(:rename_project).and_return(status: :error)
+
+ expect { subject.execute(project_from) }.to raise_error(StandardError, 'Source project rename failed during project overwrite')
+ end
+ end
+
+ context 'new project rename' do
+ it 'moves relations back, renames source project back to original name and raises' do
+ name = project_from.name
+ path = project_from.path
+
+ allow(subject).to receive(:rename_project).and_call_original
+ allow(subject).to receive(:rename_project).with(project_to, name, path).and_return(status: :error)
+
+ expect { subject.execute(project_from) }.to raise_error(StandardError, 'New project rename failed during project overwrite')
+
+ expect(project_from.name).to eq(name)
+ expect(project_from.path).to eq(path)
+ end
+ end
end
end
@@ -121,7 +163,7 @@ RSpec.describe Projects::OverwriteProjectService do
end
end
- context 'forks' do
+ context 'forks', :sidekiq_inline do
context 'when moving a root forked project' do
it 'moves the descendant forks' do
expect(project_from.forks.count).to eq 2
@@ -147,6 +189,7 @@ RSpec.describe Projects::OverwriteProjectService do
expect(project_to.fork_network.fork_network_members.map(&:project)).not_to include project_from
end
end
+
context 'when moving a intermediate forked project' do
let(:project_to) { create(:project, namespace: lvl1_forked_project_1.namespace) }
@@ -180,22 +223,26 @@ RSpec.describe Projects::OverwriteProjectService do
end
context 'if an exception is raised' do
+ before do
+ allow(subject).to receive(:rename_project).and_raise(StandardError)
+ end
+
it 'rollbacks changes' do
updated_at = project_from.updated_at
- allow(subject).to receive(:rename_project).and_raise(StandardError)
-
expect { subject.execute(project_from) }.to raise_error(StandardError)
expect(Project.find(project_from.id)).not_to be_nil
expect(project_from.reload.updated_at.change(usec: 0)).to eq updated_at.change(usec: 0)
end
- it 'tries to restore the original project repositories' do
- allow(subject).to receive(:rename_project).and_raise(StandardError)
-
- expect(subject).to receive(:attempt_restore_repositories).with(project_from)
+ it 'removes fork network member' do
+ expect(ForkNetworkMember).to receive(:create!)
+ expect(ForkNetworkMember).to receive(:find_by)
+ expect(subject).to receive(:remove_source_project_from_fork_network).and_call_original
expect { subject.execute(project_from) }.to raise_error(StandardError)
+
+ expect(project_from.fork_network_member).to be_nil
end
end
end
diff --git a/spec/services/projects/readme_renderer_service_spec.rb b/spec/services/projects/readme_renderer_service_spec.rb
new file mode 100644
index 00000000000..14cdcf67640
--- /dev/null
+++ b/spec/services/projects/readme_renderer_service_spec.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::ReadmeRendererService, '#execute' do
+ using RSpec::Parameterized::TableSyntax
+
+ subject(:service) { described_class.new(project, nil, opts) }
+
+ let_it_be(:project) { create(:project, title: 'My Project', description: '_custom_description_') }
+
+ let(:opts) { {} }
+
+ it 'renders the an ERB readme template' do
+ expect(service.execute).to start_with(<<~MARKDOWN)
+ # My Project
+
+ _custom_description_
+
+ ## Getting started
+
+ To make it easy for you to get started with GitLab, here's a list of recommended next steps.
+
+ Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
+
+ ## Add your files
+
+ - [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files
+ - [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command:
+
+ ```
+ cd existing_repo
+ git remote add origin #{project.http_url_to_repo}
+ git branch -M master
+ git push -uf origin master
+ ```
+ MARKDOWN
+ end
+
+ context 'with a custom template' do
+ before do
+ allow(File).to receive(:read).and_call_original
+ end
+
+ it 'renders that template file' do
+ opts[:template_name] = :custom_readme
+
+ expect(service).to receive(:sanitized_filename).with(:custom_readme).and_return('custom_readme.md.tt')
+ expect(File).to receive(:read).with('custom_readme.md.tt').and_return('_custom_readme_file_content_')
+ expect(service.execute).to eq('_custom_readme_file_content_')
+ end
+
+ context 'with path traversal in mind' do
+ where(:template_name, :exception, :expected_path) do
+ '../path/traversal/bad' | [Gitlab::Utils::PathTraversalAttackError, 'Invalid path'] | nil
+ '/bad/template' | [StandardError, 'path /bad/template.md.tt is not allowed'] | nil
+ 'good/template' | nil | 'good/template.md.tt'
+ end
+
+ with_them do
+ it 'raises the expected exception on bad paths' do
+ opts[:template_name] = template_name
+
+ if exception
+ expect { subject.execute }.to raise_error(*exception)
+ else
+ expect(File).to receive(:read).with(described_class::TEMPLATE_PATH.join(expected_path).to_s).and_return('')
+
+ expect { subject.execute }.not_to raise_error
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb
index ddd16100b40..fb94e94fd18 100644
--- a/spec/services/projects/transfer_service_spec.rb
+++ b/spec/services/projects/transfer_service_spec.rb
@@ -5,13 +5,14 @@ require 'spec_helper'
RSpec.describe Projects::TransferService do
include GitHelpers
- let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
+ let_it_be(:user) { create(:user) }
let_it_be(:group_integration) { create(:integrations_slack, :group, group: group, webhook: 'http://group.slack.com') }
let(:project) { create(:project, :repository, :legacy_storage, namespace: user.namespace) }
+ let(:target) { group }
- subject(:execute_transfer) { described_class.new(project, user).execute(group).tap { project.reload } }
+ subject(:execute_transfer) { described_class.new(project, user).execute(target).tap { project.reload } }
context 'with npm packages' do
before do
@@ -690,6 +691,32 @@ RSpec.describe Projects::TransferService do
end
end
+ context 'handling issue contacts' do
+ let_it_be(:root_group) { create(:group) }
+
+ let(:project) { create(:project, group: root_group) }
+
+ before do
+ root_group.add_owner(user)
+ target.add_owner(user)
+ create_list(:issue_customer_relations_contact, 2, :for_issue, issue: create(:issue, project: project))
+ end
+
+ context 'with the same root_ancestor' do
+ let(:target) { create(:group, parent: root_group) }
+
+ it 'retains issue contacts' do
+ expect { execute_transfer }.not_to change { CustomerRelations::IssueContact.count }
+ end
+ end
+
+ context 'with a different root_ancestor' do
+ it 'deletes issue contacts' do
+ expect { execute_transfer }.to change { CustomerRelations::IssueContact.count }.by(-2)
+ end
+ end
+ end
+
def rugged_config
rugged_repo(project.repository).config
end
diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb
index e56e54db6f4..ca3ae437540 100644
--- a/spec/services/quick_actions/interpret_service_spec.rb
+++ b/spec/services/quick_actions/interpret_service_spec.rb
@@ -11,13 +11,14 @@ RSpec.describe QuickActions::InterpretService do
let_it_be(:developer2) { create(:user) }
let_it_be(:developer3) { create(:user) }
let_it_be_with_reload(:issue) { create(:issue, project: project) }
- let(:milestone) { create(:milestone, project: project, title: '9.10') }
- let(:commit) { create(:commit, project: project) }
let_it_be(:inprogress) { create(:label, project: project, title: 'In Progress') }
let_it_be(:helmchart) { create(:label, project: project, title: 'Helm Chart Registry') }
let_it_be(:bug) { create(:label, project: project, title: 'Bug') }
- let(:service) { described_class.new(project, developer) }
+ let(:milestone) { create(:milestone, project: project, title: '9.10') }
+ let(:commit) { create(:commit, project: project) }
+
+ subject(:service) { described_class.new(project, developer) }
before_all do
public_project.add_developer(developer)
@@ -485,6 +486,8 @@ RSpec.describe QuickActions::InterpretService do
end
shared_examples 'failed command' do |error_msg|
+ let(:match_msg) { error_msg ? eq(error_msg) : be_empty }
+
it 'populates {} if content contains an unsupported command' do
_, updates, _ = service.execute(content, issuable)
@@ -494,11 +497,7 @@ RSpec.describe QuickActions::InterpretService do
it "returns #{error_msg || 'an empty'} message" do
_, _, message = service.execute(content, issuable)
- if error_msg
- expect(message).to eq(error_msg)
- else
- expect(message).to be_empty
- end
+ expect(message).to match_msg
end
end
@@ -703,6 +702,27 @@ RSpec.describe QuickActions::InterpretService do
end
end
+ shared_examples 'attention command' do
+ it 'updates reviewers attention status' do
+ _, _, message = service.execute(content, issuable)
+
+ expect(message).to eq("Requested attention from #{developer.to_reference}.")
+
+ reviewer.reload
+
+ expect(reviewer).to be_attention_requested
+ end
+ end
+
+ shared_examples 'remove attention command' do
+ it 'updates reviewers attention status' do
+ _, _, message = service.execute(content, issuable)
+
+ expect(message).to eq("Removed attention from #{developer.to_reference}.")
+ expect(reviewer).not_to be_attention_requested
+ end
+ end
+
it_behaves_like 'reopen command' do
let(:content) { '/reopen' }
let(:issuable) { issue }
@@ -887,9 +907,10 @@ RSpec.describe QuickActions::InterpretService do
end
end
- it_behaves_like 'failed command', "Failed to assign a user because no user was found." do
+ it_behaves_like 'failed command', 'a parse error' do
let(:content) { '/assign @abcd1234' }
let(:issuable) { issue }
+ let(:match_msg) { eq "Could not apply assign command. Failed to find users for '@abcd1234'." }
end
it_behaves_like 'failed command', "Failed to assign a user because no user was found." do
@@ -950,10 +971,38 @@ RSpec.describe QuickActions::InterpretService do
it_behaves_like 'assign_reviewer command'
end
+ context 'with a private user' do
+ let(:ref) { create(:user, :unconfirmed).to_reference }
+ let(:content) { "/assign_reviewer #{ref}" }
+
+ it_behaves_like 'failed command', 'a parse error' do
+ let(:match_msg) { eq "Could not apply assign_reviewer command. Failed to find users for '#{ref}'." }
+ end
+ end
+
+ context 'with a private user, bare username' do
+ let(:ref) { create(:user, :unconfirmed).username }
+ let(:content) { "/assign_reviewer #{ref}" }
+
+ it_behaves_like 'failed command', 'a parse error' do
+ let(:match_msg) { eq "Could not apply assign_reviewer command. Failed to find users for '#{ref}'." }
+ end
+ end
+
+ context 'with @all' do
+ let(:content) { "/assign_reviewer @all" }
+
+ it_behaves_like 'failed command', 'a parse error' do
+ let(:match_msg) { eq "Could not apply assign_reviewer command. Failed to find users for '@all'." }
+ end
+ end
+
context 'with an incorrect user' do
let(:content) { '/assign_reviewer @abcd1234' }
- it_behaves_like 'failed command', "Failed to assign a reviewer because no user was found."
+ it_behaves_like 'failed command', 'a parse error' do
+ let(:match_msg) { eq "Could not apply assign_reviewer command. Failed to find users for '@abcd1234'." }
+ end
end
context 'with the "reviewer" alias' do
@@ -971,13 +1020,15 @@ RSpec.describe QuickActions::InterpretService do
context 'with no user' do
let(:content) { '/assign_reviewer' }
- it_behaves_like 'failed command', "Failed to assign a reviewer because no user was found."
+ it_behaves_like 'failed command', "Failed to assign a reviewer because no user was specified."
end
- context 'includes only the user reference with extra text' do
- let(:content) { "/assign_reviewer @#{developer.username} do it!" }
+ context 'with extra text' do
+ let(:content) { "/assign_reviewer #{developer.to_reference} do it!" }
- it_behaves_like 'assign_reviewer command'
+ it_behaves_like 'failed command', 'a parse error' do
+ let(:match_msg) { eq "Could not apply assign_reviewer command. Failed to find users for 'do' and 'it!'." }
+ end
end
end
@@ -2279,6 +2330,82 @@ RSpec.describe QuickActions::InterpretService do
expect(message).to eq('One or more contacts were successfully removed.')
end
end
+
+ describe 'attention command' do
+ let(:issuable) { create(:merge_request, reviewers: [developer], source_project: project) }
+ let(:reviewer) { issuable.merge_request_reviewers.find_by(user_id: developer.id) }
+ let(:content) { "/attention @#{developer.username}" }
+
+ context 'with one user' do
+ before do
+ reviewer.update!(state: :reviewed)
+ end
+
+ it_behaves_like 'attention command'
+ end
+
+ context 'with no user' do
+ let(:content) { "/attention" }
+
+ it_behaves_like 'failed command', 'Failed to request attention because no user was found.'
+ end
+
+ context 'with incorrect permissions' do
+ let(:service) { described_class.new(project, create(:user)) }
+
+ it_behaves_like 'failed command', 'Could not apply attention command.'
+ end
+
+ context 'with feature flag disabled' do
+ before do
+ stub_feature_flags(mr_attention_requests: false)
+ end
+
+ it_behaves_like 'failed command', 'Could not apply attention command.'
+ end
+
+ context 'with an issue instead of a merge request' do
+ let(:issuable) { issue }
+
+ it_behaves_like 'failed command', 'Could not apply attention command.'
+ end
+ end
+
+ describe 'remove attention command' do
+ let(:issuable) { create(:merge_request, reviewers: [developer], source_project: project) }
+ let(:reviewer) { issuable.merge_request_reviewers.find_by(user_id: developer.id) }
+ let(:content) { "/remove_attention @#{developer.username}" }
+
+ context 'with one user' do
+ it_behaves_like 'remove attention command'
+ end
+
+ context 'with no user' do
+ let(:content) { "/remove_attention" }
+
+ it_behaves_like 'failed command', 'Failed to remove attention because no user was found.'
+ end
+
+ context 'with incorrect permissions' do
+ let(:service) { described_class.new(project, create(:user)) }
+
+ it_behaves_like 'failed command', 'Could not apply remove_attention command.'
+ end
+
+ context 'with feature flag disabled' do
+ before do
+ stub_feature_flags(mr_attention_requests: false)
+ end
+
+ it_behaves_like 'failed command', 'Could not apply remove_attention command.'
+ end
+
+ context 'with an issue instead of a merge request' do
+ let(:issuable) { issue }
+
+ it_behaves_like 'failed command', 'Could not apply remove_attention command.'
+ end
+ end
end
describe '#explain' do
@@ -2317,12 +2444,42 @@ RSpec.describe QuickActions::InterpretService do
end
describe 'assign command' do
- let(:content) { "/assign @#{developer.username} do it!" }
+ shared_examples 'assigns developer' do
+ it 'tells us we will assign the developer' do
+ _, explanations = service.explain(content, merge_request)
- it 'includes only the user reference' do
- _, explanations = service.explain(content, merge_request)
+ expect(explanations).to eq(["Assigns @#{developer.username}."])
+ end
+ end
+
+ context 'when using a reference' do
+ let(:content) { "/assign @#{developer.username}" }
- expect(explanations).to eq(["Assigns @#{developer.username}."])
+ include_examples 'assigns developer'
+ end
+
+ context 'when using a bare username' do
+ let(:content) { "/assign #{developer.username}" }
+
+ include_examples 'assigns developer'
+ end
+
+ context 'when using me' do
+ let(:content) { "/assign me" }
+
+ include_examples 'assigns developer'
+ end
+
+ context 'when there are unparseable arguments' do
+ let(:arg) { "#{developer.username} to this issue" }
+ let(:content) { "/assign #{arg}" }
+
+ it 'tells us why we cannot do that' do
+ _, explanations = service.explain(content, merge_request)
+
+ expect(explanations)
+ .to contain_exactly "Problem with assign command: Failed to find users for 'to', 'this', and 'issue'."
+ end
end
end
@@ -2598,6 +2755,45 @@ RSpec.describe QuickActions::InterpretService do
expect(service.commands_executed_count).to eq(3)
end
end
+
+ describe 'crm commands' do
+ let(:add_contacts) { '/add_contacts' }
+ let(:remove_contacts) { '/remove_contacts' }
+
+ before_all do
+ group.add_developer(developer)
+ end
+
+ context 'when group has no contacts' do
+ it '/add_contacts is not available' do
+ _, explanations = service.explain(add_contacts, issue)
+
+ expect(explanations).to be_empty
+ end
+
+ it '/remove_contacts is not available' do
+ _, explanations = service.explain(remove_contacts, issue)
+
+ expect(explanations).to be_empty
+ end
+ end
+
+ context 'when group has contacts' do
+ let!(:contact) { create(:contact, group: group) }
+
+ it '/add_contacts is available' do
+ _, explanations = service.explain(add_contacts, issue)
+
+ expect(explanations).to contain_exactly("Add customer relation contact(s).")
+ end
+
+ it '/remove_contacts is available' do
+ _, explanations = service.explain(remove_contacts, issue)
+
+ expect(explanations).to contain_exactly("Remove customer relation contact(s).")
+ end
+ end
+ end
end
describe '#available_commands' do
diff --git a/spec/services/releases/create_service_spec.rb b/spec/services/releases/create_service_spec.rb
index 7287825a0be..d53fc968e2a 100644
--- a/spec/services/releases/create_service_spec.rb
+++ b/spec/services/releases/create_service_spec.rb
@@ -146,7 +146,7 @@ RSpec.describe Releases::CreateService do
end
end
- context 'when multiple miletone titles are passed in but one of them does not exist' do
+ context 'when multiple milestone titles are passed in but one of them does not exist' do
let(:title) { 'v1.0' }
let(:inexistent_title) { 'v111.0' }
let!(:milestone) { create(:milestone, :active, project: project, title: title) }
diff --git a/spec/services/security/ci_configuration/container_scanning_create_service_spec.rb b/spec/services/security/ci_configuration/container_scanning_create_service_spec.rb
new file mode 100644
index 00000000000..df76750efc8
--- /dev/null
+++ b/spec/services/security/ci_configuration/container_scanning_create_service_spec.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Security::CiConfiguration::ContainerScanningCreateService, :snowplow do
+ subject(:result) { described_class.new(project, user).execute }
+
+ let(:branch_name) { 'set-container-scanning-config-1' }
+
+ let(:snowplow_event) do
+ {
+ category: 'Security::CiConfiguration::ContainerScanningCreateService',
+ action: 'create',
+ label: ''
+ }
+ end
+
+ include_examples 'services security ci configuration create service', true
+end
diff --git a/spec/services/service_ping/submit_service_ping_service_spec.rb b/spec/services/service_ping/submit_service_ping_service_spec.rb
index 2971c9a9309..73be8f000a9 100644
--- a/spec/services/service_ping/submit_service_ping_service_spec.rb
+++ b/spec/services/service_ping/submit_service_ping_service_spec.rb
@@ -55,7 +55,7 @@ RSpec.describe ServicePing::SubmitService do
shared_examples 'does not run' do
it do
expect(Gitlab::HTTP).not_to receive(:post)
- expect(Gitlab::UsageData).not_to receive(:data)
+ expect(Gitlab::Usage::ServicePingReport).not_to receive(:for)
subject.execute
end
@@ -63,7 +63,7 @@ RSpec.describe ServicePing::SubmitService do
shared_examples 'does not send a blank usage ping payload' do
it do
- expect(Gitlab::HTTP).not_to receive(:post)
+ expect(Gitlab::HTTP).not_to receive(:post).with(subject.url, any_args)
expect { subject.execute }.to raise_error(described_class::SubmissionError) do |error|
expect(error.message).to include('Usage data is blank')
@@ -118,7 +118,7 @@ RSpec.describe ServicePing::SubmitService do
it 'generates service ping' do
stub_response(body: with_dev_ops_score_params)
- expect(Gitlab::UsageData).to receive(:data).with(force_refresh: true).and_call_original
+ expect(Gitlab::Usage::ServicePingReport).to receive(:for).with(output: :all_metrics_values).and_call_original
subject.execute
end
@@ -129,6 +129,7 @@ RSpec.describe ServicePing::SubmitService do
stub_usage_data_connections
stub_database_flavor_check
stub_application_setting(usage_ping_enabled: true)
+ stub_response(body: nil, url: subject.error_url, status: 201)
end
context 'and user requires usage stats consent' do
@@ -150,7 +151,7 @@ RSpec.describe ServicePing::SubmitService do
it 'forces a refresh of usage data statistics before submitting' do
stub_response(body: with_dev_ops_score_params)
- expect(Gitlab::UsageData).to receive(:data).with(force_refresh: true).and_call_original
+ expect(Gitlab::Usage::ServicePingReport).to receive(:for).with(output: :all_metrics_values).and_call_original
subject.execute
end
@@ -166,7 +167,7 @@ RSpec.describe ServicePing::SubmitService do
recorded_at = Time.current
usage_data = { uuid: 'uuid', recorded_at: recorded_at }
- expect(Gitlab::UsageData).to receive(:data).with(force_refresh: true).and_return(usage_data)
+ expect(Gitlab::Usage::ServicePingReport).to receive(:for).with(output: :all_metrics_values).and_return(usage_data)
subject.execute
@@ -189,7 +190,7 @@ RSpec.describe ServicePing::SubmitService do
recorded_at = Time.current
usage_data = { uuid: 'uuid', recorded_at: recorded_at }
- expect(Gitlab::UsageData).to receive(:data).with(force_refresh: true).and_return(usage_data)
+ expect(Gitlab::Usage::ServicePingReport).to receive(:for).with(output: :all_metrics_values).and_return(usage_data)
subject.execute
@@ -234,7 +235,7 @@ RSpec.describe ServicePing::SubmitService do
recorded_at = Time.current
usage_data = { uuid: 'uuid', recorded_at: recorded_at }
- expect(Gitlab::UsageData).to receive(:data).with(force_refresh: true).and_return(usage_data)
+ expect(Gitlab::Usage::ServicePingReport).to receive(:for).with(output: :all_metrics_values).and_return(usage_data)
subject.execute
@@ -259,7 +260,7 @@ RSpec.describe ServicePing::SubmitService do
context 'and usage data is empty string' do
before do
- allow(Gitlab::UsageData).to receive(:data).and_return({})
+ allow(Gitlab::Usage::ServicePingReport).to receive(:for).with(output: :all_metrics_values).and_return({})
end
it_behaves_like 'does not send a blank usage ping payload'
@@ -268,7 +269,7 @@ RSpec.describe ServicePing::SubmitService do
context 'and usage data is nil' do
before do
allow(ServicePing::BuildPayloadService).to receive(:execute).and_return(nil)
- allow(Gitlab::UsageData).to receive(:data).and_return(nil)
+ allow(Gitlab::Usage::ServicePingReport).to receive(:for).with(output: :all_metrics_values).and_return(nil)
end
it_behaves_like 'does not send a blank usage ping payload'
@@ -277,13 +278,23 @@ RSpec.describe ServicePing::SubmitService do
context 'if payload service fails' do
before do
stub_response(body: with_dev_ops_score_params)
- allow(ServicePing::BuildPayloadService).to receive(:execute).and_raise(described_class::SubmissionError, 'SubmissionError')
+ allow(ServicePing::BuildPayloadService).to receive_message_chain(:new, :execute)
+ .and_raise(described_class::SubmissionError, 'SubmissionError')
end
- it 'calls UsageData .data method' do
+ it 'calls Gitlab::Usage::ServicePingReport .for method' do
usage_data = build_usage_data
- expect(Gitlab::UsageData).to receive(:data).and_return(usage_data)
+ expect(Gitlab::Usage::ServicePingReport).to receive(:for).with(output: :all_metrics_values).and_return(usage_data)
+
+ subject.execute
+ end
+
+ it 'submits error' do
+ expect(Gitlab::HTTP).to receive(:post).with(subject.url, any_args)
+ .and_call_original
+ expect(Gitlab::HTTP).to receive(:post).with(subject.error_url, any_args)
+ .and_call_original
subject.execute
end
@@ -315,10 +326,10 @@ RSpec.describe ServicePing::SubmitService do
end
end
- it 'calls UsageData .data method' do
+ it 'calls Gitlab::Usage::ServicePingReport .for method' do
usage_data = build_usage_data
- expect(Gitlab::UsageData).to receive(:data).and_return(usage_data)
+ expect(Gitlab::Usage::ServicePingReport).to receive(:for).with(output: :all_metrics_values).and_return(usage_data)
# SubmissionError is raised as a result of 404 in response from HTTP Request
expect { subject.execute }.to raise_error(described_class::SubmissionError)
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 3ec2c71b20c..a719487a219 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -77,6 +77,19 @@ RSpec.describe SystemNoteService do
end
end
+ describe '.change_issuable_contacts' do
+ let(:added_count) { 5 }
+ let(:removed_count) { 3 }
+
+ it 'calls IssuableService' do
+ expect_next_instance_of(::SystemNotes::IssuablesService) do |service|
+ expect(service).to receive(:change_issuable_contacts).with(added_count, removed_count)
+ end
+
+ described_class.change_issuable_contacts(noteable, project, author, added_count, removed_count)
+ end
+ end
+
describe '.close_after_error_tracking_resolve' do
it 'calls IssuableService' do
expect_next_instance_of(::SystemNotes::IssuablesService) do |service|
@@ -572,12 +585,26 @@ RSpec.describe SystemNoteService do
describe '.change_alert_status' do
let(:alert) { build(:alert_management_alert) }
- it 'calls AlertManagementService' do
- expect_next_instance_of(SystemNotes::AlertManagementService) do |service|
- expect(service).to receive(:change_alert_status).with(alert)
+ context 'with status change reason' do
+ let(:reason) { 'reason for status change' }
+
+ it 'calls AlertManagementService' do
+ expect_next_instance_of(SystemNotes::AlertManagementService) do |service|
+ expect(service).to receive(:change_alert_status).with(reason)
+ end
+
+ described_class.change_alert_status(alert, author, reason)
end
+ end
+
+ context 'without status change reason' do
+ it 'calls AlertManagementService' do
+ expect_next_instance_of(SystemNotes::AlertManagementService) do |service|
+ expect(service).to receive(:change_alert_status).with(nil)
+ end
- described_class.change_alert_status(alert, author)
+ described_class.change_alert_status(alert, author)
+ end
end
end
@@ -618,15 +645,29 @@ RSpec.describe SystemNoteService do
end
end
- describe '.resolve_incident_status' do
- let(:incident) { build(:incident, :closed) }
+ describe '.change_incident_status' do
+ let(:incident) { instance_double('Issue', project: project) }
- it 'calls IncidentService' do
- expect_next_instance_of(SystemNotes::IncidentService) do |service|
- expect(service).to receive(:resolve_incident_status)
+ context 'with status change reason' do
+ let(:reason) { 'reason for status change' }
+
+ it 'calls IncidentService' do
+ expect_next_instance_of(SystemNotes::IncidentService) do |service|
+ expect(service).to receive(:change_incident_status).with(reason)
+ end
+
+ described_class.change_incident_status(incident, author, reason)
end
+ end
- described_class.resolve_incident_status(incident, author)
+ context 'without status change reason' do
+ it 'calls IncidentService' do
+ expect_next_instance_of(SystemNotes::IncidentService) do |service|
+ expect(service).to receive(:change_incident_status).with(nil)
+ end
+
+ described_class.change_incident_status(incident, author)
+ end
end
end
diff --git a/spec/services/system_notes/alert_management_service_spec.rb b/spec/services/system_notes/alert_management_service_spec.rb
index 6e6bfeaa205..039975c1bf6 100644
--- a/spec/services/system_notes/alert_management_service_spec.rb
+++ b/spec/services/system_notes/alert_management_service_spec.rb
@@ -21,14 +21,26 @@ RSpec.describe ::SystemNotes::AlertManagementService do
end
describe '#change_alert_status' do
- subject { described_class.new(noteable: noteable, project: project, author: author).change_alert_status(noteable) }
+ subject { described_class.new(noteable: noteable, project: project, author: author).change_alert_status(reason) }
- it_behaves_like 'a system note' do
- let(:action) { 'status' }
+ context 'with no specified reason' do
+ let(:reason) { nil }
+
+ it_behaves_like 'a system note' do
+ let(:action) { 'status' }
+ end
+
+ it 'has the appropriate message' do
+ expect(subject.note).to eq("changed the status to **Acknowledged**")
+ end
end
- it 'has the appropriate message' do
- expect(subject.note).to eq("changed the status to **Acknowledged**")
+ context 'with reason provided' do
+ let(:reason) { ' by changing incident status' }
+
+ it 'has the appropriate message' do
+ expect(subject.note).to eq("changed the status to **Acknowledged** by changing incident status")
+ end
end
end
@@ -42,21 +54,7 @@ RSpec.describe ::SystemNotes::AlertManagementService do
end
it 'has the appropriate message' do
- expect(subject.note).to eq("created issue #{issue.to_reference(project)} for this alert")
- end
- end
-
- describe '#closed_alert_issue' do
- let_it_be(:issue) { noteable.issue }
-
- subject { described_class.new(noteable: noteable, project: project, author: author).closed_alert_issue(issue) }
-
- it_behaves_like 'a system note' do
- let(:action) { 'status' }
- end
-
- it 'has the appropriate message' do
- expect(subject.note).to eq("changed the status to **Resolved** by closing issue #{issue.to_reference(project)}")
+ expect(subject.note).to eq("created incident #{issue.to_reference(project)} for this alert")
end
end
diff --git a/spec/services/system_notes/incident_service_spec.rb b/spec/services/system_notes/incident_service_spec.rb
index 669e357b7a4..5de352ad8fa 100644
--- a/spec/services/system_notes/incident_service_spec.rb
+++ b/spec/services/system_notes/incident_service_spec.rb
@@ -57,13 +57,27 @@ RSpec.describe ::SystemNotes::IncidentService do
end
end
- describe '#resolve_incident_status' do
- subject(:resolve_incident_status) { described_class.new(noteable: noteable, project: project, author: author).resolve_incident_status }
+ describe '#change_incident_status' do
+ let_it_be(:escalation_status) { create(:incident_management_issuable_escalation_status, issue: noteable) }
- it 'creates a new note about resolved incident', :aggregate_failures do
- expect { resolve_incident_status }.to change { noteable.notes.count }.by(1)
+ let(:service) { described_class.new(noteable: noteable, project: project, author: author) }
- expect(noteable.notes.last.note).to eq('changed the status to **Resolved** by closing the incident')
+ context 'with a provided reason' do
+ subject(:change_incident_status) { service.change_incident_status(' by changing the alert status') }
+
+ it 'creates a new note for an incident status change', :aggregate_failures do
+ expect { change_incident_status }.to change { noteable.notes.count }.by(1)
+ expect(noteable.notes.last.note).to eq("changed the incident status to **Triggered** by changing the alert status")
+ end
+ end
+
+ context 'without provided reason' do
+ subject(:change_incident_status) { service.change_incident_status(nil) }
+
+ it 'creates a new note for an incident status change', :aggregate_failures do
+ expect { change_incident_status }.to change { noteable.notes.count }.by(1)
+ expect(noteable.notes.last.note).to eq("changed the incident status to **Triggered**")
+ end
end
end
end
diff --git a/spec/services/system_notes/issuables_service_spec.rb b/spec/services/system_notes/issuables_service_spec.rb
index 7e53e66303b..e1c97026418 100644
--- a/spec/services/system_notes/issuables_service_spec.rb
+++ b/spec/services/system_notes/issuables_service_spec.rb
@@ -188,6 +188,54 @@ RSpec.describe ::SystemNotes::IssuablesService do
end
end
+ describe '#change_issuable_contacts' do
+ subject { service.change_issuable_contacts(1, 1) }
+
+ let_it_be(:noteable) { create(:issue, project: project) }
+
+ it_behaves_like 'a system note' do
+ let(:action) { 'contact' }
+ end
+
+ def build_note(added_count, removed_count)
+ service.change_issuable_contacts(added_count, removed_count).note
+ end
+
+ it 'builds a correct phrase when one contact is added' do
+ expect(build_note(1, 0)).to eq "added 1 contact"
+ end
+
+ it 'builds a correct phrase when one contact is removed' do
+ expect(build_note(0, 1)).to eq "removed 1 contact"
+ end
+
+ it 'builds a correct phrase when one contact is added and one contact is removed' do
+ expect(build_note(1, 1)).to(
+ eq("added 1 contact and removed 1 contact")
+ )
+ end
+
+ it 'builds a correct phrase when three contacts are added and one removed' do
+ expect(build_note(3, 1)).to(
+ eq("added 3 contacts and removed 1 contact")
+ )
+ end
+
+ it 'builds a correct phrase when three contacts are removed and one added' do
+ expect(build_note(1, 3)).to(
+ eq("added 1 contact and removed 3 contacts")
+ )
+ end
+
+ it 'builds a correct phrase when the locale is different' do
+ Gitlab::I18n.with_locale('pt-BR') do
+ expect(build_note(1, 3)).to(
+ eq("added 1 contact and removed 3 contacts")
+ )
+ end
+ end
+ end
+
describe '#change_status' do
subject { service.change_status(status, source) }
diff --git a/spec/services/test_hooks/project_service_spec.rb b/spec/services/test_hooks/project_service_spec.rb
index cd6284b4a87..d97a6f15270 100644
--- a/spec/services/test_hooks/project_service_spec.rb
+++ b/spec/services/test_hooks/project_service_spec.rb
@@ -37,7 +37,7 @@ RSpec.describe TestHooks::ProjectService do
it 'executes hook' do
allow(Gitlab::DataBuilder::Push).to receive(:build_sample).and_return(sample_data)
- expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result)
+ expect(hook).to receive(:execute).with(sample_data, trigger_key, force: true).and_return(success_result)
expect(service.execute).to include(success_result)
end
end
@@ -49,7 +49,7 @@ RSpec.describe TestHooks::ProjectService do
it 'executes hook' do
allow(Gitlab::DataBuilder::Push).to receive(:build_sample).and_return(sample_data)
- expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result)
+ expect(hook).to receive(:execute).with(sample_data, trigger_key, force: true).and_return(success_result)
expect(service.execute).to include(success_result)
end
end
@@ -69,7 +69,7 @@ RSpec.describe TestHooks::ProjectService do
allow(Gitlab::DataBuilder::Note).to receive(:build).and_return(sample_data)
allow_next(NotesFinder).to receive(:execute).and_return(Note.all)
- expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result)
+ expect(hook).to receive(:execute).with(sample_data, trigger_key, force: true).and_return(success_result)
expect(service.execute).to include(success_result)
end
end
@@ -86,7 +86,7 @@ RSpec.describe TestHooks::ProjectService do
allow(issue).to receive(:to_hook_data).and_return(sample_data)
allow_next(IssuesFinder).to receive(:execute).and_return([issue])
- expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result)
+ expect(hook).to receive(:execute).with(sample_data, trigger_key, force: true).and_return(success_result)
expect(service.execute).to include(success_result)
end
end
@@ -119,7 +119,7 @@ RSpec.describe TestHooks::ProjectService do
allow(merge_request).to receive(:to_hook_data).and_return(sample_data)
allow_next(MergeRequestsFinder).to receive(:execute).and_return([merge_request])
- expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result)
+ expect(hook).to receive(:execute).with(sample_data, trigger_key, force: true).and_return(success_result)
expect(service.execute).to include(success_result)
end
end
@@ -138,7 +138,7 @@ RSpec.describe TestHooks::ProjectService do
allow(Gitlab::DataBuilder::Build).to receive(:build).and_return(sample_data)
allow_next(Ci::JobsFinder).to receive(:execute).and_return([ci_job])
- expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result)
+ expect(hook).to receive(:execute).with(sample_data, trigger_key, force: true).and_return(success_result)
expect(service.execute).to include(success_result)
end
end
@@ -157,7 +157,7 @@ RSpec.describe TestHooks::ProjectService do
allow(Gitlab::DataBuilder::Pipeline).to receive(:build).and_return(sample_data)
allow_next(Ci::PipelinesFinder).to receive(:execute).and_return([pipeline])
- expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result)
+ expect(hook).to receive(:execute).with(sample_data, trigger_key, force: true).and_return(success_result)
expect(service.execute).to include(success_result)
end
end
@@ -184,7 +184,7 @@ RSpec.describe TestHooks::ProjectService do
create(:wiki_page, wiki: project.wiki)
allow(Gitlab::DataBuilder::WikiPage).to receive(:build).and_return(sample_data)
- expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result)
+ expect(hook).to receive(:execute).with(sample_data, trigger_key, force: true).and_return(success_result)
expect(service.execute).to include(success_result)
end
end
@@ -203,7 +203,7 @@ RSpec.describe TestHooks::ProjectService do
allow(release).to receive(:to_hook_data).and_return(sample_data)
allow_next(ReleasesFinder).to receive(:execute).and_return([release])
- expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result)
+ expect(hook).to receive(:execute).with(sample_data, trigger_key, force: true).and_return(success_result)
expect(service.execute).to include(success_result)
end
end
diff --git a/spec/services/test_hooks/system_service_spec.rb b/spec/services/test_hooks/system_service_spec.rb
index 48c8c24212a..66a1218d123 100644
--- a/spec/services/test_hooks/system_service_spec.rb
+++ b/spec/services/test_hooks/system_service_spec.rb
@@ -32,7 +32,7 @@ RSpec.describe TestHooks::SystemService do
it 'executes hook' do
expect(Gitlab::DataBuilder::Push).to receive(:sample_data).and_call_original
- expect(hook).to receive(:execute).with(Gitlab::DataBuilder::Push::SAMPLE_DATA, trigger_key).and_return(success_result)
+ expect(hook).to receive(:execute).with(Gitlab::DataBuilder::Push::SAMPLE_DATA, trigger_key, force: true).and_return(success_result)
expect(service.execute).to include(success_result)
end
end
@@ -45,7 +45,7 @@ RSpec.describe TestHooks::SystemService do
allow(project.repository).to receive(:tags).and_return(['tag'])
expect(Gitlab::DataBuilder::Push).to receive(:sample_data).and_call_original
- expect(hook).to receive(:execute).with(Gitlab::DataBuilder::Push::SAMPLE_DATA, trigger_key).and_return(success_result)
+ expect(hook).to receive(:execute).with(Gitlab::DataBuilder::Push::SAMPLE_DATA, trigger_key, force: true).and_return(success_result)
expect(service.execute).to include(success_result)
end
end
@@ -57,7 +57,7 @@ RSpec.describe TestHooks::SystemService do
it 'executes hook' do
expect(Gitlab::DataBuilder::Repository).to receive(:sample_data).and_call_original
- expect(hook).to receive(:execute).with(Gitlab::DataBuilder::Repository::SAMPLE_DATA, trigger_key).and_return(success_result)
+ expect(hook).to receive(:execute).with(Gitlab::DataBuilder::Repository::SAMPLE_DATA, trigger_key, force: true).and_return(success_result)
expect(service.execute).to include(success_result)
end
end
@@ -76,7 +76,7 @@ RSpec.describe TestHooks::SystemService do
it 'executes hook' do
expect(MergeRequest).to receive(:of_projects).and_return([merge_request])
expect(merge_request).to receive(:to_hook_data).and_return(sample_data)
- expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result)
+ expect(hook).to receive(:execute).with(sample_data, trigger_key, force: true).and_return(success_result)
expect(service.execute).to include(success_result)
end
end
diff --git a/spec/services/update_container_registry_info_service_spec.rb b/spec/services/update_container_registry_info_service_spec.rb
index 740e53b0472..64071e79508 100644
--- a/spec/services/update_container_registry_info_service_spec.rb
+++ b/spec/services/update_container_registry_info_service_spec.rb
@@ -48,6 +48,7 @@ RSpec.describe UpdateContainerRegistryInfoService do
before do
stub_registry_info({})
+ stub_supports_gitlab_api(false)
end
it 'uses a token with no access permissions' do
@@ -63,6 +64,7 @@ RSpec.describe UpdateContainerRegistryInfoService do
context 'when unabled to detect the container registry type' do
it 'sets the application settings to their defaults' do
stub_registry_info({})
+ stub_supports_gitlab_api(false)
subject
@@ -76,20 +78,23 @@ RSpec.describe UpdateContainerRegistryInfoService do
context 'when able to detect the container registry type' do
context 'when using the GitLab container registry' do
it 'updates application settings accordingly' do
- stub_registry_info(vendor: 'gitlab', version: '2.9.1-gitlab', features: %w[a,b,c])
+ stub_registry_info(vendor: 'gitlab', version: '2.9.1-gitlab', features: %w[a b c])
+ stub_supports_gitlab_api(true)
subject
application_settings.reload
expect(application_settings.container_registry_vendor).to eq('gitlab')
expect(application_settings.container_registry_version).to eq('2.9.1-gitlab')
- expect(application_settings.container_registry_features).to eq(%w[a,b,c])
+ expect(application_settings.container_registry_features)
+ .to match_array(%W[a b c #{ContainerRegistry::GitlabApiClient::REGISTRY_GITLAB_V1_API_FEATURE}])
end
end
context 'when using a third-party container registry' do
it 'updates application settings accordingly' do
stub_registry_info(vendor: 'other', version: nil, features: nil)
+ stub_supports_gitlab_api(false)
subject
@@ -112,4 +117,10 @@ RSpec.describe UpdateContainerRegistryInfoService do
allow(client).to receive(:registry_info).and_return(output)
end
end
+
+ def stub_supports_gitlab_api(output)
+ allow_next_instance_of(ContainerRegistry::GitlabApiClient) do |client|
+ allow(client).to receive(:supports_gitlab_api?).and_return(output)
+ end
+ end
end
diff --git a/spec/services/web_hook_service_spec.rb b/spec/services/web_hook_service_spec.rb
index 7d933ea9c5c..64371f97908 100644
--- a/spec/services/web_hook_service_spec.rb
+++ b/spec/services/web_hook_service_spec.rb
@@ -52,6 +52,25 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state
end
end
+ describe '#disabled?' do
+ using RSpec::Parameterized::TableSyntax
+
+ subject { described_class.new(hook, data, :push_hooks, force: forced) }
+
+ let(:hook) { double(executable?: executable, allow_local_requests?: false) }
+
+ where(:forced, :executable, :disabled) do
+ false | true | false
+ false | false | true
+ true | true | false
+ true | false | false
+ end
+
+ with_them do
+ it { is_expected.to have_attributes(disabled?: disabled) }
+ end
+ end
+
describe '#execute' do
let!(:uuid) { SecureRandom.uuid }
let(:headers) do
@@ -129,7 +148,7 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state
end
it 'does not execute disabled hooks' do
- project_hook.update!(recent_failures: 4)
+ allow(service_instance).to receive(:disabled?).and_return(true)
expect(service_instance.execute).to eq({ status: :error, message: 'Hook disabled' })
end
@@ -149,13 +168,13 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state
.once
end
- it 'executes and logs if a recursive web hook is detected', :aggregate_failures do
+ it 'blocks and logs if a recursive web hook is detected', :aggregate_failures do
stub_full_request(project_hook.url, method: :post)
Gitlab::WebHooks::RecursionDetection.register!(project_hook)
expect(Gitlab::AuthLogger).to receive(:error).with(
include(
- message: 'Webhook recursion detected and will be blocked in future',
+ message: 'Recursive webhook blocked from executing',
hook_id: project_hook.id,
hook_type: 'ProjectHook',
hook_name: 'push_hooks',
@@ -166,12 +185,10 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state
service_instance.execute
- expect(WebMock).to have_requested(:post, stubbed_hostname(project_hook.url))
- .with(headers: headers)
- .once
+ expect(WebMock).not_to have_requested(:post, stubbed_hostname(project_hook.url))
end
- it 'executes and logs if the recursion count limit would be exceeded', :aggregate_failures do
+ it 'blocks and logs if the recursion count limit would be exceeded', :aggregate_failures do
stub_full_request(project_hook.url, method: :post)
stub_const("#{Gitlab::WebHooks::RecursionDetection.name}::COUNT_LIMIT", 3)
previous_hooks = create_list(:project_hook, 3)
@@ -179,7 +196,7 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state
expect(Gitlab::AuthLogger).to receive(:error).with(
include(
- message: 'Webhook recursion detected and will be blocked in future',
+ message: 'Recursive webhook blocked from executing',
hook_id: project_hook.id,
hook_type: 'ProjectHook',
hook_name: 'push_hooks',
@@ -190,9 +207,7 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state
service_instance.execute
- expect(WebMock).to have_requested(:post, stubbed_hostname(project_hook.url))
- .with(headers: headers)
- .once
+ expect(WebMock).not_to have_requested(:post, stubbed_hostname(project_hook.url))
end
it 'handles exceptions' do
@@ -255,6 +270,20 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state
stub_full_request(project_hook.url, method: :post).to_return(status: 200, body: 'Success')
end
+ context 'when forced' do
+ let(:service_instance) { described_class.new(project_hook, data, :push_hooks, force: true) }
+
+ it 'logs execution inline' do
+ expect(::WebHooks::LogExecutionWorker).not_to receive(:perform_async)
+ expect(::WebHooks::LogExecutionService)
+ .to receive(:new)
+ .with(hook: project_hook, log_data: Hash, response_category: :ok)
+ .and_return(double(execute: nil))
+
+ run_service
+ end
+ end
+
it 'log successful execution' do
run_service
@@ -408,7 +437,7 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state
describe '#async_execute' do
def expect_to_perform_worker(hook)
- expect(WebHookWorker).to receive(:perform_async).with(hook.id, data, 'push_hooks')
+ expect(WebHookWorker).to receive(:perform_async).with(hook.id, data, 'push_hooks', an_instance_of(Hash))
end
def expect_to_rate_limit(hook, threshold:, throttled: false)
@@ -496,15 +525,15 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state
Gitlab::WebHooks::RecursionDetection.set_request_uuid(SecureRandom.uuid)
end
- it 'queues a worker and logs an error if the call chain limit would be exceeded' do
+ it 'does not queue a worker and logs an error if the call chain limit would be exceeded' do
stub_const("#{Gitlab::WebHooks::RecursionDetection.name}::COUNT_LIMIT", 3)
previous_hooks = create_list(:project_hook, 3)
previous_hooks.each { Gitlab::WebHooks::RecursionDetection.register!(_1) }
- expect(WebHookWorker).to receive(:perform_async)
+ expect(WebHookWorker).not_to receive(:perform_async)
expect(Gitlab::AuthLogger).to receive(:error).with(
include(
- message: 'Webhook recursion detected and will be blocked in future',
+ message: 'Recursive webhook blocked from executing',
hook_id: project_hook.id,
hook_type: 'ProjectHook',
hook_name: 'push_hooks',
@@ -519,13 +548,13 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state
service_instance.async_execute
end
- it 'queues a worker and logs an error if a recursive call chain is detected' do
+ it 'does not queue a worker and logs an error if a recursive call chain is detected' do
Gitlab::WebHooks::RecursionDetection.register!(project_hook)
- expect(WebHookWorker).to receive(:perform_async)
+ expect(WebHookWorker).not_to receive(:perform_async)
expect(Gitlab::AuthLogger).to receive(:error).with(
include(
- message: 'Webhook recursion detected and will be blocked in future',
+ message: 'Recursive webhook blocked from executing',
hook_id: project_hook.id,
hook_type: 'ProjectHook',
hook_name: 'push_hooks',
diff --git a/spec/services/work_items/create_service_spec.rb b/spec/services/work_items/create_service_spec.rb
index 2c054ae59a0..f495e967b26 100644
--- a/spec/services/work_items/create_service_spec.rb
+++ b/spec/services/work_items/create_service_spec.rb
@@ -5,34 +5,46 @@ require 'spec_helper'
RSpec.describe WorkItems::CreateService do
include AfterNextHelpers
- let_it_be(:group) { create(:group) }
- let_it_be_with_reload(:project) { create(:project, group: group) }
- let_it_be(:user) { create(:user) }
+ let_it_be_with_reload(:project) { create(:project) }
+ let_it_be(:guest) { create(:user) }
+ let_it_be(:user_with_no_access) { create(:user) }
let(:spam_params) { double }
+ let(:current_user) { guest }
+ let(:opts) do
+ {
+ title: 'Awesome work_item',
+ description: 'please fix'
+ }
+ end
+
+ before_all do
+ project.add_guest(guest)
+ end
describe '#execute' do
- let(:work_item) { described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute }
+ subject(:service_result) { described_class.new(project: project, current_user: current_user, params: opts, spam_params: spam_params).execute }
before do
stub_spam_services
end
- context 'when params are valid' do
- before_all do
- project.add_guest(user)
- end
+ context 'when user is not allowed to create a work item in the project' do
+ let(:current_user) { user_with_no_access }
+
+ it { is_expected.to be_error }
- let(:opts) do
- {
- title: 'Awesome work_item',
- description: 'please fix'
- }
+ it 'returns an access error' do
+ expect(service_result.errors).to contain_exactly('Operation not allowed')
end
+ end
+ context 'when params are valid' do
it 'created instance is a WorkItem' do
expect(Issuable::CommonSystemNotesService).to receive_message_chain(:new, :execute)
+ work_item = service_result[:work_item]
+
expect(work_item).to be_persisted
expect(work_item).to be_a(::WorkItem)
expect(work_item.title).to eq('Awesome work_item')
@@ -41,17 +53,17 @@ RSpec.describe WorkItems::CreateService do
end
end
- context 'checking spam' do
- let(:params) do
- {
- title: 'Spam work_item'
- }
- end
+ context 'when params are invalid' do
+ let(:opts) { { title: '' } }
- subject do
- described_class.new(project: project, current_user: user, params: params, spam_params: spam_params)
+ it { is_expected.to be_error }
+
+ it 'returns validation errors' do
+ expect(service_result.errors).to contain_exactly("Title can't be blank")
end
+ end
+ context 'checking spam' do
it 'executes SpamActionService' do
expect_next_instance_of(
Spam::SpamActionService,
@@ -65,7 +77,7 @@ RSpec.describe WorkItems::CreateService do
expect(instance).to receive(:execute)
end
- subject.execute
+ service_result
end
end
end
diff --git a/spec/services/work_items/delete_service_spec.rb b/spec/services/work_items/delete_service_spec.rb
new file mode 100644
index 00000000000..6cca5018852
--- /dev/null
+++ b/spec/services/work_items/delete_service_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe WorkItems::DeleteService do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:guest) { create(:user) }
+ let_it_be(:work_item, refind: true) { create(:work_item, project: project, author: guest) }
+
+ let(:user) { guest }
+
+ before_all do
+ project.add_guest(guest)
+ # note necessary to test note removal as part of work item deletion
+ create(:note, project: project, noteable: work_item)
+ end
+
+ describe '#execute' do
+ subject(:result) { described_class.new(project: project, current_user: user).execute(work_item) }
+
+ context 'when user can delete the work item' do
+ it { is_expected.to be_success }
+
+ # currently we don't expect destroy to fail. Mocking here for coverage and keeping
+ # the service's return type consistent
+ context 'when there are errors preventing to delete the work item' do
+ before do
+ allow(work_item).to receive(:destroy).and_return(false)
+ work_item.errors.add(:title)
+ end
+
+ it { is_expected.to be_error }
+
+ it 'returns error messages' do
+ expect(result.errors).to contain_exactly('Title is invalid')
+ end
+ end
+ end
+
+ context 'when user cannot delete the work item' do
+ let(:user) { create(:user) }
+
+ it { is_expected.to be_error }
+
+ it 'returns error messages' do
+ expect(result.errors).to contain_exactly('User not authorized to delete work item')
+ end
+ end
+ end
+end
diff --git a/spec/services/work_items/update_service_spec.rb b/spec/services/work_items/update_service_spec.rb
new file mode 100644
index 00000000000..f71f1060e40
--- /dev/null
+++ b/spec/services/work_items/update_service_spec.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe WorkItems::UpdateService do
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:project) { create(:project).tap { |proj| proj.add_developer(developer) } }
+ let_it_be_with_reload(:work_item) { create(:work_item, project: project, assignees: [developer]) }
+
+ let(:spam_params) { double }
+ let(:opts) { {} }
+ let(:current_user) { developer }
+
+ describe '#execute' do
+ subject(:update_work_item) { described_class.new(project: project, current_user: current_user, params: opts, spam_params: spam_params).execute(work_item) }
+
+ before do
+ stub_spam_services
+ end
+
+ context 'when title is changed' do
+ let(:opts) { { title: 'changed' } }
+
+ it 'triggers issuable_title_updated graphql subscription' do
+ expect(GraphqlTriggers).to receive(:issuable_title_updated).with(work_item).and_call_original
+
+ update_work_item
+ end
+ end
+
+ context 'when title is not changed' do
+ let(:opts) { { description: 'changed' } }
+
+ it 'does not trigger issuable_title_updated graphql subscription' do
+ expect(GraphqlTriggers).not_to receive(:issuable_title_updated)
+
+ update_work_item
+ end
+ end
+
+ context 'when updating state_event' do
+ context 'when state_event is close' do
+ let(:opts) { { state_event: 'close' } }
+
+ it 'closes the work item' do
+ expect do
+ update_work_item
+ work_item.reload
+ end.to change(work_item, :state).from('opened').to('closed')
+ end
+ end
+
+ context 'when state_event is reopen' do
+ let(:opts) { { state_event: 'reopen' } }
+
+ before do
+ work_item.close!
+ end
+
+ it 'reopens the work item' do
+ expect do
+ update_work_item
+ work_item.reload
+ end.to change(work_item, :state).from('closed').to('opened')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 6d5036365e1..37e9ef1d994 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -112,11 +112,20 @@ RSpec.configure do |config|
# falling back to all tests when there is no `:focus` example.
config.filter_run focus: true
config.run_all_when_everything_filtered = true
+ end
- # Re-run failures locally with `--only-failures`
- config.example_status_persistence_file_path = './spec/examples.txt'
+ # Attempt to troubleshoot https://gitlab.com/gitlab-org/gitlab/-/issues/351531
+ config.after do |example|
+ if example.exception.is_a?(Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification::CrossDatabaseModificationAcrossUnsupportedTablesError)
+ ::CrossDatabaseModification::TransactionStackTrackRecord.log_gitlab_transactions_stack(action: :after_failure, example: example.description)
+ else
+ ::CrossDatabaseModification::TransactionStackTrackRecord.log_gitlab_transactions_stack(action: :after_example, example: example.description)
+ end
end
+ # Re-run failures locally with `--only-failures`
+ config.example_status_persistence_file_path = ENV.fetch('RSPEC_LAST_RUN_RESULTS_FILE', './spec/examples.txt')
+
config.define_derived_metadata(file_path: %r{(ee)?/spec/.+_spec\.rb\z}) do |metadata|
location = metadata[:location]
@@ -184,7 +193,6 @@ RSpec.configure do |config|
config.include RedisHelpers
config.include Rails.application.routes.url_helpers, type: :routing
config.include PolicyHelpers, type: :policy
- config.include MemoryUsageHelper
config.include ExpectRequestWithStatus, type: :request
config.include IdempotentWorkerHelper, type: :worker
config.include RailsHelpers
@@ -208,7 +216,9 @@ RSpec.configure do |config|
config.exceptions_to_hard_fail = [DeprecationToolkitEnv::DeprecationBehaviors::SelectiveRaise::RaiseDisallowedDeprecation]
end
- if ENV['FLAKY_RSPEC_GENERATE_REPORT']
+ require_relative '../tooling/rspec_flaky/config'
+
+ if RspecFlaky::Config.generate_report?
require_relative '../tooling/rspec_flaky/listener'
config.reporter.register_listener(
@@ -242,10 +252,6 @@ RSpec.configure do |config|
::Ci::ApplicationRecord.set_open_transactions_baseline
end
- config.append_before do
- Thread.current[:current_example_group] = ::RSpec.current_example.metadata[:example_group]
- end
-
config.append_after do
ApplicationRecord.reset_open_transactions_baseline
::Ci::ApplicationRecord.reset_open_transactions_baseline
@@ -288,8 +294,6 @@ RSpec.configure do |config|
# See https://gitlab.com/gitlab-org/gitlab/-/issues/33867
stub_feature_flags(file_identifier_hash: false)
- stub_feature_flags(diffs_virtual_scrolling: false)
-
# The following `vue_issues_list` stub can be removed
# once the Vue issues page has feature parity with the current Haml page
stub_feature_flags(vue_issues_list: false)
@@ -462,14 +466,6 @@ RSpec.configure do |config|
$stdout = STDOUT
end
- config.around(:each, stubbing_settings_source: true) do |example|
- original_instance = ::Settings.instance_variable_get(:@instance)
-
- example.run
-
- ::Settings.instance_variable_set(:@instance, original_instance)
- end
-
config.disable_monkey_patching!
end
diff --git a/spec/support/cross_database_modification.rb b/spec/support/cross_database_modification.rb
new file mode 100644
index 00000000000..e0d91001c03
--- /dev/null
+++ b/spec/support/cross_database_modification.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+RSpec.configure do |config|
+ config.after do |example|
+ [::ApplicationRecord, ::Ci::ApplicationRecord].each do |base_class|
+ base_class.gitlab_transactions_stack.clear if base_class.respond_to?(:gitlab_transactions_stack)
+ end
+ end
+end
diff --git a/spec/support/db_cleaner.rb b/spec/support/db_cleaner.rb
index fb70f82ef87..e3a05f17593 100644
--- a/spec/support/db_cleaner.rb
+++ b/spec/support/db_cleaner.rb
@@ -73,6 +73,8 @@ module DbCleaner
end
end
+ Gitlab::Database::Partitioning.sync_partitions_ignore_db_error
+
puts "Databases re-creation done in #{Gitlab::Metrics::System.monotonic_time - start}"
end
diff --git a/spec/support/flaky_tests.rb b/spec/support/flaky_tests.rb
index 5ce55c47aab..4df0d23bfc3 100644
--- a/spec/support/flaky_tests.rb
+++ b/spec/support/flaky_tests.rb
@@ -4,14 +4,14 @@ return unless ENV['CI']
return if ENV['SKIP_FLAKY_TESTS_AUTOMATICALLY'] == "false"
return if ENV['CI_MERGE_REQUEST_LABELS'].to_s.include?('pipeline:run-flaky-tests')
+require_relative '../../tooling/rspec_flaky/config'
require_relative '../../tooling/rspec_flaky/report'
RSpec.configure do |config|
$flaky_test_example_ids = begin # rubocop:disable Style/GlobalVars
- raise "$SUITE_FLAKY_RSPEC_REPORT_PATH is empty." if ENV['SUITE_FLAKY_RSPEC_REPORT_PATH'].to_s.empty?
- raise "#{ENV['SUITE_FLAKY_RSPEC_REPORT_PATH']} doesn't exist" unless File.exist?(ENV['SUITE_FLAKY_RSPEC_REPORT_PATH'])
+ raise "#{RspecFlaky::Config.suite_flaky_examples_report_path} doesn't exist" unless File.exist?(RspecFlaky::Config.suite_flaky_examples_report_path)
- RspecFlaky::Report.load(ENV['SUITE_FLAKY_RSPEC_REPORT_PATH']).map { |_, flaky_test_data| flaky_test_data.to_h[:example_id] }
+ RspecFlaky::Report.load(RspecFlaky::Config.suite_flaky_examples_report_path).map { |_, flaky_test_data| flaky_test_data.to_h[:example_id] }
rescue => e # rubocop:disable Style/RescueStandardError
puts e
[]
@@ -29,8 +29,9 @@ RSpec.configure do |config|
end
config.after(:suite) do
- next unless ENV['SKIPPED_FLAKY_TESTS_REPORT_PATH']
+ next unless RspecFlaky::Config.skipped_flaky_tests_report_path
+ next if $skipped_flaky_tests_report.empty? # rubocop:disable Style/GlobalVars
- File.write(ENV['SKIPPED_FLAKY_TESTS_REPORT_PATH'], "#{$skipped_flaky_tests_report.join("\n")}\n") # rubocop:disable Style/GlobalVars
+ File.write(RspecFlaky::Config.skipped_flaky_tests_report_path, "#{ENV['CI_JOB_URL']}\n#{$skipped_flaky_tests_report.join("\n")}\n\n") # rubocop:disable Style/GlobalVars
end
end
diff --git a/spec/support/gitlab_experiment.rb b/spec/support/gitlab_experiment.rb
index 3d099dc689c..823aab0436e 100644
--- a/spec/support/gitlab_experiment.rb
+++ b/spec/support/gitlab_experiment.rb
@@ -10,6 +10,16 @@ RSpec.configure do |config|
# Disable all caching for experiments in tests.
config.before do
allow(Gitlab::Experiment::Configuration).to receive(:cache).and_return(nil)
+
+ # Disable all deprecation warnings in the test environment, which can be
+ # resolved one by one and tracked in:
+ #
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/350944
+ allow(Gitlab::Experiment::Configuration).to receive(:deprecator).and_wrap_original do |method, version|
+ method.call(version).tap do |deprecator|
+ deprecator.silenced = true
+ end
+ end
end
config.before(:each, :experiment) do
diff --git a/spec/support/graphql/arguments.rb b/spec/support/graphql/arguments.rb
index d8c334c2ca4..20e940030f8 100644
--- a/spec/support/graphql/arguments.rb
+++ b/spec/support/graphql/arguments.rb
@@ -41,6 +41,7 @@ module Graphql
when Hash then "{#{new(value)}}"
when Integer, Float, Symbol then value.to_s
when String then "\"#{value.gsub(/"/, '\\"')}\""
+ when Time, Date then "\"#{value.iso8601}\""
when nil then 'null'
when true then 'true'
when false then 'false'
diff --git a/spec/support/helpers/fake_blob_helpers.rb b/spec/support/helpers/fake_blob_helpers.rb
index 6c8866deac4..47fb9a345a3 100644
--- a/spec/support/helpers/fake_blob_helpers.rb
+++ b/spec/support/helpers/fake_blob_helpers.rb
@@ -4,13 +4,14 @@ module FakeBlobHelpers
class FakeBlob
include BlobLike
- attr_reader :path, :size, :data, :lfs_oid, :lfs_size
+ attr_reader :path, :size, :data, :lfs_oid, :lfs_size, :mode
- def initialize(path: 'file.txt', size: 1.kilobyte, data: 'foo', binary: false, lfs: nil)
+ def initialize(path: 'file.txt', size: 1.kilobyte, data: 'foo', binary: false, lfs: nil, mode: nil)
@path = path
@size = size
@data = data
@binary = binary
+ @mode = mode
@lfs_pointer = lfs.present?
if @lfs_pointer
diff --git a/spec/support/helpers/features/invite_members_modal_helper.rb b/spec/support/helpers/features/invite_members_modal_helper.rb
index 11040562b49..2a4f78ca57f 100644
--- a/spec/support/helpers/features/invite_members_modal_helper.rb
+++ b/spec/support/helpers/features/invite_members_modal_helper.rb
@@ -8,7 +8,7 @@ module Spec
def invite_member(name, role: 'Guest', expires_at: nil)
click_on 'Invite members'
- page.within '[data-testid="invite-members-modal"]' do
+ page.within '[data-testid="invite-modal"]' do
find('[data-testid="members-token-select-input"]').set(name)
wait_for_requests
diff --git a/spec/support/helpers/features/iteration_helpers.rb b/spec/support/helpers/features/iteration_helpers.rb
new file mode 100644
index 00000000000..8e1d252f55f
--- /dev/null
+++ b/spec/support/helpers/features/iteration_helpers.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+module IterationHelpers
+ def iteration_period(iteration)
+ "#{iteration.start_date.to_s(:medium)} - #{iteration.due_date.to_s(:medium)}"
+ end
+end
diff --git a/spec/support/helpers/gitaly_setup.rb b/spec/support/helpers/gitaly_setup.rb
index 905c439f4d9..a4ee618457d 100644
--- a/spec/support/helpers/gitaly_setup.rb
+++ b/spec/support/helpers/gitaly_setup.rb
@@ -120,7 +120,7 @@ module GitalySetup
end
def build_gitaly
- run_command(%w[make all git], env: env.merge('GIT_VERSION' => nil))
+ run_command(%w[make all WITH_BUNDLED_GIT=YesPlease], env: env.merge('GIT_VERSION' => nil))
end
def start_gitaly(toml = nil)
@@ -327,8 +327,8 @@ module GitalySetup
message += "\nCheck log/gitaly-test.log for errors.\n"
- unless ci?
- message += "\nIf binaries are missing, try running `make -C tmp/tests/gitaly build git.`\n"
+ unless ENV['CI']
+ message += "\nIf binaries are missing, try running `make -C tmp/tests/gitaly all WITH_BUNDLED_GIT=YesPlease`.\n"
message += "\nOtherwise, try running `rm -rf #{tmp_tests_gitaly_dir}`."
end
@@ -336,7 +336,7 @@ module GitalySetup
end
def git_binary
- File.join(tmp_tests_gitaly_dir, "_build", "deps", "git", "install", "bin", "git")
+ File.join(tmp_tests_gitaly_dir, "_build", "bin", "gitaly-git")
end
def gitaly_binary
diff --git a/spec/support/helpers/import_spec_helper.rb b/spec/support/helpers/import_spec_helper.rb
index d8fb2ba08af..26b78acbc44 100644
--- a/spec/support/helpers/import_spec_helper.rb
+++ b/spec/support/helpers/import_spec_helper.rb
@@ -25,7 +25,7 @@ module ImportSpecHelper
end
def stub_omniauth_provider(name)
- provider = OpenStruct.new(
+ provider = ActiveSupport::InheritableOptions.new(
name: name,
app_id: 'asd123',
app_secret: 'asd123'
diff --git a/spec/support/helpers/key_generator_helper.rb b/spec/support/helpers/key_generator_helper.rb
deleted file mode 100644
index 58bde80a31f..00000000000
--- a/spec/support/helpers/key_generator_helper.rb
+++ /dev/null
@@ -1,44 +0,0 @@
-# frozen_string_literal: true
-
-module Spec
- module Support
- module Helpers
- class KeyGeneratorHelper
- # The components in a openssh .pub / known_host RSA public key.
- RSA_COMPONENTS = ['ssh-rsa', :e, :n].freeze
-
- attr_reader :size
-
- def initialize(size = 2048)
- @size = size
- end
-
- def generate
- key = OpenSSL::PKey::RSA.generate(size)
- components = RSA_COMPONENTS.map do |component|
- key.respond_to?(component) ? encode_mpi(key.public_send(component)) : component
- end
-
- # Ruby tries to be helpful and adds new lines every 60 bytes :(
- 'ssh-rsa ' + [pack_pubkey_components(components)].pack('m').delete("\n")
- end
-
- private
-
- # Encodes an openssh-mpi-encoded integer.
- def encode_mpi(n) # rubocop:disable Naming/UncommunicativeMethodParamName
- chars = []
- n = n.to_i
- chars << (n & 0xff) && n >>= 8 while n != 0
- chars << 0 if chars.empty? || chars.last >= 0x80
- chars.reverse.pack('C*')
- end
-
- # Packs string components into an openssh-encoded pubkey.
- def pack_pubkey_components(strings)
- (strings.flat_map { |s| [s.length].pack('N') }).zip(strings).join
- end
- end
- end
- end
-end
diff --git a/spec/support/helpers/login_helpers.rb b/spec/support/helpers/login_helpers.rb
index 4e0e8dd96ee..c0734bae375 100644
--- a/spec/support/helpers/login_helpers.rb
+++ b/spec/support/helpers/login_helpers.rb
@@ -178,7 +178,7 @@ module LoginHelpers
end
def mock_saml_config
- OpenStruct.new(name: 'saml', label: 'saml', args: {
+ ActiveSupport::InheritableOptions.new(name: 'saml', label: 'saml', args: {
assertion_consumer_service_url: 'https://localhost:3443/users/auth/saml/callback',
idp_cert_fingerprint: '26:43:2C:47:AF:F0:6B:D0:07:9C:AD:A3:74:FE:5D:94:5F:4E:9E:52',
idp_sso_target_url: 'https://idp.example.com/sso/saml',
diff --git a/spec/support/helpers/memory_usage_helper.rb b/spec/support/helpers/memory_usage_helper.rb
deleted file mode 100644
index 02d1935921f..00000000000
--- a/spec/support/helpers/memory_usage_helper.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-# frozen_string_literal: true
-
-module MemoryUsageHelper
- extend ActiveSupport::Concern
-
- def gather_memory_data(csv_path)
- write_csv_entry(csv_path,
- {
- example_group_path: TestEnv.topmost_example_group[:location],
- example_group_description: TestEnv.topmost_example_group[:description],
- time: Time.current,
- job_name: ENV['CI_JOB_NAME']
- }.merge(get_memory_usage))
- end
-
- def write_csv_entry(path, entry)
- CSV.open(path, "a", headers: entry.keys, write_headers: !File.exist?(path)) do |file|
- file << entry.values
- end
- end
-
- def get_memory_usage
- output, status = Gitlab::Popen.popen(%w(free -m))
- abort "`free -m` return code is #{status}: #{output}" unless status == 0
-
- result = output.split("\n")[1].split(" ")[1..]
- attrs = %i(m_total m_used m_free m_shared m_buffers_cache m_available).freeze
-
- attrs.zip(result).to_h
- end
-
- included do |config|
- config.after(:all) do
- gather_memory_data(ENV['MEMORY_TEST_PATH']) if ENV['MEMORY_TEST_PATH']
- end
- end
-end
diff --git a/spec/support/helpers/merge_request_diff_helpers.rb b/spec/support/helpers/merge_request_diff_helpers.rb
index 30afde7efed..7515c789add 100644
--- a/spec/support/helpers/merge_request_diff_helpers.rb
+++ b/spec/support/helpers/merge_request_diff_helpers.rb
@@ -1,8 +1,11 @@
# frozen_string_literal: true
module MergeRequestDiffHelpers
+ PageEndReached = Class.new(StandardError)
+
def click_diff_line(line_holder, diff_side = nil)
line = get_line_components(line_holder, diff_side)
+ scroll_to_elements_bottom(line_holder)
line_holder.hover
line[:num].find('.js-add-diff-note-button').click
end
@@ -27,4 +30,55 @@ module MergeRequestDiffHelpers
line_holder.find('.diff-line-num', match: :first)
{ content: line_holder.all('.line_content')[side_index], num: line_holder.all('.diff-line-num')[side_index] }
end
+
+ def has_reached_page_end
+ evaluate_script("(window.innerHeight + window.scrollY) >= document.body.offsetHeight")
+ end
+
+ def scroll_to_elements_bottom(element)
+ evaluate_script("(function(el) {
+ window.scrollBy(0, el.getBoundingClientRect().bottom - window.innerHeight);
+ })(arguments[0]);", element.native)
+ end
+
+ # we're not using Capybara's .obscured here because it also checks if the element is clickable
+ def within_viewport?(element)
+ evaluate_script("(function(el) {
+ var rect = el.getBoundingClientRect();
+ return (
+ rect.bottom >= 0 &&
+ rect.right >= 0 &&
+ rect.top <= (window.innerHeight || document.documentElement.clientHeight) &&
+ rect.left <= (window.innerWidth || document.documentElement.clientWidth)
+ );
+ })(arguments[0]);", element.native)
+ end
+
+ def find_within_viewport(selector, **options)
+ begin
+ element = find(selector, **options, wait: 2)
+ rescue Capybara::ElementNotFound
+ return
+ end
+ return element if within_viewport?(element)
+
+ nil
+ end
+
+ def find_by_scrolling(selector, **options)
+ element = find_within_viewport(selector, **options)
+ return element if element
+
+ page.execute_script "window.scrollTo(0,0)"
+ until element
+
+ if has_reached_page_end
+ raise PageEndReached, "Failed to find any elements matching a selector '#{selector}' by scrolling. Page end reached."
+ end
+
+ page.execute_script "window.scrollBy(0,window.innerHeight/1.5)"
+ element = find_within_viewport(selector, **options)
+ end
+ element
+ end
end
diff --git a/spec/support/helpers/note_interaction_helpers.rb b/spec/support/helpers/note_interaction_helpers.rb
index a4322618cd3..fa2705a64fa 100644
--- a/spec/support/helpers/note_interaction_helpers.rb
+++ b/spec/support/helpers/note_interaction_helpers.rb
@@ -1,8 +1,10 @@
# frozen_string_literal: true
module NoteInteractionHelpers
+ include MergeRequestDiffHelpers
+
def open_more_actions_dropdown(note)
- note_element = find("#note_#{note.id}")
+ note_element = find_by_scrolling("#note_#{note.id}")
note_element.find('.more-actions-toggle').click
note_element.find('.more-actions .dropdown-menu li', match: :first)
diff --git a/spec/support/helpers/rack_attack_spec_helpers.rb b/spec/support/helpers/rack_attack_spec_helpers.rb
index d50a6382a40..c82a87dc58e 100644
--- a/spec/support/helpers/rack_attack_spec_helpers.rb
+++ b/spec/support/helpers/rack_attack_spec_helpers.rb
@@ -26,14 +26,14 @@ module RackAttackSpecHelpers
{ 'AUTHORIZATION' => "Basic #{encoded_login}" }
end
- def expect_rejection(&block)
+ def expect_rejection(name = nil, &block)
yield
expect(response).to have_gitlab_http_status(:too_many_requests)
expect(response.headers.to_h).to include(
'RateLimit-Limit' => a_string_matching(/^\d+$/),
- 'RateLimit-Name' => a_string_matching(/^throttle_.*$/),
+ 'RateLimit-Name' => name || a_string_matching(/^throttle_.*$/),
'RateLimit-Observed' => a_string_matching(/^\d+$/),
'RateLimit-Remaining' => a_string_matching(/^\d+$/),
'Retry-After' => a_string_matching(/^\d+$/)
diff --git a/spec/support/helpers/repo_helpers.rb b/spec/support/helpers/repo_helpers.rb
index bbba58d60d6..f275be39dc4 100644
--- a/spec/support/helpers/repo_helpers.rb
+++ b/spec/support/helpers/repo_helpers.rb
@@ -129,7 +129,7 @@ eos
commit_message: 'Add new content')
Files::CreateService.new(
project,
- project.owner,
+ project.first_owner,
commit_message: commit_message,
start_branch: start_branch,
branch_name: branch_name,
diff --git a/spec/support/helpers/session_helpers.rb b/spec/support/helpers/session_helpers.rb
index 236585296e5..394a401afca 100644
--- a/spec/support/helpers/session_helpers.rb
+++ b/spec/support/helpers/session_helpers.rb
@@ -1,6 +1,22 @@
# frozen_string_literal: true
module SessionHelpers
+ # Stub a session in Redis, for use in request specs where we can't mock the session directly.
+ # This also needs the :clean_gitlab_redis_sessions tag on the spec.
+ def stub_session(session_hash)
+ unless RSpec.current_example.metadata[:clean_gitlab_redis_sessions]
+ raise 'Add :clean_gitlab_redis_sessions to your spec!'
+ end
+
+ session_id = Rack::Session::SessionId.new(SecureRandom.hex)
+
+ Gitlab::Redis::Sessions.with do |redis|
+ redis.set("session:gitlab:#{session_id.private_id}", Marshal.dump(session_hash))
+ end
+
+ cookies[Gitlab::Application.config.session_options[:key]] = session_id.public_id
+ end
+
def expect_single_session_with_authenticated_ttl
expect_single_session_with_expiration(Settings.gitlab['session_expire_delay'] * 60)
end
diff --git a/spec/support/helpers/stub_gitlab_calls.rb b/spec/support/helpers/stub_gitlab_calls.rb
index c3459f7bc81..749554f7786 100644
--- a/spec/support/helpers/stub_gitlab_calls.rb
+++ b/spec/support/helpers/stub_gitlab_calls.rb
@@ -51,6 +51,8 @@ module StubGitlabCalls
allow(Gitlab.config.registry).to receive_messages(registry_settings)
allow(Auth::ContainerRegistryAuthenticationService)
.to receive(:full_access_token).and_return('token')
+ allow(Auth::ContainerRegistryAuthenticationService)
+ .to receive(:import_access_token).and_return('token')
end
def stub_container_registry_tags(repository: :any, tags: [], with_manifest: false)
diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb
index 5c3ca92c4d0..18c25f4b770 100644
--- a/spec/support/helpers/test_env.rb
+++ b/spec/support/helpers/test_env.rb
@@ -371,17 +371,6 @@ module TestEnv
FileUtils.rm_rf(path)
end
- def current_example_group
- Thread.current[:current_example_group]
- end
-
- # looking for a top-level `describe`
- def topmost_example_group
- example_group = current_example_group
- example_group = example_group[:parent_example_group] until example_group[:parent_example_group].nil?
- example_group
- end
-
def seed_db
Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter.import
end
diff --git a/spec/support/import_export/import_export.yml b/spec/support/import_export/import_export.yml
index 116bc8d0b9c..fa10e1335c5 100644
--- a/spec/support/import_export/import_export.yml
+++ b/spec/support/import_export/import_export.yml
@@ -28,4 +28,4 @@ excluded_attributes:
- :iid
project:
- :id
- - :created_at \ No newline at end of file
+ - :created_at
diff --git a/spec/support/matchers/event_store.rb b/spec/support/matchers/event_store.rb
new file mode 100644
index 00000000000..96a71ae3c22
--- /dev/null
+++ b/spec/support/matchers/event_store.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+RSpec::Matchers.define :event_type do |event_class|
+ match do |actual|
+ actual.instance_of?(event_class) &&
+ actual.data == @expected_data
+ end
+
+ chain :containing do |expected_data|
+ @expected_data = expected_data
+ end
+end
diff --git a/spec/support/matchers/schema_matcher.rb b/spec/support/matchers/schema_matcher.rb
index 5e08e96f4e1..d2f32b60464 100644
--- a/spec/support/matchers/schema_matcher.rb
+++ b/spec/support/matchers/schema_matcher.rb
@@ -36,12 +36,38 @@ end
RSpec::Matchers.define :match_response_schema do |schema, dir: nil, **options|
match do |response|
- schema_path = Pathname.new(SchemaPath.expand(schema, dir))
- validator = SchemaPath.validator(schema_path)
+ @schema_path = Pathname.new(SchemaPath.expand(schema, dir))
+ validator = SchemaPath.validator(@schema_path)
- data = Gitlab::Json.parse(response.body)
+ @data = Gitlab::Json.parse(response.body)
- validator.valid?(data)
+ @schema_errors = validator.validate(@data)
+ @schema_errors.none?
+ end
+
+ failure_message do |actual|
+ message = []
+
+ message << <<~MESSAGE
+ expected JSON response to match schema #{@schema_path.inspect}.
+
+ JSON input: #{Gitlab::Json.pretty_generate(@data).indent(2)}
+
+ Schema errors:
+ MESSAGE
+
+ @schema_errors.each do |error|
+ property_name, actual_value = error.values_at('data_pointer', 'data')
+ property_name = 'root' if property_name.empty?
+
+ message << <<~MESSAGE
+ Property: #{property_name}
+ Actual value: #{Gitlab::Json.pretty_generate(actual_value).indent(2)}
+ Error: #{JSONSchemer::Errors.pretty(error)}
+ MESSAGE
+ end
+
+ message.join("\n")
end
end
diff --git a/spec/support/shared_contexts/container_repositories_shared_context.rb b/spec/support/shared_contexts/container_repositories_shared_context.rb
new file mode 100644
index 00000000000..7f61631dce0
--- /dev/null
+++ b/spec/support/shared_contexts/container_repositories_shared_context.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'importable repositories' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:valid_container_repository) { create(:container_repository, project: project, created_at: 2.days.ago) }
+ let_it_be(:valid_container_repository2) { create(:container_repository, project: project, created_at: 1.year.ago) }
+ let_it_be(:importing_container_repository) { create(:container_repository, :importing, project: project, created_at: 2.days.ago) }
+ let_it_be(:new_container_repository) { create(:container_repository, project: project) }
+
+ let_it_be(:denied_group) { create(:group) }
+ let_it_be(:denied_project) { create(:project, group: denied_group) }
+ let_it_be(:denied_container_repository) { create(:container_repository, project: denied_project, created_at: 2.days.ago) }
+
+ before do
+ stub_application_setting(container_registry_import_created_before: 1.day.ago)
+ stub_feature_flags(
+ container_registry_phase_2_deny_list: false,
+ container_registry_migration_limit_gitlab_org: false
+ )
+
+ Feature::FlipperGate.create!(
+ feature_key: 'container_registry_phase_2_deny_list',
+ key: 'actors',
+ value: "Group:#{denied_group.id}"
+ )
+ end
+end
diff --git a/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb b/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb
index 3d2b0433b21..3ea6658c0c1 100644
--- a/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb
+++ b/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb
@@ -18,6 +18,8 @@ Integration.available_integration_names.each do |integration|
hash.merge!(k => 'https://example.atlassian.net/wiki')
elsif integration == 'datadog' && k == :datadog_site
hash.merge!(k => 'datadoghq.com')
+ elsif integration == 'datadog' && k == :datadog_tags
+ hash.merge!(k => 'key:value')
elsif integration == 'packagist' && k == :server
hash.merge!(k => 'https://packagist.example.com')
elsif k =~ /^(.*_url|url|webhook)/
diff --git a/spec/support/shared_contexts/features/integrations/project_integrations_jira_context.rb b/spec/support/shared_contexts/features/integrations/project_integrations_jira_context.rb
index 54bb9fd108e..fadd46a7e12 100644
--- a/spec/support/shared_contexts/features/integrations/project_integrations_jira_context.rb
+++ b/spec/support/shared_contexts/features/integrations/project_integrations_jira_context.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-RSpec.shared_context 'project service Jira context' do
+RSpec.shared_context 'project integration Jira context' do
let(:url) { 'https://jira.example.com' }
let(:test_url) { 'https://jira.example.com/rest/api/2/serverInfo' }
diff --git a/spec/support/shared_contexts/features/integrations/project_integrations_shared_context.rb b/spec/support/shared_contexts/features/integrations/project_integrations_shared_context.rb
index 6414a4d1eb3..bac7bd00f46 100644
--- a/spec/support/shared_contexts/features/integrations/project_integrations_shared_context.rb
+++ b/spec/support/shared_contexts/features/integrations/project_integrations_shared_context.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-RSpec.shared_context 'project service activation' do
+RSpec.shared_context 'project integration activation' do
include_context 'integration activation'
let_it_be(:project) { create(:project) }
diff --git a/spec/support/shared_contexts/finders/group_projects_finder_shared_contexts.rb b/spec/support/shared_contexts/finders/group_projects_finder_shared_contexts.rb
index 6b15eadc1c1..3479dac0077 100644
--- a/spec/support/shared_contexts/finders/group_projects_finder_shared_contexts.rb
+++ b/spec/support/shared_contexts/finders/group_projects_finder_shared_contexts.rb
@@ -1,7 +1,8 @@
# frozen_string_literal: true
RSpec.shared_context 'GroupProjectsFinder context' do
- let_it_be(:group) { create(:group) }
+ let_it_be(:root_group) { create(:group) }
+ let_it_be(:group) { create(:group, parent: root_group) }
let_it_be(:subgroup) { create(:group, parent: group) }
let_it_be(:current_user) { create(:user) }
let(:params) { {} }
@@ -16,6 +17,9 @@ RSpec.shared_context 'GroupProjectsFinder context' do
let_it_be(:shared_project_3) { create(:project, :internal, path: '5', name: 'c') }
let_it_be(:subgroup_project) { create(:project, :public, path: '6', group: subgroup, name: 'b') }
let_it_be(:subgroup_private_project) { create(:project, :private, path: '7', group: subgroup, name: 'a') }
+ let_it_be(:root_group_public_project) { create(:project, :public, path: '8', group: root_group, name: 'root-public-project') }
+ let_it_be(:root_group_private_project) { create(:project, :private, path: '9', group: root_group, name: 'root-private-project') }
+ let_it_be(:root_group_private_project_2) { create(:project, :private, path: '10', group: root_group, name: 'root-private-project-2') }
before do
shared_project_1.project_group_links.create!(group_access: Gitlab::Access::MAINTAINER, group: group)
diff --git a/spec/support/shared_contexts/graphql/requests/packages_shared_context.rb b/spec/support/shared_contexts/graphql/requests/packages_shared_context.rb
index 9ac3d4a04f9..13e7ecf2669 100644
--- a/spec/support/shared_contexts/graphql/requests/packages_shared_context.rb
+++ b/spec/support/shared_contexts/graphql/requests/packages_shared_context.rb
@@ -11,7 +11,7 @@ RSpec.shared_context 'package details setup' do
let(:package_files) { all_graphql_fields_for('PackageFile') }
let(:dependency_links) { all_graphql_fields_for('PackageDependencyLink') }
let(:pipelines) { all_graphql_fields_for('Pipeline', max_depth: 1) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:package_details) { graphql_data_at(:package) }
let(:metadata_response) { graphql_data_at(:package, :metadata) }
let(:first_file) { package.package_files.find { |f| global_id_of(f) == first_file_response['id'] } }
diff --git a/spec/support/shared_contexts/lib/container_registry/client_shared_context.rb b/spec/support/shared_contexts/lib/container_registry/client_shared_context.rb
new file mode 100644
index 00000000000..a87a3247b95
--- /dev/null
+++ b/spec/support/shared_contexts/lib/container_registry/client_shared_context.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'container registry client' do
+ let(:token) { '12345' }
+ let(:options) { { token: token } }
+ let(:registry_api_url) { 'http://container-registry' }
+ let(:client) { described_class.new(registry_api_url, options) }
+ let(:push_blob_headers) do
+ {
+ 'Accept' => 'application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json',
+ 'Authorization' => "bearer #{token}",
+ 'Content-Type' => 'application/octet-stream',
+ 'User-Agent' => "GitLab/#{Gitlab::VERSION}"
+ }
+ end
+
+ let(:headers_with_accept_types) do
+ {
+ 'Accept' => 'application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json',
+ 'Authorization' => "bearer #{token}",
+ 'User-Agent' => "GitLab/#{Gitlab::VERSION}"
+ }
+ end
+
+ let(:expected_faraday_headers) { { user_agent: "GitLab/#{Gitlab::VERSION}" } }
+ let(:expected_faraday_request_options) { Gitlab::HTTP::DEFAULT_TIMEOUT_OPTIONS }
+end
diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb
index 27967850389..576a8aa44fa 100644
--- a/spec/support/shared_contexts/navbar_structure_context.rb
+++ b/spec/support/shared_contexts/navbar_structure_context.rb
@@ -22,6 +22,7 @@ RSpec.shared_context 'project navbar structure' do
nav_sub_items: [
_('Activity'),
_('Labels'),
+ _('Planning hierarchy'),
_('Members')
]
},
diff --git a/spec/support/shared_contexts/policies/group_policy_shared_context.rb b/spec/support/shared_contexts/policies/group_policy_shared_context.rb
index 0dfd76de79c..76db2bd82f1 100644
--- a/spec/support/shared_contexts/policies/group_policy_shared_context.rb
+++ b/spec/support/shared_contexts/policies/group_policy_shared_context.rb
@@ -47,6 +47,7 @@ RSpec.shared_context 'GroupPolicy context' do
create_custom_emoji
create_package
create_package_settings
+ read_cluster
]
end
@@ -54,7 +55,7 @@ RSpec.shared_context 'GroupPolicy context' do
%i[
destroy_package
create_projects
- read_cluster create_cluster update_cluster admin_cluster add_cluster
+ create_cluster update_cluster admin_cluster add_cluster
]
end
diff --git a/spec/support/shared_contexts/policies/project_policy_shared_context.rb b/spec/support/shared_contexts/policies/project_policy_shared_context.rb
index c39252cef13..3641edc845a 100644
--- a/spec/support/shared_contexts/policies/project_policy_shared_context.rb
+++ b/spec/support/shared_contexts/policies/project_policy_shared_context.rb
@@ -17,7 +17,7 @@ RSpec.shared_context 'ProjectPolicy context' do
%i[
award_emoji create_issue create_merge_request_in create_note
create_project read_issue_board read_issue read_issue_iid read_issue_link
- read_label read_issue_board_list read_milestone read_note read_project
+ read_label read_planning_hierarchy read_issue_board_list read_milestone read_note read_project
read_project_for_iids read_project_member read_release read_snippet
read_wiki upload_file
]
diff --git a/spec/support/shared_contexts/services/projects/container_repository/delete_tags_service_shared_context.rb b/spec/support/shared_contexts/services/projects/container_repository/delete_tags_service_shared_context.rb
index 21be989d697..e26b8cd8b37 100644
--- a/spec/support/shared_contexts/services/projects/container_repository/delete_tags_service_shared_context.rb
+++ b/spec/support/shared_contexts/services/projects/container_repository/delete_tags_service_shared_context.rb
@@ -3,7 +3,7 @@
RSpec.shared_context 'container repository delete tags service shared context' do
let_it_be(:user) { create(:user) }
let_it_be(:project, reload: true) { create(:project, :private) }
- let_it_be(:repository) { create(:container_repository, :root, project: project) }
+ let_it_be_with_reload(:repository) { create(:container_repository, :root, project: project) }
let(:params) { { tags: tags } }
diff --git a/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb b/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb
index 62b35923bcd..5ed8dc7ce98 100644
--- a/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb
@@ -205,10 +205,30 @@ RSpec.shared_examples 'handle uploads' do
allow_any_instance_of(FileUploader).to receive(:image?).and_return(true)
end
- it "responds with status 200" do
- show_upload
+ context "enforce_auth_checks_on_uploads feature flag" do
+ context "with flag enabled" do
+ before do
+ stub_feature_flags(enforce_auth_checks_on_uploads: true)
+ end
- expect(response).to have_gitlab_http_status(:ok)
+ it "responds with status 302" do
+ show_upload
+
+ expect(response).to have_gitlab_http_status(:redirect)
+ end
+ end
+
+ context "with flag disabled" do
+ before do
+ stub_feature_flags(enforce_auth_checks_on_uploads: false)
+ end
+
+ it "responds with status 200" do
+ show_upload
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
end
end
@@ -276,10 +296,30 @@ RSpec.shared_examples 'handle uploads' do
allow_any_instance_of(FileUploader).to receive(:image?).and_return(true)
end
- it "responds with status 200" do
- show_upload
+ context "enforce_auth_checks_on_uploads feature flag" do
+ context "with flag enabled" do
+ before do
+ stub_feature_flags(enforce_auth_checks_on_uploads: true)
+ end
+
+ it "responds with status 404" do
+ show_upload
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context "with flag disabled" do
+ before do
+ stub_feature_flags(enforce_auth_checks_on_uploads: false)
+ end
+
+ it "responds with status 200" do
+ show_upload
- expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
end
end
diff --git a/spec/support/shared_examples/features/comments_on_merge_request_files_shared_examples.rb b/spec/support/shared_examples/features/comments_on_merge_request_files_shared_examples.rb
index e0a032b1a43..8a07e52019c 100644
--- a/spec/support/shared_examples/features/comments_on_merge_request_files_shared_examples.rb
+++ b/spec/support/shared_examples/features/comments_on_merge_request_files_shared_examples.rb
@@ -2,7 +2,7 @@
RSpec.shared_examples 'comment on merge request file' do
it 'adds a comment' do
- click_diff_line(find("[id='#{sample_commit.line_code}']"))
+ click_diff_line(find_by_scrolling("[id='#{sample_commit.line_code}']"))
page.within('.js-discussion-note-form') do
fill_in(:note_note, with: 'Line is wrong')
diff --git a/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb b/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb
index 1c816ee4b0a..456175e7113 100644
--- a/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb
+++ b/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb
@@ -62,7 +62,7 @@ RSpec.shared_examples 'a creatable merge request' do
end
it 'updates the branches when selecting a new target project', :js do
- target_project_member = target_project.owner
+ target_project_member = target_project.first_owner
::Branches::CreateService.new(target_project, target_project_member)
.execute('a-brand-new-branch-to-test', 'master')
diff --git a/spec/support/shared_examples/features/sidebar/sidebar_labels_shared_examples.rb b/spec/support/shared_examples/features/sidebar/sidebar_labels_shared_examples.rb
index a9dac7a391f..281a70e46c4 100644
--- a/spec/support/shared_examples/features/sidebar/sidebar_labels_shared_examples.rb
+++ b/spec/support/shared_examples/features/sidebar/sidebar_labels_shared_examples.rb
@@ -54,7 +54,10 @@ RSpec.shared_examples 'labels sidebar widget' do
end
fill_in 'Search', with: 'Devel'
- sleep 1
+ expect(page).to have_css('.labels-fetch-loading')
+ wait_for_all_requests
+
+ expect(page).to have_css('[data-testid="dropdown-content"] .gl-new-dropdown-item')
expect(page.all(:css, '[data-testid="dropdown-content"] .gl-new-dropdown-item').length).to eq(1)
find_field('Search').native.send_keys(:enter)
diff --git a/spec/support/shared_examples/features/variable_list_shared_examples.rb b/spec/support/shared_examples/features/variable_list_shared_examples.rb
index 52451839281..c63faace6b2 100644
--- a/spec/support/shared_examples/features/variable_list_shared_examples.rb
+++ b/spec/support/shared_examples/features/variable_list_shared_examples.rb
@@ -166,7 +166,7 @@ RSpec.shared_examples 'variable list' do
wait_for_requests
expect(find('.flash-container')).to be_present
- expect(find('.flash-text').text).to have_content('Variables key (key) has already been taken')
+ expect(find('[data-testid="alert-danger"]').text).to have_content('Variables key (key) has already been taken')
end
it 'prevents a variable to be added if no values are provided when a variable is set to masked' do
diff --git a/spec/support/shared_examples/features/wiki/user_previews_wiki_changes_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_previews_wiki_changes_shared_examples.rb
index 1a981f42086..2285d9a17e2 100644
--- a/spec/support/shared_examples/features/wiki/user_previews_wiki_changes_shared_examples.rb
+++ b/spec/support/shared_examples/features/wiki/user_previews_wiki_changes_shared_examples.rb
@@ -85,7 +85,9 @@ RSpec.shared_examples 'User previews wiki changes' do
end
it 'renders content with CommonMark' do
- fill_in :wiki_content, with: "1. one\n - sublist\n"
+ # using two `\n` ensures we're sublist to it's own line due
+ # to list auto-continue
+ fill_in :wiki_content, with: "1. one\n\n - sublist\n"
click_on "Preview"
# the above generates two separate lists (not embedded) in CommonMark
diff --git a/spec/support/shared_examples/graphql/boards_shared_examples.rb b/spec/support/shared_examples/graphql/boards_shared_examples.rb
new file mode 100644
index 00000000000..e8a4c17fb92
--- /dev/null
+++ b/spec/support/shared_examples/graphql/boards_shared_examples.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'querying a GraphQL type recent boards' do
+ describe 'Get list of recently visited boards' do
+ let(:boards_data) { graphql_data[board_type]['recentIssueBoards']['nodes'] }
+
+ context 'when the request is correct' do
+ before do
+ visit_board
+ parent.add_reporter(user)
+ post_graphql(query, current_user: user)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns recent boards for user successfully' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(graphql_errors).to be_nil
+ expect(boards_data.size).to eq(1)
+ expect(boards_data[0]['name']).to eq(board.name)
+ end
+ end
+
+ context 'when requests has errors' do
+ context 'when there are no recently visited boards' do
+ it 'returns empty result' do
+ post_graphql(query, current_user: user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(graphql_errors).to be_nil
+ expect(boards_data).to be_empty
+ end
+ end
+ end
+ end
+
+ def query(query_params: {}, full_path: parent.full_path)
+ board_nodes = <<~NODE
+ nodes {
+ name
+ }
+ NODE
+
+ graphql_query_for(
+ board_type.to_sym,
+ { full_path: full_path },
+ query_graphql_field(:recent_issue_boards, query_params, board_nodes)
+ )
+ end
+
+ def visit_board
+ if board_type == 'group'
+ create(:board_group_recent_visit, group: parent, board: board, user: user)
+ else
+ create(:board_project_recent_visit, project: parent, board: board, user: user)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/graphql/mutations/can_mutate_spammable_examples.rb b/spec/support/shared_examples/graphql/mutations/can_mutate_spammable_examples.rb
index 011a2157f24..b17e59f0797 100644
--- a/spec/support/shared_examples/graphql/mutations/can_mutate_spammable_examples.rb
+++ b/spec/support/shared_examples/graphql/mutations/can_mutate_spammable_examples.rb
@@ -16,17 +16,4 @@ RSpec.shared_examples 'a mutation which can mutate a spammable' do
subject
end
end
-
- describe "#spam_action_response_fields" do
- it 'resolves with spam action fields' do
- subject
-
- # NOTE: We do not need to assert on the specific values of spam action fields here, we only need
- # to verify that #spam_action_response_fields was invoked and that the fields are present in the
- # response. The specific behavior of #spam_action_response_fields is covered in the
- # HasSpamActionResponseFields unit tests.
- expect(mutation_response.keys)
- .to include('spam', 'spamLogId', 'needsCaptchaResponse', 'captchaSiteKey')
- end
- end
end
diff --git a/spec/support/shared_examples/graphql/mutations/security/ci_configuration_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/security/ci_configuration_shared_examples.rb
index 2bb3d807aa7..14b2663a72c 100644
--- a/spec/support/shared_examples/graphql/mutations/security/ci_configuration_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/mutations/security/ci_configuration_shared_examples.rb
@@ -18,7 +18,7 @@ RSpec.shared_examples_for 'graphql mutations security ci configuration' do
ServiceResponse.success(payload: { branch: branch, success_path: success_path })
end
- let(:error) { "An error occured!" }
+ let(:error) { "An error occurred!" }
let(:service_error_response) do
ServiceResponse.error(message: error)
diff --git a/spec/support/shared_examples/integrations/integration_settings_form.rb b/spec/support/shared_examples/integrations/integration_settings_form.rb
new file mode 100644
index 00000000000..d0bb40e43ee
--- /dev/null
+++ b/spec/support/shared_examples/integrations/integration_settings_form.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'integration settings form' do
+ include IntegrationsHelper
+ # Note: these specs don't validate channel fields
+ # which are present on a few integrations
+ it 'displays all the integrations' do
+ aggregate_failures do
+ integrations.each do |integration|
+ navigate_to_integration(integration)
+
+ page.within('form.integration-settings-form') do
+ expect(page).to have_field('Active', type: 'checkbox', wait: 0),
+ "#{integration.title} active field not present"
+
+ fields = parse_json(fields_for_integration(integration))
+ fields.each do |field|
+ field_name = field[:name]
+ expect(page).to have_field(field[:title], wait: 0),
+ "#{integration.title} field #{field_name} not present"
+ end
+
+ events = parse_json(trigger_events_for_integration(integration))
+ events.each do |trigger|
+ # normalizing the title because capybara location is case sensitive
+ title = normalize_title trigger[:title], integration
+
+ expect(page).to have_field(title, type: 'checkbox', wait: 0),
+ "#{integration.title} field #{title} checkbox not present"
+ end
+ end
+ end
+ end
+ end
+
+ private
+
+ def normalize_title(title, integration)
+ return 'Merge request' if integration.is_a?(Integrations::Jira) && title == 'merge_request'
+
+ title.titlecase
+ end
+
+ def parse_json(json)
+ Gitlab::Json.parse(json, symbolize_names: true)
+ end
+end
diff --git a/spec/support/shared_examples/lib/gitlab/experimentation_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/experimentation_shared_examples.rb
index 5baa6478225..fdca326dbea 100644
--- a/spec/support/shared_examples/lib/gitlab/experimentation_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/experimentation_shared_examples.rb
@@ -1,6 +1,10 @@
# frozen_string_literal: true
RSpec.shared_examples 'tracks assignment and records the subject' do |experiment, subject_type|
+ before do
+ stub_experiments(experiment => true)
+ end
+
it 'tracks the assignment', :experiment do
expect(experiment(experiment))
.to track(:assignment)
@@ -11,9 +15,7 @@ RSpec.shared_examples 'tracks assignment and records the subject' do |experiment
end
it 'records the subject' do
- stub_experiments(experiment => :candidate)
-
- expect(Experiment).to receive(:add_subject).with(experiment.to_s, variant: :experimental, subject: subject)
+ expect(Experiment).to receive(:add_subject).with(experiment.to_s, variant: anything, subject: subject)
action
end
diff --git a/spec/lib/gitlab/usage_data_counters/vscode_extenion_activity_unique_counter_spec.rb b/spec/support/shared_examples/lib/gitlab/usage_data_counters/code_review_extension_request_examples.rb
index 7593d51fe76..6221366ab51 100644
--- a/spec/lib/gitlab/usage_data_counters/vscode_extenion_activity_unique_counter_spec.rb
+++ b/spec/support/shared_examples/lib/gitlab/usage_data_counters/code_review_extension_request_examples.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.shared_examples 'a tracked vs code unique action' do |event|
+RSpec.shared_examples 'a request from an extension' do |event|
before do
stub_application_setting(usage_ping_enabled: true)
end
@@ -11,10 +11,12 @@ RSpec.shared_examples 'a tracked vs code unique action' do |event|
Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(event_names: action, start_date: date_from, end_date: date_to)
end
- it 'tracks when the user agent is from vs code' do
- aggregate_failures do
- user_agent = { user_agent: 'vs-code-gitlab-workflow/3.11.1 VSCode/1.52.1 Node.js/12.14.1 (darwin; x64)' }
+ def track_action(params)
+ described_class.track_api_request_when_trackable(**params)
+ end
+ it 'tracks when the user agent is matching' do
+ aggregate_failures do
expect(track_action(user: user1, **user_agent)).to be_truthy
expect(track_action(user: user1, **user_agent)).to be_truthy
expect(track_action(user: user2, **user_agent)).to be_truthy
@@ -23,7 +25,7 @@ RSpec.shared_examples 'a tracked vs code unique action' do |event|
end
end
- it 'does not track when the user agent is not from vs code' do
+ it 'does not track when the user agent is not matching' do
aggregate_failures do
user_agent = { user_agent: 'normal_user_agent' }
@@ -40,24 +42,6 @@ RSpec.shared_examples 'a tracked vs code unique action' do |event|
end
it 'does not track if user is not present' do
- user_agent = { user_agent: 'vs-code-gitlab-workflow/3.11.1 VSCode/1.52.1 Node.js/12.14.1 (darwin; x64)' }
-
expect(track_action(user: nil, **user_agent)).to be_nil
end
end
-
-RSpec.describe Gitlab::UsageDataCounters::VSCodeExtensionActivityUniqueCounter, :clean_gitlab_redis_shared_state do
- let(:user1) { build(:user, id: 1) }
- let(:user2) { build(:user, id: 2) }
- let(:time) { Time.current }
-
- context 'when tracking a vs code api request' do
- it_behaves_like 'a tracked vs code unique action' do
- let(:action) { described_class::VS_CODE_API_REQUEST_ACTION }
-
- def track_action(params)
- described_class.track_api_request_when_trackable(**params)
- end
- end
- end
-end
diff --git a/spec/support/shared_examples/lib/sidebars/projects/menus/zentao_menu_shared_examples.rb b/spec/support/shared_examples/lib/sidebars/projects/menus/zentao_menu_shared_examples.rb
index d3fd28727b5..b4c438771ce 100644
--- a/spec/support/shared_examples/lib/sidebars/projects/menus/zentao_menu_shared_examples.rb
+++ b/spec/support/shared_examples/lib/sidebars/projects/menus/zentao_menu_shared_examples.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.shared_examples 'ZenTao menu with CE version' do
let(:project) { create(:project, has_external_issue_tracker: true) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) }
let(:zentao_integration) { create(:zentao_integration, project: project) }
diff --git a/spec/support/shared_examples/loose_foreign_keys/have_loose_foreign_key.rb b/spec/support/shared_examples/loose_foreign_keys/have_loose_foreign_key.rb
index 42eec74e64f..5f59d43ad19 100644
--- a/spec/support/shared_examples/loose_foreign_keys/have_loose_foreign_key.rb
+++ b/spec/support/shared_examples/loose_foreign_keys/have_loose_foreign_key.rb
@@ -7,6 +7,12 @@ RSpec.shared_examples 'it has loose foreign keys' do
let(:fully_qualified_table_name) { "#{connection.current_schema}.#{table_name}" }
let(:deleted_records) { LooseForeignKeys::DeletedRecord.where(fully_qualified_table_name: fully_qualified_table_name) }
+ around do |example|
+ LooseForeignKeys::DeletedRecord.using_connection(connection) do
+ example.run
+ end
+ end
+
it 'has at least one loose foreign key definition' do
definitions = Gitlab::Database::LooseForeignKeys.definitions_by_table[table_name]
expect(definitions.size).to be > 0
@@ -69,7 +75,9 @@ RSpec.shared_examples 'cleanup by a loose foreign key' do
expect(find_model).to be_present
- LooseForeignKeys::ProcessDeletedRecordsService.new(connection: model.connection).execute
+ LooseForeignKeys::DeletedRecord.using_connection(parent.connection) do
+ LooseForeignKeys::ProcessDeletedRecordsService.new(connection: parent.connection).execute
+ end
if foreign_key_definition.on_delete.eql?(:async_delete)
expect(find_model).not_to be_present
diff --git a/spec/support/shared_examples/models/concerns/analytics/cycle_analytics/stage_event_model_examples.rb b/spec/support/shared_examples/models/concerns/analytics/cycle_analytics/stage_event_model_examples.rb
index d823e7ac221..8ff30021d6e 100644
--- a/spec/support/shared_examples/models/concerns/analytics/cycle_analytics/stage_event_model_examples.rb
+++ b/spec/support/shared_examples/models/concerns/analytics/cycle_analytics/stage_event_model_examples.rb
@@ -178,4 +178,22 @@ RSpec.shared_examples 'StageEventModel' do
end
end
end
+
+ describe '#total_time' do
+ it 'calcualtes total time from the start_event_timestamp and end_event_timestamp columns' do
+ model = described_class.new(start_event_timestamp: Time.new(2022, 1, 1, 12, 5, 0), end_event_timestamp: Time.new(2022, 1, 1, 12, 6, 30))
+
+ expect(model.total_time).to eq(90)
+ end
+
+ context 'when total time is calculated in SQL as an extra column' do
+ it 'returns the SQL calculated time' do
+ create(stage_event_factory) # rubocop:disable Rails/SaveBang
+
+ model = described_class.select('*, 5 AS total_time').first
+
+ expect(model.total_time).to eq(5)
+ end
+ end
+ end
end
diff --git a/spec/support/shared_examples/models/member_shared_examples.rb b/spec/support/shared_examples/models/member_shared_examples.rb
index 5b4b8c8fcc1..f7e09cfca62 100644
--- a/spec/support/shared_examples/models/member_shared_examples.rb
+++ b/spec/support/shared_examples/models/member_shared_examples.rb
@@ -301,8 +301,9 @@ RSpec.shared_examples_for "member creation" do
end
context 'when `tasks_to_be_done` and `tasks_project_id` are passed' do
+ let(:task_project) { source.is_a?(Group) ? create(:project, group: source) : source }
+
it 'creates a member_task with the correct attributes', :aggregate_failures do
- task_project = source.is_a?(Group) ? create(:project, group: source) : source
described_class.new(source, user, :developer, tasks_to_be_done: %w(ci code), tasks_project_id: task_project.id).execute
member = source.members.last
@@ -310,6 +311,43 @@ RSpec.shared_examples_for "member creation" do
expect(member.tasks_to_be_done).to match_array([:ci, :code])
expect(member.member_task.project).to eq(task_project)
end
+
+ context 'with an already existing member' do
+ before do
+ source.add_user(user, :developer)
+ end
+
+ it 'does not update tasks to be done if tasks already exist', :aggregate_failures do
+ member = source.members.find_by(user_id: user.id)
+ create(:member_task, member: member, project: task_project, tasks_to_be_done: %w(code ci))
+
+ expect do
+ described_class.new(source,
+ user,
+ :developer,
+ tasks_to_be_done: %w(issues),
+ tasks_project_id: task_project.id).execute
+ end.not_to change(MemberTask, :count)
+
+ member.reset
+ expect(member.tasks_to_be_done).to match_array([:code, :ci])
+ expect(member.member_task.project).to eq(task_project)
+ end
+
+ it 'adds tasks to be done if they do not exist', :aggregate_failures do
+ expect do
+ described_class.new(source,
+ user,
+ :developer,
+ tasks_to_be_done: %w(issues),
+ tasks_project_id: task_project.id).execute
+ end.to change(MemberTask, :count).by(1)
+
+ member = source.members.find_by(user_id: user.id)
+ expect(member.tasks_to_be_done).to match_array([:issues])
+ expect(member.member_task.project).to eq(task_project)
+ end
+ end
end
end
end
@@ -393,14 +431,52 @@ RSpec.shared_examples_for "bulk member creation" do
end
context 'when `tasks_to_be_done` and `tasks_project_id` are passed' do
+ let(:task_project) { source.is_a?(Group) ? create(:project, group: source) : source }
+
it 'creates a member_task with the correct attributes', :aggregate_failures do
- task_project = source.is_a?(Group) ? create(:project, group: source) : source
members = described_class.add_users(source, [user1], :developer, tasks_to_be_done: %w(ci code), tasks_project_id: task_project.id)
member = members.last
expect(member.tasks_to_be_done).to match_array([:ci, :code])
expect(member.member_task.project).to eq(task_project)
end
+
+ context 'with an already existing member' do
+ before do
+ source.add_user(user1, :developer)
+ end
+
+ it 'does not update tasks to be done if tasks already exist', :aggregate_failures do
+ member = source.members.find_by(user_id: user1.id)
+ create(:member_task, member: member, project: task_project, tasks_to_be_done: %w(code ci))
+
+ expect do
+ described_class.add_users(source,
+ [user1.id],
+ :developer,
+ tasks_to_be_done: %w(issues),
+ tasks_project_id: task_project.id)
+ end.not_to change(MemberTask, :count)
+
+ member.reset
+ expect(member.tasks_to_be_done).to match_array([:code, :ci])
+ expect(member.member_task.project).to eq(task_project)
+ end
+
+ it 'adds tasks to be done if they do not exist', :aggregate_failures do
+ expect do
+ described_class.add_users(source,
+ [user1.id],
+ :developer,
+ tasks_to_be_done: %w(issues),
+ tasks_project_id: task_project.id)
+ end.to change(MemberTask, :count).by(1)
+
+ member = source.members.find_by(user_id: user1.id)
+ expect(member.tasks_to_be_done).to match_array([:issues])
+ expect(member.member_task.project).to eq(task_project)
+ end
+ end
end
end
end
diff --git a/spec/support/shared_examples/models/note_access_check_shared_examples.rb b/spec/support/shared_examples/models/note_access_check_shared_examples.rb
index 44edafe9091..0c9992b832f 100644
--- a/spec/support/shared_examples/models/note_access_check_shared_examples.rb
+++ b/spec/support/shared_examples/models/note_access_check_shared_examples.rb
@@ -3,7 +3,7 @@
RSpec.shared_examples 'users with note access' do
it 'returns true' do
users.each do |user|
- expect(note.system_note_with_references_visible_for?(user)).to be_truthy
+ expect(note.system_note_visible_for?(user)).to be_truthy
expect(note.readable_by?(user)).to be_truthy
end
end
@@ -12,7 +12,7 @@ end
RSpec.shared_examples 'users without note access' do
it 'returns false' do
users.each do |user|
- expect(note.system_note_with_references_visible_for?(user)).to be_falsy
+ expect(note.system_note_visible_for?(user)).to be_falsy
expect(note.readable_by?(user)).to be_falsy
end
end
diff --git a/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb b/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb
index 3f8c3b8960b..6b0ae589efb 100644
--- a/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb
+++ b/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb
@@ -235,18 +235,6 @@ RSpec.shared_examples 'Debian Distribution' do |factory, container, can_freeze|
it 'does not return them' do
expect(subject.to_a).not_to include(package_file_pending_destruction)
end
-
- context 'with packages_installable_package_files disabled' do
- before do
- stub_feature_flags(packages_installable_package_files: false)
- end
-
- it 'returns them' do
- subject
-
- expect(subject.to_a).to include(package_file_pending_destruction)
- end
- end
end
end
end
diff --git a/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb b/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb
index 06326ffac97..ad0bbc0aeff 100644
--- a/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb
+++ b/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb
@@ -115,14 +115,14 @@ RSpec.shared_examples 'UpdateProjectStatistics' do |with_counter_attribute|
expect(ProjectStatistics)
.not_to receive(:increment_statistic)
- expect(Projects::DestroyService.new(project, project.owner).execute).to eq(true)
+ expect(Projects::DestroyService.new(project, project.first_owner).execute).to eq(true)
end
it 'does not schedule a namespace statistics worker' do
expect(Namespaces::ScheduleAggregationWorker)
.not_to receive(:perform_async)
- expect(Projects::DestroyService.new(project, project.owner).execute).to eq(true)
+ expect(Projects::DestroyService.new(project, project.first_owner).execute).to eq(true)
end
end
end
diff --git a/spec/support/shared_examples/namespaces/traversal_scope_examples.rb b/spec/support/shared_examples/namespaces/traversal_scope_examples.rb
index b43b7946e69..bcb5464ed5b 100644
--- a/spec/support/shared_examples/namespaces/traversal_scope_examples.rb
+++ b/spec/support/shared_examples/namespaces/traversal_scope_examples.rb
@@ -299,4 +299,51 @@ RSpec.shared_examples 'namespace traversal scopes' do
include_examples '.self_and_descendant_ids'
end
end
+
+ shared_examples '.self_and_hierarchy' do
+ let(:base_scope) { Group.where(id: base_groups) }
+
+ subject { base_scope.self_and_hierarchy }
+
+ context 'with ancestors only' do
+ let(:base_groups) { [group_1, group_2] }
+
+ it { is_expected.to match_array(groups) }
+ end
+
+ context 'with descendants only' do
+ let(:base_groups) { [deep_nested_group_1, deep_nested_group_2] }
+
+ it { is_expected.to match_array(groups) }
+ end
+
+ context 'nodes with both ancestors and descendants' do
+ let(:base_groups) { [nested_group_1, nested_group_2] }
+
+ it { is_expected.to match_array(groups) }
+ end
+
+ context 'with duplicate base groups' do
+ let(:base_groups) { [nested_group_1, nested_group_1] }
+
+ it { is_expected.to contain_exactly(group_1, nested_group_1, deep_nested_group_1) }
+ end
+ end
+
+ describe '.self_and_hierarchy' do
+ it_behaves_like '.self_and_hierarchy'
+
+ context "use_traversal_ids_for_self_and_hierarchy_scopes feature flag is false" do
+ before do
+ stub_feature_flags(use_traversal_ids_for_self_and_hierarchy_scopes: false)
+ end
+
+ it_behaves_like '.self_and_hierarchy'
+
+ it 'make recursive queries' do
+ base_groups = Group.where(id: nested_group_1)
+ expect { base_groups.self_and_hierarchy.load }.to make_queries_matching(/WITH RECURSIVE/)
+ end
+ end
+ end
end
diff --git a/spec/support/shared_examples/path_extraction_shared_examples.rb b/spec/support/shared_examples/path_extraction_shared_examples.rb
index 39c7c1f2a94..d76348aa26a 100644
--- a/spec/support/shared_examples/path_extraction_shared_examples.rb
+++ b/spec/support/shared_examples/path_extraction_shared_examples.rb
@@ -40,12 +40,13 @@ RSpec.shared_examples 'assigns ref vars' do
end
context 'path contains space' do
- let(:params) { { path: 'with space', ref: '38008cb17ce1466d8fec2dfa6f6ab8dcfe5cf49e' } }
+ let(:ref) { '38008cb17ce1466d8fec2dfa6f6ab8dcfe5cf49e' }
+ let(:path) { 'with space' }
it 'is not converted to %20 in @path' do
assign_ref_vars
- expect(@path).to eq(params[:path])
+ expect(@path).to eq(path)
end
end
diff --git a/spec/support/shared_examples/policies/clusterable_shared_examples.rb b/spec/support/shared_examples/policies/clusterable_shared_examples.rb
index b96aa71acbe..faf283f9059 100644
--- a/spec/support/shared_examples/policies/clusterable_shared_examples.rb
+++ b/spec/support/shared_examples/policies/clusterable_shared_examples.rb
@@ -6,12 +6,24 @@ RSpec.shared_examples 'clusterable policies' do
subject { described_class.new(current_user, clusterable) }
+ context 'with a reporter' do
+ before do
+ clusterable.add_reporter(current_user)
+ end
+
+ it { expect_disallowed(:read_cluster) }
+ it { expect_disallowed(:add_cluster) }
+ it { expect_disallowed(:create_cluster) }
+ it { expect_disallowed(:update_cluster) }
+ it { expect_disallowed(:admin_cluster) }
+ end
+
context 'with a developer' do
before do
clusterable.add_developer(current_user)
end
- it { expect_disallowed(:read_cluster) }
+ it { expect_allowed(:read_cluster) }
it { expect_disallowed(:add_cluster) }
it { expect_disallowed(:create_cluster) }
it { expect_disallowed(:update_cluster) }
diff --git a/spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb
index 052fd0622d0..f414500f202 100644
--- a/spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb
@@ -66,7 +66,7 @@ RSpec.shared_examples 'issuable time tracker' do |issuable_type|
it 'shows the help state when icon is clicked' do
page.within '.time-tracking-component-wrap' do
- find('.help-button').click
+ find('[data-testid="helpButton"]').click
expect(page).to have_content 'Track time with quick actions'
expect(page).to have_content 'Learn more'
end
@@ -92,8 +92,8 @@ RSpec.shared_examples 'issuable time tracker' do |issuable_type|
it 'hides the help state when close icon is clicked' do
page.within '.time-tracking-component-wrap' do
- find('.help-button').click
- find('.close-help-button').click
+ find('[data-testid="helpButton"]').click
+ find('[data-testid="closeHelpButton"]').click
expect(page).not_to have_content 'Track time with quick actions'
expect(page).not_to have_content 'Learn more'
@@ -102,7 +102,7 @@ RSpec.shared_examples 'issuable time tracker' do |issuable_type|
it 'displays the correct help url' do
page.within '.time-tracking-component-wrap' do
- find('.help-button').click
+ find('[data-testid="helpButton"]').click
expect(find_link('Learn more')[:href]).to have_content('/help/user/project/time_tracking.md')
end
diff --git a/spec/support/shared_examples/quick_actions/merge_request/rebase_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/merge_request/rebase_quick_action_shared_examples.rb
index 28decb4011d..2258bdd2c79 100644
--- a/spec/support/shared_examples/quick_actions/merge_request/rebase_quick_action_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/merge_request/rebase_quick_action_shared_examples.rb
@@ -73,6 +73,16 @@ RSpec.shared_examples 'rebase quick action' do
expect(page).to have_content 'This merge request cannot be rebased while there are conflicts.'
end
end
+
+ context 'when the merge request branch is protected from force push' do
+ let!(:protected_branch) { create(:protected_branch, project: project, name: merge_request.source_branch, allow_force_push: false) }
+
+ it 'does not rebase the MR' do
+ add_note("/rebase")
+
+ expect(page).to have_content 'This merge request branch is protected from force push.'
+ end
+ end
end
context 'when the current user cannot rebase the MR' do
diff --git a/spec/support/shared_examples/requests/api/graphql/mutations/snippets_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/mutations/snippets_shared_examples.rb
index 8bffd1f71e9..a42a1fda62e 100644
--- a/spec/support/shared_examples/requests/api/graphql/mutations/snippets_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/graphql/mutations/snippets_shared_examples.rb
@@ -10,6 +10,8 @@ RSpec.shared_examples 'when the snippet is not found' do
end
RSpec.shared_examples 'snippet edit usage data counters' do
+ include SessionHelpers
+
context 'when user is sessionless' do
it 'does not track usage data actions' do
expect(::Gitlab::UsageDataCounters::EditorUniqueCounter).not_to receive(:track_snippet_editor_edit_action)
@@ -20,14 +22,7 @@ RSpec.shared_examples 'snippet edit usage data counters' do
context 'when user is not sessionless', :clean_gitlab_redis_sessions do
before do
- session_id = Rack::Session::SessionId.new('6919a6f1bb119dd7396fadc38fd18d0d')
- session_hash = { 'warden.user.user.key' => [[current_user.id], current_user.encrypted_password[0, 29]] }
-
- Gitlab::Redis::Sessions.with do |redis|
- redis.set("session:gitlab:#{session_id.private_id}", Marshal.dump(session_hash))
- end
-
- cookies[Gitlab::Application.config.session_options[:key]] = session_id.public_id
+ stub_session('warden.user.user.key' => [[current_user.id], current_user.encrypted_password[0, 29]])
end
it 'tracks usage data actions', :clean_gitlab_redis_sessions do
diff --git a/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb
index 882c79cb03f..127b1a6d4c4 100644
--- a/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb
@@ -3,11 +3,11 @@
RSpec.shared_examples 'group and project packages query' do
include GraphqlHelpers
- let_it_be(:versionaless_package) { create(:maven_package, project: project1, version: nil) }
- let_it_be(:maven_package) { create(:maven_package, project: project1, name: 'tab', version: '4.0.0', created_at: 5.days.ago) }
- let_it_be(:package) { create(:npm_package, project: project1, name: 'uab', version: '5.0.0', created_at: 4.days.ago) }
- let_it_be(:composer_package) { create(:composer_package, project: project2, name: 'vab', version: '6.0.0', created_at: 3.days.ago) }
- let_it_be(:debian_package) { create(:debian_package, project: project2, name: 'zab', version: '7.0.0', created_at: 2.days.ago) }
+ let_it_be(:versionless_package) { create(:maven_package, project: project1, version: nil) }
+ let_it_be(:maven_package) { create(:maven_package, project: project1, name: 'bab', version: '6.0.0', created_at: 1.day.ago) }
+ let_it_be(:npm_package) { create(:npm_package, project: project1, name: 'cab', version: '7.0.0', created_at: 4.days.ago) }
+ let_it_be(:composer_package) { create(:composer_package, project: project2, name: 'dab', version: '4.0.0', created_at: 3.days.ago) }
+ let_it_be(:debian_package) { create(:debian_package, project: project2, name: 'aab', version: '5.0.0', created_at: 2.days.ago) }
let_it_be(:composer_metadatum) do
create(:composer_metadatum, package: composer_package,
target_sha: 'afdeh',
@@ -21,11 +21,11 @@ RSpec.shared_examples 'group and project packages query' do
let(:fields) do
<<~QUERY
- count
- nodes {
- #{all_graphql_fields_for('packages'.classify, excluded: ['project'])}
- metadata { #{query_graphql_fragment('ComposerMetadata')} }
- }
+ count
+ nodes {
+ #{all_graphql_fields_for('packages'.classify, excluded: ['project'])}
+ metadata { #{query_graphql_fragment('ComposerMetadata')} }
+ }
QUERY
end
@@ -47,7 +47,7 @@ RSpec.shared_examples 'group and project packages query' do
it 'returns packages successfully' do
expect(package_names).to contain_exactly(
- package.name,
+ npm_package.name,
maven_package.name,
debian_package.name,
composer_package.name
@@ -88,7 +88,23 @@ RSpec.shared_examples 'group and project packages query' do
end
describe 'sorting and pagination' do
- let_it_be(:ascending_packages) { [maven_package, package, composer_package, debian_package].map { |package| global_id_of(package)} }
+ let_it_be(:packages_order_map) do
+ {
+ TYPE_ASC: [maven_package, npm_package, composer_package, debian_package],
+ TYPE_DESC: [debian_package, composer_package, npm_package, maven_package],
+
+ NAME_ASC: [debian_package, maven_package, npm_package, composer_package],
+ NAME_DESC: [composer_package, npm_package, maven_package, debian_package],
+
+ VERSION_ASC: [composer_package, debian_package, maven_package, npm_package],
+ VERSION_DESC: [npm_package, maven_package, debian_package, composer_package],
+
+ CREATED_ASC: [npm_package, composer_package, debian_package, maven_package],
+ CREATED_DESC: [maven_package, debian_package, composer_package, npm_package]
+ }
+ end
+
+ let(:expected_packages) { sorted_packages.map { |package| global_id_of(package) } }
let(:data_path) { [resource_type, :packages] }
@@ -96,22 +112,14 @@ RSpec.shared_examples 'group and project packages query' do
resource.add_reporter(current_user)
end
- [:CREATED_ASC, :NAME_ASC, :VERSION_ASC, :TYPE_ASC].each do |order|
+ [:CREATED_ASC, :NAME_ASC, :VERSION_ASC, :TYPE_ASC, :CREATED_DESC, :NAME_DESC, :VERSION_DESC, :TYPE_DESC].each do |order|
context "#{order}" do
- it_behaves_like 'sorted paginated query' do
- let(:sort_param) { order }
- let(:first_param) { 4 }
- let(:all_records) { ascending_packages }
- end
- end
- end
+ let(:sorted_packages) { packages_order_map.fetch(order) }
- [:CREATED_DESC, :NAME_DESC, :VERSION_DESC, :TYPE_DESC].each do |order|
- context "#{order}" do
it_behaves_like 'sorted paginated query' do
let(:sort_param) { order }
let(:first_param) { 4 }
- let(:all_records) { ascending_packages.reverse }
+ let(:all_records) { expected_packages }
end
end
end
@@ -180,7 +188,7 @@ RSpec.shared_examples 'group and project packages query' do
context 'include_versionless' do
let(:params) { { include_versionless: true } }
- it { is_expected.to include({ "name" => versionaless_package.name }) }
+ it { is_expected.to include({ "name" => versionless_package.name }) }
end
end
end
diff --git a/spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb
index 9385706d991..ab93f54111b 100644
--- a/spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb
@@ -49,17 +49,5 @@ RSpec.shared_examples 'a package with files' do
expect(response_package_file_ids).not_to include(package_file_pending_destruction.to_global_id.to_s)
end
-
- context 'with packages_installable_package_files disabled' do
- before(:context) do
- stub_feature_flags(packages_installable_package_files: false)
- end
-
- it 'returns them' do
- expect(package.reload.package_files).to include(package_file_pending_destruction)
-
- expect(response_package_file_ids).to include(package_file_pending_destruction.to_global_id.to_s)
- end
- end
end
end
diff --git a/spec/support/shared_examples/requests/api/notes_shared_examples.rb b/spec/support/shared_examples/requests/api/notes_shared_examples.rb
index 0434d0beb7e..2a157f6e855 100644
--- a/spec/support/shared_examples/requests/api/notes_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/notes_shared_examples.rb
@@ -190,7 +190,7 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name|
if parent_type == 'projects'
context 'by a project owner' do
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
it 'sets the creation time on the new note' do
post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user), params: params
diff --git a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb
index b294467d482..c6c6c44dce8 100644
--- a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb
+++ b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb
@@ -580,3 +580,88 @@ RSpec.shared_examples 'rate-limited unauthenticated requests' do
end
end
end
+
+# Requires let variables:
+# * throttle_setting_prefix: "throttle_authenticated", "throttle_unauthenticated"
+RSpec.shared_examples 'rate-limited frontend API requests' do
+ let(:requests_per_period) { 1 }
+ let(:csrf_token) { SecureRandom.base64(ActionController::RequestForgeryProtection::AUTHENTICITY_TOKEN_LENGTH) }
+ let(:csrf_session) { { _csrf_token: csrf_token } }
+ let(:personal_access_token) { nil }
+
+ let(:api_path) { '/projects' }
+
+ # These don't actually exist, so a 404 is the expected response.
+ let(:files_api_path) { '/projects/1/repository/files/ref/path' }
+ let(:packages_api_path) { '/projects/1/packages/foo' }
+ let(:deprecated_api_path) { '/groups/1?with_projects=true' }
+
+ def get_api(path: api_path, csrf: false)
+ headers = csrf ? { 'X-CSRF-Token' => csrf_token } : nil
+ get api(path, personal_access_token: personal_access_token), headers: headers
+ end
+
+ def expect_not_found(&block)
+ yield
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ before do
+ stub_application_setting(
+ "#{throttle_setting_prefix}_enabled" => true,
+ "#{throttle_setting_prefix}_requests_per_period" => requests_per_period,
+ "#{throttle_setting_prefix}_api_enabled" => true,
+ "#{throttle_setting_prefix}_api_requests_per_period" => requests_per_period,
+ "#{throttle_setting_prefix}_web_enabled" => true,
+ "#{throttle_setting_prefix}_web_requests_per_period" => requests_per_period,
+ "#{throttle_setting_prefix}_files_api_enabled" => true,
+ "#{throttle_setting_prefix}_packages_api_enabled" => true,
+ "#{throttle_setting_prefix}_deprecated_api_enabled" => true
+ )
+
+ stub_session(csrf_session)
+ end
+
+ context 'with a CSRF token' do
+ it 'uses the rate limit for web requests' do
+ requests_per_period.times { get_api csrf: true }
+
+ expect_rejection("#{throttle_setting_prefix}_web") { get_api csrf: true }
+ expect_rejection("#{throttle_setting_prefix}_web") { get_api csrf: true, path: files_api_path }
+ expect_rejection("#{throttle_setting_prefix}_web") { get_api csrf: true, path: packages_api_path }
+ expect_rejection("#{throttle_setting_prefix}_web") { get_api csrf: true, path: deprecated_api_path }
+
+ # API rate limit is not triggered yet
+ expect_ok { get_api }
+ expect_not_found { get_api path: files_api_path }
+ expect_not_found { get_api path: packages_api_path }
+ expect_not_found { get_api path: deprecated_api_path }
+ end
+
+ context 'without a CSRF session' do
+ let(:csrf_session) { nil }
+
+ it 'always uses the rate limit for API requests' do
+ requests_per_period.times { get_api csrf: true }
+
+ expect_rejection("#{throttle_setting_prefix}_api") { get_api csrf: true }
+ expect_rejection("#{throttle_setting_prefix}_api") { get_api }
+ end
+ end
+ end
+
+ context 'without a CSRF token' do
+ it 'uses the rate limit for API requests' do
+ requests_per_period.times { get_api }
+
+ expect_rejection("#{throttle_setting_prefix}_api") { get_api }
+
+ # Web and custom API rate limits are not triggered yet
+ expect_ok { get_api csrf: true }
+ expect_not_found { get_api path: files_api_path }
+ expect_not_found { get_api path: packages_api_path }
+ expect_not_found { get_api path: deprecated_api_path }
+ end
+ end
+end
diff --git a/spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb b/spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb
index d9b837258ce..a46c2f0ac5c 100644
--- a/spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb
@@ -140,19 +140,6 @@ RSpec.shared_examples 'issues move service' do |group|
expect(issue2.reload.updated_at.change(usec: 0)).to eq updated_at2.change(usec: 0)
end
- if group
- context 'when on a group board' do
- it 'sends the board_group_id parameter' do
- params.merge!(move_after_id: issue1.id, move_before_id: issue2.id)
-
- match_params = { move_between_ids: [issue1.id, issue2.id], board_group_id: parent.id }
- expect(Issues::UpdateService).to receive(:new).with(project: issue.project, current_user: user, params: match_params).and_return(double(execute: build(:issue)))
-
- described_class.new(parent, user, params).execute(issue)
- end
- end
- end
-
def reorder_issues(params, issues: [])
issues.each do |issue|
issue.move_to_end && issue.save!
diff --git a/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb b/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb
index 87bf134eeb8..c808b9a5318 100644
--- a/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb
@@ -71,7 +71,6 @@ end
RSpec.shared_examples 'an accessible' do
before do
stub_feature_flags(container_registry_migration_phase1: false)
- stub_feature_flags(container_registry_cdn_redirect: false)
end
let(:access) do
@@ -164,10 +163,9 @@ RSpec.shared_examples 'a container registry auth service' do
before do
stub_feature_flags(container_registry_migration_phase1: false)
- stub_feature_flags(container_registry_cdn_redirect: false)
end
- describe '#full_access_token' do
+ describe '.full_access_token' do
let_it_be(:project) { create(:project) }
let(:token) { described_class.full_access_token(project.full_path) }
@@ -181,7 +179,26 @@ RSpec.shared_examples 'a container registry auth service' do
it_behaves_like 'not a container repository factory'
end
- describe '#pull_access_token' do
+ describe '.import_access_token' do
+ let(:access) do
+ [{ 'type' => 'registry',
+ 'name' => 'import',
+ 'actions' => ['*'] }]
+ end
+
+ let(:token) { described_class.import_access_token }
+
+ subject { { token: token } }
+
+ it_behaves_like 'a valid token'
+ it_behaves_like 'not a container repository factory'
+
+ it 'has the correct scope' do
+ expect(payload).to include('access' => access)
+ end
+ end
+
+ describe '.pull_access_token' do
let_it_be(:project) { create(:project) }
let(:token) { described_class.pull_access_token(project.full_path) }
@@ -1126,4 +1143,72 @@ RSpec.shared_examples 'a container registry auth service' do
end
end
end
+
+ context 'when importing' do
+ let_it_be(:container_repository) { create(:container_repository, :root, :importing) }
+ let_it_be(:current_project) { container_repository.project }
+ let_it_be(:current_user) { create(:user) }
+
+ before do
+ current_project.add_developer(current_user)
+ end
+
+ shared_examples 'containing the import error' do
+ it 'includes a helpful error message' do
+ expect(subject[:errors].first).to include(message: /Your repository is currently being migrated/)
+ end
+ end
+
+ context 'push request' do
+ let(:current_params) do
+ { scopes: ["repository:#{container_repository.path}:push"] }
+ end
+
+ it_behaves_like 'a forbidden' do
+ it_behaves_like 'containing the import error'
+ end
+ end
+
+ context 'delete request' do
+ let(:current_params) do
+ { scopes: ["repository:#{container_repository.path}:delete"] }
+ end
+
+ it_behaves_like 'a forbidden' do
+ it_behaves_like 'containing the import error'
+ end
+ end
+
+ context '* request' do
+ let(:current_params) do
+ { scopes: ["repository:#{container_repository.path}:*"] }
+ end
+
+ it_behaves_like 'a forbidden' do
+ it_behaves_like 'containing the import error'
+ end
+ end
+
+ context 'pull request' do
+ let(:current_params) do
+ { scopes: ["repository:#{container_repository.path}:pull"] }
+ end
+
+ let(:project) { current_project }
+
+ it_behaves_like 'a pullable'
+ end
+
+ context 'mixed request' do
+ let(:current_params) do
+ { scopes: ["repository:#{container_repository.path}:pull,push"] }
+ end
+
+ let(:project) { current_project }
+
+ it_behaves_like 'a forbidden' do
+ it_behaves_like 'containing the import error'
+ end
+ end
+ end
end
diff --git a/spec/support/shared_examples/services/incident_shared_examples.rb b/spec/support/shared_examples/services/incident_shared_examples.rb
index 36b0acf5a51..cc26cf87322 100644
--- a/spec/support/shared_examples/services/incident_shared_examples.rb
+++ b/spec/support/shared_examples/services/incident_shared_examples.rb
@@ -28,28 +28,15 @@ end
#
# include_examples 'not an incident issue'
RSpec.shared_examples 'not an incident issue' do
- let(:label_properties) { attributes_for(:label, :incident) }
-
it 'has not incident as issue type' do
expect(issue.issue_type).not_to eq('incident')
expect(issue.work_item_type.base_type).not_to eq('incident')
end
-
- it_behaves_like 'does not have incident label'
-end
-
-RSpec.shared_examples 'does not have incident label' do
- let(:label_properties) { attributes_for(:label, :incident) }
-
- it 'has not an incident label' do
- expect(issue.labels).not_to include(have_attributes(label_properties))
- end
end
# This shared example is to test the execution of incident management label services
# For example:
# - IncidentManagement::CreateIncidentSlaExceededLabelService
-# - IncidentManagement::CreateIncidentLabelService
# It doesn't require any defined variables
diff --git a/spec/support/shared_examples/views/registration_features_prompt_shared_examples.rb b/spec/support/shared_examples/views/registration_features_prompt_shared_examples.rb
new file mode 100644
index 00000000000..661a96266f1
--- /dev/null
+++ b/spec/support/shared_examples/views/registration_features_prompt_shared_examples.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'renders registration features prompt' do |disabled_field|
+ it 'renders a placeholder input with registration features message', :aggregate_failures do
+ render
+
+ if disabled_field
+ expect(rendered).to have_field(disabled_field, disabled: true)
+ end
+
+ expect(rendered).to have_content(s_("RegistrationFeatures|Want to %{feature_title} for free?") % { feature_title: s_('RegistrationFeatures|use this feature') })
+ expect(rendered).to have_link(s_('RegistrationFeatures|Registration Features Program'))
+ end
+end
+
+RSpec.shared_examples 'does not render registration features prompt' do |disabled_field|
+ it 'does not render a placeholder input with registration features message', :aggregate_failures do
+ render
+
+ if disabled_field
+ expect(rendered).not_to have_field(disabled_field, disabled: true)
+ end
+
+ expect(rendered).not_to have_content(s_("RegistrationFeatures|Want to %{feature_title} for free?") % { feature_title: s_('RegistrationFeatures|use this feature') })
+ expect(rendered).not_to have_link(s_('RegistrationFeatures|Registration Features Program'))
+ end
+end
diff --git a/spec/support/shared_examples/workers/background_migration_worker_shared_examples.rb b/spec/support/shared_examples/workers/background_migration_worker_shared_examples.rb
index 0d3e158d358..7fdf049a823 100644
--- a/spec/support/shared_examples/workers/background_migration_worker_shared_examples.rb
+++ b/spec/support/shared_examples/workers/background_migration_worker_shared_examples.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-RSpec.shared_examples 'it runs background migration jobs' do |tracking_database, metric_name|
+RSpec.shared_examples 'it runs background migration jobs' do |tracking_database|
describe 'defining the job attributes' do
it 'defines the data_consistency as always' do
expect(described_class.get_data_consistency).to eq(:always)
@@ -33,16 +33,6 @@ RSpec.shared_examples 'it runs background migration jobs' do |tracking_database,
end
end
- describe '.unhealthy_metric_name' do
- it 'does not raise an error' do
- expect { described_class.unhealthy_metric_name }.not_to raise_error
- end
-
- it 'overrides the method to return the unhealthy metric name' do
- expect(described_class.unhealthy_metric_name).to eq(metric_name)
- end
- end
-
describe '.minimum_interval' do
it 'returns 2 minutes' do
expect(described_class.minimum_interval).to eq(2.minutes.to_i)
@@ -189,11 +179,11 @@ RSpec.shared_examples 'it runs background migration jobs' do |tracking_database,
end
it 'increments the unhealthy counter' do
- counter = Gitlab::Metrics.counter(metric_name, 'msg')
+ counter = Gitlab::Metrics.counter(:background_migration_database_health_reschedules, 'msg')
expect(described_class).to receive(:perform_in)
- expect { worker.perform('Foo', [10, 20]) }.to change { counter.get }.by(1)
+ expect { worker.perform('Foo', [10, 20]) }.to change { counter.get(db_config_name: tracking_database) }.by(1)
end
context 'when lease_attempts is 0' do
diff --git a/spec/support/shared_examples/workers/project_export_shared_examples.rb b/spec/support/shared_examples/workers/project_export_shared_examples.rb
index a9bcc3f4f7c..175ef9bd012 100644
--- a/spec/support/shared_examples/workers/project_export_shared_examples.rb
+++ b/spec/support/shared_examples/workers/project_export_shared_examples.rb
@@ -53,6 +53,10 @@ RSpec.shared_examples 'export worker' do
it 'does not raise an exception when strategy is invalid' do
expect(::Projects::ImportExport::ExportService).not_to receive(:new)
+ expect_next_instance_of(ProjectExportJob) do |job|
+ expect(job).to receive(:finish)
+ end
+
expect { subject.perform(user.id, project.id, { 'klass' => 'Whatever' }) }.not_to raise_error
end
@@ -63,6 +67,18 @@ RSpec.shared_examples 'export worker' do
it 'does not raise error when user cannot be found' do
expect { subject.perform(non_existing_record_id, project.id, {}) }.not_to raise_error
end
+
+ it 'fails the export job status' do
+ expect_next_instance_of(::Projects::ImportExport::ExportService) do |service|
+ expect(service).to receive(:execute).and_raise(Gitlab::ImportExport::Error)
+ end
+
+ expect_next_instance_of(ProjectExportJob) do |job|
+ expect(job).to receive(:fail_op)
+ end
+
+ expect { subject.perform(user.id, project.id, {}) }.to raise_error(Gitlab::ImportExport::Error)
+ end
end
end
diff --git a/spec/support/stub_settings_source.rb b/spec/support/stub_settings_source.rb
new file mode 100644
index 00000000000..c0e4e468b90
--- /dev/null
+++ b/spec/support/stub_settings_source.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+RSpec.configure do |config|
+ config.around(:each, stub_settings_source: true) do |example|
+ original_instance = ::Settings.instance_variable_get(:@instance)
+
+ example.run
+
+ ::Settings.instance_variable_set(:@instance, original_instance)
+ end
+end
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index c5e73aa3b45..e9aa8cbb991 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -4,7 +4,8 @@ require 'rake_helper'
RSpec.describe 'gitlab:app namespace rake task', :delete do
let(:enable_registry) { true }
- let(:backup_types) { %w{db repo uploads builds artifacts pages lfs terraform_state registry packages} }
+ let(:backup_tasks) { %w{db repo uploads builds artifacts pages lfs terraform_state registry packages} }
+ let(:backup_types) { %w{db repositories uploads builds artifacts pages lfs terraform_state registry packages} }
def tars_glob
Dir.glob(File.join(Gitlab.config.backup.path, '*_gitlab_backup.tar'))
@@ -48,7 +49,7 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
end
def reenable_backup_sub_tasks
- backup_types.each do |subtask|
+ backup_tasks.each do |subtask|
Rake::Task["gitlab:backup:#{subtask}:create"].reenable
end
end
@@ -72,8 +73,11 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
allow(YAML).to receive(:load_file)
.and_return({ gitlab_version: gitlab_version })
expect(Rake::Task['gitlab:db:drop_tables']).to receive(:invoke)
- backup_types.each do |subtask|
- expect(Rake::Task["gitlab:backup:#{subtask}:restore"]).to receive(:invoke)
+ expect_next_instance_of(::Backup::Manager) do |instance|
+ backup_types.each do |subtask|
+ expect(instance).to receive(:run_restore_task).with(subtask).ordered
+ end
+ expect(instance).not_to receive(:run_restore_task)
end
expect(Rake::Task['gitlab:shell:setup']).to receive(:invoke)
end
@@ -128,16 +132,14 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
.and_return({ gitlab_version: Gitlab::VERSION })
expect(Rake::Task['gitlab:db:drop_tables']).to receive(:invoke)
- expect(Rake::Task['gitlab:backup:db:restore']).to receive(:invoke)
- expect(Rake::Task['gitlab:backup:repo:restore']).to receive(:invoke)
- expect(Rake::Task['gitlab:backup:builds:restore']).to receive(:invoke)
- expect(Rake::Task['gitlab:backup:uploads:restore']).to receive(:invoke)
- expect(Rake::Task['gitlab:backup:artifacts:restore']).to receive(:invoke)
- expect(Rake::Task['gitlab:backup:pages:restore']).to receive(:invoke)
- expect(Rake::Task['gitlab:backup:lfs:restore']).to receive(:invoke)
- expect(Rake::Task['gitlab:backup:terraform_state:restore']).to receive(:invoke)
- expect(Rake::Task['gitlab:backup:registry:restore']).to receive(:invoke)
- expect(Rake::Task['gitlab:backup:packages:restore']).to receive(:invoke)
+
+ expect_next_instance_of(::Backup::Manager) do |instance|
+ backup_types.each do |subtask|
+ expect(instance).to receive(:run_restore_task).with(subtask).ordered
+ end
+ expect(instance).not_to receive(:run_restore_task)
+ end
+
expect(Rake::Task['gitlab:shell:setup']).to receive(:invoke)
end
@@ -198,7 +200,7 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
context 'specific backup tasks' do
it 'prints a progress message to stdout' do
- backup_types.each do |task|
+ backup_tasks.each do |task|
expect { run_rake_task("gitlab:backup:#{task}:create") }.to output(/Dumping /).to_stdout_from_any_process
end
end
@@ -206,7 +208,7 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
it 'logs the progress to log file' do
expect(Gitlab::BackupLogger).to receive(:info).with(message: "Dumping database ... ")
expect(Gitlab::BackupLogger).to receive(:info).with(message: "[SKIPPED]")
- expect(Gitlab::BackupLogger).to receive(:info).with(message: "Dumping repositories ...")
+ expect(Gitlab::BackupLogger).to receive(:info).with(message: "Dumping repositories ... ")
expect(Gitlab::BackupLogger).to receive(:info).with(message: "Dumping uploads ... ")
expect(Gitlab::BackupLogger).to receive(:info).with(message: "Dumping builds ... ")
expect(Gitlab::BackupLogger).to receive(:info).with(message: "Dumping artifacts ... ")
@@ -217,7 +219,7 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
expect(Gitlab::BackupLogger).to receive(:info).with(message: "Dumping packages ... ")
expect(Gitlab::BackupLogger).to receive(:info).with(message: "done").exactly(9).times
- backup_types.each do |task|
+ backup_tasks.each do |task|
run_rake_task("gitlab:backup:#{task}:create")
end
end
@@ -344,9 +346,9 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
shared_examples 'includes repositories in all repository storages' do
specify :aggregate_failures do
project_a = create(:project, :repository)
- project_snippet_a = create(:project_snippet, :repository, project: project_a, author: project_a.owner)
+ project_snippet_a = create(:project_snippet, :repository, project: project_a, author: project_a.first_owner)
project_b = create(:project, :repository, repository_storage: second_storage_name)
- project_snippet_b = create(:project_snippet, :repository, project: project_b, author: project_b.owner)
+ project_snippet_b = create(:project_snippet, :repository, project: project_b, author: project_b.first_owner)
project_snippet_b.snippet_repository.update!(shard: project_b.project_repository.shard)
create(:wiki_page, container: project_a)
create(:design, :with_file, issue: create(:issue, project: project_a))
@@ -414,11 +416,9 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
end
it 'has defaults' do
- expect_next_instance_of(::Backup::Repositories) do |instance|
- expect(instance).to receive(:dump)
- .with(max_concurrency: 1, max_storage_concurrency: 1)
- .and_call_original
- end
+ expect(::Backup::Repositories).to receive(:new)
+ .with(anything, strategy: anything, max_concurrency: 1, max_storage_concurrency: 1)
+ .and_call_original
expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout_from_any_process
end
@@ -432,11 +432,9 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
stub_env('GITLAB_BACKUP_MAX_CONCURRENCY', 5)
stub_env('GITLAB_BACKUP_MAX_STORAGE_CONCURRENCY', 2)
- expect_next_instance_of(::Backup::Repositories) do |instance|
- expect(instance).to receive(:dump)
- .with(max_concurrency: 5, max_storage_concurrency: 2)
- .and_call_original
- end
+ expect(::Backup::Repositories).to receive(:new)
+ .with(anything, strategy: anything, max_concurrency: 5, max_storage_concurrency: 2)
+ .and_call_original
expect(::Backup::GitalyBackup).to receive(:new).with(anything, max_parallelism: 5, storage_parallelism: 2).and_call_original
expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout_from_any_process
@@ -489,16 +487,12 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
.to receive(:invoke).and_return(true)
expect(Rake::Task['gitlab:db:drop_tables']).to receive :invoke
- expect(Rake::Task['gitlab:backup:db:restore']).to receive :invoke
- expect(Rake::Task['gitlab:backup:repo:restore']).not_to receive :invoke
- expect(Rake::Task['gitlab:backup:uploads:restore']).not_to receive :invoke
- expect(Rake::Task['gitlab:backup:builds:restore']).to receive :invoke
- expect(Rake::Task['gitlab:backup:artifacts:restore']).to receive :invoke
- expect(Rake::Task['gitlab:backup:pages:restore']).to receive :invoke
- expect(Rake::Task['gitlab:backup:lfs:restore']).to receive :invoke
- expect(Rake::Task['gitlab:backup:terraform_state:restore']).to receive :invoke
- expect(Rake::Task['gitlab:backup:registry:restore']).to receive :invoke
- expect(Rake::Task['gitlab:backup:packages:restore']).to receive :invoke
+ expect_next_instance_of(::Backup::Manager) do |instance|
+ (backup_types - %w{repositories uploads}).each do |subtask|
+ expect(instance).to receive(:run_restore_task).with(subtask).ordered
+ end
+ expect(instance).not_to receive(:run_restore_task)
+ end
expect(Rake::Task['gitlab:shell:setup']).to receive :invoke
expect { run_rake_task('gitlab:backup:restore') }.to output.to_stdout_from_any_process
end
@@ -538,8 +532,11 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
.to receive(:invoke).and_return(true)
expect(Rake::Task['gitlab:db:drop_tables']).to receive :invoke
- backup_types.each do |subtask|
- expect(Rake::Task["gitlab:backup:#{subtask}:restore"]).to receive :invoke
+ expect_next_instance_of(::Backup::Manager) do |instance|
+ backup_types.each do |subtask|
+ expect(instance).to receive(:run_restore_task).with(subtask).ordered
+ end
+ expect(instance).not_to receive(:run_restore_task)
end
expect(Rake::Task['gitlab:shell:setup']).to receive :invoke
expect { run_rake_task("gitlab:backup:restore") }.to output.to_stdout_from_any_process
diff --git a/spec/tasks/gitlab/db_rake_spec.rb b/spec/tasks/gitlab/db_rake_spec.rb
index 92c896b1ab0..c3fd8135ae0 100644
--- a/spec/tasks/gitlab/db_rake_spec.rb
+++ b/spec/tasks/gitlab/db_rake_spec.rb
@@ -20,6 +20,99 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout do
allow(Rake::Task['db:seed_fu']).to receive(:invoke).and_return(true)
end
+ describe 'mark_migration_complete' do
+ context 'with a single database' do
+ let(:main_model) { ActiveRecord::Base }
+
+ before do
+ skip_if_multiple_databases_are_setup
+ end
+
+ it 'marks the migration complete on the given database' do
+ expect(main_model.connection).to receive(:quote).and_call_original
+ expect(main_model.connection).to receive(:execute)
+ .with("INSERT INTO schema_migrations (version) VALUES ('123')")
+
+ run_rake_task('gitlab:db:mark_migration_complete', '[123]')
+ end
+ end
+
+ context 'with multiple databases' do
+ let(:main_model) { double(:model, connection: double(:connection)) }
+ let(:ci_model) { double(:model, connection: double(:connection)) }
+ let(:base_models) { { 'main' => main_model, 'ci' => ci_model } }
+
+ before do
+ skip_if_multiple_databases_not_setup
+
+ allow(Gitlab::Database).to receive(:database_base_models).and_return(base_models)
+ end
+
+ it 'marks the migration complete on each database' do
+ expect(main_model.connection).to receive(:quote).with('123').and_return("'123'")
+ expect(main_model.connection).to receive(:execute)
+ .with("INSERT INTO schema_migrations (version) VALUES ('123')")
+
+ expect(ci_model.connection).to receive(:quote).with('123').and_return("'123'")
+ expect(ci_model.connection).to receive(:execute)
+ .with("INSERT INTO schema_migrations (version) VALUES ('123')")
+
+ run_rake_task('gitlab:db:mark_migration_complete', '[123]')
+ end
+
+ context 'when the single database task is used' do
+ it 'marks the migration complete for the given database' do
+ expect(main_model.connection).to receive(:quote).with('123').and_return("'123'")
+ expect(main_model.connection).to receive(:execute)
+ .with("INSERT INTO schema_migrations (version) VALUES ('123')")
+
+ expect(ci_model.connection).not_to receive(:quote)
+ expect(ci_model.connection).not_to receive(:execute)
+
+ run_rake_task('gitlab:db:mark_migration_complete:main', '[123]')
+ end
+ end
+ end
+
+ context 'when the migration is already marked complete' do
+ let(:main_model) { double(:model, connection: double(:connection)) }
+ let(:base_models) { { 'main' => main_model } }
+
+ before do
+ allow(Gitlab::Database).to receive(:database_base_models).and_return(base_models)
+ end
+
+ it 'prints a warning message' do
+ allow(main_model.connection).to receive(:quote).with('123').and_return("'123'")
+
+ expect(main_model.connection).to receive(:execute)
+ .with("INSERT INTO schema_migrations (version) VALUES ('123')")
+ .and_raise(ActiveRecord::RecordNotUnique)
+
+ expect { run_rake_task('gitlab:db:mark_migration_complete', '[123]') }
+ .to output(/Migration version '123' is already marked complete on database main/).to_stdout
+ end
+ end
+
+ context 'when an invalid version is given' do
+ let(:main_model) { double(:model, connection: double(:connection)) }
+ let(:base_models) { { 'main' => main_model } }
+
+ before do
+ allow(Gitlab::Database).to receive(:database_base_models).and_return(base_models)
+ end
+
+ it 'prints an error and exits' do
+ expect(main_model).not_to receive(:quote)
+ expect(main_model.connection).not_to receive(:execute)
+
+ expect { run_rake_task('gitlab:db:mark_migration_complete', '[abc]') }
+ .to output(/Must give a version argument that is a non-zero integer/).to_stdout
+ .and raise_error(SystemExit) { |error| expect(error.status).to eq(1) }
+ end
+ end
+ end
+
describe 'configure' do
it 'invokes db:migrate when schema has already been loaded' do
allow(ActiveRecord::Base.connection).to receive(:tables).and_return(%w[table1 table2])
@@ -353,6 +446,44 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout do
end
end
+ describe 'gitlab:db:reset_as_non_superuser' do
+ let(:connection_pool) { instance_double(ActiveRecord::ConnectionAdapters::ConnectionPool ) }
+ let(:connection) { instance_double(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter) }
+ let(:configurations) { double(ActiveRecord::DatabaseConfigurations) }
+ let(:configuration) { instance_double(ActiveRecord::DatabaseConfigurations::HashConfig) }
+ let(:config_hash) { { username: 'foo' } }
+
+ it 'migrate as nonsuperuser check with default username' do
+ allow(Rake::Task['db:drop']).to receive(:invoke)
+ allow(Rake::Task['db:create']).to receive(:invoke)
+ allow(ActiveRecord::Base).to receive(:configurations).and_return(configurations)
+ allow(configurations).to receive(:configs_for).and_return([configuration])
+ allow(configuration).to receive(:configuration_hash).and_return(config_hash)
+ allow(ActiveRecord::Base).to receive(:establish_connection).and_return(connection_pool)
+
+ expect(config_hash).to receive(:merge).with({ username: 'gitlab' })
+ expect(Gitlab::Database).to receive(:check_for_non_superuser)
+ expect(Rake::Task['db:migrate']).to receive(:invoke)
+
+ run_rake_task('gitlab:db:reset_as_non_superuser')
+ end
+
+ it 'migrate as nonsuperuser check with specified username' do
+ allow(Rake::Task['db:drop']).to receive(:invoke)
+ allow(Rake::Task['db:create']).to receive(:invoke)
+ allow(ActiveRecord::Base).to receive(:configurations).and_return(configurations)
+ allow(configurations).to receive(:configs_for).and_return([configuration])
+ allow(configuration).to receive(:configuration_hash).and_return(config_hash)
+ allow(ActiveRecord::Base).to receive(:establish_connection).and_return(connection_pool)
+
+ expect(config_hash).to receive(:merge).with({ username: 'foo' })
+ expect(Gitlab::Database).to receive(:check_for_non_superuser)
+ expect(Rake::Task['db:migrate']).to receive(:invoke)
+
+ run_rake_task('gitlab:db:reset_as_non_superuser', '[foo]')
+ end
+ end
+
def run_rake_task(task_name, arguments = '')
Rake::Task[task_name].reenable
Rake.application.invoke_task("#{task_name}#{arguments}")
diff --git a/spec/tasks/gitlab/dependency_proxy/migrate_rake_spec.rb b/spec/tasks/gitlab/dependency_proxy/migrate_rake_spec.rb
new file mode 100644
index 00000000000..edd56f1667f
--- /dev/null
+++ b/spec/tasks/gitlab/dependency_proxy/migrate_rake_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require 'rake_helper'
+
+RSpec.describe 'gitlab:dependency_proxy namespace rake task', :silence_stdout do
+ before :all do
+ Rake.application.rake_require 'tasks/gitlab/dependency_proxy/migrate'
+ end
+
+ describe 'migrate' do
+ let(:local) { ObjectStorage::Store::LOCAL }
+ let(:remote) { ObjectStorage::Store::REMOTE }
+ let!(:blob) { create(:dependency_proxy_blob) }
+ let!(:manifest) { create(:dependency_proxy_manifest) }
+
+ def dependency_proxy_migrate
+ run_rake_task('gitlab:dependency_proxy:migrate')
+ end
+
+ context 'object storage disabled' do
+ before do
+ stub_dependency_proxy_object_storage(enabled: false)
+ end
+
+ it "doesn't migrate files" do
+ expect { dependency_proxy_migrate }.to raise_error('Object store is disabled for dependency proxy feature')
+ end
+ end
+
+ context 'object storage enabled' do
+ before do
+ stub_dependency_proxy_object_storage
+ end
+
+ it 'migrates local file to object storage' do
+ expect { dependency_proxy_migrate }.to change { blob.reload.file_store }.from(local).to(remote)
+ .and change { manifest.reload.file_store }.from(local).to(remote)
+ end
+ end
+
+ context 'an error is raised while migrating' do
+ let(:blob_error) { 'Failed to transfer dependency proxy blob file' }
+ let(:manifest_error) { 'Failed to transfer dependency proxy manifest file' }
+ let!(:blob_non_existent) { create(:dependency_proxy_blob) }
+ let!(:manifest_non_existent) { create(:dependency_proxy_manifest) }
+
+ before do
+ stub_dependency_proxy_object_storage
+ blob_non_existent.file.file.delete
+ manifest_non_existent.file.file.delete
+ end
+
+ it 'fails to migrate a local file that does not exist' do
+ expect { dependency_proxy_migrate }.to output(include(blob_error, manifest_error)).to_stdout
+ end
+ end
+ end
+end
diff --git a/spec/tasks/gitlab/info_rake_spec.rb b/spec/tasks/gitlab/info_rake_spec.rb
deleted file mode 100644
index 19ed43723e2..00000000000
--- a/spec/tasks/gitlab/info_rake_spec.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-# frozen_string_literal: true
-
-require 'rake_helper'
-
-RSpec.describe 'gitlab:env:info', :silence_stdout do
- before do
- Rake.application.rake_require 'tasks/gitlab/info'
-
- stub_warn_user_is_not_gitlab
- allow(Gitlab::Popen).to receive(:popen)
- end
-
- describe 'git version' do
- before do
- allow(Gitlab::Popen).to receive(:popen).with([Gitlab.config.git.bin_path, '--version'])
- .and_return(git_version)
- end
-
- context 'when git installed' do
- let(:git_version) { 'git version 2.10.0' }
-
- it 'prints git version' do
- run_rake_task('gitlab:env:info')
-
- expect($stdout.string).to match(/Git Version:(.*)2.10.0/)
- end
- end
-
- context 'when git not installed' do
- let(:git_version) { '' }
-
- it 'prints unknown' do
- run_rake_task('gitlab:env:info')
-
- expect($stdout.string).to match(/Git Version:(.*)unknown/)
- end
- end
- end
-end
diff --git a/spec/tooling/danger/project_helper_spec.rb b/spec/tooling/danger/project_helper_spec.rb
index 52aa90beb2b..1b416286f8e 100644
--- a/spec/tooling/danger/project_helper_spec.rb
+++ b/spec/tooling/danger/project_helper_spec.rb
@@ -209,6 +209,7 @@ RSpec.describe Tooling::Danger::ProjectHelper do
'lib/api/entities/project_integration.rb' | [:integrations_be, :backend]
'lib/gitlab/hook_data/note_builder.rb' | [:integrations_be, :backend]
'lib/gitlab/data_builder/note.rb' | [:integrations_be, :backend]
+ 'lib/gitlab/web_hooks/recursion_detection.rb' | [:integrations_be, :backend]
'ee/lib/ee/gitlab/integrations/sti_type.rb' | [:integrations_be, :backend]
'ee/lib/ee/api/helpers/integrations_helpers.rb' | [:integrations_be, :backend]
'ee/app/serializers/integrations/jira_serializers/issue_entity.rb' | [:integrations_be, :backend]
diff --git a/spec/tooling/docs/deprecation_handling_spec.rb b/spec/tooling/docs/deprecation_handling_spec.rb
index e389fe882b2..e43f5c7147b 100644
--- a/spec/tooling/docs/deprecation_handling_spec.rb
+++ b/spec/tooling/docs/deprecation_handling_spec.rb
@@ -1,12 +1,10 @@
# frozen_string_literal: true
-require_relative '../../fast_spec_helper'
+require 'spec_helper'
+
require_relative '../../../tooling/docs/deprecation_handling'
-require_relative '../../support/helpers/next_instance_of'
RSpec.describe Docs::DeprecationHandling do
- include ::NextInstanceOf
-
let(:type) { 'deprecation' }
subject { described_class.new(type).render }
diff --git a/spec/tooling/lib/tooling/test_map_generator_spec.rb b/spec/tooling/lib/tooling/test_map_generator_spec.rb
index eb49b1db20e..b52d78b01a3 100644
--- a/spec/tooling/lib/tooling/test_map_generator_spec.rb
+++ b/spec/tooling/lib/tooling/test_map_generator_spec.rb
@@ -39,11 +39,23 @@ RSpec.describe Tooling::TestMapGenerator do
YAML
end
+ let(:yaml3) do
+ <<~YAML
+ ---
+ :type: Crystalball::ExecutionMap
+ :commit: 74056e8d9cf3773f43faa1cf5416f8779c8284c9
+ :timestamp: 1602671965
+ :version:
+ ---
+ YAML
+ end
+
let(:pathname) { instance_double(Pathname) }
before do
stub_file_read('yaml1.yml', content: yaml1)
stub_file_read('yaml2.yml', content: yaml2)
+ stub_file_read('yaml3.yml', content: yaml3)
end
context 'with single yaml' do
@@ -74,6 +86,10 @@ RSpec.describe Tooling::TestMapGenerator do
expect(subject.mapping[file]).to match_array(tests)
end
end
+
+ it 'displays a warning when report has no examples' do
+ expect { subject.parse('yaml3.yml') }.to output(%|No examples in yaml3.yml! Metadata: {:type=>"Crystalball::ExecutionMap", :commit=>"74056e8d9cf3773f43faa1cf5416f8779c8284c9", :timestamp=>1602671965, :version=>nil}\n|).to_stdout
+ end
end
context 'with multiple yamls' do
diff --git a/spec/tooling/quality/test_level_spec.rb b/spec/tooling/quality/test_level_spec.rb
index 8a944a473d7..33d3a5b49b3 100644
--- a/spec/tooling/quality/test_level_spec.rb
+++ b/spec/tooling/quality/test_level_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe Quality::TestLevel do
context 'when level is unit' do
it 'returns a pattern' do
expect(subject.pattern(:unit))
- .to eq("spec/{bin,channels,config,db,dependencies,elastic,elastic_integration,experiments,factories,finders,frontend,graphql,haml_lint,helpers,initializers,javascripts,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")
+ .to eq("spec/{bin,channels,config,db,dependencies,elastic,elastic_integration,experiments,events,factories,finders,frontend,graphql,haml_lint,helpers,initializers,javascripts,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
@@ -110,7 +110,7 @@ RSpec.describe Quality::TestLevel do
context 'when level is unit' do
it 'returns a regexp' do
expect(subject.regexp(:unit))
- .to eq(%r{spec/(bin|channels|config|db|dependencies|elastic|elastic_integration|experiments|factories|finders|frontend|graphql|haml_lint|helpers|initializers|javascripts|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)})
+ .to eq(%r{spec/(bin|channels|config|db|dependencies|elastic|elastic_integration|experiments|events|factories|finders|frontend|graphql|haml_lint|helpers|initializers|javascripts|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
diff --git a/spec/tooling/rspec_flaky/config_spec.rb b/spec/tooling/rspec_flaky/config_spec.rb
index 12b5ed74cb2..c95e5475d66 100644
--- a/spec/tooling/rspec_flaky/config_spec.rb
+++ b/spec/tooling/rspec_flaky/config_spec.rb
@@ -11,9 +11,10 @@ RSpec.describe RspecFlaky::Config, :aggregate_failures do
before do
# Stub these env variables otherwise specs don't behave the same on the CI
stub_env('FLAKY_RSPEC_GENERATE_REPORT', nil)
- stub_env('SUITE_FLAKY_RSPEC_REPORT_PATH', nil)
+ stub_env('FLAKY_RSPEC_SUITE_REPORT_PATH', nil)
stub_env('FLAKY_RSPEC_REPORT_PATH', nil)
stub_env('NEW_FLAKY_RSPEC_REPORT_PATH', nil)
+ stub_env('SKIPPED_FLAKY_TESTS_REPORT_PATH', nil)
# Ensure the behavior is the same locally and on CI (where Rails is defined since we run this test as part of the whole suite), i.e. Rails isn't defined
allow(described_class).to receive(:rails_path).and_wrap_original do |method, path|
path
@@ -51,15 +52,15 @@ RSpec.describe RspecFlaky::Config, :aggregate_failures do
end
describe '.suite_flaky_examples_report_path' do
- context "when ENV['SUITE_FLAKY_RSPEC_REPORT_PATH'] is not set" do
+ context "when ENV['FLAKY_RSPEC_SUITE_REPORT_PATH'] is not set" do
it 'returns the default path' do
- expect(described_class.suite_flaky_examples_report_path).to eq('rspec_flaky/suite-report.json')
+ expect(described_class.suite_flaky_examples_report_path).to eq('rspec/flaky/suite-report.json')
end
end
- context "when ENV['SUITE_FLAKY_RSPEC_REPORT_PATH'] is set" do
+ context "when ENV['FLAKY_RSPEC_SUITE_REPORT_PATH'] is set" do
before do
- stub_env('SUITE_FLAKY_RSPEC_REPORT_PATH', 'foo/suite-report.json')
+ stub_env('FLAKY_RSPEC_SUITE_REPORT_PATH', 'foo/suite-report.json')
end
it 'returns the value of the env variable' do
@@ -71,7 +72,7 @@ RSpec.describe RspecFlaky::Config, :aggregate_failures do
describe '.flaky_examples_report_path' do
context "when ENV['FLAKY_RSPEC_REPORT_PATH'] is not set" do
it 'returns the default path' do
- expect(described_class.flaky_examples_report_path).to eq('rspec_flaky/report.json')
+ expect(described_class.flaky_examples_report_path).to eq('rspec/flaky/report.json')
end
end
@@ -89,7 +90,7 @@ RSpec.describe RspecFlaky::Config, :aggregate_failures do
describe '.new_flaky_examples_report_path' do
context "when ENV['NEW_FLAKY_RSPEC_REPORT_PATH'] is not set" do
it 'returns the default path' do
- expect(described_class.new_flaky_examples_report_path).to eq('rspec_flaky/new-report.json')
+ expect(described_class.new_flaky_examples_report_path).to eq('rspec/flaky/new-report.json')
end
end
@@ -103,4 +104,22 @@ RSpec.describe RspecFlaky::Config, :aggregate_failures do
end
end
end
+
+ describe '.skipped_flaky_tests_report_path' do
+ context "when ENV['SKIPPED_FLAKY_TESTS_REPORT_PATH'] is not set" do
+ it 'returns the default path' do
+ expect(described_class.skipped_flaky_tests_report_path).to eq('rspec/flaky/skipped_flaky_tests_report.txt')
+ end
+ end
+
+ context "when ENV['SKIPPED_FLAKY_TESTS_REPORT_PATH'] is set" do
+ before do
+ stub_env('SKIPPED_FLAKY_TESTS_REPORT_PATH', 'foo/skipped_flaky_tests_report.txt')
+ end
+
+ it 'returns the value of the env variable' do
+ expect(described_class.skipped_flaky_tests_report_path).to eq('foo/skipped_flaky_tests_report.txt')
+ end
+ end
+ end
end
diff --git a/spec/tooling/rspec_flaky/listener_spec.rb b/spec/tooling/rspec_flaky/listener_spec.rb
index 51a815dafbf..62bbe53cac1 100644
--- a/spec/tooling/rspec_flaky/listener_spec.rb
+++ b/spec/tooling/rspec_flaky/listener_spec.rb
@@ -54,7 +54,7 @@ RSpec.describe RspecFlaky::Listener, :aggregate_failures do
before do
# Stub these env variables otherwise specs don't behave the same on the CI
stub_env('CI_JOB_URL', nil)
- stub_env('SUITE_FLAKY_RSPEC_REPORT_PATH', nil)
+ stub_env('FLAKY_RSPEC_SUITE_REPORT_PATH', nil)
end
describe '#initialize' do
@@ -73,11 +73,11 @@ RSpec.describe RspecFlaky::Listener, :aggregate_failures do
it_behaves_like 'a valid Listener instance'
end
- context 'when SUITE_FLAKY_RSPEC_REPORT_PATH is set' do
+ context 'when FLAKY_RSPEC_SUITE_REPORT_PATH is set' do
let(:report_file_path) { 'foo/report.json' }
before do
- stub_env('SUITE_FLAKY_RSPEC_REPORT_PATH', report_file_path)
+ stub_env('FLAKY_RSPEC_SUITE_REPORT_PATH', report_file_path)
end
context 'and report file exists' do
diff --git a/spec/uploaders/import_export_uploader_spec.rb b/spec/uploaders/import_export_uploader_spec.rb
index cb7a89193e6..64e92f5d60e 100644
--- a/spec/uploaders/import_export_uploader_spec.rb
+++ b/spec/uploaders/import_export_uploader_spec.rb
@@ -10,6 +10,20 @@ RSpec.describe ImportExportUploader do
subject { described_class.new(model, :import_file) }
context 'local store' do
+ describe '#move_to_cache' do
+ it 'returns false' do
+ expect(subject.move_to_cache).to be false
+ end
+
+ context 'with project export' do
+ subject { described_class.new(model, :export_file) }
+
+ it 'returns true' do
+ expect(subject.move_to_cache).to be true
+ end
+ end
+ end
+
describe '#move_to_store' do
it 'returns true' do
expect(subject.move_to_store).to be true
@@ -33,6 +47,20 @@ RSpec.describe ImportExportUploader do
let(:fixture) { File.join('spec', 'fixtures', 'group_export.tar.gz') }
end
+ describe '#move_to_cache' do
+ it 'returns false' do
+ expect(subject.move_to_cache).to be false
+ end
+
+ context 'with project export' do
+ subject { described_class.new(model, :export_file) }
+
+ it 'returns true' do
+ expect(subject.move_to_cache).to be false
+ end
+ end
+ end
+
describe '#move_to_store' do
it 'returns false' do
expect(subject.move_to_store).to be false
diff --git a/spec/validators/x509_certificate_credentials_validator_spec.rb b/spec/validators/x509_certificate_credentials_validator_spec.rb
index 9076aee7681..5da1813e379 100644
--- a/spec/validators/x509_certificate_credentials_validator_spec.rb
+++ b/spec/validators/x509_certificate_credentials_validator_spec.rb
@@ -55,6 +55,14 @@ RSpec.describe X509CertificateCredentialsValidator do
expect(record.errors[:private_key]).to include('could not read private key, is the passphrase correct?')
end
+ it 'adds an error when private key does not match certificate' do
+ record.private_key = SSHData::PrivateKey::RSA.generate(4096).openssl.to_pem
+
+ validator.validate(record)
+
+ expect(record.errors[:private_key]).to include('private key does not match certificate.')
+ end
+
it 'has no error when the private key is correct' do
record.private_key = pkey_data
@@ -85,7 +93,7 @@ RSpec.describe X509CertificateCredentialsValidator do
validator.validate(record)
- expect(record.errors[:private_key]).not_to be_empty
+ expect(record.errors[:private_key]).to include('could not read private key, is the passphrase correct?')
end
end
end
diff --git a/spec/views/admin/application_settings/general.html.haml_spec.rb b/spec/views/admin/application_settings/general.html.haml_spec.rb
index 434ca8bf0e7..7d28175d134 100644
--- a/spec/views/admin/application_settings/general.html.haml_spec.rb
+++ b/spec/views/admin/application_settings/general.html.haml_spec.rb
@@ -33,4 +33,31 @@ RSpec.describe 'admin/application_settings/general.html.haml' do
end
end
end
+
+ describe 'prompt user about registration features' do
+ before do
+ assign(:application_setting, app_settings)
+ allow(view).to receive(:current_user).and_return(user)
+ end
+
+ context 'when service ping is enabled' do
+ before do
+ stub_application_setting(usage_ping_enabled: true)
+ end
+
+ it_behaves_like 'does not render registration features prompt', :application_setting_disabled_repository_size_limit
+ end
+
+ context 'with no license and service ping disabled' do
+ before do
+ stub_application_setting(usage_ping_enabled: false)
+
+ if Gitlab.ee?
+ allow(License).to receive(:current).and_return(nil)
+ end
+ end
+
+ it_behaves_like 'renders registration features prompt', :application_setting_disabled_repository_size_limit
+ end
+ end
end
diff --git a/spec/views/admin/dashboard/index.html.haml_spec.rb b/spec/views/admin/dashboard/index.html.haml_spec.rb
index 9db2bd3741a..9f1ff960444 100644
--- a/spec/views/admin/dashboard/index.html.haml_spec.rb
+++ b/spec/views/admin/dashboard/index.html.haml_spec.rb
@@ -63,4 +63,33 @@ RSpec.describe 'admin/dashboard/index.html.haml' do
expect(rendered).to have_selector('.js-gitlab-version-check')
end
end
+
+ describe 'GitLab KAS' do
+ before do
+ allow(Gitlab::Kas).to receive(:enabled?).and_return(enabled)
+ allow(Gitlab::Kas).to receive(:version).and_return('kas-1.2.3')
+ end
+
+ context 'KAS enabled' do
+ let(:enabled) { true }
+
+ it 'includes KAS version' do
+ render
+
+ expect(rendered).to have_content('GitLab KAS')
+ expect(rendered).to have_content('kas-1.2.3')
+ end
+ end
+
+ context 'KAS disabled' do
+ let(:enabled) { false }
+
+ it 'does not include KAS version' do
+ render
+
+ expect(rendered).not_to have_content('GitLab KAS')
+ expect(rendered).not_to have_content('kas-1.2.3')
+ end
+ end
+ end
end
diff --git a/spec/views/devise/shared/_signup_box.html.haml_spec.rb b/spec/views/devise/shared/_signup_box.html.haml_spec.rb
index 6efb2730964..37dbfd39f2d 100644
--- a/spec/views/devise/shared/_signup_box.html.haml_spec.rb
+++ b/spec/views/devise/shared/_signup_box.html.haml_spec.rb
@@ -63,6 +63,22 @@ RSpec.describe 'devise/shared/_signup_box' do
end
end
+ context 'using the borderless option' do
+ let(:border_css_classes) { '.gl-border-gray-100.gl-border-1.gl-border-solid.gl-rounded-base' }
+
+ it 'renders with a border by default' do
+ render
+
+ expect(rendered).to have_selector(border_css_classes)
+ end
+
+ it 'renders without a border when borderless is truthy' do
+ render('devise/shared/signup_box', borderless: true)
+
+ expect(rendered).not_to have_selector(border_css_classes)
+ end
+ end
+
def stub_devise
allow(view).to receive(:devise_mapping).and_return(Devise.mappings[:user])
allow(view).to receive(:resource).and_return(spy)
diff --git a/spec/views/groups/settings/_transfer.html.haml_spec.rb b/spec/views/groups/settings/_transfer.html.haml_spec.rb
deleted file mode 100644
index 911eb5b7ab3..00000000000
--- a/spec/views/groups/settings/_transfer.html.haml_spec.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'groups/settings/_transfer.html.haml' do
- describe 'render' do
- it 'enables the Select parent group dropdown and does not show an alert for a group' do
- group = build(:group)
-
- render 'groups/settings/transfer', group: group
-
- expect(rendered).to have_button 'Select parent group'
- expect(rendered).not_to have_button 'Select parent group', disabled: true
- expect(rendered).not_to have_text "This group can't be transfered because it is linked to a subscription."
- end
- end
-end
diff --git a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
index f7da288b9f3..22e925e22ae 100644
--- a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
+++ b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'layouts/nav/sidebar/_project' do
let_it_be_with_reload(:project) { create(:project, :repository) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:current_ref) { 'master' }
before do
@@ -286,10 +286,20 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
end
describe 'Pipeline Editor' do
- it 'has a link to the pipeline editor' do
- render
+ context 'with a current_ref' do
+ it 'has a link to the pipeline editor' do
+ render
+
+ expect(rendered).to have_link('Editor', href: project_ci_pipeline_editor_path(project, params: { branch_name: current_ref }))
+ end
+ end
+
+ context 'with the default_branch' do
+ it 'has a link to the pipeline editor' do
+ render
- expect(rendered).to have_link('Editor', href: project_ci_pipeline_editor_path(project))
+ expect(rendered).to have_link('Editor', href: project_ci_pipeline_editor_path(project, params: { branch_name: project.default_branch }))
+ end
end
context 'when user cannot access pipeline editor' do
diff --git a/spec/views/profiles/keys/_key.html.haml_spec.rb b/spec/views/profiles/keys/_key.html.haml_spec.rb
index bb101198ac3..ed8026d2453 100644
--- a/spec/views/profiles/keys/_key.html.haml_spec.rb
+++ b/spec/views/profiles/keys/_key.html.haml_spec.rb
@@ -90,8 +90,8 @@ RSpec.describe 'profiles/keys/_key.html.haml' do
using RSpec::Parameterized::TableSyntax
where(:valid, :expiry, :result) do
- false | 2.days.from_now | 'Key type is forbidden. Must be DSA, ECDSA, or ED25519'
- false | 2.days.ago | 'Key type is forbidden. Must be DSA, ECDSA, or ED25519'
+ false | 2.days.from_now | 'Key type is forbidden. Must be DSA, ECDSA, ED25519, ECDSA_SK, or ED25519_SK'
+ false | 2.days.ago | 'Key type is forbidden. Must be DSA, ECDSA, ED25519, ECDSA_SK, or ED25519_SK'
true | 2.days.ago | 'Key usable beyond expiration date.'
true | 2.days.from_now | ''
end
diff --git a/spec/views/projects/edit.html.haml_spec.rb b/spec/views/projects/edit.html.haml_spec.rb
index 11f542767f4..a85ddf7a005 100644
--- a/spec/views/projects/edit.html.haml_spec.rb
+++ b/spec/views/projects/edit.html.haml_spec.rb
@@ -45,10 +45,10 @@ RSpec.describe 'projects/edit' do
end
context 'merge commit template' do
- it 'displays a placeholder if none is set' do
+ it 'displays default template if none is set' do
render
- expect(rendered).to have_field('project[merge_commit_template]', placeholder: <<~MSG.rstrip)
+ expect(rendered).to have_field('project[merge_commit_template_or_default]', with: <<~MSG.rstrip)
Merge branch '%{source_branch}' into '%{target_branch}'
%{title}
@@ -64,15 +64,15 @@ RSpec.describe 'projects/edit' do
render
- expect(rendered).to have_field('project[merge_commit_template]', with: '%{title}')
+ expect(rendered).to have_field('project[merge_commit_template_or_default]', with: '%{title}')
end
end
context 'squash template' do
- it 'displays a placeholder if none is set' do
+ it 'displays default template if none is set' do
render
- expect(rendered).to have_field('project[squash_commit_template]', placeholder: '%{title}')
+ expect(rendered).to have_field('project[squash_commit_template_or_default]', with: '%{title}')
end
it 'displays the user entered value' do
@@ -80,7 +80,7 @@ RSpec.describe 'projects/edit' do
render
- expect(rendered).to have_field('project[squash_commit_template]', with: '%{first_multiline_commit}')
+ expect(rendered).to have_field('project[squash_commit_template_or_default]', with: '%{first_multiline_commit}')
end
end
@@ -139,4 +139,26 @@ RSpec.describe 'projects/edit' do
end
end
end
+
+ describe 'prompt user about registration features' do
+ context 'when service ping is enabled' do
+ before do
+ stub_application_setting(usage_ping_enabled: true)
+ end
+
+ it_behaves_like 'does not render registration features prompt', :project_disabled_repository_size_limit
+ end
+
+ context 'with no license and service ping disabled' do
+ before do
+ stub_application_setting(usage_ping_enabled: false)
+
+ if Gitlab.ee?
+ allow(License).to receive(:current).and_return(nil)
+ end
+ end
+
+ it_behaves_like 'renders registration features prompt', :project_disabled_repository_size_limit
+ end
+ end
end
diff --git a/spec/views/projects/services/_form.haml_spec.rb b/spec/views/projects/services/_form.haml_spec.rb
deleted file mode 100644
index f212fd78b1a..00000000000
--- a/spec/views/projects/services/_form.haml_spec.rb
+++ /dev/null
@@ -1,52 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'projects/services/_form' do
- let(:project) { create(:redmine_project) }
- let(:user) { create(:admin) }
-
- before do
- assign(:project, project)
-
- allow(controller).to receive(:current_user).and_return(user)
-
- allow(view).to receive_messages(
- current_user: user,
- can?: true,
- current_application_settings: Gitlab::CurrentSettings.current_application_settings,
- integration: project.redmine_integration,
- request: double(referer: '/services')
- )
- end
-
- context 'integrations form' do
- it 'does not render form element' do
- render
-
- expect(rendered).not_to have_selector('[data-testid="integration-form"]')
- end
-
- context 'when vue_integration_form feature flag is disabled' do
- before do
- stub_feature_flags(vue_integration_form: false)
- end
-
- it 'renders form element' do
- render
-
- expect(rendered).to have_selector('[data-testid="integration-form"]')
- end
-
- context 'commit_events and merge_request_events' do
- it 'display merge_request_events and commit_events descriptions' do
- allow(Integrations::Redmine).to receive(:supported_events).and_return(%w(commit merge_request))
-
- render
-
- expect(rendered).to have_css("input[name='redirect_to'][value='/services']", count: 1, visible: false)
- end
- end
- end
- end
-end
diff --git a/spec/views/shared/_gl_toggle.haml_spec.rb b/spec/views/shared/_gl_toggle.haml_spec.rb
new file mode 100644
index 00000000000..3ac1ef30c84
--- /dev/null
+++ b/spec/views/shared/_gl_toggle.haml_spec.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe 'shared/_gl_toggle.html.haml' do
+ context 'defaults' do
+ before do
+ render partial: 'shared/gl_toggle', locals: {
+ classes: '.js-gl-toggle'
+ }
+ end
+
+ it 'does not set a name' do
+ expect(rendered).not_to have_selector('[data-name]')
+ end
+
+ it 'sets default is-checked attributes' do
+ expect(rendered).to have_selector('[data-is-checked="false"]')
+ end
+
+ it 'sets default disabled attributes' do
+ expect(rendered).to have_selector('[data-disabled="false"]')
+ end
+
+ it 'sets default is-loading attributes' do
+ expect(rendered).to have_selector('[data-is-loading="false"]')
+ end
+
+ it 'does not set a label' do
+ expect(rendered).not_to have_selector('[data-label]')
+ end
+
+ it 'does not set a label position' do
+ expect(rendered).not_to have_selector('[data-label-position]')
+ end
+ end
+
+ context 'with custom options' do
+ before do
+ render partial: 'shared/gl_toggle', locals: {
+ classes: 'js-custom-gl-toggle',
+ name: 'toggle-name',
+ is_checked: true,
+ disabled: true,
+ is_loading: true,
+ label: 'Custom label',
+ label_position: 'top',
+ data: {
+ foo: 'bar'
+ }
+ }
+ end
+
+ it 'sets the custom class' do
+ expect(rendered).to have_selector('.js-custom-gl-toggle')
+ end
+
+ it 'sets the custom name' do
+ expect(rendered).to have_selector('[data-name="toggle-name"]')
+ end
+
+ it 'sets the custom is-checked attributes' do
+ expect(rendered).to have_selector('[data-is-checked="true"]')
+ end
+
+ it 'sets the custom disabled attributes' do
+ expect(rendered).to have_selector('[data-disabled="true"]')
+ end
+
+ it 'sets the custom is-loading attributes' do
+ expect(rendered).to have_selector('[data-is-loading="true"]')
+ end
+
+ it 'sets the custom label' do
+ expect(rendered).to have_selector('[data-label="Custom label"]')
+ end
+
+ it 'sets the cutom label position' do
+ expect(rendered).to have_selector('[data-label-position="top"]')
+ end
+
+ it 'sets cutom data attributes' do
+ expect(rendered).to have_selector('[data-foo="bar"]')
+ end
+ end
+end
diff --git a/spec/views/shared/_global_alert.html.haml_spec.rb b/spec/views/shared/_global_alert.html.haml_spec.rb
index 7eec068645a..84198cbb75e 100644
--- a/spec/views/shared/_global_alert.html.haml_spec.rb
+++ b/spec/views/shared/_global_alert.html.haml_spec.rb
@@ -49,12 +49,6 @@ RSpec.describe 'shared/_global_alert.html.haml' do
allow(view).to receive(:fluid_layout).and_return(false)
end
- it 'does not add layout limited class' do
- render
-
- expect(rendered).not_to have_selector('.gl-alert-layout-limited')
- end
-
it 'adds container classes' do
render
@@ -74,10 +68,6 @@ RSpec.describe 'shared/_global_alert.html.haml' do
render
end
- it 'adds layout limited class' do
- expect(rendered).to have_selector('.gl-alert-layout-limited')
- end
-
it 'does not add container classes' do
expect(rendered).not_to have_selector('.container-fluid.container-limited')
end
diff --git a/spec/views/shared/issuable/_sidebar.html.haml_spec.rb b/spec/views/shared/issuable/_sidebar.html.haml_spec.rb
new file mode 100644
index 00000000000..2097b8890cc
--- /dev/null
+++ b/spec/views/shared/issuable/_sidebar.html.haml_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'shared/issuable/_sidebar.html.haml' do
+ let_it_be(:user) { create(:user) }
+
+ subject(:rendered) do
+ render 'shared/issuable/sidebar', issuable_sidebar: IssueSerializer.new(current_user: user)
+ .represent(issuable, serializer: 'sidebar'), assignees: []
+ end
+
+ context 'project in a group' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
+ let_it_be(:issue) { create(:issue, project: project) }
+ let_it_be(:incident) { create(:incident, project: project) }
+
+ before do
+ assign(:project, project)
+ end
+
+ context 'issuable that does not support escalations' do
+ let(:issuable) { incident }
+
+ it 'shows escalation policy dropdown' do
+ expect(rendered).to have_css('[data-testid="escalation_status_container"]')
+ end
+ end
+
+ context 'issuable that supports escalations' do
+ let(:issuable) { issue }
+
+ it 'does not show escalation policy dropdown' do
+ expect(rendered).not_to have_css('[data-testid="escalation_status_container"]')
+ end
+ end
+ end
+end
diff --git a/spec/workers/auto_devops/disable_worker_spec.rb b/spec/workers/auto_devops/disable_worker_spec.rb
index 239f4b09f5c..e1de97e0ce5 100644
--- a/spec/workers/auto_devops/disable_worker_spec.rb
+++ b/spec/workers/auto_devops/disable_worker_spec.rb
@@ -26,7 +26,7 @@ RSpec.describe AutoDevops::DisableWorker, '#perform' do
let(:namespace) { create(:namespace, owner: owner) }
let(:project) { create(:project, :repository, :auto_devops, namespace: namespace) }
- it 'sends an email to pipeline user and project owner' do
+ it 'sends an email to pipeline user and project owner(s)' do
expect(NotificationService).to receive_message_chain(:new, :autodevops_disabled).with(pipeline, [user.email, owner.email])
subject.perform(pipeline.id)
diff --git a/spec/workers/background_migration/ci_database_worker_spec.rb b/spec/workers/background_migration/ci_database_worker_spec.rb
new file mode 100644
index 00000000000..82c562c4042
--- /dev/null
+++ b/spec/workers/background_migration/ci_database_worker_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BackgroundMigration::CiDatabaseWorker, :clean_gitlab_redis_shared_state, if: Gitlab::Database.has_config?(:ci) do
+ it_behaves_like 'it runs background migration jobs', 'ci'
+end
diff --git a/spec/workers/background_migration_worker_spec.rb b/spec/workers/background_migration_worker_spec.rb
index 4297e55ca6c..1558c3c9250 100644
--- a/spec/workers/background_migration_worker_spec.rb
+++ b/spec/workers/background_migration_worker_spec.rb
@@ -3,5 +3,5 @@
require 'spec_helper'
RSpec.describe BackgroundMigrationWorker, :clean_gitlab_redis_shared_state do
- it_behaves_like 'it runs background migration jobs', 'main', :background_migration_database_health_reschedules
+ it_behaves_like 'it runs background migration jobs', 'main'
end
diff --git a/spec/workers/ci/delete_objects_worker_spec.rb b/spec/workers/ci/delete_objects_worker_spec.rb
index 52d90d7667a..3d985dffdc5 100644
--- a/spec/workers/ci/delete_objects_worker_spec.rb
+++ b/spec/workers/ci/delete_objects_worker_spec.rb
@@ -6,15 +6,16 @@ RSpec.describe Ci::DeleteObjectsWorker do
let(:worker) { described_class.new }
it { expect(described_class.idempotent?).to be_truthy }
+ it { is_expected.to respond_to(:max_running_jobs) }
+ it { is_expected.to respond_to(:remaining_work_count) }
+ it { is_expected.to respond_to(:perform_work) }
describe '#perform' do
it 'executes a service' do
- allow(worker).to receive(:max_running_jobs).and_return(25)
-
expect_next_instance_of(Ci::DeleteObjectsService) do |instance|
expect(instance).to receive(:execute)
expect(instance).to receive(:remaining_batches_count)
- .with(max_batch_count: 25)
+ .with(max_batch_count: 20)
.once
.and_call_original
end
@@ -22,30 +23,4 @@ RSpec.describe Ci::DeleteObjectsWorker do
worker.perform
end
end
-
- describe '#max_running_jobs' do
- using RSpec::Parameterized::TableSyntax
-
- before do
- stub_feature_flags(
- ci_delete_objects_medium_concurrency: medium,
- ci_delete_objects_high_concurrency: high
- )
- end
-
- subject(:max_running_jobs) { worker.max_running_jobs }
-
- where(:medium, :high, :expected) do
- false | false | 2
- true | false | 20
- true | true | 20
- false | true | 50
- end
-
- with_them do
- it 'sets up concurrency depending on the feature flag' do
- expect(max_running_jobs).to eq(expected)
- end
- end
- end
end
diff --git a/spec/workers/ci/external_pull_requests/create_pipeline_worker_spec.rb b/spec/workers/ci/external_pull_requests/create_pipeline_worker_spec.rb
index 116a0e4d035..a637ac088ff 100644
--- a/spec/workers/ci/external_pull_requests/create_pipeline_worker_spec.rb
+++ b/spec/workers/ci/external_pull_requests/create_pipeline_worker_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Ci::ExternalPullRequests::CreatePipelineWorker do
let_it_be(:project) { create(:project, :auto_devops, :repository) }
- let_it_be(:user) { project.owner }
+ let_it_be(:user) { project.first_owner }
let_it_be(:external_pull_request) do
branch = project.repository.branches.last
create(:external_pull_request, project: project, source_branch: branch.name, source_sha: branch.target)
diff --git a/spec/workers/cleanup_container_repository_worker_spec.rb b/spec/workers/cleanup_container_repository_worker_spec.rb
index 6ae4308bd46..6723ea2049d 100644
--- a/spec/workers/cleanup_container_repository_worker_spec.rb
+++ b/spec/workers/cleanup_container_repository_worker_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe CleanupContainerRepositoryWorker, :clean_gitlab_redis_shared_state do
let(:repository) { create(:container_repository) }
let(:project) { repository.project }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
subject { described_class.new }
diff --git a/spec/workers/concerns/application_worker_spec.rb b/spec/workers/concerns/application_worker_spec.rb
index 85731de2a45..95d9b982fc4 100644
--- a/spec/workers/concerns/application_worker_spec.rb
+++ b/spec/workers/concerns/application_worker_spec.rb
@@ -247,45 +247,6 @@ RSpec.describe ApplicationWorker do
end
end
- describe '.perform_async' do
- using RSpec::Parameterized::TableSyntax
-
- where(:primary_only?, :skip_scheduling_ff, :data_consistency, :schedules_job?) do
- true | false | :sticky | false
- true | false | :delayed | false
- true | false | :always | false
- true | true | :sticky | false
- true | true | :delayed | false
- true | true | :always | false
- false | false | :sticky | true
- false | false | :delayed | true
- false | false | :always | false
- false | true | :sticky | false
- false | true | :delayed | false
- false | true | :always | false
- end
-
- before do
- stub_const(worker.name, worker)
- worker.data_consistency(data_consistency)
-
- allow(Gitlab::Database::LoadBalancing).to receive(:primary_only?).and_return(primary_only?)
- stub_feature_flags(skip_scheduling_workers_for_replicas: skip_scheduling_ff)
- end
-
- with_them do
- it 'schedules or enqueues the job correctly' do
- if schedules_job?
- expect(worker).to receive(:perform_in).with(described_class::DEFAULT_DELAY_INTERVAL.seconds, 123)
- else
- expect(worker).not_to receive(:perform_in)
- end
-
- worker.perform_async(123)
- end
- end
- end
-
context 'different kinds of push_bulk' do
shared_context 'set safe limit beyond the number of jobs to be enqueued' do
before do
diff --git a/spec/workers/container_expiration_policy_worker_spec.rb b/spec/workers/container_expiration_policy_worker_spec.rb
index ebf80041151..2cfb613865d 100644
--- a/spec/workers/container_expiration_policy_worker_spec.rb
+++ b/spec/workers/container_expiration_policy_worker_spec.rb
@@ -60,12 +60,11 @@ RSpec.describe ContainerExpirationPolicyWorker do
context 'with container expiration policies' do
let_it_be(:container_expiration_policy, reload: true) { create(:container_expiration_policy, :runnable) }
let_it_be(:container_repository) { create(:container_repository, project: container_expiration_policy.project) }
- let_it_be(:user) { container_expiration_policy.project.owner }
context 'a valid policy' do
it 'runs the policy' do
expect(ContainerExpirationPolicyService)
- .to receive(:new).with(container_expiration_policy.project, user).and_call_original
+ .to receive(:new).with(container_expiration_policy.project, nil).and_call_original
expect(CleanupContainerRepositoryWorker).to receive(:perform_async).once.and_call_original
expect { subject }.not_to raise_error
@@ -102,7 +101,7 @@ RSpec.describe ContainerExpirationPolicyWorker do
end
it 'disables the policy and tracks an error' do
- expect(ContainerExpirationPolicyService).not_to receive(:new).with(container_expiration_policy, user)
+ expect(ContainerExpirationPolicyService).not_to receive(:new).with(container_expiration_policy, nil)
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(instance_of(described_class::InvalidPolicyError), container_expiration_policy_id: container_expiration_policy.id)
expect { subject }.to change { container_expiration_policy.reload.enabled }.from(true).to(false)
diff --git a/spec/workers/container_registry/migration/enqueuer_worker_spec.rb b/spec/workers/container_registry/migration/enqueuer_worker_spec.rb
new file mode 100644
index 00000000000..12c14c35365
--- /dev/null
+++ b/spec/workers/container_registry/migration/enqueuer_worker_spec.rb
@@ -0,0 +1,178 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ContainerRegistry::Migration::EnqueuerWorker, :aggregate_failures do
+ let_it_be_with_reload(:container_repository) { create(:container_repository, created_at: 2.days.ago) }
+
+ let(:worker) { described_class.new }
+
+ before do
+ stub_container_registry_config(enabled: true)
+ stub_application_setting(container_registry_import_created_before: 1.day.ago)
+ stub_container_registry_tags(repository: container_repository.path, tags: %w(tag1 tag2 tag3), with_manifest: true)
+ end
+
+ describe '#perform' do
+ subject { worker.perform }
+
+ shared_examples 'no action' do
+ it 'does not queue or change any repositories' do
+ subject
+
+ expect(container_repository.reload).to be_default
+ end
+ end
+
+ shared_examples 're-enqueuing based on capacity' do
+ context 'below capacity' do
+ before do
+ allow(ContainerRegistry::Migration).to receive(:capacity).and_return(9999)
+ end
+
+ it 're-enqueues the worker' do
+ expect(ContainerRegistry::Migration::EnqueuerWorker).to receive(:perform_async)
+
+ subject
+ end
+ end
+
+ context 'above capacity' do
+ before do
+ allow(ContainerRegistry::Migration).to receive(:capacity).and_return(-1)
+ end
+
+ it 'does not re-enqueue the worker' do
+ expect(ContainerRegistry::Migration::EnqueuerWorker).not_to receive(:perform_async)
+
+ subject
+ end
+ end
+ end
+
+ context 'with qualified repository' do
+ it 'starts the pre-import for the next qualified repository' do
+ method = worker.method(:next_repository)
+ allow(worker).to receive(:next_repository) do
+ next_qualified_repository = method.call
+ allow(next_qualified_repository).to receive(:migration_pre_import).and_return(:ok)
+ next_qualified_repository
+ end
+
+ expect(worker).to receive(:log_extra_metadata_on_done)
+ .with(:container_repository_id, container_repository.id)
+ expect(worker).to receive(:log_extra_metadata_on_done)
+ .with(:import_type, 'next')
+
+ subject
+
+ expect(container_repository.reload).to be_pre_importing
+ end
+
+ it_behaves_like 're-enqueuing based on capacity'
+ end
+
+ context 'migrations are disabled' do
+ before do
+ allow(ContainerRegistry::Migration).to receive(:enabled?).and_return(false)
+ end
+
+ it_behaves_like 'no action'
+ end
+
+ context 'above capacity' do
+ before do
+ create(:container_repository, :importing)
+ create(:container_repository, :importing)
+ allow(ContainerRegistry::Migration).to receive(:capacity).and_return(1)
+ end
+
+ it_behaves_like 'no action'
+
+ it 'does not re-enqueue the worker' do
+ expect(ContainerRegistry::Migration::EnqueuerWorker).not_to receive(:perform_async)
+
+ subject
+ end
+ end
+
+ context 'too soon before previous completed import step' do
+ before do
+ create(:container_repository, :import_done, migration_import_done_at: 1.minute.ago)
+ allow(ContainerRegistry::Migration).to receive(:enqueue_waiting_time).and_return(1.hour)
+ end
+
+ it_behaves_like 'no action'
+ end
+
+ context 'when an aborted import is available' do
+ let_it_be(:aborted_repository) { create(:container_repository, :import_aborted) }
+
+ it 'retries the import for the aborted repository' do
+ method = worker.method(:next_aborted_repository)
+ allow(worker).to receive(:next_aborted_repository) do
+ next_aborted_repository = method.call
+ allow(next_aborted_repository).to receive(:migration_import).and_return(:ok)
+ allow(next_aborted_repository.gitlab_api_client).to receive(:import_status).and_return('import_failed')
+ next_aborted_repository
+ end
+
+ expect(worker).to receive(:log_extra_metadata_on_done)
+ .with(:container_repository_id, aborted_repository.id)
+ expect(worker).to receive(:log_extra_metadata_on_done)
+ .with(:import_type, 'retry')
+
+ subject
+
+ expect(aborted_repository.reload).to be_importing
+ expect(container_repository.reload).to be_default
+ end
+
+ it_behaves_like 're-enqueuing based on capacity'
+ end
+
+ context 'when no repository qualifies' do
+ include_examples 'an idempotent worker' do
+ before do
+ allow(ContainerRepository).to receive(:ready_for_import).and_return(ContainerRepository.none)
+ end
+
+ it_behaves_like 'no action'
+ end
+ end
+
+ context 'over max tag count' do
+ before do
+ stub_application_setting(container_registry_import_max_tags_count: 2)
+ end
+
+ it 'skips the repository' do
+ subject
+
+ expect(container_repository.reload).to be_import_skipped
+ expect(container_repository.migration_skipped_reason).to eq('too_many_tags')
+ expect(container_repository.migration_skipped_at).not_to be_nil
+ end
+
+ it_behaves_like 're-enqueuing based on capacity'
+ end
+
+ context 'when an error occurs' do
+ before do
+ allow(ContainerRegistry::Migration).to receive(:max_tags_count).and_raise(StandardError)
+ end
+
+ it 'aborts the import' do
+ expect(Gitlab::ErrorTracking).to receive(:log_exception).with(
+ instance_of(StandardError),
+ next_repository_id: container_repository.id,
+ next_aborted_repository_id: nil
+ )
+
+ subject
+
+ expect(container_repository.reload).to be_import_aborted
+ end
+ end
+ end
+end
diff --git a/spec/workers/container_registry/migration/guard_worker_spec.rb b/spec/workers/container_registry/migration/guard_worker_spec.rb
new file mode 100644
index 00000000000..7d1df320d4e
--- /dev/null
+++ b/spec/workers/container_registry/migration/guard_worker_spec.rb
@@ -0,0 +1,162 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ContainerRegistry::Migration::GuardWorker, :aggregate_failures do
+ include_context 'container registry client'
+
+ let(:worker) { described_class.new }
+
+ describe '#perform' do
+ let(:pre_importing_migrations) { ::ContainerRepository.with_migration_states(:pre_importing) }
+ let(:pre_import_done_migrations) { ::ContainerRepository.with_migration_states(:pre_import_done) }
+ let(:importing_migrations) { ::ContainerRepository.with_migration_states(:importing) }
+ let(:import_aborted_migrations) { ::ContainerRepository.with_migration_states(:import_aborted) }
+ let(:import_done_migrations) { ::ContainerRepository.with_migration_states(:import_done) }
+
+ subject { worker.perform }
+
+ before do
+ stub_container_registry_config(enabled: true, api_url: registry_api_url, key: 'spec/fixtures/x509_certificate_pk.key')
+ allow(::ContainerRegistry::Migration).to receive(:max_step_duration).and_return(5.minutes)
+ end
+
+ context 'on gitlab.com' do
+ before do
+ allow(::Gitlab).to receive(:com?).and_return(true)
+ end
+
+ shared_examples 'not aborting any migration' do
+ it 'will not abort the migration' do
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:stale_migrations_count, 1)
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:aborted_stale_migrations_count, 0)
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:long_running_stale_migration_container_repository_ids, [stale_migration.id])
+
+ expect { subject }
+ .to not_change(pre_importing_migrations, :count)
+ .and not_change(pre_import_done_migrations, :count)
+ .and not_change(importing_migrations, :count)
+ .and not_change(import_done_migrations, :count)
+ .and not_change(import_aborted_migrations, :count)
+ .and not_change { stale_migration.reload.migration_state }
+ .and not_change { ongoing_migration.migration_state }
+ end
+ end
+
+ context 'with no stale migrations' do
+ it_behaves_like 'an idempotent worker'
+
+ it 'will not update any migration state' do
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:stale_migrations_count, 0)
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:aborted_stale_migrations_count, 0)
+
+ expect { subject }
+ .to not_change(pre_importing_migrations, :count)
+ .and not_change(pre_import_done_migrations, :count)
+ .and not_change(importing_migrations, :count)
+ .and not_change(import_aborted_migrations, :count)
+ end
+ end
+
+ context 'with pre_importing stale migrations' do
+ let(:ongoing_migration) { create(:container_repository, :pre_importing) }
+ let(:stale_migration) { create(:container_repository, :pre_importing, migration_pre_import_started_at: 35.minutes.ago) }
+ let(:import_status) { 'test' }
+
+ before do
+ allow_next_instance_of(ContainerRegistry::GitlabApiClient) do |client|
+ allow(client).to receive(:import_status).and_return(import_status)
+ end
+ end
+
+ it 'will abort the migration' do
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:stale_migrations_count, 1)
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:aborted_stale_migrations_count, 1)
+
+ expect { subject }
+ .to change(pre_importing_migrations, :count).by(-1)
+ .and not_change(pre_import_done_migrations, :count)
+ .and not_change(importing_migrations, :count)
+ .and not_change(import_done_migrations, :count)
+ .and change(import_aborted_migrations, :count).by(1)
+ .and change { stale_migration.reload.migration_state }.from('pre_importing').to('import_aborted')
+ .and not_change { ongoing_migration.migration_state }
+ end
+
+ context 'the client returns pre_import_in_progress' do
+ let(:import_status) { 'pre_import_in_progress' }
+
+ it_behaves_like 'not aborting any migration'
+ end
+ end
+
+ context 'with pre_import_done stale migrations' do
+ let(:ongoing_migration) { create(:container_repository, :pre_import_done) }
+ let(:stale_migration) { create(:container_repository, :pre_import_done, migration_pre_import_done_at: 35.minutes.ago) }
+
+ before do
+ allow(::ContainerRegistry::Migration).to receive(:max_step_duration).and_return(5.minutes)
+ end
+
+ it 'will abort the migration' do
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:stale_migrations_count, 1)
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:aborted_stale_migrations_count, 1)
+
+ expect { subject }
+ .to not_change(pre_importing_migrations, :count)
+ .and change(pre_import_done_migrations, :count).by(-1)
+ .and not_change(importing_migrations, :count)
+ .and not_change(import_done_migrations, :count)
+ .and change(import_aborted_migrations, :count).by(1)
+ .and change { stale_migration.reload.migration_state }.from('pre_import_done').to('import_aborted')
+ .and not_change { ongoing_migration.migration_state }
+ end
+ end
+
+ context 'with importing stale migrations' do
+ let(:ongoing_migration) { create(:container_repository, :importing) }
+ let(:stale_migration) { create(:container_repository, :importing, migration_import_started_at: 35.minutes.ago) }
+ let(:import_status) { 'test' }
+
+ before do
+ allow_next_instance_of(ContainerRegistry::GitlabApiClient) do |client|
+ allow(client).to receive(:import_status).and_return(import_status)
+ end
+ end
+
+ it 'will abort the migration' do
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:stale_migrations_count, 1)
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:aborted_stale_migrations_count, 1)
+
+ expect { subject }
+ .to not_change(pre_importing_migrations, :count)
+ .and not_change(pre_import_done_migrations, :count)
+ .and change(importing_migrations, :count).by(-1)
+ .and not_change(import_done_migrations, :count)
+ .and change(import_aborted_migrations, :count).by(1)
+ .and change { stale_migration.reload.migration_state }.from('importing').to('import_aborted')
+ .and not_change { ongoing_migration.migration_state }
+ end
+
+ context 'the client returns import_in_progress' do
+ let(:import_status) { 'import_in_progress' }
+
+ it_behaves_like 'not aborting any migration'
+ end
+ end
+ end
+
+ context 'not on gitlab.com' do
+ before do
+ allow(::Gitlab).to receive(:com?).and_return(false)
+ end
+
+ it 'is a no op' do
+ expect(::ContainerRepository).not_to receive(:with_stale_migration)
+ expect(worker).not_to receive(:log_extra_metadata_on_done)
+
+ subject
+ end
+ end
+ end
+end
diff --git a/spec/workers/container_registry/migration/observer_worker_spec.rb b/spec/workers/container_registry/migration/observer_worker_spec.rb
new file mode 100644
index 00000000000..fec6640d7ec
--- /dev/null
+++ b/spec/workers/container_registry/migration/observer_worker_spec.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ContainerRegistry::Migration::ObserverWorker, :aggregate_failures do
+ let(:worker) { described_class.new }
+
+ describe '#perform' do
+ subject { worker.perform }
+
+ context 'when the migration feature flag is disabled' do
+ before do
+ stub_feature_flags(container_registry_migration_phase2_enabled: false)
+ end
+
+ it 'does nothing' do
+ expect(worker).not_to receive(:log_extra_metadata_on_done)
+
+ subject
+ end
+ end
+
+ context 'when the migration is enabled' do
+ before do
+ create_list(:container_repository, 3)
+ create(:container_repository, :pre_importing)
+ create(:container_repository, :pre_import_done)
+ create_list(:container_repository, 2, :importing)
+ create(:container_repository, :import_aborted)
+ # batch_count is not allowed within a transaction but
+ # all rspec tests run inside of a transaction.
+ # This mocks the false positive.
+ allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false) # rubocop:disable Database/MultipleDatabases
+ end
+
+ it 'logs all the counts' do
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:default_count, 3)
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:pre_importing_count, 1)
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:pre_import_done_count, 1)
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:importing_count, 2)
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:import_done_count, 0)
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:import_aborted_count, 1)
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:import_skipped_count, 0)
+
+ subject
+ end
+
+ context 'with load balancing enabled', :db_load_balancing do
+ it 'uses the replica' do
+ expect(Gitlab::Database::LoadBalancing::Session.current).to receive(:use_replicas_for_read_queries).and_call_original
+
+ subject
+ end
+ end
+ end
+ end
+end
diff --git a/spec/workers/delete_container_repository_worker_spec.rb b/spec/workers/delete_container_repository_worker_spec.rb
index b8363a2f81a..ec040eab2d4 100644
--- a/spec/workers/delete_container_repository_worker_spec.rb
+++ b/spec/workers/delete_container_repository_worker_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe DeleteContainerRepositoryWorker do
let(:registry) { create(:container_repository) }
let(:project) { registry.project }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
subject { described_class.new }
diff --git a/spec/workers/delete_merged_branches_worker_spec.rb b/spec/workers/delete_merged_branches_worker_spec.rb
index 861ca111b92..056fcb1200d 100644
--- a/spec/workers/delete_merged_branches_worker_spec.rb
+++ b/spec/workers/delete_merged_branches_worker_spec.rb
@@ -13,11 +13,11 @@ RSpec.describe DeleteMergedBranchesWorker do
expect(instance).to receive(:execute).and_return(true)
end
- worker.perform(project.id, project.owner.id)
+ worker.perform(project.id, project.first_owner.id)
end
it "returns false when project was not found" do
- expect(worker.perform('unknown', project.owner.id)).to be_falsy
+ expect(worker.perform('unknown', project.first_owner.id)).to be_falsy
end
end
end
diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb
index 4f9c207f976..1cd5d23d8fc 100644
--- a/spec/workers/every_sidekiq_worker_spec.rb
+++ b/spec/workers/every_sidekiq_worker_spec.rb
@@ -135,6 +135,7 @@ RSpec.describe 'Every Sidekiq worker' do
'AutoDevops::DisableWorker' => 3,
'AutoMergeProcessWorker' => 3,
'BackgroundMigrationWorker' => 3,
+ 'BackgroundMigration::CiDatabaseWorker' => 3,
'BuildFinishedWorker' => 3,
'BuildHooksWorker' => 3,
'BuildQueueWorker' => 3,
@@ -348,6 +349,7 @@ RSpec.describe 'Every Sidekiq worker' do
'Namespaces::OnboardingPipelineCreatedWorker' => 3,
'Namespaces::OnboardingProgressWorker' => 3,
'Namespaces::OnboardingUserAddedWorker' => 3,
+ 'Namespaces::RefreshRootStatisticsWorker' => 3,
'Namespaces::RootStatisticsWorker' => 3,
'Namespaces::ScheduleAggregationWorker' => 3,
'NetworkPolicyMetricsWorker' => 3,
@@ -371,7 +373,6 @@ RSpec.describe 'Every Sidekiq worker' do
'PagesDomainSslRenewalWorker' => 3,
'PagesDomainVerificationWorker' => 3,
'PagesTransferWorker' => 3,
- 'PagesUpdateConfigurationWorker' => 1,
'PagesWorker' => 3,
'PersonalAccessTokens::Groups::PolicyWorker' => 3,
'PersonalAccessTokens::Instance::PolicyWorker' => 3,
@@ -459,7 +460,8 @@ RSpec.describe 'Every Sidekiq worker' do
'WebHooks::DestroyWorker' => 3,
'WebHooks::LogExecutionWorker' => 3,
'Wikis::GitGarbageCollectWorker' => false,
- 'X509CertificateRevokeWorker' => 3
+ 'X509CertificateRevokeWorker' => 3,
+ 'ComplianceManagement::MergeRequests::ComplianceViolationsWorker' => 3
}
end
diff --git a/spec/workers/groups/update_statistics_worker_spec.rb b/spec/workers/groups/update_statistics_worker_spec.rb
new file mode 100644
index 00000000000..7fc166ed300
--- /dev/null
+++ b/spec/workers/groups/update_statistics_worker_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Groups::UpdateStatisticsWorker do
+ let_it_be(:group) { create(:group) }
+
+ let(:statistics) { %w(wiki_size) }
+
+ subject(:worker) { described_class.new }
+
+ describe '#perform' do
+ it 'updates the group statistics' do
+ expect(Groups::UpdateStatisticsService).to receive(:new)
+ .with(group, statistics: statistics)
+ .and_call_original
+
+ worker.perform(group.id, statistics)
+ end
+
+ context 'when group id does not exist' do
+ it 'ends gracefully' do
+ expect(Groups::UpdateStatisticsService).not_to receive(:new)
+
+ expect { worker.perform(non_existing_record_id, statistics) }.not_to raise_error
+ end
+ end
+ end
+end
diff --git a/spec/workers/loose_foreign_keys/cleanup_worker_spec.rb b/spec/workers/loose_foreign_keys/cleanup_worker_spec.rb
index 497f95cf34d..6f4389a7541 100644
--- a/spec/workers/loose_foreign_keys/cleanup_worker_spec.rb
+++ b/spec/workers/loose_foreign_keys/cleanup_worker_spec.rb
@@ -141,16 +141,6 @@ RSpec.describe LooseForeignKeys::CleanupWorker do
end
end
- context 'when the loose_foreign_key_cleanup feature flag is off' do
- before do
- stub_feature_flags(loose_foreign_key_cleanup: false)
- end
-
- it 'does nothing' do
- expect { described_class.new.perform }.not_to change { LooseForeignKeys::DeletedRecord.status_processed.count }
- end
- end
-
describe 'multi-database support' do
where(:current_minute, :configured_base_models, :expected_connection) do
2 | { main: ApplicationRecord, ci: Ci::ApplicationRecord } | ApplicationRecord.connection
diff --git a/spec/workers/namespaces/process_sync_events_worker_spec.rb b/spec/workers/namespaces/process_sync_events_worker_spec.rb
index 59be1fffdb4..c15a74a2934 100644
--- a/spec/workers/namespaces/process_sync_events_worker_spec.rb
+++ b/spec/workers/namespaces/process_sync_events_worker_spec.rb
@@ -7,10 +7,12 @@ RSpec.describe Namespaces::ProcessSyncEventsWorker do
let!(:group2) { create(:group) }
let!(:group3) { create(:group) }
+ subject(:worker) { described_class.new }
+
include_examples 'an idempotent worker'
describe '#perform' do
- subject(:perform) { described_class.new.perform }
+ subject(:perform) { worker.perform }
before do
group2.update!(parent: group1)
@@ -28,5 +30,13 @@ RSpec.describe Namespaces::ProcessSyncEventsWorker do
an_object_having_attributes(namespace_id: group3.id, traversal_ids: [group1.id, group2.id, group3.id])
)
end
+
+ it 'logs the service result', :aggregate_failures do
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:estimated_total_events, 5)
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:consumable_events, 5)
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:processed_events, 5)
+
+ perform
+ end
end
end
diff --git a/spec/workers/namespaces/update_root_statistics_worker_spec.rb b/spec/workers/namespaces/update_root_statistics_worker_spec.rb
new file mode 100644
index 00000000000..a525904b757
--- /dev/null
+++ b/spec/workers/namespaces/update_root_statistics_worker_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Namespaces::UpdateRootStatisticsWorker do
+ let(:namespace_id) { 123 }
+
+ let(:event) do
+ Projects::ProjectDeletedEvent.new(data: { project_id: 1, namespace_id: namespace_id })
+ end
+
+ subject { consume_event(event) }
+
+ def consume_event(event)
+ described_class.new.perform(event.class.name, event.data)
+ end
+
+ it 'enqueues ScheduleAggregationWorker' do
+ expect(Namespaces::ScheduleAggregationWorker).to receive(:perform_async).with(namespace_id)
+
+ subject
+ end
+end
diff --git a/spec/workers/pages_update_configuration_worker_spec.rb b/spec/workers/pages_update_configuration_worker_spec.rb
deleted file mode 100644
index af71f6b3cca..00000000000
--- a/spec/workers/pages_update_configuration_worker_spec.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-# frozen_string_literal: true
-require "spec_helper"
-
-RSpec.describe PagesUpdateConfigurationWorker do
- let_it_be(:project) { create(:project) }
-
- describe "#perform" do
- it "does not break" do
- expect { subject.perform(-1) }.not_to raise_error
- end
- end
-end
diff --git a/spec/workers/pipeline_schedule_worker_spec.rb b/spec/workers/pipeline_schedule_worker_spec.rb
index f59d8ad4615..4a7db0eca56 100644
--- a/spec/workers/pipeline_schedule_worker_spec.rb
+++ b/spec/workers/pipeline_schedule_worker_spec.rb
@@ -103,4 +103,14 @@ RSpec.describe PipelineScheduleWorker do
expect { subject }.not_to raise_error
end
end
+
+ context 'when the project is missing' do
+ before do
+ project.delete
+ end
+
+ it 'does not raise an exception' do
+ expect { subject }.not_to raise_error
+ end
+ end
end
diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index 42e39c51a88..9b33e559c71 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe PostReceive do
let(:wrongly_encoded_changes) { changes.encode("ISO-8859-1").force_encoding("UTF-8") }
let(:base64_changes) { Base64.encode64(wrongly_encoded_changes) }
let(:gl_repository) { "project-#{project.id}" }
- let(:key) { create(:key, user: project.owner) }
+ let(:key) { create(:key, user: project.first_owner) }
let!(:key_id) { key.shell_id }
let(:project) do
@@ -47,7 +47,7 @@ RSpec.describe PostReceive do
context 'with PersonalSnippet' do
let(:gl_repository) { "snippet-#{snippet.id}" }
- let(:snippet) { create(:personal_snippet, author: project.owner) }
+ let(:snippet) { create(:personal_snippet, author: project.first_owner) }
it 'does not log an error' do
expect(Gitlab::GitLogger).not_to receive(:error)
@@ -60,7 +60,7 @@ RSpec.describe PostReceive do
context 'with ProjectSnippet' do
let(:gl_repository) { "snippet-#{snippet.id}" }
- let(:snippet) { create(:snippet, type: 'ProjectSnippet', project: nil, author: project.owner) }
+ let(:snippet) { create(:snippet, type: 'ProjectSnippet', project: nil, author: project.first_owner) }
it 'returns false and logs an error' do
expect(Gitlab::GitLogger).to receive(:error).with("POST-RECEIVE: #{error_message}")
@@ -74,7 +74,7 @@ RSpec.describe PostReceive do
let(:empty_project) { create(:project, :empty_repo) }
before do
- allow_next(Gitlab::GitPostReceive).to receive(:identify).and_return(empty_project.owner)
+ allow_next(Gitlab::GitPostReceive).to receive(:identify).and_return(empty_project.first_owner)
# Need to mock here so we can expect calls on project
allow(Gitlab::GlRepository).to receive(:parse).and_return([empty_project, empty_project, Gitlab::GlRepository::PROJECT])
end
@@ -128,7 +128,7 @@ RSpec.describe PostReceive do
let(:push_service) { double(execute: true) }
before do
- allow_next(Gitlab::GitPostReceive).to receive(:identify).and_return(project.owner)
+ allow_next(Gitlab::GitPostReceive).to receive(:identify).and_return(project.first_owner)
allow(Gitlab::GlRepository).to receive(:parse).and_return([project, project, Gitlab::GlRepository::PROJECT])
end
@@ -381,7 +381,7 @@ RSpec.describe PostReceive do
allow(Project).to receive(:find_by).and_return(project)
expect_next(MergeRequests::PushedBranchesService).to receive(:execute).and_return(%w(tést))
- expect(UpdateMergeRequestsWorker).to receive(:perform_async).with(project.id, project.owner.id, any_args)
+ expect(UpdateMergeRequestsWorker).to receive(:perform_async).with(project.id, project.first_owner.id, any_args)
perform
end
@@ -461,13 +461,13 @@ RSpec.describe PostReceive do
end
context 'with PersonalSnippet' do
- let!(:snippet) { create(:personal_snippet, :repository, author: project.owner) }
+ let!(:snippet) { create(:personal_snippet, :repository, author: project.first_owner) }
it_behaves_like 'snippet changes actions'
end
context 'with ProjectSnippet' do
- let!(:snippet) { create(:project_snippet, :repository, project: project, author: project.owner) }
+ let!(:snippet) { create(:project_snippet, :repository, project: project, author: project.first_owner) }
it_behaves_like 'snippet changes actions'
end
diff --git a/spec/workers/project_destroy_worker_spec.rb b/spec/workers/project_destroy_worker_spec.rb
index 00a4ddac29f..0b0543a5089 100644
--- a/spec/workers/project_destroy_worker_spec.rb
+++ b/spec/workers/project_destroy_worker_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe ProjectDestroyWorker do
describe '#perform' do
it 'deletes the project' do
- subject.perform(project.id, project.owner.id, {})
+ subject.perform(project.id, project.first_owner.id, {})
expect(Project.all).not_to include(project)
expect(Dir.exist?(path)).to be_falsey
@@ -22,7 +22,7 @@ RSpec.describe ProjectDestroyWorker do
it 'does not raise error when project could not be found' do
expect do
- subject.perform(-1, project.owner.id, {})
+ subject.perform(-1, project.first_owner.id, {})
end.not_to raise_error
end
diff --git a/spec/workers/projects/git_garbage_collect_worker_spec.rb b/spec/workers/projects/git_garbage_collect_worker_spec.rb
index 7b54d7df4b2..ae567107443 100644
--- a/spec/workers/projects/git_garbage_collect_worker_spec.rb
+++ b/spec/workers/projects/git_garbage_collect_worker_spec.rb
@@ -32,6 +32,21 @@ RSpec.describe Projects::GitGarbageCollectWorker do
subject.perform(*params)
end
+
+ context 'when deduplication service runs into a GRPC internal error' do
+ before do
+ allow_next_instance_of(::Projects::GitDeduplicationService) do |instance|
+ expect(instance).to receive(:execute).and_raise(GRPC::Internal)
+ end
+ end
+
+ it_behaves_like 'can collect git garbage' do
+ let(:resource) { project }
+ let(:statistics_service_klass) { Projects::UpdateStatisticsService }
+ let(:statistics_keys) { [:repository_size, :lfs_objects_size] }
+ let(:expected_default_lease) { "projects:#{resource.id}" }
+ end
+ end
end
context 'LFS object garbage collection' do
diff --git a/spec/workers/projects/process_sync_events_worker_spec.rb b/spec/workers/projects/process_sync_events_worker_spec.rb
index 600fbbc6b20..963e0ad1028 100644
--- a/spec/workers/projects/process_sync_events_worker_spec.rb
+++ b/spec/workers/projects/process_sync_events_worker_spec.rb
@@ -6,10 +6,12 @@ RSpec.describe Projects::ProcessSyncEventsWorker do
let!(:group) { create(:group) }
let!(:project) { create(:project) }
+ subject(:worker) { described_class.new }
+
include_examples 'an idempotent worker'
describe '#perform' do
- subject(:perform) { described_class.new.perform }
+ subject(:perform) { worker.perform }
before do
project.update!(namespace: group)
@@ -24,5 +26,13 @@ RSpec.describe Projects::ProcessSyncEventsWorker do
an_object_having_attributes(namespace_id: group.id)
)
end
+
+ it 'logs the service result', :aggregate_failures do
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:estimated_total_events, 2)
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:consumable_events, 2)
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:processed_events, 2)
+
+ perform
+ end
end
end
diff --git a/spec/workers/run_pipeline_schedule_worker_spec.rb b/spec/workers/run_pipeline_schedule_worker_spec.rb
index bb11d1dbb58..846b4455bf9 100644
--- a/spec/workers/run_pipeline_schedule_worker_spec.rb
+++ b/spec/workers/run_pipeline_schedule_worker_spec.rb
@@ -10,12 +10,25 @@ RSpec.describe RunPipelineScheduleWorker do
let(:worker) { described_class.new }
- context 'when a project not found' do
+ context 'when a schedule not found' do
it 'does not call the Service' do
expect(Ci::CreatePipelineService).not_to receive(:new)
expect(worker).not_to receive(:run_pipeline_schedule)
- worker.perform(100000, user.id)
+ worker.perform(non_existing_record_id, user.id)
+ end
+ end
+
+ context 'when a schedule project is missing' do
+ before do
+ project.delete
+ end
+
+ it 'does not call the Service' do
+ expect(Ci::CreatePipelineService).not_to receive(:new)
+ expect(worker).not_to receive(:run_pipeline_schedule)
+
+ worker.perform(pipeline_schedule.id, user.id)
end
end
@@ -24,7 +37,7 @@ RSpec.describe RunPipelineScheduleWorker do
expect(Ci::CreatePipelineService).not_to receive(:new)
expect(worker).not_to receive(:run_pipeline_schedule)
- worker.perform(pipeline_schedule.id, 10000)
+ worker.perform(pipeline_schedule.id, non_existing_record_id)
end
end
diff --git a/spec/workers/web_hook_worker_spec.rb b/spec/workers/web_hook_worker_spec.rb
index bbb8844a447..dbdf7a2b978 100644
--- a/spec/workers/web_hook_worker_spec.rb
+++ b/spec/workers/web_hook_worker_spec.rb
@@ -19,7 +19,16 @@ RSpec.describe WebHookWorker do
expect { subject.perform(non_existing_record_id, data, hook_name) }.not_to raise_error
end
- it 'retrieves recursion detection data, reinstates it, and cleans it from payload', :request_store, :aggregate_failures do
+ it 'retrieves recursion detection data and reinstates it', :request_store, :aggregate_failures do
+ uuid = SecureRandom.uuid
+ params = { recursion_detection_request_uuid: uuid }
+
+ expect_next(WebHookService, project_hook, data.with_indifferent_access, hook_name, anything).to receive(:execute)
+ expect { subject.perform(project_hook.id, data, hook_name, params) }
+ .to change { Gitlab::WebHooks::RecursionDetection::UUID.instance.request_uuid }.to(uuid)
+ end
+
+ it 'retrieves recursion detection data, reinstates it, and cleans it from payload when passed through as data', :request_store, :aggregate_failures do
uuid = SecureRandom.uuid
full_data = data.merge({ _gitlab_recursion_detection_request_uuid: uuid })