From e4384360a16dd9a19d4d2d25d0ef1f2b862ed2a6 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Wed, 19 Jul 2023 14:16:28 +0000 Subject: Add latest changes from gitlab-org/gitlab@16-2-stable-ee --- spec/frontend/__helpers__/set_vue_error_handler.js | 30 + .../access_tokens/components/tokens_app_spec.js | 4 +- .../actioncable_connection_monitor_spec.js | 79 -- .../components/add_context_commits_modal_spec.js | 17 +- .../components/abuse_category_spec.js | 43 + .../components/abuse_report_row_spec.js | 16 +- .../components/delete_application_spec.js | 7 +- .../components/message_form_spec.js | 43 +- .../admin/topics/components/remove_avatar_spec.js | 2 +- .../admin/topics/components/topic_select_spec.js | 4 - .../components/alert_management_table_spec.js | 2 +- .../components/alerts_settings_form_spec.js | 31 +- .../components/projects_dropdown_filter_spec.js | 13 +- .../usage_trends/components/users_chart_spec.js | 47 +- spec/frontend/api/user_api_spec.js | 21 + .../batch_comments/components/draft_note_spec.js | 68 +- .../components/submit_dropdown_spec.js | 17 + .../stores/modules/batch_comments/actions_spec.js | 55 +- spec/frontend/behaviors/gl_emoji_spec.js | 196 ++-- .../frontend/behaviors/markdown/render_gfm_spec.js | 26 - .../behaviors/markdown/render_metrics_spec.js | 49 - spec/frontend/blob/line_highlighter_spec.js | 71 +- spec/frontend/blob_edit/edit_blob_spec.js | 1 - spec/frontend/boards/board_card_inner_spec.js | 2 +- spec/frontend/boards/components/board_app_spec.js | 11 + .../boards/components/board_content_spec.js | 11 +- .../boards/components/board_list_header_spec.js | 23 +- .../boards/components/board_new_issue_spec.js | 65 +- .../components/board_settings_sidebar_spec.js | 4 - spec/frontend/boards/mock_data.js | 41 + spec/frontend/boards/project_select_spec.js | 111 +- spec/frontend/boards/stores/actions_spec.js | 8 +- .../delete_merged_branches_spec.js.snap | 5 +- .../components/delete_merged_branches_spec.js | 2 +- .../components/ci_environments_dropdown_spec.js | 65 +- .../components/ci_group_variables_spec.js | 43 +- .../components/ci_variable_modal_spec.js | 5 +- .../components/ci_variable_settings_spec.js | 2 + .../components/ci_variable_shared_spec.js | 33 +- spec/frontend/ci/ci_variable_list/mocks.js | 3 + .../components/commit/commit_section_spec.js | 21 +- .../accordion_items/rules_item_spec.js | 18 +- .../components/pipeline_schedules_form_spec.js | 306 ++++- .../components/pipeline_schedules_spec.js | 37 +- .../table/cells/pipeline_schedule_actions_spec.js | 13 + spec/frontend/ci/pipeline_schedules/mock_data.js | 34 +- .../__snapshots__/grouped_issues_list_spec.js.snap | 26 - .../reports/components/grouped_issues_list_spec.js | 83 -- .../ci/reports/components/summary_row_spec.js | 63 -- spec/frontend/ci/reports/mock_data/mock_data.js | 54 + spec/frontend/ci/reports/utils_spec.js | 30 + .../admin_runner_show_app_spec.js | 82 +- .../runner/admin_runners/admin_runners_app_spec.js | 2 +- .../components/cells/runner_summary_cell_spec.js | 94 +- .../registration/registration_dropdown_spec.js | 75 +- .../registration_token_reset_dropdown_item_spec.js | 4 +- .../registration/registration_token_spec.js | 23 +- .../runner/components/runner_delete_action_spec.js | 223 ++++ .../runner/components/runner_delete_button_spec.js | 222 +--- .../runner_delete_disclosure_dropdown_item_spec.js | 68 ++ .../runner/components/runner_delete_modal_spec.js | 34 +- .../ci/runner/components/runner_detail_spec.js | 88 ++ .../runner/components/runner_edit_button_spec.js | 30 +- .../runner_edit_disclosure_dropdown_item_spec.js | 42 + .../components/runner_header_actions_spec.js | 147 +++ .../components/runner_list_empty_state_spec.js | 159 +-- .../runner/components/runner_pause_action_spec.js | 180 +++ .../runner/components/runner_pause_button_spec.js | 282 ++--- .../runner_pause_disclosure_dropdown_item_spec.js | 71 ++ .../group_runner_show_app_spec.js | 67 +- .../runner/group_runners/group_runners_app_spec.js | 29 +- .../agents/components/create_token_modal_spec.js | 13 +- .../agents/components/revoke_token_button_spec.js | 8 +- .../components/clusters_actions_spec.js | 38 +- .../components/delete_agent_button_spec.js | 10 +- .../__snapshots__/list_item_spec.js.snap | 2 +- .../commit_pipeline_status_component_spec.js | 172 --- .../frontend/commit/commit_pipeline_status_spec.js | 172 +++ .../components/bubble_menus/bubble_menu_spec.js | 2 +- .../bubble_menus/link_bubble_menu_spec.js | 2 +- .../bubble_menus/media_bubble_menu_spec.js | 71 +- .../bubble_menus/reference_bubble_menu_spec.js | 2 +- .../components/content_editor_spec.js | 20 +- .../components/formatting_toolbar_spec.js | 104 +- .../components/suggestions_dropdown_spec.js | 9 +- .../components/wrappers/code_block_spec.js | 239 +++- .../components/wrappers/image_spec.js | 100 ++ .../components/wrappers/reference_spec.js | 18 + .../extensions/code_suggestion_spec.js | 128 +++ .../content_editor/extensions/comment_spec.js | 30 - .../content_editor/extensions/copy_paste_spec.js | 385 +++++++ .../content_editor/extensions/hard_break_spec.js | 20 +- .../content_editor/extensions/html_nodes_spec.js | 6 +- .../content_editor/extensions/image_spec.js | 2 +- .../content_editor/extensions/paragraph_spec.js | 29 + .../extensions/paste_markdown_spec.js | 323 ------ .../remark_markdown_processing_spec.js | 13 +- .../services/code_suggestion_utils_spec.js | 53 + .../services/create_content_editor_spec.js | 8 - .../services/gl_api_markdown_deserializer_spec.js | 13 +- .../services/markdown_serializer_spec.js | 27 - spec/frontend/content_editor/test_utils.js | 19 +- .../contribution_event_approved_spec.js | 34 +- .../contribution_event_base_spec.js | 79 +- .../contribution_event_expired_spec.js | 30 + .../contribution_event_joined_spec.js | 30 + .../contribution_event_left_spec.js | 30 + .../contribution_event_merged_spec.js | 31 + .../contribution_event_private_spec.js | 33 + .../contribution_event_pushed_spec.js | 141 +++ .../components/contribution_events_spec.js | 37 +- .../components/resource_parent_link_spec.js | 46 +- .../components/target_link_spec.js | 43 +- spec/frontend/contribution_events/utils.js | 52 + spec/frontend/deploy_keys/components/app_spec.js | 82 +- .../design_description/description_form_spec.js | 35 +- .../__snapshots__/design_note_spec.js.snap | 86 -- .../design_notes/design_discussion_spec.js | 12 +- .../components/design_notes/design_note_spec.js | 268 +++-- .../components/design_presentation_spec.js | 4 - .../components/design_todo_button_spec.js | 4 - .../design_management/mock_data/apollo_mock.js | 29 + .../design_management/mock_data/discussion.js | 12 +- spec/frontend/design_management/mock_data/notes.js | 3 + spec/frontend/diffs/components/app_spec.js | 12 + spec/frontend/diffs/components/commit_item_spec.js | 2 +- .../components/diff_code_quality_item_spec.js | 37 +- .../diffs/components/diff_code_quality_spec.js | 55 +- .../frontend/diffs/components/diff_content_spec.js | 72 ++ .../diffs/components/diff_discussions_spec.js | 16 +- .../diffs/components/diff_file_header_spec.js | 26 +- spec/frontend/diffs/components/diff_file_spec.js | 181 ++- .../diffs/components/diff_inline_findings_spec.js | 33 + .../diffs/components/diff_line_note_form_spec.js | 40 + spec/frontend/diffs/components/diff_line_spec.js | 21 + spec/frontend/diffs/components/diff_row_spec.js | 14 + spec/frontend/diffs/components/tree_list_spec.js | 36 +- spec/frontend/diffs/mock_data/diff_code_quality.js | 87 +- spec/frontend/diffs/store/actions_spec.js | 65 +- spec/frontend/diffs/store/mutations_spec.js | 2 +- spec/frontend/diffs/store/utils_spec.js | 6 +- spec/frontend/drawio/drawio_editor_spec.js | 1 - spec/frontend/dropzone_input_spec.js | 2 +- .../editor/source_editor_extension_base_spec.js | 1 - .../source_editor_markdown_livepreview_ext_spec.js | 7 - .../frontend/editor/source_editor_yaml_ext_spec.js | 4 - spec/frontend/emoji/index_spec.js | 95 ++ .../frontend/environments/edit_environment_spec.js | 158 +-- .../frontend/environments/environment_form_spec.js | 247 ++++- spec/frontend/environments/graphql/mock_data.js | 5 + .../environments/graphql/resolvers_spec.js | 46 + .../environments/new_environment_item_spec.js | 60 +- spec/frontend/environments/new_environment_spec.js | 108 +- .../components/error_details_spec.js | 11 +- spec/frontend/fixtures/groups.rb | 33 +- spec/frontend/fixtures/issues.rb | 4 +- spec/frontend/fixtures/metrics_dashboard.rb | 42 - spec/frontend/fixtures/milestones.rb | 43 - spec/frontend/fixtures/pipeline_schedules.rb | 6 + .../frontend/fixtures/static/line_highlighter.html | 85 +- spec/frontend/fixtures/static/textarea.html | 27 + spec/frontend/fixtures/timezones.rb | 2 +- spec/frontend/fixtures/users.rb | 9 +- spec/frontend/frequent_items/mock_data.js | 2 +- spec/frontend/gfm_auto_complete_spec.js | 2 +- .../security_patch_upgrade_alert_modal_spec.js | 33 +- spec/frontend/groups/components/app_spec.js | 8 +- .../groups/components/overview_tabs_spec.js | 67 +- .../service/archived_projects_service_spec.js | 90 ++ .../frontend/groups/service/groups_service_spec.js | 19 +- spec/frontend/header_search/init_spec.js | 2 - .../frontend/ide/components/ide_status_bar_spec.js | 44 +- spec/frontend/ide/components/repo_editor_spec.js | 1 - spec/frontend/ide/mock_data.js | 2 + .../invite_members/components/group_select_spec.js | 174 ++- .../components/invite_groups_modal_spec.js | 31 +- .../components/members_token_select_spec.js | 13 +- .../components/related_issuable_item_spec.js | 6 +- .../issuable/components/status_box_spec.js | 2 + spec/frontend/issuable/issuable_form_spec.js | 35 + .../popover/components/issue_popover_spec.js | 2 +- .../issuable/popover/components/mr_popover_spec.js | 2 +- spec/frontend/issuable/popover/index_spec.js | 68 +- .../components/related_issues_block_spec.js | 51 +- .../components/related_issues_root_spec.js | 109 +- .../components/issues_dashboard_app_spec.js | 3 +- .../empty_state_without_any_issues_spec.js | 1 + .../issues/list/components/issues_list_app_spec.js | 1 + .../show/components/delete_issue_modal_spec.js | 9 +- .../show/components/fields/description_spec.js | 103 +- .../issues/show/components/header_actions_spec.js | 36 +- .../incidents/timeline_events_item_spec.js | 10 +- .../show/components/task_list_item_actions_spec.js | 6 +- spec/frontend/issues/show/issue_spec.js | 2 +- .../branches/components/project_dropdown_spec.js | 1 - .../components/source_branch_dropdown_spec.js | 1 - .../subscriptions/components/app_spec.js | 8 + .../components/feedback_banner_spec.js | 45 + spec/frontend/jobs/components/job/job_app_spec.js | 2 +- .../jobs/components/job/job_container_item_spec.js | 16 +- spec/frontend/lib/utils/common_utils_spec.js | 34 + .../lib/utils/datetime/date_format_utility_spec.js | 15 + spec/frontend/lib/utils/downloader_spec.js | 4 - spec/frontend/lib/utils/forms_spec.js | 111 +- spec/frontend/lib/utils/ref_validator_spec.js | 23 +- .../members/components/table/members_table_spec.js | 2 + .../members/components/table/role_dropdown_spec.js | 10 + .../merge_requests/generated_content_spec.js | 310 ++++++ .../index/components/ml_models_index_spec.js | 39 + .../routes/models/index/components/mock_data.js | 12 + .../__snapshots__/dashboard_template_spec.js.snap | 155 --- .../__snapshots__/empty_state_spec.js.snap | 55 - .../__snapshots__/group_empty_state_spec.js.snap | 160 --- .../components/charts/annotations_spec.js | 95 -- .../monitoring/components/charts/anomaly_spec.js | 304 ----- .../monitoring/components/charts/bar_spec.js | 53 - .../monitoring/components/charts/column_spec.js | 118 -- .../components/charts/empty_chart_spec.js | 21 - .../monitoring/components/charts/gauge_spec.js | 210 ---- .../monitoring/components/charts/heatmap_spec.js | 93 -- .../monitoring/components/charts/options_spec.js | 327 ------ .../components/charts/single_stat_spec.js | 94 -- .../components/charts/stacked_column_spec.js | 193 ---- .../components/charts/time_series_spec.js | 748 ------------- .../components/create_dashboard_modal_spec.js | 44 - .../components/dashboard_actions_menu_spec.js | 421 ------- .../monitoring/components/dashboard_header_spec.js | 395 ------- .../components/dashboard_panel_builder_spec.js | 226 ---- .../monitoring/components/dashboard_panel_spec.js | 582 ---------- .../monitoring/components/dashboard_spec.js | 784 ------------- .../components/dashboard_template_spec.js | 41 - .../components/dashboard_url_time_spec.js | 159 --- .../components/dashboards_dropdown_spec.js | 170 --- .../components/duplicate_dashboard_form_spec.js | 166 --- .../components/duplicate_dashboard_modal_spec.js | 110 -- .../components/embeds/embed_group_spec.js | 157 --- .../components/embeds/metric_embed_spec.js | 100 -- .../monitoring/components/embeds/mock_data.js | 86 -- .../monitoring/components/empty_state_spec.js | 55 - .../monitoring/components/graph_group_spec.js | 144 --- .../components/group_empty_state_spec.js | 47 - .../monitoring/components/links_section_spec.js | 64 -- .../monitoring/components/refresh_button_spec.js | 139 --- .../components/variables/dropdown_field_spec.js | 62 -- .../components/variables/text_field_spec.js | 55 - .../components/variables_section_spec.js | 125 --- spec/frontend/monitoring/csv_export_spec.js | 126 --- spec/frontend/monitoring/fixture_data.js | 49 - spec/frontend/monitoring/graph_data.js | 274 ----- spec/frontend/monitoring/mock_data.js | 574 ---------- .../monitoring/pages/dashboard_page_spec.js | 60 - .../monitoring/pages/panel_new_page_spec.js | 93 -- spec/frontend/monitoring/requests/index_spec.js | 157 --- spec/frontend/monitoring/router_spec.js | 106 -- spec/frontend/monitoring/store/actions_spec.js | 1167 -------------------- .../monitoring/store/embed_group/actions_spec.js | 16 - .../monitoring/store/embed_group/getters_spec.js | 19 - .../monitoring/store/embed_group/mutations_spec.js | 16 - spec/frontend/monitoring/store/getters_spec.js | 457 -------- spec/frontend/monitoring/store/index_spec.js | 23 - spec/frontend/monitoring/store/mutations_spec.js | 586 ---------- spec/frontend/monitoring/store/utils_spec.js | 893 --------------- .../monitoring/store/variable_mapping_spec.js | 209 ---- spec/frontend/monitoring/store_utils.js | 80 -- spec/frontend/monitoring/stubs/modal_stub.js | 11 - spec/frontend/monitoring/utils_spec.js | 464 -------- spec/frontend/monitoring/validators_spec.js | 80 -- .../frontend/notes/components/comment_form_spec.js | 51 +- .../notes/components/comment_type_dropdown_spec.js | 40 +- .../components/diff_discussion_header_spec.js | 105 +- .../notes/components/discussion_counter_spec.js | 25 +- .../notes/components/mr_discussion_filter_spec.js | 28 +- spec/frontend/notes/components/note_form_spec.js | 35 +- .../notes/components/noteable_note_spec.js | 30 +- spec/frontend/notes/components/notes_app_spec.js | 26 +- spec/frontend/notes/deprecated_notes_spec.js | 3 +- .../notes/mixins/discussion_navigation_spec.js | 1 - spec/frontend/notes/mock_data.js | 2 +- spec/frontend/notes/utils_spec.js | 31 +- .../notification_email_listbox_input_spec.js | 2 + spec/frontend/observability/client_spec.js | 66 ++ .../observability/observability_app_spec.js | 15 +- .../observability/observability_container_spec.js | 134 +++ spec/frontend/observability/skeleton_spec.js | 44 +- .../groups_and_projects/components/app_spec.js | 99 ++ .../groups_and_projects/components/mock_data.js | 98 ++ .../components/details_page/tags_list_row_spec.js | 36 +- .../dependency_proxy/app_spec.js | 94 +- .../components/manifest_list_spec.js | 24 +- .../components/manifests_empty_state_spec.js | 81 ++ .../components/details/package_files_spec.js | 143 ++- .../components/details/version_row_spec.js | 40 +- .../__snapshots__/package_list_row_spec.js.snap | 21 +- .../components/list/package_list_row_spec.js | 82 +- .../components/list/packages_list_spec.js | 26 +- .../package_registry/mock_data.js | 16 +- .../package_registry/pages/list_spec.js | 6 +- .../jobs/components/cancel_jobs_modal_spec.js | 6 +- .../pages/groups/new/components/app_spec.js | 5 + .../components/interval_pattern_input_spec.js | 13 + .../shared/wikis/components/wiki_form_spec.js | 45 +- .../pipeline_wizard/components/commit_spec.js | 8 - .../pipeline_wizard/components/editor_spec.js | 46 +- .../failure_widget/failed_job_details_spec.js | 252 +++++ .../failure_widget/failed_jobs_list_spec.js | 236 ++++ .../pipelines_list/failure_widget/mock.js | 54 +- .../pipeline_failed_jobs_widget_spec.js | 114 +- .../failure_widget/widget_failed_job_row_spec.js | 140 --- .../graph/graph_component_wrapper_spec.js | 56 +- .../pipelines/graph/job_name_component_spec.js | 4 +- .../pipelines/graph/linked_pipeline_spec.js | 6 +- spec/frontend/pipelines/graph/mock_data.js | 9 + .../pipelines/graph_shared/links_inner_spec.js | 1 - spec/frontend/pipelines/header_component_spec.js | 246 ----- spec/frontend/pipelines/mock_data.js | 8 +- .../pipelines/pipeline_details_header_spec.js | 44 +- .../frontend/pipelines/pipelines_artifacts_spec.js | 23 +- spec/frontend/pipelines/pipelines_table_spec.js | 26 - spec/frontend/pipelines/time_ago_spec.js | 13 +- .../account/components/update_username_spec.js | 7 +- spec/frontend/profile/components/follow_spec.js | 49 +- .../profile/components/followers_tab_spec.js | 2 + .../profile/components/following_tab_spec.js | 108 +- .../profile/components/profile_tabs_spec.js | 19 +- .../components/snippets/snippets_tab_spec.js | 43 +- spec/frontend/profile/mock_data.js | 1 + .../components/profile_preferences_spec.js | 8 - .../commits/components/author_select_spec.js | 110 +- .../projects/compare/components/app_spec.js | 104 +- .../new/components/__snapshots__/app_spec.js.snap | 30 + spec/frontend/projects/new/components/app_spec.js | 6 + .../projects/settings/access_dropdown_spec.js | 25 + .../branch_rules/components/view/index_spec.js | 1 - .../components/service_desk_root_spec.js | 10 +- .../components/service_desk_setting_spec.js | 24 +- .../terraform_notification_spec.js | 3 +- .../components/related_issuable_input_spec.js | 98 +- .../components/releases_pagination_spec.js | 15 +- .../__snapshots__/last_commit_spec.js.snap | 4 +- .../components/blob_content_viewer_spec.js | 7 +- .../repository/components/breadcrumbs_spec.js | 43 +- .../repository/mixins/highlight_mixin_spec.js | 8 +- spec/frontend/scripts/frontend/po_to_json_spec.js | 8 +- spec/frontend/search/mock_data.js | 21 +- .../search/sidebar/components/label_filter_spec.js | 30 + .../components/scope_legacy_navigation_spec.js | 6 +- .../components/scope_sidebar_navigation_spec.js | 4 +- spec/frontend/search/topbar/components/app_spec.js | 16 +- .../search/topbar/components/group_filter_spec.js | 5 +- .../topbar/components/project_filter_spec.js | 5 +- .../service_desk/components/info_banner_spec.js | 81 ++ .../components/service_desk_list_app_spec.js | 151 +++ spec/frontend/service_desk/mock_data.js | 118 ++ .../assignees/assignee_avatar_link_spec.js | 96 +- .../components/assignees/assignee_title_spec.js | 21 - .../sidebar/components/assignees/assignees_spec.js | 5 +- .../assignees/collapsed_assignee_list_spec.js | 2 +- .../components/assignees/sidebar_assignees_spec.js | 22 +- .../components/lock/issuable_lock_form_spec.js | 8 + .../components/participants/participants_spec.js | 13 + .../reviewers/reviewer_avatar_link_spec.js | 66 ++ .../severity/sidebar_severity_widget_spec.js | 11 +- .../time_tracking/create_timelog_form_spec.js | 50 +- .../sidebar/components/time_tracking/mock_data.js | 13 + .../components/time_tracking/report_spec.js | 57 +- .../todo_toggle/__snapshots__/todo_spec.js.snap | 1 + .../components/todo_toggle/todo_button_spec.js | 1 - spec/frontend/sidebar/sidebar_mediator_spec.js | 2 - .../snippet_description_edit_spec.js.snap | 9 +- .../components/snippet_visibility_edit_spec.js | 11 + .../super_sidebar/components/create_menu_spec.js | 15 +- .../command_palette/command_palette_items_spec.js | 93 +- .../global_search/command_palette/mock_data.js | 43 + .../global_search/command_palette/utils_spec.js | 13 + .../global_search/components/global_search_spec.js | 175 ++- .../components/global_search/mock_data.js | 14 - .../super_sidebar/components/help_center_spec.js | 6 +- .../components/sidebar_peek_behavior_spec.js | 12 + .../super_sidebar/components/super_sidebar_spec.js | 15 + .../components/super_sidebar_toggle_spec.js | 15 +- .../super_sidebar/components/user_bar_spec.js | 8 +- .../super_sidebar/components/user_menu_spec.js | 13 +- .../components/user_name_group_spec.js | 2 +- spec/frontend/super_sidebar/mock_data.js | 1 + .../super_sidebar_collapsed_state_manager_spec.js | 32 +- .../tags/components/delete_tag_modal_spec.js | 19 +- .../token_access/outbound_token_access_spec.js | 125 +-- .../tracing/components/tracing_empty_state_spec.js | 44 + .../tracing/components/tracing_list_spec.js | 131 +++ .../tracing/components/tracing_table_list_spec.js | 63 ++ spec/frontend/tracing/list_index_spec.js | 37 + spec/frontend/tracking/internal_events_spec.js | 100 ++ spec/frontend/tracking/tracking_spec.js | 1 - spec/frontend/tracking/utils_spec.js | 37 + .../storage/components/usage_graph_spec.js | 9 +- spec/frontend/usage_quotas/storage/mock_data.js | 12 +- .../actions/components/user_actions_app_spec.js | 38 + spec/frontend/vue_compat_test_setup.js | 74 +- .../mr_widget_commit_message_dropdown_spec.js | 25 +- .../states/mr_widget_failed_to_merge_spec.js | 19 +- .../states/mr_widget_ready_to_merge_spec.js | 51 +- .../__snapshots__/dynamic_content_spec.js.snap | 32 +- .../components/widget/widget_spec.js | 39 +- .../extentions/terraform/index_spec.js | 46 +- .../frontend/vue_merge_request_widget/mock_data.js | 6 +- .../mr_widget_options_spec.js | 208 ++-- .../alert_management_sidebar_todo_spec.js | 65 +- .../vue_shared/alert_details/alert_status_spec.js | 120 +- .../vue_shared/components/actions_button_spec.js | 32 +- .../vue_shared/components/awards_list_spec.js | 13 + .../components/blob_viewers/rich_viewer_spec.js | 11 +- .../frontend/vue_shared/components/ci_icon_spec.js | 63 +- .../components/code_block_highlighted_spec.js | 7 + .../confirm_danger/confirm_danger_modal_spec.js | 4 +- .../components/diff_stats_dropdown_spec.js | 9 +- .../components/entity_select/entity_select_spec.js | 7 +- .../components/entity_select/group_select_spec.js | 12 + .../entity_select/project_select_spec.js | 12 + .../filtered_search_bar_root_spec.js | 47 +- .../tokens/crm_contact_token_spec.js | 35 +- .../tokens/crm_organization_token_spec.js | 33 +- .../components/listbox_input/listbox_input_spec.js | 28 +- .../markdown/comment_templates_dropdown_spec.js | 22 +- .../markdown/editor_mode_switcher_spec.js | 100 +- .../vue_shared/components/markdown/field_spec.js | 18 +- .../vue_shared/components/markdown/header_spec.js | 112 +- .../components/markdown/markdown_editor_spec.js | 36 +- .../components/markdown/non_gfm_markdown_spec.js | 157 +++ .../vue_shared/components/markdown/toolbar_spec.js | 73 +- .../new_resource_dropdown_spec.js | 41 +- .../paginated_table_with_search_and_tabs_spec.js | 2 + .../projects_list/projects_list_item_spec.js | 45 +- .../components/projects_list/projects_list_spec.js | 1 + .../components/registry/list_item_spec.js | 46 + .../runner_docker_instructions_spec.js | 5 +- .../runner_kubernetes_instructions_spec.js | 5 +- .../runner_instructions_modal_spec.js | 23 +- .../__snapshots__/security_summary_spec.js.snap | 144 --- .../merge_request_artifact_download_spec.js | 104 -- .../components/security_reports/help_icon_spec.js | 63 -- .../security_reports/security_summary_spec.js | 33 - .../source_viewer/components/chunk_new_spec.js | 2 - .../source_viewer/highlight_util_spec.js | 11 +- .../source_viewer/plugins/wrap_child_nodes_spec.js | 7 +- .../source_viewer/plugins/wrap_lines_spec.js | 12 + .../source_viewer/source_viewer_new_spec.js | 6 + .../upload_dropzone/upload_dropzone_spec.js | 36 +- .../user_avatar/user_avatar_link_spec.js | 14 + .../user_avatar/user_avatar_list_spec.js | 5 + .../components/user_popover/user_popover_spec.js | 36 +- .../vue_shared/components/user_select_spec.js | 27 +- .../vue_shared/components/web_ide_link_spec.js | 27 +- .../frontend/vue_shared/issuable/list/mock_data.js | 4 + .../show/components/issuable_header_spec.js | 25 +- .../new_namespace/components/welcome_spec.js | 12 + .../new_namespace/new_namespace_page_spec.js | 9 +- .../vue_shared/security_reports/mock_data.js | 136 --- .../security_reports/security_reports_app_spec.js | 267 ----- .../security_reports/store/getters_spec.js | 182 --- .../store/modules/sast/actions_spec.js | 197 ---- .../store/modules/sast/mutations_spec.js | 84 -- .../store/modules/secret_detection/actions_spec.js | 198 ---- .../modules/secret_detection/mutations_spec.js | 84 -- .../security_reports/store/utils_spec.js | 63 -- .../vue_shared/security_reports/utils_spec.js | 48 - .../notes/work_item_note_actions_spec.js | 63 +- .../notes/work_item_note_awards_list_spec.js | 147 +++ .../components/notes/work_item_note_spec.js | 30 + .../components/work_item_assignees_spec.js | 8 +- .../work_item_attributes_wrapper_spec.js | 107 ++ .../components/work_item_award_emoji_spec.js | 231 +++- .../components/work_item_description_spec.js | 13 - .../work_items/components/work_item_detail_spec.js | 292 ++--- .../work_items/components/work_item_labels_spec.js | 14 - .../work_item_links/work_item_tree_spec.js | 5 +- .../work_items/components/work_item_todos_spec.js | 111 +- spec/frontend/work_items/mock_data.js | 258 ++--- spec/frontend/work_items/notes/award_utils_spec.js | 109 ++ spec/frontend/work_items/router_spec.js | 39 +- spec/frontend/work_items/utils_spec.js | 21 +- 480 files changed, 12850 insertions(+), 20036 deletions(-) create mode 100644 spec/frontend/__helpers__/set_vue_error_handler.js delete mode 100644 spec/frontend/actioncable_connection_monitor_spec.js create mode 100644 spec/frontend/admin/abuse_reports/components/abuse_category_spec.js delete mode 100644 spec/frontend/behaviors/markdown/render_metrics_spec.js delete mode 100644 spec/frontend/ci/reports/components/__snapshots__/grouped_issues_list_spec.js.snap delete mode 100644 spec/frontend/ci/reports/components/grouped_issues_list_spec.js delete mode 100644 spec/frontend/ci/reports/components/summary_row_spec.js create mode 100644 spec/frontend/ci/reports/utils_spec.js create mode 100644 spec/frontend/ci/runner/components/runner_delete_action_spec.js create mode 100644 spec/frontend/ci/runner/components/runner_delete_disclosure_dropdown_item_spec.js create mode 100644 spec/frontend/ci/runner/components/runner_detail_spec.js create mode 100644 spec/frontend/ci/runner/components/runner_edit_disclosure_dropdown_item_spec.js create mode 100644 spec/frontend/ci/runner/components/runner_header_actions_spec.js create mode 100644 spec/frontend/ci/runner/components/runner_pause_action_spec.js create mode 100644 spec/frontend/ci/runner/components/runner_pause_disclosure_dropdown_item_spec.js delete mode 100644 spec/frontend/commit/commit_pipeline_status_component_spec.js create mode 100644 spec/frontend/commit/commit_pipeline_status_spec.js create mode 100644 spec/frontend/content_editor/components/wrappers/image_spec.js create mode 100644 spec/frontend/content_editor/extensions/code_suggestion_spec.js delete mode 100644 spec/frontend/content_editor/extensions/comment_spec.js create mode 100644 spec/frontend/content_editor/extensions/copy_paste_spec.js create mode 100644 spec/frontend/content_editor/extensions/paragraph_spec.js delete mode 100644 spec/frontend/content_editor/extensions/paste_markdown_spec.js create mode 100644 spec/frontend/content_editor/services/code_suggestion_utils_spec.js create mode 100644 spec/frontend/contribution_events/components/contribution_event/contribution_event_expired_spec.js create mode 100644 spec/frontend/contribution_events/components/contribution_event/contribution_event_joined_spec.js create mode 100644 spec/frontend/contribution_events/components/contribution_event/contribution_event_left_spec.js create mode 100644 spec/frontend/contribution_events/components/contribution_event/contribution_event_merged_spec.js create mode 100644 spec/frontend/contribution_events/components/contribution_event/contribution_event_private_spec.js create mode 100644 spec/frontend/contribution_events/components/contribution_event/contribution_event_pushed_spec.js create mode 100644 spec/frontend/contribution_events/utils.js delete mode 100644 spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap create mode 100644 spec/frontend/diffs/components/diff_inline_findings_spec.js delete mode 100644 spec/frontend/fixtures/metrics_dashboard.rb delete mode 100644 spec/frontend/fixtures/milestones.rb create mode 100644 spec/frontend/fixtures/static/textarea.html create mode 100644 spec/frontend/groups/service/archived_projects_service_spec.js create mode 100644 spec/frontend/jira_connect/subscriptions/components/feedback_banner_spec.js create mode 100644 spec/frontend/merge_requests/generated_content_spec.js create mode 100644 spec/frontend/ml/model_registry/routes/models/index/components/ml_models_index_spec.js create mode 100644 spec/frontend/ml/model_registry/routes/models/index/components/mock_data.js delete mode 100644 spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap delete mode 100644 spec/frontend/monitoring/components/__snapshots__/empty_state_spec.js.snap delete mode 100644 spec/frontend/monitoring/components/__snapshots__/group_empty_state_spec.js.snap delete mode 100644 spec/frontend/monitoring/components/charts/annotations_spec.js delete mode 100644 spec/frontend/monitoring/components/charts/anomaly_spec.js delete mode 100644 spec/frontend/monitoring/components/charts/bar_spec.js delete mode 100644 spec/frontend/monitoring/components/charts/column_spec.js delete mode 100644 spec/frontend/monitoring/components/charts/empty_chart_spec.js delete mode 100644 spec/frontend/monitoring/components/charts/gauge_spec.js delete mode 100644 spec/frontend/monitoring/components/charts/heatmap_spec.js delete mode 100644 spec/frontend/monitoring/components/charts/options_spec.js delete mode 100644 spec/frontend/monitoring/components/charts/single_stat_spec.js delete mode 100644 spec/frontend/monitoring/components/charts/stacked_column_spec.js delete mode 100644 spec/frontend/monitoring/components/charts/time_series_spec.js delete mode 100644 spec/frontend/monitoring/components/create_dashboard_modal_spec.js delete mode 100644 spec/frontend/monitoring/components/dashboard_actions_menu_spec.js delete mode 100644 spec/frontend/monitoring/components/dashboard_header_spec.js delete mode 100644 spec/frontend/monitoring/components/dashboard_panel_builder_spec.js delete mode 100644 spec/frontend/monitoring/components/dashboard_panel_spec.js delete mode 100644 spec/frontend/monitoring/components/dashboard_spec.js delete mode 100644 spec/frontend/monitoring/components/dashboard_template_spec.js delete mode 100644 spec/frontend/monitoring/components/dashboard_url_time_spec.js delete mode 100644 spec/frontend/monitoring/components/dashboards_dropdown_spec.js delete mode 100644 spec/frontend/monitoring/components/duplicate_dashboard_form_spec.js delete mode 100644 spec/frontend/monitoring/components/duplicate_dashboard_modal_spec.js delete mode 100644 spec/frontend/monitoring/components/embeds/embed_group_spec.js delete mode 100644 spec/frontend/monitoring/components/embeds/metric_embed_spec.js delete mode 100644 spec/frontend/monitoring/components/embeds/mock_data.js delete mode 100644 spec/frontend/monitoring/components/empty_state_spec.js delete mode 100644 spec/frontend/monitoring/components/graph_group_spec.js delete mode 100644 spec/frontend/monitoring/components/group_empty_state_spec.js delete mode 100644 spec/frontend/monitoring/components/links_section_spec.js delete mode 100644 spec/frontend/monitoring/components/refresh_button_spec.js delete mode 100644 spec/frontend/monitoring/components/variables/dropdown_field_spec.js delete mode 100644 spec/frontend/monitoring/components/variables/text_field_spec.js delete mode 100644 spec/frontend/monitoring/components/variables_section_spec.js delete mode 100644 spec/frontend/monitoring/csv_export_spec.js delete mode 100644 spec/frontend/monitoring/fixture_data.js delete mode 100644 spec/frontend/monitoring/graph_data.js delete mode 100644 spec/frontend/monitoring/mock_data.js delete mode 100644 spec/frontend/monitoring/pages/dashboard_page_spec.js delete mode 100644 spec/frontend/monitoring/pages/panel_new_page_spec.js delete mode 100644 spec/frontend/monitoring/requests/index_spec.js delete mode 100644 spec/frontend/monitoring/router_spec.js delete mode 100644 spec/frontend/monitoring/store/actions_spec.js delete mode 100644 spec/frontend/monitoring/store/embed_group/actions_spec.js delete mode 100644 spec/frontend/monitoring/store/embed_group/getters_spec.js delete mode 100644 spec/frontend/monitoring/store/embed_group/mutations_spec.js delete mode 100644 spec/frontend/monitoring/store/getters_spec.js delete mode 100644 spec/frontend/monitoring/store/index_spec.js delete mode 100644 spec/frontend/monitoring/store/mutations_spec.js delete mode 100644 spec/frontend/monitoring/store/utils_spec.js delete mode 100644 spec/frontend/monitoring/store/variable_mapping_spec.js delete mode 100644 spec/frontend/monitoring/store_utils.js delete mode 100644 spec/frontend/monitoring/stubs/modal_stub.js delete mode 100644 spec/frontend/monitoring/utils_spec.js delete mode 100644 spec/frontend/monitoring/validators_spec.js create mode 100644 spec/frontend/observability/client_spec.js create mode 100644 spec/frontend/observability/observability_container_spec.js create mode 100644 spec/frontend/organizations/groups_and_projects/components/app_spec.js create mode 100644 spec/frontend/organizations/groups_and_projects/components/mock_data.js create mode 100644 spec/frontend/packages_and_registries/dependency_proxy/components/manifests_empty_state_spec.js create mode 100644 spec/frontend/pipelines/components/pipelines_list/failure_widget/failed_job_details_spec.js create mode 100644 spec/frontend/pipelines/components/pipelines_list/failure_widget/failed_jobs_list_spec.js delete mode 100644 spec/frontend/pipelines/components/pipelines_list/failure_widget/widget_failed_job_row_spec.js delete mode 100644 spec/frontend/pipelines/header_component_spec.js create mode 100644 spec/frontend/projects/new/components/__snapshots__/app_spec.js.snap create mode 100644 spec/frontend/service_desk/components/info_banner_spec.js create mode 100644 spec/frontend/service_desk/components/service_desk_list_app_spec.js create mode 100644 spec/frontend/service_desk/mock_data.js create mode 100644 spec/frontend/sidebar/components/reviewers/reviewer_avatar_link_spec.js create mode 100644 spec/frontend/tracing/components/tracing_empty_state_spec.js create mode 100644 spec/frontend/tracing/components/tracing_list_spec.js create mode 100644 spec/frontend/tracing/components/tracing_table_list_spec.js create mode 100644 spec/frontend/tracing/list_index_spec.js create mode 100644 spec/frontend/tracking/internal_events_spec.js create mode 100644 spec/frontend/users/profile/actions/components/user_actions_app_spec.js create mode 100644 spec/frontend/vue_shared/components/markdown/non_gfm_markdown_spec.js delete mode 100644 spec/frontend/vue_shared/components/security_reports/__snapshots__/security_summary_spec.js.snap delete mode 100644 spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js delete mode 100644 spec/frontend/vue_shared/components/security_reports/help_icon_spec.js delete mode 100644 spec/frontend/vue_shared/components/security_reports/security_summary_spec.js create mode 100644 spec/frontend/vue_shared/components/source_viewer/plugins/wrap_lines_spec.js delete mode 100644 spec/frontend/vue_shared/security_reports/security_reports_app_spec.js delete mode 100644 spec/frontend/vue_shared/security_reports/store/getters_spec.js delete mode 100644 spec/frontend/vue_shared/security_reports/store/modules/sast/actions_spec.js delete mode 100644 spec/frontend/vue_shared/security_reports/store/modules/sast/mutations_spec.js delete mode 100644 spec/frontend/vue_shared/security_reports/store/modules/secret_detection/actions_spec.js delete mode 100644 spec/frontend/vue_shared/security_reports/store/modules/secret_detection/mutations_spec.js delete mode 100644 spec/frontend/vue_shared/security_reports/store/utils_spec.js delete mode 100644 spec/frontend/vue_shared/security_reports/utils_spec.js create mode 100644 spec/frontend/work_items/components/notes/work_item_note_awards_list_spec.js create mode 100644 spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js create mode 100644 spec/frontend/work_items/notes/award_utils_spec.js (limited to 'spec/frontend') diff --git a/spec/frontend/__helpers__/set_vue_error_handler.js b/spec/frontend/__helpers__/set_vue_error_handler.js new file mode 100644 index 00000000000..d254630d1e4 --- /dev/null +++ b/spec/frontend/__helpers__/set_vue_error_handler.js @@ -0,0 +1,30 @@ +import Vue from 'vue'; + +const modifiedInstances = []; + +export function setVueErrorHandler({ instance, handler }) { + if (Vue.version.startsWith('2')) { + // only global handlers are supported + const { config } = Vue; + config.errorHandler = handler; + return; + } + + // eslint-disable-next-line no-param-reassign + instance.$.appContext.config.errorHandler = handler; + modifiedInstances.push(instance); +} + +export function resetVueErrorHandler() { + if (Vue.version.startsWith('2')) { + const { config } = Vue; + config.errorHandler = null; + return; + } + + modifiedInstances.forEach((instance) => { + // eslint-disable-next-line no-param-reassign + instance.$.appContext.config.errorHandler = null; + }); + modifiedInstances.length = 0; +} diff --git a/spec/frontend/access_tokens/components/tokens_app_spec.js b/spec/frontend/access_tokens/components/tokens_app_spec.js index 6e7dee6a2cc..59cc8e25414 100644 --- a/spec/frontend/access_tokens/components/tokens_app_spec.js +++ b/spec/frontend/access_tokens/components/tokens_app_spec.js @@ -43,8 +43,8 @@ describe('TokensApp', () => { }) => { const container = extendedWrapper(wrapper.findByTestId(testId)); - expect(container.findByText(expectedLabel, { selector: 'h4' }).exists()).toBe(true); - expect(container.findByText(expectedDescription).exists()).toBe(true); + expect(container.findByText(expectedLabel).exists()).toBe(true); + expect(container.findByText(expectedDescription, { exact: false }).exists()).toBe(true); expect(container.findByText(expectedInputDescription, { exact: false }).exists()).toBe(true); expect(container.findByText('reset this token').attributes()).toMatchObject({ 'data-confirm': expectedResetConfirmMessage, diff --git a/spec/frontend/actioncable_connection_monitor_spec.js b/spec/frontend/actioncable_connection_monitor_spec.js deleted file mode 100644 index c68eb53acde..00000000000 --- a/spec/frontend/actioncable_connection_monitor_spec.js +++ /dev/null @@ -1,79 +0,0 @@ -import ConnectionMonitor from '~/actioncable_connection_monitor'; - -describe('ConnectionMonitor', () => { - let monitor; - - beforeEach(() => { - monitor = new ConnectionMonitor({}); - }); - - describe('#getPollInterval', () => { - beforeEach(() => { - Math.originalRandom = Math.random; - }); - afterEach(() => { - Math.random = Math.originalRandom; - }); - - const { staleThreshold, reconnectionBackoffRate } = ConnectionMonitor; - const backoffFactor = 1 + reconnectionBackoffRate; - const ms = 1000; - - it('uses exponential backoff', () => { - Math.random = () => 0; - - monitor.reconnectAttempts = 0; - expect(monitor.getPollInterval()).toEqual(staleThreshold * ms); - - monitor.reconnectAttempts = 1; - expect(monitor.getPollInterval()).toEqual(staleThreshold * backoffFactor * ms); - - monitor.reconnectAttempts = 2; - expect(monitor.getPollInterval()).toEqual( - staleThreshold * backoffFactor * backoffFactor * ms, - ); - }); - - it('caps exponential backoff after some number of reconnection attempts', () => { - Math.random = () => 0; - monitor.reconnectAttempts = 42; - const cappedPollInterval = monitor.getPollInterval(); - - monitor.reconnectAttempts = 9001; - expect(monitor.getPollInterval()).toEqual(cappedPollInterval); - }); - - it('uses 100% jitter when 0 reconnection attempts', () => { - Math.random = () => 0; - expect(monitor.getPollInterval()).toEqual(staleThreshold * ms); - - Math.random = () => 0.5; - expect(monitor.getPollInterval()).toEqual(staleThreshold * 1.5 * ms); - }); - - it('uses reconnectionBackoffRate for jitter when >0 reconnection attempts', () => { - monitor.reconnectAttempts = 1; - - Math.random = () => 0.25; - expect(monitor.getPollInterval()).toEqual( - staleThreshold * backoffFactor * (1 + reconnectionBackoffRate * 0.25) * ms, - ); - - Math.random = () => 0.5; - expect(monitor.getPollInterval()).toEqual( - staleThreshold * backoffFactor * (1 + reconnectionBackoffRate * 0.5) * ms, - ); - }); - - it('applies jitter after capped exponential backoff', () => { - monitor.reconnectAttempts = 9001; - - Math.random = () => 0; - const withoutJitter = monitor.getPollInterval(); - Math.random = () => 0.5; - const withJitter = monitor.getPollInterval(); - - expect(withJitter).toBeGreaterThan(withoutJitter); - }); - }); -}); 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 27fe010c354..fa051f7a43a 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 @@ -45,14 +45,13 @@ describe('AddContextCommitsModal', () => { ...props, }, }); - return wrapper; }; const findModal = () => wrapper.findComponent(GlModal); const findSearch = () => wrapper.findComponent(GlFilteredSearch); beforeEach(() => { - wrapper = createWrapper(); + createWrapper(); }); it('renders modal with 2 tabs', () => { @@ -98,7 +97,7 @@ describe('AddContextCommitsModal', () => { }); it('enabled ok button when atleast one row is selected', async () => { - wrapper.vm.$store.state.selectedCommits = [{ ...commit, isSelected: true }]; + store.state.selectedCommits = [{ ...commit, isSelected: true }]; await nextTick(); expect(findModal().attributes('ok-disabled')).toBe(undefined); }); @@ -106,14 +105,14 @@ describe('AddContextCommitsModal', () => { describe('when in second tab, renders a modal with', () => { beforeEach(() => { - wrapper.vm.$store.state.tabIndex = 1; + store.state.tabIndex = 1; }); it('a disabled ok button when no row is selected', () => { expect(findModal().attributes('ok-disabled')).toBe('true'); }); it('an enabled ok button when atleast one row is selected', async () => { - wrapper.vm.$store.state.selectedCommits = [{ ...commit, isSelected: true }]; + store.state.selectedCommits = [{ ...commit, isSelected: true }]; await nextTick(); expect(findModal().attributes('ok-disabled')).toBe(undefined); }); @@ -126,7 +125,7 @@ describe('AddContextCommitsModal', () => { describe('has an ok button when clicked calls action', () => { it('"createContextCommits" when only new commits to be added', async () => { - wrapper.vm.$store.state.selectedCommits = [{ ...commit, isSelected: true }]; + store.state.selectedCommits = [{ ...commit, isSelected: true }]; findModal().vm.$emit('ok'); await nextTick(); expect(createContextCommits).toHaveBeenCalledWith(expect.anything(), { @@ -135,14 +134,14 @@ describe('AddContextCommitsModal', () => { }); }); it('"removeContextCommits" when only added commits are to be removed', async () => { - wrapper.vm.$store.state.toRemoveCommits = [commit.short_id]; + store.state.toRemoveCommits = [commit.short_id]; findModal().vm.$emit('ok'); 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', async () => { - wrapper.vm.$store.state.selectedCommits = [{ ...commit, isSelected: true }]; - wrapper.vm.$store.state.toRemoveCommits = [commit.short_id]; + store.state.selectedCommits = [{ ...commit, isSelected: true }]; + store.state.toRemoveCommits = [commit.short_id]; findModal().vm.$emit('ok'); await nextTick(); expect(createContextCommits).toHaveBeenCalledWith(expect.anything(), { diff --git a/spec/frontend/admin/abuse_reports/components/abuse_category_spec.js b/spec/frontend/admin/abuse_reports/components/abuse_category_spec.js new file mode 100644 index 00000000000..456df3b1857 --- /dev/null +++ b/spec/frontend/admin/abuse_reports/components/abuse_category_spec.js @@ -0,0 +1,43 @@ +import { GlLabel } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import AbuseCategory from '~/admin/abuse_reports/components/abuse_category.vue'; +import { ABUSE_CATEGORIES } from '~/admin/abuse_reports/constants'; +import { mockAbuseReports } from '../mock_data'; + +describe('AbuseCategory', () => { + let wrapper; + + const mockAbuseReport = mockAbuseReports[0]; + const category = ABUSE_CATEGORIES[mockAbuseReport.category]; + + const findLabel = () => wrapper.findComponent(GlLabel); + + const createComponent = (props = {}) => { + wrapper = shallowMountExtended(AbuseCategory, { + propsData: { + category: mockAbuseReport.category, + ...props, + }, + }); + }; + + beforeEach(() => { + createComponent(); + }); + + it('renders a label', () => { + expect(findLabel().exists()).toBe(true); + }); + + it('renders the label with the right background color for the category', () => { + expect(findLabel().props()).toMatchObject({ + backgroundColor: category.backgroundColor, + title: category.title, + target: null, + }); + }); + + it('renders the label with the right text color for the category', () => { + expect(findLabel().attributes('class')).toBe(`gl-text-${category.color}`); + }); +}); diff --git a/spec/frontend/admin/abuse_reports/components/abuse_report_row_spec.js b/spec/frontend/admin/abuse_reports/components/abuse_report_row_spec.js index f3cced81478..03bf510f3ad 100644 --- a/spec/frontend/admin/abuse_reports/components/abuse_report_row_spec.js +++ b/spec/frontend/admin/abuse_reports/components/abuse_report_row_spec.js @@ -1,6 +1,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import setWindowLocation from 'helpers/set_window_location_helper'; import AbuseReportRow from '~/admin/abuse_reports/components/abuse_report_row.vue'; +import AbuseCategory from '~/admin/abuse_reports/components/abuse_category.vue'; import ListItem from '~/vue_shared/components/registry/list_item.vue'; import { getTimeago } from '~/lib/utils/datetime_utility'; import { SORT_UPDATED_AT } from '~/admin/abuse_reports/constants'; @@ -11,7 +12,8 @@ describe('AbuseReportRow', () => { const mockAbuseReport = mockAbuseReports[0]; const findListItem = () => wrapper.findComponent(ListItem); - const findTitle = () => wrapper.findByTestId('title'); + const findAbuseCategory = () => wrapper.findComponent(AbuseCategory); + const findAbuseReportTitle = () => wrapper.findByTestId('abuse-report-title'); const findDisplayedDate = () => wrapper.findByTestId('abuse-report-date'); const createComponent = (props = {}) => { @@ -35,13 +37,13 @@ describe('AbuseReportRow', () => { const { reporter, reportedUser, category, reportPath } = mockAbuseReport; it('displays correctly formatted title', () => { - expect(findTitle().text()).toMatchInterpolatedText( + expect(findAbuseReportTitle().text()).toMatchInterpolatedText( `${reportedUser.name} reported for ${category} by ${reporter.name}`, ); }); it('links to the details page', () => { - expect(findTitle().attributes('href')).toEqual(reportPath); + expect(findAbuseReportTitle().attributes('href')).toEqual(reportPath); }); describe('when the reportedUser is missing', () => { @@ -50,7 +52,7 @@ describe('AbuseReportRow', () => { }); it('displays correctly formatted title', () => { - expect(findTitle().text()).toMatchInterpolatedText( + expect(findAbuseReportTitle().text()).toMatchInterpolatedText( `Deleted user reported for ${category} by ${reporter.name}`, ); }); @@ -62,7 +64,7 @@ describe('AbuseReportRow', () => { }); it('displays correctly formatted title', () => { - expect(findTitle().text()).toMatchInterpolatedText( + expect(findAbuseReportTitle().text()).toMatchInterpolatedText( `${reportedUser.name} reported for ${category} by Deleted user`, ); }); @@ -88,4 +90,8 @@ describe('AbuseReportRow', () => { }); }); }); + + it('renders abuse category', () => { + expect(findAbuseCategory().exists()).toBe(true); + }); }); diff --git a/spec/frontend/admin/applications/components/delete_application_spec.js b/spec/frontend/admin/applications/components/delete_application_spec.js index 315c38a2bbc..e0282b8c149 100644 --- a/spec/frontend/admin/applications/components/delete_application_spec.js +++ b/spec/frontend/admin/applications/components/delete_application_spec.js @@ -1,6 +1,7 @@ import { GlModal, GlSprintf } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; +import { stubComponent } from 'helpers/stub_component'; import DeleteApplication from '~/admin/applications/components/delete_application.vue'; const path = 'application/path/1'; @@ -14,6 +15,11 @@ describe('DeleteApplication', () => { const createComponent = () => { wrapper = shallowMount(DeleteApplication, { stubs: { + GlModal: stubComponent(GlModal, { + methods: { + show: jest.fn(), + }, + }), GlSprintf, }, }); @@ -36,7 +42,6 @@ describe('DeleteApplication', () => { describe('the modal component', () => { beforeEach(() => { - wrapper.vm.$refs.deleteModal.show = jest.fn(); document.querySelector('.js-application-delete-button').click(); }); diff --git a/spec/frontend/admin/broadcast_messages/components/message_form_spec.js b/spec/frontend/admin/broadcast_messages/components/message_form_spec.js index dca77e67cac..b937a58a742 100644 --- a/spec/frontend/admin/broadcast_messages/components/message_form_spec.js +++ b/spec/frontend/admin/broadcast_messages/components/message_form_spec.js @@ -5,7 +5,12 @@ import { createAlert } from '~/alert'; import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_BAD_REQUEST } from '~/lib/utils/http_status'; import MessageForm from '~/admin/broadcast_messages/components/message_form.vue'; -import { TYPE_BANNER, TYPE_NOTIFICATION, THEMES } from '~/admin/broadcast_messages/constants'; +import { + TYPE_BANNER, + TYPE_NOTIFICATION, + THEMES, + TARGET_OPTIONS, +} from '~/admin/broadcast_messages/constants'; import waitForPromises from 'helpers/wait_for_promises'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { MOCK_TARGET_ACCESS_LEVELS } from '../mock_data'; @@ -37,6 +42,8 @@ describe('MessageForm', () => { const findCancelButton = () => wrapper.findComponent('[data-testid=cancel-button]'); const findForm = () => wrapper.findComponent(GlForm); const findShowInCli = () => wrapper.findComponent('[data-testid=show-in-cli-checkbox]'); + const findTargetSelect = () => wrapper.findComponent('[data-testid=target-select]'); + const findTargetPath = () => wrapper.findComponent('[data-testid=target-path-input]'); function createComponent({ broadcastMessage = {} } = {}) { wrapper = mount(MessageForm, { @@ -112,10 +119,38 @@ describe('MessageForm', () => { }); }); - describe('target roles checkboxes', () => { - it('renders target roles', () => { + describe('target select', () => { + it('renders the first option and hide target path and target roles when creating message', () => { createComponent(); - expect(findTargetRoles().exists()).toBe(true); + expect(findTargetSelect().element.value).toBe(TARGET_OPTIONS[0].value); + expect(findTargetRoles().isVisible()).toBe(false); + expect(findTargetPath().isVisible()).toBe(false); + }); + + it('triggers displaying target path and target roles when selecting different options', async () => { + createComponent(); + const options = findTargetSelect().findAll('option'); + await options.at(1).setSelected(); + expect(findTargetPath().isVisible()).toBe(true); + expect(findTargetRoles().isVisible()).toBe(false); + + await options.at(2).setSelected(); + expect(findTargetPath().isVisible()).toBe(true); + expect(findTargetRoles().isVisible()).toBe(true); + }); + + it('renders the second option and hide target roles when editing message with path specified', () => { + createComponent({ broadcastMessage: { targetPath: '/welcome' } }); + expect(findTargetSelect().element.value).toBe(TARGET_OPTIONS[1].value); + expect(findTargetRoles().isVisible()).toBe(false); + expect(findTargetPath().isVisible()).toBe(true); + }); + + it('renders the third option when editing message with path and roles specified', () => { + createComponent({ broadcastMessage: { targetPath: '/welcome', targetAccessLevels: [20] } }); + expect(findTargetSelect().element.value).toBe(TARGET_OPTIONS[2].value); + expect(findTargetRoles().isVisible()).toBe(true); + expect(findTargetPath().isVisible()).toBe(true); }); }); diff --git a/spec/frontend/admin/topics/components/remove_avatar_spec.js b/spec/frontend/admin/topics/components/remove_avatar_spec.js index c069203d046..705066c3ef0 100644 --- a/spec/frontend/admin/topics/components/remove_avatar_spec.js +++ b/spec/frontend/admin/topics/components/remove_avatar_spec.js @@ -73,7 +73,7 @@ describe('RemoveAvatar', () => { let formSubmitSpy; beforeEach(() => { - formSubmitSpy = jest.spyOn(wrapper.vm.$refs.deleteForm, 'submit'); + formSubmitSpy = jest.spyOn(findForm().element, 'submit'); findModal().vm.$emit('primary'); }); diff --git a/spec/frontend/admin/topics/components/topic_select_spec.js b/spec/frontend/admin/topics/components/topic_select_spec.js index 113a0e3d404..5b7e6365606 100644 --- a/spec/frontend/admin/topics/components/topic_select_spec.js +++ b/spec/frontend/admin/topics/components/topic_select_spec.js @@ -58,10 +58,6 @@ describe('TopicSelect', () => { }); } - afterEach(() => { - jest.clearAllMocks(); - }); - it('mounts', () => { createComponent(); 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 afd88e1a6ac..9980843defb 100644 --- a/spec/frontend/alert_management/components/alert_management_table_spec.js +++ b/spec/frontend/alert_management/components/alert_management_table_spec.js @@ -186,7 +186,7 @@ describe('AlertManagementTable', () => { expect(findSeverityFields().at(0).text()).toBe('Critical'); }); - it('renders Unassigned when no assignee(s) present', () => { + it('renders Unassigned when no assignees present', () => { mountComponent({ data: { alerts: { list: mockAlerts }, alertsCount, errored: false }, loading: false, diff --git a/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js b/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js index 4a0c7f65493..e6b38a1e824 100644 --- a/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js +++ b/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js @@ -68,8 +68,11 @@ describe('AlertsSettingsForm', () => { await options.at(index).setSelected(); }; - const enableIntegration = (index, value) => { - findFormFields().at(index).setValue(value); + const enableIntegration = (index, value = '') => { + if (value !== '') { + findFormFields().at(index).setValue(value); + } + findFormToggle().vm.$emit('change', true); }; @@ -100,7 +103,8 @@ describe('AlertsSettingsForm', () => { it('hides the name input when the selected value is prometheus', async () => { createComponent(); await selectOptionAtIndex(2); - expect(findFormFields().at(0).attributes('id')).not.toBe('name-integration'); + + expect(findFormFields()).toHaveLength(0); }); it('verify pricing link url', () => { @@ -203,8 +207,8 @@ describe('AlertsSettingsForm', () => { it('create', async () => { createComponent(); await selectOptionAtIndex(2); - const apiUrl = 'https://test.com'; - enableIntegration(0, apiUrl); + enableIntegration(0); + const submitBtn = findSubmitButton(); expect(submitBtn.exists()).toBe(true); expect(submitBtn.text()).toBe('Save integration'); @@ -213,14 +217,14 @@ describe('AlertsSettingsForm', () => { expect(wrapper.emitted('create-new-integration')[0][0]).toMatchObject({ type: typeSet.prometheus, - variables: { apiUrl, active: true }, + variables: { active: true }, }); }); it('update', () => { createComponent({ data: { - integrationForm: { id: '1', apiUrl: 'https://test-pre.com', type: typeSet.prometheus }, + integrationForm: { id: '1', type: typeSet.prometheus }, currentIntegration: { id: '1' }, }, props: { @@ -228,8 +232,7 @@ describe('AlertsSettingsForm', () => { }, }); - const apiUrl = 'https://test-post.com'; - enableIntegration(0, apiUrl); + enableIntegration(0); const submitBtn = findSubmitButton(); expect(submitBtn.exists()).toBe(true); @@ -239,7 +242,7 @@ describe('AlertsSettingsForm', () => { expect(wrapper.emitted('update-integration')[0][0]).toMatchObject({ type: typeSet.prometheus, - variables: { apiUrl, active: true }, + variables: { active: true }, }); }); }); @@ -442,16 +445,8 @@ describe('AlertsSettingsForm', () => { expect(findSubmitButton().attributes('disabled')).toBe(undefined); }); - it('should not be able to submit when Prometheus integration form is invalid', async () => { - await selectOptionAtIndex(2); - await findFormFields().at(0).vm.$emit('input', ''); - - expect(findSubmitButton().attributes('disabled')).toBeDefined(); - }); - it('should be able to submit when Prometheus integration form is valid', async () => { await selectOptionAtIndex(2); - await findFormFields().at(0).vm.$emit('input', 'http://valid.url'); expect(findSubmitButton().attributes('disabled')).toBe(undefined); }); 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 4e0b546b3d2..802da47d6cd 100644 --- a/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js +++ b/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js @@ -57,7 +57,6 @@ describe('ProjectsDropdownFilter component', () => { }); }; - const findClearAllButton = () => wrapper.findByTestId('listbox-reset-button'); const findSelectedProjectsLabel = () => wrapper.findComponent(GlTruncate); const findDropdown = () => wrapper.findComponent(GlCollapsibleListbox); @@ -143,10 +142,6 @@ describe('ProjectsDropdownFilter component', () => { expect(findSelectedProjectsLabel().text()).toBe('Select projects'); }); - - it('does not render the clear all button', () => { - expect(findClearAllButton().exists()).toBe(false); - }); }); describe('with a selected project', () => { @@ -169,12 +164,6 @@ describe('ProjectsDropdownFilter component', () => { expect(findSelectedProjectsLabel().text()).toBe(projects[0].name); }); - it('renders the clear all button', async () => { - await selectDropdownItemAtIndex([0], false); - - expect(findClearAllButton().exists()).toBe(true); - }); - it('clears all selected items when the clear all button is clicked', async () => { createComponent({ mountFn: mountExtended, @@ -186,7 +175,7 @@ describe('ProjectsDropdownFilter component', () => { expect(findSelectedProjectsLabel().text()).toBe('2 projects selected'); - await findClearAllButton().vm.$emit('click'); + await findDropdown().vm.$emit('reset'); expect(findSelectedProjectsLabel().text()).toBe('Select projects'); }); 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 20836d7cc70..8638d82ae3c 100644 --- a/spec/frontend/analytics/usage_trends/components/users_chart_spec.js +++ b/spec/frontend/analytics/usage_trends/components/users_chart_spec.js @@ -22,23 +22,19 @@ describe('UsersChart', () => { let queryHandler; const createComponent = ({ - loadingError = false, - loading = false, users = [], additionalData = [], + handler = mockQueryResponse({ key: 'users', data: users, additionalData }), } = {}) => { - queryHandler = mockQueryResponse({ key: 'users', data: users, loading, additionalData }); + queryHandler = handler; - return shallowMount(UsersChart, { + wrapper = shallowMount(UsersChart, { + apolloProvider: createMockApollo([[usersQuery, queryHandler]]), props: { startDate: new Date(2020, 9, 26), endDate: new Date(2020, 10, 1), totalDataPoints: mockCountsData2.length, }, - apolloProvider: createMockApollo([[usersQuery, queryHandler]]), - data() { - return { loadingError }; - }, }); }; @@ -48,7 +44,7 @@ describe('UsersChart', () => { describe('while loading', () => { beforeEach(() => { - wrapper = createComponent({ loading: true }); + createComponent({ loading: true }); }); it('displays the skeleton loader', () => { @@ -62,7 +58,7 @@ describe('UsersChart', () => { describe('without data', () => { beforeEach(async () => { - wrapper = createComponent({ users: [] }); + createComponent({ users: [] }); await nextTick(); }); @@ -81,7 +77,7 @@ describe('UsersChart', () => { describe('with data', () => { beforeEach(async () => { - wrapper = createComponent({ users: mockCountsData2 }); + createComponent({ users: mockCountsData2 }); await waitForPromises(); }); @@ -102,11 +98,17 @@ describe('UsersChart', () => { describe('with errors', () => { beforeEach(async () => { - wrapper = createComponent({ loadingError: true }); + createComponent(); await nextTick(); }); - it('renders an error message', () => { + it('renders an error message', async () => { + createComponent({ + handler: jest.fn().mockRejectedValue({}), + }); + + await waitForPromises(); + expect(findAlert().text()).toBe( 'Could not load the user chart. Please refresh the page to try again.', ); @@ -124,42 +126,37 @@ describe('UsersChart', () => { describe('when fetching more data', () => { describe('when the fetchMore query returns data', () => { beforeEach(async () => { - wrapper = createComponent({ + createComponent({ users: mockCountsData2, additionalData: mockCountsData1, }); - jest.spyOn(wrapper.vm.$apollo.queries.users, 'fetchMore'); await nextTick(); }); it('requests data twice', () => { expect(queryHandler).toHaveBeenCalledTimes(2); }); - - it('calls fetchMore', () => { - expect(wrapper.vm.$apollo.queries.users.fetchMore).toHaveBeenCalledTimes(1); - }); }); describe('when the fetchMore query throws an error', () => { beforeEach(async () => { - wrapper = createComponent({ + createComponent({ users: mockCountsData2, additionalData: mockCountsData1, }); - jest - .spyOn(wrapper.vm.$apollo.queries.users, 'fetchMore') - .mockImplementation(jest.fn().mockRejectedValue()); await waitForPromises(); }); it('calls fetchMore', () => { - expect(wrapper.vm.$apollo.queries.users.fetchMore).toHaveBeenCalledTimes(1); + expect(queryHandler).toHaveBeenCalledTimes(2); }); - it('renders an error message', () => { + it('renders an error message', async () => { + createComponent({ handler: jest.fn().mockRejectedValue({}) }); + await waitForPromises(); + expect(findAlert().text()).toBe( 'Could not load the user chart. Please refresh the page to try again.', ); diff --git a/spec/frontend/api/user_api_spec.js b/spec/frontend/api/user_api_spec.js index b2ecfeb8394..a6e08e1cf4b 100644 --- a/spec/frontend/api/user_api_spec.js +++ b/spec/frontend/api/user_api_spec.js @@ -2,6 +2,7 @@ import MockAdapter from 'axios-mock-adapter'; import projects from 'test_fixtures/api/users/projects/get.json'; import followers from 'test_fixtures/api/users/followers/get.json'; +import following from 'test_fixtures/api/users/following/get.json'; import { followUser, unfollowUser, @@ -9,6 +10,7 @@ import { updateUserStatus, getUserProjects, getUserFollowers, + getUserFollowing, } from '~/api/user_api'; import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_OK } from '~/lib/utils/http_status'; @@ -131,4 +133,23 @@ describe('~/api/user_api', () => { expect(axiosMock.history.get[0].params).toEqual({ ...params, per_page: DEFAULT_PER_PAGE }); }); }); + + describe('getUserFollowing', () => { + it('calls correct URL and returns expected response', async () => { + const MOCK_USER_ID = 1; + const MOCK_PAGE = 2; + + const expectedUrl = `/api/v4/users/${MOCK_USER_ID}/following`; + const expectedResponse = { data: following }; + const params = { page: MOCK_PAGE }; + + axiosMock.onGet(expectedUrl).replyOnce(HTTP_STATUS_OK, expectedResponse); + + await expect(getUserFollowing(MOCK_USER_ID, params)).resolves.toEqual( + expect.objectContaining({ data: expectedResponse }), + ); + expect(axiosMock.history.get[0].url).toBe(expectedUrl); + expect(axiosMock.history.get[0].params).toEqual({ ...params, per_page: DEFAULT_PER_PAGE }); + }); + }); }); diff --git a/spec/frontend/batch_comments/components/draft_note_spec.js b/spec/frontend/batch_comments/components/draft_note_spec.js index 159e36c1364..b6042b4aa81 100644 --- a/spec/frontend/batch_comments/components/draft_note_spec.js +++ b/spec/frontend/batch_comments/components/draft_note_spec.js @@ -41,9 +41,11 @@ describe('Batch comments draft note component', () => { }, }); - jest.spyOn(wrapper.vm.$store, 'dispatch').mockImplementation(); + jest.spyOn(store, 'dispatch').mockImplementation(); }; + const findNoteableNote = () => wrapper.findComponent(NoteableNote); + beforeEach(() => { store = createStore(); draft = createDraft(); @@ -53,32 +55,28 @@ describe('Batch comments draft note component', () => { createComponent(); expect(wrapper.findComponent(GlBadge).exists()).toBe(true); - const note = wrapper.findComponent(NoteableNote); - - expect(note.exists()).toBe(true); - expect(note.props().note).toEqual(draft); + expect(findNoteableNote().exists()).toBe(true); + expect(findNoteableNote().props('note')).toEqual(draft); }); describe('update', () => { it('dispatches updateDraft', async () => { createComponent(); - const note = wrapper.findComponent(NoteableNote); - - note.vm.$emit('handleEdit'); + findNoteableNote().vm.$emit('handleEdit'); await nextTick(); const formData = { note: draft, noteText: 'a', resolveDiscussion: false, + callback: jest.fn(), + parentElement: wrapper.vm.$el, + errorCallback: jest.fn(), }; - note.vm.$emit('handleUpdateNote', formData); + findNoteableNote().vm.$emit('handleUpdateNote', formData); - expect(wrapper.vm.$store.dispatch).toHaveBeenCalledWith( - 'batchComments/updateDraft', - formData, - ); + expect(store.dispatch).toHaveBeenCalledWith('batchComments/updateDraft', formData); }); }); @@ -87,18 +85,15 @@ describe('Batch comments draft note component', () => { createComponent(); jest.spyOn(window, 'confirm').mockImplementation(() => true); - const note = wrapper.findComponent(NoteableNote); - - note.vm.$emit('handleDeleteNote', draft); + findNoteableNote().vm.$emit('handleDeleteNote', draft); - expect(wrapper.vm.$store.dispatch).toHaveBeenCalledWith('batchComments/deleteDraft', draft); + expect(store.dispatch).toHaveBeenCalledWith('batchComments/deleteDraft', draft); }); }); describe('quick actions', () => { it('renders referenced commands', async () => { - createComponent(); - wrapper.setProps({ + createComponent({ draft: { ...draft, references: { @@ -116,22 +111,27 @@ describe('Batch comments draft note component', () => { }); describe('multiline comments', () => { - describe.each` - desc | props | event | expectedCalls - ${'with `draft.position`'} | ${draftWithLineRange} | ${'mouseenter'} | ${[['setSelectedCommentPositionHover', LINE_RANGE]]} - ${'with `draft.position`'} | ${draftWithLineRange} | ${'mouseleave'} | ${[['setSelectedCommentPositionHover']]} - ${'without `draft.position`'} | ${{}} | ${'mouseenter'} | ${[]} - ${'without `draft.position`'} | ${{}} | ${'mouseleave'} | ${[]} - `('$desc', ({ props, event, expectedCalls }) => { - beforeEach(() => { - createComponent({ draft: { ...draft, ...props } }); - jest.spyOn(store, 'dispatch'); - }); + it(`calls store with draft.position with mouseenter`, () => { + createComponent({ draft: { ...draft, ...draftWithLineRange } }); + findNoteableNote().trigger('mouseenter'); - it(`calls store ${expectedCalls.length} times on ${event}`, () => { - wrapper.element.dispatchEvent(new MouseEvent(event, { bubbles: true })); - expect(store.dispatch.mock.calls).toEqual(expectedCalls); - }); + expect(store.dispatch).toHaveBeenCalledWith('setSelectedCommentPositionHover', LINE_RANGE); + }); + + it(`calls store with draft.position and mouseleave`, () => { + createComponent({ draft: { ...draft, ...draftWithLineRange } }); + findNoteableNote().trigger('mouseleave'); + + expect(store.dispatch).toHaveBeenCalledWith('setSelectedCommentPositionHover'); + }); + + it(`does not call store without draft position`, () => { + createComponent({ draft }); + + findNoteableNote().trigger('mouseenter'); + findNoteableNote().trigger('mouseleave'); + + expect(store.dispatch).not.toHaveBeenCalled(); }); }); }); diff --git a/spec/frontend/batch_comments/components/submit_dropdown_spec.js b/spec/frontend/batch_comments/components/submit_dropdown_spec.js index 5c33df882bf..7e2ff7f786f 100644 --- a/spec/frontend/batch_comments/components/submit_dropdown_spec.js +++ b/spec/frontend/batch_comments/components/submit_dropdown_spec.js @@ -3,6 +3,7 @@ import Vue from 'vue'; import Vuex from 'vuex'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import SubmitDropdown from '~/batch_comments/components/submit_dropdown.vue'; +import { mockTracking } from 'helpers/tracking_helper'; jest.mock('~/autosave'); @@ -10,9 +11,11 @@ Vue.use(Vuex); let wrapper; let publishReview; +let trackingSpy; function factory({ canApprove = true, shouldAnimateReviewButton = false } = {}) { publishReview = jest.fn(); + trackingSpy = mockTracking(undefined, null, jest.spyOn); const store = new Vuex.Store({ getters: { @@ -69,6 +72,20 @@ describe('Batch comments submit dropdown', () => { }); }); + it('tracks submit action', () => { + factory(); + + findCommentTextarea().setValue('Hello world'); + + findForm().vm.$emit('submit', { preventDefault: jest.fn() }); + + expect(trackingSpy).toHaveBeenCalledWith(undefined, 'editor_type_used', { + context: 'MergeRequest_review', + editorType: 'editor_type_plain_text_editor', + label: 'editor_tracking', + }); + }); + it('switches to the overview tab after submit', async () => { window.mrTabs = { tabShown: jest.fn() }; diff --git a/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js b/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js index 521bbf06b02..824b2a296c6 100644 --- a/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js +++ b/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js @@ -1,10 +1,15 @@ import MockAdapter from 'axios-mock-adapter'; import { TEST_HOST } from 'helpers/test_constants'; import testAction from 'helpers/vuex_action_helper'; +import { sprintf } from '~/locale'; +import { createAlert } from '~/alert'; import service from '~/batch_comments/services/drafts_service'; import * as actions from '~/batch_comments/stores/modules/batch_comments/actions'; import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status'; +import { UPDATE_COMMENT_FORM } from '~/notes/i18n'; + +jest.mock('~/alert'); describe('Batch comments store actions', () => { let res = {}; @@ -44,15 +49,15 @@ describe('Batch comments store actions', () => { }); it('does not commit ADD_NEW_DRAFT if errors returned', () => { + const commit = jest.fn(); + mock.onAny().reply(HTTP_STATUS_INTERNAL_SERVER_ERROR); - return testAction( - actions.addDraftToDiscussion, - { endpoint: TEST_HOST, data: 'test' }, - null, - [], - [], - ); + return actions + .addDraftToDiscussion({ commit }, { endpoint: TEST_HOST, data: 'test' }) + .catch(() => { + expect(commit).not.toHaveBeenCalledWith('ADD_NEW_DRAFT', expect.anything()); + }); }); }); @@ -84,15 +89,13 @@ describe('Batch comments store actions', () => { }); it('does not commit ADD_NEW_DRAFT if errors returned', () => { + const commit = jest.fn(); + mock.onAny().reply(HTTP_STATUS_INTERNAL_SERVER_ERROR); - return testAction( - actions.createNewDraft, - { endpoint: TEST_HOST, data: 'test' }, - null, - [], - [], - ); + return actions.createNewDraft({ commit }, { endpoint: TEST_HOST, data: 'test' }).catch(() => { + expect(commit).not.toHaveBeenCalledWith('ADD_NEW_DRAFT', expect.anything()); + }); }); }); @@ -239,8 +242,6 @@ describe('Batch comments store actions', () => { params = { note: { id: 1 }, noteText: 'test' }; }); - afterEach(() => jest.clearAllMocks()); - it('commits RECEIVE_DRAFT_UPDATE_SUCCESS with returned data', () => { return actions.updateDraft(context, { ...params, callback() {} }).then(() => { expect(commit).toHaveBeenCalledWith('RECEIVE_DRAFT_UPDATE_SUCCESS', { id: 1 }); @@ -267,6 +268,28 @@ describe('Batch comments store actions', () => { expect(service.update.mock.calls[0][1].position).toBe(expectation); }); }); + + describe('when updating a draft returns an error', () => { + const errorCallback = jest.fn(); + const flashContainer = null; + const error = 'server error'; + + beforeEach(async () => { + service.update.mockRejectedValue({ response: { data: { errors: error } } }); + await actions.updateDraft(context, { ...params, flashContainer, errorCallback }); + }); + + it('renders an error message', () => { + expect(createAlert).toHaveBeenCalledWith({ + message: sprintf(UPDATE_COMMENT_FORM.error, { reason: error }), + parent: flashContainer, + }); + }); + + it('calls errorCallback', () => { + expect(errorCallback).toHaveBeenCalledTimes(1); + }); + }); }); describe('expandAllDiscussions', () => { diff --git a/spec/frontend/behaviors/gl_emoji_spec.js b/spec/frontend/behaviors/gl_emoji_spec.js index 995e4219ae3..c7f4fce0e4c 100644 --- a/spec/frontend/behaviors/gl_emoji_spec.js +++ b/spec/frontend/behaviors/gl_emoji_spec.js @@ -1,11 +1,18 @@ import { initEmojiMock, clearEmojiMock } from 'helpers/emoji'; import waitForPromises from 'helpers/wait_for_promises'; +import { createMockClient } from 'helpers/mock_apollo_helper'; import installGlEmojiElement from '~/behaviors/gl_emoji'; import { EMOJI_VERSION } from '~/emoji'; +import customEmojiQuery from '~/emoji/queries/custom_emoji.query.graphql'; import * as EmojiUnicodeSupport from '~/emoji/support'; +let mockClient; + jest.mock('~/emoji/support'); +jest.mock('~/lib/graphql', () => { + return () => mockClient; +}); describe('gl_emoji', () => { const emojiData = { @@ -36,101 +43,144 @@ describe('gl_emoji', () => { return div.firstElementChild; } - beforeEach(async () => { - await initEmojiMock(emojiData); - }); - afterEach(() => { clearEmojiMock(); document.body.innerHTML = ''; }); - describe.each([ - [ - 'bomb emoji just with name attribute', - '', - '💣', - `:bomb:`, - ], - [ - 'bomb emoji with name attribute and unicode version', - '💣', - '💣', - `:bomb:`, - ], - [ - 'bomb emoji with sprite fallback', - '', - '💣', - '💣', - ], - [ - 'bomb emoji with image fallback', - '', - '💣', - ':bomb:', - ], - [ - 'invalid emoji', - '', - '', - `:grey_question:`, - ], - [ - 'custom emoji with image fallback', - '', - ':party-parrot:', - ':party-parrot:', - ], - ])('%s', (name, markup, withEmojiSupport, withoutEmojiSupport) => { - it(`renders correctly with emoji support`, async () => { - jest.spyOn(EmojiUnicodeSupport, 'default').mockReturnValue(true); - const glEmojiElement = markupToDomElement(markup); + describe('standard emoji', () => { + beforeEach(async () => { + await initEmojiMock(emojiData); + }); + + describe.each([ + [ + 'bomb emoji just with name attribute', + '', + '💣', + `:bomb:`, + ], + [ + 'bomb emoji with name attribute and unicode version', + '💣', + '💣', + `:bomb:`, + ], + [ + 'bomb emoji with sprite fallback', + '', + '💣', + '💣', + ], + [ + 'bomb emoji with image fallback', + '', + '💣', + ':bomb:', + ], + [ + 'invalid emoji', + '', + '', + `:grey_question:`, + ], + [ + 'custom emoji with image fallback', + '', + ':party-parrot:', + ':party-parrot:', + ], + ])('%s', (name, markup, withEmojiSupport, withoutEmojiSupport) => { + it(`renders correctly with emoji support`, async () => { + jest.spyOn(EmojiUnicodeSupport, 'default').mockReturnValue(true); + const glEmojiElement = markupToDomElement(markup); + + await waitForPromises(); + + expect(glEmojiElement.outerHTML).toBe(withEmojiSupport); + }); + + it(`renders correctly without emoji support`, async () => { + jest.spyOn(EmojiUnicodeSupport, 'default').mockReturnValue(false); + const glEmojiElement = markupToDomElement(markup); + + await waitForPromises(); + + expect(glEmojiElement.outerHTML).toBe(withoutEmojiSupport); + }); + }); + + it('escapes gl-emoji name', async () => { + const glEmojiElement = markupToDomElement( + "abc", + ); await waitForPromises(); - expect(glEmojiElement.outerHTML).toBe(withEmojiSupport); + expect(glEmojiElement.outerHTML).toBe( + ':"x="y" onload="alert(document.location.href)":', + ); }); - it(`renders correctly without emoji support`, async () => { + it('Adds sprite CSS if emojis are not supported', async () => { + const testPath = '/test-path.css'; jest.spyOn(EmojiUnicodeSupport, 'default').mockReturnValue(false); - const glEmojiElement = markupToDomElement(markup); + window.gon.emoji_sprites_css_path = testPath; + expect(document.head.querySelector(`link[href="${testPath}"]`)).toBe(null); + expect(window.gon.emoji_sprites_css_added).toBe(undefined); + + markupToDomElement( + '', + ); await waitForPromises(); - expect(glEmojiElement.outerHTML).toBe(withoutEmojiSupport); + expect(document.head.querySelector(`link[href="${testPath}"]`).outerHTML).toBe( + '', + ); + expect(window.gon.emoji_sprites_css_added).toBe(true); }); }); - it('escapes gl-emoji name', async () => { - const glEmojiElement = markupToDomElement( - "abc", - ); - - await waitForPromises(); + describe('custom emoji', () => { + beforeEach(async () => { + mockClient = createMockClient([ + [ + customEmojiQuery, + jest.fn().mockResolvedValue({ + data: { + group: { + id: 1, + customEmoji: { + nodes: [{ id: 1, name: 'parrot', url: 'parrot.gif' }], + }, + }, + }, + }), + ], + ]); + + window.gon = { features: { customEmoji: true } }; + document.body.dataset.groupFullPath = 'test-group'; + + await initEmojiMock(emojiData); + }); - expect(glEmojiElement.outerHTML).toBe( - ':"x="y" onload="alert(document.location.href)":', - ); - }); + afterEach(() => { + window.gon = {}; + delete document.body.dataset.groupFullPath; + }); - it('Adds sprite CSS if emojis are not supported', async () => { - const testPath = '/test-path.css'; - jest.spyOn(EmojiUnicodeSupport, 'default').mockReturnValue(false); - window.gon.emoji_sprites_css_path = testPath; + it('renders custom emoji', async () => { + const glEmojiElement = markupToDomElement(''); - expect(document.head.querySelector(`link[href="${testPath}"]`)).toBe(null); - expect(window.gon.emoji_sprites_css_added).toBe(undefined); + await waitForPromises(); - markupToDomElement( - '', - ); - await waitForPromises(); + const img = glEmojiElement.querySelector('img'); - expect(document.head.querySelector(`link[href="${testPath}"]`).outerHTML).toBe( - '', - ); - expect(window.gon.emoji_sprites_css_added).toBe(true); + expect(glEmojiElement.dataset.unicodeVersion).toBe('custom'); + expect(img.getAttribute('src')).toBe('parrot.gif'); + }); }); }); diff --git a/spec/frontend/behaviors/markdown/render_gfm_spec.js b/spec/frontend/behaviors/markdown/render_gfm_spec.js index 220ad874b47..0bbb92282e5 100644 --- a/spec/frontend/behaviors/markdown/render_gfm_spec.js +++ b/spec/frontend/behaviors/markdown/render_gfm_spec.js @@ -1,7 +1,4 @@ import { renderGFM } from '~/behaviors/markdown/render_gfm'; -import renderMetrics from '~/behaviors/markdown/render_metrics'; - -jest.mock('~/behaviors/markdown/render_metrics'); describe('renderGFM', () => { it('handles a missing element', () => { @@ -9,27 +6,4 @@ describe('renderGFM', () => { renderGFM(); }).not.toThrow(); }); - - describe('remove_monitor_metrics flag', () => { - let metricsElement; - - beforeEach(() => { - window.gon = { features: { removeMonitorMetrics: true } }; - metricsElement = document.createElement('div'); - metricsElement.setAttribute('class', '.js-render-metrics'); - }); - - it('renders metrics when the flag is disabled', () => { - window.gon.features = { features: { removeMonitorMetrics: false } }; - renderGFM(metricsElement); - - expect(renderMetrics).toHaveBeenCalled(); - }); - - it('does not render metrics when the flag is enabled', () => { - renderGFM(metricsElement); - - expect(renderMetrics).not.toHaveBeenCalled(); - }); - }); }); diff --git a/spec/frontend/behaviors/markdown/render_metrics_spec.js b/spec/frontend/behaviors/markdown/render_metrics_spec.js deleted file mode 100644 index ab81ed6b8f0..00000000000 --- a/spec/frontend/behaviors/markdown/render_metrics_spec.js +++ /dev/null @@ -1,49 +0,0 @@ -import { TEST_HOST } from 'helpers/test_constants'; -import renderMetrics from '~/behaviors/markdown/render_metrics'; - -const mockEmbedGroup = jest.fn(); - -jest.mock('vue', () => ({ extend: () => mockEmbedGroup })); -jest.mock('~/monitoring/components/embeds/embed_group.vue', () => jest.fn()); -jest.mock('~/monitoring/stores/embed_group/', () => ({ createStore: jest.fn() })); - -const getElements = () => Array.from(document.getElementsByClassName('js-render-metrics')); - -describe('Render metrics for Gitlab Flavoured Markdown', () => { - it('does nothing when no elements are found', () => { - return renderMetrics([]).then(() => { - expect(mockEmbedGroup).not.toHaveBeenCalled(); - }); - }); - - it('renders a vue component when elements are found', () => { - document.body.innerHTML = `
`; - - return renderMetrics(getElements()).then(() => { - expect(mockEmbedGroup).toHaveBeenCalledTimes(1); - expect(mockEmbedGroup).toHaveBeenCalledWith( - expect.objectContaining({ propsData: { urls: [`${TEST_HOST}`] } }), - ); - }); - }); - - it('takes sibling metrics and groups them under a shared parent', () => { - document.body.innerHTML = ` -

Hello

-
-
-

Hello

-
- `; - - return renderMetrics(getElements()).then(() => { - expect(mockEmbedGroup).toHaveBeenCalledTimes(2); - expect(mockEmbedGroup).toHaveBeenCalledWith( - expect.objectContaining({ propsData: { urls: [`${TEST_HOST}/1`, `${TEST_HOST}/2`] } }), - ); - expect(mockEmbedGroup).toHaveBeenCalledWith( - expect.objectContaining({ propsData: { urls: [`${TEST_HOST}/3`] } }), - ); - }); - }); -}); diff --git a/spec/frontend/blob/line_highlighter_spec.js b/spec/frontend/blob/line_highlighter_spec.js index b2e1a29b84f..de39a8f688a 100644 --- a/spec/frontend/blob/line_highlighter_spec.js +++ b/spec/frontend/blob/line_highlighter_spec.js @@ -1,5 +1,4 @@ /* eslint-disable no-return-assign, no-new, no-underscore-dangle */ -import $ from 'jquery'; import htmlStaticLineHighlighter from 'test_fixtures_static/line_highlighter.html'; import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; import LineHighlighter from '~/blob/line_highlighter'; @@ -9,11 +8,15 @@ describe('LineHighlighter', () => { const testContext = {}; const clickLine = (number, eventData = {}) => { - if ($.isEmptyObject(eventData)) { - return $(`#L${number}`).click(); + if (Object.keys(eventData).length === 0) { + return document.querySelector(`#L${number}`).click(); } - const e = $.Event('click', eventData); - return $(`#L${number}`).trigger(e); + const e = new MouseEvent('click', { + bubbles: true, + cancelable: true, + ...eventData, + }); + return document.querySelector(`#L${number}`).dispatchEvent(e); }; beforeEach(() => { @@ -35,32 +38,30 @@ describe('LineHighlighter', () => { it('highlights one line given in the URL hash', () => { new LineHighlighter({ hash: '#L13' }); - expect($('#LC13')).toHaveClass(testContext.css); + expect(document.querySelector('#LC13').classList).toContain(testContext.css); }); it('highlights one line given in the URL hash with given CSS class name', () => { const hiliter = new LineHighlighter({ hash: '#L13', highlightLineClass: 'hilite' }); expect(hiliter.highlightLineClass).toBe('hilite'); - expect($('#LC13')).toHaveClass('hilite'); - expect($('#LC13')).not.toHaveClass('hll'); + expect(document.querySelector('#LC13').classList).toContain('hilite'); + expect(document.querySelector('#LC13').classList).not.toContain('hll'); }); it('highlights a range of lines given in the URL hash', () => { new LineHighlighter({ hash: '#L5-25' }); - expect($(`.${testContext.css}`).length).toBe(21); for (let line = 5; line <= 25; line += 1) { - expect($(`#LC${line}`)).toHaveClass(testContext.css); + expect(document.querySelector(`#LC${line}`).classList).toContain(testContext.css); } }); it('highlights a range of lines given in the URL hash using GitHub format', () => { new LineHighlighter({ hash: '#L5-L25' }); - expect($(`.${testContext.css}`).length).toBe(21); for (let line = 5; line <= 25; line += 1) { - expect($(`#LC${line}`)).toHaveClass(testContext.css); + expect(document.querySelector(`#LC${line}`).classList).toContain(testContext.css); } }); @@ -74,11 +75,13 @@ describe('LineHighlighter', () => { it('discards click events', () => { const clickSpy = jest.fn(); - $('a[data-line-number]').click(clickSpy); + document.querySelectorAll('a[data-line-number]').forEach((el) => { + el.addEventListener('click', clickSpy); + }); clickLine(13); - expect(clickSpy.mock.calls[0][0].isDefaultPrevented()).toEqual(true); + expect(clickSpy.mock.calls[0][0].defaultPrevented).toEqual(true); }); it('handles garbage input from the hash', () => { @@ -101,27 +104,19 @@ describe('LineHighlighter', () => { }); describe('clickHandler', () => { - it('handles clicking on a child icon element', () => { - const spy = jest.spyOn(testContext.class, 'setHash'); - $('#L13 [data-testid="link-icon"]').mousedown().click(); - - expect(spy).toHaveBeenCalledWith(13); - expect($('#LC13')).toHaveClass(testContext.css); - }); - describe('without shiftKey', () => { it('highlights one line when clicked', () => { clickLine(13); - expect($('#LC13')).toHaveClass(testContext.css); + expect(document.querySelector('#LC13').classList).toContain(testContext.css); }); it('unhighlights previously highlighted lines', () => { clickLine(13); clickLine(20); - expect($('#LC13')).not.toHaveClass(testContext.css); - expect($('#LC20')).toHaveClass(testContext.css); + expect(document.querySelector('#LC13').classList).not.toContain(testContext.css); + expect(document.querySelector('#LC20').classList).toContain(testContext.css); }); it('sets the hash', () => { @@ -138,6 +133,8 @@ describe('LineHighlighter', () => { clickLine(13); clickLine(20, { shiftKey: true, + bubbles: true, + cancelable: true, }); expect(spy).toHaveBeenCalledWith(13); @@ -150,8 +147,8 @@ describe('LineHighlighter', () => { shiftKey: true, }); - expect($('#LC13')).toHaveClass(testContext.css); - expect($(`.${testContext.css}`).length).toBe(1); + expect(document.querySelector('#LC13').classList).toContain(testContext.css); + expect(document.querySelectorAll(`.${testContext.css}`).length).toBe(1); }); it('sets the hash', () => { @@ -171,9 +168,9 @@ describe('LineHighlighter', () => { shiftKey: true, }); - expect($(`.${testContext.css}`).length).toBe(6); + expect(document.querySelectorAll(`.${testContext.css}`).length).toBe(6); for (let line = 15; line <= 20; line += 1) { - expect($(`#LC${line}`)).toHaveClass(testContext.css); + expect(document.querySelector(`#LC${line}`).classList).toContain(testContext.css); } }); @@ -183,9 +180,9 @@ describe('LineHighlighter', () => { shiftKey: true, }); - expect($(`.${testContext.css}`).length).toBe(6); + expect(document.querySelectorAll(`.${testContext.css}`).length).toBe(6); for (let line = 5; line <= 10; line += 1) { - expect($(`#LC${line}`)).toHaveClass(testContext.css); + expect(document.querySelector(`#LC${line}`).classList).toContain(testContext.css); } }); }); @@ -205,9 +202,9 @@ describe('LineHighlighter', () => { shiftKey: true, }); - expect($(`.${testContext.css}`).length).toBe(6); + expect(document.querySelectorAll(`.${testContext.css}`).length).toBe(6); for (let line = 5; line <= 10; line += 1) { - expect($(`#LC${line}`)).toHaveClass(testContext.css); + expect(document.querySelector(`#LC${line}`).classList).toContain(testContext.css); } }); @@ -216,9 +213,9 @@ describe('LineHighlighter', () => { shiftKey: true, }); - expect($(`.${testContext.css}`).length).toBe(6); + expect(document.querySelectorAll(`.${testContext.css}`).length).toBe(6); for (let line = 10; line <= 15; line += 1) { - expect($(`#LC${line}`)).toHaveClass(testContext.css); + expect(document.querySelector(`#LC${line}`).classList).toContain(testContext.css); } }); }); @@ -251,13 +248,13 @@ describe('LineHighlighter', () => { it('highlights the specified line', () => { testContext.subject(13); - expect($('#LC13')).toHaveClass(testContext.css); + expect(document.querySelector('#LC13').classList).toContain(testContext.css); }); it('accepts a String-based number', () => { testContext.subject('13'); - expect($('#LC13')).toHaveClass(testContext.css); + expect(document.querySelector('#LC13').classList).toContain(testContext.css); }); }); diff --git a/spec/frontend/blob_edit/edit_blob_spec.js b/spec/frontend/blob_edit/edit_blob_spec.js index 9ab20fc2cd7..1bdc54723ce 100644 --- a/spec/frontend/blob_edit/edit_blob_spec.js +++ b/spec/frontend/blob_edit/edit_blob_spec.js @@ -61,7 +61,6 @@ describe('Blob Editing', () => { }); afterEach(() => { mock.restore(); - jest.clearAllMocks(); unuseMock.mockClear(); useMock.mockClear(); resetHTMLFixture(); diff --git a/spec/frontend/boards/board_card_inner_spec.js b/spec/frontend/boards/board_card_inner_spec.js index a925f752f5e..36556ba00af 100644 --- a/spec/frontend/boards/board_card_inner_spec.js +++ b/spec/frontend/boards/board_card_inner_spec.js @@ -92,6 +92,7 @@ describe('Board card component', () => { isEpicBoard, issuableType: TYPE_ISSUE, isGroupBoard, + isApolloBoard: false, }, }); }; @@ -111,7 +112,6 @@ describe('Board card component', () => { afterEach(() => { store = null; - jest.clearAllMocks(); }); it('renders issue title', () => { diff --git a/spec/frontend/boards/components/board_app_spec.js b/spec/frontend/boards/components/board_app_spec.js index 3d6e4c18f51..e7624437ac5 100644 --- a/spec/frontend/boards/components/board_app_spec.js +++ b/spec/frontend/boards/components/board_app_spec.js @@ -4,7 +4,9 @@ import VueApollo from 'vue-apollo'; import Vuex from 'vuex'; import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; import BoardApp from '~/boards/components/board_app.vue'; +import eventHub from '~/boards/eventhub'; import activeBoardItemQuery from 'ee_else_ce/boards/graphql/client/active_board_item.query.graphql'; import boardListsQuery from 'ee_else_ce/boards/graphql/board_lists.query.graphql'; import { rawIssue, boardListsQueryResponse } from '../mock_data'; @@ -93,5 +95,14 @@ describe('BoardApp', () => { expect(wrapper.classes()).not.toContain('is-compact'); }); + + it('refetches lists when updateBoard event is received', async () => { + jest.spyOn(eventHub, '$on').mockImplementation(() => {}); + + createComponent({ isApolloBoard: true }); + await waitForPromises(); + + expect(eventHub.$on).toHaveBeenCalledWith('updateBoard', wrapper.vm.refetchLists); + }); }); }); diff --git a/spec/frontend/boards/components/board_content_spec.js b/spec/frontend/boards/components/board_content_spec.js index 9260718a94b..0a2a78479fb 100644 --- a/spec/frontend/boards/components/board_content_spec.js +++ b/spec/frontend/boards/components/board_content_spec.js @@ -4,7 +4,7 @@ import VueApollo from 'vue-apollo'; import Vue from 'vue'; import Draggable from 'vuedraggable'; import Vuex from 'vuex'; -import eventHub from '~/boards/eventhub'; + import createMockApollo from 'helpers/mock_apollo_helper'; import { stubComponent } from 'helpers/stub_component'; import waitForPromises from 'helpers/wait_for_promises'; @@ -182,15 +182,6 @@ describe('BoardContent', () => { expect(wrapper.findComponent(BoardContentSidebar).exists()).toBe(true); }); - it('refetches lists when updateBoard event is received', async () => { - jest.spyOn(eventHub, '$on').mockImplementation(() => {}); - - createComponent({ isApolloBoard: true }); - await waitForPromises(); - - expect(eventHub.$on).toHaveBeenCalledWith('updateBoard', wrapper.vm.refetchLists); - }); - it('reorders lists', async () => { const movableListsOrder = [mockLists[0].id, mockLists[1].id]; diff --git a/spec/frontend/boards/components/board_list_header_spec.js b/spec/frontend/boards/components/board_list_header_spec.js index ad2674f9d3b..0c9e1b4646e 100644 --- a/spec/frontend/boards/components/board_list_header_spec.js +++ b/spec/frontend/boards/components/board_list_header_spec.js @@ -1,4 +1,4 @@ -import { GlDisclosureDropdown, GlDisclosureDropdownItem } from '@gitlab/ui'; +import { GlButtonGroup } from '@gitlab/ui'; import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import Vuex from 'vuex'; @@ -93,18 +93,17 @@ describe('Board List Header Component', () => { ...injectedProps, }, stubs: { - GlDisclosureDropdown, - GlDisclosureDropdownItem, + GlButtonGroup, }, }); }; - const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown); + const findButtonGroup = () => wrapper.findComponent(GlButtonGroup); const isCollapsed = () => wrapper.vm.list.collapsed; const findTitle = () => wrapper.find('.board-title'); const findCaret = () => wrapper.findByTestId('board-title-caret'); - const findNewIssueButton = () => wrapper.findByTestId('newIssueBtn'); - const findSettingsButton = () => wrapper.findByTestId('settingsBtn'); + const findNewIssueButton = () => wrapper.findByTestId('new-issue-btn'); + const findSettingsButton = () => wrapper.findByTestId('settings-btn'); const findBoardListHeader = () => wrapper.findByTestId('board-list-header'); it('renders border when label color is present', () => { @@ -131,13 +130,13 @@ describe('Board List Header Component', () => { it.each(hasNoAddButton)('does not render dropdown when List Type is `%s`', (listType) => { createComponent({ listType }); - expect(findDropdown().exists()).toBe(false); + expect(findButtonGroup().exists()).toBe(false); }); it.each(hasAddButton)('does render when List Type is `%s`', (listType) => { createComponent({ listType }); - expect(findDropdown().exists()).toBe(true); + expect(findButtonGroup().exists()).toBe(true); expect(findNewIssueButton().exists()).toBe(true); }); @@ -146,7 +145,7 @@ describe('Board List Header Component', () => { currentUserId: null, }); - expect(findDropdown().exists()).toBe(false); + expect(findButtonGroup().exists()).toBe(false); }); }); @@ -156,20 +155,20 @@ describe('Board List Header Component', () => { it.each(hasSettings)('does render for List Type `%s`', (listType) => { createComponent({ listType }); - expect(findDropdown().exists()).toBe(true); + expect(findButtonGroup().exists()).toBe(true); expect(findSettingsButton().exists()).toBe(true); }); it('does not render dropdown when ListType `closed`', () => { createComponent({ listType: ListType.closed }); - expect(findDropdown().exists()).toBe(false); + expect(findButtonGroup().exists()).toBe(false); }); it('renders dropdown but not the Settings button when ListType `backlog`', () => { createComponent({ listType: ListType.backlog }); - expect(findDropdown().exists()).toBe(true); + expect(findButtonGroup().exists()).toBe(true); expect(findSettingsButton().exists()).toBe(false); }); }); diff --git a/spec/frontend/boards/components/board_new_issue_spec.js b/spec/frontend/boards/components/board_new_issue_spec.js index 651d1daee52..a1088f1e8f7 100644 --- a/spec/frontend/boards/components/board_new_issue_spec.js +++ b/spec/frontend/boards/components/board_new_issue_spec.js @@ -1,25 +1,49 @@ import { shallowMount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; import 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, mockIssue, mockIssue2 } from '../mock_data'; +import groupBoardQuery from '~/boards/graphql/group_board.query.graphql'; +import projectBoardQuery from '~/boards/graphql/project_board.query.graphql'; +import { WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants'; + +import { + mockList, + mockGroupProjects, + mockIssue, + mockIssue2, + mockProjectBoardResponse, + mockGroupBoardResponse, +} from '../mock_data'; Vue.use(Vuex); +Vue.use(VueApollo); const addListNewIssuesSpy = jest.fn().mockResolvedValue(); const mockActions = { addListNewIssue: addListNewIssuesSpy }; +const projectBoardQueryHandlerSuccess = jest.fn().mockResolvedValue(mockProjectBoardResponse); +const groupBoardQueryHandlerSuccess = jest.fn().mockResolvedValue(mockGroupBoardResponse); + +const mockApollo = createMockApollo([ + [projectBoardQuery, projectBoardQueryHandlerSuccess], + [groupBoardQuery, groupBoardQueryHandlerSuccess], +]); + const createComponent = ({ - state = { selectedProject: mockGroupProjects[0] }, + state = {}, actions = mockActions, getters = { getBoardItemsByList: () => () => [] }, isGroupBoard = true, + data = { selectedProject: mockGroupProjects[0] }, + provide = {}, } = {}) => shallowMount(BoardNewIssue, { + apolloProvider: mockApollo, store: new Vuex.Store({ state, actions, @@ -27,13 +51,19 @@ const createComponent = ({ }), propsData: { list: mockList, + boardId: 'gid://gitlab/Board/1', }, + data: () => data, provide: { groupId: 1, fullPath: mockGroupProjects[0].fullPath, weightFeatureAvailable: false, boardWeight: null, isGroupBoard, + boardType: 'group', + isEpicBoard: false, + isApolloBoard: false, + ...provide, }, stubs: { BoardNewItem, @@ -137,4 +167,33 @@ describe('Issue boards new issue form', () => { expect(projectSelect.exists()).toBe(false); }); }); + + describe('Apollo boards', () => { + it.each` + boardType | queryHandler | notCalledHandler + ${WORKSPACE_GROUP} | ${groupBoardQueryHandlerSuccess} | ${projectBoardQueryHandlerSuccess} + ${WORKSPACE_PROJECT} | ${projectBoardQueryHandlerSuccess} | ${groupBoardQueryHandlerSuccess} + `( + 'fetches $boardType board and emits addNewIssue event', + async ({ boardType, queryHandler, notCalledHandler }) => { + wrapper = createComponent({ + provide: { + boardType, + isProjectBoard: boardType === WORKSPACE_PROJECT, + isGroupBoard: boardType === WORKSPACE_GROUP, + isApolloBoard: true, + }, + }); + + await nextTick(); + findBoardNewItem().vm.$emit('form-submit', { title: 'Foo' }); + + await nextTick(); + + expect(queryHandler).toHaveBeenCalled(); + expect(notCalledHandler).not.toHaveBeenCalled(); + expect(wrapper.emitted('addNewIssue')[0][0]).toMatchObject({ title: 'Foo' }); + }, + ); + }); }); diff --git a/spec/frontend/boards/components/board_settings_sidebar_spec.js b/spec/frontend/boards/components/board_settings_sidebar_spec.js index b1e14be8ceb..affe1260c66 100644 --- a/spec/frontend/boards/components/board_settings_sidebar_spec.js +++ b/spec/frontend/boards/components/board_settings_sidebar_spec.js @@ -90,10 +90,6 @@ describe('BoardSettingsSidebar', () => { const findModal = () => wrapper.findComponent(GlModal); const findRemoveButton = () => wrapper.findComponent(GlButton); - afterEach(() => { - jest.restoreAllMocks(); - }); - it('finds a MountingPortal component', () => { createComponent(); diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js index 447aacd9cea..8235c3e4194 100644 --- a/spec/frontend/boards/mock_data.js +++ b/spec/frontend/boards/mock_data.js @@ -836,6 +836,7 @@ export const mockTokens = (fetchLabels, fetchUsers, fetchMilestones, isSignedIn) type: TOKEN_TYPE_ASSIGNEE, operators: OPERATORS_IS_NOT, token: UserToken, + dataType: 'user', unique: true, fetchUsers, preloadedUsers: [], @@ -847,6 +848,7 @@ export const mockTokens = (fetchLabels, fetchUsers, fetchMilestones, isSignedIn) operators: OPERATORS_IS_NOT, symbol: '@', token: UserToken, + dataType: 'user', unique: true, fetchUsers, preloadedUsers: [], @@ -1040,4 +1042,43 @@ export const destroyBoardListMutationResponse = { }, }; +export const mockProjects = [ + { + id: 'gid://gitlab/Project/1', + name: 'Gitlab Shell', + nameWithNamespace: 'Gitlab Org / Gitlab Shell', + fullPath: 'gitlab-org/gitlab-shell', + archived: false, + __typename: 'Project', + }, + { + id: 'gid://gitlab/Project/2', + name: 'Gitlab Test', + nameWithNamespace: 'Gitlab Org / Gitlab Test', + fullPath: 'gitlab-org/gitlab-test', + archived: true, + __typename: 'Project', + }, +]; + +export const mockGroupProjectsResponse = (projects = mockProjects) => ({ + data: { + group: { + id: 'gid://gitlab/Group/1', + projects: { + nodes: projects, + pageInfo: { + hasNextPage: true, + hasPreviousPage: false, + startCursor: 'abc', + endCursor: 'bcd', + __typename: 'PageInfo', + }, + __typename: 'ProjectConnection', + }, + __typename: 'Group', + }, + }, +}); + export const DEFAULT_COLOR = '#1068bf'; diff --git a/spec/frontend/boards/project_select_spec.js b/spec/frontend/boards/project_select_spec.js index b4308b38947..f1daccfadda 100644 --- a/spec/frontend/boards/project_select_spec.js +++ b/spec/frontend/boards/project_select_spec.js @@ -1,17 +1,19 @@ import { GlCollapsibleListbox, GlListboxItem, GlLoadingIcon } from '@gitlab/ui'; -import { mount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; -import Vuex from 'vuex'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import groupProjectsQuery from '~/boards/graphql/group_projects.query.graphql'; import ProjectSelect from '~/boards/components/project_select.vue'; -import defaultState from '~/boards/stores/state'; -import { mockActiveGroupProjects, mockList } from './mock_data'; +import { mockList, mockGroupProjectsResponse, mockProjects } from './mock_data'; -const mockProjectsList1 = mockActiveGroupProjects.slice(0, 1); +Vue.use(VueApollo); describe('ProjectSelect component', () => { let wrapper; - let store; + let mockApollo; const findLabel = () => wrapper.find("[data-testid='header-label']"); const findGlCollapsibleListBox = () => wrapper.findComponent(GlCollapsibleListbox); @@ -26,77 +28,54 @@ describe('ProjectSelect component', () => { const findInMenuLoadingIcon = () => wrapper.find("[data-testid='listbox-search-loader']"); const findEmptySearchMessage = () => wrapper.find("[data-testid='listbox-no-results-text']"); - const createStore = ({ state, activeGroupProjects }) => { - Vue.use(Vuex); - - store = new Vuex.Store({ - state: { - defaultState, - groupProjectsFlags: { - isLoading: false, - pageInfo: { - hasNextPage: false, - }, - }, - ...state, - }, - actions: { - fetchGroupProjects: jest.fn(), - setSelectedProject: jest.fn(), - }, - getters: { - activeGroupProjects: () => activeGroupProjects, - }, - }); - }; - - const createWrapper = ({ state = {}, activeGroupProjects = [] } = {}) => { - createStore({ - state, - activeGroupProjects, - }); + const projectsQueryHandler = jest.fn().mockResolvedValue(mockGroupProjectsResponse()); + const emptyProjectsQueryHandler = jest.fn().mockResolvedValue(mockGroupProjectsResponse([])); - wrapper = mount(ProjectSelect, { + const createWrapper = ({ queryHandler = projectsQueryHandler, selectedProject = {} } = {}) => { + mockApollo = createMockApollo([[groupProjectsQuery, queryHandler]]); + wrapper = mountExtended(ProjectSelect, { + apolloProvider: mockApollo, propsData: { list: mockList, + selectedProject, }, - store, provide: { groupId: 1, + fullPath: 'gitlab-org', }, attachTo: document.body, }); }; - it('displays a header title', () => { - createWrapper(); - - expect(findLabel().text()).toBe('Projects'); - }); - - it('renders a default dropdown text', () => { - createWrapper(); - - expect(findGlCollapsibleListBox().exists()).toBe(true); - expect(findGlCollapsibleListBox().text()).toContain('Select a project'); - }); - describe('when mounted', () => { - it('displays a loading icon while projects are being fetched', async () => { + beforeEach(() => { createWrapper(); + }); + it('displays a loading icon while projects are being fetched', async () => { expect(findGlDropdownLoadingIcon().exists()).toBe(true); - await nextTick(); + await waitForPromises(); expect(findGlDropdownLoadingIcon().exists()).toBe(false); + expect(projectsQueryHandler).toHaveBeenCalled(); + }); + + it('displays a header title', () => { + expect(findLabel().text()).toBe('Projects'); + }); + + it('renders a default dropdown text', () => { + expect(findGlCollapsibleListBox().exists()).toBe(true); + expect(findGlCollapsibleListBox().text()).toContain('Select a project'); }); }); describe('when dropdown menu is open', () => { describe('by default', () => { - beforeEach(() => { - createWrapper({ activeGroupProjects: mockActiveGroupProjects }); + beforeEach(async () => { + createWrapper(); + await waitForPromises(); }); it('shows GlListboxSearchInput with placeholder text', () => { @@ -106,7 +85,7 @@ describe('ProjectSelect component', () => { it("displays the fetched project's name", () => { expect(findFirstGlDropdownItem().exists()).toBe(true); - expect(findFirstGlDropdownItem().text()).toContain(mockProjectsList1[0].name); + expect(findFirstGlDropdownItem().text()).toContain(mockProjects[0].name); }); it("doesn't render loading icon in the menu", () => { @@ -119,33 +98,31 @@ describe('ProjectSelect component', () => { }); describe('when no projects are being returned', () => { - it('renders empty search result message', () => { - createWrapper(); + it('renders empty search result message', async () => { + createWrapper({ queryHandler: emptyProjectsQueryHandler }); + await waitForPromises(); expect(findEmptySearchMessage().exists()).toBe(true); }); }); describe('when a project is selected', () => { - beforeEach(() => { - createWrapper({ activeGroupProjects: mockProjectsList1 }); - - findFirstGlDropdownItem().find('li').trigger('click'); + beforeEach(async () => { + createWrapper({ selectedProject: mockProjects[0] }); + await waitForPromises(); }); it('renders the name of the selected project', () => { expect(findGlCollapsibleListBox().find('.gl-new-dropdown-button-text').text()).toBe( - mockProjectsList1[0].name, + mockProjects[0].name, ); }); }); describe('when projects are loading', () => { - beforeEach(() => { - createWrapper({ state: { groupProjectsFlags: { isLoading: true } } }); - }); - - it('displays and hides gl-loading-icon while and after fetching data', () => { + it('displays and hides gl-loading-icon while and after fetching data', async () => { + createWrapper(); + await nextTick(); expect(findInMenuLoadingIcon().isVisible()).toBe(true); }); }); diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js index f3800ce8324..a2961fb1302 100644 --- a/spec/frontend/boards/stores/actions_spec.js +++ b/spec/frontend/boards/stores/actions_spec.js @@ -1541,8 +1541,8 @@ describe('addListNewIssue', () => { it('should add board scope to the issue being created', async () => { jest.spyOn(gqlClient, 'mutate').mockResolvedValue({ data: { - createIssue: { - issue: mockIssue, + createIssuable: { + issuable: mockIssue, errors: [], }, }, @@ -1600,8 +1600,8 @@ describe('addListNewIssue', () => { it('dispatches a correct set of mutations', () => { jest.spyOn(gqlClient, 'mutate').mockResolvedValue({ data: { - createIssue: { - issue: mockIssue, + createIssuable: { + issuable: mockIssue, errors: [], }, }, diff --git a/spec/frontend/branches/components/__snapshots__/delete_merged_branches_spec.js.snap b/spec/frontend/branches/components/__snapshots__/delete_merged_branches_spec.js.snap index 9db6a523dec..4da56a865d5 100644 --- a/spec/frontend/branches/components/__snapshots__/delete_merged_branches_spec.js.snap +++ b/spec/frontend/branches/components/__snapshots__/delete_merged_branches_spec.js.snap @@ -5,7 +5,6 @@ exports[`Delete merged branches component Delete merged branches confirmation mo { }; const findDeleteButton = () => - wrapper.findComponent('[data-qa-selector="delete_merged_branches_button"]'); + wrapper.findComponent('[data-testid="delete-merged-branches-button"]'); const findModal = () => wrapper.findComponent(GlModal); const findConfirmationButton = () => wrapper.findByTestId('delete-merged-branches-confirmation-button'); diff --git a/spec/frontend/ci/ci_variable_list/components/ci_environments_dropdown_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_environments_dropdown_spec.js index 1937e3b34b7..64227872af3 100644 --- a/spec/frontend/ci/ci_variable_list/components/ci_environments_dropdown_spec.js +++ b/spec/frontend/ci/ci_variable_list/components/ci_environments_dropdown_spec.js @@ -1,4 +1,10 @@ -import { GlListboxItem, GlCollapsibleListbox, GlDropdownItem, GlIcon } from '@gitlab/ui'; +import { + GlListboxItem, + GlCollapsibleListbox, + GlDropdownDivider, + GlDropdownItem, + GlIcon, +} from '@gitlab/ui'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import { allEnvironments, ENVIRONMENT_QUERY_LIMIT } from '~/ci/ci_variable_list/constants'; import CiEnvironmentsDropdown from '~/ci/ci_variable_list/components/ci_environments_dropdown.vue'; @@ -10,6 +16,7 @@ describe('Ci environments dropdown', () => { const defaultProps = { areEnvironmentsLoading: false, environments: envs, + hasEnvScopeQuery: false, selectedEnvironmentScope: '', }; @@ -19,19 +26,15 @@ describe('Ci environments dropdown', () => { const findListbox = () => wrapper.findComponent(GlCollapsibleListbox); const findListboxText = () => findListbox().props('toggleText'); const findCreateWildcardButton = () => wrapper.findComponent(GlDropdownItem); + const findDropdownDivider = () => wrapper.findComponent(GlDropdownDivider); const findMaxEnvNote = () => wrapper.findByTestId('max-envs-notice'); - const createComponent = ({ props = {}, searchTerm = '', enableFeatureFlag = false } = {}) => { + const createComponent = ({ props = {}, searchTerm = '' } = {}) => { wrapper = mountExtended(CiEnvironmentsDropdown, { propsData: { ...defaultProps, ...props, }, - provide: { - glFeatures: { - ciLimitEnvironmentScope: enableFeatureFlag, - }, - }, }); findListbox().vm.$emit('search', searchTerm); @@ -42,6 +45,10 @@ describe('Ci environments dropdown', () => { createComponent({ searchTerm: 'stable' }); }); + it('renders dropdown divider', () => { + expect(findDropdownDivider().exists()).toBe(true); + }); + it('renders create button with search term if environments do not contain search term', () => { const button = findCreateWildcardButton(); expect(button.exists()).toBe(true); @@ -51,14 +58,14 @@ describe('Ci environments dropdown', () => { describe('Search term is empty', () => { describe.each` - featureFlag | flagStatus | defaultEnvStatus | firstItemValue | envIndices - ${true} | ${'enabled'} | ${'prepends'} | ${'*'} | ${[1, 2, 3]} - ${false} | ${'disabled'} | ${'does not prepend'} | ${envs[0]} | ${[0, 1, 2]} + hasEnvScopeQuery | status | defaultEnvStatus | firstItemValue | envIndices + ${true} | ${'exists'} | ${'prepends'} | ${'*'} | ${[1, 2, 3]} + ${false} | ${'does not exist'} | ${'does not prepend'} | ${envs[0]} | ${[0, 1, 2]} `( - 'when ciLimitEnvironmentScope feature flag is $flagStatus', - ({ featureFlag, defaultEnvStatus, firstItemValue, envIndices }) => { + 'when query for fetching environment scope $status', + ({ defaultEnvStatus, firstItemValue, hasEnvScopeQuery, envIndices }) => { beforeEach(() => { - createComponent({ props: { environments: envs }, enableFeatureFlag: featureFlag }); + createComponent({ props: { environments: envs, hasEnvScopeQuery } }); }); it(`${defaultEnvStatus} * in listbox`, () => { @@ -91,7 +98,7 @@ describe('Ci environments dropdown', () => { }); }); - describe('When ciLimitEnvironmentScope feature flag is disabled', () => { + describe('when environments are not fetched via graphql', () => { const currentEnv = envs[2]; beforeEach(() => { @@ -118,11 +125,15 @@ describe('Ci environments dropdown', () => { }); }); - describe('When ciLimitEnvironmentScope feature flag is enabled', () => { + describe('when fetching environments via graphql', () => { const currentEnv = envs[2]; beforeEach(() => { - createComponent({ enableFeatureFlag: true }); + createComponent({ props: { hasEnvScopeQuery: true } }); + }); + + it('renders dropdown divider', () => { + expect(findDropdownDivider().exists()).toBe(true); }); it('renders environments passed down to it', async () => { @@ -131,6 +142,22 @@ describe('Ci environments dropdown', () => { expect(findAllListboxItems()).toHaveLength(envs.length); }); + it('renders dropdown loading icon while fetch query is loading', () => { + createComponent({ props: { areEnvironmentsLoading: true, hasEnvScopeQuery: true } }); + + expect(findListbox().props('loading')).toBe(true); + expect(findListbox().props('searching')).toBe(false); + expect(findDropdownDivider().exists()).toBe(false); + }); + + it('renders search loading icon while search query is loading and dropdown is open', async () => { + createComponent({ props: { areEnvironmentsLoading: true, hasEnvScopeQuery: true } }); + await findListbox().vm.$emit('shown'); + + expect(findListbox().props('loading')).toBe(false); + expect(findListbox().props('searching')).toBe(true); + }); + it('emits event when searching', async () => { expect(wrapper.emitted('search-environment-scope')).toHaveLength(1); @@ -140,12 +167,6 @@ describe('Ci environments dropdown', () => { expect(wrapper.emitted('search-environment-scope')[1]).toEqual([currentEnv]); }); - it('renders loading icon while search query is loading', () => { - createComponent({ enableFeatureFlag: true, props: { areEnvironmentsLoading: true } }); - - expect(findListbox().props('searching')).toBe(true); - }); - it('displays note about max environments shown', () => { expect(findMaxEnvNote().exists()).toBe(true); expect(findMaxEnvNote().text()).toContain(String(ENVIRONMENT_QUERY_LIMIT)); diff --git a/spec/frontend/ci/ci_variable_list/components/ci_group_variables_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_group_variables_spec.js index 7436210fe70..b364f098a3a 100644 --- a/spec/frontend/ci/ci_variable_list/components/ci_group_variables_spec.js +++ b/spec/frontend/ci/ci_variable_list/components/ci_group_variables_spec.js @@ -9,15 +9,13 @@ import { DELETE_MUTATION_ACTION, UPDATE_MUTATION_ACTION, } from '~/ci/ci_variable_list/constants'; +import getGroupEnvironments from '~/ci/ci_variable_list/graphql/queries/group_environments.query.graphql'; import getGroupVariables from '~/ci/ci_variable_list/graphql/queries/group_variables.query.graphql'; import addGroupVariable from '~/ci/ci_variable_list/graphql/mutations/group_add_variable.mutation.graphql'; import deleteGroupVariable from '~/ci/ci_variable_list/graphql/mutations/group_delete_variable.mutation.graphql'; import updateGroupVariable from '~/ci/ci_variable_list/graphql/mutations/group_update_variable.mutation.graphql'; const mockProvide = { - glFeatures: { - groupScopedCiVariables: false, - }, groupPath: '/group', groupId: 12, }; @@ -27,9 +25,16 @@ describe('Ci Group Variable wrapper', () => { const findCiShared = () => wrapper.findComponent(ciVariableShared); - const createComponent = ({ provide = {} } = {}) => { + const createComponent = ({ featureFlags } = {}) => { wrapper = shallowMount(ciGroupVariables, { - provide: { ...mockProvide, ...provide }, + provide: { + ...mockProvide, + glFeatures: { + ciGroupEnvScopeGraphql: false, + groupScopedCiVariables: false, + ...featureFlags, + }, + }, }); }; @@ -62,10 +67,10 @@ describe('Ci Group Variable wrapper', () => { }); }); - describe('feature flag', () => { + describe('groupScopedCiVariables feature flag', () => { describe('When enabled', () => { beforeEach(() => { - createComponent({ provide: { glFeatures: { groupScopedCiVariables: true } } }); + createComponent({ featureFlags: { groupScopedCiVariables: true } }); }); it('Passes down `true` to variable shared component', () => { @@ -75,7 +80,7 @@ describe('Ci Group Variable wrapper', () => { describe('When disabled', () => { beforeEach(() => { - createComponent({ provide: { glFeatures: { groupScopedCiVariables: false } } }); + createComponent(); }); it('Passes down `false` to variable shared component', () => { @@ -83,4 +88,26 @@ describe('Ci Group Variable wrapper', () => { }); }); }); + + describe('ciGroupEnvScopeGraphql feature flag', () => { + describe('When enabled', () => { + beforeEach(() => { + createComponent({ featureFlags: { ciGroupEnvScopeGraphql: true } }); + }); + + it('Passes down environments query to variable shared component', () => { + expect(findCiShared().props('queryData').environments.query).toBe(getGroupEnvironments); + }); + }); + + describe('When disabled', () => { + beforeEach(() => { + createComponent(); + }); + + it('Does not pass down environments query to variable shared component', () => { + expect(findCiShared().props('queryData').environments).toBe(undefined); + }); + }); + }); }); diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js index e9484cfce57..d843646df16 100644 --- a/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js +++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js @@ -48,6 +48,7 @@ describe('Ci variable modal', () => { areScopedVariablesAvailable: true, environments: [], hideEnvironmentScope: false, + hasEnvScopeQuery: false, mode: ADD_VARIABLE_ACTION, selectedVariable: {}, variables: [], @@ -349,14 +350,14 @@ describe('Ci variable modal', () => { expect(link.attributes('href')).toBe(defaultProvide.environmentScopeLink); }); - describe('when feature flag is enabled', () => { + describe('when query for envioronment scope exists', () => { beforeEach(() => { createComponent({ props: { environments: mockEnvs, + hasEnvScopeQuery: true, variables: mockVariablesWithUniqueScopes(projectString), }, - provide: { glFeatures: { ciLimitEnvironmentScope: true } }, }); }); diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js index 12ca9a78369..d72cfc5fc14 100644 --- a/spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js +++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js @@ -21,6 +21,7 @@ describe('Ci variable table', () => { environments: mapEnvironmentNames(mockEnvs), hideEnvironmentScope: false, isLoading: false, + hasEnvScopeQuery: false, maxVariableLimit: 5, pageInfo: { after: '' }, variables: mockVariablesWithScopes(projectString), @@ -60,6 +61,7 @@ describe('Ci variable table', () => { areEnvironmentsLoading: defaultProps.areEnvironmentsLoading, areScopedVariablesAvailable: defaultProps.areScopedVariablesAvailable, environments: defaultProps.environments, + hasEnvScopeQuery: defaultProps.hasEnvScopeQuery, hideEnvironmentScope: defaultProps.hideEnvironmentScope, variables: defaultProps.variables, mode: ADD_VARIABLE_ACTION, diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js index f7b90c3da30..6fa1915f3c1 100644 --- a/spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js +++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js @@ -52,6 +52,7 @@ const mockProvide = { const defaultProps = { areScopedVariablesAvailable: true, + hasEnvScopeQuery: false, pageInfo: {}, hideEnvironmentScope: false, refetchAfterMutation: false, @@ -219,16 +220,12 @@ describe('Ci Variable Shared Component', () => { expect(mockEnvironments).toHaveBeenCalled(); }); - describe('when Limit Environment Scope FF is enabled', () => { + // applies only to project-level CI variables + describe('when environment scope is limited', () => { beforeEach(async () => { await createComponentWithApollo({ props: { ...createProjectProps() }, - provide: { - glFeatures: { - ciLimitEnvironmentScope: true, - ciVariablesPages: isVariablePagesEnabled, - }, - }, + provide: pagesFeatureFlagProvide, }); }); @@ -251,26 +248,11 @@ describe('Ci Variable Shared Component', () => { expect.objectContaining({ search: 'staging' }), ); }); - }); - - describe('when Limit Environment Scope FF is disabled', () => { - beforeEach(async () => { - await createComponentWithApollo({ - props: { ...createProjectProps() }, - provide: pagesFeatureFlagProvide, - }); - }); - it('initial query is called with the correct variables', () => { - expect(mockEnvironments).toHaveBeenCalledWith({ fullPath: '/namespace/project/' }); - }); + it('does not show loading icon in table while searching for environments', () => { + findCiSettings().vm.$emit('search-environment-scope', 'staging'); - it(`does not refetch environments when search term is present`, async () => { - expect(mockEnvironments).toHaveBeenCalledTimes(1); - - await findCiSettings().vm.$emit('search-environment-scope', 'staging'); - - expect(mockEnvironments).toHaveBeenCalledTimes(1); + expect(findLoadingIcon().exists()).toBe(false); }); }); }); @@ -532,6 +514,7 @@ describe('Ci Variable Shared Component', () => { areEnvironmentsLoading: false, areScopedVariablesAvailable: wrapper.props().areScopedVariablesAvailable, hideEnvironmentScope: defaultProps.hideEnvironmentScope, + hasEnvScopeQuery: props.hasEnvScopeQuery, pageInfo: defaultProps.pageInfo, isLoading: false, maxVariableLimit, diff --git a/spec/frontend/ci/ci_variable_list/mocks.js b/spec/frontend/ci/ci_variable_list/mocks.js index 9c9c99ad5ea..41dfc0ebfda 100644 --- a/spec/frontend/ci/ci_variable_list/mocks.js +++ b/spec/frontend/ci/ci_variable_list/mocks.js @@ -189,6 +189,7 @@ export const createProjectProps = () => { componentName: 'ProjectVariable', entity: 'project', fullPath: '/namespace/project/', + hasEnvScopeQuery: true, id: 'gid://gitlab/Project/20', mutationData: { [ADD_MUTATION_ACTION]: addProjectVariable, @@ -213,6 +214,7 @@ export const createGroupProps = () => { componentName: 'GroupVariable', entity: 'group', fullPath: '/my-group', + hasEnvScopeQuery: false, id: 'gid://gitlab/Group/20', mutationData: { [ADD_MUTATION_ACTION]: addGroupVariable, @@ -231,6 +233,7 @@ export const createGroupProps = () => { export const createInstanceProps = () => { return { componentName: 'InstanceVariable', + hasEnvScopeQuery: false, entity: '', mutationData: { [ADD_MUTATION_ACTION]: addAdminVariable, diff --git a/spec/frontend/ci/pipeline_editor/components/commit/commit_section_spec.js b/spec/frontend/ci/pipeline_editor/components/commit/commit_section_spec.js index 8834231aaef..7a9b4ffdce8 100644 --- a/spec/frontend/ci/pipeline_editor/components/commit/commit_section_spec.js +++ b/spec/frontend/ci/pipeline_editor/components/commit/commit_section_spec.js @@ -17,7 +17,8 @@ import { import { resolvers } from '~/ci/pipeline_editor/graphql/resolvers'; import commitCreate from '~/ci/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql'; import getCurrentBranch from '~/ci/pipeline_editor/graphql/queries/client/current_branch.query.graphql'; -import updatePipelineEtag from '~/ci/pipeline_editor/graphql/mutations/client/update_pipeline_etag.mutation.graphql'; +import getPipelineEtag from '~/ci/pipeline_editor/graphql/queries/client/pipeline_etag.query.graphql'; + import { mockCiConfigPath, mockCiYml, @@ -253,18 +254,20 @@ describe('Pipeline Editor | Commit section', () => { describe('when the commit returns a different etag path', () => { beforeEach(async () => { createComponentWithApollo(); - jest.spyOn(wrapper.vm.$apollo, 'mutate'); + jest.spyOn(mockApollo.clients.defaultClient.cache, 'writeQuery'); + mockMutateCommitData.mockResolvedValue(mockCommitCreateResponseNewEtag); await submitCommit(); }); - it('calls the client mutation to update the etag', () => { - // 1:Commit submission, 2:etag update, 3:currentBranch update, 4:lastCommit update - expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(4); - expect(wrapper.vm.$apollo.mutate).toHaveBeenNthCalledWith(2, { - mutation: updatePipelineEtag, - variables: { - pipelineEtag: mockCommitCreateResponseNewEtag.data.commitCreate.commitPipelinePath, + it('calls the client mutation to update the etag in the cache', () => { + expect(mockApollo.clients.defaultClient.cache.writeQuery).toHaveBeenCalledWith({ + query: getPipelineEtag, + data: { + etags: { + __typename: 'EtagValues', + pipeline: mockCommitCreateResponseNewEtag.data.commitCreate.commitPipelinePath, + }, }, }); }); diff --git a/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/rules_item_spec.js b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/rules_item_spec.js index edaa96a197a..d40499fae87 100644 --- a/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/rules_item_spec.js +++ b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/rules_item_spec.js @@ -49,32 +49,36 @@ describe('Rules item', () => { findRulesWhenSelect().vm.$emit('input', dummyRulesWhen); - expect(wrapper.emitted('update-job')).toHaveLength(1); + expect(wrapper.emitted('update-job')).toHaveLength(2); expect(wrapper.emitted('update-job')[0]).toEqual([ 'rules[0].when', JOB_RULES_WHEN.delayed.value, ]); + expect(wrapper.emitted('update-job')[1]).toEqual([ + 'rules[0].start_in', + `1 ${JOB_RULES_START_IN.second.value}`, + ]); findRulesStartInNumberInput().vm.$emit('input', dummyRulesStartInNumber); - expect(wrapper.emitted('update-job')).toHaveLength(2); - expect(wrapper.emitted('update-job')[1]).toEqual([ + expect(wrapper.emitted('update-job')).toHaveLength(3); + expect(wrapper.emitted('update-job')[2]).toEqual([ 'rules[0].start_in', `2 ${JOB_RULES_START_IN.second.value}s`, ]); findRulesStartInUnitSelect().vm.$emit('input', dummyRulesStartInUnit); - expect(wrapper.emitted('update-job')).toHaveLength(3); - expect(wrapper.emitted('update-job')[2]).toEqual([ + expect(wrapper.emitted('update-job')).toHaveLength(4); + expect(wrapper.emitted('update-job')[3]).toEqual([ 'rules[0].start_in', `2 ${dummyRulesStartInUnit}s`, ]); findRulesAllowFailureCheckBox().vm.$emit('input', dummyRulesAllowFailure); - expect(wrapper.emitted('update-job')).toHaveLength(4); - expect(wrapper.emitted('update-job')[3]).toEqual([ + expect(wrapper.emitted('update-job')).toHaveLength(5); + expect(wrapper.emitted('update-job')[4]).toEqual([ 'rules[0].allow_failure', dummyRulesAllowFailure, ]); diff --git a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js index 639c2dbef4c..bb48d4dc38d 100644 --- a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js +++ b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js @@ -1,14 +1,47 @@ import MockAdapter from 'axios-mock-adapter'; -import { GlForm } from '@gitlab/ui'; -import { nextTick } from 'vue'; +import { GlForm, GlLoadingIcon } from '@gitlab/ui'; +import Vue, { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper'; +import waitForPromises from 'helpers/wait_for_promises'; import axios from '~/lib/utils/axios_utils'; +import { visitUrl } from '~/lib/utils/url_utility'; +import { createAlert } from '~/alert'; import PipelineSchedulesForm from '~/ci/pipeline_schedules/components/pipeline_schedules_form.vue'; import RefSelector from '~/ref/components/ref_selector.vue'; import { REF_TYPE_BRANCHES, REF_TYPE_TAGS } from '~/ref/constants'; import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown/timezone_dropdown.vue'; import IntervalPatternInput from '~/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue'; +import createPipelineScheduleMutation from '~/ci/pipeline_schedules/graphql/mutations/create_pipeline_schedule.mutation.graphql'; +import updatePipelineScheduleMutation from '~/ci/pipeline_schedules/graphql/mutations/update_pipeline_schedule.mutation.graphql'; +import getPipelineSchedulesQuery from '~/ci/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql'; import { timezoneDataFixture } from '../../../vue_shared/components/timezone_dropdown/helpers'; +import { + createScheduleMutationResponse, + updateScheduleMutationResponse, + mockSinglePipelineScheduleNode, +} from '../mock_data'; + +Vue.use(VueApollo); + +jest.mock('~/alert'); +jest.mock('~/lib/utils/url_utility', () => ({ + visitUrl: jest.fn(), + joinPaths: jest.fn().mockReturnValue(''), + queryToObject: jest.fn().mockReturnValue({ id: '1' }), +})); + +const { + data: { + project: { + pipelineSchedules: { nodes }, + }, + }, +} = mockSinglePipelineScheduleNode; + +const schedule = nodes[0]; +const variables = schedule.variables.nodes; describe('Pipeline schedules form', () => { let wrapper; @@ -17,22 +50,36 @@ describe('Pipeline schedules form', () => { const cron = ''; const dailyLimit = ''; - const createComponent = (mountFn = shallowMountExtended, stubs = {}) => { + const querySuccessHandler = jest.fn().mockResolvedValue(mockSinglePipelineScheduleNode); + const queryFailedHandler = jest.fn().mockRejectedValue(new Error('GraphQL error')); + + const createMutationHandlerSuccess = jest.fn().mockResolvedValue(createScheduleMutationResponse); + const createMutationHandlerFailed = jest.fn().mockRejectedValue(new Error('GraphQL error')); + const updateMutationHandlerSuccess = jest.fn().mockResolvedValue(updateScheduleMutationResponse); + const updateMutationHandlerFailed = jest.fn().mockRejectedValue(new Error('GraphQL error')); + + const createMockApolloProvider = ( + requestHandlers = [[createPipelineScheduleMutation, createMutationHandlerSuccess]], + ) => { + return createMockApollo(requestHandlers); + }; + + const createComponent = (mountFn = shallowMountExtended, editing = false, requestHandlers) => { wrapper = mountFn(PipelineSchedulesForm, { propsData: { timezoneData: timezoneDataFixture, refParam: 'master', + editing, }, provide: { fullPath: 'gitlab-org/gitlab', projectId, defaultBranch, - cron, - cronTimezone: '', dailyLimit, settingsLink: '', + schedulesPath: '/root/ci-project/-/pipeline_schedules', }, - stubs, + apolloProvider: createMockApolloProvider(requestHandlers), }); }; @@ -43,17 +90,24 @@ describe('Pipeline schedules form', () => { const findRefSelector = () => wrapper.findComponent(RefSelector); const findSubmitButton = () => wrapper.findByTestId('schedule-submit-button'); const findCancelButton = () => wrapper.findByTestId('schedule-cancel-button'); + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); // Variables const findVariableRows = () => wrapper.findAllByTestId('ci-variable-row'); const findKeyInputs = () => wrapper.findAllByTestId('pipeline-form-ci-variable-key'); const findValueInputs = () => wrapper.findAllByTestId('pipeline-form-ci-variable-value'); const findRemoveIcons = () => wrapper.findAllByTestId('remove-ci-variable-row'); - beforeEach(() => { - createComponent(); - }); + const addVariableToForm = () => { + const input = findKeyInputs().at(0); + input.element.value = 'test_var_2'; + input.trigger('change'); + }; describe('Form elements', () => { + beforeEach(() => { + createComponent(); + }); + it('displays form', () => { expect(findForm().exists()).toBe(true); }); @@ -102,19 +156,16 @@ describe('Pipeline schedules form', () => { it('displays the submit and cancel buttons', () => { expect(findSubmitButton().exists()).toBe(true); expect(findCancelButton().exists()).toBe(true); + expect(findCancelButton().attributes('href')).toBe('/root/ci-project/-/pipeline_schedules'); }); }); describe('CI variables', () => { let mock; - const addVariableToForm = () => { - const input = findKeyInputs().at(0); - input.element.value = 'test_var_2'; - input.trigger('change'); - }; - beforeEach(() => { + // mock is needed when we fully mount + // downstream components request needs to be mocked mock = new MockAdapter(axios); createComponent(mountExtended); }); @@ -157,4 +208,229 @@ describe('Pipeline schedules form', () => { expect(findVariableRows()).toHaveLength(1); }); }); + + describe('Button text', () => { + it.each` + editing | expectedText + ${true} | ${'Edit pipeline schedule'} + ${false} | ${'Create pipeline schedule'} + `( + 'button text is $expectedText when editing is $editing', + async ({ editing, expectedText }) => { + createComponent(shallowMountExtended, editing, [ + [getPipelineSchedulesQuery, querySuccessHandler], + ]); + + await waitForPromises(); + + expect(findSubmitButton().text()).toBe(expectedText); + }, + ); + }); + + describe('Schedule creation', () => { + it('when creating a schedule the query is not called', () => { + createComponent(); + + expect(querySuccessHandler).not.toHaveBeenCalled(); + }); + + it('does not show loading state when creating new schedule', () => { + createComponent(); + + expect(findLoadingIcon().exists()).toBe(false); + }); + + describe('schedule creation success', () => { + let mock; + + beforeEach(() => { + // mock is needed when we fully mount + // downstream components request needs to be mocked + mock = new MockAdapter(axios); + createComponent(mountExtended); + }); + + afterEach(() => { + mock.restore(); + }); + + it('creates pipeline schedule', async () => { + findDescription().element.value = 'My schedule'; + findDescription().trigger('change'); + + findTimezoneDropdown().vm.$emit('input', { + formattedTimezone: '[UTC-4] Eastern Time (US & Canada)', + identifier: 'America/New_York', + }); + + findIntervalComponent().vm.$emit('cronValue', '0 16 * * *'); + + addVariableToForm(); + + findSubmitButton().vm.$emit('click'); + + await waitForPromises(); + + expect(createMutationHandlerSuccess).toHaveBeenCalledWith({ + input: { + active: true, + cron: '0 16 * * *', + cronTimezone: 'America/New_York', + description: 'My schedule', + projectPath: 'gitlab-org/gitlab', + ref: 'main', + variables: [ + { + key: 'test_var_2', + value: '', + variableType: 'ENV_VAR', + }, + ], + }, + }); + expect(visitUrl).toHaveBeenCalledWith('/root/ci-project/-/pipeline_schedules'); + expect(createAlert).not.toHaveBeenCalled(); + }); + }); + + describe('schedule creation failure', () => { + beforeEach(() => { + createComponent(shallowMountExtended, false, [ + [createPipelineScheduleMutation, createMutationHandlerFailed], + ]); + }); + + it('shows error for failed pipeline schedule creation', async () => { + findSubmitButton().vm.$emit('click'); + + await waitForPromises(); + + expect(createAlert).toHaveBeenCalledWith({ + message: 'An error occurred while creating the pipeline schedule.', + }); + }); + }); + }); + + describe('Schedule editing', () => { + let mock; + + beforeEach(() => { + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + }); + + it('shows loading state when editing', async () => { + createComponent(shallowMountExtended, true, [ + [getPipelineSchedulesQuery, querySuccessHandler], + ]); + + expect(findLoadingIcon().exists()).toBe(true); + + await waitForPromises(); + + expect(findLoadingIcon().exists()).toBe(false); + }); + + describe('schedule fetch success', () => { + it('fetches schedule and sets form data correctly', async () => { + createComponent(mountExtended, true, [[getPipelineSchedulesQuery, querySuccessHandler]]); + + expect(querySuccessHandler).toHaveBeenCalled(); + + await waitForPromises(); + + expect(findDescription().element.value).toBe(schedule.description); + expect(findIntervalComponent().props('initialCronInterval')).toBe(schedule.cron); + expect(findTimezoneDropdown().props('value')).toBe(schedule.cronTimezone); + expect(findRefSelector().props('value')).toBe(schedule.ref); + expect(findVariableRows()).toHaveLength(3); + expect(findKeyInputs().at(0).element.value).toBe(variables[0].key); + expect(findKeyInputs().at(1).element.value).toBe(variables[1].key); + expect(findValueInputs().at(0).element.value).toBe(variables[0].value); + expect(findValueInputs().at(1).element.value).toBe(variables[1].value); + }); + }); + + it('schedule fetch failure', async () => { + createComponent(shallowMountExtended, true, [ + [getPipelineSchedulesQuery, queryFailedHandler], + ]); + + await waitForPromises(); + + expect(createAlert).toHaveBeenCalledWith({ + message: 'An error occurred while trying to fetch the pipeline schedule.', + }); + }); + + it('edit schedule success', async () => { + createComponent(mountExtended, true, [ + [getPipelineSchedulesQuery, querySuccessHandler], + [updatePipelineScheduleMutation, updateMutationHandlerSuccess], + ]); + + await waitForPromises(); + + findDescription().element.value = 'Updated schedule'; + findDescription().trigger('change'); + + findIntervalComponent().vm.$emit('cronValue', '0 22 16 * *'); + + // Ensures variable is sent with destroy property set true + findRemoveIcons().at(0).vm.$emit('click'); + + findSubmitButton().vm.$emit('click'); + + await waitForPromises(); + + expect(updateMutationHandlerSuccess).toHaveBeenCalledWith({ + input: { + active: schedule.active, + cron: '0 22 16 * *', + cronTimezone: schedule.cronTimezone, + id: schedule.id, + ref: schedule.ref, + description: 'Updated schedule', + variables: [ + { + destroy: true, + id: variables[0].id, + key: variables[0].key, + value: variables[0].value, + variableType: variables[0].variableType, + }, + { + destroy: false, + id: variables[1].id, + key: variables[1].key, + value: variables[1].value, + variableType: variables[1].variableType, + }, + ], + }, + }); + }); + + it('edit schedule failure', async () => { + createComponent(shallowMountExtended, true, [ + [getPipelineSchedulesQuery, querySuccessHandler], + [updatePipelineScheduleMutation, updateMutationHandlerFailed], + ]); + + await waitForPromises(); + + findSubmitButton().vm.$emit('click'); + + await waitForPromises(); + + expect(createAlert).toHaveBeenCalledWith({ + message: 'An error occurred while updating the pipeline schedule.', + }); + }); + }); }); diff --git a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js index 50008cedd9c..01a19711264 100644 --- a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js +++ b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js @@ -57,6 +57,7 @@ describe('Pipeline schedules app', () => { wrapper = mountExtended(PipelineSchedules, { provide: { fullPath: 'gitlab-org/gitlab', + newSchedulePath: '/root/ci-project/-/pipeline_schedules/new', }, mocks: { $toast, @@ -101,6 +102,10 @@ describe('Pipeline schedules app', () => { expect(findLoadingIcon().exists()).toBe(false); }); + + it('new schedule button links to new schedule path', () => { + expect(findNewButton().attributes('href')).toBe('/root/ci-project/-/pipeline_schedules/new'); + }); }); describe('fetching pipeline schedules', () => { @@ -146,15 +151,13 @@ describe('Pipeline schedules app', () => { [deletePipelineScheduleMutation, deleteMutationHandlerSuccess], ]); - jest.spyOn(wrapper.vm.$apollo.queries.schedules, 'refetch'); - await waitForPromises(); const scheduleId = mockPipelineScheduleNodes[0].id; findTable().vm.$emit('showDeleteModal', scheduleId); - expect(wrapper.vm.$apollo.queries.schedules.refetch).not.toHaveBeenCalled(); + expect(successHandler).toHaveBeenCalledTimes(1); findDeleteModal().vm.$emit('deleteSchedule'); @@ -163,7 +166,7 @@ describe('Pipeline schedules app', () => { expect(deleteMutationHandlerSuccess).toHaveBeenCalledWith({ id: scheduleId, }); - expect(wrapper.vm.$apollo.queries.schedules.refetch).toHaveBeenCalled(); + expect(successHandler).toHaveBeenCalledTimes(2); expect($toast.show).toHaveBeenCalledWith('Pipeline schedule successfully deleted.'); }); @@ -252,15 +255,13 @@ describe('Pipeline schedules app', () => { [takeOwnershipMutation, takeOwnershipMutationHandlerSuccess], ]); - jest.spyOn(wrapper.vm.$apollo.queries.schedules, 'refetch'); - await waitForPromises(); const scheduleId = mockPipelineScheduleNodes[1].id; findTable().vm.$emit('showTakeOwnershipModal', scheduleId); - expect(wrapper.vm.$apollo.queries.schedules.refetch).not.toHaveBeenCalled(); + expect(successHandler).toHaveBeenCalledTimes(1); findTakeOwnershipModal().vm.$emit('takeOwnership'); @@ -269,7 +270,7 @@ describe('Pipeline schedules app', () => { expect(takeOwnershipMutationHandlerSuccess).toHaveBeenCalledWith({ id: scheduleId, }); - expect(wrapper.vm.$apollo.queries.schedules.refetch).toHaveBeenCalled(); + expect(successHandler).toHaveBeenCalledTimes(2); expect($toast.show).toHaveBeenCalledWith('Successfully taken ownership from Admin.'); }); @@ -297,7 +298,7 @@ describe('Pipeline schedules app', () => { describe('pipeline schedule tabs', () => { beforeEach(async () => { - createComponent(); + createComponent([[getPipelineSchedulesQuery, successHandler]]); await waitForPromises(); }); @@ -315,13 +316,23 @@ describe('Pipeline schedules app', () => { }); it('should refetch the schedules query on a tab click', async () => { - jest.spyOn(wrapper.vm.$apollo.queries.schedules, 'refetch').mockImplementation(jest.fn()); - - expect(wrapper.vm.$apollo.queries.schedules.refetch).toHaveBeenCalledTimes(0); + expect(successHandler).toHaveBeenCalledTimes(1); await findAllTab().trigger('click'); - expect(wrapper.vm.$apollo.queries.schedules.refetch).toHaveBeenCalledTimes(1); + expect(successHandler).toHaveBeenCalledTimes(3); + }); + + it('all tab click should not send scope value with query', async () => { + findAllTab().trigger('click'); + + await nextTick(); + + expect(successHandler).toHaveBeenCalledWith({ + ids: null, + projectPath: 'gitlab-org/gitlab', + status: null, + }); }); }); diff --git a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js index be0052fc7cf..5eca355fcf4 100644 --- a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js +++ b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js @@ -1,6 +1,7 @@ import { GlButton } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import PipelineScheduleActions from '~/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { mockPipelineScheduleNodes, mockPipelineScheduleCurrentUser, @@ -28,6 +29,7 @@ describe('Pipeline schedule actions', () => { const findDeleteBtn = () => wrapper.findByTestId('delete-pipeline-schedule-btn'); const findTakeOwnershipBtn = () => wrapper.findByTestId('take-ownership-pipeline-schedule-btn'); const findPlayScheduleBtn = () => wrapper.findByTestId('play-pipeline-schedule-btn'); + const findEditScheduleBtn = () => wrapper.findByTestId('edit-pipeline-schedule-btn'); it('displays buttons when user is the owner of schedule and has adminPipelineSchedule permissions', () => { createComponent(); @@ -76,4 +78,15 @@ describe('Pipeline schedule actions', () => { playPipelineSchedule: [[mockPipelineScheduleNodes[0].id]], }); }); + + it('edit button links to edit schedule path', () => { + createComponent(); + + const { schedule } = defaultProps; + const id = getIdFromGraphQLId(schedule.id); + + const expectedPath = `${schedule.editPath}?id=${id}`; + + expect(findEditScheduleBtn().attributes('href')).toBe(expectedPath); + }); }); diff --git a/spec/frontend/ci/pipeline_schedules/mock_data.js b/spec/frontend/ci/pipeline_schedules/mock_data.js index 1485f6beea4..0a4f233f199 100644 --- a/spec/frontend/ci/pipeline_schedules/mock_data.js +++ b/spec/frontend/ci/pipeline_schedules/mock_data.js @@ -2,6 +2,7 @@ import mockGetPipelineSchedulesGraphQLResponse from 'test_fixtures/graphql/pipeline_schedules/get_pipeline_schedules.query.graphql.json'; import mockGetPipelineSchedulesAsGuestGraphQLResponse from 'test_fixtures/graphql/pipeline_schedules/get_pipeline_schedules.query.graphql.as_guest.json'; import mockGetPipelineSchedulesTakeOwnershipGraphQLResponse from 'test_fixtures/graphql/pipeline_schedules/get_pipeline_schedules.query.graphql.take_ownership.json'; +import mockGetSinglePipelineScheduleGraphQLResponse from 'test_fixtures/graphql/pipeline_schedules/get_pipeline_schedules.query.graphql.single.json'; const { data: { @@ -30,15 +31,22 @@ const { export const mockPipelineScheduleNodes = nodes; export const mockPipelineScheduleCurrentUser = currentUser; - export const mockPipelineScheduleAsGuestNodes = guestNodes; - export const mockTakeOwnershipNodes = takeOwnershipNodes; +export const mockSinglePipelineScheduleNode = mockGetSinglePipelineScheduleGraphQLResponse; + export const emptyPipelineSchedulesResponse = { data: { + currentUser: { + id: 'gid://gitlab/User/1', + username: 'root', + }, project: { id: 'gid://gitlab/Project/1', - pipelineSchedules: { nodes: [], count: 0 }, + pipelineSchedules: { + count: 0, + nodes: [], + }, }, }, }; @@ -79,4 +87,24 @@ export const takeOwnershipMutationResponse = { }, }; +export const createScheduleMutationResponse = { + data: { + pipelineScheduleCreate: { + clientMutationId: null, + errors: [], + __typename: 'PipelineScheduleCreatePayload', + }, + }, +}; + +export const updateScheduleMutationResponse = { + data: { + pipelineScheduleUpdate: { + clientMutationId: null, + errors: [], + __typename: 'PipelineScheduleUpdatePayload', + }, + }, +}; + export { mockGetPipelineSchedulesGraphQLResponse }; diff --git a/spec/frontend/ci/reports/components/__snapshots__/grouped_issues_list_spec.js.snap b/spec/frontend/ci/reports/components/__snapshots__/grouped_issues_list_spec.js.snap deleted file mode 100644 index 311a67a3e31..00000000000 --- a/spec/frontend/ci/reports/components/__snapshots__/grouped_issues_list_spec.js.snap +++ /dev/null @@ -1,26 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Grouped Issues List renders a smart virtual list with the correct props 1`] = ` -Object { - "length": 4, - "remain": 20, - "rtag": "div", - "size": 32, - "wclass": "report-block-list", - "wtag": "ul", -} -`; - -exports[`Grouped Issues List with data renders a report item with the correct props 1`] = ` -Object { - "component": "CodequalityIssueBody", - "iconComponent": "IssueStatusIcon", - "isNew": false, - "issue": Object { - "name": "foo", - }, - "showReportSectionStatusIcon": false, - "status": "none", - "statusIconSize": 24, -} -`; diff --git a/spec/frontend/ci/reports/components/grouped_issues_list_spec.js b/spec/frontend/ci/reports/components/grouped_issues_list_spec.js deleted file mode 100644 index 8beec220802..00000000000 --- a/spec/frontend/ci/reports/components/grouped_issues_list_spec.js +++ /dev/null @@ -1,83 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import GroupedIssuesList from '~/ci/reports/components/grouped_issues_list.vue'; -import ReportItem from '~/ci/reports/components/report_item.vue'; -import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue'; - -describe('Grouped Issues List', () => { - let wrapper; - - const createComponent = ({ propsData = {}, stubs = {} } = {}) => { - wrapper = shallowMount(GroupedIssuesList, { - propsData, - stubs, - }); - }; - - const findHeading = (groupName) => wrapper.find(`[data-testid="${groupName}Heading"`); - - it('renders a smart virtual list with the correct props', () => { - createComponent({ - propsData: { - resolvedIssues: [{ name: 'foo' }], - unresolvedIssues: [{ name: 'bar' }], - }, - stubs: { - SmartVirtualList, - }, - }); - - expect(wrapper.findComponent(SmartVirtualList).props()).toMatchSnapshot(); - }); - - describe('without data', () => { - beforeEach(() => { - createComponent(); - }); - - it.each(['unresolved', 'resolved'])('does not a render a header for %s issues', (issueName) => { - expect(findHeading(issueName).exists()).toBe(false); - }); - - it.each(['resolved', 'unresolved'])('does not render report items for %s issues', () => { - expect(wrapper.findComponent(ReportItem).exists()).toBe(false); - }); - }); - - describe('with data', () => { - it.each` - givenIssues | givenHeading | groupName - ${[{ name: 'foo issue' }]} | ${'Foo Heading'} | ${'resolved'} - ${[{ name: 'bar issue' }]} | ${'Bar Heading'} | ${'unresolved'} - `('renders the heading for $groupName issues', ({ givenIssues, givenHeading, groupName }) => { - createComponent({ - propsData: { [`${groupName}Issues`]: givenIssues, [`${groupName}Heading`]: givenHeading }, - }); - - expect(findHeading(groupName).text()).toBe(givenHeading); - }); - - it.each(['resolved', 'unresolved'])('renders all %s issues', (issueName) => { - const issues = [{ name: 'foo' }, { name: 'bar' }]; - - createComponent({ - propsData: { [`${issueName}Issues`]: issues }, - }); - - expect(wrapper.findAllComponents(ReportItem)).toHaveLength(issues.length); - }); - - it('renders a report item with the correct props', () => { - createComponent({ - propsData: { - resolvedIssues: [{ name: 'foo' }], - component: 'CodequalityIssueBody', - }, - stubs: { - ReportItem, - }, - }); - - expect(wrapper.findComponent(ReportItem).props()).toMatchSnapshot(); - }); - }); -}); diff --git a/spec/frontend/ci/reports/components/summary_row_spec.js b/spec/frontend/ci/reports/components/summary_row_spec.js deleted file mode 100644 index b1ae9e26b5b..00000000000 --- a/spec/frontend/ci/reports/components/summary_row_spec.js +++ /dev/null @@ -1,63 +0,0 @@ -import { mount } from '@vue/test-utils'; -import { extendedWrapper } from 'helpers/vue_test_utils_helper'; -import HelpPopover from '~/vue_shared/components/help_popover.vue'; -import SummaryRow from '~/ci/reports/components/summary_row.vue'; - -describe('Summary row', () => { - let wrapper; - - const summary = 'SAST detected 1 new vulnerability and 1 fixed vulnerability'; - const popoverOptions = { - title: 'Static Application Security Testing (SAST)', - content: 'Learn more about SAST', - }; - const statusIcon = 'warning'; - - const createComponent = ({ props = {}, slots = {} } = {}) => { - wrapper = extendedWrapper( - mount(SummaryRow, { - propsData: { - summary, - popoverOptions, - statusIcon, - ...props, - }, - slots, - }), - ); - }; - - const findSummary = () => wrapper.findByTestId('summary-row-description'); - const findStatusIcon = () => wrapper.findByTestId('summary-row-icon'); - const findHelpPopover = () => wrapper.findComponent(HelpPopover); - - it('renders provided summary', () => { - createComponent(); - expect(findSummary().text()).toContain(summary); - }); - - it('renders provided icon', () => { - createComponent(); - expect(findStatusIcon().classes()).toContain('js-ci-status-icon-warning'); - }); - - it('renders help popover if popoverOptions are provided', () => { - createComponent(); - expect(findHelpPopover().props('options')).toEqual(popoverOptions); - }); - - it('does not render help popover if popoverOptions are not provided', () => { - createComponent({ props: { popoverOptions: null } }); - expect(findHelpPopover().exists()).toBe(false); - }); - - describe('summary slot', () => { - it('replaces the summary prop', () => { - const summarySlotContent = 'Summary slot content'; - createComponent({ slots: { summary: summarySlotContent } }); - - expect(wrapper.text()).not.toContain(summary); - expect(findSummary().text()).toContain(summarySlotContent); - }); - }); -}); diff --git a/spec/frontend/ci/reports/mock_data/mock_data.js b/spec/frontend/ci/reports/mock_data/mock_data.js index 2599b0ac365..2983a9f1125 100644 --- a/spec/frontend/ci/reports/mock_data/mock_data.js +++ b/spec/frontend/ci/reports/mock_data/mock_data.js @@ -1,3 +1,6 @@ +import { SEVERITIES as SEVERITIES_CODE_QUALITY } from '~/ci/reports/codequality_report/constants'; +import { SEVERITIES as SEVERITIES_SAST } from '~/ci/reports/sast/constants'; + export const failedIssue = { result: 'failure', name: 'Test#sum when a is 1 and b is 2 returns summary', @@ -36,3 +39,54 @@ export const failedReport = { }, ], }; + +export const findingSastInfo = { + scale: 'sast', + severity: 'info', +}; + +export const findingSastInfoEnhanced = { + scale: 'sast', + severity: 'info', + class: SEVERITIES_SAST.info.class, + name: SEVERITIES_SAST.info.name, +}; + +export const findingsCodeQualityBlocker = { + scale: 'codeQuality', + severity: 'blocker', +}; + +export const findingCodeQualityBlockerEnhanced = { + scale: 'codeQuality', + severity: 'blocker', + class: SEVERITIES_CODE_QUALITY.blocker.class, + name: SEVERITIES_CODE_QUALITY.blocker.name, +}; + +export const findingCodeQualityInfo = { + scale: 'codeQuality', + severity: 'info', +}; + +export const findingCodeQualityInfoEnhanced = { + scale: 'codeQuality', + severity: 'info', + class: SEVERITIES_CODE_QUALITY.info.class, + name: SEVERITIES_CODE_QUALITY.info.name, +}; + +export const findingUnknownInfo = { + scale: 'codeQuality', + severity: 'info', +}; + +export const findingUnknownInfoEnhanced = { + scale: 'codeQuality', + severity: 'info', + class: SEVERITIES_CODE_QUALITY.info.class, + name: SEVERITIES_CODE_QUALITY.info.name, +}; + +export const findingsArray = [findingSastInfo, findingsCodeQualityBlocker]; +export const findingsArrayEnhanced = [findingSastInfoEnhanced, findingCodeQualityBlockerEnhanced]; diff --git a/spec/frontend/ci/reports/utils_spec.js b/spec/frontend/ci/reports/utils_spec.js new file mode 100644 index 00000000000..e01aa903a97 --- /dev/null +++ b/spec/frontend/ci/reports/utils_spec.js @@ -0,0 +1,30 @@ +import { getSeverity } from '~/ci/reports/utils'; + +import { + findingSastInfo, + findingSastInfoEnhanced, + findingCodeQualityInfo, + findingCodeQualityInfoEnhanced, + findingUnknownInfo, + findingUnknownInfoEnhanced, + findingsArray, + findingsArrayEnhanced, +} from './mock_data/mock_data'; + +describe('getSeverity utility function', () => { + it('should enhance finding with sast scale', () => { + expect(getSeverity(findingSastInfo)).toEqual(findingSastInfoEnhanced); + }); + + it('should enhance finding with codequality scale', () => { + expect(getSeverity(findingCodeQualityInfo)).toEqual(findingCodeQualityInfoEnhanced); + }); + + it('should use codeQuality scale when scale is unknown', () => { + expect(getSeverity(findingUnknownInfo)).toEqual(findingUnknownInfoEnhanced); + }); + + it('should correctly enhance an array of findings', () => { + expect(getSeverity(findingsArray)).toEqual(findingsArrayEnhanced); + }); +}); diff --git a/spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js b/spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js index c4ed6d1bdb5..c9349c64bfb 100644 --- a/spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js +++ b/spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js @@ -9,10 +9,8 @@ import { visitUrl } from '~/lib/utils/url_utility'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import RunnerHeader from '~/ci/runner/components/runner_header.vue'; +import RunnerHeaderActions from '~/ci/runner/components/runner_header_actions.vue'; import RunnerDetails from '~/ci/runner/components/runner_details.vue'; -import RunnerPauseButton from '~/ci/runner/components/runner_pause_button.vue'; -import RunnerDeleteButton from '~/ci/runner/components/runner_delete_button.vue'; -import RunnerEditButton from '~/ci/runner/components/runner_edit_button.vue'; import RunnerDetailsTabs from '~/ci/runner/components/runner_details_tabs.vue'; import RunnersJobs from '~/ci/runner/components/runner_jobs.vue'; @@ -46,9 +44,7 @@ describe('AdminRunnerShowApp', () => { const findRunnerHeader = () => wrapper.findComponent(RunnerHeader); const findRunnerDetails = () => wrapper.findComponent(RunnerDetails); - const findRunnerDeleteButton = () => wrapper.findComponent(RunnerDeleteButton); - const findRunnerEditButton = () => wrapper.findComponent(RunnerEditButton); - const findRunnerPauseButton = () => wrapper.findComponent(RunnerPauseButton); + const findRunnerHeaderActions = () => wrapper.findComponent(RunnerHeaderActions); const findRunnerDetailsTabs = () => wrapper.findComponent(RunnerDetailsTabs); const findRunnersJobs = () => wrapper.findComponent(RunnersJobs); @@ -94,9 +90,10 @@ describe('AdminRunnerShowApp', () => { }); it('displays the runner edit and pause buttons', () => { - expect(findRunnerEditButton().attributes('href')).toBe(mockRunner.editAdminUrl); - expect(findRunnerPauseButton().exists()).toBe(true); - expect(findRunnerDeleteButton().exists()).toBe(true); + expect(findRunnerHeaderActions().props()).toEqual({ + runner: mockRunner, + editPath: mockRunner.editAdminUrl, + }); }); it('shows runner details', () => { @@ -122,54 +119,6 @@ describe('AdminRunnerShowApp', () => { expect(wrapper.text().replace(/\s+/g, ' ')).toContain(expected); }); - describe('when runner cannot be updated', () => { - beforeEach(async () => { - mockRunnerQueryResult({ - userPermissions: { - ...mockRunner.userPermissions, - updateRunner: false, - }, - }); - - await createComponent({ - mountFn: mountExtended, - }); - }); - - it('does not display the runner edit and pause buttons', () => { - expect(findRunnerEditButton().exists()).toBe(false); - expect(findRunnerPauseButton().exists()).toBe(false); - }); - - it('displays delete button', () => { - expect(findRunnerDeleteButton().exists()).toBe(true); - }); - }); - - describe('when runner cannot be deleted', () => { - beforeEach(async () => { - mockRunnerQueryResult({ - userPermissions: { - ...mockRunner.userPermissions, - deleteRunner: false, - }, - }); - - await createComponent({ - mountFn: mountExtended, - }); - }); - - it('does not display the delete button', () => { - expect(findRunnerDeleteButton().exists()).toBe(false); - }); - - it('displays edit and pause buttons', () => { - expect(findRunnerEditButton().exists()).toBe(true); - expect(findRunnerPauseButton().exists()).toBe(true); - }); - }); - describe('when runner is deleted', () => { beforeEach(async () => { await createComponent({ @@ -178,7 +127,7 @@ describe('AdminRunnerShowApp', () => { }); it('redirects to the runner list page', () => { - findRunnerDeleteButton().vm.$emit('deleted', { message: 'Runner deleted' }); + findRunnerHeaderActions().vm.$emit('deleted', { message: 'Runner deleted' }); expect(saveAlertToLocalStorage).toHaveBeenCalledWith({ message: 'Runner deleted', @@ -187,23 +136,6 @@ describe('AdminRunnerShowApp', () => { expect(visitUrl).toHaveBeenCalledWith(mockRunnersPath); }); }); - - describe('when runner does not have an edit url', () => { - beforeEach(async () => { - mockRunnerQueryResult({ - editAdminUrl: null, - }); - - await createComponent({ - mountFn: mountExtended, - }); - }); - - it('does not display the runner edit button', () => { - expect(findRunnerEditButton().exists()).toBe(false); - expect(findRunnerPauseButton().exists()).toBe(true); - }); - }); }); describe('When loading', () => { diff --git a/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js b/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js index fc74e2947b6..1bbcb991619 100644 --- a/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js +++ b/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js @@ -156,7 +156,7 @@ describe('AdminRunnersApp', () => { await createComponent({ mountFn: mountExtended }); }); - // https://gitlab.com/gitlab-org/gitlab/-/issues/414975 + // quarantine: https://gitlab.com/gitlab-org/gitlab/-/issues/414975 // eslint-disable-next-line jest/no-disabled-tests it.skip('fetches counts', () => { expect(mockRunnersCountHandler).toHaveBeenCalledTimes(COUNT_QUERIES); diff --git a/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js b/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js index cda3876f9b2..ad20d7682ed 100644 --- a/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js +++ b/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js @@ -1,5 +1,6 @@ +import { GlSprintf } from '@gitlab/ui'; import { __, sprintf } from '~/locale'; -import { mountExtended } from 'helpers/vue_test_utils_helper'; +import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; import RunnerSummaryCell from '~/ci/runner/components/cells/runner_summary_cell.vue'; import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; @@ -31,8 +32,8 @@ describe('RunnerTypeCell', () => { wrapper.findAllComponents(RunnerSummaryField).filter((w) => w.props('icon') === icon) .wrappers[0]; - const createComponent = (runner, options) => { - wrapper = mountExtended(RunnerSummaryCell, { + const createComponent = ({ runner, mountFn = shallowMountExtended, ...options } = {}) => { + wrapper = mountFn(RunnerSummaryCell, { propsData: { runner: { ...mockRunner, @@ -40,7 +41,7 @@ describe('RunnerTypeCell', () => { }, }, stubs: { - RunnerSummaryField, + GlSprintf, }, ...options, }); @@ -51,6 +52,8 @@ describe('RunnerTypeCell', () => { }); it('Displays the runner name as id and short token', () => { + createComponent({ mountFn: mountExtended }); + expect(wrapper.text()).toContain( `#${getIdFromGraphQLId(mockRunner.id)} (${mockRunner.shortSha})`, ); @@ -58,13 +61,16 @@ describe('RunnerTypeCell', () => { it('Displays no runner manager count', () => { createComponent({ - managers: { count: 0 }, + runner: { managers: { nodes: { count: 0 } } }, + mountFn: mountExtended, }); expect(findRunnerManagersBadge().html()).toBe(''); }); it('Displays runner manager count', () => { + createComponent({ mountFn: mountExtended }); + expect(findRunnerManagersBadge().text()).toBe('2'); }); @@ -74,8 +80,8 @@ describe('RunnerTypeCell', () => { it('Displays the locked icon for locked runners', () => { createComponent({ - runnerType: PROJECT_TYPE, - locked: true, + runner: { runnerType: PROJECT_TYPE, locked: true }, + mountFn: mountExtended, }); expect(findLockIcon().exists()).toBe(true); @@ -83,8 +89,8 @@ describe('RunnerTypeCell', () => { it('Displays the runner type', () => { createComponent({ - runnerType: INSTANCE_TYPE, - locked: true, + runner: { runnerType: INSTANCE_TYPE, locked: true }, + mountFn: mountExtended, }); expect(wrapper.text()).toContain(I18N_INSTANCE_TYPE); @@ -101,7 +107,7 @@ describe('RunnerTypeCell', () => { it('Displays "No description" for missing runner description', () => { createComponent({ - description: null, + runner: { description: null }, }); expect(wrapper.findByText(I18N_NO_DESCRIPTION).classes()).toContain('gl-text-secondary'); @@ -109,7 +115,7 @@ describe('RunnerTypeCell', () => { it('Displays last contact', () => { createComponent({ - contactedAt: '2022-01-02', + runner: { contactedAt: '2022-01-02' }, }); expect(findRunnerSummaryField('clock').findComponent(TimeAgo).props('time')).toBe('2022-01-02'); @@ -124,20 +130,46 @@ describe('RunnerTypeCell', () => { expect(findRunnerSummaryField('clock').text()).toContain(__('Never')); }); - it('Displays ip address', () => { - createComponent({ - ipAddress: '127.0.0.1', + describe('IP address', () => { + it('with no managers', () => { + createComponent({ + runner: { + managers: { count: 0, nodes: [] }, + }, + }); + + expect(findRunnerSummaryField('disk')).toBeUndefined(); }); - expect(findRunnerSummaryField('disk').text()).toContain('127.0.0.1'); - }); + it('with no ip', () => { + createComponent({ + runner: { + managers: { count: 1, nodes: [{ ipAddress: null }] }, + }, + }); - it('Displays no ip address', () => { - createComponent({ - ipAddress: null, + expect(findRunnerSummaryField('disk')).toBeUndefined(); }); - expect(findRunnerSummaryField('disk')).toBeUndefined(); + it.each` + count | ipAddress | expected + ${1} | ${'127.0.0.1'} | ${'127.0.0.1'} + ${2} | ${'127.0.0.2'} | ${'127.0.0.2 (+1)'} + ${11} | ${'127.0.0.3'} | ${'127.0.0.3 (+10)'} + ${1001} | ${'127.0.0.4'} | ${'127.0.0.4 (+1,000)'} + `( + 'with $count managers, ip $ipAddress displays $expected', + ({ count, ipAddress, expected }) => { + createComponent({ + runner: { + // `first: 1` is requested, `count` varies when there are more managers + managers: { count, nodes: [{ ipAddress }] }, + }, + }); + + expect(findRunnerSummaryField('disk').text()).toMatchInterpolatedText(expected); + }, + ); }); it('Displays job count', () => { @@ -146,7 +178,7 @@ describe('RunnerTypeCell', () => { it('Formats large job counts', () => { createComponent({ - jobCount: 1000, + runner: { jobCount: 1000 }, }); expect(findRunnerSummaryField('pipeline').text()).toContain('1,000'); @@ -154,7 +186,7 @@ describe('RunnerTypeCell', () => { it('Formats large job counts with a plus symbol', () => { createComponent({ - jobCount: 1001, + runner: { jobCount: 1001 }, }); expect(findRunnerSummaryField('pipeline').text()).toContain('1,000+'); @@ -165,7 +197,7 @@ describe('RunnerTypeCell', () => { it('Displays created at ...', () => { createComponent({ - createdBy: null, + runner: { createdBy: null }, }); expect(findRunnerSummaryField('calendar').text()).toMatchInterpolatedText( @@ -177,12 +209,15 @@ describe('RunnerTypeCell', () => { }); it('Displays created at ... by ...', () => { + createComponent({ mountFn: mountExtended }); + expect(findRunnerSummaryField('calendar').text()).toMatchInterpolatedText( sprintf(I18N_CREATED_AT_BY_LABEL, { timeAgo: findCreatedTime().text(), avatar: mockRunner.createdBy.username, }), ); + expect(findCreatedTime().props('time')).toBe(mockRunner.createdAt); }); @@ -200,7 +235,7 @@ describe('RunnerTypeCell', () => { it('Displays tag list', () => { createComponent({ - tagList: ['shell', 'linux'], + runner: { tagList: ['shell', 'linux'] }, }); expect(findRunnerTags().props('tagList')).toEqual(['shell', 'linux']); @@ -209,14 +244,11 @@ describe('RunnerTypeCell', () => { it('Displays a custom runner-name slot', () => { const slotContent = 'My custom runner name'; - createComponent( - {}, - { - slots: { - 'runner-name': slotContent, - }, + createComponent({ + slots: { + 'runner-name': slotContent, }, - ); + }); expect(wrapper.text()).toContain(slotContent); }); diff --git a/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js b/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js index e564cf49ca0..e4373d1c198 100644 --- a/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js +++ b/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js @@ -1,4 +1,10 @@ -import { GlModal, GlDropdown, GlDropdownItem, GlDropdownForm, GlIcon } from '@gitlab/ui'; +import { + GlModal, + GlDisclosureDropdown, + GlDisclosureDropdownItem, + GlDropdownForm, + GlIcon, +} from '@gitlab/ui'; import { createWrapper } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; @@ -35,13 +41,16 @@ Vue.use(VueApollo); describe('RegistrationDropdown', () => { let wrapper; - const findDropdown = () => wrapper.findComponent(GlDropdown); + const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown); const findDropdownBtn = () => findDropdown().find('button'); - const findRegistrationInstructionsDropdownItem = () => wrapper.findComponent(GlDropdownItem); + const findRegistrationInstructionsDropdownItem = () => + wrapper.findComponent(GlDisclosureDropdownItem); const findTokenDropdownItem = () => wrapper.findComponent(GlDropdownForm); const findRegistrationToken = () => wrapper.findComponent(RegistrationToken); const findRegistrationTokenInput = () => - wrapper.findByLabelText(RegistrationToken.i18n.registrationToken); + wrapper.findByLabelText( + `${RegistrationToken.i18n.registrationToken} ${RegistrationDropdown.i18n.supportForRegistrationTokensDeprecated}`, + ); const findTokenResetDropdownItem = () => wrapper.findComponent(RegistrationTokenResetDropdownItem); const findModal = () => wrapper.findComponent(GlModal); @@ -52,9 +61,8 @@ describe('RegistrationDropdown', () => { .replace(/[\n\t\s]+/g, ' '); const openModal = async () => { - await findRegistrationInstructionsDropdownItem().trigger('click'); + await findRegistrationInstructionsDropdownItem().vm.$emit('action'); findModal().vm.$emit('shown'); - await waitForPromises(); }; @@ -65,6 +73,9 @@ describe('RegistrationDropdown', () => { type: INSTANCE_TYPE, ...props, }, + stubs: { + GlDisclosureDropdownItem, + }, ...options, }); }; @@ -107,12 +118,12 @@ describe('RegistrationDropdown', () => { createComponent(); expect(findDropdown().props()).toMatchObject({ - category: 'primary', - variant: 'confirm', + category: 'tertiary', + variant: 'default', }); expect(findDropdown().attributes()).toMatchObject({ - toggleclass: '', + toggleclass: 'gl-px-3!', }); }); @@ -186,6 +197,26 @@ describe('RegistrationDropdown', () => { }); }); + describe('Dropdown is expanded', () => { + beforeEach(() => { + createComponent({}, mountExtended); + findDropdownBtn().vm.$emit('click'); + }); + + it('has aria-expanded set to true', () => { + expect(findDropdownBtn().attributes('aria-expanded')).toBe('true'); + }); + + describe('when token is copied', () => { + it('should close dropdown', async () => { + findRegistrationToken().vm.$emit('copy'); + await nextTick(); + + expect(findDropdownBtn().attributes('aria-expanded')).toBeUndefined(); + }); + }); + }); + describe('When token is reset', () => { const newToken = 'mock1'; @@ -217,19 +248,15 @@ describe('RegistrationDropdown', () => { }); }); - describe.each([ - { createRunnerWorkflowForAdmin: true }, - { createRunnerWorkflowForNamespace: true }, - ])('When showing a "deprecated" warning', (glFeatures) => { + describe('When showing a "deprecated" warning', () => { it('passes deprecated variant props and attributes to dropdown', () => { - createComponent({ - provide: { glFeatures }, - }); + createComponent(); expect(findDropdown().props()).toMatchObject({ category: 'tertiary', variant: 'default', - text: '', + toggleText: I18N_REGISTER_INSTANCE_TYPE, + textSrOnly: true, }); expect(findDropdown().attributes()).toMatchObject({ @@ -249,12 +276,7 @@ describe('RegistrationDropdown', () => { }); it('shows warning text', () => { - createComponent( - { - provide: { glFeatures }, - }, - mountExtended, - ); + createComponent({}, mountExtended); const text = wrapper.findByText(s__('Runners|Support for registration tokens is deprecated')); @@ -262,12 +284,7 @@ describe('RegistrationDropdown', () => { }); it('button shows ellipsis icon', () => { - createComponent( - { - provide: { glFeatures }, - }, - mountExtended, - ); + createComponent({}, mountExtended); expect(findDropdownBtn().findComponent(GlIcon).props('name')).toBe('ellipsis_v'); expect(findDropdownBtn().findAllComponents(GlIcon)).toHaveLength(1); diff --git a/spec/frontend/ci/runner/components/registration/registration_token_reset_dropdown_item_spec.js b/spec/frontend/ci/runner/components/registration/registration_token_reset_dropdown_item_spec.js index db54bf0c80e..d599bc1291c 100644 --- a/spec/frontend/ci/runner/components/registration/registration_token_reset_dropdown_item_spec.js +++ b/spec/frontend/ci/runner/components/registration/registration_token_reset_dropdown_item_spec.js @@ -1,4 +1,4 @@ -import { GlDropdownItem, GlLoadingIcon, GlToast, GlModal } from '@gitlab/ui'; +import { GlDisclosureDropdownItem, GlLoadingIcon, GlToast, GlModal } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; @@ -27,7 +27,7 @@ describe('RegistrationTokenResetDropdownItem', () => { let showToast; const mockEvent = { preventDefault: jest.fn() }; - const findDropdownItem = () => wrapper.findComponent(GlDropdownItem); + const findDropdownItem = () => wrapper.findComponent(GlDisclosureDropdownItem); const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findModal = () => wrapper.findComponent(GlModal); const clickSubmit = () => findModal().vm.$emit('primary', mockEvent); diff --git a/spec/frontend/ci/runner/components/registration/registration_token_spec.js b/spec/frontend/ci/runner/components/registration/registration_token_spec.js index 869c032c0b5..fd3896d5500 100644 --- a/spec/frontend/ci/runner/components/registration/registration_token_spec.js +++ b/spec/frontend/ci/runner/components/registration/registration_token_spec.js @@ -7,7 +7,7 @@ import { mockRegistrationToken } from '../../mock_data'; describe('RegistrationToken', () => { let wrapper; - let showToast; + const showToastMock = jest.fn(); Vue.use(GlToast); @@ -21,9 +21,12 @@ describe('RegistrationToken', () => { ...props, }, ...options, + mocks: { + $toast: { + show: showToastMock, + }, + }, }); - - showToast = wrapper.vm.$toast ? jest.spyOn(wrapper.vm.$toast, 'show') : null; }; it('Displays value and copy button', () => { @@ -58,8 +61,14 @@ describe('RegistrationToken', () => { it('shows a copied message', () => { findInputCopyToggleVisibility().vm.$emit('copy'); - expect(showToast).toHaveBeenCalledTimes(1); - expect(showToast).toHaveBeenCalledWith('Registration token copied!'); + expect(showToastMock).toHaveBeenCalledTimes(1); + expect(showToastMock).toHaveBeenCalledWith('Registration token copied!'); + }); + + it('emits a copy event', () => { + findInputCopyToggleVisibility().vm.$emit('copy'); + + expect(wrapper.emitted('copy')).toHaveLength(1); }); }); @@ -76,9 +85,7 @@ describe('RegistrationToken', () => { }); it('passes slots to the input component', () => { - const slot = findInputCopyToggleVisibility().vm.$scopedSlots[slotName]; - - expect(slot()[0].text).toBe(slotContent); + expect(findInputCopyToggleVisibility().text()).toBe(slotContent); }); }); }); diff --git a/spec/frontend/ci/runner/components/runner_delete_action_spec.js b/spec/frontend/ci/runner/components/runner_delete_action_spec.js new file mode 100644 index 00000000000..d6617e6e75c --- /dev/null +++ b/spec/frontend/ci/runner/components/runner_delete_action_spec.js @@ -0,0 +1,223 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import { stubComponent } from 'helpers/stub_component'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import runnerDeleteMutation from '~/ci/runner/graphql/shared/runner_delete.mutation.graphql'; +import waitForPromises from 'helpers/wait_for_promises'; +import { captureException } from '~/ci/runner/sentry_utils'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import { createAlert } from '~/alert'; + +import RunnerDeleteAction from '~/ci/runner/components/runner_delete_action.vue'; +import RunnerDeleteModal from '~/ci/runner/components/runner_delete_modal.vue'; +import { allRunnersData } from '../mock_data'; + +const mockRunner = allRunnersData.data.runners.nodes[0]; +const mockRunnerId = getIdFromGraphQLId(mockRunner.id); +const mockRunnerName = `#${mockRunnerId} (${mockRunner.shortSha})`; + +Vue.use(VueApollo); + +jest.mock('~/alert'); +jest.mock('~/ci/runner/sentry_utils'); + +describe('RunnerDeleteAction', () => { + let wrapper; + let apolloProvider; + let apolloCache; + let runnerDeleteHandler; + let mockModalShow; + + const findBtn = () => wrapper.find('button'); + const findModal = () => wrapper.findComponent(RunnerDeleteModal); + + const createComponent = ({ props = {} } = {}) => { + const { runner, ...propsData } = props; + + wrapper = shallowMountExtended(RunnerDeleteAction, { + propsData: { + runner: { + // We need typename so that cache.identify works + // eslint-disable-next-line no-underscore-dangle + __typename: mockRunner.__typename, + id: mockRunner.id, + shortSha: mockRunner.shortSha, + ...runner, + }, + ...propsData, + }, + apolloProvider, + stubs: { + RunnerDeleteModal: stubComponent(RunnerDeleteModal, { + methods: { + show: mockModalShow, + }, + }), + }, + scopedSlots: { + default: '